* feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage Working-tree snapshot bundling several in-flight workstreams (v0.3.0): - Onboarding/demo system: DemoPresetGrid, DictationDemo, DubbingDemo components + tests, render scripts (render_demos_omnivoice.py, build_demos.sh, build_dub_demo.sh), personalities preview URLs, alembic 0002 voice-profile demo fields. - Opt-in bug reporting: ReportBugButton (prefilled GitHub-issue URL path). - Error transparency UX: errorDocsMap deeplinks + BootstrapSplash/error wiring. - Dub workspace: DubSegmentRow/Table, WaveformTimeline, dubSlice tweaks. - Issue triage: .planning/issue-clusters/ (plan-01..05 root-cause masters, GH #128-#132). - CLAUDE.md: hard rule — everything ships on v0.3.0, no version bumps. KNOWN GAP (why this is a draft): the generated demo audio assets are NOT in this tree, and backend/assets/samples/demo_voice.wav is deleted. onboarding.py guards the missing file (skips seeding the demo profile with a warning), so no crash — but first-run Launchpad will be empty and /demo_audio/ preview URLs 404 until assets are regenerated via scripts/build_demos.sh. Do not merge before regenerating + committing the demo assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dub): timing strategies — kill audio compression, add Concise + Stretch Video Replaces the current audio time-compression default (atempo squeeze to fit slot) that produced chipmunk/alien output on high-density target languages like Bengali. Two new user-selectable modes; legacy behaviour kept behind an explicit "Strict slot" choice. New `DubRequest.timing_strategy` enum (default "concise"): - "concise" Translator trims text to fit at natural rate; if it still overflows, hard-trim at slot with a fade so we never overlap the next speaker. Surface overflow_s per segment so the user can shorten the text. - "stretch_video" Audio plays at natural 1.0× rate. Backend computes a per-segment new timeline; persists a video_stretch_plan on the job. Mux step (dub_export) builds an ffmpeg trim+setpts+concat filter graph that stretches each segment's video portion to match the natural-rate dub audio. Gaps/pre-roll/tail pass through at 1.0×. Sub burn under stretch_video is skipped in one pass (cues would drift). - "strict_slot" Legacy atempo squeeze. Retained for back-compat. Director rate-bias side-effect (seg_speed *= bias) now gated on strict_slot only, so "urgent"/"slow" direction tokens keep their instruct effect in the new modes without chipmunking. Per-segment fit_status emitted in the SSE done event: {status: "fits" | "overflows" | "video_stretched", overflow_s?, stretch_ratio?} DubSegmentRow's "Sync: 100%" badge (which was lying — sync_ratio was always ~1.0 because the TTS loop pre-trimmed to slot) is replaced with a truthful "Fits / Overflows +Ns / Video 1.18×" label. Frontend: - prefsSlice.timingStrategy (persisted, store v3→v4 with safe migrate). - DubTab footer Segmented control: "Concise · Stretch Video · Strict slot". - useDubWorkflow passes timing_strategy on /dub/generate; consumes fit_status. Tests: tests/test_dub_timing_strategy.py — 13 cases covering schema defaults/validation, _build_video_stretch_filter_graph (pre-roll, gap, tail, empty-plan early return, post-subtitle chain-in), and _video_stretch_plan_for guards. 30/30 existing dub tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(waveform): surface missing source as "Source media missing" instead of code-4 black box When a project's underlying media file is gone (moved or deleted between save and reload) the <video> element fires MediaError code 4 and the companion audio fetch returns HTTP 404 — both were silently warned to the console while the user stared at an unresponsive black panel and an empty waveform. - WaveformTimeline now flips loadError when the video element rejects code 3 (decode) or 4 (src not supported), and tracks `sourceMissing` separately so the error UI can name the actual problem. - The audio decode fallback chain catches HTTP 404 specifically and treats it as source-missing instead of loading silent empty peaks — an empty waveform on a deleted source is more confusing than a clear "Re-upload the video to continue" message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tray): "Show OmniVoice" reloads when the webview is blank When the dev Vite server restarts (or the main window is created before the backend is ready), the webview load fails and the window is left with `<body></body>` plus a "Could not connect to the server" console error. Clicking "Show OmniVoice" from the tray menu just re-showed the broken window — there was no recovery path short of quit+relaunch. Now the show handler runs a tiny eval after `show()`/`set_focus()` that calls `location.reload()` only when `document.body.childElementCount === 0`. A healthy window doesn't blink (body is non-empty); a blank one self-recovers as soon as the user clicks Show. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#133): bug-report diagnostics field mapping + drop unused imports Address PR #133 review: - ReportBugButton: /system/info exposes `platform` + `device`, not `os`/`torch_device`/`gpu` — those reads silently dropped OS/GPU from every bug report. Map to the real fields (CodeRabbit). Also remove the dead `home` local in stripHome (CodeQL unused-variable). - DictationDemo: drop unused `Loader` import (CodeQL unused-import). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
383 lines
14 KiB
Bash
Executable File
383 lines
14 KiB
Bash
Executable File
#!/bin/sh
|
||
# OmniVoice Studio — universal installer.
|
||
#
|
||
# Works on macOS (ARM + Intel), Linux (Debian/Ubuntu, Fedora, Arch), and WSL.
|
||
# Run once, then `./run.sh` each time you want to use the app.
|
||
#
|
||
# Usage:
|
||
# curl -fsSL https://raw.githubusercontent.com/debpalash/OmniVoice-Studio/main/install.sh | sh
|
||
# # or locally:
|
||
# sh install.sh
|
||
# sh install.sh --verbose # show all subcommand output
|
||
# sh install.sh --python 3.12 # override Python version
|
||
set -e
|
||
|
||
# ── Output style ────────────────────────────────────────────────────────────
|
||
RULE=""
|
||
_rule_i=0
|
||
while [ "$_rule_i" -lt 56 ]; do
|
||
RULE="${RULE}─"
|
||
_rule_i=$((_rule_i + 1))
|
||
done
|
||
|
||
if [ -n "${NO_COLOR:-}" ]; then
|
||
C_TITLE="" C_DIM="" C_OK="" C_WARN="" C_ERR="" C_RST=""
|
||
elif [ -t 1 ] || [ -n "${FORCE_COLOR:-}" ]; then
|
||
_ESC="$(printf '\033')"
|
||
C_TITLE="${_ESC}[1;38;5;141m" # bold purple
|
||
C_DIM="${_ESC}[38;5;245m"
|
||
C_OK="${_ESC}[38;5;108m" # green
|
||
C_WARN="${_ESC}[38;5;136m" # yellow
|
||
C_ERR="${_ESC}[91m" # red
|
||
C_RST="${_ESC}[0m"
|
||
else
|
||
C_TITLE="" C_DIM="" C_OK="" C_WARN="" C_ERR="" C_RST=""
|
||
fi
|
||
|
||
step() { printf " ${C_DIM}%-18.18s${C_RST}${3:-$C_OK}%s${C_RST}\n" "$1" "$2"; }
|
||
note() { printf " ${C_DIM}%-18s${2:-$C_DIM}%s${C_RST}\n" "" "$1"; }
|
||
warn() { printf " ${C_WARN}⚠ %s${C_RST}\n" "$1"; }
|
||
die() { printf " ${C_ERR}✗ %s${C_RST}\n" "$1" >&2; exit 1; }
|
||
have() { command -v "$1" >/dev/null 2>&1; }
|
||
|
||
# ── Parse flags ─────────────────────────────────────────────────────────────
|
||
_VERBOSE=false
|
||
_USER_PYTHON=""
|
||
_next_is_python=false
|
||
for arg in "$@"; do
|
||
if [ "$_next_is_python" = true ]; then
|
||
_USER_PYTHON="$arg"
|
||
_next_is_python=false
|
||
continue
|
||
fi
|
||
case "$arg" in
|
||
--verbose|-v) _VERBOSE=true ;;
|
||
--python) _next_is_python=true ;;
|
||
esac
|
||
done
|
||
if [ "$_next_is_python" = true ]; then
|
||
die "--python requires a version argument (e.g. --python 3.12)"
|
||
fi
|
||
|
||
run_quiet() {
|
||
if [ "$_VERBOSE" = true ]; then
|
||
"$@"
|
||
else
|
||
"$@" > /dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
# ── Helper: download (curl or wget) ────────────────────────────────────────
|
||
download() {
|
||
if have curl; then
|
||
curl -LsSf "$1" -o "$2"
|
||
elif have wget; then
|
||
wget -qO "$2" "$1"
|
||
else
|
||
die "Neither curl nor wget found. Install one and re-run."
|
||
fi
|
||
}
|
||
|
||
# ── Helper: open browser (cross-platform) ──────────────────────────────────
|
||
open_browser() {
|
||
_url="$1"
|
||
if [ "$(uname)" = "Darwin" ] && have open; then
|
||
open "$_url"
|
||
elif grep -qi microsoft /proc/version 2>/dev/null; then
|
||
# WSL: use Windows browser
|
||
if have powershell.exe; then
|
||
powershell.exe -NoProfile -Command "Start-Process '$_url'" >/dev/null 2>&1 &
|
||
elif have cmd.exe; then
|
||
cmd.exe /c start "" "$_url" >/dev/null 2>&1 &
|
||
elif have xdg-open; then
|
||
xdg-open "$_url" >/dev/null 2>&1 &
|
||
else
|
||
echo " Open in your browser: $_url"
|
||
fi
|
||
elif have xdg-open; then
|
||
xdg-open "$_url" >/dev/null 2>&1 &
|
||
else
|
||
echo " Open in your browser: $_url"
|
||
fi
|
||
}
|
||
|
||
# ── Detect platform ────────────────────────────────────────────────────────
|
||
OS="linux"
|
||
if [ "$(uname)" = "Darwin" ]; then
|
||
OS="macos"
|
||
elif grep -qi microsoft /proc/version 2>/dev/null; then
|
||
OS="wsl"
|
||
fi
|
||
ARCH=$(uname -m)
|
||
|
||
echo ""
|
||
printf " ${C_TITLE}%s${C_RST}\n" "🎙 OmniVoice Studio Installer"
|
||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||
echo ""
|
||
|
||
step "platform" "$OS ($ARCH)"
|
||
|
||
# Detect Rosetta on macOS
|
||
if [ "$OS" = "macos" ] && [ "$ARCH" = "x86_64" ]; then
|
||
if [ "$(sysctl -in hw.optional.arm64 2>/dev/null || echo 0)" = "1" ]; then
|
||
warn "Apple Silicon detected running under Rosetta (x86_64)."
|
||
note "Re-run from a native arm64 terminal for full MLX support."
|
||
fi
|
||
fi
|
||
|
||
# ── Resolve script directory (for local installs) ──────────────────────────
|
||
SCRIPT_DIR=""
|
||
if [ -n "${0:-}" ] && [ -f "$0" ]; then
|
||
SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || true
|
||
fi
|
||
if [ -z "$SCRIPT_DIR" ]; then
|
||
SCRIPT_DIR=$(pwd)
|
||
fi
|
||
|
||
# If run via curl pipe, clone the repo first
|
||
if [ ! -f "$SCRIPT_DIR/pyproject.toml" ]; then
|
||
step "clone" "downloading OmniVoice Studio..."
|
||
INSTALL_DIR="$HOME/OmniVoice"
|
||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||
note "Updating existing clone at $INSTALL_DIR"
|
||
(cd "$INSTALL_DIR" && git pull --ff-only 2>/dev/null || true)
|
||
else
|
||
if have git; then
|
||
git clone --depth 1 https://github.com/debpalash/OmniVoice-Studio.git "$INSTALL_DIR"
|
||
else
|
||
die "git is required. Install git and re-run."
|
||
fi
|
||
fi
|
||
SCRIPT_DIR="$INSTALL_DIR"
|
||
fi
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# ── Python version ──────────────────────────────────────────────────────────
|
||
if [ -n "$_USER_PYTHON" ]; then
|
||
PYTHON_VERSION="$_USER_PYTHON"
|
||
note "Using user-specified Python $PYTHON_VERSION"
|
||
else
|
||
PYTHON_VERSION="3.11"
|
||
fi
|
||
|
||
# ── System dependencies ────────────────────────────────────────────────────
|
||
|
||
# Helper: install system packages with the right package manager
|
||
_install_sys_pkgs() {
|
||
case "$OS" in
|
||
macos)
|
||
if ! have brew; then
|
||
note "Installing Homebrew..."
|
||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" </dev/null
|
||
if [ -x /opt/homebrew/bin/brew ]; then
|
||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||
elif [ -x /usr/local/bin/brew ]; then
|
||
eval "$(/usr/local/bin/brew shellenv)"
|
||
fi
|
||
fi
|
||
brew install "$@" </dev/null 2>/dev/null || true
|
||
;;
|
||
linux|wsl)
|
||
if have apt-get; then
|
||
# Try without sudo first, then escalate
|
||
apt-get update -y </dev/null >/dev/null 2>&1 || true
|
||
apt-get install -y "$@" </dev/null >/dev/null 2>&1 || {
|
||
if have sudo; then
|
||
echo ""
|
||
echo " Need elevated permissions to install: $*"
|
||
sudo apt-get update -y </dev/null
|
||
sudo apt-get install -y "$@" </dev/null
|
||
else
|
||
die "Cannot install $*. Run as root or install manually."
|
||
fi
|
||
}
|
||
elif have dnf; then
|
||
sudo dnf install -y "$@" </dev/null 2>/dev/null || true
|
||
elif have yum; then
|
||
sudo yum install -y "$@" </dev/null 2>/dev/null || true
|
||
elif have pacman; then
|
||
sudo pacman -S --noconfirm "$@" 2>/dev/null || true
|
||
else
|
||
warn "No supported package manager found. Please install manually: $*"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Xcode CLT (macOS only)
|
||
if [ "$OS" = "macos" ]; then
|
||
step "xcode" "checking..."
|
||
if ! xcode-select -p >/dev/null 2>&1; then
|
||
note "Installing Xcode Command Line Tools..."
|
||
xcode-select --install </dev/null 2>/dev/null || true
|
||
until xcode-select -p >/dev/null 2>&1; do
|
||
note "Waiting for Xcode CLT install..."
|
||
sleep 10
|
||
done
|
||
fi
|
||
step "xcode" "$(xcode-select -p)"
|
||
fi
|
||
|
||
# ffmpeg (required for audio/video processing)
|
||
step "ffmpeg" "checking..."
|
||
if ! have ffmpeg; then
|
||
note "Installing ffmpeg..."
|
||
case "$OS" in
|
||
macos) _install_sys_pkgs ffmpeg ;;
|
||
linux|wsl)
|
||
if have apt-get; then
|
||
_install_sys_pkgs ffmpeg
|
||
elif have dnf; then
|
||
_install_sys_pkgs ffmpeg-free # Fedora
|
||
elif have pacman; then
|
||
_install_sys_pkgs ffmpeg
|
||
else
|
||
warn "Please install ffmpeg manually."
|
||
fi
|
||
;;
|
||
esac
|
||
fi
|
||
if have ffmpeg; then
|
||
step "ffmpeg" "$(ffmpeg -version 2>/dev/null | head -n1 | cut -d' ' -f1-3)"
|
||
else
|
||
warn "ffmpeg not found — some features will be unavailable."
|
||
fi
|
||
|
||
# ── Install uv ──────────────────────────────────────────────────────────────
|
||
step "uv" "checking..."
|
||
UV_MIN_VERSION="0.7.0"
|
||
|
||
_version_ge() {
|
||
# Returns 0 if $1 >= $2 (dotted version comparison)
|
||
_a=$1; _b=$2
|
||
while [ -n "$_a" ] || [ -n "$_b" ]; do
|
||
_a_part=${_a%%.*}; _b_part=${_b%%.*}
|
||
[ "$_a" = "$_a_part" ] && _a="" || _a=${_a#*.}
|
||
[ "$_b" = "$_b_part" ] && _b="" || _b=${_b#*.}
|
||
[ -z "$_a_part" ] && _a_part=0
|
||
[ -z "$_b_part" ] && _b_part=0
|
||
if [ "$_a_part" -gt "$_b_part" ] 2>/dev/null; then return 0; fi
|
||
if [ "$_a_part" -lt "$_b_part" ] 2>/dev/null; then return 1; fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
_uv_ok() {
|
||
have uv || return 1
|
||
_raw=$(uv --version 2>/dev/null | awk '{print $2}') || return 1
|
||
[ -n "$_raw" ] || return 1
|
||
_ver=${_raw%%[-+]*}
|
||
_version_ge "$_ver" "$UV_MIN_VERSION"
|
||
}
|
||
|
||
if ! _uv_ok; then
|
||
note "Installing uv package manager..."
|
||
_uv_tmp=$(mktemp)
|
||
download "https://astral.sh/uv/install.sh" "$_uv_tmp"
|
||
run_quiet sh "$_uv_tmp" </dev/null
|
||
rm -f "$_uv_tmp"
|
||
# Source env if uv's installer created it
|
||
if [ -f "$HOME/.local/bin/env" ]; then
|
||
. "$HOME/.local/bin/env"
|
||
fi
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
fi
|
||
step "uv" "$(uv --version 2>/dev/null)"
|
||
|
||
# ── Install bun (JS runtime for frontend) ──────────────────────────────────
|
||
step "bun" "checking..."
|
||
if ! have bun; then
|
||
note "Installing bun..."
|
||
if have curl; then
|
||
curl -fsSL https://bun.sh/install | sh </dev/null
|
||
else
|
||
die "curl is required to install bun."
|
||
fi
|
||
export PATH="$HOME/.bun/bin:$PATH"
|
||
fi
|
||
step "bun" "bun $(bun --version 2>/dev/null)"
|
||
|
||
# ── GPU detection (informational) ──────────────────────────────────────────
|
||
step "gpu" "detecting..."
|
||
GPU_INFO="CPU only"
|
||
if [ "$OS" = "macos" ] && [ "$ARCH" = "arm64" ]; then
|
||
GPU_INFO="Apple Silicon (Metal/MPS)"
|
||
elif have nvidia-smi; then
|
||
_gpu_name=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1)
|
||
_cuda_ver=$(nvidia-smi 2>/dev/null | sed -n 's/.*CUDA Version:[[:space:]]*\([0-9]*\.[0-9]*\).*/\1/p' | head -1)
|
||
if [ -n "$_gpu_name" ]; then
|
||
GPU_INFO="NVIDIA $_gpu_name (CUDA $_cuda_ver)"
|
||
fi
|
||
elif have rocminfo; then
|
||
_amd_name=$(rocminfo 2>/dev/null | awk '/Marketing Name:/{$1=$2=""; print; exit}' | sed 's/^ *//')
|
||
if [ -n "$_amd_name" ]; then
|
||
GPU_INFO="AMD $_amd_name (ROCm)"
|
||
fi
|
||
fi
|
||
step "gpu" "$GPU_INFO"
|
||
|
||
# ── Python dependencies via uv ──────────────────────────────────────────────
|
||
step "python" "syncing dependencies..."
|
||
note "This can take 5–10 min the first time (torch + torchaudio + demucs...)"
|
||
|
||
# Restricted-network support: when OMNIVOICE_REGION is set to china/russia/restricted,
|
||
# route python-build-standalone downloads through ghproxy.net. See issues #57, #60.
|
||
# Honors any existing UV_* env vars (power-user override).
|
||
case "${OMNIVOICE_REGION:-}" in
|
||
china|russia|restricted)
|
||
: "${UV_PYTHON_INSTALL_MIRROR:=https://ghproxy.net/https://github.com/astral-sh/python-build-standalone/releases/download}"
|
||
export UV_PYTHON_INSTALL_MIRROR
|
||
note "Using ghproxy.net mirror for Python download (OMNIVOICE_REGION=${OMNIVOICE_REGION})"
|
||
;;
|
||
esac
|
||
: "${UV_HTTP_TIMEOUT:=120}"
|
||
: "${UV_HTTP_RETRIES:=5}"
|
||
export UV_HTTP_TIMEOUT UV_HTTP_RETRIES
|
||
|
||
# Create venv with the target Python version if it doesn't exist.
|
||
# If the managed-python download fails (restricted network, mirror unreachable),
|
||
# fall back to the user's system Python.
|
||
if [ ! -d .venv ]; then
|
||
if ! uv venv --python "$PYTHON_VERSION"; then
|
||
warn "uv venv failed (likely Python download). Retrying with system Python..."
|
||
uv venv --python "$PYTHON_VERSION" --python-preference only-system \
|
||
|| die "uv venv failed: install Python $PYTHON_VERSION system-wide, set OMNIVOICE_REGION=china|russia|restricted to route through a mirror, or check your network."
|
||
fi
|
||
fi
|
||
|
||
# Sync all deps from pyproject.toml + uv.lock
|
||
uv sync
|
||
|
||
step "python" "OK — virtualenv at .venv/"
|
||
|
||
# ── Frontend deps + build ──────────────────────────────────────────────────
|
||
step "frontend" "installing dependencies..."
|
||
(cd frontend && bun install)
|
||
step "frontend" "OK"
|
||
|
||
step "frontend" "building bundle..."
|
||
(cd frontend && bun run build)
|
||
step "frontend" "OK — output at frontend/dist/"
|
||
|
||
# ── Log directory ──────────────────────────────────────────────────────────
|
||
case "$OS" in
|
||
macos) LOG_DIR="$HOME/Library/Application Support/OmniVoice" ;;
|
||
*) LOG_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/OmniVoice" ;;
|
||
esac
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
# ── Done ────────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
printf " ${C_TITLE}%s${C_RST}\n" "✓ Install complete!"
|
||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||
echo ""
|
||
step "next" "Run ./run.sh to start OmniVoice Studio"
|
||
echo ""
|
||
note "First launch downloads ~5 GB of ML model weights (OmniVoice TTS + Whisper)."
|
||
note "After that, launches are instant."
|
||
echo ""
|
||
note "GPU: $GPU_INFO"
|
||
note "Logs: $LOG_DIR/omnivoice.log"
|
||
echo ""
|