@goodandready/dsh-voice 0.8.27 → 0.8.29
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/cordis.patch.yml +1 -1
- package/lib/client.js +320 -92
- package/lib/http-util.js +48 -0
- package/lib/index.js +46 -10
- package/lib/normalize.js +3 -1
- package/lib/provider-http.js +64 -0
- package/lib/providers.js +2 -38
- package/lib/transcribe-core.js +4 -4
- package/lib/updater.js +266 -0
- package/lib/wav.js +2 -2
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -17,6 +17,14 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
18
18
|
let React = require('react')
|
|
19
19
|
|
|
20
|
+
let ChevronIcon = null
|
|
21
|
+
try {
|
|
22
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
23
|
+
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
|
|
24
|
+
} catch {
|
|
25
|
+
ChevronIcon = null
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
const NS = 'dsh-voice'
|
|
21
29
|
|
|
22
30
|
// UI strings live in the locale registry so a separate package can
|
|
@@ -183,6 +191,15 @@ window.__ModuleLoader__.load({
|
|
|
183
191
|
'noiseGate': 'Noise Gate (dB)',
|
|
184
192
|
'noiseGateHint': 'Mute background fan hum and keyboard clicks below threshold. Off = disabled',
|
|
185
193
|
'noiseGateOff': 'Off',
|
|
194
|
+
'versionTitle': 'Plugin Version & Updates',
|
|
195
|
+
'versionHint': 'Check for new versions and update directly from npm.',
|
|
196
|
+
'updateAvailable': 'Update available',
|
|
197
|
+
'upToDate': 'Up to date',
|
|
198
|
+
'updatePrompt': 'A new version is available on npm. Click update to install.',
|
|
199
|
+
'updateNow': 'Update now',
|
|
200
|
+
'updating': 'Updating…',
|
|
201
|
+
'updateSuccess': 'Updated successfully! Restart DSH to apply.',
|
|
202
|
+
'updateFailed': 'Update failed. Check logs.',
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
const zh = {
|
|
@@ -346,6 +363,15 @@ window.__ModuleLoader__.load({
|
|
|
346
363
|
'noiseGate': '噪声门 (dB)',
|
|
347
364
|
'noiseGateHint': '静音切除低于阈值的风扇嗡嗡声与机械键盘敲击声。Off = 关闭',
|
|
348
365
|
'noiseGateOff': '关闭',
|
|
366
|
+
'versionTitle': '插件版本与更新',
|
|
367
|
+
'versionHint': '检查新版本并直接从 npm 更新。',
|
|
368
|
+
'updateAvailable': '有可用更新',
|
|
369
|
+
'upToDate': '已是最新版本',
|
|
370
|
+
'updatePrompt': 'npm 上有新版本可用。点击更新进行安装。',
|
|
371
|
+
'updateNow': '立即更新',
|
|
372
|
+
'updating': '正在更新…',
|
|
373
|
+
'updateSuccess': '更新成功!请重启 DSH 生效。',
|
|
374
|
+
'updateFailed': '更新失败,请查看日志。',
|
|
349
375
|
}
|
|
350
376
|
|
|
351
377
|
// Strings are also needed outside components — in recording handlers and
|
|
@@ -357,7 +383,8 @@ window.__ModuleLoader__.load({
|
|
|
357
383
|
'.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}' +
|
|
358
384
|
'.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}' +
|
|
359
385
|
'.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}' +
|
|
360
|
-
'.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
|
|
386
|
+
'.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}' +
|
|
387
|
+
'.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}' +
|
|
361
388
|
'.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}' +
|
|
362
389
|
'.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
|
|
363
390
|
'.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
@@ -447,8 +474,31 @@ window.__ModuleLoader__.load({
|
|
|
447
474
|
React.createElement('path', { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }),
|
|
448
475
|
React.createElement('line', { x1: 12, y1: 9, x2: 12, y2: 13 }),
|
|
449
476
|
React.createElement('line', { x1: 12, y1: 17, x2: 12.01, y2: 17 }))
|
|
450
|
-
const chevronIcon = () =>
|
|
451
|
-
|
|
477
|
+
const chevronIcon = () => (
|
|
478
|
+
ChevronIcon
|
|
479
|
+
? React.createElement(ChevronIcon, { className: 'dvo-chev-icon' })
|
|
480
|
+
: React.createElement('svg', {
|
|
481
|
+
className: 'dvo-chev-icon',
|
|
482
|
+
width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none',
|
|
483
|
+
},
|
|
484
|
+
React.createElement('path', {
|
|
485
|
+
d: 'M4 6l4 4 4-4', stroke: 'currentColor', strokeWidth: 1.5,
|
|
486
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
487
|
+
}))
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
function focusComposer() {
|
|
491
|
+
if (typeof window === 'undefined') return
|
|
492
|
+
const actions = voice.inputActions
|
|
493
|
+
if (actions && typeof actions.focus === 'function') {
|
|
494
|
+
try { actions.focus(); return } catch { /* best effort */ }
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const el = document.querySelector('.conversation-input textarea, .cb-composer textarea, .dsh-composer textarea, [data-slate-editor="true"], textarea')
|
|
498
|
+
if (el && typeof el.focus === 'function') el.focus()
|
|
499
|
+
} catch { /* best effort */ }
|
|
500
|
+
}
|
|
501
|
+
|
|
452
502
|
|
|
453
503
|
// ------------------------------------------------------------ transport
|
|
454
504
|
function blobToBase64(blob) {
|
|
@@ -466,6 +516,8 @@ window.__ModuleLoader__.load({
|
|
|
466
516
|
const text = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
|
|
467
517
|
if (!text || text.length < 3) return []
|
|
468
518
|
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
519
|
+
// Linguistic filter: functional stop-word dictionary for vocabulary keyword extraction.
|
|
520
|
+
// Language processing data for context analysis, distinct from user-facing UI text.
|
|
469
521
|
const stop = new Set([
|
|
470
522
|
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'any', 'can', 'her', 'was',
|
|
471
523
|
'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now',
|
|
@@ -491,11 +543,24 @@ window.__ModuleLoader__.load({
|
|
|
491
543
|
const payload = { dataBase64, mimeType: mime, mode }
|
|
492
544
|
const contextWords = extractContextKeywords()
|
|
493
545
|
if (contextWords && contextWords.length > 0) payload.contextWords = contextWords
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
546
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
|
547
|
+
const timer = controller ? setTimeout(() => controller.abort(), 60000) : null
|
|
548
|
+
let res = null
|
|
549
|
+
try {
|
|
550
|
+
res = await fetch('/dsh-voice/transcribe', {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: { 'content-type': 'application/json' },
|
|
553
|
+
body: JSON.stringify(payload),
|
|
554
|
+
signal: controller ? controller.signal : undefined,
|
|
555
|
+
})
|
|
556
|
+
} catch (e) {
|
|
557
|
+
if (e && e.name === 'AbortError') {
|
|
558
|
+
throw new Error('Transcription request timed out (60s)')
|
|
559
|
+
}
|
|
560
|
+
throw e
|
|
561
|
+
} finally {
|
|
562
|
+
if (timer) clearTimeout(timer)
|
|
563
|
+
}
|
|
499
564
|
let parsed = null
|
|
500
565
|
try { parsed = await res.json() } catch (e) { /* not json */ }
|
|
501
566
|
if (!res.ok || !parsed || !parsed.ok) {
|
|
@@ -513,8 +578,8 @@ window.__ModuleLoader__.load({
|
|
|
513
578
|
return s
|
|
514
579
|
}
|
|
515
580
|
|
|
516
|
-
//
|
|
517
|
-
//
|
|
581
|
+
// Functional speech edit commands (#37): spoken line breaks and punctuation -> symbols.
|
|
582
|
+
// Linguistic data for audio transcript post-processing; not user-facing UI text.
|
|
518
583
|
// Russian phrases match RU STT output; English covers EN dictation.
|
|
519
584
|
const VOICE_COMMANDS = [
|
|
520
585
|
[/(^|[\s,.!?])с новой строки([\s,.!?]|$)/gi, '$1\n$2'],
|
|
@@ -545,7 +610,8 @@ window.__ModuleLoader__.load({
|
|
|
545
610
|
// Draft was edited manually — cut the last insert as a substring.
|
|
546
611
|
const i = draft.lastIndexOf(last.added)
|
|
547
612
|
if (i < 0) { insertHistory.push(last); return t('nothingToUndo') }
|
|
548
|
-
|
|
613
|
+
const spliced = draft.slice(0, i) + draft.slice(i + last.added.length)
|
|
614
|
+
actions.setDraft(spliced.replace(/[ \t]{2,}/g, ' ').replace(/\s+$/, '').trimStart())
|
|
549
615
|
}
|
|
550
616
|
return t('undone')
|
|
551
617
|
}
|
|
@@ -717,6 +783,17 @@ window.__ModuleLoader__.load({
|
|
|
717
783
|
if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
718
784
|
}
|
|
719
785
|
|
|
786
|
+
if (typeof window !== 'undefined') {
|
|
787
|
+
const onPageUnload = () => {
|
|
788
|
+
if (voice.rec) teardown(voice.rec)
|
|
789
|
+
if (voice.browser) {
|
|
790
|
+
try { voice.browser.abort() } catch (e) { /* ignore */ }
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
window.addEventListener('pagehide', onPageUnload)
|
|
794
|
+
window.addEventListener('beforeunload', onPageUnload)
|
|
795
|
+
}
|
|
796
|
+
|
|
720
797
|
function waitStop(recorder) {
|
|
721
798
|
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
722
799
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
@@ -765,7 +842,7 @@ window.__ModuleLoader__.load({
|
|
|
765
842
|
src.connect(filter)
|
|
766
843
|
lastNode = filter
|
|
767
844
|
rec.filterNode = filter
|
|
768
|
-
} catch (e) {}
|
|
845
|
+
} catch (e) { /* audio filter fallback */ }
|
|
769
846
|
|
|
770
847
|
rec.analyser = rec.audioCtx.createAnalyser()
|
|
771
848
|
rec.analyser.fftSize = 128
|
|
@@ -815,16 +892,18 @@ window.__ModuleLoader__.load({
|
|
|
815
892
|
rec.silenceMs = 0
|
|
816
893
|
rec.hadSpeech = false
|
|
817
894
|
rec.streamMs = 0
|
|
818
|
-
if (!rec.closing) {
|
|
895
|
+
if (!rec.closing && !rec.cancelled) {
|
|
819
896
|
try {
|
|
820
897
|
if (rec.recorder && rec.recorder.state === 'inactive') rec.recorder.start()
|
|
821
898
|
} catch (e) { /* stream already closed */ }
|
|
822
899
|
}
|
|
823
900
|
rec.cutting = false
|
|
824
|
-
if (blob.size < 600) return // too short — not speech
|
|
901
|
+
if (rec.cancelled || blob.size < 600) return // cancelled or too short — not speech
|
|
825
902
|
dictationQueue = dictationQueue.then(async () => {
|
|
903
|
+
if (rec.cancelled) return
|
|
826
904
|
try {
|
|
827
905
|
const out = await sendAudio(blob, rec.mime, 'dictation')
|
|
906
|
+
if (rec.cancelled) return
|
|
828
907
|
const text = out && out.text ? out.text : ''
|
|
829
908
|
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
830
909
|
if (text && delay > 0 && !voice.holding) {
|
|
@@ -836,7 +915,9 @@ window.__ModuleLoader__.load({
|
|
|
836
915
|
}
|
|
837
916
|
if (text) appendDraft(text)
|
|
838
917
|
} catch (e) {
|
|
839
|
-
|
|
918
|
+
if (!rec.cancelled) {
|
|
919
|
+
voice.set({ error: String(e && e.message ? e.message : e) })
|
|
920
|
+
}
|
|
840
921
|
}
|
|
841
922
|
})
|
|
842
923
|
})
|
|
@@ -987,6 +1068,7 @@ window.__ModuleLoader__.load({
|
|
|
987
1068
|
|
|
988
1069
|
function cancelCurrent() {
|
|
989
1070
|
announceVoice('end')
|
|
1071
|
+
focusComposer()
|
|
990
1072
|
if (voice.browser) {
|
|
991
1073
|
voice.browser.abort()
|
|
992
1074
|
voice.browser = null
|
|
@@ -997,6 +1079,7 @@ window.__ModuleLoader__.load({
|
|
|
997
1079
|
const rec = voice.rec
|
|
998
1080
|
voice.pending = null
|
|
999
1081
|
if (!rec) { voice.set({ phase: 'idle', error: '' }); return }
|
|
1082
|
+
rec.cancelled = true
|
|
1000
1083
|
rec.closing = true
|
|
1001
1084
|
const stopped = waitStop(rec.recorder)
|
|
1002
1085
|
try {
|
|
@@ -1009,6 +1092,7 @@ window.__ModuleLoader__.load({
|
|
|
1009
1092
|
// sends the whole recording and opens the cancel window.
|
|
1010
1093
|
function stopCurrent() {
|
|
1011
1094
|
announceVoice('end')
|
|
1095
|
+
focusComposer()
|
|
1012
1096
|
if (voice.browser) {
|
|
1013
1097
|
const mode = voice.mode
|
|
1014
1098
|
const said = (voice.browserFinals || []).join(' ').trim()
|
|
@@ -1075,14 +1159,21 @@ window.__ModuleLoader__.load({
|
|
|
1075
1159
|
try {
|
|
1076
1160
|
const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
|
|
1077
1161
|
if (draft.trim()) {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1162
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
|
1163
|
+
const timer = controller ? setTimeout(() => controller.abort(), 10000) : null
|
|
1164
|
+
try {
|
|
1165
|
+
const res = await fetch('/dsh-voice/polish', {
|
|
1166
|
+
method: 'POST',
|
|
1167
|
+
headers: { 'content-type': 'application/json' },
|
|
1168
|
+
body: JSON.stringify({ text: draft }),
|
|
1169
|
+
signal: controller ? controller.signal : undefined,
|
|
1170
|
+
})
|
|
1171
|
+
const parsed = await res.json().catch(() => null)
|
|
1172
|
+
if (parsed && parsed.ok && typeof parsed.text === 'string' && parsed.text.trim()) {
|
|
1173
|
+
actions.setDraft(parsed.text.trim())
|
|
1174
|
+
}
|
|
1175
|
+
} finally {
|
|
1176
|
+
if (timer) clearTimeout(timer)
|
|
1086
1177
|
}
|
|
1087
1178
|
}
|
|
1088
1179
|
} catch (e) { /* polish is best-effort */ }
|
|
@@ -1127,11 +1218,14 @@ window.__ModuleLoader__.load({
|
|
|
1127
1218
|
return React.createElement(React.Fragment, null,
|
|
1128
1219
|
React.createElement('button', {
|
|
1129
1220
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
1130
|
-
title: err ? v.error : t('dictationBtn'),
|
|
1221
|
+
title: err ? v.error : t('dictationBtn'),
|
|
1222
|
+
'aria-label': err ? v.error : t('dictationBtn'),
|
|
1223
|
+
onClick: startDictation,
|
|
1131
1224
|
}, micIcon()),
|
|
1132
1225
|
React.createElement('button', {
|
|
1133
1226
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
1134
1227
|
title: err ? v.error : (t('messageBtn') + hk),
|
|
1228
|
+
'aria-label': err ? v.error : (t('messageBtn') + hk),
|
|
1135
1229
|
// Hold: record while pressed; leaving the control cancels.
|
|
1136
1230
|
onPointerDown: (e) => { e.preventDefault(); beginHold('message') },
|
|
1137
1231
|
onPointerUp: () => endHold(false),
|
|
@@ -1141,6 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1141
1235
|
? React.createElement('button', {
|
|
1142
1236
|
type: 'button', className: 'dvo-btn' + (voice.showPlayer ? ' dvo-btn-active' : ''),
|
|
1143
1237
|
title: t('listenBack'),
|
|
1238
|
+
'aria-label': t('listenBack'),
|
|
1144
1239
|
onClick: () => voice.set({ showPlayer: !voice.showPlayer }),
|
|
1145
1240
|
}, playIcon())
|
|
1146
1241
|
: null,
|
|
@@ -1148,22 +1243,35 @@ window.__ModuleLoader__.load({
|
|
|
1148
1243
|
}
|
|
1149
1244
|
|
|
1150
1245
|
// ----------------------------------------------------------- visualizers
|
|
1246
|
+
let _accentColorCache = { color: '', expires: 0 }
|
|
1247
|
+
let _softColorCache = { color: '', expires: 0 }
|
|
1248
|
+
|
|
1151
1249
|
function accentColor(fallback) {
|
|
1250
|
+
const now = Date.now()
|
|
1251
|
+
if (now < _accentColorCache.expires && _accentColorCache.color) return _accentColorCache.color
|
|
1152
1252
|
try {
|
|
1153
1253
|
const root = document.documentElement
|
|
1154
1254
|
const s = getComputedStyle(root)
|
|
1155
1255
|
const pick = s.getPropertyValue('--dsw-alias-state-info-primary').trim()
|
|
1156
1256
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1157
|
-
if (pick)
|
|
1257
|
+
if (pick) {
|
|
1258
|
+
_accentColorCache = { color: pick, expires: now + 1000 }
|
|
1259
|
+
return pick
|
|
1260
|
+
}
|
|
1158
1261
|
} catch (noTheme) { /* canvas-only fallback */ }
|
|
1159
1262
|
return fallback || voice.waveColor || 'currentColor'
|
|
1160
1263
|
}
|
|
1161
1264
|
function softColor(fallback) {
|
|
1265
|
+
const now = Date.now()
|
|
1266
|
+
if (now < _softColorCache.expires && _softColorCache.color) return _softColorCache.color
|
|
1162
1267
|
try {
|
|
1163
1268
|
const s = getComputedStyle(document.documentElement)
|
|
1164
1269
|
const pick = s.getPropertyValue('--dsw-alias-bg-layer-3').trim()
|
|
1165
1270
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1166
|
-
if (pick)
|
|
1271
|
+
if (pick) {
|
|
1272
|
+
_softColorCache = { color: pick, expires: now + 1000 }
|
|
1273
|
+
return pick
|
|
1274
|
+
}
|
|
1167
1275
|
} catch (noTheme) { /* fallback */ }
|
|
1168
1276
|
return fallback || voice.waveColor || 'currentColor'
|
|
1169
1277
|
}
|
|
@@ -1349,7 +1457,7 @@ window.__ModuleLoader__.load({
|
|
|
1349
1457
|
g.clearRect(0, 0, cssW, cssH)
|
|
1350
1458
|
|
|
1351
1459
|
if (!voice.waveColor) {
|
|
1352
|
-
try { voice.waveColor = getComputedStyle(canvas).color || '
|
|
1460
|
+
try { voice.waveColor = getComputedStyle(canvas).color || 'currentColor' } catch (e) { voice.waveColor = 'currentColor' }
|
|
1353
1461
|
}
|
|
1354
1462
|
const levels = voice.levels
|
|
1355
1463
|
const style = (voice.settings && voice.settings.visualizerStyle) || 'liquid-wave'
|
|
@@ -1407,11 +1515,12 @@ window.__ModuleLoader__.load({
|
|
|
1407
1515
|
|
|
1408
1516
|
if (v.phase === 'idle') {
|
|
1409
1517
|
if (!voice.showPlayer || !voice.lastNote || !voice.lastNote.url) return null
|
|
1410
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1518
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1411
1519
|
React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
1412
1520
|
React.createElement('button', {
|
|
1413
1521
|
type: 'button', className: 'dvo-audio-play',
|
|
1414
1522
|
title: isPlaying ? t('pause') : t('play'),
|
|
1523
|
+
'aria-label': isPlaying ? t('pause') : t('play'),
|
|
1415
1524
|
onClick: () => {
|
|
1416
1525
|
const el = audioRef.current
|
|
1417
1526
|
if (!el) return
|
|
@@ -1427,13 +1536,15 @@ window.__ModuleLoader__.load({
|
|
|
1427
1536
|
}),
|
|
1428
1537
|
React.createElement('span', { className: 'dvo-audio-time' }, t('lastRecording')),
|
|
1429
1538
|
),
|
|
1430
|
-
React.createElement('span', { className: 'dvo-status' }, voice.lastNote.text || ''),
|
|
1539
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, voice.lastNote.text || ''),
|
|
1431
1540
|
React.createElement('button', {
|
|
1432
1541
|
type: 'button', className: 'dvo-pbtn', title: t('hide'),
|
|
1542
|
+
'aria-label': t('hide'),
|
|
1433
1543
|
onClick: () => {
|
|
1434
1544
|
if (audioRef.current) audioRef.current.pause()
|
|
1435
1545
|
setIsPlaying(false)
|
|
1436
1546
|
voice.set({ showPlayer: false })
|
|
1547
|
+
focusComposer()
|
|
1437
1548
|
},
|
|
1438
1549
|
}, xIcon()),
|
|
1439
1550
|
)
|
|
@@ -1446,12 +1557,16 @@ window.__ModuleLoader__.load({
|
|
|
1446
1557
|
// Live caption of what is heard right now. Until the browser emits a
|
|
1447
1558
|
// final chunk the text is interim and changes on screen.
|
|
1448
1559
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
1449
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1450
|
-
React.createElement('button', {
|
|
1560
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1561
|
+
React.createElement('button', {
|
|
1562
|
+
type: 'button', className: 'dvo-pbtn', title: t('cancel'),
|
|
1563
|
+
'aria-label': t('cancel'),
|
|
1564
|
+
onClick: cancelCurrent,
|
|
1565
|
+
}, xIcon()),
|
|
1451
1566
|
inBrowser
|
|
1452
1567
|
? null
|
|
1453
1568
|
: React.createElement('canvas', { className: 'dvo-wave', ref: canvasRef, width: 720, height: 40 }),
|
|
1454
|
-
React.createElement('span', { className: 'dvo-status' },
|
|
1569
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
1455
1570
|
caption
|
|
1456
1571
|
? caption
|
|
1457
1572
|
: (voice.holding
|
|
@@ -1459,36 +1574,43 @@ window.__ModuleLoader__.load({
|
|
|
1459
1574
|
: (inBrowser
|
|
1460
1575
|
? t('listening')
|
|
1461
1576
|
: (rec && rec.hadSpeech ? (v.speaking ? t('speaking') : t('silence')) : hint)))),
|
|
1462
|
-
React.createElement('button', {
|
|
1577
|
+
React.createElement('button', {
|
|
1578
|
+
type: 'button', className: 'dvo-pbtn', title: t('stop'),
|
|
1579
|
+
'aria-label': t('stop'),
|
|
1580
|
+
onClick: stopCurrent,
|
|
1581
|
+
}, stopIcon()),
|
|
1463
1582
|
)
|
|
1464
1583
|
}
|
|
1465
1584
|
|
|
1466
1585
|
if (v.phase === 'processing') {
|
|
1467
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1468
|
-
React.createElement('span', { className: 'dvo-status' }, spinIcon(), t('transcribing')))
|
|
1586
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1587
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, spinIcon(), t('transcribing')))
|
|
1469
1588
|
}
|
|
1470
1589
|
|
|
1471
1590
|
if (v.phase === 'pending') {
|
|
1472
1591
|
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
1473
1592
|
if (voice.pending && voice.pending.undoOnly) {
|
|
1474
1593
|
// Cancel window for delayed dictation insert (#29-5).
|
|
1475
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1476
|
-
React.createElement('span', { className: 'dvo-status' }, t('undo'), ': ', left, t('secondsShort')),
|
|
1594
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1595
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, t('undo'), ': ', left, t('secondsShort')),
|
|
1477
1596
|
React.createElement('button', {
|
|
1478
1597
|
type: 'button', className: 'dvo-pbtn', title: t('undo'),
|
|
1598
|
+
'aria-label': t('undo'),
|
|
1479
1599
|
onClick: async () => {
|
|
1480
1600
|
const msg = await undoLastInsert()
|
|
1481
1601
|
voice.set({ phase: 'idle', error: msg === t('undone') ? '' : msg })
|
|
1602
|
+
focusComposer()
|
|
1482
1603
|
},
|
|
1483
1604
|
}, xIcon()),
|
|
1484
1605
|
)
|
|
1485
1606
|
}
|
|
1486
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1607
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1487
1608
|
voice.lastNote && voice.lastNote.url
|
|
1488
1609
|
? React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
1489
1610
|
React.createElement('button', {
|
|
1490
1611
|
type: 'button', className: 'dvo-audio-play',
|
|
1491
1612
|
title: isPlaying ? t('pause') : t('play'),
|
|
1613
|
+
'aria-label': isPlaying ? t('pause') : t('play'),
|
|
1492
1614
|
onClick: () => {
|
|
1493
1615
|
const el = audioRef.current
|
|
1494
1616
|
if (!el) return
|
|
@@ -1505,15 +1627,29 @@ window.__ModuleLoader__.load({
|
|
|
1505
1627
|
React.createElement('span', { className: 'dvo-audio-time' }, t('listenBack')),
|
|
1506
1628
|
)
|
|
1507
1629
|
: null,
|
|
1508
|
-
React.createElement('span', { className: 'dvo-status' }, t('sendingIn')),
|
|
1630
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' }, t('sendingIn')),
|
|
1509
1631
|
React.createElement('span', { className: 'dvo-count' }, left + t('secondsShort')),
|
|
1510
|
-
React.createElement('button', {
|
|
1632
|
+
React.createElement('button', {
|
|
1633
|
+
type: 'button', className: 'dvo-pbtn', title: t('keepPending'),
|
|
1634
|
+
'aria-label': t('keepPending'),
|
|
1635
|
+
onClick: () => {
|
|
1636
|
+
keepPending()
|
|
1637
|
+
focusComposer()
|
|
1638
|
+
},
|
|
1639
|
+
}, xIcon()),
|
|
1511
1640
|
)
|
|
1512
1641
|
}
|
|
1513
1642
|
|
|
1514
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
1515
|
-
React.createElement('span', { className: 'dvo-status dvo-err' }, warnIcon(), v.error),
|
|
1516
|
-
React.createElement('button', {
|
|
1643
|
+
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
1644
|
+
React.createElement('span', { className: 'dvo-status dvo-err', 'aria-live': 'polite', 'aria-atomic': 'true' }, warnIcon(), v.error),
|
|
1645
|
+
React.createElement('button', {
|
|
1646
|
+
type: 'button', className: 'dvo-pbtn', title: t('hide'),
|
|
1647
|
+
'aria-label': t('hide'),
|
|
1648
|
+
onClick: () => {
|
|
1649
|
+
voice.set({ phase: 'idle', error: '' })
|
|
1650
|
+
focusComposer()
|
|
1651
|
+
},
|
|
1652
|
+
}, xIcon()),
|
|
1517
1653
|
)
|
|
1518
1654
|
}
|
|
1519
1655
|
|
|
@@ -1616,16 +1752,16 @@ window.__ModuleLoader__.load({
|
|
|
1616
1752
|
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
1617
1753
|
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
1618
1754
|
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
1619
|
-
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:
|
|
1620
|
-
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
1621
|
-
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
1755
|
+
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}' +
|
|
1756
|
+
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}' +
|
|
1757
|
+
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}' +
|
|
1622
1758
|
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
1623
1759
|
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
1624
1760
|
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
1625
1761
|
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
1626
1762
|
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
1627
1763
|
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
1628
|
-
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:
|
|
1764
|
+
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)}' +
|
|
1629
1765
|
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
1630
1766
|
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
1631
1767
|
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
@@ -1633,7 +1769,7 @@ window.__ModuleLoader__.load({
|
|
|
1633
1769
|
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
1634
1770
|
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
1635
1771
|
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
1636
|
-
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:
|
|
1772
|
+
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
1637
1773
|
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
1638
1774
|
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
1639
1775
|
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
@@ -1864,26 +2000,27 @@ window.__ModuleLoader__.load({
|
|
|
1864
2000
|
|
|
1865
2001
|
React.useEffect(() => {
|
|
1866
2002
|
if (!ready) return
|
|
2003
|
+
const val = value || {}
|
|
1867
2004
|
voice.settings = Object.assign({}, voice.settings, {
|
|
1868
|
-
vadSilenceMs: Number(
|
|
1869
|
-
autoSendMs: Number(
|
|
1870
|
-
beep: !!
|
|
1871
|
-
micDeviceId: String(
|
|
1872
|
-
historyLimit: Number(
|
|
1873
|
-
voiceCommands: !!
|
|
1874
|
-
sendDelayMs: Number(
|
|
1875
|
-
stream: !!(
|
|
1876
|
-
streamChunkMs: Number(
|
|
1877
|
-
vadAdapt: Number(
|
|
1878
|
-
wakeWord: String(
|
|
1879
|
-
bargeIn: !!
|
|
1880
|
-
polishSend: !!(
|
|
1881
|
-
sessionCommands: !!(
|
|
1882
|
-
polishBaseUrl: String(
|
|
1883
|
-
noiseSuppression:
|
|
1884
|
-
noiseGateDb: Number(
|
|
1885
|
-
contextGlossary:
|
|
1886
|
-
visualizerStyle:
|
|
2005
|
+
vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
|
|
2006
|
+
autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
|
|
2007
|
+
beep: !!val.beep,
|
|
2008
|
+
micDeviceId: String(val.micDeviceId || ''),
|
|
2009
|
+
historyLimit: Number(val.historyLimit) || 20,
|
|
2010
|
+
voiceCommands: !!val.voiceCommands,
|
|
2011
|
+
sendDelayMs: Number(val.dictation && val.dictation.sendDelayMs) || 0,
|
|
2012
|
+
stream: !!(val.dictation && val.dictation.stream),
|
|
2013
|
+
streamChunkMs: Number(val.dictation && val.dictation.streamChunkMs) || 1200,
|
|
2014
|
+
vadAdapt: Number(val.dictation && val.dictation.vadAdapt) || 0,
|
|
2015
|
+
wakeWord: String(val.wakeWord || ''),
|
|
2016
|
+
bargeIn: !!val.bargeIn,
|
|
2017
|
+
polishSend: !!(val.message && val.message.polishSend),
|
|
2018
|
+
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
2019
|
+
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
2020
|
+
noiseSuppression: val.noiseSuppression !== false,
|
|
2021
|
+
noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45),
|
|
2022
|
+
contextGlossary: val.contextGlossary !== false,
|
|
2023
|
+
visualizerStyle: val.visualizerStyle || 'liquid-wave',
|
|
1887
2024
|
})
|
|
1888
2025
|
}, [ready, value])
|
|
1889
2026
|
|
|
@@ -1896,8 +2033,8 @@ window.__ModuleLoader__.load({
|
|
|
1896
2033
|
if (testingMic) {
|
|
1897
2034
|
if (testRef.current) {
|
|
1898
2035
|
testRef.current.active = false
|
|
1899
|
-
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) {}
|
|
1900
|
-
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) {} }
|
|
2036
|
+
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
2037
|
+
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
1901
2038
|
testRef.current = null
|
|
1902
2039
|
}
|
|
1903
2040
|
setTestingMic(false)
|
|
@@ -1922,7 +2059,7 @@ window.__ModuleLoader__.load({
|
|
|
1922
2059
|
filter.Q.value = 0.707
|
|
1923
2060
|
src.connect(filter)
|
|
1924
2061
|
lastNode = filter
|
|
1925
|
-
} catch (e) {}
|
|
2062
|
+
} catch (e) { /* filter fallback */ }
|
|
1926
2063
|
const analyser = audioCtx.createAnalyser()
|
|
1927
2064
|
analyser.fftSize = 128
|
|
1928
2065
|
lastNode.connect(analyser)
|
|
@@ -1931,12 +2068,12 @@ window.__ModuleLoader__.load({
|
|
|
1931
2068
|
|
|
1932
2069
|
const check = () => {
|
|
1933
2070
|
if (!testRef.current || !testRef.current.active) return
|
|
1934
|
-
const
|
|
1935
|
-
analyser.getByteFrequencyData(
|
|
2071
|
+
const arr = new Uint8Array(analyser.frequencyBinCount)
|
|
2072
|
+
analyser.getByteFrequencyData(arr)
|
|
1936
2073
|
let sum = 0
|
|
1937
|
-
for (let i = 0; i <
|
|
1938
|
-
const
|
|
1939
|
-
setTestLevel(
|
|
2074
|
+
for (let i = 0; i < arr.length; i++) sum += arr[i]
|
|
2075
|
+
const avg = sum / (arr.length || 1)
|
|
2076
|
+
setTestLevel(Math.min(100, Math.round((avg / 128) * 100)))
|
|
1940
2077
|
requestAnimationFrame(check)
|
|
1941
2078
|
}
|
|
1942
2079
|
requestAnimationFrame(check)
|
|
@@ -1949,8 +2086,8 @@ window.__ModuleLoader__.load({
|
|
|
1949
2086
|
return () => {
|
|
1950
2087
|
if (testRef.current) {
|
|
1951
2088
|
testRef.current.active = false
|
|
1952
|
-
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) {}
|
|
1953
|
-
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) {} }
|
|
2089
|
+
try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
2090
|
+
if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
1954
2091
|
testRef.current = null
|
|
1955
2092
|
}
|
|
1956
2093
|
}
|
|
@@ -1981,6 +2118,55 @@ window.__ModuleLoader__.load({
|
|
|
1981
2118
|
fetchStatus()
|
|
1982
2119
|
}, [fetchStatus])
|
|
1983
2120
|
|
|
2121
|
+
const [updaterStatus, setUpdaterStatus] = React.useState(null)
|
|
2122
|
+
const [updating, setUpdating] = React.useState(false)
|
|
2123
|
+
const [updaterMsg, setUpdaterMsg] = React.useState('')
|
|
2124
|
+
const [updaterLoading, setUpdaterLoading] = React.useState(false)
|
|
2125
|
+
|
|
2126
|
+
const checkUpdater = React.useCallback(async () => {
|
|
2127
|
+
setUpdaterLoading(true)
|
|
2128
|
+
setUpdaterMsg('')
|
|
2129
|
+
try {
|
|
2130
|
+
const res = await fetch('/api/dsh-voice/update', {
|
|
2131
|
+
headers: { accept: 'application/json' },
|
|
2132
|
+
cache: 'no-store',
|
|
2133
|
+
})
|
|
2134
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2135
|
+
const data = await res.json()
|
|
2136
|
+
setUpdaterStatus(data)
|
|
2137
|
+
} catch (e) {
|
|
2138
|
+
/* best effort update check */
|
|
2139
|
+
} finally {
|
|
2140
|
+
setUpdaterLoading(false)
|
|
2141
|
+
}
|
|
2142
|
+
}, [])
|
|
2143
|
+
|
|
2144
|
+
const onUpdateNow = async () => {
|
|
2145
|
+
setUpdating(true)
|
|
2146
|
+
setUpdaterMsg('')
|
|
2147
|
+
try {
|
|
2148
|
+
const res = await fetch('/api/dsh-voice/update', {
|
|
2149
|
+
method: 'POST',
|
|
2150
|
+
headers: {
|
|
2151
|
+
'x-dsh-plugin-update': '1',
|
|
2152
|
+
'content-type': 'application/json',
|
|
2153
|
+
},
|
|
2154
|
+
})
|
|
2155
|
+
const data = await res.json()
|
|
2156
|
+
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`)
|
|
2157
|
+
setUpdaterStatus(data)
|
|
2158
|
+
setUpdaterMsg(t('updateSuccess'))
|
|
2159
|
+
} catch (e) {
|
|
2160
|
+
setUpdaterMsg(t('updateFailed') + ' ' + (e?.message || String(e)))
|
|
2161
|
+
} finally {
|
|
2162
|
+
setUpdating(false)
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
React.useEffect(() => {
|
|
2167
|
+
checkUpdater()
|
|
2168
|
+
}, [checkUpdater])
|
|
2169
|
+
|
|
1984
2170
|
if (!ready) {
|
|
1985
2171
|
const waiting = !snap || snap.status === 'loading'
|
|
1986
2172
|
return React.createElement('div', { className: 'cb-page' },
|
|
@@ -2012,21 +2198,22 @@ window.__ModuleLoader__.load({
|
|
|
2012
2198
|
}
|
|
2013
2199
|
}
|
|
2014
2200
|
|
|
2201
|
+
const d = draft || {}
|
|
2015
2202
|
Object.assign(voice.settings, {
|
|
2016
|
-
vadSilenceMs: Number(
|
|
2017
|
-
autoSendMs: Number(
|
|
2018
|
-
beep: !!
|
|
2019
|
-
micDeviceId: String(
|
|
2020
|
-
historyLimit: Number(
|
|
2021
|
-
voiceCommands: !!
|
|
2022
|
-
sendDelayMs: Number(
|
|
2023
|
-
stream: !!(
|
|
2024
|
-
streamChunkMs: Number(
|
|
2025
|
-
vadAdapt: Number(
|
|
2026
|
-
noiseSuppression:
|
|
2027
|
-
noiseGateDb: Number(
|
|
2028
|
-
contextGlossary:
|
|
2029
|
-
visualizerStyle:
|
|
2203
|
+
vadSilenceMs: Number(d.dictation && d.dictation.vadSilenceMs) || 700,
|
|
2204
|
+
autoSendMs: Number(d.message && d.message.autoSendMs) || 4000,
|
|
2205
|
+
beep: !!d.beep,
|
|
2206
|
+
micDeviceId: String(d.micDeviceId || ''),
|
|
2207
|
+
historyLimit: Number(d.historyLimit),
|
|
2208
|
+
voiceCommands: !!d.voiceCommands,
|
|
2209
|
+
sendDelayMs: Number(d.dictation && d.dictation.sendDelayMs) || 0,
|
|
2210
|
+
stream: !!(d.dictation && d.dictation.stream),
|
|
2211
|
+
streamChunkMs: Number(d.dictation && d.dictation.streamChunkMs) || 1200,
|
|
2212
|
+
vadAdapt: Number(d.dictation && d.dictation.vadAdapt) || 0,
|
|
2213
|
+
noiseSuppression: d.noiseSuppression !== false,
|
|
2214
|
+
noiseGateDb: Number(d.noiseGateDb !== undefined ? d.noiseGateDb : -45),
|
|
2215
|
+
contextGlossary: d.contextGlossary !== false,
|
|
2216
|
+
visualizerStyle: d.visualizerStyle || 'liquid-wave',
|
|
2030
2217
|
})
|
|
2031
2218
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
2032
2219
|
|
|
@@ -2061,7 +2248,7 @@ window.__ModuleLoader__.load({
|
|
|
2061
2248
|
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
2062
2249
|
)
|
|
2063
2250
|
|
|
2064
|
-
const gateDbVal = Number((draft && draft.noiseGateDb
|
|
2251
|
+
const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
|
|
2065
2252
|
const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
|
|
2066
2253
|
|
|
2067
2254
|
const micField = () => React.createElement('div', { className: 'cb-field' },
|
|
@@ -2409,6 +2596,47 @@ window.__ModuleLoader__.load({
|
|
|
2409
2596
|
),
|
|
2410
2597
|
),
|
|
2411
2598
|
|
|
2599
|
+
// 7. Plugin Version & Updates Card
|
|
2600
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
2601
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2602
|
+
React.createElement('span', null, '📦 ' + t('versionTitle')),
|
|
2603
|
+
React.createElement('button', {
|
|
2604
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
2605
|
+
onClick: checkUpdater, disabled: updaterLoading || updating,
|
|
2606
|
+
}, updaterLoading ? '⏳…' : '🔄 ' + t('refreshStats')),
|
|
2607
|
+
),
|
|
2608
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('versionHint')),
|
|
2609
|
+
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px' } },
|
|
2610
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
2611
|
+
'v' + ((updaterStatus && updaterStatus.currentVersion) || '0.8.29')
|
|
2612
|
+
),
|
|
2613
|
+
updaterStatus && updaterStatus.updateAvailable
|
|
2614
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
2615
|
+
t('updateAvailable') + ': v' + updaterStatus.latestVersion
|
|
2616
|
+
)
|
|
2617
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
2618
|
+
t('upToDate')
|
|
2619
|
+
),
|
|
2620
|
+
updaterStatus && updaterStatus.updateAvailable
|
|
2621
|
+
? React.createElement('button', {
|
|
2622
|
+
type: 'button', className: 'cb-btn cb-btn-primary', style: { fontSize: '12px', padding: '4px 10px' },
|
|
2623
|
+
onClick: onUpdateNow, disabled: updating,
|
|
2624
|
+
}, updating ? t('updating') : t('updateNow'))
|
|
2625
|
+
: null,
|
|
2626
|
+
),
|
|
2627
|
+
updaterMsg
|
|
2628
|
+
? React.createElement('div', {
|
|
2629
|
+
style: {
|
|
2630
|
+
marginTop: '8px',
|
|
2631
|
+
fontSize: '12px',
|
|
2632
|
+
color: updaterMsg.includes('fail') || updaterMsg.includes('失败')
|
|
2633
|
+
? 'var(--dsw-alias-state-warning-primary)'
|
|
2634
|
+
: 'var(--dsw-alias-state-success-primary)',
|
|
2635
|
+
},
|
|
2636
|
+
}, updaterMsg)
|
|
2637
|
+
: null,
|
|
2638
|
+
),
|
|
2639
|
+
|
|
2412
2640
|
// Action bar (Save)
|
|
2413
2641
|
React.createElement('div', { className: 'cb-row' },
|
|
2414
2642
|
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|