render qc · canary run

Director Title Cards

Per-phase gpt-image-2 title cards composited via Remotion, tested against 7 real, already-rendered ads — 4 distinct products (Gymshark, Pelagic Gear) spanning Reels, Stories, Feed and PMax landscape, plus one product's 3 real sibling ads across every funnel-stage intent profile. Flag ADGEN_DIRECTOR_TITLE_CARDS stays off pending the fixes below.

2 defects · not ready to ship
$0.78
real spend
7
ads tested
18
cards generated
4
formats covered

Takes

4 distinct products · real veo masters, read-only fetch
6a9c65e6…50c8
reels9:16
Running High Neck Long Sleeve Top · Gymshark
hook frame0:01.9hook
proof frame0:05.2proof
close frame0:07.2close
bug

All 3 timestamps render the hook card. The proof/close cards were generated and billed but never appear — at 7.2s the close frame still reads “The top for your run and your coffee run” instead of the product name + benefits, and it now sits directly over the shark logo.

6a9c65e8…50f5
stories9:16
Running High Neck Long Sleeve Top · Gymshark
hook frame0:01.2hook
close frame0:05.7close
bug

Same sequencing defect as Reels — the close-phase timestamp still shows the hook headline. Confirms this isn’t a Reels-specific quirk; it reproduces on every 3-phase vertical format tested.

6a9c65ea…5113
square1:1
Running High Neck Long Sleeve Top · Gymshark
only framesingle cardmain
pass

Single-phase format has nothing to sequence. Clean two-weight typesetting, correctly inset to the square safe zone, no collision with the logo mark. This is the case the feature already gets right.

6a9c65e5…50b2
landscapeYt16:9
Running High Neck Long Sleeve Top · Gymshark
phase 1 frame0:00.8phase 1
phase 2 frame0:08.3phase 2
bug

Cards do switch here — but phase 1 renders as a dithered, washed-out ghost instead of crisp text (an alpha-compositing artifact), and phase 2’s benefits copy overflows past the bottom of frame while still double-exposed against phase 1’s residue.

Intent profiles

1 product · 3 real sibling ads · funnel stage is the only variable

Copy varies today by exactly one production input: Ad.funnelStage. These three are genuine sibling Ad documents for the SAME real product — Pelagic Gear’s “Faroe” flannel, $80, Reels 9:16 — each independently rendered by production for a different stage. Nothing about the request was forced; resolveStagePresetName(ad) read each ad’s own stored funnelStage exactly as the live renderer would.

6a9b6f1a…92bae
reelsawareness
Faroe · Pelagic Gear · $80
hook framehookheadline
proof frameproofheadline
close frameclosename+benefits
intent

canonical-awareness: closing card carries productName and the full benefits list — “Lightweight all-day comfort · Versatile coastal style · Soft yet durable build.” Reads as a discovery ad: explain what it is and why it's good.

6a9b6f1b…92bc7
reelsconsideration
Faroe · Pelagic Gear · $80
hook framehookheadline
proof frameproofheadline
close frameclosename+benefits
intent

canonical-consideration: identical headline and closing content to awareness in this ad — same productName + same benefits list. Whatever this stage is meant to change didn’t surface as different TEXT here; if consideration should differ in substance, not just downstream targeting, that's a real gap to design for.

6a9b6f1c…92bcc
reelsconversion
Faroe · Pelagic Gear · $80
hook framehookheadline
proof frameproofheadline
close frameclosename only
bug + gap

canonical-conversion: the ONE real content difference across all three stages — the closing card drops the entire benefits list, showing productName alone with no price, no CTA, no urgency. If conversion-stage ads are meant to close harder, this preset currently does the opposite: it says less. Also reproduces the sequencing bug — every sampled timestamp on this ad shows the hook headline, so in practice a real conversion ad never even reaches this thin closing card today.

Findings

4 issues · ranked by severity
high

Multi-card sequencing never advances past the hook card

On both 3-phase vertical ads (Reels, Stories), proof/close cards are generated and billed via a real gpt-image-2 call, then never displayed — the composite holds the hook card’s image for the full render regardless of the sampled timestamp. Paid content silently never reaches the viewer on the two most common ad formats. The 2-phase landscape case does advance, so the defect is specific to how 3+ sequential phases wire into the composite, not the fade/timing envelope itself. Confirmed a third time on a completely different real product (Pelagic Gear “Faroe”) across all three funnel-stage siblings — not a Gymshark-specific or single-ad fluke.

repro: ad 6a9c65e6…50c8 & 6a9c65e8…50f5, timestamps 5.2s / 7.2s & 5.7s · also 6a9b6f1a/1b/1c…92bae/bc7/bcc (Faroe, all 3 funnel stages)
high

Alpha-ghosting and bottom-frame overflow on multi-card landscape

First card composites as a dithered, checkerboard-alpha ghost instead of opaque text. Second card’s benefits copy runs past the bottom edge of frame and shows a visible double-exposure against the first card’s fade-out.

repro: ad 6a9c65e5…50b2, frames at 0.8s & 8.3s
medium

Title text can collide with the brand’s own logo

A tight crop on the Reels close-phase frame shows generated title copy sitting directly over the stitched Gymshark logo. The card is generated blind to the actual video frame, so this class of collision is a structural risk of the approach rather than a one-off render.

repro: ad 6a9c65e6…50c8, frame at 7.2s
medium

The conversion preset's closing card says less, not more

Across the same real product's 3 funnel-stage siblings, awareness and consideration both close on productName + the full benefits list. Conversion — the stage where the ask should be hardest — closes on productName alone: no benefits, no price, no CTA text in the card. Whether this is intentional design (CTA/price live in platform chrome, not the card, at conversion) or an oversight is a product question, not a code bug, but it's worth a deliberate decision either way.

repro: ad 6a9b6f1a/1b/1c…92bae/bc7/bcc (Faroe), close-phase card
shipped

Burned-in safe zones — already fixed, unrelated to this canary

Reels/Stories safe zones were split from one collapsed vertical entry, the keep-out band gap is closed, and safeZoneKey now flows through the CJS plate-scan path in production (adgen #59 / backend #335). The one loose end is a stale line in session.d/KNOWN-OPEN.md still describing it as open — code disagrees with the doc, not the other way around.

Spend ledger

stage: director_title_card · CostLog
AdFormatCardsSpendResult
6a9c65e6…50c8Reels 9:163$0.1806sequencing bug
6a9c65e8…50f5Stories 9:163$0.1304sequencing bug
6a9c65ea…5113Feed 1:11$0.0100pass
6a9c65e5…50b2PMax 16:92$0.0702composite artifact
6a9b6f1a…92baeReels · awareness3$0.1304sequencing bug
6a9b6f1b…92bc7Reels · consideration3$0.1304sequencing bug
6a9b6f1c…92bccReels · conversion3$0.1304sequencing bug + gap
Total$0.7824
Method: a standalone canary (scripts/renderDirectorTitleCardsPreview.js) drives the real directorTitleCardService + directorTitleCardGenerate + Remotion TitleCardSlot stack against each ad’s existing untitled master (veoVideoUrl), read-only against the production database — Ad-collection writes are blocked at both the driver and model level, and adId is omitted from every generate call so no spend receipt lands on the ad. Videos and stills here are re-encoded for file size; visual defects reproduce at source resolution.