chore: commit all pending changes

This commit is contained in:
2026-06-04 14:58:00 +08:00
parent 8c7c713fee
commit 9cde462cd1
7 changed files with 248 additions and 10 deletions

View File

@@ -104,7 +104,7 @@ RELEASE_ENV_SOURCE=deploy/managed-portal.10.8.0.12.env \
sh deploy/package-managed-portal-ota.sh sh deploy/package-managed-portal-ota.sh
``` ```
默认情况下,主 ZIP 不包含 `managed/people_flow_project/weights/`。打包脚本会额外生成一个独立的 `people-flow-weights-<RELEASE_VERSION>.tar.gz`,用于 Ubuntu 新机器首次安装已有机器升级时OTA installer 会优先复用主机上的共享权重目录,避免每次只改安装脚本或配置时都重复打包、上传大体积权重。 默认情况下,主 ZIP 不包含 `managed/people_flow_project/weights/`。打包脚本会额外生成一个独立的 `people-flow-weights-<RELEASE_VERSION>.tar.gz`,用于 Ubuntu 新机器首次安装;如果权重目录里只有 `yolo11n.pt` 缺失,还会使用小体积的 `people-flow-yolo11n-<RELEASE_VERSION>.tar.gz` 单独补齐,避免为了一个小模型重新下载 1GB+ 的完整权重包。已有机器升级时OTA installer 会优先复用主机上的共享权重目录,避免每次只改安装脚本或配置时都重复打包、上传大体积权重。
只有两种场景才建议重新发布这个独立权重包: 只有两种场景才建议重新发布这个独立权重包:
@@ -117,7 +117,7 @@ sh deploy/package-managed-portal-ota.sh
INCLUDE_WEIGHTS=1 sh deploy/package-managed-portal-ota.sh INCLUDE_WEIGHTS=1 sh deploy/package-managed-portal-ota.sh
``` ```
Ubuntu 新机器首次安装时,如果系统没有 `unzip`OTA installer 会自动用 `apt-get` 安装;然后在共享权重目录不存在时自动下载 `people-flow-weights-<RELEASE_VERSION>.tar.gz` Ubuntu 新机器首次安装时,如果系统没有 `unzip`OTA installer 会自动用 `apt-get` 安装;然后在共享权重目录不存在时自动下载 `people-flow-weights-<RELEASE_VERSION>.tar.gz`如果 DeepFace 权重已经存在但缺少 `yolo11n.pt`installer 只下载 `people-flow-yolo11n-<RELEASE_VERSION>.tar.gz`
## 模型权重 ## 模型权重

View File

@@ -5,6 +5,7 @@ RELEASE_VERSION="${RELEASE_VERSION:-20260518-7b32b21-11}"
BASE_URL="${BASE_URL:-http://10.8.0.1/ai_deploy}" BASE_URL="${BASE_URL:-http://10.8.0.1/ai_deploy}"
BUNDLE_NAME="${BUNDLE_NAME:-managed-portal-${RELEASE_VERSION}.zip}" BUNDLE_NAME="${BUNDLE_NAME:-managed-portal-${RELEASE_VERSION}.zip}"
WEIGHTS_ARCHIVE_NAME="${WEIGHTS_ARCHIVE_NAME:-people-flow-weights-${RELEASE_VERSION}.tar.gz}" WEIGHTS_ARCHIVE_NAME="${WEIGHTS_ARCHIVE_NAME:-people-flow-weights-${RELEASE_VERSION}.tar.gz}"
YOLO_ARCHIVE_NAME="${YOLO_ARCHIVE_NAME:-people-flow-yolo11n-${RELEASE_VERSION}.tar.gz}"
INSTALL_ROOT="${INSTALL_ROOT:-/opt/managed-portal-releases}" INSTALL_ROOT="${INSTALL_ROOT:-/opt/managed-portal-releases}"
TARGET_DIR="${TARGET_DIR:-${INSTALL_ROOT}/managed-portal-${RELEASE_VERSION}}" TARGET_DIR="${TARGET_DIR:-${INSTALL_ROOT}/managed-portal-${RELEASE_VERSION}}"
SHARED_ROOT="${SHARED_ROOT:-${INSTALL_ROOT}/shared}" SHARED_ROOT="${SHARED_ROOT:-${INSTALL_ROOT}/shared}"
@@ -92,6 +93,27 @@ dir_has_payload_files() {
[ -d "$directory" ] && [ -n "$(find "$directory" -type f ! -name '.gitkeep' -print -quit 2>/dev/null)" ] [ -d "$directory" ] && [ -n "$(find "$directory" -type f ! -name '.gitkeep' -print -quit 2>/dev/null)" ]
} }
people_flow_weights_complete() {
directory="$1"
people_flow_deepface_weights_complete "$directory" &&
people_flow_yolo_weight_present "$directory"
}
people_flow_deepface_weights_complete() {
directory="$1"
[ -f "$directory/deepface/age_model_weights.h5" ] &&
[ -f "$directory/deepface/gender_model_weights.h5" ] &&
[ -f "$directory/deepface/retinaface.h5" ]
}
people_flow_yolo_weight_present() {
directory="$1"
[ -f "$directory/yolo11n.pt" ]
}
copy_dir_contents() { copy_dir_contents() {
source_dir="$1" source_dir="$1"
target_dir="$2" target_dir="$2"
@@ -120,6 +142,16 @@ download_weights_archive() {
echo "$weights_archive" echo "$weights_archive"
} }
download_yolo_archive() {
tmp_dir="$1"
yolo_archive="$tmp_dir/$YOLO_ARCHIVE_NAME"
yolo_url="${BASE_URL%/}/$YOLO_ARCHIVE_NAME"
echo "downloading $yolo_url" >&2
curl -fL "$yolo_url" -o "$yolo_archive"
echo "$yolo_archive"
}
build_overlay_image() { build_overlay_image() {
overlay_name="$1" overlay_name="$1"
base_image="$2" base_image="$2"
@@ -163,7 +195,7 @@ ensure_runtime_directories() {
} }
find_existing_people_flow_weights() { find_existing_people_flow_weights() {
if dir_has_files "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then if people_flow_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then
printf '%s\n' "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" printf '%s\n' "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
return 0 return 0
fi fi
@@ -173,7 +205,32 @@ find_existing_people_flow_weights() {
continue continue
fi fi
if dir_has_files "$candidate"; then if people_flow_weights_complete "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
find_existing_people_flow_deepface_weights() {
if people_flow_deepface_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then
printf '%s\n' "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
return 0
fi
if people_flow_deepface_weights_complete "$TARGET_DIR/managed/people_flow_project/weights"; then
printf '%s\n' "$TARGET_DIR/managed/people_flow_project/weights"
return 0
fi
for candidate in "$INSTALL_ROOT"/managed-portal-*/managed/people_flow_project/weights; do
if [ "$candidate" = "$TARGET_DIR/managed/people_flow_project/weights" ]; then
continue
fi
if people_flow_deepface_weights_complete "$candidate"; then
printf '%s\n' "$candidate" printf '%s\n' "$candidate"
return 0 return 0
fi fi
@@ -190,28 +247,63 @@ extract_people_flow_weights_archive() {
mkdir -p "$TARGET_DIR/managed" mkdir -p "$TARGET_DIR/managed"
tar -xzf "$weights_archive" -C "$TARGET_DIR/managed" tar -xzf "$weights_archive" -C "$TARGET_DIR/managed"
if ! dir_has_payload_files "$bundle_weights_dir"; then if ! people_flow_weights_complete "$bundle_weights_dir"; then
echo "downloaded weights archive did not populate $bundle_weights_dir" >&2 echo "downloaded weights archive did not populate all required people-flow weights under $bundle_weights_dir" >&2
exit 1 exit 1
fi fi
printf '%s\n' "$bundle_weights_dir" printf '%s\n' "$bundle_weights_dir"
} }
ensure_people_flow_yolo_weight() {
tmp_dir="$1"
target_weights_dir="$2"
if people_flow_yolo_weight_present "$target_weights_dir"; then
return 0
fi
yolo_extract_dir="$tmp_dir/yolo"
yolo_archive="$(download_yolo_archive "$tmp_dir")"
rm -rf "$yolo_extract_dir"
mkdir -p "$yolo_extract_dir"
tar -xzf "$yolo_archive" -C "$yolo_extract_dir"
yolo_file="$yolo_extract_dir/people_flow_project/weights/yolo11n.pt"
if [ ! -f "$yolo_file" ]; then
echo "downloaded yolo archive did not populate people_flow_project/weights/yolo11n.pt" >&2
exit 1
fi
mkdir -p "$target_weights_dir"
cp "$yolo_file" "$target_weights_dir/yolo11n.pt"
}
prepare_people_flow_weights() { prepare_people_flow_weights() {
tmp_dir="$1" tmp_dir="$1"
bundle_weights_dir="$TARGET_DIR/managed/people_flow_project/weights" bundle_weights_dir="$TARGET_DIR/managed/people_flow_project/weights"
source_weights_dir="" source_weights_dir=""
deepface_weights_dir=""
if dir_has_payload_files "$bundle_weights_dir"; then if people_flow_weights_complete "$bundle_weights_dir"; then
source_weights_dir="$bundle_weights_dir" source_weights_dir="$bundle_weights_dir"
echo "seeding shared people-flow weights from bundle" >&2 echo "seeding shared people-flow weights from bundle" >&2
elif source_weights_dir="$(find_existing_people_flow_weights 2>/dev/null)"; then elif source_weights_dir="$(find_existing_people_flow_weights 2>/dev/null)"; then
echo "reusing existing people-flow weights from $source_weights_dir" >&2 echo "reusing existing people-flow weights from $source_weights_dir" >&2
elif deepface_weights_dir="$(find_existing_people_flow_deepface_weights 2>/dev/null)"; then
echo "reusing existing people-flow DeepFace weights from $deepface_weights_dir" >&2
if [ "$deepface_weights_dir" != "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" ]; then
rm -rf "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
copy_dir_contents "$deepface_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
fi
ensure_people_flow_yolo_weight "$tmp_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
source_weights_dir="$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
elif source_weights_dir="$(extract_people_flow_weights_archive "$tmp_dir" 2>/dev/null)"; then elif source_weights_dir="$(extract_people_flow_weights_archive "$tmp_dir" 2>/dev/null)"; then
echo "seeding shared people-flow weights from downloaded archive" >&2 echo "seeding shared people-flow weights from downloaded archive" >&2
else else
echo "people-flow weights not found; seed $MANAGED_PEOPLE_FLOW_WEIGHTS_DIR, publish $WEIGHTS_ARCHIVE_NAME, or include managed/people_flow_project/weights in the release zip" >&2 echo "people-flow weights not found; seed $MANAGED_PEOPLE_FLOW_WEIGHTS_DIR, publish $WEIGHTS_ARCHIVE_NAME and $YOLO_ARCHIVE_NAME, or include managed/people_flow_project/weights in the release zip" >&2
exit 1 exit 1
fi fi
@@ -220,6 +312,11 @@ prepare_people_flow_weights() {
copy_dir_contents "$source_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" copy_dir_contents "$source_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
fi fi
if ! people_flow_weights_complete "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"; then
echo "people-flow weights are incomplete under $MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" >&2
exit 1
fi
rm -rf "$bundle_weights_dir" rm -rf "$bundle_weights_dir"
mkdir -p "$(dirname "$bundle_weights_dir")" mkdir -p "$(dirname "$bundle_weights_dir")"
ln -s "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" "$bundle_weights_dir" ln -s "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" "$bundle_weights_dir"

View File

@@ -11,10 +11,13 @@ OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/dist/ota}"
STAGE_DIR="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}" STAGE_DIR="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}"
BUNDLE_PATH="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}.zip" BUNDLE_PATH="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}.zip"
INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-${RELEASE_VERSION}.sh" INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-${RELEASE_VERSION}.sh"
LATEST_INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-Latest.sh"
LOWERCASE_LATEST_INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-latest.sh"
INCLUDE_WEIGHTS="${INCLUDE_WEIGHTS:-0}" INCLUDE_WEIGHTS="${INCLUDE_WEIGHTS:-0}"
GENERATE_WEIGHTS_ARCHIVE="${GENERATE_WEIGHTS_ARCHIVE:-1}" GENERATE_WEIGHTS_ARCHIVE="${GENERATE_WEIGHTS_ARCHIVE:-1}"
PEOPLE_FLOW_WEIGHTS_SOURCE="${PEOPLE_FLOW_WEIGHTS_SOURCE:-$REPO_ROOT/managed/people_flow_project/weights}" PEOPLE_FLOW_WEIGHTS_SOURCE="${PEOPLE_FLOW_WEIGHTS_SOURCE:-$REPO_ROOT/managed/people_flow_project/weights}"
WEIGHTS_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-weights-${RELEASE_VERSION}.tar.gz" WEIGHTS_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-weights-${RELEASE_VERSION}.tar.gz"
YOLO_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-yolo11n-${RELEASE_VERSION}.tar.gz"
require_path() { require_path() {
target="$1" target="$1"
@@ -113,6 +116,9 @@ content = re.sub(
target_path.write_text(content, encoding="utf-8") target_path.write_text(content, encoding="utf-8")
target_path.chmod(0o755) target_path.chmod(0o755)
PY PY
cp "$INSTALLER_PATH" "$LATEST_INSTALLER_PATH"
cp "$INSTALLER_PATH" "$LOWERCASE_LATEST_INSTALLER_PATH"
chmod 755 "$LATEST_INSTALLER_PATH" "$LOWERCASE_LATEST_INSTALLER_PATH"
if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_WEIGHTS_SOURCE"; then if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_WEIGHTS_SOURCE"; then
python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE" "$WEIGHTS_ARCHIVE_PATH" <<'PY' python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE" "$WEIGHTS_ARCHIVE_PATH" <<'PY'
@@ -135,8 +141,26 @@ with tarfile.open(archive_path, "w:gz") as archive:
PY PY
fi fi
if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && [ -f "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" ]; then
python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" "$YOLO_ARCHIVE_PATH" <<'PY'
from pathlib import Path
import sys
import tarfile
source_path = Path(sys.argv[1])
archive_path = Path(sys.argv[2])
if archive_path.exists():
archive_path.unlink()
with tarfile.open(archive_path, "w:gz") as archive:
archive.add(source_path, arcname="people_flow_project/weights/yolo11n.pt", recursive=False)
PY
fi
echo "OTA bundle created: $BUNDLE_PATH" echo "OTA bundle created: $BUNDLE_PATH"
echo "Versioned installer created: $INSTALLER_PATH" echo "Versioned installer created: $INSTALLER_PATH"
echo "Latest installers created: $LATEST_INSTALLER_PATH and $LOWERCASE_LATEST_INSTALLER_PATH"
if [ "$INCLUDE_WEIGHTS" = "1" ]; then if [ "$INCLUDE_WEIGHTS" = "1" ]; then
echo "Bundle includes managed/people_flow_project/weights" echo "Bundle includes managed/people_flow_project/weights"
else else
@@ -147,3 +171,8 @@ if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_
else else
echo "Separate weights archive skipped; no people-flow weights payload found under $PEOPLE_FLOW_WEIGHTS_SOURCE" echo "Separate weights archive skipped; no people-flow weights payload found under $PEOPLE_FLOW_WEIGHTS_SOURCE"
fi fi
if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && [ -f "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" ]; then
echo "Separate YOLO archive created: $YOLO_ARCHIVE_PATH"
else
echo "Separate YOLO archive skipped; no yolo11n.pt found under $PEOPLE_FLOW_WEIGHTS_SOURCE"
fi

Binary file not shown.

View File

@@ -5,6 +5,12 @@ from pathlib import Path
from urllib import request from urllib import request
def _payload_for_webhook(payload: dict) -> dict:
outbound = dict(payload)
outbound.pop("tracks", None)
return outbound
def dispatch_json_event( def dispatch_json_event(
path: str | Path, path: str | Path,
payload: dict, payload: dict,
@@ -21,7 +27,7 @@ def dispatch_json_event(
req = request.Request( req = request.Request(
url=webhook_url, url=webhook_url,
data=json.dumps(payload).encode("utf-8"), data=json.dumps(_payload_for_webhook(payload)).encode("utf-8"),
method="POST", method="POST",
) )
req.add_header("Content-Type", "application/json") req.add_header("Content-Type", "application/json")

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
import json
from src.people_flow.webhook import dispatch_json_event
def test_dispatch_json_event_omits_tracks_from_webhook_but_keeps_local_log(
tmp_path, monkeypatch
):
sent: dict[str, object] = {}
class DummyResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def fake_urlopen(req, timeout):
sent["url"] = req.full_url
sent["timeout"] = timeout
sent["payload"] = json.loads(req.data.decode("utf-8"))
return DummyResponse()
monkeypatch.setattr("src.people_flow.webhook.request.urlopen", fake_urlopen)
output = tmp_path / "logs" / "events.jsonl"
payload = {
"event": "half_hour_report",
"total_people": 3,
"tracks": [
{"track_id": 1, "direction": "in"},
{"track_id": 2, "direction": "out"},
],
}
dispatch_json_event(
output,
payload,
webhook_url="https://example.test/webhook",
timeout_seconds=7.5,
)
lines = output.read_text(encoding="utf-8").splitlines()
assert json.loads(lines[0]) == payload
assert sent["url"] == "https://example.test/webhook"
assert sent["timeout"] == 7.5
assert sent["payload"] == {
"event": "half_hour_report",
"total_people": 3,
}

View File

@@ -1,6 +1,9 @@
import json import json
from datetime import datetime
from zoneinfo import ZoneInfo
from app.modules.notifier import append_json_event from app.modules.dwell_engine import DwellEngine
from app.modules.notifier import append_json_event, dispatch_json_event
def test_append_json_event_writes_jsonl(tmp_path): def test_append_json_event_writes_jsonl(tmp_path):
@@ -13,3 +16,54 @@ def test_append_json_event_writes_jsonl(tmp_path):
assert json.loads(lines[0]) == {"event": "long_stay_alert", "count": 5} assert json.loads(lines[0]) == {"event": "long_stay_alert", "count": 5}
assert json.loads(lines[1]) == {"event": "half_hour_report", "count": 3} assert json.loads(lines[1]) == {"event": "half_hour_report", "count": 3}
def test_dispatch_json_event_posts_report_without_tracks(tmp_path, monkeypatch):
tz = ZoneInfo("Asia/Shanghai")
window_start = datetime(2026, 4, 15, 11, 7, tzinfo=tz)
engine = DwellEngine(
camera_id="store_cam_01",
queue_time_threshold_seconds=300,
crowded_count_threshold=5,
normal_count_threshold=2,
pause_timeout_seconds=300,
alert_cooldown_seconds=600,
report_window_start=window_start,
)
engine.process_observations(
[{"person_id": f"cust_{idx}", "role": "customer"} for idx in range(6)],
window_start,
)
events = engine.process_observations([], window_start.replace(minute=37))
report_event = next(event for event in events if event["event"] == "half_hour_report")
sent: dict[str, object] = {}
class DummyResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def fake_urlopen(req, timeout):
sent["url"] = req.full_url
sent["timeout"] = timeout
sent["payload"] = json.loads(req.data.decode("utf-8"))
return DummyResponse()
monkeypatch.setattr("app.modules.notifier.request.urlopen", fake_urlopen)
output = tmp_path / "logs" / "events.jsonl"
dispatch_json_event(
output,
report_event,
webhook_url="https://example.test/webhook",
timeout_seconds=5.0,
)
assert sent["url"] == "https://example.test/webhook"
assert sent["timeout"] == 5.0
assert sent["payload"]["event"] == "half_hour_report"
assert "tracks" not in sent["payload"]