[AZ-180] Refactor detection event handling and improve SSE support

- Updated the detection image endpoint to require a channel ID for event streaming.
- Introduced a new endpoint for streaming detection events, allowing clients to receive real-time updates.
- Enhanced the internal buffering mechanism for detection events to manage multiple channels.
- Refactored the inference module to support the new event handling structure.

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-03 02:42:05 +03:00
parent 2c35e59a77
commit 8baa96978b
26 changed files with 819 additions and 413 deletions
+20 -65
View File
@@ -81,16 +81,10 @@ def _weather_label_ok(label, base_names):
@pytest.mark.slow
def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm_engine, auth_headers):
r = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
headers=auth_headers,
)
assert r.status_code == 200
body = r.json()
assert isinstance(body, list)
for d in body:
def test_ft_p_03_detection_response_structure_ac1(image_detect, image_small, warm_engine):
detections, _ = image_detect(image_small, "img.jpg")
assert isinstance(detections, list)
for d in detections:
assert isinstance(d["centerX"], (int, float))
assert isinstance(d["centerY"], (int, float))
assert isinstance(d["width"], (int, float))
@@ -106,44 +100,24 @@ def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm
@pytest.mark.slow
def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine, auth_headers):
cfg_hi = json.dumps({"probability_threshold": 0.8})
r_hi = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg_hi},
headers=auth_headers,
)
assert r_hi.status_code == 200
hi = r_hi.json()
def test_ft_p_05_confidence_filtering_ac2(image_detect, image_small, warm_engine):
hi, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.8}))
assert isinstance(hi, list)
for d in hi:
assert float(d["confidence"]) + _EPS >= 0.8
cfg_lo = json.dumps({"probability_threshold": 0.1})
r_lo = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg_lo},
headers=auth_headers,
)
assert r_lo.status_code == 200
lo = r_lo.json()
lo, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.1}))
assert isinstance(lo, list)
assert len(lo) >= len(hi)
@pytest.mark.slow
def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine, auth_headers):
cfg_loose = json.dumps({"tracking_intersection_threshold": 0.6})
r1 = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_dense, "image/jpeg")},
data={"config": cfg_loose},
headers=auth_headers,
def test_ft_p_06_overlap_deduplication_ac3(image_detect, image_dense, warm_engine):
dets, _ = image_detect(
image_dense, "img.jpg",
config=json.dumps({"tracking_intersection_threshold": 0.6}),
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r1.status_code == 200
dets = r1.json()
assert isinstance(dets, list)
by_label = {}
for d in dets:
@@ -153,22 +127,18 @@ def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine
for j in range(i + 1, len(group)):
ratio = _overlap_to_min_area_ratio(group[i], group[j])
assert ratio <= 0.6 + _EPS, (label, ratio)
cfg_strict = json.dumps({"tracking_intersection_threshold": 0.01})
r2 = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_dense, "image/jpeg")},
data={"config": cfg_strict},
headers=auth_headers,
strict, _ = image_detect(
image_dense, "img.jpg",
config=json.dumps({"tracking_intersection_threshold": 0.01}),
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r2.status_code == 200
strict = r2.json()
assert isinstance(strict, list)
assert len(strict) <= len(dets)
@pytest.mark.slow
def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engine, auth_headers):
def test_ft_p_07_physical_size_filtering_ac4(image_detect, image_small, warm_engine):
by_id, _ = _load_classes_media()
wh = _image_width_height(image_small)
assert wh is not None
@@ -184,15 +154,7 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
"sensor_width": sensor_width,
}
)
r = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_small, "image/jpeg")},
data={"config": cfg},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r.status_code == 200
body = r.json()
body, _ = image_detect(image_small, "img.jpg", config=cfg, timeout=_DETECT_SLOW_TIMEOUT)
assert isinstance(body, list)
for d in body:
base_id = d["classNum"] % _WEATHER_CLASS_STRIDE
@@ -203,17 +165,10 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
@pytest.mark.slow
def test_ft_p_13_weather_mode_class_variants_ac5(
http_client, image_different_types, warm_engine, auth_headers
image_detect, image_different_types, warm_engine
):
_, base_names = _load_classes_media()
r = http_client.post(
"/detect/image",
files={"file": ("img.jpg", image_different_types, "image/jpeg")},
headers=auth_headers,
timeout=_DETECT_SLOW_TIMEOUT,
)
assert r.status_code == 200
body = r.json()
body, _ = image_detect(image_different_types, "img.jpg", timeout=_DETECT_SLOW_TIMEOUT)
assert isinstance(body, list)
for d in body:
label = d["label"]