A meeting recording and transcription app for Linux. Records microphone and system audio separately, then uses Google Gemini for transcription with speaker diarization.
- Dual-Channel Recording: Captures your microphone and system audio (meeting participants) as separate tracks
- Non-Invasive: Uses PipeWire monitor ports - works alongside Google Meet, Zoom, etc. without interference
- AI Transcription: Google Gemini 2.5 Flash with speaker diarization, summaries, and action items
- Google Calendar Integration: Meetings-first view with automatic recording-to-meeting linking
- Seamless Mic Switching: Change microphones mid-recording without interruption
- Audio Compression: Automatic WAV→FLAC conversion after transcription (~50% space savings)
- Bluetooth Support: Works with Bluetooth headsets in HFP/HSP mode
- Device Hot-Plug: Automatically detects when audio devices are connected/disconnected
- Pause/Resume: Pause recording during breaks without creating multiple files
┌─────────────────────────────────────────────────────────────┐
│ Python Application │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ PyQt6 GUI │ │ Gemini │ │ SQLite Storage │ │
│ └──────┬───────┘ │ Transcription│ └──────────────────┘ │
│ │ └──────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ quinoa_audio (Rust + PyO3) │ │
│ │ PipeWire capture, device management │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ PipeWire │
└───────────────────────┘
Why Rust for audio? PipeWire requires a dedicated event loop and low-latency buffer handling. Rust provides safety and performance, exposed to Python via PyO3.
Why separate tracks? Recording mic (left) and system audio (right) separately helps Gemini distinguish between you and remote participants.
quinoa/
├── quinoa/ # Python application
│ ├── main.py # Entry point
│ ├── config.py # Configuration (keyring for API key)
│ ├── constants.py # Application constants
│ ├── logging.py # Logging configuration
│ ├── audio/ # Audio processing utilities
│ │ ├── converter.py # WAV→FLAC compression
│ │ └── compression_worker.py # Background compression
│ ├── calendar/ # Google Calendar integration
│ │ ├── auth.py # OAuth authentication
│ │ ├── client.py # Calendar API client
│ │ └── sync_worker.py # Background sync
│ ├── search/ # Gemini File Search
│ │ ├── file_search.py # File Search manager
│ │ └── sync_worker.py # Background sync
│ ├── storage/database.py # SQLite operations
│ ├── transcription/
│ │ ├── gemini.py # Gemini API client
│ │ └── processor.py # Audio mixing for transcription
│ └── ui/
│ ├── main_window.py # Main GUI
│ ├── calendar_panel.py # Left panel (meetings list)
│ ├── middle_panel.py # Notes/transcript viewer
│ ├── right_panel.py # AI chat assistant
│ ├── settings_dialog.py # Settings
│ ├── styles.py # UI stylesheets
│ └── ...
│
├── quinoa_audio/ # Rust audio library
│ └── src/
│ ├── lib.rs # PyO3 bindings
│ ├── capture/
│ │ ├── session.rs # Recording session (with mic switching)
│ │ └── encoder.rs # WAV encoding
│ └── device/
│ ├── enumerate.rs # Device discovery
│ └── monitor.rs # Hot-plug monitoring
│
└── tests/
├── python/ # Integration tests
└── manual/ # Manual test scripts
- Rust (latest stable)
- Python 3.12+
- uv (Python package manager)
- PipeWire development headers
# Fedora
sudo dnf install pipewire-devel clang clang-devel
# Ubuntu/Debian
sudo apt install libpipewire-0.3-dev clang# Create virtual environment and install dependencies
uv venv
source .venv/bin/activate
uv pip install -e .
# Install dev dependencies (optional)
uv pip install -e ".[dev]" # Includes ruff, mypy, pytest
# Build Rust extension with PipeWire support
cd quinoa_audio
maturin develop --features real-audio
cd ..# Option 1: Set API key via environment (temporary)
export GEMINI_API_KEY="your_key"
python -m quinoa.main
# Option 2: Set via Settings dialog (stored in system keyring)
python -m quinoa.main
# Then go to Settings and enter your API key# Run with mock audio backend (no PipeWire needed)
cd quinoa_audio
maturin develop # Without --features real-audio
cd ..
python -m quinoa.main --test
# Run integration tests
pytest tests/python/
# Lint and type check
ruff check quinoa/ tests/
mypy quinoa/- Select Microphone from the dropdown (auto-detects default)
- Check "Record System Audio" to capture meeting participants
- Click "Start Recording" - watch the VU meters for audio levels
- Pause/Resume as needed during breaks
- Click "Stop Recording" when done
- Click "Transcribe" to send to Gemini
- View History tab for past recordings and transcripts
| Shortcut | Action |
|---|---|
Ctrl+R |
Start/Stop Recording |
Space |
Pause/Resume |
Ctrl+Q |
Quit |
| Data | Location |
|---|---|
| Recordings | ~/Music/Quinoa/{session_id}/ |
| Database | ~/.local/share/quinoa/quinoa.db |
| Config | ~/.config/quinoa/config.json |
| API Key | System keyring (secure) |
Each recording session creates:
~/Music/Quinoa/rec_20241115_143022/
├── microphone.wav # Your voice (converted to .flac after transcription)
├── system.wav # Meeting participants (converted to .flac after transcription)
└── mixed_stereo.wav # Combined for transcription (converted to .flac after transcription)
After transcription, WAV files are automatically compressed to FLAC (~50% space savings). Original WAV files are kept as backup.
# Check PipeWire is running
systemctl --user status pipewire
# List PipeWire nodes
pw-cli list-objects | grep -E "Audio/(Source|Sink)"Bluetooth headsets in A2DP (music) mode don't expose a microphone. Start a call or manually switch to HFP/HSP mode:
# Check current profile
pactl list cards | grep -A 20 "bluez"
# Switch to headset mode (enables mic)
pactl set-card-profile bluez_card.XX_XX_XX_XX_XX_XX headset-head-unit- Check VU meters during recording - they should move when you speak
- Verify correct microphone is selected
- Check system audio is playing through expected output device
- Verify API key is set (Settings dialog or
GEMINI_API_KEYenv var) - Check network connectivity
- Ensure audio files exist in the recording directory
MIT