fix(docker): close remaining airgap gaps for fully offline operation (#70)

Three fixes to ensure zero network access after docker pull:

1. rembg model allowlist: validate model parameter against the 7
   pre-downloaded models, preventing rembg from attempting to download
   unknown models via a raw API call.

2. GFPGAN/CodeFormer auxiliary models: pre-download facexlib's
   detection_Resnet50_Final.pth and parsing_parsenet.pth at build time.
   These were previously downloaded on first use via basicsr. Symlinks
   in /app/gfpgan/weights/ ensure codeformer-pip also finds them.

3. OpenCV colorize models: pre-download the prototxt, caffemodel, and
   pts_in_hull.npy so the lightweight OpenCV colorizer fallback works
   in addition to the primary DDColor method.

Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
This commit is contained in:
stirling-image
2026-04-14 16:51:46 +08:00
committed by GitHub
co-authored by stirling-image
parent e3bfa6585a
commit 8d8ab4bc45
3 changed files with 112 additions and 0 deletions
+5
View File
@@ -187,6 +187,11 @@ COPY --from=builder /app/apps/web/dist ./apps/web/dist
# Create required directories
RUN mkdir -p /data /data/files /tmp/workspace
# Symlink facexlib models for codeformer-pip (expects gfpgan/weights/ relative to CWD)
RUN mkdir -p /app/gfpgan/weights/CodeFormer && \
ln -sf /opt/models/gfpgan/facelib /app/gfpgan/weights/facelib && \
ln -sf /opt/models/codeformer/codeformer.pth /app/gfpgan/weights/CodeFormer/codeformer.pth
# Environment defaults
ENV PORT=1349 \
NODE_ENV=production \
+95
View File
@@ -73,6 +73,23 @@ FACE_LANDMARKER_MODEL_URL = "https://storage.googleapis.com/mediapipe-models/fac
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.1.0/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 = "http://eecs.berkeley.edu/~rich.zhang/projects/2016_colorization/files/demo_v2/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",
@@ -318,6 +335,58 @@ def download_nafnet_model():
print(f" NAFNet model downloaded: {size:,} bytes")
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...")
urllib.request.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...")
urllib.request.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)
for url, path, name in [
(OPENCV_PROTO_URL, OPENCV_PROTO_PATH, "colorization_deploy_v2.prototxt"),
(OPENCV_CAFFE_URL, OPENCV_CAFFE_PATH, "colorization_release_v2.caffemodel"),
(OPENCV_POINTS_URL, OPENCV_POINTS_PATH, "pts_in_hull.npy"),
]:
print(f" Downloading {name}...")
urllib.request.urlretrieve(url, path)
size = os.path.getsize(path)
print(f" {name} downloaded ({size / 1_000_000:.1f} MB)")
# Verify the caffemodel (the big one)
size = os.path.getsize(OPENCV_CAFFE_PATH)
assert size > OPENCV_CAFFE_MIN_SIZE, (
f"Caffemodel too small: {size} bytes (expected > {OPENCV_CAFFE_MIN_SIZE})"
)
print("OpenCV colorization models downloaded.\n")
def download_mediapipe_task_models():
"""Download MediaPipe tasks API model files for face detection and landmarks.
@@ -485,6 +554,30 @@ def smoke_test():
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")
@@ -501,6 +594,8 @@ def main():
download_paddleocr_vl_model()
download_scunet_model()
download_nafnet_model()
download_facexlib_models()
download_opencv_colorize_models()
download_mediapipe_task_models()
verify_mediapipe()
smoke_test()