| | #!/bin/bash |
| | set -uo pipefail |
| | set +H |
| |
|
| | export PIP_NO_CACHE_DIR=1 |
| | export PIP_DISABLE_PIP_VERSION_CHECK=1 |
| | export GIT_TERMINAL_PROMPT=0 |
| |
|
| | source /venv/main/bin/activate |
| | COMFYUI_DIR="${WORKSPACE:-/workspace}/ComfyUI" |
| | if [ -f "/venv/main/bin/python" ]; then |
| | PYTHON_BIN="/venv/main/bin/python" |
| | PIP_BIN="/venv/main/bin/pip" |
| | else |
| | PYTHON_BIN="python3" |
| | PIP_BIN="pip3" |
| | fi |
| |
|
| | APT_PACKAGES=( |
| | "aria2" |
| | ) |
| | PIP_PACKAGES=( |
| | ) |
| | NODES=( |
| | "https://github.com/ltdrdata/ComfyUI-Impact-Pack.git" |
| | "https://github.com/ltdrdata/ComfyUI-Impact-Subpack.git" |
| | "https://github.com/cubiq/ComfyUI_IPAdapter_plus.git" |
| | "https://github.com/ka-puna/comfyui-yanc.git" |
| | ) |
| | CHECKPOINT_MODELS=( |
| | "https://huggingface.co/JCscrew/RisuAI_asset_generator/resolve/main/models/checkpoints/waiNSFWIllustrious_v150.safetensors" |
| | ) |
| | CLIP_VISION_MODELS=( |
| | "https://huggingface.co/JCscrew/RisuAI_asset_generator/resolve/main/models/clip_vision/CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors" |
| | ) |
| | IPADAPTER_MODELS=( |
| | "https://huggingface.co/JCscrew/RisuAI_asset_generator/resolve/main/models/ipadapter/ip-adapter-plus-face_sdxl_vit-h.safetensors" |
| | ) |
| | ESRGAN_MODELS=( |
| | "https://huggingface.co/JCscrew/RisuAI_asset_generator/resolve/main/models/upscale_models/RealESRGAN_x4plus_anime_6B.pth" |
| | ) |
| | WORKFLOWS=( |
| | "https://huggingface.co/JCscrew/RisuAI_asset_generator/resolve/main/Single_Posture.json" |
| | ) |
| |
|
| | log_info() { echo "--> $1"; } |
| | log_warn() { echo " ⚠️ $1"; } |
| | log_error() { echo " ❌ $1"; } |
| | log_success() { echo " ✅ $1"; } |
| | log_step() { echo ""; echo "=== [Step $1] $2 ==="; } |
| |
|
| | package_installed() { |
| | $PYTHON_BIN -c "import $1" 2>/dev/null && return 0 || return 1 |
| | } |
| |
|
| | filter_requirements() { |
| | local req_file="$1" |
| | local tmp_file="${req_file}.filtered" |
| | > "$tmp_file" |
| | while IFS= read -r line; do |
| | [[ -z "$line" || "$line" =~ ^ |
| | local pkg_name |
| | pkg_name=$(echo "$line" | sed 's/[<>=!].*//' | xargs) |
| | if [[ -z "$pkg_name" ]]; then continue; fi |
| | local module_name="${pkg_name//-/_}" |
| | if package_installed "$module_name"; then |
| | log_info "跳過已安裝套件: $pkg_name" |
| | else |
| | echo "$line" >> "$tmp_file" |
| | fi |
| | done < "$req_file" |
| | if [ -s "$tmp_file" ]; then |
| | mv "$tmp_file" "$req_file" |
| | return 0 |
| | else |
| | rm -f "$tmp_file" |
| | log_info "所有套件都已安裝" |
| | return 1 |
| | fi |
| | } |
| |
|
| | provisioning_has_valid_hf_token() { |
| | [[ -n "${HF_TOKEN:-}" ]] || return 1 |
| | local response |
| | response=$(curl -o /dev/null -s -w "%{http_code}" -X GET "https://huggingface.co/api/whoami-v2" -H "Authorization: Bearer $HF_TOKEN" -H "Content-Type: application/json") |
| | [ "$response" -eq 200 ] |
| | } |
| |
|
| | provisioning_has_valid_civitai_token() { |
| | [[ -n "${CIVITAI_TOKEN:-}" ]] || return 1 |
| | local response |
| | response=$(curl -o /dev/null -s -w "%{http_code}" -X GET "https://civitai.com/api/v1/models?hidden=1&limit=1" -H "Authorization: Bearer $CIVITAI_TOKEN" -H "Content-Type: application/json") |
| | [ "$response" -eq 200 ] |
| | } |
| |
|
| | install_node() { |
| | local repo_url="$1" |
| | local repo_name |
| | repo_name=$(basename "$repo_url" .git) |
| | local install_path="${COMFYUI_DIR}/custom_nodes/${repo_name}" |
| |
|
| | if [ -d "$install_path" ]; then |
| | if [[ "${AUTO_UPDATE:-true}" != "false" ]]; then |
| | log_info "更新節點: $repo_name" |
| | (cd "$install_path" && git pull -q 2>&1 | grep -v "Already up to date" || true) |
| | else |
| | log_info "'$repo_name' 已存在,跳過" |
| | return |
| | fi |
| | else |
| | log_info "克隆節點: $repo_name" |
| | git clone --depth 1 --single-branch "$repo_url" "$install_path" -q 2>&1 || true |
| | fi |
| |
|
| | if [ -f "$install_path/requirements.txt" ]; then |
| | log_info "處理 $repo_name 的依賴..." |
| | sed -i -e '/^torch/d' -e '/^sam2/d' "$install_path/requirements.txt" 2>/dev/null || true |
| | if filter_requirements "$install_path/requirements.txt"; then |
| | log_info "安裝 $repo_name 的新依賴..." |
| | $PIP_BIN install -q --no-cache-dir -r "$install_path/requirements.txt" 2>&1 | grep -v "Requirement already satisfied" || log_warn "部分依賴安裝失敗" |
| | fi |
| | fi |
| |
|
| | if [ -f "$install_path/install.py" ]; then |
| | log_info "運行 $repo_name 的安裝腳本..." |
| | $PYTHON_BIN "$install_path/install.py" 2>&1 || log_warn "安裝腳本執行失敗" |
| | fi |
| | } |
| |
|
| | download_file() { |
| | local dest_path="$1" |
| | local url="$2" |
| | local filename |
| | filename=$(basename "$dest_path") |
| | local tmp_path="${dest_path}.tmp" |
| |
|
| | mkdir -p "$(dirname "$dest_path")" |
| | if [ -s "$dest_path" ]; then |
| | log_info "檔案 '$filename' 已存在且完整,跳過下載" |
| | return 0 |
| | fi |
| |
|
| | log_info "下載: $filename" |
| | local max_retries=3 |
| | local attempt=0 |
| | local auth_header="" |
| | local success=1 |
| |
|
| | if [[ "$url" =~ huggingface\.co ]] && provisioning_has_valid_hf_token; then |
| | auth_header="Authorization: Bearer $HF_TOKEN" |
| | log_info "使用 HuggingFace Token" |
| | elif [[ "$url" =~ civitai\.com ]] && provisioning_has_valid_civitai_token; then |
| | auth_header="Authorization: Bearer $CIVITAI_TOKEN" |
| | log_info "使用 CivitAI Token" |
| | fi |
| |
|
| | while [ "$attempt" -lt "$max_retries" ]; do |
| | attempt=$((attempt + 1)) |
| | [ "$attempt" -gt 1 ] && sleep 10 |
| | if command -v aria2c >/dev/null 2>&1; then |
| | log_info "使用 aria2c (3 線程) 下載: $filename (嘗試 $attempt/$max_retries)" |
| | local aria_opts=(--console-log-level=error -c -x 3 -s 3 -k 1M --max-connection-per-server=3 --max-tries=3 --retry-wait=5 --timeout=180 --file-allocation=falloc --auto-file-renaming=false -d "$(dirname "$dest_path")" -o "${filename}.tmp") |
| | [[ -n "$auth_header" ]] && aria_opts+=(--header="$auth_header") |
| | aria2c "${aria_opts[@]}" "$url" |
| | if [ $? -eq 0 ]; then success=0; break; fi |
| | else |
| | log_info "使用 wget 下載: $filename (嘗試 $attempt/$max_retries)" |
| | local wget_opts=(-O "$tmp_path" -c --timeout=60 --tries=3 --content-disposition --show-progress) |
| | [[ -n "$auth_header" ]] && wget_opts+=(--header="$auth_header") |
| | wget "${wget_opts[@]}" "$url" |
| | if [ $? -eq 0 ]; then success=0; break; fi |
| | fi |
| | done |
| |
|
| | if [ "$success" -eq 0 ] && [ -s "$tmp_path" ]; then |
| | mv "$tmp_path" "$dest_path" |
| | log_success "下載完成: $filename" |
| | return 0 |
| | else |
| | log_error "下載失敗: $filename" |
| | rm -f "$tmp_path" |
| | return 1 |
| | fi |
| | } |
| |
|
| | download_to_directory() { |
| | local dest_dir="$1"; shift; local urls=("$@") |
| | if [ ${#urls[@]} -eq 0 ]; then return 0; fi |
| | mkdir -p "$dest_dir" |
| | log_info "下載 ${#urls[@]} 個文件到 $dest_dir" |
| | local MAX_PARALLEL=3 |
| | for url in "${urls[@]}"; do |
| | while [ $(jobs -r | wc -l) -ge $MAX_PARALLEL ]; do sleep 1; done |
| | local filename |
| | filename=$(basename "$url" | sed 's/?.*//') |
| | download_file "${dest_dir}/${filename}" "$url" & |
| | done |
| | wait |
| | } |
| |
|
| | verify_and_retry_downloads() { |
| | local dest_dir="$1"; shift; local urls=("$@") |
| | if [ ${#urls[@]} -eq 0 ]; then return 0; fi |
| | log_info "檢查 $dest_dir 中的文件..." |
| | local missing_files=() |
| | for url in "${urls[@]}"; do |
| | local filename |
| | filename=$(basename "$url" | sed 's/?.*//') |
| | local dest_path="${dest_dir}/${filename}" |
| | if [ ! -s "$dest_path" ]; then |
| | log_warn "檔案缺失或不完整: $filename" |
| | missing_files+=("$url") |
| | fi |
| | done |
| | if [ ${#missing_files[@]} -gt 0 ]; then |
| | log_warn "發現 ${#missing_files[@]} 個缺失/不完整文件,重新下載..." |
| | download_to_directory "$dest_dir" "${missing_files[@]}" |
| | else |
| | log_success "所有文件已確認存在且完整" |
| | fi |
| | } |
| |
|
| | provisioning_print_header() { |
| | echo ""; echo "##############################################" |
| | echo "# #" |
| | echo "# Provisioning Container #" |
| | echo "# #" |
| | echo "# This will take some time #" |
| | echo "# #" |
| | echo "##############################################"; echo "" |
| | } |
| |
|
| | provisioning_print_end() { |
| | echo ""; echo "##############################################" |
| | echo "# #" |
| | echo "# Provisioning Complete! #" |
| | echo "# #" |
| | echo "# Total time: $((END_TIME - START_TIME)) seconds #" |
| | echo "# #" |
| | echo "##############################################"; echo "" |
| | } |
| |
|
| | provisioning_step1_install_core_deps() { |
| | log_step "1" "安裝系統與 ComfyUI 核心依賴" |
| | if [ ${#APT_PACKAGES[@]} -gt 0 ]; then |
| | log_info "準備安裝 ${#APT_PACKAGES[@]} 個系統套件..." |
| | local packages_to_install=() |
| | for pkg in "${APT_PACKAGES[@]}"; do |
| | if ! dpkg -s "$pkg" >/dev/null 2>&1; then |
| | packages_to_install+=("$pkg") |
| | else |
| | log_success "$pkg 已安裝,跳過" |
| | fi |
| | done |
| | if [ ${#packages_to_install[@]} -gt 0 ]; then |
| | log_info "正在安裝: ${packages_to_install[*]}" |
| | apt-get update -qq && apt-get install -y -qq "${packages_to_install[@]}" && log_success "系統套件安裝完成" || log_warn "部分系統套件安裝失敗" |
| | fi |
| | fi |
| |
|
| | local comfyui_req_file="${COMFYUI_DIR}/requirements.txt" |
| | if [ -f "$comfyui_req_file" ]; then |
| | log_info "處理 ComfyUI 核心依賴: $comfyui_req_file" |
| | if filter_requirements "$comfyui_req_file"; then |
| | log_info "安裝 ComfyUI 的新依賴..." |
| | $PIP_BIN install -q --no-cache-dir -r "$comfyui_req_file" 2>&1 | grep -v "Requirement already satisfied" || log_warn "ComfyUI 部分核心依賴安裝失敗" |
| | fi |
| | else |
| | log_warn "找不到 ComfyUI 的核心 requirements.txt 文件!" |
| | fi |
| | } |
| |
|
| | provisioning_step2_nodes() { |
| | log_step "2" "安裝節點與額外套件" |
| | if [ ${#NODES[@]} -gt 0 ]; then |
| | cd "${COMFYUI_DIR}/custom_nodes" || exit 1 |
| | log_info "並行安裝 ${#NODES[@]} 個節點..." |
| | local MAX_PARALLEL=2 |
| | for node in "${NODES[@]}"; do |
| | while [ $(jobs -r | wc -l) -ge $MAX_PARALLEL ]; do sleep 1; done |
| | install_node "$node" & |
| | done |
| | wait |
| | log_success "節點安裝完成" |
| | cd "${COMFYUI_DIR}" || exit 1 |
| | fi |
| | } |
| |
|
| | provisioning_step3_downloads() { |
| | log_step "3" "下載模型與工作流文件" |
| | download_to_directory "${COMFYUI_DIR}/models/checkpoints" "${CHECKPOINT_MODELS[@]}" |
| | download_to_directory "${COMFYUI_DIR}/models/clip_vision" "${CLIP_VISION_MODELS[@]}" |
| | download_to_directory "${COMFYUI_DIR}/models/ipadapter" "${IPADAPTER_MODELS[@]}" |
| | download_to_directory "${COMFYUI_DIR}/models/upscale_models" "${ESRGAN_MODELS[@]}" |
| | if [ ${#WORKFLOWS[@]} -gt 0 ]; then |
| | log_info "下載工作流文件..." |
| | download_to_directory "${COMFYUI_DIR}/user/default/workflows" "${WORKFLOWS[@]}" |
| | fi |
| | } |
| |
|
| | if [ -f "/workspace/.provision_complete" ]; then |
| | log_success "環境已配置,跳過重複執行" |
| | log_info "如需重新配置,請刪除 /workspace/.provision_complete" |
| | exit 0 |
| | fi |
| | if [[ -f /.noprovisioning ]]; then |
| | log_warn "檢測到 /.noprovisioning 文件,跳過配置" |
| | exit 0 |
| | fi |
| |
|
| | START_TIME=$(date +%s) |
| | provisioning_print_header |
| | cd "${COMFYUI_DIR}" || exit 1 |
| |
|
| | provisioning_step1_install_core_deps |
| |
|
| | provisioning_step2_nodes & |
| | NODE_PID=$! |
| | provisioning_step3_downloads |
| | wait $NODE_PID |
| |
|
| | log_step "4" "驗證下載完整性" |
| | verify_and_retry_downloads "${COMFYUI_DIR}/models/checkpoints" "${CHECKPOINT_MODELS[@]}" |
| | verify_and_retry_downloads "${COMFYUI_DIR}/models/clip_vision" "${CLIP_VISION_MODELS[@]}" |
| | verify_and_retry_downloads "${COMFYUI_DIR}/models/ipadapter" "${IPADAPTER_MODELS[@]}" |
| | verify_and_retry_downloads "${COMFYUI_DIR}/models/upscale_models" "${ESRGAN_MODELS[@]}" |
| | verify_and_retry_downloads "${COMFYUI_DIR}/user/default/workflows" "${WORKFLOWS[@]}" |
| |
|
| | log_step "5" "完成配置" |
| | touch "/workspace/.provision_complete" |
| | log_success "配置標記文件已創建" |
| |
|
| | END_TIME=$(date +%s) |
| | provisioning_print_end |
| | log_success "所有配置步驟已完成!" |
| | exit 0 |