Turn any YouTube video or podcast into ready-to-publish short-form clips in minutes.
ClipForge is a production-ready AI SaaS platform that automatically extracts viral clip candidates from long-form video, adds TikTok-style captions, applies branding, and publishes directly to YouTube Shorts.
- AI Clip Detection — Multi-model support (GPT-4o, Claude, Gemini) with 0–10 virality scores
- Auto Transcription — Local Whisper with word-level timestamps
- Smart Formatting — 9:16 vertical reformat with centre-crop or blur-pad
- TikTok Captions — Word-by-word karaoke burn-in, multiple style presets
- Watermarking — Logo overlay with configurable position + opacity
- BYOK — Encrypted API key storage, never exposed in responses
- YouTube Publishing — OAuth2 with draft/public toggle
- JWT Auth — Access + refresh tokens with per-user rate limiting
- React Frontend — Dark editorial UI with live progress polling
clipforge/
├── app/ # FastAPI backend
│ ├── agents/ # AI agents
│ │ ├── content_analyzer.py # Segments transcript into narrative blocks
│ │ ├── viral_detector.py # Detects clips + scores + generates metadata
│ │ └── prompts.py # All LLM prompts (edit here, not in agents)
│ ├── api/
│ │ ├── dependencies.py # Auth + rate limit dependencies
│ │ ├── error_handlers.py # Exception → HTTP response mapping
│ │ └── routes/
│ │ ├── auth.py # /auth/* (register, login, refresh)
│ │ ├── api_keys.py # /api/keys CRUD
│ │ ├── videos.py # /api/videos submit + status
│ │ ├── analysis.py # /api/videos/{id}/analyze
│ │ ├── clips.py # /api/clips generate + download
│ │ ├── publishing.py # /api/clips/{id}/publish + YouTube OAuth
│ │ └── health.py # /health, /health/ready
│ ├── models/
│ │ ├── database.py # Async engine + session factory
│ │ ├── tables.py # All ORM tables
│ │ └── tables_youtube.py # YouTube credential table
│ ├── services/ # Business logic
│ │ ├── youtube_validator.py
│ │ ├── downloader.py # yt-dlp
│ │ ├── audio_extractor.py # ffmpeg WAV extraction
│ │ ├── transcriber.py # Whisper (word-level)
│ │ ├── video_pipeline.py # Phase 1 orchestrator
│ │ ├── model_router.py # Unified LLM abstraction (OpenAI/Claude/Gemini)
│ │ ├── clip_processor.py # Phase 3 orchestrator
│ │ ├── clipper.py # ffmpeg cut + 9:16 reformat
│ │ ├── caption_generator.py# ASS subtitle file generator
│ │ ├── caption_burner.py # ffmpeg caption burn-in
│ │ ├── caption_style_config.py
│ │ ├── watermarker.py # Logo overlay
│ │ ├── ffmpeg_utils.py # Shared ffmpeg subprocess helpers
│ │ ├── auth_service.py # JWT + bcrypt
│ │ ├── encryption.py # Fernet AES-128 for API keys
│ │ ├── key_resolver.py # BYOK lookup + decrypt
│ │ ├── usage_logger.py # Token usage + cost tracking
│ │ ├── youtube_oauth.py # OAuth2 flow + token refresh
│ │ ├── youtube_uploader.py # Resumable upload
│ │ └── publishing_service.py
│ ├── utils/
│ │ ├── config.py # Pydantic settings
│ │ ├── exceptions.py # Custom exception hierarchy
│ │ └── logger.py # Structlog setup
│ └── main.py # FastAPI app factory
│
├── frontend/ # React + Vite SPA
│ ├── vite.config.js # /api proxy → localhost:8000
│ ├── package.json
│ └── src/
│ ├── App.jsx # Router + nav + toast
│ ├── store/index.js # Zustand global state
│ ├── services/api.js # Axios + auth interceptors
│ └── pages/
│ ├── LoginPage.jsx
│ ├── DashboardPage.jsx # URL submit + video list
│ ├── VideoPage.jsx # Clip studio
│ └── SettingsPage.jsx # API keys + YouTube
│
├── migrations/ # Alembic
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── unit/ # 21 unit test files
│ └── integration/ # E2E + load (RUN_INTEGRATION=1)
├── skills/
│ └── viral-clip-detector/ # Claude skill for prompt iteration
│ ├── SKILL.md
│ └── references/
│ ├── scoring-rubric.md
│ └── prompt-variants.md
├── storage/
│ ├── temp/ # Temporary download files
│ └── outputs/ # Final clip MP4s
├── scripts/setup.sh # One-command dev bootstrap
├── docker-compose.yml # Postgres + Redis
├── pyproject.toml
└── .env.example
| Tool | Min Version | Purpose |
|---|---|---|
| Python | 3.11+ | Backend |
| Node.js | 18+ | Frontend |
| Docker | 24+ | Postgres + Redis |
| ffmpeg | 6+ | Video processing |
| libass | any | Caption rendering |
# Ubuntu/Debian
sudo apt-get install ffmpeg libass-dev fonts-liberation
# macOS
brew install ffmpeggit clone https://github.com/Omar-Tahir/clipforge.git
cd clipforge
bash scripts/setup.shThis installs deps, generates a Fernet key, starts Docker services, and runs migrations.
# Core (auto-populated by setup.sh — review and adjust)
DATABASE_URL=postgresql+asyncpg://clipforge:clipforge@localhost:5432/clipforge
DATABASE_URL_SYNC=postgresql://clipforge:clipforge@localhost:5432/clipforge
APP_SECRET_KEY=change-me-in-production
FERNET_KEY=<auto-generated>
# YouTube publishing (get from Google Cloud Console)
YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
YOUTUBE_REDIRECT_URI=http://localhost:8000/auth/youtube/callbacksource .venv/bin/activate
uvicorn app.main:app --reload
# → http://localhost:8000
# → http://localhost:8000/docs (Swagger UI)cd frontend
npm install
npm run dev
# → http://localhost:3000| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL async URL |
DATABASE_URL_SYNC |
✅ | PostgreSQL sync URL (Alembic) |
REDIS_URL |
✅ | Redis URL |
APP_SECRET_KEY |
✅ | JWT signing secret (32+ chars) |
FERNET_KEY |
✅ | AES-128 key for API key encryption |
YOUTUBE_CLIENT_ID |
Publishing | Google OAuth client ID |
YOUTUBE_CLIENT_SECRET |
Publishing | Google OAuth client secret |
YOUTUBE_REDIRECT_URI |
Publishing | OAuth callback URL |
STORAGE_BACKEND |
No | local (default) or s3 |
SENTRY_DSN |
No | Error tracking |
Generate a Fernet key manually:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"| Method | Endpoint | Body | Response |
|---|---|---|---|
| POST | /auth/register |
{email, password} |
{access_token, refresh_token} |
| POST | /auth/login |
{email, password} |
{access_token, refresh_token} |
| POST | /auth/refresh |
{refresh_token} |
{access_token, refresh_token} |
| GET | /auth/me |
— | {id, email} |
# 1. Submit video
POST /api/videos/submit { "url": "https://youtube.com/watch?v=..." }
# 2. Poll until status = "completed"
GET /api/videos/{video_id}/status
# 3. Run AI detection (pass your API key)
POST /api/videos/{video_id}/analyze
{ "provider": "claude", "api_key": "sk-ant-...", "max_clips": 5 }
# 4. Generate processed clips
POST /api/clips/generate
{ "clip_ids": ["..."], "caption_style": "tiktok", "watermark_enabled": true }
# 5. Poll until clip status = "completed"
GET /api/clips/{clip_id}/status
# 6. Download
GET /api/clips/{clip_id}/download → MP4 file
# 7. Publish to YouTube (requires /auth/youtube/connect first)
POST /api/clips/{clip_id}/publish { "draft": true }
See /docs for the full interactive Swagger UI.
| Dimension | Weight | Measures |
|---|---|---|
| Hook strength | 35% | First 3 seconds — stop-scroll factor |
| Emotional resonance | 30% | Laugh, surprise, inspiration, anger |
| Shareability | 20% | "Send this to a friend" factor |
| Pacing | 15% | Information density for Shorts |
Score thresholds:
9–10— Guaranteed viral (rare)7–8— High potential, publish these5–6— Decent, missing one ingredient< 5— Poor fit for short-form
| Style | Font | Best for |
|---|---|---|
tiktok |
Arial Black | Bold karaoke, yellow highlight (default) |
minimal |
Helvetica | Professional, educational |
bold_yellow |
Impact | High-energy, entertainment |
users id, email, hashed_password, is_active
api_keys id, user_id, provider, encrypted_key, label
videos id, user_id, youtube_url, transcript (JSONB), status
clips id, video_id, start/end_time, virality_score,
ai_title, ai_description, ai_hashtags, output_path, status
usage_logs id, user_id, provider, input/output_tokens, estimated_cost_usd
publishing_logs id, clip_id, youtube_video_id, status, metadata_used
youtube_credentials id, user_id, encrypted_access_token, encrypted_refresh_token
# All unit tests (no network, no DB required)
pytest tests/unit/ -v
# With coverage
pytest tests/unit/ --cov=app --cov-report=term-missing
# Integration tests (needs ffmpeg + network + running DB)
RUN_INTEGRATION=1 pytest tests/integration/ -v# After editing app/models/tables.py
alembic revision --autogenerate -m "describe change"
alembic upgrade head
# Rollback
alembic downgrade -1- Google Cloud Console → Enable YouTube Data API v3
- OAuth consent screen → add scope
youtube.upload - Create OAuth 2.0 Web Application credentials
- Authorised redirect URI:
http://localhost:8000/auth/youtube/callback - Add
YOUTUBE_CLIENT_ID+YOUTUBE_CLIENT_SECRETto.env
- API keys encrypted with Fernet (AES-128-CBC + HMAC-SHA256) — DB dump is useless without
FERNET_KEY - Passwords hashed with bcrypt (work factor 12)
- Decrypted keys exist only in memory at request time, never logged
- Rate limiting: 3 jobs/minute, 20 jobs/hour per user
- JWT access tokens expire in 30 minutes, refresh tokens in 30 days
| Layer | Tech |
|---|---|
| Backend | FastAPI, SQLAlchemy (async), Alembic |
| Database | PostgreSQL 16, Redis 7 |
| AI | Whisper (local), OpenAI, Anthropic, Google Gemini |
| Video | ffmpeg, yt-dlp, libass |
| Auth | python-jose, passlib, cryptography |
| Frontend | React 18, Vite, Zustand, Axios, React Router |
| Publishing | Google OAuth2, YouTube Data API v3 |
MIT