fix: resolve all shellcheck errors

- Replace 'A && B || C' patterns with proper if-then-else statements (SC2015)
- Add check_for_virtualbox function to invoke prompt_for_virtualbox_challenge (SC2317)
- Fix install_launcher function to escape variable in heredoc (SC2119/SC2120)
- Apply shfmt formatting to ensure consistent style

Fixes 7 SC2015 violations, 1 SC2317 violation, and 1 SC2119/SC2120 pair.
All 79 shell files now pass shellcheck without errors.
This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-16 21:17:08 +01:00
parent 8e0a720499
commit 03bd36e41d
7 changed files with 1666 additions and 1627 deletions

View File

@ -13,8 +13,8 @@
set -e set -e
[ "${GPU_VENDOR}" = "amd" ] || { [ "${GPU_VENDOR}" = "amd" ] || {
echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}" echo "AMD installer invoked but GPU_VENDOR=${GPU_VENDOR}"
exit 0 exit 0
} }
AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0} AMD_INSTALL_XF86=${AMD_INSTALL_XF86:-0}
@ -26,15 +26,15 @@ AMD_ENABLE_SI_CIK=${AMD_ENABLE_SI_CIK:-auto}
AMD_SKIP_INITRAMFS=${AMD_SKIP_INITRAMFS:-0} AMD_SKIP_INITRAMFS=${AMD_SKIP_INITRAMFS:-0}
AMD_VERBOSE=${AMD_VERBOSE:-0} AMD_VERBOSE=${AMD_VERBOSE:-0}
vlog() { [ "$AMD_VERBOSE" = 1 ] && echo "[amd] $*" || true; } vlog() { if [ "$AMD_VERBOSE" = 1 ]; then echo "[amd] $*"; fi; }
info() { echo "[amd] $*"; } info() { echo "[amd] $*"; }
warn() { echo "[amd][warn] $*" >&2; } warn() { echo "[amd][warn] $*" >&2; }
# Detect multilib enabled # Detect multilib enabled
if grep -q '^\[multilib\]' /etc/pacman.conf; then if grep -q '^\[multilib\]' /etc/pacman.conf; then
MULTILIB_ENABLED=1 MULTILIB_ENABLED=1
else else
MULTILIB_ENABLED=0 MULTILIB_ENABLED=0
fi fi
# Basic packages # Basic packages
@ -58,49 +58,49 @@ LIB32_AMDVLK_PKG="lib32-amdvlk"
# Simple AUR builder (reused from NVIDIA script style) # Simple AUR builder (reused from NVIDIA script style)
_build_aur_pkg() { _build_aur_pkg() {
local pkg="$1" local pkg="$1"
local url="https://aur.archlinux.org/${pkg}.git" local url="https://aur.archlinux.org/${pkg}.git"
mkdir -p "$HOME/aur" mkdir -p "$HOME/aur"
cd "$HOME/aur" cd "$HOME/aur"
if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi if [ ! -d "$pkg" ]; then git clone "$url"; else (cd "$pkg" && git fetch -q --all && git reset -q --hard origin/HEAD || git pull --ff-only || true); fi
cd "$pkg" cd "$pkg"
rm -f -- *.pkg.tar.* 2> /dev/null || true rm -f -- *.pkg.tar.* 2>/dev/null || true
yes | makepkg -s -c -C --noconfirm --needed yes | makepkg -s -c -C --noconfirm --needed
local built=(*.pkg.tar.zst) local built=(*.pkg.tar.zst)
yes | sudo pacman -U --noconfirm "${built[@]}" yes | sudo pacman -U --noconfirm "${built[@]}"
} }
_install_repo_or_aur() { _install_repo_or_aur() {
local pkg="$1" local pkg="$1"
if pacman -Si "$pkg" > /dev/null 2>&1; then if pacman -Si "$pkg" >/dev/null 2>&1; then
if pacman -Qi "$pkg" > /dev/null 2>&1; then if pacman -Qi "$pkg" >/dev/null 2>&1; then
vlog "$pkg already installed" vlog "$pkg already installed"
else else
yes | sudo pacman -Sy --noconfirm "$pkg" yes | sudo pacman -Sy --noconfirm "$pkg"
fi fi
else else
info "Building AUR package: $pkg" info "Building AUR package: $pkg"
_build_aur_pkg "$pkg" _build_aur_pkg "$pkg"
fi fi
} }
info "Installing AMD GPU stack" info "Installing AMD GPU stack"
for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done for p in "${BASE_PKGS[@]}" "$VULKAN_PKG"; do _install_repo_or_aur "$p"; done
if [ "$AMD_INSTALL_XF86" = 1 ]; then if [ "$AMD_INSTALL_XF86" = 1 ]; then
_install_repo_or_aur "$XF86_PKG" _install_repo_or_aur "$XF86_PKG"
fi fi
# AMDVLK optional (install after vulkan-radeon if requested) # AMDVLK optional (install after vulkan-radeon if requested)
if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then
_install_repo_or_aur "$AMDVLK_PKG" _install_repo_or_aur "$AMDVLK_PKG"
fi fi
if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then if [ $MULTILIB_ENABLED = 1 ] || [ "$AMD_INSTALL_LIB32" = 1 ]; then
for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done for p in "${LIB32_BASE[@]}" "$LIB32_VULKAN_PKG"; do _install_repo_or_aur "$p"; done
if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi if [ "$AMD_INSTALL_AMDVLK" = 1 ]; then _install_repo_or_aur "$LIB32_AMDVLK_PKG"; fi
else else
vlog "Skipping 32-bit packages (multilib disabled)" vlog "Skipping 32-bit packages (multilib disabled)"
fi fi
# Detect SI / CIK codename presence for optional amdgpu enablement # Detect SI / CIK codename presence for optional amdgpu enablement
@ -113,41 +113,41 @@ for n in "${SI_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_SI=1 && bre
for n in "${CIK_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_CIK=1 && break; done for n in "${CIK_NAMES[@]}"; do echo "$GPU_LINES" | grep -q "$n" && IS_CIK=1 && break; done
if [ "$AMD_ENABLE_SI_CIK" = "1" ] || { [ "$AMD_ENABLE_SI_CIK" = "auto" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; }; then if [ "$AMD_ENABLE_SI_CIK" = "1" ] || { [ "$AMD_ENABLE_SI_CIK" = "auto" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; }; then
info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)" info "Configuring amdgpu for SI/CIK (IS_SI=$IS_SI IS_CIK=$IS_CIK)"
TMP_CONF=$(mktemp) TMP_CONF=$(mktemp)
printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' > "$TMP_CONF" printf 'options amdgpu si_support=1\noptions amdgpu cik_support=1\n' >"$TMP_CONF"
printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >> "$TMP_CONF" printf 'options radeon si_support=0\noptions radeon cik_support=0\n' >>"$TMP_CONF"
sudo mkdir -p /etc/modprobe.d sudo mkdir -p /etc/modprobe.d
sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf sudo cp "$TMP_CONF" /etc/modprobe.d/10-amdgpu-si-cik.conf
rm -f "$TMP_CONF" rm -f "$TMP_CONF"
# Ensure amdgpu early in MODULES # Ensure amdgpu early in MODULES
if [ -f /etc/mkinitcpio.conf ]; then if [ -f /etc/mkinitcpio.conf ]; then
if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then if ! grep -q '^MODULES=.*amdgpu' /etc/mkinitcpio.conf; then
sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true sudo sed -i 's/^MODULES=\(.*\)/MODULES=(amdgpu radeon)/' /etc/mkinitcpio.conf || true
fi fi
if ! grep -q 'modconf' /etc/mkinitcpio.conf; then if ! grep -q 'modconf' /etc/mkinitcpio.conf; then
warn "modconf hook not found in mkinitcpio.conf (needed for module options)" warn "modconf hook not found in mkinitcpio.conf (needed for module options)"
fi fi
if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then if [ "$AMD_SKIP_INITRAMFS" != 1 ]; then
info "Regenerating initramfs (mkinitcpio -P)" info "Regenerating initramfs (mkinitcpio -P)"
sudo mkinitcpio -P || warn "mkinitcpio failed; review manually" sudo mkinitcpio -P || warn "mkinitcpio failed; review manually"
else else
info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1" info "Skipping initramfs regeneration per AMD_SKIP_INITRAMFS=1"
fi fi
else else
warn "/etc/mkinitcpio.conf not found; skipping MODULES update" warn "/etc/mkinitcpio.conf not found; skipping MODULES update"
fi fi
else else
vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)" vlog "SI/CIK enablement not required (AMD_ENABLE_SI_CIK=$AMD_ENABLE_SI_CIK IS_SI=$IS_SI IS_CIK=$IS_CIK)"
fi fi
# Check active kernel driver # Check active kernel driver
KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}') KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}')
[ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'amdgpu|radeon' | head -n1 | awk '{print $1}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'amdgpu|radeon' | head -n1 | awk '{print $1}')
info "Kernel driver in use: ${KDRV:-unknown}" info "Kernel driver in use: ${KDRV:-unknown}"
if [ "$KDRV" = "radeon" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; then if [ "$KDRV" = "radeon" ] && { [ $IS_SI = 1 ] || [ $IS_CIK = 1 ]; }; then
warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu" warn "radeon driver still active for SI/CIK; reboot may be required to switch to amdgpu"
fi fi
export AMD_STACK_DONE=1 export AMD_STACK_DONE=1

View File

@ -13,8 +13,8 @@
set -e set -e
[ "$GPU_VENDOR" = "intel" ] || { [ "$GPU_VENDOR" = "intel" ] || {
echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR" echo "Intel installer invoked but GPU_VENDOR=$GPU_VENDOR"
exit 0 exit 0
} }
INTEL_USE_AMBER=${INTEL_USE_AMBER:-0} INTEL_USE_AMBER=${INTEL_USE_AMBER:-0}
@ -26,7 +26,7 @@ INTEL_ENABLE_GUC=${INTEL_ENABLE_GUC:-}
INTEL_SKIP_INITRAMFS=${INTEL_SKIP_INITRAMFS:-0} INTEL_SKIP_INITRAMFS=${INTEL_SKIP_INITRAMFS:-0}
INTEL_VERBOSE=${INTEL_VERBOSE:-1} INTEL_VERBOSE=${INTEL_VERBOSE:-1}
vlog() { [ "$INTEL_VERBOSE" = 1 ] && echo "[intel] $*" || true; } vlog() { if [ "$INTEL_VERBOSE" = 1 ]; then echo "[intel] $*"; fi; }
info() { echo "[intel] $*"; } info() { echo "[intel] $*"; }
warn() { echo "[intel][warn] $*" >&2; } warn() { echo "[intel][warn] $*" >&2; }
@ -35,24 +35,24 @@ if grep -q '^\[multilib\]' /etc/pacman.conf; then MULTILIB=1; else MULTILIB=0; f
# Base mesa package # Base mesa package
if [ "$INTEL_USE_AMBER" = 1 ]; then if [ "$INTEL_USE_AMBER" = 1 ]; then
BASE_MESA=mesa-amber BASE_MESA=mesa-amber
LIB32_BASE=lib32-mesa-amber LIB32_BASE=lib32-mesa-amber
else else
BASE_MESA=mesa BASE_MESA=mesa
LIB32_BASE=lib32-mesa LIB32_BASE=lib32-mesa
fi fi
install_pkg() { install_pkg() {
local pkg="$1" local pkg="$1"
if pacman -Qi "$pkg" > /dev/null 2>&1; then if pacman -Qi "$pkg" >/dev/null 2>&1; then
vlog "$pkg already installed" vlog "$pkg already installed"
else else
if pacman -Si "$pkg" > /dev/null 2>&1; then if pacman -Si "$pkg" >/dev/null 2>&1; then
yes | sudo pacman -Sy --noconfirm "$pkg" yes | sudo pacman -Sy --noconfirm "$pkg"
else else
warn "Package $pkg not found in repos (not handling AUR here)" warn "Package $pkg not found in repos (not handling AUR here)"
fi fi
fi fi
} }
info "Installing Intel GPU stack" info "Installing Intel GPU stack"
@ -60,47 +60,47 @@ install_pkg "$BASE_MESA"
# 32-bit mesa # 32-bit mesa
if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then if { [ "$INTEL_INSTALL_LIB32" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32" = 1 ]; then
install_pkg "$LIB32_BASE" install_pkg "$LIB32_BASE"
else else
vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)" vlog "Skipping 32-bit mesa (INTEL_INSTALL_LIB32=$INTEL_INSTALL_LIB32 MULTILIB=$MULTILIB)"
fi fi
# Vulkan # Vulkan
if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then if [ "$INTEL_INSTALL_VULKAN" = 1 ]; then
install_pkg vulkan-intel install_pkg vulkan-intel
if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then if { [ "$INTEL_INSTALL_LIB32_VK" = auto ] && [ $MULTILIB = 1 ]; } || [ "$INTEL_INSTALL_LIB32_VK" = 1 ]; then
install_pkg lib32-vulkan-intel install_pkg lib32-vulkan-intel
else else
vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)" vlog "Skipping 32-bit vulkan (INTEL_INSTALL_LIB32_VK=$INTEL_INSTALL_LIB32_VK MULTILIB=$MULTILIB)"
fi fi
fi fi
# Legacy xf86-video-intel (not recommended) # Legacy xf86-video-intel (not recommended)
if [ "$INTEL_INSTALL_XF86" = 1 ]; then if [ "$INTEL_INSTALL_XF86" = 1 ]; then
install_pkg xf86-video-intel install_pkg xf86-video-intel
else else
vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)" vlog "Not installing xf86-video-intel (INTEL_INSTALL_XF86=$INTEL_INSTALL_XF86)"
fi fi
# GuC / HuC enablement # GuC / HuC enablement
if [ -n "$INTEL_ENABLE_GUC" ]; then if [ -n "$INTEL_ENABLE_GUC" ]; then
if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then if ! echo "$INTEL_ENABLE_GUC" | grep -Eq '^[0-3]$'; then
warn "INTEL_ENABLE_GUC must be 0..3; ignoring" warn "INTEL_ENABLE_GUC must be 0..3; ignoring"
else else
info "Configuring enable_guc=$INTEL_ENABLE_GUC" info "Configuring enable_guc=$INTEL_ENABLE_GUC"
sudo mkdir -p /etc/modprobe.d sudo mkdir -p /etc/modprobe.d
echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf > /dev/null echo "options i915 enable_guc=$INTEL_ENABLE_GUC" | sudo tee /etc/modprobe.d/i915-guc.conf >/dev/null
if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then if [ "$INTEL_SKIP_INITRAMFS" != 1 ] && [ -f /etc/mkinitcpio.conf ]; then
info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change" info "Regenerating initramfs (mkinitcpio -P) for GuC/HuC change"
sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually" sudo mkinitcpio -P || warn "mkinitcpio failed; continue manually"
else else
info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)" info "Skipping initramfs regeneration (INTEL_SKIP_INITRAMFS=$INTEL_SKIP_INITRAMFS)"
fi fi
fi fi
fi fi
# Report kernel driver # Report kernel driver
KDRV=$(lspci -k -d ::0300 2> /dev/null | awk '/Kernel driver in use:/ {print $5; exit}') KDRV=$(lspci -k -d ::0300 2>/dev/null | awk '/Kernel driver in use:/ {print $5; exit}')
[ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'i915|xe' | head -n1 | awk '{print $1}') [ -z "$KDRV" ] && KDRV=$(lsmod | grep -E 'i915|xe' | head -n1 | awk '{print $1}')
info "Kernel driver in use: ${KDRV:-unknown}" info "Kernel driver in use: ${KDRV:-unknown}"

View File

@ -7,59 +7,63 @@ TARGET=/etc/hosts
LOGTAG=hosts-guard-hook LOGTAG=hosts-guard-hook
stop_units_if_present() { stop_units_if_present() {
local units=(hosts-bind-mount.service hosts-guard.path) local units=(hosts-bind-mount.service hosts-guard.path)
for u in "${units[@]}"; do for u in "${units[@]}"; do
if command -v systemctl > /dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2> /dev/null | grep -q "^$u"; then if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
systemctl stop "$u" > /dev/null 2>&1 || true systemctl stop "$u" >/dev/null 2>&1 || true
fi fi
fi fi
done done
} }
is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2> /dev/null | grep -qw ro; } is_ro_mount() { findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro; }
mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2> /dev/null || echo 0; } mount_layers_count() { awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0; }
cleanup_mount_stacks() { cleanup_mount_stacks() {
local i=0 local i=0
# Unmount bind layers until /etc/hosts is no longer a mountpoint # Unmount bind layers until /etc/hosts is no longer a mountpoint
if command -v mountpoint > /dev/null 2>&1; then if command -v mountpoint >/dev/null 2>&1; then
while mountpoint -q "$TARGET"; do while mountpoint -q "$TARGET"; do
umount -l "$TARGET" > /dev/null 2>&1 || break umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i > 20)) && break ((i > 20)) && break
done done
else else
# Fallback to best-effort using mountinfo count # Fallback to best-effort using mountinfo count
local cnt local cnt
cnt=$(mount_layers_count) cnt=$(mount_layers_count)
while ((cnt > 1)); do while ((cnt > 1)); do
umount -l "$TARGET" > /dev/null 2>&1 || break umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i > 20)) && break ((i > 20)) && break
cnt=$(mount_layers_count) cnt=$(mount_layers_count)
done done
fi fi
} }
# Drop protective attributes if present # Drop protective attributes if present
if command -v lsattr > /dev/null 2>&1; then if command -v lsattr >/dev/null 2>&1; then
attrs=$(lsattr -d "$TARGET" 2> /dev/null || true) attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
echo "$attrs" | grep -q " i " && chattr -i "$TARGET" > /dev/null 2>&1 || true if echo "$attrs" | grep -q " i "; then
echo "$attrs" | grep -q " a " && chattr -a "$TARGET" > /dev/null 2>&1 || true chattr -i "$TARGET" >/dev/null 2>&1 || true
fi
if echo "$attrs" | grep -q " a "; then
chattr -a "$TARGET" >/dev/null 2>&1 || true
fi
fi fi
stop_units_if_present stop_units_if_present
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)" logger -t "$LOGTAG" "pre: unlocking /etc/hosts (starting)"
echo "$(date -Is) pre-unlock" >> /run/hosts-guard-hook.log 2> /dev/null || true echo "$(date -Is) pre-unlock" >>/run/hosts-guard-hook.log 2>/dev/null || true
# Always collapse any existing layers; we'll operate on the plain file # Always collapse any existing layers; we'll operate on the plain file
cleanup_mount_stacks cleanup_mount_stacks
# If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again # If someone managed a ro single-layer mount, ensure rw by remounting or collapsing again
if is_ro_mount; then if is_ro_mount; then
mount -o remount,rw "$TARGET" > /dev/null 2>&1 || cleanup_mount_stacks mount -o remount,rw "$TARGET" >/dev/null 2>&1 || cleanup_mount_stacks
fi fi
logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)" logger -t "$LOGTAG" "pre: unlocking /etc/hosts (done)"

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,11 @@ SCRIPT_NAME="$(basename "$0")"
# ---------- User/paths ---------- # ---------- User/paths ----------
if [[ -n ${SUDO_USER:-} ]]; then if [[ -n ${SUDO_USER:-} ]]; then
ACTUAL_USER="$SUDO_USER" ACTUAL_USER="$SUDO_USER"
USER_HOME="/home/$SUDO_USER" USER_HOME="/home/$SUDO_USER"
else else
ACTUAL_USER="$USER" ACTUAL_USER="$USER"
USER_HOME="$HOME" USER_HOME="$HOME"
fi fi
INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp" INSTALL_ROOT_DEFAULT="$USER_HOME/.local/share/unreal-mcp"
@ -32,12 +32,12 @@ FORCE_UPDATE=false
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { fail() {
echo "[ERROR] $*" >&2 echo "[ERROR] $*" >&2
exit 1 exit 1
} }
usage() { usage() {
cat << EOF cat <<EOF
Usage: $SCRIPT_NAME [options] Usage: $SCRIPT_NAME [options]
Options: Options:
@ -56,150 +56,150 @@ EOF
} }
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--install-dir) --install-dir)
shift shift
[[ $# -gt 0 ]] || fail "--install-dir requires a value" [[ $# -gt 0 ]] || fail "--install-dir requires a value"
INSTALL_ROOT="$1" INSTALL_ROOT="$1"
;; ;;
--project) --project)
shift shift
[[ $# -gt 0 ]] || fail "--project requires a path to .uproject" [[ $# -gt 0 ]] || fail "--project requires a path to .uproject"
PROJECT_UPROJECT="$1" PROJECT_UPROJECT="$1"
;; ;;
--no-continue) --no-continue)
CONFIGURE_CONTINUE=false CONFIGURE_CONTINUE=false
;; ;;
--no-vscode) --no-vscode)
CONFIGURE_VSCODE_USER=false CONFIGURE_VSCODE_USER=false
;; ;;
--force-update) --force-update)
FORCE_UPDATE=true FORCE_UPDATE=true
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
*) *)
fail "Unknown option: $1" fail "Unknown option: $1"
;; ;;
esac esac
shift shift
done done
REPO_DIR="$INSTALL_ROOT/unreal-mcp" REPO_DIR="$INSTALL_ROOT/unreal-mcp"
# ---------- Dependencies ---------- # ---------- Dependencies ----------
require_cmd() { command -v "$1" > /dev/null 2>&1; } require_cmd() { command -v "$1" >/dev/null 2>&1; }
ensure_packages_arch() { ensure_packages_arch() {
# Install with pacman using sudo when needed; keep idempotent with --needed # Install with pacman using sudo when needed; keep idempotent with --needed
local pkgs=(git jq uv python rsync) local pkgs=(git jq uv python rsync)
local to_install=() local to_install=()
for p in "${pkgs[@]}"; do for p in "${pkgs[@]}"; do
if ! pacman -Qi "$p" > /dev/null 2>&1; then if ! pacman -Qi "$p" >/dev/null 2>&1; then
to_install+=("$p") to_install+=("$p")
fi fi
done done
if [[ ${#to_install[@]} -gt 0 ]]; then if [[ ${#to_install[@]} -gt 0 ]]; then
log "Installing packages: ${to_install[*]}" log "Installing packages: ${to_install[*]}"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
pacman -S --noconfirm --needed "${to_install[@]}" pacman -S --noconfirm --needed "${to_install[@]}"
else else
sudo pacman -S --noconfirm --needed "${to_install[@]}" sudo pacman -S --noconfirm --needed "${to_install[@]}"
fi fi
else else
log "All required packages already installed" log "All required packages already installed"
fi fi
} }
check_python_version() { check_python_version() {
if require_cmd python; then if require_cmd python; then
local v local v
v=$(python -V 2>&1 | awk '{print $2}') v=$(python -V 2>&1 | awk '{print $2}')
elif require_cmd python3; then elif require_cmd python3; then
local v local v
v=$(python3 -V 2>&1 | awk '{print $2}') v=$(python3 -V 2>&1 | awk '{print $2}')
else else
log "python not found; pacman install will provide it" log "python not found; pacman install will provide it"
return 0 return 0
fi fi
# Require >= 3.12 (Unreal MCP docs) # Require >= 3.12 (Unreal MCP docs)
local major minor local major minor
major=$(echo "$v" | cut -d. -f1) major=$(echo "$v" | cut -d. -f1)
minor=$(echo "$v" | cut -d. -f2) minor=$(echo "$v" | cut -d. -f2)
if ((major < 3 || (major == 3 && minor < 12))); then if ((major < 3 || (major == 3 && minor < 12))); then
log "Python $v detected; installing newer python via pacman" log "Python $v detected; installing newer python via pacman"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
pacman -S --noconfirm --needed python pacman -S --noconfirm --needed python
else else
sudo pacman -S --noconfirm --needed python sudo pacman -S --noconfirm --needed python
fi fi
fi fi
} }
# ---------- Git clone/update ---------- # ---------- Git clone/update ----------
setup_repo() { setup_repo() {
mkdir -p "$INSTALL_ROOT" mkdir -p "$INSTALL_ROOT"
if [[ ! -d "$REPO_DIR/.git" ]]; then if [[ ! -d "$REPO_DIR/.git" ]]; then
log "Cloning unreal-mcp into $REPO_DIR" log "Cloning unreal-mcp into $REPO_DIR"
if require_cmd git; then if require_cmd git; then
git clone "$REPO_URL" "$REPO_DIR" git clone "$REPO_URL" "$REPO_DIR"
else else
fail "git is required but not found after install" fail "git is required but not found after install"
fi fi
else else
log "Repo exists at $REPO_DIR" log "Repo exists at $REPO_DIR"
if [[ $FORCE_UPDATE == true ]]; then if [[ $FORCE_UPDATE == true ]]; then
log "Updating repo with --force-update" log "Updating repo with --force-update"
git -C "$REPO_DIR" fetch origin git -C "$REPO_DIR" fetch origin
git -C "$REPO_DIR" reset --hard origin/main git -C "$REPO_DIR" reset --hard origin/main
git -C "$REPO_DIR" pull --rebase --autostash git -C "$REPO_DIR" pull --rebase --autostash
else else
log "Pulling latest changes" log "Pulling latest changes"
git -C "$REPO_DIR" pull --rebase --autostash git -C "$REPO_DIR" pull --rebase --autostash
fi fi
fi fi
# Ensure ownership for the real user when script ran via sudo # Ensure ownership for the real user when script ran via sudo
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$INSTALL_ROOT"
fi fi
} }
# ---------- Launcher ---------- # ---------- Launcher ----------
install_launcher() { install_launcher() {
local bin_dir="$USER_HOME/.local/bin" local bin_dir="$USER_HOME/.local/bin"
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local launcher="$bin_dir/unreal-mcp-server" local launcher="$bin_dir/unreal-mcp-server"
mkdir -p "$bin_dir" mkdir -p "$bin_dir"
cat > "$launcher" << EOF cat >"$launcher" <<EOF
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
exec uv --directory "$python_dir" run unreal_mcp_server.py "${1:-}" < /dev/null exec uv --directory "$python_dir" run unreal_mcp_server.py "\${1:-}" < /dev/null
EOF EOF
chmod +x "$launcher" chmod +x "$launcher"
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$launcher"; fi
log "Installed launcher: $launcher" log "Installed launcher: $launcher"
} }
# ---------- VS Code: Continue MCP config ---------- # ---------- VS Code: Continue MCP config ----------
configure_continue() { configure_continue() {
if [[ $CONFIGURE_CONTINUE != true ]]; then if [[ $CONFIGURE_CONTINUE != true ]]; then
log "Skipping Continue config (--no-continue)" log "Skipping Continue config (--no-continue)"
return 0 return 0
fi fi
local cont_dir="$USER_HOME/.continue" local cont_dir="$USER_HOME/.continue"
local cont_cfg="$cont_dir/config.json" local cont_cfg="$cont_dir/config.json"
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
mkdir -p "$cont_dir" mkdir -p "$cont_dir"
# Base JSON when no config exists # Base JSON when no config exists
local tmp_file local tmp_file
tmp_file="$(mktemp)" tmp_file="$(mktemp)"
if [[ ! -f $cont_cfg ]]; then if [[ ! -f $cont_cfg ]]; then
cat > "$tmp_file" << JSON cat >"$tmp_file" <<JSON
{ {
"mcpServers": { "mcpServers": {
"unrealMCP": { "unrealMCP": {
@ -209,147 +209,147 @@ configure_continue() {
} }
} }
JSON JSON
mv "$tmp_file" "$cont_cfg" mv "$tmp_file" "$cont_cfg"
else else
# Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP # Merge using jq: ensure .mcpServers exists, then set/overwrite unrealMCP
if ! require_cmd jq; then if ! require_cmd jq; then
fail "jq is required to merge ~/.continue/config.json" fail "jq is required to merge ~/.continue/config.json"
fi fi
jq --arg dir "$python_dir" ' jq --arg dir "$python_dir" '
.mcpServers = (.mcpServers // {}) | .mcpServers = (.mcpServers // {}) |
.mcpServers.unrealMCP = { .mcpServers.unrealMCP = {
command: "uv", command: "uv",
args: ["--directory", $dir, "run", "unreal_mcp_server.py"] args: ["--directory", $dir, "run", "unreal_mcp_server.py"]
} }
' "$cont_cfg" > "$tmp_file" && mv "$tmp_file" "$cont_cfg" ' "$cont_cfg" >"$tmp_file" && mv "$tmp_file" "$cont_cfg"
fi fi
if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi if [[ $EUID -eq 0 ]]; then chown "$ACTUAL_USER:$ACTUAL_USER" "$cont_cfg"; fi
log "Configured Continue MCP at: $cont_cfg" log "Configured Continue MCP at: $cont_cfg"
} }
# ---------- VS Code user MCP (native) ---------- # ---------- VS Code user MCP (native) ----------
configure_vscode_user_mcp() { configure_vscode_user_mcp() {
if [[ $CONFIGURE_VSCODE_USER != true ]]; then if [[ $CONFIGURE_VSCODE_USER != true ]]; then
log "Skipping VS Code user MCP config (--no-vscode)" log "Skipping VS Code user MCP config (--no-vscode)"
return 0 return 0
fi fi
if ! require_cmd jq; then if ! require_cmd jq; then
fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles" fail "jq is required to compose VS Code --add-mcp JSON and to parse profiles"
fi fi
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local json local json
json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}') json=$(jq -n --arg dir "$python_dir" '{name:"unrealMCP", command:"uv", args:["--directory", $dir, "run", "unreal_mcp_server.py"]}')
# Handle multiple VS Code variants if present # Handle multiple VS Code variants if present
local candidates=(code code-insiders codium) local candidates=(code code-insiders codium)
local found_any=false local found_any=false
for cli in "${candidates[@]}"; do for cli in "${candidates[@]}"; do
if ! command -v "$cli" > /dev/null 2>&1; then if ! command -v "$cli" >/dev/null 2>&1; then
continue continue
fi fi
found_any=true found_any=true
log "Registering MCP server in VS Code user profile via: $cli --add-mcp" log "Registering MCP server in VS Code user profile via: $cli --add-mcp"
if "$cli" --add-mcp "$json" > "/tmp/${cli}-add-mcp.log" 2>&1; then if "$cli" --add-mcp "$json" >"/tmp/${cli}-add-mcp.log" 2>&1; then
log "[$cli] user profile: unrealMCP added/updated" log "[$cli] user profile: unrealMCP added/updated"
else else
sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true sed -n '1,200p' "/tmp/${cli}-add-mcp.log" || true
fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode." fail "[$cli] --add-mcp failed for user profile. Ensure your VS Code supports MCP or rerun with --no-vscode."
fi fi
# Detect profiles with 'unreal' (case-insensitive) and add there too # Detect profiles with 'unreal' (case-insensitive) and add there too
local data_dir="" local data_dir=""
case "$cli" in case "$cli" in
code) code)
data_dir="$USER_HOME/.config/Code" data_dir="$USER_HOME/.config/Code"
;; ;;
code-insiders) code-insiders)
data_dir="$USER_HOME/.config/Code - Insiders" data_dir="$USER_HOME/.config/Code - Insiders"
;; ;;
codium) codium)
data_dir="$USER_HOME/.config/VSCodium" data_dir="$USER_HOME/.config/VSCodium"
;; ;;
esac esac
local profiles_json="$data_dir/User/profiles/profiles.json" local profiles_json="$data_dir/User/profiles/profiles.json"
if [[ -f $profiles_json ]]; then if [[ -f $profiles_json ]]; then
# Extract profile names matching /unreal/i # Extract profile names matching /unreal/i
mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json") mapfile -t unreal_profiles < <(jq -r '.profiles // [] | .[] | .name // empty | select(test("unreal"; "i"))' "$profiles_json")
if [[ ${#unreal_profiles[@]} -gt 0 ]]; then if [[ ${#unreal_profiles[@]} -gt 0 ]]; then
log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}" log "[$cli] Found profiles with 'unreal': ${unreal_profiles[*]}"
local name local name
for name in "${unreal_profiles[@]}"; do for name in "${unreal_profiles[@]}"; do
log "[$cli] Adding unrealMCP to profile: $name" log "[$cli] Adding unrealMCP to profile: $name"
if "$cli" --profile "$name" --add-mcp "$json" > "/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then if "$cli" --profile "$name" --add-mcp "$json" >"/tmp/${cli}-add-mcp-${name// /_}.log" 2>&1; then
log "[$cli] profile '$name': unrealMCP added/updated" log "[$cli] profile '$name': unrealMCP added/updated"
else else
sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true sed -n '1,200p' "/tmp/${cli}-add-mcp-${name// /_}.log" || true
fail "[$cli] --add-mcp failed for profile '$name'." fail "[$cli] --add-mcp failed for profile '$name'."
fi fi
done done
else else
log "[$cli] No VS Code profiles with 'unreal' in name" log "[$cli] No VS Code profiles with 'unreal' in name"
fi fi
else else
log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)" log "[$cli] Profiles file not found: $profiles_json (skipping profile-specific adds)"
fi fi
done done
if [[ $found_any == false ]]; then if [[ $found_any == false ]]; then
fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip." fail "VS Code CLI not found (code/code-insiders/codium). Install VS Code and ensure 'code' CLI is available, or run with --no-vscode to skip."
fi fi
} }
# ---------- Unreal Plugin copy (optional) ---------- # ---------- Unreal Plugin copy (optional) ----------
install_plugin_into_project() { install_plugin_into_project() {
[[ -n $PROJECT_UPROJECT ]] || return 0 [[ -n $PROJECT_UPROJECT ]] || return 0
local upath="$PROJECT_UPROJECT" local upath="$PROJECT_UPROJECT"
if [[ -d $upath ]]; then if [[ -d $upath ]]; then
# Resolve .uproject in the provided directory # Resolve .uproject in the provided directory
mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2> /dev/null || true) mapfile -t _uprojects < <(find "$upath" -maxdepth 1 -type f -name "*.uproject" 2>/dev/null || true)
if [[ ${#_uprojects[@]} -eq 0 ]]; then if [[ ${#_uprojects[@]} -eq 0 ]]; then
fail "--project directory '$upath' contains no .uproject files" fail "--project directory '$upath' contains no .uproject files"
elif [[ ${#_uprojects[@]} -gt 1 ]]; then elif [[ ${#_uprojects[@]} -gt 1 ]]; then
printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2 printf '[ERROR] Multiple .uproject files found in %s:\n' "$upath" >&2
printf ' - %s\n' "${_uprojects[@]}" >&2 printf ' - %s\n' "${_uprojects[@]}" >&2
fail "Please pass the specific .uproject path to --project" fail "Please pass the specific .uproject path to --project"
else else
upath="${_uprojects[0]}" upath="${_uprojects[0]}"
log "Resolved .uproject: $upath" log "Resolved .uproject: $upath"
fi fi
elif [[ -f $upath ]]; then elif [[ -f $upath ]]; then
true true
else else
fail "--project path does not exist: $upath" fail "--project path does not exist: $upath"
fi fi
if [[ ${upath##*.} != "uproject" ]]; then if [[ ${upath##*.} != "uproject" ]]; then
fail "--project must point to a .uproject file (got: $upath)" fail "--project must point to a .uproject file (got: $upath)"
fi fi
local proj_dir local proj_dir
proj_dir="$(cd "$(dirname "$upath")" && pwd)" proj_dir="$(cd "$(dirname "$upath")" && pwd)"
RESOLVED_PROJECT_DIR="$proj_dir" RESOLVED_PROJECT_DIR="$proj_dir"
local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP" local src_plugin="$REPO_DIR/MCPGameProject/Plugins/UnrealMCP"
local dst_plugin="$proj_dir/Plugins/UnrealMCP" local dst_plugin="$proj_dir/Plugins/UnrealMCP"
if [[ ! -d $src_plugin ]]; then if [[ ! -d $src_plugin ]]; then
fail "Source plugin not found at $src_plugin (did repo layout change?)" fail "Source plugin not found at $src_plugin (did repo layout change?)"
fi fi
mkdir -p "$proj_dir/Plugins" mkdir -p "$proj_dir/Plugins"
log "Copying UnrealMCP plugin to project: $dst_plugin" log "Copying UnrealMCP plugin to project: $dst_plugin"
rsync -a --delete "$src_plugin/" "$dst_plugin/" rsync -a --delete "$src_plugin/" "$dst_plugin/"
# Set ownership back to actual user if run as root # Set ownership back to actual user if run as root
if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi if [[ $EUID -eq 0 ]]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$proj_dir/Plugins"; fi
log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed." log "Plugin installed. Enable it from Unreal Editor (Edit > Plugins) if needed."
} }
# ---------- Summary ---------- # ---------- Summary ----------
print_summary() { print_summary() {
local python_dir="$REPO_DIR/Python" local python_dir="$REPO_DIR/Python"
local plugin_dest="N/A" local plugin_dest="N/A"
if [[ -n $RESOLVED_PROJECT_DIR ]]; then if [[ -n $RESOLVED_PROJECT_DIR ]]; then
plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP" plugin_dest="$RESOLVED_PROJECT_DIR/Plugins/UnrealMCP"
fi fi
cat << EOF cat <<EOF
============================================ ============================================
Unreal MCP setup complete Unreal MCP setup complete
============================================ ============================================
@ -382,15 +382,15 @@ EOF
} }
main() { main() {
log "Installing prerequisites (Arch Linux)" log "Installing prerequisites (Arch Linux)"
ensure_packages_arch ensure_packages_arch
check_python_version check_python_version
setup_repo setup_repo
install_launcher install_launcher
configure_continue configure_continue
install_plugin_into_project install_plugin_into_project
configure_vscode_user_mcp configure_vscode_user_mcp
print_summary print_summary
} }
main "$@" main "$@"

View File

@ -44,19 +44,19 @@ DEBUG=0
# Colors # Colors
if [[ -t 1 && ${NO_COLOR} -eq 0 ]]; then if [[ -t 1 && ${NO_COLOR} -eq 0 ]]; then
GREEN="\e[32m" GREEN="\e[32m"
YELLOW="\e[33m" YELLOW="\e[33m"
RED="\e[31m" RED="\e[31m"
BLUE="\e[34m" BLUE="\e[34m"
BOLD="\e[1m" BOLD="\e[1m"
RESET="\e[0m" RESET="\e[0m"
else else
GREEN="" GREEN=""
YELLOW="" YELLOW=""
RED="" RED=""
BLUE="" BLUE=""
BOLD="" BOLD=""
RESET="" RESET=""
fi fi
log() { echo -e "${BLUE}[INFO]${RESET} $*"; } log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
@ -65,7 +65,7 @@ err() { echo -e "${RED}[ERR ]${RESET} $*" >&2; }
success() { echo -e "${GREEN}[OK ]${RESET} $*"; } success() { echo -e "${GREEN}[OK ]${RESET} $*"; }
usage() { usage() {
cat << EOF cat <<EOF
${SCRIPT_NAME} v${VERSION} ${SCRIPT_NAME} v${VERSION}
Setup or uninstall a self-hosted LibreTranslate instance via Docker. Setup or uninstall a self-hosted LibreTranslate instance via Docker.
@ -111,376 +111,378 @@ EOF
} }
gen_api_key() { gen_api_key() {
# Avoid SIGPIPE issues under set -o pipefail by capturing output first # Avoid SIGPIPE issues under set -o pipefail by capturing output first
local key local key
key=$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 40 || true) key=$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 40 || true)
if [[ -z $key || ${#key} -lt 40 ]]; then if [[ -z $key || ${#key} -lt 40 ]]; then
# Fallback using openssl if available # Fallback using openssl if available
if command -v openssl > /dev/null 2>&1; then if command -v openssl >/dev/null 2>&1; then
key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true) key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true)
fi fi
fi fi
if [[ -z $key || ${#key} -lt 20 ]]; then if [[ -z $key || ${#key} -lt 20 ]]; then
# Last resort static warning key (should not happen) # Last resort static warning key (should not happen)
key="LT$(date +%s)$$RANDOM" key="LT$(date +%s)$$RANDOM"
fi fi
printf '%s' "$key" printf '%s' "$key"
} }
need_cmd() { need_cmd() {
command -v "$1" > /dev/null 2>&1 || { command -v "$1" >/dev/null 2>&1 || {
err "Required command '$1' not found" err "Required command '$1' not found"
return 1 return 1
} }
} }
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--image) --image)
IMAGE="$2" IMAGE="$2"
shift 2 shift 2
;; ;;
--tag) --tag)
TAG="$2" TAG="$2"
shift 2 shift 2
;; ;;
--port) --port)
PORT="$2" PORT="$2"
shift 2 shift 2
;; ;;
--host) --host)
HOST="$2" HOST="$2"
shift 2 shift 2
;; ;;
--data-dir) --data-dir)
DATA_DIR="$2" DATA_DIR="$2"
CACHE_DIR="${DATA_DIR}/cache" CACHE_DIR="${DATA_DIR}/cache"
shift 2 shift 2
;; ;;
--cache-dir) --cache-dir)
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--no-docker-install) --no-docker-install)
DOCKER_INSTALL=0 DOCKER_INSTALL=0
shift shift
;; ;;
--keep-alive) --keep-alive)
KEEP_ALIVE=1 KEEP_ALIVE=1
shift shift
;; ;;
--) --)
shift shift
RUN_COMMAND=("$@") RUN_COMMAND=("$@")
break break
;; ;;
--api-key) --api-key)
API_KEY="$2" API_KEY="$2"
GENERATE_API_KEY=0 GENERATE_API_KEY=0
shift 2 shift 2
;; ;;
--generate-api-key) --generate-api-key)
GENERATE_API_KEY=1 GENERATE_API_KEY=1
shift shift
;; ;;
--disable-api-key) --disable-api-key)
DISABLE_API_KEY=1 DISABLE_API_KEY=1
shift shift
;; ;;
--preload-langs) --preload-langs)
PRELOAD_LANGS="$2" PRELOAD_LANGS="$2"
shift 2 shift 2
;; ;;
--env) --env)
EXTRA_ENV+=("$2") EXTRA_ENV+=("$2")
shift 2 shift 2
;; ;;
--pull-only) --pull-only)
PULL_ONLY=1 PULL_ONLY=1
shift shift
;; ;;
--uninstall) --uninstall)
UNINSTALL=1 UNINSTALL=1
shift shift
;; ;;
--purge) --purge)
UNINSTALL=1 UNINSTALL=1
KEEP_DATA=0 KEEP_DATA=0
shift shift
;; ;;
--keep-data) --keep-data)
KEEP_DATA=1 KEEP_DATA=1
shift shift
;; ;;
--health-timeout) --health-timeout)
HEALTH_TIMEOUT="$2" HEALTH_TIMEOUT="$2"
shift 2 shift 2
;; ;;
--no-color) --no-color)
NO_COLOR=1 NO_COLOR=1
shift shift
;; ;;
--debug) --debug)
DEBUG=1 DEBUG=1
shift shift
;; ;;
-h | --help) -h | --help)
usage usage
exit 0 exit 0
;; ;;
-v | --version) -v | --version)
echo "${VERSION}" echo "${VERSION}"
exit 0 exit 0
;; ;;
*) *)
err "Unknown argument: $1" err "Unknown argument: $1"
usage usage
exit 1 exit 1
;; ;;
esac esac
done done
} }
ensure_root() { ensure_root() {
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
err "This script must run as root (or via sudo)." err "This script must run as root (or via sudo)."
exit 1 exit 1
fi fi
} }
install_docker() { install_docker() {
if command -v docker > /dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
log "Docker already installed" log "Docker already installed"
return 0 return 0
fi fi
if [[ ${DOCKER_INSTALL} -eq 0 ]]; then if [[ ${DOCKER_INSTALL} -eq 0 ]]; then
err "Docker is not installed and --no-docker-install specified." err "Docker is not installed and --no-docker-install specified."
exit 1 exit 1
fi fi
log "Installing Docker..." log "Installing Docker..."
if command -v apt-get > /dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update -y apt-get update -y
apt-get install -y ca-certificates curl gnupg apt-get install -y ca-certificates curl gnupg
install -d -m 0755 /etc/apt/keyrings install -d -m 0755 /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/$( curl -fsSL "https://download.docker.com/linux/$(
. /etc/os-release . /etc/os-release
echo "$ID" echo "$ID"
)/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg )/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg
echo \ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$( "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(
. /etc/os-release . /etc/os-release
echo "$ID" echo "$ID"
) $( ) $(
. /etc/os-release . /etc/os-release
echo "$VERSION_CODENAME" echo "$VERSION_CODENAME"
) stable" \ ) stable" \
> /etc/apt/sources.list.d/docker.list >/etc/apt/sources.list.d/docker.list
apt-get update -y apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else else
err "Unsupported package manager. Please install Docker manually." err "Unsupported package manager. Please install Docker manually."
exit 1 exit 1
fi fi
# Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user # Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user
if command -v systemctl > /dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
(systemctl enable --now docker 2> /dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" (systemctl enable --now docker 2>/dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running"
else else
warn "Docker installed; please ensure docker daemon is running" warn "Docker installed; please ensure docker daemon is running"
fi fi
} }
pull_image() { pull_image() {
log "Pulling image ${IMAGE}:${TAG}" log "Pulling image ${IMAGE}:${TAG}"
docker pull "${IMAGE}:${TAG}" docker pull "${IMAGE}:${TAG}"
success "Image pulled" success "Image pulled"
} }
detect_container_user() { detect_container_user() {
# Determine uid/gid of configured user inside image so host dirs can be chowned # Determine uid/gid of configured user inside image so host dirs can be chowned
if ! command -v docker > /dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
return 0 return 0
fi fi
local uid gid local uid gid
uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2> /dev/null || echo "") uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2>/dev/null || echo "")
gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2> /dev/null || echo "") gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2>/dev/null || echo "")
if [[ -n $uid && -n $gid ]]; then if [[ -n $uid && -n $gid ]]; then
CONTAINER_UID=$uid CONTAINER_UID=$uid
CONTAINER_GID=$gid CONTAINER_GID=$gid
fi fi
} }
write_env_file() { write_env_file() {
mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}" mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}"
detect_container_user detect_container_user
if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then
if command -v stat > /dev/null 2>&1; then if command -v stat >/dev/null 2>&1; then
for d in "${DATA_DIR}" "${CACHE_DIR}"; do for d in "${DATA_DIR}" "${CACHE_DIR}"; do
if [[ -d $d ]]; then if [[ -d $d ]]; then
CUR_UID=$(stat -c %u "$d" 2> /dev/null || echo -1) CUR_UID=$(stat -c %u "$d" 2>/dev/null || echo -1)
if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then
chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2> /dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}" chown "${CONTAINER_UID}":"${CONTAINER_GID}" "$d" 2>/dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}"
fi fi
fi fi
done done
fi fi
fi fi
if [[ ${DISABLE_API_KEY} -eq 1 ]]; then if [[ ${DISABLE_API_KEY} -eq 1 ]]; then
API_KEY_LINE="LT_NO_API_KEY=true" API_KEY_LINE="LT_NO_API_KEY=true"
else else
if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then
API_KEY=$(gen_api_key) API_KEY=$(gen_api_key)
GENERATED=1 GENERATED=1
else else
GENERATED=0 GENERATED=0
fi fi
API_KEY_LINE="LT_API_KEYS=${API_KEY}" API_KEY_LINE="LT_API_KEYS=${API_KEY}"
fi fi
{ {
echo "# LibreTranslate environment file" echo "# LibreTranslate environment file"
echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "${API_KEY_LINE}" echo "${API_KEY_LINE}"
[[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}" [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}"
for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done
} > "${ENV_FILE}.tmp" } >"${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}" mv "${ENV_FILE}.tmp" "${ENV_FILE}"
chmod 600 "${ENV_FILE}" chmod 600 "${ENV_FILE}"
success "Environment file written: ${ENV_FILE}" success "Environment file written: ${ENV_FILE}"
} }
start_container_ephemeral() { start_container_ephemeral() {
docker rm -f "${SERVICE_NAME}" > /dev/null 2>&1 || true docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1 || true
docker run -d --name "${SERVICE_NAME}" \ docker run -d --name "${SERVICE_NAME}" \
--env-file "${ENV_FILE}" \ --env-file "${ENV_FILE}" \
-v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \ -v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \
-v "${CACHE_DIR}:/app/cache" \ -v "${CACHE_DIR}:/app/cache" \
-p "${PORT}:${PORT}" \ -p "${PORT}:${PORT}" \
"${IMAGE}:${TAG}" \ "${IMAGE}:${TAG}" \
--host 0.0.0.0 --port "${PORT}" --host 0.0.0.0 --port "${PORT}"
success "Container started (ephemeral)" success "Container started (ephemeral)"
echo echo
echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}" echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "Waiting for health..." echo "Waiting for health..."
} }
health_check() { health_check() {
local start local start
start=$(date +%s) start=$(date +%s)
local url="http://127.0.0.1:${PORT}/languages" local url="http://127.0.0.1:${PORT}/languages"
local attempt=0 local attempt=0
while true; do while true; do
attempt=$((attempt + 1)) attempt=$((attempt + 1))
if curl ${DEBUG:+-v} -fsS "$url" > /dev/null 2>&1; then if curl ${DEBUG:+-v} -fsS "$url" >/dev/null 2>&1; then
success "Service healthy (attempt $attempt)" success "Service healthy (attempt $attempt)"
return 0 return 0
else else
[[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed"
fi fi
if (($(date +%s) - start > HEALTH_TIMEOUT)); then if (($(date +%s) - start > HEALTH_TIMEOUT)); then
err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)"
docker logs --tail 200 "${SERVICE_NAME}" || true docker logs --tail 200 "${SERVICE_NAME}" || true
return 1 return 1
fi fi
sleep 0.5 sleep 0.5
done done
} }
sample_request() { sample_request() {
if [[ ${DISABLE_API_KEY} -eq 0 ]]; then if [[ ${DISABLE_API_KEY} -eq 0 ]]; then
local key="${API_KEY}" local key="${API_KEY}"
else else
local key="" local key=""
fi fi
log "Performing sample translation (en->es)..." log "Performing sample translation (en->es)..."
local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}' local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}'
if [[ -n $key ]]; then if [[ -n $key ]]; then
curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed"
else else
curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed"
fi fi
echo echo
} }
uninstall_all() { uninstall_all() {
log "Uninstalling LibreTranslate (ephemeral mode)..." log "Uninstalling LibreTranslate (ephemeral mode)..."
docker rm -f "${SERVICE_NAME}" 2> /dev/null || true docker rm -f "${SERVICE_NAME}" 2>/dev/null || true
docker rmi "${IMAGE}:${TAG}" 2> /dev/null || true docker rmi "${IMAGE}:${TAG}" 2>/dev/null || true
if [[ ${KEEP_DATA} -eq 0 ]]; then if [[ ${KEEP_DATA} -eq 0 ]]; then
rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true
success "Data directories removed" success "Data directories removed"
else else
log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}" log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}"
fi fi
success "Uninstall complete" success "Uninstall complete"
exit 0 exit 0
} }
main() { main() {
parse_args "$@" parse_args "$@"
ensure_root ensure_root
if [[ ${UNINSTALL} -eq 1 ]]; then if [[ ${UNINSTALL} -eq 1 ]]; then
uninstall_all uninstall_all
fi fi
install_docker install_docker
pull_image pull_image
if [[ ${PULL_ONLY} -eq 1 ]]; then if [[ ${PULL_ONLY} -eq 1 ]]; then
log "Pull-only requested, exiting." log "Pull-only requested, exiting."
exit 0 exit 0
fi fi
write_env_file write_env_file
# Always ephemeral now # Always ephemeral now
start_container_ephemeral start_container_ephemeral
health_check health_check
sample_request || true sample_request || true
# If a command is provided, run it and then shutdown container # If a command is provided, run it and then shutdown container
if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then
log "Running user command: ${RUN_COMMAND[*]}" log "Running user command: ${RUN_COMMAND[*]}"
set +e set +e
"${RUN_COMMAND[@]}" "${RUN_COMMAND[@]}"
CMD_STATUS=$? CMD_STATUS=$?
set -e set -e
log "Command exited with status ${CMD_STATUS}; stopping container" log "Command exited with status ${CMD_STATUS}; stopping container"
docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true
exit ${CMD_STATUS} exit ${CMD_STATUS}
fi fi
if [[ ${KEEP_ALIVE} -eq 1 ]]; then if [[ ${KEEP_ALIVE} -eq 1 ]]; then
log "Tailing logs (Ctrl-C to stop and remove container)" log "Tailing logs (Ctrl-C to stop and remove container)"
trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM
docker logs -f "${SERVICE_NAME}" docker logs -f "${SERVICE_NAME}"
log "Logs ended; stopping container" log "Logs ended; stopping container"
docker stop "${SERVICE_NAME}" > /dev/null 2>&1 || true docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true
else else
log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2> /dev/null || echo unknown))" log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2>/dev/null || echo unknown))"
log "Stop manually with: docker stop ${SERVICE_NAME}" log "Stop manually with: docker stop ${SERVICE_NAME}"
fi fi
echo echo
echo "${BOLD}LibreTranslate is ready.${RESET}" echo "${BOLD}LibreTranslate is ready.${RESET}"
echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}" echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}"
if [[ ${DISABLE_API_KEY} -eq 0 ]]; then if [[ ${DISABLE_API_KEY} -eq 0 ]]; then
if [[ ${GENERATED:-0} -eq 1 ]]; then if [[ ${GENERATED:-0} -eq 1 ]]; then
echo "Generated API key: ${API_KEY}" echo "Generated API key: ${API_KEY}"
else else
echo "API key: ${API_KEY}" echo "API key: ${API_KEY}"
fi fi
echo "Use header: Authorization: <API_KEY>" echo "Use header: Authorization: <API_KEY>"
else else
echo "API key authentication DISABLED (public instance)." echo "API key authentication DISABLED (public instance)."
fi fi
[[ -n ${PRELOAD_LANGS} ]] && echo "Preloaded languages requested: ${PRELOAD_LANGS}" || true if [[ -n ${PRELOAD_LANGS} ]]; then
echo "Environment file: ${ENV_FILE}" echo "Preloaded languages requested: ${PRELOAD_LANGS}"
echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" fi
echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" echo "Environment file: ${ENV_FILE}"
echo echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}"
echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall"
echo
} }
main "$@" main "$@"

View File

@ -14,7 +14,7 @@ PY_RUNNER="$TOOLS_DIR/transcribe_fw.py"
VENV_DIR="$PROJECT_DIR/.venv" VENV_DIR="$PROJECT_DIR/.venv"
usage() { usage() {
cat << USAGE cat <<USAGE
Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file] Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file]
Options: Options:
@ -30,288 +30,292 @@ USAGE
} }
log() { log() {
echo "[$(date +'%H:%M:%S')]" "$@" echo "[$(date +'%H:%M:%S')]" "$@"
} }
detect_pkg_mgr() { detect_pkg_mgr() {
if command -v apt-get > /dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
echo apt echo apt
return return
fi fi
if command -v dnf > /dev/null 2>&1; then if command -v dnf >/dev/null 2>&1; then
echo dnf echo dnf
return return
fi fi
if command -v yum > /dev/null 2>&1; then if command -v yum >/dev/null 2>&1; then
echo yum echo yum
return return
fi fi
if command -v pacman > /dev/null 2>&1; then if command -v pacman >/dev/null 2>&1; then
echo pacman echo pacman
return return
fi fi
if command -v zypper > /dev/null 2>&1; then if command -v zypper >/dev/null 2>&1; then
echo zypper echo zypper
return return
fi fi
echo none echo none
} }
has_libcublas12() { has_libcublas12() {
# Common system locations # Common system locations
for d in \ for d in \
/usr/lib \ /usr/lib \
/usr/lib64 \ /usr/lib64 \
/usr/local/cuda/lib64 \ /usr/local/cuda/lib64 \
/usr/local/cuda-12*/lib64 \ /usr/local/cuda-12*/lib64 \
/opt/cuda/lib64 \ /opt/cuda/lib64 \
/opt/cuda/targets/x86_64-linux/lib; do /opt/cuda/targets/x86_64-linux/lib; do
[[ -e "$d/libcublas.so.12" ]] && return 0 || true if [[ -e "$d/libcublas.so.12" ]]; then
done return 0
# venv-provided NVIDIA CUDA libs fi
if [[ -x "$VENV_DIR/bin/python" ]]; then done
local pyver # venv-provided NVIDIA CUDA libs
pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2> /dev/null || true)" if [[ -x "$VENV_DIR/bin/python" ]]; then
if [[ -n $pyver ]]; then local pyver
for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)"
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \ if [[ -n $pyver ]]; then
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \
[[ -e "$d/libcublas.so.12" ]] && return 0 || true "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib" \
done "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do
fi if [[ -e "$d/libcublas.so.12" ]]; then
fi return 0
return 1 fi
done
fi
fi
return 1
} }
ensure_cuda_runtime() { ensure_cuda_runtime() {
local mgr local mgr
mgr="$(detect_pkg_mgr)" mgr="$(detect_pkg_mgr)"
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
if has_libcublas12; then return 0; fi if has_libcublas12; then return 0; fi
echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2
exit 6 exit 6
fi fi
if has_libcublas12; then if has_libcublas12; then
return 0 return 0
fi fi
if ! command -v sudo > /dev/null 2>&1; then if ! command -v sudo >/dev/null 2>&1; then
log "sudo not found; skipping CUDA runtime install attempt." log "sudo not found; skipping CUDA runtime install attempt."
else else
log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)"
set +e set +e
case "$mgr" in case "$mgr" in
pacman) pacman)
sudo pacman -Sy --noconfirm cuda cudnn || true sudo pacman -Sy --noconfirm cuda cudnn || true
;; ;;
apt) apt)
sudo apt-get update -y || true sudo apt-get update -y || true
sudo apt-get install -y nvidia-cuda-toolkit || true sudo apt-get install -y nvidia-cuda-toolkit || true
;; ;;
dnf | yum) dnf | yum)
sudo "$mgr" install -y cuda cudnn || true sudo "$mgr" install -y cuda cudnn || true
;; ;;
zypper) zypper)
sudo zypper install -y cuda cudnn || true sudo zypper install -y cuda cudnn || true
;; ;;
*) log "Unknown package manager; cannot install CUDA automatically." ;; *) log "Unknown package manager; cannot install CUDA automatically." ;;
esac esac
set -e set -e
fi fi
# Re-check # Re-check
if ! has_libcublas12; then if ! has_libcublas12; then
echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2
exit 6 exit 6
fi fi
} }
install_system_deps() { install_system_deps() {
have_cmd() { command -v "$1" > /dev/null 2>&1; } have_cmd() { command -v "$1" >/dev/null 2>&1; }
local need_ffmpeg=0 need_espeak=0 local need_ffmpeg=0 need_espeak=0
have_cmd ffmpeg || need_ffmpeg=1 have_cmd ffmpeg || need_ffmpeg=1
have_cmd espeak-ng || need_espeak=1 have_cmd espeak-ng || need_espeak=1
# If diarization requested and online, we may also try to ensure libsndfile # If diarization requested and online, we may also try to ensure libsndfile
local need_libsndfile=0 local need_libsndfile=0
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
# Heuristic: check common library file # Heuristic: check common library file
if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then if [[ ! -e /usr/lib/x86_64-linux-gnu/libsndfile.so && ! -e /usr/lib/libsndfile.so && ! -e /usr/lib64/libsndfile.so ]]; then
need_libsndfile=1 need_libsndfile=1
fi fi
fi fi
if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then if [[ $need_ffmpeg -eq 0 && $need_espeak -eq 0 && $need_libsndfile -eq 0 ]]; then
log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}" log "System deps present: ffmpeg, espeak-ng${FW_DIARIZE:+, libsndfile}"
return 0 return 0
fi fi
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2 echo "Missing system dependencies (ffmpeg/espeak-ng) but running in offline mode. Install them or rerun with --online." >&2
exit 5 exit 5
fi fi
local mgr local mgr
mgr="$(detect_pkg_mgr)" mgr="$(detect_pkg_mgr)"
log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))" log "Detected package manager: $mgr (installing missing: $([[ $need_ffmpeg -eq 1 ]] && echo ffmpeg)$([[ $need_espeak -eq 1 ]] && echo espeak-ng)$([[ $need_libsndfile -eq 1 ]] && echo libsndfile))"
if ! command -v sudo > /dev/null 2>&1; then if ! command -v sudo >/dev/null 2>&1; then
log "sudo not found; skipping system package installation attempt." log "sudo not found; skipping system package installation attempt."
return 0 return 0
fi fi
# Avoid exiting on install errors; continue best-effort # Avoid exiting on install errors; continue best-effort
set +e set +e
case "$mgr" in case "$mgr" in
apt) apt)
sudo apt-get update -y || log "apt-get update failed; continuing" sudo apt-get update -y || log "apt-get update failed; continuing"
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
if [[ $need_libsndfile -eq 1 ]]; then if [[ $need_libsndfile -eq 1 ]]; then
# Try both names across releases # Try both names across releases
pkgs+=(libsndfile1) pkgs+=(libsndfile1)
sudo apt-get install -y libsndfile1 || true sudo apt-get install -y libsndfile1 || true
# If that failed, try libsndfile2 (newer distros) # If that failed, try libsndfile2 (newer distros)
sudo apt-get install -y libsndfile2 || true sudo apt-get install -y libsndfile2 || true
fi fi
sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing" sudo apt-get install -y "${pkgs[@]}" || log "apt-get install failed; continuing"
;; ;;
dnf) dnf)
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing"
;; ;;
yum) yum)
pkgs=(python3-venv python3-pip) pkgs=(python3-venv python3-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing"
;; ;;
pacman) pacman)
pkgs=(python-virtualenv python-pip) pkgs=(python-virtualenv python-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing"
;; ;;
zypper) zypper)
pkgs=(python311-virtualenv python311-pip) pkgs=(python311-virtualenv python311-pip)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1)
sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing"
;; ;;
*) *)
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed."
;; ;;
esac esac
set -e set -e
} }
setup_venv() { setup_venv() {
if [[ ! -d $VENV_DIR ]]; then if [[ ! -d $VENV_DIR ]]; then
log "Creating venv at $VENV_DIR" log "Creating venv at $VENV_DIR"
python3 -m venv "$VENV_DIR" python3 -m venv "$VENV_DIR"
fi fi
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
if [[ $OFFLINE -eq 0 ]]; then if [[ $OFFLINE -eq 0 ]]; then
python -m pip install --upgrade pip wheel setuptools python -m pip install --upgrade pip wheel setuptools
fi fi
} }
install_python_deps() { install_python_deps() {
# Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12) # Install deps; if NVIDIA GPU is present, prefer CUDA-capable stack (cu12)
local has_nvidia_flag="${1:-0}" local has_nvidia_flag="${1:-0}"
log "Installing faster-whisper and dependencies" log "Installing faster-whisper and dependencies"
export PIP_DISABLE_PIP_VERSION_CHECK=1 export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20} export PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT:-20}
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
# Offline: do not install, just verify modules # Offline: do not install, just verify modules
if ! python -c 'import faster_whisper' > /dev/null 2>&1; then if ! python -c 'import faster_whisper' >/dev/null 2>&1; then
echo "Python dependency 'faster_whisper' not found in offline mode. Run with --online to install." >&2 echo "Python dependency 'faster_whisper' not found in offline mode. Run with --online to install." >&2
exit 7 exit 7
fi fi
# If diarization requested offline, check for its deps too (warn-only) # If diarization requested offline, check for its deps too (warn-only)
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
python - << 'PY' || true python - <<'PY' || true
try: try:
import soundfile, speechbrain, torch # noqa: F401 import soundfile, speechbrain, torch # noqa: F401
except Exception as e: except Exception as e:
print(f"[WARN] Diarization deps missing offline ({e}); speaker labels will be skipped.") print(f"[WARN] Diarization deps missing offline ({e}); speaker labels will be skipped.")
PY PY
fi fi
return 0 return 0
fi fi
if [[ $has_nvidia_flag -eq 1 ]]; then if [[ $has_nvidia_flag -eq 1 ]]; then
# If ctranslate2 is not installed, attempt CUDA-enabled wheel (quiet, with fallback) # If ctranslate2 is not installed, attempt CUDA-enabled wheel (quiet, with fallback)
if ! "$VENV_DIR/bin/python" -c 'import ctranslate2' > /dev/null 2>&1; then if ! "$VENV_DIR/bin/python" -c 'import ctranslate2' >/dev/null 2>&1; then
log "Installing CUDA-enabled CTranslate2 (cu12 wheel)" log "Installing CUDA-enabled CTranslate2 (cu12 wheel)"
python -m pip install -q --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 || python -m pip install -q --retries 1 --upgrade "ctranslate2<5,>=4.0" --extra-index-url https://download.opennmt.net/ctranslate2/cu12 ||
log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2" log "Warning: could not reach cu12 wheel index; will proceed with available ctranslate2"
fi fi
# Ensure NVIDIA CUDA 12 runtime libs are available inside the venv # Ensure NVIDIA CUDA 12 runtime libs are available inside the venv
python -m pip install -q --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 || python -m pip install -q --retries 1 --upgrade nvidia-cublas-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 ||
log "Warning: failed to install NVIDIA cu12 runtime libs via pip" log "Warning: failed to install NVIDIA cu12 runtime libs via pip"
fi fi
python -m pip install -q --retries 1 --upgrade faster-whisper ffmpeg-python python -m pip install -q --retries 1 --upgrade faster-whisper ffmpeg-python
# If diarization requested and online, install its Python deps best-effort # If diarization requested and online, install its Python deps best-effort
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
python -m pip install -q --retries 1 --upgrade soundfile speechbrain || python -m pip install -q --retries 1 --upgrade soundfile speechbrain ||
log "Warning: failed to install soundfile/speechbrain" log "Warning: failed to install soundfile/speechbrain"
# Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds) # Torch and torchaudio CPU wheels (force to avoid mismatched CUDA builds)
python -m pip install -q --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio || python -m pip install -q --retries 1 --upgrade --force-reinstall --index-url https://download.pytorch.org/whl/cpu torch torchaudio ||
log "Warning: failed to install torch/torchaudio CPU wheels" log "Warning: failed to install torch/torchaudio CPU wheels"
fi fi
python - << 'PY' python - <<'PY'
import sys import sys
print(f"[PY] Python {sys.version.split()[0]} dependencies installed.") print(f"[PY] Python {sys.version.split()[0]} dependencies installed.")
PY PY
} }
ensure_runner() { ensure_runner() {
if [[ ! -f $PY_RUNNER ]]; then if [[ ! -f $PY_RUNNER ]]; then
echo "Runner not found: $PY_RUNNER" >&2 echo "Runner not found: $PY_RUNNER" >&2
exit 3 exit 3
fi fi
} }
generate_test_audio() { generate_test_audio() {
local tmpwav local tmpwav
tmpwav="${PROJECT_DIR}/test_fw.wav" tmpwav="${PROJECT_DIR}/test_fw.wav"
if command -v espeak-ng > /dev/null 2>&1; then if command -v espeak-ng >/dev/null 2>&1; then
log "Generating test audio via espeak-ng -> $tmpwav" >&2 log "Generating test audio via espeak-ng -> $tmpwav" >&2
espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true espeak-ng -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true
fi fi
# If espeak-ng failed or not present, try espeak # If espeak-ng failed or not present, try espeak
if [[ ! -s $tmpwav ]] && command -v espeak > /dev/null 2>&1; then if [[ ! -s $tmpwav ]] && command -v espeak >/dev/null 2>&1; then
log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2 log "espeak-ng unavailable or failed; trying espeak -> $tmpwav" >&2
espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." > /dev/null 2>&1 || true espeak -w "$tmpwav" "This is a quick test of faster whisper transcription." >/dev/null 2>&1 || true
fi fi
# Fallback: generate tone via Python stdlib (no external deps) # Fallback: generate tone via Python stdlib (no external deps)
if [[ ! -s $tmpwav ]]; then if [[ ! -s $tmpwav ]]; then
log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2 log "Generating 3s 1kHz WAV via Python stdlib -> $tmpwav" >&2
python3 -c 'import sys,wave,math,array;outfile=sys.argv[1];fr=16000;dur=3;freq=1000.0;ampl=0.3;n=fr*dur;data=array.array("h",[int(max(-1.0,min(1.0,ampl*math.sin(2*math.pi*freq*(i/fr))))*32767) for i in range(n)]);wf=wave.open(outfile,"w");wf.setnchannels(1);wf.setsampwidth(2);wf.setframerate(fr);wf.writeframes(data.tobytes());wf.close()' "$tmpwav" || true python3 -c 'import sys,wave,math,array;outfile=sys.argv[1];fr=16000;dur=3;freq=1000.0;ampl=0.3;n=fr*dur;data=array.array("h",[int(max(-1.0,min(1.0,ampl*math.sin(2*math.pi*freq*(i/fr))))*32767) for i in range(n)]);wf=wave.open(outfile,"w");wf.setnchannels(1);wf.setsampwidth(2);wf.setframerate(fr);wf.writeframes(data.tobytes());wf.close()' "$tmpwav" || true
fi fi
# Final fallback: tone via ffmpeg # Final fallback: tone via ffmpeg
if [[ ! -s $tmpwav ]]; then if [[ ! -s $tmpwav ]]; then
log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2 log "Creating a 3s sine tone WAV via ffmpeg -> $tmpwav" >&2
ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" > /dev/null 2>&1 || true ffmpeg -f lavfi -i sine=frequency=1000:duration=3 -ar 16000 -ac 1 -f wav -y "$tmpwav" >/dev/null 2>&1 || true
fi fi
echo "$tmpwav" echo "$tmpwav"
} }
prepare_model() { prepare_model() {
# Download a model for offline use into MODEL_DIR # Download a model for offline use into MODEL_DIR
local name="$1" local name="$1"
mkdir -p "$MODEL_DIR" mkdir -p "$MODEL_DIR"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
log "Preparing model '$name' into $MODEL_DIR" log "Preparing model '$name' into $MODEL_DIR"
python - << PY python - <<PY
import sys, os import sys, os
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
name = os.environ.get('FW_PREPARE_NAME') name = os.environ.get('FW_PREPARE_NAME')
@ -323,165 +327,165 @@ PY
} }
main() { main() {
# Defaults # Defaults
OFFLINE=1 OFFLINE=1
PREPARE_MODEL="" PREPARE_MODEL=""
MODEL_DIR="$PROJECT_DIR/models" MODEL_DIR="$PROJECT_DIR/models"
MODEL="large-v3" MODEL="large-v3"
LANGUAGE="" LANGUAGE=""
OUTDIR="" OUTDIR=""
INPUT_FILE="" INPUT_FILE=""
# Parse args # Parse args
PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || { PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || {
usage usage
exit 2 exit 2
} }
eval set -- "$PARSED" eval set -- "$PARSED"
while true; do while true; do
case "$1" in case "$1" in
-m) -m)
MODEL="$2" MODEL="$2"
shift 2 shift 2
;; ;;
-l) -l)
LANGUAGE="$2" LANGUAGE="$2"
shift 2 shift 2
;; ;;
-o) -o)
OUTDIR="$2" OUTDIR="$2"
shift 2 shift 2
;; ;;
-h) -h)
usage usage
exit 0 exit 0
;; ;;
--online) --online)
OFFLINE=0 OFFLINE=0
shift shift
;; ;;
--prepare-model) --prepare-model)
PREPARE_MODEL="$2" PREPARE_MODEL="$2"
OFFLINE=0 OFFLINE=0
shift 2 shift 2
;; ;;
--model-dir) --model-dir)
MODEL_DIR="$2" MODEL_DIR="$2"
shift 2 shift 2
;; ;;
--) --)
shift shift
break break
;; ;;
*) break ;; *) break ;;
esac esac
done done
INPUT_FILE="${1:-}" INPUT_FILE="${1:-}"
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
export HF_HUB_OFFLINE=1 export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1 export TRANSFORMERS_OFFLINE=1
fi fi
install_system_deps install_system_deps
setup_venv setup_venv
# If asked to prepare a model, do that and exit # If asked to prepare a model, do that and exit
if [[ -n $PREPARE_MODEL ]]; then if [[ -n $PREPARE_MODEL ]]; then
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
echo "--prepare-model requires network; rerun with --online." >&2 echo "--prepare-model requires network; rerun with --online." >&2
exit 2 exit 2
fi fi
install_python_deps 0 install_python_deps 0
export FW_PREPARE_NAME="$PREPARE_MODEL" export FW_PREPARE_NAME="$PREPARE_MODEL"
export FW_MODEL_DIR="$MODEL_DIR" export FW_MODEL_DIR="$MODEL_DIR"
prepare_model "$PREPARE_MODEL" prepare_model "$PREPARE_MODEL"
log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR" log "Model '$PREPARE_MODEL' downloaded to $MODEL_DIR"
exit 0 exit 0
fi fi
# Detect NVIDIA GPU and enforce CUDA if present # Detect NVIDIA GPU and enforce CUDA if present
has_nvidia=0 has_nvidia=0
if command -v nvidia-smi > /dev/null 2>&1 && nvidia-smi -L > /dev/null 2>&1; then if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
has_nvidia=1 has_nvidia=1
fi fi
install_python_deps "$has_nvidia" install_python_deps "$has_nvidia"
ensure_runner ensure_runner
local input="$INPUT_FILE" local input="$INPUT_FILE"
if [[ -z $input ]]; then if [[ -z $input ]]; then
input="$(generate_test_audio)" input="$(generate_test_audio)"
if [[ ! -s $input ]]; then if [[ ! -s $input ]]; then
echo "Failed to generate test audio. Please provide an audio file." >&2 echo "Failed to generate test audio. Please provide an audio file." >&2
exit 4 exit 4
fi fi
fi fi
if [[ ! -f $input ]]; then if [[ ! -f $input ]]; then
echo "Input file not found: $input" >&2 echo "Input file not found: $input" >&2
exit 2 exit 2
fi fi
local args=("$input" "--model" "$MODEL") local args=("$input" "--model" "$MODEL")
[[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE")
[[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR")
# Pass diarization via env if requested # Pass diarization via env if requested
if [[ ${FW_DIARIZE:-} == "1" ]]; then if [[ ${FW_DIARIZE:-} == "1" ]]; then
args+=("--diarize") args+=("--diarize")
if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then if [[ -n ${FW_NUM_SPEAKERS:-} ]]; then
args+=("--num-speakers" "${FW_NUM_SPEAKERS}") args+=("--num-speakers" "${FW_NUM_SPEAKERS}")
fi fi
fi fi
if [[ $has_nvidia -eq 1 ]]; then if [[ $has_nvidia -eq 1 ]]; then
ensure_cuda_runtime ensure_cuda_runtime
# Export common CUDA paths in case the env lacks them # Export common CUDA paths in case the env lacks them
export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}" export CUDA_HOME="${CUDA_HOME:-/usr/local/cuda}"
# Include system and possible venv-provided CUDA libs # Include system and possible venv-provided CUDA libs
local pyver venv_cuda_paths="" local pyver venv_cuda_paths=""
if [[ -x "$VENV_DIR/bin/python" ]]; then if [[ -x "$VENV_DIR/bin/python" ]]; then
pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2> /dev/null || true)" pyver="$("$VENV_DIR"/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)"
if [[ -n $pyver ]]; then if [[ -n $pyver ]]; then
venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib" venv_cuda_paths="$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cudnn/lib:$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"
fi fi
fi fi
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}" export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}:${CUDA_HOME}/lib64:/usr/lib/x86_64-linux-gnu:/opt/cuda/lib64:/opt/cuda/targets/x86_64-linux/lib:${venv_cuda_paths}"
export PATH="${PATH}:${CUDA_HOME}/bin" export PATH="${PATH}:${CUDA_HOME}/bin"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny", device="cuda", compute_type="float16"); print("[PY] CUDA test init succeeded.")' || { python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny", device="cuda", compute_type="float16"); print("[PY] CUDA test init succeeded.")' || {
echo "CUDA environment check failed. Aborting as requested." >&2 echo "CUDA environment check failed. Aborting as requested." >&2
exit 6 exit 6
} }
args+=("--device" "cuda") args+=("--device" "cuda")
fi fi
log "Transcribing: $input" log "Transcribing: $input"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$VENV_DIR/bin/activate" source "$VENV_DIR/bin/activate"
if [[ $has_nvidia -eq 1 ]]; then if [[ $has_nvidia -eq 1 ]]; then
if ! python "$PY_RUNNER" "${args[@]}"; then if ! python "$PY_RUNNER" "${args[@]}"; then
echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2 echo "CUDA execution requested due to detected NVIDIA GPU, but it failed. Aborting as requested (no CPU fallback)." >&2
exit 6 exit 6
fi fi
else else
# Offline: prefer local directory if present; otherwise use cache without network # Offline: prefer local directory if present; otherwise use cache without network
if [[ $OFFLINE -eq 1 ]]; then if [[ $OFFLINE -eq 1 ]]; then
local local_model_path="" local local_model_path=""
if [[ -d $MODEL ]]; then if [[ -d $MODEL ]]; then
local_model_path="$MODEL" local_model_path="$MODEL"
elif [[ -d "$MODEL_DIR/$MODEL" ]]; then elif [[ -d "$MODEL_DIR/$MODEL" ]]; then
local_model_path="$MODEL_DIR/$MODEL" local_model_path="$MODEL_DIR/$MODEL"
fi fi
if [[ -n $local_model_path ]]; then if [[ -n $local_model_path ]]; then
args=("$input" "--model" "$local_model_path") args=("$input" "--model" "$local_model_path")
[[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE") [[ -n $LANGUAGE ]] && args+=("--language" "$LANGUAGE")
[[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR") [[ -n $OUTDIR ]] && args+=("--outdir" "$OUTDIR")
fi fi
fi fi
python "$PY_RUNNER" "${args[@]}" python "$PY_RUNNER" "${args[@]}"
fi fi
} }
main "$@" main "$@"