@goodandready/dsh-voice 0.9.36 → 0.9.37

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
@@ -35,580 +35,579 @@ window.__ModuleLoader__.load({
35
35
  // translate them without touching this plugin. English is the source
36
36
  // language, the default, and the fallback.
37
37
  const en = {
38
- 'saveFailed': 'Some fields were not saved —',
39
- 'cardHint': 'Dictation and voice messages: providers, chains, local whisper',
40
- 'expand': 'Expand',
41
- 'collapse': 'Collapse',
42
- 'composerUnavailable': 'Composer unavailable',
43
- 'keySpace': 'Space',
44
- 'keyUnset': 'not set',
45
- 'recognitionError': 'recognition failed',
46
- 'micUnavailable': 'Microphone unavailable: needs HTTPS or localhost',
47
- 'noRecorder': 'MediaRecorder is not supported by this browser',
48
- 'browserFailed': 'Browser did not recognize: ',
49
- 'nothingHeard': 'Nothing was recognized',
50
- 'dictationBtn': 'Voice typing',
51
- 'messageBtn': 'Voice message — click or hold',
52
- 'dictationPill': 'Dictation — text is appended to the input',
53
- 'messagePill': 'Recording a voice message',
54
- 'cancel': 'Cancel',
55
- 'holdHint': 'Hold — release to send',
56
- 'listening': 'Listening in the browser…',
57
- 'stop': 'Stop',
58
- 'transcribing': 'Transcribing…',
59
- 'sendingIn': 'Sending to the agent in',
60
- 'secondsShort': ' s',
61
- 'keepPending': 'Do not send',
62
- 'hide': 'Hide',
63
- 'title': 'Voice',
64
- 'recordSlot': 'Voice recording',
65
- 'browserHint': 'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
66
- 'browserWebGpu': 'Browser WebGPU Whisper (Offline)',
67
- 'browserWebGpuHint': 'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
68
- 'npuProvider': 'Hardware Acceleration (SBC NPU / Host)',
69
- 'npuProviderHint': 'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
70
- 'npuThreads': 'Threads (CPU/SBC)',
71
- 'benchmarkTitle': 'Hardware Benchmark (Speed & Latency)',
72
- 'runBenchmarkBtn': 'Run Benchmark', 'benchmarking': 'Running benchmark…',
73
- 'benchmarkHint': 'Measures real-time factor (RTF) and inference latency on 1.0s audio for current execution provider.',
74
- 'webgpuStatus': 'Browser WebGPU Status',
75
- 'webgpuAvailable': 'WebGPU Available (hardware acceleration ready)',
76
- 'webgpuNotAvailable': 'WebGPU Not Detected (will fallback to server)',
77
- 'openaiHint': 'whisper-1 · key OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
78
- 'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
79
- 'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
80
- 'localHint': 'set when the server is started', 'up': 'Up', 'down': 'Down', 'remove': 'Remove',
81
- 'addProvider': 'Add provider',
82
- 'chainHint': 'Top to bottom is the order they are tried in',
83
- 'customName': 'name used in the chain',
84
- 'customModel': 'model',
85
- 'customKeyName': 'key name',
86
- 'customModelHint': 'model to request (empty means the built-in one)',
87
- 'addCustom': 'Add your own provider',
88
- 'loadingSettings': 'Loading settings…',
89
- 'notReady1': 'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
90
- 'notReady2': 'the section will appear on its own in a few seconds.',
91
- 'hotkey': 'Voice message key',
92
- 'pressKey': 'Press a key…',
93
- 'clearKey': 'Clear the key',
94
- 'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
95
- 'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
96
- 'language': 'Language',
97
- 'langAuto': 'Auto-detect',
98
- 'langRu': 'Russian (ru)',
99
- 'langEn': 'English (en)',
100
- 'langZh': 'Chinese (zh)',
101
- 'langUk': 'Ukrainian (uk)',
102
- 'langDe': 'German (de)',
103
- 'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
104
- 'pauseMs': 'Pause that ends a phrase, ms',
105
- 'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
106
- 'speaking': 'You are speaking…',
107
- 'silence': 'Pause…',
108
- 'normalizeTranscript': 'Normalize file transcripts',
109
- 'undo': 'Undo last insert',
110
- 'undone': 'Insert undone',
111
- 'nothingToUndo': 'Nothing to undo',
112
- 'beep': 'Beep on start/stop',
113
- 'localOnly': 'Local whisper only',
114
- 'localOnlyHint': 'Restrict both chains to the local whisper.cpp server: fully offline.',
115
- 'sendDelay': 'Dictation send delay (ms)',
116
- 'sendDelayHint': 'Wait before appending a dictated phrase, with a cancel window. 0 = off',
117
- 'mic': 'Microphone',
118
- 'micDefault': 'System default',
119
- 'vocabulary': 'Custom vocabulary (one word per line)',
120
- 'polish': 'Polish transcript with model',
121
- 'polishHint': 'Fix punctuation and fillers via the harness model before inserting',
122
- 'stream': 'Continuous dictation',
123
- 'streamHint': 'Cut phrases by a timer while you speak instead of waiting for a long pause',
124
- 'streamChunkMs': 'Stream chunk (ms)',
125
- 'vadAdapt': 'Adaptive silence',
126
- 'vadAdaptHint': 'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
127
- 'wakeWord': 'Wake word',
128
- 'wakeWordHint': 'Browser recognition starts recording when speech begins with this phrase. Empty = off',
129
- 'bargeIn': 'Barge-in',
130
- 'polishSend': 'Polish whole draft before sending',
131
- 'polishSendHint': 'Run the composed draft through the model right before sending',
132
- 'editablePreview': 'Editable preview before send',
133
- 'editablePreviewHint': 'Show recognized text before sending with editing, comparison and abort options.',
134
- 'editPreview': 'Edit',
135
- 'sendNow': 'Send',
136
- 'cancelPreview': 'Cancel',
137
- 'rawText': 'Raw',
138
- 'polishedText': 'Polished',
139
- 'sessionMismatch': 'Different chat active; canceled to prevent leak.',
140
- 'sessionCommands': 'Voice session commands',
141
- 'sessionCommandsHint': '"send", "cancel", "stop", "continue" act on the session instead of becoming text',
142
- 'polishBaseUrl': 'Offline polish endpoint',
143
- 'polishBaseUrlHint': 'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
144
- 'polishModel': 'Offline polish model',
145
- 'polishKeyEnv': 'Offline polish key credential',
146
- 'voiceCommandsLabel': 'Voice edit commands ("new line", "paragraph")',
147
- 'normalizeTranscriptHint': 'transcribe_audio: spoken numbers to digits, tidy punctuation',
148
- 'messageTitle': 'Voice message',
149
- 'messageHint': 'One whole recording, sent to the agent once it is transcribed.',
150
- 'undoMs': 'Undo window, ms',
151
- 'undoHint': 'How long the automatic send can still be called off',
152
- 'customTitle': 'Your own providers',
153
- 'customHint': 'Any OpenAI-compatible API. The name becomes available in the chains above.',
154
- 'general': 'General',
155
- 'whisperEndpoint': 'Local whisper: endpoint',
156
- 'whisperEndpointHint': 'POST /inference of a whisper.cpp server',
157
- 'deepgramEndpoint': 'Deepgram: base URL',
158
- 'deepgramEndpointHint': 'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
159
- 'whisperBin': 'Local whisper: binary',
160
- 'whisperBinHint': 'used when autostart is on',
161
- 'whisperModel': 'Local whisper: model',
162
- 'whisperModelHint': 'Absolute path to the ggml model file',
163
- 'whisperAutostart': 'Autostart the local whisper',
164
- 'save': 'Save',
165
- 'saved': 'Saved ✓',
166
- 'openrouterWarning': 'OpenRouter has no /audio/transcriptions — use the openai-chat-audio template there',
167
- 'noiseSuppression': 'Hardware noise suppression',
168
- 'noiseSuppressionHint': 'Enable browser noise suppression, echo cancellation, and auto gain control',
169
- 'contextGlossary': 'Context glossary injection',
170
- 'contextGlossaryHint': 'Auto-extract code identifiers and terms from composer to improve STT accuracy',
171
- 'providerDashboard': 'Provider Latency & Health',
172
- 'avgLatency': 'Avg latency',
173
- 'successRate': 'Success',
174
- 'fast': 'Fast',
175
- 'normal': 'Normal',
176
- 'slow': 'Slow',
177
- 'error': 'Error',
178
- 'idle': 'No calls',
179
- 'play': 'Play',
180
- 'pause': 'Pause',
181
- 'listenBack': 'Listen back',
182
- 'lastRecording': 'Last voice note',
183
- 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
184
- 'sensevoiceEndpoint': 'SenseVoice: endpoint',
185
- 'sensevoiceEndpointHint': 'POST endpoint of sherpa-onnx or compatible server',
186
- 'sensevoiceBin': 'SenseVoice: binary',
187
- 'sensevoiceBinHint': 'used when autostart is on',
188
- 'sensevoiceModel': 'SenseVoice: model path',
189
- 'sensevoiceModelHint': 'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
190
- 'sensevoiceAutostart': 'Autostart the SenseVoice server',
191
- 'visualizerStyle': 'Audio visualizer style',
192
- 'visualizerStyleHint': 'Waveform animation inside the recording pill',
193
- 'visLiquidWave': 'Liquid Wave',
194
- 'visDynamicOrb': 'Dynamic Orb',
195
- 'visBars': 'Classic Bars',
196
- 'visOff': 'Off',
197
- 'statusTitle': 'Connection & Engine Status',
198
- 'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
199
- 'badgeHostOnline': 'Host online ({ms} ms)',
200
- 'badgeHostOffline': 'Host unreachable',
201
- 'badgeWhisperActive': 'Whisper active',
202
- 'badgeWhisperInactive': 'Whisper offline',
203
- 'badgeSenseVoiceActive': 'SenseVoice active',
204
- 'badgeSenseVoiceInactive': 'SenseVoice offline',
205
- 'badgeProviders': '{count} providers ready',
206
- 'refreshStats': 'Refresh Stats',
207
- 'hardwareTitle': 'Hardware & Local Engines',
208
- 'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
209
- 'uiError': 'Voice UI Error:',
210
- 'retry': 'Retry',
211
- 'testMic': 'Test Mic',
212
- 'stopTest': 'Stop Test',
213
- 'micLevel': 'Mic level',
214
- 'noiseGate': 'Noise Gate (dB)',
215
- 'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
216
- 'noiseGateOff': 'Off',
217
- 'versionTitle': 'Plugin Version & Updates',
218
- 'versionHint': 'Check for new versions and update directly from npm.',
219
- 'updateAvailable': 'Update available',
220
- 'upToDate': 'Up to date',
221
- 'updatePrompt': 'A new version is available on npm. Click update to install.',
222
- 'updateNow': 'Update now',
223
- 'updating': 'Updating…',
224
- 'updateSuccess': 'Updated successfully! Restart DSH to apply.',
225
- 'updateFailed': 'Update failed. Check logs.',
226
- 'updateCheckFailed': 'Update check failed',
227
- 'profileLockedPid': 'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
228
- 'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
229
- 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
230
- 'checkingUpdates': 'Checking updates…',
231
- 'updateStatusUnknown': 'Version status unknown',
232
- 'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
233
- 'sensevoiceDesc': 'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
234
- 'installSensevoice': 'Install SenseVoice-Small (1-Click)',
235
- 'installingSensevoice': 'Installing SenseVoice…',
236
- 'sensevoiceReady': 'Ready',
237
- 'sensevoiceNotInstalled': 'Not Installed',
238
- 'sensevoiceDownloading': 'Downloading model…',
239
- 'turnTakingTitle': 'Turn-Taking & Audio Coordination',
240
- 'turnTakingDesc': 'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
241
- 'gatedTurnTaking': 'Gated Mode (mute mic while assistant is speaking)',
242
- 'bargeInDesc': 'True Barge-In (speaking interrupts assistant speech)',
243
- 'smartActionsTitle': 'Hands-free & Smart Actions',
244
- 'autoSendVisualRing': 'Silence Ring countdown animation',
245
- 'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
246
- 'liveInterimPreview': 'Live speech preview (ghost text preview)',
247
- 'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
248
- 'modeVerbatim': 'Verbatim',
249
- 'modeClean': 'Clean Text',
250
- 'modeTask': 'Agent Task',
251
- 'modeNote': 'Note',
252
- 'dictationModeTitle': 'Dictation Post-Processing Mode',
253
- '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).',
254
- 'dictationPromptsTitle': 'Dictation Prompt Templates',
255
- 'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
256
- 'promptClean': 'Clean text prompt',
257
- 'promptTask': 'Agent task prompt',
258
- 'promptNote': 'Note prompt',
259
- 'resetPrompts': 'Reset to defaults',
260
- 'voiceMacrosTitle': 'Voice Action Macros',
261
- 'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
262
- 'addMacro': 'Add macro',
263
- 'macroTrigger': 'Spoken trigger phrase',
264
- 'macroTemplate': 'Expanded prompt template',
265
- 'macroDelete': 'Delete',
266
- 'rawTab': 'Raw STT',
267
- 'resultTab': 'Result',
268
- 'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
269
- 'jargonTitle': 'Developer Lexicon & IT Jargon',
270
- 'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
271
- 'jargonAdd': 'Add replacement',
272
- 'jargonFrom': 'Spoken slang',
273
- 'jargonTo': 'Correct spelling',
274
- 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
275
- 'voiceInput': 'Voice input',
276
- 'assistantSpeakingGated': 'Assistant speaking (gated mode)',
277
- 'jargonFormatHint': ' (spoken -> replacement)',
278
- 'errorPrefix': 'Error',
279
- 'wizardTitle': 'First-Run Setup & Diagnostics Wizard',
280
- 'wizardDesc': 'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
281
- 'wizardBtnLaunch': 'Launch Mic & STT Wizard',
282
- 'wizardClose': 'Close Wizard',
283
- 'wizardStep1': '1. Audio & Environment',
284
- 'wizardStep2': '2. Live Signal Test',
285
- 'wizardStep3': '3. STT Transcription Test',
286
- 'wizardSecureContextOk': 'Secure context / HTTPS verified',
287
- 'wizardSecureContextBad': 'Insecure context (HTTPS or localhost required)',
288
- 'wizardMediaDevicesOk': 'Browser mediaDevices API supported',
289
- 'wizardMediaDevicesBad': 'Browser mediaDevices API missing',
290
- 'wizardMediaRecorderOk': 'MediaRecorder API supported',
291
- 'wizardMediaRecorderBad': 'MediaRecorder API unsupported in this browser',
292
- 'wizardMicPermissionOk': 'Microphone permission granted',
293
- 'wizardMicPermissionPrompt': 'Microphone permission not yet granted',
294
- 'wizardMicPermissionBad': 'Microphone permission denied',
295
- 'wizardRequestMic': 'Request Microphone Access',
296
- 'wizardMicLabel': 'Microphone input device:',
297
- 'wizardStartSignalCheck': 'Start Audio Level Test', 'wizardStopSignalCheck': 'Stop Audio Test',
298
- 'wizardSignalOk': 'Audio signal detected (RMS: {level}%)',
299
- 'wizardSignalZero': 'Zero audio level detected - speak into microphone or select correct device',
300
- 'wizardSignalTesting': 'Listening for audio signal… speak into your microphone',
301
- 'wizardProviderLabel': 'Active STT engine & model:',
302
- 'wizardProcessingArea': 'Processing area:',
303
- 'wizardAreaBrowser': 'In-browser (WebGPU / WebSpeech)', 'wizardAreaServer': 'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud': 'Remote cloud provider',
304
- 'wizardStartTestRecord': 'Record 3-Second Test Phrase', 'wizardRecordingTest': 'Recording test phrase… speak now ({left}s)',
305
- 'wizardTranscribingTest': 'Transcribing test phrase…', 'wizardTestResult': 'STT Test Result:',
306
- 'wizardTestSuccess': 'STT round-trip verified successfully! Latency: {latency}ms',
307
- 'wizardTestEmpty': 'No speech detected in test recording. Please speak clearly and try again.',
308
- 'wizardTestFailed': 'STT test failed: {error}',
309
- 'wizardNextStepHint': 'Next step: {hint}',
310
- 'wizardHintPermission': 'Click "Request Microphone Access" and allow browser permission prompt.',
311
- 'wizardHintDevice': 'Check microphone connection or select a different input device.',
312
- 'wizardHintHttps': 'Open DeepSeek Harness via HTTPS or localhost address.',
313
- 'wizardHintProvider': 'Configure STT engine credentials or install SenseVoice model below.',
314
- 'wizardFinishSuccess': 'Setup & Verification Complete',
315
- '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',
321
- 'diagTotal': 'Total: {ms}ms', 'diagBytes': 'Audio: {bytes} B', 'diagWords': 'Words: {count}',
322
- 'diagExportNotice': 'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
38
+ 'saveFailed': 'Some fields were not saved —',
39
+ 'cardHint': 'Dictation and voice messages: providers, chains, local whisper',
40
+ 'expand': 'Expand',
41
+ 'collapse': 'Collapse',
42
+ 'composerUnavailable': 'Composer unavailable',
43
+ 'keySpace': 'Space',
44
+ 'keyUnset': 'not set',
45
+ 'recognitionError': 'recognition failed',
46
+ 'micUnavailable': 'Microphone unavailable: needs HTTPS or localhost',
47
+ 'noRecorder': 'MediaRecorder is not supported by this browser',
48
+ 'browserFailed': 'Browser did not recognize: ',
49
+ 'nothingHeard': 'Nothing was recognized',
50
+ 'dictationBtn': 'Voice typing',
51
+ 'messageBtn': 'Voice message — click or hold',
52
+ 'dictationPill': 'Dictation — text is appended to the input',
53
+ 'messagePill': 'Recording a voice message',
54
+ 'cancel': 'Cancel',
55
+ 'holdHint': 'Hold — release to send',
56
+ 'listening': 'Listening in the browser…',
57
+ 'stop': 'Stop',
58
+ 'transcribing': 'Transcribing…',
59
+ 'sendingIn': 'Sending to the agent in',
60
+ 'secondsShort': ' s',
61
+ 'keepPending': 'Do not send',
62
+ 'hide': 'Hide',
63
+ 'title': 'Voice',
64
+ 'recordSlot': 'Voice recording',
65
+ 'browserHint': 'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
66
+ 'browserWebGpu': 'Browser WebGPU Whisper (Offline)',
67
+ 'browserWebGpuHint': 'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
68
+ 'npuProvider': 'Hardware Acceleration (SBC NPU / Host)',
69
+ 'npuProviderHint': 'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
70
+ 'npuThreads': 'Threads (CPU/SBC)',
71
+ 'benchmarkTitle': 'Hardware Benchmark (Speed & Latency)',
72
+ 'runBenchmarkBtn': 'Run Benchmark', 'benchmarking': 'Running benchmark…',
73
+ 'benchmarkHint': 'Measures real-time factor (RTF) and inference latency on 1.0s audio for current execution provider.',
74
+ 'webgpuStatus': 'Browser WebGPU Status',
75
+ 'webgpuAvailable': 'WebGPU Available (hardware acceleration ready)',
76
+ 'webgpuNotAvailable': 'WebGPU Not Detected (will fallback to server)',
77
+ 'openaiHint': 'whisper-1 · key OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
78
+ 'deepinfraHint': 'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · key FIREWORKS_API_KEY',
79
+ 'mistralHint': 'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
80
+ 'localHint': 'set when the server is started', 'up': 'Up', 'down': 'Down', 'remove': 'Remove',
81
+ 'addProvider': 'Add provider',
82
+ 'chainHint': 'Top to bottom is the order they are tried in',
83
+ 'customName': 'name used in the chain',
84
+ 'customModel': 'model',
85
+ 'customKeyName': 'key name',
86
+ 'customModelHint': 'model to request (empty means the built-in one)',
87
+ 'addCustom': 'Add your own provider',
88
+ 'loadingSettings': 'Loading settings…',
89
+ 'notReady1': 'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
90
+ 'notReady2': 'the section will appear on its own in a few seconds.',
91
+ 'hotkey': 'Voice message key',
92
+ 'pressKey': 'Press a key…',
93
+ 'clearKey': 'Clear the key',
94
+ 'hotkeyHint1': 'Hold it to record, release to send to the agent, Esc cancels. ',
95
+ 'hotkeyHint2': 'Any key will do: a letter, an F-key or a modifier.',
96
+ 'language': 'Language',
97
+ 'langAuto': 'Auto-detect',
98
+ 'langRu': 'Russian (ru)',
99
+ 'langEn': 'English (en)',
100
+ 'langZh': 'Chinese (zh)',
101
+ 'langUk': 'Ukrainian (uk)',
102
+ 'langDe': 'German (de)',
103
+ 'dictationHint': 'Speech is cut at pauses and the text is appended to the input.',
104
+ 'pauseMs': 'Pause that ends a phrase, ms',
105
+ 'pauseHint': 'Lower means more frequent chunks and faster text, but a higher risk of cutting a word',
106
+ 'speaking': 'You are speaking…',
107
+ 'silence': 'Pause…',
108
+ 'normalizeTranscript': 'Normalize file transcripts',
109
+ 'undo': 'Undo last insert',
110
+ 'undone': 'Insert undone',
111
+ 'nothingToUndo': 'Nothing to undo',
112
+ 'beep': 'Beep on start/stop',
113
+ 'localOnly': 'Local whisper only',
114
+ 'localOnlyHint': 'Restrict both chains to the local whisper.cpp server: fully offline.',
115
+ 'sendDelay': 'Dictation send delay (ms)',
116
+ 'sendDelayHint': 'Wait before appending a dictated phrase, with a cancel window. 0 = off',
117
+ 'mic': 'Microphone',
118
+ 'micDefault': 'System default',
119
+ 'vocabulary': 'Custom vocabulary (one word per line)',
120
+ 'polish': 'Polish transcript with model',
121
+ 'polishHint': 'Fix punctuation and fillers via the harness model before inserting',
122
+ 'stream': 'Continuous dictation',
123
+ 'streamHint': 'Cut phrases by a timer while you speak instead of waiting for a long pause',
124
+ 'streamChunkMs': 'Stream chunk (ms)',
125
+ 'vadAdapt': 'Adaptive silence',
126
+ 'vadAdaptHint': 'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
127
+ 'wakeWord': 'Wake word',
128
+ 'wakeWordHint': 'Browser recognition starts recording when speech begins with this phrase. Empty = off',
129
+ 'bargeIn': 'Barge-in',
130
+ 'polishSend': 'Polish whole draft before sending',
131
+ 'polishSendHint': 'Run the composed draft through the model right before sending',
132
+ 'editablePreview': 'Editable preview before send',
133
+ 'editablePreviewHint': 'Show recognized text before sending with editing, comparison and abort options.',
134
+ 'editPreview': 'Edit',
135
+ 'sendNow': 'Send',
136
+ 'cancelPreview': 'Cancel',
137
+ 'rawText': 'Raw',
138
+ 'polishedText': 'Polished',
139
+ 'sessionMismatch': 'Different chat active; canceled to prevent leak.',
140
+ 'sessionCommands': 'Voice session commands',
141
+ 'sessionCommandsHint': '"send", "cancel", "stop", "continue" act on the session instead of becoming text',
142
+ 'polishBaseUrl': 'Offline polish endpoint',
143
+ 'polishBaseUrlHint': 'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
144
+ 'polishModel': 'Offline polish model',
145
+ 'polishKeyEnv': 'Offline polish key credential',
146
+ 'voiceCommandsLabel': 'Voice edit commands ("new line", "paragraph")',
147
+ 'normalizeTranscriptHint': 'transcribe_audio: spoken numbers to digits, tidy punctuation',
148
+ 'messageTitle': 'Voice message',
149
+ 'messageHint': 'One whole recording, sent to the agent once it is transcribed.',
150
+ 'undoMs': 'Undo window, ms',
151
+ 'undoHint': 'How long the automatic send can still be called off',
152
+ 'customTitle': 'Your own providers',
153
+ 'customHint': 'Any OpenAI-compatible API. The name becomes available in the chains above.',
154
+ 'general': 'General',
155
+ 'whisperEndpoint': 'Local whisper: endpoint',
156
+ 'whisperEndpointHint': 'POST /inference of a whisper.cpp server',
157
+ 'deepgramEndpoint': 'Deepgram: base URL',
158
+ 'deepgramEndpointHint': 'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
159
+ 'whisperBin': 'Local whisper: binary',
160
+ 'whisperBinHint': 'used when autostart is on',
161
+ 'whisperModel': 'Local whisper: model',
162
+ 'whisperModelHint': 'Absolute path to the ggml model file',
163
+ 'whisperAutostart': 'Autostart the local whisper',
164
+ 'bilingualDeveloperPrompt': 'Bilingual developer context (RU/EN)',
165
+ 'bilingualDeveloperPromptHint': 'Hint technical terms and CLI commands to the STT model to prevent phonetic transliteration',
166
+ 'calibrateNoise': 'Calibrate Noise (2s)',
167
+ 'calibrateNoiseHint': 'Measure room noise for 2 seconds and auto-set the optimal noise gate threshold',
168
+ 'calibrating': 'Measuring noise…',
169
+ 'calibratedMsg': 'Gate set: {db} dB (ambient: {noise} dB)',
170
+ 'calibratedValue': 'Calibrated',
171
+ 'save': 'Save',
172
+ 'saved': 'Saved ✓',
173
+ 'openrouterWarning': 'OpenRouter has no /audio/transcriptions — use the openai-chat-audio template there',
174
+ 'noiseSuppression': 'Hardware noise suppression',
175
+ 'noiseSuppressionHint': 'Enable browser noise suppression, echo cancellation, and auto gain control',
176
+ 'contextGlossary': 'Context glossary injection',
177
+ 'contextGlossaryHint': 'Auto-extract code identifiers and terms from composer to improve STT accuracy',
178
+ 'providerDashboard': 'Provider Latency & Health',
179
+ 'avgLatency': 'Avg latency',
180
+ 'successRate': 'Success',
181
+ 'fast': 'Fast',
182
+ 'normal': 'Normal',
183
+ 'slow': 'Slow',
184
+ 'error': 'Error',
185
+ 'idle': 'No calls',
186
+ 'play': 'Play',
187
+ 'pause': 'Pause',
188
+ 'listenBack': 'Listen back',
189
+ 'lastRecording': 'Last voice note',
190
+ 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
191
+ 'sensevoiceEndpoint': 'SenseVoice: endpoint',
192
+ 'sensevoiceEndpointHint': 'POST endpoint of sherpa-onnx or compatible server',
193
+ 'sensevoiceBin': 'SenseVoice: binary',
194
+ 'sensevoiceBinHint': 'used when autostart is on',
195
+ 'sensevoiceModel': 'SenseVoice: model path',
196
+ 'sensevoiceModelHint': 'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
197
+ 'sensevoiceAutostart': 'Autostart the SenseVoice server',
198
+ 'visualizerStyle': 'Audio visualizer style',
199
+ 'visualizerStyleHint': 'Waveform animation inside the recording pill',
200
+ 'visLiquidWave': 'Liquid Wave',
201
+ 'visDynamicOrb': 'Dynamic Orb',
202
+ 'visBars': 'Classic Bars',
203
+ 'visOff': 'Off',
204
+ 'statusTitle': 'Connection & Engine Status',
205
+ 'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
206
+ 'badgeHostOnline': 'Host online ({ms} ms)',
207
+ 'badgeHostOffline': 'Host unreachable',
208
+ 'badgeWhisperActive': 'Whisper active',
209
+ 'badgeWhisperInactive': 'Whisper offline',
210
+ 'badgeSenseVoiceActive': 'SenseVoice active',
211
+ 'badgeSenseVoiceInactive': 'SenseVoice offline',
212
+ 'badgeProviders': '{count} providers ready',
213
+ 'refreshStats': 'Refresh Stats',
214
+ 'hardwareTitle': 'Hardware & Local Engines',
215
+ 'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
216
+ 'uiError': 'Voice UI Error:',
217
+ 'retry': 'Retry',
218
+ 'testMic': 'Test Mic',
219
+ 'stopTest': 'Stop Test',
220
+ 'micLevel': 'Mic level',
221
+ 'noiseGate': 'Noise Gate (dB)',
222
+ 'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
223
+ 'noiseGateOff': 'Off',
224
+ 'versionTitle': 'Plugin Version & Updates',
225
+ 'versionHint': 'Check for new versions and update directly from npm.',
226
+ 'updateAvailable': 'Update available',
227
+ 'upToDate': 'Up to date',
228
+ 'updatePrompt': 'A new version is available on npm. Click update to install.',
229
+ 'updateNow': 'Update now',
230
+ 'updating': 'Updating…',
231
+ 'updateSuccess': 'Updated successfully! Restart DSH to apply.',
232
+ 'updateFailed': 'Update failed. Check logs.',
233
+ 'updateCheckFailed': 'Update check failed',
234
+ 'profileLockedPid': 'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
235
+ 'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
236
+ 'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
237
+ 'checkingUpdates': 'Checking updates…',
238
+ 'updateStatusUnknown': 'Version status unknown',
239
+ 'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
240
+ 'sensevoiceDesc': 'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
241
+ 'installSensevoice': 'Install SenseVoice-Small (1-Click)',
242
+ 'installingSensevoice': 'Installing SenseVoice…',
243
+ 'sensevoiceReady': 'Ready',
244
+ 'sensevoiceNotInstalled': 'Not Installed',
245
+ 'sensevoiceDownloading': 'Downloading model…',
246
+ 'turnTakingTitle': 'Turn-Taking & Audio Coordination',
247
+ 'turnTakingDesc': 'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
248
+ 'gatedTurnTaking': 'Gated Mode (mute mic while assistant is speaking)',
249
+ 'bargeInDesc': 'True Barge-In (speaking interrupts assistant speech)',
250
+ 'smartActionsTitle': 'Hands-free & Smart Actions',
251
+ 'autoSendVisualRing': 'Silence Ring countdown animation',
252
+ 'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
253
+ 'liveInterimPreview': 'Live speech preview (ghost text preview)',
254
+ 'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
255
+ 'modeVerbatim': 'Verbatim',
256
+ 'modeClean': 'Clean Text',
257
+ 'modeTask': 'Agent Task',
258
+ 'modeNote': 'Note',
259
+ 'dictationModeTitle': 'Dictation Post-Processing Mode',
260
+ '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).',
261
+ 'dictationPromptsTitle': 'Dictation Prompt Templates',
262
+ 'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
263
+ 'promptClean': 'Clean text prompt',
264
+ 'promptTask': 'Agent task prompt',
265
+ 'promptNote': 'Note prompt',
266
+ 'resetPrompts': 'Reset to defaults',
267
+ 'voiceMacrosTitle': 'Voice Action Macros',
268
+ 'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
269
+ 'addMacro': 'Add macro',
270
+ 'macroTrigger': 'Spoken trigger phrase',
271
+ 'macroTemplate': 'Expanded prompt template',
272
+ 'macroDelete': 'Delete',
273
+ 'rawTab': 'Raw STT',
274
+ 'resultTab': 'Result',
275
+ 'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
276
+ 'jargonTitle': 'Developer Lexicon & IT Jargon',
277
+ 'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
278
+ 'jargonAdd': 'Add replacement',
279
+ 'jargonFrom': 'Spoken slang',
280
+ 'jargonTo': 'Correct spelling',
281
+ 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
282
+ 'voiceInput': 'Voice input',
283
+ 'assistantSpeakingGated': 'Assistant speaking (gated mode)',
284
+ 'jargonFormatHint': ' (spoken -> replacement)',
285
+ 'errorPrefix': 'Error',
286
+ 'wizardTitle': 'First-Run Setup & Diagnostics Wizard',
287
+ 'wizardDesc': 'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
288
+ 'wizardBtnLaunch': 'Launch Mic & STT Wizard',
289
+ 'wizardClose': 'Close Wizard',
290
+ 'wizardStep1': '1. Audio & Environment',
291
+ 'wizardStep2': '2. Live Signal Test',
292
+ 'wizardStep3': '3. STT Transcription Test',
293
+ 'wizardSecureContextOk': 'Secure context / HTTPS verified',
294
+ 'wizardSecureContextBad': 'Insecure context (HTTPS or localhost required)',
295
+ 'wizardMediaDevicesOk': 'Browser mediaDevices API supported',
296
+ 'wizardMediaDevicesBad': 'Browser mediaDevices API missing',
297
+ 'wizardMediaRecorderOk': 'MediaRecorder API supported',
298
+ 'wizardMediaRecorderBad': 'MediaRecorder API unsupported in this browser',
299
+ 'wizardMicPermissionOk': 'Microphone permission granted',
300
+ 'wizardMicPermissionPrompt': 'Microphone permission not yet granted',
301
+ 'wizardMicPermissionBad': 'Microphone permission denied',
302
+ 'wizardRequestMic': 'Request Microphone Access',
303
+ 'wizardMicLabel': 'Microphone input device:',
304
+ 'wizardStartSignalCheck': 'Start Audio Level Test', 'wizardStopSignalCheck': 'Stop Audio Test',
305
+ 'wizardSignalOk': 'Audio signal detected (RMS: {level}%)',
306
+ 'wizardSignalZero': 'Zero audio level detected - speak into microphone or select correct device',
307
+ 'wizardSignalTesting': 'Listening for audio signal… speak into your microphone',
308
+ 'wizardProviderLabel': 'Active STT engine & model:',
309
+ 'wizardProcessingArea': 'Processing area:', 'wizardAreaBrowser': 'In-browser (WebGPU / WebSpeech)', 'wizardAreaServer': 'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud': 'Remote cloud provider',
310
+ 'wizardStartTestRecord': 'Record 3-Second Test Phrase', 'wizardRecordingTest': 'Recording test phrase… speak now ({left}s)',
311
+ 'wizardTranscribingTest': 'Transcribing test phrase…', 'wizardTestResult': 'STT Test Result:',
312
+ 'wizardTestSuccess': 'STT round-trip verified successfully! Latency: {latency}ms',
313
+ 'wizardTestEmpty': 'No speech detected in test recording. Please speak clearly and try again.',
314
+ 'wizardTestFailed': 'STT test failed: {error}',
315
+ 'wizardNextStepHint': 'Next step: {hint}',
316
+ '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.',
317
+ 'wizardFinishSuccess': 'Setup & Verification Complete', 'wizardSkip': 'Skip Wizard',
318
+ 'wizardStatusCompleted': 'Microphone and STT verified',
319
+ 'exportDiagnosticsBtn': 'Export Diagnostics Report', 'exportingDiagnostics': 'Exporting report…',
320
+ 'diagnosticsExportSuccess': 'Report downloaded', 'recentRecognitionsTitle': 'Recent Recognition Telemetry',
321
+ 'noRecognitionsYet': 'No recent recognition records yet.', 'diagLatencyBreakdown': 'Latency breakdown:',
322
+ 'diagVad': 'VAD: {ms}ms', 'diagStt': 'STT: {ms}ms', 'diagPolish': 'Polish: {ms}ms', 'diagTotal': 'Total: {ms}ms', 'diagBytes': 'Audio: {bytes} B', 'diagWords': 'Words: {count}',
323
+ 'diagExportNotice': 'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
323
324
  }
324
325
 
325
326
  const zh = {
326
- 'saveFailed': '部分字段未保存 —',
327
- 'cardHint': '听写与语音消息:服务商、备用链、本地 whisper',
328
- 'expand': '展开',
329
- 'collapse': '收起',
330
- 'composerUnavailable': '输入框不可用',
331
- 'keySpace': '空格',
332
- 'keyUnset': '未设置',
333
- 'recognitionError': '识别失败',
334
- 'micUnavailable': '麦克风不可用:需要 HTTPS 或 localhost',
335
- 'noRecorder': '当前浏览器不支持 MediaRecorder',
336
- 'browserFailed': '浏览器未能识别:',
337
- 'nothingHeard': '未识别到任何内容',
338
- 'dictationBtn': '语音输入',
339
- 'messageBtn': '语音消息 — 点击或按住',
340
- 'dictationPill': '听写 — 文本将追加至输入框',
341
- 'messagePill': '正在录制语音消息',
342
- 'cancel': '取消',
343
- 'holdHint': '按住录音 — 松开即发送',
344
- 'listening': '浏览器正在倾听…',
345
- 'stop': '停止',
346
- 'transcribing': '正在转写…',
347
- 'sendingIn': '发送至智能体倒计时',
348
- 'secondsShort': ' 秒',
349
- 'keepPending': '不发送',
350
- 'hide': '隐藏',
351
- 'title': '语音',
352
- 'recordSlot': '语音录制',
353
- 'browserHint': 'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
354
- 'browserWebGpu': '浏览器 WebGPU Whisper (离线)',
355
- 'browserWebGpuHint': '通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
356
- 'npuProvider': '硬件加速 (SBC NPU / 主机)',
357
- 'npuProviderHint': 'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。',
358
- 'npuThreads': '计算线程 (CPU/SBC)',
359
- 'benchmarkTitle': '硬件基准测试 (速度与延迟)',
360
- 'runBenchmarkBtn': '运行基准测试', 'benchmarking': '正在运行测试…',
361
- 'benchmarkHint': '测试当前执行提供者在 1.0 秒音频下的实时率 (RTF) 与推理延迟。',
362
- 'webgpuStatus': '浏览器 WebGPU 状态',
363
- 'webgpuAvailable': 'WebGPU 可用 (硬件加速就绪)',
364
- 'webgpuNotAvailable': '未检测到 WebGPU (将自动回退到服务端)',
365
- 'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
366
- 'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
367
- 'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
368
- 'localHint': '服务器启动时配置', 'up': '上移', 'down': '下移', 'remove': '移除',
369
- 'addProvider': '添加服务商',
370
- 'chainHint': '自上而下为故障转移尝试顺序',
371
- 'customName': '备用链中使用的名称',
372
- 'customModel': '模型',
373
- 'customKeyName': '密钥名称',
374
- 'customModelHint': '请求的模型(留空使用内置默认值)',
375
- 'addCustom': '添加自定义服务商',
376
- 'loadingSettings': '正在加载设置…',
377
- 'notReady1': 'Harness 尚未同步此插件的设置。如果刚重启,',
378
- 'notReady2': '设置项将在数秒内自动呈现。',
379
- 'hotkey': '语音消息快捷键',
380
- 'pressKey': '请按下按键…',
381
- 'clearKey': '清除按键',
382
- 'hotkeyHint1': '按住录音,松开发送给智能体,按 Esc 取消。',
383
- 'hotkeyHint2': '支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
384
- 'language': '识别语言',
385
- 'langAuto': '自动检测',
386
- 'langRu': '俄语 (ru)',
387
- 'langEn': '英语 (en)',
388
- 'langZh': '中文 (zh)',
389
- 'langUk': '乌克兰语 (uk)',
390
- 'langDe': '德语 (de)',
391
- 'dictationHint': '语音按停顿自动切句,文字实时追加至输入框。',
392
- 'pauseMs': '断句静音停顿时长(毫秒)',
393
- 'pauseHint': '数值越小切分越频繁、出字越快,但可能切断未说完的词',
394
- 'speaking': '正在说话…',
395
- 'silence': '停顿…',
396
- 'normalizeTranscript': '规范化转写文本',
397
- 'undo': '撤销上次插入',
398
- 'undone': '插入已撤销',
399
- 'nothingToUndo': '没有可撤销的内容',
400
- 'beep': '启停提示音',
401
- 'localOnly': '仅使用本地 whisper',
402
- 'localOnlyHint': '将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。',
403
- 'sendDelay': '听写延迟追加 (ms)',
404
- 'sendDelayHint': '追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
405
- 'mic': '麦克风设备',
406
- 'micDefault': '系统默认',
407
- 'vocabulary': '自定义词汇表(每行一个词)',
408
- 'polish': '模型润色转写文本',
409
- 'polishHint': '插入前通过模型修正标点符号并消除语气口头禅',
410
- 'stream': '连续流式听写',
411
- 'streamHint': '说话期间按定时器切分短句,无需等待长时间停顿',
412
- 'streamChunkMs': '流式切片时长 (ms)',
413
- 'vadAdapt': '自适应静音阈值',
414
- 'vadAdaptHint': '根据语速动态调整停顿静音阈值。0 为固定阈值',
415
- 'wakeWord': '语音唤醒词',
416
- 'wakeWordHint': '说话以此短语开头时自动触发录音。留空为关闭',
417
- 'bargeIn': '语音插话打断 (Barge-in)',
418
- 'polishSend': '发送前润色整篇草稿',
419
- 'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
420
- 'editablePreview': '发送前可编辑预览',
421
- 'editablePreviewHint': '发送前显示识别文本,支持编辑、润色对比和撤销。',
422
- 'editPreview': '编辑',
423
- 'sendNow': '发送',
424
- 'cancelPreview': '取消',
425
- 'rawText': '原文',
426
- 'polishedText': '润色',
427
- 'sessionMismatch': '当前会话已切换,已拦截发送以防止串话。',
428
- 'sessionCommands': '会话语音控制指令',
429
- 'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
430
- 'polishBaseUrl': '离线润色服务端点',
431
- 'polishBaseUrlHint': '兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型',
432
- 'polishModel': '离线润色模型',
433
- 'polishKeyEnv': '离线润色密钥凭据名',
434
- 'voiceCommandsLabel': '语音编辑指令("换行"、"段落")',
435
- 'normalizeTranscriptHint': 'transcribe_audio:口语数字转阿拉伯数字,整理标点符号',
436
- 'messageTitle': '语音消息',
437
- 'messageHint': '录制单条完整语音,转写完成后自动发送给智能体。',
438
- 'undoMs': '撤回窗口时长(毫秒)',
439
- 'undoHint': '转写完成后多久以内可以随时取消发送',
440
- 'customTitle': '自定义语音服务商',
441
- 'customHint': '兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。',
442
- 'general': '通用设置',
443
- 'whisperEndpoint': '本地 whisper:端点地址',
444
- 'whisperEndpointHint': 'whisper.cpp 服务器的 POST /inference 接口',
445
- 'deepgramEndpoint': 'Deepgram:基础地址',
446
- 'deepgramEndpointHint': 'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)',
447
- 'whisperBin': '本地 whisper:可执行文件',
448
- 'whisperBinHint': '开启自动启动时使用',
449
- 'whisperModel': '本地 whisper:模型文件',
450
- 'whisperModelHint': 'ggml 模型文件的绝对路径',
451
- 'whisperAutostart': '自动启动本地 whisper',
452
- 'save': '保存',
453
- 'saved': '已保存 ✓',
454
- 'openrouterWarning': 'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板',
455
- 'noiseSuppression': '硬件级降噪',
456
- 'noiseSuppressionHint': '启用浏览器硬件降噪、回声消除与自动增益控制',
457
- 'contextGlossary': '上下文专业词汇注入',
458
- 'contextGlossaryHint': '自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
459
- 'providerDashboard': '服务商延迟与健康监控',
460
- 'avgLatency': '平均延迟',
461
- 'successRate': '成功率',
462
- 'fast': '极速',
463
- 'normal': '正常',
464
- 'slow': '较慢',
465
- 'error': '异常',
466
- 'idle': '无调用记录',
467
- 'play': '播放',
468
- 'pause': '暂停',
469
- 'listenBack': '试听回放',
470
- 'lastRecording': '最近一条语音记录',
471
- 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)',
472
- 'sensevoiceEndpoint': 'SenseVoice:端点地址',
473
- 'sensevoiceEndpointHint': 'sherpa-onnx 或兼容服务器的 POST 端点',
474
- 'sensevoiceBin': 'SenseVoice:可执行文件',
475
- 'sensevoiceBinHint': '开启自动启动时使用',
476
- 'sensevoiceModel': 'SenseVoice:模型路径',
477
- 'sensevoiceModelHint': 'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
478
- 'sensevoiceAutostart': '自动启动 SenseVoice 服务',
479
- 'visualizerStyle': '音频动态波形样式',
480
- 'visualizerStyleHint': '录音胶囊面板内的动态音频波形动画效果',
481
- 'visLiquidWave': '流体水波 (Liquid Wave)',
482
- 'visDynamicOrb': '动态脉冲球体 (Dynamic Orb)',
483
- 'visBars': '经典频条 (Classic Bars)',
484
- 'visOff': '关闭',
485
- 'statusTitle': '连接与引擎状态看板',
486
- 'statusDesc': '当前语音后端与识别服务的实时网络与进程就绪状态。',
487
- 'badgeHostOnline': '服务端在线 ({ms} ms)',
488
- 'badgeHostOffline': '服务端不可达',
489
- 'badgeWhisperActive': 'Whisper 就绪',
490
- 'badgeWhisperInactive': 'Whisper 未就绪',
491
- 'badgeSenseVoiceActive': 'SenseVoice 就绪',
492
- 'badgeSenseVoiceInactive': 'SenseVoice 未就绪',
493
- 'badgeProviders': '{count} 个服务商已就绪',
494
- 'refreshStats': '刷新状态',
495
- 'hardwareTitle': '硬件与本地引擎',
496
- 'hardwareDesc': '麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。',
497
- 'uiError': '语音界面错误:',
498
- 'retry': '重试',
499
- 'testMic': '测试麦克风',
500
- 'stopTest': '停止测试',
501
- 'micLevel': '麦克风电平',
502
- 'noiseGate': '噪声门 (dB)',
503
- 'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
504
- 'noiseGateOff': '关闭',
505
- 'versionTitle': '插件版本与更新',
506
- 'versionHint': '检查新版本并直接从 npm 更新。',
507
- 'updateAvailable': '有可用更新',
508
- 'upToDate': '已是最新版本',
509
- 'updatePrompt': 'npm 上有新版本可用。点击更新进行安装。',
510
- 'updateNow': '立即更新',
511
- 'updating': '正在更新…',
512
- 'updateSuccess': '更新成功!请重启 DSH 生效。',
513
- 'updateFailed': '更新失败,请查看日志。',
514
- 'updateCheckFailed': '检查更新失败',
515
- 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
516
- 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
517
- 'profileLockCheckError': '检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
518
- 'checkingUpdates': '正在检查更新…',
519
- 'updateStatusUnknown': '版本状态未知',
520
- 'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
521
- 'sensevoiceDesc': 'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。',
522
- 'installSensevoice': '一键安装 SenseVoice-Small',
523
- 'installingSensevoice': '正在安装 SenseVoice…',
524
- 'sensevoiceReady': '已就绪',
525
- 'sensevoiceNotInstalled': '未安装',
526
- 'sensevoiceDownloading': '正在下载模型…',
527
- 'turnTakingTitle': '轮流对话与音频协同',
528
- 'turnTakingDesc': '协调听与说,避免扬声器播放的声音被麦克风二次拾音。',
529
- 'gatedTurnTaking': '门控模式 (助手播报时麦克风自动静音)',
530
- 'bargeInDesc': '实时打断 (说话时立即打断助手朗读)',
531
- 'smartActionsTitle': '免提与智能指令',
532
- 'autoSendVisualRing': '环形倒计时动画指示器',
533
- 'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
534
- 'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
535
- 'structuredPromptVoice': '语音应答交互式选择提示',
536
- 'modeVerbatim': '逐字直录',
537
- 'modeClean': '清爽文本',
538
- 'modeTask': '智能体任务',
539
- 'modeNote': '便签备忘',
540
- 'dictationModeTitle': '听写后处理模式',
541
- 'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
542
- 'dictationPromptsTitle': '听写提示词模板',
543
- 'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
544
- 'promptClean': '清爽文本提示词',
545
- 'promptTask': '智能体任务提示词',
546
- 'promptNote': '便签提示词',
547
- 'resetPrompts': '恢复默认模板',
548
- 'voiceMacrosTitle': '语音动作宏',
549
- 'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
550
- 'addMacro': '添加动作宏',
551
- 'macroTrigger': '语音触发短语',
552
- 'macroTemplate': '展开提示词模板',
553
- 'macroDelete': '删除',
554
- 'rawTab': '原始识别',
555
- 'resultTab': '处理结果',
556
- 'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
557
- 'jargonTitle': '开发者词汇与 IT 术语',
558
- 'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
559
- 'jargonAdd': '添加替换项',
560
- 'jargonFrom': '口语词汇',
561
- 'jargonTo': '标准拼写',
562
- 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
563
- 'voiceInput': '语音输入',
564
- 'assistantSpeakingGated': '助手正在发言(门控模式)',
565
- 'jargonFormatHint': ' (口语 -> 替换文本)',
566
- 'errorPrefix': '错误',
567
- 'wizardTitle': '首次启动与麦克风诊断向导',
568
- 'wizardDesc': '分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。',
569
- 'wizardBtnLaunch': '启动麦克风与 STT 诊断向导',
570
- 'wizardClose': '关闭向导',
571
- 'wizardStep1': '1. 音频与运行环境',
572
- 'wizardStep2': '2. 实时信号测试',
573
- 'wizardStep3': '3. STT 识别回环测试',
574
- 'wizardSecureContextOk': '安全上下文 / HTTPS 已验证',
575
- 'wizardSecureContextBad': '非安全上下文(需 HTTPS 或 localhost)',
576
- 'wizardMediaDevicesOk': '浏览器 mediaDevices API 支持正常',
577
- 'wizardMediaDevicesBad': '浏览器缺少 mediaDevices API 支持',
578
- 'wizardMediaRecorderOk': 'MediaRecorder API 支持正常',
579
- 'wizardMediaRecorderBad': '当前浏览器不支持 MediaRecorder API',
580
- 'wizardMicPermissionOk': '麦克风权限已授予',
581
- 'wizardMicPermissionPrompt': '尚未获得麦克风授权',
582
- 'wizardMicPermissionBad': '麦克风权限被拒绝',
583
- 'wizardRequestMic': '请求麦克风权限',
584
- 'wizardMicLabel': '麦克风输入设备:',
585
- 'wizardStartSignalCheck': '开始音频电平测试',
586
- 'wizardStopSignalCheck': '停止音频测试',
587
- 'wizardSignalOk': '检测到音频信号 (RMS: {level}%)',
588
- 'wizardSignalZero': '未检测到声音信号 — 请对着麦克风说话或选择正确设备',
589
- 'wizardSignalTesting': '正在监听声音信号… 请对着麦克风发声',
590
- 'wizardProviderLabel': '当前语音引擎与模型:',
591
- 'wizardProcessingArea': '处理区域:',
592
- 'wizardAreaBrowser': '浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer': '本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud': '远程云端服务',
593
- 'wizardStartTestRecord': '录制 3 秒测试语句', 'wizardRecordingTest': '正在录制测试音频… 请说话 ({left}秒)',
594
- 'wizardTranscribingTest': '正在请求语音识别…', 'wizardTestResult': 'STT 测试结果:',
595
- 'wizardTestSuccess': 'STT 链路验证成功!耗时:{latency}毫秒',
596
- 'wizardTestEmpty': '录音中未检测到有效语音,请清晰发音后重试。',
597
- 'wizardTestFailed': 'STT 测试失败:{error}',
598
- 'wizardNextStepHint': '建议下一步:{hint}',
599
- 'wizardHintPermission': '点击“请求麦克风权限”并在浏览器弹窗中允许访问。',
600
- 'wizardHintDevice': '检查麦克风硬件连接或切换选择其他输入设备。',
601
- 'wizardHintHttps': '请通过 HTTPS 或 localhost 访问 DeepSeek Harness。',
602
- 'wizardHintProvider': '请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
603
- 'wizardFinishSuccess': '设置与验证已完成',
604
- 'wizardSkip': '跳过向导',
605
- 'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
606
- 'exportDiagnosticsBtn': '导出诊断报告', 'exportingDiagnostics': '正在导出报告…',
607
- 'diagnosticsExportSuccess': '诊断报告已下载', 'recentRecognitionsTitle': '近期识别遥测详情',
608
- 'noRecognitionsYet': '暂无近期识别记录。', 'diagLatencyBreakdown': '耗时分布:',
609
- 'diagVad': 'VAD:{ms}毫秒', 'diagStt': 'STT:{ms}毫秒', 'diagPolish': '后处理:{ms}毫秒',
610
- 'diagTotal': '总耗时:{ms}毫秒', 'diagBytes': '音频大小:{bytes} 字节', 'diagWords': '词数:{count}',
611
- 'diagExportNotice': '导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。',
327
+ 'saveFailed': '部分字段未保存 —',
328
+ 'cardHint': '听写与语音消息:服务商、备用链、本地 whisper',
329
+ 'expand': '展开',
330
+ 'collapse': '收起',
331
+ 'composerUnavailable': '输入框不可用',
332
+ 'keySpace': '空格',
333
+ 'keyUnset': '未设置',
334
+ 'recognitionError': '识别失败',
335
+ 'micUnavailable': '麦克风不可用:需要 HTTPS 或 localhost',
336
+ 'noRecorder': '当前浏览器不支持 MediaRecorder',
337
+ 'browserFailed': '浏览器未能识别:',
338
+ 'nothingHeard': '未识别到任何内容',
339
+ 'dictationBtn': '语音输入',
340
+ 'messageBtn': '语音消息 — 点击或按住',
341
+ 'dictationPill': '听写 — 文本将追加至输入框',
342
+ 'messagePill': '正在录制语音消息',
343
+ 'cancel': '取消',
344
+ 'holdHint': '按住录音 — 松开即发送',
345
+ 'listening': '浏览器正在倾听…',
346
+ 'stop': '停止',
347
+ 'transcribing': '正在转写…',
348
+ 'sendingIn': '发送至智能体倒计时',
349
+ 'secondsShort': ' 秒',
350
+ 'keepPending': '不发送',
351
+ 'hide': '隐藏',
352
+ 'title': '语音',
353
+ 'recordSlot': '语音录制',
354
+ 'browserHint': 'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
355
+ 'browserWebGpu': '浏览器 WebGPU Whisper (离线)',
356
+ 'browserWebGpuHint': '通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
357
+ 'npuProvider': '硬件加速 (SBC NPU / 主机)',
358
+ 'npuProviderHint': 'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。',
359
+ 'npuThreads': '计算线程 (CPU/SBC)',
360
+ 'benchmarkTitle': '硬件基准测试 (速度与延迟)',
361
+ 'runBenchmarkBtn': '运行基准测试', 'benchmarking': '正在运行测试…',
362
+ 'benchmarkHint': '测试当前执行提供者在 1.0 秒音频下的实时率 (RTF) 与推理延迟。',
363
+ 'webgpuStatus': '浏览器 WebGPU 状态',
364
+ 'webgpuAvailable': 'WebGPU 可用 (硬件加速就绪)',
365
+ 'webgpuNotAvailable': '未检测到 WebGPU (将自动回退到服务端)',
366
+ 'openaiHint': 'whisper-1 · 密钥 OPENAI_API_KEY', 'siliconflowHint': 'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
367
+ 'deepinfraHint': 'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint': 'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
368
+ 'mistralHint': 'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint': 'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
369
+ 'localHint': '服务器启动时配置', 'up': '上移', 'down': '下移', 'remove': '移除',
370
+ 'addProvider': '添加服务商',
371
+ 'chainHint': '自上而下为故障转移尝试顺序',
372
+ 'customName': '备用链中使用的名称',
373
+ 'customModel': '模型',
374
+ 'customKeyName': '密钥名称',
375
+ 'customModelHint': '请求的模型(留空使用内置默认值)',
376
+ 'addCustom': '添加自定义服务商',
377
+ 'loadingSettings': '正在加载设置…',
378
+ 'notReady1': 'Harness 尚未同步此插件的设置。如果刚重启,',
379
+ 'notReady2': '设置项将在数秒内自动呈现。',
380
+ 'hotkey': '语音消息快捷键',
381
+ 'pressKey': '请按下按键…',
382
+ 'clearKey': '清除按键',
383
+ 'hotkeyHint1': '按住录音,松开发送给智能体,按 Esc 取消。',
384
+ 'hotkeyHint2': '支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
385
+ 'language': '识别语言',
386
+ 'langAuto': '自动检测',
387
+ 'langRu': '俄语 (ru)',
388
+ 'langEn': '英语 (en)',
389
+ 'langZh': '中文 (zh)',
390
+ 'langUk': '乌克兰语 (uk)',
391
+ 'langDe': '德语 (de)',
392
+ 'dictationHint': '语音按停顿自动切句,文字实时追加至输入框。',
393
+ 'pauseMs': '断句静音停顿时长(毫秒)',
394
+ 'pauseHint': '数值越小切分越频繁、出字越快,但可能切断未说完的词',
395
+ 'speaking': '正在说话…',
396
+ 'silence': '停顿…',
397
+ 'normalizeTranscript': '规范化转写文本',
398
+ 'undo': '撤销上次插入',
399
+ 'undone': '插入已撤销',
400
+ 'nothingToUndo': '没有可撤销的内容',
401
+ 'beep': '启停提示音',
402
+ 'localOnly': '仅使用本地 whisper',
403
+ 'localOnlyHint': '将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。',
404
+ 'sendDelay': '听写延迟追加 (ms)',
405
+ 'sendDelayHint': '追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
406
+ 'mic': '麦克风设备',
407
+ 'micDefault': '系统默认',
408
+ 'vocabulary': '自定义词汇表(每行一个词)',
409
+ 'polish': '模型润色转写文本',
410
+ 'polishHint': '插入前通过模型修正标点符号并消除语气口头禅',
411
+ 'stream': '连续流式听写',
412
+ 'streamHint': '说话期间按定时器切分短句,无需等待长时间停顿',
413
+ 'streamChunkMs': '流式切片时长 (ms)',
414
+ 'vadAdapt': '自适应静音阈值',
415
+ 'vadAdaptHint': '根据语速动态调整停顿静音阈值。0 为固定阈值',
416
+ 'wakeWord': '语音唤醒词',
417
+ 'wakeWordHint': '说话以此短语开头时自动触发录音。留空为关闭',
418
+ 'bargeIn': '语音插话打断 (Barge-in)',
419
+ 'polishSend': '发送前润色整篇草稿',
420
+ 'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
421
+ 'editablePreview': '发送前可编辑预览',
422
+ 'editablePreviewHint': '发送前显示识别文本,支持编辑、润色对比和撤销。',
423
+ 'editPreview': '编辑',
424
+ 'sendNow': '发送',
425
+ 'cancelPreview': '取消',
426
+ 'rawText': '原文',
427
+ 'polishedText': '润色',
428
+ 'sessionMismatch': '当前会话已切换,已拦截发送以防止串话。',
429
+ 'sessionCommands': '会话语音控制指令',
430
+ 'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
431
+ 'polishBaseUrl': '离线润色服务端点',
432
+ 'polishBaseUrlHint': '兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型',
433
+ 'polishModel': '离线润色模型',
434
+ 'polishKeyEnv': '离线润色密钥凭据名',
435
+ 'voiceCommandsLabel': '语音编辑指令("换行"、"段落")',
436
+ 'normalizeTranscriptHint': 'transcribe_audio:口语数字转阿拉伯数字,整理标点符号',
437
+ 'messageTitle': '语音消息',
438
+ 'messageHint': '录制单条完整语音,转写完成后自动发送给智能体。',
439
+ 'undoMs': '撤回窗口时长(毫秒)',
440
+ 'undoHint': '转写完成后多久以内可以随时取消发送',
441
+ 'customTitle': '自定义语音服务商',
442
+ 'customHint': '兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。',
443
+ 'general': '通用设置',
444
+ 'whisperEndpoint': '本地 whisper:端点地址',
445
+ 'whisperEndpointHint': 'whisper.cpp 服务器的 POST /inference 接口',
446
+ 'deepgramEndpoint': 'Deepgram:基础地址',
447
+ 'deepgramEndpointHint': 'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)',
448
+ 'whisperBin': '本地 whisper:可执行文件',
449
+ 'whisperBinHint': '开启自动启动时使用',
450
+ 'whisperModel': '本地 whisper:模型文件',
451
+ 'whisperModelHint': 'ggml 模型文件的绝对路径',
452
+ 'whisperAutostart': '自动启动本地 whisper',
453
+ 'bilingualDeveloperPrompt': '双语开发者提示 (中/英)',
454
+ 'bilingualDeveloperPromptHint': '提示常用技术术语与命令行指令,避免语音模型音译',
455
+ 'calibrateNoise': '校准背景噪声 (2秒)',
456
+ 'calibrateNoiseHint': '测量 2 秒环境背景噪声并自动计算最佳门限',
457
+ 'calibrating': '正在测量噪声…',
458
+ 'calibratedMsg': '门限已设置为: {db} dB (背景噪声: {noise} dB)',
459
+ 'calibratedValue': '已校准',
460
+ 'save': '保存',
461
+ 'saved': '已保存 ✓',
462
+ 'openrouterWarning': 'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板',
463
+ 'noiseSuppression': '硬件级降噪',
464
+ 'noiseSuppressionHint': '启用浏览器硬件降噪、回声消除与自动增益控制',
465
+ 'contextGlossary': '上下文专业词汇注入',
466
+ 'contextGlossaryHint': '自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
467
+ 'providerDashboard': '服务商延迟与健康监控',
468
+ 'avgLatency': '平均延迟',
469
+ 'successRate': '成功率',
470
+ 'fast': '极速',
471
+ 'normal': '正常',
472
+ 'slow': '较慢',
473
+ 'error': '异常',
474
+ 'idle': '无调用记录',
475
+ 'play': '播放',
476
+ 'pause': '暂停',
477
+ 'listenBack': '试听回放',
478
+ 'lastRecording': '最近一条语音记录',
479
+ 'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)',
480
+ 'sensevoiceEndpoint': 'SenseVoice:端点地址',
481
+ 'sensevoiceEndpointHint': 'sherpa-onnx 或兼容服务器的 POST 端点',
482
+ 'sensevoiceBin': 'SenseVoice:可执行文件',
483
+ 'sensevoiceBinHint': '开启自动启动时使用',
484
+ 'sensevoiceModel': 'SenseVoice:模型路径',
485
+ 'sensevoiceModelHint': 'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
486
+ 'sensevoiceAutostart': '自动启动 SenseVoice 服务',
487
+ 'visualizerStyle': '音频动态波形样式',
488
+ 'visualizerStyleHint': '录音胶囊面板内的动态音频波形动画效果',
489
+ 'visLiquidWave': '流体水波 (Liquid Wave)',
490
+ 'visDynamicOrb': '动态脉冲球体 (Dynamic Orb)',
491
+ 'visBars': '经典频条 (Classic Bars)',
492
+ 'visOff': '关闭',
493
+ 'statusTitle': '连接与引擎状态看板',
494
+ 'statusDesc': '当前语音后端与识别服务的实时网络与进程就绪状态。',
495
+ 'badgeHostOnline': '服务端在线 ({ms} ms)',
496
+ 'badgeHostOffline': '服务端不可达',
497
+ 'badgeWhisperActive': 'Whisper 就绪',
498
+ 'badgeWhisperInactive': 'Whisper 未就绪',
499
+ 'badgeSenseVoiceActive': 'SenseVoice 就绪',
500
+ 'badgeSenseVoiceInactive': 'SenseVoice 未就绪',
501
+ 'badgeProviders': '{count} 个服务商已就绪',
502
+ 'refreshStats': '刷新状态',
503
+ 'hardwareTitle': '硬件与本地引擎',
504
+ 'hardwareDesc': '麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。',
505
+ 'uiError': '语音界面错误:',
506
+ 'retry': '重试',
507
+ 'testMic': '测试麦克风',
508
+ 'stopTest': '停止测试',
509
+ 'micLevel': '麦克风电平',
510
+ 'noiseGate': '噪声门 (dB)',
511
+ 'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
512
+ 'noiseGateOff': '关闭',
513
+ 'versionTitle': '插件版本与更新',
514
+ 'versionHint': '检查新版本并直接从 npm 更新。',
515
+ 'updateAvailable': '有可用更新',
516
+ 'upToDate': '已是最新版本',
517
+ 'updatePrompt': 'npm 上有新版本可用。点击更新进行安装。',
518
+ 'updateNow': '立即更新',
519
+ 'updating': '正在更新…',
520
+ 'updateSuccess': '更新成功!请重启 DSH 生效。',
521
+ 'updateFailed': '更新失败,请查看日志。',
522
+ 'updateCheckFailed': '检查更新失败',
523
+ 'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
524
+ 'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
525
+ 'profileLockCheckError': '检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
526
+ 'checkingUpdates': '正在检查更新…',
527
+ 'updateStatusUnknown': '版本状态未知',
528
+ 'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
529
+ 'sensevoiceDesc': 'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。',
530
+ 'installSensevoice': '一键安装 SenseVoice-Small',
531
+ 'installingSensevoice': '正在安装 SenseVoice…',
532
+ 'sensevoiceReady': '已就绪',
533
+ 'sensevoiceNotInstalled': '未安装',
534
+ 'sensevoiceDownloading': '正在下载模型…',
535
+ 'turnTakingTitle': '轮流对话与音频协同',
536
+ 'turnTakingDesc': '协调听与说,避免扬声器播放的声音被麦克风二次拾音。',
537
+ 'gatedTurnTaking': '门控模式 (助手播报时麦克风自动静音)',
538
+ 'bargeInDesc': '实时打断 (说话时立即打断助手朗读)',
539
+ 'smartActionsTitle': '免提与智能指令',
540
+ 'autoSendVisualRing': '环形倒计时动画指示器',
541
+ 'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
542
+ 'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
543
+ 'structuredPromptVoice': '语音应答交互式选择提示',
544
+ 'modeVerbatim': '逐字直录',
545
+ 'modeClean': '清爽文本',
546
+ 'modeTask': '智能体任务',
547
+ 'modeNote': '便签备忘',
548
+ 'dictationModeTitle': '听写后处理模式',
549
+ 'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
550
+ 'dictationPromptsTitle': '听写提示词模板',
551
+ 'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
552
+ 'promptClean': '清爽文本提示词',
553
+ 'promptTask': '智能体任务提示词',
554
+ 'promptNote': '便签提示词',
555
+ 'resetPrompts': '恢复默认模板',
556
+ 'voiceMacrosTitle': '语音动作宏',
557
+ 'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
558
+ 'addMacro': '添加动作宏',
559
+ 'macroTrigger': '语音触发短语',
560
+ 'macroTemplate': '展开提示词模板',
561
+ 'macroDelete': '删除',
562
+ 'rawTab': '原始识别',
563
+ 'resultTab': '处理结果',
564
+ 'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
565
+ 'jargonTitle': '开发者词汇与 IT 术语',
566
+ 'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
567
+ 'jargonAdd': '添加替换项',
568
+ 'jargonFrom': '口语词汇',
569
+ 'jargonTo': '标准拼写',
570
+ 'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
571
+ 'voiceInput': '语音输入',
572
+ 'assistantSpeakingGated': '助手正在发言(门控模式)',
573
+ 'jargonFormatHint': ' (口语 -> 替换文本)',
574
+ 'errorPrefix': '错误',
575
+ 'wizardTitle': '首次启动与麦克风诊断向导',
576
+ 'wizardDesc': '分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。',
577
+ 'wizardBtnLaunch': '启动麦克风与 STT 诊断向导',
578
+ 'wizardClose': '关闭向导',
579
+ 'wizardStep1': '1. 音频与运行环境',
580
+ 'wizardStep2': '2. 实时信号测试',
581
+ 'wizardStep3': '3. STT 识别回环测试',
582
+ 'wizardSecureContextOk': '安全上下文 / HTTPS 已验证',
583
+ 'wizardSecureContextBad': '非安全上下文(需 HTTPS 或 localhost)',
584
+ 'wizardMediaDevicesOk': '浏览器 mediaDevices API 支持正常',
585
+ 'wizardMediaDevicesBad': '浏览器缺少 mediaDevices API 支持',
586
+ 'wizardMediaRecorderOk': 'MediaRecorder API 支持正常', 'wizardMediaRecorderBad': '当前浏览器不支持 MediaRecorder API',
587
+ 'wizardMicPermissionOk': '麦克风权限已授予', 'wizardMicPermissionPrompt': '尚未获得麦克风授权', 'wizardMicPermissionBad': '麦克风权限被拒绝',
588
+ 'wizardRequestMic': '请求麦克风权限',
589
+ 'wizardMicLabel': '麦克风输入设备:',
590
+ 'wizardStartSignalCheck': '开始音频电平测试',
591
+ 'wizardStopSignalCheck': '停止音频测试',
592
+ 'wizardSignalOk': '检测到音频信号 (RMS: {level}%)',
593
+ 'wizardSignalZero': '未检测到声音信号 — 请对着麦克风说话或选择正确设备',
594
+ 'wizardSignalTesting': '正在监听声音信号… 请对着麦克风发声',
595
+ 'wizardProviderLabel': '当前语音引擎与模型:',
596
+ 'wizardProcessingArea': '处理区域:', 'wizardAreaBrowser': '浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer': '本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud': '远程云端服务',
597
+ 'wizardStartTestRecord': '录制 3 秒测试语句', 'wizardRecordingTest': '正在录制测试音频… 请说话 ({left}秒)',
598
+ 'wizardTranscribingTest': '正在请求语音识别…', 'wizardTestResult': 'STT 测试结果:',
599
+ 'wizardTestSuccess': 'STT 链路验证成功!耗时:{latency}毫秒',
600
+ 'wizardTestEmpty': '录音中未检测到有效语音,请清晰发音后重试。',
601
+ 'wizardTestFailed': 'STT 测试失败:{error}',
602
+ 'wizardNextStepHint': '建议下一步:{hint}',
603
+ 'wizardHintPermission': '点击“请求麦克风权限”并在浏览器弹窗中允许访问。', 'wizardHintDevice': '检查麦克风硬件连接或切换选择其他输入设备。', 'wizardHintHttps': '请通过 HTTPS 或 localhost 访问 DeepSeek Harness。', 'wizardHintProvider': '请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
604
+ 'wizardFinishSuccess': '设置与验证已完成', 'wizardSkip': '跳过向导',
605
+ 'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
606
+ 'exportDiagnosticsBtn': '导出诊断报告', 'exportingDiagnostics': '正在导出报告…',
607
+ 'diagnosticsExportSuccess': '诊断报告已下载', 'recentRecognitionsTitle': '近期识别遥测详情',
608
+ 'noRecognitionsYet': '暂无近期识别记录。', 'diagLatencyBreakdown': '耗时分布:',
609
+ 'diagVad': 'VAD:{ms}毫秒', 'diagStt': 'STT:{ms}毫秒', 'diagPolish': '后处理:{ms}毫秒', 'diagTotal': '总耗时:{ms}毫秒', 'diagBytes': '音频大小:{bytes} 字节', 'diagWords': '词数:{count}',
610
+ 'diagExportNotice': '导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。',
612
611
  }
613
612
 
614
613
  // Strings are also needed outside components — in recording handlers and
@@ -617,65 +616,9 @@ window.__ModuleLoader__.load({
617
616
  const t = (key) => moduleT(key)
618
617
  // ------------------------------------------------------------------ css
619
618
  const CSS =
620
- '.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}' +
621
- '.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}' +
622
- '.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}' +
623
- '.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}' +
624
- '.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}' +
625
- '.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}' +
626
- '.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
627
- '.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
628
- '.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}' +
629
- '.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
630
- '.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
631
- '.dvo-spin{animation:dvo-spin 1s linear infinite}' +
632
- '@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
633
- '.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}' +
634
- '.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)}' +
635
- '.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}' +
636
- '.dvo-audio-play:hover{opacity:0.9}' +
637
- '.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}' +
638
- '.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}' +
639
- '.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}' +
640
- '.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}' +
641
- '.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
642
- '.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
643
- '.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}' +
644
- '.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)}' +
645
- '.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)}' +
646
- '.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)}' +
647
- '.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)}' +
648
- '.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}' +
649
- '.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)}' +
650
- '.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}' +
651
- '.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}' +
652
- '.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}' +
653
- '.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}' +
654
- '.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}' +
655
- '.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}' +
656
- '.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}' +
657
- '.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}' +
658
- '.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
659
- '.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
660
- '.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
661
- '.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}' +
662
- '.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
663
- '.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}' +
664
- '.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
665
- '.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)}' +
666
- '.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}' +
667
- '.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
668
- '.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)}' +
669
- '.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
670
- '.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}' +
671
- '.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%}' +
672
- '.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
673
- '.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}' +
674
- '.dvo-btn-send:hover{opacity:0.9}' +
675
- '.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)}'
619
+ '.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)}'
676
620
  const cssId = 'dsh-voice/client.module.css'
677
- if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
678
- const tag = document.createElement('style')
621
+ if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {const tag = document.createElement('style')
679
622
  tag.textContent = CSS
680
623
  tag.setAttribute('data-dsh-plugin', 'dsh-voice')
681
624
  tag.dataset.pluginCss = cssId
@@ -854,7 +797,6 @@ window.__ModuleLoader__.load({
854
797
  } catch { /* best effort */ }
855
798
  }
856
799
 
857
-
858
800
  // ------------------------------------------------------------ transport
859
801
  function blobToBase64(blob) {
860
802
  return new Promise((resolve, reject) => {
@@ -3389,6 +3331,9 @@ function DictationModesSection(props) {
3389
3331
  const testLevel = props.testLevel || 0
3390
3332
  const gateDbVal = props.gateDbVal
3391
3333
  const gateThresholdPercent = props.gateThresholdPercent
3334
+ const calibrating = props.calibrating
3335
+ const calibrateMsg = props.calibrateMsg
3336
+ const calibrateNoise = props.calibrateNoise
3392
3337
  const onClearKey = props.onClearKey
3393
3338
 
3394
3339
  const [benchmarking, setBenchmarking] = React.useState(false)
@@ -3396,258 +3341,277 @@ function DictationModesSection(props) {
3396
3341
  const [benchmarkErr, setBenchmarkErr] = React.useState('')
3397
3342
 
3398
3343
  const runBenchmark = () => {
3399
- setBenchmarking(true)
3400
- setBenchmarkErr('')
3401
- fetch('/dsh-voice/benchmark', {
3402
- method: 'POST',
3403
- headers: {
3404
- 'x-dsh-plugin-update': '1',
3405
- 'Content-Type': 'application/json',
3406
- },
3407
- })
3408
- .then((r) => r.json())
3409
- .then((d) => {
3410
- setBenchmarking(false)
3411
- if (d && d.ok) setBenchmarkResult(d)
3412
- else setBenchmarkErr((d && d.error && d.error.message) || 'Benchmark failed')
3413
- })
3414
- .catch((e) => {
3415
- setBenchmarking(false)
3416
- setBenchmarkErr((e && e.message) || 'Network error')
3417
- })
3344
+ setBenchmarking(true)
3345
+ setBenchmarkErr('')
3346
+ fetch('/dsh-voice/benchmark', {
3347
+ method: 'POST',
3348
+ headers: {
3349
+ 'x-dsh-plugin-update': '1',
3350
+ 'Content-Type': 'application/json',
3351
+ },
3352
+ })
3353
+ .then((r) => r.json())
3354
+ .then((d) => {
3355
+ setBenchmarking(false)
3356
+ if (d && d.ok) setBenchmarkResult(d)
3357
+ else setBenchmarkErr((d && d.error && d.error.message) || 'Benchmark failed')
3358
+ })
3359
+ .catch((e) => {
3360
+ setBenchmarking(false)
3361
+ setBenchmarkErr((e && e.message) || 'Network error')
3362
+ })
3418
3363
  }
3419
3364
 
3420
3365
  const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
3421
- React.createElement('label', null, label),
3422
- React.createElement('input', {
3423
- type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
3424
- onChange: (e) => setTop(key, e.target.value),
3425
- }),
3426
- React.createElement('span', { className: 'dvs-sub' }, hint),
3366
+ React.createElement('label', null, label),
3367
+ React.createElement('input', {
3368
+ type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
3369
+ onChange: (e) => setTop(key, e.target.value),
3370
+ }),
3371
+ React.createElement('span', { className: 'dvs-sub' }, hint),
3427
3372
  )
3428
3373
 
3429
3374
  return React.createElement('div', { className: 'cb-section-card' },
3430
- React.createElement('div', { className: 'cb-section-title' },
3431
- React.createElement('span', null, '⚙️ ' + t('hardwareTitle')),
3432
- ),
3433
- React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
3434
- // Hotkey picker
3435
- React.createElement('div', { className: 'cb-field' },
3436
- React.createElement('label', null, t('hotkey')),
3437
- React.createElement('div', { className: 'cb-row' },
3438
- React.createElement('button', {
3439
- type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
3440
- onClick: () => setCatching(true),
3441
- }, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
3442
- React.createElement('button', {
3443
- type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
3444
- disabled: !writable, onClick: onClearKey,
3445
- }, '×'),
3446
- ),
3447
- React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
3448
- ),
3449
- // Mic & noise gate
3450
- React.createElement('div', { className: 'cb-field' },
3451
- React.createElement('label', null, t('mic')),
3452
- React.createElement('div', { className: 'cb-row' },
3453
- React.createElement('select', {
3454
- value: String((draft && draft.micDeviceId) ?? ''), disabled: !writable || testingMic,
3455
- onChange: (e) => setTop('micDeviceId', e.target.value),
3456
- style: { flex: 1 },
3457
- },
3458
- React.createElement('option', { value: '' }, t('micDefault')),
3459
- devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
3460
- d.label || d.deviceId.slice(0, 12))),
3461
- ),
3462
- React.createElement('button', {
3463
- type: 'button',
3464
- className: 'cb-btn ' + (testingMic ? 'cb-btn-primary' : 'cb-btn-secondary'),
3465
- onClick: toggleTestMic,
3466
- style: { flex: 'none' },
3467
- }, testingMic ? t('stopTest') : t('testMic')),
3468
- ),
3469
- testingMic ? React.createElement('div', { className: 'dvo-mic-test' },
3470
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
3471
- React.createElement('div', { className: 'dvo-meter-bar' },
3472
- React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
3473
- gateThresholdPercent > 0 ? React.createElement('div', { className: 'dvo-meter-gate', style: { left: `${gateThresholdPercent}%` }, title: `${gateDbVal} dB` }) : null,
3474
- ),
3475
- React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
3476
- ) : null,
3477
- React.createElement('div', { className: 'cb-row', style: { marginTop: '6px' } },
3478
- React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', minWidth: '120px' } }, t('noiseGate') + ':'),
3479
- React.createElement('select', {
3480
- value: String(gateDbVal), disabled: !writable,
3481
- onChange: (e) => setTop('noiseGateDb', Number(e.target.value)),
3482
- style: { flex: 1 },
3483
- },
3484
- React.createElement('option', { value: '-999' }, t('noiseGateOff')),
3485
- React.createElement('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
3486
- React.createElement('option', { value: '-45' }, '-45 dB (Standard Default)'),
3487
- React.createElement('option', { value: '-35' }, '-35 dB (Medium Room)'),
3488
- React.createElement('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
3489
- ),
3490
- ),
3491
- React.createElement('span', { className: 'dvs-sub' }, t('noiseGateHint')),
3492
- ),
3493
- // Endpoint text fields
3494
- React.createElement('div', { className: 'cb-grid-2' },
3495
- textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
3496
- textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
3497
- textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
3498
- textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
3499
- textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
3500
- textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
3501
- textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
3502
- textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
3503
- textField('polishModel', t('polishModel'), ''),
3504
- textField('polishKeyEnv', t('polishKeyEnv'), ''),
3505
- ),
3506
- // Engine checkboxes
3507
- React.createElement('div', { className: 'cb-row' },
3508
- React.createElement('label', { className: 'cb-check-label' },
3509
- React.createElement('input', {
3510
- type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
3511
- onChange: (e) => setTop('autoStart', e.target.checked),
3512
- }),
3513
- t('whisperAutostart'),
3514
- ),
3515
- React.createElement('label', { className: 'cb-check-label' },
3516
- React.createElement('input', {
3517
- type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
3518
- onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
3519
- }),
3520
- t('sensevoiceAutostart'),
3521
- ),
3522
- React.createElement('label', { className: 'cb-check-label' },
3523
- React.createElement('input', {
3524
- type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
3525
- onChange: (e) => setTop('localOnly', e.target.checked),
3526
- }),
3527
- t('localOnly'),
3528
- ),
3529
- React.createElement('label', { className: 'cb-check-label' },
3530
- React.createElement('input', {
3531
- type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
3532
- onChange: (e) => setTop('beep', e.target.checked),
3533
- }),
3534
- t('beep'),
3535
- ),
3536
- React.createElement('label', { className: 'cb-check-label' },
3537
- React.createElement('input', {
3538
- type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
3539
- onChange: (e) => setTop('noiseSuppression', e.target.checked),
3540
- }),
3541
- t('noiseSuppression'),
3542
- ),
3543
- React.createElement('label', { className: 'cb-check-label' },
3544
- React.createElement('input', {
3545
- type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
3546
- onChange: (e) => setTop('contextGlossary', e.target.checked),
3547
- }),
3548
- t('contextGlossary'),
3549
- ),
3550
- React.createElement('label', { className: 'cb-check-label' },
3551
- React.createElement('input', {
3552
- type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
3553
- onChange: (e) => setTop('voiceCommands', e.target.checked),
3554
- }),
3555
- t('voiceCommandsLabel'),
3556
- ),
3557
- React.createElement('label', { className: 'cb-check-label' },
3558
- React.createElement('input', {
3559
- type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
3560
- onChange: (e) => setTop('normalizeTranscript', e.target.checked),
3561
- }),
3562
- t('normalizeTranscript'),
3563
- ),
3564
- ),
3565
- // Visualizer select
3566
- React.createElement('div', { className: 'cb-field' },
3567
- React.createElement('label', null, t('visualizerStyle')),
3568
- React.createElement('select', {
3569
- value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
3570
- onChange: (e) => setTop('visualizerStyle', e.target.value),
3571
- },
3572
- React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
3573
- React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
3574
- React.createElement('option', { value: 'bars' }, t('visBars')),
3575
- React.createElement('option', { value: 'off' }, t('visOff')),
3576
- ),
3577
- ),
3578
- // Hardware Acceleration (SBC NPU / Host)
3579
- React.createElement('div', { className: 'cb-field' },
3580
- React.createElement('label', null, t('npuProvider')),
3581
- React.createElement('div', { className: 'cb-row' },
3582
- React.createElement('select', {
3583
- value: String((draft && draft.sensevoiceProvider) || 'cpu'),
3584
- disabled: !writable,
3585
- onChange: (e) => setTop('sensevoiceProvider', e.target.value),
3586
- style: { flex: 1 },
3587
- },
3588
- React.createElement('option', { value: 'cpu' }, 'CPU (Standard)'),
3589
- React.createElement('option', { value: 'rknn' }, 'Rockchip RK3588 NPU (RKNN)'),
3590
- React.createElement('option', { value: 'coreml' }, 'Apple Silicon CoreML'),
3591
- React.createElement('option', { value: 'openvino' }, 'Intel OpenVINO'),
3592
- React.createElement('option', { value: 'cuda' }, 'NVIDIA CUDA'),
3593
- ),
3594
- React.createElement('input', {
3595
- type: 'number',
3596
- min: 1,
3597
- max: 32,
3598
- value: Number((draft && draft.sensevoiceThreads) || 4),
3599
- disabled: !writable,
3600
- title: t('npuThreads'),
3601
- style: { width: '80px' },
3602
- onChange: (e) => setTop('sensevoiceThreads', Math.max(1, Math.min(32, Number(e.target.value) || 4))),
3603
- }),
3604
- ),
3605
- React.createElement('span', { className: 'dvs-sub' }, t('npuProviderHint')),
3606
- ),
3607
- // Hardware Benchmark
3608
- React.createElement('div', { className: 'cb-field' },
3609
- React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between' } },
3610
- React.createElement('label', { style: { marginBottom: 0 } }, t('benchmarkTitle')),
3611
- React.createElement('button', {
3612
- type: 'button', className: 'cb-btn cb-btn-secondary', style: { fontSize: '12px', padding: '4px 10px' },
3613
- disabled: benchmarking || !writable, onClick: runBenchmark,
3614
- }, benchmarking ? ('⏳ ' + t('benchmarking')) : ('⚡ ' + t('runBenchmarkBtn'))),
3615
- ),
3616
- benchmarkResult
3617
- ? React.createElement('div', {
3618
- style: { marginTop: '8px', padding: '6px 10px', borderRadius: '6px', fontSize: '12px', backgroundColor: 'var(--dsw-alias-fill-quaternary)' }
3619
- }, benchmarkResult.running
3620
- ? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs}ms (${benchmarkResult.rating}) · ${String(benchmarkResult.provider || 'cpu').toUpperCase()} · Cores: ${benchmarkResult.cpuCores}`
3621
- : `ℹ ${benchmarkResult.message || 'Offline'} (${benchmarkResult.cpuCores} cores, ${benchmarkResult.platform})`)
3622
- : (benchmarkErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-danger)', fontSize: '12px', marginTop: '4px' } }, benchmarkErr) : null),
3623
- React.createElement('span', { className: 'dvs-sub' }, t('benchmarkHint')),
3624
- ),
3625
- // WebGPU Status Indicator
3626
- React.createElement('div', { className: 'cb-field' },
3627
- React.createElement('label', null, t('webgpuStatus')),
3628
- React.createElement('div', {
3629
- style: {
3630
- padding: '6px 10px',
3631
- borderRadius: '6px',
3632
- fontSize: '12px',
3633
- backgroundColor: 'var(--dsw-alias-fill-quaternary)',
3634
- color: (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
3635
- ? 'var(--dsw-alias-state-success)'
3636
- : 'var(--dsw-alias-label-secondary)'
3637
- }
3638
- }, (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
3639
- ? ('✓ ' + t('webgpuAvailable'))
3640
- : ('ℹ ' + t('webgpuNotAvailable'))),
3641
- ),
3642
- // Vocabulary textarea
3643
- React.createElement('div', { className: 'cb-field' },
3644
- React.createElement('label', null, t('vocabulary')),
3645
- React.createElement('textarea', {
3646
- rows: 3, disabled: !writable,
3647
- value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
3648
- onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
3649
- }),
3650
- ),
3375
+ React.createElement('div', { className: 'cb-section-title' },
3376
+ React.createElement('span', null, '⚙️ ' + t('hardwareTitle')),
3377
+ ),
3378
+ React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
3379
+ // Hotkey picker
3380
+ React.createElement('div', { className: 'cb-field' },
3381
+ React.createElement('label', null, t('hotkey')),
3382
+ React.createElement('div', { className: 'cb-row' },
3383
+ React.createElement('button', {
3384
+ type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
3385
+ onClick: () => setCatching(true),
3386
+ }, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
3387
+ React.createElement('button', {
3388
+ type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
3389
+ disabled: !writable, onClick: onClearKey,
3390
+ }, '×'),
3391
+ ),
3392
+ React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
3393
+ ),
3394
+ // Mic & noise gate
3395
+ React.createElement('div', { className: 'cb-field' },
3396
+ React.createElement('label', null, t('mic')),
3397
+ React.createElement('div', { className: 'cb-row' },
3398
+ React.createElement('select', {
3399
+ value: String((draft && draft.micDeviceId) ?? ''), disabled: !writable || testingMic,
3400
+ onChange: (e) => setTop('micDeviceId', e.target.value),
3401
+ style: { flex: 1 },
3402
+ },
3403
+ React.createElement('option', { value: '' }, t('micDefault')),
3404
+ devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
3405
+ d.label || d.deviceId.slice(0, 12))),
3406
+ ),
3407
+ React.createElement('button', {
3408
+ type: 'button',
3409
+ className: 'cb-btn ' + (testingMic ? 'cb-btn-primary' : 'cb-btn-secondary'),
3410
+ onClick: toggleTestMic,
3411
+ style: { flex: 'none' },
3412
+ }, testingMic ? t('stopTest') : t('testMic')),
3413
+ React.createElement('button', {
3414
+ type: 'button',
3415
+ className: 'cb-btn cb-btn-secondary',
3416
+ onClick: () => (typeof calibrateNoise === 'function' ? calibrateNoise(setTop, t) : null),
3417
+ disabled: calibrating || !writable,
3418
+ style: { flex: 'none' },
3419
+ title: t('calibrateNoiseHint') || '',
3420
+ }, calibrating ? t('calibrating') : t('calibrateNoise')),
3421
+ ),
3422
+ calibrateMsg ? React.createElement('div', {
3423
+ style: { fontSize: '11px', color: 'var(--dsw-alias-state-success-primary)', marginTop: '4px' },
3424
+ }, calibrateMsg) : null,
3425
+ testingMic ? React.createElement('div', { className: 'dvo-mic-test' },
3426
+ React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
3427
+ React.createElement('div', { className: 'dvo-meter-bar' },
3428
+ React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
3429
+ gateThresholdPercent > 0 ? React.createElement('div', { className: 'dvo-meter-gate', style: { left: `${gateThresholdPercent}%` }, title: `${gateDbVal} dB` }) : null,
3430
+ ),
3431
+ React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
3432
+ ) : null,
3433
+ React.createElement('div', { className: 'cb-row', style: { marginTop: '6px' } },
3434
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)', minWidth: '120px' } }, t('noiseGate') + ':'),
3435
+ React.createElement('select', {
3436
+ value: String(gateDbVal), disabled: !writable,
3437
+ onChange: (e) => setTop('noiseGateDb', Number(e.target.value)),
3438
+ style: { flex: 1 },
3439
+ },
3440
+ React.createElement('option', { value: '-999' }, t('noiseGateOff')),
3441
+ React.createElement('option', { value: '-50' }, '-50 dB (Low / Gentle)'),
3442
+ React.createElement('option', { value: '-45' }, '-45 dB (Standard Default)'),
3443
+ React.createElement('option', { value: '-35' }, '-35 dB (Medium Room)'),
3444
+ React.createElement('option', { value: '-25' }, '-25 dB (High / Mechanical Clicks)'),
3445
+ !['-999', '-50', '-45', '-35', '-25'].includes(String(gateDbVal)) ? React.createElement('option', { value: String(gateDbVal) }, `${gateDbVal} dB (${t('calibratedValue') || 'Calibrated'})`) : null,
3446
+ ),
3447
+ ),
3448
+ React.createElement('span', { className: 'dvs-sub' }, t('noiseGateHint')),
3449
+ ),
3450
+ // Endpoint text fields
3451
+ React.createElement('div', { className: 'cb-grid-2' },
3452
+ textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
3453
+ textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
3454
+ textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
3455
+ textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
3456
+ textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
3457
+ textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
3458
+ textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
3459
+ textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
3460
+ textField('polishModel', t('polishModel'), ''),
3461
+ textField('polishKeyEnv', t('polishKeyEnv'), ''),
3462
+ ),
3463
+ // Engine checkboxes
3464
+ React.createElement('div', { className: 'cb-row' },
3465
+ React.createElement('label', { className: 'cb-check-label' },
3466
+ React.createElement('input', {
3467
+ type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
3468
+ onChange: (e) => setTop('autoStart', e.target.checked),
3469
+ }),
3470
+ t('whisperAutostart'),
3471
+ ),
3472
+ React.createElement('label', { className: 'cb-check-label' },
3473
+ React.createElement('input', {
3474
+ type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
3475
+ onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
3476
+ }),
3477
+ t('sensevoiceAutostart'),
3478
+ ),
3479
+ React.createElement('label', { className: 'cb-check-label' },
3480
+ React.createElement('input', {
3481
+ type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
3482
+ onChange: (e) => setTop('localOnly', e.target.checked),
3483
+ }),
3484
+ t('localOnly'),
3485
+ ),
3486
+ React.createElement('label', { className: 'cb-check-label' },
3487
+ React.createElement('input', {
3488
+ type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
3489
+ onChange: (e) => setTop('beep', e.target.checked),
3490
+ }),
3491
+ t('beep'),
3492
+ ),
3493
+ React.createElement('label', { className: 'cb-check-label' },
3494
+ React.createElement('input', {
3495
+ type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
3496
+ onChange: (e) => setTop('noiseSuppression', e.target.checked),
3497
+ }),
3498
+ t('noiseSuppression'),
3499
+ ),
3500
+ React.createElement('label', { className: 'cb-check-label' },
3501
+ React.createElement('input', {
3502
+ type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
3503
+ onChange: (e) => setTop('contextGlossary', e.target.checked),
3504
+ }),
3505
+ t('contextGlossary'),
3506
+ ),
3507
+ React.createElement('label', { className: 'cb-check-label', title: t('bilingualDeveloperPromptHint') || '' },
3508
+ React.createElement('input', {
3509
+ type: 'checkbox', checked: draft ? draft.bilingualDeveloperPrompt !== false : true, disabled: !writable,
3510
+ onChange: (e) => setTop('bilingualDeveloperPrompt', e.target.checked),
3511
+ }),
3512
+ t('bilingualDeveloperPrompt'),
3513
+ ),
3514
+ React.createElement('label', { className: 'cb-check-label' },
3515
+ React.createElement('input', {
3516
+ type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
3517
+ onChange: (e) => setTop('voiceCommands', e.target.checked),
3518
+ }),
3519
+ t('voiceCommandsLabel'),
3520
+ ),
3521
+ React.createElement('label', { className: 'cb-check-label' },
3522
+ React.createElement('input', {
3523
+ type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
3524
+ onChange: (e) => setTop('normalizeTranscript', e.target.checked),
3525
+ }),
3526
+ t('normalizeTranscript'),
3527
+ ),
3528
+ ),
3529
+ // Visualizer select
3530
+ React.createElement('div', { className: 'cb-field' },
3531
+ React.createElement('label', null, t('visualizerStyle')),
3532
+ React.createElement('select', {
3533
+ value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
3534
+ onChange: (e) => setTop('visualizerStyle', e.target.value),
3535
+ },
3536
+ React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
3537
+ React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
3538
+ React.createElement('option', { value: 'bars' }, t('visBars')),
3539
+ React.createElement('option', { value: 'off' }, t('visOff')),
3540
+ ),
3541
+ ),
3542
+ // Hardware Acceleration (SBC NPU / Host)
3543
+ React.createElement('div', { className: 'cb-field' },
3544
+ React.createElement('label', null, t('npuProvider')),
3545
+ React.createElement('div', { className: 'cb-row' },
3546
+ React.createElement('select', {
3547
+ value: String((draft && draft.sensevoiceProvider) || 'cpu'),
3548
+ disabled: !writable,
3549
+ onChange: (e) => setTop('sensevoiceProvider', e.target.value),
3550
+ style: { flex: 1 },
3551
+ },
3552
+ React.createElement('option', { value: 'cpu' }, 'CPU (Standard)'),
3553
+ React.createElement('option', { value: 'rknn' }, 'Rockchip RK3588 NPU (RKNN)'),
3554
+ React.createElement('option', { value: 'coreml' }, 'Apple Silicon CoreML'),
3555
+ React.createElement('option', { value: 'openvino' }, 'Intel OpenVINO'),
3556
+ React.createElement('option', { value: 'cuda' }, 'NVIDIA CUDA'),
3557
+ ),
3558
+ React.createElement('input', {
3559
+ type: 'number',
3560
+ min: 1,
3561
+ max: 32,
3562
+ value: Number((draft && draft.sensevoiceThreads) || 4),
3563
+ disabled: !writable,
3564
+ title: t('npuThreads'),
3565
+ style: { width: '80px' },
3566
+ onChange: (e) => setTop('sensevoiceThreads', Math.max(1, Math.min(32, Number(e.target.value) || 4))),
3567
+ }),
3568
+ ),
3569
+ React.createElement('span', { className: 'dvs-sub' }, t('npuProviderHint')),
3570
+ ),
3571
+ // Hardware Benchmark
3572
+ React.createElement('div', { className: 'cb-field' },
3573
+ React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between' } },
3574
+ React.createElement('label', { style: { marginBottom: 0 } }, t('benchmarkTitle')),
3575
+ React.createElement('button', {
3576
+ type: 'button', className: 'cb-btn cb-btn-secondary', style: { fontSize: '12px', padding: '4px 10px' },
3577
+ disabled: benchmarking || !writable, onClick: runBenchmark,
3578
+ }, benchmarking ? ('⏳ ' + t('benchmarking')) : ('⚡ ' + t('runBenchmarkBtn'))),
3579
+ ),
3580
+ benchmarkResult
3581
+ ? React.createElement('div', {
3582
+ style: { marginTop: '8px', padding: '6px 10px', borderRadius: '6px', fontSize: '12px', backgroundColor: 'var(--dsw-alias-fill-quaternary)' }
3583
+ }, benchmarkResult.running
3584
+ ? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs}ms (${benchmarkResult.rating}) · ${String(benchmarkResult.provider || 'cpu').toUpperCase()} · Cores: ${benchmarkResult.cpuCores}`
3585
+ : `ℹ ${benchmarkResult.message || 'Offline'} (${benchmarkResult.cpuCores} cores, ${benchmarkResult.platform})`)
3586
+ : (benchmarkErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-danger)', fontSize: '12px', marginTop: '4px' } }, benchmarkErr) : null),
3587
+ React.createElement('span', { className: 'dvs-sub' }, t('benchmarkHint')),
3588
+ ),
3589
+ // WebGPU Status Indicator
3590
+ React.createElement('div', { className: 'cb-field' },
3591
+ React.createElement('label', null, t('webgpuStatus')),
3592
+ React.createElement('div', {
3593
+ style: {
3594
+ padding: '6px 10px',
3595
+ borderRadius: '6px',
3596
+ fontSize: '12px',
3597
+ backgroundColor: 'var(--dsw-alias-fill-quaternary)',
3598
+ color: (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
3599
+ ? 'var(--dsw-alias-state-success)'
3600
+ : 'var(--dsw-alias-label-secondary)'
3601
+ }
3602
+ }, (typeof isWebGpuSupported === 'function' && isWebGpuSupported())
3603
+ ? ('✓ ' + t('webgpuAvailable'))
3604
+ : ('ℹ ' + t('webgpuNotAvailable'))),
3605
+ ),
3606
+ // Vocabulary textarea
3607
+ React.createElement('div', { className: 'cb-field' },
3608
+ React.createElement('label', null, t('vocabulary')),
3609
+ React.createElement('textarea', {
3610
+ rows: 3, disabled: !writable,
3611
+ value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
3612
+ onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
3613
+ }),
3614
+ ),
3651
3615
  )
3652
3616
  }
3653
3617
  // Custom hooks for VoiceSection modularity
@@ -3704,8 +3668,75 @@ function DictationModesSection(props) {
3704
3668
  function useMicTester(draft, value, setErr) {
3705
3669
  const [testingMic, setTestingMic] = React.useState(false)
3706
3670
  const [testLevel, setTestLevel] = React.useState(0)
3671
+ const [calibrating, setCalibrating] = React.useState(false)
3672
+ const [calibrateMsg, setCalibrateMsg] = React.useState('')
3707
3673
  const testRef = React.useRef(null)
3708
3674
 
3675
+ const calibrateNoise = async (setTop, translate) => {
3676
+ if (calibrating) return
3677
+ setCalibrating(true)
3678
+ setCalibrateMsg('')
3679
+ let stream = null
3680
+ let audioCtx = null
3681
+ try {
3682
+ const devId = (draft && draft.micDeviceId) || (value && value.micDeviceId) || ''
3683
+ const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
3684
+ if (devId) audio.deviceId = { exact: devId }
3685
+ stream = await navigator.mediaDevices.getUserMedia({ audio })
3686
+ const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
3687
+ if (!AC) throw new Error('AudioContext not supported')
3688
+ audioCtx = new AC()
3689
+ if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
3690
+ const src = audioCtx.createMediaStreamSource(stream)
3691
+ const analyser = audioCtx.createAnalyser()
3692
+ analyser.fftSize = 512
3693
+ src.connect(analyser)
3694
+
3695
+ const samples = []
3696
+ const buf = new Float32Array(analyser.fftSize)
3697
+ const startTime = Date.now()
3698
+
3699
+ await new Promise((resolve) => {
3700
+ const interval = setInterval(() => {
3701
+ if (Date.now() - startTime >= 2000) {
3702
+ clearInterval(interval)
3703
+ resolve()
3704
+ return
3705
+ }
3706
+ analyser.getFloatTimeDomainData(buf)
3707
+ let sum = 0
3708
+ for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
3709
+ const rms = Math.sqrt(sum / buf.length)
3710
+ if (rms > 0.00001) {
3711
+ samples.push(20 * Math.log10(rms))
3712
+ }
3713
+ }, 50)
3714
+ })
3715
+
3716
+ const avgDb = samples.length > 0 ? (samples.reduce((a, b) => a + b, 0) / samples.length) : -50
3717
+ const roundedAvg = Math.round(avgDb)
3718
+ const recommended = Math.min(-25, Math.max(-55, roundedAvg + 5))
3719
+ if (typeof setTop === 'function') setTop('noiseGateDb', recommended)
3720
+
3721
+ const tFn = typeof translate === 'function' ? translate : (k) => k
3722
+ const msg = (tFn('calibratedMsg') || 'Gate set: {db} dB (ambient: {noise} dB)')
3723
+ .replace('{db}', String(recommended))
3724
+ .replace('{noise}', String(roundedAvg))
3725
+ setCalibrateMsg(msg)
3726
+ setTimeout(() => setCalibrateMsg(''), 5000)
3727
+ } catch (e) {
3728
+ if (typeof setErr === 'function') setErr(String(e && e.message ? e.message : e))
3729
+ } finally {
3730
+ if (stream) {
3731
+ try { stream.getTracks().forEach((t) => t.stop()) } catch (e) {}
3732
+ }
3733
+ if (audioCtx) {
3734
+ try { audioCtx.close() } catch (e) {}
3735
+ }
3736
+ setCalibrating(false)
3737
+ }
3738
+ }
3739
+
3709
3740
  const toggleTestMic = async () => {
3710
3741
  if (testingMic) {
3711
3742
  if (testRef.current) {
@@ -3773,7 +3804,7 @@ function DictationModesSection(props) {
3773
3804
  const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
3774
3805
  const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
3775
3806
 
3776
- return { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent }
3807
+ return { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent, calibrating, calibrateMsg, calibrateNoise }
3777
3808
  }
3778
3809
 
3779
3810
  function usePluginUpdater() {
@@ -4271,7 +4302,7 @@ function MacrosSection(props) {
4271
4302
  sessionCommands: !!(val.message && val.message.sessionCommands),
4272
4303
  polishBaseUrl: String(val.polishBaseUrl || ''),
4273
4304
  noiseSuppression: val.noiseSuppression !== false,
4274
- noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45),
4305
+ noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45), bilingualDeveloperPrompt: val.bilingualDeveloperPrompt !== false,
4275
4306
  contextGlossary: val.contextGlossary !== false,
4276
4307
  visualizerStyle: val.visualizerStyle || 'liquid-wave',
4277
4308
  gatedTurnTaking: val.gatedTurnTaking !== false,
@@ -4314,7 +4345,7 @@ function MacrosSection(props) {
4314
4345
  fetchStatus()
4315
4346
  }, [fetchStatus])
4316
4347
  const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
4317
- const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
4348
+ const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent, calibrating, calibrateMsg, calibrateNoise } = useMicTester(draft, value, setErr)
4318
4349
  const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
4319
4350
  const save = async () => {
4320
4351
  setErr(''); setSaved(false)
@@ -4388,7 +4419,7 @@ function MacrosSection(props) {
4388
4419
  streamChunkMs: Number(d.dictation && d.dictation.streamChunkMs) || 1200,
4389
4420
  vadAdapt: Number(d.dictation && d.dictation.vadAdapt) || 0,
4390
4421
  noiseSuppression: d.noiseSuppression !== false,
4391
- noiseGateDb: Number(d.noiseGateDb !== undefined ? d.noiseGateDb : -45),
4422
+ noiseGateDb: Number(d.noiseGateDb !== undefined ? d.noiseGateDb : -45), bilingualDeveloperPrompt: d.bilingualDeveloperPrompt !== false,
4392
4423
  contextGlossary: d.contextGlossary !== false,
4393
4424
  visualizerStyle: d.visualizerStyle || 'liquid-wave',
4394
4425
  gatedTurnTaking: d.gatedTurnTaking !== false,
@@ -4563,6 +4594,9 @@ function MacrosSection(props) {
4563
4594
  testLevel: testLevel,
4564
4595
  gateDbVal: gateDbVal,
4565
4596
  gateThresholdPercent: gateThresholdPercent,
4597
+ calibrating: calibrating,
4598
+ calibrateMsg: calibrateMsg,
4599
+ calibrateNoise: calibrateNoise,
4566
4600
  onClearKey: () => {
4567
4601
  setDraft((d) => Object.assign({}, d || {}, { hotkey: '' }))
4568
4602
  voice.hotkey = ''
@@ -4719,186 +4753,186 @@ function MacrosSection(props) {
4719
4753
  const animFrameRef = React.useRef(null)
4720
4754
 
4721
4755
  React.useEffect(() => {
4722
- if (typeof navigator !== 'undefined' && navigator.permissions && navigator.permissions.query) {
4723
- navigator.permissions.query({ name: 'microphone' })
4724
- .then((p) => {
4725
- if (p) {
4726
- setMicPermission(p.state)
4727
- p.onchange = () => setMicPermission(p.state)
4728
- }
4729
- })
4730
- .catch(() => {})
4756
+ if (typeof navigator !== 'undefined' && navigator.permissions && navigator.permissions.query) {
4757
+ navigator.permissions.query({ name: 'microphone' })
4758
+ .then((p) => {
4759
+ if (p) {
4760
+ setMicPermission(p.state)
4761
+ p.onchange = () => setMicPermission(p.state)
4731
4762
  }
4763
+ })
4764
+ .catch(() => {})
4765
+ }
4732
4766
  }, [])
4733
4767
 
4734
4768
  const refreshDevices = () => {
4735
- if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
4736
- navigator.mediaDevices.enumerateDevices()
4737
- .then((list) => {
4738
- const audioInputs = list.filter((d) => d.kind === 'audioinput')
4739
- setDevices(audioInputs)
4740
- })
4741
- .catch(() => {})
4769
+ if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
4770
+ navigator.mediaDevices.enumerateDevices()
4771
+ .then((list) => {
4772
+ const audioInputs = list.filter((d) => d.kind === 'audioinput')
4773
+ setDevices(audioInputs)
4774
+ })
4775
+ .catch(() => {})
4742
4776
  }
4743
4777
 
4744
4778
  React.useEffect(() => {
4745
- refreshDevices()
4779
+ refreshDevices()
4746
4780
  }, [])
4747
4781
 
4748
4782
  const stopSignalTest = () => {
4749
- if (animFrameRef.current) {
4750
- cancelAnimationFrame(animFrameRef.current)
4751
- animFrameRef.current = null
4752
- }
4753
- if (signalStreamRef.current) {
4754
- try { signalStreamRef.current.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4755
- signalStreamRef.current = null
4756
- }
4757
- if (audioCtxRef.current) {
4758
- try { audioCtxRef.current.close() } catch (_) {}
4759
- audioCtxRef.current = null
4760
- }
4761
- setSignalTesting(false)
4762
- setCurrentRms(0)
4783
+ if (animFrameRef.current) {
4784
+ cancelAnimationFrame(animFrameRef.current)
4785
+ animFrameRef.current = null
4786
+ }
4787
+ if (signalStreamRef.current) {
4788
+ try { signalStreamRef.current.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4789
+ signalStreamRef.current = null
4790
+ }
4791
+ if (audioCtxRef.current) {
4792
+ try { audioCtxRef.current.close() } catch (_) {}
4793
+ audioCtxRef.current = null
4794
+ }
4795
+ setSignalTesting(false)
4796
+ setCurrentRms(0)
4763
4797
  }
4764
4798
 
4765
4799
  React.useEffect(() => {
4766
- return () => {
4767
- stopSignalTest()
4768
- }
4800
+ return () => {
4801
+ stopSignalTest()
4802
+ }
4769
4803
  }, [])
4770
4804
 
4771
4805
  const requestPermissionAndTestSignal = async () => {
4772
- stopSignalTest()
4773
- setTestError('')
4774
- try {
4775
- const constraints = {
4776
- audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
4777
- }
4778
- const stream = await navigator.mediaDevices.getUserMedia(constraints)
4779
- signalStreamRef.current = stream
4780
- setMicPermission('granted')
4781
- refreshDevices()
4782
-
4783
- const AC = typeof AudioContext !== 'undefined' ? AudioContext
4784
- : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
4785
- if (!AC) return
4786
- const actx = new AC()
4787
- audioCtxRef.current = actx
4788
- if (actx.state === 'suspended') { actx.resume().catch(() => {}) }
4789
- const src = actx.createMediaStreamSource(stream)
4790
- const analyser = actx.createAnalyser()
4791
- analyser.fftSize = 128
4792
- src.connect(analyser)
4793
-
4794
- setSignalTesting(true)
4795
- const buf = new Uint8Array(analyser.frequencyBinCount)
4796
-
4797
- const checkLevel = () => {
4798
- if (!signalStreamRef.current) return
4799
- analyser.getByteFrequencyData(buf)
4800
- let sum = 0
4801
- for (let i = 0; i < buf.length; i++) sum += buf[i]
4802
- const rms = Math.min(100, Math.round((sum / buf.length / 255) * 100 * 2.2))
4803
- setCurrentRms(rms)
4804
- setPeakRms((prev) => Math.max(prev, rms))
4805
- animFrameRef.current = requestAnimationFrame(checkLevel)
4806
- }
4807
- animFrameRef.current = requestAnimationFrame(checkLevel)
4808
- } catch (err) {
4809
- setMicPermission('denied')
4810
- setTestError(String(err && err.message ? err.message : err))
4811
- stopSignalTest()
4812
- }
4806
+ stopSignalTest()
4807
+ setTestError('')
4808
+ try {
4809
+ const constraints = {
4810
+ audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
4811
+ }
4812
+ const stream = await navigator.mediaDevices.getUserMedia(constraints)
4813
+ signalStreamRef.current = stream
4814
+ setMicPermission('granted')
4815
+ refreshDevices()
4816
+
4817
+ const AC = typeof AudioContext !== 'undefined' ? AudioContext
4818
+ : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
4819
+ if (!AC) return
4820
+ const actx = new AC()
4821
+ audioCtxRef.current = actx
4822
+ if (actx.state === 'suspended') { actx.resume().catch(() => {}) }
4823
+ const src = actx.createMediaStreamSource(stream)
4824
+ const analyser = actx.createAnalyser()
4825
+ analyser.fftSize = 128
4826
+ src.connect(analyser)
4827
+
4828
+ setSignalTesting(true)
4829
+ const buf = new Uint8Array(analyser.frequencyBinCount)
4830
+
4831
+ const checkLevel = () => {
4832
+ if (!signalStreamRef.current) return
4833
+ analyser.getByteFrequencyData(buf)
4834
+ let sum = 0
4835
+ for (let i = 0; i < buf.length; i++) sum += buf[i]
4836
+ const rms = Math.min(100, Math.round((sum / buf.length / 255) * 100 * 2.2))
4837
+ setCurrentRms(rms)
4838
+ setPeakRms((prev) => Math.max(prev, rms))
4839
+ animFrameRef.current = requestAnimationFrame(checkLevel)
4840
+ }
4841
+ animFrameRef.current = requestAnimationFrame(checkLevel)
4842
+ } catch (err) {
4843
+ setMicPermission('denied')
4844
+ setTestError(String(err && err.message ? err.message : err))
4845
+ stopSignalTest()
4846
+ }
4813
4847
  }
4814
4848
 
4815
4849
  const runSttTest = async () => {
4816
- stopSignalTest()
4817
- setTestError('')
4818
- setTestResult(null)
4819
- setTestLatency(null)
4820
- setTestingStt(true)
4821
- setTestCountdown(3)
4822
-
4823
- let testStream = null
4824
- let recorder = null
4825
- const chunks = []
4850
+ stopSignalTest()
4851
+ setTestError('')
4852
+ setTestResult(null)
4853
+ setTestLatency(null)
4854
+ setTestingStt(true)
4855
+ setTestCountdown(3)
4826
4856
 
4827
- try {
4828
- const constraints = {
4829
- audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
4830
- }
4831
- testStream = await navigator.mediaDevices.getUserMedia(constraints)
4832
- let mime = 'audio/webm;codecs=opus'
4833
- if (!MediaRecorder.isTypeSupported(mime)) mime = ''
4834
- recorder = mime ? new MediaRecorder(testStream, { mimeType: mime }) : new MediaRecorder(testStream)
4835
- recorder.ondataavailable = (e) => {
4836
- if (e.data && e.data.size > 0) chunks.push(e.data)
4837
- }
4838
- recorder.start()
4839
-
4840
- let left = 3
4841
- const countdownTimer = setInterval(() => {
4842
- left -= 1
4843
- setTestCountdown(left)
4844
- if (left <= 0) {
4845
- clearInterval(countdownTimer)
4846
- if (recorder.state !== 'inactive') recorder.stop()
4847
- }
4848
- }, 1000)
4857
+ let testStream = null
4858
+ let recorder = null
4859
+ const chunks = []
4849
4860
 
4850
- await new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
4861
+ try {
4862
+ const constraints = {
4863
+ audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
4864
+ }
4865
+ testStream = await navigator.mediaDevices.getUserMedia(constraints)
4866
+ let mime = 'audio/webm;codecs=opus'
4867
+ if (!MediaRecorder.isTypeSupported(mime)) mime = ''
4868
+ recorder = mime ? new MediaRecorder(testStream, { mimeType: mime }) : new MediaRecorder(testStream)
4869
+ recorder.ondataavailable = (e) => {
4870
+ if (e.data && e.data.size > 0) chunks.push(e.data)
4871
+ }
4872
+ recorder.start()
4873
+
4874
+ let left = 3
4875
+ const countdownTimer = setInterval(() => {
4876
+ left -= 1
4877
+ setTestCountdown(left)
4878
+ if (left <= 0) {
4879
+ clearInterval(countdownTimer)
4880
+ if (recorder.state !== 'inactive') recorder.stop()
4881
+ }
4882
+ }, 1000)
4851
4883
 
4852
- try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4884
+ await new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
4853
4885
 
4854
- const blob = new Blob(chunks, { type: mime || 'audio/webm' })
4855
- if (blob.size < 400) {
4856
- setTestError(t('wizardTestEmpty'))
4857
- setTestingStt(false)
4858
- return
4859
- }
4886
+ try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4860
4887
 
4861
- setTestCountdown(0)
4862
- const startMs = Date.now()
4863
- const dataBase64 = await blobToBase64(blob)
4864
- const payload = {
4865
- dataBase64,
4866
- mimeType: mime || recorder.mimeType || 'audio/webm',
4867
- mode: 'dictation',
4868
- subMode: 'clean',
4869
- }
4888
+ const blob = new Blob(chunks, { type: mime || 'audio/webm' })
4889
+ if (blob.size < 400) {
4890
+ setTestError(t('wizardTestEmpty'))
4891
+ setTestingStt(false)
4892
+ return
4893
+ }
4870
4894
 
4871
- const res = await fetch('/dsh-voice/transcribe', {
4872
- method: 'POST',
4873
- headers: { 'content-type': 'application/json' },
4874
- body: JSON.stringify(payload),
4875
- })
4876
- const data = await res.json()
4877
- const latency = Date.now() - startMs
4878
- setTestLatency(latency)
4895
+ setTestCountdown(0)
4896
+ const startMs = Date.now()
4897
+ const dataBase64 = await blobToBase64(blob)
4898
+ const payload = {
4899
+ dataBase64,
4900
+ mimeType: mime || recorder.mimeType || 'audio/webm',
4901
+ mode: 'dictation',
4902
+ subMode: 'clean',
4903
+ }
4879
4904
 
4880
- if (!res.ok || !data || !data.ok) {
4881
- const msg = (data && data.error && data.error.message) || `HTTP ${res.status}`
4882
- setTestError(msg)
4883
- } else {
4884
- const recognized = String(data.text || '').trim()
4885
- if (recognized) {
4886
- setTestResult(recognized)
4887
- if (typeof setTop === 'function') {
4888
- setTop('wizardCompleted', true)
4889
- }
4890
- } else {
4891
- setTestError(t('wizardTestEmpty'))
4892
- }
4893
- }
4894
- } catch (err) {
4895
- setTestError(String(err && err.message ? err.message : err))
4896
- } finally {
4897
- if (testStream) {
4898
- try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4899
- }
4900
- setTestingStt(false)
4905
+ const res = await fetch('/dsh-voice/transcribe', {
4906
+ method: 'POST',
4907
+ headers: { 'content-type': 'application/json' },
4908
+ body: JSON.stringify(payload),
4909
+ })
4910
+ const data = await res.json()
4911
+ const latency = Date.now() - startMs
4912
+ setTestLatency(latency)
4913
+
4914
+ if (!res.ok || !data || !data.ok) {
4915
+ const msg = (data && data.error && data.error.message) || `HTTP ${res.status}`
4916
+ setTestError(msg)
4917
+ } else {
4918
+ const recognized = String(data.text || '').trim()
4919
+ if (recognized) {
4920
+ setTestResult(recognized)
4921
+ if (typeof setTop === 'function') {
4922
+ setTop('wizardCompleted', true)
4901
4923
  }
4924
+ } else {
4925
+ setTestError(t('wizardTestEmpty'))
4926
+ }
4927
+ }
4928
+ } catch (err) {
4929
+ setTestError(String(err && err.message ? err.message : err))
4930
+ } finally {
4931
+ if (testStream) {
4932
+ try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
4933
+ }
4934
+ setTestingStt(false)
4935
+ }
4902
4936
  }
4903
4937
 
4904
4938
  // Determine active chain info
@@ -4906,177 +4940,177 @@ function MacrosSection(props) {
4906
4940
  const activeProvider = (dictationChain[0] && (dictationChain[0].provider || dictationChain[0])) || 'sensevoice'
4907
4941
  let areaLabel = t('wizardAreaServer')
4908
4942
  if (activeProvider === 'browser' || activeProvider === 'browser-webgpu') {
4909
- areaLabel = t('wizardAreaBrowser')
4943
+ areaLabel = t('wizardAreaBrowser')
4910
4944
  } else if (activeProvider !== 'sensevoice' && activeProvider !== 'local-whisper') {
4911
- areaLabel = t('wizardAreaCloud')
4945
+ areaLabel = t('wizardAreaCloud')
4912
4946
  }
4913
4947
 
4914
4948
  let nextStepHint = ''
4915
4949
  if (!isSecure) {
4916
- nextStepHint = t('wizardHintHttps')
4950
+ nextStepHint = t('wizardHintHttps')
4917
4951
  } else if (micPermission === 'denied') {
4918
- nextStepHint = t('wizardHintPermission')
4952
+ nextStepHint = t('wizardHintPermission')
4919
4953
  } else if (peakRms === 0 && signalTesting) {
4920
- nextStepHint = t('wizardHintDevice')
4954
+ nextStepHint = t('wizardHintDevice')
4921
4955
  } else if (testError) {
4922
- nextStepHint = t('wizardHintProvider')
4956
+ nextStepHint = t('wizardHintProvider')
4923
4957
  }
4924
4958
 
4925
4959
  return React.createElement('div', { className: 'cb-section-card', style: { borderColor: 'var(--dsw-alias-state-brand-primary)' } },
4926
- React.createElement('div', { className: 'cb-section-title' },
4927
- React.createElement('span', null, '✨ ' + t('wizardTitle')),
4928
- React.createElement('div', { style: { display: 'flex', gap: '8px' } },
4929
- testResult ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('wizardStatusCompleted')) : null,
4930
- onClose ? React.createElement('button', {
4931
- type: 'button', className: 'cb-btn cb-btn-mini', onClick: onClose, title: t('wizardClose'),
4932
- }, '✕') : null,
4933
- ),
4934
- ),
4935
- React.createElement('div', { className: 'cb-section-desc' }, t('wizardDesc')),
4936
-
4937
- // Step 1: Environment & Permissions
4938
- React.createElement('div', { className: 'dvs-card' },
4939
- React.createElement('div', { className: 'dvs-h' }, t('wizardStep1')),
4940
- React.createElement('div', { className: 'cb-row' },
4941
- React.createElement('span', { className: 'cb-badge ' + (isSecure ? 'cb-badge-ok' : 'cb-badge-err') },
4942
- isSecure ? ('✓ ' + t('wizardSecureContextOk')) : ('✗ ' + t('wizardSecureContextBad'))
4943
- ),
4944
- React.createElement('span', { className: 'cb-badge ' + (hasMediaDevices ? 'cb-badge-ok' : 'cb-badge-err') },
4945
- hasMediaDevices ? ('✓ ' + t('wizardMediaDevicesOk')) : ('✗ ' + t('wizardMediaDevicesBad'))
4946
- ),
4947
- React.createElement('span', { className: 'cb-badge ' + (hasMediaRecorder ? 'cb-badge-ok' : 'cb-badge-err') },
4948
- hasMediaRecorder ? ('✓ ' + t('wizardMediaRecorderOk')) : ('✗ ' + t('wizardMediaRecorderBad'))
4949
- ),
4950
- React.createElement('span', { className: 'cb-badge ' + (micPermission === 'granted' ? 'cb-badge-ok' : (micPermission === 'denied' ? 'cb-badge-err' : 'cb-badge-warn')) },
4951
- micPermission === 'granted' ? ('✓ ' + t('wizardMicPermissionOk')) : (micPermission === 'denied' ? ('✗ ' + t('wizardMicPermissionBad')) : ('· ' + t('wizardMicPermissionPrompt')))
4952
- ),
4953
- ),
4954
- React.createElement('div', { className: 'cb-field', style: { marginTop: '8px' } },
4955
- React.createElement('label', null, t('wizardMicLabel')),
4956
- React.createElement('div', { className: 'cb-row' },
4957
- React.createElement('select', {
4958
- value: selectedDev,
4959
- style: { flex: 1, minWidth: '240px' },
4960
- onChange: (e) => {
4961
- setSelectedDev(e.target.value)
4962
- if (typeof setTop === 'function') setTop('micDeviceId', e.target.value)
4963
- stopSignalTest()
4964
- },
4965
- },
4966
- React.createElement('option', { value: '' }, 'Default System Microphone'),
4967
- devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId }, d.label || `Microphone ${d.deviceId.slice(0, 8)}…`))
4968
- ),
4969
- React.createElement('button', {
4970
- type: 'button',
4971
- className: 'cb-btn',
4972
- onClick: requestPermissionAndTestSignal,
4973
- }, t('wizardRequestMic')),
4974
- ),
4975
- ),
4960
+ React.createElement('div', { className: 'cb-section-title' },
4961
+ React.createElement('span', null, '✨ ' + t('wizardTitle')),
4962
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
4963
+ testResult ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('wizardStatusCompleted')) : null,
4964
+ onClose ? React.createElement('button', {
4965
+ type: 'button', className: 'cb-btn cb-btn-mini', onClick: onClose, title: t('wizardClose'),
4966
+ }, '✕') : null,
4967
+ ),
4968
+ ),
4969
+ React.createElement('div', { className: 'cb-section-desc' }, t('wizardDesc')),
4970
+
4971
+ // Step 1: Environment & Permissions
4972
+ React.createElement('div', { className: 'dvs-card' },
4973
+ React.createElement('div', { className: 'dvs-h' }, t('wizardStep1')),
4974
+ React.createElement('div', { className: 'cb-row' },
4975
+ React.createElement('span', { className: 'cb-badge ' + (isSecure ? 'cb-badge-ok' : 'cb-badge-err') },
4976
+ isSecure ? ('✓ ' + t('wizardSecureContextOk')) : ('✗ ' + t('wizardSecureContextBad'))
4977
+ ),
4978
+ React.createElement('span', { className: 'cb-badge ' + (hasMediaDevices ? 'cb-badge-ok' : 'cb-badge-err') },
4979
+ hasMediaDevices ? ('✓ ' + t('wizardMediaDevicesOk')) : ('✗ ' + t('wizardMediaDevicesBad'))
4980
+ ),
4981
+ React.createElement('span', { className: 'cb-badge ' + (hasMediaRecorder ? 'cb-badge-ok' : 'cb-badge-err') },
4982
+ hasMediaRecorder ? ('✓ ' + t('wizardMediaRecorderOk')) : ('✗ ' + t('wizardMediaRecorderBad'))
4983
+ ),
4984
+ React.createElement('span', { className: 'cb-badge ' + (micPermission === 'granted' ? 'cb-badge-ok' : (micPermission === 'denied' ? 'cb-badge-err' : 'cb-badge-warn')) },
4985
+ micPermission === 'granted' ? ('✓ ' + t('wizardMicPermissionOk')) : (micPermission === 'denied' ? ('✗ ' + t('wizardMicPermissionBad')) : ('· ' + t('wizardMicPermissionPrompt')))
4986
+ ),
4987
+ ),
4988
+ React.createElement('div', { className: 'cb-field', style: { marginTop: '8px' } },
4989
+ React.createElement('label', null, t('wizardMicLabel')),
4990
+ React.createElement('div', { className: 'cb-row' },
4991
+ React.createElement('select', {
4992
+ value: selectedDev,
4993
+ style: { flex: 1, minWidth: '240px' },
4994
+ onChange: (e) => {
4995
+ setSelectedDev(e.target.value)
4996
+ if (typeof setTop === 'function') setTop('micDeviceId', e.target.value)
4997
+ stopSignalTest()
4998
+ },
4999
+ },
5000
+ React.createElement('option', { value: '' }, 'Default System Microphone'),
5001
+ devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId }, d.label || `Microphone ${d.deviceId.slice(0, 8)}…`))
4976
5002
  ),
5003
+ React.createElement('button', {
5004
+ type: 'button',
5005
+ className: 'cb-btn',
5006
+ onClick: requestPermissionAndTestSignal,
5007
+ }, t('wizardRequestMic')),
5008
+ ),
5009
+ ),
5010
+ ),
4977
5011
 
4978
- // Step 2: Live Audio Signal Test
4979
- React.createElement('div', { className: 'dvs-card' },
4980
- React.createElement('div', { className: 'dvs-h' }, t('wizardStep2')),
4981
- React.createElement('div', { className: 'cb-row', style: { alignItems: 'center' } },
4982
- React.createElement('button', {
4983
- type: 'button',
4984
- className: 'cb-btn ' + (signalTesting ? 'cb-btn-danger' : 'cb-btn-primary'),
4985
- onClick: signalTesting ? stopSignalTest : requestPermissionAndTestSignal,
4986
- }, signalTesting ? t('wizardStopSignalCheck') : t('wizardStartSignalCheck')),
4987
- signalTesting ? React.createElement('span', { className: 'dvs-sub' }, t('wizardSignalTesting')) : null,
4988
- ),
4989
- signalTesting ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '6px' } },
4990
- React.createElement('div', {
4991
- style: {
4992
- height: '14px',
4993
- borderRadius: '7px',
4994
- background: 'var(--dsw-alias-bg-layer-1)',
4995
- border: '1px solid var(--dsw-alias-border-l1)',
4996
- overflow: 'hidden',
4997
- position: 'relative',
4998
- },
4999
- },
5000
- React.createElement('div', {
5001
- style: {
5002
- width: `${currentRms}%`,
5003
- height: '100%',
5004
- background: currentRms > 5 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)',
5005
- transition: 'width 0.05s linear',
5006
- },
5007
- }),
5008
- ),
5009
- React.createElement('span', { className: currentRms > 5 ? 'dvs-ok' : 'dvs-sub' },
5010
- currentRms > 5
5011
- ? t('wizardSignalOk').replace('{level}', currentRms)
5012
- : t('wizardSignalZero')
5013
- ),
5014
- ) : null,
5015
- ),
5012
+ // Step 2: Live Audio Signal Test
5013
+ React.createElement('div', { className: 'dvs-card' },
5014
+ React.createElement('div', { className: 'dvs-h' }, t('wizardStep2')),
5015
+ React.createElement('div', { className: 'cb-row', style: { alignItems: 'center' } },
5016
+ React.createElement('button', {
5017
+ type: 'button',
5018
+ className: 'cb-btn ' + (signalTesting ? 'cb-btn-danger' : 'cb-btn-primary'),
5019
+ onClick: signalTesting ? stopSignalTest : requestPermissionAndTestSignal,
5020
+ }, signalTesting ? t('wizardStopSignalCheck') : t('wizardStartSignalCheck')),
5021
+ signalTesting ? React.createElement('span', { className: 'dvs-sub' }, t('wizardSignalTesting')) : null,
5022
+ ),
5023
+ signalTesting ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '6px' } },
5024
+ React.createElement('div', {
5025
+ style: {
5026
+ height: '14px',
5027
+ borderRadius: '7px',
5028
+ background: 'var(--dsw-alias-bg-layer-1)',
5029
+ border: '1px solid var(--dsw-alias-border-l1)',
5030
+ overflow: 'hidden',
5031
+ position: 'relative',
5032
+ },
5033
+ },
5034
+ React.createElement('div', {
5035
+ style: {
5036
+ width: `${currentRms}%`,
5037
+ height: '100%',
5038
+ background: currentRms > 5 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)',
5039
+ transition: 'width 0.05s linear',
5040
+ },
5041
+ }),
5042
+ ),
5043
+ React.createElement('span', { className: currentRms > 5 ? 'dvs-ok' : 'dvs-sub' },
5044
+ currentRms > 5
5045
+ ? t('wizardSignalOk').replace('{level}', currentRms)
5046
+ : t('wizardSignalZero')
5047
+ ),
5048
+ ) : null,
5049
+ ),
5016
5050
 
5017
- // Step 3: Transcription Round-Trip Test
5018
- React.createElement('div', { className: 'dvs-card' },
5019
- React.createElement('div', { className: 'dvs-h' }, t('wizardStep3')),
5020
- React.createElement('div', { className: 'cb-row' },
5021
- React.createElement('span', { className: 'dvs-sub' }, `${t('wizardProviderLabel')} ${activeProvider}`),
5022
- React.createElement('span', { className: 'cb-badge' }, `${t('wizardProcessingArea')} ${areaLabel}`),
5023
- ),
5024
- React.createElement('div', { className: 'cb-row', style: { marginTop: '8px' } },
5025
- React.createElement('button', {
5026
- type: 'button',
5027
- className: 'cb-btn cb-btn-primary',
5028
- disabled: testingStt || micPermission === 'denied',
5029
- onClick: runSttTest,
5030
- }, testingStt
5031
- ? (testCountdown > 0 ? t('wizardRecordingTest').replace('{left}', testCountdown) : t('wizardTranscribingTest'))
5032
- : t('wizardStartTestRecord')
5033
- ),
5034
- ),
5035
- testResult ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '4px' } },
5036
- React.createElement('div', { className: 'dvs-ok' },
5037
- '✓ ' + t('wizardTestSuccess').replace('{latency}', testLatency || 0)
5038
- ),
5039
- React.createElement('blockquote', {
5040
- style: {
5041
- margin: '4px 0 0 0',
5042
- padding: '8px 12px',
5043
- borderRadius: '6px',
5044
- background: 'var(--dsw-alias-bg-layer-3)',
5045
- borderLeft: '4px solid var(--dsw-alias-state-success-primary)',
5046
- color: 'var(--dsw-alias-label-primary)',
5047
- fontStyle: 'italic',
5048
- },
5049
- }, `“${testResult}”`),
5050
- ) : null,
5051
- testError ? React.createElement('div', { className: 'cb-alert-err', style: { marginTop: '8px' } },
5052
- t('wizardTestFailed').replace('{error}', testError)
5053
- ) : null,
5054
- ),
5051
+ // Step 3: Transcription Round-Trip Test
5052
+ React.createElement('div', { className: 'dvs-card' },
5053
+ React.createElement('div', { className: 'dvs-h' }, t('wizardStep3')),
5054
+ React.createElement('div', { className: 'cb-row' },
5055
+ React.createElement('span', { className: 'dvs-sub' }, `${t('wizardProviderLabel')} ${activeProvider}`),
5056
+ React.createElement('span', { className: 'cb-badge' }, `${t('wizardProcessingArea')} ${areaLabel}`),
5057
+ ),
5058
+ React.createElement('div', { className: 'cb-row', style: { marginTop: '8px' } },
5059
+ React.createElement('button', {
5060
+ type: 'button',
5061
+ className: 'cb-btn cb-btn-primary',
5062
+ disabled: testingStt || micPermission === 'denied',
5063
+ onClick: runSttTest,
5064
+ }, testingStt
5065
+ ? (testCountdown > 0 ? t('wizardRecordingTest').replace('{left}', testCountdown) : t('wizardTranscribingTest'))
5066
+ : t('wizardStartTestRecord')
5067
+ ),
5068
+ ),
5069
+ testResult ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '4px' } },
5070
+ React.createElement('div', { className: 'dvs-ok' },
5071
+ '✓ ' + t('wizardTestSuccess').replace('{latency}', testLatency || 0)
5072
+ ),
5073
+ React.createElement('blockquote', {
5074
+ style: {
5075
+ margin: '4px 0 0 0',
5076
+ padding: '8px 12px',
5077
+ borderRadius: '6px',
5078
+ background: 'var(--dsw-alias-bg-layer-3)',
5079
+ borderLeft: '4px solid var(--dsw-alias-state-success-primary)',
5080
+ color: 'var(--dsw-alias-label-primary)',
5081
+ fontStyle: 'italic',
5082
+ },
5083
+ }, `“${testResult}”`),
5084
+ ) : null,
5085
+ testError ? React.createElement('div', { className: 'cb-alert-err', style: { marginTop: '8px' } },
5086
+ t('wizardTestFailed').replace('{error}', testError)
5087
+ ) : null,
5088
+ ),
5055
5089
 
5056
- // Next Step Hint / Feedback
5057
- nextStepHint ? React.createElement('div', { className: 'dvs-wait', style: { fontStyle: 'italic' } },
5058
- t('wizardNextStepHint').replace('{hint}', nextStepHint)
5059
- ) : null,
5090
+ // Next Step Hint / Feedback
5091
+ nextStepHint ? React.createElement('div', { className: 'dvs-wait', style: { fontStyle: 'italic' } },
5092
+ t('wizardNextStepHint').replace('{hint}', nextStepHint)
5093
+ ) : null,
5060
5094
 
5061
- // Wizard Bottom Actions
5062
- React.createElement('div', { className: 'cb-row', style: { justifyContent: 'space-between', marginTop: '6px' } },
5063
- React.createElement('button', {
5064
- type: 'button',
5065
- className: 'cb-btn',
5066
- onClick: () => {
5067
- if (typeof setTop === 'function') setTop('wizardCompleted', true)
5068
- if (onClose) onClose()
5069
- },
5070
- }, t('wizardSkip')),
5071
- testResult ? React.createElement('button', {
5072
- type: 'button',
5073
- className: 'cb-btn cb-btn-primary',
5074
- onClick: () => {
5075
- if (typeof setTop === 'function') setTop('wizardCompleted', true)
5076
- if (onClose) onClose()
5077
- },
5078
- }, '✓ ' + t('wizardFinishSuccess')) : null,
5079
- ),
5095
+ // Wizard Bottom Actions
5096
+ React.createElement('div', { className: 'cb-row', style: { justifyContent: 'space-between', marginTop: '6px' } },
5097
+ React.createElement('button', {
5098
+ type: 'button',
5099
+ className: 'cb-btn',
5100
+ onClick: () => {
5101
+ if (typeof setTop === 'function') setTop('wizardCompleted', true)
5102
+ if (onClose) onClose()
5103
+ },
5104
+ }, t('wizardSkip')),
5105
+ testResult ? React.createElement('button', {
5106
+ type: 'button',
5107
+ className: 'cb-btn cb-btn-primary',
5108
+ onClick: () => {
5109
+ if (typeof setTop === 'function') setTop('wizardCompleted', true)
5110
+ if (onClose) onClose()
5111
+ },
5112
+ }, '✓ ' + t('wizardFinishSuccess')) : null,
5113
+ ),
5080
5114
  )
5081
5115
  }
5082
5116
  // Card in Settings → Plugins → Plugin settings (#18)