feat: initialize managed portal

This commit is contained in:
Yoilun
2026-04-27 10:04:36 +08:00
commit d4e351df71
145 changed files with 13425 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="${PROJECT_DIR:-$HOME/store_dwell_alert}"
PYTHON_BIN="${PYTHON_BIN:-python3}"
sudo apt-get update
sudo apt-get install -y ffmpeg python3-venv
"$PYTHON_BIN" -m venv "$PROJECT_DIR/.venv"
"$PROJECT_DIR/.venv/bin/pip" install --upgrade pip
"$PROJECT_DIR/.venv/bin/pip" install --no-cache-dir --index-url https://download.pytorch.org/whl/cu124 torch torchvision
"$PROJECT_DIR/.venv/bin/pip" install -r "$PROJECT_DIR/requirements.txt"
echo "Bootstrap complete for $PROJECT_DIR"

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env sh
set -eu
PROJECT_DIR="/app"
CONFIG_TEMPLATE="${PROJECT_DIR}/config/config.example.yaml"
CONFIG_PATH="${CONFIG_PATH:-${PROJECT_DIR}/config/local.yaml}"
LOG_DIR="${PROJECT_DIR}/logs"
CAMERA_ID="${CAMERA_ID:-store_cam_01}"
RTSP_URL="${RTSP_URL:-}"
EVENT_SINK_PATH="${EVENT_SINK_PATH:-logs/events.jsonl}"
API_HOST="${API_HOST:-0.0.0.0}"
API_PORT="${API_PORT:-18081}"
mkdir -p "${LOG_DIR}" "$(dirname "${CONFIG_PATH}")"
if [ ! -f "${CONFIG_PATH}" ]; then
cp "${CONFIG_TEMPLATE}" "${CONFIG_PATH}"
fi
python - "$CONFIG_PATH" "$CAMERA_ID" "$RTSP_URL" "$EVENT_SINK_PATH" <<'PY'
from pathlib import Path
import sys
import yaml
config_path = Path(sys.argv[1])
camera_id = sys.argv[2]
rtsp_url = sys.argv[3]
event_sink_path = sys.argv[4]
raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
raw["camera_id"] = camera_id
stream = raw.setdefault("stream", {})
if rtsp_url:
stream["rtsp_url"] = rtsp_url
event_sink = raw.setdefault("event_sink", {})
event_sink["path"] = event_sink_path
config_path.write_text(
yaml.safe_dump(raw, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
PY
exec python -m app.manage_api --config "${CONFIG_PATH}" --host "${API_HOST}" --port "${API_PORT}"

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_BIN="${PYTHON_BIN:-python3.12}"
VENV_DIR="${PROJECT_DIR}/.venv"
WHEELHOUSE_DIR="${PROJECT_DIR}/wheelhouse"
LOCK_FILE="${PROJECT_DIR}/requirements.lock.txt"
WEIGHTS_FILE="${PROJECT_DIR}/weights/yolo11n.pt"
RUN_SCRIPT="${PROJECT_DIR}/scripts/run.sh"
INSTALL_SERVICE_SCRIPT="${PROJECT_DIR}/scripts/install_service.sh"
PROJECT_USER="${SUDO_USER:-$(id -un)}"
run_privileged() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
return
fi
sudo "$@"
}
run_project_user() {
if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" ]]; then
sudo -u "${PROJECT_USER}" -H "$@"
return
fi
"$@"
}
ensure_system_package() {
local command_name="$1"
local package_name="$2"
if command -v "${command_name}" >/dev/null 2>&1; then
return
fi
echo "Installing missing package: ${package_name}"
run_privileged apt-get -o Acquire::ForceIPv4=true update
run_privileged apt-get -o Acquire::ForceIPv4=true install -y "${package_name}"
}
require_file() {
local target="$1"
if [[ ! -e "${target}" ]]; then
echo "Missing required file: ${target}" >&2
exit 1
fi
}
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
echo "Python interpreter not found: ${PYTHON_BIN}" >&2
echo "Set PYTHON_BIN=/path/to/python3.12 if needed." >&2
exit 1
fi
ensure_system_package ffmpeg ffmpeg
if [[ ! -d "/usr/lib/python3.12/venv" && ! -d "/usr/lib/python3.12/ensurepip" ]]; then
echo "Installing missing package: python3.12-venv"
run_privileged apt-get -o Acquire::ForceIPv4=true update
run_privileged apt-get -o Acquire::ForceIPv4=true install -y python3.12-venv
fi
if ! command -v nvidia-smi >/dev/null 2>&1; then
echo "nvidia-smi is required but not installed." >&2
exit 1
fi
require_file "${LOCK_FILE}"
require_file "${WEIGHTS_FILE}"
require_file "${RUN_SCRIPT}"
require_file "${INSTALL_SERVICE_SCRIPT}"
if [[ ! -d "${WHEELHOUSE_DIR}" ]]; then
echo "Missing wheelhouse directory: ${WHEELHOUSE_DIR}" >&2
exit 1
fi
run_project_user "${PYTHON_BIN}" -m venv "${VENV_DIR}"
run_project_user "${VENV_DIR}/bin/python" -m ensurepip --upgrade
while IFS= read -r requirement; do
if [[ -z "${requirement}" ]]; then
continue
fi
run_project_user "${VENV_DIR}/bin/python" -m pip install \
--no-index \
--no-deps \
--find-links "${WHEELHOUSE_DIR}" \
"${requirement}"
done < "${LOCK_FILE}"
mkdir -p \
"${PROJECT_DIR}/config" \
"${PROJECT_DIR}/data/runtime" \
"${PROJECT_DIR}/data/staff_gallery" \
"${PROJECT_DIR}/logs"
run_project_user bash "${RUN_SCRIPT}" --prepare-only
bash "${INSTALL_SERVICE_SCRIPT}"
run_privileged systemctl enable --now store-dwell-alert.service
cat <<EOF
Offline install complete.
Service started and enabled on boot: store-dwell-alert.service
Runtime log: ${PROJECT_DIR}/logs/runtime.log
Event sink: ${PROJECT_DIR}/logs/events.jsonl
EOF

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE_PATH="${PROJECT_DIR}/deploy/store-dwell-alert.service.tpl"
CONFIG_PATH="${CONFIG_PATH:-${PROJECT_DIR}/config/local.yaml}"
SERVICE_NAME="${SERVICE_NAME:-store-dwell-alert.service}"
OUTPUT_PATH="${PROJECT_DIR}/deploy/${SERVICE_NAME}"
RUN_USER="${RUN_USER:-${SUDO_USER:-$(id -un)}}"
RUN_GROUP="${RUN_GROUP:-$(id -gn "${RUN_USER}")}"
if [[ ! -f "${TEMPLATE_PATH}" ]]; then
echo "Missing service template: ${TEMPLATE_PATH}" >&2
exit 1
fi
if [[ ! -f "${CONFIG_PATH}" ]]; then
echo "Missing config file: ${CONFIG_PATH}" >&2
exit 1
fi
sed \
-e "s|__PROJECT_DIR__|${PROJECT_DIR}|g" \
-e "s|__CONFIG_PATH__|${CONFIG_PATH}|g" \
-e "s|__RUN_USER__|${RUN_USER}|g" \
-e "s|__RUN_GROUP__|${RUN_GROUP}|g" \
"${TEMPLATE_PATH}" > "${OUTPUT_PATH}"
sudo cp "${OUTPUT_PATH}" "/etc/systemd/system/${SERVICE_NAME}"
sudo systemctl daemon-reload
echo "Service installed to /etc/systemd/system/${SERVICE_NAME}"
echo "Enable and start it with: sudo systemctl enable --now ${SERVICE_NAME}"

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUNDLE_NAME="${BUNDLE_NAME:-store_dwell_alert_bundle}"
BUILD_DATE="${BUILD_DATE:-$(date +%F)}"
DIST_DIR="${PROJECT_DIR}/dist"
STAGE_DIR="${DIST_DIR}/${BUNDLE_NAME}"
ARCHIVE_PATH="${DIST_DIR}/${BUNDLE_NAME}_${BUILD_DATE}.tar.gz"
WHEELHOUSE_SOURCE="${WHEELHOUSE_SOURCE:-${PROJECT_DIR}/wheelhouse}"
WEIGHTS_SOURCE="${WEIGHTS_SOURCE:-${PROJECT_DIR}/weights/yolo11n.pt}"
require_path() {
local target="$1"
if [[ ! -e "${target}" ]]; then
echo "Missing required path: ${target}" >&2
exit 1
fi
}
require_path "${PROJECT_DIR}/app"
require_path "${PROJECT_DIR}/config/config.example.yaml"
require_path "${PROJECT_DIR}/deploy/store-dwell-alert.service.tpl"
require_path "${PROJECT_DIR}/requirements.txt"
require_path "${PROJECT_DIR}/requirements.lock.txt"
require_path "${PROJECT_DIR}/README.md"
require_path "${PROJECT_DIR}/README_zh.md"
require_path "${PROJECT_DIR}/scripts/install.sh"
require_path "${PROJECT_DIR}/scripts/run.sh"
require_path "${PROJECT_DIR}/scripts/install_service.sh"
require_path "${WHEELHOUSE_SOURCE}"
require_path "${WEIGHTS_SOURCE}"
rm -rf "${STAGE_DIR}"
mkdir -p \
"${STAGE_DIR}/config" \
"${STAGE_DIR}/data/runtime" \
"${STAGE_DIR}/data/staff_gallery" \
"${STAGE_DIR}/deploy" \
"${STAGE_DIR}/logs" \
"${STAGE_DIR}/scripts" \
"${STAGE_DIR}/weights"
cp -R "${PROJECT_DIR}/app" "${STAGE_DIR}/app"
cp "${PROJECT_DIR}/README.md" "${STAGE_DIR}/README.md"
cp "${PROJECT_DIR}/README_zh.md" "${STAGE_DIR}/README_zh.md"
cp "${PROJECT_DIR}/requirements.txt" "${STAGE_DIR}/requirements.txt"
cp "${PROJECT_DIR}/requirements.lock.txt" "${STAGE_DIR}/requirements.lock.txt"
cp "${PROJECT_DIR}/config/config.example.yaml" "${STAGE_DIR}/config/config.example.yaml"
cp "${PROJECT_DIR}/deploy/store-dwell-alert.service.tpl" "${STAGE_DIR}/deploy/store-dwell-alert.service.tpl"
cp "${PROJECT_DIR}/scripts/install.sh" "${STAGE_DIR}/scripts/install.sh"
cp "${PROJECT_DIR}/scripts/run.sh" "${STAGE_DIR}/scripts/run.sh"
cp "${PROJECT_DIR}/scripts/install_service.sh" "${STAGE_DIR}/scripts/install_service.sh"
cp "${WEIGHTS_SOURCE}" "${STAGE_DIR}/weights/yolo11n.pt"
cp -R "${WHEELHOUSE_SOURCE}" "${STAGE_DIR}/wheelhouse"
chmod +x \
"${STAGE_DIR}/scripts/install.sh" \
"${STAGE_DIR}/scripts/run.sh" \
"${STAGE_DIR}/scripts/install_service.sh"
rm -f "${ARCHIVE_PATH}"
tar -czf "${ARCHIVE_PATH}" -C "${DIST_DIR}" "${BUNDLE_NAME}"
echo "Bundle created: ${ARCHIVE_PATH}"

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENV_PYTHON="${PROJECT_DIR}/.venv/bin/python"
CONFIG_TEMPLATE="${PROJECT_DIR}/config/config.example.yaml"
CONFIG_PATH="${PROJECT_DIR}/config/local.yaml"
LOG_DIR="${PROJECT_DIR}/logs"
CAMERA_ID="${CAMERA_ID:-store_cam_01}"
RTSP_URL="${RTSP_URL:-rtsp://user:password@camera-ip:554/h264/ch1/main/av_stream}"
EVENT_SINK_PATH="${EVENT_SINK_PATH:-logs/events.jsonl}"
PREPARE_ONLY=0
if [[ "${1:-}" == "--prepare-only" ]]; then
PREPARE_ONLY=1
shift
fi
if [[ ! -x "${VENV_PYTHON}" ]]; then
echo "Virtual environment is missing. Run scripts/install.sh first." >&2
exit 1
fi
if [[ "${RTSP_URL}" == "rtsp://user:password@camera-ip:554/h264/ch1/main/av_stream" ]]; then
echo "Please edit scripts/run.sh and set RTSP_URL before starting." >&2
exit 1
fi
mkdir -p "${LOG_DIR}"
cp "${CONFIG_TEMPLATE}" "${CONFIG_PATH}"
sed -i.bak \
-e "s|^camera_id: .*|camera_id: ${CAMERA_ID}|" \
-e "s|^ rtsp_url: .*| rtsp_url: ${RTSP_URL}|" \
-e "s|^ path: .*| path: ${EVENT_SINK_PATH}|" \
"${CONFIG_PATH}"
rm -f "${CONFIG_PATH}.bak"
if [[ "${PREPARE_ONLY}" -eq 1 ]]; then
echo "Prepared config at ${CONFIG_PATH}"
exit 0
fi
exec "${VENV_PYTHON}" -m app.main --config "${CONFIG_PATH}" "$@"

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
"$PROJECT_DIR/.venv/bin/python" -m app.main "$@"