@goodandready/dsh-voice 0.8.27 → 0.8.28
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 +105 -53
- package/lib/normalize.js +1 -1
- package/lib/transcribe-core.js +4 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -491,11 +491,24 @@ window.__ModuleLoader__.load({
|
|
|
491
491
|
const payload = { dataBase64, mimeType: mime, mode }
|
|
492
492
|
const contextWords = extractContextKeywords()
|
|
493
493
|
if (contextWords && contextWords.length > 0) payload.contextWords = contextWords
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
494
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
|
495
|
+
const timer = controller ? setTimeout(() => controller.abort(), 60000) : null
|
|
496
|
+
let res = null
|
|
497
|
+
try {
|
|
498
|
+
res = await fetch('/dsh-voice/transcribe', {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'content-type': 'application/json' },
|
|
501
|
+
body: JSON.stringify(payload),
|
|
502
|
+
signal: controller ? controller.signal : undefined,
|
|
503
|
+
})
|
|
504
|
+
} catch (e) {
|
|
505
|
+
if (e && e.name === 'AbortError') {
|
|
506
|
+
throw new Error('Transcription request timed out (60s)')
|
|
507
|
+
}
|
|
508
|
+
throw e
|
|
509
|
+
} finally {
|
|
510
|
+
if (timer) clearTimeout(timer)
|
|
511
|
+
}
|
|
499
512
|
let parsed = null
|
|
500
513
|
try { parsed = await res.json() } catch (e) { /* not json */ }
|
|
501
514
|
if (!res.ok || !parsed || !parsed.ok) {
|
|
@@ -545,7 +558,8 @@ window.__ModuleLoader__.load({
|
|
|
545
558
|
// Draft was edited manually — cut the last insert as a substring.
|
|
546
559
|
const i = draft.lastIndexOf(last.added)
|
|
547
560
|
if (i < 0) { insertHistory.push(last); return t('nothingToUndo') }
|
|
548
|
-
|
|
561
|
+
const spliced = draft.slice(0, i) + draft.slice(i + last.added.length)
|
|
562
|
+
actions.setDraft(spliced.replace(/[ \t]{2,}/g, ' ').replace(/\s+$/, '').trimStart())
|
|
549
563
|
}
|
|
550
564
|
return t('undone')
|
|
551
565
|
}
|
|
@@ -717,6 +731,17 @@ window.__ModuleLoader__.load({
|
|
|
717
731
|
if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
718
732
|
}
|
|
719
733
|
|
|
734
|
+
if (typeof window !== 'undefined') {
|
|
735
|
+
const onPageUnload = () => {
|
|
736
|
+
if (voice.rec) teardown(voice.rec)
|
|
737
|
+
if (voice.browser) {
|
|
738
|
+
try { voice.browser.abort() } catch (e) { /* ignore */ }
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
window.addEventListener('pagehide', onPageUnload)
|
|
742
|
+
window.addEventListener('beforeunload', onPageUnload)
|
|
743
|
+
}
|
|
744
|
+
|
|
720
745
|
function waitStop(recorder) {
|
|
721
746
|
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
722
747
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
@@ -815,16 +840,18 @@ window.__ModuleLoader__.load({
|
|
|
815
840
|
rec.silenceMs = 0
|
|
816
841
|
rec.hadSpeech = false
|
|
817
842
|
rec.streamMs = 0
|
|
818
|
-
if (!rec.closing) {
|
|
843
|
+
if (!rec.closing && !rec.cancelled) {
|
|
819
844
|
try {
|
|
820
845
|
if (rec.recorder && rec.recorder.state === 'inactive') rec.recorder.start()
|
|
821
846
|
} catch (e) { /* stream already closed */ }
|
|
822
847
|
}
|
|
823
848
|
rec.cutting = false
|
|
824
|
-
if (blob.size < 600) return // too short — not speech
|
|
849
|
+
if (rec.cancelled || blob.size < 600) return // cancelled or too short — not speech
|
|
825
850
|
dictationQueue = dictationQueue.then(async () => {
|
|
851
|
+
if (rec.cancelled) return
|
|
826
852
|
try {
|
|
827
853
|
const out = await sendAudio(blob, rec.mime, 'dictation')
|
|
854
|
+
if (rec.cancelled) return
|
|
828
855
|
const text = out && out.text ? out.text : ''
|
|
829
856
|
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
830
857
|
if (text && delay > 0 && !voice.holding) {
|
|
@@ -836,7 +863,9 @@ window.__ModuleLoader__.load({
|
|
|
836
863
|
}
|
|
837
864
|
if (text) appendDraft(text)
|
|
838
865
|
} catch (e) {
|
|
839
|
-
|
|
866
|
+
if (!rec.cancelled) {
|
|
867
|
+
voice.set({ error: String(e && e.message ? e.message : e) })
|
|
868
|
+
}
|
|
840
869
|
}
|
|
841
870
|
})
|
|
842
871
|
})
|
|
@@ -997,6 +1026,7 @@ window.__ModuleLoader__.load({
|
|
|
997
1026
|
const rec = voice.rec
|
|
998
1027
|
voice.pending = null
|
|
999
1028
|
if (!rec) { voice.set({ phase: 'idle', error: '' }); return }
|
|
1029
|
+
rec.cancelled = true
|
|
1000
1030
|
rec.closing = true
|
|
1001
1031
|
const stopped = waitStop(rec.recorder)
|
|
1002
1032
|
try {
|
|
@@ -1075,14 +1105,21 @@ window.__ModuleLoader__.load({
|
|
|
1075
1105
|
try {
|
|
1076
1106
|
const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
|
|
1077
1107
|
if (draft.trim()) {
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1108
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
|
|
1109
|
+
const timer = controller ? setTimeout(() => controller.abort(), 10000) : null
|
|
1110
|
+
try {
|
|
1111
|
+
const res = await fetch('/dsh-voice/polish', {
|
|
1112
|
+
method: 'POST',
|
|
1113
|
+
headers: { 'content-type': 'application/json' },
|
|
1114
|
+
body: JSON.stringify({ text: draft }),
|
|
1115
|
+
signal: controller ? controller.signal : undefined,
|
|
1116
|
+
})
|
|
1117
|
+
const parsed = await res.json().catch(() => null)
|
|
1118
|
+
if (parsed && parsed.ok && typeof parsed.text === 'string' && parsed.text.trim()) {
|
|
1119
|
+
actions.setDraft(parsed.text.trim())
|
|
1120
|
+
}
|
|
1121
|
+
} finally {
|
|
1122
|
+
if (timer) clearTimeout(timer)
|
|
1086
1123
|
}
|
|
1087
1124
|
}
|
|
1088
1125
|
} catch (e) { /* polish is best-effort */ }
|
|
@@ -1148,22 +1185,35 @@ window.__ModuleLoader__.load({
|
|
|
1148
1185
|
}
|
|
1149
1186
|
|
|
1150
1187
|
// ----------------------------------------------------------- visualizers
|
|
1188
|
+
let _accentColorCache = { color: '', expires: 0 }
|
|
1189
|
+
let _softColorCache = { color: '', expires: 0 }
|
|
1190
|
+
|
|
1151
1191
|
function accentColor(fallback) {
|
|
1192
|
+
const now = Date.now()
|
|
1193
|
+
if (now < _accentColorCache.expires && _accentColorCache.color) return _accentColorCache.color
|
|
1152
1194
|
try {
|
|
1153
1195
|
const root = document.documentElement
|
|
1154
1196
|
const s = getComputedStyle(root)
|
|
1155
1197
|
const pick = s.getPropertyValue('--dsw-alias-state-info-primary').trim()
|
|
1156
1198
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1157
|
-
if (pick)
|
|
1199
|
+
if (pick) {
|
|
1200
|
+
_accentColorCache = { color: pick, expires: now + 1000 }
|
|
1201
|
+
return pick
|
|
1202
|
+
}
|
|
1158
1203
|
} catch (noTheme) { /* canvas-only fallback */ }
|
|
1159
1204
|
return fallback || voice.waveColor || 'currentColor'
|
|
1160
1205
|
}
|
|
1161
1206
|
function softColor(fallback) {
|
|
1207
|
+
const now = Date.now()
|
|
1208
|
+
if (now < _softColorCache.expires && _softColorCache.color) return _softColorCache.color
|
|
1162
1209
|
try {
|
|
1163
1210
|
const s = getComputedStyle(document.documentElement)
|
|
1164
1211
|
const pick = s.getPropertyValue('--dsw-alias-bg-layer-3').trim()
|
|
1165
1212
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1166
|
-
if (pick)
|
|
1213
|
+
if (pick) {
|
|
1214
|
+
_softColorCache = { color: pick, expires: now + 1000 }
|
|
1215
|
+
return pick
|
|
1216
|
+
}
|
|
1167
1217
|
} catch (noTheme) { /* fallback */ }
|
|
1168
1218
|
return fallback || voice.waveColor || 'currentColor'
|
|
1169
1219
|
}
|
|
@@ -1864,26 +1914,27 @@ window.__ModuleLoader__.load({
|
|
|
1864
1914
|
|
|
1865
1915
|
React.useEffect(() => {
|
|
1866
1916
|
if (!ready) return
|
|
1917
|
+
const val = value || {}
|
|
1867
1918
|
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:
|
|
1919
|
+
vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
|
|
1920
|
+
autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
|
|
1921
|
+
beep: !!val.beep,
|
|
1922
|
+
micDeviceId: String(val.micDeviceId || ''),
|
|
1923
|
+
historyLimit: Number(val.historyLimit) || 20,
|
|
1924
|
+
voiceCommands: !!val.voiceCommands,
|
|
1925
|
+
sendDelayMs: Number(val.dictation && val.dictation.sendDelayMs) || 0,
|
|
1926
|
+
stream: !!(val.dictation && val.dictation.stream),
|
|
1927
|
+
streamChunkMs: Number(val.dictation && val.dictation.streamChunkMs) || 1200,
|
|
1928
|
+
vadAdapt: Number(val.dictation && val.dictation.vadAdapt) || 0,
|
|
1929
|
+
wakeWord: String(val.wakeWord || ''),
|
|
1930
|
+
bargeIn: !!val.bargeIn,
|
|
1931
|
+
polishSend: !!(val.message && val.message.polishSend),
|
|
1932
|
+
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
1933
|
+
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
1934
|
+
noiseSuppression: val.noiseSuppression !== false,
|
|
1935
|
+
noiseGateDb: Number(val.noiseGateDb !== undefined ? val.noiseGateDb : -45),
|
|
1936
|
+
contextGlossary: val.contextGlossary !== false,
|
|
1937
|
+
visualizerStyle: val.visualizerStyle || 'liquid-wave',
|
|
1887
1938
|
})
|
|
1888
1939
|
}, [ready, value])
|
|
1889
1940
|
|
|
@@ -2012,21 +2063,22 @@ window.__ModuleLoader__.load({
|
|
|
2012
2063
|
}
|
|
2013
2064
|
}
|
|
2014
2065
|
|
|
2066
|
+
const d = draft || {}
|
|
2015
2067
|
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:
|
|
2068
|
+
vadSilenceMs: Number(d.dictation && d.dictation.vadSilenceMs) || 700,
|
|
2069
|
+
autoSendMs: Number(d.message && d.message.autoSendMs) || 4000,
|
|
2070
|
+
beep: !!d.beep,
|
|
2071
|
+
micDeviceId: String(d.micDeviceId || ''),
|
|
2072
|
+
historyLimit: Number(d.historyLimit),
|
|
2073
|
+
voiceCommands: !!d.voiceCommands,
|
|
2074
|
+
sendDelayMs: Number(d.dictation && d.dictation.sendDelayMs) || 0,
|
|
2075
|
+
stream: !!(d.dictation && d.dictation.stream),
|
|
2076
|
+
streamChunkMs: Number(d.dictation && d.dictation.streamChunkMs) || 1200,
|
|
2077
|
+
vadAdapt: Number(d.dictation && d.dictation.vadAdapt) || 0,
|
|
2078
|
+
noiseSuppression: d.noiseSuppression !== false,
|
|
2079
|
+
noiseGateDb: Number(d.noiseGateDb !== undefined ? d.noiseGateDb : -45),
|
|
2080
|
+
contextGlossary: d.contextGlossary !== false,
|
|
2081
|
+
visualizerStyle: d.visualizerStyle || 'liquid-wave',
|
|
2030
2082
|
})
|
|
2031
2083
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
2032
2084
|
|
|
@@ -2061,7 +2113,7 @@ window.__ModuleLoader__.load({
|
|
|
2061
2113
|
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
2062
2114
|
)
|
|
2063
2115
|
|
|
2064
|
-
const gateDbVal = Number((draft && draft.noiseGateDb
|
|
2116
|
+
const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
|
|
2065
2117
|
const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
|
|
2066
2118
|
|
|
2067
2119
|
const micField = () => React.createElement('div', { className: 'cb-field' },
|
package/lib/normalize.js
CHANGED
|
@@ -17,7 +17,7 @@ export function ensureTrailingPeriod(text) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const NUM_WORDS = {
|
|
20
|
-
ноль: 0, один: 1, одна: 1, два: 2, две: 2, три: 3, четыре: 4, пять: 5,
|
|
20
|
+
ноль: 0, один: 1, одна: 1, одно: 1, два: 2, две: 2, три: 3, четыре: 4, пять: 5,
|
|
21
21
|
шесть: 6, семь: 7, восемь: 8, девять: 9, десять: 10, одиннадцать: 11,
|
|
22
22
|
двенадцать: 12, тринадцать: 13, четырнадцать: 14, пятнадцать: 15,
|
|
23
23
|
шестнадцать: 16, семнадцать: 17, восемнадцать: 18, девятнадцать: 19,
|
package/lib/transcribe-core.js
CHANGED
|
@@ -29,10 +29,10 @@ export function buildProviderOrder({ localOnly, chain, customKeys, knownKeys, de
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const SESSION_COMMANDS = [
|
|
32
|
-
{ re: /^(отправь|отправить|пошли|send)\s*[.!?]*$/i, cmd: 'send' },
|
|
33
|
-
{ re: /^(отмени|отмена|cancel
|
|
34
|
-
{ re: /^(стоп|stop
|
|
35
|
-
{ re: /^(продолжи|continue
|
|
32
|
+
{ re: /^(отправь|отправить|пошли|send|发送|发出去)\s*[.!?]*$/i, cmd: 'send' },
|
|
33
|
+
{ re: /^(отмени|отмена|cancel|отменить|取消|算了)\s*[.!?]*$/i, cmd: 'cancel' },
|
|
34
|
+
{ re: /^(стоп|stop|хватит|停止|暂停)\s*[.!?]*$/i, cmd: 'stop' },
|
|
35
|
+
{ re: /^(продолжи|continue|продолжай|继续)\s*[.!?]*$/i, cmd: 'continue' },
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.28",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|