Video Transition Detector
Finding real scene boundaries in your video
- images
- boundaries_json
- boundary_count
- report
- boundary_frames
- debug_csv
If you've ever tried to re-render a long video by chunking it into LTX or Wan-sized pieces, you know the pain of picking where the chunks go. Chop mid-action and you get a mushy crossfade where a punch used to be. The whole point of TKTransitionDetector is to answer "where do I cut?" for you, and it's smarter than a naive "every N frames" splitter: it finds real cuts, catches crossfades, and only falls back to forced boundaries when the video gives it nothing to work with.
This is the TKNodes pack's answer to the scene-change detection you get in a real NLE - the README even says "similar to how Davinci works." Calm down before you expect broadcast-grade accuracy, though. The source's own docstring calls it a prep tool, "mostly correct, fast, no manual threshold babysitting." For feeding a video-to-video chunking pipeline that's exactly the right trade.
How it works
The node downscales your frame batch to a small grayscale strip (analysis_width, default 160 - detection doesn't need full resolution) and runs two signals in parallel:
- Hard cuts show up as an isolated spike in histogram distance between consecutive frames. It uses a rolling z-score over that distance, so a jump is only a cut if it's a lone spike - not part of a sustained run (that's a dissolve, not a cut).
cut_z_thresh(default 3) is your sensitivity knob: raise it to see fewer cuts. - Dissolves and crossfades don't spike; they smear. Canny edge density dips while the image blurs between shots, and the detector compares each frame against a backward-looking baseline that deliberately ignores recent frames - so the dip stays visible even while it's happening. A dip lasting at least
dissolve_min_lenframes (default 5) marks where the dissolve finishes.
Boundaries that land within three frames of each other get deduped, and then the guardrail kicks in: any gap longer than max_segment_seconds (default 10) gets a "forced" boundary inserted so downstream rendering never gets asked for more than it can chew. Video-generation models have hard frame-count ceilings - LTX wants 8n+1 frames, Wan wants 4n+1 - and forcing a 10-second cap is the chunker's job security.
The inputs that matter
You mostly touch three knobs. fps needs to match your source (feed it the same FPS the video was loaded at, or every time label will be wrong). max_segment_seconds caps chunk length. And when detection feels wrong, cut_z_thresh is where you start: too many phantom cuts, raise it; real cuts being ignored, lower it. debug_mode is worth one run - it fills the debug_csv output with per-frame z-scores, edge density, and baselines so you can see exactly why a frame got flagged instead of guessing.
Outputs
Five of them, but the two that matter: boundaries_json gives you each boundary with its frame number, time, and type (cut, dissolve_end, forced, end), and boundary_frames hands you a clean list of frame indices you can wire into the pack's chunkers or a loop. report is a human-readable summary you can drop on a Text node to check your work, and boundary_count is just the integer count for bookkeeping.
Install
It ships in the TKNodes pack, so one install gets you all of this. ComfyUI Manager, search "Handy Nodes", install, restart. Or the manual way:
cd ComfyUI/custom_nodes
git clone https://github.com/trashkollector/TKNodes
Then restart ComfyUI. The pack pulls in numpy, opencv (used here), pydub, torchaudio and more via requirements.txt, and needs FFmpeg on your system PATH for anything touching audio files. The detector itself is lightweight - no model downloads, it's pure image math on your existing tensor.
Where people get burned: forget to match fps to the source and every timestamp is off by the ratio; crank analysis_width up to 640 thinking you'll get more accuracy and mostly you'll get slower runs for the same answer. Leave it low. The detector is a chopping guide, not a precision instrument - let the chunker handle the boundary snapping, that's its job.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | — | |
| fps | FLOAT | 25.001–240 | — |
| max_segment_seconds | FLOAT | 10.01–120 | — |
| cut_z_thresh | FLOAT | 3.01–10 | — |
| dissolve_dip_ratio | FLOAT | 0.700.1–0.95 | — |
| dissolve_min_len | INT | 52–60 | — |
| analysis_width | INT | 16064–640 | — |
| debug_mode | BOOLEAN | false | — |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| boundaries_json | STRING | — |
| boundary_count | INT | — |
| report | STRING | — |
| boundary_frames | INT | — |
| debug_csv | STRING | — |