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

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 (54) hide show
  1. package/CHANGELOG.md +582 -0
  2. package/README.md +287 -304
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/package.json +11 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +171 -15
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1316 -1066
  9. package/skills/video-studio/assets/design_fixture.json +141 -0
  10. package/skills/video-studio/assets/film.json +1 -2
  11. package/skills/video-studio/assets/golden_frames.json +12 -9
  12. package/skills/video-studio/assets/scenes.py +2207 -1190
  13. package/skills/video-studio/assets/voice_loudness.json +16 -0
  14. package/skills/video-studio/references/audio-variation.md +15 -11
  15. package/skills/video-studio/references/audio.md +36 -34
  16. package/skills/video-studio/references/change-impact.md +3 -3
  17. package/skills/video-studio/references/frame-qa.md +2 -2
  18. package/skills/video-studio/references/proposal.md +177 -0
  19. package/skills/video-studio/references/quality-gates.md +2 -2
  20. package/skills/video-studio/references/scene-grammar.md +311 -0
  21. package/skills/video-studio/references/sfx-design.md +1 -1
  22. package/skills/video-studio/references/style-packs.md +107 -168
  23. package/skills/video-studio/references/v08-quality.md +8 -8
  24. package/skills/video-studio/references/web-engine.md +1 -1
  25. package/skills/video-studio/references/word-timeline.md +2 -2
  26. package/skills/video-studio/scripts/designkit.py +2120 -0
  27. package/skills/video-studio/scripts/filmcheck.py +104 -33
  28. package/skills/video-studio/scripts/filmkit.py +153 -78
  29. package/skills/video-studio/scripts/grammar.py +619 -0
  30. package/skills/video-studio/scripts/musicgen.py +2043 -0
  31. package/skills/video-studio/scripts/preflight.py +10 -3
  32. package/skills/video-studio/scripts/previewkit.py +849 -0
  33. package/skills/video-studio/scripts/propose.py +825 -0
  34. package/skills/video-studio/scripts/qa_frames.py +45 -10
  35. package/skills/video-studio/scripts/qavmaf.py +77 -11
  36. package/skills/video-studio/scripts/render.py +505 -142
  37. package/skills/video-studio/scripts/scorekit.py +50 -1748
  38. package/skills/video-studio/scripts/selfcheck.py +851 -115
  39. package/skills/video-studio/scripts/stagewatch.py +21 -5
  40. package/skills/video-studio/scripts/stylekit.py +293 -570
  41. package/skills/video-studio/scripts/voicekit.py +502 -380
  42. package/skills/video-studio/scripts/webengine.py +15 -14
  43. package/tools/audio_similarity.py +57 -30
  44. package/tools/ci_check.py +9 -1
  45. package/tools/estimate.py +14 -8
  46. package/tools/golden_frames.py +56 -11
  47. package/tools/silhouette_qa.py +1263 -0
  48. package/docs/demo-pil-sheet.png +0 -0
  49. package/docs/styles/demo-results.json +0 -76
  50. package/docs/styles/sheet-8-styles.png +0 -0
  51. package/docs/styles/sheet-ai.png +0 -0
  52. package/docs/styles/sheet-craft.png +0 -0
  53. package/docs/styles/sheet-night.png +0 -0
  54. package/skills/video-studio/assets/score_demo.json +0 -307
@@ -0,0 +1,16 @@
1
+ {
2
+ "schema": 1,
3
+ "_comment": "音色响度补偿(数据,不是身份):gain_db = 目标响度 - 该音色实测 rms_db,夹在 ±3 dB。换机/换 voice 必须重测(python voicekit.py --calibrate 会对表里的音色重新合成校准句)。designkit 与 voicekit 共用这一份:两处各存一份必然漂移——实测采样器会产出 4 把表里没有的音色。",
4
+ "target_dbfs": -20.97,
5
+ "voices": {
6
+ "zh-CN-YunyangNeural": 3.0,
7
+ "zh-CN-XiaoxiaoNeural": -2.7,
8
+ "zh-TW-HsiaoChenNeural": -3.0,
9
+ "zh-CN-XiaoyiNeural": -0.6,
10
+ "zh-CN-YunjianNeural": 1.0,
11
+ "zh-CN-YunxiNeural": 1.9,
12
+ "zh-CN-YunxiaNeural": 0.3,
13
+ "zh-CN-liaoning-XiaobeiNeural": 0.0,
14
+ "zh-CN-shaanxi-XiaoniNeural": -0.3
15
+ }
16
+ }
@@ -1,5 +1,9 @@
1
- # 音频多样性改造(v0.7)—— 接口契约
1
+ # 音频多样性改造(v0.7)—— 接口契约(**已归档**)
2
2
 
3
+ > **v1.0.1 起本文描述的 scorekit 风格层已删除**(`MUSIC_STYLES` / `render_styled` /
4
+ > 指纹结构变奏)。保留本文是因为音效侧契约(sfxkit / CueBus / duck / 旁白优先)仍然有效;
5
+ > 「A 路配乐变奏」那一节只作历史记录——现在配乐由 `musicgen.py` 参数化编排,没有风格表。
6
+ >
3
7
  > 起草:lead,2026-11-09。本文件是本次改造的**唯一定义源**。
4
8
  > 三路并行改动的文件写域不重叠,但**接口必须按本文件逐字实现**,否则集成会失败。
5
9
  > 诊断证据见 `tools/audio_similarity.py` 的输出(改完后必须重跑,阈值见 §5)。
@@ -16,11 +20,11 @@
16
20
 
17
21
  | 路 | 负责文件 | 改什么 |
18
22
  |---|---|---|
19
- | A 配乐变奏 | `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
23
+ | A 配乐变奏(**v1.0.1 已删除**) | 原 `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
20
24
  | B 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`cues()` 按画面内容生成;模板 cue 去固定化 |
21
25
  | C 接线与混音 | `skills/video-studio/scripts/filmkit.py`(lead)、`render.py`(lead)、`stylekit.py`(只读)、`selfcheck.py`(lead) | CueBus 内容派生 seed、调性传给 sfxkit、duck 加深、风格过门避让 |
22
26
 
23
- **写域规则**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
27
+ **写域规则(历史)**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
24
28
  C 路由 lead 改 filmkit.py / render.py / selfcheck.py。**任何一路不得改别人的文件**(包括顺手"优化")。
25
29
 
26
30
  ## 3. 接口契约(逐字实现,向后兼容)
@@ -56,7 +60,7 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
56
60
  并把 key 透传给配方(render(kind, seed=..., key=key, **params))。"""
57
61
  ```
58
62
 
59
- ### 3.3 scorekit.render_styled(A 路)
63
+ ### 3.3 scorekit.render_styled(A 路)——**v1.0.1 已删除,仅存档**
60
64
 
61
65
  签名不变。**内部**行为变化:
62
66
 
@@ -71,8 +75,8 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
71
75
  - **必须保证**:同 `fingerprint` 逐采样一致;不同 fingerprint 的相关性显著下降(§5 阈值)。
72
76
  - 新增报告字段:`variation`(dict:`{"chord_swaps": n, "motif_shifts": n, "drum_ghosts": n}`),
73
77
  写进 work/score.json,方便审计。
74
- - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的第 20 条会验;
75
- 别和 `selfcheck.py` 的编号搞混——那边第 21 条只查 scorekit 同 seed 确定性)。
78
+ - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的 legacy 条会验)。
79
+ (v1.0.1:`render_styled` 已删除,这一条现在只说明历史契约。)
76
80
 
77
81
  ### 3.4 assets/scenes.py 的 cues()(B 路)
78
82
 
@@ -99,7 +103,7 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
99
103
  - `st_sfx`:读 `script["_style"]`,把 `style` / `content_fp` / `key` / `sfx.content_seed` 传给
100
104
  `bus.build_audio(...)`;没有 `_style`(legacy)时不传,保持旧行为。
101
105
  - `st_audio`:duck 默认值改为按风格:在现有表基础上整体 +1.0 dB,且 `music.duck_db` 显式值仍然优先。
102
- 另外新增**过门避让**:`scorekit` 的过门音效已经在 music.wav 里,sfx 轨在
106
+ 另外新增**过门避让**:配乐在 music.wav 里已经有段落过门,sfx 轨在
103
107
  [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB(在 filmkit 里做,
104
108
  或 st_sfx 后处理;由 lead 决定实现位置,**B 路不要碰**)。
105
109
 
@@ -129,11 +133,11 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
129
133
  > 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
130
134
  > 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
131
135
 
132
- 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 51 条;v0.7.2 时的 43 条是旧的)。
133
- 2. `python skills/video-studio/scripts/scorekit.py --selftest` 全 PASS。
136
+ 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 **67 条**;v0.7.2 时的 43 条、v0.8 的 51 条是旧的)。
137
+ 2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS(原第 2 条要求 scorekit 风格层自测,v1.0.1 已随风格层删除)。
134
138
  3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
135
139
  4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
136
- 5. legacy 逐样本不变:`scorekit.py --selftest` 第 20 条 + `selfcheck.py` 第 26(风格 legacy)/33(音效 legacy)条,**这些断言不要改**。
140
+ 5. 音效 legacy 逐样本不变:`selfcheck.py` 的音效 legacy 断言(v1.0.1 起风格 legacy 断言随风格层一并删除)。
137
141
 
138
142
  ## 6. 协作规则
139
143
 
@@ -156,4 +160,4 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
156
160
  **回归基线**(改前先录、改后逐条比对):
157
161
 
158
162
  - sfxkit 9 条渲染的 sha256(ui_blip ×3 / answer_chime / text_stream ×2 / data_chirp / roulette_tick ×2)逐条相同。
159
- - scorekit legacy 路径(style=None)6 s 渲染逐样本与 v0.6 相同。
163
+ - 配乐 legacy 路径(scorekit,style=None)6 s 渲染逐样本与 v0.6 相同(v1.0.1 起该路径只保留合成原语,不再承担成片配乐)。
@@ -1,7 +1,7 @@
1
1
  # 音频:采样级混音、配乐与音效
2
2
 
3
3
  > **v0.3.0 新增三节**(§10–§12 是新的推荐做法,§1–§9.5 是底层原理与手写示例,仍然有效):
4
- > §10 提示点系统(声音跟着画面走)、§11 程序化配乐(scorekit)、§12 程序化音效(sfxkit)。
4
+ > §10 提示点系统(声音跟着画面走)、§11 程序化配乐(musicgen 参数化编排)、§12 程序化音效(sfxkit)。
5
5
  > 先读 §10——它决定了另外两节怎么用。
6
6
 
7
7
  ## 1. 为什么不用 ffmpeg 混音
@@ -139,33 +139,33 @@ trimmed = K.trim_silence(pcm, thresh_db=-42.0, keep_ms=80) # 不是 trim_tail
139
139
  # 轻 EQ:200 Hz 以下衰减 2 dB(去闷),3 kHz 提 1 dB(清晰度)
140
140
  ```
141
141
 
142
- ### 7.1 声线(persona)与语气(tone)—— v0.5.0
142
+ ### 7.1 声线(edge voice id)与语气(tone)—— v1.0.1 起没有 persona 身份表
143
143
 
144
- v0.4 及以前全片只有一个 `voice/rate/pitch`,于是"每部片的旁白都差不多"。
145
- v0.5 引入 **persona(声线)× tone(语气档)**,由 `voicekit.py` 解析成逐句 spec:
144
+ > **v0.5 的 persona 表(`bright_female` / `narrator_male` 之类 9 个身份)在 v1.0.0 已删除。**
145
+ > 现在 `voice` 只认**裸 edge-tts voice id**(如 `zh-CN-YunxiNeural`)+ 实测响度补偿表
146
+ > `assets/voice_loudness.json`;tone 的 6 档(hook/body/quote/outro/emphasis/calm)保留。
146
147
 
147
148
  ```json
148
- {"voice": {"map": {"hook": "bright_female", "body": "narrator_female", "quote": "authority_male"},
149
+ {"voice": {"id": "zh-CN-YunxiNeural",
150
+ "map": {"hook": "zh-CN-YunxiaNeural", "body": "zh-CN-YunxiNeural",
151
+ "quote": "zh-CN-YunyangNeural", "outro": "zh-CN-YunxiNeural"},
149
152
  "jitter": 0.35, "max_distinct": 3}}
150
153
  ```
151
154
 
152
155
  ```bash
153
- python voicekit.py --list # 9 个 persona + 6 个 tone(含实测响度补偿 gain_db)
154
- python voicekit.py --plan film.json # 打印某份剧本的逐句声线计划(不发请求)
156
+ python <SKILL_DIR>/scripts/voicekit.py --list # 音色响度表(裸 id + 实测 gain_db)与 6 档 tone
157
+ python <SKILL_DIR>/scripts/voicekit.py --plan film.json # 打印逐句声线计划(不发请求)
155
158
  ```
156
159
 
157
- - **persona** 决定 edge-tts 音色 id + 基线 rate/pitch/volume/gain_db;
160
+ - **voice id** 直接就是 edge-tts 的音色名;`--list` 里带的是**实测响度补偿**(不同音色天然差 3–7 dB,
161
+ 实测跨度 6.0 dB)。写了表里没有的音色,补偿回落 0 = 旁白电平没校准。
158
162
  - **tone** 决定相对偏移与句前/句后停顿:hook / body / quote / outro / emphasis / calm
159
- (`body` 严格等于 v0.4 基准:lead 0.62 / tail 0.92 / 所有 delta=0);
160
- - **内容规则**会自动升级语气:引号句 -> quote(并换到 quote 声线)、数字密集 -> emphasis、
161
- 问句/感叹 -> 稍快稍亮、长句 -> 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高;
162
- - **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感;
163
+ (`body` 严格等于 v0.4 基准:lead 0.62 / tail 0.92 / 所有 delta=0)。
164
+ - **内容规则**会自动升级语气:引号句 → quote(并换到 quote 声线)、数字密集 → emphasis、
165
+ 问句/感叹 → 稍快稍亮、长句 → 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高。
166
+ - **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感。
163
167
  - **governor** 限制全片最多 `max_distinct` 把声音、相邻句不换人 —— 超过就变成广播剧,不是旁白。
164
168
 
165
- **响度校准**:不同音色的响度天然差 3–7 dB(实测 narrator_female 比 narrator_male 响约 5.7 dB),
166
- persona 表里的 `gain_db` 是**实测回填**的(`voicekit.py --calibrate` 可复测)。
167
- 混音侧每句只应用一次增益(v0.5 顺手修掉了 v0.4 `voice_gain` 被乘两次的静默 bug)。
168
-
169
169
  **缓存键 v2**:`sha1(v2|text|voice|rate|pitch|volume|trim|trim_db)`。v0.4 的 key 不含 trim,
170
170
  于是把 `trim_silence` 改成 false 仍会命中旧的裁剪版 —— "关了裁剪其实没关"。v1 key 仍然保留给老缓存。
171
171
 
@@ -306,25 +306,27 @@ python qa_frames.py out/film.mp4 --cues work/cues.json # 看音画同步 dt
306
306
  **音画同步 dt > 60 ms 就是错**:人眼/耳对 40–60 ms 以内的错位不敏感,超过就会"感觉哪里不对但说不出来"。
307
307
  dt 大的典型原因:cue 挂在字幕出现的时刻上,而画面重击晚 100 ms——应该挂在动画时刻上。
308
308
 
309
- ## 11. 程序化配乐(scorekit,v0.3.0)
309
+ ## 11. 程序化配乐(musicgen,v1.0.1)
310
+
311
+ > **v1.0.1 起没有「配乐模板」了。** 以前这一节讲的是 scorekit 的 9 套 `MUSIC_STYLES`
312
+ > 风格表(选一个 id 得到一种配乐性格)——那套表已删除。现在配乐由 `musicgen.py`
313
+ > 按参数**现场编排**:没有可选曲风,只有参数。下面仍然是「一张和弦表贯穿全片」的技术思路,
314
+ > 实现落到了参数化编排器上。
310
315
 
311
316
  ```python
312
- import scorekit as sk
313
- samples, report = sk.render(389.0, bpm=120, key="Dm",
314
- anchors=timeline["scenes"], # [{id,kind,start,end}]
315
- seed=7) # 同 seed 逐采样一致
316
- sk.save("work/music.wav", samples)
317
+ import musicgen as MG
318
+ # 参数就是「设计」:bpm / key / scale / mood / seed,加可选的编排参数
319
+ samples, report = MG.render(389.0, bpm=120, key="Dm", scale="minor",
320
+ mood="steady", seed=7) # 同参数逐采样一致
321
+ MG.save("work/music.wav", samples)
317
322
  ```
318
323
 
319
- 或者用面向对象的写法(要更细的控制时):
324
+ 要更细的控制就传 `sections` / `progressions` / `motif` / `drums` / `voices` ——
325
+ 它们真的参与事件展开,不是死旋钮(接口见 `musicgen.py` 模块 docstring)。
320
326
 
321
- ```python
322
- sc = sk.Score(389.0, bpm=120, key="Dm", seed=7)
323
- sc.plan_from_anchors(timeline["scenes"], default_energy=0.5) # 自动分段,边界对齐小节
324
- sc.section("finale", 360.0, 389.0, energy=0.95)
325
- sc.suck(360.0, db=-6, dur=0.22) # 最终重击前"吸一口气"
326
- samples = sc.render()
327
- ```
327
+ 底层原语(吸气、分段能量、过门)仍在 `scorekit.py` 里,但**它不是配乐引擎**:
328
+ 它只提供和声表 / 律动层 / 混音 / 响度测量这些可复用件。要自己拼一条非常规配乐可以直接
329
+ 调它的 `Score` 再用 `scorekit.save()` 落盘;成片链路用的是 musicgen。
328
330
 
329
331
  ### 11.1 为什么"一张和弦表贯穿全片"
330
332
 
@@ -439,7 +441,7 @@ v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具
439
441
 
440
442
  | 层 | 行为 | 开关 |
441
443
  |---|---|---|
442
- | 配乐和声/动机/鼓型 | 内容指纹做稳定局部替换(每两小节、±1 级数、ghost 击) | `style: auto`(默认);`seed_from_content: false` **只关 seed 派生**(音符细节不再随内容变),结构变奏照旧——`render.py` 只要不是 legacy 就把 `fingerprint` 传给 scorekit(`st_score` 里 `fp_hex` 那段)。想连结构一起回到旧行为只有不写 `style`(legacy) |
444
+ | 配乐和声/动机/鼓型 | v0.7 时代由内容指纹驱动结构变奏(每两小节、±1 级数、ghost 击);**v1.0.1 起改由 musicgen 的编排参数直接决定**(没有可挑的曲风表) | `music.seed_from_content` 仍控制 seed 是否掺指纹;结构差来自 `music.sections` / `music.progressions` 等参数,而不是「换一套风格」 |
443
445
  | 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 旧 seed_base+i) |
444
446
  | 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
445
447
  | 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
@@ -477,6 +479,6 @@ D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何
477
479
  ### 13.4 改音频代码后的自检清单
478
480
 
479
481
  1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C 三组达标(在包根执行)。
480
- 2. `python selfcheck.py` —— 51 条全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
481
- 3. `python scorekit.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS。
482
- 4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
482
+ 2. `python <SKILL_DIR>/scripts/selfcheck.py` —— **67 条**全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
483
+ 3. `python musicgen.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS(musicgen 的 26 项覆盖编排与确定性)。
484
+ 4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
@@ -23,7 +23,7 @@
23
23
  | `lines[].sub`(字幕文案) | sub.srt / sub.ass / timeline 的字幕窗口 / frames / mux / qa | `layout → frames → mux → qa`(TTS 可复用) | 帧数÷40 s |
24
24
  | `lines[].hold` / `lead` / `tail` / `bar` | timeline / cues / frames / mux / qa | 同上 | 帧数÷40 s |
25
25
  | `lines[].kind` | 声线计划 / 配乐分段 / 画面版式(scenes 按 kind 选绘制函数) | `style → tts → layout → …` 全链 | 全片 |
26
- | `style`(含 `"auto"` 结果变了) | style.json / voice_plan / tts(换声线时) / score / audio / frames / qa | 看 `--stage explain`:通常是全链 | 全片 |
26
+ | `work/design.json`(设计变了:配色/字体/母题/版式/字幕/配乐/声线) | style.json / storyboard / voice_plan / tts(换声线时) / score / audio / frames / qa | 看 `--stage explain`:通常是全链 | 全片 |
27
27
  | `bpm` / `music.*` | score / audio / mux | `score → audio → mux → qa` | 秒级 |
28
28
  | `sfx.cues` / `scenes.py 的 cues()` | cues.json / sfx.wav / audio.wav / mux | `cues → sfx → audio → mux → qa` | 秒级 |
29
29
  | `scenes.py`(画面代码) | frames / qa(参考帧失效) | `frames → mux → qa`(音轨可复用) | 帧数÷40 s |
@@ -36,7 +36,7 @@
36
36
  | 错误做法 | 后果 | 正确做法 |
37
37
  |---|---|---|
38
38
  | 改了 `text` 只重跑 `tts` | 新语音配旧时间轴 → 字幕漂移、音效错位 | 跑 `--stage explain`,按它给的最小重跑命令 |
39
- | 改了 `style` 只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | `style` 是所有下游的上游,通常整链重跑 |
39
+ | 改了 `design.json`(或重新 `--confirm` 了另一份)只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | 设计是所有下游的上游,通常整链重跑;`--stage style` 先看解析结果 |
40
40
  | 手工删 `work/segments/_plan.json` 绕过指纹 | 拼接出的片子画面和旁白对不上,且**不报错** | 删 `work/segments` 整目录重渲 |
41
41
 
42
42
  ## 3. 一条命令代替这张表
@@ -51,4 +51,4 @@ python scripts/stagewatch.py --work work --json # 机读(给脚本/agent 用
51
51
 
52
52
  签核(`--stage accept`)绑的是**内容指纹**(剧本 + scenes.py + 引擎)。
53
53
  所以:**改了上面表里任何一项,之前的签核都会自动失效**,需要重新看、重新签。
54
- 这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
54
+ 这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
@@ -122,7 +122,7 @@ r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
122
122
  | 判定 | 记 error | 跳过 |
123
123
 
124
124
  没有这道闸时,**每一段"缓动入场 / 逐字入场 / 转场黑场起坡"的最快帧都会被报成闪烁**。
125
- 实测反例(本技能自带的 `assets/web/` 模板片):12 s 的片子里报了 3 处——
125
+ 实测反例(本技能自带的 `assets/web/` 示例片):12 s 的片子里报了 3 处——
126
126
 
127
127
  | 时刻 | score | 真相 |
128
128
  |---|---|---|
@@ -188,7 +188,7 @@ 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 段**
191
+ 为什么旧判据要废:全帧均值对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
192
192
  (411/814 个滑动窗口命中),而画面一直在动。新判据在同一片上 **0 段**。
193
193
 
194
194
  实测分布(160×90 解码,H.264):
@@ -0,0 +1,177 @@
1
+ # 提案层(Proposal)与设计 DSL —— v1.0.2
2
+
3
+ > 一句话:**出片之前先出方案;方案由用户拍板;拍板的那份设计被冻结成 design.json,渲染只认它。**
4
+ > v1.0.2 起还有一条:**交付过的设计会进台账**,下一次提案主动避开——这是"不同片不重复"的落地机制。
5
+
6
+ ## 0. 为什么要有这一层
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
+ 所以现在**没有"以某套预设为起点"这回事**——每次都是现场采样。
19
+
20
+ ## 1. 三个产物
21
+
22
+ | 文件 | 谁写 | 语义 |
23
+ |---|---|---|
24
+ | `work/proposal.json` | `propose.py` | 一版提案:N 个候选(含 design + 静帧 + 成本 + 差异度 + 台账比对结果)+ 确认状态 |
25
+ | `work/design.json` | `propose.py --confirm` | **冻结**后的那一份设计(唯一真源,渲染只认它) |
26
+ | `work/proposals/revN/proposal.json` | `propose.py` | 每一版的快照(rev 不覆盖,可 diff、可回滚) |
27
+
28
+ ## 2. 流程(零拷贝:脚本留在技能目录里直接跑)
29
+
30
+ ```bash
31
+ # 1) 出一版方案(离线,不联网,秒级)
32
+ python <SKILL_DIR>/scripts/propose.py --brief "AI 大模型的三年,30 秒科普" --script film.json --work work --n 3
33
+
34
+ # 产物:work/proposal.json + work/proposals/rev1/{A,B,C}/_sheet.png + _compare.png
35
+ # 每个候选带:立意一句话、每个候选 4 张静帧、与其它候选的设计距离(n = 不同的维度数)
36
+
37
+ # 2) 不满意就提要求重出(新 rev,不覆盖旧的)
38
+ python <SKILL_DIR>/scripts/propose.py --revise "太冷了,要暖一点、女声、节奏快一点" --work work
39
+
40
+ # 3) 对比两版
41
+ python <SKILL_DIR>/scripts/propose.py --diff 1 2 --work work
42
+
43
+ # 4) 确认其中一个 -> 冻结 design.json(同时记进片间去重台账)
44
+ python <SKILL_DIR>/scripts/propose.py --confirm --candidate B --work work
45
+
46
+ # 5) 现在才允许出片。--scenes 不写就用剧本同目录 / 技能内置的渲染器
47
+ python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/film.mp4 --stage all
48
+ ```
49
+
50
+ ## 3. 提案闸(frames 之前)
51
+
52
+ `render.py` 在 `frames` / `all` 之前会调 `_proposal_gate()`,判定五档:
53
+
54
+ | 状态 | 含义 | 默认行为 |
55
+ |---|---|---|
56
+ | `confirmed` | 已确认,且 `design.json` 指纹与确认时一致 | 放行(记 pass) |
57
+ | `absent` | 没有任何提案 | 警告(`--require-proposal` 下失败) |
58
+ | `draft` | 有提案但没确认 | 同上 |
59
+ | `stale` | 确认过但 `design.json` 被改过 | 同上 |
60
+ | `broken` | `proposal.json` 坏了 | 同上 |
61
+
62
+ 为什么默认只警告:`--stage frames` 是开发期高频命令(改一帧看一帧)。要硬拦就加 `--require-proposal`。
63
+
64
+ `design.json` 同时是 stagewatch 的**第三个源头输入**(另外两个是 `film.json` 与 `scenes.py`):
65
+ 改了设计但没重跑 `frames`,`--stage explain` 会直接报出来。
66
+
67
+ ## 4. design.json 的形状
68
+
69
+ 六块:`structure`(几段/什么用途/目标时长)、`visual`(配色/字体/母题/转场/动效/重心/版式/版式轮换/HUD/字幕/画幅)、
70
+ `audio`(声音/配乐/音效)、`pacing`(留白与停留)、`id`/`pitch`(标识与立意)。
71
+
72
+ 完整契约见 `<REPO>/schemas/design.schema.json`;校验器是 `designkit.validate_design()`,
73
+ 命令行入口 `python <SKILL_DIR>/scripts/designkit.py --lint work/design.json`。
74
+
75
+ ### 4.1 fail-closed(这条最重要)
76
+
77
+ 设计里**缺字段、枚举越界、对比度不达标**,一律报错,**绝不静默回落默认值**。
78
+ 理由:v0.5–v0.9 最大的病就是「改了不生效也不报错」——审计实测出 11 个静默失效的旋钮
79
+ (`voice` 写字符串时 map/jitter 全失效、`theme_overrides.ease` 被视觉语言遮住、
80
+ `sfx.cues[].seed` 必崩、`pan` 对立体声配方无效……)。设计层不允许重演这一幕。
81
+
82
+ ### 4.2 visual.subtitle(v1.0.2,可选)
83
+
84
+ 字幕样式不再写死在 `filmkit` 里。所有字段可选,**不写 = 补齐默认值且与过去逐像素一致**:
85
+
86
+ | 字段 | 默认 | 取值 | 说明 |
87
+ |---|---|---|---|
88
+ | `size` | 46 | 20–96 | 字号 |
89
+ | `y` | null | 0–1030 | 字幕带基准线;null = 默认贴底安全区 |
90
+ | `align` | center | center / left | 对齐 |
91
+ | `plate_alpha` | 170 | 0–255 | 背板透明度(0 = 无背板) |
92
+ | `radius` | 10 | 0–48 | 背板圆角 |
93
+ | `family` | null | sans / serif / kai / round | null = 跟随大标题字族 |
94
+ | `color` | null | [r,g,b] 或 #rrggbb | null = 默认墨色 |
95
+ | `max_lines` | 2 | 1–3 | 最多折几行 |
96
+
97
+ 渲染侧只读 `designkit.subtitle_of(design)` 这一份口径(`filmkit`、参考帧渲染共用),避免"字幕样式在两处漂移"。
98
+
99
+ ### 4.3 visual.layout_cycle(v1.0.2,可选)
100
+
101
+ 按**章节**轮换版式:同一章内版式固定,换章才换。元素取 `left-stack` / `center-stack` / `split-right`,
102
+ **至少 2 种不同版式**(只写一种会被 `validate_design` 拒绝)。不写时 `design_to_plan` 按设计指纹自动派生
103
+ **2–3 种**——默认就不会"全片一张版式"。`--stage layout` 会把每段选的版式写进 `work/storyboard.json` 的
104
+ `layout` 字段,可逐段审计。
105
+
106
+ ### 4.4 配色不是随便取色
107
+
108
+ 采样器的配色走**对比度求解**:定一个色相锚点,在 (s,v) 网格里找「最接近设计意图且满足
109
+ `theme_lint` 全部对比度门槛」的颜色。实测:纯 HSV 随机取色的通过率 **21/60 = 35%**(不可用);
110
+ 改成网格求解后 **60/60 = 100%**。配色是**连续色相空间**里的一点,不是"8 个命名配色族"。
111
+
112
+ ## 5. 采样器(sample_designs)
113
+
114
+ 输入 brief(题材 + 可选时长/必须有/不要),输出 N 个**两两可区分**的设计:
115
+
116
+ - **默认每次都不一样**(v1.0.1):不传 `seed` 就生成新的 nonce,同一 brief 跑两次得到不同设计;
117
+ 传 `--seed` 才逐字节可复现。这是"每条视频都不一样"的来源。
118
+ - 所有选择由 `sha1(nonce + brief + index + seed + attempt)` 派生,**禁 random / time**。
119
+ - **两两必须在 ≥2 个设计维度**上不同(10 个离散轴:配色 / 母题 / 转场 / 缓动 / 编号 / **配乐(调式+BPM 档+情绪)** /
120
+ 声线 / 情绪 / 版式 / 调式),否则视为「伪多方案」(只换颜色不算)。
121
+ - **底色还要拉开色相**:色相差 < 6° 一律重采;< 12° 时要求结构差异 ≥ 3 个轴。
122
+ v1.0.2 修掉了旧实现按 24° 分桶判重的坑——带 `--brief "深蓝"` 这类约束时窗口只有 52°,
123
+ 旧实现 n=3 近一半概率失败;现在实测带约束的 brief **10/10 成功**。
124
+ - **片间去重台账**(见 §6):候选还要与"最近交付过的设计"至少差 2 维。
125
+ - 每个候选自带一句中文立意 `pitch`,写进提案卡。
126
+
127
+ ## 6. 片间去重台账(v1.0.2,默认开启)
128
+
129
+ "每条视频都不一样"不能只靠随机:同题材连拍三部仍可能撞到相近的配色/版式组合。
130
+ `propose.py` 现在把**确认过(= 真要出片)的设计**记进用户级台账,提案时主动避开:
131
+
132
+ | 项 | 值 |
133
+ |---|---|
134
+ | 默认路径 | `~/.dsh/video-studio/design-ledger.json`(最多保留 500 条) |
135
+ | 环境变量 | `VIDEO_STUDIO_LEDGER` 改路径;设为 `off` / `0` / `false` / `no` 即关闭 |
136
+ | CLI | `--no-ledger`(本次不读不写)/ `--show-ledger`(打印路径与最近 20 条) |
137
+ | 判据 | 候选与最近交付设计的最小距离 < **2 维** → 换新 nonce 重采(最多 4 轮),并输出告警 |
138
+ | 记录时机 | `--confirm` 时才记(草稿候选不进台账) |
139
+
140
+ 读写失败/文件损坏一律当空表处理——**台账永远不会阻塞出片**。
141
+ 封锁测试或写文档时把 `VIDEO_STUDIO_LEDGER` 指到临时目录,不要动用户真实台账。
142
+
143
+ ## 7. 离线预览(previewkit)
144
+
145
+ **不联网、不合成旁白**:用 `FAKE_VOICE_DUR=2.0` 的合成时间轴排轴(同 `tools/golden_frames.py` 的口径),
146
+ 渲 3–4 帧/候选 + 联系表 + N 个候选纵向并排的对比图。
147
+
148
+ 成本(作者机器 2026-11-11 实测):单候选 3 帧 **0.6–0.9 s**;3 个候选含并排对比图 **约 2.1 s**;
149
+ 全程离线。成本大头是每个候选的首帧准备(主题/母题派生),同一候选内多渲几张几乎不要钱。
150
+ 换机会有波动,**别把这里的秒数当承诺**。
151
+
152
+ > 为什么不用 `--stage stills`:它缺 `tts.json` 时会**自动联网合成旁白**。
153
+ > 提案阶段不该付这个网络成本,也不该往用户的 TTS 缓存里写东西。
154
+
155
+ ## 8. 常见误用
156
+
157
+ | 现象 | 原因 | 修法 |
158
+ |---|---|---|
159
+ | 改了 design.json 但画面没变 | 没重跑 frames(或没确认) | `python render.py --stage explain` 看是否 `source-changed` |
160
+ | `--confirm` 报「设计没过校验」 | 手改过 design.json 或采样器版本变了 | 按报错逐条修,或 `--revise` 重出 |
161
+ | 候选都「太像」 | 采样器的差异度约束被放宽了 | 看 `--show` 里的 `diversity.vs[].n`,< 2 就是伪多方案 |
162
+ | 提案闸一直 WARN | 没跑过 `propose.py --confirm` | 按提示两步走;要硬拦加 `--require-proposal` |
163
+ | 预览图和成片不一致 | 预览用合成时长,成片用实测时长 | 正常现象:静帧只用于看构图/配色,节奏以成片为准 |
164
+ | 「这批候选和上一部太像」告警 | 片间去重台账命中(< 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 / 假绿) |
@@ -192,7 +192,7 @@ python qa_frames.py out/film.mp4 --cues work/cues.json \
192
192
  | 响度曲线 | 无 >12 s 低于 -40 LUFS | `loudness[]` | warning:长段无声 |
193
193
 
194
194
  > **画面静止的判据在 v0.4 换过**:旧版用"全帧平均亮度 3 s 波动 < 2%",对逐字入场/数字滚动这类
195
- > 局部动画天生不敏感(自带模板片误报 4 段)。现在看的是"最活跃 8×8 块在 0.5 s 内的漂移",
195
+ > 局部动画天生不敏感(自带示例片误报 4 段)。现在看的是"最活跃 8×8 块在 0.5 s 内的漂移",
196
196
  > 详见 `references/frame-qa.md` §2.4b。**别再按旧公式调阈值。**
197
197
 
198
198
  ### 单帧闪烁的判据(照抄实测有效的做法)
@@ -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 51 / FAIL 0 / SKIP 0
246
+ 自检:python selfcheck.py -> PASS 55 / FAIL 0 / SKIP 0
247
247
  ```