dsh-voice-mode 0.7.8 → 0.7.10
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 +3 -3
- package/README.md +5 -3
- package/lib/client.js +98 -43
- package/lib/index.js +11 -3
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -14,7 +14,7 @@ interrupts playback and the running turn. No API key.
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
-
> **Version note (v0.7.
|
|
17
|
+
> **Version note (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.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -37,7 +37,7 @@ interrupts playback and the running turn. No API key.
|
|
|
37
37
|
- **Two interaction modes (switchable in settings, plus a mode-switch button beside the mic)**:
|
|
38
38
|
- `toggle` (default) continuous listening: RMS VAD segmentation → streaming zipformer2 ASR (words appear as you speak, live caption preview) → automatic sentence split after 1500 ms of silence into the draft, consecutive segments joined into one message, then auto-sent after ~1500 ms more of silence (≈3 s total); hold `Ctrl` to force an immediate send
|
|
39
39
|
- `hold` push-to-talk: short tap to enter/exit, **hold the mic button to talk, release to send** (swipe up to cancel, `Esc`/blur abandons the segment; pauses do not split while held, up to 10 min); hold `Ctrl` to record-by-keyboard, release to send
|
|
40
|
-
- **Wake word (optional, off by default)**: after setting `wakeWord`, entering voice mode starts in standby, and recognition only begins once the wake word is spoken (e.g. `你好小D`),
|
|
40
|
+
- **Wake word (optional, off by default)**: after setting `wakeWord`, entering voice mode starts in standby, and recognition only begins once the wake word is spoken (e.g. `你好小D`). **You may say it together with your command** ("wake word, check the weather") — the wake word is stripped from the transcript/caption and never sent; matching is fault-tolerant (homophone substitutions, a wrong first char, leading fillers); 3-4 characters recommended; in standby the status bar live-shows "say '<word>' · what it heard". Boundaries (important): **toggle mode only** (inactive with `hold` / manual barge-in); **it must be repeated after each completed utterance or barge-in** (if you only said the wake word, the engine keeps listening so you may pause before the command); **saying it while the agent is reading does not trigger** (barge-in is VAD-based and unrelated to the wake word)
|
|
41
41
|
- **Caption tiers (batch 3)**: `captionFontSize` 4 levels (0=12px / 1=14px / 2=18px / 3=24px) + `captionMaxWidth` 3 levels (0=50vw / 1=70vw / 2=90vw); adapts to narrow viewports
|
|
42
42
|
- **Yield semantics (batch 5 / ADR-0008)**: `backchannelYield` on by default (I10-exempt) — saying `嗯 / 对 / right` while the AI is reading auto-pauses for 1.5 s; genuine speech still triggers hard barge-in; lets the LLM yield the turn
|
|
43
43
|
- **Output pipeline**: only the final answer's `text-delta` is read (reasoning/tool calls are skipped), streamed sentence-by-sentence (Edge cloud by default; local VITS / Kokoro, int8/fp32, optional) with a live caption overlay at the bottom-right; tool calls trigger a beep; the full text is still written to the chat; in voice mode a spoken-format system prompt is injected (short natural sentences, no Markdown decoration), and the reader side strips markers as well for a smoother listening experience
|
|
@@ -143,7 +143,7 @@ If a wake word is configured, you land in standby first (the status bar prompts
|
|
|
143
143
|
| `modelHost` | default | Model download host (use `https://hf-mirror.com` on mainland networks) |
|
|
144
144
|
| `autoSend` | `true` | Auto-send once quiet (consecutive segments join into one message); when off, text only goes to the draft (hold `Ctrl` / release in hold mode still sends) |
|
|
145
145
|
| `mode` | `toggle` | Interaction mode: `toggle` continuous listening + 1500 ms silence split; `hold` push-to-talk, release to send (short tap exits) |
|
|
146
|
-
| `wakeWord` | empty (off) | Wake word (e.g. `你好小D`): speak it after entering to activate
|
|
146
|
+
| `wakeWord` | empty (off) | Wake word (e.g. `你好小D`): speak it after entering to activate; empty = off. **May be said together with your command** — the wake word is stripped and never sent; fault-tolerant matching (edit distance ≤1 + a 3-char leading window absorbs homophones/fillers); **3-4 characters recommended** (single-char words get no tolerance; a 2-char word also absorbs any same-first-char 2-char word); must be repeated after each utterance split or barge-in; **toggle mode only**; not triggered while the agent is reading |
|
|
147
147
|
| `spokenFormat` | `true` | Spoken-format system prompt: inject "short natural sentences, no Markdown decoration" into voice-mode replies only, **applies live** |
|
|
148
148
|
| `senseITN` | `true` | Batch 2 P0: SenseVoice inverse text normalization (number / date / currency; on by default) |
|
|
149
149
|
| `captionFontSize` | `0` | Batch 3 P0: caption font tier 0=12px / 1=14px / 2=18px / 3=24px (default 0 is byte-equivalent to legacy) |
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
> **版本说明(v0.7.
|
|
16
|
+
> **版本说明(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 毫秒。
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -42,7 +42,7 @@ DeepSeek Harness 语音双工对话模式:会话内一键进入 → 边说边
|
|
|
42
42
|
- **两种交互模式(输入框旁按钮或设置可切换,切换即持久化)**:
|
|
43
43
|
- `toggle`(默认)持续聆听:RMS VAD 分段 → zipformer2 流式识别(边说边出字,实时字幕预览)→ 静音约 1500 毫秒断句进草稿,连续多段拼成一条消息,再静音约 1500 毫秒(合计约 3 秒)自动发送;按住 `Ctrl` 强制立即发送
|
|
44
44
|
- `hold` 按住说话:短按进入/退出,**按住麦克风按钮说话、松手即发**(滑出取消、`Esc`/失焦放弃本段);按住期间停顿不断句(上限 10 分钟);`Ctrl` 按住即录、松开即发
|
|
45
|
-
- **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D
|
|
45
|
+
- **唤醒词(可选,默认关)**:设置 `wakeWord` 后进入待机态,说出唤醒词才开始识别(如「你好小D」)。**可直接与命令连说**(「你好小D,帮我查天气」)——唤醒词会在定稿/字幕里自动剥掉、不进消息;匹配带容错(同音字替换「小莫→小墨」、首字错、前置语气词「呃/喂/我说」),建议 3-4 字;待机态状态条实时显示「说『x』开始 · 它听到的转写」,唤醒成败可自查。边界(重要):**仅 `toggle` 模式生效**(`hold` / 手动打断下唤醒词静默失效);**说完整一句后回到待机态需重说唤醒词**(只喊唤醒词、还没说命令时保持聆听,可停顿想好再说);**朗读期说唤醒词不触发**(打断由 VAD 开口即触发,与唤醒词无关)
|
|
46
46
|
- **字幕档位**(批 3):`captionFontSize` 4 档(0=12px / 1=14px / 2=18px / 3=24px)+ `captionMaxWidth` 3 档(0=50vw / 1=70vw / 2=90vw),窄屏自适应
|
|
47
47
|
- **让位语义**(批 5 / ADR-0008):`backchannelYield` 默认开(I10 豁免)—— 朗读期用户插话「嗯/对」自动让位 1.5s,真要说走硬打断;让 LLM 主动让出话轮(人格层让位);关掉恢复改造前行为
|
|
48
48
|
- **输出链路**:只朗读最终答复的 `text-delta`(reasoning/工具调用不读),按句流式朗读(默认 Edge 云端;可切本地 VITS/Kokoro,中英混读选 Kokoro)+ 右下角实时字幕浮层;工具调用触发提示音;全文照常写入聊天记录;口语化提示词(设置 `spokenFormat`,默认开)让回复为自然短句、不带 Markdown 排版符号
|
|
@@ -100,7 +100,7 @@ bundle 插件安装后需重启 dsh 生效(Linux:`systemctl restart dsh`;
|
|
|
100
100
|
| `modelHost` | 默认源 | 模型下载源(国内网络填 `https://hf-mirror.com`) |
|
|
101
101
|
| `autoSend` | `true` | 静音到点自动发送(连续多段拼成一条消息);关闭则只进草稿(按住 `Ctrl` / hold 松手仍会发送) |
|
|
102
102
|
| `mode` | `toggle` | 交互模式:`toggle` 持续聆听 + 1500ms 静音断句;`hold` 按住说话、松手发送(短按退出) |
|
|
103
|
-
| `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 =
|
|
103
|
+
| `wakeWord` | 空(关) | 唤醒词(如「你好小D」):进入后先说唤醒词激活,避免误触;空 = 关闭。**可与命令连说**,词头自动剥掉不进消息;匹配带容错(编辑距离 ≤1 + 前 3 字符前导窗口,吸收同音字/语气词);**建议 3-4 字**——单字词无容错、2 字词的容错会连带吸收所有同首字的 2 字词(如「小莫」也会唤醒「小张」),介意误唤醒用 3 字以上;每句断句或打断后回待机需重说;**仅 toggle 模式生效**(hold / 手动打断下不生效);朗读期说唤醒词不触发 |
|
|
104
104
|
| `spokenFormat` | `true` | 语音会话注入口语化提示词:开启后**仅当前语音会话**的回复被注入「口语化短句、不用 Markdown 排版符号」提示词(朗读更顺),**即时生效** |
|
|
105
105
|
| `senseITN` | `true` | 批 2 P0:SenseVoice 逆文本归一化(数字/日期/货币规范化;默认开) |
|
|
106
106
|
| `captionFontSize` | `0` | 批 3 P0:字幕字号档位 0=12px / 1=14px / 2=18px / 3=24px(默认 0 与现状字节等价) |
|
|
@@ -221,10 +221,12 @@ flowchart LR
|
|
|
221
221
|
| 点麦克风无反应,状态条红字 | 浏览器拒绝麦克风:地址栏(iOS 为 设置 → Safari → 麦克风)开启后重试 |
|
|
222
222
|
| 状态条「正在加载模型… x%」卡住 | 检查网络;模型较大可先 `npm run prefetch`;国内网络 `modelHost` 配 `https://hf-mirror.com` |
|
|
223
223
|
| 朗读无声音/无字幕 | 本地引擎首次合成需加载模型;若持续失败查看状态条提示(自动退避重试);确认页面前台且未静音 |
|
|
224
|
+
| AI 回复**偶尔不朗读**(某句没声/整条没声) | 已定位三类原因并修(v0.7.10):① 云端 TTS 偶发超时——单句自动重试 3 次,仍失败会**跳过该句并在状态条提示**「有一句朗读失败,已跳过」(此前是静默丢句);② 浏览器挂起 AudioContext(切到后台标签页/长时间静音后回来)——播放被调度但无声、UI 却显示朗读中;现在每次入队与**任意点击/按键**都会自动恢复;③ SSE 断线丢帧导致整句不完整——按设计丢弃该句(避免播坏音频),可开 `localStorage['dsh-voice-mode.telemetry']='1'` 看 `tts-drop-sentence` 诊断 |
|
|
224
225
|
| 语音模式进不去 | 检查插件 `enabled`;多标签页时确认当前会话为活动会话 |
|
|
225
226
|
| 识别到但不是我要说的 | 环境噪声或唤醒词误判:降低音量、提高 `interruptLevel`(高门槛)或启用 `wakeWord` |
|
|
226
227
|
| 唤醒词唤不醒 | 先看待机态状态条的实时转写(它听到了什么):同音字 1 字内已自动容错;停顿约 1.5 秒(= 静音断句时长)再说,清掉待机段残留语音、从段首重新匹配;换一个转写稳定的词(建议 3-4 字) |
|
|
227
228
|
| 打断后说话没反应(开了唤醒词) | 唤醒词只管「开始识别」的门:打断/每句断句后回到待机态,**需重说唤醒词**再继续说;待机态状态条会显示「说『x』开始」提示 |
|
|
229
|
+
| 唤醒词完全没反应 | ① 确认交互模式是 `toggle`(`hold` 与手动打断下唤醒词不生效);② 确认 `bargeInMode`:`manual`(或 `detect` 在本机无原生回声消除时自动落 manual)会关闭常驻聆听 → 唤醒词失效,改回 `auto` 或按住麦克风/Ctrl;③ 朗读期说唤醒词不触发(先等 AI 说完);④ 看待机态实时转写,确认它听到了什么(同音字 1 字内已容错,建议 3-4 字词) |
|
|
228
230
|
| 按住说话松手后没反应 | 确认交互模式为「按住说」且按住期间按钮高亮;松手后识别定稿约 1 秒内进入草稿 |
|
|
229
231
|
| 打不断(朗读中开口无反应) | 调高 `interruptLevel`(更敏感档 = 0 或 1)或检查麦克风权限;若 VAD 持续不触发可临时切到「手动打断」(`mode: hold` / 唤醒词定时延后);**不要**调 `echoGateDb`——真机 3.1 分钟朗读期里 Silero 0/777 帧把回声判成语音,原生 AEC 生效时此阈值从未被执行(详见 ADR-0006) |
|
|
230
232
|
| 字幕被输入框挡住 | 默认 `captionMaxWidth=1`(70vw)+ `captionFontSize=0`(12px)在窄屏可能与底部输入框重叠;调高档位或点字幕浮层「×」收起 |
|
package/lib/client.js
CHANGED
|
@@ -87,6 +87,24 @@ function editDistanceWithin(a, b, max) {
|
|
|
87
87
|
}
|
|
88
88
|
return prev[lb];
|
|
89
89
|
}
|
|
90
|
+
function wakePrefixLength(text, wakeWord) {
|
|
91
|
+
const w = normalizeWake(wakeWord);
|
|
92
|
+
if (!w || w.length < 2) return 0;
|
|
93
|
+
const raw = String(text ?? "");
|
|
94
|
+
for (let i = 1; i <= raw.length; i++) {
|
|
95
|
+
const p = normalizeWake(raw.slice(0, i));
|
|
96
|
+
if (p.length < w.length) continue;
|
|
97
|
+
if (p.length > w.length + WAKE_MAX_EDITS) break;
|
|
98
|
+
if (editDistanceWithin(p, w, WAKE_MAX_EDITS) <= WAKE_MAX_EDITS) return i;
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
function stripWakePrefix(text, wakeWord) {
|
|
103
|
+
const n = wakePrefixLength(text, wakeWord);
|
|
104
|
+
const raw = String(text ?? "");
|
|
105
|
+
if (n <= 0) return raw;
|
|
106
|
+
return raw.slice(n).replace(/^[\s\u3000,。!?!?;;、,.]+/, "");
|
|
107
|
+
}
|
|
90
108
|
function matchWakeWord(partial, wakeWord) {
|
|
91
109
|
const w = normalizeWake(wakeWord);
|
|
92
110
|
if (!w) return false;
|
|
@@ -477,6 +495,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
477
495
|
let resetGate = Promise.resolve();
|
|
478
496
|
let segmentEpoch = 0;
|
|
479
497
|
let wakeSilenceMs = 0;
|
|
498
|
+
let wakeConsumed = false;
|
|
480
499
|
let forcePending = false;
|
|
481
500
|
let uploadedSamples = 0;
|
|
482
501
|
let detectChunks = [];
|
|
@@ -583,15 +602,9 @@ function createAsrEngine(config, sessionId) {
|
|
|
583
602
|
uploadedSamples = Math.max(uploadedSamples, from + samples.length);
|
|
584
603
|
emit(partialListeners, out.text ?? "");
|
|
585
604
|
if (matchWakeWord(out.text ?? "", wakeWord)) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
speechMs = 0;
|
|
590
|
-
silenceMs = 0;
|
|
591
|
-
prePad = [];
|
|
592
|
-
uploadedSamples = 0;
|
|
593
|
-
utteranceEndAt = null;
|
|
594
|
-
await resetHostStream();
|
|
605
|
+
wakeConsumed = true;
|
|
606
|
+
speechActive = true;
|
|
607
|
+
silenceMs = wakeSilenceMs;
|
|
595
608
|
if (active) setState("listening");
|
|
596
609
|
}
|
|
597
610
|
return;
|
|
@@ -599,7 +612,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
599
612
|
if (out.isSpeech !== void 0) config.onIsSpeech?.(out.isSpeech);
|
|
600
613
|
if (state === "loading-model") setState("speech");
|
|
601
614
|
uploadedSamples = Math.max(uploadedSamples, from + samples.length);
|
|
602
|
-
emit(partialListeners, out.text ?? "");
|
|
615
|
+
emit(partialListeners, wakeConsumed ? stripWakePrefix(out.text ?? "", wakeWord) : out.text ?? "");
|
|
603
616
|
if (config.backchannelYield !== false && // 默认 true(I10 豁免);显式 false 时跳过
|
|
604
617
|
speechActive && (config.isPlaying?.() ?? false) && matchBackchannel(out.text ?? "")) {
|
|
605
618
|
config.onBackchannel?.();
|
|
@@ -671,6 +684,30 @@ function createAsrEngine(config, sessionId) {
|
|
|
671
684
|
release();
|
|
672
685
|
})();
|
|
673
686
|
};
|
|
687
|
+
const trimPrePad = () => {
|
|
688
|
+
let total = 0;
|
|
689
|
+
let cut = 0;
|
|
690
|
+
for (let i = prePad.length - 1; i >= 0; i--) {
|
|
691
|
+
total += prePad[i].length / SAMPLE_RATE2 * 1e3;
|
|
692
|
+
if (total > PRE_PAD_MS) {
|
|
693
|
+
cut = i + 1;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (cut > 0) prePad = prePad.slice(cut);
|
|
698
|
+
};
|
|
699
|
+
const clearWakeSegment = () => {
|
|
700
|
+
segmentEpoch++;
|
|
701
|
+
segment = [];
|
|
702
|
+
segmentMs = 0;
|
|
703
|
+
speechMs = 0;
|
|
704
|
+
silenceMs = 0;
|
|
705
|
+
wakeSilenceMs = 0;
|
|
706
|
+
wakeConsumed = false;
|
|
707
|
+
prePad = [];
|
|
708
|
+
uploadedSamples = 0;
|
|
709
|
+
void resetHostStream();
|
|
710
|
+
};
|
|
674
711
|
const finalizeSegment = (force = false) => {
|
|
675
712
|
if (segment.length === 0) return;
|
|
676
713
|
if (config.isPlaying?.() && !forcePending && !force) return;
|
|
@@ -685,6 +722,8 @@ function createAsrEngine(config, sessionId) {
|
|
|
685
722
|
const epochSnapshot = segmentEpoch;
|
|
686
723
|
segmentEpoch++;
|
|
687
724
|
const meta = { force: forcePending };
|
|
725
|
+
const consumedWake = wakeConsumed;
|
|
726
|
+
wakeConsumed = false;
|
|
688
727
|
forcePending = false;
|
|
689
728
|
speechMs = 0;
|
|
690
729
|
uploadedSamples = 0;
|
|
@@ -769,7 +808,13 @@ function createAsrEngine(config, sessionId) {
|
|
|
769
808
|
continue;
|
|
770
809
|
}
|
|
771
810
|
if (segmentEpoch !== epochSnapshot + 1) return;
|
|
772
|
-
|
|
811
|
+
const rawText = out.text ?? "";
|
|
812
|
+
const finalText = consumedWake ? stripWakePrefix(rawText, wakeWord) : rawText;
|
|
813
|
+
if (finalText) {
|
|
814
|
+
emit(transcriptListeners, finalText, meta);
|
|
815
|
+
} else if (consumedWake && active && !speechActive && !holdActive) {
|
|
816
|
+
setState("listening");
|
|
817
|
+
}
|
|
773
818
|
return;
|
|
774
819
|
}
|
|
775
820
|
emitError("recognitionFail");
|
|
@@ -838,30 +883,27 @@ function createAsrEngine(config, sessionId) {
|
|
|
838
883
|
if (segmentMs > MAX_SEGMENT_MS) finalizeSegment();
|
|
839
884
|
} else if (config.mode === "hold") {
|
|
840
885
|
} else if (state === "wake") {
|
|
841
|
-
if (
|
|
886
|
+
if (config.isPlaying?.()) {
|
|
887
|
+
if (segment.length > 0) clearWakeSegment();
|
|
888
|
+
prePad = [];
|
|
889
|
+
} else if (rms > SPEECH_RMS) {
|
|
890
|
+
if (segment.length === 0) {
|
|
891
|
+
for (const p of prePad) segment.push(p);
|
|
892
|
+
}
|
|
893
|
+
prePad = [];
|
|
842
894
|
segmentMs += durationMs;
|
|
895
|
+
speechMs += durationMs;
|
|
843
896
|
segment.push(data);
|
|
844
897
|
wakeSilenceMs = 0;
|
|
845
|
-
if (segmentMs > MAX_SEGMENT_MS)
|
|
846
|
-
segmentEpoch++;
|
|
847
|
-
segment = [];
|
|
848
|
-
segmentMs = 0;
|
|
849
|
-
silenceMs = 0;
|
|
850
|
-
wakeSilenceMs = 0;
|
|
851
|
-
uploadedSamples = 0;
|
|
852
|
-
void resetHostStream();
|
|
853
|
-
}
|
|
898
|
+
if (segmentMs > MAX_SEGMENT_MS) clearWakeSegment();
|
|
854
899
|
} else if (segment.length > 0) {
|
|
900
|
+
segmentMs += durationMs;
|
|
901
|
+
segment.push(data);
|
|
855
902
|
wakeSilenceMs += durationMs;
|
|
856
|
-
if (wakeSilenceMs >= config.silenceMs)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
silenceMs = 0;
|
|
861
|
-
wakeSilenceMs = 0;
|
|
862
|
-
uploadedSamples = 0;
|
|
863
|
-
void resetHostStream();
|
|
864
|
-
}
|
|
903
|
+
if (wakeSilenceMs >= config.silenceMs) clearWakeSegment();
|
|
904
|
+
} else {
|
|
905
|
+
prePad.push(data);
|
|
906
|
+
trimPrePad();
|
|
865
907
|
}
|
|
866
908
|
} else if (rms > SPEECH_RMS) {
|
|
867
909
|
if (config.isPlaying?.()) {
|
|
@@ -910,16 +952,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
910
952
|
}
|
|
911
953
|
} else {
|
|
912
954
|
prePad.push(data);
|
|
913
|
-
|
|
914
|
-
let cut = 0;
|
|
915
|
-
for (let i = prePad.length - 1; i >= 0; i--) {
|
|
916
|
-
total += prePad[i].length / SAMPLE_RATE2 * 1e3;
|
|
917
|
-
if (total > PRE_PAD_MS) {
|
|
918
|
-
cut = i + 1;
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (cut > 0) prePad = prePad.slice(cut);
|
|
955
|
+
trimPrePad();
|
|
923
956
|
}
|
|
924
957
|
const nowMs = Date.now();
|
|
925
958
|
if (nowMs - lastPollAt >= PARTIAL_INTERVAL_MS) {
|
|
@@ -1013,6 +1046,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
1013
1046
|
segmentMs = 0;
|
|
1014
1047
|
speechMs = 0;
|
|
1015
1048
|
wakeSilenceMs = 0;
|
|
1049
|
+
wakeConsumed = false;
|
|
1016
1050
|
uploadedSamples = 0;
|
|
1017
1051
|
prePad = [];
|
|
1018
1052
|
detectChunks = [];
|
|
@@ -1098,6 +1132,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
1098
1132
|
if (!active || holdActive) return;
|
|
1099
1133
|
holdActive = true;
|
|
1100
1134
|
segmentEpoch++;
|
|
1135
|
+
wakeConsumed = false;
|
|
1101
1136
|
utteranceEndAt = null;
|
|
1102
1137
|
segment = [];
|
|
1103
1138
|
segmentMs = 0;
|
|
@@ -1119,6 +1154,7 @@ function createAsrEngine(config, sessionId) {
|
|
|
1119
1154
|
speechMs = 0;
|
|
1120
1155
|
silenceMs = 0;
|
|
1121
1156
|
wakeSilenceMs = 0;
|
|
1157
|
+
wakeConsumed = false;
|
|
1122
1158
|
speechActive = false;
|
|
1123
1159
|
prePad = [];
|
|
1124
1160
|
uploadedSamples = 0;
|
|
@@ -1337,6 +1373,7 @@ function estimateBulkDelay(mic, ref, opts = {}) {
|
|
|
1337
1373
|
var zh = {
|
|
1338
1374
|
stateVoiceMode: "\u8BED\u97F3\u6A21\u5F0F",
|
|
1339
1375
|
ttsNoticeFail: "\u6717\u8BFB\u8FDE\u63A5\u5931\u8D25\uFF1A\u6B63\u5728\u91CD\u8BD5\u2026",
|
|
1376
|
+
ttsSkipNotice: "\u6709\u4E00\u53E5\u6717\u8BFB\u5931\u8D25\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u4E91\u7AEF\u6717\u8BFB\u7F51\u7EDC\u6296\u52A8\uFF0C\u53EF\u91CD\u53D1\u8FD9\u6761\u6D88\u606F\uFF09",
|
|
1340
1377
|
enterFail: "\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u5931\u8D25",
|
|
1341
1378
|
disabled: "\u8BED\u97F3\u6A21\u5F0F\u5DF2\u7981\u7528\uFF08\u63D2\u4EF6 enabled=false\uFF09",
|
|
1342
1379
|
sendFailKept: "\u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u4FDD\u7559\u5728\u8349\u7A3F",
|
|
@@ -1444,7 +1481,7 @@ var zh = {
|
|
|
1444
1481
|
descMode: "\u4EA4\u4E92\u6A21\u5F0F\uFF08toggle \u6301\u7EED\u8046\u542C+\u9759\u97F3\u65AD\u53E5 / hold \u6309\u4F4F\u8BF4\u8BDD\uFF09",
|
|
1445
1482
|
modeToggle: "\u6301\u7EED\u8046\u542C",
|
|
1446
1483
|
modeHold: "\u6309\u4F4F\u8BF4\u8BDD",
|
|
1447
|
-
descWakeWord: "\u5524\u9192\u8BCD\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\
|
|
1484
|
+
descWakeWord: "\u5524\u9192\u8BCD\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF09\uFF1A\u8BF4\u51FA\u8BCD\u5934\u624D\u8FDB\u5165\u8BC6\u522B\uFF0C\u907F\u514D\u8BEF\u89E6\u3002\u53EF\u4E0E\u547D\u4EE4\u8FDE\u8BF4\uFF08\u300C\u4F60\u597D\u5C0FD\uFF0C\u5E2E\u6211\u67E5\u5929\u6C14\u300D\u2014\u2014\u8BCD\u5934\u4F1A\u81EA\u52A8\u5265\u6389\u3001\u4E0D\u8FDB\u6D88\u606F\uFF09\uFF1B\u6BCF\u53E5\u65AD\u53E5\u6216\u6253\u65AD\u540E\u9700\u91CD\u8BF4\uFF1B\u4EC5 toggle \u6A21\u5F0F\u751F\u6548\uFF08hold / \u624B\u52A8\u6253\u65AD\u4E0B\u4E0D\u751F\u6548\uFF09\uFF1B\u6717\u8BFB\u671F\u8BF4\u5524\u9192\u8BCD\u4E0D\u89E6\u53D1\uFF08\u6253\u65AD\u4ECD\u6309 VAD \u5F00\u53E3\u5373\u6253\u65AD\uFF09\u3002\u5339\u914D\u5E26\u5BB9\u9519\uFF08\u540C\u97F3\u5B57 / \u524D\u5BFC\u8BED\u6C14\u8BCD\uFF09\uFF0C\u5EFA\u8BAE 3-4 \u5B57\uFF1B\u975E\u4E13\u7528 KWS \u5F15\u64CE\uFF0C\u5608\u6742\u73AF\u5883\u53EF\u80FD\u5EF6\u8FDF\u6216\u8BEF\u6FC0\u6D3B",
|
|
1448
1485
|
wakePlaceholder: "\u5982\uFF1A\u4F60\u597D\u5C0FD",
|
|
1449
1486
|
settingsCardDesc: "\u6717\u8BFB\u5F15\u64CE / \u97F3\u8272 / \u8BED\u901F / \u6253\u65AD\u7075\u654F\u5EA6 / \u6253\u65AD\u65B9\u5F0F / \u56DE\u58F0\u95E8\u63A7 / \u9759\u97F3\u505C\u987F / \u7A7A\u95F2\u8D85\u65F6 / \u6A21\u578B\u955C\u50CF / \u81EA\u52A8\u53D1\u9001 / \u81EA\u52A8\u6062\u590D / \u4EA4\u4E92\u6A21\u5F0F / \u5524\u9192\u8BCD / \u5DE5\u5177\u63D0\u793A\u97F3 / \u9006\u6587\u672C\u5F52\u4E00\u5316 / \u5B57\u5E55\u5B57\u53F7 / \u5B57\u5E55\u5BBD\u5EA6 / \u77ED\u5E94\u7B54\u8BA9\u4F4D / \u8BA9\u4F4D\u7A97\u53E3",
|
|
1450
1487
|
settingsEffectiveNote: "\u6717\u8BFB\u5F15\u64CE / \u97F3\u8272 / \u8BED\u901F / \u6A21\u578B\u7CBE\u5EA6 / \u53E3\u8BED\u5316\u63D0\u793A\u8BCD / \u91CD\u8BD1 / \u5B57\u5E55\u5B57\u53F7 / \u5B57\u5E55\u5BBD\u5EA6 / \u77ED\u5E94\u7B54\u8BA9\u4F4D / \u8BA9\u4F4D\u7A97\u53E3 \u5373\u65F6\u751F\u6548\uFF1B\u9006\u6587\u672C\u5F52\u4E00\u5316 \u5373\u65F6\u751F\u6548\uFF08\u4E0B\u6B21\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u91CD\u5EFA\u6D41\u5F0F\u8BC6\u522B\u5668\uFF09\uFF1B\u5176\u4F59\uFF08\u6253\u65AD\u7075\u654F\u5EA6 / \u6253\u65AD\u65B9\u5F0F / \u56DE\u58F0\u95E8\u63A7 / \u5FEB\u6377\u952E / \u9759\u97F3 / \u7A7A\u95F2 / \u955C\u50CF / \u81EA\u52A8\u53D1\u9001 / \u81EA\u52A8\u6062\u590D / \u4EA4\u4E92\u6A21\u5F0F / \u5524\u9192\u8BCD / \u5DE5\u5177\u63D0\u793A\u97F3\uFF09\u4E0B\u6B21\u8FDB\u5165\u8BED\u97F3\u6A21\u5F0F\u65F6\u751F\u6548\u3002",
|
|
@@ -1516,6 +1553,7 @@ var zh = {
|
|
|
1516
1553
|
var en = {
|
|
1517
1554
|
stateVoiceMode: "Voice Mode",
|
|
1518
1555
|
ttsNoticeFail: "Read-aloud connection lost: retrying\u2026",
|
|
1556
|
+
ttsSkipNotice: "One sentence failed to read and was skipped (cloud TTS network hiccup \u2014 resend the message to retry)",
|
|
1519
1557
|
enterFail: "Failed to enter voice mode",
|
|
1520
1558
|
disabled: "Voice mode disabled (plugin enabled=false)",
|
|
1521
1559
|
sendFailKept: "Send failed; text kept in draft",
|
|
@@ -1621,7 +1659,7 @@ var en = {
|
|
|
1621
1659
|
descMode: "Interaction mode (toggle: continuous listen + auto-send / hold: press to talk)",
|
|
1622
1660
|
modeToggle: "Continue listen",
|
|
1623
1661
|
modeHold: "Hold to talk",
|
|
1624
|
-
descWakeWord:
|
|
1662
|
+
descWakeWord: 'Wake word (default off; e.g. "Hey D"): recognition starts only after you say it, to avoid accidental triggers. You may say it together with your command ("Hey D, check the weather" \u2014 the wake word is stripped and never sent); it must be repeated after each utterance split or barge-in; toggle mode only (inactive in hold / manual barge-in); saying it while the agent is reading does not trigger (barge-in stays VAD-based). Fault-tolerant matching (homophones / leading fillers); 3-4 characters recommended; not a dedicated KWS engine \u2014 noisy environments may delay or falsely trigger',
|
|
1625
1663
|
wakePlaceholder: "e.g. Hey D",
|
|
1626
1664
|
settingsCardDesc: "Engine / voice / rate / interrupt / barge-in / echo gate / silence / idle / model host / auto-send / auto-resume / mode / wake word / tool beep / ITN / caption font / caption width / yielding / yield window",
|
|
1627
1665
|
settingsEffectiveNote: "Engine / voice / rate / model precision / spoken format / re-transcribe / caption font / caption width / yielding / yield window apply immediately; ITN applies immediately (next time you enter voice mode the streaming recognizer is rebuilt); the rest (interrupt / barge-in / echo gate / shortcut / silence / idle / mirror / auto-send / auto-resume / mode / wake word / tool beep) apply next time you enter voice mode.",
|
|
@@ -2787,7 +2825,7 @@ var TELEMETRY_VIEW = [
|
|
|
2787
2825
|
{ stage: "first-tts-chunk", key: "telFirstChunk" },
|
|
2788
2826
|
{ stage: "first-audio-played", key: "telFirstPlayed" }
|
|
2789
2827
|
];
|
|
2790
|
-
var BUILD_TAG = "
|
|
2828
|
+
var BUILD_TAG = "466b0f3";
|
|
2791
2829
|
var TELEMETRY_FLAG = "dsh-voice-mode.telemetry";
|
|
2792
2830
|
var telemetryEnabled = typeof localStorage !== "undefined" && localStorage.getItem(TELEMETRY_FLAG) === "1";
|
|
2793
2831
|
console.log("[dsh-voice] build=" + BUILD_TAG);
|
|
@@ -2871,6 +2909,11 @@ function setLastVoiceSession(id) {
|
|
|
2871
2909
|
}
|
|
2872
2910
|
function apply(ctx) {
|
|
2873
2911
|
const bus = createVoiceBus(void 0, ctx);
|
|
2912
|
+
if (typeof document !== "undefined") {
|
|
2913
|
+
const resumeAudio = () => bus.warmAudio();
|
|
2914
|
+
document.addEventListener("pointerdown", resumeAudio, { passive: true });
|
|
2915
|
+
document.addEventListener("keydown", resumeAudio, { passive: true });
|
|
2916
|
+
}
|
|
2874
2917
|
ctx.slots.inject(
|
|
2875
2918
|
"conversation.input.right",
|
|
2876
2919
|
() => ctx.slots.register(
|
|
@@ -3044,6 +3087,7 @@ function createAudioEngine(setUi, onPlayed, onPlaybackRef, onAllPlayed) {
|
|
|
3044
3087
|
};
|
|
3045
3088
|
return {
|
|
3046
3089
|
push(frame) {
|
|
3090
|
+
warm();
|
|
3047
3091
|
if (fallback || !ctx) {
|
|
3048
3092
|
pending.push(frame);
|
|
3049
3093
|
if (fallbackAudio.paused) playFallback();
|
|
@@ -3357,6 +3401,16 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
3357
3401
|
} catch {
|
|
3358
3402
|
}
|
|
3359
3403
|
});
|
|
3404
|
+
source.addEventListener("tts-skip", (e) => {
|
|
3405
|
+
try {
|
|
3406
|
+
const p = JSON.parse(e.data);
|
|
3407
|
+
if (p.sessionId !== activeSessionId) return;
|
|
3408
|
+
ui.ttsNotice = t("ttsSkipNotice");
|
|
3409
|
+
notify();
|
|
3410
|
+
debugLog("tts-skip", { text: (p.text ?? "").slice(0, 40) });
|
|
3411
|
+
} catch {
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3360
3414
|
source.addEventListener("tts-error", (e) => {
|
|
3361
3415
|
try {
|
|
3362
3416
|
const p = JSON.parse(e.data);
|
|
@@ -3412,6 +3466,7 @@ function createVoiceBus(basePath = BASE_PATH2, ctx) {
|
|
|
3412
3466
|
}
|
|
3413
3467
|
if (frame.final) {
|
|
3414
3468
|
if (frame.chunkId !== curChunkCount) {
|
|
3469
|
+
debugLog("tts-drop-sentence", { got: frame.chunkId, have: curChunkCount, sentenceId: frame.sentenceId });
|
|
3415
3470
|
curSentenceId = null;
|
|
3416
3471
|
curChunks = [];
|
|
3417
3472
|
curBytes = 0;
|
package/lib/index.js
CHANGED
|
@@ -1131,9 +1131,12 @@ var TtsQueue = class {
|
|
|
1131
1131
|
engine;
|
|
1132
1132
|
/** TTS 全体不可达通知(每会话去重,成功后复位)。 */
|
|
1133
1133
|
onError;
|
|
1134
|
+
/** 单句合成重试耗尽被跳过通知(每句一次;此前是静默丢句——真机「回复偶尔不朗读」根因之一)。 */
|
|
1135
|
+
onSkip;
|
|
1134
1136
|
constructor(options) {
|
|
1135
1137
|
this.engine = options.engine;
|
|
1136
1138
|
this.onError = options.onError;
|
|
1139
|
+
this.onSkip = options.onSkip;
|
|
1137
1140
|
}
|
|
1138
1141
|
/** 当前引擎音频 MIME(/preview 的 Content-Type 也用它)。 */
|
|
1139
1142
|
get mime() {
|
|
@@ -1233,7 +1236,10 @@ var TtsQueue = class {
|
|
|
1233
1236
|
}
|
|
1234
1237
|
}
|
|
1235
1238
|
if (item.epoch !== q.epoch) continue;
|
|
1236
|
-
if (buf === null)
|
|
1239
|
+
if (buf === null) {
|
|
1240
|
+
this.onSkip?.(sessionId, item.text);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1237
1243
|
q.errorNotified = false;
|
|
1238
1244
|
q.backoff = 0;
|
|
1239
1245
|
const sentenceId = q.seq++;
|
|
@@ -1974,7 +1980,7 @@ function createVoiceSettingsSchema(defs) {
|
|
|
1974
1980
|
shortcut: z.string().default(d.shortcut).description("\u8FDB\u5165/\u9000\u51FA\u8BED\u97F3\u6A21\u5F0F\u7684\u5FEB\u6377\u952E\uFF08\u5F62\u5982 Ctrl+Shift+V\uFF0C\u4FEE\u9970\u952E Ctrl/Shift/Alt/Meta + \u4E00\u4E2A\u5B57\u6BCD\u952E\uFF1B\u7559\u7A7A\u7981\u7528\u5FEB\u6377\u952E\uFF0C\u7528\u9EA6\u514B\u98CE\u6309\u94AE\uFF09"),
|
|
1975
1981
|
spokenFormat: z.boolean().default(d.spokenFormat).description("\u8BED\u97F3\u4F1A\u8BDD\u6CE8\u5165\u53E3\u8BED\u5316\u63D0\u793A\u8BCD\uFF08\u53E3\u8BED\u5316\u77ED\u53E5\u3001\u4E0D\u7528 Markdown \u6392\u7248\u7B26\u53F7\uFF0C\u6717\u8BFB\u66F4\u987A\u66F4\u5FEB\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u6539\u52A8\u5373\u65F6\u751F\u6548\uFF09"),
|
|
1976
1982
|
senseVoice: z.boolean().default(d.senseVoice).description("\u5B9A\u7A3F\u7528 SenseVoice \u91CD\u8BD1\uFF08\u5E26\u6807\u70B9+\u6570\u5B57\u5F52\u4E00\u5316\u3001\u8BC6\u522B\u66F4\u51C6\uFF1B\u9ED8\u8BA4\u5F00\u3002\u5173\u95ED\u53EF\u7701 228MB \u6A21\u578B\uFF0C\u53EA\u8D70\u6D41\u5F0F\u8BC6\u522B\uFF09"),
|
|
1977
|
-
wakeWord: z.string().default(d.wakeWord).description("\u5524\u9192\u8BCD\uFF1A\u5728\u5F85\u673A\u6001\u8BF4\u51FA\u540E\u5F00\u59CB\u8BC6\u522B\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF1B\
|
|
1983
|
+
wakeWord: z.string().default(d.wakeWord).description("\u5524\u9192\u8BCD\uFF1A\u5728\u5F85\u673A\u6001\u8BF4\u51FA\u540E\u5F00\u59CB\u8BC6\u522B\uFF08\u9ED8\u8BA4\u5173\uFF1B\u5982\u300C\u4F60\u597D\u5C0FD\u300D\uFF09\u3002\u53EF\u4E0E\u547D\u4EE4\u8FDE\u8BF4\u2014\u2014\u8BCD\u5934\u81EA\u52A8\u5265\u6389\u4E0D\u8FDB\u6D88\u606F\uFF1B\u6BCF\u53E5\u65AD\u53E5/\u6253\u65AD\u540E\u9700\u91CD\u8BF4\uFF1B\u4EC5 toggle \u6A21\u5F0F\u751F\u6548\uFF08hold/\u624B\u52A8\u6253\u65AD\u4E0B\u4E0D\u751F\u6548\uFF09\uFF1B\u6717\u8BFB\u671F\u8BF4\u5524\u9192\u8BCD\u4E0D\u89E6\u53D1\uFF08\u6253\u65AD\u6309 VAD\uFF09\uFF1B\u5339\u914D\u5E26\u5BB9\u9519\uFF08\u540C\u97F3\u5B57/\u524D\u5BFC\u8BED\u6C14\u8BCD\uFF09\uFF0C\u5EFA\u8BAE 3-4 \u5B57\uFF1B\u975E\u4E13\u7528 KWS \u5F15\u64CE\uFF0C\u5608\u6742\u73AF\u5883\u53EF\u80FD\u5EF6\u8FDF\u6216\u8BEF\u6FC0\u6D3B"),
|
|
1978
1984
|
toolBeep: z.boolean().default(d.toolBeep).description('\u5DE5\u5177\u8C03\u7528\u63D0\u793A\u97F3\uFF08\u9ED8\u8BA4\u5173\uFF09\uFF1A\u5F00\u542F\u540E AI \u8C03\u7528\u5DE5\u5177\u65F6"\u6EF4"\u4E00\u58F0\uFF0C\u5173\u95ED\u5219\u5168\u7A0B\u9759\u9ED8'),
|
|
1979
1985
|
senseITN: z.boolean().default(d.senseITN).description("SenseVoice \u9006\u6587\u672C\u5F52\u4E00\u5316\uFF08\u6570\u5B57/\u65E5\u671F\u89C4\u8303\u5316\uFF0C\u9ED8\u8BA4\u5F00\uFF1B\u5173\u95ED\u540E\u8F93\u51FA\u66F4\u63A5\u8FD1\u53E3\u8BED\u539F\u6587\uFF09"),
|
|
1980
1986
|
captionFontSize: z.union([z.const(0), z.const(1), z.const(2), z.const(3)]).default(d.captionFontSize).description(
|
|
@@ -2104,7 +2110,9 @@ function apply(ctx, config) {
|
|
|
2104
2110
|
let activeKokoroModel = vset.kokoroModel;
|
|
2105
2111
|
const queue = new TtsQueue({
|
|
2106
2112
|
engine: makeEngine(engineKind),
|
|
2107
|
-
onError: (sessionId) => broadcast("tts-error", { sessionId })
|
|
2113
|
+
onError: (sessionId) => broadcast("tts-error", { sessionId }),
|
|
2114
|
+
// 单句重试耗尽被跳过:显式下行(客户端提示 + 诊断),不再静默丢句。
|
|
2115
|
+
onSkip: (sessionId, text) => broadcast("tts-skip", { sessionId, text: text.slice(0, 80) })
|
|
2108
2116
|
});
|
|
2109
2117
|
queue.updateVoice(vset.voice, vset.rate);
|
|
2110
2118
|
const unsubscribe = queue.subscribe((frame) => broadcast("audio", frame));
|
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 → 0.1.5-rc.2). · DSH 语音双工插件:流式识别入草稿、按句朗读+实时字幕、开口即打断;免 API Key、安全加固、全版本兼容(0.1.1-rc.2 起,含 0.1.5-rc.2 端到端验证)。",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "node build.mjs",
|
|
44
44
|
"prepack": "node build.mjs",
|
|
45
|
-
"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",
|
|
45
|
+
"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",
|
|
46
46
|
"verify:client": "node test/verify-client.mjs",
|
|
47
47
|
"prefetch": "node scripts/prefetch.mjs",
|
|
48
48
|
"list-voices": "node scripts/list-voices.mjs",
|