# projtex: projection texturing API (agent guide) > Give a 3D mesh and a prompt; get back the same model with a baked colour texture (GLB). > Images are generated from virtual cameras by Qwen-Image-2.1-Turbo (default), the full Qwen-Image-2.1 or Bria > Fibo 2.0 Edit Turbo, projected onto the geometry, blended in texture space and baked into the mesh's UVs. > Long operations are asynchronous jobs: create, then poll. Every status tells you what to do next. > Terrain, landscapes and ground: use POST /api/terrain instead (see "Terrain and ground" below). Interfaces (same functionality): - REST: base URL of this Space (https://townbox-projection-texturing.hf.space). OpenAPI/Swagger at /docs, JSON schema at /openapi.json. - MCP (streamable HTTP, JSON responses): POST /mcp. Tools: texture_mesh, texture_terrain, get_job, view_image, regenerate_views, disable_views, fill_gaps, retouch, undo_retouch, bake, pick_point, submit_view_image, service_info, guide. ## Quota and identity (read this first) Generation runs on ZeroGPU Spaces and spends the caller's daily GPU quota (about 7 s of GPU per view with the default backend qwen-turbo, about 30 s with qwen-space). - Send your Hugging Face token so your own quota is used: header `X-HF-Token: $HF_TOKEN` (REST and MCP), or the `hf_token` field. It is kept in memory for the job only, never stored, logged or returned. Prefer the header: a body field ends up in any copy of the request you save next to your project. - Without a token the job runs as your forwarded visitor identity if the Hub provides one, otherwise anonymously (shared ~2 GPU-minutes per day: about 4 views). GET /api/identity shows which applies to your requests. - backend "mock" runs the whole pipeline with a procedural texture and no quota: use it to test your integration. - backend "external" lets you supply the images yourself (any generator): no quota either. ## Minimal workflow (REST) 1. Send the mesh. A local file (e.g. exported from Blender or three.js): upload the raw bytes, then create the job with plain JSON (no multipart quoting problems, any characters in the prompt): curl -s --data-binary @house.glb -H 'content-type: model/gltf-binary' $BASE/api/uploads -> {"upload_id": "up_...", ...} curl -s -X POST $BASE/api/jobs -H 'content-type: application/json' -H "X-HF-Token: $HF_TOKEN" \ -d '{"upload_id": "up_...", "prompt": "weathered red brick walls, dark slate roof; white window frames"}' Python: requests.post(f"{BASE}/api/uploads", data=open("house.glb", "rb")).json()["upload_id"], then requests.post(f"{BASE}/api/jobs", json={...}, headers={"X-HF-Token": token}). Other ways: "mesh_url" (public URL), "mesh_base64" (the file base64-encoded, up to 20 MB; handy over MCP), "sample" (avocado | lantern | head), or multipart POST /api/jobs/upload with field "file" plus either a "params" JSON field or separate fields (prompt, preset, ...). With curl -F, a value containing ';' is cut: use --form-string 'params=...' or -F 'params= 202 {"job_id": "...", "state": "queued", "next": [...], "poll_after_seconds": 15} 2. GET /api/jobs/{job_id} every `poll_after_seconds`. While running, `progress` shows the step, view i/N and `eta_seconds`; while queued, `queue` shows your position and an estimated start. Step "unwrapping UVs" (new UVs for a mesh without usable ones, or uv "regen") takes about 1 min per 40k triangles: minutes for a detailed model; the job continues by itself, and the same mesh sent again (a mock run, then the real job) skips it. 3. When state == "done": - files.textured_glb: your model with the baked texture (see "Using the result" below) - files.contact_sheet / files.comparison: renders to look at; files.albedo: the texture (PNG) - summary: coverage_pct, holes_pct, views_rejected, views_flagged_inconsistent (see "Reading results") - output: how the result maps onto your own scene (UV layout kept or not) - next: concrete follow-up calls (method, url, body) when quality can be improved 4. Optional quality loop (each is a job operation; poll again until done): - POST /api/jobs/{id}/regenerate {"views": [...]} new seeds; failed regenerations keep the old image - POST /api/jobs/{id}/disable-views {"views": [...]} drop views that stay inconsistent - POST /api/jobs/{id}/fill-gaps {"max_views": 3} cameras for still-uncovered surface - POST /api/jobs/{id}/retouch {"suggestion": 0} local inpainting of the worst region - POST /api/jobs/{id}/bake {"resolution": 4096, "single_mesh": true} ## Using the result (Blender, three.js, game engines) - textured_glb has the same objects (node names and transforms) and materials (names, metallic, roughness, alpha, emission, and glTF material extensions such as glass transmission/IOR, emissive strength or clearcoat) as the file you sent, in the same orientation; every material's base colour now uses one shared baked texture atlas. Roughness/metallic maps come with "pbr": true (below); normal maps are not produced. Import it in place of your model: Blender: bpy.ops.import_scene.gltf(filepath="textured.glb") (objects keep their names; the image is packed in the GLB). three.js: GLTFLoader; the texture is sRGB and already flagged as such; `getObjectByName` works with your original names. - output.uv_layout tells whether your UVs were kept. If kept (output.albedo_matches_your_uvs), files.albedo also fits your original model directly: plug it into your own material's base colour. If they were regenerated (they overlapped or were missing, e.g. three.js BoxGeometry/CylinderGeometry, Blender primitives after joining), use the GLB, whose UVs are new. Force a choice with "uv": "keep" | "regen"; "keep" still unwraps anew when more than 10 % of your layout is shared by several faces (Smart UV Project run on each object separately: every part fills the 0-1 square and would show the others' paint). Unwrap all parts in one run to keep your UVs. - PBR materials: "pbr": true (or POST /api/jobs/{id}/pbr on a finished job) splits every generated view into albedo, roughness and metallic maps (a LoRA for Qwen-Image-Edit-2511 trained on renders of MatSynth materials, 4 steps on a ZeroGPU Space with your quota, ~5 s per view) and bakes them like the colour: files.textured_pbr_glb has a de-lit base colour and a glTF metallicRoughness texture (G roughness, B metallic) on every material, so the model reacts to your scene's light (varnished wood and porcelain catch highlights, brass and chrome reflect); files.albedo_pbr, roughness, metallic, metallic_roughness are the maps for your own material. Material names and part_prompts settle metal or not where they say it ("Brass", "walnut veneer", "gloss paint"), so name your materials: metal is the hardest map to read from a picture. pbr.metallic_fraction and pbr.roughness_mean in the status summarise the result. - Game-ready single asset: "single_mesh": true returns one object with one material (metallic/roughness are the area-weighted means of yours); POST /api/jobs/{id}/bake {"single_mesh": true} switches an existing job. - Prepare meshes like this: one GLB with all parts (several objects and materials are fine), modifiers applied, faces pointing outwards, real thickness or separate parts rather than overlapping coplanar faces. UVs are optional (Blender: Smart UV Project is fine; or let the service unwrap). - Orientation: glTF/GLB is always +Y up (Blender's glTF exporter converts its Z-up scene), so do not pass "up" for GLB files (mode "character" detects figures lying along Z by itself). Use "up": "z" only for OBJ/PLY/STL files whose up axis is Z. Recipes: Blender (bpy, background mode): bpy.ops.export_scene.gltf(filepath="model.glb", export_format="GLB", export_apply=True) # modifiers applied ... upload, create the job, poll, download files.textured_glb ... bpy.ops.import_scene.gltf(filepath="textured.glb") # same object names; delete or hide the untextured ones three.js in Node (GLTFExporter needs FileReader, which Node lacks): globalThis.FileReader ??= class { readAsArrayBuffer(b) { b.arrayBuffer().then(r => { this.result = r; this.onloadend?.(); }); } readAsDataURL(b) { b.arrayBuffer().then(r => { this.result = `data:${b.type || "application/octet-stream"};base64,${Buffer.from(r).toString("base64")}`; this.onloadend?.(); }); } }; const glb = await new GLTFExporter().parseAsync(scene, { binary: true }); fs.writeFileSync("model.glb", Buffer.from(glb)); // name your meshes and materials: the names come back three.js in the browser: new GLTFLoader().load("textured.glb", g => scene.add(g.scene)). ## Parameters that matter - prompt: describe MATERIALS and COLOURS of what the mesh models ("olive drab chipped paint, gold visor"), not new geometry. Name the modelled parts (door, windows, roof, glasses...) so the model paints them. - Lettering (a name on a van, a fleetname, a shop sign): put the exact words in quotes. Large lettering (a name across a panel, letters ~1/20 of the view's height or more) comes out spelled right; small lettering (a bus fleetname, route numbers, number plates) comes out misspelled with every backend, because each view shows it only a few pixels high. Fix it afterwards with a close-up retouch, where the words fill the frame: pick a point on the lettering, then POST /retouch {"camera": {"eye": , "target": , "fov": 40}, "mask": {"rect": [0.05, 0.4, 0.95, 0.6]}, "mode": "replace", "prompt": "gold 'LONDON TRANSPORT' lettering on red paint"} (measured: misspelled in every 1024 px view of a 9 m bus with both Qwen models, spelled right from 4.5 m away). Or leave small text out and add it as a decal in your scene. - part_prompts (recommended when your file has several named materials or objects, e.g. a Blender scene): {"": "description"}, e.g. {"Walls": "white clapboard siding", "Roof": "grey asphalt shingles", "FrontDoor": "dark green panel door"}, with `prompt` kept for the overall style ("a tidy suburban house, late afternoon"). Each view's prompt then names only the parts that view shows, so a side view of a house is not given "a green door" and does not paint one on a blank wall; clauses of your `prompt` and `subject` that name parts out of view (by their names' words) are left out of that view's prompt too. Names match material names first, then object names, case-insensitive; the status lists your file's names under model.materials / model.objects, and warnings lists names that were not found. Parts you leave out are still textured from `prompt`. Without part_prompts, meaningful material/object names are only used to keep out-of-view parts out of each picture (negative prompt). - mode: object (orbit views; also buildings and props) | character (+ face close-ups, auto up-axis/facing; set focus_prompt to describe only the head) | environment (360° probes from inside, for rooms and interiors you walk through; slow on this CPU machine). - preset: minimal (4 views, fastest) | balanced (7, default) | dense (12) | greedy. Gap views (up to 3) are added. - backend: qwen-turbo (default: Qwen-Image-2.1-Turbo, 8 steps) | qwen-space (the full Qwen-Image-2.1, 40 steps: slower, about 4x the GPU quota) | fibo-space (most consistent views, best masked edits; CC BY-NC outputs) | mock | external. - texture_size 1024 | 2048 | 4096; view_size 512-1024; uv auto | keep | regen; up (non-glTF files only); inside plain (default) | paint (the inside of mugs, vases and pots: see "Reading results"). pattern_swatches auto (default) | on | off: each view is shown a close-up of the decoration already painted on the part it has to paint, so an all-over pattern (porcelain florals, folk art, fabric) continues onto every side instead of stopping where the white clay looks finished; auto leaves it out for parts described as a scene (landscape, river, mountains), where views must continue the scene, not repeat its motifs. metal keep (default) | flat: the image model paints chrome with reflections, which read as chrome in any renderer. With environment lighting (Cycles/Eevee with an HDRI, three.js with an environment map) they show twice (streaks across a polished surface): flat gives materials your file marks as mirror-like metal (metallic >= 0.9, roughness <= 0.3) an even base colour instead: for grey metals (chrome, steel) your file's own base colour, for metals coloured in the file or named gold, brass, bronze or copper the painted colour. Your metallic and roughness values are kept either way. - Name your parts in Blender/three.js (object.name, material.name): names come back in the GLB and can be used in part_prompts. Avoid default names (Cube.001, Material.002): they carry no meaning. On carved or toy heads (a totem, a doll, a statue) described as a face in part_prompts, model the eyes, nose or mouth as objects named so (Eye_L, Nose, Mouth): views from behind then get that head as "its back, face on the far side" instead of its face description, which otherwise paints a second face on the back of the head. - Give each material a plausible base colour (Blender Principled Base Color, three.js material.color): the image model is shown these colours as a guide to which part is which, so parts that differ only by material (painted stripes, a door panel flush with the wall, eyes on a sphere) are painted exactly where you modelled them and consistently from every side. Automatic when at least two materials have clearly different colours; "color_guide": false if your colours are placeholders. - Regular patterns that must line up all round (a checkered floor, a striped sail or lighthouse, a chessboard) come out wobbly or smeared when only described in the prompt: each view draws its own. Model them instead: faces or strips with two alternating materials (and colours), so the guide places every square and stripe. Time: ~(views x 15 s) with qwen-turbo, ~(views x 35 s) with qwen-space, plus 2-3 min of CPU work here (projecting, baking, previews: the same for every backend). Measured on three 10-view jobs: 3.5-4.5 min with qwen-turbo, 7-10 min with qwen-space; a repair (regenerate a view, retouch) 1-3.5 min either way, mostly re-baking. preset "minimal" is for quick tests: it leaves much of a complex mesh uncovered; use balanced or dense for real work. ## Terrain and ground (POST /api/terrain) For landscapes and the ground of a scene (hills, mountains, valleys, deserts, farmland, a game level's ground, a garden plot), not POST /api/jobs: a few camera views over a 1 km terrain give about 1 m per pixel, blurry up close, and valleys come out as invented lakes. The terrain endpoint textures ground the way game engines do: - a top-down colour map of the whole terrain (the "macro" map): one overview image painted from a colour guide of your layers, then refined tile by tile (each tile repaints its crop of the overview, so all tiles share one layout and look) up to 4096 px, and projected straight down onto the ground; - ground layers (grass, rock, snow, a road, a field...) placed by rules on slope, height, curvature and water flow, or along paths and inside areas you give: splat maps (one weight per layer per point); - one seamless photo texture per layer (it repeats every tile_m metres: 1024 px per tile, ~250 px per metre at 4 m), blended by the splat maps under the colour map, for close-up detail. Request (JSON; only the ground and a prompt are required): {"upload_id": "up_...", # the ground mesh (POST /api/uploads), or mesh_url / mesh_base64 "prompt": "an alpine valley in early summer", "macro_size": 2048, # 1024 (one image) | 2048 (default, 9 tiles) | 4096 (25 tiles) "water_level": 0.0, # optional: your water plane's height (sea, lake): water painted below "layers": [ # optional (up to 8), bottom to top; else "preset" {"name": "grass", "prompt": "short green grass with clover and small daisies", "tile_m": 3}, {"name": "rock", "prompt": "grey granite with cracks and lichen", "where": {"slope": [35, null]}}, {"name": "snow", "prompt": "granular old snow", "where": {"height": [0.75, null], "slope": [null, 40]}}, {"name": "mud", "prompt": "wet stream bank with pebbles", "where": {"flow": [0.8, null]}}, {"name": "road", "prompt": "gravel road with wheel ruts", "paths": [{"points": [[-400, 20], [0, 60], [350, -10]], "width": 5}]}, {"name": "wheat", "prompt": "ripe golden wheat seen from above", "areas": [[[-300, -300], [-100, -300], [-100, -150], [-300, -150]]]}]} - The ground: the terrain object alone (no houses, trees or water planes: everything in the mesh is painted as ground seen from above), exported as GLB (+Y up; Blender's exporter converts), any triangle layout; up to 600k triangles. Or a heightmap instead of a mesh: "heightmap": {"upload_id" | "url" | "base64": a grey PNG (16-bit best) or .npy, "size_m": width or [width, depth], "height_m": height from black to white}; the GLB is then built from it (centred on the origin, row 0 of the image at -Z). - layers: the first layer is the base (everywhere); each next layer covers the earlier ones where its rules hold, with soft, noisy edges ("breakup" 0-1, default 0.5). Rules are [min, max] ranges, null = no bound: slope in degrees; height 0 (lowest point) to 1 (highest); elevation in metres, your mesh's own heights (bands tied to a sea or lake level: {"elevation": [-0.3, 1.2]} for wet sand at a sea at 0; a height band narrower than about 0.2 blurs out); curvature -1 (ridges) to 1 (hollows, valley floors); flow 0 to 1 (water collecting downhill: 0.8+ streams and gullies, 0.95+ the main river). "patches": 0.2 keeps 20 % of the layer's area in natural patches (dirt in grass). paths/areas are ground coordinates in metres: [x, z] of glTF / three.js; from Blender pass "axes": "z-up" and give [x, y]. A layer with only paths or areas exists only there (roads, fields); with rules too, the rules apply inside them. The layer prompt describes the ground close up (it becomes a 1024 px photo texture of tile_m metres, made from that prompt alone: name the material, "groomed snow piste", not "groomed piste"); the main prompt describes the place as a whole: climate, season, region. The colour map shows only the ground: buildings named in the prompt are left out (put them in your scene as 3D models; texture them with POST /api/jobs and give the ground around them as a layer area, e.g. a yard), and roads or streams drawn by a layer's paths follow your points, not the prompt's words. Likewise, the ground covers your layers are named for (meadows, forest, scree...) are left out of the main prompt: the layers say where they go and how much ("wide green meadows" in it had a drawn forest painted as meadow). Trees and bushes appear only when the prompt or a layer asks for them, and with a forest layer only inside it (ask for "scattered trees" or "hedgerows" to get them elsewhere). The texture repeats in fixed directions, so describe surfaces, not structures with a direction: "packed gravel", not "a lane with two ruts and a grass strip" (paths get an even surface automatically). "from_above" (optional) says how a layer looks in the colour map when that differs from its close-up; forests and woods default to their tree crowns. Trees named in the prompt ("cypress trees") are painted from above wherever the prompt puts them; leave them out (or say "no trees") if you place your own 3D trees there. Under a wood of your own 3D trees, make the forest layer their floor: from_above "pine-needle forest floor, no tree crowns" and a brown colour (a green patch is painted as crowns); then no trees are painted anywhere. A layer's "color" [r, g, b] is kept: the colour map is turned to its hue (the image model drifts, e.g. red autumn leaves toward olive) and a layer painted far from it is moved to within 15 (CIELAB), listed in terrain.colours_pulled. Narrow features repeated across a large ground (13 m stripes on 2.4 km) are smaller than the image model can follow: tiles that redraw them give only their fine texture (terrain.tiles_off_layout), softer there; on a smaller ground (13 m stripes on 700 m) they keep their detail. The colour map is lit evenly for your own scene's sun: when the image model paints slopes lit from one side (canyon walls, steep massifs), that brightness is taken out (terrain.light_removed). Presets (when layers is omitted, chosen from the prompt's words): alpine, temperate, desert, arctic, winter, volcanic (GET /api lists them). - uv: planar (default: terrain.glb gets top-down UVs) | keep (the colour map is baked into your own UVs). - backend: qwen-turbo (default) | qwen-space | mock (free dry run). Cost: layers + 1 + tiles images (2048: about 15 images, ~2 min of GPU quota and ~2-3 min in all; 4096: ~31 images). Result (state done): - files.contact_sheet: the whole terrain, then a mid-distance and a close view, each with the colour map alone (left) and with the detail layers (right). layers[] gives each layer's coverage_pct: a layer near 0 % selects no ground (loosen its rules; files.heightmap16 and maps/slope.png show the terrain's values). warnings explain holes in the ground, empty layers, rules that will not do what they say (an unknown rule name, a height band too narrow) and buildings named in the prompt. - files.terrain_glb: your ground (object and material names kept) with the colour map, ready to import in place of your terrain: right from a distance (terrain.macro.metres_per_pixel), soft up close. - files.bundle_zip: everything for close-up detail: terrain.glb, terrain.json (bounds, every file, splat channel per layer, tile sizes, mean colours, the blend formula), blender_terrain.py, three_terrain.js, macro/macro.jpg, splat/splat_0.png (RGBA = layers 1-4; splat_1.png 5-8), layers/.jpg + _normal.jpg + _height.png, maps/height16.png, slope.png, curvature.png, flow.png, macro/color_mod.png (for engines that multiply splatted layers by a colour map: base = sum(w_i * layer_i) * 2 * color_mod). Using the bundle (unzip it next to your scene): Blender: blender scene.blend -P terrain/blender_terrain.py -- terrain Terrain (folder, then your terrain object's name; without a name it imports terrain.glb). Inside Blender: exec(open("terrain/blender_terrain.py") .read()); build("terrain", "Terrain"). The material reads world positions: do not move the terrain after exporting it (or apply the same move to both). three.js: import { createTerrainMaterial } from './terrain/three_terrain.js'; const mat = await createTerrainMaterial(THREE, './terrain/'); gltf.scene.traverse(o => { if (o.isMesh) o.material = mat; }); (or your own ground mesh: same world placement as exported). Up to 4 layers get normal maps; 5-8 layers use colour only (WebGL's 16 texture limit). Both materials hide the repetition of a layer's texture by blending in a second, rotated sampling with low-frequency noise, except for textures with a direction (furrows, rock strata: "variation": false in terrain.json). Close to the camera, where the colour map would be magnified into blurry blobs, they switch to a blurred copy of it (macro/macro_soft.jpg) so the colour stays and the layers give the detail; in Blender the switch distance follows the scene camera present when build() runs (else 1920 px and a 36 mm lens; build(..., focal_px=...) overrides). terrain.glb alone (no detail layers) stays soft close up. Other engines: terrain.json; Unity: splat_*.png are the alphamaps (one channel per layer), Unreal: one weight map per layer channel. Quality loop: POST /api/jobs/{id}/regenerate {"views": ["overview"]} repaints the colour map (new layout and colours, every tile again), ["tile_1_2"] one tile (row from -Z, column from -X), ["layer:rock"] one detail texture. To change where layers go or what they are, create a new terrain job with edited layers. Notes: the colour map comes from above, so terrain.glb alone shows it stretched on near-vertical cliffs. The materials fix that: on faces steeper than about 40-63 degrees they switch to the cliff layers (layers whose slope rule starts at 30 degrees or more, projected from the sides). Give terrains with cliffs such a layer (rock). Forest layers show tree crowns in the colour map and the forest floor up close: add tree models for close views. Model water as your own plane and send its height as "water_level" (the mesh's own heights, e.g. 0 for a sea at 0): the colour map then paints open water below it (turquoise for tropical seas), so the painted shore meets your plane (water the image model paints above that height, as it does around small ponds, is repainted as land and reported in terrain.water_repainted); layers can follow the shore with an elevation rule. Without water_level, a prompt that mentions a lake, pond or sea gets water painted wherever it fits, which need not be where your plane is. Flow rules paint banks and stream beds. ## Reading results - summary.verdict: good (complete and consistent: use it) | usable (fine at a distance; `next` lists optional fixes) | needs work (follow `next`), with verdict_why. - summary.view_mismatch: the texture seen from each view's camera against what that view painted (colour difference, 90th percentile, median over the views): good jobs stay under 30; from 38 the verdict is needs work (parts sharing UV space, views that do not line up with the model). - summary.coverage_pct / holes_pct are measured over the surface a camera can see from the side or above (summary.measured_over). The underside, visible only from below (e.g. the bottom of a crate standing on the ground), is reported apart as underside_pct / underside_covered_pct; uncovered texels are filled with nearby colours. So is the inside of hollow parts seen from above through their opening (a mug, a vase, a pot): inside_pct. It gets the main painted colour of its material (the white glaze of a blue-and-white vase), because views into an opening fail or paint the motif there; send "inside": "paint" for a bowl decorated inside. An underside that will be seen (the graphic under a skateboard deck, a boat's hull): keep the model the right way up and, once the job is done, POST /fill-gaps {"max_views": 2} (offered in `next`): cameras from below paint it, following your part_prompts. The contact sheet does not show the underside: look at those views' image_url (`next` lists them); a view from below sometimes paints the top of the object instead (laces across a sneaker's sole): disable it. Typical good results: coverage 85-95 %, holes < 10 %. - last_result: outcome of the latest operation. retouch: status (applied | empty_mask | rejected), images.compare (before | model input | model output | after), validation attempts; regenerate/texture: per-view status; fill_gaps: coverage before/after and views added. If a retouch was not applied, `next` says how to aim it. - views[].status: generated (used) | rejected (did not line up with the geometry after 3 attempts; excluded) | planned/pending. elevation: degrees above (+) or below (-) the model's centre. edge_agreement/silhouette_iou: alignment scores (good views ~0.3-0.7 / >0.85); painted_corr: agreement with what earlier views already painted (good > 0.7; a view that re-imagines the model from another viewpoint, e.g. a facade instead of a roof seen from above, scores low and is rejected). partial: true = the view matched the geometry but not the texture already painted (each view of a teapot invents its own flowers), so it only paints where it sees the surface better than the views before it. masked_pct (gap views): share of the view left out because it repainted, in other colours, what the views before it had painted (e.g. coloured panels drawn over a plain side seen from above); their paint stays there. spill_pct: share of the view left out where it painted a touching part's colour onto an evenly coloured part (a view drawn slightly larger put a pink bar's paint on the wooden frame next to it); other views paint there. edges_moved (gap views from more than 50 degrees above): share of the render's edges the output drew elsewhere (beyond a slight overall zoom or shift); above 0.2 the subject was redrawn from another angle (a bus's upper windows where its roof is) and the view is rejected. reject_reasons (rejected views): edges | silhouette | painted | colors | moved | below. lowered_from (gap views): the view was rejected from that elevation (degrees; image models often ignore a view from high above and draw their usual angle) and was tried again from 40 degrees. parts_painted_as_one: with the colour guide on, a view that paints a part in the colour of a part it touches although their base colours clearly differ (a dark fretboard painted like the maple neck it lies on) is retried; this lists the parts when every try did it (summary.views_parts_painted_as_one; verdict at most usable). Look at that view: regenerate it or retouch the part, unless your description makes them alike. - summary.views_flagged_inconsistent: views whose content contradicts the others (e.g. a face painted on the back of a head); flagged_views_shown_pct is how much of the surface they actually paint that way. Regenerate them; disable if still flagged. Below about 2 % it rarely shows: look at the contact sheet before spending a retry; below 0.5 % it does not lower the verdict. - retouch_suggestions: problem regions (uncovered / stretched / inconsistent) with an index for /retouch. - warnings: parameter problems worth fixing (e.g. "up" given for a GLB). ## Retouch POST /api/jobs/{id}/retouch with either {"suggestion": i} or a camera + mask: - camera, one top-level field: "view": "" | "render_camera": "orbit03" | "focus": {"point":[x,y,z],"radius":r} | "camera": {"eye":[x,y,z],"target":[x,y,z],"fov":40} e.g. {"render_camera": "orbit00", "mask": {"rect": [0.45, 0.4, 0.55, 0.48]}, "mode": "replace", "prompt": "..."} - mask: "all" | "uncovered" | "low_quality" | {"sphere":[x,y,z,r]} | {"rect":[x0,y0,x1,y1]} | {"circle":[cx,cy,r]} (rect/circle in pixels or 0-1 fractions of the image) - mode: repair | replace (+prompt) | upscale ({"capture_size":256,"output_size":1024}: re-imagine detail) - "dry_run": true answers in seconds with aim_url, the capture with your mask outlined in red, and changes nothing: check that the outline sits on the spot you mean (a guessed rect easily lands on the nose instead of the chin) before spending a generation, then send the same request without dry_run. - A mask cutting through a motif (half a painted stag) makes the model redraw it smaller inside the mask while the rest stays outside it: cover the whole motif, or retouch from a camera where it fills the frame. Use POST /api/jobs/{id}/pick {"render_camera":"orbit02","pixels":[[0.5,0.4]]} to turn a pixel you saw in a render into a 3D point for focus/sphere masks. Retouches that repaint outside the mask or lose the geometry are rejected. Something painted in the wrong place (a window on a roof)? pick that pixel: `painted_by` names the view whose image put it there; regenerate that view, or disable it if it keeps happening. ## Bring your own images (backend "external") The job pauses with state "waiting_for_images" and `pending_view` = {view, condition_url, prompt, width, height}. Generate an image with exactly the conditioning render's framing (paint the white clay, keep geometry), then POST /api/jobs/{id}/views/{view}/image (multipart "file", or JSON {"image_url"} / {"image_base64"}). Views are requested one at a time, in order, each conditioned on the previous ones. ## Errors All errors are JSON {"error", "hint"}. 409 = the job is busy (poll first). 503 = queue full (retry later). state "stopped" = ZeroGPU quota exhausted: retry later with `regenerate` on the remaining views, pass a token, or switch backend to "external". Jobs, uploads and files expire after 12 h or when the Space restarts: download what you need. ## Samples avocado (object, CC0) | lantern (object with thin parts, CC0) | head (scanned bust, character, CC-BY 3.0 Infinite-Realities).