dsh-voice-mode 0.7.13 → 0.7.15

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 CHANGED
@@ -16,7 +16,7 @@ interrupts playback and the running turn. No API key.
16
16
 
17
17
  ![Real recording: streaming transcription → auto-send → sentence-by-sentence read-aloud with live captions](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/demo-voice-flow.gif)
18
18
 
19
- > **Version note (as of v0.7.12, 2026-09-19)**: v0.7.12 / v0.7.11 are **asset, documentation and search-metadata updates** (zero runtime change; v0.7.11 includes the five-version compatibility run). The following are the **runtime** changes since v0.7.10: **Silent-audio causes in the output pipeline fixed** — ① a sentence whose cloud-TTS synthesis fails after 3 retries is no longer dropped silently (the status bar now says one sentence failed and was skipped; previously this showed up as "the reply is occasionally not read" with no way to tell why); ② audio playback no longer goes silent after the browser suspends the AudioContext (background tab / long idle) — the context is resumed on every enqueue and on any click/keypress; ③ dropped incomplete sentences (SSE frame loss) now leave a diagnostic trace. Otherwise as v0.7.9: **Wake-word pipeline overhaul** (Issue #10 + real-machine retest) — a harness driving the real engine pinpointed and fixed five flow defects: ① TTS echo polluting the standby segment while the agent reads (could not wake); ② only above-threshold frames uploaded, so trailing characters never flushed (wake word truncated); ③ discarding the whole segment on a wake hit (saying "wake word + command" in one breath sent only the tail); ④ the command hanging when the hit arrived after you stopped speaking; ⑤ pausing after the wake word closed the command window (command lost). **The wake word may now be said together with your command — it is stripped and never sent**, standby live-shows what it heard, and "wake word … pause … command" works. Otherwise as v0.7.7: Edge cloud TTS by default, local TTS (VITS / Kokoro) optional; silence split defaults to 1500 ms.
19
+ > **Runtime changes (most recent batch: v0.7.10, 2026-09-18)**: **Silent-audio causes in the output pipeline fixed** — ① a sentence whose cloud-TTS synthesis fails after 3 retries is no longer dropped silently (the status bar now says one sentence failed and was skipped; previously this showed up as "the reply is occasionally not read" with no way to tell why); ② audio playback no longer goes silent after the browser suspends the AudioContext (background tab / long idle) — the context is resumed on every enqueue and on any click/keypress; ③ dropped incomplete sentences (SSE frame loss) now leave a diagnostic trace. Otherwise as v0.7.9: **Wake-word pipeline overhaul** (Issue #10 + real-machine retest) — a harness driving the real engine pinpointed and fixed five flow defects: ① TTS echo polluting the standby segment while the agent reads (could not wake); ② only above-threshold frames uploaded, so trailing characters never flushed (wake word truncated); ③ discarding the whole segment on a wake hit (saying "wake word + command" in one breath sent only the tail); ④ the command hanging when the hit arrived after you stopped speaking; ⑤ pausing after the wake word closed the command window (command lost). **The wake word may now be said together with your command — it is stripped and never sent**, standby live-shows what it heard, and "wake word … pause … command" works. Otherwise as v0.7.7: Edge cloud TTS by default, local TTS (VITS / Kokoro) optional; silence split defaults to 1500 ms.
20
20
 
21
21
  ---
22
22
 
@@ -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 → 0.1.5-rc.2 full range** |
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
@@ -15,7 +15,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
15
15
 
16
16
  ![全双工对话闭环:声音 → 文字 → 声音](https://raw.githubusercontent.com/qishuilalala/dsh-voice-mode/HEAD/plugin/dsh-voice-mode/assets/duplex-banner.png)
17
17
 
18
- > **版本说明(截至 v0.7.12,2026-09-19)**:v0.7.12 / v0.7.11 为**素材、文档与检索元数据更新**(运行时零改动;v0.7.11 含五版本兼容实测)。以下为 v0.7.10 起的**运行时**变更:**输出链路静默丢音根治**——① 云端 TTS 单句重试耗尽后不再静默跳句(现在状态条提示「有一句朗读失败,已跳过」,此前表现为「AI 回复偶尔不朗读」且无从判断);② 浏览器挂起 AudioContext 后的「无声播放」(UI 显示朗读中、字幕照走)现在每次入队 + 任意点击/按键自动恢复;③ SSE 丢帧导致的坏句丢弃留诊断痕迹。其余同 v0.7.9:**唤醒词链路全面修复**(Issue #10 + 真机复测)——台架驱动真引擎逐项定位并修掉五处流程缺陷:① 朗读期 TTS 回声污染待机段(喊不醒);② 只上传超门限帧导致尾字不 flush(唤醒词只剩半截);③ 唤醒命中丢整段(连说「唤醒词+命令」只发出去尾部几个字);④ 命中晚于停口时命令悬挂不定稿;⑤ 只喊唤醒词后停顿会关掉命令窗口(命令丢失)。**现在唤醒词可与命令连说、词头自动剥离不进消息**,待机态实时显示「它听到了什么」,支持「唤醒词…停顿…命令」。其余同 v0.7.7:朗读默认 Edge 云端,本地 TTS(VITS / Kokoro)可选;静音断句默认 1500 毫秒。
18
+ > **运行时变更(最近一批:v0.7.10,2026-09-18)**:**输出链路静默丢音根治**——① 云端 TTS 单句重试耗尽后不再静默跳句(现在状态条提示「有一句朗读失败,已跳过」,此前表现为「AI 回复偶尔不朗读」且无从判断);② 浏览器挂起 AudioContext 后的「无声播放」(UI 显示朗读中、字幕照走)现在每次入队 + 任意点击/按键自动恢复;③ SSE 丢帧导致的坏句丢弃留诊断痕迹。其余同 v0.7.9:**唤醒词链路全面修复**(Issue #10 + 真机复测)——台架驱动真引擎逐项定位并修掉五处流程缺陷:① 朗读期 TTS 回声污染待机段(喊不醒);② 只上传超门限帧导致尾字不 flush(唤醒词只剩半截);③ 唤醒命中丢整段(连说「唤醒词+命令」只发出去尾部几个字);④ 命中晚于停口时命令悬挂不定稿;⑤ 只喊唤醒词后停顿会关掉命令窗口(命令丢失)。**现在唤醒词可与命令连说、词头自动剥离不进消息**,待机态实时显示「它听到了什么」,支持「唤醒词…停顿…命令」。其余同 v0.7.7:朗读默认 Edge 云端,本地 TTS(VITS / Kokoro)可选;静音断句默认 1500 毫秒。
19
19
 
20
20
  ---
21
21
 
@@ -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 → 0.1.5-rc.2 全版本(+ 0.1.6-alpha.2 预览)** |
250
+ | 兼容 dsh | — | **0.1.1-rc.2 起全版本(含 0.1.7-rc.1)** |
252
251
 
253
252
  ---
254
253
 
package/lib/client.js CHANGED
@@ -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 = "928a72b";
2828
+ var BUILD_TAG = "4575aac";
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);
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 settingsScope = ctx.settings.register(
2060
- NS_VOICE_MODE,
2061
- createVoiceSettingsSchema(),
2062
- {
2063
- base: {
2064
- ttsEngine: config.ttsEngine,
2065
- voice: config.voice,
2066
- rate: config.rate,
2067
- interruptLevel: config.interruptLevel,
2068
- silenceMs: config.silenceMs,
2069
- idleTimeoutMinutes: config.idleTimeoutMinutes,
2070
- modelHost: config.modelHost
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
- let vset = settingsScope.get();
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
- ctx.effect(
2122
- () => settingsScope.watch((next) => {
2123
- const prev = vset;
2124
- vset = next;
2125
- if (next.ttsEngine !== engineKind) {
2126
- engineKind = next.ttsEngine;
2127
- queue.setEngine(makeEngine(engineKind));
2128
- } else if (engineKind === "kokoro" && next.kokoroModel !== activeKokoroModel) {
2129
- activeKokoroModel = next.kokoroModel;
2130
- queue.setEngine(makeEngine("kokoro"));
2131
- }
2132
- queue.updateVoice(next.voice, next.rate);
2133
- if (next.senseITN !== prev.senseITN || next.senseVoice !== prev.senseVoice) {
2134
- asr.markStale();
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
- void settingsScope.update({ mode }).then(() => {
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
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.1.5-rc.2, plus 0.1.6-alpha.2 preview). · DSH 语音双工插件:流式识别入草稿、按句朗读+实时字幕、开口即打断;免 API Key、安全加固、全版本兼容(0.1.1-rc.2 起,含 0.1.5-rc.2 端到端验证 + 0.1.6-alpha.2 预览核验)。",
4
- "version": "0.7.13",
4
+ "version": "0.7.15",
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.1",
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