@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 +57 -2
- package/lib/index.js +8 -0
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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",
|