feat(ui): add Foundry local ASR provider
This commit is contained in:
1 parent
05e45b41fb
commit
71240e2c3d
5 files changed
+123
-10
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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}} 未下载',
|
||||
|
||||
@@ -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}} 未下載',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
{/* 当前激活模型状态 + 跳转按钮 */}
|
||||
|
||||
Reference in new issue
Block a user