#!/usr/bin/env python3 """ VerdictTank PDF Generator - fills template with verdict data, renders to PDF via WeasyPrint. Usage: python3 generate-pdf.py [--data data.json] [--output verdict-report.pdf] """ import json, os, sys, re from datetime import datetime from weasyprint import HTML TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "pdf-template.html") OUTPUT_DIR = "/var/www/verdicttank/reports" EM_DASH_PATTERN = re.compile(r'\u2014|\u2013') # em dash and en dash def clean(text): """Strip all em/en dashes - replace with hyphen.""" return EM_DASH_PATTERN.sub('-', str(text)) def score_bar(score): if score is None: return ' N/A' width = score * 8 if score <= 3: cls, label = "score-critical", "Critical" elif score <= 5: cls, label = "score-warning", "Weak" elif score <= 7: cls, label = "score-ok", "Adequate" else: cls, label = "score-good", "Strong" return clean(f' {score}/10 - {label}') def flaw_label(severity): mapping = { "Existential": "flaw-existential", "Structural": "flaw-structural", "Financial": "flaw-financial", "Operational": "flaw-operational", "Brand-fatal": "flaw-existential", } cls = mapping.get(severity, "flaw-structural") return clean(f'{severity}') def build_score_rows(scores): rows = "" for dim in scores: rows += clean(f'{dim["name"]}{score_bar(dim["score"])}{dim["note"]}\n') return rows def build_flaws(flaws): html = "" for i, f in enumerate(flaws, 1): html += clean(f'''
#{i} {f["title"]} {flaw_label(f.get("severity", "Structural"))}

{f["detail"]}

\n''') return html def build_judge_cards(judges): """Build judge cards - uses data-provided names (expected: Judge 1, Judge 2, Judge 3).""" html = "" for i, j in enumerate(judges): verdict_html = "" if j.get("verdict") == "upheld": verdict_html = 'UPHOLDS' else: verdict_html = clean(f'{j.get("verdict", "DISAGREES")}') confidence = j.get("confidence", "") conf_html = f'{confidence} confidence' if confidence else "" html += clean(f'''
{j["name"]}
{verdict_html} {conf_html}

{j.get("note", "")}

\n''') return html def build_strengths(items): return "\n".join(clean(f"
  • {s}
  • ") for s in items) def build_action_rows(actions): html = "" for a in actions: p = a["priority"] cls = f"priority-p{p}" if p in ("0", "1", "2") else "" html += clean(f'P{p}{a["action"]}{a["why"]}{a["effort"]}\n') return html def build_research(data): html = "" for r in data: html += clean(f'''
    {r["label"]}
    {r["text"]}
    Source: {r.get("source", "Research sweep")}
    \n''') return html def build_competitor_rows(competitors): html = "" for c in competitors: html += clean(f'{c["name"]}{c["strengths"]}{c["gaps"]}{c["threat"]}\n') return html def build_judge_notes(judges): """Judge notes - uses data-provided judge names, no model identifiers.""" html = "" for i, j in enumerate(judges): if j.get("dissent"): html += clean(f'''
    {j["name"]}: {j["dissent"]}
    \n''') else: html += clean(f'''
    {j["name"]}: No dissent. Agreed with findings and scoring without material deviation.
    \n''') return html def build_citations(citations): html = "" for i, c in enumerate(citations, 1): url_html = f' {c["url"]}' if c.get("url") else "" html += clean(f'
    [{i}] {c["text"]}{url_html}
    \n') return html def fill_template(data): with open(TEMPLATE_PATH, "r") as f: template = f.read() judges = data.get("judges", []) replacements = { "{{PROPOSAL_NAME}}": clean(data.get("proposal_name", "Untitled Proposal")), "{{SUBMITTER_NAME}}": clean(data.get("submitter_name", "Unknown")), "{{REVIEW_DATE}}": clean(data.get("review_date", datetime.now().strftime("%B %d, %Y"))), "{{REVIEW_ID}}": clean(data.get("review_id", "VT-" + datetime.now().strftime("%Y%m%d-%H%M"))), "{{EXECUTIVE_SUMMARY}}": clean(data.get("executive_summary", "")), "{{SCORE_ROWS}}": build_score_rows(data.get("scores", [])), "{{FATAL_FLAWS}}": build_flaws(data.get("fatal_flaws", [])), "{{JUDGE_CARDS}}": build_judge_cards(judges), "{{JUDGE_MAJORITY_VERDICT}}": clean(data.get("judge_majority_verdict", "NO CONSENSUS")), "{{JUDGE_AGREEMENT}}": str(data.get("judge_agreement", 0)), "{{JUDGE_TOTAL}}": str(len(judges)), "{{STRENGTHS}}": build_strengths(data.get("strengths", [])), "{{RESEARCH_FINDINGS}}": build_research(data.get("research", [])), "{{COMPETITOR_ROWS}}": build_competitor_rows(data.get("competitors", [])), "{{ACTION_ROWS}}": build_action_rows(data.get("action_plan", [])), "{{JUDGE_NOTES}}": build_judge_notes(judges), "{{CITATIONS}}": build_citations(data.get("citations", [])), } for placeholder, value in replacements.items(): template = template.replace(placeholder, value) # Kill any remaining unreplaced placeholders template = re.sub(r'\{\{[A-Z_]+\}\}', 'N/A', template) return template def generate(data_path, output_path=None): with open(data_path, "r") as f: data = json.load(f) html = fill_template(data) if not output_path: os.makedirs(OUTPUT_DIR, exist_ok=True) client = data.get("submitter_name", "Client") safe_client = re.sub(r'[^a-zA-Z0-9]', '', client) if not safe_client: safe_client = "Client" output_path = os.path.join(OUTPUT_DIR, f"{safe_client}-VerdictTank-Report.pdf") HTML(string=html).write_pdf(output_path) return output_path if __name__ == "__main__": import argparse ap = argparse.ArgumentParser() ap.add_argument("--data", required=True, help="JSON data file") ap.add_argument("--output", help="Output PDF path") args = ap.parse_args() path = generate(args.data, args.output) print(f"PDF generated: {path}")