Files
Behavision/tests/test_capture.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

80 lines
2.3 KiB
Python

import numpy as np
from behavision.capture import VideoSource
def _source_with_frame(ts=100.0):
"""VideoSource opens nothing until run(), so the frame slot can be
populated directly — no camera needed."""
src = VideoSource("cam1", 0)
src._frame = np.zeros((4, 4, 3), dtype=np.uint8)
src._frame_ts = ts
return src
def test_latest_since_skips_an_already_seen_frame():
src = _source_with_frame(ts=100.0)
frame, ts = src.latest_since(100.0)
assert frame is None
assert ts == 100.0
def test_latest_since_returns_a_newer_frame():
src = _source_with_frame(ts=100.0)
frame, ts = src.latest_since(99.0)
assert frame is not None
assert ts == 100.0
def test_latest_since_returns_a_copy():
src = _source_with_frame()
frame, _ = src.latest_since(0.0)
frame[0, 0, 0] = 255
assert src._frame[0, 0, 0] == 0
def test_latest_since_with_no_frame_yet():
frame, ts = VideoSource("cam1", 0).latest_since(0.0)
assert frame is None
assert ts == 0.0
def test_first_connect_is_not_counted_as_a_reconnect():
assert VideoSource("cam1", 0).stats()["reconnects"] == 0
def _elapsed(fn):
import time
t = time.perf_counter()
result = fn()
return result, time.perf_counter() - t
def test_probe_fails_fast_on_an_unroutable_host():
"""The Test button must answer, not hang. cv2.VideoCapture blocks in its
constructor on TCP connect (~75s OS timeout), so a pre-flight check is the
only thing that bounds this."""
from behavision.capture import probe_source
result, seconds = _elapsed(lambda: probe_source(
"rtsp://10.255.255.1:554/x", connect_timeout=1.0))
assert result["ok"] is False
assert "10.255.255.1:554" in result["error"]
assert seconds < 5, f"took {seconds:.1f}s - the pre-flight is not working"
def test_probe_reports_connection_refused_immediately():
from behavision.capture import probe_source
result, seconds = _elapsed(lambda: probe_source("rtsp://127.0.0.1:1/x"))
assert result["ok"] is False
assert seconds < 5
def test_tcp_precheck_passes_through_non_url_sources():
"""A webcam index has no host to check; it must not be blocked."""
from behavision.capture import _tcp_reachable
assert _tcp_reachable(0, 1.0) == (True, "")
assert _tcp_reachable("not-a-url", 1.0)[0] is True