@goodandready/dsh-voice 0.9.35 → 0.9.36
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 +58 -57
- package/lib/host-routes.js +3 -2
- package/lib/http-util.js +46 -6
- package/lib/index.js +7 -11
- package/lib/local-daemon.js +3 -1
- package/lib/sensevoice-installer.js +34 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -618,61 +618,61 @@ window.__ModuleLoader__.load({
|
|
|
618
618
|
// ------------------------------------------------------------------ css
|
|
619
619
|
const CSS =
|
|
620
620
|
'.dvo-btn{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
621
|
+
'.dvo-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}' +
|
|
622
|
+
'.dvo-btn[data-err="1"]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}' +
|
|
623
|
+
'.dvo-pill{display:flex;align-items:center;gap:10px;height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box;user-select:none;-webkit-user-select:none}' +
|
|
624
|
+
'.dvo-chev-icon{width:14px;height:14px;display:block;flex:none}' +
|
|
625
|
+
'.dvo-pbtn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);user-select:none;-webkit-user-select:none;touch-action:manipulation}' +
|
|
626
|
+
'.dvo-pbtn:hover{background:var(--dsw-alias-bg-layer-2)}' +
|
|
627
|
+
'.dvo-wave{flex:1;min-width:0;max-width:100%;height:40px;width:100%;color:var(--dsw-alias-label-primary)}' +
|
|
628
|
+
'.dvo-status{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
|
|
629
|
+
'.dvo-err{color:var(--dsw-alias-state-error-primary)}' +
|
|
630
|
+
'.dvo-count{font-variant-numeric:tabular-nums;font-size:13px;color:var(--dsw-alias-label-secondary)}' +
|
|
631
|
+
'.dvo-spin{animation:dvo-spin 1s linear infinite}' +
|
|
632
|
+
'@keyframes dvo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
|
|
633
|
+
'.dvo-btn-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}' +
|
|
634
|
+
'.dvo-audio-wrap{display:flex;align-items:center;gap:8px;padding:2px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:14px;border:1px solid var(--dsw-alias-border-l1)}' +
|
|
635
|
+
'.dvo-audio-play{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);border:0;cursor:pointer;padding:0}' +
|
|
636
|
+
'.dvo-audio-play:hover{opacity:0.9}' +
|
|
637
|
+
'.dvo-audio-time{font-size:12px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}' +
|
|
638
|
+
'.dvo-dash{display:flex;flex-direction:column;gap:8px;margin-top:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}' +
|
|
639
|
+
'.dvo-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:4px}' +
|
|
640
|
+
'.dvo-dash-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);border-radius:8px}' +
|
|
641
|
+
'.dvo-dash-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
642
|
+
'.dvo-dash-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
|
|
643
|
+
'.dvo-badge{display:inline-flex;align-items:center;font-size:11px;padding:1px 6px;border-radius:6px;font-weight:600}' +
|
|
644
|
+
'.dvo-badge-fast{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary)}' +
|
|
645
|
+
'.dvo-badge-norm{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-primary)}' +
|
|
646
|
+
'.dvo-badge-slow{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-border-l2)}' +
|
|
647
|
+
'.dvo-badge-err{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary)}' +
|
|
648
|
+
'.dvo-badge-idle{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}' +
|
|
649
|
+
'.dvo-mic-test{display:flex;align-items:center;gap:10px;margin-top:8px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}' +
|
|
650
|
+
'.dvo-meter-bar{flex:1;height:8px;background:var(--dsw-alias-bg-layer-3);border-radius:4px;overflow:hidden;position:relative}' +
|
|
651
|
+
'.dvo-meter-fill{height:100%;background:var(--dsw-alias-state-success-primary);border-radius:4px;transition:width 60ms ease-out}' +
|
|
652
|
+
'.dvo-meter-gate{position:absolute;top:0;bottom:0;width:2px;background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 4px var(--dsw-alias-state-warning-primary);z-index:2;pointer-events:none}' +
|
|
653
|
+
'.dvo-silence-ring{width:34px;height:34px;transform:rotate(-90deg);flex:none}' +
|
|
654
|
+
'.dvo-ring-bg{fill:none;stroke:var(--dsw-alias-border-l1);stroke-width:2.5}' +
|
|
655
|
+
'.dvo-ring-fg{fill:none;stroke:var(--dsw-alias-state-info-primary);stroke-width:2.5;stroke-linecap:round;transition:stroke-dashoffset 100ms linear}' +
|
|
656
|
+
'.dvo-ghost{font-style:italic;color:var(--dsw-alias-label-tertiary);margin-left:6px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}' +
|
|
657
|
+
'.dvo-installer-box{display:flex;flex-direction:column;gap:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:8px}' +
|
|
658
|
+
'.dvo-installer-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
659
|
+
'.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
|
|
660
|
+
'.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
|
|
661
|
+
'.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}' +
|
|
662
|
+
'.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
|
|
663
|
+
'.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}' +
|
|
664
|
+
'.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
|
|
665
|
+
'.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}' +
|
|
666
|
+
'.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}' +
|
|
667
|
+
'.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
|
|
668
|
+
'.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}' +
|
|
669
|
+
'.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
|
|
670
|
+
'.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}' +
|
|
671
|
+
'.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}' +
|
|
672
|
+
'.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
|
|
673
|
+
'.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}' +
|
|
674
|
+
'.dvo-btn-send:hover{opacity:0.9}' +
|
|
675
|
+
'.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}'
|
|
676
676
|
const cssId = 'dsh-voice/client.module.css'
|
|
677
677
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
|
|
678
678
|
const tag = document.createElement('style')
|
|
@@ -935,7 +935,7 @@ window.__ModuleLoader__.load({
|
|
|
935
935
|
try {
|
|
936
936
|
res = await fetch('/dsh-voice/transcribe', {
|
|
937
937
|
method: 'POST',
|
|
938
|
-
headers: { 'content-type': 'application/json' },
|
|
938
|
+
headers: { 'content-type': 'application/json', ...(typeof window !== 'undefined' && window.__dshVoiceToken ? { 'x-dsh-voice-token': window.__dshVoiceToken } : {}) },
|
|
939
939
|
body: JSON.stringify(payload),
|
|
940
940
|
signal: controller ? controller.signal : undefined,
|
|
941
941
|
})
|
|
@@ -1805,7 +1805,7 @@ voice._chainsPromise = null
|
|
|
1805
1805
|
try {
|
|
1806
1806
|
const pRes = await fetch('/dsh-voice/polish', {
|
|
1807
1807
|
method: 'POST',
|
|
1808
|
-
headers: { 'content-type': 'application/json' },
|
|
1808
|
+
headers: { 'content-type': 'application/json', ...(typeof window !== 'undefined' && window.__dshVoiceToken ? { 'x-dsh-voice-token': window.__dshVoiceToken } : {}) },
|
|
1809
1809
|
body: JSON.stringify({ text }),
|
|
1810
1810
|
})
|
|
1811
1811
|
const pData = await pRes.json()
|
|
@@ -4299,6 +4299,7 @@ function MacrosSection(props) {
|
|
|
4299
4299
|
setStatusLatency(Math.round(performance.now() - start))
|
|
4300
4300
|
if (data && data.ok) {
|
|
4301
4301
|
setStatusData(data)
|
|
4302
|
+
if (data.sessionToken && typeof window !== 'undefined') window.__dshVoiceToken = data.sessionToken
|
|
4302
4303
|
if (data.webgpuModel && voice.settings) {
|
|
4303
4304
|
voice.settings.webgpuModel = data.webgpuModel
|
|
4304
4305
|
}
|
package/lib/host-routes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// HTTP host routes registered on ctx.webServer.
|
|
2
2
|
import os from 'node:os'
|
|
3
|
-
import { readBody, isLoopback, isTrustedCaller } from './http-util.js'
|
|
3
|
+
import { readBody, isLoopback, isTrustedCaller, getVoiceSessionToken } from './http-util.js'
|
|
4
4
|
import { resolveSensevoiceProvider } from './sensevoice-installer.js'
|
|
5
5
|
import { applyVoiceMacros } from './macros.js'
|
|
6
6
|
import { createSyntheticWav } from './wav.js'
|
|
@@ -45,6 +45,7 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
45
45
|
const sensevoiceError = sensevoiceOk ? null : getSensevoiceError()
|
|
46
46
|
writeJson(res, 200, {
|
|
47
47
|
ok: true,
|
|
48
|
+
sessionToken: getVoiceSessionToken(),
|
|
48
49
|
whisperRunning: whisperOk,
|
|
49
50
|
whisperError: whisperOk ? null : whisperError,
|
|
50
51
|
sensevoiceRunning: sensevoiceOk,
|
|
@@ -154,7 +155,7 @@ export function registerHostRoutes(ctx, deps) {
|
|
|
154
155
|
path: '/dsh-voice/benchmark',
|
|
155
156
|
handler: async (req, res) => {
|
|
156
157
|
if (req.method !== 'POST') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } }); return }
|
|
157
|
-
if (!isTrustedCaller(req)) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
158
|
+
if (!isTrustedCaller(req, { loopbackOnly: true })) { writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden source origin' } }); return }
|
|
158
159
|
|
|
159
160
|
const cfg = live()
|
|
160
161
|
const effectiveProvider = resolveSensevoiceProvider(cfg)
|
package/lib/http-util.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Pure HTTP helpers shared by host routes. No cordis, no network.
|
|
2
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
2
3
|
|
|
3
4
|
export class BodyTooLargeError extends Error {
|
|
4
5
|
constructor(message = 'body too large') {
|
|
@@ -9,6 +10,29 @@ export class BodyTooLargeError extends Error {
|
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
let sessionSecret = ''
|
|
14
|
+
export function getVoiceSessionToken() {
|
|
15
|
+
if (!sessionSecret) {
|
|
16
|
+
sessionSecret = randomBytes(24).toString('hex')
|
|
17
|
+
}
|
|
18
|
+
return sessionSecret
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setVoiceSessionToken(t) {
|
|
22
|
+
sessionSecret = String(t || '')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function verifyVoiceSessionToken(token) {
|
|
26
|
+
if (!token || typeof token !== 'string') return false
|
|
27
|
+
const secret = getVoiceSessionToken()
|
|
28
|
+
if (token.length !== secret.length) return false
|
|
29
|
+
try {
|
|
30
|
+
return timingSafeEqual(Buffer.from(token), Buffer.from(secret))
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
export function writeJson(res, code, body) {
|
|
13
37
|
try {
|
|
14
38
|
const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }
|
|
@@ -77,12 +101,16 @@ export function isLoopback(addr) {
|
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
/**
|
|
80
|
-
* Verify that the HTTP request originates from a trusted caller
|
|
81
|
-
* Fail-closed security guard for state-changing or quota-consuming POST routes.
|
|
104
|
+
* Verify that the HTTP request originates from a trusted caller.
|
|
105
|
+
* Fail-closed security guard for state-changing or quota-consuming POST routes (#117, #173, #277).
|
|
106
|
+
* - Loopback callers (127.0.0.1, ::1) are trusted unless rejected by browser security headers.
|
|
107
|
+
* - Non-loopback LAN mutating/quota-consuming POST/PUT requests require valid session token (#277).
|
|
108
|
+
* - Routes marked loopbackOnly strictly reject non-loopback callers.
|
|
82
109
|
* @param {import('node:http').IncomingMessage} req
|
|
110
|
+
* @param {{ loopbackOnly?: boolean }} [options]
|
|
83
111
|
* @returns {boolean}
|
|
84
112
|
*/
|
|
85
|
-
export function isTrustedCaller(req) {
|
|
113
|
+
export function isTrustedCaller(req, options = {}) {
|
|
86
114
|
if (!req) return false
|
|
87
115
|
|
|
88
116
|
// 1. Browser security headers: immediately reject cross-site and same-site requests (#117, #173)
|
|
@@ -113,7 +141,6 @@ export function isTrustedCaller(req) {
|
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
// 4. For state-changing or data-carrying requests (POST/PUT/PATCH), require application/json or octet-stream
|
|
116
|
-
// Cross-origin simple form submissions cannot set application/json without a CORS preflight.
|
|
117
144
|
const method = String(req.method || 'GET').toUpperCase()
|
|
118
145
|
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
119
146
|
const contentType = String(req.headers?.['content-type'] || '').toLowerCase()
|
|
@@ -128,7 +155,20 @@ export function isTrustedCaller(req) {
|
|
|
128
155
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || ''
|
|
129
156
|
if (isLoopback(remote)) return true
|
|
130
157
|
|
|
131
|
-
//
|
|
158
|
+
// If route strictly requires loopback (e.g. host model installer or hardware benchmark)
|
|
159
|
+
if (options && options.loopbackOnly) return false
|
|
160
|
+
|
|
161
|
+
// 6. Non-loopback LAN requests: POST/PUT/PATCH quota-burning operations require session auth token (#277)
|
|
162
|
+
const isMutating = method === 'POST' || method === 'PUT' || method === 'PATCH'
|
|
163
|
+
if (isMutating) {
|
|
164
|
+
const incomingToken = String(req.headers?.['x-dsh-voice-token'] || req.headers?.['x-voice-token'] || '')
|
|
165
|
+
if (incomingToken && verifyVoiceSessionToken(incomingToken)) {
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 7. Non-state-changing GET requests from LAN: verify same-origin
|
|
132
172
|
if (secSite === 'same-origin') return true
|
|
133
173
|
if (origin) {
|
|
134
174
|
try {
|
|
@@ -146,4 +186,4 @@ export function isTrustedCaller(req) {
|
|
|
146
186
|
}
|
|
147
187
|
|
|
148
188
|
return false
|
|
149
|
-
}
|
|
189
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -114,7 +114,9 @@ export function apply(ctx, baseConfig) {
|
|
|
114
114
|
ctx.inject(['settings'], (sctx) => {
|
|
115
115
|
settingsService = sctx.settings
|
|
116
116
|
try {
|
|
117
|
-
|
|
117
|
+
if (typeof sctx.settings?.register === 'function') {
|
|
118
|
+
scope = sctx.settings.register(NS, Config, { base: baseConfig })
|
|
119
|
+
}
|
|
118
120
|
if (scope && typeof scope.get === 'function') {
|
|
119
121
|
getConfig = () => scope.get() ?? describeRow()?.config ?? readEntryConfig()
|
|
120
122
|
} else {
|
|
@@ -133,16 +135,10 @@ export function apply(ctx, baseConfig) {
|
|
|
133
135
|
triggerAutostart()
|
|
134
136
|
})
|
|
135
137
|
|
|
136
|
-
if (typeof ctx.on === 'function') {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ctx.effect(() => ctx.on('config', () => triggerAutostart()), 'dsh-voice: config autostart')
|
|
141
|
-
} else {
|
|
142
|
-
ctx.on('loader/volatile-update', () => triggerAutostart())
|
|
143
|
-
ctx.on('settings/document-updated', () => triggerAutostart())
|
|
144
|
-
ctx.on('config', () => triggerAutostart())
|
|
145
|
-
}
|
|
138
|
+
if (typeof ctx.effect === 'function' && typeof ctx.on === 'function') {
|
|
139
|
+
ctx.effect(() => ctx.on('loader/volatile-update', () => triggerAutostart()), 'dsh-voice: volatile autostart')
|
|
140
|
+
ctx.effect(() => ctx.on('settings/document-updated', () => triggerAutostart()), 'dsh-voice: settings autostart')
|
|
141
|
+
ctx.effect(() => ctx.on('config', () => triggerAutostart()), 'dsh-voice: config autostart')
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
triggerAutostart()
|
package/lib/local-daemon.js
CHANGED
|
@@ -35,8 +35,10 @@ export function createLocalDaemons({ live, ctx }) {
|
|
|
35
35
|
try { child.kill() } catch { /* ignore */ }
|
|
36
36
|
child = null
|
|
37
37
|
}
|
|
38
|
+
const rawLang = cfg?.dictation?.language
|
|
39
|
+
const safeLang = isAutoLang(rawLang) ? 'auto' : String(rawLang || '').replace(/[^a-zA-Z0-9_-]/g, '') || 'auto'
|
|
38
40
|
const command = `${JSON.stringify(cfg.whisperBin)} -m ${JSON.stringify(cfg.whisperModel)}`
|
|
39
|
-
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${
|
|
41
|
+
+ ` --host 127.0.0.1 --port 8001 -t 8 -p 1 -l ${JSON.stringify(safeLang)}`
|
|
40
42
|
const specOpts = { command, onExpiry: 'none', timeoutMs: 60000, stdoutMaxBytes: 4 * 1024 * 1024 }
|
|
41
43
|
const spec = typeof ctx.shell?.resolve === 'function' ? ctx.shell.resolve(specOpts) : specOpts
|
|
42
44
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// lib/sensevoice-installer.js — 1-Click installer for SenseVoice-Small & Whisper Large-v3-Turbo
|
|
2
2
|
// Downloads pre-quantized ONNX models and configures autostart on the host via sherpa-onnx.
|
|
3
3
|
|
|
4
|
-
import { createWriteStream, existsSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { createWriteStream, createReadStream, existsSync, readdirSync } from 'node:fs'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
6
7
|
import { homedir } from 'node:os'
|
|
7
8
|
import path from 'node:path'
|
|
@@ -20,6 +21,29 @@ export const WHISPER_TURBO_MODEL_URL =
|
|
|
20
21
|
export const WHISPER_TURBO_MODEL_MIRROR =
|
|
21
22
|
'https://huggingface.co/csukuangfj/sherpa-onnx-whisper-turbo/resolve/main/sherpa-onnx-whisper-turbo.tar.bz2'
|
|
22
23
|
|
|
24
|
+
export const MODEL_SHA256 = {
|
|
25
|
+
'sensevoice': 'f6b2a72ebcb1ac7a764d4cfccd886e6bcb2a95c4657c2199d0ba95ed4b9ea71a',
|
|
26
|
+
'whisper-turbo': 'b11acbbcd660b44a8e0df33724feb5aaa709cf65668f2823d59f656312544f22',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function verifyArchiveSha256(filePath, expectedSha256) {
|
|
30
|
+
if (!expectedSha256) return Promise.resolve(true)
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const hash = createHash('sha256')
|
|
33
|
+
const stream = createReadStream(filePath)
|
|
34
|
+
stream.on('data', (chunk) => hash.update(chunk))
|
|
35
|
+
stream.on('end', () => {
|
|
36
|
+
const digest = hash.digest('hex').toLowerCase()
|
|
37
|
+
if (digest === expectedSha256.toLowerCase()) {
|
|
38
|
+
resolve(true)
|
|
39
|
+
} else {
|
|
40
|
+
reject(new Error(`SHA-256 verification failed: expected ${expectedSha256}, got ${digest}`))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
stream.on('error', reject)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
let installerState = {
|
|
24
48
|
installing: false,
|
|
25
49
|
progress: 0,
|
|
@@ -176,7 +200,7 @@ export async function downloadArchive(url, destPath, onProgress) {
|
|
|
176
200
|
|
|
177
201
|
export function extractTarBz2(archivePath, targetDir) {
|
|
178
202
|
return new Promise((resolve, reject) => {
|
|
179
|
-
const child = spawn('tar', ['-xjf', archivePath, '-C', targetDir], { windowsHide: true })
|
|
203
|
+
const child = spawn('tar', ['--no-same-owner', '-xjf', archivePath, '-C', targetDir], { windowsHide: true })
|
|
180
204
|
let stderr = ''
|
|
181
205
|
child.stderr.on('data', (d) => { stderr += d })
|
|
182
206
|
child.on('close', (code) => {
|
|
@@ -233,6 +257,13 @@ export async function startSensevoiceInstall(ctx, cfg, updateConfigFn, startSens
|
|
|
233
257
|
|
|
234
258
|
if (!downloadOk) throw new Error(`Could not download ${targetModel} model from primary or mirror URL`)
|
|
235
259
|
|
|
260
|
+
installerState.step = 'verifying checksum'
|
|
261
|
+
installerState.progress = 92
|
|
262
|
+
const expectedHash = MODEL_SHA256[targetModel]
|
|
263
|
+
if (expectedHash) {
|
|
264
|
+
await verifyArchiveSha256(archivePath, expectedHash)
|
|
265
|
+
}
|
|
266
|
+
|
|
236
267
|
installerState.step = 'extracting'
|
|
237
268
|
installerState.progress = 95
|
|
238
269
|
await extractTarBz2(archivePath, modelDir)
|
|
@@ -281,7 +312,7 @@ export function registerSensevoiceInstaller(ctx, { liveConfig, isAlive, updateCo
|
|
|
281
312
|
kind: 'exact',
|
|
282
313
|
path: '/dsh-voice/sensevoice-installer',
|
|
283
314
|
handler: async (req, res) => {
|
|
284
|
-
if (!isTrustedCaller(req)) {
|
|
315
|
+
if (!isTrustedCaller(req, { loopbackOnly: true })) {
|
|
285
316
|
writeJson(res, 403, { ok: false, error: 'Forbidden: untrusted caller' })
|
|
286
317
|
return
|
|
287
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
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",
|