# AutoRig — Full API Reference for AI Agents 100% local auto-rigging for humanoid 3D models. This document is the canonical reference for AI agents (Claude, Cursor, Cline, Windsurf, etc.) calling the AutoRig service. Copy-paste the relevant sections into your prompt or use them as a system message. **Base URL (production, SSD):** `https://rig.neuralworkshk.com` **Base URL (staging, NVMe ~1.63× faster):** `https://rig2.neuralworkshk.com` **Base URL (local dev):** `http://127.0.0.1:8421` **MCP endpoint:** `/mcp/sse` (13 tools — includes `autorig_build_character_pack` for iOS fighting games; see MCP section) **OpenAPI spec:** `/openapi.json` **Swagger UI:** `/docs` ### Choosing a server Both run **identical v2.7.0 code**. Pick based on workload: - **rig.neuralworkshk.com** — prod SSD. Has all your historical jobs/rigs. - **rig2.neuralworkshk.com** — NVMe. ~38% faster end-to-end rig, ~4× faster disk reads. Best for new heavy/parallel workloads. - **Data is NOT synced** between them. A `job_id` from rig.* won’t exist on rig2.* and vice versa. Pick one server per character/pipeline. - Both expose the same REST + MCP endpoints. AI agents can register both as parallel MCP servers and round-robin. --- ## Concepts **Job ID (jid)**: every uploaded model gets a UUID. All subsequent calls reference this `jid`. Jobs auto-delete after 24 hours. **Status states**: `preview` → `ready` → `rigging` → `done` (or `error` / `cancelled` at any point). **Coordinate convention**: glTF 2.0 standard. `+Y` up, `-Z` forward, `+X` right. After P0 #2 (2026-05-20), exported GLBs have canonical hips: identity rotation, only Y translation, no baked rotation. Mesh forward = `-Z` in any viewer. --- ## Authentication Open by default. If `AUTORIG_API_KEY` env var is set on the server, every `/api/*` route (except the public allow-list) requires header `X-API-Key: `. **Public routes** (no auth): - `GET /api/health` - `GET /api/version` - `GET /api/anim-list` **MCP endpoint**: currently always open. Tunnel through a Cloudflare Worker or nginx basic-auth for additional protection. Check current auth state: ```bash curl https://rig.neuralworkshk.com/api/health # {"status":"ok","version":"2.0","auth":"open"} # or "auth":"required" ``` --- ## Workflows ### Workflow A — Minimum rig (5 calls, polling) ```bash BASE=https://rig.neuralworkshk.com # 1. Upload — returns job_id + light mesh stats JID=$(curl -sk -F file=@character.glb $BASE/api/upload | jq -r .job_id) # 2. Wait for preview ready while [ "$(curl -sk $BASE/api/status/$JID | jq -r .status)" != "ready" ]; do sleep 2; done # 3. Start rigging (default = standard quality, full fingers) curl -sk -X POST -H "Content-Type: application/json" -d '{}' $BASE/api/rig/$JID # 4. Wait for done while [ "$(curl -sk $BASE/api/status/$JID | jq -r .status)" != "done" ]; do sleep 2; done # 5. Download curl -sk $BASE/api/result/$JID.glb -o rigged.glb ``` ### Workflow B — With webhook (no polling) ```bash JID=$(curl -sk -F file=@char.glb "$BASE/api/upload?callback_url=https://my.app/rig-done" | jq -r .job_id) curl -sk -X POST -d '{}' $BASE/api/rig/$JID # Your server receives POST at https://my.app/rig-done when complete: # {"job_id":"...","status":"done","result_url":"/api/result/.../glb","timestamp":...} ``` ### Workflow C — Retarget animations + export sequence ```bash # Pick animations by tag ANIMS=$(curl -sk "$BASE/api/anim-list?tag=combat&tag=punch") # Retarget specific ones (just metadata, no heavy quaternion payload) curl -sk -X POST -H "Content-Type: application/json" \ -d '{"names":["A_Hook_Punch","Cross_Punch"]}' \ "$BASE/api/anim/$JID?fields=name,duration,frame_count" # Get merge timing meta (NO GLB, just clip [start, end]) curl -sk -X POST -H "Content-Type: application/json" \ -d '{"clips":[ {"name":"Idle","crossfade":0.3}, {"name":"A_Hook_Punch","crossfade":0.3}, {"name":"Walking","crossfade":0.3} ]}' \ $BASE/api/anim-merge-meta/$JID # {"clips":[{"name":"Idle","start":0.0,"end":3.0,"duration":3.0}, ...], "total_duration":..., "fps":30} # Export merged GLB (X-Clip-Timings header has the same meta) curl -sk -X POST -H "Content-Type: application/json" \ -d '{"clips":[...same...]}' \ -D /dev/stderr -o merged.glb $BASE/api/anim-merge-export/$JID 2>&1 | grep -i clip-timings ``` ### Workflow D — Export to all formats ```bash curl -sk $BASE/api/result/$JID.glb -o rigged.glb # glTF binary curl -sk $BASE/api/result/$JID.fbx -o rigged.fbx # ASCII FBX 7.4 (+ baseColor sidecar PNG in response dir) curl -sk $BASE/api/usdz/$JID -o rigged.usdz # iOS RealityKit / AR Quick Look ``` --- ## Endpoint Reference ### System | Method | Path | Description | |--------|------|-------------| | GET | `/api/health` | `{status, version, auth}` (public) | | GET | `/api/version` | `{version: "v2.7.0"}` (public) | | GET | `/api/jobs` | List active jobs (truncated) | | GET | `/api/status/{jid}` | Current job state | `/api/status/{jid}` response: ```json { "status": "rigging", // preview | ready | rigging | done | error | cancelled "progress": 45, // optional, 0..100 "filename": "char.glb", "ext": ".glb", "created": 1716000000.0, "error": null } ``` ### Upload `POST /api/upload` — multipart `file` field. Optional `?callback_url=https://...`. Response: ```json { "job_id": "abc-1234-...", "filename": "char.glb", "format": "glb", "upload_bytes": 248744, "polygons": 5670, "vertices": 5595, "has_existing_rig": false, "warnings": [] } ``` `polygons`/`vertices`/`has_existing_rig` are best-effort (GLB/OBJ/STL accurate; FBX/DAE skipped to keep response fast). Supported uploads: `.fbx .obj .gltf .glb .dae .stl .3ds .x3d .ply .zip`. Max 200 MB. ### Joints (T-pose joint detection) `GET /api/joints/{jid}` — default returns legacy `[{id, label, x, y, z}, ...]`. `GET /api/joints/{jid}?details=true`: ```json { "joints": [ {"id":"head", "label":"Head Top", "x":0.0, "y":0.0, "z":0.845, "confidence":0.92}, ... ], "overall_quality": "good", // good | marginal | poor "warnings": [] } ``` Quality buckets (based on `min` and `avg` of per-joint confidence): - `good`: min ≥ 0.7 AND avg ≥ 0.85 - `marginal`: min ≥ 0.4 AND avg ≥ 0.6 - `poor`: otherwise Confidence = avg(inside_mesh_proximity, lr_symmetry, anatomical_z_ordering). Coarse heuristic — catches gross failures, not subtle misplacements. ### Rig `POST /api/rig/{jid}` — strongly typed Pydantic body: ```json { "mode": "standard", // "standard" | "template" "quality": "standard", // "fast" | "standard" | "high" "finger_mode": "full", // "none" | "simple" | "full" "poly_mode": "desktop", // "mobile" | "desktop" | "full" | "original" "rig_version": "v2", // "v1" | "v2" "skin_method": "heat", // "heat" | "distance" "joints": null // optional override array; null = use auto-detected } ``` Returns `{"status":"rigging"}`. Poll `/api/status/{jid}` or use webhook. Optional `?callback_url=https://...` overrides upload-time webhook. ### Re-rig `POST /api/rerig/{jid}` — modify joint positions, re-skin without re-uploading: ```json { "bone_edits": {"head": {"hx":0.0, "hy":0.0, "hz":0.86}}, "quality": "standard", "rig_version": "v2", "skin_method": "heat" } ``` ### Animations | Endpoint | Description | |----------|-------------| | `GET /api/anim-list` | Plain string list (back-compat) | | `GET /api/anim-list?tag=combat&tag=punch` | AND filter, returns `[{name,tags}, ...]` | | `GET /api/anim-list?details=true` | Always-enriched form | | `POST /api/anim/{jid}` | Retarget on demand | | `POST /api/anim-merge/{jid}` | Merge w/ crossfade — returns JSON | | `POST /api/anim-merge-meta/{jid}` | Clip timing meta only (NO GLB) | | `POST /api/anim-merge-export/{jid}` | Merge + export GLB (includes `X-Clip-Timings` header) | | `POST /api/anim-pack-glb/{jid}` | GLB with N independently named glTF animations (no merge — for Three.js / Unity / Unreal / Godot / NeuralForge runtime clip selection) | Anim tags vocabulary: - **Movement**: locomotion, walk, run, jog, jump, crouch, crawl, climb, swim, turn, strafe - **Posture**: idle, sit, lie, sleep, pose - **Combat**: combat, punch, kick, block, dodge, hit_react, death, boxing, capoeira - **Weapon**: weapon, sword, rifle, pistol, bow, shoot, reload, aim - **Dance**: dance - **Sport**: sport, stretch - **Gesture**: gesture, wave, nod, point, clap, salute, bow, talk, laugh, cry, victory - **Other**: zombie, magic `POST /api/anim/{jid}` body: ```json {"names": ["Walking", "A_Hook_Punch"]} ``` Response (default — full): ```json { "animations": [ { "name": "Walking", "duration": 1.2, "frame_count": 36, "bone_rotations": {"hips": [[0,0,0,1], ...], "spine": [...], ...} } ] } ``` With `?fields=name,duration,frame_count` — skip the heavy `bone_rotations` payload: ```json {"animations": [{"name":"Walking", "duration":1.2, "frame_count":36}]} ``` Max 50 names per call. `POST /api/anim-merge-meta/{jid}` body and response: ```json // Request {"clips":[{"name":"Idle","crossfade":0.3},{"name":"Walking","crossfade":0.2}]} // Response { "clips":[ {"name":"Idle","start":0.0,"end":3.0,"duration":3.0}, {"name":"Walking","start":2.7,"end":3.9,"duration":1.2} ], "total_duration": 3.9, "fps": 30 } ``` `POST /api/anim-merge-export/{jid}` returns GLB binary + `X-Clip-Timings` header (same JSON as above). `POST /api/anim-pack-glb/{jid}` returns a single GLB containing the rigged mesh + skeleton + **N independently named** glTF Animation entries (no merging, no crossfade — clips stand alone for engine runtime selection): ```json // Request { "names": ["Bouncing_Boxing_Idle","Walking","A_Hook_Punch","Cross_Punch","Jump","Hit_React","Death"], "rename": { "Bouncing_Boxing_Idle": "Idle", "A_Hook_Punch": "LightPunch", "Cross_Punch": "HeavyPunch", "Hit_React": "Hit", "Death": "Die" } } // Response: model/gltf-binary + X-Anim-Clips header // X-Anim-Clips: [{"name":"Idle","duration":2.23}, ...] ``` Three.js validation: ```js const gltf = await new GLTFLoader().loadAsync(url); console.log(gltf.animations.map(a => a.name)); // ["Idle","Walking","LightPunch","HeavyPunch","Jump","Hit","Die"] mixer.clipAction(gltf.animations.find(a => a.name === "LightPunch")).play(); ``` This is the **glTF equivalent** of `/api/character-pack/{jid}` (which writes named UsdSkel.Animations in USDZ). Use this for engines that consume glTF natively (Three.js, Babylon.js, Godot, Unity / Unreal glTF importers, NeuralForge); use the USDZ pack for iOS RealityKit. ### Exports | Method | Path | Description | |--------|------|-------------| | GET | `/api/result/{jid}.glb` | Rigged GLB (canonical orientation, T-pose, no animations) | | GET | `/api/result/{jid}.glb?edited=1` | GLB with texture edits + attachments baked in | | GET | `/api/result/{jid}.fbx` | ASCII FBX 7.4 + sidecar baseColor PNG | | GET | `/api/usdz/{jid}` | USDZ for iOS / RealityKit / AR Quick Look | | POST | `/api/anim-pack-glb/{jid}` | GLB with N independently named glTF animations (Three.js / Babylon / Godot / Unity / Unreal) | | POST | `/api/character-pack/{jid}` | ZIP: USDZ (skeleton + skin + materials + N named UsdSkel.Animations) + JSON sidecar for iOS fighting games | #### Character pack body Produces a **single-file iOS RealityKit fighting-game character** per call. Output ZIP is spec-compliant — validates against the iOS team's section-5 validator script. **Request body**: ```json { "char_id": "aether_k", "action_map": { "idle": "Bouncing_Boxing_Idle", "walk_forward": "Walking", "jump": "Jump", "block": "Block_Idle_Pose", "hit": "Hit_React", "victory": "Post_Putting_Emotional_Victory_Gesture", "light_punch": "A_Hook_Punch", "heavy_punch": "Cross_Punch", "light_kick": "Capoeira_Front_Kick", "heavy_kick": "A_Roundhouse_Kick_To_The_Side_Of_An_Opponent" }, "metadata": { "displayName": "Aether-K", "tagline": "Balanced Rushdown", "primaryColor": [0.95, 0.30, 0.20] } } ``` - `action_map` keys: snake_case **game actions** (NOT Mixamo anim names). Values: any Mixamo anim name from `/api/anim-list`. - `char_id`: lowercase snake_case (used as folder name + JSON `id` field). Default = `jid[:8]`. - `metadata`: optional overrides for the JSON sidecar. Anything not provided uses sensible defaults (see "JSON sidecar fields" below). **Required action_map keys** (10 — JSON validator enforces these all present): ``` idle, walk_forward, jump, block, hit, victory, light_punch, heavy_punch, light_kick, heavy_kick ``` **Optional action_map keys** (4): ``` knockdown, get_up, projectile, uppercut ``` **Missing actions auto-fallback** to similar anims so the validator passes. Fallback chains (e.g.): - `block` missing → falls back to `idle` (visual freeze) - `light_kick` missing → falls back to `heavy_kick` → `light_punch` → `idle` - `walk_forward` missing → `walk_back` → `run` → `idle` The JSON's `_actionsFallback` debug field lists which actions were satisfied via fallback so iOS can detect and (optionally) compensate. #### Response `application/zip`, content-disposition `attachment; filename="_pack.zip"`. Typical size: ~5-8 MB. ZIP layout (matches iOS team's spec section 2): ``` characters//.usdz ← UsdSkel skeleton + skinned mesh + materials + N named UsdSkel.Animation prims (canonical PascalCase names: Idle, Walk, LightPunch, HeavyPunch, LightKick, HeavyKick, Block, Hit, Victory, Jump, Knockdown, GetUp, Projectile, Uppercut) characters//.json ← spec section 4 JSON sidecar: id, displayName, tagline, primaryColor, secondaryColor, weightClass, stats, animations{action: {clip, loop, speed}}, hitboxes (defaults for 4 base attacks), hurtbox, plus debug fields (_actionsIncluded, _actionsFallback, _animClipsInUsdz) ``` #### JSON sidecar fields | Field | Default | Notes | |-------|---------|-------| | `id` | `` | Matches folder name. Validator checks. | | `displayName` | `` | Shown in character-select UI | | `tagline` | "Auto-rigged character" | Override via metadata | | `heightMeters` | 1.8 | Mesh height (validator informational only) | | `primaryColor` | `[0.50, 0.50, 0.95]` | Linear RGB 0-1 for HP bar / UI | | `secondaryColor` | `[0.95, 0.95, 0.95]` | Accent color | | `weightClass` | `"medium"` | `"light"` / `"medium"` / `"heavy"` | | `stats.maxHP` | 100 | HP | | `stats.walkSpeed` | 2.5 | m/s | | `stats.jumpHeight` | 3.2 | m peak | | `stats.knockbackResistance` | 1.0 | 0.5-1.5 multiplier | | `animations..clip` | canonical name | UsdSkel.Animation prim name (case-sensitive) | | `animations..loop` | derived | true for idle/walk/block/victory; false otherwise | | `animations..speed` | 1.0 | 1.2 for jump+light_punch, 1.1 for light_kick | | `hitboxes.light_punch` | spec defaults | offsetMeters, sizeMeters, activeFrames, damage, hitstunFrames | | `hitboxes.heavy_punch` | spec defaults | (same) | | `hitboxes.light_kick` | spec defaults | (same) | | `hitboxes.heavy_kick` | spec defaults | (same) | | `hurtbox` | offset=[0,1.0,0], size=[0.7,2.0,0.6] | Hittable volume | You can override any nested field via `metadata` (deep merge — only paths you provide are replaced, rest stays default): ```json { "metadata": { "stats": {"maxHP": 120, "walkSpeed": 3.0}, "hitboxes": {"light_punch": {"damage": 7}} } } ``` #### iOS RealityKit usage ```swift let url = Bundle.main.url( forResource: characterID, withExtension: "usdz", subdirectory: "Models3D/characters/\(characterID)" )! let entity = try await Entity.load(contentsOf: url) let anims = entity.availableAnimations // ["Idle", "Walk", "LightPunch", ...] // Look up the canonical clip name from JSON, NOT the action name let meta = try JSONDecoder().decode(CharacterMeta.self, from: Data(contentsOf: jsonURL)) let idleClipName = meta.animations["idle"]!.clip // "Idle" let idleClip = anims.first { $0.name == idleClipName }! entity.playAnimation(idleClip, transitionDuration: 0.15, startsPaused: false) ``` #### Validation Run the spec's section-5 validator after building (we ship a compatible implementation at `tools/validate_character_pack.py`): ```bash unzip aether_k_pack.zip -d out/ python3 tools/validate_character_pack.py out/ # → PASS aether_k: USDZ has 1 mesh, N anims (Idle, Walk, ...), 4 hitboxes ``` #### Pitfalls / known limitations 1. **No root motion** — character animates in place; game engine drives translation (matches fighting-game convention). 2. **`preview.png` not generated** — spec marks it optional; we don't render it. Provide your own and add to the ZIP if needed. 3. **Anims must already retarget** — if you call this immediately after rigging and an anim isn't in the cache, the endpoint retargets on-demand (~5-10 ms per anim with C accel). For 10 anims expect ~8-12s end-to-end. 4. **`metadata` deep merge** — `hitboxes`, `stats`, `animations` blocks are merged path-by-path; you don't need to repeat the whole structure to override a single value. 5. **Spec section 3a coordinate system** is satisfied because P0 #2 fix bakes canonical hips into the rigged GLB. Mesh forward = `-Z`, up = `+Y`. ### Texture Editor | Method | Path | Description | |--------|------|-------------| | GET / POST | `/api/texture-edit/{jid}` | Per-material HSB + tint | | POST | `/api/texture-replace/{jid}/{material_key}` | Upload new image (.png/.jpg/.webp, 10MB) | | GET | `/api/texture-replace/{jid}/{material_key}/image` | Serve current replacement | | DELETE | `/api/texture-replace/{jid}/{material_key}` | Restore original | ### Attachments (weapons / props on bones) 6 predefined sockets: | Socket | Bone | Use | |--------|------|-----| | `hand_R` | hand.R | Weapon, tool | | `hand_L` | hand.L | Shield, secondary | | `back` | chest | Backpack, large weapon | | `hip_R` | thigh.R | Holster | | `hip_L` | thigh.L | Holster | | `head` | head | Hat, helmet | | Method | Path | Description | |--------|------|-------------| | GET | `/api/attachment/sockets` | List of predefined sockets | | GET | `/api/attachment/{jid}` | Current attachments | | POST | `/api/attachment/{jid}` | Upload item (multipart `file` + form `socket`) — .glb/.gltf/.fbx/.stl/.obj/.dae, 50MB | | PUT | `/api/attachment/{jid}/{idx}` | Adjust offset_pos / offset_rot_euler / scale | | DELETE | `/api/attachment/{jid}/{idx}` | Remove | | GET | `/api/attachment/{jid}/{idx}/item` | Serve attachment file | Attachments bake into the exported GLB via `?edited=1`. ### AI Chat | Endpoint | Description | |----------|-------------| | GET `/api/ai/providers` | List configured providers | | POST `/api/ai/chat` | Send chat turn | Configure providers via env vars: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `XAI_API_KEY`, `GEMINI_API_KEY`, or run Ollama locally (auto-detected at `http://localhost:11434`). --- ## MCP (Model Context Protocol) Integration Mounted at `/mcp/sse`. Add to your AI client config: **Claude Code** (`~/.claude.json` or `claude_desktop_config.json`): ```json { "mcpServers": { "autorig": { "url": "https://rig.neuralworkshk.com/mcp/sse" } } } ``` **Cursor** (`Settings → MCP`): ```json { "mcpServers": { "autorig": { "url": "https://rig.neuralworkshk.com/mcp/sse" } } } ``` ### 13 MCP tools | Tool | Maps to | |------|---------| | `autorig_health` | GET /api/health | | `autorig_upload` | POST /api/upload (multipart) — server reads from `file_path` argument | | `autorig_get_status` | GET /api/status/{jid} | | `autorig_get_joints` | GET /api/joints/{jid}?details=true | | `autorig_rig` | POST /api/rig/{jid} | | `autorig_wait_for_done` | Polls status until terminal state (convenience) | | `autorig_list_anims` | GET /api/anim-list?tag=...&details=true | | `autorig_retarget` | POST /api/anim/{jid}?fields=duration,frame_count | | `autorig_merge_meta` | POST /api/anim-merge-meta/{jid} | | `autorig_export_glb` | GET /api/result/{jid}.glb | | `autorig_export_fbx` | GET /api/result/{jid}.fbx | | `autorig_export_usdz` | GET /api/usdz/{jid} | | `autorig_build_character_pack` | POST /api/character-pack/{jid} (saves ZIP to disk) | | `autorig_export_glb_with_clips` | POST /api/anim-pack-glb/{jid} (saves GLB with N named clips to disk) | Note: `autorig_upload` accepts `file_path` argument. Server reads from local disk — works when AI client runs on the same host as the AutoRig server. For remote use, upload via REST POST or fetch from an HTTP URL. --- ## Python SDK example (minimal) ```python import requests, time BASE = "https://rig.neuralworkshk.com" # Upload with open("char.glb", "rb") as f: r = requests.post(f"{BASE}/api/upload", files={"file": f}).json() jid = r["job_id"] print(f"Job {jid}: {r['polygons']} polys, {r['vertices']} verts") # Wait for preview ready while requests.get(f"{BASE}/api/status/{jid}").json()["status"] != "ready": time.sleep(2) # Check joint confidence joints = requests.get(f"{BASE}/api/joints/{jid}?details=true").json() print(f"Quality: {joints['overall_quality']}") if joints['overall_quality'] == 'poor': raise SystemExit("Joint detection failed — model may not be humanoid") # Rig requests.post(f"{BASE}/api/rig/{jid}", json={"quality": "standard"}) # Wait for done while requests.get(f"{BASE}/api/status/{jid}").json()["status"] != "done": time.sleep(2) # Find combat anims, retarget metadata only combat_anims = requests.get(f"{BASE}/api/anim-list?tag=combat&tag=punch").json() names = [a["name"] for a in combat_anims[:3]] requests.post(f"{BASE}/api/anim/{jid}?fields=duration,frame_count", json={"names": names}) # Export USDZ for iOS usdz = requests.get(f"{BASE}/api/usdz/{jid}").content with open("rigged.usdz", "wb") as f: f.write(usdz) ``` ### Character pack (iOS fighting game) — full end-to-end example ```python import requests BASE = "https://rig.neuralworkshk.com" # Assume jid is a rigged job in status="done" jid = "..." # Pick your animations (game action → Mixamo anim name) action_map = { "idle": "Bouncing_Boxing_Idle", "walk_forward": "Walking", "jump": "Jump", "block": "Block_Idle_Pose", # may not exist; will fall back to idle "hit": "Hit_React", "victory": "Post_Putting_Emotional_Victory_Gesture", "light_punch": "A_Hook_Punch", "heavy_punch": "Cross_Punch", "light_kick": "Capoeira_Front_Kick", "heavy_kick": "A_Roundhouse_Kick_To_The_Side_Of_An_Opponent", } r = requests.post( f"{BASE}/api/character-pack/{jid}", json={ "char_id": "aether_k", "action_map": action_map, "metadata": { "displayName": "Aether-K", "tagline": "Balanced Rushdown", "primaryColor": [0.95, 0.30, 0.20], "stats": {"maxHP": 110, "walkSpeed": 2.8}, }, }, ) r.raise_for_status() with open("aether_k_pack.zip", "wb") as f: f.write(r.content) print(f"Saved {len(r.content) / 1e6:.1f} MB pack to aether_k_pack.zip") # Inspect what made it in: import zipfile, json with zipfile.ZipFile("aether_k_pack.zip") as z: meta = json.loads(z.read("characters/aether_k/aether_k.json")) print("Included:", meta["_actionsIncluded"]) print("Fallbacks:", meta.get("_actionsFallback", {})) print("USDZ clips:", meta["_animClipsInUsdz"]) ``` Drop the resulting ZIP into your iOS app's `Models3D/` folder. The validator at `tools/validate_character_pack.py` confirms section 5 compliance. --- ## Error codes | HTTP | Meaning | Action | |------|---------|--------| | 400 | Bad request — missing field, invalid value, oversized file | Check body schema in `/openapi.json` | | 401 | Missing or invalid `X-API-Key` (only if auth enabled) | Set `X-API-Key` header | | 404 | Job not found (may have expired after 24h) | Re-upload | | 422 | Pydantic validation failed | Fix body shape — see error.detail | | 500 | Internal error | Check `/api/health`; report with `jid` and request | | 503 | Subsystem unavailable (e.g. AI chat without httpx) | Server config issue | --- ## Common pitfalls 1. **Default `User-Agent: python-urllib/*` may be blocked by Cloudflare.** Set a custom UA: `headers={"User-Agent": "MyApp/1.0"}`. 2. **Re-rigged jobs from before 2026-05-20 have non-canonical hips rotation.** Re-rig once to get identity hips. Anything rigged AFTER is glTF-canonical (mesh faces -Z). 3. **`POST /api/anim/{jid}` default response is heavy** (~100KB per animation). Always pass `?fields=duration,frame_count` if you don't need rotations. 4. **Job auto-deletes after 24h.** Save output GLB locally if you need it later. 5. **Webhook URL must be reachable from VPS** (public Internet). `localhost` / `192.168.*` / `10.*` etc. are rejected by SSRF guard. 6. **`mocap/cache_glb/`** must exist on the server for fast animation retarget. Without it, fallback to FBX path (slower but works). 7. **MCP `/mcp/sse` via Cloudflare may need nginx SSE-friendly headers** (`proxy_buffering off; proxy_http_version 1.1; proxy_set_header Connection "";`). Local dev works without config. --- ## Performance characteristics - Upload: ~1 second per 10 MB - Preview generation: ~3-5 seconds - Rig (standard quality, heat skinning): ~30-60 seconds for typical 5000-poly model - Animation retarget: ~5 ms per anim with C extension (was 100 ms in pure Python) - USDZ conversion: ~5 seconds - FBX export: ~3 seconds for 35-bone skeleton Total upload-to-rigged for a typical character: **~40 seconds**. --- ## Version history - **v2.7.0** (2026-05-20): Canonical hips orientation, USDZ working, FBX export, anim tags, joint confidence, MCP wrapper, webhook callback, paint-on-model, texture extract, IK + onion skinning - **v2.0** (2026-05-18): Texture editor, weapon attachments, API auth + Unity/Python SDK, AI chat L1 - **v1.0** (2026-05-17): Stable baseline, GLB animation cache, C extension retarget (19.82× speedup), USDZ stubs --- ## Contact - GitHub: (private repo) - Issues: file via your AI agent's session Last updated: 2026-05-20