[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
+22 -55
View File
@@ -1,6 +1,4 @@
import io
import os
import tempfile
import threading
import av
@@ -14,7 +12,7 @@ from ai_config cimport AIRecognitionConfig
from engines.inference_engine cimport InferenceEngine
from loader_http_client cimport LoaderHttpClient
from threading import Thread
from engines import EngineClass
from engines import engine_factory
def ai_config_from_dict(dict data):
@@ -76,29 +74,23 @@ cdef class Inference:
raise Exception(res.err)
return <bytes>res.data
cdef convert_and_upload_model(self, bytes source_bytes, str engine_filename, str calib_cache_path):
cdef convert_and_upload_model(self, bytes source_bytes, str models_dir):
try:
self.ai_availability_status.set_status(AIAvailabilityEnum.CONVERTING)
models_dir = constants_inf.MODELS_FOLDER
model_bytes = EngineClass.convert_from_source(source_bytes, calib_cache_path)
engine_bytes, engine_filename = engine_factory.build_from_source(source_bytes, self.loader_client, models_dir)
self.ai_availability_status.set_status(AIAvailabilityEnum.UPLOADING)
res = self.loader_client.upload_big_small_resource(model_bytes, engine_filename, models_dir)
res = self.loader_client.upload_big_small_resource(engine_bytes, engine_filename, models_dir)
if res.err is not None:
self.ai_availability_status.set_status(AIAvailabilityEnum.WARNING, <str>f"Failed to upload converted model: {res.err}")
self._converted_model_bytes = model_bytes
self._converted_model_bytes = engine_bytes
self.ai_availability_status.set_status(AIAvailabilityEnum.ENABLED)
except Exception as e:
self.ai_availability_status.set_status(AIAvailabilityEnum.ERROR, <str> str(e))
self._converted_model_bytes = <bytes>None
finally:
self.is_building_engine = <bint>False
if calib_cache_path is not None:
try:
os.unlink(calib_cache_path)
except Exception:
pass
cdef init_ai(self):
constants_inf.log(<str> 'init AI...')
@@ -110,7 +102,7 @@ cdef class Inference:
if self._converted_model_bytes is not None:
try:
self.engine = EngineClass(self._converted_model_bytes)
self.engine = engine_factory.create(self._converted_model_bytes)
self.ai_availability_status.set_status(AIAvailabilityEnum.ENABLED)
except Exception as e:
self.ai_availability_status.set_status(AIAvailabilityEnum.ERROR, <str> str(e))
@@ -119,58 +111,33 @@ cdef class Inference:
return
models_dir = constants_inf.MODELS_FOLDER
engine_filename_fp16 = EngineClass.get_engine_filename()
if engine_filename_fp16 is not None:
engine_filename_int8 = EngineClass.get_engine_filename(<str>"int8")
for candidate in [engine_filename_int8, engine_filename_fp16]:
try:
self.ai_availability_status.set_status(AIAvailabilityEnum.DOWNLOADING)
res = self.loader_client.load_big_small_resource(candidate, models_dir)
if res.err is not None:
raise Exception(res.err)
self.engine = EngineClass(res.data)
self.ai_availability_status.set_status(AIAvailabilityEnum.ENABLED)
return
except Exception:
pass
self.ai_availability_status.set_status(AIAvailabilityEnum.DOWNLOADING)
engine = engine_factory.load_engine(self.loader_client, models_dir)
if engine is not None:
self.engine = engine
self.ai_availability_status.set_status(AIAvailabilityEnum.ENABLED)
return
source_filename = EngineClass.get_source_filename()
if source_filename is None:
self.ai_availability_status.set_status(AIAvailabilityEnum.ERROR, <str>"Pre-built engine not found and no source available")
return
source_filename = engine_factory.get_source_filename()
if source_filename is None:
self.ai_availability_status.set_status(AIAvailabilityEnum.ERROR, <str>"No engine available and no source to build from")
return
source_bytes = self.download_model(source_filename)
if engine_factory.has_build_step:
self.ai_availability_status.set_status(AIAvailabilityEnum.WARNING, <str>"Cached engine not found, converting from source")
source_bytes = self.download_model(source_filename)
calib_cache_path = self._try_download_calib_cache(models_dir)
target_engine_filename = EngineClass.get_engine_filename(<str>"int8") if calib_cache_path is not None else engine_filename_fp16
self.is_building_engine = <bint>True
thread = Thread(target=self.convert_and_upload_model, args=(source_bytes, target_engine_filename, calib_cache_path))
thread = Thread(target=self.convert_and_upload_model, args=(source_bytes, models_dir))
thread.daemon = True
thread.start()
return
else:
self.engine = EngineClass(<bytes>self.download_model(constants_inf.AI_ONNX_MODEL_FILE))
self.engine = engine_factory.create(source_bytes)
self.ai_availability_status.set_status(AIAvailabilityEnum.ENABLED)
self.is_building_engine = <bint>False
except Exception as e:
self.ai_availability_status.set_status(AIAvailabilityEnum.ERROR, <str>str(e))
self.is_building_engine = <bint>False
cdef str _try_download_calib_cache(self, str models_dir):
try:
res = self.loader_client.load_big_small_resource(constants_inf.INT8_CALIB_CACHE_FILE, models_dir)
if res.err is not None:
constants_inf.log(<str>f"INT8 calibration cache not available: {res.err}")
return <str>None
fd, path = tempfile.mkstemp(suffix='.cache')
with os.fdopen(fd, 'wb') as f:
f.write(res.data)
constants_inf.log(<str>'INT8 calibration cache downloaded')
return <str>path
except Exception as e:
constants_inf.log(<str>f"INT8 calibration cache download failed: {str(e)}")
return <str>None
cpdef run_detect_image(self, bytes image_bytes, AIRecognitionConfig ai_config, str media_name,
object annotation_callback, object status_callback=None):
cdef list all_frame_data = []