A meeting recording and transcription app for Linux. Records microphone and system audio as separate tracks, 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 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.
- Trim UI: Visually cut silence and unwanted regions from recordings.
- AI Chat Assistant: Ask questions across meetings using File Search context.
┌─────────────────────────────────────────────────────────────┐
│ 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 utilities (FFmpeg mixing/trimming)
│ │ ├── converter.py # WAV→FLAC compression
│ │ ├── compression_worker.py # Background compression
│ │ ├── mixer.py # Stereo mix for transcription
│ │ └── trimmer.py # Silence/region trimming
│ ├── calendar/ # Google Calendar integration
│ ├── search/ # Gemini File Search
│ ├── storage/database.py # SQLite operations
│ ├── transcription/ # Gemini API client and manager
│ └── ui/ # PyQt6 UI components
│
├── quinoa_audio/ # Rust audio library
│ └── src/
│ ├── lib.rs # PyO3 bindings
│ ├── capture/ # Recording session, WAV encoding
│ └── device/ # Device discovery and hot-plug monitoring
│
├── tests/
│ ├── python/ # Automated Python tests
│ └── manual/ # Manual test scripts
│
├── scripts/
│ ├── check.sh # Canonical local/CI quality gate
│ └── bundle.sh # Local desktop bundling
│
├── quinoa.desktop # Portable desktop entry template
├── pyproject.toml # Python project + Maturin config
├── quinoa_audio/Cargo.toml # Rust crate features
└── LICENSE # MIT License
- Rust (latest stable)
- Python 3.12+
- uv
- PipeWire development headers
- FFmpeg and ffprobe (runtime dependency for audio mixing and trimming)
System packages:
# Fedora
sudo dnf install pipewire-devel clang clang-devel ffmpeg
# Ubuntu/Debian
sudo apt install libpipewire-0.3-dev clang ffmpegOptional, only for scripts/bundle.sh:
# Fedora
sudo dnf install ImageMagick desktop-file-utils
# Ubuntu/Debian
sudo apt install imagemagick desktop-file-utils# Sync Python dependencies (dev group included)
uv sync --locked --all-groups
# Build the Rust extension for mock audio (no PipeWire needed)
cd quinoa_audio
uv run maturin develop --no-default-features --features extension-module,mock
cd ..
# Or build the real PipeWire extension from the project root
uv run maturin developuv run python -m quinoa.mainThen open Settings and enter your Gemini API key. The app stores the key in
your system keyring (GNOME Keyring, KDE Wallet, etc.) and reads it from there
for transcription and chat. The UI and TranscriptionManager gate transcription
on the keyring value, so setting GEMINI_API_KEY in the environment does not
bypass the Settings/keyring flow.
The canonical quality gate is:
./scripts/check.shIt checks locked dependencies, Ruff format and lint, Mypy, Python tests against a mock audio build, Rust format, Rust real/mock checks and tests, and then restores the real PipeWire extension. It also checks shell syntax and desktop entry validation.
Run only the Python tests with:
uv run pytest tests/pythonuv run python -m quinoa.main --testThis starts a short recording and exits, but it writes a real recording to
~/Music/Quinoa/ and mutates the production database. Run it only when you
intend to create recording state. It is intentionally excluded from
./scripts/check.sh and CI.
- Select Microphone from the dropdown (auto-detects default).
- Check "Record System Audio" to capture meeting participants.
- Click "Start Recording" and watch the VU meters for audio levels.
- Pause/Resume as needed during breaks.
- Click "Stop Recording" when done.
- Click "Transcribe" to send to Gemini (or enable auto-transcribe in Settings).
- View History for past recordings, transcripts, and the trim view.
| 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 a directory such as
~/Music/Quinoa/rec_20241115_143022/ containing:
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. Original WAV files are kept as backup.
scripts/bundle.sh builds a local desktop release:
./scripts/bundle.shIt builds the Rust extension in release mode, creates a wrapper at
~/.local/bin/quinoa, installs the icon, and writes
~/.local/share/applications/quinoa.desktop with Exec and Icon rewritten
to current-user absolute paths. The checked-in quinoa.desktop remains portable
and validates successfully with desktop-file-validate.
Requires ImageMagick (magick) and desktop-file-utils (for
update-desktop-database and gtk-update-icon-cache).
# 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 do not 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 the correct microphone is selected.
- Check system audio is playing through the expected output device.
- Verify the API key is saved in Settings (stored in the system keyring).
- Check network connectivity.
- Ensure audio files exist in the recording directory.
MIT. See LICENSE.