Turn a long interview into publish-ready vertical clips: cut on sentence boundaries, reframed to 9:16, karaoke subtitles burned in.
Project description
🎬 shortsmaker
A long interview goes in. Publish-ready vertical clips come out.
Cut on sentence boundaries · reframed to 9:16 by body tracking · karaoke subtitles burned in
uvx shortsmaker run interview.mp4
A 15-minute interview becomes four or five postable clips in about five minutes, on a laptop with no GPU, for roughly $0.10 of API credit.
▶ Watch an example clip —
21 seconds, straight out of shortsmaker run, not retouched.
What it is for
The source it is built for is a filmed talking-head interview: a podcast, a long-form YouTube interview, a panel. One or two people, a static camera, a multi-camera edit.
Within that, it does one thing well: it finds the passages worth clipping, cuts them so they open and close on whole sentences, keeps the speaker in frame at 9:16, and burns in captions.
It is not a general-purpose video editor. On a vlog, a product demo or a sketch, the picture carries meaning that this pipeline deliberately ignores — it sends the model the audio, never the video. Expect poor results, and see SPEC.md for why that trade was made.
Install
Requires Python 3.12+, uv, and FFmpeg built with libass
(apt install ffmpeg, brew install ffmpeg — the distro build is fine).
uvx shortsmaker run interview.mp4 # run it without installing anything
uv tool install shortsmaker # or install the command for good
The font and the pose model are vendored inside the package: nothing is downloaded on first run.
Configure
Two keys, read from environment variables:
export OPENAI_API_KEY=sk-... # Whisper — the transcript
export GEMINI_API_KEY=AIza... # Gemini — choosing the passages
Get them from platform.openai.com and aistudio.google.com.
The tool never reads a .env file, and that is deliberate: a CLI runs in whatever directory you
happen to be standing in, so a tool that loads .env from the working directory would silently pick up
a file you did not know was there. Keys come from the environment, and how they get there is your
business — export, a CI secret, a container's --env-file, or direnv loading a
.envrc when you cd into the project. For local work, direnv is the comfortable answer; copy
.envrc.example and run
direnv allow.
What it costs
Only two of the five stages call a paid API, and both cache their result — so re-rendering a dozen times while you tune the look costs nothing.
| Source length | Whisper | Gemini | Total |
|---|---|---|---|
| 15 minutes | ~$0.09 | a few cents | ~$0.10 |
| 1 h 40 | ~$0.61 | a few cents | ~$0.65 |
Whisper is billed on audio duration ($0.006/min). Gemini is negligible beside it, because it is sent the audio and the transcript — never the video.
No key, no spend: --mock fakes both APIs with realistic, deterministic data, and the full pipeline
runs offline. It is how the test suite runs, and the fastest way to see what the tool actually does.
shortsmaker run interview.mp4 --mock
Use
shortsmaker run interview.mp4 # everything → clips/
shortsmaker run interview.mp4 -n 3 # keep only the 3 best-scoring passages
shortsmaker run interview.mp4 -f 1:1 -l en # square, English
What you get
clips/
01-s0.94-le-vrai-luxe.mp4 ← index, score, and the title the model wrote
02-s0.88-pourquoi-il-a-dit-non.mp4
03-s0.71-UNANCHORED-la-premiere-usine.mp4
.shortsmaker/ ← intermediates, cached, safe to delete
transcript.json analysis.json segments.json reframe/
The filenames make a directory listing a ranking: index, score, title. A clip flagged UNANCHORED is
one whose quote could not be located in the transcript, so its bounds fell back to the model's rough
guess — expect a ragged cut. It is kept and flagged, never dropped silently, because the decision to
throw a clip away is yours.
Options
| Default | ||
|---|---|---|
-f --format |
9:16 |
9:16 TikTok/Reels/Shorts · 4:5 Instagram · 1:1 LinkedIn/X · 16:9 keep the source shape (no crop, tracking skipped) |
-l --language |
fr |
The spoken language, and the language of the generated titles. Also en, es, de, it, pt, nl. |
-n --clips |
all | Keep only the N best-scoring passages. |
--min-score |
none | Keep only passages scoring at least this. Combines with -n. |
-o --out |
clips/ |
Where the finished clips land. |
-w --work-dir |
.shortsmaker/ |
Where intermediates are cached. |
-j --jobs |
auto | Clips rendered in parallel. |
--mock |
off | Fake the APIs. No key, no cost. |
--force |
off | Ignore the cache and re-run the stage — including the paid ones. |
-v --verbose |
off | Show the underlying ffmpeg commands and the raw API traffic. |
shortsmaker --help works on every command, and every default carries its reasoning.
One stage at a time
The pipeline is five stages, and they are separate commands for one reason: only the first two cost money. They cache, so everything downstream can be re-run for free.
shortsmaker transcribe interview.mp4 # Whisper → transcript.json ($, cached)
shortsmaker analyze interview.mp4 # Gemini → analysis.json ($, cached)
shortsmaker show interview.mp4 # print what was found, with scores — nothing rendered
shortsmaker reframe interview.mp4 # MediaPipe → the crop plan
shortsmaker render interview.mp4 # FFmpeg → clips/
Re-selecting and restyling never call an API:
shortsmaker analyze interview.mp4 --min-score 0.8 # change your mind — free
shortsmaker render interview.mp4 --size 96 --accent "#00E5FF" # restyle — free
analyze writes everything it found to analysis.json and never narrows it; -n and --min-score
select from that. A stricter filter is a re-read, not a re-analysis.
As a library
Importing the package does not turn it into a CLI: nothing is written to stdout, and the stages log
through the standard logging module, silent until your application adds a handler.
from shortsmaker import Paths, Settings
from shortsmaker.stages import analyze, transcribe
transcript = transcribe.transcribe(paths, Settings())
segments = analyze.analyze(paths, transcript, Settings(), clips=3)
See the package docstring for the full loop.
How it works
Gemini decides what to cut. Whisper decides when. Neither is asked to do the other's job.
Gemini reliably picks the right moments but cannot place them in time — its timestamps drift by around two seconds, enough to open a clip mid-sentence. So it is never asked for a timestamp. It quotes the words, and the quote is looked up in Whisper's word-level transcript. Cut points land on real word boundaries by construction rather than by luck.
The reframe tracks bodies, not faces — a face vanishes in profile, a body does not — decides per shot rather than per frame, and never guesses who is speaking: two people in frame means both are shown.
→ SPEC.md has the flowchart and the reasoning, stage by stage. Four spikes were built and measured before any of this was written, and nearly every rule in the code replaced one that made the clips worse.
Performance
Measured on a 16-core CPU with no GPU: reframing runs at 0.71× realtime, rendering at 0.45×, so local processing costs roughly 1.2× the source duration. Transcription and analysis are network-bound — about 90 seconds for a 15-minute video.
Development
uv sync # runtime deps + pytest
uv run pytest # tests
uvx ruff@0.15.21 check . # lint
uvx ty@0.0.58 check # types
uvx bandit@1.9.4 -c pyproject.toml -r src # security
These are, verbatim, the four commands CI runs — green here means green there. The conventions they enforce, and the traps worth knowing before changing anything, are in CLAUDE.md.
What changed between versions: CHANGELOG.md. To cut a release: PUBLISHING.md — the short version is push a tag.
Licence and credits
The code is MIT — see LICENSE.
The example clip is not. It is a short excerpt of an interview with Bernard Arnault from Legend (Guillaume Pley), reproduced solely to illustrate what the tool outputs. All rights remain with its owners; it is not covered by this project's licence and is not redistributable as part of it.
Vendored assets carry their own licences: Lato (Łukasz Dziedzic, SIL Open Font License 1.1) and MediaPipe's pose_landmarker (Google, Apache 2.0).
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shortsmaker-0.2.0.tar.gz.
File metadata
- Download URL: shortsmaker-0.2.0.tar.gz
- Upload date:
- Size: 24.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57bd4d77e01c15039903943993af3ccc8e17db8147b7bffc01970871f22608c1
|
|
| MD5 |
874a6ce4f8db46a12f80de87192a19b3
|
|
| BLAKE2b-256 |
93a4d57a56c70e20efd85934dbc2407e2d0d2c83ed666a2bd88dd224449772e7
|
Provenance
The following attestation bundles were made for shortsmaker-0.2.0.tar.gz:
Publisher:
publish.yml on pirocheto/shortsmaker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shortsmaker-0.2.0.tar.gz -
Subject digest:
57bd4d77e01c15039903943993af3ccc8e17db8147b7bffc01970871f22608c1 - Sigstore transparency entry: 2164987565
- Sigstore integration time:
-
Permalink:
pirocheto/shortsmaker@af4c4d0de027835e77dd8b2ecc6e8e2dfd478352 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pirocheto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@af4c4d0de027835e77dd8b2ecc6e8e2dfd478352 -
Trigger Event:
push
-
Statement type:
File details
Details for the file shortsmaker-0.2.0-py3-none-any.whl.
File metadata
- Download URL: shortsmaker-0.2.0-py3-none-any.whl
- Upload date:
- Size: 5.3 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efc8a086605abbe80e3286077f6a9c5c08a223819506cb9817c9185f8b07e512
|
|
| MD5 |
4c90b5cb6fb788ba512a36aa7236b48f
|
|
| BLAKE2b-256 |
3b47bc686c25c34767a27fc324c9cc3d189a2da71a385a6b4aafc8c785e035f6
|
Provenance
The following attestation bundles were made for shortsmaker-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on pirocheto/shortsmaker
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shortsmaker-0.2.0-py3-none-any.whl -
Subject digest:
efc8a086605abbe80e3286077f6a9c5c08a223819506cb9817c9185f8b07e512 - Sigstore transparency entry: 2164987574
- Sigstore integration time:
-
Permalink:
pirocheto/shortsmaker@af4c4d0de027835e77dd8b2ecc6e8e2dfd478352 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/pirocheto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@af4c4d0de027835e77dd8b2ecc6e8e2dfd478352 -
Trigger Event:
push
-
Statement type: