diff --git a/src-tauri/src/commands/live.rs b/src-tauri/src/commands/live.rs index 47cc720..80f9081 100644 --- a/src-tauri/src/commands/live.rs +++ b/src-tauri/src/commands/live.rs @@ -61,6 +61,10 @@ pub struct StartLiveTranscriptionConfig { /// Optional explicit microphone device name (from `list_audio_devices`). /// None or empty string = let `MicrophoneCapture::start` auto-select. pub microphone_device: Option, + /// Optional profile id. None falls back to `DEFAULT_PROFILE_ID`. Drives + /// the post-processing dictionary via `profile_terms` and, when the + /// request's `initial_prompt` is empty, supplies the Whisper prompt. + pub profile_id: Option, } #[derive(Debug, Serialize)] @@ -131,7 +135,7 @@ struct InferenceTask { pub async fn start_live_transcription_session( state: tauri::State<'_, AppState>, live_state: tauri::State<'_, LiveTranscriptionState>, - config: StartLiveTranscriptionConfig, + mut config: StartLiveTranscriptionConfig, result_channel: Channel, status_channel: Channel, ) -> Result { @@ -142,6 +146,37 @@ pub async fn start_live_transcription_session( } } + let resolved_profile_id = config + .profile_id + .clone() + .unwrap_or_else(|| kon_storage::DEFAULT_PROFILE_ID.to_string()); + + let profile = kon_storage::database::get_profile(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; + + let profile_terms: Vec = + kon_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .into_iter() + .map(|t| t.term) + .collect(); + + // Collapse the effective initial_prompt on the struct so downstream + // `TranscriptionOptions` construction (see `maybe_dispatch_chunk`) picks + // up profile fallback without further plumbing. + let effective_prompt = match config.initial_prompt.as_deref() { + Some(p) if !p.is_empty() => p.to_string(), + _ => profile.initial_prompt.clone(), + }; + config.initial_prompt = if effective_prompt.is_empty() { + None + } else { + Some(effective_prompt) + }; + let model_id = config .model_id .clone() @@ -166,13 +201,7 @@ pub async fn start_live_transcription_session( let worker_status = status_channel.clone(); let worker_results = result_channel.clone(); - let dictionary_terms: Vec = - kon_storage::database::list_dictionary(&state.db) - .await - .unwrap_or_default() - .into_iter() - .map(|e| e.term) - .collect(); + let dictionary_terms = profile_terms.clone(); let handle = tokio::task::spawn_blocking(move || { run_live_session( diff --git a/src-tauri/src/commands/transcription.rs b/src-tauri/src/commands/transcription.rs index 458b51b..4c5aaa7 100644 --- a/src-tauri/src/commands/transcription.rs +++ b/src-tauri/src/commands/transcription.rs @@ -36,12 +36,39 @@ pub async fn transcribe_pcm( british_english: bool, anti_hallucination: bool, format_mode: String, + profile_id: Option, ) -> Result<(), String> { + let resolved_profile_id = profile_id + .unwrap_or_else(|| kon_storage::DEFAULT_PROFILE_ID.to_string()); + + let profile = kon_storage::database::get_profile(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; + + let profile_terms: Vec = + kon_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .into_iter() + .map(|t| t.term) + .collect(); + + let effective_prompt = if !initial_prompt.is_empty() { + initial_prompt + } else { + profile.initial_prompt.clone() + }; + let engine = state.whisper_engine.clone(); let audio = kon_core::AudioSamples::mono_16khz(samples); let options = TranscriptionOptions { language: Some(language), - initial_prompt: Some(initial_prompt), + initial_prompt: if effective_prompt.is_empty() { + None + } else { + Some(effective_prompt) + }, }; let timed = tokio::task::spawn_blocking(move || { @@ -50,12 +77,7 @@ pub async fn transcribe_pcm( .await .map_err(|e| e.to_string())??; - let dictionary_terms: Vec = kon_storage::database::list_dictionary(&state.db) - .await - .unwrap_or_default() - .into_iter() - .map(|e| e.term) - .collect(); + let dictionary_terms = profile_terms.clone(); let mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments( @@ -98,7 +120,30 @@ pub async fn transcribe_file( british_english: bool, anti_hallucination: bool, format_mode: String, + profile_id: Option, ) -> Result { + let resolved_profile_id = profile_id + .unwrap_or_else(|| kon_storage::DEFAULT_PROFILE_ID.to_string()); + + let profile = kon_storage::database::get_profile(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; + + let profile_terms: Vec = + kon_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .into_iter() + .map(|t| t.term) + .collect(); + + let effective_prompt = if !initial_prompt.is_empty() { + initial_prompt + } else { + profile.initial_prompt.clone() + }; + let engine_name = engine.unwrap_or_else(|| "whisper".to_string()); let model_id = model_id .unwrap_or_else(|| default_model_id_for_engine(&engine_name).to_string()); @@ -107,7 +152,11 @@ pub async fn transcribe_file( let engine = pick_engine(&state, &engine_name)?; let options = TranscriptionOptions { language: Some(language), - initial_prompt: Some(initial_prompt), + initial_prompt: if effective_prompt.is_empty() { + None + } else { + Some(effective_prompt) + }, }; let timed = tokio::task::spawn_blocking(move || { @@ -122,12 +171,7 @@ pub async fn transcribe_file( .await .map_err(|e| e.to_string())??; - let dictionary_terms: Vec = kon_storage::database::list_dictionary(&state.db) - .await - .unwrap_or_default() - .into_iter() - .map(|e| e.term) - .collect(); + let dictionary_terms = profile_terms.clone(); let mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments( @@ -162,7 +206,26 @@ pub async fn transcribe_pcm_parakeet( british_english: bool, anti_hallucination: bool, format_mode: String, + profile_id: Option, ) -> Result<(), String> { + let resolved_profile_id = profile_id + .unwrap_or_else(|| kon_storage::DEFAULT_PROFILE_ID.to_string()); + + // Validate the profile exists so parakeet and whisper behave identically + // when a bogus id slips through from the frontend. + kon_storage::database::get_profile(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; + + let profile_terms: Vec = + kon_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + .await + .map_err(|e| e.to_string())? + .into_iter() + .map(|t| t.term) + .collect(); + let engine = state.parakeet_engine.clone(); let audio = kon_core::AudioSamples::mono_16khz(samples); let options = TranscriptionOptions::default(); @@ -173,12 +236,7 @@ pub async fn transcribe_pcm_parakeet( .await .map_err(|e| e.to_string())??; - let dictionary_terms: Vec = kon_storage::database::list_dictionary(&state.db) - .await - .unwrap_or_default() - .into_iter() - .map(|e| e.term) - .collect(); + let dictionary_terms = profile_terms.clone(); let mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments(