@topmindspace/tms-skills 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +9 -3
- package/bin/tms-skills.js +200 -160
- package/package.json +12 -9
- package/top-ppt-html/README.md +162 -254
- package/top-ppt-html/SKILL.md +126 -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-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-mckinsey.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.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/assets/theme-overview-architecture.png +0 -0
- package/top-ppt-html/assets/theme-overview-research.png +0 -0
- package/top-ppt-html/assets/theme-overview.png +0 -0
- 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 +186 -0
- package/top-ppt-html/package.json +11 -11
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +110 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +37 -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 +510 -490
- package/top-ppt-html/references/default-surface.md +61 -0
- package/top-ppt-html/references/design-system.md +478 -478
- package/top-ppt-html/references/failure-modes.md +227 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +176 -397
- 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 +325 -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 +259 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +284 -266
- package/top-ppt-html/references/pptx-export.md +227 -209
- package/top-ppt-html/references/presentation-craft.md +69 -0
- 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 +231 -220
- package/top-ppt-html/scripts/audit_styles.py +351 -351
- package/top-ppt-html/scripts/build_examples.py +55 -2276
- package/top-ppt-html/scripts/build_pptx.js +2386 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
- package/top-ppt-html/scripts/checks_html.py +166 -127
- package/top-ppt-html/scripts/cross_verify.py +298 -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 +111 -18
- package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/negative_tests.py +433 -307
- package/top-ppt-html/scripts/package_skill.py +294 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +187 -188
- package/top-ppt-html/scripts/quality_gate.py +309 -301
- package/top-ppt-html/scripts/recommend_layout.py +384 -0
- 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/smoke_pptx.sh +30 -0
- package/top-ppt-html/scripts/sync_runtime.py +659 -662
- package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
- package/top-ppt-html/scripts/validate_report.py +1755 -1456
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/references/design-system-engine.md +0 -235
- package/top-ppt-html/references/industry-benchmark.md +0 -105
- package/top-ppt-html/references/reform-plan.md +0 -252
- package/top-ppt-html/scripts/layout_slots.json +0 -830
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 布局选型建议(Batch 2)
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/recommend_layout.py --mode B --intent "经营分析 占比极偏" --pages 10
|
|
7
|
+
python scripts/recommend_layout.py --mode A --intent "路演" --intent "趋势" --json
|
|
8
|
+
python scripts/recommend_layout.py --mode C --from-model path/to.model.json
|
|
9
|
+
echo '{"mode":"research","intents":["架构"]}' | python scripts/recommend_layout.py --stdin
|
|
10
|
+
|
|
11
|
+
输出 JSON 页序列:{pageType, skel, chart?, rationale, v?}
|
|
12
|
+
启发式:playbook V1–V4 / sizeByComplexity / 极偏禁 donut / 高容量多页序列 / layoutSystem.defaultCharts(核图 8);advanced 仅意图命中;Mode A 主力骨架 P1–P4+P6/P10,次级 P7–P9/P11–P12 不默认。
|
|
13
|
+
不列入 package REQUIRED(可选工具;Batch 3 再定是否纳入)。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parent
|
|
28
|
+
LC = json.loads((ROOT / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
29
|
+
LS = LC.get('layoutSystem') or {}
|
|
30
|
+
PAGE_TO_PRESET = dict(LS.get('pageToPreset') or {})
|
|
31
|
+
DEFAULT_CHARTS = list(LS.get('defaultCharts') or
|
|
32
|
+
['bar', 'hbar', 'line', 'donut', 'progress', 'area', 'stack', 'dualline'])
|
|
33
|
+
ADVANCED_CHARTS = set(LS.get('advancedCharts') or [])
|
|
34
|
+
_MODE_SKELS = LS.get('modeSkels') or {}
|
|
35
|
+
PRIMARY_SKELS = {
|
|
36
|
+
'presentation': list((_MODE_SKELS.get('presentation') or {}).get('primary') or
|
|
37
|
+
['P1', 'P2', 'P3', 'P4', 'P6', 'P10']),
|
|
38
|
+
'research': list((_MODE_SKELS.get('research') or {}).get('primary') or
|
|
39
|
+
['P5', 'P7', 'P8', 'P4', 'P6']),
|
|
40
|
+
'architecture': list((_MODE_SKELS.get('architecture') or {}).get('primary') or
|
|
41
|
+
['P9', 'P10', 'P11']),
|
|
42
|
+
}
|
|
43
|
+
SECONDARY_SKELS = {
|
|
44
|
+
'presentation': list((_MODE_SKELS.get('presentation') or {}).get('secondary') or
|
|
45
|
+
['P7', 'P8', 'P9', 'P11', 'P12']),
|
|
46
|
+
'research': list((_MODE_SKELS.get('research') or {}).get('secondary') or []),
|
|
47
|
+
'architecture': list((_MODE_SKELS.get('architecture') or {}).get('secondary') or []),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SIZE_BANDS = list(((LC.get('charts') or {}).get('sizeByComplexity') or {}).get('bands') or [])
|
|
51
|
+
V_MAP = dict(LS.get('vMap') or {'V1': 'P1', 'V2': 'P2', 'V3': 'P3', 'V4': 'P4'})
|
|
52
|
+
|
|
53
|
+
MODE_ALIAS = {
|
|
54
|
+
'a': 'presentation', 'presentation': 'presentation', '演示': 'presentation',
|
|
55
|
+
'b': 'research', 'research': 'research', '研究': 'research',
|
|
56
|
+
'c': 'architecture', 'architecture': 'architecture', '架构': 'architecture',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# 模式默认节奏(页型序列);cover/closing 由调用方决定是否保留
|
|
60
|
+
DEFAULT_SEQ = {
|
|
61
|
+
# Mode A:偏 V1–V4 / claim 页(kpi/points/quote);降低 donut 默认权重(构成意图命中再用)
|
|
62
|
+
'presentation': [
|
|
63
|
+
'cover', 'agenda', 'kpi', 'points', 'bar', 'points', 'comparison', 'quote', 'closing',
|
|
64
|
+
],
|
|
65
|
+
'research': [
|
|
66
|
+
'cover', 'agenda', 'metrics', 'exhibit', 'twocol', 'halftable',
|
|
67
|
+
'exhibit', 'threecol', 'matrix', 'flags', 'closing',
|
|
68
|
+
],
|
|
69
|
+
'architecture': [
|
|
70
|
+
'cover', 'diagram', 'lane', 'diagram', 'points', 'closing',
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
INTENT_RULES: list[tuple[tuple[str, ...], dict]] = [
|
|
75
|
+
(('极偏', '占比失衡', '长尾', '0.5%'), {
|
|
76
|
+
'pageType': 'kpi', 'chart': None, 'v': 'V3',
|
|
77
|
+
'rationale': '极偏占比禁 donut/pie → V3 大数+佐证',
|
|
78
|
+
}),
|
|
79
|
+
(('占比', '构成', '份额', '结构'), {
|
|
80
|
+
'pageType': 'points', 'chart': 'stack', 'v': 'V1',
|
|
81
|
+
'rationale': '构成类优先 stack/hbar+V1;donut 仅非极偏且需环形直觉时用(降默认权重)',
|
|
82
|
+
}),
|
|
83
|
+
(('趋势', '爬坡', '时间序列', '同比', '环比'), {
|
|
84
|
+
'pageType': 'bar', 'chart': 'line', 'v': 'V2',
|
|
85
|
+
'rationale': '时间趋势 → line;演示用 V2 上图下带',
|
|
86
|
+
}),
|
|
87
|
+
(('排名', '对比', '谁高谁低', '对标'), {
|
|
88
|
+
'pageType': 'bar', 'chart': 'hbar', 'v': 'V1',
|
|
89
|
+
'rationale': '排名/对比 → hbar',
|
|
90
|
+
}),
|
|
91
|
+
(('架构', '拓扑', '分层', '泳道', '流程'), {
|
|
92
|
+
'pageType': 'diagram', 'chart': 'network', 'v': None,
|
|
93
|
+
'rationale': '结构图为王;skel P10/P11',
|
|
94
|
+
}),
|
|
95
|
+
(('路演', '融资', 'pitch', '发布', '汇报', '演讲', 'demo', '演示'), {
|
|
96
|
+
'pageType': 'points', 'chart': None, 'v': 'V1',
|
|
97
|
+
'rationale': '演示节奏偏 V1–V4 + claim;简单图禁全幅;motion=none',
|
|
98
|
+
}),
|
|
99
|
+
(('经营', '复盘', '分析', '调研'), {
|
|
100
|
+
'pageType': 'exhibit', 'chart': 'bar', 'v': None,
|
|
101
|
+
'rationale': '研究证据页 exhibit + bar',
|
|
102
|
+
}),
|
|
103
|
+
(('瀑布', 'waterfall', '增减拆解', '桥图'), {
|
|
104
|
+
'pageType': 'bar', 'chart': 'waterfall', 'v': 'V1',
|
|
105
|
+
'rationale': 'advanced:waterfall 意图命中(非默认池)',
|
|
106
|
+
}),
|
|
107
|
+
(('漏斗', 'funnel', '转化率'), {
|
|
108
|
+
'pageType': 'bar', 'chart': 'funnel', 'v': 'V1',
|
|
109
|
+
'rationale': 'advanced:funnel 意图命中',
|
|
110
|
+
}),
|
|
111
|
+
(('甘特', 'gantt', '排期', '里程碑计划'), {
|
|
112
|
+
'pageType': 'bar', 'chart': 'gantt', 'v': None,
|
|
113
|
+
'rationale': 'advanced:gantt 意图命中',
|
|
114
|
+
}),
|
|
115
|
+
(('桑基', 'sankey', '流向', '分流'), {
|
|
116
|
+
'pageType': 'diagram', 'chart': 'sankey', 'v': None,
|
|
117
|
+
'rationale': 'advanced/信息图:流向意图',
|
|
118
|
+
}),
|
|
119
|
+
(('长文', '密集', '材料多', '证据多', '详细', 'deep dive', '白皮书', '高容量'), {
|
|
120
|
+
'pageType': 'points', 'chart': None, 'v': 'V1',
|
|
121
|
+
'rationale': '高容量→多页序列(勿塞一页)',
|
|
122
|
+
'sequence': 'high_volume',
|
|
123
|
+
}),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
# 高容量内容:推荐多页分解,而非一页塞爆
|
|
127
|
+
HIGH_VOLUME_SEQ = {
|
|
128
|
+
'presentation': [
|
|
129
|
+
'cover', 'agenda',
|
|
130
|
+
'kpi', # claim
|
|
131
|
+
'points', # evidence cards
|
|
132
|
+
'cards', # secondary evidence
|
|
133
|
+
'bar', # chart evidence
|
|
134
|
+
'points', # detail list
|
|
135
|
+
'table', # detail table if needed
|
|
136
|
+
'quote', # breath
|
|
137
|
+
'closing',
|
|
138
|
+
],
|
|
139
|
+
'research': [
|
|
140
|
+
'cover', 'agenda', 'metrics', 'exhibit', 'twocol',
|
|
141
|
+
'halftable', 'exhibit', 'threecol', 'table', 'flags', 'closing',
|
|
142
|
+
],
|
|
143
|
+
'architecture': [
|
|
144
|
+
'cover', 'diagram', 'lane', 'diagram', 'points', 'points', 'closing',
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _norm_mode(m: str) -> str:
|
|
150
|
+
return MODE_ALIAS.get((m or 'b').lower().strip(), 'research')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _skel(page_type: str, v: str | None = None, mode: str | None = None,
|
|
154
|
+
allow_secondary: bool = False) -> str:
|
|
155
|
+
if page_type in ('cover', 'closing'):
|
|
156
|
+
return 'P1'
|
|
157
|
+
if page_type in ('agenda',):
|
|
158
|
+
return 'P2'
|
|
159
|
+
if v and v in V_MAP:
|
|
160
|
+
return V_MAP[v]
|
|
161
|
+
sk = PAGE_TO_PRESET.get(page_type, 'P4')
|
|
162
|
+
# Mode A: remap secondary skels to primary unless intent explicitly allows
|
|
163
|
+
if mode == 'presentation' and not allow_secondary and sk in set(SECONDARY_SKELS.get('presentation') or []):
|
|
164
|
+
remap = {
|
|
165
|
+
'P7': 'P4', 'P8': 'P1', 'P9': 'P10', 'P11': 'P6', 'P12': 'P6',
|
|
166
|
+
}
|
|
167
|
+
sk = remap.get(sk, 'P4')
|
|
168
|
+
return sk
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _chart_ok(chart: str | None, n_cats: int, n_pts: int) -> str | None:
|
|
172
|
+
"""Apply sizeByComplexity forbid list; return possibly replaced chart."""
|
|
173
|
+
if not chart:
|
|
174
|
+
return None
|
|
175
|
+
for band in SIZE_BANDS:
|
|
176
|
+
if n_cats <= int(band.get('maxCats') or 99) and n_pts <= int(band.get('maxPts') or 99):
|
|
177
|
+
forbid = set(band.get('forbid') or [])
|
|
178
|
+
if chart in forbid:
|
|
179
|
+
prefer = list(band.get('prefer') or ['kpi'])
|
|
180
|
+
return None if prefer[0] in ('kpi', 'progress', 'vsbar') else prefer[0]
|
|
181
|
+
return chart
|
|
182
|
+
return chart if chart in DEFAULT_CHARTS else DEFAULT_CHARTS[0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _complexity(sec: dict) -> tuple[int, int]:
|
|
186
|
+
ch = sec.get('chart') if isinstance(sec.get('chart'), dict) else {}
|
|
187
|
+
vals = ch.get('values') or ch.get('data') or []
|
|
188
|
+
labels = ch.get('labels') or []
|
|
189
|
+
pts = sec.get('points') or sec.get('items') or []
|
|
190
|
+
n_pts = len(vals) if vals else len(pts)
|
|
191
|
+
n_cats = len(labels) if labels else (len(vals) if vals else (1 if pts else 0))
|
|
192
|
+
return max(n_cats, 0), max(n_pts, 0)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _skew(sec: dict) -> bool:
|
|
196
|
+
ch = sec.get('chart') if isinstance(sec.get('chart'), dict) else {}
|
|
197
|
+
vals = [float(v) for v in (ch.get('values') or []) if isinstance(v, (int, float))]
|
|
198
|
+
vals = [v for v in vals if v >= 0]
|
|
199
|
+
if len(vals) < 2:
|
|
200
|
+
return False
|
|
201
|
+
total = sum(vals) or 1.0
|
|
202
|
+
pcts = [v / total * 100 for v in vals]
|
|
203
|
+
mn, mx = min(pcts), max(pcts)
|
|
204
|
+
return mn < 5.0 or (mn > 0 and mx / mn > 20)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _match_intent(text: str) -> dict | None:
|
|
208
|
+
for keys, hint in INTENT_RULES:
|
|
209
|
+
if any(k in text for k in keys):
|
|
210
|
+
return dict(hint)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def recommend(
|
|
215
|
+
mode: str,
|
|
216
|
+
intents: list[str] | None = None,
|
|
217
|
+
pages: int | None = None,
|
|
218
|
+
model: dict | None = None,
|
|
219
|
+
) -> list[dict]:
|
|
220
|
+
mode = _norm_mode(mode)
|
|
221
|
+
intents = intents or []
|
|
222
|
+
blob = ' '.join(intents)
|
|
223
|
+
|
|
224
|
+
if model and isinstance(model.get('sections'), list) and model['sections']:
|
|
225
|
+
out = []
|
|
226
|
+
for i, sec in enumerate(model['sections'], 1):
|
|
227
|
+
if not isinstance(sec, dict):
|
|
228
|
+
continue
|
|
229
|
+
pt = str(sec.get('type') or 'points')
|
|
230
|
+
n_cats, n_pts = _complexity(sec)
|
|
231
|
+
ch = None
|
|
232
|
+
if isinstance(sec.get('chart'), dict):
|
|
233
|
+
ch = sec['chart'].get('type')
|
|
234
|
+
elif pt in DEFAULT_CHARTS:
|
|
235
|
+
ch = pt if pt != 'bar' else 'bar'
|
|
236
|
+
rationale = 'from-model'
|
|
237
|
+
v = None
|
|
238
|
+
if _skew(sec) and (ch in ('donut', 'pie', 'multidonut') or pt == 'donut'):
|
|
239
|
+
pt, ch, v = 'kpi', None, 'V3'
|
|
240
|
+
rationale = 'model 极偏占比 → V3 KPI(禁 donut)'
|
|
241
|
+
else:
|
|
242
|
+
ch = _chart_ok(ch, n_cats or 3, n_pts or 3)
|
|
243
|
+
if mode == 'presentation':
|
|
244
|
+
if n_cats <= 2 and n_pts <= 3:
|
|
245
|
+
v = 'V3'
|
|
246
|
+
rationale = '演示·简单数据 → V3 大数,禁全幅简单图'
|
|
247
|
+
elif n_pts >= 8:
|
|
248
|
+
v = 'V2'
|
|
249
|
+
rationale = '演示·复杂序列 → V2 上图下带'
|
|
250
|
+
else:
|
|
251
|
+
v = 'V1'
|
|
252
|
+
rationale = '演示·默认 V1 主视觉+右注解'
|
|
253
|
+
if blob:
|
|
254
|
+
hit = _match_intent(blob)
|
|
255
|
+
if hit and i == min(3, len(model['sections'])):
|
|
256
|
+
pt = hit.get('pageType', pt)
|
|
257
|
+
ch = hit.get('chart', ch)
|
|
258
|
+
v = hit.get('v', v)
|
|
259
|
+
rationale = hit.get('rationale', rationale)
|
|
260
|
+
allow_sec = bool(ch and ch in ADVANCED_CHARTS) or pt in ('diagram', 'lane', 'matrix', 'heatmap')
|
|
261
|
+
skel = sec.get('layoutPreset') or _skel(pt, v, mode, allow_secondary=allow_sec)
|
|
262
|
+
row = {'page': i, 'pageType': pt, 'skel': skel, 'chart': ch,
|
|
263
|
+
'rationale': rationale}
|
|
264
|
+
if v:
|
|
265
|
+
row['v'] = v
|
|
266
|
+
out.append(row)
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
intent_hit = _match_intent(blob) if blob else None
|
|
270
|
+
high_vol = bool(intent_hit and intent_hit.get('sequence') == 'high_volume')
|
|
271
|
+
# 页数偏多或意图含高容量 → 多页序列(议程→主张→证据卡→明细),禁一页塞爆
|
|
272
|
+
if high_vol or (pages and pages >= 10) or any(
|
|
273
|
+
k in blob for k in ('长文', '密集', '材料多', '证据多', '详细', '白皮书')):
|
|
274
|
+
seq = list(HIGH_VOLUME_SEQ.get(mode, HIGH_VOLUME_SEQ['presentation']))
|
|
275
|
+
high_vol = True
|
|
276
|
+
else:
|
|
277
|
+
seq = list(DEFAULT_SEQ.get(mode, DEFAULT_SEQ['research']))
|
|
278
|
+
if pages and pages > 0:
|
|
279
|
+
mid = seq[1:-1] if len(seq) > 2 else seq
|
|
280
|
+
if pages <= 2:
|
|
281
|
+
seq = seq[:1] + seq[-1:]
|
|
282
|
+
else:
|
|
283
|
+
need = pages - 2
|
|
284
|
+
body = (mid * ((need // max(len(mid), 1)) + 1))[:need]
|
|
285
|
+
seq = [seq[0]] + body + [seq[-1]]
|
|
286
|
+
|
|
287
|
+
out = []
|
|
288
|
+
for i, pt in enumerate(seq, 1):
|
|
289
|
+
# 默认只从核图 8 轮换;advanced 不进默认节奏
|
|
290
|
+
ch = pt if pt in DEFAULT_CHARTS else (DEFAULT_CHARTS[(i - 1) % len(DEFAULT_CHARTS)]
|
|
291
|
+
if pt in ('bar', 'exhibit', 'donut') else None)
|
|
292
|
+
if ch in ADVANCED_CHARTS:
|
|
293
|
+
ch = DEFAULT_CHARTS[(i - 1) % len(DEFAULT_CHARTS)]
|
|
294
|
+
if pt == 'bar':
|
|
295
|
+
ch = 'bar'
|
|
296
|
+
if pt == 'donut':
|
|
297
|
+
ch = 'donut'
|
|
298
|
+
if pt == 'exhibit':
|
|
299
|
+
ch = 'bar'
|
|
300
|
+
if pt == 'comparison':
|
|
301
|
+
ch = 'hbar' # 对比默认 hbar,避免 donut 占默认序列
|
|
302
|
+
# Mode A:内容页默认 V1;kpi→V3;复杂序列由 intent/from-model 升 V2
|
|
303
|
+
if mode == 'presentation' and pt not in ('cover', 'agenda', 'closing', 'quote'):
|
|
304
|
+
v = 'V3' if pt == 'kpi' else 'V1'
|
|
305
|
+
else:
|
|
306
|
+
v = None
|
|
307
|
+
rationale = f'default {mode} rhythm'
|
|
308
|
+
allow_sec = bool(ch and ch in ADVANCED_CHARTS) or pt in ('diagram', 'lane', 'matrix', 'heatmap')
|
|
309
|
+
skel = _skel(pt, v, mode, allow_secondary=allow_sec)
|
|
310
|
+
row = {'page': i, 'pageType': pt, 'skel': skel, 'chart': ch, 'rationale': rationale}
|
|
311
|
+
if v:
|
|
312
|
+
row['v'] = v
|
|
313
|
+
out.append(row)
|
|
314
|
+
|
|
315
|
+
if intent_hit and out and intent_hit.get('sequence') != 'high_volume':
|
|
316
|
+
idx = 2 if len(out) > 2 else 0
|
|
317
|
+
if out[idx]['pageType'] in ('cover', 'agenda'):
|
|
318
|
+
idx = min(idx + 1, len(out) - 1)
|
|
319
|
+
out[idx]['pageType'] = intent_hit.get('pageType', out[idx]['pageType'])
|
|
320
|
+
out[idx]['chart'] = intent_hit.get('chart')
|
|
321
|
+
if intent_hit.get('v'):
|
|
322
|
+
out[idx]['v'] = intent_hit['v']
|
|
323
|
+
ch_i = out[idx].get('chart')
|
|
324
|
+
allow_sec = bool(ch_i and ch_i in ADVANCED_CHARTS) or out[idx]['pageType'] in ('diagram', 'lane', 'matrix', 'heatmap')
|
|
325
|
+
out[idx]['skel'] = _skel(out[idx]['pageType'], out[idx].get('v'), mode, allow_secondary=allow_sec)
|
|
326
|
+
out[idx]['rationale'] = intent_hit.get('rationale', '')
|
|
327
|
+
if high_vol:
|
|
328
|
+
for r in out:
|
|
329
|
+
if r['pageType'] in ('cover', 'closing', 'agenda'):
|
|
330
|
+
continue
|
|
331
|
+
r['rationale'] = (r.get('rationale') or '') + (
|
|
332
|
+
' · 高容量多页分解(禁一页塞爆/截断)' if '高容量' not in (r.get('rationale') or '') else '')
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main() -> int:
|
|
337
|
+
ap = argparse.ArgumentParser(description='Recommend pageType/skel/chart sequence')
|
|
338
|
+
ap.add_argument('--mode', default=None, help='A|B|C or presentation|research|architecture')
|
|
339
|
+
ap.add_argument('--intent', action='append', default=[], help='repeatable intent phrases')
|
|
340
|
+
ap.add_argument('--pages', type=int, default=None)
|
|
341
|
+
ap.add_argument('--from-model', default=None, help='path to REPORT_MODEL JSON')
|
|
342
|
+
ap.add_argument('--stdin', action='store_true', help='read JSON {mode,intents,pages,model} from stdin')
|
|
343
|
+
ap.add_argument('--json', action='store_true', help='machine-readable (default when --stdin/--from-model)')
|
|
344
|
+
args = ap.parse_args()
|
|
345
|
+
|
|
346
|
+
model = None
|
|
347
|
+
intents = list(args.intent or [])
|
|
348
|
+
mode = args.mode
|
|
349
|
+
pages = args.pages
|
|
350
|
+
|
|
351
|
+
if args.stdin:
|
|
352
|
+
raw = sys.stdin.read()
|
|
353
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
354
|
+
mode = mode or data.get('mode')
|
|
355
|
+
intents = intents or list(data.get('intents') or [])
|
|
356
|
+
pages = pages if pages is not None else data.get('pages')
|
|
357
|
+
model = data.get('model') or data if 'sections' in data else None
|
|
358
|
+
|
|
359
|
+
if args.from_model:
|
|
360
|
+
model = json.loads(Path(args.from_model).read_text(encoding='utf-8'))
|
|
361
|
+
mode = mode or model.get('mode')
|
|
362
|
+
|
|
363
|
+
if not mode:
|
|
364
|
+
ap.error('--mode is required (or provide via --from-model/--stdin)')
|
|
365
|
+
|
|
366
|
+
rows = recommend(mode, intents, pages, model)
|
|
367
|
+
as_json = args.json or args.stdin or bool(args.from_model)
|
|
368
|
+
payload = {'mode': _norm_mode(mode), 'pages': rows}
|
|
369
|
+
if as_json:
|
|
370
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
371
|
+
else:
|
|
372
|
+
print(f"# recommend_layout · mode={payload['mode']}"
|
|
373
|
+
+ (f" · intents={intents!r}" if intents else '')
|
|
374
|
+
+ (f" · pages={pages}" if pages else ''))
|
|
375
|
+
print(f"{'#':>3} {'pageType':<14} {'skel':<6} {'chart':<12} {'v':<4} rationale")
|
|
376
|
+
for r in rows:
|
|
377
|
+
print(f"{r['page']:>3} {r['pageType']:<14} {r['skel']:<6} "
|
|
378
|
+
f"{str(r.get('chart') or '—'):<12} {str(r.get('v') or '—'):<4} "
|
|
379
|
+
f"{r.get('rationale') or ''}")
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
if __name__ == '__main__':
|
|
384
|
+
sys.exit(main())
|