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:
49
src-tauri/src/commands/audio.rs
Normal file
49
src-tauri/src/commands/audio.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
use kon_core::types::AudioSamples;
|
||||
|
||||
/// Save PCM f32 samples as a WAV file. Returns the file path.
|
||||
#[tauri::command]
|
||||
pub async fn save_audio(
|
||||
app: tauri::AppHandle,
|
||||
samples: Vec<f32>,
|
||||
output_folder: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let recordings_dir = if let Some(ref folder) = output_folder {
|
||||
if !folder.is_empty() {
|
||||
PathBuf::from(folder)
|
||||
} else {
|
||||
app.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|e: tauri::Error| e.to_string())?
|
||||
.join("recordings")
|
||||
}
|
||||
} else {
|
||||
app.path()
|
||||
.app_local_data_dir()
|
||||
.map_err(|e: tauri::Error| e.to_string())?
|
||||
.join("recordings")
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&recordings_dir)
|
||||
.map_err(|e| format!("Failed to create recordings dir: {e}"))?;
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let filename = format!("kon-{timestamp}.wav");
|
||||
let path = recordings_dir.join(&filename);
|
||||
let path_clone = path.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let audio = AudioSamples::mono_16khz(samples);
|
||||
kon_audio::write_wav(&path_clone, &audio).map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
53
src-tauri/src/commands/llm.rs
Normal file
53
src-tauri/src/commands/llm.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// LLM stubs — ONNX-based local LLM coming post-revenue.
|
||||
|
||||
#[tauri::command]
|
||||
pub fn check_llm_engine() -> Result<bool, String> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn check_llm_model(_size: String) -> Result<bool, String> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_llm_models() -> Result<Vec<String>, String> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_llm_model(
|
||||
_app: tauri::AppHandle,
|
||||
_size: String,
|
||||
) -> Result<String, String> {
|
||||
Err("LLM not available yet — coming soon".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_llm_model(
|
||||
_size: String,
|
||||
) -> Result<String, String> {
|
||||
Err("LLM not available yet — coming soon".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn llm_generate(
|
||||
_prompt: String,
|
||||
_max_tokens: Option<u32>,
|
||||
) -> Result<String, String> {
|
||||
Err("LLM not available yet — coming soon".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn llm_extract_tasks(
|
||||
_transcript: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
Err("LLM not available yet — coming soon".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn llm_format_transcript(
|
||||
_transcript: String,
|
||||
) -> Result<String, String> {
|
||||
Err("LLM not available yet — coming soon".into())
|
||||
}
|
||||
5
src-tauri/src/commands/mod.rs
Normal file
5
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod audio;
|
||||
pub mod llm;
|
||||
pub mod models;
|
||||
pub mod transcription;
|
||||
pub mod windows;
|
||||
172
src-tauri/src/commands/models.rs
Normal file
172
src-tauri/src/commands/models.rs
Normal 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())
|
||||
}
|
||||
165
src-tauri/src/commands/transcription.rs
Normal file
165
src-tauri/src/commands/transcription.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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 tauri::Emitter;
|
||||
|
||||
use crate::AppState;
|
||||
use kon_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions};
|
||||
use kon_core::types::{Segment, TranscriptionOptions};
|
||||
|
||||
/// 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 result = tokio::task::spawn_blocking(move || {
|
||||
let transcript = engine.transcribe_sync(&audio, &options)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>(transcript)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
let mut segments: Vec<Segment> = result.segments().to_vec();
|
||||
post_process_segments(
|
||||
&mut segments,
|
||||
&PostProcessOptions {
|
||||
remove_fillers,
|
||||
british_english,
|
||||
anti_hallucination,
|
||||
format_mode: FormatMode::parse(&format_mode),
|
||||
},
|
||||
);
|
||||
|
||||
app.emit(
|
||||
"transcription-result",
|
||||
serde_json::json!({
|
||||
"status": "transcription",
|
||||
"segments": segments,
|
||||
"language": result.language(),
|
||||
"duration": result.duration(),
|
||||
"chunk_id": chunk_id,
|
||||
}),
|
||||
)
|
||||
.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,
|
||||
language: String,
|
||||
initial_prompt: String,
|
||||
remove_fillers: bool,
|
||||
british_english: bool,
|
||||
anti_hallucination: bool,
|
||||
format_mode: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let engine = state.whisper_engine.clone();
|
||||
let options = TranscriptionOptions {
|
||||
language: Some(language),
|
||||
initial_prompt: Some(initial_prompt),
|
||||
};
|
||||
|
||||
let result = 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())?;
|
||||
let transcript = engine
|
||||
.transcribe_sync(&resampled, &options)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>(transcript)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
let mut segments: Vec<Segment> = result.segments().to_vec();
|
||||
post_process_segments(
|
||||
&mut segments,
|
||||
&PostProcessOptions {
|
||||
remove_fillers,
|
||||
british_english,
|
||||
anti_hallucination,
|
||||
format_mode: FormatMode::parse(&format_mode),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"segments": segments,
|
||||
"language": result.language(),
|
||||
"duration": result.duration(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) -> Result<(), String> {
|
||||
let engine = state.parakeet_engine.clone();
|
||||
let audio = kon_core::AudioSamples::mono_16khz(samples);
|
||||
let options = TranscriptionOptions::default();
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let transcript = engine
|
||||
.transcribe_sync(&audio, &options)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>(transcript)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
let mut segments: Vec<Segment> = result.segments().to_vec();
|
||||
post_process_segments(
|
||||
&mut segments,
|
||||
&PostProcessOptions {
|
||||
remove_fillers,
|
||||
british_english,
|
||||
anti_hallucination,
|
||||
format_mode: FormatMode::parse("Clean"),
|
||||
},
|
||||
);
|
||||
|
||||
app.emit(
|
||||
"transcription-result",
|
||||
serde_json::json!({
|
||||
"status": "transcription",
|
||||
"segments": segments,
|
||||
"language": result.language(),
|
||||
"duration": result.duration(),
|
||||
"chunk_id": chunk_id,
|
||||
}),
|
||||
)
|
||||
.map_err(|e| format!("Failed to emit result: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
58
src-tauri/src/commands/windows.rs
Normal file
58
src-tauri/src/commands/windows.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
/// Open a floating always-on-top task window.
|
||||
#[tauri::command]
|
||||
pub async fn open_task_window(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("tasks-float") {
|
||||
window.show().map_err(|e| e.to_string())?;
|
||||
window.set_focus().map_err(|e| e.to_string())?;
|
||||
app.emit("task-window-focus", ())
|
||||
.map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"tasks-float",
|
||||
WebviewUrl::App("/float".into()),
|
||||
)
|
||||
.title("Kon Tasks")
|
||||
.inner_size(480.0, 520.0)
|
||||
.min_inner_size(400.0, 400.0)
|
||||
.always_on_top(true)
|
||||
.decorations(false)
|
||||
.resizable(true)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open the transcript viewer window.
|
||||
#[tauri::command]
|
||||
pub async fn open_viewer_window(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
if let Some(window) = app.get_webview_window("transcript-viewer") {
|
||||
window.show().map_err(|e| e.to_string())?;
|
||||
window.set_focus().map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"transcript-viewer",
|
||||
WebviewUrl::App("/viewer".into()),
|
||||
)
|
||||
.title("Kon - Viewer")
|
||||
.inner_size(600.0, 700.0)
|
||||
.min_inner_size(450.0, 500.0)
|
||||
.decorations(false)
|
||||
.resizable(true)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user