dsh-chubby-whale 1.1.63 → 1.1.64
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/CHANGELOG.md +21 -0
- package/dist/client.js +69 -8
- package/dist/host/routes/asr.js +14 -4
- package/dist/host/routes/tts.js +9 -1
- package/docs//345/217/221/345/270/203/350/247/204/350/214/203.md +5 -5
- package/docs//345/276/205/351/207/215/345/220/257/347/224/237/346/225/210.md +13 -0
- package/docs//346/250/241/345/235/227/346/216/245/345/217/243/346/270/205/345/215/225.md +2 -2
- package/docs//350/267/250/345/271/263/345/217/260/344/270/216/345/210/206/345/267/245.md +3 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## [未发布]
|
|
6
6
|
|
|
7
|
+
## [1.1.64] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
### 修复(B92 客户端朗读播放路径分身)
|
|
10
|
+
- **`pitch` 选项完全失效**:`buildTtsJobs` 既不收也不复制 `pitch`,而 `voice.ts` 拼 `/tts` 请求体时读 `job.pitch` → 永远是空串;念诗明明传了 `+15Hz`(音高从未生效)
|
|
11
|
+
- **没开麦克风却显示「在听」**:`pumpTts` 队列空时无条件 `setStatus('listening')`,而 `stopSpeaking` 是 `running ? listening : idle`;试听/说书走 `ensure()` 不开麦,念完状态就变成「在听」
|
|
12
|
+
- **纯朗读实例没有看门狗**:旧实现只在 `start()` 里装 `setInterval`,而 `ensure()` 建的实例从不 `start()` → 音频元素不播也不报错时 `ttsBusy` 永久为真,之后每句只排队(表现「朗读全哑、只能刷新页面」)
|
|
13
|
+
- **诊断计数虚高**:`ttsRequests++` 在冷却分支之前,走系统语音的段也被算成一次 `/tts` 请求
|
|
14
|
+
- **打断整句被 dup 闸门吞**:`isRepeat(minLen:4)` 对 ≥4 字整句生效 —— 说书时连说两遍「停下来吧」「不要说了」,第二声被判重复丢弃;而 `voice-bridge` 的停止正则本应能停。现在打断整句跳过 short/dup 闸门(不去放宽 `sim.ts`,避免削弱短句去重)
|
|
15
|
+
|
|
16
|
+
### 修复(B92 宿主语音路由分身)
|
|
17
|
+
- **`/asr` 解码后超限回 200**:同一路由「请求体超限」走 413、「解码后超限」却回 200,代理/日志会把超限记成识别成功;统一 413
|
|
18
|
+
- **`/asr` 缓存键缺 `format`**:同一份字节先以 `wav` 识别、再以 `webm` 上传会直接命中旧文本(实测 `fetch=1`、`cached:true`);键升 v2 并加入 format,旧条目自然失效
|
|
19
|
+
|
|
20
|
+
### 修复(主代理)
|
|
21
|
+
- **设备 192kHz 导致半速音频**:`AudioContext.sampleRate` 跟随输出设备,192k 设备上数据是 192k 而 WAV 头被夹到 96k → ASR 整段识别必错。新增 `capSampleRate`(整数倍块平均)先降采样,**头与数据同采样率**
|
|
22
|
+
- **`/tts` 静默截断**:6001 字回 HTTP 200 但引擎只收到 6000 字(听到半句、无任何信号);改为 413 `{error:'text too large', limit:6000}`,正好 6000 仍正常
|
|
23
|
+
|
|
24
|
+
### 测试
|
|
25
|
+
- 新增 `test-voice-routes.mjs`(23)、`test-voice-tts.mjs`(99),接入 `npm test`;`test-audio-sim` 补 6 条采样率断言
|
|
26
|
+
- 全链 **3799 项 0 失败**(`node tools/test-count.mjs`:52 个套件,失败 0,未识别 0)
|
|
27
|
+
|
|
7
28
|
## [1.1.63] - 2026-11-07
|
|
8
29
|
|
|
9
30
|
### 修复(B91 共享层与语音子模块分身)
|
package/dist/client.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// src/client/version.ts
|
|
28
|
-
var VERSION = "1.1.
|
|
28
|
+
var VERSION = "1.1.64";
|
|
29
29
|
|
|
30
30
|
// src/client/styles.ts
|
|
31
31
|
var W = 240;
|
|
@@ -799,6 +799,29 @@
|
|
|
799
799
|
}
|
|
800
800
|
return buf;
|
|
801
801
|
}
|
|
802
|
+
function capSampleRate(samples, sampleRate, maxRate = 96e3) {
|
|
803
|
+
const src = samples || [];
|
|
804
|
+
const n = Math.max(0, src.length >>> 0);
|
|
805
|
+
const rate = Math.round(Number(sampleRate) || 48e3);
|
|
806
|
+
const cap = Math.max(8e3, Math.round(Number(maxRate) || 96e3));
|
|
807
|
+
if (rate <= cap || n === 0) {
|
|
808
|
+
const same = src instanceof Float32Array ? src : Float32Array.from(src);
|
|
809
|
+
return { samples: same, sampleRate: rate };
|
|
810
|
+
}
|
|
811
|
+
const factor = Math.max(2, Math.ceil(rate / cap));
|
|
812
|
+
const outLen = Math.floor(n / factor);
|
|
813
|
+
const out = new Float32Array(outLen);
|
|
814
|
+
for (let i = 0; i < outLen; i++) {
|
|
815
|
+
let sum = 0;
|
|
816
|
+
const base = i * factor;
|
|
817
|
+
for (let k = 0; k < factor; k++) {
|
|
818
|
+
const v = Number(src[base + k]);
|
|
819
|
+
sum += isFinite(v) ? v : 0;
|
|
820
|
+
}
|
|
821
|
+
out[i] = sum / factor;
|
|
822
|
+
}
|
|
823
|
+
return { samples: out, sampleRate: Math.round(rate / factor) };
|
|
824
|
+
}
|
|
802
825
|
function wavBytes(seconds, sampleRate) {
|
|
803
826
|
const sec = Math.max(0, Number(seconds) || 0);
|
|
804
827
|
const sr = Math.max(8e3, Math.min(96e3, Math.round(Number(sampleRate) || 48e3)));
|
|
@@ -1007,11 +1030,14 @@
|
|
|
1007
1030
|
const n = Math.min(2, Number(buf.numberOfChannels) || 1);
|
|
1008
1031
|
for (let i = 0; i < n; i++) chans.push(buf.getChannelData(i));
|
|
1009
1032
|
const mono = downmixToMono(chans);
|
|
1010
|
-
|
|
1033
|
+
const capped = capSampleRate(mono, buf.sampleRate);
|
|
1034
|
+
const samples = capped.samples;
|
|
1035
|
+
const rate = capped.sampleRate;
|
|
1036
|
+
if (wavBytes(samples.length / rate, rate) > maxWavBytes) {
|
|
1011
1037
|
done(null);
|
|
1012
1038
|
return;
|
|
1013
1039
|
}
|
|
1014
|
-
done(new Blob([encodeWavPcm16(
|
|
1040
|
+
done(new Blob([encodeWavPcm16(samples, rate)], { type: "audio/wav" }));
|
|
1015
1041
|
} catch (e) {
|
|
1016
1042
|
done(null);
|
|
1017
1043
|
}
|
|
@@ -1157,6 +1183,31 @@
|
|
|
1157
1183
|
}
|
|
1158
1184
|
|
|
1159
1185
|
// src/client/voice/echo-filter.ts
|
|
1186
|
+
var SINGLE_INTERRUPT = /^[停别不]$/;
|
|
1187
|
+
var INTERRUPT_PHRASES = [
|
|
1188
|
+
"停下",
|
|
1189
|
+
"停下来",
|
|
1190
|
+
"停下来吧",
|
|
1191
|
+
"停一下",
|
|
1192
|
+
"停一停",
|
|
1193
|
+
"停吧",
|
|
1194
|
+
"别说了",
|
|
1195
|
+
"别讲了",
|
|
1196
|
+
"别念了",
|
|
1197
|
+
"别吵",
|
|
1198
|
+
"别说下去了",
|
|
1199
|
+
"不要说了",
|
|
1200
|
+
"不用说了",
|
|
1201
|
+
"闭嘴",
|
|
1202
|
+
"安静",
|
|
1203
|
+
"打住",
|
|
1204
|
+
"够了",
|
|
1205
|
+
"暂停",
|
|
1206
|
+
"算了"
|
|
1207
|
+
];
|
|
1208
|
+
function isInterruptCommand(plain) {
|
|
1209
|
+
return SINGLE_INTERRUPT.test(plain) || INTERRUPT_PHRASES.indexOf(plain) >= 0;
|
|
1210
|
+
}
|
|
1160
1211
|
function createEchoFilter() {
|
|
1161
1212
|
const spokenRecent = [];
|
|
1162
1213
|
const recentHeard = [];
|
|
@@ -1178,7 +1229,8 @@
|
|
|
1178
1229
|
}
|
|
1179
1230
|
const plain = normalizePlain(text).toLowerCase();
|
|
1180
1231
|
const plainKey = echoKey(text).toLowerCase();
|
|
1181
|
-
const interrupt =
|
|
1232
|
+
const interrupt = SINGLE_INTERRUPT.test(plain);
|
|
1233
|
+
const stopCmd = isInterruptCommand(plain);
|
|
1182
1234
|
if (plain.length < 2 && !interrupt) return { ok: false, reason: "trivial" };
|
|
1183
1235
|
if (!interrupt && isNoiseText(plain)) return { ok: false, reason: "noise" };
|
|
1184
1236
|
if (/^本鱼在听/.test(plain)) return { ok: false, reason: "self" };
|
|
@@ -1198,8 +1250,8 @@
|
|
|
1198
1250
|
if (plain.length >= 6 && target.indexOf(plain.slice(0, 6)) >= 0) return { ok: false, reason: "echo" };
|
|
1199
1251
|
if (plain.length >= 3 && now - s.at < 6e3 && target.indexOf(plain) >= 0) return { ok: false, reason: "echo" };
|
|
1200
1252
|
}
|
|
1201
|
-
if (isShortEchoFragment(plain, hadRecentSpeech)) return { ok: false, reason: "short" };
|
|
1202
|
-
if (isRepeat(plain, { recent: recentHeard, windowMs: 8e3, threshold: 0.9, minLen: 4, now })) {
|
|
1253
|
+
if (!stopCmd && isShortEchoFragment(plain, hadRecentSpeech)) return { ok: false, reason: "short" };
|
|
1254
|
+
if (!stopCmd && isRepeat(plain, { recent: recentHeard, windowMs: 8e3, threshold: 0.9, minLen: 4, now })) {
|
|
1203
1255
|
return { ok: false, reason: "dup" };
|
|
1204
1256
|
}
|
|
1205
1257
|
recentHeard.push({ text, at: now });
|
|
@@ -1414,11 +1466,13 @@
|
|
|
1414
1466
|
}
|
|
1415
1467
|
function buildTtsJobs(segs, opts) {
|
|
1416
1468
|
const out = [];
|
|
1469
|
+
const pitch = opts.pitch ? String(opts.pitch) : void 0;
|
|
1417
1470
|
for (let i = 0; i < segs.length; i++) {
|
|
1418
1471
|
out.push({
|
|
1419
1472
|
seq: opts.nextSeq + i,
|
|
1420
1473
|
text: segs[i],
|
|
1421
1474
|
rate: opts.rate,
|
|
1475
|
+
pitch,
|
|
1422
1476
|
narration: opts.narration,
|
|
1423
1477
|
attempts: 0,
|
|
1424
1478
|
strict: opts.strict,
|
|
@@ -1992,6 +2046,10 @@
|
|
|
1992
2046
|
} catch (e) {
|
|
1993
2047
|
}
|
|
1994
2048
|
}
|
|
2049
|
+
function ensureTtsWatch() {
|
|
2050
|
+
if (ttsWatch) return;
|
|
2051
|
+
ttsWatch = setInterval(ttsWatchdog, TTS_WATCH_MS);
|
|
2052
|
+
}
|
|
1995
2053
|
function ttsWatchdog() {
|
|
1996
2054
|
try {
|
|
1997
2055
|
const audioPlaying = !!(ttsAudio && !ttsAudio.paused && !ttsAudio.ended && ttsAudio.readyState > 0);
|
|
@@ -2147,6 +2205,7 @@
|
|
|
2147
2205
|
}
|
|
2148
2206
|
}
|
|
2149
2207
|
function pumpTts(force) {
|
|
2208
|
+
ensureTtsWatch();
|
|
2150
2209
|
if (ttsBusy) return;
|
|
2151
2210
|
if (ttsNextRetryAt > 0) {
|
|
2152
2211
|
if (!force && Date.now() < ttsNextRetryAt) return;
|
|
@@ -2154,7 +2213,7 @@
|
|
|
2154
2213
|
}
|
|
2155
2214
|
const job = ttsQ.shift();
|
|
2156
2215
|
if (!job) {
|
|
2157
|
-
setStatus("listening");
|
|
2216
|
+
setStatus(running ? "listening" : "idle");
|
|
2158
2217
|
return;
|
|
2159
2218
|
}
|
|
2160
2219
|
ttsBusy = true;
|
|
@@ -2164,7 +2223,6 @@
|
|
|
2164
2223
|
const myToken = ++speakToken;
|
|
2165
2224
|
job.token = myToken;
|
|
2166
2225
|
setStatus("speaking");
|
|
2167
|
-
stats.ttsRequests++;
|
|
2168
2226
|
if (ttsInCooldown() && job.attempts <= 0) {
|
|
2169
2227
|
ttsSynthAt = 0;
|
|
2170
2228
|
ttsDiag(job, true, "cooldown");
|
|
@@ -2175,6 +2233,7 @@
|
|
|
2175
2233
|
ttsBrowserFallback(job, myToken);
|
|
2176
2234
|
return;
|
|
2177
2235
|
}
|
|
2236
|
+
stats.ttsRequests++;
|
|
2178
2237
|
const cached = ttsPrefetch.get(job.seq);
|
|
2179
2238
|
const cur = cached || synth(job);
|
|
2180
2239
|
if (cached) {
|
|
@@ -2490,6 +2549,8 @@
|
|
|
2490
2549
|
echoFilter.noteSpoken(raw);
|
|
2491
2550
|
const jobs = buildTtsJobs(segs, {
|
|
2492
2551
|
rate,
|
|
2552
|
+
// 音高一路传到 /tts 的请求体:不在这里交给队列任务,宿主永远只收到空串(B92 修)
|
|
2553
|
+
pitch: o && o.pitch,
|
|
2493
2554
|
narration,
|
|
2494
2555
|
strict,
|
|
2495
2556
|
nextSeq: ttsSeq + 1,
|
package/dist/host/routes/asr.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* host/routes/asr.ts —— 语音识别路由(硅基流动 Qwen3-ASR)
|
|
3
3
|
* POST JSON { audio: base64, format: 'webm'|'wav'|'mp3' } → { text }
|
|
4
4
|
* Key 由入口注入(getKey),路由不再自己读 config.json / 环境变量。
|
|
5
|
-
* 体积:请求体(base64)上限 12MB → 413;解码后音频上限 8MB
|
|
5
|
+
* 体积:请求体(base64)上限 12MB → 413;解码后音频上限 8MB → 同样 413(口径统一,2026-11-07)
|
|
6
|
+
* 外呼超时 60 秒
|
|
6
7
|
*/
|
|
7
8
|
import { hashBytes } from '../../shared/hash.js';
|
|
8
9
|
import { isInterruptWord, isNoiseText } from '../../shared/noise.js';
|
|
@@ -49,9 +50,13 @@ export function createAsrHandler(opts) {
|
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
const buf = Buffer.from(audioB64, 'base64');
|
|
52
|
-
// 解码后再判音频上限(8MB
|
|
53
|
+
// 解码后再判音频上限(8MB)。
|
|
54
|
+
// 必须与上面「请求体超限」同一口径回 413:同一个路由对「太大」给出 200/413 两种状态码,
|
|
55
|
+
// 客户端虽只看 ok 字段、表现不变,但代理、网关、访问日志只认状态码——200 会被记成
|
|
56
|
+
// 「识别成功」,排障时查不出来(2026-11-07 B92 实测:8MB+1 字节走的正是这条分支)。
|
|
53
57
|
if (buf.length > ASR_MAX_AUDIO) {
|
|
54
|
-
|
|
58
|
+
console.error('[asr] AUDIO-TOO-LARGE ' + buf.length + ' bytes > ' + ASR_MAX_AUDIO);
|
|
59
|
+
sendJson(res, { ok: false, error: 'audio too large', text: '' }, 413);
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
62
|
const key = opts.getKey();
|
|
@@ -61,7 +66,12 @@ export function createAsrHandler(opts) {
|
|
|
61
66
|
}
|
|
62
67
|
// 用完整音频字节做 key(只取前 4000 字符容易撞 key)
|
|
63
68
|
// key 里带模型名:换 ASR 模型后不会命中旧文本
|
|
64
|
-
|
|
69
|
+
// key 里还必须带 format:同一份字节在 multipart 里声明的 Content-Type / filename 不同,
|
|
70
|
+
// 上游的解码方式就不同。原来只按「模型 + 字节」缓存,实测同一份字节先用 format=wav
|
|
71
|
+
// 识别成功,再以 format=webm 上传会直接命中上一次的文本(cached:true)——A 容器的结果
|
|
72
|
+
// 顶了 B 容器的请求。v1 前缀的旧条目没有 format,刻意升到 v2 让它们自然失效(7 天 TTL 内
|
|
73
|
+
// 最多少几次命中,不会读出别人的文本)。
|
|
74
|
+
const ck = hashBytes(Buffer.concat([Buffer.from('v2|' + ASR_MODEL + '|' + format + '|'), buf]));
|
|
65
75
|
if (opts.cache) {
|
|
66
76
|
const hit = await opts.cache.getJson(ck, 'json', ASR_CACHE_MAX_AGE);
|
|
67
77
|
if (hit && typeof hit.text === 'string') {
|
package/dist/host/routes/tts.js
CHANGED
|
@@ -54,7 +54,15 @@ export function createTtsHandler(opts) {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
const body = parsed.body;
|
|
57
|
-
const
|
|
57
|
+
const rawText = typeof body?.text === 'string' ? body.text.trim() : '';
|
|
58
|
+
// 超长不再静默截断(2026-11-07 B92 审计):原来 slice(0, MAX_TEXT) 会回 200,
|
|
59
|
+
// 但引擎只收到前 6000 字 —— 调用方听到的是半句话、却没有任何信号。
|
|
60
|
+
// 客户端本来就按句切分后再请求,这里超限基本只可能是外部直调:明确回 413 更诚实。
|
|
61
|
+
if (rawText.length > MAX_TEXT) {
|
|
62
|
+
sendJson(res, { ok: false, error: 'text too large', limit: MAX_TEXT }, 413);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const msg = rawText;
|
|
58
66
|
const voiceIn = typeof body?.voice === 'string' ? body.voice : '';
|
|
59
67
|
const isZh = /[\u4e00-\u9fa5]/.test(msg);
|
|
60
68
|
let voice = (voiceIn && /^[a-zA-Z0-9-]+$/.test(voiceIn)) ? voiceIn : '';
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
| 项 | 值 |
|
|
14
14
|
| --- | --- |
|
|
15
|
-
| `package.json` version | 1.1.
|
|
16
|
-
| `src/client/version.ts` VERSION | 1.1.
|
|
17
|
-
| 最近 Gitee tag | `v1.1.
|
|
15
|
+
| `package.json` version | 1.1.64 |
|
|
16
|
+
| `src/client/version.ts` VERSION | 1.1.64(由 `scripts/gen-version.mjs` 生成) |
|
|
17
|
+
| 最近 Gitee tag | `v1.1.64`(见 `git tag --list`) |
|
|
18
18
|
| Gitee `main` HEAD | `git log -1` 实读 |
|
|
19
19
|
| GitHub `main` | 发布后 `node tools/gh-verify.mjs v1.1.62` 核对(脚本按 tag 名推期望版本) |
|
|
20
|
-
| npm latest | **1.1.
|
|
20
|
+
| npm latest | **1.1.64**(发布后轮询确认) |
|
|
21
21
|
| README 版本 | 无硬编码版本号 |
|
|
22
|
-
| `npm test` | **
|
|
22
|
+
| `npm test` | **3799 项**(52 个套件,`node tools/test-count.mjs` 核准、失败 0) |
|
|
23
23
|
| `npm pack --dry-run` | **129 个文件 / 17.1 MB**(包);`data/cache`、运行数据、`config.json`、`src/`、`scripts/`、`tools/` 均不进包 |
|
|
24
24
|
## 一、版本号规则
|
|
25
25
|
|
|
@@ -355,3 +355,16 @@ Measure-Command { Invoke-RestMethod -Method Post -Uri http://127.0.0.1:3080/api/
|
|
|
355
355
|
| 4 | 弹琴中提问会同时静琴 | `stopTts()` 原来不停琴 | 弹琴时说一句话:琴声与朗读不叠 |
|
|
356
356
|
| 5 | TTS/ASR 超时现在包含响应体读取 | 以前只到响应头,慢网会永久 pending | 弱网下朗读失败会走降级/重试,而不是卡住 |
|
|
357
357
|
| 6 | 诗名索引失败后会自动重试 | 原来本页永不再试 | 服务端文件缺失后补上,60s 内不刷新也能点名诗 |
|
|
358
|
+
|
|
359
|
+
## 二十、2026-11-07 凌晨批次(B92:1.1.64)待重启核对
|
|
360
|
+
|
|
361
|
+
| # | 改动 | 为什么 | 怎么验证 |
|
|
362
|
+
|---|------|--------|----------|
|
|
363
|
+
| 1 | 192kHz 设备先降采样 | 头写 96k、数据 192k = 半速,ASR 必错 | 高采样率声卡设备上说一句话:识别应正常(不出现整段错乱) |
|
|
364
|
+
| 2 | `/asr` 超限统一 413 | 解码后超限原回 200,监控会记成成功 | 传超长音频:应 413 而不是「成功但空文本」 |
|
|
365
|
+
| 3 | `/asr` 缓存键含 format | 换格式会命中旧格式的识别文本 | 同段音频换格式上传:文本应重新识别 |
|
|
366
|
+
| 4 | `/tts` 超长回 413 | 6001 字静默截断成 6000,听到半句 | 外部直调超长文本:应 413 `text too large` |
|
|
367
|
+
| 5 | 念诗的 `pitch` 真正生效 | 音高选项此前被整段丢弃 | 念诗 vs 普通朗读:音高应不同 |
|
|
368
|
+
| 6 | 纯朗读后状态回 idle | 没开麦时念完显示「在听」 | 关待命→让它读一段:读完状态应回 idle,不是「在听」 |
|
|
369
|
+
| 7 | 纯朗读实例也有看门狗 | 音频不播不报错时永久 `ttsBusy` → 朗读全哑 | 长时间朗读后仍能继续朗读(不必刷新) |
|
|
370
|
+
| 8 | 连说两遍「停下来吧」也能停 | ≥4 字打断句被 dup 闸门吞 | 说书中连说两次「停下来吧」:第二声也应停 |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> 架构宪章:**模块只对外暴露这些导出,外部不得读取模块内部实现**。
|
|
6
6
|
> 新增/修改导出 = 接口变更,必须同步调用点并在提交信息里写明。
|
|
7
7
|
|
|
8
|
-
> 统计:119 个模块文件,
|
|
8
|
+
> 统计:119 个模块文件,718 个导出项。
|
|
9
9
|
|
|
10
10
|
## 宿主(Node)(`src/host/`)
|
|
11
11
|
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
| `actions/voice.ts` | `registerVoiceActions` |
|
|
83
83
|
| `anim.ts` | `AnimState`、`EYE_L`、`EYE_R`、`MOUTH`、`createAnimator` |
|
|
84
84
|
| `ape.ts` | `APE_DEFAULTS`、`Ape`、`ApeParams`、`ApeStep`、`simulateSegment` |
|
|
85
|
-
| `audio-codec.ts` | `HostAsrFormat`、`RECORDER_MIME_CANDIDATES`、`RecFormat`、`RecorderPick`、`WAV_HEADER_BYTES`、`downmixToMono`、`encodeWavPcm16`、`formatFromMime`、`hostAsrFormat`、`pickRecorderMime`、`wavBytes` |
|
|
85
|
+
| `audio-codec.ts` | `HostAsrFormat`、`RECORDER_MIME_CANDIDATES`、`RecFormat`、`RecorderPick`、`WAV_HEADER_BYTES`、`capSampleRate`、`downmixToMono`、`encodeWavPcm16`、`formatFromMime`、`hostAsrFormat`、`pickRecorderMime`、`wavBytes` |
|
|
86
86
|
| `browser-tts.ts` | `browserVoiceUsable`、`speakViaBrowser`、`watchdogMs` |
|
|
87
87
|
| `bubble.ts` | `BUBBLE_CSS`、`Bubble`、`BubbleOptions`、`STATUS_TEXTS`、`createBubble`、`ensureBubbleStyle` |
|
|
88
88
|
| `busy.ts` | `BUSY_STALE_MS`、`BusyLock`、`Epoch`、`createBusyLock`、`createEpoch` |
|
|
@@ -142,3 +142,6 @@
|
|
|
142
142
|
| B91b | 2026-11-07 | 共享层分身:`deepFreeze` 漏冻子树与 Map/Set/symbol / 噪声先 NFKC 去零宽补全角标点 / 去 6 字重复上限 / 音色白名单大小写不敏感 / 硬超时覆盖响应体;新增 `test-shared-voice.mjs`(89) | 双 tsc 0 / 3671 项 0 失败 | 见 git |
|
|
143
143
|
| B91c | 2026-11-07 | 音频/模拟分身:`sim.isRepeat` 完全相等分支补 `minLen`(第二声「停」不再被吞)/ `melody` resume 1200ms 幂等看门狗;新增 `test-audio-sim.mjs`(169) | 双 tsc 0 / 3671 项 0 失败 | 见 git |
|
|
144
144
|
| B91d | 2026-11-07 | 主代理:`stopTts()` 补 `melody.stop()`(P0 打断同时静琴,扬声器互斥补全另一半) | 双 tsc 0 / 3671 项 0 失败 | 见 git |
|
|
145
|
+
| B92a | 2026-11-07 | 主代理:`capSampleRate` 修「192kHz 设备半速音频」(头与数据同采样率)/ `/tts` 超长改 413(不再静默截断);`test-audio-sim` +6、`test-voice-routes` +2 | 双 tsc 0 / 3799 项 0 失败 | 见 git |
|
|
146
|
+
| B92b | 2026-11-07 | 宿主语音路由分身:`/asr` 解码后超限 200→413 / 缓存键补 `format`(v1→v2);新增 `test-voice-routes.mjs`(23) | 双 tsc 0 / 3799 项 0 失败 | 见 git |
|
|
147
|
+
| B92c | 2026-11-07 | 朗读播放路径分身:`pitch` 丢失 / 没开麦报「在听」/ 纯朗读无看门狗 / `ttsRequests` 虚高 / 打断整句被 dup 吞;新增 `test-voice-tts.mjs`(99) | 双 tsc 0 / 3799 项 0 失败 | 见 git |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-chubby-whale",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.64",
|
|
4
4
|
"description": "DSH 网页版桌面宠物「肥鲸」——蓝发鲸鱼娘,固定在 harness 页面右下角:会眨眼、能拖拽、点击可聊天,支持语音对话、念诗说书、饭点提醒。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "node scripts/gen-version.mjs && node scripts/check-bom.mjs && tsc -p tsconfig.json && tsc -p tsconfig.client.json && esbuild src/client.ts --bundle --format=iife --external:react --charset=utf8 --outfile=dist/client.js",
|
|
76
76
|
"prepublishOnly": "npm run build",
|
|
77
|
-
"test": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/check-imports.mjs && node scripts/gen-module-index.mjs --check && node scripts/check-boundaries.mjs && node scripts/check-diag-keys.mjs && node scripts/check-text.mjs && node scripts/test.mjs && node scripts/test-chat-limits.mjs && node scripts/test-stream-ledger.mjs && node scripts/test-story-security.mjs && node scripts/test-behavior-regressions.mjs && node scripts/verify-voice.mjs && node scripts/smoke-client.mjs && node scripts/ape-bench.mjs && node scripts/smoke-host.mjs && node scripts/test-voice-queue.mjs && node scripts/test-tts-engine.mjs && node scripts/test-data-layer.mjs && node scripts/test-repos.mjs && node scripts/test-personality.mjs && node scripts/test-story-player.mjs && node scripts/test-chat-routes.mjs && node scripts/test-menu-actions.mjs && node scripts/test-story-client.mjs && node scripts/test-story-auto.mjs && node scripts/test-content-routes.mjs && node scripts/test-dataport-poem.mjs && node scripts/test-render-layer.mjs && node scripts/test-store-cache.mjs && node scripts/test-actions-rest.mjs && node scripts/test-misc-routes.mjs && node scripts/test-chat-client.mjs && node scripts/test-prefs-config.mjs && node scripts/test-media-reminders.mjs && node scripts/test-image-reader.mjs && node scripts/test-boundaries-guard.mjs && node scripts/test-assembly.mjs && node scripts/test-asr-concurrency.mjs && node scripts/test-host-http.mjs && node scripts/test-entry-narration.mjs && node scripts/test-poem-narration.mjs && node scripts/test-multidrop.mjs && node scripts/test-utils-pure.mjs && node scripts/test-net-client.mjs && node scripts/test-persona-cloud.mjs && node scripts/test-client-glue.mjs && node scripts/test-poem-titles-retry.mjs && node scripts/test-shared-voice.mjs && node scripts/test-audio-sim.mjs && node scripts/test-voice-bridge.mjs && node scripts/platform-check.mjs",
|
|
77
|
+
"test": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/check-imports.mjs && node scripts/gen-module-index.mjs --check && node scripts/check-boundaries.mjs && node scripts/check-diag-keys.mjs && node scripts/check-text.mjs && node scripts/test.mjs && node scripts/test-chat-limits.mjs && node scripts/test-stream-ledger.mjs && node scripts/test-story-security.mjs && node scripts/test-behavior-regressions.mjs && node scripts/verify-voice.mjs && node scripts/smoke-client.mjs && node scripts/ape-bench.mjs && node scripts/smoke-host.mjs && node scripts/test-voice-queue.mjs && node scripts/test-tts-engine.mjs && node scripts/test-data-layer.mjs && node scripts/test-repos.mjs && node scripts/test-personality.mjs && node scripts/test-story-player.mjs && node scripts/test-chat-routes.mjs && node scripts/test-menu-actions.mjs && node scripts/test-story-client.mjs && node scripts/test-story-auto.mjs && node scripts/test-content-routes.mjs && node scripts/test-dataport-poem.mjs && node scripts/test-render-layer.mjs && node scripts/test-store-cache.mjs && node scripts/test-actions-rest.mjs && node scripts/test-misc-routes.mjs && node scripts/test-chat-client.mjs && node scripts/test-prefs-config.mjs && node scripts/test-media-reminders.mjs && node scripts/test-image-reader.mjs && node scripts/test-boundaries-guard.mjs && node scripts/test-assembly.mjs && node scripts/test-asr-concurrency.mjs && node scripts/test-host-http.mjs && node scripts/test-entry-narration.mjs && node scripts/test-poem-narration.mjs && node scripts/test-multidrop.mjs && node scripts/test-utils-pure.mjs && node scripts/test-net-client.mjs && node scripts/test-persona-cloud.mjs && node scripts/test-client-glue.mjs && node scripts/test-poem-titles-retry.mjs && node scripts/test-shared-voice.mjs && node scripts/test-audio-sim.mjs && node scripts/test-voice-routes.mjs && node scripts/test-voice-tts.mjs && node scripts/test-voice-bridge.mjs && node scripts/platform-check.mjs",
|
|
78
78
|
"version:check": "node tools/version-check.mjs",
|
|
79
79
|
"typecheck": "node scripts/gen-version.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
80
80
|
"smoke:client": "node scripts/smoke-client.mjs",
|