Signal Systems / Local AI / RF Memory

Radio Stack Showcase

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.

Single Station Monitor live RF, transcript, decoder chatter, and gain state in one terminal view.
Shared Capture Window Record several nearby channels at once when they fit inside the same practical HackRF span.
Persistent Local Memory Suppress repeated forecast loops and keep the changed information instead of logging every sermon.
01 / What It Actually Does

From RF noise to structured memory

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.

  1. radio / radio_monitor.py Headless tuning, status, transcription, and decoder visibility for the single-station path.
  2. radio_archive.py Rolling or permanent audio capture with sidecar metadata and transcripts.
  3. radio_multichannel.py One center frequency, several nearby channels, one optional live playback channel.
  4. radio_session.py SDR ownership, health checks, and stale-primary cleanup when gqrx decides to become half-dead.
  5. radio-cortex Event classification, coalescing, novelty filtering, and disk-backed working memory.
02 / The Useful Examples

Weather loop versus human world

NOAA / low entropy
Repeated forecast cycles get merged and suppressed. Instead of logging “sunny, clear, winds south” every pass, cortex coalesces adjacent weather fragments, suppresses low-value repeats, and remembers when the forecast is unchanged.
NOAA / signal shift
Actual changes stand out. “Elevated fire weather conditions will develop again tomorrow” becomes an 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.
FM / high entropy
Commercial chatter can become a structured event. A line like “Empire of the Sun at Dos Equis on September 20th” can carry 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.
Monitor / metadata
Station changes now land in the event stream. Monitor startup state, retunes, and meaningful UI/RDS metadata changes are appended to events.jsonl, but duplicate settle-state churn is now debounced instead of being logged like a nervous breakdown.
Session / cleanup
The stack now remembers its own wounds. If 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.
Example Event Shapes
{
  "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?"
Actual desktop capture from this machine: the monitor is tuned to 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.
Live Event Excerpt
{"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."}
Current Bundle
Download the latest compiled snapshot or grab the installer script.
03 / Why The Architecture Matters

One tuner, several jobs, fewer lies

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.

Practical Constraint

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.

Archive

Rolling FLAC segments, metadata, transcripts, and status files for one already-demodulated source.

Multichannel

Parallel nearby-channel demodulation with per-channel audio and transcript sidecars.

Cortex

Classification, coalescing, novelty suppression, SOL log lines, and a durable working memory.

04 / Operator View

The files that make it real

Useful Commands
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.
  • Logging contract in the README The bundle now documents which files are authoritative, which are transient, and which are just telemetry wearing a serious expression.
  • decoder.log and decoder_hits.log Raw digital decoder chatter plus the heuristic “maybe this is real” stream.
tail -f ~/.local/state/radio/sol_log.txt
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..
cat ~/.local/state/radio/working_memory.json
{
  "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
  }
}
tail -f ~/.local/state/radio/events.jsonl
{"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."}
05 / Installation

One script, then the usual caveats

Installer Usage
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
  • System packages Installs the apt-level runtime: ffmpeg, gqrx-sdr, hackrf, gnuradio, gr-osmosdr, multimon-ng, xvfb, OCR tools, and the rest of the machine glue.
  • Python runtime Installs openai, vosk, and PyYAML for the scripts that run under normal python3, plus a dedicated ~/.venvs/radio for the multichannel backend.
  • Current default inference path The bundled cortex config currently defaults to OpenAI, so OPENAI_API_KEY still matters unless you explicitly run the stack in --all-local mode.
  • Optional heavier extras NeMo and llama-cpp-python stay opt-in because disk space, compile time, and wheel drama are all capable of petty revenge.
06 / What Makes It Interesting

Not just transcription, not quite understanding

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.

Current Character

A little ham rig, a little black-box recorder, and a little local sensory cortex.

Still Rough

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.