diff --git a/.gitignore b/.gitignore index 9b9193b..90b9755 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ assets/ scenes/ renders/ +films/ +sequences/* +!sequences/demo-two-shot.json *.log __pycache__/ *.pyc diff --git a/deploy/deploy.sh b/deploy/deploy.sh index af465f9..0cc62c3 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -1,9 +1,13 @@ #!/bin/bash # rsync-deploy scenegod to the dealgod VPS (NOT a git checkout there — meshgod model). +# NOTE: assets/ is EXCLUDED on purpose (it lives in the VPS volume). Templates and +# sequences ship with the code, so after adding an asset one of them references: +# rsync -az assets/ root@100.94.195.115:/opt/scenegod-data/assets/ set -e VPS=root@100.94.195.115 rsync -az --delete \ --exclude .git --exclude .venv --exclude assets --exclude scenes \ - --exclude renders --exclude '*.pyc' --exclude __pycache__ --exclude .env \ + --exclude renders --exclude films --exclude '*.pyc' --exclude __pycache__ \ + --exclude .env --exclude '*.log' \ "$(cd "$(dirname "$0")/.." && pwd)/" "$VPS":/opt/scenegod/ -ssh "$VPS" 'mkdir -p /opt/scenegod-data/{assets,scenes,renders} && cd /opt/scenegod/deploy && docker compose up -d --build' +ssh "$VPS" 'mkdir -p /opt/scenegod-data/{assets,scenes,renders,sequences,films} && cd /opt/scenegod/deploy && docker compose up -d --build' diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index ffe826e..dfb9890 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -14,6 +14,8 @@ services: - SCENEGOD_ASSETS=/app/assets # mounted volume — synced from the Macs - SCENEGOD_SCENES=/app/scenes - SCENEGOD_RENDERS=/app/renders + - SCENEGOD_SEQUENCES=/app/sequences # M8 films: shot lists survive a rebuild + - SCENEGOD_FILMS=/app/films # deliberately UNSET on the VPS (each 503s cleanly / hides its UI): # SCENEGOD_LLM_URL / SCENEGOD_LLM_MODEL (tailnet Ollama — ultra-side feature) # SCENEGOD_MB (MODELBEAST proxy — NEVER on the VPS) @@ -21,6 +23,8 @@ services: - /opt/scenegod-data/assets:/app/assets - /opt/scenegod-data/scenes:/app/scenes - /opt/scenegod-data/renders:/app/renders + - /opt/scenegod-data/sequences:/app/sequences + - /opt/scenegod-data/films:/app/films networks: [dealgod_default] networks: dealgod_default: diff --git a/lanes/C-server.md b/lanes/C-server.md index ab42c80..6dbd1f0 100644 --- a/lanes/C-server.md +++ b/lanes/C-server.md @@ -183,3 +183,21 @@ that pattern for anything else that ships data alongside code. NEXT for Lane C when you want it: the post-M7 backlog in PLAN §5 is yours to start — multi-shot SEQUENCING (ordered shot list of scenes, rendered and ffmpeg-concatenated into one film) is the biggest single unlock left in the project. + +### 2026-07-25 — M8-C ACCEPTED at SYNC 10. **Sequencing works; SCENEGOD makes FILMS now.** +Verified live end-to-end, twice. (1) Your shipped `demo-two-shot` → one 11.500s mp4; the +frame at t=4.25 is visibly TWO shots superimposed, so the xfade really composites, and the +hard cut into shot 3 lands. (2) A fresh film built from the M7 templates — saved +two-hander-golden-hour and record-store as scenes, cut them into `milk-bar-film` (wide → +dissolve → closer → cut → interior) → 11.800s @960×540, correct on every boundary. +Your two flagged deploy lines are done by the orchestrator: compose now sets +SCENEGOD_SEQUENCES/SCENEGOD_FILMS with `/opt/scenegod-data/{sequences,films}` volumes, and +deploy.sh mkdirs both + excludes `films`/`*.log`. +Excellent instincts this round: ffprobe'ing real durations for xfade offsets rather than +trusting nominal length, validating dissolve-shorter-than-both-shots (xfade's actual +requirement), persisting the film plan so /end survives a restart, and catching the +pre-existing missing `apad` that let short audio truncate the PICTURE. +NEXT for Lane C: audio `in`/`out` at mux is STILL the open gap (Lane B's head-trim made it +easy to hit) — close that. Then beat-sync groundwork: a `POST /beats?path=` that runs a beat +detector over an audio asset and returns a grid the timeline can snap to (MODELBEAST demucs +is available on ultra; the endpoint should 503 cleanly on the VPS like the others). diff --git a/logs/laneC.md b/logs/laneC.md index e24a29b..bde4e58 100644 --- a/logs/laneC.md +++ b/logs/laneC.md @@ -225,3 +225,111 @@ BLOCKED/REQUESTS: 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). + +## 2026-07-25 session 6 +DONE: **M8-C MULTI-SHOT SEQUENCING** — the film-level object. 15 test groups green +(was 10). A scene is one take; a *sequence* is an ordered list of trimmed scene +windows joined by cuts/dissolves over a film-level audio bed. +- **Sequence doc + validator** — `scenegod/server.py:443 plan_sequence()` validates + AND resolves in ONE pass (save-time validation and render-time planning can never + disagree). Roots at `:32-33` (`SCENEGOD_SEQUENCES` -> ./sequences, `SCENEGOD_FILMS` + -> ./films, both mkdir'd on boot like scenes/renders). Violations, all 422 with a + message per offence: version!=1, empty/oversized shots (max 200), unknown scene, + bad scene name, in/out not numbers, in<0, in>=out, out past the scene's duration, + unknown transition, transitionDur out of 0..10s, dissolve with dur<=0, dissolve not + shorter than BOTH shots it joins (ffmpeg xfade requirement), scenes with mixed fps + ("one film, one fps"), audio path traversal / missing file / bad start|gain. +- **Endpoints** `:563-800`: `GET /sequences` (name,title,mtime,shots,duration,ok), + `GET|POST|DELETE /sequences/{name}` (slugify + safe_under + capped 1MB + atomic + write), `POST /sequences/{name}/render` -> `{filmId,fps,width,height,duration, + warnings,shots[]}`, `POST /films/{id}/shot/{i} {renderId}`, `POST /films/{id}/end`, + `GET /films/{id}/status` -> `{state,shot,of,log}`, `GET /films/{id}/out.mp4`. + States: collecting -> queued -> encoding -> done|error. +- **The film render is a client-driven job** (`:648 _concat_film`): the browser is the + only thing that can render a scene, so the server hands out the resolved shot plan, + the client renders each shot through the UNCHANGED `/render/*` session endpoints and + reports renderId-per-index, and `/end` folds the shot mp4s with ONE ffmpeg + filter_complex: each input `scale=WxH,setsar=1,fps,format=yuv420p`, then a left fold + — cut = `concat=n=2:v=1:a=0`, dissolve = `xfade=transition=fade:duration=D:offset=` + (running accumulated length - D). Film-level `audio[]` -> adelay/volume/amix/apad, + `-shortest`. Film dirs reaped >48h (`:75 reap_dirs(root)`, now called for RENDERS + and FILMS). +- **`scenegod/web/film.js`** (new): `renderSequence(stage, timeline, seqName, opts)` + drives the whole loop — POST render -> per shot `GET scenes/{scene}` -> + `timeline.applyScene(json)` (Lane B's single apply path) -> `captureFrames` over + frames [in*fps, out*fps) -> end+poll the shot -> `POST films/{id}/shot/{i}` -> + `films/{id}/end` -> poll -> resolve with `films/{id}/out.mp4`. Plus + list/get/save/deleteSequence helpers so a UI lane never hand-rolls (or absolutises) + the URLs. Relative URLs only. +- **`web/render.js` refactored, not duplicated**: the frame loop is now + `captureFrames(stage,timeline,{renderId,from,to,onProgress})` + `withRenderSize` / + `beginRender` / `endRender` / `pollRender` / `postJSON` / `getJSON`, all exported; + `finalRender(stage,timeline,opts)` keeps its exact old signature and is now 6 lines + built from them. film.js imports them — one frame-post path in the repo. +- **`sequences/demo-two-shot.json`**: 3 shots, 11.5s — sync2-sunset 0..4.5 (cam1) + dissolve 0.5s into sync2-sunset 4.5..8.0 (cam2, same scene, other side of its cut), + cut to sync1-demo 0..4.0. Verified live: `GET /sequences` -> ok:true duration 11.5, + `POST /sequences/demo-two-shot/render` -> the 3-shot plan, and a full fake-shot + end-to-end (lavfi mp4s registered through the real endpoints) produced out.mp4 at + exactly 11.500000s. +- **`scripts/test_server.py`**: +5 groups (`:225` CRUD round-trip incl. slugify + + lossless read-back + DELETE 200/404; `:248` 13 validation failures each asserted for + its own message + traversal/unknown guards on GET and DELETE; `:281` film smoke — + two lavfi mp4s registered as shots, end, poll, ffprobe duration 5.0s = 2+3, output + 64x64, plus guards: end-before-registration 400, bad index 400, traversal renderId + 400, unknown renderId 400, 32x32 shot -> 400 "one resolution per film", unknown + filmId 404; `:340` dissolve variant -> 4.5s = 2+3-0.5 with an audio bed muxed; + `:370` the shipped example can't rot — every shot's scene must exist and every + `out` must be within that scene's duration). `OK: 15 test groups passed`. +DECISIONS: +- **One fps per film, validated** rather than resampling: `timeline.step(frame)` is + the deterministic seek and it divides by the SCENE's fps, so a film whose fps + differed from a scene's would silently render the wrong instants. Mixed-fps + sequences are a clear 422 and the film's fps comes from the scenes, not the client. + Client passes width/height only. +- **One resolution per film, enforced at registration** (compare `renders/{rid}/ + meta.json` w/h/fps to the film's) so the failure is a clear 400 the moment it + happens, not a cryptic ffmpeg filter error after the last shot. The scale/setsar + filter stays as the belt to that braces (renders with no meta.json). +- **xfade offsets come from ffprobe'd durations** (`:636 _probe_dur`, nominal length + as fallback): nominal length can differ from the encoded mp4 by up to a frame, and + an absolute xfade offset that's off by a frame shows as a stutter at the join. +- **Shots render silent; the soundtrack is film-level.** Concat with per-shot audio + needs every shot to have an audio stream (mixed presence breaks the filter), and a + film's bed rarely respects shot boundaries anyway. Not silent-by-surprise: the + render-start response carries a `warnings[]` naming any referenced scene whose + `audio[]` won't be carried, and film.js logs them. +- Existing `/render/end` mux gained `apad` before `-shortest` (`:326`) — a VO shorter + than the shot was truncating the PICTURE. Same fix in the film mux; the M4 audio + assert still passes. +- `sequences/` ships (like `templates/`) but is user-writable, so `.gitignore` takes + `sequences/*` + `!sequences/demo-two-shot.json`: the example is tracked, John's own + sequences stay out of `git status`. `films/` fully ignored. +- Film plan + shot registrations persist in `films/{id}/meta.json` (atomic tmp+rename), + not just memory: a film spans minutes of client rendering and `/end` must survive a + server restart underneath it. Only the encode state/log is in-memory (renders model). +BLOCKED/REQUESTS: +- @orchestrator (deploy/, not my lane) — two one-line prod fixes so sequences persist: + 1. `deploy/docker-compose.yml`: add `- SCENEGOD_SEQUENCES=/app/sequences` and + `- SCENEGOD_FILMS=/app/films` to environment, and + `- /opt/scenegod-data/sequences:/app/sequences` + + `- /opt/scenegod-data/films:/app/films` to volumes. + 2. `deploy/deploy.sh`: `mkdir -p /opt/scenegod-data/{assets,scenes,renders,sequences,films}` + and add `--exclude films` to the rsync. + Without these the app still boots and works (it mkdirs both dirs inside the + container) but a rebuild eats every saved sequence. Nothing else new is needed in + prod: no LLM/MB env, ffmpeg is already in the image. +- @orchestrator — the :8020 uvicorn is not `--reload`; restart it before browser-driving + the film (it will 404 `/sequences`). Test instances only ever ran on 8097/8099. +- @Lane B (whenever it's unlocked) — the UI contract is ready and stable: + `import { renderSequence, listSequences, getSequence, saveSequence, deleteSequence } + from './web/film.js'`. A sequence editor is a list of `{scene,in,out,transition, + transitionDur}` rows over `GET /scenes`; `renderSequence(stage, timeline, name, + {width, height, onShot, onProgress})` does the rest and resolves with the mp4 URL. + NOTE it calls `timeline.applyScene()` per shot, so it CLOBBERS the loaded scene — + the UI should confirm via `timeline.hasContent()` first, same as new-from-template. +NEXT: nothing outstanding in Lane C. Backlog: per-scene audio carried into a film +(auto-derive the bed from each shot's scene `audio[]` offset into film time — needs a +silence-input path for shots with no audio); more transition types (xfade has ~50, +`wipeleft`/`slideup` are one vocab entry each); still-unverified MediaRecorder draft +path (carried from session 3). diff --git a/scenegod-8020.log b/scenegod-8020.log index ac31200..d04aa11 100644 --- a/scenegod-8020.log +++ b/scenegod-8020.log @@ -764,3 +764,826 @@ INFO: 127.0.0.1:53066 - "GET /web/grammar.js HTTP/1.1" 200 OK INFO: 127.0.0.1:53066 - "GET /director HTTP/1.1" 405 Method Not Allowed INFO: 127.0.0.1:53067 - "GET /web/timeline.js HTTP/1.1" 200 OK INFO: 127.0.0.1:53067 - "GET /web/tlui.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:57474 - "GET /scenes HTTP/1.1" 200 OK +INFO: Shutting down +INFO: Waiting for application shutdown. +INFO: Application shutdown complete. +INFO: Finished server process [88415] +INFO: Started server process [99875] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8020 (Press CTRL+C to quit) +INFO: 127.0.0.1:58556 - "GET /sequences HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET /web/style.css HTTP/1.1" 200 OK +INFO: 127.0.0.1:58628 - "GET /web/dock.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58627 - "GET /web/stage.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET /web/room3d.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58627 - "GET /web/presets.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET /assets/tree HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET /favicon.ico HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:58626 - "GET /web/grammar.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58626 - "GET /director HTTP/1.1" 405 Method Not Allowed +INFO: 127.0.0.1:58627 - "GET /web/timeline.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58627 - "GET /web/tlui.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /web/film.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /web/render.js HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /sequences/demo-two-shot/render HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /scenes/sync2-sunset HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/105 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/106 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/107 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/108 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/109 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/110 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/111 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/112 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/113 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/114 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/115 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/116 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/117 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/118 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/119 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/120 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/121 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/122 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/123 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/124 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/125 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/126 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/127 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/128 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/129 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/130 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/131 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/132 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/133 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/frame/134 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/19ede17adda04629880e89d392f4aede/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /render/19ede17adda04629880e89d392f4aede/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /films/227a5ce43dd1480fa2ef39756e90ad15/shot/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /scenes/sync2-sunset HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/c3af5d4d2c5048ce96ac7b457ebad21d/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /render/c3af5d4d2c5048ce96ac7b457ebad21d/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /films/227a5ce43dd1480fa2ef39756e90ad15/shot/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /scenes/sync1-demo HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/105 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/106 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/107 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/108 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/109 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/110 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/111 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/112 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/113 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/114 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/115 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/116 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/117 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/118 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/frame/119 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /render/6e12a264f49547b28949260c46774f3d/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /render/6e12a264f49547b28949260c46774f3d/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /films/227a5ce43dd1480fa2ef39756e90ad15/shot/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "POST /films/227a5ce43dd1480fa2ef39756e90ad15/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:58700 - "GET /films/227a5ce43dd1480fa2ef39756e90ad15/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /templates/two-hander-golden-hour HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /scenes/shot-street HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /templates/record-store HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /scenes/shot-store HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /sequences/milk-bar-film HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /sequences/milk-bar-film/render HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /scenes/shot-street HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /assets/thumb?path=backdrops%2Fvideo%2Fscenegod_plates%2Fsuburb_strip_goldenhour.mp4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/105 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/106 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/107 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/108 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/109 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/110 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/111 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/112 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/113 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/114 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/115 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/116 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/117 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/118 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/119 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/120 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/121 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/122 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/123 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/124 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/125 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/126 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/127 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/128 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/129 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/130 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/131 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/132 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/133 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/frame/134 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/0d7b1ade2b5e45f6883356d22b041e62/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /render/0d7b1ade2b5e45f6883356d22b041e62/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /films/67b76370c643433ba4ab9f569cea82f4/shot/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /scenes/shot-street HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /render/d080521d3963440299fc6de1dfcec07f/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /render/d080521d3963440299fc6de1dfcec07f/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "POST /films/67b76370c643433ba4ab9f569cea82f4/shot/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /scenes/shot-store HTTP/1.1" 200 OK +INFO: 127.0.0.1:60401 - "GET /assets/file?path=backdrops%2Fvideo%2Fscenegod_plates%2Frecord_store_interior.mp4 HTTP/1.1" 206 Partial Content +INFO: 127.0.0.1:61531 - "GET /assets/file?path=backdrops%2Fvideo%2Fscenegod_plates%2Frecord_store_interior.mp4 HTTP/1.1" 206 Partial Content +INFO: 127.0.0.1:61531 - "POST /render/begin HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "GET /assets/file?path=backdrops%2Fvideo%2Fscenegod_plates%2Frecord_store_interior.mp4 HTTP/1.1" 206 Partial Content +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/1 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/3 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/4 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/5 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/6 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/8 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/9 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/12 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/15 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/16 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/17 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/18 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/19 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/20 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/21 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/22 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/23 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/24 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/25 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/26 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/27 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/28 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/29 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/30 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/31 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/32 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/33 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/34 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/35 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/36 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/37 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/38 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/39 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/40 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/41 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/42 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/43 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/44 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/45 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/46 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/47 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/48 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/49 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/51 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/52 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/53 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/54 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/55 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/56 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/57 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/58 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/59 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/61 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/62 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/63 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/64 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/65 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/66 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/67 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/68 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/69 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/70 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/71 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/72 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/73 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/74 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/75 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/76 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/77 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/78 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/79 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/80 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/81 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/82 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/83 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/84 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/85 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/86 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/87 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/88 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/89 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/90 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/91 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/92 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/93 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/94 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/95 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/96 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/97 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/98 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/99 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/100 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/101 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/102 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/103 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/104 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/105 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/106 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/107 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/108 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/109 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/110 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/111 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/112 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/113 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/114 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/115 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/116 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/117 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/118 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/119 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/120 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/121 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/122 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/123 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/124 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/125 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/126 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/127 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/128 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/129 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/130 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/131 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/132 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/133 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/134 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/135 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/136 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/137 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/138 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/139 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/140 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/141 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/142 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/143 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/144 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/145 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/146 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/147 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/148 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/frame/149 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /render/4c29712db270417a95697e00c73712e3/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "GET /render/4c29712db270417a95697e00c73712e3/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /films/67b76370c643433ba4ab9f569cea82f4/shot/2 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "POST /films/67b76370c643433ba4ab9f569cea82f4/end HTTP/1.1" 200 OK +INFO: 127.0.0.1:61531 - "GET /films/67b76370c643433ba4ab9f569cea82f4/status HTTP/1.1" 200 OK diff --git a/scenegod/server.py b/scenegod/server.py index 78d413c..e83109d 100644 --- a/scenegod/server.py +++ b/scenegod/server.py @@ -29,6 +29,8 @@ ASSETS = Path(os.environ.get("SCENEGOD_ASSETS", ROOT / "assets")).resolve() SCENES = Path(os.environ.get("SCENEGOD_SCENES", ROOT / "scenes")).resolve() RENDERS = Path(os.environ.get("SCENEGOD_RENDERS", ROOT / "renders")).resolve() TEMPLATES = Path(os.environ.get("SCENEGOD_TEMPLATES", ROOT / "templates")).resolve() +SEQUENCES = Path(os.environ.get("SCENEGOD_SEQUENCES", ROOT / "sequences")).resolve() +FILMS = Path(os.environ.get("SCENEGOD_FILMS", ROOT / "films")).resolve() MODEL_EXTS = {"glb", "gltf", "fbx", "obj", "bvh"} IMG_EXTS = {"jpg", "jpeg", "png", "webp"} @@ -47,6 +49,8 @@ MEDIA_TYPES = { SCENES.mkdir(parents=True, exist_ok=True) RENDERS.mkdir(parents=True, exist_ok=True) +SEQUENCES.mkdir(parents=True, exist_ok=True) +FILMS.mkdir(parents=True, exist_ok=True) HAVE_FFMPEG = shutil.which("ffmpeg") is not None HAVE_RHUBARB = shutil.which("rhubarb") is not None @@ -68,10 +72,10 @@ def _mb_token() -> str: raise HTTPException(503, f"no MB token (set MB_TOKEN or provide {MB_ENV})") -def reap_renders(max_age_s=48 * 3600): - """Delete render dirs older than max_age_s. Called at boot (C5).""" +def reap_dirs(root: Path, max_age_s=48 * 3600): + """Delete job dirs under root older than max_age_s. Called at boot (C5/M8).""" now = time.time() - for d in RENDERS.iterdir() if RENDERS.is_dir() else []: + for d in root.iterdir() if root.is_dir() else []: try: if d.is_dir() and now - d.stat().st_mtime > max_age_s: shutil.rmtree(d, ignore_errors=True) @@ -79,7 +83,8 @@ def reap_renders(max_age_s=48 * 3600): pass -reap_renders() +reap_dirs(RENDERS) +reap_dirs(FILMS) app = FastAPI(title="SCENEGOD") @@ -315,8 +320,10 @@ def _encode(rid: str, fps: int, audio: list[dict]): if audio: # delay each track to its start, apply gain, mix down to one stereo stream parts = [f"[{i}:a]adelay={int(a['start']*1000)}:all=1,volume={a['gain']}[a{i}]" for i, a in enumerate(audio, start=1)] + # apad + -shortest: the VIDEO decides the length. Without the pad, audio + # shorter than the shot truncates the picture (silent footage loss). parts.append("".join(f"[a{i}]" for i in range(1, len(audio) + 1)) - + f"amix=inputs={len(audio)}:normalize=0[aout]") + + f"amix=inputs={len(audio)}:normalize=0,apad[aout]") cmd += ["-filter_complex", ";".join(parts), "-map", "0:v", "-map", "[aout]", "-c:a", "aac", "-shortest"] cmd += [str(d / "out.mp4")] @@ -408,6 +415,388 @@ def render_out(rid: str): return FileResponse(out, media_type="video/mp4") +# ---- sequences: multi-shot films (M8-C) ---- +# A scene is ONE take. A sequence is the film-level object: an ordered list of +# shots, each a trimmed window [in,out) of a saved scene, joined by cuts or +# dissolves, over an optional film-level audio bed. +# +# The server CANNOT render a shot — three.js lives in the browser. So a film is a +# client-driven job the server orchestrates: POST /sequences/{n}/render hands back +# the resolved shot plan, the client renders each shot through the ORDINARY +# /render/* session endpoints and reports which renderId belongs to which shot +# index, and /films/{id}/end concatenates those mp4s with ffmpeg. See web/film.js. +SEQ_NAME = re.compile(r"[a-z0-9_-]{1,64}\Z") # same alphabet slugify() emits +TRANSITIONS = {"cut", "dissolve"} +MAX_SHOTS = 200 +MAX_TRANSITION_S = 10.0 + + +def _num(v, default=None): + """Strict number coercion: None -> default, wrong type -> None (a violation).""" + if v is None: + return default + if isinstance(v, bool) or not isinstance(v, (int, float)): + return None + return float(v) + + +def plan_sequence(seq: dict) -> tuple[list[str], dict]: + """Validate a sequence AND resolve it to a concrete shot plan in one pass, so + save-time validation and render-time planning can never disagree about what a + sequence means. Returns (violations, plan); plan is meaningful only when the + violation list is empty.""" + errs: list[str] = [] + warnings: list[str] = [] + plan = {"fps": 30, "shots": [], "audio": [], "duration": 0.0, "warnings": warnings} + if seq.get("version") != 1: + errs.append(f"version must be 1, got {seq.get('version')!r}") + shots = seq.get("shots") + if not isinstance(shots, list) or not shots: + errs.append("shots must be a non-empty list") + shots = [] + if len(shots) > MAX_SHOTS: + errs.append(f"too many shots ({len(shots)} > {MAX_SHOTS})") + shots = shots[:MAX_SHOTS] + + fps_seen: dict[int, str] = {} + has_bed = bool(seq.get("audio")) + for i, sh in enumerate(shots): + if not isinstance(sh, dict): + errs.append(f"shot[{i}]: must be an object") + continue + nm = sh.get("scene") + if not isinstance(nm, str) or not SEQ_NAME.match(nm): + errs.append(f"shot[{i}]: bad scene name {nm!r}") + continue + p = SCENES / (nm + ".json") # name is [a-z0-9_-] only: no traversal possible + if not p.is_file(): + errs.append(f"shot[{i}]: unknown scene {nm!r}") + continue + try: + sc = json.loads(p.read_text()) + except (OSError, json.JSONDecodeError) as e: + errs.append(f"shot[{i}]: scene {nm!r} unreadable ({e.__class__.__name__})") + continue + sdur = _num(sc.get("duration"), 0.0) or 0.0 + fps_seen.setdefault(int(_num(sc.get("fps"), 30) or 30), nm) + if (sc.get("audio") or []) and not has_bed: + warnings.append(f"scene {nm!r} has audio[] — shot audio is NOT carried into the " + f"film; put the soundtrack in the sequence's own audio[]") + + t_in, t_out = _num(sh.get("in"), 0.0), _num(sh.get("out"), sdur) + if t_in is None or t_out is None: + errs.append(f"shot[{i}]: in/out must be numbers") + continue + if t_in < 0: + errs.append(f"shot[{i}]: in must be >= 0, got {t_in}") + if t_out <= t_in: + errs.append(f"shot[{i}]: in ({t_in}) must be less than out ({t_out})") + if t_out > sdur + 1e-6: + errs.append(f"shot[{i}]: out ({t_out}) is past the end of scene {nm!r} ({sdur})") + + tr, td = sh.get("transition", "cut"), _num(sh.get("transitionDur"), 0.5) + if tr not in TRANSITIONS: + errs.append(f"shot[{i}]: unknown transition {tr!r} (one of {sorted(TRANSITIONS)})") + elif td is None or td < 0 or td > MAX_TRANSITION_S: + errs.append(f"shot[{i}]: transitionDur must be 0..{MAX_TRANSITION_S}s, " + f"got {sh.get('transitionDur')!r}") + elif tr == "dissolve" and td <= 0: + errs.append(f"shot[{i}]: a dissolve needs transitionDur > 0") + plan["shots"].append({"index": i, "scene": nm, "in": t_in, "out": t_out, + "len": max(0.0, t_out - t_in), + "transition": tr if tr in TRANSITIONS else "cut", + "transitionDur": (td or 0.0) if tr == "dissolve" else 0.0}) + + # a dissolve eats the tail of this shot and the head of the next: ffmpeg xfade + # needs it shorter than BOTH, or the fold produces garbage instead of a film. + ps = plan["shots"] + for k, s in enumerate(ps): + if s["transition"] != "dissolve" or k + 1 >= len(ps): + continue + nxt = ps[k + 1] + if s["transitionDur"] >= min(s["len"], nxt["len"]) - 1e-6: + errs.append(f"shot[{s['index']}]: dissolve {s['transitionDur']}s must be shorter than " + f"both shots it joins ({s['len']:.3f}s, {nxt['len']:.3f}s)") + if len(fps_seen) > 1: + errs.append("shots mix scene fps (" + + ", ".join(f"{f} in {n!r}" for f, n in sorted(fps_seen.items())) + + ") — one film, one fps") + if fps_seen: + plan["fps"] = min(fps_seen) + + aud = seq.get("audio") or [] + if not isinstance(aud, list): + errs.append("audio must be a list") + aud = [] + for k, a in enumerate(aud): + if not isinstance(a, dict) or not isinstance(a.get("path"), str): + errs.append(f"audio[{k}]: needs a string path") + continue + try: + f = safe_under(ASSETS, a["path"]) # path-guard every audio input + except HTTPException: + errs.append(f"audio[{k}]: bad path {a['path']!r}") + continue + if not f.is_file(): + errs.append(f"audio[{k}]: not found under assets: {a['path']!r}") + continue + start, gain = _num(a.get("start"), 0.0), _num(a.get("gain"), 1.0) + if start is None or gain is None or start < 0: + errs.append(f"audio[{k}]: start/gain must be numbers (start >= 0)") + continue + plan["audio"].append({"path": a["path"], "file": str(f), "start": start, "gain": gain}) + + plan["duration"] = round(sum(s["len"] for s in ps) - sum(s["transitionDur"] for s in ps[:-1]), 6) + plan["warnings"] = list(dict.fromkeys(warnings)) # dedupe, keep order + return errs, plan + + +def validate_sequence(seq: dict) -> list[str]: + return plan_sequence(seq)[0] + + +def _seq_path(name: str) -> Path: + return safe_under(SEQUENCES, slugify(name) + ".json") + + +@app.get("/sequences") +def sequences_list(): + out = [] + for p in sorted(SEQUENCES.glob("*.json")): + try: + seq = json.loads(p.read_text()) + except (OSError, json.JSONDecodeError): + continue + errs, plan = plan_sequence(seq) + out.append({"name": p.stem, "title": seq.get("title") or p.stem, + "mtime": int(p.stat().st_mtime), "shots": len(seq.get("shots") or []), + "duration": None if errs else plan["duration"], "ok": not errs}) + return out + + +@app.get("/sequences/{name}") +def sequence_get(name: str): + p = _seq_path(name) + if not p.is_file(): + raise HTTPException(404, "not found") + return json.loads(p.read_text()) + + +@app.post("/sequences/{name}") +async def sequence_save(name: str, request: Request): + body = await capped_body(request, 1024 * 1024) + try: + seq = json.loads(body) + except json.JSONDecodeError as e: + raise HTTPException(400, f"invalid JSON: {e}") + errs, plan = plan_sequence(seq) + if errs: + return JSONResponse({"errors": errs}, status_code=422) + p = _seq_path(name) + tmp = p.with_suffix(".json.tmp") + tmp.write_text(json.dumps(seq, indent=2)) + tmp.replace(p) # atomic + return {"ok": True, "name": p.stem, "duration": plan["duration"], + "shots": len(plan["shots"]), "warnings": plan["warnings"]} + + +@app.delete("/sequences/{name}") +def sequence_delete(name: str): + p = _seq_path(name) + if not p.is_file(): + raise HTTPException(404, "not found") + p.unlink() + return {"ok": True, "name": p.stem} + + +# ---- film render jobs (M8-C) ---- +_films: dict[str, dict] = {} # fid -> {state: queued|encoding|done|error, log} (encode only) + + +def _film_dir(fid: str) -> Path: + return safe_under(FILMS, fid) + + +def _film_meta(fid: str) -> dict: + """Plan + shot registrations live on disk: a film spans minutes of client + rendering, and /end must still work if the process was restarted underneath.""" + p = _film_dir(fid) / "meta.json" + if not p.is_file(): + raise HTTPException(404, "unknown filmId") + return json.loads(p.read_text()) + + +def _film_write(fid: str, meta: dict): + p = _film_dir(fid) / "meta.json" + tmp = p.with_suffix(".json.tmp") + tmp.write_text(json.dumps(meta)) + tmp.replace(p) + + +def _probe_dur(f: Path) -> float | None: + """Actual mp4 duration — xfade offsets are absolute, so a half-frame of drift + between nominal and encoded length shows up as a stutter at the join.""" + try: + out = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "default=nw=1:nokey=1", str(f)], + capture_output=True, text=True, timeout=30) + return float(out.stdout.strip()) + except (OSError, ValueError, subprocess.SubprocessError): + return None + + +def _concat_film(fid: str): + """Join the shot mp4s in order: cut = concat filter, dissolve = xfade. The + concat FILTER (not the demuxer) because a film may need xfade anyway, and it + re-encodes once instead of twice.""" + meta = json.loads((_film_dir(fid) / "meta.json").read_text()) + _films[fid] = {"state": "encoding", "log": ""} + d, shots = _film_dir(fid), meta["shots"] + w, h, fps = meta["width"], meta["height"], meta["fps"] + srcs = [RENDERS / s["renderId"] / "out.mp4" for s in shots] + missing = [str(f) for f in srcs if not f.is_file()] + if missing: + _films[fid] = {"state": "error", "log": "shot mp4 missing (reaped?): " + ", ".join(missing)} + return + durs = [_probe_dur(f) or s["len"] for f, s in zip(srcs, shots)] + + cmd = ["ffmpeg", "-y"] + [x for f in srcs for x in ("-i", str(f))] + cmd += [x for a in meta["audio"] for x in ("-i", a["file"])] + # normalise every shot to the film's grid — mismatches are rejected at + # registration, this is the belt to that braces (SAR/fps oddities included). + parts = [f"[{i}:v]scale={w}:{h},setsar=1,fps={fps},format=yuv420p[v{i}]" for i in range(len(srcs))] + cur, acc = "v0", durs[0] + for i in range(1, len(srcs)): + prev = shots[i - 1] + td = prev["transitionDur"] if prev["transition"] == "dissolve" else 0.0 + if td > 0: + parts.append(f"[{cur}][v{i}]xfade=transition=fade:duration={td}" + f":offset={max(0.0, acc - td):.6f}[x{i}]") + acc += durs[i] - td + else: + parts.append(f"[{cur}][v{i}]concat=n=2:v=1:a=0[x{i}]") + acc += durs[i] + cur = f"x{i}" + for k, a in enumerate(meta["audio"]): + parts.append(f"[{len(srcs)+k}:a]adelay={int(a['start']*1000)}:all=1,volume={a['gain']}[a{k}]") + if meta["audio"]: + parts.append("".join(f"[a{k}]" for k in range(len(meta["audio"]))) + + f"amix=inputs={len(meta['audio'])}:normalize=0,apad[aout]") + cmd += ["-filter_complex", ";".join(parts), "-map", f"[{cur}]"] + if meta["audio"]: + cmd += ["-map", "[aout]", "-c:a", "aac", "-shortest"] # apad+shortest: video sets the length + cmd += ["-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", str(d / "out.mp4")] + try: + p = subprocess.run(cmd, capture_output=True, text=True) + ok = p.returncode == 0 and (d / "out.mp4").is_file() + _films[fid] = {"state": "done" if ok else "error", "log": p.stderr[-2000:]} + except Exception as e: # noqa: BLE001 — surface any spawn failure as error state + _films[fid] = {"state": "error", "log": str(e)} + meta["state"] = _films[fid]["state"] + _film_write(fid, meta) + + +@app.post("/sequences/{name}/render") +async def film_begin(name: str, request: Request): + """Start a film job. Returns the ordered shot list the CLIENT must render + (web/film.js) — one /render/* session per shot, reported back per index.""" + if not HAVE_FFMPEG: + raise HTTPException(503, "ffmpeg not found on server") + p = _seq_path(name) + if not p.is_file(): + raise HTTPException(404, "not found") + errs, plan = plan_sequence(json.loads(p.read_text())) + if errs: + return JSONResponse({"errors": errs}, status_code=422) # a scene may have vanished since save + try: + body = json.loads(await capped_body(request, 64 * 1024) or b"{}") + except json.JSONDecodeError: + body = {} + try: + w, h = int(body.get("width", 1920)), int(body.get("height", 1080)) + except (TypeError, ValueError): + raise HTTPException(400, "width/height must be integers") + if not (16 <= w <= 4096 and 16 <= h <= 4096): + raise HTTPException(400, "width/height out of range") + fid = uuid.uuid4().hex + (FILMS / fid).mkdir(parents=True) + meta = {"sequence": p.stem, "fps": plan["fps"], "width": w, "height": h, + "audio": plan["audio"], "state": "collecting", "created": time.time(), + "shots": [dict(s, renderId=None) for s in plan["shots"]]} + _film_write(fid, meta) + return {"filmId": fid, "fps": plan["fps"], "width": w, "height": h, + "duration": plan["duration"], "warnings": plan["warnings"], + "shots": [{k: s[k] for k in ("index", "scene", "in", "out", "len", + "transition", "transitionDur")} for s in plan["shots"]]} + + +@app.post("/films/{fid}/shot/{i}") +async def film_shot(fid: str, i: int, request: Request): + """The client says 'renderId R is shot i'. The render must be finished and cut + to the film's grid — one resolution and one fps per film, checked here so the + failure is a clear 400 and not a cryptic ffmpeg filter error 10 minutes later.""" + meta = _film_meta(fid) + if not (0 <= i < len(meta["shots"])): + raise HTTPException(400, f"shot index {i} out of range (0..{len(meta['shots'])-1})") + try: + rid = str(json.loads(await capped_body(request, 8 * 1024)).get("renderId") or "") + except json.JSONDecodeError: + raise HTTPException(400, "invalid JSON") + if not re.fullmatch(r"[0-9a-f]{8,64}", rid): + raise HTTPException(400, "bad renderId") + rd = safe_under(RENDERS, rid) + if not (rd / "out.mp4").is_file(): + raise HTTPException(400, f"render {rid} has no out.mp4 yet (encode it before registering)") + try: + rm = json.loads((rd / "meta.json").read_text()) + rw, rh, rf = int(rm.get("width", 0)), int(rm.get("height", 0)), int(rm.get("fps", 0)) + except (OSError, json.JSONDecodeError, TypeError, ValueError): + rw = rh = rf = 0 # no meta (restart/manual) — trust the pixels + if rw and rh and (rw, rh) != (meta["width"], meta["height"]): + raise HTTPException(400, f"shot {i} rendered {rw}x{rh}, film is " + f"{meta['width']}x{meta['height']} — one resolution per film") + if rf and rf != meta["fps"]: + raise HTTPException(400, f"shot {i} rendered at {rf}fps, film is {meta['fps']}fps") + meta["shots"][i]["renderId"] = rid + _film_write(fid, meta) + done = sum(1 for s in meta["shots"] if s.get("renderId")) + return {"ok": True, "shot": i, "of": len(meta["shots"]), "registered": done} + + +@app.post("/films/{fid}/end") +async def film_end(fid: str, request: Request): + await capped_body(request, 8 * 1024) # no payload today; cap it anyway (ship-check) + meta = _film_meta(fid) + missing = [s["index"] for s in meta["shots"] if not s.get("renderId")] + if missing: + raise HTTPException(400, f"shots not registered yet: {missing}") + meta["state"] = "queued" + _film_write(fid, meta) + _films[fid] = {"state": "queued", "log": ""} + threading.Thread(target=_concat_film, args=(fid,), daemon=True).start() + return {"ok": True, "shots": len(meta["shots"])} + + +@app.get("/films/{fid}/status") +def film_status(fid: str): + meta = _film_meta(fid) + n = len(meta["shots"]) + done = sum(1 for s in meta["shots"] if s.get("renderId")) + st = _films.get(fid) + if not st: # fell out of memory (restart) — infer from disk + st = {"state": "done" if (_film_dir(fid) / "out.mp4").is_file() + else meta.get("state", "collecting"), "log": ""} + return {"state": st["state"], "shot": done, "of": n, "log": st["log"]} + + +@app.get("/films/{fid}/out.mp4") +def film_out(fid: str): + out = _film_dir(fid) / "out.mp4" + if not out.is_file(): + raise HTTPException(404, "not ready") + return FileResponse(out, media_type="video/mp4") + + # ---- viseme lip-sync (M4) — feeds Lane A's viseme lane ---- @app.get("/rhubarb") def rhubarb(path: str): diff --git a/scenegod/web/film.js b/scenegod/web/film.js new file mode 100644 index 0000000..1267431 --- /dev/null +++ b/scenegod/web/film.js @@ -0,0 +1,68 @@ +// film.js — multi-shot sequencing client (Lane C, M8-C). +// +// renderSequence(stage, timeline, seqName, opts) -> Promise +// +// A scene is one continuous take; a SEQUENCE is the film — an ordered list of +// shots, each a trimmed window of a saved scene, joined by cuts or dissolves. +// Only the browser can render a shot (three.js lives here), so the server +// orchestrates and this file does the work: +// +// POST sequences/{name}/render -> {filmId, fps, width, height, shots[]} +// per shot: GET scenes/{scene} -> timeline.applyScene -> render frames +// [in*fps, out*fps) through the ORDINARY render session endpoints +// -> POST films/{id}/shot/{i} {renderId} +// POST films/{id}/end -> ffmpeg concat/xfade -> poll -> films/{id}/out.mp4 +// +// Shots are rendered SILENT on purpose: a film's soundtrack is the sequence's +// own audio[] (mixed server-side over the finished cut), not per-scene audio — +// the server warns when a referenced scene has audio[] that will be skipped. +// All URLs relative (the app serves under /scenegod/ in prod). +import { withRenderSize, beginRender, captureFrames, endRender, pollRender, + postJSON, getJSON } from './render.js'; + +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +export async function renderSequence(stage, timeline, seqName, opts = {}) { + const { width = 1920, height = 1080, onProgress, onShot, onWarning } = opts; + const film = await postJSON(`sequences/${encodeURIComponent(seqName)}/render`, { width, height }); + const { filmId, fps, shots } = film; + for (const w of film.warnings || []) (onWarning || (m => console.warn('[film]', m)))(w); + if (!shots?.length) throw new Error('sequence has no shots'); + + // one canvas resize for the whole film — shots must all be the same size, the + // server rejects a mismatched shot at registration anyway. + await withRenderSize(stage, width, height, async () => { + for (const shot of shots) { + const i = shot.index; + onShot?.(i, shots.length, shot); + const scene = await getJSON(`scenes/${encodeURIComponent(shot.scene)}`); + await timeline.applyScene(scene); // stage.applyState + tracks + clip preload (Lane B) + const from = Math.round(shot.in * fps), to = Math.round(shot.out * fps); + const { renderId } = await beginRender({ name: `${seqName}-${i}`, fps, width, height }); + await captureFrames(stage, timeline, { renderId, from, to, + onProgress: (done, total) => onProgress?.({ shot: i, of: shots.length, scene: shot.scene, + frame: done, frames: total }) }); + await endRender(renderId); // silent: the bed is film-level + await pollRender(renderId); // shot mp4 must exist before we register it + await postJSON(`films/${filmId}/shot/${i}`, { renderId }); + } + }); + + await postJSON(`films/${filmId}/end`, {}); + let st; + do { await sleep(500); st = await getJSON(`films/${filmId}/status`); } + while (st.state === 'collecting' || st.state === 'queued' || st.state === 'encoding'); + if (st.state !== 'done') throw new Error('film encode failed: ' + (st.log || st.state)); + return `films/${filmId}/out.mp4`; +} + +// convenience: the sequence list/read/save/delete surface, so a UI lane never has +// to hand-roll the URLs (and never gets them absolute). +export const listSequences = () => getJSON('sequences'); +export const getSequence = name => getJSON(`sequences/${encodeURIComponent(name)}`); +export const saveSequence = (name, seq) => postJSON(`sequences/${encodeURIComponent(name)}`, seq); +export async function deleteSequence(name) { + const r = await fetch(`sequences/${encodeURIComponent(name)}`, { method: 'DELETE' }); + if (!r.ok) throw new Error(`delete ${name}: ${r.status}`); + return r.json(); +} diff --git a/scenegod/web/render.js b/scenegod/web/render.js index dba2a89..2783ccb 100644 --- a/scenegod/web/render.js +++ b/scenegod/web/render.js @@ -1,8 +1,13 @@ // render.js — draft + final video capture (Lane C, PLAN §4.3 / C-server §C5). // Talks to Stage/Timeline through their public APIs only. No three.js import. // -// draftRecord(stage, timeline) -> Promise quick webm via MediaRecorder -// finalRender(stage, timeline, opts) -> Promise deterministic PNG->ffmpeg mp4 +// draftRecord(stage, timeline) -> Promise quick webm via MediaRecorder +// finalRender(stage, timeline, opts) -> Promise deterministic PNG->ffmpeg mp4 +// +// The pieces finalRender is built from are exported too — web/film.js drives the +// same loop once per shot of a multi-shot sequence (M8-C), so the frame-post path +// exists exactly once. +// All URLs are RELATIVE: the app serves at / locally and /scenegod/ in prod. const sleep = ms => new Promise(r => setTimeout(r, ms)); @@ -24,45 +29,72 @@ export function draftRecord(stage, timeline, { fps = 30, mimeType = 'video/webm' }); } -// --- final: step frame-by-frame (deterministic), upload PNGs, ffmpeg encode --- -export async function finalRender(stage, timeline, opts = {}) { - const { fps = 30, width = 1920, height = 1080, name = 'scene', audio = [], onProgress } = opts; +// --- render-session primitives (shared with film.js) ------------------------- + +// Render at exactly width x height, then put the viewport back however fn ends. +export async function withRenderSize(stage, width, height, fn) { const canvas = stage.renderer.domElement; const prev = { w: canvas.width, h: canvas.height, dpr: stage.renderer.getPixelRatio() }; stage.renderer.setPixelRatio(1); stage.renderer.setSize(width, height, false); // false = don't touch CSS size - - try { - const total = Math.max(1, Math.round(timeline.duration * fps)); - const { renderId } = await postJSON('render/begin', { name, fps, width, height }); - - const inflight = new Set(); - for (let f = 0; f < total; f++) { - timeline.step(f); // seek(f/fps)+evaluate — never wall-clock - if (stage.syncVideos) await stage.syncVideos(timeline.time); // M6 video plates: seek + wait 'seeked'; no-op until Lane A lands it - stage.renderActiveCamera(null); // draw active cam (or director cam) to main canvas - const blob = await new Promise(res => canvas.toBlob(res, 'image/png')); - while (inflight.size >= 4) await Promise.race(inflight); // backpressure: <=4 in flight - const p = fetch(`render/${renderId}/frame/${f}`, { method: 'POST', body: blob }) - .then(r => { if (!r.ok) throw new Error(`frame ${f}: ${r.status}`); }) - .finally(() => inflight.delete(p)); - inflight.add(p); - onProgress?.(f + 1, total); - } - await Promise.all(inflight); - - await postJSON(`render/${renderId}/end`, { audio }); // scene audio[] muxed by ffmpeg - let st; - do { await sleep(500); st = await (await fetch(`render/${renderId}/status`)).json(); } - while (st.state === 'queued' || st.state === 'encoding'); - if (st.state !== 'done') throw new Error('encode failed: ' + (st.log || st.state)); - return `render/${renderId}/out.mp4`; - } finally { + try { return await fn(); } + finally { stage.renderer.setPixelRatio(prev.dpr); stage.renderer.setSize(prev.w, prev.h, false); } } +export async function beginRender({ name = 'scene', fps = 30, width = 1920, height = 1080 } = {}) { + return postJSON('render/begin', { name, fps, width, height }); +} + +// Step [from, to) on the timeline and post those frames as 0..n-1 of renderId. +// Deterministic: timeline.step() is seek+evaluate, never wall-clock. +export async function captureFrames(stage, timeline, { renderId, from = 0, to, onProgress } = {}) { + const canvas = stage.renderer.domElement; + const total = Math.max(0, to - from); + const inflight = new Set(); + for (let f = from; f < to; f++) { + timeline.step(f); + if (stage.syncVideos) await stage.syncVideos(timeline.time); // M6 video plates: seek + wait 'seeked' + stage.renderActiveCamera(null); // draw active cam (or director cam) to main canvas + const blob = await new Promise(res => canvas.toBlob(res, 'image/png')); + while (inflight.size >= 4) await Promise.race(inflight); // backpressure: <=4 in flight + const n = f - from; + const p = fetch(`render/${renderId}/frame/${n}`, { method: 'POST', body: blob }) + .then(r => { if (!r.ok) throw new Error(`frame ${n}: ${r.status}`); }) + .finally(() => inflight.delete(p)); + inflight.add(p); + onProgress?.(n + 1, total); + } + await Promise.all(inflight); + return total; +} + +export async function endRender(renderId, audio = []) { + return postJSON(`render/${renderId}/end`, { audio }); // scene audio[] muxed by ffmpeg +} + +export async function pollRender(renderId, every = 500) { + let st; + do { await sleep(every); st = await getJSON(`render/${renderId}/status`); } + while (st.state === 'queued' || st.state === 'encoding'); + if (st.state !== 'done') throw new Error('encode failed: ' + (st.log || st.state)); + return `render/${renderId}/out.mp4`; +} + +// --- final: step frame-by-frame (deterministic), upload PNGs, ffmpeg encode --- +export async function finalRender(stage, timeline, opts = {}) { + const { fps = 30, width = 1920, height = 1080, name = 'scene', audio = [], onProgress } = opts; + return withRenderSize(stage, width, height, async () => { + const total = Math.max(1, Math.round(timeline.duration * fps)); + const { renderId } = await beginRender({ name, fps, width, height }); + await captureFrames(stage, timeline, { renderId, from: 0, to: total, onProgress }); + await endRender(renderId, audio); + return pollRender(renderId); + }); +} + // trigger a browser download of a blob or url (draft webm / final mp4) export function download(blobOrUrl, filename) { const a = document.createElement('a'); @@ -72,9 +104,15 @@ export function download(blobOrUrl, filename) { if (typeof blobOrUrl !== 'string') setTimeout(() => URL.revokeObjectURL(a.href), 1000); } -async function postJSON(url, body) { +export async function postJSON(url, body) { const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + if (!r.ok) throw new Error(`${url}: ${r.status} ${await r.text().catch(() => '')}`.trim()); + return r.json(); +} + +export async function getJSON(url) { + const r = await fetch(url); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json(); } diff --git a/scripts/test_server.py b/scripts/test_server.py index fc4385c..93d368c 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -57,9 +57,10 @@ def main(): (assets / d).mkdir() (assets / "props" / "box.glb").write_bytes(b"b") # no image sidecar - renders = tmp / "renders" + renders, sequences, films = tmp / "renders", tmp / "sequences", tmp / "films" env = {**os.environ, "SCENEGOD_ASSETS": str(assets), "SCENEGOD_SCENES": str(scenes), - "SCENEGOD_RENDERS": str(renders)} + "SCENEGOD_RENDERS": str(renders), "SCENEGOD_SEQUENCES": str(sequences), + "SCENEGOD_FILMS": str(films)} env.pop("SCENEGOD_LLM_URL", None) # main instance must 503 on /director proc = subprocess.Popen( [str(PY), "-m", "uvicorn", "scenegod.server:app", "--port", str(PORT)], @@ -221,6 +222,166 @@ def main(): else: print("(render test skipped — ffmpeg/ffprobe not found)") + # --- sequences (M8-C): CRUD round-trip --- + for nm, dur, fps in (("clip-a", 2.0, 30), ("clip-b", 3.0, 30), ("clip-24", 2.0, 24)): + st, _ = req("POST", "/scenes/" + nm, json.dumps( + {"version": 1, "name": nm, "fps": fps, "duration": dur, "entities": []})) + assert st == 200, (nm, st) + + seq = {"version": 1, "name": "my-film", "title": "My Film", + "shots": [{"scene": "clip-a", "in": 0.0, "out": 2.0, + "transition": "dissolve", "transitionDur": 0.5}, + {"scene": "clip-b", "in": 0.5, "out": 3.0}]} + st, b = req("POST", "/sequences/" + urllib.parse.quote("My Film!"), json.dumps(seq)) + assert st == 200, b + saved = json.loads(b) + assert saved["name"] == "my-film", saved # slugified + assert abs(saved["duration"] - (2.0 + 2.5 - 0.5)) < 1e-6, saved # dissolve eats 0.5s + assert get_json("/sequences/my-film") == seq, "sequence round-trip must be lossless" + row = [s for s in get_json("/sequences") if s["name"] == "my-film"][0] + assert row["title"] == "My Film" and row["shots"] == 2 and row["ok"], row + assert req("DELETE", "/sequences/my-film")[0] == 200 + assert req("GET", "/sequences/my-film")[0] == 404 + assert req("DELETE", "/sequences/my-film")[0] == 404 + n += 1 + + # --- sequence validation: every violation a 422 that says what's wrong --- + def sq(shots, **kw): + return json.dumps({"version": 1, "shots": shots, **kw}) + + bad_seqs = [ + (sq([{"scene": "nope", "in": 0, "out": 1}]), "unknown scene"), + (sq([{"scene": "clip-a", "in": 2.0, "out": 1.0}]), "less than"), + (sq([{"scene": "clip-a", "in": 0, "out": 1, "transition": "wipe"}]), "transition"), + (sq([]), "non-empty"), + (json.dumps({"version": 2, "shots": [{"scene": "clip-a"}]}), "version"), + (sq([{"scene": "clip-a", "in": 0, "out": 9}]), "past the end"), + (sq([{"scene": "clip-a", "transition": "dissolve", "transitionDur": 5}, + {"scene": "clip-b"}]), "shorter than"), + (sq([{"scene": "clip-a", "transition": "dissolve", "transitionDur": 0}]), "needs"), + (sq([{"scene": "clip-a", "transitionDur": 99, "transition": "dissolve"}]), "transitionDur"), + (sq([{"scene": "clip-a"}, {"scene": "clip-24"}]), "one fps"), + (sq([{"scene": "../../etc/hosts"}]), "bad scene name"), + (sq([{"scene": "clip-a"}], audio=[{"path": "../../etc/hosts"}]), "bad path"), + (sq([{"scene": "clip-a"}], audio=[{"path": "audio/nope.wav"}]), "not found"), + ] + for body, needle in bad_seqs: + st, b = req("POST", "/sequences/bad", body) + assert st == 422, f"expected 422 for {body}, got {st}" + errs = json.loads(b)["errors"] + assert any(needle in e for e in errs), (needle, errs) + assert req("POST", "/sequences/bad", "{not json")[0] == 400 + # name guards on read/delete (same discipline as /scenes, /templates) + for bad in ("../../etc/hosts", "..%2f..%2fetc%2fhosts", + urllib.parse.quote("/etc/hosts", safe=""), "nope"): + assert req("GET", "/sequences/" + bad)[0] >= 400, bad + assert req("DELETE", "/sequences/" + bad)[0] >= 400, bad + n += 1 + + # --- FILM smoke: two shots -> one mp4, cut and dissolve (M8-C) --- + if shutil.which("ffmpeg") and shutil.which("ffprobe"): + def fake_shot_render(dur, w=64, h=64, fps=30, color="red"): + """A *finished* /render session minus the browser: lavfi mp4 + meta.""" + rid = os.urandom(16).hex() + d = renders / rid + (d / "frames").mkdir(parents=True) + (d / "meta.json").write_text(json.dumps( + {"name": "shot", "fps": fps, "width": w, "height": h})) + subprocess.run(["ffmpeg", "-y", "-f", "lavfi", + "-i", f"color=c={color}:s={w}x{h}:r={fps}:d={dur}", + "-c:v", "libx264", "-pix_fmt", "yuv420p", str(d / "out.mp4")], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return rid + + def probe(f, entries="format=duration"): + return subprocess.check_output( + ["ffprobe", "-v", "error", "-show_entries", entries, + "-of", "default=nw=1:nokey=1", str(f)], text=True).strip() + + def make_film(seq_name, body): + assert req("POST", "/sequences/" + seq_name, json.dumps(body))[0] == 200, seq_name + st, b = req("POST", f"/sequences/{seq_name}/render", + json.dumps({"width": 64, "height": 64})) + assert st == 200, b + return json.loads(b) + + def finish_film(fid, rids): + for i, rid in enumerate(rids): + st, b = req("POST", f"/films/{fid}/shot/{i}", json.dumps({"renderId": rid})) + assert st == 200, b + assert req("POST", f"/films/{fid}/end", "{}")[0] == 200 + for _ in range(150): # poll up to ~30s + stt = json.loads(req("GET", f"/films/{fid}/status")[1]) + if stt["state"] in ("done", "error"): + break + time.sleep(0.2) + assert stt["state"] == "done", stt + assert stt["shot"] == stt["of"] == len(rids), stt + assert req("GET", f"/films/{fid}/out.mp4")[0] == 200 + return films / fid / "out.mp4" + + shots = [{"scene": "clip-a", "in": 0.0, "out": 2.0}, + {"scene": "clip-b", "in": 0.0, "out": 3.0}] + film = make_film("film-cut", {"version": 1, "shots": shots}) + assert film["fps"] == 30 and film["duration"] == 5.0, film + assert [s["scene"] for s in film["shots"]] == ["clip-a", "clip-b"], film + fid = film["filmId"] + assert json.loads(req("GET", f"/films/{fid}/status")[1]) == { + "state": "collecting", "shot": 0, "of": 2, "log": ""} + assert req("POST", f"/films/{fid}/end", "{}")[0] == 400 # shots not registered yet + rid_a, rid_b = fake_shot_render(2.0), fake_shot_render(3.0, color="blue") + # guards: bad index, bad/unknown renderId, wrong resolution, unknown film + assert req("POST", f"/films/{fid}/shot/9", json.dumps({"renderId": rid_a}))[0] == 400 + assert req("POST", f"/films/{fid}/shot/0", json.dumps({"renderId": "../../etc"}))[0] == 400 + assert req("POST", f"/films/{fid}/shot/0", + json.dumps({"renderId": "deadbeefdeadbeef"}))[0] == 400 # no out.mp4 + wrong = fake_shot_render(1.0, w=32, h=32) + st, b = req("POST", f"/films/{fid}/shot/0", json.dumps({"renderId": wrong})) + assert st == 400 and b"one resolution per film" in b, b + assert req("GET", "/films/nosuchfilm/status")[0] == 404 + out = finish_film(fid, [rid_a, rid_b]) + dur = float(probe(out)) + assert abs(dur - 5.0) < 0.15, f"cut film {dur}s, expected ~5s (2+3)" + assert probe(out, "stream=width,height").split() == ["64", "64"], probe(out, "stream=width,height") + n += 1 + + # dissolve variant: sum MINUS the transition, plus a film-level audio bed + tone = assets / "audio" / "test" / "tone.wav" + if not tone.is_file(): + tone.parent.mkdir(parents=True, exist_ok=True) + subprocess.run(["ffmpeg", "-y", "-f", "lavfi", + "-i", "sine=frequency=440:duration=1", str(tone)], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + dshots = [dict(shots[0], transition="dissolve", transitionDur=0.5), shots[1]] + film2 = make_film("film-dissolve", {"version": 1, "shots": dshots, + "audio": [{"path": "audio/test/tone.wav", + "start": 0.25, "gain": 0.8}]}) + assert film2["duration"] == 4.5, film2 + assert not film2["warnings"], film2 # bed present -> no warning + out2 = finish_film(film2["filmId"], [fake_shot_render(2.0), fake_shot_render(3.0, color="blue")]) + dur2 = float(probe(out2)) + assert abs(dur2 - 4.5) < 0.15, f"dissolve film {dur2}s, expected ~4.5s (2+3-0.5)" + codecs = probe(out2, "stream=codec_type").split() + assert "audio" in codecs, f"film audio bed not muxed: {codecs}" + n += 1 + else: + print("(film test skipped — ffmpeg/ffprobe not found)") + + # --- shipped example sequence must not rot (same rule as templates) --- + real_seqs, real_scenes = ROOT / "sequences", ROOT / "scenes" + shipped = sorted(real_seqs.glob("*.json")) if real_seqs.is_dir() else [] + assert shipped, "no example sequence shipped in sequences/" + for p in shipped: + js = json.loads(p.read_text()) + assert js["version"] == 1 and js["shots"], p.name + if real_scenes.is_dir(): + for sh in js["shots"]: + sc = real_scenes / (sh["scene"] + ".json") + assert sc.is_file(), f"{p.name}: missing scene {sh['scene']!r}" + dur = json.loads(sc.read_text()).get("duration", 0) + assert sh.get("out", dur) <= dur + 1e-6, f"{p.name}: shot past end of {sh['scene']}" + n += 1 + # --- rhubarb: 503 when the binary is absent (happy path needs rhubarb) --- if not shutil.which("rhubarb"): assert req("GET", "/rhubarb?path=audio/x.wav")[0] == 503 diff --git a/sequences/demo-two-shot.json b/sequences/demo-two-shot.json new file mode 100644 index 0000000..bb3aa84 --- /dev/null +++ b/sequences/demo-two-shot.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "name": "demo-two-shot", + "title": "Demo — three shots, one dissolve", + "shots": [ + { + "scene": "sync2-sunset", + "in": 0.0, + "out": 4.5, + "transition": "dissolve", + "transitionDur": 0.5 + }, + { + "scene": "sync2-sunset", + "in": 4.5, + "out": 8.0, + "transition": "cut" + }, + { + "scene": "sync1-demo", + "in": 0.0, + "out": 4.0, + "transition": "cut" + } + ] +}