Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,19 +1455,41 @@ impl Default for NotificationConfig {
}
}

/// Post-processing command configuration
/// Post-processing configuration
///
/// Pipes transcribed text through an external command for cleanup/formatting.
/// Commonly used with local LLMs (Ollama, llama.cpp) or text processing tools.
/// Supports two modes:
/// 1. Shell command: pipes text through an external command (e.g., Ollama)
/// 2. Anthropic API: sends text to a Claude model for cleanup
///
/// If `anthropic_model` is set, uses the Anthropic API directly.
/// Otherwise, uses the shell `command`.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PostProcessConfig {
/// Shell command to execute
/// Shell command to execute (used when anthropic_model is not set)
/// Receives transcribed text on stdin, outputs processed text on stdout
pub command: String,
#[serde(default)]
pub command: Option<String>,

/// Timeout in milliseconds (default: 30000 = 30 seconds)
/// Timeout in milliseconds (default: 10000 for Anthropic, 30000 for command)
#[serde(default = "default_post_process_timeout")]
pub timeout_ms: u64,

/// Anthropic API key (or use ANTHROPIC_API_KEY env var, or anthropic_api_key_file)
#[serde(default)]
pub anthropic_api_key: Option<String>,

/// Path to file containing anthropic_api_key=... (e.g., ".env")
#[serde(default)]
pub anthropic_api_key_file: Option<String>,

/// Anthropic model to use (e.g., "claude-haiku-4-5-20251001")
/// Setting this enables Anthropic API mode
#[serde(default)]
pub anthropic_model: Option<String>,

/// Custom system prompt for Anthropic cleanup
#[serde(default)]
pub anthropic_prompt: Option<String>,
}

/// Named profile for context-specific settings
Expand Down
72 changes: 62 additions & 10 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::hotkey::{self, HotkeyEvent};
use crate::meeting::{self, MeetingDaemon, MeetingEvent, StorageConfig};
use crate::model_manager::ModelManager;
use crate::output;
use crate::output::anthropic::AnthropicPostProcessor;
use crate::output::post_process::PostProcessor;
use crate::state::{ChunkResult, State};
use crate::text::TextProcessor;
Expand Down Expand Up @@ -475,6 +476,7 @@ pub struct Daemon {
audio_feedback: Option<AudioFeedback>,
text_processor: TextProcessor,
post_processor: Option<PostProcessor>,
anthropic_processor: Option<AnthropicPostProcessor>,
// Model manager for multi-model support
model_manager: Option<ModelManager>,
// Background task for loading model on-demand
Expand Down Expand Up @@ -546,14 +548,49 @@ impl Daemon {
}

// Initialize post-processor if configured
let post_processor = config.output.post_process.as_ref().map(|cfg| {
tracing::info!(
"Post-processing enabled: command={:?}, timeout={}ms",
cfg.command,
cfg.timeout_ms
);
PostProcessor::new(cfg)
});
let mut post_processor = None;
let mut anthropic_processor = None;

if let Some(cfg) = config.output.post_process.as_ref() {
if cfg.anthropic_model.is_some() {
// Anthropic API mode
use crate::output::anthropic::{resolve_api_key, AnthropicPostProcessor};
let api_key = resolve_api_key(
cfg.anthropic_api_key.as_deref(),
cfg.anthropic_api_key_file.as_deref(),
);
if let Some(api_key) = api_key {
let model = cfg
.anthropic_model
.clone()
.unwrap_or_else(|| "claude-haiku-4-5-20251001".to_string());
tracing::info!(
"Post-processing enabled: Anthropic API (model={}), timeout={}ms",
model,
cfg.timeout_ms
);
anthropic_processor = Some(AnthropicPostProcessor::new(
api_key,
model,
cfg.anthropic_prompt.clone(),
cfg.timeout_ms,
));
} else {
tracing::error!(
"Anthropic post-processing configured but no API key found. \
Set anthropic_api_key, ANTHROPIC_API_KEY env var, or anthropic_api_key_file"
);
}
} else if let Some(ref command) = cfg.command {
// Shell command mode
tracing::info!(
"Post-processing enabled: command={:?}, timeout={}ms",
command,
cfg.timeout_ms
);
post_processor = Some(PostProcessor::new(cfg));
}
}

// Initialize Voice Activity Detection if enabled
let vad = match crate::vad::create_vad(&config) {
Expand Down Expand Up @@ -588,6 +625,7 @@ impl Daemon {
audio_feedback,
text_processor,
post_processor,
anthropic_processor,
model_manager: None,
model_load_task: None,
transcription_task: None,
Expand Down Expand Up @@ -1282,8 +1320,12 @@ impl Daemon {
if let Some(ref cmd) = profile.post_process_command {
let timeout_ms = profile.post_process_timeout_ms.unwrap_or(30000);
let profile_config = crate::config::PostProcessConfig {
command: cmd.clone(),
command: Some(cmd.clone()),
timeout_ms,
anthropic_api_key: None,
anthropic_api_key_file: None,
anthropic_model: None,
anthropic_prompt: None,
};
let profile_processor = PostProcessor::new(&profile_config);
tracing::info!(
Expand All @@ -1295,7 +1337,12 @@ impl Daemon {
result
} else {
// Profile exists but has no post_process_command, use default
if let Some(ref post_processor) = self.post_processor {
if let Some(ref anthropic_processor) = self.anthropic_processor {
tracing::info!("Post-processing via Anthropic: {:?}", processed_text);
let result = anthropic_processor.process(&processed_text).await;
tracing::info!("Post-processed: {:?}", result);
result
} else if let Some(ref post_processor) = self.post_processor {
tracing::info!("Post-processing: {:?}", processed_text);
let result = post_processor.process(&processed_text).await;
tracing::info!("Post-processed: {:?}", result);
Expand All @@ -1304,6 +1351,11 @@ impl Daemon {
processed_text
}
}
} else if let Some(ref anthropic_processor) = self.anthropic_processor {
tracing::info!("Post-processing via Anthropic: {:?}", processed_text);
let result = anthropic_processor.process(&processed_text).await;
tracing::info!("Post-processed: {:?}", result);
result
} else if let Some(ref post_processor) = self.post_processor {
tracing::info!("Post-processing: {:?}", processed_text);
let result = post_processor.process(&processed_text).await;
Expand Down
Loading