dsh-boot-suite 2.13.12 → 2.13.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/CHANGELOG.md +67 -0
- package/README.md +3 -1
- package/lib/assets/voices/f/line-1.mp3 +0 -0
- package/lib/assets/voices/f/line-2.mp3 +0 -0
- package/lib/assets/voices/f/line-3.mp3 +0 -0
- package/lib/assets/voices/f/line-4.mp3 +0 -0
- package/lib/assets/voices/f/line-5.mp3 +0 -0
- package/lib/assets/voices/f/line-6.mp3 +0 -0
- package/lib/assets/voices/g/line-1.mp3 +0 -0
- package/lib/assets/voices/g/line-2.mp3 +0 -0
- package/lib/assets/voices/g/line-3.mp3 +0 -0
- package/lib/assets/voices/g/line-4.mp3 +0 -0
- package/lib/assets/voices/g/line-5.mp3 +0 -0
- package/lib/assets/voices/g/line-6.mp3 +0 -0
- package/lib/assets/voices/h/line-1.mp3 +0 -0
- package/lib/assets/voices/h/line-2.mp3 +0 -0
- package/lib/assets/voices/h/line-3.mp3 +0 -0
- package/lib/assets/voices/h/line-4.mp3 +0 -0
- package/lib/assets/voices/h/line-5.mp3 +0 -0
- package/lib/assets/voices/h/line-6.mp3 +0 -0
- package/lib/assets/voices/i/line-1.mp3 +0 -0
- package/lib/assets/voices/i/line-2.mp3 +0 -0
- package/lib/assets/voices/i/line-3.mp3 +0 -0
- package/lib/assets/voices/i/line-4.mp3 +0 -0
- package/lib/assets/voices/i/line-5.mp3 +0 -0
- package/lib/assets/voices/i/line-6.mp3 +0 -0
- package/lib/assets/voices/j/line-1.mp3 +0 -0
- package/lib/assets/voices/j/line-2.mp3 +0 -0
- package/lib/assets/voices/j/line-3.mp3 +0 -0
- package/lib/assets/voices/j/line-4.mp3 +0 -0
- package/lib/assets/voices/j/line-5.mp3 +0 -0
- package/lib/assets/voices/j/line-6.mp3 +0 -0
- package/lib/assets/voices/k/line-1.mp3 +0 -0
- package/lib/assets/voices/k/line-2.mp3 +0 -0
- package/lib/assets/voices/k/line-3.mp3 +0 -0
- package/lib/assets/voices/k/line-4.mp3 +0 -0
- package/lib/assets/voices/k/line-5.mp3 +0 -0
- package/lib/assets/voices/k/line-6.mp3 +0 -0
- package/lib/assets/voices/manifest.json +32 -0
- package/lib/client.js +100 -54
- package/lib/host-startup.js +48 -0
- package/lib/host-wallpaper.js +8 -2
- package/lib/splash.js +73 -23
- package/package.json +2 -2
- package/test/smoke-client.mjs +27 -4
- package/test/smoke-host.mjs +23 -4
- package/test/smoke-package.mjs +4 -1
- package/test/smoke-splash.mjs +8 -1
- package/test/smoke-wallpaper-client.mjs +1 -1
- package/test/smoke-wallpaper-host.mjs +7 -2
- package/lib/assets/line-1.mp3 +0 -0
- package/lib/assets/line-2.mp3 +0 -0
- package/lib/assets/line-3.mp3 +0 -0
- package/lib/assets/line-4.mp3 +0 -0
- package/lib/assets/line-5.mp3 +0 -0
- package/lib/assets/line-6.mp3 +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
+
## [2.13.19] — 2026-10-30
|
|
2
|
+
|
|
3
|
+
### 变更
|
|
4
|
+
- **对外音色精简**:英文只保留音色F(默认),日文保留 5 套(G–K);设置页重排并改名为「英文音色F(默认)」+「日文音色1–5」。本地被撤下的英文音色 A–E 已移到 `~/.dsh/startup-voices/旧按音频-暂不使用`,没有删除。
|
|
5
|
+
- **音色F 换回最早那套原始干音**:用 dry-01..05 + dry-06-原版 重做 F 的 6 句,停顿/时长/响度逐句复刻老版本(line-1 3.670s、line-2 3.250s、line-3 1.440s、line-4 1.090s、line-5 2.750s、line-6 4.302s),老用户听感不变。
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **终幕欢迎语进得太早**:05→06 的过场和声钟还没响完,「Welcome to DeepSeek Harness」就开口了。现在默认等 550ms(实测最顺)再进最后一句;设置页「语音与音效」新增「终幕语音延迟」滑块(0–3000ms,50ms 步进),可边预览边微调(滑杆属临时调试项,后续版本会删掉)。
|
|
9
|
+
- 默认配音改为「音色F(英文)+ 机器人效果」,贴近旧版听感;用户可在设置里改回任意音色/无效果。
|
|
10
|
+
- **音色 H / I 的 line-2 尾音**:日文「ください」的「さい」之前收得太早;按实际波形能量重切这两条(只改 H、I 各一条),其余音色不动。
|
|
11
|
+
- 版本号 2.13.18 → 2.13.19;client BUILD `cw-42`。
|
|
12
|
+
|
|
13
|
+
## [2.13.17] — 2026-10-30
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
- **11 套音色全部重新切分**:之前第 2 句起开头会被切掉一点(软辅音/气声被当成静音,日语「ユーザー」的 you 音不完整);现在句间切点取静音段中点、按真实开口/收尾各留 120ms,A–K(英/日)全部重切并已试听确认。
|
|
17
|
+
- **启动动画完整没声音**:音色包路由注册成了 `/dsh-startup/voice/`(尾部多一个斜杠),真实 DSH 的 prefix 路由匹配不到 → 每句配音都拿不到素材、静音。改为 `/dsh-startup/voice` 并兼容旧地址。
|
|
18
|
+
- **丢图目录显示「?」、打开图片文件夹按钮没反应**:新装/老用户目录不存在时面板拿到空 dropDir;现在插件启动时自动创建 `~/.dsh/chat-wallpaper`,`/images` 也兜底创建;打开按钮会回显成功/失败。
|
|
19
|
+
|
|
20
|
+
### 新增
|
|
21
|
+
- **聊天背景「色彩模式」**:彩色(默认,不调色)/ 黑白(原版素材,不染色)。黑白模式会关掉主题色纱,适合本身就是黑白的动态壁纸,避免误以为插件把画面染灰了。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [2.13.15] — 2026-10-30
|
|
26
|
+
|
|
27
|
+
### 修复
|
|
28
|
+
- **「立即预览」不认未保存的修改**:以前预览只灌宿主已保存的配置,选完「配音音色 / 声音效果」直接点预览,跑的还是旧配置(例如选日文音色仍播英文内置包)。现在预览会把当前面板里的修改一并带上;要永久生效仍要点「保存设置」。
|
|
29
|
+
- 版本号与构建号更新(client BUILD cw-38)。
|
|
30
|
+
|
|
31
|
+
## [2.13.16] — 2026-10-30
|
|
32
|
+
|
|
33
|
+
本次把旧的 TTS 配音链路彻底退役,配音只保留「音色包 + 声音效果」:
|
|
34
|
+
|
|
35
|
+
- 删除设置页「语音来源」「音色」「播报语言」「语速/音调」等旧 TTS 选项,只保留「配音音色」和「声音效果」。
|
|
36
|
+
- 删除内置 TTS 生成的旧配音素材(line-1..6、dry-line-1..5),默认音色改为音色 A;旧配置里的 builtin 自动迁移为 A。
|
|
37
|
+
- 配音素材拿不到时改为静音,不再回落系统 TTS(避免旧机械音盖掉新音色)。
|
|
38
|
+
- 11 套干音(A–F 英文、G–K 日文)+ 5 种声音效果保持可用。
|
|
39
|
+
|
|
40
|
+
## [2.13.14] — 2026-10-30
|
|
41
|
+
|
|
42
|
+
### 新增:11 套配音音色 + 5 种声音效果
|
|
43
|
+
- 内置新增 A–K 共 11 套完整配音(A–F 英文、G–K 日文),每套 6 句;设置页「配音音色」里直接选。
|
|
44
|
+
- 「声音效果」可再叠 AI 机械音 / 机器人 / 无线电 / 老电视 / 低沉,或选「无」保留干净人声;与模板编辑器同一套参数,实时处理、不改素材文件。
|
|
45
|
+
- 模板自带的配音不受全局音色/效果影响;缺素材仍自动回落 TTS。
|
|
46
|
+
- 素材已统一做响度归一(I=-18),切换音色不会忽大忽小。
|
|
47
|
+
|
|
48
|
+
### 构建
|
|
49
|
+
- 版本号 2.13.13 → 2.13.14;client BUILD `cw-37`;全套测试通过。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
## [2.13.13] — 2026-10-29
|
|
53
|
+
|
|
54
|
+
### 新增:第一幕配音可选「处理版 / 原声」
|
|
55
|
+
- 内置 5 句英文真声此前只有经过效果处理的版本(AI 机械音)。新增一套原声干音素材 `lib/assets/dry-line-1..5.mp3`——与处理版同一时长、同一前后留白,只换文件,不破坏动画节奏。
|
|
56
|
+
- 设置页「语音与音效」新增「配音效果」:**处理版(AI 机械音,默认)** / **原声(不处理)**。
|
|
57
|
+
- 只影响内置真声素材;模板自带的配音、以及选择「系统 TTS」时都不受影响;默认仍为处理版,老用户升级后听感不变。
|
|
58
|
+
|
|
59
|
+
### 改进:壁纸「刷新」给出明确反馈
|
|
60
|
+
- 之前点「刷新」没有任何提示,看起来像没反应。现在按钮会变成「刷新中…」,完成后显示「已重新读取:图片 N 张 / 视频 N 个」。
|
|
61
|
+
- 图片为空时的提示补上「只扫描这一层,子文件夹里的不算;支持 jpg/png/webp/gif/avif/bmp」,直接对应“图放进去了但不显示”的常见原因。
|
|
62
|
+
|
|
63
|
+
### 构建
|
|
64
|
+
- 版本号 2.13.12 → 2.13.13;client BUILD `cw-36`;全套测试通过。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
1
68
|
## [2.13.12] — 2026-10-29
|
|
2
69
|
|
|
3
70
|
### 修复:更新时「浏览器下载监听」过早超时、找不到下载文件
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ DeepSeek Harness(DSH)冷启动时会先播六幕动画,播完进聊天,
|
|
|
31
31
|
| 入口 | DSH 设置页 →「启动动画」 | DSH 设置页 →「聊天背景」 |
|
|
32
32
|
| 内容 | 六幕序列、逐字打出的英文副标题、环形准星、色散故障 | 静态底图 / 大字横幅 |
|
|
33
33
|
| 素材 | 5 句真声台词 + 终幕欢迎语 + 2 个界面音效(mp3) | 插件自带默认底图 + 你自己的图片 |
|
|
34
|
-
| 可调 |
|
|
34
|
+
| 可调 | 34 项:身份名称 / 编号 / 权限等级 / 配色 / 速度 / 音色 / 语速 / 每处文案 | 默认/自选底图、循环视频、纱的浓度、横幅文字、丢图即换 |
|
|
35
35
|
|
|
36
36
|
两个半侧**各自独立**:动画坏了不影响壁纸,壁纸坏了不影响动画,界面照常进。
|
|
37
37
|
|
|
@@ -104,6 +104,7 @@ https://github.com/LeeRolling/dsh-boot-suite/releases/latest/download/dsh-boot-s
|
|
|
104
104
|
|
|
105
105
|
- 中文主字 + 英文等宽副标题,**逐字打出**,像终端接收数据
|
|
106
106
|
- **真声台词**(`lib/assets/line-1..6.mp3`),素材缺失自动回落系统 TTS
|
|
107
|
+
- **配音音色可选** —— 内置 6 套完整配音(英文音色F(默认)+ 日文音色1–5),每套 6 句;另可叠 AI 机械音 / 机器人 / 无线电 / 老电视 / 低沉 5 种声音效果,或选「无」保留原声
|
|
107
108
|
- **语音没播完不推进** —— 每步停留时长由实际音频决定
|
|
108
109
|
- 显示模式:每次会话 / 总是 / 每天;可要求确认,也允许跳过
|
|
109
110
|
|
|
@@ -120,6 +121,7 @@ https://github.com/LeeRolling/dsh-boot-suite/releases/latest/download/dsh-boot-s
|
|
|
120
121
|
- **窗口按钮固定高对比(v2.13.10)**:右上角最小化 / 最大化 / 关闭是 Windows 原生画在页面之上的,插件给它们固定一对「浅色外观=浅底深图标、深色外观=深底浅图标」的颜色,不再被半透明壁纸底色带成白底白图标;片头播放期间标题栏仍保持透明
|
|
121
122
|
- **只留在聊天里**:不和启动动画第二幕抢画面(v2.11.8 起开场视频已移除);v2.13.0 新增可选的「循环视频」背景(默认关闭)
|
|
122
123
|
- **循环视频(v2.13.0)**:把 mp4/webm 或壁纸引擎项目文件夹放进 `~/.dsh/chat-wallpaper/loops/`(可添加自定义目录),设置页「聊天背景」→「循环视频」可选 静态底图/循环视频、手动/随机轮换;默认静音循环、只读本机;列表可删除(壁纸引擎项目整体删除)
|
|
124
|
+
- **色彩模式(v2.13.17)**:聊天背景可切「彩色(默认,不调色)/ 黑白(原版素材,不染色)」;黑白模式关掉主题色纱,画面保持原样
|
|
123
125
|
|
|
124
126
|
配置:`~/.dsh/chat-wallpaper.json`(设置页改)
|
|
125
127
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "f",
|
|
4
|
+
"name": "英文音色F(默认)",
|
|
5
|
+
"lang": "en"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"id": "g",
|
|
9
|
+
"name": "日文音色1",
|
|
10
|
+
"lang": "ja"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "h",
|
|
14
|
+
"name": "日文音色2",
|
|
15
|
+
"lang": "ja"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "i",
|
|
19
|
+
"name": "日文音色3",
|
|
20
|
+
"lang": "ja"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "j",
|
|
24
|
+
"name": "日文音色4",
|
|
25
|
+
"lang": "ja"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "k",
|
|
29
|
+
"name": "日文音色5",
|
|
30
|
+
"lang": "ja"
|
|
31
|
+
}
|
|
32
|
+
]
|
package/lib/client.js
CHANGED
|
@@ -249,10 +249,11 @@ window.__ModuleLoader__.load({
|
|
|
249
249
|
面板也要能正常显示这些控件,而不是一片 undefined。 */
|
|
250
250
|
var FALLBACK = {
|
|
251
251
|
voice: true, voiceLang: 'en', voiceName: '', voiceRate: 0.88, voicePitch: 0.85,
|
|
252
|
-
voiceVolume: 0.9, voiceCue: true, voiceLead: 240, voiceSource: 'asset',
|
|
252
|
+
voiceVolume: 0.9, voiceCue: true, voiceLead: 240, welcomeDelay: 550, voiceSource: 'asset', voicePack: 'f', voiceFx: 'robot',
|
|
253
253
|
sound: true, soundVolume: 0.5, bedNoise: false,
|
|
254
254
|
transitionIn: 'dissolve', transitionJoin: 'dissolve', transitionOut: 'dissolve',
|
|
255
|
-
transitionMs: 600, transitionDir: 'left'
|
|
255
|
+
transitionMs: 600, transitionDir: 'left',
|
|
256
|
+
colorMode: 'color'
|
|
256
257
|
}
|
|
257
258
|
function num(v, d) {
|
|
258
259
|
var n = Number(v)
|
|
@@ -265,7 +266,7 @@ window.__ModuleLoader__.load({
|
|
|
265
266
|
—— 桌面端走浏览器半侧自举时没有 1,就靠这一条
|
|
266
267
|
3) 下面这个常量:两份都没拿到时的兜底,别让面板显示 undefined
|
|
267
268
|
版本号本身只在 lib/index.js 里读一次,这里不重复维护。 */
|
|
268
|
-
var FALLBACK_VERSION = '2.13.
|
|
269
|
+
var FALLBACK_VERSION = '2.13.19'
|
|
269
270
|
/* 手动选包成功后置 true:让还在后台跑的“浏览器下载监听”不要再弹第二次更新器 */
|
|
270
271
|
var __manualUpdateApplied = false
|
|
271
272
|
function readInjectedVersion() {
|
|
@@ -285,6 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
285
286
|
var [busy, setBusy] = react.useState(false)
|
|
286
287
|
var [templates, setTemplates] = react.useState([])
|
|
287
288
|
var [hiddenTemplates, setHiddenTemplates] = react.useState([])
|
|
289
|
+
var [voicePacks, setVoicePacks] = react.useState([])
|
|
288
290
|
var [update, setUpdate] = react.useState(null)
|
|
289
291
|
var [checking, setChecking] = react.useState(false)
|
|
290
292
|
var [updating, setUpdating] = react.useState(false)
|
|
@@ -321,6 +323,25 @@ window.__ModuleLoader__.load({
|
|
|
321
323
|
}
|
|
322
324
|
react.useEffect(() => { refreshTemplates() }, [])
|
|
323
325
|
|
|
326
|
+
/* 音色包列表:来自 lib/assets/voices/manifest.json,由宿主只读下发 */
|
|
327
|
+
function refreshVoices() {
|
|
328
|
+
return fetch('/dsh-startup/voices', { headers: { Accept: 'application/json' }, cache: 'no-store' })
|
|
329
|
+
.then((r) => r.json())
|
|
330
|
+
.then((j) => { if (j && j.ok === true && Array.isArray(j.packs)) setVoicePacks(j.packs) })
|
|
331
|
+
.catch(() => { /* 拉不到就只显示内置音色 */ })
|
|
332
|
+
}
|
|
333
|
+
react.useEffect(() => { refreshVoices() }, [])
|
|
334
|
+
|
|
335
|
+
function voiceOptions() {
|
|
336
|
+
var opts = []
|
|
337
|
+
for (var i = 0; i < voicePacks.length; i++) {
|
|
338
|
+
var v = voicePacks[i]
|
|
339
|
+
if (!v || !v.id) continue
|
|
340
|
+
opts.push([v.id, String(v.name || v.id)])
|
|
341
|
+
}
|
|
342
|
+
return opts
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
function set(key, value) {
|
|
325
346
|
setNotice(null)
|
|
326
347
|
setDraft((d) => Object.assign({}, d, { [key]: value }))
|
|
@@ -644,7 +665,10 @@ window.__ModuleLoader__.load({
|
|
|
644
665
|
.then((r) => r.json())
|
|
645
666
|
.then((j) => {
|
|
646
667
|
if (!j || j.ok !== true) return
|
|
647
|
-
|
|
668
|
+
/* v2.13.15:预览必须带上「当前还没保存的修改」。
|
|
669
|
+
原来只灌宿主已保存的配置:用户选完音色/效果直接点「立即预览」,
|
|
670
|
+
运行时拿到的还是旧配置 —— 表现就是「选了日文音色,预览仍播英文」。 */
|
|
671
|
+
if (j.config) window.__DSH_STARTUP__ = Object.assign({}, FALLBACK, j.config, draft || {})
|
|
648
672
|
try {
|
|
649
673
|
if (j.template) window.__DSH_STARTUP_TEMPLATE__ = j.template
|
|
650
674
|
else delete window.__DSH_STARTUP_TEMPLATE__
|
|
@@ -904,56 +928,38 @@ window.__ModuleLoader__.load({
|
|
|
904
928
|
h('div', { className: '__ss_groupTitle' }, '语音与音效'),
|
|
905
929
|
h(Check, {
|
|
906
930
|
label: '女声播报(念每一行的英文)',
|
|
907
|
-
hint: '
|
|
908
|
-
+ '动画会等这句话念完再走下一步,不会把语音掐断;浏览器要求先有一次点击才准出声,'
|
|
909
|
-
+ '所以第一次点按钮/按 Enter 时会把当前这行补念一次。',
|
|
931
|
+
hint: '每一行的英文都在文字出现的同一时刻念出来,动画等这句话念完再走下一步。',
|
|
910
932
|
checked: draft.voice,
|
|
911
933
|
onChange: (v) => set('voice', v)
|
|
912
934
|
}),
|
|
913
|
-
h(Field, {
|
|
914
|
-
label: '
|
|
915
|
-
hint: '
|
|
916
|
-
+ '
|
|
935
|
+
draft.voice ? h(Field, {
|
|
936
|
+
label: '配音音色',
|
|
937
|
+
hint: '6 套完整干音:英文 1 套(音色F·默认)+ 日文 5 套(日文音色1–5);可再叠声音效果。'
|
|
938
|
+
+ '选完音色后,下面的「声音效果」可以再叠一层处理,或选「无」保留原声。'
|
|
917
939
|
},
|
|
918
940
|
h(Select, {
|
|
919
|
-
value: draft.
|
|
920
|
-
options:
|
|
921
|
-
onChange: (v) => set('
|
|
941
|
+
value: draft.voicePack || 'builtin',
|
|
942
|
+
options: voiceOptions(),
|
|
943
|
+
onChange: (v) => set('voicePack', v)
|
|
922
944
|
})
|
|
923
|
-
),
|
|
924
|
-
h(Field, {
|
|
945
|
+
) : null,
|
|
946
|
+
draft.voice ? h(Field, {
|
|
947
|
+
label: '声音效果',
|
|
948
|
+
hint: '5 个音效都来自模板编辑器里的同一套参数,实时叠加在所选音色上;选「无(原声)」就是干净人声。'
|
|
949
|
+
+ '只影响全局配音素材,模板自带的配音不受影响。'
|
|
950
|
+
},
|
|
925
951
|
h(Select, {
|
|
926
|
-
value: draft.
|
|
927
|
-
options: [
|
|
928
|
-
|
|
952
|
+
value: draft.voiceFx || 'none',
|
|
953
|
+
options: [
|
|
954
|
+
['none', '无(原声,默认)'],
|
|
955
|
+
['ai-mech', 'AI 机械音'],
|
|
956
|
+
['robot', '机器人'],
|
|
957
|
+
['radio', '无线电'],
|
|
958
|
+
['oldtv', '老电视'],
|
|
959
|
+
['dark', '低沉']
|
|
960
|
+
],
|
|
961
|
+
onChange: (v) => set('voiceFx', v)
|
|
929
962
|
})
|
|
930
|
-
),
|
|
931
|
-
draft.voice ? h(VoicePicker, {
|
|
932
|
-
value: draft.voiceName,
|
|
933
|
-
onChange: (v) => set('voiceName', v),
|
|
934
|
-
onPreview: () => speakSample(null, draft)
|
|
935
|
-
}) : null,
|
|
936
|
-
draft.voice ? h('div', { className: '__ss_row' },
|
|
937
|
-
h('button', {
|
|
938
|
-
className: '__ss_btn',
|
|
939
|
-
type: 'button',
|
|
940
|
-
onClick: () => {
|
|
941
|
-
set('voiceRate', 0.88)
|
|
942
|
-
set('voicePitch', 0.85)
|
|
943
|
-
set('voiceName', '')
|
|
944
|
-
set('voiceCue', true)
|
|
945
|
-
set('voiceLead', 240)
|
|
946
|
-
}
|
|
947
|
-
}, '↺ 套用「电影旁白」档'),
|
|
948
|
-
h('span', { className: '__ss_hint' }, '语速 0.88 · 音调 0.85(压抑、深沉)· 自动挑神经网络女声 · 提示音 + 开口留白')
|
|
949
|
-
) : null,
|
|
950
|
-
draft.voice ? h('div', { className: '__ss_two' },
|
|
951
|
-
h(Field, { label: '语速' },
|
|
952
|
-
h(Range, { value: num(draft.voiceRate, 1), min: 0.5, max: 2, display: num(draft.voiceRate, 1).toFixed(2) + '×', onChange: (v) => set('voiceRate', v) })
|
|
953
|
-
),
|
|
954
|
-
h(Field, { label: '音调' },
|
|
955
|
-
h(Range, { value: num(draft.voicePitch, 1), min: 0, max: 2, display: num(draft.voicePitch, 1).toFixed(2), onChange: (v) => set('voicePitch', v) })
|
|
956
|
-
)
|
|
957
963
|
) : null,
|
|
958
964
|
draft.voice ? h(Field, { label: '开口前留白', hint: '提示音响过之后停多久再开口。电影旁白/机场广播都有这一拍,240ms 起最像"系统在播报"。' },
|
|
959
965
|
h(Range, {
|
|
@@ -962,6 +968,13 @@ window.__ModuleLoader__.load({
|
|
|
962
968
|
onChange: (v) => set('voiceLead', v)
|
|
963
969
|
})
|
|
964
970
|
) : null,
|
|
971
|
+
draft.voice ? h(Field, { label: '终幕语音延迟', hint: '05→06 的过场和声钟约 1.5 秒;默认等它收尾再进最后一句。觉得欢迎语进得太早/太晚,就调这里。' },
|
|
972
|
+
h(Range, {
|
|
973
|
+
value: num(draft.welcomeDelay, 550), min: 0, max: 3000, step: 50,
|
|
974
|
+
display: Math.round(num(draft.welcomeDelay, 550)) + ' ms',
|
|
975
|
+
onChange: (v) => set('welcomeDelay', v)
|
|
976
|
+
})
|
|
977
|
+
) : null,
|
|
965
978
|
draft.voice ? h(Field, { label: '音量' },
|
|
966
979
|
h(Range, { value: num(draft.voiceVolume, 0.9), min: 0, max: 1, display: Math.round(num(draft.voiceVolume, 0.9) * 100) + '%', onChange: (v) => set('voiceVolume', v) })
|
|
967
980
|
) : null,
|
|
@@ -1587,7 +1600,7 @@ window.__ModuleLoader__.load({
|
|
|
1587
1600
|
/* 每次改客户端就把这个号 +1。
|
|
1588
1601
|
有了它在控制台一眼就能判断"浏览器里跑的是不是最新那份" ——
|
|
1589
1602
|
排查问题时,分不清是代码问题还是旧包,会白猜一轮。 */
|
|
1590
|
-
var BUILD = 'cw-
|
|
1603
|
+
var BUILD = 'cw-42'
|
|
1591
1604
|
|
|
1592
1605
|
function log() {
|
|
1593
1606
|
try {
|
|
@@ -2093,6 +2106,8 @@ window.__ModuleLoader__.load({
|
|
|
2093
2106
|
L.push(B + ' > ' + V + ' { position: absolute; inset: 0; }')
|
|
2094
2107
|
L.push('html.dsh-cw-light ' + B + ' > ' + V + ' { background: linear-gradient(' + cfg.veilLightA + ', ' + cfg.veilLightB + '); }')
|
|
2095
2108
|
L.push('html.dsh-cw-dark ' + B + ' > ' + V + ' { background: linear-gradient(' + cfg.veilDarkA + ', ' + cfg.veilDarkB + '); }')
|
|
2109
|
+
/* v2.13.17:黑白原版素材模式 —— 不染色,直接关掉主题色纱(模糊仍可单独调) */
|
|
2110
|
+
L.push('html.dsh-cw-mono ' + B + ' > ' + V + ' { background: transparent !important; }')
|
|
2096
2111
|
/* v2.13.11 镜头模糊:只模糊纱后面的壁纸/视频,纱上的聊天文字不受影响。
|
|
2097
2112
|
0 = 不生成规则(完全不动);100 = 约 24px。 */
|
|
2098
2113
|
if (Number(cfg.veilBlur) > 0) {
|
|
@@ -2388,6 +2403,8 @@ window.__ModuleLoader__.load({
|
|
|
2388
2403
|
current = cfg
|
|
2389
2404
|
var dark = looksDark()
|
|
2390
2405
|
setThemeClass(dark)
|
|
2406
|
+
/* v2.13.17:黑白(原版素材)模式 → 关掉主题色纱,保留原画面 */
|
|
2407
|
+
try { document.documentElement.classList.toggle('dsh-cw-mono', cfg.colorMode === 'mono') } catch (e) { /* 忽略 */ }
|
|
2391
2408
|
log('套用壁纸', { image: cfg.__image, theme: dark ? 'dark' : 'light' })
|
|
2392
2409
|
injectCss(cfg)
|
|
2393
2410
|
mountBg(cfg) /* 最底:聊天底图(用户自己定的那张,可选)*/
|
|
@@ -2421,7 +2438,7 @@ window.__ModuleLoader__.load({
|
|
|
2421
2438
|
removeBg()
|
|
2422
2439
|
removeBanner()
|
|
2423
2440
|
try {
|
|
2424
|
-
document.documentElement.classList.remove('dsh-cw-dark', 'dsh-cw-light')
|
|
2441
|
+
document.documentElement.classList.remove('dsh-cw-dark', 'dsh-cw-light', 'dsh-cw-mono')
|
|
2425
2442
|
themeClass = null
|
|
2426
2443
|
} catch (e) { /* 忽略 */ }
|
|
2427
2444
|
}
|
|
@@ -2555,9 +2572,11 @@ window.__ModuleLoader__.load({
|
|
|
2555
2572
|
var [busy, setBusy] = cwReact.useState(false)
|
|
2556
2573
|
var [error, setError] = cwReact.useState(null)
|
|
2557
2574
|
var [notice, setNotice] = cwReact.useState(null)
|
|
2575
|
+
var [refreshing, setRefreshing] = cwReact.useState(false)
|
|
2558
2576
|
|
|
2559
|
-
function load() {
|
|
2577
|
+
function load(manual) {
|
|
2560
2578
|
setError(null)
|
|
2579
|
+
if (manual) { setNotice(null); setRefreshing(true) }
|
|
2561
2580
|
return Promise.all([api('/images'), api('/config', { cache: 'no-store' }), api('/videos')])
|
|
2562
2581
|
.then(function (arr) {
|
|
2563
2582
|
var images = arr[0] || {}
|
|
@@ -2578,6 +2597,7 @@ window.__ModuleLoader__.load({
|
|
|
2578
2597
|
veilBlur: cfg.veilBlur == null ? 0 : Math.max(0, Math.min(100, Number(cfg.veilBlur) || 0)),
|
|
2579
2598
|
image: conf.image || null,
|
|
2580
2599
|
imageFit: cfg.imageFit === 'contain' ? 'contain' : 'cover',
|
|
2600
|
+
colorMode: cfg.colorMode === 'mono' ? 'mono' : 'color',
|
|
2581
2601
|
video: {
|
|
2582
2602
|
bgMode: vids.bgMode || 'image',
|
|
2583
2603
|
videoMode: vids.videoMode || 'single',
|
|
@@ -2590,8 +2610,13 @@ window.__ModuleLoader__.load({
|
|
|
2590
2610
|
}
|
|
2591
2611
|
setData(next)
|
|
2592
2612
|
setDraft({ banner: next.banner, bannerOpacity: next.bannerOpacity, dark: looksDark(), newRoot: '', shuffleMin: next.video.shuffleMinutes })
|
|
2613
|
+
if (manual) {
|
|
2614
|
+
setNotice('已重新读取:图片 ' + next.images.length + ' 张 / 视频 ' + next.video.items.length + ' 个'
|
|
2615
|
+
+ (next.dropDirExists ? '' : '(丢图目录还不存在,点「打开图片文件夹」会创建)'))
|
|
2616
|
+
}
|
|
2593
2617
|
})
|
|
2594
2618
|
.catch(function (e) { setError('读取失败:' + String((e && e.message) || e)) })
|
|
2619
|
+
.then(function () { if (manual) setRefreshing(false) })
|
|
2595
2620
|
}
|
|
2596
2621
|
cwReact.useEffect(function () { load() }, [])
|
|
2597
2622
|
|
|
@@ -2694,11 +2719,20 @@ window.__ModuleLoader__.load({
|
|
|
2694
2719
|
cwH('br'),
|
|
2695
2720
|
'循环视频:' + (data.video && data.video.items.length ? ('已扫描 ' + data.video.items.length + ' 段' + (data.video.bgMode === 'video' && data.video.currentId ? ',正在播放' : ',未启用')) : '(视频目录里还没有视频)'),
|
|
2696
2721
|
cwH('br'),
|
|
2697
|
-
'丢图目录:' + (data.dropDir || '
|
|
2722
|
+
'丢图目录:' + (data.dropDir || '(尚未创建,点「打开图片文件夹」会自动创建)')
|
|
2698
2723
|
),
|
|
2699
2724
|
cwH('div', { className: '__ss_row' },
|
|
2700
|
-
cwH('button', {
|
|
2701
|
-
|
|
2725
|
+
cwH('button', {
|
|
2726
|
+
className: '__ss_btn', type: 'button', disabled: busy,
|
|
2727
|
+
onClick: function () {
|
|
2728
|
+
setNotice(null); setError(null)
|
|
2729
|
+
api('/open-dir', { method: 'POST' }).then(function (j) {
|
|
2730
|
+
if (j && j.ok) setNotice('已打开图片文件夹:' + (j.opened || data.dropDir))
|
|
2731
|
+
else setError('打开图片文件夹失败:' + ((j && j.error) || '宿主没有返回'))
|
|
2732
|
+
})
|
|
2733
|
+
}
|
|
2734
|
+
}, '打开图片文件夹'),
|
|
2735
|
+
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || refreshing, onClick: function () { load(true) } }, refreshing ? '刷新中…' : '刷新')
|
|
2702
2736
|
)
|
|
2703
2737
|
),
|
|
2704
2738
|
|
|
@@ -2707,7 +2741,7 @@ window.__ModuleLoader__.load({
|
|
|
2707
2741
|
cwH('div', { className: '__ss_hint' }, '点一下立刻换。第一张「默认背景(内置)」是插件自带、开箱就有的;把图丢进上面的目录也会出现在这里,按修改时间取最新那张。'),
|
|
2708
2742
|
data.images.length
|
|
2709
2743
|
? cwH('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, data.images.map(thumb))
|
|
2710
|
-
: cwH('div', { className: '__ss_hint' }, '目录里还没有图。点「打开图片文件夹」,把 jpg / png / webp
|
|
2744
|
+
: cwH('div', { className: '__ss_hint' }, '目录里还没有图。点「打开图片文件夹」,把 jpg / png / webp 丢进去,再点刷新。只扫描这一层,子文件夹里的不算(支持 jpg/png/webp/gif/avif/bmp)。'),
|
|
2711
2745
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2712
2746
|
cwH('span', { className: '__ss_status' }, '图片适配'),
|
|
2713
2747
|
cwH('select', {
|
|
@@ -2749,6 +2783,18 @@ window.__ModuleLoader__.load({
|
|
|
2749
2783
|
cwH('option', { value: 'shuffle' }, '随机轮换')
|
|
2750
2784
|
)
|
|
2751
2785
|
),
|
|
2786
|
+
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2787
|
+
cwH('span', { className: '__ss_status' }, '色彩模式'),
|
|
2788
|
+
cwH('select', {
|
|
2789
|
+
className: '__ss_select', disabled: busy,
|
|
2790
|
+
value: data.colorMode,
|
|
2791
|
+
onChange: function (ev) { apply({ colorMode: ev.target.value }, true) }
|
|
2792
|
+
},
|
|
2793
|
+
cwH('option', { value: 'color' }, '彩色(默认)'),
|
|
2794
|
+
cwH('option', { value: 'mono' }, '黑白(原版素材,不染色)')
|
|
2795
|
+
)
|
|
2796
|
+
),
|
|
2797
|
+
cwH('div', { className: '__ss_hint' }, '黑白模式会关掉主题色纱、保留原画面颜色,适合本身就是黑白的动态壁纸;彩色模式不做任何调色。'),
|
|
2752
2798
|
data.video.videoMode === 'shuffle'
|
|
2753
2799
|
? cwH('div', { className: '__ss_row' },
|
|
2754
2800
|
cwH('span', { className: '__ss_status' }, '轮换间隔(分钟,0 = 每次启动随机)'),
|
|
@@ -2799,7 +2845,7 @@ window.__ModuleLoader__.load({
|
|
|
2799
2845
|
: cwH('div', { className: '__ss_hint' }, '还没有视频。把 mp4/webm 或壁纸引擎的项目文件夹放进上面的目录,再点下方「刷新」。'),
|
|
2800
2846
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2801
2847
|
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || !(data.video.roots && data.video.roots.length), onClick: function () { if (data.video.roots && data.video.roots.length) rootAction('open', data.video.roots[0]) } }, '打开视频文件夹'),
|
|
2802
|
-
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy, onClick: function () { load() } }, '刷新')
|
|
2848
|
+
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || refreshing, onClick: function () { load(true) } }, refreshing ? '刷新中…' : '刷新')
|
|
2803
2849
|
)
|
|
2804
2850
|
),
|
|
2805
2851
|
|
package/lib/host-startup.js
CHANGED
|
@@ -19,6 +19,18 @@ import { spawn } from 'node:child_process'
|
|
|
19
19
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
20
|
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
21
21
|
const CONFIG_FILE = path.join(DSH_HOME, 'dsh-startup.json')
|
|
22
|
+
const VOICE_ROOT = path.join(PACKAGE_ROOT, 'lib', 'assets', 'voices')
|
|
23
|
+
/* 音色包清单由 lib/assets/voices/manifest.json 描述(随安装包发布)。
|
|
24
|
+
读不到就当作没有扩展音色,内置音色不受影响。 */
|
|
25
|
+
const VOICE_PACKS = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const list = JSON.parse(fs.readFileSync(path.join(VOICE_ROOT, 'manifest.json'), 'utf8'))
|
|
28
|
+
if (!Array.isArray(list)) return []
|
|
29
|
+
return list.filter((v) => v && /^[a-z]$/.test(String(v.id)) && (v.lang === 'en' || v.lang === 'ja'))
|
|
30
|
+
.map((v) => ({ id: String(v.id), name: String(v.name || '').slice(0, 12) || ('音色 ' + String(v.id).toUpperCase()), lang: v.lang }))
|
|
31
|
+
} catch (err) { return [] }
|
|
32
|
+
})()
|
|
33
|
+
const VOICE_PACK_IDS = [...VOICE_PACKS.map((v) => v.id)]
|
|
22
34
|
|
|
23
35
|
const NS = 'dsh-startup-screen'
|
|
24
36
|
const ROUTE_BASE = '/dsh-startup'
|
|
@@ -75,7 +87,10 @@ const DEFAULTS = {
|
|
|
75
87
|
voiceCue: true, // 念台词前的一声轻提示
|
|
76
88
|
sfx: 'audio', // 音效来源:audio=lib/assets 里的真素材 / synth=现场合成
|
|
77
89
|
voiceSource: 'asset', // 语音来源:asset=优先用真声素材(拿不到自动回落 TTS)/ tts=全 TTS
|
|
90
|
+
voicePack: 'f', // 配音音色:f..k=音色包(默认 F 英文);用户在设置里可换
|
|
91
|
+
voiceFx: 'robot', // 声音效果:none | ai-mech | robot | radio | oldtv | dark(默认机器人,贴近旧版听感)
|
|
78
92
|
voiceLead: 240, // 提示音之后留一小段空白再开口
|
|
93
|
+
welcomeDelay: 550, // 05→06 过场和声钟之后等多久再进终幕欢迎语(0–3000ms;实测 550 最顺)
|
|
79
94
|
identity: 'JOYCE MOORE', // ← 设置页里可改的「身份名称」
|
|
80
95
|
identityId: '0087',
|
|
81
96
|
accessLevel: '3',
|
|
@@ -117,6 +132,8 @@ const ENUMS = {
|
|
|
117
132
|
voiceLang: ['en', 'zh', 'both'],
|
|
118
133
|
sfx: ['audio', 'synth'],
|
|
119
134
|
voiceSource: ['asset', 'tts'],
|
|
135
|
+
voicePack: VOICE_PACK_IDS,
|
|
136
|
+
voiceFx: ['none', 'ai-mech', 'robot', 'radio', 'oldtv', 'dark'],
|
|
120
137
|
transitionIn: TRANSITION_TYPES,
|
|
121
138
|
transitionJoin: TRANSITION_TYPES,
|
|
122
139
|
transitionOut: TRANSITION_TYPES,
|
|
@@ -125,6 +142,7 @@ const ENUMS = {
|
|
|
125
142
|
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue']
|
|
126
143
|
/** 数值字段的合法区间(越界即夹取)。 */
|
|
127
144
|
const RANGES = {
|
|
145
|
+
welcomeDelay: [0, 3000],
|
|
128
146
|
speed: [0.35, 3],
|
|
129
147
|
transitionMs: [200, 2000],
|
|
130
148
|
soundVolume: [0, 1],
|
|
@@ -397,6 +415,36 @@ function apply(ctx) {
|
|
|
397
415
|
}
|
|
398
416
|
}))
|
|
399
417
|
|
|
418
|
+
/* 音色包:6 套扩展配音(英文 F + 日文 G–K)。设置页只拿 id/名称/语言,音频走只读白名单路由;
|
|
419
|
+
id 与文件名都白名单,且不接受子路径 —— 和 /asset 同样防目录穿越。 */
|
|
420
|
+
disposers.push(registerRoute({
|
|
421
|
+
kind: 'exact',
|
|
422
|
+
path: ROUTE_BASE + '/voices',
|
|
423
|
+
handler: (req, res) => sendJson(res, 200, { ok: true, packs: VOICE_PACKS, default: 'f' })
|
|
424
|
+
}))
|
|
425
|
+
disposers.push(registerRoute({
|
|
426
|
+
kind: 'prefix',
|
|
427
|
+
path: ROUTE_BASE + '/voice',
|
|
428
|
+
handler: (req, res) => {
|
|
429
|
+
const rest = decodeURIComponent(String(req.url || '').split('?')[0])
|
|
430
|
+
.slice((ROUTE_BASE + '/voice').length).replace(/^\/+/, '')
|
|
431
|
+
const parts = rest.split('/')
|
|
432
|
+
const id = String(parts[0] || '')
|
|
433
|
+
const name = String(parts[1] || '')
|
|
434
|
+
if (!/^[a-z]$/.test(id) || !/^line-[1-6]\.mp3$/.test(name) || !VOICE_PACK_IDS.includes(id)) {
|
|
435
|
+
res.writeHead(404); res.end(); return
|
|
436
|
+
}
|
|
437
|
+
let bytes
|
|
438
|
+
try { bytes = fs.readFileSync(path.join(VOICE_ROOT, id, name)) } catch (err) { res.writeHead(404); res.end(); return }
|
|
439
|
+
res.writeHead(200, {
|
|
440
|
+
'Content-Type': 'audio/mpeg',
|
|
441
|
+
'Cache-Control': 'no-store',
|
|
442
|
+
'Content-Length': String(bytes.length)
|
|
443
|
+
})
|
|
444
|
+
res.end(bytes)
|
|
445
|
+
}
|
|
446
|
+
}))
|
|
447
|
+
|
|
400
448
|
/* 模板自带声音:/dsh-startup/template-asset/<模板id>/<文件名>
|
|
401
449
|
先找用户目录 ~/.dsh/startup-templates/<id>/assets/,再找插件自带 templates/<id>/assets/。
|
|
402
450
|
id 与文件名都过白名单,且不接受子路径 —— 模板作者塞不进来任意文件读取。 */
|
package/lib/host-wallpaper.js
CHANGED
|
@@ -63,7 +63,8 @@ const DEFAULTS = {
|
|
|
63
63
|
videoMode: 'single', // single 手动选 / shuffle 随机轮换
|
|
64
64
|
videoId: '', // 选中的视频 id(视频文件的绝对路径规范化)
|
|
65
65
|
videoRoots: [], // 额外的视频存放目录(默认 loops 始终包含)
|
|
66
|
-
videoShuffleMinutes: 0
|
|
66
|
+
videoShuffleMinutes: 0, // 0 = 每次启动随机;>0 = 每 N 分钟换一段
|
|
67
|
+
colorMode: 'color' // color 彩色 | mono 黑白(原版素材,不染色)
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function readConfig() {
|
|
@@ -407,6 +408,8 @@ export default {
|
|
|
407
408
|
const cfg = readConfig()
|
|
408
409
|
const cur = resolveImage(cfg)
|
|
409
410
|
const dir = wallpaperImageDir()
|
|
411
|
+
/* v2.13.17:目录不存在时直接建出来,面板不再显示「?」,打开按钮也才有东西可开 */
|
|
412
|
+
try { mkdirSync(dir, { recursive: true }) } catch { /* 权限不足就保持原样 */ }
|
|
410
413
|
json(res, 200, {
|
|
411
414
|
ok: true,
|
|
412
415
|
dropDir: dir.replace(/\\/g, '/'),
|
|
@@ -464,7 +467,7 @@ export default {
|
|
|
464
467
|
* 只接受白名单键,值做长度/类型校验,避免面板把配置写成垃圾。
|
|
465
468
|
*/
|
|
466
469
|
const ALLOWED_SET = new Set(['image', 'imageFit', 'veilLightA', 'veilLightB', 'veilDarkA', 'veilDarkB', 'veilBlur', 'banner', 'bannerOpacity',
|
|
467
|
-
'bgMode', 'videoMode', 'videoId', 'videoShuffleMinutes'])
|
|
470
|
+
'bgMode', 'videoMode', 'videoId', 'videoShuffleMinutes', 'colorMode'])
|
|
468
471
|
const setHandler = (req, res) => {
|
|
469
472
|
let body = ''
|
|
470
473
|
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy() })
|
|
@@ -483,6 +486,7 @@ export default {
|
|
|
483
486
|
continue
|
|
484
487
|
}
|
|
485
488
|
if (k === 'bgMode') { if (v === 'image' || v === 'video') { cfg.bgMode = v; applied.bgMode = v } continue }
|
|
489
|
+
if (k === 'colorMode') { if (v === 'color' || v === 'mono') { cfg.colorMode = v; applied.colorMode = v } continue }
|
|
486
490
|
if (k === 'videoMode') { if (v === 'single' || v === 'shuffle') { cfg.videoMode = v; applied.videoMode = v } continue }
|
|
487
491
|
if (k === 'videoShuffleMinutes') {
|
|
488
492
|
const n = Number(v)
|
|
@@ -691,6 +695,8 @@ export default {
|
|
|
691
695
|
}
|
|
692
696
|
}
|
|
693
697
|
|
|
698
|
+
/* v2.13.17:开机就把丢图目录建好;老用户/新装用户都不会再看到「?」 */
|
|
699
|
+
try { mkdirSync(wallpaperImageDir(), { recursive: true }) } catch { /* 忽略 */ }
|
|
694
700
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/config', handler: configHandler }))
|
|
695
701
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/image', handler: imageHandler }))
|
|
696
702
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/images', handler: imagesHandler }))
|
package/lib/splash.js
CHANGED
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
voiceCue: true, // 念台词前的一声轻提示
|
|
89
89
|
sfx: 'audio', // audio=真素材(用户音频里抠的音效)/ synth=现场合成
|
|
90
90
|
voiceSource: 'asset', // asset=优先用真声素材 / tts=全部 TTS 合成
|
|
91
|
-
voiceLead: 240,
|
|
91
|
+
voiceLead: 240,
|
|
92
|
+
welcomeDelay: 550, // 终幕欢迎语相对 05→06 和声钟的延迟(ms)
|
|
92
93
|
identity: 'JOYCE MOORE', // ← 设置页可改的"身份名称"
|
|
93
94
|
identityId: '0087',
|
|
94
95
|
accessLevel: '3',
|
|
@@ -264,6 +265,9 @@
|
|
|
264
265
|
没有就回落全局 lib/assets/line-N.mp3(原来的行为)。
|
|
265
266
|
这样不同模板可以有各自的配音,互不覆盖。 */
|
|
266
267
|
function assetUrl(name) {
|
|
268
|
+
/* 扩展音色包:slot 形如 voice:a:line-1,走 /dsh-startup/voice/a/line-1.mp3 */
|
|
269
|
+
var mv = /^voice:([a-z]):(line-[1-9]\d?)$/.exec(name)
|
|
270
|
+
if (mv) return '/dsh-startup/voice/' + mv[1] + '/' + mv[2] + '.mp3'
|
|
267
271
|
var map = (TEMPLATE && TEMPLATE.assets) || null
|
|
268
272
|
var file = (map && map[name]) ? String(map[name]).replace(/^.*\//, '') : ''
|
|
269
273
|
if (file && /^[a-z0-9][a-z0-9._-]{0,40}\.(mp3|wav|ogg|m4a|aac)$/i.test(file) && TEMPLATE && TEMPLATE.id) {
|
|
@@ -1299,7 +1303,6 @@
|
|
|
1299
1303
|
function ensureAsset(name) {
|
|
1300
1304
|
if (buffers[name]) return Promise.resolve(true)
|
|
1301
1305
|
if (pending[name]) return pending[name]
|
|
1302
|
-
if (C.voiceSource === 'tts') return Promise.resolve(false)
|
|
1303
1306
|
pending[name] = new Promise(function (resolve) {
|
|
1304
1307
|
try {
|
|
1305
1308
|
fetch(assetUrl(name))
|
|
@@ -1314,20 +1317,58 @@
|
|
|
1314
1317
|
return pending[name]
|
|
1315
1318
|
}
|
|
1316
1319
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1320
|
+
/* 配音效果预设:与模板编辑器里的 5 个音效同参数(AI 机械音/机器人/无线电/老电视/低沉)。
|
|
1321
|
+
素材先解码成 buffer,再在 BufferSource 后面串滤波 + 失真 + 环形调制,实时生效。 */
|
|
1322
|
+
var VOICE_FX_PRESETS = {
|
|
1323
|
+
'ai-mech': { hp: 130, lp: 4200, peak: { f: 260, g: 4, q: 1.0 }, drive: 0.15, ring: 0 },
|
|
1324
|
+
'robot': { hp: 180, lp: 3200, peak: { f: 1500, g: 3, q: 0.9 }, drive: 0.35, ring: 42 },
|
|
1325
|
+
'radio': { hp: 420, lp: 2800, peak: { f: 1800, g: 5, q: 1.2 }, drive: 0.6, ring: 0 },
|
|
1326
|
+
'oldtv': { hp: 220, lp: 2200, peak: null, drive: 0.45, ring: 0 },
|
|
1327
|
+
'dark': { hp: 90, lp: 1200, peak: { f: 180, g: 5, q: 0.8 }, drive: 0.1, ring: 0 }
|
|
1328
|
+
}
|
|
1329
|
+
function voiceDriveCurve(amount) {
|
|
1330
|
+
var k = Math.max(0, Math.min(1, Number(amount) || 0)) * 60
|
|
1331
|
+
var n = 1024, curve = new Float32Array(n)
|
|
1332
|
+
for (var i = 0; i < n; i++) {
|
|
1333
|
+
var x = (i * 2) / n - 1
|
|
1334
|
+
curve[i] = ((3 + k) * x * 20 * Math.PI / 180) / (Math.PI + k * Math.abs(x))
|
|
1335
|
+
}
|
|
1336
|
+
return curve
|
|
1337
|
+
}
|
|
1338
|
+
/** 播素材并等它播完,resolve 实际时长(ms)。fx 传 5 个效果之一;拿不到素材 resolve 0。 */
|
|
1339
|
+
function playAssetAndWait(name, gain, fx) {
|
|
1319
1340
|
return new Promise(function (resolve) {
|
|
1320
1341
|
var c = ac(true)
|
|
1321
1342
|
var buf = buffers[name]
|
|
1322
1343
|
if (!c || !buf) { resolve(0); return }
|
|
1344
|
+
var osc = null
|
|
1323
1345
|
try {
|
|
1324
1346
|
var src = c.createBufferSource()
|
|
1325
1347
|
src.buffer = buf
|
|
1326
1348
|
var g = c.createGain()
|
|
1327
1349
|
g.gain.value = Math.min(1, (gain == null ? 0.85 : gain) * clamp01(C.voiceVolume, 0, 1) * 1.6)
|
|
1328
|
-
src
|
|
1329
|
-
|
|
1350
|
+
var node = src
|
|
1351
|
+
var P = fx && VOICE_FX_PRESETS[fx]
|
|
1352
|
+
if (P) {
|
|
1353
|
+
if (P.hp) { var hp = c.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = P.hp; node.connect(hp); node = hp }
|
|
1354
|
+
if (P.lp) { var lp = c.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = P.lp; node.connect(lp); node = lp }
|
|
1355
|
+
if (P.peak) { var pk = c.createBiquadFilter(); pk.type = 'peaking'; pk.frequency.value = P.peak.f; pk.gain.value = P.peak.g; pk.Q.value = P.peak.q; node.connect(pk); node = pk }
|
|
1356
|
+
if (P.drive) { var ws = c.createWaveShaper(); ws.curve = voiceDriveCurve(P.drive); ws.oversample = '2x'; node.connect(ws); node = ws }
|
|
1357
|
+
if (P.ring) {
|
|
1358
|
+
var ringGain = c.createGain(); ringGain.gain.value = 0.55
|
|
1359
|
+
osc = c.createOscillator(); osc.frequency.value = P.ring; osc.type = 'sine'
|
|
1360
|
+
var modGain = c.createGain(); modGain.gain.value = 0.45
|
|
1361
|
+
osc.connect(modGain); modGain.connect(ringGain.gain)
|
|
1362
|
+
node.connect(ringGain); node = ringGain
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
node.connect(g); g.connect(c.destination)
|
|
1366
|
+
src.onended = function () {
|
|
1367
|
+
if (osc) { try { osc.stop() } catch (eO) {}; try { osc.disconnect() } catch (eD) {} }
|
|
1368
|
+
resolve(Math.round(buf.duration * 1000))
|
|
1369
|
+
}
|
|
1330
1370
|
src.start()
|
|
1371
|
+
if (osc) { try { osc.start() } catch (eS) {} }
|
|
1331
1372
|
} catch (e) { resolve(0) }
|
|
1332
1373
|
})
|
|
1333
1374
|
}
|
|
@@ -1599,28 +1640,32 @@
|
|
|
1599
1640
|
* onend 时按估算兜底,绝不把动画卡死。
|
|
1600
1641
|
*/
|
|
1601
1642
|
line: function (en, zh, asset) {
|
|
1602
|
-
/*
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (ms > 0) { saidOnce = true; return ms }
|
|
1613
|
-
return ttsLine(en, zh)
|
|
1614
|
-
})
|
|
1643
|
+
/* v2.13.16:旧 TTS 配音链路退役。这里只用「音色包 + 声音效果」两段:
|
|
1644
|
+
音色包决定用哪套干音(a–k);效果在运行时叠加。
|
|
1645
|
+
素材缺失时静音,不再回落系统 TTS,避免旧机械音盖掉新音色。 */
|
|
1646
|
+
if (!asset || !C.voice || finished) return Promise.resolve(0)
|
|
1647
|
+
var useAsset = asset
|
|
1648
|
+
var tplAssets = (TEMPLATE && TEMPLATE.assets) || null
|
|
1649
|
+
var isGlobalLine = /^line-[1-9]\d?$/.test(asset) && !(tplAssets && tplAssets[asset])
|
|
1650
|
+
if (isGlobalLine) {
|
|
1651
|
+
var pack = String(C.voicePack || 'a')
|
|
1652
|
+
if (/^[a-k]$/.test(pack)) useAsset = 'voice:' + pack + ':' + asset
|
|
1615
1653
|
}
|
|
1616
|
-
|
|
1654
|
+
var fx = String(C.voiceFx || 'none')
|
|
1655
|
+
return audio.ensureAsset(useAsset).then(function (ok) {
|
|
1656
|
+
if (!ok || finished) return 0
|
|
1657
|
+
if (C.voiceCue !== false) audio.cue()
|
|
1658
|
+
return audio.playAssetAndWait(useAsset, 0.85, isGlobalLine ? fx : 'none').then(function (ms) {
|
|
1659
|
+
if (ms > 0) setMeter(true)
|
|
1660
|
+
return ms
|
|
1661
|
+
})
|
|
1662
|
+
})
|
|
1617
1663
|
},
|
|
1618
|
-
|
|
1619
1664
|
/** TTS 路径:素材缺失 / voiceSource=tts / 终幕那条动态名字 */
|
|
1620
1665
|
tts: function (en, zh) { return ttsLine(en, zh) },
|
|
1621
1666
|
|
|
1622
1667
|
/** 不等待的版本:设置页试听 / 任何"念完就行"的场合 */
|
|
1623
|
-
say: function (en, zh) { return
|
|
1668
|
+
say: function (en, zh) { return ttsLine(en, zh) },
|
|
1624
1669
|
|
|
1625
1670
|
/** 浏览器要求"先有用户手势"才放声:第一次点击/按键解锁 + 补播当前行 */
|
|
1626
1671
|
unlock: function () {
|
|
@@ -3455,7 +3500,12 @@
|
|
|
3455
3500
|
/* 终幕欢迎语:用从 Video Project 3 截出来的真声素材(line-6)。
|
|
3456
3501
|
素材缺失 / voiceSource=tts / voice 关闭时自动静音,不会退回 TTS 念白 ——
|
|
3457
3502
|
因为这条素材是「欢迎访问」整句,和动态身份名称无关。 */
|
|
3458
|
-
|
|
3503
|
+
/* 05→06 的过场和声钟约 1.5 秒。旧行为是钟声一响欢迎语就开口,听感太挤;
|
|
3504
|
+
现在按配置等钟声收尾再进最后一句,默认 1500ms,可在设置里微调。 */
|
|
3505
|
+
var welcomeDelay = Number(C.welcomeDelay)
|
|
3506
|
+
if (!isFinite(welcomeDelay)) welcomeDelay = 1500
|
|
3507
|
+
welcomeDelay = Math.max(0, Math.min(3000, welcomeDelay))
|
|
3508
|
+
var welcome = wait(welcomeDelay).then(function () { return voice.line(null, null, 'line-6') })
|
|
3459
3509
|
|
|
3460
3510
|
ui.status.classList.remove('dsu-in')
|
|
3461
3511
|
ui.subEn.classList.remove('dsu-in')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-boot-suite",
|
|
3
|
-
"version": "2.13.
|
|
4
|
-
"description": "DeepSeek Harness
|
|
3
|
+
"version": "2.13.19",
|
|
4
|
+
"description": "DeepSeek Harness 启动动画与聊天背景插件:两幕开机动画、自定义动画导入、三段转场、6 套干音音色(英文 1 + 日文 5)+ 5 种声音效果、聊天背景图片/循环视频、更新前自动回滚;聊天背景新增黑白原版素材模式。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
package/test/smoke-client.mjs
CHANGED
|
@@ -112,6 +112,12 @@ const fetchStub = (url, init) => {
|
|
|
112
112
|
if (!HIDDEN.includes(id)) HIDDEN.push(id)
|
|
113
113
|
return Promise.resolve({ json: () => Promise.resolve({ ok: true, id, removed: false, hidden: true }) })
|
|
114
114
|
}
|
|
115
|
+
if (u.includes('/dsh-startup/voices')) {
|
|
116
|
+
return Promise.resolve({ json: () => Promise.resolve({ ok: true, packs: [
|
|
117
|
+
{ id: 'f', name: '英文音色F(默认)', lang: 'en' },
|
|
118
|
+
{ id: 'g', name: '日文音色1', lang: 'ja' },
|
|
119
|
+
], default: 'f' }) })
|
|
120
|
+
}
|
|
115
121
|
if (u.includes('/dsh-startup/templates')) {
|
|
116
122
|
return Promise.resolve({ json: () => Promise.resolve({ ok: true, templates: liveTemplates(), hidden: HIDDEN.slice() }) })
|
|
117
123
|
}
|
|
@@ -211,10 +217,13 @@ const idInput = find(tree, (n) => n.type === 'input' && n.props.type === 'text'
|
|
|
211
217
|
check('身份名称输入框回填了当前配置', !!idInput, idInput && idInput.props.value)
|
|
212
218
|
check('渲染出「启动动画」开关', texts.includes('启用启动动画'))
|
|
213
219
|
check('渲染出「女声播报」开关', texts.includes('女声播报(念每一行的英文)'))
|
|
214
|
-
check('
|
|
220
|
+
check('已删除旧 TTS「播报语言」字段', !texts.includes('播报语言'))
|
|
221
|
+
check('渲染出「配音音色」字段', texts.includes('配音音色'))
|
|
222
|
+
check('渲染出「声音效果」字段', texts.includes('声音效果'))
|
|
223
|
+
check('渲染出「终幕语音延迟」字段', texts.includes('终幕语音延迟'))
|
|
215
224
|
check('渲染出「界面音效」开关', texts.includes('界面音效'))
|
|
216
|
-
check('
|
|
217
|
-
check('
|
|
225
|
+
check('已删除旧 TTS 音色选择', !texts.includes('试听所选音色') && !texts.includes('语音来源'))
|
|
226
|
+
check('渲染出音量、不再有语速/音调', texts.includes('音量') && !texts.includes('语速') && !texts.includes('音调'))
|
|
218
227
|
check('渲染出「立即预览」按钮', !!find(tree, (n) => n.type === 'button' && n.props.children === '立即预览'))
|
|
219
228
|
check('渲染出「恢复默认」按钮', !!find(tree, (n) => n.type === 'button' && n.props.children === '恢复默认'))
|
|
220
229
|
|
|
@@ -490,7 +499,21 @@ const beforePreview = requests.length
|
|
|
490
499
|
previewBtn.props.onClick()
|
|
491
500
|
await flush(); await flush(); await flush()
|
|
492
501
|
check('立即预览先重新拉配置', requests.length > beforePreview && requests.slice(beforePreview).some((r) => /\/dsh-startup\/config/.test(r.url || '')), '')
|
|
493
|
-
check('立即预览调用 __DSH_STARTUP_API__.replay()', windowStub.__replayed === true)
|
|
502
|
+
check('立即预览调用 __DSH_STARTUP_API__.replay()', windowStub.__replayed === true)
|
|
503
|
+
/* v2.13.15 回归:选完「配音音色」直接点预览(还没点保存)也必须按当前选择播 */
|
|
504
|
+
tree = renderPanel()
|
|
505
|
+
const voiceSel = walk(tree).filter((n) => n.type === 'select' && Array.isArray(n.props.children) && n.props.children.some((o) => o && o.props.value === 'g'))[0]
|
|
506
|
+
check('配音音色下拉能从 /voices 拿到音色G', !!voiceSel, JSON.stringify(walk(tree).filter((n) => n.type === 'select').map((n) => (n.props.children || []).map((o) => o && o.props.value))))
|
|
507
|
+
if (voiceSel) {
|
|
508
|
+
voiceSel.props.onChange({ target: { value: 'g' } })
|
|
509
|
+
await flush()
|
|
510
|
+
tree = renderPanel()
|
|
511
|
+
const previewBtn2 = find(tree, (n) => n.type === 'button' && n.props.children === '立即预览')
|
|
512
|
+
previewBtn2.props.onClick()
|
|
513
|
+
await flush(); await flush(); await flush()
|
|
514
|
+
check('立即预览使用未保存的 draft(voicePack=g)', windowStub.__DSH_STARTUP__ && windowStub.__DSH_STARTUP__.voicePack === 'g', windowStub.__DSH_STARTUP__ && windowStub.__DSH_STARTUP__.voicePack)
|
|
515
|
+
check('preview 合并 draft 的源码标记存在', code.includes('j.config, draft || {}'))
|
|
516
|
+
}
|
|
494
517
|
delete windowStub.__DSH_STARTUP_API__
|
|
495
518
|
previewBtn.props.onClick()
|
|
496
519
|
await flush(); await flush(); await flush()
|
package/test/smoke-host.mjs
CHANGED
|
@@ -132,7 +132,7 @@ check('没有 Config 导出(普通对象不是 schemastery schema)', mod.Con
|
|
|
132
132
|
|
|
133
133
|
console.log('\n[2] apply(ctx):路由与注入')
|
|
134
134
|
mod.apply(ctx)
|
|
135
|
-
check('注册了
|
|
135
|
+
check('注册了 40 条路由(开机动画 29 + 聊天壁纸 11)', ROUTES.size === 40, [...ROUTES.keys()].join(' | '))
|
|
136
136
|
check('注册了 1 个 tapIndex', TAPS.length === 1)
|
|
137
137
|
check('用了 ctx.effect 管理生命周期(开机动画 1 + 壁纸 11)', effects === 12, String(effects))
|
|
138
138
|
|
|
@@ -154,9 +154,9 @@ r = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asse
|
|
|
154
154
|
check('目录穿越被拒 -> 404', r.statusCode === 404, String(r.statusCode))
|
|
155
155
|
r = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/nope.mp3')
|
|
156
156
|
check('不存在的素材 -> 404', r.statusCode === 404, String(r.statusCode))
|
|
157
|
-
for (const f of ['line-1', '
|
|
158
|
-
const rr = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/' + f + '.mp3')
|
|
159
|
-
check('
|
|
157
|
+
for (const f of ['line-1', 'dry-line-1']) {
|
|
158
|
+
const rr = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/' + f + '.mp3')
|
|
159
|
+
check('旧 TTS 素材已退役:' + f + '.mp3 -> 404', rr.statusCode === 404, String(rr.statusCode))
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
console.log('\n[4] 配置读写落盘')
|
|
@@ -165,6 +165,9 @@ let cfg = JSON.parse(r.body)
|
|
|
165
165
|
check('GET config ok', cfg.ok === true)
|
|
166
166
|
check('默认身份名称 = JOYCE MOORE', cfg.config.identity === 'JOYCE MOORE', cfg.config.identity)
|
|
167
167
|
check('默认身份编号 = 0087', cfg.config.identityId === '0087', cfg.config.identityId)
|
|
168
|
+
check('默认配音音色 = F(英文)', cfg.config.voicePack === 'f', cfg.config.voicePack)
|
|
169
|
+
check('默认声音效果 = 机器人(robot)', cfg.config.voiceFx === 'robot', cfg.config.voiceFx)
|
|
170
|
+
check('默认终幕语音延迟 = 550ms', cfg.config.welcomeDelay === 550, cfg.config.welcomeDelay)
|
|
168
171
|
check('返回了配置文件路径', typeof cfg.file === 'string' && cfg.file.startsWith(SMOKE_HOME), cfg.file)
|
|
169
172
|
check('返回了插件版本号(设置面板顶部要显示它)', typeof cfg.version === 'string' && /^\d+\.\d+\.\d+/.test(cfg.version), cfg.version)
|
|
170
173
|
|
|
@@ -433,6 +436,22 @@ console.log('\n[6h] v2.13.12:打开下载文件夹路由存在且只接受 POS
|
|
|
433
436
|
check('GET /open-downloads 返回 405', getOpen.statusCode === 405, String(getOpen.statusCode))
|
|
434
437
|
}
|
|
435
438
|
|
|
439
|
+
console.log('\n[6i] v2.13.19:6 套音色包清单与只读音频路由')
|
|
440
|
+
{
|
|
441
|
+
const vp = await call('exact /dsh-startup/voices', 'GET')
|
|
442
|
+
const vpj = JSON.parse(vp.body)
|
|
443
|
+
check('GET /voices ok', vpj.ok === true)
|
|
444
|
+
check('6 个音色包(f、g–k)', Array.isArray(vpj.packs) && vpj.packs.length === 6, String(vpj.packs && vpj.packs.length))
|
|
445
|
+
check('F 包英文 / K 包日文', vpj.packs[0].id === 'f' && vpj.packs[0].lang === 'en' && vpj.packs[5].id === 'k' && vpj.packs[5].lang === 'ja', JSON.stringify(vpj.packs.slice(0, 1).concat(vpj.packs.slice(-1))))
|
|
446
|
+
check('默认音色 = f', vpj.default === 'f', String(vpj.default))
|
|
447
|
+
const va = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/f/line-1.mp3')
|
|
448
|
+
check('音色F line-1 音频 200 且是 mp3', va.statusCode === 200 && va.body.length > 1000 && (va.body[0] === 0xFF || va.body.slice(0, 3).toString() === 'ID3'), va.statusCode + ' ' + va.body.length)
|
|
449
|
+
const vb = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/f/line-7.mp3')
|
|
450
|
+
check('非法句号 line-7 -> 404', vb.statusCode === 404, String(vb.statusCode))
|
|
451
|
+
const vz = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/z/line-1.mp3')
|
|
452
|
+
check('未注册音色 z -> 404', vz.statusCode === 404, String(vz.statusCode))
|
|
453
|
+
}
|
|
454
|
+
|
|
436
455
|
console.log('\n[7] XSS 边界:身份名称里的尖括号不会逃出内联脚本')
|
|
437
456
|
await call('exact /dsh-startup/config', 'POST', JSON.stringify({ patch: { identity: '</script><img src=x onerror=alert(1)>' } }))
|
|
438
457
|
const payload = inlinePayload(renderRows(before, collectRows()))
|
package/test/smoke-package.mjs
CHANGED
|
@@ -8,7 +8,7 @@ if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
|
|
|
8
8
|
}
|
|
9
9
|
const pkg = JSON.parse(buf.toString('utf8'));
|
|
10
10
|
if (!pkg.version) throw new Error('package.json 缺少 version');
|
|
11
|
-
for (const f of ['lib/index.js', 'lib/splash.js', 'lib/client.js', 'lib/assets/default-background.jpg', 'templates', 'tools/install.ps1', 'tools/update-apply.ps1', 'tools/rollback-version.ps1']) {
|
|
11
|
+
for (const f of ['lib/index.js', 'lib/splash.js', 'lib/client.js', 'lib/assets/default-background.jpg', 'templates', 'tools/install.ps1', 'tools/update-apply.ps1', 'tools/rollback-version.ps1', 'lib/assets/voices/manifest.json']) {
|
|
12
12
|
if (!fs.existsSync(path.join(root, f))) throw new Error('缺少 ' + f);
|
|
13
13
|
}
|
|
14
14
|
const installPs1 = fs.readFileSync(path.join(root, 'tools/install.ps1'), 'utf8');
|
|
@@ -24,4 +24,7 @@ const rollbackPs1 = fs.readFileSync(path.join(root, 'tools/rollback-version.ps1'
|
|
|
24
24
|
for (const needle of ['TargetPkg', 'CheckOnly', 'DSH_BOOT_SUITE_ROLLBACK', '回滚日志']) {
|
|
25
25
|
if (!rollbackPs1.includes(needle)) throw new Error('rollback-version.ps1 缺少关键逻辑:' + needle);
|
|
26
26
|
}
|
|
27
|
+
for (const f of ['line-1.mp3', 'line-6.mp3', 'dry-line-1.mp3']) {
|
|
28
|
+
if (fs.existsSync(path.join(root, 'lib/assets', f))) throw new Error('旧 TTS 配音素材未删除:' + f);
|
|
29
|
+
}
|
|
27
30
|
console.log('ok package.json version=' + pkg.version + ',无 BOM,关键文件齐全(含更新前回滚 checkpoint)');
|
package/test/smoke-splash.mjs
CHANGED
|
@@ -327,11 +327,18 @@ console.log('\n[8] 终幕用真声欢迎语(来自 Video Project 3 的 line-6
|
|
|
327
327
|
{
|
|
328
328
|
const src = fs.readFileSync(BUNDLE, 'utf8')
|
|
329
329
|
check('全程没有 voice.tts( 调用(不再用 TTS 念白)', !/voice\.tts\(/.test(src))
|
|
330
|
-
check('终幕挂了 line-6 素材', /voice\.line\(null, null, 'line-6'\)/.test(src))
|
|
330
|
+
check('终幕挂了 line-6 素材', /voice\.line\(null, null, 'line-6'\)/.test(src))
|
|
331
|
+
check('终幕欢迎语按 welcomeDelay 延迟后播放', /C\.welcomeDelay/.test(src) && /welcomeDelay = Math\.max\(0, Math\.min\(3000/.test(src))
|
|
331
332
|
check('揭幕前会等欢迎语播完', /\.then\(function \(\) \{ return welcome \}\)/.test(src))
|
|
332
333
|
check('预加载包含 line-6', /ensureAsset\('line-6'\)/.test(src))
|
|
333
334
|
check('五步仍然各自挂着真声素材', (src.match(/audio: 'line-/g) || []).length === 5)
|
|
334
335
|
check('总共 6 份人声素材被引用', (src.match(/'line-[1-6]'/g) || []).length >= 6)
|
|
336
|
+
check('配音音色可切换:voicePack 为 f–k 时走 voice:<pack>:line-N 路由',
|
|
337
|
+
/C\.voicePack/.test(src) && /'voice:' \+ pack \+ ':' \+ asset/.test(src)
|
|
338
|
+
&& /tplAssets && tplAssets\[asset\]/.test(src))
|
|
339
|
+
check('5 套声音效果预设都在,播放链路按 C.voiceFx 叠加',
|
|
340
|
+
['ai-mech', 'robot', 'radio', 'oldtv', 'dark'].every((k) => src.includes("'" + k + "':"))
|
|
341
|
+
&& /playAssetAndWait\(useAsset, 0\.85, isGlobalLine \? fx : 'none'\)/.test(src))
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
console.log('\n[9] 模板:分幕可以自定义(1~24 幕,缺字段回落内置)')
|
|
@@ -463,7 +463,7 @@ ok(intervals.every((t) => t.cleared) || intervals.length === 0, '旧的 interval
|
|
|
463
463
|
intervals.map((t) => (t.cleared ? 'cleared' : 'live')).join(','))
|
|
464
464
|
|
|
465
465
|
console.log('\n=== ⑦ v2.13.0 测试钩子 / BUILD ===')
|
|
466
|
-
ok(mod.__BUILD === 'cw-
|
|
466
|
+
ok(mod.__BUILD === 'cw-42', '__BUILD = cw-42(排查浏览器跑的是不是新包)', String(mod.__BUILD))
|
|
467
467
|
ok(mod.__VIDEO_ID === 'dsh-cw-video', '__VIDEO_ID = dsh-cw-video', String(mod.__VIDEO_ID))
|
|
468
468
|
ok(typeof mod.__startShuffle === 'function', '__startShuffle 导出')
|
|
469
469
|
ok(typeof mod.__stopShuffle === 'function', '__stopShuffle 导出')
|
|
@@ -163,10 +163,10 @@ try {
|
|
|
163
163
|
sortKeys(r.json) === 'config,image,imageOk,imageSource,ok,video', sortKeys(r.json))
|
|
164
164
|
check('/config ok=true', r.json && r.json.ok === true, r.json && String(r.json.ok))
|
|
165
165
|
const CONFIG_KEYS = ['banner', 'bannerOpacity', 'fallbackDark', 'fallbackLight', 'image', 'imageFit',
|
|
166
|
-
'sidebarDark', 'sidebarLight', 'veilBlur', 'veilDarkA', 'veilDarkB', 'veilLightA', 'veilLightB',
|
|
166
|
+
'sidebarDark', 'sidebarLight', 'veilBlur', 'veilDarkA', 'veilDarkB', 'veilLightA', 'veilLightB', 'colorMode',
|
|
167
167
|
/* v2.13.0 循环视频 */
|
|
168
168
|
'bgMode', 'videoMode', 'videoId', 'videoRoots', 'videoShuffleMinutes']
|
|
169
|
-
check('config 恰好
|
|
169
|
+
check('config 恰好 19 个键(12 旧 + 5 循环视频 + 1 镜头模糊 + 1 色彩模式)',
|
|
170
170
|
sortKeys(r.json && r.json.config) === CONFIG_KEYS.slice().sort().join(','), sortKeys(r.json && r.json.config))
|
|
171
171
|
const DEAD_KEYS = ['video', 'mode', 'placement', 'loopStart', 'loopEnd', 'oncePerSession',
|
|
172
172
|
'waitForBoot', 'fadeOutMs', 'objectFit', 'opacity', 'playbackRate', 'fps']
|
|
@@ -177,6 +177,7 @@ try {
|
|
|
177
177
|
check('/config 的 imageSource = builtin', r.json && r.json.imageSource === 'builtin', String(r.json && r.json.imageSource))
|
|
178
178
|
check('★ v2.13.4 config.imageFit 默认 cover', r.json && r.json.config && r.json.config.imageFit === 'cover', String(r.json && r.json.config && r.json.config.imageFit))
|
|
179
179
|
check('★ v2.13.11 config.veilBlur 默认 0(不模糊)', r.json && r.json.config && r.json.config.veilBlur === 0, String(r.json && r.json.config && r.json.config.veilBlur))
|
|
180
|
+
check('★ v2.13.17 config.colorMode 默认 color', r.json && r.json.config && r.json.config.colorMode === 'color', String(r.json && r.json.config && r.json.config.colorMode))
|
|
180
181
|
const v0 = (r.json && r.json.video) || {}
|
|
181
182
|
check('★ /config.video 默认值:enabled=false / mode=single / shuffleMinutes=0 / selectedId 空 / count=0 / current=null',
|
|
182
183
|
v0.enabled === false && v0.mode === 'single' && v0.shuffleMinutes === 0 &&
|
|
@@ -228,6 +229,10 @@ try {
|
|
|
228
229
|
check('★ veilBlur 下限夹到 0', r.json && r.json.applied && r.json.applied.veilBlur === 0, JSON.stringify(r.json && r.json.applied))
|
|
229
230
|
r = await postJson('exact /chat-wallpaper/set', { veilBlur: 'abc' })
|
|
230
231
|
check('★ 非法 veilBlur 被忽略', r.json && r.json.applied && !('veilBlur' in r.json.applied), JSON.stringify(r.json && r.json.applied))
|
|
232
|
+
r = await postJson('exact /chat-wallpaper/set', { colorMode: 'mono' })
|
|
233
|
+
check('★ v2.13.17 /set 接受 colorMode=mono', r.json && r.json.applied && r.json.applied.colorMode === 'mono', JSON.stringify(r.json && r.json.applied))
|
|
234
|
+
r = await postJson('exact /chat-wallpaper/set', { colorMode: 'rainbow' })
|
|
235
|
+
check('★ 非法 colorMode 被忽略', r.json && r.json.applied && !('colorMode' in r.json.applied), JSON.stringify(r.json && r.json.applied))
|
|
231
236
|
r = await callJson('exact /chat-wallpaper/config')
|
|
232
237
|
check('再读 /config:banner = B', r.json && r.json.config && r.json.config.banner === 'B', r.json && r.json.config && r.json.config.banner)
|
|
233
238
|
const cfgFile = path.join(HOME, 'chat-wallpaper.json')
|
package/lib/assets/line-1.mp3
DELETED
|
Binary file
|
package/lib/assets/line-2.mp3
DELETED
|
Binary file
|
package/lib/assets/line-3.mp3
DELETED
|
Binary file
|
package/lib/assets/line-4.mp3
DELETED
|
Binary file
|
package/lib/assets/line-5.mp3
DELETED
|
Binary file
|
package/lib/assets/line-6.mp3
DELETED
|
Binary file
|