matrice-inference 0.1.2__tar.gz → 0.1.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of matrice-inference might be problematic. Click here for more details.
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/PKG-INFO +1 -1
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/matrice_inference.egg-info/PKG-INFO +1 -1
- matrice_inference-0.1.22/src/matrice_inference/__init__.py +89 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/__init__.py +17 -11
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/model/triton_server.py +1 -3
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/server.py +3 -4
- matrice_inference-0.1.22/src/matrice_inference/server/stream/consumer_worker.py +458 -0
- matrice_inference-0.1.22/src/matrice_inference/server/stream/frame_cache.py +222 -0
- matrice_inference-0.1.22/src/matrice_inference/server/stream/inference_worker.py +252 -0
- matrice_inference-0.1.22/src/matrice_inference/server/stream/post_processing_worker.py +295 -0
- matrice_inference-0.1.22/src/matrice_inference/server/stream/producer_worker.py +204 -0
- matrice_inference-0.1.22/src/matrice_inference/server/stream/stream_pipeline.py +423 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/analytics.py +1 -1
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/overall_inference_testing.py +0 -4
- matrice_inference-0.1.2/src/matrice_inference/__init__.py +0 -72
- matrice_inference-0.1.2/src/matrice_inference/server/stream/consumer_worker.py +0 -201
- matrice_inference-0.1.2/src/matrice_inference/server/stream/frame_cache.py +0 -127
- matrice_inference-0.1.2/src/matrice_inference/server/stream/inference_worker.py +0 -163
- matrice_inference-0.1.2/src/matrice_inference/server/stream/post_processing_worker.py +0 -230
- matrice_inference-0.1.2/src/matrice_inference/server/stream/producer_worker.py +0 -147
- matrice_inference-0.1.2/src/matrice_inference/server/stream/stream_pipeline.py +0 -451
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/LICENSE.txt +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/README.md +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/matrice_inference.egg-info/SOURCES.txt +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/matrice_inference.egg-info/dependency_links.txt +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/matrice_inference.egg-info/not-zip-safe +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/matrice_inference.egg-info/top_level.txt +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/pyproject.toml +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/setup.cfg +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/setup.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/py.typed +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/inference_interface.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/model/__init__.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/model/model_manager.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/model/model_manager_wrapper.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/model/triton_model_manager.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/proxy_interface.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/stream/__init__.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/stream/app_deployment.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/stream/utils.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/abstract_model_manager.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/__init__.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/aggregator.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/ingestor.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/latency.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/pipeline.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/publisher.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/aggregator/synchronizer.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/batch_manager.py +0 -0
- {matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/tmp/triton_utils.py +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Module providing __init__ functionality."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import platform
|
|
6
|
+
from matrice_common.utils import dependencies_check
|
|
7
|
+
|
|
8
|
+
base = [
|
|
9
|
+
"httpx",
|
|
10
|
+
"fastapi",
|
|
11
|
+
"uvicorn",
|
|
12
|
+
"pillow",
|
|
13
|
+
"confluent_kafka[snappy]",
|
|
14
|
+
"aiokafka",
|
|
15
|
+
"aiohttp",
|
|
16
|
+
"filterpy",
|
|
17
|
+
"scipy",
|
|
18
|
+
"scikit-learn",
|
|
19
|
+
"matplotlib",
|
|
20
|
+
"scikit-image",
|
|
21
|
+
"python-snappy",
|
|
22
|
+
"pyyaml",
|
|
23
|
+
"imagehash",
|
|
24
|
+
"Pillow",
|
|
25
|
+
"transformers"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Helper to attempt installation and verify importability
|
|
29
|
+
def _install_and_verify(pkg: str, import_name: str):
|
|
30
|
+
"""Install a package expression and return True if the import succeeds."""
|
|
31
|
+
try:
|
|
32
|
+
if pkg=='onnxruntime-gpu':
|
|
33
|
+
pkg = 'onnxruntime'
|
|
34
|
+
__import__(pkg)
|
|
35
|
+
return True
|
|
36
|
+
except:
|
|
37
|
+
if dependencies_check([pkg]):
|
|
38
|
+
try:
|
|
39
|
+
__import__(import_name)
|
|
40
|
+
return True
|
|
41
|
+
except ImportError:
|
|
42
|
+
return False
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# Runtime gating for optional OCR bootstrap (default OFF), and never on Jetson
|
|
46
|
+
_ENABLE_OCR_BOOTSTRAP = os.getenv("MATRICE_ENABLE_OCR_BOOTSTRAP", "0")
|
|
47
|
+
_IS_JETSON = (platform.machine().lower() in ("aarch64", "arm64"))
|
|
48
|
+
|
|
49
|
+
print("*******************************Deployment ENV Info**********************************")
|
|
50
|
+
print(f"ENABLE_JETSON_PIP_SETTINGS: {_ENABLE_OCR_BOOTSTRAP}") #0 if OFF, 1 if ON, this will be set to 1 in jetson byom codebase.
|
|
51
|
+
print(f"IS_JETSON_ARCH?: {_IS_JETSON}") #True if Jetson, False otherwise
|
|
52
|
+
print("*************************************************************************************")
|
|
53
|
+
|
|
54
|
+
if not int(_ENABLE_OCR_BOOTSTRAP) and not _IS_JETSON:
|
|
55
|
+
# Install base dependencies first
|
|
56
|
+
dependencies_check(base)
|
|
57
|
+
|
|
58
|
+
if not dependencies_check(["opencv-python"]):
|
|
59
|
+
dependencies_check(["opencv-python-headless"])
|
|
60
|
+
|
|
61
|
+
# Attempt GPU-specific dependencies first
|
|
62
|
+
_gpu_ok = _install_and_verify("onnxruntime-gpu", "onnxruntime") and _install_and_verify(
|
|
63
|
+
"fast-plate-ocr[onnx-gpu]", "fast_plate_ocr"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not _gpu_ok:
|
|
67
|
+
# Fallback to CPU variants
|
|
68
|
+
_cpu_ok = _install_and_verify("onnxruntime", "onnxruntime") and _install_and_verify(
|
|
69
|
+
"fast-plate-ocr[onnx]", "fast_plate_ocr"
|
|
70
|
+
)
|
|
71
|
+
if not _cpu_ok:
|
|
72
|
+
# Last-chance fallback without extras tag (PyPI sometimes lacks them)
|
|
73
|
+
_install_and_verify("fast-plate-ocr", "fast_plate_ocr")
|
|
74
|
+
|
|
75
|
+
# matrice_deps = ["matrice_common", "matrice_analytics", "matrice"]
|
|
76
|
+
|
|
77
|
+
# dependencies_check(matrice_deps)
|
|
78
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
79
|
+
from server.server import MatriceDeployServer # noqa: E402
|
|
80
|
+
from server.server import MatriceDeployServer as MatriceDeploy # noqa: E402 # Keep this for backwards compatibility
|
|
81
|
+
from server.inference_interface import InferenceInterface # noqa: E402
|
|
82
|
+
from server.proxy_interface import MatriceProxyInterface # noqa: E402
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
"MatriceDeploy",
|
|
86
|
+
"MatriceDeployServer",
|
|
87
|
+
"InferenceInterface",
|
|
88
|
+
"MatriceProxyInterface",
|
|
89
|
+
]
|
{matrice_inference-0.1.2 → matrice_inference-0.1.22}/src/matrice_inference/server/__init__.py
RENAMED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
# Define paths
|
|
5
|
+
log_path = os.path.join(os.getcwd(), "deploy_server.log")
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# Create handlers explicitly
|
|
8
8
|
console_handler = logging.StreamHandler()
|
|
9
|
-
console_handler.setLevel(logging.INFO)
|
|
10
|
-
|
|
11
|
-
# File handler (DEBUG+)
|
|
12
|
-
log_path = os.path.join(os.getcwd(), "deploy_server.log")
|
|
13
9
|
file_handler = logging.FileHandler(log_path)
|
|
10
|
+
|
|
11
|
+
# Set levels
|
|
12
|
+
console_handler.setLevel(logging.INFO)
|
|
14
13
|
file_handler.setLevel(logging.DEBUG)
|
|
15
14
|
|
|
16
|
-
#
|
|
15
|
+
# Define a formatter
|
|
17
16
|
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
18
17
|
console_handler.setFormatter(formatter)
|
|
19
18
|
file_handler.setFormatter(formatter)
|
|
20
19
|
|
|
21
|
-
#
|
|
22
|
-
logging.getLogger()
|
|
23
|
-
logging.
|
|
20
|
+
# Get the root logger
|
|
21
|
+
logger = logging.getLogger()
|
|
22
|
+
logger.setLevel(logging.DEBUG) # Root level must be the lowest (DEBUG)
|
|
23
|
+
|
|
24
|
+
# Optional: remove any default handlers if basicConfig was called earlier
|
|
25
|
+
if logger.hasHandlers():
|
|
26
|
+
logger.handlers.clear()
|
|
27
|
+
|
|
28
|
+
logger.addHandler(console_handler)
|
|
29
|
+
logger.addHandler(file_handler)
|
|
@@ -17,8 +17,6 @@ from matrice_common.utils import dependencies_check
|
|
|
17
17
|
TRITON_DOCKER_IMAGE = "nvcr.io/nvidia/tritonserver:23.08-py3"
|
|
18
18
|
BASE_PATH = "./model_repository"
|
|
19
19
|
|
|
20
|
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
21
|
-
|
|
22
20
|
class TritonServer:
|
|
23
21
|
def __init__(
|
|
24
22
|
self,
|
|
@@ -1161,7 +1159,7 @@ class TritonInference:
|
|
|
1161
1159
|
model_alphabet: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_",
|
|
1162
1160
|
return_confidence: bool = True,
|
|
1163
1161
|
confidence_threshold: float = 0.0, # Disabled threshold to match ONNX
|
|
1164
|
-
) -> Tuple[
|
|
1162
|
+
) -> Union[Tuple[List[str], np.ndarray], List[str]]:
|
|
1165
1163
|
"""Postprocess OCR model outputs into license plate strings.
|
|
1166
1164
|
|
|
1167
1165
|
Args:
|
|
@@ -320,7 +320,10 @@ class MatriceDeployServer:
|
|
|
320
320
|
post_processing_config = {}
|
|
321
321
|
if isinstance(post_processing_config, dict):
|
|
322
322
|
post_processing_config["facial_recognition_server_id"] = self.job_params.get("facial_recognition_server_id", None)
|
|
323
|
+
post_processing_config["lpr_server_id"] = self.job_params.get("lpr_server_id", None)
|
|
323
324
|
post_processing_config["session"] = self.session # Pass the session to post-processing
|
|
325
|
+
# Pass deployment_id for facial recognition deployment update
|
|
326
|
+
post_processing_config["deployment_id"] = self.deployment_id
|
|
324
327
|
|
|
325
328
|
# Get index_to_category from action_tracker if available
|
|
326
329
|
index_to_category = None
|
|
@@ -374,10 +377,6 @@ class MatriceDeployServer:
|
|
|
374
377
|
self.streaming_pipeline = StreamingPipeline(
|
|
375
378
|
inference_interface=self.inference_interface,
|
|
376
379
|
post_processor=self.post_processor,
|
|
377
|
-
consumer_threads=self.job_params.get("consumer_threads", 4),
|
|
378
|
-
producer_threads=self.job_params.get("producer_threads", 2),
|
|
379
|
-
inference_threads=self.job_params.get("inference_threads", 4),
|
|
380
|
-
postprocessing_threads=self.job_params.get("postprocessing_threads", 2),
|
|
381
380
|
inference_queue_maxsize=self.job_params.get("inference_queue_maxsize", 5000),
|
|
382
381
|
postproc_queue_maxsize=self.job_params.get("postproc_queue_maxsize", 5000),
|
|
383
382
|
output_queue_maxsize=self.job_params.get("output_queue_maxsize", 5000),
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# Import moved to method where it's needed to avoid circular imports
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import base64
|
|
9
|
+
import copy
|
|
10
|
+
import cv2
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Dict, Any, Optional
|
|
13
|
+
from matrice_inference.server.stream.utils import CameraConfig, StreamMessage
|
|
14
|
+
|
|
15
|
+
class ConsumerWorker:
|
|
16
|
+
"""Handles message consumption from streams with optimized processing."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_PRIORITY = 1
|
|
19
|
+
DEFAULT_DB = 0
|
|
20
|
+
DEFAULT_CONNECTION_TIMEOUT = 120
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
camera_id: str,
|
|
25
|
+
worker_id: int,
|
|
26
|
+
stream_config: Dict[str, Any],
|
|
27
|
+
input_topic: str,
|
|
28
|
+
inference_queue: queue.PriorityQueue,
|
|
29
|
+
message_timeout: float,
|
|
30
|
+
camera_config: CameraConfig,
|
|
31
|
+
frame_cache: Optional[Any] = None
|
|
32
|
+
):
|
|
33
|
+
self.camera_id = camera_id
|
|
34
|
+
self.worker_id = worker_id
|
|
35
|
+
self.stream_config = stream_config
|
|
36
|
+
self.input_topic = input_topic
|
|
37
|
+
self.inference_queue = inference_queue
|
|
38
|
+
self.message_timeout = message_timeout
|
|
39
|
+
self.camera_config = camera_config
|
|
40
|
+
self.running = False
|
|
41
|
+
self.stream: Optional[Any] = None
|
|
42
|
+
self.logger = logging.getLogger(f"{__name__}.consumer.{camera_id}.{worker_id}")
|
|
43
|
+
# H.265 stream decoder instance (initialized lazily per worker)
|
|
44
|
+
self._h265_stream_decoder = None
|
|
45
|
+
# Optional frame cache for low-latency caching at ingestion
|
|
46
|
+
self.frame_cache = frame_cache
|
|
47
|
+
|
|
48
|
+
def start(self) -> threading.Thread:
|
|
49
|
+
"""Start the consumer worker in a separate thread."""
|
|
50
|
+
self.running = True
|
|
51
|
+
thread = threading.Thread(
|
|
52
|
+
target=self._run,
|
|
53
|
+
name=f"Consumer-{self.camera_id}-{self.worker_id}",
|
|
54
|
+
daemon=False
|
|
55
|
+
)
|
|
56
|
+
thread.start()
|
|
57
|
+
return thread
|
|
58
|
+
|
|
59
|
+
def stop(self):
|
|
60
|
+
"""Stop the consumer worker."""
|
|
61
|
+
self.running = False
|
|
62
|
+
try:
|
|
63
|
+
if self._h265_stream_decoder is not None:
|
|
64
|
+
self._h265_stream_decoder.stop()
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _run(self) -> None:
|
|
69
|
+
"""Main consumer loop with proper resource management."""
|
|
70
|
+
loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(loop)
|
|
72
|
+
|
|
73
|
+
self.logger.info(f"Started consumer worker for camera {self.camera_id}")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
loop.run_until_complete(self._initialize_stream())
|
|
77
|
+
self._consume_messages(loop)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self.logger.error(f"Fatal error in consumer worker: {e}")
|
|
80
|
+
finally:
|
|
81
|
+
self._cleanup_resources(loop)
|
|
82
|
+
|
|
83
|
+
def _consume_messages(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
84
|
+
"""Main message consumption loop."""
|
|
85
|
+
while self.running and self.camera_config.enabled:
|
|
86
|
+
try:
|
|
87
|
+
message_data = loop.run_until_complete(self._get_message_safely())
|
|
88
|
+
if message_data:
|
|
89
|
+
self._process_message(message_data)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.error(f"Error processing message: {e}")
|
|
92
|
+
time.sleep(1.0)
|
|
93
|
+
|
|
94
|
+
def _cleanup_resources(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
95
|
+
"""Clean up stream and event loop resources."""
|
|
96
|
+
if self.stream:
|
|
97
|
+
try:
|
|
98
|
+
loop.run_until_complete(self.stream.async_close())
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self.logger.error(f"Error closing stream: {e}")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
loop.close()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
self.logger.error(f"Error closing event loop: {e}")
|
|
106
|
+
|
|
107
|
+
self.logger.info(f"Consumer worker stopped for camera {self.camera_id}")
|
|
108
|
+
|
|
109
|
+
async def _initialize_stream(self) -> None:
|
|
110
|
+
"""Initialize MatriceStream with proper configuration."""
|
|
111
|
+
try:
|
|
112
|
+
from matrice_common.stream.matrice_stream import MatriceStream, StreamType
|
|
113
|
+
|
|
114
|
+
stream_type = self._get_stream_type()
|
|
115
|
+
stream_params = self._build_stream_params(stream_type)
|
|
116
|
+
|
|
117
|
+
self.stream = MatriceStream(stream_type, **stream_params)
|
|
118
|
+
consumer_group = f"inference_consumer_{self.camera_id}_{self.worker_id}"
|
|
119
|
+
await self.stream.async_setup(self.input_topic, consumer_group)
|
|
120
|
+
|
|
121
|
+
self.logger.info(f"Initialized {stream_type.value} stream for consumer worker {self.worker_id}")
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.logger.error(f"Failed to initialize stream: {e}")
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
def _get_stream_type(self):
|
|
128
|
+
"""Determine stream type from configuration."""
|
|
129
|
+
from matrice_common.stream.matrice_stream import StreamType
|
|
130
|
+
stream_type_str = self.stream_config.get("stream_type", "kafka").lower()
|
|
131
|
+
return StreamType.KAFKA if stream_type_str == "kafka" else StreamType.REDIS
|
|
132
|
+
|
|
133
|
+
def _build_stream_params(self, stream_type) -> Dict[str, Any]:
|
|
134
|
+
"""Build stream parameters based on type."""
|
|
135
|
+
from matrice_common.stream.matrice_stream import StreamType
|
|
136
|
+
|
|
137
|
+
if stream_type == StreamType.KAFKA:
|
|
138
|
+
return {
|
|
139
|
+
"bootstrap_servers": self.stream_config.get("bootstrap_servers", "localhost:9092"),
|
|
140
|
+
"sasl_username": self.stream_config.get("sasl_username", "matrice-sdk-user"),
|
|
141
|
+
"sasl_password": self.stream_config.get("sasl_password", "matrice-sdk-password"),
|
|
142
|
+
"sasl_mechanism": self.stream_config.get("sasl_mechanism", "SCRAM-SHA-256"),
|
|
143
|
+
"security_protocol": self.stream_config.get("security_protocol", "SASL_PLAINTEXT"),
|
|
144
|
+
}
|
|
145
|
+
else:
|
|
146
|
+
return {
|
|
147
|
+
"host": self.stream_config.get("host", "localhost"),
|
|
148
|
+
"port": self.stream_config.get("port", 6379),
|
|
149
|
+
"password": self.stream_config.get("password"),
|
|
150
|
+
"username": self.stream_config.get("username"),
|
|
151
|
+
"db": self.stream_config.get("db", self.DEFAULT_DB),
|
|
152
|
+
"connection_timeout": self.stream_config.get("connection_timeout", self.DEFAULT_CONNECTION_TIMEOUT),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async def _get_message_safely(self) -> Optional[Dict[str, Any]]:
|
|
156
|
+
"""Safely get message from stream."""
|
|
157
|
+
if not self.stream:
|
|
158
|
+
self.logger.error("Stream not initialized")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
return await self.stream.async_get_message(self.message_timeout)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
self.logger.debug(f"Error getting message: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# -------------------- H.265 helpers --------------------
|
|
168
|
+
def _decode_h265_frame(self, h265_bytes: bytes, width: int, height: int):
|
|
169
|
+
"""Decode a single H.265-encoded frame to OpenCV BGR image."""
|
|
170
|
+
try:
|
|
171
|
+
try:
|
|
172
|
+
# Prefer local matrice_common implementation if available
|
|
173
|
+
from matrice_common.video.h265_processor import H265FrameDecoder
|
|
174
|
+
decoder = H265FrameDecoder()
|
|
175
|
+
frame = decoder.decode_frame(h265_bytes, width=width, height=height)
|
|
176
|
+
return frame
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self.logger.error(f"H.265 single-frame decode failed: {e}")
|
|
179
|
+
return None
|
|
180
|
+
except Exception as e:
|
|
181
|
+
self.logger.error(f"Unexpected error in H.265 frame decode: {e}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def _ensure_h265_stream_decoder(self, width: int, height: int):
|
|
185
|
+
"""Ensure a continuous H.265 stream decoder exists with given dimensions."""
|
|
186
|
+
if self._h265_stream_decoder is not None:
|
|
187
|
+
return True
|
|
188
|
+
try:
|
|
189
|
+
from matrice_common.video.h265_processor import H265StreamDecoder
|
|
190
|
+
decoder = H265StreamDecoder(width=width, height=height)
|
|
191
|
+
if not decoder.start():
|
|
192
|
+
self.logger.error("Failed to start H.265 stream decoder")
|
|
193
|
+
return False
|
|
194
|
+
self._h265_stream_decoder = decoder
|
|
195
|
+
return True
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.logger.error(f"Failed to initialize H.265 stream decoder: {e}")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def _frame_to_jpeg_bytes(self, frame) -> bytes:
|
|
201
|
+
"""Encode an OpenCV BGR frame to JPEG bytes."""
|
|
202
|
+
try:
|
|
203
|
+
ok, buf = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
|
|
204
|
+
if not ok:
|
|
205
|
+
raise RuntimeError("cv2.imencode failed")
|
|
206
|
+
return buf.tobytes()
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self.logger.error(f"Failed to encode frame to JPEG: {e}")
|
|
209
|
+
return b""
|
|
210
|
+
|
|
211
|
+
def _process_message(self, message_data: Dict[str, Any]) -> None:
|
|
212
|
+
"""Process incoming message and add to inference queue."""
|
|
213
|
+
try:
|
|
214
|
+
message_key = self._extract_message_key(message_data)
|
|
215
|
+
data = self._parse_message_data(message_data)
|
|
216
|
+
input_stream = self._extract_input_stream(data)
|
|
217
|
+
extra_params = self._normalize_extra_params(data)
|
|
218
|
+
frame_id = self._determine_frame_id(data, message_data)
|
|
219
|
+
|
|
220
|
+
self._enrich_input_stream(input_stream, frame_id)
|
|
221
|
+
|
|
222
|
+
# Codec detection
|
|
223
|
+
codec = None
|
|
224
|
+
codec_lower = None
|
|
225
|
+
try:
|
|
226
|
+
if isinstance(input_stream, dict):
|
|
227
|
+
codec = input_stream.get("video_codec") or input_stream.get("compression_format")
|
|
228
|
+
if isinstance(codec, str):
|
|
229
|
+
codec_lower = codec.lower()
|
|
230
|
+
except Exception:
|
|
231
|
+
codec_lower = None
|
|
232
|
+
|
|
233
|
+
# H.264 handling (frame-wise) - upstream always sends JPEG-encoded frames
|
|
234
|
+
# Content is base64-encoded JPEG, ready for PIL/inference
|
|
235
|
+
if codec_lower == "h264" and isinstance(input_stream, dict):
|
|
236
|
+
stream_unit = input_stream.get("stream_unit", "frame")
|
|
237
|
+
if isinstance(stream_unit, str) and stream_unit.lower() != "frame":
|
|
238
|
+
self.logger.warning("Received H.264 with non-frame stream_unit; skipping")
|
|
239
|
+
return
|
|
240
|
+
content_b64 = input_stream.get("content")
|
|
241
|
+
if isinstance(content_b64, str) and content_b64:
|
|
242
|
+
# Cache JPEG base64 as-is
|
|
243
|
+
self._cache_frame(frame_id, content_b64)
|
|
244
|
+
stream_msg = self._create_stream_message(message_key, data)
|
|
245
|
+
task_data = self._build_task_data(stream_msg, input_stream, extra_params, frame_id)
|
|
246
|
+
self.inference_queue.put((stream_msg.priority, time.time(), task_data))
|
|
247
|
+
return
|
|
248
|
+
self.logger.warning("H.264 frame missing content; skipping")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# H.265 handling: convert to JPEG base64 before enqueuing
|
|
252
|
+
if codec_lower in ["h265", "hevc"] and isinstance(input_stream, dict):
|
|
253
|
+
# Resolve resolution
|
|
254
|
+
width = None
|
|
255
|
+
height = None
|
|
256
|
+
try:
|
|
257
|
+
res = input_stream.get("stream_resolution") or input_stream.get("original_resolution") or {}
|
|
258
|
+
width = int(res.get("width")) if res and res.get("width") else None
|
|
259
|
+
height = int(res.get("height")) if res and res.get("height") else None
|
|
260
|
+
except Exception:
|
|
261
|
+
width, height = None, None
|
|
262
|
+
|
|
263
|
+
payload_b64 = input_stream.get("content")
|
|
264
|
+
payload_bytes = b""
|
|
265
|
+
if isinstance(payload_b64, str) and payload_b64:
|
|
266
|
+
try:
|
|
267
|
+
payload_bytes = base64.b64decode(payload_b64)
|
|
268
|
+
except Exception:
|
|
269
|
+
payload_bytes = b""
|
|
270
|
+
|
|
271
|
+
stream_unit = input_stream.get("stream_unit", "frame")
|
|
272
|
+
is_stream_chunk = bool(input_stream.get("is_video_chunk")) or (isinstance(stream_unit, str) and stream_unit.lower() != "frame")
|
|
273
|
+
|
|
274
|
+
stream_msg = self._create_stream_message(message_key, data)
|
|
275
|
+
|
|
276
|
+
if not is_stream_chunk:
|
|
277
|
+
# Single-frame H.265
|
|
278
|
+
if payload_bytes and width and height:
|
|
279
|
+
frame_img = self._decode_h265_frame(payload_bytes, width, height)
|
|
280
|
+
if frame_img is not None:
|
|
281
|
+
jpeg_bytes = self._frame_to_jpeg_bytes(frame_img)
|
|
282
|
+
if jpeg_bytes:
|
|
283
|
+
input_stream_jpeg = copy.deepcopy(input_stream)
|
|
284
|
+
input_stream_jpeg["content"] = base64.b64encode(jpeg_bytes).decode("utf-8")
|
|
285
|
+
input_stream_jpeg["video_codec"] = "jpeg"
|
|
286
|
+
# Low-latency cache write
|
|
287
|
+
self._cache_frame(frame_id, input_stream_jpeg["content"])
|
|
288
|
+
task_data = self._build_task_data(stream_msg, input_stream_jpeg, extra_params, frame_id)
|
|
289
|
+
self.inference_queue.put((stream_msg.priority, time.time(), task_data))
|
|
290
|
+
return
|
|
291
|
+
# Drop undecodable H.265 frame
|
|
292
|
+
self.logger.warning("Dropping H.265 frame due to missing payload/resolution or decode failure")
|
|
293
|
+
return
|
|
294
|
+
else:
|
|
295
|
+
# Stream-chunk H.265 (emit at most one frame per message using upstream frame_id)
|
|
296
|
+
if width and height and self._ensure_h265_stream_decoder(width, height) and payload_bytes:
|
|
297
|
+
try:
|
|
298
|
+
self._h265_stream_decoder.decode_bytes(payload_bytes)
|
|
299
|
+
latest_frame = None
|
|
300
|
+
while True:
|
|
301
|
+
frame_img = self._h265_stream_decoder.read_frame()
|
|
302
|
+
if frame_img is None:
|
|
303
|
+
break
|
|
304
|
+
latest_frame = frame_img
|
|
305
|
+
if latest_frame is not None:
|
|
306
|
+
jpeg_bytes = self._frame_to_jpeg_bytes(latest_frame)
|
|
307
|
+
if jpeg_bytes:
|
|
308
|
+
input_stream_jpeg = copy.deepcopy(input_stream)
|
|
309
|
+
input_stream_jpeg["content"] = base64.b64encode(jpeg_bytes).decode("utf-8")
|
|
310
|
+
input_stream_jpeg["video_codec"] = "jpeg"
|
|
311
|
+
# Keep upstream frame_id as-is
|
|
312
|
+
try:
|
|
313
|
+
input_stream_jpeg["frame_id"] = frame_id
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
# Low-latency cache write
|
|
317
|
+
self._cache_frame(frame_id, input_stream_jpeg["content"])
|
|
318
|
+
task_data = self._build_task_data(stream_msg, input_stream_jpeg, extra_params, frame_id)
|
|
319
|
+
self.inference_queue.put((stream_msg.priority, time.time(), task_data))
|
|
320
|
+
return
|
|
321
|
+
except Exception as e:
|
|
322
|
+
self.logger.error(f"H.265 stream decode error: {e}")
|
|
323
|
+
# No complete frame available yet for this chunk; skip forwarding
|
|
324
|
+
self.logger.debug("No decoded frame available from H.265 stream chunk for this message")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Default path (other formats): enqueue as-is
|
|
328
|
+
stream_msg = self._create_stream_message(message_key, data)
|
|
329
|
+
# Cache if there is a base64 content present
|
|
330
|
+
try:
|
|
331
|
+
if isinstance(input_stream, dict) and isinstance(input_stream.get("content"), str) and input_stream.get("content"):
|
|
332
|
+
self._cache_frame(frame_id, input_stream.get("content"))
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
task_data = self._build_task_data(stream_msg, input_stream, extra_params, frame_id)
|
|
336
|
+
self.inference_queue.put((stream_msg.priority, time.time(), task_data))
|
|
337
|
+
|
|
338
|
+
except json.JSONDecodeError as e:
|
|
339
|
+
self.logger.error(f"Failed to parse message JSON: {e}")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
self.logger.error(f"Error processing message: {e}")
|
|
342
|
+
|
|
343
|
+
def _extract_message_key(self, message_data: Dict[str, Any]) -> Optional[str]:
|
|
344
|
+
"""Extract message key from Kafka/Redis message."""
|
|
345
|
+
if not isinstance(message_data, dict):
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
key = message_data.get('key') or message_data.get('message_key')
|
|
349
|
+
if isinstance(key, bytes):
|
|
350
|
+
return key.decode('utf-8')
|
|
351
|
+
return key
|
|
352
|
+
|
|
353
|
+
def _parse_message_data(self, message_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
354
|
+
"""Parse message data from different stream formats."""
|
|
355
|
+
for field in ['value', 'data']:
|
|
356
|
+
if field in message_data:
|
|
357
|
+
value = message_data[field]
|
|
358
|
+
if isinstance(value, dict):
|
|
359
|
+
return value
|
|
360
|
+
elif isinstance(value, (str, bytes)):
|
|
361
|
+
if isinstance(value, bytes):
|
|
362
|
+
value = value.decode('utf-8')
|
|
363
|
+
return json.loads(value)
|
|
364
|
+
return message_data
|
|
365
|
+
|
|
366
|
+
def _extract_input_stream(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
367
|
+
"""Extract input stream from message data."""
|
|
368
|
+
input_stream = data.get("input_stream", {})
|
|
369
|
+
return input_stream if input_stream else data
|
|
370
|
+
|
|
371
|
+
def _normalize_extra_params(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
372
|
+
"""Normalize extra_params to ensure it's a dictionary."""
|
|
373
|
+
extra_params = data.get("extra_params", {})
|
|
374
|
+
|
|
375
|
+
if isinstance(extra_params, dict):
|
|
376
|
+
return extra_params
|
|
377
|
+
elif isinstance(extra_params, list):
|
|
378
|
+
return self._merge_list_params(extra_params)
|
|
379
|
+
else:
|
|
380
|
+
self.logger.warning(f"Invalid extra_params type {type(extra_params)}, using empty dict")
|
|
381
|
+
return {}
|
|
382
|
+
|
|
383
|
+
def _merge_list_params(self, params_list: list) -> Dict[str, Any]:
|
|
384
|
+
"""Merge list of dictionaries into single dictionary."""
|
|
385
|
+
if not params_list:
|
|
386
|
+
return {}
|
|
387
|
+
|
|
388
|
+
if all(isinstance(item, dict) for item in params_list):
|
|
389
|
+
merged = {}
|
|
390
|
+
for item in params_list:
|
|
391
|
+
merged.update(item)
|
|
392
|
+
return merged
|
|
393
|
+
|
|
394
|
+
return {}
|
|
395
|
+
|
|
396
|
+
def _determine_frame_id(self, data: Dict[str, Any], message_data: Dict[str, Any]) -> str:
|
|
397
|
+
"""Determine frame ID from message data."""
|
|
398
|
+
frame_id = data.get("frame_id")
|
|
399
|
+
if frame_id:
|
|
400
|
+
return frame_id
|
|
401
|
+
|
|
402
|
+
fallback_key = message_data.get("key") or data.get("input_name")
|
|
403
|
+
if fallback_key:
|
|
404
|
+
return str(fallback_key)
|
|
405
|
+
|
|
406
|
+
return f"{self.camera_id}_{int(time.time() * 1000)}"
|
|
407
|
+
|
|
408
|
+
def _enrich_input_stream(self, input_stream: Dict[str, Any], frame_id: str) -> None:
|
|
409
|
+
"""Add frame_id to input_stream if not present."""
|
|
410
|
+
try:
|
|
411
|
+
if isinstance(input_stream, dict) and "frame_id" not in input_stream:
|
|
412
|
+
input_stream["frame_id"] = frame_id
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
def _create_stream_message(self, message_key: Optional[str], data: Dict[str, Any]) -> StreamMessage:
|
|
417
|
+
"""Create StreamMessage instance."""
|
|
418
|
+
final_key = message_key or data.get("input_name") or f"{self.camera_id}_{int(time.time())}"
|
|
419
|
+
|
|
420
|
+
return StreamMessage(
|
|
421
|
+
camera_id=self.camera_id,
|
|
422
|
+
message_key=final_key,
|
|
423
|
+
data=data,
|
|
424
|
+
timestamp=datetime.now(timezone.utc),
|
|
425
|
+
priority=self.DEFAULT_PRIORITY
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _build_task_data(self, stream_msg: StreamMessage, input_stream: Dict[str, Any],
|
|
429
|
+
extra_params: Dict[str, Any], frame_id: str) -> Dict[str, Any]:
|
|
430
|
+
"""Build task data for inference queue."""
|
|
431
|
+
return {
|
|
432
|
+
"message": stream_msg,
|
|
433
|
+
"input_stream": input_stream,
|
|
434
|
+
"stream_key": stream_msg.message_key,
|
|
435
|
+
"extra_params": extra_params,
|
|
436
|
+
"camera_config": self.camera_config.__dict__,
|
|
437
|
+
"frame_id": frame_id
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def _cache_frame(self, frame_id: Optional[str], content_b64: Optional[str]) -> None:
|
|
441
|
+
"""Write frame to Redis cache if configured, non-blocking.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
frame_id: Unique frame identifier (uuid expected)
|
|
445
|
+
content_b64: Base64-encoded JPEG string
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
if not self.frame_cache:
|
|
449
|
+
return
|
|
450
|
+
if not frame_id or not isinstance(frame_id, str):
|
|
451
|
+
return
|
|
452
|
+
if not content_b64 or not isinstance(content_b64, str):
|
|
453
|
+
return
|
|
454
|
+
self.frame_cache.put(frame_id, content_b64)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
# Do not block pipeline on cache errors
|
|
457
|
+
self.logger.debug(f"Frame cache put failed for frame_id={frame_id}: {e}")
|
|
458
|
+
|