Every cut lands on a downbeat
Audio is extracted (22.05 kHz mono), beats are detected with librosa.beat.beat_track, and the beat list is split into N contiguous groups — so every segment starts and ends on a musical downbeat.
Split a long video into N segments whose boundaries land on musical beats — each clip plays forward then mirrors back, so dropped into a loop slot it cycles without a visible seam. Beat detection via librosa; rendering via MoviePy. Falls back to equal-time slices when the video has no usable audio. Everything runs locally.
A live illustration of what the CLI does: beat grid in, N windows out, each window built as forward + reversed. Move the controls and the segmentation re-divides. Illustrative — this is a simulation of the documented algorithm, not a video render.
| Segment | Start | End | Built |
|---|
Audio is extracted (22.05 kHz mono), beats are detected with librosa.beat.beat_track, and the beat list is split into N contiguous groups — so every segment starts and ends on a musical downbeat.
Each segment is built as [forward, reverse] (vfx.time_mirror) — the tail mirrors back to the head, no visible seam.
No audio or too few beats? Equal-duration fallback kicks in instead of failing. The pipeline does not return empty-handed.
segment_01.mp4 … segment_NN.mp4 in one archive, handed back through the UI file widget or written by --out.
Setup is a one-time uv sync (same pattern as the CLIP Interrogator). ffmpeg must be on PATH — MoviePy shells out to it for encode/decode.
$ brew install ffmpeg # if not already installed
$ npm run lvs:setup # uv sync --project lib/loopable-video-segmenter
$ npm run lvs:check # imports + deps + ffmpeg presence, no model downloads$ npm run lvs:ui
# gradio app on http://127.0.0.1:7861 (7860 is the CLIP Interrogator's port)$ npm run lvs:segment -- video.mp4 --segments 4
$ npm run lvs:segment -- video.mp4 --segments 8 --no-loopable --json
$ npm run lvs:segment -- video.mp4 --segments 2 --out /path/to/out.zip
--json emits the zip path, segment count and which segmentation path ran
(beat detection or equal-time fallback). Exit codes: 0 ok,
2 bad input, 3 runtime failure.
| Parameter | Range / default | Meaning |
|---|---|---|
| Number of segments | 1–20 · --segments, default 4 | How many loopable clips to produce from the source. Make each segment loopable |
| Make each segment loopable | on by default · --no-loopable | Forward + reverse mirror build; off = plain cuts. |
| Input | MP4 (h264 + aac works best) | Audio is extracted for beat tracking; silent video falls back to equal time. |
22.05 kHz mono, written to a temp file via MoviePy.
moviepy · wav
Beats detected with librosa; only times inside the video duration are kept.
librosa.beat.beat_track
The beat list is split into N groups → N (start, end) windows that begin and end on downbeats.
beats → (start, end)
Each clip is subclip(start, end) concatenated with its own time mirror, encoded h264 + aac.
concatenate([sub, sub.fx(time_mirror)])
Segments are zipped and handed back — UI file widget, or written by --out.
segment_01…NN.zip
Quality depends on the audio. Live or percussive music gives the cleanest splits. Spoken word and ambient tracks fall through to the equal-time fallback — the pipeline never refuses to produce output.
The mirrored half plays audio backwards. The time_mirror trick makes visuals loop seamlessly, but the second half of each clip's audio is reversed. Fine for visual loops; for music-video work, mute the second half in your DAW or cross-fade.
Source code: lib/loopable-video-segmenter/ (uv-managed, src/loopable_video_segmenter/).
MoviePy is pinned to 1.x — the code uses the 1.x API (moviepy.editor, subclip, vfx.time_mirror).
Docs: docs/LOOPABLE-VIDEO-SEGMENTER.md. Source repo: github.com/kajica2/loopable-video-segmenter.