SCENEGOD/lanes/C-server.md
type-two 86b76f6bdc [m6] Flow video plates spec + flow_intake.py; M6 queued behind M5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 01:25:14 +10:00

9.6 KiB
Raw Blame History

Lane C — Server: FastAPI, assets, scenes, render pipeline

You own: scenegod/server.py, web/render.js, scripts/*, requirements.txt, .gitignore. Milestone: M1.

C1. Scaffold (first commit)

  • requirements.txt: fastapi, uvicorn. Nothing else.
  • .gitignore: .env, assets/, scenes/, renders/, __pycache__/, *.pyc, .DS_Store.
  • scenegod/server.py, single file (split only when it hurts — MESHGOD's server.py is one file at 800+ lines and fine). SCENEGOD_PORT 8020, bind 127.0.0.1 (env SCENEGOD_BIND to override later — not now).
  • Statics: GET /web/index.html with Cache-Control: no-store (MESHGOD stale-cache lesson); /web/* served with short cache. Use FastAPI FileResponse/StaticFiles, headers set explicitly for the HTML.

C2. Assets endpoints

  • Roots from env with defaults: SCENEGOD_ASSETS=./assets, SCENEGOD_SCENES=./scenes, SCENEGOD_RENDERS=./renders (create scenes/ renders on boot; assets must exist — clear error if not).
  • GET /assets/tree: walk assets/{characters,animations,props,backdrops,audio}. Group rigroom-library style: files sharing dir+stem = one entry with a formats list ({"name":"lady","path":"characters/pack01/lady.glb", "formats":["glb","fbx"]}). Extensions: glb/gltf/fbx/obj/bvh + jpg/png/webp (backdrops) + wav/mp3 (audio). Scan live, no index file, cache 5s.
  • GET /assets/file?path=: resolve and enforce path.resolve().is_relative_to(ASSETS_ROOT) — reject .., absolute paths, symlink escapes (resolve first, then check). Correct content-types (model/gltf-binary, etc.). This guard pattern applies to every path-taking endpoint you ever add here (ship-check rule).
  • Read /Users/johnking/Documents/MESHGOD/meshgod/library.py first — same idea, steal the grouping approach.

C3. Scenes CRUD

  • GET /scenes[{name, mtime, duration}]; GET /scenes/{name}; POST /scenes/{name} with JSON body. name slugified [a-z0-9-_], file scenes/{name}.json, atomic write (tmp + rename).
  • Validate on save (stdlib, no jsonschema dep): version==1, entity ids unique, every track sorted by t, clip blocks non-overlapping per entity (fade overlap allowed), cameraCuts reference existing camera entities. Reject with a 422 listing every violation (agents and UI both rely on the messages).

C4. scripts/test_server.py (M1 gate)

Stdlib urllib + subprocess (uvicorn on a test port, tmp asset/scene dirs): asserts tree grouping, file streaming, traversal attempts → 4xx (?path=../../etc/hosts, absolute, URL-encoded ..), scene round-trip, validation failures (unsorted keys, dup ids). python3 scripts/test_server.py green = M1 code-done. Run with | lm -l 2.

C5. Render pipeline — M3, but design lands now (endpoints stubbed 501)

  • Session model: POST /render/begin {name,fps,width,height}{renderId} (uuid, dir renders/{id}/frames/). POST /render/{id}/frame/{n}: raw PNG body → frames/{n:06d}.png. POST /render/{id}/end {audio?} → spawn ffmpeg in a background thread: ffmpeg -framerate {fps} -i frames/%06d.png -c:v libx264 -pix_fmt yuv420p -crf 18 out.mp4 (audio inputs + -map/amix in M4). GET /render/{id}/status = queued|encoding|done|error(+log tail); GET /render/{id}/out.mp4. ffmpeg presence checked at boot (shutil.which), endpoint 503s w/ message if absent. Reap render dirs >48h old on boot.
  • web/render.js (M3): exports draftRecord(stage, timeline)canvas.captureStream(fps) + MediaRecorder while timeline.play()s, save webm; and finalRender(stage, timeline, opts) — resize canvas, loop timeline.step(f)stage.renderActiveCamera()canvas.toBlob('image/png') → POST frame (3-4 in flight, backpressure via await), then /end, poll status, download link. Deterministic: never uses wall-clock time.

C6. M4 (locked): audio, MODELBEAST, graft

  • ffmpeg audio mux per scene JSON audio[].
  • POST /mb/submit proxy re-implementing the contract in MESHGOD meshgod/ops.py (upload input to /api/assets first; token read from ~/Documents/backnforth/.env at request time, never logged; endpoint only active when SCENEGOD_MB=1).
  • scripts/graft_limb.py headless Blender (pattern: MESHGOD scripts/glb2fbx.py): --body body.glb --limb hand.glb --bone mixamorig:RightHand → align limb root bone to target bone head, join armatures, parent-keep-offset, join meshes (weights survive), export. If the body armature ALREADY has the bones the limb provides → exit 2 with "bones exist; mitten weights — re-skin, don't graft". Never route output through any finish/decimate path (rig contract).

M1 acceptance (log it)

  • uvicorn scenegod.server:app --port 8020 serves Lane A's page (or a placeholder index if A hasn't landed — don't block on them).
  • test_server.py green; paste the summary line in your log.

ORCHESTRATOR UPDATES

2026-07-18 — M1 ACCEPTED. M2 unlocked AND M3 render pipeline green-lit early.

test_server.py green, tree/file/scenes verified live through the browser page (scene save 200 + lossless reload). You're ahead of the other lanes, so skip the wait: after the small M2 items (thumbnails endpoint; validation already solid), go straight into §C5 — implement /render/begin|frame|end|status|out

  • ffmpeg encode, and web/render.js. Notes:
  1. timeline.step(frame) is verified deterministic against the real Stage (orchestrator stepped 0/30/60/90/120 and got exact eased positions) — your final-render loop can rely on it today. stage.renderActiveCamera as a render target is Lane A M2 work; until it lands, test finalRender with the director view canvas, and test the ffmpeg path with synthetic PNGs from a script (extend test_server.py: begin → POST 30 generated frames → end → poll → assert mp4 exists + ffprobe duration ≈ 1s).
  2. ffmpeg on ultra: check shutil.which at boot as designed; it's installed via homebrew here.
  3. Test assets for the tree now exist under assets/*/test/ (gitignored).
  4. uvicorn for local dev: .venv/bin/uvicorn scenegod.server:app --port 8020 — there's also a launch entry in MESHGOD/.claude/launch.json ("scenegod") used by the orchestrator's browser preview.

2026-07-18 — M2+M3 ACCEPTED, SYNC 3 PASSED. M4 is UNLOCKED.

Orchestrator ran your finalRender end-to-end in the integrated app against Lane A's renderActiveCamera: 240 frames @1920×1080 → ffmpeg → out.mp4, h264, exactly 8.000s, with camera cuts and sunset light lerp visible in extracted frames. The browser client path you couldn't exercise is now verified — no changes needed. Proceed with M4 in this order:

  1. ffmpeg audio mux from scene audio[] (the sync2-sunset scene in scenes/ is your fixture; add a wav to assets/audio/test/).
  2. scripts/graft_limb.py (spec in §C6 — blender binary is on ultra's PATH as blender; verify with a mixamo body + any rigged hand GLB, and add the refusal-path test).
  3. /mb/submit MODELBEAST proxy last (needs SCENEGOD_MB=1 gating like riggermore; test only that auth/upload contract matches meshgod/ops.py — don't burn farm jobs on repeat runs). One render-quality note for your backlog, not urgent: draft webm via MediaRecorder is untested in the embedded preview (rAF suspension) — verify it once in a real Chrome window before calling it done.

2026-07-19 — M5 DIRECTOR MODE unlocked: POST /director (spec PLAN §5 M5).

  • rhubarb is now INSTALLED on ultra (built from source, arm64, v1.14.0, symlinked /opt/homebrew/bin/rhubarb) — your happy path is verified: GET /rhubarb?path=audio/test/vo.wav → real mouthCues. Nothing to do there.
  • POST /director {text, scene:{entities:[{id,kind,label}], time}} → chat request to SCENEGOD_LLM_URL (OpenAI-compat; ultra's tailnet Ollama is http://100.69.21.128:11434, model via SCENEGOD_LLM_MODEL). Orchestrator checked /api/tags on that box: it serves qwen2.5:7b (+ an embed model) — default SCENEGOD_LLM_MODEL to qwen2.5:7b; John can pull a gemma later and it's just an env change. 503 with a hint when env unset. stdlib urllib only.
  • System prompt: translate director language into a JSON array of ops matching Lane A's scenegod:direct contract EXACTLY (enumerate the shot/ angle/focal/light/mark vocabularies from grammar.js in the prompt; read the file at boot so the vocab never drifts). format: json / low temp.
  • VALIDATE server-side before returning: unknown op/shot/preset/entity id → drop the op and include it in a rejected list so the UI can toast it. Never forward raw model text to the client.
  • test_server.py: mock the LLM (in-process http.server, same pattern as your MB mock): assert prompt carries the vocab, valid ops pass, garbage ops land in rejected, 503 when env unset.

2026-07-18 — M4 ACCEPTED. Lane C scope (M1M4) is COMPLETE.

Orchestrator verified in the integrated app: audio-muxed finalRender → h264+aac mp4, both streams exactly 8.000s; graft_limb --selftest green under Blender 5.0.1 (both refusal + graft paths); 8 server test groups pass. Your blender-path correction noted (it's /Applications/Blender.app/..., not on PATH — my error). Standing by is fine; remaining work is A/B UI (viseme lane, audio rows). One item if you want it: add a rhubarb check + /rhubarb?path= endpoint (subprocess → JSON) IF rhubarb is installed — 503-with-message otherwise, same pattern as ffmpeg. Log a REQUEST first if it needs more than that.

2026-07-19 — M6 FLOW VIDEO PLATES queued behind M5 (full spec PLAN §5 M6).

Do M5 first; M6 is unlocked the moment your M5 acceptance is logged. Test media will be arriving in assets/backdrops/video/ via scripts/flow_intake.py.