# Offline (air-gapped / 폐쇄망) install The deployment target has **no internet**. All Python dependencies must be carried across the air gap as pre-downloaded wheels. The build/staging machine (which has internet) does the downloading; the target only installs. ## Prerequisites - Same OS + CPU architecture + **Python 3.13** on build and target machines (wheels are platform- and version-specific). - The repository itself transferred to the target. ## 1. On the build machine (has internet) ```bash # Runtime only (--only-binary keeps sdists out so nothing has to build on the # offline target; also carry pip/setuptools/wheel themselves): pip download --only-binary=:all: -r requirements.txt -d wheelhouse/ pip download --only-binary=:all: pip setuptools wheel -d wheelhouse/ # Or runtime + test tooling: pip download --only-binary=:all: -r requirements-dev.txt -d wheelhouse/ ``` This fills `wheelhouse/` with every wheel (including transitive deps). ### Hash-verified lock (recommended) Pin the full transitive tree with hashes so a tampered/substituted wheel is rejected at install time: ```bash # On the build machine (pip-tools): pip-compile --generate-hashes -o requirements.lock requirements.txt ``` Then install on the target with `--require-hashes` (step 3). ### Model weights (`models/`) Some analysis paths use pre-trained weights that are **not** pip packages and are **not** committed to the repository (`models/` is gitignored, same treatment as `wheelhouse/`). Fetch them on the build machine: ```bash mkdir -p models # YuNet face detector (OpenCV Zoo, MIT). Used by face/person presence detection; # cv2.FaceDetectorYN itself ships inside opencv-python-headless, only the weights # are external. curl -sSL -o models/face_detection_yunet_2023mar.onnx \ https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx ``` Verify before transferring — a truncated or substituted model must not cross the air gap unnoticed: | file | bytes | sha256 | |---|---:|---| | `face_detection_yunet_2023mar.onnx` | 232589 | `8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4` | ```bash python - <<'PY' import hashlib, pathlib p = pathlib.Path("models/face_detection_yunet_2023mar.onnx") print(len(p.read_bytes()), hashlib.sha256(p.read_bytes()).hexdigest()) PY ``` If `models/` is absent on the target the server still starts: face detection falls back to the bundled Haar cascades and logs the downgrade once. It does not fail closed, because a missing optional weight must not stop an operator from reviewing. ## 2. Transfer Copy the repository, the `wheelhouse/` directory, and the `models/` directory to the target via the approved air-gap transfer process (e.g. removable media). ## 3. On the target machine (no internet) ```bash python -m venv .venv . .venv/bin/activate # Windows: .venv\Scripts\activate pip install --no-index --find-links wheelhouse/ -r requirements.txt ``` `--no-index` guarantees pip never reaches out to the internet; `--find-links` points it at the local wheelhouse. ## 4. Verify offline ```bash # Confirm no network access is attempted and imports resolve: python -c "import numpy, cv2, PIL; print('deps ok')" python -m pytest -q # if test deps were installed ``` ## Optional: Ollama LLM model (evidence narration) The LLM assistant is optional. If you want it on the air-gapped target, the model weights must be carried across too — they are **not** part of the Python wheelhouse: ```bash # Build machine (has internet): ollama pull qwen2.5:0.5b-instruct # or your pinned OLLAMA_MODEL # Copy the model store across the air gap: # default: ~/.ollama/models (or set OLLAMA_MODELS to a directory you transfer) # Target machine (no internet): export OLLAMA_MODELS=/path/to/transferred/models ollama serve & # Smoke test: ollama run qwen2.5:0.5b-instruct "hello" ``` Set `OLLAMA_BASE_URL` / `OLLAMA_MODEL` in `.env`. Leave them unset to run without the LLM (the `llm` provider then reports disabled). ## Optional: Playwright Chromium (E2E tests) The operator-GUI E2E suite needs a Chromium binary, which Playwright normally downloads. For the air gap, fetch it on the build machine and carry it: ```bash # Build machine: PLAYWRIGHT_BROWSERS_PATH=./pw-browsers playwright install chromium # Transfer the pw-browsers/ directory to the target, then: # Target machine: export PLAYWRIGHT_BROWSERS_PATH=/path/to/pw-browsers python -m pytest tests/operator_gui -q ``` ## Notes - The operator GUI (`web/operator-gui/`) is fully self-contained: all scripts/styles are local and the Pretendard font is bundled (`assets/fonts/PretendardVariable.woff2`). No CDN fetch at runtime. - External **search** APIs (Google / Naver / Cloud Vision) are an allowed runtime exception and require outbound access to those providers. They are optional; missing credentials disable the provider (see the startup provider-readiness log). - **Broader egress:** beyond the API hosts, the image-comparison feature also fetches arbitrary public result-page / image / stylesheet URLs returned by those searches (SSRF-guarded: private/loopback/CGNAT/link-local ranges are blocked). Restricting egress to the API hosts only disables image comparison of external results; it does not break core review. - `COPYRIGHTER_AUTH_TOKEN` should be set in `.env` for any multi-operator or networked deployment so the data routes require a shared token.