返回 ppt-master
beautify_inventory.py
根目录 / skills / ppt-master / scripts / beautify_inventory.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Beautify Inventory Builder
4
5 Mechanically merge a source deck's extracts into one per-slide ledger for the
6 beautify-pptx profile: text blocks + tables + charts + SmartArt structure (from a
7 `template_fill_pptx.py analyze` slide_library.json) joined with the images
8 bound to each slide (from a `ppt_to_md.py` image_manifest.json). The deterministic
9 join only — `ignored` and `needs_confirmation` are emitted empty for the agent
10 to fill with judgment (hidden shapes, combo charts, overcrowded pages, ...).
11
12 Usage:
13 python3 scripts/beautify_inventory.py <slide_library.json> [--images <image_manifest.json>] [-o inventory.json]
14
15 Examples:
16 python3 scripts/beautify_inventory.py projects/x/analysis/<stem>.slide_library.json \
17 --images projects/x/images/image_manifest.json -o projects/x/analysis/beautify_inventory.json
18
19 Dependencies:
20 None (standard library only).
21
22 See workflows/profiles/beautify-pptx.md Step 4 for how the inventory is consumed.
23 """
24
25 from __future__ import annotations
26
27 import argparse
28 import json
29 import sys
30 from pathlib import Path
31 from typing import Optional
32
33 from console_encoding import configure_utf8_stdio
34
35 configure_utf8_stdio()
36
37
38 def _images_by_slide(manifest: list) -> dict[int, list[dict]]:
39 """Map slide_index -> [image entries on that slide], from ppt_to_md occurrences."""
40 by_slide: dict[int, list[dict]] = {}
41 for entry in manifest:
42 filename = entry.get("filename")
43 for occ in entry.get("occurrences", []):
44 idx = occ.get("slide_index")
45 if idx is None:
46 continue
47 by_slide.setdefault(idx, []).append({
48 "filename": filename,
49 "shape_name": occ.get("shape_name"),
50 "pixel_width": entry.get("pixel_width"),
51 "pixel_height": entry.get("pixel_height"),
52 "display_ratio": occ.get("display_ratio"),
53 "display_left_emu": occ.get("display_left_emu"),
54 "display_top_emu": occ.get("display_top_emu"),
55 "display_width_emu": occ.get("display_width_emu"),
56 "display_height_emu": occ.get("display_height_emu"),
57 "usage_count": entry.get("usage_count"),
58 })
59 return by_slide
60
61
62 def _table_cells(table: dict) -> list[list[str]]:
63 """Row-major 2D grid of cell text from a slide_library table."""
64 grid = []
65 for row in table.get("rows", []):
66 grid.append([c.get("text", "") for c in row.get("cells", [])])
67 return grid
68
69
70 def build_inventory(slide_library: dict, images_by_slide: dict[int, list[dict]]) -> dict:
71 """Join slide_library slides with per-slide images into one ledger."""
72 slides_out = []
73 for slide in slide_library.get("slides", []):
74 idx = slide.get("slide_index")
75 text_blocks = [
76 {
77 "slot_id": s.get("slot_id"),
78 "role": s.get("role"),
79 "text": s.get("text", ""),
80 "paragraph_count": s.get("paragraph_count"),
81 "geometry": s.get("geometry"),
82 }
83 for s in slide.get("slots", [])
84 ]
85 tables = [
86 {
87 "table_id": t.get("table_id"),
88 "row_count": t.get("row_count"),
89 "column_count": t.get("column_count"),
90 "cells": _table_cells(t), # 2D text grid — convenient for diff
91 "rows": t.get("rows", []), # raw row/col cells — preserves merged / multi-header fidelity
92 }
93 for t in slide.get("tables", [])
94 ]
95 charts = [
96 {
97 "chart_id": c.get("chart_id"),
98 "chart_type": c.get("chart_type"),
99 "category_count": c.get("category_count"),
100 "series_count": c.get("series_count"),
101 "categories": c.get("categories", []), # frozen data
102 "series": c.get("series", []), # frozen data (name + values)
103 }
104 for c in slide.get("charts", [])
105 ]
106 diagrams = [
107 {
108 "diagram_id": diagram.get("diagram_id"),
109 "shape_name": diagram.get("shape_name"),
110 "geometry": diagram.get("geometry"),
111 "layout": diagram.get("layout", {}),
112 "root_ids": diagram.get("root_ids", []),
113 "nodes": diagram.get("nodes", []),
114 "connections": diagram.get("connections", []),
115 "status": diagram.get("status"),
116 "warnings": diagram.get("warnings", []),
117 }
118 for diagram in slide.get("diagrams", [])
119 ]
120 slides_out.append({
121 "slide_index": idx,
122 "page_type": slide.get("page_type"),
123 "text_blocks": text_blocks,
124 "tables": tables,
125 "charts": charts,
126 "diagrams": diagrams,
127 "images": images_by_slide.get(idx, []),
128 "ignored": [], # agent fills: hidden shapes, master-only text, image crop/rotation
129 "needs_confirmation": [], # agent fills: combo/dual-axis charts, merged-cell tables, overcrowded pages
130 })
131
132 return {
133 "schema": "beautify_inventory.v1",
134 "source": slide_library.get("source_pptx"),
135 "slide_count": slide_library.get("slide_count", len(slides_out)),
136 "canvas_px": slide_library.get("canvas_px"),
137 "slides": slides_out,
138 }
139
140
141 def build_parser() -> argparse.ArgumentParser:
142 parser = argparse.ArgumentParser(
143 description="Merge slide_library.json + image_manifest.json into a per-slide beautify inventory.",
144 formatter_class=argparse.RawDescriptionHelpFormatter,
145 )
146 parser.add_argument("slide_library", help="slide_library.json from `template_fill_pptx.py analyze`")
147 parser.add_argument("--images", help="image_manifest.json from `ppt_to_md.py` (optional)")
148 parser.add_argument("-o", "--output", help="Write JSON here (default: stdout)")
149 return parser
150
151
152 def main(argv: Optional[list[str]] = None) -> int:
153 args = build_parser().parse_args(argv)
154
155 lib_path = Path(args.slide_library)
156 if not lib_path.is_file():
157 print(f"[ERROR] slide_library not found: {lib_path}", file=sys.stderr)
158 return 1
159 slide_library = json.loads(lib_path.read_text(encoding="utf-8"))
160
161 manifest: list = []
162 if args.images:
163 img_path = Path(args.images)
164 if not img_path.is_file():
165 print(f"[ERROR] image manifest not found: {img_path}", file=sys.stderr)
166 return 1
167 manifest = json.loads(img_path.read_text(encoding="utf-8"))
168
169 inventory = build_inventory(slide_library, _images_by_slide(manifest))
170
171 payload = json.dumps(inventory, ensure_ascii=False, indent=2)
172 if args.output:
173 out = Path(args.output)
174 out.parent.mkdir(parents=True, exist_ok=True)
175 out.write_text(payload + "\n", encoding="utf-8")
176 print(f"[OK] inventory written to: {out}", file=sys.stderr)
177 else:
178 print(payload)
179 return 0
180
181
182 if __name__ == "__main__":
183 raise SystemExit(main())
184
184 lines PYTHON