Find and cut the most seamless loop in a video of repetitive motion (fidget toys, pendulums, spinners, pouring). Use when the user wants a perfect/seamless/infinite loop, cinemagraph-style segment, or "GIF that never pops" from real footage. Headless CLI; outputs the best-seam MP4 plus ranked JSON candidates.
Resources
6Install
npx skillscat add shihanqu/looplab Install via the SkillsCat registry.
looplab — seamless-loop extraction for agents
When to use
The user has a video of a repetitive motion from a static camera and wants a segment that loops invisibly. Do not use for multi-shot edits, moving-camera footage, or synthetic loop generation — looplab finds cuts that already exist in the footage; it does not fabricate frames.
Requirements
ffmpeg + ffprobe on PATH; Python ≥ 3.10. Install once:
pip install 'looplab[all] @ git+https://github.com/shihanqu/looplab'Backends auto-select: MLX on Apple Silicon, CuPy on NVIDIA (pip install 'looplab[nvidia]'; designed in but untested on real CUDA hardware), numpy elsewhere — slower, same results. Force one with LOOPLAB_BACKEND=mlx|cupy|numpy; the JSON reports which ran in backend. GPU out-of-memory exits 3 with a hint — retry with --proxy-long 256 or LOOPLAB_BACKEND=numpy.
Canonical invocation
looplab INPUT.mp4 --json --quiet- stdout: one JSON object (below). stderr: progress logs (suppressed by
--quiet). - Renders the #1 loop to
INPUT.loop.mp4(override with-o). - Writes
scores.npz+candidates.json+ extra renders toINPUT.mp4.looplab/(override with--workdir).
JSON contract
{
"ok": true,
"output": "clip.loop.mp4",
"rendered": [{"rank": 1, "path": "clip.loop.mp4"}],
"workdir": "clip.mp4.looplab",
"backend": "mlx",
"fps": 30.018, "n_frames": 2021, "period_s": 0.4,
"chosen": {
"rank": 1, "start_frame": 540, "end_frame": 608,
"start_s": 17.989, "end_s": 20.254, "len_frames": 68, "len_s": 2.265,
"score": 0.489, "activity": 1.09, "percentile": 0.0
},
"candidates": ["… up to --top entries, same shape, best first …"]
}Semantics: the loop plays frames [start_frame, end_frame - 1] and wraps — frame end_frame visually matches frame start_frame. score is a relative distance (lower = better; only comparable within one run). percentile is the fraction of all scored pairs that beat this one (0.0 = the best seam in the video). activity is in-loop motion vs the video median (≈1 = typical motion; near the --min-activity floor = sluggish stretch, eyeball it).
On failure: {"ok": false, "error": "...", "exit_code": N} on stdout, same code as process exit. 0 ok · 2 no candidate survived the gates · 3 missing dependency · 4 unreadable input.
Interpreting results / next actions
- Trust
chosenwhenpercentile≤ ~0.05 andactivity≥ ~0.8. Deliveroutputdirectly. - Verify visually when the top scores cluster tightly: render more with
--render-top 3and compare, or build the human-facing explorer with--explore(addsindex.html+ self-containedartifact.htmlto the workdir). For an interactive session,looplab --uiserves the explorer locally with a native OS file picker — meant for humans; agents should stay on the headless flags. - Exit 2 (all gated): retry with
--min-activity 0.5; if still failing, widen--max-loop(e.g. 5) or lower--min-loop. Persistent failure usually means camera motion or no true repetition. - Seam pops on a specific object despite a good score: raise
--focus-weight(2.0) — it weights the bright-object state stream. Footage without a bright subject:--focus-weight 0. - Slow machine / long video:
--proxy-long 256quarters the math at slight discrimination cost.
Tuning quick reference
| Flag | Default | Raise when… | Lower when… |
|---|---|---|---|
--max-loop |
3.0 | user wants longer, more organic loops | — |
--min-activity |
0.7 | degenerate low-motion loops win | exit 2 / slow deliberate motion |
--focus-weight |
1.0 | prop state mismatches at the seam | subject isn't bright/white |
--vel-weight |
1.0 | direction flips at the seam | motion blur dominates |
--window |
5 | seams pose-match but don't flow | very fast cycles (< 0.5 s) |
--crop X,Y,W,H |
full frame | background events pollute the score; loop only one region | — |
--ignore A-B |
— | a known-bad span (fumble, interruption) keeps producing candidates | — |
--proxy-long defaults to auto (512/384/256 stepping down with video length) to bound memory on long inputs; set it explicitly to pin quality. --crop is an attention crop in normalized 0–1 fractions: it constrains only the search — rendered loops remain full-frame. --ignore (seconds, repeatable) forbids loops overlapping a span; the automatic disruption detector already excludes sustained anomalies, so reach for it when you know a stretch is bad.
Apple spatial video (MV-HEVC) is handled: the base (left-eye) view is analyzed and exports are flat H.264.
Library use
from looplab import run_search, SearchParams
result = run_search("clip.mp4", SearchParams(max_loop=4.0))
result.candidates[0] # same dict shape as the JSON contract