dawn-statics — brand-locked, card-locked statics
Why this exists (receipts, 9-03→9-04): 30 statics shipped with each layout's own palette (plum, teal, magenta, yellow boxes); Fish: "there should be some sort of theme congruence with the dawn product page… purple ones are just so off." Same batch carried my phrases in her slot ("the other one", "leftover mom", "different person for the first hour") and invented events; Fish: "fix all statics with real voc / real not inferred feelings/events." Both are now mechanical.
The eight locks (every static, no exceptions)
- Brand lock.
knowledge/brands/dawnbands/brand-kit.md+brand-board.png(pulled from the live PDP: off-white#faf9f7/ sand#f3ece4/ deep navy#2d3452; slate navy text#2f4a63; gold#fdac2bsingle accent; green checks; Halant serif headlines, Montserrat body; gradient lowercasedawnwordmark). The board is ALWAYS the third input image and the prompt carries the BRAND LOCK block. Re-pull the theme if the PDP changes (curlthe PDP, grepfont-familyand--color-*). - Product lock.
product-ref-new-band-2026-08-27.pngis the first input image; LED face UNLIT; never a square-faced watch. - Card lock (the core emotional appeal). Each static's callout is ONE item from the sub-avatar card (experience / emotion / behavior) in HER words. Every
vocline is grep-checked against the raw corpus (scripts/statics/voc_check.py; 0 hits = your phrase, rewrite). Photo-led or mechanism statics declarecard_iteminstead. Quotes on image are verbatim and carry NO source line: no "a mom on Reddit", no subreddit, no survey credit (Fish 10-03 + 10-06: it shows it's just a random person). Gate rule12 fails any attribution. Diverse callouts across the set: no two statics on the same item. - Qualifier + my-phrase ban. No clock times (one mother's verbatim "by 8 a.m." excepted), counts/durations, ages/grades, bus/phone/car/drop-off, checklists, lunch boxes, hair, uniforms, sibling gender outside a real quote; never "the other one", "leftover", "what's left of you", "different person", "first hour", "school version"; no similes, em dashes, "builds", "signal", "channel", "reports to nobody". ADHD named on every image.
- Rationale + alternates (added 9-04 from the Evolve intake brief). Every static declares
why(which card row it opens: experience / emotion / behavior / desire / angle, and why that line stops her scroll) andalts(2+ headline iterations on the SAME card item; they become the ad's titles). Alts run the same qualifier gate as the headline. - Congruency. The image depicts the card item, not a mood.
scenemust share a content word with the headline / voc / card_item or the gate fails; photo-led layouts (tt-064 / 105 / 116 / 143 / 165) require a scene. - Headline cap per layout, not global. Bold-headline layouts (tt-161, t01, t02, t05, t07) cap the headline and its alts at 8 words. Quote cards (tt-121, tt-045, tt-064) keep her full sentence; a cap there forces paraphrase, and paraphrase is my phrase in her slot.
- Layout-only references.
knowledge/brands/dawnbands/static-layouts/(10 TrendTrack proven runners + 13 Evolve winning-ads templates). A layout ref supplies layout, type hierarchy and spacing; never its colors. Pull fresh layout refs from TrendTrack (/ads/query, image ads, proven runners) when Fish asks for new references; never from our own winners alone.
The copy method (Fish 9-04: "the statics became a lot better after you did this to the copy. Add this too")
Run this on every line that will sit on a static, in this order, before the spec is written:
1. Grep every line against the raw corpus (scripts/statics/voc_check.py: raw thread dumps, the 774-quote VOC bank, 10-03 Reddit comment JSON, voc-pull-* pulls, the buyer survey CSVs, Fish's notes; never docs we wrote; prints the file of every hit; competitor + Okendo hits are listed as NOT COUNTED, 10-06; --files lists the corpus). Zero hits = our phrase. Replace it with the nearest line a mother actually wrote; do not paraphrase around it. Receipts: "the other one" → "the other kid" / "the little one"; "leftover mom" → "I feel so bad for the other kid"; "different person for the first hour" → "comes downstairs in a foul mood" / "fine at school, like it never happened"; "what's left of you" → "we're all stressed and running late every single day".
2. One card item per static, stated as she states it. No composite headlines that stack two ideas. Fifteen statics in a unit = fifteen different doors (experience / emotion / behavior rows of the sub-avatar card, plus the "Separate angles" table). Quotes stay verbatim and land on their own, with no source line (gate rule12).
3. No qualifier that is not the card's own item. Clock times (one mother's verbatim "by 8 a.m." is the only exception), counts and durations, ages and grades, bus / phone / car / drop-off, checklists, lunch boxes, hair, uniforms, a sibling's gender outside a real quote. Each one excludes a mother whose morning looks different for a reason unrelated to the desire.
4. Mechanism = the three ruled facts, plain. Wake-up spikes stress hormones before his feet hit the floor / he starts the day in fight or flight / an ADHD brain takes longer to come back down; then the product line (wakes on the wrist, no sound, no startle, turns it off himself). No "builds", "signal", "channel", poster fragments, inversions.
5. Captions match the post. Image copy is in the script's voice (second person "your ADHD kid…") and promises only what the primary text delivers ("why it happens is in the post", never "what changed it is below" when the body has no result story). Meta shows the primary text above the image: say "the post", not "below".
6. ADHD named on every image. Add it to the product line if the callout does not carry it.
7. Gate, not habit. gen_statics.py --check fails the spec on any zero-hit voc line, banned qualifier (headline, sub, bullets, attribution AND alts), missing card item, missing ADHD, missing layout ref, missing why / alts, over-cap headline on a bold layout, scene that does not depict the card item, the belief chain (rule9: default = 9-04 Apology-Mom chain; a spec or static may declare its own chain), before any image is generated. Opt-outs go in waivers ({"slug","rule","reason"}, rule id like rule9 or a failure substring; no reason = FAIL); --check/--list/--out/--dry-run and every 10-06 flag apply them and print each as WAIVED (10-06; spec format in the gen_statics docstring, items 10-11).
8. Approval before render. gen_statics.py --list prints the sheet (slug / layout / headline / item / why / alts / scene). Fish approves the callouts on that sheet; only then --out. A 30-static rebuild costs less at the sheet than at the hub.
Workflow
- Concept formula first (Fish 2026-10-03, binding): every static = proven structure (our long-running grid or a competitor long-runner) + verbatim customer line + belief they hold + buyer who converts. See
knowledge/concept-formula.md. - Read the sub-avatar card (research doc: card rows + the "Separate angles" table). List 10-15 callouts, one card item each, verbatim where a quote exists.
- Write
spec.json(shape inscripts/statics/gen_statics.pydocstring): slug, layout, scene, lines (headline / sub / bullets; never attribution, rule12),vocorcard_item,why,alts(2+). python3 scripts/statics/gen_statics.py spec.json --check→ must PASS.python3 scripts/statics/gen_statics.py spec.json --list→ paste the sheet to Fish; he picks / cuts callouts. Fix the spec, re-run--check.python3 scripts/statics/gen_statics.py spec.json --out <batch>/statics(--outis required on every render path since 10-06, no./staticsdefault; GPT Image 2.5 on fal, 2048 square, product + layout + board + logo as inputs;--backend nbonly if Fish asks for the old Nano Banana Pro path; layouts can come fromknowledge/brands/dawnbands/static-templates/(make_prompts.py); runuvfrom the workspace dir, not the output dir).- Design review is automatic (10-02,
scripts/statics/static_review.py, wired into--out): Gemini scores every render on text-exact / feed-legible / breathing-room / hierarchy / simplicity / scene-fit / brand / product. Two-vote FAIL (hard fail or any axis <6) moves to<out>/rejected/and re-renders once with the reviewer's fix as a TARGETED FIX (--retry-fails N, 0 = review only); WARN (mean <7) is kept and must be listed for Fish beside the static;review-summary.jsonper batch. Standalone:static_review.py <dir> --spec spec.json. Still eyeball full size for faces and reference artifacts; the reviewer misses borderline garbled props about half the time. - Statics carry the sub-avatar script (hook 1) as the one body + 3 titles + UTMs (
launch_angles.pypattern;url_tagson every creative). Stage to the hub, Fish image approval gates launch.
Not adopted from the Evolve intake brief (and why)
The 11 famous-brand example ads (Ryanair, Tesla, Patrón, Corona, Chipotle, Huel…) as the headline source: nine are wordplay for brands with existing awareness; the said-out-loud and direct-but-new rules ban that register. Per-example palettes: the 9-03 purple problem. A global 6-8 word cap: kills her verbatim sentence on quote cards. Curiosity-only openers with no reader named: the parked unaware lane. Our layout refs + the card + the corpus replace all of it.
QA regen mechanics (first full run, SA2 9-04: 18 statics, 4 rejected on pass 1, 3 on pass 2, all clean on pass 3)
fixfield on a static = one-line targeted correction appended to the prompt; move the rejected png tostatics/rejected/<slug>.vN.pngand re-run--out(existing files are skipped). Never re-prompt from scratch.lines.button= the only button text. Layout refs with their own button (tt-161 "Start Repair Today") get copied verbatim unless the button is an explicit spec line; a note infixalone did NOT stop it. TAIL now defaults every copied button to "Shop Dawn Band".- tt-161's reference band has a lit LED face; the model copies the digits. The fix that worked: "front panel is a blank matte black rectangle with NOTHING on it" in
fix, plus the button line. - pov / lifestyle photos with a person in frame render a readable face two runs out of two, even with "seen from behind". Objects-only scenes (mug, band, backpack, morning light) pass first time. Prefer no people on photo-led layouts.
- The hub body for a static batch = the sub-avatar script (
script.json: 3 hooks, 3 titles, body after hook), gated bybuild_ads.pywith the same QUAL regex + voc check before ads.json is written.
Review gate is mandatory (Fish 2026-10-04: "is this static check running for all static generations from now on")
scripts/statics/static_review.py(RUBRIC_VERSION 2026-10-04) hard-fails bare_wrist, worn_band_wrong, illogical_product_placement, no_adhd on top of the earlier list. It stamps every review withrubric_version+ the imagesha1and writes<stem>.review.jsonnext to the image.~/ad-batch-hub/stage-session.py --type staticREFUSES to stage (before any write) unless every image has a passing sidecar on the current rubric for those exact bytes. Custom batch scripts can't bypass it: run static_review on the batch dir before staging.--no-review-gate= Fish-approved exceptions only.- Worn bands: composite a real band-on-wrist photo; never ship a generated wrist band. Eyeball every static at full size before it reaches Fish.
Next format lane: rip templates from NON-direct competitors (Fish 2026-10-04, binding for upcoming static batches)
Fish: "need to start trying different static formats too in the future ripping template from non direct competitors like native BOF like the band in real life with a note for example." - Source formats from long-running static winners OUTSIDE the wrist-alarm category (adjacent mom-buyer brands: Saphire, Hello Bloom, First Day, EllaOla, Loftie, NATPAT, Big Life Journal; plus any DTC long-runner), found on TrendTrack (browser route, CDP 9333) by days running / duplicates, then live-checked on FB Ad Library. Our own D01–D14 grids are the control, not the source. - Priority format: native bottom-of-funnel "product in real life": a real photo of the Dawn band in an actual home moment (nightstand, kitchen counter, backpack strap, a teen's wrist) with a handwritten or sticky note / text-message overlay carrying the callout. It should look like a mom's photo, not an ad. - Rip = copy the FORMAT (layout, photo style, note device, text placement), never their copy or claims. The band must be the real product (photographed or real-cutout composite); the callout is our verbatim VOC; the review gate still applies (static_review + hub gate). - Each new format gets its own ad set (one variable = the format) against our proven body, with a floor.
Reference implementation
output/dawn-sa2-apology-2026-09-04/ — spec.json (18, eight-lock gate, why/alts/button/fix fields), script.json, build_ads.py, hub/ (staged as hub session 2026-09-04-dawn-band-static). Older: output/dawn-evolve-angles-2026-09-03/ — spec-a4.json / spec-a5.json (30 statics, five-lock gate), jobs-statics-6…9.json, gen_nb.py, sheets. Plan: ~/.claude/plans/dawn-evolve-angles-2026-09-03.md.
- Fidelity (9-04): photographed band only, logo file as 4th input, quote → rule → mechanism → product → guarantee pill;
lines.buttontext only, never a label copied from the layout reference. - Product fidelity, hard path (9-04): the product ref is now
knowledge/brands/dawnbands/product-cutout-unlit.png(transparent, LED repainted). When the model still redraws the band (it did on a4-s4 and a5-s13 even with the cutout as input 1), render the layout with "NO PRODUCT, leave the area empty" and paste the cutout with PIL; for product-hero layouts compose the whole static in PIL with the brand fonts (knowledge/brands/dawnbands/fonts/: Halant-700, Montserrat-400/600). Deterministic beats another regen.
Fish 2026-09-30 rulings (audit of hub 2026-09-30-dawn-band-native) — binding
- Generator = GPT Image 2 (
higgsfield generate create gpt_image_2 ...), not GPT Image 2.5, for statics. - Every static ties the hook to OUR solution with subtitles: root cause, then mechanism. Root cause = habituation ("Their brain tunes out a sound it hears every morning." / "It was never about volume."); mechanism = "Dawn vibrates on the wrist and keeps getting stronger until they press the button." (Fish-approved lines; never "can't be tuned out").
- Plain words only in quotes/headlines (rejected: "impervious"). Quotes short enough to read in one glance (≤ ~15 words); pick a shorter verbatim line rather than cramming a long one.
- Scenes must make sense for the line: morning light, not night, for a wake-up static; the background depicts the stated situation.
- Inspiration = fresh TrendTrack static winners (image ads, long-running) for format ideas, never a full layout copy.
- Rejected formats/looks: cramped quote cards, cluttered us-vs-them at night, busy testimonial composites. Clean hierarchy: headline → root-cause sub → mechanism sub → product → guarantee pill.
10-06 upgrades (davidaistar diff)
Source: ~/research/davidaistar/analysis/diff_statics.md (top-5 items 2-5). All in scripts/statics/gen_statics.py, fal backend only. The eight locks hold on every pass: input order is always 1 product, 2 layout ref (or the image being edited), 3 brand board, 4 logo, 5 scale ref (hero only); PROD + BRAND LOCK + HIER blocks ride every prompt; gate() (VOC grep, qualifier ban, card item, ADHD, why/alts) runs before any path renders. Spend convention unchanged: gate PASS, --list sheet approved by Fish, then an explicit --out. The new paid flags refuse to run without --out (or --dry-run). Backup: gen_statics.py.bak-2026-10-06.
| Flag | When | What it does |
|---|---|---|
--new-layout |
First use of a layout that is NOT a proven D0x template | Unlocks --improvise / --candidates >1. Refuses any static whose template or layout ref is D\d\d: proven templates keep the one-pass exact-text render. Code-level "proven" = D\d\d ONLY: the tt-/t0 files in static-layouts/ (TrendTrack runners, Evolve templates) pass as new, so don't use --improvise on them unless the layout is genuinely new to us. |
--improvise |
New layouts only (with --new-layout) |
Pass 1 <slug>.layout.png: layout + product + scene, placeholder copy, no exact text (forcing exact copy on gen 1 degrades composition). Pass 2 = edit pass on that image with the exact text, then the normal review + retry. A retry re-runs only the edit pass; delete <slug>.layout.png to redo the composition. |
--candidates N |
New layout, N = 2-3 | N compositions per slug, all saved (<slug>.cN.png, review sidecars written, nothing moved or picked). With --improvise: N layout-only bases (<slug>.layout.cN.png), no review, then finish the pick with --recycle. |
--recycle PNG (+ --only SLUG) |
Refresh an approved static with a new card line | Edit pass: the approved PNG goes in slot 2, only the words change. If --out already holds <slug>.png (e.g. the approved base itself), the edit writes <slug>.r2.png (then .r3...) and reviews that file; the base is never touched (fixed 10-06, dry-run verified). Spec must resolve to one static. Gate runs on the new text; refuses a base whose .review.json FAILED; warns if no sidecar. The output goes through static_review like any render. The PIL path ("Product fidelity, hard path" above) is still the deterministic option when the edit drifts. |
--scale-ref PATH |
Product-hero statics: D01 / D10 / D13, or "product_hero": true |
Adds a hand-held product photo as the 5th input so the model sizes the band from a hand, not a flat studio shot. Default knowledge/brands/dawnbands/product-ref-handheld.png. |
--edit-model ID |
Testing a text-pass model | Model for the edit pass only (improvise pass 2, recycle). Default = GPT Image 2.5 edit (proven). The registry candidate ideogram/v4.5/edit ("Exact text on image" role) is UNTESTED: its fal request body may differ from GPT Image's, so dry-run it, then run one paid test before any batch uses it. |
--dry-run |
Any of the above (fal only) | Prints every prompt and input list. No API call, no review, no files. Honoured on --backend nb too (fixed 10-06: prints the nb prompt + inputs, no call). Any render without --out or --dry-run refuses (fixed 10-06). |
--only a,b / --spec |
Slug filter; --spec is an alias for the positional spec. |
Scale-ref asset (missing as of 10-06): product-ref-handheld.png does not exist yet. Until it does, every product-hero static prints a warning and renders without it. Per the 10-05 rule, it must be a REAL photo of the Dawn band on a wrist or in a hand: our own shot first. An Okendo review photo only after checking it against product-truth (diagonal ribbing, small flush display, dark when idle): 94.6 % of the Okendo corpus predates the store's first order (output/dawn-bottleneck-2026-10-03/E-shopify-nonbuyer.md §6a), so its photos may show another unit. Never generate it. Strip EXIF and crop it to band + hand. Note SCALE_RULE in the code still says "Okendo review photo or our own shot" without that check.
Hub staging: stage only the final <slug>.png. Intermediate *.layout*.png and unpicked *.cN.png stay out of the hub folder (no sidecar / not picked).