Upload in. Approved, queryable and rendered as an ad out.
The Creative Engine is a small library of approved photography that ad tools can query. Two Claude Vision calls read every upload. One fills the tags from fixed lists and measures where the subject sits. The other lists the brands it can read in the frame. Neither call writes anything. The 23 assets in review on 24 Oct carry no tags.
Four of the rules come from an earlier migration, where bulk writes put wrong data into thousands of records before anyone checked them. Each of those failures is a rule here.
1 · Visionmodel
thefeed_1784760109930, a licensed 1080×1920 Tour frame, uploaded 22 Jul. Vision drew the box and the crosshair: focal 0.35, 0.78, subject box at 0.10, 0.68, half the width and 0.28 of the height.
2 · Reviewperson
The card a person sees. Tags arrive filled from the lists. Caja Rural and Seguros RGA are read off the jersey and offered as suggestions; they are never stored. Brand, source, tier and format are left for the person.
3 · Layoutrule
Drawn from the spec and the record. Type block from x80, 920 wide, top at y300. Meta safe zone 120 to 960 by 250 to 1250. The model's box is in blue; nothing in the layout reads it.
4 · Renderedrendered
thefeed_tourdefeed_chews_9x16_04_v1.png. Headline auto-fit to 920px under a 190px ceiling. The focal point went in as the crop position, and on a 9:16 frame in a 9:16 ad it moves nothing.
The pipeline
Who decides what
Blue marks the steps where a model decides. The other six are a rule or a person. Both model steps suggest; neither writes.
A model decidesA rule or a person decides
Upload
signed URL
direct to storage
Ingest
same name → 409
status review
no tags written
Model: tags + composition
enum schema
suggests only
Model: brand suggestions
second call
advisory, unsaved
Person
edit chips · drag
brand · source · tier
approve or reject
Write
approved only
tags + focal
focal clamped 0 to 1
Query
/api/assets/query
approved by default
max 50 per page
Render
spec → png
A failed call leaves the card empty. The asset waits in review for manual tags.
Longest side over 5000px: Vision gets a resized copy, the stored file keeps its size.
has_text true: the frame is excluded from the ad batch, because a headline would collide with it.
Renderer: type block x80, 920 wide, top y300. Headline auto-fit, 190px ceiling. Focal point sets the crop only.
Both calls read the same image and fail separately. The focal reprocess route reruns only the composition half, eight assets per request.
One asset, with its owners
Every value has an owner
Copied from the approved record that /api/assets/query returned on 24 Oct 2026, and from the review card for the brand suggestions, which are never stored. The record does not say who approved it or how sure the model was, so neither appears here.
1
Focal point ← Vision, then the crosshair
The model returns the subject's centre. A person can drag it before approving. The card showed 0.350, 0.780 and the record holds the same values.
focal_x 0.35 · focal_y 0.78 · clamped to 0–1 on write · the renderer uses it as object-position
2
Subject box ← Vision
A tight box around the primary subject, here the rider and his shadow. It is stored and returned by the API. Nothing downstream reads it yet.
subject_box = {x 0.10, y 0.68, w 0.50, h 0.28}
3
Tone map ← Vision
Each horizontal third classed as clean, busy, light or dark. The ad spec copied the top value. The scrim is the same on every ad, so the value changed nothing.
tone_map top clean · middle clean · bottom busy
spec tone_top "clean" · not read by render-batch.mjs
4
Tags ← fixed lists
Structured output against an enum schema, so the model can only answer with values on the list. Brand, source, quality tier and output format are set by a person.
The second call read the team sponsors off the kit. Suggestions go to the card and nowhere else. The person filed the frame under The Feed.
suggested: Caja Rural · Seguros RGA (review card, never stored)
stored: brand = thefeed
Rejections
What failed
Three sources, labelled. The first is the migration. The second is the Creative Engine in use, from screenshots and the live API. The third is the renderer.
Migration
roadmap log · May to June 2026
3,546 assets tagged Optimized in one pass
exclusion list before any bulk write
Every 2000×2000 file got the tag. 2,771 were types that should never be marked Optimized, such as logos, nutrition panels and lifestyle frames. 775 were real. In the Engine, the focal backfill set aside Logo, Product Studio and no-person assets first: 18 went to centre by rule and 29 went to Vision.
/hpt showed 1,000 of 7,197
page every bulk read
Supabase returns at most 1,000 rows and says nothing. Mortal Hydration showed 1,000 of its 1,121. The Engine's backfill reads in pages of 1,000 until a page comes back short.
6,750 athlete photos with a default brand
brand is suggested, then confirmed
The migration set one brand on every upload. In the Engine the brand call is advisory and its suggestions are never written.
"AI unavailable"
the error shows on the card
An invalid API key returned 401 and the queue said nothing more. In the Engine the error text prints on the card, and the asset waits in review for manual tags.
Creative Engine, in use
review queue screenshots and /api/assets/query · June to October 2026
5464×8192 jpeg
resized copy for Vision
Vision refuses any side over 8000px. The asset was saved, the card printed the error and offered manual tags. Vision now gets a copy resized to 5000px on the longest side; the stored file keeps its size.
has_text = true
model tag · excluded from the batch
Vision found type already in the frame. The ad spec drops any frame with has_text true because a headline would collide with it. Two frames went out this way.
Sun
Outdoor Sunny
not caught by a rule
A person typed "Sun" next to the existing value. This card was fixed before approval. Two other approved frames carry "Outdoor Sun", because confirm saves any value it is given.
Admin › Focal Reprocess. 29 / 29 done.
Backfilled, with the list first
The Maurten pilot was approved before the focal fields existed, which left 47 approved assets with none. The backfill ran dry by default, wrote its rollback file before writing anything, and split them by rule.
Logos, studio product shots and frames with no person went to the centre, 0.5 by 0.5, without a model call. The other 29 went back through Vision in batches of 8.
Pending (approved, focal_x IS NULL): 47 · default to center (no Vision): 18 · needs Vision reprocess: 29
rollback-focal-1781203467632.sql · 18 asset_ids
The renderer
renderer/CONTEXT.md · Tour de Feed batch · 21 frames
Headline and support only. The wordmark was taken out of the static path.
Logo placement
I tried to place the wordmark by rule. The best single fixed position worked on 14 of the 21 frames, or 16 at 360px. Placing it per ad found a spot on 18, but the positions scattered and read as unconsidered, and 3 frames had no clean spot at any position or colour.
Placing a mark over photography nobody controls is a composition call, so designers place it. If it comes back, the direction is to choose a corner from the focal point.
fixed: 14/21 · fixed at 360px: 16/21 · per-ad: 18/21, scattered · no clean spot: 3
The set
The twenty-one ads
Spec order. The first thirteen came through the Engine, with focal point and tone taken from the approved record. The last eight, frames 16 to 23, were rendered from local files at a neutral 0.5, 0.5. Same type block, same scrim on all twenty-one.
chews_04 Final stage, / final savingshydration_13 Last climb, / last chancegels_08 The final / descentgels_10 Fuel the / last stagewaffles_15 Last stage, / last dealswaffles_01 Ride it outsitewide_12 Tour de Feed / ends soonsitewide_02 Whole tour / on salegels_10alt The final / descentsitewide_11 Final stage, / final savingschews_03 Last climb, / last chancewaffles_05 Ride it outsitewide_06 Tour de Feed / ends soonsitewide_16 Last drop, / last deal · local framesitewide_17 Last podium, / last prices · local framesitewide_18 Blink and / it's over · local framesitewide_19 Last climb, / last call · local framesitewide_20 Down to / the wire · local framesitewide_21 Last push, / last chance · local framesitewide_22 Last sprint, / last savings · local framesitewide_23 Still time / to save · local frame
Numbers and limits
Counts and limits
71
approved and queryable
0on 23
tags on assets in review
18of 47
focal points set by rule, no model
2
frames dropped for has_text
What's in it
71 approved: 48 Maurten vendor uploads and 23 licensed frames filed under The Feed. 23 in review and 21 rejected. Built from 9 June to 22 July 2026 in 14 commits.
What stays human, and why
Brand. The model reads logos off kit and packaging, and the project notes warn that it can confuse Maurten's M with Myprotein's MP.
Source, quality tier and output format. They describe rights and intent, which aren't in the pixels.
AI Generated. Vision can't reliably tell a good generated image from a photograph.
Logo placement, for the reasons above.
Approval. Nothing reaches the query API without it.
Sources
Counts, tags and composition: /api/assets/query, read 24 Oct 2026. Rules: the Creative Engine source and its rollback file. Migration figures: the roadmap log. Renderer figures: CONTEXT.md and thefeed_tourdefeed_9x16_spec.json.
Known weaknesses
Quality tier is stored twice. A person tagged 15 approved assets Select, but the query API filters on a separate column that reads standard on all 71, so a request for Select returns nothing.
Confirm saves any value it is given. "Outdoor Sun" now sits on two assets next to "Outdoor Sunny".
"AI Generated" is still on the list Vision can answer from, against the project's own rule. No approved asset carries it.
has_text is true on 43 of 48 approved Maurten assets. The prompt asks for overlaid text, and I haven't checked how many of those are pack labels.
The spec's safe zone is 120 to 960, and the type block runs from 80 to 1000. The hero headline crosses both edges.
No approver and no confidence score are recorded.
Not yet wired
The subject box and tone map are stored and returned, and nothing reads them.
The focal point only sets the crop. 9:16 frames in 9:16 ads don't crop, so it moved nothing in this batch. It starts to matter with 4:5 and 1:1 cuts.
Brand tags mix cases: Maurten on 45 assets, thefeed on 23, none on 3.
The query
The query API
Everything above exists so that a request from an ad tool gets the right frames back. This is the hero asset as the API returned it.
GET /api/assets/query?status=approved&brand=thefeed&limit=50