@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 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 res = await fetch('/dsh-voice/transcribe', {
495
- method: 'POST',
496
- headers: { 'content-type': 'application/json' },
497
- body: JSON.stringify(payload),
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
- actions.setDraft((draft.slice(0, i) + draft.slice(i + last.added.length)).replace(/\s+$/, ''))
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
- voice.set({ error: String(e && e.message ? e.message : e) })
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 res = await fetch('/dsh-voice/polish', {
1079
- method: 'POST',
1080
- headers: { 'content-type': 'application/json' },
1081
- body: JSON.stringify({ text: draft }),
1082
- })
1083
- const parsed = await res.json().catch(() => null)
1084
- if (parsed && parsed.ok && typeof parsed.text === 'string' && parsed.text.trim()) {
1085
- actions.setDraft(parsed.text.trim())
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) return 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) return 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(value && value.dictation && value.dictation.vadSilenceMs) || 700,
1869
- autoSendMs: Number(value && value.message && value.message.autoSendMs) || 4000,
1870
- beep: !!(value && value.beep),
1871
- micDeviceId: String((value && value.micDeviceId) || ''),
1872
- historyLimit: Number(value && value.historyLimit) || 20,
1873
- voiceCommands: !!(value && value.voiceCommands),
1874
- sendDelayMs: Number(value && value.dictation && value.dictation.sendDelayMs) || 0,
1875
- stream: !!(value && value.dictation && value.dictation.stream),
1876
- streamChunkMs: Number(value && value.dictation && value.dictation.streamChunkMs) || 1200,
1877
- vadAdapt: Number(value && value.dictation && value.dictation.vadAdapt) || 0,
1878
- wakeWord: String((value && value.wakeWord) || ''),
1879
- bargeIn: !!(value && value.bargeIn),
1880
- polishSend: !!(value && value.message && value.message.polishSend),
1881
- sessionCommands: !!(value && value.message && value.message.sessionCommands),
1882
- polishBaseUrl: String((value && value.polishBaseUrl) || ''),
1883
- noiseSuppression: value.noiseSuppression !== false,
1884
- noiseGateDb: Number((value && value.noiseGateDb) !== undefined ? value.noiseGateDb : -45),
1885
- contextGlossary: value.contextGlossary !== false,
1886
- visualizerStyle: (value && value.visualizerStyle) || 'liquid-wave',
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(draft.dictation && draft.dictation.vadSilenceMs) || 700,
2017
- autoSendMs: Number(draft.message && draft.message.autoSendMs) || 4000,
2018
- beep: !!draft.beep,
2019
- micDeviceId: String(draft.micDeviceId || ''),
2020
- historyLimit: Number(draft.historyLimit),
2021
- voiceCommands: !!draft.voiceCommands,
2022
- sendDelayMs: Number(draft.dictation && draft.dictation.sendDelayMs) || 0,
2023
- stream: !!(draft.dictation && draft.dictation.stream),
2024
- streamChunkMs: Number(draft.dictation && draft.dictation.streamChunkMs) || 1200,
2025
- vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
2026
- noiseSuppression: draft.noiseSuppression !== false,
2027
- noiseGateDb: Number(draft.noiseGateDb !== undefined ? draft.noiseGateDb : -45),
2028
- contextGlossary: draft.contextGlossary !== false,
2029
- visualizerStyle: draft.visualizerStyle || 'liquid-wave',
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) !== undefined ? draft.noiseGateDb : (value && value.noiseGateDb) !== undefined ? value.noiseGateDb : -45)
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,
@@ -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|отменить)\s*[.!?]*$/i, cmd: 'cancel' },
34
- { re: /^(стоп|stop|хватит)\s*[.!?]*$/i, cmd: 'stop' },
35
- { re: /^(продолжи|continue|продолжай)\s*[.!?]*$/i, cmd: '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.27",
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",