@topmindspace/tms-skills 0.1.6 → 0.1.8
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 +31 -0
- package/README.md +48 -69
- package/package.json +1 -1
- package/top-ppt-html/README.md +25 -6
- package/top-ppt-html/SKILL.md +6 -6
- package/top-ppt-html/agents/openai.yaml +1 -1
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +10 -5
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +10 -5
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +10 -5
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +4016 -0
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +315 -0
- package/top-ppt-html/assets/pptx-export.js +9 -4
- 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-split.png +0 -0
- package/top-ppt-html/assets/showcase/style-business-blue-cover.png +0 -0
- package/top-ppt-html/assets/showcase/style-graphite-cover.png +0 -0
- package/top-ppt-html/assets/showcase/style-mckinsey-cover.png +0 -0
- package/top-ppt-html/assets/templates/architecture.html +10 -5
- package/top-ppt-html/assets/templates/presentation.html +10 -5
- package/top-ppt-html/assets/templates/research.html +10 -5
- 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/package-lock.json +2 -2
- package/top-ppt-html/package.json +1 -1
- package/top-ppt-html/references/playbook.md +1 -1
- package/top-ppt-html/scripts/build_examples.py +4 -3
- package/top-ppt-html/scripts/build_pptx.js +12 -5
- package/top-ppt-html/scripts/package_skill.py +4 -3
- package/top-ppt-html/scripts/smoke_pptx.sh +1 -0
- package/top-ppt-html/scripts/test_feedback_gates.py +96 -0
- package/top-ppt-html/scripts/validate_pptx.py +60 -37
|
@@ -209,6 +209,98 @@ def test_font_size_not_snapped() -> None:
|
|
|
209
209
|
)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def test_annotation_band_reports_all_invaders() -> None:
|
|
213
|
+
"""D9: do not break after the first ANNOTATION_BAND_OVERLAP on a slide."""
|
|
214
|
+
def sp(y, h, w, text):
|
|
215
|
+
return ET.fromstring(
|
|
216
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
217
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
218
|
+
"<p:spPr><a:xfrm>"
|
|
219
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
220
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
221
|
+
"</a:xfrm></p:spPr>"
|
|
222
|
+
f"<p:txBody><a:p><a:r><a:t>{text}</a:t></a:r></a:p></p:txBody>"
|
|
223
|
+
"</p:sp>"
|
|
224
|
+
)
|
|
225
|
+
els = [
|
|
226
|
+
sp(5.0, 1.2, 4.0, "PlotSeriesA"),
|
|
227
|
+
sp(5.2, 1.1, 4.0, "PlotSeriesB"),
|
|
228
|
+
sp(6.05, 0.55, 12.0, "SO WHAT locked path"),
|
|
229
|
+
]
|
|
230
|
+
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
231
|
+
codes = [i["code"] for i in issues]
|
|
232
|
+
ok(
|
|
233
|
+
"3e D9 reports all invaders (not just first)",
|
|
234
|
+
codes.count("ANNOTATION_BAND_OVERLAP") >= 2,
|
|
235
|
+
f"codes={codes}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_annotation_band_so_what_band_top() -> None:
|
|
240
|
+
"""D11: with so-what present, invasions in (6.05, 6.40] must fire."""
|
|
241
|
+
def sp(y, h, w, text):
|
|
242
|
+
return ET.fromstring(
|
|
243
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
244
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
245
|
+
"<p:spPr><a:xfrm>"
|
|
246
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
247
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
248
|
+
"</a:xfrm></p:spPr>"
|
|
249
|
+
f"<p:txBody><a:p><a:r><a:t>{text}</a:t></a:r></a:p></p:txBody>"
|
|
250
|
+
"</p:sp>"
|
|
251
|
+
)
|
|
252
|
+
els = [
|
|
253
|
+
# Body ends at 6.30 — inside (soWhatY=6.05, contentBottomWithNote=6.40]
|
|
254
|
+
sp(5.0, 1.3, 4.0, "LegendCrushMid"),
|
|
255
|
+
sp(6.05, 0.55, 12.0, "SO WHAT platform first"),
|
|
256
|
+
]
|
|
257
|
+
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
258
|
+
codes = [i["code"] for i in issues]
|
|
259
|
+
msgs = " ".join(i.get("message") or "" for i in issues)
|
|
260
|
+
ok(
|
|
261
|
+
"3f D11 band_top=soWhatY catches invasion in (6.05, 6.40]",
|
|
262
|
+
"ANNOTATION_BAND_OVERLAP" in codes and "6.05" in msgs,
|
|
263
|
+
f"codes={codes} msgs={msgs[:120]}",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_chrome_footer_ignores_bare_integers() -> None:
|
|
268
|
+
"""CHROME_DRIFT FP: bare year/exhibit numbers must not count as pager."""
|
|
269
|
+
def sp(y, text):
|
|
270
|
+
return ET.fromstring(
|
|
271
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
272
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
273
|
+
"<p:spPr><a:xfrm>"
|
|
274
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
275
|
+
f'<a:ext cx="{int(1.0 * EMU)}" cy="{int(0.3 * EMU)}"/>'
|
|
276
|
+
"</a:xfrm></p:spPr>"
|
|
277
|
+
f'<p:txBody><a:p><a:r><a:rPr sz="1100"/><a:t>{text}</a:t></a:r></a:p></p:txBody>'
|
|
278
|
+
"</p:sp>"
|
|
279
|
+
)
|
|
280
|
+
shapes = [sp(6.14, "2024"), sp(6.14, "06"), sp(7.0, "3 / 14")]
|
|
281
|
+
y = V.chrome_footer_y_in(shapes, int(7.5 * EMU))
|
|
282
|
+
ok(
|
|
283
|
+
"5 chrome_footer_y_in uses N / M only (ignores bare ints)",
|
|
284
|
+
y is not None and abs(y - 7.0) < 0.05,
|
|
285
|
+
f"got {y}",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_chart_bottom_no_short_circuit() -> None:
|
|
290
|
+
"""D8: chartBottom must Math.min soWhat/footnote — no footnote-first return."""
|
|
291
|
+
build = (ROOT.parent / "scripts" / "build_pptx.js").read_text(encoding="utf-8")
|
|
292
|
+
m = re.search(r"function chartBottom\([\s\S]*?\n\}", build)
|
|
293
|
+
body = m.group(0) if m else ""
|
|
294
|
+
ok(
|
|
295
|
+
"D8 chartBottom uses Math.min (no footnote short-circuit)",
|
|
296
|
+
"Math.min" in body
|
|
297
|
+
and "hasFootnote) return" not in body
|
|
298
|
+
and "CONTENT_BOTTOM_NOTE" in body,
|
|
299
|
+
f"body={body[:200]!r}",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
212
304
|
def test_engine_static() -> None:
|
|
213
305
|
export = (ROOT.parent / "assets" / "pptx-export.js").read_text(encoding="utf-8")
|
|
214
306
|
build = (ROOT.parent / "scripts" / "build_pptx.js").read_text(encoding="utf-8")
|
|
@@ -262,6 +354,10 @@ def main() -> int:
|
|
|
262
354
|
test_annotation_band_ignores_full_bleed_bg()
|
|
263
355
|
test_annotation_band_allows_content_without_note()
|
|
264
356
|
test_annotation_band_fires_when_note_present()
|
|
357
|
+
test_annotation_band_reports_all_invaders()
|
|
358
|
+
test_annotation_band_so_what_band_top()
|
|
359
|
+
test_chrome_footer_ignores_bare_integers()
|
|
360
|
+
test_chart_bottom_no_short_circuit()
|
|
265
361
|
test_font_size_h2_17_allowed()
|
|
266
362
|
test_font_size_not_snapped()
|
|
267
363
|
test_engine_static()
|
|
@@ -362,18 +362,20 @@ def annotation_band_overlap_check(
|
|
|
362
362
|
soWhatY ≈ 6.05 · contentBottomWithNote ≈ 6.4 · footnote/note ≈ 6.55 ·
|
|
363
363
|
contentBottom ≈ 6.9 · pager ≈ 7.0
|
|
364
364
|
|
|
365
|
-
Semantics:
|
|
365
|
+
Semantics (0.1.7 · D9/D11):
|
|
366
366
|
- Full-bleed backgrounds, full-height accent strips, and pager/footer
|
|
367
367
|
chrome are never "content crushing the band".
|
|
368
368
|
- so-what / 口径 / 来源 bars that live in the annotation zone are the
|
|
369
369
|
band itself — not invaders.
|
|
370
|
-
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
(
|
|
370
|
+
- Crush = starts ABOVE band_top and overlaps down into the band.
|
|
371
|
+
Shapes whose top is already at/below band_top are in-band chrome.
|
|
372
|
+
- When so-what is present, band_top = soWhatY so invasions in
|
|
373
|
+
(soWhatY, contentBottomWithNote] are reported (D11). Footnote/source
|
|
374
|
+
only keeps band_top = contentBottomWithNote.
|
|
374
375
|
- When no annotation is present, main content may use up to
|
|
375
376
|
contentBottom; only a severe invasion (≥0.35in into the nominal
|
|
376
377
|
withNote band) still fires as a safety net.
|
|
378
|
+
- Report ALL invaders per slide (D9) — do not break after the first.
|
|
377
379
|
"""
|
|
378
380
|
out: list[dict[str, Any]] = []
|
|
379
381
|
try:
|
|
@@ -382,14 +384,16 @@ def annotation_band_overlap_check(
|
|
|
382
384
|
lay = ((lc.get("pageTypes") or {}).get("layout") or {})
|
|
383
385
|
exhibit = ((lc.get("pageTypes") or {}).get("exhibit") or {})
|
|
384
386
|
note = ((lc.get("pageTypes") or {}).get("note") or {})
|
|
385
|
-
|
|
387
|
+
nominal_band_top_in = float(lay.get("contentBottomWithNote") or 6.4)
|
|
386
388
|
band_bot_in = float(lay.get("contentBottom") or 6.9)
|
|
387
389
|
so_what_y_in = float(exhibit.get("soWhatY") or 6.05)
|
|
388
390
|
note_y_in = float(note.get("y") or exhibit.get("footnoteY") or 6.55)
|
|
389
391
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
390
|
-
|
|
392
|
+
nominal_band_top_in, band_bot_in, so_what_y_in, note_y_in = 6.4, 6.9, 6.05, 6.55
|
|
391
393
|
|
|
394
|
+
band_top_in = nominal_band_top_in
|
|
392
395
|
band_top = int(band_top_in * 914400)
|
|
396
|
+
nominal_band_top = int(nominal_band_top_in * 914400)
|
|
393
397
|
band_bot = int(band_bot_in * 914400)
|
|
394
398
|
so_what_y = int(so_what_y_in * 914400)
|
|
395
399
|
note_y = int(note_y_in * 914400)
|
|
@@ -397,12 +401,14 @@ def annotation_band_overlap_check(
|
|
|
397
401
|
slide_h = int(slide_height_emu)
|
|
398
402
|
slide_area = max(slide_w * slide_h, 1)
|
|
399
403
|
pager_floor = int(min(band_bot_in + 0.05, slide_h / 914400.0 - 0.05) * 914400)
|
|
400
|
-
min_overlap = int(0.10 * 914400)
|
|
401
|
-
severe_overlap = int(0.35 * 914400)
|
|
404
|
+
min_overlap = int(0.10 * 914400)
|
|
405
|
+
severe_overlap = int(0.35 * 914400)
|
|
402
406
|
full_bleed_area = 0.85
|
|
403
407
|
full_height_frac = 0.90
|
|
408
|
+
# Label-like prefixes only — body text containing「来源」must not count (agenda FP)
|
|
404
409
|
ann_pat = re.compile(
|
|
405
|
-
r"(结论|SO\s*WHAT
|
|
410
|
+
r"(?:^|\n)\s*(结论|SO\s*WHAT|口径\s*[::]?|来源\s*[::]|数据来源\s*[::]|"
|
|
411
|
+
r"Source\s*[::]|Note\s*[::]|footnote|注\s*[::])",
|
|
406
412
|
re.IGNORECASE,
|
|
407
413
|
)
|
|
408
414
|
|
|
@@ -420,23 +426,31 @@ def annotation_band_overlap_check(
|
|
|
420
426
|
return True
|
|
421
427
|
return False
|
|
422
428
|
|
|
429
|
+
def _is_bar_like(cy: int, cx: int) -> bool:
|
|
430
|
+
return cy <= int(0.75 * 914400) and cx >= int(0.45 * slide_w)
|
|
431
|
+
|
|
432
|
+
def _is_note_like(y: int, cy: int, text: str) -> bool:
|
|
433
|
+
return y >= note_y - int(0.05 * 914400) and cy <= int(0.55 * 914400) and bool(text)
|
|
434
|
+
|
|
435
|
+
def _at_so_what_slot(y: int) -> bool:
|
|
436
|
+
# Engine locks so-what to soWhatY; agenda trailing rows sit lower (~+0.12)
|
|
437
|
+
return abs(y - so_what_y) <= int(0.08 * 914400)
|
|
438
|
+
|
|
423
439
|
def _is_annotation_self(y: int, cy: int, cx: int, text: str) -> bool:
|
|
424
440
|
"""so-what / footnote / source row living in the annotation zone."""
|
|
425
|
-
if y
|
|
426
|
-
|
|
441
|
+
if _at_so_what_slot(y) and cy <= int(0.85 * 914400):
|
|
442
|
+
# Fill rect, accent strip, or SO WHAT text — all live in the slot
|
|
443
|
+
if ann_pat.search(text) or _is_bar_like(cy, cx) or cx <= int(0.15 * 914400):
|
|
427
444
|
return True
|
|
428
|
-
|
|
429
|
-
if cy <= int(0.75 * 914400) and cx >= int(0.45 * slide_w):
|
|
430
|
-
return True
|
|
431
|
-
if y >= note_y - int(0.05 * 914400) and cy <= int(0.55 * 914400):
|
|
445
|
+
if _is_note_like(y, cy, text):
|
|
432
446
|
return True
|
|
433
|
-
# Entirely inside the nominal withNote band
|
|
434
|
-
if y >=
|
|
447
|
+
# Entirely inside the nominal withNote band (6.40 口径)
|
|
448
|
+
if y >= nominal_band_top - int(0.02 * 914400):
|
|
435
449
|
return True
|
|
436
450
|
return False
|
|
437
451
|
|
|
438
|
-
# Pass 1: does this slide reserve an annotation?
|
|
439
452
|
has_annotation = False
|
|
453
|
+
has_so_what = False
|
|
440
454
|
for el in elements:
|
|
441
455
|
box = shape_bounds(el)
|
|
442
456
|
if box is None:
|
|
@@ -446,19 +460,24 @@ def annotation_band_overlap_check(
|
|
|
446
460
|
continue
|
|
447
461
|
if _is_chrome_or_bg(y, cy, cx):
|
|
448
462
|
continue
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
463
|
+
txt = _text(el)
|
|
464
|
+
note_like = _is_note_like(y, cy, txt)
|
|
465
|
+
labeled = bool(ann_pat.search(txt))
|
|
466
|
+
bar = _is_bar_like(cy, cx)
|
|
467
|
+
# Fill rect sits on soWhatY (±0.02); labeled SO WHAT text may sit slightly below
|
|
468
|
+
on_fill = abs(y - so_what_y) <= int(0.02 * 914400) and bar
|
|
469
|
+
labeled_slot = labeled and _at_so_what_slot(y) and cy <= int(0.85 * 914400)
|
|
470
|
+
if note_like:
|
|
471
|
+
has_annotation = True
|
|
472
|
+
elif labeled_slot or on_fill:
|
|
473
|
+
has_annotation = True
|
|
474
|
+
has_so_what = True
|
|
475
|
+
|
|
476
|
+
if has_so_what:
|
|
477
|
+
band_top_in = min(nominal_band_top_in, so_what_y_in)
|
|
478
|
+
band_top = int(band_top_in * 914400)
|
|
479
|
+
|
|
480
|
+
# Pass 2: body invaders that START ABOVE the band and crush down (D9: all of them)
|
|
462
481
|
for el in elements:
|
|
463
482
|
box = shape_bounds(el)
|
|
464
483
|
if box is None:
|
|
@@ -471,10 +490,14 @@ def annotation_band_overlap_check(
|
|
|
471
490
|
continue
|
|
472
491
|
if _is_annotation_self(y, cy, cx, _text(el)):
|
|
473
492
|
continue
|
|
474
|
-
|
|
475
|
-
if
|
|
493
|
+
# Crush-from-above only: top must sit above band_top
|
|
494
|
+
if y >= band_top - int(0.02 * 914400):
|
|
495
|
+
continue
|
|
496
|
+
if bottom <= band_top:
|
|
497
|
+
continue
|
|
498
|
+
overlap = min(bottom, band_bot) - band_top
|
|
499
|
+
if overlap < min_overlap:
|
|
476
500
|
continue
|
|
477
|
-
# Enforce when annotation present, or when invasion is severe anyway
|
|
478
501
|
if has_annotation or overlap >= severe_overlap:
|
|
479
502
|
out.append(issue(
|
|
480
503
|
"ANNOTATION_BAND_OVERLAP",
|
|
@@ -483,7 +506,6 @@ def annotation_band_overlap_check(
|
|
|
483
506
|
"图例/系列请收入主图区或压缩系列数,禁止压进 so-what/来源行。",
|
|
484
507
|
slide=slide_no,
|
|
485
508
|
))
|
|
486
|
-
break
|
|
487
509
|
return out
|
|
488
510
|
|
|
489
511
|
|
|
@@ -795,7 +817,8 @@ def chrome_footer_y_in(shapes: list[ET.Element], height: int) -> float | None:
|
|
|
795
817
|
bottom_frac = float(chrome[1] if isinstance(chrome, list) and len(chrome) > 1 else 24) / 100.0
|
|
796
818
|
threshold = int(height * (1.0 - bottom_frac))
|
|
797
819
|
ys: list[float] = []
|
|
798
|
-
|
|
820
|
+
# Engine footer is always "N / M"; bare integers are chart/exhibit labels (CHROME_DRIFT FP)
|
|
821
|
+
pager = re.compile(r"^\s*\d+\s*/\s*\d+\s*$")
|
|
799
822
|
for shape in shapes:
|
|
800
823
|
text = (text_content(shape) or "").strip()
|
|
801
824
|
box = shape_bounds(shape)
|