What it does
From a gallery photo (a street, a parking lot, the plate visible), the Studio replaces only the background and the light (AI features spec §2). The person picks a scene chip (rooftop at sunset, night garage, mountain road, rainy waterfront, black studio, underground car park) and a format (cover 4:3, hero, story 9:16, 1:1); three variations follow. Each variation keeps the car: the model is asked to change nothing of it, and every answer is checked against the original before anyone sees it. The plate is always blurred, the picture carries an "AI-edited" badge and a link to its original, and the original is never replaced. The Studio never removes scratches, dents or dirt: the prompt forbids it, the fidelity check rejects a changed car, and there is no free-text prompt to ask for it.
ai.studio gates it, with an image model in AI_MODELS (studio) and its provider's key (AI foundation). The locate step uses studioLocate (any text model with vision; it falls back to AI_PROVIDER/AI_MODEL).
Pipeline
- Locate (
studio.create, on the request): the photo goes to the vision model with a fixed schema: the car's box and every plate's box in thousandths. A car smaller than 4 % of the photo, or none, isBAD_REQUESTand counts nothing. Only then does the request count one Studio image against the plan's quota (QUOTApast it), and it is queued. - Generate (the cron tick's
studiojob, one request per tick): three edits of the original in parallel (generateImagewith the photo as an image part: OpenAI's edit endpoint, Gemini's native image models), each with a 90 s deadline (STUDIO_CALL_MS; one gpt-image-2 edit took 18 s alone, three in parallel take longer) inside the Studio's own 150 s of the tick (STUDIO_JOB_MS), and never retried. - Fidelity check (
studio.fidelity.ts): the original's car core is found in the answer by sliding its brightness over the picture at sizes from 0.5× to 1.6× (another format or framing moves the car). The comparison keeps only the car's pixels: a k-means model of the ring around its box marks the background. Structure is the normalised correlation of the car's details (brightness minus its local mean: body edges, wheels, windows, lights), matched finely around that place: the scene's light falling off across the body and its reflections barely move it, a redrawn wheel or another car's lines do. Colour is the mean CIELAB ab distance (a warmer light moves it a little, a repaint a lot). Thresholds live inFIDELITY(studio.rules.ts): structure at least 0.45 (faithful gpt-image-2 answers scored 0.54–0.76, other cars 0.33 at most), colour at most 18. Each variation's numbers (and nothing of the car or plate) are logged asstudio.fidelity. - Plate blur: every plate on the car, grown by 35 %, is placed where the check found the car and blurred pixel by pixel (three box-blur passes, radius half the plate's height). The picture is written as a PNG without metadata.
- Store: each kept picture becomes a private uploaded file of the requester (
orgs/<org>/studio/<request>/…) and astudio_variation. With none kept, the request fails (fidelitywhen answers came back but changed the car, elseprovider). No second round runs on its own: it would double the paid calls behind one Studio image without the person asking. The result screen offers "Try another scene", back to Studio-Pick, which shows the Studio images left before the next Generate.
Pixels are handled without a native library: the original is read at 1/8 scale from its JPEG (the cover-colour decoder, EXIF orientation applied), the answer as a PNG through DecompressionStream. shortcut: a JPEG answer is discarded as unreadable; add a decoder if a chosen model only answers JPEG.
Quota
One request is one Studio image (Free 3, Plus 60 a month), whatever the variations: three pictures to choose from are one try, and at most three image calls (a tick interrupted mid-generation retries the request, at most STUDIO_ATTEMPTS claims). A request that ends without variations still counts, as every provider call is billed (ADR-0017); the next try is the person's own choice on Studio-Pick, with the images left in view.
Data and isolation
studio_request (photo, scene, format, regions, state queued/ready/failed, failure, rounds, lease) and studio_variation (its uploaded file and position) belong to the Organization and the car; row-level security shows them to the Members who see the car. Tenant traffic only reads and queues; the job writes on the system path. vehicle_photo.ai_edited and original_photo_id mark a saved Studio picture; deleting the original keeps the picture and its badge. Requests older than 7 days are deleted with the files nobody saved.
A passport carries Studio pictures only when the owner ticks "Studio photos" on M-Vender, each labelled "AI-generated background · original car". Overlays are never stored: the gallery keeps the clean picture, so no lettering or emblem reaches a passport or a showcase.
Procedures
| Procedure | Who | Failures |
|---|---|---|
studio.create { id, scene, format, platform } | an editor of the car; owners and admins | BAD_REQUEST (no car), QUOTA, TOO_MANY_REQUESTS, NOT_FOUND (off, no model, not the household's photo, a Studio picture), FORBIDDEN |
studio.get { id } | every Member who sees the car | NOT_FOUND |
studio.latest { vehicleId } | every Member who sees the car | none: the newest request or null |
studio.save { id, cover } | an editor of the car | CONFLICT (gallery full, US05), NOT_FOUND, FORBIDDEN |
App
- Studio-Pick (
screen-Studio-Pick,/studio-pick/<photoId>?vehicleId=): the photo, scene chips (studio-cena-<scene>), format (studio-formato), what changes and what stays, the Studio images left, and Generate (studio-gerar). From M-Galeria's Studio button (galeria-studio). - Studio-Result (
screen-Studio-Result,/studio/<id>): progress while queued (about a minute; the gallery'sgaleria-studio-ultimorow reopens it), the failure and "Try another scene", then Studio | Original (studio-comparar), the variations, "Model name lettering" (studio-letreiro), "Brand emblem" (studio-emblema, only withai.studio.brand-emblem), "Save and use as cover" (studio-capa), Save to gallery (studio-salvar) and Share (studio-compartilhar). Ap-Studio-Emblema is this screen with its overlays (?lettering=1). - Studio-Story (
screen-Studio-Story,/studio-story/<id>): the variation as a 9:16 story with the chosen overlays and the badge, made on the phone and shared like a share card.
The brand emblem is a vetted SVG per make from a licensed set, overlaid by the app, never generated; the set is empty until a legal review, so the switch says no emblem is licensed yet. autofolio://dev/Studio-Pick, Studio-Result, Studio-Story and Ap-Studio-Emblema open on the seeded M3 request (its variations are the M3's photos as stand-ins).
Blind model test
The model is chosen by a blind test on 30 real photos; car fidelity beats background beauty:
pnpm --filter @autofolio/api studio:blind-test <photos-dir> <out-dir> \
--models gemini:gemini-3.1-flash-image,openai:gpt-image-2 --scene night_garage --format coverIt writes sheet.html (each photo with the candidates as A, B, C in a fresh order, rated 1–5 for the car, then the scene), fidelity.csv (the check's structure and colour per answer, to tune FIDELITY) and key.sealed.json (which model is which letter; open it after rating). Run it only with providers the owner approved for real photos. It also writes regions.json (the locator's boxes), so the answers can be checked again offline, for free, after changing FIDELITY:
pnpm --filter @autofolio/api studio:fidelity-replay <out-dir>The replay checks every original against every picture in the folder: its own answers (<photo>-<label>.png, or any other shot named <photo>-….jpg) must pass, every other car must fail. It prints both numbers per pair and exits non-zero on a wrong verdict.
Running it locally
The studio job runs in the jobs process, not the web server: export the AI variables in the shell that runs it, as in the web shell (AI_PROVIDER=openai AI_MODELS='{"studio":"openai:gpt-image-2"}' pnpm jobs:run --every 60, with --any-db against a non-local dev database). Without them the job fails a queued request as provider.