diff --git a/openless-all/app/crates/openless-core/src/domains.rs b/openless-all/app/crates/openless-core/src/domains.rs index 1aefdd1e..cf8600b7 100644 --- a/openless-all/app/crates/openless-core/src/domains.rs +++ b/openless-all/app/crates/openless-core/src/domains.rs @@ -280,6 +280,12 @@ pub trait LocalAsrApi: Send + Sync { fn release(&self, runtime: LocalAsrRuntime) -> BoxFuture<'static, Result<(), BackendError>>; fn preload(&self, runtime: LocalAsrRuntime) -> BoxFuture<'static, Result<(), BackendError>>; fn delete_model(&self, target: LocalAsrTarget) -> BoxFuture<'static, Result<(), BackendError>>; + fn cleanup_incomplete( + &self, + target: LocalAsrTarget, + ) -> BoxFuture<'static, Result<(), BackendError>> { + unsupported("local ASR incomplete download cleanup") + } fn model_dir( &self, target: LocalAsrTarget, diff --git a/openless-all/app/crates/openless-core/src/local_asr_service.rs b/openless-all/app/crates/openless-core/src/local_asr_service.rs index 07a1954f..83c99774 100644 --- a/openless-all/app/crates/openless-core/src/local_asr_service.rs +++ b/openless-all/app/crates/openless-core/src/local_asr_service.rs @@ -1254,6 +1254,14 @@ impl LocalAsrApi for LocalAsrService { }) } + fn cleanup_incomplete( + &self, + target: LocalAsrTarget, + ) -> BoxFuture<'static, Result<(), BackendError>> { + let store = Arc::clone(&self.model_store); + Box::pin(async move { store.cleanup_incomplete(&target) }) + } + fn model_dir( &self, target: LocalAsrTarget, diff --git a/openless-all/app/src-tauri/src/commands/local_asr.rs b/openless-all/app/src-tauri/src/commands/local_asr.rs index b93db541..faddfb4e 100644 --- a/openless-all/app/src-tauri/src/commands/local_asr.rs +++ b/openless-all/app/src-tauri/src/commands/local_asr.rs @@ -76,12 +76,19 @@ impl From for LocalAsrStorageSettings { } } +/// 与 Sherpa `SherpaCatalogWire` 对齐:透出展示名、家族、模式、语言与远端尺寸, +/// 前端无需再为基础元数据实时访问 HuggingFace。 #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct LocalAsrModelStatus { pub id: String, pub hf_repo: String, + pub display_name: String, + pub family: String, + pub mode: Option, + pub languages: Vec, pub downloaded_bytes: u64, + pub size_bytes: Option, pub is_downloaded: bool, } @@ -89,8 +96,13 @@ impl From for LocalAsrModelStatus { fn from(model: openless_core::LocalAsrModel) -> Self { Self { id: model.target.model_id().to_string(), - hf_repo: model.repository.unwrap_or_default(), + hf_repo: model.repository.clone().unwrap_or_default(), + display_name: model.display_name, + family: model.family, + mode: model.mode, + languages: model.languages, downloaded_bytes: model.downloaded_bytes, + size_bytes: model.size_bytes, is_downloaded: model.installed, } } @@ -355,6 +367,20 @@ pub async fn local_asr_delete_model( .map_err(core_error) } +/// 清理指定模型中断下载遗留的 staging 目录,不触碰已安装模型。 +#[tauri::command] +pub async fn local_asr_cleanup_incomplete( + backend: CoreState<'_>, + model_id: String, +) -> Result<(), String> { + backend + .services() + .local_asr + .cleanup_incomplete(target(LocalAsrRuntime::Generic, model_id)?) + .await + .map_err(core_error) +} + #[tauri::command] pub async fn local_asr_model_dir( backend: CoreState<'_>, @@ -489,4 +515,40 @@ mod wire_contract_tests { }) ); } + + #[test] + fn generic_model_status_keeps_the_legacy_fields_and_adds_catalog_metadata() { + let core = openless_core::LocalAsrModel { + target: openless_core::LocalAsrTarget::parse( + openless_core::LocalAsrRuntime::Generic, + "qwen3-asr-0.6b", + ) + .unwrap(), + display_name: "Qwen3 ASR 0.6B".into(), + family: "qwen3_asr".into(), + mode: None, + repository: Some("Qwen/Qwen3-ASR-0.6B".into()), + languages: vec!["zh".into(), "en".into()], + installed: false, + downloaded_bytes: 4096, + size_bytes: Some(1_234_567), + }; + + let value = serde_json::to_value(LocalAsrModelStatus::from(core)).unwrap(); + + assert_eq!( + value, + serde_json::json!({ + "id": "qwen3-asr-0.6b", + "hfRepo": "Qwen/Qwen3-ASR-0.6B", + "displayName": "Qwen3 ASR 0.6B", + "family": "qwen3_asr", + "mode": null, + "languages": ["zh", "en"], + "downloadedBytes": 4096, + "sizeBytes": 1234567, + "isDownloaded": false, + }) + ); + } } diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index ee24582d..0706a431 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -351,6 +351,7 @@ macro_rules! app_invoke_handler_desktop { commands::local_asr_download_model, commands::local_asr_cancel_download, commands::local_asr_delete_model, + commands::local_asr_cleanup_incomplete, commands::local_asr_model_dir, commands::local_asr_reveal_model_dir, commands::local_asr_reveal_models_root, diff --git a/openless-all/app/src/i18n/de.ts b/openless-all/app/src/i18n/de.ts index 766a1371..514c65bc 100644 --- a/openless-all/app/src/i18n/de.ts +++ b/openless-all/app/src/i18n/de.ts @@ -2129,6 +2129,9 @@ export const de: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3 (mehrsprachig)', sherpaModelWhisperLargeV3Desc: 'Leistungsstärkste mehrsprachige Open-Source-Whisper-Variante: hohe Qualität, großer Download.', + sherpaModelZipformer: 'Zipformer-Streaming (zh/en)', + sherpaModelZipformerDesc: + 'Streaming-Modell für Chinesisch und Englisch mit der geringsten Latenz – Text erscheint beim Sprechen.', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: 'Konvertiertes sherpa-onnx-Qwen3-ASR-Modell mit mehrsprachiger Erkennung und besserer Verarbeitung längerer Kontexte.', diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index faa584a8..340ba794 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -2063,6 +2063,9 @@ export const en: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingual)', sherpaModelWhisperLargeV3Desc: 'The best open-source multilingual Whisper tier — high quality, large download.', + sherpaModelZipformer: 'Zipformer Streaming (zh/en)', + sherpaModelZipformerDesc: + 'Streaming Chinese-English model with the lowest latency — text appears as you speak.', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: 'Converted sherpa-onnx Qwen3-ASR model with multilingual recognition and stronger long-form context handling.', diff --git a/openless-all/app/src/i18n/es.ts b/openless-all/app/src/i18n/es.ts index 5fa2c6b0..d8779961 100644 --- a/openless-all/app/src/i18n/es.ts +++ b/openless-all/app/src/i18n/es.ts @@ -2110,6 +2110,9 @@ export const es: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingüe)', sherpaModelWhisperLargeV3Desc: 'La versión multilingüe de código abierto más avanzada de Whisper: gran calidad y descarga de gran tamaño.', + sherpaModelZipformer: 'Zipformer en streaming (zh/en)', + sherpaModelZipformerDesc: + 'Modelo en streaming de chino e inglés con la latencia más baja: el texto aparece mientras hablas.', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: 'Modelo Qwen3-ASR convertido para sherpa-onnx, con reconocimiento multilingüe y mejor tratamiento del contexto en textos largos.', diff --git a/openless-all/app/src/i18n/fr.ts b/openless-all/app/src/i18n/fr.ts index 39a796ba..55b54e7e 100644 --- a/openless-all/app/src/i18n/fr.ts +++ b/openless-all/app/src/i18n/fr.ts @@ -2139,6 +2139,9 @@ export const fr: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingue)', sherpaModelWhisperLargeV3Desc: 'La version multilingue open source la plus avancée de Whisper : grande qualité et téléchargement volumineux.', + sherpaModelZipformer: 'Zipformer streaming (zh/en)', + sherpaModelZipformerDesc: + 'Modèle streaming chinois-anglais à la latence la plus faible — le texte apparaît pendant que vous parlez.', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: 'Modèle Qwen3-ASR converti pour sherpa-onnx, avec reconnaissance multilingue et meilleure gestion du contexte long.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 08a9d41d..c40b4197 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -2041,6 +2041,9 @@ export const ja: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3(多言語)', sherpaModelWhisperLargeV3Desc: 'オープンソース多言語モデルの中で最高品質の Whisper 系。高品質だが大容量。', + sherpaModelZipformer: 'Zipformer ストリーミング(中英)', + sherpaModelZipformerDesc: + '話しながら文字が出るストリーミング型の中英モデル。遅延が最も小さく、リアルタイム文字起こしに適します。', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: '変換済み sherpa-onnx Qwen3-ASR モデル。多言語認識とより強い長文コンテキスト処理に対応。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 12073c7f..b71a5e2d 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -2025,6 +2025,9 @@ export const ko: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3 (다국어)', sherpaModelWhisperLargeV3Desc: '오픈소스 다국어 모델 중 품질이 가장 좋은 Whisper 계열. 고품질이지만 용량이 큽니다.', + sherpaModelZipformer: 'Zipformer 스트리밍(중·영)', + sherpaModelZipformerDesc: + '말하는 동안 텍스트가 나오는 스트리밍 중·영어 모델로, 지연이 가장 낮아 실시간 받아쓰기에 적합합니다.', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: '변환된 sherpa-onnx Qwen3-ASR 모델로 다국어 인식과 더 강한 긴 문맥 처리를 지원합니다.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 02fe972b..8fbe3a3d 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -1952,6 +1952,9 @@ export const zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3(多语)', sherpaModelWhisperLargeV3Desc: '开源多语通用里效果最好的 Whisper 档,质量高、体积大,适合高质量转写。', + sherpaModelZipformer: 'Zipformer 流式(中英)', + sherpaModelZipformerDesc: + '边说边出的流式中英模型,延迟最低,适合实时听写。', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: '转换后的 sherpa-onnx Qwen3-ASR 模型,支持多语言识别与更强的长上下文能力。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 44863ba3..eef2b33b 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -1938,6 +1938,9 @@ export const zhTW: typeof zhCN = { sherpaModelWhisperLargeV3: 'Whisper Large V3(多語)', sherpaModelWhisperLargeV3Desc: '開源多語通用中效果最好的 Whisper 檔,品質高、體積大,適合高品質轉寫。', + sherpaModelZipformer: 'Zipformer 串流(中英)', + sherpaModelZipformerDesc: + '邊說邊出的串流中英模型,延遲最低,適合即時聽寫。', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3Desc: '轉換後的 sherpa-onnx Qwen3-ASR 模型,支援多語言識別與更強的長上下文能力。', diff --git a/openless-all/app/src/lib/localAsr.ts b/openless-all/app/src/lib/localAsr.ts index 8263c556..088db766 100644 --- a/openless-all/app/src/lib/localAsr.ts +++ b/openless-all/app/src/lib/localAsr.ts @@ -37,7 +37,12 @@ export interface LocalAsrStorageSettings { export interface LocalAsrModelStatus { id: string; hfRepo: string; + displayName: string; + family: string; + mode: string | null; + languages: string[]; downloadedBytes: number; + sizeBytes: number | null; isDownloaded: boolean; } @@ -212,13 +217,23 @@ const MOCK_MODELS: LocalAsrModelStatus[] = [ { id: 'qwen3-asr-0.6b', hfRepo: 'Qwen/Qwen3-ASR-0.6B', + displayName: 'Qwen3 ASR 0.6B', + family: 'qwen3_asr', + mode: null, + languages: ['zh', 'en', 'ja', 'ko', 'yue'], downloadedBytes: 0, + sizeBytes: null, isDownloaded: false, }, { id: 'qwen3-asr-1.7b', hfRepo: 'Qwen/Qwen3-ASR-1.7B', + displayName: 'Qwen3 ASR 1.7B', + family: 'qwen3_asr', + mode: null, + languages: ['zh', 'en', 'ja', 'ko', 'yue'], downloadedBytes: 0, + sizeBytes: null, isDownloaded: false, }, ]; @@ -302,6 +317,11 @@ export function deleteLocalAsrModel(modelId: string): Promise { return invokeOrMock('local_asr_delete_model', { modelId }, () => undefined); } +/** 清理中断下载遗留的 staging 目录,不触碰已安装模型。 */ +export function cleanupIncompleteLocalAsrModel(modelId: string): Promise { + return invokeOrMock('local_asr_cleanup_incomplete', { modelId }, () => undefined); +} + export function getLocalAsrModelDir(modelId: string): Promise { return invokeOrMock('local_asr_model_dir', { modelId }, () => ''); } @@ -422,6 +442,7 @@ export type SherpaOnnxModelAlias = | 'paraformer-zh' | 'whisper-small-multi' | 'whisper-large-v3-multi' + | 'zipformer-bilingual-zh-en-streaming' | 'qwen3-asr-0.6b-int8'; export type SherpaOnnxMirror = 'huggingface' | 'hf-mirror' | 'github-release'; @@ -470,6 +491,11 @@ export const SHERPA_ONNX_ASR_MODELS: SherpaOnnxModelOption[] = [ labelKey: 'localAsr.sherpaModelWhisperLargeV3', descKey: 'localAsr.sherpaModelWhisperLargeV3Desc', }, + { + alias: 'zipformer-bilingual-zh-en-streaming', + labelKey: 'localAsr.sherpaModelZipformer', + descKey: 'localAsr.sherpaModelZipformerDesc', + }, { alias: 'qwen3-asr-0.6b-int8', labelKey: 'localAsr.sherpaModelQwen3', @@ -518,6 +544,13 @@ export function getSherpaOnnxAsrCatalog(): Promise { downloadedBytes: 0, fileSizeMb: 1700, }, + { + alias: 'zipformer-bilingual-zh-en-streaming' as const, + displayName: 'Zipformer Streaming (zh/en)', + cached: false, + downloadedBytes: 0, + fileSizeMb: 260, + }, { alias: 'qwen3-asr-0.6b-int8' as const, displayName: 'Qwen3-ASR 0.6B INT8',