@goodandready/dsh-voice 0.9.28 → 0.9.30
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 +3 -0
- package/README.ru.md +3 -0
- package/README.zh.md +3 -0
- package/lib/client.js +68 -20
- package/lib/local-daemon.js +2 -2
- package/lib/sensevoice-installer.js +10 -9
- package/lib/updater.js +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Requirements
|
|
39
|
+
### Browser Microphone & Secure Context\nModern browsers require a **Secure Context** (`https://` or `http://localhost` / `http://127.0.0.1`) for microphone capture (`navigator.mediaDevices.getUserMedia`). When accessing DSH over a local network (e.g., `http://192.168.1.x:3080` via `dsh-lanmode`), browsers will block microphone access by default. Serve DSH behind an HTTPS reverse proxy or configure browser security flags (`chrome://flags/#unsafely-treat-insecure-origin-as-secure`) for LAN IP access.\n
|
|
40
|
+
|
|
38
41
|
## ⚡ Overview
|
|
39
42
|
|
|
40
43
|
**`dsh-voice`** brings voice superpowers to the **DeepSeek Harness** Web UI. Whether you need hands-free real-time streaming dictation segmented on natural breath pauses or crisp voice notes with keyboard/mouse Push-to-Talk gestures, `dsh-voice` ensures your audio is never lost thanks to **automatic multi-provider fallback chains**.
|
package/README.ru.md
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Требования
|
|
39
|
+
### Доступ к микрофону и Secure Context\nСовременные веб-браузеры требуют безопасный контекст (**Secure Context**: `https://` или `http://localhost` / `http://127.0.0.1`) для захвата аудио с микрофона (`navigator.mediaDevices.getUserMedia`). При открытии DSH в локальной сети по обычному HTTP (например, `http://192.168.1.x:3080` через `dsh-lanmode`) браузер блокирует микрофон. Для работы в LAN настройте HTTPS (через reverse proxy, например caddy/nginx) либо укажите адрес в настройках браузера (`chrome://flags/#unsafely-treat-insecure-origin-as-secure`).\n
|
|
40
|
+
|
|
38
41
|
## ⚡ Обзор
|
|
39
42
|
|
|
40
43
|
**`dsh-voice`** добавляет полноценные голосовые возможности в веб-интерфейс **DeepSeek Harness**. Будь то непрерывная диктовка с нарезкой фраз по естественным паузам или голосовые сообщения с удобными жестами Push-to-Talk (мышь и клавиатура) — `dsh-voice` гарантирует сохранность каждой записи благодаря **автоматическим цепочкам отказоустойчивости**.
|
package/README.zh.md
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## 运行要求
|
|
39
|
+
### 浏览器麦克风与安全上下文 (Secure Context)\n现代浏览器要求在**安全上下文**(`https://` 或 `http://localhost` / `http://127.0.0.1`)下才允许调用麦克风接口(`navigator.mediaDevices.getUserMedia`)。当通过局域网以普通 HTTP 访问 DSH(如使用 `dsh-lanmode` 访问 `http://192.168.1.x:3080`)时,浏览器默认会阻止录音。在局域网环境下,请配置 HTTPS 反向代理,或在浏览器中启用标志(`chrome://flags/#unsafely-treat-insecure-origin-as-secure`)。\n
|
|
40
|
+
|
|
38
41
|
## ⚡ 插件概览
|
|
39
42
|
|
|
40
43
|
**`dsh-voice`** 为 **DeepSeek Harness** Web 界面带来极速语音交互体验。无论是按自然停顿切分的流式听写,还是带撤回保护的语音消息以及键盘/鼠标 Push-to-Talk 对讲,`dsh-voice` 凭借**多服务商自动故障转移备用链**确保您的录音万无一失。
|
package/lib/client.js
CHANGED
|
@@ -1080,39 +1080,73 @@ window.__ModuleLoader__.load({
|
|
|
1080
1080
|
// Fetch the mode chain from the host once. Only needed to decide whether
|
|
1081
1081
|
|
|
1082
1082
|
voice._core = { tidyPhrase, applyVoiceCommands, VOICE_COMMANDS, undoLastInsert, insertHistory, insertDraftText, appendDraft, resolveBrowserRecognitionLang }
|
|
1083
|
-
// lib/client-src/35-webgpu.js — Client-side WebGPU Whisper offline recognition
|
|
1084
|
-
// Detects WebGPU availability in browser and provides offline transcription with graceful fallback.
|
|
1083
|
+
// lib/client-src/35-webgpu.js — Client-side WebGPU/WASM Whisper offline recognition
|
|
1084
|
+
// Detects WebGPU and WebAssembly availability in browser and provides offline transcription with graceful WASM fallback.
|
|
1085
1085
|
|
|
1086
1086
|
function isWebGpuSupported() {
|
|
1087
1087
|
if (typeof navigator === 'undefined' || !navigator.gpu) return false
|
|
1088
1088
|
return true
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
+
function isWasmSupported() {
|
|
1092
|
+
return typeof WebAssembly !== 'undefined'
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function isOfflineSupported() {
|
|
1096
|
+
return isWebGpuSupported() || isWasmSupported()
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1091
1099
|
const _webGpuPipelines = new Map()
|
|
1092
1100
|
|
|
1093
|
-
function loadWebGpuWhisper(modelName) {
|
|
1101
|
+
function loadWebGpuWhisper(modelName, options) {
|
|
1094
1102
|
const targetModel = modelName || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'
|
|
1095
|
-
|
|
1096
|
-
|
|
1103
|
+
const forceDevice = options?.device
|
|
1104
|
+
const key = `${targetModel}:${forceDevice || 'auto'}`
|
|
1105
|
+
if (_webGpuPipelines.has(key)) {
|
|
1106
|
+
return _webGpuPipelines.get(key)
|
|
1097
1107
|
}
|
|
1098
1108
|
const promise = (async () => {
|
|
1099
|
-
if (!
|
|
1100
|
-
throw new Error('WebGPU is
|
|
1109
|
+
if (!isOfflineSupported()) {
|
|
1110
|
+
throw new Error('Neither WebGPU nor WebAssembly is supported in this browser environment')
|
|
1101
1111
|
}
|
|
1102
1112
|
const mod = typeof globalThis !== 'undefined' && globalThis.__mockTransformers
|
|
1103
1113
|
? globalThis.__mockTransformers
|
|
1104
1114
|
: (await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.3'))
|
|
1105
1115
|
const pipeline = mod.pipeline || (mod.default && mod.default.pipeline)
|
|
1106
1116
|
if (!pipeline) throw new Error('Transformers pipeline not found in loaded bundle')
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1117
|
+
|
|
1118
|
+
// Prefer WebGPU if available and not forced to wasm; fall back to WASM on error or when WebGPU is absent
|
|
1119
|
+
const canGpu = forceDevice === 'webgpu' || (!forceDevice && isWebGpuSupported())
|
|
1120
|
+
if (canGpu) {
|
|
1121
|
+
try {
|
|
1122
|
+
return await pipeline('automatic-speech-recognition', targetModel, {
|
|
1123
|
+
device: 'webgpu',
|
|
1124
|
+
dtype: 'fp32',
|
|
1125
|
+
})
|
|
1126
|
+
} catch (gpuErr) {
|
|
1127
|
+
if (isWasmSupported()) {
|
|
1128
|
+
return await pipeline('automatic-speech-recognition', targetModel, {
|
|
1129
|
+
device: 'wasm',
|
|
1130
|
+
dtype: 'q8',
|
|
1131
|
+
})
|
|
1132
|
+
}
|
|
1133
|
+
throw gpuErr
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (isWasmSupported()) {
|
|
1138
|
+
return await pipeline('automatic-speech-recognition', targetModel, {
|
|
1139
|
+
device: 'wasm',
|
|
1140
|
+
dtype: 'q8',
|
|
1141
|
+
})
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
throw new Error('Offline speech recognition failed: no WebGPU or WebAssembly available')
|
|
1111
1145
|
})().catch((err) => {
|
|
1112
|
-
_webGpuPipelines.delete(
|
|
1146
|
+
_webGpuPipelines.delete(key)
|
|
1113
1147
|
throw err
|
|
1114
1148
|
})
|
|
1115
|
-
_webGpuPipelines.set(
|
|
1149
|
+
_webGpuPipelines.set(key, promise)
|
|
1116
1150
|
return promise
|
|
1117
1151
|
}
|
|
1118
1152
|
|
|
@@ -1131,10 +1165,10 @@ async function audioBlobToFloat32Array(blob) {
|
|
|
1131
1165
|
}
|
|
1132
1166
|
|
|
1133
1167
|
async function transcribeWithWebGpu(blob, mime, options) {
|
|
1134
|
-
if (!
|
|
1135
|
-
throw new Error('WebGPU unavailable in browser')
|
|
1168
|
+
if (!isOfflineSupported()) {
|
|
1169
|
+
throw new Error('Offline recognition (WebGPU/WASM) unavailable in browser')
|
|
1136
1170
|
}
|
|
1137
|
-
const transcriber = await loadWebGpuWhisper(options && options.model)
|
|
1171
|
+
const transcriber = await loadWebGpuWhisper(options && options.model, options)
|
|
1138
1172
|
const audioData = await audioBlobToFloat32Array(blob)
|
|
1139
1173
|
const generateArgs = {}
|
|
1140
1174
|
if (options && options.language && options.language !== 'auto') {
|
|
@@ -1146,7 +1180,9 @@ async function transcribeWithWebGpu(blob, mime, options) {
|
|
|
1146
1180
|
}
|
|
1147
1181
|
|
|
1148
1182
|
voice.webGpu = {
|
|
1149
|
-
isSupported:
|
|
1183
|
+
isSupported: isOfflineSupported,
|
|
1184
|
+
isWebGpuSupported,
|
|
1185
|
+
isWasmSupported,
|
|
1150
1186
|
transcribe: transcribeWithWebGpu,
|
|
1151
1187
|
load: loadWebGpuWhisper,
|
|
1152
1188
|
clearCache: () => { _webGpuPipelines.clear() },
|
|
@@ -3051,11 +3087,13 @@ voice._chainsPromise = null
|
|
|
3051
3087
|
const [updaterStatus, setUpdaterStatus] = React.useState(null)
|
|
3052
3088
|
const [updating, setUpdating] = React.useState(false)
|
|
3053
3089
|
const [updaterMsg, setUpdaterMsg] = React.useState('')
|
|
3090
|
+
const [updaterMsgKind, setUpdaterMsgKind] = React.useState('')
|
|
3054
3091
|
const [updaterLoading, setUpdaterLoading] = React.useState(false)
|
|
3055
3092
|
|
|
3056
3093
|
const checkUpdater = React.useCallback(async () => {
|
|
3057
3094
|
setUpdaterLoading(true)
|
|
3058
3095
|
setUpdaterMsg('')
|
|
3096
|
+
setUpdaterMsgKind('')
|
|
3059
3097
|
try {
|
|
3060
3098
|
const res = await fetch('/api/dsh-voice/update', {
|
|
3061
3099
|
headers: { accept: 'application/json' },
|
|
@@ -3066,10 +3104,12 @@ voice._chainsPromise = null
|
|
|
3066
3104
|
setUpdaterStatus(data)
|
|
3067
3105
|
if (data && data.latestCheckFailed) {
|
|
3068
3106
|
setUpdaterMsg(t('updateCheckFailed'))
|
|
3107
|
+
setUpdaterMsgKind('err')
|
|
3069
3108
|
}
|
|
3070
3109
|
} catch (e) {
|
|
3071
3110
|
setUpdaterStatus(null)
|
|
3072
3111
|
setUpdaterMsg(t('updateCheckFailed') + ': ' + (e?.message || String(e)))
|
|
3112
|
+
setUpdaterMsgKind('err')
|
|
3073
3113
|
} finally {
|
|
3074
3114
|
setUpdaterLoading(false)
|
|
3075
3115
|
}
|
|
@@ -3079,6 +3119,7 @@ voice._chainsPromise = null
|
|
|
3079
3119
|
if (updating) return
|
|
3080
3120
|
setUpdating(true)
|
|
3081
3121
|
setUpdaterMsg('')
|
|
3122
|
+
setUpdaterMsgKind('')
|
|
3082
3123
|
try {
|
|
3083
3124
|
const res = await fetch('/api/dsh-voice/update', {
|
|
3084
3125
|
method: 'POST',
|
|
@@ -3104,15 +3145,17 @@ voice._chainsPromise = null
|
|
|
3104
3145
|
throw new Error(data.error || `HTTP ${res.status}`)
|
|
3105
3146
|
}
|
|
3106
3147
|
setUpdaterMsg(data.message || t('updateSuccess'))
|
|
3148
|
+
setUpdaterMsgKind('ok')
|
|
3107
3149
|
checkUpdater()
|
|
3108
3150
|
} catch (err) {
|
|
3109
3151
|
setUpdaterMsg(err && err.message ? err.message : String(err))
|
|
3152
|
+
setUpdaterMsgKind('err')
|
|
3110
3153
|
} finally {
|
|
3111
3154
|
setUpdating(false)
|
|
3112
3155
|
}
|
|
3113
3156
|
}
|
|
3114
3157
|
|
|
3115
|
-
return { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow }
|
|
3158
|
+
return { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow }
|
|
3116
3159
|
}
|
|
3117
3160
|
// Developer Lexicon & IT Jargon
|
|
3118
3161
|
function JargonEditor(props) {
|
|
@@ -3249,6 +3292,7 @@ voice._chainsPromise = null
|
|
|
3249
3292
|
const updaterLoading = props.updaterLoading
|
|
3250
3293
|
const updating = props.updating
|
|
3251
3294
|
const updaterMsg = props.updaterMsg
|
|
3295
|
+
const updaterMsgKind = props.updaterMsgKind
|
|
3252
3296
|
const onCheck = props.onCheck
|
|
3253
3297
|
const onUpdateNow = props.onUpdateNow
|
|
3254
3298
|
|
|
@@ -3292,7 +3336,7 @@ voice._chainsPromise = null
|
|
|
3292
3336
|
style: {
|
|
3293
3337
|
marginTop: '8px',
|
|
3294
3338
|
fontSize: '12px',
|
|
3295
|
-
color: updaterMsg.includes('fail') || updaterMsg.includes('失败')
|
|
3339
|
+
color: (updaterMsgKind === 'err' || (!updaterMsgKind && (updaterMsg.includes('fail') || updaterMsg.includes('失败'))))
|
|
3296
3340
|
? 'var(--dsw-alias-state-warning-primary)'
|
|
3297
3341
|
: 'var(--dsw-alias-state-success-primary)',
|
|
3298
3342
|
},
|
|
@@ -3495,7 +3539,7 @@ voice._chainsPromise = null
|
|
|
3495
3539
|
|
|
3496
3540
|
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3497
3541
|
|
|
3498
|
-
const { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3542
|
+
const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3499
3543
|
|
|
3500
3544
|
const save = async () => {
|
|
3501
3545
|
setErr(''); setSaved(false)
|
|
@@ -3869,6 +3913,7 @@ voice._chainsPromise = null
|
|
|
3869
3913
|
updaterLoading: updaterLoading,
|
|
3870
3914
|
updating: updating,
|
|
3871
3915
|
updaterMsg: updaterMsg,
|
|
3916
|
+
updaterMsgKind: updaterMsgKind,
|
|
3872
3917
|
onCheck: checkUpdater,
|
|
3873
3918
|
onUpdateNow: onUpdateNow,
|
|
3874
3919
|
}),
|
|
@@ -3997,6 +4042,9 @@ voice._chainsPromise = null
|
|
|
3997
4042
|
stopCurrent,
|
|
3998
4043
|
submitPending,
|
|
3999
4044
|
loadWebGpuWhisper,
|
|
4045
|
+
isWebGpuSupported,
|
|
4046
|
+
isWasmSupported,
|
|
4047
|
+
isOfflineSupported,
|
|
4000
4048
|
waitStop,
|
|
4001
4049
|
currentLevel,
|
|
4002
4050
|
accentColor,
|
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') {
|
|
@@ -249,8 +249,13 @@ export function resolveSensevoiceProvider(cfg) {
|
|
|
249
249
|
return 'cpu'
|
|
250
250
|
}
|
|
251
251
|
if (ext === '.rknn') {
|
|
252
|
-
|
|
252
|
+
if (modelPath && existsSync(modelPath)) {
|
|
253
|
+
return 'rknn'
|
|
254
|
+
}
|
|
255
|
+
// Guard: missing .rknn model file cannot be claimed as running accelerator (#168)
|
|
256
|
+
return 'cpu'
|
|
253
257
|
}
|
|
258
|
+
return 'cpu'
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
return normProv || 'cpu'
|
|
@@ -263,10 +268,9 @@ export function buildSensevoiceArgs(cfg, port) {
|
|
|
263
268
|
? cfg.sensevoiceTokens
|
|
264
269
|
: findTokensFile(modelDir)
|
|
265
270
|
const tokensArg = tokensPath ? ` --tokens=${JSON.stringify(tokensPath)}` : ''
|
|
266
|
-
const
|
|
267
|
-
let normProv = rawProv === 'rknpu' ? 'rknn' : rawProv
|
|
271
|
+
const effectiveProv = resolveSensevoiceProvider(cfg)
|
|
268
272
|
|
|
269
|
-
if (
|
|
273
|
+
if (effectiveProv === 'rknn') {
|
|
270
274
|
const ext = path.extname(modelPath).toLowerCase()
|
|
271
275
|
if (ext === '.onnx') {
|
|
272
276
|
const rknnCandidate = path.join(modelDir, path.basename(modelPath, ext) + '.rknn')
|
|
@@ -275,15 +279,12 @@ export function buildSensevoiceArgs(cfg, port) {
|
|
|
275
279
|
modelPath = rknnCandidate
|
|
276
280
|
} else if (existsSync(defaultRknn)) {
|
|
277
281
|
modelPath = defaultRknn
|
|
278
|
-
} else {
|
|
279
|
-
// Guard: ordinary ONNX model cannot be loaded by sherpa-onnx RKNN runtime (#221)
|
|
280
|
-
normProv = 'cpu'
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
const provider = (
|
|
286
|
-
? ` --provider=${
|
|
286
|
+
const provider = (effectiveProv && effectiveProv !== 'cpu')
|
|
287
|
+
? ` --provider=${effectiveProv}`
|
|
287
288
|
: ''
|
|
288
289
|
const threads = Number(cfg?.sensevoiceThreads) || 4
|
|
289
290
|
return ` --sense-voice-model=${JSON.stringify(modelPath)}`
|
package/lib/updater.js
CHANGED
|
@@ -18,6 +18,17 @@ function header(request, name) {
|
|
|
18
18
|
return Array.isArray(value) ? value[0] : value;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function isPrivateLan(value) {
|
|
22
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '');
|
|
23
|
+
if (!address) return false;
|
|
24
|
+
const ipv4 = address.startsWith('::ffff:') ? address.slice(7) : address;
|
|
25
|
+
return (
|
|
26
|
+
ipv4.startsWith('192.168.') ||
|
|
27
|
+
ipv4.startsWith('10.') ||
|
|
28
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ipv4)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
function isLoopback(value) {
|
|
22
33
|
const address = value?.toLowerCase().replace(/^\[|\]$/g, '');
|
|
23
34
|
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
@@ -27,7 +38,8 @@ function isLoopback(value) {
|
|
|
27
38
|
|
|
28
39
|
export function isTrustedUpdateRequest(request) {
|
|
29
40
|
if (header(request, UPDATE_HEADER) !== '1') return false;
|
|
30
|
-
|
|
41
|
+
const remote = request.socket?.remoteAddress;
|
|
42
|
+
if (!isLoopback(remote) && !isPrivateLan(remote)) return false;
|
|
31
43
|
const site = header(request, 'sec-fetch-site');
|
|
32
44
|
if (site !== undefined && site !== 'same-origin') return false;
|
|
33
45
|
const origin = header(request, 'origin');
|
|
@@ -35,8 +47,11 @@ export function isTrustedUpdateRequest(request) {
|
|
|
35
47
|
if (origin === undefined || host === undefined) return false;
|
|
36
48
|
try {
|
|
37
49
|
const url = new URL(origin);
|
|
38
|
-
return (
|
|
39
|
-
|
|
50
|
+
return (
|
|
51
|
+
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
52
|
+
(isLoopback(url.hostname) || isPrivateLan(url.hostname)) &&
|
|
53
|
+
url.host === host
|
|
54
|
+
);
|
|
40
55
|
} catch {
|
|
41
56
|
return false;
|
|
42
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
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",
|