Archive
Rolling FLAC segments, metadata, transcripts, and status files for one already-demodulated source.
Signal Systems / Local AI / RF Memory
This is a local radio system that listens, records, transcribes, interprets, and remembers. It started as a headless HackRF + gqrx toolchain and mutated into something closer to a small RF perception engine.
It can monitor a station live, archive rolling audio, decode digital text noise, transcribe speech, split nearby channels from one capture window, and run a local language model over the results without handing the whole thing to the internet like a lab intern with no supervision. The live monitor path now also captures the intended GQRX stream instead of the entire desktop mix, which is a modest but meaningful improvement in dignity.
The stack has five useful layers. First: the ordinary radio control surface. Second: rolling archives. Third: nearby-station multichannel capture inside one shared RF window. Fourth: a session brain that keeps the SDR backends from stomping on each other. Fifth: radio-cortex, which turns transcripts into event records and novelty-aware summaries.
The result is not just “radio with subtitles.” It is an indexed, replayable, timestamped record of what the air was saying, with enough interpretation to notice when a NOAA loop changes, when an FM ad contains a real event worth preserving, or when the monitor itself saw a station change and wrote it down.
radio / radio_monitor.py
Headless tuning, status, transcription, and decoder visibility for the single-station path.
radio_archive.py
Rolling or permanent audio capture with sidecar metadata and transcripts.
radio_multichannel.py
One center frequency, several nearby channels, one optional live playback channel.
radio_session.py
SDR ownership, health checks, and stale-primary cleanup when gqrx decides to become half-dead.
radio-cortex
Event classification, coalescing, novelty filtering, and disk-backed working memory.
advisory, not another bland weather line quietly dissolved into filler. When the source is known weather mode, cortex now treats that as an assumption instead of waiting to be talked into it by a transcript fragment having a good day.
content_type, artist, location, date, and event_type, and the OpenAI path now uses structured outputs so those fields survive more often. Slower enrichment now runs every three minutes instead of continuously, with a manual “infer now” escape hatch in the monitor when patience runs out first.
events.jsonl, but duplicate settle-state churn is now debounced instead of being logged like a nervous breakdown.
gqrx is alive enough to hold a PID and dead enough to ignore the remote socket, the session layer eventually stops letting that corpse block the SDR forever.
{
"type": "advisory",
"content_type": "weather_advisory",
"summary": "weather advisory: elevated fire weather conditions develop tomorrow",
"detailed_summary": "Elevated fire weather conditions will develop again tomorrow with dry air and stronger winds.",
"full_text": "Elevated fire weather conditions will develop again tomorrow..."
}
{
"type": "event",
"content_type": "concert",
"entity": "Empire of the Sun",
"location": "Dos Equis Pavilion",
"date": "2026-09-20",
"event_type": "concert"
}
{
"type": "system",
"content_type": "station_change",
"summary": "station changed to 162.550000 MHz FM / 12000"
}
RF -> demod -> transcript -> events.jsonl -> working_memory.json -> "what changed?"
162.550 MHz weather radio, transcription is live, cortex reports mode: weather with a current-conditions inference, and the neighboring terminals show the structured event stream plus cortex working-memory/state output.{"ts":"2026-03-21T12:14:47-0500","type":"system","content_type":"receiver_metadata_update","summary":"receiver metadata updated on 162.550000 MHz","metadata":{"audio_source":"sink-input:3340","transcribe_backend":"vosk","transcribe_model":"vosk:en-us","cortex_backend":"llama_cli","cortex_model":"/home/david/.cache/models/Llama-3.2-1B-Instruct-Q4_K_M.gguf"}}
{"ts":"2026-03-21T12:21:12-0500","type":"station_id","content_type":"station_identification","radio_mode":"weather","summary":"At 20 p.m. Central Daylight Time, station identification announcing NOAA All Hazards Radio.","entity":"Station KEC55"}
{"ts":"2026-03-21T12:22:08-0500","type":"weather","content_type":"weather_report","radio_mode":"weather","summary":"Current conditions (noon / 20 p.m. CDT): sunny with temperatures 79-82F and south winds 7-13 mph, pressure 29.93 in falling."}
{"ts":"2026-03-21T12:22:50-0500","type":"weather","content_type":"weather_report","radio_mode":"weather","summary":"weather report: Today in the Dallas/Fort Worth Metroplex: sunny with highs in the lower 90s; tonight clear with lows in the mid 60s and light winds."}
The stack is explicit about hardware constraints. One HackRF does not magically receive anything and everything at once. What it can do is capture a usable band around one center frequency and split several nearby channels inside that window. The software now respects that instead of pretending a single audio stream is the whole story.
That is why there are separate paths for single-station gqrx, archive jobs, and direct multichannel SDR capture. The session layer sits above them because otherwise you get the usual charming failure mode where every process believes it owns the same radio and none of them are technically wrong enough to stop.
162.55 MHz and 162.4 MHz: viable together inside one weather-band capture window.
103.7 MHz and 162.55 MHz: not viable together on one HackRF in the current setup. Physics remains rude.
Rolling FLAC segments, metadata, transcripts, and status files for one already-demodulated source.
Parallel nearby-channel demodulation with per-channel audio and transcript sidecars.
Classification, coalescing, novelty suppression, SOL log lines, and a durable working memory.
radio monitor --transcribe --all-local radio archive start wx --cache-hours 1 --transcribe radio multichannel start wxband \ --center 162.55 \ --channel main:162.55:nfm:play \ --channel alt:162.4:nfm \ --transcribe radio cortex start tail -f ~/.local/state/radio/events.jsonl tail -f ~/.local/state/radio/sol_log.txt cat ~/.local/state/radio/working_memory.json
events.jsonl
Structured event history from transcripts, DTMF hits, monitor metadata, and station-change events.
sol_log.txt
Short human-readable running log of what the air seems to be doing.
working_memory.json
Recent summaries, known entities, dominant type, suppression counts, and cycle hints that survive restarts.
decoder.log and decoder_hits.log
Raw digital decoder chatter plus the heuristic “maybe this is real” stream.
2026-03-21T12:21:12-0500 [unknown] Band log: At 20 p.m. Central Daylight Time, station identification announcing NOAA All Hazards Radio.. Recent pattern stays station_id. 2026-03-21T12:22:08-0500 [unknown] Weather rolls in: Current conditions (noon / 20 p.m. CDT): sunny with temperatures 79-82F and south winds 7-13 mph, pressure 29.93 in falling.. 2026-03-21T12:22:55-0500 [unknown] Weather rolls in: weather report: Current observations and forecast for today/tonight/Sunday for the Dallas/Fort Worth area: sunny with highs in the lower 90s, tonight clear with lows in the mid 60s.. 2026-03-21T12:22:50-0500 [unknown] Weather rolls in: weather report: Today in the Dallas/Fort Worth Metroplex: sunny with highs in the lower 90s; tonight clear with lows in the mid 60s and light winds..
{
"last_updated": "2026-03-21T12:22:55-0500",
"patterns": {
"dominant_type": "weather",
"cycle_length": 61.0,
"last_cycle_phase": "forecast",
"last_change_reason": "new_information"
},
"recent_summaries": [
"At 20 p.m. Central Daylight Time, station identification announcing NOAA All Hazards Radio.",
"Current conditions (noon / 20 p.m. CDT): sunny with temperatures 79-82F and south winds 7-13 mph, pressure 29.93 in falling.",
"weather report: Today in the Dallas/Fort Worth Metroplex: sunny with highs in the lower 90s; tonight clear with lows in the mid 60s and light winds."
],
"stats": {
"emitted_events": 416,
"suppressed_events": 32,
"anomalies": 8
},
"known_entities": {
"NOAA All Hazards Radio": 1,
"Station KEC55": 2,
"Dallas/Fort Worth Metroplex": 2
}
}
{"type":"system","content_type":"receiver_metadata_update","summary":"receiver metadata updated on 162.550000 MHz"}
{"type":"station_id","content_type":"station_identification","radio_mode":"weather","summary":"At 20 p.m. Central Daylight Time, station identification announcing NOAA All Hazards Radio."}
{"type":"weather","content_type":"weather_report","radio_mode":"weather","summary":"Current conditions (noon / 20 p.m. CDT): sunny with temperatures 79-82F and south winds 7-13 mph, pressure 29.93 in falling."}
{"type":"weather","content_type":"weather_report","radio_mode":"weather","summary":"weather report: Today in the Dallas/Fort Worth Metroplex: sunny with highs in the lower 90s; tonight clear with lows in the mid 60s and light winds."}
curl -O http://127.0.0.1:8888/install_radio_stack.sh bash install_radio_stack.sh # Optional heavier bits bash install_radio_stack.sh --with-nemo bash install_radio_stack.sh --with-llama-cpp bash install_radio_stack.sh --with-caddy
ffmpeg, gqrx-sdr, hackrf, gnuradio, gr-osmosdr, multimon-ng, xvfb, OCR tools, and the rest of the machine glue.
openai, vosk, and PyYAML for the scripts that run under normal python3, plus a dedicated ~/.venvs/radio for the multichannel backend.
OPENAI_API_KEY still matters unless you explicitly run the stack in --all-local mode.
llama-cpp-python stay opt-in because disk space, compile time, and wheel drama are all capable of petty revenge.
There is a useful boundary here. The system is not claiming to “understand radio” in any grand sense. It does something more honest: it preserves the raw material, compresses repetition, extracts structure when available, and keeps enough continuity to notice when something changed.
That is enough to make weather radio less numbing, FM chatter more searchable, and local RF history less disposable. It also leaves a clear list of next steps: a proper NOAA phase model, replay across channels, and a shared real-time transcript bus so the monitor path stops living slightly off to the side like a neglected cousin.
A little ham rig, a little black-box recorder, and a little local sensory cortex.
gqrx can remain brittle, event extraction still depends on transcript quality, and the monitor transcript path is configured before it is fully fed. Systems engineering: one indignity after another.