Murmur and it types.
Voice-to-text and instant voice translation for Linux/Wayland. Press a hotkey, speak, press again — your words appear in the focused window. Powered by Whisper via Groq (free, blazing fast). Single Python file, zero dependencies.
Works with niri, sway, Hyprland, and any Wayland compositor.
Most Linux voice-to-text tools require heavy setups (local models, Python packages, system services). murmur-type is a single file with no dependencies — just Python stdlib, a Groq API key, and your Wayland compositor. It takes 30 seconds to set up.
- Voice → Type — speak in any language, text appears in the focused window (VSCode, terminal, browser, anywhere)
- Voice Translate — say a word in one language, see the translation + 3 context examples in a rofi popup with the word underlined in each sentence
- Webhook Integration — press Enter in the popup to save translations to any REST API (flashcard apps, Notion, Anki, custom backends)
- Multi-language — separate hotkeys per language, or auto-detect
- Toggle design — one hotkey starts recording, same hotkey stops and processes. No daemon, no background service
- Single file — one Python script, stdlib only. No pip, no venv, no Docker
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐
│ Mic │───→│ pw-record │───→│ Groq │───→│ wtype │
│ (hotkey) │ │ (PipeWire) │ │ Whisper │ │ (type) │
└──────────┘ └─────────────┘ └──────────┘ └─────────┘
Translate mode:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌──────────┐
│ Mic │───→│ Whisper │───→│ LLM │───→│ rofi │───→│ Webhook │
│ (hotkey) │ │ (STT) │ │ (translate)│ │ popup│ │ (save) │
└──────────┘ └──────────┘ └──────────┘ └──────┘ └──────────┘
The translated word is highlighted with underline in each context sentence:
🇷🇺 выдержка
🇬🇧 endurance
1) His endurance was tested during the marathon.
↳ Его выдержка была проверена во время марафона.
2) The job requires both patience and endurance.
↳ Работа требует как терпения, так и выдержки.
3) She showed remarkable endurance under pressure.
↳ Она проявила замечательную выдержку под давлением.
⏎ Enter = save to vocabulary | Esc = dismiss
- Linux with Wayland (tested on niri + Arch Linux)
- Python 3.10+ (stdlib only, no pip packages)
- PipeWire —
pw-recordfor microphone capture - wtype — types text into the focused Wayland window
- rofi — popup for translate mode
- notify-send — desktop notifications
- Groq API key (free) — for Whisper speech-to-text and LLM translation
sudo pacman -S python pipewire wtype rofi libnotify- Go to https://console.groq.com/keys
- Sign up (free, Google/GitHub login)
- Create an API key
# Clone the repo
git clone https://github.com/Skippia/murmur-type.git
cd murmur-type
# Run the installer
./install.sh
# Edit config with your API key
nano config.jsonThe installer:
- Creates a symlink
~/.local/bin/murmur-type→murmur-type.py - Copies
config.example.json→config.json(if not exists) - Checks that all dependencies are installed
# 1. Clone anywhere
git clone https://github.com/Skippia/murmur-type.git ~/murmur-type
# 2. Create config
cp config.example.json config.json
# Edit config.json — at minimum set "api_key"
# 3. Symlink to PATH
ln -s ~/murmur-type/murmur-type.py ~/.local/bin/murmur-type
# 4. Make sure ~/.local/bin is in your PATHEdit config.json:
{
"provider": "groq",
"api_key": "gsk_YOUR_KEY_HERE",
"model": "whisper-large-v3",
"language": "",
"translate_model": "llama-3.3-70b-versatile",
"app_url": "http://localhost:3009",
"app_login": "",
"app_password": "",
"app_topic_id": ""
}| Field | Description |
|---|---|
provider |
"groq" (recommended) or "openrouter" |
api_key |
Your Groq API key (gsk_...) |
model |
Whisper model: "whisper-large-v3" (best accuracy) or "whisper-large-v3-turbo" (faster) |
| Field | Description |
|---|---|
language |
Default language hint. Leave "" for auto-detect, or set "en", "ru", "uk", etc. |
translate_model |
LLM for translation. Default: "llama-3.3-70b-versatile" |
webhook |
Webhook config for saving translations (optional, see Webhook Integration) |
Add these to your compositor config. Examples below for different compositors:
binds {
Mod+Shift+E hotkey-overlay-title="Voice-to-text (English)" { spawn "murmur-type" "en"; }
Mod+Shift+R hotkey-overlay-title="Voice-to-text (Russian)" { spawn "murmur-type" "ru"; }
Mod+Shift+A hotkey-overlay-title="Voice translate (RU → EN)" { spawn "murmur-type" "translate"; }
}bindsym $mod+Shift+e exec murmur-type en
bindsym $mod+Shift+r exec murmur-type ru
bindsym $mod+Shift+a exec murmur-type translate
bind = $mainMod SHIFT, E, exec, murmur-type en
bind = $mainMod SHIFT, R, exec, murmur-type ru
bind = $mainMod SHIFT, A, exec, murmur-type translate
- Press Mod+Shift+E — notification "Recording (EN)..."
- Speak in English
- Press Mod+Shift+E again — notification "Processing..."
- Transcribed text is typed into the focused window
Same as above but with Mod+Shift+R.
- Press Mod+Shift+A — notification "Recording (RU → EN)..."
- Say a Russian word or phrase
- Press Mod+Shift+A again
- A rofi popup appears with:
- The Russian word you said
- English translation (bold)
- 3 example sentences with the word underlined
- Russian translation for each example
- Enter — saves as a vocabulary card (if app is configured)
- Escape — dismiss
If you use a VPN (e.g., Windscribe) that routes through datacenter IPs, Groq will block your requests with a 403 error. The included groq-route.sh script adds direct routes for Groq's Cloudflare IPs, bypassing the VPN tunnel.
# Install as a systemd service (persists across reboots)
sudo cp groq-route.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now groq-route.serviceThis only affects traffic to Groq's API IPs (172.64.149.20, 104.18.38.236). All other traffic continues through your VPN.
murmur-type/
├── murmur-type.py # Main script (single file, stdlib only)
├── config.json # Your config (gitignored)
├── config.example.json # Config template
├── install.sh # Installer script
├── groq-route.sh # VPN split-tunnel route script
├── groq-route.service # Systemd unit for persistent routes
├── .run/ # Runtime data (gitignored, auto-created)
│ ├── recording.pid # PID of pw-record process
│ ├── recording.wav # Temporary audio file
│ ├── mode # Current recording mode
│ └── app_token # Cached auth token (if webhook uses auth)
└── README.md
When using the translate mode (Mod+Shift+A), pressing Enter in the rofi popup can send the word and translation to any HTTP endpoint. This lets you integrate with flashcard apps, Notion, Anki, Google Sheets, or any service with a REST API.
Set "webhook": null (or omit it) to disable — the rofi popup will still work, just without saving.
Send a POST request with the word and translation to any URL:
{
"webhook": {
"url": "https://your-app.com/api/words",
"body": {
"word": "{{word}}",
"translation": "{{translation}}"
}
}
}{{word}} and {{translation}} are placeholders — they get replaced with the actual values at runtime.
If your API uses an API key or static token:
{
"webhook": {
"url": "https://your-app.com/api/words",
"headers": {
"X-Api-Key": "your-api-key",
"Authorization": "Bearer your-static-token"
},
"body": {
"word": "{{word}}",
"translation": "{{translation}}"
}
}
}If your API requires logging in first to get a JWT token:
{
"webhook": {
"url": "https://your-app.com/api/words",
"body": {
"topicId": "some-category-id",
"word": "{{word}}",
"translation": "{{translation}}"
},
"auth": {
"url": "https://your-app.com/api/auth/login",
"body": {
"login": "your-username",
"password": "your-password"
},
"token_path": "data.token"
}
}
}How the auth flow works:
- On first request, murmur-type sends a POST to
auth.urlwithauth.body - Extracts the JWT token from the response using
token_path(dot notation — e.g.,"data.token"extractsresponse.data.token) - Adds
Authorization: Bearer <token>to the webhook request - Caches the token in
.run/app_tokenso subsequent calls skip the login - If the webhook returns 401 (token expired), automatically re-authenticates and retries once
The body object can contain any structure your API expects. Only {{word}} and {{translation}} are replaced — everything else is sent as-is:
{
"webhook": {
"url": "https://api.notion.com/v1/pages",
"headers": {
"Notion-Version": "2022-06-28",
"Authorization": "Bearer ntn_your_token"
},
"body": {
"parent": { "database_id": "abc123" },
"properties": {
"Word": { "title": [{ "text": { "content": "{{word}}" } }] },
"Translation": { "rich_text": [{ "text": { "content": "{{translation}}" } }] }
}
}
}
}Your IP is blocked (datacenter/VPN). See VPN Split-Tunnel section.
You pressed the hotkey twice too fast. Hold for at least 1 second.
Check that PipeWire is running and your mic is the default source:
pw-record --list-targetsMake sure you're on Wayland (not XWayland). Some apps (e.g., Electron with --disable-gpu) may not receive wtype input.
MIT