@topmindspace/tms-skills 2.0.1 → 2.1.0
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 +145 -134
- package/README.md +1 -1
- package/bin/tms-skills.js +160 -160
- package/package.json +48 -48
- package/top-ppt-html/README.md +254 -254
- package/top-ppt-html/SKILL.md +111 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
- package/top-ppt-html/assets/pptx-export.js +1944 -1944
- package/top-ppt-html/assets/style-gallery.html +589 -589
- package/top-ppt-html/assets/templates/architecture.html +3728 -3728
- package/top-ppt-html/assets/templates/engine.css +840 -840
- package/top-ppt-html/assets/templates/presentation.html +3738 -3738
- package/top-ppt-html/assets/templates/research.html +4017 -4017
- package/top-ppt-html/assets/templates/ui.js +520 -520
- package/top-ppt-html/evals/prompts.csv +15 -15
- package/top-ppt-html/evals/rubric.schema.json +25 -25
- package/top-ppt-html/evals/run_evals.py +220 -220
- package/top-ppt-html/evals/trace.example.json +16 -16
- package/top-ppt-html/package-lock.json +207 -0
- package/top-ppt-html/package.json +35 -35
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +108 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +28 -28
- package/top-ppt-html/references/components-atoms.md +624 -624
- package/top-ppt-html/references/components.md +30 -30
- package/top-ppt-html/references/content-rules.md +490 -490
- package/top-ppt-html/references/design-system-engine.md +235 -235
- package/top-ppt-html/references/design-system.md +478 -478
- package/top-ppt-html/references/failure-modes.md +214 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +397 -397
- package/top-ppt-html/references/industry-benchmark.md +105 -105
- package/top-ppt-html/references/infographics-stats.md +308 -308
- package/top-ppt-html/references/infographics-structure.md +226 -226
- package/top-ppt-html/references/infographics.md +43 -43
- package/top-ppt-html/references/layout-grammar.md +315 -315
- package/top-ppt-html/references/layouts-architecture.md +108 -108
- package/top-ppt-html/references/layouts-combo.md +600 -600
- package/top-ppt-html/references/layouts-research.md +160 -160
- package/top-ppt-html/references/modes.md +254 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +266 -266
- package/top-ppt-html/references/pptx-export.md +209 -209
- package/top-ppt-html/references/reform-plan.md +252 -252
- package/top-ppt-html/references/styles.md +370 -370
- package/top-ppt-html/references/tech-design.md +138 -138
- package/top-ppt-html/scripts/audit_css.py +109 -109
- package/top-ppt-html/scripts/audit_docs.py +176 -176
- package/top-ppt-html/scripts/audit_skill.py +220 -220
- package/top-ppt-html/scripts/audit_styles.py +351 -351
- package/top-ppt-html/scripts/build_examples.py +2276 -2276
- package/top-ppt-html/scripts/build_pptx.js +2380 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
- package/top-ppt-html/scripts/checks_html.py +127 -127
- package/top-ppt-html/scripts/cross_verify.py +294 -294
- package/top-ppt-html/scripts/env_probe.py +158 -158
- package/top-ppt-html/scripts/extract_model.py +210 -210
- package/top-ppt-html/scripts/extract_snippet.py +374 -374
- package/top-ppt-html/scripts/gen_channel_a.js +214 -214
- package/top-ppt-html/scripts/layout-constants.json +3377 -3377
- package/top-ppt-html/scripts/layout_slots.json +830 -830
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/model-schema.json +547 -547
- package/top-ppt-html/scripts/negative_tests.py +307 -307
- package/top-ppt-html/scripts/package_skill.py +291 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +188 -188
- package/top-ppt-html/scripts/quality_gate.py +301 -301
- package/top-ppt-html/scripts/regression.py +308 -308
- package/top-ppt-html/scripts/render_compare.py +275 -275
- package/top-ppt-html/scripts/render_from_model.py +698 -698
- package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
- package/top-ppt-html/scripts/section-file-map.json +104 -104
- package/top-ppt-html/scripts/sync_runtime.py +662 -662
- package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
- package/top-ppt-html/scripts/validate_report.py +1456 -1456
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""TopPPT HTML · 素材图片导出探针(维护工具,不进示例矩阵)
|
|
4
|
-
|
|
5
|
-
为什么单列探针:示例矩阵刻意保持「零图片全原生」基线(`pictures=0`),真实素材图片
|
|
6
|
-
(用户提供的位图)不在示例里长期携带。但图片路径必须被回归覆盖,否则「相对路径解析 /
|
|
7
|
-
pictures 声明式放行 / half·grid 版式几何 / 裁切策略」这类改动会静默退化。
|
|
8
|
-
|
|
9
|
-
本探针用 assets/ 下已有的主题总览图当素材,构造一个最小模型:
|
|
10
|
-
· image.layout="half" + fit="contain"(左图右注,完整显示不裁切)
|
|
11
|
-
· image.layout="grid" + fit="cover"(四图网格,裁切填满)
|
|
12
|
-
然后跑 build_pptx.js → validate_pptx.py --strict --model,断言 0/0 且 pictures == 声明数。
|
|
13
|
-
|
|
14
|
-
用法:
|
|
15
|
-
python scripts/probe_image_export.py # 用内置素材
|
|
16
|
-
python scripts/probe_image_export.py <图片路径> # 用指定素材(相对模型目录)
|
|
17
|
-
|
|
18
|
-
环境:需要 Node + pptxgenjs(自动探测 TOP_PPT_NODE_PATH / NODE_PATH / 常见托管目录)。
|
|
19
|
-
"""
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import json
|
|
23
|
-
import os
|
|
24
|
-
import subprocess
|
|
25
|
-
import sys
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
30
|
-
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
31
|
-
except Exception:
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
35
|
-
from env_probe import find_node, find_node_modules, node_env # noqa: E402
|
|
36
|
-
|
|
37
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
38
|
-
OUT = ROOT / 'dist' / 'probe'
|
|
39
|
-
PY = sys.executable
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def main() -> int:
|
|
43
|
-
node, node_path = find_node(), find_node_modules()
|
|
44
|
-
if not node or not node_path:
|
|
45
|
-
print('SKIP:未找到 Node / pptxgenjs(本探针为维护工具,可跳过)。')
|
|
46
|
-
return 0
|
|
47
|
-
|
|
48
|
-
assets = ROOT / 'assets'
|
|
49
|
-
picks = [Path(a) for a in sys.argv[1:]]
|
|
50
|
-
if not picks:
|
|
51
|
-
picks = [assets / 'theme-overview.png',
|
|
52
|
-
assets / 'theme-overview-presentation.png',
|
|
53
|
-
assets / 'theme-overview-research.png',
|
|
54
|
-
assets / 'theme-overview-architecture.png']
|
|
55
|
-
missing = [str(p) for p in picks if not p.exists()]
|
|
56
|
-
if missing:
|
|
57
|
-
print(f'错误:素材缺失 {missing}')
|
|
58
|
-
return 2
|
|
59
|
-
|
|
60
|
-
OUT.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
model_path = OUT / 'image-probe.model.json'
|
|
62
|
-
pptx_path = OUT / 'image-probe.pptx'
|
|
63
|
-
# 相对路径以「模型目录」为锚(与 build_pptx.js 的 resolveImagePath 同规则)
|
|
64
|
-
rel = [os.path.relpath(p, OUT).replace('\\', '/') for p in picks]
|
|
65
|
-
grid = (rel * 4)[:4]
|
|
66
|
-
|
|
67
|
-
model = {
|
|
68
|
-
"mode": "presentation", "style": "business-blue", "theme": "light",
|
|
69
|
-
"title": "素材图片导出探针:双通道落地与门禁",
|
|
70
|
-
"subtitle": "half / grid 版式 · cover / contain 裁切 · 声明式放行",
|
|
71
|
-
"meta": "维护工具 · 不进示例矩阵",
|
|
72
|
-
"agenda": [["01", "图版", "半幅与网格"], ["02", "门禁", "声明式放行"]],
|
|
73
|
-
"sections": [
|
|
74
|
-
{"type": "image", "eyebrow": "01 · 图版",
|
|
75
|
-
"title": "半幅图加右栏注解:左图右注的图文互证版式",
|
|
76
|
-
"lead": "左侧锁定 4:3 素材图,右侧逐条给出读图要点。",
|
|
77
|
-
"image": {"src": rel[0], "layout": "half", "fit": "contain",
|
|
78
|
-
"caption": "图 1:主题总览(contain 完整显示,不裁切)"},
|
|
79
|
-
"points": [["版式", "左图右注,图宽占版心 56%,右侧逐条注解"],
|
|
80
|
-
["裁切", "contain 完整显示不裁切;cover 裁切填满不留白"],
|
|
81
|
-
["比例", "half 版式锁定 4:3,与 PPTX 几何同源"],
|
|
82
|
-
["占位", "无图时同一几何出原生配图占位,交付前替换 src"]]},
|
|
83
|
-
{"type": "image", "eyebrow": "01 · 图版",
|
|
84
|
-
"title": "四图网格:同一套护栏下的四类业务现场互证",
|
|
85
|
-
"lead": "四张等比例素材并排,图下小图注标明来源场景。",
|
|
86
|
-
"image": {"layout": "grid", "fit": "cover",
|
|
87
|
-
"caption": "图 2–5:四类业务现场(建议 900×675px · 4:3)",
|
|
88
|
-
"items": [{"src": grid[0], "caption": "演示模式总览:封面与要点页节奏"},
|
|
89
|
-
{"src": grid[1], "caption": "汇报模式:大数字与图表页对照"},
|
|
90
|
-
{"src": grid[2], "caption": "研究模式:密排证据与 Exhibit 编号"},
|
|
91
|
-
{"src": grid[3], "caption": "架构模式:全幅分层与泳道图"}]}},
|
|
92
|
-
{"type": "image", "eyebrow": "02 · 门禁",
|
|
93
|
-
"title": "配图占位:无素材时锁版式,交付前替换 src",
|
|
94
|
-
"lead": "placeholder 出原生圆角框,pictures 不增。",
|
|
95
|
-
"image": {"placeholder": True, "layout": "full",
|
|
96
|
-
"caption": "占位:建议 2400×800px · 3:1"},
|
|
97
|
-
"note": "占位不算图片,不触发 PICTURES_* 门禁"},
|
|
98
|
-
],
|
|
99
|
-
"closing": {"title": "探针结束:图片路径与门禁已被覆盖",
|
|
100
|
-
"points": [["路径", "相对模型目录解析,跨目录调用不失效"],
|
|
101
|
-
["门禁", "pictures 数不得超过声明数"],
|
|
102
|
-
["回落", "图片不可读时回落原生占位框,不留空洞"]]},
|
|
103
|
-
}
|
|
104
|
-
model_path.write_text(json.dumps(model, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
105
|
-
# 真实位图声明数(half 1 + grid 4);占位不计入 pictures
|
|
106
|
-
declared = 1 + len(model['sections'][1]['image']['items'])
|
|
107
|
-
|
|
108
|
-
# 第二段:用户素材路径缺失 → build 不中断 + strict 报 IMAGE_SRC_MISSING + pictures 不虚增
|
|
109
|
-
miss_model = dict(model)
|
|
110
|
-
miss_model['sections'] = list(model['sections']) + [{
|
|
111
|
-
"type": "image", "eyebrow": "02 · 门禁",
|
|
112
|
-
"title": "相对路径缺失时回落占位,不留空洞",
|
|
113
|
-
"lead": "IMAGE_SRC_MISSING 应回落原生占位框。",
|
|
114
|
-
"image": {"src": "missing-user-photo.jpg", "layout": "half", "fit": "cover",
|
|
115
|
-
"caption": "用户素材(路径应以模型目录为锚)"},
|
|
116
|
-
"note": "文件缺失回落占位,交付前修正路径",
|
|
117
|
-
}]
|
|
118
|
-
miss_model_path = OUT / 'image-probe-missing.model.json'
|
|
119
|
-
miss_pptx_path = OUT / 'image-probe-missing.pptx'
|
|
120
|
-
miss_model_path.write_text(json.dumps(miss_model, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
121
|
-
|
|
122
|
-
env = node_env(node_path)
|
|
123
|
-
r = subprocess.run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(pptx_path),
|
|
124
|
-
'--model=' + str(model_path)],
|
|
125
|
-
capture_output=True, text=True, encoding='utf-8', env=env)
|
|
126
|
-
if r.returncode != 0 or not pptx_path.exists():
|
|
127
|
-
print('[FAIL] build_pptx 失败')
|
|
128
|
-
print((r.stderr or r.stdout or '')[-900:])
|
|
129
|
-
return 1
|
|
130
|
-
print(f'[OK] build_pptx 生成 {pptx_path.name}(声明图片 {declared} 张)')
|
|
131
|
-
|
|
132
|
-
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(pptx_path),
|
|
133
|
-
'--strict', '--model=' + str(model_path)],
|
|
134
|
-
capture_output=True, text=True, encoding='utf-8')
|
|
135
|
-
report = {}
|
|
136
|
-
try:
|
|
137
|
-
report = json.loads(r.stdout or '{}')
|
|
138
|
-
except json.JSONDecodeError:
|
|
139
|
-
print('[FAIL] 无法解析 validate_pptx 报告')
|
|
140
|
-
print((r.stdout or '')[-900:])
|
|
141
|
-
return 1
|
|
142
|
-
pics = int((report.get('summary') or {}).get('pictures', 0))
|
|
143
|
-
errors = report.get('errors') or []
|
|
144
|
-
warnings = report.get('warnings') or []
|
|
145
|
-
print(f'[{"OK" if r.returncode == 0 else "FAIL"}] strict: '
|
|
146
|
-
f'{len(errors)} errors / {len(warnings)} warnings · pictures={pics}/{declared}')
|
|
147
|
-
for e in errors[:6]:
|
|
148
|
-
print(' E', e.get('code'), str(e.get('message'))[:140])
|
|
149
|
-
for w in warnings[:6]:
|
|
150
|
-
print(' W', w.get('code'), str(w.get('message'))[:140])
|
|
151
|
-
if r.returncode != 0:
|
|
152
|
-
return 1
|
|
153
|
-
if pics != declared:
|
|
154
|
-
print(f'[FAIL] pictures={pics} != 声明数 {declared}(声明式放行门禁失效)')
|
|
155
|
-
return 1
|
|
156
|
-
|
|
157
|
-
# ── 缺失路径探针:build 成功 + strict 抓 IMAGE_SRC_MISSING + pictures 不虚增 ──
|
|
158
|
-
r = subprocess.run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(miss_pptx_path),
|
|
159
|
-
'--model=' + str(miss_model_path)],
|
|
160
|
-
capture_output=True, text=True, encoding='utf-8', env=env)
|
|
161
|
-
if r.returncode != 0 or not miss_pptx_path.exists():
|
|
162
|
-
print('[FAIL] 缺失路径模型 build_pptx 应成功回落占位,却中断了')
|
|
163
|
-
print((r.stderr or r.stdout or '')[-600:])
|
|
164
|
-
return 1
|
|
165
|
-
print('[OK] 缺失路径 build 成功(已回落占位框,未中断交付)')
|
|
166
|
-
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(miss_pptx_path),
|
|
167
|
-
'--strict', '--model=' + str(miss_model_path)],
|
|
168
|
-
capture_output=True, text=True, encoding='utf-8')
|
|
169
|
-
try:
|
|
170
|
-
mreport = json.loads(r.stdout or '{}')
|
|
171
|
-
except json.JSONDecodeError:
|
|
172
|
-
print('[FAIL] 缺失路径 validate 报告不可解析')
|
|
173
|
-
return 1
|
|
174
|
-
mcodes = {(e or {}).get('code') for e in (mreport.get('errors') or [])}
|
|
175
|
-
mpics = int((mreport.get('summary') or {}).get('pictures', 0))
|
|
176
|
-
if 'IMAGE_SRC_MISSING' not in mcodes:
|
|
177
|
-
print(f'[FAIL] 缺失路径应报 IMAGE_SRC_MISSING,实际 errors={mcodes}')
|
|
178
|
-
return 1
|
|
179
|
-
if mpics != declared:
|
|
180
|
-
print(f'[FAIL] 缺失路径 pictures={mpics} != 有效声明 {declared}(缺失 src 不应计入图片数)')
|
|
181
|
-
return 1
|
|
182
|
-
print(f'[OK] 缺失路径 strict 抓到 IMAGE_SRC_MISSING · pictures={mpics}/{declared}')
|
|
183
|
-
print('图片导出探针通过:真实素材按声明数落地、占位不增图、缺失路径回落且被门禁拦下。')
|
|
184
|
-
return 0
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if __name__ == '__main__':
|
|
188
|
-
sys.exit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 素材图片导出探针(维护工具,不进示例矩阵)
|
|
4
|
+
|
|
5
|
+
为什么单列探针:示例矩阵刻意保持「零图片全原生」基线(`pictures=0`),真实素材图片
|
|
6
|
+
(用户提供的位图)不在示例里长期携带。但图片路径必须被回归覆盖,否则「相对路径解析 /
|
|
7
|
+
pictures 声明式放行 / half·grid 版式几何 / 裁切策略」这类改动会静默退化。
|
|
8
|
+
|
|
9
|
+
本探针用 assets/ 下已有的主题总览图当素材,构造一个最小模型:
|
|
10
|
+
· image.layout="half" + fit="contain"(左图右注,完整显示不裁切)
|
|
11
|
+
· image.layout="grid" + fit="cover"(四图网格,裁切填满)
|
|
12
|
+
然后跑 build_pptx.js → validate_pptx.py --strict --model,断言 0/0 且 pictures == 声明数。
|
|
13
|
+
|
|
14
|
+
用法:
|
|
15
|
+
python scripts/probe_image_export.py # 用内置素材
|
|
16
|
+
python scripts/probe_image_export.py <图片路径> # 用指定素材(相对模型目录)
|
|
17
|
+
|
|
18
|
+
环境:需要 Node + pptxgenjs(自动探测 TOP_PPT_NODE_PATH / NODE_PATH / 常见托管目录)。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
30
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
35
|
+
from env_probe import find_node, find_node_modules, node_env # noqa: E402
|
|
36
|
+
|
|
37
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
38
|
+
OUT = ROOT / 'dist' / 'probe'
|
|
39
|
+
PY = sys.executable
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
node, node_path = find_node(), find_node_modules()
|
|
44
|
+
if not node or not node_path:
|
|
45
|
+
print('SKIP:未找到 Node / pptxgenjs(本探针为维护工具,可跳过)。')
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
assets = ROOT / 'assets'
|
|
49
|
+
picks = [Path(a) for a in sys.argv[1:]]
|
|
50
|
+
if not picks:
|
|
51
|
+
picks = [assets / 'theme-overview.png',
|
|
52
|
+
assets / 'theme-overview-presentation.png',
|
|
53
|
+
assets / 'theme-overview-research.png',
|
|
54
|
+
assets / 'theme-overview-architecture.png']
|
|
55
|
+
missing = [str(p) for p in picks if not p.exists()]
|
|
56
|
+
if missing:
|
|
57
|
+
print(f'错误:素材缺失 {missing}')
|
|
58
|
+
return 2
|
|
59
|
+
|
|
60
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
model_path = OUT / 'image-probe.model.json'
|
|
62
|
+
pptx_path = OUT / 'image-probe.pptx'
|
|
63
|
+
# 相对路径以「模型目录」为锚(与 build_pptx.js 的 resolveImagePath 同规则)
|
|
64
|
+
rel = [os.path.relpath(p, OUT).replace('\\', '/') for p in picks]
|
|
65
|
+
grid = (rel * 4)[:4]
|
|
66
|
+
|
|
67
|
+
model = {
|
|
68
|
+
"mode": "presentation", "style": "business-blue", "theme": "light",
|
|
69
|
+
"title": "素材图片导出探针:双通道落地与门禁",
|
|
70
|
+
"subtitle": "half / grid 版式 · cover / contain 裁切 · 声明式放行",
|
|
71
|
+
"meta": "维护工具 · 不进示例矩阵",
|
|
72
|
+
"agenda": [["01", "图版", "半幅与网格"], ["02", "门禁", "声明式放行"]],
|
|
73
|
+
"sections": [
|
|
74
|
+
{"type": "image", "eyebrow": "01 · 图版",
|
|
75
|
+
"title": "半幅图加右栏注解:左图右注的图文互证版式",
|
|
76
|
+
"lead": "左侧锁定 4:3 素材图,右侧逐条给出读图要点。",
|
|
77
|
+
"image": {"src": rel[0], "layout": "half", "fit": "contain",
|
|
78
|
+
"caption": "图 1:主题总览(contain 完整显示,不裁切)"},
|
|
79
|
+
"points": [["版式", "左图右注,图宽占版心 56%,右侧逐条注解"],
|
|
80
|
+
["裁切", "contain 完整显示不裁切;cover 裁切填满不留白"],
|
|
81
|
+
["比例", "half 版式锁定 4:3,与 PPTX 几何同源"],
|
|
82
|
+
["占位", "无图时同一几何出原生配图占位,交付前替换 src"]]},
|
|
83
|
+
{"type": "image", "eyebrow": "01 · 图版",
|
|
84
|
+
"title": "四图网格:同一套护栏下的四类业务现场互证",
|
|
85
|
+
"lead": "四张等比例素材并排,图下小图注标明来源场景。",
|
|
86
|
+
"image": {"layout": "grid", "fit": "cover",
|
|
87
|
+
"caption": "图 2–5:四类业务现场(建议 900×675px · 4:3)",
|
|
88
|
+
"items": [{"src": grid[0], "caption": "演示模式总览:封面与要点页节奏"},
|
|
89
|
+
{"src": grid[1], "caption": "汇报模式:大数字与图表页对照"},
|
|
90
|
+
{"src": grid[2], "caption": "研究模式:密排证据与 Exhibit 编号"},
|
|
91
|
+
{"src": grid[3], "caption": "架构模式:全幅分层与泳道图"}]}},
|
|
92
|
+
{"type": "image", "eyebrow": "02 · 门禁",
|
|
93
|
+
"title": "配图占位:无素材时锁版式,交付前替换 src",
|
|
94
|
+
"lead": "placeholder 出原生圆角框,pictures 不增。",
|
|
95
|
+
"image": {"placeholder": True, "layout": "full",
|
|
96
|
+
"caption": "占位:建议 2400×800px · 3:1"},
|
|
97
|
+
"note": "占位不算图片,不触发 PICTURES_* 门禁"},
|
|
98
|
+
],
|
|
99
|
+
"closing": {"title": "探针结束:图片路径与门禁已被覆盖",
|
|
100
|
+
"points": [["路径", "相对模型目录解析,跨目录调用不失效"],
|
|
101
|
+
["门禁", "pictures 数不得超过声明数"],
|
|
102
|
+
["回落", "图片不可读时回落原生占位框,不留空洞"]]},
|
|
103
|
+
}
|
|
104
|
+
model_path.write_text(json.dumps(model, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
105
|
+
# 真实位图声明数(half 1 + grid 4);占位不计入 pictures
|
|
106
|
+
declared = 1 + len(model['sections'][1]['image']['items'])
|
|
107
|
+
|
|
108
|
+
# 第二段:用户素材路径缺失 → build 不中断 + strict 报 IMAGE_SRC_MISSING + pictures 不虚增
|
|
109
|
+
miss_model = dict(model)
|
|
110
|
+
miss_model['sections'] = list(model['sections']) + [{
|
|
111
|
+
"type": "image", "eyebrow": "02 · 门禁",
|
|
112
|
+
"title": "相对路径缺失时回落占位,不留空洞",
|
|
113
|
+
"lead": "IMAGE_SRC_MISSING 应回落原生占位框。",
|
|
114
|
+
"image": {"src": "missing-user-photo.jpg", "layout": "half", "fit": "cover",
|
|
115
|
+
"caption": "用户素材(路径应以模型目录为锚)"},
|
|
116
|
+
"note": "文件缺失回落占位,交付前修正路径",
|
|
117
|
+
}]
|
|
118
|
+
miss_model_path = OUT / 'image-probe-missing.model.json'
|
|
119
|
+
miss_pptx_path = OUT / 'image-probe-missing.pptx'
|
|
120
|
+
miss_model_path.write_text(json.dumps(miss_model, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
121
|
+
|
|
122
|
+
env = node_env(node_path)
|
|
123
|
+
r = subprocess.run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(pptx_path),
|
|
124
|
+
'--model=' + str(model_path)],
|
|
125
|
+
capture_output=True, text=True, encoding='utf-8', env=env)
|
|
126
|
+
if r.returncode != 0 or not pptx_path.exists():
|
|
127
|
+
print('[FAIL] build_pptx 失败')
|
|
128
|
+
print((r.stderr or r.stdout or '')[-900:])
|
|
129
|
+
return 1
|
|
130
|
+
print(f'[OK] build_pptx 生成 {pptx_path.name}(声明图片 {declared} 张)')
|
|
131
|
+
|
|
132
|
+
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(pptx_path),
|
|
133
|
+
'--strict', '--model=' + str(model_path)],
|
|
134
|
+
capture_output=True, text=True, encoding='utf-8')
|
|
135
|
+
report = {}
|
|
136
|
+
try:
|
|
137
|
+
report = json.loads(r.stdout or '{}')
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
print('[FAIL] 无法解析 validate_pptx 报告')
|
|
140
|
+
print((r.stdout or '')[-900:])
|
|
141
|
+
return 1
|
|
142
|
+
pics = int((report.get('summary') or {}).get('pictures', 0))
|
|
143
|
+
errors = report.get('errors') or []
|
|
144
|
+
warnings = report.get('warnings') or []
|
|
145
|
+
print(f'[{"OK" if r.returncode == 0 else "FAIL"}] strict: '
|
|
146
|
+
f'{len(errors)} errors / {len(warnings)} warnings · pictures={pics}/{declared}')
|
|
147
|
+
for e in errors[:6]:
|
|
148
|
+
print(' E', e.get('code'), str(e.get('message'))[:140])
|
|
149
|
+
for w in warnings[:6]:
|
|
150
|
+
print(' W', w.get('code'), str(w.get('message'))[:140])
|
|
151
|
+
if r.returncode != 0:
|
|
152
|
+
return 1
|
|
153
|
+
if pics != declared:
|
|
154
|
+
print(f'[FAIL] pictures={pics} != 声明数 {declared}(声明式放行门禁失效)')
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
# ── 缺失路径探针:build 成功 + strict 抓 IMAGE_SRC_MISSING + pictures 不虚增 ──
|
|
158
|
+
r = subprocess.run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(miss_pptx_path),
|
|
159
|
+
'--model=' + str(miss_model_path)],
|
|
160
|
+
capture_output=True, text=True, encoding='utf-8', env=env)
|
|
161
|
+
if r.returncode != 0 or not miss_pptx_path.exists():
|
|
162
|
+
print('[FAIL] 缺失路径模型 build_pptx 应成功回落占位,却中断了')
|
|
163
|
+
print((r.stderr or r.stdout or '')[-600:])
|
|
164
|
+
return 1
|
|
165
|
+
print('[OK] 缺失路径 build 成功(已回落占位框,未中断交付)')
|
|
166
|
+
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(miss_pptx_path),
|
|
167
|
+
'--strict', '--model=' + str(miss_model_path)],
|
|
168
|
+
capture_output=True, text=True, encoding='utf-8')
|
|
169
|
+
try:
|
|
170
|
+
mreport = json.loads(r.stdout or '{}')
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
print('[FAIL] 缺失路径 validate 报告不可解析')
|
|
173
|
+
return 1
|
|
174
|
+
mcodes = {(e or {}).get('code') for e in (mreport.get('errors') or [])}
|
|
175
|
+
mpics = int((mreport.get('summary') or {}).get('pictures', 0))
|
|
176
|
+
if 'IMAGE_SRC_MISSING' not in mcodes:
|
|
177
|
+
print(f'[FAIL] 缺失路径应报 IMAGE_SRC_MISSING,实际 errors={mcodes}')
|
|
178
|
+
return 1
|
|
179
|
+
if mpics != declared:
|
|
180
|
+
print(f'[FAIL] 缺失路径 pictures={mpics} != 有效声明 {declared}(缺失 src 不应计入图片数)')
|
|
181
|
+
return 1
|
|
182
|
+
print(f'[OK] 缺失路径 strict 抓到 IMAGE_SRC_MISSING · pictures={mpics}/{declared}')
|
|
183
|
+
print('图片导出探针通过:真实素材按声明数落地、占位不增图、缺失路径回落且被门禁拦下。')
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == '__main__':
|
|
188
|
+
sys.exit(main())
|