Deterministic render — spec for render_thread.py (BUILT 2026-10-06: scripts/statics/render_thread.py, see "As built" at the end)
What exists (checked 2026-10-06)
- No iMessage / UI-thread renderer anywhere in
scripts/,apps/oroutput/(grep: imessage, html2image, ImageDraw,.screenshot(). The 7-25 m1c text thread was an image-model render from a Pinterest ref; its build script isn't on disk. gen_statics.pyrenders only through GPT Image (scripts.cartoon_h3.canvas.gen_canvas_fal) or Nano Banana. Image models garble UI text, so it can't be used here.- Building blocks that DO exist:
- Python Playwright (
import playwrightOK) with Chromium in~/Library/Caches/ms-playwright/chromium-1208. - Pillow 12.1.1. PIL compositing precedent:
output/dawn-iterate-2026-10-04/statics/compose_c1.py(cutout + contact shadow); font fitting:_fit_font()inscripts/cartoon_h3/endcard.py. - Brand fonts:
knowledge/brands/dawnbands/fonts/Halant-700.ttf,Montserrat-400.ttf,Montserrat-600.ttf,Montserrat-800.ttf. - Product:
knowledge/brands/dawnbands/product-cutout-unlit.png(transparent, LED unlit). - UI font:
/System/Library/Fonts/SFNS.ttf(SF Pro; licence caveat in SKILL.md §12). Ad default since 10-06: Inter (OFL)knowledge/brands/dawnbands/fonts/Inter-400.ttf+Inter-600.ttf. - Missing: a real iOS Messages light-mode screenshot ref (
references/ui-refs/, empty). The renderer exists; its iOS CSS values are provisional until the ref is measured.
Pipeline
- Ref first. Source 2-3 real current-iOS Messages light-mode screenshots (Reddit r/iphone or r/ios via StealthyFetcher, per
reference_scrapling_reddit_access). Save toskills/format-ui-mockup/references/ui-refs/ios-messages-light-<n>.png, cropped to the thread (no status bar, usernames removed). Measure from the ref and write down: bubble radius, padding, max bubble width (%), greyreceivedand bluesentfills, text colors, sender-group spacing vs turn spacing, tail shape, header layout. CSS values come from the ref, never from memory. - HTML. One self-contained HTML per static, generated from
spec.json(lines.bullets+thread.sides+thread.contact+thread.receipt).@font-facepoints toSFNS.ttf(or the licensed substitute). Header = initials avatar circle + contact name + chevron only. Then bubbles, then "Delivered" right-aligned under the lastmebubble. No status bar, no timestamp row, no input bar, no keyboard. - Screenshot. Playwright Chromium, viewport width 390 (iPhone logical width),
device_scale_factor=3,locator('#thread').screenshot()→renders/<slug>.thread.png(≈1170 px wide). - Frame (PIL), 2048×2048, sand
#f3ece4: - Caption 0-18%: Halant 700, slate navy#2f4a63, centered, max 2 lines, fit with_fit_font(start ~120 px). Optional gold#fdac2bunderline under one word. - Thread card 20-64%:thread.pngscaled so the bubble text renders ≥60 px tall at 2048; rounded 48 px corners, white card, soft shadow (blur ~30 px, 18% black). Width ≤80% of canvas, centered. If the thread at 60 px text overflows 44% height, the copy is too long: cut a bubble, never shrink the text. - Product zone 66-96%: cutout left (~28% canvas width, contact shadow per compose_c1.py); right column =lines.sub(Montserrat 600, ~58 px) thenlines.mechanism(Montserrat 400, ~50 px), slate navy; pill centered at the bottom, navy#2d3452, white Montserrat 600 text =lines.pill. - Outputrenders/<slug>.png+renders/<slug>.render.json(fonts, sizes, card box, sha1 of the ref used). - Checks before review: OCR-free text check = the HTML is the source, so diff
linesagainst the HTML text nodes; assert bubble font px ≥60 at 2048; assert no\d{1,2}:\d{2}anywhere in the HTML. - Review:
python3 scripts/statics/static_review.py <B>/renders --spec <B>/spec.json. Move or delete the*.thread.pngintermediates out ofrenders/first: static_review reviews every png in the dir, and stage-session stages every image.
Variant B (photo overlay)
Same thread.png, composited onto a real home photo of the band (Fish-shot; the only real worn-band asset today is output/dawn-iterate-2026-10-04/statics/assets/arm-real-unlit.png). Card at 55-65% width in the photo's quiet area; its drop shadow offset points AWAY from the photo's key light (direction noted at the shoot), so the card reads as lying in that light, not pasted on. Before compositing, check the photo at 100% zoom: band edges sharp, colour true to the matte black unit. Mechanism + pill in a sand bar across the bottom 22%, caption in a sand bar at the top. The band must be lying flat or worn naturally: static_review failed every non-flat placement on 10-04 (illogical_product_placement).
Build estimate
~150 lines (HTML template + Playwright + PIL frame), one session, $0. Accept test: render the §Structure example from beat-sheet.md with placeholder-free real VOC, side-by-side with the iOS ref, Fish's eye, then static_review PASS/WARN.
As built (2026-10-06)
python3 scripts/statics/render_thread.py <spec.json> [--out DIR] [--only slugs] [--size 2048x2048[,1080x1350,1080x1920]] [--text-px N] [--ui-font inter|sfpro] [--watermark TEXT]. Out:renders/<slug>[.<WxH>].png+.render.json(fonts + licence, device scale, bubble px, card/pill/product boxes,ios_values: provisional|measured+ ref sha1). HTML +*.thread.pnggo tothread/besiderenders/(step 6 needs no move).- Deviations from the spec above: no resample (Playwright device scale = text px ÷ 17, so the 3x screenshot is replaced by an exact-size one); default bubble text = the 60 px/2048 floor; tails are inline SVG; layouts for 4:5 (caption 2-15 %, card 17-65 %, product 67-87 %) and 9:16 (Meta Reels safe box: everything in y 14-65 %, sides 6 %).
- Checks (exit 1): spec (3-6 bubbles, ≤12 words each, ≤40 total, sides parallel, receipt "Delivered", no clock time), HTML bubble text ==
lines.bullets, no\d{1,2}:\d{2}in the thread, UI font loaded, card fits its zone at the floor text size. - Finding for Fish: at 60 px/2048 the 1:1 card zone (44 % of height) fits the contact header + about 3 one-line bubbles; a 4-bubble thread with one two-line bubble overflows 2048² (1376×1115 vs 901 px) but fits 1080×1350. "3-6 bubbles" in §6 can't hold at 1:1 under the text floor: 1:1 threads run 3 short bubbles, or the floor/zone moves (his call).
- Test renders (ILLUSTRATIVE placeholder copy, stamped on the image, not VOC):
output/ui-mockup-test/renders/ph-{coparent,teen}.{1080x1350,1080x1920}.pngfromoutput/ui-mockup-test/spec.json.