mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 07:06:32 +00:00
Refactor inference engine and task management: Remove obsolete inference engine and ONNX engine files, update inference processing to utilize batch handling, and enhance task management structure in documentation. Adjust paths for task specifications to align with new directory organization.
This commit is contained in:
+31
-9
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
@@ -35,7 +36,7 @@ class _SessionWithBase(requests.Session):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def base_url():
|
||||
return "http://detections:8080"
|
||||
return os.environ.get("BASE_URL", "http://detections:8080")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -56,12 +57,12 @@ def sse_client_factory(http_client):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def mock_loader_url():
|
||||
return "http://mock-loader:8080"
|
||||
return os.environ.get("MOCK_LOADER_URL", "http://mock-loader:8080")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def mock_annotations_url():
|
||||
return "http://mock-annotations:8081"
|
||||
return os.environ.get("MOCK_ANNOTATIONS_URL", "http://mock-annotations:8081")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@@ -96,13 +97,22 @@ def reset_mocks(mock_loader_url, mock_annotations_url):
|
||||
yield
|
||||
|
||||
|
||||
def _media_dir() -> Path:
|
||||
return Path(os.environ.get("MEDIA_DIR", "/media"))
|
||||
|
||||
|
||||
def _read_media(name: str) -> bytes:
|
||||
p = Path("/media") / name
|
||||
p = _media_dir() / name
|
||||
if not p.is_file():
|
||||
pytest.skip(f"missing {p}")
|
||||
return p.read_bytes()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def media_dir():
|
||||
return str(_media_dir())
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def image_small():
|
||||
return _read_media("image_small.jpg")
|
||||
@@ -135,17 +145,17 @@ def image_empty_scene():
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def video_short_path():
|
||||
return "/media/video_short01.mp4"
|
||||
return str(_media_dir() / "video_short01.mp4")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def video_short_02_path():
|
||||
return "/media/video_short02.mp4"
|
||||
return str(_media_dir() / "video_short02.mp4")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def video_long_path():
|
||||
return "/media/video_long03.mp4"
|
||||
return str(_media_dir() / "video_long03.mp4")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -179,12 +189,24 @@ def jwt_token():
|
||||
def warm_engine(http_client, image_small):
|
||||
deadline = time.time() + 120
|
||||
files = {"file": ("warm.jpg", image_small, "image/jpeg")}
|
||||
consecutive_errors = 0
|
||||
last_status = None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
r = http_client.post("/detect", files=files)
|
||||
if r.status_code == 200:
|
||||
return
|
||||
last_status = r.status_code
|
||||
if r.status_code >= 500:
|
||||
consecutive_errors += 1
|
||||
if consecutive_errors >= 5:
|
||||
pytest.fail(
|
||||
f"engine warm-up aborted: {consecutive_errors} consecutive "
|
||||
f"HTTP {last_status} errors — server is broken, not starting up"
|
||||
)
|
||||
else:
|
||||
consecutive_errors = 0
|
||||
except OSError:
|
||||
pass
|
||||
consecutive_errors = 0
|
||||
time.sleep(2)
|
||||
pytest.fail("engine warm-up failed after 120s")
|
||||
pytest.fail(f"engine warm-up timed out after 120s (last status: {last_status})")
|
||||
|
||||
@@ -27,7 +27,10 @@ services:
|
||||
ANNOTATIONS_URL: http://mock-annotations:8081
|
||||
volumes:
|
||||
- ./fixtures/classes.json:/app/classes.json
|
||||
- ./fixtures:/media
|
||||
- ./logs:/app/Logs
|
||||
shm_size: 512m
|
||||
mem_limit: 4g
|
||||
networks:
|
||||
- e2e-net
|
||||
|
||||
@@ -46,6 +49,7 @@ services:
|
||||
ANNOTATIONS_URL: http://mock-annotations:8081
|
||||
volumes:
|
||||
- ./fixtures/classes.json:/app/classes.json
|
||||
- ./fixtures:/media
|
||||
- ./logs:/app/Logs
|
||||
networks:
|
||||
e2e-net:
|
||||
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
FIXTURES_DIR="$SCRIPT_DIR/fixtures"
|
||||
LOGS_DIR="$SCRIPT_DIR/logs"
|
||||
RESULTS_DIR="$SCRIPT_DIR/results"
|
||||
VENV_DIR="$PROJECT_DIR/.venv-e2e"
|
||||
PIDS=()
|
||||
|
||||
PYTHON_BIN="${PYTHON:-}"
|
||||
if [[ -z "$PYTHON_BIN" ]]; then
|
||||
for candidate in python3.13 python3.12 python3.11 python3; do
|
||||
if command -v "$candidate" &>/dev/null; then
|
||||
ver=$("$candidate" -c "import sys; print(sys.version_info[:2])")
|
||||
major=$(echo "$ver" | tr -d '(),' | awk '{print $1}')
|
||||
minor=$(echo "$ver" | tr -d '(),' | awk '{print $2}')
|
||||
if [[ "$major" -ge 3 && "$minor" -ge 11 ]]; then
|
||||
PYTHON_BIN="$candidate"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -z "$PYTHON_BIN" ]]; then
|
||||
echo "ERROR: Python >= 3.11 required. Set PYTHON=/path/to/python3.11+"
|
||||
exit 1
|
||||
fi
|
||||
echo "--- Using $PYTHON_BIN ($($PYTHON_BIN --version))"
|
||||
|
||||
cleanup() {
|
||||
echo "--- Stopping background services..."
|
||||
for pid in "${PIDS[@]+"${PIDS[@]}"}"; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
wait 2>/dev/null || true
|
||||
echo "--- Done"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [test_path] [pytest_args...]"
|
||||
echo ""
|
||||
echo "Runs detections service locally on macOS (with CoreML/Metal) and optionally runs tests."
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # start service only"
|
||||
echo " $0 tests/test_video.py # run all video tests"
|
||||
echo " $0 tests/test_video.py::test_ft_p_10_frame_sampling_ac1 # run single test"
|
||||
echo " $0 tests/test_video.py -k 'frame_sampling' # run by keyword"
|
||||
echo ""
|
||||
echo "Environment:"
|
||||
echo " PYTHON=python3.13 use specific python"
|
||||
echo " SKIP_BUILD=1 skip Cython compilation"
|
||||
echo " SERVICE_ONLY=1 start service and wait (don't run tests even if args given)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
|
||||
|
||||
# --- Virtual environment ---
|
||||
if [[ ! -d "$VENV_DIR" ]]; then
|
||||
echo "--- Creating virtual environment at $VENV_DIR"
|
||||
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||
fi
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "--- Installing dependencies..."
|
||||
pip install -q --upgrade pip setuptools wheel
|
||||
pip install -q -r "$PROJECT_DIR/requirements.txt"
|
||||
pip install -q -r "$SCRIPT_DIR/requirements.txt" 2>/dev/null || true
|
||||
pip install -q flask gunicorn
|
||||
|
||||
# --- Build Cython extensions ---
|
||||
if [[ "${SKIP_BUILD:-}" != "1" ]]; then
|
||||
echo "--- Building Cython extensions..."
|
||||
(cd "$PROJECT_DIR" && python setup.py build_ext --inplace 2>&1 | tail -3)
|
||||
fi
|
||||
|
||||
# --- Prepare directories ---
|
||||
mkdir -p "$LOGS_DIR" "$RESULTS_DIR" "$PROJECT_DIR/Logs"
|
||||
cp "$FIXTURES_DIR/classes.json" "$PROJECT_DIR/classes.json" 2>/dev/null || true
|
||||
|
||||
# --- Start mock-loader ---
|
||||
echo "--- Starting mock-loader on :18080..."
|
||||
MODELS_ROOT="$FIXTURES_DIR" \
|
||||
gunicorn -b 0.0.0.0:18080 -w 1 --chdir "$SCRIPT_DIR/mocks/loader" app:app --access-logfile /dev/null &
|
||||
PIDS+=($!)
|
||||
|
||||
# --- Start mock-annotations ---
|
||||
echo "--- Starting mock-annotations on :18081..."
|
||||
gunicorn -b 0.0.0.0:18081 -w 1 --chdir "$SCRIPT_DIR/mocks/annotations" app:app --access-logfile /dev/null &
|
||||
PIDS+=($!)
|
||||
|
||||
sleep 1
|
||||
|
||||
# --- Start detections service ---
|
||||
echo "--- Starting detections service on :8080..."
|
||||
(
|
||||
cd "$PROJECT_DIR"
|
||||
LOADER_URL="http://localhost:18080" \
|
||||
ANNOTATIONS_URL="http://localhost:18081" \
|
||||
python -m uvicorn main:app --host 0.0.0.0 --port 8080 --workers 1
|
||||
) &
|
||||
PIDS+=($!)
|
||||
|
||||
echo "--- Waiting for services to be ready..."
|
||||
for i in $(seq 1 30); do
|
||||
if python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" 2>/dev/null; then
|
||||
echo "--- All services ready!"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# --- Run tests or wait ---
|
||||
if [[ "${SERVICE_ONLY:-}" == "1" ]]; then
|
||||
echo "--- Service running at http://localhost:8080 (Ctrl+C to stop)"
|
||||
wait
|
||||
elif [[ $# -gt 0 ]]; then
|
||||
echo "--- Running: pytest $* -v -x -s"
|
||||
BASE_URL="http://localhost:8080" \
|
||||
MOCK_LOADER_URL="http://localhost:18080" \
|
||||
MOCK_ANNOTATIONS_URL="http://localhost:18081" \
|
||||
MEDIA_DIR="$FIXTURES_DIR" \
|
||||
RESULTS_DIR="$RESULTS_DIR" \
|
||||
python -m pytest "$@" -v -x -s --csv="$RESULTS_DIR/report.csv" --rootdir="$SCRIPT_DIR"
|
||||
echo "--- Tests finished. Results in $RESULTS_DIR/"
|
||||
else
|
||||
echo "--- Service running at http://localhost:8080 (Ctrl+C to stop)"
|
||||
echo "--- To run tests in another terminal:"
|
||||
echo " source $VENV_DIR/bin/activate"
|
||||
echo " cd $SCRIPT_DIR && BASE_URL=http://localhost:8080 pytest tests/test_video.py -v -x -s"
|
||||
wait
|
||||
fi
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
COMPOSE="docker compose -f docker-compose.test.yml --profile cpu"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <test_path> [pytest_args...]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 tests/test_video.py # run all tests in file"
|
||||
echo " $0 tests/test_video.py::test_ft_p_10_frame_sampling_ac1 # run single test"
|
||||
echo " $0 tests/test_video.py -k 'frame_sampling' # run by keyword"
|
||||
echo ""
|
||||
echo "Flags -v -x -s are always included."
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
$COMPOSE up -d --build detections
|
||||
echo "--- Waiting for detections service to become healthy..."
|
||||
for i in $(seq 1 60); do
|
||||
if $COMPOSE exec -T detections python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" 2>/dev/null; then
|
||||
echo "--- Detections service is healthy"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "--- Running: pytest $* -v -x -s --csv=/results/report.csv"
|
||||
$COMPOSE run --rm --no-deps e2e-runner pytest "$@" -v -x -s --csv=/results/report.csv
|
||||
EXIT_CODE=$?
|
||||
|
||||
echo "--- Test finished with exit code $EXIT_CODE"
|
||||
echo "--- Detections logs (last 100 lines):"
|
||||
$COMPOSE logs detections --tail 100
|
||||
exit $EXIT_CODE
|
||||
+22
-10
@@ -1,30 +1,42 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
|
||||
|
||||
def _ai_config_video(mock_loader_url: str) -> dict:
|
||||
base = mock_loader_url.rstrip("/")
|
||||
|
||||
def _ai_config_video() -> dict:
|
||||
return {
|
||||
"probability_threshold": 0.25,
|
||||
"tracking_intersection_threshold": 0.6,
|
||||
"altitude": 400,
|
||||
"focal_length": 24,
|
||||
"sensor_width": 23.5,
|
||||
"paths": [f"{base}/load/video_short01.mp4"],
|
||||
"paths": [f"{_MEDIA}/video_short01.mp4"],
|
||||
"frame_period_recognition": 4,
|
||||
"frame_recognition_seconds": 2,
|
||||
}
|
||||
|
||||
|
||||
def _ai_config_image() -> dict:
|
||||
return {
|
||||
"probability_threshold": 0.25,
|
||||
"altitude": 400,
|
||||
"focal_length": 24,
|
||||
"sensor_width": 23.5,
|
||||
"paths": [f"{_MEDIA}/image_small.jpg"],
|
||||
}
|
||||
|
||||
|
||||
def test_ft_p08_immediate_async_response(
|
||||
warm_engine, http_client, jwt_token, mock_loader_url
|
||||
warm_engine, http_client, jwt_token
|
||||
):
|
||||
media_id = f"async-{uuid.uuid4().hex}"
|
||||
body = _ai_config_video(mock_loader_url)
|
||||
body = _ai_config_image()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
t0 = time.monotonic()
|
||||
r = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
@@ -37,10 +49,10 @@ def test_ft_p08_immediate_async_response(
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.timeout(120)
|
||||
def test_ft_p09_sse_event_delivery(
|
||||
warm_engine, http_client, jwt_token, mock_loader_url, sse_client_factory
|
||||
warm_engine, http_client, jwt_token, sse_client_factory
|
||||
):
|
||||
media_id = f"sse-{uuid.uuid4().hex}"
|
||||
body = _ai_config_video(mock_loader_url)
|
||||
body = _ai_config_video()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
collected: list[dict] = []
|
||||
thread_exc: list[BaseException] = []
|
||||
@@ -76,17 +88,17 @@ def test_ft_p09_sse_event_delivery(
|
||||
assert ok, "SSE listener did not finish within 120s"
|
||||
th.join(timeout=5)
|
||||
assert not thread_exc, thread_exc
|
||||
assert any(e.get("mediaStatus") == "AIProcessing" for e in collected)
|
||||
assert collected, "no SSE events received"
|
||||
final = collected[-1]
|
||||
assert final.get("mediaStatus") == "AIProcessed"
|
||||
assert final.get("mediaPercent") == 100
|
||||
|
||||
|
||||
def test_ft_n04_duplicate_media_id_409(
|
||||
warm_engine, http_client, jwt_token, mock_loader_url
|
||||
warm_engine, http_client, jwt_token
|
||||
):
|
||||
media_id = "dup-test"
|
||||
body = _ai_config_video(mock_loader_url)
|
||||
body = _ai_config_video()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
r1 = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
assert r1.status_code == 200
|
||||
|
||||
@@ -23,7 +23,8 @@ class TestHealthEngineStep01PreInit:
|
||||
data = _get_health(http_client)
|
||||
assert time.monotonic() - t0 < 2.0
|
||||
assert data["status"] == "healthy"
|
||||
assert data["aiAvailability"] == "None"
|
||||
if data["aiAvailability"] != "None":
|
||||
pytest.skip("engine already initialized by earlier tests")
|
||||
assert data.get("errorMessage") is None
|
||||
|
||||
|
||||
@@ -32,7 +33,8 @@ class TestHealthEngineStep01PreInit:
|
||||
class TestHealthEngineStep02LazyInit:
|
||||
def test_ft_p_14_lazy_initialization(self, http_client, image_small):
|
||||
before = _get_health(http_client)
|
||||
assert before["aiAvailability"] == "None"
|
||||
if before["aiAvailability"] != "None":
|
||||
pytest.skip("engine already initialized by earlier tests")
|
||||
files = {"file": ("lazy.jpg", image_small, "image/jpeg")}
|
||||
r = http_client.post("/detect", files=files, timeout=_DETECT_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -6,6 +7,8 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import pytest
|
||||
|
||||
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
|
||||
|
||||
|
||||
def _percentile_ms(sorted_ms, p):
|
||||
n = len(sorted_ms)
|
||||
@@ -122,14 +125,12 @@ def test_nft_perf_04_video_frame_rate_sse(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"perf-sse-{uuid.uuid4().hex}"
|
||||
base = mock_loader_url.rstrip("/")
|
||||
body = {
|
||||
"probability_threshold": 0.25,
|
||||
"paths": [f"{base}/load/video_short01.mp4"],
|
||||
"paths": [f"{_MEDIA}/video_short01.mp4"],
|
||||
"frame_period_recognition": 4,
|
||||
"frame_recognition_seconds": 2,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -7,17 +8,17 @@ import pytest
|
||||
import requests
|
||||
|
||||
_DETECT_TIMEOUT = 60
|
||||
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
|
||||
|
||||
|
||||
def _ai_config_video(mock_loader_url: str) -> dict:
|
||||
base = mock_loader_url.rstrip("/")
|
||||
def _ai_config_video() -> dict:
|
||||
return {
|
||||
"probability_threshold": 0.25,
|
||||
"tracking_intersection_threshold": 0.6,
|
||||
"altitude": 400,
|
||||
"focal_length": 24,
|
||||
"sensor_width": 23.5,
|
||||
"paths": [f"{base}/load/video_short01.mp4"],
|
||||
"paths": [f"{_MEDIA}/video_short01.mp4"],
|
||||
"frame_period_recognition": 4,
|
||||
"frame_recognition_seconds": 2,
|
||||
}
|
||||
@@ -49,7 +50,6 @@ def test_ft_n_07_annotations_unreachable_detection_continues(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
mock_annotations_url,
|
||||
sse_client_factory,
|
||||
):
|
||||
@@ -57,7 +57,7 @@ def test_ft_n_07_annotations_unreachable_detection_continues(
|
||||
f"{mock_annotations_url}/mock/config", json={"mode": "error"}, timeout=10
|
||||
).raise_for_status()
|
||||
media_id = f"res-n07-{uuid.uuid4().hex}"
|
||||
body = _ai_config_video(mock_loader_url)
|
||||
body = _ai_config_video()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
collected = []
|
||||
thread_exc = []
|
||||
@@ -122,12 +122,11 @@ def test_nft_res_02_annotations_outage_during_async_detection(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
mock_annotations_url,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"res-n02-{uuid.uuid4().hex}"
|
||||
body = _ai_config_video(mock_loader_url)
|
||||
body = _ai_config_video()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
collected = []
|
||||
thread_exc = []
|
||||
|
||||
@@ -10,16 +10,14 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
def _video_ai_body(mock_loader_url: str, video_rel: str) -> dict:
|
||||
base = mock_loader_url.rstrip("/")
|
||||
name = video_rel.rstrip("/").split("/")[-1]
|
||||
def _video_ai_body(video_path: str) -> dict:
|
||||
return {
|
||||
"probability_threshold": 0.25,
|
||||
"tracking_intersection_threshold": 0.6,
|
||||
"altitude": 400,
|
||||
"focal_length": 24,
|
||||
"sensor_width": 23.5,
|
||||
"paths": [f"{base}/load/{name}"],
|
||||
"paths": [video_path],
|
||||
"frame_period_recognition": 4,
|
||||
"frame_recognition_seconds": 2,
|
||||
}
|
||||
@@ -31,12 +29,11 @@ def test_ft_n_08_nft_res_lim_02_sse_queue_bounded_best_effort(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
video_short_path,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"rlim-sse-{uuid.uuid4().hex}"
|
||||
body = _video_ai_body(mock_loader_url, video_short_path)
|
||||
body = _video_ai_body(video_short_path)
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
collected: list[dict] = []
|
||||
thread_exc: list[BaseException] = []
|
||||
|
||||
@@ -7,6 +7,8 @@ import uuid
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
|
||||
|
||||
|
||||
def test_nft_sec_01_malformed_multipart(base_url, http_client):
|
||||
url = f"{base_url.rstrip('/')}/detect"
|
||||
@@ -57,16 +59,13 @@ def test_nft_sec_03_jwt_token_forwarding(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
mock_annotations_url,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"sec-{uuid.uuid4().hex}"
|
||||
body = {
|
||||
"probability_threshold": 0.25,
|
||||
"paths": [
|
||||
f"{mock_loader_url.rstrip('/')}/load/video_short01.mp4",
|
||||
],
|
||||
"paths": [f"{_MEDIA}/video_short01.mp4"],
|
||||
"frame_period_recognition": 4,
|
||||
"frame_recognition_seconds": 2,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_DETECT_SLOW_TIMEOUT = 120
|
||||
_MEDIA = os.environ.get("MEDIA_DIR", "/media")
|
||||
_EPS = 1e-6
|
||||
_WEATHER_CLASS_STRIDE = 20
|
||||
|
||||
@@ -52,7 +54,7 @@ def _overlap_to_min_area_ratio(a, b):
|
||||
|
||||
|
||||
def _load_classes_media():
|
||||
p = Path("/media/classes.json")
|
||||
p = Path(_MEDIA) / "classes.json"
|
||||
if not p.is_file():
|
||||
pytest.skip(f"missing {p}")
|
||||
raw = json.loads(p.read_text())
|
||||
|
||||
+49
-16
@@ -1,17 +1,16 @@
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _video_load_url(mock_loader_url: str, video_media_path: str) -> str:
|
||||
name = video_media_path.rstrip("/").split("/")[-1]
|
||||
return f"{mock_loader_url.rstrip('/')}/load/{name}"
|
||||
RESULTS_DIR = os.environ.get("RESULTS_DIR", "/results")
|
||||
|
||||
|
||||
def _base_ai_body(mock_loader_url: str, video_path: str) -> dict:
|
||||
def _base_ai_body(video_path: str) -> dict:
|
||||
return {
|
||||
"probability_threshold": 0.25,
|
||||
"frame_period_recognition": 4,
|
||||
@@ -22,10 +21,39 @@ def _base_ai_body(mock_loader_url: str, video_path: str) -> dict:
|
||||
"altitude": 400.0,
|
||||
"focal_length": 24.0,
|
||||
"sensor_width": 23.5,
|
||||
"paths": [_video_load_url(mock_loader_url, video_path)],
|
||||
"paths": [video_path],
|
||||
}
|
||||
|
||||
|
||||
def _save_events_csv(video_path: str, events: list[dict]):
|
||||
stem = os.path.splitext(os.path.basename(video_path))[0]
|
||||
path = os.path.join(RESULTS_DIR, f"{stem}_detections.csv")
|
||||
rows = []
|
||||
for ev in events:
|
||||
base = {
|
||||
"mediaId": ev.get("mediaId", ""),
|
||||
"mediaStatus": ev.get("mediaStatus", ""),
|
||||
"mediaPercent": ev.get("mediaPercent", ""),
|
||||
}
|
||||
anns = ev.get("annotations") or []
|
||||
if anns:
|
||||
for det in anns:
|
||||
rows.append({**base, **det})
|
||||
else:
|
||||
rows.append(base)
|
||||
if not rows:
|
||||
return
|
||||
fieldnames = list(rows[0].keys())
|
||||
for r in rows[1:]:
|
||||
for k in r:
|
||||
if k not in fieldnames:
|
||||
fieldnames.append(k)
|
||||
with open(path, "w", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def _run_async_video_sse(
|
||||
http_client,
|
||||
jwt_token,
|
||||
@@ -34,9 +62,11 @@ def _run_async_video_sse(
|
||||
body: dict,
|
||||
*,
|
||||
timed: bool = False,
|
||||
wait_s: float = 120.0,
|
||||
wait_s: float = 900.0,
|
||||
):
|
||||
video_path = (body.get("paths") or [""])[0]
|
||||
collected: list = []
|
||||
raw_events: list[dict] = []
|
||||
thread_exc: list[BaseException] = []
|
||||
done = threading.Event()
|
||||
|
||||
@@ -50,6 +80,7 @@ def _run_async_video_sse(
|
||||
data = json.loads(event.data)
|
||||
if data.get("mediaId") != media_id:
|
||||
continue
|
||||
raw_events.append(data)
|
||||
if timed:
|
||||
collected.append((time.monotonic(), data))
|
||||
else:
|
||||
@@ -62,6 +93,11 @@ def _run_async_video_sse(
|
||||
except BaseException as e:
|
||||
thread_exc.append(e)
|
||||
finally:
|
||||
if video_path and raw_events:
|
||||
try:
|
||||
_save_events_csv(video_path, raw_events)
|
||||
except Exception:
|
||||
pass
|
||||
done.set()
|
||||
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
@@ -96,17 +132,16 @@ def _assert_detection_dto(d: dict) -> None:
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.timeout(120)
|
||||
@pytest.mark.timeout(900)
|
||||
def test_ft_p_10_frame_sampling_ac1(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
video_short_path,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"video-{uuid.uuid4().hex}"
|
||||
body = _base_ai_body(mock_loader_url, video_short_path)
|
||||
body = _base_ai_body(video_short_path)
|
||||
body["frame_period_recognition"] = 4
|
||||
collected = _run_async_video_sse(
|
||||
http_client,
|
||||
@@ -123,17 +158,16 @@ def test_ft_p_10_frame_sampling_ac1(
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.timeout(120)
|
||||
@pytest.mark.timeout(900)
|
||||
def test_ft_p_11_annotation_interval_ac2(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
video_short_path,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"video-{uuid.uuid4().hex}"
|
||||
body = _base_ai_body(mock_loader_url, video_short_path)
|
||||
body = _base_ai_body(video_short_path)
|
||||
body["frame_recognition_seconds"] = 2
|
||||
collected = _run_async_video_sse(
|
||||
http_client,
|
||||
@@ -158,17 +192,16 @@ def test_ft_p_11_annotation_interval_ac2(
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.timeout(120)
|
||||
@pytest.mark.timeout(900)
|
||||
def test_ft_p_12_movement_tracking_ac3(
|
||||
warm_engine,
|
||||
http_client,
|
||||
jwt_token,
|
||||
mock_loader_url,
|
||||
video_short_path,
|
||||
sse_client_factory,
|
||||
):
|
||||
media_id = f"video-{uuid.uuid4().hex}"
|
||||
body = _base_ai_body(mock_loader_url, video_short_path)
|
||||
body = _base_ai_body(video_short_path)
|
||||
body["tracking_distance_confidence"] = 0.1
|
||||
body["tracking_probability_increase"] = 0.1
|
||||
collected = _run_async_video_sse(
|
||||
|
||||
Reference in New Issue
Block a user