VSE Gamma Cross
The GAMMA_CROSS fade is not the naive linear mix: it blends in a gamma-0.5 space, so the mid-cross dips below the sRGB lerp. Tiny per-frame renders are asserted against the closed form per frame.
examples/vse-cut-list/
The sequencer API rename from 4.5 LTS to 5.x — strips (never .sequences), new_effect ending in length= vs frame_end=, and left_handle/right_handle/duration replacing the deprecated frame_final_*. A deterministic cut list — color programs, a clamped GAMMA_CROSS, a scene strip, a text strip — asserted before and after save/reload.
Rendered headless by the example itself. Select it to enlarge.
runs on 4.5 LTS · 5.1 · 5.2 LTS
blender --background --python examples/vse-cut-list/vse_cut_list.py --
falsify it Breaks the contract on purpose and must exit 7
blender --background --python examples/vse-cut-list/vse_cut_list.py -- --swap-inputs
A runnable example that witnesses the sequencer API rename between Blender 4.5 LTS and 5.x — the single most common way AI-generated VSE code dies on a modern Blender. Every tutorial-era snippet does scene.sequence_editor.sequences.new_effect(..., frame_end=...); on 5.x that path is gone twice over (sequences removed, frame_end kwarg rejected), and on 4.5 the modern spelling (strips, length=) is already the only one the new collection accepts.
What it witnesses: a deterministic cut list — three color programs A/B/C, a GAMMA_CROSS fed by a dedicated source pair T1/T2, a scene strip, and a text strip — asserted against closed-form spans on each version's canonical accessors, then re-asserted after a save/reload round-trip:
sequence_editor.sequences raises AttributeError; only .strips exists. 4.5: both accessors exist and see the same strips (the transition bridge).strips.new_effect(...) ends a strip with frame_end= on 4.5 but length= on 5.x; the wrong kwarg raises TypeError on each side (asserted, not assumed). 5.x also rejects the removed TRANSFORM effect type — per-strip strip.transform (StripTransform) places strips in the frame on both versions.frame_final_* on 4.5; left_handle/right_handle/duration on 5.x, where the frame_final_* names are deprecated aliases (removal announced for 6.0) that must still read equal — the bridge is asserted lossless, with the RNA is_deprecated flag checked on 5.x and its absence checked on 4.5.input_1/input_2 (not input1) on both versions, in authored order, and the cross clamped to exactly the T1/T2 overlap.new_scene(name, scene, channel, frame_start) on both versions (no length/frame_end kwarg); default span is the source scene's frame range; the strip sources a separate Stage scene.save_as_mainfile → open_mainfile.--check-pixels) — a 96×54 render asserts each mosaic cell center carries its strip color (tol 0.1), the cross cell is a true mid blend (strictly between the sources on every channel), and a margin point matches neither cross source — because a consumed input compositing independently would paint it full-frame.What each check catches on failure:
.sequences returning on 5.x, or the 4.5 bridge seeing different contents (falsified: the legacy path on 5.1.1 raises AttributeError: 'SequenceEditor' object has no attribute 'sequences').new_effect(frame_end=...) on 5.1.1 raises TypeError: ... expected (name, type, channel, frame_start, length, input1, input2)).GC span (25, 33, 8) != closed form (25, 34, 9) — note the cross was clamped to the 8-frame overlap, itself a witnessed behavior).input1=t2, input2=t1 exited 7 with measured inputs=(T2, T1), expected T1 -> T2).Hazards discovered while authoring (all witnessed by the checks above):
bpy_prop_collection is falsy — se.strips or se.sequences silently falls through to the legacy accessor on an empty timeline. Always branch on hasattr.Version divergence: the whole example is the divergence — gated on the bpy.app.version tuple (>= (5, 0, 0)), never on version_string ("4.5.11 LTS" is not bare semver). Each side asserts its own canonical contract plus the other side's removal/bridge state. Measured values are identical on 4.5.11 and 5.1.2, including the pixel witness. 5.2 LTS COLOR strips bake readonly width/height from the scene render resolution at new_effect time (4.5/5.1 have no intrinsic size — scale is a fraction of the output frame). The pixel check sets 96×54 before building the cut list; creating at factory 1920×1080 then rendering 96×54 makes 0.36-scaled cells larger than the output, C covers the frame, and TL samples amber (0.949, 0.62, 0.102) instead of crimson.
Render: the program wall is the sequencer output at frame 29 (mid cross) — crimson A, teal B, amber long-runner C, and the 50/50 cross blend, over the Stage scene strip showing the dark studio. An off-by-one in end-exclusive span math drops its cell to the dark stage; the caption strip carries the closed form. The hero presents that authentic frame on a reference monitor in a dark-studio editing bay — the pixels on the screen are the genuine sequencer output (evidence); only the bay around them is staged (presentation). Rendered locally with EEVEE (GPU host); the checks and --check-pixels need no GPU (Cycles CPU).
In front of the monitor, a tilted timeline console makes the same cut list physical: one lane per channel (A, B, C, TXT, GC), each strip a coloured block spanning its [left_handle, right_handle) read back through the same strip_span accessor the check asserts, with raised handle notches at both ends, a frame ruler, and the playhead at frame 29. The GC block ramps from its input_1 colour to its input_2 colour, so the wiring reads too. The console is laid out from the strips, never from constants — a span that drifted would move its block. The monitor's neck is seated into the monitor back and its foot sits on the walnut desk top; none of the bay touches the sequencer frame or any check.
The still builds the cut list at the 1280×720 render resolution. 5.2 COLOR strips bake width/height from the render resolution at new_effect time (the same trap --check-pixels documents), and the previous still built at the factory 1920×1080 and rendered at 1280×720, which pushed every mosaic cell off its crosshair slot.
The gallery still renders from the dedicated Bay scene (camera BayCam), not bpy.context.scene — a framing measurement with examples/gallery_framing.py must be handed that (scene, camera) pair explicitly.
# Cheap correctness check (no render) — the CI check:
blender --background --python vse_cut_list.py --
# Falsifier: GC T2 -> T1. Must exit non-zero (wiring). `--check-pixels` is a
# second witness, not this falsifier.
blender --background --python vse_cut_list.py -- --swap-inputs
# Compositing witness on a tiny render (Cycles CPU, CI-safe):
blender --background --python vse_cut_list.py -- --check-pixels --engine cycles
# Also render the still (EEVEE on a GPU host; --engine cycles on GPU-less):
blender --background --python vse_cut_list.py -- --output vse.png
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is also the shared framing helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | .sequences accessor contract for this Blender |
| 4 | Wrong-era new_effect end kwarg or TRANSFORM type did not TypeError |
| 5 | Strip span or channel off closed form |
| 6 | frame_final_* deprecation bridge for this Blender |
| 7 | GC wiring or strip .type enum (--swap-inputs lands here) |
| 8 | Scene strip span or Stage source |
| 9 | Mosaic transform, compositing defaults, text body, or strip color |
| 10 | Gallery framing violation (--output only; gallery_framing) |
| 12 | --check-pixels compositing contract |
| 13 | Save/reload round-trip re-assert failed |
| 14 | --output produced no file |
The blender-smoke workflow runs the check and the pixel witness on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output. Its catalog falsifier is --swap-inputs (expects exit 7).