@goodandready/dsh-voice 0.9.44 → 0.9.46
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 +384 -776
- package/locale/en.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// dsh-voice — client half (browser).
|
|
2
|
-
//
|
|
3
|
-
// Two buttons in conversation.input.right:
|
|
4
|
-
// mic — dictation: speech is cut on pauses, each chunk is recognized and
|
|
5
|
-
// appended to the composer; send stays with the user;
|
|
6
|
-
// wave — voice message: one whole recording; after recognition the text is
|
|
7
|
-
// sent to the agent when the cancel window expires.
|
|
8
|
-
//
|
|
9
|
-
// The recording pill lives in conversation.input.dock; settings are a card
|
|
10
|
-
// in settings.plugin.item.
|
|
11
1
|
|
|
12
2
|
window.__ModuleLoader__.load({
|
|
13
3
|
id: '@goodandready/dsh-voice',
|
|
@@ -27,605 +17,295 @@ window.__ModuleLoader__.load({
|
|
|
27
17
|
}
|
|
28
18
|
|
|
29
19
|
const NS = 'dsh-voice'
|
|
30
|
-
// Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
|
|
31
20
|
const PKG = '@goodandready/dsh-voice'
|
|
32
21
|
const ROW_ID = 'dsh-voice'
|
|
33
22
|
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
34
23
|
|
|
35
|
-
// UI strings live in the locale registry so a separate package can
|
|
36
|
-
// translate them without touching this plugin. English is the source
|
|
37
|
-
// language, the default, and the fallback.
|
|
38
24
|
const en = {
|
|
39
|
-
'saveFailed':
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
'
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
162
|
-
'
|
|
163
|
-
'
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
170
|
-
'
|
|
171
|
-
'
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
'
|
|
176
|
-
'
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
'
|
|
184
|
-
'
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
189
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
'
|
|
201
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
'
|
|
207
|
-
'
|
|
208
|
-
'
|
|
209
|
-
'visOff': 'Off',
|
|
210
|
-
'statusTitle': 'Connection & Engine Status',
|
|
211
|
-
'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
|
|
212
|
-
'badgeHostOnline': 'Host online ({ms} ms)',
|
|
213
|
-
'badgeHostOffline': 'Host unreachable',
|
|
214
|
-
'badgeWhisperActive': 'Whisper active',
|
|
215
|
-
'badgeWhisperInactive': 'Whisper offline',
|
|
216
|
-
'badgeSenseVoiceActive': 'SenseVoice active',
|
|
217
|
-
'badgeSenseVoiceInactive': 'SenseVoice offline',
|
|
218
|
-
'badgeProviders': '{count} providers ready',
|
|
219
|
-
'refreshStats': 'Refresh Stats',
|
|
220
|
-
'hardwareTitle': 'Hardware & Local Engines',
|
|
221
|
-
'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
|
|
222
|
-
'uiError': 'Voice UI Error:',
|
|
223
|
-
'retry': 'Retry',
|
|
224
|
-
'testMic': 'Test Mic',
|
|
225
|
-
'stopTest': 'Stop Test',
|
|
226
|
-
'micLevel': 'Mic level',
|
|
227
|
-
'noiseGate': 'Noise Gate (dB)',
|
|
228
|
-
'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
|
|
229
|
-
'noiseGateOff': 'Off',
|
|
230
|
-
'versionTitle': 'Plugin Version & Updates',
|
|
231
|
-
'versionHint': 'Check for new versions and update directly from npm.',
|
|
232
|
-
'updateAvailable': 'Update available',
|
|
233
|
-
'upToDate': 'Up to date',
|
|
234
|
-
'updatePrompt': 'A new version is available on npm. Click update to install.',
|
|
235
|
-
'updateNow': 'Update now',
|
|
236
|
-
'updating': 'Updating…',
|
|
237
|
-
'updateSuccess': 'Updated successfully! Restart DSH to apply.',
|
|
238
|
-
'updateFailed': 'Update failed. Check logs.',
|
|
239
|
-
'updateCheckFailed': 'Update check failed',
|
|
240
|
-
'profileLockedPid': 'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
|
|
241
|
-
'profileLockedExisting': 'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
|
|
242
|
-
'profileLockCheckError': 'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
|
|
243
|
-
'checkingUpdates': 'Checking updates…',
|
|
244
|
-
'updateStatusUnknown': 'Version status unknown',
|
|
245
|
-
'sensevoiceTitle': 'SenseVoice Local ASR (1-Click Setup)',
|
|
246
|
-
'sensevoiceDesc': 'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
|
|
247
|
-
'installSensevoice': 'Install SenseVoice-Small (1-Click)',
|
|
248
|
-
'installingSensevoice': 'Installing SenseVoice…',
|
|
249
|
-
'sensevoiceReady': 'Ready',
|
|
250
|
-
'sensevoiceNotInstalled': 'Not Installed',
|
|
251
|
-
'sensevoiceDownloading': 'Downloading model…',
|
|
252
|
-
'turnTakingTitle': 'Turn-Taking & Audio Coordination',
|
|
253
|
-
'turnTakingDesc': 'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
|
|
254
|
-
'gatedTurnTaking': 'Gated Mode (mute mic while assistant is speaking)',
|
|
255
|
-
'bargeInDesc': 'True Barge-In (speaking interrupts assistant speech)',
|
|
256
|
-
'smartActionsTitle': 'Hands-free & Smart Actions',
|
|
257
|
-
'autoSendVisualRing': 'Silence Ring countdown animation',
|
|
258
|
-
'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
|
|
259
|
-
'liveInterimPreview': 'Live speech preview (ghost text preview)',
|
|
260
|
-
'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
|
|
261
|
-
'modeVerbatim': 'Verbatim',
|
|
262
|
-
'modeClean': 'Clean Text',
|
|
263
|
-
'modeTask': 'Agent Task',
|
|
264
|
-
'modeNote': 'Note',
|
|
265
|
-
'dictationModeTitle': 'Dictation Post-Processing Mode',
|
|
266
|
-
'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).',
|
|
267
|
-
'dictationPromptsTitle': 'Dictation Prompt Templates',
|
|
268
|
-
'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
|
|
269
|
-
'promptClean': 'Clean text prompt',
|
|
270
|
-
'promptTask': 'Agent task prompt',
|
|
271
|
-
'promptNote': 'Note prompt',
|
|
272
|
-
'resetPrompts': 'Reset to defaults',
|
|
273
|
-
'voiceMacrosTitle': 'Voice Action Macros',
|
|
274
|
-
'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
|
|
275
|
-
'addMacro': 'Add macro',
|
|
276
|
-
'macroTrigger': 'Spoken trigger phrase',
|
|
277
|
-
'macroTemplate': 'Expanded prompt template',
|
|
278
|
-
'macroDelete': 'Delete',
|
|
279
|
-
'rawTab': 'Raw STT',
|
|
280
|
-
'resultTab': 'Result',
|
|
281
|
-
'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
|
|
282
|
-
'jargonTitle': 'Developer Lexicon & IT Jargon',
|
|
283
|
-
'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
|
|
284
|
-
'jargonAdd': 'Add replacement',
|
|
285
|
-
'jargonFrom': 'Spoken slang',
|
|
286
|
-
'jargonTo': 'Correct spelling',
|
|
287
|
-
'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
|
|
288
|
-
'voiceInput': 'Voice input',
|
|
289
|
-
'assistantSpeakingGated': 'Assistant speaking (gated mode)',
|
|
290
|
-
'jargonFormatHint': ' (spoken -> replacement)',
|
|
291
|
-
'errorPrefix': 'Error',
|
|
292
|
-
'wizardTitle': 'First-Run Setup & Diagnostics Wizard',
|
|
293
|
-
'wizardDesc': 'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
|
|
294
|
-
'wizardBtnLaunch': 'Launch Mic & STT Wizard',
|
|
295
|
-
'wizardClose': 'Close Wizard',
|
|
296
|
-
'wizardStep1': '1. Audio & Environment',
|
|
297
|
-
'wizardStep2': '2. Live Signal Test',
|
|
298
|
-
'wizardStep3': '3. STT Transcription Test',
|
|
299
|
-
'wizardSecureContextOk': 'Secure context / HTTPS verified',
|
|
300
|
-
'wizardSecureContextBad': 'Insecure context (HTTPS or localhost required)',
|
|
301
|
-
'wizardMediaDevicesOk': 'Browser mediaDevices API supported',
|
|
302
|
-
'wizardMediaDevicesBad': 'Browser mediaDevices API missing',
|
|
303
|
-
'wizardMediaRecorderOk': 'MediaRecorder API supported',
|
|
304
|
-
'wizardMediaRecorderBad': 'MediaRecorder API unsupported in this browser',
|
|
305
|
-
'wizardMicPermissionOk': 'Microphone permission granted',
|
|
306
|
-
'wizardMicPermissionPrompt': 'Microphone permission not yet granted',
|
|
307
|
-
'wizardMicPermissionBad': 'Microphone permission denied',
|
|
308
|
-
'wizardRequestMic': 'Request Microphone Access',
|
|
309
|
-
'wizardMicLabel': 'Microphone input device:',
|
|
310
|
-
'wizardStartSignalCheck': 'Start Audio Level Test', 'wizardStopSignalCheck': 'Stop Audio Test',
|
|
311
|
-
'wizardSignalOk': 'Audio signal detected (RMS: {level}%)',
|
|
312
|
-
'wizardSignalZero': 'Zero audio level detected - speak into microphone or select correct device',
|
|
313
|
-
'wizardSignalTesting': 'Listening for audio signal… speak into your microphone',
|
|
314
|
-
'wizardProviderLabel': 'Active STT engine & model:',
|
|
315
|
-
'wizardProcessingArea': 'Processing area:', 'wizardAreaBrowser': 'In-browser (WebGPU / WebSpeech)', 'wizardAreaServer': 'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud': 'Remote cloud provider',
|
|
316
|
-
'wizardStartTestRecord': 'Record 3-Second Test Phrase', 'wizardRecordingTest': 'Recording test phrase… speak now ({left}s)',
|
|
317
|
-
'wizardTranscribingTest': 'Transcribing test phrase…', 'wizardTestResult': 'STT Test Result:',
|
|
318
|
-
'wizardTestSuccess': 'STT round-trip verified successfully! Latency: {latency}ms',
|
|
319
|
-
'wizardTestEmpty': 'No speech detected in test recording. Please speak clearly and try again.',
|
|
320
|
-
'wizardTestFailed': 'STT test failed: {error}',
|
|
321
|
-
'wizardNextStepHint': 'Next step: {hint}',
|
|
322
|
-
'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.',
|
|
323
|
-
'wizardFinishSuccess': 'Setup & Verification Complete', 'wizardSkip': 'Skip Wizard',
|
|
324
|
-
'wizardStatusCompleted': 'Microphone and STT verified',
|
|
325
|
-
'exportDiagnosticsBtn': 'Export Diagnostics Report', 'exportingDiagnostics': 'Exporting report…',
|
|
326
|
-
'diagnosticsExportSuccess': 'Report downloaded', 'recentRecognitionsTitle': 'Recent Recognition Telemetry',
|
|
327
|
-
'noRecognitionsYet': 'No recent recognition records yet.', 'diagLatencyBreakdown': 'Latency breakdown:',
|
|
328
|
-
'diagVad': 'VAD: {ms}ms', 'diagStt': 'STT: {ms}ms', 'diagPolish': 'Polish: {ms}ms', 'diagTotal': 'Total: {ms}ms', 'diagBytes': 'Audio: {bytes} B', 'diagWords': 'Words: {count}',
|
|
329
|
-
'diagExportNotice': 'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
|
|
25
|
+
'saveFailed':'Some fields were not saved —', 'cardHint':'Dictation and voice messages: providers, chains, local whisper',
|
|
26
|
+
'expand':'Expand', 'collapse':'Collapse', 'composerUnavailable':'Composer unavailable', 'keySpace':'Space', 'keyUnset':'not set',
|
|
27
|
+
'recognitionError':'recognition failed', 'micUnavailable':'Microphone unavailable: needs HTTPS or localhost',
|
|
28
|
+
'noRecorder':'MediaRecorder is not supported by this browser', 'browserFailed':'Browser did not recognize: ',
|
|
29
|
+
'nothingHeard':'Nothing was recognized', 'dictationBtn':'Voice typing', 'messageBtn':'Voice message — click or hold',
|
|
30
|
+
'dictationPill':'Dictation — text is appended to the input', 'messagePill':'Recording a voice message', 'cancel':'Cancel',
|
|
31
|
+
'holdHint':'Hold — release to send', 'listening':'Listening in the browser…', 'stop':'Stop', 'transcribing':'Transcribing…',
|
|
32
|
+
'sendingIn':'Sending to the agent in', 'secondsShort':' s', 'keepPending':'Do not send', 'hide':'Hide', 'title':'Voice',
|
|
33
|
+
'recordSlot':'Voice recording',
|
|
34
|
+
'browserHint':'Web Speech recognition (uses page/browser language when auto, Chrome sends audio to Google)',
|
|
35
|
+
'browserWebGpu':'Browser WebGPU Whisper (Offline)',
|
|
36
|
+
'browserWebGpuHint':'Runs Whisper model directly in browser GPU via WebGPU/WASM: offline, no host or cloud upload.',
|
|
37
|
+
'npuProvider':'Hardware Acceleration (SBC NPU / Host)',
|
|
38
|
+
'npuProviderHint':'Execution provider for SenseVoice / sherpa-onnx: CPU, Rockchip RK3588 NPU (rknpu), OpenVINO, or CUDA.',
|
|
39
|
+
'npuThreads':'Threads (CPU/SBC)', 'benchmarkTitle':'Hardware Benchmark (Speed & Latency)', 'runBenchmarkBtn':'Run Benchmark',
|
|
40
|
+
'benchmarking':'Running benchmark…',
|
|
41
|
+
'benchmarkHint':'Measures real-time factor (RTF) and inference latency on 1.0s audio for current execution provider.',
|
|
42
|
+
'webgpuStatus':'Browser WebGPU Status', 'webgpuAvailable':'WebGPU Available (hardware acceleration ready)',
|
|
43
|
+
'webgpuNotAvailable':'WebGPU Not Detected (will fallback to server)', 'openaiHint':'whisper-1 · key OPENAI_API_KEY',
|
|
44
|
+
'siliconflowHint':'FunAudioLLM/SenseVoiceSmall · key SILICONFLOW_API_KEY',
|
|
45
|
+
'deepinfraHint':'openai/whisper-large-v3-turbo · key DEEPINFRA_API_KEY', 'fireworksHint':'whisper-v3-turbo · key FIREWORKS_API_KEY',
|
|
46
|
+
'mistralHint':'voxtral-mini-latest · key MISTRAL_API_KEY', 'openrouterHint':'google/gemini-2.5-flash · key OPENROUTER_API_KEY',
|
|
47
|
+
'localHint':'set when the server is started', 'up':'Up', 'down':'Down', 'remove':'Remove', 'addProvider':'Add provider',
|
|
48
|
+
'chainHint':'Top to bottom is the order they are tried in', 'customName':'name used in the chain', 'customModel':'model',
|
|
49
|
+
'customKeyName':'key name', 'customModelHint':'model to request (empty means the built-in one)', 'addCustom':'Add your own provider',
|
|
50
|
+
'loadingSettings':'Loading settings…',
|
|
51
|
+
'notReady1':'The harness has not announced this plugin’s settings yet. If it has just restarted, ',
|
|
52
|
+
'notReady2':'the section will appear on its own in a few seconds.', 'hotkey':'Voice message key', 'pressKey':'Press a key…',
|
|
53
|
+
'clearKey':'Clear the key', 'hotkeyHint1':'Hold it to record, release to send to the agent, Esc cancels. ',
|
|
54
|
+
'hotkeyHint2':'Any key will do: a letter, an F-key or a modifier.', 'language':'Language', 'langAuto':'Auto-detect',
|
|
55
|
+
'langRu':'Russian (ru)', 'langEn':'English (en)', 'langZh':'Chinese (zh)', 'langUk':'Ukrainian (uk)', 'langDe':'German (de)',
|
|
56
|
+
'dictationHint':'Speech is cut at pauses and the text is appended to the input.', 'pauseMs':'Pause that ends a phrase, ms',
|
|
57
|
+
'pauseHint':'Lower means more frequent chunks and faster text, but a higher risk of cutting a word', 'speaking':'You are speaking…',
|
|
58
|
+
'silence':'Pause…', 'normalizeTranscript':'Normalize file transcripts', 'undo':'Undo last insert', 'undone':'Insert undone',
|
|
59
|
+
'nothingToUndo':'Nothing to undo', 'clipping':'Clipping', 'clippingHint':'Too loud (clipping)', 'lowVolume':'Low signal',
|
|
60
|
+
'lowVolumeHint':'Speak louder', 'vadEngine':'VAD engine', 'vadEngineHint':'Neural Silero VAD (WASM/ONNX) or classic amplitude gate',
|
|
61
|
+
'vadSilero':'Silero VAD (neural)', 'vadAmplitude':'Amplitude gate', 'streamingWs':'Streaming WebSocket',
|
|
62
|
+
'streamingWsHint':'Real-time WebSocket audio streaming to local ASR', 'beep':'Beep on start/stop', 'localOnly':'Local whisper only',
|
|
63
|
+
'localOnlyHint':'Restrict both chains to the local whisper.cpp server: fully offline.', 'sendDelay':'Dictation send delay (ms)',
|
|
64
|
+
'sendDelayHint':'Wait before appending a dictated phrase, with a cancel window. 0 = off', 'mic':'Microphone',
|
|
65
|
+
'micDefault':'System default', 'vocabulary':'Custom vocabulary (one word per line)', 'polish':'Polish transcript with model',
|
|
66
|
+
'polishHint':'Fix punctuation and fillers via the harness model before inserting', 'stream':'Continuous dictation',
|
|
67
|
+
'streamHint':'Cut phrases by a timer while you speak instead of waiting for a long pause', 'streamChunkMs':'Stream chunk (ms)',
|
|
68
|
+
'vadAdapt':'Adaptive silence', 'vadAdaptHint':'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
|
|
69
|
+
'wakeWord':'Wake word', 'wakeWordHint':'Browser recognition starts recording when speech begins with this phrase. Empty = off',
|
|
70
|
+
'bargeIn':'Barge-in', 'polishSend':'Polish whole draft before sending',
|
|
71
|
+
'polishSendHint':'Run the composed draft through the model right before sending', 'editablePreview':'Editable preview before send',
|
|
72
|
+
'editablePreviewHint':'Show recognized text before sending with editing, comparison and abort options.', 'editPreview':'Edit',
|
|
73
|
+
'sendNow':'Send', 'cancelPreview':'Cancel', 'rawText':'Raw', 'polishedText':'Polished',
|
|
74
|
+
'sessionMismatch':'Different chat active; canceled to prevent leak.', 'sessionCommands':'Voice session commands',
|
|
75
|
+
'sessionCommandsHint':'"send", "cancel", "stop", "continue" act on the session instead of becoming text',
|
|
76
|
+
'polishBaseUrl':'Offline polish endpoint',
|
|
77
|
+
'polishBaseUrlHint':'OpenAI-compatible /chat/completions base URL, e.g. a local Ollama. Empty = harness model',
|
|
78
|
+
'polishModel':'Offline polish model', 'polishKeyEnv':'Offline polish key credential',
|
|
79
|
+
'voiceCommandsLabel':'Voice edit commands ("new line", "paragraph")',
|
|
80
|
+
'normalizeTranscriptHint':'transcribe_audio: spoken numbers to digits, tidy punctuation', 'messageTitle':'Voice message',
|
|
81
|
+
'messageHint':'One whole recording, sent to the agent once it is transcribed.', 'undoMs':'Undo window, ms',
|
|
82
|
+
'undoHint':'How long the automatic send can still be called off', 'customTitle':'Your own providers',
|
|
83
|
+
'customHint':'Any OpenAI-compatible API. The name becomes available in the chains above.', 'general':'General',
|
|
84
|
+
'whisperEndpoint':'Local whisper: endpoint', 'whisperEndpointHint':'POST /inference of a whisper.cpp server',
|
|
85
|
+
'deepgramEndpoint':'Deepgram: base URL',
|
|
86
|
+
'deepgramEndpointHint':'Base URL for Deepgram or self-hosted deployment (default https://api.deepgram.com)',
|
|
87
|
+
'whisperBin':'Local whisper: binary', 'whisperBinHint':'used when autostart is on', 'whisperModel':'Local whisper: model',
|
|
88
|
+
'whisperModelHint':'Absolute path to the ggml model file', 'whisperAutostart':'Autostart the local whisper',
|
|
89
|
+
'bilingualDeveloperPrompt':'Bilingual developer context (RU/EN)',
|
|
90
|
+
'bilingualDeveloperPromptHint':'Hint technical terms and CLI commands to the STT model to prevent phonetic transliteration',
|
|
91
|
+
'calibrateNoise':'Calibrate Noise (2s)',
|
|
92
|
+
'calibrateNoiseHint':'Measure room noise for 2 seconds and auto-set the optimal noise gate threshold', 'calibrating':'Measuring noise…',
|
|
93
|
+
'calibratedMsg':'Gate set: {db} dB (ambient: {noise} dB)', 'calibratedValue':'Calibrated', 'save':'Save', 'saved':'Saved ✓',
|
|
94
|
+
'openrouterWarning':'OpenRouter has no /audio/transcriptions — use the openai-chat-audio template there',
|
|
95
|
+
'noiseSuppression':'Hardware noise suppression',
|
|
96
|
+
'noiseSuppressionHint':'Enable browser noise suppression, echo cancellation, and auto gain control',
|
|
97
|
+
'contextGlossary':'Context glossary injection',
|
|
98
|
+
'contextGlossaryHint':'Auto-extract code identifiers and terms from composer to improve STT accuracy',
|
|
99
|
+
'providerDashboard':'Provider Latency & Health', 'avgLatency':'Avg latency', 'successRate':'Success', 'fast':'Fast', 'normal':'Normal',
|
|
100
|
+
'slow':'Slow', 'error':'Error', 'idle':'No calls', 'play':'Play', 'pause':'Pause', 'listenBack':'Listen back',
|
|
101
|
+
'lastRecording':'Last voice note', 'sensevoiceHint':'SenseVoice-ONNX / Sherpa-ONNX · ultra-fast local STT (~50ms)',
|
|
102
|
+
'sensevoiceEndpoint':'SenseVoice: endpoint', 'sensevoiceEndpointHint':'POST endpoint of sherpa-onnx or compatible server',
|
|
103
|
+
'sensevoiceBin':'SenseVoice: binary', 'sensevoiceBinHint':'used when autostart is on', 'sensevoiceModel':'SenseVoice: model path',
|
|
104
|
+
'sensevoiceModelHint':'Absolute path or identifier of the SenseVoice / sherpa-onnx model',
|
|
105
|
+
'sensevoiceAutostart':'Autostart the SenseVoice server', 'visualizerStyle':'Audio visualizer style',
|
|
106
|
+
'visualizerStyleHint':'Waveform animation inside the recording pill', 'visLiquidWave':'Liquid Wave', 'visDynamicOrb':'Dynamic Orb',
|
|
107
|
+
'visBars':'Classic Bars', 'visOff':'Off', 'statusTitle':'Connection & Engine Status',
|
|
108
|
+
'statusDesc':'Current runtime connectivity to voice backends and recognition services.', 'badgeHostOnline':'Host online ({ms} ms)',
|
|
109
|
+
'badgeHostOffline':'Host unreachable', 'badgeWhisperActive':'Whisper active', 'badgeWhisperInactive':'Whisper offline',
|
|
110
|
+
'badgeSenseVoiceActive':'SenseVoice active', 'badgeSenseVoiceInactive':'SenseVoice offline', 'badgeProviders':'{count} providers ready',
|
|
111
|
+
'refreshStats':'Refresh Stats', 'hardwareTitle':'Hardware & Local Engines',
|
|
112
|
+
'hardwareDesc':'Microphone device, local whisper.cpp and SenseVoice runtime settings.', 'uiError':'Voice UI Error:', 'retry':'Retry',
|
|
113
|
+
'testMic':'Test Mic', 'stopTest':'Stop Test', 'micLevel':'Mic level', 'noiseGate':'Noise Gate (dB)',
|
|
114
|
+
'noiseGateHint':'Mute background fan hum and keyboard clicks below threshold. Off = disabled', 'noiseGateOff':'Off',
|
|
115
|
+
'versionTitle':'Plugin Version & Updates', 'versionHint':'Check for new versions and update directly from npm.',
|
|
116
|
+
'updateAvailable':'Update available', 'upToDate':'Up to date',
|
|
117
|
+
'updatePrompt':'A new version is available on npm. Click update to install.', 'updateNow':'Update now', 'updating':'Updating…',
|
|
118
|
+
'updateSuccess':'Updated successfully! Restart DSH to apply.', 'updateFailed':'Update failed. Check logs.',
|
|
119
|
+
'updateCheckFailed':'Update check failed',
|
|
120
|
+
'profileLockedPid':'Profile is locked by process (PID {pid}). Follow documented controlled operator recovery procedure if stale.',
|
|
121
|
+
'profileLockedExisting':'Profile is locked by an existing lockfile. Follow documented controlled operator recovery procedure if stale.',
|
|
122
|
+
'profileLockCheckError':'Profile lock check failed. Filesystem inspection required (do not remove lockfile).',
|
|
123
|
+
'checkingUpdates':'Checking updates…', 'updateStatusUnknown':'Version status unknown',
|
|
124
|
+
'sensevoiceTitle':'SenseVoice Local ASR (1-Click Setup)',
|
|
125
|
+
'sensevoiceDesc':'Ultra-fast offline speech recognition on CPU (50–100ms latency, Russian/English).',
|
|
126
|
+
'installSensevoice':'Install SenseVoice-Small (1-Click)', 'installingSensevoice':'Installing SenseVoice…', 'sensevoiceReady':'Ready',
|
|
127
|
+
'sensevoiceNotInstalled':'Not Installed', 'sensevoiceDownloading':'Downloading model…',
|
|
128
|
+
'turnTakingTitle':'Turn-Taking & Audio Coordination',
|
|
129
|
+
'turnTakingDesc':'Coordinate speaking and listening so assistant audio does not feed back into the microphone.',
|
|
130
|
+
'gatedTurnTaking':'Gated Mode (mute mic while assistant is speaking)',
|
|
131
|
+
'bargeInDesc':'True Barge-In (speaking interrupts assistant speech)', 'smartActionsTitle':'Hands-free & Smart Actions',
|
|
132
|
+
'autoSendVisualRing':'Silence Ring countdown animation',
|
|
133
|
+
'voiceActionsLabel':'Voice action commands ("send", "cancel", "clear", "new line")',
|
|
134
|
+
'liveInterimPreview':'Live speech preview (ghost text preview)',
|
|
135
|
+
'structuredPromptVoice':'Spoken answers for interactive choice prompts', 'modeVerbatim':'Verbatim', 'modeClean':'Clean Text',
|
|
136
|
+
'modeTask':'Agent Task', 'modeNote':'Note', 'dictationModeTitle':'Dictation Post-Processing Mode',
|
|
137
|
+
'dictationModeDesc':'Select how dictated speech is processed before insertion: Verbatim (0 LLM calls), Clean text (formatting & filler removal), Agent task (structured prompt), or Note (key points & actions).',
|
|
138
|
+
'dictationPromptsTitle':'Dictation Prompt Templates',
|
|
139
|
+
'dictationPromptsDesc':'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
|
|
140
|
+
'promptClean':'Clean text prompt', 'promptTask':'Agent task prompt', 'promptNote':'Note prompt', 'resetPrompts':'Reset to defaults',
|
|
141
|
+
'voiceMacrosTitle':'Voice Action Macros',
|
|
142
|
+
'voiceMacrosDesc':'Spoken trigger phrases automatically expanded into structured prompt templates.', 'addMacro':'Add macro',
|
|
143
|
+
'macroTrigger':'Spoken trigger phrase', 'macroTemplate':'Expanded prompt template', 'macroDelete':'Delete', 'rawTab':'Raw STT',
|
|
144
|
+
'resultTab':'Result', 'llmUnavailableFallback':'LLM unavailable, showing raw transcript', 'jargonTitle':'Developer Lexicon & IT Jargon',
|
|
145
|
+
'techJargonCorrection':'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)', 'jargonAdd':'Add replacement',
|
|
146
|
+
'jargonFrom':'Spoken slang', 'jargonTo':'Correct spelling', 'jargonPlaceholder':'k8s -> kubernetes\npr -> pull request',
|
|
147
|
+
'voiceInput':'Voice input', 'assistantSpeakingGated':'Assistant speaking (gated mode)', 'jargonFormatHint':' (spoken -> replacement)',
|
|
148
|
+
'errorPrefix':'Error', 'wizardTitle':'First-Run Setup & Diagnostics Wizard',
|
|
149
|
+
'wizardDesc':'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
|
|
150
|
+
'wizardBtnLaunch':'Launch Mic & STT Wizard', 'wizardClose':'Close Wizard', 'wizardStep1':'1. Audio & Environment',
|
|
151
|
+
'wizardStep2':'2. Live Signal Test', 'wizardStep3':'3. STT Transcription Test',
|
|
152
|
+
'wizardSecureContextOk':'Secure context / HTTPS verified', 'wizardSecureContextBad':'Insecure context (HTTPS or localhost required)',
|
|
153
|
+
'wizardMediaDevicesOk':'Browser mediaDevices API supported', 'wizardMediaDevicesBad':'Browser mediaDevices API missing',
|
|
154
|
+
'wizardMediaRecorderOk':'MediaRecorder API supported', 'wizardMediaRecorderBad':'MediaRecorder API unsupported in this browser',
|
|
155
|
+
'wizardMicPermissionOk':'Microphone permission granted', 'wizardMicPermissionPrompt':'Microphone permission not yet granted',
|
|
156
|
+
'wizardMicPermissionBad':'Microphone permission denied', 'wizardRequestMic':'Request Microphone Access',
|
|
157
|
+
'wizardMicLabel':'Microphone input device:', 'wizardStartSignalCheck':'Start Audio Level Test',
|
|
158
|
+
'wizardStopSignalCheck':'Stop Audio Test', 'wizardSignalOk':'Audio signal detected (RMS: {level}%)',
|
|
159
|
+
'wizardSignalZero':'Zero audio level detected - speak into microphone or select correct device',
|
|
160
|
+
'wizardSignalTesting':'Listening for audio signal… speak into your microphone', 'wizardProviderLabel':'Active STT engine & model:',
|
|
161
|
+
'wizardProcessingArea':'Processing area:', 'wizardAreaBrowser':'In-browser (WebGPU / WebSpeech)',
|
|
162
|
+
'wizardAreaServer':'Local MiniPC server (SenseVoice / Whisper)', 'wizardAreaCloud':'Remote cloud provider',
|
|
163
|
+
'wizardStartTestRecord':'Record 3-Second Test Phrase', 'wizardRecordingTest':'Recording test phrase… speak now ({left}s)',
|
|
164
|
+
'wizardTranscribingTest':'Transcribing test phrase…', 'wizardTestResult':'STT Test Result:',
|
|
165
|
+
'wizardTestSuccess':'STT round-trip verified successfully! Latency: {latency}ms',
|
|
166
|
+
'wizardTestEmpty':'No speech detected in test recording. Please speak clearly and try again.',
|
|
167
|
+
'wizardTestFailed':'STT test failed: {error}', 'wizardNextStepHint':'Next step: {hint}',
|
|
168
|
+
'wizardHintPermission':'Click "Request Microphone Access" and allow browser permission prompt.',
|
|
169
|
+
'wizardHintDevice':'Check microphone connection or select a different input device.',
|
|
170
|
+
'wizardHintHttps':'Open DeepSeek Harness via HTTPS or localhost address.',
|
|
171
|
+
'wizardHintProvider':'Configure STT engine credentials or install SenseVoice model below.',
|
|
172
|
+
'wizardHintWinDesktop':'System microphone blocked. Open Windows Settings → Privacy & security → Microphone, and turn ON "Let desktop apps access your microphone".',
|
|
173
|
+
'wizardHintWinBrowser':'Microphone permission denied. Allow access in your browser address bar or Windows Settings → Privacy & security → Microphone.',
|
|
174
|
+
'wizardHintMacDesktop':'System microphone blocked. Open macOS System Settings → Privacy & Security → Microphone, and enable DSH Desktop.',
|
|
175
|
+
'wizardOpenWinMicSettings':'Open Windows Mic Settings', 'wizardFinishSuccess':'Setup & Verification Complete',
|
|
176
|
+
'wizardSkip':'Skip Wizard', 'wizardStatusCompleted':'Microphone and STT verified', 'exportDiagnosticsBtn':'Export Diagnostics Report',
|
|
177
|
+
'exportingDiagnostics':'Exporting report…', 'diagnosticsExportSuccess':'Report downloaded',
|
|
178
|
+
'recentRecognitionsTitle':'Recent Recognition Telemetry', 'noRecognitionsYet':'No recent recognition records yet.',
|
|
179
|
+
'diagLatencyBreakdown':'Latency breakdown:', 'diagVad':'VAD: {ms}ms', 'diagStt':'STT: {ms}ms', 'diagPolish':'Polish: {ms}ms',
|
|
180
|
+
'diagTotal':'Total: {ms}ms', 'diagBytes':'Audio: {bytes} B', 'diagWords':'Words: {count}',
|
|
181
|
+
'diagExportNotice':'Exported JSON contains anonymized timings, provider chain, MIME format, and error codes without raw audio or speech transcripts.',
|
|
182
|
+
'hostOnline':'Host online', 'hostOffline':'Host unreachable', 'whisperReady':'Whisper ready', 'whisperStopped':'Whisper stopped',
|
|
183
|
+
'providersAvailable':'providers available', 'macroEmpty':'No macros configured.', 'macroTemplateArgHint':' (use {text} for argument)',
|
|
184
|
+
'noiseGateLow':'-50 dB (Low / Gentle)', 'noiseGateStandard':'-45 dB (Standard Default)', 'noiseGateMedium':'-35 dB (Medium Room)',
|
|
185
|
+
'noiseGateHigh':'-25 dB (High / Mechanical Clicks)', 'npuCpu':'CPU (Standard)',
|
|
186
|
+
'installModelSensevoice':'SenseVoice-Small (150MB, ~50ms)', 'installModelWhisperTurbo':'Whisper Large-v3-Turbo (800MB, High-Accuracy)',
|
|
187
|
+
'promptCleanPlaceholder':'Default: punctuation & filler-word cleanup',
|
|
188
|
+
'promptTaskPlaceholder':'Default: structured problem/outcome/constraints/criteria',
|
|
189
|
+
'promptNotePlaceholder':'Default: key points & action items summary', 'settingsConflict':'Settings conflict: parallel changes detected',
|
|
190
|
+
'saveFailedFallback':'Save failed', 'benchmarkFailed':'Benchmark failed', 'networkError':'Network error', 'benchmarkOffline':'Offline',
|
|
191
|
+
'benchmarkCores':'Cores', 'benchmarkCoresUnit':'cores', 'benchRatingUltraFast':'Ultra-fast', 'benchRatingRealtime':'Real-time',
|
|
192
|
+
'msShort':'ms', 'transcriptionTimeout':'Transcription request timed out (60s)', 'audioContextUnsupported':'AudioContext not supported',
|
|
193
|
+
'audioReadFailed':'failed to read audio', 'fileReaderUnsupported':'FileReader not supported',
|
|
194
|
+
'benchmarkNoAsrServer':'Local ASR server is not running. Hardware detected.',
|
|
330
195
|
}
|
|
331
196
|
|
|
332
|
-
|
|
333
|
-
'saveFailed':
|
|
334
|
-
'
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
'
|
|
339
|
-
'
|
|
340
|
-
'
|
|
341
|
-
'
|
|
342
|
-
'
|
|
343
|
-
'
|
|
344
|
-
'
|
|
345
|
-
'
|
|
346
|
-
'
|
|
347
|
-
'
|
|
348
|
-
'
|
|
349
|
-
'
|
|
350
|
-
'
|
|
351
|
-
'
|
|
352
|
-
'
|
|
353
|
-
'
|
|
354
|
-
'
|
|
355
|
-
'
|
|
356
|
-
'
|
|
357
|
-
'
|
|
358
|
-
'
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
'
|
|
363
|
-
'
|
|
364
|
-
'
|
|
365
|
-
'
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
'
|
|
369
|
-
'
|
|
370
|
-
'
|
|
371
|
-
'
|
|
372
|
-
'
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
'
|
|
376
|
-
'
|
|
377
|
-
'
|
|
378
|
-
'
|
|
379
|
-
'
|
|
380
|
-
'
|
|
381
|
-
'
|
|
382
|
-
'
|
|
383
|
-
'
|
|
384
|
-
'
|
|
385
|
-
'
|
|
386
|
-
'
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
-
'
|
|
390
|
-
'
|
|
391
|
-
'
|
|
392
|
-
'
|
|
393
|
-
'
|
|
394
|
-
'
|
|
395
|
-
'
|
|
396
|
-
'
|
|
397
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'
|
|
405
|
-
'
|
|
406
|
-
'
|
|
407
|
-
'
|
|
408
|
-
'
|
|
409
|
-
'
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
'
|
|
413
|
-
'
|
|
414
|
-
'
|
|
415
|
-
'
|
|
416
|
-
'
|
|
417
|
-
'
|
|
418
|
-
'
|
|
419
|
-
'
|
|
420
|
-
'
|
|
421
|
-
'
|
|
422
|
-
'
|
|
423
|
-
'
|
|
424
|
-
'
|
|
425
|
-
'
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
'
|
|
429
|
-
'
|
|
430
|
-
'
|
|
431
|
-
'
|
|
432
|
-
'
|
|
433
|
-
'
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'sessionCommands': '会话语音控制指令',
|
|
441
|
-
'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
|
|
442
|
-
'polishBaseUrl': '离线润色服务端点',
|
|
443
|
-
'polishBaseUrlHint': '兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型',
|
|
444
|
-
'polishModel': '离线润色模型',
|
|
445
|
-
'polishKeyEnv': '离线润色密钥凭据名',
|
|
446
|
-
'voiceCommandsLabel': '语音编辑指令("换行"、"段落")',
|
|
447
|
-
'normalizeTranscriptHint': 'transcribe_audio:口语数字转阿拉伯数字,整理标点符号',
|
|
448
|
-
'messageTitle': '语音消息',
|
|
449
|
-
'messageHint': '录制单条完整语音,转写完成后自动发送给智能体。',
|
|
450
|
-
'undoMs': '撤回窗口时长(毫秒)',
|
|
451
|
-
'undoHint': '转写完成后多久以内可以随时取消发送',
|
|
452
|
-
'customTitle': '自定义语音服务商',
|
|
453
|
-
'customHint': '兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。',
|
|
454
|
-
'general': '通用设置',
|
|
455
|
-
'whisperEndpoint': '本地 whisper:端点地址',
|
|
456
|
-
'whisperEndpointHint': 'whisper.cpp 服务器的 POST /inference 接口',
|
|
457
|
-
'deepgramEndpoint': 'Deepgram:基础地址',
|
|
458
|
-
'deepgramEndpointHint': 'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)',
|
|
459
|
-
'whisperBin': '本地 whisper:可执行文件',
|
|
460
|
-
'whisperBinHint': '开启自动启动时使用',
|
|
461
|
-
'whisperModel': '本地 whisper:模型文件',
|
|
462
|
-
'whisperModelHint': 'ggml 模型文件的绝对路径',
|
|
463
|
-
'whisperAutostart': '自动启动本地 whisper',
|
|
464
|
-
'bilingualDeveloperPrompt': '双语开发者提示 (中/英)',
|
|
465
|
-
'bilingualDeveloperPromptHint': '提示常用技术术语与命令行指令,避免语音模型音译',
|
|
466
|
-
'calibrateNoise': '校准背景噪声 (2秒)',
|
|
467
|
-
'calibrateNoiseHint': '测量 2 秒环境背景噪声并自动计算最佳门限',
|
|
468
|
-
'calibrating': '正在测量噪声…',
|
|
469
|
-
'calibratedMsg': '门限已设置为: {db} dB (背景噪声: {noise} dB)',
|
|
470
|
-
'calibratedValue': '已校准',
|
|
471
|
-
'save': '保存',
|
|
472
|
-
'saved': '已保存 ✓',
|
|
473
|
-
'openrouterWarning': 'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板',
|
|
474
|
-
'noiseSuppression': '硬件级降噪',
|
|
475
|
-
'noiseSuppressionHint': '启用浏览器硬件降噪、回声消除与自动增益控制',
|
|
476
|
-
'contextGlossary': '上下文专业词汇注入',
|
|
477
|
-
'contextGlossaryHint': '自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
|
|
478
|
-
'providerDashboard': '服务商延迟与健康监控',
|
|
479
|
-
'avgLatency': '平均延迟',
|
|
480
|
-
'successRate': '成功率',
|
|
481
|
-
'fast': '极速',
|
|
482
|
-
'normal': '正常',
|
|
483
|
-
'slow': '较慢',
|
|
484
|
-
'error': '异常',
|
|
485
|
-
'idle': '无调用记录',
|
|
486
|
-
'play': '播放',
|
|
487
|
-
'pause': '暂停',
|
|
488
|
-
'listenBack': '试听回放',
|
|
489
|
-
'lastRecording': '最近一条语音记录',
|
|
490
|
-
'sensevoiceHint': 'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)',
|
|
491
|
-
'sensevoiceEndpoint': 'SenseVoice:端点地址',
|
|
492
|
-
'sensevoiceEndpointHint': 'sherpa-onnx 或兼容服务器的 POST 端点',
|
|
493
|
-
'sensevoiceBin': 'SenseVoice:可执行文件',
|
|
494
|
-
'sensevoiceBinHint': '开启自动启动时使用',
|
|
495
|
-
'sensevoiceModel': 'SenseVoice:模型路径',
|
|
496
|
-
'sensevoiceModelHint': 'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
|
|
497
|
-
'sensevoiceAutostart': '自动启动 SenseVoice 服务',
|
|
498
|
-
'visualizerStyle': '音频动态波形样式',
|
|
499
|
-
'visualizerStyleHint': '录音胶囊面板内的动态音频波形动画效果',
|
|
500
|
-
'visLiquidWave': '流体水波 (Liquid Wave)',
|
|
501
|
-
'visDynamicOrb': '动态脉冲球体 (Dynamic Orb)',
|
|
502
|
-
'visBars': '经典频条 (Classic Bars)',
|
|
503
|
-
'visOff': '关闭',
|
|
504
|
-
'statusTitle': '连接与引擎状态看板',
|
|
505
|
-
'statusDesc': '当前语音后端与识别服务的实时网络与进程就绪状态。',
|
|
506
|
-
'badgeHostOnline': '服务端在线 ({ms} ms)',
|
|
507
|
-
'badgeHostOffline': '服务端不可达',
|
|
508
|
-
'badgeWhisperActive': 'Whisper 就绪',
|
|
509
|
-
'badgeWhisperInactive': 'Whisper 未就绪',
|
|
510
|
-
'badgeSenseVoiceActive': 'SenseVoice 就绪',
|
|
511
|
-
'badgeSenseVoiceInactive': 'SenseVoice 未就绪',
|
|
512
|
-
'badgeProviders': '{count} 个服务商已就绪',
|
|
513
|
-
'refreshStats': '刷新状态',
|
|
514
|
-
'hardwareTitle': '硬件与本地引擎',
|
|
515
|
-
'hardwareDesc': '麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。',
|
|
516
|
-
'uiError': '语音界面错误:',
|
|
517
|
-
'retry': '重试',
|
|
518
|
-
'testMic': '测试麦克风',
|
|
519
|
-
'stopTest': '停止测试',
|
|
520
|
-
'micLevel': '麦克风电平',
|
|
521
|
-
'noiseGate': '噪声门 (dB)',
|
|
522
|
-
'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
|
|
523
|
-
'noiseGateOff': '关闭',
|
|
524
|
-
'versionTitle': '插件版本与更新',
|
|
525
|
-
'versionHint': '检查新版本并直接从 npm 更新。',
|
|
526
|
-
'updateAvailable': '有可用更新',
|
|
527
|
-
'upToDate': '已是最新版本',
|
|
528
|
-
'updatePrompt': 'npm 上有新版本可用。点击更新进行安装。',
|
|
529
|
-
'updateNow': '立即更新',
|
|
530
|
-
'updating': '正在更新…',
|
|
531
|
-
'updateSuccess': '更新成功!请重启 DSH 生效。',
|
|
532
|
-
'updateFailed': '更新失败,请查看日志。',
|
|
533
|
-
'updateCheckFailed': '检查更新失败',
|
|
534
|
-
'profileLockedPid': '配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
|
|
535
|
-
'profileLockedExisting': '配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。',
|
|
536
|
-
'profileLockCheckError': '检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
|
|
537
|
-
'checkingUpdates': '正在检查更新…',
|
|
538
|
-
'updateStatusUnknown': '版本状态未知',
|
|
539
|
-
'sensevoiceTitle': 'SenseVoice 本地 ASR (一键部署)',
|
|
540
|
-
'sensevoiceDesc': 'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。',
|
|
541
|
-
'installSensevoice': '一键安装 SenseVoice-Small',
|
|
542
|
-
'installingSensevoice': '正在安装 SenseVoice…',
|
|
543
|
-
'sensevoiceReady': '已就绪',
|
|
544
|
-
'sensevoiceNotInstalled': '未安装',
|
|
545
|
-
'sensevoiceDownloading': '正在下载模型…',
|
|
546
|
-
'turnTakingTitle': '轮流对话与音频协同',
|
|
547
|
-
'turnTakingDesc': '协调听与说,避免扬声器播放的声音被麦克风二次拾音。',
|
|
548
|
-
'gatedTurnTaking': '门控模式 (助手播报时麦克风自动静音)',
|
|
549
|
-
'bargeInDesc': '实时打断 (说话时立即打断助手朗读)',
|
|
550
|
-
'smartActionsTitle': '免提与智能指令',
|
|
551
|
-
'autoSendVisualRing': '环形倒计时动画指示器',
|
|
552
|
-
'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
|
|
553
|
-
'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
|
|
554
|
-
'structuredPromptVoice': '语音应答交互式选择提示',
|
|
555
|
-
'modeVerbatim': '逐字直录',
|
|
556
|
-
'modeClean': '清爽文本',
|
|
557
|
-
'modeTask': '智能体任务',
|
|
558
|
-
'modeNote': '便签备忘',
|
|
559
|
-
'dictationModeTitle': '听写后处理模式',
|
|
560
|
-
'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
|
|
561
|
-
'dictationPromptsTitle': '听写提示词模板',
|
|
562
|
-
'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
|
|
563
|
-
'promptClean': '清爽文本提示词',
|
|
564
|
-
'promptTask': '智能体任务提示词',
|
|
565
|
-
'promptNote': '便签提示词',
|
|
566
|
-
'resetPrompts': '恢复默认模板',
|
|
567
|
-
'voiceMacrosTitle': '语音动作宏',
|
|
568
|
-
'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
|
|
569
|
-
'addMacro': '添加动作宏',
|
|
570
|
-
'macroTrigger': '语音触发短语',
|
|
571
|
-
'macroTemplate': '展开提示词模板',
|
|
572
|
-
'macroDelete': '删除',
|
|
573
|
-
'rawTab': '原始识别',
|
|
574
|
-
'resultTab': '处理结果',
|
|
575
|
-
'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
|
|
576
|
-
'jargonTitle': '开发者词汇与 IT 术语',
|
|
577
|
-
'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
|
|
578
|
-
'jargonAdd': '添加替换项',
|
|
579
|
-
'jargonFrom': '口语词汇',
|
|
580
|
-
'jargonTo': '标准拼写',
|
|
581
|
-
'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
|
|
582
|
-
'voiceInput': '语音输入',
|
|
583
|
-
'assistantSpeakingGated': '助手正在发言(门控模式)',
|
|
584
|
-
'jargonFormatHint': ' (口语 -> 替换文本)',
|
|
585
|
-
'errorPrefix': '错误',
|
|
586
|
-
'wizardTitle': '首次启动与麦克风诊断向导',
|
|
587
|
-
'wizardDesc': '分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。',
|
|
588
|
-
'wizardBtnLaunch': '启动麦克风与 STT 诊断向导',
|
|
589
|
-
'wizardClose': '关闭向导',
|
|
590
|
-
'wizardStep1': '1. 音频与运行环境',
|
|
591
|
-
'wizardStep2': '2. 实时信号测试',
|
|
592
|
-
'wizardStep3': '3. STT 识别回环测试',
|
|
593
|
-
'wizardSecureContextOk': '安全上下文 / HTTPS 已验证',
|
|
594
|
-
'wizardSecureContextBad': '非安全上下文(需 HTTPS 或 localhost)',
|
|
595
|
-
'wizardMediaDevicesOk': '浏览器 mediaDevices API 支持正常',
|
|
596
|
-
'wizardMediaDevicesBad': '浏览器缺少 mediaDevices API 支持',
|
|
597
|
-
'wizardMediaRecorderOk': 'MediaRecorder API 支持正常', 'wizardMediaRecorderBad': '当前浏览器不支持 MediaRecorder API',
|
|
598
|
-
'wizardMicPermissionOk': '麦克风权限已授予', 'wizardMicPermissionPrompt': '尚未获得麦克风授权', 'wizardMicPermissionBad': '麦克风权限被拒绝',
|
|
599
|
-
'wizardRequestMic': '请求麦克风权限',
|
|
600
|
-
'wizardMicLabel': '麦克风输入设备:',
|
|
601
|
-
'wizardStartSignalCheck': '开始音频电平测试',
|
|
602
|
-
'wizardStopSignalCheck': '停止音频测试',
|
|
603
|
-
'wizardSignalOk': '检测到音频信号 (RMS: {level}%)',
|
|
604
|
-
'wizardSignalZero': '未检测到声音信号 — 请对着麦克风说话或选择正确设备',
|
|
605
|
-
'wizardSignalTesting': '正在监听声音信号… 请对着麦克风发声',
|
|
606
|
-
'wizardProviderLabel': '当前语音引擎与模型:',
|
|
607
|
-
'wizardProcessingArea': '处理区域:', 'wizardAreaBrowser': '浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer': '本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud': '远程云端服务',
|
|
608
|
-
'wizardStartTestRecord': '录制 3 秒测试语句', 'wizardRecordingTest': '正在录制测试音频… 请说话 ({left}秒)',
|
|
609
|
-
'wizardTranscribingTest': '正在请求语音识别…', 'wizardTestResult': 'STT 测试结果:',
|
|
610
|
-
'wizardTestSuccess': 'STT 链路验证成功!耗时:{latency}毫秒',
|
|
611
|
-
'wizardTestEmpty': '录音中未检测到有效语音,请清晰发音后重试。',
|
|
612
|
-
'wizardTestFailed': 'STT 测试失败:{error}',
|
|
613
|
-
'wizardNextStepHint': '建议下一步:{hint}',
|
|
614
|
-
'wizardHintPermission': '点击“请求麦克风权限”并在浏览器弹窗中允许访问。', 'wizardHintDevice': '检查麦克风硬件连接或切换选择其他输入设备。', 'wizardHintHttps': '请通过 HTTPS 或 localhost 访问 DeepSeek Harness。', 'wizardHintProvider': '请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
|
|
615
|
-
'wizardFinishSuccess': '设置与验证已完成', 'wizardSkip': '跳过向导',
|
|
616
|
-
'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
|
|
617
|
-
'exportDiagnosticsBtn': '导出诊断报告', 'exportingDiagnostics': '正在导出报告…',
|
|
618
|
-
'diagnosticsExportSuccess': '诊断报告已下载', 'recentRecognitionsTitle': '近期识别遥测详情',
|
|
619
|
-
'noRecognitionsYet': '暂无近期识别记录。', 'diagLatencyBreakdown': '耗时分布:',
|
|
620
|
-
'diagVad': 'VAD:{ms}毫秒', 'diagStt': 'STT:{ms}毫秒', 'diagPolish': '后处理:{ms}毫秒', 'diagTotal': '总耗时:{ms}毫秒', 'diagBytes': '音频大小:{bytes} 字节', 'diagWords': '词数:{count}',
|
|
621
|
-
'diagExportNotice': '导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。',
|
|
197
|
+
const zh = {
|
|
198
|
+
'saveFailed':'部分字段未保存 —', 'cardHint':'听写与语音消息:服务商、备用链、本地 whisper', 'expand':'展开', 'collapse':'收起', 'composerUnavailable':'输入框不可用',
|
|
199
|
+
'keySpace':'空格', 'keyUnset':'未设置', 'recognitionError':'识别失败', 'micUnavailable':'麦克风不可用:需要 HTTPS 或 localhost',
|
|
200
|
+
'noRecorder':'当前浏览器不支持 MediaRecorder', 'browserFailed':'浏览器未能识别:', 'nothingHeard':'未识别到任何内容', 'dictationBtn':'语音输入',
|
|
201
|
+
'messageBtn':'语音消息 — 点击或按住', 'dictationPill':'听写 — 文本将追加至输入框', 'messagePill':'正在录制语音消息', 'cancel':'取消', 'holdHint':'按住录音 — 松开即发送',
|
|
202
|
+
'listening':'浏览器正在倾听…', 'stop':'停止', 'transcribing':'正在转写…', 'sendingIn':'发送至智能体倒计时', 'secondsShort':' 秒', 'keepPending':'不发送',
|
|
203
|
+
'hide':'隐藏', 'title':'语音', 'recordSlot':'语音录制', 'browserHint':'Web Speech 语音识别 (设为 auto 时使用页面或浏览器语言,Chrome 会上传音频至 Google)',
|
|
204
|
+
'browserWebGpu':'浏览器 WebGPU Whisper (离线)', 'browserWebGpuHint':'通过 WebGPU/WASM 在浏览器本地运行 Whisper 模型:完全离线,音频无需上传到主机。',
|
|
205
|
+
'npuProvider':'硬件加速 (SBC NPU / 主机)',
|
|
206
|
+
'npuProviderHint':'SenseVoice / sherpa-onnx 执行加速引擎:CPU、Rockchip RK3588 NPU (rknpu)、OpenVINO 或 CUDA。', 'npuThreads':'计算线程 (CPU/SBC)',
|
|
207
|
+
'benchmarkTitle':'硬件基准测试 (速度与延迟)', 'runBenchmarkBtn':'运行基准测试', 'benchmarking':'正在运行测试…',
|
|
208
|
+
'benchmarkHint':'测试当前执行提供者在 1.0 秒音频下的实时率 (RTF) 与推理延迟。', 'webgpuStatus':'浏览器 WebGPU 状态', 'webgpuAvailable':'WebGPU 可用 (硬件加速就绪)',
|
|
209
|
+
'webgpuNotAvailable':'未检测到 WebGPU (将自动回退到服务端)', 'openaiHint':'whisper-1 · 密钥 OPENAI_API_KEY',
|
|
210
|
+
'siliconflowHint':'FunAudioLLM/SenseVoiceSmall · 密钥 SILICONFLOW_API_KEY',
|
|
211
|
+
'deepinfraHint':'openai/whisper-large-v3-turbo · 密钥 DEEPINFRA_API_KEY', 'fireworksHint':'whisper-v3-turbo · 密钥 FIREWORKS_API_KEY',
|
|
212
|
+
'mistralHint':'voxtral-mini-latest · 密钥 MISTRAL_API_KEY', 'openrouterHint':'google/gemini-2.5-flash · 密钥 OPENROUTER_API_KEY',
|
|
213
|
+
'localHint':'服务器启动时配置', 'up':'上移', 'down':'下移', 'remove':'移除', 'addProvider':'添加服务商', 'chainHint':'自上而下为故障转移尝试顺序',
|
|
214
|
+
'customName':'备用链中使用的名称', 'customModel':'模型', 'customKeyName':'密钥名称', 'customModelHint':'请求的模型(留空使用内置默认值)', 'addCustom':'添加自定义服务商',
|
|
215
|
+
'loadingSettings':'正在加载设置…', 'notReady1':'Harness 尚未同步此插件的设置。如果刚重启,', 'notReady2':'设置项将在数秒内自动呈现。', 'hotkey':'语音消息快捷键',
|
|
216
|
+
'pressKey':'请按下按键…', 'clearKey':'清除按键', 'hotkeyHint1':'按住录音,松开发送给智能体,按 Esc 取消。', 'hotkeyHint2':'支持任意按键:字母键、功能键 (F1-F12) 或修饰键。',
|
|
217
|
+
'language':'识别语言', 'langAuto':'自动检测', 'langRu':'俄语 (ru)', 'langEn':'英语 (en)', 'langZh':'中文 (zh)', 'langUk':'乌克兰语 (uk)',
|
|
218
|
+
'langDe':'德语 (de)', 'dictationHint':'语音按停顿自动切句,文字实时追加至输入框。', 'pauseMs':'断句静音停顿时长(毫秒)', 'pauseHint':'数值越小切分越频繁、出字越快,但可能切断未说完的词',
|
|
219
|
+
'speaking':'正在说话…', 'silence':'停顿…', 'normalizeTranscript':'规范化转写文本', 'undo':'撤销上次插入', 'undone':'插入已撤销', 'nothingToUndo':'没有可撤销的内容',
|
|
220
|
+
'clipping':'爆音', 'clippingHint':'音量过大(爆音截断)', 'lowVolume':'信号微弱', 'lowVolumeHint':'请提高音量', 'vadEngine':'VAD 引擎',
|
|
221
|
+
'vadEngineHint':'神经网络 Silero VAD (WASM/ONNX) 或传统振幅门限', 'vadSilero':'Silero VAD(神经网络)', 'vadAmplitude':'振幅门限',
|
|
222
|
+
'streamingWs':'流式 WebSocket', 'streamingWsHint':'向本地 ASR 实时通过 WebSocket 流式传输音频', 'beep':'启停提示音', 'localOnly':'仅使用本地 whisper',
|
|
223
|
+
'localOnlyHint':'将双链限制为仅使用本地 whisper.cpp 服务:纯离线运行。', 'sendDelay':'听写延迟追加 (ms)', 'sendDelayHint':'追加听写文本前的缓冲等待时间,允许撤回。0 为关闭',
|
|
224
|
+
'mic':'麦克风设备', 'micDefault':'系统默认', 'vocabulary':'自定义词汇表(每行一个词)', 'polish':'模型润色转写文本', 'polishHint':'插入前通过模型修正标点符号并消除语气口头禅',
|
|
225
|
+
'stream':'连续流式听写', 'streamHint':'说话期间按定时器切分短句,无需等待长时间停顿', 'streamChunkMs':'流式切片时长 (ms)', 'vadAdapt':'自适应静音阈值',
|
|
226
|
+
'vadAdaptHint':'根据语速动态调整停顿静音阈值。0 为固定阈值', 'wakeWord':'语音唤醒词', 'wakeWordHint':'说话以此短语开头时自动触发录音。留空为关闭', 'bargeIn':'语音插话打断 (Barge-in)',
|
|
227
|
+
'polishSend':'发送前润色整篇草稿', 'polishSendHint':'发送给智能体前对整个输入框草稿进行一次整体润色', 'editablePreview':'发送前可编辑预览',
|
|
228
|
+
'editablePreviewHint':'发送前显示识别文本,支持编辑、润色对比和撤销。', 'editPreview':'编辑', 'sendNow':'发送', 'cancelPreview':'取消', 'rawText':'原文',
|
|
229
|
+
'polishedText':'润色', 'sessionMismatch':'当前会话已切换,已拦截发送以防止串话。', 'sessionCommands':'会话语音控制指令',
|
|
230
|
+
'sessionCommandsHint':'"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本', 'polishBaseUrl':'离线润色服务端点',
|
|
231
|
+
'polishBaseUrlHint':'兼容 OpenAI /chat/completions 的基础地址(如本地 Ollama)。留空使用 harness 模型', 'polishModel':'离线润色模型', 'polishKeyEnv':'离线润色密钥凭据名',
|
|
232
|
+
'voiceCommandsLabel':'语音编辑指令("换行"、"段落")', 'normalizeTranscriptHint':'transcribe_audio:口语数字转阿拉伯数字,整理标点符号', 'messageTitle':'语音消息',
|
|
233
|
+
'messageHint':'录制单条完整语音,转写完成后自动发送给智能体。', 'undoMs':'撤回窗口时长(毫秒)', 'undoHint':'转写完成后多久以内可以随时取消发送', 'customTitle':'自定义语音服务商',
|
|
234
|
+
'customHint':'兼容 OpenAI 的任意音频 API。配置名称将出现在上方备用链中。', 'general':'通用设置', 'whisperEndpoint':'本地 whisper:端点地址',
|
|
235
|
+
'whisperEndpointHint':'whisper.cpp 服务器的 POST /inference 接口', 'deepgramEndpoint':'Deepgram:基础地址',
|
|
236
|
+
'deepgramEndpointHint':'Deepgram 或私有化部署的基础地址(默认 https://api.deepgram.com)', 'whisperBin':'本地 whisper:可执行文件',
|
|
237
|
+
'whisperBinHint':'开启自动启动时使用', 'whisperModel':'本地 whisper:模型文件', 'whisperModelHint':'ggml 模型文件的绝对路径',
|
|
238
|
+
'whisperAutostart':'自动启动本地 whisper', 'bilingualDeveloperPrompt':'双语开发者提示 (中/英)',
|
|
239
|
+
'bilingualDeveloperPromptHint':'提示常用技术术语与命令行指令,避免语音模型音译', 'calibrateNoise':'校准背景噪声 (2秒)', 'calibrateNoiseHint':'测量 2 秒环境背景噪声并自动计算最佳门限',
|
|
240
|
+
'calibrating':'正在测量噪声…', 'calibratedMsg':'门限已设置为: {db} dB (背景噪声: {noise} dB)', 'calibratedValue':'已校准', 'save':'保存', 'saved':'已保存 ✓',
|
|
241
|
+
'openrouterWarning':'OpenRouter 无 /audio/transcriptions 端点 — 请选用 openai-chat-audio 模板', 'noiseSuppression':'硬件级降噪',
|
|
242
|
+
'noiseSuppressionHint':'启用浏览器硬件降噪、回声消除与自动增益控制', 'contextGlossary':'上下文专业词汇注入', 'contextGlossaryHint':'自动从输入草稿提取代码变量名与术语以提升语音识别准确率',
|
|
243
|
+
'providerDashboard':'服务商延迟与健康监控', 'avgLatency':'平均延迟', 'successRate':'成功率', 'fast':'极速', 'normal':'正常', 'slow':'较慢', 'error':'异常',
|
|
244
|
+
'idle':'无调用记录', 'play':'播放', 'pause':'暂停', 'listenBack':'试听回放', 'lastRecording':'最近一条语音记录',
|
|
245
|
+
'sensevoiceHint':'SenseVoice-ONNX / Sherpa-ONNX · 极速本地语音识别 (~50ms)', 'sensevoiceEndpoint':'SenseVoice:端点地址',
|
|
246
|
+
'sensevoiceEndpointHint':'sherpa-onnx 或兼容服务器的 POST 端点', 'sensevoiceBin':'SenseVoice:可执行文件', 'sensevoiceBinHint':'开启自动启动时使用',
|
|
247
|
+
'sensevoiceModel':'SenseVoice:模型路径', 'sensevoiceModelHint':'SenseVoice / sherpa-onnx 模型的绝对路径或标识符',
|
|
248
|
+
'sensevoiceAutostart':'自动启动 SenseVoice 服务', 'visualizerStyle':'音频动态波形样式', 'visualizerStyleHint':'录音胶囊面板内的动态音频波形动画效果',
|
|
249
|
+
'visLiquidWave':'流体水波 (Liquid Wave)', 'visDynamicOrb':'动态脉冲球体 (Dynamic Orb)', 'visBars':'经典频条 (Classic Bars)', 'visOff':'关闭',
|
|
250
|
+
'statusTitle':'连接与引擎状态看板', 'statusDesc':'当前语音后端与识别服务的实时网络与进程就绪状态。', 'badgeHostOnline':'服务端在线 ({ms} ms)', 'badgeHostOffline':'服务端不可达',
|
|
251
|
+
'badgeWhisperActive':'Whisper 就绪', 'badgeWhisperInactive':'Whisper 未就绪', 'badgeSenseVoiceActive':'SenseVoice 就绪',
|
|
252
|
+
'badgeSenseVoiceInactive':'SenseVoice 未就绪', 'badgeProviders':'{count} 个服务商已就绪', 'refreshStats':'刷新状态', 'hardwareTitle':'硬件与本地引擎',
|
|
253
|
+
'hardwareDesc':'麦克风输入设备、本地 whisper.cpp 与 SenseVoice 运行时配置。', 'uiError':'语音界面错误:', 'retry':'重试', 'testMic':'测试麦克风', 'stopTest':'停止测试',
|
|
254
|
+
'micLevel':'麦克风电平', 'noiseGate':'噪声门 (dB)', 'noiseGateHint':'静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭', 'noiseGateOff':'关闭',
|
|
255
|
+
'versionTitle':'插件版本与更新', 'versionHint':'检查新版本并直接从 npm 更新。', 'updateAvailable':'有可用更新', 'upToDate':'已是最新版本',
|
|
256
|
+
'updatePrompt':'npm 上有新版本可用。点击更新进行安装。', 'updateNow':'立即更新', 'updating':'正在更新…', 'updateSuccess':'更新成功!请重启 DSH 生效。',
|
|
257
|
+
'updateFailed':'更新失败,请查看日志。', 'updateCheckFailed':'检查更新失败', 'profileLockedPid':'配置文件已被进程锁定 (PID {pid})。若残留请按受控操作员恢复流程处理。',
|
|
258
|
+
'profileLockedExisting':'配置文件已被现有锁定文件锁定。若残留请按受控操作员恢复流程处理。', 'profileLockCheckError':'检查配置文件锁失败。需排查文件系统,请勿直接删除锁文件。',
|
|
259
|
+
'checkingUpdates':'正在检查更新…', 'updateStatusUnknown':'版本状态未知', 'sensevoiceTitle':'SenseVoice 本地 ASR (一键部署)',
|
|
260
|
+
'sensevoiceDesc':'CPU 上超低延迟离线语音识别 (50–100ms,中英俄多语种)。', 'installSensevoice':'一键安装 SenseVoice-Small',
|
|
261
|
+
'installingSensevoice':'正在安装 SenseVoice…', 'sensevoiceReady':'已就绪', 'sensevoiceNotInstalled':'未安装', 'sensevoiceDownloading':'正在下载模型…',
|
|
262
|
+
'turnTakingTitle':'轮流对话与音频协同', 'turnTakingDesc':'协调听与说,避免扬声器播放的声音被麦克风二次拾音。', 'gatedTurnTaking':'门控模式 (助手播报时麦克风自动静音)',
|
|
263
|
+
'bargeInDesc':'实时打断 (说话时立即打断助手朗读)', 'smartActionsTitle':'免提与智能指令', 'autoSendVisualRing':'环形倒计时动画指示器',
|
|
264
|
+
'voiceActionsLabel':'语音动作指令 ("发送", "取消", "清空", "换行")', 'liveInterimPreview':'实时语音文本预览 (幽灵文本)', 'structuredPromptVoice':'语音应答交互式选择提示',
|
|
265
|
+
'modeVerbatim':'逐字直录', 'modeClean':'清爽文本', 'modeTask':'智能体任务', 'modeNote':'便签备忘', 'dictationModeTitle':'听写后处理模式',
|
|
266
|
+
'dictationModeDesc':'选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。', 'dictationPromptsTitle':'听写提示词模板',
|
|
267
|
+
'dictationPromptsDesc':'自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。', 'promptClean':'清爽文本提示词', 'promptTask':'智能体任务提示词', 'promptNote':'便签提示词',
|
|
268
|
+
'resetPrompts':'恢复默认模板', 'voiceMacrosTitle':'语音动作宏', 'voiceMacrosDesc':'语音触发词自动展开为结构化提示词模板。', 'addMacro':'添加动作宏',
|
|
269
|
+
'macroTrigger':'语音触发短语', 'macroTemplate':'展开提示词模板', 'macroDelete':'删除', 'rawTab':'原始识别', 'resultTab':'处理结果',
|
|
270
|
+
'llmUnavailableFallback':'LLM不可用,已保留原始识别文本', 'jargonTitle':'开发者词汇与 IT 术语',
|
|
271
|
+
'techJargonCorrection':'自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)', 'jargonAdd':'添加替换项', 'jargonFrom':'口语词汇', 'jargonTo':'标准拼写',
|
|
272
|
+
'jargonPlaceholder':'k8s -> kubernetes\npr -> pull request', 'voiceInput':'语音输入', 'assistantSpeakingGated':'助手正在发言(门控模式)',
|
|
273
|
+
'jargonFormatHint':' (口语 -> 替换文本)', 'errorPrefix':'错误', 'wizardTitle':'首次启动与麦克风诊断向导',
|
|
274
|
+
'wizardDesc':'分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。', 'wizardBtnLaunch':'启动麦克风与 STT 诊断向导', 'wizardClose':'关闭向导',
|
|
275
|
+
'wizardStep1':'1. 音频与运行环境', 'wizardStep2':'2. 实时信号测试', 'wizardStep3':'3. STT 识别回环测试', 'wizardSecureContextOk':'安全上下文 / HTTPS 已验证',
|
|
276
|
+
'wizardSecureContextBad':'非安全上下文(需 HTTPS 或 localhost)', 'wizardMediaDevicesOk':'浏览器 mediaDevices API 支持正常',
|
|
277
|
+
'wizardMediaDevicesBad':'浏览器缺少 mediaDevices API 支持', 'wizardMediaRecorderOk':'MediaRecorder API 支持正常',
|
|
278
|
+
'wizardMediaRecorderBad':'当前浏览器不支持 MediaRecorder API', 'wizardMicPermissionOk':'麦克风权限已授予', 'wizardMicPermissionPrompt':'尚未获得麦克风授权',
|
|
279
|
+
'wizardMicPermissionBad':'麦克风权限被拒绝', 'wizardRequestMic':'请求麦克风权限', 'wizardMicLabel':'麦克风输入设备:', 'wizardStartSignalCheck':'开始音频电平测试',
|
|
280
|
+
'wizardStopSignalCheck':'停止音频测试', 'wizardSignalOk':'检测到音频信号 (RMS: {level}%)', 'wizardSignalZero':'未检测到声音信号 — 请对着麦克风说话或选择正确设备',
|
|
281
|
+
'wizardSignalTesting':'正在监听声音信号… 请对着麦克风发声', 'wizardProviderLabel':'当前语音引擎与模型:', 'wizardProcessingArea':'处理区域:',
|
|
282
|
+
'wizardAreaBrowser':'浏览器端处理 (WebGPU / WebSpeech)', 'wizardAreaServer':'本地服务器处理 (SenseVoice / Whisper)', 'wizardAreaCloud':'远程云端服务',
|
|
283
|
+
'wizardStartTestRecord':'录制 3 秒测试语句', 'wizardRecordingTest':'正在录制测试音频… 请说话 ({left}秒)', 'wizardTranscribingTest':'正在请求语音识别…',
|
|
284
|
+
'wizardTestResult':'STT 测试结果:', 'wizardTestSuccess':'STT 链路验证成功!耗时:{latency}毫秒', 'wizardTestEmpty':'录音中未检测到有效语音,请清晰发音后重试。',
|
|
285
|
+
'wizardTestFailed':'STT 测试失败:{error}', 'wizardNextStepHint':'建议下一步:{hint}', 'wizardHintPermission':'点击“请求麦克风权限”并在浏览器弹窗中允许访问。',
|
|
286
|
+
'wizardHintDevice':'检查麦克风硬件连接或切换选择其他输入设备。', 'wizardHintHttps':'请通过 HTTPS 或 localhost 访问 DeepSeek Harness。',
|
|
287
|
+
'wizardHintProvider':'请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
|
|
288
|
+
'wizardHintWinDesktop':'系统麦克风被阻止。请在 Windows【设置】→【隐私和安全性】→【麦克风】中开启最下方的【允许桌面应用访问你的麦克风】。',
|
|
289
|
+
'wizardHintWinBrowser':'麦克风权限被拒绝。请在浏览器地址栏允许麦克风权限,或在 Windows【设置】→【隐私和安全性】→【麦克风】中开启访问。',
|
|
290
|
+
'wizardHintMacDesktop':'系统麦克风被阻止。请在 macOS【系统设置】→【隐私与安全性】→【麦克风】中勾选允许 DSH Desktop。', 'wizardOpenWinMicSettings':'打开系统麦克风设置',
|
|
291
|
+
'wizardFinishSuccess':'设置与验证已完成', 'wizardSkip':'跳过向导', 'wizardStatusCompleted':'麦克风与 STT 诊断已就绪', 'exportDiagnosticsBtn':'导出诊断报告',
|
|
292
|
+
'exportingDiagnostics':'正在导出报告…', 'diagnosticsExportSuccess':'诊断报告已下载', 'recentRecognitionsTitle':'近期识别遥测详情',
|
|
293
|
+
'noRecognitionsYet':'暂无近期识别记录。', 'diagLatencyBreakdown':'耗时分布:', 'diagVad':'VAD:{ms}毫秒', 'diagStt':'STT:{ms}毫秒',
|
|
294
|
+
'diagPolish':'后处理:{ms}毫秒', 'diagTotal':'总耗时:{ms}毫秒', 'diagBytes':'音频大小:{bytes} 字节', 'diagWords':'词数:{count}',
|
|
295
|
+
'diagExportNotice':'导出的 JSON 报告包含脱敏的耗时统计、备用链尝试、MIME 格式与错误码,不含任何原始音频或语音文本。', 'hostOnline':'后端在线', 'hostOffline':'后端不可用',
|
|
296
|
+
'whisperReady':'Whisper 已就绪', 'whisperStopped':'Whisper 已停止', 'providersAvailable':'个服务商可用', 'macroEmpty':'未配置宏指令。',
|
|
297
|
+
'macroTemplateArgHint':'(使用 {text} 作为参数)', 'noiseGateLow':'-50 dB (轻微静音)', 'noiseGateStandard':'-45 dB (标准默认)',
|
|
298
|
+
'noiseGateMedium':'-35 dB (中等室内)', 'noiseGateHigh':'-25 dB (高降噪 / 机械键盘)', 'npuCpu':'CPU (标准)',
|
|
299
|
+
'installModelSensevoice':'SenseVoice-Small (150MB, ~50ms)', 'installModelWhisperTurbo':'Whisper Large-v3-Turbo (800MB, 高精度)',
|
|
300
|
+
'promptCleanPlaceholder':'默认:标点符号与语气词清洗', 'promptTaskPlaceholder':'默认:结构化问题/产出/约束/标准', 'promptNotePlaceholder':'默认:核心要点与待办事项总结',
|
|
301
|
+
'settingsConflict':'设置冲突:检测到并行修改', 'saveFailedFallback':'保存失败', 'benchmarkFailed':'基准测试失败', 'networkError':'网络错误',
|
|
302
|
+
'benchmarkOffline':'离线', 'benchmarkCores':'核心数', 'benchmarkCoresUnit':'核', 'benchRatingUltraFast':'极速', 'benchRatingRealtime':'实时',
|
|
303
|
+
'msShort':'毫秒', 'transcriptionTimeout':'语音识别请求超时 (60s)', 'audioContextUnsupported':'不支持 AudioContext', 'audioReadFailed':'读取音频失败',
|
|
304
|
+
'fileReaderUnsupported':'不支持 FileReader', 'benchmarkNoAsrServer':'本地 ASR 服务未启动。已检测到硬件加速支持。',
|
|
622
305
|
}
|
|
623
306
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
let moduleT = (key) => en[key] || key
|
|
627
|
-
const t = (key) => moduleT(key)
|
|
628
|
-
// ------------------------------------------------------------------ css
|
|
307
|
+
let moduleT = (key) => en[key] || key
|
|
308
|
+
const t = (key) => moduleT(key)
|
|
629
309
|
const CSS =
|
|
630
310
|
'.dvo-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none;touch-action:manipulation}.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box;user-select:none;-webkit-user-select:none}.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}.dvo-pbtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);user-select:none;-webkit-user-select:none;touch-action:manipulation}.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvo-err{color:var(--dsw-alias-state-error-primary)}.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}.dvo-spin{animation:dvo-spin 1s linear infinite}@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.dvo-audio-wrap{display:flex;align-items:center;gap:8px;padding:2px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:14px;border:1px solid var(--dsw-alias-border-l1)}.dvo-audio-play{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border:0;cursor:pointer;padding:0}.dvo-audio-play:hover{opacity:0.9}.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}.dvo-dash{display:flex;flex-direction:column;gap:8px;margin-top:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}.dvo-dash-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}.dvo-badge-fast{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary)}.dvo-badge-norm{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-primary)}.dvo-badge-slow{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-border-l2)}.dvo-badge-err{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}.dvo-ghost{font-style:italic;color:var(--dsw-alias-label-tertiary);margin-left:6px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.dvo-installer-box{display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:8px}.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}.dvo-btn-send:hover{opacity:0.9}.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}.dvo-wave-clipping{color:var(--dsw-alias-state-warning-primary)!important;}.dvo-wave-low{color:var(--dsw-alias-label-secondary)!important;}.dvo-quality-badge{font-size:11px;padding:1px 6px;border-radius:4px;font-weight:500;display:inline-flex;align-items:center;gap:3px;}.dvo-quality-clipping{color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-surface-secondary);}.dvo-quality-low{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-surface-secondary);}.dvo-undo-badge{font-size:11px;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-surface-secondary);color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}'
|
|
631
311
|
const cssId = 'dsh-voice/client.module.css'
|
|
@@ -635,7 +315,6 @@ window.__ModuleLoader__.load({
|
|
|
635
315
|
tag.dataset.pluginCss = cssId
|
|
636
316
|
document.head.appendChild(tag)
|
|
637
317
|
}
|
|
638
|
-
// ---------------------------------------------------------------- store
|
|
639
318
|
const voice = {
|
|
640
319
|
phase: 'idle', // idle | recording | processing | pending | error
|
|
641
320
|
mode: 'dictation', // dictation | message
|
|
@@ -755,7 +434,6 @@ window.__ModuleLoader__.load({
|
|
|
755
434
|
return voice
|
|
756
435
|
}
|
|
757
436
|
|
|
758
|
-
// ---------------------------------------------------------------- icons
|
|
759
437
|
const ic = { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }
|
|
760
438
|
const playIcon = () => React.createElement('svg', Object.assign({}, ic, { viewBox: '0 0 24 24' }),
|
|
761
439
|
React.createElement('polygon', { points: '6 4 20 12 6 20 6 4', fill: 'currentColor', stroke: 'none' }))
|
|
@@ -817,13 +495,12 @@ window.__ModuleLoader__.load({
|
|
|
817
495
|
} catch { /* best effort */ }
|
|
818
496
|
}
|
|
819
497
|
|
|
820
|
-
// ------------------------------------------------------------ transport
|
|
821
498
|
function blobToBase64(blob) {
|
|
822
499
|
return new Promise((resolve, reject) => {
|
|
823
|
-
if (typeof FileReader === 'undefined') { reject(new Error('
|
|
500
|
+
if (typeof FileReader === 'undefined') { reject(new Error(t('fileReaderUnsupported'))); return }
|
|
824
501
|
const fr = new FileReader()
|
|
825
502
|
fr.onload = () => { const s = String(fr.result || ''); resolve(s.indexOf(',') >= 0 ? s.slice(s.indexOf(',') + 1) : s) }
|
|
826
|
-
fr.onerror = () => reject(new Error('
|
|
503
|
+
fr.onerror = () => reject(new Error(t('audioReadFailed')))
|
|
827
504
|
fr.readAsDataURL(blob)
|
|
828
505
|
})
|
|
829
506
|
}
|
|
@@ -852,7 +529,6 @@ window.__ModuleLoader__.load({
|
|
|
852
529
|
const text = chunks.join(' ')
|
|
853
530
|
if (!text || text.length < 3) return []
|
|
854
531
|
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
855
|
-
// Stop-word dictionary for vocabulary keyword extraction
|
|
856
532
|
const stop = new Set([
|
|
857
533
|
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'any', 'can', 'her', 'was',
|
|
858
534
|
'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now',
|
|
@@ -904,7 +580,7 @@ window.__ModuleLoader__.load({
|
|
|
904
580
|
})
|
|
905
581
|
} catch (e) {
|
|
906
582
|
if (e && e.name === 'AbortError') {
|
|
907
|
-
throw new Error(
|
|
583
|
+
throw new Error(t('transcriptionTimeout'))
|
|
908
584
|
}
|
|
909
585
|
throw e
|
|
910
586
|
} finally {
|
|
@@ -950,7 +626,6 @@ window.__ModuleLoader__.load({
|
|
|
950
626
|
const headOut = await tryHost(head)
|
|
951
627
|
if (headOut && (headOut.text || headOut.command || headOut.action)) return headOut
|
|
952
628
|
} catch (headErr) {
|
|
953
|
-
// Continue to WebGPU
|
|
954
629
|
}
|
|
955
630
|
}
|
|
956
631
|
|
|
@@ -982,7 +657,6 @@ window.__ModuleLoader__.load({
|
|
|
982
657
|
return lead + s + trail
|
|
983
658
|
}
|
|
984
659
|
|
|
985
|
-
// Speech edit commands (#37): spoken line breaks and punctuation -> symbols
|
|
986
660
|
const VOICE_COMMANDS = [
|
|
987
661
|
[/(^|[\s,.!?])с новой строки([\s,.!?]|$)/gi, '$1\n$2'],
|
|
988
662
|
[/(^|[\s,.!?])новая строка([\s,.!?]|$)/gi, '$1\n$2'],
|
|
@@ -1003,7 +677,6 @@ window.__ModuleLoader__.load({
|
|
|
1003
677
|
return lead + trimmed + trail
|
|
1004
678
|
}
|
|
1005
679
|
|
|
1006
|
-
// Insert history for undo (#29-9). Browser-only storage.
|
|
1007
680
|
const insertHistory = []
|
|
1008
681
|
|
|
1009
682
|
async function undoLastInsert() {
|
|
@@ -1014,7 +687,6 @@ window.__ModuleLoader__.load({
|
|
|
1014
687
|
const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
|
|
1015
688
|
if (draft === last.after) actions.setDraft(last.before)
|
|
1016
689
|
else {
|
|
1017
|
-
// Draft was edited manually — cut the last insert as a substring.
|
|
1018
690
|
const i = draft.lastIndexOf(last.added)
|
|
1019
691
|
if (i < 0) { insertHistory.push(last); return t('nothingToUndo') }
|
|
1020
692
|
const spliced = draft.slice(0, i) + draft.slice(i + last.added.length)
|
|
@@ -1106,7 +778,6 @@ window.__ModuleLoader__.load({
|
|
|
1106
778
|
if (voice.settings.beep) playBeep(phase === 'start' ? 880 : 660)
|
|
1107
779
|
}
|
|
1108
780
|
|
|
1109
|
-
// Short WebAudio beep so start/stop is audible without looking (#29-6).
|
|
1110
781
|
function playBeep(freq) {
|
|
1111
782
|
try {
|
|
1112
783
|
const AC = typeof AudioContext !== 'undefined' ? AudioContext
|
|
@@ -1125,7 +796,6 @@ window.__ModuleLoader__.load({
|
|
|
1125
796
|
} catch (noAudio) { /* no audio — not critical */ }
|
|
1126
797
|
}
|
|
1127
798
|
|
|
1128
|
-
// browser recognition: in-browser, no upload, only enabled when in chain
|
|
1129
799
|
function speechRecognitionCtor() {
|
|
1130
800
|
if (typeof window === 'undefined') return null
|
|
1131
801
|
return window.SpeechRecognition || window.webkitSpeechRecognition || null
|
|
@@ -1176,7 +846,6 @@ window.__ModuleLoader__.load({
|
|
|
1176
846
|
options.onInterim(interim)
|
|
1177
847
|
}
|
|
1178
848
|
recognition.onerror = (event) => {
|
|
1179
|
-
// no-speech and aborted are normal life, not failures.
|
|
1180
849
|
const code = event && event.error
|
|
1181
850
|
if (code === 'no-speech' || code === 'aborted') return
|
|
1182
851
|
stopped = true
|
|
@@ -1184,8 +853,6 @@ window.__ModuleLoader__.load({
|
|
|
1184
853
|
if (resolveStop) { resolveStop(); resolveStop = null }
|
|
1185
854
|
options.onError(code || t('recognitionError'))
|
|
1186
855
|
}
|
|
1187
|
-
// The browser ends recognition on its own (pauses, timeout). Restart
|
|
1188
|
-
// until we stop it, otherwise dictation dies silently on the first pause.
|
|
1189
856
|
recognition.onend = () => {
|
|
1190
857
|
if (stopped) {
|
|
1191
858
|
if (resolveStop) { resolveStop(); resolveStop = null }
|
|
@@ -1216,11 +883,8 @@ window.__ModuleLoader__.load({
|
|
|
1216
883
|
}
|
|
1217
884
|
}
|
|
1218
885
|
|
|
1219
|
-
// Fetch the mode chain from the host once. Only needed to decide whether
|
|
1220
886
|
|
|
1221
887
|
voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, undoLastChunk: undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
|
|
1222
|
-
// lib/client-src/35-webgpu.js — Client-side WebGPU/WASM Whisper offline recognition
|
|
1223
|
-
// Detects WebGPU and WebAssembly availability in browser and provides offline transcription with graceful WASM fallback.
|
|
1224
888
|
|
|
1225
889
|
function isWebGpuSupported() {
|
|
1226
890
|
if (typeof navigator === 'undefined' || !navigator.gpu) return false
|
|
@@ -1254,7 +918,6 @@ function loadWebGpuWhisper(modelName, options) {
|
|
|
1254
918
|
const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
|
|
1255
919
|
if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
|
|
1256
920
|
|
|
1257
|
-
// Prefer WebGPU if available and not forced to wasm; fall back to WASM on error or when WebGPU is absent
|
|
1258
921
|
const canGpu = forceDevice === 'webgpu' || (!forceDevice && isWebGpuSupported())
|
|
1259
922
|
if (canGpu) {
|
|
1260
923
|
try {
|
|
@@ -1326,8 +989,6 @@ voice.webGpu = {
|
|
|
1326
989
|
load: loadWebGpuWhisper,
|
|
1327
990
|
clearCache: () => { _webGpuPipelines.clear() },
|
|
1328
991
|
}
|
|
1329
|
-
// lib/client-src/36-silero-vad.js — Neural Silero VAD (WASM/ONNX) client module (#279)
|
|
1330
|
-
// Provides neural voice activity detection to filter out mechanical keyboard clicks, sighs and fan noise.
|
|
1331
992
|
|
|
1332
993
|
function isSileroVadSupported() {
|
|
1333
994
|
return typeof Float32Array !== 'undefined'
|
|
@@ -1341,7 +1002,6 @@ async function initSileroVad() {
|
|
|
1341
1002
|
return _sileroLoaded
|
|
1342
1003
|
}
|
|
1343
1004
|
|
|
1344
|
-
// 512 samples at 16kHz = 32ms
|
|
1345
1005
|
function evaluateSileroFrame(pcmFrame) {
|
|
1346
1006
|
if (!pcmFrame || pcmFrame.length === 0) return { isSpeech: false, probability: 0 }
|
|
1347
1007
|
const len = pcmFrame.length
|
|
@@ -1357,8 +1017,6 @@ function evaluateSileroFrame(pcmFrame) {
|
|
|
1357
1017
|
const rms = Math.sqrt(sumSq / len)
|
|
1358
1018
|
const zcrRate = zcr / len
|
|
1359
1019
|
|
|
1360
|
-
// Neural probability estimation: vocal cords exhibit harmonic energy with moderate ZCR,
|
|
1361
|
-
// whereas mechanical switch clicks exhibit impulsive high ZCR (>0.45) with rapid decay.
|
|
1362
1020
|
if (rms < 0.015) return { isSpeech: false, probability: 0.05 }
|
|
1363
1021
|
if (zcrRate > 0.42 && rms < 0.08) return { isSpeech: false, probability: 0.12 } // click / keystroke
|
|
1364
1022
|
|
|
@@ -1373,8 +1031,6 @@ voice.sileroVad = {
|
|
|
1373
1031
|
evaluate: evaluateSileroFrame,
|
|
1374
1032
|
reset: () => { _sileroState = { h: null, c: null } },
|
|
1375
1033
|
}
|
|
1376
|
-
// lib/client-src/37-ws-stream.js — Ultra-low latency streaming WebSocket client (#282)
|
|
1377
|
-
// Streams audio frames to local SenseVoice/Whisper servers with seamless HTTP fallback.
|
|
1378
1034
|
|
|
1379
1035
|
function isWsStreamingSupported() {
|
|
1380
1036
|
return typeof WebSocket !== 'undefined'
|
|
@@ -1526,7 +1182,6 @@ voice._chainsPromise = null
|
|
|
1526
1182
|
}
|
|
1527
1183
|
})
|
|
1528
1184
|
}
|
|
1529
|
-
// ------------------------------------------------------------ recording
|
|
1530
1185
|
function teardown(rec) {
|
|
1531
1186
|
if (!rec) return
|
|
1532
1187
|
if (rec.wsStream) { try { rec.wsStream.close() } catch (_) {} rec.wsStream = null }
|
|
@@ -2031,7 +1686,6 @@ voice._chainsPromise = null
|
|
|
2031
1686
|
if (actions && typeof actions.setDraft === 'function') actions.setDraft('')
|
|
2032
1687
|
}
|
|
2033
1688
|
|
|
2034
|
-
// Voice actions (#29-7): spoken triggers perform composer actions.
|
|
2035
1689
|
function executeVoiceAction(action, rawText, target) {
|
|
2036
1690
|
const activeTarget = target || voice._activeTarget
|
|
2037
1691
|
const actions = (activeTarget && activeTarget.actions) || voice.inputActions
|
|
@@ -2082,7 +1736,6 @@ voice._chainsPromise = null
|
|
|
2082
1736
|
function keepPending() {
|
|
2083
1737
|
keepPendingPreview(voice)
|
|
2084
1738
|
}
|
|
2085
|
-
// ----------------------------------------------------------- components
|
|
2086
1739
|
function VoiceButtons(props) {
|
|
2087
1740
|
const v = useVoice()
|
|
2088
1741
|
voice.inputActions = props.inputActions
|
|
@@ -2129,7 +1782,6 @@ voice._chainsPromise = null
|
|
|
2129
1782
|
)
|
|
2130
1783
|
}
|
|
2131
1784
|
|
|
2132
|
-
// Human-readable key name.
|
|
2133
1785
|
const KEY_LABELS = {
|
|
2134
1786
|
Control: 'Ctrl', Alt: 'Alt', Shift: 'Shift', Meta: 'Win',
|
|
2135
1787
|
Escape: 'Esc',
|
|
@@ -2142,13 +1794,9 @@ voice._chainsPromise = null
|
|
|
2142
1794
|
}
|
|
2143
1795
|
if (name === 'Space') return t('keySpace')
|
|
2144
1796
|
if (KEY_LABELS[name]) return KEY_LABELS[name]
|
|
2145
|
-
// Drop the Key/Digit prefix from codes like KeyR and Digit5.
|
|
2146
1797
|
return String(name).replace(/^Key/, '').replace(/^Digit/, '')
|
|
2147
1798
|
}
|
|
2148
1799
|
|
|
2149
|
-
// What to store on key press. Pure modifiers are remembered by name:
|
|
2150
|
-
// left and right have different codes but the user means "either".
|
|
2151
|
-
// Supports combos like Control+Space or Alt+KeyV.
|
|
2152
1800
|
function keyFromEvent(event) {
|
|
2153
1801
|
if (!event) return ''
|
|
2154
1802
|
const modifiers = []
|
|
@@ -2211,13 +1859,7 @@ voice._chainsPromise = null
|
|
|
2211
1859
|
return event.code === name || event.key === name
|
|
2212
1860
|
}
|
|
2213
1861
|
|
|
2214
|
-
// Announce that the user started speaking.
|
|
2215
|
-
//
|
|
2216
|
-
// Playback should mute immediately: listening and talking at once is
|
|
2217
|
-
// impossible. There is no plugin-to-plugin API — this broadcasts a window
|
|
2218
|
-
// event that anyone may hear, so either side works alone.
|
|
2219
1862
|
|
|
2220
|
-
// --------------------------------------------------------- hold gesture
|
|
2221
1863
|
const HOLD_THRESHOLD_MS = 350
|
|
2222
1864
|
const hold = { active: false, mode: null, startedAt: 0, armed: false }
|
|
2223
1865
|
let activePointerCleanup = null
|
|
@@ -2332,8 +1974,6 @@ voice._chainsPromise = null
|
|
|
2332
1974
|
}
|
|
2333
1975
|
return cleanup
|
|
2334
1976
|
}
|
|
2335
|
-
// ------------------------------------------------------------------ 43-preview
|
|
2336
|
-
// Editable preview state machine, session pinning & UI component (#170).
|
|
2337
1977
|
|
|
2338
1978
|
function initPendingPreview(text, target, options = {}) {
|
|
2339
1979
|
const autoMs = Number(options.autoSendMs) || 4000
|
|
@@ -2371,7 +2011,6 @@ function cancelPendingPreview(voice) {
|
|
|
2371
2011
|
const target = pending.target || voice._activeTarget
|
|
2372
2012
|
const actions = (target && target.actions) || voice.inputActions
|
|
2373
2013
|
|
|
2374
|
-
// Restore untouched composer draft if preview was active
|
|
2375
2014
|
if (pending.rawText !== undefined && pending.initialDraft !== undefined) {
|
|
2376
2015
|
if (actions && typeof actions.setDraft === 'function') {
|
|
2377
2016
|
try { actions.setDraft(pending.initialDraft) } catch (_) {}
|
|
@@ -2391,7 +2030,6 @@ function keepPendingPreview(voice) {
|
|
|
2391
2030
|
const actions = (target && target.actions) || voice.inputActions
|
|
2392
2031
|
|
|
2393
2032
|
if (pending.rawText !== undefined) {
|
|
2394
|
-
// Insert final edited text into composer draft without auto-sending
|
|
2395
2033
|
const initial = pending.initialDraft || ''
|
|
2396
2034
|
const textToInsert = pending.activeText || ''
|
|
2397
2035
|
const full = initial
|
|
@@ -2411,7 +2049,6 @@ function submitPendingPreview(voice) {
|
|
|
2411
2049
|
const target = pending.target || voice._activeTarget
|
|
2412
2050
|
const actions = (target && target.actions) || voice.inputActions
|
|
2413
2051
|
|
|
2414
|
-
// Session guard (#170): verify that current session matches initiating session
|
|
2415
2052
|
const currentSessionId = (target && target.session && (target.session.sessionId || target.session.id))
|
|
2416
2053
|
|| (voice.session && (voice.session.sessionId || voice.session.id))
|
|
2417
2054
|
|| voice.currentSessionId || ''
|
|
@@ -2439,7 +2076,6 @@ function submitPendingPreview(voice) {
|
|
|
2439
2076
|
try { actions.submit() } catch (_) {}
|
|
2440
2077
|
}
|
|
2441
2078
|
} else {
|
|
2442
|
-
// Legacy path without rich preview
|
|
2443
2079
|
if (voice.settings.polishSend) {
|
|
2444
2080
|
fetch('/dsh-voice/polish', {
|
|
2445
2081
|
method: 'POST',
|
|
@@ -2472,7 +2108,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2472
2108
|
const hasPolish = p.polishedText && p.polishedText !== p.rawText
|
|
2473
2109
|
|
|
2474
2110
|
return h('div', { className: 'dvo-pill-preview', role: 'region', 'aria-label': t('editablePreview') },
|
|
2475
|
-
// Header row: tabs, audio playback, status/countdown, cancel button
|
|
2476
2111
|
h('div', { className: 'dvo-preview-header' },
|
|
2477
2112
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2478
2113
|
hasPolish
|
|
@@ -2541,7 +2176,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2541
2176
|
}, xIcon()),
|
|
2542
2177
|
),
|
|
2543
2178
|
),
|
|
2544
|
-
// Body row: editable input or preview text, Edit button, Send button
|
|
2545
2179
|
h('div', { className: 'dvo-preview-body' },
|
|
2546
2180
|
p.isEditing
|
|
2547
2181
|
? h('input', {
|
|
@@ -2578,14 +2212,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2578
2212
|
onClick: () => { submitPending(); focusComposer() },
|
|
2579
2213
|
}, sendIcon()),
|
|
2580
2214
|
),
|
|
2581
|
-
// Mismatch warning banner
|
|
2582
2215
|
p.sessionMismatch
|
|
2583
2216
|
? h('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
|
|
2584
2217
|
: null,
|
|
2585
2218
|
)
|
|
2586
2219
|
}
|
|
2587
|
-
// ------------------------------------------------------------------ 45-dictation
|
|
2588
|
-
// Dictation modes UI helper, mode cycler and labels (#264)
|
|
2589
2220
|
|
|
2590
2221
|
const DICTATION_MODES = ['clean', 'task', 'note', 'verbatim']
|
|
2591
2222
|
|
|
@@ -2603,7 +2234,6 @@ function cycleDictationMode() {
|
|
|
2603
2234
|
voice.selectedDictationMode = DICTATION_MODES[nextIdx]
|
|
2604
2235
|
voice.notify()
|
|
2605
2236
|
}
|
|
2606
|
-
// ----------------------------------------------------------- visualizers
|
|
2607
2237
|
let _accentColorCache = { color: '', expires: 0 }
|
|
2608
2238
|
let _softColorCache = { color: '', expires: 0 }
|
|
2609
2239
|
|
|
@@ -2636,7 +2266,6 @@ function cycleDictationMode() {
|
|
|
2636
2266
|
} catch (noTheme) { /* fallback */ }
|
|
2637
2267
|
return fallback || voice.waveColor || 'currentColor'
|
|
2638
2268
|
}
|
|
2639
|
-
// 1. Liquid Wave: organic multi-layer wave
|
|
2640
2269
|
function drawLiquidWave(g, w, h, levels, color, time) {
|
|
2641
2270
|
const midY = h / 2
|
|
2642
2271
|
const curLevel = levels.length ? levels[levels.length - 1] : 0
|
|
@@ -2674,7 +2303,6 @@ function cycleDictationMode() {
|
|
|
2674
2303
|
g.globalAlpha = 1
|
|
2675
2304
|
}
|
|
2676
2305
|
|
|
2677
|
-
// 2. Dynamic Orb: interactive pulsing sphere in the center
|
|
2678
2306
|
function drawDynamicOrb(g, w, h, levels, color, time) {
|
|
2679
2307
|
const cx = w / 2
|
|
2680
2308
|
const cy = h / 2
|
|
@@ -2722,7 +2350,6 @@ function cycleDictationMode() {
|
|
|
2722
2350
|
g.globalAlpha = 1
|
|
2723
2351
|
}
|
|
2724
2352
|
|
|
2725
|
-
// 3. Classic Bars: classic vertical bars
|
|
2726
2353
|
function drawClassicBars(g, w, h, levels, color) {
|
|
2727
2354
|
const midY = h / 2
|
|
2728
2355
|
for (let i = 0; i < levels.length && i * 7 < w; i++) {
|
|
@@ -2770,7 +2397,6 @@ function cycleDictationMode() {
|
|
|
2770
2397
|
return () => window.removeEventListener('keydown', onKey, true)
|
|
2771
2398
|
}, [v.phase])
|
|
2772
2399
|
|
|
2773
|
-
// VAD: accumulate silence and cut the phrase when the pause exceeds the threshold.
|
|
2774
2400
|
React.useEffect(() => {
|
|
2775
2401
|
if (v.phase !== 'recording') return
|
|
2776
2402
|
const tick = 50
|
|
@@ -2787,16 +2413,11 @@ function cycleDictationMode() {
|
|
|
2787
2413
|
const adapt = Number(voice.settings.vadAdapt) || 0
|
|
2788
2414
|
let effectiveVad = Number(voice.settings.vadSilenceMs) || 700
|
|
2789
2415
|
if (adapt > 0 && rec.hadSpeech) {
|
|
2790
|
-
// Adaptive threshold (#41): speech density over ~1s (20 samples).
|
|
2791
|
-
// Dense speech -> lower threshold (cut more precisely);
|
|
2792
|
-
// pause-heavy -> threshold rises toward base (do not cut on breaths).
|
|
2793
2416
|
const win = voice.levels.slice(-20)
|
|
2794
2417
|
const density = win.length ? win.filter((v) => v > 0.06).length / win.length : 0
|
|
2795
2418
|
const k = adapt * (density - 0.5) * 2
|
|
2796
2419
|
effectiveVad = Math.max(150, Math.round(Number(voice.settings.vadSilenceMs) * (1 - k)))
|
|
2797
2420
|
}
|
|
2798
|
-
// Continuous mode (#40): cut on a timer while speech continues,
|
|
2799
|
-
// without waiting for a long pause.
|
|
2800
2421
|
const stream = !!voice.settings.stream && rec.mode === 'dictation'
|
|
2801
2422
|
if (stream && rec.hadSpeech && !rec.cutting) {
|
|
2802
2423
|
rec.streamMs += tick
|
|
@@ -2813,7 +2434,6 @@ function cycleDictationMode() {
|
|
|
2813
2434
|
return () => dispose()
|
|
2814
2435
|
}, [v.phase])
|
|
2815
2436
|
|
|
2816
|
-
// Waveform / visualizer animation: requestAnimationFrame with HiDPI scaling
|
|
2817
2437
|
React.useEffect(() => {
|
|
2818
2438
|
if (v.phase !== 'recording') return
|
|
2819
2439
|
let frame = 0
|
|
@@ -2880,7 +2500,6 @@ function cycleDictationMode() {
|
|
|
2880
2500
|
}
|
|
2881
2501
|
}, [v.phase])
|
|
2882
2502
|
|
|
2883
|
-
// Message-mode cancel window.
|
|
2884
2503
|
React.useEffect(() => {
|
|
2885
2504
|
if (v.phase !== 'pending') return
|
|
2886
2505
|
const tick = 100
|
|
@@ -2888,15 +2507,12 @@ function cycleDictationMode() {
|
|
|
2888
2507
|
const p = voice.pending
|
|
2889
2508
|
if (!p) return
|
|
2890
2509
|
if (p.undoOnly) {
|
|
2891
|
-
// Undo-only mode (#29-5): when the window expires just hide the
|
|
2892
|
-
// panel; the text either stayed or was already undone.
|
|
2893
2510
|
p.leftMs -= tick
|
|
2894
2511
|
if (p.leftMs <= 0) voice.set({ phase: 'idle' })
|
|
2895
2512
|
else voice.notify()
|
|
2896
2513
|
return
|
|
2897
2514
|
}
|
|
2898
2515
|
if (p.paused || p.isEditing) {
|
|
2899
|
-
// Confirmation or editing mode freezes auto-send countdown (#170)
|
|
2900
2516
|
return
|
|
2901
2517
|
}
|
|
2902
2518
|
p.leftMs -= tick
|
|
@@ -2947,8 +2563,6 @@ function cycleDictationMode() {
|
|
|
2947
2563
|
const inBrowser = !!voice.browser
|
|
2948
2564
|
const rec = voice.rec
|
|
2949
2565
|
const hint = v.mode === 'dictation' ? t('dictationPill') : t('messagePill')
|
|
2950
|
-
// Live caption of what is heard right now. Until the browser emits a
|
|
2951
|
-
// final chunk the text is interim and changes on screen.
|
|
2952
2566
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
2953
2567
|
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2954
2568
|
|| voice.selectedDictationMode
|
|
@@ -3030,7 +2644,6 @@ function cycleDictationMode() {
|
|
|
3030
2644
|
}
|
|
3031
2645
|
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
3032
2646
|
if (voice.pending && voice.pending.undoOnly) {
|
|
3033
|
-
// Cancel window for delayed dictation insert (#29-5).
|
|
3034
2647
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
3035
2648
|
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, t('undo'), ': ', left, t('secondsShort')),
|
|
3036
2649
|
React.createElement('button', {
|
|
@@ -3110,10 +2723,7 @@ function cycleDictationMode() {
|
|
|
3110
2723
|
)
|
|
3111
2724
|
}
|
|
3112
2725
|
|
|
3113
|
-
// --------------------------------------------------------------- slots
|
|
3114
2726
|
function registerComposer(ctx) {
|
|
3115
|
-
// The hotkey lives for as long as the plugin is applied and can change
|
|
3116
|
-
// without restart: the settings card broadcasts and the composer reloads.
|
|
3117
2727
|
ctx.effect(() => {
|
|
3118
2728
|
let dispose = () => {}
|
|
3119
2729
|
let alive = true
|
|
@@ -3128,7 +2738,6 @@ function cycleDictationMode() {
|
|
|
3128
2738
|
const key = data && data.hotkey
|
|
3129
2739
|
voice.hotkey = key || ''
|
|
3130
2740
|
if (key) dispose = installHotkey(ctx, key, 'message')
|
|
3131
|
-
// The composer needs fresh settings without opening the card.
|
|
3132
2741
|
Object.assign(voice.settings, {
|
|
3133
2742
|
beep: !!(data && data.beep),
|
|
3134
2743
|
localOnly: !!(data && data.localOnly),
|
|
@@ -3176,9 +2785,6 @@ function cycleDictationMode() {
|
|
|
3176
2785
|
}
|
|
3177
2786
|
}, 'dsh-voice: hold hotkey')
|
|
3178
2787
|
|
|
3179
|
-
// locale: NS is required — the core resolves `label` through the inject face,
|
|
3180
|
-
// and without it the label throws "cannot get property t without inject" while
|
|
3181
|
-
// the slot snapshot is built, breaking the whole client batch.
|
|
3182
2788
|
ctx.effect(() => {
|
|
3183
2789
|
const offRight = ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
3184
2790
|
{ name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, locale: NS, label: () => t('title') },
|
|
@@ -3195,14 +2801,11 @@ function cycleDictationMode() {
|
|
|
3195
2801
|
}, 'dsh-voice: composer slots')
|
|
3196
2802
|
}
|
|
3197
2803
|
|
|
3198
|
-
// ------------------------------------------------------- settings page
|
|
3199
2804
|
const BUILTIN = [
|
|
3200
2805
|
'browser', 'browser-webgpu', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
3201
|
-
// Presets: address and model are filled on the host; only a key is needed.
|
|
3202
2806
|
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
3203
2807
|
]
|
|
3204
2808
|
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
3205
|
-
// Resolve at render time: t() must not be captured before locale bind.
|
|
3206
2809
|
const MODEL_HINT_KEYS = {
|
|
3207
2810
|
browser: 'browserHint',
|
|
3208
2811
|
'browser-webgpu': 'browserWebGpuHint',
|
|
@@ -3294,7 +2897,6 @@ function cycleDictationMode() {
|
|
|
3294
2897
|
}
|
|
3295
2898
|
const ErrorBoundary = createErrorBoundary()
|
|
3296
2899
|
|
|
3297
|
-
// One chain editor: provider+model rows with reordering.
|
|
3298
2900
|
function ChainEditor(props) {
|
|
3299
2901
|
const t = (props && props.t) || moduleT
|
|
3300
2902
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
@@ -3335,7 +2937,6 @@ function cycleDictationMode() {
|
|
|
3335
2937
|
)
|
|
3336
2938
|
}
|
|
3337
2939
|
|
|
3338
|
-
// Custom providers: name, API template, where to call, how to authorize.
|
|
3339
2940
|
function CustomEditor(props) {
|
|
3340
2941
|
const t = (props && props.t) || moduleT
|
|
3341
2942
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
@@ -3384,7 +2985,6 @@ function cycleDictationMode() {
|
|
|
3384
2985
|
),
|
|
3385
2986
|
)
|
|
3386
2987
|
}
|
|
3387
|
-
// Provider Telemetry & Latency Dashboard (#266)
|
|
3388
2988
|
function ProviderDashboard(props) {
|
|
3389
2989
|
const t = props.t || moduleT
|
|
3390
2990
|
const statsData = props.statsData || {}
|
|
@@ -3454,13 +3054,13 @@ function cycleDictationMode() {
|
|
|
3454
3054
|
badgeText = t('error')
|
|
3455
3055
|
} else if (avg > 0 && avg < 400) {
|
|
3456
3056
|
badgeClass = 'dvo-badge-fast'
|
|
3457
|
-
badgeText = avg + '
|
|
3057
|
+
badgeText = avg + ' ' + t('msShort') + ' · ' + t('fast')
|
|
3458
3058
|
} else if (avg >= 400 && avg <= 1500) {
|
|
3459
3059
|
badgeClass = 'dvo-badge-norm'
|
|
3460
|
-
badgeText = avg + '
|
|
3060
|
+
badgeText = avg + ' ' + t('msShort') + ' · ' + t('normal')
|
|
3461
3061
|
} else {
|
|
3462
3062
|
badgeClass = 'dvo-badge-slow'
|
|
3463
|
-
badgeText = avg + '
|
|
3063
|
+
badgeText = avg + ' ' + t('msShort') + ' · ' + t('slow')
|
|
3464
3064
|
}
|
|
3465
3065
|
}
|
|
3466
3066
|
const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
|
|
@@ -3478,7 +3078,6 @@ function cycleDictationMode() {
|
|
|
3478
3078
|
}),
|
|
3479
3079
|
),
|
|
3480
3080
|
),
|
|
3481
|
-
// Per-recognition telemetry breakdown list (#266)
|
|
3482
3081
|
h('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '10px' } },
|
|
3483
3082
|
h('div', { className: 'dvs-h', style: { marginBottom: '8px' } }, t('recentRecognitionsTitle')),
|
|
3484
3083
|
recentDiagnostics.length === 0
|
|
@@ -3515,8 +3114,6 @@ function cycleDictationMode() {
|
|
|
3515
3114
|
),
|
|
3516
3115
|
)
|
|
3517
3116
|
}
|
|
3518
|
-
// ------------------------------------------------------------------ 72-dictation-modes
|
|
3519
|
-
// Dictation modes configuration card and prompt template editors (#264)
|
|
3520
3117
|
|
|
3521
3118
|
function DictationModesSection(props) {
|
|
3522
3119
|
const { t, draft, setTop, writable } = props
|
|
@@ -3571,7 +3168,7 @@ function DictationModesSection(props) {
|
|
|
3571
3168
|
className: 'cb-input',
|
|
3572
3169
|
style: { minHeight: '48px', resize: 'vertical' },
|
|
3573
3170
|
value: prompts.clean || '',
|
|
3574
|
-
placeholder: '
|
|
3171
|
+
placeholder: t('promptCleanPlaceholder'),
|
|
3575
3172
|
disabled: !writable,
|
|
3576
3173
|
onChange: (e) => setPrompt('clean', e.target.value),
|
|
3577
3174
|
}),
|
|
@@ -3582,7 +3179,7 @@ function DictationModesSection(props) {
|
|
|
3582
3179
|
className: 'cb-input',
|
|
3583
3180
|
style: { minHeight: '48px', resize: 'vertical' },
|
|
3584
3181
|
value: prompts.task || '',
|
|
3585
|
-
placeholder: '
|
|
3182
|
+
placeholder: t('promptTaskPlaceholder'),
|
|
3586
3183
|
disabled: !writable,
|
|
3587
3184
|
onChange: (e) => setPrompt('task', e.target.value),
|
|
3588
3185
|
}),
|
|
@@ -3593,7 +3190,7 @@ function DictationModesSection(props) {
|
|
|
3593
3190
|
className: 'cb-input',
|
|
3594
3191
|
style: { minHeight: '48px', resize: 'vertical' },
|
|
3595
3192
|
value: prompts.note || '',
|
|
3596
|
-
placeholder: '
|
|
3193
|
+
placeholder: t('promptNotePlaceholder'),
|
|
3597
3194
|
disabled: !writable,
|
|
3598
3195
|
onChange: (e) => setPrompt('note', e.target.value),
|
|
3599
3196
|
}),
|
|
@@ -3602,7 +3199,6 @@ function DictationModesSection(props) {
|
|
|
3602
3199
|
),
|
|
3603
3200
|
)
|
|
3604
3201
|
}
|
|
3605
|
-
// 5. Hardware & Audio Engines Card
|
|
3606
3202
|
function HardwareOptionsCard(props) {
|
|
3607
3203
|
const {
|
|
3608
3204
|
t = moduleT, draft, setTop, writable, catching, setCatching,
|
|
@@ -3628,14 +3224,27 @@ function DictationModesSection(props) {
|
|
|
3628
3224
|
.then((d) => {
|
|
3629
3225
|
setBenchmarking(false)
|
|
3630
3226
|
if (d && d.ok) setBenchmarkResult(d)
|
|
3631
|
-
else setBenchmarkErr((d && d.error && d.error.message) || '
|
|
3227
|
+
else setBenchmarkErr((d && d.error && d.error.message) || t('benchmarkFailed'))
|
|
3632
3228
|
})
|
|
3633
3229
|
.catch((e) => {
|
|
3634
3230
|
setBenchmarking(false)
|
|
3635
|
-
setBenchmarkErr((e && e.message) || '
|
|
3231
|
+
setBenchmarkErr((e && e.message) || t('networkError'))
|
|
3636
3232
|
})
|
|
3637
3233
|
}
|
|
3638
3234
|
|
|
3235
|
+
const ratingLabel = (rating) => {
|
|
3236
|
+
const key = { 'Ultra-fast': 'benchRatingUltraFast', 'Fast': 'fast', 'Real-time': 'benchRatingRealtime', 'Slow': 'slow' }[rating]
|
|
3237
|
+
return key ? t(key) : String(rating || '')
|
|
3238
|
+
}
|
|
3239
|
+
const HOST_BENCH_MESSAGES = {
|
|
3240
|
+
'Local ASR server is not running. Hardware detected.': 'benchmarkNoAsrServer',
|
|
3241
|
+
}
|
|
3242
|
+
const benchmarkMessage = (result) => {
|
|
3243
|
+
const raw = String(result && result.message || '')
|
|
3244
|
+
const key = HOST_BENCH_MESSAGES[raw]
|
|
3245
|
+
return (key && t(key)) || raw || t('benchmarkOffline')
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3639
3248
|
const textField = (key, label, hint) => h('div', { className: 'cb-field' },
|
|
3640
3249
|
h('label', null, label),
|
|
3641
3250
|
h('input', {
|
|
@@ -3650,7 +3259,6 @@ function DictationModesSection(props) {
|
|
|
3650
3259
|
h('span', null, '⚙️ ' + t('hardwareTitle')),
|
|
3651
3260
|
),
|
|
3652
3261
|
h('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
|
|
3653
|
-
// Hotkey picker
|
|
3654
3262
|
h('div', { className: 'cb-field' },
|
|
3655
3263
|
h('label', null, t('hotkey')),
|
|
3656
3264
|
h('div', { className: 'cb-row' },
|
|
@@ -3665,7 +3273,6 @@ function DictationModesSection(props) {
|
|
|
3665
3273
|
),
|
|
3666
3274
|
h('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
3667
3275
|
),
|
|
3668
|
-
// Mic & noise gate
|
|
3669
3276
|
h('div', { className: 'cb-field' },
|
|
3670
3277
|
h('label', null, t('mic')),
|
|
3671
3278
|
h('div', { className: 'cb-row' },
|
|
@@ -3712,10 +3319,10 @@ function DictationModesSection(props) {
|
|
|
3712
3319
|
style: { flex: 1 },
|
|
3713
3320
|
},
|
|
3714
3321
|
h('option', { value: '-999' }, t('noiseGateOff')),
|
|
3715
|
-
h('option', { value: '-50' },
|
|
3716
|
-
h('option', { value: '-45' },
|
|
3717
|
-
h('option', { value: '-35' },
|
|
3718
|
-
h('option', { value: '-25' },
|
|
3322
|
+
h('option', { value: '-50' }, t('noiseGateLow')),
|
|
3323
|
+
h('option', { value: '-45' }, t('noiseGateStandard')),
|
|
3324
|
+
h('option', { value: '-35' }, t('noiseGateMedium')),
|
|
3325
|
+
h('option', { value: '-25' }, t('noiseGateHigh')),
|
|
3719
3326
|
!['-999', '-50', '-45', '-35', '-25'].includes(String(gateDbVal)) ? h('option', { value: String(gateDbVal) }, `${gateDbVal} dB (${t('calibratedValue') || 'Calibrated'})`) : null,
|
|
3720
3327
|
),
|
|
3721
3328
|
),
|
|
@@ -3742,7 +3349,6 @@ function DictationModesSection(props) {
|
|
|
3742
3349
|
),
|
|
3743
3350
|
h('span', { className: 'dvs-sub' }, t('streamingWsHint')),
|
|
3744
3351
|
),
|
|
3745
|
-
// Endpoint text fields
|
|
3746
3352
|
h('div', { className: 'cb-grid-2' },
|
|
3747
3353
|
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
3748
3354
|
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
@@ -3755,7 +3361,6 @@ function DictationModesSection(props) {
|
|
|
3755
3361
|
textField('polishModel', t('polishModel'), ''),
|
|
3756
3362
|
textField('polishKeyEnv', t('polishKeyEnv'), ''),
|
|
3757
3363
|
),
|
|
3758
|
-
// Engine checkboxes
|
|
3759
3364
|
h('div', { className: 'cb-row' },
|
|
3760
3365
|
h('label', { className: 'cb-check-label' },
|
|
3761
3366
|
h('input', {
|
|
@@ -3821,7 +3426,6 @@ function DictationModesSection(props) {
|
|
|
3821
3426
|
t('normalizeTranscript'),
|
|
3822
3427
|
),
|
|
3823
3428
|
),
|
|
3824
|
-
// Visualizer select
|
|
3825
3429
|
h('div', { className: 'cb-field' },
|
|
3826
3430
|
h('label', null, t('visualizerStyle')),
|
|
3827
3431
|
h('select', {
|
|
@@ -3834,7 +3438,6 @@ function DictationModesSection(props) {
|
|
|
3834
3438
|
h('option', { value: 'off' }, t('visOff')),
|
|
3835
3439
|
),
|
|
3836
3440
|
),
|
|
3837
|
-
// Hardware Acceleration (SBC NPU / Host)
|
|
3838
3441
|
h('div', { className: 'cb-field' },
|
|
3839
3442
|
h('label', null, t('npuProvider')),
|
|
3840
3443
|
h('div', { className: 'cb-row' },
|
|
@@ -3844,7 +3447,7 @@ function DictationModesSection(props) {
|
|
|
3844
3447
|
onChange: (e) => setTop('sensevoiceProvider', e.target.value),
|
|
3845
3448
|
style: { flex: 1 },
|
|
3846
3449
|
},
|
|
3847
|
-
h('option', { value: 'cpu' },
|
|
3450
|
+
h('option', { value: 'cpu' }, t('npuCpu')),
|
|
3848
3451
|
h('option', { value: 'rknn' }, 'Rockchip RK3588 NPU (RKNN)'),
|
|
3849
3452
|
h('option', { value: 'coreml' }, 'Apple Silicon CoreML'),
|
|
3850
3453
|
h('option', { value: 'openvino' }, 'Intel OpenVINO'),
|
|
@@ -3863,7 +3466,6 @@ function DictationModesSection(props) {
|
|
|
3863
3466
|
),
|
|
3864
3467
|
h('span', { className: 'dvs-sub' }, t('npuProviderHint')),
|
|
3865
3468
|
),
|
|
3866
|
-
// Hardware Benchmark
|
|
3867
3469
|
h('div', { className: 'cb-field' },
|
|
3868
3470
|
h('div', { className: 'cb-row', style: { alignItems: 'center', justifyContent: 'space-between' } },
|
|
3869
3471
|
h('label', { style: { marginBottom: 0 } }, t('benchmarkTitle')),
|
|
@@ -3876,12 +3478,11 @@ function DictationModesSection(props) {
|
|
|
3876
3478
|
? h('div', {
|
|
3877
3479
|
style: { marginTop: '8px', padding: '6px 10px', borderRadius: '6px', fontSize: '12px', backgroundColor: 'var(--dsw-alias-fill-quaternary)' }
|
|
3878
3480
|
}, benchmarkResult.running
|
|
3879
|
-
? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs}
|
|
3880
|
-
: `ℹ ${benchmarkResult
|
|
3481
|
+
? `⚡ RTF: ${benchmarkResult.rtf}x · ${benchmarkResult.latencyMs} ${t('msShort')} (${ratingLabel(benchmarkResult.rating)}) · ${String(benchmarkResult.provider || 'cpu').toUpperCase()} · ${t('benchmarkCores')}: ${benchmarkResult.cpuCores}`
|
|
3482
|
+
: `ℹ ${benchmarkMessage(benchmarkResult)} (${benchmarkResult.cpuCores} ${t('benchmarkCoresUnit')}, ${benchmarkResult.platform})`)
|
|
3881
3483
|
: (benchmarkErr ? h('div', { style: { color: 'var(--dsw-alias-state-danger)', fontSize: '12px', marginTop: '4px' } }, benchmarkErr) : null),
|
|
3882
3484
|
h('span', { className: 'dvs-sub' }, t('benchmarkHint')),
|
|
3883
3485
|
),
|
|
3884
|
-
// WebGPU Status Indicator
|
|
3885
3486
|
h('div', { className: 'cb-field' },
|
|
3886
3487
|
h('label', null, t('webgpuStatus')),
|
|
3887
3488
|
h('div', {
|
|
@@ -3898,7 +3499,6 @@ function DictationModesSection(props) {
|
|
|
3898
3499
|
? ('✓ ' + t('webgpuAvailable'))
|
|
3899
3500
|
: ('ℹ ' + t('webgpuNotAvailable'))),
|
|
3900
3501
|
),
|
|
3901
|
-
// Vocabulary textarea
|
|
3902
3502
|
h('div', { className: 'cb-field' },
|
|
3903
3503
|
h('label', null, t('vocabulary')),
|
|
3904
3504
|
h('textarea', {
|
|
@@ -3909,7 +3509,6 @@ function DictationModesSection(props) {
|
|
|
3909
3509
|
),
|
|
3910
3510
|
)
|
|
3911
3511
|
}
|
|
3912
|
-
// Custom hooks for VoiceSection modularity
|
|
3913
3512
|
function useSensevoiceInstaller(fetchStatus) {
|
|
3914
3513
|
const [sensevoiceState, setSensevoiceState] = React.useState(null)
|
|
3915
3514
|
const [installingSensevoice, setInstallingSensevoice] = React.useState(false)
|
|
@@ -3979,7 +3578,7 @@ function DictationModesSection(props) {
|
|
|
3979
3578
|
if (devId) audio.deviceId = { exact: devId }
|
|
3980
3579
|
stream = await navigator.mediaDevices.getUserMedia({ audio })
|
|
3981
3580
|
const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
3982
|
-
if (!AC) throw new Error('
|
|
3581
|
+
if (!AC) throw new Error(t('audioContextUnsupported'))
|
|
3983
3582
|
audioCtx = new AC()
|
|
3984
3583
|
if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
|
|
3985
3584
|
const src = audioCtx.createMediaStreamSource(stream)
|
|
@@ -4176,7 +3775,6 @@ function DictationModesSection(props) {
|
|
|
4176
3775
|
|
|
4177
3776
|
return { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow }
|
|
4178
3777
|
}
|
|
4179
|
-
// Developer Lexicon & IT Jargon
|
|
4180
3778
|
function JargonEditor(props) {
|
|
4181
3779
|
const t = props.t || moduleT
|
|
4182
3780
|
const draft = props.draft
|
|
@@ -4230,8 +3828,6 @@ function DictationModesSection(props) {
|
|
|
4230
3828
|
),
|
|
4231
3829
|
)
|
|
4232
3830
|
}
|
|
4233
|
-
// ------------------------------------------------------------------ 72-macros
|
|
4234
|
-
// Voice action macros configuration card (#167)
|
|
4235
3831
|
|
|
4236
3832
|
function MacrosSection(props) {
|
|
4237
3833
|
const { t, draft, setTop, writable } = props
|
|
@@ -4266,7 +3862,7 @@ function MacrosSection(props) {
|
|
|
4266
3862
|
h('div', { className: 'cb-field-desc' }, t('voiceMacrosDesc')),
|
|
4267
3863
|
macros.length === 0
|
|
4268
3864
|
? h('div', { style: { fontStyle: 'italic', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', marginTop: '6px' } },
|
|
4269
|
-
'
|
|
3865
|
+
t('macroEmpty'),
|
|
4270
3866
|
)
|
|
4271
3867
|
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px' } },
|
|
4272
3868
|
macros.map((m, idx) =>
|
|
@@ -4292,7 +3888,7 @@ function MacrosSection(props) {
|
|
|
4292
3888
|
h('textarea', {
|
|
4293
3889
|
className: 'cb-input',
|
|
4294
3890
|
style: { minHeight: '38px', resize: 'vertical', fontSize: '12px' },
|
|
4295
|
-
placeholder: t('macroTemplate') + '
|
|
3891
|
+
placeholder: t('macroTemplate') + t('macroTemplateArgHint'),
|
|
4296
3892
|
value: m.template || '',
|
|
4297
3893
|
disabled: !writable,
|
|
4298
3894
|
onChange: (e) => updateMacro(idx, 'template', e.target.value),
|
|
@@ -4302,7 +3898,6 @@ function MacrosSection(props) {
|
|
|
4302
3898
|
),
|
|
4303
3899
|
)
|
|
4304
3900
|
}
|
|
4305
|
-
// Local ASR (SenseVoice-Small & Whisper Large-v3-Turbo 1-Click Setup)
|
|
4306
3901
|
function SensevoiceSection(props) {
|
|
4307
3902
|
const t = props.t || moduleT
|
|
4308
3903
|
const sensevoiceState = props.sensevoiceState
|
|
@@ -4331,8 +3926,8 @@ function MacrosSection(props) {
|
|
|
4331
3926
|
onChange: (e) => setModelChoice(e.target.value),
|
|
4332
3927
|
style: { flex: 1, fontSize: '12px', minWidth: '180px' },
|
|
4333
3928
|
},
|
|
4334
|
-
h('option', { value: 'sensevoice' },
|
|
4335
|
-
h('option', { value: 'whisper-turbo' },
|
|
3929
|
+
h('option', { value: 'sensevoice' }, t('installModelSensevoice')),
|
|
3930
|
+
h('option', { value: 'whisper-turbo' }, t('installModelWhisperTurbo')),
|
|
4336
3931
|
),
|
|
4337
3932
|
h('button', {
|
|
4338
3933
|
type: 'button',
|
|
@@ -4360,7 +3955,6 @@ function MacrosSection(props) {
|
|
|
4360
3955
|
)
|
|
4361
3956
|
)
|
|
4362
3957
|
}
|
|
4363
|
-
// 1. Connection & Engine Status Card
|
|
4364
3958
|
function ConnectionStatusCard(props) {
|
|
4365
3959
|
const t = props.t || moduleT
|
|
4366
3960
|
const hostOnline = props.hostOnline
|
|
@@ -4386,7 +3980,7 @@ function MacrosSection(props) {
|
|
|
4386
3980
|
),
|
|
4387
3981
|
h('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
4388
3982
|
h('span', { className: 'cb-badge ' + (hostOnline ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
4389
|
-
hostOnline ? (`✓ ${t('hostOnline')} (${statusLatency}
|
|
3983
|
+
hostOnline ? (`✓ ${t('hostOnline')} (${statusLatency} ${t('msShort')})`) : (`✗ ${t('hostOffline')}`)
|
|
4390
3984
|
),
|
|
4391
3985
|
h('span', { className: 'cb-badge ' + (whisperRunning ? 'cb-badge-ok' : 'cb-badge-warn') },
|
|
4392
3986
|
whisperRunning ? (`✓ ${t('whisperReady')}`) : (`· ${t('whisperStopped')}`)
|
|
@@ -4395,7 +3989,6 @@ function MacrosSection(props) {
|
|
|
4395
3989
|
),
|
|
4396
3990
|
)
|
|
4397
3991
|
}
|
|
4398
|
-
// Plugin Version & Updates Card
|
|
4399
3992
|
function UpdaterCard(props) {
|
|
4400
3993
|
const t = props.t || moduleT
|
|
4401
3994
|
const updaterStatus = props.updaterStatus
|
|
@@ -4454,15 +4047,9 @@ function MacrosSection(props) {
|
|
|
4454
4047
|
: null,
|
|
4455
4048
|
)
|
|
4456
4049
|
}
|
|
4457
|
-
// ------------------------------------------------------- settings voice section
|
|
4458
4050
|
function VoiceSection(props) {
|
|
4459
4051
|
const t = (props && props.t) || moduleT
|
|
4460
4052
|
const ctx = (props && props.ctx) || {}
|
|
4461
|
-
// On a non-localhost page the kernel disables settings entirely: the
|
|
4462
|
-
// shared document is not readable, every section gets "unavailable" and
|
|
4463
|
-
// writes are dropped. The server does not share that restriction, and
|
|
4464
|
-
// dsh-lanmode rebuilds the same mechanics on the same calls. Prefer its
|
|
4465
|
-
// service when installed, otherwise the kernel one (fine on loopback).
|
|
4466
4053
|
const settingsService = (ctx.get && ctx.get('lanSettings')) || ctx.configForms || ctx.settingsScope
|
|
4467
4054
|
const scope = settingsService && typeof settingsService.get === 'function'
|
|
4468
4055
|
? settingsService.get(NS)
|
|
@@ -4492,7 +4079,6 @@ function MacrosSection(props) {
|
|
|
4492
4079
|
})
|
|
4493
4080
|
return () => { alive = false }
|
|
4494
4081
|
}, [])
|
|
4495
|
-
// Hotkey picker: not a name field — press the key you want.
|
|
4496
4082
|
const [catching, setCatching] = React.useState(false)
|
|
4497
4083
|
const [jargonInput, setJargonInput] = React.useState(null)
|
|
4498
4084
|
const [showWizard, setShowWizard] = React.useState(false)
|
|
@@ -4674,9 +4260,9 @@ function MacrosSection(props) {
|
|
|
4674
4260
|
if (data.config) setServerConfig(data.config)
|
|
4675
4261
|
if (typeof data.revision === 'number') setRevision(data.revision)
|
|
4676
4262
|
} else if (res.status === 409) {
|
|
4677
|
-
httpError = data?.error?.message || '
|
|
4263
|
+
httpError = data?.error?.message || t('settingsConflict')
|
|
4678
4264
|
} else {
|
|
4679
|
-
httpError = data?.error?.message || data?.error || res.statusText || '
|
|
4265
|
+
httpError = data?.error?.message || data?.error || res.statusText || t('saveFailedFallback')
|
|
4680
4266
|
}
|
|
4681
4267
|
} catch (e) {
|
|
4682
4268
|
httpError = e?.message || String(e)
|
|
@@ -4778,10 +4364,7 @@ function MacrosSection(props) {
|
|
|
4778
4364
|
onOpenWizard: () => setShowWizard((v) => !v),
|
|
4779
4365
|
}),
|
|
4780
4366
|
showWizard ? h(FirstRunWizard, {
|
|
4781
|
-
t
|
|
4782
|
-
draft: draft,
|
|
4783
|
-
setTop: setTop,
|
|
4784
|
-
writable: writable,
|
|
4367
|
+
t, draft, setTop, writable,
|
|
4785
4368
|
onClose: () => setShowWizard(false),
|
|
4786
4369
|
}) : null,
|
|
4787
4370
|
h('div', { className: 'cb-section-card' },
|
|
@@ -4877,10 +4460,7 @@ function MacrosSection(props) {
|
|
|
4877
4460
|
}),
|
|
4878
4461
|
),
|
|
4879
4462
|
h(HardwareOptionsCard, {
|
|
4880
|
-
t
|
|
4881
|
-
draft: draft,
|
|
4882
|
-
setTop: setTop,
|
|
4883
|
-
writable: writable,
|
|
4463
|
+
t, draft, setTop, writable,
|
|
4884
4464
|
catching: catching,
|
|
4885
4465
|
setCatching: setCatching,
|
|
4886
4466
|
devices: devices,
|
|
@@ -4974,25 +4554,14 @@ function MacrosSection(props) {
|
|
|
4974
4554
|
),
|
|
4975
4555
|
),
|
|
4976
4556
|
),
|
|
4977
|
-
// Dictation Modes & Prompts (#264)
|
|
4978
4557
|
h(DictationModesSection, {
|
|
4979
|
-
t
|
|
4980
|
-
draft: draft,
|
|
4981
|
-
setTop: setTop,
|
|
4982
|
-
writable: writable,
|
|
4558
|
+
t, draft, setTop, writable,
|
|
4983
4559
|
}),
|
|
4984
|
-
// Voice Action Macros (#167)
|
|
4985
4560
|
h(MacrosSection, {
|
|
4986
|
-
t
|
|
4987
|
-
draft: draft,
|
|
4988
|
-
setTop: setTop,
|
|
4989
|
-
writable: writable,
|
|
4561
|
+
t, draft, setTop, writable,
|
|
4990
4562
|
}),
|
|
4991
4563
|
h(JargonEditor, {
|
|
4992
|
-
t
|
|
4993
|
-
draft: draft,
|
|
4994
|
-
setTop: setTop,
|
|
4995
|
-
writable: writable,
|
|
4564
|
+
t, draft, setTop, writable,
|
|
4996
4565
|
jargonInput: jargonInput,
|
|
4997
4566
|
setJargonInput: setJargonInput,
|
|
4998
4567
|
}),
|
|
@@ -5019,7 +4588,6 @@ function MacrosSection(props) {
|
|
|
5019
4588
|
),
|
|
5020
4589
|
)
|
|
5021
4590
|
}
|
|
5022
|
-
// ------------------------------------------------------- first-run wizard (#265)
|
|
5023
4591
|
function FirstRunWizard(props) {
|
|
5024
4592
|
const t = props.t || moduleT
|
|
5025
4593
|
const draft = props.draft
|
|
@@ -5030,6 +4598,16 @@ function MacrosSection(props) {
|
|
|
5030
4598
|
const isSecure = typeof window !== 'undefined' ? (window.isSecureContext || window.location.protocol === 'https:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') : true
|
|
5031
4599
|
const hasMediaDevices = typeof navigator !== 'undefined' && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function'
|
|
5032
4600
|
const hasMediaRecorder = typeof MediaRecorder !== 'undefined'
|
|
4601
|
+
const isDesktop = typeof navigator !== 'undefined' && (
|
|
4602
|
+
/dsh-desktop|electron/i.test(navigator.userAgent || '') ||
|
|
4603
|
+
(typeof window !== 'undefined' && window.location.protocol === 'dsh-app:')
|
|
4604
|
+
)
|
|
4605
|
+
const isWindows = typeof navigator !== 'undefined' && (
|
|
4606
|
+
/win/i.test(navigator.platform || '') || /windows/i.test(navigator.userAgent || '')
|
|
4607
|
+
)
|
|
4608
|
+
const isMac = typeof navigator !== 'undefined' && (
|
|
4609
|
+
/mac/i.test(navigator.platform || '') || /macintosh|mac os x/i.test(navigator.userAgent || '')
|
|
4610
|
+
)
|
|
5033
4611
|
|
|
5034
4612
|
const [micPermission, setMicPermission] = React.useState('prompt')
|
|
5035
4613
|
const [devices, setDevices] = React.useState([])
|
|
@@ -5104,7 +4682,16 @@ function MacrosSection(props) {
|
|
|
5104
4682
|
const constraints = {
|
|
5105
4683
|
audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
|
|
5106
4684
|
}
|
|
5107
|
-
|
|
4685
|
+
let stream
|
|
4686
|
+
try {
|
|
4687
|
+
stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
4688
|
+
} catch (firstErr) {
|
|
4689
|
+
if (selectedDev) {
|
|
4690
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
4691
|
+
} else {
|
|
4692
|
+
throw firstErr
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
5108
4695
|
signalStreamRef.current = stream
|
|
5109
4696
|
setMicPermission('granted')
|
|
5110
4697
|
refreshDevices()
|
|
@@ -5157,7 +4744,15 @@ function MacrosSection(props) {
|
|
|
5157
4744
|
const constraints = {
|
|
5158
4745
|
audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
|
|
5159
4746
|
}
|
|
5160
|
-
|
|
4747
|
+
try {
|
|
4748
|
+
testStream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
4749
|
+
} catch (firstErr) {
|
|
4750
|
+
if (selectedDev) {
|
|
4751
|
+
testStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
4752
|
+
} else {
|
|
4753
|
+
throw firstErr
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
5161
4756
|
let mime = 'audio/webm;codecs=opus'
|
|
5162
4757
|
if (!MediaRecorder.isTypeSupported(mime)) mime = ''
|
|
5163
4758
|
recorder = mime ? new MediaRecorder(testStream, { mimeType: mime }) : new MediaRecorder(testStream)
|
|
@@ -5230,7 +4825,6 @@ function MacrosSection(props) {
|
|
|
5230
4825
|
}
|
|
5231
4826
|
}
|
|
5232
4827
|
|
|
5233
|
-
// Determine active chain info
|
|
5234
4828
|
const dictationChain = (draft && draft.dictation && draft.dictation.chain) || []
|
|
5235
4829
|
const activeProvider = (dictationChain[0] && (dictationChain[0].provider || dictationChain[0])) || 'sensevoice'
|
|
5236
4830
|
let areaLabel = t('wizardAreaServer')
|
|
@@ -5241,10 +4835,19 @@ function MacrosSection(props) {
|
|
|
5241
4835
|
}
|
|
5242
4836
|
|
|
5243
4837
|
let nextStepHint = ''
|
|
4838
|
+
const isPermDenied = micPermission === 'denied' || (testError && /permission|denied|notallowed/i.test(testError))
|
|
5244
4839
|
if (!isSecure) {
|
|
5245
4840
|
nextStepHint = t('wizardHintHttps')
|
|
5246
|
-
} else if (
|
|
5247
|
-
|
|
4841
|
+
} else if (isPermDenied) {
|
|
4842
|
+
if (isDesktop && isWindows) {
|
|
4843
|
+
nextStepHint = t('wizardHintWinDesktop')
|
|
4844
|
+
} else if (isDesktop && isMac) {
|
|
4845
|
+
nextStepHint = t('wizardHintMacDesktop')
|
|
4846
|
+
} else if (isWindows) {
|
|
4847
|
+
nextStepHint = t('wizardHintWinBrowser')
|
|
4848
|
+
} else {
|
|
4849
|
+
nextStepHint = t('wizardHintPermission')
|
|
4850
|
+
}
|
|
5248
4851
|
} else if (peakRms === 0 && signalTesting) {
|
|
5249
4852
|
nextStepHint = t('wizardHintDevice')
|
|
5250
4853
|
} else if (testError) {
|
|
@@ -5263,7 +4866,6 @@ function MacrosSection(props) {
|
|
|
5263
4866
|
),
|
|
5264
4867
|
h('div', { className: 'cb-section-desc' }, t('wizardDesc')),
|
|
5265
4868
|
|
|
5266
|
-
// Step 1: Environment & Permissions
|
|
5267
4869
|
h('div', { className: 'dvs-card' },
|
|
5268
4870
|
h('div', { className: 'dvs-h' }, t('wizardStep1')),
|
|
5269
4871
|
h('div', { className: 'cb-row' },
|
|
@@ -5292,19 +4894,25 @@ function MacrosSection(props) {
|
|
|
5292
4894
|
stopSignalTest()
|
|
5293
4895
|
},
|
|
5294
4896
|
},
|
|
5295
|
-
h('option', { value: '' }, '
|
|
5296
|
-
devices.map((d) => h('option', { key: d.deviceId, value: d.deviceId }, d.label ||
|
|
4897
|
+
h('option', { value: '' }, t('micDefault')),
|
|
4898
|
+
devices.map((d) => h('option', { key: d.deviceId, value: d.deviceId }, d.label || (t('mic') + ' ' + d.deviceId.slice(0, 8) + '…')))
|
|
5297
4899
|
),
|
|
5298
4900
|
h('button', {
|
|
5299
4901
|
type: 'button',
|
|
5300
4902
|
className: 'cb-btn',
|
|
5301
4903
|
onClick: requestPermissionAndTestSignal,
|
|
5302
4904
|
}, t('wizardRequestMic')),
|
|
4905
|
+
(isWindows && isPermDenied) ? h('a', {
|
|
4906
|
+
href: 'ms-settings:privacy-microphone',
|
|
4907
|
+
target: '_blank',
|
|
4908
|
+
rel: 'noopener noreferrer',
|
|
4909
|
+
className: 'cb-btn cb-btn-mini',
|
|
4910
|
+
style: { marginLeft: '8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
4911
|
+
}, '⚙ ' + t('wizardOpenWinMicSettings')) : null,
|
|
5303
4912
|
),
|
|
5304
4913
|
),
|
|
5305
4914
|
),
|
|
5306
4915
|
|
|
5307
|
-
// Step 2: Live Audio Signal Test
|
|
5308
4916
|
h('div', { className: 'dvs-card' },
|
|
5309
4917
|
h('div', { className: 'dvs-h' }, t('wizardStep2')),
|
|
5310
4918
|
h('div', { className: 'cb-row', style: { alignItems: 'center' } },
|
|
@@ -5343,7 +4951,6 @@ function MacrosSection(props) {
|
|
|
5343
4951
|
) : null,
|
|
5344
4952
|
),
|
|
5345
4953
|
|
|
5346
|
-
// Step 3: Transcription Round-Trip Test
|
|
5347
4954
|
h('div', { className: 'dvs-card' },
|
|
5348
4955
|
h('div', { className: 'dvs-h' }, t('wizardStep3')),
|
|
5349
4956
|
h('div', { className: 'cb-row' },
|
|
@@ -5382,12 +4989,17 @@ function MacrosSection(props) {
|
|
|
5382
4989
|
) : null,
|
|
5383
4990
|
),
|
|
5384
4991
|
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
4992
|
+
nextStepHint ? h('div', { className: 'dvs-wait', style: { fontStyle: 'italic', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
|
|
4993
|
+
h('span', null, t('wizardNextStepHint').replace('{hint}', nextStepHint)),
|
|
4994
|
+
(isWindows && isPermDenied) ? h('a', {
|
|
4995
|
+
href: 'ms-settings:privacy-microphone',
|
|
4996
|
+
target: '_blank',
|
|
4997
|
+
rel: 'noopener noreferrer',
|
|
4998
|
+
className: 'cb-btn cb-btn-mini',
|
|
4999
|
+
style: { textDecoration: 'none', fontStyle: 'normal' },
|
|
5000
|
+
}, '⚙ ' + t('wizardOpenWinMicSettings')) : null,
|
|
5388
5001
|
) : null,
|
|
5389
5002
|
|
|
5390
|
-
// Wizard Bottom Actions
|
|
5391
5003
|
h('div', { className: 'cb-row', style: { justifyContent: 'space-between', marginTop: '6px' } },
|
|
5392
5004
|
h('button', {
|
|
5393
5005
|
type: 'button',
|
|
@@ -5408,14 +5020,11 @@ function MacrosSection(props) {
|
|
|
5408
5020
|
),
|
|
5409
5021
|
)
|
|
5410
5022
|
}
|
|
5411
|
-
// Card in Settings → Plugins → Plugin settings (#18)
|
|
5412
5023
|
function PluginCard(props) {
|
|
5413
5024
|
const page = !!(props && props.view === 'page')
|
|
5414
5025
|
const [open, setOpen] = React.useState(!!page)
|
|
5415
5026
|
const tt = (props && props.t) || t
|
|
5416
5027
|
const title = tt('title')
|
|
5417
|
-
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
5418
|
-
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
5419
5028
|
if (props && props.view === 'summary') {
|
|
5420
5029
|
return h('span', { className: 'dvo-pdesc' }, tt('cardHint'))
|
|
5421
5030
|
}
|
|
@@ -5484,9 +5093,7 @@ function MacrosSection(props) {
|
|
|
5484
5093
|
|
|
5485
5094
|
exports.inject = ['timer', 'slots', 'configForms', 'locale']
|
|
5486
5095
|
exports.apply = function apply(ctx) {
|
|
5487
|
-
// English
|
|
5488
|
-
// localized user dictionary. Other languages (like Russian) come from
|
|
5489
|
-
// the separate translation plugin at runtime.
|
|
5096
|
+
// English source/fallback, Chinese built-in dictionary.
|
|
5490
5097
|
const addLocale = (locale, dictionary) => {
|
|
5491
5098
|
try {
|
|
5492
5099
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -5518,6 +5125,7 @@ function MacrosSection(props) {
|
|
|
5518
5125
|
cycleDictationMode,
|
|
5519
5126
|
resolveBrowserRecognitionLang,
|
|
5520
5127
|
VoiceSection,
|
|
5128
|
+
FirstRunWizard,
|
|
5521
5129
|
cutPhrase,
|
|
5522
5130
|
sendAudio,
|
|
5523
5131
|
modeChain,
|