mirror of
https://github.com/kuhyx/scripts.git
synced 2026-07-04 13:03:05 +02:00
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:
parent
8e0a720499
commit
03bd36e41d
@ -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
|
||||||
|
|||||||
@ -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}"
|
||||||
|
|
||||||
|
|||||||
@ -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
@ -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 "$@"
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user