90 lines
4.9 KiB
Markdown
90 lines
4.9 KiB
Markdown
# 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
|
|
(none yet — check back each session)
|