#!/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
Comments
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎