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 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 → 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
@@ -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
@@ -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", "settingsScope"];
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 = "4a95ba9";
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
- if (ctx.settingsScope) {
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: ctx.settingsScope.bind({ namespace: "voice-mode" }) })
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 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
- "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.14",
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.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