@goodandready/dsh-voice 0.9.29 → 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 +12 -3
- package/lib/local-daemon.js +2 -2
- 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
|
@@ -3087,11 +3087,13 @@ voice._chainsPromise = null
|
|
|
3087
3087
|
const [updaterStatus, setUpdaterStatus] = React.useState(null)
|
|
3088
3088
|
const [updating, setUpdating] = React.useState(false)
|
|
3089
3089
|
const [updaterMsg, setUpdaterMsg] = React.useState('')
|
|
3090
|
+
const [updaterMsgKind, setUpdaterMsgKind] = React.useState('')
|
|
3090
3091
|
const [updaterLoading, setUpdaterLoading] = React.useState(false)
|
|
3091
3092
|
|
|
3092
3093
|
const checkUpdater = React.useCallback(async () => {
|
|
3093
3094
|
setUpdaterLoading(true)
|
|
3094
3095
|
setUpdaterMsg('')
|
|
3096
|
+
setUpdaterMsgKind('')
|
|
3095
3097
|
try {
|
|
3096
3098
|
const res = await fetch('/api/dsh-voice/update', {
|
|
3097
3099
|
headers: { accept: 'application/json' },
|
|
@@ -3102,10 +3104,12 @@ voice._chainsPromise = null
|
|
|
3102
3104
|
setUpdaterStatus(data)
|
|
3103
3105
|
if (data && data.latestCheckFailed) {
|
|
3104
3106
|
setUpdaterMsg(t('updateCheckFailed'))
|
|
3107
|
+
setUpdaterMsgKind('err')
|
|
3105
3108
|
}
|
|
3106
3109
|
} catch (e) {
|
|
3107
3110
|
setUpdaterStatus(null)
|
|
3108
3111
|
setUpdaterMsg(t('updateCheckFailed') + ': ' + (e?.message || String(e)))
|
|
3112
|
+
setUpdaterMsgKind('err')
|
|
3109
3113
|
} finally {
|
|
3110
3114
|
setUpdaterLoading(false)
|
|
3111
3115
|
}
|
|
@@ -3115,6 +3119,7 @@ voice._chainsPromise = null
|
|
|
3115
3119
|
if (updating) return
|
|
3116
3120
|
setUpdating(true)
|
|
3117
3121
|
setUpdaterMsg('')
|
|
3122
|
+
setUpdaterMsgKind('')
|
|
3118
3123
|
try {
|
|
3119
3124
|
const res = await fetch('/api/dsh-voice/update', {
|
|
3120
3125
|
method: 'POST',
|
|
@@ -3140,15 +3145,17 @@ voice._chainsPromise = null
|
|
|
3140
3145
|
throw new Error(data.error || `HTTP ${res.status}`)
|
|
3141
3146
|
}
|
|
3142
3147
|
setUpdaterMsg(data.message || t('updateSuccess'))
|
|
3148
|
+
setUpdaterMsgKind('ok')
|
|
3143
3149
|
checkUpdater()
|
|
3144
3150
|
} catch (err) {
|
|
3145
3151
|
setUpdaterMsg(err && err.message ? err.message : String(err))
|
|
3152
|
+
setUpdaterMsgKind('err')
|
|
3146
3153
|
} finally {
|
|
3147
3154
|
setUpdating(false)
|
|
3148
3155
|
}
|
|
3149
3156
|
}
|
|
3150
3157
|
|
|
3151
|
-
return { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow }
|
|
3158
|
+
return { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow }
|
|
3152
3159
|
}
|
|
3153
3160
|
// Developer Lexicon & IT Jargon
|
|
3154
3161
|
function JargonEditor(props) {
|
|
@@ -3285,6 +3292,7 @@ voice._chainsPromise = null
|
|
|
3285
3292
|
const updaterLoading = props.updaterLoading
|
|
3286
3293
|
const updating = props.updating
|
|
3287
3294
|
const updaterMsg = props.updaterMsg
|
|
3295
|
+
const updaterMsgKind = props.updaterMsgKind
|
|
3288
3296
|
const onCheck = props.onCheck
|
|
3289
3297
|
const onUpdateNow = props.onUpdateNow
|
|
3290
3298
|
|
|
@@ -3328,7 +3336,7 @@ voice._chainsPromise = null
|
|
|
3328
3336
|
style: {
|
|
3329
3337
|
marginTop: '8px',
|
|
3330
3338
|
fontSize: '12px',
|
|
3331
|
-
color: updaterMsg.includes('fail') || updaterMsg.includes('失败')
|
|
3339
|
+
color: (updaterMsgKind === 'err' || (!updaterMsgKind && (updaterMsg.includes('fail') || updaterMsg.includes('失败'))))
|
|
3332
3340
|
? 'var(--dsw-alias-state-warning-primary)'
|
|
3333
3341
|
: 'var(--dsw-alias-state-success-primary)',
|
|
3334
3342
|
},
|
|
@@ -3531,7 +3539,7 @@ voice._chainsPromise = null
|
|
|
3531
3539
|
|
|
3532
3540
|
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3533
3541
|
|
|
3534
|
-
const { updaterStatus, updating, updaterMsg, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3542
|
+
const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3535
3543
|
|
|
3536
3544
|
const save = async () => {
|
|
3537
3545
|
setErr(''); setSaved(false)
|
|
@@ -3905,6 +3913,7 @@ voice._chainsPromise = null
|
|
|
3905
3913
|
updaterLoading: updaterLoading,
|
|
3906
3914
|
updating: updating,
|
|
3907
3915
|
updaterMsg: updaterMsg,
|
|
3916
|
+
updaterMsgKind: updaterMsgKind,
|
|
3908
3917
|
onCheck: checkUpdater,
|
|
3909
3918
|
onUpdateNow: onUpdateNow,
|
|
3910
3919
|
}),
|
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') {
|
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",
|