move_detect/edge/tools/fetch_movenet.py
유창욱 6b6f29c447 feat: 납품 잔여 3건 해소 (Pi 번들 · MoveNet 폴백 · 자격증명 회전)
Pi 번들 — arm64 빌드 머신이 필요하다고 적어 둔 것은 과한 가정이었다.
--cross 모드에서 pip 이 태그로 aarch64/cp311 휠을 받으므로 네트워크만
있으면 된다. 휠 11개 전부 aarch64. "Pi 번들 미빌드"로 스킵되던 테스트가
이제 실제로 돈다.

MoveNet — P3-3 의 잔여였던 죽은 원본을 유효한 tf2onnx 변환본으로 교체하고
sha256+size 로 고정했다. 고정 전에 어댑터 계약과 대조했다: 입력
[1,192,192,3] int32 NHWC, 출력 [1,1,17,3]. 레이아웃이 다른 변환본은
onnxruntime 에 그냥 로드되고 좌표만 틀리는 실패라, 파일이 바뀌었는지가
아니라 계약이 맞는지를 테스트로 박았다.

자격증명 회전 — 손으로 적던 SQL 절차를 명령 하나로 바꿨다. DB 에 접속하지
않고 붙여넣을 SQL 만 낸다(운영자의 창구가 phpMyAdmin 이므로 접속 정보 없이
어느 머신에서나 돌아야 한다). 비밀번호는 STDIN 으로만 받는다 — 명령행은
ps 와 셸 히스토리에 남는다. 테스트가 잠그는 것은 서버 키와 Pi 키의 일치,
그리고 앱이 그 해시를 검증하는지 두 가지다. 회전 실행 자체는 운영 계정
조작이라 저장소에서 수행하지 않는다.

검증: phpunit 276 passed(스킵 0, 신규 7) · pytest 스킵 2(환경)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBjuR7DW6PNNMows4KnbYU
2026-09-21 10:47:16 +09:00

87 lines
3.9 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).
SOURCE: the historical PINTO_model_zoo raw URL is DEAD (that repo hosts its models on external
storage, not as repo blobs — the raw path returns a GitHub HTML 404). The pinned source below is
a tf2onnx export of Google's MoveNet SinglePose Lightning v4, checked to match what
edge/adapters/movenet_pose.py feeds and reads: input [1,192,192,3] int32 NHWC, output
[1,1,17,3] float (y, x, score). A conversion with a different layout would load fine and then
produce silently wrong keypoints, so the digest below is what makes this safe to fetch — not the
hostname. For the Hailo delivery path this model is optional (POSE_BACKEND=hailo).
"""
import hashlib
import os
import sys
import tempfile
import urllib.request
URL = os.environ.get(
"MOVENET_ONNX_URL",
"https://huggingface.co/Xenova/movenet-singlepose-lightning/resolve/main/onnx/model.onnx",
)
DEST = os.path.join(os.path.dirname(__file__), "..", "..", "models", "movenet_lightning.onnx")
# Pinned from the bytes of the source above. An override must bring its own digest — pointing
# MOVENET_ONNX_URL elsewhere while keeping this pin just makes the download fail, which is the
# intended direction of failure.
PINNED_SHA256 = os.environ.get(
"MOVENET_ONNX_SHA256", "1ad4f8d6c2f776a9967db3993c9ca740bc350104f9d37c151dc183fc29a464ad")
PINNED_SIZE = int(os.environ.get("MOVENET_ONNX_SIZE", "9413268") 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()