Multi-Asset Sheet
The AI needs to draw ten game sprites that look like they belong together. How does one AI call produce a consistent art-style spritesheet, and how does each sprite end up cropped and transparent in the right place?
§1 What it is (and isn't)
One sheet, many sprites, one style
Multi-asset sheet is one FAL API call that produces a single spritesheet image containing N sprites sharing a consistent art style. Gemini Vision then detects each sprite's bounding box, each sprite is individually background-removed, and each named asset is served as its own cached WebP.
Not grid-select. Not character variants. Not one-image-many-styles. It's: one visual call → a whole family of named sprites.
§2 How the AI declares a sheet
Two tags in the AI's HTML response. The declaration and each grab site.
<!-- 1. The declaration — once per sheet --> <multi-asset-sheet id="main_sheet" assets="player_idle, player_walk, enemy, coin" style="16-bit pixel art" theme="forest adventure" requirements="all sprites face right, transparent bg" sheet_size="2x2" ></multi-asset-sheet> <!-- 2. Each grab site — one per <img> --> <img src="https://images.fuzzycode.dev/multi_ai?asset_sheet_id=main_sheet&grab=player_idle&resize=64x64" /> <img src="https://images.fuzzycode.dev/multi_ai?asset_sheet_id=main_sheet&grab=enemy&resize=32x32" />
${playerName}) break the prewarm because the post-processor can only rewrite static strings. This rule is repeated four times in core_ai.py:82-86, 117-125.
§3 The pipeline — declare to served-pixel
1. AI emits tags
AI generates HTML containing <multi-asset-sheet> + N <img ...asset_sheet_id=main_sheet&grab=...>
2. Prewarm (synchronous)
prewarm_multi_asset_sheet_urls() at html_processing.py:382-482. Parses all sheets, resolves spritesheet_ref dependencies.
3. Generate + slice
Single FAL call for the sheet image. Gemini Vision detects bounding boxes per named asset. Each sprite bg-removed and cached.
4. Rewrite + inject
Every asset_sheet_id= URL rewritten to sheet_key=<hash>. Tags receive server_sheet_key, server_sheet_hash, server_sheet_dt attrs.
5. Per-asset GETs
Browser fetches multi_ai?sheet_key=<hash>&grab=<name>&resize=WxH. ImageBuddy serves the pre-sliced WebP.
Blocking step: step 2 runs synchronously during the AI turn — the user sees no extra loading; they just see the AI "thinking" for a bit longer. The sheet key is embedded in HTML before it reaches the browser, so <img> fetches in step 5 are cache hits.
§4 The cache-key inputs — what counts as "a different sheet"
The hash that becomes sheet_key is computed over this set. Change any input and a whole new spritesheet is generated, and all previous visual art is discarded.
assets
comma-separated list of named sprites
style
"16-bit pixel art", "watercolor", etc.
theme
world / setting descriptor
requirements
free-text constraints (facing direction, transparency)
sheet_size
grid dimensions like 2x2
model
AI model id if overridden
spritesheet_ref
id of parent sheet for style consistency (optional)
core_ai.py:109) — because changing any hash input regenerates everything, losing the user's existing sprites. There is no UI enforcement; if the AI disobeys, the user silently loses art.
§5 spritesheet_ref — art style consistency across sheets
When a user wants to add new sprites that match the art style of an existing sheet, the AI can declare a second sheet with spritesheet_ref="main_sheet". The parent sheet's generated image is passed as a reference to FAL flux-2/edit, keeping style aligned.
assets: player_idle, player_walk, enemy, coin
style: 16-bit pixel art]:::sheet A --> GA[FAL: text-to-image
1 sheet produced]:::gen GA --> I1[sheet image · cached]:::out I1 --> S1[sliced: 4 WebPs
by Gemini Vision]:::out B[boss_sheet
assets: boss_idle, boss_attack
spritesheet_ref: main_sheet]:::sheet B --> GB[FAL: flux-2/edit
with main_sheet image as reference]:::gen I1 -.->|ref image| GB GB --> I2[boss_sheet image · cached]:::out I2 --> S2[sliced: 2 WebPs
by Gemini Vision
same art style]:::out
spritesheet_ref resolves transitively — a third sheet can ref boss_sheet which refs main_sheet. Transitive resolution uses a pending-queue algorithm (html_processing.py:425) with a self-reference deadlock guard (line 428).
§6 Failure modes (what the user might see)
| Failure | Detection | UX | Evidence |
|---|---|---|---|
AI omits server_sheet_key after prewarm |
checkUnprocessedAssetSheets walks the HTML |
Yellow toast: "Found N asset sheet(s) without server_sheet_key. Run an AI change to prewarm these assets." | static/script.js:1509-1532 |
AI hallucinates an asset_sheet_id that doesn't match any declaration |
validateMultiAiUrls cross-references ids to sheets |
Yellow toast: "asset_sheet_id X doesn't match any declared sheet" with offending URL | script.js:1534-1655 |
spritesheet_ref deadlock (self or cyclic) |
Pending-queue guard in prewarm | Sheets without resolved refs remain unprocessed; surfaces via the toast above | html_processing.py:428 |
| Gemini Vision miscount (fewer labels than declared assets) | ImageBuddy returns the sheet with fewer slices than requested | Missing <img>s will 404 at browser; broken-image icon — no specific UI signal |
ImageBuddy side (not in this repo) |
| Prewarm timeout (long wait) | AI turn blocks on prewarm | User sees main Send/Swipe button spinner for the combined duration (AI + prewarm) | core_ai.py:1050 |
Dynamic URL interpolation by the AI (${name} style) |
Post-processor cannot rewrite; URL reaches browser with raw asset_sheet_id |
Broken image (server sees unhashed id) | AI rule: core_ai.py:82-86, 117-125 |
§7 Doc vs code cross-check
The existing doc at docs/MULTI_ASSET_SHEET_CURRENT_STATE.md matches code for phases 1-7 as verified. One section is explicitly labelled unimplemented and is confirmed absent:
html_processing.py. The doc is accurate about its own gaps — a rare and welcome property.
§8 Verification pointers
- AI rules
FuzzyCode/quart_server/blueprints/core_ai.py:68-134 - Dynamic URL prohibition
core_ai.py:82-86, 117-125 - Frozen-sheet rule
core_ai.py:109 - Prewarm orchestrator
FuzzyCode/quart_server/utils/html_processing.py:382-482 - URL rewrite (asset_sheet_id → sheet_key)
html_processing.py:271, 307 - Prewarm invocation points
core_ai.py:1050, 1268, 1504 - Transitive ref resolver + deadlock guard
html_processing.py:425, 428 - Client validation: missing server_sheet_key
static/script.js:1509-1532 - Client validation: hallucinated id
static/script.js:1534-1655 - Canonical-state doc
docs/MULTI_ASSET_SHEET_CURRENT_STATE.md - Backend per-asset serve
ImageBuddyRobustFastAPI/multi_ai.py(not inspected in this pass)