dsh-voice-mode 0.7.18 → 0.7.20
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 +4 -2
- package/README.md +5 -3
- package/lib/client.js +1 -1
- package/lib/index.js +57 -13
- package/package.json +3 -5
package/README.en.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](../../LICENSE)
|
|
4
4
|
[](https://github.com/qishuilalala/dsh-voice-mode/releases)
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-voice-mode)
|
|
6
|
-
[](../../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
|
|
@@ -16,7 +16,7 @@ interrupts playback and the running turn. No API key.
|
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
-
> **Runtime changes (most recent batch: v0.7.
|
|
19
|
+
> **Runtime changes (most recent batch: v0.7.17 – v0.7.19, 2026-10-02)**: (1) **Settings work on every dsh version** — fixed the settings page and persistence on dsh 0.1.7+ (dedicated Settings → Voice Mode page, writes survive restarts); (2) **i18n reworked** — the UI follows dsh's official language setting, Chinese/English are fully consistent and switch without a reload; (3) **fixed fresh-install failures with npm/pnpm and `failed to import` on dsh ≥0.1.7** (affects 0.7.17 and earlier — please upgrade to ≥0.7.18); (4) **local Kokoro fixed on the official Electron desktop host** — needs Node.js ≥18 on PATH (see the desktop note below). Earlier batches (v0.7.10: silent-audio fixes, etc.) and the full history are in [`CHANGELOG.md`](../../CHANGELOG.md).
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -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)
|
|
4
4
|
[](https://github.com/qishuilalala/dsh-voice-mode/releases)
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-voice-mode)
|
|
6
|
-
[](../../docs/rules/STATE.md)
|
|
7
7
|
|
|
8
8
|
DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边出字的流式识别 → 停顿自动发送 → 最终答复按句流式朗读 + 实时字幕,开口即可打断(真 barge-in)。无需 API Key,识别模型在本地宿主端推理。
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
17
17
|
|
|
18
18
|

|
|
19
19
|
|
|
20
|
-
> **运行时变更(最近一批:v0.7.
|
|
20
|
+
> **运行时变更(最近一批:v0.7.17 ~ v0.7.19,2026-10-02)**:① **设置面全版本打通**——dsh 0.1.7+ 上设置页与持久化修复(Settings → 语音模式 专属页、写入与重启保留);② **国际化重做**——界面语言跟随 dsh 官方语言设置,中/英文完整一致、切换无需刷新;③ **修复 npm/pnpm 全新安装失败与 dsh ≥0.1.7 已安装形态「failed to import」**(0.7.17 及更早版本受影响,请升级到 ≥0.7.18);④ **官方桌面端(Electron)下本地 Kokoro 修复**——需系统 PATH 上有 Node.js ≥18(见下文「桌面端提示」)。此前批次(v0.7.10:输出链路静默丢音根治等)与完整历史见 [`CHANGELOG.md`](../../CHANGELOG.md)。
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -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
|
|
|
@@ -256,7 +258,7 @@ flowchart LR
|
|
|
256
258
|
| 热词偏置 | 无 | 无(已移除,详见 v0.7.7 文档说明) |
|
|
257
259
|
| 字幕 a11y | 无 | **4 档字号 + 3 档宽度 + 主题跟随** |
|
|
258
260
|
| 唤醒词 | 无 | **轻量流式匹配 + 前缀语气词白名单** |
|
|
259
|
-
| 兼容 dsh | — | **0.1.1-rc.2 起全版本(含 0.2.
|
|
261
|
+
| 兼容 dsh | — | **0.1.1-rc.2 起全版本(含 0.2.1-alpha.1)** |
|
|
260
262
|
|
|
261
263
|
---
|
|
262
264
|
|
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 = "
|
|
3102
|
+
var BUILD_TAG = "65cadbc";
|
|
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
|
|
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 {
|
|
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
|
|
609
|
-
if (
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
5
|
+
"version": "0.7.20",
|
|
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",
|
|
@@ -60,9 +60,7 @@
|
|
|
60
60
|
"typecheck:dual": "bash scripts/typecheck-dual.sh",
|
|
61
61
|
"smoke:runtime": "bash scripts/smoke-runtime.sh",
|
|
62
62
|
"verify:dual": "bash scripts/verify-dual.sh",
|
|
63
|
-
"
|
|
64
|
-
"e2e:full": "bash scripts/full-e2e.sh",
|
|
65
|
-
"release": "bash scripts/release.sh"
|
|
63
|
+
"e2e:full": "bash scripts/full-e2e.sh"
|
|
66
64
|
},
|
|
67
65
|
"dsh": {
|
|
68
66
|
"bundle": {
|