Skip to content
TsakmakTsakmak

Project

Transcript extraction: from audio to checked facts

Audio to transcript to events to a checked record, with a source on every fact.

Where it runs: On my home server (private)

Platforms
Web (private, login)

The problem

The path is: audio, then a transcript, then extracted events, then a checked record. Each step is a place where an error can enter, so each step has to leave a trail back to the one before it.

My test case is an amateur football club that streams almost every match with live commentary. Much of its history (line-ups, substitutions, cards, who scored and how) exists only in what the commentator said on those recordings. There are about 125 streams of about 100 minutes each, roughly 200 hours, and nobody can rewatch that to answer a question. A summary written by an AI model is only useful if you can see where each fact came from and whether anyone has checked it.

What it does

I built a pipeline that follows that path and turns the recordings, the club's public match pages and its public posts into one match database with a strict evidence discipline. Every fact stores its source. Anything a language model extracted enters as unverified and becomes verified only when a second source agrees or a person checks it. Totals are computed, unknowns stay empty, names are matched through an alias table, and disagreements go to review lists instead of being resolved silently. A private statistics site and a read-only assistant sit on top, and every extracted event keeps the second in the stream where it was said.

How it works

  1. Collect public sources

    Match pages from the club and the football associations go into tables with a source address on every row. The other inputs are the club's public live streams and public posts.

  2. Transcribe locally

    The Greek commentary is transcribed with an open speech-to-text model on my RTX 5090 workstation. Known errors of the model (music read as captions, repeated loops) are removed by explicit rules, and every removal is logged in the transcript file. The audio is deleted after transcription.

  3. Extract events

    For each match, an AI agent reads the full transcript with a fixed written instruction ("only what the speaker says, never guess") and writes goals, cards, substitutions and line-ups as structured data. Each event carries the second in the stream and the short quote it came from.

  4. Load with the evidence rules

    A loader stores everything as unverified, checks it against the match pages (same team, minute within 2), marks agreeing facts as verified, and writes unmatched names and conflicts to review lists. Score cards in posts, read by a local vision model, act as a further independent source.

  5. Review and answer

    I work through the review lists. The statistics site and the assistant read the database, and both show what is still unverified.

Key features

Every fact stores its source

Each goal, card, line-up and score points to a source record: a match page, a post, a stream or its transcript, with the address and the time it was retrieved. A separate table records which source covers what for each match (score, line-up, events), so you can see why a value is there.

Screenshot to follow
From recordings to checked facts

Model output starts as unverified

The database default is "unverified". A goal extracted from a transcript becomes verified only when the club's match page has the same goal by the same team within two minutes, or a person confirms it. A different scorer does not overwrite anything: it goes to a conflicts list. The site marks unverified items with a question mark and can count verified goals only.

Screenshot to follow
Unverified items are marked, and can be filtered out

Totals are computed, unknowns stay empty

Goals, minutes played, records and head-to-head figures are always calculated from the individual events, never typed in. When the commentator says a running total, it is kept as a "reported" figure to compare with the calculated one. A value nobody said stays empty, not zero. Where minutes played depend on a missing value, the total is labelled a lower bound.

Screenshot to follow
What is known, what is missing, what still needs a check

An assistant that answers from the database first

The assistant answers only from tools that read the database, never from memory, and says so when something is not there. It uses the fixed queries first and searches the transcripts only for moments and quotes the database cannot cover. It mentions when an event has only one source and links to the stream about ten seconds before the moment. The same read-only tools also work with a local model on my workstation.

Screenshot to follow
An answer from the database, with a link back to the recording
  • Names go through an alias table

    Speech-to-text gets names wrong, and commentators use short forms. Names are stored as heard, then matched through an alias table that handles Greek case endings and similar-sounding forms. A name that does not match, or matches more than one person, is not loaded: it goes to a review list.

  • Back to the exact moment

    Every extracted event keeps the second in the stream, the quote it came from and a confidence value when the commentary was not explicit. Match minutes are calibrated against the kick-off of each half, so the minute the commentator said can be checked against the recording.

Security and privacy

  • The sources are the club's public match pages, public live streams and public posts. Comments on posts are not collected.
  • Transcription and the reading of score-card images run locally on my workstation. The audio is deleted once it is transcribed.
  • Event extraction from the transcripts was done by hosted AI agents, working on the text, not on the audio.
  • The assistant uses a hosted model, read-only, with a daily limit. Questions and the database rows it reads are sent to that model.
  • The database is opened read-only. Fixed queries use parameters only. The assistant's free query tool is limited to reading by the database engine itself, with a time limit and a row limit, and is covered by injection tests.
  • The statistics site has a single user, strong password hashing, optional two-factor login, login rate limits and a strict content security policy. No data is in the public pages; it is served only after login.
  • Real match data is not public. The pictures on this page show matches, scores and team-level figures only.

Under the hood

A set of Python scripts on my workstation does the collection, transcription, cleaning, extraction and loading into SQLite, with full-text search over the transcript segments. The rules live in three places, so they do not depend on a prompt being followed: the schema (unverified by default, empty means unknown), the loaders (matching, conflicts, review lists) and the views (every total). The statistics site is a small FastAPI app that serves a read-only copy of the database.

One measured detail: transcribing in 15-second windows instead of 30 kept text that the longer window lost (about 15% of it).

I built it by directing AI coding agents under a written rule file that sets the data rules. Research agents collected the public match data with a source address on every row, and one agent per match did the extraction with the same fixed instruction. I set the rules, confirmed identities, resolved conflicts and reviewed the results.

Limits:

  • Events were extracted from all but one of the streams (that one has no audio).
  • One known discrepancy is open: for one player the database counts one goal more than the club's official count, and the goal that should be an own goal has not been found yet.
  • The checks found errors in the sources too: one season's scorers on the club's own match pages were wrong and were corrected from the transcripts and the dates of posts.
  • The vision model often merges two names printed close together on score cards, so many of its readings end up as "not recognised" and need a person.
  • The data pipeline scripts have no automated tests yet; the statistics site has 25.
  • Only goals can be cross-checked automatically today, so most other extracted events (cards, substitutions, line-ups) still have one source until a person checks them. Verification is ongoing, and I have not used the method on any other recordings.

Stack

  • Python
  • SQLite with full-text search
  • an open source speech-to-text library that runs locally
  • Local vision-language model
  • FastAPI
  • Plain JavaScript
  • Docker

Get in touch