← Files Investment BankingARCHIVED FILE
skills/pitch-deck-builder/scripts/build_deck_storyboard_html.py
7.96 KB · Oct 5, 2026 · 18:28 UTC
#!/usr/bin/env python3
"""Build a standalone HTML pitch-deck storyboard from a page plan."""
from __future__ import annotations
import argparse
import json
import sys
from html import escape
from pathlib import Path
from typing import Any, Mapping, Sequence
PLUGIN_ROOT = Path(__file__).resolve().parents[3]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from shared.artifacts import artifact_item, support_dir, write_artifact_manifest # noqa: E402
def load_payload(path: Path | None) -> dict[str, Any]:
if path is None:
return {}
return json.loads(path.read_text(encoding="utf-8"))
def as_rows(value: Any, fallback: list[dict[str, Any]]) -> list[dict[str, Any]]:
if isinstance(value, list) and all(isinstance(item, dict) for item in value):
return value
return fallback
def write_support_payload(outdir: Path, payload: Mapping[str, Any]) -> Path:
target = support_dir(outdir) / "deck_plan.json"
target.write_text(json.dumps(dict(payload), indent=2) + "\n", encoding="utf-8")
return target
def print_summary(
json_run_log: bool, quiet: bool, summary: dict[str, Any], human_lines: Sequence[str]
) -> None:
if json_run_log:
print(json.dumps(summary, indent=2))
elif not quiet:
for line in human_lines:
print(line)
def cell(value: Any) -> str:
return escape(str(value if value not in (None, "") else "-"))
def write_storyboard_html(
out: Path,
company: str,
as_of_date: str,
slides: list[dict[str, Any]],
qc: list[dict[str, Any]],
) -> Path:
slide_rows = "\n".join(
"<tr>"
f"<td>{cell(row.get('slide', row.get('page', index)))}</td>"
f"<td><strong>{cell(row.get('title', row.get('section', 'Planned slide')))}</strong></td>"
f"<td>{cell(row.get('purpose', 'Advance the decision'))}</td>"
f"<td><span class='status'>{cell(row.get('source_status', row.get('evidence_status', 'needs review')))}</span></td>"
"</tr>"
for index, row in enumerate(slides, start=1)
)
qc_rows = "\n".join(
"<tr>"
f"<td>{cell(row.get('item', 'Open review item'))}</td>"
f"<td><span class='status'>{cell(row.get('status', 'open'))}</span></td>"
f"<td>{cell('Yes' if row.get('blocks_circulation') else 'No')}</td>"
"</tr>"
for row in qc
)
html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{cell(company)} | Pitch Deck Storyboard</title>
<style>
:root {{ --ink:#101d2c; --muted:#566476; --navy:#112437; --gold:#b88a3c; --line:#d7dce0; --paper:#f7f4ee; --white:#ffffff; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:var(--paper); color:var(--ink); font:15px/1.5 Arial, sans-serif; }}
.hero {{ background:var(--navy); color:var(--white); padding:52px max(6vw,48px) 48px; }}
.eyebrow {{ color:#ddba72; font-size:12px; font-weight:700; letter-spacing:.11em; text-transform:uppercase; }}
h1 {{ font:700 clamp(36px,4vw,50px)/1.08 Georgia, serif; margin:18px 0 14px; max-width:950px; }}
.subtitle {{ max-width:820px; color:#dbe2e8; font-size:17px; }}
.meta {{ display:flex; gap:32px; margin-top:35px; font-size:13px; color:#dbe2e8; }}
main {{ max-width:1160px; margin:0 auto; padding:38px 38px 60px; }}
.decision {{ background:var(--white); border-left:5px solid var(--gold); padding:23px 26px; margin-bottom:34px; }}
.decision h2 {{ margin:0 0 8px; font:700 24px Georgia, serif; }}
h2 {{ margin:38px 0 14px; font:700 28px Georgia, serif; }}
table {{ width:100%; border-collapse:collapse; background:var(--white); }}
th {{ background:#e9e2d5; color:#32485c; font-size:12px; letter-spacing:.05em; text-transform:uppercase; text-align:left; }}
td, th {{ border-bottom:1px solid var(--line); padding:14px 15px; vertical-align:top; }}
.status {{ color:#7c5b23; font-weight:700; }}
.note {{ margin-top:30px; color:var(--muted); font-size:13px; }}
</style>
</head>
<body>
<header class="hero">
<div class="eyebrow">Standalone HTML Storyboard | Working Draft</div>
<h1>{cell(company)} Pitch Deck Storyboard</h1>
<p class="subtitle">A banker-readable page plan for native deck development. This fallback is not a completed PowerPoint deck or circulation-ready client material.</p>
<div class="meta"><span>Prepared: {cell(as_of_date)}</span><span>Format: HTML fallback</span><span>Posture: Working draft</span></div>
</header>
<main>
<section class="decision">
<h2>Decision Framing</h2>
<p>Use the page architecture below to refine the storyline, close source gaps, and determine which exhibits should be built into a native editable deck.</p>
</section>
<h2>Proposed Deck Architecture</h2>
<table>
<thead><tr><th style="width:8%">#</th><th style="width:36%">Slide</th><th>Purpose</th><th style="width:18%">Evidence status</th></tr></thead>
<tbody>{slide_rows}</tbody>
</table>
<h2>QC And Open Items</h2>
<table>
<thead><tr><th>Review item</th><th style="width:18%">Status</th><th style="width:18%">Blocks circulation</th></tr></thead>
<tbody>{qc_rows}</tbody>
</table>
<p class="note">Deck-plan JSON is retained as support material. Complete native deck construction, source and model tie-out, rendered-artifact QA, and `ib-deck-qc` review before circulation.</p>
</main>
</body>
</html>
"""
target = out / "pitch_deck_storyboard.html"
target.write_text(html, encoding="utf-8")
return target
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--json-run-log", "--json", dest="json_run_log", action="store_true")
parser.add_argument("--quiet-human-output", action="store_true")
args = parser.parse_args()
out = args.output_dir
out.mkdir(parents=True, exist_ok=True)
payload = load_payload(args.input)
company = str(payload.get("company", "Subject Company"))
as_of_date = str(payload.get("as_of_date", "Date not provided"))
slides = as_rows(
payload.get("slides") or payload.get("page_plan"),
[
{
"slide": 1,
"title": "Situation Update",
"purpose": "Frame decision",
"source_status": "needs review",
},
{
"slide": 2,
"title": "Recommendation",
"purpose": "Lead to action",
"source_status": "needs review",
},
],
)
qc = as_rows(
payload.get("qc_items"),
[{"item": "Source/model citations", "status": "open", "blocks_circulation": True}],
)
intake = write_support_payload(out, payload)
html = write_storyboard_html(out, company, as_of_date, slides, qc)
write_artifact_manifest(
out,
"pitch-deck-builder",
"html_report",
html,
companion_deliverables=[],
support_artifacts=[
artifact_item(
intake,
"support_artifact",
"json",
"Deck plan/control JSON.",
False,
True,
"Deck-plan JSON is support/control material.",
)
],
blocked_or_partial_status={
"status": "partial",
"reason": "HTML storyboard requires native deck construction, source/model tie-out, and deck QC before circulation.",
"missing_inputs": ["native deck build", "source/model tie-out", "deck QC"],
},
)
summary = {
"primary_human_deliverable": str(html),
"manifest": str(out / "manifest.json"),
}
print_summary(
args.json_run_log,
args.quiet_human_output,
summary,
["Pitch deck standalone HTML storyboard complete", f"Open first: {html}"],
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 002eaf6fd940a99297215a689adace255c1f0faf06e120130d0185b7b7195e2a