Section 1 — Operation
GMRS-TTY is a desktop app (Python + PySide6) that sits between a GMRS or FRS radio and its operator. Point the radio's audio at a sound card and the app transcribes every transmission into a chat log as it happens; type a reply and the app synthesizes it to speech and keys the radio. The idea traces back to the TTY — the teletypewriter deaf users ran over telephone lines — brought to radio: the author's sister used one, and this is the same conversation model on a different medium.
Everything runs locally. Speech-to-text (faster-whisper), text-to-speech (Piper), and voice activity detection (Silero) are all offline models staged on disk; the core radio workflow never touches the network. The one online feature — FCC callsign verification — is strictly opt-in via a connectivity probe and degrades cleanly when the network is gone.
“This application exists for users with disabilities, so accessibility is a hard design constraint rather than a nice-to-have.” — from the README
The UI targets WCAG 2.1 Level AA: every text color meets contrast, no state is conveyed by color alone, the whole app drives from the keyboard, and every widget is labelled for NVDA / JAWS / Orca / VoiceOver. A rolling audio waterfall below the chat gives deaf and hard-of-hearing operators a visual readout of the channel — squelch breaks, voice formants, and neighbor-channel splatter are visible the moment they arrive.
Section 2 — Receive
The receive pipeline is built around how radio traffic actually behaves, not around clean studio audio.
- Squelch-open pre-trigger — buffering starts the moment the remote carrier opens, so the leading syllables survive VAD onset latency. If the carrier drops without voice ever being detected — a kerchunk, accidental key, or noise burst — the buffer is discarded and nothing reaches the chat.
- Silero VAD gating with a tunable threshold; after ~30 s of continuous silence it re-baselines so detection stays responsive on long-quiet channels.
- Streaming transcription — speech longer than ~5 s is sliced at the quietest point (cuts land between words, not mid-syllable) and the chat line grows in real time instead of waiting for the unkey. When the squelch closes mid-speech, the static crash is trimmed and the utterance finalizes immediately.
- Offline Whisper via faster-whisper (
small.enint8 on CPU by default; any staged model fromtiny.entolarge-v3). An optional two-tier mode re-transcribes each finished utterance with a larger model (large-v3-turbo) on a background thread and upgrades the chat line in place. Optional GPU (CUDA / ROCm) for the final pass; the base install stays CPU-only. - Radio-band DSP — 300–3000 Hz bandpass matched to narrowband FM, spectral-gating noise reduction, a gain stage (dynamic AGC by default), and an optional noise-profile mode that samples the closed-squelch noise floor instead of guessing from the speech.
- Vocabulary biasing — Whisper's prompt is assembled from NATO alphabet, Q-codes, prowords, your custom phrases, and your saved contacts' callsigns, updated live when contacts change.
- Calibration wizard — records a reference passage over the air, then sweeps every staged model × gain mode × noise profile and ranks the combinations by word-error-rate. One click applies the winner.
A monitor toggle routes the raw channel audio to speakers while transcription continues, a listen-only mode blocks every TX path for safe monitoring, and a system-loopback input source transcribes whatever is playing through the computer's own output. The waterfall's FFT runs on its own thread and is designed to hold under 10% CPU on a Raspberry Pi 4.
Section 3 — Transmit
Typed messages go out through Piper, an offline TTS engine with local ONNX voices (five are bundled; speech rate adjusts from 0.70× to 1.50×, with a Test button to audition before saving).
- Three PTT modes — Manual (you key the radio), VOX (the app appends a silence tail so the last syllable survives the hang dropout), or USB FTDI/serial (the app keys PTT through an adapter's RTS or DTR line). VOX radios that clip the first word can be primed with a 1 kHz tone burst or a spoken priming word.
- TX audio conditioning — band-limits synthesized speech to the 300–3000 Hz voice channel, compresses gently, normalizes to −18 dBFS, and caps peaks at −1 dBFS so the voice modulates consistently without splatter.
- Watchdog and abort — a hard cap on transmission length (default 60 s) cancels over-long messages before the radio is ever keyed; a synthesis timeout abandons a stuck synthesis without keying; an Abort TX button (Esc) is the operator's kill switch mid-transmission.
- Shorthand expansion — TTY glossary and CW/radio vernacular
(
GA,SKSK,73,QSL,QTH…) are rewritten to full words before speaking, so the receiver hears “go ahead” rather than “G A”. - Spoken-callsign formatting — digits are read one at a time (“WSLZ 2 3 3”, never “two hundred thirty-three”).
- Quick messages — a configurable strip of one-click presets
(Alt+1…Alt+9), with
{N}-style tokens that prompt for a value before transmitting.
Section 4 — Compliance
FCC Part 95 station-ID rules are enforced by the message flow itself, not left to the operator's memory:
- Messages targeting a specific station are automatically prefaced
[Your call] [Your name] calling [Target]. - The 15-minute ID rule appends your callsign and name whenever more than 15 minutes have passed since the last identification — even on short messages.
- A standalone “This is” button sends a one-click station ID with the NATO-phonetic spelling and resets the timer.
- A GMRS / FRS service toggle switches to unlicensed FRS (Subpart B) operation, which has no callsign requirement — so every callsign feature deliberately shuts off rather than transmitting IDs that don't apply.
- A PG-13 profanity filter (default on) masks strong language in both directions to stay inside Part 95 obscenity expectations.
From the README, verbatim: “You are still responsible for legal operation. This app does not replace a valid FCC GMRS license.”
Section 5 — Installation
The release page carries a
self-contained .deb (x86-64) that bundles Python wheels, the Whisper model, and five
Piper voices — no internet needed after the download.
| Platform | Install method |
|---|---|
| Debian 13 (trixie) / LMDE 7 / Ubuntu 24.10+ | direct apt install |
| Ubuntu 22.04 / 24.04, Mint 21–22, Pop!_OS | install.sh from the release page (adds the deadsnakes PPA for Python 3.13) |
| Debian 12 (bookworm), Ubuntu 20.04 | build from source |
sudo apt install ./gmrs-tty_1.9.0_amd64.deb
gmrs-tty
First run: open Settings → Configuration and set your callsign, name, location, and voice.
From source
Five steps on any Linux with Python 3.11+: pip install -r requirements.txt, fetch
voices with ./scripts/fetch-voices.sh, stage the Whisper model once with
python bootstrap_models.py on a connected machine, copy
config.example.json to config.json, and run python main.py.
For air-gapped targets, copy the Models/ and Voices/ directories along
with the source — nothing else is fetched at runtime. Full detail in the
README and the
user manual.
Section 6 — Specifications
| Runtime | Python 3.11+ (3.13 recommended), PySide6 / Qt |
| Speech-to-text | faster-whisper (CTranslate2), Silero VAD, offline |
| Text-to-speech | Piper, local ONNX voice models, offline |
| Audio interface | Any sound card feeding the radio — USB dongle, Signalink, Digirig |
| PTT hardware | None (manual / VOX), or a USB-serial FTDI adapter (RTS/DTR line) |
| Radio | Any GMRS or FRS HT, mobile, or SDR with an audio path |
| Reference target | Raspberry Pi 4 (waterfall budgeted under 10% CPU) |
| Network | Not required at runtime; FCC callsign lookup is opt-in when online |
| Accessibility | WCAG 2.1 Level AA; NVDA / JAWS / Orca / VoiceOver |
| License | MIT; bundled third-party models keep their own licenses (see NOTICES — the libritts voice is CC BY 4.0) |
Roadmap: multi-arch Docker image (amd64 + arm64), parallel LoRa-mesh transmit (Meshtastic / Meshcore), Bluetooth HT audio, hamlib CAT/CI-V rig control.