@dsh-gis/dsh-video-studio-skill 1.0.2 → 1.0.4

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 (51) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +84 -108
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/package.json +1 -1
  5. package/schemas/design.schema.json +5 -5
  6. package/schemas/film.schema.json +31 -31
  7. package/skills/video-studio/SKILL.md +168 -268
  8. package/skills/video-studio/assets/film.json +2 -3
  9. package/skills/video-studio/assets/golden_frames.json +11 -11
  10. package/skills/video-studio/assets/scenes.py +92 -49
  11. package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
  12. package/skills/video-studio/references/audio-variation.md +73 -96
  13. package/skills/video-studio/references/audio.md +40 -44
  14. package/skills/video-studio/references/change-impact.md +2 -2
  15. package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
  16. package/skills/video-studio/references/frame-qa.md +47 -15
  17. package/skills/video-studio/references/pipeline.md +4 -4
  18. package/skills/video-studio/references/pitfalls.md +22 -3
  19. package/skills/video-studio/references/proposal.md +19 -36
  20. package/skills/video-studio/references/quality-gates.md +11 -11
  21. package/skills/video-studio/references/scene-authoring.md +2 -2
  22. package/skills/video-studio/references/scene-grammar.md +17 -24
  23. package/skills/video-studio/references/sfx-design.md +7 -5
  24. package/skills/video-studio/references/web-engine.md +7 -7
  25. package/skills/video-studio/references/word-timeline.md +7 -7
  26. package/skills/video-studio/requirements.txt +9 -13
  27. package/skills/video-studio/scripts/designkit.py +84 -89
  28. package/skills/video-studio/scripts/filmcheck.py +19 -19
  29. package/skills/video-studio/scripts/filmkit.py +2949 -2948
  30. package/skills/video-studio/scripts/gates.py +4 -4
  31. package/skills/video-studio/scripts/grammar.py +10 -10
  32. package/skills/video-studio/scripts/musicgen.py +3 -4
  33. package/skills/video-studio/scripts/preflight.py +5 -5
  34. package/skills/video-studio/scripts/previewkit.py +11 -12
  35. package/skills/video-studio/scripts/propose.py +11 -11
  36. package/skills/video-studio/scripts/qa_frames.py +34 -35
  37. package/skills/video-studio/scripts/qaasr.py +26 -7
  38. package/skills/video-studio/scripts/qavmaf.py +19 -19
  39. package/skills/video-studio/scripts/render.py +137 -141
  40. package/skills/video-studio/scripts/scorekit.py +21 -23
  41. package/skills/video-studio/scripts/selfcheck.py +208 -135
  42. package/skills/video-studio/scripts/sfxkit.py +27 -26
  43. package/skills/video-studio/scripts/stagewatch.py +4 -4
  44. package/skills/video-studio/scripts/stylekit.py +77 -282
  45. package/skills/video-studio/scripts/voicekit.py +43 -44
  46. package/skills/video-studio/scripts/webengine.py +62 -64
  47. package/tools/audio_similarity.py +12 -12
  48. package/tools/ci_check.py +16 -16
  49. package/tools/estimate.py +24 -25
  50. package/tools/golden_frames.py +17 -18
  51. package/tools/silhouette_qa.py +13 -14
@@ -1,6 +1,6 @@
1
1
  # 帧级深度质检(qa_frames)
2
2
 
3
- > v0.3.0 新增。成片质检 v0.2 只看容器、响度、黑帧、锐度——"能出片"不等于"画面里没有一帧白闪"。
3
+ > 成片质检的容器 / 响度 / 黑帧 / 锐度几项只看"能出片",看不出"画面里有一帧白闪"——本模块补的正是这一层。
4
4
  > 这份文档讲清楚 **判据、阈值来源、报告字段、怎么调参、以及这套方法的边界**。
5
5
 
6
6
  实现:`skills/video-studio/scripts/qa_frames.py`(只依赖 stdlib + numpy + Pillow,全部解码走 ffmpeg)。
@@ -102,10 +102,10 @@ s[t] = max over 8x8 blocks of mean(d[t]) # 分块取最大均值(块内
102
102
 
103
103
  实测正常运动画面 `s <= 6.4`,真实异常 `s = 10–15`,所以阈值默认 `6.0`。
104
104
 
105
- > **历史**:早期版本还有一条"严格孤立"(要求 `s[t-1] < thresh` 且 `s[t+1] < thresh`)。
106
- > 它被证明只会制造假阳性(16.933 s 案例就是两侧恰好低谷),已删除,只保留比值判据。
105
+ > **不要加回"严格孤立"**(要求 `s[t-1] < thresh` 且 `s[t+1] < thresh`):它只会制造假阳性
106
+ > (16.933 s 案例就是两侧恰好低谷),判据只保留比值。
107
107
 
108
- **第三步:必须"回到原位"**(`FLICKER_RETURN_RATIO = 1.6`,v0.3.0 实测补充):
108
+ **第三步:必须"回到原位"**(`FLICKER_RETURN_RATIO = 1.6`):
109
109
 
110
110
  ```
111
111
  r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
@@ -175,9 +175,9 @@ r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
175
175
  **绝对响度是否合规(-16 LUFS ±、真峰 ≤ -1 dBTP)以 `filmkit.loudness()`(ffmpeg ebur128)为门禁**,
176
176
  本曲线只回答"哪段掉下去了、重击比环境高多少、空窗处声音是不是也停了"。
177
177
 
178
- ### 2.4b `_static_warnings` — 画面长时间静止(v0.4 重写)
178
+ ### 2.4b `_static_warnings` — 画面长时间静止
179
179
 
180
- 判据(**替换** v0.3 的"全帧平均亮度 3 s 内波动 < 0.002"):
180
+ 判据(**不要用**"全帧平均亮度 3 s 内波动 < 0.002"这类公式):
181
181
 
182
182
  ```
183
183
  drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_LAG_S = 0.5 s)
@@ -188,8 +188,8 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
188
188
  人眼看到的就是一张静止的图。用"最活跃块"而不是"全帧均值",是因为逐字入场、数字滚动、卡片切换
189
189
  只占全屏几个百分点,全帧均值几乎不动,而观众显然看得到它们在动。
190
190
 
191
- 为什么旧判据要废:全帧均值对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
192
- (411/814 个滑动窗口命中),而画面一直在动。新判据在同一片上 **0 段**。
191
+ 为什么不用全帧均值:它对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
192
+ (411/814 个滑动窗口命中),而画面一直在动。现判据在同一片上 **0 段**。
193
193
 
194
194
  实测分布(160×90 解码,H.264):
195
195
 
@@ -214,7 +214,7 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
214
214
  2. 否则**时间最近**:任意事件里取最近者,**但只有 `|dt| <= tol` 才认可**——用来佐证"确实同步了";
215
215
  3. 都找不到 → 视为"这个 cue 没有对应画面事件",**不判超差**。
216
216
 
217
- 第 2 条为什么收紧(v0.3.0 实测):画面事件和音频 cue 本来就不是一一对应。
217
+ 第 2 条为什么只认 `|dt| <= tol` 的最近事件:画面事件和音频 cue 本来就不是一一对应。
218
218
  实测反例:4.100 s 的 `data_chirp` 被 3.700 s 的"交界黑场起坡"事件配对,报 `dt = -0.400 s` 超差;
219
219
  8.020 s 的 `reverse_swell` 被 7.700 s 的事件配成 `-0.320 s`。两者都是**假 FAIL**——
220
220
  跨 kind 的"最近事件"根本不是"预期事件"。
@@ -229,9 +229,9 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
229
229
  `flash / shake / ring` 而 cue 是 `impact / whoosh`,同 kind 永远配不上,跨 kind 又只做确认,
230
230
  结果就是 **`sync` 恒为空、检查静默全过**——看起来"零超差",其实是"一条都没在查"。
231
231
 
232
- #### v0.4:PIL 路线的两条补救(这条门禁以前真的没在查)
232
+ #### PIL 路线的两条补救(否则这条门禁不会真的在查)
233
233
 
234
- `--engine pil` 根本没有页面、没有 `events`,所以上面的配对必然配不上。v0.4 给了两条路:
234
+ `--engine pil` 根本没有页面、没有 `events`,所以上面的配对必然配不上。两条补救:
235
235
 
236
236
  1. **cue 自带画面锚点**:给 cue 加 `"visual_at"`(或 `"visual_t"`)字段,值是画面上该事件
237
237
  **真正发生的绝对秒数**。`sync_check` 会直接算 `dt = visual_at - cue.t` 并判超差,
@@ -462,9 +462,9 @@ JSON 里 `flicker` 两项:
462
462
 
463
463
  ---
464
464
 
465
- ## 9. 案例:16.933 s「小元素动画」误报(判据是怎么演化的)
465
+ ## 9. 案例:16.933 s「小元素动画」误报
466
466
 
467
- PIL 模板演示片(30 s / 1080p30)早期版本报过 `t=16.933s score=6.61`,bbox `96,40–104,48`。
467
+ PIL 模板演示片(30 s / 1080p30)报过 `t=16.933s score=6.61`,bbox `96,40–104,48`。
468
468
  逐层复现(160×90、8×8 块、area 采样)后,数据是这样的:
469
469
 
470
470
  | 帧号(t) | 504 (16.800) | 505 (16.833) | 506 (16.867) | 507 (16.900) | 508 (16.933) | 509 (16.967) | 510 (17.000) |
@@ -481,8 +481,8 @@ PIL 模板演示片(30 s / 1080p30)早期版本报过 `t=16.933s score=6.61`
481
481
  3. **真相是"小元素在动"**:卡片上一个绿色光点在漂移(全分辨率二阶差分 > 30 的像素只占全屏 0.33%),
482
482
  504–510 帧连续都在动;508 只是恰好被两侧 507/509 的低谷夹住,`ratio = 6.48 / max(5.66,3.52) = 1.15 < 1.5`。
483
483
 
484
- **判据因此改了一次**:删掉"要求邻居低于阈值"的严格路径(它只会制造这种假阳性),
485
- 只保留比值判据;后来又加了"回到原位"闸(§2.2)处理加速运动。
484
+ **所以判据不要求"邻居低于阈值"**:那条严格路径只会制造这种假阳性,只保留比值判据;
485
+ 另加"回到原位"闸(§2.2)处理加速运动。
486
486
  复核这类案例最快的两个线索:**`median`(这里是 1.0,真白闪是 143–199)** 和
487
487
  **变化像素占全屏的比例(这里是 0.33%)**。
488
488
 
@@ -500,3 +500,35 @@ PIL 模板演示片(30 s / 1080p30)早期版本报过 `t=16.933s score=6.61`
500
500
  | 空窗漏报 | 近黑但没到 0.045(例如深灰 0.06)→ 调高 `BLACK_LUMA_MAX`;或时长不足 0.8 s |
501
501
  | 同步全不报 | ① `cues.json` 没有 `events`(页面未导出);② **events.kind 与 cue 配方名不同名**(最常见,且不会任何报错);③ cue 与事件时间差 > 同 kind 搜索窗 2 s |
502
502
  | 想确认同步检查真的在工作 | 故意把某个 cue 的时刻改错 0.3 s 重跑,**必须**报出超差;不报就是没配上(见上一行) |
503
+
504
+ ## 11. 取景抖动(噪声沸腾)—— 单帧闪烁判据抓不到的一类
505
+
506
+ **现象**:成片看起来"一直在抖",但 `flicker_scan` 报 0、`ssim`/`vmaf` 都很好。
507
+ 用户的一句话往往是"生成的视频有明显的抖动感,这是为啥?"
508
+
509
+ **根因**:底图把颗粒(`_grain_layer`)和材质一起烘进了 1920×1080 的静态图层,逐帧再用
510
+ `Image.EXTENT + NEAREST` 按镜头缓推的缩放比 `s` 取景。NEAREST 的采样网格随 `s` 整像素跳变,
511
+ 颗粒图案跟着**整体错位 1px**——这就是"满屏噪点在沸腾"。它是**位移**型瑕疵,不是亮度型,
512
+ 所以按"前后帧平均亮度差"定义的 `flicker_scan` 对它完全不敏感。
513
+
514
+ **探测方法**(不用整片,10 秒就够):
515
+
516
+ ```python
517
+ import numpy as np
518
+ from PIL import Image
519
+ # 同一主题,连续取 s = 1.0 -> 1+push 的 20~40 帧
520
+ frames = [mod._bg_plate(th, 1.0 + th["push"] * i / 39.0) for i in range(40)]
521
+ worst = max(int(np.abs(np.asarray(a, np.int16) - np.asarray(b, np.int16)).max())
522
+ for a, b in zip(frames, frames[1:]))
523
+ print(worst) # NEAREST 直采 4~6 灰阶(可见沸腾);端点预烘 + blend 后 0~1
524
+ ```
525
+
526
+ **修法**:不要用 NEAREST 直采缩放(BILINEAR/BICUBIC 整帧重采样 29/64 ms,超单帧预算)。
527
+ 改成**端点预烘 + 逐帧 blend**:`s=1.0` 与 `s=1+push` 各烘一张 BILINEAR 底版(每张约 60ms,
528
+ 只做一次),逐帧用 `Image.blend(lo, hi, (s-1)/push)` 插值(约 4.4 ms/帧)。
529
+ 端点内部是平滑重采样,没有网格跳变;颗粒质感保留。上限 `1+push` 是精确边界
530
+ (`_push_scale` 恒 <= 1+push),超出就钳到端点上,不做外推。
531
+
532
+ **护栏**:`selfcheck` 第 68 条(`t_zoom_stability`)断言相邻帧最大差 <= 2 灰阶。
533
+ 改回 NEAREST 直采在这条上直接 fail——这就是"再也不会悄悄退回 NEAREST"的机制。
534
+
@@ -45,7 +45,7 @@ BPM 96, 4/4 → 1 小节 = 2.5 s = 75 帧 @30fps
45
45
  2. 单独一节"不能上屏"黑名单:写清为什么不行、实际是什么
46
46
  3. 口径表:同一数字在报告/字幕/画面里表述完全一致
47
47
 
48
- 真实收益(来自会话考古):75 条主张 + 25 条黑名单,抓出 8 处硬伤,其中"疫情在拆把手后很快平息"这类因果讲反的错误会被评论区第一条指出。
48
+ 真实收益(一次实际核查的账):75 条主张 + 25 条黑名单,抓出 8 处硬伤,其中"疫情在拆把手后很快平息"这类因果讲反的错误会被评论区第一条指出。
49
49
 
50
50
  ## P3 TTS(唯一正确的时长来源)
51
51
 
@@ -73,10 +73,10 @@ tl = K.build_timeline(lines, lead=0.55, tail=0.85, min_dur=3.0)
73
73
  字幕窗口规则(防交叠,来自实战):
74
74
  ```
75
75
  t0 = voice_start - 0.12
76
- t1 = voice_end + 0.45 # v0.2 起不再有 5.2 s 硬钳;要钳短显式传 max_hold
76
+ t1 = voice_end + 0.45 # 没有 5.2 s 硬钳;要钳短显式传 max_hold
77
77
  t1 = min(t1, next.voice_start - 0.12) # 绝不压住下一句
78
78
  ```
79
- 停留时长参考 `0.5 + 字数/9` 秒。**没有默认上限**(v0.2 移除了 5.2 s 硬钳)——
79
+ 停留时长参考 `0.5 + 字数/9` 秒。**没有默认上限**——
80
80
  真要缩短就显式传 `max_hold` / `sub_max_hold`。按字符比例平均分配会产生大量交叠。
81
81
 
82
82
  ## P5 混音(numpy 总线)
@@ -109,7 +109,7 @@ python render.py ... --stage qa # 只读质检,不重
109
109
  ```python
110
110
  sink = K.FrameSink("work/silent.mp4", crf=18, preset="medium") # rawvideo 直通
111
111
  ...
112
- # 视频流默认 -c:v copy(不再二次编码);需要改分辨率时 reencode_video=True
112
+ # 视频流默认 -c:v copy(不二次编码);需要改分辨率时 reencode_video=True
113
113
  K.mux("work/silent.mp4", "work/audio.wav", "out/film.mp4")
114
114
  ```
115
115
 
@@ -9,7 +9,7 @@
9
9
  | 3 | **音频实测反推时间,绝不按字数估算** | 字幕漂移是观众最先察觉的瑕疵 |
10
10
  | 4 | **字幕文本与朗读文本分离**(`sub` 短 / `text` 长) | 上屏可读性与朗读自然度是两个约束 |
11
11
  | 5 | **提示点由画面声明、由声音消费** | 音画对齐从"感觉"变成"数据" |
12
- | 6 | **分块键用绝对帧号**(`seg_025200_025560.mp4`),拼接时显式检缺口;再存一份**计划指纹**(`_plan.json`) | 唯一一次"少了 183 秒还不报错"的根因。只按"文件在不在"跳过还会导致"改了台词却复用旧画面"——v0.4 起指纹不符会**拒绝拼接**并提示删目录 |
12
+ | 6 | **分块键用绝对帧号**(`seg_025200_025560.mp4`),拼接时显式检缺口;再存一份**计划指纹**(`_plan.json`) | 唯一一次"少了 183 秒还不报错"的根因。只按"文件在不在"跳过还会导致"改了台词却复用旧画面"——计划指纹不符会**拒绝拼接**并提示删目录 |
13
13
  | 7 | **JPEG q94 截图替代 PNG**(浏览器方案) | 215 → 55 ms/帧,PSNR 41–44 dB 肉眼无差 |
14
14
  | 8 | **worker 复用浏览器、出错才重建、finally 必关** | 每块开新页会把进程堆到 28 个、内存 2.7 GB |
15
15
  | 9 | **前瞻限幅器补响度,不整体拉高** | 只压峰值不改主体响度 |
@@ -25,7 +25,7 @@
25
25
  | 19 | **写入边界 + 接口先写死 + 一份施工文档** | 多 agent 并行不打架的三个前提 |
26
26
  | 20 | **交付文档如实声明已知边界** | 把"局限"变成可信度,而不是藏起来 |
27
27
 
28
- ## 二、16 条反模式(全部有实证)
28
+ ## 二、17 条反模式(全部有实证)
29
29
 
30
30
  | # | 反模式 | 后果 | 正确做法 |
31
31
  |---|---|---|---|
@@ -45,8 +45,9 @@
45
45
  | 14 | 音频用单声道总线后复制成双声道 | 损失立体声场 | 全程 float32 立体声(配乐做宽度) |
46
46
  | 15 | 静态图叠加忘了 `-loop 1` | 时间轴被截成 1 帧 | `-loop 1 -framerate ` |
47
47
  | 16 | 靠"注意准确性"这种叮嘱做质量 | 无法验收 | 事实核查有编号、出处、黑名单三件套 |
48
+ | 17 | 把颗粒烘进底图,再用 NEAREST 逐帧缩放取景 | **满屏噪点沸腾**(用户原话"明显的抖动感"),而单帧闪烁门禁全程报 0 | 端点预烘 + `Image.blend` 取景;颗粒留在底图但只走平滑重采样(见复盘 ④) |
48
49
 
49
- ## 三、三个非确定性 bug 复盘(最值得读)
50
+ ## 三、四个 bug 复盘(最值得读)
50
51
 
51
52
  ### ① 音轨中断:成片只有前 15 秒有声
52
53
 
@@ -81,3 +82,21 @@ if (inP < 1 && act.i > 0) { ... } // 永远不成立
81
82
  字段从 `t0` 改名成 `start` 后,全片十幕的淡入特效与所有图名框**静默失效**。没有任何报错,画面照常渲染,只是"少了一点东西"。
82
83
 
83
84
  **教训**:质检分两类——**"不能有"**(闪烁、重叠、出画、削波)和**"必须有"**(图名框、淡入、字幕、每段至少一个动态元素)。后者更难,因为需要一份"应然清单"。
85
+
86
+ ### ④ 整片"发抖":颗粒 + NEAREST 取景
87
+
88
+ **现象**:成片从头到尾有轻微抖动感,用户说"有明显的抖动感",但 `flicker_scan` 0 条、
89
+ VMAF 97+、SSIM 0.999——所有现有门禁都说没问题。
90
+
91
+ **根因**:`_bg()` 把颗粒(逐像素 hash 噪声)烘进了静态底图,逐帧再用
92
+ `EXTENT + NEAREST` 按缓推比例 `s` 取景。NEAREST 的采样网格随 `s` 整像素跳变,
93
+ 颗粒图案整体错位 1px。实测背景-only 相邻帧差 **4~6 灰阶**(整屏低幅位移),
94
+ 而"单帧闪烁"判据比的是前后帧平均亮度——压根看不到这种位移。
95
+
96
+ **修复**:`s=1.0` 与 `s=1+push` 两个端点各烘一张 BILINEAR 底版(每张约 60 ms,一次性),
97
+ 逐帧 `Image.blend(lo, hi, (s-1)/push)`(约 4.4 ms/帧)。实测相邻帧差降到 **0~1 灰阶**。
98
+ 为什么不用 BILINEAR/BICUBIC 直采:整帧重采样 29/64 ms,超 15 ms 单帧预算。
99
+
100
+ **教训**:**"能出片 + 指标好看"不等于"看着不抖"**。质检判据要按**瑕疵的物理类型**分类:
101
+ 亮度型(闪烁/黑帧)、位移型(抖动/漂移)、时序型(同步/丢句)。缺哪一类,
102
+ 那一类就会一直漏。补判据的同时补一条 `selfcheck` 断言(第 68 条),否则下次改回 NEAREST 没人发现。
@@ -1,21 +1,14 @@
1
- # 提案层(Proposal)与设计 DSL —— v1.0.2
1
+ # 提案层(Proposal)与设计 DSL
2
2
 
3
3
  > 一句话:**出片之前先出方案;方案由用户拍板;拍板的那份设计被冻结成 design.json,渲染只认它。**
4
- > v1.0.2 起还有一条:**交付过的设计会进台账**,下一次提案主动避开——这是"不同片不重复"的落地机制。
4
+ > 还有一条:**交付过的设计会进台账**,下一次提案主动避开——这是"不同片不重复"的落地机制。
5
5
 
6
6
  ## 0. 为什么要有这一层
7
7
 
8
- v0.9 及以前,`style: "auto"` 会让 `pick_style` 按关键词 + 指纹**自动挑一套**内置风格。
9
- 这带来两个后果(都有实测):
10
-
11
- | 后果 | 实测 |
12
- |---|---|
13
- | 同一份内容的身份是确定的 —— 改不出第二种样子 | 8 个不同题材只命中 5/8 套风格,其中 3 个都落进 tech-deep |
14
- | 不写 `style` 就直接落到 legacy —— 全片只有一套皮肤 | 8/8 题材全落 legacy:120BPM / Dm / 同一套配色 |
15
-
16
- v1.0 把这条路径**整条删掉**:剧本不再决定长相,长相由 `design.json` 决定;
17
- `design.json` 只能由「用户确认过的提案」产生。v1.0.1 又把残留的风格表全部删除,
18
- 所以现在**没有"以某套预设为起点"这回事**——每次都是现场采样。
8
+ 长相不能由剧本决定,也不能由程序替用户拍板:同一份稿子该长什么样,是个需要看图的审美决策,
9
+ 而"看过了再点一个"正是用户唯一能参与决策的位置。所以 `propose.py` 现场采样 N 个**两两可区分**
10
+ 的设计,每个配 4 张静帧,你确认哪一个,那一个才被冻结成 `design.json`(渲染只认它)。
11
+ **没有"以某套预设为起点"这回事**,每次都是现场采样。
19
12
 
20
13
  ## 1. 三个产物
21
14
 
@@ -75,13 +68,14 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
75
68
  ### 4.1 fail-closed(这条最重要)
76
69
 
77
70
  设计里**缺字段、枚举越界、对比度不达标**,一律报错,**绝不静默回落默认值**。
78
- 理由:v0.5–v0.9 最大的病就是「改了不生效也不报错」——审计实测出 11 个静默失效的旋钮
71
+ 理由:**改了不生效也不报错**是这套流程里最贵的故障——审计实测过 11 个静默失效的旋钮
79
72
  (`voice` 写字符串时 map/jitter 全失效、`theme_overrides.ease` 被视觉语言遮住、
80
- `sfx.cues[].seed` 必崩、`pan` 对立体声配方无效……)。设计层不允许重演这一幕。
73
+ `sfx.cues[].seed` 必崩、`pan` 对立体声配方无效……)。设计层不允许出现这种状态:
74
+ 要么字段有效,要么当场报错。
81
75
 
82
- ### 4.2 visual.subtitle(v1.0.2,可选)
76
+ ### 4.2 visual.subtitle(可选)
83
77
 
84
- 字幕样式不再写死在 `filmkit` 里。所有字段可选,**不写 = 补齐默认值且与过去逐像素一致**:
78
+ 字幕样式由设计 DSL 决定(不是 `filmkit` 的写死常量)。所有字段可选,**不写 = 补齐默认值,与默认常量逐像素一致**:
85
79
 
86
80
  | 字段 | 默认 | 取值 | 说明 |
87
81
  |---|---|---|---|
@@ -96,7 +90,7 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
96
90
 
97
91
  渲染侧只读 `designkit.subtitle_of(design)` 这一份口径(`filmkit`、参考帧渲染共用),避免"字幕样式在两处漂移"。
98
92
 
99
- ### 4.3 visual.layout_cycle(v1.0.2,可选)
93
+ ### 4.3 visual.layout_cycle(可选)
100
94
 
101
95
  按**章节**轮换版式:同一章内版式固定,换章才换。元素取 `left-stack` / `center-stack` / `split-right`,
102
96
  **至少 2 种不同版式**(只写一种会被 `validate_design` 拒绝)。不写时 `design_to_plan` 按设计指纹自动派生
@@ -113,21 +107,21 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
113
107
 
114
108
  输入 brief(题材 + 可选时长/必须有/不要),输出 N 个**两两可区分**的设计:
115
109
 
116
- - **默认每次都不一样**(v1.0.1):不传 `seed` 就生成新的 nonce,同一 brief 跑两次得到不同设计;
110
+ - **默认每次都不一样**:不传 `seed` 就生成新的 nonce,同一 brief 跑两次得到不同设计;
117
111
  传 `--seed` 才逐字节可复现。这是"每条视频都不一样"的来源。
118
112
  - 所有选择由 `sha1(nonce + brief + index + seed + attempt)` 派生,**禁 random / time**。
119
113
  - **两两必须在 ≥2 个设计维度**上不同(10 个离散轴:配色 / 母题 / 转场 / 缓动 / 编号 / **配乐(调式+BPM 档+情绪)** /
120
114
  声线 / 情绪 / 版式 / 调式),否则视为「伪多方案」(只换颜色不算)。
121
115
  - **底色还要拉开色相**:色相差 < 6° 一律重采;< 12° 时要求结构差异 ≥ 3 个轴。
122
- v1.0.2 修掉了旧实现按 24° 分桶判重的坑——带 `--brief "深蓝"` 这类约束时窗口只有 52°,
123
- 旧实现 n=3 近一半概率失败;现在实测带约束的 brief **10/10 成功**。
116
+ 判重直接比色相距离,不按固定角度分桶:带 `--brief "深蓝"` 这类约束时可选色相窗口只有 52°,
117
+ 分桶会让 n=3 有近一半概率采样失败;直接比距离时带约束的 brief 实测 **10/10 成功**。
124
118
  - **片间去重台账**(见 §6):候选还要与"最近交付过的设计"至少差 2 维。
125
119
  - 每个候选自带一句中文立意 `pitch`,写进提案卡。
126
120
 
127
- ## 6. 片间去重台账(v1.0.2,默认开启)
121
+ ## 6. 片间去重台账(默认开启)
128
122
 
129
123
  "每条视频都不一样"不能只靠随机:同题材连拍三部仍可能撞到相近的配色/版式组合。
130
- `propose.py` 现在把**确认过(= 真要出片)的设计**记进用户级台账,提案时主动避开:
124
+ `propose.py` 把**确认过(= 真要出片)的设计**记进用户级台账,提案时主动避开:
131
125
 
132
126
  | 项 | 值 |
133
127
  |---|---|
@@ -162,16 +156,5 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
162
156
  | 提案闸一直 WARN | 没跑过 `propose.py --confirm` | 按提示两步走;要硬拦加 `--require-proposal` |
163
157
  | 预览图和成片不一致 | 预览用合成时长,成片用实测时长 | 正常现象:静帧只用于看构图/配色,节奏以成片为准 |
164
158
  | 「这批候选和上一部太像」告警 | 片间去重台账命中(< 2 维) | 自动重采过 4 轮仍撞:`--revise "换个完全不同的方向"`,或 `--no-ledger` 忽略 |
165
- | 带「深蓝」这类要求时提案失败 | 约束把可选空间收窄到不可行 | v1.0.2 已修(色相距离判重);仍失败就放宽 brief 或 `--n 2` |
166
-
167
- ## 9. 已删除的东西(迁移须知)
168
-
169
- | 旧写法 | 现在 |
170
- |---|---|
171
- | `style: "auto"` 自动选风格 | **已删除**(v1.0.1)。没有 design 就不出片 |
172
- | `stylekit.THEMES/STYLES/VISUAL_LANGUAGES` 8 套 | **已全部删除**(v1.0.1):没有内置风格表 |
173
- | `--preset <id>` 以出厂预设为起点 | **已删除**(v1.0.1):只有现场采样一条路,想复现用 `--seed` |
174
- | legacy(不写 style 走 v0.4 绘制器) | **已删除**(v1.0.1):老片复现只能靠当年留存的 `work/` 快照 |
175
- | `scorekit.MUSIC_STYLES` 9 套 | **已删除**(v1.0.1):配乐改由 `musicgen.py` 参数化编排 |
176
- | `voicekit.VOICE_PERSONAS` 9 个 | **已删除**(v1.0.1):换成裸 edge voice id + 实测响度补偿表 |
177
- | `stylekit.py --list/--show/--lint` | **已移除**:明确说明「内置风格表已删除」并 **exit 2**(不再 NameError / 假绿) |
159
+ | 带「深蓝」这类要求时提案失败 | 约束把可选空间收窄到不可行 | 判重按色相距离、不按固定角度分桶;仍失败就放宽 brief 或 `--n 2` |
160
+ | `film.json` 里写 `style`(`"auto"` / 风格 id / `"legacy"`)画面没变 | 外观只认 `work/design.json`,这个字段不参与选型 | 删掉它;没有 design 就先 `propose.py --brief ...` 再 `--confirm` |
@@ -4,20 +4,20 @@ Agent 看不见画面、听不见声音,所以**验收必须脚本化 + 静帧
4
4
 
5
5
  > ## ⚠ 两套编号别混用
6
6
  >
7
- > 本页的 `G0–G8` 是 v0.3 起的**方法论分组**(按"查什么"分类,写给写剧本/写场景的人看)。
7
+ > 本页的 `G0–G8` 是**方法论分组**(按"查什么"分类,写给写剧本/写场景的人看)。
8
8
  > `render.py` 落进 `work/gates.json` 与 `out/QC-REPORT.md` 的,是**运行时账本 id**(按"哪一步判"排)。
9
9
  > 两套编号**有重名但不同义**,看到 `G0` 先确认是哪一套:
10
10
  >
11
11
  > | 本页(方法论) | 账本 id(运行时) | 关系 |
12
12
  > |---|---|---|
13
- > | G0 内容门禁:上屏数字可溯源 | `G0` 渲染前资源预检(v0.8) | **同名不同义**:账本 G0 查字形/字体/TTS/web 页,不核对数字出处 |
13
+ > | G0 内容门禁:上屏数字可溯源 | `G0` 渲染前资源预检 | **同名不同义**:账本 G0 查字形/字体/TTS/web 页,不核对数字出处 |
14
14
  > | G1 帧级门禁(确定性/单帧预算/元素存在性) | — | 人工/代码评审,不进账本 |
15
15
  > | G2 静帧门禁(母版预览目检) | `signoff`(`stills` 闸) | 目检结论走人工签核 |
16
16
  > | G3 音频门禁(音效/混音/母带) | `G5` 响度/真峰 | 账本只查成片级的两个数 |
17
17
  > | G4 成片门禁(容器/时长/锐度/字幕/黑帧) | `G4` 容器 · `G5` 响度 · `G6` 锐度比 · `G7` 字幕条数 · `G8` 黑帧 | 账本把 G4 拆成 5 行 |
18
18
  > | G7 时间层门禁(闪烁/空窗/同步) | `G9` 帧级深检 | **同名不同义**:账本 G9 才是时间层 |
19
19
  > | G8 页面层门禁(web 文字重叠/出画) | web 引擎 qa_text 结果 | 只有 `--engine web` 才有 |
20
- > | — | `G10` 词级时间戳 · `G11` VMAF/SSIM · `G12` ASR 回读 | **v0.8 新增**,详见 [v08-quality.md](v08-quality.md) 与 [word-timeline.md](word-timeline.md) |
20
+ > | — | `G10` 词级时间戳 · `G11` VMAF/SSIM · `G12` ASR 回读 | 详见 [delivery-gates.md](delivery-gates.md) 与 [word-timeline.md](word-timeline.md) |
21
21
  >
22
22
  > 一句话记法:**本页 G0–G8 是"查什么",账本 G0 + G4–G12 是"哪一行判"**。
23
23
  > 账本里**没有** `G1`/`G2`/`G3`:`G0` 之后直接从 `G4` 开始(容器),人工目检走 `signoff` 的 `stills` 闸,不进 gate matrix。
@@ -81,8 +81,8 @@ pr = K.probe(film); f = pr["format"]
81
81
  # 容器
82
82
  assert f["duration"] > 0
83
83
  v = [s for s in pr["streams"] if s["codec_type"] == "video"][0]
84
- # v0.8:比的是**交付规格**不是内部构图。qa_report(expect_w=, expect_h=, expect_fps=) 会从
85
- # timeline.json 的 video 块取期望值;缺省回落 1920x1080@30(老项目行为不变)。
84
+ # 比的是**交付规格**不是内部构图。qa_report(expect_w=, expect_h=, expect_fps=) 会从
85
+ # timeline.json 的 video 块取期望值;缺省回落 1920x1080@30。
86
86
  assert (v["width"], v["height"]) == (W, H) # W/H 这里代表交付宽高,不是 1920x1080 常量
87
87
  assert any(s["codec_type"] == "audio" for s in pr["streams"]), "无音轨"
88
88
 
@@ -169,7 +169,7 @@ q = K.encode_quality("out/film.mp4", "work/qa/snap.png", at_sec=12.0)
169
169
 
170
170
  ---
171
171
 
172
- # v0.3.0 新增:时间层门禁(G7)与页面层门禁(G8)
172
+ # 时间层门禁(G7)与页面层门禁(G8)
173
173
 
174
174
  > G0–G6 都是"某一帧/某一条音效"层面的检查。**时间层检查的是"看完全片才能发现的问题"**:
175
175
  > 单看任何一帧都正常,整片看下来却"一帧白闪过去了""这段没东西看""这一下没有声音"。
@@ -186,14 +186,14 @@ python qa_frames.py out/film.mp4 --cues work/cues.json \
186
186
  | 单帧闪烁 | 0 条 | `flicker[]` | 只亮一帧、前后帧都正常(`bbox` 指出位置) |
187
187
  | 画面空窗 | 首尾外 0 段 | `black_windows[]` | ≥0.8 s 平均亮度极低 |
188
188
  | 音画同步 | `\|dt\| ≤ 60 ms` | `sync[]` | 视觉事件与 cue 的时间差 |
189
- | **画面长时间静止(v0.4)** | 连续 6 s 内最活跃 8×8 块 0.5 s 漂移 ≤ 1.5 级灰度 | `warnings[]` | warning:可能卡帧,也可能是刻意定版 |
189
+ | **画面长时间静止** | 连续 6 s 内最活跃 8×8 块 0.5 s 漂移 ≤ 1.5 级灰度 | `warnings[]` | warning:可能卡帧,也可能是刻意定版 |
190
190
  | 音画同步"没在查" | `sync[]` 为空且 `sync_info.judged == 0` | warning(默认)/ error(开 `QA_REQUIRE_SYNC=1`) | **最危险的一条:看起来零超差,其实一条都没查** |
191
191
  | 时长复核 | 与预期差 ≤ 0.5 s | `errors[]` | error:独立于容器层再查一次 |
192
192
  | 响度曲线 | 无 >12 s 低于 -40 LUFS | `loudness[]` | warning:长段无声 |
193
193
 
194
- > **画面静止的判据在 v0.4 换过**:旧版用"全帧平均亮度 3 s 波动 < 2%",对逐字入场/数字滚动这类
195
- > 局部动画天生不敏感(自带示例片误报 4 段)。现在看的是"最活跃 8×8 块在 0.5 s 内的漂移",
196
- > 详见 `references/frame-qa.md` §2.4b。**别再按旧公式调阈值。**
194
+ > **画面静止的判据是"最活跃 8×8 块在 0.5 s 内的漂移"**,详见 `references/frame-qa.md` §2.4b。
195
+ > 不要用"全帧平均亮度 3 s 波动 < 2%"这类公式:全帧均值对逐字入场/数字滚动这类局部动画
196
+ > 天生不敏感(自带示例片会误报 4 段)。
197
197
 
198
198
  ### 单帧闪烁的判据(照抄实测有效的做法)
199
199
 
@@ -243,5 +243,5 @@ python webengine.py textqa ./web/index.html --step 0.2
243
243
  信号:-16.3 LUFS / 真峰 -1.2 dBTP / 锐度比 0.94 / 黑帧 0 / 字幕 86 条 = 剧本 86 句
244
244
  时间:单帧闪烁 0 / 画面空窗 0 / 音画同步最大偏差 38 ms
245
245
  页面:文字重叠 0 / 出画 0 / 一闪而过 0 / 从没露面 0
246
- 自检:python selfcheck.py -> PASS 55 / FAIL 0 / SKIP 0
246
+ 自检:python selfcheck.py -> PASS 69 / FAIL 0 / SKIP 0
247
247
  ```
@@ -191,7 +191,7 @@ out = canvas.resize((W, H), Image.LANCZOS)
191
191
 
192
192
  ### 路线 B:外挂/烧录 SRT/ASS(libass)
193
193
 
194
- 历史会话里踩过两个坑,必须记住:
194
+ 这条路有两个坑,必须记住:
195
195
 
196
196
  **坑 1:SRT 交 libass 渲染,字号会小得看不见。**
197
197
  libass 按 `PlayResY=288` 的虚拟分辨率排版,而你这个视频是 1080 高——**所有 FontSize / MarginV 要乘 3.75**(1080/288)。
@@ -215,7 +215,7 @@ PlayResY: 1080
215
215
 
216
216
  ### 880 硬规则
217
217
 
218
- 字幕带以下(`y > 880`)**不要放任何正文元素**。历史上多部片出现过"元素被字幕挡住 / 字幕压住图表"。
218
+ 字幕带以下(`y > 880`)**不要放任何正文元素**。真实项目里反复出现过"元素被字幕挡住 / 字幕压住图表"。
219
219
 
220
220
  ⚠ **这里没有"天然满足"**:`filmkit.SAFE` 的 y1 = 974 是**硬边界**(超了才报错),
221
221
  而字幕带 `SUB_BAND` 从 872 开始——两者重叠 102 px,正文画进 872–974 就会被字幕压住。
@@ -1,27 +1,20 @@
1
1
  # 场景语法(Scene Grammar):内容决定构图
2
2
 
3
- > v0.9.0 新增。配套文件:`skills/video-studio/scripts/grammar.py`(纯标准库)、
3
+ > 配套文件:`skills/video-studio/scripts/grammar.py`(纯标准库)、
4
4
  > `skills/video-studio/assets/scenes.py` 第三节(10 类绘制器)、`tools/silhouette_qa.py`(剪影门禁 G13)。
5
5
  > 一句话:**这一段内容该长成什么构图,由内容自己决定;风格只决定"长成什么样"。**
6
6
 
7
7
  ## 0. 为什么需要它
8
8
 
9
- v0.5 的"风格包"解决了"每部片的画风/配乐/旁白都差不多",但它只覆盖**皮肤**:
10
- 配色、字体族、纹理、颗粒、字距、缓动、HUD 三档、3 种版式几何。
9
+ 设计层只覆盖**皮肤**:配色、字体族、纹理、颗粒、字距、缓动、HUD 三档、3 种版式几何。
10
+ 皮肤换完,画面骨架并不会跟着变——同一套骨架(背景板 → 眉标 → 大字标题 → 副标题 → 强调线 → HUD)
11
+ 只换颜色、字号、位移,**剪影(灰度三值化后的前景占比 / 重心 / 边缘占用 / 对称度)几乎不动**。
11
12
 
12
- 画面骨架没有跟着变。证据在代码里:
13
+ 这不是审美判断,是可以量化的:用 `tools/silhouette_qa.py` 的判据两两对比 28 对同题材设计,
14
+ 同明暗族 12 对里有 4 对五项指标一项都不达标——换皮肤真的只是换色。
15
+ 用户的原话是"每个视频都差不多,只是主题和颜色不同"。
13
16
 
14
- - `assets/scenes.py` 的 v0.5 绘制器只有 3 个(`_DRAWERS` 三项:hook / body / outro);
15
- - 叙事 kind 里的 quote / emphasis / calm 全部回落到 body 绘制器(`_DRAWERS.get(kind) or _draw_body`);
16
- - 三个绘制器共用同一套骨架:背景板 → 眉标 → 大字标题 → 副标题 → 强调线 → HUD,
17
- 差异只在颜色、字号、位移;
18
- - 于是 8 套风格换完,**剪影(灰度三值化后的前景占比 / 重心 / 边缘占用 / 对称度)几乎不动**。
19
-
20
- 用户的原话是"每个视频都差不多,只是主题和颜色不同"。这不是审美判断,是可以量化的:
21
- 改造前用 `tools/silhouette_qa.py` 的判据跑 8 套风格两两对比(28 对),
22
- 同明暗族 12 对里有 4 对五项指标一项都不达标——也就是说换风格真的只是换色。
23
-
24
- v0.9 的做法不是"再加几套风格",而是把"内容 → 构图"这条选择逻辑独立成一层。
17
+ 所以缺的那一层不是"更多风格",而是把"内容 → 构图"这条选择逻辑独立出来。
25
18
 
26
19
  ## 1. 三层正交
27
20
 
@@ -169,7 +162,7 @@ python render.py --script film.json --scenes scenes.py --work work --stage layou
169
162
  |---|---|
170
163
  | `version` | 目前恒为 1 |
171
164
  | `generator` | `"grammar 0.9.0"`(改版本号时同步,便于区分不同版本的分镜表) |
172
- | `style` / `legacy` | 本片风格 id / 是否走 v0.4 兼容路径。**legacy 不落 storyboard**(见 §7) |
165
+ | `style` / `legacy` | 本片设计标识(`design:<hash>`)/ 兼容位(恒 `false`,见 §7) |
173
166
  | `scenes[]` | 每段一行:`id`、`kind`、`type`、`variant`(0–2)、`decor`(0–2)、`layout`、`dur`、`reason[]` |
174
167
  | `type_counts` | `{"hero": 1, "list": 2, ...}`,给了哪些类型用过几次 |
175
168
 
@@ -217,15 +210,15 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
217
210
  变体号、密度档、相位都**不按时间/帧号/随机数**派生——同一稿子同一结果,
218
211
  这是"可复现"的一部分。改 `chapter` 字段会改变 `variant_of` 的输入,进而改变变体号。
219
212
 
220
- ## 7. 没有 design 就没有画面(v1.0.1 起没有 legacy 路径)
213
+ ## 7. 没有 design 就没有画面(没有 legacy 路径)
221
214
 
222
- v1.0.1 删掉了 legacy 外观:不写 `style` 不再「走 v0.4 三个老绘制器」,而是**直接报错**并给两步迁移指引。
223
- 现在整条链路只认 `work/design.json`:
215
+ 不写 `style` 不会「走老绘制器」,而是**直接报错**,并给出两步指引(先提案、再确认)。
216
+ 整条链路只认 `work/design.json`:
224
217
 
225
218
  - `render.st_layout` 调 `grammar.plan_scenes`,按 `design.visual.layout_cycle` 做**按章节的版式轮换**,
226
219
  写 `work/storyboard.json`(每段的 type / variant / decor / layout / reason 都可审计);
227
220
  - `scenes.draw_frame` 只走 10 类构图绘制器(`_DRAWERS`),没有 legacy 分支;
228
- - 字幕样式来自 `design.visual.subtitle`(v1.0.2),字号/位置/背板/圆角不再写死;
221
+ - 字幕样式来自 `design.visual.subtitle`,字号/位置/背板/圆角由设计决定;
229
222
  - 金帧基线 `assets/golden_frames.json` 绑的是固定 fixture 设计(`assets/design_fixture.json`),
230
223
  不是「某套内置风格」——改绘制代码后按 `tools/golden_frames.py --write` 重录。
231
224
 
@@ -254,7 +247,7 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
254
247
  - 坐标过 `_view`(视差),文字用 `_draw_text`(字号不重采样);
255
248
  - 持续动效至少一个(相位用 `_phase(scene)`),否则 3–6 s 的静态画面会撞"画面静止"判据;
256
249
  - 重的东西预烘焙并合成进底版(参考 `_motif_layer` + `_plate`:一次烘焙,逐帧只 copy/EXTENT
257
- 约 1.3–2.0 ms)。**不要逐帧全屏 RGBA paste**(实测 5.6 ms,v0.9.0 踩过);
250
+ 约 1.3–2.0 ms)。**不要逐帧全屏 RGBA paste**(实测 5.6 ms,直接撑爆单帧预算);
258
251
  也不要每帧全屏 GaussianBlur / 全屏双线性 resize(预算 25 ms)。
259
252
  5. **`assets/scenes.py` 的 `_DRAWERS` 加键**。缺了会在渲染时回落 list 并 warn 一次。
260
253
  6. **`filmcheck.SCENE_TYPES` 加值**(这份只做成员校验,顺序无所谓),
@@ -265,7 +258,7 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
265
258
  再跑一次剪影门禁(`--selftest` / `--designs N`)确认新类型在不同现场设计下也"看得出来"。这一步不做,等于新加的构图
266
259
  没有进入 G13 的覆盖范围。
267
260
 
268
- 跑完的验证顺序:`python selfcheck.py`(**67 条**,第 52–55 条专盯场景语法/视觉语言/单真值/剪影)
261
+ 跑完的验证顺序:`python selfcheck.py`(**69 条**,第 52–55 条专盯场景语法/视觉语言/单真值/剪影)
269
262
  → `python <REPO>/tools/ci_check.py`(语法/包内容/文档路径)→ `python <REPO>/tools/silhouette_qa.py`(不同设计两两仍可区分)。
270
263
 
271
264
  ## 9. 常见误用
@@ -286,7 +279,7 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
286
279
  ## 10. 已知限制(本版没做,写下来别装看不见)
287
280
 
288
281
  - **web 引擎不跟随场景语法**:`--engine web` 的页面由用户自己写(`assets/web/film.js`),
289
- 本次不改造。web 路线的画风仍只有主题色,没有 10 类构图;
282
+ web 路线不经过 grammar。画风仍只有主题色,没有 10 类构图;
290
283
  剪影门禁也只覆盖 PIL 渲染器。
291
284
  - **同一部片仍锁一个明暗族**:段界跨明暗族会整屏换底,精确触发闪烁门禁,观感也像换片。
292
285
  主题覆盖(`scene["theme"]`)只允许同族。
@@ -302,7 +295,7 @@ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/fi
302
295
  - **部分视觉语言字段"已声明、已体检、但绘制器没读"**:`motion.enter`、`motion.push`、
303
296
  `typography.align`、`typography.ratio`由 `designkit.validate_design` 校验范围,
304
297
  但 `assets/scenes.py` 当前不读它们——写了不会报错,也不会改变画面。
305
- 详见 [style-packs.md](style-packs.md) 的"视觉语言包"一节。
298
+ 详见 [appearance.md](appearance.md) 的 §2 visual 字段表。
306
299
  - **场景数据超容量会被截断**:`stats` >3 / `series` >6 / `steps` >5 / `sides` 每侧 >3
307
300
  的条目不会出现在画面上。filmcheck 报 `scene_capacity` WARN(容量表在
308
301
  `skills/video-studio/scripts/filmcheck.py:652`),但**不阻断渲染**——
@@ -1,6 +1,6 @@
1
1
  # 音效设计(sfxkit)— 物理建模程序化音效库
2
2
 
3
- > 版本 v0.3.0 | 实现 `skills/video-studio/scripts/sfxkit.py` | 契约 CONTRACT.md §5.1
3
+ > 实现 `skills/video-studio/scripts/sfxkit.py` | 契约 CONTRACT.md §5.1
4
4
  >
5
5
  > 算法思想参考 **ai-history-film(MIT)** 的 `sfx/` 包(模态合成、泊松/Cox 纹理、镜像声源混响、
6
6
  > BS.1770 响度、块级动态处理)。本版本为**单文件重写**:为“确定性 / 无 scipy 也能跑 / 长片也能跑”
@@ -156,7 +156,7 @@ CUE_ALIASES # 语义别名表
156
156
 
157
157
  | 类别 | 配方 | 主要参数 | 物理依据 |
158
158
  |---|---|---|---|
159
- | 界面 | `ui_blip` | pitch=2200, kind=tick | FM 微音 + 塑料/玻璃接触瞬态,音高吸附 D 小调五声 |
159
+ | 界面 | `ui_blip` | pitch=2200, kind=tick | FM 微音 + 塑料/玻璃接触瞬态,音高吸附五声音阶(随配乐调性;未给 key 时 D 小调五声) |
160
160
  | 界面 | `ui_click` | press, release | 塑料外壳模态 + 接触噪声,按下/松开两声 |
161
161
  | 界面 | `relay_click` | count, gap | 衔铁金属模态 + 外壳闷响 + 触点回弹 |
162
162
  | 界面 | `roulette_tick` | last=False | 硬木舌拨木钉,两次接触;last 更低更重 |
@@ -197,7 +197,7 @@ CUE_ALIASES # 语义别名表
197
197
  | 389 s campfire | ≤ 8 s | **6.3–6.7 s** | — |
198
198
  | `--selftest` 全量 | ≤ 60 s | 约 11 s(无 scipy 时更久,含性能复核一次重试) | — |
199
199
 
200
- > 表格里的区间是**两次不同负载**的实测(v0.3 首测 / v0.4 复测),不是抖动范围猜测。
200
+ > 表格里的区间是**两次不同负载**下的实测,不是抖动范围猜测。
201
201
  > 所有数字都是"作者机器、空载或已知负载"口径,换机请用 `--bench-long` 重测。
202
202
 
203
203
  **长环境声的成本控制**(`_sustain`):短于 ~32 s 直接整段渲染;更长则渲染一段 24 s
@@ -214,8 +214,10 @@ CUE_ALIASES # 语义别名表
214
214
  ## 10. 集成方式
215
215
 
216
216
  1. **pil 引擎(filmkit.CueBus)**:`film.json` 的 `sfx.cues` → `CueBus` →
217
- `build_audio()` 逐条 `SX.render(type, seed=1000+i, **params)`,按 `pan` 展开立体声、
218
- 按 `CUE_BASE_DB` 与 `gain` 落位到音频总线;未知 type 告警一次并跳过。
217
+ `build_audio()` 逐条 `SX.render(type, seed=..., key=..., **params)`:seed 由
218
+ `content_seed(base, style, content_fp, recipe, t)` 内容派生(`sfx.content_seed=false` 时退回
219
+ `seed_base + i`),`key` 是配乐调性;按 `pan` 展开立体声、按 `CUE_BASE_DB` 与 `gain`
220
+ 落位到音频总线;未知 type 告警一次并跳过。
219
221
  2. **web 引擎**:页面 `window.getCues()` 导出同 schema 的 `work/cues.json`(CONTRACT §4.2)。
220
222
  3. **配乐(musicgen)**:和自己写音效一样,直接用 sfxkit 原语;`import sfxkit as S`,有 `sine/saw/supersaw/fm/pluck/adsr`、
221
223
  `lowpass/highpass/tv_filter`、`white/burst/exp_decay/attack_decay`、`smooth_random`、`Mix`。
@@ -1,6 +1,6 @@
1
1
  # 浏览器逐帧渲染引擎(webengine.py + assets/web/)
2
2
 
3
- > v0.3.0 新增的第二条渲染路线:无头浏览器打开 Canvas 页面,逐帧跳到「帧号 / FPS」后截 PNG,
3
+ > 第二条渲染路线:无头浏览器打开 Canvas 页面,逐帧跳到「帧号 / FPS」后截 PNG,
4
4
  > 经 ffmpeg image2pipe 编成 H.264。适合粒子、发光、连续形变、复杂矢量这类 PIL 画不动的画面;
5
5
  > 图文/数据/科普片的主体仍然推荐 PIL 路线(快 3–5 倍,调试成本低)。
6
6
 
@@ -111,7 +111,7 @@ assets/web/film.js 是 12 秒 / 3 个场景的完整模板:
111
111
  2. 不读上一帧状态:每帧清空画布、从头按 T 重画(不要写"每帧位移 += speed"这种累加)。
112
112
  3. 字体固定:canvas 内 fallback 到 "Microsoft YaHei", sans-serif;文档字体加载完才 ready。
113
113
  4. 视口默认 1920x1080、devicePixelRatio=1;画布 CSS 尺寸与分辨率一致,截图才是 1:1 像素。
114
- (v0.8:`style/width/height/fps` 可让画布跟着交付规格走——只允许 **16:9 横屏偶数尺寸**,
114
+ (`style/width/height/fps` 可让画布跟着交付规格走——只允许 **16:9 横屏偶数尺寸**,
115
115
  竖屏 / 4:3 / 奇数在 `open_page` 入口就 raise;尺寸由 `window.__STYLE__.canvas` 注入。)
116
116
  5. 动画相位只用绝对时间 T(如 Math.sin(T*2)),不要用帧计数差或挂钟。
117
117
  6. 离屏缓存(发光精灵)在 boot 阶段建好,之后只读不写。
@@ -123,21 +123,21 @@ assets/web/film.js 是 12 秒 / 3 个场景的完整模板:
123
123
  |---|---|
124
124
  | 1920x1080 PNG 截图 + 页面绘制 | 约 70–120 ms/帧(2 workers 时 70–85 ms/帧均摊) |
125
125
  | 12 秒 / 360 帧 / 2 workers / chunk 3s | 28.0 s 出片(约 78 ms/帧均摊) |
126
- | 自检(**35 项**,含真实无头浏览器、跨会话确定性验证与 v0.8 风格注入断言) | 约 60 s |
126
+ | 自检(**35 项**,含真实无头浏览器、跨会话确定性验证与风格注入断言) | 约 60 s |
127
127
 
128
128
  调参顺序:先降 workers(1–2 个足够,多了内存涨),再降 chunk_s(块小重渲代价低),
129
129
  最后才动 crf/preset(crf 17 / preset slow 是质量档;预览可用 crf 20 / preset veryfast)。
130
130
  PNG 截图比 JPEG q94 慢但无压缩伪影,本模块按契约固定 PNG。
131
131
 
132
- ### 8.1 v0.8:跟随风格包(`window.__STYLE__`)
132
+ ### 8.1 跟随设计(`window.__STYLE__`)
133
133
 
134
- `--engine web` 以前画风写死深蓝橙,现在是「`style` plan 驱动」:`open_page` 在 `page.goto`
134
+ `--engine web` 的画风由「`style` plan 驱动」:`open_page` 在 `page.goto`
135
135
  **之前**用 `add_init_script` 注入 `window.__STYLE__`(主题色板 / 版式 / 字体栈 / 画布 `w·h·fps`),
136
- `assets/web/engine.js` 与 `film.js` 优先读它、取不到就退回 v0.7 的深蓝橙常量。
136
+ `assets/web/engine.js` 与 `film.js` 优先读它、取不到就退回页面内置的深蓝橙常量。
137
137
 
138
138
  | 情形 | 行为 |
139
139
  |---|---|
140
- | `style=None`(pil 路线 / 显式关闭) | **连 `add_init_script` 都不调用**;页面里不存在 `window.__STYLE__`,像素与 v0.7 逐字节一致(自检有断言) |
140
+ | `style=None`(pil 路线 / 显式关闭) | **连 `add_init_script` 都不调用**;页面里不存在 `window.__STYLE__`,像素与不带注入时逐字节一致(自检有断言) |
141
141
  | 页面不认 `__STYLE__`(老 `engine.js`) | **当场报错**并给出入口页与 grep 提示,不能"截图能出图"就当通过 |
142
142
  | 注入的画布尺寸与页面实际画布不一致 | 报错(页面没按 `panel` 建画布);`fps` 注入了却与 `window.FPS` 不一致也报错 |
143
143