#!/bin/bash # setup-voxd-ptt.sh # # Sets up voxd with push-to-talk on Linux -- a Wispr Flow-style # dictation experience using local speech recognition + cloud AI cleanup. # # Usage: bash setup-voxd-ptt.sh PTT_DEST="$HOME/voxd-ptt.py" SERVICE_FILE="$HOME/.config/systemd/user/voxd-ptt.service" VOXD_CONFIG="$HOME/.config/voxd/config.yaml" BASHRC="$HOME/.bashrc" # ================================================ # WHAT THIS SCRIPT DOES # ================================================ echo "" echo "================================================" echo " voxd Push-to-Talk Setup" echo "================================================" echo "" echo " This script turns voxd into a Wispr Flow-style" echo " push-to-talk dictation tool on Linux." echo "" echo " HOW IT WORKS:" echo " Hold Left Ctrl + Windows key to start recording." echo " Release to stop. Your speech is transcribed locally" echo " on your machine using OpenAI Whisper (offline, private)." echo " The transcribed text is then cleaned up by an AI and" echo " typed directly into whatever app has focus." echo "" echo " DEFAULT HOTKEY: Left Ctrl + Windows key" echo " (Same as Wispr Flow on Windows. You will be asked to" echo " choose your hotkey during setup.)" echo "" echo " AI CLEANUP OPTIONS & COST (prices as of March 2026)" echo "" echo " Cost comparison -- monthly estimate at 50 uses/day:" echo " (assumes ~50 words/dictation, which is ~3 sentences)" echo "" echo " +-----------+---------------------------+----------+------------+" echo " | Option | Model | /use | /month |" echo " +-----------+---------------------------+----------+------------+" echo " | OpenAI | gpt-4o-mini-2024-07-18 | \$0.000058| \$0.09/mo |" echo " | Anthropic | claude-haiku-4-5 | \$0.000450| \$0.68/mo |" echo " | Local | (your Ollama model) | free | free |" echo " | None | no cleanup | free | free |" echo " +-----------+---------------------------+----------+------------+" echo "" echo " If you dictate MORE (200 uses/day, ~1 paragraph each):" echo " OpenAI: ~\$0.93/month Anthropic: ~\$7.38/month" echo "" echo " Pricing sources (verify before purchasing):" echo " OpenAI: https://openai.com/api/pricing/" echo " Anthropic: https://www.anthropic.com/pricing" echo "" echo " 1) OpenAI API (recommended)" echo " Fast, accurate, cheapest cloud option." echo " NOT used for training. Retained 30 days (abuse monitoring)." echo " Verify: https://developers.openai.com/api/docs/guides/your-data" echo "" echo " 2) Anthropic API" echo " Excellent quality, ~8x more expensive than OpenAI." echo " NOT used for training. Deleted within 30 days." echo " (The 5-year policy only applies to Claude.ai consumer accounts," echo " NOT the API. API data is explicitly excluded.)" echo " Verify: https://www.anthropic.com/news/updates-to-our-consumer-terms" echo " https://privacy.anthropic.com/en/articles/10023548-how-long-do-you-store-personal-data" echo "" echo " 3) Local model (Ollama / llama.cpp)" echo " 100% offline -- nothing leaves your machine, ever." echo " No ongoing cost after setup." echo " Slower and less accurate than cloud options." echo " Requires Ollama or llama.cpp already installed." echo "" echo " 4) No AI cleanup" echo " Raw transcription only. No cost, no external connections." echo " Text will include filler words and grammar errors." echo "" echo " PRIVACY NOTE:" echo " Your VOICE never leaves your machine. voxd transcribes" echo " speech locally using Whisper. Only the resulting TEXT" echo " is sent to the cloud AI -- the same as pasting a" echo " sentence into a search engine." echo "" echo " Both OpenAI and Anthropic API are explicitly excluded" echo " from the consumer data-training policies that made news" echo " in 2025. API data is NOT used for training and is" echo " deleted within 30 days (for abuse monitoring only)." echo " For zero data leaving your machine, choose local model." echo "" echo " WHAT WILL HAPPEN:" echo " - Check you are on X11 (required) -- script/assist if not" echo " - Install voxd if missing" echo " - Ask for your preferred push-to-talk hotkey" echo " - Ask for AI provider and API key" echo " - Configure voxd AIPP settings" echo " - Remove any conflicting keyboard shortcuts" echo " - Install pynput and create ~/voxd-ptt.py" echo " - Set up systemd to autostart on login and restart on crash" echo "" read -rp " Press Enter to continue or Ctrl+C to cancel..." echo "" # ================================================ # 0. Detect X11 vs Wayland # ================================================ echo "[0/10] Checking display server..." SESSION_TYPE="${XDG_SESSION_TYPE:-unknown}" DESKTOP="${XDG_CURRENT_DESKTOP:-unknown}" if [ "$SESSION_TYPE" = "wayland" ]; then echo "" echo " !! WAYLAND DETECTED !!" echo "" echo " You are running Wayland. This tool requires X11." echo "" echo " WHY: The push-to-talk script listens for key press and" echo " release events globally -- across all apps at once." echo " Wayland's security model blocks this by design." echo " No workaround exists. X11 is required." echo "" echo " X11 vs Wayland in plain English:" echo " X11 (older): apps can see each other's input -- global" echo " hotkeys work, accessibility tools work, this works." echo " Wayland (newer): apps are isolated for security -- global" echo " hotkeys are blocked, which breaks push-to-talk." echo "" # Try to script the switch via GDM3 config GDM_CONF="" [ -f /etc/gdm3/custom.conf ] && GDM_CONF="/etc/gdm3/custom.conf" [ -f /etc/gdm/custom.conf ] && GDM_CONF="/etc/gdm/custom.conf" if [ -n "$GDM_CONF" ]; then echo " GDM config found at $GDM_CONF" echo " This can be patched automatically to force X11." echo "" read -rp " Patch $GDM_CONF to force X11 and log out? (y/N): " PATCH_GDM echo "" if [ "${PATCH_GDM,,}" = "y" ]; then echo " Patching $GDM_CONF..." # Uncomment or add WaylandEnable=false under [daemon] if grep -q "^#*WaylandEnable" "$GDM_CONF"; then sudo sed -i 's/^#*WaylandEnable=.*/WaylandEnable=false/' "$GDM_CONF" else # Add it under [daemon] section sudo sed -i '/^\[daemon\]/a WaylandEnable=false' "$GDM_CONF" fi echo " Done. WaylandEnable=false set in $GDM_CONF" echo "" echo " You need to log out for this to take effect." echo " At the login screen, select your normal session" echo " (NOT the Wayland option) and log back in." echo " Then re-run this script." echo "" read -rp " Log out now? (y/N): " DO_LOGOUT if [ "${DO_LOGOUT,,}" = "y" ]; then echo " Logging out..." # Try common logout commands if command -v gnome-session-quit > /dev/null 2>&1; then gnome-session-quit --logout --no-prompt elif command -v loginctl > /dev/null 2>&1; then loginctl terminate-session "$XDG_SESSION_ID" else echo " Could not log out automatically." echo " Please log out manually, then re-run this script." fi else echo " Please log out manually, then re-run this script." fi exit 0 fi fi # No GDM or user declined -- give manual instructions echo " HOW TO SWITCH TO X11 MANUALLY:" echo "" case "$DESKTOP" in *GNOME*|*gnome*|*ubuntu*|*Ubuntu*) echo " Ubuntu / GNOME:" echo " 1. Log out (top-right menu -> Log Out)" echo " 2. Click your username at the login screen" echo " 3. Click the gear icon (bottom-right)" echo " 4. Select 'GNOME on Xorg'" echo " 5. Log in, then re-run this script" ;; *Cinnamon*|*cinnamon*|*X-Cinnamon*) echo " Linux Mint Cinnamon:" echo " 1. Log out (Menu -> Log Out)" echo " 2. Click your username at the login screen" echo " 3. Click the session icon near your name" echo " 4. Select 'Cinnamon' (not 'Cinnamon on Wayland')" echo " 5. Log in, then re-run this script" ;; *KDE*|*kde*|*plasma*) echo " KDE Plasma:" echo " 1. Log out" echo " 2. At the login screen find the session selector" echo " 3. Select 'Plasma (X11)'" echo " 4. Log in, then re-run this script" ;; *) echo " 1. Log out of your current session" echo " 2. At the login screen find a session selector" echo " (gear icon or dropdown near your username)" echo " 3. Choose the X11 or Xorg option" echo " 4. Log in, then re-run this script" ;; esac echo "" echo " After switching: echo \$XDG_SESSION_TYPE (should say x11)" echo "" exit 1 elif [ "$SESSION_TYPE" = "x11" ]; then echo " X11 confirmed -- good." else echo " Session type '$SESSION_TYPE' -- assuming X11, proceeding." fi echo "" # ================================================ # 1. Install voxd if not already installed # ================================================ echo "[1/10] Checking for voxd..." if command -v voxd > /dev/null 2>&1; then VOXD_VER=$(voxd --version 2>/dev/null | head -1 || echo "installed") echo " voxd already installed ($VOXD_VER) -- skipping." else echo " voxd not found. Downloading latest release..." LATEST_DEB=$(curl -s https://api.github.com/repos/jakovius/voxd/releases/latest \ | grep "browser_download_url" \ | grep "amd64.deb" \ | head -1 \ | cut -d '"' -f 4) if [ -z "$LATEST_DEB" ]; then echo " ERROR: Could not find voxd download URL." echo " Install manually from https://github.com/jakovius/voxd/releases" exit 1 fi echo " Downloading $LATEST_DEB ..." TMP_DEB=$(mktemp /tmp/voxd_XXXXXX.deb) curl -L -o "$TMP_DEB" "$LATEST_DEB" echo " Installing (requires sudo)..." sudo dpkg -i "$TMP_DEB" sudo apt-get install -f -y 2>/dev/null || true rm -f "$TMP_DEB" echo " Running voxd first-time setup..." voxd --setup echo " voxd installed." fi echo "" # ================================================ # 2. Ask for AI provider and API key # ================================================ # ================================================ # 2. Choose push-to-talk hotkey # ================================================ echo "[2/10] Choose your push-to-talk hotkey..." echo "" echo " This is the key you hold while speaking." echo " Release it to stop and transcribe." echo "" echo " 1) Left Ctrl + Windows key (default, same as Wispr Flow)" echo " 2) Right Ctrl alone (no Windows key needed)" echo " 3) Scroll Lock alone (dedicated PTT key, no conflicts)" echo " 4) Pause/Break alone (good if Scroll Lock is used elsewhere)" echo " 5) Custom (you enter the pynput key name)" echo "" read -rp " Enter 1-5: " HOTKEY_CHOICE echo "" PTT_KEY="keyboard.Key.ctrl_l" PTT_MODIFIER="keyboard.Key.cmd" case "$HOTKEY_CHOICE" in 1) PTT_KEY="keyboard.Key.ctrl_l" PTT_MODIFIER="keyboard.Key.cmd" echo " Hotkey: Left Ctrl + Windows key" ;; 2) PTT_KEY="keyboard.Key.ctrl_r" PTT_MODIFIER="None" echo " Hotkey: Right Ctrl" ;; 3) PTT_KEY="keyboard.Key.scroll_lock" PTT_MODIFIER="None" echo " Hotkey: Scroll Lock" ;; 4) PTT_KEY="keyboard.Key.pause" PTT_MODIFIER="None" echo " Hotkey: Pause/Break" ;; 5) echo " Enter the pynput key name for your main key." echo " Examples: keyboard.Key.f13 / keyboard.KeyCode.from_char('\`')" read -rp " PUSH_TO_TALK_KEY = " PTT_KEY echo " Enter modifier key, or press Enter for none." echo " Examples: keyboard.Key.alt_l / keyboard.Key.cmd" read -rp " MODIFIER_KEY = (Enter for None): " PTT_MODIFIER [ -z "$PTT_MODIFIER" ] && PTT_MODIFIER="None" echo " Hotkey: $PTT_KEY + $PTT_MODIFIER" ;; *) PTT_KEY="keyboard.Key.ctrl_l" PTT_MODIFIER="keyboard.Key.cmd" echo " Invalid choice -- using default: Left Ctrl + Windows key" ;; esac echo "" echo "[3/10] AI Post-Processing (AIPP) setup..." echo "" echo " Choose how to clean up your transcriptions:" echo "" echo " 1) OpenAI API (recommended)" echo " Fast, cheap (~\$0.002/1000 words), excellent accuracy." echo " Text sent to OpenAI servers. NOT used for training." echo " Retained max 30 days for abuse monitoring only." echo " Get key: https://platform.openai.com/api-keys" echo "" echo " 2) Anthropic API" echo " Excellent quality. Similar privacy to OpenAI API." echo " API usage is NOT used for model training." echo " Get key: https://console.anthropic.com/settings/keys" echo "" echo " 3) Local model (llama.cpp / Ollama)" echo " 100% offline. Nothing leaves your machine." echo " Slower and less accurate than cloud options." echo " Requires Ollama or llama.cpp already installed." echo "" echo " 4) Skip / no AI cleanup" echo " Raw transcription only. No cleanup, no API key needed." echo "" read -rp " Enter 1, 2, 3, or 4: " PROVIDER_CHOICE echo "" PROVIDER="" API_KEY="" API_KEY_VAR="" MODEL="" case "$PROVIDER_CHOICE" in 1) PROVIDER="openai" API_KEY_VAR="OPENAI_API_KEY" MODEL="gpt-4o-mini-2024-07-18" echo " OpenAI selected." read -rsp " Paste your OpenAI API key (input hidden): " API_KEY echo "" ;; 2) PROVIDER="anthropic" API_KEY_VAR="ANTHROPIC_API_KEY" MODEL="claude-haiku-4-5-20251001" echo " Anthropic selected." read -rsp " Paste your Anthropic API key (input hidden): " API_KEY echo "" ;; 3) PROVIDER="ollama" MODEL="llama3.2:latest" echo " Local model selected." echo " Make sure Ollama is running: ollama serve" echo " And model is pulled: ollama pull llama3.2" ;; 4) echo " Skipping AI cleanup." ;; *) echo " Invalid choice -- skipping AI cleanup." ;; esac echo "" # ================================================ # 3. Store API key in ~/.bashrc # ================================================ if [ -n "$API_KEY" ] && [ -n "$API_KEY_VAR" ]; then echo "[4/10] Storing API key in $BASHRC..." sed -i "/export ${API_KEY_VAR}=/d" "$BASHRC" 2>/dev/null || true echo "export ${API_KEY_VAR}=\"${API_KEY}\"" >> "$BASHRC" export "${API_KEY_VAR}=${API_KEY}" echo " Done." else echo "[4/10] No API key to store -- skipping." fi echo "" # ================================================ # 4. Configure voxd config.yaml # ================================================ echo "[5/10] Configuring voxd AIPP settings..." AIPP_PROMPT="Rewrite the following spoken transcription so that it is clean, concise, and professional. Fix all spelling mistakes and grammar errors. Improve clarity and remove filler words such as um, uh, and like. Do not add any commentary or extra text. Output only the corrected text." if [ -f "$VOXD_CONFIG" ] && [ -n "$PROVIDER" ]; then python3 - << PYEOF import yaml config_path = "$VOXD_CONFIG" provider = "$PROVIDER" model = "$MODEL" prompt = "$AIPP_PROMPT" with open(config_path, 'r') as f: config = yaml.safe_load(f) config['aipp_enabled'] = True config['aipp_provider'] = provider config['aipp_active_prompt'] = 'default' if 'aipp_prompts' not in config: config['aipp_prompts'] = {} config['aipp_prompts']['default'] = prompt if 'aipp_selected_models' not in config: config['aipp_selected_models'] = {} config['aipp_selected_models'][provider] = model with open(config_path, 'w') as f: yaml.dump(config, f, default_flow_style=False, allow_unicode=True) print(f" AIPP enabled: provider={provider}, model={model}") PYEOF elif [ -z "$PROVIDER" ]; then echo " No provider selected -- leaving AIPP disabled." else echo " WARNING: $VOXD_CONFIG not found. Run 'voxd --setup' first." fi echo "" # ================================================ # 5. Kill everything currently running # ================================================ echo "[6/10] Stopping any running instances..." if systemctl --user is-active --quiet voxd-ptt.service 2>/dev/null; then systemctl --user stop voxd-ptt.service 2>/dev/null || true echo " Stopped voxd-ptt systemd service." fi if pgrep -f "python.*voxd-ptt\.py" > /dev/null 2>&1; then pkill -f "python.*voxd-ptt\.py" 2>/dev/null || true echo " Killed stray voxd-ptt.py process." fi VOXD_PID=$(pgrep -x voxd 2>/dev/null || true) if [ -n "$VOXD_PID" ]; then kill "$VOXD_PID" 2>/dev/null || true echo " Killed voxd (pid $VOXD_PID)." fi sleep 1 echo " Done." echo "" # ================================================ # 6. Remove conflicting keyboard shortcuts # ================================================ echo "[7/10] Checking for conflicting keyboard shortcuts..." FOUND_CONFLICT=false CUSTOM_KEYS=$(dconf list /org/cinnamon/desktop/keybindings/custom-keybindings/ 2>/dev/null || true) for entry in $CUSTOM_KEYS; do CMD=$(dconf read "/org/cinnamon/desktop/keybindings/custom-keybindings/${entry}command" 2>/dev/null || true) if echo "$CMD" | grep -q "trigger-record"; then echo " Found conflicting shortcut at $entry -- removing..." dconf reset -f "/org/cinnamon/desktop/keybindings/custom-keybindings/${entry}" 2>/dev/null || true FOUND_CONFLICT=true fi done [ "$FOUND_CONFLICT" = false ] && echo " No conflicting shortcuts found." echo " Done." echo "" # ================================================ # 7. Install pynput # ================================================ echo "[8/10] Installing pynput..." pip install pynput --break-system-packages --quiet 2>/dev/null || true echo " Done." echo "" # ================================================ # 8. Create voxd-ptt.py if it does not exist # ================================================ echo "[9/10] Checking for voxd-ptt.py..." if [ -f "$PTT_DEST" ]; then echo " $PTT_DEST already exists -- skipping creation." echo " (Delete it and re-run to recreate from scratch.)" else echo " Creating $PTT_DEST..." cat > "$PTT_DEST" << 'PYEOF' #!/usr/bin/env python3 """ voxd-ptt.py -- Push-to-talk wrapper for voxd on Linux (X11) Hold Left Ctrl + Windows key -> voxd starts recording Release -> voxd stops and transcribes Managed by systemd. To restart after editing: systemctl --user restart voxd-ptt.service """ import subprocess import sys import time from pynput import keyboard # --------------------------------------------- # CONFIGURATION -- edit this section # --------------------------------------------- PUSH_TO_TALK_KEY = keyboard.Key.ctrl_l MODIFIER_KEY = keyboard.Key.cmd # Windows/Super key # voxd uses --trigger-record to toggle recording on and off. # We call it on key-down to START and again on key-up to STOP. VOXD_TRIGGER_CMD = ["voxd", "--trigger-record"] # Minimum hold time in seconds to prevent accidental taps MIN_HOLD_SECONDS = 0.15 # --------------------------------------------- # END CONFIGURATION # --------------------------------------------- _recording = False _key_down_time = None _modifier_held = False def trigger_voxd(): try: subprocess.Popen( VOXD_TRIGGER_CMD, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except FileNotFoundError: print("[voxd-ptt] ERROR: 'voxd' not found. Is voxd installed?") sys.exit(1) def on_press(key): global _recording, _key_down_time, _modifier_held if MODIFIER_KEY and key == MODIFIER_KEY: _modifier_held = True return if key == PUSH_TO_TALK_KEY: if MODIFIER_KEY and not _modifier_held: return if not _recording: _key_down_time = time.monotonic() _recording = True print("[voxd-ptt] Recording started...") trigger_voxd() def on_release(key): global _recording, _key_down_time, _modifier_held if MODIFIER_KEY and key == MODIFIER_KEY: _modifier_held = False if _recording: _stop_recording() return if key == PUSH_TO_TALK_KEY and _recording: if _key_down_time is not None: held = time.monotonic() - _key_down_time if held < MIN_HOLD_SECONDS: print(f"[voxd-ptt] Held {held:.2f}s (min {MIN_HOLD_SECONDS}s) -- too short, cancelling") _recording = False _key_down_time = None trigger_voxd() return _stop_recording() def _stop_recording(): global _recording, _key_down_time _recording = False held = (time.monotonic() - _key_down_time) if _key_down_time else 0 _key_down_time = None print(f"[voxd-ptt] Stopped (held {held:.1f}s). Transcribing...") trigger_voxd() def get_voxd_pid(): try: result = subprocess.run(["pgrep", "-x", "voxd"], capture_output=True, text=True) if result.returncode == 0: return int(result.stdout.strip().split()[0]) except (ValueError, IndexError): pass return None def kill_voxd(): pid = get_voxd_pid() if pid: print(f"[voxd-ptt] Killing voxd (pid {pid})...") subprocess.run(["kill", str(pid)], capture_output=True) time.sleep(1) if get_voxd_pid(): subprocess.run(["kill", "-9", str(pid)], capture_output=True) time.sleep(0.5) else: print("[voxd-ptt] voxd was not running.") def start_voxd(): print("[voxd-ptt] Starting voxd --tray...") subprocess.Popen(["voxd", "--tray"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(2) print("[voxd-ptt] voxd started.") def main(): print("=" * 52) print(" voxd Push-to-Talk") print("=" * 52) key_name = getattr(PUSH_TO_TALK_KEY, 'name', str(PUSH_TO_TALK_KEY)) if MODIFIER_KEY: mod_name = getattr(MODIFIER_KEY, 'name', str(MODIFIER_KEY)) print(f" Hold: {mod_name} + {key_name}") else: print(f" Hold: {key_name}") print(f" Min hold: {MIN_HOLD_SECONDS}s") print("=" * 52) print() try: subprocess.run(["voxd", "-h"], capture_output=True) except FileNotFoundError: print("ERROR: 'voxd' not found.") sys.exit(1) kill_voxd() start_voxd() print("[voxd-ptt] Ready. Hold the key combo to dictate.\n") with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: try: listener.join() except KeyboardInterrupt: print("\n[voxd-ptt] Exiting -- leaving voxd running.") if _recording: trigger_voxd() if __name__ == "__main__": main() PYEOF chmod +x "$PTT_DEST" # Substitute the chosen hotkey values into the config section sed -i "s|PUSH_TO_TALK_KEY = keyboard.Key.ctrl_l|PUSH_TO_TALK_KEY = $PTT_KEY|" "$PTT_DEST" sed -i "s|MODIFIER_KEY = keyboard.Key.cmd # Windows/Super key|MODIFIER_KEY = $PTT_MODIFIER|" "$PTT_DEST" echo " Created at $PTT_DEST" echo " Hotkey configured: $PTT_KEY + $PTT_MODIFIER" fi echo "" # ================================================ # 9. Set up and start systemd service # ================================================ echo "[10/10] Setting up and starting systemd service..." mkdir -p "$HOME/.config/systemd/user" ENV_LINES="Environment=DISPLAY=:0 Environment=XAUTHORITY=$HOME/.Xauthority" if [ -n "$API_KEY_VAR" ] && [ -n "$API_KEY" ]; then ENV_LINES="$ENV_LINES Environment=${API_KEY_VAR}=${API_KEY}" else [ -n "$OPENAI_API_KEY" ] && ENV_LINES="$ENV_LINES Environment=OPENAI_API_KEY=$OPENAI_API_KEY" [ -n "$ANTHROPIC_API_KEY" ] && ENV_LINES="$ENV_LINES Environment=ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" fi cat > "$SERVICE_FILE" << EOF [Unit] Description=voxd Push-to-Talk After=graphical-session.target PartOf=graphical-session.target [Service] ExecStart=/usr/bin/python3 $PTT_DEST Restart=on-failure RestartSec=3 $ENV_LINES [Install] WantedBy=graphical-session.target EOF systemctl --user daemon-reload systemctl --user enable voxd-ptt.service 2>/dev/null || true nohup systemctl --user start voxd-ptt.service > /dev/null 2>&1 & sleep 5 if systemctl --user is-active --quiet voxd-ptt.service 2>/dev/null; then echo " Service is running." else echo " WARNING: Service may not have started." echo " Check: journalctl --user -u voxd-ptt.service -n 20" fi echo "" # ================================================ # DONE -- print quick reference # ================================================ echo "================================================" echo " Setup complete!" echo "================================================" echo "" echo " PTT key: $PTT_KEY + $PTT_MODIFIER" echo " Hold to record, release to transcribe." if [ -n "$PROVIDER" ]; then echo " AI cleanup: $PROVIDER ($MODEL)" fi echo "" echo " CHANGING THE HOTKEY:" echo " Edit ~/voxd-ptt.py -- change these lines:" echo " PUSH_TO_TALK_KEY = keyboard.Key.ctrl_l" echo " MODIFIER_KEY = keyboard.Key.cmd" echo " Options:" echo " Right Ctrl alone: ctrl_r + MODIFIER_KEY = None" echo " Scroll Lock alone: scroll_lock + MODIFIER_KEY = None" echo " Pause/Break alone: pause + MODIFIER_KEY = None" echo " Then: systemctl --user restart voxd-ptt.service" echo "" echo " KEY COMMANDS:" echo " systemctl --user status voxd-ptt.service check status" echo " systemctl --user restart voxd-ptt.service restart after edits" echo " journalctl --user -u voxd-ptt.service -f live logs" echo " voxd --gui voxd settings" echo "" echo " TROUBLESHOOTING:" echo " Behaves like toggle? A keyboard shortcut in System Settings" echo " is also calling voxd --trigger-record." echo " System Settings -> Keyboard -> Shortcuts -> Custom Shortcuts" echo " Delete any entry with that command, then restart the service." echo "" echo " See README.md for full documentation." echo "" # ================================================ # Generate README.md next to this script # ================================================ README_DEST="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/README.md" cat > "$README_DEST" << 'READMEEOF' # voxd Push-to-Talk for Linux Wispr Flow-style push-to-talk dictation on Linux using [voxd](https://github.com/jakovius/voxd) as the speech engine. **Hold Left Ctrl + Windows key** to record. Release to transcribe. Text is typed into whatever app has focus -- browser, LibreOffice, terminal, Slack, anything. --- ## How it works Your voice is transcribed **locally on your machine** using OpenAI Whisper (built into voxd -- no internet required for this part). The resulting text is then optionally sent to a cloud AI to clean up spelling, grammar, and filler words before being typed into your app. This means your **voice never leaves your machine**. Only the transcribed text is sent to the cloud -- the same as pasting a sentence into a website. --- ## Cost & privacy ### What does it cost? Prices current as of March 2026. Costs are per token -- roughly per word. Both cloud options are extremely cheap for typical dictation use. **Cost per single dictation (~50 words, ~3 sentences):** | Option | Model | Per use | 50/day/mo | 200/day/mo | |--------|-------|---------|-----------|------------| | OpenAI | gpt-4o-mini-2024-07-18 | ~$0.000058 | ~$0.09 | ~$0.35 | | Anthropic | claude-haiku-4-5 | ~$0.000450 | ~$0.68 | ~$2.70 | | Local model | your Ollama model | free | free | free | | No cleanup | -- | free | free | free | **If you dictate longer passages (~150 words / ~1 paragraph):** | Option | 50/day/mo | 200/day/mo | |--------|-----------|------------| | OpenAI | ~$0.23 | ~$0.93 | | Anthropic | ~$1.85 | ~$7.38 | Actual costs vary with dictation length. Check current prices: - OpenAI: https://openai.com/api/pricing/ - Anthropic: https://www.anthropic.com/pricing ### What gets sent where? Your **voice never leaves your machine**. voxd transcribes speech locally using Whisper. Only the resulting text is sent to the cloud AI for cleanup. | Option | What is sent | Used for training? | Retention | |--------|-------------|-------------------|-----------| | OpenAI API | Transcribed text only | **No** | 30 days (abuse monitoring) | | Anthropic API | Transcribed text only | **No** | 30 days then deleted | | Local model | Nothing | N/A | N/A | | No cleanup | Nothing | N/A | N/A | **Important:** The 2025 consumer data-training policy changes at Anthropic (5-year retention, opt-out required) apply only to Claude.ai consumer accounts. The API is explicitly excluded -- API data is not used for training and is deleted within 30 days. Verify the policies yourself: - OpenAI API data policy: https://developers.openai.com/api/docs/guides/your-data - OpenAI enterprise privacy: https://openai.com/enterprise-privacy/ - Anthropic API data policy: https://www.anthropic.com/news/updates-to-our-consumer-terms - Anthropic privacy center: https://privacy.anthropic.com/en/articles/10023548-how-long-do-you-store-personal-data **Important distinction:** OpenAI and Anthropic API usage has much stronger privacy protections than their consumer products (ChatGPT, Claude.ai). API data is not used for model training and is not retained beyond 30 days. This is separate from consumer account policies which changed in 2025. For maximum privacy, choose the local model option (Ollama or llama.cpp). It runs entirely on your machine with no outbound connections at all. Trade-off: local models are slower and less accurate than cloud options. --- ## Requirements - Linux on X11 (see Wayland section below) - Python 3 (pre-installed on most distros) - OpenAI or Anthropic API key, OR Ollama installed for local models voxd is downloaded and installed automatically if not present. --- ## Installation You only need one file: `setup-voxd-ptt.sh` ```bash bash setup-voxd-ptt.sh ``` The script explains what it will do before starting. It handles everything: detects X11/Wayland, installs voxd, configures AI cleanup, removes conflicting shortcuts, creates `~/voxd-ptt.py`, and sets up systemd. Safe to re-run. `~/voxd-ptt.py` is only created on first run -- your edits are preserved on subsequent runs. To recreate it from scratch: ```bash rm ~/voxd-ptt.py && bash setup-voxd-ptt.sh ``` --- ## X11 vs Wayland **Linux Mint Cinnamon** defaults to X11 -- you are almost certainly fine. **Ubuntu GNOME** defaults to Wayland -- you need to switch to X11. Check which you are on: ```bash echo $XDG_SESSION_TYPE ``` ### Why X11 is required X11 (older): apps can see each other's input events. Global hotkeys work. Wayland (newer, more secure): apps are isolated. They cannot monitor each other's input. This is intentional -- it improves security -- but it breaks global hotkey detection. `pynput` cannot work on Wayland and has no reliable workaround. ### Switching to X11 **If you are on Ubuntu GNOME (uses GDM)**, the setup script can do this automatically. It patches `/etc/gdm3/custom.conf` to set `WaylandEnable=false` and offers to log you out. After logging back in, select your normal session (not the Wayland one) and re-run setup. **If the script can't do it automatically**, log out manually: | Desktop | Steps | |---------|-------| | Ubuntu GNOME | Log out → gear icon at login → select GNOME on Xorg | | Linux Mint Cinnamon | Log out → session icon → select Cinnamon (not Wayland) | | KDE Plasma | Log out → session selector → select Plasma (X11) | After switching: `echo $XDG_SESSION_TYPE` should say `x11`. --- ## Usage Hold **Left Ctrl + Windows key**, speak naturally, release. - Works in any app: Chrome, Firefox, LibreOffice, terminal, etc. - AI cleanup removes filler words, fixes spelling, grammar, clarity - 0.15s minimum hold prevents accidental triggers - voxd icon sits in your system tray showing recording status --- ## Changing the hotkey Edit `~/voxd-ptt.py` near the top: ```python PUSH_TO_TALK_KEY = keyboard.Key.ctrl_l MODIFIER_KEY = keyboard.Key.cmd ``` | Combo | PUSH_TO_TALK_KEY | MODIFIER_KEY | |-------|-----------------|--------------| | Left Ctrl + Windows (default) | `keyboard.Key.ctrl_l` | `keyboard.Key.cmd` | | Right Ctrl alone | `keyboard.Key.ctrl_r` | `None` | | Scroll Lock alone | `keyboard.Key.scroll_lock` | `None` | | Pause/Break alone | `keyboard.Key.pause` | `None` | After editing: ```bash systemctl --user restart voxd-ptt.service ``` --- ## Useful commands ```bash systemctl --user status voxd-ptt.service # check status systemctl --user restart voxd-ptt.service # restart after editing journalctl --user -u voxd-ptt.service -f # live logs voxd --gui # voxd settings UI ``` --- ## Changing your API key Re-run setup, or manually: ```bash nano ~/.bashrc # update export line nano ~/.config/systemd/user/voxd-ptt.service # update Environment= line systemctl --user daemon-reload systemctl --user restart voxd-ptt.service ``` --- ## Troubleshooting **Behaves like toggle instead of push-to-talk** A keyboard shortcut in System Settings is also calling `voxd --trigger-record`. System Settings → Keyboard → Shortcuts → Custom Shortcuts → delete it. Then: `systemctl --user restart voxd-ptt.service` **No text appears after speaking** ```bash pgrep -x voxd # is voxd running? systemctl --user status voxd-ptt.service # is service running? journalctl --user -u voxd-ptt.service -f # live logs python3 ~/voxd-ptt.py # run directly for errors ``` **AI cleanup not working (raw transcription)** ```bash cat ~/.config/voxd/config.yaml | grep aipp # aipp_enabled should be true cat ~/.config/systemd/user/voxd-ptt.service # API key should be present ``` **On Wayland** Run the setup script -- it will detect Wayland, offer to patch GDM automatically, and guide you through switching to X11. **pynput not found** ```bash pip install pynput --break-system-packages systemctl --user restart voxd-ptt.service ``` READMEEOF echo " README.md written to $README_DEST" echo ""