matrice-inference 0.1.2__py3-none-any.whl
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/__init__.py +72 -0
- matrice_inference/py.typed +0 -0
- matrice_inference/server/__init__.py +23 -0
- matrice_inference/server/inference_interface.py +176 -0
- matrice_inference/server/model/__init__.py +1 -0
- matrice_inference/server/model/model_manager.py +274 -0
- matrice_inference/server/model/model_manager_wrapper.py +550 -0
- matrice_inference/server/model/triton_model_manager.py +290 -0
- matrice_inference/server/model/triton_server.py +1248 -0
- matrice_inference/server/proxy_interface.py +371 -0
- matrice_inference/server/server.py +1004 -0
- matrice_inference/server/stream/__init__.py +0 -0
- matrice_inference/server/stream/app_deployment.py +228 -0
- matrice_inference/server/stream/consumer_worker.py +201 -0
- matrice_inference/server/stream/frame_cache.py +127 -0
- matrice_inference/server/stream/inference_worker.py +163 -0
- matrice_inference/server/stream/post_processing_worker.py +230 -0
- matrice_inference/server/stream/producer_worker.py +147 -0
- matrice_inference/server/stream/stream_pipeline.py +451 -0
- matrice_inference/server/stream/utils.py +23 -0
- matrice_inference/tmp/abstract_model_manager.py +58 -0
- matrice_inference/tmp/aggregator/__init__.py +18 -0
- matrice_inference/tmp/aggregator/aggregator.py +330 -0
- matrice_inference/tmp/aggregator/analytics.py +906 -0
- matrice_inference/tmp/aggregator/ingestor.py +438 -0
- matrice_inference/tmp/aggregator/latency.py +597 -0
- matrice_inference/tmp/aggregator/pipeline.py +968 -0
- matrice_inference/tmp/aggregator/publisher.py +431 -0
- matrice_inference/tmp/aggregator/synchronizer.py +594 -0
- matrice_inference/tmp/batch_manager.py +239 -0
- matrice_inference/tmp/overall_inference_testing.py +338 -0
- matrice_inference/tmp/triton_utils.py +638 -0
- matrice_inference-0.1.2.dist-info/METADATA +28 -0
- matrice_inference-0.1.2.dist-info/RECORD +37 -0
- matrice_inference-0.1.2.dist-info/WHEEL +5 -0
- matrice_inference-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
- matrice_inference-0.1.2.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from matrice_common.session import Session
|
|
6
|
+
from matrice_inference.server.stream.utils import CameraConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppDeployment:
|
|
10
|
+
"""Handles app deployment configuration and camera setup for streaming pipeline."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, session: Session, app_deployment_id: str, connection_timeout: int = 1200): # Increased from 300 to 1200
|
|
13
|
+
self.app_deployment_id = app_deployment_id
|
|
14
|
+
self.rpc = session.rpc
|
|
15
|
+
self.session = session
|
|
16
|
+
self.connection_timeout = connection_timeout
|
|
17
|
+
self.logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
def get_input_topics(self) -> List[Dict]:
|
|
20
|
+
"""Get input topics for the app deployment."""
|
|
21
|
+
try:
|
|
22
|
+
response = self.rpc.get(f"/v1/inference/get_input_topics_by_app_deployment_id/{self.app_deployment_id}")
|
|
23
|
+
if response.get("success", False):
|
|
24
|
+
return response.get("data", [])
|
|
25
|
+
else:
|
|
26
|
+
self.logger.error(f"Failed to get input topics: {response.get('message', 'Unknown error')}")
|
|
27
|
+
return []
|
|
28
|
+
except Exception as e:
|
|
29
|
+
self.logger.error(f"Exception getting input topics: {str(e)}")
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
def get_output_topics(self) -> List[Dict]:
|
|
33
|
+
"""Get output topics for the app deployment."""
|
|
34
|
+
try:
|
|
35
|
+
response = self.rpc.get(f"/v1/inference/get_output_topics_by_app_deployment_id/{self.app_deployment_id}")
|
|
36
|
+
if response.get("success", False):
|
|
37
|
+
return response.get("data", [])
|
|
38
|
+
else:
|
|
39
|
+
self.logger.error(f"Failed to get output topics: {response.get('message', 'Unknown error')}")
|
|
40
|
+
return []
|
|
41
|
+
except Exception as e:
|
|
42
|
+
self.logger.error(f"Exception getting output topics: {str(e)}")
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
def get_camera_configs(self) -> Dict[str, CameraConfig]:
|
|
46
|
+
"""
|
|
47
|
+
Get camera configurations for the streaming pipeline.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dict[str, CameraConfig]: Dictionary mapping camera_id to CameraConfig
|
|
51
|
+
"""
|
|
52
|
+
camera_configs = {}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Get input and output topics
|
|
56
|
+
input_topics = self.get_input_topics()
|
|
57
|
+
output_topics = self.get_output_topics()
|
|
58
|
+
|
|
59
|
+
if not input_topics:
|
|
60
|
+
self.logger.warning("No input topics found for app deployment")
|
|
61
|
+
return camera_configs
|
|
62
|
+
|
|
63
|
+
# Create mapping of camera_id to output topic
|
|
64
|
+
output_topic_map = {}
|
|
65
|
+
for output_topic in output_topics:
|
|
66
|
+
camera_id = output_topic.get("cameraId")
|
|
67
|
+
if camera_id:
|
|
68
|
+
output_topic_map[camera_id] = output_topic
|
|
69
|
+
|
|
70
|
+
# Process each input topic to create camera config
|
|
71
|
+
for input_topic in input_topics:
|
|
72
|
+
try:
|
|
73
|
+
camera_id = input_topic.get("cameraId")
|
|
74
|
+
if not camera_id:
|
|
75
|
+
self.logger.warning("Input topic missing camera ID, skipping")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Get corresponding output topic
|
|
79
|
+
output_topic = output_topic_map.get(camera_id)
|
|
80
|
+
if not output_topic:
|
|
81
|
+
self.logger.warning(f"No output topic found for camera {camera_id}, skipping")
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Get connection info for this server
|
|
85
|
+
server_id = input_topic.get("serverId")
|
|
86
|
+
server_type = input_topic.get("serverType", "redis").lower()
|
|
87
|
+
|
|
88
|
+
if not server_id:
|
|
89
|
+
self.logger.warning(f"No server ID found for camera {camera_id}, skipping")
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
connection_info = self.get_and_wait_for_connection_info(server_type, server_id)
|
|
93
|
+
if not connection_info:
|
|
94
|
+
self.logger.error(f"Could not get connection info for camera {camera_id}, skipping")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Create stream config
|
|
98
|
+
stream_config = connection_info.copy()
|
|
99
|
+
stream_config["stream_type"] = server_type
|
|
100
|
+
|
|
101
|
+
# Create camera config
|
|
102
|
+
camera_config = CameraConfig(
|
|
103
|
+
camera_id=camera_id,
|
|
104
|
+
input_topic=input_topic.get("topicName"),
|
|
105
|
+
output_topic=output_topic.get("topicName"),
|
|
106
|
+
stream_config=stream_config,
|
|
107
|
+
enabled=True
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
camera_configs[camera_id] = camera_config
|
|
111
|
+
self.logger.info(f"Created camera config for {camera_id} using {server_type}")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self.logger.error(f"Error creating config for camera {camera_id}: {str(e)}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
self.logger.info(f"Successfully created {len(camera_configs)} camera configurations")
|
|
118
|
+
return camera_configs
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self.logger.error(f"Error getting camera configs: {str(e)}")
|
|
122
|
+
return camera_configs
|
|
123
|
+
|
|
124
|
+
def get_and_wait_for_connection_info(self, server_type: str, server_id: str) -> Optional[Dict]:
|
|
125
|
+
"""Get the connection information for the streaming gateway."""
|
|
126
|
+
def _get_kafka_connection_info():
|
|
127
|
+
try:
|
|
128
|
+
response = self.rpc.get(f"/v1/actions/get_kafka_server/{server_id}")
|
|
129
|
+
if response.get("success", False):
|
|
130
|
+
data = response.get("data")
|
|
131
|
+
if (
|
|
132
|
+
data
|
|
133
|
+
and data.get("ipAddress")
|
|
134
|
+
and data.get("port")
|
|
135
|
+
and data.get("status") == "running"
|
|
136
|
+
):
|
|
137
|
+
return {
|
|
138
|
+
'bootstrap_servers': f'{data["ipAddress"]}:{data["port"]}',
|
|
139
|
+
'sasl_mechanism': 'SCRAM-SHA-256',
|
|
140
|
+
'sasl_username': 'matrice-sdk-user',
|
|
141
|
+
'sasl_password': 'matrice-sdk-password',
|
|
142
|
+
'security_protocol': 'SASL_PLAINTEXT'
|
|
143
|
+
}
|
|
144
|
+
else:
|
|
145
|
+
self.logger.debug("Kafka connection information is not complete, waiting...")
|
|
146
|
+
return None
|
|
147
|
+
else:
|
|
148
|
+
self.logger.debug("Failed to get Kafka connection information: %s", response.get("message", "Unknown error"))
|
|
149
|
+
return None
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
self.logger.debug("Exception getting Kafka connection info: %s", str(exc))
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _get_redis_connection_info():
|
|
155
|
+
try:
|
|
156
|
+
response = self.rpc.get(f"/v1/actions/redis_servers/{server_id}")
|
|
157
|
+
if response.get("success", False):
|
|
158
|
+
data = response.get("data")
|
|
159
|
+
if (
|
|
160
|
+
data
|
|
161
|
+
and data.get("host")
|
|
162
|
+
and data.get("port")
|
|
163
|
+
and data.get("status") == "running"
|
|
164
|
+
):
|
|
165
|
+
return {
|
|
166
|
+
'host': data["host"],
|
|
167
|
+
'port': int(data["port"]),
|
|
168
|
+
'password': data.get("password", ""),
|
|
169
|
+
'username': data.get("username"),
|
|
170
|
+
'db': data.get("db", 0),
|
|
171
|
+
'connection_timeout': 120 # Increased from 30 to 120
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
self.logger.debug("Redis connection information is not complete, waiting...")
|
|
175
|
+
return None
|
|
176
|
+
else:
|
|
177
|
+
self.logger.debug("Failed to get Redis connection information: %s", response.get("message", "Unknown error"))
|
|
178
|
+
return None
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
self.logger.debug("Exception getting Redis connection info: %s", str(exc))
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
last_log_time = 0
|
|
185
|
+
|
|
186
|
+
while True:
|
|
187
|
+
current_time = time.time()
|
|
188
|
+
|
|
189
|
+
# Get connection info based on server type
|
|
190
|
+
connection_info = None
|
|
191
|
+
if server_type == "kafka":
|
|
192
|
+
connection_info = _get_kafka_connection_info()
|
|
193
|
+
elif server_type == "redis":
|
|
194
|
+
connection_info = _get_redis_connection_info()
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Unsupported server type: {server_type}")
|
|
197
|
+
|
|
198
|
+
# If we got valid connection info, return it
|
|
199
|
+
if connection_info:
|
|
200
|
+
self.logger.info("Successfully retrieved %s connection information", server_type)
|
|
201
|
+
return connection_info
|
|
202
|
+
|
|
203
|
+
# Check timeout
|
|
204
|
+
if current_time - start_time > self.connection_timeout:
|
|
205
|
+
error_msg = f"Timeout waiting for {server_type} connection information after {self.connection_timeout} seconds"
|
|
206
|
+
self.logger.error(error_msg)
|
|
207
|
+
|
|
208
|
+
# Log the last response for debugging
|
|
209
|
+
try:
|
|
210
|
+
if server_type == "kafka":
|
|
211
|
+
response = self.rpc.get(f"/v1/actions/get_kafka_server/{server_id}")
|
|
212
|
+
else:
|
|
213
|
+
response = self.rpc.get(f"/v1/actions/redis_servers/{server_id}")
|
|
214
|
+
self.logger.error("Last response received: %s", response)
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
self.logger.error("Failed to get last response for debugging: %s", str(exc))
|
|
217
|
+
|
|
218
|
+
return None # Return None instead of raising exception to allow graceful handling
|
|
219
|
+
|
|
220
|
+
# Log waiting message every 10 seconds to avoid spam
|
|
221
|
+
if current_time - last_log_time >= 10:
|
|
222
|
+
elapsed = current_time - start_time
|
|
223
|
+
remaining = self.connection_timeout - elapsed
|
|
224
|
+
self.logger.info("Waiting for %s connection information... (%.1fs elapsed, %.1fs remaining)",
|
|
225
|
+
server_type, elapsed, remaining)
|
|
226
|
+
last_log_time = current_time
|
|
227
|
+
|
|
228
|
+
time.sleep(1)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Import moved to method where it's needed to avoid circular imports
|
|
2
|
+
from matrice_inference.server.stream.utils import CameraConfig, StreamMessage
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import queue
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
class ConsumerWorker:
|
|
13
|
+
"""Handles message consumption from streams."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, camera_id: str, worker_id: int, stream_config: dict, input_topic: str,
|
|
16
|
+
inference_queue: queue.PriorityQueue, message_timeout: float,
|
|
17
|
+
camera_config: CameraConfig):
|
|
18
|
+
self.camera_id = camera_id
|
|
19
|
+
self.worker_id = worker_id
|
|
20
|
+
self.stream_config = stream_config
|
|
21
|
+
self.input_topic = input_topic
|
|
22
|
+
self.inference_queue = inference_queue
|
|
23
|
+
self.message_timeout = message_timeout
|
|
24
|
+
self.camera_config = camera_config
|
|
25
|
+
self.running = False
|
|
26
|
+
self.stream = None # Will be created in worker thread's event loop
|
|
27
|
+
self.logger = logging.getLogger(f"{__name__}.consumer.{camera_id}.{worker_id}")
|
|
28
|
+
|
|
29
|
+
def start(self):
|
|
30
|
+
"""Start the consumer worker in a separate thread."""
|
|
31
|
+
self.running = True
|
|
32
|
+
thread = threading.Thread(target=self._run, name=f"Consumer-{self.camera_id}-{self.worker_id}", daemon=False)
|
|
33
|
+
thread.start()
|
|
34
|
+
return thread
|
|
35
|
+
|
|
36
|
+
def stop(self):
|
|
37
|
+
"""Stop the consumer worker."""
|
|
38
|
+
self.running = False
|
|
39
|
+
|
|
40
|
+
def _run(self):
|
|
41
|
+
"""Main consumer loop."""
|
|
42
|
+
# Create a new event loop for this worker thread
|
|
43
|
+
loop = asyncio.new_event_loop()
|
|
44
|
+
asyncio.set_event_loop(loop)
|
|
45
|
+
|
|
46
|
+
self.logger.info(f"Started consumer worker for camera {self.camera_id}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# Initialize stream in this event loop
|
|
50
|
+
loop.run_until_complete(self._initialize_stream())
|
|
51
|
+
|
|
52
|
+
while self.running and self.camera_config.enabled:
|
|
53
|
+
try:
|
|
54
|
+
# Get message from stream
|
|
55
|
+
message_data = loop.run_until_complete(
|
|
56
|
+
self._get_message_safely()
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not message_data:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Parse and create task
|
|
63
|
+
self._process_message(message_data)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.error(f"Consumer error: {e}")
|
|
67
|
+
time.sleep(1.0)
|
|
68
|
+
|
|
69
|
+
finally:
|
|
70
|
+
# Clean up stream
|
|
71
|
+
if self.stream:
|
|
72
|
+
try:
|
|
73
|
+
loop.run_until_complete(self.stream.async_close())
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.error(f"Error closing stream: {e}")
|
|
76
|
+
loop.close()
|
|
77
|
+
self.logger.info(f"Consumer worker stopped for camera {self.camera_id}")
|
|
78
|
+
|
|
79
|
+
async def _initialize_stream(self):
|
|
80
|
+
"""Initialize MatriceStream in the current event loop."""
|
|
81
|
+
try:
|
|
82
|
+
from matrice_common.stream.matrice_stream import MatriceStream, StreamType
|
|
83
|
+
|
|
84
|
+
# Determine stream type
|
|
85
|
+
stream_type = StreamType.KAFKA if self.stream_config.get("stream_type", "kafka").lower() == "kafka" else StreamType.REDIS
|
|
86
|
+
|
|
87
|
+
# Create stream configuration
|
|
88
|
+
if stream_type == StreamType.KAFKA:
|
|
89
|
+
stream_params = {
|
|
90
|
+
"bootstrap_servers": self.stream_config.get("bootstrap_servers", "localhost:9092"),
|
|
91
|
+
"sasl_username": self.stream_config.get("sasl_username", "matrice-sdk-user"),
|
|
92
|
+
"sasl_password": self.stream_config.get("sasl_password", "matrice-sdk-password"),
|
|
93
|
+
"sasl_mechanism": self.stream_config.get("sasl_mechanism", "SCRAM-SHA-256"),
|
|
94
|
+
"security_protocol": self.stream_config.get("security_protocol", "SASL_PLAINTEXT"),
|
|
95
|
+
}
|
|
96
|
+
else: # Redis
|
|
97
|
+
stream_params = {
|
|
98
|
+
"host": self.stream_config.get("host", "localhost"),
|
|
99
|
+
"port": self.stream_config.get("port", 6379),
|
|
100
|
+
"password": self.stream_config.get("password"),
|
|
101
|
+
"username": self.stream_config.get("username"),
|
|
102
|
+
"db": self.stream_config.get("db", 0),
|
|
103
|
+
"connection_timeout": self.stream_config.get("connection_timeout", 120),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Create and setup stream
|
|
107
|
+
self.stream = MatriceStream(stream_type, **stream_params)
|
|
108
|
+
await self.stream.async_setup(self.input_topic, f"inference_consumer_{self.camera_id}_{self.worker_id}")
|
|
109
|
+
# TODO: Add app name to the consumer group id to make sure it processing once only
|
|
110
|
+
|
|
111
|
+
self.logger.info(f"Initialized {stream_type.value} stream for consumer worker {self.worker_id}")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self.logger.error(f"Failed to initialize stream for consumer worker: {e}")
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
async def _get_message_safely(self):
|
|
118
|
+
"""Safely get message from stream in the current event loop."""
|
|
119
|
+
try:
|
|
120
|
+
if not self.stream:
|
|
121
|
+
self.logger.error("Stream not initialized")
|
|
122
|
+
return None
|
|
123
|
+
return await self.stream.async_get_message(self.message_timeout)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
# Handle stream issues gracefully
|
|
126
|
+
self.logger.debug(f"Error getting message from stream: {e}")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def _process_message(self, message_data):
|
|
130
|
+
"""Process incoming message and add to inference queue."""
|
|
131
|
+
try:
|
|
132
|
+
# Parse message data - handle camera streamer format
|
|
133
|
+
if isinstance(message_data.get("data"), bytes):
|
|
134
|
+
data = json.loads(message_data["data"].decode("utf-8"))
|
|
135
|
+
else:
|
|
136
|
+
data = message_data.get("data", {})
|
|
137
|
+
|
|
138
|
+
# Handle camera streamer input format
|
|
139
|
+
input_stream = data.get("input_stream", {})
|
|
140
|
+
if not input_stream:
|
|
141
|
+
# Fallback to direct format
|
|
142
|
+
input_stream = data
|
|
143
|
+
|
|
144
|
+
# Create stream message
|
|
145
|
+
stream_msg = StreamMessage(
|
|
146
|
+
camera_id=self.camera_id,
|
|
147
|
+
message_key=message_data.get("key", data.get("input_name", f"{self.camera_id}_{int(time.time())}")),
|
|
148
|
+
data=data,
|
|
149
|
+
timestamp=datetime.now(timezone.utc),
|
|
150
|
+
priority=1
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Ensure extra_params is a dictionary
|
|
154
|
+
extra_params = data.get("extra_params", {})
|
|
155
|
+
if not isinstance(extra_params, dict):
|
|
156
|
+
self.logger.warning(f"extra_params is not a dict, converting from {type(extra_params)}: {extra_params}")
|
|
157
|
+
if isinstance(extra_params, list):
|
|
158
|
+
# Convert list to dict if possible
|
|
159
|
+
if len(extra_params) == 0:
|
|
160
|
+
extra_params = {}
|
|
161
|
+
elif all(isinstance(item, dict) for item in extra_params):
|
|
162
|
+
# Merge all dictionaries in the list
|
|
163
|
+
merged_params = {}
|
|
164
|
+
for item in extra_params:
|
|
165
|
+
merged_params.update(item)
|
|
166
|
+
extra_params = merged_params
|
|
167
|
+
else:
|
|
168
|
+
extra_params = {}
|
|
169
|
+
else:
|
|
170
|
+
extra_params = {}
|
|
171
|
+
|
|
172
|
+
# Determine frame_id (prefer value from upstream gateway; otherwise fallback to message key)
|
|
173
|
+
frame_id = data.get("frame_id")
|
|
174
|
+
if not frame_id:
|
|
175
|
+
frame_id = message_data.get("key", data.get("input_name", f"{self.camera_id}_{int(time.time() * 1000)}"))
|
|
176
|
+
|
|
177
|
+
# Attach frame_id into input_stream for propagation if not present
|
|
178
|
+
try:
|
|
179
|
+
if isinstance(input_stream, dict) and "frame_id" not in input_stream:
|
|
180
|
+
input_stream["frame_id"] = frame_id
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Create inference task with camera streamer format
|
|
185
|
+
task_data = {
|
|
186
|
+
"message": stream_msg,
|
|
187
|
+
"input_stream": input_stream, # Pass the full input_stream
|
|
188
|
+
"stream_key": f"{self.camera_id}_{stream_msg.message_key}",
|
|
189
|
+
"extra_params": extra_params,
|
|
190
|
+
"camera_config": self.camera_config.__dict__,
|
|
191
|
+
"frame_id": frame_id
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Add to inference queue with timestamp as tie-breaker for priority queue comparison
|
|
195
|
+
self.inference_queue.put((stream_msg.priority, time.time(), task_data))
|
|
196
|
+
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
self.logger.error(f"Failed to parse message JSON: {e}")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self.logger.error(f"Error processing message: {e}")
|
|
201
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import queue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import redis # type: ignore
|
|
8
|
+
except Exception: # pragma: no cover
|
|
9
|
+
redis = None # type: ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RedisFrameCache:
|
|
13
|
+
"""Non-blocking Redis cache for frames keyed by frame_id.
|
|
14
|
+
|
|
15
|
+
Stores base64 string content under key 'stream:frames:{frame_id}' with field 'frame'.
|
|
16
|
+
Each insert sets or refreshes the TTL.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
host: str = "localhost",
|
|
22
|
+
port: int = 6379,
|
|
23
|
+
db: int = 0,
|
|
24
|
+
password: str = None,
|
|
25
|
+
username: str = None,
|
|
26
|
+
ttl_seconds: int = 300,
|
|
27
|
+
prefix: str = "stream:frames:",
|
|
28
|
+
max_queue: int = 10000,
|
|
29
|
+
worker_threads: int = 2,
|
|
30
|
+
connect_timeout: float = 2.0,
|
|
31
|
+
socket_timeout: float = 0.5,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.logger = logging.getLogger(__name__ + ".frame_cache")
|
|
34
|
+
self.ttl_seconds = int(ttl_seconds)
|
|
35
|
+
self.prefix = prefix
|
|
36
|
+
self.queue: "queue.Queue" = queue.Queue(maxsize=max_queue)
|
|
37
|
+
self.threads = []
|
|
38
|
+
self.running = False
|
|
39
|
+
self._client = None
|
|
40
|
+
self._worker_threads = max(1, int(worker_threads))
|
|
41
|
+
|
|
42
|
+
if redis is None:
|
|
43
|
+
self.logger.warning("redis package not installed; frame caching disabled")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
self._client = redis.Redis(
|
|
48
|
+
host=host,
|
|
49
|
+
port=port,
|
|
50
|
+
db=db,
|
|
51
|
+
password=password,
|
|
52
|
+
username=username,
|
|
53
|
+
socket_connect_timeout=connect_timeout,
|
|
54
|
+
socket_timeout=socket_timeout,
|
|
55
|
+
health_check_interval=30,
|
|
56
|
+
retry_on_timeout=True,
|
|
57
|
+
decode_responses=True, # store strings directly
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self.logger.warning("Failed to init Redis client: %s", e)
|
|
61
|
+
self._client = None
|
|
62
|
+
|
|
63
|
+
def start(self) -> None:
|
|
64
|
+
if not self._client or self.running:
|
|
65
|
+
return
|
|
66
|
+
self.running = True
|
|
67
|
+
for i in range(self._worker_threads):
|
|
68
|
+
t = threading.Thread(target=self._worker, name=f"FrameCache-{i}", daemon=True)
|
|
69
|
+
t.start()
|
|
70
|
+
self.threads.append(t)
|
|
71
|
+
|
|
72
|
+
def stop(self) -> None:
|
|
73
|
+
if not self.running:
|
|
74
|
+
return
|
|
75
|
+
self.running = False
|
|
76
|
+
for _ in self.threads:
|
|
77
|
+
try:
|
|
78
|
+
self.queue.put_nowait(None)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
for t in self.threads:
|
|
82
|
+
try:
|
|
83
|
+
t.join(timeout=2.0)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
self.threads.clear()
|
|
87
|
+
|
|
88
|
+
def put(self, frame_id: str, base64_content: str) -> None:
|
|
89
|
+
"""Enqueue a cache write for the given frame.
|
|
90
|
+
|
|
91
|
+
- frame_id: unique identifier
|
|
92
|
+
- base64_content: base64-encoded image string
|
|
93
|
+
"""
|
|
94
|
+
if not self._client or not self.running:
|
|
95
|
+
return
|
|
96
|
+
if not frame_id or not base64_content:
|
|
97
|
+
return
|
|
98
|
+
try:
|
|
99
|
+
key = f"{self.prefix}{frame_id}"
|
|
100
|
+
self.queue.put_nowait((key, base64_content))
|
|
101
|
+
except queue.Full:
|
|
102
|
+
# Drop silently; never block pipeline
|
|
103
|
+
self.logger.debug("Frame cache queue full; dropping frame_id=%s", frame_id)
|
|
104
|
+
|
|
105
|
+
def _worker(self) -> None:
|
|
106
|
+
while self.running:
|
|
107
|
+
try:
|
|
108
|
+
item = self.queue.get(timeout=0.5)
|
|
109
|
+
except queue.Empty:
|
|
110
|
+
continue
|
|
111
|
+
if item is None:
|
|
112
|
+
break
|
|
113
|
+
key, base64_content = item
|
|
114
|
+
try:
|
|
115
|
+
# Store base64 string in a Redis hash field 'frame', then set TTL
|
|
116
|
+
# Mimics the Go backend behavior
|
|
117
|
+
self._client.hset(key, "frame", base64_content)
|
|
118
|
+
self._client.expire(key, self.ttl_seconds)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
self.logger.debug("Failed to cache frame %s: %s", key, e)
|
|
121
|
+
finally:
|
|
122
|
+
try:
|
|
123
|
+
self.queue.task_done()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|