@topmindspace/tms-skills 0.1.13 → 0.1.15
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 +34 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +3 -3
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +59 -20
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +64 -26
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +64 -42
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +85 -46
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +1 -1
- package/top-ppt-html/assets/pptx-export.js +40 -17
- package/top-ppt-html/assets/showcase/showcase-charts.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-cover.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-gates.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-modes.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-positioning.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-split.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-toolbar.png +0 -0
- package/top-ppt-html/assets/style-gallery.html +3 -3
- package/top-ppt-html/assets/templates/architecture.html +59 -20
- package/top-ppt-html/assets/templates/engine.css +17 -1
- package/top-ppt-html/assets/templates/presentation.html +59 -20
- package/top-ppt-html/assets/templates/research.html +60 -27
- package/top-ppt-html/assets/templates/ui.js +1 -1
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +1 -1
- package/top-ppt-html/references/components-atoms.md +14 -1
- package/top-ppt-html/references/content-rules.md +1 -1
- package/top-ppt-html/references/design-system.md +1 -1
- package/top-ppt-html/references/icons.md +19 -0
- package/top-ppt-html/references/layout-grammar.md +17 -0
- package/top-ppt-html/references/layouts-combo.md +2 -4
- package/top-ppt-html/references/modes.md +1 -1
- package/top-ppt-html/references/pptx-export.md +1 -1
- package/top-ppt-html/scripts/build_pptx.js +50 -22
- package/top-ppt-html/scripts/layout-constants.json +18 -6
- package/top-ppt-html/scripts/quality_gate.py +2 -2
- package/top-ppt-html/scripts/render_from_model.py +2 -4
- package/top-ppt-html/scripts/scaffold_report.py +4 -10
- package/top-ppt-html/scripts/test_feedback_gates.py +2 -2
- package/top-ppt-html/scripts/validate_pptx.py +27 -11
- package/top-ppt-html/scripts/validate_report.py +27 -5
|
@@ -77,8 +77,7 @@ def sowhat(sec: dict) -> str:
|
|
|
77
77
|
t = sec.get('soWhat')
|
|
78
78
|
if not t:
|
|
79
79
|
return ''
|
|
80
|
-
return (f' <div class="sowhat rv"><span class="
|
|
81
|
-
f'<span class="sowhat__v">{cite(t)}</span></div>\n')
|
|
80
|
+
return (f' <div class="sowhat rv"><span class="sowhat__v">{cite(t)}</span></div>\n')
|
|
82
81
|
|
|
83
82
|
|
|
84
83
|
def footnote(sec: dict) -> str:
|
|
@@ -326,8 +325,7 @@ def r_comparison(i, sec):
|
|
|
326
325
|
+ panel(sec.get('left')) + '\n' + panel(sec.get('right'), True) + '\n'
|
|
327
326
|
+ ' </div>\n')
|
|
328
327
|
if sec.get('verdict'):
|
|
329
|
-
body += (f' <div class="sowhat rv"><span class="
|
|
330
|
-
f'<span class="sowhat__v">{cite(sec["verdict"])}</span></div>\n')
|
|
328
|
+
body += (f' <div class="sowhat rv"><span class="sowhat__v">{cite(sec["verdict"])}</span></div>\n')
|
|
331
329
|
sec = dict(sec)
|
|
332
330
|
sec.pop('verdict', None)
|
|
333
331
|
return wrap(i, 'comparison', body, sec)
|
|
@@ -282,8 +282,7 @@ def pg_chart(i, sec):
|
|
|
282
282
|
</div>
|
|
283
283
|
{SIDE_NOTES}
|
|
284
284
|
</div>
|
|
285
|
-
<div class="sowhat rv"><span class="
|
|
286
|
-
<span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
285
|
+
<div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
287
286
|
</div>
|
|
288
287
|
</section>'''
|
|
289
288
|
return html, {'type': 'bar', 'chart': {'type': ct, 'labels': ['A', 'B', 'C'], 'values': [42, 61, 35],
|
|
@@ -303,8 +302,7 @@ def pg_donut(i, sec):
|
|
|
303
302
|
</div>
|
|
304
303
|
{SIDE_NOTES}
|
|
305
304
|
</div>
|
|
306
|
-
<div class="sowhat rv"><span class="
|
|
307
|
-
<span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
305
|
+
<div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
308
306
|
</div>
|
|
309
307
|
</section>'''
|
|
310
308
|
return html, {'type': 'donut', 'chart': {'labels': ['A', 'B', 'C'], 'values': [45, 35, 20],
|
|
@@ -327,7 +325,6 @@ def pg_exhibit(i, sec):
|
|
|
327
325
|
{SIDE_NOTES}
|
|
328
326
|
</div>
|
|
329
327
|
<div class="sowhat rv">
|
|
330
|
-
<span class="sowhat__k">So what</span>
|
|
331
328
|
<span class="sowhat__v">一行含义或建议(≤60 字)。</span>
|
|
332
329
|
</div>
|
|
333
330
|
</div>
|
|
@@ -556,8 +553,7 @@ def pg_comparison(i, sec):
|
|
|
556
553
|
<h3 class="t-h3" style="color:var(--accent)">目标</h3>
|
|
557
554
|
<ul class="ul"><li><strong>要点。</strong>一句话。</li><li><strong>要点。</strong>一句话。</li></ul></div>
|
|
558
555
|
</div>
|
|
559
|
-
<div class="sowhat rv"><span class="
|
|
560
|
-
<span class="sowhat__v">一行结论(≤60 字)。</span></div>
|
|
556
|
+
<div class="sowhat rv"><span class="sowhat__v">一行结论(≤60 字)。</span></div>
|
|
561
557
|
</div>
|
|
562
558
|
</section>'''
|
|
563
559
|
return html, {'type': 'comparison',
|
|
@@ -721,8 +717,7 @@ def pg_info(i, sec):
|
|
|
721
717
|
<text x="{w // 2}" y="{h // 2}" text-anchor="middle" class="f-txt3" font-size="13">{t} 信息图占位</text>
|
|
722
718
|
</svg>
|
|
723
719
|
</div>
|
|
724
|
-
{src} <div class="sowhat rv"><span class="
|
|
725
|
-
<span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
720
|
+
{src} <div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
|
|
726
721
|
</div>
|
|
727
722
|
</section>'''
|
|
728
723
|
model = {'type': t}
|
|
@@ -760,7 +755,6 @@ BUILDERS = {
|
|
|
760
755
|
|
|
761
756
|
|
|
762
757
|
SOWHAT_BLOCK = ''' <div class="sowhat rv">
|
|
763
|
-
<span class="sowhat__k">So what</span>
|
|
764
758
|
<span class="sowhat__v">这一页的含义或下一步建议(≤60 字)。</span>
|
|
765
759
|
</div>
|
|
766
760
|
'''
|
|
@@ -225,7 +225,7 @@ def test_annotation_band_reports_all_invaders() -> None:
|
|
|
225
225
|
els = [
|
|
226
226
|
sp(5.0, 1.2, 4.0, "PlotSeriesA"),
|
|
227
227
|
sp(5.2, 1.1, 4.0, "PlotSeriesB"),
|
|
228
|
-
sp(6.05, 0.55, 12.0, "
|
|
228
|
+
sp(6.05, 0.55, 12.0, "Platform locked path takeaway"),
|
|
229
229
|
]
|
|
230
230
|
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
231
231
|
codes = [i["code"] for i in issues]
|
|
@@ -252,7 +252,7 @@ def test_annotation_band_so_what_band_top() -> None:
|
|
|
252
252
|
els = [
|
|
253
253
|
# Body ends at 6.30 — inside (soWhatY=6.05, contentBottomWithNote=6.40]
|
|
254
254
|
sp(5.0, 1.3, 4.0, "LegendCrushMid"),
|
|
255
|
-
sp(6.05, 0.55, 12.0, "
|
|
255
|
+
sp(6.05, 0.55, 12.0, "Platform first takeaway text"),
|
|
256
256
|
]
|
|
257
257
|
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
258
258
|
codes = [i["code"] for i in issues]
|
|
@@ -439,7 +439,7 @@ def annotation_band_overlap_check(
|
|
|
439
439
|
def _is_annotation_self(y: int, cy: int, cx: int, text: str) -> bool:
|
|
440
440
|
"""so-what / footnote / source row living in the annotation zone."""
|
|
441
441
|
if _at_so_what_slot(y) and cy <= int(0.85 * 914400):
|
|
442
|
-
# Fill rect, accent strip, or
|
|
442
|
+
# Fill rect, accent strip, or 结论条正文 — all live in the slot
|
|
443
443
|
if ann_pat.search(text) or _is_bar_like(cy, cx) or cx <= int(0.15 * 914400):
|
|
444
444
|
return True
|
|
445
445
|
if _is_note_like(y, cy, text):
|
|
@@ -464,7 +464,7 @@ def annotation_band_overlap_check(
|
|
|
464
464
|
note_like = _is_note_like(y, cy, txt)
|
|
465
465
|
labeled = bool(ann_pat.search(txt))
|
|
466
466
|
bar = _is_bar_like(cy, cx)
|
|
467
|
-
# Fill rect sits on soWhatY (±0.02);
|
|
467
|
+
# Fill rect sits on soWhatY (±0.02); 结论条正文 may sit slightly below
|
|
468
468
|
on_fill = abs(y - so_what_y) <= int(0.02 * 914400) and bar
|
|
469
469
|
labeled_slot = labeled and _at_so_what_slot(y) and cy <= int(0.85 * 914400)
|
|
470
470
|
if note_like:
|
|
@@ -503,7 +503,7 @@ def annotation_band_overlap_check(
|
|
|
503
503
|
"ANNOTATION_BAND_OVERLAP",
|
|
504
504
|
f"主内容侵入注释带(元素底边 {bottom/914400:.2f}in 越过注释带顶 "
|
|
505
505
|
f"{band_top_in:.2f}in,重叠 {overlap/914400:.2f}in)——"
|
|
506
|
-
"
|
|
506
|
+
"图例/系列请收入主图区或压缩系列数,禁止压进结论条/来源行。",
|
|
507
507
|
slide=slide_no,
|
|
508
508
|
))
|
|
509
509
|
return out
|
|
@@ -619,9 +619,10 @@ def _infer_container_pad(shape: ET.Element, pad_map: dict[str, float], min_pad:
|
|
|
619
619
|
return float(pad_map[kind]), kind
|
|
620
620
|
if kind:
|
|
621
621
|
return min_pad, kind
|
|
622
|
-
#
|
|
622
|
+
# 启发式兜底:仅对明确的结论条 / 待核实文本用对应 pad(其余 minPad)
|
|
623
623
|
text = text_content(shape)
|
|
624
|
-
|
|
624
|
+
# Legacy "SO WHAT" label OR tr:soWhat already handled above; keep SO WHAT for old files
|
|
625
|
+
if "SO WHAT" in text or "结论条" in text:
|
|
625
626
|
return float(pad_map.get("soWhat") or min_pad), "soWhat"
|
|
626
627
|
if "待核实" in text:
|
|
627
628
|
return float(pad_map.get("band") or min_pad), "band"
|
|
@@ -995,12 +996,27 @@ def inspect_slide(
|
|
|
995
996
|
issue("LOW_TEXT_DENSITY", "多个文本框但总字符 <25;信息密度不足。", slide=slide_number)
|
|
996
997
|
)
|
|
997
998
|
|
|
998
|
-
if re.search(r"EXHIBIT\s*\d", combined_text, re.IGNORECASE)
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
999
|
+
if re.search(r"EXHIBIT\s*\d", combined_text, re.IGNORECASE):
|
|
1000
|
+
has_conclusion_bar = False
|
|
1001
|
+
for el in all_elements:
|
|
1002
|
+
oname = _shape_object_name(el)
|
|
1003
|
+
if "soWhat" in (oname or ""):
|
|
1004
|
+
has_conclusion_bar = True
|
|
1005
|
+
break
|
|
1006
|
+
box = shape_bounds(el)
|
|
1007
|
+
if box is None:
|
|
1008
|
+
continue
|
|
1009
|
+
_x, y, cx, cy = box
|
|
1010
|
+
# Wide fill at soWhatY ≈ 6.05in (MD3 结论条衬底)
|
|
1011
|
+
if abs(y / 914400 - 6.05) <= 0.08 and cx / 914400 >= 6.0 and cy / 914400 <= 0.85:
|
|
1012
|
+
has_conclusion_bar = True
|
|
1013
|
+
break
|
|
1014
|
+
if not has_conclusion_bar and "SO WHAT" not in combined_text:
|
|
1015
|
+
warnings.append(
|
|
1016
|
+
issue("EXHIBIT_PAGE_MISSING_SOWHAT",
|
|
1017
|
+
"页面带 Exhibit 编号但没有结论条(research R2 版式 · MD3 衬条)。",
|
|
1018
|
+
slide=slide_number)
|
|
1019
|
+
)
|
|
1004
1020
|
|
|
1005
1021
|
# v9:HTML 标签源码泄漏进 PPTX 文本(模型字段未净化时原样露出)
|
|
1006
1022
|
tag_leaks = re.findall(
|
|
@@ -23,7 +23,7 @@ research / architecture 仍须显式传 --layout-qa(不强制)。
|
|
|
23
23
|
强调带约束(band--deep 反相页 ≤ 上限且不作末页) / 待核实标注(.tbd ↔ .tbd-legend/.flagbar) /
|
|
24
24
|
素材图片与配图占位(零外链 + alt + 内联体积 + 版式/比例锁定类对应 + 占位可见标签 + 模型对应) /
|
|
25
25
|
PPTX 预览配套(预览按钮 + 模型 + 运行时,页面无导出按钮) /
|
|
26
|
-
内容级质量(版式节奏连用上限 /
|
|
26
|
+
内容级质量(版式节奏连用上限 / 结论条实质与空洞套话 / research 标题含数字或判断词)
|
|
27
27
|
"""
|
|
28
28
|
import sys
|
|
29
29
|
import re
|
|
@@ -97,7 +97,7 @@ IMAGE_MAX_PER_PAGE = int(IMAGE_SPEC.get('maxPerPage') or 6)
|
|
|
97
97
|
IMAGE_MAX_INLINE = int(IMAGE_SPEC.get('maxInlineBytes') or 1572864)
|
|
98
98
|
IMAGE_MAX_TOTAL = int(IMAGE_SPEC.get('maxTotalInlineBytes') or 8388608)
|
|
99
99
|
|
|
100
|
-
AGENDA_SINGLE_MAX = 8
|
|
100
|
+
AGENDA_SINGLE_MAX = int((((LC.get("contentQuality") or {}).get("agenda") or {}).get("singleMax")) or 8) # 超过则须 agenda--2col
|
|
101
101
|
# 页高估算参数(单源 layout-constants.pageHeightEstimate)
|
|
102
102
|
_HE = LC.get('pageHeightEstimate') or {}
|
|
103
103
|
SCREEN_BUDGET_PX = int(_HE.get('screenBudgetPx') or 1000)
|
|
@@ -356,7 +356,7 @@ def _check_bands(txt, chk, mode="presentation"):
|
|
|
356
356
|
|
|
357
357
|
|
|
358
358
|
def _check_icons(txt, chk):
|
|
359
|
-
"""
|
|
359
|
+
"""图标使用:长报告不应全文无图标;含 ≥2 张卡的内容页应有 .card__ico(WARN)。"""
|
|
360
360
|
if len(_bands(txt)) < 6:
|
|
361
361
|
return
|
|
362
362
|
has_icon = ('ul--ico' in txt or 'metric__ico' in txt or
|
|
@@ -364,6 +364,24 @@ def _check_icons(txt, chk):
|
|
|
364
364
|
re.search(r'card__ico">\s*<svg', txt))
|
|
365
365
|
chk("长报告(≥6页)要点卡/关键列表有图标", bool(has_icon),
|
|
366
366
|
"全文未用图标(见 icons.md 使用准则)", level="WARN")
|
|
367
|
+
# Soft gate: points/cards pages with ≥2 .card and zero .card__ico
|
|
368
|
+
bare_card_pages = 0
|
|
369
|
+
for m in re.finditer(r'<section\b([^>]*)>([\s\S]*?)</section>', txt):
|
|
370
|
+
attrs, body = m.group(1), m.group(2)
|
|
371
|
+
pt = re.search(r'data-page-type="([^"]+)"', attrs)
|
|
372
|
+
page_type = (pt.group(1) if pt else '') or ''
|
|
373
|
+
if page_type not in ('points', 'cards', 'split', 'comparison'):
|
|
374
|
+
# Also catch sections that clearly use card grids
|
|
375
|
+
if body.count('class="card') + body.count("class='card") < 2:
|
|
376
|
+
continue
|
|
377
|
+
n_cards = len(re.findall(r'class="[^"]*\bcard\b', body))
|
|
378
|
+
if n_cards < 2:
|
|
379
|
+
continue
|
|
380
|
+
if 'card__ico' not in body:
|
|
381
|
+
bare_card_pages += 1
|
|
382
|
+
chk("要点/卡片页(≥2 卡)含 .card__ico", bare_card_pages == 0,
|
|
383
|
+
f"{bare_card_pages} 页有多卡但无图标(见 icons.md 四正当位置)" if bare_card_pages else "",
|
|
384
|
+
level="WARN")
|
|
367
385
|
|
|
368
386
|
|
|
369
387
|
def _check_table_rows(txt, chk, mode="presentation"):
|
|
@@ -763,7 +781,7 @@ def _check_content_quality(txt, chk, mode, model):
|
|
|
763
781
|
if any(f in body for f in forbidden):
|
|
764
782
|
hollow.append(f'{src}:{body[:24]}')
|
|
765
783
|
if texts:
|
|
766
|
-
chk(f"
|
|
784
|
+
chk(f"结论条实质(长度 ≥{min_chars} 字且无空洞套话)", not short and not hollow,
|
|
767
785
|
"; ".join((short + hollow)[:4]) if (short or hollow) else "", level="WARN")
|
|
768
786
|
|
|
769
787
|
# ③ research 行动标题须含数字或判断词
|
|
@@ -1651,8 +1669,12 @@ def main():
|
|
|
1651
1669
|
n_items = txt.count('class="agenda__a"')
|
|
1652
1670
|
chk("Agenda 条目可点击跳转 (≥4)", n_items >= 4, f"{n_items} 条")
|
|
1653
1671
|
if n_items > AGENDA_SINGLE_MAX:
|
|
1672
|
+
# 只认大纲元素 class(禁被 engine.css 的 .agenda--2col 选择器误伤)
|
|
1673
|
+
has_2col = bool(re.search(
|
|
1674
|
+
r'<ol\b[^>]*class="[^"]*\bagenda--2col\b', txt)) or bool(re.search(
|
|
1675
|
+
r'<ol\b[^>]*class="[^"]*\bagenda\b[^"]*\bagenda--2col\b', txt))
|
|
1654
1676
|
chk(f"Agenda >{AGENDA_SINGLE_MAX} 条时已用 agenda--2col 双列",
|
|
1655
|
-
|
|
1677
|
+
has_2col, f"{n_items} 条未双列(大纲很多→两排/两列,禁单列撑爆一屏)", level="WARN")
|
|
1656
1678
|
|
|
1657
1679
|
# ── 主标题粗体 ──
|
|
1658
1680
|
chk("主标题粗体 --fw-title/--fw-display",
|