Files
Behavision/behavision/model_assets.py
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
Five components that ship as one product:

- behavision/  the recognition engine. RTSP ingest, YuNet detection, IoU
               tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
               FastAPI dashboard. Identity is decided once per TRACK from an
               average of at least three embeddings, never per frame.
- agent/       the Go edge agent: supervises the engine, holds a durable
               spool, and drains it to MQTT. Nothing is acked before the
               broker confirms.
- desktop/     the shop PC application (Wails + React + tray).
- server/      the cloud API, MQTT consumer, reports and assistant.
- web/         platform.loyaly.ai, the head-office app, embedded in the
               server binary.

The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.

CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-04 11:14:18 +05:30

120 lines
5.1 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 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")
urllib.request.urlretrieve(YUNET_URL, tmp)
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"
urllib.request.urlretrieve(BUFFALO_L_URL, tmp)
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