@goodandready/dsh-voice 0.9.29 → 0.9.31
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 +252 -249
- package/README.ru.md +251 -248
- package/README.zh.md +179 -176
- package/lib/client.js +346 -42
- package/lib/host-routes.js +188 -188
- package/lib/local-daemon.js +2 -2
- package/lib/schema.js +161 -160
- package/lib/updater.js +18 -3
- package/package.json +72 -72
package/lib/host-routes.js
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
// lib/host-routes.js — Host HTTP routes for dsh-voice status, polish, and transcribe
|
|
2
|
-
import { writeJson, readBody, isTrustedCaller } from './http-util.js'
|
|
3
|
-
import { resolveSensevoiceProvider } from './sensevoice-installer.js'
|
|
4
|
-
|
|
5
|
-
export function registerHostRoutes(ctx, {
|
|
6
|
-
live,
|
|
7
|
-
daemons,
|
|
8
|
-
statsTracker,
|
|
9
|
-
transcribe,
|
|
10
|
-
polishText,
|
|
11
|
-
sessionCommand,
|
|
12
|
-
extractVoiceActions,
|
|
13
|
-
applyJargonDictionary,
|
|
14
|
-
KNOWN_KEYS,
|
|
15
|
-
}) {
|
|
16
|
-
const { whisperAlive, sensevoiceAlive, getWhisperError, getSensevoiceError, startWhisper, startSensevoice } = daemons
|
|
17
|
-
|
|
18
|
-
ctx.effect(() => ctx.webServer.register({
|
|
19
|
-
kind: 'exact',
|
|
20
|
-
path: '/dsh-voice/status',
|
|
21
|
-
handler: async (req, res) => {
|
|
22
|
-
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
23
|
-
const cfg = live()
|
|
24
|
-
const whisperOk = await whisperAlive()
|
|
25
|
-
const sensevoiceOk = await sensevoiceAlive()
|
|
26
|
-
const whisperError = whisperOk ? null : getWhisperError()
|
|
27
|
-
const sensevoiceError = sensevoiceOk ? null : getSensevoiceError()
|
|
28
|
-
writeJson(res, 200, {
|
|
29
|
-
ok: true,
|
|
30
|
-
whisperRunning: whisperOk,
|
|
31
|
-
whisperError: whisperOk ? null : whisperError,
|
|
32
|
-
sensevoiceRunning: sensevoiceOk,
|
|
33
|
-
sensevoiceError: sensevoiceOk ? null : sensevoiceError,
|
|
34
|
-
hotkey: cfg.hotkey,
|
|
35
|
-
providers: KNOWN_KEYS.concat(
|
|
36
|
-
(Array.isArray(cfg.customProviders) ? cfg.customProviders : [])
|
|
37
|
-
.map((c) => String(c && c.key || '').trim()).filter(Boolean),
|
|
38
|
-
),
|
|
39
|
-
modes: {
|
|
40
|
-
dictation: {
|
|
41
|
-
chain: cfg.dictation.chain, language: cfg.dictation.language,
|
|
42
|
-
vadSilenceMs: cfg.dictation.vadSilenceMs,
|
|
43
|
-
sendDelayMs: cfg.dictation.sendDelayMs, polish: cfg.dictation.polish,
|
|
44
|
-
stream: cfg.dictation.stream, streamChunkMs: cfg.dictation.streamChunkMs,
|
|
45
|
-
vadAdapt: cfg.dictation.vadAdapt,
|
|
46
|
-
},
|
|
47
|
-
message: {
|
|
48
|
-
chain: cfg.message.chain, language: cfg.message.language,
|
|
49
|
-
autoSendMs: cfg.message.autoSendMs, polish: cfg.message.polish,
|
|
50
|
-
sessionCommands: cfg.message.sessionCommands, polishSend: cfg.message.polishSend,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
beep: cfg.beep,
|
|
54
|
-
localOnly: cfg.localOnly,
|
|
55
|
-
micDeviceId: cfg.micDeviceId,
|
|
56
|
-
noiseGateDb: cfg.noiseGateDb !== undefined ? cfg.noiseGateDb : -45,
|
|
57
|
-
historyLimit: cfg.historyLimit,
|
|
58
|
-
voiceCommands: cfg.voiceCommands,
|
|
59
|
-
wakeWord: String(cfg.wakeWord || ''),
|
|
60
|
-
bargeIn: !!cfg.bargeIn,
|
|
61
|
-
polishBaseUrl: String(cfg.polishBaseUrl || ''),
|
|
62
|
-
noiseSuppression: cfg.noiseSuppression !== false,
|
|
63
|
-
contextGlossary: cfg.contextGlossary !== false,
|
|
64
|
-
sensevoiceUrl: cfg.sensevoiceUrl,
|
|
65
|
-
sensevoiceAutostart: !!cfg.sensevoiceAutostart,
|
|
66
|
-
sensevoiceProvider: cfg.sensevoiceProvider || 'cpu',
|
|
67
|
-
effectiveSensevoiceProvider: resolveSensevoiceProvider(cfg),
|
|
68
|
-
sensevoiceThreads: cfg.sensevoiceThreads || 4,
|
|
69
|
-
webgpuModel: cfg.webgpuModel || 'onnx-community/whisper-tiny',
|
|
70
|
-
deepgramBaseUrl: cfg.deepgramBaseUrl || 'https://api.deepgram.com',
|
|
71
|
-
visualizerStyle: cfg.visualizerStyle || 'liquid-wave',
|
|
72
|
-
gatedTurnTaking: cfg.gatedTurnTaking !== false,
|
|
73
|
-
autoSendVisualRing: cfg.autoSendVisualRing !== false,
|
|
74
|
-
voiceActions: cfg.voiceActions !== false,
|
|
75
|
-
techJargonCorrection: cfg.techJargonCorrection !== false,
|
|
76
|
-
jargonDictionary: Array.isArray(cfg.jargonDictionary) ? cfg.jargonDictionary : [],
|
|
77
|
-
liveInterimPreview: cfg.liveInterimPreview !== false,
|
|
78
|
-
structuredPromptVoice: cfg.structuredPromptVoice !== false,
|
|
79
|
-
providerStats: statsTracker.get(),
|
|
80
|
-
})
|
|
81
|
-
},
|
|
82
|
-
}), 'dsh-voice: /status route')
|
|
83
|
-
|
|
84
|
-
ctx.effect(() => ctx.webServer.register({
|
|
85
|
-
kind: 'exact',
|
|
86
|
-
path: '/dsh-voice/polish',
|
|
87
|
-
handler: async (req, res) => {
|
|
88
|
-
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
89
|
-
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
90
|
-
const cfg = live()
|
|
91
|
-
let raw
|
|
92
|
-
try { raw = await readBody(req, 1024 * 1024) } catch (e) {
|
|
93
|
-
if (e && (e.statusCode === 413 || e.message === 'body too large')) {
|
|
94
|
-
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: 'body exceeds limit of 1MB' } })
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
let payload = {}
|
|
101
|
-
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { /* empty */ }
|
|
102
|
-
const text = typeof payload.text === 'string' ? payload.text.trim() : ''
|
|
103
|
-
if (!text) { writeJson(res, 400, { ok: false, error: { code: 'empty', message: 'text required' } }); return }
|
|
104
|
-
const controller = new AbortController()
|
|
105
|
-
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
106
|
-
try {
|
|
107
|
-
const out = await polishText(text, { polish: true }, controller.signal)
|
|
108
|
-
writeJson(res, 200, { ok: true, text: out })
|
|
109
|
-
} catch (e) {
|
|
110
|
-
writeJson(res, 502, { ok: false, error: { code: 'polish', message: String(e && e.message || e) } })
|
|
111
|
-
} finally { clearTimeout(timer) }
|
|
112
|
-
},
|
|
113
|
-
}), 'dsh-voice: /polish route')
|
|
114
|
-
|
|
115
|
-
ctx.effect(() => ctx.webServer.register({
|
|
116
|
-
kind: 'exact',
|
|
117
|
-
path: '/dsh-voice/transcribe',
|
|
118
|
-
handler: async (req, res) => {
|
|
119
|
-
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
120
|
-
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
121
|
-
const cfg = live()
|
|
122
|
-
const maxBodyBytes = Math.ceil((cfg.maxFileBytes * 4) / 3) + 64 * 1024
|
|
123
|
-
let raw
|
|
124
|
-
try {
|
|
125
|
-
raw = await readBody(req, maxBodyBytes)
|
|
126
|
-
} catch (e) {
|
|
127
|
-
if (e && e.message === 'body too large') {
|
|
128
|
-
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: `audio body exceeds limit for max ${cfg.maxFileBytes} bytes` } })
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } }); return
|
|
132
|
-
}
|
|
133
|
-
let payload
|
|
134
|
-
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { payload = {} }
|
|
135
|
-
|
|
136
|
-
const dataBase64 = typeof payload.dataBase64 === 'string' ? payload.dataBase64 : ''
|
|
137
|
-
if (!dataBase64) { writeJson(res, 400, { ok: false, error: { code: 'no-audio', message: 'no audio data' } }); return }
|
|
138
|
-
const mime = typeof payload.mimeType === 'string' && payload.mimeType ? payload.mimeType : 'audio/webm'
|
|
139
|
-
const baseModeCfg = payload.mode === 'message' ? cfg.message : cfg.dictation
|
|
140
|
-
const modeCfg = Array.isArray(payload.chain)
|
|
141
|
-
? { ...baseModeCfg, chain: payload.chain }
|
|
142
|
-
: baseModeCfg
|
|
143
|
-
|
|
144
|
-
let bytes
|
|
145
|
-
try { bytes = Buffer.from(dataBase64, 'base64') } catch { bytes = null }
|
|
146
|
-
if (!bytes || bytes.length === 0) {
|
|
147
|
-
writeJson(res, 400, { ok: false, error: { code: 'decode', message: 'failed to decode audio' } }); return
|
|
148
|
-
}
|
|
149
|
-
if (bytes.length > cfg.maxFileBytes) {
|
|
150
|
-
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: `audio is ${bytes.length} bytes, max ${cfg.maxFileBytes}` } }); return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if ((modeCfg.chain || []).some((e) => e.provider === 'local-whisper') && !(await whisperAlive())) {
|
|
154
|
-
await startWhisper()
|
|
155
|
-
}
|
|
156
|
-
if ((modeCfg.chain || []).some((e) => e.provider === 'sensevoice') && !(await sensevoiceAlive())) {
|
|
157
|
-
await startSensevoice()
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const controller = new AbortController()
|
|
161
|
-
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
162
|
-
try {
|
|
163
|
-
const out = await transcribe(modeCfg, bytes, mime, controller.signal, payload.contextWords)
|
|
164
|
-
if (payload.mode === 'message' && modeCfg.sessionCommands === true) {
|
|
165
|
-
const cmd = sessionCommand(out.text)
|
|
166
|
-
if (cmd) { writeJson(res, 200, { ok: true, command: cmd, provider: out.provider, tookMs: out.tookMs }); return }
|
|
167
|
-
}
|
|
168
|
-
let text = await polishText(out.text, modeCfg, controller.signal)
|
|
169
|
-
if (cfg.techJargonCorrection !== false) {
|
|
170
|
-
text = applyJargonDictionary(text, cfg.jargonDictionary)
|
|
171
|
-
}
|
|
172
|
-
let action = null
|
|
173
|
-
if (cfg.voiceActions !== false) {
|
|
174
|
-
const actionRes = extractVoiceActions(text)
|
|
175
|
-
if (actionRes.action) {
|
|
176
|
-
text = actionRes.text
|
|
177
|
-
action = actionRes.action
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
writeJson(res, 200, { ok: true, text, action, provider: out.provider, tookMs: out.tookMs })
|
|
181
|
-
} catch (e) {
|
|
182
|
-
writeJson(res, 502, { ok: false, error: { code: 'chain', message: String(e && e.message || e) } })
|
|
183
|
-
} finally {
|
|
184
|
-
clearTimeout(timer)
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
}), 'dsh-voice: /transcribe route')
|
|
188
|
-
}
|
|
1
|
+
// lib/host-routes.js — Host HTTP routes for dsh-voice status, polish, and transcribe
|
|
2
|
+
import { writeJson, readBody, isTrustedCaller } from './http-util.js'
|
|
3
|
+
import { resolveSensevoiceProvider } from './sensevoice-installer.js'
|
|
4
|
+
|
|
5
|
+
export function registerHostRoutes(ctx, {
|
|
6
|
+
live,
|
|
7
|
+
daemons,
|
|
8
|
+
statsTracker,
|
|
9
|
+
transcribe,
|
|
10
|
+
polishText,
|
|
11
|
+
sessionCommand,
|
|
12
|
+
extractVoiceActions,
|
|
13
|
+
applyJargonDictionary,
|
|
14
|
+
KNOWN_KEYS,
|
|
15
|
+
}) {
|
|
16
|
+
const { whisperAlive, sensevoiceAlive, getWhisperError, getSensevoiceError, startWhisper, startSensevoice } = daemons
|
|
17
|
+
|
|
18
|
+
ctx.effect(() => ctx.webServer.register({
|
|
19
|
+
kind: 'exact',
|
|
20
|
+
path: '/dsh-voice/status',
|
|
21
|
+
handler: async (req, res) => {
|
|
22
|
+
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
23
|
+
const cfg = live()
|
|
24
|
+
const whisperOk = await whisperAlive()
|
|
25
|
+
const sensevoiceOk = await sensevoiceAlive()
|
|
26
|
+
const whisperError = whisperOk ? null : getWhisperError()
|
|
27
|
+
const sensevoiceError = sensevoiceOk ? null : getSensevoiceError()
|
|
28
|
+
writeJson(res, 200, {
|
|
29
|
+
ok: true,
|
|
30
|
+
whisperRunning: whisperOk,
|
|
31
|
+
whisperError: whisperOk ? null : whisperError,
|
|
32
|
+
sensevoiceRunning: sensevoiceOk,
|
|
33
|
+
sensevoiceError: sensevoiceOk ? null : sensevoiceError,
|
|
34
|
+
hotkey: cfg.hotkey,
|
|
35
|
+
providers: KNOWN_KEYS.concat(
|
|
36
|
+
(Array.isArray(cfg.customProviders) ? cfg.customProviders : [])
|
|
37
|
+
.map((c) => String(c && c.key || '').trim()).filter(Boolean),
|
|
38
|
+
),
|
|
39
|
+
modes: {
|
|
40
|
+
dictation: {
|
|
41
|
+
chain: cfg.dictation.chain, language: cfg.dictation.language,
|
|
42
|
+
vadSilenceMs: cfg.dictation.vadSilenceMs,
|
|
43
|
+
sendDelayMs: cfg.dictation.sendDelayMs, polish: cfg.dictation.polish,
|
|
44
|
+
stream: cfg.dictation.stream, streamChunkMs: cfg.dictation.streamChunkMs,
|
|
45
|
+
vadAdapt: cfg.dictation.vadAdapt,
|
|
46
|
+
},
|
|
47
|
+
message: {
|
|
48
|
+
chain: cfg.message.chain, language: cfg.message.language,
|
|
49
|
+
autoSendMs: cfg.message.autoSendMs, polish: cfg.message.polish,
|
|
50
|
+
sessionCommands: cfg.message.sessionCommands, polishSend: cfg.message.polishSend, editablePreview: cfg.message.editablePreview !== false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
beep: cfg.beep,
|
|
54
|
+
localOnly: cfg.localOnly,
|
|
55
|
+
micDeviceId: cfg.micDeviceId,
|
|
56
|
+
noiseGateDb: cfg.noiseGateDb !== undefined ? cfg.noiseGateDb : -45,
|
|
57
|
+
historyLimit: cfg.historyLimit,
|
|
58
|
+
voiceCommands: cfg.voiceCommands,
|
|
59
|
+
wakeWord: String(cfg.wakeWord || ''),
|
|
60
|
+
bargeIn: !!cfg.bargeIn,
|
|
61
|
+
polishBaseUrl: String(cfg.polishBaseUrl || ''),
|
|
62
|
+
noiseSuppression: cfg.noiseSuppression !== false,
|
|
63
|
+
contextGlossary: cfg.contextGlossary !== false,
|
|
64
|
+
sensevoiceUrl: cfg.sensevoiceUrl,
|
|
65
|
+
sensevoiceAutostart: !!cfg.sensevoiceAutostart,
|
|
66
|
+
sensevoiceProvider: cfg.sensevoiceProvider || 'cpu',
|
|
67
|
+
effectiveSensevoiceProvider: resolveSensevoiceProvider(cfg),
|
|
68
|
+
sensevoiceThreads: cfg.sensevoiceThreads || 4,
|
|
69
|
+
webgpuModel: cfg.webgpuModel || 'onnx-community/whisper-tiny',
|
|
70
|
+
deepgramBaseUrl: cfg.deepgramBaseUrl || 'https://api.deepgram.com',
|
|
71
|
+
visualizerStyle: cfg.visualizerStyle || 'liquid-wave',
|
|
72
|
+
gatedTurnTaking: cfg.gatedTurnTaking !== false,
|
|
73
|
+
autoSendVisualRing: cfg.autoSendVisualRing !== false,
|
|
74
|
+
voiceActions: cfg.voiceActions !== false,
|
|
75
|
+
techJargonCorrection: cfg.techJargonCorrection !== false,
|
|
76
|
+
jargonDictionary: Array.isArray(cfg.jargonDictionary) ? cfg.jargonDictionary : [],
|
|
77
|
+
liveInterimPreview: cfg.liveInterimPreview !== false,
|
|
78
|
+
structuredPromptVoice: cfg.structuredPromptVoice !== false,
|
|
79
|
+
providerStats: statsTracker.get(),
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
}), 'dsh-voice: /status route')
|
|
83
|
+
|
|
84
|
+
ctx.effect(() => ctx.webServer.register({
|
|
85
|
+
kind: 'exact',
|
|
86
|
+
path: '/dsh-voice/polish',
|
|
87
|
+
handler: async (req, res) => {
|
|
88
|
+
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
89
|
+
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
90
|
+
const cfg = live()
|
|
91
|
+
let raw
|
|
92
|
+
try { raw = await readBody(req, 1024 * 1024) } catch (e) {
|
|
93
|
+
if (e && (e.statusCode === 413 || e.message === 'body too large')) {
|
|
94
|
+
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: 'body exceeds limit of 1MB' } })
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
let payload = {}
|
|
101
|
+
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { /* empty */ }
|
|
102
|
+
const text = typeof payload.text === 'string' ? payload.text.trim() : ''
|
|
103
|
+
if (!text) { writeJson(res, 400, { ok: false, error: { code: 'empty', message: 'text required' } }); return }
|
|
104
|
+
const controller = new AbortController()
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
106
|
+
try {
|
|
107
|
+
const out = await polishText(text, { polish: true }, controller.signal)
|
|
108
|
+
writeJson(res, 200, { ok: true, text: out })
|
|
109
|
+
} catch (e) {
|
|
110
|
+
writeJson(res, 502, { ok: false, error: { code: 'polish', message: String(e && e.message || e) } })
|
|
111
|
+
} finally { clearTimeout(timer) }
|
|
112
|
+
},
|
|
113
|
+
}), 'dsh-voice: /polish route')
|
|
114
|
+
|
|
115
|
+
ctx.effect(() => ctx.webServer.register({
|
|
116
|
+
kind: 'exact',
|
|
117
|
+
path: '/dsh-voice/transcribe',
|
|
118
|
+
handler: async (req, res) => {
|
|
119
|
+
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
120
|
+
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
121
|
+
const cfg = live()
|
|
122
|
+
const maxBodyBytes = Math.ceil((cfg.maxFileBytes * 4) / 3) + 64 * 1024
|
|
123
|
+
let raw
|
|
124
|
+
try {
|
|
125
|
+
raw = await readBody(req, maxBodyBytes)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e && e.message === 'body too large') {
|
|
128
|
+
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: `audio body exceeds limit for max ${cfg.maxFileBytes} bytes` } })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
writeJson(res, 400, { ok: false, error: { code: 'body', message: e.message } }); return
|
|
132
|
+
}
|
|
133
|
+
let payload
|
|
134
|
+
try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { payload = {} }
|
|
135
|
+
|
|
136
|
+
const dataBase64 = typeof payload.dataBase64 === 'string' ? payload.dataBase64 : ''
|
|
137
|
+
if (!dataBase64) { writeJson(res, 400, { ok: false, error: { code: 'no-audio', message: 'no audio data' } }); return }
|
|
138
|
+
const mime = typeof payload.mimeType === 'string' && payload.mimeType ? payload.mimeType : 'audio/webm'
|
|
139
|
+
const baseModeCfg = payload.mode === 'message' ? cfg.message : cfg.dictation
|
|
140
|
+
const modeCfg = Array.isArray(payload.chain)
|
|
141
|
+
? { ...baseModeCfg, chain: payload.chain }
|
|
142
|
+
: baseModeCfg
|
|
143
|
+
|
|
144
|
+
let bytes
|
|
145
|
+
try { bytes = Buffer.from(dataBase64, 'base64') } catch { bytes = null }
|
|
146
|
+
if (!bytes || bytes.length === 0) {
|
|
147
|
+
writeJson(res, 400, { ok: false, error: { code: 'decode', message: 'failed to decode audio' } }); return
|
|
148
|
+
}
|
|
149
|
+
if (bytes.length > cfg.maxFileBytes) {
|
|
150
|
+
writeJson(res, 413, { ok: false, error: { code: 'too-large', message: `audio is ${bytes.length} bytes, max ${cfg.maxFileBytes}` } }); return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ((modeCfg.chain || []).some((e) => e.provider === 'local-whisper') && !(await whisperAlive())) {
|
|
154
|
+
await startWhisper()
|
|
155
|
+
}
|
|
156
|
+
if ((modeCfg.chain || []).some((e) => e.provider === 'sensevoice') && !(await sensevoiceAlive())) {
|
|
157
|
+
await startSensevoice()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const controller = new AbortController()
|
|
161
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs)
|
|
162
|
+
try {
|
|
163
|
+
const out = await transcribe(modeCfg, bytes, mime, controller.signal, payload.contextWords)
|
|
164
|
+
if (payload.mode === 'message' && modeCfg.sessionCommands === true) {
|
|
165
|
+
const cmd = sessionCommand(out.text)
|
|
166
|
+
if (cmd) { writeJson(res, 200, { ok: true, command: cmd, provider: out.provider, tookMs: out.tookMs }); return }
|
|
167
|
+
}
|
|
168
|
+
let text = await polishText(out.text, modeCfg, controller.signal)
|
|
169
|
+
if (cfg.techJargonCorrection !== false) {
|
|
170
|
+
text = applyJargonDictionary(text, cfg.jargonDictionary)
|
|
171
|
+
}
|
|
172
|
+
let action = null
|
|
173
|
+
if (cfg.voiceActions !== false) {
|
|
174
|
+
const actionRes = extractVoiceActions(text)
|
|
175
|
+
if (actionRes.action) {
|
|
176
|
+
text = actionRes.text
|
|
177
|
+
action = actionRes.action
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
writeJson(res, 200, { ok: true, text, action, provider: out.provider, tookMs: out.tookMs })
|
|
181
|
+
} catch (e) {
|
|
182
|
+
writeJson(res, 502, { ok: false, error: { code: 'chain', message: String(e && e.message || e) } })
|
|
183
|
+
} finally {
|
|
184
|
+
clearTimeout(timer)
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
}), 'dsh-voice: /transcribe route')
|
|
188
|
+
}
|
package/lib/local-daemon.js
CHANGED
|
@@ -37,7 +37,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
37
37
|
}
|
|
38
38
|
const command = `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
|
|
39
39
|
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`
|
|
40
|
-
const specOpts = { command, onExpiry: 'none', timeoutMs:
|
|
40
|
+
const specOpts = { command, onExpiry: 'none', timeoutMs: 60000, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
41
41
|
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
42
42
|
|
|
43
43
|
if (typeof ctx.shell?.execute === 'function') {
|
|
@@ -216,7 +216,7 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
216
216
|
try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
|
|
217
217
|
const cmdArgs = buildSensevoiceArgs(cfg, port)
|
|
218
218
|
const commandStr = `${JSON.stringify(cfg.sensevoiceBin)}${cmdArgs}`
|
|
219
|
-
const specOpts = { command: commandStr, onExpiry: 'none', timeoutMs:
|
|
219
|
+
const specOpts = { command: commandStr, onExpiry: 'none', timeoutMs: 60000, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
220
220
|
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
221
221
|
|
|
222
222
|
if (typeof ctx.shell?.execute === 'function') {
|