= ({
) : isFireRed ? (
+ ) : isParakeet ? (
+
) : installedOnly && !hasAnyInstalled ? (
{t('noInstalledModels')}
diff --git a/renderer/components/resources/ParakeetModelSection.tsx b/renderer/components/resources/ParakeetModelSection.tsx
new file mode 100644
index 00000000..5de8237b
--- /dev/null
+++ b/renderer/components/resources/ParakeetModelSection.tsx
@@ -0,0 +1,288 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'next-i18next';
+import { Card, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog';
+import { Download, Trash2, X, Mic, Upload } from 'lucide-react';
+import { toast } from 'sonner';
+import DownloadSourcePopover, {
+ type DownloadSourceConfig,
+} from '@/components/resources/engines/DownloadSourcePopover';
+import SherpaModelRow from '@/components/resources/SherpaModelRow';
+import { importModelFromFolder } from 'lib/importModel';
+import { resolveModelDownloadUrl } from 'lib/resolveModelDownloadUrl';
+
+type ParakeetModelId = 'parakeet-tdt-0.6b-v3';
+type ParakeetModelSource = 'ghproxy' | 'github';
+
+const PARAKEET_MODEL_SOURCES: ParakeetModelSource[] = ['ghproxy', 'github'];
+const PARAKEET_SOURCE_STORAGE_KEY = 'parakeetModelDownloadSource';
+
+function readParakeetModelSource(): ParakeetModelSource {
+ if (typeof window === 'undefined') return 'ghproxy';
+ const value = window.localStorage.getItem(PARAKEET_SOURCE_STORAGE_KEY);
+ return value === 'github' || value === 'ghproxy' ? value : 'ghproxy';
+}
+
+interface ParakeetModelStatus {
+ engineInstalled: boolean;
+ vadInstalled: boolean;
+ ready: boolean;
+ models: { id: ParakeetModelId; installed: boolean }[];
+}
+
+const MODEL_ID: ParakeetModelId = 'parakeet-tdt-0.6b-v3';
+const PROGRESS_KEY = `parakeet:${MODEL_ID}`;
+
+const ParakeetModelSection: React.FC<{ onUpdate?: () => void }> = ({
+ onUpdate,
+}) => {
+ const { t } = useTranslation('resources');
+ const { t: commonT } = useTranslation('common');
+ const [status, setStatus] = useState(null);
+ const [progress, setProgress] = useState>({});
+ const [phase, setPhase] = useState>({});
+ const [downloading, setDownloading] = useState(false);
+ const [showConfirm, setShowConfirm] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [source, setSource] = useState('ghproxy');
+
+ useEffect(() => {
+ setSource(readParakeetModelSource());
+ }, []);
+
+ const handleSelectSource = useCallback((next: ParakeetModelSource) => {
+ setSource(next);
+ if (typeof window !== 'undefined') {
+ window.localStorage.setItem(PARAKEET_SOURCE_STORAGE_KEY, next);
+ }
+ }, []);
+
+ const load = useCallback(async () => {
+ try {
+ const result = await window?.ipc?.invoke('getParakeetModelStatus');
+ if (result?.success) setStatus(result as ParakeetModelStatus);
+ } catch {
+ // 保持上次状态
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ const unsubscribe = window?.ipc?.on(
+ 'downloadProgress',
+ (key: string, value: number) => {
+ if (typeof key !== 'string' || !key.startsWith('parakeet:')) return;
+ setProgress((previous) => ({ ...previous, [key]: value }));
+ if (value >= 1) {
+ void load();
+ onUpdate?.();
+ }
+ },
+ );
+ const unsubscribeDetail = window?.ipc?.on(
+ 'modelDownloadDetail',
+ (key: string, detail: { status?: string }) => {
+ if (typeof key !== 'string' || !key.startsWith('parakeet:')) return;
+ setPhase((previous) => ({
+ ...previous,
+ [key]: detail?.status ?? '',
+ }));
+ },
+ );
+ return () => {
+ unsubscribe?.();
+ unsubscribeDetail?.();
+ };
+ }, [load, onUpdate]);
+
+ const installed =
+ status?.models.find((model) => model.id === MODEL_ID)?.installed ?? false;
+
+ const sourceConfig: DownloadSourceConfig = {
+ value: source,
+ options: PARAKEET_MODEL_SOURCES.map((item) => ({
+ value: item,
+ label: t(`engines.parakeet.modelSources.${item}`),
+ })),
+ onChange: (next) => handleSelectSource(next as ParakeetModelSource),
+ label: t('engines.parakeet.downloadSource'),
+ confirmLabel: commonT('startDownload'),
+ hint: t(`engines.parakeet.modelSourceHint.${source}`),
+ getCopyUrl: (next) => resolveModelDownloadUrl('parakeet', next, MODEL_ID),
+ };
+
+ const handleDownload = async () => {
+ setShowConfirm(false);
+ setDownloading(true);
+ try {
+ const result = await window?.ipc?.invoke('downloadParakeetModel', {
+ model: MODEL_ID,
+ source,
+ });
+ if (result?.success) {
+ await load();
+ onUpdate?.();
+ } else {
+ toast.error(
+ result?.error === 'anotherDownloadInProgress'
+ ? t('engines.parakeet.anotherDownload')
+ : result?.error || 'Failed to download model',
+ );
+ }
+ } catch (error) {
+ toast.error(String(error));
+ } finally {
+ setDownloading(false);
+ setProgress((previous) => ({ ...previous, [PROGRESS_KEY]: 0 }));
+ }
+ };
+
+ const handleCancel = async () => {
+ await window?.ipc?.invoke('cancelModelDownload');
+ setDownloading(false);
+ };
+
+ const handleImport = async () => {
+ const outcome = await importModelFromFolder('parakeet', MODEL_ID);
+ if (outcome.kind === 'success') {
+ toast.success(t('importModelSuccess'), { duration: 2000 });
+ await load();
+ onUpdate?.();
+ } else if (outcome.kind === 'invalid-layout') {
+ toast.error(
+ t('importInvalidLayout', { files: outcome.missing.join(', ') }),
+ );
+ } else if (outcome.kind === 'error') {
+ toast.error(t('importModelFailed', { error: outcome.message }));
+ }
+ };
+
+ const handleDelete = async () => {
+ setShowDeleteConfirm(false);
+ const result = await window?.ipc?.invoke('deleteParakeetModel', MODEL_ID);
+ if (result?.success) {
+ await load();
+ onUpdate?.();
+ } else {
+ toast.error(result?.error || 'Failed to delete model');
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t('engines.parakeet.modelsTitle')}
+
+
+
+
+
+
+ {commonT('cancel')}
+
+ ) : installed ? (
+
+ ) : (
+
+
+
+
+
+
+ )
+ }
+ />
+
+
+
+
+
+
+
+ {commonT('confirmDeleteModel')}
+
+ {commonT('deleteModelDesc')}
+
+
+
+
+
+ {commonT('cancel')}
+
+
+
+ {commonT('delete')}
+
+
+
+
+
+ );
+};
+
+export default ParakeetModelSection;
diff --git a/renderer/components/resources/engines/EngineIcon.tsx b/renderer/components/resources/engines/EngineIcon.tsx
index 020619a4..b4982bcb 100644
--- a/renderer/components/resources/engines/EngineIcon.tsx
+++ b/renderer/components/resources/engines/EngineIcon.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import type { TranscriptionEngine } from '../../../../types/engine';
interface EngineIconProps {
- /** 真实引擎 id,或合并展示组 'sherpa'(FunASR · Qwen · FireRed)。 */
+ /** 真实引擎 id,或合并展示组 'sherpa'。 */
engine: TranscriptionEngine | 'sherpa';
className?: string;
}
@@ -13,7 +13,7 @@ interface EngineIconProps {
* - builtin(whisper.cpp,内置本地):芯片内的声波
* - fasterWhisper(主打速度):闪电
* - funasr(阿里达摩院):橙色声波(语音识别)
- * - sherpa(FunASR · Qwen · FireRed 合并组):堆叠的声波层,示意「多模型共用一套运行库」
+ * - sherpa(本地多模型合并组):堆叠的声波层,示意「多模型共用一套运行库」
* - localCli(本地命令行):终端提示符
*/
const EngineIcon: React.FC = ({ engine, className }) => {
@@ -100,6 +100,29 @@ const EngineIcon: React.FC = ({ engine, className }) => {
);
}
+ if (engine === 'parakeet') {
+ return (
+
+ );
+ }
if (engine === 'cloud') {
return (
- {/* 共享运行库卡:三族同一份内置运行库,恒为就绪,只此一处 */}
+ {/* 共享运行库卡:各族同一份内置运行库,恒为就绪,只此一处 */}
{t('engines.sherpa.builtinRuntime')}
@@ -189,7 +191,7 @@ const SherpaEngineGroupPanel: React.FC
= ({
)}
- {/* 三族分区:仅模型清单(复用 ModelLibrarySection 的下载/导入/删除/换路径) */}
+ {/* 各族分区:仅模型清单(复用 ModelLibrarySection 的下载/导入/删除/换路径) */}
{families.map((f, index) => (
= ({
))}
- {/* 合并的高级设置:线程数(三族统一)+ ITN(仅 FunASR) */}
+ {/* 合并的高级设置:线程数(各族统一)+ ITN(仅 FunASR) */}
diff --git a/renderer/components/resources/engines/useSherpaRuntime.ts b/renderer/components/resources/engines/useSherpaRuntime.ts
index 99fc3ab1..f2d412fb 100644
--- a/renderer/components/resources/engines/useSherpaRuntime.ts
+++ b/renderer/components/resources/engines/useSherpaRuntime.ts
@@ -8,7 +8,7 @@ export interface SherpaRuntime {
}
/**
- * FunASR / Qwen / FireRed 共用的 sherpa-onnx 原生运行库已随安装包内置(不再运行时下载)。
+ * 本地 sherpa ASR 引擎共用的原生运行库已随安装包内置(不再运行时下载)。
* 此 hook 仅查询内置状态(installed + 内置版本),供各引擎面板展示「已随应用内置」。
* 状态上提到常驻挂载的父组件(EngineModelTab)统一持有,避免各面板重复查询。
*/
diff --git a/renderer/components/tasks/InlineConfigBar.tsx b/renderer/components/tasks/InlineConfigBar.tsx
index c1704fc6..8a3c7257 100644
--- a/renderer/components/tasks/InlineConfigBar.tsx
+++ b/renderer/components/tasks/InlineConfigBar.tsx
@@ -162,6 +162,9 @@ const InlineConfigBar: React.FC = ({
fireRedVadInstalled={systemInfo?.fireRedVadInstalled}
fireRedModelsInstalled={systemInfo?.fireRedModelsInstalled}
fireRedEngineInstalled={systemInfo?.fireRedEngineInstalled}
+ parakeetVadInstalled={systemInfo?.parakeetVadInstalled}
+ parakeetModelsInstalled={systemInfo?.parakeetModelsInstalled}
+ parakeetEngineInstalled={systemInfo?.parakeetEngineInstalled}
includeLocalCli={includeLocalCli}
/>
) : (
diff --git a/renderer/lib/engineModels.ts b/renderer/lib/engineModels.ts
index 461a9708..0b21eba8 100644
--- a/renderer/lib/engineModels.ts
+++ b/renderer/lib/engineModels.ts
@@ -37,6 +37,12 @@ export interface EngineModelInfo {
fireRedModelsInstalled?: string[];
/** fireRed 运行库(sherpa-onnx,与 funasr 同库)是否已安装 */
fireRedEngineInstalled?: boolean;
+ /** Parakeet 共享 silero VAD 是否就绪 */
+ parakeetVadInstalled?: boolean;
+ /** Parakeet 已安装的模型 id 列表 */
+ parakeetModelsInstalled?: string[];
+ /** Parakeet 运行库(sherpa-onnx,与其它本地 sherpa ASR 共用)是否已安装 */
+ parakeetEngineInstalled?: boolean;
}
/** 解析当前转写引擎,兼容旧的 useLocalWhisper 开关 */
@@ -73,6 +79,9 @@ export function getInstalledModelsForEngine(
if (engine === 'fireRedAsr') {
return info?.fireRedModelsInstalled ?? [];
}
+ if (engine === 'parakeet') {
+ return info?.parakeetModelsInstalled ?? [];
+ }
return info?.modelsInstalled ?? [];
}
@@ -102,6 +111,9 @@ export function getSelectableModelsForEngine(
if (engine === 'fireRedAsr') {
return info?.fireRedModelsInstalled ?? [];
}
+ if (engine === 'parakeet') {
+ return info?.parakeetModelsInstalled ?? [];
+ }
return info?.modelsInstalled ?? [];
}
@@ -129,6 +141,12 @@ export function hasModelsForEngine(
(info?.fireRedModelsInstalled?.length ?? 0) > 0
);
}
+ if (engine === 'parakeet') {
+ return (
+ !!info?.parakeetVadInstalled &&
+ (info?.parakeetModelsInstalled?.length ?? 0) > 0
+ );
+ }
return getInstalledModelsForEngine(info, useLocalWhisper).length > 0;
}
@@ -176,9 +194,7 @@ export function encodeEngineModel(
}
/** 解析分组下拉选项 value 为 (引擎,模型[,云实例]);非法返回 null。 */
-export function decodeEngineModel(
- value: string | undefined,
-): {
+export function decodeEngineModel(value: string | undefined): {
engine: TranscriptionEngine;
model: string;
asrProviderId?: string;
@@ -226,8 +242,8 @@ function isFasterWhisperRunnable(info: EngineModelInfo | undefined): boolean {
* 仅纳入「引擎运行时已安装」的引擎——只下了模型但没装对应引擎不可转写,故从任务选择中过滤掉。
* - builtin: ggml 已装模型(内置运行时,始终可运行)
* - fasterWhisper: ct2 已装模型,且引擎包已安装(`pythonEngineStatus.state==='ready'`)
- * - funasr / qwen / fireRedAsr: 需 VAD 就绪 + 至少一个模型即可。
- * 三族共用的 sherpa-onnx 运行库现随安装包内置(见 sherpaLibPaths / fetch-sherpa-native),
+ * - funasr / qwen / fireRedAsr / parakeet: 需 VAD 就绪 + 至少一个模型即可。
+ * 各族共用的 sherpa-onnx 运行库现随安装包内置(见 sherpaLibPaths / fetch-sherpa-native),
* 不再单独安装,故口径与引擎页(`is*Ready()` = 内置 VAD + 模型)一致,
* 不再附加 `*EngineInstalled` 条件——否则会出现「引擎页显示已就绪、任务页下拉却不列出」的不一致。
* - localCli: 用户自备模型/命令,无"已装模型"概念;仅当 `includeLocalCli` 时以
@@ -263,6 +279,11 @@ export function getEngineModelGroups(
groups.push({ engine: 'fireRedAsr', models: fireRedModels });
}
+ const parakeetModels = info?.parakeetModelsInstalled ?? [];
+ if (info?.parakeetVadInstalled && parakeetModels.length) {
+ groups.push({ engine: 'parakeet', models: parakeetModels });
+ }
+
if (opts?.includeLocalCli) {
groups.push({ engine: 'localCli', models: models.map((m) => m.name) });
}
@@ -287,7 +308,7 @@ export function getEngineModelGroups(
/**
* 跨引擎就绪判断:"任意引擎装有任意可运行模型即视为就绪"。
* 用于新手引导 / 全景概览 / 任务页"去下载模型"引导。
- * 与 getEngineModelGroups 同口径:fw 还需引擎包已安装;funasr/qwen/fireRedAsr 的
+ * 与 getEngineModelGroups 同口径:fw 还需引擎包已安装;本地 sherpa ASR 的
* sherpa-onnx 运行库随包内置(见 getEngineModelGroups 注释),只看内置 VAD + 模型;
* localCli 不计入(自备模型,无可下载模型;其可用性由是否配置命令决定,另行处理)。
*/
@@ -319,6 +340,12 @@ export function hasAnyModelAnyEngine(
) {
return true;
}
+ if (
+ info?.parakeetVadInstalled &&
+ (info?.parakeetModelsInstalled?.length ?? 0) > 0
+ ) {
+ return true;
+ }
return false;
}
diff --git a/renderer/lib/engineViews.ts b/renderer/lib/engineViews.ts
index 1e88710d..eb3f89b0 100644
--- a/renderer/lib/engineViews.ts
+++ b/renderer/lib/engineViews.ts
@@ -3,7 +3,7 @@ import { CLOUD_VIEW_PREFIX } from '../../types/asrProvider';
/**
* 「引擎与模型」左栏视图 id 体系。
*
- * 本地四视图为固定枚举('sherpa' 是 FunASR · Qwen · FireRed 的 UI 合并组);
+ * 本地四视图为固定枚举('sherpa' 是各本地 sherpa ASR 模型族的 UI 合并组);
* 云端听写为逐条目平级入口,视图 id 形如 `cloud:`(品牌/孤儿)、
* `cloud::`(预设槽位)、`cloud::i:`
* (自定义实例),见 types/asrProvider 的 cloudViewId 家族 / buildCloudViews。
diff --git a/renderer/lib/importModel.ts b/renderer/lib/importModel.ts
index f9f38d00..8ca491a3 100644
--- a/renderer/lib/importModel.ts
+++ b/renderer/lib/importModel.ts
@@ -10,7 +10,7 @@ export type ImportOutcome =
| { kind: 'error'; message: string };
export async function importModelFromFolder(
- engine: 'funasr' | 'qwen' | 'fireRedAsr' | 'fasterWhisper',
+ engine: 'funasr' | 'qwen' | 'fireRedAsr' | 'parakeet' | 'fasterWhisper',
modelId: string,
): Promise {
try {
diff --git a/renderer/lib/resolveModelDownloadUrl.ts b/renderer/lib/resolveModelDownloadUrl.ts
index 57a170f5..1dd2dee2 100644
--- a/renderer/lib/resolveModelDownloadUrl.ts
+++ b/renderer/lib/resolveModelDownloadUrl.ts
@@ -5,7 +5,12 @@
* 导致与真实下载链接漂移。解析失败(未知模型/源、IPC 异常)统一返回 null,
* 由调用方(气泡复制按钮)给出失败提示。
*/
-export type ModelUrlScope = 'funasr' | 'qwen' | 'firered' | 'pyEngine';
+export type ModelUrlScope =
+ | 'funasr'
+ | 'qwen'
+ | 'firered'
+ | 'parakeet'
+ | 'pyEngine';
export async function resolveModelDownloadUrl(
scope: ModelUrlScope,
diff --git a/renderer/lib/subtitleOutcome.ts b/renderer/lib/subtitleOutcome.ts
index 29542c0f..ae0ef237 100644
--- a/renderer/lib/subtitleOutcome.ts
+++ b/renderer/lib/subtitleOutcome.ts
@@ -17,7 +17,7 @@ export const SUBTITLE_OUTCOME_TIERS: Exclude[] = [
'clean',
];
-const SHERPA_ENGINES = new Set(['funasr', 'qwen', 'fireRedAsr']);
+const SHERPA_ENGINES = new Set(['funasr', 'qwen', 'fireRedAsr', 'parakeet']);
/** sherpa 系:VAD 结构性常开、无上下文/抗重复概念。 */
export function isSherpaEngine(engine?: string): boolean {
diff --git a/renderer/public/locales/en/common.json b/renderer/public/locales/en/common.json
index 95985615..7a6fb528 100644
--- a/renderer/public/locales/en/common.json
+++ b/renderer/public/locales/en/common.json
@@ -333,6 +333,7 @@
"funasr": "FunASR",
"qwen": "Qwen3-ASR",
"fireRedAsr": "FireRedASR",
+ "parakeet": "Parakeet TDT",
"localCli": "Local command",
"cloud": "Cloud ASR"
}
diff --git a/renderer/public/locales/en/resources.json b/renderer/public/locales/en/resources.json
index 4ddc5bd1..7072782a 100644
--- a/renderer/public/locales/en/resources.json
+++ b/renderer/public/locales/en/resources.json
@@ -83,18 +83,18 @@
},
"sherpa": {
"name": "Local multi-model engine",
- "subtitle": "FunASR · Qwen · FireRed",
- "tags": ["FunASR", "Qwen3-ASR", "FireRedASR"],
- "desc": "FunASR, Qwen3-ASR and FireRedASR share one bundled sherpa-onnx runtime; they differ only in models and a few parameters. Pick and download a model from any family below to start — fully offline on CPU, no GPU or Python required.",
- "builtinRuntime": "The runtime is bundled with the app (shared by FunASR · Qwen · FireRed) — no separate download needed.",
+ "subtitle": "FunASR · Qwen · FireRed · Parakeet",
+ "tags": ["FunASR", "Qwen3-ASR", "FireRedASR", "Parakeet TDT"],
+ "desc": "FunASR, Qwen3-ASR, FireRedASR and NVIDIA Parakeet share one bundled sherpa-onnx runtime; they differ only in models and a few parameters. Pick and download a model from any family below to start — fully offline, no Python required.",
+ "builtinRuntime": "The runtime is bundled with the app and shared by all four model families — no separate download needed.",
"installedVersion": "Bundled v{{version}}",
"needsModels": "Download a model from any family to get started",
"advanced": "Advanced settings",
"numThreads": "Inference threads",
- "numThreadsHint": "Threads used for CPU inference — higher is faster but uses more CPU (shared by all three families)",
+ "numThreadsHint": "Threads used for CPU inference — higher is faster but uses more CPU (shared by all four families)",
"itn": "Inverse text normalization (ITN)",
"itnHint": "Converts spoken numbers, dates, etc. into standard written form and restores punctuation",
- "itnFunasrOnly": "Only supported by FunASR (SenseVoice); Qwen and FireRed handle normalization internally."
+ "itnFunasrOnly": "Only supported by FunASR (SenseVoice); Qwen, FireRed and Parakeet handle text formatting internally."
},
"funasr": {
"name": "FunASR",
@@ -259,6 +259,34 @@
}
}
},
+ "parakeet": {
+ "name": "NVIDIA Parakeet",
+ "builtinRuntime": "The runtime is bundled with the app and shared with the other sherpa ASR engines.",
+ "desc": "NVIDIA Parakeet TDT 0.6B v3 running locally through sherpa-onnx. It supports 25 European languages, punctuation and capitalization. The int8 model runs on CPU without Python; segment timestamps come from VAD.",
+ "scenario": "Best for: English and European-language transcription, CC-BY-4.0",
+ "needsModels": "Models needed",
+ "anotherDownload": "Another download is in progress. Please wait for it to finish.",
+ "downloading": "Downloading…",
+ "extracting": "Extracting… (this can take a while)",
+ "downloadSource": "Download source",
+ "modelsTitle": "Models",
+ "modelDownload": "Download",
+ "modelDelete": "Delete",
+ "modelSources": {
+ "ghproxy": "GitHub Mirror (China)",
+ "github": "GitHub"
+ },
+ "modelSourceHint": {
+ "ghproxy": "Downloads the official sherpa-onnx release through gh-proxy.com, then extracts it",
+ "github": "Direct official sherpa-onnx GitHub release; recommended outside mainland China"
+ },
+ "models": {
+ "parakeet-tdt-0.6b-v3": {
+ "name": "Parakeet TDT 0.6B v3 (int8, ~670MB)",
+ "desc": "25 European languages with punctuation and capitalization; optimized NeMo transducer model for accurate English transcription."
+ }
+ }
+ },
"localCli": {
"name": "Local CLI",
"desc": "Use a self-installed Whisper-compatible command-line tool",
diff --git a/renderer/public/locales/en/tasks.json b/renderer/public/locales/en/tasks.json
index b83ef5a3..00b7b978 100644
--- a/renderer/public/locales/en/tasks.json
+++ b/renderer/public/locales/en/tasks.json
@@ -146,7 +146,7 @@
"title": "Custom",
"desc": "Manually tune context length, VAD, repetition reduction and other low-level parameters.",
"sectionTitle": "Custom parameters",
- "sherpaNote": "For the current engine (FunASR / Qwen / FireRed), VAD is always on and there is no context-length / repetition concept. To fine-tune VAD sensitivity, use the Settings page."
+ "sherpaNote": "For the current engine (FunASR / Qwen / FireRed / Parakeet), VAD is always on and there is no context-length / repetition concept. To fine-tune VAD sensitivity, use the Settings page."
},
"compare": {
"toggle": "See how the effects differ",
diff --git a/renderer/public/locales/zh/common.json b/renderer/public/locales/zh/common.json
index 65a7739e..7d727017 100644
--- a/renderer/public/locales/zh/common.json
+++ b/renderer/public/locales/zh/common.json
@@ -333,6 +333,7 @@
"funasr": "FunASR",
"qwen": "Qwen3-ASR",
"fireRedAsr": "FireRedASR",
+ "parakeet": "Parakeet TDT",
"localCli": "本地命令",
"cloud": "云端听写"
}
diff --git a/renderer/public/locales/zh/resources.json b/renderer/public/locales/zh/resources.json
index 9c2fd0d7..272f862f 100644
--- a/renderer/public/locales/zh/resources.json
+++ b/renderer/public/locales/zh/resources.json
@@ -83,18 +83,18 @@
},
"sherpa": {
"name": "本地多模型引擎",
- "subtitle": "FunASR · Qwen · FireRed",
- "tags": ["FunASR", "Qwen3-ASR", "FireRedASR"],
- "desc": "FunASR、Qwen3-ASR、FireRedASR 共用同一套 sherpa-onnx 本地运行库(已随应用内置),区别只在模型与少量参数。在下方按需选择并下载对应模型即可使用,全程 CPU 离线、无需显卡或 Python。",
- "builtinRuntime": "运行库已随应用内置(FunASR · Qwen · FireRed 共用),无需单独下载。",
+ "subtitle": "FunASR · Qwen · FireRed · Parakeet",
+ "tags": ["FunASR", "Qwen3-ASR", "FireRedASR", "Parakeet TDT"],
+ "desc": "FunASR、Qwen3-ASR、FireRedASR、NVIDIA Parakeet 共用同一套 sherpa-onnx 本地运行库(已随应用内置),区别只在模型与少量参数。在下方按需选择并下载对应模型即可使用,全程离线、无需 Python。",
+ "builtinRuntime": "运行库已随应用内置,由四个模型族共用,无需单独下载。",
"installedVersion": "内置 v{{version}}",
"needsModels": "下载任一族模型即可开始使用",
"advanced": "高级设置",
"numThreads": "推理线程数",
- "numThreadsHint": "CPU 推理使用的线程数,越大越快但占用更多 CPU(三族共用同一运行库,统一生效)",
+ "numThreadsHint": "CPU 推理使用的线程数,越大越快但占用更多 CPU(四族共用同一运行库,统一生效)",
"itn": "逆文本规整(ITN)",
"itnHint": "将口语化的数字、日期等转换为标准书写形式,并自动补全标点",
- "itnFunasrOnly": "仅 FunASR(SenseVoice)支持;Qwen、FireRed 已在内部处理规整。"
+ "itnFunasrOnly": "仅 FunASR(SenseVoice)支持;Qwen、FireRed、Parakeet 已在内部处理文本格式。"
},
"funasr": {
"name": "FunASR",
@@ -259,6 +259,34 @@
}
}
},
+ "parakeet": {
+ "name": "NVIDIA Parakeet",
+ "builtinRuntime": "运行库已随应用内置,并与其它 sherpa ASR 引擎共用。",
+ "desc": "NVIDIA Parakeet TDT 0.6B v3 本地引擎,基于 sherpa-onnx 运行,支持 25 种欧洲语言、自带标点与大小写。int8 模型可在 CPU 上运行,无需 Python;段级时间戳来自 VAD。",
+ "scenario": "适合:英文及欧洲语言高质量转写,CC-BY-4.0",
+ "needsModels": "需下载模型",
+ "anotherDownload": "已有下载任务进行中,请等待完成后再试。",
+ "downloading": "下载中…",
+ "extracting": "解包中…(可能需要一会儿)",
+ "downloadSource": "下载源",
+ "modelsTitle": "模型",
+ "modelDownload": "下载",
+ "modelDelete": "删除",
+ "modelSources": {
+ "ghproxy": "GitHub 国内加速",
+ "github": "GitHub"
+ },
+ "modelSourceHint": {
+ "ghproxy": "经 gh-proxy.com 下载 sherpa-onnx 官方整包,下载后自动解包",
+ "github": "直连 sherpa-onnx 官方 GitHub Release,适合海外网络"
+ },
+ "models": {
+ "parakeet-tdt-0.6b-v3": {
+ "name": "Parakeet TDT 0.6B v3(int8,约 670MB)",
+ "desc": "支持 25 种欧洲语言、自带标点与大小写;NeMo transducer 架构,英文识别准确率高。"
+ }
+ }
+ },
"localCli": {
"name": "本地命令行",
"desc": "使用自行安装的 Whisper 兼容 CLI",
diff --git a/renderer/public/locales/zh/tasks.json b/renderer/public/locales/zh/tasks.json
index 21bba3c6..ef3536f9 100644
--- a/renderer/public/locales/zh/tasks.json
+++ b/renderer/public/locales/zh/tasks.json
@@ -146,7 +146,7 @@
"title": "自定义",
"desc": "手动调整上下文长度、VAD、抗重复等底层参数。",
"sectionTitle": "自定义参数",
- "sherpaNote": "当前引擎(FunASR / Qwen / FireRed)的 VAD 为结构性常开,且无「上下文长度 / 抗重复」概念。如需细调 VAD 灵敏度,请到「设置」页调整。"
+ "sherpaNote": "当前引擎(FunASR / Qwen / FireRed / Parakeet)的 VAD 为结构性常开,且无「上下文长度 / 抗重复」概念。如需细调 VAD 灵敏度,请到「设置」页调整。"
},
"compare": {
"toggle": "看看不同效果的字幕差异",
diff --git a/scripts/fetch-sherpa-native.mjs b/scripts/fetch-sherpa-native.mjs
index 70e6bd18..001d3915 100644
--- a/scripts/fetch-sherpa-native.mjs
+++ b/scripts/fetch-sherpa-native.mjs
@@ -2,7 +2,7 @@
/**
* 构建期拉取 sherpa-onnx 原生库到 extraResources/sherpa/native//。
*
- * funasr / qwen / fireRedAsr 三引擎共用 sherpa-onnx 原生运行库。过去它在运行时下载
+ * 本地 sherpa ASR 引擎共用 sherpa-onnx 原生运行库。过去它在运行时下载
* 到 userData(下载/重签/自检失败面大);现改为**随安装包内置**(像 whisper.cpp 的
* addon.node 一样走 extraResources,asar 内 .node 不可 dlopen 的限制只针对 asar,
* extraResources 不受限)。
@@ -58,7 +58,13 @@ function releaseUrl(asset) {
async function main() {
const platformKey = getPlatformKey();
const asset = assetName(platformKey);
- const outDir = path.join(root, 'extraResources', 'sherpa', 'native', platformKey);
+ const outDir = path.join(
+ root,
+ 'extraResources',
+ 'sherpa',
+ 'native',
+ platformKey,
+ );
const tmp = path.join(os.tmpdir(), asset);
console.log(`Fetching ${asset} ...`);
diff --git a/scripts/test-engine-units.ts b/scripts/test-engine-units.ts
index 0491e109..d5c3262f 100644
--- a/scripts/test-engine-units.ts
+++ b/scripts/test-engine-units.ts
@@ -73,6 +73,7 @@ import {
resolveQwenSelection,
} from '../main/helpers/qwenModelCatalog';
import { FIRERED_MODELS } from '../main/helpers/fireRedModelCatalog';
+import { PARAKEET_MODELS } from '../main/helpers/parakeetModelCatalog';
import {
CT2_REQUIRED_FILES,
CT2_REQUIRED_CONFIG_ARRAYS,
@@ -89,12 +90,19 @@ import {
prepareDownloadTarget,
validateDownloadResponse,
} from '../main/helpers/download/resumeIntegrity';
+import { commitStagedDirectory } from '../main/helpers/download/atomicDirectoryInstall';
+import { DownloadSessionTracker } from '../main/helpers/download/downloadSession';
+import {
+ downloadFileSingle,
+ SINGLE_DOWNLOAD_CANCELLED,
+} from '../main/helpers/download/singleFileDownloader';
import { fetchJson } from '../main/helpers/download/fetchJson';
import {
buildVadConfig,
buildRecognizerConfig,
buildQwenRecognizerConfig,
buildFireRedRecognizerConfig,
+ buildParakeetRecognizerConfig,
segmentTiming,
progressPercent,
} from '../main/helpers/sherpaOnnx/sherpaConfig';
@@ -105,10 +113,13 @@ import {
FIRERED_HARD_MAX_SPEECH_S,
FIRERED_DEFAULT_MAX_SPEECH_S,
} from '../main/helpers/engines/fireRedParams';
+import { buildParakeetParams } from '../main/helpers/engines/parakeetParams';
import {
getSelectableModelsForEngine,
getInstalledModelsForEngine,
+ getEngineModelGroups,
hasModelsForEngine,
+ hasAnyModelAnyEngine,
} from '../renderer/lib/engineModels';
import {
formatFunasrDownloadFailureToast,
@@ -556,6 +567,7 @@ eq(
'funasr',
'qwen',
'fireRedAsr',
+ 'parakeet',
'cloud',
undefined,
].some(supportsFasterWhisperAdvancedParams),
@@ -1379,6 +1391,140 @@ eq(
'sherpa: fire_red_asr has no qwen3Asr block',
);
+// --- engineModels/catalog: NVIDIA Parakeet awareness ---
+const parakeetReady = {
+ transcriptionEngine: 'parakeet' as const,
+ parakeetEngineInstalled: true,
+ parakeetVadInstalled: true,
+ parakeetModelsInstalled: ['parakeet-tdt-0.6b-v3'],
+};
+eq(
+ getSelectableModelsForEngine(parakeetReady),
+ ['parakeet-tdt-0.6b-v3'],
+ 'engineModels: parakeet selectable = installed models',
+);
+eq(
+ getInstalledModelsForEngine(parakeetReady),
+ ['parakeet-tdt-0.6b-v3'],
+ 'engineModels: parakeet installed = installed models',
+);
+eq(
+ hasModelsForEngine(parakeetReady),
+ true,
+ 'engineModels: parakeet ready w/ vad+model',
+);
+eq(
+ getEngineModelGroups(parakeetReady),
+ [
+ {
+ engine: 'parakeet',
+ models: ['parakeet-tdt-0.6b-v3'],
+ },
+ ],
+ 'engineModels: parakeet appears in task model groups',
+);
+eq(
+ hasAnyModelAnyEngine(parakeetReady),
+ true,
+ 'engineModels: parakeet satisfies cross-engine readiness',
+);
+eq(
+ hasModelsForEngine({
+ transcriptionEngine: 'parakeet',
+ parakeetVadInstalled: false,
+ parakeetModelsInstalled: ['parakeet-tdt-0.6b-v3'],
+ }),
+ false,
+ 'engineModels: parakeet not ready without vad',
+);
+eq(
+ hasModelsForEngine({
+ transcriptionEngine: 'parakeet',
+ parakeetVadInstalled: true,
+ parakeetModelsInstalled: [],
+ }),
+ false,
+ 'engineModels: parakeet not ready without model',
+);
+eq(
+ PARAKEET_MODELS['parakeet-tdt-0.6b-v3'].requiredFiles,
+ ['encoder.int8.onnx', 'decoder.int8.onnx', 'joiner.int8.onnx', 'tokens.txt'],
+ 'parakeet: catalog validates the complete int8 transducer layout',
+);
+eq(
+ {
+ upstreamModel: PARAKEET_MODELS['parakeet-tdt-0.6b-v3'].upstreamModel,
+ license: PARAKEET_MODELS['parakeet-tdt-0.6b-v3'].license,
+ languageCount: PARAKEET_MODELS['parakeet-tdt-0.6b-v3'].languageCount,
+ supportsPunctuation:
+ PARAKEET_MODELS['parakeet-tdt-0.6b-v3'].supportsPunctuation,
+ },
+ {
+ upstreamModel: 'nvidia/parakeet-tdt-0.6b-v3',
+ license: 'CC-BY-4.0',
+ languageCount: 25,
+ supportsPunctuation: true,
+ },
+ 'parakeet: catalog exposes upstream capability and license metadata',
+);
+
+const PARAKEET_RP = { num_threads: 4, provider: 'cpu' };
+const parakeetRecognizerConfig = buildParakeetRecognizerConfig(
+ {
+ encoder: '/m/encoder.int8.onnx',
+ decoder: '/m/decoder.int8.onnx',
+ joiner: '/m/joiner.int8.onnx',
+ },
+ '/m/tokens.txt',
+ PARAKEET_RP,
+);
+eq(
+ parakeetRecognizerConfig.modelConfig.transducer,
+ {
+ encoder: '/m/encoder.int8.onnx',
+ decoder: '/m/decoder.int8.onnx',
+ joiner: '/m/joiner.int8.onnx',
+ },
+ 'sherpa: parakeet maps encoder+decoder+joiner',
+);
+eq(
+ parakeetRecognizerConfig.modelConfig.tokens,
+ '/m/tokens.txt',
+ 'sherpa: parakeet uses top-level tokens',
+);
+eq(
+ parakeetRecognizerConfig.modelConfig.modelType,
+ 'nemo_transducer',
+ 'sherpa: parakeet explicitly selects nemo_transducer',
+);
+eq(
+ buildParakeetParams({}),
+ {
+ provider: 'cpu',
+ num_threads: 2,
+ vad_threshold: 0.5,
+ vad_min_silence_duration_ms: 100,
+ vad_min_speech_duration_ms: 250,
+ vad_max_speech_duration_s: 0,
+ },
+ 'parakeet: default params reuse shared VAD defaults',
+);
+eq(
+ buildParakeetParams({
+ parakeetProvider: 'cuda',
+ parakeetNumThreads: 8,
+ }),
+ {
+ provider: 'cuda',
+ num_threads: 8,
+ vad_threshold: 0.5,
+ vad_min_silence_duration_ms: 100,
+ vad_min_speech_duration_ms: 250,
+ vad_max_speech_duration_s: 0,
+ },
+ 'parakeet: provider and threads passthrough',
+);
+
// --- fireRedParams: 默认值 + 段长安全闸(design D8) ---
eq(
buildFireRedParams({}),
@@ -1471,6 +1617,28 @@ eq(
fs.rmSync(tmp, { recursive: true, force: true });
}
+// --- downloadSession: stale finally 不能清掉新会话 ---
+{
+ const tracker = new DownloadSessionTracker();
+ const oldSession = tracker.begin();
+ const newSession = tracker.begin();
+ eq(
+ tracker.finish(oldSession),
+ false,
+ 'download session: stale completion does not own active session',
+ );
+ eq(
+ tracker.owns(newSession),
+ true,
+ 'download session: new session survives stale completion',
+ );
+ eq(
+ tracker.finish(newSession),
+ true,
+ 'download session: active completion clears its own session',
+ );
+}
+
// --- modelImport: resolveOverridePath(覆盖优先/空值回退) ---
eq(
resolveOverridePath('/custom/models', '/default/models'),
@@ -2844,7 +3012,7 @@ eq(
'outcome/fw: clean → reduceRepetition on',
);
- // sherpa(funasr/qwen/fireRedAsr):只映射 VAD 灵敏度,绝不关 VAD / 设 ctx / 抗重复
+ // sherpa:只映射 VAD 灵敏度,绝不关 VAD / 设 ctx / 抗重复
const sherpaAccurate = resolveEffectiveSettings(
{ transcriptionEngine: 'funasr', subtitleOutcome: 'accurate' },
{},
@@ -2890,6 +3058,14 @@ eq(
0.5,
'outcome/sherpa(fireRed): balanced → VAD standard threshold',
);
+ eq(
+ resolveEffectiveSettings(
+ { transcriptionEngine: 'parakeet', subtitleOutcome: 'clean' },
+ {},
+ ).vadThreshold,
+ 0.65,
+ 'outcome/sherpa(parakeet): clean → VAD conservative threshold',
+ );
// custom 档:回读用户底层值(builtin 从 formData.maxContext 取)
eq(
@@ -3062,6 +3238,7 @@ eq(
eq(isSherpaEngineId('funasr'), true, 'isSherpa: funasr');
eq(isSherpaEngineId('qwen'), true, 'isSherpa: qwen');
eq(isSherpaEngineId('fireRedAsr'), true, 'isSherpa: fireRedAsr');
+ eq(isSherpaEngineId('parakeet'), true, 'isSherpa: parakeet');
eq(isSherpaEngineId('builtin'), false, 'isSherpa: builtin no');
eq(isSherpaEngineId('fasterWhisper'), false, 'isSherpa: fasterWhisper no');
eq(
@@ -6178,7 +6355,7 @@ async function runAsyncConcurrencyTests(): Promise {
bAcquired = true;
return r;
});
- const pC = acquireTranscribeSlot('fireRedAsr').then((r) => {
+ const pC = acquireTranscribeSlot('parakeet').then((r) => {
cAcquired = true;
return r;
});
@@ -6249,6 +6426,154 @@ async function runAsyncConcurrencyTests(): Promise {
);
}
+ // 模型目录事务提交:提交失败恢复旧目录,成功后替换旧目录。
+ {
+ const tmp = fs.mkdtempSync(nodePath.join(os.tmpdir(), 'model-atomic-'));
+ const dest = nodePath.join(tmp, 'model');
+ const missingStaged = nodePath.join(tmp, 'missing-stage');
+ const rollbackBackup = nodePath.join(tmp, 'rollback-backup');
+ fs.mkdirSync(dest);
+ fs.writeFileSync(nodePath.join(dest, 'marker.txt'), 'old');
+ let rollbackError: unknown = null;
+ await commitStagedDirectory({
+ stagedDir: missingStaged,
+ destDir: dest,
+ backupDir: rollbackBackup,
+ }).catch((error) => {
+ rollbackError = error;
+ });
+ eq(
+ rollbackError instanceof Error,
+ true,
+ 'atomic install: missing staging fails the commit',
+ );
+ eq(
+ fs.readFileSync(nodePath.join(dest, 'marker.txt'), 'utf8'),
+ 'old',
+ 'atomic install: failed commit restores the existing model',
+ );
+
+ const staged = nodePath.join(tmp, 'valid-stage');
+ const successBackup = nodePath.join(tmp, 'success-backup');
+ fs.mkdirSync(staged);
+ fs.writeFileSync(nodePath.join(staged, 'marker.txt'), 'new');
+ await commitStagedDirectory({
+ stagedDir: staged,
+ destDir: dest,
+ backupDir: successBackup,
+ });
+ eq(
+ fs.readFileSync(nodePath.join(dest, 'marker.txt'), 'utf8'),
+ 'new',
+ 'atomic install: valid staging replaces the existing model',
+ );
+ eq(
+ fs.existsSync(successBackup),
+ false,
+ 'atomic install: successful commit removes the backup',
+ );
+ fs.rmSync(tmp, { recursive: true, force: true });
+ }
+
+ // 单连接下载:完整响应成功;截断、停滞和取消都必须可靠拒绝。
+ {
+ const tmp = fs.mkdtempSync(nodePath.join(os.tmpdir(), 'single-download-'));
+ const server = http.createServer((req, res) => {
+ if (req.url === '/ok') {
+ res.writeHead(200, { 'Content-Length': '5' });
+ res.end('hello');
+ return;
+ }
+ if (req.url === '/truncated') {
+ res.writeHead(200, { 'Content-Length': '10' });
+ res.end('short');
+ return;
+ }
+ if (req.url === '/stall') {
+ res.writeHead(200, { 'Content-Length': '5' });
+ res.flushHeaders();
+ return;
+ }
+ res.writeHead(404);
+ res.end();
+ });
+ const port = await new Promise((resolve, reject) => {
+ server.once('error', reject);
+ server.listen(0, '127.0.0.1', () => {
+ const address = server.address();
+ if (!address || typeof address === 'string') {
+ reject(new Error('test server did not expose a TCP port'));
+ return;
+ }
+ resolve(address.port);
+ });
+ });
+ const base = `http://127.0.0.1:${port}`;
+ try {
+ const okPath = nodePath.join(tmp, 'ok.bin');
+ await downloadFileSingle({
+ url: `${base}/ok`,
+ destPath: okPath,
+ timeoutMs: 100,
+ });
+ eq(
+ fs.readFileSync(okPath, 'utf8'),
+ 'hello',
+ 'single download: complete response closes successfully',
+ );
+
+ let truncatedError: unknown = null;
+ await downloadFileSingle({
+ url: `${base}/truncated`,
+ destPath: nodePath.join(tmp, 'truncated.bin'),
+ timeoutMs: 100,
+ }).catch((error) => {
+ truncatedError = error;
+ });
+ eq(
+ truncatedError instanceof Error,
+ true,
+ 'single download: truncated content-length is rejected',
+ );
+
+ let timeoutError: unknown = null;
+ await downloadFileSingle({
+ url: `${base}/stall`,
+ destPath: nodePath.join(tmp, 'timeout.bin'),
+ timeoutMs: 40,
+ }).catch((error) => {
+ timeoutError = error;
+ });
+ eq(
+ String(timeoutError).includes('timed out'),
+ true,
+ 'single download: stalled response hits the inactivity timeout',
+ );
+
+ const abort = new AbortController();
+ let cancelError: unknown = null;
+ const pending = downloadFileSingle({
+ url: `${base}/stall`,
+ destPath: nodePath.join(tmp, 'cancel.bin'),
+ signal: abort.signal,
+ timeoutMs: 1000,
+ }).catch((error) => {
+ cancelError = error;
+ });
+ setTimeout(() => abort.abort(), 20);
+ await pending;
+ eq(
+ cancelError instanceof Error &&
+ cancelError.message === SINGLE_DOWNLOAD_CANCELLED,
+ true,
+ 'single download: abort rejects with the shared cancellation marker',
+ );
+ } finally {
+ await new Promise((resolve) => server.close(() => resolve()));
+ fs.rmSync(tmp, { recursive: true, force: true });
+ }
+ }
+
// 云端服务商闸:并发上限跨调用共享
{
const gate = getCloudProviderGate('test-provider-a');
diff --git a/scripts/test-storage-paths.ts b/scripts/test-storage-paths.ts
index a7b32904..94090925 100644
--- a/scripts/test-storage-paths.ts
+++ b/scripts/test-storage-paths.ts
@@ -105,6 +105,7 @@ function run(): void {
funasr: ['models', 'funasr'],
qwen: ['models', 'qwen'],
firered: ['models', 'firered'],
+ parakeet: ['models', 'parakeet'],
tts: ['models', 'tts'],
};
(Object.keys(expectedSubpaths) as StorageKind[]).forEach((kind) => {
diff --git a/types/engine.ts b/types/engine.ts
index bedf0fef..1d30b798 100644
--- a/types/engine.ts
+++ b/types/engine.ts
@@ -4,6 +4,7 @@ export type TranscriptionEngine =
| 'funasr'
| 'qwen'
| 'fireRedAsr'
+ | 'parakeet'
| 'localCli'
/** 云端听写(在线 ASR):单一适配器,按 asrProviderId 实例分发到具体 service。 */
| 'cloud';
@@ -79,7 +80,7 @@ export interface RemoteEngineManifest {
runtime?: RemoteRuntimeInfo;
}
-/** 可独立下载的 Python 引擎运行时标识。faster-whisper 是唯一 Python 引擎(funasr/qwen/firered 已改用内置 sherpa-onnx 原生库)。 */
+/** 可独立下载的 Python 引擎运行时标识。faster-whisper 是唯一 Python 引擎(其余本地 ASR 使用内置 sherpa-onnx)。 */
export type PyEngineId = 'faster-whisper';
export interface PyEngineUpdateInfo {
diff --git a/types/types.ts b/types/types.ts
index 3b3b3f43..a744005d 100644
--- a/types/types.ts
+++ b/types/types.ts
@@ -39,6 +39,14 @@ export interface ISystemInfo {
fireRedModelsInstalled?: string[];
/** fireRed 模型根目录(固定路径,仅展示用,不可更改) */
fireRedModelsPath?: string;
+ /** Parakeet 引擎包(sherpa-onnx,与其它本地 sherpa ASR 共用)是否已安装 */
+ parakeetEngineInstalled?: boolean;
+ /** Parakeet 共用 silero VAD 是否已安装 */
+ parakeetVadInstalled?: boolean;
+ /** 已安装的 Parakeet 模型 id */
+ parakeetModelsInstalled?: string[];
+ /** Parakeet 模型根目录 */
+ parakeetModelsPath?: string;
/** userData 默认存储基座(「默认路径含中文」警示判定用) */
userDataPath?: string;
/** 统一存储根目录原始设置值('' = 未设置) */
@@ -50,6 +58,7 @@ export interface ISystemInfo {
funasr: StoragePathSource;
qwen: StoragePathSource;
firered: StoragePathSource;
+ parakeet: StoragePathSource;
};
}