creative/cut-talking-takes/SKILL.md

name: cut-talking-takes description: >- Cut raw talking-head video (retakes of the same lines, OR a full walkthrough of a whole script) into a tight, modern jump-cut assembly — transcribe locally, drop dead footage/fumbles/false-starts/redundant re-explanations, keep the clean takes, and preserve 10-bit HDR color. Use when the user drops a .MOV/.mp4 of intro/script takes and asks to "cut the dead footage", "put together the best takes", or assemble a tight cut.

cut-talking-takes

Raw take footage = one locked-off camera, the same line(s) attempted many times, with warmups, throat-clears, false starts, and long resets between. Goal: a tight jump-cut of only the clean, complete takes, with all dead air gone.

takecut.py (in this dir) does the deterministic parts. Picking which takes are clean/best is editorial — that's the agent's job, done by reading the take map.

Two footage shapes (tell them apart first)

  1. Intro retakes — the same one or two lines attempted ~20×. Group by beat, take the last clean take of each, butt-join. Offer both a selects-reel (all clean takes) and a best cut (one take per beat).
  2. Full-script walkthrough — one pass of the whole video, hook → build → outro, mostly delivered continuously with retake clusters at the hook and the outro and the occasional restart mid-body. Here: keep the continuous good run, apply "last clean take" only at the retake clusters, and cut the redundant re-explanations — people often explain the same beat twice (a detailed pass and a tighter recap, or restate "wrap it in a UI" two ways). Keep the clearer/more-complete one, drop the dupe. Deliver one full-cut.mov. (If several sources cover the same script, offer to compare and pick a master — the latest take is usually the tightest.)

Hard-won facts (don't relearn these)

  • Cut points come from energy, not words. whisper word-end times drift up to ~1s; ffmpeg silencedetect is the truth. Snap every in/out into the surrounding silence so no consonants clip. takecut.py does this (-38dB, 0.30s).
  • iPhone footage is 10-bit HEVC, BT.2020, HLG (arib-std-b67) HDR. You MUST carry the color tags or the cut looks washed out / dark.
  • takecut.py encodes with libx265. hevc_videotoolbox does open 10-bit HLG/BT.2020 sessions on this Mac (verified 2026-09-02 at 4K), so the software encoder is a script choice, not a hardware limit — a candidate speed-up for the script, not a rule to carry. prores_videotoolbox works but is ~193 Mbps.
  • Map only [v][a] — iPhone .MOV carries extra data streams that break naive concat.
  • "Best take" means on-voice: dry, concise, no hype. If the project keeps a voice or positioning doc, read it before picking.

Workflow

  1. Analyze (transcribe + take map):

    python3 ~/.claude/skills/cut-talking-takes/takecut.py analyze /path/clip.MOV
    

    Prints a table Tn IN OUT dur text and writes <clip>_takes/takes.json. Warmups/restarts are obvious (same phrase repeated; very short fragments).

  2. Pick the clean takes. A take is clean = complete thought, no false start, no repeated word, no mid-sentence throat-clear, no trailing flub. Group the takes by which line/"beat" they are (the footage is usually the same few sentences attempted over and over).

    • DEFAULT SELECTION RULE: take the LAST clean/complete take of each beat — that's almost always the one they're happy with. Walk the beats in order, pick the final complete attempt of each, and string them in narrative order.
    • Exceptions to "last": if the last take starts mid-sentence / breaks a referent, or is a redundant terse recap of a beat already delivered better earlier, prefer the clean complete earlier take (or splice an anchor in front). Last is the default, not a law.
    • Truncated text ≠ incomplete take. large-v3-turbo makes the takes.json labels reliable, but a label can still look clipped where whisper timestamp drift cuts the window text — the window in/out are accurate and usually already contain the whole sentence. Before discarding a take as "trailing off", re-transcribe its exact window — it's usually complete: ffmpeg -ss A -to B -i clip -ar 16000 -ac 1 x.wav && whisper-cli -m MODEL -f x.wav -nt. (sentences.srt is fine for reading content, but its timestamps drift — never cut on them.)
    • Trailing/leading flub or a dangling pronoun? Trim with an explicit in/out: find the gap with a fine pass ffmpeg -ss A -to B -i clip -af silencedetect=noise=-34dB:d=0.12 -f null - and cut inside it. (e.g. a take that opens "I used it…" needs the prior "I used this framework called X" take spliced in front so "it" has a referent.)
    • Common trim targets: a leading throat-clear or false start ("And then you can also give it some—"), a trailing connector ("…and then it's working. So yeah,"), doubled CTA tails ("give this a shot… check this, give this a shot"), and stray edge blips — when a window's duration is much longer than its content, there's often a false-start syllable at the head or a mumble at the tail; cut it off.
    • Two silence thresholds: analyze groups takes at -38dB:0.30; for fine trim edges inside a take use -34dB:0.12 (catches word/sentence gaps the coarse pass merges).
  3. QC framing (optional but cheap): extract a mid-frame per candidate take, tonemap for viewing, and tile a contact sheet to confirm eye-contact/framing: -vf "zscale=t=linear:npl=100,tonemap=hable,zscale=p=bt709:t=bt709:m=bt709,format=yuv420p,scale=303:540".

  4. Render the assembly (tight butt-cuts, HDR preserved):

    # by explicit seconds (use for flub trims / custom order):
    python3 .../takecut.py render /path/clip.MOV --segments "74.1-80.9,372.06-382.77" --out best-intro.mov
    # or by take id from the map:
    python3 .../takecut.py render /path/clip.MOV --workdir /path/clip_takes --keep T3,T8 --out reel.mov
    

    Default deliverables to offer: a selects-reel (all clean takes, chronological) and a best cut (single strongest take of each line). Always write a TAKES.md next to the outputs — final wording, per-beat source timecodes, what was dropped and why, offered swaps — the user uses it to request swaps.

    Tight modern pacing — --tighten. By default render only tightens between segments; the speaker's pauses inside a kept span stay verbatim. Fine for intro retakes (short spans), but it leaves the long full-video spans loose. Add --tighten to cap every pause inside each span at --max-pause seconds (default 0.30): pauses ≤ that pass through (natural rhythm kept), longer dead gaps collapse to it. Cuts land inside detected silence, so no words clip and no clicks.

    python3 .../takecut.py render clip.MOV --segments "..." --tighten --out full-cut-tight.mov
    python3 .../takecut.py render clip.MOV --segments "..." --tighten --max-pause 0.22 --out tighter.mov
    
    • --max-pause guide: 0.30 natural-but-tight (default), 0.20–0.25 snappy/aggressive, 0.38–0.42 relaxed (only kills the long dead air). It's a uniform cap (no special sentence-end allowance), so below ~0.18 starts to sound breathless.
    • Locked-off camera makes the micro jump-cuts read as intentional; jumpier on handheld.
    • Worth it mainly on full-video passes; intro retakes barely change. Keep both the natural and tightened renders so the user can compare.
  5. Verify end-to-end: re-transcribe the finished cut and confirm every line survived complete, in order, and flubs are gone.

    • False-alarm watch: the full-cut transcription occasionally drops a clause right after a hard cut (whisper artifact at the boundary) even though the audio is fine. If a line looks missing, re-transcribe just that tail/segment (or the source window) before re-cutting — the audio is almost always present.

Output location

Put outputs in a new folder next to the source (e.g. ~/Downloads/<slug>-cut/), not in the Obsidian vault (too large). Outputs are 10-bit HEVC .mov, ~6 Mbps, play in QuickTime and drop straight into an editor.

Notes

  • Model: ggml-large-v3-turbo (~1.6 GB). This machine keeps whisper models in ~/Models/whisper.cpp/ (shared with footage-index); takecut.py looks there first, then ~/.cache/whisper-cpp/, and downloads into ~/Models/whisper.cpp/ if neither has it. WHISPER_MODEL=<path> overrides the search. ggml-small.en.bin in the same dir trades label accuracy for speed. Needs brew install whisper-cpp + ffmpeg.
  • Transcribes once (word-level); the readable sentences.srt is rebuilt from those words in-code, not a second decode.
  • Tighter cuts: lower --head/--tail in analyze (defaults 0.08 / 0.12s of breath).
  • Non-HDR / 8-bit sources are handled too — color flags adapt to the source tags.
  • Dolby Vision: iPhone clips carry a DOVI profile-8.4 RPU; libx265 drops the RPU but keeps the HLG/BT.2020 base layer, so it still looks correct. Don't panic about "losing DV".
  • Render time: render decodes the whole source once per run, so a 7-min clip takes ~3–5 min. Run it in the background and poll.