Turn a long YouTube video into upload-ready 9:16 vertical Shorts — AI clip selection, smart reframing, animated captions, background music, watermarking, and direct YouTube publishing. Runs entirely on your own machine; no web service, no data leaves your PC except the API calls you configure.
You can drive it three ways:
- CLI — one command, fully automatic, resumable.
- Web editor — a local React UI with step-by-step control and instant preview.
- Claude — talk to Claude Desktop or claude.ai, which drives the pipeline itself via an MCP server (~18 tools) and a bundled Skill that does creator research and SEO metadata for you.
flowchart LR
A["1. Transcript\n(YouTube captions)"] --> B["2. AI Clip Selection\n(Gemini 2.5 Flash)"]
B --> C["3. Download\n(yt-dlp, up to 1080p)"]
C --> D["4. Reframe to 9:16\n(YOLO11 + EMA camera)"]
D --> E["5. Captions\n(Groq Whisper large-v3)"]
E --> F["6. Render\n(FFmpeg NVENC)"]
F --> G["Optional: Upload\n(YouTube OAuth)"]
Every step is cached in a per-video state.json, so an interrupted or re-run job skips whatever's already done.
Clip selection
- Gemini 2.5 Flash scores and picks the best moments from the transcript (hook, viral signals, natural start/end points, self-contained context) — or skip Gemini and hand the pipeline your own timestamps (
set_custom_clipsvia Claude, or type them into the web editor). - Auto-generated per-platform metadata: YouTube title/description/tags, Instagram caption, TikTok caption.
Reframing (long → 9:16)
- YOLO11n person detection drives a lock-and-snap camera: static by default, only moves when a detection confirms real movement — no jittery micro-adjustments.
- SOLO mode for one speaker, SPLIT mode (stacked halves, divider line) when two people are far enough apart on screen.
- Scene-cut detection resets tracking instantly on hard cuts instead of drifting toward the wrong subject.
Captions
- Word-level timestamps from Groq Whisper (
large-v3), burned in as animated ASS subtitles. - Five styles:
pop(scale + color pop per word),karaoke(classic fill),classic(plain),magnify(grow while spoken),oneword(one word at a time, large). - Optional AI-picked emoji per caption line (Claude picks these directly when driving via MCP; a Groq/Llama call does it in the web/CLI path).
- Built-in Hinglish support: a
rewrite_captionstool exists specifically to romanize Hindi (Whisper often returns Devanagari) into Latin-script Hinglish so captions never render in a broken script.
Polish
- Background music: a categorized library (
hype/motivational/chill/funny) undermusic/, auto-picked or manually assigned per clip, volume-mixed under the voice. - Watermark overlay: image or text, any of 4 corner positions, adjustable opacity/scale.
- Horizontal mirror/flip, applied before captions are burned in so on-screen text never reads backwards.
- Fast 15-second NVENC preview render before committing to the full-length final render.
Publishing
- Direct YouTube upload via OAuth2 (resumable chunked upload).
- Batch scheduling: clips auto-publish one after another, spaced a random 15–30 min apart, using a persistent global cursor so a new batch always queues after whatever the last batch scheduled — even across separate runs.
- Fetch-merge-update editing of already-uploaded videos (only the fields you pass are changed).
Everything is resumable — state.json per video tracks transcript, clip list, source video path, and each clip's status (pending → reframing → reframed → captioning → captioned → previewed → rendering → finalized).
clipper/
├── main.py # CLI entry point (the 6-step pipeline)
├── app.py # FastAPI backend — one-shot + phase endpoints + MCP mount + YouTube OAuth
├── mcp_server.py # MCP tool definitions/handlers, mounted at POST /mcp in app.py
├── config.py # Loads .env → API keys
├── pipeline/
│ ├── transcript.py # YouTube caption fetch
│ ├── analyzer.py # Gemini clip selection + metadata generation
│ ├── downloader.py # yt-dlp download
│ ├── reframe.py # YOLO11 + EMA camera → 9:16 crop (SOLO/SPLIT)
│ ├── captions.py # Groq Whisper + ASS subtitle generation
│ ├── renderer.py # FFmpeg final/preview render (captions + BGM + watermark + mirror)
│ ├── music.py # Background-music library lookup
│ ├── state.py # video_id-keyed state.json read/write helpers
│ └── uploader.py # YouTube OAuth + resumable upload + scheduling cursor
├── routers/
│ └── upload.py # /api/upload/* — batch YouTube upload/schedule endpoints
├── skills/
│ └── clipping-youtube-shorts/ # Claude Skill: pipeline call order, research phase, SEO rules
├── frontend/ # React + Vite editor UI
│ └── src/pages/
│ ├── Dashboard.jsx # Video list
│ └── VideoPage.jsx # 6-step wizard: Analyze → Download → Reframe → Captions → Export → Upload
├── music/ # BGM library (hype/motivational/chill/funny subfolders)
├── watermarks/ # Drop your own watermark PNGs here
├── output/ # Generated per-video: state.json, clips, temp files (gitignored)
├── yolo11n.pt # YOLO11 nano weights (checked into the repo — no separate download needed)
└── requirements.txt
| Requirement | Notes |
|---|---|
| Python 3.11 | Developed/tested on 3.11; other 3.10+ versions likely work but aren't verified. |
| ffmpeg | Must be on PATH. On Windows, the code also checks a WinGet fallback install path automatically. |
| NVIDIA GPU with NVENC | Required, not optional. Every FFmpeg encode call in this codebase is hardcoded to h264_nvenc — there is no CPU/libx264 fallback path. Without an NVIDIA GPU (with current drivers), the cut/reframe/render steps will fail. |
| CUDA (via PyTorch) | Used for YOLO inference; this part does fall back to CPU automatically if unavailable, but will be much slower. |
| Node.js | Only needed if you're running the web editor's frontend. |
| Gemini API key | Required — clip selection + metadata generation. |
| Groq API key | Required — Whisper transcription + emoji picks. |
cd clipper
pip install -r requirements.txt
cp .env.example .envEdit .env and fill in:
GEMINI_API_KEY=... # required — Google AI Studio: https://aistudio.google.com/apikey
GROQ_API_KEY=... # required — https://console.groq.com/keys
ANTHROPIC_API_KEY=... # currently unused by the code — safe to leave blank
API_KEY= # optional — only needed if exposing the backend publicly (see below)
yolo11n.pt is already committed to the repo, so no separate model download step is needed.
If you'll be using the web editor, also install the frontend deps:
cd frontend
npm installIf you'll be using YouTube upload, see YouTube upload setup below — it's not required for clipping/captioning, only for the publish step.
cd clipper
python main.py "https://www.youtube.com/watch?v=VIDEO_ID" --clips 5 --duration 60Common flags:
| Flag | Default | Purpose |
|---|---|---|
--clips N |
5 |
Number of clips to produce |
--duration S |
60 |
Target clip length in seconds |
--output DIR |
./output |
Output directory |
--force |
off | Discard cached state.json, rerun everything |
--no-captions |
off | Skip transcription + caption burn-in |
--style |
pop |
pop | karaoke | classic | magnify | oneword |
--font |
Montserrat ExtraBold |
Caption font |
--size |
72 |
Caption font size |
--color |
&H0000FFFF |
Highlight color — web hex (#FF0000) or raw ASS format |
--y-pos |
1650 |
Vertical caption position (0–1920) |
--emojis |
off | Enable AI-generated emoji overlays |
Output lands in output/<video_id>/clip_01.mp4, clip_02.mp4, etc.
# terminal 1 — backend, from clipper/
uvicorn app:app --host 127.0.0.1 --port 8000 --reload
# terminal 2 — frontend, from clipper/frontend/
npm run devOpen the Vite dev server URL (default http://localhost:5173). The wizard walks through: Analyze (transcript + clip picks + metadata) → Download → Reframe (editable clip timestamps) → Captions (live style/color/font controls) → Export (preview render, then final render with per-clip BGM/mirror overrides) → Upload (YouTube).
Run the backend (uvicorn app:app ... as above). The MCP server is mounted at POST /mcp on the same FastAPI app — no separate process to start.
- Claude Desktop: point an MCP connector at
http://127.0.0.1:8000/mcp. - claude.ai (remote connector): expose the backend publicly (e.g. via
ngrok http 8000), setAPI_KEYin.envto a strong random string first, and point the connector at your public/mcpURL. The backend implements a minimal OAuth handshake (/authorize,/token) that claude.ai's connector flow expects.
Once connected, just paste a YouTube URL and ask for shorts — the bundled Skill (skills/clipping-youtube-shorts/) tells Claude the required tool call order, makes it research the creator/topic for SEO before picking clips, enforces the Latin-alphabet/Hinglish caption rule, and defaults every render to mirrored output. Claude drives the same 18 MCP tools (fetch_transcript, analyze_clips, set_custom_clips, trigger_pipeline, get_video_status, set_clip_music, rerender_clip, upload_clip, schedule_uploads, etc.) that back the CLI and web UI.
Only needed for the publish/schedule features — clipping and captioning work without it.
- Go to Google Cloud Console → APIs & Services → Credentials.
- Create an OAuth 2.0 Client ID (type: Web application), with an authorized redirect URI of
http://localhost:8000/api/youtube/oauth-callback. - Download the JSON and save it as
clipper/client_secrets.json. - Start the backend, then either:
- click "Connect YouTube Account" in the web editor's Upload tab, or
- call
GET /api/youtube/auth-url, visit the URL, approve, and the callback will storeclipper/token.jsonautomatically.
Both client_secrets.json and token.json are gitignored — never commit them, they contain live OAuth credentials.
Environment variables (.env, see .env.example):
| Variable | Required | Purpose |
|---|---|---|
GEMINI_API_KEY |
Yes | Clip selection + metadata (Gemini 2.5 Flash) |
GROQ_API_KEY |
Yes | Whisper transcription + emoji picks |
ANTHROPIC_API_KEY |
No | Present in .env.example; not currently referenced by any code path |
API_KEY |
No | Bearer token gate for the backend — only relevant when exposing it beyond localhost (e.g. for claude.ai) |
Caption styles (--style / CaptionSettings.style): pop, karaoke, classic, magnify, oneword — see Features above for what each looks like.
Reframe tuning constants (pipeline/reframe.py, edit directly if you want to retune): CROP_W / CROP_W_SPLIT (crop widths), DETECT_EVERY (YOLO sampling rate), EMA_ALPHA (camera smoothing), SCENE_CUT_THRESH, SPLIT_MIN_SEPARATION, SPLIT_OVERLAP_THRESHOLD.
ffmpeg not found— install FFmpeg and ensure it's onPATH. On Windows the code also checks the WinGet install location as a fallback.- FFmpeg exits immediately / NVENC errors — you need an NVIDIA GPU with a current driver; there's no CPU encode fallback in this codebase (see Prerequisites above).
- yt-dlp HTTP 403 on download — YouTube changes frequently break older yt-dlp versions. Run
pip install -U yt-dlpand retry. - "No transcripts available" — the source video has no YouTube captions (manual or auto-generated) in any language; this pipeline depends on YouTube's own captions, it does not transcribe the full source video.
- Resuming a broken run — just re-run the same command/URL with the same
--clips/--duration; it'll pick up fromstate.json. Use--forceto discard the cache and start over.