mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 18 QA-discovered bugs across tools, previews, and the AI pipeline (#242)
Exhaustive QA sweep of all 157 tools. Fixes: CSP blob media, csv-excel ExcelJS interop, ocr-pdf segfault, chart-maker upload, non-PDF doc preview, RAW decode, merge-tool multi-file path, html-to-image chromium, ogv/wma/amr/ac3 preview fallbacks, meme/gif/stabilize codecs, nav+home a11y. Plus orphan-format and test-debt cleanup, the AI bundle build script, and a reusable Playwright QA harness under tests/qa/.
This commit is contained in:
Executable
+636
@@ -0,0 +1,636 @@
|
||||
#!/usr/bin/env bash
|
||||
# seed-ai-models.sh -- Install real AI models into a running SnapOtter container.
|
||||
# Designed for QA seeding: runs entirely via `docker exec`, idempotent, resumable.
|
||||
# Usage: bash tests/qa/seed-ai-models.sh [container_name]
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CONTAINER="${1:-snapotter-qa}"
|
||||
# IMPORTANT: Use "python3 -m pip" NOT the pip binary directly.
|
||||
# The pip shebang points to /opt/venv/bin/python3 (the build-time venv),
|
||||
# but the runtime venv is /data/ai/venv. Using python3 -m pip ensures
|
||||
# packages land in the correct site-packages.
|
||||
PYTHON="/data/ai/venv/bin/python3"
|
||||
MODELS="/data/ai/models"
|
||||
INSTALLED="/data/ai/installed.json"
|
||||
VERSION="2.0.0"
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
dexec() { docker exec "$CONTAINER" "$@"; }
|
||||
dpip() { docker exec "$CONTAINER" "$PYTHON" -m pip "$@"; }
|
||||
dpython() { docker exec "$CONTAINER" "$PYTHON" "$@"; }
|
||||
|
||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
ok() { printf '[%s] \033[32mOK\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
warn() { printf '[%s] \033[33mWARN\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
fail() { printf '[%s] \033[31mFAIL\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
|
||||
|
||||
declare -A BUNDLE_STATUS # populated as we go
|
||||
|
||||
# Check a file exists in the container and meets a minimum size (0 = any).
|
||||
file_ok() {
|
||||
local path="$1" min_size="${2:-0}"
|
||||
local size
|
||||
size=$(dexec stat -c '%s' "$path" 2>/dev/null) || return 1
|
||||
[[ "$size" -ge "$min_size" ]]
|
||||
}
|
||||
|
||||
# Check a directory exists (non-empty) in the container.
|
||||
dir_ok() {
|
||||
dexec test -d "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Install pip packages idempotently. Accepts flags BEFORE the package spec.
|
||||
pip_install() {
|
||||
local flags=()
|
||||
while [[ "$1" == --* ]]; do flags+=("$1"); shift; done
|
||||
local pkg="$1"
|
||||
log "pip install ${flags[*]:-} $pkg"
|
||||
dpip install --cache-dir /data/ai/pip-cache "${flags[@]}" "$pkg" 2>&1
|
||||
}
|
||||
|
||||
# Download a URL to a path inside the container (skip if already present).
|
||||
curl_model() {
|
||||
local url="$1" dest="$2" min_size="${3:-0}"
|
||||
if file_ok "$dest" "$min_size"; then
|
||||
ok "Already present: $dest"
|
||||
return 0
|
||||
fi
|
||||
local dir
|
||||
dir=$(dirname "$dest")
|
||||
dexec mkdir -p "$dir"
|
||||
log "Downloading $(basename "$dest") ..."
|
||||
dexec curl -fSL --retry 3 --retry-delay 5 -o "$dest" "$url" 2>&1
|
||||
}
|
||||
|
||||
# Download a single file from HuggingFace Hub (skip if already present).
|
||||
hf_file() {
|
||||
local repo="$1" filename="$2" local_dir="$3" min_size="${4:-0}" repo_type="${5:-model}"
|
||||
local dest="$local_dir/$filename"
|
||||
if file_ok "$dest" "$min_size"; then
|
||||
ok "Already present: $dest"
|
||||
return 0
|
||||
fi
|
||||
log "HF download $repo / $filename -> $local_dir"
|
||||
dpython -c "
|
||||
from huggingface_hub import hf_hub_download
|
||||
hf_hub_download('$repo', '$filename', local_dir='$local_dir', repo_type='$repo_type')
|
||||
print('Done')
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
# Download a full HuggingFace repo snapshot (skip if dir already exists).
|
||||
hf_snapshot() {
|
||||
local repo="$1" local_dir="$2"
|
||||
if dir_ok "$local_dir"; then
|
||||
ok "Already present: $local_dir"
|
||||
return 0
|
||||
fi
|
||||
log "HF snapshot $repo -> $local_dir"
|
||||
dpython -c "
|
||||
from huggingface_hub import snapshot_download
|
||||
snapshot_download('$repo', local_dir='$local_dir')
|
||||
print('Done')
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
# Write/update a bundle entry in installed.json.
|
||||
mark_installed() {
|
||||
local bundle_id="$1"
|
||||
shift
|
||||
# Remaining args are model IDs
|
||||
local models_json="["
|
||||
local first=true
|
||||
for m in "$@"; do
|
||||
if $first; then first=false; else models_json+=","; fi
|
||||
models_json+="\"$m\""
|
||||
done
|
||||
models_json+="]"
|
||||
|
||||
dpython -c "
|
||||
import json, os
|
||||
path = '$INSTALLED'
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
except:
|
||||
data = {'bundles': {}}
|
||||
from datetime import datetime, timezone
|
||||
data['bundles']['$bundle_id'] = {
|
||||
'version': '$VERSION',
|
||||
'installedAt': datetime.now(timezone.utc).isoformat(),
|
||||
'models': $models_json
|
||||
}
|
||||
with open(path + '.tmp', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
os.rename(path + '.tmp', path)
|
||||
print('Marked $bundle_id as installed')
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 1. FACE-DETECTION (~200-300 MB)
|
||||
# ========================================================================
|
||||
install_face_detection() {
|
||||
log "=== Bundle 1/7: face-detection ==="
|
||||
|
||||
# --- pip packages ---
|
||||
pip_install "mediapipe>=0.10.18" || { fail "mediapipe pip failed"; BUNDLE_STATUS[face-detection]="FAILED: mediapipe pip install"; return 1; }
|
||||
|
||||
# --- models ---
|
||||
curl_model \
|
||||
"https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite" \
|
||||
"$MODELS/mediapipe/blaze_face_short_range.tflite" 100000 \
|
||||
|| { fail "blaze_face download failed"; BUNDLE_STATUS[face-detection]="FAILED: model download"; return 1; }
|
||||
|
||||
curl_model \
|
||||
"https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task" \
|
||||
"$MODELS/mediapipe/face_landmarker.task" 1000000 \
|
||||
|| { fail "face_landmarker download failed"; BUNDLE_STATUS[face-detection]="FAILED: model download"; return 1; }
|
||||
|
||||
# --- verify ---
|
||||
if file_ok "$MODELS/mediapipe/blaze_face_short_range.tflite" 100000 && \
|
||||
file_ok "$MODELS/mediapipe/face_landmarker.task" 1000000; then
|
||||
mark_installed face-detection mediapipe-face-detector mediapipe-face-landmarker
|
||||
BUNDLE_STATUS[face-detection]="INSTALLED"
|
||||
ok "face-detection complete"
|
||||
else
|
||||
fail "face-detection verification failed"
|
||||
BUNDLE_STATUS[face-detection]="FAILED: verification"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 2. TRANSCRIPTION (~600 MB)
|
||||
# ========================================================================
|
||||
install_transcription() {
|
||||
log "=== Bundle 2/7: transcription ==="
|
||||
|
||||
# --- pip packages ---
|
||||
pip_install "huggingface-hub" || { fail "huggingface-hub pip failed"; BUNDLE_STATUS[transcription]="FAILED: pip"; return 1; }
|
||||
pip_install "faster-whisper>=1.0.0" || { fail "faster-whisper pip failed"; BUNDLE_STATUS[transcription]="FAILED: pip"; return 1; }
|
||||
|
||||
# --- model (full repo snapshot) ---
|
||||
hf_snapshot "Systran/faster-whisper-small" "$MODELS/faster-whisper-small" \
|
||||
|| { fail "faster-whisper-small download failed"; BUNDLE_STATUS[transcription]="FAILED: model download"; return 1; }
|
||||
|
||||
# --- verify ---
|
||||
if dir_ok "$MODELS/faster-whisper-small"; then
|
||||
mark_installed transcription faster-whisper-small
|
||||
BUNDLE_STATUS[transcription]="INSTALLED"
|
||||
ok "transcription complete"
|
||||
else
|
||||
fail "transcription verification failed"
|
||||
BUNDLE_STATUS[transcription]="FAILED: verification"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 3. OBJECT-ERASER-COLORIZE (~1-2 GB)
|
||||
# ========================================================================
|
||||
install_object_eraser_colorize() {
|
||||
log "=== Bundle 3/7: object-eraser-colorize ==="
|
||||
|
||||
# --- pip packages (huggingface-hub already from transcription) ---
|
||||
pip_install "huggingface-hub" || true # already installed, no-op
|
||||
pip_install "onnxruntime==1.20.1" || { fail "onnxruntime pip failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: pip"; return 1; }
|
||||
|
||||
# --- LaMa ONNX model ---
|
||||
hf_file "Carve/LaMa-ONNX" "lama_fp32.onnx" "$MODELS/lama" 100000000 \
|
||||
|| { fail "lama download failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: lama model"; return 1; }
|
||||
|
||||
# --- DDColor ONNX model (from facefusion repo, single file) ---
|
||||
hf_file "facefusion/models-3.0.0" "ddcolor.onnx" "$MODELS/ddcolor" 50000000 \
|
||||
|| { fail "ddcolor download failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: ddcolor model"; return 1; }
|
||||
|
||||
# --- OpenCV colorize prototxt ---
|
||||
curl_model \
|
||||
"https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/models/colorization_deploy_v2.prototxt" \
|
||||
"$MODELS/colorize-opencv/colorization_deploy_v2.prototxt" 0 \
|
||||
|| { fail "prototxt download failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: prototxt"; return 1; }
|
||||
|
||||
# --- OpenCV colorize points (npy) ---
|
||||
curl_model \
|
||||
"https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/resources/pts_in_hull.npy" \
|
||||
"$MODELS/colorize-opencv/pts_in_hull.npy" 0 \
|
||||
|| { fail "npy download failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: npy"; return 1; }
|
||||
|
||||
# --- OpenCV colorize caffemodel (from HF space) ---
|
||||
hf_file "BilalSardar/Black-N-White-To-Color" "colorization_release_v2.caffemodel" \
|
||||
"$MODELS/colorize-opencv" 100000000 "space" \
|
||||
|| { fail "caffemodel download failed"; BUNDLE_STATUS[object-eraser-colorize]="FAILED: caffemodel"; return 1; }
|
||||
|
||||
# --- verify ---
|
||||
local all_ok=true
|
||||
file_ok "$MODELS/lama/lama_fp32.onnx" 100000000 || all_ok=false
|
||||
file_ok "$MODELS/ddcolor/ddcolor.onnx" 50000000 || all_ok=false
|
||||
file_ok "$MODELS/colorize-opencv/colorization_deploy_v2.prototxt" 0 || all_ok=false
|
||||
file_ok "$MODELS/colorize-opencv/pts_in_hull.npy" 0 || all_ok=false
|
||||
file_ok "$MODELS/colorize-opencv/colorization_release_v2.caffemodel" 100000000 || all_ok=false
|
||||
|
||||
if $all_ok; then
|
||||
mark_installed object-eraser-colorize \
|
||||
lama-onnx ddcolor-onnx opencv-colorize-prototxt opencv-colorize-caffemodel opencv-colorize-points
|
||||
BUNDLE_STATUS[object-eraser-colorize]="INSTALLED"
|
||||
ok "object-eraser-colorize complete"
|
||||
else
|
||||
fail "object-eraser-colorize verification failed"
|
||||
BUNDLE_STATUS[object-eraser-colorize]="FAILED: verification"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 4. BACKGROUND-REMOVAL (~4-5 GB)
|
||||
# ========================================================================
|
||||
install_background_removal() {
|
||||
log "=== Bundle 4/7: background-removal ==="
|
||||
|
||||
# --- pip packages ---
|
||||
pip_install "onnxruntime==1.20.1" || true # likely already installed
|
||||
pip_install "mediapipe>=0.10.18" || true # likely already installed
|
||||
pip_install "rembg==2.0.62" || { fail "rembg pip failed"; BUNDLE_STATUS[background-removal]="FAILED: pip rembg"; return 1; }
|
||||
|
||||
# --- Standard rembg sessions (6 models) ---
|
||||
local STANDARD_SESSIONS=("u2net" "isnet-general-use" "bria-rmbg" "birefnet-general-lite" "birefnet-portrait" "birefnet-general")
|
||||
for sess in "${STANDARD_SESSIONS[@]}"; do
|
||||
if file_ok "$MODELS/rembg/${sess}.onnx" 0; then
|
||||
ok "Already present: rembg/$sess.onnx"
|
||||
continue
|
||||
fi
|
||||
log "Downloading rembg session: $sess"
|
||||
dexec env U2NET_HOME="$MODELS/rembg" "$PYTHON" -c "
|
||||
import os
|
||||
os.makedirs('$MODELS/rembg', exist_ok=True)
|
||||
from rembg import new_session
|
||||
sess = new_session('$sess')
|
||||
print(f'Session $sess loaded OK')
|
||||
" 2>&1 || { warn "rembg session $sess failed (non-fatal, continuing)"; }
|
||||
done
|
||||
|
||||
# --- Custom sessions: birefnet-matting and birefnet-hr-matting ---
|
||||
# These use direct download URLs (from remove_bg.py custom classes).
|
||||
curl_model \
|
||||
"https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet-matting-epoch_100.onnx" \
|
||||
"$MODELS/rembg/birefnet-matting.onnx" 0 \
|
||||
|| warn "birefnet-matting download failed (non-fatal)"
|
||||
|
||||
curl_model \
|
||||
"https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet_HR-matting-epoch_135.onnx" \
|
||||
"$MODELS/rembg/birefnet-hr-matting.onnx" 0 \
|
||||
|| warn "birefnet-hr-matting download failed (non-fatal)"
|
||||
|
||||
# --- verify (check all 8) ---
|
||||
local all_ok=true
|
||||
for sess in "${STANDARD_SESSIONS[@]}" birefnet-matting birefnet-hr-matting; do
|
||||
file_ok "$MODELS/rembg/${sess}.onnx" 0 || { warn "Missing: rembg/$sess.onnx"; all_ok=false; }
|
||||
done
|
||||
|
||||
if $all_ok; then
|
||||
mark_installed background-removal \
|
||||
rembg-u2net rembg-isnet-general-use rembg-bria-rmbg \
|
||||
rembg-birefnet-general-lite rembg-birefnet-portrait rembg-birefnet-general \
|
||||
rembg-birefnet-matting rembg-birefnet-hr-matting
|
||||
BUNDLE_STATUS[background-removal]="INSTALLED"
|
||||
ok "background-removal complete"
|
||||
else
|
||||
fail "background-removal verification failed (some sessions missing)"
|
||||
BUNDLE_STATUS[background-removal]="FAILED: missing rembg sessions"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 5. UPSCALE-ENHANCE (~4-5 GB, heaviest, arm64 compile risk)
|
||||
# ========================================================================
|
||||
install_upscale_enhance() {
|
||||
log "=== Bundle 5/7: upscale-enhance ==="
|
||||
|
||||
local pip_failed=""
|
||||
|
||||
# --- pip packages (ordered for arm64 safety) ---
|
||||
pip_install "setuptools<75" || true
|
||||
pip_install "einops" || true
|
||||
pip_install --no-deps "codeformer-pip==0.0.4" || { warn "codeformer-pip failed"; pip_failed+=" codeformer-pip"; }
|
||||
pip_install "lpips" || { warn "lpips failed"; pip_failed+=" lpips"; }
|
||||
|
||||
# basicsr needs --no-build-isolation and can fail on arm64
|
||||
log "pip install basicsr==1.4.2 (may compile C extensions) ..."
|
||||
dpip install --cache-dir /data/ai/pip-cache --no-build-isolation "basicsr==1.4.2" 2>&1
|
||||
local basicsr_rc=$?
|
||||
if [[ $basicsr_rc -ne 0 ]]; then
|
||||
warn "basicsr==1.4.2 compile failed on arm64 (exit $basicsr_rc)"
|
||||
pip_failed+=" basicsr"
|
||||
fi
|
||||
|
||||
# realesrgan depends on basicsr
|
||||
if [[ "$pip_failed" != *"basicsr"* ]]; then
|
||||
pip_install "realesrgan==0.3.0" || { warn "realesrgan failed"; pip_failed+=" realesrgan"; }
|
||||
else
|
||||
warn "Skipping realesrgan (basicsr prerequisite failed)"
|
||||
pip_failed+=" realesrgan"
|
||||
fi
|
||||
|
||||
pip_install "mediapipe>=0.10.18" || true # already installed
|
||||
|
||||
# --- postInstall: force-reinstall numpy, re-pin Pillow and opencv ---
|
||||
log "Post-install: re-pinning numpy, Pillow, opencv"
|
||||
dpip install --cache-dir /data/ai/pip-cache --force-reinstall "numpy==1.26.4" 2>&1 || true
|
||||
dpip install --cache-dir /data/ai/pip-cache "Pillow==12.2.0" "opencv-python-headless==4.10.0.84" 2>&1 || true
|
||||
|
||||
# --- Compat shim: basicsr 1.4.2 imports torchvision.transforms.functional_tensor ---
|
||||
# which was removed in torchvision 0.18+. Create a forwarding module.
|
||||
log "Creating torchvision.transforms.functional_tensor compatibility shim"
|
||||
dpython -c "
|
||||
import torchvision.transforms as _t
|
||||
import os
|
||||
shim = os.path.join(_t.__path__[0], 'functional_tensor.py')
|
||||
if not os.path.exists(shim):
|
||||
with open(shim, 'w') as f:
|
||||
f.write('from torchvision.transforms.functional import *\n')
|
||||
print('Shim created')
|
||||
else:
|
||||
print('Shim already exists')
|
||||
" 2>&1 || warn "Could not create torchvision shim (non-fatal)"
|
||||
|
||||
if [[ -n "$pip_failed" ]]; then
|
||||
warn "Some pip packages failed:$pip_failed"
|
||||
warn "Continuing with model downloads (partially-working bundle)"
|
||||
fi
|
||||
|
||||
# --- models ---
|
||||
curl_model \
|
||||
"https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth" \
|
||||
"$MODELS/realesrgan/RealESRGAN_x4plus.pth" 60000000
|
||||
|
||||
curl_model \
|
||||
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth" \
|
||||
"$MODELS/gfpgan/GFPGANv1.3.pth" 300000000
|
||||
|
||||
curl_model \
|
||||
"https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth" \
|
||||
"$MODELS/codeformer/codeformer.pth" 350000000
|
||||
|
||||
hf_file "facefusion/models-3.0.0" "codeformer.onnx" "$MODELS/codeformer" 100000000
|
||||
|
||||
curl_model \
|
||||
"https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth" \
|
||||
"$MODELS/gfpgan/facelib/detection_Resnet50_Final.pth" 100000000
|
||||
|
||||
curl_model \
|
||||
"https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth" \
|
||||
"$MODELS/gfpgan/facelib/parsing_parsenet.pth" 80000000
|
||||
|
||||
curl_model \
|
||||
"https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth" \
|
||||
"$MODELS/scunet/scunet_color_real_psnr.pth" 3000000
|
||||
|
||||
hf_file "mikestealth/nafnet-models" "NAFNet-SIDD-width64.pth" "$MODELS/nafnet" 60000000
|
||||
|
||||
# --- verify models ---
|
||||
local models_ok=true
|
||||
file_ok "$MODELS/realesrgan/RealESRGAN_x4plus.pth" 60000000 || models_ok=false
|
||||
file_ok "$MODELS/gfpgan/GFPGANv1.3.pth" 300000000 || models_ok=false
|
||||
file_ok "$MODELS/codeformer/codeformer.pth" 350000000 || models_ok=false
|
||||
file_ok "$MODELS/codeformer/codeformer.onnx" 100000000 || models_ok=false
|
||||
file_ok "$MODELS/gfpgan/facelib/detection_Resnet50_Final.pth" 100000000 || models_ok=false
|
||||
file_ok "$MODELS/gfpgan/facelib/parsing_parsenet.pth" 80000000 || models_ok=false
|
||||
file_ok "$MODELS/scunet/scunet_color_real_psnr.pth" 3000000 || models_ok=false
|
||||
file_ok "$MODELS/nafnet/NAFNet-SIDD-width64.pth" 60000000 || models_ok=false
|
||||
|
||||
if $models_ok; then
|
||||
if [[ -n "$pip_failed" ]]; then
|
||||
mark_installed upscale-enhance \
|
||||
realesrgan-x4plus gfpgan-v1.3 codeformer-pth codeformer-onnx \
|
||||
facexlib-detection facexlib-parsing scunet-color-real nafnet-sidd
|
||||
BUNDLE_STATUS[upscale-enhance]="INSTALLED (pip failures:$pip_failed -- models OK, tools may fail at import)"
|
||||
warn "upscale-enhance: models present but pip packages incomplete"
|
||||
else
|
||||
mark_installed upscale-enhance \
|
||||
realesrgan-x4plus gfpgan-v1.3 codeformer-pth codeformer-onnx \
|
||||
facexlib-detection facexlib-parsing scunet-color-real nafnet-sidd
|
||||
BUNDLE_STATUS[upscale-enhance]="INSTALLED"
|
||||
ok "upscale-enhance complete"
|
||||
fi
|
||||
else
|
||||
fail "upscale-enhance model verification failed"
|
||||
BUNDLE_STATUS[upscale-enhance]="FAILED: model files missing"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 6. PHOTO-RESTORATION (mostly overlaps with #3 + #5)
|
||||
# ========================================================================
|
||||
install_photo_restoration() {
|
||||
log "=== Bundle 6/7: photo-restoration ==="
|
||||
|
||||
# --- pip packages (all should be installed from previous bundles) ---
|
||||
pip_install "onnxruntime==1.20.1" || true
|
||||
pip_install "mediapipe>=0.10.18" || true
|
||||
pip_install "huggingface-hub" || true
|
||||
pip_install "setuptools<75" || true
|
||||
pip_install --no-deps "codeformer-pip==0.0.4" || true
|
||||
# lpips, basicsr, realesrgan: attempt but don't block on failure
|
||||
pip_install "lpips" || true
|
||||
pip_install --no-build-isolation "basicsr==1.4.2" || true
|
||||
pip_install "realesrgan==0.3.0" || true
|
||||
|
||||
# Re-pin numpy after any installs
|
||||
dpip install --cache-dir /data/ai/pip-cache --force-reinstall "numpy==1.26.4" 2>&1 || true
|
||||
|
||||
# --- models (most overlap with previous bundles) ---
|
||||
# lama (shared with object-eraser-colorize)
|
||||
hf_file "Carve/LaMa-ONNX" "lama_fp32.onnx" "$MODELS/lama" 100000000 || true
|
||||
|
||||
# codeformer.onnx (shared with upscale-enhance)
|
||||
hf_file "facefusion/models-3.0.0" "codeformer.onnx" "$MODELS/codeformer" 100000000 || true
|
||||
|
||||
# realesrgan (shared with upscale-enhance)
|
||||
curl_model \
|
||||
"https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth" \
|
||||
"$MODELS/realesrgan/RealESRGAN_x4plus.pth" 60000000 || true
|
||||
|
||||
# facexlib (shared with upscale-enhance)
|
||||
curl_model \
|
||||
"https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth" \
|
||||
"$MODELS/gfpgan/facelib/detection_Resnet50_Final.pth" 100000000 || true
|
||||
curl_model \
|
||||
"https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth" \
|
||||
"$MODELS/gfpgan/facelib/parsing_parsenet.pth" 80000000 || true
|
||||
|
||||
# mediapipe (shared with face-detection)
|
||||
curl_model \
|
||||
"https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite" \
|
||||
"$MODELS/mediapipe/blaze_face_short_range.tflite" 100000 || true
|
||||
curl_model \
|
||||
"https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task" \
|
||||
"$MODELS/mediapipe/face_landmarker.task" 1000000 || true
|
||||
|
||||
# scunet (shared with upscale-enhance)
|
||||
curl_model \
|
||||
"https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth" \
|
||||
"$MODELS/scunet/scunet_color_real_psnr.pth" 3000000 || true
|
||||
|
||||
# --- verify ---
|
||||
local all_ok=true
|
||||
file_ok "$MODELS/lama/lama_fp32.onnx" 100000000 || all_ok=false
|
||||
file_ok "$MODELS/codeformer/codeformer.onnx" 100000000 || all_ok=false
|
||||
file_ok "$MODELS/realesrgan/RealESRGAN_x4plus.pth" 60000000 || all_ok=false
|
||||
file_ok "$MODELS/gfpgan/facelib/detection_Resnet50_Final.pth" 100000000 || all_ok=false
|
||||
file_ok "$MODELS/gfpgan/facelib/parsing_parsenet.pth" 80000000 || all_ok=false
|
||||
file_ok "$MODELS/mediapipe/blaze_face_short_range.tflite" 100000 || all_ok=false
|
||||
file_ok "$MODELS/mediapipe/face_landmarker.task" 1000000 || all_ok=false
|
||||
file_ok "$MODELS/scunet/scunet_color_real_psnr.pth" 3000000 || all_ok=false
|
||||
|
||||
if $all_ok; then
|
||||
mark_installed photo-restoration \
|
||||
lama-onnx codeformer-onnx realesrgan-x4plus \
|
||||
facexlib-detection facexlib-parsing \
|
||||
mediapipe-face-detector mediapipe-face-landmarker \
|
||||
scunet-color-real
|
||||
BUNDLE_STATUS[photo-restoration]="INSTALLED"
|
||||
ok "photo-restoration complete"
|
||||
else
|
||||
fail "photo-restoration verification failed"
|
||||
BUNDLE_STATUS[photo-restoration]="FAILED: verification"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# 7. OCR (~3-4 GB, paddlepaddle arm64 risk)
|
||||
# ========================================================================
|
||||
install_ocr() {
|
||||
log "=== Bundle 7/7: ocr ==="
|
||||
|
||||
# --- pip packages ---
|
||||
pip_install "huggingface-hub" || true
|
||||
|
||||
log "pip install paddlepaddle>=3.2.1 (arm64 wheel -- may take a while) ..."
|
||||
dpip install --cache-dir /data/ai/pip-cache "paddlepaddle>=3.2.1" 2>&1
|
||||
local paddle_rc=$?
|
||||
if [[ $paddle_rc -ne 0 ]]; then
|
||||
fail "paddlepaddle install failed on arm64 (exit $paddle_rc)"
|
||||
BUNDLE_STATUS[ocr]="FAILED: paddlepaddle arm64 pip install"
|
||||
return 1
|
||||
fi
|
||||
|
||||
pip_install "paddleocr[doc-parser]>=3.4.0,<3.5.0" || { fail "paddleocr pip failed"; BUNDLE_STATUS[ocr]="FAILED: paddleocr pip"; return 1; }
|
||||
|
||||
# Re-pin numpy after paddlepaddle (it may drag in a different version)
|
||||
dpip install --cache-dir /data/ai/pip-cache --force-reinstall "numpy==1.26.4" 2>&1 || true
|
||||
|
||||
# --- models (8 HF snapshots) ---
|
||||
local OCR_REPOS=(
|
||||
"PaddlePaddle/PP-OCRv5_server_det:PP-OCRv5_server_det"
|
||||
"PaddlePaddle/PP-OCRv5_server_rec:PP-OCRv5_server_rec"
|
||||
"PaddlePaddle/PP-OCRv5_mobile_det:PP-OCRv5_mobile_det"
|
||||
"PaddlePaddle/PP-OCRv5_mobile_rec:PP-OCRv5_mobile_rec"
|
||||
"PaddlePaddle/latin_PP-OCRv5_mobile_rec:latin_PP-OCRv5_mobile_rec"
|
||||
"PaddlePaddle/korean_PP-OCRv5_mobile_rec:korean_PP-OCRv5_mobile_rec"
|
||||
"PaddlePaddle/PP-LCNet_x1_0_textline_ori:PP-LCNet_x1_0_textline_ori"
|
||||
"PaddlePaddle/PaddleOCR-VL-1.5:PaddleOCR-VL-1.5"
|
||||
)
|
||||
|
||||
for entry in "${OCR_REPOS[@]}"; do
|
||||
local repo="${entry%%:*}"
|
||||
local dirname="${entry##*:}"
|
||||
hf_snapshot "$repo" "$MODELS/$dirname" \
|
||||
|| { warn "HF snapshot $repo failed"; }
|
||||
done
|
||||
|
||||
# --- verify ---
|
||||
local all_ok=true
|
||||
for entry in "${OCR_REPOS[@]}"; do
|
||||
local dirname="${entry##*:}"
|
||||
dir_ok "$MODELS/$dirname" || { warn "Missing: $dirname"; all_ok=false; }
|
||||
done
|
||||
|
||||
if $all_ok; then
|
||||
mark_installed ocr \
|
||||
paddleocr-server-det paddleocr-server-rec \
|
||||
paddleocr-mobile-det paddleocr-mobile-rec \
|
||||
paddleocr-latin-rec paddleocr-korean-rec \
|
||||
paddleocr-textline-ori paddleocr-vl
|
||||
BUNDLE_STATUS[ocr]="INSTALLED"
|
||||
ok "ocr complete"
|
||||
else
|
||||
fail "ocr verification failed (some model dirs missing)"
|
||||
BUNDLE_STATUS[ocr]="FAILED: missing model directories"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ========================================================================
|
||||
# Main
|
||||
# ========================================================================
|
||||
|
||||
main() {
|
||||
log "Seeding AI models into container: $CONTAINER"
|
||||
log "Architecture: $(dexec uname -m)"
|
||||
log "Python: $(dpython --version 2>&1)"
|
||||
echo
|
||||
|
||||
# Remove stale install lock if present
|
||||
dexec rm -f /data/ai/install.lock 2>/dev/null || true
|
||||
|
||||
# Ensure models directory
|
||||
dexec mkdir -p "$MODELS" "$MODELS/rembg" "$MODELS/mediapipe" 2>/dev/null || true
|
||||
|
||||
# Run bundles smallest-first
|
||||
install_face_detection || true
|
||||
echo
|
||||
install_transcription || true
|
||||
echo
|
||||
install_object_eraser_colorize || true
|
||||
echo
|
||||
install_background_removal || true
|
||||
echo
|
||||
install_upscale_enhance || true
|
||||
echo
|
||||
install_photo_restoration || true
|
||||
echo
|
||||
install_ocr || true
|
||||
echo
|
||||
|
||||
# -- Summary --
|
||||
log "========================================"
|
||||
log "SEED COMPLETE -- Per-bundle results:"
|
||||
log "========================================"
|
||||
for b in face-detection transcription object-eraser-colorize background-removal \
|
||||
upscale-enhance photo-restoration ocr; do
|
||||
local status="${BUNDLE_STATUS[$b]:-NOT RUN}"
|
||||
if [[ "$status" == INSTALLED* ]]; then
|
||||
ok "$b: $status"
|
||||
else
|
||||
fail "$b: $status"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
log "Models directory:"
|
||||
dexec du -sh "$MODELS" 2>/dev/null || true
|
||||
dexec du -sh "$MODELS"/* 2>/dev/null || true
|
||||
|
||||
echo
|
||||
log "installed.json:"
|
||||
dexec cat "$INSTALLED" 2>/dev/null || warn "No installed.json"
|
||||
|
||||
echo
|
||||
log "Total /data disk usage:"
|
||||
dexec du -sh /data 2>/dev/null || true
|
||||
}
|
||||
|
||||
main
|
||||
Reference in New Issue
Block a user