Lane A: grammar.js (shot/angle/focal/light/mark data), presets.js (bbox shot solver, light/mark appliers, DIRECT bar UI, NL box), stage video plates (VideoTexture plane/corner/dome, screen kind, syncVideos), dock video cards. Lane B: scenegod:direct → grouped-undo keys/cuts, 🗣 lip-sync from audio via /rhubarb, Cmd+wheel zoom / wheel scroll, audio trim, video clock-follow via nullable entityVideo seam. Lane C: POST /director (LLM proxy, server-side op validation vs vocab+scene, mock-tested), render.js awaits stage.syncVideos. Sync6 (orchestrator): VIDEO_EXTS in server tree/media types (video primary, img poster as thumb), stage.entityVideo accessor, syncVideos forces videoTex.needsUpdate (rVFC never fires in hidden tabs → black plates). Verified: 9 server groups + timeline + presets suites green; browser SYNC: one-click MCU framing lands camera+fov keys+cut (single undo), golden_hour relight keys, plate scrub-follow frame-exact, finalRender mp4 shows the Flow golden-hour street plate behind an animated character. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
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_PORT8020, bind127.0.0.1(envSCENEGOD_BINDto override later — not now).- Statics:
GET /→web/index.htmlwithCache-Control: no-store(MESHGOD stale-cache lesson);/web/*served with short cache. Use FastAPIFileResponse/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: walkassets/{characters,animations,props,backdrops,audio}. Group rigroom-library style: files sharing dir+stem = one entry with aformatslist ({"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 enforcepath.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.pyfirst — same idea, steal the grouping approach.
C3. Scenes CRUD
GET /scenes→[{name, mtime, duration}];GET /scenes/{name};POST /scenes/{name}with JSON body.nameslugified[a-z0-9-_], filescenes/{name}.json, atomic write (tmp + rename).- Validate on save (stdlib, no jsonschema dep):
version==1, entity ids unique, every track sorted byt, clip blocks non-overlapping per entity (fade overlap allowed),cameraCutsreference 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, dirrenders/{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): exportsdraftRecord(stage, timeline)—canvas.captureStream(fps)+ MediaRecorder whiletimeline.play()s, save webm; andfinalRender(stage, timeline, opts)— resize canvas, looptimeline.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/submitproxy re-implementing the contract in MESHGODmeshgod/ops.py(upload input to /api/assets first; token read from~/Documents/backnforth/.envat request time, never logged; endpoint only active whenSCENEGOD_MB=1).scripts/graft_limb.pyheadless Blender (pattern: MESHGODscripts/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 8020serves 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:
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.renderActiveCameraas a render target is Lane A M2 work; until it lands, testfinalRenderwith 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).- ffmpeg on ultra: check
shutil.whichat boot as designed; it's installed via homebrew here. - Test assets for the tree now exist under
assets/*/test/(gitignored). - 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:
- ffmpeg audio mux from scene
audio[](the sync2-sunset scene inscenes/is your fixture; add a wav toassets/audio/test/). scripts/graft_limb.py(spec in §C6 — blender binary is on ultra's PATH asblender; verify with a mixamo body + any rigged hand GLB, and add the refusal-path test)./mb/submitMODELBEAST proxy last (needsSCENEGOD_MB=1gating 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 toSCENEGOD_LLM_URL(OpenAI-compat; ultra's tailnet Ollama ishttp://100.69.21.128:11434, model viaSCENEGOD_LLM_MODEL). Orchestrator checked /api/tags on that box: it servesqwen2.5:7b(+ an embed model) — default SCENEGOD_LLM_MODEL toqwen2.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:directcontract 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
rejectedlist 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 (M1–M4) 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.
2026-07-20 — M5+M6 ACCEPTED at SYNC 6 (all lanes ran as orchestrator subagents).
Verified live: DIRECT bar CU/low/frame → camera+keys+cut, ONE undo reverts; golden_hour keys; video plate follows the clock frame-exact; finalRender shows the Flow plate behind an animating character. Orchestrator seam fixes to absorb: (1) server: VIDEO_EXTS in tree/media-types, video beats poster as primary; (2) stage.entityVideo(id) added (Lane B seam); (3) syncVideos forces videoTex.needsUpdate — r160 VideoTexture rides requestVideoFrameCallback which never fires in hidden tabs → black plates in renders. Backlog carried: honor audio in/out at mux (C), walk-to-mark prevPos pre-apply (A), live viseme asset still owed by John.