feat(settings): hide local ASR behind new Advanced tab + confirm dialog (#400)
* feat(settings): hide local ASR behind new Advanced tab + system confirm dialog
Move local ASR options (local-qwen3 / foundry-local-whisper) out of
the main Providers → ASR dropdown into a new "Advanced" Settings tab,
so first-time users can't accidentally enable CPU inference and get
slower / less-accurate transcription than they expected. Enabling
local ASR now requires reading a strong yellow/amber warning and
confirming a system-level dialog (@tauri-apps/plugin-dialog ask()
with kind:'warning') — the in-page modal pattern wasn't loud enough.
Behavior changes:
- visibleAsrPresets now excludes local-qwen3 and foundry-local-whisper
(the dropdown is cloud-only)
- useEffect lookup uses ASR_PRESETS (all) so committedAsrProvider can
still resolve to a local provider when one is active
- When committedAsrProvider is local, the ASR Card replaces the
dropdown with a plain notice ("Local ASR (X) is currently active —
switch / disable in the Advanced tab"); existing
LocalAsrProviderHint rendering for the Card body is preserved
- New AdvancedSection component:
- Strong warning block (amber background + amber border + amber text,
fontWeight 500, ⚠️ icon) — meets user's "yellow/red obvious" ask
- Per-platform engine selector:
macOS : Qwen3-ASR only
Windows: Qwen3-ASR + Foundry Local Whisper
Linux : "Local ASR model integration is not supported on this platform"
- Enable buttons trigger Tauri ask() confirm dialog before switching
- When already on local: shows a "Disable local ASR" button
(switches to volcengine, no confirm needed) plus the existing
LocalAsrProviderHint for model status / download navigation
- SettingsSectionId gains 'advanced'; SECTION_ORDER appends it after
language so it sits last in the sidebar (matches macOS conventions
for Advanced/Experimental panes)
i18n: 5 locales (zh-CN, zh-TW, en, ja, ko) gain
- settings.sections.advanced
- settings.providers.localAsrActiveNotice
- settings.advanced.{localAsrTitle, localAsrDesc, localAsrWarningStrong,
qwen3Desc, foundryDesc, enable, alreadyActive, disableLocalLabel,
disableLocalDesc, disable, platformNotSupported,
confirmEnableLocalTitle, confirmEnableLocalBody}
Test plan:
- npm run build (tsc + vite) — green; the typeof zhCN constraint
catches any locale that drifted out of shape
- Manual: switch Settings tab to "Advanced", verify warning is
amber-on-amber and clearly visible, click "Enable" on Qwen3-ASR,
verify a system dialog (not a React modal) appears with kind:warning
iconography, cancel — verify nothing changed; confirm — verify the
Providers tab now shows "Local ASR is currently active" instead of
the dropdown
- Manual on macOS: confirm Foundry option does NOT appear
- Manual on Linux (if available): confirm "platform not supported"
message renders
* feat(advanced): inline LocalAsr model UI + page-top confirm popup + concise warning
User feedback: the standalone "Models" page (LocalAsr.tsx as a main
nav tab) was redundant once Advanced settings exists, the inline
amber warning was a wall of text, and clicking Enable triggered an
OS-native dialog that didn't feel like part of the app. Refactor
the whole local-ASR surface around what users actually need:
- "Models" main-nav tab removed (FloatingShell + NAVIGATE_LOCAL_ASR_EVENT
listener gone). Model management is now exclusively reachable via
Settings → Advanced.
- LocalAsr.tsx gains an `embedded` prop. When embedded, the outer
page padding/height, PageHeader, and standalone warning Card are
skipped — the host (AdvancedSection) supplies the chrome.
- AdvancedSection no longer calls @tauri-apps/plugin-dialog ask().
Enabling local ASR sets pendingTarget which renders a single
amber-bordered Card at the very top of the section with
Confirm / Cancel buttons. One box, in-app, dismissable. Disable
is direct (no confirm needed — going back to cloud is safe).
- Persistent inline warning shrunk to one line. The longer wording
(including the new "may drop words on under-spec hardware" risk
the user asked for) lives in the popup body so it's only seen
when actually consequential.
- ProvidersSection ASR Card body returns null when active is
local — the dropdown row's localAsrActiveNotice already points
to Advanced, no need to duplicate manage UI here.
i18n (5 locales): localAsrWarningStrong replaced by
localAsrWarningShort; confirmEnableLocalBody / Title shortened;
new `confirm` key for the popup CTA.
LocalAsrProviderHint and NAVIGATE_LOCAL_ASR_EVENT in Settings.tsx
are now unused but left in place to keep the diff focused; will
clean up in a follow-up commit if pr_agent flags them.
Test plan:
- npm run build (tsc + vite) — green
- Manual on macOS: Settings → Advanced shows one short amber line +
Enable Qwen3-ASR button + LocalAsr model UI inline. Click Enable
→ page-top popup appears (no OS dialog), with Confirm / Cancel.
Cancel → no change. Confirm → switches; Disable button appears,
still single-click no confirm.
- Manual: Providers → ASR with active=local-qwen3 shows only the
"Local ASR is currently active — switch in Advanced" notice,
no manage UI.
* fix(advanced): pr_agent #400 follow-up — keep disable accessible on unsupported platform
If a user enables local-qwen3 (or foundry-local-whisper) on a machine
that supports it, then their preferences sync to a Linux/unsupported
machine, the previous Advanced UI rendered only the
"platformNotSupported" message and hid the disable button — they had
no in-app path to switch back to cloud and dictation would fail
silently on every press.
Move the disable SettingRow out of the platformSupported branch.
It's gated only on isOnAnyLocal now, so:
- Supported platform + on local → enable Qwen3/Foundry rows + disable row
- Supported platform + on cloud → enable rows only
- Unsupported platform + on local → "not supported" notice + disable row
- Unsupported platform + on cloud → "not supported" notice only
This is the pr_agent advisory from PR #400 review @ d0a425a1.
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
a47e27e2c5
commit
ce21d99f05
8 files changed
+349
-42
No files matched your search
@@ -15,7 +15,8 @@ import { Vocab } from '../pages/Vocab';
|
||||
import { Style } from '../pages/Style';
|
||||
import { Translation } from '../pages/Translation';
|
||||
import { SelectionAsk } from '../pages/SelectionAsk';
|
||||
import { LocalAsr } from '../pages/LocalAsr';
|
||||
// LocalAsr 不再作为主 nav tab——本地 ASR 模型管理已合并到 Settings → Advanced 中
|
||||
// 通过 <LocalAsr embedded /> 渲染。这里之前的 import 与 NAV_BASE 条目都已移除。
|
||||
import { APP_VERSION_LABEL, IS_BETA_BUILD } from '../lib/appVersion';
|
||||
import {
|
||||
HOTKEY_MODE_MIGRATION_ACK_KEY,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
PROVIDER_SETUP_PROMPT_DEFERRED_KEY,
|
||||
shouldShowProviderSetupPrompt,
|
||||
} from '../lib/providerSetup';
|
||||
import { NAVIGATE_LOCAL_ASR_EVENT, type SettingsSectionId } from '../pages/Settings';
|
||||
import { type SettingsSectionId } from '../pages/Settings';
|
||||
import { useAppState, type AppTab } from '../state/useAppState';
|
||||
|
||||
interface NavItem {
|
||||
@@ -45,7 +46,6 @@ const NAV_BASE: Array<Omit<NavItem, 'name'>> = [
|
||||
{ id: 'style', icon: 'style', cmp: Style },
|
||||
{ id: 'translation', icon: 'translate', cmp: Translation },
|
||||
{ id: 'selectionAsk', icon: 'selectionAsk', cmp: SelectionAsk },
|
||||
{ id: 'localAsr', icon: 'archive', cmp: LocalAsr },
|
||||
];
|
||||
|
||||
interface FloatingShellProps {
|
||||
@@ -128,15 +128,9 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Settings → ASR 选 local-qwen3 时的"前往模型设置"事件 → 关 modal + 切 tab。
|
||||
useEffect(() => {
|
||||
const onNavigate = () => {
|
||||
setSettingsOpen(false);
|
||||
setCurrentTab('localAsr');
|
||||
};
|
||||
window.addEventListener(NAVIGATE_LOCAL_ASR_EVENT, onNavigate);
|
||||
return () => window.removeEventListener(NAVIGATE_LOCAL_ASR_EVENT, onNavigate);
|
||||
}, [setCurrentTab, setSettingsOpen]);
|
||||
// 之前监听的 NAVIGATE_LOCAL_ASR_EVENT 已无意义——「模型设置」独立 tab 已下线,
|
||||
// 模型管理 UI 现在通过 Settings → Advanced 的 <LocalAsr embedded /> 渲染,
|
||||
// 用户在 Settings 内即可一站式管理,无需跨页跳转。
|
||||
|
||||
const rememberProviderPrompt = () => {
|
||||
window.sessionStorage.setItem(PROVIDER_SETUP_PROMPT_DEFERRED_KEY, '1');
|
||||
|
||||
@@ -294,6 +294,7 @@ export const en: typeof zhCN = {
|
||||
shortcuts: 'Shortcuts',
|
||||
permissions: 'Permissions',
|
||||
language: 'Language',
|
||||
advanced: 'Advanced',
|
||||
about: 'About',
|
||||
},
|
||||
recording: {
|
||||
@@ -381,6 +382,7 @@ export const en: typeof zhCN = {
|
||||
volcengineAccessKeyLabel: 'Access Token',
|
||||
volcengineResourceIdLabel: 'Resource ID',
|
||||
volcengineMappingNote: 'Secret Key is not required right now. Resource ID defaults to volc.bigasr.sauc.duration.',
|
||||
localAsrActiveNotice: 'Local ASR ({{name}}) is currently active. Switch or disable it from the Advanced tab.',
|
||||
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.',
|
||||
@@ -469,6 +471,22 @@ export const en: typeof zhCN = {
|
||||
notWindows: 'Only available on Windows.',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
localAsrTitle: 'Local ASR models (experimental)',
|
||||
localAsrDesc: 'Move transcription from cloud ASR to on-device inference. Offline / privacy-sensitive use only.',
|
||||
localAsrWarningShort: 'Local ASR is several seconds slower than cloud, typically less accurate, and on under-spec hardware can drop words (output only part of your speech).',
|
||||
qwen3Desc: 'Alibaba Qwen3-ASR, cross-platform. Models download from HuggingFace.',
|
||||
foundryDesc: 'Microsoft Foundry Local Whisper. Windows only.',
|
||||
enable: 'Enable',
|
||||
alreadyActive: 'Active',
|
||||
disableLocalLabel: 'Disable local ASR',
|
||||
disableLocalDesc: 'Switch back to cloud ASR (defaults to Volcengine bigasr).',
|
||||
disable: 'Disable',
|
||||
platformNotSupported: 'Local ASR model integration is not supported on this platform.',
|
||||
confirmEnableLocalTitle: 'Enable local ASR?',
|
||||
confirmEnableLocalBody: 'Enabling "{{target}}" makes transcription several seconds slower than cloud and typically less accurate. On under-spec hardware it can drop words (output only part of your speech).',
|
||||
confirm: 'Enable',
|
||||
},
|
||||
language: {
|
||||
title: 'Interface language',
|
||||
desc: 'Switch the UI language. Applies to the current session immediately and persists across launches.',
|
||||
|
||||
@@ -296,6 +296,7 @@ export const ja: typeof zhCN = {
|
||||
shortcuts: 'ショートカット',
|
||||
permissions: '権限',
|
||||
language: '言語',
|
||||
advanced: '詳細設定',
|
||||
about: '情報',
|
||||
},
|
||||
recording: {
|
||||
@@ -383,6 +384,7 @@ export const ja: typeof zhCN = {
|
||||
volcengineAccessKeyLabel: 'Access Token',
|
||||
volcengineResourceIdLabel: 'Resource ID',
|
||||
volcengineMappingNote: 'Secret Key は現在不要。Resource ID のデフォルトは volc.bigasr.sauc.duration。',
|
||||
localAsrActiveNotice: '現在「{{name}}」を使用中。「詳細設定」タブから切り替えまたは無効化できます。',
|
||||
localAsrHint: 'ローカル Qwen3-ASR は本機で実行されるため API Key 不要。HuggingFace からモデルをダウンロードすればすぐに利用できます。',
|
||||
foundryLocalAsrHint: 'Windows ローカル Whisper は本機で実行され、ASR API Key は不要です。初回使用時に Foundry Local ランタイムコンポーネントと Whisper モデルをダウンロードします。LLM 整文は引き続き設定済みの LLM プロバイダーを使用します。',
|
||||
localAsrPerformanceWarning: 'ローカル推論は CPU + Apple Silicon Accelerate で動作するため、1 回の転写時間は **クラウド ASR より数秒長くなります**。中国語認識精度や方言/訛り対応も **通常は** Volcengine / Whisper turbo に劣ります。ネットワーク制限下またはプライバシー重視の場合に選択してください。',
|
||||
@@ -471,6 +473,22 @@ export const ja: typeof zhCN = {
|
||||
notWindows: 'Windows のみ利用可能。',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
localAsrTitle: 'ローカル ASR モデル(実験的)',
|
||||
localAsrDesc: '転写をクラウドから本機推論に切り替えます。オフライン/プライバシー重視向け。',
|
||||
localAsrWarningShort: 'ローカル ASR はクラウドより数秒遅く、精度も通常は低め。スペック不足のマシンでは欠字(音声の一部のみ出力)が起きる場合があります。',
|
||||
qwen3Desc: 'Alibaba Qwen3-ASR、クロスプラットフォーム。モデルは HuggingFace からダウンロード。',
|
||||
foundryDesc: 'Microsoft Foundry Local Whisper、Windows のみ。',
|
||||
enable: '有効化',
|
||||
alreadyActive: '有効',
|
||||
disableLocalLabel: 'ローカル ASR を無効化',
|
||||
disableLocalDesc: 'クラウド ASR(既定は Volcengine bigasr)に戻します。',
|
||||
disable: '無効化',
|
||||
platformNotSupported: 'このプラットフォームではローカル ASR モデル統合に対応していません。',
|
||||
confirmEnableLocalTitle: 'ローカル ASR を有効化しますか?',
|
||||
confirmEnableLocalBody: '「{{target}}」を有効化すると、転写はクラウドより数秒遅くなり、精度も通常は低くなります。スペック不足のマシンでは欠字(音声の一部のみ出力)が起きる場合があります。',
|
||||
confirm: '有効化する',
|
||||
},
|
||||
language: {
|
||||
title: '表示言語',
|
||||
desc: 'UI の表示言語を切り替えます。現在のセッションに即時反映され、次回起動時も維持されます。',
|
||||
|
||||
@@ -296,6 +296,7 @@ export const ko: typeof zhCN = {
|
||||
shortcuts: '단축키',
|
||||
permissions: '권한',
|
||||
language: '언어',
|
||||
advanced: '고급',
|
||||
about: '정보',
|
||||
},
|
||||
recording: {
|
||||
@@ -383,6 +384,7 @@ export const ko: typeof zhCN = {
|
||||
volcengineAccessKeyLabel: 'Access Token',
|
||||
volcengineResourceIdLabel: 'Resource ID',
|
||||
volcengineMappingNote: 'Secret Key 는 현재 입력 불필요. Resource ID 기본값은 volc.bigasr.sauc.duration.',
|
||||
localAsrActiveNotice: '현재 "{{name}}" 사용 중. "고급" 탭에서 전환 또는 비활성화할 수 있습니다.',
|
||||
localAsrHint: '로컬 Qwen3-ASR 은 본 기기에서 실행되며 API Key 가 필요 없습니다. HuggingFace 에서 모델을 로컬로 다운로드하면 즉시 사용 가능합니다.',
|
||||
foundryLocalAsrHint: 'Windows 로컬 Whisper 는 이 기기에서 실행되며 ASR API Key 가 필요 없습니다. 첫 사용 시 Foundry Local 런타임 구성 요소와 Whisper 모델을 다운로드합니다. LLM 정리는 계속 설정된 LLM 공급자를 사용합니다.',
|
||||
localAsrPerformanceWarning: '로컬 추론은 CPU + Apple Silicon Accelerate 에서 동작하므로, 한 번의 전사 시간이 **클라우드 ASR 보다 몇 초 더 걸립니다**. 중국어 인식 정확도와 방언/억양 대응도 **일반적으로** Volcengine / Whisper turbo 에 미치지 못합니다. 네트워크 제한 또는 프라이버시가 중요한 경우에 사용하세요.',
|
||||
@@ -471,6 +473,22 @@ export const ko: typeof zhCN = {
|
||||
notWindows: 'Windows 만 사용 가능.',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
localAsrTitle: '로컬 ASR 모델 (실험적)',
|
||||
localAsrDesc: '전사를 클라우드에서 로컬 추론으로 전환합니다. 오프라인 / 프라이버시용에만 권장됩니다.',
|
||||
localAsrWarningShort: '로컬 ASR 은 클라우드보다 몇 초 느리고 정확도도 일반적으로 낮습니다. 사양이 부족한 기기에서는 글자 누락(음성의 일부만 출력)이 발생할 수 있습니다.',
|
||||
qwen3Desc: 'Alibaba Qwen3-ASR, 크로스 플랫폼. 모델은 HuggingFace 에서 다운로드.',
|
||||
foundryDesc: 'Microsoft Foundry Local Whisper, Windows 전용.',
|
||||
enable: '활성화',
|
||||
alreadyActive: '활성',
|
||||
disableLocalLabel: '로컬 ASR 비활성화',
|
||||
disableLocalDesc: '클라우드 ASR (기본 Volcengine bigasr) 로 돌아갑니다.',
|
||||
disable: '비활성화',
|
||||
platformNotSupported: '이 플랫폼에서는 로컬 ASR 모델 통합이 아직 지원되지 않습니다.',
|
||||
confirmEnableLocalTitle: '로컬 ASR 을 활성화할까요?',
|
||||
confirmEnableLocalBody: '"{{target}}" 을 활성화하면 전사가 클라우드보다 몇 초 느려지고 정확도도 일반적으로 낮아집니다. 사양이 부족한 기기에서는 글자 누락(음성의 일부만 출력)이 발생할 수 있습니다.',
|
||||
confirm: '활성화',
|
||||
},
|
||||
language: {
|
||||
title: '인터페이스 언어',
|
||||
desc: 'UI 표시 언어를 전환합니다. 현재 세션에 즉시 반영되며 다음 실행에도 유지됩니다.',
|
||||
|
||||
@@ -292,6 +292,7 @@ export const zhCN = {
|
||||
shortcuts: '快捷键',
|
||||
permissions: '权限',
|
||||
language: '语言',
|
||||
advanced: '高级',
|
||||
about: '关于',
|
||||
},
|
||||
recording: {
|
||||
@@ -379,6 +380,7 @@ export const zhCN = {
|
||||
volcengineAccessKeyLabel: 'Access Token',
|
||||
volcengineResourceIdLabel: 'Resource ID',
|
||||
volcengineMappingNote: 'Secret Key 当前无需填写。Resource ID 默认使用 volc.bigasr.sauc.duration。',
|
||||
localAsrActiveNotice: '当前已启用「{{name}}」,可在「高级」中切换或禁用。',
|
||||
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。请按需取舍:网络受限或对隐私敏感时再用本地。',
|
||||
@@ -467,6 +469,22 @@ export const zhCN = {
|
||||
notWindows: '仅 Windows 可用。',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
localAsrTitle: '本地 ASR 模型(实验性)',
|
||||
localAsrDesc: '把转写从云端切到本机推理。仅推荐离线 / 隐私敏感场景。',
|
||||
localAsrWarningShort: '本地 ASR 比云端慢若干秒、准确率更低;配置不足时可能吞字(仅输出部分语音)。',
|
||||
qwen3Desc: 'Alibaba Qwen3-ASR,跨平台。模型从 HuggingFace 下载。',
|
||||
foundryDesc: 'Microsoft Foundry Local Whisper,仅 Windows。',
|
||||
enable: '启用',
|
||||
alreadyActive: '已启用',
|
||||
disableLocalLabel: '禁用本地 ASR',
|
||||
disableLocalDesc: '切回云端 ASR(默认火山引擎 bigasr)。',
|
||||
disable: '禁用',
|
||||
platformNotSupported: '该平台暂未支持本地 ASR 模型集成。',
|
||||
confirmEnableLocalTitle: '启用本地 ASR?',
|
||||
confirmEnableLocalBody: '启用「{{target}}」后,转写会比云端慢若干秒,准确率更低;如果机器配置不足,可能出现吞字(仅输出部分语音)。',
|
||||
confirm: '确认启用',
|
||||
},
|
||||
language: {
|
||||
title: '界面语言',
|
||||
desc: '切换 UI 显示语言。当前会话即时生效,下次启动自动沿用。',
|
||||
|
||||
@@ -294,6 +294,7 @@ export const zhTW: typeof zhCN = {
|
||||
shortcuts: '快捷鍵',
|
||||
permissions: '權限',
|
||||
language: '語言',
|
||||
advanced: '高級',
|
||||
about: '關於',
|
||||
},
|
||||
recording: {
|
||||
@@ -381,6 +382,7 @@ export const zhTW: typeof zhCN = {
|
||||
volcengineAccessKeyLabel: 'Access Token',
|
||||
volcengineResourceIdLabel: 'Resource ID',
|
||||
volcengineMappingNote: 'Secret Key 當前無需填寫。Resource ID 默認使用 volc.bigasr.sauc.duration。',
|
||||
localAsrActiveNotice: '當前已啓用「{{name}}」,可在「高級」中切換或停用。',
|
||||
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。',
|
||||
@@ -469,6 +471,22 @@ export const zhTW: typeof zhCN = {
|
||||
notWindows: '僅 Windows 可用。',
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
localAsrTitle: '本地 ASR 模型(實驗性)',
|
||||
localAsrDesc: '把轉寫從雲端切到本機推理。僅推薦離線 / 隱私敏感場景。',
|
||||
localAsrWarningShort: '本地 ASR 比雲端慢若干秒、準確率更低;配置不足時可能吞字(僅輸出部分語音)。',
|
||||
qwen3Desc: 'Alibaba Qwen3-ASR,跨平臺。模型從 HuggingFace 下載。',
|
||||
foundryDesc: 'Microsoft Foundry Local Whisper,僅 Windows。',
|
||||
enable: '啓用',
|
||||
alreadyActive: '已啓用',
|
||||
disableLocalLabel: '停用本地 ASR',
|
||||
disableLocalDesc: '切回雲端 ASR(默認火山引擎 bigasr)。',
|
||||
disable: '停用',
|
||||
platformNotSupported: '該平臺暫未支持本地 ASR 模型集成。',
|
||||
confirmEnableLocalTitle: '啓用本地 ASR?',
|
||||
confirmEnableLocalBody: '啓用「{{target}}」後,轉寫會比雲端慢若干秒,準確率更低;如果機器配置不足,可能出現吞字(僅輸出部分語音)。',
|
||||
confirm: '確認啓用',
|
||||
},
|
||||
language: {
|
||||
title: '界面語言',
|
||||
desc: '切換 UI 顯示語言。當前會話即時生效,下次啓動自動沿用。',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// - 监听 `local-asr-download-progress` 事件实时刷新进度
|
||||
// - Win 端引擎不可用时禁用下载按钮,提示见 issue #256
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isTauri, setActiveAsrProvider } from '../lib/ipc';
|
||||
import {
|
||||
@@ -61,7 +61,16 @@ interface RemoteSize {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function LocalAsr() {
|
||||
interface LocalAsrProps {
|
||||
/// `embedded=true` 表示作为子组件嵌入「高级」设置页(Settings → Advanced);
|
||||
/// 此时跳过外层 page padding/height、PageHeader 与独立警告 Card —— 这些由
|
||||
/// 宿主 AdvancedSection 决定(包括把警告统一到页面顶部的浮层 popup 上)。
|
||||
/// `embedded=false`(默认)保留原全屏页样式,供 v 旧版本的独立「模型设置」
|
||||
/// 页面入口使用——但当前代码里该入口已删,本分支会一并移除。
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
const { t } = useTranslation();
|
||||
const { prefs, updatePrefs } = useHotkeySettings();
|
||||
const [settings, setSettings] = useState<LocalAsrSettings | null>(null);
|
||||
@@ -553,20 +562,35 @@ export function LocalAsr() {
|
||||
? t('localAsr.foundryRetryPrepare')
|
||||
: t('localAsr.foundryPrepare');
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 28px 32px', overflowY: 'auto', height: '100%' }}>
|
||||
<PageHeader
|
||||
kicker={t('localAsr.kicker')}
|
||||
title={t('localAsr.title')}
|
||||
desc={t('localAsr.desc')}
|
||||
/>
|
||||
|
||||
{/* 性能/质量预期警告 —— 用户硬要求要写清楚 */}
|
||||
<Card style={{ marginBottom: 16, background: 'rgba(255, 215, 130, 0.18)' }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--ol-ink-2)', lineHeight: 1.6 }}>
|
||||
⚠️ {t('localAsr.performanceWarning')}
|
||||
// embedded=true 嵌入「高级」设置:跳过外层 page padding/height、PageHeader,
|
||||
// 与独立警告 Card——AdvancedSection 自己负责标题与短警告 + 启用时的浮层 popup,
|
||||
// LocalAsr 只输出实际功能 Cards(Foundry / Qwen3 模型状态 / 模型列表)。
|
||||
const Wrapper = embedded
|
||||
? (props: { children: ReactNode }) => <>{props.children}</>
|
||||
: (props: { children: ReactNode }) => (
|
||||
<div style={{ padding: '20px 28px 32px', overflowY: 'auto', height: '100%' }}>
|
||||
{props.children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{!embedded && (
|
||||
<PageHeader
|
||||
kicker={t('localAsr.kicker')}
|
||||
title={t('localAsr.title')}
|
||||
desc={t('localAsr.desc')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!embedded && (
|
||||
/* 性能/质量预期警告 —— embedded 模式下由 AdvancedSection 自己渲染,避免重复。 */
|
||||
<Card style={{ marginBottom: 16, background: 'rgba(255, 215, 130, 0.18)' }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--ol-ink-2)', lineHeight: 1.6 }}>
|
||||
⚠️ {t('localAsr.performanceWarning')}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{IS_WINDOWS && (
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
@@ -873,7 +897,7 @@ export function LocalAsr() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Icon } from '../components/Icon';
|
||||
import { ShortcutRecorder } from '../components/ShortcutRecorder';
|
||||
import { isDialogStatus, UpdateDialog, useAutoUpdate } from '../components/AutoUpdate';
|
||||
import { detectOS } from '../components/WindowChrome';
|
||||
import { LocalAsr } from './LocalAsr';
|
||||
import { APP_VERSION_LABEL } from '../lib/appVersion';
|
||||
import { isHotkeyModeMigrationNoticeActive } from '../lib/hotkeyMigration';
|
||||
import {
|
||||
@@ -80,9 +81,12 @@ interface SettingsProps {
|
||||
initialSection?: SettingsSectionId;
|
||||
}
|
||||
// "关于" tab 已移除(内容并入外层 SettingsModal 的 About 页,避免设置内外重复入口)。
|
||||
export type SettingsSectionId = 'recording' | 'providers' | 'shortcuts' | 'permissions' | 'language';
|
||||
export type SettingsSectionId = 'recording' | 'providers' | 'shortcuts' | 'permissions' | 'language' | 'advanced';
|
||||
|
||||
const SECTION_ORDER: SettingsSectionId[] = ['recording', 'providers', 'shortcuts', 'permissions', 'language'];
|
||||
// 「高级」放最末——本地推理 / 实验性开关都集中到这一栏,避免新手用户在主流程
|
||||
// 里误开 CPU 推理(之前提案:把 local-qwen3 / foundry-local-whisper 从主 ASR
|
||||
// 下拉藏进高级)。位置末尾也是「实验性」语义在 macOS 系统偏好里的惯用位置。
|
||||
const SECTION_ORDER: SettingsSectionId[] = ['recording', 'providers', 'shortcuts', 'permissions', 'language', 'advanced'];
|
||||
|
||||
async function autostartIsEnabled(): Promise<boolean> {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
@@ -195,6 +199,7 @@ export function Settings({ embedded = false, initialSection = 'recording' }: Set
|
||||
{section === 'shortcuts' && <ShortcutsSection />}
|
||||
{section === 'permissions' && <PermissionsSection />}
|
||||
{section === 'language' && <LanguageSection />}
|
||||
{section === 'advanced' && <AdvancedSection />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -1240,8 +1245,10 @@ function ProvidersSection() {
|
||||
const [llmModelRevision, setLlmModelRevision] = useState(0);
|
||||
const [asrModelRevision, setAsrModelRevision] = useState(0);
|
||||
const os = detectOS();
|
||||
// 主 ASR 下拉只列云端选项;本地推理(local-qwen3 / foundry-local-whisper)
|
||||
// 移到「高级」标签页,防止新手误开 CPU 推理。详见 AdvancedSection。
|
||||
const visibleAsrPresets = ASR_PRESETS.filter(
|
||||
p => p.id !== 'foundry-local-whisper' || os === 'win',
|
||||
p => p.id !== 'foundry-local-whisper' && p.id !== 'local-qwen3',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1250,7 +1257,10 @@ function ProvidersSection() {
|
||||
const llmId = knownLlm ? knownLlm.id : 'custom';
|
||||
setLlmProvider(llmId);
|
||||
setCommittedLlmProvider(llmId);
|
||||
const knownAsr = visibleAsrPresets.find(x => x.id === prefs.activeAsrProvider);
|
||||
// ASR 在 ALL ASR_PRESETS 里查(不是 visibleAsrPresets)——本地选项虽然
|
||||
// 从下拉里藏起来了,但若用户曾在「高级」里启用过 local-qwen3,主 Card
|
||||
// 仍要识别出 active 是本地,并切到「正在使用本地 ASR」的 notice 渲染。
|
||||
const knownAsr = ASR_PRESETS.find(x => x.id === prefs.activeAsrProvider);
|
||||
const asrId = knownAsr ? knownAsr.id : 'volcengine';
|
||||
setAsrProvider(asrId);
|
||||
setCommittedAsrProvider(asrId);
|
||||
@@ -1368,15 +1378,26 @@ function ProvidersSection() {
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginTop: 2 }}>{t('settings.providers.asrDesc')}</div>
|
||||
</div>
|
||||
<SettingRow label={t('settings.providers.providerLabel')} desc={t('settings.providers.asrProviderDesc')}>
|
||||
<select
|
||||
value={asrProvider}
|
||||
onChange={e => onAsrProviderChange(e.target.value as AsrPresetId)}
|
||||
style={{ ...inputStyle, maxWidth: 200 }}
|
||||
>
|
||||
{visibleAsrPresets.map(p => (
|
||||
<option key={p.id} value={p.id}>{t(`settings.providers.presets.${p.nameKey}`)}</option>
|
||||
))}
|
||||
</select>
|
||||
{committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' ? (
|
||||
// active 是本地 ASR 时,下拉里没有它的 <option>(被 visibleAsrPresets
|
||||
// 过滤掉了),用受控 <select> 强行渲染会回退到第一项制造视觉假象。
|
||||
// 直接换成纯文本 notice,告诉用户"正在用本地 ASR,去高级中切换/禁用"。
|
||||
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-3)', lineHeight: 1.6 }}>
|
||||
{t('settings.providers.localAsrActiveNotice', {
|
||||
name: t(`settings.providers.presets.${committedAsrProvider === 'local-qwen3' ? 'asrLocalQwen3' : 'asrFoundryLocalWhisper'}`),
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={asrProvider}
|
||||
onChange={e => onAsrProviderChange(e.target.value as AsrPresetId)}
|
||||
style={{ ...inputStyle, maxWidth: 200 }}
|
||||
>
|
||||
{visibleAsrPresets.map(p => (
|
||||
<option key={p.id} value={p.id}>{t(`settings.providers.presets.${p.nameKey}`)}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</SettingRow>
|
||||
{committedAsrProvider === 'volcengine' ? (
|
||||
<>
|
||||
@@ -1405,7 +1426,10 @@ function ProvidersSection() {
|
||||
</div>
|
||||
</>
|
||||
) : committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper' ? (
|
||||
<LocalAsrProviderHint provider={committedAsrProvider} selectedProvider={asrProvider} />
|
||||
// 用户已经在用本地 ASR——dropdown 行的 localAsrActiveNotice 已经把
|
||||
// "在高级中切换或禁用"讲清楚了,body 不再重复 LocalAsrProviderHint。
|
||||
// 模型管理 UI 唯一入口在 AdvancedSection 里的 <LocalAsr embedded />。
|
||||
null
|
||||
) : (
|
||||
<>
|
||||
<CredentialField key={`${committedAsrProvider}:api_key`} label={t('settings.providers.apiKeyLabel')} account="asr.api_key" mono mask />
|
||||
@@ -1436,6 +1460,181 @@ function ProvidersSection() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 「高级」标签页——本地推理 / 实验性开关都集中在这里。
|
||||
///
|
||||
/// 设计:
|
||||
/// - 主 Providers 下拉只列云端选项;本地推理 (local-qwen3 / foundry-local-whisper)
|
||||
/// 完全收纳到此栏,新手不会在主流程里误开 CPU 推理。
|
||||
/// - 启用本地推理时弹出**页面顶部**的 React 浮层 popup(不再用 Tauri 系统对话框,
|
||||
/// 也不再常驻一段长警告文本)——一个框、一段短话、确认/取消。
|
||||
/// - 旧的「模型设置」独立页 (LocalAsr.tsx 作为 nav tab) 已下线,模型管理 UI
|
||||
/// 通过 <LocalAsr embedded /> 内嵌在此处统一管理。
|
||||
///
|
||||
/// 平台可见性:
|
||||
/// - macOS: 仅 Qwen3-ASR
|
||||
/// - Windows: Qwen3-ASR + Foundry Local Whisper
|
||||
/// - Linux: 「该平台暂未支持本地 ASR 模型集成」
|
||||
function AdvancedSection() {
|
||||
const { t } = useTranslation();
|
||||
const { prefs, updatePrefs } = useHotkeySettings();
|
||||
const os = detectOS();
|
||||
const platformSupported = os === 'mac' || os === 'win';
|
||||
const switchSeqRef = useRef(0);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// 待确认的启用目标。!== null 时浮层 popup 显示在页面顶部;用户点确认 → 真切;
|
||||
// 点取消 → 回到 null。一次只允许一个 popup(用户的"只一个框"硬要求)。
|
||||
const [pendingTarget, setPendingTarget] = useState<AsrPresetId | null>(null);
|
||||
|
||||
const activeAsrProvider = (prefs?.activeAsrProvider ?? 'volcengine') as AsrPresetId;
|
||||
const isOnLocalQwen3 = activeAsrProvider === 'local-qwen3';
|
||||
const isOnFoundry = activeAsrProvider === 'foundry-local-whisper';
|
||||
const isOnAnyLocal = isOnLocalQwen3 || isOnFoundry;
|
||||
|
||||
const requestEnable = (target: AsrPresetId) => {
|
||||
setPendingTarget(target);
|
||||
};
|
||||
|
||||
const performSwitch = async (target: AsrPresetId) => {
|
||||
setBusy(true);
|
||||
const seq = ++switchSeqRef.current;
|
||||
try {
|
||||
await setActiveAsrProvider(target);
|
||||
if (seq !== switchSeqRef.current) return;
|
||||
if (prefs) {
|
||||
await updatePrefs({ ...prefs, activeAsrProvider: target });
|
||||
}
|
||||
} finally {
|
||||
if (seq === switchSeqRef.current) {
|
||||
setBusy(false);
|
||||
setPendingTarget(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pendingNameKey =
|
||||
pendingTarget === 'local-qwen3' ? 'asrLocalQwen3'
|
||||
: pendingTarget === 'foundry-local-whisper' ? 'asrFoundryLocalWhisper'
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ─── 页面顶部确认浮层 ────────────────────────────────────────────
|
||||
用户硬要求:启用本地推理时**只**弹一个框、直接出现在页面最上方。
|
||||
琥珀边框 + 琥珀字醒目;不阻塞滚动;点取消返回不切。 */}
|
||||
{pendingTarget && pendingNameKey && (
|
||||
<Card
|
||||
style={{
|
||||
background: 'rgba(255, 188, 60, 0.12)',
|
||||
border: '1px solid rgba(220, 110, 0, 0.55)',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#A04500', marginBottom: 6 }}>
|
||||
⚠️ {t('settings.advanced.confirmEnableLocalTitle')}
|
||||
</div>
|
||||
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-2)', lineHeight: 1.6, marginBottom: 10 }}>
|
||||
{t('settings.advanced.confirmEnableLocalBody', {
|
||||
target: t(`settings.providers.presets.${pendingNameKey}`),
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Btn
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => void performSwitch(pendingTarget)}>
|
||||
{t('settings.advanced.confirm')}
|
||||
</Btn>
|
||||
<Btn variant="ghost" size="sm" disabled={busy} onClick={() => setPendingTarget(null)}>
|
||||
{t('common.cancel')}
|
||||
</Btn>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>{t('settings.advanced.localAsrTitle')}</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginTop: 2 }}>
|
||||
{t('settings.advanced.localAsrDesc')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 常驻短警告——一行字够了,详细解释留给点 Enable 后的页面顶部 popup。 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(255, 188, 60, 0.14)',
|
||||
border: '1px solid rgba(220, 110, 0, 0.4)',
|
||||
borderRadius: 6,
|
||||
fontSize: 12.5,
|
||||
color: '#A04500',
|
||||
fontWeight: 500,
|
||||
marginBottom: 14,
|
||||
}}>
|
||||
⚠️ {t('settings.advanced.localAsrWarningShort')}
|
||||
</div>
|
||||
|
||||
{!platformSupported ? (
|
||||
<div style={{ fontSize: 12.5, color: 'var(--ol-ink-3)', lineHeight: 1.6, padding: '8px 0' }}>
|
||||
{t('settings.advanced.platformNotSupported')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SettingRow
|
||||
label={t('settings.providers.presets.asrLocalQwen3')}
|
||||
desc={t('settings.advanced.qwen3Desc')}>
|
||||
<Btn
|
||||
variant={isOnLocalQwen3 ? 'ghost' : 'primary'}
|
||||
size="sm"
|
||||
disabled={busy || isOnLocalQwen3 || pendingTarget !== null}
|
||||
onClick={() => requestEnable('local-qwen3')}>
|
||||
{isOnLocalQwen3 ? t('settings.advanced.alreadyActive') : t('settings.advanced.enable')}
|
||||
</Btn>
|
||||
</SettingRow>
|
||||
|
||||
{os === 'win' && (
|
||||
<SettingRow
|
||||
label={t('settings.providers.presets.asrFoundryLocalWhisper')}
|
||||
desc={t('settings.advanced.foundryDesc')}>
|
||||
<Btn
|
||||
variant={isOnFoundry ? 'ghost' : 'primary'}
|
||||
size="sm"
|
||||
disabled={busy || isOnFoundry || pendingTarget !== null}
|
||||
onClick={() => requestEnable('foundry-local-whisper')}>
|
||||
{isOnFoundry ? t('settings.advanced.alreadyActive') : t('settings.advanced.enable')}
|
||||
</Btn>
|
||||
</SettingRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 「禁用本地 ASR」按钮放在 platformSupported 分支之外——若用户在另一台
|
||||
支持本地推理的机器上启用过 local-qwen3 / foundry,preferences 同步到
|
||||
当前 Linux/不支持平台后会卡死(无 enable / disable UI 可点),polish
|
||||
走老路径必失败。这里始终在 isOnAnyLocal 时露出 disable 入口,给用户
|
||||
退路(PR #400 pr_agent advisory 的修法)。 */}
|
||||
{isOnAnyLocal && (
|
||||
<SettingRow
|
||||
label={t('settings.advanced.disableLocalLabel')}
|
||||
desc={t('settings.advanced.disableLocalDesc')}>
|
||||
<Btn
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={busy || pendingTarget !== null}
|
||||
onClick={() => void performSwitch('volcengine')}>
|
||||
{t('settings.advanced.disable')}
|
||||
</Btn>
|
||||
</SettingRow>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 模型管理 UI(镜像源 / 模型列表 / 下载 / 删除 / 设为默认 / Foundry Local)
|
||||
inline 渲染——「模型设置」独立页已删,这里是唯一入口。 */}
|
||||
{platformSupported && <LocalAsr embedded />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ProviderToolStatus = 'idle' | 'loading' | 'success' | 'empty' | 'error';
|
||||
|
||||
function ProviderTools({ kind, modelAccount, onModelSelected }: { kind: 'llm' | 'asr'; modelAccount: string; onModelSelected: () => void }) {
|
||||
|
||||
Reference in new issue
Block a user