@topmindspace/tms-skills 0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/tms-skills.js +160 -0
  5. package/package.json +48 -0
  6. package/top-ppt-html/README.md +254 -0
  7. package/top-ppt-html/SKILL.md +111 -0
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
  11. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
  17. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
  19. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
  21. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
  23. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
  25. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
  26. package/top-ppt-html/assets/pptx-export.js +1944 -0
  27. package/top-ppt-html/assets/style-gallery.html +589 -0
  28. package/top-ppt-html/assets/templates/architecture.html +3728 -0
  29. package/top-ppt-html/assets/templates/engine.css +840 -0
  30. package/top-ppt-html/assets/templates/presentation.html +3738 -0
  31. package/top-ppt-html/assets/templates/research.html +4017 -0
  32. package/top-ppt-html/assets/templates/ui.js +520 -0
  33. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  35. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  36. package/top-ppt-html/assets/theme-overview.png +0 -0
  37. package/top-ppt-html/evals/prompts.csv +15 -0
  38. package/top-ppt-html/evals/rubric.schema.json +25 -0
  39. package/top-ppt-html/evals/run_evals.py +220 -0
  40. package/top-ppt-html/evals/trace.example.json +16 -0
  41. package/top-ppt-html/package.json +35 -0
  42. package/top-ppt-html/references/charts-basic.md +624 -0
  43. package/top-ppt-html/references/charts-discipline.md +108 -0
  44. package/top-ppt-html/references/charts-extended.md +482 -0
  45. package/top-ppt-html/references/charts.md +28 -0
  46. package/top-ppt-html/references/components-atoms.md +624 -0
  47. package/top-ppt-html/references/components.md +30 -0
  48. package/top-ppt-html/references/content-rules.md +490 -0
  49. package/top-ppt-html/references/design-system-engine.md +235 -0
  50. package/top-ppt-html/references/design-system.md +478 -0
  51. package/top-ppt-html/references/failure-modes.md +214 -0
  52. package/top-ppt-html/references/high-fidelity.md +127 -0
  53. package/top-ppt-html/references/icons.md +397 -0
  54. package/top-ppt-html/references/industry-benchmark.md +105 -0
  55. package/top-ppt-html/references/infographics-stats.md +308 -0
  56. package/top-ppt-html/references/infographics-structure.md +226 -0
  57. package/top-ppt-html/references/infographics.md +43 -0
  58. package/top-ppt-html/references/layout-grammar.md +315 -0
  59. package/top-ppt-html/references/layouts-architecture.md +108 -0
  60. package/top-ppt-html/references/layouts-combo.md +600 -0
  61. package/top-ppt-html/references/layouts-research.md +160 -0
  62. package/top-ppt-html/references/modes.md +254 -0
  63. package/top-ppt-html/references/outline-design.md +275 -0
  64. package/top-ppt-html/references/playbook.md +266 -0
  65. package/top-ppt-html/references/pptx-export.md +209 -0
  66. package/top-ppt-html/references/reform-plan.md +252 -0
  67. package/top-ppt-html/references/styles.md +370 -0
  68. package/top-ppt-html/references/tech-design.md +138 -0
  69. package/top-ppt-html/scripts/audit_css.py +109 -0
  70. package/top-ppt-html/scripts/audit_docs.py +176 -0
  71. package/top-ppt-html/scripts/audit_skill.py +220 -0
  72. package/top-ppt-html/scripts/audit_styles.py +351 -0
  73. package/top-ppt-html/scripts/build_examples.py +2276 -0
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -0
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
  76. package/top-ppt-html/scripts/checks_html.py +127 -0
  77. package/top-ppt-html/scripts/cross_verify.py +294 -0
  78. package/top-ppt-html/scripts/env_probe.py +158 -0
  79. package/top-ppt-html/scripts/extract_model.py +210 -0
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -0
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -0
  82. package/top-ppt-html/scripts/layout-constants.json +3377 -0
  83. package/top-ppt-html/scripts/layout_slots.json +830 -0
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
  85. package/top-ppt-html/scripts/measure_height.py +178 -0
  86. package/top-ppt-html/scripts/model-schema.json +547 -0
  87. package/top-ppt-html/scripts/negative_tests.py +307 -0
  88. package/top-ppt-html/scripts/package_skill.py +291 -0
  89. package/top-ppt-html/scripts/prepare_images.py +341 -0
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -0
  91. package/top-ppt-html/scripts/quality_gate.py +301 -0
  92. package/top-ppt-html/scripts/regression.py +308 -0
  93. package/top-ppt-html/scripts/render_compare.py +275 -0
  94. package/top-ppt-html/scripts/render_from_model.py +698 -0
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -0
  96. package/top-ppt-html/scripts/section-file-map.json +105 -0
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -0
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -0
  99. package/top-ppt-html/scripts/validate_report.py +1456 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /* TopPPT HTML ยท ไธป้ข˜ๅ‚่€ƒๅ›พ็”Ÿๆˆๅ™จ๏ผˆ็ปดๆŠคๅทฅๅ…ท๏ผŒ้žไบคไป˜ไพ่ต–ใ€้žไบคไป˜็กฌ้—จ็ฆ๏ผ‰
3
+ *
4
+ * ็”จๆณ•๏ผš
5
+ * npm install playwright # ๆˆ–ๅœจไปปๆ„ไฝ็ฝฎๅฎ‰่ฃ… playwright ๅŽ่ฎพ็ฝฎ NODE_PATH
6
+ * npx playwright install chromium # ้ฆ–ๆฌก้œ€่ฆไธ‹่ฝฝๆต่งˆๅ™จ
7
+ * node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
8
+ *
9
+ * --scale = deviceScaleFactor๏ผˆ้ป˜่ฎค 1.25๏ผ‰ใ€‚ๅ‚่€ƒๅ›พๅช็”จไบŽ"่ฎฉ็”จๆˆท็œ‹่ง้ฃŽๆ ผไธŽไธป้ข˜"๏ผŒ
10
+ * 1.25 ๅœจ 1480px ่ง†ๅฃไธ‹ๅทฒ่ถณๅคŸๆธ…ๆ™ฐ๏ผŒๅŒๆ—ถๆŠŠๅ•ๅผ  PNG ๆŽงๅˆถๅœจ ~1MB ๅ†…๏ผˆไพฟไบŽๅœจ IDE ้‡Œๅฟซ้€Ÿๆ‰“ๅผ€๏ผ‰ใ€‚
11
+ *
12
+ * ๆต่งˆๅ™จ้€š้“่‡ชๅŠจๆŽขๆต‹๏ผšไผ˜ๅ…ˆๅ‘ฝไปค่กŒ --channel=๏ผŒๅ…ถๆฌก็Žฏๅขƒๅ˜้‡ TOP_PPT_BROWSER_CHANNEL๏ผŒ
13
+ * ๆœ€ๅŽๅ›ž่ฝๅˆฐ Playwright ่‡ชๅธฆ็š„ chromium๏ผˆๆ— ๆœฌๆœบๆต่งˆๅ™จไพ่ต–๏ผ‰ใ€‚
14
+ *
15
+ * ไบง็‰ฉ๏ผˆassets/๏ผŒ่ฆ†็›–ๆ—งๅ›พ๏ผ‰๏ผš
16
+ * theme-overview.png โ€”โ€” ้ป˜่ฎค๏ผˆๆผ”็คบ Tab๏ผ‰ๆ•ดไฝ“ๅ‚่€ƒๅ›พ
17
+ * theme-overview-presentation.png โ€”โ€” ๆผ”็คบๆจกๅผ Tab
18
+ * theme-overview-research.png โ€”โ€” ็ ”็ฉถๆจกๅผ Tab๏ผˆๅ’จ่ฏขๅฏ†ๆŽ’ๅฝขๆ€๏ผ‰
19
+ * theme-overview-architecture.png โ€”โ€” ๆžถๆž„ๆจกๅผ Tab๏ผˆๅ›พไธบ็Ž‹ๅฝขๆ€๏ผ‰
20
+ *
21
+ * ๆฏๆฌกๆˆชๅ›พๅ‰ๆ–ญ่จ€ๅฝ“ๅ‰ Tab ๆ–‡ๆกˆ๏ผŒ้˜ฒๆญข Tab ๅˆ‡ๆขๆœช็”Ÿๆ•ˆๅฏผ่‡ดไบงๅ‡บ้›ทๅŒๅ›พใ€‚ */
22
+ const path = require('path');
23
+ let chromium;
24
+ try {
25
+ ({ chromium } = require('playwright'));
26
+ } catch (e) {
27
+ console.error('ๆœชๆ‰พๅˆฐ playwrightใ€‚่ฏทๅ…ˆๅฎ‰่ฃ…๏ผšnpm install playwright && npx playwright install chromium');
28
+ console.error('๏ผˆๆœฌ่„šๆœฌไป…็”จไบŽๅˆทๆ–ฐ assets/theme-overview*.png ๅ‚่€ƒๅ›พ๏ผŒ้žๆŠ€่ƒฝไบคไป˜ไพ่ต–ใ€‚๏ผ‰');
29
+ process.exit(2);
30
+ }
31
+
32
+ const TAB_TEXT = { presentation: 'ๆผ”็คบๆฑ‡ๆŠฅ', research: '็ ”็ฉถๆŠฅๅ‘Š', architecture: 'ไฟกๆฏๆžถๆž„ๅ›พ' };
33
+ const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
34
+ const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
35
+ const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
36
+ const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
37
+ || process.env.TOP_PPT_BROWSER_CHANNEL || '';
38
+
39
+ (async () => {
40
+ const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
41
+ const outDir = path.resolve(__dirname, '../assets');
42
+ /* channel ไธบ็ฉบ โ†’ ็”จ Playwright ่‡ชๅธฆ chromium๏ผˆไธไพ่ต–ๆœฌๆœบๅทฒ่ฃ…ๆต่งˆๅ™จ๏ผ‰ */
43
+ const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
44
+ const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
45
+ await page.goto(gallery);
46
+ await page.waitForSelector('.preset');
47
+
48
+ const modes = ['presentation', 'research', 'architecture'];
49
+ for (const m of modes) {
50
+ const idx = modes.indexOf(m);
51
+ await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
52
+ await page.waitForTimeout(500);
53
+ const on = await page.evaluate(() =>
54
+ (document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
55
+ if (on !== TAB_TEXT[m]) {
56
+ throw new Error(`Tab ๅˆ‡ๆขๆ–ญ่จ€ๅคฑ่ดฅ๏ผšๆœŸๆœ›ใ€Œ${TAB_TEXT[m]}ใ€ๅฎž้™…ใ€Œ${on}ใ€๏ผˆ้˜ฒๆญข้›ทๅŒๅ›พๅ›žๅฝ’๏ผ‰`);
57
+ }
58
+ /* ๅก็‰‡ๆ•ฐๅฟ…้กป็ญ‰ไบŽๅ•ๆบ้ฃŽๆ ผๆ•ฐ๏ผˆPRESETS ็”ฑ sync_runtime.py ไปŽ layout-constants.json ๆณจๅ…ฅ๏ผ‰๏ผŒ
59
+ * ้˜ฒๆญขใ€Œๆ‰ฉไบ†้ฃŽๆ ผไฝ†็”ปๅปŠ/ๅ‚่€ƒๅ›พๆฒก่ทŸไธŠใ€็š„้™้ป˜ๆผ‚็งป๏ผˆๆ—งๆ–ญ่จ€ๅชๆŸฅ n < 9๏ผŒๅคชๅผฑ๏ผ‰ใ€‚ */
60
+ const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
61
+ const expected = await page.evaluate(() =>
62
+ (typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
63
+ if (expected && n !== expected) {
64
+ throw new Error(`็”ปๅปŠๅก็‰‡ๆ•ฐ ${n} โ‰  ๅ•ๆบ้ฃŽๆ ผๆ•ฐ ${expected}๏ผˆ็”ปๅปŠไธŽ layout-constants.json ๆผ‚็งป๏ผ‰`);
65
+ }
66
+ if (n < 9) throw new Error(`ๅก็‰‡ๆ•ฐๅผ‚ๅธธ๏ผš${n}`);
67
+ await page.screenshot({ path: path.join(outDir, `theme-overview-${m}.png`), fullPage: true });
68
+ console.log(`captured: theme-overview-${m}.png (tab=${on}, cards=${n})`);
69
+ }
70
+
71
+ /* ไธปๅ›พๅ›žๅˆฐๆผ”็คบ Tab๏ผˆ้ป˜่ฎคๆ€๏ผ‰ */
72
+ await page.evaluate(() => document.querySelectorAll('.mtab')[0].click());
73
+ await page.waitForTimeout(500);
74
+ await page.screenshot({ path: path.join(outDir, 'theme-overview.png'), fullPage: true });
75
+ console.log('captured: theme-overview.png (default presentation tab)');
76
+ await browser.close();
77
+ console.log(`done: 4 ๅผ ไธป้ข˜ๅ‚่€ƒๅ›พๅทฒๅˆทๆ–ฐ๏ผˆdeviceScaleFactor=${SCALE}๏ผ‰`);
78
+ })().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML ยท HTML ๆฃ€ๆŸฅๅ…ฌๅ…ฑๅบ“๏ผˆๅ•ๆบ๏ผšๆ‰ฟ่ฝฝๆญฃๅˆ™ / ๅ›พ่กจ้€š้“ / ๅคšๆ ทๆ€งไธ‹้™ / ็ป„ๅˆ็‰ˆๅผ๏ผ‰
4
+
5
+ ๆถˆ่ดนๆ–น๏ผšvalidate_report.py ยท quality_gate.py ยท evals/run_evals.py
6
+ ็ฆๆญขๅœจๆถˆ่ดนๆ–นๅ†ๆ‰‹ๆŠ„ CARRIERS / ๅคšๆ ทๆ€งๅ…ฌๅผ / ็ป„ๅˆ็‰ˆๅผๅˆคๅฎšใ€‚
7
+ ้˜ˆๅ€ผไป่ฏป layout-constants.json๏ผˆcharts.variety / charts.registry๏ผ‰ใ€‚
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ _LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
17
+
18
+ # ๆ‰ฟ่ฝฝ็ฑปๅž‹ๆญฃๅˆ™๏ผˆ็ป„ๅˆ็‰ˆๅผ๏ผšไธ€้กตๆ˜ฏๅฆๅซ โ‰ฅ2 ็งๆ‰ฟ่ฝฝ๏ผ‰
19
+ CARRIERS = [
20
+ r'<svg\b[^>]*data-chart=',
21
+ r'class="tbl-wrap',
22
+ r'class="card[\s"]',
23
+ r'class="metric[\s"]',
24
+ r'class="media[\s"]',
25
+ r'class="arch[\s"]',
26
+ r'class="lane[\s"]',
27
+ r'class="matrix[\s"]',
28
+ r'class="heat[\s"]',
29
+ r'class="bul[\s"]',
30
+ r'class="pyr[\s"]',
31
+ r'class="steps[\s"]',
32
+ r'class="exhibit[\s"]',
33
+ r'class="cols-2',
34
+ r'class="cols-3',
35
+ ]
36
+
37
+ SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
38
+
39
+
40
+ def load_lc() -> dict:
41
+ return json.loads(_LC_PATH.read_text(encoding='utf-8'))
42
+
43
+
44
+ def chart_registry(lc: dict | None = None) -> dict:
45
+ lc = lc or load_lc()
46
+ return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
47
+ if isinstance(v, dict)}
48
+
49
+
50
+ def chart_variety(lc: dict | None = None) -> dict:
51
+ lc = lc or load_lc()
52
+ return (lc.get('charts') or {}).get('variety') or {}
53
+
54
+
55
+ def bands(html: str) -> list[str]:
56
+ return re.split(r'(?=<section class="band)', html)[1:]
57
+
58
+
59
+ def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
60
+ out = []
61
+ for b in bands(html):
62
+ head = b[:240]
63
+ if any(f'id="{i}"' in head for i in skip_ids):
64
+ continue
65
+ out.append(b)
66
+ return out
67
+
68
+
69
+ def chart_types_per_page(html: str) -> list[list[str]]:
70
+ return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
71
+
72
+
73
+ def distinct_chart_types(html: str) -> list[str]:
74
+ used = [t for ts in chart_types_per_page(html) for t in ts]
75
+ return sorted(set(used))
76
+
77
+
78
+ def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
79
+ """็ฑปๅž‹ๆ•ฐไธ‹้™ = min(ๆจกๅผไธŠ้™, max(1, โŒˆๅ›พ่กจ้กตๆ•ฐ ร— ratioโŒ‰))"""
80
+ v = variety if variety is not None else chart_variety()
81
+ floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
82
+ ratio = float(v.get('minTypesRatio') or 0.6)
83
+ return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
84
+
85
+
86
+ def adjacent_same_type(pages: list[list[str]]) -> list[str]:
87
+ """็›ธ้‚ปๅ›พ่กจ้กตๅŒๅž‹ๅ‘Š่ญฆๆ–‡ๆกˆๅˆ—่กจใ€‚"""
88
+ adj, prev, prev_i = [], None, 0
89
+ for i, ts in enumerate(pages):
90
+ if not ts:
91
+ continue
92
+ if prev and set(ts) & set(prev):
93
+ adj.append(f'็ฌฌ {prev_i + 1}/{i + 1} ้กตๅŒๅž‹ {sorted(set(ts) & set(prev))}')
94
+ prev, prev_i = ts, i
95
+ return adj
96
+
97
+
98
+ def carrier_count(band: str) -> int:
99
+ return sum(1 for pat in CARRIERS if re.search(pat, band))
100
+
101
+
102
+ def composite_pages(html: str) -> tuple[int, int]:
103
+ """่ฟ”ๅ›ž (ๅซ โ‰ฅ2 ็งๆ‰ฟ่ฝฝ็š„ๅ†…ๅฎน้กตๆ•ฐ, ๅ†…ๅฎน้กตๆ€ปๆ•ฐ)ใ€‚"""
104
+ content = content_bands(html)
105
+ multi = sum(1 for b in content if carrier_count(b) >= 2)
106
+ return multi, len(content)
107
+
108
+
109
+ def composite_required(mode: str, variety: dict | None = None) -> float:
110
+ """็ป„ๅˆ็‰ˆๅผๆฏ”ไพ‹ไธ‹้™๏ผ›้ž compositeModes ่ฟ”ๅ›ž 0ใ€‚"""
111
+ v = variety if variety is not None else chart_variety()
112
+ if mode not in (v.get('compositeModes') or ['research']):
113
+ return 0.0
114
+ return float(v.get('compositeMinRatio') or 0)
115
+
116
+
117
+ def chart_channel(chart_type: str, reg: dict | None = None) -> str:
118
+ reg = reg if reg is not None else chart_registry()
119
+ spec = reg.get(str(chart_type or 'bar').lower()) or {}
120
+ return str(spec.get('pptx') or 'native')
121
+
122
+
123
+ def chart_datatable_mode(chart: dict, reg: dict | None = None) -> str:
124
+ reg = reg if reg is not None else chart_registry()
125
+ spec = reg.get(str(chart.get('type') or 'bar').lower()) or {}
126
+ mode = str(chart.get('dataTable') or spec.get('dataTable') or 'notes').lower()
127
+ return 'inline' if mode == 'appendix' else mode
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML ยท ๅŒ้€š้“ไบคๅ‰้ชŒ่ฏ๏ผˆๅฏ้€‰ไพ่ต– python-pptx๏ผ‰๏ผš
4
+ โ‘  A ้€š้“๏ผˆๆ‰‹ๅ†™ OOXML๏ผ‰ไธŽ B ้€š้“๏ผˆpptxgenjs๏ผ‰ไบง็‰ฉๅ‡ๅฏ่ขซ python-pptx ไธฅๆ ผ่งฃๆž
5
+ โ‘ก ไธค้€š้“้€้กตๆ–‡ๆœฌ้›†ๅˆไธ€่‡ด๏ผˆๅŒไธ€ๆจกๅž‹ โ†’ ๅŒไธ€ๅ†…ๅฎน๏ผ›ๆฎต่ฝๆข่กŒ่กจ็คบๅทฎๅผ‚ๅทฒๅฝ’ไธ€๏ผ‰
6
+ ๅฝ’ไธ€ๅŒ–๏ผšB ้€š้“ๅธฆๆ•ฐๆฎ็š„ๅ›พ่กจไธบๅŽŸ็”Ÿ chart part๏ผˆๅฏ็ผ–่พ‘ๆ•ฐๆฎ๏ผ‰โ€”โ€”ๅ›พ่กจ็ฑปๅˆซๆ ‡็ญพๅนถๅ…ฅ
7
+ ๆ–‡ๆœฌ้›†ๅˆใ€็บฏๆ•ฐๅ€ผ token๏ผˆๅซ็™พๅˆ†ๆ•ฐ๏ผ‰ไธค็ซฏ่ฟ‡ๆปค๏ผˆA ้€š้“ๅฝข็Šถ็‰ˆ็š„ๆ•ฐๅ€ผๆ ‡็ญพ vs B ้€š้“ chart ๆ•ฐๆฎๅ€ผ๏ผ‰
8
+ โ‘ข ๆ–‡ๆœฌๆบขๅ‡บๅฏๅ‘ๅผ๏ผšๆ–‡ๆœฌไผฐ็ฎ—ๅฎฝ vs shape ๅฎฝ ร— ๅฏๅฎน่กŒๆ•ฐ
9
+ โ‘ฃ B ้€š้“ๅŽŸ็”Ÿๅ›พ่กจๆ•ฐๅ€ผ โ†” ๆจกๅž‹ๆ•ฐๅ€ผ๏ผˆ็ฑปๅˆซๆ ‡็ญพไธ€่‡ดไธไปฃ่กจๆ•ฐๅ€ผไธ€่‡ด๏ผ›ๆ•ฐๅ€ผ้”™ไนฑๅช่ƒฝ่ฟ™้‡Œๆ‹“๏ผ‰
10
+ โ‘ค ๅญ—ๅทๆฏ”ไพ‹ๅฐบๅŒๆบ๏ผšB ็š„ๅญ—ๅทๅฟ…้กปๆ˜ฏ A ๅญ—ๅท้›†็š„ๅญ้›†๏ผˆ้˜ฒ modeSize()/sz() ๅŽๅค„็†ๆผ‚็งป๏ผ‰
11
+ ็”จๆณ•๏ผšๅ…ˆ node scripts/gen_channel_a.js ็”Ÿๆˆ A ้€š้“ไบง็‰ฉ๏ผŒๅ† python scripts/cross_verify.py
12
+ ๆ”น pptx-export.js ๅบๅˆ—ๅŒ–้ชจๆžถๅŽๅฟ…่ท‘๏ผˆ่ง references/pptx-export.md ๅŒ่ฃๅˆคๆ•™่ฎญ๏ผ‰ใ€‚
13
+ """
14
+ import json
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
20
+ # ไพ่ต–็ผบๅคฑไธๅœจๅฏผๅ…ฅๆœŸ้€€ๅ‡บโ€”โ€”ๆœฌๆจกๅ—็š„ๆ ธๅฏนๅ‡ฝๆ•ฐไผš่ขซ negative_tests.py ็›ดๆŽฅๅค็”จ
21
+ try:
22
+ from pptx import Presentation
23
+ except ImportError:
24
+ Presentation = None
25
+
26
+ ROOT = Path(__file__).resolve().parent.parent
27
+ A_DIR = ROOT / 'dist' / 'regression-a' # A ้€š้“๏ผˆ้กต้ข็›ดๅฏผๅผ•ๆ“Ž๏ผ‰
28
+ B_DIR = ROOT / 'dist' / 'regression' # B ้€š้“๏ผˆpptxgenjs๏ผ‰
29
+ MODEL_DIR = ROOT / 'assets' / 'examples'
30
+
31
+ # ๆ–‡ๆœฌๅบฆ้‡ๅ•ๆบ๏ผˆไธŽ build_pptx.js / validate_pptx.py ๅŒๅ€ผ๏ผ‰โ€”โ€”ไธ‰ไพงๅ„่‡ช็กฌ็ผ–็ ไผš่ฎฉ
32
+ # ๆบขๅ‡บๅˆคๅฎšๅœจไธๅŒ่ฃๅˆค้—ด้ฃ˜็งป๏ผŒๅ‡บ็Žฐโ€œไธ€ไพงๆŠฅๆบขๅ‡บใ€ไธ€ไพง่ฏดๆฒกไบ‹โ€ใ€‚
33
+ try:
34
+ _TM = (json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
35
+ .get('containers', {}).get('textMetrics', {}))
36
+ except (OSError, json.JSONDecodeError):
37
+ _TM = {}
38
+ LINE_FACTOR = float(_TM.get('lineFactor') or 1.45)
39
+ EM_ASCII_RATIO = float(_TM.get('emAsciiRatio') or 0.52)
40
+
41
+ # ๅฝขๆ€ๅ˜ๆข็ฑปๅ›พ่กจ๏ผšๆจกๅž‹้‡Œ็š„่ดŸๅ€ผๅœจๅ›พไธŠๆ˜ฏใ€Œๅ‘ไธ‹็š„ๆญฃ้ซ˜ๅบฆๆกใ€๏ผŒๆŒ‰็ปๅฏนๅ€ผๆฏ”ๅฏน
42
+ SIGNLESS_CHARTS = {'waterfall', 'gauge', 'funnel', 'bullet'}
43
+
44
+ # ็บฏๆ•ฐๅ€ผ token๏ผˆๅซๅƒๅˆ†ไฝ/ๅฐๆ•ฐ/็™พๅˆ†ๅท/ๅธธ่งๅ•ไฝๅŽ็ผ€๏ผ‰โ€”โ€”ๅ›พ่กจๆ•ฐๆฎๆ ‡็ญพๅœจ A ้€š้“ๆ˜ฏ
45
+ # ๆ–‡ๆœฌ๏ผˆๅ€ผ+unit๏ผŒๅฆ‚ "9pt"๏ผ‰๏ผŒB ้€š้“่ฟ› chart part๏ผ›ๆฏ”ๅฏนๅ‰ไธค็ซฏ่ฟ‡ๆปคใ€‚็ฑปๅˆซๆ ‡็ญพ๏ผˆๅฆ‚ "2026Q3"๏ผ‰
46
+ # ไธๅŒน้…ๆญคๆจกๅผ๏ผŒไปๅ‚ไธŽไธ€่‡ดๆ€งๆฃ€ๆŸฅใ€‚
47
+ NUMERIC_TOKEN = re.compile(
48
+ r'^[+-]?[\d.,]+\s*(?:%|โ€ฐ|pt|pct|pp|TB|GB|MB|KB|k|w|ไธ‡|ไบฟ|ๅ…ƒ|ๅ€|ไธช|ไบบ|ๅคฉ|ๅฎถ|ๆฌก|้กน|ๅนด'
49
+ r'|ๆก|็ฌ”|ๅฐ|ๅฅ—|ไปฝ|ๆฌพ|ๅผ |็ฏ‡|้กต|ไปถ|ไพ‹|ๅœบ|่ฝฎ|่‰˜|่พ†|ๅจ|็ฑณ|็ง’|ๅˆ†|ๆ—ถ|ๆกฃ|็บง)?$')
50
+
51
+
52
+ def slide_texts(prs):
53
+ """ๆฏ้กตๆ–‡ๆœฌ้›†ๅˆ๏ผˆ้€่กŒๆ‹†ๅˆ†โ€”โ€”A ้€š้“ๆฎต่ฝ \\n ไธŽ B ้€š้“ breakLine \\n\\n ่กจ็คบๅทฎๅผ‚ๅฝ’ไธ€๏ผ›
54
+ ๅฟฝ็•ฅ็ฉบ็™ฝใ€้กต็ ไธŽ็บฏๆ•ฐๅ€ผ token๏ผ›B ้€š้“ๅŽŸ็”Ÿๅ›พ่กจ็š„็ฑปๅˆซๆ ‡็ญพๅนถๅ…ฅ้›†ๅˆ๏ผ‰ใ€‚"""
55
+ pages = []
56
+ for slide in prs.slides:
57
+ texts = []
58
+ for shape in slide.shapes:
59
+ if shape.has_text_frame:
60
+ for line in shape.text_frame.text.splitlines():
61
+ t = line.strip()
62
+ if t and not NUMERIC_TOKEN.match(t):
63
+ texts.append(t)
64
+ elif shape.has_table:
65
+ for row in shape.table.rows:
66
+ for cell in row.cells:
67
+ for line in cell.text.splitlines():
68
+ t = line.strip()
69
+ if t and not NUMERIC_TOKEN.match(t):
70
+ texts.append(t)
71
+ elif getattr(shape, 'has_chart', False):
72
+ # B ้€š้“ๅŽŸ็”Ÿๆ•ฐๆฎๅ›พ่กจโ€”โ€”็ฑปๅˆซๆ ‡็ญพ๏ผˆๅฎž่ดจๅ†…ๅฎน๏ผ‰ๅนถๅ…ฅ๏ผ›ๆ•ฐๆฎๅ€ผๅทฒๆŒ‰็บฏๆ•ฐๅ€ผ่ฟ‡ๆปคใ€‚
73
+ # ็ฑปๅˆซๆ ‡็ญพๅŒๆ ท่ฟ‡ NUMERIC_TOKENโ€”โ€”A ้€š้“ๆ–‡ๆœฌ็ปŸไธ€่ฟ‡ๆปค๏ผŒไธค็ซฏ่ฏญไน‰ๅฏน็งฐ
74
+ # ๏ผˆๅฆๅˆ™ gauge ็š„ '76%'ใ€stack ็š„ๅนดไปฝ '2023' ่ฟ™็ฑปๆ•ฐๅ€ผๅž‹็ฑปๅˆซไผš้€ ๆˆๅ•่พน่ฏฏๆŠฅ๏ผ‰ใ€‚
75
+ try:
76
+ cats = list(shape.chart.plots[0].categories)
77
+ texts.extend(str(c).strip() for c in cats
78
+ if str(c).strip() and not NUMERIC_TOKEN.match(str(c).strip()))
79
+ except Exception:
80
+ pass
81
+ pages.append(sorted(set(texts))) # ้›†ๅˆ่ฏญไน‰ๅŽป้‡๏ผˆB ็ซฏ chart categories ไธŽ่‡ช็ป˜ๅ›พไพ‹ๅฏ่ƒฝ้‡ๅค๏ผ‰
82
+ return pages
83
+
84
+
85
+ def _model_chart(sec):
86
+ """ๅ–็ซ ่Š‚้กต็š„ๅ›พ่กจๅฏน่ฑก๏ผˆๅฃๅพ„ไธŽ validate_pptx._section_chart ไธ€่‡ด๏ผ‰ใ€‚"""
87
+ st = sec.get('type') or ''
88
+ if st in {'bar', 'donut', 'exhibit', 'halftable'}:
89
+ c = sec.get('chart') or {}
90
+ elif st == 'split':
91
+ c = sec.get('right') or {}
92
+ if (c.get('type') or 'bar') == 'table':
93
+ return None
94
+ else:
95
+ return None
96
+ return c if isinstance(c, dict) and c.get('labels') and c.get('values') else None
97
+
98
+
99
+ def _model_values(chart):
100
+ """ๆจกๅž‹ๅฃฐๆ˜Ž็š„ๅ…จ้ƒจๆ•ฐๅ€ผ๏ผˆๅ•็ณปๅˆ— values ๆˆ–ๅคš็ณปๅˆ— series[].values๏ผ‰ใ€‚"""
101
+ out = []
102
+ if isinstance(chart.get('series'), list) and chart['series']:
103
+ for se in chart['series']:
104
+ out += [v for v in (se.get('values') or []) if isinstance(v, (int, float))]
105
+ else:
106
+ out += [v for v in (chart.get('values') or []) if isinstance(v, (int, float))]
107
+ return out
108
+
109
+
110
+ def chart_data_verify(prs, model, name):
111
+ """B ้€š้“ๅŽŸ็”Ÿ chart part ็š„ๆ•ฐๅ€ผๅฟ…้กป่ฆ†็›–ๆจกๅž‹ๅฃฐๆ˜Žๅ€ผใ€‚
112
+
113
+ ๅชๅšใ€Œๆจกๅž‹ๅ€ผ โІ ๅ›พ่กจๅ€ผใ€็š„ๅŒ…ๅซๅˆคๅฎš๏ผŒไธๅšไธฅๆ ผ็›ธ็ญ‰โ€”โ€”waterfall ็š„็ดฏ่ฎกๅŸบๅบงใ€
114
+ gauge ็š„่กฅ่ง’่ฟ™็ฑปๆดพ็”Ÿ็ณปๅˆ—ๆ˜ฏๅˆๆณ•็š„้ขๅค–ๆ•ฐๆฎ๏ผŒไธฅๆ ผ็›ธ็ญ‰ไผšๅˆถ้€ ๅ‡้˜ณๆ€งใ€‚
115
+ ๅฝขๆ€ๅ˜ๆข็ฑปๅ›พ่กจ๏ผˆwaterfall ๆŠŠ -3 ็”ปๆˆ้ซ˜ๅบฆ 3 ็š„ๆ‚ฌๆตฎๆก๏ผ‰ๆŒ‰็ปๅฏนๅ€ผๆฏ”ๅฏนใ€‚
116
+ """
117
+ issues = []
118
+ secs = [s for s in (model.get('sections') or []) if isinstance(s, dict)]
119
+ first = 3 if model.get('agenda') else 2
120
+ slides = list(prs.slides)
121
+ for i, sec in enumerate(secs):
122
+ chart = _model_chart(sec)
123
+ if chart is None:
124
+ continue
125
+ idx = first + i - 1
126
+ if idx >= len(slides):
127
+ break
128
+ pool = []
129
+ for shape in slides[idx].shapes:
130
+ if not getattr(shape, 'has_chart', False):
131
+ continue
132
+ try:
133
+ for plot in shape.chart.plots:
134
+ for se in plot.series:
135
+ pool += [v for v in se.values if v is not None]
136
+ except Exception:
137
+ pass
138
+ if not pool:
139
+ continue # ่ฏฅ้กตๆ˜ฏๅฝข็Šถ่ฟ˜ๅŽŸ้€š้“๏ผŒๆ•ฐๅ€ผ่ตฐๆ•ฐๆฎ่กจ/ๅค‡ๆณจ๏ผŒๅฆๆœ‰้—จ็ฆ
140
+ ctype = str(chart.get('type') or 'bar').lower()
141
+ signless = ctype in SIGNLESS_CHARTS
142
+ if signless:
143
+ pool = [abs(p) for p in pool]
144
+ missing = []
145
+ for v in _model_values(chart):
146
+ target = abs(v) if signless else v
147
+ if not any(abs(target - p) <= max(0.01, abs(target) * 0.001) for p in pool):
148
+ missing.append(v)
149
+ if missing:
150
+ issues.append(f'{name} ็ฌฌ{idx + 1}้กต {ctype} ๅ›พ่กจๆ•ฐๅ€ผไธŽๆจกๅž‹ไธ็ฌฆ๏ผŒ'
151
+ f'็ผบ {missing[:5]}๏ผˆๅ›พ่กจๅฎž้™…ๅซ {sorted(set(pool))[:6]}๏ผ‰')
152
+ return issues
153
+
154
+
155
+ def font_sizes(prs):
156
+ """ๅ…จๆ–‡ๆ–‡ๆœฌๆก†ๅ‡บ็Žฐ่ฟ‡็š„ๅญ—ๅท้›†ๅˆ๏ผˆpt๏ผ‰ใ€‚"""
157
+ out = set()
158
+ for slide in prs.slides:
159
+ for sh in slide.shapes:
160
+ if not sh.has_text_frame:
161
+ continue
162
+ for p in sh.text_frame.paragraphs:
163
+ for r in p.runs:
164
+ if r.font.size and r.text.strip():
165
+ out.add(round(r.font.size.pt, 2))
166
+ return out
167
+
168
+
169
+ def overflow_estimate(prs, name):
170
+ """ๅฏๅ‘ๅผ๏ผšๆ–‡ๆœฌไผฐ็ฎ—ๅฎฝ๏ผˆCJK ๅญ—็ฌฆๆŒ‰ 1emใ€ASCII ๆŒ‰ emAsciiRatio๏ผ‰vs shape ๅฎฝ ร— ๅฏๅฎน่กŒๆ•ฐใ€‚"""
171
+ issues = []
172
+ for si, slide in enumerate(prs.slides, 1):
173
+ for shape in slide.shapes:
174
+ if not shape.has_text_frame or not shape.width or not shape.height:
175
+ continue
176
+ for para in shape.text_frame.paragraphs:
177
+ line = ''.join(r.text for r in para.runs)
178
+ if not line.strip():
179
+ continue
180
+ sz = None
181
+ for r in para.runs:
182
+ if r.font.size:
183
+ sz = r.font.size.pt
184
+ break
185
+ sz = sz or 12
186
+ emu_w = int(shape.width)
187
+ in_w = emu_w / 914400
188
+ # ๆฏ่กŒๅฏๅฎนๅญ—็ฌฆๅฎฝ๏ผˆ่‹ฑๅฏธ๏ผ‰โ†’ em ๆ•ฐ
189
+ est_w = sum(1.0 if ord(c) > 0x2E80 else EM_ASCII_RATIO for c in line) * sz / 72
190
+ lines_avail = max(1, int(int(shape.height) / 914400 / (sz / 72 * LINE_FACTOR)))
191
+ if est_w > in_w * lines_avail * 1.18: # 18% ๅฎนๅทฎ
192
+ issues.append(f'{name} ็ฌฌ{si}้กต "{line[:14]}โ€ฆ" ไผฐ็ฎ—{est_w:.1f}in > ๅฎน้‡{in_w*lines_avail:.1f}in')
193
+ return issues
194
+
195
+
196
+ def main() -> int:
197
+ if Presentation is None:
198
+ print('python-pptx ๆœชๅฎ‰่ฃ…')
199
+ return 2
200
+ fails = []
201
+ a_files = sorted(A_DIR.glob('*.pptx'))
202
+ print(f'A ้€š้“ไบง็‰ฉ {len(a_files)} ไธช ยท B ้€š้“ไบง็‰ฉ {len(sorted(B_DIR.glob("*.pptx")))} ไธช\n')
203
+ all_overflow = []
204
+
205
+ for ap in a_files:
206
+ bp = B_DIR / ap.name
207
+ name = ap.stem
208
+ # โ‘  ๅฏ่งฃๆžๆ€ง
209
+ try:
210
+ prs_a = Presentation(str(ap))
211
+ except Exception as e:
212
+ fails.append(f'{name}: A ้€š้“ python-pptx ่งฃๆžๅคฑ่ดฅ {e}')
213
+ continue
214
+ if not bp.exists():
215
+ fails.append(f'{name}: B ้€š้“ไบง็‰ฉ็ผบๅคฑ')
216
+ continue
217
+ try:
218
+ prs_b = Presentation(str(bp))
219
+ except Exception as e:
220
+ fails.append(f'{name}: B ้€š้“ python-pptx ่งฃๆžๅคฑ่ดฅ {e}')
221
+ continue
222
+ # โ‘ก ้€้กตๆ–‡ๆœฌไธ€่‡ดๆ€ง๏ผˆv9๏ผšA/B ไธๅฟ…้€ๅญ—ๅ…จ็ญ‰โ€”โ€”A ้ข„่งˆไธบๅฝข็Šถ่ฟ‘ไผผ๏ผ›
223
+ # ่ฆๆฑ‚ใ€Œๆ ‡้ข˜+ไธปๆ–‡ๆœฌ โ‰ฅ80% ไบค้›†่ฆ†็›–ใ€๏ผŒๆœ็ปๆˆชๆ–ญ/ไธฒ้กต๏ผŒๅˆไธๅ› ๅ›พ่กจๆ ‡็ญพๅฝขๆ€ๅทฎ่ฏฏๆ€๏ผ‰
224
+ ta, tb = slide_texts(prs_a), slide_texts(prs_b)
225
+ if len(ta) != len(tb):
226
+ fails.append(f'{name}: ้กตๆ•ฐไธไธ€่‡ด A={len(ta)} B={len(tb)}')
227
+ continue
228
+ min_cov = 0.80
229
+ try:
230
+ min_cov = float((json.loads((ROOT / 'scripts' / 'layout-constants.json')
231
+ .read_text(encoding='utf-8'))
232
+ .get('qualityGates') or {}).get('roundtripMin') or 0.8)
233
+ except Exception:
234
+ pass
235
+ diff_pages = []
236
+ for i, (x, y) in enumerate(zip(ta, tb), 1):
237
+ sx, sy = set(x), set(y)
238
+ if not sx and not sy:
239
+ continue
240
+ inter = sx & sy
241
+ cov = len(inter) / max(1, max(len(sx), len(sy)))
242
+ if cov < min_cov:
243
+ diff_pages.append((i, cov, sorted(sx - sy)[:3], sorted(sy - sx)[:3]))
244
+ if diff_pages:
245
+ for i, cov, only_a, only_b in diff_pages[:3]:
246
+ fails.append(f'{name}: ็ฌฌ{i}้กตๆ–‡ๆœฌ่ฆ†็›– {cov:.0%} <{min_cov:.0%} '
247
+ f'A็‹ฌๆœ‰{only_a} B็‹ฌๆœ‰{only_b}')
248
+ # โ‘ข ๆบขๅ‡บๅฏๅ‘ๅผ๏ผˆๅฏนไธค้€š้“้ƒฝ่ท‘๏ผ‰
249
+ all_overflow += overflow_estimate(prs_a, name + '/A')
250
+ all_overflow += overflow_estimate(prs_b, name + '/B')
251
+ # โ‘ฃ B ้€š้“ๅŽŸ็”Ÿๅ›พ่กจๆ•ฐๅ€ผ โ†” ๆจกๅž‹๏ผˆA ้€š้“ๆ˜ฏๅฝข็Šถ่ฟ˜ๅŽŸ๏ผŒไธไบง chart part๏ผ‰
252
+ chart_note = ''
253
+ mp = MODEL_DIR / f'{name}.model.json'
254
+ if mp.exists():
255
+ try:
256
+ model = json.loads(mp.read_text(encoding='utf-8'))
257
+ except json.JSONDecodeError as e:
258
+ fails.append(f'{name}: ๆจกๅž‹ไธๅฏ่งฃๆž {e}')
259
+ model = None
260
+ if model:
261
+ data_issues = chart_data_verify(prs_b, model, name)
262
+ fails += data_issues
263
+ chart_note = ' ๅ›พ่กจๆ•ฐๅ€ผ=' + ('PASS' if not data_issues else f'DIFF({len(data_issues)})')
264
+ status = 'PASS' if not diff_pages else f'LOWCOV p{[d[0] for d in diff_pages]}'
265
+ # โ‘ค ๅญ—ๅทๆฏ”ไพ‹ๅฐบๅŒๆบ๏ผšB ็š„ๅญ—ๅทๅฟ…้กป้ƒฝๆฅ่‡ช A ๅŒไธ€ๅผ ่กจใ€‚
266
+ # ๅๅ‘ไธๆˆ็ซ‹โ€”โ€”A ๆŠŠๅ›พ่กจๆธฒๆŸ“ๆˆๅฝข็Šถ๏ผŒๆ ‡็ญพๅญ—ๅทไผšๅคšๅ‡บๆฅ๏ผˆB ็š„ๅœจ chart part ๅ†…๏ผ‰ใ€‚
267
+ fa, fb = font_sizes(prs_a), font_sizes(prs_b)
268
+ alien = sorted(fb - fa)
269
+ if alien:
270
+ fails.append(f'{name}: B ้€š้“ๅ‡บ็Žฐ A ๆฒกๆœ‰็š„ๅญ—ๅท {alien}'
271
+ f'๏ผˆไธค้€š้“ๅบ”ๅŒ่ตฐ modeTypeScale๏ผ›modeSize() ไธŽ sz() ็š„้’ณๅˆถ/ๅ–ๆ•ด้กปไธ€่‡ด๏ผ‰')
272
+ sz_note = ' ๅญ—ๅทๆฏ”ไพ‹ๅฐบ=' + ('PASS' if not alien else 'DRIFT')
273
+ print(f'{name}: Aๅฏ่งฃๆžโœ“ Bๅฏ่งฃๆžโœ“ ้กตๆ•ฐ={len(ta)} ้€้กตๆ–‡ๆœฌ={status}{chart_note}{sz_note}')
274
+
275
+ print('\nโ”€โ”€ ๆบขๅ‡บๅฏๅ‘ๅผ๏ผˆcheck-overflow ยท 18% ๅฎนๅทฎ๏ผ‰โ”€โ”€')
276
+ if all_overflow:
277
+ for i in all_overflow[:20]:
278
+ print(' [WARN]', i)
279
+ else:
280
+ print(' ไธค้€š้“ๅ…จ้ƒจ้€š่ฟ‡๏ผˆๆ— ๆ–‡ๆœฌ่ถ…ๅฎน shape๏ผ‰')
281
+
282
+ print('\n' + '=' * 50)
283
+ if fails:
284
+ print('ไบคๅ‰้ชŒ่ฏๅคฑ่ดฅ:')
285
+ for f in fails:
286
+ print(' [FAIL]', f)
287
+ return 1
288
+ print('ๅŒ้€š้“ไบคๅ‰้ชŒ่ฏ้€š่ฟ‡๏ผšpython-pptx ็ฌฌไธ‰ๆ–น่ฃๅˆค็กฎ่ฎค A/B ไบง็‰ฉๅ‡ๅฏ่งฃๆžใ€'
289
+ '้€้กตๆ–‡ๆœฌไธ€่‡ดใ€ๅŽŸ็”Ÿๅ›พ่กจๆ•ฐๅ€ผไธŽๆจกๅž‹ไธ€่‡ด')
290
+ return 0
291
+
292
+
293
+ if __name__ == '__main__':
294
+ sys.exit(main())
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML ยท ่ฟ่กŒ็ŽฏๅขƒๆŽขๆต‹๏ผˆๅ…ฌๅ…ฑๆจกๅ— ยท ่ขซ regression / probe_image_export ็ญ‰ๅค็”จ๏ผ‰
4
+
5
+ ็ปŸไธ€ Node / pptxgenjs / python-pptx ็š„ๆŽขๆต‹ๅฃๅพ„๏ผŒ้ฟๅ…ๅ„่„šๆœฌๅ„ๅ†™ไธ€ไปฝๅฏผ่‡ดๆผ‚็งปใ€‚
6
+ **ไธ็ป‘ๅฎšไปปไฝ•ๆœบๅ™จ็š„ๅ›บๅฎš่ทฏๅพ„**๏ผš็Žฏๅขƒๅ˜้‡ > PATH > ๅธธ่งๆ‰˜็ฎก็›ฎๅฝ•๏ผˆๅญ˜ๅœจๆ‰็”จ๏ผŒไธๅญ˜ๅœจไธๅฝฑๅ“๏ผ‰ใ€‚
7
+
8
+ ่งฃๆžไผ˜ๅ…ˆ็บง๏ผš
9
+ Node ๅฏๆ‰ง่กŒๆ–‡ไปถ
10
+ TOP_PPT_NODE_EXE > NODE_EXE > NODE
11
+ > PATH ไธŠ็š„ node > ๅธธ่งๆ‰˜็ฎก็›ฎๅฝ•ๆœ€ๆ–ฐ็‰ˆ
12
+ ๅซ pptxgenjs ็š„ node_modules
13
+ TOP_PPT_NODE_PATH > NODE_PATH
14
+ > ไป“ๅบ“ๅ†… node_modules > ้€็บง็ˆถ็›ฎๅฝ• node_modules > ๅธธ่งๆ‰˜็ฎก็›ฎๅฝ• > ๅ…จๅฑ€ npm root
15
+ python-pptx
16
+ ๅญ่ฟ›็จ‹ `import pptx` ๆ˜ฏๅฆๆˆๅŠŸ๏ผˆๅฏ้€‰็ฌฌไธ‰ๆ–น่ฃๅˆค๏ผ›ๆœช่ฃ…ๅˆ™่ฐƒ็”จๆ–น่ทณ่ฟ‡๏ผ‰
17
+
18
+ ่‡ชๆฃ€๏ผš
19
+ python scripts/env_probe.py
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ try:
30
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
31
+ except Exception:
32
+ pass
33
+
34
+ ROOT = Path(__file__).resolve().parent.parent
35
+
36
+ NODE_ENV_VARS = ('TOP_PPT_NODE_EXE', 'NODE_EXE', 'NODE')
37
+ NODE_PATH_ENV_VARS = ('TOP_PPT_NODE_PATH', 'NODE_PATH')
38
+ NODE_BASES = (
39
+ Path.home() / '.local' / 'share' / 'node',
40
+ Path.home() / 'AppData' / 'Local' / 'nvs',
41
+ )
42
+ MODULE_BASES = (
43
+ Path.home() / 'node_modules',
44
+ )
45
+ NODE_EXES = ('node.exe', 'bin/node', 'bin/node.exe')
46
+ DEFAULT_PKG = 'pptxgenjs'
47
+
48
+
49
+ # โ”€โ”€ Node ๅฏๆ‰ง่กŒๆ–‡ไปถ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+
51
+ def node_candidates() -> list[str]:
52
+ """ๆŒ‰ไผ˜ๅ…ˆ็บง่ฟ”ๅ›žๅ€™้€‰ node ๅฏๆ‰ง่กŒๆ–‡ไปถ๏ผˆๅŽป้‡ใ€ไฟๅบ๏ผ‰ใ€‚"""
53
+ cands: list[str] = []
54
+ for var in NODE_ENV_VARS:
55
+ val = os.environ.get(var)
56
+ if val and Path(val).is_file():
57
+ cands.append(val)
58
+ found = shutil.which('node')
59
+ if found:
60
+ cands.append(found)
61
+ for base in NODE_BASES:
62
+ if not base.is_dir():
63
+ continue
64
+ try:
65
+ for ver in sorted((p for p in base.iterdir() if p.is_dir()), reverse=True):
66
+ for exe in NODE_EXES:
67
+ p = ver / exe
68
+ if p.is_file():
69
+ cands.append(str(p))
70
+ break
71
+ except OSError:
72
+ pass
73
+ seen, out = set(), []
74
+ for c in cands:
75
+ if c not in seen:
76
+ seen.add(c)
77
+ out.append(c)
78
+ return out
79
+
80
+
81
+ def find_node() -> str | None:
82
+ for c in node_candidates():
83
+ if Path(c).is_file():
84
+ return c
85
+ return None
86
+
87
+
88
+ # โ”€โ”€ ๅซ pptxgenjs ็š„ node_modules โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
89
+
90
+ def node_modules_candidates() -> list[str]:
91
+ """ๆŒ‰ไผ˜ๅ…ˆ็บง่ฟ”ๅ›žๅฏ่ƒฝๅซ็›ฎๆ ‡ๅŒ…็š„ node_modules ็›ฎๅฝ•๏ผˆๅŽป้‡ใ€ไฟๅบ๏ผ‰ใ€‚"""
92
+ cands: list[str] = []
93
+ for var in NODE_PATH_ENV_VARS:
94
+ val = os.environ.get(var)
95
+ if val:
96
+ cands.extend(p for p in val.split(os.pathsep) if p)
97
+ cands.append(str(ROOT / 'node_modules'))
98
+ cands.extend(str(p / 'node_modules') for p in list(ROOT.parents)[:3]) # monorepo ๅœบๆ™ฏ
99
+ cands.extend(str(b) for b in MODULE_BASES)
100
+ try:
101
+ npm_root = subprocess.run(['npm', 'root', '-g'], capture_output=True, text=True,
102
+ encoding='utf-8', timeout=30).stdout.strip()
103
+ if npm_root:
104
+ cands.append(npm_root)
105
+ except Exception:
106
+ pass
107
+ seen, out = set(), []
108
+ for c in cands:
109
+ if c and c not in seen:
110
+ seen.add(c)
111
+ out.append(c)
112
+ return out
113
+
114
+
115
+ def find_node_modules(pkg: str = DEFAULT_PKG) -> str | None:
116
+ for c in node_modules_candidates():
117
+ if (Path(c) / pkg).is_dir():
118
+ return c
119
+ return None
120
+
121
+
122
+ # โ”€โ”€ ็ป„ๅˆไธŽๆ็คบ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
+
124
+ def node_env(node_modules: str | None = None) -> dict[str, str]:
125
+ """่ฟ”ๅ›žๅฏ็›ดๆŽฅไผ ็ป™ subprocess ็š„็Žฏๅขƒ๏ผˆๆŠŠ NODE_PATH ๆŒ‡ๅˆฐ่งฃๆžๅ‡บ็š„ node_modules๏ผ‰ใ€‚"""
126
+ env = dict(os.environ)
127
+ if node_modules:
128
+ env['NODE_PATH'] = node_modules
129
+ return env
130
+
131
+
132
+ def has_python_pptx(python_exe: str | None = None) -> bool:
133
+ exe = python_exe or sys.executable
134
+ try:
135
+ return subprocess.run([exe, '-c', 'import pptx'], capture_output=True).returncode == 0
136
+ except OSError:
137
+ return False
138
+
139
+
140
+ NODE_HINT = ('ๆœชๆ‰พๅˆฐ Node ๅฏๆ‰ง่กŒๆ–‡ไปถ๏ผˆPPTX ็ฒพๅฏผไธŽๅ›žๅฝ’ๅฟ…้œ€๏ผ‰ใ€‚ๅฎ‰่ฃ… Node ๅŽ้‡่ฏ•๏ผŒ'
141
+ 'ๆˆ–็”จ็Žฏๅขƒๅ˜้‡ TOP_PPT_NODE_EXE ๆŒ‡ๅฎš่ทฏๅพ„ใ€‚')
142
+ NODE_MODULES_HINT = ('ๆœชๆ‰พๅˆฐๅซ pptxgenjs ็š„ node_modules๏ผˆPPTX ็”Ÿๆˆๅฟ…้œ€๏ผ‰ใ€‚'
143
+ 'ๅœจๆŠ€่ƒฝ็›ฎๅฝ•ๆ‰ง่กŒ `npm install`๏ผˆไพ package.json๏ผ‰๏ผŒ'
144
+ 'ๆˆ–็”จ็Žฏๅขƒๅ˜้‡ TOP_PPT_NODE_PATH ๆŒ‡ๅฎš node_modules ่ทฏๅพ„ใ€‚')
145
+
146
+
147
+ def resolve(pkg: str = DEFAULT_PKG) -> tuple[str | None, str | None]:
148
+ """ไธ€ๆฌกๆ€ง่ฟ”ๅ›ž (node, node_modules)ใ€‚"""
149
+ return find_node(), find_node_modules(pkg)
150
+
151
+
152
+ if __name__ == '__main__':
153
+ node, mods = resolve()
154
+ print('็ŽฏๅขƒๆŽขๆต‹๏ผˆTopPPT HTML๏ผ‰')
155
+ print(f' node : {node or "ๆœชๆ‰พๅˆฐ"}')
156
+ print(f' node_modules : {mods or "ๆœชๆ‰พๅˆฐ๏ผˆ็ผบ pptxgenjs๏ผ‰"}')
157
+ print(f' python-pptx : {"ๅฏ็”จ" if has_python_pptx() else "ๆœชๅฎ‰่ฃ…๏ผˆๅฏ้€‰๏ผŒไบคๅ‰่ฃๅˆคไผš่ทณ่ฟ‡๏ผ‰"}')
158
+ sys.exit(0 if (node and mods) else 1)