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
|
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`。
|
||||||
|
|
||||||
## 模型权重
|
## 模型权重
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
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")
|
||||||
|
|||||||
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
|
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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user