Files
Templated-PPT-Master/runtime/ppt-master/scripts/beautify_inventory.py
T

335 lines
12 KiB
Python

#!/usr/bin/env python3
"""
PPT Master - Beautify Inventory Builder
Mechanically merge a source deck's extracts into one per-slide ledger for the
beautify-pptx profile: text blocks + tables + charts + SmartArt structure (from a
`template_fill_pptx.py analyze` slide_library.json) joined with the images
bound to each slide (from a `ppt_to_md.py` image_manifest.json). The deterministic
join only — `ignored` and `needs_confirmation` are emitted empty for the agent
to fill with judgment (hidden shapes, combo charts, overcrowded pages, ...).
Usage:
python3 scripts/beautify_inventory.py <slide_library.json> [--images <image_manifest.json>] [-o inventory.json]
python3 scripts/beautify_inventory.py <inventory.json> --summary
python3 scripts/beautify_inventory.py <inventory.json> --page N [--with-geometry]
Examples:
python3 scripts/beautify_inventory.py projects/x/analysis/<stem>.slide_library.json \
--images projects/x/images/image_manifest.json -o projects/x/analysis/beautify_inventory.json
python3 scripts/beautify_inventory.py projects/x/analysis/beautify_inventory.json --summary
python3 scripts/beautify_inventory.py projects/x/analysis/beautify_inventory.json --page 7
Dependencies:
None (standard library only).
See workflows/profiles/beautify-pptx.md Step 4 for how the inventory is consumed.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Optional
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
_GEOMETRY_KEYS = {
"geometry",
"display_ratio",
"display_left_emu",
"display_top_emu",
"display_width_emu",
"display_height_emu",
}
def _images_by_slide(manifest: list) -> dict[int, list[dict]]:
"""Map slide_index -> [image entries on that slide], from ppt_to_md occurrences."""
by_slide: dict[int, list[dict]] = {}
for entry in manifest:
filename = entry.get("filename")
for occ in entry.get("occurrences", []):
idx = occ.get("slide_index")
if idx is None:
continue
by_slide.setdefault(idx, []).append({
"filename": filename,
"shape_name": occ.get("shape_name"),
"pixel_width": entry.get("pixel_width"),
"pixel_height": entry.get("pixel_height"),
"display_ratio": occ.get("display_ratio"),
"display_left_emu": occ.get("display_left_emu"),
"display_top_emu": occ.get("display_top_emu"),
"display_width_emu": occ.get("display_width_emu"),
"display_height_emu": occ.get("display_height_emu"),
"usage_count": entry.get("usage_count"),
})
return by_slide
def _table_cells(table: dict) -> list[list[str]]:
"""Row-major 2D grid of cell text from a slide_library table."""
grid = []
for row in table.get("rows", []):
grid.append([c.get("text", "") for c in row.get("cells", [])])
return grid
def build_inventory(slide_library: dict, images_by_slide: dict[int, list[dict]]) -> dict:
"""Join slide_library slides with per-slide images into one ledger."""
slides_out = []
for slide in slide_library.get("slides", []):
idx = slide.get("slide_index")
text_blocks = [
{
"slot_id": s.get("slot_id"),
"role": s.get("role"),
"text": s.get("text", ""),
"paragraph_count": s.get("paragraph_count"),
"geometry": s.get("geometry"),
}
for s in slide.get("slots", [])
]
tables = [
{
"table_id": t.get("table_id"),
"row_count": t.get("row_count"),
"column_count": t.get("column_count"),
"cells": _table_cells(t), # 2D text grid — convenient for diff
"rows": t.get("rows", []), # raw row/col cells — preserves merged / multi-header fidelity
}
for t in slide.get("tables", [])
]
charts = [
{
"chart_id": c.get("chart_id"),
"chart_type": c.get("chart_type"),
"category_count": c.get("category_count"),
"series_count": c.get("series_count"),
"categories": c.get("categories", []), # frozen data
"series": c.get("series", []), # frozen data (name + values)
}
for c in slide.get("charts", [])
]
diagrams = [
{
"diagram_id": diagram.get("diagram_id"),
"shape_name": diagram.get("shape_name"),
"geometry": diagram.get("geometry"),
"layout": diagram.get("layout", {}),
"root_ids": diagram.get("root_ids", []),
"nodes": diagram.get("nodes", []),
"connections": diagram.get("connections", []),
"status": diagram.get("status"),
"warnings": diagram.get("warnings", []),
}
for diagram in slide.get("diagrams", [])
]
slides_out.append({
"slide_index": idx,
"page_type": slide.get("page_type"),
"text_blocks": text_blocks,
"tables": tables,
"charts": charts,
"diagrams": diagrams,
"images": images_by_slide.get(idx, []),
"ignored": [], # agent fills: hidden shapes, master-only text, image crop/rotation
"needs_confirmation": [], # agent fills: combo/dual-axis charts, merged-cell tables, overcrowded pages
})
return {
"schema": "beautify_inventory.v1",
"source": slide_library.get("source_pptx"),
"slide_count": slide_library.get("slide_count", len(slides_out)),
"canvas_px": slide_library.get("canvas_px"),
"slides": slides_out,
}
def _view_payload(inventory: dict, view: str, slides: list[dict]) -> dict:
"""Wrap projected slides with the canonical deck-level facts."""
return {
"schema": "beautify_inventory.view.v1",
"inventory_schema": inventory.get("schema", "beautify_inventory.v1"),
"view": view,
"source": inventory.get("source"),
"slide_count": inventory.get("slide_count", len(inventory.get("slides", []))),
"canvas_px": inventory.get("canvas_px"),
"slides": slides,
}
def _summary_view(inventory: dict) -> dict:
"""Project the whole roster into compact per-slide counts and review flags."""
slides = []
for slide in inventory.get("slides", []):
slides.append({
"slide_index": slide.get("slide_index"),
"page_type": slide.get("page_type"),
"text_block_count": len(slide.get("text_blocks", [])),
"table_count": len(slide.get("tables", [])),
"chart_count": len(slide.get("charts", [])),
"diagram_count": len(slide.get("diagrams", [])),
"image_count": len(slide.get("images", [])),
"ignored": slide.get("ignored", []),
"needs_confirmation": slide.get("needs_confirmation", []),
})
return _view_payload(inventory, "summary", slides)
def _without_geometry(value: Any) -> Any:
"""Remove explicit source-layout geometry while preserving content and data."""
if isinstance(value, dict):
return {
key: _without_geometry(item)
for key, item in value.items()
if key not in _GEOMETRY_KEYS
}
if isinstance(value, list):
return [_without_geometry(item) for item in value]
return value
def _page_view(inventory: dict, slide_index: int, with_geometry: bool) -> Optional[dict]:
"""Project one slide by its canonical slide_index."""
for slide in inventory.get("slides", []):
if slide.get("slide_index") != slide_index:
continue
projected = slide if with_geometry else _without_geometry(slide)
return _view_payload(inventory, "page", [projected])
return None
def _positive_int(value: str) -> int:
"""Parse a positive slide index for argparse."""
parsed = int(value)
if parsed < 1:
raise argparse.ArgumentTypeError("page must be a positive integer")
return parsed
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Build a beautify inventory from a slide library, or print a read-only "
"model view from a canonical beautify inventory."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"input_json",
help="slide_library.json in builder mode, or beautify_inventory.v1 JSON in a view mode",
)
parser.add_argument(
"--images",
help="image_manifest.json for a slide_library input (optional)",
)
parser.add_argument(
"-o",
"--output",
help="Write the full built inventory here (builder mode only; default: stdout)",
)
view_group = parser.add_mutually_exclusive_group()
view_group.add_argument(
"--summary",
action="store_true",
help="Print deck facts, per-slide object counts, and review flags to stdout",
)
view_group.add_argument(
"--page",
type=_positive_int,
metavar="N",
help="Print one slide's frozen content/data by slide_index to stdout",
)
parser.add_argument(
"--with-geometry",
action="store_true",
help="Retain source-layout geometry in a --page view",
)
return parser
def main(argv: Optional[list[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
view_requested = args.summary or args.page is not None
if view_requested and args.output:
parser.error("--summary/--page write to stdout and cannot be combined with --output")
if args.with_geometry and args.page is None:
parser.error("--with-geometry requires --page")
input_path = Path(args.input_json)
if not input_path.is_file():
print(f"[ERROR] input JSON not found: {input_path}", file=sys.stderr)
return 1
input_data = json.loads(input_path.read_text(encoding="utf-8"))
input_schema = input_data.get("schema")
if input_schema == "beautify_inventory.view.v1":
parser.error(
"beautify_inventory.view.v1 is a read-only stdout projection and "
"cannot be used as input"
)
is_inventory = input_schema == "beautify_inventory.v1"
if is_inventory and not view_requested:
parser.error(
"beautify_inventory.v1 input requires --summary or --page; "
"builder mode requires slide_library.json"
)
if is_inventory and args.images:
parser.error("--images cannot be combined with a beautify_inventory.v1 input")
if view_requested and not is_inventory:
parser.error("--summary/--page require a canonical beautify_inventory.v1 input")
manifest: list = []
if args.images and not is_inventory:
img_path = Path(args.images)
if not img_path.is_file():
print(f"[ERROR] image manifest not found: {img_path}", file=sys.stderr)
return 1
manifest = json.loads(img_path.read_text(encoding="utf-8"))
inventory = input_data if is_inventory else build_inventory(
input_data,
_images_by_slide(manifest),
)
if args.summary:
print(json.dumps(_summary_view(inventory), ensure_ascii=False, indent=2))
return 0
if args.page is not None:
page_view = _page_view(inventory, args.page, args.with_geometry)
if page_view is None:
available = ", ".join(
str(slide.get("slide_index"))
for slide in inventory.get("slides", [])
)
print(
f"[ERROR] slide_index {args.page} not found; available: {available or 'none'}",
file=sys.stderr,
)
return 1
print(json.dumps(page_view, ensure_ascii=False, indent=2))
return 0
payload = json.dumps(inventory, ensure_ascii=False, indent=2)
if args.output:
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(payload + "\n", encoding="utf-8")
print(f"[OK] inventory written to: {out}", file=sys.stderr)
else:
print(payload)
return 0
if __name__ == "__main__":
raise SystemExit(main())