feat(models): 2.0 模型设置页与使用页——目录元数据、使用中模型选择与残留清理
模型设置页:新增 settings/models/LocalModelsSection 替换旧 LocalModelSection, 管理看板条目接入 Core 目录元数据快照(displayName / languages / sizeBytes), 基础信息展示不再依赖实时访问 HuggingFace;详情面板新增「清理未完成下载」 (local_asr_cleanup_incomplete,仅在有 staging 残留时出现)。 模型使用页:渠道编辑器本地引擎分支内嵌 LocalModelPicker——直接切换使用中的 模型(qwen/whisper 切换后预载引擎;sherpa/foundry 走各自 set 接口),显示引擎 可用性与残留清理入口;通过 ServicesTab 提供的 LocalModelsNavContext 一键跳到 「服务 → 本地模型」管理页(复用此前闲置的 localAsrManage 文案键)。 八语文案补齐(activeModelLabel / partialDownloads* / cleanupIncomplete / languagesLabel / partialBytesLabel 等 7 键)。tsc 0 错误,前端全套通过。
This commit is contained in:
1 parent
fe95354e25
commit
3b93d7421a
15 files changed
+485
-82
No files matched your search
@@ -2221,6 +2221,15 @@ export const de: typeof zhCN = {
|
||||
downloading: 'Wird heruntergeladen',
|
||||
startDownload: 'Download starten',
|
||||
downloadNewModel: 'Neues Modell herunterladen',
|
||||
activeModelLabel: 'Aktives Modell',
|
||||
pickerNoModelDownloaded:
|
||||
'Noch keine Modelle heruntergeladen — lade sie zuerst auf der Seite „Lokale Modelle“.',
|
||||
partialDownloadsLabel: 'Unvollständige Downloads',
|
||||
partialDownloadsDesc:
|
||||
'Abgebrochene Downloads haben temporäre Dateien hinterlassen; bereinige sie ohne die installierten Modelle zu beeinflussen.',
|
||||
cleanupIncomplete: 'Unvollständigen Download bereinigen',
|
||||
languagesLabel: 'Sprachen',
|
||||
partialBytesLabel: 'Restdateien',
|
||||
downloadDialogTitle: 'Modell herunterladen',
|
||||
downloadDialogAlreadyHave:
|
||||
'Die Modelldateien sind heruntergeladen. Kehre zur Modellseite zurück, um es zu laden und zu testen, oder wähle seinen Anbieter unter „ASR-Transkription“.',
|
||||
|
||||
@@ -1748,17 +1748,24 @@ export const en: typeof zhCN = {
|
||||
regeneratePin: 'Regenerate',
|
||||
portInUse: 'Port {{port}} is in use, please change it',
|
||||
startError: 'Failed to start the remote input service: {{reason}}',
|
||||
securityHint: 'Reachable only on the same LAN and requires the pairing code; turn it off when not in use.',
|
||||
certHint: "Verify the root certificate fingerprint before trusting it on first use. Older versions require one-time setup; subsequent restarts and IP changes preserve trust.",
|
||||
securityHint:
|
||||
'Reachable only on the same LAN and requires the pairing code; turn it off when not in use.',
|
||||
certHint:
|
||||
'Verify the root certificate fingerprint before trusting it on first use. Older versions require one-time setup; subsequent restarts and IP changes preserve trust.',
|
||||
certFingerprintLabel: "This computer's root CA SHA-256",
|
||||
certFingerprintCopy: "Copy full fingerprint",
|
||||
certFingerprintCopied: "Fingerprint copied",
|
||||
certFingerprintUnavailable: "The full fingerprint is unavailable. Do not install or trust a downloaded certificate.",
|
||||
certVerifyHint: "Find SHA-256 in the phone's system certificate details and compare all 64 characters with this value (ignore spaces and colons) before enabling full trust. A web page, profile name or identifier cannot prove identity. If the fingerprint differs or cannot be viewed in full, stop and remove the downloaded or installed profile.",
|
||||
certProfileHint: "Expect exactly one root certificate. Do not install a profile containing additional certificates, VPN or device management settings.",
|
||||
certTrustWarning: "The initial certificate download cannot verify the computer's identity; a malicious device on the LAN could replace the root certificate in a man-in-the-middle attack. Install it only on a trusted home or private network, never on a public or shared network. The root CA can issue certificates and its private key stays on this computer; remove it from your phone when no longer needed.",
|
||||
certSetupLink: "Copy iPhone certificate link",
|
||||
waitingStart: 'The service is not running yet. Turn the switch off, then on again. Do not restart the app.',
|
||||
certFingerprintCopy: 'Copy full fingerprint',
|
||||
certFingerprintCopied: 'Fingerprint copied',
|
||||
certFingerprintUnavailable:
|
||||
'The full fingerprint is unavailable. Do not install or trust a downloaded certificate.',
|
||||
certVerifyHint:
|
||||
"Find SHA-256 in the phone's system certificate details and compare all 64 characters with this value (ignore spaces and colons) before enabling full trust. A web page, profile name or identifier cannot prove identity. If the fingerprint differs or cannot be viewed in full, stop and remove the downloaded or installed profile.",
|
||||
certProfileHint:
|
||||
'Expect exactly one root certificate. Do not install a profile containing additional certificates, VPN or device management settings.',
|
||||
certTrustWarning:
|
||||
"The initial certificate download cannot verify the computer's identity; a malicious device on the LAN could replace the root certificate in a man-in-the-middle attack. Install it only on a trusted home or private network, never on a public or shared network. The root CA can issue certificates and its private key stays on this computer; remove it from your phone when no longer needed.",
|
||||
certSetupLink: 'Copy iPhone certificate link',
|
||||
waitingStart:
|
||||
'The service is not running yet. Turn the switch off, then on again. Do not restart the app.',
|
||||
starting: 'Starting the remote input service…',
|
||||
urlsStale: 'These addresses come from the previous run and may be out of date.',
|
||||
},
|
||||
@@ -2146,6 +2153,15 @@ export const en: typeof zhCN = {
|
||||
downloading: 'Downloading',
|
||||
startDownload: 'Start download',
|
||||
downloadNewModel: 'Download new model',
|
||||
activeModelLabel: 'Model in use',
|
||||
pickerNoModelDownloaded:
|
||||
'No downloaded models yet — download one on the Local models page first.',
|
||||
partialDownloadsLabel: 'Incomplete downloads',
|
||||
partialDownloadsDesc:
|
||||
'Interrupted downloads left staging files behind; clean them up without affecting installed models.',
|
||||
cleanupIncomplete: 'Clean up incomplete download',
|
||||
languagesLabel: 'Languages',
|
||||
partialBytesLabel: 'Leftover files',
|
||||
downloadDialogTitle: 'Download Model',
|
||||
downloadDialogAlreadyHave:
|
||||
'The model files are downloaded. Return to the model page to load and test, or choose its provider in ASR transcription.',
|
||||
|
||||
@@ -2202,6 +2202,15 @@ export const es: typeof zhCN = {
|
||||
downloading: 'Descargando',
|
||||
startDownload: 'Iniciar descarga',
|
||||
downloadNewModel: 'Descargar nuevo modelo',
|
||||
activeModelLabel: 'Modelo en uso',
|
||||
pickerNoModelDownloaded:
|
||||
'Aún no hay modelos descargados; descarga primero uno en la página de modelos locales.',
|
||||
partialDownloadsLabel: 'Descargas incompletas',
|
||||
partialDownloadsDesc:
|
||||
'Las descargas interrumpidas dejaron archivos temporales; límpialos sin afectar a los modelos instalados.',
|
||||
cleanupIncomplete: 'Limpiar descarga incompleta',
|
||||
languagesLabel: 'Idiomas',
|
||||
partialBytesLabel: 'Archivos residuales',
|
||||
downloadDialogTitle: 'Descargar modelo',
|
||||
downloadDialogAlreadyHave:
|
||||
'Los archivos del modelo ya están descargados. Vuelve a su página para cargarlo y probarlo, o elige su proveedor en Transcripción ASR.',
|
||||
|
||||
@@ -2231,6 +2231,15 @@ export const fr: typeof zhCN = {
|
||||
downloading: 'Téléchargement',
|
||||
startDownload: 'Démarrer le téléchargement',
|
||||
downloadNewModel: 'Télécharger un nouveau modèle',
|
||||
activeModelLabel: 'Modèle utilisé',
|
||||
pickerNoModelDownloaded:
|
||||
'Aucun modèle téléchargé pour l’instant — commencez par la page des modèles locaux.',
|
||||
partialDownloadsLabel: 'Téléchargements incomplets',
|
||||
partialDownloadsDesc:
|
||||
'Des téléchargements interrompus ont laissé des fichiers temporaires ; nettoyez-les sans toucher aux modèles installés.',
|
||||
cleanupIncomplete: 'Nettoyer le téléchargement incomplet',
|
||||
languagesLabel: 'Langues',
|
||||
partialBytesLabel: 'Fichiers restants',
|
||||
downloadDialogTitle: 'Télécharger un modèle',
|
||||
downloadDialogAlreadyHave:
|
||||
'Les fichiers du modèle sont téléchargés. Revenez à sa page pour le charger et le tester, ou choisissez son fournisseur dans Transcription ASR.',
|
||||
|
||||
@@ -1717,17 +1717,24 @@ export const ja: typeof zhCN = {
|
||||
regeneratePin: '再生成',
|
||||
portInUse: 'ポート {{port}} は使用中です。変更してください',
|
||||
startError: 'リモート入力サービスの起動に失敗しました:{{reason}}',
|
||||
securityHint: '同一 LAN からのみアクセス可能で、ペアリングコードの入力が必要です。使わないときはオフにすることを推奨します。',
|
||||
certHint: "初回接続ではルート証明書の指紋を確認してから信頼してください。旧バージョンからは一度設定が必要ですが、その後は再起動や IP 変更でも信頼が保持されます。",
|
||||
certFingerprintLabel: "このコンピューターのルート CA SHA-256",
|
||||
certFingerprintCopy: "指紋全体をコピー",
|
||||
certFingerprintCopied: "指紋をコピーしました",
|
||||
certFingerprintUnavailable: "完全な指紋を取得できません。ダウンロードした証明書をインストールしたり信頼したりしないでください。",
|
||||
certVerifyHint: "スマートフォンのシステム証明書詳細にある SHA-256 の全 64 文字を、空白とコロンを除いてこの値と照合し、完全に信頼する前に確認してください。Web ページ、プロファイル名や識別子は身元の証明にはなりません。一致しない場合や全体を表示できない場合は中止し、ダウンロード済みまたはインストール済みのプロファイルを削除してください。",
|
||||
certProfileHint: "プロファイルにはルート証明書が 1 枚だけ含まれるはずです。追加の証明書、VPN、デバイス管理の設定がある場合はインストールしないでください。",
|
||||
certTrustWarning: "初回の証明書ダウンロードではコンピューターの身元を確認できず、LAN 上の悪意あるデバイスが中間者攻撃でルート証明書を置き換える可能性があります。信頼できる家庭内またはプライベートネットワークでのみインストールし、公共または共有ネットワークでは操作しないでください。ルート CA は証明書を発行でき、秘密鍵はこのコンピューターに保存されます。不要になったらスマートフォンから削除してください。",
|
||||
certSetupLink: "iPhone 証明書リンクをコピー",
|
||||
waitingStart: 'サービスはまだ起動していません。スイッチを一度オフにしてからオンにしてください。アプリを再起動しないでください。',
|
||||
securityHint:
|
||||
'同一 LAN からのみアクセス可能で、ペアリングコードの入力が必要です。使わないときはオフにすることを推奨します。',
|
||||
certHint:
|
||||
'初回接続ではルート証明書の指紋を確認してから信頼してください。旧バージョンからは一度設定が必要ですが、その後は再起動や IP 変更でも信頼が保持されます。',
|
||||
certFingerprintLabel: 'このコンピューターのルート CA SHA-256',
|
||||
certFingerprintCopy: '指紋全体をコピー',
|
||||
certFingerprintCopied: '指紋をコピーしました',
|
||||
certFingerprintUnavailable:
|
||||
'完全な指紋を取得できません。ダウンロードした証明書をインストールしたり信頼したりしないでください。',
|
||||
certVerifyHint:
|
||||
'スマートフォンのシステム証明書詳細にある SHA-256 の全 64 文字を、空白とコロンを除いてこの値と照合し、完全に信頼する前に確認してください。Web ページ、プロファイル名や識別子は身元の証明にはなりません。一致しない場合や全体を表示できない場合は中止し、ダウンロード済みまたはインストール済みのプロファイルを削除してください。',
|
||||
certProfileHint:
|
||||
'プロファイルにはルート証明書が 1 枚だけ含まれるはずです。追加の証明書、VPN、デバイス管理の設定がある場合はインストールしないでください。',
|
||||
certTrustWarning:
|
||||
'初回の証明書ダウンロードではコンピューターの身元を確認できず、LAN 上の悪意あるデバイスが中間者攻撃でルート証明書を置き換える可能性があります。信頼できる家庭内またはプライベートネットワークでのみインストールし、公共または共有ネットワークでは操作しないでください。ルート CA は証明書を発行でき、秘密鍵はこのコンピューターに保存されます。不要になったらスマートフォンから削除してください。',
|
||||
certSetupLink: 'iPhone 証明書リンクをコピー',
|
||||
waitingStart:
|
||||
'サービスはまだ起動していません。スイッチを一度オフにしてからオンにしてください。アプリを再起動しないでください。',
|
||||
starting: 'リモート入力サービスを起動しています…',
|
||||
urlsStale: 'これらのアドレスは前回の起動時のもので、古くなっている可能性があります。',
|
||||
},
|
||||
@@ -2115,6 +2122,15 @@ export const ja: typeof zhCN = {
|
||||
downloading: 'ダウンロード中',
|
||||
startDownload: 'ダウンロード開始',
|
||||
downloadNewModel: '新しいモデルをダウンロード',
|
||||
activeModelLabel: '使用中のモデル',
|
||||
pickerNoModelDownloaded:
|
||||
'ダウンロード済みのモデルがありません。先にローカルモデルページで取得してください。',
|
||||
partialDownloadsLabel: '未完了のダウンロード',
|
||||
partialDownloadsDesc:
|
||||
'中断されたダウンロードの一時ファイルが残っています。インストール済みモデルに影響せず一括削除できます。',
|
||||
cleanupIncomplete: '未完了ダウンロードを削除',
|
||||
languagesLabel: '言語',
|
||||
partialBytesLabel: '残存ファイル',
|
||||
downloadDialogTitle: 'モデルをダウンロード',
|
||||
downloadDialogAlreadyHave:
|
||||
'モデルファイルはダウンロード済みです。モデルページで読み込みとテストを行うか、「ASR 音声文字起こし」で対応するプロバイダーを選択してください。',
|
||||
|
||||
@@ -1705,17 +1705,24 @@ export const ko: typeof zhCN = {
|
||||
regeneratePin: '재생성',
|
||||
portInUse: '포트 {{port}}이(가) 사용 중입니다. 변경하세요',
|
||||
startError: '원격 입력 서비스 시작에 실패했습니다: {{reason}}',
|
||||
securityHint: '같은 LAN에서만 접속 가능하며 페어링 코드 입력이 필요합니다. 사용하지 않을 때는 끄는 것을 권장합니다.',
|
||||
certHint: "처음 연결할 때 루트 인증서 지문을 확인한 후 신뢰하세요. 이전 버전에서는 한 번 설정해야 하며, 이후 재시작과 IP 변경 시 신뢰가 유지됩니다.",
|
||||
certFingerprintLabel: "이 컴퓨터의 루트 CA SHA-256",
|
||||
certFingerprintCopy: "전체 지문 복사",
|
||||
certFingerprintCopied: "지문 복사됨",
|
||||
certFingerprintUnavailable: "전체 지문을 확인할 수 없습니다. 다운로드한 인증서를 설치하거나 신뢰하지 마세요.",
|
||||
certVerifyHint: "휴대폰 시스템의 인증서 상세 정보에서 SHA-256을 찾아, 완전한 신뢰를 켜기 전에 공백과 콜론을 제외한 64자 전체를 이 값과 비교하세요. 웹 페이지, 프로파일 이름이나 식별자는 신원 증명이 아닙니다. 일치하지 않거나 전체 지문을 볼 수 없으면 중단하고 다운로드했거나 설치한 프로파일을 제거하세요.",
|
||||
certProfileHint: "프로파일에는 루트 인증서 한 개만 있어야 합니다. 추가 인증서, VPN 또는 기기 관리 설정이 있으면 설치하지 마세요.",
|
||||
certTrustWarning: "최초 인증서 다운로드에서는 컴퓨터의 신원을 확인할 수 없으며, LAN의 악성 기기가 중간자 공격으로 루트 인증서를 바꿀 수 있습니다. 신뢰할 수 있는 가정용 또는 사설 네트워크에서만 설치하고 공용 또는 공유 네트워크에서는 진행하지 마세요. 루트 CA는 인증서를 발급할 수 있고 개인 키는 이 컴퓨터에 저장됩니다. 더 이상 사용하지 않으면 휴대폰에서 제거하세요.",
|
||||
certSetupLink: "iPhone 인증서 링크 복사",
|
||||
waitingStart: '서비스가 아직 시작되지 않았습니다. 스위치를 끈 다음 다시 켜세요. 앱을 다시 시작하지 마세요.',
|
||||
securityHint:
|
||||
'같은 LAN에서만 접속 가능하며 페어링 코드 입력이 필요합니다. 사용하지 않을 때는 끄는 것을 권장합니다.',
|
||||
certHint:
|
||||
'처음 연결할 때 루트 인증서 지문을 확인한 후 신뢰하세요. 이전 버전에서는 한 번 설정해야 하며, 이후 재시작과 IP 변경 시 신뢰가 유지됩니다.',
|
||||
certFingerprintLabel: '이 컴퓨터의 루트 CA SHA-256',
|
||||
certFingerprintCopy: '전체 지문 복사',
|
||||
certFingerprintCopied: '지문 복사됨',
|
||||
certFingerprintUnavailable:
|
||||
'전체 지문을 확인할 수 없습니다. 다운로드한 인증서를 설치하거나 신뢰하지 마세요.',
|
||||
certVerifyHint:
|
||||
'휴대폰 시스템의 인증서 상세 정보에서 SHA-256을 찾아, 완전한 신뢰를 켜기 전에 공백과 콜론을 제외한 64자 전체를 이 값과 비교하세요. 웹 페이지, 프로파일 이름이나 식별자는 신원 증명이 아닙니다. 일치하지 않거나 전체 지문을 볼 수 없으면 중단하고 다운로드했거나 설치한 프로파일을 제거하세요.',
|
||||
certProfileHint:
|
||||
'프로파일에는 루트 인증서 한 개만 있어야 합니다. 추가 인증서, VPN 또는 기기 관리 설정이 있으면 설치하지 마세요.',
|
||||
certTrustWarning:
|
||||
'최초 인증서 다운로드에서는 컴퓨터의 신원을 확인할 수 없으며, LAN의 악성 기기가 중간자 공격으로 루트 인증서를 바꿀 수 있습니다. 신뢰할 수 있는 가정용 또는 사설 네트워크에서만 설치하고 공용 또는 공유 네트워크에서는 진행하지 마세요. 루트 CA는 인증서를 발급할 수 있고 개인 키는 이 컴퓨터에 저장됩니다. 더 이상 사용하지 않으면 휴대폰에서 제거하세요.',
|
||||
certSetupLink: 'iPhone 인증서 링크 복사',
|
||||
waitingStart:
|
||||
'서비스가 아직 시작되지 않았습니다. 스위치를 끈 다음 다시 켜세요. 앱을 다시 시작하지 마세요.',
|
||||
starting: '원격 입력 서비스를 시작하는 중입니다…',
|
||||
urlsStale: '이 주소는 이전 실행에서 가져온 것으로 최신이 아닐 수 있습니다.',
|
||||
},
|
||||
@@ -2098,6 +2105,15 @@ export const ko: typeof zhCN = {
|
||||
downloading: '다운로드 중',
|
||||
startDownload: '다운로드 시작',
|
||||
downloadNewModel: '새 모델 다운로드',
|
||||
activeModelLabel: '사용 중인 모델',
|
||||
pickerNoModelDownloaded:
|
||||
'다운로드된 모델이 아직 없습니다. 로컬 모델 페이지에서 먼저 내려받으세요.',
|
||||
partialDownloadsLabel: '완료되지 않은 다운로드',
|
||||
partialDownloadsDesc:
|
||||
'중단된 다운로드의 임시 파일이 남아 있습니다. 설치된 모델에 영향 없이 정리할 수 있습니다.',
|
||||
cleanupIncomplete: '미완료 다운로드 정리',
|
||||
languagesLabel: '언어',
|
||||
partialBytesLabel: '남은 파일',
|
||||
downloadDialogTitle: '모델 다운로드',
|
||||
downloadDialogAlreadyHave:
|
||||
'모델 파일이 다운로드되었습니다. 모델 페이지에서 불러와 테스트하거나 ASR 음성 전사에서 해당 제공업체를 선택하세요.',
|
||||
|
||||
@@ -1651,15 +1651,18 @@ export const zhCN = {
|
||||
portInUse: '端口 {{port}} 被占用,请更换',
|
||||
startError: '远程输入服务启动失败:{{reason}}',
|
||||
securityHint: '仅同一局域网可访问,需输入配对码;不用时建议关闭。',
|
||||
certHint: "首次连接需核对根证书指纹后再信任。升级旧版需设置一次;以后重启和换 IP 会保留信任。",
|
||||
certFingerprintLabel: "本机根证书 SHA-256",
|
||||
certFingerprintCopy: "复制完整指纹",
|
||||
certFingerprintCopied: "指纹已复制",
|
||||
certFingerprintUnavailable: "完整指纹不可用。请勿安装或信任下载的证书。",
|
||||
certVerifyHint: "在手机系统的证书详情中找到 SHA-256,与这里的全部 64 个字符逐一核对(忽略空格和冒号)。必须在开启完全信任前完成。网页、描述文件名称和标识不能证明证书身份;若不一致或无法查看完整指纹,请停止并移除已下载或安装的描述文件。",
|
||||
certProfileHint: "描述文件应只包含一张根证书。若有其他证书、VPN 或设备管理配置,请勿安装。",
|
||||
certTrustWarning: "首次证书下载无法验证电脑身份,恶意局域网设备可能通过中间人攻击替换根证书。仅在可信的家庭或私人网络中安装,勿在公共或共享网络操作。根证书具备签发能力,私钥保存在这台电脑;不再使用时请从手机移除。",
|
||||
certSetupLink: "复制 iPhone 证书链接",
|
||||
certHint:
|
||||
'首次连接需核对根证书指纹后再信任。升级旧版需设置一次;以后重启和换 IP 会保留信任。',
|
||||
certFingerprintLabel: '本机根证书 SHA-256',
|
||||
certFingerprintCopy: '复制完整指纹',
|
||||
certFingerprintCopied: '指纹已复制',
|
||||
certFingerprintUnavailable: '完整指纹不可用。请勿安装或信任下载的证书。',
|
||||
certVerifyHint:
|
||||
'在手机系统的证书详情中找到 SHA-256,与这里的全部 64 个字符逐一核对(忽略空格和冒号)。必须在开启完全信任前完成。网页、描述文件名称和标识不能证明证书身份;若不一致或无法查看完整指纹,请停止并移除已下载或安装的描述文件。',
|
||||
certProfileHint: '描述文件应只包含一张根证书。若有其他证书、VPN 或设备管理配置,请勿安装。',
|
||||
certTrustWarning:
|
||||
'首次证书下载无法验证电脑身份,恶意局域网设备可能通过中间人攻击替换根证书。仅在可信的家庭或私人网络中安装,勿在公共或共享网络操作。根证书具备签发能力,私钥保存在这台电脑;不再使用时请从手机移除。',
|
||||
certSetupLink: '复制 iPhone 证书链接',
|
||||
waitingStart: '服务尚未启动。请关闭开关再打开一次,不要重启软件。',
|
||||
starting: '正在启动远程输入服务…',
|
||||
urlsStale: '这些地址来自上次运行,可能已经过期。',
|
||||
@@ -2031,6 +2034,13 @@ export const zhCN = {
|
||||
downloading: '下载中',
|
||||
startDownload: '开始下载',
|
||||
downloadNewModel: '下载新模型',
|
||||
activeModelLabel: '使用中的模型',
|
||||
pickerNoModelDownloaded: '还没有已下载的模型,请先在本地模型页下载。',
|
||||
partialDownloadsLabel: '未完成下载',
|
||||
partialDownloadsDesc: '存在中断下载的临时残留,可一键清理,不影响已安装模型。',
|
||||
cleanupIncomplete: '清理未完成下载',
|
||||
languagesLabel: '语言',
|
||||
partialBytesLabel: '残留文件',
|
||||
downloadDialogTitle: '下载模型',
|
||||
downloadDialogAlreadyHave:
|
||||
'模型文件已下载。可回到模型页加载并测试,或在「ASR 语音转写」中选择对应供应商。',
|
||||
|
||||
@@ -1637,15 +1637,18 @@ export const zhTW: typeof zhCN = {
|
||||
portInUse: '連接埠 {{port}} 被佔用,請更換',
|
||||
startError: '遠端輸入服務啟動失敗:{{reason}}',
|
||||
securityHint: '僅同一區域網路可存取,需輸入配對碼;不用時建議關閉。',
|
||||
certHint: "首次連線需核對根憑證指紋後再信任。升級舊版需設定一次;之後重新啟動和更換 IP 會保留信任。",
|
||||
certFingerprintLabel: "本機根憑證 SHA-256",
|
||||
certFingerprintCopy: "複製完整指紋",
|
||||
certFingerprintCopied: "已複製指紋",
|
||||
certFingerprintUnavailable: "完整指紋無法取得。請勿安裝或信任下載的憑證。",
|
||||
certVerifyHint: "在手機系統的憑證詳細資訊中找到 SHA-256,與此處全部 64 個字元逐一核對(忽略空格和冒號)。必須在開啟完全信任前完成。網頁、描述檔名稱與識別碼不能證明憑證身分;若不一致或無法查看完整指紋,請停止並移除已下載或安裝的描述檔。",
|
||||
certProfileHint: "描述檔應只包含一張根憑證。若有其他憑證、VPN 或裝置管理設定,請勿安裝。",
|
||||
certTrustWarning: "首次憑證下載無法驗證電腦身分,惡意區域網路裝置可能透過中間人攻擊替換根憑證。僅在可信任的家庭或私人網路中安裝,請勿在公共或共享網路操作。根憑證能簽發憑證,私密金鑰保存在這台電腦;不再使用時請從手機移除。",
|
||||
certSetupLink: "複製 iPhone 憑證連結",
|
||||
certHint:
|
||||
'首次連線需核對根憑證指紋後再信任。升級舊版需設定一次;之後重新啟動和更換 IP 會保留信任。',
|
||||
certFingerprintLabel: '本機根憑證 SHA-256',
|
||||
certFingerprintCopy: '複製完整指紋',
|
||||
certFingerprintCopied: '已複製指紋',
|
||||
certFingerprintUnavailable: '完整指紋無法取得。請勿安裝或信任下載的憑證。',
|
||||
certVerifyHint:
|
||||
'在手機系統的憑證詳細資訊中找到 SHA-256,與此處全部 64 個字元逐一核對(忽略空格和冒號)。必須在開啟完全信任前完成。網頁、描述檔名稱與識別碼不能證明憑證身分;若不一致或無法查看完整指紋,請停止並移除已下載或安裝的描述檔。',
|
||||
certProfileHint: '描述檔應只包含一張根憑證。若有其他憑證、VPN 或裝置管理設定,請勿安裝。',
|
||||
certTrustWarning:
|
||||
'首次憑證下載無法驗證電腦身分,惡意區域網路裝置可能透過中間人攻擊替換根憑證。僅在可信任的家庭或私人網路中安裝,請勿在公共或共享網路操作。根憑證能簽發憑證,私密金鑰保存在這台電腦;不再使用時請從手機移除。',
|
||||
certSetupLink: '複製 iPhone 憑證連結',
|
||||
waitingStart: '服務尚未啟動。請關閉開關再打開一次,不要重啟軟體。',
|
||||
starting: '正在啟動遠端輸入服務…',
|
||||
urlsStale: '這些地址來自上次執行,可能已經過期。',
|
||||
@@ -2017,6 +2020,13 @@ export const zhTW: typeof zhCN = {
|
||||
downloading: '下載中',
|
||||
startDownload: '開始下載',
|
||||
downloadNewModel: '下載新模型',
|
||||
activeModelLabel: '使用中的模型',
|
||||
pickerNoModelDownloaded: '尚無已下載的模型,請先在本機模型頁下載。',
|
||||
partialDownloadsLabel: '未完成下載',
|
||||
partialDownloadsDesc: '存在中斷下載的暫存殘留,可一鍵清理,不影響已安裝模型。',
|
||||
cleanupIncomplete: '清理未完成下載',
|
||||
languagesLabel: '語言',
|
||||
partialBytesLabel: '殘留檔案',
|
||||
downloadDialogTitle: '下載模型',
|
||||
downloadDialogAlreadyHave:
|
||||
'模型檔案已下載。可返回模型頁載入並測試,或在「ASR 語音轉寫」中選擇對應供應商。',
|
||||
|
||||
@@ -474,6 +474,14 @@ export interface SidebarModelEntry {
|
||||
id: string;
|
||||
/** 展示名(如 qwen3-asr-0.6b / whisper-small)。 */
|
||||
name: string;
|
||||
/** 目录快照的正式展示名(Core descriptor);无则回退 name。 */
|
||||
displayName?: string;
|
||||
/** 目录快照的支持语言短码(zh / en / ja…)。 */
|
||||
languages?: string[];
|
||||
/** 目录快照的远端完整尺寸;无需实时访问 HuggingFace 即可展示。 */
|
||||
sizeBytes?: number;
|
||||
/** 有已下载字节但未装好 = 中断残留,可一键清理 staging 目录。 */
|
||||
partialBytes?: number;
|
||||
/** HF 仓库标识(Qwen3 有;sherpa/foundry 可能为空)。 */
|
||||
repo?: string;
|
||||
/** 已下载字节数(HF 拉取的真实尺寸)。 */
|
||||
@@ -532,11 +540,14 @@ function ModelChoice({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<button type="button" className="ol-model-choice" aria-pressed={selected} onClick={onSelect}>
|
||||
<span className="ol-model-choice-name">{entry.name}</span>
|
||||
<span className="ol-model-choice-name">{entry.displayName || entry.name}</span>
|
||||
<span className="ol-model-choice-meta">
|
||||
<span>{ENGINE_LABELS[entry.engine]}</span>
|
||||
{entry.languages?.length ? <span>{entry.languages.join(' / ')}</span> : null}
|
||||
<span>
|
||||
{entry.remoteBytes ? formatBytes(entry.remoteBytes) : t('localAsr.sizeUnknown')}
|
||||
{entry.remoteBytes || entry.sizeBytes
|
||||
? formatBytes(entry.remoteBytes ?? entry.sizeBytes!)
|
||||
: t('localAsr.sizeUnknown')}
|
||||
</span>
|
||||
</span>
|
||||
<span className="ol-model-choice-state">
|
||||
@@ -605,8 +616,24 @@ function ModelFacts({
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t('localAsr.sizeLabel')}</dt>
|
||||
<dd>{entry.remoteBytes ? formatBytes(entry.remoteBytes) : t('localAsr.sizeUnknown')}</dd>
|
||||
<dd>
|
||||
{entry.remoteBytes || entry.sizeBytes
|
||||
? formatBytes(entry.remoteBytes ?? entry.sizeBytes!)
|
||||
: t('localAsr.sizeUnknown')}
|
||||
</dd>
|
||||
</div>
|
||||
{entry.languages?.length ? (
|
||||
<div>
|
||||
<dt>{t('localAsr.languagesLabel')}</dt>
|
||||
<dd>{entry.languages.join(' / ')}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{entry.partialBytes ? (
|
||||
<div>
|
||||
<dt>{t('localAsr.partialBytesLabel')}</dt>
|
||||
<dd>{formatBytes(entry.partialBytes)}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{fileCount != null && fileCount > 0 && (
|
||||
<div>
|
||||
<dt>{t('localAsr.files')}</dt>
|
||||
@@ -641,6 +668,7 @@ export function ModelDetailPanel({
|
||||
onDelete,
|
||||
onReveal,
|
||||
onTest,
|
||||
onCleanup,
|
||||
showTest,
|
||||
testResult,
|
||||
testing,
|
||||
@@ -655,6 +683,8 @@ export function ModelDetailPanel({
|
||||
onDelete: () => void;
|
||||
onReveal: () => void;
|
||||
onTest: () => void;
|
||||
/** 清理中断下载的 staging 目录(仅存在残留且未在下载时出现)。 */
|
||||
onCleanup?: () => void;
|
||||
showTest: boolean;
|
||||
testResult: LocalAsrTestResult | { error: string } | null;
|
||||
testing: boolean;
|
||||
@@ -665,7 +695,7 @@ export function ModelDetailPanel({
|
||||
<div className="ol-model-detail">
|
||||
<div className="ol-model-detail-heading">
|
||||
<span className="ol-model-eyebrow">{t('localAsr.detailsTitle')}</span>
|
||||
<h3>{entry.name}</h3>
|
||||
<h3>{entry.displayName || entry.name}</h3>
|
||||
<ModelStatus entry={entry} />
|
||||
</div>
|
||||
<ModelFacts entry={entry} fileCount={fileCount} mirrorLabel={mirrorLabel} />
|
||||
@@ -685,6 +715,11 @@ export function ModelDetailPanel({
|
||||
{entry.downloadError ? t('common.retry') : t('localAsr.download')}
|
||||
</Btn>
|
||||
)}
|
||||
{!entry.isDownloaded && !entry.isDownloading && entry.partialBytes && onCleanup && (
|
||||
<Btn variant="ghost" disabled={busy} onClick={onCleanup}>
|
||||
{t('localAsr.cleanupIncomplete')}
|
||||
</Btn>
|
||||
)}
|
||||
{entry.isDownloading && (
|
||||
<Btn variant="ghost" onClick={onCancel}>
|
||||
{t('localAsr.cancel')}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode }
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { restartApp } from '../../lib/ipc/permissions';
|
||||
import { isTauri } from '../../lib/ipc';
|
||||
import { emitSaved } from '../../lib/savedEvent';
|
||||
import { useLayoutStack } from '../../lib/useMobileLayout';
|
||||
import {
|
||||
FOUNDRY_LOCAL_ASR_MODELS,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
cancelSherpaOnnxAsrDownload,
|
||||
cancelSherpaOnnxAsrPrepare,
|
||||
cancelLocalAsrDownload,
|
||||
cleanupIncompleteLocalAsrModel,
|
||||
deleteFoundryLocalAsrModel,
|
||||
deleteSherpaOnnxAsrModel,
|
||||
deleteLocalAsrModel,
|
||||
@@ -1366,6 +1368,21 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
// 清理中断下载的 staging 目录;已安装模型不受影响(后端保证)。
|
||||
const handleCleanupIncomplete = async (modelId: string) => {
|
||||
setBusyModelId(modelId);
|
||||
try {
|
||||
setError(null);
|
||||
await cleanupIncompleteLocalAsrModel(modelId);
|
||||
emitSaved('saved', t('common.saved'));
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusyModelId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeepLoadedChange = async (seconds: number) => {
|
||||
try {
|
||||
await setLocalAsrKeepLoadedSecs(seconds);
|
||||
@@ -1619,6 +1636,14 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
entries.push({
|
||||
id: m.id,
|
||||
name: m.id.replace(/^qwen3-asr-/, 'Qwen3-ASR ').replace(/^whisper-/, 'Whisper '),
|
||||
// 目录元数据来自 Core descriptor 快照,展示不再依赖实时 HuggingFace。
|
||||
displayName: m.displayName || undefined,
|
||||
languages: m.languages?.length ? m.languages : undefined,
|
||||
sizeBytes: m.sizeBytes ?? undefined,
|
||||
partialBytes:
|
||||
!m.isDownloaded && !isDownloading && m.downloadedBytes > 0
|
||||
? m.downloadedBytes
|
||||
: undefined,
|
||||
repo: m.hfRepo,
|
||||
remoteBytes:
|
||||
remoteSizes[m.id]?.totalBytes || (m.isDownloaded ? m.downloadedBytes : undefined),
|
||||
@@ -1650,6 +1675,8 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
entries.push({
|
||||
id: c.alias,
|
||||
name: c.displayName || c.alias,
|
||||
displayName: c.displayName || undefined,
|
||||
sizeBytes: c.fileSizeMb != null ? c.fileSizeMb * 1024 * 1024 : undefined,
|
||||
remoteBytes:
|
||||
sherpaRemoteSizes[c.alias]?.totalBytes ||
|
||||
(c.fileSizeMb != null ? c.fileSizeMb * 1024 * 1024 : undefined),
|
||||
@@ -1682,6 +1709,8 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
entries.push({
|
||||
id: c.alias,
|
||||
name: c.displayName || c.alias,
|
||||
displayName: c.displayName || undefined,
|
||||
sizeBytes: c.fileSizeMb != null ? c.fileSizeMb * 1024 * 1024 : undefined,
|
||||
remoteBytes: c.fileSizeMb != null ? c.fileSizeMb * 1024 * 1024 : undefined,
|
||||
isDownloaded: c.cached,
|
||||
isDownloading,
|
||||
@@ -1928,6 +1957,13 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
|
||||
}}
|
||||
onDelete={() => selectedEntry && dispatchEntryAction(selectedEntry, 'delete')}
|
||||
onReveal={() => selectedEntry && dispatchEntryAction(selectedEntry, 'reveal')}
|
||||
onCleanup={
|
||||
selectedEntry &&
|
||||
(selectedEntry.engine === 'qwen3' || selectedEntry.engine === 'whisper') &&
|
||||
selectedEntry.partialBytes
|
||||
? () => void handleCleanupIncomplete(selectedEntry.id)
|
||||
: undefined
|
||||
}
|
||||
onTest={() => {
|
||||
if (
|
||||
selectedEntry?.engine === 'qwen3' ||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '../../lib/ipc';
|
||||
import { LlmProtocolFields } from './LlmProtocolFields';
|
||||
import { ProviderFormContext } from './ProviderForm';
|
||||
import { LocalModelPicker } from './models/LocalModelPicker';
|
||||
import { emitSaved } from '../../lib/savedEvent';
|
||||
import { useLayoutStack, useConservativeLayout } from '../../lib/useMobileLayout';
|
||||
import { useHotkeySettings } from '../../state/HotkeySettingsContext';
|
||||
@@ -588,10 +589,15 @@ export function ChannelCredentialFields({
|
||||
// 本地引擎(qwen3 / sherpa / foundry / Apple 语音)没有 key 与地址;模型的下载与
|
||||
// 切换仍由「高级 → 本地模型」里的 <LocalAsr embedded /> 负责,这里只说明一句。
|
||||
if (descriptor?.authRequirement === 'none') {
|
||||
// 本地引擎:模型即凭据。这里直接切换使用中的模型(全局生效),
|
||||
// 下载 / 删除 / 镜像在「服务 → 本地模型」管理页。
|
||||
return (
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.6 }}>
|
||||
{t('settings.providers.localEngineNoCredentials')}
|
||||
</div>
|
||||
<>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.6 }}>
|
||||
{t('settings.providers.localEngineNoCredentials')}
|
||||
</div>
|
||||
<LocalModelPicker providerType={providerType} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// 模型使用页:渠道编辑器「本地引擎」分支的内嵌模型选择器。
|
||||
// 全局 active model 与引擎状态来自本地 ASR IPC——本地模型不按渠道隔离,
|
||||
// 这里切的是全局使用中的模型;下载 / 删除 / 镜像等管理动作仍在
|
||||
// 「服务 → 本地模型」(useLocalModelsNav 跳转)。
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectLite } from '../../../components/ui/SelectLite';
|
||||
import { Btn } from '../../_atoms';
|
||||
import { SettingRow, inputStyle } from '../shared';
|
||||
import {
|
||||
cleanupIncompleteLocalAsrModel,
|
||||
getFoundryLocalAsrCatalog,
|
||||
getFoundryLocalAsrStatus,
|
||||
getLocalAsrSettings,
|
||||
getSherpaOnnxAsrCatalog,
|
||||
getSherpaOnnxAsrStatus,
|
||||
listLocalAsrModels,
|
||||
preloadLocalAsr,
|
||||
setFoundryLocalAsrModel,
|
||||
setLocalAsrActiveModel,
|
||||
setSherpaOnnxAsrModel,
|
||||
type FoundryLocalAsrCatalogModel,
|
||||
type LocalAsrModelStatus,
|
||||
type SherpaOnnxCatalogModel,
|
||||
} from '../../../lib/localAsr';
|
||||
import { emitSaved } from '../../../lib/savedEvent';
|
||||
import { useLocalModelsNav } from './modelsNav';
|
||||
|
||||
/** 本地引擎 providerType → 选择器数据源家族。 */
|
||||
function familyOfProviderType(providerType: string): 'generic' | 'sherpa' | 'foundry' | 'apple' {
|
||||
if (providerType === 'sherpa-onnx-local') return 'sherpa';
|
||||
if (providerType === 'foundry-local-whisper') return 'foundry';
|
||||
if (providerType === 'apple-speech') return 'apple';
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
downloaded: boolean;
|
||||
/** 有已下载字节但未装好 = 中断残留,可一键清理。 */
|
||||
partialBytes?: number;
|
||||
}
|
||||
|
||||
export function LocalModelPicker({ providerType }: { providerType: string }) {
|
||||
const { t } = useTranslation();
|
||||
const openModels = useLocalModelsNav();
|
||||
const family = familyOfProviderType(providerType);
|
||||
|
||||
const [options, setOptions] = useState<ModelOption[] | null>(null);
|
||||
const [activeModel, setActiveModel] = useState('');
|
||||
const [engineNote, setEngineNote] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
if (family === 'generic') {
|
||||
const [settings, models] = await Promise.all([getLocalAsrSettings(), listLocalAsrModels()]);
|
||||
setOptions(
|
||||
models.map((m: LocalAsrModelStatus) => ({
|
||||
value: m.id,
|
||||
label: m.displayName || m.id,
|
||||
downloaded: m.isDownloaded,
|
||||
partialBytes: !m.isDownloaded && m.downloadedBytes > 0 ? m.downloadedBytes : undefined,
|
||||
})),
|
||||
);
|
||||
setActiveModel(settings.activeModel);
|
||||
setEngineNote(settings.engineAvailable ? null : t('localAsr.engineUnavailable'));
|
||||
} else if (family === 'sherpa') {
|
||||
const [status, catalog] = await Promise.all([
|
||||
getSherpaOnnxAsrStatus(),
|
||||
getSherpaOnnxAsrCatalog(),
|
||||
]);
|
||||
setOptions(
|
||||
catalog.map((c: SherpaOnnxCatalogModel) => ({
|
||||
value: c.alias,
|
||||
label: c.displayName || c.alias,
|
||||
downloaded: c.cached,
|
||||
})),
|
||||
);
|
||||
setActiveModel(status.activeModel);
|
||||
setEngineNote(status.available ? null : (status.error ?? t('localAsr.engineUnavailable')));
|
||||
} else if (family === 'foundry') {
|
||||
const [status, catalog] = await Promise.all([
|
||||
getFoundryLocalAsrStatus(),
|
||||
getFoundryLocalAsrCatalog(),
|
||||
]);
|
||||
setOptions(
|
||||
catalog.map((c: FoundryLocalAsrCatalogModel) => ({
|
||||
value: c.alias,
|
||||
label: c.displayName || c.alias,
|
||||
downloaded: c.cached,
|
||||
})),
|
||||
);
|
||||
setActiveModel(status.activeModel);
|
||||
setEngineNote(status.available ? null : (status.error ?? t('localAsr.engineUnavailable')));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[local-models] failed to load picker state', error);
|
||||
setEngineNote(t('settings.providers.readFailed'));
|
||||
}
|
||||
}, [family, t]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const partials = useMemo(
|
||||
() => (options ?? []).filter((option) => option.partialBytes),
|
||||
[options],
|
||||
);
|
||||
|
||||
const changeModel = async (value: string) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
if (family === 'generic') {
|
||||
await setLocalAsrActiveModel(value);
|
||||
await preloadLocalAsr().catch(() => undefined);
|
||||
} else if (family === 'sherpa') {
|
||||
await setSherpaOnnxAsrModel(value);
|
||||
} else if (family === 'foundry') {
|
||||
await setFoundryLocalAsrModel(value);
|
||||
}
|
||||
emitSaved('saved', t('common.saved'));
|
||||
await load();
|
||||
} catch (error) {
|
||||
console.error('[local-models] failed to switch active model', error);
|
||||
emitSaved('failed', t('common.operationFailed'));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupPartial = async (modelId: string) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await cleanupIncompleteLocalAsrModel(modelId);
|
||||
emitSaved('saved', t('common.saved'));
|
||||
await load();
|
||||
} catch (error) {
|
||||
console.error('[local-models] failed to cleanup incomplete download', error);
|
||||
emitSaved('failed', t('common.operationFailed'));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (family === 'apple') {
|
||||
return (
|
||||
<p className="ol-channel-local-hint">{t('settings.providers.localEngineNoCredentials')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
const downloadedCount = (options ?? []).filter((option) => option.downloaded).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow label={t('localAsr.activeModelLabel')}>
|
||||
{options === null ? (
|
||||
<span style={{ fontSize: 12, color: 'var(--ol-ink-3)' }}>{t('common.loading')}</span>
|
||||
) : downloadedCount === 0 ? (
|
||||
<span style={{ fontSize: 12, color: 'var(--ol-ink-3)' }}>
|
||||
{t('localAsr.pickerNoModelDownloaded')}
|
||||
</span>
|
||||
) : (
|
||||
<SelectLite
|
||||
value={activeModel}
|
||||
disabled={busy}
|
||||
onChange={(value) => void changeModel(value)}
|
||||
options={(options ?? [])
|
||||
.filter((option) => option.downloaded)
|
||||
.map((option) => ({ value: option.value, label: option.label }))}
|
||||
ariaLabel={t('localAsr.activeModelLabel')}
|
||||
style={{ ...inputStyle, width: '100%', maxWidth: '100%', height: 38 }}
|
||||
/>
|
||||
)}
|
||||
</SettingRow>
|
||||
|
||||
{partials.length > 0 && (
|
||||
<SettingRow
|
||||
label={t('localAsr.partialDownloadsLabel')}
|
||||
desc={t('localAsr.partialDownloadsDesc')}
|
||||
>
|
||||
<Btn
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
for (const option of partials) void cleanupPartial(option.value);
|
||||
}}
|
||||
>
|
||||
{t('localAsr.cleanupIncomplete')}
|
||||
</Btn>
|
||||
</SettingRow>
|
||||
)}
|
||||
|
||||
{engineNote && (
|
||||
<p role="alert" style={{ fontSize: 12, color: 'var(--ol-warn)' }}>
|
||||
{engineNote}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{openModels && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 2 }}>
|
||||
<Btn variant="ghost" size="sm" onClick={openModels}>
|
||||
{t('settings.providers.localAsrManage')}
|
||||
</Btn>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+16
-14
@@ -1,20 +1,21 @@
|
||||
// 服务 → 本地模型:本地 ASR 模型的管理入口。含 Qwen3(macOS)/ Foundry Local +
|
||||
// sherpa-onnx(Windows)三条本地引擎。
|
||||
// 模型设置页(2.0):本地 ASR 模型的下载与管理看板。
|
||||
// 含 Qwen3(macOS)/ Foundry Local + sherpa-onnx(Windows)三条本地引擎。
|
||||
//
|
||||
// 2026-08 重构:本地 ASR 不再有「启用开关」——本地 ASR 始终可用,是否激活由
|
||||
// 「服务 → AI 提供商」的 ASR 语音转写供应商决定:选到本地模型供应商即使用本地
|
||||
// 引擎(与 Apple 语音同理)。这里只保留模型下载 / 管理看板(<LocalAsr embedded />)。
|
||||
// 本地 ASR 不再有「启用开关」——是否激活由「服务 → AI 提供商」的 ASR
|
||||
// 语音转写供应商决定:选到本地模型供应商即使用本地引擎(与 Apple 语音同理)。
|
||||
// 这里是模型目录与管理(下载 / 删除 / 清理残留 / 测试 / 镜像源);
|
||||
// 使用中的模型切换在渠道编辑器的 LocalModelPicker。
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { PlatformCapabilities } from '../../lib/types';
|
||||
import type { PlatformCapabilities } from '../../../lib/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LocalAsr } from '../LocalAsr';
|
||||
import { detectOS } from '../../components/WindowChrome';
|
||||
import { getPlatformCapabilities } from '../../lib/platform';
|
||||
import { Card } from '../_atoms';
|
||||
import { ExperimentalSectionTitle } from './shared';
|
||||
import { LocalAsr } from '../../LocalAsr';
|
||||
import { detectOS } from '../../../components/WindowChrome';
|
||||
import { getPlatformCapabilities } from '../../../lib/platform';
|
||||
import { Card } from '../../_atoms';
|
||||
import { ExperimentalSectionTitle } from '../shared';
|
||||
|
||||
export function LocalModelSection() {
|
||||
export function LocalModelsSection() {
|
||||
const { t } = useTranslation();
|
||||
const os = detectOS();
|
||||
const isWin = os === 'win';
|
||||
@@ -68,8 +69,9 @@ export function LocalModelSection() {
|
||||
{t('settings.advanced.platformNotSupported')}
|
||||
</div>
|
||||
) : (
|
||||
/* 模型下载 / 管理看板(模型选择 · 下载 · 删除 · 测试 · 镜像源收纳)——
|
||||
较大的内嵌框。启用入口在「AI 提供商 → ASR 语音转写」选择本地供应商。 */
|
||||
/* 模型目录 / 管理看板(模型选择 · 下载 · 清理 · 删除 · 测试 · 镜像源)。
|
||||
使用入口在「AI 提供商 → ASR 语音转写」选择本地供应商后,
|
||||
由渠道编辑器内的 LocalModelPicker 切换使用中的模型。 */
|
||||
<div style={{ marginTop: 16, borderTop: '0.5px solid var(--ol-line)', paddingTop: 16 }}>
|
||||
<LocalAsr embedded />
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
/**
|
||||
* 跳转到「服务 → 本地模型」视图的回调。由拥有视图状态的 ServicesTab 提供,
|
||||
* 渠道编辑器等深层界面通过它把用户带到模型管理页;取不到时(如独立测试渲染)
|
||||
* 调用方应退化为隐藏跳转入口。
|
||||
*/
|
||||
export const LocalModelsNavContext = createContext<(() => void) | null>(null);
|
||||
|
||||
export function useLocalModelsNav(): (() => void) | null {
|
||||
return useContext(LocalModelsNavContext);
|
||||
}
|
||||
@@ -16,7 +16,8 @@ import { MarketplaceSection } from './MarketplaceSection';
|
||||
import { PermissionsSection } from './PermissionsSection';
|
||||
import { DataStorageSection } from './DataStorageSection';
|
||||
import { CloudSyncSection } from './CloudSyncSection';
|
||||
import { LocalModelSection } from './LocalModelSection';
|
||||
import { LocalModelsSection } from './models/LocalModelsSection';
|
||||
import { LocalModelsNavContext } from './models/modelsNav';
|
||||
import { DebugToolsSection } from './DebugToolsSection';
|
||||
import { MultimodalPipelineSection } from './MultimodalPipelineSection';
|
||||
import { CodingAgentSection } from './CodingAgentSection';
|
||||
@@ -163,16 +164,19 @@ export function ServicesTab() {
|
||||
})}
|
||||
</div>
|
||||
<div key={selectedView} ref={contentRef} className="ol-service-content">
|
||||
{selectedView === 'llm' && <ProvidersSection kind="llm" />}
|
||||
{selectedView === 'asr' && <ProvidersSection kind="asr" />}
|
||||
{selectedView === 'omni' && <ProvidersSection />}
|
||||
{selectedView === 'models' && <LocalModelSection />}
|
||||
{selectedView === 'connections' && (
|
||||
<>
|
||||
<NetworkSection />
|
||||
<MarketplaceSection />
|
||||
</>
|
||||
)}
|
||||
{/* 渠道编辑器里的 LocalModelPicker 通过该上下文跳到本视图。 */}
|
||||
<LocalModelsNavContext.Provider value={() => setView('models')}>
|
||||
{selectedView === 'llm' && <ProvidersSection kind="llm" />}
|
||||
{selectedView === 'asr' && <ProvidersSection kind="asr" />}
|
||||
{selectedView === 'omni' && <ProvidersSection />}
|
||||
{selectedView === 'models' && <LocalModelsSection />}
|
||||
{selectedView === 'connections' && (
|
||||
<>
|
||||
<NetworkSection />
|
||||
<MarketplaceSection />
|
||||
</>
|
||||
)}
|
||||
</LocalModelsNavContext.Provider>
|
||||
{(selectedView === 'llm' || selectedView === 'asr') && (
|
||||
<p className="ol-service-storage-note">
|
||||
{t('settings.providers.credentialStorageNotice')}
|
||||
|
||||
Reference in new issue
Block a user