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
669 lines
25 KiB
Bash
Executable File
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 "$@"
|