"""Pre-download and verify all ML models for the Docker image. This script runs at Docker build time. Any failure exits non-zero, failing the build. No silent fallbacks. """ import os import sys import time import urllib.error import urllib.request # Some servers (e.g. Berkeley) block the default Python-urllib User-Agent. _opener = urllib.request.build_opener() _opener.addheaders = [("User-Agent", "snapotter/1.0")] urllib.request.install_opener(_opener) def _urlretrieve(url: str, path: str, max_retries: int = 5) -> None: """Download url to path with exponential backoff on transient errors (5xx, timeout).""" for attempt in range(1, max_retries + 1): try: urllib.request.urlretrieve(url, path) return except urllib.error.HTTPError as e: if attempt == max_retries or e.code < 500: raise delay = 10 * (2 ** (attempt - 1)) # 10s, 20s, 40s, 80s print(f" HTTP {e.code} on attempt {attempt}/{max_retries}, retrying in {delay}s...") time.sleep(delay) except (urllib.error.URLError, OSError) as e: if attempt == max_retries: raise delay = 10 * (2 ** (attempt - 1)) print(f" Network error on attempt {attempt}/{max_retries}: {e}, retrying in {delay}s...") time.sleep(delay) def _hf_download( repo_id: str, filename: str, local_dir: str, min_size: int, label: str, repo_type: str = "model", ) -> str: """Download a file from HuggingFace Hub with built-in retry and CDN routing. Uses hf_hub_download which handles retries, resumable downloads, and respects the HF_ENDPOINT env var for mirror support. repo_type can be "model" or "space". """ from huggingface_hub import hf_hub_download print(f" Downloading {label} from HuggingFace ({repo_id}/{filename})...") path = hf_hub_download( repo_id=repo_id, filename=filename, local_dir=local_dir, repo_type=repo_type ) # hf_hub_download may place the file in a cache subdir; normalise to local_dir/filename dest = os.path.join(local_dir, filename) if path != dest and os.path.exists(path): os.makedirs(os.path.dirname(dest), exist_ok=True) os.rename(path, dest) size = os.path.getsize(dest) assert size > min_size, f"{label} too small: {size} bytes (expected > {min_size})" print(f" {label} ready ({size / 1_000_000:.1f} MB)") return dest # Force CPU mode during build - no GPU driver available at build time. # Must be set before any ML library import. os.environ["PADDLE_DEVICE"] = "cpu" os.environ["FLAGS_use_cuda"] = "0" os.environ["CUDA_VISIBLE_DEVICES"] = "" # Prevent ONNX Runtime from loading the CUDA Execution Provider at build time. # The cudnn-runtime base image includes cuDNN, which makes ONNX try to init # the CUDA EP. Without a GPU driver (only available at container runtime), # this segfaults. Temporarily renaming the provider .so is the most reliable # way to prevent this — env vars alone are not enough. def _hide_cuda_provider(): """Rename ONNX CUDA provider .so to prevent load at build time.""" try: import onnxruntime as _ort ep_dir = os.path.join(os.path.dirname(_ort.__file__), "capi") for name in ("libonnxruntime_providers_cuda.so", "libonnxruntime_providers_tensorrt.so"): src = os.path.join(ep_dir, name) if os.path.exists(src): os.rename(src, src + ".build_hide") except ImportError: pass def _restore_cuda_provider(): """Restore hidden ONNX CUDA provider .so after build-time downloads.""" try: import onnxruntime as _ort ep_dir = os.path.join(os.path.dirname(_ort.__file__), "capi") for name in ("libonnxruntime_providers_cuda.so", "libonnxruntime_providers_tensorrt.so"): bak = os.path.join(ep_dir, name + ".build_hide") if os.path.exists(bak): os.rename(bak, os.path.join(ep_dir, name)) except ImportError: pass _hide_cuda_provider() LAMA_MODEL_DIR = "/opt/models/lama" LAMA_MODEL_URL = "https://huggingface.co/Carve/LaMa-ONNX/resolve/main/lama_fp32.onnx" LAMA_MODEL_PATH = os.path.join(LAMA_MODEL_DIR, "lama_fp32.onnx") LAMA_MIN_SIZE = 100_000_000 # ~200 MB REALESRGAN_MODEL_DIR = "/opt/models/realesrgan" REALESRGAN_MODEL_URL = ( "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth" ) REALESRGAN_MODEL_PATH = os.path.join(REALESRGAN_MODEL_DIR, "RealESRGAN_x4plus.pth") REALESRGAN_MIN_SIZE = 60_000_000 # ~67 MB GFPGAN_MODEL_DIR = "/opt/models/gfpgan" GFPGAN_MODEL_URL = ( "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth" ) GFPGAN_MODEL_PATH = os.path.join(GFPGAN_MODEL_DIR, "GFPGANv1.3.pth") GFPGAN_MIN_SIZE = 300_000_000 # ~332 MB CODEFORMER_MODEL_DIR = "/opt/models/codeformer" CODEFORMER_MODEL_URL = ( "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth" ) CODEFORMER_MODEL_PATH = os.path.join(CODEFORMER_MODEL_DIR, "codeformer.pth") CODEFORMER_MIN_SIZE = 350_000_000 # ~375 MB DDCOLOR_MODEL_DIR = "/opt/models/ddcolor" DDCOLOR_MODEL_URL = ( "https://huggingface.co/piddnad/DDColor-models/resolve/main/ddcolor_paper_tiny.pth" ) DDCOLOR_ONNX_PATH = os.path.join(DDCOLOR_MODEL_DIR, "ddcolor.onnx") DDCOLOR_MIN_SIZE = 50_000_000 # ~220 MB ONNX SCUNET_MODEL_DIR = "/opt/models/scunet" SCUNET_MODEL_URL = ( "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth" ) SCUNET_MODEL_PATH = os.path.join(SCUNET_MODEL_DIR, "scunet_color_real_psnr.pth") SCUNET_MIN_SIZE = 3_000_000 # ~4 MB CODEFORMER_MODEL_DIR = "/opt/models/codeformer" CODEFORMER_ONNX_PATH = os.path.join(CODEFORMER_MODEL_DIR, "codeformer.onnx") CODEFORMER_MIN_SIZE = 100_000_000 # ~377 MB NAFNET_MODEL_DIR = "/opt/models/nafnet" NAFNET_MODEL_URL = ( "https://huggingface.co/mikestealth/nafnet-models/resolve/main/" "NAFNet-SIDD-width64.pth" ) NAFNET_MODEL_PATH = os.path.join(NAFNET_MODEL_DIR, "NAFNet-SIDD-width64.pth") NAFNET_MIN_SIZE = 60_000_000 # ~67 MB MEDIAPIPE_MODEL_DIR = "/opt/models/mediapipe" FACE_DETECT_MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite" FACE_DETECT_MODEL_PATH = os.path.join(MEDIAPIPE_MODEL_DIR, "blaze_face_short_range.tflite") FACE_DETECT_MIN_SIZE = 100_000 # ~200 KB FACE_LANDMARKER_MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task" FACE_LANDMARKER_MODEL_PATH = os.path.join(MEDIAPIPE_MODEL_DIR, "face_landmarker.task") FACE_LANDMARKER_MIN_SIZE = 1_000_000 # ~7 MB FACEXLIB_MODEL_DIR = "/opt/models/gfpgan/facelib" FACEXLIB_DET_URL = "https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth" FACEXLIB_DET_PATH = os.path.join(FACEXLIB_MODEL_DIR, "detection_Resnet50_Final.pth") FACEXLIB_DET_MIN_SIZE = 100_000_000 # ~104 MB FACEXLIB_PARSE_URL = "https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth" FACEXLIB_PARSE_PATH = os.path.join(FACEXLIB_MODEL_DIR, "parsing_parsenet.pth") FACEXLIB_PARSE_MIN_SIZE = 80_000_000 # ~85 MB OPENCV_COLORIZE_DIR = "/opt/models/colorize-opencv" OPENCV_PROTO_URL = "https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/models/colorization_deploy_v2.prototxt" OPENCV_PROTO_PATH = os.path.join(OPENCV_COLORIZE_DIR, "colorization_deploy_v2.prototxt") OPENCV_CAFFE_URL = "https://huggingface.co/spaces/BilalSardar/Black-N-White-To-Color/resolve/main/colorization_release_v2.caffemodel" OPENCV_CAFFE_PATH = os.path.join(OPENCV_COLORIZE_DIR, "colorization_release_v2.caffemodel") OPENCV_CAFFE_MIN_SIZE = 100_000_000 # ~129 MB OPENCV_POINTS_URL = "https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/resources/pts_in_hull.npy" OPENCV_POINTS_PATH = os.path.join(OPENCV_COLORIZE_DIR, "pts_in_hull.npy") REMBG_MODELS = [ "u2net", "isnet-general-use", "bria-rmbg", "birefnet-general-lite", "birefnet-portrait", "birefnet-general", "birefnet-matting", "birefnet-hr-matting", ] # PaddleOCR PP-OCRv5 HuggingFace model repos to pre-download. # These are the models used by PaddleOCR(ocr_version="PP-OCRv5"). # Downloaded via huggingface_hub to avoid initializing the PaddlePaddle # inference engine, which segfaults under QEMU emulation at build time. PADDLEOCR_MODELS = [ "PaddlePaddle/PP-OCRv5_server_det", "PaddlePaddle/PP-OCRv5_server_rec", "PaddlePaddle/PP-OCRv5_mobile_det", "PaddlePaddle/PP-OCRv5_mobile_rec", "PaddlePaddle/latin_PP-OCRv5_mobile_rec", "PaddlePaddle/korean_PP-OCRv5_mobile_rec", "PaddlePaddle/PP-LCNet_x1_0_textline_ori", ] PADDLEOCR_VL_MODEL = "PaddlePaddle/PaddleOCR-VL-1.5" # PaddleX stores models here by default PADDLEX_MODEL_DIR = "/opt/models/paddlex/official_models" def _register_birefnet_matting(): """Register BiRefNet-matting ONNX session for Ultra quality mode.""" import os import pooch from rembg.sessions import sessions_class from rembg.sessions.birefnet_general import BiRefNetSessionGeneral class BiRefNetMattingSession(BiRefNetSessionGeneral): @classmethod def download_models(cls, *args, **kwargs): fname = f"{cls.name(*args, **kwargs)}.onnx" pooch.retrieve( "https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet-matting-epoch_100.onnx", None, # Skip checksum for GitHub release assets fname=fname, path=cls.u2net_home(*args, **kwargs), progressbar=True, ) return os.path.join(cls.u2net_home(*args, **kwargs), fname) @classmethod def name(cls, *args, **kwargs): return "birefnet-matting" sessions_class.append(BiRefNetMattingSession) def _register_birefnet_hr_matting(): """Register BiRefNet HR-matting ONNX session for 2048x2048 high-res matting.""" import os import numpy as np import pooch from PIL import Image from rembg.sessions import sessions_class from rembg.sessions.birefnet_general import BiRefNetSessionGeneral class BiRefNetHRMattingSession(BiRefNetSessionGeneral): @classmethod def download_models(cls, *args, **kwargs): fname = f"{cls.name(*args, **kwargs)}.onnx" pooch.retrieve( "https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet_HR-matting-epoch_135.onnx", None, fname=fname, path=cls.u2net_home(*args, **kwargs), progressbar=True, ) return os.path.join(cls.u2net_home(*args, **kwargs), fname) @classmethod def name(cls, *args, **kwargs): return "birefnet-hr-matting" def predict(self, img, *args, **kwargs): ort_outs = self.inner_session.run( None, self.normalize( img, (0.485, 0.456, 0.406), (0.229, 0.224, 0.225), (2048, 2048) ), ) pred = ort_outs[0][:, 0, :, :] ma = np.max(pred) mi = np.min(pred) denom = ma - mi pred = (pred - mi) / denom if denom > 0 else pred * 0 pred = np.squeeze(pred) mask = Image.fromarray((pred * 255).astype("uint8"), mode="L") mask = mask.resize(img.size, Image.LANCZOS) return [mask] sessions_class.append(BiRefNetHRMattingSession) def download_rembg_models(): """Download all rembg ONNX models.""" print("=== Downloading rembg models ===") from rembg import new_session _register_birefnet_matting() _register_birefnet_hr_matting() for model in REMBG_MODELS: print(f" Downloading {model}...") new_session(model) print(f" {model} ready") print(f"All {len(REMBG_MODELS)} rembg models downloaded.\n") def download_lama_model(): """Download LaMa ONNX inpainting model from HuggingFace Hub.""" print("=== Downloading LaMa ONNX model ===") os.makedirs(LAMA_MODEL_DIR, exist_ok=True) _hf_download("Carve/LaMa-ONNX", "lama_fp32.onnx", LAMA_MODEL_DIR, LAMA_MIN_SIZE, "LaMa ONNX") print() def download_realesrgan_model(): """Download RealESRGAN_x4plus.pth pretrained weights.""" print("=== Downloading RealESRGAN model ===") os.makedirs(REALESRGAN_MODEL_DIR, exist_ok=True) print(f" Downloading from {REALESRGAN_MODEL_URL}...") _urlretrieve(REALESRGAN_MODEL_URL, REALESRGAN_MODEL_PATH) size = os.path.getsize(REALESRGAN_MODEL_PATH) assert size > REALESRGAN_MIN_SIZE, ( f"RealESRGAN model too small: {size} bytes (expected > {REALESRGAN_MIN_SIZE})" ) print(f" RealESRGAN_x4plus.pth downloaded ({size / 1_000_000:.1f} MB)\n") def download_gfpgan_model(): """Download GFPGANv1.3.pth pretrained weights for face enhancement.""" print("=== Downloading GFPGAN model ===") os.makedirs(GFPGAN_MODEL_DIR, exist_ok=True) print(f" Downloading from {GFPGAN_MODEL_URL}...") _urlretrieve(GFPGAN_MODEL_URL, GFPGAN_MODEL_PATH) size = os.path.getsize(GFPGAN_MODEL_PATH) assert size > GFPGAN_MIN_SIZE, ( f"GFPGAN model too small: {size} bytes (expected > {GFPGAN_MIN_SIZE})" ) print(f" GFPGANv1.3.pth downloaded ({size / 1_000_000:.1f} MB)\n") def download_codeformer_model(): """Download codeformer.pth pretrained weights for face enhancement.""" print("=== Downloading CodeFormer model ===") os.makedirs(CODEFORMER_MODEL_DIR, exist_ok=True) print(f" Downloading from {CODEFORMER_MODEL_URL}...") _urlretrieve(CODEFORMER_MODEL_URL, CODEFORMER_MODEL_PATH) size = os.path.getsize(CODEFORMER_MODEL_PATH) assert size > CODEFORMER_MIN_SIZE, ( f"CodeFormer model too small: {size} bytes (expected > {CODEFORMER_MIN_SIZE})" ) print(f" codeformer.pth downloaded ({size / 1_000_000:.1f} MB)\n") def download_ddcolor_model(): """Download pre-exported DDColor ONNX model for AI photo colorization. Uses the pre-converted ONNX model from HuggingFace (facefusion repo) for direct inference via onnxruntime without needing PyTorch. """ print("=== Downloading DDColor ONNX model ===") os.makedirs(DDCOLOR_MODEL_DIR, exist_ok=True) from huggingface_hub import hf_hub_download print(" Downloading DDColor ONNX from HuggingFace...") downloaded_path = hf_hub_download( repo_id="facefusion/models-3.0.0", filename="ddcolor.onnx", local_dir=DDCOLOR_MODEL_DIR, ) # huggingface_hub downloads to local_dir/filename actual_path = os.path.join(DDCOLOR_MODEL_DIR, "ddcolor.onnx") if not os.path.exists(actual_path) and os.path.exists(downloaded_path): os.rename(downloaded_path, actual_path) size = os.path.getsize(actual_path) assert size > DDCOLOR_MIN_SIZE, ( f"DDColor model too small: {size} bytes (expected > {DDCOLOR_MIN_SIZE})" ) print(f" DDColor ONNX model ready ({size / 1_000_000:.1f} MB)\n") def download_codeformer_onnx_model(): """Download CodeFormer ONNX model for AI face restoration. Uses the pre-converted ONNX model from HuggingFace (facefusion repo) for direct inference via onnxruntime without needing PyTorch. """ print("=== Downloading CodeFormer ONNX model ===") os.makedirs(CODEFORMER_MODEL_DIR, exist_ok=True) from huggingface_hub import hf_hub_download print(" Downloading CodeFormer ONNX from HuggingFace...") downloaded_path = hf_hub_download( repo_id="facefusion/models-3.0.0", filename="codeformer.onnx", local_dir=CODEFORMER_MODEL_DIR, ) actual_path = os.path.join(CODEFORMER_MODEL_DIR, "codeformer.onnx") if not os.path.exists(actual_path) and os.path.exists(downloaded_path): os.rename(downloaded_path, actual_path) size = os.path.getsize(actual_path) assert size > CODEFORMER_MIN_SIZE, ( f"CodeFormer model too small: {size} bytes (expected > {CODEFORMER_MIN_SIZE})" ) print(f" CodeFormer ONNX model ready ({size / 1_000_000:.1f} MB)\n") def download_paddleocr_models(): """Pre-download PaddleOCR PP-OCRv5 model weights from HuggingFace. Uses huggingface_hub.snapshot_download() to fetch model files directly into the PaddleX cache directory. This avoids initializing PaddlePaddle's C++ inference engine, which segfaults under QEMU emulation (arm64 host building amd64 image). """ print("=== Downloading PaddleOCR PP-OCRv5 models ===") from huggingface_hub import snapshot_download os.makedirs(PADDLEX_MODEL_DIR, exist_ok=True) for repo_id in PADDLEOCR_MODELS: model_name = repo_id.split("/", 1)[1] local_dir = os.path.join(PADDLEX_MODEL_DIR, model_name) print(f" Downloading {model_name}...") snapshot_download(repo_id=repo_id, local_dir=local_dir) print(f" {model_name} ready") print(f"All {len(PADDLEOCR_MODELS)} PaddleOCR PP-OCRv5 models downloaded.\n") def download_paddleocr_vl_model(): """Pre-download PaddleOCR-VL 1.5 model weights from HuggingFace.""" print("=== Downloading PaddleOCR-VL 1.5 model ===") from huggingface_hub import snapshot_download model_name = PADDLEOCR_VL_MODEL.split("/", 1)[1] local_dir = os.path.join(PADDLEX_MODEL_DIR, model_name) print(f" Downloading {model_name} (~1.93 GB)...") snapshot_download(repo_id=PADDLEOCR_VL_MODEL, local_dir=local_dir) print(f" {model_name} ready\n") def download_scunet_model(): """Download SCUNet real-noise denoising model.""" print(f"Downloading SCUNet model to {SCUNET_MODEL_PATH}...") os.makedirs(SCUNET_MODEL_DIR, exist_ok=True) _urlretrieve(SCUNET_MODEL_URL, SCUNET_MODEL_PATH) size = os.path.getsize(SCUNET_MODEL_PATH) assert size > SCUNET_MIN_SIZE, ( f"SCUNet model too small: {size} bytes (expected >{SCUNET_MIN_SIZE})" ) print(f" SCUNet model downloaded: {size:,} bytes") def download_nafnet_model(): """Download NAFNet SIDD width-64 denoising model from HuggingFace Hub.""" print("=== Downloading NAFNet model ===") os.makedirs(NAFNET_MODEL_DIR, exist_ok=True) _hf_download( "mikestealth/nafnet-models", "NAFNet-SIDD-width64.pth", NAFNET_MODEL_DIR, NAFNET_MIN_SIZE, "NAFNet", ) def download_facexlib_models(): """Download face detection and parsing models used by GFPGAN and CodeFormer. These are auxiliary models from facexlib that GFPGAN/CodeFormer download on first use via basicsr. Pre-downloading prevents runtime network access. """ print("=== Downloading facexlib auxiliary models ===") os.makedirs(FACEXLIB_MODEL_DIR, exist_ok=True) print(f" Downloading detection_Resnet50_Final.pth...") _urlretrieve(FACEXLIB_DET_URL, FACEXLIB_DET_PATH) size = os.path.getsize(FACEXLIB_DET_PATH) assert size > FACEXLIB_DET_MIN_SIZE, ( f"Face detection model too small: {size} bytes (expected > {FACEXLIB_DET_MIN_SIZE})" ) print(f" detection_Resnet50_Final.pth downloaded ({size / 1_000_000:.1f} MB)") print(f" Downloading parsing_parsenet.pth...") _urlretrieve(FACEXLIB_PARSE_URL, FACEXLIB_PARSE_PATH) size = os.path.getsize(FACEXLIB_PARSE_PATH) assert size > FACEXLIB_PARSE_MIN_SIZE, ( f"Face parsing model too small: {size} bytes (expected > {FACEXLIB_PARSE_MIN_SIZE})" ) print(f" parsing_parsenet.pth downloaded ({size / 1_000_000:.1f} MB)\n") def download_opencv_colorize_models(): """Download OpenCV DNN colorization models (Zhang et al.). Three files needed for the lightweight OpenCV colorizer fallback. """ print("=== Downloading OpenCV colorization models ===") os.makedirs(OPENCV_COLORIZE_DIR, exist_ok=True) # Prototxt and cluster-centres file are from GitHub raw content for url, path, name in [ (OPENCV_PROTO_URL, OPENCV_PROTO_PATH, "colorization_deploy_v2.prototxt"), (OPENCV_POINTS_URL, OPENCV_POINTS_PATH, "pts_in_hull.npy"), ]: print(f" Downloading {name}...") _urlretrieve(url, path) size = os.path.getsize(path) print(f" {name} downloaded ({size / 1_000_000:.1f} MB)") # Caffemodel lives in a HuggingFace space — use hf_hub_download for reliability _hf_download( "BilalSardar/Black-N-White-To-Color", "colorization_release_v2.caffemodel", OPENCV_COLORIZE_DIR, OPENCV_CAFFE_MIN_SIZE, "OpenCV caffemodel", repo_type="space", ) print("OpenCV colorization models downloaded.\n") def download_mediapipe_task_models(): """Download MediaPipe tasks API model files for face detection and landmarks. These models are used by the mp.tasks fallback when mp.solutions is unavailable (mediapipe >= 0.10.30). Pre-downloading ensures the Docker image works fully airgapped. """ print("=== Downloading MediaPipe task models ===") os.makedirs(MEDIAPIPE_MODEL_DIR, exist_ok=True) for url, path, name, min_size in [ (FACE_DETECT_MODEL_URL, FACE_DETECT_MODEL_PATH, "blaze_face_short_range", FACE_DETECT_MIN_SIZE), (FACE_LANDMARKER_MODEL_URL, FACE_LANDMARKER_MODEL_PATH, "face_landmarker", FACE_LANDMARKER_MIN_SIZE), ]: print(f" Downloading {name}...") _urlretrieve(url, path) size = os.path.getsize(path) assert size > min_size, ( f"{name} model too small: {size} bytes (expected > {min_size})" ) print(f" {name} downloaded ({size / 1_000_000:.1f} MB)") print("MediaPipe task models downloaded.\n") def verify_mediapipe(): """Verify MediaPipe face detection models are bundled in the wheel.""" print("=== Verifying MediaPipe models ===") import mediapipe as mp try: for selection in [0, 1]: label = "short-range" if selection == 0 else "full-range" print(f" Verifying {label} model (selection={selection})...") detector = mp.solutions.face_detection.FaceDetection( model_selection=selection, min_detection_confidence=0.5 ) detector.close() print(f" {label} model OK") except AttributeError: # mediapipe >= 0.10.30 removed mp.solutions; verify tasks API instead print(" mp.solutions unavailable, verifying mp.tasks API...") options = mp.tasks.vision.FaceDetectorOptions( base_options=mp.tasks.BaseOptions( model_asset_path=FACE_DETECT_MODEL_PATH ), running_mode=mp.tasks.vision.RunningMode.IMAGE, min_detection_confidence=0.5, ) detector = mp.tasks.vision.FaceDetector.create_from_options(options) detector.close() print(" mp.tasks FaceDetector OK") print("MediaPipe models verified.\n") def smoke_test(): """Final verification that all ML libraries and models are loadable. GPU-dependent libraries (paddlepaddle-gpu, torch CUDA) cannot be imported at build time because the CUDA driver is only available at runtime. We verify CPU-only imports and check that model files exist on disk. """ print("=== Running smoke test ===") # CPU-only imports that work on all platforms at build time from PIL import Image import cv2 import numpy from rembg import new_session print(" CPU imports OK (Pillow, cv2, numpy, rembg)") # MediaPipe is CPU-only, should always import import mediapipe as mp print(" MediaPipe import OK") # LaMa model file must exist assert os.path.exists(LAMA_MODEL_PATH), ( f"LaMa model missing: {LAMA_MODEL_PATH}" ) assert os.path.getsize(LAMA_MODEL_PATH) > LAMA_MIN_SIZE, ( "LaMa model file is too small" ) print(" LaMa ONNX model file verified") # RealESRGAN model file must exist assert os.path.exists(REALESRGAN_MODEL_PATH), ( f"RealESRGAN model missing: {REALESRGAN_MODEL_PATH}" ) assert os.path.getsize(REALESRGAN_MODEL_PATH) > REALESRGAN_MIN_SIZE, ( "RealESRGAN model file is too small" ) print(" RealESRGAN model file verified") # GFPGAN model file must exist assert os.path.exists(GFPGAN_MODEL_PATH), ( f"GFPGAN model missing: {GFPGAN_MODEL_PATH}" ) assert os.path.getsize(GFPGAN_MODEL_PATH) > GFPGAN_MIN_SIZE, ( "GFPGAN model file is too small" ) print(" GFPGAN model file verified") # CodeFormer model file must exist assert os.path.exists(CODEFORMER_MODEL_PATH), ( f"CodeFormer model missing: {CODEFORMER_MODEL_PATH}" ) assert os.path.getsize(CODEFORMER_MODEL_PATH) > CODEFORMER_MIN_SIZE, ( "CodeFormer model file is too small" ) print(" CodeFormer model file verified") # DDColor ONNX model must exist assert os.path.exists(DDCOLOR_ONNX_PATH), ( f"DDColor model missing: {DDCOLOR_ONNX_PATH}" ) assert os.path.getsize(DDCOLOR_ONNX_PATH) > DDCOLOR_MIN_SIZE, ( "DDColor model file is too small" ) print(" DDColor ONNX model file verified") # CodeFormer ONNX model must exist assert os.path.exists(CODEFORMER_ONNX_PATH), ( f"CodeFormer model missing: {CODEFORMER_ONNX_PATH}" ) assert os.path.getsize(CODEFORMER_ONNX_PATH) > CODEFORMER_MIN_SIZE, ( "CodeFormer model file is too small" ) print(" CodeFormer ONNX model file verified") # SCUNet model file must exist assert os.path.exists(SCUNET_MODEL_PATH), f"SCUNet model not found: {SCUNET_MODEL_PATH}" assert os.path.getsize(SCUNET_MODEL_PATH) > SCUNET_MIN_SIZE print(" SCUNet model file verified") # NAFNet model file must exist assert os.path.exists(NAFNET_MODEL_PATH), f"NAFNet model not found: {NAFNET_MODEL_PATH}" assert os.path.getsize(NAFNET_MODEL_PATH) > NAFNET_MIN_SIZE print(" NAFNet model file verified") # PaddleOCR model directories must exist for repo_id in PADDLEOCR_MODELS: model_name = repo_id.split("/", 1)[1] model_dir = os.path.join(PADDLEX_MODEL_DIR, model_name) assert os.path.isdir(model_dir), f"PaddleOCR model missing: {model_dir}" print(f" PaddleOCR models verified ({len(PADDLEOCR_MODELS)} models)") # PaddleOCR-VL model directory must exist vl_name = PADDLEOCR_VL_MODEL.split("/", 1)[1] vl_dir = os.path.join(PADDLEX_MODEL_DIR, vl_name) assert os.path.isdir(vl_dir), f"PaddleOCR-VL model missing: {vl_dir}" print(" PaddleOCR-VL model verified") # MediaPipe task models must exist (for mp.tasks fallback) assert os.path.exists(FACE_DETECT_MODEL_PATH), ( f"MediaPipe face detector model missing: {FACE_DETECT_MODEL_PATH}" ) assert os.path.getsize(FACE_DETECT_MODEL_PATH) > FACE_DETECT_MIN_SIZE print(" MediaPipe face detector model verified") assert os.path.exists(FACE_LANDMARKER_MODEL_PATH), ( f"MediaPipe face landmarker model missing: {FACE_LANDMARKER_MODEL_PATH}" ) assert os.path.getsize(FACE_LANDMARKER_MODEL_PATH) > FACE_LANDMARKER_MIN_SIZE print(" MediaPipe face landmarker model verified") # Facexlib auxiliary models must exist (for GFPGAN/CodeFormer) assert os.path.exists(FACEXLIB_DET_PATH), ( f"Facexlib detection model missing: {FACEXLIB_DET_PATH}" ) assert os.path.getsize(FACEXLIB_DET_PATH) > FACEXLIB_DET_MIN_SIZE assert os.path.exists(FACEXLIB_PARSE_PATH), ( f"Facexlib parsing model missing: {FACEXLIB_PARSE_PATH}" ) assert os.path.getsize(FACEXLIB_PARSE_PATH) > FACEXLIB_PARSE_MIN_SIZE print(" Facexlib auxiliary models verified") # OpenCV colorization models must exist assert os.path.exists(OPENCV_PROTO_PATH), ( f"OpenCV colorize prototxt missing: {OPENCV_PROTO_PATH}" ) assert os.path.exists(OPENCV_CAFFE_PATH), ( f"OpenCV colorize caffemodel missing: {OPENCV_CAFFE_PATH}" ) assert os.path.getsize(OPENCV_CAFFE_PATH) > OPENCV_CAFFE_MIN_SIZE assert os.path.exists(OPENCV_POINTS_PATH), ( f"OpenCV colorize points missing: {OPENCV_POINTS_PATH}" ) print(" OpenCV colorization models verified") print("Smoke test passed.\n") def main(): import concurrent.futures import threading print("Pre-downloading all ML models (parallel)...\n") print_lock = threading.Lock() # All download functions are independent (separate dirs, separate CDNs). # Run them in parallel to cut download time from ~30 min to ~5-10 min. download_fns = [ download_lama_model, download_rembg_models, download_realesrgan_model, download_gfpgan_model, download_codeformer_model, download_ddcolor_model, download_codeformer_onnx_model, download_paddleocr_models, download_paddleocr_vl_model, download_scunet_model, download_nafnet_model, download_facexlib_models, download_opencv_colorize_models, download_mediapipe_task_models, ] # 6 workers balances parallelism with CDN rate limits errors = [] with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool: future_to_name = { pool.submit(fn): fn.__name__ for fn in download_fns } for future in concurrent.futures.as_completed(future_to_name): name = future_to_name[future] try: future.result() except Exception as e: errors.append((name, e)) print(f"\n*** {name} FAILED: {e}\n") if errors: print(f"\n{len(errors)} download(s) failed:") for name, e in errors: print(f" {name}: {e}") sys.exit(1) print("\nAll downloads complete. Running verification...\n") verify_mediapipe() smoke_test() _restore_cuda_provider() print("All models downloaded and verified.") if __name__ == "__main__": main()