Media
Image generation, the media library as single source of truth, and the reusable visual assets that keep creative consistent.
Media generation is asynchronous by design. Nothing renders inside a request. The app writes a row, submits to the pipeline Worker, and the UI polls or waits for a live card update.
Generation path
Credits are charged when the row is created and refunded if the submit fails. A submit that fails silently is the worst outcome here: the row sits at Pending forever and the user is charged. That is why submitToPipelineWorker throws on any non-2xx.
Two entry points produce the same rows: the chat host, through its generate_image and generate_video action tools, and Creative Studio’s prompt box. Both ask for settings the same way, and both render cards that poll for completion.
Media library
The media library is the single source of truth for user media. Anything that enters the app enters the library: generated images and videos, uploads, and images dropped into a chat (which get a user_uploads row with source chat).
The trade-off is real and deliberate: deleting an asset from the library deletes the R2 object, so an old chat message referencing it will show a broken image.
Folders are shared across media types. Generated campaign clips are foldered automatically under {Offer}/{Ad}.
The listing pattern (tabs, then query, then a grid of cards, with sort and refresh controls and no dead disabled buttons) is established in components/images/media-library-content.tsx. Model new listing surfaces on it rather than inventing another.
Characters
A character is a reusable person: an identity plus a generated character map that keeps them consistent across shots.
Creating one is a deliberate intent, never a side effect of generating an image. Two ways in:
Describe or upload references in a character chat. The host generates a cheap portrait, the user iterates conversationally (“make her older”), and on explicit approval the character is saved. The full character map generates in the background and appears live in the chat as a card.
Tools involved: generate_character_portrait (cheap, iterable, restates the full look), save_character (approval-gated, charges one map credit), link_character_persona.
Type #Character in the Creative Studio prompt with a reference image and direction. It is the pinned Actions item at the top of the # dropdown. A photorealistic character-definition portrait generates, the character row appears immediately as “Creating portrait…”, and the map generation auto-fires when the portrait completes.
The auto-chain from portrait to map lives in the GET /api/characters reconcile: lazy, with an atomic claim so concurrent polls cannot double-fire, surviving a closed tab, with a 10-minute self-heal for crashed chains. Both credits are charged upfront and refunded once on failure.
Presets for ethnicity, archetype, and style live in lib/character-presets.ts. The host asks for ethnicity rather than assuming it.
Elements
An element is a reusable non-person asset: a location, a prop, or a product. Same lifecycle as characters (draft, generating, completed or failed, with lazy reconcile and refund-once), same per-user unique slug so they can be @-mentioned.
Element sheet prompts are deterministic code, not an LLM call: buildElementSheetPrompt in lib/elements.ts composes kind framing, description, and the style preset modifier. Locations render 16, props and products 1.
The point of elements is continuity. A storyboard names its elements per shot, and the scene image generation attaches those element sheets as reference images, so the same kitchen looks like the same kitchen in shot 1 and shot 7. Maximum three element references per scene image.
Mentions
@ mentions attach context: offers, personas, characters, elements. # mentions apply styles and actions. Offers and personas are single-select; visual assets are multi-select, deduped, capped at 14.
Selecting a character or element from the @ dropdown does not just tag it, it attaches its sheet as a generation reference in both image and video modes.
Composer
Every prompt box in the app is TintedPrompt plus components/chat/composer.tsx. There is no second implementation. It provides mentions, attachments, media thumbnails inside the input, a header slot that renders only when there is content, and a toolbar slot.