[m8-C] multi-shot sequencing — a film is now a first-class object

Sequences (ordered shots = scene + in/out + transition) with full validation,
CRUD, and a client-driven film render the server orchestrates: browser renders
each shot through the existing /render/* endpoints, server ffmpeg-concatenates
in order with cuts or xfade dissolves, muxes a film-level audio bed, reaps
after 48h. render.js refactored so there is one frame-post path in the repo
(captureFrames/withRenderSize/begin/end/poll); new web/film.js drives the loop.
Also fixes a pre-existing mux bug: no apad meant short audio truncated PICTURE.

Orchestrator: deploy compose/script now carry SCENEGOD_SEQUENCES + SCENEGOD_FILMS
volumes so shot lists survive a rebuild.

Verified live: demo-two-shot renders one 11.500s mp4 with a genuinely
compositing dissolve; a film built from the M7 templates (street wide ->
dissolve -> closer -> cut -> record store) renders 11.800s at 960x540.
15 test groups green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 23:21:45 +10:00
parent 7420a4ecdb
commit b773d29776
11 changed files with 1684 additions and 42 deletions

3
.gitignore vendored
View File

@ -3,6 +3,9 @@
assets/
scenes/
renders/
films/
sequences/*
!sequences/demo-two-shot.json
*.log
__pycache__/
*.pyc

View File

@ -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'

View File

@ -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:

View File

@ -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).

View File

@ -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).

View File

@ -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

View File

@ -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):

68
scenegod/web/film.js Normal file
View File

@ -0,0 +1,68 @@
// film.js — multi-shot sequencing client (Lane C, M8-C).
//
// renderSequence(stage, timeline, seqName, opts) -> Promise<url of the film mp4>
//
// 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();
}

View File

@ -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<Blob> quick webm via MediaRecorder
// finalRender(stage, timeline, opts) -> Promise<url> deterministic PNG->ffmpeg mp4
// draftRecord(stage, timeline) -> Promise<Blob> quick webm via MediaRecorder
// finalRender(stage, timeline, opts) -> Promise<url> 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();
}

View File

@ -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

View File

@ -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"
}
]
}