Files
Claude cc847cff67 Wrap script in main() to prevent line-by-line execution bug
When run via `bash <(curl ...)`, bash reads the script incrementally
from a process substitution fd. If a `read < /dev/tty` call disrupts
the fd state, bash can lose its place and the remaining script lines
get fed to the interactive shell prompt as individual commands.

Wrapping everything in main() forces bash to parse the entire script
into memory before executing any of it — the standard fix for
curl-piped scripts.

Also changed README one-liner to download-then-execute pattern
(curl -o /tmp/... && bash /tmp/...) as a belt-and-suspenders approach.

https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm
2026-03-25 13:37:29 +00:00

669 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
# setup-openwhispr.sh — One-command installer for OpenWhispr on Linux
# https://github.com/outis1one/openwhispr-easy-setup
# License: MIT
# ── Wrap everything in main() so bash reads the entire script before executing.
# This prevents the "line-by-line interpreted as commands" bug when run via:
# bash <(curl -fsSL ...) or curl ... | bash
# Without this wrapper, bash may lose track of the script source after a read
# from /dev/tty and start feeding remaining script lines to the shell prompt.
main() {
# Do NOT use set -e — we handle errors explicitly
readonly SCRIPT_VERSION="1.0.0"
readonly BASHRC="$HOME/.bashrc"
readonly MARKER="# Added by openwhispr-easy-setup"
readonly GITHUB_API="https://api.github.com/repos/OpenWhispr/openwhispr/releases/latest"
readonly TMP_DIR="/tmp/openwhispr-install"
# ── Colours & formatting ─────────────────────────────────────────────────────
if [[ -t 1 ]]; then
BOLD='\033[1m'
DIM='\033[2m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
RESET='\033[0m'
else
BOLD='' DIM='' GREEN='' YELLOW='' RED='' CYAN='' RESET=''
fi
# ── Helper functions ──────────────────────────────────────────────────────────
# shellcheck disable=SC2059
info() { printf "${GREEN}[✓]${RESET} %s\n" "$*"; }
# shellcheck disable=SC2059
warn() { printf "${YELLOW}[!]${RESET} %s\n" "$*"; }
# shellcheck disable=SC2059
err() { printf "${RED}[✗]${RESET} %s\n" "$*" >&2; }
# shellcheck disable=SC2059
step() { printf "\n${BOLD}${CYAN}[%s]${RESET} ${BOLD}%s${RESET}\n" "$1" "$2"; }
# shellcheck disable=SC2059
divider() { printf "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"; }
prompt_default() {
# Usage: prompt_default "prompt text" "default" VARNAME
local prompt="$1" default="$2" varname="$3" reply
printf "%s [default: %s]: " "$prompt" "$default" > /dev/tty
read -r reply < /dev/tty
reply="${reply:-$default}"
printf -v "$varname" '%s' "$reply"
}
prompt_hidden() {
# Usage: prompt_hidden "prompt text" VARNAME
local prompt="$1" varname="$2" reply
printf "%s: " "$prompt" > /dev/tty
read -rs reply < /dev/tty
printf "\n" > /dev/tty
printf -v "$varname" '%s' "$reply"
}
store_api_key() {
# Usage: store_api_key VARNAME value
local keyname="$1" keyval="$2"
if [[ -z "$keyval" ]]; then
warn "Empty value for $keyname — skipping."
return 1
fi
# Check if already present in bashrc
if grep -q "^export ${keyname}=" "$BASHRC" 2>/dev/null; then
info "$keyname already set in $BASHRC — not overwriting."
else
printf '\n%s\nexport %s="%s"\n' "$MARKER" "$keyname" "$keyval" >> "$BASHRC"
info "$keyname stored in $BASHRC"
fi
# Also export for current session
export "$keyname=$keyval"
}
command_exists() { command -v "$1" >/dev/null 2>&1; }
# ── Variables set during the run ──────────────────────────────────────────────
DISTRO="" # debian, ubuntu, mint, fedora, arch, opensuse
DISTRO_FAMILY="" # deb, rpm-dnf, rpm-zypper, arch
ARCH="" # amd64, arm64
SESSION_TYPE="" # x11, wayland
DESKTOP="" # GNOME, KDE, etc.
STT_CHOICE=""
CLEANUP_CHOICE=""
STT_LABEL=""
CLEANUP_LABEL=""
INSTALLED_VERSION=""
LATEST_VERSION=""
# ══════════════════════════════════════════════════════════════════════════════
# [1/8] Intro
# ══════════════════════════════════════════════════════════════════════════════
step "1/8" "Welcome to OpenWhispr Easy Setup v${SCRIPT_VERSION}"
divider
cat <<'INTRO'
OpenWhispr is an Electron-based voice dictation app for Linux.
Press a hotkey, speak, and your words appear wherever the cursor is.
Your voice stays on your machine. Always.
The only question is whether you want AI to clean up
the text after transcription.
Speech-to-text (STT) is local Whisper/Parakeet by default.
Audio never leaves your machine unless YOU explicitly choose a cloud option.
This script will:
1. Detect your distro and architecture
2. Install OpenWhispr (latest release)
3. Install paste/typing dependencies
4. Configure STT provider
5. Configure AI text cleanup
6. Launch OpenWhispr for first-run setup
INTRO
divider
printf "\nPress Enter to continue or Ctrl-C to cancel... " > /dev/tty
read -r < /dev/tty
# ══════════════════════════════════════════════════════════════════════════════
# [2/8] Detect distro + arch
# ══════════════════════════════════════════════════════════════════════════════
step "2/8" "Detecting distribution and architecture"
detect_distro() {
if [[ -f /etc/os-release ]]; then
# shellcheck source=/dev/null
. /etc/os-release
case "${ID,,}" in
ubuntu) DISTRO="ubuntu"; DISTRO_FAMILY="deb" ;;
debian) DISTRO="debian"; DISTRO_FAMILY="deb" ;;
linuxmint) DISTRO="mint"; DISTRO_FAMILY="deb" ;;
fedora) DISTRO="fedora"; DISTRO_FAMILY="rpm-dnf" ;;
opensuse*|sles) DISTRO="opensuse"; DISTRO_FAMILY="rpm-zypper" ;;
arch|manjaro|endeavouros) DISTRO="arch"; DISTRO_FAMILY="arch" ;;
*)
err "Unsupported distribution: ${ID} (${PRETTY_NAME:-unknown})"
err "Supported: Ubuntu, Debian, Mint, Fedora, openSUSE, Arch"
exit 1
;;
esac
else
err "Cannot detect distribution (/etc/os-release not found)."
exit 1
fi
}
detect_arch() {
local machine
machine="$(uname -m)"
case "$machine" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*)
err "Unsupported architecture: $machine"
err "Supported: x86_64 (amd64), aarch64 (arm64)"
exit 1
;;
esac
}
detect_distro
detect_arch
info "Distro: ${DISTRO} (${DISTRO_FAMILY}) | Arch: ${ARCH}"
# ── Check base dependencies ───────────────────────────────────────────────────
if ! command_exists curl; then
err "curl is required but not installed."
err "Install it with your package manager and re-run this script."
exit 1
fi
if ! command_exists jq; then
warn "jq is required but not installed."
printf "Install jq now? [Y/n]: " > /dev/tty
read -r jq_reply < /dev/tty
jq_reply="${jq_reply:-Y}"
if [[ "${jq_reply,,}" == "y" ]]; then
case "$DISTRO_FAMILY" in
deb) sudo apt update && sudo apt install -y jq ;;
rpm-dnf) sudo dnf install -y jq ;;
rpm-zypper) sudo zypper install -y jq ;;
arch) sudo pacman -S --noconfirm jq ;;
esac
if ! command_exists jq; then
err "Failed to install jq. Please install it manually and re-run."
exit 1
fi
info "jq installed."
else
err "jq is required. Please install it and re-run."
exit 1
fi
fi
# ══════════════════════════════════════════════════════════════════════════════
# [3/8] Install OpenWhispr
# ══════════════════════════════════════════════════════════════════════════════
step "3/8" "Installing OpenWhispr"
# Check currently installed version
get_installed_version() {
if command_exists openwhispr; then
openwhispr --version 2>/dev/null || true
elif command_exists open-whispr; then
open-whispr --version 2>/dev/null || true
fi
}
INSTALLED_VERSION="$(get_installed_version)"
# Fetch latest release info
info "Fetching latest release info..."
RELEASE_JSON="$(curl -sf "$GITHUB_API")" || {
err "Failed to fetch release info from GitHub."
err "Check your internet connection and try again."
exit 1
}
LATEST_VERSION="$(printf '%s' "$RELEASE_JSON" | jq -r '.tag_name // empty')"
if [[ -z "$LATEST_VERSION" ]]; then
err "Could not determine latest version from GitHub."
exit 1
fi
info "Latest version: ${LATEST_VERSION}"
# Strip leading 'v' for comparison
LATEST_CLEAN="${LATEST_VERSION#v}"
INSTALLED_CLEAN="${INSTALLED_VERSION#v}"
if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_CLEAN" == "$LATEST_CLEAN" ]]; then
info "OpenWhispr ${LATEST_VERSION} is already installed — skipping."
else
if [[ -n "$INSTALLED_VERSION" ]]; then
info "Upgrading from ${INSTALLED_VERSION} to ${LATEST_VERSION}..."
fi
# Determine asset pattern
case "$DISTRO_FAMILY" in
deb) ASSET_EXT=".deb" ;;
rpm-dnf|rpm-zypper) ASSET_EXT=".rpm" ;;
arch) ASSET_EXT=".tar.gz" ;;
esac
# Map arch names (assets may use different conventions)
case "$ARCH" in
amd64) ARCH_PATTERNS=("amd64" "x86_64" "x64") ;;
arm64) ARCH_PATTERNS=("arm64" "aarch64") ;;
esac
# Find matching asset URL
ASSET_URL=""
for arch_pat in "${ARCH_PATTERNS[@]}"; do
ASSET_URL="$(printf '%s' "$RELEASE_JSON" | jq -r \
--arg ext "$ASSET_EXT" --arg arch "$arch_pat" \
'.assets[] | select(.name | (endswith($ext) and contains($arch))) | .browser_download_url' \
| head -1)"
[[ -n "$ASSET_URL" ]] && break
done
if [[ -z "$ASSET_URL" ]]; then
err "No ${ASSET_EXT} asset found for ${ARCH} in release ${LATEST_VERSION}."
err "Check: https://github.com/OpenWhispr/openwhispr/releases/latest"
exit 1
fi
ASSET_NAME="$(basename "$ASSET_URL")"
info "Downloading ${ASSET_NAME}..."
mkdir -p "$TMP_DIR"
curl --progress-bar -fL -o "${TMP_DIR}/${ASSET_NAME}" "$ASSET_URL" || {
err "Download failed."
exit 1
}
info "Installing..."
case "$DISTRO_FAMILY" in
deb)
sudo apt install -y "${TMP_DIR}/${ASSET_NAME}" || {
err "Installation failed. Try: sudo dpkg -i ${TMP_DIR}/${ASSET_NAME}"
exit 1
}
;;
rpm-dnf)
sudo dnf install -y "${TMP_DIR}/${ASSET_NAME}" || {
err "Installation failed."
exit 1
}
;;
rpm-zypper)
sudo zypper install -y "${TMP_DIR}/${ASSET_NAME}" || {
err "Installation failed."
exit 1
}
;;
arch)
sudo mkdir -p /opt/openwhispr
sudo tar -xzf "${TMP_DIR}/${ASSET_NAME}" -C /opt/openwhispr || {
err "Extraction failed."
exit 1
}
# Find the binary and symlink
local_bin="$(find /opt/openwhispr -maxdepth 2 -name 'openwhispr' -o -name 'open-whispr' 2>/dev/null | head -1)"
if [[ -n "$local_bin" ]]; then
sudo ln -sf "$local_bin" /usr/local/bin/openwhispr
fi
;;
esac
# Clean up
rm -rf "$TMP_DIR"
info "OpenWhispr ${LATEST_VERSION} installed successfully."
fi
# ══════════════════════════════════════════════════════════════════════════════
# [4/8] Install paste dependencies
# ══════════════════════════════════════════════════════════════════════════════
step "4/8" "Installing paste/typing dependencies"
SESSION_TYPE="${XDG_SESSION_TYPE:-}"
DESKTOP="${XDG_CURRENT_DESKTOP:-}"
if [[ -z "$SESSION_TYPE" ]]; then
# Try to detect
if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then
SESSION_TYPE="wayland"
elif [[ -n "${DISPLAY:-}" ]]; then
SESSION_TYPE="x11"
else
warn "Cannot detect session type (X11/Wayland). Skipping paste deps."
fi
fi
info "Session: ${SESSION_TYPE:-unknown} | Desktop: ${DESKTOP:-unknown}"
install_paste_dep() {
local pkg="$1"
if command_exists "$pkg"; then
info "$pkg is already installed."
return 0
fi
info "Installing $pkg..."
case "$DISTRO_FAMILY" in
deb) sudo apt install -y "$pkg" ;;
rpm-dnf) sudo dnf install -y "$pkg" ;;
rpm-zypper) sudo zypper install -y "$pkg" ;;
arch) sudo pacman -S --noconfirm "$pkg" ;;
esac
}
case "$SESSION_TYPE" in
x11)
install_paste_dep "xdotool"
;;
wayland)
if [[ "$DESKTOP" == *"GNOME"* ]]; then
info "GNOME on Wayland — OpenWhispr uses D-Bus natively. No extra deps."
elif [[ "$DESKTOP" == *"KDE"* ]]; then
install_paste_dep "kdotool"
else
install_paste_dep "wtype"
fi
;;
*)
warn "Skipping paste dependency install (unknown session type)."
;;
esac
# ══════════════════════════════════════════════════════════════════════════════
# [5/8] STT provider
# ══════════════════════════════════════════════════════════════════════════════
step "5/8" "Speech-to-text (STT) provider"
cat <<'STT_MENU'
How should OpenWhispr transcribe your voice?
Option Method Streaming Cost Audio sent to
─────────────────────────────────────────────────────────────────────────────
1) Local Whisper/Parakeet (default) no free nobody, ever
2) Groq whisper-large-v3-turbo no $0.04/hr* Groq servers
3) OpenAI Realtime API YES $0.06/min OpenAI servers
*Groq free tier: ~2,000 audio seconds/day (~600 dictations/day free).
NOT used for training. 30-day retention max.
Verify: https://console.groq.com/docs/your-data
Streaming (option 3) = text appears word-by-word as you speak.
Options 1 and 2 transcribe after you stop speaking.
All cloud options send audio to a server (like Siri/Google Assistant)
but unlike Siri/Google: NOT tied to your identity, NOT used for
training, deleted within 30 days.
Best free combo: option 1 (local) + Groq cleanup [step 6]
Best free cloud combo: option 2 (Groq STT) + Groq cleanup [same key]
Best streaming: option 3 (OpenAI Realtime) + any cleanup
STT_MENU
prompt_default "Enter 1-3" "1" STT_CHOICE
case "$STT_CHOICE" in
1)
STT_LABEL="Local Whisper/Parakeet (offline)"
info "STT: Local — audio never leaves your machine."
;;
2)
STT_LABEL="Groq whisper-large-v3-turbo (cloud)"
info "STT: Groq cloud transcription."
if [[ -n "${GROQ_API_KEY:-}" ]]; then
info "GROQ_API_KEY already set — reusing."
else
prompt_hidden "Enter your Groq API key (from https://console.groq.com)" GROQ_KEY_INPUT
store_api_key "GROQ_API_KEY" "$GROQ_KEY_INPUT"
fi
;;
3)
STT_LABEL="OpenAI Realtime API (cloud, streaming)"
info "STT: OpenAI Realtime (streaming)."
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
info "OPENAI_API_KEY already set — reusing."
else
prompt_hidden "Enter your OpenAI API key (from https://platform.openai.com/api-keys)" OPENAI_KEY_INPUT
store_api_key "OPENAI_API_KEY" "$OPENAI_KEY_INPUT"
fi
;;
*)
warn "Invalid choice '$STT_CHOICE' — defaulting to local."
STT_CHOICE="1"
STT_LABEL="Local Whisper/Parakeet (offline)"
;;
esac
# ══════════════════════════════════════════════════════════════════════════════
# [6/8] AI cleanup provider
# ══════════════════════════════════════════════════════════════════════════════
step "6/8" "AI text cleanup provider"
cat <<'CLEANUP_MENU'
After transcription, AI can fix grammar, spelling, and filler words.
Only transcribed TEXT is sent — your voice stays local regardless.
None of these providers use your text for training.
Provider Model Per use 50/day/mo 200/day/mo
──────────────────────────────────────────────────────────────────────────
1) OpenAI gpt-4o-mini-2024-07-18 $0.000058 $0.09 $0.35
2) Anthropic claude-haiku-4-5 $0.000450 $0.68 $2.70
3) Groq llama-3.3-70b-versatile free* free* free*
4) Local Ollama (fully offline) free free free
5) Skip raw transcription only free free free
*Groq free: 1,000 req/day, 100K tokens/day. NOT used for training.
Verify: https://console.groq.com/docs/your-data
Pricing based on ~50 words per dictation (~125 input + 65 output tokens).
Verify current pricing:
OpenAI: https://openai.com/api/pricing
Anthropic: https://www.anthropic.com/pricing
Groq: https://groq.com/pricing
CLEANUP_MENU
prompt_default "Enter 1-5" "1" CLEANUP_CHOICE
case "$CLEANUP_CHOICE" in
1)
CLEANUP_LABEL="OpenAI gpt-4o-mini"
info "Cleanup: OpenAI gpt-4o-mini."
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
info "OPENAI_API_KEY already set — reusing."
else
prompt_hidden "Enter your OpenAI API key (from https://platform.openai.com/api-keys)" OPENAI_KEY_INPUT
store_api_key "OPENAI_API_KEY" "$OPENAI_KEY_INPUT"
fi
;;
2)
CLEANUP_LABEL="Anthropic claude-haiku-4-5"
info "Cleanup: Anthropic claude-haiku-4-5."
if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
info "ANTHROPIC_API_KEY already set — reusing."
else
prompt_hidden "Enter your Anthropic API key (from https://console.anthropic.com)" ANTHROPIC_KEY_INPUT
store_api_key "ANTHROPIC_API_KEY" "$ANTHROPIC_KEY_INPUT"
fi
;;
3)
CLEANUP_LABEL="Groq llama-3.3-70b (free)"
info "Cleanup: Groq llama-3.3-70b."
if [[ -n "${GROQ_API_KEY:-}" ]]; then
info "GROQ_API_KEY already set — reusing."
else
prompt_hidden "Enter your Groq API key (from https://console.groq.com)" GROQ_KEY_INPUT
store_api_key "GROQ_API_KEY" "$GROQ_KEY_INPUT"
fi
;;
4)
CLEANUP_LABEL="Local Ollama (offline)"
info "Cleanup: Local Ollama."
if command_exists ollama; then
info "Ollama is already installed."
else
warn "Ollama is not installed."
cat <<'OLLAMA_INFO'
To install Ollama:
curl -fsSL https://ollama.ai/install.sh | sh
Then pull a model:
ollama pull llama3.1
OLLAMA_INFO
printf "Install Ollama now? [Y/n]: " > /dev/tty
read -r ollama_reply < /dev/tty
ollama_reply="${ollama_reply:-Y}"
if [[ "${ollama_reply,,}" == "y" ]]; then
curl -fsSL https://ollama.ai/install.sh | sh || {
warn "Ollama install failed. Install manually later."
}
else
warn "Skipping Ollama install — you can install it later."
fi
fi
;;
5)
CLEANUP_LABEL="None (raw transcription)"
info "Cleanup: Skipped — raw transcription only."
;;
*)
warn "Invalid choice '$CLEANUP_CHOICE' — defaulting to OpenAI."
CLEANUP_CHOICE="1"
CLEANUP_LABEL="OpenAI gpt-4o-mini"
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
info "OPENAI_API_KEY already set — reusing."
else
prompt_hidden "Enter your OpenAI API key (from https://platform.openai.com/api-keys)" OPENAI_KEY_INPUT
store_api_key "OPENAI_API_KEY" "$OPENAI_KEY_INPUT"
fi
;;
esac
# ══════════════════════════════════════════════════════════════════════════════
# [7/8] First launch
# ══════════════════════════════════════════════════════════════════════════════
step "7/8" "First launch"
cat <<'LAUNCH_INFO'
OpenWhispr will open now for first-run setup.
- It will download speech models (~75-500 MB depending on choice).
- Set your hotkey in Settings (default: backtick `).
- The app manages its own autostart.
LAUNCH_INFO
LAUNCHED=false
LAUNCH_CMD=""
# Check PATH first, then common Electron install locations
for cmd in openwhispr open-whispr; do
if command_exists "$cmd"; then
LAUNCH_CMD="$cmd"
break
fi
done
# If not in PATH, search common install directories
if [[ -z "$LAUNCH_CMD" ]]; then
for path in \
/opt/OpenWhispr/open-whispr \
/opt/open-whispr/open-whispr \
/opt/openwhispr/openwhispr \
/usr/lib/open-whispr/open-whispr \
/usr/lib/openwhispr/openwhispr \
/snap/bin/openwhispr \
/snap/bin/open-whispr; do
if [[ -x "$path" ]]; then
LAUNCH_CMD="$path"
break
fi
done
fi
# Last resort: ask dpkg where it installed the binary
if [[ -z "$LAUNCH_CMD" ]] && command_exists dpkg; then
LAUNCH_CMD="$(dpkg -L open-whispr 2>/dev/null | grep -E '/bin/|/opt/' | head -1 || true)"
if [[ -n "$LAUNCH_CMD" && ! -x "$LAUNCH_CMD" ]]; then
LAUNCH_CMD=""
fi
fi
if [[ -n "$LAUNCH_CMD" ]]; then
"$LAUNCH_CMD" &>/dev/null &
LAUNCHED=true
fi
if [[ "$LAUNCHED" == true ]]; then
info "OpenWhispr launched in the background."
else
warn "Could not auto-detect OpenWhispr binary location."
warn "Try launching manually:"
warn " open-whispr"
warn " openwhispr"
if command_exists dpkg; then
warn " Find it: dpkg -L open-whispr | grep bin"
fi
fi
# ══════════════════════════════════════════════════════════════════════════════
# [8/8] Summary
# ══════════════════════════════════════════════════════════════════════════════
step "8/8" "Done!"
printf "\n"
divider
# shellcheck disable=SC2059
printf "${BOLD} OpenWhispr is ready${RESET}\n"
divider
DISPLAY_CMD="${LAUNCH_CMD:-open-whispr}"
DISPLAY_CMD_BASE="$(basename "$DISPLAY_CMD")"
cat <<EOF
Hotkey: backtick (\`) — change in Settings → Hotkey
STT: ${STT_LABEL}
AI cleanup: ${CLEANUP_LABEL}
API keys stored in: ${BASHRC}
Apply now: source ${BASHRC}
Useful commands:
${DISPLAY_CMD_BASE} launch the app
${DISPLAY_CMD_BASE} --help show options
Settings → Hotkey change your hotkey
Settings → Models download better speech models
Settings → Processing change STT or AI provider
Troubleshoot: https://github.com/OpenWhispr/openwhispr/blob/main/TROUBLESHOOTING.md
This installer: https://github.com/outis1one/openwhispr-easy-setup
EOF
divider
printf "\n"
} # end main()
# Call main — this ensures the entire script is parsed before execution
main "$@"