A vocabulary and some narrower rows
Ember's generator catalog carries a list of aspect ratios at the top level, each an id with the sentence Ember adds to the prompt. Today the vocabulary is four shapes: 1:1, 3:2, 4:3 and 16:9. A row may also carry its own aspect_ratios. ElevenLabs rows do: Ember intersects what the model declares with its own vocabulary, and a model that offers none of the four gets no row at all. A row without its own list offers the whole vocabulary.
A request in Avatars has exactly one aspect_ratio, 1:1 unless the request says otherwise, and it covers the whole batch. So when a batch asks three generators for 16:9, all three must be able to draw 16:9.
The bug that taught it
On 2026-09-30 the studio offered every shape for every generator, and plan() checked only the top-level vocabulary. Some ElevenLabs rows are narrower than that. The commit that fixed it names them: GPT Image 1 and 1.5 offer 1:1 and 3:2, and both Seedream rows lack 3:2. A 16:9 request to one of those passed the plan, the batch was created, the tasks were queued and submitted, and Ember failed each job when it ran, because Ember's ElevenLabs path checks the row's list at generation time. Every one of those failures was knowable before the first task existed.
That is the shape of most avoidable failures in a pipeline like this one. The fact needed to refuse was published by the owner all along; the client simply asked a coarser question than the one the owner could answer.
Refuse by name, before anything is queued
The fix is two small module-level functions in backend/studio.py. aspect_ids reads both shapes Ember uses (plain ids on a row, {id, prompt} entries in the vocabulary). require_aspect refuses a ratio a row's own list leaves out, and its message names the generator, the shape asked for and the shapes it does offer. The plan now checks three things in order:
- the ratio is in Ember's vocabulary at all (This aspect ratio is not available in Ember.);
- every selected generator's row offers it;
- the tier the automatic route resolved offers it too.
The third check is a second guard. Avatars already passes the ratio to Ember's route preview, which skips tiers that lack it; require_aspect holds the resolved row to its list anyway, so a route decision can never be the reason a bad shape gets through.
The test for this builds a catalog where one row, labelled Narrow Credits, offers only 1:1 and 3:2. A 16:9 request gets exactly Narrow Credits does not offer 16:9 output; it offers 1:1, 3:2. Choose one of those, or another generator. A second test sends the same body to the preview route, the submit route and Pippa's internal gateway, expects that same sentence as a 422 from all three, and then checks that the batch list is still empty. The studio and every brain see the same refusal because they reach the same function.
The picker never changes your choice
The web studio uses the same facts to help before the server has to refuse. narrowAspects tags a generator whose list is narrower than the vocabulary, so the row reads, for example, credits · 1:1, 3:2 only. aspectConflicts disables, in the aspect menu, every shape a selected generator lacks. If the chosen shape still conflicts (say Dad picked 16:9 first and then ticked a narrow generator), a warning names the generator and what it offers, and the Review button stays disabled until he changes the shape or deselects the generator.
What the studio never does is change the ratio for him. Substituting 3:2 for 16:9 would quietly produce a batch of the wrong shape for a thumbnail or a diary cover. The architecture doc says it in one line: it never changes the chosen aspect ratio itself. The automatic row carries no list of its own, since the tier is decided later, so the picker cannot predict that case; the server check covers it.
References: the model's limit, or none
The same rule applies to how many reference images a generator accepts. Until 2026-10-06 the plan had a fallback ceiling of ten for rows that declared nothing. That ceiling was removed: Avatars now uses only the limit a model declares. Ember's own wording is that a missing limit is unknown, not an invented application-wide ceiling. Today Ember declares 14 for Nano Banana Pro, 16 for the OpenAI rows, none for Codex, 10 for its local instruction family, and whatever each ElevenLabs model lists.
The check runs per task, because a task's inputs are the frame's mapped original (for remix and regenerate) plus the request's references, with duplicates removed. Over the limit, the plan refuses with the row's label, the limit and the actual count. A test keeps all 24 references for a model with no declared limit, then sets that model's limit to 14 and expects a refusal naming both numbers. Remix adds one more refusal of the same kind: a frame with no mapped original stops the plan with Remix needs an original mapped to and the frame's name, because the architecture rules that a missing mapping is shown before submission, never guessed from the default.