Three intertwined bodies of work developed and tested together this session. Full test suite green: python (edge) + phpunit (cloud). ## Edge/Pi install path - setup_online.sh: apt-lock retry (apt-daily contention), Type=exec unit + StartLimitIntervalSec in [Unit], journalctl-on-failure, crash-loop detection. - onnxruntime is now best-effort (no sdist / no 32-bit-ARM wheel); [6/8] probes the real backend and hard-fails if none works. - Hailo HEF auto-fetch: edge/tools/fetch_hailo_hef.py (pinned sha256, refuses to guess 8 vs 8L, HAILO_ARCH override for serial-less boards); hailo_pose.py run() timeout fix. - install_offline.sh: mirrors the backend gate; onnxruntime optional. ## Cafe24 cloud package (cloud/deploy/) - build_cafe24_package.sh emits move_detect_app/ (private, one folder) + www/move_detect/ (docroot subdir); no-dev classmap autoloader; preflight.php; hardened .htaccess. - Subdirectory mount support: Request::baseFrom/pathFor strip the mount prefix; index.php walks up to find the app root; views/JS prefix every URL with APP_BASE; session cookie scoped to the mount. ## Delivery audit — silent-failure & correctness (docs/delivery-audit.md) All 4 blockers + all Phase-1 silent-failure + Phase-2 HIGH correctness: - Edge cloud clients send a custom User-Agent (cafe24 WAF blocks python-requests) and allow_redirects=False. - Daemon fails loud on blank CAFE24_URL/DEVICE_SECRET; run_loop logs swallowed exceptions; heartbeat carries real fps + per-camera health; UvcSource reopens a dead camera; offline queue is size-capped and drops poison rows; TARGET_FPS/QUEUE_DB validated. - Zone coordinates normalized 0..1 on both sides (were canvas-px vs camera-px — every zone misplaced); pinch zones emit PINCH/HIGH (were downgraded to WARNING). - Fall detection uses bbox cues when keypoints are low-confidence (occluded fallen person); ghost incident not cancelled by a bystander; displacement normalized per-second (FPS-invariant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
92 lines
4 KiB
Python
92 lines
4 KiB
Python
"""TDD for HailoPoseAdapter — locks the yolov8s_pose host decode (DFL box + keypoint
|
|
decode + NMS + COCO-17 assembly + letterbox un-mapping) WITHOUT hardware/hailort,
|
|
by injecting a fake infer_fn that returns synthetic raw output tensors.
|
|
|
|
yolov8s_pose emits 9 raw tensors (no on-chip NMS): per stride (8/16/32) a box(H,W,64
|
|
DFL), score(H,W,1 already-sigmoid), and kpts(H,W,51 = 17*(x,y,score)).
|
|
"""
|
|
import inspect
|
|
import re
|
|
|
|
import numpy as np
|
|
|
|
from edge.adapters.hailo_pose import HailoPoseAdapter
|
|
|
|
INPUT = 640
|
|
STRIDES = (8, 16, 32)
|
|
|
|
|
|
def _blank_outputs():
|
|
outs = []
|
|
for s in STRIDES:
|
|
size = INPUT // s
|
|
outs.append(np.zeros((1, size, size, 64), np.float32)) # box DFL
|
|
outs.append(np.zeros((1, size, size, 1), np.float32)) # class score (sigmoid'd)
|
|
outs.append(np.zeros((1, size, size, 51), np.float32)) # keypoints
|
|
return outs
|
|
|
|
|
|
def _one_person_outputs():
|
|
outs = _blank_outputs()
|
|
# activate a single cell on the stride-8 map (size 80), grid cell (40, 40)
|
|
box, score, kpts = outs[0], outs[1], outs[2]
|
|
r = c = 40
|
|
score[0, r, c, 0] = 0.9 # person prob (already sigmoid on-chip)
|
|
# DFL: one-hot each of the 4 sides at bin index 2 -> distance = 2 cells * stride8 = 16 px
|
|
b = box[0, r, c].reshape(4, 16)
|
|
b[:, 2] = 50.0
|
|
box[0, r, c] = b.reshape(64)
|
|
# keypoints: raw x=y=0.5, raw score=3.0 for all 17 joints
|
|
k = kpts[0, r, c].reshape(17, 3)
|
|
k[:, 0] = 0.5
|
|
k[:, 1] = 0.5
|
|
k[:, 2] = 3.0
|
|
kpts[0, r, c] = k.reshape(51)
|
|
return outs
|
|
|
|
|
|
def test_infer_maps_yolov8_pose_to_coco17_person():
|
|
frame = np.zeros((640, 640, 3), np.uint8) # already 640x640 -> scale 1, no padding
|
|
outs = _one_person_outputs()
|
|
a = HailoPoseAdapter("unused.hef", infer_fn=lambda f: outs)
|
|
persons = a.infer(frame)
|
|
assert len(persons) == 1
|
|
p = persons[0]
|
|
assert len(p.keypoints) == 17
|
|
# grid center = (40 + 0.5) * 8 = 324 ; box +/- 16 -> [308, 340]
|
|
assert abs(p.bbox.x1 - 308) < 2 and abs(p.bbox.y1 - 308) < 2
|
|
assert abs(p.bbox.x2 - 340) < 2 and abs(p.bbox.y2 - 340) < 2
|
|
# keypoint decode: stride*(2*0.5 - 0.5) + 324 = 8*0.5 + 324 = 328
|
|
assert abs(p.keypoints[0].x - 328) < 2 and abs(p.keypoints[0].y - 328) < 2
|
|
# joint score sigmoided: sigmoid(3.0) ~= 0.9526
|
|
assert abs(p.keypoints[0].conf - 0.9526) < 0.01
|
|
|
|
|
|
def test_no_detection_returns_empty():
|
|
a = HailoPoseAdapter("unused.hef", infer_fn=lambda f: _blank_outputs())
|
|
assert a.infer(np.zeros((640, 640, 3), np.uint8)) == []
|
|
|
|
|
|
def test_hailort_run_is_called_positionally_not_with_timeout_ms():
|
|
"""HailoRT's ConfiguredInferModel.run() takes `timeout`, not `timeout_ms`.
|
|
|
|
Verified verbatim against upstream pyhailort.py at v4.19.0, v4.20.0, v4.23.0 and master —
|
|
all four are `def run(self, bindings, timeout):`. (The neighbouring
|
|
`wait_for_async_ready(self, timeout_ms=1000, ...)` DOES use `timeout_ms`, which is the
|
|
trap.) So `run(..., timeout_ms=...)` is a TypeError on EVERY frame.
|
|
|
|
That failure is invisible without this test: `_build_hailo_infer` is device-only
|
|
(`# pragma: no cover`), so nothing executes it here, and on the Pi `run_loop` catches
|
|
Exception per frame and only bumps an error counter — the unit stays green and emits
|
|
zero events, forever. Hence a source-level assertion rather than a behavioural one.
|
|
"""
|
|
src = inspect.getsource(HailoPoseAdapter._build_hailo_infer)
|
|
# Inspect the CALL, not the whole source: the code comment beside it necessarily spells
|
|
# out the wrong form, and a naive `"timeout_ms=" not in src` matches that comment.
|
|
call = re.search(r"configured\.run\((.*?)\)\n", src)
|
|
assert call, "configured.run(...) not found in _build_hailo_infer"
|
|
args = call.group(1).strip()
|
|
assert "timeout_ms" not in args, \
|
|
f"ConfiguredInferModel.run() has no timeout_ms kwarg; pass it positionally. Got: run({args})"
|
|
assert re.fullmatch(r"\[bindings\],\s*\d+", args), \
|
|
f"expected run([bindings], <ms int>) — positional timeout. Got: run({args})"
|