mirror of
https://github.com/kuhyx/scripts.git
synced 2026-07-04 14:23:08 +02:00
chore: fix shell check issues
This commit is contained in:
parent
f11ce4002f
commit
6d78878b99
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@ -40,3 +40,4 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s 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.
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user