@goodandready/dsh-voice 0.8.5 → 0.8.6

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
@@ -96,6 +96,11 @@ window.__ModuleLoader__.load({
96
96
  'vocabulary': 'Custom vocabulary (one word per line)',
97
97
  'polish': 'Polish transcript with model',
98
98
  'polishHint': 'Fix punctuation and fillers via the harness model before inserting',
99
+ 'stream': 'Continuous dictation',
100
+ 'streamHint': 'Cut phrases by a timer while you speak instead of waiting for a long pause',
101
+ 'streamChunkMs': 'Stream chunk (ms)',
102
+ 'vadAdapt': 'Adaptive silence',
103
+ 'vadAdaptHint': 'Auto-tune the silence threshold to the pace of your speech. 0 = fixed',
99
104
  'voiceCommandsLabel': 'Voice edit commands ("new line", "paragraph")',
100
105
  'normalizeTranscriptHint': 'transcribe_audio: spoken numbers to digits, tidy punctuation',
101
106
  'messageTitle': 'Voice message',
@@ -189,6 +194,11 @@ window.__ModuleLoader__.load({
189
194
  'vocabulary': 'Свой словарь (одно слово в строке)',
190
195
  'polish': 'Полировка текста моделью',
191
196
  'polishHint': 'Пунктуация и слова-паразиты через модель харнесса перед вставкой',
197
+ 'stream': 'Непрерывная диктовка',
198
+ 'streamHint': 'Резать фразы по таймеру во время речи, а не по длинной паузе',
199
+ 'streamChunkMs': 'Кусок потока (мс)',
200
+ 'vadAdapt': 'Адаптивная тишина',
201
+ 'vadAdaptHint': 'Авто-подстройка порога под темп речи. 0 = фикс. поведение',
192
202
  'voiceCommandsLabel': 'Голосовые команды («с новой строки», «абзац»)',
193
203
  'normalizeTranscriptHint': 'transcribe_audio: числа словами — в цифры, аккуратная пунктуация',
194
204
  'messageTitle': 'Голосовое сообщение',
@@ -247,7 +257,7 @@ window.__ModuleLoader__.load({
247
257
  pending: null, // {text, leftMs} — окно отмены режима message
248
258
  inputActions: null,
249
259
  input: null,
250
- settings: { vadSilenceMs: 700, autoSendMs: 4000 },
260
+ settings: { vadSilenceMs: 700, autoSendMs: 4000, stream: false, streamChunkMs: 1200, vadAdapt: 0 },
251
261
  listeners: new Set(),
252
262
  notify() { this.listeners.forEach((l) => l()) },
253
263
  set(patch) { Object.assign(this, patch); this.notify() },
@@ -535,6 +545,7 @@ window.__ModuleLoader__.load({
535
545
  audioCtx: null, analyser: null,
536
546
  cutting: false, closing: false,
537
547
  silenceMs: 0, hadSpeech: false,
548
+ streamMs: 0,
538
549
  }
539
550
  recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) rec.chunks.push(e.data) }
540
551
  const AC = typeof AudioContext !== 'undefined' ? AudioContext
@@ -573,6 +584,7 @@ window.__ModuleLoader__.load({
573
584
  rec.chunks = []
574
585
  rec.silenceMs = 0
575
586
  rec.hadSpeech = false
587
+ rec.streamMs = 0
576
588
  if (!rec.closing) {
577
589
  try { rec.recorder.start() } catch (e) { /* поток закрылся */ }
578
590
  }
@@ -833,7 +845,29 @@ window.__ModuleLoader__.load({
833
845
  else if (rec.hadSpeech) rec.silenceMs += tick
834
846
  const speaking = level > 0.06
835
847
  if (voice.speaking !== speaking) { voice.speaking = speaking; voice.notify() }
836
- if (rec.mode === 'dictation' && rec.hadSpeech && rec.silenceMs >= voice.settings.vadSilenceMs) {
848
+ const adapt = Number(voice.settings.vadAdapt) || 0
849
+ let effectiveVad = Number(voice.settings.vadSilenceMs) || 700
850
+ if (adapt > 0 && rec.hadSpeech) {
851
+ // Адаптивный порог (#41): считаем плотность речи за последние
852
+ // ~1s (20 сэмплов). Плотная речь -> порог ниже (точнее режем),
853
+ // паузная -> порог растёт к базе (не режем на вдохе).
854
+ const win = voice.levels.slice(-20)
855
+ const density = win.length ? win.filter((v) => v > 0.06).length / win.length : 0
856
+ const k = adapt * (density - 0.5) * 2
857
+ effectiveVad = Math.max(150, Math.round(Number(voice.settings.vadSilenceMs) * (1 - k)))
858
+ }
859
+ // Непрерывный режим (#40): режем по таймеру непрерывной речи,
860
+ // не дожидаясь длинной паузы.
861
+ const stream = !!voice.settings.stream && rec.mode === 'dictation'
862
+ if (stream && rec.hadSpeech && !rec.cutting) {
863
+ rec.streamMs += tick
864
+ const chunk = Number(voice.settings.streamChunkMs) || 1200
865
+ if (rec.streamMs >= chunk) { cutPhrase(); return }
866
+ } else {
867
+ rec.streamMs = 0
868
+ }
869
+ if (rec.mode === 'dictation' && rec.hadSpeech && rec.silenceMs >= effectiveVad) {
870
+ rec.streamMs = 0
837
871
  cutPhrase()
838
872
  }
839
873
  }, tick)
@@ -973,6 +1007,9 @@ window.__ModuleLoader__.load({
973
1007
  historyLimit: Number(data && data.historyLimit),
974
1008
  voiceCommands: !!(data && data.voiceCommands),
975
1009
  sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
1010
+ stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
1011
+ streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
1012
+ vadAdapt: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadAdapt) || 0,
976
1013
  })
977
1014
  })
978
1015
  .catch(() => { /* без подсказки хоста клавиши просто не будет */ })
@@ -1227,6 +1264,9 @@ window.__ModuleLoader__.load({
1227
1264
  historyLimit: Number(snap && snap.historyLimit),
1228
1265
  voiceCommands: !!(snap && snap.voiceCommands),
1229
1266
  sendDelayMs: Number(value && value.dictation && value.dictation.sendDelayMs) || 0,
1267
+ stream: !!(value && value.dictation && value.dictation.stream),
1268
+ streamChunkMs: Number(value && value.dictation && value.dictation.streamChunkMs) || 1200,
1269
+ vadAdapt: Number(value && value.dictation && value.dictation.vadAdapt) || 0,
1230
1270
  })
1231
1271
  }, [ready, value, snap])
1232
1272
 
@@ -1274,6 +1314,9 @@ window.__ModuleLoader__.load({
1274
1314
  historyLimit: Number(draft.historyLimit),
1275
1315
  voiceCommands: !!draft.voiceCommands,
1276
1316
  sendDelayMs: Number(draft.dictation && draft.dictation.sendDelayMs) || 0,
1317
+ stream: !!(draft.dictation && draft.dictation.stream),
1318
+ streamChunkMs: Number(draft.dictation && draft.dictation.streamChunkMs) || 1200,
1319
+ vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
1277
1320
  })
1278
1321
  // Композер держит обработчик клавиши: пусть перечитает настройку,
1279
1322
  // иначе новая клавиша заработает только после перезагрузки страницы.
@@ -1370,6 +1413,18 @@ window.__ModuleLoader__.load({
1370
1413
  onChange: (e) => setIn('dictation', 'polish', e.target.checked),
1371
1414
  })),
1372
1415
  React.createElement('div', { className: 'dvs-sub' }, t('polishHint')),
1416
+ React.createElement('label', { className: 'dvs-field', title: t('streamHint') }, t('stream'),
1417
+ React.createElement('input', {
1418
+ type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.stream), disabled: !writable,
1419
+ onChange: (e) => setIn('dictation', 'stream', e.target.checked),
1420
+ })),
1421
+ numField('dictation', 'streamChunkMs', t('streamChunkMs'), t('streamHint')),
1422
+ React.createElement('label', { className: 'dvs-field', title: t('vadAdaptHint') }, t('vadAdapt'),
1423
+ React.createElement('input', {
1424
+ type: 'range', min: 0, max: 1, step: 0.1,
1425
+ value: Number(draft && draft.dictation && draft.dictation.vadAdapt) || 0, disabled: !writable,
1426
+ onChange: (e) => setIn('dictation', 'vadAdapt', Number(e.target.value)),
1427
+ })),
1373
1428
  ),
1374
1429
  React.createElement('div', { className: 'dvs-block' },
1375
1430
  React.createElement('div', { className: 'dvs-h' }, t('messageTitle')),
package/lib/index.js CHANGED
@@ -69,6 +69,12 @@ export const Config = z.object({
69
69
  .description('Dictation: wait this many ms after a phrase before appending it, with a cancel window. 0 disables the delay.'),
70
70
  polish: z.boolean().default(false)
71
71
  .description('Polish the transcript through the harness model before inserting: punctuation, paragraphs, filler-word removal.'),
72
+ stream: z.boolean().default(false)
73
+ .description('Continuous dictation: cut phrases by a timer instead of waiting for a long silence, so text flows while you speak.'),
74
+ streamChunkMs: z.number().default(1200)
75
+ .description('Continuous dictation: phrase length in ms of uninterrupted speech before the chunk is sent.'),
76
+ vadAdapt: z.number().min(0).max(1).default(0)
77
+ .description('Adaptive silence threshold: 0 = fixed (current behaviour); >0 shrinks the threshold during dense speech and grows it during pauses.'),
72
78
  }).default({}),
73
79
  message: z.object({
74
80
  chain: z.array(ChainEntry)
@@ -271,6 +277,8 @@ export function apply(ctx, baseConfig) {
271
277
  chain: cfg.dictation.chain, language: cfg.dictation.language,
272
278
  vadSilenceMs: cfg.dictation.vadSilenceMs,
273
279
  sendDelayMs: cfg.dictation.sendDelayMs, polish: cfg.dictation.polish,
280
+ stream: cfg.dictation.stream, streamChunkMs: cfg.dictation.streamChunkMs,
281
+ vadAdapt: cfg.dictation.vadAdapt,
274
282
  },
275
283
  message: {
276
284
  chain: cfg.message.chain, language: cfg.message.language,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
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",