SCENEGOD/lanes/C-server.md
type-two a8273127ba [m5] DIRECTOR MODE round: cinema-grammar presets spec + /director LLM lane; rhubarb live
- PLAN §5 M5: shot sizes/angles/focals/light presets/blocking marks as
  one-click deterministic presets (grammar.js data + presets.js solver,
  Lane A), keyed at the playhead via scenegod:direct events (Lane B),
  optional NL box -> POST /director -> tailnet Ollama with server-side op
  validation (Lane C; m4pro serves qwen2.5:7b)
- rhubarb 1.14.0 built from John's source zip (JING5), installed arm64 at
  ~/.local/opt/rhubarb, symlinked /opt/homebrew/bin; happy path verified:
  /rhubarb on a real spoken wav (assets/audio/test/vo.wav) returns mouthCues
- lane B unlocked to wire the lip-sync-from-audio button now

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 01:09:30 +10:00

160 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.