From f8bbcd791c9aaaed506f5e1ca5d1aae56c295c13 Mon Sep 17 00:00:00 2001 From: Ivan Zhuravlev Date: Mon, 16 Mar 2026 12:31:44 +0400 Subject: [PATCH] Migrate to transcribe-rs-0.3.1 and add Canary support (#1023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: migrate transcribe-rs from v0.2.8 to v0.3.0 Breaking API migration: - engines::* → onnx::* module paths, whisper_cpp::* for Whisper - TranscriptionEngine trait → SpeechModel trait - Two-step new()+load_model() → one-step Model::load() - transcribe_samples(Vec) → transcribe(&[f32]) - Explicit unload_model() → RAII drop - Canary engine now from transcribe-rs instead of local crate Removes canary-engine local crate dependency. Uses path dependency to local transcribe-rs clone (temporary). Co-Authored-By: Claude Opus 4.6 * feat: upgrade transcribe-rs to v0.3.1, add Canary models Update transcribe-rs dependency from v0.2.8 to v0.3.1 with restructured features (whisper + onnx). Add Canary 180M Flash (4 languages, 146MB) and Canary 1B v2 (25 EU languages, 692MB) with translation support. Add i18n translations for all 17 locales. Languages verified against HuggingFace model cards: - Canary 180M Flash: en, de, es, fr - Canary 1B v2: bg, hr, cs, da, nl, en, et, fi, fr, de, el, hu, it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, ru, uk Co-Authored-By: Claude Opus 4.6 (1M context) * make sure when changing models the language changes with it * format --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: CJ Pais --- src-tauri/Cargo.lock | 4 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/models.rs | 18 ++ src-tauri/src/managers/model.rs | 82 +++++++ src-tauri/src/managers/transcription.rs | 207 ++++++++++-------- src/bindings.ts | 4 +- .../settings/general/ModelSettingsCard.tsx | 3 +- src/i18n/locales/ar/translation.json | 8 + src/i18n/locales/cs/translation.json | 8 + src/i18n/locales/de/translation.json | 8 + src/i18n/locales/en/translation.json | 8 + src/i18n/locales/es/translation.json | 8 + src/i18n/locales/fr/translation.json | 8 + src/i18n/locales/it/translation.json | 8 + src/i18n/locales/ja/translation.json | 8 + src/i18n/locales/ko/translation.json | 8 + src/i18n/locales/pl/translation.json | 8 + src/i18n/locales/pt/translation.json | 8 + src/i18n/locales/ru/translation.json | 8 + src/i18n/locales/tr/translation.json | 8 + src/i18n/locales/uk/translation.json | 8 + src/i18n/locales/vi/translation.json | 8 + src/i18n/locales/zh-TW/translation.json | 8 + src/i18n/locales/zh/translation.json | 8 + src/stores/settingsStore.ts | 7 + 25 files changed, 369 insertions(+), 94 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5384ca8..2c55c88 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7076,9 +7076,9 @@ dependencies = [ [[package]] name = "transcribe-rs" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "811b3e12455e39ad39003be9a4b7043363a7082b50484a3eb6e3e862466520e4" +checksum = "a74103eb9d8148a277bcf0a4f0e8fb8c08a624e7daf39d1b255af190927c3040" dependencies = [ "base64 0.22.1", "derive_builder", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5c3d80f..34878cd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -68,7 +68,7 @@ chrono = "0.4" rusqlite = { version = "0.37", features = ["bundled"] } tar = "0.4.44" flate2 = "1.0" -transcribe-rs = { version = "0.2.8", features = ["whisper", "parakeet", "moonshine", "sense_voice", "gigaam"] } +transcribe-rs = { version = "0.3.1", features = ["whisper", "onnx"] } handy-keys = "0.2.4" ferrous-opencc = "0.2.3" clap = { version = "4", features = ["derive"] } diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index a3197e3..fca4f0a 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -92,6 +92,24 @@ pub fn switch_active_model(app: &AppHandle, model_id: &str) -> Result<(), String // when it reacts to events emitted by load_model. let mut settings = settings; settings.selected_model = model_id.to_string(); + + // Reset language to auto if the new model doesn't support the currently selected language. + // This prevents stale language settings from causing errors (e.g. Canary receiving zh-Hans) + // and stops downstream processing (e.g. OpenCC) from running on an irrelevant language. + if settings.selected_language != "auto" + && !model_info.supported_languages.is_empty() + && !model_info + .supported_languages + .contains(&settings.selected_language) + { + log::info!( + "Resetting language from '{}' to 'auto' (not supported by {})", + settings.selected_language, + model_id + ); + settings.selected_language = "auto".to_string(); + } + write_settings(app, settings); // Skip eager loading if unload is set to "Immediately" — the model diff --git a/src-tauri/src/managers/model.rs b/src-tauri/src/managers/model.rs index 59150c7..74f2515 100644 --- a/src-tauri/src/managers/model.rs +++ b/src-tauri/src/managers/model.rs @@ -24,6 +24,7 @@ pub enum EngineType { MoonshineStreaming, SenseVoice, GigaAM, + Canary, } #[derive(Debug, Clone, Serialize, Deserialize, Type)] @@ -44,6 +45,7 @@ pub struct ModelInfo { pub supports_translation: bool, // Whether the model supports translating to English pub is_recommended: bool, // Whether this is the recommended model for new users pub supported_languages: Vec, // Languages this model can transcribe + pub supports_language_selection: bool, // Whether the user can explicitly pick a language pub is_custom: bool, // Whether this is a user-provided custom model } @@ -112,6 +114,7 @@ impl ModelManager { supports_translation: true, is_recommended: false, supported_languages: whisper_languages.clone(), + supports_language_selection: true, is_custom: false, }, ); @@ -136,6 +139,7 @@ impl ModelManager { supports_translation: true, is_recommended: false, supported_languages: whisper_languages.clone(), + supports_language_selection: true, is_custom: false, }, ); @@ -159,6 +163,7 @@ impl ModelManager { supports_translation: false, // Turbo doesn't support translation is_recommended: false, supported_languages: whisper_languages.clone(), + supports_language_selection: true, is_custom: false, }, ); @@ -182,6 +187,7 @@ impl ModelManager { supports_translation: true, is_recommended: false, supported_languages: whisper_languages.clone(), + supports_language_selection: true, is_custom: false, }, ); @@ -206,6 +212,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: whisper_languages, + supports_language_selection: true, is_custom: false, }, ); @@ -230,6 +237,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: false, is_custom: false, }, ); @@ -263,6 +271,7 @@ impl ModelManager { supports_translation: false, is_recommended: true, supported_languages: parakeet_v3_languages, + supports_language_selection: false, is_custom: false, }, ); @@ -286,6 +295,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: false, is_custom: false, }, ); @@ -311,6 +321,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: false, is_custom: false, }, ); @@ -336,6 +347,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: false, is_custom: false, }, ); @@ -361,6 +373,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: false, is_custom: false, }, ); @@ -392,6 +405,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: sense_voice_languages, + supports_language_selection: true, is_custom: false, }, ); @@ -418,6 +432,72 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: gigaam_languages, + supports_language_selection: false, + is_custom: false, + }, + ); + + // Canary 180m Flash supported languages (4 languages) + let canary_flash_languages: Vec = vec!["en", "de", "es", "fr"] + .into_iter() + .map(String::from) + .collect(); + + available_models.insert( + "canary-180m-flash".to_string(), + ModelInfo { + id: "canary-180m-flash".to_string(), + name: "Canary 180M Flash".to_string(), + description: "Very fast. English, German, Spanish, French. Supports translation." + .to_string(), + filename: "canary-180m-flash".to_string(), + url: Some("https://blob.handy.computer/canary-180m-flash.tar.gz".to_string()), + size_mb: 146, + is_downloaded: false, + is_downloading: false, + partial_size: 0, + is_directory: true, + engine_type: EngineType::Canary, + accuracy_score: 0.75, + speed_score: 0.85, + supports_translation: true, + is_recommended: false, + supported_languages: canary_flash_languages, + supports_language_selection: true, + is_custom: false, + }, + ); + + // Canary 1B v2 supported languages (25 EU languages) + let canary_1b_languages: Vec = vec![ + "bg", "hr", "cs", "da", "nl", "en", "et", "fi", "fr", "de", "el", "hu", "it", "lv", + "lt", "mt", "pl", "pt", "ro", "sk", "sl", "es", "sv", "ru", "uk", + ] + .into_iter() + .map(String::from) + .collect(); + + available_models.insert( + "canary-1b-v2".to_string(), + ModelInfo { + id: "canary-1b-v2".to_string(), + name: "Canary 1B v2".to_string(), + description: "Accurate multilingual. 25 European languages. Supports translation." + .to_string(), + filename: "canary-1b-v2".to_string(), + url: Some("https://blob.handy.computer/canary-1b-v2.tar.gz".to_string()), + size_mb: 692, + is_downloaded: false, + is_downloading: false, + partial_size: 0, + is_directory: true, + engine_type: EngineType::Canary, + accuracy_score: 0.85, + speed_score: 0.70, + supports_translation: true, + is_recommended: false, + supported_languages: canary_1b_languages, + supports_language_selection: true, is_custom: false, }, ); @@ -689,6 +769,7 @@ impl ModelManager { supports_translation: false, is_recommended: false, supported_languages: vec![], + supports_language_selection: true, is_custom: true, }, ); @@ -1240,6 +1321,7 @@ mod tests { supports_translation: true, is_recommended: false, supported_languages: vec!["en".to_string()], + supports_language_selection: true, is_custom: false, }, ); diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index afedc49..90010de 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -11,22 +11,16 @@ use std::thread; use std::time::{Duration, SystemTime}; use tauri::{AppHandle, Emitter}; use transcribe_rs::{ - engines::{ - gigaam::GigaAMEngine, - moonshine::{ - ModelVariant, MoonshineEngine, MoonshineModelParams, MoonshineStreamingEngine, - StreamingModelParams, - }, - parakeet::{ - ParakeetEngine, ParakeetInferenceParams, ParakeetModelParams, TimestampGranularity, - }, - sense_voice::{ - Language as SenseVoiceLanguage, SenseVoiceEngine, SenseVoiceInferenceParams, - SenseVoiceModelParams, - }, - whisper::{WhisperEngine, WhisperInferenceParams}, + onnx::{ + canary::CanaryModel, + gigaam::GigaAMModel, + moonshine::{MoonshineModel, MoonshineVariant, StreamingModel}, + parakeet::{ParakeetModel, ParakeetParams, TimestampGranularity}, + sense_voice::{SenseVoiceModel, SenseVoiceParams}, + Quantization, }, - TranscriptionEngine, + whisper_cpp::{WhisperEngine, WhisperInferenceParams}, + SpeechModel, TranscribeOptions, }; #[derive(Clone, Debug, Serialize)] @@ -39,11 +33,12 @@ pub struct ModelStateEvent { enum LoadedEngine { Whisper(WhisperEngine), - Parakeet(ParakeetEngine), - Moonshine(MoonshineEngine), - MoonshineStreaming(MoonshineStreamingEngine), - SenseVoice(SenseVoiceEngine), - GigaAM(GigaAMEngine), + Parakeet(ParakeetModel), + Moonshine(MoonshineModel), + MoonshineStreaming(StreamingModel), + SenseVoice(SenseVoiceModel), + GigaAM(GigaAMModel), + Canary(CanaryModel), } /// RAII guard that clears the `is_loading` flag and notifies waiters on drop. @@ -186,17 +181,8 @@ impl TranscriptionManager { { let mut engine = self.lock_engine(); - if let Some(ref mut loaded_engine) = *engine { - match loaded_engine { - LoadedEngine::Whisper(ref mut e) => e.unload_model(), - LoadedEngine::Parakeet(ref mut e) => e.unload_model(), - LoadedEngine::Moonshine(ref mut e) => e.unload_model(), - LoadedEngine::MoonshineStreaming(ref mut e) => e.unload_model(), - LoadedEngine::SenseVoice(ref mut e) => e.unload_model(), - LoadedEngine::GigaAM(ref mut e) => e.unload_model(), - } - } - *engine = None; // Drop the engine to free memory + // v0.3.0: no explicit unload — dropping the engine frees all resources + *engine = None; } { let mut current_model = self.current_model_id.lock().unwrap(); @@ -274,8 +260,7 @@ impl TranscriptionManager { // Create appropriate engine based on model type let loaded_engine = match model_info.engine_type { EngineType::Whisper => { - let mut engine = WhisperEngine::new(); - engine.load_model(&model_path).map_err(|e| { + let engine = WhisperEngine::load(&model_path).map_err(|e| { let error_msg = format!("Failed to load whisper model {}: {}", model_id, e); let _ = self.app_handle.emit( "model-state-changed", @@ -291,10 +276,8 @@ impl TranscriptionManager { LoadedEngine::Whisper(engine) } EngineType::Parakeet => { - let mut engine = ParakeetEngine::new(); - engine - .load_model_with_params(&model_path, ParakeetModelParams::int8()) - .map_err(|e| { + let engine = + ParakeetModel::load(&model_path, &Quantization::Int8).map_err(|e| { let error_msg = format!("Failed to load parakeet model {}: {}", model_id, e); let _ = self.app_handle.emit( @@ -311,32 +294,28 @@ impl TranscriptionManager { LoadedEngine::Parakeet(engine) } EngineType::Moonshine => { - let mut engine = MoonshineEngine::new(); - engine - .load_model_with_params( - &model_path, - MoonshineModelParams::variant(ModelVariant::Base), - ) - .map_err(|e| { - let error_msg = - format!("Failed to load moonshine model {}: {}", model_id, e); - let _ = self.app_handle.emit( - "model-state-changed", - ModelStateEvent { - event_type: "loading_failed".to_string(), - model_id: Some(model_id.to_string()), - model_name: Some(model_info.name.clone()), - error: Some(error_msg.clone()), - }, - ); - anyhow::anyhow!(error_msg) - })?; + let engine = MoonshineModel::load( + &model_path, + MoonshineVariant::Base, + &Quantization::default(), + ) + .map_err(|e| { + let error_msg = format!("Failed to load moonshine model {}: {}", model_id, e); + let _ = self.app_handle.emit( + "model-state-changed", + ModelStateEvent { + event_type: "loading_failed".to_string(), + model_id: Some(model_id.to_string()), + model_name: Some(model_info.name.clone()), + error: Some(error_msg.clone()), + }, + ); + anyhow::anyhow!(error_msg) + })?; LoadedEngine::Moonshine(engine) } EngineType::MoonshineStreaming => { - let mut engine = MoonshineStreamingEngine::new(); - engine - .load_model_with_params(&model_path, StreamingModelParams::default()) + let engine = StreamingModel::load(&model_path, 0, &Quantization::default()) .map_err(|e| { let error_msg = format!( "Failed to load moonshine streaming model {}: {}", @@ -356,10 +335,8 @@ impl TranscriptionManager { LoadedEngine::MoonshineStreaming(engine) } EngineType::SenseVoice => { - let mut engine = SenseVoiceEngine::new(); - engine - .load_model_with_params(&model_path, SenseVoiceModelParams::int8()) - .map_err(|e| { + let engine = + SenseVoiceModel::load(&model_path, &Quantization::Int8).map_err(|e| { let error_msg = format!("Failed to load SenseVoice model {}: {}", model_id, e); let _ = self.app_handle.emit( @@ -376,8 +353,7 @@ impl TranscriptionManager { LoadedEngine::SenseVoice(engine) } EngineType::GigaAM => { - let mut engine = GigaAMEngine::new(); - engine.load_model(&model_path).map_err(|e| { + let engine = GigaAMModel::load(&model_path, &Quantization::Int8).map_err(|e| { let error_msg = format!("Failed to load gigaam model {}: {}", model_id, e); let _ = self.app_handle.emit( "model-state-changed", @@ -392,6 +368,22 @@ impl TranscriptionManager { })?; LoadedEngine::GigaAM(engine) } + EngineType::Canary => { + let engine = CanaryModel::load(&model_path, &Quantization::Int8).map_err(|e| { + let error_msg = format!("Failed to load canary model {}: {}", model_id, e); + let _ = self.app_handle.emit( + "model-state-changed", + ModelStateEvent { + event_type: "loading_failed".to_string(), + model_id: Some(model_id.to_string()), + model_name: Some(model_info.name.clone()), + error: Some(error_msg.clone()), + }, + ); + anyhow::anyhow!(error_msg) + })?; + LoadedEngine::Canary(engine) + } }; // Update the current engine and model ID @@ -486,6 +478,33 @@ impl TranscriptionManager { // Get current settings for configuration let settings = get_settings(&self.app_handle); + // Validate selected language against the model's supported languages. + // If the language isn't supported, fall back to "auto" to prevent errors. + let validated_language = if settings.selected_language == "auto" { + "auto".to_string() + } else { + let is_supported = self + .model_manager + .get_model_info(&settings.selected_model) + .map(|info| { + info.supported_languages.is_empty() + || info + .supported_languages + .contains(&settings.selected_language) + }) + .unwrap_or(true); + + if is_supported { + settings.selected_language.clone() + } else { + warn!( + "Language '{}' not supported by current model, falling back to auto-detect", + settings.selected_language + ); + "auto".to_string() + } + }; + // Perform transcription with the appropriate engine. // We use catch_unwind to prevent engine panics from poisoning the mutex, // which would make the app hang indefinitely on subsequent operations. @@ -511,15 +530,15 @@ impl TranscriptionManager { || -> Result { match &mut engine { LoadedEngine::Whisper(whisper_engine) => { - let whisper_language = if settings.selected_language == "auto" { + let whisper_language = if validated_language == "auto" { None } else { - let normalized = if settings.selected_language == "zh-Hans" - || settings.selected_language == "zh-Hant" + let normalized = if validated_language == "zh-Hans" + || validated_language == "zh-Hant" { "zh".to_string() } else { - settings.selected_language.clone() + validated_language.clone() }; Some(normalized) }; @@ -536,50 +555,64 @@ impl TranscriptionManager { }; whisper_engine - .transcribe_samples(audio, Some(params)) + .transcribe_with(&audio, ¶ms) .map_err(|e| anyhow::anyhow!("Whisper transcription failed: {}", e)) } LoadedEngine::Parakeet(parakeet_engine) => { - let params = ParakeetInferenceParams { - timestamp_granularity: TimestampGranularity::Segment, + let params = ParakeetParams { + timestamp_granularity: Some(TimestampGranularity::Segment), ..Default::default() }; parakeet_engine - .transcribe_samples(audio, Some(params)) + .transcribe_with(&audio, ¶ms) .map_err(|e| { anyhow::anyhow!("Parakeet transcription failed: {}", e) }) } LoadedEngine::Moonshine(moonshine_engine) => moonshine_engine - .transcribe_samples(audio, None) + .transcribe(&audio, &TranscribeOptions::default()) .map_err(|e| anyhow::anyhow!("Moonshine transcription failed: {}", e)), LoadedEngine::MoonshineStreaming(streaming_engine) => streaming_engine - .transcribe_samples(audio, None) + .transcribe(&audio, &TranscribeOptions::default()) .map_err(|e| { anyhow::anyhow!("Moonshine streaming transcription failed: {}", e) }), LoadedEngine::SenseVoice(sense_voice_engine) => { - let language = match settings.selected_language.as_str() { - "zh" | "zh-Hans" | "zh-Hant" => SenseVoiceLanguage::Chinese, - "en" => SenseVoiceLanguage::English, - "ja" => SenseVoiceLanguage::Japanese, - "ko" => SenseVoiceLanguage::Korean, - "yue" => SenseVoiceLanguage::Cantonese, - _ => SenseVoiceLanguage::Auto, + let language = match validated_language.as_str() { + "zh" | "zh-Hans" | "zh-Hant" => Some("zh".to_string()), + "en" => Some("en".to_string()), + "ja" => Some("ja".to_string()), + "ko" => Some("ko".to_string()), + "yue" => Some("yue".to_string()), + _ => None, }; - let params = SenseVoiceInferenceParams { + let params = SenseVoiceParams { language, - use_itn: true, + use_itn: Some(true), }; sense_voice_engine - .transcribe_samples(audio, Some(params)) + .transcribe_with(&audio, ¶ms) .map_err(|e| { anyhow::anyhow!("SenseVoice transcription failed: {}", e) }) } LoadedEngine::GigaAM(gigaam_engine) => gigaam_engine - .transcribe_samples(audio, None) + .transcribe(&audio, &TranscribeOptions::default()) .map_err(|e| anyhow::anyhow!("GigaAM transcription failed: {}", e)), + LoadedEngine::Canary(canary_engine) => { + let lang = if validated_language == "auto" { + None + } else { + Some(validated_language.clone()) + }; + let options = TranscribeOptions { + language: lang, + translate: settings.translate_to_english, + }; + canary_engine + .transcribe(&audio, &options) + .map_err(|e| anyhow::anyhow!("Canary transcription failed: {}", e)) + } } }, )); diff --git a/src/bindings.ts b/src/bindings.ts index f8a259f..1f89f3e 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -763,7 +763,7 @@ export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter" export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null } export type ClipboardHandling = "dont_modify" | "copy_to_clipboard" export type CustomSounds = { start: boolean; stop: boolean } -export type EngineType = "Whisper" | "Parakeet" | "Moonshine" | "MoonshineStreaming" | "SenseVoice" | "GigaAM" +export type EngineType = "Whisper" | "Parakeet" | "Moonshine" | "MoonshineStreaming" | "SenseVoice" | "GigaAM" | "Canary" export type HistoryEntry = { id: number; file_name: string; timestamp: number; saved: boolean; title: string; transcription_text: string; post_processed_text: string | null; post_process_prompt: string | null } /** * Result of changing keyboard implementation @@ -776,7 +776,7 @@ reset_bindings: string[] } export type KeyboardImplementation = "tauri" | "handy_keys" export type LLMPrompt = { id: string; name: string; prompt: string } export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" -export type ModelInfo = { id: string; name: string; description: string; filename: string; url: string | null; size_mb: number; is_downloaded: boolean; is_downloading: boolean; partial_size: number; is_directory: boolean; engine_type: EngineType; accuracy_score: number; speed_score: number; supports_translation: boolean; is_recommended: boolean; supported_languages: string[]; is_custom: boolean } +export type ModelInfo = { id: string; name: string; description: string; filename: string; url: string | null; size_mb: number; is_downloaded: boolean; is_downloading: boolean; partial_size: number; is_directory: boolean; engine_type: EngineType; accuracy_score: number; speed_score: number; supports_translation: boolean; is_recommended: boolean; supported_languages: string[]; supports_language_selection: boolean; is_custom: boolean } export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null } export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5" export type OverlayPosition = "none" | "top" | "bottom" diff --git a/src/components/settings/general/ModelSettingsCard.tsx b/src/components/settings/general/ModelSettingsCard.tsx index 39b8df4..51ec8fc 100644 --- a/src/components/settings/general/ModelSettingsCard.tsx +++ b/src/components/settings/general/ModelSettingsCard.tsx @@ -13,8 +13,7 @@ export const ModelSettingsCard: React.FC = () => { const currentModelInfo = models.find((m: ModelInfo) => m.id === currentModel); const supportsLanguageSelection = - currentModelInfo?.engine_type === "Whisper" || - currentModelInfo?.engine_type === "SenseVoice"; + currentModelInfo?.supports_language_selection ?? false; const supportsTranslation = currentModelInfo?.supports_translation ?? false; const hasAnySettings = supportsLanguageSelection || supportsTranslation; diff --git a/src/i18n/locales/ar/translation.json b/src/i18n/locales/ar/translation.json index ae34e3f..251ee29 100644 --- a/src/i18n/locales/ar/translation.json +++ b/src/i18n/locales/ar/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "التعرف على الكلام الروسي. سريع ودقيق." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "سريع جداً. الإنجليزية، الألمانية، الإسبانية، الفرنسية. يدعم الترجمة." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "متعدد اللغات ودقيق. 25 لغة أوروبية. يدعم الترجمة." } }, "errors": { diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index 0b3e490..b671318 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Rozpoznávání ruské řeči. Rychlé a přesné." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Velmi rychlý. Angličtina, němčina, španělština, francouzština. Podporuje překlad." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Přesný vícejazyčný. 25 evropských jazyků. Podporuje překlad." } }, "errors": { diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 42d5f58..5451c49 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Russische Spracherkennung. Schnell und präzise." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Sehr schnell. Englisch, Deutsch, Spanisch, Französisch. Unterstützt Übersetzung." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Genaue mehrsprachige Erkennung. 25 europäische Sprachen. Unterstützt Übersetzung." } }, "errors": { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index b2667bd..66bfa24 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Russian speech recognition. Fast and accurate." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Very fast. English, German, Spanish, French. Supports translation." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Accurate multilingual. 25 European languages. Supports translation." } }, "errors": { diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index d18843b..18fae60 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Reconocimiento de voz en ruso. Rápido y preciso." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Muy rápido. Inglés, alemán, español, francés. Soporta traducción." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Multilingüe preciso. 25 idiomas europeos. Soporta traducción." } }, "errors": { diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index 58aedec..2fa70e8 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Reconnaissance vocale en russe. Rapide et précis." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Très rapide. Anglais, allemand, espagnol, français. Supporte la traduction." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Multilingue précis. 25 langues européennes. Supporte la traduction." } }, "errors": { diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index 6c5fc22..7cde7dc 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Riconoscimento vocale in russo. Veloce e preciso." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Molto veloce. Inglese, tedesco, spagnolo, francese. Supporta la traduzione." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Multilingue accurato. 25 lingue europee. Supporta la traduzione." } }, "errors": { diff --git a/src/i18n/locales/ja/translation.json b/src/i18n/locales/ja/translation.json index f16c9ef..4b1f953 100644 --- a/src/i18n/locales/ja/translation.json +++ b/src/i18n/locales/ja/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "ロシア語音声認識。高速かつ高精度。" + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "非常に高速。英語、ドイツ語、スペイン語、フランス語。翻訳対応。" + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "高精度な多言語対応。25のヨーロッパ言語。翻訳対応。" } }, "errors": { diff --git a/src/i18n/locales/ko/translation.json b/src/i18n/locales/ko/translation.json index bda6629..6931315 100644 --- a/src/i18n/locales/ko/translation.json +++ b/src/i18n/locales/ko/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "러시아어 음성 인식. 빠르고 정확함." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "매우 빠름. 영어, 독일어, 스페인어, 프랑스어. 번역 지원." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "정확한 다국어 지원. 25개 유럽 언어. 번역 지원." } }, "errors": { diff --git a/src/i18n/locales/pl/translation.json b/src/i18n/locales/pl/translation.json index 0343dab..071dfea 100644 --- a/src/i18n/locales/pl/translation.json +++ b/src/i18n/locales/pl/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Rozpoznawanie mowy rosyjskiej. Szybkie i dokładne." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Bardzo szybki. Angielski, niemiecki, hiszpański, francuski. Obsługuje tłumaczenie." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Dokładny wielojęzyczny. 25 języków europejskich. Obsługuje tłumaczenie." } }, "errors": { diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index 834b16a..fced01a 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Reconhecimento de voz em russo. Rápido e preciso." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Muito rápido. Inglês, alemão, espanhol, francês. Suporta tradução." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Multilíngue preciso. 25 idiomas europeus. Suporta tradução." } }, "errors": { diff --git a/src/i18n/locales/ru/translation.json b/src/i18n/locales/ru/translation.json index a6cd534..1c90413 100644 --- a/src/i18n/locales/ru/translation.json +++ b/src/i18n/locales/ru/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Распознавание русской речи. Быстро и точно." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Очень быстрая. Английский, немецкий, испанский, французский. Поддержка перевода." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Точная многоязычная. 25 европейских языков. Поддержка перевода." } }, "errors": { diff --git a/src/i18n/locales/tr/translation.json b/src/i18n/locales/tr/translation.json index 8c27716..3b33ebe 100644 --- a/src/i18n/locales/tr/translation.json +++ b/src/i18n/locales/tr/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Rusça konuşma tanıma. Hızlı ve doğru." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Çok hızlı. İngilizce, Almanca, İspanyolca, Fransızca. Çeviri desteği." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Doğru çok dilli. 25 Avrupa dili. Çeviri desteği." } }, "errors": { diff --git a/src/i18n/locales/uk/translation.json b/src/i18n/locales/uk/translation.json index fac5675..2c5a6f9 100644 --- a/src/i18n/locales/uk/translation.json +++ b/src/i18n/locales/uk/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Розпізнавання російського мовлення. Швидко і точно." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Дуже швидка. Англійська, німецька, іспанська, французька. Підтримує переклад." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Точна багатомовна. 25 європейських мов. Підтримує переклад." } }, "errors": { diff --git a/src/i18n/locales/vi/translation.json b/src/i18n/locales/vi/translation.json index e651d93..e72d8cc 100644 --- a/src/i18n/locales/vi/translation.json +++ b/src/i18n/locales/vi/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "Nhận dạng giọng nói tiếng Nga. Nhanh và chính xác." + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "Rất nhanh. Tiếng Anh, tiếng Đức, tiếng Tây Ban Nha, tiếng Pháp. Hỗ trợ dịch thuật." + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "Đa ngôn ngữ chính xác. 25 ngôn ngữ châu Âu. Hỗ trợ dịch thuật." } }, "errors": { diff --git a/src/i18n/locales/zh-TW/translation.json b/src/i18n/locales/zh-TW/translation.json index 49e5150..f2a315f 100644 --- a/src/i18n/locales/zh-TW/translation.json +++ b/src/i18n/locales/zh-TW/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "俄語語音辨識。快速且準確。" + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "速度極快。支援英語、德語、西班牙語、法語。支援翻譯。" + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "準確的多語言模型。25種歐洲語言。支援翻譯。" } }, "errors": { diff --git a/src/i18n/locales/zh/translation.json b/src/i18n/locales/zh/translation.json index 2dbc934..874e975 100644 --- a/src/i18n/locales/zh/translation.json +++ b/src/i18n/locales/zh/translation.json @@ -80,6 +80,14 @@ "gigaam-v3-e2e-ctc": { "name": "GigaAM v3", "description": "俄语语音识别。快速且准确。" + }, + "canary-180m-flash": { + "name": "Canary 180M Flash", + "description": "非常快速。支持英语、德语、西班牙语、法语。支持翻译。" + }, + "canary-1b-v2": { + "name": "Canary 1B v2", + "description": "准确的多语言模型。25种欧洲语言。支持翻译。" } }, "errors": { diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 3de95f9..2ef36f7 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; +import { listen } from "@tauri-apps/api/event"; import type { AppSettings as Settings, AudioDevice } from "@/bindings"; import { commands } from "@/bindings"; @@ -565,6 +566,12 @@ export const useSettingsStore = create()( refreshSettings(), checkCustomSounds(), ]); + + // Re-fetch settings when the backend changes them (e.g. language + // reset during model switch). The backend is the source of truth. + listen("model-state-changed", () => { + get().refreshSettings(); + }); }, })), );