Rendering the EDL gating film

These tools render the supplied v5 scene; they do not introduce a new animation framework. Paths passed to Node/Python tools are relative to this film folder unless absolute. You may run the scripts from another working directory. Node renderers start and close their own localhost server, so a separate server process is optional.

Dependencies

Use Node.js 20 or later, Python 3.10 or later, a recent Chrome/Chromium (Edge is also detected on Windows), and FFmpeg with the libx264 and AAC encoders. puppeteer-core does not download a browser. Install the Node dependencies with the committed lock file:

npm ci --ignore-scripts
python -m venv .venv

Activate the environment (.venv\Scripts\Activate.ps1 in PowerShell, or source .venv/bin/activate in a POSIX shell), then:

python -m pip install -r requirements.txt
ffmpeg -version

The Python requirements specify compatible ranges rather than an exact environment lock. Future library/browser versions can change numeric audio or rasterization results.

Fonts and browser selection

Install Noto Sans CJK SC, Noto Serif CJK SC, DejaVu Serif, DejaVu Sans, and DejaVu Sans Mono before rendering if you want the font families used by the original source. Fonts are not bundled. Missing fonts fall back to system fonts, which can change caption widths and layout. Waiting for document.fonts.ready does not prove that a named system font is installed. Review stills on the target machine before rendering the whole film.

The tools look for common Chrome/Chromium/Edge locations. If necessary set CHROME_PATH to the executable, or append --chrome "/path/to/chrome" to a Node render/timeline command. Windows PowerShell example:

$env:CHROME_PATH = 'C:\Program Files\Google\Chrome\Application\chrome.exe'

Rendering is headless and uses SwiftShader for a predictable software WebGL path; it does not require an interactive browser window. The browser sandbox remains enabled. In an already isolated Linux container that requires it, --no-sandbox is an explicit opt-in.

Full pipeline

Run the following in the film folder. The supplied src/timeline.json already exists; --force explicitly replaces this generated timeline after reading the live scene. The score and encoded outputs refuse overwrite by default.

npm run timeline -- --force
python audio/score.py
npm run plan
npm run render -- --fps 30 --workers 4 --out frames
python tools/encode.py --frames frames --variant medium

The output is output/v5_medium.mp4. Encoding includes a temporary loudness-normalized mix (-16 LUFS target, -1.5 dBTP, stereo 48 kHz) and produces H.264/AAC with fast-start MP4 metadata. It trims the procedural score's one-second tail to the rendered frame duration, without truncating the video to a shorter audio stream. Temporary normalization/pass-log files are cleaned after encoding; the source WAV and frames are preserved.

The frame renderer reads live window.DUR, after the caption reading-time warp, rather than the 102-second story length. In the supplied scene DUR = 146.0507586206897 seconds. At 30 fps the default is 4,382 frames, numbered 0–4,381, ending at 146.0667 seconds after frame quantization. The historical delivered MP4 has 4,383 frames (146.1 seconds); its rounded render end included an extra held-tail frame. The small tail difference is expected. This workflow covers the whole scene and does not claim byte-identical reproduction of the delivered MP4.

render.mjs skips existing frames to resume an interrupted render. Use a fresh frame directory after changing source, fonts, frame rate, scaling or other settings. Use --force only when you intend to replace generated frames. A plan prints timing and frame count without writing images; --report path.json additionally saves the plan.

Short checks before a full render

npm run render -- --stills 10,60,120,145.9 --out output/check-stills
npm run render -- --to 0.2 --workers 1 --out output/check-frames
python tools/encode.py --frames output/check-frames --duration 0.2 --variant medium --out output/check.mp4

This produces six frames and a 0.2-second MP4. It checks rendering/encoding plumbing; it is not full-film visual or audio acceptance. The score must be generated first. To render a segment later in the film, --from and --to use film seconds, preserve global frame numbers, and are limited to the live film duration. Match the encoder's --start-number and --duration so that audio seeks to the same film time:

npm run render -- --from 60 --to 61 --out output/segment-frames
python tools/encode.py --frames output/segment-frames --start-number 1800 --duration 1 --out output/segment.mp4

Quality variants

python tools/encode.py --variant all
Variant Video setting Audio Purpose
master H.264 CRF 16 AAC 256 kb/s High-quality derivative
medium Two-pass budget of about 95 decimal MB AAC 192 kb/s Main downloadable viewing version
viewing Two-pass budget of about 55 decimal MB AAC 192 kb/s Smaller optional viewing version
preview Two-pass budget of about 22.5 decimal MB AAC 128 kb/s Lightweight draft

All variants are 1920×1080. Size budgets apply to a complete film of the timeline duration, include audio, and are approximate; short clips use the corresponding bitrate. Add --output-dir output/alternate to choose a variant folder, or --out path.mp4 for one variant. Add --plan to validate frame coverage and print FFmpeg commands without encoding. A single missing frame fails before encoding. Use --ffmpeg or the FFMPEG environment variable if it is not on PATH. --ext png supports lossless source frames.

The POSIX encode_v5.sh and legacy positional mux.sh are convenience wrappers around the Python encoder. On Windows use encode.py directly. PYTHON may select the interpreter used by the shell wrappers.

Optional diagnostics and temporal sampling

The full 4,382-frame rerender remains a separate acceptance step. Short smoke tests, a complete timing plan, score synthesis and selected late stills establish tool operation, not full-film visual equivalence or proof that a supplied historical soundtrack was generated by the current environment.