@dsh-gis/dsh-video-studio-skill 0.9.1 → 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.
- package/CHANGELOG.md +473 -0
- package/README.md +263 -327
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +9 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +31 -42
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1216 -1123
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +2 -4
- package/skills/video-studio/assets/golden_frames.json +13 -10
- package/skills/video-studio/assets/scenes.py +153 -65
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/appearance.md +123 -0
- package/skills/video-studio/references/audio-variation.md +74 -93
- package/skills/video-studio/references/audio.md +64 -66
- package/skills/video-studio/references/change-impact.md +5 -5
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +16 -18
- package/skills/video-studio/references/frame-qa.md +48 -16
- package/skills/video-studio/references/pipeline.md +4 -4
- package/skills/video-studio/references/pitfalls.md +22 -3
- package/skills/video-studio/references/proposal.md +160 -0
- package/skills/video-studio/references/quality-gates.md +11 -11
- package/skills/video-studio/references/scene-authoring.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +40 -42
- package/skills/video-studio/references/sfx-design.md +8 -6
- package/skills/video-studio/references/web-engine.md +8 -8
- package/skills/video-studio/references/word-timeline.md +9 -9
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +2115 -0
- package/skills/video-studio/scripts/filmcheck.py +22 -48
- package/skills/video-studio/scripts/filmkit.py +2980 -2904
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +73 -13
- package/skills/video-studio/scripts/musicgen.py +2042 -0
- package/skills/video-studio/scripts/preflight.py +14 -7
- package/skills/video-studio/scripts/previewkit.py +848 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +73 -39
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +89 -23
- package/skills/video-studio/scripts/render.py +545 -231
- package/skills/video-studio/scripts/scorekit.py +55 -1755
- package/skills/video-studio/scripts/selfcheck.py +815 -215
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +24 -8
- package/skills/video-studio/scripts/stylekit.py +207 -888
- package/skills/video-studio/scripts/voicekit.py +508 -387
- package/skills/video-studio/scripts/webengine.py +71 -72
- package/tools/audio_similarity.py +64 -37
- package/tools/ci_check.py +20 -14
- package/tools/estimate.py +34 -29
- package/tools/golden_frames.py +72 -28
- package/tools/silhouette_qa.py +520 -168
- package/docs/demo-pil-sheet.png +0 -0
- package/docs/styles/demo-results.json +0 -76
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
- package/skills/video-studio/references/style-packs.md +0 -283
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 提案层(Proposal)与设计 DSL
|
|
2
|
+
|
|
3
|
+
> 一句话:**出片之前先出方案;方案由用户拍板;拍板的那份设计被冻结成 design.json,渲染只认它。**
|
|
4
|
+
> 还有一条:**交付过的设计会进台账**,下一次提案主动避开——这是"不同片不重复"的落地机制。
|
|
5
|
+
|
|
6
|
+
## 0. 为什么要有这一层
|
|
7
|
+
|
|
8
|
+
长相不能由剧本决定,也不能由程序替用户拍板:同一份稿子该长什么样,是个需要看图的审美决策,
|
|
9
|
+
而"看过了再点一个"正是用户唯一能参与决策的位置。所以 `propose.py` 现场采样 N 个**两两可区分**
|
|
10
|
+
的设计,每个配 4 张静帧,你确认哪一个,那一个才被冻结成 `design.json`(渲染只认它)。
|
|
11
|
+
**没有"以某套预设为起点"这回事**,每次都是现场采样。
|
|
12
|
+
|
|
13
|
+
## 1. 三个产物
|
|
14
|
+
|
|
15
|
+
| 文件 | 谁写 | 语义 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `work/proposal.json` | `propose.py` | 一版提案:N 个候选(含 design + 静帧 + 成本 + 差异度 + 台账比对结果)+ 确认状态 |
|
|
18
|
+
| `work/design.json` | `propose.py --confirm` | **冻结**后的那一份设计(唯一真源,渲染只认它) |
|
|
19
|
+
| `work/proposals/revN/proposal.json` | `propose.py` | 每一版的快照(rev 不覆盖,可 diff、可回滚) |
|
|
20
|
+
|
|
21
|
+
## 2. 流程(零拷贝:脚本留在技能目录里直接跑)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1) 出一版方案(离线,不联网,秒级)
|
|
25
|
+
python <SKILL_DIR>/scripts/propose.py --brief "AI 大模型的三年,30 秒科普" --script film.json --work work --n 3
|
|
26
|
+
|
|
27
|
+
# 产物:work/proposal.json + work/proposals/rev1/{A,B,C}/_sheet.png + _compare.png
|
|
28
|
+
# 每个候选带:立意一句话、每个候选 4 张静帧、与其它候选的设计距离(n = 不同的维度数)
|
|
29
|
+
|
|
30
|
+
# 2) 不满意就提要求重出(新 rev,不覆盖旧的)
|
|
31
|
+
python <SKILL_DIR>/scripts/propose.py --revise "太冷了,要暖一点、女声、节奏快一点" --work work
|
|
32
|
+
|
|
33
|
+
# 3) 对比两版
|
|
34
|
+
python <SKILL_DIR>/scripts/propose.py --diff 1 2 --work work
|
|
35
|
+
|
|
36
|
+
# 4) 确认其中一个 -> 冻结 design.json(同时记进片间去重台账)
|
|
37
|
+
python <SKILL_DIR>/scripts/propose.py --confirm --candidate B --work work
|
|
38
|
+
|
|
39
|
+
# 5) 现在才允许出片。--scenes 不写就用剧本同目录 / 技能内置的渲染器
|
|
40
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/film.mp4 --stage all
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 3. 提案闸(frames 之前)
|
|
44
|
+
|
|
45
|
+
`render.py` 在 `frames` / `all` 之前会调 `_proposal_gate()`,判定五档:
|
|
46
|
+
|
|
47
|
+
| 状态 | 含义 | 默认行为 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `confirmed` | 已确认,且 `design.json` 指纹与确认时一致 | 放行(记 pass) |
|
|
50
|
+
| `absent` | 没有任何提案 | 警告(`--require-proposal` 下失败) |
|
|
51
|
+
| `draft` | 有提案但没确认 | 同上 |
|
|
52
|
+
| `stale` | 确认过但 `design.json` 被改过 | 同上 |
|
|
53
|
+
| `broken` | `proposal.json` 坏了 | 同上 |
|
|
54
|
+
|
|
55
|
+
为什么默认只警告:`--stage frames` 是开发期高频命令(改一帧看一帧)。要硬拦就加 `--require-proposal`。
|
|
56
|
+
|
|
57
|
+
`design.json` 同时是 stagewatch 的**第三个源头输入**(另外两个是 `film.json` 与 `scenes.py`):
|
|
58
|
+
改了设计但没重跑 `frames`,`--stage explain` 会直接报出来。
|
|
59
|
+
|
|
60
|
+
## 4. design.json 的形状
|
|
61
|
+
|
|
62
|
+
六块:`structure`(几段/什么用途/目标时长)、`visual`(配色/字体/母题/转场/动效/重心/版式/版式轮换/HUD/字幕/画幅)、
|
|
63
|
+
`audio`(声音/配乐/音效)、`pacing`(留白与停留)、`id`/`pitch`(标识与立意)。
|
|
64
|
+
|
|
65
|
+
完整契约见 `<REPO>/schemas/design.schema.json`;校验器是 `designkit.validate_design()`,
|
|
66
|
+
命令行入口 `python <SKILL_DIR>/scripts/designkit.py --lint work/design.json`。
|
|
67
|
+
|
|
68
|
+
### 4.1 fail-closed(这条最重要)
|
|
69
|
+
|
|
70
|
+
设计里**缺字段、枚举越界、对比度不达标**,一律报错,**绝不静默回落默认值**。
|
|
71
|
+
理由:**改了不生效也不报错**是这套流程里最贵的故障——审计实测过 11 个静默失效的旋钮
|
|
72
|
+
(`voice` 写字符串时 map/jitter 全失效、`theme_overrides.ease` 被视觉语言遮住、
|
|
73
|
+
`sfx.cues[].seed` 必崩、`pan` 对立体声配方无效……)。设计层不允许出现这种状态:
|
|
74
|
+
要么字段有效,要么当场报错。
|
|
75
|
+
|
|
76
|
+
### 4.2 visual.subtitle(可选)
|
|
77
|
+
|
|
78
|
+
字幕样式由设计 DSL 决定(不是 `filmkit` 的写死常量)。所有字段可选,**不写 = 补齐默认值,与默认常量逐像素一致**:
|
|
79
|
+
|
|
80
|
+
| 字段 | 默认 | 取值 | 说明 |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `size` | 46 | 20–96 | 字号 |
|
|
83
|
+
| `y` | null | 0–1030 | 字幕带基准线;null = 默认贴底安全区 |
|
|
84
|
+
| `align` | center | center / left | 对齐 |
|
|
85
|
+
| `plate_alpha` | 170 | 0–255 | 背板透明度(0 = 无背板) |
|
|
86
|
+
| `radius` | 10 | 0–48 | 背板圆角 |
|
|
87
|
+
| `family` | null | sans / serif / kai / round | null = 跟随大标题字族 |
|
|
88
|
+
| `color` | null | [r,g,b] 或 #rrggbb | null = 默认墨色 |
|
|
89
|
+
| `max_lines` | 2 | 1–3 | 最多折几行 |
|
|
90
|
+
|
|
91
|
+
渲染侧只读 `designkit.subtitle_of(design)` 这一份口径(`filmkit`、参考帧渲染共用),避免"字幕样式在两处漂移"。
|
|
92
|
+
|
|
93
|
+
### 4.3 visual.layout_cycle(可选)
|
|
94
|
+
|
|
95
|
+
按**章节**轮换版式:同一章内版式固定,换章才换。元素取 `left-stack` / `center-stack` / `split-right`,
|
|
96
|
+
**至少 2 种不同版式**(只写一种会被 `validate_design` 拒绝)。不写时 `design_to_plan` 按设计指纹自动派生
|
|
97
|
+
**2–3 种**——默认就不会"全片一张版式"。`--stage layout` 会把每段选的版式写进 `work/storyboard.json` 的
|
|
98
|
+
`layout` 字段,可逐段审计。
|
|
99
|
+
|
|
100
|
+
### 4.4 配色不是随便取色
|
|
101
|
+
|
|
102
|
+
采样器的配色走**对比度求解**:定一个色相锚点,在 (s,v) 网格里找「最接近设计意图且满足
|
|
103
|
+
`theme_lint` 全部对比度门槛」的颜色。实测:纯 HSV 随机取色的通过率 **21/60 = 35%**(不可用);
|
|
104
|
+
改成网格求解后 **60/60 = 100%**。配色是**连续色相空间**里的一点,不是"8 个命名配色族"。
|
|
105
|
+
|
|
106
|
+
## 5. 采样器(sample_designs)
|
|
107
|
+
|
|
108
|
+
输入 brief(题材 + 可选时长/必须有/不要),输出 N 个**两两可区分**的设计:
|
|
109
|
+
|
|
110
|
+
- **默认每次都不一样**:不传 `seed` 就生成新的 nonce,同一 brief 跑两次得到不同设计;
|
|
111
|
+
传 `--seed` 才逐字节可复现。这是"每条视频都不一样"的来源。
|
|
112
|
+
- 所有选择由 `sha1(nonce + brief + index + seed + attempt)` 派生,**禁 random / time**。
|
|
113
|
+
- **两两必须在 ≥2 个设计维度**上不同(10 个离散轴:配色 / 母题 / 转场 / 缓动 / 编号 / **配乐(调式+BPM 档+情绪)** /
|
|
114
|
+
声线 / 情绪 / 版式 / 调式),否则视为「伪多方案」(只换颜色不算)。
|
|
115
|
+
- **底色还要拉开色相**:色相差 < 6° 一律重采;< 12° 时要求结构差异 ≥ 3 个轴。
|
|
116
|
+
判重直接比色相距离,不按固定角度分桶:带 `--brief "深蓝"` 这类约束时可选色相窗口只有 52°,
|
|
117
|
+
分桶会让 n=3 有近一半概率采样失败;直接比距离时带约束的 brief 实测 **10/10 成功**。
|
|
118
|
+
- **片间去重台账**(见 §6):候选还要与"最近交付过的设计"至少差 2 维。
|
|
119
|
+
- 每个候选自带一句中文立意 `pitch`,写进提案卡。
|
|
120
|
+
|
|
121
|
+
## 6. 片间去重台账(默认开启)
|
|
122
|
+
|
|
123
|
+
"每条视频都不一样"不能只靠随机:同题材连拍三部仍可能撞到相近的配色/版式组合。
|
|
124
|
+
`propose.py` 把**确认过(= 真要出片)的设计**记进用户级台账,提案时主动避开:
|
|
125
|
+
|
|
126
|
+
| 项 | 值 |
|
|
127
|
+
|---|---|
|
|
128
|
+
| 默认路径 | `~/.dsh/video-studio/design-ledger.json`(最多保留 500 条) |
|
|
129
|
+
| 环境变量 | `VIDEO_STUDIO_LEDGER` 改路径;设为 `off` / `0` / `false` / `no` 即关闭 |
|
|
130
|
+
| CLI | `--no-ledger`(本次不读不写)/ `--show-ledger`(打印路径与最近 20 条) |
|
|
131
|
+
| 判据 | 候选与最近交付设计的最小距离 < **2 维** → 换新 nonce 重采(最多 4 轮),并输出告警 |
|
|
132
|
+
| 记录时机 | `--confirm` 时才记(草稿候选不进台账) |
|
|
133
|
+
|
|
134
|
+
读写失败/文件损坏一律当空表处理——**台账永远不会阻塞出片**。
|
|
135
|
+
封锁测试或写文档时把 `VIDEO_STUDIO_LEDGER` 指到临时目录,不要动用户真实台账。
|
|
136
|
+
|
|
137
|
+
## 7. 离线预览(previewkit)
|
|
138
|
+
|
|
139
|
+
**不联网、不合成旁白**:用 `FAKE_VOICE_DUR=2.0` 的合成时间轴排轴(同 `tools/golden_frames.py` 的口径),
|
|
140
|
+
渲 3–4 帧/候选 + 联系表 + N 个候选纵向并排的对比图。
|
|
141
|
+
|
|
142
|
+
成本(作者机器 2026-11-11 实测):单候选 3 帧 **0.6–0.9 s**;3 个候选含并排对比图 **约 2.1 s**;
|
|
143
|
+
全程离线。成本大头是每个候选的首帧准备(主题/母题派生),同一候选内多渲几张几乎不要钱。
|
|
144
|
+
换机会有波动,**别把这里的秒数当承诺**。
|
|
145
|
+
|
|
146
|
+
> 为什么不用 `--stage stills`:它缺 `tts.json` 时会**自动联网合成旁白**。
|
|
147
|
+
> 提案阶段不该付这个网络成本,也不该往用户的 TTS 缓存里写东西。
|
|
148
|
+
|
|
149
|
+
## 8. 常见误用
|
|
150
|
+
|
|
151
|
+
| 现象 | 原因 | 修法 |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| 改了 design.json 但画面没变 | 没重跑 frames(或没确认) | `python render.py --stage explain` 看是否 `source-changed` |
|
|
154
|
+
| `--confirm` 报「设计没过校验」 | 手改过 design.json 或采样器版本变了 | 按报错逐条修,或 `--revise` 重出 |
|
|
155
|
+
| 候选都「太像」 | 采样器的差异度约束被放宽了 | 看 `--show` 里的 `diversity.vs[].n`,< 2 就是伪多方案 |
|
|
156
|
+
| 提案闸一直 WARN | 没跑过 `propose.py --confirm` | 按提示两步走;要硬拦加 `--require-proposal` |
|
|
157
|
+
| 预览图和成片不一致 | 预览用合成时长,成片用实测时长 | 正常现象:静帧只用于看构图/配色,节奏以成片为准 |
|
|
158
|
+
| 「这批候选和上一部太像」告警 | 片间去重台账命中(< 2 维) | 自动重采过 4 轮仍撞:`--revise "换个完全不同的方向"`,或 `--no-ledger` 忽略 |
|
|
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`
|
|
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`
|
|
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 回读 |
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
|
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
|
-
>
|
|
195
|
-
>
|
|
196
|
-
>
|
|
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
|
|
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
|
-
>
|
|
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
|
-
|
|
10
|
-
|
|
9
|
+
设计层只覆盖**皮肤**:配色、字体族、纹理、颗粒、字距、缓动、HUD 三档、3 种版式几何。
|
|
10
|
+
皮肤换完,画面骨架并不会跟着变——同一套骨架(背景板 → 眉标 → 大字标题 → 副标题 → 强调线 → HUD)
|
|
11
|
+
只换颜色、字号、位移,**剪影(灰度三值化后的前景占比 / 重心 / 边缘占用 / 对称度)几乎不动**。
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
这不是审美判断,是可以量化的:用 `tools/silhouette_qa.py` 的判据两两对比 28 对同题材设计,
|
|
14
|
+
同明暗族 12 对里有 4 对五项指标一项都不达标——换皮肤真的只是换色。
|
|
15
|
+
用户的原话是"每个视频都差不多,只是主题和颜色不同"。
|
|
13
16
|
|
|
14
|
-
|
|
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
|
|
|
@@ -29,9 +22,9 @@ v0.9 的做法不是"再加几套风格",而是把"内容 → 构图"这条选
|
|
|
29
22
|
│ ① 场景语法:这段内容该长什么样(由内容决定,不由风格决定)
|
|
30
23
|
▼
|
|
31
24
|
scene_type ∈ {hero, statement, list, stat, compare, process, quote, chapter, chart, outro}
|
|
32
|
-
│ ②
|
|
25
|
+
│ ② 设计(design.json):这套设计怎么画(motion / motif / transition / typography / bias)
|
|
33
26
|
▼
|
|
34
|
-
|
|
27
|
+
work/design.json 的 visual 五组签名(designkit.design_to_plan 转成 plan)
|
|
35
28
|
│ ③ 片内编排:同一部片里连续段落怎么不"复制粘贴"(变体轮换 / 装饰密度 / 相位,确定性)
|
|
36
29
|
▼
|
|
37
30
|
draw_frame(...) → PIL.Image
|
|
@@ -40,9 +33,9 @@ v0.9 的做法不是"再加几套风格",而是把"内容 → 构图"这条选
|
|
|
40
33
|
|
|
41
34
|
| 层 | 由谁决定 | 落在哪 | 改它会怎样 |
|
|
42
35
|
|---|---|---|---|
|
|
43
|
-
| 叙事 kind | 作者写 `lines[].kind` | hook / body / quote / outro / emphasis / calm | 换旁白语气与配乐能量(voicekit /
|
|
36
|
+
| 叙事 kind | 作者写 `lines[].kind` | hook / body / quote / outro / emphasis / calm | 换旁白语气与配乐能量(voicekit / musicgen 读它) |
|
|
44
37
|
| 场景 type | grammar 按内容打分,或作者显式写 `lines[].type` | `work/storyboard.json`、timeline 的 `scene_type` | 换构图(10 个绘制器之一) |
|
|
45
|
-
|
|
|
38
|
+
| 设计 visual | `work/design.json`(由 `propose.py --confirm` 冻结) | `work/style.json` 的 `language` 与 `layout_cycle` | 换母题 / 转场 / 字距 / 重心 / 版式轮换 / 字幕样式 |
|
|
46
39
|
|
|
47
40
|
**为什么没有新增 kind 值**:kind 同时是 voicekit 的语气 id 与 filmcheck 的 `KINDS`
|
|
48
41
|
(两者是同一组 6 个值)。把 hero/stat 塞进 kind 会撞语气枚举,也让"引语"和"引语构图"
|
|
@@ -54,7 +47,7 @@ v0.9 的做法不是"再加几套风格",而是把"内容 → 构图"这条选
|
|
|
54
47
|
坐标基于 1920×1080;正文底界 860(下方是字幕带),HUD 顶部 ≤96。
|
|
55
48
|
"最短时长"是 `grammar.MIN_DUR`(`skills/video-studio/scripts/grammar.py:49`)的硬门槛,
|
|
56
49
|
低于它按降级表换类型(见 §3.3)。类型名单与顺序在 `grammar.SCENE_TYPES`(grammar.py:30),
|
|
57
|
-
绘制器注册表在 `assets/scenes.py
|
|
50
|
+
绘制器注册表在 `assets/scenes.py` 的 `_DRAWERS`(搜这个名字,行号会漂)。
|
|
58
51
|
|
|
59
52
|
| 类型 | 构图 / 重心 | 吃哪些字段 | 持续动效 | 自动选型特征 | 最短 |
|
|
60
53
|
|---|---|---|---|---|---|
|
|
@@ -111,7 +104,7 @@ v0.9 的做法不是"再加几套风格",而是把"内容 → 构图"这条选
|
|
|
111
104
|
|
|
112
105
|
几个刻意的取舍(都是实测反例逼出来的,别"优化"掉):
|
|
113
106
|
|
|
114
|
-
- **关键词只看叙述文字,不看 `items
|
|
107
|
+
- **关键词只看叙述文字,不看 `items`**。反例:示例片第 3 段正文讲"画质三件事",
|
|
115
108
|
条目里写的是 `rawvideo -> libx264 CRF17`,`->` 命中顺序词表,整段被误判成"流程轴"——
|
|
116
109
|
画面上一条横轴连着四个不相干的要点。条目是载荷不是叙事。
|
|
117
110
|
- **`hero`/`outro` 的结构分刻意低于字段分**:开场如果带 5 条要点,观众要看的是要点,
|
|
@@ -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` |
|
|
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,19 +210,25 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
|
|
|
217
210
|
变体号、密度档、相位都**不按时间/帧号/随机数**派生——同一稿子同一结果,
|
|
218
211
|
这是"可复现"的一部分。改 `chapter` 字段会改变 `variant_of` 的输入,进而改变变体号。
|
|
219
212
|
|
|
220
|
-
## 7.
|
|
213
|
+
## 7. 没有 design 就没有画面(没有 legacy 路径)
|
|
214
|
+
|
|
215
|
+
不写 `style` 不会「走老绘制器」,而是**直接报错**,并给出两步指引(先提案、再确认)。
|
|
216
|
+
整条链路只认 `work/design.json`:
|
|
221
217
|
|
|
222
|
-
`
|
|
218
|
+
- `render.st_layout` 调 `grammar.plan_scenes`,按 `design.visual.layout_cycle` 做**按章节的版式轮换**,
|
|
219
|
+
写 `work/storyboard.json`(每段的 type / variant / decor / layout / reason 都可审计);
|
|
220
|
+
- `scenes.draw_frame` 只走 10 类构图绘制器(`_DRAWERS`),没有 legacy 分支;
|
|
221
|
+
- 字幕样式来自 `design.visual.subtitle`,字号/位置/背板/圆角由设计决定;
|
|
222
|
+
- 金帧基线 `assets/golden_frames.json` 绑的是固定 fixture 设计(`assets/design_fixture.json`),
|
|
223
|
+
不是「某套内置风格」——改绘制代码后按 `tools/golden_frames.py --write` 重录。
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
timeline 里也不会出现 `scene_type` 等字段;
|
|
226
|
-
- `scenes.draw_frame`(`assets/scenes.py:2003`)读 `plan["legacy"]` 为真,
|
|
227
|
-
直接走 `_LEGACY_DRAWERS`(scenes.py:232,hook / body / outro 三个 v0.4 绘制器),逐字节不变;
|
|
228
|
-
- 金帧基线(`skills/video-studio/assets/golden_frames.json`)就是钉这条的:
|
|
229
|
-
legacy 项目跑出来的 3 帧像素与基线一致,改坏了立刻红。
|
|
225
|
+
没有 design 时的正确做法(脚本留在技能目录里,零拷贝):
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
```bash
|
|
228
|
+
python <SKILL_DIR>/scripts/propose.py --brief "题材/要求" --script film.json --work work
|
|
229
|
+
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
|
|
230
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/film.mp4 --stage all
|
|
231
|
+
```
|
|
233
232
|
|
|
234
233
|
## 8. 加第 11 类构图(8 步)
|
|
235
234
|
|
|
@@ -248,7 +247,7 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
|
|
|
248
247
|
- 坐标过 `_view`(视差),文字用 `_draw_text`(字号不重采样);
|
|
249
248
|
- 持续动效至少一个(相位用 `_phase(scene)`),否则 3–6 s 的静态画面会撞"画面静止"判据;
|
|
250
249
|
- 重的东西预烘焙并合成进底版(参考 `_motif_layer` + `_plate`:一次烘焙,逐帧只 copy/EXTENT
|
|
251
|
-
约 1.3–2.0 ms)。**不要逐帧全屏 RGBA paste**(实测 5.6 ms
|
|
250
|
+
约 1.3–2.0 ms)。**不要逐帧全屏 RGBA paste**(实测 5.6 ms,直接撑爆单帧预算);
|
|
252
251
|
也不要每帧全屏 GaussianBlur / 全屏双线性 resize(预算 25 ms)。
|
|
253
252
|
5. **`assets/scenes.py` 的 `_DRAWERS` 加键**。缺了会在渲染时回落 list 并 warn 一次。
|
|
254
253
|
6. **`filmcheck.SCENE_TYPES` 加值**(这份只做成员校验,顺序无所谓),
|
|
@@ -256,17 +255,17 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
|
|
|
256
255
|
7. **`selfcheck.py` 第 54 条(`scripts/selfcheck.py:1526`)会自动兜底**:它断言 grammar / filmcheck / scenes.`_DRAWERS` /
|
|
257
256
|
schema enum 四处**集合相等**(顺序允许不同)。这四份真值来源必须同时改,漏一处立刻红。
|
|
258
257
|
8. **`tools/silhouette_qa.py` 的 `SCENE_CONTENT` 补一段合成内容**(给足该类型的原料字段),
|
|
259
|
-
|
|
258
|
+
再跑一次剪影门禁(`--selftest` / `--designs N`)确认新类型在不同现场设计下也"看得出来"。这一步不做,等于新加的构图
|
|
260
259
|
没有进入 G13 的覆盖范围。
|
|
261
260
|
|
|
262
|
-
跑完的验证顺序:`python selfcheck.py
|
|
263
|
-
|
|
261
|
+
跑完的验证顺序:`python selfcheck.py`(**69 条**,第 52–55 条专盯场景语法/视觉语言/单真值/剪影)
|
|
262
|
+
→ `python <REPO>/tools/ci_check.py`(语法/包内容/文档路径)→ `python <REPO>/tools/silhouette_qa.py`(不同设计两两仍可区分)。
|
|
264
263
|
|
|
265
264
|
## 9. 常见误用
|
|
266
265
|
|
|
267
266
|
| 现象 | 原因 | 修法 |
|
|
268
267
|
|---|---|---|
|
|
269
|
-
| 分镜表里一段都没变,全是 list |
|
|
268
|
+
| 分镜表里一段都没变,全是 list | 没有 `work/design.json`,或 grammar 没跑 | 写 先 `propose.py --confirm` 冻结设计;再 `--stage layout` 看分镜表 |
|
|
270
269
|
| 某段想要数字卡,结果画成列表 | 没有 `stats[]`,文本里的数字是"2024 年"(年份被排除)或没有单位 | 显式写 `stats[]`;或让那句话带"数字 + 单位" |
|
|
271
270
|
| 写了 `type: "stats"` 却没生效,还报错 | `type` 是枚举,`stats` 才是字段名;写错值 filmcheck 报 ERROR | 改 `type` 为 10 类之一,数据放 `stats[]` |
|
|
272
271
|
| 对比段左右两栏一栏空 | 只有 1 条 `items`(对半分不出来),或 `sides{}` 只写了一侧 | 补到 ≥2 条,或显式写 `sides` 两侧 |
|
|
@@ -280,7 +279,7 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
|
|
|
280
279
|
## 10. 已知限制(本版没做,写下来别装看不见)
|
|
281
280
|
|
|
282
281
|
- **web 引擎不跟随场景语法**:`--engine web` 的页面由用户自己写(`assets/web/film.js`),
|
|
283
|
-
|
|
282
|
+
web 路线不经过 grammar。画风仍只有主题色,没有 10 类构图;
|
|
284
283
|
剪影门禁也只覆盖 PIL 渲染器。
|
|
285
284
|
- **同一部片仍锁一个明暗族**:段界跨明暗族会整屏换底,精确触发闪烁门禁,观感也像换片。
|
|
286
285
|
主题覆盖(`scene["theme"]`)只允许同族。
|
|
@@ -294,13 +293,12 @@ timeline 顶层再加一个 `storyboard: {"type_counts": ...}` 摘要。
|
|
|
294
293
|
但当前只有 hero / statement / list / stat 用它,其余六类构图唯一。
|
|
295
294
|
这条不修的话,"每类型 3 个变体"只在四类上成立。
|
|
296
295
|
- **部分视觉语言字段"已声明、已体检、但绘制器没读"**:`motion.enter`、`motion.push`、
|
|
297
|
-
`typography.align`、`typography.ratio
|
|
298
|
-
`stylekit.VISUAL_LANGUAGES` 里声明、`stylekit.visual_lint` 会查范围,
|
|
296
|
+
`typography.align`、`typography.ratio`由 `designkit.validate_design` 校验范围,
|
|
299
297
|
但 `assets/scenes.py` 当前不读它们——写了不会报错,也不会改变画面。
|
|
300
|
-
详见 [
|
|
298
|
+
详见 [appearance.md](appearance.md) 的 §2 visual 字段表。
|
|
301
299
|
- **场景数据超容量会被截断**:`stats` >3 / `series` >6 / `steps` >5 / `sides` 每侧 >3
|
|
302
300
|
的条目不会出现在画面上。filmcheck 报 `scene_capacity` WARN(容量表在
|
|
303
301
|
`skills/video-studio/scripts/filmcheck.py:652`),但**不阻断渲染**——
|
|
304
302
|
WARN 不是通过,也不是失败,是"画面上没有"。
|
|
305
303
|
- **`work/storyboard.json` 是审计产物,不是输入**:改它不会改变渲染结果;
|
|
306
|
-
想要固定构图请改 `film.json` 的 `lines[].type`。
|
|
304
|
+
想要固定构图请改 `film.json` 的 `lines[].type`。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 音效设计(sfxkit)— 物理建模程序化音效库
|
|
2
2
|
|
|
3
|
-
>
|
|
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 微音 +
|
|
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
|
-
>
|
|
200
|
+
> 表格里的区间是**两次不同负载**下的实测,不是抖动范围猜测。
|
|
201
201
|
> 所有数字都是"作者机器、空载或已知负载"口径,换机请用 `--bench-long` 重测。
|
|
202
202
|
|
|
203
203
|
**长环境声的成本控制**(`_sustain`):短于 ~32 s 直接整段渲染;更长则渲染一段 24 s
|
|
@@ -214,10 +214,12 @@ 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
|
|
218
|
-
|
|
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
|
-
3. **配乐(
|
|
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`。
|
|
222
224
|
|
|
223
225
|
## 11. 自检
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 浏览器逐帧渲染引擎(webengine.py + assets/web/)
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
-
|
|
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
|
|
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
|
|
132
|
+
### 8.1 跟随设计(`window.__STYLE__`)
|
|
133
133
|
|
|
134
|
-
`--engine web`
|
|
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`
|
|
136
|
+
`assets/web/engine.js` 与 `film.js` 优先读它、取不到就退回页面内置的深蓝橙常量。
|
|
137
137
|
|
|
138
138
|
| 情形 | 行为 |
|
|
139
139
|
|---|---|
|
|
140
|
-
| `style=None`(pil 路线 / 显式关闭) | **连 `add_init_script` 都不调用**;页面里不存在 `window.__STYLE__
|
|
140
|
+
| `style=None`(pil 路线 / 显式关闭) | **连 `add_init_script` 都不调用**;页面里不存在 `window.__STYLE__`,像素与不带注入时逐字节一致(自检有断言) |
|
|
141
141
|
| 页面不认 `__STYLE__`(老 `engine.js`) | **当场报错**并给出入口页与 grep 提示,不能"截图能出图"就当通过 |
|
|
142
142
|
| 注入的画布尺寸与页面实际画布不一致 | 报错(页面没按 `panel` 建画布);`fps` 注入了却与 `window.FPS` 不一致也报错 |
|
|
143
143
|
|
|
@@ -164,7 +164,7 @@ PNG 截图比 JPEG q94 慢但无压缩伪影,本模块按契约固定 PNG。
|
|
|
164
164
|
|
|
165
165
|
## 10. 和别的模块怎么接
|
|
166
166
|
|
|
167
|
-
- **配乐 / 音效**:webengine.py cues 输出 work/cues.json(§4.2),
|
|
167
|
+
- **配乐 / 音效**:webengine.py cues 输出 work/cues.json(§4.2),musicgen / sfxkit 按 t 对齐;
|
|
168
168
|
页面 cue 的 type 用 sfxkit 配方名(ui_blip / whoosh / impact / data_chirp ...)。
|
|
169
169
|
- **音画同步质检**:页面 S.event 登记的视觉事件进 work/cues.json 的 events,qa_frames.sync_check 用。
|
|
170
170
|
- **合成成片**:webengine 只出无声画面;配音/配乐/混音由 filmkit 音频总线和 render.py 的 mux 阶段负责。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 词级时间轴与词锚点
|
|
2
2
|
|
|
3
3
|
> 本页是 `work/words.json` 的**契约**、`cue at` 词锚点的**语义**,以及四个在真实跑里踩过的坑。
|
|
4
4
|
> 设计前提一句话:**句子时长仍以 TTS 实测时长为权威**;词级时间戳只是附加值,
|
|
@@ -28,8 +28,8 @@ timeline.json 的 scenes[].words_abs (句内词表平移到绝对时间)
|
|
|
28
28
|
| 防呆 | 为什么 |
|
|
29
29
|
|---|---|
|
|
30
30
|
| 显式 `boundary="WordBoundary"` + `stream()` | 用 `save()` 会把词事件丢掉;不传 `boundary` 时默认 `SentenceBoundary`,**一条词事件都收不到**(实测 edge-tts 7.3.1 / zh-CN:词事件数 = 0) |
|
|
31
|
-
| 音频先写 `.part` 再 `os.replace` |
|
|
32
|
-
| 词表独立缓存键 `tts_words_key` | 并进 `tts_key_v2`
|
|
31
|
+
| 音频先写 `.part` 再 `os.replace` | 直接 `open(目标, "wb")` 时,一次断网/超时就把**已缓存的好音频截成 0 字节**;先写 `.part` 再替换,失败只丢本次新音频,旧缓存还在 |
|
|
32
|
+
| 词表独立缓存键 `tts_words_key` | 并进 `tts_key_v2` 会让**全部已有音频缓存一次性失效**(必须整片重新联网合成)。分开记 -> 已有音频照旧复用,只在"这次显式要词级"时补采一次 |
|
|
33
33
|
|
|
34
34
|
### 1.1 `work/words.json` 契约
|
|
35
35
|
|
|
@@ -68,9 +68,9 @@ G10 会因此记 `skipped` 或 `fail`(看是全部为空还是个别句为空
|
|
|
68
68
|
| `words_summary(words, dur=None)` | 出 `n / covered / gaps / tail / mono / start`(G10 判据全在这里算) | — |
|
|
69
69
|
| `sub_words_at(scenes, t)` | 当前上屏字幕对应的 `words_abs`,没有就 `None` | 无词级 -> `None`(调用方回退整句) |
|
|
70
70
|
| `wrap_by_words(text, f, max_w, words)` | 按真实词边界折行;词表与原文对不上(有尾差/切不开)就返回 `None` | `None` -> `fit_sub_lines` 回退字符折行 |
|
|
71
|
-
| `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 排版入口,多一个可选参数 | `words=None`
|
|
71
|
+
| `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 排版入口,多一个可选参数 | `words=None` 时排版与不带词级时逐像素一致 |
|
|
72
72
|
| `build_timeline(..., fps=)` | 给每段补 `words_abs`(句内词表平移到绝对时间) | 该句没有 `words` 就没有 `words_abs` |
|
|
73
|
-
| `cue_time(at, scenes=, total=)` | 解析 `at
|
|
73
|
+
| `cue_time(at, scenes=, total=)` | 解析 `at`(含 `l03:w2` 词锚点) | 见 §3 |
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -88,7 +88,7 @@ G10 会因此记 `skipped` 或 `fail`(看是全部为空还是个别句为空
|
|
|
88
88
|
`[WARN] cue at='l03:w99':l03 只有 13 个词,w99 不存在,退化为句偏移`,然后按 `l03+偏移` 处理。
|
|
89
89
|
—— 所以**改完台词一定要看 stdout 有没有这条 WARN**,否则你会以为自己精确打到了某个词。
|
|
90
90
|
- 场景 id 本身不存在时才 `raise ValueError`(先跑 layout,或者改用 `abs:秒`)。
|
|
91
|
-
-
|
|
91
|
+
- `abs:12.5` / `12.5` / `l03` / `l03+0.35` 这几种写法不含词锚点,行为与不带词级时**完全一致**。
|
|
92
92
|
|
|
93
93
|
---
|
|
94
94
|
|
|
@@ -97,13 +97,13 @@ G10 会因此记 `skipped` 或 `fail`(看是全部为空还是个别句为空
|
|
|
97
97
|
### 4.1 词间有 0.1–0.3 s 的空隙
|
|
98
98
|
|
|
99
99
|
edge-tts 的 WordBoundary 只覆盖"发音区间",相邻词之间的停顿**不在任何事件里**。
|
|
100
|
-
所以 `covered = Σ dur`
|
|
100
|
+
所以 `covered = Σ dur` 永远小于句长。示例片实测 4 句 64 词,覆盖率约 0.7 量级。
|
|
101
101
|
**判据不能写 `covered == dur`**——G10 的覆盖率下限是 **45%**(低于它才是"明显少了")。
|
|
102
102
|
|
|
103
103
|
### 4.2 句尾静音不在事件里(约 0.5 s)
|
|
104
104
|
|
|
105
105
|
排轴用的是**整句实测时长**,不是词时长求和。G10 只检查"末词终点离句尾不超过 **0.40 s**"
|
|
106
|
-
|
|
106
|
+
(示例片实测 0.09 s)。这条超过 0.40 s,通常不是"片子坏了",而是词事件本身整体错位了。
|
|
107
107
|
|
|
108
108
|
### 4.3 裁剪会让词整体提前(`words_shift`)
|
|
109
109
|
|
|
@@ -141,7 +141,7 @@ python render.py ... --stage qa # 看 QC-REPORT.md 的 G10 行
|
|
|
141
141
|
## 6. 关掉它
|
|
142
142
|
|
|
143
143
|
- 单片:`film.json` 写 `"words": false` -> `--stage tts` 不再收词事件,G10 `skipped`(写明"本片未启用"),
|
|
144
|
-
|
|
144
|
+
字幕退回整句折行。**不要词级能力就写这一行。**
|
|
145
145
|
- 只跳过门禁不跳过能力:没有开关——G10 就是词级数据的体检,没数据就 skipped,不会 fail。
|
|
146
146
|
- 词事件拿不到(服务端变了):不阻断出片,G10 `skipped` / `fail` 二选一看覆盖面,
|
|
147
147
|
meta 里 `words_ok=false`、`words_events=0` 是排查入口。
|