tools/check.py and tools/test_motion.mjs
What the tests prove.
And what they don’t. Each check below shows its latest real output for every film, copied from the reports by the sync script. A check only proves what it measures.
Determinism
Pass or failProves
Two renders of 0–2 s give identical frame hashes (ffmpeg -f framemd5).
Doesn’t prove
Anything after 2 s; the same result on another machine, GPU or Chromium version; that the live preview matches the render.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | 120/120 frame hashes identical across two renders of 0-2 s | OK |
| Safari OS v2 · 9x16 | 120/120 frame hashes identical across two renders of 0-2 s | OK |
| Safari OS v2 · 1x1 | 120/120 frame hashes identical across two renders of 0-2 s | OK |
| Florios morph | 120/120 frame hashes identical across two renders of 0-2 s | OK |
| Safari OS v1 | 120/120 frame hashes identical across two renders of 0-2 s | OK |
| The showreel | 120/120 frame hashes identical across two renders of 0-2 s | OK |
Loop seam
Pass or failProves
On a looping film, the jump from the last frame to the first is no bigger than the film’s 95th-percentile frame-to-frame change.
Doesn’t prove
That motion continues across the seam: speed can still jump. The loop-state probe covers that for morph films.
| Film | Latest output | Status |
|---|---|---|
| Florios morph | last→first change 0.05 vs median per-frame change 0.07, 95th pct 3.78 | OK |
| The showreel | last→first change 0.64 vs median per-frame change 0.89, 95th pct 12.23 | OK |
Contact sheet
Report onlyProves
Nothing by itself. It is one frame per beat for a person, or a model, to look at.
Doesn’t prove
Motion quality. Stills can’t show easing, judder or timing.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | out/safarios-v2-contact.png (64 frames, one per beat) | Report |
| Safari OS v2 · 9x16 | out/safarios-v2-9x16-contact.png (64 frames, one per beat) | Report |
| Safari OS v2 · 1x1 | out/safarios-v2-1x1-contact.png (64 frames, one per beat) | Report |
| Florios morph | out/florios-morph-contact.png (24 frames, one per beat) | Report |
| Safari OS v1 | out/safarios-contact.png (40 frames, one per beat) | Report |
| The showreel | out/reel-contact.png (24 frames, one per beat) | Report |
Loudness
Pass or failProves
Integrated loudness within ±1 LU of −14 LUFS, and true peak at or below −1 dBTP, measured on the final mp4.
Doesn’t prove
That the mix sounds good, is balanced, or suits the film.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | -14.1 LUFS integrated, true peak -1.2 dBTP | OK |
| Safari OS v2 · 9x16 | -14.1 LUFS integrated, true peak -1.2 dBTP | OK |
| Safari OS v2 · 1x1 | -14.1 LUFS integrated, true peak -1.2 dBTP | OK |
| Florios morph | -14.1 LUFS integrated, true peak -1.6 dBTP | OK |
| Safari OS v1 | -14.0 LUFS integrated, true peak -4.0 dBTP | OK |
| The showreel | -13.9 LUFS integrated, true peak -4.0 dBTP | OK |
Cue sync
Pass or failProves
For every cue in timeline.json, the first visible pixel change in that cue’s element lands within one frame of its time.
Doesn’t prove
That the right sound plays there, or is audible; anything not listed as a cue. The film reports its own element boxes, so a wrong box gives a wrong answer.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | 51/51 cues: first visible change of the cued element within 1 frame of its sound (median +0.0 ms, range +0..+17 ms) | OK |
| Safari OS v2 · 9x16 | 51/51 cues: first visible change of the cued element within 1 frame of its sound (median +0.0 ms, range +0..+17 ms) | OK |
| Safari OS v2 · 1x1 | 51/51 cues: first visible change of the cued element within 1 frame of its sound (median +0.0 ms, range -17..+17 ms) | OK |
| Florios morph | 15/15 cues: first visible change of the cued element within 1 frame of its sound (median +0.0 ms, range +0..+8 ms) | OK |
| Safari OS v1 | 48/48 cues: first visible change of the cued element within 1 frame of its sound (median +0.0 ms, range +0..+17 ms) | OK |
| The showreel | 28/28 cues: first visible change of the cued element within 1 frame of its sound (median +6.2 ms, range -8..+17 ms) | OK |
States and loop state
Pass or failProves
On a morph film, each state’s container is within ±2 px of its spec table (size and corner radius), and every animated track’s value and velocity match at the loop point.
Doesn’t prove
Anything inside the container.
| Film | Latest output | Status |
|---|---|---|
| Florios morph | states: 8/8 within ±2 px of the table | OK |
| Florios morph | loop state: max |value(t=12) − value(0)| 0.00e+00, max |velocity(t=12) − velocity(0)| 0.00e+00 (all tracks) | OK |
Text visible
Pass or failProves
At each scene’s settle time, the frame is pixel-identical with the text masks off, and with the container clip off (within 8 levels): no text at rest is clipped.
Doesn’t prove
Text mid-motion; text overlapping other elements; contrast.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | 9/9 scenes identical at their settle time with text masks off (exact) | OK |
| Safari OS v2 · 16x9 | 9/9 scenes identical at their settle time with container clip off too (tolerance 8 levels) | OK |
| Safari OS v2 · 9x16 | 9/9 scenes identical at their settle time with text masks off (exact) | OK |
| Safari OS v2 · 9x16 | 9/9 scenes identical at their settle time with container clip off too (tolerance 8 levels) | OK |
| Safari OS v2 · 1x1 | 9/9 scenes identical at their settle time with text masks off (exact) | OK |
| Safari OS v2 · 1x1 | 9/9 scenes identical at their settle time with container clip off too (tolerance 8 levels) | OK |
| Florios morph | 8/8 states identical at state + 1.0 s with text masks off (exact) | OK |
| Florios morph | 8/8 states identical at state + 1.0 s with container clip off too (tolerance 8 levels) | OK |
Text size
Pass or failProves
The smallest readable string on screen is at least the format’s minimum size (minTextPx).
Doesn’t prove
Strings the film doesn’t register as readable; contrast, or legibility at real phone size.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | smallest readable text per scene ≥ 56 px | OK |
| Safari OS v2 · 9x16 | smallest readable text per scene ≥ 40 px | OK |
| Safari OS v2 · 1x1 | smallest readable text per scene ≥ 40 px | OK |
Audio onsets
Report onlyProves
Report only: how close the mix’s onsets sit to the 16th-note grid.
Doesn’t prove
Pass or fail; sync to the picture.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | 240 detected: median |offset| from 16th grid 6.7 ms, 94% within 20 ms | Report |
| Safari OS v2 · 9x16 | 240 detected: median |offset| from 16th grid 6.7 ms, 94% within 20 ms | Report |
| Safari OS v2 · 1x1 | 240 detected: median |offset| from 16th grid 6.7 ms, 94% within 20 ms | Report |
| Florios morph | 52 detected: median |offset| from 16th grid 5.5 ms, 98% within 20 ms | Report |
| Safari OS v1 | 128 detected: median |offset| from 16th grid 7.7 ms, 99% within 20 ms | Report |
| The showreel | 73 detected: median |offset| from 16th grid 5.7 ms, 99% within 20 ms | Report |
SFX placement
Report onlyProves
score.py put every sound effect exactly at its cue’s time (internal consistency).
Doesn’t prove
Measured audio–visual sync. Cue sync measures the picture side.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | sfx placement (internal consistency): 51 cues, max |sfx time − cue time| 0.00 ms | Report |
| Safari OS v2 · 9x16 | sfx placement (internal consistency): 51 cues, max |sfx time − cue time| 0.00 ms | Report |
| Safari OS v2 · 1x1 | sfx placement (internal consistency): 51 cues, max |sfx time − cue time| 0.00 ms | Report |
| Florios morph | sfx placement (internal consistency): 15 cues, max |sfx time − cue time| 0.00 ms | Report |
| Safari OS v1 | sfx placement (internal consistency): 48 cues, max |sfx time − cue time| 0.00 ms | Report |
| The showreel | sfx placement (internal consistency): 28 cues, max |sfx time − cue time| 0.00 ms | Report |
Clipping and astats
Pass or failProves
No decoded sample at or beyond full scale. It also reports DC offset and crest factor.
Doesn’t prove
Distortion added before the limiter.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| Safari OS v2 · 16x9 | astats: DC offset -0.000075 / +0.000005 (L / R), crest factor 5.82 (15.3 dB) / 5.62 (15.0 dB) | Report |
| Safari OS v2 · 9x16 | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| Safari OS v2 · 9x16 | astats: DC offset -0.000075 / +0.000005 (L / R), crest factor 5.82 (15.3 dB) / 5.62 (15.0 dB) | Report |
| Safari OS v2 · 1x1 | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| Safari OS v2 · 1x1 | astats: DC offset -0.000075 / +0.000005 (L / R), crest factor 5.82 (15.3 dB) / 5.62 (15.0 dB) | Report |
| Florios morph | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| Florios morph | astats: DC offset -0.000228 / -0.000273 (L / R), crest factor 4.98 (13.9 dB) / 4.91 (13.8 dB) | Report |
| Safari OS v1 | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| Safari OS v1 | astats: DC offset -0.000014 / +0.000133 (L / R), crest factor 3.16 (10.0 dB) / 3.31 (10.4 dB) | Report |
| The showreel | clipping: 0 samples at or beyond full scale (|x| >= 1.0); astats flat factor 0.00 | OK |
| The showreel | astats: DC offset +0.000319 / +0.000509 (L / R), crest factor 3.05 (9.7 dB) / 3.18 (10.1 dB) | Report |
Small speaker
Report onlyProves
Report only: the loudness drop after two 150 Hz high-passes, a stand-in for a phone speaker.
Doesn’t prove
How it really sounds on a phone. It isn’t gated; each brief sets its own target.
| Film | Latest output | Status |
|---|---|---|
| Safari OS v2 · 16x9 | -15.7 LUFS after two 150 Hz high-pass stages vs -14.1 LUFS full range: 1.6 LU drop | Report |
| Safari OS v2 · 9x16 | -15.7 LUFS after two 150 Hz high-pass stages vs -14.1 LUFS full range: 1.6 LU drop | Report |
| Safari OS v2 · 1x1 | -15.7 LUFS after two 150 Hz high-pass stages vs -14.1 LUFS full range: 1.6 LU drop | Report |
| Florios morph | -15.8 LUFS after two 150 Hz high-pass stages vs -14.1 LUFS full range: 1.7 LU drop | Report |
| Safari OS v1 | -18.7 LUFS after two 150 Hz high-pass stages vs -14.0 LUFS full range: 4.7 LU drop | Report |
| The showreel | -17.9 LUFS after two 150 Hz high-pass stages vs -13.9 LUFS full range: 4.0 LU drop | Report |
Spring tests
All passProves
Every closed-form spring in lib/motion.js matches the spring ODE, integrated at dt = 1e-5, to within 1e-3: value, velocity, retargeting with springTrack, and loop seams.
Doesn’t prove
That a spring looks right in a film, or that a film uses the right preset. It tests the maths, not the taste.
Latest output of node tools/test_motion.mjs: “all spring tests passed”
step response (closed form vs semi-implicit Euler, dt = 1e-5, t in 0..2 s)
| case | freq | damp | max|closed−sim| | overshoot | settle 2% | |1−x(2 s)| | |1−x(10 s)| |
|---|---|---|---|---|---|---|---|
| SNAPPY | 3.5 | 0.85 | 7.40e-5 | 0.63 % | 0.190 s | 0.00e+0 | 0.00e+0 |
| DEFAULT | 2.2 | 0.8 | 4.78e-5 | 1.52 % | 0.272 s | 4.13e-10 | 0.00e+0 |
| HEAVY | 1.4 | 1 | 2.74e-5 | 0.00 % | 0.663 s | 4.25e-7 | 0.00e+0 |
| CAMERA | 1.2 | 0.95 | 2.41e-5 | 0.01 % | 0.698 s | 1.83e-6 | 0.00e+0 |
| PLAYFUL | 2.5 | 0.5 | 6.41e-5 | 16.30 % | 0.514 s | 3.51e-9 | 0.00e+0 |
| damp 0.3 | 2.2 | 0.3 | 6.28e-5 | 37.23 % | 0.812 s | 1.56e-4 | 0.00e+0 |
| damp 0.8 | 2.2 | 0.8 | 4.78e-5 | 1.52 % | 0.272 s | 4.13e-10 | 0.00e+0 |
| damp 1 | 2.2 | 1 | 4.30e-5 | 0.00 % | 0.422 s | 2.82e-11 | 0.00e+0 |
| damp 1.5 | 2.2 | 1.5 | 3.40e-5 | 0.00 % | 0.771 s | 3.04e-5 | 0.00e+0 |
| damp 3 | 2.2 | 3 | 2.03e-5 | 0.00 % | 1.662 s | 8.97e-3 | 5.16e-11 |
springTrack retargeting (targets change before earlier moves settle), t in 0..3 s
| case | max|track − sim| |
|---|---|
| SNAPPY | 1.85e-4 |
| DEFAULT | 1.19e-4 |
| HEAVY | 7.07e-5 |
| CAMERA | 6.29e-5 |
| PLAYFUL | 1.63e-4 |
| damp 0.3 | 1.78e-4 |
| damp 0.8 | 1.19e-4 |
| damp 1 | 1.08e-4 |
| damp 1.5 | 8.64e-5 |
| damp 3 | 4.98e-5 |
periodic springTrack (period 3 s): seam and steady state vs ODE
| case | |Δvalue| | |Δvelocity| | steady state: max|track − sim| | max|velocity err| |
|---|---|---|---|---|
| SNAPPY | 0.0e+0 | 1.5e-4 | 1.04e-4 | 1.11e-3 |
| DEFAULT | 0.0e+0 | 5.7e-5 | 6.68e-5 | 4.34e-4 |
| HEAVY | 0.0e+0 | 2.4e-5 | 3.96e-5 | 1.73e-4 |
| CAMERA | 0.0e+0 | 1.8e-5 | 3.55e-5 | 1.23e-4 |
| PLAYFUL | 0.0e+0 | 7.5e-5 | 8.97e-5 | 5.36e-4 |
| damp 0.3 | 0.0e+0 | 6.9e-5 | 9.09e-5 | 4.66e-4 |
| damp 0.8 | 0.0e+0 | 5.7e-5 | 6.68e-5 | 4.34e-4 |
| damp 1 | 0.0e+0 | 5.7e-5 | 6.04e-5 | 4.47e-4 |
| damp 1.5 | 0.0e+0 | 5.8e-5 | 4.87e-5 | 4.65e-4 |
| damp 3 | 0.0e+0 | 5.8e-5 | 2.83e-5 | 4.87e-4 |
The determinism hook
Blocks the editProves
No line Claude writes in render code calls Math.random, a timer, requestAnimationFrame, the wall clock, or a CSS transition. Exit code 2 sends the violation back to Claude. CI runs the same script over every film.
Doesn’t prove
Anything a line-by-line pattern can’t see: state carried between frames, or values read back from layout. The // preview-only exemption works on trust. The determinism check above is the backstop.
Source: .claude/hooks/check-determinism.mjs · CI: .github/workflows/films.yml