Files
Lumotia/src-tauri/src/commands/transcription.rs

210 lines
6.3 KiB
Rust

// 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<Arc<kon_transcription::LocalEngine>, 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<f32>,
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<String> = kon_storage::database::list_dictionary(&state.db)
.await
.unwrap_or_default()
.into_iter()
.map(|e| e.term)
.collect();
let mut segments: Vec<Segment> = 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<String>,
model_id: Option<String>,
language: String,
initial_prompt: String,
remove_fillers: bool,
british_english: bool,
anti_hallucination: bool,
format_mode: String,
) -> Result<serde_json::Value, String> {
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<String> = kon_storage::database::list_dictionary(&state.db)
.await
.unwrap_or_default()
.into_iter()
.map(|e| e.term)
.collect();
let mut segments: Vec<Segment> = 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<f32>,
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<String> = kon_storage::database::list_dictionary(&state.db)
.await
.unwrap_or_default()
.into_iter()
.map(|e| e.term)
.collect();
let mut segments: Vec<Segment> = 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(())
}