chore: fix shell check issues

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-06 19:39:04 +01:00
parent f11ce4002f
commit 6d78878b99
16 changed files with 1374 additions and 1099 deletions

View File

@ -40,3 +40,4 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. Its pri
- Follow the sudo re-exec + idempotent install pattern from `setup_periodic_system.sh` and `hosts/guard/setup_hosts_guard.sh`. - Follow the sudo re-exec + idempotent install pattern from `setup_periodic_system.sh` and `hosts/guard/setup_hosts_guard.sh`.
- Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them. - Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them.
- Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper. - Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper.
- Run `scripts/meta/shell_check.sh` to detect things to fix before committing.

View File

@ -16,169 +16,181 @@ LOG_FILE="/var/log/xbox-controller-fix.log"
timestamp() { date '+%Y-%m-%d %H:%M:%S%z'; } timestamp() { date '+%Y-%m-%d %H:%M:%S%z'; }
log() { log() {
local msg="$1" local msg="$1"
echo "[$(timestamp)] $msg" echo "[$(timestamp)] $msg"
if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e "$LOG_FILE" && -w /var/log ]]; then if [[ -w "$(dirname "$LOG_FILE")" ]] || [[ ! -e $LOG_FILE && -w /var/log ]]; then
echo "[$(timestamp)] $msg" >>"$LOG_FILE" || true echo "[$(timestamp)] $msg" >> "$LOG_FILE" || true
fi fi
} }
require_root() { require_root() {
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "$SCRIPT_NAME needs root to load kernel modules and read some diagnostics. Re-executing with sudo..." echo "$SCRIPT_NAME needs root to load kernel modules and read some diagnostics. Re-executing with sudo..."
exec sudo -E bash "$0" "$@" exec sudo -E bash "$0" "$@"
fi fi
} }
print_header() { print_header() {
echo "=== $1 ===" echo "=== $1 ==="
} }
detect_distro() { detect_distro() {
if command -v pacman >/dev/null 2>&1; then if command -v pacman > /dev/null 2>&1; then
echo "arch" echo "arch"
else else
echo "other" echo "other"
fi fi
} }
list_input_nodes() { list_input_nodes() {
print_header "Input device nodes" print_header "Input device nodes"
ls -l /dev/input/by-id 2>/dev/null | sed -n '1,120p' || true if [[ -d /dev/input/by-id ]]; then
echo # Robust listing with proper handling of special characters
if compgen -G "/dev/input/*js*" >/dev/null; then local count=0
ls -l /dev/input/js* || true while IFS= read -r -d '' f; do
else stat -c '%A %a %U:%G %N' "$f" 2> /dev/null || true
echo "No legacy /dev/input/js* nodes (joydev) present. That's okay for most apps using evdev." count=$((count + 1))
fi [[ $count -ge 120 ]] && break
echo done < <(find /dev/input/by-id -maxdepth 1 -mindepth 1 -print0 2> /dev/null)
else
echo "/dev/input/by-id not present"
fi
echo
if compgen -G "/dev/input/*js*" > /dev/null; then
ls -l /dev/input/js* || true
else
echo "No legacy /dev/input/js* nodes (joydev) present. That's okay for most apps using evdev."
fi
echo
} }
show_lsusb() { show_lsusb() {
print_header "USB devices (filtered)" print_header "USB devices (filtered)"
if command -v lsusb >/dev/null 2>&1; then if command -v lsusb > /dev/null 2>&1; then
lsusb | grep -Ei 'microsoft|xbox|045e:' || { echo "No Microsoft/Xbox device found via lsusb."; true; } lsusb | grep -Ei 'microsoft|xbox|045e:' || {
else echo "No Microsoft/Xbox device found via lsusb."
echo "lsusb not found (usbutils). Install usbutils for richer diagnostics." true
fi }
echo else
echo "lsusb not found (usbutils). Install usbutils for richer diagnostics."
fi
echo
} }
show_modules() { show_modules() {
print_header "Kernel modules state" print_header "Kernel modules state"
lsmod | grep -E '(^|\s)(xpad|joydev|hid_microsoft|hid_generic|hid_xpadneo|xone)(\s|$)' || echo "No matching modules currently loaded." lsmod | grep -E '(^|\s)(xpad|joydev|hid_microsoft|hid_generic|hid_xpadneo|xone)(\s|$)' || echo "No matching modules currently loaded."
echo echo
} }
modprobe_safe() { modprobe_safe() {
local mod="$1" local mod="$1"
if ! lsmod | grep -q "^${mod}\b"; then if ! lsmod | grep -q "^${mod}\b"; then
if modprobe "$mod" 2>/dev/null; then if modprobe "$mod" 2> /dev/null; then
log "Loaded module: $mod" log "Loaded module: $mod"
else else
log "Module $mod not loaded (may be built-in or unavailable)." log "Module $mod not loaded (may be built-in or unavailable)."
fi fi
fi fi
} }
show_dmesg_hints() { show_dmesg_hints() {
print_header "Recent kernel messages (xpad/xbox/hid/input)" print_header "Recent kernel messages (xpad/xbox/hid/input)"
dmesg --color=never | grep -Ei 'xbox|xpad|045e:|Microsoft|input:.*gamepad|event.*joystick|hid.*(xbox|microsoft)' | tail -n 200 || true dmesg --color=never | grep -Ei 'xbox|xpad|045e:|Microsoft|input:.*gamepad|event.*joystick|hid.*(xbox|microsoft)' | tail -n 200 || true
echo echo
} }
check_permissions() { check_permissions() {
print_header "Permissions on event/joystick nodes" print_header "Permissions on event/joystick nodes"
local any=0 local any=0
for path in /dev/input/by-id/*-event-joystick /dev/input/js*; do for path in /dev/input/by-id/*-event-joystick /dev/input/js*; do
if [[ -e "$path" ]]; then if [[ -e $path ]]; then
any=1 any=1
printf '%s -> ' "$path" printf '%s -> ' "$path"
local dev local dev
dev=$(readlink -f "$path" 2>/dev/null || echo "$path") dev=$(readlink -f "$path" 2> /dev/null || echo "$path")
stat -c '%A %a %U:%G %n' "$dev" 2>/dev/null || true stat -c '%A %a %U:%G %n' "$dev" 2> /dev/null || true
fi fi
done done
if [[ $any -eq 0 ]]; then if [[ $any -eq 0 ]]; then
echo "No event-joystick or js nodes found to check permissions." echo "No event-joystick or js nodes found to check permissions."
fi fi
echo echo
if [[ $(detect_distro) == "arch" ]]; then if [[ $(detect_distro) == "arch" ]]; then
echo "On Arch, prefer TAG+\"uaccess\"-based access over adding users to the 'input' group." echo "On Arch, prefer TAG+\"uaccess\"-based access over adding users to the 'input' group."
echo "If access is denied in apps, install: pacman -S game-devices-udev (provides modern udev rules)." echo "If access is denied in apps, install: pacman -S game-devices-udev (provides modern udev rules)."
fi fi
echo echo
} }
suggest_tests() { suggest_tests() {
print_header "Next steps / tests" print_header "Next steps / tests"
echo "- Test evdev: install 'evtest' and run: evtest /dev/input/by-id/*-event-joystick" echo "- Test evdev: install 'evtest' and run: evtest /dev/input/by-id/*-event-joystick"
echo "- Test joystick API: install 'joystick' (jstest) and run: jstest /dev/input/js0 (if present)" echo "- Test joystick API: install 'joystick' (jstest) and run: jstest /dev/input/js0 (if present)"
echo "- For force feedback test (rumble): install 'linuxconsole' (fftest): fftest /dev/input/by-id/*-event-joystick" echo "- For force feedback test (rumble): install 'linuxconsole' (fftest): fftest /dev/input/by-id/*-event-joystick"
echo echo
echo "Steam users: Ensure Steam Input settings match your use case. If rumble fails in SDL titles, try: SDL_JOYSTICK_HIDAPI=0" echo "Steam users: Ensure Steam Input settings match your use case. If rumble fails in SDL titles, try: SDL_JOYSTICK_HIDAPI=0"
echo echo
echo "If you are actually using Bluetooth: consider xpadneo (AUR: xpadneo-dkms)." echo "If you are actually using Bluetooth: consider xpadneo (AUR: xpadneo-dkms)."
echo "If you are using the official wireless USB adapter: consider xone (AUR: xone-dkms and xone-dongle-firmware)." echo "If you are using the official wireless USB adapter: consider xone (AUR: xone-dkms and xone-dongle-firmware)."
echo echo
} }
main() { main() {
require_root "$@" require_root "$@"
print_header "${SCRIPT_NAME} starting" print_header "${SCRIPT_NAME} starting"
log "Kernel: $(uname -r) | Distro: $(detect_distro)" log "Kernel: $(uname -r) | Distro: $(detect_distro)"
show_lsusb show_lsusb
show_modules show_modules
# Load common modules safely (idempotent) # Load common modules safely (idempotent)
modprobe_safe usbhid modprobe_safe usbhid
modprobe_safe xpad modprobe_safe xpad
modprobe_safe joydev modprobe_safe joydev
# If xpad failed to load and kernel says it's a module, but it's not present, hint about out-of-sync modules # If xpad failed to load and kernel says it's a module, but it's not present, hint about out-of-sync modules
if ! lsmod | grep -q '^xpad\b'; then if ! lsmod | grep -q '^xpad\b'; then
if command -v zcat >/dev/null 2>&1 && [[ -r /proc/config.gz ]] && zcat /proc/config.gz 2>/dev/null | grep -q '^CONFIG_JOYSTICK_XPAD=m'; then if command -v zcat > /dev/null 2>&1 && [[ -r /proc/config.gz ]] && zcat /proc/config.gz 2> /dev/null | grep -q '^CONFIG_JOYSTICK_XPAD=m'; then
if ! find "/lib/modules/$(uname -r)" -type f -name 'xpad*.ko*' 2>/dev/null | grep -q .; then if ! find "/lib/modules/$(uname -r)" -type f -name 'xpad*.ko*' 2> /dev/null | grep -q .; then
log "xpad is configured as a module but missing under /lib/modules/$(uname -r). Your kernel modules may be out-of-sync or incomplete." log "xpad is configured as a module but missing under /lib/modules/$(uname -r). Your kernel modules may be out-of-sync or incomplete."
if [[ $(detect_distro) == "arch" ]]; then if [[ $(detect_distro) == "arch" ]]; then
echo "Arch hint: reinstall the matching kernel package (e.g. 'sudo pacman -S linux' or your variant like linux-zen) and reboot." echo "Arch hint: reinstall the matching kernel package (e.g. 'sudo pacman -S linux' or your variant like linux-zen) and reboot."
else else
echo "Hint: reinstall your running kernel's modules then reboot." echo "Hint: reinstall your running kernel's modules then reboot."
fi fi
echo echo
fi fi
fi fi
fi fi
list_input_nodes list_input_nodes
check_permissions check_permissions
show_dmesg_hints show_dmesg_hints
# Simple heuristic: do we see an Xbox/Microsoft event-joystick? # Simple heuristic: do we see an Xbox/Microsoft event-joystick?
if compgen -G "/dev/input/by-id/*-event-joystick" >/dev/null; then if compgen -G "/dev/input/by-id/*-event-joystick" > /dev/null; then
local found_label=0 local found_label=0
for f in /dev/input/by-id/*-event-joystick; do for f in /dev/input/by-id/*-event-joystick; do
[[ -e "$f" ]] || continue [[ -e $f ]] || continue
if printf '%s' "$(basename "$f")" | grep -Eqi 'xbox|microsoft|controller|wireless'; then if printf '%s' "$(basename "$f")" | grep -Eqi 'xbox|microsoft|controller|wireless'; then
found_label=1 found_label=1
break break
fi fi
done done
if (( found_label == 1 )); then if ((found_label == 1)); then
log "Controller event device detected." log "Controller event device detected."
else else
log "Event-joystick device(s) exist but not obviously Xbox-labelled. Still likely usable." log "Event-joystick device(s) exist but not obviously Xbox-labelled. Still likely usable."
fi fi
else else
log "No -event-joystick device found. If the controller vibrated but no input node exists, check the cable and try another USB port/cable." log "No -event-joystick device found. If the controller vibrated but no input node exists, check the cable and try another USB port/cable."
log "Also check dmesg for descriptor errors; for Xbox 360 Play&Charge cable: note it only charges and does not carry input." log "Also check dmesg for descriptor errors; for Xbox 360 Play&Charge cable: note it only charges and does not carry input."
fi fi
suggest_tests suggest_tests
print_header "Done" print_header "Done"
} }
main "$@" main "$@"

View File

@ -285,8 +285,24 @@ run_linters() {
local cbi_out="$TMPDIR/checkbashisms.txt" local cbi_out="$TMPDIR/checkbashisms.txt"
local cbi_status=0 local cbi_status=0
if is_cmd checkbashisms; then if is_cmd checkbashisms; then
# checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings # Only run checkbashisms on scripts that are intended for /bin/sh (or unspecified),
checkbashisms "${FILES[@]}" > "$cbi_out" 2>&1 # skip explicit bash/zsh scripts to avoid false positives.
local -a CBI_FILES
CBI_FILES=()
for f in "${FILES[@]}"; do
local first
first=$(head -n 1 -- "$f" 2> /dev/null || true)
if [[ $first =~ bash || $first =~ zsh ]]; then
continue
fi
CBI_FILES+=("$f")
done
if [[ ${#CBI_FILES[@]} -gt 0 ]]; then
# checkbashisms exits 0 if OK, 1 if issues, other codes for tool warnings
checkbashisms "${CBI_FILES[@]}" > "$cbi_out" 2>&1
else
: > "$cbi_out"
fi
cbi_status=$? cbi_status=$?
if [[ $cbi_status -eq 1 ]]; then if [[ $cbi_status -eq 1 ]]; then
issues=$((issues + 1)) issues=$((issues + 1))

View File

@ -22,10 +22,10 @@ set -euo pipefail
# Bash/clean_audio.sh input.wav --preset podcast # -> add dynamics leveler # Bash/clean_audio.sh input.wav --preset podcast # -> add dynamics leveler
# #
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
print_usage() { print_usage() {
cat <<EOF cat << EOF
Usage: $0 <input-file|input-dir> [options] Usage: $0 <input-file|input-dir> [options]
Options: Options:
@ -49,7 +49,7 @@ EOF
} }
require_cmd() { require_cmd() {
command -v "$1" >/dev/null 2>&1 || { command -v "$1" > /dev/null 2>&1 || {
echo "Error: Required command '$1' not found in PATH" >&2 echo "Error: Required command '$1' not found in PATH" >&2
exit 1 exit 1
} }
@ -60,7 +60,7 @@ OUT_DIR=""
OUT_EXT="wav" OUT_EXT="wav"
RN_MODEL="" RN_MODEL=""
NO_ML=false NO_ML=false
REQUIRE_ML=true # default: require RNNoise; install/guide if missing; fail fast if unavailable REQUIRE_ML=true # default: require RNNoise; install/guide if missing; fail fast if unavailable
PRESET="asr" PRESET="asr"
JOBS=1 JOBS=1
FORCE=false FORCE=false
@ -68,9 +68,9 @@ QUIET=false
LOWPASS="" LOWPASS=""
SUFFIX="_clean" SUFFIX="_clean"
HIGHPASS="80" HIGHPASS="80"
AFFTDN_NF="-25" # noise floor in dB for afftdn AFFTDN_NF="-25" # noise floor in dB for afftdn
AFFTDN_MD="8" # mode for afftdn (higher can be more aggressive); requires builds that support 'md' AFFTDN_MD="8" # mode for afftdn (higher can be more aggressive); requires builds that support 'md'
NO_ADVANCED=false # when true, avoid advanced options that some ffmpeg builds lack NO_ADVANCED=false # when true, avoid advanced options that some ffmpeg builds lack
# Parse args # Parse args
if [[ $# -lt 1 ]]; then if [[ $# -lt 1 ]]; then
@ -78,40 +78,68 @@ if [[ $# -lt 1 ]]; then
exit 1 exit 1
fi fi
INPUT_PATH="$1"; shift || true INPUT_PATH="$1"
shift || true
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-O|--out-dir) -O | --out-dir)
OUT_DIR="$2"; shift 2;; OUT_DIR="$2"
-e|--ext) shift 2
OUT_EXT="$2"; shift 2;; ;;
-m|--model) -e | --ext)
RN_MODEL="$2"; shift 2;; OUT_EXT="$2"
shift 2
;;
-m | --model)
RN_MODEL="$2"
shift 2
;;
--no-ml) --no-ml)
NO_ML=true; shift;; NO_ML=true
shift
;;
--preset) --preset)
PRESET="$2"; shift 2;; PRESET="$2"
-j|--jobs) shift 2
JOBS="$2"; shift 2;; ;;
-f|--force) -j | --jobs)
FORCE=true; shift;; JOBS="$2"
-q|--quiet) shift 2
QUIET=true; shift;; ;;
-f | --force)
FORCE=true
shift
;;
-q | --quiet)
QUIET=true
shift
;;
--lowpass) --lowpass)
LOWPASS="$2"; shift 2;; LOWPASS="$2"
shift 2
;;
--suffix) --suffix)
SUFFIX="$2"; shift 2;; SUFFIX="$2"
--no-advanced|--compat) shift 2
NO_ADVANCED=true; shift;; ;;
--no-advanced | --compat)
NO_ADVANCED=true
shift
;;
--allow-fallback) --allow-fallback)
REQUIRE_ML=false; shift;; REQUIRE_ML=false
-h|--help) shift
print_usage; exit 0;; ;;
-h | --help)
print_usage
exit 0
;;
*) *)
echo "Unknown option: $1" >&2 echo "Unknown option: $1" >&2
print_usage print_usage
exit 1;; exit 1
;;
esac esac
done done
@ -119,7 +147,7 @@ require_cmd ffmpeg
# Resolve FFmpeg binary (env override -> local build -> system) # Resolve FFmpeg binary (env override -> local build -> system)
FFMPEG_BIN=${FFMPEG_BIN:-} FFMPEG_BIN=${FFMPEG_BIN:-}
if [[ -z "${FFMPEG_BIN}" ]]; then if [[ -z ${FFMPEG_BIN} ]]; then
if [[ -x "$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg" ]]; then if [[ -x "$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg" ]]; then
FFMPEG_BIN="$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg" FFMPEG_BIN="$SCRIPT_DIR/ffmpeg-build/FFmpeg/ffmpeg"
else else
@ -127,7 +155,7 @@ if [[ -z "${FFMPEG_BIN}" ]]; then
fi fi
fi fi
if ! command -v "$FFMPEG_BIN" >/dev/null 2>&1 && [[ ! -x "$FFMPEG_BIN" ]]; then if ! command -v "$FFMPEG_BIN" > /dev/null 2>&1 && [[ ! -x $FFMPEG_BIN ]]; then
echo "Error: FFmpeg binary not found: $FFMPEG_BIN" >&2 echo "Error: FFmpeg binary not found: $FFMPEG_BIN" >&2
exit 1 exit 1
fi fi
@ -137,9 +165,9 @@ fi
FFMPEG_LOG=(-hide_banner) FFMPEG_LOG=(-hide_banner)
if $QUIET; then if $QUIET; then
FFMPEG_LOG+=( -loglevel error ) FFMPEG_LOG+=(-loglevel error)
else else
FFMPEG_LOG+=( -loglevel info ) FFMPEG_LOG+=(-loglevel info)
fi fi
FFMPEG_OVERWRITE=(-n) FFMPEG_OVERWRITE=(-n)
@ -148,10 +176,10 @@ if $FORCE; then
fi fi
arnndn_available=false arnndn_available=false
if "$FFMPEG_BIN" -hide_banner -h filter=arnndn >/dev/null 2>&1; then if "$FFMPEG_BIN" -hide_banner -h filter=arnndn > /dev/null 2>&1; then
arnndn_available=true arnndn_available=true
else else
if "$FFMPEG_BIN" -hide_banner -filters 2>/dev/null | grep -Eq '(^|[[:space:]])arnndn([[:space:]]|$)'; then if "$FFMPEG_BIN" -hide_banner -filters 2> /dev/null | grep -Eq '(^|[[:space:]])arnndn([[:space:]]|$)'; then
arnndn_available=true arnndn_available=true
fi fi
fi fi
@ -161,15 +189,15 @@ fi
# Check if afftdn supports 'md' option # Check if afftdn supports 'md' option
afftdn_supports_md=false afftdn_supports_md=false
if "$FFMPEG_BIN" -hide_banner -h filter=afftdn 2>/dev/null | grep -q " md="; then if "$FFMPEG_BIN" -hide_banner -h filter=afftdn 2> /dev/null | grep -q " md="; then
afftdn_supports_md=true afftdn_supports_md=true
fi fi
# Try to auto-discover an RNNoise model if none provided # Try to auto-discover an RNNoise model if none provided
find_default_rn_model() { find_default_rn_model() {
local candidate="" # local candidate reserved for future selection logic
# Allow env variable override # Allow env variable override
if [[ -n "${RNNOISE_MODEL:-}" && -f "${RNNOISE_MODEL}" ]]; then if [[ -n ${RNNOISE_MODEL:-} && -f ${RNNOISE_MODEL} ]]; then
echo "${RNNOISE_MODEL}" echo "${RNNOISE_MODEL}"
return 0 return 0
fi fi
@ -184,11 +212,11 @@ find_default_rn_model() {
# Prefer '.rnnn' models (rnnoise-nu style) over legacy '.nn' # Prefer '.rnnn' models (rnnoise-nu style) over legacy '.nn'
local exts=("rnnn" "nn" "model") local exts=("rnnn" "nn" "model")
for d in "${dirs[@]}"; do for d in "${dirs[@]}"; do
if [[ -d "$d" ]]; then if [[ -d $d ]]; then
for ext in "${exts[@]}"; do for ext in "${exts[@]}"; do
# Pick the first matching model file # Pick the first matching model file
for f in "$d"/*."$ext"; do for f in "$d"/*."$ext"; do
if [[ -f "$f" ]]; then if [[ -f $f ]]; then
echo "$f" echo "$f"
return 0 return 0
fi fi
@ -208,7 +236,7 @@ if [[ $NO_ML == false ]]; then
fi fi
else else
# arnndn available; require an external model # arnndn available; require an external model
if [[ -n "$RN_MODEL" && -f "$RN_MODEL" ]]; then if [[ -n $RN_MODEL && -f $RN_MODEL ]]; then
: :
else else
if model_path=$(find_default_rn_model); then if model_path=$(find_default_rn_model); then
@ -222,7 +250,7 @@ if [[ $NO_ML == false ]]; then
fi fi
fi fi
fi fi
if [[ -z "$RN_MODEL" ]]; then if [[ -z $RN_MODEL ]]; then
echo "Error: RNNoise model required but not found. Automatic download failed." >&2 echo "Error: RNNoise model required but not found. Automatic download failed." >&2
echo "Hint: Set RN_URL to a reachable model URL and run Bash/get_rnnoise_model.sh, or supply -m /path/to/model.nn." >&2 echo "Hint: Set RN_URL to a reachable model URL and run Bash/get_rnnoise_model.sh, or supply -m /path/to/model.nn." >&2
exit 10 exit 10
@ -263,7 +291,7 @@ build_filters() {
fi fi
# Optional low-pass to shave hiss; keep disabled unless requested # Optional low-pass to shave hiss; keep disabled unless requested
if [[ -n "$LOWPASS" ]]; then if [[ -n $LOWPASS ]]; then
filters+=("lowpass=f=${LOWPASS}") filters+=("lowpass=f=${LOWPASS}")
fi fi
@ -291,7 +319,8 @@ build_filters() {
filters+=("aresample=16000") filters+=("aresample=16000")
filters+=("aformat=channel_layouts=mono:sample_fmts=s16") filters+=("aformat=channel_layouts=mono:sample_fmts=s16")
local IFS=","; echo "${filters[*]}" local IFS=","
echo "${filters[*]}"
} }
make_out_path_for_file() { make_out_path_for_file() {
@ -300,7 +329,7 @@ make_out_path_for_file() {
base=$(basename -- "$in_file") base=$(basename -- "$in_file")
base="${base%.*}" base="${base%.*}"
local out_base="${base}${SUFFIX}.${OUT_EXT}" local out_base="${base}${SUFFIX}.${OUT_EXT}"
if [[ -n "$OUT_DIR" ]]; then if [[ -n $OUT_DIR ]]; then
mkdir -p -- "$OUT_DIR" mkdir -p -- "$OUT_DIR"
echo "$OUT_DIR/$out_base" echo "$OUT_DIR/$out_base"
else else
@ -316,15 +345,15 @@ process_one() {
out_file=$(make_out_path_for_file "$in_file") out_file=$(make_out_path_for_file "$in_file")
# Choose codec based on extension # Choose codec based on extension
local codec=( -c:a pcm_s16le ) local codec=(-c:a pcm_s16le)
if [[ "$OUT_EXT" == "flac" ]]; then if [[ $OUT_EXT == "flac" ]]; then
codec=( -c:a flac ) codec=(-c:a flac)
fi fi
local af local af
af=$(build_filters) af=$(build_filters)
if [[ -f "$out_file" && $FORCE == false ]]; then if [[ -f $out_file && $FORCE == false ]]; then
echo "Skip (exists): $out_file" echo "Skip (exists): $out_file"
return 0 return 0
fi fi
@ -335,7 +364,7 @@ process_one() {
# Concurrency helpers (bash >= 5 supports wait -n; fallback to sequential if not) # Concurrency helpers (bash >= 5 supports wait -n; fallback to sequential if not)
supports_wait_n=false supports_wait_n=false
if [[ -n "${BASH_VERSINFO:-}" && ${BASH_VERSINFO[0]} -ge 5 ]]; then if [[ -n ${BASH_VERSINFO:-} && ${BASH_VERSINFO[0]} -ge 5 ]]; then
supports_wait_n=true supports_wait_n=true
fi fi
@ -344,7 +373,7 @@ run_dir() {
# Common audio extensions (case-insensitive) # Common audio extensions (case-insensitive)
mapfile -d '' files < <(find "$dir" -type f \ mapfile -d '' files < <(find "$dir" -type f \
\( -iname "*.wav" -o -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.aac" -o -iname "*.flac" \ \( -iname "*.wav" -o -iname "*.mp3" -o -iname "*.m4a" -o -iname "*.aac" -o -iname "*.flac" \
-o -iname "*.ogg" -o -iname "*.opus" -o -iname "*.wma" -o -iname "*.webm" \) -print0) -o -iname "*.ogg" -o -iname "*.opus" -o -iname "*.wma" -o -iname "*.webm" \) -print0)
if [[ ${#files[@]} -eq 0 ]]; then if [[ ${#files[@]} -eq 0 ]]; then
echo "No audio files found in: $dir" echo "No audio files found in: $dir"
@ -353,12 +382,12 @@ run_dir() {
local running=0 local running=0
for f in "${files[@]}"; do for f in "${files[@]}"; do
if [[ "$JOBS" -le 1 || $supports_wait_n == false ]]; then if [[ $JOBS -le 1 || $supports_wait_n == false ]]; then
process_one "$f" process_one "$f"
else else
process_one "$f" & process_one "$f" &
((running++)) ((running++))
if (( running >= JOBS )); then if ((running >= JOBS)); then
wait -n || true wait -n || true
((running--)) ((running--))
fi fi
@ -366,20 +395,20 @@ run_dir() {
done done
# Wait for any remaining background jobs # Wait for any remaining background jobs
if (( JOBS > 1 )) && $supports_wait_n; then if ((JOBS > 1)) && $supports_wait_n; then
wait || true wait || true
fi fi
} }
main() { main() {
# Sanity checks and notices # Sanity checks and notices
if [[ -n "$RN_MODEL" && $use_arnndn == false && $NO_ML == false ]]; then if [[ -n $RN_MODEL && $use_arnndn == false && $NO_ML == false ]]; then
echo "Note: arnndn filter not available in your ffmpeg or model missing — using afftdn." >&2 echo "Note: arnndn filter not available in your ffmpeg or model missing — using afftdn." >&2
fi fi
if [[ -f "$INPUT_PATH" ]]; then if [[ -f $INPUT_PATH ]]; then
process_one "$INPUT_PATH" process_one "$INPUT_PATH"
elif [[ -d "$INPUT_PATH" ]]; then elif [[ -d $INPUT_PATH ]]; then
run_dir "$INPUT_PATH" run_dir "$INPUT_PATH"
else else
echo "Error: Input path not found: $INPUT_PATH" >&2 echo "Error: Input path not found: $INPUT_PATH" >&2

View File

@ -27,29 +27,29 @@ mkdir -p "$OUTPUT_DIR"
convert_video() { convert_video() {
local input_file="$1" local input_file="$1"
local output_file="$OUTPUT_DIR/${input_file%.*}.$TARGET_EXT" local output_file="$OUTPUT_DIR/${input_file%.*}.$TARGET_EXT"
# Get video duration in seconds # Get video duration in seconds
DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file") DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file")
echo "Duration: $DURATION seconds" echo "Duration: $DURATION seconds"
# Convert target size to bytes # Convert target size to bytes
TARGET_SIZE_BYTES=$(numfmt --from=iec "$TARGET_SIZE") TARGET_SIZE_BYTES=$(numfmt --from=iec "$TARGET_SIZE")
# Calculate target bitrate in kilobits per second # Calculate target bitrate in kilobits per second
TARGET_BITRATE=$(echo "($TARGET_SIZE_BYTES * 8) / $DURATION / 2000" | bc) TARGET_BITRATE=$(echo "($TARGET_SIZE_BYTES * 8) / $DURATION / 2000" | bc)
# Convert video # Convert video
ffmpeg -i "$input_file" -vcodec libx264 -b:v "${TARGET_BITRATE}k" -preset veryslow -acodec aac -c:a copy "$output_file" ffmpeg -i "$input_file" -vcodec libx264 -b:v "${TARGET_BITRATE}k" -preset veryslow -acodec aac -c:a copy "$output_file"
# Get original and converted video sizes # Get original and converted video sizes
ORIGINAL_SIZE=$(stat -c%s "$input_file") ORIGINAL_SIZE=$(stat -c%s "$input_file")
CONVERTED_SIZE=$(stat -c%s "$output_file") CONVERTED_SIZE=$(stat -c%s "$output_file")
# Print out details # Print out details
echo "Original size: $(numfmt --to=iec $ORIGINAL_SIZE)" echo "Original size: $(numfmt --to=iec "$ORIGINAL_SIZE")"
echo "Video length: $DURATION seconds" echo "Video length: $DURATION seconds"
echo "Target size: $TARGET_SIZE" echo "Target size: $TARGET_SIZE"
echo "Converted size: $(numfmt --to=iec $CONVERTED_SIZE)" echo "Converted size: $(numfmt --to=iec "$CONVERTED_SIZE")"
echo "Target bitrate: ${TARGET_BITRATE}kbps" echo "Target bitrate: ${TARGET_BITRATE}kbps"
} }
@ -57,12 +57,12 @@ convert_video() {
move_video() { move_video() {
local input_file="$1" local input_file="$1"
local output_file="$OUTPUT_DIR/${input_file##*/}" local output_file="$OUTPUT_DIR/${input_file##*/}"
# Get original video size # Get original video size
ORIGINAL_SIZE=$(stat -c%s "$input_file") ORIGINAL_SIZE=$(stat -c%s "$input_file")
# Check if video is below target size and in desired format # Check if video is below target size and in desired format
if [[ "$ORIGINAL_SIZE" -le "$TARGET_SIZE_BYTES" && "${input_file##*.}" == "$TARGET_EXT" ]]; then if [[ $ORIGINAL_SIZE -le $TARGET_SIZE_BYTES && ${input_file##*.} == "$TARGET_EXT" ]]; then
mv "$input_file" "$output_file" mv "$input_file" "$output_file"
echo "Moved $input_file to $output_file" echo "Moved $input_file to $output_file"
else else

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Get the list of directories in the current script directory # Get the list of directories in the current script directory
directories=($(find . -maxdepth 1 -type d ! -name .)) mapfile -t directories < <(find . -maxdepth 1 -type d ! -name . -printf '%f\n')
# Check if there is exactly one directory # Check if there is exactly one directory
if [ ${#directories[@]} -ne 1 ]; then if [ ${#directories[@]} -ne 1 ]; then
@ -13,16 +13,16 @@ fi
folder_name=${directories[0]} folder_name=${directories[0]}
random_string() { random_string() {
local length=$1 local length="$1"
tr -dc 'a-zA-Z0-9!@#$%^&*()_+{}|:<>?~' < /dev/urandom | head -c $length tr -dc 'a-zA-Z0-9!@#$%^&*()_+{}|:<>?~' < /dev/urandom | head -c "$length"
} }
# Number of copies to create (default 100) # Number of copies to create (default 100)
num_copies=${1:-100} num_copies="${1:-100}"
# Create the specified number of copies # Create the specified number of copies
for ((i=1; i<=num_copies; i++)); do for ((i = 1; i <= num_copies; i++)); do
new_folder_name="$(random_string 255)" new_folder_name="$(random_string 255)"
cp -r "$folder_name" "$new_folder_name" cp -r "$folder_name" "$new_folder_name"
echo "Folder copied and renamed to '$new_folder_name'" echo "Folder copied and renamed to '$new_folder_name'"
done done

View File

@ -3,8 +3,8 @@
# Check if there are any .txt files in the current directory # Check if there are any .txt files in the current directory
txt_files=(*.txt) txt_files=(*.txt)
if [ ${#txt_files[@]} -eq 0 ]; then if [ ${#txt_files[@]} -eq 0 ]; then
echo "No .txt files found in the current directory!" echo "No .txt files found in the current directory!"
exit 1 exit 1
fi fi
total_files=0 total_files=0
@ -14,33 +14,33 @@ downloaded_size=0
# Calculate total number of files and total size to download # Calculate total number of files and total size to download
for file in *.txt; do for file in *.txt; do
while IFS= read -r url; do while IFS= read -r url; do
if [[ -n "$url" ]]; then if [[ -n $url ]]; then
total_files=$((total_files + 1)) total_files=$((total_files + 1))
size=$(wget --spider "$url" 2>&1 | grep Length | awk '{print $2}') size=$(wget --spider "$url" 2>&1 | awk '/Length/ {print $2}')
total_size=$((total_size + size)) total_size=$((total_size + size))
fi fi
done < "$file" done < "$file"
done done
# Loop through each .txt file and download each URL in parallel # Loop through each .txt file and download each URL in parallel
for file in *.txt; do for file in *.txt; do
echo "Processing $file..." echo "Processing $file..."
while IFS= read -r url; do while IFS= read -r url; do
if [[ -n "$url" ]]; then if [[ -n $url ]]; then
{ {
wget -q --show-progress "$url" wget -q --show-progress "$url"
downloaded_files=$((downloaded_files + 1)) downloaded_files=$((downloaded_files + 1))
size=$(wget --spider "$url" 2>&1 | grep Length | awk '{print $2}') size=$(wget --spider "$url" 2>&1 | awk '/Length/ {print $2}')
downloaded_size=$((downloaded_size + size)) downloaded_size=$((downloaded_size + size))
remaining_files=$((total_files - downloaded_files)) remaining_files=$((total_files - downloaded_files))
remaining_size=$((total_size - downloaded_size)) remaining_size=$((total_size - downloaded_size))
echo "Downloaded: $downloaded_files/$total_files files, $downloaded_size/$total_size bytes" echo "Downloaded: $downloaded_files/$total_files files, $downloaded_size/$total_size bytes"
echo "Remaining: $remaining_files files, $remaining_size bytes" echo "Remaining: $remaining_files files, $remaining_size bytes"
} & } &
fi fi
done < "$file" done < "$file"
done done
# Wait for all background jobs to complete # Wait for all background jobs to complete
wait wait

View File

@ -18,10 +18,14 @@
set -euo pipefail set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; BLUE="\033[1;34m"; NC="\033[0m" GREEN="\033[1;32m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } YELLOW="\033[1;33m"
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } RED="\033[1;31m"
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; } log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
DO_POLICY=false DO_POLICY=false
@ -29,7 +33,7 @@ SET_DEFAULT=false
DO_RESTART=false DO_RESTART=false
usage() { usage() {
cat <<EOF cat << EOF
fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium fix_thorium_unity.sh - Auto-allow unityhub:// from Unity origins in Thorium/Chromium
Options: Options:
@ -46,16 +50,32 @@ EOF
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--policy) DO_POLICY=true; shift ;; --policy)
--set-default) SET_DEFAULT=true; shift ;; DO_POLICY=true
--restart) DO_RESTART=true; shift ;; shift
-h|--help) usage; exit 0 ;; ;;
*) log_error "Unknown argument: $1"; usage; exit 1 ;; --set-default)
SET_DEFAULT=true
shift
;;
--restart)
DO_RESTART=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac esac
done done
ensure_sudo() { ensure_sudo() {
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo > /dev/null 2>&1; then
log_error "sudo not found; cannot install system policy. Use --set-default or run from root." log_error "sudo not found; cannot install system policy. Use --set-default or run from root."
exit 1 exit 1
fi fi
@ -65,16 +85,16 @@ install_policy() {
ensure_sudo ensure_sudo
# Candidate policy directories (most common for Chromium forks) # Candidate policy directories (most common for Chromium forks)
local candidates=( local candidates=(
"/etc/thorium-browser/policies/managed" # Thorium "/etc/thorium-browser/policies/managed" # Thorium
"/etc/chromium/policies/managed" # Chromium "/etc/chromium/policies/managed" # Chromium
"/etc/opt/chrome/policies/managed" # Google Chrome "/etc/opt/chrome/policies/managed" # Google Chrome
) )
local wrote_any=false local wrote_any=false
for target in "${candidates[@]}"; do for target in "${candidates[@]}"; do
log_info "Installing policy into: $target" log_info "Installing policy into: $target"
sudo mkdir -p "$target" sudo mkdir -p "$target"
local policy_file="$target/unityhub-policy.json" local policy_file="$target/unityhub-policy.json"
sudo tee "$policy_file" >/dev/null <<'JSON' sudo tee "$policy_file" > /dev/null << 'JSON'
{ {
"AutoLaunchProtocolsFromOrigins": [ "AutoLaunchProtocolsFromOrigins": [
{ "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true }, { "protocol": "unityhub", "origin": "https://id.unity.com", "allow": true },
@ -90,13 +110,13 @@ JSON
log_ok "Policy written: $policy_file" log_ok "Policy written: $policy_file"
wrote_any=true wrote_any=true
done done
if [[ "$wrote_any" != true ]]; then if [[ $wrote_any != true ]]; then
log_warn "Policy may not have been written. No candidate directories processed." log_warn "Policy may not have been written. No candidate directories processed."
fi fi
} }
set_default_browser() { set_default_browser() {
if command -v xdg-settings >/dev/null 2>&1; then if command -v xdg-settings > /dev/null 2>&1; then
# Prefer the upstream desktop id if it exists # Prefer the upstream desktop id if it exists
local desktop="thorium-browser.desktop" local desktop="thorium-browser.desktop"
if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then if [[ ! -f "/usr/share/applications/$desktop" && -f "$HOME/.local/share/applications/$desktop" ]]; then
@ -107,7 +127,7 @@ set_default_browser() {
fi fi
log_info "Setting default browser to $desktop" log_info "Setting default browser to $desktop"
xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings" xdg-settings set default-web-browser "$desktop" || log_warn "Failed to set default browser via xdg-settings"
log_ok "Default browser set to: $(xdg-settings get default-web-browser 2>/dev/null || echo "$desktop")" log_ok "Default browser set to: $(xdg-settings get default-web-browser 2> /dev/null || echo "$desktop")"
else else
log_warn "xdg-settings not found; cannot set default browser automatically." log_warn "xdg-settings not found; cannot set default browser automatically."
fi fi
@ -116,12 +136,13 @@ set_default_browser() {
restart_thorium() { restart_thorium() {
# Kill Thorium processes and start fresh # Kill Thorium processes and start fresh
log_info "Restarting Thorium..." log_info "Restarting Thorium..."
pkill -9 -f 'thorium-browser' 2>/dev/null || true pkill -9 -f 'thorium-browser' 2> /dev/null || true
# Also kill unityhub-bin's embedded Chromium if any leftover (harmless) # Also kill unityhub-bin's embedded Chromium if any leftover (harmless)
pkill -9 -f 'unityhub-bin' 2>/dev/null || true pkill -9 -f 'unityhub-bin' 2> /dev/null || true
# Start Thorium detached if available # Start Thorium detached if available
if command -v thorium-browser >/dev/null 2>&1; then if command -v thorium-browser > /dev/null 2>&1; then
nohup thorium-browser >/dev/null 2>&1 & disown || true nohup thorium-browser > /dev/null 2>&1 &
disown || true
fi fi
log_ok "Thorium restart attempted." log_ok "Thorium restart attempted."
} }
@ -131,7 +152,7 @@ main() {
$SET_DEFAULT && set_default_browser $SET_DEFAULT && set_default_browser
$DO_RESTART && restart_thorium $DO_RESTART && restart_thorium
cat <<'NEXT' cat << 'NEXT'
--- ---
Next steps: Next steps:
- Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app. - Open Unity Hub, click Sign in, complete in Thorium; when prompted, allow the unityhub link to open the app.

View File

@ -23,15 +23,19 @@ set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
SCRIPT_NAME="$(basename "$0")" SCRIPT_NAME="$(basename "$0")"
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; BLUE="\033[1;34m"; NC="\033[0m" GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
BLUE="\033[1;34m"
NC="\033[0m"
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } log_ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; } log_error() { echo -e "${RED}[ERR ]${NC} $*" 1>&2; }
usage() { usage() {
cat <<EOF cat << EOF
${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler ${SCRIPT_NAME} - Fix Unity Hub sign-in by registering unityhub:// URL handler
Options: Options:
@ -49,22 +53,35 @@ RUN_TEST=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-y|--yes) AUTO_INSTALL=true; shift ;; -y | --yes)
--test) RUN_TEST=true; shift ;; AUTO_INSTALL=true
-h|--help) usage; exit 0 ;; shift
*) log_error "Unknown argument: $1"; usage; exit 1 ;; ;;
--test)
RUN_TEST=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
log_error "Unknown argument: $1"
usage
exit 1
;;
esac esac
done done
require_cmd() { require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then if ! command -v "$1" > /dev/null 2>&1; then
return 1 return 1
fi fi
} }
ensure_deps_arch() { ensure_deps_arch() {
# Best-effort install for Arch-based systems # Best-effort install for Arch-based systems
if [[ "$AUTO_INSTALL" != true ]]; then if [[ $AUTO_INSTALL != true ]]; then
log_warn "Skipping package installation (use -y to auto-install)." log_warn "Skipping package installation (use -y to auto-install)."
return 0 return 0
fi fi
@ -100,8 +117,8 @@ detect_unityhub() {
local install_type="UNKNOWN" exec_cmd="" local install_type="UNKNOWN" exec_cmd=""
# 1) Flatpak # 1) Flatpak
if command -v flatpak >/dev/null 2>&1; then if command -v flatpak > /dev/null 2>&1; then
if flatpak info com.unity.UnityHub >/dev/null 2>&1; then if flatpak info com.unity.UnityHub > /dev/null 2>&1; then
install_type="FLATPAK" install_type="FLATPAK"
exec_cmd="flatpak run com.unity.UnityHub %U" exec_cmd="flatpak run com.unity.UnityHub %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
@ -110,7 +127,7 @@ detect_unityhub() {
fi fi
# 2) Native binary in PATH # 2) Native binary in PATH
if command -v unityhub >/dev/null 2>&1; then if command -v unityhub > /dev/null 2>&1; then
local path local path
path="$(command -v unityhub)" path="$(command -v unityhub)"
install_type="NATIVE" install_type="NATIVE"
@ -128,16 +145,16 @@ detect_unityhub() {
) )
local found_exec="" local found_exec=""
for d in "${search_dirs[@]}"; do for d in "${search_dirs[@]}"; do
[[ -d "$d" ]] || continue [[ -d $d ]] || continue
# prefer official naming when present # prefer official naming when present
local f local f
for f in "$d"/*.desktop; do for f in "$d"/*.desktop; do
[[ -e "$f" ]] || continue [[ -e $f ]] || continue
if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2>/dev/null || \ if grep -qiE '^(Name|Comment)=.*Unity Hub' "$f" 2> /dev/null ||
grep -qiE 'Exec=.*unityhub' "$f" 2>/dev/null; then grep -qiE 'Exec=.*unityhub' "$f" 2> /dev/null; then
local exec_line local exec_line
exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')" exec_line="$(grep -iE '^Exec=' "$f" | head -n1 | sed 's/^Exec=//')"
if [[ -n "$exec_line" ]]; then if [[ -n $exec_line ]]; then
found_exec="$exec_line" found_exec="$exec_line"
break 2 break 2
fi fi
@ -145,14 +162,14 @@ detect_unityhub() {
done done
done done
if [[ -n "$found_exec" ]]; then if [[ -n $found_exec ]]; then
# Normalize: ensure %U present # Normalize: ensure %U present
if [[ "$found_exec" != *"%U"* && "$found_exec" != *"%u"* ]]; then if [[ $found_exec != *"%U"* && $found_exec != *"%u"* ]]; then
found_exec+=" %U" found_exec+=" %U"
fi fi
if [[ "$found_exec" == flatpak* ]]; then if [[ $found_exec == flatpak* ]]; then
install_type="FLATPAK" install_type="FLATPAK"
elif [[ "$found_exec" == *AppImage* || "$found_exec" == *appimage* ]]; then elif [[ $found_exec == *AppImage* || $found_exec == *appimage* ]]; then
install_type="APPIMAGE" install_type="APPIMAGE"
else else
install_type="NATIVE" install_type="NATIVE"
@ -170,7 +187,7 @@ detect_unityhub() {
local ai local ai
for ai in "${ai_candidates[@]}"; do for ai in "${ai_candidates[@]}"; do
for p in $ai; do for p in $ai; do
if [[ -f "$p" && -x "$p" ]]; then if [[ -f $p && -x $p ]]; then
install_type="APPIMAGE" install_type="APPIMAGE"
exec_cmd="$p %U" exec_cmd="$p %U"
echo "$install_type|$exec_cmd" echo "$install_type|$exec_cmd"
@ -186,7 +203,7 @@ create_handler_desktop() {
local exec_cmd="$1" local exec_cmd="$1"
local dest="$desktop_dir/unityhub-url-handler.desktop" local dest="$desktop_dir/unityhub-url-handler.desktop"
log_info "Writing handler desktop entry: $dest" log_info "Writing handler desktop entry: $dest"
cat > "$dest" <<DESK cat > "$dest" << DESK
[Desktop Entry] [Desktop Entry]
Name=Unity Hub URL Handler Name=Unity Hub URL Handler
Comment=Handle unityhub:// links for Unity Hub sign-in Comment=Handle unityhub:// links for Unity Hub sign-in
@ -206,14 +223,14 @@ DESK
register_mime_handler() { register_mime_handler() {
local desktop_file="$1" local desktop_file="$1"
# Update desktop database if available # Update desktop database if available
if command -v update-desktop-database >/dev/null 2>&1; then if command -v update-desktop-database > /dev/null 2>&1; then
update-desktop-database "$desktop_dir" || true update-desktop-database "$desktop_dir" || true
else else
log_warn "update-desktop-database not found (install desktop-file-utils)." log_warn "update-desktop-database not found (install desktop-file-utils)."
fi fi
# Register as default handler for both schemes # Register as default handler for both schemes
if command -v xdg-mime >/dev/null 2>&1; then if command -v xdg-mime > /dev/null 2>&1; then
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unityhub || true
xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true xdg-mime default "$(basename "$desktop_file")" x-scheme-handler/unity || true
else else
@ -224,12 +241,13 @@ register_mime_handler() {
} }
verify_registration() { verify_registration() {
local expected="$(basename "$1")" local expected cur1 cur2
local cur1="$(xdg-mime query default x-scheme-handler/unityhub 2>/dev/null || true)" expected="$(basename "$1")"
local cur2="$(xdg-mime query default x-scheme-handler/unity 2>/dev/null || true)" cur1="$(xdg-mime query default x-scheme-handler/unityhub 2> /dev/null || true)"
cur2="$(xdg-mime query default x-scheme-handler/unity 2> /dev/null || true)"
log_info "Current handler (unityhub): ${cur1:-<none>}" log_info "Current handler (unityhub): ${cur1:-<none>}"
log_info "Current handler (unity): ${cur2:-<none>}" log_info "Current handler (unity): ${cur2:-<none>}"
if [[ "$cur1" == "$expected" ]]; then if [[ $cur1 == "$expected" ]]; then
log_ok "unityhub scheme correctly set to $expected" log_ok "unityhub scheme correctly set to $expected"
else else
log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})." log_warn "unityhub scheme not set to $expected (currently: ${cur1:-none})."
@ -237,10 +255,10 @@ verify_registration() {
} }
maybe_test_open() { maybe_test_open() {
if [[ "$RUN_TEST" == true ]]; then if [[ $RUN_TEST == true ]]; then
log_info "Opening test link: unityhub://v1/editor-signin" log_info "Opening test link: unityhub://v1/editor-signin"
if command -v xdg-open >/dev/null 2>&1; then if command -v xdg-open > /dev/null 2>&1; then
xdg-open 'unityhub://v1/editor-signin' >/dev/null 2>&1 || true xdg-open 'unityhub://v1/editor-signin' > /dev/null 2>&1 || true
log_ok "Test link invoked. Check if Unity Hub launches or focuses." log_ok "Test link invoked. Check if Unity Hub launches or focuses."
else else
log_warn "xdg-open not found; cannot run test automatically." log_warn "xdg-open not found; cannot run test automatically."
@ -257,7 +275,7 @@ main() {
log_info "Detecting Unity Hub installation..." log_info "Detecting Unity Hub installation..."
IFS='|' read -r install_type exec_cmd < <(detect_unityhub) IFS='|' read -r install_type exec_cmd < <(detect_unityhub)
log_info "Detected type: $install_type" log_info "Detected type: $install_type"
if [[ -z "${exec_cmd:-}" ]]; then if [[ -z ${exec_cmd:-} ]]; then
log_warn "Could not find Unity Hub executable automatically." log_warn "Could not find Unity Hub executable automatically."
log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'" log_warn "- If using Flatpak: install with 'flatpak install flathub com.unity.UnityHub'"
log_warn "- If native (AUR): ensure 'unityhub' is in PATH" log_warn "- If native (AUR): ensure 'unityhub' is in PATH"
@ -273,7 +291,7 @@ main() {
register_mime_handler "$desktop_file" register_mime_handler "$desktop_file"
verify_registration "$desktop_file" verify_registration "$desktop_file"
cat <<'NOTE' cat << 'NOTE'
--- ---
Next steps: Next steps:
- Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub. - Sign in from Unity Hub. When the browser finishes, ALLOW the prompt to open xdg-open/Unity Hub.
@ -283,7 +301,7 @@ NOTE
maybe_test_open maybe_test_open
log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R "x-scheme-handler/unityhub" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10" log_ok "Done. If login still fails, check the Hub's logs and share the outputs of:\n which unityhub || true\n flatpak info com.unity.UnityHub 2>/dev/null | sed -n '1,5p' || true\n xdg-mime query default x-scheme-handler/unityhub\n grep -R \"x-scheme-handler/unityhub\" ~/.local/share/applications /usr/share/applications 2>/dev/null | head -n 10"
} }
main "$@" main "$@"

View File

@ -2,72 +2,81 @@
# Function to generate random number between two values # Function to generate random number between two values
random_number() { random_number() {
echo $((RANDOM % ($2 - $1 + 1) + $1)) echo $((RANDOM % ($2 - $1 + 1) + $1))
} }
# Function to generate random string with non-computer-friendly characters # Function to generate random string with non-computer-friendly characters
random_string() { random_string() {
local length=$1 local length="$1"
tr -dc 'a-zA-Z0-9!@#$%^&*()_+{}|:<>?~' < /dev/urandom | head -c $length tr -dc 'a-zA-Z0-9!@#$%^&*()_+{}|:<>?~' < /dev/urandom | head -c "$length"
} }
# Function to calculate total number of folders to be created # Function to calculate total number of folders to be created
calculate_total_folders() { calculate_total_folders() {
local depth=$1 local depth="$1"
local total=0 local total=0
if [ "$depth" -le 10 ]; then if [ "$depth" -le 10 ]; then
local num_subfolders=$(random_number 1 50) local num_subfolders
total=$((num_subfolders + total)) num_subfolders=$(random_number 1 50)
for ((i=1; i<=num_subfolders; i++)); do total=$((num_subfolders + total))
total=$((total + $(calculate_total_folders $((depth + 1))))) for ((i = 1; i <= num_subfolders; i++)); do
done total=$((total + $(calculate_total_folders $((depth + 1)))))
fi done
echo $total fi
echo "$total"
} }
# Function to create folders and files recursively # Function to create folders and files recursively
create_structure() { create_structure() {
local current_depth=$1 local current_depth="$1"
local parent_dir=$2 local parent_dir="$2"
local start_time=$3 local start_time="$3"
if [ "$current_depth" -le 10 ]; then if [ "$current_depth" -le 10 ]; then
local num_subfolders=$(random_number 1 50) local num_subfolders
echo "Creating $num_subfolders subfolders at depth $current_depth" num_subfolders=$(random_number 1 50)
for ((i=1; i<=num_subfolders; i++)); do echo "Creating $num_subfolders subfolders at depth $current_depth"
local subfolder="$parent_dir/$(random_string 255)" for ((i = 1; i <= num_subfolders; i++)); do
mkdir -p "$subfolder" local subfolder
((generated_folders++)) subfolder="$parent_dir/$(random_string 255)"
mkdir -p "$subfolder"
# Display progress ((generated_folders++))
local elapsed_time=$(( $(date +%s) - start_time ))
local estimated_total_time=$(( elapsed_time * total_folders / generated_folders ))
local remaining_time=$(( estimated_total_time - elapsed_time ))
echo "Generated: $generated_folders/$total_folders folders. Estimated time left: $remaining_time seconds."
# Create random number of empty files # Display progress
local num_files=$(random_number 10 100) local elapsed_time
echo "Creating $num_files files" elapsed_time=$(($(date +%s) - start_time))
for ((j=1; j<=num_files; j++)); do local estimated_total_time
touch "$subfolder/$(random_string 255)" estimated_total_time=$((elapsed_time * total_folders / generated_folders))
done local remaining_time
remaining_time=$((estimated_total_time - elapsed_time))
echo "Generated: $generated_folders/$total_folders folders. Estimated time left: $remaining_time seconds."
# Recursively create subfolders # Create random number of empty files
create_structure $((current_depth + 1)) "$subfolder" $start_time local num_files
done num_files=$(random_number 10 100)
fi echo "Creating $num_files files"
for ((j = 1; j <= num_files; j++)); do
touch "$subfolder/$(random_string 255)"
done
# Recursively create subfolders
create_structure $((current_depth + 1)) "$subfolder" "$start_time"
done
fi
} }
# Main folder # Main folder
main_folder="/home/k.rudnicki@aiclearing.com/testsAndMisc/Bash/main_folder" main_folder="/home/k.rudnicki@aiclearing.com/testsAndMisc/Bash/main_folder"
mkdir -p "$main_folder" mkdir -p "$main_folder"
# Calculate total folders to be created # Calculate total folders to be created (best-effort). If calculation is expensive, you can uncomment.
# total_folders=$(calculate_total_folders 1) # total_folders=$(calculate_total_folders 1)
# Fallback when not precomputed: estimate grows as we generate
total_folders=${total_folders:-0}
generated_folders=0 generated_folders=0
echo "Total folders to be generated: $total_folders" echo "Total folders to be generated: ${total_folders:-unknown}"
# Start creating structure from the main folder # Start creating structure from the main folder
start_time=$(date +%s) start_time=$(date +%s)
create_structure 1 "$main_folder" $start_time create_structure 1 "$main_folder" "$start_time"

View File

@ -14,18 +14,24 @@ set -euo pipefail
ask_yes_no() { ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in case "${ans:-}" in
y|Y|yes|YES) return 0;; y | Y | yes | YES) return 0 ;;
*) return 1;; *) return 1 ;;
esac esac
} }
has_cmd() { command -v "$1" >/dev/null 2>&1; } has_cmd() { command -v "$1" > /dev/null 2>&1; }
YES=false YES=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-y|--yes) YES=true; shift;; -y | --yes)
*) echo "Unknown option: $1" >&2; exit 2;; YES=true
shift
;;
*)
echo "Unknown option: $1" >&2
exit 2
;;
esac esac
done done
@ -35,7 +41,7 @@ RN_TARGET_NAME=${RN_TARGET_NAME:-"rnnoise_model.rnnn"}
mkdir -p "$RN_TARGET_DIR" mkdir -p "$RN_TARGET_DIR"
dest="$RN_TARGET_DIR/$RN_TARGET_NAME" dest="$RN_TARGET_DIR/$RN_TARGET_NAME"
if [[ -f "$dest" ]]; then if [[ -f $dest ]]; then
echo "Model already exists at: $dest" echo "Model already exists at: $dest"
exit 0 exit 0
fi fi
@ -53,7 +59,7 @@ if ! has_cmd curl && ! has_cmd wget; then
fi fi
# Priority 1: explicit URL # Priority 1: explicit URL
if [[ -n "${RN_URL:-}" ]]; then if [[ -n ${RN_URL:-} ]]; then
echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2 echo "Downloading RNNoise model from RN_URL: $RN_URL" >&2
tmp=$(mktemp) tmp=$(mktemp)
if has_cmd curl; then if has_cmd curl; then
@ -61,7 +67,7 @@ if [[ -n "${RN_URL:-}" ]]; then
else else
wget -qO "$tmp" "$RN_URL" wget -qO "$tmp" "$RN_URL"
fi fi
if [[ -s "$tmp" ]]; then if [[ -s $tmp ]]; then
mv "$tmp" "$dest" mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" echo "Saved RNNoise model to: $dest"
exit 0 exit 0
@ -83,7 +89,7 @@ for u in "${NU_URLS[@]}"; do
tmp=$(mktemp) tmp=$(mktemp)
if has_cmd curl; then if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then if curl -fsSL "$u" -o "$tmp"; then
if [[ -s "$tmp" ]]; then if [[ -s $tmp ]]; then
mv "$tmp" "$dest" mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2 echo "Saved RNNoise model to: $dest" >&2
exit 0 exit 0
@ -91,7 +97,7 @@ for u in "${NU_URLS[@]}"; do
fi fi
else else
if wget -qO "$tmp" "$u"; then if wget -qO "$tmp" "$u"; then
if [[ -s "$tmp" ]]; then if [[ -s $tmp ]]; then
mv "$tmp" "$dest" mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2 echo "Saved RNNoise model to: $dest" >&2
exit 0 exit 0
@ -110,7 +116,7 @@ for u in "${RNNDN_URLS[@]}"; do
tmp=$(mktemp) tmp=$(mktemp)
if has_cmd curl; then if has_cmd curl; then
if curl -fsSL "$u" -o "$tmp"; then if curl -fsSL "$u" -o "$tmp"; then
if [[ -s "$tmp" ]]; then if [[ -s $tmp ]]; then
mv "$tmp" "$dest" mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2 echo "Saved RNNoise model to: $dest" >&2
exit 0 exit 0
@ -118,7 +124,7 @@ for u in "${RNNDN_URLS[@]}"; do
fi fi
else else
if wget -qO "$tmp" "$u"; then if wget -qO "$tmp" "$u"; then
if [[ -s "$tmp" ]]; then if [[ -s $tmp ]]; then
mv "$tmp" "$dest" mv "$tmp" "$dest"
echo "Saved RNNoise model to: $dest" >&2 echo "Saved RNNoise model to: $dest" >&2
exit 0 exit 0
@ -172,10 +178,10 @@ done
if has_cmd yay; then if has_cmd yay; then
echo "Attempting to install AUR packages that may include RNNoise models..." >&2 echo "Attempting to install AUR packages that may include RNNoise models..." >&2
set +e set +e
yay -S --noconfirm denoiseit-git 2>/dev/null yay -S --noconfirm denoiseit-git 2> /dev/null
yay -S --noconfirm speech-denoiser-git 2>/dev/null yay -S --noconfirm speech-denoiser-git 2> /dev/null
set -e set -e
mapfile -t found < <(bash -lc 'shopt -s globstar nullglob; for f in /usr/share/**/*.nn /usr/share/**/*.rnnn /usr/local/share/**/*.nn /usr/local/share/**/*.rnnn; do [[ -f "$f" ]] && echo "$f"; done' 2>/dev/null || true) mapfile -t found < <(bash -lc 'shopt -s globstar nullglob; for f in /usr/share/**/*.nn /usr/share/**/*.rnnn /usr/local/share/**/*.nn /usr/local/share/**/*.rnnn; do [[ -f "$f" ]] && echo "$f"; done' 2> /dev/null || true)
if [[ ${#found[@]} -gt 0 ]]; then if [[ ${#found[@]} -gt 0 ]]; then
echo "Found candidate models:" >&2 echo "Found candidate models:" >&2
printf ' %s\n' "${found[@]}" >&2 printf ' %s\n' "${found[@]}" >&2

View File

@ -14,12 +14,12 @@ print_info() {
ask_yes_no() { ask_yes_no() {
read -r -p "$1 [y/N]: " ans || true read -r -p "$1 [y/N]: " ans || true
case "${ans:-}" in case "${ans:-}" in
y|Y|yes|YES) return 0;; y | Y | yes | YES) return 0 ;;
*) return 1;; *) return 1 ;;
esac esac
} }
has_cmd() { command -v "$1" >/dev/null 2>&1; } has_cmd() { command -v "$1" > /dev/null 2>&1; }
detect_distro() { detect_distro() {
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
@ -39,13 +39,13 @@ main() {
print_info "Your ffmpeg already supports arnndn." print_info "Your ffmpeg already supports arnndn."
else else
case "$distro" in case "$distro" in
ubuntu|debian) ubuntu | debian)
print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source." print_info "On Ubuntu/Debian, the official repo may lack newer filters. Consider a PPA or build from source."
echo "Options:" echo "Options:"
echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg" echo " - ppa: sudo add-apt-repository ppa:savoury1/ffmpeg6 && sudo apt update && sudo apt install ffmpeg"
echo " - source build (recommended for latest): run this script to build from source" echo " - source build (recommended for latest): run this script to build from source"
;; ;;
arch|manjaro|endeavouros) arch | manjaro | endeavouros)
print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg" print_info "On Arch-based distros, ffmpeg is recent. Try: sudo pacman -Syu ffmpeg"
;; ;;
fedora) fedora)
@ -89,12 +89,12 @@ main() {
fi fi
fi fi
# Dependencies # Dependencies
if [[ "$distro" == "ubuntu" || "$distro" == "debian" ]]; then if [[ $distro == "ubuntu" || $distro == "debian" ]]; then
sudo apt update sudo apt update
sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev sudo apt install -y git build-essential yasm nasm pkg-config libx264-dev libx265-dev libvpx-dev libopus-dev libfdk-aac-dev libmp3lame-dev libvorbis-dev libass-dev libfreetype6-dev libgnutls28-dev libaom-dev libdav1d-dev libxvidcore-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxcb-shape0-dev libdrm-dev libvulkan-dev libva-dev libvdpau-dev librtmp-dev libunistring-dev libgnutls28-dev libchromaprint-dev libbluray-dev librubberband-dev libspeex-dev libsoxr-dev libvmaf-dev libzimg-dev libsvtav1-dev libtheora-dev libwebp-dev libopenal-dev libjack-jackd2-dev libpulse-dev librnnoise-dev
elif [[ "$distro" == "arch" || "$distro" == "manjaro" || "$distro" == "endeavouros" ]]; then elif [[ $distro == "arch" || $distro == "manjaro" || $distro == "endeavouros" ]]; then
sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise sudo pacman -Syu --needed base-devel yasm nasm pkgconf rnnoise
elif [[ "$distro" == "fedora" ]]; then elif [[ $distro == "fedora" ]]; then
sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel sudo dnf install -y git make gcc yasm nasm pkgconf-pkg-config rnnoise-devel libX11-devel libXext-devel libXfixes-devel libXv-devel libXrandr-devel libXi-devel libXtst-devel libXinerama-devel freetype-devel fontconfig-devel libass-devel libvpx-devel libaom-devel libdav1d-devel zimg-devel rubberband-devel soxr-devel libvorbis-devel opus-devel lame-devel
else else
echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2 echo "Note: please ensure rnnoise development headers are installed (pkg-config rnnoise)." >&2

View File

@ -10,183 +10,185 @@ BLUE="\033[34m"
RESET="\033[0m" RESET="\033[0m"
info() { info() {
printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*" printf "%b[%s]%b %s\n" "$BLUE" "$SCRIPT_NAME" "$RESET" "$*"
} }
warn() { warn() {
printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2 printf "%b[%s]%b %s\n" "$YELLOW" "$SCRIPT_NAME" "$RESET" "$*" >&2
} }
error() { error() {
printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2 printf "%b[%s]%b %s\n" "$RED" "$SCRIPT_NAME" "$RESET" "$*" >&2
} }
require_command() { require_command() {
local cmd="$1" local cmd="$1"
local package_hint="${2:-}" local package_hint="${2:-}"
if ! command -v "$cmd" >/dev/null 2>&1; then if ! command -v "$cmd" > /dev/null 2>&1; then
if [[ -n "$package_hint" ]]; then if [[ -n $package_hint ]]; then
error "Missing command '$cmd'. Try installing the package: $package_hint" error "Missing command '$cmd'. Try installing the package: $package_hint"
else else
error "Missing command '$cmd'." error "Missing command '$cmd'."
fi fi
exit 1 exit 1
fi fi
} }
ensure_pacman_packages() { ensure_pacman_packages() {
local packages=("python" "git" "curl" "jq" "code") local packages=("python" "git" "curl" "jq" "code")
local missing=() local missing=()
for pkg in "${packages[@]}"; do for pkg in "${packages[@]}"; do
if ! pacman -Qi "$pkg" >/dev/null 2>&1; then if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
missing+=("$pkg") missing+=("$pkg")
fi fi
done done
if (( ${#missing[@]} > 0 )); then if ((${#missing[@]} > 0)); then
info "Installing required packages with pacman: ${missing[*]}" info "Installing required packages with pacman: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}" sudo pacman -S --needed --noconfirm "${missing[@]}"
else else
info "All required pacman packages are already installed." info "All required pacman packages are already installed."
fi fi
} }
install_uv() { install_uv() {
if command -v uv >/dev/null 2>&1; then if command -v uv > /dev/null 2>&1; then
info "uv is already installed." info "uv is already installed."
return return
fi fi
info "Installing uv toolchain manager via official installer." info "Installing uv toolchain manager via official installer."
curl -LsSf https://astral.sh/uv/install.sh | sh curl -LsSf https://astral.sh/uv/install.sh | sh
local local_bin="$HOME/.local/bin" local local_bin="$HOME/.local/bin"
if [[ ":$PATH:" != *":$local_bin:"* ]]; then if [[ :$PATH: != *":$local_bin:"* ]]; then
warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply." warn "Adding $local_bin to PATH in ~/.profile and ~/.zshrc. Open a new shell to apply."
printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$HOME/.profile" printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.profile"
printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$HOME/.zshrc" printf "\nexport PATH=\"\$HOME/.local/bin:\$PATH\"\n" >> "$HOME/.zshrc"
fi fi
} }
ensure_unity_hub() { ensure_unity_hub() {
if command -v unityhub >/dev/null 2>&1; then if command -v unityhub > /dev/null 2>&1; then
info "Unity Hub already installed." info "Unity Hub already installed."
return return
fi fi
if command -v yay >/dev/null 2>&1; then if command -v yay > /dev/null 2>&1; then
info "Installing Unity Hub from AUR using yay." info "Installing Unity Hub from AUR using yay."
yay -S --needed --noconfirm unityhub yay -S --needed --noconfirm unityhub
elif command -v flatpak >/dev/null 2>&1; then elif command -v flatpak > /dev/null 2>&1; then
warn "Unity Hub not found. Attempting Flatpak installation." warn "Unity Hub not found. Attempting Flatpak installation."
flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download" flatpak install -y com.unity.UnityHub || warn "Flatpak installation failed. Install Unity Hub manually via https://unity.com/download"
else else
warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download." warn "Unity Hub not found and neither yay nor flatpak is available. Install Unity Hub manually from https://unity.com/download."
fi fi
} }
sync_unity_mcp_repo() { sync_unity_mcp_repo() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local unity_mcp_root="$data_home/UnityMCP" local unity_mcp_root="$data_home/UnityMCP"
local repo_dir="$unity_mcp_root/unity-mcp-repo" local repo_dir="$unity_mcp_root/unity-mcp-repo"
local server_link="$unity_mcp_root/UnityMcpServer" local server_link="$unity_mcp_root/UnityMcpServer"
local candidates=( local candidates=(
"UnityMcpServer" "UnityMcpServer"
"UnityMcpBridge/UnityMcpServer" "UnityMcpBridge/UnityMcpServer"
"UnityMcpBridge/UnityMcpServer~" "UnityMcpBridge/UnityMcpServer~"
) )
local server_subdir="" local server_subdir=""
mkdir -p "$unity_mcp_root" mkdir -p "$unity_mcp_root"
if [[ -d "$repo_dir/.git" ]]; then if [[ -d "$repo_dir/.git" ]]; then
info "Updating existing unity-mcp repository." info "Updating existing unity-mcp repository."
git -C "$repo_dir" pull --ff-only git -C "$repo_dir" pull --ff-only
else else
info "Cloning unity-mcp repository." info "Cloning unity-mcp repository."
rm -rf "$repo_dir" rm -rf "$repo_dir"
git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir" git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "$repo_dir"
fi fi
for candidate in "${candidates[@]}"; do for candidate in "${candidates[@]}"; do
if [[ -d "$repo_dir/$candidate/src" ]]; then if [[ -d "$repo_dir/$candidate/src" ]]; then
server_subdir="$candidate" server_subdir="$candidate"
break break
fi fi
done done
if [[ -z "$server_subdir" ]]; then if [[ -z $server_subdir ]]; then
error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}" error "UnityMcpServer src directory not found. Checked candidates: ${candidates[*]}"
error "Repository layout may have changed. Inspect $repo_dir for the new server location." error "Repository layout may have changed. Inspect $repo_dir for the new server location."
exit 1 exit 1
fi fi
ln -sfn "$repo_dir/$server_subdir" "$server_link" ln -sfn "$repo_dir/$server_subdir" "$server_link"
info "UnityMcpServer synchronized at $server_link (source: $server_subdir)" info "UnityMcpServer synchronized at $server_link (source: $server_subdir)"
} }
configure_vscode_mcp() { configure_vscode_mcp() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local server_src="$data_home/UnityMCP/UnityMcpServer/src" local server_src="$data_home/UnityMCP/UnityMcpServer/src"
local mcp_config_dir="$HOME/.config/Code/User" local mcp_config_dir="$HOME/.config/Code/User"
local mcp_config="$mcp_config_dir/mcp.json" local mcp_config="$mcp_config_dir/mcp.json"
local tmp local tmp
if [[ ! -d "$server_src" ]]; then if [[ ! -d $server_src ]]; then
error "Server source directory $server_src is missing." error "Server source directory $server_src is missing."
exit 1 exit 1
fi fi
mkdir -p "$mcp_config_dir" mkdir -p "$mcp_config_dir"
if [[ ! -f "$mcp_config" ]]; then if [[ ! -f $mcp_config ]]; then
info "Creating new VS Code MCP configuration at $mcp_config" info "Creating new VS Code MCP configuration at $mcp_config"
echo '{}' > "$mcp_config" echo '{}' > "$mcp_config"
else else
info "Updating existing VS Code MCP configuration at $mcp_config" info "Updating existing VS Code MCP configuration at $mcp_config"
fi fi
tmp="$(mktemp)" tmp="$(mktemp)"
if ! jq '.' "$mcp_config" >/dev/null 2>&1; then if ! jq '.' "$mcp_config" > /dev/null 2>&1; then
error "Existing $mcp_config is not valid JSON. Please fix it before running this script again." error "Existing $mcp_config is not valid JSON. Please fix it before running this script again."
exit 1 exit 1
fi fi
jq \ jq \
--arg path "$server_src" \ --arg path "$server_src" \
'(.servers //= {}) | '(.servers //= {}) |
.servers.unityMCP = { .servers.unityMCP = {
command: "uv", command: "uv",
args: ["--directory", $path, "run", "server.py"], args: ["--directory", $path, "run", "server.py"],
type: "stdio" type: "stdio"
}' \ }' \
"$mcp_config" > "$tmp" "$mcp_config" > "$tmp"
mv "$tmp" "$mcp_config" mv "$tmp" "$mcp_config"
info "VS Code MCP server configuration updated for UnityMCP." info "VS Code MCP server configuration updated for UnityMCP."
} }
verify_python_version() { verify_python_version() {
require_command python "python"
local version require_command python "python"
version="$(python - <<'PY' local version
version="$(
python - << 'PY'
import sys import sys
print("%d.%d.%d" % sys.version_info[:3]) print("%d.%d.%d" % sys.version_info[:3])
PY PY
)" )"
local major minor local major minor
IFS='.' read -r major minor _ <<< "$version" IFS='.' read -r major minor _ <<< "$version"
if (( major < 3 || (major == 3 && minor < 12) )); then if ((major < 3 || (major == 3 && minor < 12))); then
error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing." error "Python 3.12+ is required. Detected version $version. Upgrade python before continuing."
exit 1 exit 1
fi fi
info "Python version $version satisfies requirement (>= 3.12)." info "Python version $version satisfies requirement (>= 3.12)."
} }
print_next_steps() { print_next_steps() {
cat <<'EOT' cat << 'EOT'
Next steps: Next steps:
1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer. 1. Launch Unity Hub and install a Unity Editor version 2021.3 LTS or newer.
@ -210,22 +212,22 @@ EOT
} }
main() { main() {
if [[ ! -f /etc/arch-release ]]; then if [[ ! -f /etc/arch-release ]]; then
error "This script is intended for Arch Linux." error "This script is intended for Arch Linux."
exit 1 exit 1
fi fi
info "Ensuring base dependencies are installed." info "Ensuring base dependencies are installed."
require_command sudo "sudo" require_command sudo "sudo"
require_command pacman "pacman" require_command pacman "pacman"
ensure_pacman_packages ensure_pacman_packages
verify_python_version verify_python_version
install_uv install_uv
ensure_unity_hub ensure_unity_hub
sync_unity_mcp_repo sync_unity_mcp_repo
configure_vscode_mcp configure_vscode_mcp
print_next_steps print_next_steps
info "Setup complete. Follow the next steps above to finish configuration inside Unity." info "Setup complete. Follow the next steps above to finish configuration inside Unity."
} }
main "$@" main "$@"

View File

@ -44,9 +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"; YELLOW="\e[33m"; RED="\e[31m"; BLUE="\e[34m"; BOLD="\e[1m"; RESET="\e[0m" GREEN="\e[32m"
YELLOW="\e[33m"
RED="\e[31m"
BLUE="\e[34m"
BOLD="\e[1m"
RESET="\e[0m"
else else
GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET="" GREEN=""
YELLOW=""
RED=""
BLUE=""
BOLD=""
RESET=""
fi fi
log() { echo -e "${BLUE}[INFO]${RESET} $*"; } log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
@ -55,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.
@ -101,281 +111,376 @@ 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 || { err "Required command '$1' not found"; return 1; } command -v "$1" > /dev/null 2>&1 || {
err "Required command '$1' not found"
return 1
}
} }
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--image) IMAGE="$2"; shift 2;; --image)
--tag) TAG="$2"; shift 2;; IMAGE="$2"
--port) PORT="$2"; shift 2;; shift 2
--host) HOST="$2"; shift 2;; ;;
--data-dir) DATA_DIR="$2"; CACHE_DIR="${DATA_DIR}/cache"; shift 2;; --tag)
--cache-dir) CACHE_DIR="$2"; shift 2;; TAG="$2"
--no-docker-install) DOCKER_INSTALL=0; shift;; shift 2
--keep-alive) KEEP_ALIVE=1; shift;; ;;
--) shift; RUN_COMMAND=("$@"); break;; --port)
--api-key) API_KEY="$2"; GENERATE_API_KEY=0; shift 2;; PORT="$2"
--generate-api-key) GENERATE_API_KEY=1; shift;; shift 2
--disable-api-key) DISABLE_API_KEY=1; shift;; ;;
--preload-langs) PRELOAD_LANGS="$2"; shift 2;; --host)
--env) EXTRA_ENV+=("$2"); shift 2;; HOST="$2"
--pull-only) PULL_ONLY=1; shift;; shift 2
--uninstall) UNINSTALL=1; shift;; ;;
--purge) UNINSTALL=1; KEEP_DATA=0; shift;; --data-dir)
--keep-data) KEEP_DATA=1; shift;; DATA_DIR="$2"
--health-timeout) HEALTH_TIMEOUT="$2"; shift 2;; CACHE_DIR="${DATA_DIR}/cache"
--no-color) NO_COLOR=1; shift;; shift 2
--debug) DEBUG=1; shift;; ;;
-h|--help) usage; exit 0;; --cache-dir)
-v|--version) echo "${VERSION}"; exit 0;; CACHE_DIR="$2"
*) err "Unknown argument: $1"; usage; exit 1;; shift 2
esac ;;
done --no-docker-install)
DOCKER_INSTALL=0
shift
;;
--keep-alive)
KEEP_ALIVE=1
shift
;;
--)
shift
RUN_COMMAND=("$@")
break
;;
--api-key)
API_KEY="$2"
GENERATE_API_KEY=0
shift 2
;;
--generate-api-key)
GENERATE_API_KEY=1
shift
;;
--disable-api-key)
DISABLE_API_KEY=1
shift
;;
--preload-langs)
PRELOAD_LANGS="$2"
shift 2
;;
--env)
EXTRA_ENV+=("$2")
shift 2
;;
--pull-only)
PULL_ONLY=1
shift
;;
--uninstall)
UNINSTALL=1
shift
;;
--purge)
UNINSTALL=1
KEEP_DATA=0
shift
;;
--keep-data)
KEEP_DATA=1
shift
;;
--health-timeout)
HEALTH_TIMEOUT="$2"
shift 2
;;
--no-color)
NO_COLOR=1
shift
;;
--debug)
DEBUG=1
shift
;;
-h | --help)
usage
exit 0
;;
-v | --version)
echo "${VERSION}"
exit 0
;;
*)
err "Unknown argument: $1"
usage
exit 1
;;
esac
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)."; exit 1 err "This script must run as root (or via sudo)."
fi exit 1
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."; exit 1 err "Docker is not installed and --no-docker-install specified."
fi exit 1
log "Installing Docker..." fi
if command -v apt-get >/dev/null 2>&1; then log "Installing Docker..."
apt-get update -y if command -v apt-get > /dev/null 2>&1; then
apt-get install -y ca-certificates curl gnupg apt-get update -y
install -d -m 0755 /etc/apt/keyrings apt-get install -y ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg install -d -m 0755 /etc/apt/keyrings
chmod a+r /etc/apt/keyrings/docker.gpg curl -fsSL "https://download.docker.com/linux/$(
echo \ . /etc/os-release
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(. /etc/os-release; echo "$VERSION_CODENAME") stable" \ echo "$ID"
> /etc/apt/sources.list.d/docker.list )/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
apt-get update -y chmod a+r /etc/apt/keyrings/docker.gpg
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin echo \
else "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(
err "Unsupported package manager. Please install Docker manually."; exit 1 . /etc/os-release
fi echo "$ID"
# Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user ) $(
if command -v systemctl >/dev/null 2>&1; then . /etc/os-release
(systemctl enable --now docker 2>/dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" echo "$VERSION_CODENAME"
else ) stable" \
warn "Docker installed; please ensure docker daemon is running" > /etc/apt/sources.list.d/docker.list
fi apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else
err "Unsupported package manager. Please install Docker manually."
exit 1
fi
# Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user
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"
else
warn "Docker installed; please ensure docker daemon is running"
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 "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"; echo "${API_KEY_LINE}"; {
[[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}"; echo "# LibreTranslate environment file"
for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done; } > "${ENV_FILE}.tmp" echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"
mv "${ENV_FILE}.tmp" "${ENV_FILE}" echo "${API_KEY_LINE}"
chmod 600 "${ENV_FILE}" [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}"
success "Environment file written: ${ENV_FILE}" for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done
} > "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
chmod 600 "${ENV_FILE}"
success "Environment file written: ${ENV_FILE}"
} }
start_container_ephemeral() { start_container_ephemeral() {
log "Starting ephemeral container..." 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=$(date +%s) local start
local url="http://127.0.0.1:${PORT}/languages" start=$(date +%s)
local attempt=0 local url="http://127.0.0.1:${PORT}/languages"
while true; do local attempt=0
attempt=$((attempt+1)) while true; do
if curl ${DEBUG:+-v} -fsS "$url" >/dev/null 2>&1; then attempt=$((attempt + 1))
success "Service healthy (attempt $attempt)" if curl ${DEBUG:+-v} -fsS "$url" > /dev/null 2>&1; then
return 0 success "Service healthy (attempt $attempt)"
else return 0
[[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" else
fi [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed"
if (( $(date +%s) - start > HEALTH_TIMEOUT )); then fi
err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" if (($(date +%s) - start > HEALTH_TIMEOUT)); then
docker logs --tail 200 "${SERVICE_NAME}" || true err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)"
return 1 docker logs --tail 200 "${SERVICE_NAME}" || true
fi return 1
sleep 0.5 fi
done sleep 0.5
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 [[ -n ${PRELOAD_LANGS} ]] && echo "Preloaded languages requested: ${PRELOAD_LANGS}" || true
echo "Environment file: ${ENV_FILE}" echo "Environment file: ${ENV_FILE}"
echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}"
echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall"
echo echo
} }
main "$@" main "$@"

View File

@ -1,53 +1,52 @@
#!/bin/bash #!/bin/bash
process_table_schema() { process_table_schema() {
while IFS=$'\t' read -r column_name _ data_type _; do while IFS=$'\t' read -r column_name _ data_type _; do
# Print the column name and data type # Print the column name and data type
echo -e "$column_name\t$data_type" printf '%s\t%s\n' "$column_name" "$data_type"
done < "$1" done < "$1"
} }
input_file="$1" input_file="$1"
# Check if a file is provided as an argument # Check if a file is provided as an argument
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "Usage: $0 <filename>" echo "Usage: $0 <filename>"
exit 1 exit 1
fi fi
# Process the provided file and skip the first row # Process the provided file and skip the first row
first_line=true first_line=true
process_table_schema "$input_file" | while IFS=$'\t' read -r column_name data_type; do process_table_schema "$input_file" | while IFS=$'\t' read -r column_name data_type; do
if [ "$first_line" = true ]; then if [ "$first_line" = true ]; then
first_line=false first_line=false
continue continue
fi fi
case "$data_type" in case "$data_type" in
"timestamp") "timestamp")
sqlalchemy_type="DateTime" sqlalchemy_type="DateTime"
;; ;;
"int"|"integer"|"int4") "int" | "integer" | "int4")
sqlalchemy_type="Integer" sqlalchemy_type="Integer"
;; ;;
"varchar"*|"text") "varchar"* | "text")
sqlalchemy_type="String" # handles types like varchar(256) sqlalchemy_type="String" # handles types like varchar(256)
;; ;;
"boolean"|"bool") "boolean" | "bool")
sqlalchemy_type="Boolean" sqlalchemy_type="Boolean"
;; ;;
"float"|"float8") "float" | "float8")
sqlalchemy_type="Float" sqlalchemy_type="Float"
;; ;;
"serial4") "serial4")
sqlalchemy_type="Integer" sqlalchemy_type="Integer"
;; ;;
"numeric"*) "numeric"*)
sqlalchemy_type="Numeric" # handles types like numeric(12, 2) sqlalchemy_type="Numeric" # handles types like numeric(12, 2)
;; ;;
*) *)
sqlalchemy_type="UNDEFINED_CHANGE_ME" # default to UNDEFINED_CHANGE_ME if data type is unrecognized sqlalchemy_type="UNDEFINED_CHANGE_ME" # default to UNDEFINED_CHANGE_ME if data type is unrecognized
;; ;;
esac esac
echo "$column_name = Column($sqlalchemy_type)" echo "$column_name = Column($sqlalchemy_type)"
done done

View File

@ -14,7 +14,7 @@ PY_RUNNER="$TOOLS_DIR/transcribe_fw.py"
VENV_DIR="$PROJECT_DIR/.venv" VENV_DIR="$PROJECT_DIR/.venv"
usage() { usage() {
cat <<USAGE cat << USAGE
Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file] Usage: $(basename "$0") [--online] [--prepare-model NAME --model-dir DIR] [-m model] [-l lang] [-o outdir] [audio_file]
Options: Options:
@ -30,261 +30,288 @@ 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 echo apt; return; fi if command -v apt-get > /dev/null 2>&1; then
if command -v dnf >/dev/null 2>&1; then echo dnf; return; fi echo apt
if command -v yum >/dev/null 2>&1; then echo yum; return; fi return
if command -v pacman >/dev/null 2>&1; then echo pacman; return; fi fi
if command -v zypper >/dev/null 2>&1; then echo zypper; return; fi if command -v dnf > /dev/null 2>&1; then
echo none echo dnf
return
fi
if command -v yum > /dev/null 2>&1; then
echo yum
return
fi
if command -v pacman > /dev/null 2>&1; then
echo pacman
return
fi
if command -v zypper > /dev/null 2>&1; then
echo zypper
return
fi
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 [[ -e "$d/libcublas.so.12" ]] && return 0 || true
done done
# venv-provided NVIDIA CUDA libs # venv-provided NVIDIA CUDA libs
if [[ -x "$VENV_DIR/bin/python" ]]; then if [[ -x "$VENV_DIR/bin/python" ]]; then
local pyver local pyver
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
for d in "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cublas/lib" \ for d in "$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/cudnn/lib" \
"$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do "$VENV_DIR/lib/python$pyver/site-packages/nvidia/cuda_runtime/lib"; do
[[ -e "$d/libcublas.so.12" ]] && return 0 || true [[ -e "$d/libcublas.so.12" ]] && return 0 || true
done done
fi fi
fi fi
return 1 return 1
} }
ensure_cuda_runtime() { ensure_cuda_runtime() {
local mgr; mgr="$(detect_pkg_mgr)" local mgr
if [[ $OFFLINE -eq 1 ]]; then mgr="$(detect_pkg_mgr)"
if has_libcublas12; then return 0; fi if [[ $OFFLINE -eq 1 ]]; then
echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2 if has_libcublas12; then return 0; fi
exit 6 echo "CUDA runtime (libcublas.so.12) not found and offline mode is enabled. Install CUDA 12 runtime or rerun with --online." >&2
fi exit 6
if has_libcublas12; then fi
return 0 if has_libcublas12; then
fi return 0
if ! command -v sudo >/dev/null 2>&1; then fi
log "sudo not found; skipping CUDA runtime install attempt." if ! command -v sudo > /dev/null 2>&1; then
else log "sudo not found; skipping CUDA runtime install attempt."
log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)" else
set +e log "CUDA cuBLAS 12 not found; attempting to install CUDA runtime (manager: $mgr)"
case "$mgr" in set +e
pacman) case "$mgr" in
sudo pacman -Sy --noconfirm cuda cudnn || true ;; pacman)
apt) sudo pacman -Sy --noconfirm cuda cudnn || true
sudo apt-get update -y || true ;;
sudo apt-get install -y nvidia-cuda-toolkit || true ;; apt)
dnf|yum) sudo apt-get update -y || true
sudo "$mgr" install -y cuda cudnn || true ;; sudo apt-get install -y nvidia-cuda-toolkit || true
zypper) ;;
sudo zypper install -y cuda cudnn || true ;; dnf | yum)
*) log "Unknown package manager; cannot install CUDA automatically." ;; sudo "$mgr" install -y cuda cudnn || true
esac ;;
set -e zypper)
fi sudo zypper install -y cuda cudnn || true
# Re-check ;;
if ! has_libcublas12; then *) log "Unknown package manager; cannot install CUDA automatically." ;;
echo "CUDA runtime (libcublas.so.12) not found after attempted install. Please install CUDA 12 toolkit/runtime and re-run." >&2 esac
exit 6 set -e
fi fi
# Re-check
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
exit 6
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; mgr="$(detect_pkg_mgr)" local 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))" 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))"
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) ;;
pkgs=(python3-venv python3-pip) dnf)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) pkgs=(python3-venv python3-pip)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing" ;; [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
yum) sudo dnf install -y "${pkgs[@]}" || log "dnf install failed; continuing"
pkgs=(python3-venv python3-pip) ;;
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) yum)
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) pkgs=(python3-venv python3-pip)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing" ;; [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
pacman) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
pkgs=(python-virtualenv python-pip) sudo yum install -y "${pkgs[@]}" || log "yum install failed; continuing"
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) ;;
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) pacman)
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile) pkgs=(python-virtualenv python-pip)
sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing" ;; [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
zypper) [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
pkgs=(python311-virtualenv python311-pip) [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile)
[[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg) sudo pacman -Sy --noconfirm "${pkgs[@]}" || log "pacman install failed; continuing"
[[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng) ;;
[[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1) zypper)
sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing" ;; pkgs=(python311-virtualenv python311-pip)
*) [[ $need_ffmpeg -eq 1 ]] && pkgs+=(ffmpeg)
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed." ;; [[ $need_espeak -eq 1 ]] && pkgs+=(espeak-ng)
esac [[ $need_libsndfile -eq 1 ]] && pkgs+=(libsndfile1)
set -e sudo zypper install -y "${pkgs[@]}" || log "zypper install failed; continuing"
;;
*)
log "Unknown package manager; please ensure ffmpeg and espeak-ng are installed."
;;
esac
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')
@ -296,135 +323,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: -- "$@") || { usage; exit 2; } PARSED=$(getopt -o m:l:o:h -l online,prepare-model:,model-dir: -- "$@") || {
eval set -- "$PARSED" usage
while true; do exit 2
case "$1" in }
-m) MODEL="$2"; shift 2;; eval set -- "$PARSED"
-l) LANGUAGE="$2"; shift 2;; while true; do
-o) OUTDIR="$2"; shift 2;; case "$1" in
-h) usage; exit 0;; -m)
--online) OFFLINE=0; shift;; MODEL="$2"
--prepare-model) PREPARE_MODEL="$2"; OFFLINE=0; shift 2;; shift 2
--model-dir) MODEL_DIR="$2"; shift 2;; ;;
--) shift; break;; -l)
*) break;; LANGUAGE="$2"
esac shift 2
done ;;
INPUT_FILE="${1:-}" -o)
OUTDIR="$2"
shift 2
;;
-h)
usage
exit 0
;;
--online)
OFFLINE=0
shift
;;
--prepare-model)
PREPARE_MODEL="$2"
OFFLINE=0
shift 2
;;
--model-dir)
MODEL_DIR="$2"
shift 2
;;
--)
shift
break
;;
*) break ;;
esac
done
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.")' || { echo "CUDA environment check failed. Aborting as requested." >&2; exit 6; } python -c 'from faster_whisper import WhisperModel; WhisperModel("tiny", device="cuda", compute_type="float16"); print("[PY] CUDA test init succeeded.")' || {
args+=("--device" "cuda") echo "CUDA environment check failed. Aborting as requested." >&2
fi exit 6
}
args+=("--device" "cuda")
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 "$@"