2026-04-10 13:21:06 +08:00
|
|
|
"""Pre-download and verify all ML models for the Docker image.
|
2026-03-23 11:46:45 +08:00
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
This script runs at Docker build time. Any failure exits non-zero,
|
|
|
|
|
failing the build. No silent fallbacks.
|
|
|
|
|
"""
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
2026-04-14 22:49:49 +08:00
|
|
|
import time
|
|
|
|
|
import urllib.error
|
2026-04-10 13:21:06 +08:00
|
|
|
import urllib.request
|
|
|
|
|
|
2026-04-14 17:26:01 +08:00
|
|
|
# Some servers (e.g. Berkeley) block the default Python-urllib User-Agent.
|
|
|
|
|
_opener = urllib.request.build_opener()
|
2026-04-24 18:02:21 +08:00
|
|
|
_opener.addheaders = [("User-Agent", "snapotter/1.0")]
|
2026-04-14 17:26:01 +08:00
|
|
|
urllib.request.install_opener(_opener)
|
|
|
|
|
|
2026-04-14 22:49:49 +08:00
|
|
|
|
2026-04-14 23:05:09 +08:00
|
|
|
def _urlretrieve(url: str, path: str, max_retries: int = 5) -> None:
|
|
|
|
|
"""Download url to path with exponential backoff on transient errors (5xx, timeout)."""
|
2026-04-14 22:49:49 +08:00
|
|
|
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
|
2026-04-14 23:05:09 +08:00
|
|
|
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)
|
2026-04-14 22:49:49 +08:00
|
|
|
except (urllib.error.URLError, OSError) as e:
|
|
|
|
|
if attempt == max_retries:
|
|
|
|
|
raise
|
2026-04-14 23:05:09 +08:00
|
|
|
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
|
2026-04-14 22:49:49 +08:00
|
|
|
|
2026-07-15 03:34:24 +08:00
|
|
|
# No GPU driver is available during build-time model verification.
|
2026-04-10 13:21:06 +08:00
|
|
|
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
|
|
|
|
|
2026-04-17 23:06:31 +08:00
|
|
|
# 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()
|
|
|
|
|
|
2026-04-13 00:48:05 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-12 21:22:55 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 21:56:59 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 19:40:55 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 19:50:23 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 21:57:51 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 19:50:23 +08:00
|
|
|
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
|
2026-04-13 19:40:55 +08:00
|
|
|
|
2026-04-14 16:18:17 +08:00
|
|
|
MEDIAPIPE_MODEL_DIR = "/opt/models/mediapipe"
|
2026-04-14 16:43:42 +08:00
|
|
|
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")
|
2026-04-14 16:18:17 +08:00
|
|
|
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
|
|
|
|
|
|
2026-04-14 16:51:46 +08:00
|
|
|
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
|
2026-04-14 17:07:23 +08:00
|
|
|
FACEXLIB_PARSE_URL = "https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth"
|
2026-04-14 16:51:46 +08:00
|
|
|
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")
|
2026-04-14 17:41:39 +08:00
|
|
|
OPENCV_CAFFE_URL = "https://huggingface.co/spaces/BilalSardar/Black-N-White-To-Color/resolve/main/colorization_release_v2.caffemodel"
|
2026-04-14 16:51:46 +08:00
|
|
|
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")
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
REMBG_MODELS = [
|
2026-03-23 11:46:45 +08:00
|
|
|
"u2net",
|
|
|
|
|
"isnet-general-use",
|
|
|
|
|
"bria-rmbg",
|
|
|
|
|
"birefnet-general-lite",
|
|
|
|
|
"birefnet-portrait",
|
|
|
|
|
"birefnet-general",
|
2026-04-12 18:23:09 +08:00
|
|
|
"birefnet-matting",
|
2026-05-05 22:56:31 +08:00
|
|
|
"birefnet-hr-matting",
|
2026-03-23 11:46:45 +08:00
|
|
|
]
|
|
|
|
|
|
2026-04-12 18:23:09 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 22:56:31 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
def download_rembg_models():
|
|
|
|
|
"""Download all rembg ONNX models."""
|
|
|
|
|
print("=== Downloading rembg models ===")
|
|
|
|
|
from rembg import new_session
|
|
|
|
|
|
2026-04-12 18:23:09 +08:00
|
|
|
_register_birefnet_matting()
|
2026-05-05 22:56:31 +08:00
|
|
|
_register_birefnet_hr_matting()
|
2026-04-12 18:23:09 +08:00
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
for model in REMBG_MODELS:
|
|
|
|
|
print(f" Downloading {model}...")
|
2026-03-23 11:46:45 +08:00
|
|
|
new_session(model)
|
|
|
|
|
print(f" {model} ready")
|
2026-04-10 13:21:06 +08:00
|
|
|
print(f"All {len(REMBG_MODELS)} rembg models downloaded.\n")
|
2026-03-23 11:46:45 +08:00
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
|
2026-04-13 00:48:05 +08:00
|
|
|
def download_lama_model():
|
2026-04-14 23:05:09 +08:00
|
|
|
"""Download LaMa ONNX inpainting model from HuggingFace Hub."""
|
2026-04-13 00:48:05 +08:00
|
|
|
print("=== Downloading LaMa ONNX model ===")
|
|
|
|
|
os.makedirs(LAMA_MODEL_DIR, exist_ok=True)
|
2026-04-14 23:05:09 +08:00
|
|
|
_hf_download("Carve/LaMa-ONNX", "lama_fp32.onnx", LAMA_MODEL_DIR, LAMA_MIN_SIZE, "LaMa ONNX")
|
|
|
|
|
print()
|
2026-04-13 00:48:05 +08:00
|
|
|
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
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}...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(REALESRGAN_MODEL_URL, REALESRGAN_MODEL_PATH)
|
2026-04-10 13:21:06 +08:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-04-12 21:22:55 +08:00
|
|
|
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}...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(GFPGAN_MODEL_URL, GFPGAN_MODEL_PATH)
|
2026-04-10 13:21:06 +08:00
|
|
|
|
2026-04-12 21:22:55 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 21:56:59 +08:00
|
|
|
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}...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(CODEFORMER_MODEL_URL, CODEFORMER_MODEL_PATH)
|
2026-04-13 21:56:59 +08:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 19:40:55 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 18:00:47 +08:00
|
|
|
def download_codeformer_onnx_model():
|
2026-04-13 21:57:51 +08:00
|
|
|
"""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")
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 19:50:23 +08:00
|
|
|
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)
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(SCUNET_MODEL_URL, SCUNET_MODEL_PATH)
|
2026-04-13 19:50:23 +08:00
|
|
|
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():
|
2026-04-14 23:05:09 +08:00
|
|
|
"""Download NAFNet SIDD width-64 denoising model from HuggingFace Hub."""
|
|
|
|
|
print("=== Downloading NAFNet model ===")
|
2026-04-13 19:50:23 +08:00
|
|
|
os.makedirs(NAFNET_MODEL_DIR, exist_ok=True)
|
2026-04-14 23:05:09 +08:00
|
|
|
_hf_download(
|
|
|
|
|
"mikestealth/nafnet-models",
|
|
|
|
|
"NAFNet-SIDD-width64.pth",
|
|
|
|
|
NAFNET_MODEL_DIR,
|
|
|
|
|
NAFNET_MIN_SIZE,
|
|
|
|
|
"NAFNet",
|
2026-04-13 19:50:23 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 16:51:46 +08:00
|
|
|
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...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(FACEXLIB_DET_URL, FACEXLIB_DET_PATH)
|
2026-04-14 16:51:46 +08:00
|
|
|
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...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(FACEXLIB_PARSE_URL, FACEXLIB_PARSE_PATH)
|
2026-04-14 16:51:46 +08:00
|
|
|
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)
|
|
|
|
|
|
2026-04-14 23:05:09 +08:00
|
|
|
# Prototxt and cluster-centres file are from GitHub raw content
|
2026-04-14 16:51:46 +08:00
|
|
|
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}...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(url, path)
|
2026-04-14 16:51:46 +08:00
|
|
|
size = os.path.getsize(path)
|
|
|
|
|
print(f" {name} downloaded ({size / 1_000_000:.1f} MB)")
|
|
|
|
|
|
2026-04-14 23:05:09 +08:00
|
|
|
# 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",
|
2026-04-14 16:51:46 +08:00
|
|
|
)
|
|
|
|
|
print("OpenCV colorization models downloaded.\n")
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 16:18:17 +08:00
|
|
|
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}...")
|
2026-04-14 22:49:49 +08:00
|
|
|
_urlretrieve(url, path)
|
2026-04-14 16:18:17 +08:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
def verify_mediapipe():
|
|
|
|
|
"""Verify MediaPipe face detection models are bundled in the wheel."""
|
|
|
|
|
print("=== Verifying MediaPipe models ===")
|
|
|
|
|
import mediapipe as mp
|
|
|
|
|
|
2026-04-14 16:18:17 +08:00
|
|
|
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,
|
2026-04-10 13:21:06 +08:00
|
|
|
)
|
2026-04-14 16:18:17 +08:00
|
|
|
detector = mp.tasks.vision.FaceDetector.create_from_options(options)
|
2026-04-10 13:21:06 +08:00
|
|
|
detector.close()
|
2026-04-14 16:18:17 +08:00
|
|
|
print(" mp.tasks FaceDetector OK")
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
print("MediaPipe models verified.\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def smoke_test():
|
|
|
|
|
"""Final verification that all ML libraries and models are loadable.
|
|
|
|
|
|
2026-07-15 03:34:24 +08:00
|
|
|
GPU-dependent libraries such as the CUDA build of torch cannot be imported
|
2026-04-10 13:21:06 +08:00
|
|
|
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
|
2026-04-11 17:49:28 +08:00
|
|
|
print(" CPU imports OK (Pillow, cv2, numpy, rembg)")
|
2026-04-10 13:21:06 +08:00
|
|
|
|
|
|
|
|
# MediaPipe is CPU-only, should always import
|
|
|
|
|
import mediapipe as mp
|
|
|
|
|
print(" MediaPipe import OK")
|
|
|
|
|
|
2026-04-13 00:48:05 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-12 21:22:55 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-13 21:56:59 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-13 19:40:55 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-13 21:57:51 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-13 19:50:23 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-14 16:18:17 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-14 16:51:46 +08:00
|
|
|
# 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")
|
|
|
|
|
|
2026-04-10 13:21:06 +08:00
|
|
|
print("Smoke test passed.\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2026-04-17 14:54:01 +08:00
|
|
|
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_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")
|
2026-04-10 13:21:06 +08:00
|
|
|
verify_mediapipe()
|
|
|
|
|
smoke_test()
|
2026-04-17 23:06:31 +08:00
|
|
|
_restore_cuda_provider()
|
2026-04-10 13:21:06 +08:00
|
|
|
print("All models downloaded and verified.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|