← All work

The Race Board

A live leaderboard for the Black Ops 7 race to Master Prestige that reads each streamer's rank straight off their Twitch broadcast, so nobody has to type in a score.

Year
2025
Made with
Python, Flask, streamlink, OpenCV, Tesseract, EasyOCR, Firebase
Links
Live siteSource on GitHubFrontend source
The Race Board leaderboard: streamers ranked by prestige and level, the top two at Prestige Master, each with a Watch Stream button
Two made it to Master. The board found out by watching.
Why

A handful of streamers were racing to Master Prestige in Black Ops 7, and the only way to know who was ahead was to check in on every stream. I wanted the standings without the tab-hopping, so I built something to watch them for me.

It was just for fun. Nobody asked for it, which is most of the appeal.

How it works

Each channel gets its own capture thread. streamlink turns the channel into an HLS URL, OpenCV reads it, and a two-slot queue keeps only the newest frame, so a slow detector never builds a backlog. Every two seconds the detector looks at whatever is current.

Twitch stream→Newest frame→Find "PROGRESSION"→OCR level + prestige→JSON in Firebase→Static site

Is this the Progression screen?

Almost every frame is gameplay. The detector crops the top-middle of the frame, binarizes it three ways, and asks Tesseract for the word PROGRESSION. No word, no detection. When the header is found it becomes the anchor, and the level and prestige boxes are defined as percentages of it.

Reading the numbers

The level crop gets three binarizations, a 4x upscale and a digits-only Tesseract pass. Only plausible numbers get a vote, and the most common answer wins.

Prestige is stylized text, which is harder. EasyOCR goes first, Tesseract tries sixteen preprocessing and page-mode combinations if that comes up empty, and a fuzzy matcher forgives the usual misreadings: PRESTICE, PREST0E, PREST6E. "Prestige Master" is stored as prestige 20 so it sorts above everything else.

Anything lower than what's already stored is treated as a misread. It only sticks if lower readings keep coming for five detections and ten minutes, so one bad frame can't knock anyone down the board.

No server for the public site

The backend is the only writer, so state is a plain streams_data.json, re-uploaded to Firebase Storage on every save. theraceboard.com is one static page on GitHub Pages that fetches the file and ranks streamers client-side, prestige then level. Each row has an info button with the last annotated frame and the raw OCR text, so anyone can check the robot's work.

What it sees
A Black Ops 7 Progression screen captured from a Twitch stream, with detection boxes drawn around the header, the Prestige 10 label and the level number 45
A published detection: header in green, prestige in magenta, level in yellow. This is what the info button shows.
Stack
Python, FlaskAPI, admin and test pages, background threads
streamlinkTwitch channel to HLS URL, and "is this channel live?" once a minute
OpenCVCapture, cropping, thresholding, annotation
TesseractHeader, level, and fallback prestige OCR
EasyOCRPrimary prestige OCR, optional
FirebaseStorage for the published JSON and frames; Auth for the admin page
HTML, CSS, JSThe public leaderboard, one static page
What it taught me
  1. 1

    Find the word, not the picture

    OCRing the header beat template-matching the UI; the old matcher is still in the repo, unused.

  2. 2

    OCR is a vote, not a read

    No single preprocessing pass gets trusted on its own.

  3. 3

    Teach the code your OCR's typos

    Part of the detector exists only to forgive a misread G.

  4. 4

    One writer, one JSON file

    With a single writer, a file in a bucket is a perfectly good database.

  5. 5

    Better OCR costs gigabytes

    EasyOCR reads the prestige font better and needs 1–2 GB of RAM to do it, hence the off switch.

Rough edges
  • Detection regions are fixed percentages, so a stream overlay sitting on the menu can push the text out of the box.
  • It can't see you level up mid-match. It finds out when you open the Progression screen on stream.