← Work

Creative engine

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
Licensed Tour photograph of a rider on a seawall road, with the model's subject box and focal crosshair drawn by the review queue
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 review card for the same frame: tag chips, suggested brands Caja Rural and Seguros RGA, empty brand, format, tier and source fields, Approve and Reject
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
The finished ad with the renderer's type block, the Meta safe zone and the model's subject box and focal point drawn over it
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
Finished ad: FINAL STAGE, FINAL SAVINGS, support line 20% off chews at The Feed
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
  1. Upload
    signed URL
    direct to storage
  2. Ingest
    same name → 409
    status review
    no tags written
  3. Model: tags + composition
    enum schema
    suggests only
  4. Model: brand suggestions
    second call
    advisory, unsaved
  5. Person
    edit chips · drag
    brand · source · tier
    approve or reject
  6. Write
    approved only
    tags + focal
    focal clamped 0 to 1
  7. Query
    /api/assets/query
    approved by default
    max 50 per page
  8. 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.

The source frame with the model's subject box and focal crosshair
  1. 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. 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. 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. 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.

    Lifestyle Action · Cycling · Portrait · has_person true · has_text false · Outdoor Sunny · Male
    person: brand thefeed · source licensed · quality_tier Select · output_format 9:16
  5. 5

    Brands on the jersey ← suggestion only

    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
Review card for a 5464 by 8192 Maurten photo, with the Vision error printed above empty tag fields
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.

Licensed frame of a rider in the yellow jersey with a stage-finish banner in the background
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.

Tag panel with a hand-typed Sun environment chip and a hand-typed licensed source chip
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.

Focal Reprocess admin panel showing 29 of 29 done, with focal values per Maurten asset
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.

Final stage, final savings
chews_04
Final stage, / final savings
Last climb, last chance
hydration_13
Last climb, / last chance
The final descent
gels_08
The final / descent
Fuel the last stage
gels_10
Fuel the / last stage
Last stage, last deals
waffles_15
Last stage, / last deals
Ride it out
waffles_01
Ride it out
Tour de Feed ends soon
sitewide_12
Tour de Feed / ends soon
Whole tour on sale
sitewide_02
Whole tour / on sale
The final descent
gels_10alt
The final / descent
Final stage, final savings
sitewide_11
Final stage, / final savings
Last climb, last chance
chews_03
Last climb, / last chance
Ride it out
waffles_05
Ride it out
Tour de Feed ends soon
sitewide_06
Tour de Feed / ends soon
Last drop, last deal
sitewide_16
Last drop, / last deal · local frame
Last podium, last prices
sitewide_17
Last podium, / last prices · local frame
Blink and it's over
sitewide_18
Blink and / it's over · local frame
Last climb, last call
sitewide_19
Last climb, / last call · local frame
Down to the wire
sitewide_20
Down to / the wire · local frame
Last push, last chance
sitewide_21
Last push, / last chance · local frame
Last sprint, last savings
sitewide_22
Last sprint, / last savings · local frame
Still time to save
sitewide_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
total 23 · read 24 Oct 2026
asset_id thefeed_1784760109930
storage_path thefeed/1784760109930_thefeed_gruber_tour2026_04.png
dimensions 1080 × 1920 png
source licensed
status approved
created_at 2026-07-22T22:41:53Z
focal 0.35, 0.78
subject_box x 0.1 y 0.68 w 0.5 h 0.28
tone_map top clean · middle clean · bottom busy
quality_tier standard (tag says Select)