chore: commit all pending changes
This commit is contained in:
@@ -104,7 +104,7 @@ RELEASE_ENV_SOURCE=deploy/managed-portal.10.8.0.12.env \
|
||||
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
|
||||
```
|
||||
|
||||
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`。
|
||||
|
||||
## 模型权重
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ RELEASE_VERSION="${RELEASE_VERSION:-20260518-7b32b21-11}"
|
||||
BASE_URL="${BASE_URL:-http://10.8.0.1/ai_deploy}"
|
||||
BUNDLE_NAME="${BUNDLE_NAME:-managed-portal-${RELEASE_VERSION}.zip}"
|
||||
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}"
|
||||
TARGET_DIR="${TARGET_DIR:-${INSTALL_ROOT}/managed-portal-${RELEASE_VERSION}}"
|
||||
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)" ]
|
||||
}
|
||||
|
||||
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() {
|
||||
source_dir="$1"
|
||||
target_dir="$2"
|
||||
@@ -120,6 +142,16 @@ download_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() {
|
||||
overlay_name="$1"
|
||||
base_image="$2"
|
||||
@@ -163,7 +195,7 @@ ensure_runtime_directories() {
|
||||
}
|
||||
|
||||
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"
|
||||
return 0
|
||||
fi
|
||||
@@ -173,7 +205,32 @@ find_existing_people_flow_weights() {
|
||||
continue
|
||||
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"
|
||||
return 0
|
||||
fi
|
||||
@@ -190,28 +247,63 @@ extract_people_flow_weights_archive() {
|
||||
mkdir -p "$TARGET_DIR/managed"
|
||||
tar -xzf "$weights_archive" -C "$TARGET_DIR/managed"
|
||||
|
||||
if ! dir_has_payload_files "$bundle_weights_dir"; then
|
||||
echo "downloaded weights archive did not populate $bundle_weights_dir" >&2
|
||||
if ! people_flow_weights_complete "$bundle_weights_dir"; then
|
||||
echo "downloaded weights archive did not populate all required people-flow weights under $bundle_weights_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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() {
|
||||
tmp_dir="$1"
|
||||
bundle_weights_dir="$TARGET_DIR/managed/people_flow_project/weights"
|
||||
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"
|
||||
echo "seeding shared people-flow weights from bundle" >&2
|
||||
elif source_weights_dir="$(find_existing_people_flow_weights 2>/dev/null)"; then
|
||||
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
|
||||
echo "seeding shared people-flow weights from downloaded archive" >&2
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -220,6 +312,11 @@ prepare_people_flow_weights() {
|
||||
copy_dir_contents "$source_weights_dir" "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR"
|
||||
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"
|
||||
mkdir -p "$(dirname "$bundle_weights_dir")"
|
||||
ln -s "$MANAGED_PEOPLE_FLOW_WEIGHTS_DIR" "$bundle_weights_dir"
|
||||
|
||||
@@ -11,10 +11,13 @@ OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/dist/ota}"
|
||||
STAGE_DIR="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}"
|
||||
BUNDLE_PATH="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}.zip"
|
||||
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}"
|
||||
GENERATE_WEIGHTS_ARCHIVE="${GENERATE_WEIGHTS_ARCHIVE:-1}"
|
||||
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"
|
||||
YOLO_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-yolo11n-${RELEASE_VERSION}.tar.gz"
|
||||
|
||||
require_path() {
|
||||
target="$1"
|
||||
@@ -113,6 +116,9 @@ content = re.sub(
|
||||
target_path.write_text(content, encoding="utf-8")
|
||||
target_path.chmod(0o755)
|
||||
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
|
||||
python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE" "$WEIGHTS_ARCHIVE_PATH" <<'PY'
|
||||
@@ -135,8 +141,26 @@ with tarfile.open(archive_path, "w:gz") as archive:
|
||||
PY
|
||||
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 "Versioned installer created: $INSTALLER_PATH"
|
||||
echo "Latest installers created: $LATEST_INSTALLER_PATH and $LOWERCASE_LATEST_INSTALLER_PATH"
|
||||
if [ "$INCLUDE_WEIGHTS" = "1" ]; then
|
||||
echo "Bundle includes managed/people_flow_project/weights"
|
||||
else
|
||||
@@ -147,3 +171,8 @@ if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_
|
||||
else
|
||||
echo "Separate weights archive skipped; no people-flow weights payload found under $PEOPLE_FLOW_WEIGHTS_SOURCE"
|
||||
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
|
||||
|
||||
BIN
docs/《AI门店人流识别系统技术方案》.pdf
Normal file
BIN
docs/《AI门店人流识别系统技术方案》.pdf
Normal file
Binary file not shown.
@@ -5,6 +5,12 @@ from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
|
||||
def _payload_for_webhook(payload: dict) -> dict:
|
||||
outbound = dict(payload)
|
||||
outbound.pop("tracks", None)
|
||||
return outbound
|
||||
|
||||
|
||||
def dispatch_json_event(
|
||||
path: str | Path,
|
||||
payload: dict,
|
||||
@@ -21,7 +27,7 @@ def dispatch_json_event(
|
||||
|
||||
req = request.Request(
|
||||
url=webhook_url,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
data=json.dumps(_payload_for_webhook(payload)).encode("utf-8"),
|
||||
method="POST",
|
||||
)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
52
managed/people_flow_project/tests/test_webhook.py
Normal file
52
managed/people_flow_project/tests/test_webhook.py
Normal 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,
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
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):
|
||||
@@ -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[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"]
|
||||
|
||||
Reference in New Issue
Block a user