Files
VoiceStudio/scripts/desktop-prod.sh
T
4eac1b50d5 chore(desktop-prod): add --keep-models for fast fresh-app runs (#650)
`bun desktop-prod` (clean) wipes everything including the HF model cache, so
every fresh-install emulation re-downloads multi-GB weights — slow and bandwidth
-heavy, and the exact pain users on flaky networks hit. --keep-models wipes
app/backend data, logs, and webview state for an honest first-run, but KEEPS the
model cache so the weights aren't re-pulled. Ignored under --keep-data (which
keeps everything). Adds the `desktop-prod:keep-models` convenience script.
Scripts-only package.json change — no deps, bun.lock unaffected.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:30:14 +05:30

279 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────
# desktop-prod.sh — Build & launch OmniVoice Studio as a "fresh install"
#
# This gives you the EXACT same experience as a user downloading the
# installer (DMG on macOS, AppImage on Linux):
# • Full Rust bootstrap (venv creation, uv sync, model setup)
# • Splash screen with live logs
# • Region selector, version badge, etc.
#
# Usage:
# bun desktop-prod # build debug + wipe + launch
# bun desktop-prod:run # re-launch last build (skip compile)
# bun desktop-prod:upgrade # rebuild, but keep data (test upgrade)
# ──────────────────────────────────────────────────────────────────────────
set -euo pipefail
APP_ID="com.debpalash.omnivoice-studio"
TAURI_DIR="frontend/src-tauri"
APP_NAME="OmniVoice Studio"
# ── Detect platform ───────────────────────────────────────────────────────
OS="$(uname -s)"
case "$OS" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" ;; # Git Bash / MSYS2 / Cygwin on Windows
*) echo "❌ Unsupported platform: $OS"; exit 1 ;;
esac
# ── Platform-specific paths ───────────────────────────────────────────────
# Two directories matter for fresh-install simulation:
# APP_DATA — Tauri's bundle dir (keyed by APP_ID); holds the post-install
# Python venv + webview state.
# BACKEND_DATA — Where backend/core/config.py::get_app_data_dir() writes:
# SQLite db, voice profiles, generation outputs, logs. This is
# NOT under APP_ID — it's a separate hardcoded name. Cleaning
# only APP_DATA leaves all user data behind, defeating the
# fresh-emulation promise.
if [ "$PLATFORM" = "macos" ]; then
APP_DATA="$HOME/Library/Application Support/${APP_ID}"
BACKEND_DATA="$HOME/Library/Application Support/OmniVoice"
TAURI_LOGS="$HOME/Library/Logs/${APP_ID}"
WEBKIT_DATA="$HOME/Library/WebKit/${APP_ID}"
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
elif [ "$PLATFORM" = "windows" ]; then
# Git Bash exposes Windows env vars. Backend writes to %APPDATA%\OmniVoice
# (backend/core/config.py::get_app_data_dir) and relocates the HF cache to
# %LOCALAPPDATA%\OmniVoice\hf_cache. Tauri keys its data by APP_ID under
# LOCALAPPDATA; WebView2 state lives in EBWebView. All paths are APP_ID/
# OmniVoice-scoped, and each rm is guarded by `[ -d ]`, so a slightly-off
# path is a no-op, never a wrong delete.
APP_DATA="${LOCALAPPDATA}/${APP_ID}"
BACKEND_DATA="${APPDATA}/OmniVoice"
TAURI_LOGS="${LOCALAPPDATA}/${APP_ID}/logs"
WEBKIT_DATA="${LOCALAPPDATA}/${APP_ID}/EBWebView"
HF_CACHE="${HF_HOME:-${LOCALAPPDATA}/OmniVoice/hf_cache}"
else
# Linux: backend uses ~/.omnivoice (not XDG — see backend/core/config.py).
APP_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}"
BACKEND_DATA="$HOME/.omnivoice"
TAURI_LOGS="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}/logs"
WEBKIT_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}/webview"
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
fi
# ── Flags ──────────────────────────────────────────────────────────────────
SKIP_BUILD=false
KEEP_DATA=false
KEEP_MODELS=false
PILL_MODE=false
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=true ;;
--keep-data) KEEP_DATA=true ;;
--keep-models) KEEP_MODELS=true ;;
--pill) PILL_MODE=true ;;
-h|--help)
echo "Usage: $0 [--skip-build] [--keep-data] [--keep-models] [--pill]"
echo ""
echo " --skip-build Skip cargo build, use last compiled binary"
echo " --keep-data Don't wipe app data (test upgrade path)"
echo " --keep-models Wipe app/backend data for a fresh app, but KEEP the"
echo " HF model cache — fresh first-run without re-downloading"
echo " the multi-GB weights. Ignored when --keep-data is set."
echo " --pill Launch in dictation-widget mode (no main window)"
exit 0
;;
esac
done
# ── Wipe app data for fresh-install simulation ─────────────────────────────
if [ "$KEEP_DATA" = false ]; then
echo "🧹 Cleaning all OmniVoice data for fresh prod emulation..."
echo ""
# 1. App data (Tauri bundle dir: post-install venv + webview state)
if [ -d "${APP_DATA}" ]; then
echo " ✗ App data: ${APP_DATA}"
rm -rf "${APP_DATA}"
else
echo " ○ App data: (already clean)"
fi
# 1b. Backend data (SQLite db, voice profiles, outputs, logs)
# — separate dir hardcoded in backend/core/config.py, NOT under APP_ID.
if [ -d "${BACKEND_DATA}" ]; then
BD_SIZE=$(du -sh "${BACKEND_DATA}" 2>/dev/null | cut -f1)
echo " ✗ Backend data: ${BACKEND_DATA} (${BD_SIZE})"
rm -rf "${BACKEND_DATA}"
else
echo " ○ Backend data: (already clean)"
fi
# 2. HF model cache (downloaded .safetensors, tokenizers, etc.)
# --keep-models preserves it so a "fresh app" run doesn't re-pull multi-GB
# weights (the model-download is the slow, bandwidth-heavy part of a clean
# run; everything else still resets for an honest first-run emulation).
if [ "$KEEP_MODELS" = true ]; then
if [ -d "${HF_CACHE}" ]; then
HF_SIZE=$(du -sh "${HF_CACHE}" 2>/dev/null | cut -f1)
echo " ◆ HF cache: ${HF_CACHE} (${HF_SIZE}) — KEPT (--keep-models)"
else
echo " ○ HF cache: (already clean)"
fi
elif [ -d "${HF_CACHE}" ]; then
HF_SIZE=$(du -sh "${HF_CACHE}" 2>/dev/null | cut -f1)
echo " ✗ HF cache: ${HF_CACHE} (${HF_SIZE})"
rm -rf "${HF_CACHE}"
else
echo " ○ HF cache: (already clean)"
fi
# 3. Tauri log dir
if [ -d "${TAURI_LOGS}" ]; then
echo " ✗ Tauri logs: ${TAURI_LOGS}"
rm -rf "${TAURI_LOGS}"
else
echo " ○ Tauri logs: (already clean)"
fi
# 4. WebView cache / local storage
if [ -d "${WEBKIT_DATA}" ]; then
echo " ✗ WebKit data: ${WEBKIT_DATA}"
rm -rf "${WEBKIT_DATA}"
else
echo " ○ WebKit data: (already clean)"
fi
echo ""
echo " ✅ All clean — next launch bootstraps from zero."
else
echo "📦 Keeping existing app data (upgrade test mode)"
fi
# ── Build debug binary ─────────────────────────────────────────────────────
if [ "$SKIP_BUILD" = false ]; then
echo ""
echo "🔨 Building debug bundle (this takes 1-3 min first time)..."
# Remove stale bundle so we never accidentally launch old code
if [ "$PLATFORM" = "macos" ]; then
APP_BUNDLE="${TAURI_DIR}/target/debug/bundle/macos/${APP_NAME}.app"
[ -d "$APP_BUNDLE" ] && rm -rf "$APP_BUNDLE"
fi
# Linux: linuxdeploy uses FUSE to mount itself; if FUSE is unavailable
# (containers, some hardened kernels), set APPIMAGE_EXTRACT_AND_RUN=1 to
# extract-and-run instead. Safe to always set on Linux.
if [ "$PLATFORM" = "linux" ]; then
export APPIMAGE_EXTRACT_AND_RUN=1
fi
# The build creates the bundle successfully, but then may fail trying
# to sign the updater artifact (no TAURI_SIGNING_PRIVATE_KEY) or to
# run linuxdeploy. The binary itself is fine — tolerate known errors.
BUILD_LOG=$(mktemp)
cd frontend
set +e
bunx tauri build --debug 2>&1 | tee "$BUILD_LOG"
BUILD_EXIT=$?
set -e
cd ..
if [ $BUILD_EXIT -ne 0 ]; then
# Known-harmless failures:
# - Missing TAURI_SIGNING_PRIVATE_KEY (updater signing)
# - "failed to run linuxdeploy" (AppImage bundling — binary still works)
if grep -qi "TAURI_SIGNING_PRIVATE_KEY\|private key\|failed to run linuxdeploy\|failed to bundle" "$BUILD_LOG"; then
echo "⚠️ Non-fatal bundle error — binary is fine (see above for details)."
else
echo "❌ Build failed with exit code $BUILD_EXIT"
rm -f "$BUILD_LOG"
exit $BUILD_EXIT
fi
fi
rm -f "$BUILD_LOG"
echo "✅ Build complete."
else
echo "⏭️ Skipping build (--skip-build)"
fi
# ── Build launch args ──────────────────────────────────────────────────────
LAUNCH_ARGS=()
if [ "$PILL_MODE" = true ]; then
LAUNCH_ARGS+=("--pill")
echo "📌 Launch mode: pill (dictation-only widget, no main window)"
fi
# ── Find and launch the app ────────────────────────────────────────────────
if [ "$PLATFORM" = "macos" ]; then
APP_BUNDLE="${TAURI_DIR}/target/debug/bundle/macos/${APP_NAME}.app"
BINARY="${TAURI_DIR}/target/debug/omnivoice-studio"
if [ -d "$APP_BUNDLE" ]; then
echo ""
echo "🚀 Launching ${APP_NAME} (.app bundle)..."
echo " Bundle: ${APP_BUNDLE}"
# macOS `open` needs -n to spawn a fresh instance, --args to forward flags.
if [ ${#LAUNCH_ARGS[@]} -gt 0 ]; then
open -n "$APP_BUNDLE" --args "${LAUNCH_ARGS[@]}"
else
open "$APP_BUNDLE"
fi
elif [ -f "$BINARY" ]; then
echo ""
echo "🚀 Launching ${APP_NAME} (raw binary — no .app bundle)..."
echo " Binary: ${BINARY}"
"$BINARY" "${LAUNCH_ARGS[@]}" &
else
echo "❌ No bundle or binary found. Run without --skip-build first."
exit 1
fi
elif [ "$PLATFORM" = "windows" ]; then
# Windows: launch the raw debug .exe (Git Bash can exec it directly).
BINARY="${TAURI_DIR}/target/debug/omnivoice-studio.exe"
if [ -f "$BINARY" ]; then
echo ""
echo "🚀 Launching ${APP_NAME} (Windows debug .exe)..."
echo " Binary: ${BINARY}"
"$BINARY" "${LAUNCH_ARGS[@]}" &
else
echo "❌ No .exe found at ${BINARY}. Run without --skip-build first."
exit 1
fi
else
# Linux: prefer AppImage, fall back to raw binary
APPIMAGE=$(find "${TAURI_DIR}/target/debug/bundle/appimage" -name "*.AppImage" -type f 2>/dev/null | head -1)
BINARY="${TAURI_DIR}/target/debug/omnivoice-studio"
if [ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ]; then
echo ""
echo "🚀 Launching ${APP_NAME} (AppImage)..."
echo " AppImage: ${APPIMAGE}"
chmod +x "$APPIMAGE"
"$APPIMAGE" "${LAUNCH_ARGS[@]}" &
elif [ -f "$BINARY" ]; then
echo ""
echo "🚀 Launching ${APP_NAME} (raw binary)..."
echo " Binary: ${BINARY}"
"$BINARY" "${LAUNCH_ARGS[@]}" &
else
echo "❌ No AppImage or binary found. Run without --skip-build first."
exit 1
fi
fi
echo " App data: ${APP_DATA}"
echo ""
echo "✅ App launched. Check the splash screen for bootstrap logs."
if [ "$PILL_MODE" = true ]; then
echo " To re-run pill mode without rebuilding: bun desktop-prod:run:pill"
echo " To switch back to studio: bun desktop-prod:run"
else
echo " To re-run without rebuilding: bun desktop-prod:run"
echo " To launch as dictation widget: bun desktop-prod:pill"
fi