clippr — what it is, its parts, where we are
updated 2026-09-03 · MVP, runs on your own computer · page: http://127.0.0.1:8000 (
uvicorn web.app:app)framing rule: designed = an idea · built = code exists ·
verified = we watched it work. nothing built is called "working" without a run, a frame, or a number behind it.
what it does, in one breath
give clippr an episode or a film; it reads the subtitles, finds the moments that work on their own, and cuts each one into a vertical clip under 20 seconds, with or without music, ranked for you to review and download.
- for people scrolling shorts who have never seen the show — every clip must make sense alone
- two shapes of clip: a mini narrative (setup → turn → payoff, joined from two moments if needed) or an engaging situation (a confrontation, a confession, a reveal, a joke that lands)
- nothing leaves the machine: the chosen file is copied in only for the run (removed after), the clips land in a local folder the page points to
the parts (one box each on the board)
| box | what it does | where it lives |
|---|---|---|
| the page | choose the video (and a .srt only if needed), press "find the clips", watch the four stages, review the ranked cards, open the local folder, download; this documentation is embedded at the bottom and served at /docs | web/app.py, web/static/index.html |
| ingest | reads the .srt — or pulls the English text track out of the video itself — cleans it, gives every line a [mm:ss-mm:ss] stamp; picks the English audio track too | clippr/ingest.py, clippr/ff.py |
| story finder | four model passes over the whole transcript (below) → a ranked list of stories, each = 1–6 segments with exact line boundaries | clippr/story.py, prompts in clippr/prompts/ |
| cutter | cuts each segment, crops to 9:16, joins segments with a crossfade, levels the dialogue, keeps every file ≤ 20.0s → NN_dry.mp4 + thumbnail | clippr/cut.py |
| music | picks a free Creative Commons track for the mood and ducks it under the dialogue → NN_music.mp4, attribution kept | clippr/music.py, music/<mood>/ |
| the run | one call that chains the parts and writes clips.json after every clip, so a crash keeps what is done | clippr/pipeline.py, run.py |
| the model | Claude Haiku (owner's pick) reads and judges; DeepSeek and NVIDIA NIM are wired in as fallbacks | clippr/config.py |
flowchart LR A[video on this computer
+ .srt or embedded subs] --> B[ingest
clean lines with times] B --> C[story finder
4 model passes] C --> D[cutter
9:16 · ≤20s · levelled] D --> E[music
CC track, ducked] D --> F[(output/<episode>/
dry + music + clips.json)] E --> F F --> G[the page
ranked cards · play · download · open folder] A -.-> G
how the AI decides (the four passes)
- read the episode — characters and their relationships, scenes with what is physically happening, and "threads": setups that might pay off later (a lie, a threat, a running joke).
- propose the moments — every candidate that passes the stranger test ("cover the rest of the episode, read only these lines: is it clear who wants what, and does it end on something?"), with a score 1–10, a hook (the first two seconds), and a line on why it works alone. Rules: 1–6 segments, ≤20s total, cut on subtitle lines only, a segment is a few lines, never a whole scene.
- tighten — anything over 20s once measured on real timings is handed back with the exact length of every line and a running total; up to two rounds.
- the judge — a fresh conversation that sees each candidate ONLY as its own lines, as a scrolling stranger would, and scores understand and interest 1–10. Under 6 on either → dropped, with the reason kept. The card's score is this cold-read score; the author's score is kept alongside.
sequenceDiagram participant P as clippr participant M as claude-haiku-4-5 P->>M: full transcript → "who is who, scenes, threads" M-->>P: characters + scenes P->>M: "every clip-worthy moment; stranger test; score, hook, why-alone" M-->>P: candidate stories P->>P: snap to real lines · merge touching segments · measure P->>M: "these are over 20s — here is every line's length; tighten or drop" (×≤2) M-->>P: tightened stories P->>M: fresh chat: "you never saw this show; here are the words of each clip — understand? interest?" M-->>P: verdicts → drop < 6, rank by cold-read score
how one clip gets made
- each segment: start 0.3s early, end 0.3s late (the padding shrinks automatically when the story is already near 20s)
- crop the middle 9:16 of the frame → 1080×1920 at 30 fps (subtitles are burned in only if you tick the box)
- segments joined with a 0.2s crossfade (picture and sound)
- dialogue levelled to −16 LUFS — streaming masters downmixed from 5.1 arrive near-silent otherwise
- a thumbnail at 0.5s, then the music version: track for the mood, looped/trimmed, faded, ducked under the speech, mixed
where the files live
output/<episode>/
01_dry.mp4 01_music.mp4 01_thumb.jpg ← ranked, 01 = best
02_…
clips.json ← everything the page shows (title, scores, judge, segments, files, music credit)
<episode>.eng.srt ← the subtitles that were used (extracted if they were embedded)
story/ pass1.json pass2.json pass3*.json pass4.json stories.json provider.json prompt_*.md
↑ every model answer, for prompt tuning: what was proposed, what was dropped and why
uploads/<episode>/ ← only for uploaded files; deleted after the run unless "keep the uploaded copy"
music/<mood>/ ← the free tracks (tense funny sad heartwarming epic chill neutral) + tracks.json credits
how to run
pip install -r requirements.txt
cp .env.example .env # ANTHROPIC_API_KEY
python tools/fetch_music.py --per-mood 3
start_clippr.cmd # Windows: double-click → starts the page + opens the browser
uvicorn web.app:app # or by hand → http://127.0.0.1:8000
python run.py --video "C:\path\episode.mkv" # same thing from a terminal; add --srt if there is no embedded track
python tools/make_sample.py # a synthetic 3-min demo episode if you have no real one at hand
where we are (2026-09-03, end of day 1)
verified (we watched it work)
- hosted demo is live and ran end to end — clippr.deskhand.ca on Render's free tier (Docker: python + ffmpeg), hosted mode (upload cap 250 MB, no server paths, no open-folder, notice on the page), a try the sample episode button: the sample finished in 142s (story 40s · one clip 80s · music 20s) at 540x960 — at 1080x1920 the 512 MB box was OOM-killed on the first cut. Repo on GitHub (
jacesabr/clippr, private), auto-deploys on push - end to end on a real episode — Silo S02E01, 614 MB MKV, no
.srtgiven: embedded English subtitles found among 43 tracks, English audio picked over the Italian default (confirmed by correlating the clip's loudness envelope with both source tracks: 1.000 vs 0.678), clips cut, levelled (−19.5 dB mean, −1.5 dB peak), playable on the page, frames checked by eye - the judge changes what ships — on that episode the author pass proposed 6; the cold read kept 3 (8.5 / 7.5 / 7.0, all setup+payoff) and dropped 3 with reasons like "what is 'it'? context missing"
- the page — drop zones for the video and an optional
.srt, a four-step stepper while it runs, ranked cards with the cold-read verdict / player / music switch / download / credit, "open folder" for the local output, fits a narrow window - synthetic demo —
tools/make_sample.py(TTS dialogue, scripted setup/payoff): the scripted story is found as #1 every run - toolchain — python 3.14, ffmpeg 8.1.1; free music from the Openverse API with no key (21 tracks cached)
measured
| what | number |
|---|---|
| story stage on a 45-min episode (4 passes, haiku) | ≈ 55s, ≈ $0.05 |
| cutting + music | ≈ 15–20s per clip (HEVC 1080p source) |
| whole run, real episode | 2.5–4 min |
| deepseek direct | 402 — account empty (owner: keep haiku) |
| NIM deepseek-v4 (free) | 464s for two passes — last resort only |
| hosted free tier (Render, 0.1 CPU / 512 MB) | sample episode 142s end to end at 540x960; 1080x1920 = OOM |
known limits / not built
- crop is centre-only: faces in wide two-shots can sit off-centre (face-aware crop is the next step)
- bitmap subtitle tracks (PGS / VobSub, Blu-ray rips) cannot be read — a real
.srtis needed - the model varies run to run at temperature 0.3; a quiet episode (168 lines) yields 3 strong clips, a chatty one will yield more
- one user, one machine: a single background thread per run, no queue
decisions to talk about
- quantity vs. quality: the judge threshold is 6/10 on both understand and interest; lower it (
CLIPPR_JUDGE_MIN) to ship more, raise it to ship only the best - padding 0.3s each side vs. total; music bed level (0.35 before ducking reads as "present but subtle")
- music licences: openverse pulls cc0 / by / by-sa / by-nd; by-nd under a video is a derivative → cc0 + by only, or a paid catalogue, before selling
- uploaded copies are deleted after the run by default (files are large and already on disk); the checkbox keeps them
- how do people run this (2026-09-04): browser-only users need a server; the free Render tier only demos it. Next step proposed: E2B sandboxes do the processing per run (credits available), the browser uploads straight to the sandbox, Render only serves the page. Alternative for power users: a packaged desktop app (python + ffmpeg in one zip). Browser-side ffmpeg.wasm rejected (single-threaded, 2 GB ceiling, weak HEVC).
- git:
jacesabr/clippron GitHub (private), Render auto-deploysmain; every push is a deploy, so pushes stay explicit