@goodandready/dsh-voice 0.9.45 → 0.9.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,6 +1,3 @@
1
- // dsh-voice — client half (browser).
2
- //
3
- // Dictation (mic), voice message (wave), dock pill and settings card.
4
1
 
5
2
  window.__ModuleLoader__.load({
6
3
  id: '@goodandready/dsh-voice',
@@ -20,605 +17,295 @@ window.__ModuleLoader__.load({
20
17
  }
21
18
 
22
19
  const NS = 'dsh-voice'
23
- // Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
24
20
  const PKG = '@goodandready/dsh-voice'
25
21
  const ROW_ID = 'dsh-voice'
26
22
  const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
27
23
 
28
- // UI strings in locale registry; English is source/default.
29
24
  const en = {
30
- 'saveFailed': 'Some fields were not saved —',
31
- 'cardHint': 'Dictation and voice messages: providers, chains, local whisper',
32
- 'expand': 'Expand',
33
- 'collapse': 'Collapse',
34
- 'composerUnavailable': 'Composer unavailable',
35
- 'keySpace': 'Space',
36
- 'keyUnset': 'not set',
37
- 'recognitionError': 'recognition failed',
38
- 'micUnavailable': 'Microphone unavailable: needs HTTPS or localhost',
39
- 'noRecorder': 'MediaRecorder is not supported by this browser',
40
- 'browserFailed': 'Browser did not recognize: ',
41
- 'nothingHeard': 'Nothing was recognized',
42
- 'dictationBtn': 'Voice typing',
43
- 'messageBtn': 'Voice message — click or hold',
44
- 'dictationPill': 'Dictation — text is appended to the input',
45
- 'messagePill': 'Recording a voice message',
46
- 'cancel': 'Cancel',
47
- 'holdHint': 'Hold — release to send',
48
- 'listening': 'Listening in the browser…',
49
- 'stop': 'Stop',
50
- 'transcribing': 'Transcribing…',
51
- 'sendingIn': 'Sending to the agent in',
52
- 'secondsShort': ' s',
53
- 'keepPending': 'Do not send',
54
- 'hide': 'Hide',
55
- 'title': 'Voice',
56
- 'recordSlot': 'Voice recording',
57
- 'browserHint': 'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
58
- 'browserWebGpu': 'Browser WebGPU Whisper (Offline)',
59
- 'browserWebGpuHint': 'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
60
- 'npuProvider': 'Hardware Acceleration (SBC NPU / Host)',
61
- 'npuProviderHint': 'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
62
- 'npuThreads': 'Threads (CPU/SBC)',
63
- 'benchmarkTitle': 'Hardware Benchmark (Speed & Latency)',
64
- 'runBenchmarkBtn': 'Run Benchmark', 'benchmarking': 'Running benchmark…',
65
- 'benchmarkHint': 'Measures real-time factor (RTF) and inference latency on 1.0s audio for current execution provider.',
66
- 'webgpuStatus': 'Browser WebGPU Status',
67
- 'webgpuAvailable': 'WebGPU Available (hardware acceleration ready)',
68
- 'webgpuNotAvailable': 'WebGPU Not Detected (will fallback to server)',
69
- 'openaiHint': 'whisper-1 · key OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
70
- 'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
71
- 'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
72
- 'localHint': 'set when the server is started', 'up': 'Up', 'down': 'Down', 'remove': 'Remove',
73
- 'addProvider': 'Add provider',
74
- 'chainHint': 'Top to bottom is the order they are tried in',
75
- 'customName': 'name used in the chain',
76
- 'customModel': 'model',
77
- 'customKeyName': 'key name',
78
- 'customModelHint': 'model to request (empty means the built-in one)',
79
- 'addCustom': 'Add your own provider',
80
- 'loadingSettings': 'Loading settings…',
81
- 'notReady1': 'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
82
- 'notReady2': 'the section will appear on its own in a few seconds.',
83
- 'hotkey': 'Voice message key',
84
- 'pressKey': 'Press a key…',
85
- 'clearKey': 'Clear the key',
86
- 'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
87
- 'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
88
- 'language': 'Language',
89
- 'langAuto': 'Auto-detect',
90
- 'langRu': 'Russian (ru)',
91
- 'langEn': 'English (en)',
92
- 'langZh': 'Chinese (zh)',
93
- 'langUk': 'Ukrainian (uk)',
94
- 'langDe': 'German (de)',
95
- 'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
96
- 'pauseMs': 'Pause that ends a phrase, ms',
97
- 'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
98
- 'speaking': 'You are speaking…',
99
- 'silence': 'Pause…',
100
- 'normalizeTranscript': 'Normalize file transcripts',
101
- 'undo': 'Undo last insert',
102
- 'undone': 'Insert undone',
103
- 'nothingToUndo': 'Nothing to undo',
104
- 'clipping': 'Clipping', 'clippingHint': 'Too loud (clipping)',
105
- 'lowVolume': 'Low signal', 'lowVolumeHint': 'Speak louder',
106
- 'vadEngine': 'VAD engine', 'vadEngineHint': 'Neural Silero VAD (WASM/ONNX) or classic amplitude gate',
107
- 'vadSilero': 'Silero VAD (neural)', 'vadAmplitude': 'Amplitude gate',
108
- 'streamingWs': 'Streaming WebSocket', 'streamingWsHint': 'Real-time WebSocket audio streaming to local ASR',
109
- 'beep': 'Beep on start/stop',
110
- 'localOnly': 'Local whisper only',
111
- 'localOnlyHint': 'Restrict both chains to the local whisper.cpp server: fully offline.',
112
- 'sendDelay': 'Dictation send delay (ms)',
113
- 'sendDelayHint': 'Wait before appending a dictated phrase, with a cancel window. 0 = off',
114
- 'mic': 'Microphone',
115
- 'micDefault': 'System default',
116
- 'vocabulary': 'Custom vocabulary (one word per line)',
117
- 'polish': 'Polish transcript with model',
118
- 'polishHint': 'Fix punctuation and fillers via the harness model before inserting',
119
- 'stream': 'Continuous dictation',
120
- 'streamHint': 'Cut phrases by a timer while you speak instead of waiting for a long pause',
121
- 'streamChunkMs': 'Stream chunk (ms)',
122
- 'vadAdapt': 'Adaptive silence',
123
- 'vadAdaptHint': 'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
124
- 'wakeWord': 'Wake word',
125
- 'wakeWordHint': 'Browser recognition starts recording when speech begins with this phrase. Empty = off',
126
- 'bargeIn': 'Barge-in',
127
- 'polishSend': 'Polish whole draft before sending',
128
- 'polishSendHint': 'Run the composed draft through the model right before sending',
129
- 'editablePreview': 'Editable preview before send',
130
- 'editablePreviewHint': 'Show recognized text before sending with editing, comparison and abort options.',
131
- 'editPreview': 'Edit',
132
- 'sendNow': 'Send',
133
- 'cancelPreview': 'Cancel',
134
- 'rawText': 'Raw',
135
- 'polishedText': 'Polished',
136
- 'sessionMismatch': 'Different chat active; canceled to prevent leak.',
137
- 'sessionCommands': 'Voice session commands',
138
- 'sessionCommandsHint': '"send", "cancel", "stop", "continue" act on the session instead of becoming text',
139
- 'polishBaseUrl': 'Offline polish endpoint',
140
- 'polishBaseUrlHint': 'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
141
- 'polishModel': 'Offline polish model',
142
- 'polishKeyEnv': 'Offline polish key credential',
143
- 'voiceCommandsLabel': 'Voice edit commands ("new line", "paragraph")',
144
- 'normalizeTranscriptHint': 'transcribe_audio: spoken numbers to digits, tidy punctuation',
145
- 'messageTitle': 'Voice message',
146
- 'messageHint': 'One whole recording, sent to the agent once it is transcribed.',
147
- 'undoMs': 'Undo window, ms',
148
- 'undoHint': 'How long the automatic send can still be called off',
149
- 'customTitle': 'Your own providers',
150
- 'customHint': 'Any OpenAI-compatible API. The name becomes available in the chains above.',
151
- 'general': 'General',
152
- 'whisperEndpoint': 'Local whisper: endpoint',
153
- 'whisperEndpointHint': 'POST /inference of a whisper.cpp server',
154
- 'deepgramEndpoint': 'Deepgram: base URL',
155
- 'deepgramEndpointHint': 'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
156
- 'whisperBin': 'Local whisper: binary',
157
- 'whisperBinHint': 'used when autostart is on',
158
- 'whisperModel': 'Local whisper: model',
159
- 'whisperModelHint': 'Absolute path to the ggml model file',
160
- 'whisperAutostart': 'Autostart the local whisper',
161
- 'bilingualDeveloperPrompt': 'Bilingual developer context (RU/EN)',
162
- 'bilingualDeveloperPromptHint': 'Hint technical terms and CLI commands to the STT model to prevent phonetic transliteration',
163
- 'calibrateNoise': 'Calibrate Noise (2s)',
164
- 'calibrateNoiseHint': 'Measure room noise for 2 seconds and auto-set the optimal noise gate threshold',
165
- 'calibrating': 'Measuring noise…',
166
- 'calibratedMsg': 'Gate set: {db} dB (ambient: {noise} dB)',
167
- 'calibratedValue': 'Calibrated',
168
- 'save': 'Save',
169
- 'saved': 'Saved ✓',
170
- 'openrouterWarning': 'OpenRouter has no /audio/transcriptions — use the openai-chat-audio template there',
171
- 'noiseSuppression': 'Hardware noise suppression',
172
- 'noiseSuppressionHint': 'Enable browser noise suppression, echo cancellation, and auto gain control',
173
- 'contextGlossary': 'Context glossary injection',
174
- 'contextGlossaryHint': 'Auto-extract code identifiers and terms from composer to improve STT accuracy',
175
- 'providerDashboard': 'Provider Latency & Health',
176
- 'avgLatency': 'Avg latency',
177
- 'successRate': 'Success',
178
- 'fast': 'Fast',
179
- 'normal': 'Normal',
180
- 'slow': 'Slow',
181
- 'error': 'Error',
182
- 'idle': 'No calls',
183
- 'play': 'Play',
184
- 'pause': 'Pause',
185
- 'listenBack': 'Listen back',
186
- 'lastRecording': 'Last voice note',
187
- 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
188
- 'sensevoiceEndpoint': 'SenseVoice: endpoint',
189
- 'sensevoiceEndpointHint': 'POST endpoint of sherpa-onnx or compatible server',
190
- 'sensevoiceBin': 'SenseVoice: binary',
191
- 'sensevoiceBinHint': 'used when autostart is on',
192
- 'sensevoiceModel': 'SenseVoice: model path',
193
- 'sensevoiceModelHint': 'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
194
- 'sensevoiceAutostart': 'Autostart the SenseVoice server',
195
- 'visualizerStyle': 'Audio visualizer style',
196
- 'visualizerStyleHint': 'Waveform animation inside the recording pill',
197
- 'visLiquidWave': 'Liquid Wave',
198
- 'visDynamicOrb': 'Dynamic Orb',
199
- 'visBars': 'Classic Bars',
200
- 'visOff': 'Off',
201
- 'statusTitle': 'Connection & Engine Status',
202
- 'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
203
- 'badgeHostOnline': 'Host online ({ms} ms)',
204
- 'badgeHostOffline': 'Host unreachable',
205
- 'badgeWhisperActive': 'Whisper active',
206
- 'badgeWhisperInactive': 'Whisper offline',
207
- 'badgeSenseVoiceActive': 'SenseVoice active',
208
- 'badgeSenseVoiceInactive': 'SenseVoice offline',
209
- 'badgeProviders': '{count} providers ready',
210
- 'refreshStats': 'Refresh Stats',
211
- 'hardwareTitle': 'Hardware & Local Engines',
212
- 'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
213
- 'uiError': 'Voice UI Error:',
214
- 'retry': 'Retry',
215
- 'testMic': 'Test Mic',
216
- 'stopTest': 'Stop Test',
217
- 'micLevel': 'Mic level',
218
- 'noiseGate': 'Noise Gate (dB)',
219
- 'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
220
- 'noiseGateOff': 'Off',
221
- 'versionTitle': 'Plugin Version & Updates',
222
- 'versionHint': 'Check for new versions and update directly from npm.',
223
- 'updateAvailable': 'Update available',
224
- 'upToDate': 'Up to date',
225
- 'updatePrompt': 'A new version is available on npm. Click update to install.',
226
- 'updateNow': 'Update now',
227
- 'updating': 'Updating…',
228
- 'updateSuccess': 'Updated successfully! Restart DSH to apply.',
229
- 'updateFailed': 'Update failed. Check logs.',
230
- 'updateCheckFailed': 'Update check failed',
231
- 'profileLockedPid': 'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
232
- 'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
233
- 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
234
- 'checkingUpdates': 'Checking updates…',
235
- 'updateStatusUnknown': 'Version status unknown',
236
- 'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
237
- 'sensevoiceDesc': 'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
238
- 'installSensevoice': 'Install SenseVoice-Small (1-Click)',
239
- 'installingSensevoice': 'Installing SenseVoice…',
240
- 'sensevoiceReady': 'Ready',
241
- 'sensevoiceNotInstalled': 'Not Installed',
242
- 'sensevoiceDownloading': 'Downloading model…',
243
- 'turnTakingTitle': 'Turn-Taking & Audio Coordination',
244
- 'turnTakingDesc': 'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
245
- 'gatedTurnTaking': 'Gated Mode (mute mic while assistant is speaking)',
246
- 'bargeInDesc': 'True Barge-In (speaking interrupts assistant speech)',
247
- 'smartActionsTitle': 'Hands-free & Smart Actions',
248
- 'autoSendVisualRing': 'Silence Ring countdown animation',
249
- 'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
250
- 'liveInterimPreview': 'Live speech preview (ghost text preview)',
251
- 'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
252
- 'modeVerbatim': 'Verbatim',
253
- 'modeClean': 'Clean Text',
254
- 'modeTask': 'Agent Task',
255
- 'modeNote': 'Note',
256
- 'dictationModeTitle': 'Dictation Post-Processing Mode',
257
- 'dictationModeDesc': 'Select how dictated speech is processed before insertion: Verbatim (0 LLM calls), Clean text (formatting & filler removal), Agent task (structured prompt), or Note (key points & actions).',
258
- 'dictationPromptsTitle': 'Dictation Prompt Templates',
259
- 'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
260
- 'promptClean': 'Clean text prompt',
261
- 'promptTask': 'Agent task prompt',
262
- 'promptNote': 'Note prompt',
263
- 'resetPrompts': 'Reset to defaults',
264
- 'voiceMacrosTitle': 'Voice Action Macros',
265
- 'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
266
- 'addMacro': 'Add macro',
267
- 'macroTrigger': 'Spoken trigger phrase',
268
- 'macroTemplate': 'Expanded prompt template',
269
- 'macroDelete': 'Delete',
270
- 'rawTab': 'Raw STT',
271
- 'resultTab': 'Result',
272
- 'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
273
- 'jargonTitle': 'Developer Lexicon & IT Jargon',
274
- 'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
275
- 'jargonAdd': 'Add replacement',
276
- 'jargonFrom': 'Spoken slang',
277
- 'jargonTo': 'Correct spelling',
278
- 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
279
- 'voiceInput': 'Voice input',
280
- 'assistantSpeakingGated': 'Assistant speaking (gated mode)',
281
- 'jargonFormatHint': ' (spoken -> replacement)',
282
- 'errorPrefix': 'Error',
283
- 'wizardTitle': 'First-Run Setup & Diagnostics Wizard',
284
- 'wizardDesc': 'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
285
- 'wizardBtnLaunch': 'Launch Mic & STT Wizard',
286
- 'wizardClose': 'Close Wizard',
287
- 'wizardStep1': '1. Audio & Environment',
288
- 'wizardStep2': '2. Live Signal Test',
289
- 'wizardStep3': '3. STT Transcription Test',
290
- 'wizardSecureContextOk': 'Secure context / HTTPS verified',
291
- 'wizardSecureContextBad': 'Insecure context (HTTPS or localhost required)',
292
- 'wizardMediaDevicesOk': 'Browser mediaDevices API supported',
293
- 'wizardMediaDevicesBad': 'Browser mediaDevices API missing',
294
- 'wizardMediaRecorderOk': 'MediaRecorder API supported',
295
- 'wizardMediaRecorderBad': 'MediaRecorder API unsupported in this browser',
296
- 'wizardMicPermissionOk': 'Microphone permission granted',
297
- 'wizardMicPermissionPrompt': 'Microphone permission not yet granted',
298
- 'wizardMicPermissionBad': 'Microphone permission denied',
299
- 'wizardRequestMic': 'Request Microphone Access',
300
- 'wizardMicLabel': 'Microphone input device:',
301
- 'wizardStartSignalCheck': 'Start Audio Level Test', 'wizardStopSignalCheck': 'Stop Audio Test',
302
- 'wizardSignalOk': 'Audio signal detected (RMS: {level}%)',
303
- 'wizardSignalZero': 'Zero audio level detected - speak into microphone or select correct device',
304
- 'wizardSignalTesting': 'Listening for audio signal… speak into your microphone',
305
- 'wizardProviderLabel': 'Active STT engine & model:',
306
- 'wizardProcessingArea': 'Processing area:', 'wizardAreaBrowser': 'In-browser (WebGPU / WebSpeech)', 'wizardAreaServer': 'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud': 'Remote cloud provider',
307
- 'wizardStartTestRecord': 'Record 3-Second Test Phrase', 'wizardRecordingTest': 'Recording test phrase… speak now ({left}s)',
308
- 'wizardTranscribingTest': 'Transcribing test phrase…', 'wizardTestResult': 'STT Test Result:',
309
- 'wizardTestSuccess': 'STT round-trip verified successfully! Latency: {latency}ms',
310
- 'wizardTestEmpty': 'No speech detected in test recording. Please speak clearly and try again.',
311
- 'wizardTestFailed': 'STT test failed: {error}',
312
- 'wizardNextStepHint': 'Next step: {hint}',
313
- 'wizardHintPermission': 'Click "Request Microphone Access" and allow browser permission prompt.', 'wizardHintDevice': 'Check microphone connection or select a different input device.', 'wizardHintHttps': 'Open DeepSeek Harness via HTTPS or localhost address.', 'wizardHintProvider': 'Configure STT engine credentials or install SenseVoice model below.',
314
- 'wizardHintWinDesktop': 'System microphone blocked. Open Windows Settings → Privacy & security → Microphone, and turn ON "Let desktop apps access your microphone".', 'wizardHintWinBrowser': 'Microphone permission denied. Allow access in your browser address bar or Windows Settings → Privacy & security → Microphone.', 'wizardHintMacDesktop': 'System microphone blocked. Open macOS System Settings → Privacy & Security → Microphone, and enable DSH Desktop.', 'wizardOpenWinMicSettings': 'Open Windows Mic Settings',
315
- 'wizardFinishSuccess': 'Setup & Verification Complete', 'wizardSkip': 'Skip Wizard',
316
- 'wizardStatusCompleted': 'Microphone and STT verified',
317
- 'exportDiagnosticsBtn': 'Export Diagnostics Report', 'exportingDiagnostics': 'Exporting report…',
318
- 'diagnosticsExportSuccess': 'Report downloaded', 'recentRecognitionsTitle': 'Recent Recognition Telemetry',
319
- 'noRecognitionsYet': 'No recent recognition records yet.', 'diagLatencyBreakdown': 'Latency breakdown:',
320
- 'diagVad': 'VAD: {ms}ms', 'diagStt': 'STT: {ms}ms', 'diagPolish': 'Polish: {ms}ms', 'diagTotal': 'Total: {ms}ms', 'diagBytes': 'Audio: {bytes} B', 'diagWords': 'Words: {count}',
321
- 'diagExportNotice': 'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
25
+ 'saveFailed':'Some fields were not saved —', 'cardHint':'Dictation and voice messages: providers, chains, local whisper',
26
+ 'expand':'Expand', 'collapse':'Collapse', 'composerUnavailable':'Composer unavailable', 'keySpace':'Space', 'keyUnset':'not set',
27
+ 'recognitionError':'recognition failed', 'micUnavailable':'Microphone unavailable: needs HTTPS or localhost',
28
+ 'noRecorder':'MediaRecorder is not supported by this browser', 'browserFailed':'Browser did not recognize: ',
29
+ 'nothingHeard':'Nothing was recognized', 'dictationBtn':'Voice typing', 'messageBtn':'Voice message — click or hold',
30
+ 'dictationPill':'Dictation — text is appended to the input', 'messagePill':'Recording a voice message', 'cancel':'Cancel',
31
+ 'holdHint':'Hold — release to send', 'listening':'Listening in the browser…', 'stop':'Stop', 'transcribing':'Transcribing…',
32
+ 'sendingIn':'Sending to the agent in', 'secondsShort':' s', 'keepPending':'Do not send', 'hide':'Hide', 'title':'Voice',
33
+ 'recordSlot':'Voice recording',
34
+ 'browserHint':'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
35
+ 'browserWebGpu':'Browser WebGPU Whisper (Offline)',
36
+ 'browserWebGpuHint':'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
37
+ 'npuProvider':'Hardware Acceleration (SBC NPU / Host)',
38
+ 'npuProviderHint':'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
39
+ 'npuThreads':'Threads (CPU/SBC)', 'benchmarkTitle':'Hardware Benchmark (Speed & Latency)', 'runBenchmarkBtn':'Run Benchmark',
40
+ 'benchmarking':'Running benchmark…',
41
+ 'benchmarkHint':'Measures real-time factor (RTF) and inference latency on 1.0s audio for current execution provider.',
42
+ 'webgpuStatus':'Browser WebGPU Status', 'webgpuAvailable':'WebGPU Available (hardware acceleration ready)',
43
+ 'webgpuNotAvailable':'WebGPU Not Detected (will fallback to server)', 'openaiHint':'whisper-1 · key OPENAI_API_KEY',
44
+ 'siliconflowHint':'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
45
+ 'deepinfraHint':'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint':'whisper-v3-turbo · key FIREWORKS_API_KEY',
46
+ 'mistralHint':'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint':'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
47
+ 'localHint':'set when the server is started', 'up':'Up', 'down':'Down', 'remove':'Remove', 'addProvider':'Add provider',
48
+ 'chainHint':'Top to bottom is the order they are tried in', 'customName':'name used in the chain', 'customModel':'model',
49
+ 'customKeyName':'key name', 'customModelHint':'model to request (empty means the built-in one)', 'addCustom':'Add your own provider',
50
+ 'loadingSettings':'Loading settings…',
51
+ 'notReady1':'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
52
+ 'notReady2':'the section will appear on its own in a few seconds.', 'hotkey':'Voice message key', 'pressKey':'Press a key…',
53
+ 'clearKey':'Clear the key', 'hotkeyHint1':'Hold it to record, release to send to the agent, Esc cancels. ',
54
+ 'hotkeyHint2':'Any key will do: a letter, an F-key or a modifier.', 'language':'Language', 'langAuto':'Auto-detect',
55
+ 'langRu':'Russian (ru)', 'langEn':'English (en)', 'langZh':'Chinese (zh)', 'langUk':'Ukrainian (uk)', 'langDe':'German (de)',
56
+ 'dictationHint':'Speech is cut at pauses and the text is appended to the input.', 'pauseMs':'Pause that ends a phrase, ms',
57
+ 'pauseHint':'Lower means more frequent chunks and faster text, but a higher risk of cutting a word', 'speaking':'You are speaking…',
58
+ 'silence':'Pause…', 'normalizeTranscript':'Normalize file transcripts', 'undo':'Undo last insert', 'undone':'Insert undone',
59
+ 'nothingToUndo':'Nothing to undo', 'clipping':'Clipping', 'clippingHint':'Too loud (clipping)', 'lowVolume':'Low signal',
60
+ 'lowVolumeHint':'Speak louder', 'vadEngine':'VAD engine', 'vadEngineHint':'Neural Silero VAD (WASM/ONNX) or classic amplitude gate',
61
+ 'vadSilero':'Silero VAD (neural)', 'vadAmplitude':'Amplitude gate', 'streamingWs':'Streaming WebSocket',
62
+ 'streamingWsHint':'Real-time WebSocket audio streaming to local ASR', 'beep':'Beep on start/stop', 'localOnly':'Local whisper only',
63
+ 'localOnlyHint':'Restrict both chains to the local whisper.cpp server: fully offline.', 'sendDelay':'Dictation send delay (ms)',
64
+ 'sendDelayHint':'Wait before appending a dictated phrase, with a cancel window. 0 = off', 'mic':'Microphone',
65
+ 'micDefault':'System default', 'vocabulary':'Custom vocabulary (one word per line)', 'polish':'Polish transcript with model',
66
+ 'polishHint':'Fix punctuation and fillers via the harness model before inserting', 'stream':'Continuous dictation',
67
+ 'streamHint':'Cut phrases by a timer while you speak instead of waiting for a long pause', 'streamChunkMs':'Stream chunk (ms)',
68
+ 'vadAdapt':'Adaptive silence', 'vadAdaptHint':'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
69
+ 'wakeWord':'Wake word', 'wakeWordHint':'Browser recognition starts recording when speech begins with this phrase. Empty = off',
70
+ 'bargeIn':'Barge-in', 'polishSend':'Polish whole draft before sending',
71
+ 'polishSendHint':'Run the composed draft through the model right before sending', 'editablePreview':'Editable preview before send',
72
+ 'editablePreviewHint':'Show recognized text before sending with editing, comparison and abort options.', 'editPreview':'Edit',
73
+ 'sendNow':'Send', 'cancelPreview':'Cancel', 'rawText':'Raw', 'polishedText':'Polished',
74
+ 'sessionMismatch':'Different chat active; canceled to prevent leak.', 'sessionCommands':'Voice session commands',
75
+ 'sessionCommandsHint':'"send", "cancel", "stop", "continue" act on the session instead of becoming text',
76
+ 'polishBaseUrl':'Offline polish endpoint',
77
+ 'polishBaseUrlHint':'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
78
+ 'polishModel':'Offline polish model', 'polishKeyEnv':'Offline polish key credential',
79
+ 'voiceCommandsLabel':'Voice edit commands ("new line", "paragraph")',
80
+ 'normalizeTranscriptHint':'transcribe_audio: spoken numbers to digits, tidy punctuation', 'messageTitle':'Voice message',
81
+ 'messageHint':'One whole recording, sent to the agent once it is transcribed.', 'undoMs':'Undo window, ms',
82
+ 'undoHint':'How long the automatic send can still be called off', 'customTitle':'Your own providers',
83
+ 'customHint':'Any OpenAI-compatible API. The name becomes available in the chains above.', 'general':'General',
84
+ 'whisperEndpoint':'Local whisper: endpoint', 'whisperEndpointHint':'POST /inference of a whisper.cpp server',
85
+ 'deepgramEndpoint':'Deepgram: base URL',
86
+ 'deepgramEndpointHint':'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
87
+ 'whisperBin':'Local whisper: binary', 'whisperBinHint':'used when autostart is on', 'whisperModel':'Local whisper: model',
88
+ 'whisperModelHint':'Absolute path to the ggml model file', 'whisperAutostart':'Autostart the local whisper',
89
+ 'bilingualDeveloperPrompt':'Bilingual developer context (RU/EN)',
90
+ 'bilingualDeveloperPromptHint':'Hint technical terms and CLI commands to the STT model to prevent phonetic transliteration',
91
+ 'calibrateNoise':'Calibrate Noise (2s)',
92
+ 'calibrateNoiseHint':'Measure room noise for 2 seconds and auto-set the optimal noise gate threshold', 'calibrating':'Measuring noise…',
93
+ 'calibratedMsg':'Gate set: {db} dB (ambient: {noise} dB)', 'calibratedValue':'Calibrated', 'save':'Save', 'saved':'Saved ✓',
94
+ 'openrouterWarning':'OpenRouter has no /audio/transcriptions — use the openai-chat-audio template there',
95
+ 'noiseSuppression':'Hardware noise suppression',
96
+ 'noiseSuppressionHint':'Enable browser noise suppression, echo cancellation, and auto gain control',
97
+ 'contextGlossary':'Context glossary injection',
98
+ 'contextGlossaryHint':'Auto-extract code identifiers and terms from composer to improve STT accuracy',
99
+ 'providerDashboard':'Provider Latency & Health', 'avgLatency':'Avg latency', 'successRate':'Success', 'fast':'Fast', 'normal':'Normal',
100
+ 'slow':'Slow', 'error':'Error', 'idle':'No calls', 'play':'Play', 'pause':'Pause', 'listenBack':'Listen back',
101
+ 'lastRecording':'Last voice note', 'sensevoiceHint':'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
102
+ 'sensevoiceEndpoint':'SenseVoice: endpoint', 'sensevoiceEndpointHint':'POST endpoint of sherpa-onnx or compatible server',
103
+ 'sensevoiceBin':'SenseVoice: binary', 'sensevoiceBinHint':'used when autostart is on', 'sensevoiceModel':'SenseVoice: model path',
104
+ 'sensevoiceModelHint':'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
105
+ 'sensevoiceAutostart':'Autostart the SenseVoice server', 'visualizerStyle':'Audio visualizer style',
106
+ 'visualizerStyleHint':'Waveform animation inside the recording pill', 'visLiquidWave':'Liquid Wave', 'visDynamicOrb':'Dynamic Orb',
107
+ 'visBars':'Classic Bars', 'visOff':'Off', 'statusTitle':'Connection & Engine Status',
108
+ 'statusDesc':'Current runtime connectivity to voice backends and recognition services.', 'badgeHostOnline':'Host online ({ms} ms)',
109
+ 'badgeHostOffline':'Host unreachable', 'badgeWhisperActive':'Whisper active', 'badgeWhisperInactive':'Whisper offline',
110
+ 'badgeSenseVoiceActive':'SenseVoice active', 'badgeSenseVoiceInactive':'SenseVoice offline', 'badgeProviders':'{count} providers ready',
111
+ 'refreshStats':'Refresh Stats', 'hardwareTitle':'Hardware & Local Engines',
112
+ 'hardwareDesc':'Microphone device, local whisper.cpp and SenseVoice runtime settings.', 'uiError':'Voice UI Error:', 'retry':'Retry',
113
+ 'testMic':'Test Mic', 'stopTest':'Stop Test', 'micLevel':'Mic level', 'noiseGate':'Noise Gate (dB)',
114
+ 'noiseGateHint':'Mute background fan hum and keyboard clicks below threshold. Off = disabled', 'noiseGateOff':'Off',
115
+ 'versionTitle':'Plugin Version & Updates', 'versionHint':'Check for new versions and update directly from npm.',
116
+ 'updateAvailable':'Update available', 'upToDate':'Up to date',
117
+ 'updatePrompt':'A new version is available on npm. Click update to install.', 'updateNow':'Update now', 'updating':'Updating…',
118
+ 'updateSuccess':'Updated successfully! Restart DSH to apply.', 'updateFailed':'Update failed. Check logs.',
119
+ 'updateCheckFailed':'Update check failed',
120
+ 'profileLockedPid':'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
121
+ 'profileLockedExisting':'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
122
+ 'profileLockCheckError':'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
123
+ 'checkingUpdates':'Checking updates…', 'updateStatusUnknown':'Version status unknown',
124
+ 'sensevoiceTitle':'SenseVoice Local ASR (1-Click Setup)',
125
+ 'sensevoiceDesc':'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
126
+ 'installSensevoice':'Install SenseVoice-Small (1-Click)', 'installingSensevoice':'Installing SenseVoice…', 'sensevoiceReady':'Ready',
127
+ 'sensevoiceNotInstalled':'Not Installed', 'sensevoiceDownloading':'Downloading model…',
128
+ 'turnTakingTitle':'Turn-Taking & Audio Coordination',
129
+ 'turnTakingDesc':'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
130
+ 'gatedTurnTaking':'Gated Mode (mute mic while assistant is speaking)',
131
+ 'bargeInDesc':'True Barge-In (speaking interrupts assistant speech)', 'smartActionsTitle':'Hands-free & Smart Actions',
132
+ 'autoSendVisualRing':'Silence Ring countdown animation',
133
+ 'voiceActionsLabel':'Voice action commands ("send", "cancel", "clear", "new line")',
134
+ 'liveInterimPreview':'Live speech preview (ghost text preview)',
135
+ 'structuredPromptVoice':'Spoken answers for interactive choice prompts', 'modeVerbatim':'Verbatim', 'modeClean':'Clean Text',
136
+ 'modeTask':'Agent Task', 'modeNote':'Note', 'dictationModeTitle':'Dictation Post-Processing Mode',
137
+ 'dictationModeDesc':'Select how dictated speech is processed before insertion: Verbatim (0 LLM calls), Clean text (formatting & filler removal), Agent task (structured prompt), or Note (key points & actions).',
138
+ 'dictationPromptsTitle':'Dictation Prompt Templates',
139
+ 'dictationPromptsDesc':'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
140
+ 'promptClean':'Clean text prompt', 'promptTask':'Agent task prompt', 'promptNote':'Note prompt', 'resetPrompts':'Reset to defaults',
141
+ 'voiceMacrosTitle':'Voice Action Macros',
142
+ 'voiceMacrosDesc':'Spoken trigger phrases automatically expanded into structured prompt templates.', 'addMacro':'Add macro',
143
+ 'macroTrigger':'Spoken trigger phrase', 'macroTemplate':'Expanded prompt template', 'macroDelete':'Delete', 'rawTab':'Raw STT',
144
+ 'resultTab':'Result', 'llmUnavailableFallback':'LLM unavailable, showing raw transcript', 'jargonTitle':'Developer Lexicon & IT Jargon',
145
+ 'techJargonCorrection':'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)', 'jargonAdd':'Add replacement',
146
+ 'jargonFrom':'Spoken slang', 'jargonTo':'Correct spelling', 'jargonPlaceholder':'k8s -> kubernetes\npr -> pull request',
147
+ 'voiceInput':'Voice input', 'assistantSpeakingGated':'Assistant speaking (gated mode)', 'jargonFormatHint':' (spoken -> replacement)',
148
+ 'errorPrefix':'Error', 'wizardTitle':'First-Run Setup & Diagnostics Wizard',
149
+ 'wizardDesc':'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
150
+ 'wizardBtnLaunch':'Launch Mic & STT Wizard', 'wizardClose':'Close Wizard', 'wizardStep1':'1. Audio & Environment',
151
+ 'wizardStep2':'2. Live Signal Test', 'wizardStep3':'3. STT Transcription Test',
152
+ 'wizardSecureContextOk':'Secure context / HTTPS verified', 'wizardSecureContextBad':'Insecure context (HTTPS or localhost required)',
153
+ 'wizardMediaDevicesOk':'Browser mediaDevices API supported', 'wizardMediaDevicesBad':'Browser mediaDevices API missing',
154
+ 'wizardMediaRecorderOk':'MediaRecorder API supported', 'wizardMediaRecorderBad':'MediaRecorder API unsupported in this browser',
155
+ 'wizardMicPermissionOk':'Microphone permission granted', 'wizardMicPermissionPrompt':'Microphone permission not yet granted',
156
+ 'wizardMicPermissionBad':'Microphone permission denied', 'wizardRequestMic':'Request Microphone Access',
157
+ 'wizardMicLabel':'Microphone input device:', 'wizardStartSignalCheck':'Start Audio Level Test',
158
+ 'wizardStopSignalCheck':'Stop Audio Test', 'wizardSignalOk':'Audio signal detected (RMS: {level}%)',
159
+ 'wizardSignalZero':'Zero audio level detected - speak into microphone or select correct device',
160
+ 'wizardSignalTesting':'Listening for audio signal… speak into your microphone', 'wizardProviderLabel':'Active STT engine & model:',
161
+ 'wizardProcessingArea':'Processing area:', 'wizardAreaBrowser':'In-browser (WebGPU / WebSpeech)',
162
+ 'wizardAreaServer':'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud':'Remote cloud provider',
163
+ 'wizardStartTestRecord':'Record 3-Second Test Phrase', 'wizardRecordingTest':'Recording test phrase… speak now ({left}s)',
164
+ 'wizardTranscribingTest':'Transcribing test phrase…', 'wizardTestResult':'STT Test Result:',
165
+ 'wizardTestSuccess':'STT round-trip verified successfully! Latency: {latency}ms',
166
+ 'wizardTestEmpty':'No speech detected in test recording. Please speak clearly and try again.',
167
+ 'wizardTestFailed':'STT test failed: {error}', 'wizardNextStepHint':'Next step: {hint}',
168
+ 'wizardHintPermission':'Click "Request Microphone Access" and allow browser permission prompt.',
169
+ 'wizardHintDevice':'Check microphone connection or select a different input device.',
170
+ 'wizardHintHttps':'Open DeepSeek Harness via HTTPS or localhost address.',
171
+ 'wizardHintProvider':'Configure STT engine credentials or install SenseVoice model below.',
172
+ 'wizardHintWinDesktop':'System microphone blocked. Open Windows Settings → Privacy & security → Microphone, and turn ON "Let desktop apps access your microphone".',
173
+ 'wizardHintWinBrowser':'Microphone permission denied. Allow access in your browser address bar or Windows Settings → Privacy & security → Microphone.',
174
+ 'wizardHintMacDesktop':'System microphone blocked. Open macOS System Settings → Privacy & Security → Microphone, and enable DSH Desktop.',
175
+ 'wizardOpenWinMicSettings':'Open Windows Mic Settings', 'wizardFinishSuccess':'Setup & Verification Complete',
176
+ 'wizardSkip':'Skip Wizard', 'wizardStatusCompleted':'Microphone and STT verified', 'exportDiagnosticsBtn':'Export Diagnostics Report',
177
+ 'exportingDiagnostics':'Exporting report…', 'diagnosticsExportSuccess':'Report downloaded',
178
+ 'recentRecognitionsTitle':'Recent Recognition Telemetry', 'noRecognitionsYet':'No recent recognition records yet.',
179
+ 'diagLatencyBreakdown':'Latency breakdown:', 'diagVad':'VAD: {ms}ms', 'diagStt':'STT: {ms}ms', 'diagPolish':'Polish: {ms}ms',
180
+ 'diagTotal':'Total: {ms}ms', 'diagBytes':'Audio: {bytes} B', 'diagWords':'Words: {count}',
181
+ 'diagExportNotice':'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
182
+ 'hostOnline':'Host online', 'hostOffline':'Host unreachable', 'whisperReady':'Whisper ready', 'whisperStopped':'Whisper stopped',
183
+ 'providersAvailable':'providers available', 'macroEmpty':'No macros configured.', 'macroTemplateArgHint':' (use {text} for argument)',
184
+ 'noiseGateLow':'-50 dB (Low / Gentle)', 'noiseGateStandard':'-45 dB (Standard Default)', 'noiseGateMedium':'-35 dB (Medium Room)',
185
+ 'noiseGateHigh':'-25 dB (High / Mechanical Clicks)', 'npuCpu':'CPU (Standard)',
186
+ 'installModelSensevoice':'SenseVoice-Small (150MB, ~50ms)', 'installModelWhisperTurbo':'Whisper Large-v3-Turbo (800MB, High-Accuracy)',
187
+ 'promptCleanPlaceholder':'Default: punctuation & filler-word cleanup',
188
+ 'promptTaskPlaceholder':'Default: structured problem/outcome/constraints/criteria',
189
+ 'promptNotePlaceholder':'Default: key points & action items summary', 'settingsConflict':'Settings conflict: parallel changes detected',
190
+ 'saveFailedFallback':'Save failed', 'benchmarkFailed':'Benchmark failed', 'networkError':'Network error', 'benchmarkOffline':'Offline',
191
+ 'benchmarkCores':'Cores', 'benchmarkCoresUnit':'cores', 'benchRatingUltraFast':'Ultra-fast', 'benchRatingRealtime':'Real-time',
192
+ 'msShort':'ms', 'transcriptionTimeout':'Transcription request timed out (60s)', 'audioContextUnsupported':'AudioContext not supported',
193
+ 'audioReadFailed':'failed to read audio', 'fileReaderUnsupported':'FileReader not supported',
194
+ 'benchmarkNoAsrServer':'Local ASR server is not running. Hardware detected.',
322
195
  }
323
196
 
324
- const zh = {
325
- 'saveFailed': '部分字段未保存 —',
326
- 'cardHint': '听写与语音消息:服务商、备用链、本地 whisper',
327
- 'expand': '展开',
328
- 'collapse': '收起',
329
- 'composerUnavailable': '输入框不可用',
330
- 'keySpace': '空格',
331
- 'keyUnset': '未设置',
332
- 'recognitionError': '识别失败',
333
- 'micUnavailable': '麦克风不可用:需要 HTTPS 或 localhost',
334
- 'noRecorder': '当前浏览器不支持 MediaRecorder',
335
- 'browserFailed': '浏览器未能识别:',
336
- 'nothingHeard': '未识别到任何内容',
337
- 'dictationBtn': '语音输入',
338
- 'messageBtn': '语音消息 — 点击或按住',
339
- 'dictationPill': '听写 — 文本将追加至输入框',
340
- 'messagePill': '正在录制语音消息',
341
- 'cancel': '取消',
342
- 'holdHint': '按住录音 — 松开即发送',
343
- 'listening': '浏览器正在倾听…',
344
- 'stop': '停止',
345
- 'transcribing': '正在转写…',
346
- 'sendingIn': '发送至智能体倒计时',
347
- 'secondsShort': ' 秒',
348
- 'keepPending': '不发送',
349
- 'hide': '隐藏',
350
- 'title': '语音',
351
- 'recordSlot': '语音录制',
352
- 'browserHint': 'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
353
- 'browserWebGpu': '浏览器 WebGPU Whisper (离线)',
354
- 'browserWebGpuHint': '通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
355
- 'npuProvider': '硬件加速 (SBC NPU / 主机)',
356
- 'npuProviderHint': 'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。',
357
- 'npuThreads': '计算线程 (CPU/SBC)',
358
- 'benchmarkTitle': '硬件基准测试 (速度与延迟)',
359
- 'runBenchmarkBtn': '运行基准测试', 'benchmarking': '正在运行测试…',
360
- 'benchmarkHint': '测试当前执行提供者在 1.0 秒音频下的实时率 (RTF) 与推理延迟。',
361
- 'webgpuStatus': '浏览器 WebGPU 状态',
362
- 'webgpuAvailable': 'WebGPU 可用 (硬件加速就绪)',
363
- 'webgpuNotAvailable': '未检测到 WebGPU (将自动回退到服务端)',
364
- 'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
365
- 'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
366
- 'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
367
- 'localHint': '服务器启动时配置', 'up': '上移', 'down': '下移', 'remove': '移除',
368
- 'addProvider': '添加服务商',
369
- 'chainHint': '自上而下为故障转移尝试顺序',
370
- 'customName': '备用链中使用的名称',
371
- 'customModel': '模型',
372
- 'customKeyName': '密钥名称',
373
- 'customModelHint': '请求的模型(留空使用内置默认值)',
374
- 'addCustom': '添加自定义服务商',
375
- 'loadingSettings': '正在加载设置…',
376
- 'notReady1': 'Harness 尚未同步此插件的设置。如果刚重启,',
377
- 'notReady2': '设置项将在数秒内自动呈现。',
378
- 'hotkey': '语音消息快捷键',
379
- 'pressKey': '请按下按键…',
380
- 'clearKey': '清除按键',
381
- 'hotkeyHint1': '按住录音,松开发送给智能体,按 Esc 取消。',
382
- 'hotkeyHint2': '支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
383
- 'language': '识别语言',
384
- 'langAuto': '自动检测',
385
- 'langRu': '俄语 (ru)',
386
- 'langEn': '英语 (en)',
387
- 'langZh': '中文 (zh)',
388
- 'langUk': '乌克兰语 (uk)',
389
- 'langDe': '德语 (de)',
390
- 'dictationHint': '语音按停顿自动切句,文字实时追加至输入框。',
391
- 'pauseMs': '断句静音停顿时长(毫秒)',
392
- 'pauseHint': '数值越小切分越频繁、出字越快,但可能切断未说完的词',
393
- 'speaking': '正在说话…',
394
- 'silence': '停顿…',
395
- 'normalizeTranscript': '规范化转写文本',
396
- 'undo': '撤销上次插入',
397
- 'undone': '插入已撤销',
398
- 'nothingToUndo': '没有可撤销的内容',
399
- 'clipping': '爆音', 'clippingHint': '音量过大(爆音截断)',
400
- 'lowVolume': '信号微弱', 'lowVolumeHint': '请提高音量',
401
- 'vadEngine': 'VAD 引擎', 'vadEngineHint': '神经网络 Silero VAD (WASM/ONNX) 或传统振幅门限',
402
- 'vadSilero': 'Silero VAD(神经网络)', 'vadAmplitude': '振幅门限',
403
- 'streamingWs': '流式 WebSocket', 'streamingWsHint': '向本地 ASR 实时通过 WebSocket 流式传输音频',
404
- 'beep': '启停提示音',
405
- 'localOnly': '仅使用本地 whisper',
406
- 'localOnlyHint': '将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。',
407
- 'sendDelay': '听写延迟追加 (ms)',
408
- 'sendDelayHint': '追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
409
- 'mic': '麦克风设备',
410
- 'micDefault': '系统默认',
411
- 'vocabulary': '自定义词汇表(每行一个词)',
412
- 'polish': '模型润色转写文本',
413
- 'polishHint': '插入前通过模型修正标点符号并消除语气口头禅',
414
- 'stream': '连续流式听写',
415
- 'streamHint': '说话期间按定时器切分短句,无需等待长时间停顿',
416
- 'streamChunkMs': '流式切片时长 (ms)',
417
- 'vadAdapt': '自适应静音阈值',
418
- 'vadAdaptHint': '根据语速动态调整停顿静音阈值。0 为固定阈值',
419
- 'wakeWord': '语音唤醒词',
420
- 'wakeWordHint': '说话以此短语开头时自动触发录音。留空为关闭',
421
- 'bargeIn': '语音插话打断 (Barge-in)',
422
- 'polishSend': '发送前润色整篇草稿',
423
- 'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
424
- 'editablePreview': '发送前可编辑预览',
425
- 'editablePreviewHint': '发送前显示识别文本,支持编辑、润色对比和撤销。',
426
- 'editPreview': '编辑',
427
- 'sendNow': '发送',
428
- 'cancelPreview': '取消',
429
- 'rawText': '原文',
430
- 'polishedText': '润色',
431
- 'sessionMismatch': '当前会话已切换,已拦截发送以防止串话。',
432
- 'sessionCommands': '会话语音控制指令',
433
- 'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
434
- 'polishBaseUrl': '离线润色服务端点',
435
- 'polishBaseUrlHint': '兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型',
436
- 'polishModel': '离线润色模型',
437
- 'polishKeyEnv': '离线润色密钥凭据名',
438
- 'voiceCommandsLabel': '语音编辑指令("换行"、"段落")',
439
- 'normalizeTranscriptHint': 'transcribe_audio:口语数字转阿拉伯数字,整理标点符号',
440
- 'messageTitle': '语音消息',
441
- 'messageHint': '录制单条完整语音,转写完成后自动发送给智能体。',
442
- 'undoMs': '撤回窗口时长(毫秒)',
443
- 'undoHint': '转写完成后多久以内可以随时取消发送',
444
- 'customTitle': '自定义语音服务商',
445
- 'customHint': '兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。',
446
- 'general': '通用设置',
447
- 'whisperEndpoint': '本地 whisper:端点地址',
448
- 'whisperEndpointHint': 'whisper.cpp 服务器的 POST /inference 接口',
449
- 'deepgramEndpoint': 'Deepgram:基础地址',
450
- 'deepgramEndpointHint': 'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)',
451
- 'whisperBin': '本地 whisper:可执行文件',
452
- 'whisperBinHint': '开启自动启动时使用',
453
- 'whisperModel': '本地 whisper:模型文件',
454
- 'whisperModelHint': 'ggml 模型文件的绝对路径',
455
- 'whisperAutostart': '自动启动本地 whisper',
456
- 'bilingualDeveloperPrompt': '双语开发者提示 (中/英)',
457
- 'bilingualDeveloperPromptHint': '提示常用技术术语与命令行指令,避免语音模型音译',
458
- 'calibrateNoise': '校准背景噪声 (2秒)',
459
- 'calibrateNoiseHint': '测量 2 秒环境背景噪声并自动计算最佳门限',
460
- 'calibrating': '正在测量噪声…',
461
- 'calibratedMsg': '门限已设置为: {db} dB (背景噪声: {noise} dB)',
462
- 'calibratedValue': '已校准',
463
- 'save': '保存',
464
- 'saved': '已保存 ✓',
465
- 'openrouterWarning': 'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板',
466
- 'noiseSuppression': '硬件级降噪',
467
- 'noiseSuppressionHint': '启用浏览器硬件降噪、回声消除与自动增益控制',
468
- 'contextGlossary': '上下文专业词汇注入',
469
- 'contextGlossaryHint': '自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
470
- 'providerDashboard': '服务商延迟与健康监控',
471
- 'avgLatency': '平均延迟',
472
- 'successRate': '成功率',
473
- 'fast': '极速',
474
- 'normal': '正常',
475
- 'slow': '较慢',
476
- 'error': '异常',
477
- 'idle': '无调用记录',
478
- 'play': '播放',
479
- 'pause': '暂停',
480
- 'listenBack': '试听回放',
481
- 'lastRecording': '最近一条语音记录',
482
- 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)',
483
- 'sensevoiceEndpoint': 'SenseVoice:端点地址',
484
- 'sensevoiceEndpointHint': 'sherpa-onnx 或兼容服务器的 POST 端点',
485
- 'sensevoiceBin': 'SenseVoice:可执行文件',
486
- 'sensevoiceBinHint': '开启自动启动时使用',
487
- 'sensevoiceModel': 'SenseVoice:模型路径',
488
- 'sensevoiceModelHint': 'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
489
- 'sensevoiceAutostart': '自动启动 SenseVoice 服务',
490
- 'visualizerStyle': '音频动态波形样式',
491
- 'visualizerStyleHint': '录音胶囊面板内的动态音频波形动画效果',
492
- 'visLiquidWave': '流体水波 (Liquid Wave)',
493
- 'visDynamicOrb': '动态脉冲球体 (Dynamic Orb)',
494
- 'visBars': '经典频条 (Classic Bars)',
495
- 'visOff': '关闭',
496
- 'statusTitle': '连接与引擎状态看板',
497
- 'statusDesc': '当前语音后端与识别服务的实时网络与进程就绪状态。',
498
- 'badgeHostOnline': '服务端在线 ({ms} ms)',
499
- 'badgeHostOffline': '服务端不可达',
500
- 'badgeWhisperActive': 'Whisper 就绪',
501
- 'badgeWhisperInactive': 'Whisper 未就绪',
502
- 'badgeSenseVoiceActive': 'SenseVoice 就绪',
503
- 'badgeSenseVoiceInactive': 'SenseVoice 未就绪',
504
- 'badgeProviders': '{count} 个服务商已就绪',
505
- 'refreshStats': '刷新状态',
506
- 'hardwareTitle': '硬件与本地引擎',
507
- 'hardwareDesc': '麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。',
508
- 'uiError': '语音界面错误:',
509
- 'retry': '重试',
510
- 'testMic': '测试麦克风',
511
- 'stopTest': '停止测试',
512
- 'micLevel': '麦克风电平',
513
- 'noiseGate': '噪声门 (dB)',
514
- 'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
515
- 'noiseGateOff': '关闭',
516
- 'versionTitle': '插件版本与更新',
517
- 'versionHint': '检查新版本并直接从 npm 更新。',
518
- 'updateAvailable': '有可用更新',
519
- 'upToDate': '已是最新版本',
520
- 'updatePrompt': 'npm 上有新版本可用。点击更新进行安装。',
521
- 'updateNow': '立即更新',
522
- 'updating': '正在更新…',
523
- 'updateSuccess': '更新成功!请重启 DSH 生效。',
524
- 'updateFailed': '更新失败,请查看日志。',
525
- 'updateCheckFailed': '检查更新失败',
526
- 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
527
- 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
528
- 'profileLockCheckError': '检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
529
- 'checkingUpdates': '正在检查更新…',
530
- 'updateStatusUnknown': '版本状态未知',
531
- 'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
532
- 'sensevoiceDesc': 'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。',
533
- 'installSensevoice': '一键安装 SenseVoice-Small',
534
- 'installingSensevoice': '正在安装 SenseVoice…',
535
- 'sensevoiceReady': '已就绪',
536
- 'sensevoiceNotInstalled': '未安装',
537
- 'sensevoiceDownloading': '正在下载模型…',
538
- 'turnTakingTitle': '轮流对话与音频协同',
539
- 'turnTakingDesc': '协调听与说,避免扬声器播放的声音被麦克风二次拾音。',
540
- 'gatedTurnTaking': '门控模式 (助手播报时麦克风自动静音)',
541
- 'bargeInDesc': '实时打断 (说话时立即打断助手朗读)',
542
- 'smartActionsTitle': '免提与智能指令',
543
- 'autoSendVisualRing': '环形倒计时动画指示器',
544
- 'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
545
- 'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
546
- 'structuredPromptVoice': '语音应答交互式选择提示',
547
- 'modeVerbatim': '逐字直录',
548
- 'modeClean': '清爽文本',
549
- 'modeTask': '智能体任务',
550
- 'modeNote': '便签备忘',
551
- 'dictationModeTitle': '听写后处理模式',
552
- 'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
553
- 'dictationPromptsTitle': '听写提示词模板',
554
- 'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
555
- 'promptClean': '清爽文本提示词',
556
- 'promptTask': '智能体任务提示词',
557
- 'promptNote': '便签提示词',
558
- 'resetPrompts': '恢复默认模板',
559
- 'voiceMacrosTitle': '语音动作宏',
560
- 'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
561
- 'addMacro': '添加动作宏',
562
- 'macroTrigger': '语音触发短语',
563
- 'macroTemplate': '展开提示词模板',
564
- 'macroDelete': '删除',
565
- 'rawTab': '原始识别',
566
- 'resultTab': '处理结果',
567
- 'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
568
- 'jargonTitle': '开发者词汇与 IT 术语',
569
- 'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
570
- 'jargonAdd': '添加替换项',
571
- 'jargonFrom': '口语词汇',
572
- 'jargonTo': '标准拼写',
573
- 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
574
- 'voiceInput': '语音输入',
575
- 'assistantSpeakingGated': '助手正在发言(门控模式)',
576
- 'jargonFormatHint': ' (口语 -> 替换文本)',
577
- 'errorPrefix': '错误',
578
- 'wizardTitle': '首次启动与麦克风诊断向导',
579
- 'wizardDesc': '分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。',
580
- 'wizardBtnLaunch': '启动麦克风与 STT 诊断向导',
581
- 'wizardClose': '关闭向导',
582
- 'wizardStep1': '1. 音频与运行环境',
583
- 'wizardStep2': '2. 实时信号测试',
584
- 'wizardStep3': '3. STT 识别回环测试',
585
- 'wizardSecureContextOk': '安全上下文 / HTTPS 已验证',
586
- 'wizardSecureContextBad': '非安全上下文(需 HTTPS 或 localhost)',
587
- 'wizardMediaDevicesOk': '浏览器 mediaDevices API 支持正常',
588
- 'wizardMediaDevicesBad': '浏览器缺少 mediaDevices API 支持',
589
- 'wizardMediaRecorderOk': 'MediaRecorder API 支持正常', 'wizardMediaRecorderBad': '当前浏览器不支持 MediaRecorder API',
590
- 'wizardMicPermissionOk': '麦克风权限已授予', 'wizardMicPermissionPrompt': '尚未获得麦克风授权', 'wizardMicPermissionBad': '麦克风权限被拒绝',
591
- 'wizardRequestMic': '请求麦克风权限',
592
- 'wizardMicLabel': '麦克风输入设备:',
593
- 'wizardStartSignalCheck': '开始音频电平测试',
594
- 'wizardStopSignalCheck': '停止音频测试',
595
- 'wizardSignalOk': '检测到音频信号 (RMS: {level}%)',
596
- 'wizardSignalZero': '未检测到声音信号 — 请对着麦克风说话或选择正确设备',
597
- 'wizardSignalTesting': '正在监听声音信号… 请对着麦克风发声',
598
- 'wizardProviderLabel': '当前语音引擎与模型:',
599
- 'wizardProcessingArea': '处理区域:', 'wizardAreaBrowser': '浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer': '本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud': '远程云端服务',
600
- 'wizardStartTestRecord': '录制 3 秒测试语句', 'wizardRecordingTest': '正在录制测试音频… 请说话 ({left}秒)',
601
- 'wizardTranscribingTest': '正在请求语音识别…', 'wizardTestResult': 'STT 测试结果:',
602
- 'wizardTestSuccess': 'STT 链路验证成功!耗时:{latency}毫秒',
603
- 'wizardTestEmpty': '录音中未检测到有效语音,请清晰发音后重试。',
604
- 'wizardTestFailed': 'STT 测试失败:{error}',
605
- 'wizardNextStepHint': '建议下一步:{hint}',
606
- 'wizardHintPermission': '点击“请求麦克风权限”并在浏览器弹窗中允许访问。', 'wizardHintDevice': '检查麦克风硬件连接或切换选择其他输入设备。', 'wizardHintHttps': '请通过 HTTPS 或 localhost 访问 DeepSeek Harness。', 'wizardHintProvider': '请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
607
- 'wizardHintWinDesktop': '系统麦克风被阻止。请在 Windows【设置】→【隐私和安全性】→【麦克风】中开启最下方的【允许桌面应用访问你的麦克风】。', 'wizardHintWinBrowser': '麦克风权限被拒绝。请在浏览器地址栏允许麦克风权限,或在 Windows【设置】→【隐私和安全性】→【麦克风】中开启访问。', 'wizardHintMacDesktop': '系统麦克风被阻止。请在 macOS【系统设置】→【隐私与安全性】→【麦克风】中勾选允许 DSH Desktop。', 'wizardOpenWinMicSettings': '打开系统麦克风设置',
608
- 'wizardFinishSuccess': '设置与验证已完成', 'wizardSkip': '跳过向导',
609
- 'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
610
- 'exportDiagnosticsBtn': '导出诊断报告', 'exportingDiagnostics': '正在导出报告…',
611
- 'diagnosticsExportSuccess': '诊断报告已下载', 'recentRecognitionsTitle': '近期识别遥测详情',
612
- 'noRecognitionsYet': '暂无近期识别记录。', 'diagLatencyBreakdown': '耗时分布:',
613
- 'diagVad': 'VAD:{ms}毫秒', 'diagStt': 'STT:{ms}毫秒', 'diagPolish': '后处理:{ms}毫秒', 'diagTotal': '总耗时:{ms}毫秒', 'diagBytes': '音频大小:{bytes} 字节', 'diagWords': '词数:{count}',
614
- 'diagExportNotice': '导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。',
197
+ const zh = {
198
+ 'saveFailed':'部分字段未保存 —', 'cardHint':'听写与语音消息:服务商、备用链、本地 whisper', 'expand':'展开', 'collapse':'收起', 'composerUnavailable':'输入框不可用',
199
+ 'keySpace':'空格', 'keyUnset':'未设置', 'recognitionError':'识别失败', 'micUnavailable':'麦克风不可用:需要 HTTPS 或 localhost',
200
+ 'noRecorder':'当前浏览器不支持 MediaRecorder', 'browserFailed':'浏览器未能识别:', 'nothingHeard':'未识别到任何内容', 'dictationBtn':'语音输入',
201
+ 'messageBtn':'语音消息 — 点击或按住', 'dictationPill':'听写 — 文本将追加至输入框', 'messagePill':'正在录制语音消息', 'cancel':'取消', 'holdHint':'按住录音 — 松开即发送',
202
+ 'listening':'浏览器正在倾听…', 'stop':'停止', 'transcribing':'正在转写…', 'sendingIn':'发送至智能体倒计时', 'secondsShort':' 秒', 'keepPending':'不发送',
203
+ 'hide':'隐藏', 'title':'语音', 'recordSlot':'语音录制', 'browserHint':'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
204
+ 'browserWebGpu':'浏览器 WebGPU Whisper (离线)', 'browserWebGpuHint':'通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
205
+ 'npuProvider':'硬件加速 (SBC NPU / 主机)',
206
+ 'npuProviderHint':'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。', 'npuThreads':'计算线程 (CPU/SBC)',
207
+ 'benchmarkTitle':'硬件基准测试 (速度与延迟)', 'runBenchmarkBtn':'运行基准测试', 'benchmarking':'正在运行测试…',
208
+ 'benchmarkHint':'测试当前执行提供者在 1.0 秒音频下的实时率 (RTF) 与推理延迟。', 'webgpuStatus':'浏览器 WebGPU 状态', 'webgpuAvailable':'WebGPU 可用 (硬件加速就绪)',
209
+ 'webgpuNotAvailable':'未检测到 WebGPU (将自动回退到服务端)', 'openaiHint':'whisper-1 · 密钥 OPENAI_API_KEY',
210
+ 'siliconflowHint':'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
211
+ 'deepinfraHint':'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint':'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
212
+ 'mistralHint':'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint':'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
213
+ 'localHint':'服务器启动时配置', 'up':'上移', 'down':'下移', 'remove':'移除', 'addProvider':'添加服务商', 'chainHint':'自上而下为故障转移尝试顺序',
214
+ 'customName':'备用链中使用的名称', 'customModel':'模型', 'customKeyName':'密钥名称', 'customModelHint':'请求的模型(留空使用内置默认值)', 'addCustom':'添加自定义服务商',
215
+ 'loadingSettings':'正在加载设置…', 'notReady1':'Harness 尚未同步此插件的设置。如果刚重启,', 'notReady2':'设置项将在数秒内自动呈现。', 'hotkey':'语音消息快捷键',
216
+ 'pressKey':'请按下按键…', 'clearKey':'清除按键', 'hotkeyHint1':'按住录音,松开发送给智能体,按 Esc 取消。', 'hotkeyHint2':'支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
217
+ 'language':'识别语言', 'langAuto':'自动检测', 'langRu':'俄语 (ru)', 'langEn':'英语 (en)', 'langZh':'中文 (zh)', 'langUk':'乌克兰语 (uk)',
218
+ 'langDe':'德语 (de)', 'dictationHint':'语音按停顿自动切句,文字实时追加至输入框。', 'pauseMs':'断句静音停顿时长(毫秒)', 'pauseHint':'数值越小切分越频繁、出字越快,但可能切断未说完的词',
219
+ 'speaking':'正在说话…', 'silence':'停顿…', 'normalizeTranscript':'规范化转写文本', 'undo':'撤销上次插入', 'undone':'插入已撤销', 'nothingToUndo':'没有可撤销的内容',
220
+ 'clipping':'爆音', 'clippingHint':'音量过大(爆音截断)', 'lowVolume':'信号微弱', 'lowVolumeHint':'请提高音量', 'vadEngine':'VAD 引擎',
221
+ 'vadEngineHint':'神经网络 Silero VAD (WASM/ONNX) 或传统振幅门限', 'vadSilero':'Silero VAD(神经网络)', 'vadAmplitude':'振幅门限',
222
+ 'streamingWs':'流式 WebSocket', 'streamingWsHint':'向本地 ASR 实时通过 WebSocket 流式传输音频', 'beep':'启停提示音', 'localOnly':'仅使用本地 whisper',
223
+ 'localOnlyHint':'将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。', 'sendDelay':'听写延迟追加 (ms)', 'sendDelayHint':'追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
224
+ 'mic':'麦克风设备', 'micDefault':'系统默认', 'vocabulary':'自定义词汇表(每行一个词)', 'polish':'模型润色转写文本', 'polishHint':'插入前通过模型修正标点符号并消除语气口头禅',
225
+ 'stream':'连续流式听写', 'streamHint':'说话期间按定时器切分短句,无需等待长时间停顿', 'streamChunkMs':'流式切片时长 (ms)', 'vadAdapt':'自适应静音阈值',
226
+ 'vadAdaptHint':'根据语速动态调整停顿静音阈值。0 为固定阈值', 'wakeWord':'语音唤醒词', 'wakeWordHint':'说话以此短语开头时自动触发录音。留空为关闭', 'bargeIn':'语音插话打断 (Barge-in)',
227
+ 'polishSend':'发送前润色整篇草稿', 'polishSendHint':'发送给智能体前对整个输入框草稿进行一次整体润色', 'editablePreview':'发送前可编辑预览',
228
+ 'editablePreviewHint':'发送前显示识别文本,支持编辑、润色对比和撤销。', 'editPreview':'编辑', 'sendNow':'发送', 'cancelPreview':'取消', 'rawText':'原文',
229
+ 'polishedText':'润色', 'sessionMismatch':'当前会话已切换,已拦截发送以防止串话。', 'sessionCommands':'会话语音控制指令',
230
+ 'sessionCommandsHint':'"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本', 'polishBaseUrl':'离线润色服务端点',
231
+ 'polishBaseUrlHint':'兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型', 'polishModel':'离线润色模型', 'polishKeyEnv':'离线润色密钥凭据名',
232
+ 'voiceCommandsLabel':'语音编辑指令("换行"、"段落")', 'normalizeTranscriptHint':'transcribe_audio:口语数字转阿拉伯数字,整理标点符号', 'messageTitle':'语音消息',
233
+ 'messageHint':'录制单条完整语音,转写完成后自动发送给智能体。', 'undoMs':'撤回窗口时长(毫秒)', 'undoHint':'转写完成后多久以内可以随时取消发送', 'customTitle':'自定义语音服务商',
234
+ 'customHint':'兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。', 'general':'通用设置', 'whisperEndpoint':'本地 whisper:端点地址',
235
+ 'whisperEndpointHint':'whisper.cpp 服务器的 POST /inference 接口', 'deepgramEndpoint':'Deepgram:基础地址',
236
+ 'deepgramEndpointHint':'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)', 'whisperBin':'本地 whisper:可执行文件',
237
+ 'whisperBinHint':'开启自动启动时使用', 'whisperModel':'本地 whisper:模型文件', 'whisperModelHint':'ggml 模型文件的绝对路径',
238
+ 'whisperAutostart':'自动启动本地 whisper', 'bilingualDeveloperPrompt':'双语开发者提示 (中/英)',
239
+ 'bilingualDeveloperPromptHint':'提示常用技术术语与命令行指令,避免语音模型音译', 'calibrateNoise':'校准背景噪声 (2秒)', 'calibrateNoiseHint':'测量 2 秒环境背景噪声并自动计算最佳门限',
240
+ 'calibrating':'正在测量噪声…', 'calibratedMsg':'门限已设置为: {db} dB (背景噪声: {noise} dB)', 'calibratedValue':'已校准', 'save':'保存', 'saved':'已保存 ✓',
241
+ 'openrouterWarning':'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板', 'noiseSuppression':'硬件级降噪',
242
+ 'noiseSuppressionHint':'启用浏览器硬件降噪、回声消除与自动增益控制', 'contextGlossary':'上下文专业词汇注入', 'contextGlossaryHint':'自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
243
+ 'providerDashboard':'服务商延迟与健康监控', 'avgLatency':'平均延迟', 'successRate':'成功率', 'fast':'极速', 'normal':'正常', 'slow':'较慢', 'error':'异常',
244
+ 'idle':'无调用记录', 'play':'播放', 'pause':'暂停', 'listenBack':'试听回放', 'lastRecording':'最近一条语音记录',
245
+ 'sensevoiceHint':'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)', 'sensevoiceEndpoint':'SenseVoice:端点地址',
246
+ 'sensevoiceEndpointHint':'sherpa-onnx 或兼容服务器的 POST 端点', 'sensevoiceBin':'SenseVoice:可执行文件', 'sensevoiceBinHint':'开启自动启动时使用',
247
+ 'sensevoiceModel':'SenseVoice:模型路径', 'sensevoiceModelHint':'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
248
+ 'sensevoiceAutostart':'自动启动 SenseVoice 服务', 'visualizerStyle':'音频动态波形样式', 'visualizerStyleHint':'录音胶囊面板内的动态音频波形动画效果',
249
+ 'visLiquidWave':'流体水波 (Liquid Wave)', 'visDynamicOrb':'动态脉冲球体 (Dynamic Orb)', 'visBars':'经典频条 (Classic Bars)', 'visOff':'关闭',
250
+ 'statusTitle':'连接与引擎状态看板', 'statusDesc':'当前语音后端与识别服务的实时网络与进程就绪状态。', 'badgeHostOnline':'服务端在线 ({ms} ms)', 'badgeHostOffline':'服务端不可达',
251
+ 'badgeWhisperActive':'Whisper 就绪', 'badgeWhisperInactive':'Whisper 未就绪', 'badgeSenseVoiceActive':'SenseVoice 就绪',
252
+ 'badgeSenseVoiceInactive':'SenseVoice 未就绪', 'badgeProviders':'{count} 个服务商已就绪', 'refreshStats':'刷新状态', 'hardwareTitle':'硬件与本地引擎',
253
+ 'hardwareDesc':'麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。', 'uiError':'语音界面错误:', 'retry':'重试', 'testMic':'测试麦克风', 'stopTest':'停止测试',
254
+ 'micLevel':'麦克风电平', 'noiseGate':'噪声门 (dB)', 'noiseGateHint':'静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭', 'noiseGateOff':'关闭',
255
+ 'versionTitle':'插件版本与更新', 'versionHint':'检查新版本并直接从 npm 更新。', 'updateAvailable':'有可用更新', 'upToDate':'已是最新版本',
256
+ 'updatePrompt':'npm 上有新版本可用。点击更新进行安装。', 'updateNow':'立即更新', 'updating':'正在更新…', 'updateSuccess':'更新成功!请重启 DSH 生效。',
257
+ 'updateFailed':'更新失败,请查看日志。', 'updateCheckFailed':'检查更新失败', 'profileLockedPid':'配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
258
+ 'profileLockedExisting':'配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。', 'profileLockCheckError':'检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
259
+ 'checkingUpdates':'正在检查更新…', 'updateStatusUnknown':'版本状态未知', 'sensevoiceTitle':'SenseVoice 本地 ASR (一键部署)',
260
+ 'sensevoiceDesc':'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。', 'installSensevoice':'一键安装 SenseVoice-Small',
261
+ 'installingSensevoice':'正在安装 SenseVoice…', 'sensevoiceReady':'已就绪', 'sensevoiceNotInstalled':'未安装', 'sensevoiceDownloading':'正在下载模型…',
262
+ 'turnTakingTitle':'轮流对话与音频协同', 'turnTakingDesc':'协调听与说,避免扬声器播放的声音被麦克风二次拾音。', 'gatedTurnTaking':'门控模式 (助手播报时麦克风自动静音)',
263
+ 'bargeInDesc':'实时打断 (说话时立即打断助手朗读)', 'smartActionsTitle':'免提与智能指令', 'autoSendVisualRing':'环形倒计时动画指示器',
264
+ 'voiceActionsLabel':'语音动作指令 ("发送", "取消", "清空", "换行")', 'liveInterimPreview':'实时语音文本预览 (幽灵文本)', 'structuredPromptVoice':'语音应答交互式选择提示',
265
+ 'modeVerbatim':'逐字直录', 'modeClean':'清爽文本', 'modeTask':'智能体任务', 'modeNote':'便签备忘', 'dictationModeTitle':'听写后处理模式',
266
+ 'dictationModeDesc':'选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。', 'dictationPromptsTitle':'听写提示词模板',
267
+ 'dictationPromptsDesc':'自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。', 'promptClean':'清爽文本提示词', 'promptTask':'智能体任务提示词', 'promptNote':'便签提示词',
268
+ 'resetPrompts':'恢复默认模板', 'voiceMacrosTitle':'语音动作宏', 'voiceMacrosDesc':'语音触发词自动展开为结构化提示词模板。', 'addMacro':'添加动作宏',
269
+ 'macroTrigger':'语音触发短语', 'macroTemplate':'展开提示词模板', 'macroDelete':'删除', 'rawTab':'原始识别', 'resultTab':'处理结果',
270
+ 'llmUnavailableFallback':'LLM不可用,已保留原始识别文本', 'jargonTitle':'开发者词汇与 IT 术语',
271
+ 'techJargonCorrection':'自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)', 'jargonAdd':'添加替换项', 'jargonFrom':'口语词汇', 'jargonTo':'标准拼写',
272
+ 'jargonPlaceholder':'k8s -> kubernetes\npr -> pull request', 'voiceInput':'语音输入', 'assistantSpeakingGated':'助手正在发言(门控模式)',
273
+ 'jargonFormatHint':' (口语 -> 替换文本)', 'errorPrefix':'错误', 'wizardTitle':'首次启动与麦克风诊断向导',
274
+ 'wizardDesc':'分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。', 'wizardBtnLaunch':'启动麦克风与 STT 诊断向导', 'wizardClose':'关闭向导',
275
+ 'wizardStep1':'1. 音频与运行环境', 'wizardStep2':'2. 实时信号测试', 'wizardStep3':'3. STT 识别回环测试', 'wizardSecureContextOk':'安全上下文 / HTTPS 已验证',
276
+ 'wizardSecureContextBad':'非安全上下文(需 HTTPS 或 localhost)', 'wizardMediaDevicesOk':'浏览器 mediaDevices API 支持正常',
277
+ 'wizardMediaDevicesBad':'浏览器缺少 mediaDevices API 支持', 'wizardMediaRecorderOk':'MediaRecorder API 支持正常',
278
+ 'wizardMediaRecorderBad':'当前浏览器不支持 MediaRecorder API', 'wizardMicPermissionOk':'麦克风权限已授予', 'wizardMicPermissionPrompt':'尚未获得麦克风授权',
279
+ 'wizardMicPermissionBad':'麦克风权限被拒绝', 'wizardRequestMic':'请求麦克风权限', 'wizardMicLabel':'麦克风输入设备:', 'wizardStartSignalCheck':'开始音频电平测试',
280
+ 'wizardStopSignalCheck':'停止音频测试', 'wizardSignalOk':'检测到音频信号 (RMS: {level}%)', 'wizardSignalZero':'未检测到声音信号 — 请对着麦克风说话或选择正确设备',
281
+ 'wizardSignalTesting':'正在监听声音信号… 请对着麦克风发声', 'wizardProviderLabel':'当前语音引擎与模型:', 'wizardProcessingArea':'处理区域:',
282
+ 'wizardAreaBrowser':'浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer':'本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud':'远程云端服务',
283
+ 'wizardStartTestRecord':'录制 3 秒测试语句', 'wizardRecordingTest':'正在录制测试音频… 请说话 ({left}秒)', 'wizardTranscribingTest':'正在请求语音识别…',
284
+ 'wizardTestResult':'STT 测试结果:', 'wizardTestSuccess':'STT 链路验证成功!耗时:{latency}毫秒', 'wizardTestEmpty':'录音中未检测到有效语音,请清晰发音后重试。',
285
+ 'wizardTestFailed':'STT 测试失败:{error}', 'wizardNextStepHint':'建议下一步:{hint}', 'wizardHintPermission':'点击“请求麦克风权限”并在浏览器弹窗中允许访问。',
286
+ 'wizardHintDevice':'检查麦克风硬件连接或切换选择其他输入设备。', 'wizardHintHttps':'请通过 HTTPS 或 localhost 访问 DeepSeek Harness。',
287
+ 'wizardHintProvider':'请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
288
+ 'wizardHintWinDesktop':'系统麦克风被阻止。请在 Windows【设置】→【隐私和安全性】→【麦克风】中开启最下方的【允许桌面应用访问你的麦克风】。',
289
+ 'wizardHintWinBrowser':'麦克风权限被拒绝。请在浏览器地址栏允许麦克风权限,或在 Windows【设置】→【隐私和安全性】→【麦克风】中开启访问。',
290
+ 'wizardHintMacDesktop':'系统麦克风被阻止。请在 macOS【系统设置】→【隐私与安全性】→【麦克风】中勾选允许 DSH Desktop。', 'wizardOpenWinMicSettings':'打开系统麦克风设置',
291
+ 'wizardFinishSuccess':'设置与验证已完成', 'wizardSkip':'跳过向导', 'wizardStatusCompleted':'麦克风与 STT 诊断已就绪', 'exportDiagnosticsBtn':'导出诊断报告',
292
+ 'exportingDiagnostics':'正在导出报告…', 'diagnosticsExportSuccess':'诊断报告已下载', 'recentRecognitionsTitle':'近期识别遥测详情',
293
+ 'noRecognitionsYet':'暂无近期识别记录。', 'diagLatencyBreakdown':'耗时分布:', 'diagVad':'VAD:{ms}毫秒', 'diagStt':'STT:{ms}毫秒',
294
+ 'diagPolish':'后处理:{ms}毫秒', 'diagTotal':'总耗时:{ms}毫秒', 'diagBytes':'音频大小:{bytes} 字节', 'diagWords':'词数:{count}',
295
+ 'diagExportNotice':'导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。', 'hostOnline':'后端在线', 'hostOffline':'后端不可用',
296
+ 'whisperReady':'Whisper 已就绪', 'whisperStopped':'Whisper 已停止', 'providersAvailable':'个服务商可用', 'macroEmpty':'未配置宏指令。',
297
+ 'macroTemplateArgHint':'(使用 {text} 作为参数)', 'noiseGateLow':'-50 dB (轻微静音)', 'noiseGateStandard':'-45 dB (标准默认)',
298
+ 'noiseGateMedium':'-35 dB (中等室内)', 'noiseGateHigh':'-25 dB (高降噪 / 机械键盘)', 'npuCpu':'CPU (标准)',
299
+ 'installModelSensevoice':'SenseVoice-Small (150MB, ~50ms)', 'installModelWhisperTurbo':'Whisper Large-v3-Turbo (800MB, 高精度)',
300
+ 'promptCleanPlaceholder':'默认:标点符号与语气词清洗', 'promptTaskPlaceholder':'默认:结构化问题/产出/约束/标准', 'promptNotePlaceholder':'默认:核心要点与待办事项总结',
301
+ 'settingsConflict':'设置冲突:检测到并行修改', 'saveFailedFallback':'保存失败', 'benchmarkFailed':'基准测试失败', 'networkError':'网络错误',
302
+ 'benchmarkOffline':'离线', 'benchmarkCores':'核心数', 'benchmarkCoresUnit':'核', 'benchRatingUltraFast':'极速', 'benchRatingRealtime':'实时',
303
+ 'msShort':'毫秒', 'transcriptionTimeout':'语音识别请求超时 (60s)', 'audioContextUnsupported':'不支持 AudioContext', 'audioReadFailed':'读取音频失败',
304
+ 'fileReaderUnsupported':'不支持 FileReader', 'benchmarkNoAsrServer':'本地 ASR 服务未启动。已检测到硬件加速支持。',
615
305
  }
616
306
 
617
- // Strings are also needed outside components — in recording handlers and
618
- // slot labels — so the translator is module-level, not only via props.
619
- let moduleT = (key) => en[key] || key
620
- const t = (key) => moduleT(key)
621
- // ------------------------------------------------------------------ css
307
+ let moduleT = (key) => en[key] || key
308
+ const t = (key) => moduleT(key)
622
309
  const CSS =
623
310
  '.dvo-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none;touch-action:manipulation}.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box;user-select:none;-webkit-user-select:none}.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}.dvo-pbtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);user-select:none;-webkit-user-select:none;touch-action:manipulation}.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvo-err{color:var(--dsw-alias-state-error-primary)}.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}.dvo-spin{animation:dvo-spin 1s linear infinite}@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.dvo-audio-wrap{display:flex;align-items:center;gap:8px;padding:2px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:14px;border:1px solid var(--dsw-alias-border-l1)}.dvo-audio-play{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border:0;cursor:pointer;padding:0}.dvo-audio-play:hover{opacity:0.9}.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}.dvo-dash{display:flex;flex-direction:column;gap:8px;margin-top:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}.dvo-dash-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}.dvo-badge-fast{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary)}.dvo-badge-norm{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-primary)}.dvo-badge-slow{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-border-l2)}.dvo-badge-err{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}.dvo-ghost{font-style:italic;color:var(--dsw-alias-label-tertiary);margin-left:6px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.dvo-installer-box{display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:8px}.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}.dvo-btn-send:hover{opacity:0.9}.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}.dvo-wave-clipping{color:var(--dsw-alias-state-warning-primary)!important;}.dvo-wave-low{color:var(--dsw-alias-label-secondary)!important;}.dvo-quality-badge{font-size:11px;padding:1px 6px;border-radius:4px;font-weight:500;display:inline-flex;align-items:center;gap:3px;}.dvo-quality-clipping{color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-surface-secondary);}.dvo-quality-low{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-surface-secondary);}.dvo-undo-badge{font-size:11px;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-surface-secondary);color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}'
624
311
  const cssId = 'dsh-voice/client.module.css'
@@ -628,7 +315,6 @@ window.__ModuleLoader__.load({
628
315
  tag.dataset.pluginCss = cssId
629
316
  document.head.appendChild(tag)
630
317
  }
631
- // ---------------------------------------------------------------- store
632
318
  const voice = {
633
319
  phase: 'idle', // idle | recording | processing | pending | error
634
320
  mode: 'dictation', // dictation | message
@@ -748,7 +434,6 @@ window.__ModuleLoader__.load({
748
434
  return voice
749
435
  }
750
436
 
751
- // ---------------------------------------------------------------- icons
752
437
  const ic = { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }
753
438
  const playIcon = () => React.createElement('svg', Object.assign({}, ic, { viewBox: '0 0 24 24' }),
754
439
  React.createElement('polygon', { points: '6 4 20 12 6 20 6 4', fill: 'currentColor', stroke: 'none' }))
@@ -810,13 +495,12 @@ window.__ModuleLoader__.load({
810
495
  } catch { /* best effort */ }
811
496
  }
812
497
 
813
- // ------------------------------------------------------------ transport
814
498
  function blobToBase64(blob) {
815
499
  return new Promise((resolve, reject) => {
816
- if (typeof FileReader === 'undefined') { reject(new Error('FileReader not supported')); return }
500
+ if (typeof FileReader === 'undefined') { reject(new Error(t('fileReaderUnsupported'))); return }
817
501
  const fr = new FileReader()
818
502
  fr.onload = () => { const s = String(fr.result || ''); resolve(s.indexOf(',') >= 0 ? s.slice(s.indexOf(',') + 1) : s) }
819
- fr.onerror = () => reject(new Error('failed to read audio'))
503
+ fr.onerror = () => reject(new Error(t('audioReadFailed')))
820
504
  fr.readAsDataURL(blob)
821
505
  })
822
506
  }
@@ -845,7 +529,6 @@ window.__ModuleLoader__.load({
845
529
  const text = chunks.join(' ')
846
530
  if (!text || text.length < 3) return []
847
531
  const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
848
- // Stop-word dictionary for vocabulary keyword extraction
849
532
  const stop = new Set([
850
533
  'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'any', 'can', 'her', 'was',
851
534
  'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now',
@@ -897,7 +580,7 @@ window.__ModuleLoader__.load({
897
580
  })
898
581
  } catch (e) {
899
582
  if (e && e.name === 'AbortError') {
900
- throw new Error('Transcription request timed out (60s)')
583
+ throw new Error(t('transcriptionTimeout'))
901
584
  }
902
585
  throw e
903
586
  } finally {
@@ -943,7 +626,6 @@ window.__ModuleLoader__.load({
943
626
  const headOut = await tryHost(head)
944
627
  if (headOut && (headOut.text || headOut.command || headOut.action)) return headOut
945
628
  } catch (headErr) {
946
- // Continue to WebGPU
947
629
  }
948
630
  }
949
631
 
@@ -975,7 +657,6 @@ window.__ModuleLoader__.load({
975
657
  return lead + s + trail
976
658
  }
977
659
 
978
- // Speech edit commands (#37): spoken line breaks and punctuation -> symbols
979
660
  const VOICE_COMMANDS = [
980
661
  [/(^|[\s,.!?])с новой строки([\s,.!?]|$)/gi, '$1\n$2'],
981
662
  [/(^|[\s,.!?])новая строка([\s,.!?]|$)/gi, '$1\n$2'],
@@ -996,7 +677,6 @@ window.__ModuleLoader__.load({
996
677
  return lead + trimmed + trail
997
678
  }
998
679
 
999
- // Insert history for undo (#29-9). Browser-only storage.
1000
680
  const insertHistory = []
1001
681
 
1002
682
  async function undoLastInsert() {
@@ -1007,7 +687,6 @@ window.__ModuleLoader__.load({
1007
687
  const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
1008
688
  if (draft === last.after) actions.setDraft(last.before)
1009
689
  else {
1010
- // Draft was edited manually — cut the last insert as a substring.
1011
690
  const i = draft.lastIndexOf(last.added)
1012
691
  if (i < 0) { insertHistory.push(last); return t('nothingToUndo') }
1013
692
  const spliced = draft.slice(0, i) + draft.slice(i + last.added.length)
@@ -1099,7 +778,6 @@ window.__ModuleLoader__.load({
1099
778
  if (voice.settings.beep) playBeep(phase === 'start' ? 880 : 660)
1100
779
  }
1101
780
 
1102
- // Short WebAudio beep so start/stop is audible without looking (#29-6).
1103
781
  function playBeep(freq) {
1104
782
  try {
1105
783
  const AC = typeof AudioContext !== 'undefined' ? AudioContext
@@ -1118,7 +796,6 @@ window.__ModuleLoader__.load({
1118
796
  } catch (noAudio) { /* no audio — not critical */ }
1119
797
  }
1120
798
 
1121
- // browser recognition: in-browser, no upload, only enabled when in chain
1122
799
  function speechRecognitionCtor() {
1123
800
  if (typeof window === 'undefined') return null
1124
801
  return window.SpeechRecognition || window.webkitSpeechRecognition || null
@@ -1169,7 +846,6 @@ window.__ModuleLoader__.load({
1169
846
  options.onInterim(interim)
1170
847
  }
1171
848
  recognition.onerror = (event) => {
1172
- // no-speech and aborted are normal life, not failures.
1173
849
  const code = event && event.error
1174
850
  if (code === 'no-speech' || code === 'aborted') return
1175
851
  stopped = true
@@ -1177,7 +853,6 @@ window.__ModuleLoader__.load({
1177
853
  if (resolveStop) { resolveStop(); resolveStop = null }
1178
854
  options.onError(code || t('recognitionError'))
1179
855
  }
1180
- // Restart browser recognition until explicitly stopped
1181
856
  recognition.onend = () => {
1182
857
  if (stopped) {
1183
858
  if (resolveStop) { resolveStop(); resolveStop = null }
@@ -1208,10 +883,8 @@ window.__ModuleLoader__.load({
1208
883
  }
1209
884
  }
1210
885
 
1211
- // Fetch the mode chain from the host once. Only needed to decide whether
1212
886
 
1213
887
  voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, undoLastChunk: undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
1214
- // Client WebGPU/WASM Whisper offline recognition (#168)
1215
888
 
1216
889
  function isWebGpuSupported() {
1217
890
  if (typeof navigator === 'undefined' || !navigator.gpu) return false
@@ -1245,7 +918,6 @@ function loadWebGpuWhisper(modelName, options) {
1245
918
  const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
1246
919
  if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
1247
920
 
1248
- // Prefer WebGPU if available and not forced to wasm; fall back to WASM on error or when WebGPU is absent
1249
921
  const canGpu = forceDevice === 'webgpu' || (!forceDevice && isWebGpuSupported())
1250
922
  if (canGpu) {
1251
923
  try {
@@ -1317,7 +989,6 @@ voice.webGpu = {
1317
989
  load: loadWebGpuWhisper,
1318
990
  clearCache: () => { _webGpuPipelines.clear() },
1319
991
  }
1320
- // Neural Silero VAD client module (#279)
1321
992
 
1322
993
  function isSileroVadSupported() {
1323
994
  return typeof Float32Array !== 'undefined'
@@ -1331,7 +1002,6 @@ async function initSileroVad() {
1331
1002
  return _sileroLoaded
1332
1003
  }
1333
1004
 
1334
- // 512 samples at 16kHz = 32ms
1335
1005
  function evaluateSileroFrame(pcmFrame) {
1336
1006
  if (!pcmFrame || pcmFrame.length === 0) return { isSpeech: false, probability: 0 }
1337
1007
  const len = pcmFrame.length
@@ -1347,8 +1017,6 @@ function evaluateSileroFrame(pcmFrame) {
1347
1017
  const rms = Math.sqrt(sumSq / len)
1348
1018
  const zcrRate = zcr / len
1349
1019
 
1350
- // Neural probability estimation: vocal cords exhibit harmonic energy with moderate ZCR,
1351
- // whereas mechanical switch clicks exhibit impulsive high ZCR (>0.45) with rapid decay.
1352
1020
  if (rms < 0.015) return { isSpeech: false, probability: 0.05 }
1353
1021
  if (zcrRate > 0.42 && rms < 0.08) return { isSpeech: false, probability: 0.12 } // click / keystroke
1354
1022
 
@@ -1363,7 +1031,6 @@ voice.sileroVad = {
1363
1031
  evaluate: evaluateSileroFrame,
1364
1032
  reset: () => { _sileroState = { h: null, c: null } },
1365
1033
  }
1366
- // Streaming WebSocket client (#282)
1367
1034
 
1368
1035
  function isWsStreamingSupported() {
1369
1036
  return typeof WebSocket !== 'undefined'
@@ -1515,7 +1182,6 @@ voice._chainsPromise = null
1515
1182
  }
1516
1183
  })
1517
1184
  }
1518
- // ------------------------------------------------------------ recording
1519
1185
  function teardown(rec) {
1520
1186
  if (!rec) return
1521
1187
  if (rec.wsStream) { try { rec.wsStream.close() } catch (_) {} rec.wsStream = null }
@@ -2020,7 +1686,6 @@ voice._chainsPromise = null
2020
1686
  if (actions && typeof actions.setDraft === 'function') actions.setDraft('')
2021
1687
  }
2022
1688
 
2023
- // Voice actions (#29-7): spoken triggers perform composer actions.
2024
1689
  function executeVoiceAction(action, rawText, target) {
2025
1690
  const activeTarget = target || voice._activeTarget
2026
1691
  const actions = (activeTarget && activeTarget.actions) || voice.inputActions
@@ -2071,7 +1736,6 @@ voice._chainsPromise = null
2071
1736
  function keepPending() {
2072
1737
  keepPendingPreview(voice)
2073
1738
  }
2074
- // ----------------------------------------------------------- components
2075
1739
  function VoiceButtons(props) {
2076
1740
  const v = useVoice()
2077
1741
  voice.inputActions = props.inputActions
@@ -2118,7 +1782,6 @@ voice._chainsPromise = null
2118
1782
  )
2119
1783
  }
2120
1784
 
2121
- // Human-readable key name.
2122
1785
  const KEY_LABELS = {
2123
1786
  Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
2124
1787
  Escape: 'Esc',
@@ -2131,11 +1794,9 @@ voice._chainsPromise = null
2131
1794
  }
2132
1795
  if (name === 'Space') return t('keySpace')
2133
1796
  if (KEY_LABELS[name]) return KEY_LABELS[name]
2134
- // Drop the Key/Digit prefix from codes like KeyR and Digit5.
2135
1797
  return String(name).replace(/^Key/, '').replace(/^Digit/, '')
2136
1798
  }
2137
1799
 
2138
- // Hotkey combo normalization
2139
1800
  function keyFromEvent(event) {
2140
1801
  if (!event) return ''
2141
1802
  const modifiers = []
@@ -2198,9 +1859,7 @@ voice._chainsPromise = null
2198
1859
  return event.code === name || event.key === name
2199
1860
  }
2200
1861
 
2201
- // Broadcast window event so active playback mutes when speaking.
2202
1862
 
2203
- // --------------------------------------------------------- hold gesture
2204
1863
  const HOLD_THRESHOLD_MS = 350
2205
1864
  const hold = { active: false, mode: null, startedAt: 0, armed: false }
2206
1865
  let activePointerCleanup = null
@@ -2315,7 +1974,6 @@ voice._chainsPromise = null
2315
1974
  }
2316
1975
  return cleanup
2317
1976
  }
2318
- // Editable preview state machine (#170)
2319
1977
 
2320
1978
  function initPendingPreview(text, target, options = {}) {
2321
1979
  const autoMs = Number(options.autoSendMs) || 4000
@@ -2353,7 +2011,6 @@ function cancelPendingPreview(voice) {
2353
2011
  const target = pending.target || voice._activeTarget
2354
2012
  const actions = (target && target.actions) || voice.inputActions
2355
2013
 
2356
- // Restore untouched composer draft if preview was active
2357
2014
  if (pending.rawText !== undefined && pending.initialDraft !== undefined) {
2358
2015
  if (actions && typeof actions.setDraft === 'function') {
2359
2016
  try { actions.setDraft(pending.initialDraft) } catch (_) {}
@@ -2373,7 +2030,6 @@ function keepPendingPreview(voice) {
2373
2030
  const actions = (target && target.actions) || voice.inputActions
2374
2031
 
2375
2032
  if (pending.rawText !== undefined) {
2376
- // Insert final edited text into composer draft without auto-sending
2377
2033
  const initial = pending.initialDraft || ''
2378
2034
  const textToInsert = pending.activeText || ''
2379
2035
  const full = initial
@@ -2393,7 +2049,6 @@ function submitPendingPreview(voice) {
2393
2049
  const target = pending.target || voice._activeTarget
2394
2050
  const actions = (target && target.actions) || voice.inputActions
2395
2051
 
2396
- // Session guard (#170): verify that current session matches initiating session
2397
2052
  const currentSessionId = (target && target.session && (target.session.sessionId || target.session.id))
2398
2053
  || (voice.session && (voice.session.sessionId || voice.session.id))
2399
2054
  || voice.currentSessionId || ''
@@ -2421,7 +2076,6 @@ function submitPendingPreview(voice) {
2421
2076
  try { actions.submit() } catch (_) {}
2422
2077
  }
2423
2078
  } else {
2424
- // Legacy path without rich preview
2425
2079
  if (voice.settings.polishSend) {
2426
2080
  fetch('/dsh-voice/polish', {
2427
2081
  method: 'POST',
@@ -2454,7 +2108,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2454
2108
  const hasPolish = p.polishedText && p.polishedText !== p.rawText
2455
2109
 
2456
2110
  return h('div', { className: 'dvo-pill-preview', role: 'region', 'aria-label': t('editablePreview') },
2457
- // Header row: tabs, audio playback, status/countdown, cancel button
2458
2111
  h('div', { className: 'dvo-preview-header' },
2459
2112
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
2460
2113
  hasPolish
@@ -2523,7 +2176,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2523
2176
  }, xIcon()),
2524
2177
  ),
2525
2178
  ),
2526
- // Body row: editable input or preview text, Edit button, Send button
2527
2179
  h('div', { className: 'dvo-preview-body' },
2528
2180
  p.isEditing
2529
2181
  ? h('input', {
@@ -2560,14 +2212,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2560
2212
  onClick: () => { submitPending(); focusComposer() },
2561
2213
  }, sendIcon()),
2562
2214
  ),
2563
- // Mismatch warning banner
2564
2215
  p.sessionMismatch
2565
2216
  ? h('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
2566
2217
  : null,
2567
2218
  )
2568
2219
  }
2569
- // ------------------------------------------------------------------ 45-dictation
2570
- // Dictation modes UI helper, mode cycler and labels (#264)
2571
2220
 
2572
2221
  const DICTATION_MODES = ['clean', 'task', 'note', 'verbatim']
2573
2222
 
@@ -2585,7 +2234,6 @@ function cycleDictationMode() {
2585
2234
  voice.selectedDictationMode = DICTATION_MODES[nextIdx]
2586
2235
  voice.notify()
2587
2236
  }
2588
- // ----------------------------------------------------------- visualizers
2589
2237
  let _accentColorCache = { color: '', expires: 0 }
2590
2238
  let _softColorCache = { color: '', expires: 0 }
2591
2239
 
@@ -2618,7 +2266,6 @@ function cycleDictationMode() {
2618
2266
  } catch (noTheme) { /* fallback */ }
2619
2267
  return fallback || voice.waveColor || 'currentColor'
2620
2268
  }
2621
- // 1. Liquid Wave: organic multi-layer wave
2622
2269
  function drawLiquidWave(g, w, h, levels, color, time) {
2623
2270
  const midY = h / 2
2624
2271
  const curLevel = levels.length ? levels[levels.length - 1] : 0
@@ -2656,7 +2303,6 @@ function cycleDictationMode() {
2656
2303
  g.globalAlpha = 1
2657
2304
  }
2658
2305
 
2659
- // 2. Dynamic Orb: interactive pulsing sphere in the center
2660
2306
  function drawDynamicOrb(g, w, h, levels, color, time) {
2661
2307
  const cx = w / 2
2662
2308
  const cy = h / 2
@@ -2704,7 +2350,6 @@ function cycleDictationMode() {
2704
2350
  g.globalAlpha = 1
2705
2351
  }
2706
2352
 
2707
- // 3. Classic Bars: classic vertical bars
2708
2353
  function drawClassicBars(g, w, h, levels, color) {
2709
2354
  const midY = h / 2
2710
2355
  for (let i = 0; i < levels.length && i * 7 < w; i++) {
@@ -2752,7 +2397,6 @@ function cycleDictationMode() {
2752
2397
  return () => window.removeEventListener('keydown', onKey, true)
2753
2398
  }, [v.phase])
2754
2399
 
2755
- // VAD: accumulate silence and cut the phrase when the pause exceeds the threshold.
2756
2400
  React.useEffect(() => {
2757
2401
  if (v.phase !== 'recording') return
2758
2402
  const tick = 50
@@ -2769,13 +2413,11 @@ function cycleDictationMode() {
2769
2413
  const adapt = Number(voice.settings.vadAdapt) || 0
2770
2414
  let effectiveVad = Number(voice.settings.vadSilenceMs) || 700
2771
2415
  if (adapt > 0 && rec.hadSpeech) {
2772
- // Adaptive threshold (#41)
2773
2416
  const win = voice.levels.slice(-20)
2774
2417
  const density = win.length ? win.filter((v) => v > 0.06).length / win.length : 0
2775
2418
  const k = adapt * (density - 0.5) * 2
2776
2419
  effectiveVad = Math.max(150, Math.round(Number(voice.settings.vadSilenceMs) * (1 - k)))
2777
2420
  }
2778
- // Continuous mode (#40)
2779
2421
  const stream = !!voice.settings.stream && rec.mode === 'dictation'
2780
2422
  if (stream && rec.hadSpeech && !rec.cutting) {
2781
2423
  rec.streamMs += tick
@@ -2792,7 +2434,6 @@ function cycleDictationMode() {
2792
2434
  return () => dispose()
2793
2435
  }, [v.phase])
2794
2436
 
2795
- // Waveform / visualizer animation: requestAnimationFrame with HiDPI scaling
2796
2437
  React.useEffect(() => {
2797
2438
  if (v.phase !== 'recording') return
2798
2439
  let frame = 0
@@ -2859,7 +2500,6 @@ function cycleDictationMode() {
2859
2500
  }
2860
2501
  }, [v.phase])
2861
2502
 
2862
- // Message-mode cancel window.
2863
2503
  React.useEffect(() => {
2864
2504
  if (v.phase !== 'pending') return
2865
2505
  const tick = 100
@@ -2867,14 +2507,12 @@ function cycleDictationMode() {
2867
2507
  const p = voice.pending
2868
2508
  if (!p) return
2869
2509
  if (p.undoOnly) {
2870
- // Undo-only mode (#29-5)
2871
2510
  p.leftMs -= tick
2872
2511
  if (p.leftMs <= 0) voice.set({ phase: 'idle' })
2873
2512
  else voice.notify()
2874
2513
  return
2875
2514
  }
2876
2515
  if (p.paused || p.isEditing) {
2877
- // Confirmation or editing mode freezes auto-send countdown (#170)
2878
2516
  return
2879
2517
  }
2880
2518
  p.leftMs -= tick
@@ -2925,7 +2563,6 @@ function cycleDictationMode() {
2925
2563
  const inBrowser = !!voice.browser
2926
2564
  const rec = voice.rec
2927
2565
  const hint = v.mode === 'dictation' ? t('dictationPill') : t('messagePill')
2928
- // Live caption: interim preview
2929
2566
  const caption = voice.caption || (inBrowser ? '' : null)
2930
2567
  const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
2931
2568
  || voice.selectedDictationMode
@@ -3007,7 +2644,6 @@ function cycleDictationMode() {
3007
2644
  }
3008
2645
  const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
3009
2646
  if (voice.pending && voice.pending.undoOnly) {
3010
- // Cancel window for delayed dictation insert (#29-5).
3011
2647
  return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
3012
2648
  React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, t('undo'), ': ', left, t('secondsShort')),
3013
2649
  React.createElement('button', {
@@ -3087,9 +2723,7 @@ function cycleDictationMode() {
3087
2723
  )
3088
2724
  }
3089
2725
 
3090
- // --------------------------------------------------------------- slots
3091
2726
  function registerComposer(ctx) {
3092
- // Hotkey listener with dynamic reload
3093
2727
  ctx.effect(() => {
3094
2728
  let dispose = () => {}
3095
2729
  let alive = true
@@ -3104,7 +2738,6 @@ function cycleDictationMode() {
3104
2738
  const key = data && data.hotkey
3105
2739
  voice.hotkey = key || ''
3106
2740
  if (key) dispose = installHotkey(ctx, key, 'message')
3107
- // The composer needs fresh settings without opening the card.
3108
2741
  Object.assign(voice.settings, {
3109
2742
  beep: !!(data && data.beep),
3110
2743
  localOnly: !!(data && data.localOnly),
@@ -3152,7 +2785,6 @@ function cycleDictationMode() {
3152
2785
  }
3153
2786
  }, 'dsh-voice: hold hotkey')
3154
2787
 
3155
- // locale: NS required for core slot snapshot injection
3156
2788
  ctx.effect(() => {
3157
2789
  const offRight = ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
3158
2790
  { name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, locale: NS, label: () => t('title') },
@@ -3169,14 +2801,11 @@ function cycleDictationMode() {
3169
2801
  }, 'dsh-voice: composer slots')
3170
2802
  }
3171
2803
 
3172
- // ------------------------------------------------------- settings page
3173
2804
  const BUILTIN = [
3174
2805
  'browser', 'browser-webgpu', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
3175
- // Presets: address and model are filled on the host; only a key is needed.
3176
2806
  'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
3177
2807
  ]
3178
2808
  const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
3179
- // Resolve at render time: t() must not be captured before locale bind.
3180
2809
  const MODEL_HINT_KEYS = {
3181
2810
  browser: 'browserHint',
3182
2811
  'browser-webgpu': 'browserWebGpuHint',
@@ -3268,7 +2897,6 @@ function cycleDictationMode() {
3268
2897
  }
3269
2898
  const ErrorBoundary = createErrorBoundary()
3270
2899
 
3271
- // One chain editor: provider+model rows with reordering.
3272
2900
  function ChainEditor(props) {
3273
2901
  const t = (props && props.t) || moduleT
3274
2902
  const rows = Array.isArray(props.value) ? props.value : []
@@ -3309,7 +2937,6 @@ function cycleDictationMode() {
3309
2937
  )
3310
2938
  }
3311
2939
 
3312
- // Custom providers: name, API template, where to call, how to authorize.
3313
2940
  function CustomEditor(props) {
3314
2941
  const t = (props && props.t) || moduleT
3315
2942
  const rows = Array.isArray(props.value) ? props.value : []
@@ -3358,7 +2985,6 @@ function cycleDictationMode() {
3358
2985
  ),
3359
2986
  )
3360
2987
  }
3361
- // Provider Telemetry & Latency Dashboard (#266)
3362
2988
  function ProviderDashboard(props) {
3363
2989
  const t = props.t || moduleT
3364
2990
  const statsData = props.statsData || {}
@@ -3428,13 +3054,13 @@ function cycleDictationMode() {
3428
3054
  badgeText = t('error')
3429
3055
  } else if (avg > 0 && avg < 400) {
3430
3056
  badgeClass = 'dvo-badge-fast'
3431
- badgeText = avg + 'ms · ' + t('fast')
3057
+ badgeText = avg + ' ' + t('msShort') + ' · ' + t('fast')
3432
3058
  } else if (avg >= 400 && avg <= 1500) {
3433
3059
  badgeClass = 'dvo-badge-norm'
3434
- badgeText = avg + 'ms · ' + t('normal')
3060
+ badgeText = avg + ' ' + t('msShort') + ' · ' + t('normal')
3435
3061
  } else {
3436
3062
  badgeClass = 'dvo-badge-slow'
3437
- badgeText = avg + 'ms · ' + t('slow')
3063
+ badgeText = avg + ' ' + t('msShort') + ' · ' + t('slow')
3438
3064
  }
3439
3065
  }
3440
3066
  const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
@@ -3452,7 +3078,6 @@ function cycleDictationMode() {
3452
3078
  }),
3453
3079
  ),
3454
3080
  ),
3455
- // Per-recognition telemetry breakdown list (#266)
3456
3081
  h('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '10px' } },
3457
3082
  h('div', { className: 'dvs-h', style: { marginBottom: '8px' } }, t('recentRecognitionsTitle')),
3458
3083
  recentDiagnostics.length === 0
@@ -3489,8 +3114,6 @@ function cycleDictationMode() {
3489
3114
  ),
3490
3115
  )
3491
3116
  }
3492
- // ------------------------------------------------------------------ 72-dictation-modes
3493
- // Dictation modes configuration card and prompt template editors (#264)
3494
3117
 
3495
3118
  function DictationModesSection(props) {
3496
3119
  const { t, draft, setTop, writable } = props
@@ -3545,7 +3168,7 @@ function DictationModesSection(props) {
3545
3168
  className: 'cb-input',
3546
3169
  style: { minHeight: '48px', resize: 'vertical' },
3547
3170
  value: prompts.clean || '',
3548
- placeholder: 'Default: punctuation & filler-word cleanup',
3171
+ placeholder: t('promptCleanPlaceholder'),
3549
3172
  disabled: !writable,
3550
3173
  onChange: (e) => setPrompt('clean', e.target.value),
3551
3174
  }),
@@ -3556,7 +3179,7 @@ function DictationModesSection(props) {
3556
3179
  className: 'cb-input',
3557
3180
  style: { minHeight: '48px', resize: 'vertical' },
3558
3181
  value: prompts.task || '',
3559
- placeholder: 'Default: structured problem/outcome/constraints/criteria',
3182
+ placeholder: t('promptTaskPlaceholder'),
3560
3183
  disabled: !writable,
3561
3184
  onChange: (e) => setPrompt('task', e.target.value),
3562
3185
  }),
@@ -3567,7 +3190,7 @@ function DictationModesSection(props) {
3567
3190
  className: 'cb-input',
3568
3191
  style: { minHeight: '48px', resize: 'vertical' },
3569
3192
  value: prompts.note || '',
3570
- placeholder: 'Default: key points & action items summary',
3193
+ placeholder: t('promptNotePlaceholder'),
3571
3194
  disabled: !writable,
3572
3195
  onChange: (e) => setPrompt('note', e.target.value),
3573
3196
  }),
@@ -3576,7 +3199,6 @@ function DictationModesSection(props) {
3576
3199
  ),
3577
3200
  )
3578
3201
  }
3579
- // 5. Hardware & Audio Engines Card
3580
3202
  function HardwareOptionsCard(props) {
3581
3203
  const {
3582
3204
  t = moduleT, draft, setTop, writable, catching, setCatching,
@@ -3602,14 +3224,27 @@ function DictationModesSection(props) {
3602
3224
  .then((d) => {
3603
3225
  setBenchmarking(false)
3604
3226
  if (d && d.ok) setBenchmarkResult(d)
3605
- else setBenchmarkErr((d && d.error && d.error.message) || 'Benchmark failed')
3227
+ else setBenchmarkErr((d && d.error && d.error.message) || t('benchmarkFailed'))
3606
3228
  })
3607
3229
  .catch((e) => {
3608
3230
  setBenchmarking(false)
3609
- setBenchmarkErr((e && e.message) || 'Network error')
3231
+ setBenchmarkErr((e && e.message) || t('networkError'))
3610
3232
  })
3611
3233
  }
3612
3234
 
3235
+ const ratingLabel = (rating) => {
3236
+ const key = { 'Ultra-fast': 'benchRatingUltraFast', 'Fast': 'fast', 'Real-time': 'benchRatingRealtime', 'Slow': 'slow' }[rating]
3237
+ return key ? t(key) : String(rating || '')
3238
+ }
3239
+ const HOST_BENCH_MESSAGES = {
3240
+ 'Local ASR server is not running. Hardware detected.': 'benchmarkNoAsrServer',
3241
+ }
3242
+ const benchmarkMessage = (result) => {
3243
+ const raw = String(result && result.message || '')
3244
+ const key = HOST_BENCH_MESSAGES[raw]
3245
+ return (key && t(key)) || raw || t('benchmarkOffline')
3246
+ }
3247
+
3613
3248
  const textField = (key, label, hint) => h('div', { className: 'cb-field' },
3614
3249
  h('label', null, label),
3615
3250
  h('input', {
@@ -3624,7 +3259,6 @@ function DictationModesSection(props) {
3624
3259
  h('span', null, '⚙️ ' + t('hardwareTitle')),
3625
3260
  ),
3626
3261
  h('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
3627
- // Hotkey picker
3628
3262
  h('div', { className: 'cb-field' },
3629
3263
  h('label', null, t('hotkey')),
3630
3264
  h('div', { className: 'cb-row' },
@@ -3639,7 +3273,6 @@ function DictationModesSection(props) {
3639
3273
  ),
3640
3274
  h('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
3641
3275
  ),
3642
- // Mic & noise gate
3643
3276
  h('div', { className: 'cb-field' },
3644
3277
  h('label', null, t('mic')),
3645
3278
  h('div', { className: 'cb-row' },
@@ -3686,10 +3319,10 @@ function DictationModesSection(props) {
3686
3319
  style: { flex: 1 },
3687
3320
  },
3688
3321
  h('option', { value: '-999' }, t('noiseGateOff')),
3689
- h('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
3690
- h('option', { value: '-45' }, '-45 dB (Standard Default)'),
3691
- h('option', { value: '-35' }, '-35 dB (Medium Room)'),
3692
- h('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
3322
+ h('option', { value: '-50' }, t('noiseGateLow')),
3323
+ h('option', { value: '-45' }, t('noiseGateStandard')),
3324
+ h('option', { value: '-35' }, t('noiseGateMedium')),
3325
+ h('option', { value: '-25' }, t('noiseGateHigh')),
3693
3326
  !['-999', '-50', '-45', '-35', '-25'].includes(String(gateDbVal)) ? h('option', { value: String(gateDbVal) }, `${gateDbVal} dB (${t('calibratedValue') || 'Calibrated'})`) : null,
3694
3327
  ),
3695
3328
  ),
@@ -3716,7 +3349,6 @@ function DictationModesSection(props) {
3716
3349
  ),
3717
3350
  h('span', { className: 'dvs-sub' }, t('streamingWsHint')),
3718
3351
  ),
3719
- // Endpoint text fields
3720
3352
  h('div', { className: 'cb-grid-2' },
3721
3353
  textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
3722
3354
  textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
@@ -3729,7 +3361,6 @@ function DictationModesSection(props) {
3729
3361
  textField('polishModel', t('polishModel'), ''),
3730
3362
  textField('polishKeyEnv', t('polishKeyEnv'), ''),
3731
3363
  ),
3732
- // Engine checkboxes
3733
3364
  h('div', { className: 'cb-row' },
3734
3365
  h('label', { className: 'cb-check-label' },
3735
3366
  h('input', {
@@ -3795,7 +3426,6 @@ function DictationModesSection(props) {
3795
3426
  t('normalizeTranscript'),
3796
3427
  ),
3797
3428
  ),
3798
- // Visualizer select
3799
3429
  h('div', { className: 'cb-field' },
3800
3430
  h('label', null, t('visualizerStyle')),
3801
3431
  h('select', {
@@ -3808,7 +3438,6 @@ function DictationModesSection(props) {
3808
3438
  h('option', { value: 'off' }, t('visOff')),
3809
3439
  ),
3810
3440
  ),
3811
- // Hardware Acceleration (SBC NPU / Host)
3812
3441
  h('div', { className: 'cb-field' },
3813
3442
  h('label', null, t('npuProvider')),
3814
3443
  h('div', { className: 'cb-row' },
@@ -3818,7 +3447,7 @@ function DictationModesSection(props) {
3818
3447
  onChange: (e) => setTop('sensevoiceProvider', e.target.value),
3819
3448
  style: { flex: 1 },
3820
3449
  },
3821
- h('option', { value: 'cpu' }, 'CPU (Standard)'),
3450
+ h('option', { value: 'cpu' }, t('npuCpu')),
3822
3451
  h('option', { value: 'rknn' }, 'Rockchip RK3588 NPU (RKNN)'),
3823
3452
  h('option', { value: 'coreml' }, 'Apple Silicon CoreML'),
3824
3453
  h('option', { value: 'openvino' }, 'Intel OpenVINO'),
@@ -3837,7 +3466,6 @@ function DictationModesSection(props) {
3837
3466
  ),
3838
3467
  h('span', { className: 'dvs-sub' }, t('npuProviderHint')),
3839
3468
  ),
3840
- // Hardware Benchmark
3841
3469
  h('div', { className: 'cb-field' },
3842
3470
  h('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between' } },
3843
3471
  h('label', { style: { marginBottom: 0 } }, t('benchmarkTitle')),
@@ -3850,12 +3478,11 @@ function DictationModesSection(props) {
3850
3478
  ? h('div', {
3851
3479
  style: { marginTop: '8px', padding: '6px 10px', borderRadius: '6px', fontSize: '12px', backgroundColor: 'var(--dsw-alias-fill-quaternary)' }
3852
3480
  }, benchmarkResult.running
3853
- ? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs}ms (${benchmarkResult.rating}) · ${String(benchmarkResult.provider || 'cpu').toUpperCase()} · Cores: ${benchmarkResult.cpuCores}`
3854
- : `ℹ ${benchmarkResult.message || 'Offline'} (${benchmarkResult.cpuCores} cores, ${benchmarkResult.platform})`)
3481
+ ? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs} ${t('msShort')} (${ratingLabel(benchmarkResult.rating)}) · ${String(benchmarkResult.provider || 'cpu').toUpperCase()} · ${t('benchmarkCores')}: ${benchmarkResult.cpuCores}`
3482
+ : `ℹ ${benchmarkMessage(benchmarkResult)} (${benchmarkResult.cpuCores} ${t('benchmarkCoresUnit')}, ${benchmarkResult.platform})`)
3855
3483
  : (benchmarkErr ? h('div', { style: { color: 'var(--dsw-alias-state-danger)', fontSize: '12px', marginTop: '4px' } }, benchmarkErr) : null),
3856
3484
  h('span', { className: 'dvs-sub' }, t('benchmarkHint')),
3857
3485
  ),
3858
- // WebGPU Status Indicator
3859
3486
  h('div', { className: 'cb-field' },
3860
3487
  h('label', null, t('webgpuStatus')),
3861
3488
  h('div', {
@@ -3872,7 +3499,6 @@ function DictationModesSection(props) {
3872
3499
  ? ('✓ ' + t('webgpuAvailable'))
3873
3500
  : ('ℹ ' + t('webgpuNotAvailable'))),
3874
3501
  ),
3875
- // Vocabulary textarea
3876
3502
  h('div', { className: 'cb-field' },
3877
3503
  h('label', null, t('vocabulary')),
3878
3504
  h('textarea', {
@@ -3883,7 +3509,6 @@ function DictationModesSection(props) {
3883
3509
  ),
3884
3510
  )
3885
3511
  }
3886
- // Custom hooks for VoiceSection modularity
3887
3512
  function useSensevoiceInstaller(fetchStatus) {
3888
3513
  const [sensevoiceState, setSensevoiceState] = React.useState(null)
3889
3514
  const [installingSensevoice, setInstallingSensevoice] = React.useState(false)
@@ -3953,7 +3578,7 @@ function DictationModesSection(props) {
3953
3578
  if (devId) audio.deviceId = { exact: devId }
3954
3579
  stream = await navigator.mediaDevices.getUserMedia({ audio })
3955
3580
  const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
3956
- if (!AC) throw new Error('AudioContext not supported')
3581
+ if (!AC) throw new Error(t('audioContextUnsupported'))
3957
3582
  audioCtx = new AC()
3958
3583
  if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
3959
3584
  const src = audioCtx.createMediaStreamSource(stream)
@@ -4150,7 +3775,6 @@ function DictationModesSection(props) {
4150
3775
 
4151
3776
  return { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow }
4152
3777
  }
4153
- // Developer Lexicon & IT Jargon
4154
3778
  function JargonEditor(props) {
4155
3779
  const t = props.t || moduleT
4156
3780
  const draft = props.draft
@@ -4204,8 +3828,6 @@ function DictationModesSection(props) {
4204
3828
  ),
4205
3829
  )
4206
3830
  }
4207
- // ------------------------------------------------------------------ 72-macros
4208
- // Voice action macros configuration card (#167)
4209
3831
 
4210
3832
  function MacrosSection(props) {
4211
3833
  const { t, draft, setTop, writable } = props
@@ -4240,7 +3862,7 @@ function MacrosSection(props) {
4240
3862
  h('div', { className: 'cb-field-desc' }, t('voiceMacrosDesc')),
4241
3863
  macros.length === 0
4242
3864
  ? h('div', { style: { fontStyle: 'italic', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', marginTop: '6px' } },
4243
- 'No macros configured.',
3865
+ t('macroEmpty'),
4244
3866
  )
4245
3867
  : h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px' } },
4246
3868
  macros.map((m, idx) =>
@@ -4266,7 +3888,7 @@ function MacrosSection(props) {
4266
3888
  h('textarea', {
4267
3889
  className: 'cb-input',
4268
3890
  style: { minHeight: '38px', resize: 'vertical', fontSize: '12px' },
4269
- placeholder: t('macroTemplate') + ' (use {text} for argument)',
3891
+ placeholder: t('macroTemplate') + t('macroTemplateArgHint'),
4270
3892
  value: m.template || '',
4271
3893
  disabled: !writable,
4272
3894
  onChange: (e) => updateMacro(idx, 'template', e.target.value),
@@ -4276,7 +3898,6 @@ function MacrosSection(props) {
4276
3898
  ),
4277
3899
  )
4278
3900
  }
4279
- // Local ASR (SenseVoice-Small & Whisper Large-v3-Turbo 1-Click Setup)
4280
3901
  function SensevoiceSection(props) {
4281
3902
  const t = props.t || moduleT
4282
3903
  const sensevoiceState = props.sensevoiceState
@@ -4305,8 +3926,8 @@ function MacrosSection(props) {
4305
3926
  onChange: (e) => setModelChoice(e.target.value),
4306
3927
  style: { flex: 1, fontSize: '12px', minWidth: '180px' },
4307
3928
  },
4308
- h('option', { value: 'sensevoice' }, 'SenseVoice-Small (150MB, ~50ms)'),
4309
- h('option', { value: 'whisper-turbo' }, 'Whisper Large-v3-Turbo (800MB, High-Accuracy)'),
3929
+ h('option', { value: 'sensevoice' }, t('installModelSensevoice')),
3930
+ h('option', { value: 'whisper-turbo' }, t('installModelWhisperTurbo')),
4310
3931
  ),
4311
3932
  h('button', {
4312
3933
  type: 'button',
@@ -4334,7 +3955,6 @@ function MacrosSection(props) {
4334
3955
  )
4335
3956
  )
4336
3957
  }
4337
- // 1. Connection & Engine Status Card
4338
3958
  function ConnectionStatusCard(props) {
4339
3959
  const t = props.t || moduleT
4340
3960
  const hostOnline = props.hostOnline
@@ -4360,7 +3980,7 @@ function MacrosSection(props) {
4360
3980
  ),
4361
3981
  h('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
4362
3982
  h('span', { className: 'cb-badge ' + (hostOnline ? 'cb-badge-ok' : 'cb-badge-err') },
4363
- hostOnline ? (`✓ ${t('hostOnline')} (${statusLatency}ms)`) : (`✗ ${t('hostOffline')}`)
3983
+ hostOnline ? (`✓ ${t('hostOnline')} (${statusLatency} ${t('msShort')})`) : (`✗ ${t('hostOffline')}`)
4364
3984
  ),
4365
3985
  h('span', { className: 'cb-badge ' + (whisperRunning ? 'cb-badge-ok' : 'cb-badge-warn') },
4366
3986
  whisperRunning ? (`✓ ${t('whisperReady')}`) : (`· ${t('whisperStopped')}`)
@@ -4369,7 +3989,6 @@ function MacrosSection(props) {
4369
3989
  ),
4370
3990
  )
4371
3991
  }
4372
- // Plugin Version & Updates Card
4373
3992
  function UpdaterCard(props) {
4374
3993
  const t = props.t || moduleT
4375
3994
  const updaterStatus = props.updaterStatus
@@ -4428,11 +4047,9 @@ function MacrosSection(props) {
4428
4047
  : null,
4429
4048
  )
4430
4049
  }
4431
- // ------------------------------------------------------- settings voice section
4432
4050
  function VoiceSection(props) {
4433
4051
  const t = (props && props.t) || moduleT
4434
4052
  const ctx = (props && props.ctx) || {}
4435
- // Settings adapter: prefer dsh-lanmode when remote, otherwise kernel.
4436
4053
  const settingsService = (ctx.get && ctx.get('lanSettings')) || ctx.configForms || ctx.settingsScope
4437
4054
  const scope = settingsService && typeof settingsService.get === 'function'
4438
4055
  ? settingsService.get(NS)
@@ -4462,7 +4079,6 @@ function MacrosSection(props) {
4462
4079
  })
4463
4080
  return () => { alive = false }
4464
4081
  }, [])
4465
- // Hotkey picker: not a name field — press the key you want.
4466
4082
  const [catching, setCatching] = React.useState(false)
4467
4083
  const [jargonInput, setJargonInput] = React.useState(null)
4468
4084
  const [showWizard, setShowWizard] = React.useState(false)
@@ -4644,9 +4260,9 @@ function MacrosSection(props) {
4644
4260
  if (data.config) setServerConfig(data.config)
4645
4261
  if (typeof data.revision === 'number') setRevision(data.revision)
4646
4262
  } else if (res.status === 409) {
4647
- httpError = data?.error?.message || 'Settings conflict: parallel changes detected'
4263
+ httpError = data?.error?.message || t('settingsConflict')
4648
4264
  } else {
4649
- httpError = data?.error?.message || data?.error || res.statusText || 'Save failed'
4265
+ httpError = data?.error?.message || data?.error || res.statusText || t('saveFailedFallback')
4650
4266
  }
4651
4267
  } catch (e) {
4652
4268
  httpError = e?.message || String(e)
@@ -4938,11 +4554,9 @@ function MacrosSection(props) {
4938
4554
  ),
4939
4555
  ),
4940
4556
  ),
4941
- // Dictation Modes & Prompts (#264)
4942
4557
  h(DictationModesSection, {
4943
4558
  t, draft, setTop, writable,
4944
4559
  }),
4945
- // Voice Action Macros (#167)
4946
4560
  h(MacrosSection, {
4947
4561
  t, draft, setTop, writable,
4948
4562
  }),
@@ -4974,7 +4588,6 @@ function MacrosSection(props) {
4974
4588
  ),
4975
4589
  )
4976
4590
  }
4977
- // ------------------------------------------------------- first-run wizard (#265)
4978
4591
  function FirstRunWizard(props) {
4979
4592
  const t = props.t || moduleT
4980
4593
  const draft = props.draft
@@ -5212,7 +4825,6 @@ function MacrosSection(props) {
5212
4825
  }
5213
4826
  }
5214
4827
 
5215
- // Determine active chain info
5216
4828
  const dictationChain = (draft && draft.dictation && draft.dictation.chain) || []
5217
4829
  const activeProvider = (dictationChain[0] && (dictationChain[0].provider || dictationChain[0])) || 'sensevoice'
5218
4830
  let areaLabel = t('wizardAreaServer')
@@ -5254,7 +4866,6 @@ function MacrosSection(props) {
5254
4866
  ),
5255
4867
  h('div', { className: 'cb-section-desc' }, t('wizardDesc')),
5256
4868
 
5257
- // Step 1: Environment & Permissions
5258
4869
  h('div', { className: 'dvs-card' },
5259
4870
  h('div', { className: 'dvs-h' }, t('wizardStep1')),
5260
4871
  h('div', { className: 'cb-row' },
@@ -5283,8 +4894,8 @@ function MacrosSection(props) {
5283
4894
  stopSignalTest()
5284
4895
  },
5285
4896
  },
5286
- h('option', { value: '' }, 'Default System Microphone'),
5287
- devices.map((d) => h('option', { key: d.deviceId, value: d.deviceId }, d.label || `Microphone ${d.deviceId.slice(0, 8)}…`))
4897
+ h('option', { value: '' }, t('micDefault')),
4898
+ devices.map((d) => h('option', { key: d.deviceId, value: d.deviceId }, d.label || (t('mic') + ' ' + d.deviceId.slice(0, 8) + '…')))
5288
4899
  ),
5289
4900
  h('button', {
5290
4901
  type: 'button',
@@ -5302,7 +4913,6 @@ function MacrosSection(props) {
5302
4913
  ),
5303
4914
  ),
5304
4915
 
5305
- // Step 2: Live Audio Signal Test
5306
4916
  h('div', { className: 'dvs-card' },
5307
4917
  h('div', { className: 'dvs-h' }, t('wizardStep2')),
5308
4918
  h('div', { className: 'cb-row', style: { alignItems: 'center' } },
@@ -5341,7 +4951,6 @@ function MacrosSection(props) {
5341
4951
  ) : null,
5342
4952
  ),
5343
4953
 
5344
- // Step 3: Transcription Round-Trip Test
5345
4954
  h('div', { className: 'dvs-card' },
5346
4955
  h('div', { className: 'dvs-h' }, t('wizardStep3')),
5347
4956
  h('div', { className: 'cb-row' },
@@ -5380,7 +4989,6 @@ function MacrosSection(props) {
5380
4989
  ) : null,
5381
4990
  ),
5382
4991
 
5383
- // Next Step Hint / Feedback
5384
4992
  nextStepHint ? h('div', { className: 'dvs-wait', style: { fontStyle: 'italic', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
5385
4993
  h('span', null, t('wizardNextStepHint').replace('{hint}', nextStepHint)),
5386
4994
  (isWindows && isPermDenied) ? h('a', {
@@ -5392,7 +5000,6 @@ function MacrosSection(props) {
5392
5000
  }, '⚙ ' + t('wizardOpenWinMicSettings')) : null,
5393
5001
  ) : null,
5394
5002
 
5395
- // Wizard Bottom Actions
5396
5003
  h('div', { className: 'cb-row', style: { justifyContent: 'space-between', marginTop: '6px' } },
5397
5004
  h('button', {
5398
5005
  type: 'button',
@@ -5413,14 +5020,11 @@ function MacrosSection(props) {
5413
5020
  ),
5414
5021
  )
5415
5022
  }
5416
- // Card in Settings → Plugins → Plugin settings (#18)
5417
5023
  function PluginCard(props) {
5418
5024
  const page = !!(props && props.view === 'page')
5419
5025
  const [open, setOpen] = React.useState(!!page)
5420
5026
  const tt = (props && props.t) || t
5421
5027
  const title = tt('title')
5422
- // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
5423
- // padding, so the summary is a one-liner and the page drops our card chrome.
5424
5028
  if (props && props.view === 'summary') {
5425
5029
  return h('span', { className: 'dvo-pdesc' }, tt('cardHint'))
5426
5030
  }