Watches a Twitch stream, OCR-reads the on-screen LiveSplit timer, and logs every speedrun attempt to SQLite. Needs nothing from the streamer — no capture card, no LiveSplit server, no chat presence. Everything it knows it read off the public video.
It publishes a static records site (https://ng.lexicone.com) with daily heartbeat timelines, death charts, gold segments and click-into-run splits, and a JSON feed for bots.
The reference deployment follows arcus — one of the fastest Ninja Gaiden (NES) runners in the world — but the game, layout rectangles and record semantics are all configuration.
- It survives the layout moving. Streamers switch OBS scenes and nudge the LiveSplit window. The bot probes layouts across a grid of pixel offsets, re-anchors on drift, and re-measures the pane's row pitch every minute, so the splits column follows the window rather than a hand-measured rectangle.
- It knows what it is looking at. The pane's own header, category, attempt counter and row names are read on every pass and weighed against the tracked game, so a broadcast of something else is not recorded as this one. See docs/detection.md.
- It says how well it saw. Every session records what share of the feed it read and every layout event; the site shows it per day, so a thin day is visible rather than silently thin.
- It reads speedrun semantics, not just digits. Seasonal best vs lifetime PB, run identity by the streamer's own LiveSplit attempt counter, and a plausibility floor so a frozen timer is never a finish.
- It backfills. Old VODs stream straight from Twitch and land on the original broadcast timeline.
- It reads a whole marathon board. A ten-game Arcathlon or the twenty-game
Big 20 race is tracked by which rows have completed, not by the timer:
cumulatives that agree across passes, the board's own arithmetic as the
check on every row, rows placed by the roster's order, and the previous
run's times told apart from today's. The site gets a page per event and,
for the race, a prep dashboard (
/big20/) and a run-by-run comparison (/big20/runs/). See docs/marathons.md.
Rust, no Python in the toolchain. The bot shells out only to ffmpeg and
tesseract; the scripts also use sqlite3, curl, jq, flock and the
AWS CLI for the site deploy.
How it works, in one page with a diagram and a glossary: docs/overview.md.
Status: in daily use. Expect rough edges on layouts it was not
calibrated for — locate and calibrate exist to make new ones quick.
- ffmpeg
- tesseract — CLI on
$PATH, or a user-space AppImage, or compiled in with--features leptess-ocr(about 1.4x faster). Root is not required. docs/install.md has all three routes.
SQLite, HLS resolution and Twitch chat are compiled in; a clean build needs
cmake and a C compiler for the TLS library the chat client links.
cargo build --release
cp config.example.toml config.toml # edit: channel, crop rectangleEvery field is commented with its default in config.example.toml. Point
it at a new stream with locate and calibrate; see
docs/detection.md.
Try it on a VOD before pointing it at a live stream. Set source = "vod" with a VOD id, or source = "file" with a local recording, and the
run lands on the original broadcast timeline.
ngtwitchtimer # watch, detect, log (config.toml in cwd)
ngtwitchtimer -c live.toml run # any config file
ngtwitchtimer report # PBs, today's stats, recent runs
ngtwitchtimer report --json # machine-readable (feeds the site)
ngtwitchtimer locate # find the LiveSplit pane in a frame
ngtwitchtimer pane [--image f.png] # what the pane pass reads, per layout and threshold
ngtwitchtimer calibrate # tune the timer crop by eye
ngtwitchtimer glyphs train|test|boxes # the timer's template reader
ngtwitchtimer audit --dir arcathlon-db # score a replayed marathon against its own board (NG_MARATHON_TRACE=<row> says why a row was or was not filed)
ngtwitchtimer report --api-dir site/api/v1 # the per-day feed
RUST_LOG=ngtwitchtimer=debug ngtwitchtimer # per-frame tracingIt survives stream drops, goes dormant while the channel is offline, and
stores everything as millisecond integers in SQLite (sessions, runs,
splits, transitions, settings).
For a deployment that outlives the terminal, scripts/run-live.sh
supervises it under tmux on a server of its own (scripts/start-supervisor.sh) and scripts/rollout.sh ships a new build. See
docs/operations.md.
To develop in a sandboxed session with the same toolchain as every other
contributor, minimal.toml is a Minimal blueprint;
see docs/minimal.md for how to bring one up here and
what stays on the host.
Optional, under [chat], with a bot account and an IRC OAuth token. Point
channel at your own channel to test without posting in the streamer's.
command_prefix namespaces everything for shared channels.
Viewers: !pb, !lastrun, !today, !attempts, !deaths, !pace,
!splits, !golds, !status. Mods (broadcaster, badge mods, or logins in
chat.mods): !correct, !void, !note. Each has a cooldown shared by
all viewers.
Static JSON under https://ng.lexicone.com/api/v1/, rebuilt on every
deploy:
| file | size | what |
|---|---|---|
latest.json |
~4 KB | the grind in one document — start here |
summary.json |
~35 KB | every aggregate the site shows, no per-run rows |
report.json |
~1 MB | every run, split and session |
manifest.json + days/<date>.json |
— | per-day feed, for fetching only what changed |
/llms.txt is the entry point for assistants;
site/static/api/v1/README.md is the field
reference. Within /api/v1/ only fields are added — a change of meaning
gets a new version path.
| docs/overview.md | the map: the pipeline, the three kinds of record, a glossary, a reading order |
| docs/detection.md | how video becomes runs, and why each step is shaped that way |
| docs/operations.md | running the deployed bot: scripts, schedule, known footguns |
| docs/marathons.md | the ten-game days, and the tooling that is theirs alone |
| docs/big20.md | the race he is preparing for, and the practice days that go with it |
| docs/install.md | getting ffmpeg and tesseract in place, including without root |
| CLAUDE.md | ground rules for changing this repo safely |
Issues and PRs welcome. The best first contributions are layout
configurations for other streamers (send a locate printout and a frame)
and detection edge cases with an obs_log excerpt.
Two standing constraints. Keep the bot Rust + ffmpeg + tesseract — the
scripts may use sqlite3, jq, curl, flock and the AWS CLI, but
please add no new interpreters. And for any change to OCR, locking or
detection, include scripts/replay-window.sh output for the old and the
new binary on the same VOD window; a claim about detection without a
replay is not reviewable.
CI runs cargo fmt --all -- --check, cargo clippy --release --all-targets -- -D warnings, cargo build --release, cargo test --release and
bash -n scripts/*.sh. Run those locally first — a formatting warning
fails the check. CI builds with default features, so the in-process OCR
path (--features leptess-ocr) is not compiled there.
The tests carry the behaviour: the state machine, timer parser, counter
tracker, geometry, glyph reader and roster matching are unit-tested against
real crops and real board logs under tests/fixtures/, and need neither
tesseract nor ffmpeg. Add a test with any behaviour change.
Dual licensed under Apache-2.0 or MIT, at your option. Contributions are dual licensed the same way unless you say otherwise.
Not affiliated with Twitch, LiveSplit, or the streamers it follows. It reads publicly broadcast video only.