// Tauri command handlers must match the frontend's invoke() parameter lists, // so the argument counts are dictated by the Svelte code. #![allow(clippy::too_many_arguments)] use std::path::Path; use std::sync::Arc; use tauri::Emitter; use crate::commands::models::{default_model_id_for_engine, ensure_model_loaded}; use crate::AppState; use kon_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; use kon_core::types::{Segment, TranscriptionOptions}; fn pick_engine( state: &AppState, engine: &str, ) -> Result, String> { match engine { "whisper" => Ok(state.whisper_engine.clone()), "parakeet" => Ok(state.parakeet_engine.clone()), other => Err(format!("Unknown engine: {other}")), } } /// Transcribe raw PCM f32 samples (Whisper). Emits "transcription-result" event. #[tauri::command] pub async fn transcribe_pcm( state: tauri::State<'_, AppState>, app: tauri::AppHandle, samples: Vec, chunk_id: u32, language: String, initial_prompt: String, remove_fillers: bool, british_english: bool, anti_hallucination: bool, format_mode: String, ) -> Result<(), String> { 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), }; let timed = tokio::task::spawn_blocking(move || { engine.transcribe_sync(&audio, &options).map_err(|e| e.to_string()) }) .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 mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments( &mut segments, &PostProcessOptions { remove_fillers, british_english, anti_hallucination, format_mode: FormatMode::parse(&format_mode), dictionary_terms, }, ); app.emit( "transcription-result", serde_json::json!({ "status": "transcription", "segments": segments, "language": timed.transcript.language(), "duration": timed.transcript.duration(), "chunk_id": chunk_id, "inference_ms": timed.inference_ms, }), ) .map_err(|e| format!("Failed to emit result: {e}"))?; Ok(()) } /// Transcribe an audio file by path. Decodes, resamples to 16kHz, runs Whisper. #[tauri::command] pub async fn transcribe_file( state: tauri::State<'_, AppState>, path: String, engine: Option, model_id: Option, language: String, initial_prompt: String, remove_fillers: bool, british_english: bool, anti_hallucination: bool, format_mode: String, ) -> Result { 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()); ensure_model_loaded(&state, &engine_name, &model_id).await?; let engine = pick_engine(&state, &engine_name)?; let options = TranscriptionOptions { language: Some(language), initial_prompt: Some(initial_prompt), }; let timed = tokio::task::spawn_blocking(move || { let audio = kon_audio::decode_audio_file(Path::new(&path)) .map_err(|e| e.to_string())?; let resampled = kon_audio::resample_to_16khz(&audio).map_err(|e| e.to_string())?; engine .transcribe_sync(&resampled, &options) .map_err(|e| e.to_string()) }) .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 mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments( &mut segments, &PostProcessOptions { remove_fillers, british_english, anti_hallucination, format_mode: FormatMode::parse(&format_mode), dictionary_terms, }, ); Ok(serde_json::json!({ "engine": engine_name, "modelId": model_id, "segments": segments, "language": timed.transcript.language(), "duration": timed.transcript.duration(), "inference_ms": timed.inference_ms, })) } /// Transcribe raw PCM f32 samples (Parakeet). Emits "transcription-result" event. #[tauri::command] pub async fn transcribe_pcm_parakeet( state: tauri::State<'_, AppState>, app: tauri::AppHandle, samples: Vec, chunk_id: u32, remove_fillers: bool, british_english: bool, anti_hallucination: bool, format_mode: String, ) -> Result<(), String> { let engine = state.parakeet_engine.clone(); let audio = kon_core::AudioSamples::mono_16khz(samples); let options = TranscriptionOptions::default(); let timed = tokio::task::spawn_blocking(move || { engine.transcribe_sync(&audio, &options).map_err(|e| e.to_string()) }) .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 mut segments: Vec = timed.transcript.segments().to_vec(); post_process_segments( &mut segments, &PostProcessOptions { remove_fillers, british_english, anti_hallucination, format_mode: FormatMode::parse(&format_mode), dictionary_terms, }, ); app.emit( "transcription-result", serde_json::json!({ "status": "transcription", "segments": segments, "language": timed.transcript.language(), "duration": timed.transcript.duration(), "chunk_id": chunk_id, "inference_ms": timed.inference_ms, }), ) .map_err(|e| format!("Failed to emit result: {e}"))?; Ok(()) }