Skip to content

Repository files navigation

Readback

ATC phraseology, read back to you.

Readback_demo.mp4

"I was parked at the threshold of 28R at KSFO, listening to the tower frequency on my handheld, when I heard something that didn't sound right — a clearance that seemed to conflict with another aircraft still on the runway. By the time I processed it, the controller had already issued a go-around. I wished I had something that could catch those moments automatically, log them, and tell me exactly what regulation was implicated."

Readback is an educational tool. Transcriptions may be imperfect and feeds are often one-sided — notes and events are advisory, not authoritative. It is for learning and situational awareness, not enforcement.

This project exists because radio communications between pilots and air traffic controllers are dense, fast, and consequential. Readback listens to live ATC audio streams, transcribes them in real time, and uses AI to compare transmissions against FAA/ICAO standard phraseology — surfacing read-back errors, non-standard calls, and situational events so you can learn from them.

Built for aviation enthusiasts, safety researchers, student pilots, and anyone who finds themselves glued to LiveATC on a Saturday afternoon.


What It Does

  • Streams live audio from LiveATC.net feeds (or any compatible MP3 stream)
  • Transcribes ATC communications using faster-whisper (local, no cloud STT cost)
  • Batches transcripts and sends them to Gemini Flash for phraseology analysis every 5 minutes (configurable in Settings)
  • Applies the Reasonable Controller Test — notes genuine deviations, ignores transcription noise and one-sided readback gaps
  • Classifies observations using HFACS taxonomy (Human Factors Analysis and Classification System)
  • Correlates findings with live ADS-B traffic from OpenSky Network
  • Pulls METAR weather, NOTAMs, and SIGMET/AIRMET/PIREP hazards for full situational context
  • Streams results live to a React dashboard via WebSocket

Why This Exists

Standard ATC monitoring tools show you what is happening — radar returns, frequency activity, flight strips. They don't tell you whether what was said matched standard phraseology. Readback fills that gap:

  • Student pilots can study phraseology against real-world examples and see where actual transmissions depart from the book
  • Enthusiasts can monitor their home airport and get notified of notable events (emergencies, go-arounds, TCAS RAs)
  • Safety researchers can build a longitudinal dataset of phraseology observations at specific airports
  • Instructors can use real-world clips to illustrate what a read-back error or non-standard clearance sounds like in practice

Architecture

LiveATC Stream (MP3)
│
▼
ffmpeg chunker chunks every N seconds
│
▼
faster-whisper local STT, confidence-gated
(WhisperModel) rejects low-quality audio
│
▼
transcript_queue asyncio queue, shared across airports
│
▼
run_batcher() drains queue every 5 minutes (configurable)
│ caps at batch_max_items per batch (default 40)
▼
Gemini Flash single API call covering all airports
(batch analysis) applies FAA/ICAO/HFACS rules
│
├──► OpenSky ADS-B correlate traffic at analysis time
│
▼
PostgreSQL persist results + ADS-B snapshots
│
▼
WebSocket /ws/live broadcast to all connected clients
│
▼
React Dashboard
├── HeaderBar feed controls + live audio
├── LiveFeed real-time transcript + observation stream
│ └── ObservationCard per-transmission detail + HFACS category
├── AirportSidebar map, ADS-B, weather + NOTAMs for the selected airport
└── SettingsPage configure feeds, batch interval, STT model, alerts

Key Design Decisions

DecisionReason
Batch Gemini calls (5-min window)Conserves free-tier daily quota; one call covers all airports
Local Whisper STTNo per-minute STT cost; runs on CPU with int8 quantisation
Confidence gating before AI callAvoids sending garbage transcripts to Gemini; saves tokens
ADS-B snapshot at analysis timeCorrelates what was said with what aircraft were actually doing
HFACS taxonomyIndustry-standard classification used in NTSB/ASRS investigations

Phraseology Notes

TypeExample
Read-back ErrorIncorrect or missing readback of a cleared altitude
Frequency/Channel ErrorFrequency confusion, wrong channel
Communication FailureLoss of contact, blocked transmission
Navigation ErrorWrong fix or approach named in a transmission

Situational Events

TypeExample
Runway Incursion / ExcursionAircraft enters a runway without clearance
Altitude / Speed DeviationCrew reports leaving a wrong altitude
CFIT RiskTerrain-proximity indications
TCAS EventCrew responds to a resolution advisory
Go-aroundMissed approach or rejected landing
Fuel AdvisoryMinimum fuel or fuel emergency declared

Quick Start

You need just two things to begin:

  • A free Gemini API key (the free tier is plenty).
  • A runtime: Docker (simplest), or Node + Python from source — pick a path below.

First, clone the repo and add your key:

git clone https://github.com/MuddyWinds/Readback.git
cd Readback
cp .env.example .env # then open .env and paste your Gemini key
# .env — the only required settingGEMINI_API_KEY=your_key_here

Now choose one of the two paths.

Option A — Docker (recommended)

The simplest way to run everything. Docker starts the database, backend, and frontend together — one command, nothing else to install.

Make sure Docker Desktop is running first.

docker compose up

Open http://localhost:3000 and jump to First run.

Option B — Run from source (database in Docker)

Best if you'd rather run the backend and frontend from source while developing. The backend and frontend run natively; only the database stays in Docker — a single lightweight Postgres container so your data persists in one place and matches Option A exactly. (Don't mix this with SQLite — see the note below — or your records will split across two separate databases. Want zero Docker? See Option C.)

Install Node 18+, Python 3.11+, and ffmpeg (brew install ffmpeg on Mac, apt install ffmpeg on Linux). Then, from the repo root:

# Terminal 1 — backend (reads DATABASE_URL from .env → Postgres on localhost:5432)
docker compose up -d db
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m uvicorn backend.main:app --port 8000 --reload
# Terminal 2 — frontendcd frontend
npm install
npm run dev

Open http://localhost:3000.

Don't pass a DATABASE_URL= override here. Without it the backend uses the value in .env (postgresql://atc:atc@localhost:5432/atcmonitor, auto-upgraded to the async driver), so it always reads and writes the same Postgres data. Prepending a DATABASE_URL="sqlite+aiosqlite:///./readback.db" would point it at a separate, empty SQLite file and your existing threads would appear to vanish.

Really want a server-less SQLite file instead? Install the extra driver and pass the override explicitly — but commit to it, because SQLite and Postgres never share data: .venv/bin/pip install aiosqlite then run with DATABASE_URL="sqlite+aiosqlite:///./readback.db" .venv/bin/python -m uvicorn backend.main:app --port 8000 --reload.

Option C — Fully Docker-free (native Postgres)

For zero Docker at all, run Postgres natively. The trade-off: a native install is a separate, empty database from the Docker volume, so if you already have data in Docker (Option A/B) you must migrate it once — otherwise you start fresh.

Install Node 18+, Python 3.11+, ffmpeg, and Postgres 16 (brew install postgresql@16 ffmpeg on Mac). Then create the database and role to match the project's default DATABASE_URL:

# One-time: create the atc role + atcmonitor database
brew services start postgresql@16
createuser -s atc 2>/dev/null; psql -d postgres -c "ALTER USER atc WITH PASSWORD 'atc';"
createdb -O atc atcmonitor
# Terminal 1 — backend (no Docker; uses .env → postgresql://atc:atc@localhost:5432/atcmonitor)
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m uvicorn backend.main:app --port 8000 --reload

Run the frontend exactly as in Option B (Terminal 2). The schema is created automatically on first backend start.

Already have data in the Docker volume? Migrate it once with the helper scripts (details below):

docker compose up -d db # start the Docker DB
scripts/db_export.sh # → backups/readback_<timestamp>.sql
docker compose down # free port 5432
brew services start postgresql@16
scripts/db_import.sh backups/readback_*.sql # load into native Postgres

Only one Postgres can bind port 5432 at a time, so stop the Docker DB before starting the native one (and vice versa).

Moving data between databases

Your records live in whichever Postgres DATABASE_URL points at — there is no automatic sync between a Docker and a native install. To move them, use the two helper scripts (both read DATABASE_URL, strip any +driver suffix, and fall back to the readback-db-1 container when no local pg_dump/psql is installed):

# Export the current database to a timestamped dump in backups/
scripts/db_export.sh [output_file]
# Restore a dump into the database DATABASE_URL points at.# Refuses to run if the target already has rows (pass --force to merge anyway).
scripts/db_import.sh <dump_file> [--force]

db_export.sh doubles as a backup tool; db_import.sh is the safe way to seed a fresh database (e.g. when switching to Option C). Always restore into an empty database — pg_dump preserves the ID sequence, so new rows continue past your existing IDs without collisions.

First run: configure a feed

On first launch the app opens the Settings tab automatically (it detects that no feeds are configured yet). Add one or more LiveATC feeds — each with an airport code — and save. Then switch to the Live tab and click ▶ Start All to begin monitoring.

You can paste either a LiveATC stream URL (audio.liveatc.net/<mount>) or the listen-page link from your browser (e.g. https://www.liveatc.net/hlisten.php?mount=vhhh5&icao=vhhh) — click Verify and it is converted to a working stream URL with the airport code filled in. Coordinates, runways, the map marker, ADS-B correlation, hazards, and weather then resolve automatically for any operational fixed-wing ICAO airport worldwide — the airports listed below are just convenient starting points.

Prefer the command line? You can drive monitoring through the API instead:

# Start a feed
curl -X POST "http://localhost:8000/api/monitor/start?feed_url=http://feeds.liveatc.net/ksfo&airport_code=KSFO"# Stop it
curl -X POST "http://localhost:8000/api/monitor/stop?airport_code=KSFO"

View it on your phone (same Wi-Fi)

Yes — you can keep your laptop as the server and open the dashboard on your phone, as long as both are on the same Wi-Fi network. The dashboard is just a web page that your laptop serves; the phone is only a viewer.

Two small settings let the phone's browser reach your laptop (instead of looking for the backend on the phone itself):

  1. Find your laptop's local IP address — say it's 192.168.1.42:

    ipconfig getifaddr en0 # macOS (Wi-Fi)
    hostname -I | awk '{print $1}'# Linux
  2. Point the frontend at that IP, and allow it through CORS. In .env:

    ALLOWED_ORIGINS=http://localhost:3000,http://192.168.1.42:3000

    In frontend/.env.development:

    VITE_API_BASE=http://192.168.1.42:8000VITE_WS_URL=ws://192.168.1.42:8000/ws/live
  3. Restart so the changes take effect — for Docker run docker compose up -d --build (the frontend image bakes its env in at build time); for Option B, just restart the two dev servers.

  4. On your phone, open http://192.168.1.42:3000.

Swap 192.168.1.42 for your real IP everywhere above. On macOS, approve the firewall prompt to allow incoming connections the first time. This setup keeps working on the laptop at http://localhost:3000 too, so you don't have to choose.


Everyday Use

Once the project is set up, these are the routine commands. Always make sure Docker Desktop is running first.

# Start the whole stack (db + backend + frontend) in the background
docker compose up -d
# Open the dashboard
open http://localhost:3000
# See what's running and tail logs
docker compose ps
docker compose logs -f # all services
docker compose logs -f backend # one service# Stop the stack (the Postgres volume is preserved)
docker compose down
# Restart one service (e.g. after editing backend code)
docker compose restart backend
# Rebuild after changing dependencies (requirements.txt / package.json)
docker compose up -d --build

The stack publishes host ports 3000 (frontend), 8000 (backend) and 5432 (Postgres).

Port conflicts. If another process already holds one of those ports — e.g. a stray frontend dev server from another project on :3000 — Docker may bind only one IP family and http://localhost:3000 becomes ambiguous (you can end up looking at the wrong app). Free the port, then let the container rebind:

lsof -nP -iTCP:3000 -sTCP:LISTEN # find what's holding itkill<PID># stop the stray process
docker compose restart frontend # rebind the container

Troubleshooting. If the dashboard shows "Unable to load analysis cards" or "Loading settings…" that never finishes, the frontend can't reach the backend on :8000. Confirm docker compose ps lists readback-backend-1 as Up and that nothing else is squatting on port 8000.


Configuration

All backend settings are read from .env (template in .env.example). The ones you're most likely to touch:

VariableDefaultWhat it does
GEMINI_API_KEY(required)Your Gemini API key
DATABASE_URLpostgresql://atc:atc@localhost:5432/atcmonitorDatabase connection. Use sqlite+aiosqlite:///./readback.db for a zero-install local file; Docker sets the Postgres URL for you
ALLOWED_ORIGINShttp://localhost:3000Comma-separated browser origins allowed to call the API. Add your laptop's LAN origin to view on a phone, or your deployed domain
WHISPER_MODELbasefaster-whisper model: tiny / base / small / medium / large

The frontend resolves the backend from the page's own origin by default, so a same-origin production build behind a reverse proxy needs no configuration. For the split :3000 / :8000 dev setup it reads frontend/.env.development (VITE_API_BASE, VITE_WS_URL). Point those at another host (e.g. your LAN IP) to reach the backend from a different device — and add that origin to ALLOWED_ORIGINS so CORS lets it through.

Running Tests

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m pytest

API Reference

MethodPathDescription
POST/api/monitor/startStart monitoring a feed
POST/api/monitor/stopStop monitoring
GET/api/monitor/statusActive monitors
GET/api/pipeline/statusPipeline / worker health
GET/api/airportsConfigured / supported airports
GET/api/resultsPaginated analysis history
PATCH/api/results/{result_id}Update a single result (e.g. dismiss/annotate)
GET/api/statsAggregate phraseology statistics
GET/api/adsb/{airport_code}Live ADS-B traffic (60s cache)
GET/api/adsb-snapshot/{result_id}ADS-B state captured at analysis time
GET/api/metar/{airport_code}Current METAR weather
GET/api/notam/{airport_code}Active NOTAMs (5-min cache)
GET/api/hazards/{airport_code}SIGMET / AIRMET / PIREP (5-min cache)
GET/api/study-sheet/{result_id}Per-aircraft Gemini study sheet
GET/api/settingsRead current settings (feeds, batch interval, …)
PUT/api/settingsUpdate settings
POST/api/settings/verify-feedValidate a LiveATC feed URL before saving
WS/ws/liveReal-time results stream

Finding Feeds

Browse available feeds at liveatc.net/feedindex.php.

Example feeds to get you started - any operational fixed-wing ICAO airport resolves the same way (map marker, ADS-B, hazards, runway overlay):

ICAOAirport
KJFKNew York JFK
KATLAtlanta Hartsfield–Jackson
KLAXLos Angeles International
KORDChicago O'Hare
VHHHHong Kong International

Limitations & Caveats

  • One-sided transcripts — LiveATC captures one radio side only. The analyser is explicitly told not to flag missing readbacks that may simply be on the other side.
  • Transcription noise — Whisper on VHF radio audio is imperfect. Low-confidence segments are filtered out rather than sent for analysis.
  • Not a safety-critical system — This is a hobbyist/research tool. Do not use it for operational decisions.
  • Gemini quota — The free tier has a daily token limit. The 5-minute batch window and the per-batch transcript cap (batch_max_items, default 40) are designed to stay within it; lower either in Settings if you hit limits.

Tech Stack

LayerTechnology
Audio ingestionffmpeg
Speech-to-textfaster-whisper (configurable model, default base, int8)
Phraseology AIGoogle Gemini Flash
BackendPython / FastAPI / SQLAlchemy (async)
DatabasePostgreSQL 16
ADS-B dataOpenSky Network (free, anonymous)
Weather / NOTAMaviationweather.gov / aviationapi.com
FrontendReact / TypeScript
ContainerisationDocker Compose

Contributing

PRs welcome. If you add a new airport, add its coordinates to AIRPORT_GEO in backend/core/batcher.py so ADS-B correlation works.


License

Released under the MIT License.

About

Listens to live ATC audio, transcribes it in real time, and uses AI to flag read-back errors and non-standard phraseology against FAA/ICAO standards. Educational tool.

Topics

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages