Cinematic Science Short Film · Unified Production Standard v1
Sources: "A Brief History of Humankind: The Self-Portrait of the Universe" (4K / 4'58" / 14 Scenes) and "DESCENT" (2K / 1'30" / 90-second vertical profile). The specifications, workflows, style, and pitfalls encountered for both films are all gathered here. Before starting a new film, first read Sections 0 and 1, then read the section relevant to the current project.
0. Hard Rules (Violating any will require a redo)
- Audio first, visuals second. Generate voiceover sentence by sentence → measure the duration → derive frame counts for each scene from the duration. Never lay out visuals before matching the audio.
- Voiceover and data must be consistent: Numbers, units, and magnitudes appearing in the film must be cross-referenced against a fact sheet line by line; if the voiceover says 98, the visuals cannot show 90. Unit errors (e.g., pressure atm ↔ t/m²) are ten times more serious than color mismatches.
- One shot, one piece of information. Each scene allows only one main thesis + a maximum of 3 readings; any additional information goes to the voiceover.
- Text must always be readable. Any text must have a background or darkened overlay underneath; layer names ≥ 30px, readings ≥ 13px. Small text opacity must not be less than 0.6.
- No hard cuts. Use dissolves / black-frame breathing / continuous motion between scenes; one film must use only one transition language.
- Silent rendering. In Remotion, always turn off
<Audio>with{false && ...}. Audio is mixed in externally via ffmpeg (long WAV files can stall rendering at audio preprocessing). - Render only after
npx tsc --noEmitpasses cleanly, otherwise you will render a lot of bad frames and have to redo them. - Three checks before delivery: full length verification, first and last frame verification, and frame-by-frame verification of key transitions (3–6 frames each).
1. Standard Pipeline (In order, no skipping)
script.json ──► VO Generation ──► VO Post-processing ──► Duration Table ──► Timeline
│
└──► Music (numpy)──► Mixing ──► master.m4a
Visuals (Remotion)──► Silent Render ──► ffmpeg mux ──► Delivery
1.1 Script
- One file per film:
script.json, with fieldsid / act / text / caption. - Chinese voiceover ≈ 4.2 characters/second; a 60-second film ≈ 200–230 characters, a 90-second film ≈ 300–340 characters.
- Write numbers in Chinese reading form (e.g., "three thousand six hundred eighty-eight"), otherwise the TTS may mispronounce them; use Arabic numerals in the visuals.
1.2 Voiceover (Qwen3-TTS VoiceDesign, male documentary voice)
INSTRUCT = ("A deep, calm, authoritative male documentary narrator. Slow, deliberate, cinematic. "
"Low register, warm, intimate, restrained emotion, close-mic. Consistent, even pacing. Mandarin Chinese.")
generate_audio(text=..., model=model, lang_code="zh", instruct=INSTRUCT,
temperature=0.65, top_p=0.9, repetition_penalty=1.05,
output_path="vo_raw", file_prefix=id)
- Model:
mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-bf16, fixed seedmx.random.seed(11). - Immediately after each generation, check duration with
ffprobe: if a clip is > 30 seconds or has entire repeated sections, the TTS model has degraded; delete and regenerate (change the seed or raiserepetition_penaltyto 1.12). - ASR double-check: transcribe with
mlx_whisperto verify numbers and uncommon characters (e.g., hydrogen→qing, silicon→gui, iron→ti).
1.3 Audio Post-processing (Fixed chain, no variations per clip)
highpass=f=70,
asetrate=24000*0.912341, aresample=48000, atempo=1.096081, atempo=1.055,
equalizer=f=190:t=q:w=1.0:g=1.4, equalizer=f=3300:t=q:w=1.4:g=-1.8, equalizer=f=7200:t=q:w=1.8:g=-1.0,
acompressor=threshold=0.14:ratio=2.2:attack=10:release=200,
loudnorm=I=-20:TP=-3:LRA=7
Then trim leading and trailing silence using a 20ms energy envelope, compress pauses longer than 0.34s down to 0.34s, and apply 12ms/50ms fade in/out at both ends.
1.4 Timeline
frames = round(seconds * 24); voiceover offset = scene start + lead (typically 18–24 frames).- Each scene:
lead + vo + tail, tail 16–90 frames (generous for ending cards). - Generate
timeline.ts(auto-generated, do not edit manually) with fields{ id, from, dur, voAt, voDur }.
1.5 Music (Pure code synthesis, numpy)
- Structure, don't blanket: write "events" by timecode, not a single pad throughout.
- Harmony: change every 12–18 seconds, crossfade at boundaries; bass only in sustained tones above 55 Hz.
- No low-frequency rumble: audiences will describe it as "booming" or "too noisy". Low cutoff at 85–100 Hz; long notes use pure sine waves without adding octave harmonics.
- Prioritize quietness: music level ≈ voiceover −20 to −27 dB; when voiceover is present, further compress by 40–70% (sidechain ducking).
- Final mix loudness: documentary −18 to −24 LUFS (not −14, which is for streaming pop music).
- Tutorial-style "sound design" (e.g., underwater thuds, sonar pings, metallic creaks) only add when the client explicitly requests it; it was rejected once, so it's off by default.
1.6 Visuals (Remotion)
See Section 2.
1.7 Rendering and Muxing
npx tsc --noEmit
npx remotion render Film4K out/silent.mp4 --codec=h264 --crf=18 --concurrency=7
ffmpeg -i out/silent.mp4 -i public/audio/master.m4a \
-map 0:v -map 1:a -c:v copy -c:a aac -b:a 320k -shortest -movflags +faststart out/final.mp4
- 4K = 1920×1080 layout +
transform: scale(2); 2K =scale(2560/1920). Write only one layout. - Partial rework: only re-render the affected frame range →
ffmpegtrim/concatthree segments →-c:v copyto replace only the audio track; if visuals change, re-encode and concatenate all three segments (about 5–10 minutes).
2. Visual System
2.1 Base Layer
- Canvas 1920×1080, 24 fps;
AbsoluteFillbackground near-black (#01050A/#010407), never pure black #000. - Three-layer structure: Background Gradient → Volumetric Light/Particles → Information Layer (text, depth scale, readings).
- Uniform finishing:
Vignette+ staticGrain(0.04, must beanimate={false}; dynamic grain makes thin lines flicker).
2.2 Fonts
- Titles/Poetry:
Songti SC(serif), letter spacing 0.4–0.6em, large sizes (90–250px). - Data/Readings:
SF Mono, 8–14px, letter spacing 0.16–0.36em. - Descriptions:
PingFang SC, 14–19px, opacity 0.55–0.7.
2.3 Color Palette (Each film's own "world color")
| Film Type | Top Highlight | Mid | Bottom | Accent Color |
|---|---|---|---|---|
| Universe / Time | #050506 base | Cream #F3E7D0 | — | Amber #E8B06A |
| Ocean / Depth | #1E7C93 water surface | #07232E | #01050A | Cyan #4FE0C0 |
Rule: One film uses only one accent color; exceptions for bioluminescence/hydrothermal vents are limited to two, and each is tied to a specific physical quantity.
2.4 Particle Engine (Canvas 2D)
- Deterministic random (hash), same result every frame; cloud types:
ball / filament / disk / globe-surface / sphere-shell / figure / city / helix / neuron / glyph / single / text / timeline. - Two-pass drawing: first large radius, low opacity for glow; then small radius, high opacity for core;
globalCompositeOperation = 'lighter'. - Point cloud boundaries must be outside the frame:
makeCloud('text', n)creates a 3×1.2 box, which after projection results in rectangular edges (the most commonly reported bug by viewers). Useexpand(cloud, kx, ky, kz)by 2.4–3.2 times.
2.5 Creatures and Terrain (SVG)
- All in side view, head facing movement direction; at least 4 anatomical details (e.g., eyes, fins, mouth lines, throat folds, barnacles), otherwise it looks like a texture map.
- Silhouette + rim light (0.2–0.35 opacity); do not use
feTurbulenceoverlaid on the body (it will look like granite). - Terrain: multi-segment polylines + rock layers + debris + sediment, not triangles; mountains must not have a baseline (it would look like a chart).
- Creature appearance windows tied to depth/time; don't show more than three types simultaneously.
2.6 Information Layer (HUD)
- Zone nameplate: dark gradient background (0.72→0) + 2px accent color bar on the left + Chinese text 30–35px pure white.
- Readings: 2–3 per layer,
LABEL8.5px /VALUE13–14.5px. - Scale ruler: tick marks + bold current value; current value must be computed in real-time, not hardcoded (hardcoded values will conflict with pointers).
- All numbers must include units; separate units with a space (e.g.,
1,093 atm).
2.7 Transitions
- First choice: continuous motion (one continuous shot): depth/time is a continuous quantity, content enters and exits the frame with it.
- Second choice: short dissolve (4–6 frames, only the new shot fades in, the old shot remains opaque and is covered) — brightness doesn't collapse, HUD doesn't overlap text.
- Disabled: diagonal wipes, long cross dissolves, vertical push (exposes dark edges), hard cuts after black (unless there's a physical motivation like "entering water").
3. Facts and Data
- Create a
facts.mdfile: each entryNumber / Unit / Source Range / How to Write in the Film. - Three most error-prone areas: ① Unit conversion (1 atm ≈ 10.3 t/m² ≈ 1.03 kgf/cm²); ② Confusing "average" with "extreme" (average 3,688 m vs. deepest 10,935 m); ③ Simplifications (90 vs. 98 elements).
- Use one consistent precise version throughout the film; voiceover and visuals must come from the same source.
4. Deliverables Checklist
deliverables/
FilmTitle-4K-Voiceover.mp4 # Main version
FilmTitle-4K-MusicOnly.mp4 # If needed
Poster-4K.png / Preview-4K.png # Keyframe composites
Cover/ # 1920 PNG + 4K PNG + 1146×717 JPG
Director's Statement.md # Structure, audio, revision history
Publishing Copy-Bilibili.md # Title/description/tags/pinned comment/cover suggestions
FactSheet.md
The release package must include: AI-generated content declaration, production division of labor (what the model did, what the human did), and a pinned comment version of "easily misread numbers."
5. Pitfall Log (Sorted by Frequency)
| Symptom | Root Cause | Fix |
|---|---|---|
| Rendering hangs at audio preprocessing | Long WAV + <Audio> | Silent render, external mux |
| Text blurry over particles | No background + opacity 0.4–0.5 | Add background + pure white + increase font size |
| Visible rectangular shapes in frame (viewers' most common question) | Point cloud box boundary enters frame | expand() by 2.4–3.2× |
| Thin lines / terrain flickers | Dynamic film grain | Grain animate={false} |
| Whales/fish "swimming backward" | Head and tail reversed, eye at tail | Side-view reconstruction: head at +x |
| Terrain looks like a chart | Path uses L ... Z with a horizontal baseline | Lower edge sinks outside frame |
| Transitions break line of sight | Wipes/long dissolves | Short dissolve or continuous motion |
| Music "booming" | Continuous low drone at 27–80 Hz | High-pass at 85–100 Hz, remove sub drone |
| Audio-visual sync issues | Visual beat doesn't match voiceover content | Rearrange depth/time curve based on voiceover content |
| Single TTS clip 600 seconds | Model degradation loop | Delete and regenerate, repetition_penalty 1.12 |
| 4K rework too slow | Full film re-render | Re-render only affected frame range + concat |
6. Location of the Two Reference Films
- "A Brief History of Humankind: The Self-Portrait of the Universe":
~/dsh-video-remotion/film/src/scenes/*, 14 scenes, 4K, 4'58". - "DESCENT":
~/dsh-video-remotion/film/src/ocean/*, 6 depth zones, 2K, 1'30"; voiceover and music in~/dsh-video-remotion/ocean/. - Both share
src/engine/particles.tsx(particles),src/engine/atmos.tsx(Vignette/Grain), andsrc/theme.ts(fonts/color palette/easing).