feat(local-asr): 模型状态透出目录元数据、新增中断下载清理命令与 Zipformer 目录项

LocalAsrModelStatus 补齐 display_name/family/mode/languages/size_bytes,
前端无需为基础元数据实时访问 HuggingFace;新增 local_asr_cleanup_incomplete
命令(LocalAsrApi 默认实现 + Service 实现 + 前端包装)清理 staging 目录;
sherpa 目录新增 zipformer-bilingual-zh-en-streaming 流式中英模型并补齐八语文案。
修复测试里 LocalAsrTarget::parse 的 .into() 类型推断歧义。
This commit is contained in:
sim committed 2026-09-10 16:09:00 +08:00
1 parent bd5656b5fc
commit 786e4b4d07
13 files changed
+135 -1

No files matched your search

@@ -280,6 +280,12 @@ pub trait LocalAsrApi: Send + Sync {
fn release(&self, runtime: LocalAsrRuntime) -> BoxFuture<'static, Result<(), BackendError>>; fn release(&self, runtime: LocalAsrRuntime) -> BoxFuture<'static, Result<(), BackendError>>;
fn preload(&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 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( fn model_dir(
&self, &self,
target: LocalAsrTarget, target: LocalAsrTarget,
@@ -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( fn model_dir(
&self, &self,
target: LocalAsrTarget, target: LocalAsrTarget,
@@ -76,12 +76,19 @@ impl From<openless_core::LocalAsrStorageSettings> for LocalAsrStorageSettings {
} }
} }
/// 与 Sherpa `SherpaCatalogWire` 对齐:透出展示名、家族、模式、语言与远端尺寸,
/// 前端无需再为基础元数据实时访问 HuggingFace。
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LocalAsrModelStatus { pub struct LocalAsrModelStatus {
pub id: String, pub id: String,
pub hf_repo: String, pub hf_repo: String,
pub display_name: String,
pub family: String,
pub mode: Option<String>,
pub languages: Vec<String>,
pub downloaded_bytes: u64, pub downloaded_bytes: u64,
pub size_bytes: Option<u64>,
pub is_downloaded: bool, pub is_downloaded: bool,
} }
@@ -89,8 +96,13 @@ impl From<openless_core::LocalAsrModel> for LocalAsrModelStatus {
fn from(model: openless_core::LocalAsrModel) -> Self { fn from(model: openless_core::LocalAsrModel) -> Self {
Self { Self {
id: model.target.model_id().to_string(), 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, downloaded_bytes: model.downloaded_bytes,
size_bytes: model.size_bytes,
is_downloaded: model.installed, is_downloaded: model.installed,
} }
} }
@@ -355,6 +367,20 @@ pub async fn local_asr_delete_model(
.map_err(core_error) .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] #[tauri::command]
pub async fn local_asr_model_dir( pub async fn local_asr_model_dir(
backend: CoreState<'_>, 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,
})
);
}
} }
+1
View File
@@ -351,6 +351,7 @@ macro_rules! app_invoke_handler_desktop {
commands::local_asr_download_model, commands::local_asr_download_model,
commands::local_asr_cancel_download, commands::local_asr_cancel_download,
commands::local_asr_delete_model, commands::local_asr_delete_model,
commands::local_asr_cleanup_incomplete,
commands::local_asr_model_dir, commands::local_asr_model_dir,
commands::local_asr_reveal_model_dir, commands::local_asr_reveal_model_dir,
commands::local_asr_reveal_models_root, commands::local_asr_reveal_models_root,
+3
View File
@@ -2129,6 +2129,9 @@ export const de: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3 (mehrsprachig)', sherpaModelWhisperLargeV3: 'Whisper Large V3 (mehrsprachig)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'Leistungsstärkste mehrsprachige Open-Source-Whisper-Variante: hohe Qualität, großer Download.', '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', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'Konvertiertes sherpa-onnx-Qwen3-ASR-Modell mit mehrsprachiger Erkennung und besserer Verarbeitung längerer Kontexte.', 'Konvertiertes sherpa-onnx-Qwen3-ASR-Modell mit mehrsprachiger Erkennung und besserer Verarbeitung längerer Kontexte.',
+3
View File
@@ -2063,6 +2063,9 @@ export const en: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingual)', sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingual)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'The best open-source multilingual Whisper tier — high quality, large download.', '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', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'Converted sherpa-onnx Qwen3-ASR model with multilingual recognition and stronger long-form context handling.', 'Converted sherpa-onnx Qwen3-ASR model with multilingual recognition and stronger long-form context handling.',
+3
View File
@@ -2110,6 +2110,9 @@ export const es: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingüe)', sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingüe)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'La versión multilingüe de código abierto más avanzada de Whisper: gran calidad y descarga de gran tamaño.', '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', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'Modelo Qwen3-ASR convertido para sherpa-onnx, con reconocimiento multilingüe y mejor tratamiento del contexto en textos largos.', 'Modelo Qwen3-ASR convertido para sherpa-onnx, con reconocimiento multilingüe y mejor tratamiento del contexto en textos largos.',
+3
View File
@@ -2139,6 +2139,9 @@ export const fr: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingue)', sherpaModelWhisperLargeV3: 'Whisper Large V3 (multilingue)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'La version multilingue open source la plus avancée de Whisper : grande qualité et téléchargement volumineux.', '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', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'Modèle Qwen3-ASR converti pour sherpa-onnx, avec reconnaissance multilingue et meilleure gestion du contexte long.', 'Modèle Qwen3-ASR converti pour sherpa-onnx, avec reconnaissance multilingue et meilleure gestion du contexte long.',
+3
View File
@@ -2041,6 +2041,9 @@ export const ja: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3(多言語)', sherpaModelWhisperLargeV3: 'Whisper Large V3(多言語)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'オープンソース多言語モデルの中で最高品質の Whisper 系。高品質だが大容量。', 'オープンソース多言語モデルの中で最高品質の Whisper 系。高品質だが大容量。',
sherpaModelZipformer: 'Zipformer ストリーミング(中英)',
sherpaModelZipformerDesc:
'話しながら文字が出るストリーミング型の中英モデル。遅延が最も小さく、リアルタイム文字起こしに適します。',
sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'変換済み sherpa-onnx Qwen3-ASR モデル。多言語認識とより強い長文コンテキスト処理に対応。', '変換済み sherpa-onnx Qwen3-ASR モデル。多言語認識とより強い長文コンテキスト処理に対応。',
+3
View File
@@ -2025,6 +2025,9 @@ export const ko: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3 (다국어)', sherpaModelWhisperLargeV3: 'Whisper Large V3 (다국어)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'오픈소스 다국어 모델 중 품질이 가장 좋은 Whisper 계열. 고품질이지만 용량이 큽니다.', '오픈소스 다국어 모델 중 품질이 가장 좋은 Whisper 계열. 고품질이지만 용량이 큽니다.',
sherpaModelZipformer: 'Zipformer 스트리밍(중·영)',
sherpaModelZipformerDesc:
'말하는 동안 텍스트가 나오는 스트리밍 중·영어 모델로, 지연이 가장 낮아 실시간 받아쓰기에 적합합니다.',
sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'변환된 sherpa-onnx Qwen3-ASR 모델로 다국어 인식과 더 강한 긴 문맥 처리를 지원합니다.', '변환된 sherpa-onnx Qwen3-ASR 모델로 다국어 인식과 더 강한 긴 문맥 처리를 지원합니다.',
+3
View File
@@ -1952,6 +1952,9 @@ export const zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3(多语)', sherpaModelWhisperLargeV3: 'Whisper Large V3(多语)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'开源多语通用里效果最好的 Whisper 档,质量高、体积大,适合高质量转写。', '开源多语通用里效果最好的 Whisper 档,质量高、体积大,适合高质量转写。',
sherpaModelZipformer: 'Zipformer 流式(中英)',
sherpaModelZipformerDesc:
'边说边出的流式中英模型,延迟最低,适合实时听写。',
sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'转换后的 sherpa-onnx Qwen3-ASR 模型,支持多语言识别与更强的长上下文能力。', '转换后的 sherpa-onnx Qwen3-ASR 模型,支持多语言识别与更强的长上下文能力。',
+3
View File
@@ -1938,6 +1938,9 @@ export const zhTW: typeof zhCN = {
sherpaModelWhisperLargeV3: 'Whisper Large V3(多語)', sherpaModelWhisperLargeV3: 'Whisper Large V3(多語)',
sherpaModelWhisperLargeV3Desc: sherpaModelWhisperLargeV3Desc:
'開源多語通用中效果最好的 Whisper 檔,品質高、體積大,適合高品質轉寫。', '開源多語通用中效果最好的 Whisper 檔,品質高、體積大,適合高品質轉寫。',
sherpaModelZipformer: 'Zipformer 串流(中英)',
sherpaModelZipformerDesc:
'邊說邊出的串流中英模型,延遲最低,適合即時聽寫。',
sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8', sherpaModelQwen3: 'Qwen3-ASR 0.6B INT8',
sherpaModelQwen3Desc: sherpaModelQwen3Desc:
'轉換後的 sherpa-onnx Qwen3-ASR 模型,支援多語言識別與更強的長上下文能力。', '轉換後的 sherpa-onnx Qwen3-ASR 模型,支援多語言識別與更強的長上下文能力。',
+33
View File
@@ -37,7 +37,12 @@ export interface LocalAsrStorageSettings {
export interface LocalAsrModelStatus { export interface LocalAsrModelStatus {
id: string; id: string;
hfRepo: string; hfRepo: string;
displayName: string;
family: string;
mode: string | null;
languages: string[];
downloadedBytes: number; downloadedBytes: number;
sizeBytes: number | null;
isDownloaded: boolean; isDownloaded: boolean;
} }
@@ -212,13 +217,23 @@ const MOCK_MODELS: LocalAsrModelStatus[] = [
{ {
id: 'qwen3-asr-0.6b', id: 'qwen3-asr-0.6b',
hfRepo: 'Qwen/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, downloadedBytes: 0,
sizeBytes: null,
isDownloaded: false, isDownloaded: false,
}, },
{ {
id: 'qwen3-asr-1.7b', id: 'qwen3-asr-1.7b',
hfRepo: 'Qwen/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, downloadedBytes: 0,
sizeBytes: null,
isDownloaded: false, isDownloaded: false,
}, },
]; ];
@@ -302,6 +317,11 @@ export function deleteLocalAsrModel(modelId: string): Promise<void> {
return invokeOrMock('local_asr_delete_model', { modelId }, () => undefined); return invokeOrMock('local_asr_delete_model', { modelId }, () => undefined);
} }
/** 清理中断下载遗留的 staging 目录,不触碰已安装模型。 */
export function cleanupIncompleteLocalAsrModel(modelId: string): Promise<void> {
return invokeOrMock('local_asr_cleanup_incomplete', { modelId }, () => undefined);
}
export function getLocalAsrModelDir(modelId: string): Promise<string> { export function getLocalAsrModelDir(modelId: string): Promise<string> {
return invokeOrMock('local_asr_model_dir', { modelId }, () => ''); return invokeOrMock('local_asr_model_dir', { modelId }, () => '');
} }
@@ -422,6 +442,7 @@ export type SherpaOnnxModelAlias =
| 'paraformer-zh' | 'paraformer-zh'
| 'whisper-small-multi' | 'whisper-small-multi'
| 'whisper-large-v3-multi' | 'whisper-large-v3-multi'
| 'zipformer-bilingual-zh-en-streaming'
| 'qwen3-asr-0.6b-int8'; | 'qwen3-asr-0.6b-int8';
export type SherpaOnnxMirror = 'huggingface' | 'hf-mirror' | 'github-release'; export type SherpaOnnxMirror = 'huggingface' | 'hf-mirror' | 'github-release';
@@ -470,6 +491,11 @@ export const SHERPA_ONNX_ASR_MODELS: SherpaOnnxModelOption[] = [
labelKey: 'localAsr.sherpaModelWhisperLargeV3', labelKey: 'localAsr.sherpaModelWhisperLargeV3',
descKey: 'localAsr.sherpaModelWhisperLargeV3Desc', descKey: 'localAsr.sherpaModelWhisperLargeV3Desc',
}, },
{
alias: 'zipformer-bilingual-zh-en-streaming',
labelKey: 'localAsr.sherpaModelZipformer',
descKey: 'localAsr.sherpaModelZipformerDesc',
},
{ {
alias: 'qwen3-asr-0.6b-int8', alias: 'qwen3-asr-0.6b-int8',
labelKey: 'localAsr.sherpaModelQwen3', labelKey: 'localAsr.sherpaModelQwen3',
@@ -518,6 +544,13 @@ export function getSherpaOnnxAsrCatalog(): Promise<SherpaOnnxCatalogModel[]> {
downloadedBytes: 0, downloadedBytes: 0,
fileSizeMb: 1700, 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, alias: 'qwen3-asr-0.6b-int8' as const,
displayName: 'Qwen3-ASR 0.6B INT8', displayName: 'Qwen3-ASR 0.6B INT8',