The first launch was a code box with a link under it, then an empty Live screen with 'No cameras' in amber in a far corner, then a form asking for an IP address, and for the first few minutes of all of it the engine silently downloading 275 MB with nothing on screen but a stopped-looking status. Walked in a browser with the new mock; nobody who was not an installer would have got through it. Now: a welcome that asks the one question a shop owner can answer - managed from a head office, or on this PC only - with each path in a sentence; a Getting Started checklist on Live that reads its three steps from the engine and ticks them itself (recognition ready, camera added and connected, camera proven by a walk-past), with the one button for the next step, and that disappears the moment somebody is recognised; and the model download reported as a percentage in the tray, the sidebar and the checklist, parsed by the supervisor from the engine's own progress lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
145 lines
6.0 KiB
Python
145 lines
6.0 KiB
Python
"""Model acquisition: download YuNet, copy reusable models from the old
|
|
projects on this machine when present. Idempotent — safe to re-run."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
YUNET_URL = ("https://github.com/opencv/opencv_zoo/raw/main/models/"
|
|
"face_detection_yunet/face_detection_yunet_2023mar.onnx")
|
|
BUFFALO_SC_URL = ("https://github.com/deepinsight/insightface/releases/"
|
|
"download/v0.7/buffalo_sc.zip")
|
|
RECOGNIZERS = ["adaface_ir101.onnx", "adaface_ir50.onnx", "w600k_r50.onnx",
|
|
"arcface_int8.onnx", "w600k_mbf.onnx", "arcface.onnx"]
|
|
|
|
# Known locations of reusable models from the previous projects.
|
|
_LEGACY_MODEL_DIRS = [
|
|
Path(r"D:\NEARLE\WOrking now\RTSP_16072025\pattern_reg\models"),
|
|
]
|
|
|
|
BUFFALO_L_URL = ("https://github.com/deepinsight/insightface/releases/"
|
|
"download/v0.7/buffalo_l.zip")
|
|
|
|
# target filename -> legacy filename
|
|
_COPY_MAP = {
|
|
"arcface.onnx": "arcface.onnx",
|
|
"age_deploy.prototxt": "age_deploy.prototxt",
|
|
"age_net.caffemodel": "age_net.caffemodel",
|
|
"gender_deploy.prototxt": "gender_deploy.prototxt",
|
|
"gender_net.caffemodel": "gender_net.caffemodel",
|
|
"emotion-ferplus-8.onnx": "emotion-ferplus-8.onnx",
|
|
}
|
|
|
|
|
|
def _fetch(url: str, dest: Path, label: str) -> None:
|
|
"""Download with progress on stdout the supervisor can read.
|
|
|
|
On first run this is minutes of nothing: the API is not up yet, so the
|
|
app cannot ask the engine what it is doing, and a shop PC that shows a
|
|
stopped engine for five minutes after install looks broken. The
|
|
supervisor watches for `download: <label> <n>%` and puts the number in
|
|
the tray and the window. Logged every 5 points, not every chunk, so the
|
|
log file does not fill with a progress bar.
|
|
"""
|
|
last = -5
|
|
|
|
def hook(blocks: int, block_size: int, total: int) -> None:
|
|
nonlocal last
|
|
if total <= 0:
|
|
return
|
|
pct = min(100, blocks * block_size * 100 // total)
|
|
if pct >= last + 5:
|
|
last = pct
|
|
log.info("download: %s %d%%", label, pct)
|
|
|
|
urllib.request.urlretrieve(url, dest, hook)
|
|
log.info("download: %s 100%%", label)
|
|
|
|
|
|
def setup_models(models_dir: Path) -> "list[str]":
|
|
"""Ensure all model files exist in models_dir. Returns missing ones."""
|
|
models_dir = Path(models_dir)
|
|
models_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
yunet = models_dir / "face_detection_yunet_2023mar.onnx"
|
|
if not yunet.exists():
|
|
log.info("downloading YuNet face detector (~230 KB)...")
|
|
tmp = yunet.with_suffix(".part")
|
|
_fetch(YUNET_URL, tmp, "face detector")
|
|
tmp.rename(yunet)
|
|
log.info("YuNet saved to %s", yunet)
|
|
|
|
for target_name, legacy_name in _COPY_MAP.items():
|
|
target = models_dir / target_name
|
|
if target.exists():
|
|
continue
|
|
for legacy_dir in _LEGACY_MODEL_DIRS:
|
|
src = legacy_dir / legacy_name
|
|
if src.exists():
|
|
log.info("copying %s from %s ...", legacy_name, legacy_dir)
|
|
shutil.copy2(src, target)
|
|
break
|
|
|
|
# Any one recognizer is enough; get the lightweight MobileFaceNet if
|
|
# none is present (13 MB, loads reliably on low-memory machines).
|
|
if not any((models_dir / n).exists() for n in RECOGNIZERS):
|
|
log.info("downloading MobileFaceNet recognizer (buffalo_sc, ~15 MB)...")
|
|
import io
|
|
import zipfile
|
|
|
|
with urllib.request.urlopen(BUFFALO_SC_URL) as resp:
|
|
payload = io.BytesIO(resp.read())
|
|
with zipfile.ZipFile(payload) as zf, \
|
|
zf.open("w600k_mbf.onnx") as src, \
|
|
open(models_dir / "w600k_mbf.onnx", "wb") as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
log.info("w600k_mbf.onnx saved")
|
|
|
|
# buffalo_l carries both the modern gender+age net (1.3 MB) and the
|
|
# ResNet50 recognizer (~166 MB, IJB-C 97.25 vs MobileFaceNet's 95.02).
|
|
# One 275 MB download serves both, so fetch it once and take what is
|
|
# missing. Optional: failure here must never block the pipeline.
|
|
wanted = {name: models_dir / name
|
|
for name in ("genderage.onnx", "w600k_r50.onnx")
|
|
if not (models_dir / name).exists()}
|
|
if wanted:
|
|
try:
|
|
log.info("downloading %s from the buffalo_l bundle (~275 MB "
|
|
"one-time download)...", ", ".join(wanted))
|
|
import zipfile
|
|
|
|
tmp = models_dir / "buffalo_l.zip.part"
|
|
_fetch(BUFFALO_L_URL, tmp, "recognition models")
|
|
with zipfile.ZipFile(tmp) as zf:
|
|
for name, target in wanted.items():
|
|
member = next((n for n in zf.namelist()
|
|
if n.endswith(name)), None)
|
|
if member is None:
|
|
log.warning("%s not found in bundle", name)
|
|
continue
|
|
part = target.with_suffix(".part")
|
|
with zf.open(member) as src, open(part, "wb") as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
part.rename(target) # never leave a half-written model
|
|
log.info("%s saved", name)
|
|
tmp.unlink()
|
|
except Exception:
|
|
log.warning("buffalo_l download failed - falling back to the "
|
|
"models already present", exc_info=True)
|
|
|
|
missing = []
|
|
if not (models_dir / "face_detection_yunet_2023mar.onnx").exists():
|
|
missing.append("face_detection_yunet_2023mar.onnx")
|
|
if not any((models_dir / n).exists() for n in RECOGNIZERS):
|
|
missing.append("a recognition model (any of: %s)" % ", ".join(RECOGNIZERS))
|
|
optional_missing = [n for n in _COPY_MAP
|
|
if not (models_dir / n).exists() and n not in missing]
|
|
if optional_missing:
|
|
log.warning("optional attribute models missing (age/gender/emotion "
|
|
"will be skipped): %s", ", ".join(optional_missing))
|
|
return missing
|