move_detect/edge/tools/fetch_movenet.py
유창욱 7a86545367 feat(delivery-audit): privacy — face-blur sizing + model integrity (P3-1, P3-3)
- P3-1 [H] _head_boxes sizes the anonymization region from the person bbox (scales with
  distance) + bbox-top fallback when the nose keypoint is low-confidence; a fixed ~40px
  patch had left close faces identifiable. test_faceblur.py.
- P3-3 [H] fetch_movenet.py rewritten to stage-validate (reject HTML/LFS-pointer, optional
  pinned sha256+size, fail loud) per the SHA256 spec requirement. Discovery: the old PINTO
  raw URL is DEAD (GitHub 404 → it was silently saving an HTML page as the .onnx). Non-blocking
  for the Hailo delivery path; the CPU MoveNet fallback needs a valid MOVENET_ONNX_URL pinned.

python + phpunit suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:35:02 +09:00

87 lines
3.8 KiB
Python

"""빌드/온라인 머신 전용: MoveNet SinglePose Lightning ONNX 다운로드(폐쇄망 번들). Apache-2.0.
Integrity is enforced (spec: SHA256/GPG verify of every vendored model). The download is staged
to a temp file, validated, and only then moved into place — a truncated / HTML-error-page /
tampered download must never be saved as the model (onnxruntime would then fail to load it, and
the operator would see a cryptic runtime error instead of a clear fetch failure).
STATUS: the historical PINTO_model_zoo raw URL below is DEAD (that repo hosts its models on
external storage, not as repo blobs — the raw path now returns a GitHub HTML 404). So there is
no known-good sha256 to pin here yet. Until a working source is set:
* this tool FAILS LOUD if the download is not a real ONNX (rejects the HTML it currently gets),
instead of silently saving garbage;
* set MOVENET_ONNX_URL to a valid source and pin PINNED_SHA256/PINNED_SIZE from its bytes.
For the Hailo delivery path this model is optional (POSE_BACKEND=hailo), so a missing MoveNet is
non-blocking; the CPU fallback needs this resolved.
"""
import hashlib
import os
import sys
import tempfile
import urllib.request
URL = os.environ.get(
"MOVENET_ONNX_URL",
"https://github.com/PINTO0309/PINTO_model_zoo/raw/main/115_MoveNet/resources/"
"movenet_singlepose_lightning_4.onnx",
)
DEST = os.path.join(os.path.dirname(__file__), "..", "..", "models", "movenet_lightning.onnx")
# Pin from the bytes of a known-good source once MOVENET_ONNX_URL points at one. Empty = unpinned
# (still validated as a real ONNX below, but not byte-identical-verified).
PINNED_SHA256 = os.environ.get("MOVENET_ONNX_SHA256", "")
PINNED_SIZE = int(os.environ.get("MOVENET_ONNX_SIZE", "0") or "0")
def _looks_like_onnx(head: bytes) -> bool:
# ONNX is a protobuf ModelProto; it is NOT text. Reject an HTML error page / LFS pointer
# outright (the current dead URL returns exactly that).
stripped = head.lstrip()
if stripped[:1] in (b"<", b"{") or stripped[:5].lower() == b"<!doc":
return False
if b"version https://git-lfs" in head:
return False
return True
def main():
dest = os.path.abspath(DEST)
os.makedirs(os.path.dirname(dest), exist_ok=True)
print(f"downloading MoveNet ONNX\n {URL}\n -> {dest}")
tmp = None
try:
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(dest), prefix=".movenet.", suffix=".part")
os.close(fd)
urllib.request.urlretrieve(URL, tmp)
with open(tmp, "rb") as f:
head = f.read(64)
if not _looks_like_onnx(head):
raise SystemExit(
f"ERROR: {URL}\n did not return an ONNX model (looks like HTML / an error page /"
f" an LFS pointer).\n Set MOVENET_ONNX_URL to a valid source and pin"
f" MOVENET_ONNX_SHA256. (Hailo path does not need this model.)"
)
size = os.path.getsize(tmp)
digest = hashlib.sha256(open(tmp, "rb").read()).hexdigest()
if PINNED_SHA256 and (digest != PINNED_SHA256 or (PINNED_SIZE and size != PINNED_SIZE)):
raise SystemExit(
f"ERROR: MoveNet ONNX does not match the pinned digest.\n"
f" expected: {PINNED_SIZE} bytes, sha256 {PINNED_SHA256}\n"
f" got : {size} bytes, sha256 {digest}\n Refusing to install it."
)
if not PINNED_SHA256:
print(f" WARNING: integrity not pinned (MOVENET_ONNX_SHA256 unset). Got {size} bytes,"
f" sha256 {digest}. Pin this once the source is trusted.", file=sys.stderr)
os.replace(tmp, dest)
tmp = None
print(f"done: {dest} ({size} bytes)")
finally:
if tmp and os.path.exists(tmp):
os.remove(tmp)
if __name__ == "__main__":
main()