@goodandready/dsh-voice 0.8.26 → 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 +145 -61
- package/lib/index.js +2 -1
- package/lib/normalize.js +1 -1
- package/lib/providers.js +9 -8
- package/lib/transcribe-core.js +4 -4
- package/lib/wav.js +33 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -465,8 +465,14 @@ window.__ModuleLoader__.load({
|
|
|
465
465
|
if (!voice.settings || voice.settings.contextGlossary === false) return []
|
|
466
466
|
const text = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
|
|
467
467
|
if (!text || text.length < 3) return []
|
|
468
|
-
const matches = text.match(
|
|
469
|
-
const stop = new Set([
|
|
468
|
+
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
469
|
+
const stop = new Set([
|
|
470
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'any', 'can', 'her', 'was',
|
|
471
|
+
'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now',
|
|
472
|
+
'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she',
|
|
473
|
+
'too', 'use', 'это', 'как', 'что', 'для', 'или', 'если', 'все', 'при', 'так', 'уже',
|
|
474
|
+
'был', 'быть', 'только', 'тоже', 'под', 'над', 'без', 'нет', 'даже', 'где', 'чем',
|
|
475
|
+
])
|
|
470
476
|
const words = []
|
|
471
477
|
const seen = new Set()
|
|
472
478
|
for (const m of matches) {
|
|
@@ -485,11 +491,24 @@ window.__ModuleLoader__.load({
|
|
|
485
491
|
const payload = { dataBase64, mimeType: mime, mode }
|
|
486
492
|
const contextWords = extractContextKeywords()
|
|
487
493
|
if (contextWords && contextWords.length > 0) payload.contextWords = contextWords
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
}
|
|
493
512
|
let parsed = null
|
|
494
513
|
try { parsed = await res.json() } catch (e) { /* not json */ }
|
|
495
514
|
if (!res.ok || !parsed || !parsed.ok) {
|
|
@@ -539,7 +558,8 @@ window.__ModuleLoader__.load({
|
|
|
539
558
|
// Draft was edited manually — cut the last insert as a substring.
|
|
540
559
|
const i = draft.lastIndexOf(last.added)
|
|
541
560
|
if (i < 0) { insertHistory.push(last); return t('nothingToUndo') }
|
|
542
|
-
|
|
561
|
+
const spliced = draft.slice(0, i) + draft.slice(i + last.added.length)
|
|
562
|
+
actions.setDraft(spliced.replace(/[ \t]{2,}/g, ' ').replace(/\s+$/, '').trimStart())
|
|
543
563
|
}
|
|
544
564
|
return t('undone')
|
|
545
565
|
}
|
|
@@ -706,10 +726,24 @@ window.__ModuleLoader__.load({
|
|
|
706
726
|
if (!rec) return
|
|
707
727
|
try { rec.stream.getTracks().forEach((t) => t.stop()) } catch (e) { /* already stopped */ }
|
|
708
728
|
if (rec.srcNode) { try { rec.srcNode.disconnect() } catch (e) { /* already disconnected */ } }
|
|
729
|
+
if (rec.filterNode) { try { rec.filterNode.disconnect() } catch (e) { /* already disconnected */ } }
|
|
730
|
+
if (rec.analyser) { try { rec.analyser.disconnect() } catch (e) { /* already disconnected */ } }
|
|
709
731
|
if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
710
732
|
}
|
|
711
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
|
+
|
|
712
745
|
function waitStop(recorder) {
|
|
746
|
+
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
713
747
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
714
748
|
}
|
|
715
749
|
|
|
@@ -792,21 +826,32 @@ window.__ModuleLoader__.load({
|
|
|
792
826
|
if (!rec || rec.cutting || rec.closing) return
|
|
793
827
|
rec.cutting = true
|
|
794
828
|
const stopped = waitStop(rec.recorder)
|
|
795
|
-
try {
|
|
829
|
+
try {
|
|
830
|
+
if (rec.recorder && rec.recorder.state !== 'inactive') {
|
|
831
|
+
rec.recorder.stop()
|
|
832
|
+
}
|
|
833
|
+
} catch (e) {
|
|
834
|
+
rec.cutting = false
|
|
835
|
+
return
|
|
836
|
+
}
|
|
796
837
|
stopped.then(async () => {
|
|
797
838
|
const blob = new Blob(rec.chunks, { type: rec.mime })
|
|
798
839
|
rec.chunks = []
|
|
799
840
|
rec.silenceMs = 0
|
|
800
841
|
rec.hadSpeech = false
|
|
801
842
|
rec.streamMs = 0
|
|
802
|
-
if (!rec.closing) {
|
|
803
|
-
try {
|
|
843
|
+
if (!rec.closing && !rec.cancelled) {
|
|
844
|
+
try {
|
|
845
|
+
if (rec.recorder && rec.recorder.state === 'inactive') rec.recorder.start()
|
|
846
|
+
} catch (e) { /* stream already closed */ }
|
|
804
847
|
}
|
|
805
848
|
rec.cutting = false
|
|
806
|
-
if (blob.size < 600) return // too short — not speech
|
|
849
|
+
if (rec.cancelled || blob.size < 600) return // cancelled or too short — not speech
|
|
807
850
|
dictationQueue = dictationQueue.then(async () => {
|
|
851
|
+
if (rec.cancelled) return
|
|
808
852
|
try {
|
|
809
853
|
const out = await sendAudio(blob, rec.mime, 'dictation')
|
|
854
|
+
if (rec.cancelled) return
|
|
810
855
|
const text = out && out.text ? out.text : ''
|
|
811
856
|
const delay = Number(voice.settings.sendDelayMs) || 0
|
|
812
857
|
if (text && delay > 0 && !voice.holding) {
|
|
@@ -818,7 +863,9 @@ window.__ModuleLoader__.load({
|
|
|
818
863
|
}
|
|
819
864
|
if (text) appendDraft(text)
|
|
820
865
|
} catch (e) {
|
|
821
|
-
|
|
866
|
+
if (!rec.cancelled) {
|
|
867
|
+
voice.set({ error: String(e && e.message ? e.message : e) })
|
|
868
|
+
}
|
|
822
869
|
}
|
|
823
870
|
})
|
|
824
871
|
})
|
|
@@ -979,9 +1026,12 @@ window.__ModuleLoader__.load({
|
|
|
979
1026
|
const rec = voice.rec
|
|
980
1027
|
voice.pending = null
|
|
981
1028
|
if (!rec) { voice.set({ phase: 'idle', error: '' }); return }
|
|
1029
|
+
rec.cancelled = true
|
|
982
1030
|
rec.closing = true
|
|
983
1031
|
const stopped = waitStop(rec.recorder)
|
|
984
|
-
try {
|
|
1032
|
+
try {
|
|
1033
|
+
if (rec.recorder && rec.recorder.state !== 'inactive') rec.recorder.stop()
|
|
1034
|
+
} catch (e) { /* already stopped */ }
|
|
985
1035
|
stopped.then(() => { teardown(rec); voice.rec = null; voice.set({ phase: 'idle', error: '' }) })
|
|
986
1036
|
}
|
|
987
1037
|
|
|
@@ -1011,7 +1061,9 @@ window.__ModuleLoader__.load({
|
|
|
1011
1061
|
rec.closing = true
|
|
1012
1062
|
const mode = rec.mode
|
|
1013
1063
|
const stopped = waitStop(rec.recorder)
|
|
1014
|
-
try {
|
|
1064
|
+
try {
|
|
1065
|
+
if (rec.recorder && rec.recorder.state !== 'inactive') rec.recorder.stop()
|
|
1066
|
+
} catch (e) { /* already stopped */ }
|
|
1015
1067
|
stopped.then(async () => {
|
|
1016
1068
|
const blob = new Blob(rec.chunks, { type: rec.mime })
|
|
1017
1069
|
teardown(rec)
|
|
@@ -1053,14 +1105,21 @@ window.__ModuleLoader__.load({
|
|
|
1053
1105
|
try {
|
|
1054
1106
|
const draft = voice.input && typeof voice.input.draft === 'string' ? voice.input.draft : ''
|
|
1055
1107
|
if (draft.trim()) {
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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)
|
|
1064
1123
|
}
|
|
1065
1124
|
}
|
|
1066
1125
|
} catch (e) { /* polish is best-effort */ }
|
|
@@ -1126,22 +1185,35 @@ window.__ModuleLoader__.load({
|
|
|
1126
1185
|
}
|
|
1127
1186
|
|
|
1128
1187
|
// ----------------------------------------------------------- visualizers
|
|
1188
|
+
let _accentColorCache = { color: '', expires: 0 }
|
|
1189
|
+
let _softColorCache = { color: '', expires: 0 }
|
|
1190
|
+
|
|
1129
1191
|
function accentColor(fallback) {
|
|
1192
|
+
const now = Date.now()
|
|
1193
|
+
if (now < _accentColorCache.expires && _accentColorCache.color) return _accentColorCache.color
|
|
1130
1194
|
try {
|
|
1131
1195
|
const root = document.documentElement
|
|
1132
1196
|
const s = getComputedStyle(root)
|
|
1133
1197
|
const pick = s.getPropertyValue('--dsw-alias-state-info-primary').trim()
|
|
1134
1198
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1135
|
-
if (pick)
|
|
1199
|
+
if (pick) {
|
|
1200
|
+
_accentColorCache = { color: pick, expires: now + 1000 }
|
|
1201
|
+
return pick
|
|
1202
|
+
}
|
|
1136
1203
|
} catch (noTheme) { /* canvas-only fallback */ }
|
|
1137
1204
|
return fallback || voice.waveColor || 'currentColor'
|
|
1138
1205
|
}
|
|
1139
1206
|
function softColor(fallback) {
|
|
1207
|
+
const now = Date.now()
|
|
1208
|
+
if (now < _softColorCache.expires && _softColorCache.color) return _softColorCache.color
|
|
1140
1209
|
try {
|
|
1141
1210
|
const s = getComputedStyle(document.documentElement)
|
|
1142
1211
|
const pick = s.getPropertyValue('--dsw-alias-bg-layer-3').trim()
|
|
1143
1212
|
|| s.getPropertyValue('--dsw-alias-label-primary').trim()
|
|
1144
|
-
if (pick)
|
|
1213
|
+
if (pick) {
|
|
1214
|
+
_softColorCache = { color: pick, expires: now + 1000 }
|
|
1215
|
+
return pick
|
|
1216
|
+
}
|
|
1145
1217
|
} catch (noTheme) { /* fallback */ }
|
|
1146
1218
|
return fallback || voice.waveColor || 'currentColor'
|
|
1147
1219
|
}
|
|
@@ -1528,6 +1600,7 @@ window.__ModuleLoader__.load({
|
|
|
1528
1600
|
polishSend: !!(data && data.modes && data.modes.message && data.modes.message.polishSend),
|
|
1529
1601
|
sessionCommands: !!(data && data.modes && data.modes.message && data.modes.message.sessionCommands),
|
|
1530
1602
|
noiseSuppression: data && data.noiseSuppression !== false,
|
|
1603
|
+
noiseGateDb: Number(data && data.noiseGateDb !== undefined ? data.noiseGateDb : -45),
|
|
1531
1604
|
contextGlossary: data && data.contextGlossary !== false,
|
|
1532
1605
|
visualizerStyle: (data && data.visualizerStyle) || 'liquid-wave',
|
|
1533
1606
|
})
|
|
@@ -1825,7 +1898,7 @@ window.__ModuleLoader__.load({
|
|
|
1825
1898
|
const timer = setInterval(() => {
|
|
1826
1899
|
if (tries >= 15) { clearInterval(timer); return }
|
|
1827
1900
|
tries += 1
|
|
1828
|
-
try {
|
|
1901
|
+
try { ctx.settingsScope.describe().load() } catch (e) { /* service not up yet */ }
|
|
1829
1902
|
}, 1000)
|
|
1830
1903
|
return () => clearInterval(timer)
|
|
1831
1904
|
}, [ready])
|
|
@@ -1841,26 +1914,27 @@ window.__ModuleLoader__.load({
|
|
|
1841
1914
|
|
|
1842
1915
|
React.useEffect(() => {
|
|
1843
1916
|
if (!ready) return
|
|
1917
|
+
const val = value || {}
|
|
1844
1918
|
voice.settings = Object.assign({}, voice.settings, {
|
|
1845
|
-
vadSilenceMs: Number(
|
|
1846
|
-
autoSendMs: Number(
|
|
1847
|
-
beep: !!
|
|
1848
|
-
micDeviceId: String(
|
|
1849
|
-
historyLimit: Number(
|
|
1850
|
-
voiceCommands: !!
|
|
1851
|
-
sendDelayMs: Number(
|
|
1852
|
-
stream: !!(
|
|
1853
|
-
streamChunkMs: Number(
|
|
1854
|
-
vadAdapt: Number(
|
|
1855
|
-
wakeWord: String(
|
|
1856
|
-
bargeIn: !!
|
|
1857
|
-
polishSend: !!(
|
|
1858
|
-
sessionCommands: !!(
|
|
1859
|
-
polishBaseUrl: String(
|
|
1860
|
-
noiseSuppression:
|
|
1861
|
-
noiseGateDb: Number(
|
|
1862
|
-
contextGlossary:
|
|
1863
|
-
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',
|
|
1864
1938
|
})
|
|
1865
1939
|
}, [ready, value])
|
|
1866
1940
|
|
|
@@ -1891,9 +1965,18 @@ window.__ModuleLoader__.load({
|
|
|
1891
1965
|
const audioCtx = new AC()
|
|
1892
1966
|
if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
|
|
1893
1967
|
const src = audioCtx.createMediaStreamSource(stream)
|
|
1968
|
+
let lastNode = src
|
|
1969
|
+
try {
|
|
1970
|
+
const filter = audioCtx.createBiquadFilter()
|
|
1971
|
+
filter.type = 'highpass'
|
|
1972
|
+
filter.frequency.value = 80
|
|
1973
|
+
filter.Q.value = 0.707
|
|
1974
|
+
src.connect(filter)
|
|
1975
|
+
lastNode = filter
|
|
1976
|
+
} catch (e) {}
|
|
1894
1977
|
const analyser = audioCtx.createAnalyser()
|
|
1895
1978
|
analyser.fftSize = 128
|
|
1896
|
-
|
|
1979
|
+
lastNode.connect(analyser)
|
|
1897
1980
|
testRef.current = { stream, audioCtx, analyser, active: true }
|
|
1898
1981
|
setTestingMic(true)
|
|
1899
1982
|
|
|
@@ -1980,21 +2063,22 @@ window.__ModuleLoader__.load({
|
|
|
1980
2063
|
}
|
|
1981
2064
|
}
|
|
1982
2065
|
|
|
2066
|
+
const d = draft || {}
|
|
1983
2067
|
Object.assign(voice.settings, {
|
|
1984
|
-
vadSilenceMs: Number(
|
|
1985
|
-
autoSendMs: Number(
|
|
1986
|
-
beep: !!
|
|
1987
|
-
micDeviceId: String(
|
|
1988
|
-
historyLimit: Number(
|
|
1989
|
-
voiceCommands: !!
|
|
1990
|
-
sendDelayMs: Number(
|
|
1991
|
-
stream: !!(
|
|
1992
|
-
streamChunkMs: Number(
|
|
1993
|
-
vadAdapt: Number(
|
|
1994
|
-
noiseSuppression:
|
|
1995
|
-
noiseGateDb: Number(
|
|
1996
|
-
contextGlossary:
|
|
1997
|
-
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',
|
|
1998
2082
|
})
|
|
1999
2083
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
2000
2084
|
|
|
@@ -2029,7 +2113,7 @@ window.__ModuleLoader__.load({
|
|
|
2029
2113
|
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
2030
2114
|
)
|
|
2031
2115
|
|
|
2032
|
-
const gateDbVal = Number((draft && draft.noiseGateDb
|
|
2116
|
+
const gateDbVal = Number((draft && draft.noiseGateDb !== undefined) ? draft.noiseGateDb : (value && value.noiseGateDb !== undefined) ? value.noiseGateDb : -45)
|
|
2033
2117
|
const gateThresholdPercent = gateDbVal <= -90 ? 0 : Math.min(100, Math.max(0, Math.round(((gateDbVal + 60) / 40) * 100)))
|
|
2034
2118
|
|
|
2035
2119
|
const micField = () => React.createElement('div', { className: 'cb-field' },
|
package/lib/index.js
CHANGED
|
@@ -303,7 +303,7 @@ export function apply(ctx, baseConfig) {
|
|
|
303
303
|
: (Array.isArray(cfg.vocabulary) ? cfg.vocabulary : [])
|
|
304
304
|
|
|
305
305
|
const providers = makeProviders(
|
|
306
|
-
{ resolveKey, fetchImpl: fetch, cfg, toWav: (b) => toWav16k(b, cfg.ffmpegBin) },
|
|
306
|
+
{ resolveKey, fetchImpl: fetch, cfg, toWav: (b, sig) => toWav16k(b, cfg.ffmpegBin, sig || signal) },
|
|
307
307
|
{ bytes, mime, lang: modeCfg.language, signal, models, vocabulary: vocab },
|
|
308
308
|
)
|
|
309
309
|
return runChain(order, providers, statsTracker.record)
|
|
@@ -356,6 +356,7 @@ export function apply(ctx, baseConfig) {
|
|
|
356
356
|
beep: cfg.beep,
|
|
357
357
|
localOnly: cfg.localOnly,
|
|
358
358
|
micDeviceId: cfg.micDeviceId,
|
|
359
|
+
noiseGateDb: cfg.noiseGateDb !== undefined ? cfg.noiseGateDb : -45,
|
|
359
360
|
historyLimit: cfg.historyLimit,
|
|
360
361
|
voiceCommands: cfg.voiceCommands,
|
|
361
362
|
wakeWord: String(cfg.wakeWord || ''),
|
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/providers.js
CHANGED
|
@@ -158,6 +158,7 @@ export function makeProviders(deps, req) {
|
|
|
158
158
|
form.append('file', new Blob([bytes], { type: mime }), fileName(mime))
|
|
159
159
|
form.append('model', pickModel(models, 'groq'))
|
|
160
160
|
if (!isAutoLang(lang)) form.append('language', lang)
|
|
161
|
+
if (vocab) form.append('prompt', vocab)
|
|
161
162
|
form.append('response_format', 'json')
|
|
162
163
|
const res = await fetchImpl('https://api.groq.com/openai/v1/audio/transcriptions', {
|
|
163
164
|
method: 'POST',
|
|
@@ -205,7 +206,7 @@ export function makeProviders(deps, req) {
|
|
|
205
206
|
return { ok: false, provider: 'local-whisper', reason: 'local whisper needs WAV, no converter configured' }
|
|
206
207
|
}
|
|
207
208
|
try {
|
|
208
|
-
sendBytes = await deps.toWav(bytes)
|
|
209
|
+
sendBytes = await deps.toWav(bytes, signal)
|
|
209
210
|
sendMime = 'audio/wav'
|
|
210
211
|
} catch (e) {
|
|
211
212
|
return { ok: false, provider: 'local-whisper', reason: `local whisper: ${String(e && e.message || e)}` }
|
|
@@ -249,7 +250,7 @@ export function makeProviders(deps, req) {
|
|
|
249
250
|
return { ok: false, provider: 'sensevoice', reason: 'sensevoice needs WAV, no converter configured' }
|
|
250
251
|
}
|
|
251
252
|
try {
|
|
252
|
-
sendBytes = await deps.toWav(bytes)
|
|
253
|
+
sendBytes = await deps.toWav(bytes, signal)
|
|
253
254
|
sendMime = 'audio/wav'
|
|
254
255
|
} catch (e) {
|
|
255
256
|
return { ok: false, provider: 'sensevoice', reason: `sensevoice: ${String(e && e.message || e)}` }
|
|
@@ -259,12 +260,11 @@ export function makeProviders(deps, req) {
|
|
|
259
260
|
form.append('file', new Blob([sendBytes], { type: sendMime }), fileName(sendMime))
|
|
260
261
|
const isOpenAI = url.includes('/transcriptions')
|
|
261
262
|
if (isOpenAI) {
|
|
262
|
-
|
|
263
|
-
form.append('
|
|
263
|
+
form.append('model', pickModel(models, 'sensevoice'))
|
|
264
|
+
if (!isAutoLang(lang)) form.append('language', lang)
|
|
265
|
+
if (vocab) form.append('prompt', vocab)
|
|
264
266
|
form.append('response_format', 'json')
|
|
265
267
|
}
|
|
266
|
-
if (!isAutoLang(lang)) form.append('language', lang)
|
|
267
|
-
if (vocab) form.append('prompt', vocab)
|
|
268
268
|
|
|
269
269
|
let res
|
|
270
270
|
try {
|
|
@@ -316,6 +316,7 @@ export function makeProviders(deps, req) {
|
|
|
316
316
|
form.append('file', new Blob([bytes], { type: mime }), fileName(mime))
|
|
317
317
|
form.append('model', model)
|
|
318
318
|
if (lang && lang !== 'auto') form.append('language', lang)
|
|
319
|
+
if (vocab) form.append('prompt', vocab)
|
|
319
320
|
form.append('response_format', 'json')
|
|
320
321
|
const res = await fetchImpl(`${base}/audio/transcriptions`, {
|
|
321
322
|
method: 'POST', headers, body: form, signal,
|
|
@@ -330,9 +331,9 @@ export function makeProviders(deps, req) {
|
|
|
330
331
|
let format = chatAudioFormat(mime)
|
|
331
332
|
if (!format) {
|
|
332
333
|
if (typeof deps.toWav !== 'function') {
|
|
333
|
-
throw new Error(`${label} needs wav or mp3,
|
|
334
|
+
throw new Error(`${label}: chat template needs wav or mp3, unsupported format ${mime}`)
|
|
334
335
|
}
|
|
335
|
-
sendBytes = await deps.toWav(bytes)
|
|
336
|
+
sendBytes = await deps.toWav(bytes, signal)
|
|
336
337
|
format = 'wav'
|
|
337
338
|
}
|
|
338
339
|
const ask = (spec.prompt || CHAT_AUDIO_PROMPT)
|
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/lib/wav.js
CHANGED
|
@@ -11,8 +11,12 @@ import { spawn } from 'node:child_process'
|
|
|
11
11
|
* @param ffmpegBin {string} ffmpeg binary path
|
|
12
12
|
* @returns {Promise<Buffer>} 16 kHz mono WAV
|
|
13
13
|
*/
|
|
14
|
-
export function toWav16k(bytes, ffmpegBin = 'ffmpeg') {
|
|
14
|
+
export function toWav16k(bytes, ffmpegBin = 'ffmpeg', signal = null) {
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
|
+
if (signal?.aborted) {
|
|
17
|
+
reject(new Error('ffmpeg conversion aborted before start'))
|
|
18
|
+
return
|
|
19
|
+
}
|
|
16
20
|
const proc = spawn(ffmpegBin, [
|
|
17
21
|
'-hide_banner', '-loglevel', 'error',
|
|
18
22
|
'-i', 'pipe:0',
|
|
@@ -21,10 +25,37 @@ export function toWav16k(bytes, ffmpegBin = 'ffmpeg') {
|
|
|
21
25
|
])
|
|
22
26
|
const out = []
|
|
23
27
|
const err = []
|
|
28
|
+
let done = false
|
|
29
|
+
|
|
30
|
+
const timeout = setTimeout(() => {
|
|
31
|
+
cleanup()
|
|
32
|
+
try { proc.kill('SIGKILL') } catch {}
|
|
33
|
+
reject(new Error('ffmpeg conversion timed out after 30s'))
|
|
34
|
+
}, 30000)
|
|
35
|
+
|
|
36
|
+
const onAbort = () => {
|
|
37
|
+
cleanup()
|
|
38
|
+
try { proc.kill('SIGKILL') } catch {}
|
|
39
|
+
reject(new Error('ffmpeg conversion aborted'))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true })
|
|
43
|
+
|
|
44
|
+
function cleanup() {
|
|
45
|
+
if (done) return
|
|
46
|
+
done = true
|
|
47
|
+
clearTimeout(timeout)
|
|
48
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
proc.stdout.on('data', (c) => out.push(c))
|
|
25
52
|
proc.stderr.on('data', (c) => err.push(c))
|
|
26
|
-
proc.on('error', (e) =>
|
|
53
|
+
proc.on('error', (e) => {
|
|
54
|
+
cleanup()
|
|
55
|
+
reject(new Error(`ffmpeg unavailable: ${e.message}`))
|
|
56
|
+
})
|
|
27
57
|
proc.on('close', (code) => {
|
|
58
|
+
cleanup()
|
|
28
59
|
if (code !== 0) {
|
|
29
60
|
reject(new Error(`ffmpeg exit ${code}: ${Buffer.concat(err).toString('utf8').slice(0, 200)}`))
|
|
30
61
|
return
|
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",
|