project.yml
The only file you edit. Everything else is generated from it. config/templates/room.yml is the
annotated template; this is the reference.
brief
| Key | Purpose |
|---|---|
what_it_is |
One or two sentences a stranger could understand the room from |
register |
The cultural and social register. Be specific — this is what stops renders drifting into generic hotel styling |
must_appear |
Things present in EVERY render, forever. Inherited furniture, religious objects, anything the household will not part with |
fixed_furniture_note |
An extra sentence pinned into every prompt about retained items |
problems |
{id, text}. Directions refer back to these ids |
constraints |
Hard limits no direction may break |
room
| Key | Purpose |
|---|---|
shape |
rectangle · rectangle_with_alcove · polygon. Walls are generated as w1, w2, … anticlockwise |
walls |
Instead of shape, give explicit segments {id, name, from, to, role} for any shape |
wall_names |
Friendly names and roles keyed by wall id |
key_dimensions |
{across, along}. Use when an alcove or bay makes the bounding box overstate how wide the room feels |
openings |
{tag, type, wall, pos, width, sill, head, desc, extra}. pos is the centre, measured along that wall from its start. Every opening must be listed — renders are told anything absent does not exist |
features |
Vents, niches, radiators, columns — fixed things that are not openings |
shell_notes |
Sentences appended to the shell description in every prompt |
existing |
Current floor, ceiling, walls; optional fan: {x, y, blade_span} |
confirmed |
false until a tape has actually been on it |
notes |
Anything inferred and still to be verified |
Opening type affects how it is drawn: door and doorway get a swing, arch gets a
semicircular head, window gets a sill.
deliverables
| Key | Purpose |
|---|---|
directions |
How many distinct directions |
aspect_ratio |
Render aspect, default 16:9 |
elevations |
Which wall ids get an elevation drawing |
iso_hide_walls |
Walls omitted from the dollhouse so you can see in |
views |
One render per direction each. See below |
Each view:
- id: alcove
caption: "The alcove bay"
drawings: [elev-w4, plan] # which measured drawing constrains this camera
photos: [right-side.jpg] # client photo nearest this standpoint
from: [2.40, 1.90] # where the camera stands, in room coordinates
look_at: [0.00, 1.90] # what it points at
camera: >
CAMERA — ... describe the standpoint, lens, eye level and what must be in frame.
from and look_at matter more than they look. From them the engine generates an orientation
block naming which wall lands on the LEFT, the RIGHT, AHEAD and BEHIND for that specific frame, and
which openings can admit daylight in it. Without them the model has world coordinates and prose,
no way to convert one into the other, and will mirror the room roughly half the time and invent
daylight on a wall the model says is solid. This was observed, not theorised.
drawings matters more than it looks. A square-on view of one wall needs that wall’s
elevation as its first reference, or the model defaults to a long axial view. Axial views take
[plan, iso].
style
richness — leave blank to use the built-in rules, which are what stop renders looking bare
(layered textiles, objects in use, planting, several lamps actually lit). Override only to change
the level of dressing.
render
| Key | Purpose |
|---|---|
backend |
codex (free, quota-limited) or openrouter (paid) |
fallback |
Optional. Backend to degrade to when the primary runs out mid-run |
max_spend_usd |
Hard ceiling, enforced against the real per-image cost the API reports |
How the backend is chosen
There is no automatic choice. Precedence, highest first:
--backend openrouteron the command linerender.backendinproject.ymlcodex
A paid backend is refused outright until the spend gate is approved. This is deliberate: a
tool that decides by itself to start spending money is one you cannot hand an API key to.
design render prints which backend it picked and why before it starts.
Falling back
By default, running out of quota stops the run. Finished views are skipped on the next attempt, so nothing is wasted. Two ways to keep going:
./bin/design render --wait # sit out the reset, then continue on the same backend
render:
backend: codex # start free
fallback: openrouter # switch to paid when the free quota runs out
max_spend_usd: 15
A fallback is only ever used when all of these hold: you set render.fallback explicitly; the
spend gate is approved if it costs money; and the spend is under the ceiling. Otherwise the run
stops and tells you exactly which condition failed. Every switch is written to STATE.json.
Secrets live in .env, never here.