whispypy is a signal-controlled audio transcription daemon written in Python. It enables on-demand audio recording triggered by system signals (SIGUSR2) and provides instant transcription using locally-running AI models. The transcribed text is automatically copied to the clipboard and can optionally be auto-pasted.
This is a Python rewrite of the original whispy by @daaku.
- Signal-controlled recording (SIGUSR2 to start/stop)
- Multiple transcription engines (Whisper, NVIDIA Parakeet, Parakeet INT8), each an independent
TranscriptionEngineimplementation - Audio device discovery and validation
- Clipboard integration (Wayland/X11)
- Auto-paste functionality
- Persistent configuration management
- State file management for external indicators (e.g., Waybar)
The engine-separation refactoring described in docs/refactoring-plan-engine-separation.md has been implemented.
Transcription engines live under src/whispypy/engines/ as independent TranscriptionEngine implementations, and WhispypyDaemon is a pure orchestrator: it records audio, standardizes it to WAV, and delegates transcription to whichever engine it was given.
The main daemon class that orchestrates the recording/transcription workflow.
It holds a TranscriptionEngine instance and never contains engine-specific logic.
Responsibilities:
- Signal handling (SIGINT, SIGUSR2)
- Audio recording lifecycle management (ALSA and PipeWire)
- Converting PipeWire's raw samples to WAV before handing off to the engine
- Clipboard integration
- State file management
- Audio device validation
Key Methods:
_handle_sigusr2(): Toggle recording on/off_start_recording(): Initialize audio capture (WAV via ALSA, raw samples via PipeWire)_convert_raw_to_wav(): Convert PipeWire's raw samples to a standard WAV file_stop_recording_and_transcribe(): Stop capture, convert if needed, and callself.engine.transcribe()validate_device(): Verify the configured audio device is accessible
Manages persistent configuration with caching and validation.
Responsibilities:
- Configuration file I/O
- Config caching with mtime-based invalidation
- Device configuration persistence
- Dotool layout/variant configuration
- Configuration validation
Key Methods:
save_device(): Persist audio device selectionload_device(): Retrieve saved deviceload_dotool_layout(): Get keyboard layout for dotoolvalidate_config(): Validate configuration format and values
Each engine is an independent implementation of the TranscriptionEngine ABC (src/whispypy/engines/base.py), with just three methods: load_model(), transcribe(audio_file: Path) -> str, and get_pipewire_format().
src/whispypy/engines/factory.py builds the right engine from --engine and its associated CLI args.
SherpaOnnxParakeetInt8Transcriber and the sherpa-onnx model auto-download helpers live in src/whispypy/engines/parakeet_onnx_engine.py, since they're only ever used by that engine.
- Models: tiny, base, small, medium, large, large-v2, large-v3
- Dependencies: openai-whisper
- PipeWire format: f32
- Use Case: General-purpose, works out of the box
- Model: nvidia/parakeet-tdt-0.6b-v3
- Dependencies: nemo_toolkit[asr]
- PipeWire format: f32
- Use Case: High-performance ASR with GPU support
- Model: sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8
- Dependencies: sherpa-onnx
- PipeWire format: s16
- Use Case: CPU-friendly quantized model
- Auto-download: Model bundle downloaded on first run
All engines receive a standardized WAV file; none of them deal with ALSA/PipeWire or raw sample formats directly.
-
Device Selection:
- PipeWire (preferred): Uses
pw-recordandpw-cli - ALSA (fallback): Uses
arecord - Device validation before recording
- PipeWire (preferred): Uses
-
Audio Capture:
- Sample rate: 16000 Hz (Whisper's expected rate)
- Channels: 1 (mono)
- ALSA records S16_LE directly into a WAV container
- PipeWire records raw samples in the engine's declared format (
f32ors16, viaengine.get_pipewire_format())
-
Audio Processing:
- ALSA recordings are already WAV; passed straight to
engine.transcribe() - PipeWire recordings are converted to WAV by
WhispypyDaemon._convert_raw_to_wav()(requiressoundfileandnumpy) before being passed toengine.transcribe()
- ALSA recordings are already WAV; passed straight to
- Recording State:
/tmp/whispypy_recording - Ready State:
/tmp/whispypy_ready - Used by external indicators (e.g., Waybar modules)
~/.config/whispypy/config.conf- INI format with
[DEFAULT]section
device: Audio input device namedotool_xkb_layout: Keyboard layout for dotooldotool_xkb_variant: Keyboard variant for dotool
- Wayland:
wl-copy - X11:
xcliporxsel
- Wayland:
wtype,ydotool, ordotool - X11:
xdotool - Terminal detection to avoid pasting in terminal windows
- Python 3.13+
- Type hints throughout
- Logging for debugging and user feedback
- Error handling with graceful fallbacks
test_audio_devices.py: Audio device discovery and validation- Manual testing with
--check-modelflag
- Ruff: Code formatting and linting (
ruff.toml) - MyPy: Static type checking (
mypy.ini)
- Core: openai-whisper, soundfile, numpy
- Optional: nemo_toolkit[asr], sherpa-onnx
- Dev: mypy, ruff, types-requests
When working with this project, AI agents should:
- Start with the README.md to understand features and requirements
- Review whispypy-daemon.py for the main implementation
- Check config.conf.example for configuration format
- Examine test_audio_devices.py for device handling patterns
- Create a new module in
src/whispypy/engines/implementing theTranscriptionEngineABC (load_model(),transcribe(),get_pipewire_format()) - Register it in
src/whispypy/engines/factory.py'screate_engine() - Add any engine-specific CLI arguments in
main()and thread them through to the factory call - Add an availability check in
main()if the engine has optional dependencies (see thenemo/sherpa_onnximportlib.util.find_specchecks) - Update README.md with installation instructions
No changes to WhispypyDaemon are needed: it only calls engine.load_model(), engine.transcribe(), and engine.get_pipewire_format().
- Check constants at top of
whispypy-daemon.py:SAMPLE_RATE,CHANNELS
- Update recording methods:
_start_recording(),_stop_recording_and_transcribe(),_convert_raw_to_wav() - Ensure compatibility with all transcription engines (they only ever see the final WAV file)
- Test with both PipeWire and ALSA
- Add to
ConfigManagerclass - Implement
load_<option>()method - Update
validate_config()if needed - Document in
config.conf.example - Update README.md
- Modify device discovery functions:
discover_pipewire_devices()discover_alsa_devices()
- Update
test_audio_devices.pyfor testing - Ensure backward compatibility with saved devices
- whispypy-daemon.py: Main daemon implementation (recording, device handling, orchestration)
- src/whispypy/engines/base.py:
TranscriptionEngineABC - src/whispypy/engines/whisper_engine.py, parakeet_engine.py, parakeet_onnx_engine.py: Engine implementations
- src/whispypy/engines/factory.py:
create_engine()factory - test_audio_devices.py: Device discovery and testing utility
- config.conf.example: Configuration template
- pyproject.toml: Project metadata and dependencies
- README.md: User-facing documentation
signal.signal(signal.SIGUSR2, self._handle_sigusr2)- SIGUSR2 toggles recording
- SIGINT for graceful shutdown
- Lazy loading on daemon initialization
- Timing logged for performance monitoring
- Graceful error handling with ImportError
- All recordings are standardized to WAV before reaching an engine
- ALSA records WAV directly; PipeWire records raw samples that get converted via
_convert_raw_to_wav() - Each engine declares its preferred PipeWire sample format (
f32ors16) viaget_pipewire_format()
- Created/removed to signal recording state
- Used by external tools (Waybar, etc.)
- Atomic operations for reliability
-
Device Testing:
python test_audio_devices.py
-
Model Verification:
python whispypy-daemon.py --engine <engine> --check-model
-
Integration Testing:
- Start daemon
- Send SIGUSR2 signal
- Verify recording and transcription
- Check clipboard content
- Audio Format Mismatch: The daemon must record in the format the active engine declares via
get_pipewire_format() - Device Validation: Always validate before recording
- Signal Handling: Proper cleanup in signal handlers
- Model Loading: Handle ImportError for optional dependencies
- Clipboard Tools: Check availability before use
- State Files: Clean up on shutdown
- New Engines: Add a new module under
src/whispypy/engines/and register it infactory.py - Audio Backends: Extend device discovery
- Clipboard Backends: Add new clipboard tools
- Configuration: Extend ConfigManager
- State Indicators: Add new state files or protocols
whispypy/
├── whispypy-daemon.py # Main daemon implementation (orchestration, recording, CLI)
├── test_audio_devices.py # Device testing utility
├── config.conf.example # Configuration template
├── pyproject.toml # Project metadata
├── README.md # User documentation
├── AGENTS.md # This file
├── assets/ # Audio beeps and resources
├── docs/ # Additional documentation
│ └── refactoring-plan-engine-separation.md # Engine-separation refactoring plan (implemented)
└── src/whispypy/engines/ # TranscriptionEngine implementations
├── base.py # TranscriptionEngine ABC
├── whisper_engine.py
├── parakeet_engine.py
├── parakeet_onnx_engine.py # Also hosts SherpaOnnxParakeetInt8Transcriber + model auto-download helpers
└── factory.py # create_engine()
When contributing to this project:
- Follow existing code style (Ruff + MyPy)
- Add type hints to all functions
- Update README.md for user-facing changes
- Update AGENTS.md for architectural changes
- Test with multiple engines and audio backends
- Ensure backward compatibility with existing configs
- Original Project: https://github.com/daaku/whispy
- OpenAI Whisper: https://github.com/openai/whisper
- NVIDIA NeMo: https://github.com/NVIDIA/NeMo
- Sherpa-ONNX: https://github.com/k2-fsa/sherpa-onnx