@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.
- package/CHANGELOG.md +49 -0
- package/README.md +84 -108
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +1 -1
- package/schemas/design.schema.json +5 -5
- package/schemas/film.schema.json +31 -31
- package/skills/video-studio/SKILL.md +168 -268
- package/skills/video-studio/assets/film.json +2 -3
- package/skills/video-studio/assets/golden_frames.json +11 -11
- package/skills/video-studio/assets/scenes.py +92 -49
- package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
- package/skills/video-studio/references/audio-variation.md +73 -96
- package/skills/video-studio/references/audio.md +40 -44
- package/skills/video-studio/references/change-impact.md +2 -2
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
- package/skills/video-studio/references/frame-qa.md +47 -15
- 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 +19 -36
- 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 +17 -24
- package/skills/video-studio/references/sfx-design.md +7 -5
- package/skills/video-studio/references/web-engine.md +7 -7
- package/skills/video-studio/references/word-timeline.md +7 -7
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +84 -89
- package/skills/video-studio/scripts/filmcheck.py +19 -19
- package/skills/video-studio/scripts/filmkit.py +2949 -2948
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +10 -10
- package/skills/video-studio/scripts/musicgen.py +3 -4
- package/skills/video-studio/scripts/preflight.py +5 -5
- package/skills/video-studio/scripts/previewkit.py +11 -12
- package/skills/video-studio/scripts/propose.py +11 -11
- package/skills/video-studio/scripts/qa_frames.py +34 -35
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +19 -19
- package/skills/video-studio/scripts/render.py +137 -141
- package/skills/video-studio/scripts/scorekit.py +21 -23
- package/skills/video-studio/scripts/selfcheck.py +208 -135
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +4 -4
- package/skills/video-studio/scripts/stylekit.py +77 -282
- package/skills/video-studio/scripts/voicekit.py +43 -44
- package/skills/video-studio/scripts/webengine.py +62 -64
- package/tools/audio_similarity.py +12 -12
- package/tools/ci_check.py +16 -16
- package/tools/estimate.py +24 -25
- package/tools/golden_frames.py +17 -18
- package/tools/silhouette_qa.py +13 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "自测/演示样例剧本(仓库测试夹具,不是给用户抄的模板)。外观由 work/design.json 决定(propose.py --brief 现场生成后 --confirm
|
|
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": "
|
|
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
|
|
4
|
-
"comment": "金帧基线:字体指纹+环境指纹一致时,每一帧像素 sha256 必须相同;换机/字体/Pillow 版本不同判 skipped(不误报),同环境像素变了才是 fail
|
|
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": "
|
|
38
|
-
"scenes_sha1": "
|
|
39
|
-
"filmkit_sha1": "
|
|
40
|
-
"stylekit_sha1": "
|
|
37
|
+
"script_sha1": "6f70eabff4a90ba4c488aada0af81e806c3cce92",
|
|
38
|
+
"scenes_sha1": "13902dfa5a23edec00eb5306dbc5297e6403ac1c",
|
|
39
|
+
"filmkit_sha1": "752eeda4b47a739925b60d4d6b98fee99261d905",
|
|
40
|
+
"stylekit_sha1": "bff8f89bf09ff64e12a96d7a1dbb241319553db4",
|
|
41
41
|
"design_sha1": "f87cc13e4c55040d9de5675933c01aa8220134d1",
|
|
42
|
-
"designkit_sha1": "
|
|
43
|
-
"golden_tool_sha1": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
65
|
+
"sha256": "1850ff2313378c1fe0e7579aeb433c1d1fce072478f5924b913244095d2991f0"
|
|
66
66
|
}
|
|
67
67
|
]
|
|
68
68
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""scenes.py —
|
|
2
|
+
"""scenes.py — 技能内置的场景渲染器(默认渲染路径 + 自定义场景的范例)。
|
|
3
3
|
|
|
4
|
-
⚠ 这**不是**给用户拷走改的模板:
|
|
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
|
|
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
|
-
# 零、
|
|
65
|
+
# 零、legacy 绘制器(逐字节基线,别动这里的任何一行)
|
|
66
66
|
# ============================================================================
|
|
67
67
|
#
|
|
68
|
-
# legacy = 老项目(film.json 没有 style
|
|
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
|
-
# 一、风格计划 /
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
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
|
-
# 三、场景语法(
|
|
825
|
+
# 三、场景语法(10 类构图 × 8 套视觉语言)
|
|
778
826
|
# ============================================================================
|
|
779
827
|
#
|
|
780
828
|
# 为什么重写这一节
|
|
781
829
|
# ---------------
|
|
782
|
-
#
|
|
830
|
+
# 旧的 _draw_hook / _draw_body / _draw_outro 是"一套骨架 + 换色":背景板 → 眉标 →
|
|
783
831
|
# 大字 → 副标题 → 强调线 → HUD,三种 kind 只差字号与位置。于是 8 套风格换完只剩配色不同。
|
|
784
|
-
#
|
|
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
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
"""背景板 +
|
|
1095
|
+
"""背景板 + 装饰母题合成后的整帧底版。
|
|
1048
1096
|
|
|
1049
1097
|
这是单帧预算里最大的一处优化:装饰是静态的,逐帧单独 paste 一张全屏 RGBA 要 5.6ms,
|
|
1050
|
-
合成进底版后逐帧只剩 copy(1.3ms
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
2094
|
+
只有一条路:按 scene_type(grammar.py 选好、render.py 写进 timeline)
|
|
2052
2095
|
派发到 10 类构图之一;没有 scene_type 的旧 timeline 按 kind 映射到兼容类型。
|
|
2053
2096
|
|
|
2054
|
-
原来的第 1 条分支(legacy —— film.json 不写 style
|
|
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
|
-
# ----------------
|
|
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
|
-
|
|
2087
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2134
|
-
|
|
2176
|
+
条目落位步长与场景绘制共用 plan["pacing"]["item_stagger"],
|
|
2177
|
+
两边读同一个数,否则音效会错位(写死 0.32 时踩过这个坑)。
|
|
2135
2178
|
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
|
1
|
+
# 外观定制:设计 DSL 实操手册
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
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
|
-
|
|
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 种不同) |
|
|
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 |
|
|
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
|
|
55
|
+
## 3. 字幕样式(visual.subtitle)
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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
|
-
| 想"用某套预设当起点" |
|
|
120
|
-
| `render.py --preset x` 报错 | `--preset`
|
|
121
|
-
| `stylekit.py --list` 只有一段说明、exit 2 |
|
|
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` 的配色采样示例(那不是可选清单) |
|