@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,5 +1,5 @@
1
1
  {
2
- "_comment": "自测/演示样例剧本(仓库测试夹具,不是给用户抄的模板)。外观由 work/design.json 决定(propose.py --brief 现场生成后 --confirm 冻结);v1.0.1 起 style 字段已废弃。文本仅用于跑通全链与金帧/门禁回归,换题材请自己写 lines[]。",
2
+ "_comment": "自测/演示样例剧本(仓库测试夹具,不是给用户抄的模板)。外观由 work/design.json 决定(propose.py --brief 现场生成后 --confirm 冻结),这里没有 style 字段。文本仅用于跑通全链与金帧/门禁回归,换题材请自己写 lines[]。",
3
3
  "title": "测试片 demo",
4
4
  "end_text": "完",
5
5
  "voice": "zh-CN-YunxiNeural",
@@ -17,7 +17,6 @@
17
17
  "music": {
18
18
  "enabled": true,
19
19
  "mode": "score",
20
- "style": "auto",
21
20
  "mood": "auto",
22
21
  "bpm": "auto",
23
22
  "key": "auto",
@@ -54,7 +53,7 @@
54
53
  "enabled": true,
55
54
  "gain_db": 0,
56
55
  "content_seed": true,
57
- "_comment": "v0.7 去固定化(references/audio-variation.md §3.5):模板只留 1 条转场 whoosh 作示例,具体提示点建议交给 assets/scenes.py 的 cues() 钩子按画面内容生成(hook=impact+riser;body=ui_blip 按条目文本定音高 + answer_chime + 段尾 whoosh;quote=reverse_swell+impact;outro=answer_chime+sub_drop)。content_seed=true(默认)= 每条 cue 的 seed 由 style/content_fp 派生(不同片子同一位置也不同),显式 false 回到 v0.6 的 seed_base+i 旧行为。",
56
+ "_comment": "夹具只留 1 条转场 whoosh 作示例(见 references/audio-variation.md §3.4);具体提示点交给 assets/scenes.py 的 cues() 钩子按画面内容生成(hook=impact+riser;body=ui_blip 按条目文本定音高 + answer_chime + 段尾 whoosh;quote=reverse_swell+impact;outro=answer_chime+sub_drop)。content_seed=true(默认)= 每条 cue 的 seed 由 style/content_fp 派生(不同片子同一位置也不同),显式 false 时按 seed_base+i 只跟 cue 顺序。",
58
57
  "cues": [
59
58
  {
60
59
  "at": "l02-0.15",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "generator": "golden_frames.py v0.8.0",
4
- "comment": "金帧基线:字体指纹+环境指纹一致时,每一帧像素 sha256 必须相同;换机/字体/Pillow 版本不同判 skipped(不误报),同环境像素变了才是 fail。改动模板(film.json / scenes.py / stylekit 主题)后请 --write 重录并提交本文件。",
3
+ "generator": "golden_frames.py v1.0.4",
4
+ "comment": "金帧基线:字体指纹+环境指纹一致时,每一帧像素 sha256 必须相同;换机/字体/Pillow 版本不同判 skipped(不误报),同环境像素变了才是 fail。改动夹具(film.json / scenes.py / 设计)后请 --write 重录并提交本文件。",
5
5
  "canvas": [
6
6
  1920,
7
7
  1080
@@ -34,13 +34,13 @@
34
34
  "platform": "Windows 11"
35
35
  },
36
36
  "inputs": {
37
- "script_sha1": "9ad24749d19cf0fcec1b7fa62e906ed78ce5501b",
38
- "scenes_sha1": "1ff8eaeed66c226a48875580d96d41ff6daad1e3",
39
- "filmkit_sha1": "aca2f742ee4e7f7f2e99901a151864d8046d1547",
40
- "stylekit_sha1": "869098c23a1d9e704a0ffacbc58da6c987712817",
37
+ "script_sha1": "6f70eabff4a90ba4c488aada0af81e806c3cce92",
38
+ "scenes_sha1": "13902dfa5a23edec00eb5306dbc5297e6403ac1c",
39
+ "filmkit_sha1": "752eeda4b47a739925b60d4d6b98fee99261d905",
40
+ "stylekit_sha1": "bff8f89bf09ff64e12a96d7a1dbb241319553db4",
41
41
  "design_sha1": "f87cc13e4c55040d9de5675933c01aa8220134d1",
42
- "designkit_sha1": "074b9705ae0f90a8b35162946142f8c28a3a39db",
43
- "golden_tool_sha1": "da09795aaf3cb92bc9e14f9561bb64027917acbf"
42
+ "designkit_sha1": "785d699349bfef56b9acfbb94dd4f2b92ab3444e",
43
+ "golden_tool_sha1": "1807499ab7d15d466aeba3aef361c95fe4e5c387"
44
44
  },
45
45
  "frames": [
46
46
  {
@@ -48,21 +48,21 @@
48
48
  "scene": "l01",
49
49
  "t_local": 1.2,
50
50
  "label": "hook",
51
- "sha256": "155eaa030a519b7ded1c49664e0007314f2a4dc4eb6ef1e4b69d3b70bae0b367"
51
+ "sha256": "af738e3252e22abd3034c794b515ba0fde0d25eb41c6fa6ae8e1efdbff055a21"
52
52
  },
53
53
  {
54
54
  "frame": 270,
55
55
  "scene": "l03",
56
56
  "t_local": 1.6,
57
57
  "label": "body",
58
- "sha256": "9d205ae43f2632c78446d372d8f3194ecfb7a9634ca42c36943ce31fd6be7981"
58
+ "sha256": "b86f97ab6a9120446700bc2c790944ebe7db6fbc24ed20871f79426a94a0980e"
59
59
  },
60
60
  {
61
61
  "frame": 369,
62
62
  "scene": "l04",
63
63
  "t_local": 1.5,
64
64
  "label": "outro",
65
- "sha256": "c3ccdbedc567402728677f9f4d7ccf62bb98930bebf52934b8bb2becb0e9fb4f"
65
+ "sha256": "1850ff2313378c1fe0e7579aeb433c1d1fce072478f5924b913244095d2991f0"
66
66
  }
67
67
  ]
68
68
  }
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
- """scenes.py — 技能内置的场景渲染器(v1.0.2:默认渲染路径 + 自定义场景的范例)。
2
+ """scenes.py — 技能内置的场景渲染器(默认渲染路径 + 自定义场景的范例)。
3
3
 
4
- ⚠ 这**不是**给用户拷走改的模板:v1.0.2 起 render.py 不写 --scenes 时会自动回落到本文件,
4
+ ⚠ 这**不是**给用户拷走改的模板:render.py 不写 --scenes 时会自动回落到本文件,
5
5
  用户工程目录里只需要一个 film.json。想改画面时,在剧本同目录放一个自己的 scenes.py
6
6
  (或 --scenes 显式指定),它会被优先发现;本文件就是那份自写场景的**参考实现**:
7
7
  10 类构图 / 8 种母题 / 6 种转场 / 35 字段主题 / 版式轮换 / 字幕参数全部从计划里读。
@@ -19,7 +19,7 @@
19
19
  ----------
20
20
  render.py 把 designkit.design_to_plan() 的计划塞进 script["_style"](主题 / 视觉语言 /
21
21
  版式轮换 / 配乐 / 声线 / 节奏 / 字幕样式)。本文件从这份计划里取配色、材质、字体族、
22
- 版式、缓动、HUD、镜头缓推——**没有任何内置风格表**(v1.0.1 已全部删除):
22
+ 版式、缓动、HUD、镜头缓推——**没有任何内置风格表**:
23
23
  同一份代码画出来是什么样,完全由现场采样并确认过的 design.json 决定。
24
24
 
25
25
  铁律(决定画质与可复现性):
@@ -62,10 +62,10 @@ import filmkit as K
62
62
  from filmkit import W, H, clamp, smooth, ease_out, rng
63
63
 
64
64
  # ============================================================================
65
- # 零、v0.4 兼容层(legacy 逐字节基线,别动这里的任何一行)
65
+ # 零、legacy 绘制器(逐字节基线,别动这里的任何一行)
66
66
  # ============================================================================
67
67
  #
68
- # legacy = 老项目(film.json 没有 style 字段)。这组函数是 v0.4 的原文,
68
+ # legacy = 老项目(film.json 没有 style 字段)。这组函数是逐字节基线,
69
69
  # 一个字符都不许改:Lead 的基线三帧 sha256 直接对拍这里的输出。
70
70
  # 新主题化代码只在 _legacy_* 之外生长,互不影响。
71
71
 
@@ -243,7 +243,7 @@ _LEGACY_DRAWERS = {
243
243
 
244
244
 
245
245
  # ============================================================================
246
- # 一、风格计划 / 主题(v0.5)
246
+ # 一、风格计划 / 主题
247
247
  # ============================================================================
248
248
 
249
249
  _PLAN_CACHE = {}
@@ -358,14 +358,14 @@ def _ease(name):
358
358
  _EASES = {}
359
359
 
360
360
 
361
- # ---------------- 字体:family 是 v0.5 新加的参数(旧 filmkit 没有,自动回退) ----------------
361
+ # ---------------- 字体:family 参数(旧 filmkit 没有就自动回退) ----------------
362
362
 
363
363
  _FONT_CACHE = {}
364
364
  _FAMILY_OK = None
365
365
 
366
366
 
367
367
  def _family_ok():
368
- """filmkit.font 是否支持 family=...(Lead 在 v0.5 加;老版本没有就回退旧签名)。"""
368
+ """filmkit.font 是否支持 family=...(老版本没有就回退旧签名)。"""
369
369
  global _FAMILY_OK
370
370
  if _FAMILY_OK is None:
371
371
  try:
@@ -453,7 +453,7 @@ class _View(object):
453
453
 
454
454
  为什么不是整帧重采样:实测 1080p 双线性整帧变换 29ms/帧(预算 15ms),
455
455
  而整帧按 push 放大又会把 150px 边距的内容推出 SAFE。所以分两层:
456
- 背景层做**真缩放**(EXTENT + NEAREST,1.7~3.5ms,见 _bg_plate);
456
+ 背景层做**真缩放**(端点预烘 + blend,约 4.4ms,见 _plate_zoom/_bg_plate);
457
457
  前景层做同向**视差**(0.35x,位置与卡片尺寸随镜头收放,字形不重采样保清晰)。
458
458
  """
459
459
  __slots__ = ("s", "cx", "cy")
@@ -599,15 +599,63 @@ def _bg(th):
599
599
  return im
600
600
 
601
601
 
602
- def _bg_plate(th, s):
603
- """逐帧背景:按镜头缩放取景(NEAREST 重采样,实测 1.7~3.5ms/帧)。"""
604
- base = _bg(th)
602
+ _ZOOM_ORDER = []
603
+
604
+
605
+ def _zoom_tag(th, extra=None):
606
+ """取景端点的缓存标签:主题决定底图,extra 用来区分装饰母题档位。"""
607
+ return _hashable(("zoom", th.get("id"), th["bg"], th["bg2"], th["texture"],
608
+ th["grain"], th["grain_freq"], th["vignette"],
609
+ th["vignette_tint"], th["texture_alpha"], extra))
610
+
611
+
612
+ def _plate_zoom(src, s, th, tag):
613
+ """按镜头缓推取景:端点预烘 + 逐帧 blend。
614
+
615
+ 为什么不再用 NEAREST 直采:底图把颗粒(`_grain_layer`)一起烘了进去,颗粒是
616
+ 逐像素的高频图案;NEAREST 在缓慢缩放时采样网格会整像素跳变,颗粒图案随之整体
617
+ 错位 1px,观感就是"满屏噪点在沸腾"——用户实测反馈是"明显的抖动感",而背景-only
618
+ 的帧差只有 4~6 灰阶,单帧闪烁门禁根本抓不到。
619
+
620
+ 做法:s=1.0 与 s=1+push 两个端点各烘一张 BILINEAR 底版(每张约 60ms,只做一次),
621
+ 逐帧用 `Image.blend` 在两端点间线性插值(实测 4.4ms/帧)。端点内部是**平滑重采样**,
622
+ 没有 NEAREST 的网格跳变,帧差降到 0~1 灰阶;同时保留颗粒质感(不是把 grain 抹掉)。
623
+
624
+ 上限取 1+push:`_push_scale` 恒 <= 1+push,超出的 s 一律钳到端点上,不会外推。
625
+ """
605
626
  if s <= 1.0008:
606
- return base.copy()
607
- bw, bh = W / s, H / s
608
- x0 = (W - bw) * 0.5
609
- y0 = (H - bh) * 0.5
610
- return base.transform((W, H), Image.EXTENT, (x0, y0, x0 + bw, y0 + bh), Image.NEAREST)
627
+ return src.copy()
628
+ cap = 1.0 + max(0.0, float(th.get("push") or _DEFAULT_PUSH))
629
+ if cap <= 1.0008:
630
+ return src.copy()
631
+ s = min(float(s), cap)
632
+ key = _hashable(("zpts", tag, round(cap, 6)))
633
+ pair = _CACHE.get(key)
634
+ if pair is None:
635
+ lo = src.copy()
636
+ bw, bh = W / cap, H / cap
637
+ x0 = (W - bw) * 0.5
638
+ y0 = (H - bh) * 0.5
639
+ hi = src.transform((W, H), Image.EXTENT, (x0, y0, x0 + bw, y0 + bh), Image.BILINEAR)
640
+ pair = (lo, hi)
641
+ _CACHE[key] = pair
642
+ _ZOOM_ORDER.append(key)
643
+ while len(_ZOOM_ORDER) > 4: # 一对端点约 12MB,限量防多片同进程膨胀
644
+ old = _ZOOM_ORDER.pop(0)
645
+ if old != key:
646
+ _CACHE.pop(old, None)
647
+ lo, hi = pair
648
+ w = (s - 1.0) / (cap - 1.0)
649
+ if w <= 0.0:
650
+ return lo.copy()
651
+ if w >= 1.0:
652
+ return hi.copy()
653
+ return Image.blend(lo, hi, w)
654
+
655
+
656
+ def _bg_plate(th, s):
657
+ """逐帧背景:按镜头缩放取景(端点预烘 + blend,约 4.4ms/帧;见 _plate_zoom)。"""
658
+ return _plate_zoom(_bg(th), s, th, _zoom_tag(th, "bg"))
611
659
 
612
660
 
613
661
  # ---------------- 版式几何(全部从 ST.LAYOUTS 取) ----------------
@@ -630,7 +678,7 @@ def _panel_x(lay):
630
678
  def _panel_boxes(lay, th, n, y_floor, v):
631
679
  """条目卡片布局:选列数 + 压缩卡片高度,保证①不压标题②不越字幕带(856)。
632
680
 
633
- panel_gap 在 stylekit 里是**行距(pitch)**不是间隙:v0.4 用 240 + i*118 排卡片,
681
+ panel_gap 在 stylekit 里是**行距(pitch)**不是间隙:按 240 + i*118 排卡片时,
634
682
  所以 pitch 至少得比卡片高度多 6px,否则卡片会叠在一起。条目多时先加列,再压高度,
635
683
  最后才截断——静默重叠或画到字幕带下面比少画两条更糟。
636
684
  """
@@ -774,14 +822,14 @@ def _hud(img, th, chapter, progress, title):
774
822
 
775
823
 
776
824
  # ============================================================================
777
- # 三、场景语法(v0.9:10 类构图 × 8 套视觉语言)
825
+ # 三、场景语法(10 类构图 × 8 套视觉语言)
778
826
  # ============================================================================
779
827
  #
780
828
  # 为什么重写这一节
781
829
  # ---------------
782
- # v0.5 的 _draw_hook / _draw_body / _draw_outro 是"一套骨架 + 换色":背景板 → 眉标 →
830
+ # 旧的 _draw_hook / _draw_body / _draw_outro 是"一套骨架 + 换色":背景板 → 眉标 →
783
831
  # 大字 → 副标题 → 强调线 → HUD,三种 kind 只差字号与位置。于是 8 套风格换完只剩配色不同。
784
- # v0.9 把"画什么"(scene type,由 grammar.py 按内容选)与"怎么画"(视觉语言,由 stylekit
832
+ # 现在把"画什么"(scene type,由 grammar.py 按内容选)与"怎么画"(视觉语言,由 stylekit
785
833
  # 的 VISUAL_LANGUAGES 给)拆开:
786
834
  # · 10 类构图各自有独立的视觉重心与留白(hero 偏左上 / statement 正中 / compare 强制对称 …)
787
835
  # · 每套风格有自己的装饰母题(括角 / 手绘内框 / letterbox / 信号条 …)与转场签名
@@ -915,9 +963,9 @@ def _motif_layer(th, lang):
915
963
  """把该风格独有的装饰图形烘焙成一张全屏 RGBA 图层(不含动态)。
916
964
 
917
965
  为什么烘焙:这些图形是静态的(括角、页眉线、letterbox、信号条),
918
- 逐帧重画一次就是 3–8ms。注意:v0.9.0 这里写的是"paste 只要 0.3ms",
919
- 实测全屏 RGBA paste 是 **5.6ms**(Pillow 13 / 1080p),所以 v0.9.1 起
920
- 装饰直接烘焙进底版(见 _plate),逐帧只 copy 底版,不再单独 paste。
966
+ 逐帧重画一次就是 3–8ms。注意:全屏 RGBA paste 实测是 **5.6ms**
967
+ (Pillow 13 / 1080p),所以装饰直接烘焙进底版(见 _plate),
968
+ 逐帧只 copy 底版,不再单独 paste。
921
969
  """
922
970
  mf = lang.get("motif") or _DEFAULT_LANG["motif"]
923
971
  key = _motif_key(th, lang)
@@ -1044,10 +1092,10 @@ _PLATE_ORDER = []
1044
1092
 
1045
1093
 
1046
1094
  def _plate(th, lang, scene, s_bg):
1047
- """背景板 + 装饰母题合成后的整帧底版(v0.9.1)。
1095
+ """背景板 + 装饰母题合成后的整帧底版。
1048
1096
 
1049
1097
  这是单帧预算里最大的一处优化:装饰是静态的,逐帧单独 paste 一张全屏 RGBA 要 5.6ms,
1050
- 合成进底版后逐帧只剩 copy(1.3ms)或 EXTENT 缩放(1.7–2.0ms)。
1098
+ 合成进底版后逐帧只剩 copy(1.3ms)或端点 blend 取景(约 4.4ms,见 _plate_zoom)。
1051
1099
  顺带一个好处:镜头缓推现在会把装饰一起推——装饰本来就该跟着镜头走,
1052
1100
  以前"背景推、装饰不动"在强 push 的风格(cinema 0.12 / epic 0.14)里其实是穿帮。
1053
1101
 
@@ -1072,12 +1120,7 @@ def _plate(th, lang, scene, s_bg):
1072
1120
  old = _PLATE_ORDER.pop(0)
1073
1121
  if old != key:
1074
1122
  _CACHE.pop(old, None)
1075
- if s_bg <= 1.0008:
1076
- return got.copy()
1077
- bw, bh = W / s_bg, H / s_bg
1078
- x0 = (W - bw) * 0.5
1079
- y0 = (H - bh) * 0.5
1080
- return got.transform((W, H), Image.EXTENT, (x0, y0, x0 + bw, y0 + bh), Image.NEAREST)
1123
+ return _plate_zoom(got, s_bg, th, _zoom_tag(th, key))
1081
1124
 
1082
1125
 
1083
1126
  def _apply_motif(img, th, lang, scene):
@@ -1095,7 +1138,7 @@ def _apply_veil(img, th, lang, scene, t_local, s_bg):
1095
1138
  """段首从背景"长出来"的过场。不做跨场景 cross-dissolve(draw_frame 拿不到上一段帧),
1096
1139
  只做"从本段背景板渐显 / 擦入",观感是 dip 而不是 dissolve。
1097
1140
 
1098
- 为什么必须做:v0.9 起相邻两段可能是完全不同的构图(数字卡 → 左右对比),
1141
+ 为什么必须做:相邻两段可能是完全不同的构图(数字卡 → 左右对比),
1099
1142
  整屏瞬切会精确触发 qa_frames 的单帧闪烁门禁(8x8 分块灰度差 ≥6.0),
1100
1143
  观众看着也像"换了一部片"。
1101
1144
  """
@@ -1241,7 +1284,7 @@ def _draw_hero(scene, t, dur, tg, script, frame, plan, th):
1241
1284
 
1242
1285
  sub = scene.get("lead") or scene.get("sub") or ""
1243
1286
  # 副标题与大字重复时不要画第二遍:模板片里 big 与 sub 都是"零素材,全代码生成",
1244
- # 照抄会让画面出现两行一样的字(v0.9 实测第一版就是这样)。
1287
+ # 照抄会让画面出现两行一样的字(实测踩到过)。
1245
1288
  if sub and (str(sub).strip() == str(big).strip()
1246
1289
  or (len(str(sub)) >= 6 and str(sub).strip() in str(big).strip())):
1247
1290
  sub = ""
@@ -1418,7 +1461,7 @@ def _draw_list(scene, t, dur, tg, script, frame, plan, th):
1418
1461
  body = max(22, int(th["scale"]["body"][0]) - 1)
1419
1462
  # 行高:优先"每条 96–118px",条目多时往下压,硬下限 56px(再低中文就糊成一片)。
1420
1463
  # 为什么不是固定行高:模板片 5 条要点在 9.3s 的段里,固定 118px 会溢出 856 硬边界,
1421
- # 于是第 5 条被静默截断(v0.9 第一版实测),观众看到"五步"只列出四步。
1464
+ # 于是第 5 条被静默截断(实测踩到过),观众看到"五步"只列出四步。
1422
1465
  avail = 856.0 - top
1423
1466
  row_h = avail / max(1, len(items)) if items else 118.0
1424
1467
  row_h = min(118.0, max(56.0, row_h - 6.0))
@@ -1546,7 +1589,7 @@ def _draw_stat(scene, t, dur, tg, script, frame, plan, th):
1546
1589
  v.y(768)], fill=_rgba(th["accent"] if on else th["rule"],
1547
1590
  int((150 if on else 34))))
1548
1591
  # 次要统计:**数值 + 单位 + 标签**都要画出来。
1549
- # v1.0 修:早先这里只画了 s["label"],于是 stats[] 里的第 2、3 项(例如
1592
+ # 这里必须把数值也画出来:只画 s["label"] 时 stats[] 里的第 2、3 项(例如
1550
1593
  # "338207 次 / 工具调用"、"22.62 B / Token 重放")在画面上只剩两个光秃秃的词,
1551
1594
  # 并且行距 30px < 字号,两行直接叠在一起 —— 观众看到的是"工具调用/Token 重放"两行字
1552
1595
  # 糊在一块、数字完全消失。数字卡的价值就在数字,丢数字等于这段白做。
@@ -1615,7 +1658,7 @@ def _draw_compare(scene, t, dur, tg, script, frame, plan, th):
1615
1658
 
1616
1659
  # 两栏内容垂直居中(上界 360,下界 800):只有一两条时不会全挤在顶部留一大片空白。
1617
1660
  # 栏头:优先 sides 里作者写的 head;没有就**不要**写"左/右"这种零信息标签
1618
- # (v0.9 第一版实测:观众看到"左 / 右"会以为画面坏了),改画一个 VS 徽标在中缝上方。
1661
+ # (实测:观众看到"左 / 右"会以为画面坏了),改画一个 VS 徽标在中缝上方。
1619
1662
  cols = [("left", 150.0, 700.0, -1), ("right", W - 850.0, 700.0, 1)]
1620
1663
  max_rows = max(len((sides.get("left") or {}).get("items") or []),
1621
1664
  len((sides.get("right") or {}).get("items") or []), 1)
@@ -1762,7 +1805,7 @@ def _draw_quote(scene, t, dur, tg, script, frame, plan, th):
1762
1805
  by = str(by or "")
1763
1806
 
1764
1807
  # 巨型引号:**用字形**,不用矢量线。
1765
- # v0.9 第一版画的是两条短线拼成的「《」,实测看起来像播放器的"快退"图标,
1808
+ # 用两条短线拼「《」实测看起来像播放器的"快退"图标,
1766
1809
  # 观众会以为点错了什么。雅黑粗体的 U+201C 是有字形的(preflight 查过不缺口),
1767
1810
  # 直接放大 150px 就是最标准的引语符号。
1768
1811
  pq = ease(rng(t, 0.0, 0.45))
@@ -1969,7 +2012,7 @@ def _draw_outro(scene, t, dur, tg, script, frame, plan, th):
1969
2012
  gap = _gap(th, lang)
1970
2013
  ph = _phase(scene)
1971
2014
 
1972
- # 光点环:数量随视觉语言母题走(epic 18 / cinema 12 / mono 0),且比 v0.5 更少
2015
+ # 光点环:数量随视觉语言母题走(epic 18 / cinema 12 / mono 0)
1973
2016
  mf = str((lang.get("motif") or {}).get("kind") or "minimal")
1974
2017
  ring_n = {"rays": 18, "letterbox": 12, "brackets": 10, "signal": 8,
1975
2018
  "handdrawn": 6, "slabs": 12, "hairlines": 6, "minimal": 0}.get(mf, 8)
@@ -2048,10 +2091,10 @@ _KIND_TO_TYPE = {
2048
2091
  def draw_frame(scene, t_local, dur, t_global, script, frame):
2049
2092
  """按帧画一张 1920x1080 RGB 图。
2050
2093
 
2051
- v1.0 / D3 起只有一条路:按 scene_type(grammar.py 选好、render.py 写进 timeline)
2094
+ 只有一条路:按 scene_type(grammar.py 选好、render.py 写进 timeline)
2052
2095
  派发到 10 类构图之一;没有 scene_type 的旧 timeline 按 kind 映射到兼容类型。
2053
2096
 
2054
- 原来的第 1 条分支(legacy —— film.json 不写 style 就走 v0.4 三个老绘制器)**已删除**:
2097
+ 原来的第 1 条分支(legacy —— film.json 不写 style 就走三个老绘制器)**已删除**:
2055
2098
  stylekit 不再产出 legacy 计划(不写设计会直接报错),那条分支永远走不到。
2056
2099
  留着死分支比删掉更危险——它会让"legacy 还能用"的错觉一直存在。
2057
2100
 
@@ -2073,7 +2116,7 @@ def draw_frame(scene, t_local, dur, t_global, script, frame):
2073
2116
  return fn(scene, t_local, dur, t_global, script, frame, plan, th)
2074
2117
 
2075
2118
 
2076
- # ---------------- 提示点(v0.3.0 起可选;v0.7 按 kind 与内容生成) ----------------
2119
+ # ---------------- 提示点(按 kind 与内容生成) ----------------
2077
2120
  def _sha1_int(*parts):
2078
2121
  """把若干文本片段拼起来取 sha1 前 8 位(提示点"内容派生"的稳定来源)。"""
2079
2122
  raw = "|".join(str(p) for p in parts).encode("utf-8")
@@ -2083,8 +2126,8 @@ def _sha1_int(*parts):
2083
2126
  def _item_pitch(text, base=1760.0):
2084
2127
  """条目文本 -> ui_blip 音高(sha1 派生,不用序号)。
2085
2128
 
2086
- v0.7 契约 §3.4:body 条目的 pitch 必须由**条目文本**派生,不再按序号
2087
- (旧实现 1760+110*i 每片都一样)——同一句文案换到别的片子里仍是同一个音高,
2129
+ 契约 §3.4:body 条目的 pitch 必须由**条目文本**派生,不再按序号
2130
+ (1760+110*i 每片都一样)——同一句文案换到别的片子里仍是同一个音高,
2088
2131
  不同文案落到 -5~+7 半音的不同音级;万一两条碰撞,_dedupe_segment 会再升半音。
2089
2132
  """
2090
2133
  off = _sha1_int("pitch", text) % 13 - 5
@@ -2094,7 +2137,7 @@ def _item_pitch(text, base=1760.0):
2094
2137
  def _cue_gain_decay(gain, repeat):
2095
2138
  """同段内同类型 cue 第 repeat 次重复(0 起)时的增益:每次再低 2 dB(-2/-4/-6/…)。
2096
2139
 
2097
- v0.7 契约 §3.4:不许再出现"每段都一模一样的三连击"——同类型重复必须**严格递减**
2140
+ 契约 §3.4:不许出现"每段都一模一样的三连击"——同类型重复必须**严格递减**
2098
2141
  (第 2 次 -2、第 3 次 -4、第 4 次 -6……),且 pitch 必须不同。
2099
2142
  """
2100
2143
  return round(float(gain) - 2.0 * int(repeat), 2)
@@ -2130,11 +2173,11 @@ def cues(tl, script):
2130
2173
  好处是"声音跟着画面的像素走":这里算的是卡片落位、数字滚完的时刻,
2131
2174
  画面一改,声音自动跟着改,不用回来手工对时间。
2132
2175
 
2133
- v0.5:条目落位步长与场景绘制共用 plan["pacing"]["item_stagger"],
2134
- 两边读同一个数,否则音效会错位(这是 v0.4 写死 0.32 留下的坑)。
2176
+ 条目落位步长与场景绘制共用 plan["pacing"]["item_stagger"],
2177
+ 两边读同一个数,否则音效会错位(写死 0.32 时踩过这个坑)。
2135
2178
 
2136
- v0.7(references/audio-variation.md §3.4):提示点改为按**场景 kind 与内容**生成,
2137
- 不再是"每段 ui_blip 三连 + 结尾 answer_chime"的同质化固定套路(跨片音效雷同的
2179
+ 见 references/audio-variation.md §3.3:提示点按**场景 kind 与内容**生成,
2180
+ 不是"每段 ui_blip 三连 + 结尾 answer_chime"的同质化固定套路(跨片音效雷同的
2138
2181
  来源之一):
2139
2182
  hook : 段首 impact(size 按字数 0.9~1.3)+ 落定 riser
2140
2183
  body : 条目落位 ui_blip(pitch 由条目文本 sha1 派生)+ items>=3 补 answer_chime
@@ -1,8 +1,8 @@
1
- # 外观定制:设计 DSL 实操手册(v1.0.2)
1
+ # 外观定制:设计 DSL 实操手册
2
2
 
3
- > **这篇不是「8 套风格选一套」。** v1.0.1 起仓库里没有任何内置风格表——
4
- > 没有 `STYLES` / `THEMES` / `VISUAL_LANGUAGES` / `presets/`,也没有 `style` 字段。
5
- > 外观由 `work/design.json` 决定,而它是 `propose.py` 现场采样 + 你确认之后冻结的。
3
+ > **外观由 `work/design.json` 决定:`propose.py` 现场采样,你确认之后冻结。**
4
+ > 没有现成外观目录可挑,也没有 `style` 字段——剧本不决定长相。
5
+ > 同一份稿子换一份 design.json 就是另一种画面,反之亦然。
6
6
  > 这篇讲的是:**想控制画面的哪一块,就改 design.json 的哪个字段,怎么验证它真的生效。**
7
7
 
8
8
  ## 0. 三条正确姿势
@@ -26,7 +26,9 @@
26
26
 
27
27
  校验:`python <SKILL_DIR>/scripts/designkit.py --lint work/design.json`;
28
28
  契约:`<REPO>/schemas/design.schema.json`。**fail-closed**:缺字段/越界/对比度不达标一律报错,
29
- 不会静默回落(这是对 v0.5–v0.9「改了不生效」的直接反思)。
29
+ 不会静默回落——设计层不允许「改了不生效也不报错」这种状态存在。
30
+ (实测过 11 个静默失效的旋钮:`voice` 写字符串时 map/jitter 全失效、`pan` 对立体声配方无效……
31
+ 每个都"配置生效、画面不动",且不报错。设计层改成一律报错,这类问题在 `--lint` 就暴露。)
30
32
 
31
33
  ## 2. visual 字段表(能控制什么)
32
34
 
@@ -40,20 +42,20 @@
40
42
  | `motion` | `ease`(out3/out5/inOut3/outBack)/ `enter`(dx,dy) / `push`(镜头缓推)/ `breathe_hz` | 入场与镜头运动 |
41
43
  | `bias` | `x` / `y` / `inset` | 整屏重心偏移与安全区收缩 |
42
44
  | `layout` | `left-stack` / `center-stack` / `split-right` | 默认版式 |
43
- | `layout_cycle` | 版式数组(≥2 种不同) | **v1.0.2**:按章节轮换版式;不写则自动派生 2–3 种 |
45
+ | `layout_cycle` | 版式数组(≥2 种不同) | 按章节轮换版式;不写则自动派生 2–3 种 |
44
46
  | `hud` | `chip-left` / `corner-line` / `none` | 页眉/角落装饰 |
45
47
  | `size` / `fps` | `1920x1080` / `30` | 只支持 16:9 横屏(编码时可下采样到 720p) |
46
- | `subtitle` | 见 §3 | **v1.0.2**:字幕字号/位置/对齐/背板/圆角/字族/颜色/行数 |
48
+ | `subtitle` | 见 §3 | 字幕字号/位置/对齐/背板/圆角/字族/颜色/行数 |
47
49
 
48
50
  > **能组合出多少种画面?** 离散维度是 8 母题 × 6 转场 × 4 缓动 × 8 编号 × 3 版式 × 3 HUD × 4 字族
49
51
  > × 7 情绪 × 2 调式(= 774,144 种),再乘配色这个连续量(色相 0–360° 任意取值)。
50
52
  > 这个数字只说明"字段可组合空间足够大",**不是推荐你从中挑**——好看与否仍由对比度体检、
51
53
  > 分镜语法和你的目检决定。
52
54
 
53
- ## 3. 字幕样式(visual.subtitle,v1.0.2)
55
+ ## 3. 字幕样式(visual.subtitle)
54
56
 
55
- 以前字幕的字号 / 位置 / 背板 / 圆角是 `filmkit` 的写死常量,所以**任何两条片子**的字幕带一模一样。
56
- 现在进设计 DSL;**不写 = 默认值,与过去逐像素一致**(老 `design.json` 不受影响)。
57
+ 字幕的字号 / 位置 / 背板 / 圆角由设计 DSL 决定;写死的常量会让**任何两条片子**的字幕带一模一样。
58
+ **不写 = 用默认值**,老 `design.json` 不受影响、字幕与默认常量逐像素一致。
57
59
 
58
60
  ```json
59
61
  "subtitle": {"size": 46, "y": 560, "align": "left", "plate_alpha": 170,
@@ -74,7 +76,7 @@
74
76
  验证:`render.py --stage style` 会打印一行「字幕: ... 对齐=... 背板α=... 圆角=... 最多N行」,
75
77
  改 design 后重跑这一行就能看重算结果,不用等渲完。
76
78
 
77
- ## 4. 版式轮换(visual.layout_cycle,v1.0.2)
79
+ ## 4. 版式轮换(visual.layout_cycle)
78
80
 
79
81
  ```json
80
82
  "layout_cycle": ["split-right", "left-stack", "center-stack"]
@@ -106,7 +108,7 @@
106
108
  | 层 | 机制 | 防的是什么 |
107
109
  |---|---|---|
108
110
  | 段内 | `lines[].type`(10 类构图,grammar 自动选) | 每段都长一个样 |
109
- | 章内 | `layout_cycle`(v1.0.2,按章节轮换) | 全片一张版式 |
111
+ | 章内 | `layout_cycle`(按章节轮换) | 全片一张版式 |
110
112
  | 片间 | 采样 nonce + 候选 ≥2 维 + 底色色相分叉 + **片间去重台账** | 不同片撞衫 |
111
113
 
112
114
  ## 7. 常见误用
@@ -116,6 +118,6 @@
116
118
  | 改了 `design.json` 画面没变 | 没重跑阶段 / 没重签核 | `render.py --stage explain` 看哪些阶段过时 |
117
119
  | `--stage style` 报「找不到设计」 | 工程里没有 `work/design.json` | 先 `propose.py --brief ... --script film.json --work work` 再 `--confirm` |
118
120
  | 手改 design 后 `--confirm` 报校验失败 | 字段越界/对比度不达标 | 按报错逐条修;或干脆 `--revise` 重出 |
119
- | 想"用某套预设当起点" | v1.0.1 已删除全部预设 | 没有这条路:现场采样 + `--revise` 表达要求,要复现用 `--seed` |
120
- | `render.py --preset x` 报错 | `--preset` 已删除 | 用 `--brief` 或直接写 design.json |
121
- | `stylekit.py --list` 只有一段说明、exit 2 | 内置风格表已删除(预期行为) | 看 `designkit.py --list-palettes` 的配色采样示例(那不是可选清单) |
121
+ | 想"用某套预设当起点" | 没有预设目录,外观只由 design.json 决定 | 现场采样 + `--revise` 表达要求,要复现用 `--seed` |
122
+ | `render.py --preset x` 报错 | render.py 没有 `--preset` 这个参数 | 用 `--brief` 或直接写 design.json |
123
+ | `stylekit.py --list` 只有一段说明、exit 2 | 没有可选外观清单(预期行为) | 看 `designkit.py --list-palettes` 的配色采样示例(那不是可选清单) |