@goodandready/dsh-voice 0.9.12 → 0.9.14
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/README.md +2 -2
- package/README.ru.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +70 -47
- package/lib/config-routes.js +4 -0
- package/lib/http-util.js +43 -5
- package/lib/index.js +8 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ graph LR
|
|
|
76
76
|
* 🎮 **Tactile Push-to-Talk**:
|
|
77
77
|
* **Mouse**: Hold the wave button — releasing sends the message; dragging pointer away discards.
|
|
78
78
|
* **Keyboard**: Hold <kbd>Ctrl</kbd> (or custom hotkey) for hands-free speaking; press <kbd>Esc</kbd> to cancel.
|
|
79
|
-
* ⚡ **
|
|
79
|
+
* ⚡ **Live In-Browser Captions (`browser`)**: Web Speech API speech-to-text with real-time floating captions as you speak (Note: standard browser recognition typically streams audio to vendor servers like Google in Chrome; for true 100% offline privacy, use local `whisper` or `sensevoice`).
|
|
80
80
|
* 🛡️ **Ironclad Multi-Provider Fallbacks**: If your primary cloud provider runs out of credits or hits a 429 rate limit, requests seamlessly fail over down the chain.
|
|
81
81
|
* 🧠 **Context Glossary Injection**: Automatically extracts code variables and identifiers from your composer draft to steer STT model accuracy on technical jargon.
|
|
82
82
|
* 🎵 **Embedded Audio Player**: Preview, scrubber, and playback of your recorded voice message directly in chat and the composer dock.
|
|
@@ -151,7 +151,7 @@ Quality batch after the v0.8.18 review (Gitea #79–#87, PR #88):
|
|
|
151
151
|
|
|
152
152
|
| Provider Key | Service Backend | Default Model | Credential Ref | Features & Notes |
|
|
153
153
|
|---|---|---|---|---|
|
|
154
|
-
| `browser` | Web Speech API | Native Browser | *None* | Zero latency, floating live captions in Chrome |
|
|
154
|
+
| `browser` | Web Speech API | Native Browser | *None* | Zero latency, floating live captions in Chrome (vendor server-based; not offline) |
|
|
155
155
|
| `deepgram` | Deepgram API | `nova-2` | `DEEPGRAM_API_KEY` | Ultra-fast cloud transcription |
|
|
156
156
|
| `groq` | Groq Whisper | `whisper-large-v3-turbo` | `GROQ_API_KEY` | Near-instant inference speed |
|
|
157
157
|
| `hf` | HuggingFace Inference | `openai/whisper-large-v3` | `HF_TOKEN` | High-accuracy open Whisper |
|
package/README.ru.md
CHANGED
|
@@ -76,7 +76,7 @@ graph LR
|
|
|
76
76
|
* 🎮 **Тактильный Push-to-Talk**:
|
|
77
77
|
* **Мышь**: зажмите кнопку волны — запись идёт пока кнопка зажата; отпускание отправляет, увод курсора отменяет запись.
|
|
78
78
|
* **Клавиатура**: зажмите <kbd>Ctrl</kbd> для записи без мыши; нажмите <kbd>Esc</kbd> для отмены.
|
|
79
|
-
* ⚡ **Субтитры браузера в реальном времени (`browser`)**:
|
|
79
|
+
* ⚡ **Субтитры браузера в реальном времени (`browser`)**: распознавание Web Speech API с плавающими субтитрами в реальном времени (Внимание: стандартный браузерный движок в Chrome отправляет аудио на серверы Google; для полностью локальной приватной работы без внешних вызовов используйте локальный `whisper` или `sensevoice`).
|
|
80
80
|
* 🛡️ **Надёжные цепочки фолбеков**: при исчерпании квоты или ошибке 429 плагин мгновенно обращается к следующему провайдеру в списке.
|
|
81
81
|
* 🧠 **Контекстный словарь (Context Glossary)**: автоматическое извлечение переменных и технических терминов из черновика композера для точного распознавания редких слов и кода.
|
|
82
82
|
* 🎵 **Встроенный аудиоплеер**: предпросмотр и воспроизведение записанного голосового сообщения в чате и доке перед отправкой или для переслушивания.
|
|
@@ -151,7 +151,7 @@ Quality-батч после ревью v0.8.18 (Gitea #79–#87, PR #88):
|
|
|
151
151
|
|
|
152
152
|
| Ключ | Сервис | Модель по умолчанию | Переменная секрета | Особенности |
|
|
153
153
|
|---|---|---|---|---|
|
|
154
|
-
| `browser` | Web Speech API | Нативная в браузере | *Не требуется* | Нулевая задержка, живые субтитры в Chrome |
|
|
154
|
+
| `browser` | Web Speech API | Нативная в браузере | *Не требуется* | Нулевая задержка, живые субтитры в Chrome (обработка на серверах вендора; не является офлайн) |
|
|
155
155
|
| `deepgram` | Deepgram API | `nova-2` | `DEEPGRAM_API_KEY` | Сверхбыстрая облачная транскрипция |
|
|
156
156
|
| `groq` | Groq Whisper | `whisper-large-v3-turbo` | `GROQ_API_KEY` | Мгновенная скорость генерации |
|
|
157
157
|
| `hf` | HuggingFace Inference | `openai/whisper-large-v3` | `HF_TOKEN` | Высокоточный облачный Whisper |
|
package/README.zh.md
CHANGED
|
@@ -76,7 +76,7 @@ graph LR
|
|
|
76
76
|
* 🎮 **沉浸式 Push-to-Talk 对讲**:
|
|
77
77
|
* **鼠标操作**:按住声波按钮开始录音,松开发送,拖离按钮取消。
|
|
78
78
|
* **键盘操作**:按住 <kbd>Ctrl</kbd> 无需鼠标即刻说话,按 <kbd>Esc</kbd> 放弃本次录音。
|
|
79
|
-
* ⚡
|
|
79
|
+
* ⚡ **浏览器实时同声字幕 (`browser`)**:Web Speech API 实时语音识别与浮动同声字幕(注意:Chrome 等标准浏览器通常将音频发送至服务商云端解析;若需 100% 本地离线隐私保护,请使用本地 `whisper` 或 `sensevoice`)。
|
|
80
80
|
* 🛡️ **多服务商自动容灾切换**:首选 API 额度耗尽或遭遇 429 限流时,毫秒级顺位切换备用引擎。
|
|
81
81
|
* 🧠 **上下文术语注入 (Context Glossary)**:自动从输入草稿中提取代码变量名与专业术语,引导 STT 模型精准转写专业词汇。
|
|
82
82
|
* 🎵 **内嵌音频播放器**:在输入框与录音浮层中随时试听和回放刚刚录制的原始音频片段。
|
|
@@ -120,7 +120,7 @@ v0.8.18 质量审查后的修复批次(Gitea #79–#87,PR #88):
|
|
|
120
120
|
|
|
121
121
|
| 服务商标识 | 对应引擎 | 默认模型 | 环境变量凭据 | 特性说明 |
|
|
122
122
|
|---|---|---|---|---|
|
|
123
|
-
| `browser` | Web Speech API | 浏览器原生引擎 | *无需密钥* |
|
|
123
|
+
| `browser` | Web Speech API | 浏览器原生引擎 | *无需密钥* | 零延迟同声字幕输出(依赖浏览器厂商云端解析;非离线本地) |
|
|
124
124
|
| `deepgram` | Deepgram API | `nova-2` | `DEEPGRAM_API_KEY` | 极速高精云端转写 |
|
|
125
125
|
| `groq` | Groq Whisper | `whisper-large-v3-turbo` | `GROQ_API_KEY` | 毫秒级极速推理 |
|
|
126
126
|
| `hf` | HuggingFace Inference | `openai/whisper-large-v3` | `HF_TOKEN` | 经典高精度开源模型 |
|
package/lib/client.js
CHANGED
|
@@ -235,6 +235,10 @@ window.__ModuleLoader__.load({
|
|
|
235
235
|
'jargonFrom': 'Spoken slang',
|
|
236
236
|
'jargonTo': 'Correct spelling',
|
|
237
237
|
'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
|
|
238
|
+
'voiceInput': 'Voice input',
|
|
239
|
+
'assistantSpeakingGated': 'Assistant speaking (gated mode)',
|
|
240
|
+
'jargonFormatHint': ' (spoken -> replacement)',
|
|
241
|
+
'errorPrefix': 'Error',
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
const zh = {
|
|
@@ -438,11 +442,15 @@ window.__ModuleLoader__.load({
|
|
|
438
442
|
'jargonFrom': '口语词汇',
|
|
439
443
|
'jargonTo': '标准拼写',
|
|
440
444
|
'jargonPlaceholder': 'k8s -> kubernetes\npr -> pull request',
|
|
445
|
+
'voiceInput': '语音输入',
|
|
446
|
+
'assistantSpeakingGated': '助手正在发言(门控模式)',
|
|
447
|
+
'jargonFormatHint': ' (口语 -> 替换文本)',
|
|
448
|
+
'errorPrefix': '错误',
|
|
441
449
|
}
|
|
442
450
|
|
|
443
451
|
// Strings are also needed outside components — in recording handlers and
|
|
444
452
|
// slot labels — so the translator is module-level, not only via props.
|
|
445
|
-
let moduleT = (key) => key
|
|
453
|
+
let moduleT = (key) => en[key] || key
|
|
446
454
|
const t = (key) => moduleT(key)
|
|
447
455
|
// ------------------------------------------------------------------ css
|
|
448
456
|
const CSS =
|
|
@@ -862,6 +870,50 @@ window.__ModuleLoader__.load({
|
|
|
862
870
|
return main
|
|
863
871
|
}
|
|
864
872
|
|
|
873
|
+
function hotkeyMatches(event, name) {
|
|
874
|
+
if (!event || !name) return false
|
|
875
|
+
if (typeof name === 'string' && name.includes('+')) {
|
|
876
|
+
const parts = name.split('+')
|
|
877
|
+
const main = parts[parts.length - 1]
|
|
878
|
+
const needCtrl = parts.includes('Control')
|
|
879
|
+
const needAlt = parts.includes('Alt')
|
|
880
|
+
const needShift = parts.includes('Shift')
|
|
881
|
+
const needMeta = parts.includes('Meta')
|
|
882
|
+
if (needCtrl && !event.ctrlKey) return false
|
|
883
|
+
if (needAlt && !event.altKey) return false
|
|
884
|
+
if (needShift && !event.shiftKey) return false
|
|
885
|
+
if (needMeta && !event.metaKey) return false
|
|
886
|
+
return event.code === main || event.key === main
|
|
887
|
+
}
|
|
888
|
+
if (name === 'Control') return event.key === 'Control'
|
|
889
|
+
if (name === 'Alt') return event.key === 'Alt'
|
|
890
|
+
if (name === 'Shift') return event.key === 'Shift'
|
|
891
|
+
if (name === 'Meta') return event.key === 'Meta'
|
|
892
|
+
return event.code === name || event.key === name
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isAllowedInEditable(name) {
|
|
896
|
+
if (!name) return false
|
|
897
|
+
if (['Control', 'Alt', 'Shift', 'Meta'].includes(name)) return true
|
|
898
|
+
if (/^F\d{1,2}$/.test(name)) return true
|
|
899
|
+
if (typeof name === 'string' && name.includes('+')) return true
|
|
900
|
+
if (['Tab', 'Pause', 'ScrollLock', 'Insert'].includes(name)) return true
|
|
901
|
+
return false
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function isHotkeyRelease(event, name) {
|
|
905
|
+
if (!event || !name) return false
|
|
906
|
+
if (typeof name === 'string' && name.includes('+')) {
|
|
907
|
+
const parts = name.split('+')
|
|
908
|
+
if (event.code === parts[parts.length - 1] || event.key === parts[parts.length - 1]) return true
|
|
909
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
910
|
+
if (event.key === parts[i] || event.code === parts[i]) return true
|
|
911
|
+
}
|
|
912
|
+
return false
|
|
913
|
+
}
|
|
914
|
+
return event.code === name || event.key === name
|
|
915
|
+
}
|
|
916
|
+
|
|
865
917
|
// Announce that the user started speaking.
|
|
866
918
|
//
|
|
867
919
|
// Playback should mute immediately: listening and talking at once is
|
|
@@ -1216,7 +1268,7 @@ window.__ModuleLoader__.load({
|
|
|
1216
1268
|
if (voice.settings.bargeIn) {
|
|
1217
1269
|
triggerBargeIn()
|
|
1218
1270
|
} else if (voice.settings.gatedTurnTaking !== false) {
|
|
1219
|
-
voice.set({ phase: 'error', error: '
|
|
1271
|
+
voice.set({ phase: 'error', error: t('assistantSpeakingGated') })
|
|
1220
1272
|
return
|
|
1221
1273
|
}
|
|
1222
1274
|
}
|
|
@@ -1281,35 +1333,7 @@ window.__ModuleLoader__.load({
|
|
|
1281
1333
|
if (cancelled) cancelCurrent()
|
|
1282
1334
|
else stopCurrent()
|
|
1283
1335
|
}
|
|
1284
|
-
|
|
1285
|
-
if (!event || !name) return false
|
|
1286
|
-
if (typeof name === 'string' && name.includes('+')) {
|
|
1287
|
-
const parts = name.split('+')
|
|
1288
|
-
const main = parts[parts.length - 1]
|
|
1289
|
-
const needCtrl = parts.includes('Control')
|
|
1290
|
-
const needAlt = parts.includes('Alt')
|
|
1291
|
-
const needShift = parts.includes('Shift')
|
|
1292
|
-
const needMeta = parts.includes('Meta')
|
|
1293
|
-
if (needCtrl && !event.ctrlKey) return false
|
|
1294
|
-
if (needAlt && !event.altKey) return false
|
|
1295
|
-
if (needShift && !event.shiftKey) return false
|
|
1296
|
-
if (needMeta && !event.metaKey) return false
|
|
1297
|
-
return event.code === main || event.key === main
|
|
1298
|
-
}
|
|
1299
|
-
if (name === 'Control') return event.key === 'Control'
|
|
1300
|
-
if (name === 'Alt') return event.key === 'Alt'
|
|
1301
|
-
if (name === 'Shift') return event.key === 'Shift'
|
|
1302
|
-
if (name === 'Meta') return event.key === 'Meta'
|
|
1303
|
-
return event.code === name || event.key === name
|
|
1304
|
-
}
|
|
1305
|
-
function isAllowedInEditable(name) {
|
|
1306
|
-
if (!name) return false
|
|
1307
|
-
if (['Control', 'Alt', 'Shift', 'Meta'].includes(name)) return true
|
|
1308
|
-
if (/^F\d{1,2}$/.test(name)) return true
|
|
1309
|
-
if (typeof name === 'string' && name.includes('+')) return true
|
|
1310
|
-
if (['Tab', 'Pause', 'ScrollLock', 'Insert'].includes(name)) return true
|
|
1311
|
-
return false
|
|
1312
|
-
}
|
|
1336
|
+
|
|
1313
1337
|
let activeHotkeyCleanup = null
|
|
1314
1338
|
function clearGlobalHotkey() {
|
|
1315
1339
|
if (typeof activeHotkeyCleanup === 'function') {
|
|
@@ -1336,18 +1360,7 @@ window.__ModuleLoader__.load({
|
|
|
1336
1360
|
beginHold(mode)
|
|
1337
1361
|
}
|
|
1338
1362
|
}
|
|
1339
|
-
|
|
1340
|
-
if (!event || !name) return false
|
|
1341
|
-
if (typeof name === 'string' && name.includes('+')) {
|
|
1342
|
-
const parts = name.split('+')
|
|
1343
|
-
if (event.code === parts[parts.length - 1] || event.key === parts[parts.length - 1]) return true
|
|
1344
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1345
|
-
if (event.key === parts[i] || event.code === parts[i]) return true
|
|
1346
|
-
}
|
|
1347
|
-
return false
|
|
1348
|
-
}
|
|
1349
|
-
return event.code === name || event.key === name
|
|
1350
|
-
}
|
|
1363
|
+
|
|
1351
1364
|
const up = (event) => {
|
|
1352
1365
|
if (hold.armed && isHotkeyRelease(event, keyName)) {
|
|
1353
1366
|
event.preventDefault()
|
|
@@ -2386,7 +2399,7 @@ window.__ModuleLoader__.load({
|
|
|
2386
2399
|
}
|
|
2387
2400
|
}
|
|
2388
2401
|
const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
|
|
2389
|
-
return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ?
|
|
2402
|
+
return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ? `${t('errorPrefix')}: ${item.lastError}` : '' },
|
|
2390
2403
|
React.createElement('div', { className: 'dvo-dash-name' }, key),
|
|
2391
2404
|
React.createElement('div', { className: 'dvo-dash-row' },
|
|
2392
2405
|
React.createElement('span', { className: 'dvo-badge ' + badgeClass }, badgeText),
|
|
@@ -2757,7 +2770,7 @@ window.__ModuleLoader__.load({
|
|
|
2757
2770
|
if (!res.ok || data.ok === false) {
|
|
2758
2771
|
throw new Error(data.error || `HTTP ${res.status}`)
|
|
2759
2772
|
}
|
|
2760
|
-
setUpdaterMsg(data.message || '
|
|
2773
|
+
setUpdaterMsg(data.message || t('updateSuccess'))
|
|
2761
2774
|
checkUpdater()
|
|
2762
2775
|
} catch (err) {
|
|
2763
2776
|
setUpdaterMsg(err && err.message ? err.message : String(err))
|
|
@@ -2791,7 +2804,7 @@ window.__ModuleLoader__.load({
|
|
|
2791
2804
|
t('techJargonCorrection'),
|
|
2792
2805
|
),
|
|
2793
2806
|
React.createElement('div', { className: 'cb-field' },
|
|
2794
|
-
React.createElement('label', null, t('jargonAdd') +
|
|
2807
|
+
React.createElement('label', null, t('jargonAdd') + t('jargonFormatHint')),
|
|
2795
2808
|
React.createElement('textarea', {
|
|
2796
2809
|
rows: 3,
|
|
2797
2810
|
disabled: !writable,
|
|
@@ -3551,7 +3564,7 @@ window.__ModuleLoader__.load({
|
|
|
3551
3564
|
name: 'plugins.item',
|
|
3552
3565
|
id: ROW_ID,
|
|
3553
3566
|
order: 60,
|
|
3554
|
-
label: () => '
|
|
3567
|
+
label: () => t('voiceInput'),
|
|
3555
3568
|
locale: NS,
|
|
3556
3569
|
inject: () => ({ ctx: ctx }),
|
|
3557
3570
|
},
|
|
@@ -3607,6 +3620,16 @@ window.__ModuleLoader__.load({
|
|
|
3607
3620
|
registerComposer(ctx)
|
|
3608
3621
|
registerSettings(ctx)
|
|
3609
3622
|
}
|
|
3623
|
+
exports._test = {
|
|
3624
|
+
voice,
|
|
3625
|
+
keyLabel,
|
|
3626
|
+
keyFromEvent,
|
|
3627
|
+
hotkeyMatches,
|
|
3628
|
+
isAllowedInEditable,
|
|
3629
|
+
isHotkeyRelease,
|
|
3630
|
+
extractContextKeywords,
|
|
3631
|
+
resolveBrowserRecognitionLang,
|
|
3632
|
+
}
|
|
3610
3633
|
return module.exports
|
|
3611
3634
|
},
|
|
3612
3635
|
})
|
package/lib/config-routes.js
CHANGED
|
@@ -51,6 +51,10 @@ export function registerConfigRoutes(ctx, {
|
|
|
51
51
|
try {
|
|
52
52
|
raw = await readBody(req, 1024 * 1024)
|
|
53
53
|
} catch (e) {
|
|
54
|
+
if (e && (e.statusCode === 413 || e.message === 'body too large')) {
|
|
55
|
+
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: 'payload too large' } })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
54
58
|
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
55
59
|
return
|
|
56
60
|
}
|
package/lib/http-util.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
// Pure HTTP helpers shared by host routes. No cordis, no network.
|
|
2
2
|
|
|
3
|
+
export class BodyTooLargeError extends Error {
|
|
4
|
+
constructor(message = 'body too large') {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = 'BodyTooLargeError'
|
|
7
|
+
this.statusCode = 413
|
|
8
|
+
this.code = 'ERR_BODY_TOO_LARGE'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export function writeJson(res, code, body) {
|
|
4
13
|
try {
|
|
5
|
-
|
|
14
|
+
const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }
|
|
15
|
+
if (code === 413) {
|
|
16
|
+
headers['Connection'] = 'close'
|
|
17
|
+
}
|
|
18
|
+
res.writeHead(code, headers)
|
|
6
19
|
res.end(JSON.stringify(body))
|
|
20
|
+
if (code === 413 && res && typeof res.on === 'function') {
|
|
21
|
+
res.on('finish', () => {
|
|
22
|
+
const sock = res.socket || (res.req && res.req.socket)
|
|
23
|
+
if (sock && !sock.destroyed && typeof sock.destroy === 'function') {
|
|
24
|
+
sock.destroy()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
7
28
|
} catch { /* socket may already be closed */ }
|
|
8
29
|
}
|
|
9
30
|
|
|
@@ -17,13 +38,30 @@ export function readBody(req, maxBytes) {
|
|
|
17
38
|
return new Promise((resolve, reject) => {
|
|
18
39
|
const chunks = []
|
|
19
40
|
let size = 0
|
|
20
|
-
|
|
41
|
+
let exceeded = false
|
|
42
|
+
const onData = (c) => {
|
|
43
|
+
if (exceeded) return
|
|
21
44
|
size += c.length
|
|
22
|
-
if (size > maxBytes) {
|
|
45
|
+
if (size > maxBytes) {
|
|
46
|
+
exceeded = true
|
|
47
|
+
if (typeof req.removeListener === 'function') {
|
|
48
|
+
req.removeListener('data', onData)
|
|
49
|
+
}
|
|
50
|
+
if (typeof req.resume === 'function') {
|
|
51
|
+
req.resume()
|
|
52
|
+
}
|
|
53
|
+
reject(new BodyTooLargeError('body too large'))
|
|
54
|
+
return
|
|
55
|
+
}
|
|
23
56
|
chunks.push(c)
|
|
57
|
+
}
|
|
58
|
+
req.on('data', onData)
|
|
59
|
+
req.on('end', () => {
|
|
60
|
+
if (!exceeded) resolve(Buffer.concat(chunks))
|
|
61
|
+
})
|
|
62
|
+
req.on('error', (err) => {
|
|
63
|
+
if (!exceeded) reject(err)
|
|
24
64
|
})
|
|
25
|
-
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
26
|
-
req.on('error', reject)
|
|
27
65
|
})
|
|
28
66
|
}
|
|
29
67
|
|
package/lib/index.js
CHANGED
|
@@ -421,7 +421,14 @@ export function apply(ctx, baseConfig) {
|
|
|
421
421
|
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
422
422
|
const cfg = live()
|
|
423
423
|
let raw
|
|
424
|
-
try { raw = await readBody(req, 1024 * 1024) } catch (e) {
|
|
424
|
+
try { raw = await readBody(req, 1024 * 1024) } catch (e) {
|
|
425
|
+
if (e && (e.statusCode === 413 || e.message === 'body too large')) {
|
|
426
|
+
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: 'body exceeds limit of 1MB' } })
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
430
|
+
return
|
|
431
|
+
}
|
|
425
432
|
let payload = {}
|
|
426
433
|
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { /* empty */ }
|
|
427
434
|
const text = typeof payload.text === 'string' ? payload.text.trim() : ''
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test": "node --test test/*.test.mjs",
|
|
45
45
|
"build:client": "node scripts/build-client.mjs",
|
|
46
46
|
"build:check": "node scripts/build-client.mjs --check",
|
|
47
|
-
"pretest": "node scripts/build-client.mjs",
|
|
48
|
-
"lint": "node
|
|
47
|
+
"pretest": "node scripts/build-client.mjs && node scripts/lint.mjs && node scripts/pack-check.mjs",
|
|
48
|
+
"lint": "node scripts/lint.mjs",
|
|
49
49
|
"pack:check": "node scripts/pack-check.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dsh": {
|