Real-time HAM radio transcription & intelligent logbook generation, powered by Apple Silicon GPU acceleration and Google Gemini.
"Zankar" (ΰ€ΰ€Όΰ€ΰ€ΰ€Ύΰ€°) β the resonant vibration of a struck string; the lingering hum that carries meaning through the air.
Zankar is a two-stage, end-to-end pipeline that captures live amateur (HAM) radio transmissions, transcribes them in real-time using OpenAI's Whisper model (accelerated on Apple Silicon via MLX), and then sends the filtered recording + raw transcript to Google Gemini 2.5 Flash β which listens to the audio, cross-references the Whisper transcript, corrects errors, decodes NATO phonetic callsigns, and produces a fully structured amateur radio logbook in JSON.
In short: Plug in your radio β Zankar listens β You get a beautiful, corrected QSO log without writing a single line by hand.
Manual logging during fast-paced QSOs is error-prone and tedious. Zankar eliminates this friction entirely:
- Zero manual logging β focus on the conversation, not the paperwork.
- AI-corrected transcripts β Gemini cross-checks audio against Whisper's raw output to fix misheard callsigns and signal reports.
- Offline-capable β when the internet drops (common in the field), Zankar falls back to a local Gemma 3 model on-device.
- Native Apple Silicon performance β no cloud latency for transcription; Whisper runs directly on your M-series GPU.
ββββββββββββββββ βββββββββββββββββββββββ ββββββββββββββββββββββββ
β Microphone ββββββΆβ live_transcribe.py ββββββΆβ gemini_parser.py β
β / SDR Input β β β β β
ββββββββββββββββ β β’ WebRTC VAD β β β’ Uploads .wav + β
β β’ DSP bandpass β β .log to Gemini β
β β’ MLX Whisper GPU β β β’ Extracts QSOs, β
β β’ Text stabilizer β β callsigns, RST β
β β’ .wav + .log output β β β’ Structured JSON β
βββββββββββββββββββββββ ββββββββββββββββββββββββ
Stage 1 Stage 2
| Feature | Description |
|---|---|
| ποΈ Real-time Capture | 16 kHz mono audio via sounddevice with zero-copy buffering |
| π§ WebRTC VAD | GMM-based Voice Activity Detection β only processes frames containing human speech, ignoring dead air and static |
| π DSP Bandpass Filter | 5th-order Butterworth filter (300 Hz β 3000 Hz) isolates the amateur radio voice band |
| β‘ GPU Acceleration | On-device transcription via mlx-whisper with FP16 precision β no cloud latency |
| π Streaming Stabilization | Prefix-matching algorithm locks confirmed words, showing uncertain suffixes as live previews |
| π Session Logging | UTC-timestamped sentences written to .log files in real time |
| π΅ Clean Audio Export | Concatenates only VAD-verified, DSP-filtered frames into a single .wav for downstream processing |
| Feature | Description |
|---|---|
| π€ Multimodal Analysis | Uploads both filtered .wav audio and Whisper .log transcript to Gemini 2.5 Flash |
| π Offline Fallback | Automatically falls back to local inference via LM Studio, MLX-VLM (Gemma 3), mlx-lm, or llama.cpp (GGUF) when the network is unavailable |
| π€ NATO Phonetic Decoding | Converts spoken callsigns ("Whiskey Two Papa Victor Foxtrot") β W2PVF |
| π Structured QSO Extraction | Date, time, callsign, operator name, QTH, frequency, mode, RST reports |
| π¬ Dialogue Reconstruction | Corrected, speaker-attributed conversation history |
| π Session Summary | Auto-generated paragraph summarizing the entire radio session |
| πΎ JSON Export | Complete structured logbook saved to logs/ |
Project-Zankar/
βββ live_transcribe.py # Stage 1: Real-time VAD + DSP + Whisper transcription
βββ gemini_parser.py # Stage 2: Gemini multimodal log parser (with offline fallback)
βββ test_transcribe.py # Quick test script for file-based transcription
βββ fix_config.py # Config mapper: Hugging Face β MLX-compatible Whisper params
βββ .env.example # Template for environment variables
βββ .gitignore
βββ README.md
βββ recordings/ # Session outputs (.wav audio + .log transcripts)
β βββ session_YYYYMMDD_HHMMSS.wav
β βββ session_YYYYMMDD_HHMMSS.log
βββ logs/ # Structured JSON logbooks from Gemini
β βββ session_YYYYMMDD_HHMMSS_log.json
βββ Test_audio_files/ # Sample audio files for testing
βββ sample.mp3
| Requirement | Details |
|---|---|
| Hardware | Apple Silicon Mac (M1 / M2 / M3 / M4) β MLX requires Apple GPU |
| Python | 3.9 or higher |
| API Key | Google Gemini API key (free tier works) |
| Audio Input | Microphone, SDR receiver, or line-in from your radio |
git clone https://github.com/Yog-1to1-code/Zankar.git
cd Zankarpython3 -m venv venv
source venv/bin/activatepip install mlx-whisper sounddevice numpy scipy webrtcvad \
google-generativeai python-dotenv huggingface_hubπ¦ Full dependency breakdown
| Package | Purpose |
|---|---|
mlx-whisper |
Whisper transcription on Apple Silicon GPU |
sounddevice |
Real-time audio capture from mic / line-in |
numpy |
Numerical array operations |
scipy |
DSP bandpass filter (Butterworth) |
webrtcvad |
Voice Activity Detection (Google's WebRTC VAD) |
google-generativeai |
Google Gemini API client |
python-dotenv |
.env file loader |
huggingface_hub |
Model downloading from Hugging Face |
Optional (for offline fallback):
| Package | Purpose |
|---|---|
mlx-vlm |
Local multimodal inference (Gemma 3 via MLX) |
mlx-lm |
Local text-only inference on MLX |
llama-cpp-python |
Local GGUF model inference via llama.cpp |
Download a Whisper model in MLX format to a local directory:
# Using huggingface_hub (recommended)
python3 -c "
from huggingface_hub import snapshot_download
snapshot_download(
repo_id='mlx-community/whisper-large-v3-turbo',
local_dir='~/Downloads/AI_Models/whisper-large-v3-turbo'
)
"Then run the config fixer to map Hugging Face parameters to MLX format:
python3 fix_config.pyNote: Update the
model_idpath inlive_transcribe.py(line 215) to match your download location.
cp .env.example .envOpen .env and set your Gemini API key:
GEMINI_API_KEY=your_actual_api_key_herepython3 test_transcribe.pyPlace an audio file (e.g., sample.mp3) in the project root and update the path in test_transcribe.py to confirm Whisper is working.
python3 live_transcribe.py- Zankar begins listening on your default microphone / audio input
- The console displays a live, streaming transcript with locked (confirmed) and unstable (in-progress) words
- Press Ctrl+C to stop β the session is saved as:
recordings/session_<timestamp>.wavβ clean, VAD-filtered audiorecordings/session_<timestamp>.logβ UTC-timestamped raw transcript
Example console output:
=================================================================
ποΈ ASYNC HAM TRANSCRIPTION (WebRTC GMM & Prefix Matching)
Logging pure conversation to: recordings/session_20260322_063507.wav
Listening... (Press Ctrl+C to stop & save session)
=================================================================
π£οΈ LU-1ZE LU-1 Zebra Echo Here is New Jersey W2 Papa Victor
π£οΈ W2 Pampa Vector Foxtrot, have a copy.
π£οΈ Argentina and Antarctica returned. My name is Charlie...
... and the signal is five nine [ποΈ...]
# Parse the most recent session automatically
python3 gemini_parser.py
# Or specify a session explicitly
python3 gemini_parser.py recordings/session_20260320_122817Gemini analyzes both the audio and transcript, then outputs:
- A formatted QSO logbook table in the terminal
- A complete structured JSON logbook saved to
logs/
π STRUCTURED HAM RADIO LOGBOOK
=================================================================
π Summary: This session captured a brief but remarkable SSB contact with LU1ZE,
operated by Charlie from the Argentine Research Base in Antarctica aboard
the research vessel HERO...
# Date Time Callsign Name QTH RST S/R Mode
--------------------------------------------------------------------------------
1 2026-03-20 12:28 LU1ZE Charlie Antarctica (HERO) ?/59 SSB
π¬ Contact from Argentine Research Base, operating from vessel HERO.
--------------------------------------------------------------------------------
π¬ FULL DIALOGUE (Corrected):
LU1ZE: W2 Papa Victor Foxtrot, how copy?
W2PVF: LU1ZE, LU1 Zebra Echo. Here is New Jersey, W2 Papa Victor Foxtrot.
LU1ZE: This is LU1 Zebra Echo, calling from Antarctica. My name is Charlie...
{
"qso_entries": [
{
"date_utc": "2026-03-20",
"time_utc": "12:28",
"callsign": "LU1ZE",
"operator_name": "Charlie",
"qth": "Antarctica (Argentine Research Base, vessel HERO)",
"mode": "SSB",
"rst_received": "59",
"remarks": "Contact from Argentine Research Base in Antarctica..."
}
],
"session_summary": "A brief SSB QSO with LU1ZE operating from Antarctica...",
"dialogue": [
{ "speaker": "W2PVF", "text": "LU1ZE, LU1 Zebra Echo. Here is New Jersey, W2 Papa Victor Foxtrot." },
{ "speaker": "LU1ZE", "text": "W2 Papa Victor Foxtrot, how copy?" }
]
}graph LR
A[ποΈ Mic / SDR] --> B[sounddevice\n16kHz Mono]
B --> C[WebRTC VAD\nSpeech Detection]
C -->|Speech| D[Pre-buffer\n1s Lookback]
C -->|Silence| E[Discard]
D --> F[Butterworth\nBandpass 300-3000Hz]
F --> G[MLX Whisper\nGPU Inference]
G --> H[Text Stabilizer\nPrefix Matching]
H --> I[π .log File]
F --> J[π΅ .wav File]
- Capture β Audio streams in at 16 kHz, 32-bit float, mono via
sounddevice - VAD Gating β Each 500ms chunk is evaluated by WebRTC's GMM-based VAD; only speech frames pass
- Pre-buffering β 1 second of audio before detected speech is prepended to catch sentence beginnings
- Bandpass Filter β A 5th-order Butterworth filter (300β3000 Hz) removes noise outside the voice band
- Whisper Transcription β Cleaned audio is fed to
mlx-whisperfor GPU-accelerated FP16 inference - Text Stabilization β Prefix-matching compares consecutive transcriptions, locking words that appear consistently
- Commit & Log β After 2.5s of silence (or 12s max buffer), the sentence is finalized and logged
graph LR
A[π Latest Session] --> B[Upload .wav\nto Gemini File API]
A --> C[Read .log\nTranscript]
B --> D[Gemini 2.5 Flash\nMultimodal Prompt]
C --> D
D --> E[Structured JSON\nQSO Logbook]
E --> F[πΎ logs/]
D -.->|API Failure| G[Local Fallback\nLM Studio / Gemma / GGUF]
G --> E
- File Discovery β Automatically finds the latest
.wav+.logpair inrecordings/ - Gemini Upload β The clean
.wavaudio is uploaded via the Gemini File API - Multimodal Prompt β Gemini receives both the audio and Whisper transcript with instructions to decode callsigns, extract RST reports, and attribute speakers
- Offline Fallback β On network failure, the system cascades through: LM Studio β MLX-VLM (Gemma 3) β mlx-lm (text-only) β llama.cpp (GGUF)
- JSON Extraction β The response is parsed into a structured logbook format
- Cleanup β Uploaded files are removed from Gemini's servers after processing
| Variable | Required | Description |
|---|---|---|
GEMINI_API_KEY |
Yes (for Stage 2) | Google Gemini API key (get one here) |
GEMMA_MODEL_PATH |
No | Path to local fallback model (MLX, GGUF, or HuggingFace ID) |
| Parameter | File | Default | Description |
|---|---|---|---|
FS |
live_transcribe.py:212 |
16000 |
Audio sample rate (Hz) |
CHUNK_SEC |
live_transcribe.py:213 |
0.5 |
Audio chunk duration (seconds) |
POST_SPEECH_SEC |
live_transcribe.py:123 |
2.5 |
Silence before committing a sentence |
MAX_BUFFER_SEC |
live_transcribe.py:124 |
12.0 |
Max buffer before forced commit |
| VAD Aggressiveness | live_transcribe.py:115 |
2 |
WebRTC VAD mode (0β3; higher = stricter) |
| Bandpass Range | live_transcribe.py:119 |
300β3000 Hz |
Voice band isolation |
model_id |
live_transcribe.py:215 |
Local path | Path to Whisper model directory |
| Gemini Model | gemini_parser.py:279 |
gemini-2.5-flash |
Gemini model for parsing |
| Temperature | gemini_parser.py:284 |
0.1 |
Low temperature for factual extraction |
When Stage 2 can't reach the Gemini API (network down, API quota exceeded, etc.), Zankar cascades through local inference backends:
Gemini 2.5 Flash (Cloud)
β β Network Error
βΌ
LM Studio Local Server (localhost:1234)
β β Not Running
βΌ
MLX-VLM (Gemma 3 β Multimodal, Apple Silicon)
β β Model Not Supported
βΌ
mlx-lm (Text-Only, Apple Silicon)
β β Import Error
βΌ
llama.cpp (GGUF β CPU / Metal)
Each level uses the best available input β multimodal backends receive audio + transcript; text-only backends receive the Whisper transcript alone.
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- π Multi-language transcription support
- π» Automatic frequency detection from SDR metadata
- π Web dashboard for browsing and searching session logs
- π ADIF export for integration with standard logging software (e.g., Log4OM, N1MM)
- π Real-time Gemini streaming for live corrected output
- π₯ Speaker diarization for multi-party QSOs
- π± Remote operation support via network audio streaming
- ποΈ Session search and analytics dashboard
This project is open-source and available under the MIT License.
- MLX by Apple β for making on-device ML on Apple Silicon a reality
- OpenAI Whisper β the backbone of our transcription engine
- Google Gemini β multimodal intelligence that understands both audio and text
- WebRTC VAD β battle-tested voice activity detection from Google
- The global amateur radio community β for keeping the airwaves alive π‘
Built with ποΈ by a HAM radio enthusiast who got tired of writing logs by hand.
73 de Zankar π‘