@goodandready/dsh-voice 0.9.8 → 0.9.10
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 +21 -15
- package/lib/index.js +117 -34
- package/lib/polish.js +10 -0
- package/lib/updater.js +32 -24
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -922,7 +922,6 @@ window.__ModuleLoader__.load({
|
|
|
922
922
|
}
|
|
923
923
|
})
|
|
924
924
|
}
|
|
925
|
-
|
|
926
925
|
// ------------------------------------------------------------ recording
|
|
927
926
|
function teardown(rec) {
|
|
928
927
|
if (!rec) return
|
|
@@ -932,7 +931,6 @@ window.__ModuleLoader__.load({
|
|
|
932
931
|
if (rec.analyser) { try { rec.analyser.disconnect() } catch (e) { /* already disconnected */ } }
|
|
933
932
|
if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
|
|
934
933
|
}
|
|
935
|
-
|
|
936
934
|
if (typeof window !== 'undefined') {
|
|
937
935
|
const onPageUnload = () => {
|
|
938
936
|
if (voice.rec) teardown(voice.rec)
|
|
@@ -943,12 +941,10 @@ window.__ModuleLoader__.load({
|
|
|
943
941
|
window.addEventListener('pagehide', onPageUnload)
|
|
944
942
|
window.addEventListener('beforeunload', onPageUnload)
|
|
945
943
|
}
|
|
946
|
-
|
|
947
944
|
function waitStop(recorder) {
|
|
948
945
|
if (!recorder || recorder.state === 'inactive') return Promise.resolve()
|
|
949
946
|
return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
950
947
|
}
|
|
951
|
-
|
|
952
948
|
async function openMic(mode) {
|
|
953
949
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
954
950
|
throw new Error(t('micUnavailable'))
|
|
@@ -982,7 +978,6 @@ window.__ModuleLoader__.load({
|
|
|
982
978
|
if (rec.audioCtx.state === 'suspended') { rec.audioCtx.resume().catch(() => {}) }
|
|
983
979
|
const src = rec.audioCtx.createMediaStreamSource(stream)
|
|
984
980
|
rec.srcNode = src
|
|
985
|
-
|
|
986
981
|
let lastNode = src
|
|
987
982
|
try {
|
|
988
983
|
const filter = rec.audioCtx.createBiquadFilter()
|
|
@@ -993,7 +988,6 @@ window.__ModuleLoader__.load({
|
|
|
993
988
|
lastNode = filter
|
|
994
989
|
rec.filterNode = filter
|
|
995
990
|
} catch (e) { /* audio filter fallback */ }
|
|
996
|
-
|
|
997
991
|
rec.analyser = rec.audioCtx.createAnalyser()
|
|
998
992
|
rec.analyser.fftSize = 128
|
|
999
993
|
lastNode.connect(rec.analyser)
|
|
@@ -1002,7 +996,6 @@ window.__ModuleLoader__.load({
|
|
|
1002
996
|
recorder.start()
|
|
1003
997
|
return rec
|
|
1004
998
|
}
|
|
1005
|
-
|
|
1006
999
|
function currentLevel(rec) {
|
|
1007
1000
|
if (!rec || !rec.analyser) return 0
|
|
1008
1001
|
const data = new Uint8Array(rec.analyser.frequencyBinCount)
|
|
@@ -1010,7 +1003,6 @@ window.__ModuleLoader__.load({
|
|
|
1010
1003
|
let sum = 0
|
|
1011
1004
|
for (let i = 0; i < data.length; i++) sum += data[i]
|
|
1012
1005
|
const raw = Math.min(1, (sum / data.length / 255) * 2.2)
|
|
1013
|
-
|
|
1014
1006
|
const gateDb = Number(voice.settings && voice.settings.noiseGateDb !== undefined ? voice.settings.noiseGateDb : -45)
|
|
1015
1007
|
if (gateDb > -90) {
|
|
1016
1008
|
const gateAmp = Math.pow(10, gateDb / 20) * 2.2
|
|
@@ -1018,11 +1010,9 @@ window.__ModuleLoader__.load({
|
|
|
1018
1010
|
}
|
|
1019
1011
|
return raw
|
|
1020
1012
|
}
|
|
1021
|
-
|
|
1022
1013
|
// Cut the current phrase: stop the recorder, send the finished file and
|
|
1023
1014
|
// immediately start a new recording with the same recorder.
|
|
1024
1015
|
let dictationQueue = Promise.resolve()
|
|
1025
|
-
|
|
1026
1016
|
function cutPhrase() {
|
|
1027
1017
|
const rec = voice.rec
|
|
1028
1018
|
if (!rec || rec.cutting || rec.closing) return
|
|
@@ -1037,6 +1027,7 @@ window.__ModuleLoader__.load({
|
|
|
1037
1027
|
return
|
|
1038
1028
|
}
|
|
1039
1029
|
stopped.then(async () => {
|
|
1030
|
+
if (rec.cancelled || voice.activeOpId !== opId) return
|
|
1040
1031
|
const blob = new Blob(rec.chunks, { type: rec.mime })
|
|
1041
1032
|
rec.chunks = []
|
|
1042
1033
|
rec.silenceMs = 0
|
|
@@ -1107,6 +1098,7 @@ window.__ModuleLoader__.load({
|
|
|
1107
1098
|
// Browser recognition instead of file recording. Returns false when the
|
|
1108
1099
|
// browser cannot do it — then fall back to the normal path.
|
|
1109
1100
|
function startBrowserLeg(mode, language) {
|
|
1101
|
+
if (voice.settings && voice.settings.localOnly) return false
|
|
1110
1102
|
if (!browserRecognitionAvailable()) return false
|
|
1111
1103
|
const finals = []
|
|
1112
1104
|
voice.caption = ''
|
|
@@ -1163,6 +1155,7 @@ window.__ModuleLoader__.load({
|
|
|
1163
1155
|
|
|
1164
1156
|
function startRecording(mode) {
|
|
1165
1157
|
if (voice.phase !== 'idle' && voice.phase !== 'error') return
|
|
1158
|
+
voice.activeOpId = (voice.activeOpId || 0) + 1
|
|
1166
1159
|
if (voice.isTtsSpeaking) {
|
|
1167
1160
|
if (voice.settings.bargeIn) {
|
|
1168
1161
|
triggerBargeIn()
|
|
@@ -1326,6 +1319,7 @@ window.__ModuleLoader__.load({
|
|
|
1326
1319
|
}
|
|
1327
1320
|
|
|
1328
1321
|
function cancelCurrent() {
|
|
1322
|
+
voice.activeOpId = (voice.activeOpId || 0) + 1
|
|
1329
1323
|
announceVoice('end')
|
|
1330
1324
|
focusComposer()
|
|
1331
1325
|
if (voice.browser) {
|
|
@@ -1372,6 +1366,8 @@ window.__ModuleLoader__.load({
|
|
|
1372
1366
|
const rec = voice.rec
|
|
1373
1367
|
if (!rec || rec.closing) return
|
|
1374
1368
|
rec.closing = true
|
|
1369
|
+
voice.activeOpId = (voice.activeOpId || 0) + 1
|
|
1370
|
+
const opId = voice.activeOpId
|
|
1375
1371
|
const mode = rec.mode
|
|
1376
1372
|
const stopped = waitStop(rec.recorder)
|
|
1377
1373
|
try {
|
|
@@ -1391,6 +1387,7 @@ window.__ModuleLoader__.load({
|
|
|
1391
1387
|
voice.set({ phase: 'processing' })
|
|
1392
1388
|
try {
|
|
1393
1389
|
const out = await sendAudio(blob, rec.mime, mode)
|
|
1390
|
+
if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1394
1391
|
if (out.command) { runSessionCommand(out.command); return }
|
|
1395
1392
|
const text = out.text || ''
|
|
1396
1393
|
const action = out.action || null
|
|
@@ -1429,6 +1426,7 @@ window.__ModuleLoader__.load({
|
|
|
1429
1426
|
voice.set({ phase: 'idle' })
|
|
1430
1427
|
}
|
|
1431
1428
|
} catch (e) {
|
|
1429
|
+
if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
|
|
1432
1430
|
voice.set({ phase: 'error', error: String(e && e.message ? e.message : e) })
|
|
1433
1431
|
}
|
|
1434
1432
|
})
|
|
@@ -1980,9 +1978,12 @@ window.__ModuleLoader__.load({
|
|
|
1980
1978
|
// The composer needs fresh settings without opening the card.
|
|
1981
1979
|
Object.assign(voice.settings, {
|
|
1982
1980
|
beep: !!(data && data.beep),
|
|
1981
|
+
localOnly: !!(data && data.localOnly),
|
|
1983
1982
|
micDeviceId: String((data && data.micDeviceId) || ''),
|
|
1984
1983
|
historyLimit: Number(data && data.historyLimit),
|
|
1985
1984
|
voiceCommands: !!(data && data.voiceCommands),
|
|
1985
|
+
vadSilenceMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadSilenceMs) || 700,
|
|
1986
|
+
autoSendMs: Number(data && data.modes && data.modes.message && data.modes.message.autoSendMs) || 4000,
|
|
1986
1987
|
sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
|
|
1987
1988
|
stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
|
|
1988
1989
|
streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
|
|
@@ -2673,7 +2674,8 @@ window.__ModuleLoader__.load({
|
|
|
2673
2674
|
const draft = props.draft
|
|
2674
2675
|
const setTop = props.setTop
|
|
2675
2676
|
const writable = props.writable
|
|
2676
|
-
const
|
|
2677
|
+
const jargonInput = props.jargonInput
|
|
2678
|
+
const setJargonInput = props.setJargonInput
|
|
2677
2679
|
|
|
2678
2680
|
return React.createElement('div', { className: 'cb-section-card' },
|
|
2679
2681
|
React.createElement('div', { className: 'cb-section-title' },
|
|
@@ -2694,14 +2696,14 @@ window.__ModuleLoader__.load({
|
|
|
2694
2696
|
rows: 3,
|
|
2695
2697
|
disabled: !writable,
|
|
2696
2698
|
placeholder: t('jargonPlaceholder'),
|
|
2697
|
-
value: jargonInput !== null
|
|
2699
|
+
value: jargonInput !== null && jargonInput !== undefined
|
|
2698
2700
|
? jargonInput
|
|
2699
2701
|
: (Array.isArray(draft && draft.jargonDictionary)
|
|
2700
2702
|
? draft.jargonDictionary.map((item) => item && item.from ? `${item.from} -> ${item.to}` : '').filter(Boolean).join('\n')
|
|
2701
2703
|
: ''),
|
|
2702
|
-
onChange: (e) => setJargonInput(e.target.value),
|
|
2704
|
+
onChange: (e) => setJargonInput && setJargonInput(e.target.value),
|
|
2703
2705
|
onBlur: () => {
|
|
2704
|
-
if (jargonInput === null) return
|
|
2706
|
+
if (jargonInput === null || jargonInput === undefined) return
|
|
2705
2707
|
const lines = jargonInput.split('\n')
|
|
2706
2708
|
const parsed = lines
|
|
2707
2709
|
.map((line) => {
|
|
@@ -2883,6 +2885,7 @@ window.__ModuleLoader__.load({
|
|
|
2883
2885
|
|
|
2884
2886
|
// Hotkey picker: not a name field — press the key you want.
|
|
2885
2887
|
const [catching, setCatching] = React.useState(false)
|
|
2888
|
+
const [jargonInput, setJargonInput] = React.useState(null)
|
|
2886
2889
|
React.useEffect(() => {
|
|
2887
2890
|
if (!catching) return undefined
|
|
2888
2891
|
const onKey = (event) => {
|
|
@@ -2956,6 +2959,7 @@ window.__ModuleLoader__.load({
|
|
|
2956
2959
|
vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
|
|
2957
2960
|
autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
|
|
2958
2961
|
beep: !!val.beep,
|
|
2962
|
+
localOnly: !!val.localOnly,
|
|
2959
2963
|
micDeviceId: String(val.micDeviceId || ''),
|
|
2960
2964
|
historyLimit: Number(val.historyLimit) || 20,
|
|
2961
2965
|
voiceCommands: !!val.voiceCommands,
|
|
@@ -3099,7 +3103,7 @@ window.__ModuleLoader__.load({
|
|
|
3099
3103
|
})
|
|
3100
3104
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
3101
3105
|
|
|
3102
|
-
setSaved(true)
|
|
3106
|
+
setSaved(true); setJargonInput(null)
|
|
3103
3107
|
setTimeout(() => setSaved(false), 2000)
|
|
3104
3108
|
fetchStatus()
|
|
3105
3109
|
}
|
|
@@ -3356,6 +3360,8 @@ window.__ModuleLoader__.load({
|
|
|
3356
3360
|
draft: draft,
|
|
3357
3361
|
setTop: setTop,
|
|
3358
3362
|
writable: writable,
|
|
3363
|
+
jargonInput: jargonInput,
|
|
3364
|
+
setJargonInput: setJargonInput,
|
|
3359
3365
|
}),
|
|
3360
3366
|
|
|
3361
3367
|
// 10. Provider Telemetry & Latency Dashboard
|
package/lib/index.js
CHANGED
|
@@ -92,27 +92,38 @@ export function apply(ctx, baseConfig) {
|
|
|
92
92
|
async function startWhisper() {
|
|
93
93
|
if (startingWhisper) return false
|
|
94
94
|
startingWhisper = true
|
|
95
|
-
const cfg = live()
|
|
96
|
-
if (!cfg.autoStart) return false
|
|
97
|
-
// Without a model path there is nothing to start: the package cannot know
|
|
98
|
-
// where a user's model lives and must not launch a random binary.
|
|
99
|
-
if (!cfg.whisperModel) return false
|
|
100
|
-
if (await whisperAlive()) {
|
|
101
|
-
whisperError = null
|
|
102
|
-
return true
|
|
103
|
-
}
|
|
104
95
|
try {
|
|
96
|
+
const cfg = live()
|
|
97
|
+
if (!cfg.autoStart) return false
|
|
98
|
+
// Without a model path there is nothing to start: the package cannot know
|
|
99
|
+
// where a user's model lives and must not launch a random binary.
|
|
100
|
+
if (!cfg.whisperModel) return false
|
|
101
|
+
if (await whisperAlive()) {
|
|
102
|
+
whisperError = null
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
105
|
if (child && typeof child.kill === 'function') {
|
|
106
106
|
try { child.kill() } catch { /* ignore */ }
|
|
107
107
|
child = null
|
|
108
108
|
}
|
|
109
|
-
const spec = ctx.shell
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
const spec = typeof ctx.shell?.resolve === 'function'
|
|
110
|
+
? ctx.shell.resolve({
|
|
111
|
+
command: `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
|
|
112
|
+
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`,
|
|
113
|
+
timeoutMs: 0,
|
|
114
|
+
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
115
|
+
})
|
|
116
|
+
: {
|
|
117
|
+
command: `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
|
|
118
|
+
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${isAutoLang(cfg.dictation.language) ? 'auto' : cfg.dictation.language}`,
|
|
119
|
+
timeoutMs: 0,
|
|
120
|
+
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
121
|
+
}
|
|
122
|
+
child = typeof ctx.shell?.execute === 'function'
|
|
123
|
+
? ctx.shell.execute(spec)
|
|
124
|
+
: typeof ctx.shell?.start === 'function'
|
|
125
|
+
? ctx.shell.start(spec)
|
|
126
|
+
: null
|
|
116
127
|
if (child && typeof child.on === 'function') {
|
|
117
128
|
child.on('error', (err) => {
|
|
118
129
|
whisperError = err?.message || String(err)
|
|
@@ -123,6 +134,15 @@ export function apply(ctx, baseConfig) {
|
|
|
123
134
|
}
|
|
124
135
|
})
|
|
125
136
|
}
|
|
137
|
+
if (child && child.done && typeof child.done.then === 'function') {
|
|
138
|
+
child.done.then((res) => {
|
|
139
|
+
if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
|
|
140
|
+
whisperError = `whisper server process exited with code ${res.code}`
|
|
141
|
+
}
|
|
142
|
+
}).catch((err) => {
|
|
143
|
+
whisperError = err?.message || String(err)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
126
146
|
// Wait for it to become healthy (up to 30s for cold model loading on Metal/CPU).
|
|
127
147
|
for (let i = 0; i < 60; i++) {
|
|
128
148
|
await new Promise((r) => setTimeout(r, 500))
|
|
@@ -134,6 +154,10 @@ export function apply(ctx, baseConfig) {
|
|
|
134
154
|
whisperError = `whisper server process exited prematurely with code ${child.exitCode}`
|
|
135
155
|
return false
|
|
136
156
|
}
|
|
157
|
+
if (child && (child.status === 'exited' || child.status === 'failed')) {
|
|
158
|
+
whisperError = `whisper server process exited prematurely with status ${child.status}`
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
137
161
|
}
|
|
138
162
|
whisperError = 'whisper server process started but failed health check on port 8001'
|
|
139
163
|
return false
|
|
@@ -164,14 +188,14 @@ export function apply(ctx, baseConfig) {
|
|
|
164
188
|
async function startSensevoice() {
|
|
165
189
|
if (startingSensevoice) return false
|
|
166
190
|
startingSensevoice = true
|
|
167
|
-
const cfg = live()
|
|
168
|
-
if (!cfg.sensevoiceAutostart) return false
|
|
169
|
-
if (!cfg.sensevoiceModel) return false
|
|
170
|
-
if (await sensevoiceAlive()) {
|
|
171
|
-
sensevoiceError = null
|
|
172
|
-
return true
|
|
173
|
-
}
|
|
174
191
|
try {
|
|
192
|
+
const cfg = live()
|
|
193
|
+
if (!cfg.sensevoiceAutostart) return false
|
|
194
|
+
if (!cfg.sensevoiceModel) return false
|
|
195
|
+
if (await sensevoiceAlive()) {
|
|
196
|
+
sensevoiceError = null
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
175
199
|
if (sensevoiceChild && typeof sensevoiceChild.kill === 'function') {
|
|
176
200
|
try { sensevoiceChild.kill() } catch { /* ignore */ }
|
|
177
201
|
sensevoiceChild = null
|
|
@@ -180,15 +204,28 @@ export function apply(ctx, baseConfig) {
|
|
|
180
204
|
try { port = new URL(cfg.sensevoiceUrl).port || '6006' } catch { /* invalid URL */ }
|
|
181
205
|
const tokensPath = findTokensFile(path.dirname(cfg.sensevoiceModel))
|
|
182
206
|
const tokensArg = tokensPath ? ` --tokens=${JSON.stringify(tokensPath)}` : ''
|
|
183
|
-
const spec = ctx.shell
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
const spec = typeof ctx.shell?.resolve === 'function'
|
|
208
|
+
? ctx.shell.resolve({
|
|
209
|
+
command: `${JSON.stringify(cfg.sensevoiceBin)}`
|
|
210
|
+
+ ` --sense-voice-model=${JSON.stringify(cfg.sensevoiceModel)}`
|
|
211
|
+
+ tokensArg
|
|
212
|
+
+ ` --port=${port} --num-threads=4`,
|
|
213
|
+
timeoutMs: 0,
|
|
214
|
+
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
215
|
+
})
|
|
216
|
+
: {
|
|
217
|
+
command: `${JSON.stringify(cfg.sensevoiceBin)}`
|
|
218
|
+
+ ` --sense-voice-model=${JSON.stringify(cfg.sensevoiceModel)}`
|
|
219
|
+
+ tokensArg
|
|
220
|
+
+ ` --port=${port} --num-threads=4`,
|
|
221
|
+
timeoutMs: 0,
|
|
222
|
+
stdoutMaxBytes: 4 * 1024 * 1024,
|
|
223
|
+
}
|
|
224
|
+
sensevoiceChild = typeof ctx.shell?.execute === 'function'
|
|
225
|
+
? ctx.shell.execute(spec)
|
|
226
|
+
: typeof ctx.shell?.start === 'function'
|
|
227
|
+
? ctx.shell.start(spec)
|
|
228
|
+
: null
|
|
192
229
|
if (sensevoiceChild && typeof sensevoiceChild.on === 'function') {
|
|
193
230
|
sensevoiceChild.on('error', (err) => {
|
|
194
231
|
sensevoiceError = err?.message || String(err)
|
|
@@ -199,6 +236,15 @@ export function apply(ctx, baseConfig) {
|
|
|
199
236
|
}
|
|
200
237
|
})
|
|
201
238
|
}
|
|
239
|
+
if (sensevoiceChild && sensevoiceChild.done && typeof sensevoiceChild.done.then === 'function') {
|
|
240
|
+
sensevoiceChild.done.then((res) => {
|
|
241
|
+
if (res && res.code !== 0 && res.code !== null && res.code !== undefined) {
|
|
242
|
+
sensevoiceError = `sensevoice process exited with code ${res.code}`
|
|
243
|
+
}
|
|
244
|
+
}).catch((err) => {
|
|
245
|
+
sensevoiceError = err?.message || String(err)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
202
248
|
// Wait for it to become healthy (up to 30s).
|
|
203
249
|
for (let i = 0; i < 60; i++) {
|
|
204
250
|
await new Promise((r) => setTimeout(r, 500))
|
|
@@ -210,6 +256,10 @@ export function apply(ctx, baseConfig) {
|
|
|
210
256
|
sensevoiceError = `sensevoice process exited prematurely with code ${sensevoiceChild.exitCode}`
|
|
211
257
|
return false
|
|
212
258
|
}
|
|
259
|
+
if (sensevoiceChild && (sensevoiceChild.status === 'exited' || sensevoiceChild.status === 'failed')) {
|
|
260
|
+
sensevoiceError = `sensevoice process exited prematurely with status ${sensevoiceChild.status}`
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
213
263
|
}
|
|
214
264
|
sensevoiceError = 'sensevoice process started but failed health check'
|
|
215
265
|
return false
|
|
@@ -486,8 +536,41 @@ export function apply(ctx, baseConfig) {
|
|
|
486
536
|
liveConfig: live,
|
|
487
537
|
isAlive: sensevoiceAlive,
|
|
488
538
|
updateConfig: async (patch) => {
|
|
489
|
-
|
|
490
|
-
|
|
539
|
+
let saved = false
|
|
540
|
+
const current = live()
|
|
541
|
+
const merged = { ...current, ...patch }
|
|
542
|
+
if (settingsService) {
|
|
543
|
+
let rev
|
|
544
|
+
try {
|
|
545
|
+
rev = settingsService.describe?.()?.find?.((r) => r.ns === NS)?.revision
|
|
546
|
+
} catch { /* ignore revision lookup errors */ }
|
|
547
|
+
|
|
548
|
+
if (typeof settingsService.replace === 'function') {
|
|
549
|
+
await settingsService.replace(NS, merged, rev)
|
|
550
|
+
saved = true
|
|
551
|
+
} else if (typeof settingsService.update === 'function') {
|
|
552
|
+
await settingsService.update(NS, merged, rev)
|
|
553
|
+
saved = true
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (scope) {
|
|
558
|
+
if (typeof scope.replace === 'function') {
|
|
559
|
+
await scope.replace(merged)
|
|
560
|
+
saved = true
|
|
561
|
+
} else if (typeof scope.patch === 'function') {
|
|
562
|
+
await scope.patch(merged)
|
|
563
|
+
saved = true
|
|
564
|
+
} else if (typeof scope.set === 'function') {
|
|
565
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
566
|
+
await scope.set(k, v).catch(() => {})
|
|
567
|
+
}
|
|
568
|
+
saved = true
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!saved) {
|
|
573
|
+
throw new Error('failed to persist settings: neither settingsService nor scope available')
|
|
491
574
|
}
|
|
492
575
|
},
|
|
493
576
|
startSensevoice,
|
package/lib/polish.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLoopback } from './http-util.js'
|
|
1
2
|
// Transcript polish. Pure factory: inject fetch, key resolver, LLM, config.
|
|
2
3
|
// On any failure return the original text — polish must never block input.
|
|
3
4
|
|
|
@@ -14,6 +15,15 @@ export function createPolishText({ resolveKey, fetchImpl, getConfig, llm, getAge
|
|
|
14
15
|
const enable = modeCfg && (modeCfg.polish === true || modeCfg.polishSend === true)
|
|
15
16
|
if (!enable || !text) return text
|
|
16
17
|
const cfg = getConfig() || {}
|
|
18
|
+
if (cfg.localOnly) {
|
|
19
|
+
if (!cfg.polishBaseUrl) return text
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(cfg.polishBaseUrl)
|
|
22
|
+
if (!isLoopback(u.hostname)) return text
|
|
23
|
+
} catch {
|
|
24
|
+
return text
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
const ask =
|
|
18
28
|
'Fix the punctuation and spelling of this dictated text and split it into '
|
|
19
29
|
+ 'paragraphs where the speaker changes topic. Remove filler words ("um", "uh", '
|
package/lib/updater.js
CHANGED
|
@@ -198,23 +198,25 @@ export function checkProfileLock(profileDir) {
|
|
|
198
198
|
const lockPath = resolve(profileDir, 'package.json.lock');
|
|
199
199
|
if (!existsSync(lockPath)) return { locked: false };
|
|
200
200
|
const pid = readLockPid(lockPath);
|
|
201
|
-
if (pid
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
if (pid !== null) {
|
|
202
|
+
if (isProcessAlive(pid)) {
|
|
203
|
+
return { locked: true, pid };
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
unlinkSync(lockPath);
|
|
207
|
+
return { locked: false, cleanedStale: true };
|
|
208
|
+
} catch {
|
|
209
|
+
return { locked: false };
|
|
210
|
+
}
|
|
210
211
|
}
|
|
212
|
+
return { locked: true, pid: null };
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
async function installExact(target, packageSpec, options) {
|
|
214
216
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
|
|
215
217
|
const lockCheck = checkProfileLock(target.profileDir);
|
|
216
218
|
if (lockCheck.locked) {
|
|
217
|
-
throw new Error(`Another package installation is currently in progress (PID ${lockCheck.pid}).`);
|
|
219
|
+
throw new Error(`Another package installation is currently in progress${lockCheck.pid ? ` (PID ${lockCheck.pid})` : ''}.`);
|
|
218
220
|
}
|
|
219
221
|
await new Promise((resolvePromise, reject) => {
|
|
220
222
|
const child = spawn(process.execPath, [
|
|
@@ -228,28 +230,34 @@ async function installExact(target, packageSpec, options) {
|
|
|
228
230
|
env: { ...process.env, NO_COLOR: '1' },
|
|
229
231
|
});
|
|
230
232
|
let detail = '';
|
|
233
|
+
let timedOut = false;
|
|
234
|
+
let killTimer = null;
|
|
231
235
|
child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
232
236
|
child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
checkProfileLock(target.profileDir);
|
|
240
|
-
}, 1000).unref?.();
|
|
241
|
-
reject(new Error('Update timed out; use the normal DSH update flow.'));
|
|
237
|
+
const timeoutTimer = setTimeout(() => {
|
|
238
|
+
timedOut = true;
|
|
239
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
240
|
+
killTimer = setTimeout(() => {
|
|
241
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
242
|
+
}, 1000);
|
|
242
243
|
}, UPDATE_TIMEOUT_MS);
|
|
243
244
|
child.once('error', error => {
|
|
244
|
-
clearTimeout(
|
|
245
|
+
clearTimeout(timeoutTimer);
|
|
246
|
+
if (killTimer) clearTimeout(killTimer);
|
|
245
247
|
checkProfileLock(target.profileDir);
|
|
246
248
|
reject(error);
|
|
247
249
|
});
|
|
248
250
|
child.once('exit', code => {
|
|
249
|
-
clearTimeout(
|
|
251
|
+
clearTimeout(timeoutTimer);
|
|
252
|
+
if (killTimer) clearTimeout(killTimer);
|
|
250
253
|
checkProfileLock(target.profileDir);
|
|
251
|
-
if (
|
|
252
|
-
|
|
254
|
+
if (timedOut) {
|
|
255
|
+
reject(new Error('Update timed out; use the normal DSH update flow.'));
|
|
256
|
+
} else if (code === 0) {
|
|
257
|
+
resolvePromise();
|
|
258
|
+
} else {
|
|
259
|
+
reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
|
|
260
|
+
}
|
|
253
261
|
});
|
|
254
262
|
});
|
|
255
263
|
}
|
|
@@ -293,7 +301,7 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
293
301
|
if (installing || lockStatus.locked) {
|
|
294
302
|
json(response, 409, {
|
|
295
303
|
error: lockStatus.locked
|
|
296
|
-
? `Another package installation is currently in progress (PID ${lockStatus.pid}).`
|
|
304
|
+
? `Another package installation is currently in progress${lockStatus.pid ? ` (PID ${lockStatus.pid})` : ''}.`
|
|
297
305
|
: 'This plugin is already updating.',
|
|
298
306
|
});
|
|
299
307
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
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",
|