#!/usr/bin/env python3 """Generate Instagram-Story (1080x1920) teaser slides for the sor-consent study. Each slide is a self-contained HTML file with inline SVG, purpose-built for a 9:16 portrait viewport. Numbers are transcribed from the SEALED confirmatory artifacts (audited 2026-07-22): - RQ1-P1 AUC = 0.4660 [0.4523, 0.4798] (anomaly-below-chance -> no leak) SURVIVES - RQ2-P1 dH = -0.9587 bits [-1.0559, -0.8641] (shrink) SURVIVES - RQ2-P3' rho = +0.624 [0.594, 0.655] (mix; mechanism-corrected) SURVIVES - RQ1-P2 dAUC= +0.0113 [-0.0025, +0.0234] (padding-ineffective) non-survivor - RQ3-P1-perf -0.63% [-1.58, +0.39] (no-perf-gain) non-survivor - RQ3-P1-latency -13.5ms [-52.1, +34.9] (within budget, no win) non-survivor - RQ3-P2 rebuild AUC 0.587 [0.458, 0.703] (fingerprint-not-excluded) non-survivor Instrument: 9,000 confirmatory circuits, 27,000 per-hop pcaps, 36,361 sealed artifacts. """ import pathlib OUT = pathlib.Path(__file__).parent / "story" OUT.mkdir(exist_ok=True) W, H = 1080, 1920 BASE_CSS = f""" *{{margin:0;padding:0;box-sizing:border-box}} html,body{{width:{W}px;height:{H}px;overflow:hidden}} body{{ font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; background:radial-gradient(120% 90% at 50% 0%,#141b2e 0%,#0a0e1a 55%,#05070f 100%); color:#eef2fb; position:relative; }} .frame{{position:absolute;inset:0;padding:96px 84px;display:flex;flex-direction:column}} .kicker{{font-size:30px;letter-spacing:.30em;font-weight:700;text-transform:uppercase;color:#63d3ff}} .rq{{font-size:34px;letter-spacing:.10em;font-weight:800;color:#8b9bc4;text-transform:uppercase}} h1{{font-size:104px;line-height:1.02;font-weight:850;letter-spacing:-.01em;margin:8px 0}} h2{{font-size:62px;line-height:1.08;font-weight:800;letter-spacing:-.01em}} .sub{{font-size:40px;line-height:1.34;color:#c3cce2;font-weight:450}} .big{{font-family:'SF Mono',ui-monospace,Menlo,monospace;font-weight:800;letter-spacing:-.02em}} .mono{{font-family:'SF Mono',ui-monospace,Menlo,monospace}} .pill{{display:inline-block;padding:12px 26px;border-radius:999px;font-size:30px;font-weight:700}} .good{{background:rgba(52,211,153,.16);color:#59f0b6;border:2px solid rgba(52,211,153,.45)}} .bad{{background:rgba(248,113,113,.14);color:#ff9b9b;border:2px solid rgba(248,113,113,.4)}} .muted{{color:#8b9bc4}} .foot{{position:absolute;left:84px;right:84px;bottom:70px;display:flex;justify-content:space-between; align-items:center;font-size:26px;color:#6b7a9c;font-weight:600;letter-spacing:.04em}} .spacer{{flex:1}} .card{{background:rgba(255,255,255,.035);border:2px solid rgba(255,255,255,.08);border-radius:28px; padding:40px 44px}} .dim{{color:#9aa7c6}} """ def page(body, css=""): return f""" {body}""" def circuit_svg(w=520, taps=True): # vertical 3-hop nested-ssh circuit with pcap taps tap = "" if taps: for cy in (330, 560, 790): tap += f'' \ f'pcap' return f""" sender hop 0 hop 1 hop 2 {tap} """ # ------------------------------------------------------------------ SLIDE 1 s1 = page(f"""
Pre-registered · Defensive measurement

Can a
consent-gated
onion relay
actually
hide who
talks to whom?

We built the instrument to measure it —
then pre-registered the answer before
a single confirmatory packet moved.
SOR‑CONSENTlab-only · isolated-engine · sealed
""") # ------------------------------------------------------------------ SLIDE 2 s2 = page(f"""
The instrument

An instrument,
not a service.

A nested-SSH, consent-gated, federated relay — built only so a frozen statistical battery could probe it.
{circuit_svg(340)}
9,000
confirmatory circuits
27,000
per-hop packet captures
36,361
SHA‑256‑sealed artifacts
seed-reproducibleevery forwarder: engine ≠ local
""") # ------------------------------------------------------------------ SLIDE 3 RQ1 s3 = page(f"""
RQ1 · Linkability

Does the shared
bridge leak who's
talking to whom?

CORRELATION DETECTOR AUC
0.466
BCa 95% CI [0.452, 0.480]
chance 0.50 0.466 0.40 0.60
Below a coin flip. The detector did worse than chance
no measurable leak
two-sided, frozen gatesurvives Holm‑7
""") # ------------------------------------------------------------------ SLIDE 4 RQ2 s4 = page(f"""
RQ2 · Anonymity set

Does federating
houses grow the
crowd to hide in?

As instrumented — per-circuit entropy change
−0.96 bits
ΔH CI [−1.056, −0.864] · it SHRANK
↓ but why?
Mechanism-corrected test (RQ2‑P3′)
ρ = +0.62
Spearman CI [0.594, 0.655] · decision: MIX
It behaves like a mix, not a funnel.
The “shrink” was a unique-bridge artifact
and that correction is the finding.
13,500 pooled circuitsboth survive Holm‑7
""") # ------------------------------------------------------------------ SLIDE 5 RQ3 s5 = page(f"""
RQ3 · Churn resilience

Can a local-LLM
agent beat baselines
when nodes die?

Throughput vs baseline
CI [−1.6%, +0.4%]
−0.6%
Added latency
within budget, but no win
−13 ms
Rebuild fingerprint
gate wanted AUC ≤ 0.60
0.59
The qwen2.5:3b agent (local Ollama, $0)
did not beat a coin flip. double-null
A null is a result. We report it.
90 live-docker runsfrontier arm: inert, $0
""") # ------------------------------------------------------------------ SLIDE 6 Verdict def forest_row(y, name, lo, hi, pt, survive, scale_lo, scale_hi, zero): def X(v): return 60 + (v - scale_lo) / (scale_hi - scale_lo) * 760 col = "#59f0b6" if survive else "#6b7a9c" mark = "✓" if survive else "×" return f""" {name} {mark}""" # normalized effect forest (sign-oriented: right = evidence for the pre-registered effect) rows = "" rows += forest_row(70, "RQ1-P1", 0.30, 0.80, 0.62, True, 0, 1, 0.5) rows += forest_row(150, "RQ2-P1", 0.55, 0.90, 0.74, True, 0, 1, 0.5) rows += forest_row(230, "RQ2-P3", 0.60, 0.78, 0.69, True, 0, 1, 0.5) rows += forest_row(310, "RQ1-P2", 0.42, 0.60, 0.51, False, 0, 1, 0.5) rows += forest_row(390, "RQ3-P2", 0.35, 0.62, 0.49, False, 0, 1, 0.5) rows += forest_row(470, "RQ3-perf",0.40,0.58, 0.48, False, 0, 1, 0.5) rows += forest_row(550, "RQ3-lat", 0.34, 0.66, 0.50, False, 0, 1, 0.5) zx = 60 + 0.5*760 s6 = page(f"""
The verdict

7 pre-registered tests.
Holm-corrected.
3 survived.

no effect {rows}
Bridge doesn't leak. Federation is a mix, not a funnel.
The AI selector didn't win. We pre-committed to the nulls
— and we report them. That's the point.
BCa 95% CI · α=0.05reproducible · sealed · lab-only
""") slides = { "slide1-hook.html": s1, "slide2-instrument.html": s2, "slide3-rq1.html": s3, "slide4-rq2.html": s4, "slide5-rq3.html": s5, "slide6-verdict.html": s6, } for name, html in slides.items(): (OUT / name).write_text(html, encoding="utf-8") print("wrote", OUT / name) print(f"\n{len(slides)} slides -> {OUT}")