@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
package/tools/silhouette_qa.py
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""silhouette_qa.py —
|
|
2
|
+
"""silhouette_qa.py — 跨设计剪影可区分门禁(G13)。
|
|
3
3
|
|
|
4
4
|
解决什么问题
|
|
5
5
|
------------
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
用户原则 #5:"不同视频的整体风格不能大面积/长时间重复。" 颜色差异肉眼明显、结构差异却
|
|
7
|
+
从来没人量过:同一批绘制器与版式几何下,换一套皮肤(配色 / 字体 / 纹理 / 字距)之后,
|
|
8
|
+
灰度三值化后的**构图骨架**(前景占比、重心、边缘占用、左右对称、纵向明暗剖面)是否真的
|
|
9
|
+
变了?本工具把"骨架到底有没有变"变成可复核的数字,而不是审美口水。
|
|
10
|
+
|
|
11
|
+
输入从哪来
|
|
12
|
+
----------
|
|
13
|
+
技能里没有内置风格表(stylekit.STYLE_IDS == ()),"同一剧本换 style"这种输入不存在。
|
|
14
|
+
"多部片"来自 **designkit 现场采样**:同一 brief 不同 seed(或不同 brief)得到 N 个设计,
|
|
15
|
+
每个设计是一整条独立外观(配色/母题/版式/HUD/字体族/字号比例/字距/行距/重心偏移/转场/缓动/编号写法…)。
|
|
16
|
+
所以本工具比的是 **N 个设计之间两两比较**——这才对应"不同视频会不会长得一样"。
|
|
10
17
|
|
|
11
18
|
算法(验收口径;改阈值 / 改指标之前先读这一节)
|
|
12
19
|
----------------------------------------------
|
|
13
|
-
|
|
14
|
-
1. 用同一份合成剧本(title="剪影校验",固定 3 段内容,每段 dur=4.0s
|
|
15
|
-
|
|
20
|
+
对每一对设计 (A, B):
|
|
21
|
+
1. 用同一份合成剧本(title="剪影校验",固定 3 段内容,每段 dur=4.0s),只换设计;
|
|
22
|
+
对每个场景类型(默认 hero/list/stat)各渲 3 帧:t_local = 0.05 / 0.5 / 0.9 × dur。
|
|
16
23
|
frame = int(t_global * fps),fps=30;渲染直接调 assets/scenes.py 的 draw_frame,
|
|
17
24
|
**不叠字幕、不经过 ffmpeg** —— 本工具量的是"渲染器画出来的构图",不是成片编码。
|
|
18
|
-
合成剧本的 scene 同时写 kind / type
|
|
19
|
-
并补上 stats / sides / steps / series / quote
|
|
20
|
-
|
|
25
|
+
合成剧本的 scene 同时写 kind / type / scene_type(新旧绘制器都能认出场景类型),
|
|
26
|
+
并补上 stats / sides / steps / series / quote 等内容字段:同一段内容在不同版本的
|
|
27
|
+
绘制器下都有素材可画,才有资格比较"构图是不是同一张骨架"。
|
|
28
|
+
scene_layout 取该设计的 plan["layout_default"]:设计说"标题居中"就该渲成居中,
|
|
29
|
+
否则版式差异会被强行抹平成 left-stack,等于把"设计意图"从判据里删掉。
|
|
21
30
|
2. 灰度化(PIL 的 ITU-R 601 亮度)+ 降采样到 480×270(box)—— 低分辨率压掉字体、
|
|
22
31
|
纹理、抗锯齿这些"皮肤级"差异,留下块状结构。
|
|
23
32
|
3. 三值化:基准亮度 b = 本帧**四角各 40×40 块灰度中位数**。用四角而不是主题 bg 色,
|
|
@@ -26,65 +35,73 @@
|
|
|
26
35
|
d < 15 -> 0 背景
|
|
27
36
|
15 <= d < 60 -> 1 中间(纹理、半透明卡片、渐变、抗锯齿边)
|
|
28
37
|
d >= 60 -> 2 前景(文字、实心图形)
|
|
29
|
-
4.
|
|
38
|
+
4. 六项指标(全部在 480×270 上算,百分比相对画面):
|
|
30
39
|
F 前景档(2) 像素占比 × 100
|
|
31
40
|
Cx/Cy 前景像素质心 x / y(0..100,按像素中心口径)
|
|
32
41
|
B 四条边各 6% 带宽内"非背景档(1 或 2)"覆盖率的最大值 × 100
|
|
33
42
|
S 左右半幅前景掩码的 IoU:**右半幅水平翻转后**再与左半幅比。不翻转就不是
|
|
34
|
-
"对称度",而是"左右两半同一相对位置都有前景"——那对居中标题恒为 0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
"对称度",而是"左右两半同一相对位置都有前景"——那对居中标题恒为 0。
|
|
44
|
+
P 垂直 9 带平均灰度剖面减自身中位数后,两帧逐带差的最大值。Why:F/Cx/Cy/B/S
|
|
45
|
+
都是"按前景像素算"的指标,而前景档要求 |gray-b|>=60;letterbox 的上下黑边
|
|
46
|
+
与近黑主题底色只差 ~9,**黑边被算成背景,cinema 最标志性的结构在判据里根本
|
|
47
|
+
不存在**。P 量的是"画面纵向明暗分布",不受字体影响。
|
|
48
|
+
5. 判定"该场景可区分":任一项达标即可 ——
|
|
49
|
+
ΔF >= 6pp、|ΔCx| >= 8、|ΔCy| >= 8、ΔB >= 14、ΔS >= 0.20、ΔP >= 6.0。
|
|
38
50
|
关键约束:**同一项指标必须在 3 帧上全部达标**。挑单帧最容易骗过门禁:入场瞬间
|
|
39
|
-
|
|
40
|
-
6.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
但**不要期待改造前"全红"**:8 套风格本身的版式(left-stack / split-right / center-stack)
|
|
52
|
-
与 HUD 档位不同,灰度结构上仍会拉开一部分对子;尤其跨明暗族(深底 vs 浅底)会被 ΔB
|
|
53
|
-
天然抬到很高(B 的基准是四角中位数,浅色主题整屏 bg→bg2 渐变就可能让整条边带"非背景")。
|
|
54
|
-
所以报告按**明暗族分组**统计:同族通过数才是"骨架真的不同"的保守下界;跨族且只靠 ΔB
|
|
55
|
-
通过的对会被单独点名,复核时别把它们当成结构差异的证据。这是口径本身的局限,不是
|
|
56
|
-
实现 bug —— 改判定线要单独评审,不能靠这里偷偷放宽/收紧。
|
|
51
|
+
两个设计的位移差本来就大,只看一帧会把"同一骨架 + 不同缓动"误判成结构不同。
|
|
52
|
+
6. 一对设计在**任一场景类型**上可区分即算 PASS;所有场景都不达标才算 FAIL。
|
|
53
|
+
|
|
54
|
+
同族 vs 跨族(口径局限,别读错数)
|
|
55
|
+
----------------------------------
|
|
56
|
+
B 的基准是四角中位数:浅色主题整屏 bg→bg2 渐变本身就可能超过 15 的中间档门槛,整条边带
|
|
57
|
+
都被判"非背景",于是浅 vs 深的 ΔB 往往只是背景亮度差,不是构图差。报告按**明暗族**分组
|
|
58
|
+
统计:同族通过数才是"骨架真的不同"的保守下界;跨族且只靠 ΔB 通过的对会被单独点名。
|
|
59
|
+
这是口径本身的局限,不是实现 bug —— 改判定线要单独评审,不能靠这里偷偷放宽/收紧。
|
|
60
|
+
|
|
61
|
+
注意:本工具**只量骨架,不量配色**。不同设计之间颜色必然不同(连续性色相空间),
|
|
62
|
+
但"颜色不同"不是原则 #5 的证据;所以判定完全由灰度结构指标给出,"颜色差异"不计分。
|
|
57
63
|
|
|
58
64
|
输出与退出码
|
|
59
65
|
------------
|
|
60
|
-
*
|
|
61
|
-
* 汇总:N
|
|
66
|
+
* 人读表格:设计对 × 场景类型 × 六项差值(按帧列出)+ PASS/FAIL(靠哪项达标)
|
|
67
|
+
* 汇总:N 对设计、通过 M 对,失败对按"最接近阈值"排序(便于优先修)
|
|
62
68
|
* --json 输出机读结构(含每对、每场景、每帧的指标明细),供门禁消费
|
|
63
69
|
* 退出码:全部通过 0;有失败 1;--strict 下存在跳过(skip)时 2。
|
|
64
70
|
跳过 = 帧渲不出来 / 指标缺值,**skipped != 通过**:它只说明这次没证据,不说明
|
|
65
71
|
骨架相同;--strict 用于 CI 里把"没跑成"也当红。
|
|
66
72
|
|
|
67
|
-
CLI
|
|
68
|
-
|
|
69
|
-
python tools/silhouette_qa.py [--
|
|
73
|
+
CLI(没有 --styles:STYLE_IDS 是空元组,按风格 ID 选已经无意义)
|
|
74
|
+
------------------------------------------------------------------------------------------
|
|
75
|
+
python tools/silhouette_qa.py [--designs 3] [--seed 1] [--brief "题材/要求"]
|
|
76
|
+
[--design-json PATH]
|
|
70
77
|
[--scenes hero,list,stat]
|
|
71
78
|
[--frames 0.05,0.5,0.9]
|
|
72
79
|
[--json] [--strict] [--out PATH]
|
|
73
|
-
[--scenes-py PATH] [--skill-root DIR]
|
|
74
|
-
--
|
|
75
|
-
--
|
|
76
|
-
--
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
--
|
|
80
|
-
--
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
[--scenes-py PATH] [--skill-root DIR] [--selftest]
|
|
81
|
+
--designs 现场采样几个设计(>=2,缺省 3;两两比较)
|
|
82
|
+
--seed 采样种子:固定值 = 可复现(缺省 1,CI 用它);传 -1 / random = 每次新设计
|
|
83
|
+
--brief 采样用的 brief 文本(缺省内置的一句话题材,不联网、不需要用户输入)
|
|
84
|
+
--design-json 诊断用:不采样,直接读 design.json(单设计 / 数组 / {"designs":[...]});
|
|
85
|
+
给 1 个设计时只能做确定性自检,给 >=2 个才做两两比较
|
|
86
|
+
--scenes 场景类型(逗号分隔,默认 hero,list,stat)
|
|
87
|
+
--scenes-py 诊断用:换一份 scenes.py 渲染(默认 <skill-root>/assets/scenes.py)
|
|
88
|
+
--skill-root 诊断用:改用含 scripts/ 与 assets/ 的另一份技能快照(如 git archive
|
|
89
|
+
出来的改造前版本),便于"改造前 vs 改造后"用同一个工具对比;
|
|
90
|
+
JSON 的 inputs 会记录两边的 sha1,避免比错文件
|
|
91
|
+
--out 额外落一份 JSON(缺省只打印,不写文件)
|
|
92
|
+
--strict 有跳过时退出码 2(缺省跳过不影响退出码,但报告里一定单独列出)
|
|
93
|
+
--selftest 自检(离线,<=60s):合成 2 组设计并渲染,断言
|
|
94
|
+
① 不同设计之间可区分(PASS)
|
|
95
|
+
② 同一设计重复渲染逐像素一致(PASS,确定性)
|
|
96
|
+
③ 负控制:同一设计自己跟自己比必须 FAIL(否则门禁恒真,等于没门禁)
|
|
97
|
+
全过退出 0,任一不过退出 1。ci_check 会调用它。
|
|
98
|
+
|
|
99
|
+
依赖:stdlib + numpy + Pillow + 同仓库的 designkit / stylekit / filmkit / scenes
|
|
100
|
+
(项目已有依赖,无新增);不依赖 TTS / ffmpeg / 网络,纯本地渲染。
|
|
84
101
|
"""
|
|
85
102
|
from __future__ import annotations
|
|
86
103
|
|
|
87
|
-
__version__ = "0.
|
|
104
|
+
__version__ = "1.0.4"
|
|
88
105
|
|
|
89
106
|
import argparse
|
|
90
107
|
import copy
|
|
@@ -107,12 +124,20 @@ SCRIPTS = os.path.join(SKILL, "scripts")
|
|
|
107
124
|
ASSETS = os.path.join(SKILL, "assets")
|
|
108
125
|
DEFAULT_SCENES_PY = os.path.join(ASSETS, "scenes.py")
|
|
109
126
|
|
|
110
|
-
RESULT_VERSION =
|
|
127
|
+
RESULT_VERSION = 2
|
|
111
128
|
FPS = 30
|
|
112
129
|
SCENE_DUR = 4.0
|
|
113
130
|
DEFAULT_FRACS = (0.05, 0.5, 0.9)
|
|
114
131
|
DEFAULT_SCENE_TYPES = ("hero", "list", "stat")
|
|
115
132
|
SYNTH_TITLE = "剪影校验"
|
|
133
|
+
# 现场采样的默认数:3 个设计 = 3 对,足够回答"会不会长得一样",又比 4 个少 1/3 渲染。
|
|
134
|
+
DEFAULT_DESIGNS = 3
|
|
135
|
+
# 默认固定种子:门禁的结论必须可复现(同一条命令两次跑出同一个 PASS/FAIL)。
|
|
136
|
+
# 想要"每次新设计"就传 --seed -1;CI 与 selfcheck 走缺省值。
|
|
137
|
+
DEFAULT_SEED = 1
|
|
138
|
+
AUTO_SEEDS = (-1, "random", "auto")
|
|
139
|
+
# 采样 brief:一句话题材即可,采样器只把它当"给什么题材做方案"的背景信息。
|
|
140
|
+
DEFAULT_BRIEF = "剪影校验:同一份内容,多套现场采样的设计"
|
|
116
141
|
|
|
117
142
|
# 量化口径(480×270 是本工具定的"结构分辨率":再低会把小字抹掉,再高会开始量字体)
|
|
118
143
|
QUAL_W, QUAL_H = 480, 270
|
|
@@ -120,27 +145,29 @@ CORNER = 40
|
|
|
120
145
|
TIER_BG_MAX = 15 # d < 15 -> 背景档 0
|
|
121
146
|
TIER_FG_MIN = 60 # d >= 60 -> 前景档 2
|
|
122
147
|
EDGE_BAND = 0.06 # 四边各 6% 带宽
|
|
123
|
-
METRIC_ORDER = ("F", "Cx", "Cy", "B", "S")
|
|
124
|
-
#
|
|
125
|
-
|
|
148
|
+
METRIC_ORDER = ("F", "Cx", "Cy", "B", "S", "P")
|
|
149
|
+
# 达标线:F>=6pp / Cx,Cy>=8 / B>=14 / S>=0.20 / P>=6.0。
|
|
150
|
+
# 改这里等于改验收口径,需说明理由;v2.0.0 只换输入(设计而不是风格),不放宽任何一条。
|
|
151
|
+
THRESHOLDS = {"F": 6.0, "Cx": 8.0, "Cy": 8.0, "B": 14.0, "S": 0.20, "P": 6.0}
|
|
126
152
|
METRIC_LABELS = {
|
|
127
153
|
"F": "前景像素占比(%)", "Cx": "前景质心x(占宽%)", "Cy": "前景质心y(占高%)",
|
|
128
154
|
"B": "四边非背景覆盖率(%)", "S": "左右半幅前景 IoU",
|
|
155
|
+
"P": "垂直明暗剖面最大偏差(灰度级)",
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
# ============================================================================
|
|
132
|
-
#
|
|
159
|
+
# 合成剧本:同一份内容,只有设计不同
|
|
133
160
|
# ============================================================================
|
|
134
161
|
# 每类场景给足该类型的"原料字段":hero 要 big/sub/kicker,list 要 items,stat 要
|
|
135
162
|
# stats 或"数字+单位",其余类型按 grammar.py 的契约补齐。给全的理由:本工具要能在
|
|
136
|
-
#
|
|
137
|
-
#
|
|
163
|
+
# 改造前的旧绘制器、改造后的 10 绘制器上都跑——旧版忽略多余字段,新版缺字段就画空。
|
|
164
|
+
# 文案固定不随设计变化,否则"构图差"里会混进"内容差"。
|
|
138
165
|
SCENE_CONTENT = {
|
|
139
166
|
"hero": dict(
|
|
140
167
|
kicker="00 · 开场", chapter="01 · 开场",
|
|
141
168
|
big="结构决定第一印象",
|
|
142
|
-
sub="
|
|
143
|
-
text="
|
|
169
|
+
sub="同一份稿子,多套构图骨架",
|
|
170
|
+
text="结构决定第一印象。同一份稿子,多套构图骨架。",
|
|
144
171
|
items=[]),
|
|
145
172
|
"statement": dict(
|
|
146
173
|
kicker="01 · 金句", chapter="01 · 金句",
|
|
@@ -250,9 +277,19 @@ def _short_err(exc):
|
|
|
250
277
|
return "%s: %s" % (type(exc).__name__, exc)
|
|
251
278
|
|
|
252
279
|
|
|
280
|
+
def _sha256_image(img):
|
|
281
|
+
"""一帧像素的 sha256:先写画布尺寸,再写 RGB 字节(尺寸变了哈希也一定变)。"""
|
|
282
|
+
if img.mode != "RGB":
|
|
283
|
+
img = img.convert("RGB")
|
|
284
|
+
h = hashlib.sha256()
|
|
285
|
+
h.update(("%dx%d|" % tuple(img.size)).encode("ascii"))
|
|
286
|
+
h.update(img.tobytes())
|
|
287
|
+
return h.hexdigest()
|
|
288
|
+
|
|
289
|
+
|
|
253
290
|
# ---------------------------------------------------------------- 依赖装载
|
|
254
291
|
def _modules(scripts_dir=SCRIPTS):
|
|
255
|
-
"""装载 filmkit / stylekit。tools/ 与 scripts/ 不同目录,先补 sys.path
|
|
292
|
+
"""装载 filmkit / stylekit / designkit。tools/ 与 scripts/ 不同目录,先补 sys.path。
|
|
256
293
|
|
|
257
294
|
支持换一个 scripts 目录(--skill-root 诊断用):先清掉 sys.modules 里的同名条目,
|
|
258
295
|
否则第二次调用拿到的还是上一份模块,"改造前 vs 改造后"会串档。
|
|
@@ -260,7 +297,7 @@ def _modules(scripts_dir=SCRIPTS):
|
|
|
260
297
|
scripts_dir = os.path.abspath(str(scripts_dir))
|
|
261
298
|
if scripts_dir not in sys.path:
|
|
262
299
|
sys.path.insert(0, scripts_dir)
|
|
263
|
-
for name in ("filmkit", "stylekit"):
|
|
300
|
+
for name in ("filmkit", "stylekit", "designkit"):
|
|
264
301
|
if sys.modules.get(name) is not None:
|
|
265
302
|
path = str(getattr(sys.modules[name], "__file__", "") or "")
|
|
266
303
|
if os.path.dirname(os.path.abspath(path)) != scripts_dir:
|
|
@@ -268,16 +305,25 @@ def _modules(scripts_dir=SCRIPTS):
|
|
|
268
305
|
try:
|
|
269
306
|
import filmkit as K
|
|
270
307
|
import stylekit as ST
|
|
271
|
-
|
|
308
|
+
import designkit as DK
|
|
309
|
+
return K, ST, DK
|
|
272
310
|
except Exception as e: # noqa: BLE001
|
|
273
|
-
raise RuntimeError("装载 filmkit/stylekit 失败(%s):%s"
|
|
311
|
+
raise RuntimeError("装载 filmkit/stylekit/designkit 失败(%s):%s"
|
|
312
|
+
% (scripts_dir, e)) from e
|
|
274
313
|
|
|
275
314
|
|
|
276
315
|
def _load_scenes(path):
|
|
277
|
-
"""按路径装载 scenes.py(独立模块名,避免与站点包里的同名模块串档)。
|
|
316
|
+
"""按路径装载 scenes.py(独立模块名,避免与站点包里的同名模块串档)。
|
|
317
|
+
|
|
318
|
+
scenes.py 自己会 import filmkit / stylekit:装载前把 scripts/ 与 assets/ 都补进
|
|
319
|
+
sys.path,否则换一份 skill-root 时 ModuleNotFoundError(golden_frames 同处理)。
|
|
320
|
+
"""
|
|
278
321
|
path = os.path.abspath(str(path))
|
|
279
322
|
if not os.path.isfile(path):
|
|
280
323
|
raise RuntimeError("scenes.py 不存在:%s" % path)
|
|
324
|
+
for p in (os.path.dirname(path), SCRIPTS, ASSETS):
|
|
325
|
+
if p and p not in sys.path:
|
|
326
|
+
sys.path.insert(0, p)
|
|
281
327
|
spec = importlib.util.spec_from_file_location("silhouette_scenes", path)
|
|
282
328
|
if spec is None or spec.loader is None:
|
|
283
329
|
raise RuntimeError("scenes.py 无法作为模块加载:%s" % path)
|
|
@@ -290,9 +336,88 @@ def _load_scenes(path):
|
|
|
290
336
|
return mod
|
|
291
337
|
|
|
292
338
|
|
|
339
|
+
# ---------------------------------------------------------------- 设计 -> 计划
|
|
340
|
+
def load_designkits(scripts_dir=SCRIPTS):
|
|
341
|
+
"""只把 designkit 拿出来(采样 / 校验 / design_to_plan 的唯一真源)。"""
|
|
342
|
+
return _modules(scripts_dir)[2]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def sample_designs(DK, n, seed, brief=None):
|
|
346
|
+
"""现场采样 n 个设计(不同 brief 或同 brief 不同 seed,跨设计两两比较)。
|
|
347
|
+
|
|
348
|
+
为什么要"现场采样"而不是读一份写死的清单:技能里没有任何内置外观表,
|
|
349
|
+
多部片的差异全部来自 designkit 的连续采样;门禁必须量**采样器真的会产出的东西**。
|
|
350
|
+
seed=None 表示每次新设计(采样器的默认语义);本工具缺省固定 seed 以保结论可复现。
|
|
351
|
+
"""
|
|
352
|
+
b = {"topic": str(brief or DEFAULT_BRIEF)}
|
|
353
|
+
if seed is None:
|
|
354
|
+
return list(DK.sample_designs(b, n=int(n)))
|
|
355
|
+
return list(DK.sample_designs(b, n=int(n), seed=int(seed)))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _normalize_designs(payload):
|
|
359
|
+
"""design.json 可以是单个设计 / 设计数组 / {"designs":[...]} / {"candidates":[...]}。"""
|
|
360
|
+
if isinstance(payload, list):
|
|
361
|
+
designs = payload
|
|
362
|
+
elif isinstance(payload, dict):
|
|
363
|
+
designs = None
|
|
364
|
+
for key in ("designs", "candidates"):
|
|
365
|
+
if isinstance(payload.get(key), list):
|
|
366
|
+
designs = payload[key]
|
|
367
|
+
break
|
|
368
|
+
if designs is None:
|
|
369
|
+
designs = [payload]
|
|
370
|
+
else:
|
|
371
|
+
raise ValueError("design.json 必须是设计对象或设计数组")
|
|
372
|
+
bad = [i for i, d in enumerate(designs) if not isinstance(d, dict)]
|
|
373
|
+
if bad:
|
|
374
|
+
raise ValueError("design.json 里第 %s 项不是对象(设计必须是 dict)"
|
|
375
|
+
% ", ".join(str(i + 1) for i in bad[:4]))
|
|
376
|
+
if not designs:
|
|
377
|
+
raise ValueError("design.json 里没有任何设计")
|
|
378
|
+
return designs
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _design_ids(designs):
|
|
382
|
+
"""设计对外的短 id:用设计自己的 id;重复时加 -2/-3 后缀(采样 id 是 8 位哈希,极少撞)。"""
|
|
383
|
+
ids, used = [], set()
|
|
384
|
+
for i, d in enumerate(designs):
|
|
385
|
+
base = str(d.get("id") or ("design-%02d" % (i + 1)))
|
|
386
|
+
did, k = base, 2
|
|
387
|
+
while did in used:
|
|
388
|
+
did = "%s-%d" % (base, k)
|
|
389
|
+
k += 1
|
|
390
|
+
used.add(did)
|
|
391
|
+
ids.append(did)
|
|
392
|
+
return ids
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def design_plan(DK, design):
|
|
396
|
+
"""设计 -> 计划(designkit.design_to_plan 已含 validate + theme_lint,失败直接抛)。"""
|
|
397
|
+
plan = DK.design_to_plan(design)
|
|
398
|
+
if not isinstance(plan, dict):
|
|
399
|
+
raise RuntimeError("design_to_plan 返回 %r,不是 dict" % type(plan).__name__)
|
|
400
|
+
return plan
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _family_of(ST, plan):
|
|
404
|
+
"""明暗族:dark / light。只作诊断分组,不参与判定(见模块文档"口径局限")。"""
|
|
405
|
+
th = plan.get("theme")
|
|
406
|
+
try:
|
|
407
|
+
if isinstance(th, dict) and th:
|
|
408
|
+
return "dark" if ST.is_dark(th) else "light"
|
|
409
|
+
except Exception: # noqa: BLE001 —— 分组失败不该毁门禁
|
|
410
|
+
pass
|
|
411
|
+
return "unknown"
|
|
412
|
+
|
|
413
|
+
|
|
293
414
|
# ---------------------------------------------------------------- 合成剧本 / 场景
|
|
294
|
-
def build_script(
|
|
295
|
-
"""造一份"
|
|
415
|
+
def build_script(design_id, scene_types):
|
|
416
|
+
"""造一份"只有设计不同"的合成剧本(title/lines 固定,dur 固定 4.0s)。
|
|
417
|
+
|
|
418
|
+
style 字段不写任何风格 id(没有内置风格表);渲染用的计划由
|
|
419
|
+
run_gate 塞进 script["_style"],scenes.draw_frame 优先读它。
|
|
420
|
+
"""
|
|
296
421
|
lines = []
|
|
297
422
|
for i, stype in enumerate(scene_types):
|
|
298
423
|
c = SCENE_CONTENT[stype]
|
|
@@ -300,18 +425,19 @@ def build_script(style_id, scene_types):
|
|
|
300
425
|
row.update(id="qa-%02d-%s" % (i + 1, stype), kind=stype, type=stype,
|
|
301
426
|
dur=SCENE_DUR)
|
|
302
427
|
lines.append(row)
|
|
303
|
-
script = dict(title=SYNTH_TITLE,
|
|
304
|
-
_total=round(SCENE_DUR * len(scene_types), 3)
|
|
428
|
+
script = dict(title=SYNTH_TITLE, fps=FPS, lines=lines,
|
|
429
|
+
_total=round(SCENE_DUR * len(scene_types), 3),
|
|
430
|
+
_design=str(design_id))
|
|
305
431
|
return script
|
|
306
432
|
|
|
307
433
|
|
|
308
|
-
def build_scenes(scene_types):
|
|
434
|
+
def build_scenes(scene_types, layout_default="left-stack"):
|
|
309
435
|
"""造 scene dict:start/end 连续排,三条内容共用同一段长。
|
|
310
436
|
|
|
311
|
-
kind
|
|
312
|
-
body ——
|
|
313
|
-
|
|
314
|
-
|
|
437
|
+
kind / type / scene_type 都写同一个场景类型:旧绘制器只认 kind(认不出就回落
|
|
438
|
+
body —— 那正是基线大量 FAIL 的机制),新绘制器认 type / scene_type;多写几个别名
|
|
439
|
+
不改变任何绘制结果,只增加版本兼容面。scene_layout 从设计计划来:设计说居中就该
|
|
440
|
+
居中,否则版式差异会被强行抹平。
|
|
315
441
|
"""
|
|
316
442
|
rows = []
|
|
317
443
|
for i, stype in enumerate(scene_types):
|
|
@@ -323,7 +449,7 @@ def build_scenes(scene_types):
|
|
|
323
449
|
row.setdefault("items", [])
|
|
324
450
|
row.update(id="qa-%02d-%s" % (i + 1, stype), kind=stype, type=stype,
|
|
325
451
|
scene_type=stype, start=start, end=round(start + SCENE_DUR, 6),
|
|
326
|
-
dur=SCENE_DUR)
|
|
452
|
+
dur=SCENE_DUR, scene_layout=str(layout_default or "left-stack"))
|
|
327
453
|
rows.append(row)
|
|
328
454
|
return rows
|
|
329
455
|
|
|
@@ -356,7 +482,7 @@ def quantize(img):
|
|
|
356
482
|
|
|
357
483
|
base 取四角 40×40 的中位数:本项目正文/装饰都在安全区内,四角一定是背景;而
|
|
358
484
|
主题 bg 色在不同明暗族之间差一个数量级,直接拿"主题色"当基准会让深色与浅色
|
|
359
|
-
|
|
485
|
+
设计的 |gray-b| 落在不同的分档分布里——那不是构图差,是配色差被误算成结构差。
|
|
360
486
|
"""
|
|
361
487
|
gray = np.asarray(img.convert("L").resize((QUAL_W, QUAL_H), Image.BOX),
|
|
362
488
|
dtype=np.float32)
|
|
@@ -375,12 +501,11 @@ def quantize(img):
|
|
|
375
501
|
|
|
376
502
|
|
|
377
503
|
def measure(img):
|
|
378
|
-
"""一张图 -> (
|
|
504
|
+
"""一张图 -> (六项指标, 基准亮度)。数值全部是 Python float(JSON 可序列化)。
|
|
379
505
|
|
|
380
|
-
基准亮度一并返回是为了**诊断**:它定义了"本帧背景有多亮"
|
|
506
|
+
基准亮度一并返回是为了**诊断**:它定义了"本帧背景有多亮",据此可以把设计分成
|
|
381
507
|
明暗族。量到的 B 在深浅主题之间天然不可比(浅色主题整屏 bg→bg2 的渐变就可能超过
|
|
382
|
-
15 的中间档门槛,整条边带都算"非背景"
|
|
383
|
-
背景亮度差"冒充"构图骨架差"。判定口径本身不因此改变。
|
|
508
|
+
15 的中间档门槛,整条边带都算"非背景"),所以报告要按明暗族分组看。
|
|
384
509
|
"""
|
|
385
510
|
_gray, base, tier = quantize(img)
|
|
386
511
|
fg = tier == 2
|
|
@@ -398,8 +523,7 @@ def measure(img):
|
|
|
398
523
|
nonbg = tier != 0
|
|
399
524
|
band_x = max(1, int(round(QUAL_W * EDGE_BAND)))
|
|
400
525
|
band_y = max(1, int(round(QUAL_H * EDGE_BAND)))
|
|
401
|
-
# B =
|
|
402
|
-
# 取最大值而不是平均,是为了不被另外三条空边稀释(大多数版式只有一侧有内容)。
|
|
526
|
+
# B = 四边覆盖率的**最大值**:只要有一条边被构图压住,两套"边缘语言"就不同。
|
|
403
527
|
cover = (float(nonbg[:band_y, :].mean()), float(nonbg[-band_y:, :].mean()),
|
|
404
528
|
float(nonbg[:, :band_x].mean()), float(nonbg[:, -band_x:].mean()))
|
|
405
529
|
B = max(cover) * 100.0
|
|
@@ -410,30 +534,39 @@ def measure(img):
|
|
|
410
534
|
inter = int(np.logical_and(left, right).sum())
|
|
411
535
|
# 两半都空 = 完美对称(空构图自己跟自己的 IoU 是 1);只有一边空才是 0。
|
|
412
536
|
S = 1.0 if union == 0 else float(inter) / float(union)
|
|
537
|
+
# P:垂直 9 带的平均灰度剖面,减去自身中位数后再取"两套设计逐带差的最大值"。
|
|
538
|
+
# 减中位数是为了抗整体亮度/主题差(否则跨明暗族会把配色差当成结构差);
|
|
539
|
+
# 用整行平均而不是逐像素,是为了抗字体(字形一变,逐像素全变,行均值几乎不动)。
|
|
540
|
+
rows = np.array_split(np.arange(QUAL_H), 9)
|
|
541
|
+
prof = np.array([_gray[r].mean() for r in rows], dtype=np.float32)
|
|
542
|
+
prof = prof - float(np.median(prof))
|
|
413
543
|
metrics = {"F": round(F, 3), "Cx": None if Cx is None else round(Cx, 3),
|
|
414
544
|
"Cy": None if Cy is None else round(Cy, 3),
|
|
415
|
-
"B": round(B, 3), "S": round(S, 4)
|
|
545
|
+
"B": round(B, 3), "S": round(S, 4),
|
|
546
|
+
"P": [round(float(x), 3) for x in prof]}
|
|
416
547
|
return metrics, base
|
|
417
548
|
|
|
418
549
|
|
|
419
|
-
# ----------------------------------------------------------------
|
|
420
|
-
def
|
|
421
|
-
"""
|
|
550
|
+
# ---------------------------------------------------------------- 逐设计渲染
|
|
551
|
+
def render_design(scenes_mod, ST, DK, design_id, design, scene_types, fracs, fps=FPS):
|
|
552
|
+
"""渲一套设计的全部帧。逐帧 try/except:一帧炸了记 skip,不让整个门禁崩。
|
|
422
553
|
|
|
423
554
|
返回 {"meta": {...}, "scenes": {scene_type: [frame_record, ...]}}。
|
|
424
555
|
"""
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
#
|
|
556
|
+
plan = design_plan(DK, design)
|
|
557
|
+
layout_default = str(plan.get("layout_default") or "left-stack")
|
|
558
|
+
script = build_script(design_id, scene_types)
|
|
559
|
+
# 先解好计划塞进 _style:scenes._plan() 优先读它,避免每帧重复解析;
|
|
560
|
+
# 也保证同一设计 9 帧用的是同一份计划(含 theme_overrides)。
|
|
429
561
|
script["_style"] = plan
|
|
430
|
-
out = {"meta": dict(
|
|
431
|
-
|
|
562
|
+
out = {"meta": dict(design=str(design_id),
|
|
563
|
+
label=str(design.get("label") or ""),
|
|
432
564
|
theme_id=plan.get("theme_id"),
|
|
433
|
-
layout_default=
|
|
434
|
-
|
|
565
|
+
layout_default=layout_default,
|
|
566
|
+
family=_family_of(ST, plan),
|
|
567
|
+
design_fp=_design_fp(DK, design)),
|
|
435
568
|
"scenes": {}}
|
|
436
|
-
for scene in build_scenes(scene_types):
|
|
569
|
+
for scene in build_scenes(scene_types, layout_default):
|
|
437
570
|
stype = str(scene.get("type"))
|
|
438
571
|
recs = []
|
|
439
572
|
for frac in fracs:
|
|
@@ -443,6 +576,7 @@ def render_style(scenes_mod, ST, style_id, scene_types, fracs, fps=FPS):
|
|
|
443
576
|
rec["t_local"] = round(float(frac) * SCENE_DUR, 4)
|
|
444
577
|
rec["t_global"] = round(float(scene["start"]) + float(frac) * SCENE_DUR, 4)
|
|
445
578
|
rec["frame"] = int(frame)
|
|
579
|
+
rec["sha256"] = _sha256_image(img)
|
|
446
580
|
rec["metrics"], base = measure(img)
|
|
447
581
|
rec["base"] = round(float(base), 1)
|
|
448
582
|
rec["error"] = None
|
|
@@ -453,7 +587,6 @@ def render_style(scenes_mod, ST, style_id, scene_types, fracs, fps=FPS):
|
|
|
453
587
|
recs.append(rec)
|
|
454
588
|
out["scenes"][stype] = recs
|
|
455
589
|
# 明暗族:一帧四角基准亮度的中位数。只作诊断分组,不参与判定。
|
|
456
|
-
# 阈值 128 是灰阶中点:本项目 8 套主题明显分成"深底亮字"与"浅底暗字"两族。
|
|
457
590
|
bases = [rec["base"] for rows in out["scenes"].values() for rec in rows
|
|
458
591
|
if rec.get("base") is not None]
|
|
459
592
|
meta = out["meta"]
|
|
@@ -463,29 +596,52 @@ def render_style(scenes_mod, ST, style_id, scene_types, fracs, fps=FPS):
|
|
|
463
596
|
return out
|
|
464
597
|
|
|
465
598
|
|
|
599
|
+
def _design_fp(DK, design):
|
|
600
|
+
"""设计指纹(诊断用):优先 designkit.design_fingerprint,没有就按规范 JSON 取 sha1。
|
|
601
|
+
|
|
602
|
+
指纹只用于"这份设计是不是同一版"的追溯,不参与任何判定(跨机器可复现同一指纹)。
|
|
603
|
+
"""
|
|
604
|
+
fn = getattr(DK, "design_fingerprint", None)
|
|
605
|
+
if callable(fn):
|
|
606
|
+
try:
|
|
607
|
+
return str(fn(design))
|
|
608
|
+
except Exception: # noqa: BLE001 —— 指纹坏了不毁门禁
|
|
609
|
+
pass
|
|
610
|
+
payload = json.dumps(design, ensure_ascii=False, sort_keys=True,
|
|
611
|
+
separators=(",", ":"), default=str)
|
|
612
|
+
return hashlib.sha1(payload.encode("utf-8")).hexdigest()[:16]
|
|
613
|
+
|
|
614
|
+
|
|
466
615
|
# ---------------------------------------------------------------- 比较 / 判定
|
|
467
616
|
def _metric_of(rec, key):
|
|
468
|
-
"""取一帧的某项指标;渲染失败或质心缺值 -> None。
|
|
617
|
+
"""取一帧的某项指标;渲染失败或质心缺值 -> None。
|
|
618
|
+
|
|
619
|
+
P(垂直明暗剖面)是**向量**,原样返回列表;其余五项是标量,统一转 float。
|
|
620
|
+
"""
|
|
469
621
|
md = rec.get("metrics")
|
|
470
622
|
if not isinstance(md, dict):
|
|
471
623
|
return None
|
|
472
624
|
v = md.get(key)
|
|
473
|
-
|
|
625
|
+
if v is None:
|
|
626
|
+
return None
|
|
627
|
+
if key == "P":
|
|
628
|
+
return [float(x) for x in v] if isinstance(v, (list, tuple)) else None
|
|
629
|
+
return float(v)
|
|
474
630
|
|
|
475
631
|
|
|
476
|
-
def compare_pair(render,
|
|
477
|
-
"""
|
|
632
|
+
def compare_pair(render, design_a, design_b, scene_types):
|
|
633
|
+
"""比较一对设计。返回可 JSON 化的 pair 结果(含每场景 × 每指标 × 每帧的差值)。
|
|
478
634
|
|
|
479
635
|
判定核心(与模块文档同口径):
|
|
480
636
|
* 场景可区分 = 存在一项指标,其**全部帧**的 |Δ| 都 >= 阈值;
|
|
481
|
-
*
|
|
637
|
+
* 设计对通过 = 任一场景可区分。
|
|
482
638
|
"""
|
|
483
|
-
pair = {"a":
|
|
639
|
+
pair = {"a": design_a, "b": design_b, "status": "fail",
|
|
484
640
|
"passed": [], "closest": None, "scenes": []}
|
|
485
641
|
best = None # (ratio, scene, metric, min_delta),给失败对排序用
|
|
486
642
|
for stype in scene_types:
|
|
487
|
-
rows_a = render[
|
|
488
|
-
rows_b = render[
|
|
643
|
+
rows_a = render[design_a]["scenes"].get(stype) or []
|
|
644
|
+
rows_b = render[design_b]["scenes"].get(stype) or []
|
|
489
645
|
scene_row = {"scene": stype, "status": "fail",
|
|
490
646
|
"passed_metrics": [], "metrics": {}, "skip_reason": ""}
|
|
491
647
|
n = min(len(rows_a), len(rows_b))
|
|
@@ -498,6 +654,15 @@ def compare_pair(render, style_a, style_b, scene_types):
|
|
|
498
654
|
if va is None or vb is None:
|
|
499
655
|
deltas.append(None)
|
|
500
656
|
missing = True
|
|
657
|
+
elif m == "P":
|
|
658
|
+
# 剖面是向量:逐带求差,取绝对值最大的一带作为这对设计的 P 差值。
|
|
659
|
+
pa, pb = list(va), list(vb)
|
|
660
|
+
if len(pa) != len(pb) or not pa:
|
|
661
|
+
deltas.append(None)
|
|
662
|
+
missing = True
|
|
663
|
+
else:
|
|
664
|
+
deltas.append(round(max(abs(float(x) - float(y))
|
|
665
|
+
for x, y in zip(pa, pb)), 4))
|
|
501
666
|
else:
|
|
502
667
|
deltas.append(round(abs(va - vb), 4))
|
|
503
668
|
all_frames = (n > 0) and not missing
|
|
@@ -539,35 +704,44 @@ def compare_pair(render, style_a, style_b, scene_types):
|
|
|
539
704
|
return pair
|
|
540
705
|
|
|
541
706
|
|
|
542
|
-
def
|
|
543
|
-
|
|
544
|
-
|
|
707
|
+
def run_gate(scenes_py, designs, scene_types, fracs, fps=FPS, scripts_dir=SCRIPTS,
|
|
708
|
+
design_ids=None):
|
|
709
|
+
"""主流程:装载 -> 逐设计渲帧 -> 两两比较 -> 汇总。返回 (result, elapsed_s)。
|
|
710
|
+
|
|
711
|
+
designs 是已经物化的设计 dict 列表(采样或 --design-json 读入);design_ids 缺省
|
|
712
|
+
用设计自带的 id。**同一设计之间不做比较**(自己跟自己必然 "不可区分",没有意义)。
|
|
713
|
+
"""
|
|
714
|
+
_K, ST, DK = _modules(scripts_dir)
|
|
545
715
|
scenes_mod = _load_scenes(scenes_py)
|
|
716
|
+
ids = list(design_ids or _design_ids(designs))
|
|
717
|
+
if len(ids) != len(designs):
|
|
718
|
+
raise RuntimeError("design_ids 数量(%d)与 designs(%d)不一致"
|
|
719
|
+
% (len(ids), len(designs)))
|
|
546
720
|
t0 = time.perf_counter()
|
|
547
721
|
render = {}
|
|
548
|
-
for
|
|
549
|
-
render[
|
|
722
|
+
for did, design in zip(ids, designs):
|
|
723
|
+
render[did] = render_design(scenes_mod, ST, DK, did, design, scene_types, fracs, fps)
|
|
550
724
|
pairs = [compare_pair(render, a, b, scene_types)
|
|
551
|
-
for a, b in itertools.combinations(
|
|
725
|
+
for a, b in itertools.combinations(ids, 2)]
|
|
552
726
|
elapsed = time.perf_counter() - t0
|
|
553
|
-
|
|
554
|
-
summary = summarize(pairs,
|
|
727
|
+
designs_meta = [render[did]["meta"] for did in ids]
|
|
728
|
+
summary = summarize(pairs, designs_meta)
|
|
555
729
|
render_errors = []
|
|
556
|
-
for
|
|
730
|
+
for did in ids:
|
|
557
731
|
for stype in scene_types:
|
|
558
|
-
for rec in render[
|
|
732
|
+
for rec in render[did]["scenes"].get(stype) or []:
|
|
559
733
|
if rec.get("error"):
|
|
560
|
-
render_errors.append({"
|
|
734
|
+
render_errors.append({"design": did, "scene": stype,
|
|
561
735
|
"frac": rec.get("frac"),
|
|
562
736
|
"error": rec.get("error")})
|
|
563
737
|
result = {
|
|
564
738
|
"version": RESULT_VERSION,
|
|
565
739
|
"generator": "silhouette_qa.py %s" % __version__,
|
|
566
|
-
"comment": "G13
|
|
567
|
-
"F/Cx/Cy/B/S;同一场景 3
|
|
568
|
-
"
|
|
740
|
+
"comment": "G13 跨设计剪影可区分:同一份合成剧本只换设计(designkit 现场采样),"
|
|
741
|
+
"灰度三值化后比 F/Cx/Cy/B/S/P;同一场景 3 帧须同一项指标全部达标,"
|
|
742
|
+
"任一场景达标即通过。颜色差异不参与判定。",
|
|
569
743
|
"params": {
|
|
570
|
-
"
|
|
744
|
+
"designs": list(ids), "scenes": list(scene_types),
|
|
571
745
|
"frames": [round(float(x), 4) for x in fracs],
|
|
572
746
|
"fps": int(fps), "scene_dur": SCENE_DUR,
|
|
573
747
|
"synth_title": SYNTH_TITLE,
|
|
@@ -582,22 +756,27 @@ def run_qa(scenes_py, styles, scene_types, fracs, fps=FPS, scripts_dir=SCRIPTS):
|
|
|
582
756
|
"scenes_py": _rel(scenes_py),
|
|
583
757
|
"scenes_sha1": _sha1_file(scenes_py),
|
|
584
758
|
"stylekit_sha1": _sha1_file(os.path.join(scripts_dir, "stylekit.py")),
|
|
759
|
+
"designkit_sha1": _sha1_file(os.path.join(scripts_dir, "designkit.py")),
|
|
585
760
|
"filmkit_sha1": _sha1_file(os.path.join(scripts_dir, "filmkit.py")),
|
|
586
761
|
"tool_sha1": _sha1_file(os.path.abspath(__file__)),
|
|
587
762
|
},
|
|
588
|
-
"
|
|
589
|
-
"render": {
|
|
763
|
+
"designs": designs_meta,
|
|
764
|
+
"render": {did: render[did]["scenes"] for did in ids},
|
|
590
765
|
"pairs": pairs,
|
|
591
766
|
"summary": summary,
|
|
767
|
+
# 顶层结论:直接把三档写出来,消费方不必再从 pairs/summary 里推导。
|
|
768
|
+
# 顺序与退出码一致:有 fail -> FAIL;全 skip -> SKIP;否则 PASS。
|
|
769
|
+
"verdict": ("FAIL" if summary["fail"] else
|
|
770
|
+
("SKIP" if summary["skip"] else "PASS")),
|
|
592
771
|
"render_errors": render_errors,
|
|
593
772
|
}
|
|
594
773
|
return result, elapsed
|
|
595
774
|
|
|
596
775
|
|
|
597
|
-
def summarize(pairs,
|
|
776
|
+
def summarize(pairs, designs_meta):
|
|
598
777
|
"""汇总:通过 / 失败 / 跳过各几对,失败对按"最接近阈值"降序排。
|
|
599
778
|
|
|
600
|
-
额外按**明暗族**分组统计。理由:B
|
|
779
|
+
额外按**明暗族**分组统计。理由:B 的基准是四角中位数,浅色设计(bg 240 左右、
|
|
601
780
|
bg2 220 左右)整屏渐变就可能超过 15 的中间档门槛,整条边带都被判"非背景",
|
|
602
781
|
于是浅 vs 深的 ΔB 往往只是背景亮度差,不是构图差。分组统计能把这种"跨族虚高"
|
|
603
782
|
与"同族内真的画得不一样"分开——同族通过数才是骨架差异的保守下界。
|
|
@@ -607,7 +786,7 @@ def summarize(pairs, styles_meta):
|
|
|
607
786
|
failed = [p for p in pairs if p["status"] == "fail"]
|
|
608
787
|
skipped = [{"a": p["a"], "b": p["b"]}
|
|
609
788
|
for p in pairs if p["status"] == "skip"]
|
|
610
|
-
fam = {m.get("
|
|
789
|
+
fam = {m.get("design"): m.get("family") for m in designs_meta}
|
|
611
790
|
by_family = {}
|
|
612
791
|
for label, same in (("same", True), ("cross", False)):
|
|
613
792
|
rows = [p for p in pairs
|
|
@@ -665,7 +844,6 @@ def _fmt_deltas(metric_data, key):
|
|
|
665
844
|
|
|
666
845
|
|
|
667
846
|
def _scene_line(row):
|
|
668
|
-
mark = ""
|
|
669
847
|
if row["status"] == "pass":
|
|
670
848
|
mark = "PASS(%s)" % "+".join(row["passed_metrics"])
|
|
671
849
|
elif row["status"] == "skip":
|
|
@@ -684,15 +862,15 @@ def print_human(res):
|
|
|
684
862
|
"""打印人读报告:每对的每类场景一行,失败对按接近程度排序。"""
|
|
685
863
|
p = res["params"]
|
|
686
864
|
th = p["thresholds"]
|
|
687
|
-
print("== silhouette_qa(G13
|
|
865
|
+
print("== silhouette_qa(G13 跨设计剪影可区分)")
|
|
688
866
|
print("画布量化 %d×%d(四角 %d×%d 取基准),帧时刻 %s × %.1fs,fps=%d"
|
|
689
867
|
% (p["quant"]["size"][0], p["quant"]["size"][1], p["quant"]["corner"],
|
|
690
868
|
p["quant"]["corner"], "/".join("%.2f" % f for f in p["frames"]),
|
|
691
869
|
p["scene_dur"], p["fps"]))
|
|
692
|
-
print("
|
|
870
|
+
print("设计 %d 套:%s" % (len(p["designs"]), ", ".join(p["designs"])))
|
|
693
871
|
fam = {}
|
|
694
|
-
for m in res.get("
|
|
695
|
-
fam.setdefault(str(m.get("family")), []).append(str(m.get("
|
|
872
|
+
for m in res.get("designs") or []:
|
|
873
|
+
fam.setdefault(str(m.get("family")), []).append(str(m.get("design")))
|
|
696
874
|
print("明暗族(四角基准亮度中位数):%s"
|
|
697
875
|
% ";".join("%s=[%s]" % (k, ", ".join(v)) for k, v in sorted(fam.items())))
|
|
698
876
|
print("场景 %d 类:%s;合成剧本 title=%r"
|
|
@@ -702,21 +880,21 @@ def print_human(res):
|
|
|
702
880
|
% (th["F"], th["Cx"], th["Cy"], th["B"], th["S"]))
|
|
703
881
|
print("指标:%s" % ";".join("%s=%s" % (k, METRIC_LABELS[k]) for k in METRIC_ORDER))
|
|
704
882
|
print("法:右半幅翻转后与左半幅算 IoU;Δ 列 = 各帧 |差值|;* = 该项达标")
|
|
883
|
+
print("注:只比灰度骨架,配色差异不计分(颜色不同 ≠ 结构不同)")
|
|
705
884
|
print("")
|
|
706
885
|
total = len(res["pairs"])
|
|
707
886
|
b_only = {(x["a"], x["b"]) for x in
|
|
708
887
|
(res["summary"].get("cross_family_b_only") or [])}
|
|
709
888
|
for i, pair in enumerate(res["pairs"], 1):
|
|
710
889
|
if pair["status"] == "pass":
|
|
711
|
-
verdict = "PASS"
|
|
712
890
|
detail = ";".join("%s·%s" % (r["scene"], "+".join(r["metrics"]))
|
|
713
891
|
for r in pair["passed"])
|
|
714
892
|
warn = "[跨族仅 ΔB,慎判]" if (pair["a"], pair["b"]) in b_only else ""
|
|
715
893
|
head = "[%2d/%d] %-22s %-4s(%s)%s" % (
|
|
716
|
-
i, total, "%s vs %s" % (pair["a"], pair["b"]),
|
|
717
|
-
verdict, detail, warn)
|
|
894
|
+
i, total, "%s vs %s" % (pair["a"], pair["b"]), "PASS", detail, warn)
|
|
718
895
|
elif pair["status"] == "skip":
|
|
719
|
-
head = "[%2d/%d] %-22s SKIP(没有可比的帧)" % (
|
|
896
|
+
head = "[%2d/%d] %-22s SKIP(没有可比的帧)" % (
|
|
897
|
+
i, total, "%s vs %s" % (pair["a"], pair["b"]))
|
|
720
898
|
else:
|
|
721
899
|
c = pair.get("closest") or {}
|
|
722
900
|
head = "[%2d/%d] %-22s FAIL(最接近 %s:Δ%s=%.3f/%.3f,完成度 %.0f%%)" % (
|
|
@@ -732,7 +910,7 @@ def print_human(res):
|
|
|
732
910
|
print("")
|
|
733
911
|
s = res["summary"]
|
|
734
912
|
print("== 汇总")
|
|
735
|
-
print("
|
|
913
|
+
print("设计对 %d:通过 %d,失败 %d,跳过 %d%s"
|
|
736
914
|
% (s["pairs"], s["pass"], s["fail"], s["skip"],
|
|
737
915
|
"(全部通过)" if s["all_pass"] else ""))
|
|
738
916
|
bf = s.get("by_family") or {}
|
|
@@ -754,7 +932,7 @@ def print_human(res):
|
|
|
754
932
|
if s["passed"]:
|
|
755
933
|
print("通过的对:")
|
|
756
934
|
for row in s["passed"]:
|
|
757
|
-
print(" %-
|
|
935
|
+
print(" %-32s vs %-32s %s"
|
|
758
936
|
% (row["a"], row["b"],
|
|
759
937
|
";".join("%s·%s" % (x["scene"], "+".join(x["metrics"]))
|
|
760
938
|
for x in row["passed"])))
|
|
@@ -763,12 +941,12 @@ def print_human(res):
|
|
|
763
941
|
for i, row in enumerate(s["failed"], 1):
|
|
764
942
|
c = row.get("closest") or {}
|
|
765
943
|
if c:
|
|
766
|
-
print(" %2d. %-
|
|
944
|
+
print(" %2d. %-32s vs %-32s 最接近 %s·Δ%s=%.3f(阈值 %.3f,完成度 %.0f%%)"
|
|
767
945
|
% (i, row["a"], row["b"], c.get("scene"), c.get("metric"),
|
|
768
946
|
c.get("delta", 0.0), c.get("need", 0.0),
|
|
769
947
|
100.0 * c.get("ratio", 0.0)))
|
|
770
948
|
else:
|
|
771
|
-
print(" %2d. %-
|
|
949
|
+
print(" %2d. %-32s vs %-32s 无可比指标" % (i, row["a"], row["b"]))
|
|
772
950
|
if s["skipped"]:
|
|
773
951
|
print("跳过的对(skipped != 通过):")
|
|
774
952
|
for row in s["skipped"]:
|
|
@@ -776,28 +954,10 @@ def print_human(res):
|
|
|
776
954
|
if res.get("render_errors"):
|
|
777
955
|
print("渲染失败帧:")
|
|
778
956
|
for e in res["render_errors"]:
|
|
779
|
-
print(" %s/%s/t=%s: %s" % (e["
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
# ---------------------------------------------------------------- CLI
|
|
783
|
-
def _parse_styles(ST, raw):
|
|
784
|
-
"""--styles:缺省 = stylekit.STYLE_IDS 全部 8 套(不含 legacy)。"""
|
|
785
|
-
if not str(raw or "").strip():
|
|
786
|
-
return list(ST.STYLE_IDS)
|
|
787
|
-
ids = []
|
|
788
|
-
for part in str(raw).replace(" ", "").split(","):
|
|
789
|
-
if not part:
|
|
790
|
-
continue
|
|
791
|
-
if part in ids:
|
|
792
|
-
raise SystemExit("--styles 里有重复风格 %r" % part)
|
|
793
|
-
if part not in getattr(ST, "STYLES", {}):
|
|
794
|
-
raise SystemExit("未知风格 %r;可用:%s" % (part, " / ".join(ST.ALL_STYLE_IDS)))
|
|
795
|
-
ids.append(part)
|
|
796
|
-
if not ids:
|
|
797
|
-
raise SystemExit("--styles 里没有有效风格")
|
|
798
|
-
return ids
|
|
957
|
+
print(" %s/%s/t=%s: %s" % (e["design"], e["scene"], e["frac"], e["error"]))
|
|
799
958
|
|
|
800
959
|
|
|
960
|
+
# ---------------------------------------------------------------- 参数解析
|
|
801
961
|
def _parse_scene_types(raw):
|
|
802
962
|
"""--scenes:场景类型列表;默认 hero,list,stat(3 类,够用且快)。"""
|
|
803
963
|
if not str(raw or "").strip():
|
|
@@ -836,17 +996,178 @@ def _parse_frames(raw):
|
|
|
836
996
|
return out
|
|
837
997
|
|
|
838
998
|
|
|
999
|
+
def _seed_arg(raw):
|
|
1000
|
+
"""--seed:固定整数 = 可复现;-1 / random / auto = 每次新设计。"""
|
|
1001
|
+
s = str(raw).strip().lower()
|
|
1002
|
+
if s in ("-1", "random", "auto", "none"):
|
|
1003
|
+
return None
|
|
1004
|
+
try:
|
|
1005
|
+
v = int(s)
|
|
1006
|
+
except ValueError:
|
|
1007
|
+
raise SystemExit("--seed 必须是整数,或 -1/random 表示每次新设计;收到 %r" % raw)
|
|
1008
|
+
if v < 0:
|
|
1009
|
+
return None
|
|
1010
|
+
return v
|
|
1011
|
+
|
|
1012
|
+
|
|
839
1013
|
def _json_bytes(data):
|
|
840
1014
|
"""确定性 JSON 字节:固定缩进、不带时间戳(基线可 diff、可提交)。"""
|
|
841
1015
|
return (json.dumps(data, ensure_ascii=False, indent=1) + "\n").encode("utf-8")
|
|
842
1016
|
|
|
843
1017
|
|
|
1018
|
+
def _load_json(path):
|
|
1019
|
+
path = os.path.abspath(str(path))
|
|
1020
|
+
if not os.path.isfile(path):
|
|
1021
|
+
raise SystemExit("文件不存在:%s" % path)
|
|
1022
|
+
try:
|
|
1023
|
+
with open(path, encoding="utf-8") as fh:
|
|
1024
|
+
return json.load(fh)
|
|
1025
|
+
except ValueError as e:
|
|
1026
|
+
raise SystemExit("%s 不是合法 JSON:%s" % (path, e)) from e
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
# ---------------------------------------------------------------- 自检
|
|
1030
|
+
# 自检用的采样种子:
|
|
1031
|
+
# SELFTEST_SEED_GAP 第一个采样对的种子(确定性检查用这一个设计);
|
|
1032
|
+
# SELFTEST_SEED_SCAN 正控制在 seed 1..N 里找第一对可区分的设计(不写死单个 seed)。
|
|
1033
|
+
# 为什么不写死"seed=4 一定过":designkit 仍在演进,实测同一个 seed 的可区分结论会在
|
|
1034
|
+
# 改动后翻转;写死会让 selftest 变成"测试采样器哈希",而不是"测试门禁能不能分辨"。
|
|
1035
|
+
SELFTEST_SEED_GAP = 4
|
|
1036
|
+
SELFTEST_SEED_SCAN = 8
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _selftest_render(DK, scenes_mod, scene_types, fracs, design, tag):
|
|
1040
|
+
"""渲一组帧(selftest 用;返回 {scene: [rec, ...]},失败直接抛)。"""
|
|
1041
|
+
plan = design_plan(DK, design)
|
|
1042
|
+
script = build_script(tag, scene_types)
|
|
1043
|
+
script["_style"] = plan
|
|
1044
|
+
layout_default = str(plan.get("layout_default") or "left-stack")
|
|
1045
|
+
out = {}
|
|
1046
|
+
for scene in build_scenes(scene_types, layout_default):
|
|
1047
|
+
recs = []
|
|
1048
|
+
for frac in fracs:
|
|
1049
|
+
img, frame = render_frame(scenes_mod, script, scene, frac)
|
|
1050
|
+
recs.append({"frac": round(float(frac), 4), "frame": int(frame),
|
|
1051
|
+
"metrics": measure(img)[0],
|
|
1052
|
+
"sha256": _sha256_image(img), "error": None})
|
|
1053
|
+
out[str(scene.get("type"))] = recs
|
|
1054
|
+
return out
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def selftest(scripts_dir=SCRIPTS, scenes_py=DEFAULT_SCENES_PY):
|
|
1058
|
+
"""自检:现场采样 2 组设计 + 渲染(离线、不联网、<=60s),断言三件事。
|
|
1059
|
+
|
|
1060
|
+
1. **同一设计重复渲染逐像素一致**(确定性):同进程渲两遍,9 帧 sha256 全等;
|
|
1061
|
+
2. **不同设计之间可区分**(正控制):seed 1..SELFTEST_SEED_SCAN 里找第一对
|
|
1062
|
+
门禁判 PASS 的采样设计 —— 找得到才算"不同片画面结构可区分";
|
|
1063
|
+
3. **同一设计自己跟自己比必须 FAIL**(负控制):否则门禁恒真,等于没门禁。
|
|
1064
|
+
|
|
1065
|
+
为什么用"同一 brief 的两个 seed"而不是两份不同 brief:门禁要回答的是"同一个采样器
|
|
1066
|
+
产出的两个设计像不像"——不同 brief 会混进题材差异,不是本门禁的判据。
|
|
1067
|
+
为什么正控制扫 seed 而不写死 seed:designkit 仍在演进,固定 seed 的结论会随采样器
|
|
1068
|
+
改动漂移;扫描找得到任意一对可区分即可证明"采样器产出的设计在骨架上确实有差异",
|
|
1069
|
+
而全扫完都不行正该 FAIL(绝不靠放宽阈值让自检变绿)。
|
|
1070
|
+
|
|
1071
|
+
返回 (ok, report);report 是可直接 --json 的 dict。耗时实测 ~1.2s(首对命中即停),
|
|
1072
|
+
最坏(8 个 seed 全失败)约 10s,远低于 60s 上限。
|
|
1073
|
+
"""
|
|
1074
|
+
t0 = time.perf_counter()
|
|
1075
|
+
_K, _ST, DK = _modules(scripts_dir)
|
|
1076
|
+
scenes_mod = _load_scenes(scenes_py)
|
|
1077
|
+
scene_types = list(DEFAULT_SCENE_TYPES)
|
|
1078
|
+
fracs = list(DEFAULT_FRACS)
|
|
1079
|
+
checks = []
|
|
1080
|
+
try:
|
|
1081
|
+
# 1) 确定性:同一设计渲两遍(同进程两次渲染,逐像素 sha256 必须相等)
|
|
1082
|
+
a = sample_designs(DK, 2, SELFTEST_SEED_GAP)[0]
|
|
1083
|
+
ra1 = _selftest_render(DK, scenes_mod, scene_types, fracs, a, "st-a-1")
|
|
1084
|
+
ra2 = _selftest_render(DK, scenes_mod, scene_types, fracs, a, "st-a-1")
|
|
1085
|
+
det_bad = []
|
|
1086
|
+
for stype in scene_types:
|
|
1087
|
+
for r1, r2 in zip(ra1[stype], ra2[stype]):
|
|
1088
|
+
if r1["sha256"] != r2["sha256"]:
|
|
1089
|
+
det_bad.append("%s@%s(%s!=%s)" % (stype, r1["frac"],
|
|
1090
|
+
r1["sha256"][:8], r2["sha256"][:8]))
|
|
1091
|
+
elif r1["metrics"] != r2["metrics"]:
|
|
1092
|
+
det_bad.append("%s@%s 指标不一致" % (stype, r1["frac"]))
|
|
1093
|
+
checks.append({"name": "同一设计重复渲染逐像素一致(确定性)",
|
|
1094
|
+
"ok": not det_bad,
|
|
1095
|
+
"detail": ("%d 帧 sha256 全等" % sum(len(v) for v in ra1.values())
|
|
1096
|
+
if not det_bad else ";".join(det_bad[:4]))})
|
|
1097
|
+
# 2) 正控制:两个不同设计必须可区分。
|
|
1098
|
+
# 不写死"seed=N 一定过":designkit 仍在演进,硬编码一个 seed 会让 selftest 随
|
|
1099
|
+
# 采样器改动漂移(例如 v2.0.0 开发期 seed=3/4 的结论在半天内就翻转了两次)。
|
|
1100
|
+
# 改为扫 seed 1..8:第一对可区分的就作为正控制证据;全扫完都不行 = 采样器产出的
|
|
1101
|
+
# 设计在灰度骨架上确实雷同 —— 那正该 FAIL,不许悄悄放宽。
|
|
1102
|
+
fwd = None
|
|
1103
|
+
for seed in range(1, SELFTEST_SEED_SCAN + 1):
|
|
1104
|
+
pair = sample_designs(DK, 2, seed)
|
|
1105
|
+
x, y = pair[0], pair[1]
|
|
1106
|
+
rx = _selftest_render(DK, scenes_mod, scene_types, fracs, x, "st-fwd-%d-a" % seed)
|
|
1107
|
+
ry = _selftest_render(DK, scenes_mod, scene_types, fracs, y, "st-fwd-%d-b" % seed)
|
|
1108
|
+
pair_xy = compare_pair({"A": {"scenes": rx}, "B": {"scenes": ry}},
|
|
1109
|
+
"A", "B", scene_types)
|
|
1110
|
+
if pair_xy["status"] == "pass":
|
|
1111
|
+
fwd = (seed, pair_xy)
|
|
1112
|
+
break
|
|
1113
|
+
checks.append({"name": "不同设计之间可区分(正控制)",
|
|
1114
|
+
"ok": fwd is not None,
|
|
1115
|
+
"detail": ("seed=%d PASS:%s" % (fwd[0], ";".join(
|
|
1116
|
+
"%s·%s" % (r["scene"], "+".join(r["metrics"]))
|
|
1117
|
+
for r in fwd[1]["passed"]))
|
|
1118
|
+
if fwd is not None else
|
|
1119
|
+
"seed 1..%d 全部不可区分(采样器入场设计骨架雷同)"
|
|
1120
|
+
% SELFTEST_SEED_SCAN)})
|
|
1121
|
+
# 3) 负控制:同一设计自己跟自己比必须 FAIL(否则门禁恒真,等于没门禁)
|
|
1122
|
+
render_self = {"A": {"scenes": ra1}, "A2": {"scenes": ra2}}
|
|
1123
|
+
pair_aa = compare_pair(render_self, "A", "A2", scene_types)
|
|
1124
|
+
checks.append({"name": "同一设计自比必须不可区分(负控制)",
|
|
1125
|
+
"ok": pair_aa["status"] == "fail",
|
|
1126
|
+
"detail": ("FAIL(符合预期:Δ 全为 0)"
|
|
1127
|
+
if pair_aa["status"] == "fail" else
|
|
1128
|
+
"PASS(不符合预期:门禁把同一设计判成可区分,判据有 bug)")})
|
|
1129
|
+
except Exception as e: # noqa: BLE001
|
|
1130
|
+
checks.append({"name": "自检执行", "ok": False,
|
|
1131
|
+
"detail": "%s: %s" % (type(e).__name__, e)})
|
|
1132
|
+
traceback.print_exc()
|
|
1133
|
+
elapsed = round(time.perf_counter() - t0, 3)
|
|
1134
|
+
ok = bool(checks) and all(c["ok"] for c in checks)
|
|
1135
|
+
report = {"version": RESULT_VERSION, "generator": "silhouette_qa.py %s" % __version__,
|
|
1136
|
+
"selftest": True, "ok": ok, "elapsed_s": elapsed,
|
|
1137
|
+
"checks": checks,
|
|
1138
|
+
"inputs": {"scenes_py": _rel(scenes_py),
|
|
1139
|
+
"scenes_sha1": _sha1_file(scenes_py),
|
|
1140
|
+
"designkit_sha1": _sha1_file(os.path.join(scripts_dir, "designkit.py")),
|
|
1141
|
+
"tool_sha1": _sha1_file(os.path.abspath(__file__))}}
|
|
1142
|
+
return ok, report
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def print_selftest(report):
|
|
1146
|
+
print("== silhouette_qa --selftest(G13 自检:造 2 组设计现场验证判据)")
|
|
1147
|
+
for c in report["checks"]:
|
|
1148
|
+
print(" [%s] %s — %s" % ("PASS" if c["ok"] else "FAIL", c["name"], c["detail"]))
|
|
1149
|
+
print("自检 %s(用时 %.2fs)" % ("PASS" if report["ok"] else "FAIL",
|
|
1150
|
+
report.get("elapsed_s") or 0.0))
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# ---------------------------------------------------------------- CLI
|
|
844
1154
|
def main(argv=None):
|
|
845
1155
|
_setup_stdio()
|
|
846
1156
|
ap = argparse.ArgumentParser(
|
|
847
|
-
description="
|
|
848
|
-
|
|
849
|
-
|
|
1157
|
+
description="跨设计剪影可区分门禁 G13:同一内容换设计(designkit 现场采样)渲帧,"
|
|
1158
|
+
"三值化后比结构指标。没有 --styles:技能不提供内置"
|
|
1159
|
+
"风格表(stylekit.STYLE_IDS 为空),输入改为现场采样的设计。",
|
|
1160
|
+
epilog="例:python tools/silhouette_qa.py --designs 4 --json;"
|
|
1161
|
+
"python tools/silhouette_qa.py --selftest")
|
|
1162
|
+
ap.add_argument("--designs", type=int, default=DEFAULT_DESIGNS,
|
|
1163
|
+
help="现场采样几个设计两两比较(>=2,缺省 %d)" % DEFAULT_DESIGNS)
|
|
1164
|
+
ap.add_argument("--seed", default=str(DEFAULT_SEED),
|
|
1165
|
+
help="采样种子:固定整数 = 可复现(缺省 %d);-1/random = 每次新设计"
|
|
1166
|
+
% DEFAULT_SEED)
|
|
1167
|
+
ap.add_argument("--brief", default="",
|
|
1168
|
+
help="采样用的 brief 文本(缺省内置一句话题材;不联网)")
|
|
1169
|
+
ap.add_argument("--design-json", default="",
|
|
1170
|
+
help="诊断用:直接读设计文件(单设计/数组/{designs:[...]}),不再采样")
|
|
850
1171
|
ap.add_argument("--scenes", default="",
|
|
851
1172
|
help="逗号分隔场景类型(缺省 hero,list,stat)")
|
|
852
1173
|
ap.add_argument("--frames", default="",
|
|
@@ -860,24 +1181,55 @@ def main(argv=None):
|
|
|
860
1181
|
ap.add_argument("--strict", action="store_true",
|
|
861
1182
|
help="存在跳过(渲不出/缺值)时退出码 2;跳过绝不写成通过")
|
|
862
1183
|
ap.add_argument("--out", default="", help="额外把 JSON 写到该路径(缺省不写文件)")
|
|
1184
|
+
ap.add_argument("--selftest", action="store_true",
|
|
1185
|
+
help="自检:合成 2 组设计 + 渲染(离线 <=60s),验证判据可用且非恒真")
|
|
863
1186
|
a = ap.parse_args(argv)
|
|
1187
|
+
if a.selftest:
|
|
1188
|
+
try:
|
|
1189
|
+
skill_root = os.path.abspath(a.skill_root) if a.skill_root else SKILL
|
|
1190
|
+
scripts_dir = os.path.join(skill_root, "scripts")
|
|
1191
|
+
scenes_py = (os.path.abspath(a.scenes_py) if a.scenes_py
|
|
1192
|
+
else os.path.join(skill_root, "assets", "scenes.py"))
|
|
1193
|
+
ok, report = selftest(scripts_dir, scenes_py)
|
|
1194
|
+
except SystemExit:
|
|
1195
|
+
raise
|
|
1196
|
+
except Exception as e: # noqa: BLE001
|
|
1197
|
+
sys.stderr.write("silhouette_qa --selftest 运行失败:%s: %s\n"
|
|
1198
|
+
% (type(e).__name__, e))
|
|
1199
|
+
sys.stderr.write(traceback.format_exc()[-2000:])
|
|
1200
|
+
return 1
|
|
1201
|
+
if a.json:
|
|
1202
|
+
sys.stdout.write(_json_bytes(report).decode("utf-8"))
|
|
1203
|
+
sys.stdout.flush()
|
|
1204
|
+
else:
|
|
1205
|
+
print_selftest(report)
|
|
1206
|
+
return 0 if ok else 1
|
|
864
1207
|
try:
|
|
865
1208
|
skill_root = os.path.abspath(a.skill_root) if a.skill_root else SKILL
|
|
866
1209
|
scripts_dir = os.path.join(skill_root, "scripts")
|
|
867
1210
|
scenes_py = (os.path.abspath(a.scenes_py) if a.scenes_py
|
|
868
1211
|
else os.path.join(skill_root, "assets", "scenes.py"))
|
|
869
|
-
_K,
|
|
870
|
-
styles = _parse_styles(ST, a.styles)
|
|
1212
|
+
_K, _ST, DK = _modules(scripts_dir)
|
|
871
1213
|
scene_types = _parse_scene_types(a.scenes)
|
|
872
1214
|
fracs = _parse_frames(a.frames)
|
|
873
|
-
if
|
|
874
|
-
|
|
875
|
-
|
|
1215
|
+
if a.design_json:
|
|
1216
|
+
designs = _normalize_designs(_load_json(a.design_json))
|
|
1217
|
+
if len(designs) == 1:
|
|
1218
|
+
raise SystemExit("--design-json 只给了 1 个设计:两两比较至少需要 2 个设计"
|
|
1219
|
+
"(要验证确定性请用 --selftest)")
|
|
1220
|
+
else:
|
|
1221
|
+
if a.designs < 2:
|
|
1222
|
+
raise SystemExit("--designs 必须 >= 2(两两比较),收到 %d" % a.designs)
|
|
1223
|
+
seed = _seed_arg(a.seed)
|
|
1224
|
+
designs = sample_designs(DK, a.designs, seed, a.brief or DEFAULT_BRIEF)
|
|
1225
|
+
ids = _design_ids(designs)
|
|
1226
|
+
res, elapsed = run_gate(scenes_py, designs, scene_types, fracs, FPS,
|
|
1227
|
+
scripts_dir, design_ids=ids)
|
|
876
1228
|
except SystemExit:
|
|
877
1229
|
raise
|
|
878
1230
|
except Exception as e: # noqa: BLE001
|
|
879
1231
|
# 装载/全局性失败是**工具跑不成**,不是判 FAIL:明确报错并按失败退出,
|
|
880
|
-
# 免得 CI 把"工具没跑起来"当成"
|
|
1232
|
+
# 免得 CI 把"工具没跑起来"当成"设计不可区分"去修构图。
|
|
881
1233
|
sys.stderr.write("silhouette_qa 运行失败:%s: %s\n" % (type(e).__name__, e))
|
|
882
1234
|
sys.stderr.write(traceback.format_exc()[-2000:])
|
|
883
1235
|
return 1
|
|
@@ -894,8 +1246,8 @@ def main(argv=None):
|
|
|
894
1246
|
sys.stdout.flush()
|
|
895
1247
|
else:
|
|
896
1248
|
print_human(res)
|
|
897
|
-
print("渲染 %d
|
|
898
|
-
% (len(
|
|
1249
|
+
print("渲染 %d 设计 × %d 场景 × %d 帧,用时 %.2fs"
|
|
1250
|
+
% (len(ids), len(scene_types), len(fracs), elapsed))
|
|
899
1251
|
if a.out:
|
|
900
1252
|
print("JSON 已写入:%s" % os.path.abspath(a.out))
|
|
901
1253
|
s = res["summary"]
|