rocket-welder-sdk 1.1.38.dev22__py3-none-any.whl → 1.1.38.dev23__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.
- rocket_welder_sdk/__init__.py +20 -0
- rocket_welder_sdk/controllers.py +7 -0
- rocket_welder_sdk/gst_metadata.py +48 -4
- rocket_welder_sdk/high_level/frame_sink_factory.py +2 -2
- rocket_welder_sdk/rocket_welder_client.py +202 -0
- rocket_welder_sdk/segmentation_result.py +132 -0
- rocket_welder_sdk/transport/unix_socket_transport.py +51 -1
- {rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/METADATA +1 -1
- {rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/RECORD +11 -11
- {rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/top_level.txt +0 -0
rocket_welder_sdk/__init__.py
CHANGED
|
@@ -12,9 +12,21 @@ from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
|
12
12
|
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
13
13
|
from .frame_metadata import FRAME_METADATA_SIZE, FrameMetadata, GstVideoFormat
|
|
14
14
|
from .gst_metadata import GstCaps, GstMetadata
|
|
15
|
+
from .keypoints_protocol import (
|
|
16
|
+
IKeyPointsSink,
|
|
17
|
+
IKeyPointsWriter,
|
|
18
|
+
KeyPointsSink,
|
|
19
|
+
KeyPointsWriter,
|
|
20
|
+
)
|
|
15
21
|
from .opencv_controller import OpenCvController
|
|
16
22
|
from .periodic_timer import PeriodicTimer, PeriodicTimerSync
|
|
17
23
|
from .rocket_welder_client import RocketWelderClient
|
|
24
|
+
from .segmentation_result import (
|
|
25
|
+
ISegmentationResultSink,
|
|
26
|
+
ISegmentationResultWriter,
|
|
27
|
+
SegmentationResultSink,
|
|
28
|
+
SegmentationResultWriter,
|
|
29
|
+
)
|
|
18
30
|
from .session_id import (
|
|
19
31
|
# Explicit URL functions (PREFERRED - set by rocket-welder2)
|
|
20
32
|
ACTIONS_SINK_URL_ENV,
|
|
@@ -74,12 +86,20 @@ __all__ = [
|
|
|
74
86
|
"GstMetadata",
|
|
75
87
|
"GstVideoFormat",
|
|
76
88
|
"IController",
|
|
89
|
+
"IKeyPointsSink",
|
|
90
|
+
"IKeyPointsWriter",
|
|
91
|
+
"ISegmentationResultSink",
|
|
92
|
+
"ISegmentationResultWriter",
|
|
93
|
+
"KeyPointsSink",
|
|
94
|
+
"KeyPointsWriter",
|
|
77
95
|
"OneWayShmController",
|
|
78
96
|
"OpenCvController",
|
|
79
97
|
"PeriodicTimer",
|
|
80
98
|
"PeriodicTimerSync",
|
|
81
99
|
"Protocol",
|
|
82
100
|
"RocketWelderClient",
|
|
101
|
+
"SegmentationResultSink",
|
|
102
|
+
"SegmentationResultWriter",
|
|
83
103
|
"get_actions_url",
|
|
84
104
|
"get_actions_url_from_env",
|
|
85
105
|
"get_configured_nng_urls",
|
rocket_welder_sdk/controllers.py
CHANGED
|
@@ -758,6 +758,13 @@ class DuplexShmController(IController):
|
|
|
758
758
|
|
|
759
759
|
# GstCaps must be available for width/height/format
|
|
760
760
|
# (FrameMetadata no longer contains these - they're stream-level, not per-frame)
|
|
761
|
+
# If not available yet, try to read metadata again (race condition with C# Server)
|
|
762
|
+
if not self._gst_caps and self._duplex_server and self._duplex_server.request_reader:
|
|
763
|
+
logger.debug("GstCaps not available, attempting to read metadata again...")
|
|
764
|
+
metadata = self._duplex_server.request_reader.get_metadata()
|
|
765
|
+
if metadata:
|
|
766
|
+
self._on_metadata(metadata)
|
|
767
|
+
|
|
761
768
|
if not self._gst_caps:
|
|
762
769
|
logger.warning(
|
|
763
770
|
"GstCaps not available, skipping frame %d", frame_metadata.frame_number
|
|
@@ -54,11 +54,14 @@ class GstCaps:
|
|
|
54
54
|
@classmethod
|
|
55
55
|
def parse(cls, caps_string: str) -> GstCaps:
|
|
56
56
|
"""
|
|
57
|
-
Parse GStreamer caps string.
|
|
58
|
-
|
|
57
|
+
Parse GStreamer caps string or simple format string.
|
|
58
|
+
|
|
59
|
+
Supports two formats:
|
|
60
|
+
1. Full GStreamer format: "video/x-raw, format=(string)RGB, width=(int)640, height=(int)480"
|
|
61
|
+
2. Simple format: "640x480 RGB" or "640x480 RGB @ 30.00fps"
|
|
59
62
|
|
|
60
63
|
Args:
|
|
61
|
-
caps_string:
|
|
64
|
+
caps_string: Caps string in either format
|
|
62
65
|
|
|
63
66
|
Returns:
|
|
64
67
|
GstCaps instance
|
|
@@ -71,9 +74,50 @@ class GstCaps:
|
|
|
71
74
|
|
|
72
75
|
caps_string = caps_string.strip()
|
|
73
76
|
|
|
77
|
+
# Try simple format first: "WIDTHxHEIGHT FORMAT" or "WIDTHxHEIGHT FORMAT @ FPS"
|
|
78
|
+
# Example: "320x240 BGR" or "1920x1080 RGB @ 30.00fps"
|
|
79
|
+
simple_match = re.match(r"^(\d+)x(\d+)\s+(\w+)(?:\s*@\s*([\d.]+)\s*fps)?$", caps_string)
|
|
80
|
+
if simple_match:
|
|
81
|
+
width = int(simple_match.group(1))
|
|
82
|
+
height = int(simple_match.group(2))
|
|
83
|
+
format_str = simple_match.group(3)
|
|
84
|
+
fps_str = simple_match.group(4)
|
|
85
|
+
|
|
86
|
+
framerate_num = None
|
|
87
|
+
framerate_den = None
|
|
88
|
+
if fps_str:
|
|
89
|
+
# Convert float FPS to fraction
|
|
90
|
+
fps = float(fps_str)
|
|
91
|
+
# Use common framerates or default to fps/1
|
|
92
|
+
if abs(fps - 30.0) < 0.01:
|
|
93
|
+
framerate_num, framerate_den = 30, 1
|
|
94
|
+
elif abs(fps - 29.97) < 0.01:
|
|
95
|
+
framerate_num, framerate_den = 30000, 1001
|
|
96
|
+
elif abs(fps - 25.0) < 0.01:
|
|
97
|
+
framerate_num, framerate_den = 25, 1
|
|
98
|
+
elif abs(fps - 60.0) < 0.01:
|
|
99
|
+
framerate_num, framerate_den = 60, 1
|
|
100
|
+
elif abs(fps - 59.94) < 0.01:
|
|
101
|
+
framerate_num, framerate_den = 60000, 1001
|
|
102
|
+
else:
|
|
103
|
+
framerate_num, framerate_den = int(fps * 1000), 1000
|
|
104
|
+
|
|
105
|
+
depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format_str)
|
|
106
|
+
return cls(
|
|
107
|
+
width=width,
|
|
108
|
+
height=height,
|
|
109
|
+
format=format_str,
|
|
110
|
+
depth_type=depth_type,
|
|
111
|
+
channels=channels,
|
|
112
|
+
bytes_per_pixel=bytes_per_pixel,
|
|
113
|
+
framerate_num=framerate_num,
|
|
114
|
+
framerate_den=framerate_den,
|
|
115
|
+
caps_string=None, # Not a real GStreamer caps string
|
|
116
|
+
)
|
|
117
|
+
|
|
74
118
|
# Check if it's a video caps
|
|
75
119
|
if not caps_string.startswith("video/x-raw"):
|
|
76
|
-
raise ValueError(f"Not a video/x-raw caps string: {caps_string}")
|
|
120
|
+
raise ValueError(f"Not a video/x-raw caps string or simple format: {caps_string}")
|
|
77
121
|
|
|
78
122
|
try:
|
|
79
123
|
# Parse width
|
|
@@ -84,8 +84,8 @@ class FrameSinkFactory:
|
|
|
84
84
|
return StreamFrameSink(file_handle)
|
|
85
85
|
|
|
86
86
|
if protocol.is_socket:
|
|
87
|
-
log.info("Creating Unix socket frame sink at: %s", address)
|
|
88
|
-
return UnixSocketFrameSink.
|
|
87
|
+
log.info("Creating Unix socket frame sink (server/bind) at: %s", address)
|
|
88
|
+
return UnixSocketFrameSink.bind(address)
|
|
89
89
|
|
|
90
90
|
if protocol.is_nng:
|
|
91
91
|
log.info("Creating NNG frame sink (%s) at: %s", protocol.schema, address)
|
|
@@ -15,7 +15,15 @@ import numpy as np
|
|
|
15
15
|
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
16
16
|
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
17
17
|
from .frame_metadata import FrameMetadata # noqa: TC001 - used at runtime in callbacks
|
|
18
|
+
from .high_level.connection_strings import KeyPointsConnectionString, SegmentationConnectionString
|
|
19
|
+
from .high_level.frame_sink_factory import FrameSinkFactory
|
|
20
|
+
from .keypoints_protocol import IKeyPointsSink, IKeyPointsWriter, KeyPointsSink
|
|
18
21
|
from .opencv_controller import OpenCvController
|
|
22
|
+
from .segmentation_result import (
|
|
23
|
+
ISegmentationResultSink,
|
|
24
|
+
ISegmentationResultWriter,
|
|
25
|
+
SegmentationResultSink,
|
|
26
|
+
)
|
|
19
27
|
from .session_id import (
|
|
20
28
|
get_configured_nng_urls,
|
|
21
29
|
get_nng_urls_from_env,
|
|
@@ -246,6 +254,144 @@ class RocketWelderClient:
|
|
|
246
254
|
self._controller.start(actual_callback, cancellation_token) # type: ignore[arg-type]
|
|
247
255
|
logger.info("RocketWelder client started with %s", self._connection)
|
|
248
256
|
|
|
257
|
+
def start_with_writers(
|
|
258
|
+
self,
|
|
259
|
+
on_frame: Callable[[Mat, ISegmentationResultWriter, IKeyPointsWriter, Mat], None], # type: ignore[valid-type]
|
|
260
|
+
cancellation_token: Optional[threading.Event] = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Start receiving frames with segmentation and keypoints output support.
|
|
264
|
+
|
|
265
|
+
Creates sinks for streaming AI results to rocket-welder2.
|
|
266
|
+
|
|
267
|
+
Configuration via environment variables:
|
|
268
|
+
- SEGMENTATION_SINK_URL: URL for segmentation output (e.g., socket:///tmp/seg.sock)
|
|
269
|
+
- KEYPOINTS_SINK_URL: URL for keypoints output (e.g., socket:///tmp/kp.sock)
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
on_frame: Callback receiving (input_mat, seg_writer, kp_writer, output_mat).
|
|
273
|
+
The writers are created per-frame and auto-flush on context exit.
|
|
274
|
+
cancellation_token: Optional cancellation token
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
def process_frame(input_mat, seg_writer, kp_writer, output_mat):
|
|
278
|
+
# Run AI inference
|
|
279
|
+
result = ai_model.infer(input_mat)
|
|
280
|
+
|
|
281
|
+
# Write segmentation results
|
|
282
|
+
for instance in result.instances:
|
|
283
|
+
seg_writer.append(instance.class_id, instance.instance_id, instance.points)
|
|
284
|
+
|
|
285
|
+
# Write keypoints
|
|
286
|
+
for kp in result.keypoints:
|
|
287
|
+
kp_writer.append(kp.id, kp.x, kp.y, kp.confidence)
|
|
288
|
+
|
|
289
|
+
# Copy/draw to output
|
|
290
|
+
output_mat[:] = input_mat
|
|
291
|
+
|
|
292
|
+
client.start_with_writers(process_frame)
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
RuntimeError: If already running
|
|
296
|
+
ValueError: If connection type is not supported or not duplex mode
|
|
297
|
+
"""
|
|
298
|
+
with self._lock:
|
|
299
|
+
if self._controller and self._controller.is_running:
|
|
300
|
+
raise RuntimeError("Client is already running")
|
|
301
|
+
|
|
302
|
+
# This overload requires duplex mode
|
|
303
|
+
if self._connection.connection_mode != ConnectionMode.DUPLEX:
|
|
304
|
+
raise ValueError("start_with_writers() requires duplex connection mode")
|
|
305
|
+
|
|
306
|
+
# Create controller
|
|
307
|
+
if self._connection.protocol == Protocol.SHM:
|
|
308
|
+
self._controller = DuplexShmController(self._connection)
|
|
309
|
+
elif self._connection.protocol == Protocol.FILE or bool(
|
|
310
|
+
self._connection.protocol & Protocol.MJPEG # type: ignore[operator]
|
|
311
|
+
):
|
|
312
|
+
self._controller = OpenCvController(self._connection)
|
|
313
|
+
else:
|
|
314
|
+
raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
|
|
315
|
+
|
|
316
|
+
# Create sinks from environment
|
|
317
|
+
seg_sink = self._get_or_create_segmentation_sink()
|
|
318
|
+
kp_sink = self._get_or_create_keypoints_sink()
|
|
319
|
+
|
|
320
|
+
logger.info(
|
|
321
|
+
"Starting RocketWelder client with AI output support: seg=%s, kp=%s",
|
|
322
|
+
"configured" if seg_sink else "null",
|
|
323
|
+
"configured" if kp_sink else "null",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Wrapper callback that creates per-frame writers
|
|
327
|
+
def writer_callback(
|
|
328
|
+
frame_metadata: FrameMetadata, input_mat: Mat, output_mat: Mat # type: ignore[valid-type]
|
|
329
|
+
) -> None:
|
|
330
|
+
# Get caps from controller metadata (width/height for segmentation)
|
|
331
|
+
metadata = self._controller.get_metadata() if self._controller else None
|
|
332
|
+
caps = metadata.caps if metadata else None
|
|
333
|
+
|
|
334
|
+
if caps is None:
|
|
335
|
+
logger.warning(
|
|
336
|
+
"GstCaps not available for frame %d, using no-op writers",
|
|
337
|
+
frame_metadata.frame_number,
|
|
338
|
+
)
|
|
339
|
+
# Use no-op writers
|
|
340
|
+
on_frame(
|
|
341
|
+
input_mat, _NoOpSegmentationWriter(), _NoOpKeyPointsWriter(), output_mat
|
|
342
|
+
)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Create per-frame writers from sinks
|
|
346
|
+
with seg_sink.create_writer(
|
|
347
|
+
frame_metadata.frame_number, caps.width, caps.height
|
|
348
|
+
) as seg_writer, kp_sink.create_writer(frame_metadata.frame_number) as kp_writer:
|
|
349
|
+
# Call user callback with writers
|
|
350
|
+
on_frame(input_mat, seg_writer, kp_writer, output_mat)
|
|
351
|
+
# Writers auto-flush on context exit
|
|
352
|
+
|
|
353
|
+
# Start the controller with our wrapper
|
|
354
|
+
self._controller.start(writer_callback, cancellation_token) # type: ignore[arg-type]
|
|
355
|
+
logger.info("RocketWelder client started with writers: %s", self._connection)
|
|
356
|
+
|
|
357
|
+
def _get_or_create_segmentation_sink(self) -> ISegmentationResultSink:
|
|
358
|
+
"""Get or create segmentation result sink from environment."""
|
|
359
|
+
import os
|
|
360
|
+
|
|
361
|
+
url = os.environ.get("SEGMENTATION_SINK_URL")
|
|
362
|
+
if not url:
|
|
363
|
+
logger.debug("SEGMENTATION_SINK_URL not set, using null sink")
|
|
364
|
+
return _NullSegmentationSink()
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
cs = SegmentationConnectionString.parse(url)
|
|
368
|
+
frame_sink = FrameSinkFactory.create(cs.protocol, cs.address)
|
|
369
|
+
return SegmentationResultSink(frame_sink=frame_sink, owns_sink=True)
|
|
370
|
+
except Exception as ex:
|
|
371
|
+
logger.warning("Failed to create segmentation sink from %s: %s", url, ex)
|
|
372
|
+
return _NullSegmentationSink()
|
|
373
|
+
|
|
374
|
+
def _get_or_create_keypoints_sink(self) -> IKeyPointsSink:
|
|
375
|
+
"""Get or create keypoints sink from environment."""
|
|
376
|
+
import os
|
|
377
|
+
|
|
378
|
+
url = os.environ.get("KEYPOINTS_SINK_URL")
|
|
379
|
+
if not url:
|
|
380
|
+
logger.debug("KEYPOINTS_SINK_URL not set, using null sink")
|
|
381
|
+
return _NullKeyPointsSink()
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
cs = KeyPointsConnectionString.parse(url)
|
|
385
|
+
frame_sink = FrameSinkFactory.create(cs.protocol, cs.address)
|
|
386
|
+
return KeyPointsSink(
|
|
387
|
+
frame_sink=frame_sink,
|
|
388
|
+
master_frame_interval=cs.master_frame_interval,
|
|
389
|
+
owns_sink=True,
|
|
390
|
+
)
|
|
391
|
+
except Exception as ex:
|
|
392
|
+
logger.warning("Failed to create keypoints sink from %s: %s", url, ex)
|
|
393
|
+
return _NullKeyPointsSink()
|
|
394
|
+
|
|
249
395
|
def stop(self) -> None:
|
|
250
396
|
"""Stop the client and clean up resources."""
|
|
251
397
|
with self._lock:
|
|
@@ -495,3 +641,59 @@ class RocketWelderClient:
|
|
|
495
641
|
f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=Duplex"
|
|
496
642
|
)
|
|
497
643
|
return cls(connection_str)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# No-op implementations for when sinks are not configured
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class _NoOpKeyPointsWriter(IKeyPointsWriter):
|
|
650
|
+
"""No-op keypoints writer that discards all data."""
|
|
651
|
+
|
|
652
|
+
def append(self, keypoint_id: int, x: int, y: int, confidence: float) -> None:
|
|
653
|
+
"""Discard keypoint data."""
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
def append_point(self, keypoint_id: int, point: tuple, confidence: float) -> None: # type: ignore[type-arg]
|
|
657
|
+
"""Discard keypoint data."""
|
|
658
|
+
pass
|
|
659
|
+
|
|
660
|
+
def close(self) -> None:
|
|
661
|
+
"""No-op close."""
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class _NoOpSegmentationWriter(ISegmentationResultWriter):
|
|
666
|
+
"""No-op segmentation writer that discards all data."""
|
|
667
|
+
|
|
668
|
+
def append(self, class_id: int, instance_id: int, points: Any) -> None:
|
|
669
|
+
"""Discard segmentation data."""
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
def close(self) -> None:
|
|
673
|
+
"""No-op close."""
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class _NullKeyPointsSink(IKeyPointsSink):
|
|
678
|
+
"""Null keypoints sink that creates no-op writers."""
|
|
679
|
+
|
|
680
|
+
def create_writer(self, frame_id: int) -> IKeyPointsWriter:
|
|
681
|
+
"""Create a no-op writer."""
|
|
682
|
+
return _NoOpKeyPointsWriter()
|
|
683
|
+
|
|
684
|
+
@staticmethod
|
|
685
|
+
def read(json_definition: str, blob_stream: Any) -> Any:
|
|
686
|
+
"""Not supported for null sink."""
|
|
687
|
+
raise NotImplementedError("NullKeyPointsSink does not support reading")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class _NullSegmentationSink(ISegmentationResultSink):
|
|
691
|
+
"""Null segmentation sink that creates no-op writers."""
|
|
692
|
+
|
|
693
|
+
def create_writer(self, frame_id: int, width: int, height: int) -> ISegmentationResultWriter:
|
|
694
|
+
"""Create a no-op writer."""
|
|
695
|
+
return _NoOpSegmentationWriter()
|
|
696
|
+
|
|
697
|
+
def close(self) -> None:
|
|
698
|
+
"""No-op close."""
|
|
699
|
+
pass
|
|
@@ -21,6 +21,7 @@ Features:
|
|
|
21
21
|
|
|
22
22
|
import io
|
|
23
23
|
import struct
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
24
25
|
from dataclasses import dataclass
|
|
25
26
|
from typing import BinaryIO, Iterator, List, Optional, Tuple, Union
|
|
26
27
|
|
|
@@ -418,3 +419,134 @@ class SegmentationResultReader:
|
|
|
418
419
|
def __exit__(self, *args: object) -> None:
|
|
419
420
|
"""Context manager exit."""
|
|
420
421
|
pass
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class ISegmentationResultWriter(ABC):
|
|
425
|
+
"""Interface for writing segmentation results for a single frame."""
|
|
426
|
+
|
|
427
|
+
@abstractmethod
|
|
428
|
+
def append(
|
|
429
|
+
self,
|
|
430
|
+
class_id: int,
|
|
431
|
+
instance_id: int,
|
|
432
|
+
points: Union[List[Point], PointArray],
|
|
433
|
+
) -> None:
|
|
434
|
+
"""
|
|
435
|
+
Append an instance with contour points.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
class_id: Object class ID (0-255)
|
|
439
|
+
instance_id: Instance ID within class (0-255)
|
|
440
|
+
points: List of (x, y) tuples or NumPy array of shape (N, 2)
|
|
441
|
+
"""
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
@abstractmethod
|
|
445
|
+
def close(self) -> None:
|
|
446
|
+
"""Flush and close the writer."""
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
def __enter__(self) -> "ISegmentationResultWriter":
|
|
450
|
+
"""Context manager entry."""
|
|
451
|
+
return self
|
|
452
|
+
|
|
453
|
+
def __exit__(self, *args: object) -> None:
|
|
454
|
+
"""Context manager exit."""
|
|
455
|
+
self.close()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class ISegmentationResultSink(ABC):
|
|
459
|
+
"""
|
|
460
|
+
Factory for creating segmentation result writers per frame (transport-agnostic).
|
|
461
|
+
|
|
462
|
+
Mirrors C# ISegmentationResultSink interface.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
@abstractmethod
|
|
466
|
+
def create_writer(self, frame_id: int, width: int, height: int) -> ISegmentationResultWriter:
|
|
467
|
+
"""
|
|
468
|
+
Create a writer for the current frame.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
frame_id: Unique frame identifier
|
|
472
|
+
width: Frame width in pixels
|
|
473
|
+
height: Frame height in pixels
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Segmentation result writer for this frame
|
|
477
|
+
"""
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
@abstractmethod
|
|
481
|
+
def close(self) -> None:
|
|
482
|
+
"""Close the sink and release resources."""
|
|
483
|
+
pass
|
|
484
|
+
|
|
485
|
+
def __enter__(self) -> "ISegmentationResultSink":
|
|
486
|
+
"""Context manager entry."""
|
|
487
|
+
return self
|
|
488
|
+
|
|
489
|
+
def __exit__(self, *args: object) -> None:
|
|
490
|
+
"""Context manager exit."""
|
|
491
|
+
self.close()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class SegmentationResultSink(ISegmentationResultSink):
|
|
495
|
+
"""
|
|
496
|
+
Transport-agnostic segmentation result sink.
|
|
497
|
+
|
|
498
|
+
Creates writers for each frame that serialize to the underlying IFrameSink.
|
|
499
|
+
|
|
500
|
+
Thread-safe: No (caller must synchronize)
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
def __init__(
|
|
504
|
+
self,
|
|
505
|
+
stream: Optional[BinaryIO] = None,
|
|
506
|
+
*,
|
|
507
|
+
frame_sink: Optional[IFrameSink] = None,
|
|
508
|
+
owns_sink: bool = False,
|
|
509
|
+
) -> None:
|
|
510
|
+
"""
|
|
511
|
+
Initialize segmentation result sink.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
stream: BinaryIO stream (convenience - auto-wraps in StreamFrameSink)
|
|
515
|
+
frame_sink: IFrameSink to write frames to (keyword-only, transport-agnostic)
|
|
516
|
+
owns_sink: If True, closes the sink on disposal (keyword-only)
|
|
517
|
+
|
|
518
|
+
Note:
|
|
519
|
+
Either stream or frame_sink must be provided (not both).
|
|
520
|
+
For convenience, stream is the primary parameter (auto-wraps in StreamFrameSink).
|
|
521
|
+
For transport-agnostic usage, use frame_sink= keyword argument.
|
|
522
|
+
"""
|
|
523
|
+
if frame_sink is None and stream is None:
|
|
524
|
+
raise TypeError("Either stream or frame_sink must be provided")
|
|
525
|
+
|
|
526
|
+
if frame_sink is not None and stream is not None:
|
|
527
|
+
raise TypeError("Cannot provide both stream and frame_sink")
|
|
528
|
+
|
|
529
|
+
# Convenience: auto-wrap stream in StreamFrameSink
|
|
530
|
+
if stream is not None:
|
|
531
|
+
self._frame_sink: IFrameSink = StreamFrameSink(stream, leave_open=False)
|
|
532
|
+
self._owns_sink = True
|
|
533
|
+
else:
|
|
534
|
+
assert frame_sink is not None
|
|
535
|
+
self._frame_sink = frame_sink
|
|
536
|
+
self._owns_sink = owns_sink
|
|
537
|
+
|
|
538
|
+
def create_writer(self, frame_id: int, width: int, height: int) -> ISegmentationResultWriter:
|
|
539
|
+
"""Create a writer for the current frame."""
|
|
540
|
+
# SegmentationResultWriter implements the write methods we need
|
|
541
|
+
# We return it as ISegmentationResultWriter
|
|
542
|
+
return SegmentationResultWriter( # type: ignore[return-value]
|
|
543
|
+
frame_id=frame_id,
|
|
544
|
+
width=width,
|
|
545
|
+
height=height,
|
|
546
|
+
frame_sink=self._frame_sink,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
def close(self) -> None:
|
|
550
|
+
"""Close the sink and release resources."""
|
|
551
|
+
if self._owns_sink:
|
|
552
|
+
self._frame_sink.close()
|
|
@@ -22,19 +22,26 @@ class UnixSocketFrameSink(IFrameSink):
|
|
|
22
22
|
Each frame is prefixed with a 4-byte little-endian length header.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
sock: socket.socket,
|
|
28
|
+
leave_open: bool = False,
|
|
29
|
+
server: Optional["UnixSocketServer"] = None,
|
|
30
|
+
):
|
|
26
31
|
"""
|
|
27
32
|
Create a Unix socket frame sink.
|
|
28
33
|
|
|
29
34
|
Args:
|
|
30
35
|
sock: Connected Unix domain socket
|
|
31
36
|
leave_open: If True, doesn't close socket on close
|
|
37
|
+
server: Optional server to clean up on close (used by bind())
|
|
32
38
|
"""
|
|
33
39
|
if sock.family != socket.AF_UNIX:
|
|
34
40
|
raise ValueError("Socket must be a Unix domain socket")
|
|
35
41
|
|
|
36
42
|
self._socket = sock
|
|
37
43
|
self._leave_open = leave_open
|
|
44
|
+
self._server = server
|
|
38
45
|
self._closed = False
|
|
39
46
|
|
|
40
47
|
@classmethod
|
|
@@ -69,6 +76,45 @@ class UnixSocketFrameSink(IFrameSink):
|
|
|
69
76
|
await loop.sock_connect(sock, socket_path)
|
|
70
77
|
return cls(sock, leave_open=False)
|
|
71
78
|
|
|
79
|
+
@classmethod
|
|
80
|
+
def bind(cls, socket_path: str) -> "UnixSocketFrameSink":
|
|
81
|
+
"""
|
|
82
|
+
Bind to a Unix socket path as a server and wait for a client to connect.
|
|
83
|
+
|
|
84
|
+
Use this when the SDK is the producer (server) and rocket-welder2 is
|
|
85
|
+
the consumer (client).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
socket_path: Path to Unix socket file
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Frame sink connected to the first client
|
|
92
|
+
|
|
93
|
+
Note:
|
|
94
|
+
This is the server-side counterpart to connect().
|
|
95
|
+
The server binds and listens, then blocks until a client connects.
|
|
96
|
+
"""
|
|
97
|
+
server = UnixSocketServer(socket_path)
|
|
98
|
+
server.start()
|
|
99
|
+
client_socket = server.accept()
|
|
100
|
+
return cls(client_socket, leave_open=False, server=server)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
async def bind_async(cls, socket_path: str) -> "UnixSocketFrameSink":
|
|
104
|
+
"""
|
|
105
|
+
Bind to a Unix socket path as a server and wait asynchronously for a client.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
socket_path: Path to Unix socket file
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Frame sink connected to the first client
|
|
112
|
+
"""
|
|
113
|
+
server = UnixSocketServer(socket_path)
|
|
114
|
+
server.start()
|
|
115
|
+
client_socket = await server.accept_async()
|
|
116
|
+
return cls(client_socket, leave_open=False, server=server)
|
|
117
|
+
|
|
72
118
|
def write_frame(self, frame_data: bytes) -> None:
|
|
73
119
|
"""Write frame with 4-byte length prefix to Unix socket."""
|
|
74
120
|
if self._closed:
|
|
@@ -112,6 +158,10 @@ class UnixSocketFrameSink(IFrameSink):
|
|
|
112
158
|
with contextlib.suppress(OSError):
|
|
113
159
|
self._socket.shutdown(socket.SHUT_WR)
|
|
114
160
|
self._socket.close()
|
|
161
|
+
# Clean up server if we created one via bind()
|
|
162
|
+
if self._server is not None:
|
|
163
|
+
self._server.stop()
|
|
164
|
+
self._server = None
|
|
115
165
|
|
|
116
166
|
async def close_async(self) -> None:
|
|
117
167
|
"""Close the Unix socket sink asynchronously."""
|
{rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rocket-welder-sdk
|
|
3
|
-
Version: 1.1.38.
|
|
3
|
+
Version: 1.1.38.dev23
|
|
4
4
|
Summary: High-performance video streaming SDK for RocketWelder services using ZeroBuffer IPC
|
|
5
5
|
Home-page: https://github.com/modelingevolution/rocket-welder-sdk
|
|
6
6
|
Author: ModelingEvolution
|
{rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/RECORD
RENAMED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
rocket_welder_sdk/__init__.py,sha256=
|
|
1
|
+
rocket_welder_sdk/__init__.py,sha256=xAy90cHORXwtphmNrvWaNs-YP4MAx3GIOZX_jJoIWxs,3530
|
|
2
2
|
rocket_welder_sdk/bytes_size.py,sha256=Myl29-wyWCIYdbMmgaxXebT8Dz8_Fwcr3fnfaNW81P0,7463
|
|
3
3
|
rocket_welder_sdk/connection_string.py,sha256=NIC6OiOXF-DeBFCWzgMFOWsenrSS45hY81j_HLMSpgo,9974
|
|
4
|
-
rocket_welder_sdk/controllers.py,sha256=
|
|
4
|
+
rocket_welder_sdk/controllers.py,sha256=uuVyKsNHtpaIzQt8pXCAu7SywoFa7cfww8VSa4LNi_E,33130
|
|
5
5
|
rocket_welder_sdk/frame_metadata.py,sha256=TMLIY47cIdIlxqk9xj7I3M8FZFmZ3GcVoLZht7prjQM,3929
|
|
6
|
-
rocket_welder_sdk/gst_metadata.py,sha256=
|
|
6
|
+
rocket_welder_sdk/gst_metadata.py,sha256=6Ov-DekGsKNuAo3UIc-g9fh4_SAPdHYQTnnPqCK3Ks0,16180
|
|
7
7
|
rocket_welder_sdk/keypoints_protocol.py,sha256=NKiSPrevWG4_RrD6jtFxPjwftlaPWe1CqoFVKRMwp4k,21858
|
|
8
8
|
rocket_welder_sdk/opencv_controller.py,sha256=MDM6_yFBB9BaMa5jnZRqw7xZZB-WuLr7EPrrfHQ2DK4,9905
|
|
9
9
|
rocket_welder_sdk/periodic_timer.py,sha256=hnObybmrnf3J47QrNKJhYAytLKwria016123NvPRfQ0,9369
|
|
10
10
|
rocket_welder_sdk/py.typed,sha256=0cXFZXmes4Y-vnl4lO3HtyyyWaFNw85B7tJdFeCtHDc,67
|
|
11
|
-
rocket_welder_sdk/rocket_welder_client.py,sha256=
|
|
12
|
-
rocket_welder_sdk/segmentation_result.py,sha256=
|
|
11
|
+
rocket_welder_sdk/rocket_welder_client.py,sha256=dhjjak7F6xTlLZDVl7TPPiX0KpyWb0DQhrTP3GrkuPM,26655
|
|
12
|
+
rocket_welder_sdk/segmentation_result.py,sha256=c2C42C9l8iaRyTWR6ZOL1kLO4jHP_yjeVMf5DaKkY8E,17525
|
|
13
13
|
rocket_welder_sdk/session_id.py,sha256=sRhzQw90shqq_DJVtrsSggcGZ775kz7cRfbI-1LMeSA,7027
|
|
14
14
|
rocket_welder_sdk/external_controls/__init__.py,sha256=ldOLGhLLS5BQL8m4VKFYV0SvsNNlV2tghlc7rkqadU8,699
|
|
15
15
|
rocket_welder_sdk/external_controls/contracts.py,sha256=3DU6pdpteN50gF2fsS7C2279dGjDa0tZLrLntkBa2LM,2607
|
|
@@ -18,7 +18,7 @@ rocket_welder_sdk/high_level/__init__.py,sha256=jzLaAhmay_k03gaPgv9fdfBjn6b1xbiQ
|
|
|
18
18
|
rocket_welder_sdk/high_level/client.py,sha256=IFnbO9JI1KhnCljHiwFZbhx4qMW_z8BdTz4uAi0d7TA,8284
|
|
19
19
|
rocket_welder_sdk/high_level/connection_strings.py,sha256=q1uZJQ7mt1RR-E8MJzIwG6vz3Ddruoc3pTCdTvg_pe4,10434
|
|
20
20
|
rocket_welder_sdk/high_level/data_context.py,sha256=SXJvDpDBFi8Lm4XqSRSHK7YUUHuugXGo4ZRCb6_z5l0,4833
|
|
21
|
-
rocket_welder_sdk/high_level/frame_sink_factory.py,sha256=
|
|
21
|
+
rocket_welder_sdk/high_level/frame_sink_factory.py,sha256=JeQXp7_OoAQzcAb6XOsBt_Ewen_KbvYQ82YLDyx71tQ,4000
|
|
22
22
|
rocket_welder_sdk/high_level/schema.py,sha256=2Vv0rDwahtGswWB_ceaCdc7JDtmbkx4wE2jQePzeTpU,5367
|
|
23
23
|
rocket_welder_sdk/high_level/transport_protocol.py,sha256=EFF0bgNn9hxRMj67FwU6MVu-UiEFINSGhd2VC8agrgc,7393
|
|
24
24
|
rocket_welder_sdk/transport/__init__.py,sha256=iU5keW_VYHybvGBkDX1AxcCp9IhYvbqPBegu-w9IPX8,787
|
|
@@ -27,14 +27,14 @@ rocket_welder_sdk/transport/frame_source.py,sha256=G1rBAQS1AgOOdtASB0_CYon8g20hU
|
|
|
27
27
|
rocket_welder_sdk/transport/nng_transport.py,sha256=o-qgcmHCGnwtdPe-mwwrC-a9H0rgS-VdH2QvU-6kFlI,5838
|
|
28
28
|
rocket_welder_sdk/transport/stream_transport.py,sha256=FhxFlZT-CTo6aPq6VclM1A_ecqfeHcwR4Ty1vmlt3W0,5886
|
|
29
29
|
rocket_welder_sdk/transport/tcp_transport.py,sha256=Tui6nKgu50C1KV_UDANQKgVK3M52XN7zrkZTcWIpGmY,4745
|
|
30
|
-
rocket_welder_sdk/transport/unix_socket_transport.py,sha256=
|
|
30
|
+
rocket_welder_sdk/transport/unix_socket_transport.py,sha256=t44Q2Fj6fMv53Ll561hN35-ZbHPdAQt0t9tUAXoL0rY,12245
|
|
31
31
|
rocket_welder_sdk/ui/__init__.py,sha256=5-fCkv3vG0VVVZkselrifkMmx8d51EJp3nNT9mt4JK4,886
|
|
32
32
|
rocket_welder_sdk/ui/controls.py,sha256=cRBxdUDWMOYlCxtw1PV11fTlte6qLxSfCGpr9bQiLcE,11162
|
|
33
33
|
rocket_welder_sdk/ui/icons.py,sha256=DcDklZkPmiEzlOD4IR7VTJOtGPCuuh_OM_WN7ScghWE,8934592
|
|
34
34
|
rocket_welder_sdk/ui/ui_events_projection.py,sha256=siiNhjLEBOPfTKw1ZhOPGkwIN5rLDH7V9VCZTNrhEtQ,7836
|
|
35
35
|
rocket_welder_sdk/ui/ui_service.py,sha256=uRdpyJGoCQmtOli_HKSrxLwhZYG-XRuHIYdkmFz1zNk,12026
|
|
36
36
|
rocket_welder_sdk/ui/value_types.py,sha256=f7OA_9zgXEDPoITc8v8SfAR23I4XeFhE3E2_GcAbR6k,1616
|
|
37
|
-
rocket_welder_sdk-1.1.38.
|
|
38
|
-
rocket_welder_sdk-1.1.38.
|
|
39
|
-
rocket_welder_sdk-1.1.38.
|
|
40
|
-
rocket_welder_sdk-1.1.38.
|
|
37
|
+
rocket_welder_sdk-1.1.38.dev23.dist-info/METADATA,sha256=H4xaBC0-0Klx4Z7Egg16M9QuV0UE7Gjl2hurUDyNB_U,24853
|
|
38
|
+
rocket_welder_sdk-1.1.38.dev23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
39
|
+
rocket_welder_sdk-1.1.38.dev23.dist-info/top_level.txt,sha256=2iZvBjnwVCUW-uDE23-eJld5PZ9-mlPI69QiXM5IrTA,18
|
|
40
|
+
rocket_welder_sdk-1.1.38.dev23.dist-info/RECORD,,
|
|
File without changes
|
{rocket_welder_sdk-1.1.38.dev22.dist-info → rocket_welder_sdk-1.1.38.dev23.dist-info}/top_level.txt
RENAMED
|
File without changes
|