feat(kon): wire Tauri shell — thin commands, ProviderRegistry, tray

- lib.rs rewritten from scaffold to full app setup (~70 lines)
- AppState holds Arc<LocalEngine> for Whisper and Parakeet
- commands/models.rs: download, check, list, load for both engines
  Maps legacy size strings (Tiny/Base/Small/Medium) to ModelId
- commands/transcription.rs: transcribe_pcm, transcribe_file, transcribe_pcm_parakeet
  Delegates to LocalEngine.transcribe_sync() + post_process_segments()
- commands/audio.rs: save_audio via kon_audio::write_wav
- commands/windows.rs: open_task_window, open_viewer_window
- commands/llm.rs: 8 stub commands preserved for frontend compatibility
- tray.rs: extracted system tray setup (show, status, quit, click-to-show)
- Close-to-tray behaviour preserved
- All 25 v0.2 command names preserved for Svelte frontend compatibility
- clippy clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-16 21:15:16 +00:00
parent 0646c75de7
commit 29ff91d3f6
9 changed files with 638 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
use tauri::Emitter;
use crate::AppState;
use kon_core::types::ModelId;
use kon_transcription::model_manager;
/// Map legacy size strings to ModelId.
fn whisper_model_id(size: &str) -> ModelId {
match size.to_lowercase().as_str() {
"tiny" => ModelId::new("whisper-tiny-en"),
"base" => ModelId::new("whisper-base-en"),
"small" => ModelId::new("whisper-small-en"),
"medium" => ModelId::new("whisper-medium-en"),
other => ModelId::new(other),
}
}
fn parakeet_model_id(name: &str) -> ModelId {
match name {
"ctc-int8" => ModelId::new("parakeet-ctc-0.6b-int8"),
other => ModelId::new(other),
}
}
// --- Whisper model commands ---
#[tauri::command]
pub async fn download_model(
app: tauri::AppHandle,
size: String,
) -> Result<String, String> {
let id = whisper_model_id(&size);
let app_clone = app.clone();
model_manager::download(&id, move |progress| {
let _ = app_clone.emit("model-download-progress", &progress);
})
.await
.map_err(|e| e.to_string())?;
Ok(format!("Model {} downloaded", size))
}
#[tauri::command]
pub fn check_model(size: String) -> Result<bool, String> {
let id = whisper_model_id(&size);
Ok(model_manager::is_downloaded(&id))
}
#[tauri::command]
pub fn list_models() -> Result<Vec<String>, String> {
let ids = model_manager::list_downloaded();
Ok(ids
.into_iter()
.filter(|id| id.as_str().starts_with("whisper-"))
.map(|id| {
match id.as_str() {
"whisper-tiny-en" => "Tiny".to_string(),
"whisper-base-en" => "Base".to_string(),
"whisper-small-en" => "Small".to_string(),
"whisper-medium-en" => "Medium".to_string(),
other => other.to_string(),
}
})
.collect())
}
#[tauri::command]
pub async fn load_model(
state: tauri::State<'_, AppState>,
size: String,
) -> Result<String, String> {
let id = whisper_model_id(&size);
let dir = model_manager::model_dir(&id);
let model_file = dir.join(
kon_core::model_registry::find_model(&id)
.ok_or_else(|| format!("Unknown model: {size}"))?
.files
.first()
.ok_or_else(|| format!("No files for model: {size}"))?
.filename,
);
if !model_file.exists() {
return Err(format!("Model not downloaded: {size}"));
}
let engine = state.whisper_engine.clone();
tokio::task::spawn_blocking(move || {
let model = kon_transcription::load_whisper(&model_file)
.map_err(|e| e.to_string())?;
engine.load(model);
Ok::<_, String>(())
})
.await
.map_err(|e| e.to_string())??;
Ok(format!("Model {} loaded", size))
}
#[tauri::command]
pub fn check_engine(state: tauri::State<AppState>) -> Result<bool, String> {
Ok(state.whisper_engine.is_loaded())
}
// --- Parakeet model commands ---
#[tauri::command]
pub async fn download_parakeet_model(
app: tauri::AppHandle,
name: String,
) -> Result<String, String> {
let id = parakeet_model_id(&name);
let app_clone = app.clone();
model_manager::download(&id, move |progress| {
let _ = app_clone.emit("parakeet-download-progress", &progress);
})
.await
.map_err(|e| e.to_string())?;
Ok(format!("Parakeet model {} downloaded", name))
}
#[tauri::command]
pub fn check_parakeet_model(name: String) -> Result<bool, String> {
let id = parakeet_model_id(&name);
Ok(model_manager::is_downloaded(&id))
}
#[tauri::command]
pub fn list_parakeet_models() -> Result<Vec<String>, String> {
let ids = model_manager::list_downloaded();
Ok(ids
.into_iter()
.filter(|id| id.as_str().starts_with("parakeet-"))
.map(|id| {
match id.as_str() {
"parakeet-ctc-0.6b-int8" => "ctc-int8".to_string(),
other => other.to_string(),
}
})
.collect())
}
#[tauri::command]
pub async fn load_parakeet_model(
state: tauri::State<'_, AppState>,
name: String,
) -> Result<String, String> {
let id = parakeet_model_id(&name);
let dir = model_manager::model_dir(&id);
if !dir.exists() {
return Err(format!("Parakeet model not downloaded: {name}"));
}
let engine = state.parakeet_engine.clone();
tokio::task::spawn_blocking(move || {
let model = kon_transcription::load_parakeet(&dir)
.map_err(|e| e.to_string())?;
engine.load(model);
Ok::<_, String>(())
})
.await
.map_err(|e| e.to_string())??;
Ok(format!("Parakeet model {} loaded", name))
}
#[tauri::command]
pub fn check_parakeet_engine(
state: tauri::State<AppState>,
) -> Result<bool, String> {
Ok(state.parakeet_engine.is_loaded())
}