dsh-voice-mode 0.7.18 → 0.7.19

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
@@ -3,7 +3,7 @@
3
3
  [![License: MIT](https://img.shields.io/github/license/qishuilalala/dsh-voice-mode?style=flat-square&color=blue)](../../LICENSE)
4
4
  [![Latest Release](https://img.shields.io/github/v/release/qishuilalala/dsh-voice-mode?style=flat-square&color=brightgreen&include_prereleases)](https://github.com/qishuilalala/dsh-voice-mode/releases)
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-voice-mode?style=flat-square&color=orange)](https://www.npmjs.com/package/dsh-voice-mode)
6
- [![Tests: 424 passing](https://img.shields.io/badge/tests-424%20%E2%9C%93-2ea043?style=flat-square)](../../docs/rules/STATE.md)
6
+ [![Tests: 431 passing](https://img.shields.io/badge/tests-431%20%E2%9C%93-2ea043?style=flat-square)](../../docs/rules/STATE.md)
7
7
 
8
8
  Full-duplex voice conversation mode for DeepSeek Harness (dsh): speak, get a
9
9
  spoken answer. Streamed zipformer2 ASR → editable draft → auto send → the
@@ -171,6 +171,8 @@ Effect timing: `voice`/`rate`/`ttsEngine`/`kokoroModel`/`spokenFormat` take effe
171
171
 
172
172
  ### Common voices (full list: `node scripts/list-voices.mjs`)
173
173
 
174
+ > **Desktop (Electron) note**: the official desktop host runs inside Electron, whose V8 forbids the external buffers returned by Kokoro's native add-on. The plugin automatically runs Kokoro in a real Node.js instead — **it needs Node.js >= 18 on PATH** (or set `DSHVM_NODE` to its path). Without it Kokoro reports an error; use the default Edge engine or local VITS (both work on desktop).
175
+
174
176
  | ShortName | Description |
175
177
  | --- | --- |
176
178
  | `zh-CN-XiaoxiaoNeural` | Xiaoxiao · Female (default) |
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![License: MIT](https://img.shields.io/github/license/qishuilalala/dsh-voice-mode?style=flat-square&color=blue)](LICENSE)
4
4
  [![Latest Release](https://img.shields.io/github/v/release/qishuilalala/dsh-voice-mode?style=flat-square&color=brightgreen&include_prereleases)](https://github.com/qishuilalala/dsh-voice-mode/releases)
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-voice-mode?style=flat-square&color=orange)](https://www.npmjs.com/package/dsh-voice-mode)
6
- [![Tests: 424 passing](https://img.shields.io/badge/tests-424%20%E2%9C%93-2ea043?style=flat-square)](../../docs/rules/STATE.md)
6
+ [![Tests: 431 passing](https://img.shields.io/badge/tests-431%20%E2%9C%93-2ea043?style=flat-square)](../../docs/rules/STATE.md)
7
7
 
8
8
  DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边出字的流式识别 → 停顿自动发送 → 最终答复按句流式朗读 + 实时字幕,开口即可打断(真 barge-in)。无需 API Key,识别模型在本地宿主端推理。
9
9
 
@@ -129,6 +129,8 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
129
129
 
130
130
  ### 本地音色(VITS / Kokoro)
131
131
 
132
+ > **桌面端(Electron)提示**:官方桌面端的宿主运行在 Electron 里,其 V8 不允许 Kokoro 的原生 addon 返回外部缓冲区。插件会自动改用一个真正的 Node.js 来跑 Kokoro——**需要系统 PATH 上有 Node.js ≥18**(或用环境变量 `DSHVM_NODE` 指定路径);没有则 Kokoro 会提示失败,请改用默认的 Edge 或本地 VITS(两者在桌面端不受影响)。
133
+
132
134
  - **VITS(纯中文)**:`suyingxue` 素映雪·女 / `gunian` 顾念·男 / `fushiyu` 傅斯遇·女 / `bingjiao` 冰娇·男 / `bazong` 霸总·男
133
135
  - **Kokoro(中英混读均可)**:103 个音色全量入表,面板按编号 + 实测性别标注;四个常用男声置顶:`62` 深沉 / `68` 浑厚 / `75` 清亮 / `76` 磁性;中文女声 `48` 小北 / `49` 小妮 / `50` 小小 / `51` 小艺。音色只是风格向量,**语言能力与音色无关**。
134
136
 
package/lib/client.js CHANGED
@@ -3099,7 +3099,7 @@ var TELEMETRY_VIEW = [
3099
3099
  { stage: "first-tts-chunk", key: "telFirstChunk" },
3100
3100
  { stage: "first-audio-played", key: "telFirstPlayed" }
3101
3101
  ];
3102
- var BUILD_TAG = "552d191";
3102
+ var BUILD_TAG = "62781d5";
3103
3103
  var TELEMETRY_FLAG = "dsh-voice-mode.telemetry";
3104
3104
  var telemetryEnabled = typeof localStorage !== "undefined" && localStorage.getItem(TELEMETRY_FLAG) === "1";
3105
3105
  console.log("[dsh-voice] build=" + BUILD_TAG);
package/lib/index.js CHANGED
@@ -1,12 +1,11 @@
1
1
  // src/index.ts
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { dirname as dirname3, join as join5 } from "node:path";
3
+ import { dirname as dirname2, join as join5 } from "node:path";
4
4
  import { homedir as homedir2 } from "node:os";
5
5
  import { rm } from "node:fs/promises";
6
6
 
7
7
  // src/asr-host.ts
8
8
  import { statSync } from "node:fs";
9
- import { stat as stat2 } from "node:fs/promises";
10
9
  import { join as join2 } from "node:path";
11
10
  import { fileURLToPath } from "node:url";
12
11
  import { Worker } from "node:worker_threads";
@@ -185,7 +184,7 @@ async function sha256OfFile(path) {
185
184
  return hash.digest("hex");
186
185
  }
187
186
  async function ensureModelFile(opts) {
188
- const { repo, repoDir, spec, primaryHost, allowCustomHost, broadcast } = opts;
187
+ const { repoDir, spec, primaryHost, broadcast } = opts;
189
188
  const localPath = join(repoDir, spec.file);
190
189
  const partPath = `${localPath}.part`;
191
190
  if ((await stat(localPath).catch(() => null))?.isFile()) {
@@ -605,8 +604,8 @@ function createAsrRuntime(options) {
605
604
  }
606
605
  let finMap = finalized.get(sessionId);
607
606
  const myGen = resetGen.get(sessionId) ?? 0;
608
- const cached = finMap?.get(epoch);
609
- if (cached !== void 0) return { text: cached };
607
+ const cached2 = finMap?.get(epoch);
608
+ if (cached2 !== void 0) return { text: cached2 };
610
609
  let sessSegs = segments.get(sessionId);
611
610
  if (!sessSegs) {
612
611
  sessSegs = /* @__PURE__ */ new Map();
@@ -847,10 +846,6 @@ function createAsrRuntime(options) {
847
846
  senseWorkerLangKey = "";
848
847
  },
849
848
  modelStatus: () => {
850
- const statFile = async (dir, repo, name2) => {
851
- const st = await stat2(join2(dir, repo, name2)).catch(() => null);
852
- return { exists: !!st?.isFile(), size: st?.size ?? 0 };
853
- };
854
849
  const asrFiles = MODEL_FILES.map((n) => ({
855
850
  name: n.file,
856
851
  exists: (() => {
@@ -1300,6 +1295,50 @@ var TtsQueue = class {
1300
1295
 
1301
1296
  // src/tts-local.ts
1302
1297
  import { fork } from "node:child_process";
1298
+
1299
+ // src/tts-runtime.ts
1300
+ import { spawnSync } from "node:child_process";
1301
+ var probeNode = (command, env) => {
1302
+ try {
1303
+ const r = spawnSync(
1304
+ command,
1305
+ ["-p", "JSON.stringify([process.versions.node, !!process.versions.electron, process.execPath])"],
1306
+ { env, encoding: "utf8", timeout: 5e3, windowsHide: true }
1307
+ );
1308
+ if (r.status !== 0) return null;
1309
+ const [version, electron, execPath] = JSON.parse(r.stdout.trim());
1310
+ return !electron && Number.parseInt(version, 10) >= 18 && typeof execPath === "string" ? execPath : null;
1311
+ } catch {
1312
+ return null;
1313
+ }
1314
+ };
1315
+ var NATIVE_RUNTIME_UNAVAILABLE = 'Local Kokoro cannot run inside the Electron-based desktop host (native add-on blocked: "External buffers are not allowed"). Install Node.js >= 18 on PATH (or set DSHVM_NODE to its path), or switch the read-aloud engine to Edge or VITS.';
1316
+ var cached;
1317
+ function resolveNativeRuntime(opts = {}) {
1318
+ const env = opts.env ?? process.env;
1319
+ const useCache = opts.useCache ?? opts.env === void 0;
1320
+ if (useCache && cached !== void 0) return cached;
1321
+ const electron = opts.electron ?? !!process.versions.electron;
1322
+ let result;
1323
+ if (!electron) {
1324
+ result = { env };
1325
+ } else {
1326
+ const childEnv = { ...env };
1327
+ delete childEnv.ELECTRON_RUN_AS_NODE;
1328
+ const probe = opts.probe ?? probeNode;
1329
+ const candidates = [env.DSHVM_NODE, "node"].filter((c) => typeof c === "string" && c.length > 0);
1330
+ let execPath = null;
1331
+ for (const c of candidates) {
1332
+ execPath = probe(c, childEnv);
1333
+ if (execPath) break;
1334
+ }
1335
+ result = execPath ? { execPath, env: childEnv } : null;
1336
+ }
1337
+ if (useCache) cached = result;
1338
+ return result;
1339
+ }
1340
+
1341
+ // src/tts-local.ts
1303
1342
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1304
1343
  import { join as join3 } from "node:path";
1305
1344
  import { statSync as statSync2 } from "node:fs";
@@ -1668,7 +1707,12 @@ function createSherpaLocalEngine(options) {
1668
1707
  if (!treeOk) throw new Error("local TTS model download failed: espeak-ng-data");
1669
1708
  }
1670
1709
  if (!child) {
1671
- child = fork(workerPath, [], { stdio: ["ignore", "ignore", "pipe", "ipc"] });
1710
+ const runtime = options.kind === "kokoro" ? resolveNativeRuntime() : { env: process.env };
1711
+ if (!runtime) throw new Error(NATIVE_RUNTIME_UNAVAILABLE);
1712
+ child = fork(workerPath, [], {
1713
+ stdio: ["ignore", "ignore", "pipe", "ipc"],
1714
+ ...runtime.execPath ? { execPath: runtime.execPath, env: runtime.env } : {}
1715
+ });
1672
1716
  let stderrTail = "";
1673
1717
  child.stderr?.on("data", (chunk) => {
1674
1718
  stderrTail += String(chunk);
@@ -2143,7 +2187,7 @@ function previewSample(engine, voice) {
2143
2187
  // src/settings-store.ts
2144
2188
  import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2145
2189
  import { homedir } from "node:os";
2146
- import { dirname as dirname2, join as join4 } from "node:path";
2190
+ import { dirname, join as join4 } from "node:path";
2147
2191
  var SETTINGS_FILE_NAME = "voice-mode.settings.json";
2148
2192
  function settingsFilePath(profileHome) {
2149
2193
  const home = typeof profileHome === "string" && profileHome ? profileHome : process.env.DSH_HOME || join4(homedir(), ".dsh");
@@ -2181,7 +2225,7 @@ function readOverrides(file, keys) {
2181
2225
  }
2182
2226
  }
2183
2227
  function writeOverrides(file, values) {
2184
- mkdirSync(dirname2(file), { recursive: true });
2228
+ mkdirSync(dirname(file), { recursive: true });
2185
2229
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
2186
2230
  writeFileSync(tmp, JSON.stringify(values, null, 2) + "\n", { encoding: "utf8", mode: 384 });
2187
2231
  try {
@@ -2536,7 +2580,7 @@ function apply(ctx, config) {
2536
2580
  if (loaded.warn) console.warn(`[dsh-voice-mode] ignoring settings overlay ${settingsFile}: ${loaded.warn}`);
2537
2581
  let migrated = false;
2538
2582
  if (!loaded.exists) {
2539
- const legacy = readLegacyVoiceSettings(dirname3(settingsFile), SETTING_KEYS);
2583
+ const legacy = readLegacyVoiceSettings(dirname2(settingsFile), SETTING_KEYS);
2540
2584
  if (Object.keys(legacy).length > 0) {
2541
2585
  loaded.values = legacy;
2542
2586
  migrated = true;
package/package.json CHANGED
@@ -2,7 +2,7 @@
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.2.0-rc.2).",
4
4
  "icon": "./icon.svg",
5
- "version": "0.7.18",
5
+ "version": "0.7.19",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "scripts": {
48
48
  "build": "node build.mjs",
49
49
  "prepack": "node build.mjs",
50
- "test": "node test/segmenter.test.mjs && node test/aec.test.mjs && node test/download.test.mjs && node test/endpoint.test.mjs && node test/endpoint-short.test.mjs && node test/resample.test.mjs && node test/sense-worker.test.mjs && node test/detect-route.test.mjs && node test/verify-client.mjs && node test/asr-host-rebuild.test.mjs && node test/barge-in-manual.test.mjs && node test/sense-lang.test.mjs && node test/caption-a11y.test.mjs && node test/emotion.test.mjs && node test/emotion-tts-local.test.mjs && node test/emotion-integration.test.mjs && node test/backchannel.test.mjs && node test/strings-coverage.test.mjs && node test/yield-ms-wiring.test.mjs && node test/matchBackchannel.test.mjs && node test/settings-load.test.mjs && node test/preview-error.test.mjs && node test/hold-clear.test.mjs && node test/barge-in-detect.test.mjs && node test/wakeword.test.mjs && node test/wake-standby.test.mjs && node test/wake-flow.test.mjs && node test/tts-playback.test.mjs && node test/settings-store.test.mjs && node test/i18n.test.mjs && node test/i18n-protocol.test.mjs && node test/package-install.test.mjs",
50
+ "test": "node test/segmenter.test.mjs && node test/aec.test.mjs && node test/download.test.mjs && node test/endpoint.test.mjs && node test/endpoint-short.test.mjs && node test/resample.test.mjs && node test/sense-worker.test.mjs && node test/detect-route.test.mjs && node test/verify-client.mjs && node test/asr-host-rebuild.test.mjs && node test/barge-in-manual.test.mjs && node test/sense-lang.test.mjs && node test/caption-a11y.test.mjs && node test/emotion.test.mjs && node test/emotion-tts-local.test.mjs && node test/emotion-integration.test.mjs && node test/backchannel.test.mjs && node test/strings-coverage.test.mjs && node test/yield-ms-wiring.test.mjs && node test/matchBackchannel.test.mjs && node test/settings-load.test.mjs && node test/preview-error.test.mjs && node test/hold-clear.test.mjs && node test/barge-in-detect.test.mjs && node test/wakeword.test.mjs && node test/wake-standby.test.mjs && node test/wake-flow.test.mjs && node test/tts-playback.test.mjs && node test/settings-store.test.mjs && node test/i18n.test.mjs && node test/i18n-protocol.test.mjs && node test/package-install.test.mjs && node test/tts-runtime.test.mjs",
51
51
  "verify:client": "node test/verify-client.mjs",
52
52
  "prefetch": "node scripts/prefetch.mjs",
53
53
  "list-voices": "node scripts/list-voices.mjs",