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.
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)
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.
ffmpeg+ffprobe(brew install ffmpeg)tesseract(brew install tesseract) — reads the killfeed- Python 3.11+, Node 18+
# 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:5173Upload footage → pick your game → enter your exact in-game name → Detect 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.
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.
node web/e2e_test.mjs [video] [gamertag]— full UI flow against headless Chrome.
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 + tier —
api/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_logingates the heavy endpoints (any logged-in user); with noRESEND_API_KEYthe link is returned in the API response for local login. - Quota —
api/db.pymeters source-video seconds processed per user per month (usage_log)./api/analyzepre-checks against the plan limit (402 when over); the detect-completion callback records the real source duration. Free renders are watermarked (top banner overlaid incore/montage.py; the worker rasterizeswatermark.svgvialibrsvg2-bin). - Billing —
api/billing.py: Stripe Checkout, Customer Portal, a signature-verified webhook that syncssub_status, and a/api/billing/successreturn 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).
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=….
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.
- New Railway project → add the Postgres plugin (sets
DATABASE_URL) and a Volume mounted at the app'sdata/dir (uploads/outputs survive redeploys). - Deploy from GitHub
rlmsinclair/montage_maker(Railway builds the Dockerfile). - Set the env vars above.
PORTis provided by Railway. - 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. - Domain
montagemaker.co: GoDaddy can't CNAME a bare apex → put free Cloudflare in front (apex CNAME-flattening → Railway), or usewww+ 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.