dsh-voice-mode 0.7.14 → 0.7.16
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.en.md +1 -2
- package/README.md +1 -2
- package/lib/client.js +5 -4
- package/lib/index.js +121 -34
- package/package.json +3 -4
- package/assets/demo.gif +0 -0
package/README.en.md
CHANGED
|
@@ -30,7 +30,6 @@ interrupts playback and the running turn. No API key.
|
|
|
30
30
|
- **Hardening**: session-existence check, loopback + Origin guards, per-endpoint rate limits, model SHA256 pinning, download-host allowlist.
|
|
31
31
|
|
|
32
32
|
> ℹ️ The `demo-voice-flow.gif` at the top is a **real recording of the current UI** (voice mode → live transcription → pause auto-send → sentence-by-sentence read-aloud with live captions), produced by driving the real pipeline via `screenshots/scripts/capture-demo.mjs`.
|
|
33
|
-
> ⚠️ `assets/demo.gif` and the older screenshots under the repo-root `assets/` still show the **upstream legacy single-button UI**; the current UI adds a mode-switch button next to the mic.
|
|
34
33
|
|
|
35
34
|
---
|
|
36
35
|
|
|
@@ -283,7 +282,7 @@ flowchart LR
|
|
|
283
282
|
| Hotword biasing | None | None (removed in v0.7.7; see version note) |
|
|
284
283
|
| Caption a11y | None | **4 font tiers + 3 width tiers + theme-following** |
|
|
285
284
|
| Wake word | None | **Lightweight streaming match + prefix filler whitelist** |
|
|
286
|
-
| dsh compatibility | — | **0.1.1-rc.2
|
|
285
|
+
| dsh compatibility | — | **full range since 0.1.1-rc.2 (incl. 0.1.7-rc.1)** |
|
|
287
286
|
|
|
288
287
|
---
|
|
289
288
|
|
package/README.md
CHANGED
|
@@ -35,7 +35,6 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
35
35
|
- **安全加固**:会话存在性校验 / 回环+Origin 校验 / 全端点限流 / **ASR+TTS 全模型 SHA256 固定** / 下载域名白名单 / 重定向守卫。
|
|
36
36
|
|
|
37
37
|
> ℹ️ 顶部 `demo-voice-flow.gif` 是**当前界面的真实录制**(语音模式 → 边说边出字 → 停顿自动发送 → 按句朗读 + 实时字幕),由 `screenshots/scripts/capture-demo.mjs` 驱动真实链路产出。
|
|
38
|
-
> ⚠️ 仓库内 `assets/demo.gif` 与根目录 `assets/` 下的旧截图仍为**上游旧版界面**(单按钮时期);当前界面在语音按钮旁多一颗「模式切换」按钮。
|
|
39
38
|
|
|
40
39
|
---
|
|
41
40
|
|
|
@@ -248,7 +247,7 @@ flowchart LR
|
|
|
248
247
|
| 热词偏置 | 无 | 无(已移除,详见 v0.7.7 文档说明) |
|
|
249
248
|
| 字幕 a11y | 无 | **4 档字号 + 3 档宽度 + 主题跟随** |
|
|
250
249
|
| 唤醒词 | 无 | **轻量流式匹配 + 前缀语气词白名单** |
|
|
251
|
-
| 兼容 dsh | — | **0.1.1-rc.2
|
|
250
|
+
| 兼容 dsh | — | **0.1.1-rc.2 起全版本(含 0.1.7-rc.1)** |
|
|
252
251
|
|
|
253
252
|
---
|
|
254
253
|
|
package/lib/client.js
CHANGED
|
@@ -2815,7 +2815,7 @@ var interruptFirstAt = 0;
|
|
|
2815
2815
|
var isSpeechFalseRun = 0;
|
|
2816
2816
|
var lastReenterAt = 0;
|
|
2817
2817
|
var INT_CONFIRM_FRAMES = { 0: 3, 1: 2, 2: 1 };
|
|
2818
|
-
var inject = ["slots", "sessions"
|
|
2818
|
+
var inject = ["slots", "sessions"];
|
|
2819
2819
|
var TELEMETRY_VIEW = [
|
|
2820
2820
|
{ stage: "utterance-end", key: "telUtteranceEnd" },
|
|
2821
2821
|
{ stage: "endpoint-fired", key: "telEndpoint" },
|
|
@@ -2825,7 +2825,7 @@ var TELEMETRY_VIEW = [
|
|
|
2825
2825
|
{ stage: "first-tts-chunk", key: "telFirstChunk" },
|
|
2826
2826
|
{ stage: "first-audio-played", key: "telFirstPlayed" }
|
|
2827
2827
|
];
|
|
2828
|
-
var BUILD_TAG = "
|
|
2828
|
+
var BUILD_TAG = "37c709b";
|
|
2829
2829
|
var TELEMETRY_FLAG = "dsh-voice-mode.telemetry";
|
|
2830
2830
|
var telemetryEnabled = typeof localStorage !== "undefined" && localStorage.getItem(TELEMETRY_FLAG) === "1";
|
|
2831
2831
|
console.log("[dsh-voice] build=" + BUILD_TAG);
|
|
@@ -2950,7 +2950,8 @@ function apply(ctx) {
|
|
|
2950
2950
|
VoiceOverlay
|
|
2951
2951
|
)
|
|
2952
2952
|
);
|
|
2953
|
-
|
|
2953
|
+
const settingsScope = ctx.get?.("settingsScope");
|
|
2954
|
+
if (settingsScope) {
|
|
2954
2955
|
ctx.slots.inject(
|
|
2955
2956
|
"settings.plugin.item",
|
|
2956
2957
|
() => ctx.slots.register(
|
|
@@ -2961,7 +2962,7 @@ function apply(ctx) {
|
|
|
2961
2962
|
order: 100,
|
|
2962
2963
|
label: t("stateVoiceMode")
|
|
2963
2964
|
},
|
|
2964
|
-
() => React.createElement(VoiceSettingsCard, { scope:
|
|
2965
|
+
() => React.createElement(VoiceSettingsCard, { scope: settingsScope.bind({ namespace: "voice-mode" }) })
|
|
2965
2966
|
)
|
|
2966
2967
|
);
|
|
2967
2968
|
}
|
package/lib/index.js
CHANGED
|
@@ -2010,8 +2010,81 @@ var Config = z.object({
|
|
|
2010
2010
|
rate: z.number().default(1.1),
|
|
2011
2011
|
interruptLevel: z.union([z.const(0), z.const(1), z.const(2)]).default(0),
|
|
2012
2012
|
silenceMs: z.number().default(1500),
|
|
2013
|
-
idleTimeoutMinutes: z.number().default(5)
|
|
2013
|
+
idleTimeoutMinutes: z.number().default(5),
|
|
2014
|
+
// 设置面板字段(与 VoiceSettingsValue 一一对应;0.1.7+ 由插件自身 Config 派生读取)。
|
|
2015
|
+
// 注意:刻意不标 .volatile() —— schemastery 3.18.4 的 volatile 会破坏 schema 函数
|
|
2016
|
+
// 调用形态(sv({}) → {field:{}}),而 0.1.5-rc.3 起的设置分层会调用插件 Config 做
|
|
2017
|
+
// merge,{} 透过 mergeLayers 污染并触发 ValidationError(2026-09-23 实测)。
|
|
2018
|
+
// 0.1.7 功能读取走 config 直接读,不依赖 volatile(仅官方 UI 自动投影受影响,
|
|
2019
|
+
// 本插件自带 settings-form 设置面板 + /voice-mode/config,不受影响)。
|
|
2020
|
+
autoSend: z.boolean().default(true),
|
|
2021
|
+
autoResume: z.boolean().default(false),
|
|
2022
|
+
mode: z.union([z.const("toggle"), z.const("hold")]).default("toggle"),
|
|
2023
|
+
bargeInMode: z.union([z.const("auto"), z.const("manual"), z.const("detect")]).default("detect"),
|
|
2024
|
+
echoGateDb: z.number().default(6),
|
|
2025
|
+
shortcut: z.string().default("Ctrl+Shift+V"),
|
|
2026
|
+
spokenFormat: z.boolean().default(true),
|
|
2027
|
+
senseVoice: z.boolean().default(true),
|
|
2028
|
+
wakeWord: z.string().default(""),
|
|
2029
|
+
toolBeep: z.boolean().default(false),
|
|
2030
|
+
senseITN: z.boolean().default(true),
|
|
2031
|
+
captionFontSize: z.union([z.const(0), z.const(1), z.const(2), z.const(3)]).default(0),
|
|
2032
|
+
captionMaxWidth: z.union([z.const(0), z.const(1), z.const(2)]).default(1),
|
|
2033
|
+
backchannelYield: z.boolean().default(true),
|
|
2034
|
+
yieldMs: z.number().default(1500)
|
|
2014
2035
|
});
|
|
2036
|
+
function voiceSettingsFromConfig(config) {
|
|
2037
|
+
const {
|
|
2038
|
+
ttsEngine,
|
|
2039
|
+
kokoroModel,
|
|
2040
|
+
voice,
|
|
2041
|
+
rate,
|
|
2042
|
+
interruptLevel,
|
|
2043
|
+
silenceMs,
|
|
2044
|
+
idleTimeoutMinutes,
|
|
2045
|
+
modelHost,
|
|
2046
|
+
autoSend,
|
|
2047
|
+
autoResume,
|
|
2048
|
+
mode,
|
|
2049
|
+
bargeInMode,
|
|
2050
|
+
echoGateDb,
|
|
2051
|
+
shortcut,
|
|
2052
|
+
spokenFormat,
|
|
2053
|
+
senseVoice,
|
|
2054
|
+
wakeWord,
|
|
2055
|
+
toolBeep,
|
|
2056
|
+
senseITN,
|
|
2057
|
+
captionFontSize,
|
|
2058
|
+
captionMaxWidth,
|
|
2059
|
+
backchannelYield,
|
|
2060
|
+
yieldMs
|
|
2061
|
+
} = config;
|
|
2062
|
+
return {
|
|
2063
|
+
ttsEngine,
|
|
2064
|
+
kokoroModel,
|
|
2065
|
+
voice,
|
|
2066
|
+
rate,
|
|
2067
|
+
interruptLevel,
|
|
2068
|
+
silenceMs,
|
|
2069
|
+
idleTimeoutMinutes,
|
|
2070
|
+
modelHost,
|
|
2071
|
+
autoSend,
|
|
2072
|
+
autoResume,
|
|
2073
|
+
mode,
|
|
2074
|
+
bargeInMode,
|
|
2075
|
+
echoGateDb,
|
|
2076
|
+
shortcut,
|
|
2077
|
+
spokenFormat,
|
|
2078
|
+
senseVoice,
|
|
2079
|
+
wakeWord,
|
|
2080
|
+
toolBeep,
|
|
2081
|
+
senseITN,
|
|
2082
|
+
captionFontSize,
|
|
2083
|
+
captionMaxWidth,
|
|
2084
|
+
backchannelYield,
|
|
2085
|
+
yieldMs
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2015
2088
|
function apply(ctx, config) {
|
|
2016
2089
|
let activeVoiceSession = null;
|
|
2017
2090
|
let activeTabId = null;
|
|
@@ -2056,22 +2129,32 @@ function apply(ctx, config) {
|
|
|
2056
2129
|
}
|
|
2057
2130
|
}
|
|
2058
2131
|
};
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2132
|
+
const legacySettings = ctx.settings;
|
|
2133
|
+
const useLegacySettings = typeof legacySettings?.register === "function";
|
|
2134
|
+
let settingsScopeRef = null;
|
|
2135
|
+
let vset;
|
|
2136
|
+
if (useLegacySettings && legacySettings && typeof legacySettings.register === "function") {
|
|
2137
|
+
const settingsScope = legacySettings.register.call(
|
|
2138
|
+
legacySettings,
|
|
2139
|
+
NS_VOICE_MODE,
|
|
2140
|
+
createVoiceSettingsSchema(),
|
|
2141
|
+
{
|
|
2142
|
+
base: {
|
|
2143
|
+
ttsEngine: config.ttsEngine,
|
|
2144
|
+
voice: config.voice,
|
|
2145
|
+
rate: config.rate,
|
|
2146
|
+
interruptLevel: config.interruptLevel,
|
|
2147
|
+
silenceMs: config.silenceMs,
|
|
2148
|
+
idleTimeoutMinutes: config.idleTimeoutMinutes,
|
|
2149
|
+
modelHost: config.modelHost
|
|
2150
|
+
}
|
|
2071
2151
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2152
|
+
);
|
|
2153
|
+
vset = settingsScope.get();
|
|
2154
|
+
settingsScopeRef = settingsScope;
|
|
2155
|
+
} else {
|
|
2156
|
+
vset = voiceSettingsFromConfig(config);
|
|
2157
|
+
}
|
|
2075
2158
|
const asr = createAsrRuntime({
|
|
2076
2159
|
cacheDir: config.cacheDir,
|
|
2077
2160
|
modelHost: () => vset.modelHost,
|
|
@@ -2118,23 +2201,26 @@ function apply(ctx, config) {
|
|
|
2118
2201
|
const unsubscribe = queue.subscribe((frame) => broadcast("audio", frame));
|
|
2119
2202
|
ctx.effect(() => unsubscribe);
|
|
2120
2203
|
ctx.effect(() => () => void queue.close());
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2204
|
+
if (settingsScopeRef) {
|
|
2205
|
+
const scopeRef = settingsScopeRef;
|
|
2206
|
+
ctx.effect(
|
|
2207
|
+
() => scopeRef.watch((next) => {
|
|
2208
|
+
const prev = vset;
|
|
2209
|
+
vset = next;
|
|
2210
|
+
if (next.ttsEngine !== engineKind) {
|
|
2211
|
+
engineKind = next.ttsEngine;
|
|
2212
|
+
queue.setEngine(makeEngine(engineKind));
|
|
2213
|
+
} else if (engineKind === "kokoro" && next.kokoroModel !== activeKokoroModel) {
|
|
2214
|
+
activeKokoroModel = next.kokoroModel;
|
|
2215
|
+
queue.setEngine(makeEngine("kokoro"));
|
|
2216
|
+
}
|
|
2217
|
+
queue.updateVoice(next.voice, next.rate);
|
|
2218
|
+
if (next.senseITN !== prev.senseITN || next.senseVoice !== prev.senseVoice) {
|
|
2219
|
+
asr.markStale();
|
|
2220
|
+
}
|
|
2221
|
+
})
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2138
2224
|
const currentVoice = () => vset.voice;
|
|
2139
2225
|
const currentRate = () => vset.rate;
|
|
2140
2226
|
const currentInterrupt = () => vset.interruptLevel;
|
|
@@ -2571,7 +2657,8 @@ function apply(ctx, config) {
|
|
|
2571
2657
|
res.end(JSON.stringify({ error: "mode must be toggle or hold" }));
|
|
2572
2658
|
return;
|
|
2573
2659
|
}
|
|
2574
|
-
|
|
2660
|
+
const persistMode = settingsScopeRef ? settingsScopeRef.update({ mode }) : ctx.settings.mutate ? ctx.settings.mutate(NS_VOICE_MODE, [{ op: "set", path: ["mode"], value: mode }]) : Promise.reject(new Error("mode persistence unsupported on this host"));
|
|
2661
|
+
void persistMode.then(() => {
|
|
2575
2662
|
res.statusCode = 200;
|
|
2576
2663
|
res.setHeader("content-type", "application/json");
|
|
2577
2664
|
res.end(JSON.stringify({ ok: true, mode }));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-voice-mode",
|
|
3
|
-
"description": "Full-duplex voice plugin for DeepSeek Harness: local zipformer2 streaming ASR (no API key) → editable draft; Edge TTS or local VITS / Kokoro read-aloud with live captions; true barge-in; hardened HTTP surface + model SHA256 pinning; compatible with all dsh releases since 0.1.1-rc.2 (verified 0.1.1-rc.2 → 0.
|
|
4
|
-
"version": "0.7.
|
|
3
|
+
"description": "Full-duplex voice plugin for DeepSeek Harness: local zipformer2 streaming ASR (no API key) → editable draft; Edge TTS or local VITS / Kokoro read-aloud with live captions; true barge-in; hardened HTTP surface + model SHA256 pinning; compatible with all dsh releases since 0.1.1-rc.2 (verified 0.1.1-rc.2 → 0.2.0-rc.2). · DSH 语音双工插件:流式识别入草稿、按句朗读+实时字幕、开口即打断;免 API Key、安全加固、全版本兼容(0.1.1-rc.2 起,已验证至 0.2.0-rc.2 端到端)。",
|
|
4
|
+
"version": "0.7.16",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"lib/sense-worker.mjs",
|
|
30
30
|
"cordis.patch.yml",
|
|
31
31
|
"assets/architecture.svg",
|
|
32
|
-
"assets/demo.gif",
|
|
33
32
|
"scripts/prefetch.mjs",
|
|
34
33
|
"scripts/list-voices.mjs",
|
|
35
34
|
"scripts/bench-asr.mjs",
|
|
@@ -79,7 +78,7 @@
|
|
|
79
78
|
}
|
|
80
79
|
},
|
|
81
80
|
"dependencies": {
|
|
82
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
81
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
83
82
|
"msedge-tts": "^2.0.7",
|
|
84
83
|
"sherpa-onnx": "^1.13.6",
|
|
85
84
|
"sherpa-onnx-node": "1.13.6"
|
package/assets/demo.gif
DELETED
|
Binary file
|