#!/data/data/com.termux/files/usr/bin/env bash # # KoboldCpp Termux Interactive Installer & Manager # Version 3.1 (with Model Cache Management enhancements) # # --- Usage & Requirements --- # # Usage: # bash koboldcpp_manager.sh # # Requirements: # - Termux environment on Android (v0.118+ recommended) # - Bash shell # - Internet connection for downloading KoboldCpp and models # - Termux:API package (optional, but recommended for wake lock functionality) # Install with: pkg install termux-api # # For more advanced KoboldCpp setup and options, refer to the official documentation: # https://github.com/LostRuins/koboldcpp # # --- Prevent Sourcing --- if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then echo "This script is meant to be executed, not sourced." >&2 if (return 0 2>/dev/null); then return 1; else exit 1; fi fi # --- Script Configuration --- set -euo pipefail # Exit on error, undefined variable, and pipe failure. # --- Early Initializations --- # Global flag to ensure log directory creation is attempted only once. LOG_FILE_DIR_INIT_ATTEMPTED=false # Function to ensure the log directory exists _ensure_log_dir_exists_once() { if [ "$LOG_FILE_DIR_INIT_ATTEMPTED" = true ]; then return fi local current_log_file_path="$HOME/koboldcpp_install.log" local log_dir log_dir=$(dirname "$current_log_file_path") if ! mkdir -p "$log_dir"; then echo "CRITICAL ERROR: Could not create log directory '$log_dir'. Logging will fail. Aborting." >&2 exit 1 fi LOG_FILE_DIR_INIT_ATTEMPTED=true } _ensure_log_dir_exists_once # Call immediately # --- Global Readonly Variables --- readonly KOBOLDCPP_DIR="$HOME/koboldcpp" readonly MODEL_DIR="$KOBOLDCPP_DIR/models" # This is your model cache directory readonly EXPECTED_MODEL_DIR_PATH="$HOME/koboldcpp/models" readonly DOWNLOAD_DIR="$HOME/storage/downloads" readonly BACKUP_DIR="$HOME/koboldcpp_backup" readonly LOG_FILE="$HOME/koboldcpp_install.log" readonly REPO_URL="https://github.com/LostRuins/koboldcpp.git" readonly DEFAULT_BRANCH="main" readonly CONFIG_FILE="$HOME/.koboldcpp_config" readonly VERSION="3.1" # Colors for prettier output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[0;33m' readonly BLUE='\033[0;34m' readonly CYAN='\033[0;36m' readonly BOLD='\033[1m' readonly NC='\033[0m' # No Color # --- Portable File Size Command Initialization --- DU_BYTES_CMD="" DU_BYTES_TYPE="unknown" # 'du_b', 'du_bytes', 'stat', or 'unknown' _initialize_du_commands() { local test_file_path="/proc/self/exe" # Generally reliable link local temp_file_created=false if [ ! -r "$test_file_path" ] || [ ! -f "$test_file_path" ]; then test_file_path=$(mktemp) if [ -z "$test_file_path" ] || [ ! -f "$test_file_path" ]; then if typeset -f warn > /dev/null; then warn "mktemp failed for 'du' test."; else echo "Warning: mktemp failed for 'du' test." >&2; fi DU_BYTES_TYPE="unknown" return fi temp_file_created=true trap '_cleanup_temp_file "$test_file_path"; trap - EXIT' EXIT fi if du -b "$test_file_path" >/dev/null 2>&1; then DU_BYTES_CMD="du -b" DU_BYTES_TYPE="du_b" elif du --bytes "$test_file_path" >/dev/null 2>&1; then DU_BYTES_CMD="du --bytes" DU_BYTES_TYPE="du_bytes" elif stat -c %s "$test_file_path" >/dev/null 2>&1; then DU_BYTES_CMD="stat -c %s" DU_BYTES_TYPE="stat" else DU_BYTES_TYPE="unknown" if typeset -f warn > /dev/null; then warn "No reliable command for exact byte counts found."; else echo "Warning: No reliable command for exact byte counts found." >&2; fi fi if [ "$temp_file_created" = true ]; then _cleanup_temp_file "$test_file_path" trap - EXIT # Clear the specific trap if we cleaned up fi } _cleanup_temp_file() { if [ -n "${1:-}" ] && [ -f "$1" ]; then rm -f "$1" fi } _initialize_du_commands # Initialize once # --- Utility functions --- log() { echo -e "${GREEN}→${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE" } error() { echo -e "${RED}❌ ERROR:${NC} $1" >&2 echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" return 1 } success() { echo -e "${GREEN}✅${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE" } warn() { echo -e "${YELLOW}⚠️ WARNING:${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE" } info() { echo -e "${BLUE}ℹ️ INFO:${NC} $1" } # Core pause logic. _pause_script_execution() { local clear_behavior="${1:-clear}" # Default to "clear" echo echo -n "Press Enter to continue..." read -r if [ "$clear_behavior" = "clear" ]; then clear fi } # Pauses script and keeps screen content. wait_for_keypress_keep_screen() { _pause_script_execution "keep" } # Pauses script and clears screen (default for progressing through menus/steps). press_enter() { _pause_script_execution "clear" } clear_screen() { clear } # Function to save user configurations save_config() { local key="$1" local value="$2" # Create config file if it doesn't exist touch "$CONFIG_FILE" # Check if key already exists, update if it does, add if it doesn't if grep -q "^$key=" "$CONFIG_FILE"; then sed -i "s/^$key=.*/$key=$value/" "$CONFIG_FILE" else echo "$key=$value" >> "$CONFIG_FILE" fi } # Function to load user configurations load_config() { local key="$1" local default="$2" if [ -f "$CONFIG_FILE" ] && grep -q "^$key=" "$CONFIG_FILE"; then grep "^$key=" "$CONFIG_FILE" | cut -d= -f2 else echo "$default" fi } get_file_size_bytes() { local filepath="${1:-}" if [ -z "$filepath" ] || [ ! -e "$filepath" ]; then echo "0" return fi local size_output="" case "$DU_BYTES_TYPE" in "du_b") size_output=$(du -b "$filepath" 2>/dev/null | cut -f1) ;; "du_bytes") size_output=$(du --bytes "$filepath" 2>/dev/null | cut -f1) ;; "stat") size_output=$(stat -c %s "$filepath" 2>/dev/null) ;; *) local size_k size_k=$(du -k "$filepath" 2>/dev/null | cut -f1) if [[ "$size_k" =~ ^[0-9]+$ ]]; then size_output=$((size_k * 1024)) else size_output="0" fi ;; esac echo "${size_output:-0}" # Ensure output, default to 0 if empty } get_file_size_human() { local filepath="${1:-}" if [ -z "$filepath" ] || [ ! -e "$filepath" ]; then echo "N/A" return fi du -h "$filepath" 2>/dev/null | cut -f1 || echo "N/A" } # Check if a model is corrupted (basic checks) # Returns 0 for likely OK, 1 for likely corrupted. check_model_integrity() { local model_path="$1" local model_name=$(basename "$model_path") # Simple size check - empty or very small files are likely corrupted local size_bytes=$(get_file_size_bytes "$model_path") if [ "$size_bytes" -lt 10240 ]; then # Less than 10KB warn "Model $model_name appears to be too small ($(get_file_size_human "$model_path")). It may be corrupted." return 1 fi # Check file header for GGUF signature (basic check, requires hexdump) if command -v hexdump >/dev/null 2>&1; then # Read the first 8 bytes and check for the GGUF magic number (0x46554747) # This is "GGUF" in ASCII, followed by version number (typically 0x00000003 or 0x00000002) # We check for 47 47 55 46 (GGUF) followed by any 4 bytes. local header=$(hexdump -n 8 -e '1/1 "%02x"' "$model_path" 2>/dev/null) if [[ "$header" != "47475546"* ]]; then warn "Model $model_name does not start with a valid GGUF magic number. It may be corrupted or not a GGUF file." return 1 fi else # If hexdump is not available, we can only do the size check if [ "$size_bytes" -lt 1000000 ]; then # Arbitrary larger size threshold if no hexdump warn "Could not verify GGUF header (hexdump not found). Model $model_name is relatively small ($(get_file_size_human "$model_path")) and might be incomplete or corrupted." return 1 fi fi # If basic checks pass return 0 } # Validates critical directory paths against expected values and common pitfalls. _validate_critical_directory_path() { local dir_to_check="${1:-}" local expected_path="${2:-}" local operation_name="${3:-operation}" # e.g., "backup", "restore" if [ -z "$dir_to_check" ]; then error "Safety check failed for $operation_name: Directory path is empty." return 1 fi if [ "$dir_to_check" = "/" ] || [ "$dir_to_check" = "$HOME" ]; then error "Safety check failed for $operation_name: Directory path ('$dir_to_check') is a root or home directory." return 1 fi # Disabled exact path check for more flexibility, rely on starts-with and critical path checks # if [ -n "$expected_path" ] && [ "$dir_to_check" != "$expected_path" ]; then # error "Safety check failed for $operation_name: Directory path ('$dir_to_check') does not match expected path ('$expected_path')." # return 1 # fi # Add check to prevent operations outside HOME if [[ ! "$dir_to_check" == "$HOME"* ]]; then error "Safety check failed for $operation_name: Directory path ('$dir_to_check') is outside the home directory." return 1 fi return 0 } # Function to check available storage space check_available_space() { local required_space="$1" # in bytes local directory="$2" if [ ! -d "$directory" ]; then warn "Target directory '$directory' does not exist. Cannot check available space." return 0 # Assume enough space if dir doesn't exist yet (mkdir -p will create) fi # Get available space in the target directory local available_space=0 if command -v df >/dev/null 2>&1; then available_space=$(df -B1 "$directory" | awk 'NR==2 {print $4}') else # If df is not available, assume there's enough space warn "df command not found. Cannot reliably check available storage space." return 0 fi if [ -z "$available_space" ] || [ "$available_space" -lt "$required_space" ]; then local required_human=$(numfmt --to=iec-i --suffix=B "$required_space" 2>/dev/null || echo "$required_space bytes") local available_human=$(numfmt --to=iec-i --suffix=B "$available_space" 2>/dev/null || echo "$available_space bytes") warn "Not enough storage space in '$directory'." warn "Required: $required_human, Available: $available_human" return 1 fi return 0 } # Find device CPU info get_device_info() { local device_info="" # Get CPU model if [ -f "/proc/cpuinfo" ]; then local cpu_model=$(grep "model name\|Processor" /proc/cpuinfo | head -n1 | cut -d: -f2 | xargs) if [ -n "$cpu_model" ]; then device_info="CPU: $cpu_model" fi fi # Get total RAM if [ -f "/proc/meminfo" ]; then local total_ram=$(grep "MemTotal" /proc/meminfo | awk '{print $2}') if [ -n "$total_ram" ]; then local ram_gb=$(awk -v ram="$total_ram" 'BEGIN {printf "%.2f", ram/1024/1024}') device_info="$device_info, RAM: ${ram_gb}GB" fi fi # Get Android version if available if command -v getprop >/dev/null 2>&1; then local android_ver=$(getprop ro.build.version.release) if [ -n "$android_ver" ]; then device_info="$device_info, Android: $android_ver" fi fi echo "$device_info" } # --- Prerequisite Checks --- check_install() { local py_script_exists=false local binary_exists=false if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then py_script_exists=true fi if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then binary_exists=true fi if [ ! -d "$KOBOLDCPP_DIR" ] || { [ "$py_script_exists" = false ] && [ "$binary_exists" = false ]; }; then echo -e "${YELLOW}KoboldCpp is not installed or key components are missing.${NC}" echo echo -n "Would you like to install/reinstall it now? [Y/n]: " read -r INSTALL_CHOICE if [[ "${INSTALL_CHOICE:-Y}" =~ ^[Nn]$ ]]; then # Default to Y error "KoboldCpp installation is required to continue." wait_for_keypress_keep_screen return 1 else install_all return $? fi fi return 0 } check_termux_storage() { if [ ! -d "$DOWNLOAD_DIR" ]; then log "Setting up Termux storage access..." if command -v termux-setup-storage &>/dev/null; then termux-setup-storage else error "termux-setup-storage command not found. Please ensure Termux:API is installed and configured." info "You might need to run: pkg install termux-api && termux-setup-storage" wait_for_keypress_keep_screen return 1 fi for i in {1..10}; do if [ -d "$DOWNLOAD_DIR" ]; then break fi log "Waiting for storage access... ($i/10)" sleep 1 done if [ ! -d "$DOWNLOAD_DIR" ]; then error "Storage access not granted or Downloads directory '$DOWNLOAD_DIR' not found." info "Please ensure you granted storage permission and the directory exists." wait_for_keypress_keep_screen return 1 fi fi return 0 } # Check internet connectivity check_internet() { log "Checking internet connectivity..." if ping -c 1 8.8.8.8 >/dev/null 2>&1 || ping -c 1 google.com >/dev/null 2>&1; then return 0 else error "No internet connection detected. Some functions may not work properly." return 1 fi } # --- Core Functionality --- install_all() { clear_screen echo -e "${BOLD}${CYAN}=== Installing KoboldCpp ===${NC}" echo log "Starting KoboldCpp installation..." if command -v termux-wake-lock &>/dev/null; then log "Acquiring wake lock to prevent sleep..." termux-wake-lock else warn "termux-api not installed or termux-wake-lock not available. Consider 'pkg install termux-api'." fi check_termux_storage || { return 1; } # Check internet connection before proceeding check_internet log "Updating package lists..." pkg update -y || { error "Failed to update package lists"; wait_for_keypress_keep_screen; return 1; } log "Upgrading installed packages..." pkg upgrade -y || { warn "Failed to upgrade packages. Continuing, but this might lead to issues."; } log "Installing dependencies: openssl wget git python build-essential clang cmake libjpeg-turbo..." pkg install -y openssl wget git python build-essential clang cmake libjpeg-turbo || { error "Failed to install core packages"; wait_for_keypress_keep_screen; return 1; } if ! command -v termux-wake-lock &>/dev/null; then log "Attempting to install termux-api for better system integration..." pkg install -y termux-api || warn "Could not install termux-api, continuing anyway." fi if [ -d "$KOBOLDCPP_DIR/.git" ]; then log "KoboldCpp repository already exists, updating..." cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; } git fetch --all --prune || { error "Failed to fetch updates"; wait_for_keypress_keep_screen; return 1; } git reset --hard "origin/$DEFAULT_BRANCH" || { error "Failed to update to latest version of branch '$DEFAULT_BRANCH'"; wait_for_keypress_keep_screen; return 1; } else log "Cloning KoboldCpp repository (branch: $DEFAULT_BRANCH)..." git clone --depth 1 --branch "$DEFAULT_BRANCH" "$REPO_URL" "$KOBOLDCPP_DIR" || { error "Failed to clone repository"; wait_for_keypress_keep_screen; return 1; } cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR' after cloning"; wait_for_keypress_keep_screen; return 1; } fi mkdir -p "$MODEL_DIR" || { error "Failed to create models directory: '$MODEL_DIR'"; wait_for_keypress_keep_screen; return 1; } # Get system information for optimized build local device_info=$(get_device_info) log "Building for: $device_info" log "Building KoboldCpp with portable settings for Android..." export LLAMA_PORTABLE=1 export KOBOLDCPP_PLATFORM="android" log "Cleaning previous build files (if any)..." make clean || warn "Failed to clean build files, continuing anyway." log "Building with optimizations for your device..." make -j$(nproc) || { warn "Build failed with default optimizations, trying with safer settings (LLAMA_NO_ACCELERATE=1)..." make clean make -j$(nproc) LLAMA_NO_ACCELERATE=1 || { warn "Build failed with first fallback, trying with fewer threads..." make clean make LLAMA_NO_ACCELERATE=1 || { error "Build failed with all fallback options. Please check the error messages above." wait_for_keypress_keep_screen return 1 } } } local py_script_exists=false local binary_exists=false if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then py_script_exists=true fi if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then binary_exists=true fi if [ "$py_script_exists" = false ] && [ "$binary_exists" = false ]; then error "Build verification failed. Neither koboldcpp.py nor a compiled binary (e.g., koboldcpp-android-arm64 or koboldcpp) was found." info "Please check the build logs in '$LOG_FILE' for errors." wait_for_keypress_keep_screen return 1 fi # Save build information to help with future builds if [ "$binary_exists" = true ]; then # local build_success="1" # This variable was declared but not used. Removed. save_config "last_successful_build" "$(date +%Y%m%d)" # Save the last used build option (0 for default, 1 for LLAMA_NO_ACCELERATE=1) # Check if LLAMA_NO_ACCELERATE is set to 1 in the environment or last command if [[ "${build_command:-}" == *"LLAMA_NO_ACCELERATE=1"* ]]; then save_config "build_options" "1" else save_config "build_options" "0" fi fi success "KoboldCpp installation complete!" if command -v termux-wake-unlock &>/dev/null; then log "Releasing wake lock..." termux-wake-unlock fi press_enter return 0 } add_model() { clear_screen echo -e "${BOLD}${CYAN}=== Add Model from Downloads ===${NC}" echo log "This copies a .gguf model file from your Termux Downloads directory ($DOWNLOAD_DIR) to the KoboldCpp models directory ($MODEL_DIR)." echo check_install || { return 1; } check_termux_storage || { return 1; } # Search for models recursively in Downloads echo -e "${YELLOW}Searching for .gguf files in Downloads directory...${NC}" local FILES=() local SEARCH_DEPTH=3 # Search up to 3 levels deep # Use a safer find command with -print0 and read -d $'\0' while IFS= read -r -d $'\0' file; do # Filter out files directly in DOWNLOAD_DIR if they don't end with .gguf - already done by -name "*.gguf" # Additional check to prevent adding files already in MODEL_DIR (unlikely if copying, but safe) # if [[ "$(dirname "$file")" == "$MODEL_DIR" ]]; then # continue # Skip files already in the target directory # fi FILES+=("$file") done < <(find "$DOWNLOAD_DIR" -maxdepth "$SEARCH_DEPTH" -name "*.gguf" -type f -print0 2>/dev/null | sort -z) if [ ${#FILES[@]} -eq 0 ]; then error "No .gguf files found in your Downloads directory (searched $SEARCH_DEPTH levels deep)." echo "Please download a model (ending with .gguf) to that directory first." press_enter return 1 fi echo -e "${YELLOW}Found models:${NC}" for i in "${!FILES[@]}"; do local FILE="${FILES[$i]}" local FILENAME FILENAME=$(basename "$FILE") local REL_PATH=${FILE#"$DOWNLOAD_DIR/"} local SIZE SIZE=$(get_file_size_human "$FILE") echo "[$i] $FILENAME ($SIZE)" echo " Location: $REL_PATH" done echo echo -n "Enter number of the model to add (or 'q' to return to menu): " read -r CHOICE if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then # Handle empty CHOICE return 0 fi if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#FILES[@]}" ]; then error "Invalid selection." wait_for_keypress_keep_screen return 1 fi local SRC="${FILES[$CHOICE]}" local FILENAME FILENAME=$(basename "$SRC") local DEST="$MODEL_DIR/$FILENAME" if [ -f "$DEST" ]; then echo -n "File '$FILENAME' already exists in models directory. Overwrite? [y/N]: " read -r CONFIRM if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default to N info "Operation cancelled." press_enter return 0 fi fi # Check for available space before copying local file_size=$(get_file_size_bytes "$SRC") check_available_space "$file_size" "$MODEL_DIR" || { error "Not enough space to copy the model. Please free up some storage." wait_for_keypress_keep_screen return 1 } log "Copying '$FILENAME' to models directory..." mkdir -p "$MODEL_DIR" if cp "$SRC" "$DEST"; then success "Model '$FILENAME' copied to '$MODEL_DIR'." # Verify the copied model integrity log "Verifying integrity of copied model..." if check_model_integrity "$DEST"; then success "Integrity check passed for $FILENAME." else warn "Integrity check failed for $FILENAME. The copied model may be corrupted." warn "Consider deleting it and adding it again, or re-downloading the original file." wait_for_keypress_keep_screen fi else error "Failed to copy model file '$FILENAME'. Check permissions and storage space." wait_for_keypress_keep_screen return 1 fi press_enter return 0 } download_model() { clear_screen echo -e "${BOLD}${CYAN}=== Download Model ===${NC}" echo log "This downloads a .gguf model file directly to the KoboldCpp models directory ($MODEL_DIR)." echo check_install || { return 1; } check_internet || { error "Internet connection is required to download models." wait_for_keypress_keep_screen return 1 } # Show recently used URLs if they exist local recent_urls=() local config_urls=$(grep "^recent_url_" "$CONFIG_FILE" 2>/dev/null | cut -d= -f2) if [ -n "$config_urls" ]; then echo -e "${YELLOW}Recently used URLs:${NC}" local url_index=0 while IFS= read -r url; do recent_urls+=("$url") echo "[$url_index] $url" ((url_index++)) done <<< "$config_urls" echo "[n] Enter a new URL" echo echo -n "Select an option: " read -r URL_CHOICE local URL="" if [[ "$URL_CHOICE" =~ ^[0-9]+$ ]] && [ "$URL_CHOICE" -ge 0 ] && [ "$URL_CHOICE" -lt "${#recent_urls[@]}" ]; then URL="${recent_urls[$URL_CHOICE]}" info "Selected recent URL: $URL" else echo "Enter a direct download URL for the model file (must be a .gguf file):" echo -n "> " read -r URL # Add new valid URL to recent list if [ -n "$URL" ] && [[ "$URL" == *.gguf* ]]; then save_config "recent_url_${#recent_urls[@]}" "$URL" fi fi else echo "Enter a direct download URL for the model file (must be a .gguf file):" echo -n "> " read -r URL # Add new valid URL to recent list if [ -n "$URL" ] && [[ "$URL" == *.gguf* ]]; then save_config "recent_url_0" "$URL" fi fi if [ -z "${URL:-}" ]; then info "Operation cancelled (empty URL)." press_enter return 0 fi local FILENAME FILENAME=$(basename "${URL:-}") # Handle potentially empty URL FILENAME="${FILENAME%%\?*}" # Remove query string parameters if [ -z "$FILENAME" ] || [[ ! "$FILENAME" == *.gguf ]]; then warn "Couldn't determine a valid .gguf filename from the URL ('$FILENAME')." echo -n "Please enter a filename for this model (should end with .gguf): " read -r CUSTOM_FILENAME if [ -z "${CUSTOM_FILENAME:-}" ]; then info "Operation cancelled (empty filename)." press_enter return 0 fi FILENAME="$CUSTOM_FILENAME" fi if [[ ! "$FILENAME" == *.gguf ]]; then FILENAME="${FILENAME}.gguf" info "Added .gguf extension. Filename will be: $FILENAME" fi local DEST="$MODEL_DIR/$FILENAME" if [ -f "$DEST" ]; then echo -n "File '$FILENAME' already exists in models directory. Overwrite? [y/N]: " read -r CONFIRM if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N info "Operation cancelled." press_enter return 0 fi fi # Check file size before downloading if possible local file_size=0 if command -v curl >/dev/null 2>&1; then log "Checking file size before download..." # Use -s silent, -I head request, -L follow redirects, grep Content-Length, awk to get value, tr to remove carriage return local size_output=$(curl -sIL "$URL" 2>/dev/null | grep -i Content-Length | tail -n 1 | awk '{print $2}' | tr -d '\r') if [ -n "$size_output" ] && [[ "$size_output" =~ ^[0-9]+$ ]]; then file_size="$size_output" fi if [ "$file_size" -gt 0 ]; then local size_human=$(numfmt --to=iec-i --suffix=B "$file_size" 2>/dev/null || echo "$file_size bytes") info "Estimated model size: $size_human" # Check available space check_available_space "$file_size" "$MODEL_DIR" || { error "Not enough space to download the model. Please free up some storage." wait_for_keypress_keep_screen return 1 } else warn "Could not reliably determine model size. Proceeding anyway, but ensure sufficient space is available." # As a fallback, check if there's at least 5GB free (a common minimum for larger models) check_available_space 5368709120 "$MODEL_DIR" || { # 5GB in bytes warn "Less than 5GB free space detected and could not get exact file size. Download might fail due to lack of space." echo -n "Continue download anyway? [y/N]: " read -r FORCE_DOWNLOAD if [[ ! "${FORCE_DOWNLOAD:-N}" =~ ^[Yy]$ ]]; then info "Download cancelled due to potential space issues." press_enter return 1 fi } fi else warn "curl command not found. Cannot check file size before downloading. Proceeding anyway, but ensure sufficient space is available." # As a fallback, check if there's at least 5GB free (a common minimum for larger models) check_available_space 5368709120 "$MODEL_DIR" || { # 5GB in bytes warn "Less than 5GB free space detected and could not get exact file size. Download might fail due to lack of space." echo -n "Continue download anyway? [y/N]: " read -r FORCE_DOWNLOAD if [[ ! "${FORCE_DOWNLOAD:-N}" =~ ^[Yy]$ ]]; then info "Download cancelled due to potential space issues." press_enter return 1 fi } fi log "Downloading model from: $URL" log "This may take a while depending on file size and your connection." log "Model will be saved to: $DEST" if command -v termux-wake-lock &>/dev/null; then termux-wake-lock fi mkdir -p "$MODEL_DIR" # Use curl with progress bar if available, otherwise fall back to wget if command -v curl >/dev/null 2>&1; then log "Downloading with curl..." if ! curl -L --progress-bar "$URL" -o "$DEST"; then error "Download failed. Please check the URL and your internet connection." # Clean up potentially incomplete file rm -f "$DEST" 2>/dev/null if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi elif command -v wget >/dev/null 2>&1; then # Added explicit check for wget log "Downloading with wget..." if ! wget -c --show-progress "$URL" -O "$DEST"; then error "Download failed. Please check the URL and your internet connection." # Clean up potentially incomplete file (wget often cleans up .part files itself, but clean the target too) rm -f "$DEST" "$DEST.part" 2>/dev/null if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi else error "Neither 'curl' nor 'wget' found. Cannot download the file." if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi if [ -f "$DEST" ] && [ -s "$DEST" ]; then local downloaded_size=$(get_file_size_human "$DEST") success "Model '$FILENAME' ($downloaded_size) downloaded successfully to '$DEST'" # Verify the downloaded model integrity log "Verifying integrity of downloaded model..." if check_model_integrity "$DEST"; then success "Integrity check passed for $FILENAME." else warn "Integrity check failed for $FILENAME. The downloaded model may be corrupted." warn "Consider deleting it and downloading it again." wait_for_keypress_keep_screen # Pause to show integrity warning fi # Show notification if available if command -v termux-notification &>/dev/null; then termux-notification --title "Download Complete" --content "Model $FILENAME has been downloaded successfully." fi else error "Download failed or resulted in an empty file: File not found or empty after download completion." rm -f "$DEST" 2>/dev/null wait_for_keypress_keep_screen fi press_enter return 0 } delete_model() { clear_screen echo -e "${BOLD}${CYAN}=== Delete Model ===${NC}" echo log "This will permanently delete a model file from your KoboldCpp models directory ($MODEL_DIR)." echo check_install || { return 1; } echo -e "${YELLOW}Available models in $MODEL_DIR:${NC}" local MODELS=() while IFS= read -r -d '' file; do # Use '' for null delimiter, portable MODELS+=("$file") done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z) if [ ${#MODELS[@]} -eq 0 ]; then error "No models found in '$MODEL_DIR'." wait_for_keypress_keep_screen return 1 fi for i in "${!MODELS[@]}"; do local MODEL="${MODELS[$i]}" local FILENAME FILENAME=$(basename "$MODEL") local SIZE SIZE=$(get_file_size_human "$MODEL") echo "[$i] $FILENAME ($SIZE)" done echo echo -n "Enter number of the model to delete (or 'q' to return to menu): " read -r CHOICE if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then return 0 fi if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then error "Invalid selection." wait_for_keypress_keep_screen return 1 fi local TARGET="${MODELS[$CHOICE]}" local FILENAME FILENAME=$(basename "$TARGET") local SIZE SIZE=$(get_file_size_human "$TARGET") echo -e "${RED}WARNING: This action is irreversible!${NC}" echo -n "Are you sure you want to delete '$FILENAME' ($SIZE)? [y/N]: " read -r CONFIRM if [[ "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N log "Deleting model file: $TARGET" if rm "$TARGET"; then success "Deleted model '$FILENAME' ($SIZE freed)." else error "Failed to delete '$FILENAME'. Check permissions." wait_for_keypress_keep_screen return 1 fi else info "Deletion cancelled." fi press_enter return 0 } list_models() { clear_screen echo -e "${BOLD}${CYAN}=== Available Models ===${NC}" echo log "Listing .gguf models in the cache directory: $MODEL_DIR" echo check_install || { return 1; } mkdir -p "$MODEL_DIR" local MODELS=() while IFS= read -r -d '' file; do # Use '' for null delimiter, portable MODELS+=("$file") done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z) if [ ${#MODELS[@]} -eq 0 ]; then echo "No models found in '$MODEL_DIR'." echo echo "You can add models using the main menu options:" echo "2. Add model from Downloads" echo "3. Download model from URL" press_enter return 0 fi echo -e "${YELLOW}Size\tStatus\tModel${NC}" local TOTAL_SIZE_BYTES=0 local CORRUPT_COUNT=0 for MODEL_PATH in "${MODELS[@]}"; do local SIZE_BYTES SIZE_BYTES=$(get_file_size_bytes "$MODEL_PATH") TOTAL_SIZE_BYTES=$((TOTAL_SIZE_BYTES + SIZE_BYTES)) local HUMAN_SIZE HUMAN_SIZE=$(get_file_size_human "$MODEL_PATH") local FILENAME FILENAME=$(basename "$MODEL_PATH") # Check if model might be corrupted local status="✓ OK" local status_color="$GREEN" if ! check_model_integrity "$MODEL_PATH" >/dev/null 2>&1; then # Check silently for listing status="⚠️ Issues" status_color="$YELLOW" ((CORRUPT_COUNT++)) fi # Use printf for consistent formatting printf "%s\t${status_color}%s${NC}\t%s\n" "$HUMAN_SIZE" "$status" "$FILENAME" done local HUMAN_TOTAL_SIZE="0B" if [ "$TOTAL_SIZE_BYTES" -gt 0 ]; then if command -v numfmt &>/dev/null; then HUMAN_TOTAL_SIZE=$(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE_BYTES") else if [ "$TOTAL_SIZE_BYTES" -ge 1073741824 ]; then HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fGiB", ts/1073741824 }') elif [ "$TOTAL_SIZE_BYTES" -ge 1048576 ]; then HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fMiB", ts/1048576 }') elif [ "$TOTAL_SIZE_BYTES" -ge 1024 ]; then HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fKiB", ts/1024 }') else HUMAN_TOTAL_SIZE="${TOTAL_SIZE_BYTES}B" fi fi fi echo -e "${YELLOW}------------------------------------${NC}" echo -e "${GREEN}Total Models:\t${#MODELS[@]}${NC}" echo -e "${GREEN}Total Size:\t$HUMAN_TOTAL_SIZE${NC}" if [ "$CORRUPT_COUNT" -gt 0 ]; then echo -e "${YELLOW}Models with Potential Issues:\t$CORRUPT_COUNT${NC}" fi echo echo "Legend: ✓ OK = Basic checks passed, ⚠️ Issues = Potential corruption or not a GGUF file" press_enter return 0 } # New function to interactively verify a specific model's integrity verify_model_integrity_interactive() { clear_screen echo -e "${BOLD}${CYAN}=== Verify Model Integrity ===${NC}" echo log "This will perform basic checks on a selected model file to detect potential corruption." echo check_install || { return 1; } mkdir -p "$MODEL_DIR" # Ensure directory exists before checking local MODELS=() while IFS= read -r -d '' file; do MODELS+=("$file") done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z) if [ ${#MODELS[@]} -eq 0 ]; then error "No models found in '$MODEL_DIR' to verify." wait_for_keypress_keep_screen return 1 fi echo -e "${YELLOW}Available models in $MODEL_DIR:${NC}" for i in "${!MODELS[@]}"; do local MODEL="${MODELS[$i]}" local FILENAME=$(basename "$MODEL") local SIZE=$(get_file_size_human "$MODEL") echo "[$i] $FILENAME ($SIZE)" done echo echo -n "Enter number of the model to verify (or 'q' to return to menu): " read -r CHOICE if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then return 0 fi if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then error "Invalid selection." wait_for_keypress_keep_screen return 1 fi local TARGET_MODEL_PATH="${MODELS[$CHOICE]}" local TARGET_MODEL_FILENAME=$(basename "$TARGET_MODEL_PATH") log "Verifying integrity of '$TARGET_MODEL_FILENAME'..." if check_model_integrity "$TARGET_MODEL_PATH"; then success "Model '$TARGET_MODEL_FILENAME' appears to be intact based on basic checks." else error "Model '$TARGET_MODEL_FILENAME' failed integrity checks. It may be corrupted or incomplete." info "This model might not load or run correctly." fi press_enter return 0 } run_model() { clear_screen echo -e "${BOLD}${CYAN}=== Run KoboldCpp ===${NC}" echo log "Starting the KoboldCpp server with a selected model from the cache directory ($MODEL_DIR)." echo check_install || { return 1; } mkdir -p "$MODEL_DIR" echo -e "${YELLOW}Available models:${NC}" local MODELS=() while IFS= read -r -d '' file; do # Use '' for null delimiter, portable MODELS+=("$file") done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z) if [ ${#MODELS[@]} -eq 0 ]; then error "No models found in '$MODEL_DIR'." echo echo "You need to add models first. Return to the main menu and select:" echo "2. Add model from Downloads" echo "3. Download model from URL" wait_for_keypress_keep_screen return 1 fi # Sort models by last used time if available local last_used_model="" if [ -f "$CONFIG_FILE" ]; then last_used_model=$(grep "^last_used_model=" "$CONFIG_FILE" | cut -d= -f2) # Ensure last_used_model still exists if [ -n "$last_used_model" ] && [ ! -f "$last_used_model" ]; then warn "Last used model '$last_used_model' not found. Resetting last used model setting." last_used_model="" # Consider removing the invalid config entry # sed -i '/^last_used_model=/d' "$CONFIG_FILE" fi if [ -n "$last_used_model" ]; then for i in "${!MODELS[@]}"; do if [ "${MODELS[$i]}" = "$last_used_model" ]; then # Move last used model to the top local temp=${MODELS[0]} MODELS[0]=${MODELS[$i]} MODELS[$i]=$temp break fi done fi fi for i in "${!MODELS[@]}"; do local MODEL="${MODELS[$i]}" local FILENAME FILENAME=$(basename "$MODEL") local SIZE SIZE=$(get_file_size_human "$MODEL") # Mark the last used model local marker="" if [ "$MODEL" = "$last_used_model" ]; then marker=" (last used)" fi echo "[$i] $FILENAME ($SIZE)$marker" done echo echo -n "Enter number of the model to run (or 'q' to return to menu): " read -r CHOICE if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then return 0 fi if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then error "Invalid selection." wait_for_keypress_keep_screen return 1 fi local TARGET_MODEL_PATH="${MODELS[$CHOICE]}" local TARGET_MODEL_FILENAME=$(basename "$TARGET_MODEL_PATH") local TARGET_MODEL_SIZE=$(get_file_size_human "$TARGET_MODEL_PATH") # Save last used model save_config "last_used_model" "$TARGET_MODEL_PATH" # Check model integrity before running if ! check_model_integrity "$TARGET_MODEL_PATH"; then echo -e "${RED}WARNING: This model may have integrity issues.${NC}" echo -n "Do you still want to continue? [y/N]: " read -r INTEGRITY_CONFIRM if [[ ! "${INTEGRITY_CONFIRM:-N}" =~ ^[Yy]$ ]]; then info "Operation cancelled." press_enter return 0 fi fi clear_screen echo -e "${BOLD}${CYAN}=== Run Configuration: $TARGET_MODEL_FILENAME ===${NC}" echo echo "Model: $TARGET_MODEL_FILENAME ($TARGET_MODEL_SIZE)" echo info "You can customize parameters like threads, context size, and port." info "For optimal performance, consider GPU acceleration if your device/build supports CLBlast (--useclblast 0 0)." echo # Load saved configuration if exists local saved_config=false local saved_threads="" local saved_context="" local saved_port="" local saved_extra="" local model_config_key="model_${TARGET_MODEL_FILENAME//[^a-zA-Z0-9_]/_}" # Sanitize filename for config key if [ -f "$CONFIG_FILE" ]; then saved_threads=$(grep "^${model_config_key}_threads=" "$CONFIG_FILE" | cut -d= -f2) saved_context=$(grep "^${model_config_key}_context=" "$CONFIG_FILE" | cut -d= -f2) saved_port=$(grep "^${model_config_key}_port=" "$CONFIG_FILE" | cut -d= -f2) saved_extra=$(grep "^${model_config_key}_extra=" "$CONFIG_FILE" | cut -d= -f2) if [ -n "$saved_threads" ] && [ -n "$saved_context" ] && [ -n "$saved_port" ]; then saved_config=true echo "Found saved configuration for this model:" echo "- Threads: ${YELLOW}$saved_threads${NC}" echo "- Context: ${YELLOW}$saved_context${NC}" echo "- Port: ${YELLOW}$saved_port${NC}" if [ -n "$saved_extra" ]; then echo "- Extra options: ${YELLOW}$saved_extra${NC}" fi echo fi fi echo "Select run configuration:" echo "[1] Quick start (recommended settings for this device)" if [ "$saved_config" = true ]; then echo "[2] Use saved configuration for this model" echo "[3] Custom settings" else echo "[2] Custom settings" fi echo "[q] Return to main menu" echo echo -n "Your choice: " read -r CONFIG_CHOICE local ARGS_STRING="" local PORT_NUMBER="5001" local NPROC_COUNT=4 if command -v nproc &>/dev/null; then NPROC_COUNT=$(nproc) if [ "$NPROC_COUNT" -eq 0 ]; then NPROC_COUNT=1; fi # Avoid 0 threads fi local SUGGESTED_THREADS="$NPROC_COUNT" if [ "$NPROC_COUNT" -gt 8 ]; then # Suggest fewer threads on high-core count devices for balance SUGGESTED_THREADS=$((NPROC_COUNT / 2)) if [ "$SUGGESTED_THREADS" -lt 4 ]; then SUGGESTED_THREADS=4; fi fi local current_choice="${CONFIG_CHOICE:-1}" # Default to 1 (Quick Start) if [ "$saved_config" = false ] && [ "$current_choice" = "3" ]; then # If saved_config is false, option 3 means custom, but the menu showed [2] Custom. Adjust choice. current_choice="2" fi case "$current_choice" in 1) ARGS_STRING="--threads ${SUGGESTED_THREADS} --contextsize 2048 --port $PORT_NUMBER" info "Using Quick Start with $SUGGESTED_THREADS threads, context 2048, port $PORT_NUMBER." ;; 2) if [ "$saved_config" = true ]; then # Use saved configuration (only if option 2 was explicitly chosen AND saved config exists) ARGS_STRING="--threads $saved_threads --contextsize $saved_context --port $saved_port" if [ -n "$saved_extra" ]; then ARGS_STRING="$ARGS_STRING $saved_extra" fi info "Using saved configuration with $saved_threads threads, context $saved_context, port $saved_port." else # Saved config doesn't exist, this was the "Custom settings" option # Fall through to custom settings block CONFIG_CHOICE="custom" # Internal flag fi ;; 3) # This case is only reached if saved_config is true AND user chose 3 CONFIG_CHOICE="custom" # Internal flag ;; q|Q) return 0 ;; *) info "Invalid configuration choice, using Quick Start defaults." ARGS_STRING="--threads ${SUGGESTED_THREADS} --contextsize 2048 --port $PORT_NUMBER" ;; esac # Custom settings block (executed if CONFIG_CHOICE was "custom") if [ "${CONFIG_CHOICE:-}" = "custom" ]; then clear_screen echo -e "${BOLD}${CYAN}=== Custom Settings for $TARGET_MODEL_FILENAME ===${NC}" echo echo -n "Number of CPU threads (suggested: $SUGGESTED_THREADS, max detected: $NPROC_COUNT): " read -r THREADS_INPUT local THREADS=${THREADS_INPUT:-$SUGGESTED_THREADS} # Add validation for threads input if ! [[ "$THREADS" =~ ^[0-9]+$ ]] || [ "$THREADS" -le 0 ]; then warn "Invalid thread count '$THREADS_INPUT', using suggested count: $SUGGESTED_THREADS." THREADS="$SUGGESTED_THREADS" fi ARGS_STRING="--threads $THREADS" # Get model size to suggest appropriate context size local MODEL_SIZE_BYTES=$(get_file_size_bytes "$TARGET_MODEL_PATH") local SUGGESTED_CONTEXT=2048 if [ "$MODEL_SIZE_BYTES" -gt 10737418240 ]; then # >10GB models (10 * 1024^3) SUGGESTED_CONTEXT=4096 elif [ "$MODEL_SIZE_BYTES" -lt 5368709120 ]; then # <5GB models (5 * 1024^3) SUGGESTED_CONTEXT=8192 fi echo -n "Context size (e.g., 2048, 4096, 8192; suggested: $SUGGESTED_CONTEXT): " read -r CONTEXT_INPUT local CONTEXT=${CONTEXT_INPUT:-$SUGGESTED_CONTEXT} # Add validation for context size input if ! [[ "$CONTEXT" =~ ^[0-9]+$ ]] || [ "$CONTEXT" -le 0 ]; then warn "Invalid context size '$CONTEXT_INPUT', using suggested size: $SUGGESTED_CONTEXT." CONTEXT="$SUGGESTED_CONTEXT" fi ARGS_STRING="$ARGS_STRING --contextsize $CONTEXT" echo -n "Server port (default: 5001): " read -r PORT_INPUT PORT_NUMBER=${PORT_INPUT:-5001} # Add validation for port number if ! [[ "$PORT_NUMBER" =~ ^[0-9]+$ ]] || [ "$PORT_NUMBER" -lt 1024 ] || [ "$PORT_NUMBER" -gt 65535 ]; then warn "Invalid port number '$PORT_INPUT', using default port: 5001." PORT_NUMBER="5001" fi ARGS_STRING="$ARGS_STRING --port $PORT_NUMBER" echo echo "Additional options:" echo "[1] No additional options - continue with settings above" echo "[2] Enable GPU acceleration (CLBlast - if supported by your device/build)" echo "[3] Use low VRAM mode (slower, uses less memory, implies --nostream)" echo "[4] Enable Mmapped IO (faster loading but more memory usage)" echo "[5] Set custom LORA files (if you have them)" echo "[6] Custom extra arguments (enter raw arguments)" echo echo -n "Your choice for additional options [1]: " read -r EXTRA_OPTIONS_CHOICE local EXTRA_ARGS_VAL="" case "${EXTRA_OPTIONS_CHOICE:-1}" in # Default to 1 2) EXTRA_ARGS_VAL="--useclblast 0 0" info "GPU acceleration (CLBlast) enabled. Ensure your KoboldCpp is built with CLBlast support." ;; 3) EXTRA_ARGS_VAL="--lowvram --nostream" info "Low VRAM mode enabled." ;; 4) EXTRA_ARGS_VAL="--mmap" info "Memory-mapped IO enabled." ;; 5) echo echo "Enter full path to LORA file:" read -r LORA_PATH if [ -n "$LORA_PATH" ] && [ -f "$LORA_PATH" ]; then echo "Enter LORA scale factor (default: 1.0):" read -r LORA_SCALE LORA_SCALE=${LORA_SCALE:-1.0} EXTRA_ARGS_VAL="--lora \"$LORA_PATH:$LORA_SCALE\"" # Quote path in case of spaces info "LORA '$LORA_PATH' will be loaded with scale $LORA_SCALE." else warn "LORA file not found or path empty. No LORA will be loaded." fi ;; 6) echo echo "Enter custom additional arguments (e.g., --gpulayers 10 --unbantokens):" read -r RAW_EXTRA_ARGS EXTRA_ARGS_VAL="$RAW_EXTRA_ARGS" info "Using custom extra arguments: $RAW_EXTRA_ARGS" ;; *) # Includes 1 and any other input info "No additional special options selected." ;; esac if [ -n "$EXTRA_ARGS_VAL" ]; then ARGS_STRING="$ARGS_STRING $EXTRA_ARGS_VAL" fi # Save this configuration for future use echo -n "Save this configuration for future use with this model? [Y/n]: " read -r SAVE_CONFIG_CHOICE if [[ ! "${SAVE_CONFIG_CHOICE:-Y}" =~ ^[Nn]$ ]]; then # Default Y save_config "${model_config_key}_threads" "$THREADS" save_config "${model_config_key}_context" "$CONTEXT" save_config "${model_config_key}_port" "$PORT_NUMBER" save_config "${model_config_key}_extra" "$EXTRA_ARGS_VAL" success "Configuration saved for future use." fi fi # End Custom settings block clear_screen log "Starting KoboldCpp with model: $TARGET_MODEL_FILENAME ($TARGET_MODEL_SIZE)" if [ -n "$ARGS_STRING" ]; then log "Run arguments: $ARGS_STRING" fi # Show available network interfaces local DEVICE_IP_MSG="" if command -v ip &>/dev/null; then local interface_ip interface_ip=$(ip -o -4 addr show scope global | awk '!/ lo / && $4 !~ /^169\.254\./ {print $4}' | cut -d/ -f1 | head -n1) if [ -n "$interface_ip" ]; then DEVICE_IP_MSG=" (or http://$interface_ip:$PORT_NUMBER from another device on the same network)" fi fi log "Server will be available at: http://localhost:$PORT_NUMBER${DEVICE_IP_MSG}" log "Press Ctrl+C in this terminal to stop the server." echo cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; } local KOBOLDCPP_BASE_CMD="" if [ -f "./koboldcpp-android-arm64" ] && [ -x "./koboldcpp-android-arm64" ]; then KOBOLDCPP_BASE_CMD="./koboldcpp-android-arm64" elif [ -f "./koboldcpp" ] && [ -x "./koboldcpp" ]; then KOBOLDCPP_BASE_CMD="./koboldcpp" elif [ -f "./koboldcpp.py" ]; then KOBOLDCPP_BASE_CMD="python koboldcpp.py" else error "Could not find a runnable KoboldCpp executable (e.g., koboldcpp-android-arm64, koboldcpp) or koboldcpp.py script in $KOBOLDCPP_DIR." info "Please ensure KoboldCpp is installed and built correctly." wait_for_keypress_keep_screen return 1 fi local cmd_array=() if [[ "$KOBOLDCPP_BASE_CMD" == "python koboldcpp.py" ]]; then cmd_array+=("python" "koboldcpp.py") else cmd_array+=("$KOBOLDCPP_BASE_CMD") fi cmd_array+=("--model" "$TARGET_MODEL_PATH") local args_temp_array=() # Safely split ARGS_STRING into arguments, handling quotes # This is tricky in pure bash without eval/set -f, but read -r -a is better than simple split # Using a temporary array and disabling/enabling globbing around read -a set -f # Disable globbing read -r -a args_temp_array <<< "$ARGS_STRING" set +f # Re-enable globbing if [ ${#args_temp_array[@]} -gt 0 ]; then cmd_array+=("${args_temp_array[@]}") fi info "Executing: ${cmd_array[*]}" if command -v termux-wake-lock &>/dev/null; then termux-wake-lock fi # Start time measurement local start_time=$(date +%s) # Execute the command array if ! "${cmd_array[@]}"; then local exit_code=$? local end_time=$(date +%s) local runtime=$((end_time - start_time)) local hours=$((runtime / 3600)) local minutes=$(( (runtime % 3600) / 60 )) local seconds=$((runtime % 60)) warn "KoboldCpp server exited with code $exit_code. Runtime: ${hours}h ${minutes}m ${seconds}s" echo "Check the console output above for error messages." # Save partial runtime statistics on error save_config "last_runtime_hours" "$hours" save_config "last_runtime_minutes" "$minutes" save_config "last_runtime_seconds" "$seconds" save_config "last_runtime_model" "$TARGET_MODEL_FILENAME (exit $exit_code)" if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi # End time measurement (only if command finished without error) local end_time=$(date +%s) local runtime=$((end_time - start_time)) local hours=$((runtime / 3600)) local minutes=$(( (runtime % 3600) / 60 )) local seconds=$((runtime % 60)) # Save runtime statistics save_config "last_runtime_hours" "$hours" save_config "last_runtime_minutes" "$minutes" save_config "last_runtime_seconds" "$seconds" save_config "last_runtime_model" "$TARGET_MODEL_FILENAME" if command -v termux-wake-unlock &>/dev/null; then log "Releasing wake lock..." termux-wake-unlock fi echo info "KoboldCpp server stopped normally. Runtime: ${hours}h ${minutes}m ${seconds}s" press_enter return 0 } rebuild() { clear_screen echo -e "${BOLD}${CYAN}=== Rebuild KoboldCpp ===${NC}" echo log "Rebuilding KoboldCpp from source in $KOBOLDCPP_DIR..." # Check if KoboldCpp directory exists and is a git repo if [ ! -d "$KOBOLDCPP_DIR/.git" ]; then error "KoboldCpp directory '$KOBOLDCPP_DIR' is not a git repository. Cannot rebuild." info "Please use the 'Install / Update / Rebuild' option from the main menu to install first." wait_for_keypress_keep_screen return 1 fi cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; } if command -v termux-wake-lock &>/dev/null; then termux-wake-lock fi log "Cleaning build files..." make clean || warn "Failed to clean build files, attempting rebuild anyway." # Get system information for optimized build local device_info=$(get_device_info) log "Building for: $device_info" # Try to use previously successful build options local build_options=$(load_config "build_options" "0") # Default to 0 (default build) export LLAMA_PORTABLE=1 export KOBOLDCPP_PLATFORM="android" local build_command="make -j$(nproc)" if [ "$build_options" = "1" ]; then log "Using previously successful build options (LLAMA_NO_ACCELERATE=1)..." build_command="make -j$(nproc) LLAMA_NO_ACCELERATE=1" fi log "Starting build with command: $build_command" # Use eval to execute the build_command string if eval "$build_command"; then # If the build was successful, save the options used if [[ "$build_command" == *"LLAMA_NO_ACCELERATE=1"* ]]; then save_config "build_options" "1" else save_config "build_options" "0" fi success "KoboldCpp rebuilt successfully." else local build_exit_code=$? error "Rebuild failed with command: $build_command (Exit code $build_exit_code)." # If default build failed, try safer settings regardless of previous config if [ "$build_options" != "1" ]; then warn "Trying rebuild with safer settings (LLAMA_NO_ACCELERATE=1)..." make clean # Clean before retrying if make LLAMA_NO_ACCELERATE=1; then success "KoboldCpp rebuilt successfully with LLAMA_NO_ACCELERATE=1." save_config "build_options" "1" # Save the successful option else error "Rebuild failed even with safer settings. Please check error messages." wait_for_keypress_keep_screen return 1 fi else # Safer settings were already tried or previously saved, and it failed again error "Rebuild failed with saved safer settings. Please check error messages." wait_for_keypress_keep_screen return 1 fi fi if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi press_enter return 0 } update_koboldcpp() { clear_screen echo -e "${BOLD}${CYAN}=== Update KoboldCpp ===${NC}" echo log "Updating the KoboldCpp source code from the repository." # Check if KoboldCpp directory exists and is a git repo if [ ! -d "$KOBOLDCPP_DIR/.git" ]; then error "KoboldCpp directory '$KOBOLDCPP_DIR' is not a git repository. Cannot update." info "Please use the 'Install / Update / Rebuild' option from the main menu to install first." wait_for_keypress_keep_screen return 1 fi check_install || { return 1; } check_internet || { warn "Internet connection is required to update KoboldCpp." echo -n "Do you want to continue anyway? [y/N]: " read -r CONTINUE if [[ ! "${CONTINUE:-N}" =~ ^[Yy]$ ]]; then press_enter return 0 fi } log "Checking for KoboldCpp updates..." cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; } # Check for updates before proceeding # Use git fetch --dry-run or similar for a cleaner check without fetching # git fetch origin "$DEFAULT_BRANCH" --quiet --dry-run # --dry-run isn't always supported or clear # Alternative: fetch and compare if ! git fetch origin "$DEFAULT_BRANCH" --quiet; then warn "Failed to fetch updates from origin. Cannot determine if updates are available." echo -n "Attempt update anyway? [y/N]: " read -r ATTEMPT_UPDATE if [[ ! "${ATTEMPT_UPDATE:-N}" =~ ^[Yy]$ ]]; then press_enter return 0 fi # If user wants to attempt, proceed with reset below which might fail or succeed with old data local updates_available=false # Assume no updates to skip the log output below else local LOCAL=$(git rev-parse HEAD) local REMOTE=$(git rev-parse origin/"$DEFAULT_BRANCH") if [ "$LOCAL" = "$REMOTE" ]; then info "KoboldCpp is already up to date (on branch '$DEFAULT_BRANCH')." echo -n "Force update/rebuild anyway? [y/N]: " read -r FORCE_UPDATE if [[ ! "${FORCE_UPDATE:-N}" =~ ^[Yy]$ ]]; then press_enter return 0 fi local updates_available=false # No actual updates else echo -e "${YELLOW}Updates available:${NC}" git log --oneline --graph --decorate HEAD..origin/"$DEFAULT_BRANCH" | head -n 10 if [ "$(git log --oneline HEAD..origin/"$DEFAULT_BRANCH" 2>/dev/null | wc -l)" -gt 10 ]; then echo "... and more changes" fi echo local updates_available=true fi fi echo -n "This will fetch the latest version of KoboldCpp from '$DEFAULT_BRANCH' branch and rebuild. Any local changes not committed might be lost (they will be stashed). Continue? [Y/n]: " read -r CONFIRM if [[ "${CONFIRM:-Y}" =~ ^[Nn]$ ]]; then # Default Y info "Update cancelled." press_enter return 0 fi local stashed_changes=false local stash_pop_failed=false # Check for uncommitted changes that are tracked or untracked if ! git diff-index --quiet HEAD -- || git ls-files --others --exclude-standard | grep .; then log "Stashing local changes..." if git stash push -u -m "Auto-stash by KCPP Manager script $(date)"; then stashed_changes=true info "Local changes stashed." else warn "Failed to stash local changes. If you have modifications, they might be overwritten or cause conflicts." fi else info "No local changes to stash." fi log "Fetching updates from origin..." # Re-fetch in case --quiet fetch missed something or was skipped earlier if ! git fetch --all --prune; then error "Failed to fetch updates from repository." # Attempt to reapply stash before exiting on fetch failure if [ "$stashed_changes" = true ]; then info "Attempting to reapply stashed changes after fetch failure..." if git stash pop; then success "Successfully reapplied stashed local changes." else warn "Could not automatically reapply stashed local changes after fetch failure. Conflicts may have occurred." info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually." stash_pop_failed=true fi fi if [ "$stash_pop_failed" = true ]; then wait_for_keypress_keep_screen; fi return 1 fi log "Resetting to latest version of '$DEFAULT_BRANCH'..." if ! git reset --hard "origin/$DEFAULT_BRANCH"; then error "Failed to update to latest version of branch '$DEFAULT_BRANCH'. Your local repository might be in an inconsistent state." # Attempt to reapply stash before exiting on reset failure if [ "$stashed_changes" = true ]; then info "Attempting to reapply stashed changes after reset failure..." if git stash pop; then success "Successfully reapplied stashed local changes." else warn "Could not automatically reapply stashed local changes after reset failure. Conflicts may have occurred." info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually." stash_pop_failed=true fi fi if [ "$stash_pop_failed" = true ]; then wait_for_keypress_keep_screen; fi return 1 fi log "Rebuilding KoboldCpp after update..." if ! rebuild; then # rebuild() calls wait_for_keypress_keep_screen on its own error # Stash reapplying is handled below after rebuild attempt local rebuild_failed=true else local rebuild_failed=false fi if [ "$stashed_changes" = true ]; then info "Attempting to reapply stashed local changes..." if git stash pop; then success "Successfully reapplied stashed local changes." else stash_pop_failed=true warn "Could not automatically reapply stashed local changes. Conflicts may have occurred." info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually." fi fi if [ "$rebuild_failed" = true ]; then error "KoboldCpp update completed (source fetched and reset), but rebuild failed." wait_for_keypress_keep_screen # Keep rebuild error message visible return 1 # Indicate failure elif [ "$updates_available" = true ]; then success "KoboldCpp updated to the latest version from '$DEFAULT_BRANCH' and rebuilt." else info "KoboldCpp source code was already up-to-date." success "Rebuild completed." fi if [ "$stash_pop_failed" = true ]; then wait_for_keypress_keep_screen # Keep complex stash message visible else press_enter fi return 0 } backup_models() { clear_screen echo -e "${BOLD}${CYAN}=== Backup Models ===${NC}" echo log "Creating a compressed archive of all .gguf models in '$MODEL_DIR'." echo check_install || { return 1; } _validate_critical_directory_path "$MODEL_DIR" "$EXPECTED_MODEL_DIR_PATH" "backup" || { wait_for_keypress_keep_screen; return 1; } local MODELS_EXIST_COUNT MODELS_EXIST_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | awk 'END{print NR}' RS='\0') if [ "${MODELS_EXIST_COUNT:-0}" -eq 0 ]; then warn "No models found in '$MODEL_DIR' to backup." press_enter return 0 fi mkdir -p "$BACKUP_DIR" || { error "Failed to create backup directory: '$BACKUP_DIR'"; wait_for_keypress_keep_screen; return 1; } local BACKUP_FILE="$BACKUP_DIR/koboldcpp_models_$(date +%Y%m%d_%H%M%S).tar.gz" # Estimate backup size local TOTAL_MODELS_SIZE_BYTES=0 while IFS= read -r -d '' model_file; do local file_size=$(get_file_size_bytes "$model_file") TOTAL_MODELS_SIZE_BYTES=$((TOTAL_MODELS_SIZE_BYTES + file_size)) done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null) if [ "$TOTAL_MODELS_SIZE_BYTES" -eq 0 ]; then warn "Found .gguf files, but their total size is zero. Skipping backup." press_enter return 0 fi # Add 5% overhead for tar/gzip, minimum 1MB local ESTIMATED_BACKUP_SIZE=$((TOTAL_MODELS_SIZE_BYTES * 105 / 100)) if [ "$ESTIMATED_BACKUP_SIZE" -lt 1048576 ] && [ "$TOTAL_MODELS_SIZE_BYTES" -gt 0 ]; then ESTIMATED_BACKUP_SIZE=1048576 # Minimum 1MB estimate if models exist fi # Check available space if ! check_available_space "$ESTIMATED_BACKUP_SIZE" "$BACKUP_DIR"; then error "Not enough space for backup. Please free up some storage or backup to an external device." wait_for_keypress_keep_screen return 1 fi log "Backing up models from '$MODEL_DIR' to '$BACKUP_FILE'..." log "This may take a while for large models." if command -v termux-wake-lock &>/dev/null; then termux-wake-lock fi # Use tar with progress bar if 'pv' is installed if command -v pv &>/dev/null; then log "Using 'pv' for progress indication." if ! tar -c -C "$(dirname "$MODEL_DIR")" "$(basename "$MODEL_DIR")" 2>/dev/null | pv -s "$TOTAL_MODELS_SIZE_BYTES" -p -e -r -b -W | gzip > "$BACKUP_FILE"; then local tar_gzip_exit=$? error "Backup failed (tar/gzip with pv exited with code $tar_gzip_exit). Check permissions and available space." rm -f "$BACKUP_FILE" # Clean up failed backup file if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi else log "Using 'tar' and 'gzip' without progress indicator (pv not found)." if ! tar -czf "$BACKUP_FILE" -C "$(dirname "$MODEL_DIR")" "$(basename "$MODEL_DIR")"; then local tar_gzip_exit=$? error "Backup failed (tar/gzip exited with code $tar_gzip_exit). Check permissions and available space." rm -f "$BACKUP_FILE" # Clean up failed backup file if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi wait_for_keypress_keep_screen return 1 fi fi if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi if [ -f "$BACKUP_FILE" ]; then success "Models backed up successfully to '$BACKUP_FILE'" info "Backup size: $(get_file_size_human "$BACKUP_FILE")" # Ask if user wants to share/copy the backup elsewhere echo echo "What would you like to do with the backup file?" echo "[1] Keep the backup in the default location ($BACKUP_DIR)" echo "[2] Share the backup file via Android share menu" echo "[3] Copy the backup to external storage" echo "[4] Move the backup to external storage (removes from default location)" echo echo -n "Your choice [1]: " read -r BACKUP_ACTION case "${BACKUP_ACTION:-1}" in 2) if command -v termux-share &>/dev/null; then log "Sharing backup file '$BACKUP_FILE'..." termux-share "$BACKUP_FILE" || warn "Failed to share file using termux-share." else warn "termux-api not installed. Cannot share the backup file. Install with 'pkg install termux-api'." fi ;; 3|4) local action="Copy" if [ "${BACKUP_ACTION:-}" = "4" ]; then action="Move"; fi echo echo "Enter destination directory (e.g., /sdcard/Download or $DOWNLOAD_DIR):" read -r DEST_DIR # Basic validation if [ -z "$DEST_DIR" ]; then error "Destination directory cannot be empty. Operation cancelled." elif [ ! -d "$DEST_DIR" ]; then error "Destination directory '$DEST_DIR' does not exist. Operation cancelled." elif [ ! -w "$DEST_DIR" ]; then error "Destination directory '$DEST_DIR' is not writable. Check permissions. Operation cancelled." else log "${action}ing '$BACKUP_FILE' to '$DEST_DIR/'..." if [ "$action" = "Copy" ]; then if cp "$BACKUP_FILE" "$DEST_DIR/"; then success "Backup copied to $DEST_DIR/$(basename "$BACKUP_FILE")" else error "Failed to copy backup to $DEST_DIR. Check permissions or space." fi else # Move if mv "$BACKUP_FILE" "$DEST_DIR/"; then success "Backup moved to $DEST_DIR/$(basename "$BACKUP_FILE")" else error "Failed to move backup to $DEST_DIR. Check permissions or space." warn "Original backup might still be in '$BACKUP_DIR'." fi fi fi ;; *) info "Backup kept in default location: $BACKUP_FILE" ;; esac else error "Backup file '$BACKUP_FILE' was not created successfully." fi press_enter return 0 } restore_models() { clear_screen echo -e "${BOLD}${CYAN}=== Restore Models ===${NC}" echo log "This will extract models from a backup archive (.tar.gz) into your KoboldCpp models directory ($MODEL_DIR)." echo -e "${RED}WARNING: This will REPLACE your current models in '$MODEL_DIR'.${NC}" echo check_install || { return 1; } _validate_critical_directory_path "$MODEL_DIR" "$EXPECTED_MODEL_DIR_PATH" "restore" || { wait_for_keypress_keep_screen; return 1; } # Check for backups in the default location if [ ! -d "$BACKUP_DIR" ]; then mkdir -p "$BACKUP_DIR" info "Backup directory '$BACKUP_DIR' created, but no backups found." echo echo "Options:" echo "[1] Import a backup file from elsewhere into the backup directory" echo "[2] Return to menu" echo echo -n "Your choice: " read -r IMPORT_CHOICE if [ "$IMPORT_CHOICE" = "1" ]; then echo echo "Enter full path to the backup file (.tar.gz) you want to import:" read -r IMPORT_FILE if [ -f "$IMPORT_FILE" ] && [[ "$IMPORT_FILE" == *.tar.gz ]]; then log "Importing '$IMPORT_FILE' to '$BACKUP_DIR/'..." if cp "$IMPORT_FILE" "$BACKUP_DIR/"; then success "Backup file imported to $BACKUP_DIR/$(basename "$IMPORT_FILE")" else error "Failed to import backup file '$IMPORT_FILE'. Check permissions or space." wait_for_keypress_keep_screen return 1 fi else error "File doesn't exist, isn't a .tar.gz file, or path was empty." wait_for_keypress_keep_screen return 1 fi else return 0 # User chose to return fi fi # End of backup directory check and import option echo -e "${YELLOW}Available backups in $BACKUP_DIR:${NC}" local BACKUPS=() # Sort by modification time, newest first while IFS= read -r -d '' file; do BACKUPS+=("$file") done < <(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -print0 2>/dev/null | sort -rz) if [ ${#BACKUPS[@]} -eq 0 ]; then error "No backup archives (.tar.gz) found in '$BACKUP_DIR'." wait_for_keypress_keep_screen return 1 fi for i in "${!BACKUPS[@]}"; do local BACKUP_ARCHIVE="${BACKUPS[$i]}" local SIZE SIZE=$(get_file_size_human "$BACKUP_ARCHIVE") local DATE_MODIFIED DATE_MODIFIED=$(date -r "$BACKUP_ARCHIVE" "+%Y-%m-%d %H:%M:%S") echo "[$i] $(basename "$BACKUP_ARCHIVE") ($SIZE) - $DATE_MODIFIED" done echo echo -n "Enter number of the backup to restore (or 'q' to return to menu): " read -r CHOICE if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then return 0 fi if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#BACKUPS[@]}" ]; then error "Invalid selection." wait_for_keypress_keep_screen return 1 fi local BACKUP_TO_RESTORE="${BACKUPS[$CHOICE]}" log "Selected backup for restore: $(basename "$BACKUP_TO_RESTORE")" # Preview backup contents echo echo -e "${YELLOW}Contents of the backup (first 10 .gguf files):${NC}" local backup_contents=$(tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -E '\.gguf$' | head -n 10) if [ -n "$backup_contents" ]; then echo "$backup_contents" local total_items=$(tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -E '\.gguf$' | wc -l) if [ "$total_items" -gt 10 ]; then echo "... and $((total_items - 10)) more .gguf items" fi else warn "Could not list contents or no .gguf files found in the backup archive." fi echo # Check if backup has expected directory structure ('models/') if ! tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -q "^models/.*\.gguf$"; then warn "This backup may not have the expected directory structure ('models/*.gguf')." info "It might extract files directly into the parent directory of '$MODEL_DIR'." echo -n "Continue anyway? [y/N]: " read -r CONTINUE_RESTORE if [[ ! "${CONTINUE_RESTORE:-N}" =~ ^[Yy]$ ]]; then info "Restore operation cancelled." press_enter return 0 fi fi # Check available space for restore local BACKUP_SIZE=$(get_file_size_bytes "$BACKUP_TO_RESTORE") # Estimate uncompressed size (gzip compression ratio varies, estimate 2x as a minimum) local ESTIMATED_RESTORE_SIZE=$((BACKUP_SIZE * 3)) # Use 3x for a slightly safer estimate if [ "$ESTIMATED_RESTORE_SIZE" -lt 1048576 ]; then ESTIMATED_RESTORE_SIZE=1048576; fi # Minimum 1MB estimate if ! check_available_space "$ESTIMATED_RESTORE_SIZE" "$(dirname "$MODEL_DIR")"; then error "Not enough space to restore the backup in '$(dirname "$MODEL_DIR")'. Required estimate: $(numfmt --to=iec-i --suffix=B "$ESTIMATED_RESTORE_SIZE" 2>/dev/null || echo "$ESTIMATED_RESTORE_SIZE bytes"). Please free up some storage." wait_for_keypress_keep_screen return 1 fi echo -e "${RED}WARNING: This will DELETE all existing files in '$MODEL_DIR' before restoring!${NC}" echo -n "Are you absolutely sure you want to restore from this backup? [y/N]: " read -r CONFIRM if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N info "Restore operation cancelled." press_enter return 0 fi local PRE_RESTORE_BACKUP_DIR="" # Initialize as empty local current_models_count=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f 2>/dev/null | wc -l) if [ -d "$MODEL_DIR" ] && [ "$current_models_count" -gt 0 ]; then log "Backing up current models to a temporary location before restoring..." PRE_RESTORE_BACKUP_DIR="$BACKUP_DIR/pre_restore_$(date +%Y%m%d_%H%M%S)_$$" # Add PID for uniqueness if mkdir -p "$PRE_RESTORE_BACKUP_DIR"; then if cp -a "$MODEL_DIR/." "$PRE_RESTORE_BACKUP_DIR/"; then info "Current models backed up to '$PRE_RESTORE_BACKUP_DIR'" else warn "Failed to backup current models to '$PRE_RESTORE_BACKUP_DIR'. Continuing with restore, but current models are NOT backed up by this operation." PRE_RESTORE_BACKUP_DIR="" # Clear path if backup failed fi else warn "Could not create directory '$PRE_RESTORE_BACKUP_DIR' for pre-restore backup. Skipping this step." PRE_RESTORE_BACKUP_DIR="" # Clear path if directory creation failed fi else info "No existing models in '$MODEL_DIR' to back up before restoring." fi log "Clearing existing models in '$MODEL_DIR'..." # Use find -delete for safer recursive deletion within the specific directory if [ -d "$MODEL_DIR" ]; then if find "$MODEL_DIR" -mindepth 1 -delete; then log "Existing models cleared from '$MODEL_DIR'." else error "Failed to clear existing models from '$MODEL_DIR'. Check permissions." _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore pre-restore backup return 1 fi else mkdir -p "$MODEL_DIR" || { error "Failed to create model directory: '$MODEL_DIR'"; wait_for_keypress_keep_screen; return 1; } fi log "Extracting backup: $(basename "$BACKUP_TO_RESTORE") to '$(dirname "$MODEL_DIR")'..." log "This may take a while for large backups." # Ensure the parent directory exists for extraction mkdir -p "$(dirname "$MODEL_DIR")" || { error "Failed to create parent directory for models: '$(dirname "$MODEL_DIR")'"; wait_for_keypress_keep_screen; return 1; } if command -v termux-wake-lock &>/dev/null; then termux-wake-lock fi # Extract with progress if available if command -v pv &>/dev/null; then log "Using 'pv' for progress indication during extraction." if ! pv "$BACKUP_TO_RESTORE" | tar -xzf - -C "$(dirname "$MODEL_DIR")"; then local extract_exit=$? error "Restore failed during extraction (Exit code $extract_exit). The backup might be corrupted or there might be permission/space issues." if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore the pre-restore backup return 1 fi else log "Extracting with 'tar' without progress indicator (pv not found)." if ! tar -xzf "$BACKUP_TO_RESTORE" -C "$(dirname "$MODEL_DIR")"; then local extract_exit=$? error "Restore failed during extraction (Exit code $extract_exit). The backup might be corrupted or there might be permission/space issues." if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore the pre-restore backup return 1 fi fi if command -v termux-wake-unlock &>/dev/null; then termux-wake-unlock fi # Verify restored models integrity log "Verifying restored models integrity..." local corrupt_models=0 local restored_models_count=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f 2>/dev/null | wc -l) if [ "$restored_models_count" -eq 0 ]; then warn "No .gguf files found in '$MODEL_DIR' after extraction. Restore may have failed or the backup was empty/incorrect." _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore pre-restore backup return 1 fi while IFS= read -r -d '' model_file; do if ! check_model_integrity "$model_file" >/dev/null 2>&1; then # Check silently warn "Model $(basename "$model_file") may have integrity issues after restoring." ((corrupt_models++)) fi done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null) if [ "$corrupt_models" -gt 0 ]; then warn "$corrupt_models models out of $restored_models_count may have integrity issues after restoring. Consider verifying them individually." wait_for_keypress_keep_screen # Pause to show warnings fi success "Models restored successfully from $(basename "$BACKUP_TO_RESTORE"). Total restored models: $restored_models_count." # Clean up the temporary pre-restore backup if it exists and restore was successful if [ -n "$PRE_RESTORE_BACKUP_DIR" ] && [ -d "$PRE_RESTORE_BACKUP_DIR" ]; then log "Cleaning up temporary pre-restore backup at '$PRE_RESTORE_BACKUP_DIR'..." rm -rf "$PRE_RESTORE_BACKUP_DIR" || warn "Failed to remove temporary pre-restore backup." fi press_enter return 0 } # Helper function for restore failures _handle_restore_failure() { local pre_restore_dir="$1" local model_dir="$2" error "Restore operation failed." info "Attempting to restore the pre-restore backup if it exists and contains files..." if [ -n "$pre_restore_dir" ] && [ -d "$pre_restore_dir" ] && [ "$(ls -A "$pre_restore_dir" 2>/dev/null)" ]; then info "Clearing potentially inconsistent state in '$model_dir'..." find "$model_dir" -mindepth 1 -delete || warn "Failed to clear '$model_dir' before attempting pre-restore recovery." mkdir -p "$model_dir" || warn "Failed to recreate '$model_dir'." info "Copying models back from pre-restore backup '$pre_restore_dir'..." if cp -a "$pre_restore_dir/." "$model_dir/"; then success "Successfully restored models from the pre-restore backup at '$pre_restore_dir'." info "You may want to verify the integrity of these models." # Clean up the temporary backup after successful recovery log "Cleaning up temporary pre-restore backup..." rm -rf "$pre_restore_dir" || warn "Failed to remove temporary pre-restore backup." else warn "Failed to restore models from the pre-restore backup." warn "Model directory '$model_dir' might be in an inconsistent state." warn "Your original models might still be in '$pre_restore_dir'." fi else warn "No valid pre-restore backup found or it was empty. Model directory '$model_dir' might be empty or in an inconsistent state." fi wait_for_keypress_keep_screen # Ensure the failure message and recovery status are seen } # Function to clean up unnecessary or temporary files cleanup() { clear_screen echo -e "${BOLD}${CYAN}=== Clean Up Files ===${NC}" echo log "This utility helps free up disk space by removing temporary files, old backups, and build artifacts." echo log "Searching for files to clean up..." local SPACE_FREED=0 local TEMP_SPACE_SAVED=0 # Clean up build artifacts in KoboldCpp directory if [ -d "$KOBOLDCPP_DIR" ]; then cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; } # Clean build directory if it exists if [ -d "build" ]; then local BUILD_SIZE_KB=$(du -sk "build" 2>/dev/null | cut -f1) if [ -n "$BUILD_SIZE_KB" ] && [ "$BUILD_SIZE_KB" -gt 0 ]; then local BUILD_SIZE_BYTES=$((BUILD_SIZE_KB * 1024)) echo -n "Clean build artifacts in '$KOBOLDCPP_DIR/build' ($(numfmt --to=iec-i --suffix=B "$BUILD_SIZE_BYTES" 2>/dev/null || echo "$BUILD_SIZE_BYTES bytes"))? [Y/n]: " read -r CLEAN_BUILD if [[ ! "${CLEAN_BUILD:-Y}" =~ ^[Nn]$ ]]; then rm -rf build/ TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + BUILD_SIZE_BYTES)) success "Build artifacts cleaned." fi else info "No significant build artifacts found in '$KOBOLDCPP_DIR/build'." fi else info "Build directory '$KOBOLDCPP_DIR/build' not found." fi # Compact Git repository if [ -d ".git" ]; then local GIT_SIZE_BEFORE_KB=$(du -sk ".git" 2>/dev/null | cut -f1) if [ -n "$GIT_SIZE_BEFORE_KB" ] && [ "$GIT_SIZE_BEFORE_KB" -gt 0 ]; then local GIT_SIZE_BEFORE_BYTES=$((GIT_SIZE_BEFORE_KB * 1024)) echo -n "Compact Git repository in '$KOBOLDCPP_DIR/.git' ($(numfmt --to=iec-i --suffix=B "$GIT_SIZE_BEFORE_BYTES" 2>/dev/null || echo "$GIT_SIZE_BEFORE_BYTES bytes"))? [Y/n]: " read -r CLEAN_GIT if [[ ! "${CLEAN_GIT:-Y}" =~ ^[Nn]$ ]]; then log "Running git gc --aggressive..." git gc --aggressive --prune=now 2>&1 | grep -v 'Counting objects:' | grep -v 'Delta compression' | grep -v 'Compressing objects' | grep -v 'Writing objects' | grep -v 'Total' # Suppress standard gc output local GIT_SIZE_AFTER_KB=$(du -sk ".git" 2>/dev/null | cut -f1) if [ -n "$GIT_SIZE_AFTER_KB" ] && [ "$GIT_SIZE_AFTER_KB" -ge 0 ]; then local GIT_SIZE_AFTER_BYTES=$((GIT_SIZE_AFTER_KB * 1024)) local saved=$((GIT_SIZE_BEFORE_BYTES - GIT_SIZE_AFTER_BYTES)) if [ "$saved" -gt 0 ]; then TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + saved)) success "Git repository compacted, saved $(numfmt --to=iec-i --suffix=B "$saved" 2>/dev/null || echo "$saved bytes")." else info "Git repository already optimized, no significant space saved." fi else warn "Could not determine Git repository size after compaction." fi fi else info "No significant Git repository data found in '$KOBOLDCPP_DIR/.git'." fi else info "Git directory '$KOBOLDCPP_DIR/.git' not found." fi fi # Clean up temporary files and manage old backups in BACKUP_DIR if [ -d "$BACKUP_DIR" ]; then local temp_backups=$(find "$BACKUP_DIR" -maxdepth 1 -name "pre_restore_*" -type d 2>/dev/null) if [ -n "$temp_backups" ]; then local TEMP_SIZE_KB=0 while IFS= read -r dir; do local size_kb=$(du -sk "$dir" 2>/dev/null | cut -f1) if [ -n "$size_kb" ]; then TEMP_SIZE_KB=$((TEMP_SIZE_KB + size_kb)) fi done <<< "$temp_backups" local TEMP_SIZE_BYTES=$((TEMP_SIZE_KB * 1024)) if [ "$TEMP_SIZE_BYTES" -gt 0 ]; then echo -n "Clean up temporary pre-restore backup directories ($(numfmt --to=iec-i --suffix=B "$TEMP_SIZE_BYTES" 2>/dev/null || echo "$TEMP_SIZE_BYTES bytes"))? [Y/n]: " read -r CLEAN_TEMP if [[ ! "${CLEAN_TEMP:-Y}" =~ ^[Nn]$ ]]; then while IFS= read -r dir; do rm -rf "$dir" done <<< "$temp_backups" SPACE_FREED=$((SPACE_FREED + TEMP_SIZE_BYTES)) # This is permanent space freed success "Temporary backup files cleaned." fi else info "No significant temporary backup directories found in '$BACKUP_DIR'." fi else info "No temporary pre-restore backup directories found in '$BACKUP_DIR'." fi # Manage old backups local backup_files=($(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -print0 2>/dev/null | xargs -0)) local backup_count=${#backup_files[@]} if [ "$backup_count" -gt 5 ]; then echo -n "You have $backup_count model backups. Keep only the 5 most recent? [y/N]: " read -r CLEAN_BACKUPS if [[ "${CLEAN_BACKUPS:-N}" =~ ^[Yy]$ ]]; then # Find backups to remove (older than the 5 most recent) local old_backups=($(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -printf "%T@ %p\n" 2>/dev/null | sort -n | head -n $((backup_count - 5)) | cut -d' ' -f2-)) local OLD_SIZE_BYTES=0 local files_removed=0 for file in "${old_backups[@]}"; do if [ -f "$file" ]; then # Ensure file still exists before attempting to delete local size=$(get_file_size_bytes "$file") OLD_SIZE_BYTES=$((OLD_SIZE_BYTES + size)) if rm -f "$file"; then ((files_removed++)) else warn "Failed to delete old backup file: $file" fi fi done SPACE_FREED=$((SPACE_FREED + OLD_SIZE_BYTES)) # This is permanent space freed success "Removed $files_removed old backups, freed $(numfmt --to=iec-i --suffix=B "$OLD_SIZE_BYTES" 2>/dev/null || echo "$OLD_SIZE_BYTES bytes")." fi elif [ "$backup_count" -gt 0 ]; then info "You have $backup_count model backups. No old backups to remove (keeping 5 most recent)." else info "No model backups found in '$BACKUP_DIR'." fi else info "Backup directory '$BACKUP_DIR' not found." fi # Log file rotation if [ -f "$LOG_FILE" ]; then local log_size=$(get_file_size_bytes "$LOG_FILE") if [ "$log_size" -gt 1048576 ]; then # 1MB echo -n "Rotate log file ($(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes"))? [Y/n]: " read -r ROTATE_LOG if [[ ! "${ROTATE_LOG:-Y}" =~ ^[Nn]$ ]]; then mv "$LOG_FILE" "${LOG_FILE}.old" touch "$LOG_FILE" echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: Log rotated, old log saved as ${LOG_FILE}.old" >> "$LOG_FILE" success "Log file rotated, freed $(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes")." TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + log_size)) # This space is potentially recoverable by deleting the .old file fi else info "Log file size is manageable ($(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes"))." fi else info "Log file '$LOG_FILE' not found." fi # Show summary echo if [ "$SPACE_FREED" -gt 0 ] || [ "$TEMP_SPACE_SAVED" -gt 0 ]; then success "Cleanup complete." if [ "$SPACE_FREED" -gt 0 ]; then info "Permanent space freed: $(numfmt --to=iec-i --suffix=B "$SPACE_FREED" 2>/dev/null || echo "$SPACE_FREED bytes")." fi if [ "$TEMP_SPACE_SAVED" -gt 0 ]; then info "Temporary/compressible space cleaned: $(numfmt --to=iec-i --suffix=B "$TEMP_SPACE_SAVED" 2>/dev/null || echo "$TEMP_SPACE_SAVED bytes")." fi else info "No files were cleaned up." fi press_enter return 0 } install_update_menu() { while true; do clear_screen echo -e "${BOLD}${CYAN}=== Installation, Updates, & Rebuild ===${NC}" echo local kobold_installed=false if [ -d "$KOBOLDCPP_DIR" ] && ([ -f "$KOBOLDCPP_DIR/koboldcpp.py" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]); then kobold_installed=true echo -e "${GREEN}KoboldCpp is currently installed in $KOBOLDCPP_DIR.${NC}" if [ -d "$KOBOLDCPP_DIR/.git" ]; then local current_commit=$(git -C "$KOBOLDCPP_DIR" rev-parse --short HEAD 2>/dev/null || echo "Unknown") echo -e "Current version (commit): ${YELLOW}$current_commit${NC}" fi else echo -e "${YELLOW}KoboldCpp is not currently installed.${NC}" fi echo echo -e "${BOLD}Options:${NC}" echo echo "1. Install / Reinstall KoboldCpp" if [ "$kobold_installed" = true ]; then echo "2. Update KoboldCpp (fetch latest code and rebuild)" echo "3. Rebuild KoboldCpp (from current source code)" fi echo "0. Back to Main Menu" echo echo -n "Enter your choice: " read -r CHOICE case "${CHOICE:-}" in 1) install_all ;; 2) if [ "$kobold_installed" = true ]; then update_koboldcpp else error "Option 2 is only available if KoboldCpp is installed." wait_for_keypress_keep_screen fi ;; 3) if [ "$kobold_installed" = true ]; then rebuild else error "Option 3 is only available if KoboldCpp is installed." wait_for_keypress_keep_screen fi ;; 0) return ;; *) error "Invalid choice. Please try again." wait_for_keypress_keep_screen ;; esac done } backup_restore_menu() { while true; do clear_screen echo -e "${BOLD}${CYAN}=== Backup & Restore Models ===${NC}" echo local backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f 2>/dev/null | wc -l) echo -e "Backups found in $BACKUP_DIR: ${YELLOW}${backup_count:-0}${NC}" echo echo -e "${BOLD}Options:${NC}" echo echo "1. Backup current models" echo "2. Restore models from backup" echo "0. Back to Main Menu" echo echo -n "Enter your choice: " read -r CHOICE case "${CHOICE:-}" in 1) backup_models ;; 2) restore_models ;; 0) return ;; *) error "Invalid choice. Please try again." wait_for_keypress_keep_screen ;; esac done } advanced_menu() { while true; do clear_screen echo -e "${BOLD}${CYAN}=== Advanced Options ===${NC}" echo echo -e "${YELLOW}Use these options with caution.${NC}" echo echo -e "${BOLD}Options:${NC}" echo echo "1. Clean up temporary files and old backups" echo "2. Open log file ($LOG_FILE)" echo "3. Reset configuration file ($CONFIG_FILE)" echo "0. Back to Main Menu" echo echo -n "Enter your choice: " read -r CHOICE case "${CHOICE:-}" in 1) cleanup ;; 2) if [ -f "$LOG_FILE" ]; then log "Opening log file in default viewer..." termux-open "$LOG_FILE" || { warn "Failed to open log file. You might need to install a text editor or file viewer." info "You can view it manually using: less \"$LOG_FILE\"" wait_for_keypress_keep_screen } else warn "Log file '$LOG_FILE' not found." wait_for_keypress_keep_screen fi ;; 3) echo -e "${RED}WARNING: This will delete your saved configurations (last used model, run parameters, etc.).${NC}" echo -n "Are you sure you want to reset the configuration file '$CONFIG_FILE'? [y/N]: " read -r RESET_CONFIRM if [[ "${RESET_CONFIRM:-N}" =~ ^[Yy]$ ]]; then if rm -f "$CONFIG_FILE"; then success "Configuration file '$CONFIG_FILE' reset." # Re-initialize default build options save_config "build_options" "0" info "Default build options reset." else error "Failed to reset configuration file '$CONFIG_FILE'. Check permissions." fi else info "Configuration reset cancelled." fi press_enter ;; 0) return ;; *) error "Invalid choice. Please try again." wait_for_keypress_keep_screen ;; esac done } about() { clear_screen echo -e "${BOLD}${CYAN}=== About KoboldCpp Termux Manager ===${NC}" echo echo "KoboldCpp Termux Manager v$VERSION" echo echo "This tool helps you install, manage, and run KoboldCpp on Android using Termux." echo echo "Original KoboldCpp project: $REPO_URL" echo echo "Installation directory: ${BLUE}$KOBOLDCPP_DIR${NC}" echo "Model Cache directory: ${BLUE}$MODEL_DIR${NC}" # Clarified as Model Cache echo "Downloads directory: ${BLUE}$DOWNLOAD_DIR${NC}" echo "Backups directory: ${BLUE}$BACKUP_DIR${NC}" echo "Log file: ${BLUE}$LOG_FILE${NC}" echo "Config file: ${BLUE}$CONFIG_FILE${NC}" echo log "The Model Cache directory ($MODEL_DIR) is where your downloaded or added model (.gguf) files are stored for quick access and management by this script." echo # Show system information local device_info=$(get_device_info) if [ -n "$device_info" ]; then echo "Device information: ${BLUE}$device_info${NC}" echo fi # Show usage statistics if available if [ -f "$CONFIG_FILE" ]; then local last_runtime_model=$(grep "^last_runtime_model=" "$CONFIG_FILE" | cut -d= -f2) local last_runtime_hours=$(grep "^last_runtime_hours=" "$CONFIG_FILE" | cut -d= -f2) local last_runtime_minutes=$(grep "^last_runtime_minutes=" "$CONFIG_FILE" | cut -d= -f2) local last_runtime_seconds=$(grep "^last_runtime_seconds=" "$CONFIG_FILE" | cut -d= -f2) if [ -n "$last_runtime_model" ]; then echo "Last session:" echo "- Model: ${CYAN}$last_runtime_model${NC}" if [ -n "$last_runtime_hours" ]; then echo "- Runtime: ${CYAN}${last_runtime_hours}h ${last_runtime_minutes}m ${last_runtime_seconds}s${NC}" fi echo fi fi echo "New in version $VERSION:" echo "- Completed Model Management menu." echo "- Added 'Verify model integrity' option to Model Management." echo "- Clarified '$MODEL_DIR' as the Model Cache directory." echo "- Improved input validation for run parameters." echo "- Enhanced file size checks and reporting." echo "- Better error handling and cleanup for downloads and restores." echo "- Added option to import backups in Restore menu." echo "- Option to move backups to external storage." echo "- More detailed output during cleanup." echo press_enter } clean_exit() { clear_screen if command -v termux-wake-unlock &>/dev/null; then log "Releasing wake lock on exit (if active)..." termux-wake-unlock 2>/dev/null || true # Ignore error if no lock was active fi echo "--- KoboldCpp Termux Manager Session Ended: $(date '+%Y-%m-%d %H:%M:%S') ---" >> "$LOG_FILE" echo "Thank you for using KoboldCpp Termux Manager!" # Remove trap for temp file if it was set and not cleared by _initialize_du_commands # This is a general cleanup, though _initialize_du_commands tries to manage its own trap. trap - EXIT exit 0 } # --- Check for updates --- check_script_updates() { # This is a placeholder function - in a real implementation, this would # check a repository or URL for newer versions of the script # For now, it just returns 0 to indicate no check is performed. return 0 } # --- Menus --- main_menu() { echo "--- KoboldCpp Termux Manager Session Started: $(date '+%Y-%m-%d %H:%M:%S') ---" >> "$LOG_FILE" # Check for script updates (placeholder) # check_script_updates trap clean_exit SIGINT SIGTERM while true; do clear_screen echo -e "${BOLD}${CYAN}=== KoboldCpp Termux Manager v$VERSION ===${NC}" echo log "Your model cache is located at: $MODEL_DIR" echo local MODEL_COUNT=0 if [ -d "$MODEL_DIR" ]; then # Use find with null delimiter for safety MODEL_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | grep -c -z '\.gguf$') fi local py_script_exists=false local binary_exists=false local kobold_installed=false if [ -d "$KOBOLDCPP_DIR" ]; then if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then py_script_exists=true fi if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then binary_exists=true fi if [ "$py_script_exists" = true ] || [ "$binary_exists" = true ]; then kobold_installed=true fi fi if [ "$kobold_installed" = true ]; then echo -e "${GREEN}KoboldCpp is installed.${NC}" echo -e "Models in cache: ${YELLOW}${MODEL_COUNT:-0}${NC}" # Show last run information if available if [ -f "$CONFIG_FILE" ]; then local last_runtime_model=$(grep "^last_runtime_model=" "$CONFIG_FILE" | cut -d= -f2) local last_runtime_hours=$(grep "^last_runtime_hours=" "$CONFIG_FILE" | cut -d= -f2) if [ -n "$last_runtime_model" ]; then local last_model_base=$(basename "$last_runtime_model") # Show just filename local last_model_status="" if [ -n "$last_runtime_hours" ]; then last_model_status="(last ran)" else last_model_status="(last selected, possibly failed)" # Indicate if runtime wasn't saved fi echo -e "Last model used: ${CYAN}$last_model_base${NC} $last_model_status" fi fi else echo -e "${YELLOW}KoboldCpp is not installed (or key components are missing).${NC}" fi # Show device info local device_info=$(get_device_info) if [ -n "$device_info" ]; then echo -e "Device: ${BLUE}$device_info${NC}" fi echo echo -e "${BOLD}Main Menu:${NC}" echo echo "1. Run KoboldCpp (Select model from cache)" echo "2. Add model to cache (from Downloads)" echo "3. Download model to cache (from URL)" echo "4. Manage models in cache" # Renamed for clarity echo "5. Install / Update / Rebuild KoboldCpp" echo "6. Backup & Restore Models" echo "7. Advanced options" echo "8. About" echo "0. Exit" echo echo -n "Enter your choice: " read -r CHOICE case "${CHOICE:-}" in # Handle empty CHOICE 1) run_model ;; 2) add_model ;; 3) download_model ;; 4) models_menu ;; 5) install_update_menu ;; 6) backup_restore_menu ;; 7) advanced_menu ;; 8) about ;; 0) clean_exit ;; *) error "Invalid choice. Please try again." wait_for_keypress_keep_screen ;; esac done } models_menu() { while true; do clear_screen echo -e "${BOLD}${CYAN}=== Model Cache Management ===${NC}" echo log "Manage your .gguf models stored in the cache directory: $MODEL_DIR" echo local MODEL_COUNT=0 if [ -d "$MODEL_DIR" ]; then # Use find with null delimiter for safety MODEL_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | grep -c -z '\.gguf$') fi echo -e "Models currently in cache: ${YELLOW}${MODEL_COUNT:-0}${NC}" echo echo -e "${BOLD}Options:${NC}" echo echo "1. List all models in cache" echo "2. Add model to cache (from Downloads)" # Re-added for convenience echo "3. Download model to cache (from URL)" # Re-added for convenience echo "4. Delete a model from cache" echo "5. Verify integrity of a model in cache" # Calls the new interactive function echo "0. Back to Main Menu" echo echo -n "Enter your choice: " read -r CHOICE case "${CHOICE:-}" in 1) list_models ;; 2) add_model ;; 3) download_model ;; 4) delete_model ;; 5) verify_model_integrity_interactive ;; # Call the new function here 0) return ;; *) error "Invalid choice. Please try again." wait_for_keypress_keep_screen ;; esac done } # --- Script Entry Point --- main_menu