From 71240e2c3d68e5c7482dfe80a2f6fe157916d9f1 Mon Sep 17 00:00:00 2001 From: millionart Date: Wed, 6 May 2026 17:14:10 +0800 Subject: [PATCH] feat(ui): add Foundry local ASR provider --- openless-all/app/src/i18n/en.ts | 2 + openless-all/app/src/i18n/zh-CN.ts | 2 + openless-all/app/src/i18n/zh-TW.ts | 2 + openless-all/app/src/lib/localAsr.ts | 36 ++++++++++ openless-all/app/src/pages/Settings.tsx | 91 ++++++++++++++++++++++--- 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 0d8bfd98..7b3be613 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -305,6 +305,7 @@ export const en: typeof zhCN = { asrZhipu: 'Zhipu GLM-ASR', asrGroq: 'Groq Whisper-large-v3', asrWhisper: 'OpenAI Whisper (compatible)', + asrFoundryLocalWhisper: 'Local Whisper (Foundry Local)', asrLocalQwen3: 'Local Qwen3-ASR', }, volcengineAppKeyLabel: 'APP ID', @@ -312,6 +313,7 @@ export const en: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key is not required right now. Resource ID defaults to volc.bigasr.sauc.duration.', localAsrHint: 'Local Qwen3-ASR runs entirely on this machine. No API key needed — just download the model from HuggingFace.', + foundryLocalAsrHint: 'Windows local Whisper runs on this device and does not need an ASR API key. First use downloads Foundry Local runtime components and a Whisper model; LLM polishing still uses your configured LLM provider.', localAsrPerformanceWarning: 'Local inference runs on CPU + Apple Silicon Accelerate; each transcription takes **several seconds longer than cloud ASR**, and Chinese / dialect accuracy is **typically lower** than Volcengine or Whisper turbo. Use it for offline, privacy-sensitive, or no-cloud-API scenarios.', localAsrReady: '{{model}} downloaded', localAsrNotReady: '{{model}} not downloaded', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 93743926..fd1a32d8 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -303,6 +303,7 @@ export const zhCN = { asrZhipu: '智谱 GLM-ASR', asrGroq: 'Groq Whisper-large-v3', asrWhisper: 'OpenAI Whisper(兼容)', + asrFoundryLocalWhisper: '本地 Whisper(Foundry Local)', asrLocalQwen3: '本地 Qwen3-ASR', }, volcengineAppKeyLabel: 'APP ID', @@ -310,6 +311,7 @@ export const zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key 当前无需填写。Resource ID 默认使用 volc.bigasr.sauc.duration。', localAsrHint: '本地 Qwen3-ASR 在本机运行,无需 API Key。模型从 HuggingFace 下载到本地后即可使用。', + foundryLocalAsrHint: 'Windows 本地 Whisper 在本机运行,无需 ASR API Key。首次使用会下载 Foundry Local 运行组件和 Whisper 模型;LLM 润色仍按你配置的 LLM 提供商调用。', localAsrPerformanceWarning: '本地推理跑在 CPU + Apple Silicon Accelerate 上,单次转写时间会**比云端 ASR 长几秒**;中文识别准确率与方言/口音表现也**通常不如**火山引擎 / Whisper turbo。请按需取舍:网络受限或对隐私敏感时再用本地。', localAsrReady: '{{model}} 已下载', localAsrNotReady: '{{model}} 未下载', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 63b0ca84..270db07f 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -305,6 +305,7 @@ export const zhTW: typeof zhCN = { asrZhipu: '智譜 GLM-ASR', asrGroq: 'Groq Whisper-large-v3', asrWhisper: 'OpenAI Whisper(兼容)', + asrFoundryLocalWhisper: '本地 Whisper(Foundry Local)', asrLocalQwen3: '本地 Qwen3-ASR', }, volcengineAppKeyLabel: 'APP ID', @@ -312,6 +313,7 @@ export const zhTW: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key 當前無需填寫。Resource ID 默認使用 volc.bigasr.sauc.duration。', localAsrHint: '本地 Qwen3-ASR 在本機運行,無需 API Key。模型從 HuggingFace 下載到本地後即可使用。', + foundryLocalAsrHint: 'Windows 本地 Whisper 在本機運行,無需 ASR API Key。首次使用會下載 Foundry Local 運行組件和 Whisper 模型;LLM 潤色仍按你配置的 LLM 提供商調用。', localAsrPerformanceWarning: '本地推理跑在 CPU + Apple Silicon Accelerate 上,**首次轉寫需要加載模型(數秒)**,之後單次轉寫也會比雲端 ASR 慢若干秒;中文識別準確率與方言/口音表現通常不如火山引擎 / Whisper turbo。適用場景:離線 / 隱私敏感 / 不願付費雲 API。', localAsrReady: '{{model}} 已下載', localAsrNotReady: '{{model}} 未下載', diff --git a/openless-all/app/src/lib/localAsr.ts b/openless-all/app/src/lib/localAsr.ts index ceacc19c..741323c7 100644 --- a/openless-all/app/src/lib/localAsr.ts +++ b/openless-all/app/src/lib/localAsr.ts @@ -55,6 +55,15 @@ export interface LocalAsrDownloadProgress { error: string | null; } +export interface FoundryLocalAsrStatus { + providerId: string; + available: boolean; + activeModel: string; + loadedModelId: string | null; + endpoint: string | null; + error: string | null; +} + const MOCK_SETTINGS: LocalAsrSettings = { providerId: 'local-qwen3', activeModel: 'qwen3-asr-0.6b', @@ -175,3 +184,30 @@ export function preloadLocalAsr(): Promise { export function setLocalAsrKeepLoadedSecs(seconds: number): Promise { return invokeOrMock('local_asr_set_keep_loaded_secs', { seconds }, () => undefined); } + +export function getFoundryLocalAsrStatus(): Promise { + return invokeOrMock('foundry_local_asr_status', undefined, () => ({ + providerId: 'foundry-local-whisper', + available: true, + activeModel: 'whisper-small', + loadedModelId: null, + endpoint: null, + error: null, + })); +} + +export function setFoundryLocalAsrModel(modelAlias: string): Promise { + return invokeOrMock('foundry_local_asr_set_model', { modelAlias }, () => undefined); +} + +export function setFoundryLocalAsrLanguageHint(languageHint: string): Promise { + return invokeOrMock( + 'foundry_local_asr_set_language_hint', + { languageHint }, + () => undefined, + ); +} + +export function releaseFoundryLocalAsr(): Promise { + return invokeOrMock('foundry_local_asr_release', undefined, () => undefined); +} diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 2fdd0a82..946e15d4 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -402,6 +402,7 @@ const ASR_PRESETS = [ { id: 'zhipu', nameKey: 'asrZhipu', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-asr-2512' }, { id: 'groq', nameKey: 'asrGroq', baseUrl: 'https://api.groq.com/openai/v1', model: 'whisper-large-v3-turbo' }, { id: 'whisper', nameKey: 'asrWhisper', baseUrl: 'https://api.openai.com/v1', model: 'whisper-1' }, + { id: 'foundry-local-whisper', nameKey: 'asrFoundryLocalWhisper', baseUrl: '', model: '' }, // 本地 Qwen3-ASR:无 baseUrl/model 配置,模型在「模型设置」页下载与切换。 { id: 'local-qwen3', nameKey: 'asrLocalQwen3', baseUrl: '', model: '' }, ] as const; @@ -576,8 +577,8 @@ function ProvidersSection() { {t('settings.providers.volcengineMappingNote')} - ) : committedAsrProvider === 'local-qwen3' ? ( - + ) : committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' ? ( + ) : ( <> @@ -1268,45 +1269,100 @@ function adapterDisplayName(adapter: HotkeyCapability['adapter'] | HotkeyStatus[ /// 本地 Qwen3-ASR 在 Settings → 服务商区里**不**让用户填空——展示当前激活模型 /// 是否已下载、列出所有已下载模型 + 删除按钮,并提示性能/质量预期,引导跳到 /// 「模型设置」页做下载。 -function LocalAsrProviderHint() { +function LocalAsrProviderHint({ + provider, + selectedProvider, +}: { + provider: 'local-qwen3' | 'foundry-local-whisper'; + selectedProvider: AsrPresetId; +}) { const { t } = useTranslation(); const [settings, setSettings] = useState(null); const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); + const refreshSeqRef = useRef(0); + const providerStateRef = useRef({ provider, selectedProvider }); + providerStateRef.current = { provider, selectedProvider }; - const refresh = async () => { + const qwenReadyForFetch = () => { + const state = providerStateRef.current; + return state.provider === 'local-qwen3' && state.selectedProvider === 'local-qwen3'; + }; + + const refresh = async (seq: number) => { try { const [s, list] = await Promise.all([getLocalAsrSettings(), listLocalAsrModels()]); + if (seq !== refreshSeqRef.current) { + return; + } setSettings(s); setModels(list); } catch (err) { + if (seq !== refreshSeqRef.current) { + return; + } console.warn('[settings] load local asr status failed', err); } finally { - setLoading(false); + if (seq === refreshSeqRef.current) { + setLoading(false); + } } }; + const beginRefresh = () => { + const seq = ++refreshSeqRef.current; + setSettings(null); + setModels([]); + setDeletingId(null); + if (provider !== selectedProvider) { + setLoading(true); + return; + } + if (provider === 'foundry-local-whisper') { + setLoading(false); + return; + } + setLoading(true); + void refresh(seq); + }; + useEffect(() => { - void refresh(); - }, []); + beginRefresh(); + return () => { + refreshSeqRef.current += 1; + }; + }, [provider, selectedProvider]); const goToLocalAsr = () => { window.dispatchEvent(new CustomEvent(NAVIGATE_LOCAL_ASR_EVENT)); }; const handleDelete = async (modelId: string) => { + const seq = refreshSeqRef.current; + if (!qwenReadyForFetch()) { + return; + } setDeletingId(modelId); try { await deleteLocalAsrModel(modelId); - await refresh(); + if (seq !== refreshSeqRef.current || !qwenReadyForFetch()) { + return; + } + beginRefresh(); } catch (err) { console.warn('[settings] delete local model failed', err); } finally { - setDeletingId(null); + if (seq === refreshSeqRef.current && provider === 'local-qwen3') { + setDeletingId(null); + } } }; + const hintKey = provider === 'foundry-local-whisper' + ? 'settings.providers.foundryLocalAsrHint' + : 'settings.providers.localAsrHint'; + if (loading) { return (
@@ -1319,6 +1375,21 @@ function LocalAsrProviderHint() { const isReady = active?.isDownloaded ?? false; const downloaded = models.filter(m => m.isDownloaded); + if (provider === 'foundry-local-whisper') { + return ( +
+
+ {t(hintKey)} +
+
+ + {t('settings.providers.localAsrManage')} + +
+
+ ); + } + return (
{/* 性能/质量预期警告 —— 用户硬要求要写清楚 */} @@ -1335,7 +1406,7 @@ function LocalAsrProviderHint() {
- {t('settings.providers.localAsrHint')} + {t(hintKey)}
{/* 当前激活模型状态 + 跳转按钮 */}