SCENEGOD/logs/laneC.md
type-two 586a3ecbd2 [m7-C] starter templates: read-only API + three working scenes
templates/ ships with the repo (two-hander-golden-hour, record-store,
empty-stage), GET /templates + /templates/{name} behind the same slug and
path-traversal guards as /scenes; presentation metadata rides in a template
key that validate_scene ignores and toJSON drops, so loading then saving
yields plain scene JSON.

Test group asserts the three list in order, round-trip, traversal/unknown
rejected, write verbs refused, and every template passes validate_scene with
every referenced asset present on disk - the check that stops a rotten
template shipping. 10 groups green.

Verified live: two-hander loads 7 entities, 8s, both performers animating,
cut fires cam1 to cam2 at 4.5s; other two load clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 22:34:53 +10:00

14 KiB

Lane C log — append-only, newest at bottom

Format per session:

## <date> session N
DONE: …
DECISIONS: …
BLOCKED/REQUESTS: …
NEXT: …

2026-07-18 session 1

DONE: M1 code-complete. Commit bb85eef.

  • requirements.txt (fastapi, uvicorn only); .gitignore += .venv/.
  • scenegod/server.py single file: statics (/ no-store placeholder, /web/{path} short-cache, path-guarded), /assets/tree (5s cache, per-category grouping), /assets/file (traversal-guarded), /scenes CRUD with atomic write + slugify + validate_scene, render endpoints (begin/frame accept + save; end/status/out 501 until M3).
  • scripts/test_server.py: 5 groups green (tree grouping, file stream, 4 traversal attempts rejected, scene round-trip w/ slugify, 5 validation failures + 1 fade-overlap-allowed). OK: 5 test groups passed. DECISIONS:
  • System python3 is 3.14 (fastapi present but pydantic missing); made a .venv on python3.11 per CLAUDE.md stack rule. Tests + server use .venv/bin/python. .venv/ gitignored.
  • web/ is under scenegod/web/ per PLAN §2 (not repo root) — WEB points there; Lane A/B statics resolve via /web/{path}.
  • Asset grouping: per-category primary ext set (models for chars/anims/ props, images for backdrops, audio for audio). Non-primary image sharing a stem → thumb, not a format. path prefers glb else first model. Entries with no primary format dropped (thumb-only stems aren't assets).
  • Clip-overlap check: violation if next.start < a.end - a.fade, where a.end = start + (out-in)*loop. Fade-region overlap allowed. BLOCKED/REQUESTS: none. NEXT: M2 (when unlocked): scene-validation hardening + asset thumbnails endpoint. M3 render pipeline (ffmpeg encode + web/render.js) is designed in C-server.md §C5, endpoints already stubbed — implement at SYNC-2/M3.

2026-07-18 session 2

DONE: M2 thumbnails + M3 render pipeline (orchestrator green-lit both). Commit b627a0f.

  • /render/begin (uuid dir + meta.json, 503 if no ffmpeg), /frame/{n} (PNG->frames/{n:06d}.png, 400 on empty), /end (spawns bg ffmpeg thread: libx264 crf18 yuv420p, 400 if no frames), /status (queued|encoding| done|error+log tail; on-disk out.mp4 fallback after restart), /out.mp4.
  • Boot-time reap_renders() deletes render dirs >48h.
  • /assets/thumb?path= serves same-stem image sidecar (404 if none).
  • web/render.js: draftRecord (MediaRecorder webm), finalRender (timeline.step -> renderActiveCamera -> toBlob -> POST, <=4 in flight, resize+restore renderer), download() helper. node --check clean.
  • test_server.py: added render group (ffmpeg lavfi -> 30 PNGs -> begin -> post -> end -> poll -> ffprobe duration ~1s) + thumbnail asserts. OK: 6 test groups passed. DECISIONS:
  • Render state in-memory (single-process local tool); status() infers from disk after a restart. ponytail-commented.
  • Thumbnails pass-through, no resize — Pillow would be a new dep (stack rule says log a REQUEST first); not worth it until dock load is measurably slow.
  • finalRender total frames = round(duration*fps); relies on timeline.step being deterministic (orchestrator verified). BLOCKED/REQUESTS:
  • finalRender's per-frame path depends on Lane A stage.renderActiveCamera as a real render target (their M2). Server + ffmpeg path fully tested with synthetic frames; the browser client path is unexercised until A lands it. NEXT: M4 is locked. When unlocked: ffmpeg audio mux (scene audio[]), /mb/submit MODELBEAST proxy, scripts/graft_limb.py. Meanwhile available to integration-test finalRender once Lane A's renderActiveCamera lands.

2026-07-18 session 3

DONE: M4 complete (unlocked after SYNC 3). Commit be65355.

  • Audio mux: /render/end now accepts {audio:[{path,start,gain}]}, muxes via ffmpeg adelay+volume+amix -> aac -shortest; audio paths path-guarded (400 on missing). render.js finalRender passes opts.audio through.
  • scripts/graft_limb.py: headless Blender (blender -b -P ... -- --body B --limb L --bone NAME --out O). Align limb root head->target bone world head, join armatures, parent limb root under target (keep offset), join meshes (vgroup weights survive). Refusal: overlapping bones -> GraftRefused -> exit 2 "re-skin, don't graft". No finish/decimate. --selftest builds synthetic body/limb GLBs and verifies refusal + graft paths under Blender.
  • /mb/submit: MODELBEAST proxy, SCENEGOD_MB=1 gated (503 otherwise), token from ~/Documents/backnforth/.env at request time (never logged; errors scrub it), /api/assets multipart -> /api/jobs, mirrors meshgod/ops.py.
  • test_server.py: 8 groups green (added audio-stream assert, MB gating, MB two-step contract via in-process mock — no farm jobs). DECISIONS:
  • Blender 5 glTF importer injects a stray unparented 42-vert icosphere on every import; _import now keeps only rig-connected meshes (parented to armature / has vgroups / armature modifier) and deletes strays. Right filter for a graft tool regardless.
  • graft refusal check: provides = limb bones - {limb root}; refuse if provides ∩ body bones. Attach/root bone sharing the target name is expected.
  • MB test uses a stdlib http.server mock + a 2nd scenegod instance; asserts Bearer token forwarded, multipart upload, asset_id chained into job payload. BLOCKED/REQUESTS: none. BACKLOG (from orchestrator, not urgent): draft webm via MediaRecorder is untested in the embedded preview (rAF suspension). Needs one verification in a real Chrome window before calling draftRecord done — client-only, needs Lane A stage running live; deferred. NEXT: Lane C M1-M4 all shipped. Available for polish / integration support. Blender binary here is /Applications/Blender.app/Contents/MacOS/Blender (not on PATH as blender in this session, contrary to orchestrator note).

2026-07-18 session 3b

DONE: optional item from orchestrator — /rhubarb?path= endpoint. Commit fb59601.

  • GET /rhubarb?path= (path-guarded) -> rhubarb -f json <wav> -> returns {metadata, mouthCues:[{start,end,value}]} for Lane A's viseme lane. 503 with install hint when the binary is absent (ffmpeg-style gate).
  • test_server.py: +503-when-absent assert (9 groups green). DECISIONS: synchronous subprocess (VO clips are short); rhubarb NOT installed on ultra, so only the 503 path is exercised here — happy path (JSON parse) is untested until rhubarb is on PATH. Flagged same as MediaRecorder backlog. BLOCKED/REQUESTS: none. To fully verify: install rhubarb-lip-sync on ultra, then GET /rhubarb?path=audio/.wav. NEXT: nothing outstanding — Lane C M1-M4 + rhubarb shipped. Standing by.

2026-07-20 session 4

DONE: M5 /director + M6 render.js sliver.

  • scenegod/server.py: POST /director {text, scene:{entities,time}} -> OpenAI-compat chat (SCENEGOD_LLM_URL + /v1/chat/completions, model SCENEGOD_LLM_MODEL default qwen2.5:7b, temp 0.1, response_format json_object, stdlib urllib, 120s timeout). 503 + env hint when URL unset; 502 on LLM error/unreachable/unparseable output — raw model text is NEVER forwarded. _validate_ops: ops shot/light/mark checked against vocab + scene entity ids; violations dropped into rejected [{op, reason}]; response {ops, rejected}. Shot defaults angle="eye", focal=35 filled server-side.
  • Vocab (SHOTS/ANGLES/FOCALS/LIGHTS/MARKS) defined ONCE at top of the director section, mirroring PLAN §5 M5.
  • web/render.js finalRender: after timeline.step(f), before capture: if (stage.syncVideos) await stage.syncVideos(timeline.time) — guarded no-op until Lane A lands it (M6 determinism hook).
  • scripts/test_server.py: +director group (in-process http.server LLM mock, MB-mock pattern): 503 when env unset, 3 valid ops pass through verbatim, unknown op / unknown entity id / unknown preset land in rejected with reasons, prompt carries 10 vocab words + roster ids + playhead, model name
    • low temp asserted. OK: 9 test groups passed (rhubarb-503 group now self-skips — rhubarb is installed on ultra, so that group's absent-path doesn't run; happy path was orchestrator-verified live). DECISIONS:
  • web/grammar.js does NOT exist yet (no presets.js either) — vocab lives in server.py per spec fallback. AT SYNC: re-point the SHOTS/ANGLES/FOCALS/ LIGHTS/MARKS lists + DIRECTOR_SYS at Lane A's grammar.js (read at boot) so server prompt and client presets can't drift. Op shapes I defined (scenegod:direct): {op:"shot",subject,shot,angle,focal} / {op:"light",preset} / {op:"mark",subject,mark,target?} — Lane A's presets.js appliers must accept exactly these.
  • Model may return {"ops":[...]} or a bare array — both accepted (json_object mode forces an object wrapper; bare array tolerated for servers that ignore response_format).
  • Env read at request time (matches _mb_token pattern) so John can set SCENEGOD_LLM_URL without a restart-order footgun; test env explicitly pops it for the 503 assert.
  • focal coerced via int() before vocab check (7b models love "35" strings). BLOCKED/REQUESTS:
  • Lane A: need web/grammar.js (data only) + presets.js appliers matching the op shapes above; flag any shape disagreement at SYNC and I'll conform.
  • Live end-to-end against real qwen2.5:7b on m4pro untested this session (SCENEGOD_LLM_URL unset locally by design) — orchestrator: one smoke call at SYNC with env set is the acceptance step. NEXT: M6 FLOW VIDEO PLATES server side (unlocked on this acceptance): flow_intake.py poster-jpg intake is already in scripts/; verify video exts in /assets/tree (mp4 not in ASSET_EXTS yet — backdrops video category will need it) + deterministic render check once Lane A's stage.syncVideos lands.

2026-07-25 session 5

DONE: M7-C starter templates — server side complete, 10 test groups green.

  • templates/ at repo root (NOT gitignored — ships with the code, unlike assets/scenes/renders). Three files, all valid against the live validate_scene, every referenced path verified present in assets/:
    • templates/two-hander-golden-hour.json — lady + man facing each other (yaw +0.55 / -0.6; characters face +Z at yaw 0 per presets.js), Flow golden-hour street plate (mode:"video", fit:"frustum", groundTint), grammar.js golden_hour key+ambient verbatim (#ffb469 @2.6 / #ffd9b0 @0.55, bg #d98a52, fog 0.02), cam1 "wide" fov40 @5m + cam2 "closer" fov32 @3.4m, cut at t=4.5. 8s. Both carry carrying.fbx; lady's block is trimmed in:2,out:10 so the two performances don't move in lockstep.
    • templates/record-store.json — record-store interior plate, man + carrying.fbx, one camera fov38, warm key + ambient (fog 0.012), 6s.
    • templates/empty-stage.json — one camera, day key+ambient, 10s, no characters, no plate. The clean start.
  • scenegod/server.py:31 TEMPLATES root (SCENEGOD_TEMPLATES, default ./templates); :261-293 the read-only endpoints: GET /templates -> [{name,title,description,thumb?}], GET /templates/{name} -> scene JSON, both through safe_under + slugify (same guard discipline as /scenes). No POST/DELETE by design — load a template, then POST /scenes/{name}.
  • scripts/test_server.py:137 templates group: lists exactly the three, order-leads with the two-hander, round-trips each one, rejects ../../etc/hosts / encoded .. / absolute / unknown (all >=400), write verbs 404/405, and — the check that stops a rotten template shipping — POSTs every template through the real validate_scene and asserts every source.path / params.video|image / clips[].path / audio[].path / thumb exists under assets/, plus camera+light source.type == "none". OK: 10 test groups passed.
  • .gitignore: *.log (a 65KB scenegod-8020.log was sitting untracked at repo root, one git add away from the repo). DECISIONS:
  • Presentation metadata rides INSIDE the scene JSON under a template key ({title, description, thumb, order}) rather than a sidecar or a filename convention: validate_scene ignores unknown top-level keys and Timeline.load/toJSON doesn't carry it, so a saved scene comes out as plain scene JSON with no template residue. order is server-side sort only (not in the response) so the picker leads with the wow template, not alphabetically.
  • Verified before shipping, not assumed: lady.glb is a CC_Base rig (103 CC_Base strings, 0 mixamo) — safe only because Lane A's M7-A alias layer landed; _normalize scales both characters to 1.7m with feet at y=0, so the camera heights/fovs above frame them for real (wide vspan 3.65m, closer 1.97m). Both clips probed under headless Blender: carrying.fbx = 10.0s and hiphop.fbx = 4.467s, and both are in-place (hips travel 10cm / 2cm), so no template performer can wander out of frame.
  • Video plates follow the orchestrator shape (params.video, no image): _buildBackdrop reads p.image || en.source.path, and source.path is the same mp4, so the fallback carries it. If Lane A ever drops that fallback, add image alongside video.
  • Cameras/lights get source:{type:"none"} (sync2-sunset's "upload" was the bug). Light rot is not decorative — _buildLight aims a directional down the wrapper's -Z, so every key light's euler is lookAtEuler(pos,[0,1,0]) computed with presets.js's own algebra. BLOCKED/REQUESTS:
  • @orchestrator: the uvicorn on :8020 is NOT --reload — it still 404s /templates. Restart it before browser-checking.
  • @Lane B (M7-B): the picker contract is GET /templates -> [{name,title,description,thumb?}] (already ordered; thumb is asset-relative -> assets/file?path= or assets/thumb?path=), then GET /templates/{name} -> feed straight to timeline.load() / stage.applyState(), and save with the existing POST /scenes/{name}. NEXT: nothing outstanding in Lane C. Backlog if wanted: a fourth template once a prop/screen asset bank exists, and the still-unverified MediaRecorder draft path (carried from session 3).