"""Reproduce this guide's local HTML extraction and PPTX object experiment. Writes only below this script's directory. No network requests, installs, browser execution, or Office rendering. Existing upstream evidence is retained. """ from __future__ import annotations import argparse from collections import Counter from datetime import datetime, timezone import hashlib from importlib.metadata import version import json from pathlib import Path import platform import re import subprocess import time import xml.etree.ElementTree as ET from zipfile import ZipFile LAB = Path(__file__).resolve().parent CORPUS = LAB / "corpus" SAMPLES = ["article", "article-no-main", "article-css-grid", "article-script-table"] NS = {"a": "http://schemas.openxmlformats.org/drawingml/2006/main"} def write_json(path, data): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def relative(path): return str(path.relative_to(LAB)) def command(args): started = time.perf_counter() result = subprocess.run(args, capture_output=True, text=True, check=True) return result.stdout, result.stderr, round(time.perf_counter() - started, 6) def build_samples(): original = (CORPUS / "article.html").read_text(encoding="utf-8") truth = json.loads((CORPUS / "article.truth.json").read_text(encoding="utf-8")) table = re.search(r".*?
", original, flags=re.S).group() cells = re.findall(r"(.*?)", table) grid = '
' + "".join(f'
{x}
' for x in cells) + "
" css = "" script = '
" variants = { "article-no-main": original.replace("
", "").replace("
", ""), "article-css-grid": original.replace("", css + "").replace(table, grid), "article-script-table": original.replace(table, script), } for sample, html in variants.items(): (CORPUS / f"{sample}.html").write_text(html, encoding="utf-8") write_json(CORPUS / f"{sample}.truth.json", truth) notes = """# HTML-to-PPT experiment corpus All variants derive from the supplied article.html and its ground truth. | Sample | Controlled change | Expected browser content, not a rendering claim | | --- | --- | --- | | article.html | Supplied static semantic article, main wrapper, table | Article and semantic data table | | article-no-main.html | Only removes main opening/closing tags | Same article and data, site wrapper still present | | article-css-grid.html | Replaces table with div cells and CSS Grid | Same cell words arranged by CSS, no semantic table | | article-script-table.html | Replaces table with empty div and script assigning the original table as innerHTML | Table requires JavaScript execution | Each same-named truth.json preserves the original target content. Script-table truth describes the intended content after the script, so its missing cells are counted as omissions by static extractors. No browser was executed to validate visual appearance. The synthetic database throughput values are fixture data, not measurements of database performance or converter speed. """ (CORPUS / "HTML-EXPERIMENT.md").write_text(notes, encoding="utf-8") def count_ast(obj): found = Counter() def walk(value): if isinstance(value, dict): if "t" in value: found[value["t"]] += 1 for child in value.values(): walk(child) elif isinstance(value, list): for child in value: walk(child) walk(obj) return dict(sorted(found.items())) def inspect_pptx(path, transcript, xml_output): from pptx import Presentation deck = Presentation(str(path)) tables = [] text_shapes = 0 image_shapes = 0 lines = [] slide_titles = [] for slide_index, slide in enumerate(deck.slides, 1): title_shape = slide.shapes.title title = title_shape.text if title_shape else "Untitled slide" slide_titles.append(title) lines.extend(["# " + title.replace("\n", " "), ""]) for shape in slide.shapes: if shape.has_table: table = shape.table rows = [[cell.text for cell in row.cells] for row in table.rows] tables.append({"slide": slide_index, "rows": rows, "row_count": len(rows), "column_count": len(table.columns)}) for row_index, row in enumerate(rows): lines.append("| " + " | ".join(cell.replace("\n", " ") for cell in row) + " |") if row_index == 0: lines.append("| " + " | ".join("---" for _ in row) + " |") lines.append("") elif shape.has_text_frame: text_shapes += 1 if shape == title_shape: continue for paragraph in shape.text_frame.paragraphs: if paragraph.text.strip(): lines.extend([paragraph.text, ""]) if shape.shape_type == 13: image_shapes += 1 xml_parts = [] native_table_elements = 0 with ZipFile(path) as archive: names = sorted(n for n in archive.namelist() if re.fullmatch(r"ppt/slides/slide\d+\.xml", n)) for name in names: raw = archive.read(name).decode("utf-8") native_table_elements += len(ET.fromstring(raw).findall(".//a:tbl", NS)) xml_parts.extend([f"===== {name} =====", raw, ""]) assert native_table_elements == len(tables), "Object and XML table counts disagree" transcript.parent.mkdir(parents=True, exist_ok=True) transcript.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") xml_output.write_text("\n".join(xml_parts), encoding="utf-8") return { "deck_local_only": relative(path), "transcript": relative(transcript), "slide_xml": relative(xml_output), "slides": len(deck.slides), "slide_titles": slide_titles, "native_tables": native_table_elements, "native_text_shapes": text_shapes, "image_shapes": image_shapes, "tables": tables, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), } def run(): from markitdown import MarkItDown from markitdown.converters import HtmlConverter provenance = LAB / "provenance" provenance.mkdir(exist_ok=True) upstream = provenance / "upstream-runs.json" if not upstream.exists(): upstream.write_bytes((LAB / "runs.json").read_bytes()) baseline = {} for name in ["corpus/article.html", "corpus/article.truth.json", "markovo/article.md", "markovo/article.json"]: path = LAB / name baseline[name] = {"sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size} write_json(provenance / "input-hashes.json", baseline) build_samples() pandoc_version = command(["pandoc", "--version"])[0].splitlines()[0] runner = MarkItDown(enable_builtins=False, enable_plugins=False) runner.register_converter(HtmlConverter()) results = [] commands = [] structures = [] ast_rows = [] for sample in SAMPLES: source = CORPUS / f"{sample}.html" p_dir, m_dir = LAB / "pandoc-html", LAB / "markitdown-html" p_dir.mkdir(exist_ok=True) m_dir.mkdir(exist_ok=True) p_md, m_md = p_dir / f"{sample}.md", m_dir / f"{sample}.md" args = ["pandoc", str(source), "--from=html", "--to=gfm", "--wrap=none", "--output", str(p_md)] _, stderr, elapsed = command(args) (p_dir / f"{sample}.stderr.txt").write_text(stderr, encoding="utf-8") commands.append(args) results.append({"tool": "pandoc-html", "sample": sample, "output": relative(p_md), "elapsed_s": elapsed, "error": None}) args = ["pandoc", str(source), "--from=html", "--to=json"] stdout, stderr, _ = command(args) ast_path = p_dir / f"{sample}.ast.json" ast_path.write_text(stdout, encoding="utf-8") ast_rows.append({"sample": sample, "output": relative(ast_path), "nodes": count_ast(json.loads(stdout))}) commands.append(args) started = time.perf_counter() result = runner.convert_local(str(source)) m_md.write_text(result.text_content + "\n", encoding="utf-8") elapsed = round(time.perf_counter() - started, 6) results.append({"tool": "markitdown-html", "sample": sample, "output": relative(m_md), "elapsed_s": elapsed, "error": None}) for tool, input_file, reader in [("pandoc-pptx", source, "html"), ("markitdown-pandoc-pptx", m_md, "gfm")]: out_dir = LAB / tool out_dir.mkdir(exist_ok=True) deck = out_dir / f"{sample}.pptx" transcript = out_dir / f"{sample}.md" args = ["pandoc", str(input_file), "--from=" + reader, "--to=pptx", "--slide-level=2", "--output", str(deck)] _, stderr, elapsed = command(args) (out_dir / f"{sample}.stderr.txt").write_text(stderr, encoding="utf-8") commands.append(args) row = inspect_pptx(deck, transcript, out_dir / f"{sample}.slide-xml.txt") structures.append({"tool": tool, "sample": sample, "elapsed_s": elapsed, **row}) results.append({"tool": tool, "sample": sample, "output": relative(transcript), "elapsed_s": elapsed, "error": None}) snapshot = json.loads((LAB / "markovo/article.json").read_text(encoding="utf-8")) out_dir = LAB / "pandoc-pptx-no-title" out_dir.mkdir(exist_ok=True) args = ["pandoc", str(CORPUS / "article.html"), "--from=html", "--to=pptx", "--slide-level=2", "--metadata=title:", "--output", str(out_dir / "article.pptx")] _, stderr, elapsed = command(args) commands.append(args) (out_dir / "article.stderr.txt").write_text(stderr, encoding="utf-8") row = inspect_pptx(out_dir / "article.pptx", out_dir / "article.md", out_dir / "article.slide-xml.txt") structures.append({"tool": "pandoc-pptx-no-title", "sample": "article", "elapsed_s": elapsed, **row}) results.append({"tool": "pandoc-pptx-no-title", "sample": "article", "output": relative(out_dir / "article.md"), "elapsed_s": elapsed, "error": None}) results.append({"tool": "markovo", "sample": "article", "output": "markovo/article.md", "elapsed_s": None, "error": None}) out_dir = LAB / "markovo-pandoc-pptx" out_dir.mkdir(exist_ok=True) args = ["pandoc", str(LAB / "markovo/article.md"), "--from=gfm", "--to=pptx", "--slide-level=2", "--output", str(out_dir / "article.pptx")] _, stderr, elapsed = command(args) commands.append(args) (out_dir / "article.stderr.txt").write_text(stderr, encoding="utf-8") row = inspect_pptx(out_dir / "article.pptx", out_dir / "article.md", out_dir / "article.slide-xml.txt") structures.append({"tool": "markovo-pandoc-pptx", "sample": "article", "elapsed_s": elapsed, **row}) results.append({"tool": "markovo-pandoc-pptx", "sample": "article", "output": relative(out_dir / "article.md"), "elapsed_s": elapsed, "error": None}) manifest = { "python": platform.python_version(), "date": datetime.now(timezone.utc).date().isoformat(), "scope": "HTML source structure and native PPTX objects; not a PDF/OCR or browser rendering benchmark", "tools": { "pandoc-html": {"version": pandoc_version, "options": ["--from=html", "--to=gfm", "--wrap=none"]}, "markitdown-html": {"version": version("markitdown"), "enable_builtins": False, "enable_plugins": False, "registered_converter": "HtmlConverter", "beautifulsoup4": version("beautifulsoup4"), "markdownify": version("markdownify")}, "pandoc-pptx": {"version": pandoc_version, "options": ["--from=html", "--to=pptx", "--slide-level=2"]}, "pandoc-pptx-no-title": {"version": pandoc_version, "options": ["--from=html", "--to=pptx", "--slide-level=2", "--metadata=title:"], "sample_scope": ["article"]}, "markitdown-pandoc-pptx": {"version": pandoc_version, "options": ["--from=gfm", "--to=pptx", "--slide-level=2"]}, "markovo": {"version": snapshot["job"]["rate_card_version"], "created_at": snapshot["job"]["created_at"], "sample_scope": ["article"], "note": "Reused upstream API output. No new API call. Original zero timing discarded, not a measured duration."}, "markovo-pandoc-pptx": {"version": pandoc_version, "note": "Local PPTX generation from supplied upstream Markdown; no API call"}, "pptx_inspector": {"python-pptx": version("python-pptx"), "lxml": version("lxml")}, }, "results": results, } write_json(LAB / "runs.json", manifest) write_json(LAB / "pptx-inspection.json", {"method": "python-pptx shape.has_table and table cells; cross-checked with a:tbl elements in slide XML; no Office rendering/editing", "cells": structures, "html_ast": ast_rows}) write_json(LAB / "commands.json", {"subprocess_commands": commands, "in_process": "MarkItDown(enable_builtins=False, enable_plugins=False), register HtmlConverter(), convert_local(source); see this script", "entrypoint": "PYTHONDONTWRITEBYTECODE=1 tools/lab-venv/bin/python runs/2026-11-04-markovo-html-to-ppt/lab/run_html_to_ppt.py"}) print(json.dumps({"samples": SAMPLES, "scored_outputs": len(results), "decks_inspected": len(structures), "versions": manifest["tools"]}, ensure_ascii=False, indent=2)) def attach_structures(): path = LAB / "scores.json" scores = json.loads(path.read_text(encoding="utf-8")) inspection = json.loads((LAB / "pptx-inspection.json").read_text(encoding="utf-8")) scores["pptx_structures"] = inspection["cells"] scores["html_ast"] = inspection["html_ast"] scores["supplement_method"] = "Generated by run_html_to_ppt.py --attach-structures after the unchanged tools/lab_score.py. Original scorer cells and metrics_doc are not altered. Native table counts describe OOXML objects, not visual fidelity or Office editing." scores["supplement_metrics_doc"] = {"native_tables": "Count of python-pptx table shapes, cross-checked against a:tbl in slide XML", "native_text_shapes": "Count of shapes with text frames including slide titles", "image_shapes": "Count of picture shapes; does not establish rendering quality", "slides": "Number of slide objects", "html_ast": "Counts of Pandoc AST node tags before PPTX generation"} write_json(path, scores) print("Attached mechanically measured PPTX/AST structures; original scorer cells unchanged.") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--attach-structures", action="store_true") args = parser.parse_args() if args.attach_structures: attach_structures() else: run()