An AI sports commentator for live streams. Point it at a live sport stream β like an NOS livestream β and it replaces the dry, boring official commentary with a live commentator thatβs half Aussie larrikin, half weary observational stand-up, while keeping the crowd and ambient stadium sound from the original broadcast.
Commentary is generated with OpenAI models: a vision model watches frames from the stream
and writes the next line in character, and the gpt-4o-mini-tts speech model speaks it
with a rowdy, off-the-cuff commentator delivery (via the TTS instructions parameter).
stream page URL βββΆ /api/resolve βββΆ .m3u8 URL
β
browser βββ /api/proxy (CORS) βββ (hls.js plays it; canvas can capture frames)
β
ββ every N seconds: capture 2β3 frames βββΆ POST /api/commentary
β β vision model β commentary text
β β gpt-4o-mini-tts β mp3
β βββββββββββββββββββββββ
β
ββ Web Audio mixer:
original stereo βββ
L β R (voice-cancelled ambience) βββ€βββΆ crossfade ββΆ duck ββΆ π
Ozzy's TTS lines βββββββββββββββββββββββββββββββββββββββββββΆ π
Keeping the ambient sound: broadcast commentators are mixed dead-centre, while crowd and ambience are stereo. The player subtracts the right channel from the left (karaoke-style centre-channel cancellation), which removes most of the original commentatorβs voice but keeps the stereo atmosphere. A slider crossfades between the original mix and the voice-cancelled mix, and the whole ambient bed automatically ducks while Ozzy is talking.
npm install
export OPENAI_API_KEY=sk-...
npm start
# open http://localhost:3210
Paste a stream URL, click Find stream, then click the Watch with Ozzy Sports
commentary link. That link (/watch.html?src=β¦) is shareable with anyone who can reach
your server. On the watch page, press Start Ozzy Sports (a click is required β browsers
demand a user gesture before audio can play).
| Variable | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
β | Required. |
OPENAI_VISION_MODEL |
gpt-4o-mini |
Model that watches frames and writes commentary. |
OPENAI_TTS_MODEL |
gpt-4o-mini-tts |
OpenAI speech model. |
OPENAI_TTS_VOICE |
ash |
TTS voice (ash, verse, ballad, coral, β¦). |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Override for proxies/compatible APIs. |
PORT |
3210 |
HTTP port. |
APP_PASSWORD |
β | If set, the whole site requires this password (HTTP Basic auth, any username). Strongly recommended for public deployments β the commentary endpoint spends your OpenAI credits. |
ALLOW_PRIVATE_NETWORKS |
β | Set to 1 to let the resolver/proxy fetch private/localhost addresses (local development only; they are blocked by default to prevent SSRF). |
The docs/ folder is a fully static build of Ozzy Sports that runs entirely in the
browser, deployed to GitHub Pages by .github/workflows/pages.yml on every push to
main. Differences from the server version:
.m3u8
URL, and the CDN must allow cross-origin access for frame capture (most HLS CDNs do).
Geo-blocked streams follow the viewerβs own location; DRM streams donβt work anywhere.docs/config.js holds the site defaults: a pre-loaded stream URL (note: NOS
URLs carry signed tokens that expire β refresh the value when playback stops) and the
default commentator context. With a default stream set, the watch page shows a single
big Play button that starts stream + commentary in one press. Never put an API key
in config.js β itβs public. For one-click setup on a new device, use a private
bookmark of the form watch.html#key=sk-...: the page stores the key in that browser
and strips it from the address bar.main (or run the βDeploy GitHub Pagesβ workflow manually). The workflow
enables Pages itself (enablement: true) and the site appears at
https://pas-mllr.github.io/sportsozzy/.docs/CNAME already declares ozzysports.live. At your domain registrar, add:
| Type | Name | Value |
|---|---|---|
| A | @ |
185.199.108.153 |
| A | @ |
185.199.109.153 |
| A | @ |
185.199.110.153 |
| A | @ |
185.199.111.153 |
| AAAA | @ |
2606:50c0:8000::153 |
| AAAA | @ |
2606:50c0:8001::153 |
| AAAA | @ |
2606:50c0:8002::153 |
| AAAA | @ |
2606:50c0:8003::153 |
| CNAME | www |
pas-mllr.github.io |
ozzysports.live, wait for
the DNS check and certificate, then tick Enforce HTTPS./api/resolve scrapes the page for .m3u8 URLs and knows a
couple of NOS API endpoints. Some streams are geo-blocked (NOS streams generally require
a Dutch IP) or DRM-protected β DRM streams canβt be played or captured. If auto-resolve
fails, grab the .m3u8 URL from your browserβs dev tools (Network tab, filter m3u8)
and paste it directly.