mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: remove automatic third-party egress of user data + optional strict offline mode (OSM tiles, Scalar fonts, editor fonts, AI model downloads) (#422)
* fix: remove all automatic third-party egress (OSM tiles, Scalar fonts, editor Google Fonts, AI model download fallbacks) Phone-home audit follow-up. The product no longer makes any automatic third-party request; user-initiated click-outs stay, and production now fails closed on missing AI models. 1. GPS leak via OSM tiles: the strip-metadata panel auto-loaded tile.openstreetmap.org tiles encoding the photo's GPS position. The Leaflet mini-map is gone; coordinates render as text plus an explicit View on map link (openstreetmap.org, opens on click only). Removed tile.openstreetmap.org from the CSP img-src, dropped the leaflet dependency, added the viewOnMap i18n key to all 21 locales. 2. Scalar docs fonts: /api/docs loaded Inter and JetBrains Mono from fonts.scalar.com. Scalar now renders with withDefaultFonts: false and both --scalar-font and --scalar-font-code pinned to system stacks; fonts.scalar.com removed from the docs CSP font-src. Verified by injecting GET /api/docs/: config carries withDefaultFonts false and the served page has no fonts.scalar.com reference. 3. Editor Google Fonts: the editor font picker built fonts.googleapis.com stylesheet URLs for 25 web fonts the served CSP already blocked. The remote loading path is deleted; the picker now offers system fonts only, with a SELF_HOSTED_FONTS seam (FontFace API, same origin) for bundling fonts later. Unknown families saved in old documents fall back to the browser default. 4. Python sidecar fails closed on model downloads: new packages/ai/python/offline_guard.py gates every runtime download fallback (inpaint, outpaint, restore, noise_removal, detect_faces, enhance_faces, face_landmarks, red_eye_removal, remove_bg, ocr, transcribe, upscale) behind SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1 with an actionable error. Bundled models keep working untouched. 5. OCR and transcription library-internal downloads: unbundled PaddleOCR language and detection fallbacks now raise the guard error naming the language instead of resolving models over the network; faster-whisper gets local_files_only when downloads are off. 6. GFPGAN and CodeFormer cwd-relative weights: facexlib and codeformer-pip resolve helper weights relative to the process cwd and fetch them from GitHub when absent. They are now symlinked from the installed bundle files under MODELS_PATH/gfpgan/facelib before the libraries load, failing closed when unresolvable. Defense in depth: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set in the runtime image and in the sidecar spawn env; install_feature.py lifts them for user-initiated bundle installs and restores them afterwards (it can run in-process inside the dispatcher). SNAPOTTER_ALLOW_MODEL_DOWNLOAD is documented in .env.example, default off. Validation: typecheck 9/9 workspaces, Biome clean on touched files, 5178 unit tests pass, py_compile on all touched scripts, guard behavior exercised in both dispatcher exec and per-request import modes, zero remaining runtime references to the three hosts. Docker build and live AI inference need post-merge verification on the GPU host. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 * fix: allow AI model downloads by default, make strict offline mode opt-in Product call: ease of use first. The download gating from the previous commit inverts its default: runtime model fetches (public model weights only, never user data) are allowed out of the box so AI tools self-heal, and SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 becomes the explicit strict offline mode for airgapped deployments, where every fallback raises the actionable error instead of fetching. Changes: offline_guard blocks only on an explicit 0/false; the unconditional HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE image ENV is removed and bridge.ts sets those flags for the sidecar only in strict mode; .env.example documents the new default; install_feature's lift/restore stays. All bundled-path preferences, pre-existence checks, and symlink pre-placement remain, so installed bundles never trigger a download. The OSM, Scalar font, and editor font fixes are unchanged. Validation rerun: typecheck 9/9, Biome clean on touched files, 5178 unit tests pass, py_compile on touched scripts, guard behavior verified for unset/1 (allowed) and 0/false (blocked with the new message). Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
This commit is contained in:
@@ -25,6 +25,8 @@ def _ensure_face_detect_model():
|
||||
return _DOCKER_MODEL_PATH
|
||||
if os.path.exists(_LOCAL_MODEL_PATH):
|
||||
return _LOCAL_MODEL_PATH
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("Face detection model (blaze_face_short_range.tflite)")
|
||||
os.makedirs(_LOCAL_MODEL_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
emit_progress(15, "Downloading face detection model")
|
||||
|
||||
@@ -55,6 +55,8 @@ def _ensure_face_detect_model():
|
||||
return _DOCKER_MODEL_PATH
|
||||
if os.path.exists(_LOCAL_MODEL_PATH):
|
||||
return _LOCAL_MODEL_PATH
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("Face detection model (blaze_face_short_range.tflite)")
|
||||
os.makedirs(_LOCAL_MODEL_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
emit_progress(15, "Downloading face detection model")
|
||||
@@ -154,6 +156,12 @@ def enhance_with_gfpgan(img_array, only_center_face):
|
||||
if not os.path.exists(GFPGAN_MODEL_PATH):
|
||||
raise FileNotFoundError(f"GFPGAN model not found: {GFPGAN_MODEL_PATH}")
|
||||
|
||||
# GFPGANer resolves its facexlib helper weights relative to the cwd and
|
||||
# downloads them from GitHub when missing; resolve them from the bundle
|
||||
# first so no download is needed (strict offline mode errors instead).
|
||||
from offline_guard import prepare_gfpgan_helper_weights
|
||||
prepare_gfpgan_helper_weights(_MODELS_BASE)
|
||||
|
||||
use_gpu = gpu_available()
|
||||
device = torch.device("cuda" if use_gpu else "cpu")
|
||||
|
||||
@@ -193,6 +201,12 @@ def enhance_with_codeformer(img_array, fidelity_weight):
|
||||
|
||||
use_gpu = gpu_available()
|
||||
|
||||
# codeformer-pip downloads four weights into a cwd-relative tree at import
|
||||
# time when they are missing; resolve the bundled ones first so only a
|
||||
# genuinely unbundled weight can trigger the download fallback.
|
||||
from offline_guard import prepare_codeformer_weights
|
||||
prepare_codeformer_weights(_MODELS_BASE)
|
||||
|
||||
_orig_cuda_check = torch.cuda.is_available
|
||||
if not use_gpu:
|
||||
torch.cuda.is_available = lambda: False
|
||||
|
||||
@@ -92,6 +92,8 @@ def ensure_model():
|
||||
return _DOCKER_MODEL_PATH
|
||||
if os.path.exists(MODEL_PATH):
|
||||
return MODEL_PATH
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("Face landmark model (face_landmarker.task)")
|
||||
os.makedirs(MODEL_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
emit_progress(15, "Downloading face model")
|
||||
|
||||
@@ -24,13 +24,14 @@ MODEL_SIZE = 512
|
||||
|
||||
|
||||
def _get_model_path():
|
||||
"""Return path to the LaMa ONNX model, downloading if needed."""
|
||||
"""Return path to the LaMa ONNX model, downloading only if allowed."""
|
||||
if os.path.exists(LAMA_MODEL_PATH):
|
||||
return LAMA_MODEL_PATH
|
||||
if os.path.exists(LAMA_LOCAL_PATH):
|
||||
return LAMA_LOCAL_PATH
|
||||
|
||||
# Auto-download for local dev
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("LaMa inpainting model (lama_fp32.onnx)")
|
||||
emit_progress(5, "Downloading LaMa model")
|
||||
os.makedirs(LAMA_LOCAL_CACHE, exist_ok=True)
|
||||
import urllib.request
|
||||
|
||||
@@ -357,6 +357,29 @@ def write_installed_atomic(ai_dir: str, data: dict) -> None:
|
||||
# -- Main --
|
||||
|
||||
def main() -> None:
|
||||
"""Run the install with runtime-download restrictions lifted.
|
||||
|
||||
In strict offline mode (SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0) the sidecar
|
||||
runs with HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE=1; a bundle install is an
|
||||
explicitly user-initiated download, so those flags are lifted here
|
||||
regardless. The previous values are restored in the finally block because
|
||||
this script can run in-process inside the long-lived dispatcher, where
|
||||
os.environ changes would otherwise leak into every later request.
|
||||
"""
|
||||
saved = {key: os.environ.get(key) for key in ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE")}
|
||||
os.environ["HF_HUB_OFFLINE"] = "0"
|
||||
os.environ["TRANSFORMERS_OFFLINE"] = "0"
|
||||
try:
|
||||
_install()
|
||||
finally:
|
||||
for key, value in saved.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def _install() -> None:
|
||||
if len(sys.argv) < 4:
|
||||
fail(
|
||||
f"Usage: {sys.argv[0]} <bundleId> <manifestPath> <modelsDir>\n"
|
||||
|
||||
@@ -39,7 +39,8 @@ def _get_model_path(env_path, filename, url):
|
||||
if os.path.exists(local_path):
|
||||
return local_path
|
||||
|
||||
# Auto-download
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"Denoising model ({filename})")
|
||||
emit_progress(10, f"Downloading {filename}")
|
||||
os.makedirs(_CACHE_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
|
||||
@@ -220,10 +220,17 @@ def run_paddleocr_v5(input_path, language):
|
||||
emit_progress(20, "Loading")
|
||||
mk = _bundled_paddle_kwargs(paddle_lang)
|
||||
# When a bundled recognizer is pinned, the model selects the script, so
|
||||
# we omit lang (this is the proven fully-offline path). Only fall back to
|
||||
# lang-based (online) resolution when no bundled rec exists (e.g. ja).
|
||||
# we omit lang (this is the proven fully-offline path). Lang-based
|
||||
# resolution for a language without a bundled recognizer (e.g. ja) and
|
||||
# a missing detection model both make PaddleOCR fetch models over the
|
||||
# network, which strict offline mode blocks with a clear error.
|
||||
if "text_recognition_model_dir" not in mk:
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"PaddleOCR recognition model for language '{language}'")
|
||||
mk["lang"] = paddle_lang
|
||||
if "text_detection_model_dir" not in mk:
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"PaddleOCR text detection model ({PADDLE_DET_MODEL})")
|
||||
ocr = PaddleOCR(
|
||||
device=device,
|
||||
ocr_version="PP-OCRv5",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Gate for runtime model downloads, with an optional strict offline mode.
|
||||
|
||||
Models normally arrive through user-initiated feature bundle installs
|
||||
(install_feature.py), and the resolvers in the AI scripts always prefer those
|
||||
bundled files. When a model is missing, scripts may fetch the public model
|
||||
weights as a fallback so tools work out of the box; that fallback only ever
|
||||
downloads public model files, never user data.
|
||||
|
||||
Setting SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 enables strict offline mode for
|
||||
airgapped or locked-down deployments: every script calls
|
||||
ensure_download_allowed() immediately before any download fallback, so a
|
||||
missing file then surfaces as an actionable error instead of an outbound
|
||||
fetch.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
def downloads_allowed():
|
||||
"""True unless strict offline mode is explicitly enabled.
|
||||
|
||||
Runtime model downloads are allowed by default; only an explicit
|
||||
SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 (or "false") blocks them.
|
||||
"""
|
||||
return os.environ.get("SNAPOTTER_ALLOW_MODEL_DOWNLOAD", "1").lower() not in ("0", "false")
|
||||
|
||||
|
||||
def ensure_download_allowed(what):
|
||||
"""Raise a clear, actionable error when strict offline mode blocks a fetch."""
|
||||
if downloads_allowed():
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"{what} is missing and automatic downloads are disabled by "
|
||||
"SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0. Reinstall the feature bundle from "
|
||||
"Settings, or unset SNAPOTTER_ALLOW_MODEL_DOWNLOAD to permit downloads."
|
||||
)
|
||||
|
||||
|
||||
def link_bundled_weight(link_path, target_path):
|
||||
"""Best-effort: make link_path resolve to an installed bundle file.
|
||||
|
||||
gfpgan and codeformer-pip hardcode weight paths relative to the process
|
||||
cwd, while the feature bundles install those weights under MODELS_PATH.
|
||||
Symlinking the expected path to the bundled file lets the libraries find
|
||||
the weight without downloading. Returns True when link_path exists
|
||||
afterwards (already present, or successfully linked).
|
||||
"""
|
||||
if os.path.exists(link_path):
|
||||
return True
|
||||
if not os.path.exists(target_path):
|
||||
return False
|
||||
try:
|
||||
parent = os.path.dirname(link_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
os.symlink(target_path, link_path)
|
||||
except OSError:
|
||||
return os.path.exists(link_path)
|
||||
return True
|
||||
|
||||
|
||||
GFPGAN_HELPER_WEIGHTS = ("detection_Resnet50_Final.pth", "parsing_parsenet.pth")
|
||||
|
||||
|
||||
def prepare_gfpgan_helper_weights(models_base):
|
||||
"""Resolve GFPGAN's cwd-relative facexlib helper weights offline.
|
||||
|
||||
gfpgan 1.3.x hardcodes FaceRestoreHelper(model_rootpath="gfpgan/weights"),
|
||||
a path relative to the process cwd, and facexlib downloads any file
|
||||
missing from it (GitHub release URLs). The feature bundles install those
|
||||
weights under <models>/gfpgan/facelib, so link them into the expected
|
||||
location; when a weight cannot be resolved locally, strict offline mode
|
||||
errors instead of downloading.
|
||||
"""
|
||||
for fname in GFPGAN_HELPER_WEIGHTS:
|
||||
link = os.path.join("gfpgan", "weights", fname)
|
||||
target = os.path.join(models_base, "gfpgan", "facelib", fname)
|
||||
if not link_bundled_weight(link, target):
|
||||
ensure_download_allowed(f"GFPGAN helper weight {fname}")
|
||||
|
||||
|
||||
def prepare_codeformer_weights(models_base):
|
||||
"""Resolve codeformer-pip's cwd-relative weights offline.
|
||||
|
||||
codeformer-pip 0.0.4 downloads four weights into a cwd-relative
|
||||
CodeFormer/weights/ tree at import time of codeformer.app. Three of them
|
||||
ship in the feature bundles and are linked here so they never re-download;
|
||||
RealESRGAN_x2plus.pth (a background-upscale helper this app never invokes)
|
||||
is not bundled, so it downloads once on first use unless strict offline
|
||||
mode blocks it.
|
||||
"""
|
||||
expected = {
|
||||
os.path.join("CodeFormer", "weights", "CodeFormer", "codeformer.pth"): os.path.join(
|
||||
models_base, "codeformer", "codeformer.pth"
|
||||
),
|
||||
os.path.join("CodeFormer", "weights", "facelib", "detection_Resnet50_Final.pth"): os.path.join(
|
||||
models_base, "gfpgan", "facelib", "detection_Resnet50_Final.pth"
|
||||
),
|
||||
os.path.join("CodeFormer", "weights", "facelib", "parsing_parsenet.pth"): os.path.join(
|
||||
models_base, "gfpgan", "facelib", "parsing_parsenet.pth"
|
||||
),
|
||||
}
|
||||
for link, target in expected.items():
|
||||
if not link_bundled_weight(link, target):
|
||||
ensure_download_allowed(f"CodeFormer weight {os.path.basename(link)}")
|
||||
|
||||
x2plus = os.path.join("CodeFormer", "weights", "realesrgan", "RealESRGAN_x2plus.pth")
|
||||
if not os.path.exists(x2plus):
|
||||
ensure_download_allowed("CodeFormer helper weight RealESRGAN_x2plus.pth")
|
||||
@@ -28,12 +28,14 @@ LAMA_HF_URL = "https://huggingface.co/Carve/LaMa-ONNX/resolve/main/lama_fp32.onn
|
||||
|
||||
|
||||
def _get_model_path():
|
||||
"""Return path to the LaMa ONNX model, downloading if needed."""
|
||||
"""Return path to the LaMa ONNX model, downloading only if allowed."""
|
||||
if os.path.exists(LAMA_MODEL_PATH):
|
||||
return LAMA_MODEL_PATH
|
||||
if os.path.exists(LAMA_LOCAL_PATH):
|
||||
return LAMA_LOCAL_PATH
|
||||
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("LaMa inpainting model (lama_fp32.onnx)")
|
||||
emit_progress(5, "Downloading LaMa model")
|
||||
os.makedirs(LAMA_LOCAL_CACHE, exist_ok=True)
|
||||
import urllib.request
|
||||
|
||||
@@ -25,6 +25,8 @@ def _ensure_face_mesh_model():
|
||||
return _DOCKER_MODEL_PATH
|
||||
if os.path.exists(_LOCAL_MODEL_PATH):
|
||||
return _LOCAL_MODEL_PATH
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("Face landmark model (face_landmarker.task)")
|
||||
os.makedirs(_LOCAL_MODEL_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
emit_progress(15, "Downloading face mesh model")
|
||||
|
||||
@@ -104,6 +104,10 @@ def _register_matting_session(sessions_class):
|
||||
@classmethod
|
||||
def download_models(cls, *args, **kwargs):
|
||||
fname = f"{cls.name(*args, **kwargs)}.onnx"
|
||||
target = os.path.join(cls.u2net_home(*args, **kwargs), fname)
|
||||
if not os.path.exists(target):
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"Background removal model '{cls.name(*args, **kwargs)}'")
|
||||
pooch.retrieve(
|
||||
"https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet-matting-epoch_100.onnx",
|
||||
None, # Skip checksum for GitHub release assets
|
||||
@@ -111,7 +115,7 @@ def _register_matting_session(sessions_class):
|
||||
path=cls.u2net_home(*args, **kwargs),
|
||||
progressbar=True,
|
||||
)
|
||||
return os.path.join(cls.u2net_home(*args, **kwargs), fname)
|
||||
return target
|
||||
|
||||
@classmethod
|
||||
def name(cls, *args, **kwargs):
|
||||
@@ -138,6 +142,10 @@ def _register_hr_matting_session(sessions_class):
|
||||
@classmethod
|
||||
def download_models(cls, *args, **kwargs):
|
||||
fname = f"{cls.name(*args, **kwargs)}.onnx"
|
||||
target = os.path.join(cls.u2net_home(*args, **kwargs), fname)
|
||||
if not os.path.exists(target):
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"Background removal model '{cls.name(*args, **kwargs)}'")
|
||||
pooch.retrieve(
|
||||
"https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet_HR-matting-epoch_135.onnx",
|
||||
None,
|
||||
@@ -145,7 +153,7 @@ def _register_hr_matting_session(sessions_class):
|
||||
path=cls.u2net_home(*args, **kwargs),
|
||||
progressbar=True,
|
||||
)
|
||||
return os.path.join(cls.u2net_home(*args, **kwargs), fname)
|
||||
return target
|
||||
|
||||
@classmethod
|
||||
def name(cls, *args, **kwargs):
|
||||
@@ -194,6 +202,17 @@ def main():
|
||||
_register_matting_session(sessions_class)
|
||||
_register_hr_matting_session(sessions_class)
|
||||
|
||||
# Every built-in rembg session downloads its .onnx (pooch,
|
||||
# GitHub/HuggingFace) when it is missing from the rembg home dir;
|
||||
# strict offline mode blocks that fallback with a clear error.
|
||||
# Mirrors rembg's own home resolution.
|
||||
model_home = os.path.expanduser(
|
||||
os.getenv("U2NET_HOME", os.path.join(os.getenv("XDG_DATA_HOME", "~"), ".u2net"))
|
||||
)
|
||||
if not os.path.exists(os.path.join(model_home, f"{model}.onnx")):
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed(f"Background removal model '{model}'")
|
||||
|
||||
emit_progress(10, "Loading model")
|
||||
|
||||
providers, device = onnx_providers()
|
||||
|
||||
@@ -164,12 +164,13 @@ def _filter_components(mask, total_pixels):
|
||||
# ── LaMa inpainting ──────────────────────────────────────────────────
|
||||
|
||||
def _get_lama_path():
|
||||
"""Resolve LaMa model path, downloading if needed."""
|
||||
"""Resolve LaMa model path, downloading only if allowed."""
|
||||
if os.path.exists(LAMA_MODEL_PATH):
|
||||
return LAMA_MODEL_PATH
|
||||
if os.path.exists(LAMA_LOCAL_PATH):
|
||||
return LAMA_LOCAL_PATH
|
||||
# Auto-download for local dev
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("LaMa inpainting model (lama_fp32.onnx)")
|
||||
os.makedirs(LAMA_LOCAL_CACHE, exist_ok=True)
|
||||
import urllib.request
|
||||
url = "https://huggingface.co/Carve/LaMa-ONNX/resolve/main/lama_fp32.onnx"
|
||||
@@ -294,13 +295,14 @@ def _inpaint_tiled(img_rgb, mask, session):
|
||||
# ── CodeFormer face enhancement ──────────────────────────────────────
|
||||
|
||||
def _get_codeformer_path():
|
||||
"""Resolve CodeFormer ONNX model path, downloading if needed."""
|
||||
"""Resolve CodeFormer ONNX model path, downloading only if allowed."""
|
||||
if os.path.exists(CODEFORMER_MODEL_PATH):
|
||||
return CODEFORMER_MODEL_PATH
|
||||
if os.path.exists(CODEFORMER_LOCAL_PATH):
|
||||
return CODEFORMER_LOCAL_PATH
|
||||
|
||||
# Auto-download for local dev
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("CodeFormer model (codeformer.onnx)")
|
||||
os.makedirs(CODEFORMER_LOCAL_CACHE, exist_ok=True)
|
||||
emit_progress(35, "Downloading CodeFormer model")
|
||||
from huggingface_hub import hf_hub_download
|
||||
@@ -326,6 +328,8 @@ def _ensure_face_detect_model():
|
||||
return _FACE_DETECT_DOCKER_PATH
|
||||
if os.path.exists(_FACE_DETECT_LOCAL_PATH):
|
||||
return _FACE_DETECT_LOCAL_PATH
|
||||
from offline_guard import ensure_download_allowed
|
||||
ensure_download_allowed("Face detection model (blaze_face_short_range.tflite)")
|
||||
os.makedirs(_FACE_DETECT_LOCAL_DIR, exist_ok=True)
|
||||
import urllib.request
|
||||
emit_progress(15, "Downloading face detection model")
|
||||
|
||||
@@ -35,12 +35,24 @@ def main():
|
||||
|
||||
model_dir = os.path.join(MODELS_PATH, "faster-whisper-small")
|
||||
|
||||
# When the bundled model dir is absent, faster-whisper treats the
|
||||
# argument as a Hugging Face repo id and downloads it; strict offline
|
||||
# mode blocks that fallback with a clear error.
|
||||
from offline_guard import downloads_allowed, ensure_download_allowed
|
||||
if not os.path.isdir(model_dir):
|
||||
ensure_download_allowed("Whisper transcription model (faster-whisper-small)")
|
||||
|
||||
if gpu_available():
|
||||
device, compute_type = "cuda", "float16"
|
||||
else:
|
||||
device, compute_type = "cpu", "int8"
|
||||
|
||||
model = WhisperModel(model_dir, device=device, compute_type=compute_type)
|
||||
model = WhisperModel(
|
||||
model_dir,
|
||||
device=device,
|
||||
compute_type=compute_type,
|
||||
local_files_only=not downloads_allowed(),
|
||||
)
|
||||
|
||||
emit_progress(20, "Transcribing")
|
||||
|
||||
|
||||
@@ -172,6 +172,13 @@ def main():
|
||||
f"GFPGAN model not found at {GFPGAN_MODEL_PATH}. "
|
||||
"Install the upscale-enhance feature or disable faceEnhance."
|
||||
)
|
||||
# GFPGANer resolves its facexlib helper weights
|
||||
# relative to the cwd and downloads them from GitHub
|
||||
# when missing; resolve them from the bundle first so
|
||||
# no download is needed (strict offline mode errors
|
||||
# instead).
|
||||
from offline_guard import prepare_gfpgan_helper_weights
|
||||
prepare_gfpgan_helper_weights(_MODELS_BASE)
|
||||
face_enhancer = GFPGANer(
|
||||
model_path=GFPGAN_MODEL_PATH,
|
||||
upscale=scale,
|
||||
|
||||
Reference in New Issue
Block a user