#!/usr/bin/env python3 """Pre-generate state-graph SVGs for every scenario. Calls mermaid.ink once per scenario, validates the response is a real SVG, writes to outputs/state_graphs/.svg. Run this once before deploying: uv run python scripts/generate_state_graphs.py The Gradio UI then serves these files via the /state_graphs/ static mount, so there's no per-request network call to mermaid.ink at runtime. """ from __future__ import annotations import re import sys from pathlib import Path import httpx # Make project root importable sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from server.ui_data import build_mermaid, load_scenarios, mermaid_to_url OUT_DIR = Path(__file__).resolve().parent.parent / "outputs" / "state_graphs" def fetch_svg(mermaid_code: str) -> str | None: """Hit mermaid.ink, return SVG text or None on failure.""" url = mermaid_to_url(mermaid_code) try: resp = httpx.get(url, timeout=20.0, follow_redirects=True) except Exception as e: print(f" ERROR: HTTP request failed: {e}") return None if resp.status_code != 200: print(f" ERROR: status {resp.status_code}") return None text = resp.text if not text.lstrip().startswith(" attribute, # or add a new one if missing — never duplicate. extra = "max-width:100%;height:auto;display:block;margin:0 auto" if re.search(r']*\sstyle="', text): text = re.sub( r'(]*\sstyle=")([^"]*)(")', lambda m: f'{m.group(1)}{extra};{m.group(2)}{m.group(3)}', text, count=1, ) else: text = re.sub( r"]*)>", rf'', text, count=1, ) return text def validate_svg(svg_text: str, scenario_id: str) -> tuple[bool, list[str]]: """Sanity checks on a generated SVG. Returns (ok, list of issues). """ issues: list[str] = [] if len(svg_text) < 1000: issues.append(f"too small ({len(svg_text)} bytes)") if " root tag") if "" not in svg_text: issues.append("missing closing tag") # Mermaid renders graph-related elements if 'class="flowchart"' not in svg_text and "flowchart" not in svg_text: issues.append("no flowchart class — mermaid may have failed to parse") # Check that we got at least a few node rects rect_count = svg_text.count(" elements (expected ≥ 3 for state nodes)") return (len(issues) == 0, issues) def main() -> int: OUT_DIR.mkdir(parents=True, exist_ok=True) scenarios = load_scenarios() print(f"Generating state-graph SVGs for {len(scenarios)} scenarios → {OUT_DIR}") print("=" * 70) successes = 0 failures: list[str] = [] for s in scenarios: sid = s["id"] print(f"\n[{sid}]") mermaid_src = build_mermaid(s) print(f" mermaid LOC: {len(mermaid_src.splitlines())}") svg = fetch_svg(mermaid_src) if svg is None: print(f" ✗ FAILED to fetch") failures.append(sid) continue ok, issues = validate_svg(svg, sid) if not ok: print(f" ✗ VALIDATION FAILED:") for issue in issues: print(f" - {issue}") failures.append(sid) continue out_path = OUT_DIR / f"{sid}.svg" out_path.write_text(svg) print(f" ✓ wrote {out_path.name} ({len(svg):,} bytes, {svg.count('