Skip to content

rlmsinclair/montage_maker

Repository files navigation

🎬 Montage Maker

AI gameplay-highlight engine. Ingests gameplay footage, detects every kill by reading the game's own kill log (OCR), marks them on an editable timeline, and lets you delete or speed up the dead air — or auto-build a punchy TikTok montage in one click — before rendering.

Game-agnostic by design — Battlefield 6 is just the first game profile.

Architecture

core/        pure Python engine (no web, no game specifics)
  games/     ★ pluggable game profiles — add a game = drop a profile.json
  detect_killfeed.py   read the kill log (OCR) -> kills
  segment.py           kills -> kept windows + dead-air gaps
  montage.py           render the edited EDL with ffmpeg
  pipeline.py          orchestration · ffmpeg_utils.py · events.py · eval.py
api/         FastAPI: upload, analyze (SSE progress), render EDL, serve media
web/         Vite + React timeline editor (no heavy deps)

How detection works — the killfeed (core/detect_killfeed.py)

The red hitmarker only tells you that you hit something — it can't separate a lethal hit from an explosion or a red reticle (even a CNN trained in-sample on that centre-red region tops out at the same wall, because the information that makes a hit lethal isn't in those ~100px). The killfeed is the game telling you, unambiguously, that you got a kill: a row appears top-right reading

▶ <your name>   <weapon>   <victim>

with your name on the killer (left) side. We OCR that region on every sampled frame, keep rows where the player's gamertag is the killer, and emit one kill per distinct victim — so multikills are counted and deaths (name on the victim/right side) are ignored. Each detection carries the victim's name, so it's self-verifying.

It's tuned for accuracy over speed: every frame is read with two binarizations so faint low-contrast kills aren't missed. OCR is fanned out across CPU cores, but a long clip still takes a few minutes — that's the cost of catching every kill.

Needs the user's exact in-game name (entered in the UI) and tesseract. Measure recall against the labelled test kills anytime: python -m core.eval all.

Prerequisites

  • ffmpeg + ffprobe (brew install ffmpeg)
  • tesseract (brew install tesseract) — reads the killfeed
  • Python 3.11+, Node 18+

Run (local)

# 1. backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/uvicorn api.main:app --port 8765

# 2. frontend (separate terminal)
cd web && npm install && npm run dev
# open http://localhost:5173

Upload footage → pick your game → enter your exact in-game nameDetect kills. Every kill shows on the timeline. Then either:

  • Auto TikTok montage — one click builds a punchy edit (tight kill clips, dead air sped up or cut) and renders it. The policy (lead/tail, speed-up threshold, speed, delete-vs-speed for long gaps) is editable in the panel, or
  • Tune it yourself — set each dead-air gap to Keep / Delete / Speed (or use the bulk buttons), adjust how much to keep around each kill, then Render.

Aspect ratio is left untouched — crop/reframe for your platform separately.

Adding another game

Create core/games/<id>/profile.json with a killfeed block: the kill-log region as normalized roi coords [x, y, w, h] (so it scales to any resolution) and icon_frac (where the weapon icon sits across that region, so the killer's name is to its left). No core code changes — it shows up in the UI's game picker automatically.

Tests

  • node web/e2e_test.mjs [video] [gamertag] — full UI flow against headless Chrome.

SaaS — freemium (auth + quota + billing)

Anyone can use the app. Tiers are: Free (10 h of footage/month, watermarked), Pro ($9.99/mo — 30 h/month, no watermark), and owner comp (unlimited). Three layers:

  • Identity + tierapi/auth.py. Entering an email gives instant access on the free tier: an unverified signed session cookie (itsdangerous). Clicking the emailed magic link (Resend) — or completing Stripe checkout — makes the session verified, which is what unlocks comp/Pro perks. So typing someone else's comp/paid email can't inherit their benefits. plan_of(user)free | pro | comp. require_login gates the heavy endpoints (any logged-in user); with no RESEND_API_KEY the link is returned in the API response for local login.
  • Quotaapi/db.py meters source-video seconds processed per user per month (usage_log). /api/analyze pre-checks against the plan limit (402 when over); the detect-completion callback records the real source duration. Free renders are watermarked (top banner overlaid in core/montage.py; the worker rasterizes watermark.svg via librsvg2-bin).
  • Billingapi/billing.py: Stripe Checkout, Customer Portal, a signature-verified webhook that syncs sub_status, and a /api/billing/success return that verifies the session so paying unlocks Pro immediately. No card data touches the server.

Storage is Postgres when DATABASE_URL is set, else a local SQLite file (zero setup for dev).

Env vars

SESSION_SECRET (random), ADMIN_TOKEN (admin endpoints), PUBLIC_BASE_URL (e.g. https://montagemaker.co), DATABASE_URL (Railway injects it), RESEND_API_KEY + RESEND_FROM, STRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET. Grant unlimited comp: POST /api/admin/comp?token=… {email}; export signups: GET /api/admin/users?token=….

Deploy on Railway

A multi-stage Dockerfile builds the React app and runs FastAPI (with ffmpeg + tesseract) which serves both the API and the built SPA — one service.

  1. New Railway project → add the Postgres plugin (sets DATABASE_URL) and a Volume mounted at the app's data/ dir (uploads/outputs survive redeploys).
  2. Deploy from GitHub rlmsinclair/montage_maker (Railway builds the Dockerfile).
  3. Set the env vars above. PORT is provided by Railway.
  4. Stripe (SVID AI LTD account): create a recurring Price ($9.99/mo for Pro), copy the price id → STRIPE_PRICE_ID; add a webhook to …/api/stripe/webhook (checkout.session.completed, customer.subscription.*) → STRIPE_WEBHOOK_SECRET.
  5. Domain montagemaker.co: GoDaddy can't CNAME a bare apex → put free Cloudflare in front (apex CNAME-flattening → Railway), or use www + GoDaddy apex→www forwarding.

Video processing runs on Google Cloud Run Jobs (worker/) reading/writing a GCS bucket, activated when the GCP_* env is set on Railway (else an inline fallback runs the same core/ engine on a background thread). Scales to zero between jobs.

About

AI gameplay-highlight engine: killfeed OCR to TikTok montage, full SaaS with Stripe

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages