feat(ui): add Foundry local ASR provider

This commit is contained in:
millionart committed 2026-05-06 17:28:52 +08:00
1 parent 05e45b41fb
commit 71240e2c3d
5 files changed
+123 -10

No files matched your search

+2
View File
@@ -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',
+2
View File
@@ -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}} 未下载',
+2
View File
@@ -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}} 未下載',
+36
View File
@@ -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<void> {
export function setLocalAsrKeepLoadedSecs(seconds: number): Promise<void> {
return invokeOrMock('local_asr_set_keep_loaded_secs', { seconds }, () => undefined);
}
export function getFoundryLocalAsrStatus(): Promise<FoundryLocalAsrStatus> {
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<void> {
return invokeOrMock('foundry_local_asr_set_model', { modelAlias }, () => undefined);
}
export function setFoundryLocalAsrLanguageHint(languageHint: string): Promise<void> {
return invokeOrMock(
'foundry_local_asr_set_language_hint',
{ languageHint },
() => undefined,
);
}
export function releaseFoundryLocalAsr(): Promise<void> {
return invokeOrMock('foundry_local_asr_release', undefined, () => undefined);
}
+81 -10
View File
@@ -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')}
</div>
</>
) : committedAsrProvider === 'local-qwen3' ? (
<LocalAsrProviderHint />
) : committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' ? (
<LocalAsrProviderHint provider={committedAsrProvider} selectedProvider={asrProvider} />
) : (
<>
<CredentialField key={`${committedAsrProvider}:api_key`} label={t('settings.providers.apiKeyLabel')} account="asr.api_key" mono mask />
@@ -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<LocalAsrSettings | null>(null);
const [models, setModels] = useState<LocalAsrModelStatus[]>([]);
const [loading, setLoading] = useState(true);
const [deletingId, setDeletingId] = useState<string | null>(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 (
<div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--ol-ink-4)' }}>
@@ -1319,6 +1375,21 @@ function LocalAsrProviderHint() {
const isReady = active?.isDownloaded ?? false;
const downloaded = models.filter(m => m.isDownloaded);
if (provider === 'foundry-local-whisper') {
return (
<div style={{ padding: '8px 0 4px', display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-3)', lineHeight: 1.6 }}>
{t(hintKey)}
</div>
<div>
<Btn variant="ghost" size="sm" onClick={goToLocalAsr}>
{t('settings.providers.localAsrManage')}
</Btn>
</div>
</div>
);
}
return (
<div style={{ padding: '8px 0 4px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 性能/质量预期警告 —— 用户硬要求要写清楚 */}
@@ -1335,7 +1406,7 @@ function LocalAsrProviderHint() {
</div>
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-3)', lineHeight: 1.6 }}>
{t('settings.providers.localAsrHint')}
{t(hintKey)}
</div>
{/* 当前激活模型状态 + 跳转按钮 */}