@dsh-gis/dsh-video-studio-skill 0.6.0 → 0.8.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +951 -0
  2. package/README.md +89 -21
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/index.js +23 -7
  5. package/package.json +7 -3
  6. package/schemas/film.schema.json +235 -4
  7. package/skills/video-studio/SKILL.md +299 -27
  8. package/skills/video-studio/assets/film.json +7 -14
  9. package/skills/video-studio/assets/golden_frames.json +65 -0
  10. package/skills/video-studio/assets/scenes.py +115 -10
  11. package/skills/video-studio/assets/web/engine.js +81 -3
  12. package/skills/video-studio/assets/web/film.js +249 -101
  13. package/skills/video-studio/assets/web/index.html +9 -2
  14. package/skills/video-studio/assets/web/qa_text.js +36 -8
  15. package/skills/video-studio/references/audio-variation.md +159 -0
  16. package/skills/video-studio/references/audio.md +67 -5
  17. package/skills/video-studio/references/change-impact.md +2 -2
  18. package/skills/video-studio/references/quality-gates.md +25 -3
  19. package/skills/video-studio/references/style-packs.md +7 -3
  20. package/skills/video-studio/references/v08-quality.md +256 -0
  21. package/skills/video-studio/references/web-engine.md +19 -2
  22. package/skills/video-studio/references/word-timeline.md +147 -0
  23. package/skills/video-studio/requirements.txt +16 -1
  24. package/skills/video-studio/scripts/filmcheck.py +202 -12
  25. package/skills/video-studio/scripts/filmkit.py +792 -56
  26. package/skills/video-studio/scripts/gates.py +161 -14
  27. package/skills/video-studio/scripts/preflight.py +774 -0
  28. package/skills/video-studio/scripts/qa_frames.py +154 -23
  29. package/skills/video-studio/scripts/qaasr.py +1563 -0
  30. package/skills/video-studio/scripts/qavmaf.py +1442 -0
  31. package/skills/video-studio/scripts/render.py +1134 -78
  32. package/skills/video-studio/scripts/scorekit.py +305 -76
  33. package/skills/video-studio/scripts/selfcheck.py +576 -2
  34. package/skills/video-studio/scripts/sfxkit.py +262 -44
  35. package/skills/video-studio/scripts/stagewatch.py +159 -36
  36. package/skills/video-studio/scripts/stylekit.py +137 -18
  37. package/skills/video-studio/scripts/voicekit.py +23 -7
  38. package/skills/video-studio/scripts/webengine.py +1186 -85
  39. package/tools/audio_similarity.py +279 -0
  40. package/tools/ci_check.py +310 -59
  41. package/tools/estimate.py +251 -61
  42. package/tools/golden_frames.py +664 -0
  43. package/tools/selftest_noscipy.py +70 -0
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # @dsh-gis/dsh-video-studio-skill
2
2
 
3
- ![version](https://img.shields.io/badge/version-0.6.0-blue) ![license](https://img.shields.io/badge/license-MIT-green)
3
+ ![version](https://img.shields.io/badge/version-0.8.1-blue) ![license](https://img.shields.io/badge/license-MIT-green)
4
4
 
5
5
  **DeepSeek Harness 技能插件:用代码从零生成中文视频。**
6
6
 
7
7
  写剧本 → edge-tts 旁白 → **实测时长**排时间轴 → **提示点**驱动配乐与音效 → PIL 或浏览器逐帧渲染 →
8
- ffmpeg 编码 → 采样级混音 → **四层质检**(容器 / 信号 / 时间 / 页面)。
8
+ ffmpeg 编码 → 采样级混音 → **四层质检**(容器 / 信号 / 时间 / 页面)+ **v0.8 的四个新闸**
9
+ (渲前资源预检 `G0` / 词级时间戳 `G10` / 全片 VMAF `G11` / ASR 回读 `G12`)。
9
10
  零素材、可复现、可断点续跑、可脚本化验收;装成插件后,整片生产流程随技能一起进入每个会话。
10
11
 
11
12
  > 源自 `dsh-v` 资产包的 `02-技能包-video-studio`(817 个 DSH 会话的考古分析 + 5 套现存工程逆向 + 本机实测)。
@@ -28,6 +29,35 @@ ffmpeg 编码 → 采样级混音 → **四层质检**(容器 / 信号 / 时
28
29
  > 顺手修掉一个**已发布版本里的真 bug**:`--jobs >1` 并行渲染因主题色元组被 JSON 变成 list 而直接崩溃
29
30
  > ——也就是 README 下面教的"长片分块并行"此前根本跑不通。
30
31
  > 详见 [CHANGELOG.md](CHANGELOG.md)、SKILL.md §7.0 与 references/change-impact.md。
32
+ > **v0.7.0(声音也不再一片一样)**:内容指纹从 seed 推进到**结构层**——和弦替换、进行档位、动机偏移、
33
+ > 鼓型 ghost 都随内容变化;音效 seed 改内容派生并跟随配乐调性(`in_key`);段落边界前 2.2 s 的
34
+ > whoosh/riser 自动 -3 dB,避免与配乐过门撞车;duck 按风格加深到 3.0–5.5 dB。legacy 路径逐样本不变。
35
+ > **v0.7.1(旁白优先)**:配乐基线从 -1.5 降到 **-12 dB**,有人声处再按包络压配乐 6 dB / 音效 3 dB,
36
+ > 混音后按台词活跃区实测旁白/背景差,**不足 12 dB 自动补压背景**(`guard_snr_db`,最多 30 dB)。
37
+ > 实测(真实素材):旧混音 -4.2 dB → 新混音 12.0 dB,`audio_similarity --strict` D 组 12.9 dB;
38
+ > selfcheck 34 → 38 条,其中第 35–38 条专盯这条链路。
39
+ > 详见 [CHANGELOG.md](CHANGELOG.md) 与 SKILL.md §5.6 / §5.7、references/audio-variation.md。
40
+ > **v0.8.0(把"看得见"变成"抓得住")**:四个方向——① **词级时间轴**:edge-tts 的 WordBoundary 事件落
41
+ > `work/words.json`(4 句 / 64 词 / 末词偏差 0.09 s),字幕改按词折行,cue 可写词锚点 `"l03:w2+0.1"`;
42
+ > ② **四道新门禁**:渲前资源预检 `G0`(缺字形/坏音频在渲第一帧前 fail,< 1 s)、词级时间戳 `G10`、
43
+ > 全片 VMAF/SSIM `G11`(参考帧由场景函数**实时重渲**,不是成片抽帧;29 s 片 VMAF 97.22 / 14.2 s)、ASR 回读 `G12`
44
+ > (成片音轨转文字对账,0.72 以下才 fail,8.7 s/片);③ **画幅/帧率参数化**(只做 16:9 横屏,
45
+ > 内部恒 1080p、编码时下采样,720p 已实测全门通过)、**批量** `--batch`(≤100 条、开跑前全量预检、
46
+ > 单条失败不阻断)、**预览** `--preview N`(前 N 秒出 mp4+gif)、**运行版本化** `--run-id`(`work/runs/<runId>/`);
47
+ > ④ web 引擎跟随风格包(`window.__STYLE__` 注入,`style=None` 时逐字节等于 v0.7)+ 金帧像素回归
48
+ > `tools/golden_frames.py`。**代价要说清**:29 s 模板片全链从约 28 s 涨到 **47.3 s**,其中 `qa` **23.9 s**
49
+ > 已经**超过渲染本身**(`frames` 12.5 s);`estimate` 预演也从 28.4 s 变成 **58.8–108.2 s**(vmaf 31–50 s 是瓶颈)。
50
+ > v0.8 之后"出片时间"不再只是渲染时间。selfcheck 43 → **51 条**。
51
+ > 详见 [CHANGELOG.md](CHANGELOG.md) 与 SKILL.md §6.1 / §7.5、references/word-timeline.md、references/v08-quality.md。
52
+ > **v0.8.1(全项目深审,无新功能)**:把 14 个模块 / 5 个前端文件 / 12 篇 references / 仓库工具链
53
+ > 全部过一遍,修掉 **60+ 处**「看着没问题、其实没在查」。举几个真会伤到交付的:
54
+ > ① **VMAF 参考帧与成片的字幕折行口径不一致**(渲染传 `words`、参考帧没传)——近静态画面
55
+ > 从 **99.95 掉到 8.55**,同源好片被判 fail;② `--stage style/stills/frames/mux` 的门禁 fail
56
+ > **没进退出码**(stdout 打 [FAIL]、进程仍 exit 0);③ `webengine` 在 `style=None` + 720p 时
57
+ > **静默输出 1080p 原画的左上角裁切**(逐像素相同、标题/HUD 全丢、exit 0);
58
+ > ④ `visual.layout` 与 `qa.vmaf.sample_step` 写了**完全没人读**;⑤ `selfcheck` 第 51 条把金帧
59
+ > 的 skipped 当 PASS(**这条门禁从没真的比过一帧**);⑥ `ci_check` 把"跳过(没装 node)"记成 PASS。
60
+ > 详见 [CHANGELOG.md](CHANGELOG.md) 的 0.8.1 段(含行为变化表与升级须知)。
31
61
 
32
62
  ## 安装
33
63
 
@@ -60,23 +90,33 @@ mv ~/.dsh/skills/video-studio ~/.dsh/backup/video-studio.bak-$(date +%Y%m%d)
60
90
  <p align="center"><img src="docs/demo-pil-sheet.png" width="100%" alt="默认模板片联系表:开场 / 正文卡片 / 正文卡片 / 收尾"></p>
61
91
 
62
92
  上面四帧是用仓库自带的 `assets/film.json` + `assets/scenes.py` 从零跑一遍 `python render.py` 的
63
- `work/qa/sheet.png`,未经任何修图。配套实测(同一台机器):
93
+ `work/qa/sheet.png`,未经任何修图。配套实测(**作者机器** Windows 11 / OSGeo4W Python 3.12,
94
+ v0.6 时期口径;TTS 实测时长与 AAC 编码会让这些数字在小范围内浮动,换机请按 SKILL.md §14 重测):
64
95
  **时长 30.10 s / 1920×1080 / 30 fps / h264+aac 48 kHz / -16.0 LUFS / 真峰 -1.2 dBTP / 黑帧 0 /
65
96
  帧级深检:闪烁 0、空窗 0、画面静止 0**(音画同步在 PIL 路线上需要写 `visual_at` 才会真的查,见 SKILL.md §7)。
66
97
 
98
+ 怎么复现:按 [SKILL.md §1](skills/video-studio/SKILL.md) 的五步上手,把自带模板拷到**空目录**里跑一遍即可
99
+ (`cp assets/film.json assets/scenes.py` + 拷 `scripts/*.py` → `python render.py`)。数字每次都会差一点:
100
+ TTS 实测时长、AAC 编码都会让真峰/响度/锐度浮动,**别 chasing 小数点**。本机(另一台)复测同模板得到的是
101
+ 29.00 s / -16.5 LUFS / -1.4 dBTP / 锐度比 1.01(2026-11-10 另一台 Windows 机器复测)——
102
+ **不同机器、不同 TTS 结果,两组都对,别混着引用**。
103
+
67
104
  ## 效果:同一个模板,八种气质(v0.5)
68
105
 
69
106
  同一份文字,只改 `"style"`(画风 / 配乐 / 旁白 / 节奏一起变):
70
107
 
71
108
  <p align="center"><img src="docs/styles/sheet-8-styles.png" width="100%" alt="8 套风格对比:科技深蓝 / 杂志暖白 / 手绘纸感 / 活力撞色 / 极简黑白 / 电影青橙 / 暖灰薄荷 / 深黑鎏金"></p>
72
109
 
73
- 三份**不同题材**的短片(全流程真跑:TTS → 排轴 → 配乐 → 音效 → 混音 → 逐帧 → 编码 → 质检):
110
+ 三份**不同题材**的短片(全流程真跑:TTS → 排轴 → 配乐 → 音效 → 混音 → 逐帧 → 编码 → 质检;数据见
111
+ `docs/styles/demo-results.json`,是 v0.5 时期的实测快照,片源脚本未随仓库分发,**不可直接复现**;
112
+ **别把这张表当成"现在跑出来就是这样"**——BPM/调性由内容指纹从风格梯度里选(v0.7 起指纹还参与配乐结构),
113
+ 同一题材换一版正文就可能落到别的 BPM/调性,当前风格解析结果请以 `--stage style` 的打印为准):
74
114
 
75
115
  | 题材 | 自动选中的风格 | 配乐 | 旁白声线 | 质检 |
76
116
  |---|---|---|---|---|
77
117
  | AI 大模型的三年 | tech-deep 科技深蓝 | 小调 116BPM / Em | 青年开场 → 男声正文 → 女声引用 | PASS(17.3s,-16.0 LUFS) |
78
- | 手工木作的一天 | paper 手绘纸感 | 大调 84BPM / C | 女声 + 软糯女声正文 | PASS(19.9s) |
79
- | 深夜学习指南 | lo-fi 暖灰薄荷 | 大调 84BPM / G | 温柔女声 + 女声开场 | PASS(20.1s) |
118
+ | 手工木作的一天 | paper 手绘纸感 | 大调 88BPM / G | 女声 + 软糯女声正文 | PASS(19.9s) |
119
+ | 深夜学习指南 | lo-fi 暖灰薄荷 | 大调 84BPM / F | 温柔女声 + 女声开场 | PASS(20.1s) |
80
120
 
81
121
  <p align="center"><img src="docs/styles/sheet-ai.png" width="32%" alt="AI 题材"><img src="docs/styles/sheet-craft.png" width="32%" alt="手作题材"><img src="docs/styles/sheet-night.png" width="32%" alt="深夜题材"></p>
82
122
 
@@ -94,10 +134,14 @@ mkdir -p myfilm && cd myfilm
94
134
  cp <SKILL_DIR>/assets/film.json . # 剧本模板("style": "auto")
95
135
  cp <SKILL_DIR>/assets/scenes.py . # 场景模板
96
136
  cp <SKILL_DIR>/scripts/*.py . # filmkit/stylekit/voicekit/render/selfcheck + sfxkit/scorekit/qa_frames/webengine
137
+ # v0.8 还含 preflight/qaasr/qavmaf(可选:缺了对应门禁记 skipped)
97
138
  cp -r <SKILL_DIR>/assets/web ./web # 只在走浏览器引擎时需要
98
139
 
99
- python selfcheck.py # 32 条环境/回归断言(20 秒,不联网)
100
- python <REPO>/tools/estimate.py --script film.json # v0.6:开渲前先算耗时/磁盘/分块
140
+ python selfcheck.py # 51 条环境/回归断言(20 秒内,不联网)
141
+ python <REPO>/tools/estimate.py --script film.json # v0.6:开渲前先算耗时/磁盘/分块(仓库/包里的工具,
142
+ # 不在 skills/video-studio/ 内,quickstart 拷出来的目录没有它)
143
+ # v0.8:已含 preflight/vmaf/asr 三段;29 s 模板片
144
+ # 预演 total 28.4 s -> 37.5-83.0 s(区间上界,实测 47.3 s)
101
145
  python render.py --stage stills --stills "l01:3,l02:5" # 母版预览(缺 tts/layout 会自动补跑)
102
146
  python render.py --script film.json --scenes scenes.py --out out/film.mp4 --work work
103
147
  cat out/QC-REPORT.md # v0.6:机器可读交付结论(pass/fail/skipped)
@@ -122,10 +166,11 @@ tts → layout → cues → score → sfx → audio → frames → mux → qa
122
166
 
123
167
  | 想要 | 怎么做 |
124
168
  |---|---|
125
- | 让 PIL 路线的音画同步门禁真的生效 | cue 加 `"visual_at": 绝对秒`;CI 里再加 `QA_REQUIRE_SYNC=1` 把"没查"变成失败 |
169
+ | 让 PIL 路线的音画同步门禁真的生效 | cue 加 `"visual_at": 绝对秒`;再把 `QA_REQUIRE_SYNC=1` 加进你自己的交付流水线(本仓库的 workflow 没设它) |
126
170
  | 浏览器渲染要跨会话字节级可复现 | `--deterministic`(或 `WEBENGINE_DETERMINISTIC=1`),代价约 +22%/帧 |
127
171
 
128
- `<SKILL_DIR>` 是 `skill` 工具加载时回报的 resourceBase 目录(`.../skills/video-studio/`)。
172
+ `<SKILL_DIR>` 是 `skill` 工具加载时回报的 resourceBase 目录(`.../skills/video-studio/`);
173
+ `<REPO>` 是包根(仓库根,或装好后 profile 里的 `node_modules/@dsh-gis/dsh-video-studio-skill/`),即 `<SKILL_DIR>` 往上两级——`tools/`、`schemas/` 都在 `<REPO>` 下,`cp` 到 `myfilm/` 后那里**没有** `tools/`。
129
174
 
130
175
  分阶段(v0.2 起**每个 stage 只做自己的事**):`--stage style | tts | layout | cues | score | sfx | audio | frames | mux | qa | stills | accept | explain`。
131
176
 
@@ -137,6 +182,15 @@ tts → layout → cues → score → sfx → audio → frames → mux → qa
137
182
  | 改完东西不知道要重跑什么 | `--stage explain`(机器判断过时阶段 + 给最小重跑命令),表见 [references/change-impact.md](skills/video-studio/references/change-impact.md) |
138
183
  | 规整 `film.json` 避免静默失效 | `python skills/video-studio/scripts/filmcheck.py film.json`(`holds`→提示写成 `hold`) |
139
184
 
185
+ **v0.8 的四个新开关**(前三个会改变门禁结果,第四个只是看片更快):
186
+
187
+ | 想要 | 怎么做 |
188
+ |---|---|
189
+ | 交付 720p / 换帧率 | `film.json` 写 `"video": {"width":1280,"height":720,"fps":24}`(只做 16:9 横屏、只能下采样;720p 已实测全门通过) |
190
+ | 批量出片 | `python render.py --batch films.json`(≤100 条、开跑前全量预检、单条失败不阻断、末尾 `batch-report.json`) |
191
+ | 只渲前 N 秒给人看 | `--preview 15`(出 `out/preview/*.mp4` + `.gif`,不写交付账本) |
192
+ | 让 QA 快一点 | `--vmaf sample`(每 5 帧取 1,实测 8.0 s vs 全片 14.2 s,VMAF 只差 0.34)或 `--vmaf off`(G11 记 skipped) |
193
+
140
194
  长片:`--stage frames --jobs 4`(按绝对帧号分块并行)+ `--resume`(中断只补缺块)。
141
195
 
142
196
  ## 这个技能解决什么
@@ -164,33 +218,45 @@ cordis.patch.yml 组合包 patch(insert: video-studio)
164
218
  icon.svg / locale/*.json 插件页展示元数据
165
219
  skills/video-studio/
166
220
  SKILL.md 主线:上手 / 八阶段 / 引擎选型 / 画质 / 声音 / 提示点 / 停留窗口 / 质检 / 故障速查
167
- references/ pipeline · scene-authoring · audio · sfx-design · web-engine · frame-qa
168
- quality-gates · change-impact · style-packs · pitfalls
221
+ references/ pipeline · scene-authoring · audio · audio-variation · sfx-design · web-engine
222
+ frame-qa · quality-gates · change-impact · style-packs · pitfalls
223
+ word-timeline(v0.8)· v08-quality(v0.8)
169
224
  scripts/filmkit.py 生产内核(字体/排版/字幕/音频总线/CueBus/TTS/时间轴/编码/质检)
170
225
  scripts/gates.py v0.6 门禁账本 / QC 报告渲染 / 人工签核(绑内容指纹)
171
226
  scripts/stagewatch.py v0.6 阶段新鲜度(work/manifest.json + --stage explain)
172
227
  scripts/filmcheck.py v0.6 film.json 契约校验(未知键/枚举/跨字段)
173
- scripts/render.py 编排器(13 个 stage、pil|web 双引擎、分块并行、断点续跑、母版预览、门禁账本)
174
- scripts/selfcheck.py 回归自检(32 条"曾真的坏过"的断言)
228
+ scripts/render.py 编排器(13 个 stage、pil|web 双引擎、分块并行、断点续跑、母版预览、门禁账本;
229
+ v0.8 增 --batch / --preview / --run-id / --vmaf / --no-preflight)
230
+ scripts/preflight.py v0.8 渲染前资源预检(字形 / TTS 音频 / 字体 / web 入口页,门禁 G0)
231
+ scripts/qavmaf.py v0.8 全片 VMAF/SSIM(参考帧由场景函数实时重渲,门禁 G11)
232
+ scripts/qaasr.py v0.8 ASR 回读(成片音轨转文字对账,门禁 G12)
233
+ scripts/selfcheck.py 回归自检(51 条"曾真的坏过"的断言)
175
234
  scripts/sfxkit.py 物理建模音效库(24 配方,scipy 可选)
176
235
  scripts/scorekit.py 程序化配乐(和弦表 / 分段能量 / 吸气)
177
236
  scripts/webengine.py 浏览器逐帧渲染(CDP 截图、分块并行、崩溃重开、cue 导出、文字质检)
178
237
  scripts/qa_frames.py 帧级深检(单帧闪烁 / 画面空窗 / 画面静止 / 音画同步 / 亮度响度曲线)
179
238
  requirements.txt Python 依赖(必需 / 强烈建议 / 可选三档,带注释)
180
- assets/film.json 剧本模板(含 hold / bpm / music / sfx.cues)
239
+ assets/film.json 剧本模板(含 bpm / music / sfx.cues / 逐句 hold;顶层 hold 可选,缺省用风格 pacing)
181
240
  assets/scenes.py 场景模板(开场 / 正文卡片 / 收尾 + cues() 钩子)
182
- assets/web/ 浏览器页面模板(原生 Canvas2D 确定性时间轴)
241
+ assets/golden_frames.json v0.8 金帧基线(3 帧 sha256 + 字体/环境指纹,改绘制代码后重录)
242
+ assets/web/ 浏览器页面模板(原生 Canvas2D 确定性时间轴;v0.8 读 window.__STYLE__)
183
243
  tools/ci_check.py 仓库级体检(12 项;语法/JSON/依赖白名单/打包卫生/文档引用/film 契约,不需 ffmpeg)
184
- tools/estimate.py 开渲前成本预演(帧数/耗时/磁盘/分块建议,--json 可机读)
244
+ tools/estimate.py 开渲前成本预演(帧数/耗时/磁盘/分块建议,--json 可机读;v0.8 含 preflight/vmaf/asr)
245
+ tools/golden_frames.py v0.8 金帧像素回归(--write 重录 / --check 校验;同环境像素变了 = fail)
246
+ tools/audio_similarity.py 音频差异/相似度验收(--strict 阈值判定,改音频代码后必跑)
247
+ tools/selftest_noscipy.py 无 scipy 兜底路径自检(CI 用;屏蔽 scipy 后跑 sfxkit/scorekit 自检)
185
248
  schemas/film.schema.json film.json 的契约(draft-07 子集)
186
249
  .github/workflows/ci.yml CI:repo 体检(ubuntu+windows)+ 技能自检(ubuntu)
187
250
  ```
188
251
 
189
252
  ## 环境前提
190
253
 
254
+ **插件运行**:Node.js ≥ 20(`package.json` 的 `engines`;这是 DSH 加载插件用的运行时,不必另装)。
191
255
  **必需**:ffmpeg + Python 的 numpy / Pillow / edge-tts。
192
256
  **可选**:cv2(编码锐度比)、scipy(音效滤波加速,缺了走 FFT 卷积)、
193
- playwright + Chromium/Edge(只在 `--engine web` 时需要)。
257
+ playwright + Chromium/Edge(只在 `--engine web` 时需要)、
258
+ faster-whisper(v0.8 的 ASR 回读门禁 G12;没装会降级到 ffmpeg whisper 滤镜,仍不可用则记 skipped)。
259
+ v0.8 的 G11(全片 VMAF)不需要新依赖,但要求 ffmpeg 构建**带 libvmaf**(9.0.2 full_build 有)。
194
260
  不需要 moviepy / pydub / soundfile / librosa / manim——本技能一行都不 import 它们。
195
261
 
196
262
  依赖清单有机器可读的一份,别再手抄:
@@ -219,9 +285,11 @@ peer 范围 `@deepseek-ai/dsh-skill-filesystem >=0.1.0-rc.6 <0.3.0-0`,覆盖 D
219
285
  ## 仓库自检(给贡献者与 CI 用,不是给用户用的)
220
286
 
221
287
  ```bash
222
- python tools/ci_check.py -v # 12 项:py 语法 / node --check / JSON / 技能清单 / 依赖白名单 /
223
- # npm pack 打包卫生 / 文档路径引用 / Python 3.9 语法 /
224
- # Markdown 围栏平衡 / CI workflow 语法 / 单文件体积 /
288
+ python tools/ci_check.py -v # 12 项:py 语法(含 tools/,SyntaxWarning 记失败)/ node --check /
289
+ # JSON + 版本号一致性(package.json vs README 徽章)/ 技能清单 /
290
+ # 依赖白名单 / npm pack 打包卫生 / 文档路径 + 正文相对链接 /
291
+ # Python 3.9 语法 / Markdown 围栏(状态机)/ CI workflow
292
+ # (裸冒号 + run 里的 python 脚本是否存在)/ 单文件体积 /
225
293
  # film.json 契约(调 filmcheck.py,规则只留一份)
226
294
  ```
227
295
 
@@ -30,6 +30,7 @@
30
30
  | opencv-python(cv2) | 编码锐度比(Laplacian 方差) | Apache-2.0 | 可选(缺了跳过锐度门禁) |
31
31
  | scipy | sfxkit 的时间域滤波加速 | BSD-3-Clause | 可选(缺了走 FFT 卷积兜底) |
32
32
  | playwright | web 引擎驱动 Chromium/Edge | Apache-2.0 | 可选(只在 `--engine web` 时需要) |
33
+ | faster-whisper | ASR 回读门禁(G12)把成片音轨转回文字 | MIT(模型权重另有许可,随模型分发) | 可选(缺了降级到 ffmpeg whisper 滤镜 / skipped) |
33
34
  | Chromium / Microsoft Edge | web 引擎的渲染内核 | BSD-3-Clause / 微软条款 | 可选 |
34
35
 
35
36
  **字体**:PIL 引擎使用系统自带中文字体(Windows 的 `msyh.ttc`/`msyhbd.ttc` 等),
package/index.js CHANGED
@@ -31,11 +31,12 @@ export const inject = ["skills"];
31
31
  /**
32
32
  * 注册 bundled 技能根。
33
33
  *
34
- * 三件事任何一件不成立都只打日志、不抛错——插件本身没有"必须成功"的业务,
34
+ * 四件事任何一件不成立都只打日志、不抛错——插件本身没有"必须成功"的业务,
35
35
  * 挂不上技能是降级,不是崩溃:
36
36
  * 1. 找不到包内 SKILL.md(打包漏了文件 / 被裁剪)
37
37
  * 2. 运行时没有 ctx.skills(profile 没装技能注册表)
38
38
  * 3. 可选 peer 没装(只装了本插件)
39
+ * 4. provider 自己抛错(版本不匹配 / 目录无权限 / 注册表已占用)
39
40
  *
40
41
  * @param ctx - 携带 skills 服务的 Cordis 上下文。
41
42
  */
@@ -63,12 +64,27 @@ export async function apply(ctx) {
63
64
  return;
64
65
  }
65
66
 
66
- applyFilesystemProvider(ctx, {
67
- providerName: "video-studio",
68
- includeDefaultRoots: false,
69
- bundledSkillDir,
70
- watch: false,
71
- });
67
+ // 第 4 件可能失败的事:provider 自己抛错(版本不匹配、目录无权限、注册表已占用……)。
68
+ // 这里必须兜住:插件行的加载失败会让整行从 profile 里消失,而"挂不上技能"本来只是个降级。
69
+ // (v0.7.1 之前没兜:provider 一抛错 apply 就 reject,而且一行日志都没有——stub 实测复现过。)
70
+ try {
71
+ // 必须 await:provider 的 apply 是 Cordis 插件函数,允许返回 Promise。
72
+ // 不 await 时这里的 try/catch 只能拦住同步 throw,异步 reject 会变成
73
+ // unhandledRejection(Node >=15 默认让进程退出),"挂不上技能只是降级"就变成崩进程。
74
+ await applyFilesystemProvider(ctx, {
75
+ providerName: "video-studio",
76
+ includeDefaultRoots: false,
77
+ bundledSkillDir,
78
+ watch: false,
79
+ });
80
+ } catch (error) {
81
+ warn(
82
+ ctx,
83
+ `挂载 bundled 技能根失败(本次只是空行,不影响 profile 里其它插件):` +
84
+ `${error?.code ?? error?.message ?? error}` +
85
+ `;检查 ${PROVIDER_PKG} 的版本是否满足 peer 范围,以及 ${bundledSkillDir} 是否可读`
86
+ );
87
+ }
72
88
  }
73
89
 
74
90
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-gis/dsh-video-studio-skill",
3
- "version": "0.6.0",
4
- "description": "Video Studio skill plugin for DeepSeek Harness: style-pack driven code-generated Chinese videos — one style id decides palette/typography/music style/voice persona/pacing, measured TTS timeline, deterministic PIL/browser rendering, programmatic SFX and score, cue-driven sound sync, ffmpeg encoding, sample-level mixing and multi-layer QA gates.",
3
+ "version": "0.8.1",
4
+ "description": "Video Studio skill plugin for DeepSeek Harness: style-pack driven code-generated Chinese videos — one style id decides palette/typography/music style/voice persona/pacing, measured TTS timeline with word-level boundaries, deterministic PIL/browser rendering (16:9 landscape, 720p/1080p), programmatic SFX and score, cue-driven sound sync, ffmpeg encoding, sample-level mixing, batch production and multi-layer QA gates (preflight, full-film VMAF/SSIM, ASR readback).",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -24,6 +24,7 @@
24
24
  "locale/*.json",
25
25
  "docs/demo-pil-sheet.png",
26
26
  "THIRD_PARTY_NOTICES.md",
27
+ "CHANGELOG.md",
27
28
  "skills",
28
29
  "!skills/**/__pycache__",
29
30
  "!skills/**/*.pyc",
@@ -34,7 +35,10 @@
34
35
  "docs/styles/demo-results.json",
35
36
  "schemas",
36
37
  "tools/estimate.py",
37
- "tools/ci_check.py"
38
+ "tools/ci_check.py",
39
+ "tools/audio_similarity.py",
40
+ "tools/selftest_noscipy.py",
41
+ "tools/golden_frames.py"
38
42
  ],
39
43
  "publishConfig": {
40
44
  "access": "public"
@@ -49,7 +49,45 @@
49
49
  "string",
50
50
  "object"
51
51
  ],
52
- "description": "字符串 = edge-tts voice id(如 zh-CN-YunxiNeural,legacy 单声线路径);对象 = v0.5 声线策略(map 按 kind 映射 persona、auto、jitter、max_distinct、rate_range、cps_band),此时忽略 rate/pitch 的单值覆盖语义"
52
+ "description": "字符串 = edge-tts voice id(如 zh-CN-YunxiNeural,legacy 单声线路径);对象 = v0.5 声线策略(map 按 kind 映射 persona、auto、jitter、max_distinct、rate_range、cps_band),此时忽略 rate/pitch 的单值覆盖语义",
53
+ "properties": {
54
+ "map": {
55
+ "type": "object",
56
+ "description": "按 kind 映射 persona:{\"hook\": \"young_male\", \"body\": \"narrator_male\", \"quote\": \"narrator_female\", \"outro\": \"narrator_male\"}(键取 lines[].kind 的 6 个值;值取 voicekit.PERSONA_IDS)"
57
+ },
58
+ "default": {
59
+ "type": "string",
60
+ "description": "没有映射时用的 persona 名(voicekit.PERSONA_IDS 之一)"
61
+ },
62
+ "auto": {
63
+ "type": "boolean",
64
+ "description": "是否按内容自动挑 persona(默认 true;false = 只用 map/default)"
65
+ },
66
+ "jitter": {
67
+ "type": "number",
68
+ "minimum": 0,
69
+ "description": "逐句 rate/pitch/lead/tail 的确定性抖动幅度(默认 0.35;legacy 默认 0)"
70
+ },
71
+ "max_distinct": {
72
+ "type": "integer",
73
+ "minimum": 1,
74
+ "description": "全片最多用几个不同 persona(默认 3;相邻句不换人是防广播剧的关键)"
75
+ },
76
+ "rate_range": {
77
+ "type": "array",
78
+ "description": "语速夹取区间(百分数,例如 [-15, 12])",
79
+ "items": {
80
+ "type": "number"
81
+ }
82
+ },
83
+ "cps_band": {
84
+ "type": "array",
85
+ "description": "可接受的每秒字数区间(默认 [3.6, 5.6])",
86
+ "items": {
87
+ "type": "number"
88
+ }
89
+ }
90
+ }
53
91
  },
54
92
  "rate": {
55
93
  "type": "string",
@@ -180,7 +218,7 @@
180
218
  "tail": {
181
219
  "type": "number",
182
220
  "minimum": 0,
183
- "description": "句后留白秒数(一般通过 tail_map / voice_plan 控制,这里少见)"
221
+ "description": "注意:**当前不生效**(保留字段)。filmkit.build_timeline 只读 tail_map,逐句句后留白请写顶层 tail_map,或交给 voicekit 的 voice_plan(st_layout 会把它的逐句 tail 并进 tail_map)。filmcheck 对写了 lines[].tail 的剧本会报 WARN 提醒。"
184
222
  },
185
223
  "persona": {
186
224
  "type": "string",
@@ -382,11 +420,35 @@
382
420
  },
383
421
  "gain_db": {
384
422
  "type": "number",
385
- "description": "配乐总线增益 dB(默认 -1.5,混音前)"
423
+ "description": "配乐总线增益 dB(v0.7.1 默认 -12:视频以旁白为主,背景必须明显低于人声;旧版 -1.5 会让背景与台词几乎等高)"
386
424
  },
387
425
  "duck_db": {
388
426
  "type": "number",
389
- "description": "音效响起时配乐让位深度 dB(默认按风格 2.0–4.5)"
427
+ "description": "音效响起时配乐让位深度 dB(v0.7 默认按风格 3.0–5.5)"
428
+ },
429
+ "voice_duck_db": {
430
+ "type": "number",
431
+ "description": "v0.7.1:旁白说话时配乐额外让位深度 dB(默认 6;0 = 关闭旁白侧链)"
432
+ },
433
+ "voice_duck_sfx_db": {
434
+ "type": "number",
435
+ "description": "v0.7.1:旁白说话时音效额外让位深度 dB(默认 3;0 = 关闭)"
436
+ },
437
+ "voice_thresh_db": {
438
+ "type": "number",
439
+ "description": "v0.7.1:语音活跃度阈值 dBFS(默认 -42;低于它算静音间隙,背景恢复)"
440
+ },
441
+ "voice_floor_db": {
442
+ "type": "number",
443
+ "description": "v0.7.1:语音包络的地板 dBFS(默认 -60;与阈值之间做 0~1 映射)"
444
+ },
445
+ "guard_snr_db": {
446
+ "type": "number",
447
+ "description": "v0.7.1:旁白清晰度兜底(默认 12)。混音后实测人声活跃区的人声/背景响度差,不足就整体再压背景;0 = 关闭"
448
+ },
449
+ "guard_cap_db": {
450
+ "type": "number",
451
+ "description": "v0.7.1:SNR 兜底最多补压多少 dB(默认 30,防止把背景压成忽有忽无;补压 >18 dB 会提示检查素材电平)"
390
452
  },
391
453
  "seg": {
392
454
  "type": "number",
@@ -441,10 +503,23 @@
441
503
  "type": "number",
442
504
  "description": "音效总线增益 dB(默认 0)"
443
505
  },
506
+ "base_db": {
507
+ "type": "object",
508
+ "additionalProperties": true,
509
+ "description": "按配方名覆盖基础电平 dB:{\"whoosh\": -6}。render.py 的 st_sfx 原样透传给 filmkit.CueBus.build_audio(base_db=...),只对键名命中的配方生效(未知配方名不报错、也不生效)"
510
+ },
444
511
  "mix_gain_db": {
445
512
  "type": "number",
446
513
  "description": "混音时音效总线额外增益 dB(默认 0)"
447
514
  },
515
+ "content_seed": {
516
+ "type": "boolean",
517
+ "description": "v0.7:true(默认)= 每条 cue 的 seed 由 style/content_fp 派生(不同影片同一位置也不同);false = 回到 v0.6 的 seed_base+i 位置派生(老片逐样本兼容)"
518
+ },
519
+ "duck_transitions": {
520
+ "type": "boolean",
521
+ "description": "v0.7:true(默认)= 段落边界前 2.2s 内的 whoosh/riser/reverse_swell 自动 -3 dB,避免与配乐过门在同一频段撞车;false = 不处理"
522
+ },
448
523
  "cues": {
449
524
  "type": "array",
450
525
  "description": "提示点数组:每条 {at, type, dur, gain, ...};未知参数原样透传给 sfxkit 配方(size/pan/seed/pitch…)",
@@ -522,6 +597,162 @@
522
597
  "description": "页面分块秒数(webengine 切块用,默认 30)"
523
598
  }
524
599
  }
600
+ },
601
+ "words": {
602
+ "type": "boolean",
603
+ "description": "v0.8 词级时间戳开关(默认 true)。true 时 --stage tts 会额外收 edge-tts 的 WordBoundary 事件落 work/words.json,字幕按词边界折行、cue 可用词锚点 l03:w2、QA 走 G10 门禁;false = 退回整句字幕(不影响出片)。"
604
+ },
605
+ "tts": {
606
+ "type": "object",
607
+ "additionalProperties": true,
608
+ "description": "v0.8 TTS 后端选择(接口化,默认 edge;其它后端需先 filmkit.register_tts_backend 注册)。",
609
+ "properties": {
610
+ "backend": {
611
+ "type": "string",
612
+ "description": "后端名,缺省 edge"
613
+ },
614
+ "voice": {
615
+ "type": [
616
+ "string",
617
+ "object"
618
+ ],
619
+ "description": "覆盖顶层 voice"
620
+ }
621
+ }
622
+ },
623
+ "video": {
624
+ "type": "object",
625
+ "additionalProperties": true,
626
+ "description": "v0.8 画幅/帧率(**只支持 16:9 横屏**)。内部构图恒为 1920x1080,输出只能更小(编码时 lanczos 下采样);竖屏 / 非 16:9 / 上采样 / 非法 fps 会被 filmkit.video_plan 直接报错。",
627
+ "properties": {
628
+ "width": {
629
+ "type": "integer",
630
+ "enum": [
631
+ 1920,
632
+ 1280,
633
+ 960,
634
+ 2560
635
+ ],
636
+ "description": "输出宽(16:9 横屏)"
637
+ },
638
+ "height": {
639
+ "type": "integer",
640
+ "enum": [
641
+ 1080,
642
+ 720,
643
+ 540,
644
+ 1440
645
+ ],
646
+ "description": "输出高(16:9 横屏)"
647
+ },
648
+ "fps": {
649
+ "type": "integer",
650
+ "enum": [
651
+ 24,
652
+ 25,
653
+ 30,
654
+ 50,
655
+ 60
656
+ ],
657
+ "description": "帧率,缺省 30"
658
+ }
659
+ }
660
+ },
661
+ "qa": {
662
+ "type": "object",
663
+ "additionalProperties": true,
664
+ "description": "v0.8 质检门禁参数:全片 VMAF/SSIM(G11)与 ASR 回读(G12)。",
665
+ "properties": {
666
+ "vmaf": {
667
+ "type": "object",
668
+ "additionalProperties": true,
669
+ "description": "全片 VMAF/SSIM 门禁(参考帧由场景函数实时渲染,不是成片抽帧)。注意:本对象里的 enabled 与 qa.require_vmaf 是**惰性键**(v0.8 起草时留下,代码无读取点),真正生效的开关是 mode=off / --vmaf off;filmcheck.py 对惰性键会报 WARN。",
670
+ "properties": {
671
+ "enabled": {
672
+ "type": "boolean",
673
+ "description": "惰性键:写它不生效(要跳过 G11 请写 mode=\"off\" 或命令行 --vmaf off)"
674
+ },
675
+ "mode": {
676
+ "type": "string",
677
+ "enum": [
678
+ "full",
679
+ "sample",
680
+ "off"
681
+ ],
682
+ "description": "full=逐帧(默认,约 1.2x 实时);sample=每 N 帧取 1;off=不跑"
683
+ },
684
+ "pass": {
685
+ "type": "number",
686
+ "minimum": 0,
687
+ "maximum": 100,
688
+ "description": "VMAF 低于此值 -> fail(默认 92,权威定义在 qavmaf.py 的 VMAF_PASS;标定依据见 references/v08-quality.md)"
689
+ },
690
+ "warn": {
691
+ "type": "number",
692
+ "minimum": 0,
693
+ "maximum": 100,
694
+ "description": "介于 pass 与 warn 之间 -> pass+warning(默认 96,权威定义在 qavmaf.py 的 VMAF_WARN)"
695
+ },
696
+ "sample_step": {
697
+ "type": "integer",
698
+ "minimum": 1,
699
+ "description": "mode=sample 时的抽样步长"
700
+ }
701
+ }
702
+ },
703
+ "ssim": {
704
+ "type": "object",
705
+ "additionalProperties": true,
706
+ "description": "SSIM 阈值(诊断项,不单独阻断;默认 pass=0.95 / warn=0.98,权威定义在 qavmaf.py)",
707
+ "properties": {
708
+ "enabled": {
709
+ "type": "boolean",
710
+ "description": "惰性键:写它不生效(SSIM 本来就只是诊断项,不单独阻断)"
711
+ },
712
+ "pass": {
713
+ "type": "number",
714
+ "minimum": 0,
715
+ "maximum": 1
716
+ },
717
+ "warn": {
718
+ "type": "number",
719
+ "minimum": 0,
720
+ "maximum": 1
721
+ }
722
+ }
723
+ },
724
+ "require_vmaf": {
725
+ "type": "boolean",
726
+ "description": "惰性键:写它不生效(要强制 G11 不许记 skipped,请用 --strict-gates)"
727
+ },
728
+ "asr": {
729
+ "type": "object",
730
+ "additionalProperties": true,
731
+ "description": "ASR 回读门禁(把成片音轨转回文字与剧本对账)",
732
+ "properties": {
733
+ "enabled": {
734
+ "type": "boolean",
735
+ "description": "false = 本次不做回读(记 skipped)"
736
+ },
737
+ "model": {
738
+ "type": "string",
739
+ "description": "faster-whisper 模型名,缺省 small"
740
+ },
741
+ "recall_pass": {
742
+ "type": "number",
743
+ "minimum": 0,
744
+ "maximum": 1,
745
+ "description": "召回率 >= 此值算干净(默认 0.85)"
746
+ },
747
+ "recall_warn": {
748
+ "type": "number",
749
+ "minimum": 0,
750
+ "maximum": 1,
751
+ "description": "低于此值 -> fail(默认 0.72)"
752
+ }
753
+ }
754
+ }
755
+ }
525
756
  }
526
757
  },
527
758
  "additionalProperties": true