From e1f0a0b01445cb50578d731e3bc55402f84caf35 Mon Sep 17 00:00:00 2001 From: Outis Date: Tue, 24 Mar 2026 20:16:03 -0400 Subject: [PATCH] Add files via upload --- setup-voxd-ptt.sh | 1023 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1023 insertions(+) create mode 100644 setup-voxd-ptt.sh diff --git a/setup-voxd-ptt.sh b/setup-voxd-ptt.sh new file mode 100644 index 0000000..cf92e96 --- /dev/null +++ b/setup-voxd-ptt.sh @@ -0,0 +1,1023 @@ +#!/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 ""