#!/usr/bin/env bash set -euo pipefail # release.py replaces this placeholder, or --pubkey-file provides a development key. VISKA_RELEASE_PUBKEY_PEM='-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAsfAJMY6MB24e1ez38QWayD2G0Sm0BUUzUFkY6lvwHKQ= -----END PUBLIC KEY-----' VISKA_RELEASE_PUBKEY_PLACEHOLDER='__VISKA_RELEASE_PUB'"KEY__" VISKA_DEPLOY_BUNDLE_B64='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' VISKA_DEPLOY_BUNDLE_PLACEHOLDER='__VISKA_DEPLOY_BUN'"DLE__" ROOT=${VISKA_SERVER_ROOT:-} if [[ -n $ROOT ]]; then ROOT=${ROOT%/}; fi HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) path() { printf '%s%s' "$ROOT" "$1"; } fail() { printf 'Viska Server: %s\n' "$*" >&2; exit 1; } ask() { local reply; printf '%s ' "$1" > /dev/tty; IFS= read -r reply < /dev/tty || :; printf '%s' "$reply"; } confirm() { local reply; [ "$YES" = 1 ] && return 0; reply=$(ask "$1 [y/N]"); [[ $reply == [yY]* ]]; } need() { command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"; } YES=0 EXPOSE='' ACCEL=auto DOMAIN='' PUBLIC_PORT=443 LANGS=sv,en HOME_MODEL='' BUDGET=1500 WHEEL='' CT2_WHEEL='' PUBKEY_FILE='' MANIFEST_URL=https://getviska.app/server/manifest.json MODEL_SV='' MODEL_EN='' MODEL_AUTO='' while (($#)); do case $1 in --yes) YES=1; shift ;; --expose|--accel|--domain|--public-port|--languages|--model|--home|--budget|--wheel|--manifest-url|--ct2-wheel|--pubkey-file) (($# >= 2)) || fail "Missing argument for $1" flag=$1 value=$2; shift 2 case $flag in --expose) EXPOSE=$value ;; --accel) ACCEL=$value ;; --domain) DOMAIN=$value; EXPOSE=caddy ;; --public-port) PUBLIC_PORT=$value ;; --languages) LANGS=$value ;; --home) HOME_MODEL=$value ;; --budget) BUDGET=$value ;; --wheel) WHEEL=$value ;; --manifest-url) MANIFEST_URL=$value ;; --ct2-wheel) CT2_WHEEL=$value ;; --pubkey-file) PUBKEY_FILE=$value ;; --model) case $value in sv=*) MODEL_SV=${value#*=} ;; en=*) MODEL_EN=${value#*=} ;; auto=*) MODEL_AUTO=${value#*=} ;; *) fail "Invalid --model LANG=ID" ;; esac ;; esac ;; --uninstall) if [[ $VISKA_DEPLOY_BUNDLE_B64 != "$VISKA_DEPLOY_BUNDLE_PLACEHOLDER" ]]; then uninstall_tmp=$(mktemp -d) printf '%s' "$VISKA_DEPLOY_BUNDLE_B64" | base64 -d | tar -xz -C "$uninstall_tmp" uninstall.sh bash "$uninstall_tmp/uninstall.sh" "${@:2}" result=$? rm -rf "$uninstall_tmp" exit "$result" fi exec "$HERE/uninstall.sh" "${@:2}" ;; *) fail "Unknown argument: $1" ;; esac done [[ $PUBLIC_PORT == 443 || $PUBLIC_PORT == 8443 || $PUBLIC_PORT == 10000 ]] || fail 'Invalid public port' [[ $BUDGET =~ ^[0-9]+$ ]] || fail 'Budget must be a nonnegative integer' [[ $ACCEL =~ ^(auto|cuda|rocm|cpu)$ ]] || fail 'Invalid accelerator' [[ -z $EXPOSE || $EXPOSE =~ ^(tailscale|caddy|none)$ ]] || fail 'Invalid exposure' [[ $MANIFEST_URL == https://* ]] || { [[ -n $WHEEL ]] || fail 'Manifest URL must use HTTPS'; } [ -z "$DOMAIN" ] || [[ $DOMAIN =~ ^[[:alnum:]-]+(\.[[:alnum:]-]+)+$ ]] || fail 'Invalid domain' [ "$(id -u)" -eq 0 ] || fail 'Run as root (sudo)' need python3; need openssl; need systemctl; need apt-get if [ -z "$ROOT" ]; then [ -d /run/systemd/system ] || fail 'systemd is required' # shellcheck disable=SC1091 . /etc/os-release [[ ${ID:-} == ubuntu || ${ID:-} == debian || " ${ID_LIKE:-} " == *' debian '* ]] || fail 'Ubuntu or Debian required' [[ $(uname -m) == x86_64 ]] || fail 'x86_64 is required' for disk in /opt /var/lib; do free_kb=$(df -Pk "$disk" | awk 'NR==2 {print $4}') (( free_kb >= 10485760 )) || fail "At least 10 GB free required on $disk" done fi python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' || fail 'Python 3.10 or later required' if [[ -z $ROOT ]]; then VENV_PROBE=$(mktemp -d) if ! python3 -m venv "$VENV_PROBE/check" >/dev/null 2>&1; then apt-get install -y python3-venv; fi rm -rf "$VENV_PROBE" else python3 -m venv --help >/dev/null 2>&1 || apt-get install -y python3-venv fi if ! command -v ffmpeg >/dev/null 2>&1; then apt-get install -y ffmpeg; fi if [[ $ACCEL == auto ]]; then if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then ACCEL=cuda elif [[ -e $(path /dev/kfd) ]] && command -v rocminfo >/dev/null 2>&1 && rocminfo >/dev/null 2>&1; then ACCEL=rocm elif command -v lspci >/dev/null 2>&1 && lspci | grep -qi nvidia; then fail 'NVIDIA GPU detected without working driver. On Ubuntu run: sudo ubuntu-drivers install; reboot and rerun the installer.' else confirm 'No GPU found; install CPU-only (much slower)?' || fail 'CPU install declined' ACCEL=cpu fi fi if [[ $ACCEL == rocm && -z $CT2_WHEEL ]]; then fail 'ROCm requires --ct2-wheel PATH|URL (experimental)'; fi if [[ $CT2_WHEEL == http://* ]]; then fail 'ROCm wheel must be a local path or HTTPS URL'; fi if [[ -z $EXPOSE ]]; then if command -v tailscale >/dev/null 2>&1; then EXPOSE=tailscale; else EXPOSE=none; fi fi [[ $EXPOSE != caddy || -n $DOMAIN ]] || fail 'Caddy exposure requires --domain DOMAIN' if [[ $YES != 1 ]]; then if [[ $LANGS == sv,en && -z $MODEL_SV && -z $MODEL_EN ]]; then selected=$(ask 'Languages (sv,en / sv / en / multilingual) [sv,en]:') [[ -z $selected ]] || LANGS=$selected fi fi MULTILINGUAL=0 if [[ $LANGS == multilingual ]]; then MULTILINGUAL=1 if [[ $YES != 1 && -z $MODEL_SV ]]; then printf 'Multilingual model: 1 whisper-large-v3-turbo-ct2; 2 whisper-large-v3-ct2; 3 kb-whisper-medium-ct2; 4 skip\n' > /dev/tty selected=$(ask 'Choose [1]:') case $selected in ''|1) MODEL_SV=whisper-large-v3-turbo-ct2 ;; 2) MODEL_SV=whisper-large-v3-ct2 ;; 3) MODEL_SV=kb-whisper-medium-ct2 ;; 4) MODEL_SV='' ;; *) fail 'Invalid selection' ;; esac else MODEL_SV=${MODEL_SV:-whisper-large-v3-turbo-ct2} fi MODEL_EN=${MODEL_EN:-$MODEL_SV} LANGS=sv,en fi case "$LANGS" in sv|en|sv,en|en,sv) ;; *) fail 'Languages must be sv,en, sv, en or multilingual' ;; esac [[ ,$LANGS, == *,sv,* ]] || MODEL_SV= [[ ,$LANGS, == *,en,* ]] || MODEL_EN= choose_model() { local lang=$1 selection if [[ $lang == sv ]]; then printf 'Swedish: 1 kb-whisper-small-ct2; 2 kb-whisper-medium-ct2; 3 kb-whisper-large-ct2; 4 skip\n' > /dev/tty selection=$(ask 'Choose [2]:') case $selection in 1) MODEL_SV=kb-whisper-small-ct2 ;; ''|2) MODEL_SV=kb-whisper-medium-ct2 ;; 3) MODEL_SV=kb-whisper-large-ct2 ;; 4) MODEL_SV= ;; *) fail 'Invalid selection' ;; esac else printf 'English: 1 whisper-medium-en-ct2; 2 whisper-large-v3-turbo-ct2; 3 whisper-large-v3-ct2; 4 skip\n' > /dev/tty selection=$(ask 'Choose [1]:') case $selection in ''|1) MODEL_EN=whisper-medium-en-ct2 ;; 2) MODEL_EN=whisper-large-v3-turbo-ct2 ;; 3) MODEL_EN=whisper-large-v3-ct2 ;; 4) MODEL_EN= ;; *) fail 'Invalid selection' ;; esac fi } if [[ ,$LANGS, == *,sv,* && -z $MODEL_SV && $MULTILINGUAL != 1 ]]; then if [[ $YES == 1 ]]; then MODEL_SV=kb-whisper-medium-ct2; else choose_model sv; fi; fi if [[ ,$LANGS, == *,en,* && -z $MODEL_EN && $MULTILINGUAL != 1 ]]; then if [[ $YES == 1 ]]; then MODEL_EN=whisper-medium-en-ct2; else choose_model en; fi; fi if [[ $HOME_MODEL == none ]]; then HOME_MODEL=''; else HOME_MODEL=${HOME_MODEL:-${MODEL_SV:-$MODEL_EN}}; fi for model in "$MODEL_SV" "$MODEL_EN" "$MODEL_AUTO" "$HOME_MODEL"; do [[ -z $model || $model =~ ^[a-zA-Z0-9_-]+$ ]] || fail 'Invalid model ID' done if ! id viska-server >/dev/null 2>&1; then useradd --system --home-dir /var/lib/viska-server --shell /usr/sbin/nologin viska-server; fi if [[ $ACCEL == rocm ]]; then for group in video render; do if getent group "$group" >/dev/null; then usermod -aG "$group" viska-server; fi; done; fi install -d -m 0755 "$(path /opt/viska-server/bin)" "$(path /opt/viska-server/releases)" "$(path /etc/viska-server/prompts)" "$(path /etc/systemd/system)" "$(path /usr/local/bin)" install -d -m 0750 -o viska-server -g viska-server "$(path /var/lib/viska-server)" "$(path /var/lib/viska-server/models)" "$(path /var/lib/viska-server/hf)" install -d -m 0700 "$(path /var/lib/viska-server-updater)" if [[ -n $PUBKEY_FILE ]]; then PUBKEY=$PUBKEY_FILE; else [[ $VISKA_RELEASE_PUBKEY_PEM != "$VISKA_RELEASE_PUBKEY_PLACEHOLDER" ]] || fail 'Installer has no release key; supply --pubkey-file for development' PUBKEY=$(mktemp); printf '%s\n' "$VISKA_RELEASE_PUBKEY_PEM" > "$PUBKEY" fi WORK=$(mktemp -d) DEPLOY_DIR=$HERE if [[ $VISKA_DEPLOY_BUNDLE_B64 != "$VISKA_DEPLOY_BUNDLE_PLACEHOLDER" ]]; then DEPLOY_DIR=$WORK/deploy mkdir -p "$DEPLOY_DIR" printf '%s' "$VISKA_DEPLOY_BUNDLE_B64" | base64 -d | tar -xz -C "$DEPLOY_DIR" fi cleanup() { rm -rf "$WORK"; if [[ -z $PUBKEY_FILE ]]; then rm -f "$PUBKEY"; fi; } trap cleanup EXIT if [[ -n $WHEEL ]]; then [[ -f $WHEEL ]] || fail "Local wheel missing: $WHEEL" WHEEL_NAME=$(basename "$WHEEL") cp "$WHEEL" "$WORK/$WHEEL_NAME" VERSION=$(python3 - "$WHEEL" <<'PY' import re,sys m=re.search(r'viska_server-([0-9]+(?:\.[0-9]+){2}(?:[a-zA-Z0-9.]*)?)-', sys.argv[1].rsplit('/',1)[-1]) if not m: raise SystemExit('Invalid Viska Server wheel filename') print(m.group(1)) PY ) else need curl; need base64 curl -fsSL --max-filesize 65536 "$MANIFEST_URL" -o "$WORK/manifest.json" [[ $(wc -c < "$WORK/manifest.json" | tr -d ' ') -le 65536 ]] || fail 'Manifest too large' curl -fsSL --max-filesize 4096 "$MANIFEST_URL.sig" -o "$WORK/manifest.json.sig" base64 -d < "$WORK/manifest.json.sig" > "$WORK/signature.bin" || fail 'Invalid manifest signature encoding' openssl pkeyutl -verify -pubin -inkey "$PUBKEY" -rawin -in "$WORK/manifest.json" -sigfile "$WORK/signature.bin" >/dev/null || fail 'Manifest signature verification failed (OpenSSL 3 required)' python3 - "$WORK/manifest.json" > "$WORK/manifest-fields" <<'PY' import json,re,sys m=json.load(open(sys.argv[1])) w=m['wheel']; v=m['version'] assert m['product']=='viska-server' and re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', v) assert w['url'].startswith('https://') and 00 print(v,w['url'],w['sha256'],w['size'],m['seq']) PY read -r VERSION WHEEL_URL SHA SIZE INITIAL_SEQ < "$WORK/manifest-fields" WHEEL_NAME=${WHEEL_URL##*/} case $WHEEL_NAME in viska_server-*.whl) ;; *) fail 'Invalid wheel filename' ;; esac [[ $WHEEL_NAME =~ ^viska_server-[a-zA-Z0-9_.+-]+\.whl$ ]] || fail 'Invalid wheel filename' curl -fsSL --max-filesize 209715200 "$WHEEL_URL" -o "$WORK/$WHEEL_NAME" [[ $(wc -c < "$WORK/$WHEEL_NAME" | tr -d ' ') == "$SIZE" ]] || fail 'Wheel size mismatch' [[ $(openssl dgst -sha256 "$WORK/$WHEEL_NAME" | awk '{print $NF}') == "$SHA" ]] || fail 'Wheel SHA256 mismatch' [[ $INITIAL_SEQ =~ ^[0-9]+$ ]] || fail 'Invalid manifest seq' if [[ -f $(path /var/lib/viska-server-updater/state.json) ]]; then stored_seq=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("seq",0))' "$(path /var/lib/viska-server-updater/state.json)") ((INITIAL_SEQ >= stored_seq)) || fail 'Refusing to downgrade installed release' fi fi RELEASE=$(path "/opt/viska-server/releases/$VERSION") if [[ ! -x $RELEASE/bin/viska-server ]]; then STAGING=${RELEASE}.tmp rm -rf "$STAGING" python3 -m venv "$STAGING" case $ACCEL in cuda) "$STAGING/bin/python" -m pip install "$WORK/${WHEEL_NAME}[cuda]" ;; *) "$STAGING/bin/python" -m pip install "$WORK/$WHEEL_NAME" ;; esac if [[ $ACCEL == rocm ]]; then "$STAGING/bin/python" -m pip install "$CT2_WHEEL"; fi mv "$STAGING" "$RELEASE" fi install -m 0755 "$DEPLOY_DIR/bin/viska-server" "$(path /opt/viska-server/bin/viska-server)" install -m 0755 "$DEPLOY_DIR/bin/viska-server-updater" "$(path /opt/viska-server/bin/viska-server-updater)" ln -sfn "$RELEASE" "$(path /opt/viska-server/current)" ln -sfn "$(path /opt/viska-server/bin/viska-server)" "$(path /usr/local/bin/viska-server)" ETC=$(path /etc/viska-server) if [[ ! -f $ETC/secrets.env ]]; then TOKEN=$(openssl rand -base64 48 | tr -d '\n/+=' | cut -c1-48) KEY= LEGACY=$(path /etc/viska-remote/env) if [[ -f $LEGACY ]]; then previous_token=$(grep '^VISKA_REMOTE_TOKEN=' "$LEGACY" | head -n 1 | cut -d= -f2- || :) [[ -z $previous_token ]] || TOKEN=$previous_token KEY=$(grep '^OLLAMA_API_KEY=' "$LEGACY" | head -n 1 | cut -d= -f2- || :) elif [[ $YES != 1 ]]; then printf 'Ollama Cloud API key (empty skips): ' > /dev/tty IFS= read -rs KEY < /dev/tty || : printf '\n' > /dev/tty fi (umask 027; printf 'VISKA_SERVER_TOKEN=%s\nOLLAMA_API_KEY=%s\nOPENAI_COMPAT_API_KEY=\n' "$TOKEN" "$KEY" > "$ETC/secrets.env") chown root:viska-server "$ETC/secrets.env"; chmod 0640 "$ETC/secrets.env" fi if [[ -f $(path /etc/viska-remote/env) ]] && confirm 'Disable old viska-remote service (same port)?'; then systemctl disable --now viska-remote; fi PUBLIC_URL= if [[ $EXPOSE == tailscale ]]; then need tailscale tailscale serve --bg "--https=$PUBLIC_PORT" http://127.0.0.1:8765 DNS=$(tailscale status --json | python3 -c 'import json,sys; print(json.load(sys.stdin).get("Self",{}).get("DNSName","").rstrip("."))') if [[ -n $DNS ]]; then PUBLIC_URL="https://$DNS"; if [[ $PUBLIC_PORT != 443 ]]; then PUBLIC_URL+=":$PUBLIC_PORT"; fi; fi elif [[ $EXPOSE == caddy ]]; then need curl; need gpg apt-get install -y debian-keyring debian-archive-keyring apt-transport-https install -d -m 0755 "$(path /usr/share/keyrings)" "$(path /etc/apt/sources.list.d)" curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key | gpg --batch --yes --dearmor -o "$(path /usr/share/keyrings/caddy-stable-archive-keyring.gpg)" curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt -o "$(path /etc/apt/sources.list.d/caddy-stable.list)" if [[ -n $ROOT ]]; then sed -i.bak "s|/usr/share/keyrings/|$ROOT/usr/share/keyrings/|g" "$(path /etc/apt/sources.list.d/caddy-stable.list)" rm -f "$(path /etc/apt/sources.list.d/caddy-stable.list.bak)" fi apt-get update; apt-get install -y caddy install -d -m 0755 "$(path /etc/caddy/Caddyfile.d)" cat > "$(path /etc/caddy/Caddyfile.d/viska-server.caddy)" <uri query { delete token } } } } } EOF CADDYFILE=$(path /etc/caddy/Caddyfile) touch "$CADDYFILE" grep -Fxq 'import Caddyfile.d/*.caddy' "$CADDYFILE" || printf '\nimport Caddyfile.d/*.caddy\n' >> "$CADDYFILE" if command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then ufw allow 80/tcp; ufw allow 443/tcp; fi systemctl reload caddy PUBLIC_URL="https://$DOMAIN" fi if [[ ! -f $ETC/config.toml ]]; then python3 - "$DEPLOY_DIR/config.toml.in" "$ETC/config.toml" "$MODEL_SV" "$MODEL_EN" "$HOME_MODEL" "$BUDGET" "$PUBLIC_URL" "$MANIFEST_URL" <<'PY' from pathlib import Path import sys source, target, sv, en, home, budget, url, manifest = sys.argv[1:] s=Path(source).read_text() for k,v in {'SV':sv,'EN':en,'HOME':home,'BUDGET':budget,'PUBLIC_URL':url,'MANIFEST_URL':manifest}.items(): s=s.replace('@'+k+'@', v.replace('\\','\\\\').replace('"','\\"')) Path(target).write_text(s) PY chown root:viska-server "$ETC/config.toml"; chmod 0640 "$ETC/config.toml" fi printf 'ACCEL=%s\nEXPOSE=%s\nPUBLIC_URL=%s\n' "$ACCEL" "$EXPOSE" "$PUBLIC_URL" > "$ETC/install.env" chmod 0644 "$ETC/install.env" for unit in viska-server.service viska-server-update.service viska-server-update.timer; do install -m 0644 "$DEPLOY_DIR/systemd/$unit" "$(path "/etc/systemd/system/$unit")" done DROP=$(path /etc/systemd/system/viska-server.service.d) install -d -m 0755 "$DROP" MEM_KB=$(awk '/^MemTotal:/ { print $2; exit }' "$(path /proc/meminfo)") [[ $MEM_KB =~ ^[0-9]+$ ]] || fail 'Cannot read MemTotal from /proc/meminfo' printf '[Service]\nMemoryHigh=%sK\nMemoryMax=%sK\nMemorySwapMax=0\n' "$((MEM_KB * 70 / 100))" "$((MEM_KB * 85 / 100))" > "$DROP/10-memory.conf" if [[ $ACCEL == rocm ]]; then printf '[Service]\nSupplementaryGroups=video render\n' > "$DROP/20-rocm.conf"; else rm -f "$DROP/20-rocm.conf"; fi systemctl daemon-reload models=() for model in "$MODEL_SV" "$MODEL_EN" "$MODEL_AUTO"; do if [[ -n $model ]]; then found=0 for existing in "${models[@]+"${models[@]}"}"; do [[ $existing != "$model" ]] || found=1; done [[ $found == 1 ]] || models+=("$model") fi done if ((${#models[@]})); then "$(path /opt/viska-server/bin/viska-server)" models install "${models[@]}"; fi systemctl enable --now viska-server.service READY=0 for _ in $(seq 1 300); do if curl -fsS http://127.0.0.1:8765/health 2>/dev/null | python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("ready") is True else 1)' 2>/dev/null; then READY=1; break; fi if [[ -n $ROOT ]]; then break; fi sleep 1 done if [[ $READY != 1 ]]; then printf 'WARNING: Viska Server did not become ready within 300 seconds. Check journalctl -u viska-server.\n' >&2; fi systemctl enable --now viska-server-update.timer printf '\nViska Server %s installed; accelerator=%s, exposure=%s, URL=%s\n' "$VERSION" "$ACCEL" "$EXPOSE" "${PUBLIC_URL:-local only}" if [[ -n ${INITIAL_SEQ:-} && ! -f $(path /var/lib/viska-server-updater/state.json) ]]; then printf '{"seq":%s,"bad_versions":[]}\n' "$INITIAL_SEQ" > "$(path /var/lib/viska-server-updater/state.json)" chmod 0600 "$(path /var/lib/viska-server-updater/state.json)" fi "$(path /opt/viska-server/bin/viska-server)" pair