TakeCheck - video acceptance
Run takecheck's acceptance checks on a generated take: frozen frames, duplicate frames, blank frames, flicker, motion, subtitle residue and audio. CPU only.
takecheck
pytest for AI-generated video. Turn “is this take usable?” into an executable spec and a CI gate.
Status: v0.1.0 — published on PyPI. Spec compliance plus frozen / duplicate / blank / flicker / motion / subtitle residue / audio, with a self-contained HTML report.
Why
A 20-minute render should not be judged by scrubbing frames by hand. takecheck reads a takes.yml
spec, compares the delivered file against it, and returns pass / fail + the reason — in seconds,
on CPU, while the GPU is still rendering.
Generation pipelines fail in ways a thumbnail cannot show: a shot freezes for 300 ms, the camera starts flickering, the reference clip’s burned-in subtitles survive the redraw, the audio track silently disappears, the last frame of segment 3 does not match the first frame of segment 4. Each is cheap to detect and expensive to discover after a 20-minute render — or after stitching the whole film.
Existing tools do not cover this layer: VMAF/DOVER/DVQA score perceptual quality against a reference or a learned aesthetic prior, VideoGen-Eval is an LLM-API research harness, and ComfyUI QC nodes check tensors inside a graph instead of the delivered file.
Install
pip install takecheck
Requires ffmpeg / ffprobe on PATH. No PyTorch, no GPU — numpy is the only numeric dependency.
Quick start
takes.yml:
name: h3-ep12-shot03
expect:
duration: 5.0
fps: 24
frames: 121
resolution: 1280x720
audio: required
subtitles: forbidden
frozen_max_ms: 200
black_max_frames: 0
flicker_max: 0.35
motion_min: 0.002
takecheck check out/shot03.mp4 --spec takes.yml --json report.json --html report.html
FAIL h3-ep12-shot03 (shot03.mp4)
PASS spec 1280x720 | 24fps | 121f | 5.04s
FAIL frozen 1 frozen stretch(es) over 200ms: 0.00-0.83s (longest 833ms)
PASS duplicate 0/120 frames replay an earlier frame (0.0%)
PASS blank 0 blank frame(s), luma 12-241
PASS flicker worst 5-frame luma swing 9/255 (3.5% <= 35%)
PASS motion mean frame-to-frame motion 0.0412 >= 0.0020
PASS subtitle no persistent text band (row contrast 1.28 < 2 on 0% of frames)
PASS audio peak -3.2 dB, -18.4 LUFS, 0% silent
Add --html report.html for a self-contained report: a contact sheet with failing moments outlined in
red, every measured value, and the raw JSON inline. No external assets, so it can be emailed or
attached to a PR as a single file.
Exit codes: 0 pass, 1 fail, 2 bad spec / unreadable file.
What the checks measure
Every check reports the numbers behind its verdict in --json and --html, not just PASS/FAIL.
| Check | Signal | Fails when |
|---|---|---|
| frozen | pHash Hamming distance between consecutive frames | ≤ 3 for longer than frozen_max_ms |
| duplicate | repeated frame hashes, ignoring neighbours, blanks and freezes | more than 1% of frames replay an earlier one |
| blank | mean luma per frame | below 16 or above 240, more than black_max_frames |
| flicker | peak-to-peak luma inside a 5-frame window | above flicker_max (fraction of 0–255) |
| motion | mean absolute frame-to-frame difference | below motion_min |
| subtitle | busiest row's horizontal activity ÷ that band's median row | above 2.0 on ≥ 30% of frames, when subtitles: forbidden |
| audio | peak dBFS, EBU R128 loudness, silent fraction | peak ≥ −0.1 dBFS, or more than 60% silent |
Three design choices worth knowing:
- One defect, one check. A cut to black is reported by
blankonly;duplicateskips blank frames, andflickerskips windows containing them. A freeze is reported byfrozen, andduplicateignores frames a freeze already explains. A missing audio track is reported by the spec check, soaudiostays quiet about it instead of failing twice for one problem. motionis exempt on purpose. It is a whole-clip aggregate ("does this clip move at all"), not an event with a timestamp, so it may legitimately co-fire with a local check: a fully black clip really is both blank and motionless.- Thresholds are calibrated, not guessed.
motion_mindefaults to0.002because a normally moving clip measures ≈ 0.009 and a still render ≈ 0.000 on the bundled fixtures. Real footage differs — set the values intakes.ymlfor your own pipeline. - Deliberately slow shots read as still. A clip slowed down 8x measures
mean_motion ≈ 0.0010and will fail the defaultmotion_minof0.002. If your pipeline uses slow motion or static compositions on purpose, lowermotion_min(or set it to0) intakes.yml.
Known limitations
- The
subtitlecheck has only been validated on synthetic footage. Its fixtures use a hard white bar on black. Real subtitle ghosts are semi-transparent and blurred, and a scene that legitimately contains text in the lower third will trip the check. Treat the default threshold as a starting point, not a calibrated value. - Loudness is reported, not enforced. Only clipping and excessive silence fail the audio check;
a quiet or hot mix is recorded under
measuredfor you to judge. - Long clips are untested. Decoding is streamed, so memory should stay flat, but a 20-minute render has not been benchmarked.
Roadmap
- [x] v0.0.1 — spec format, ffprobe compliance check, CLI,
takecheck selftest - [x] v0.1.0 — frozen / duplicate / blank / flicker / motion / subtitle / audio, HTML report, on PyPI
- [x] ComfyUI node — drop it after a sampler, branch on
pass - [ ] v0.2.0 — calibration against accepted/rejected takes, head/tail continuity
- [ ] v0.3.0 — pipeline lint (custom-node provenance, model hash pinning)
- [ ] v0.4.0 — pluggable VLM judge
ComfyUI
comfyui/ holds a node that runs these checks on the IMAGE batch you just
generated. It reuses this package — no check logic is duplicated — and runs on
the CPU while your GPU keeps sampling.

cmd /c mklink /J "<ComfyUI>\custom_nodes\ComfyUI-TakeCheck" "<repo>\comfyui"
"<ComfyUI>/../python/python.exe" -m pip install takecheck
Restart ComfyUI; the node appears under video/validation → TakeCheck.
Outputs are pass (BOOLEAN), verdict, report (path) and detail (the
failing checks in one line). See comfyui/README.md for the
spec options, and run python comfyui/TakeCheck.py to self-test without ComfyUI.
Two things to know about the node:
flickeris less sensitive there than in the CLI. ComfyUI hands overIMAGEas clamped RGB, so a take whose brightness was pushed past 0/255 in YUV arrives with the swing already squashed. A real exposure flicker is still caught; run the CLI on the saved file if you depend on this check.- A malformed spec raises an error rather than returning "fail", so you can tell a broken config from a bad take.
Test media
testmedia/synthetic/ holds seven tiny clips that each reproduce exactly one defect — frozen frames,
duplicate frames, blank frames, flicker, missing audio, burned-in subtitles — plus good.mp4 as the
control that must always pass. Rebuild them with:
python scripts/make_testmedia.py
The script also verifies each clip against its expected profile (duration, frame count, audio) and runs in CI, so the fixtures cannot silently rot. A second script asserts the verdict for each one:
python scripts/check_synthetic.py
It fails if good.mp4 trips any check, or if a defect clip stops being caught — so a threshold change
cannot quietly break detection.
Real footage never goes in the repository: testmedia/real/ is git-ignored.
Development
pip install -e ".[dev]"
pytest -q
takecheck selftest # builds synthetic clips in a temp dir; needs ffmpeg
python scripts/check_synthetic.py
Note for contributors on Windows: drawtext needs an explicit fontfile=, otherwise ffmpeg aborts
with a Fontconfig error instead of drawing anything. The test helpers search a list of common font
paths for this reason.
中文简介
takecheck 是给 AI 生成视频用的验收层:用 takes.yml 声明一条片子该满足什么(时长、帧率、
无静帧、无字幕残影、音轨完整),一条命令返回通过与否和原因,可直接当 CI 门禁。
全程 CPU、秒级,能与渲染并行,不占显存。
License
MIT.