agent: foundation — import legacy codebase from Obsidian vault
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2
src-tauri/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[env]
|
||||
LIBCLANG_PATH = "C:\\Program Files\\LLVM\\bin"
|
||||
37
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "kon"
|
||||
version = "0.1.0"
|
||||
description = "Kon — Think out loud"
|
||||
authors = ["CORBEL Ltd"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "kon_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Workspace crates
|
||||
kon-core = { path = "../crates/core" }
|
||||
kon-audio = { path = "../crates/audio" }
|
||||
kon-transcription = { path = "../crates/transcription" }
|
||||
kon-ai-formatting = { path = "../crates/ai-formatting" }
|
||||
kon-storage = { path = "../crates/storage" }
|
||||
kon-cloud-providers = { path = "../crates/cloud-providers" }
|
||||
|
||||
# Tauri
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
# Serialisation
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Async runtime (spawn_blocking for inference)
|
||||
tokio = { version = "1", features = ["rt", "sync"] }
|
||||
arboard = "3.6.1"
|
||||
tauri-plugin-mcp = "0.7.1"
|
||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
22
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main", "tasks-float"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-focus",
|
||||
"opener:default",
|
||||
"dialog:default",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister"
|
||||
]
|
||||
}
|
||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","tasks-float"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-set-always-on-top","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-is-maximized","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-focus","opener:default","dialog:default","global-shortcut:allow-register","global-shortcut:allow-unregister"]}}
|
||||
2663
src-tauri/gen/schemas/desktop-schema.json
Normal file
2663
src-tauri/gen/schemas/windows-schema.json
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
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())
|
||||
}
|
||||
12
src-tauri/src/commands/clipboard.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use arboard::Clipboard;
|
||||
|
||||
/// Copy text to the system clipboard via arboard.
|
||||
#[tauri::command]
|
||||
pub fn copy_to_clipboard(text: String) -> Result<(), String> {
|
||||
let mut clipboard =
|
||||
Clipboard::new().map_err(|e| format!("Clipboard init failed: {e}"))?;
|
||||
clipboard
|
||||
.set_text(&text)
|
||||
.map_err(|e| format!("Clipboard write failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
69
src-tauri/src/commands/hardware.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use kon_core::hardware::{self, Os};
|
||||
use kon_core::recommendation;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SystemInfo {
|
||||
pub ram_mb: u64,
|
||||
pub cpu_brand: String,
|
||||
pub cpu_cores: usize,
|
||||
pub os: String,
|
||||
pub gpu: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelRecommendation {
|
||||
pub id: String,
|
||||
pub display_name: &'static str,
|
||||
pub disk_size_mb: u64,
|
||||
pub ram_required_mb: u64,
|
||||
pub description: &'static str,
|
||||
pub score: f64,
|
||||
pub reason: String,
|
||||
pub is_downloaded: bool,
|
||||
}
|
||||
|
||||
/// Probe system hardware and return a summary.
|
||||
#[tauri::command]
|
||||
pub fn probe_system() -> Result<SystemInfo, String> {
|
||||
let profile = hardware::probe_system();
|
||||
Ok(SystemInfo {
|
||||
ram_mb: profile.ram.0,
|
||||
cpu_brand: profile.cpu.brand,
|
||||
cpu_cores: profile.cpu.logical_processors,
|
||||
os: match profile.os {
|
||||
Os::Windows => "Windows",
|
||||
Os::Linux => "Linux",
|
||||
Os::MacOs => "macOS",
|
||||
Os::Ios => "iOS",
|
||||
Os::Android => "Android",
|
||||
}
|
||||
.to_string(),
|
||||
gpu: profile.gpu.map(|g| format!("{:?}", g.vendor)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Rank models for the current system and return recommendations.
|
||||
#[tauri::command]
|
||||
pub fn rank_models() -> Result<Vec<ModelRecommendation>, String> {
|
||||
let profile = hardware::probe_system();
|
||||
let ranked = recommendation::rank_recommendations(&profile);
|
||||
|
||||
Ok(ranked
|
||||
.into_iter()
|
||||
.map(|scored| {
|
||||
let downloaded = kon_transcription::is_downloaded(&scored.entry.id);
|
||||
ModelRecommendation {
|
||||
id: scored.entry.id.as_str().to_string(),
|
||||
display_name: scored.entry.display_name,
|
||||
disk_size_mb: scored.entry.disk_size.0,
|
||||
ram_required_mb: scored.entry.ram_required.0,
|
||||
description: scored.entry.description,
|
||||
score: scored.score,
|
||||
reason: scored.reason,
|
||||
is_downloaded: downloaded,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
6
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod audio;
|
||||
pub mod clipboard;
|
||||
pub mod hardware;
|
||||
pub mod models;
|
||||
pub mod transcription;
|
||||
pub mod windows;
|
||||
174
src-tauri/src/commands/models.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
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();
|
||||
let mid = id.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let model = kon_transcription::load_whisper(&model_file)
|
||||
.map_err(|e| e.to_string())?;
|
||||
engine.load(model, mid);
|
||||
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();
|
||||
let mid = id.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let model = kon_transcription::load_parakeet(&dir)
|
||||
.map_err(|e| e.to_string())?;
|
||||
engine.load(model, mid);
|
||||
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())
|
||||
}
|
||||
163
src-tauri/src/commands/transcription.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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 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 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),
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
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 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 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),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"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 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),
|
||||
},
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
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(())
|
||||
}
|
||||
81
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
mod commands;
|
||||
mod tray;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
use kon_core::types::EngineName;
|
||||
use kon_transcription::LocalEngine;
|
||||
|
||||
/// Shared app state holding the transcription engines.
|
||||
pub struct AppState {
|
||||
pub whisper_engine: Arc<LocalEngine>,
|
||||
pub parakeet_engine: Arc<LocalEngine>,
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.manage(AppState {
|
||||
whisper_engine: Arc::new(LocalEngine::new(
|
||||
EngineName::new("whisper"),
|
||||
)),
|
||||
parakeet_engine: Arc::new(LocalEngine::new(
|
||||
EngineName::new("parakeet"),
|
||||
)),
|
||||
})
|
||||
.setup(|app| {
|
||||
if let Err(e) = tray::setup(app) {
|
||||
eprintln!("Failed to setup tray: {e}");
|
||||
}
|
||||
|
||||
// Close-to-tray: hide window instead of exiting
|
||||
if let Some(main_window) = app.get_webview_window("main") {
|
||||
let win = main_window.clone();
|
||||
main_window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } =
|
||||
event
|
||||
{
|
||||
api.prevent_close();
|
||||
let _ = win.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Whisper model management
|
||||
commands::models::download_model,
|
||||
commands::models::check_model,
|
||||
commands::models::list_models,
|
||||
commands::models::load_model,
|
||||
commands::models::check_engine,
|
||||
// Parakeet model management
|
||||
commands::models::download_parakeet_model,
|
||||
commands::models::check_parakeet_model,
|
||||
commands::models::list_parakeet_models,
|
||||
commands::models::load_parakeet_model,
|
||||
commands::models::check_parakeet_engine,
|
||||
// Transcription
|
||||
commands::transcription::transcribe_pcm,
|
||||
commands::transcription::transcribe_file,
|
||||
commands::transcription::transcribe_pcm_parakeet,
|
||||
// Audio
|
||||
commands::audio::save_audio,
|
||||
// Windows
|
||||
commands::windows::open_task_window,
|
||||
commands::windows::open_viewer_window,
|
||||
// Clipboard
|
||||
commands::clipboard::copy_to_clipboard,
|
||||
// Hardware
|
||||
commands::hardware::probe_system,
|
||||
commands::hardware::rank_models,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Kon");
|
||||
}
|
||||
5
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
kon_lib::run()
|
||||
}
|
||||
59
src-tauri/src/tray.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use tauri::image::Image;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::Manager;
|
||||
|
||||
pub fn setup(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let show = MenuItemBuilder::with_id("show", "Show Kon").build(app)?;
|
||||
let status = MenuItemBuilder::with_id("status", "Ready")
|
||||
.enabled(false)
|
||||
.build(app)?;
|
||||
let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&show)
|
||||
.separator()
|
||||
.item(&status)
|
||||
.separator()
|
||||
.item(&quit)
|
||||
.build()?;
|
||||
|
||||
let icon = app
|
||||
.default_window_icon()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Image::new(&[0u8; 4], 1, 1));
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.icon(icon)
|
||||
.tooltip("Kon — Ready")
|
||||
.menu(&menu)
|
||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let tauri::tray::TrayIconEvent::Click {
|
||||
button: tauri::tray::MouseButton::Left,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if let Some(window) =
|
||||
tray.app_handle().get_webview_window("main")
|
||||
{
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
40
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Kon",
|
||||
"version": "0.1.0",
|
||||
"identifier": "uk.co.corbel.kon",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Kon",
|
||||
"width": 1020,
|
||||
"height": 720,
|
||||
"minWidth": 1020,
|
||||
"minHeight": 540,
|
||||
"decorations": false,
|
||||
"resizable": true,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost; connect-src ipc: http://ipc.localhost asset: https://asset.localhost http://localhost:* ws://localhost:*; media-src 'self' asset: https://asset.localhost"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||