rocket-welder-sdk 1.1.34.dev10__tar.gz → 1.1.34.dev11__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.
Files changed (69) hide show
  1. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/PKG-INFO +1 -1
  2. rocket_welder_sdk-1.1.34.dev11/VERSION +1 -0
  3. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/pyproject.toml +2 -2
  4. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/controllers.py +1 -1
  5. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/frame_metadata.py +2 -2
  6. rocket_welder_sdk-1.1.34.dev11/rocket_welder_sdk/high_level/__init__.py +52 -0
  7. rocket_welder_sdk-1.1.34.dev11/rocket_welder_sdk/high_level/client.py +262 -0
  8. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/high_level/connection_strings.py +56 -55
  9. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/high_level/data_context.py +17 -11
  10. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/high_level/schema.py +44 -27
  11. rocket_welder_sdk-1.1.34.dev11/rocket_welder_sdk/high_level/transport_protocol.py +238 -0
  12. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk.egg-info/PKG-INFO +1 -1
  13. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk.egg-info/SOURCES.txt +1 -0
  14. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_high_level_api.py +136 -77
  15. rocket_welder_sdk-1.1.34.dev10/VERSION +0 -1
  16. rocket_welder_sdk-1.1.34.dev10/rocket_welder_sdk/high_level/__init__.py +0 -66
  17. rocket_welder_sdk-1.1.34.dev10/rocket_welder_sdk/high_level/transport_protocol.py +0 -166
  18. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/MANIFEST.in +0 -0
  19. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/README.md +0 -0
  20. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/logo.png +0 -0
  21. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/__init__.py +0 -0
  22. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/bytes_size.py +0 -0
  23. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/connection_string.py +0 -0
  24. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/external_controls/__init__.py +0 -0
  25. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/external_controls/contracts.py +0 -0
  26. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/external_controls/contracts_old.py +0 -0
  27. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/gst_metadata.py +0 -0
  28. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/keypoints_protocol.py +0 -0
  29. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/opencv_controller.py +0 -0
  30. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/periodic_timer.py +0 -0
  31. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/py.typed +0 -0
  32. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/rocket_welder_client.py +0 -0
  33. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/segmentation_result.py +0 -0
  34. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/session_id.py +0 -0
  35. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/__init__.py +0 -0
  36. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/frame_sink.py +0 -0
  37. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/frame_source.py +0 -0
  38. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/nng_transport.py +0 -0
  39. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/stream_transport.py +0 -0
  40. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/tcp_transport.py +0 -0
  41. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/transport/unix_socket_transport.py +0 -0
  42. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/__init__.py +0 -0
  43. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/controls.py +0 -0
  44. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/icons.py +0 -0
  45. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/ui_events_projection.py +0 -0
  46. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/ui_service.py +0 -0
  47. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk/ui/value_types.py +0 -0
  48. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
  49. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk.egg-info/requires.txt +0 -0
  50. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
  51. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/setup.cfg +0 -0
  52. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/setup.py +0 -0
  53. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_bytes_size.py +0 -0
  54. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_connection_string.py +0 -0
  55. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_controllers.py +0 -0
  56. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_external_controls_serialization.py +0 -0
  57. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_external_controls_serialization_v2.py +0 -0
  58. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_frame_metadata.py +0 -0
  59. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_gst_metadata.py +0 -0
  60. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_icons.py +0 -0
  61. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_keypoints_cross_platform.py +0 -0
  62. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_keypoints_protocol.py +0 -0
  63. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_rocket_welder_client.py +0 -0
  64. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_segmentation_cross_platform.py +0 -0
  65. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_segmentation_result.py +0 -0
  66. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_session_id.py +0 -0
  67. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_transport_cross_platform.py +0 -0
  68. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_ui_controls.py +0 -0
  69. {rocket_welder_sdk-1.1.34.dev10 → rocket_welder_sdk-1.1.34.dev11}/tests/test_ui_service_happy_path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocket-welder-sdk
3
- Version: 1.1.34.dev10
3
+ Version: 1.1.34.dev11
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
@@ -0,0 +1 @@
1
+ 1.1.34.dev11
@@ -82,8 +82,7 @@ show_error_codes = true
82
82
  show_column_numbers = true
83
83
  pretty = true
84
84
  exclude = [
85
- "examples/05-traktorek",
86
- "examples/rocket-welder-client-python-yolo",
85
+ "examples/", # Examples are not packages, exclude from type checking
87
86
  ]
88
87
 
89
88
  [[tool.mypy.overrides]]
@@ -148,6 +147,7 @@ ignore = [
148
147
  [tool.ruff.lint.per-file-ignores]
149
148
  "__init__.py" = ["F401"] # imported but unused
150
149
  "tests/*" = ["S101"] # use of assert
150
+ "examples/*" = ["N999", "SIM112"] # Module names and env var casing (matches C# SDK)
151
151
 
152
152
  [tool.pytest.ini_options]
153
153
  minversion = "7.0"
@@ -338,7 +338,7 @@ class OneWayShmController(IController):
338
338
  Matches C# CreateMat behavior - creates Mat wrapping the data.
339
339
 
340
340
  Frame data layout from GStreamer zerosink:
341
- [FrameMetadata (16 bytes)][Pixel Data (W×H×C bytes)]
341
+ [FrameMetadata (16 bytes)][Pixel Data (WxHxC bytes)]
342
342
 
343
343
  Args:
344
344
  frame: ZeroBuffer frame
@@ -18,7 +18,7 @@ from __future__ import annotations
18
18
 
19
19
  import struct
20
20
  from dataclasses import dataclass
21
- from typing import Optional
21
+ from typing import ClassVar, Dict, Optional
22
22
 
23
23
  # Size of the FrameMetadata structure in bytes
24
24
  FRAME_METADATA_SIZE = 16
@@ -113,7 +113,7 @@ class GstVideoFormat:
113
113
  GRAY16_BE = 26
114
114
  GRAY16_LE = 27
115
115
 
116
- _FORMAT_NAMES: dict[int, str] = {
116
+ _FORMAT_NAMES: ClassVar[Dict[int, str]] = {
117
117
  0: "UNKNOWN",
118
118
  2: "I420",
119
119
  3: "YV12",
@@ -0,0 +1,52 @@
1
+ """
2
+ High-level API for RocketWelder SDK.
3
+
4
+ Mirrors C# RocketWelder.SDK API for consistent developer experience.
5
+
6
+ Example:
7
+ from rocket_welder_sdk.high_level import RocketWelderClient
8
+
9
+ with RocketWelderClient.from_environment() as client:
10
+ nose = client.keypoints.define_point("nose")
11
+ person = client.segmentation.define_class(1, "person")
12
+ client.start(process_frame)
13
+ """
14
+
15
+ from .client import RocketWelderClient, RocketWelderClientOptions
16
+ from .connection_strings import (
17
+ KeyPointsConnectionString,
18
+ SegmentationConnectionString,
19
+ VideoSourceConnectionString,
20
+ VideoSourceType,
21
+ )
22
+ from .data_context import (
23
+ IKeyPointsDataContext,
24
+ ISegmentationDataContext,
25
+ )
26
+ from .schema import (
27
+ IKeyPointsSchema,
28
+ ISegmentationSchema,
29
+ KeyPointDefinition,
30
+ SegmentClass,
31
+ )
32
+ from .transport_protocol import (
33
+ TransportKind,
34
+ TransportProtocol,
35
+ )
36
+
37
+ __all__ = [
38
+ "IKeyPointsDataContext",
39
+ "IKeyPointsSchema",
40
+ "ISegmentationDataContext",
41
+ "ISegmentationSchema",
42
+ "KeyPointDefinition",
43
+ "KeyPointsConnectionString",
44
+ "RocketWelderClient",
45
+ "RocketWelderClientOptions",
46
+ "SegmentClass",
47
+ "SegmentationConnectionString",
48
+ "TransportKind",
49
+ "TransportProtocol",
50
+ "VideoSourceConnectionString",
51
+ "VideoSourceType",
52
+ ]
@@ -0,0 +1,262 @@
1
+ """
2
+ RocketWelderClient - High-level API matching C# RocketWelder.SDK.
3
+
4
+ Usage:
5
+ with RocketWelderClient.from_environment() as client:
6
+ # Define schema
7
+ nose = client.keypoints.define_point("nose")
8
+ person = client.segmentation.define_class(1, "person")
9
+
10
+ # Start processing
11
+ client.start(process_frame)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass, field
18
+ from typing import TYPE_CHECKING, Any, Callable, Optional
19
+
20
+ import numpy as np
21
+ import numpy.typing as npt
22
+ from typing_extensions import TypeAlias
23
+
24
+ from .connection_strings import (
25
+ KeyPointsConnectionString,
26
+ SegmentationConnectionString,
27
+ VideoSourceConnectionString,
28
+ )
29
+ from .data_context import (
30
+ IKeyPointsDataContext,
31
+ ISegmentationDataContext,
32
+ KeyPointsDataContext,
33
+ SegmentationDataContext,
34
+ )
35
+ from .schema import (
36
+ IKeyPointsSchema,
37
+ ISegmentationSchema,
38
+ KeyPointsSchema,
39
+ SegmentationSchema,
40
+ )
41
+ from .transport_protocol import TransportKind
42
+
43
+ if TYPE_CHECKING:
44
+ from rocket_welder_sdk.keypoints_protocol import KeyPointsSink
45
+ from rocket_welder_sdk.transport.frame_sink import IFrameSink
46
+
47
+ # Type alias for OpenCV Mat (numpy array)
48
+ Mat: TypeAlias = npt.NDArray[np.uint8]
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ @dataclass
54
+ class RocketWelderClientOptions:
55
+ """Configuration options for RocketWelderClient."""
56
+
57
+ video_source: VideoSourceConnectionString = field(
58
+ default_factory=VideoSourceConnectionString.default
59
+ )
60
+ keypoints: KeyPointsConnectionString = field(default_factory=KeyPointsConnectionString.default)
61
+ segmentation: SegmentationConnectionString = field(
62
+ default_factory=SegmentationConnectionString.default
63
+ )
64
+
65
+ @classmethod
66
+ def from_environment(cls) -> RocketWelderClientOptions:
67
+ """Create from environment variables."""
68
+ return cls(
69
+ video_source=VideoSourceConnectionString.from_environment(),
70
+ keypoints=KeyPointsConnectionString.from_environment(),
71
+ segmentation=SegmentationConnectionString.from_environment(),
72
+ )
73
+
74
+
75
+ class RocketWelderClient:
76
+ """
77
+ High-level client for RocketWelder SDK.
78
+
79
+ Mirrors C# RocketWelder.SDK.IRocketWelderClient interface.
80
+ """
81
+
82
+ def __init__(self, options: RocketWelderClientOptions) -> None:
83
+ self._options = options
84
+ self._keypoints_schema = KeyPointsSchema()
85
+ self._segmentation_schema = SegmentationSchema()
86
+ self._keypoints_sink: Optional[KeyPointsSink] = None
87
+ self._keypoints_frame_sink: Optional[IFrameSink] = None
88
+ self._segmentation_frame_sink: Optional[IFrameSink] = None
89
+ self._closed = False
90
+ logger.debug("RocketWelderClient created with options: %s", options)
91
+
92
+ @classmethod
93
+ def from_environment(cls) -> RocketWelderClient:
94
+ """Create client from environment variables."""
95
+ logger.info("Creating RocketWelderClient from environment variables")
96
+ return cls(RocketWelderClientOptions.from_environment())
97
+
98
+ @classmethod
99
+ def create(cls, options: Optional[RocketWelderClientOptions] = None) -> RocketWelderClient:
100
+ """Create client with explicit options."""
101
+ return cls(options or RocketWelderClientOptions())
102
+
103
+ @property
104
+ def keypoints(self) -> IKeyPointsSchema:
105
+ """Schema for defining keypoints."""
106
+ return self._keypoints_schema
107
+
108
+ @property
109
+ def segmentation(self) -> ISegmentationSchema:
110
+ """Schema for defining segmentation classes."""
111
+ return self._segmentation_schema
112
+
113
+ def start(
114
+ self,
115
+ process_frame: Callable[[Mat, ISegmentationDataContext, IKeyPointsDataContext, Mat], None],
116
+ ) -> None:
117
+ """Start with both keypoints and segmentation."""
118
+ self._run_loop(process_frame, use_keypoints=True, use_segmentation=True)
119
+
120
+ def start_keypoints(
121
+ self,
122
+ process_frame: Callable[[Mat, IKeyPointsDataContext, Mat], None],
123
+ ) -> None:
124
+ """Start with keypoints only."""
125
+ self._run_loop(process_frame, use_keypoints=True, use_segmentation=False)
126
+
127
+ def start_segmentation(
128
+ self,
129
+ process_frame: Callable[[Mat, ISegmentationDataContext, Mat], None],
130
+ ) -> None:
131
+ """Start with segmentation only."""
132
+ self._run_loop(process_frame, use_keypoints=False, use_segmentation=True)
133
+
134
+ def _run_loop(
135
+ self,
136
+ process_frame: Callable[..., None],
137
+ use_keypoints: bool,
138
+ use_segmentation: bool,
139
+ ) -> None:
140
+ """Run processing loop."""
141
+ from rocket_welder_sdk.keypoints_protocol import KeyPointsSink
142
+
143
+ logger.info(
144
+ "Starting processing loop (keypoints=%s, segmentation=%s)",
145
+ use_keypoints,
146
+ use_segmentation,
147
+ )
148
+
149
+ # Initialize sinks
150
+ if use_keypoints:
151
+ cs = self._options.keypoints
152
+ logger.info("Initializing keypoints sink: %s -> %s", cs.protocol, cs.address)
153
+ self._keypoints_frame_sink = self._create_frame_sink(cs.protocol, cs.address)
154
+ self._keypoints_sink = KeyPointsSink(
155
+ frame_sink=self._keypoints_frame_sink,
156
+ master_frame_interval=cs.master_frame_interval,
157
+ owns_sink=False, # We manage frame sink lifecycle in close()
158
+ )
159
+ logger.debug(
160
+ "KeyPointsSink created with master_frame_interval=%d", cs.master_frame_interval
161
+ )
162
+
163
+ if use_segmentation:
164
+ seg_cs = self._options.segmentation
165
+ logger.info("Initializing segmentation sink: %s -> %s", seg_cs.protocol, seg_cs.address)
166
+ self._segmentation_frame_sink = self._create_frame_sink(seg_cs.protocol, seg_cs.address)
167
+ logger.debug("Segmentation frame sink created")
168
+
169
+ # TODO: Video capture loop - for now raise NotImplementedError
170
+ raise NotImplementedError(
171
+ "Video capture not implemented. Use process_frame_sync() or low-level API."
172
+ )
173
+
174
+ def process_frame_sync(
175
+ self,
176
+ frame_id: int,
177
+ input_frame: Mat,
178
+ output_frame: Mat,
179
+ width: int,
180
+ height: int,
181
+ ) -> tuple[Optional[IKeyPointsDataContext], Optional[ISegmentationDataContext]]:
182
+ """
183
+ Process a single frame synchronously.
184
+
185
+ Returns (keypoints_context, segmentation_context) for the caller to use.
186
+ Caller must call commit() on contexts when done.
187
+ """
188
+ from rocket_welder_sdk.segmentation_result import SegmentationResultWriter
189
+
190
+ kp_ctx: Optional[IKeyPointsDataContext] = None
191
+ seg_ctx: Optional[ISegmentationDataContext] = None
192
+
193
+ if self._keypoints_sink is not None:
194
+ kp_writer = self._keypoints_sink.create_writer(frame_id)
195
+ kp_ctx = KeyPointsDataContext(frame_id, kp_writer)
196
+
197
+ if self._segmentation_frame_sink is not None:
198
+ seg_writer = SegmentationResultWriter(
199
+ frame_id, width, height, frame_sink=self._segmentation_frame_sink
200
+ )
201
+ seg_ctx = SegmentationDataContext(frame_id, seg_writer)
202
+
203
+ return kp_ctx, seg_ctx
204
+
205
+ def _create_frame_sink(self, protocol: Any, address: str) -> IFrameSink:
206
+ """Create frame sink from protocol."""
207
+ from rocket_welder_sdk.transport import NngFrameSink
208
+ from rocket_welder_sdk.transport.stream_transport import StreamFrameSink
209
+ from rocket_welder_sdk.transport.unix_socket_transport import UnixSocketFrameSink
210
+
211
+ from .transport_protocol import TransportProtocol
212
+
213
+ if not isinstance(protocol, TransportProtocol):
214
+ raise TypeError(f"Expected TransportProtocol, got {type(protocol)}")
215
+
216
+ if protocol.kind == TransportKind.FILE:
217
+ logger.debug("Creating file sink: %s", address)
218
+ file_handle = open(address, "wb")
219
+ try:
220
+ return StreamFrameSink(file_handle)
221
+ except Exception:
222
+ file_handle.close()
223
+ raise
224
+ elif protocol.kind == TransportKind.SOCKET:
225
+ logger.debug("Creating Unix socket sink: %s", address)
226
+ return UnixSocketFrameSink.connect(address)
227
+ elif protocol.kind in (TransportKind.NNG_PUSH_IPC, TransportKind.NNG_PUSH_TCP):
228
+ logger.debug("Creating NNG pusher: %s", address)
229
+ return NngFrameSink.create_pusher(address)
230
+ elif protocol.kind in (TransportKind.NNG_PUB_IPC, TransportKind.NNG_PUB_TCP):
231
+ logger.debug("Creating NNG publisher: %s", address)
232
+ return NngFrameSink.create_publisher(address)
233
+ else:
234
+ raise ValueError(f"Unsupported protocol: {protocol}")
235
+
236
+ def close(self) -> None:
237
+ """Release resources."""
238
+ if self._closed:
239
+ return
240
+
241
+ logger.info("Closing RocketWelderClient")
242
+
243
+ # Close frame sinks (KeyPointsSink has owns_sink=False, so we manage lifecycle)
244
+ self._keypoints_sink = None
245
+ if self._keypoints_frame_sink is not None:
246
+ logger.debug("Closing keypoints frame sink")
247
+ self._keypoints_frame_sink.close()
248
+ self._keypoints_frame_sink = None
249
+
250
+ if self._segmentation_frame_sink is not None:
251
+ logger.debug("Closing segmentation frame sink")
252
+ self._segmentation_frame_sink.close()
253
+ self._segmentation_frame_sink = None
254
+
255
+ self._closed = True
256
+ logger.info("RocketWelderClient closed")
257
+
258
+ def __enter__(self) -> RocketWelderClient:
259
+ return self
260
+
261
+ def __exit__(self, *args: object) -> None:
262
+ self.close()
@@ -6,7 +6,8 @@ Connection string format: protocol://path?param1=value1&param2=value2
6
6
  Examples:
7
7
  nng+push+ipc://tmp/keypoints?masterFrameInterval=300
8
8
  nng+pub+tcp://localhost:5555
9
- file://path/to/output.bin
9
+ file:///path/to/output.bin
10
+ socket:///tmp/my.sock
10
11
  """
11
12
 
12
13
  from __future__ import annotations
@@ -144,19 +145,19 @@ class KeyPointsConnectionString:
144
145
  """
145
146
  Strongly-typed connection string for KeyPoints output.
146
147
 
147
- Supported protocols (composable with + operator):
148
- - Transport.Nng + Transport.Push + Transport.Ipc → nng+push+ipc://tmp/keypoints
149
- - Transport.Nng + Transport.Push + Transport.Tcp → nng+push+tcp://host:port
150
- - file://path/to/file.bin - File output
148
+ Supported protocols:
149
+ - file:///path/to/file.bin - File output (absolute path)
150
+ - socket:///tmp/socket.sock - Unix domain socket
151
+ - nng+push+ipc://tmp/keypoints - NNG Push over IPC
152
+ - nng+push+tcp://host:port - NNG Push over TCP
151
153
 
152
154
  Supported parameters:
153
155
  - masterFrameInterval: Interval between master frames (default: 300)
154
156
  """
155
157
 
156
158
  value: str
157
- protocol: Optional[TransportProtocol] = None
158
- is_file: bool = False
159
- address: str = ""
159
+ protocol: TransportProtocol
160
+ address: str
160
161
  master_frame_interval: int = 300
161
162
  parameters: Dict[str, str] = field(default_factory=dict)
162
163
 
@@ -199,25 +200,26 @@ class KeyPointsConnectionString:
199
200
 
200
201
  # Parse protocol and address
201
202
  scheme_end = endpoint_part.find("://")
202
- if scheme_end > 0:
203
- protocol_str = endpoint_part[:scheme_end]
204
- path_part = endpoint_part[scheme_end + 3 :] # skip "://"
205
-
206
- if protocol_str.lower() == "file":
207
- address = "/" + path_part # Restore absolute path
208
- is_file = True
209
- protocol = None
210
- else:
211
- protocol = TransportProtocol.try_parse(protocol_str)
212
- if protocol is None:
213
- return None
214
- address = protocol.create_nng_address(path_part)
215
- is_file = False
216
- elif s.startswith("/"):
217
- # Assume absolute file path
218
- address = s
219
- is_file = True
220
- protocol = None
203
+ if scheme_end <= 0:
204
+ return None
205
+
206
+ schema_str = endpoint_part[:scheme_end]
207
+ path_part = endpoint_part[scheme_end + 3 :] # skip "://"
208
+
209
+ protocol = TransportProtocol.try_parse(schema_str)
210
+ if protocol is None:
211
+ return None
212
+
213
+ # Build address based on protocol type
214
+ if protocol.is_file:
215
+ # file:///absolute/path -> /absolute/path
216
+ address = path_part if path_part.startswith("/") else "/" + path_part
217
+ elif protocol.is_socket:
218
+ # socket:///tmp/sock -> /tmp/sock
219
+ address = path_part if path_part.startswith("/") else "/" + path_part
220
+ elif protocol.is_nng:
221
+ # NNG protocols need proper address format
222
+ address = protocol.create_nng_address(path_part)
221
223
  else:
222
224
  return None
223
225
 
@@ -230,7 +232,6 @@ class KeyPointsConnectionString:
230
232
  return cls(
231
233
  value=s,
232
234
  protocol=protocol,
233
- is_file=is_file,
234
235
  address=address,
235
236
  master_frame_interval=master_frame_interval,
236
237
  parameters=parameters,
@@ -245,16 +246,16 @@ class SegmentationConnectionString:
245
246
  """
246
247
  Strongly-typed connection string for Segmentation output.
247
248
 
248
- Supported protocols (composable with + operator):
249
- - Transport.Nng + Transport.Push + Transport.Ipc → nng+push+ipc://tmp/segmentation
250
- - Transport.Nng + Transport.Push + Transport.Tcp → nng+push+tcp://host:port
251
- - file://path/to/file.bin - File output
249
+ Supported protocols:
250
+ - file:///path/to/file.bin - File output (absolute path)
251
+ - socket:///tmp/socket.sock - Unix domain socket
252
+ - nng+push+ipc://tmp/segmentation - NNG Push over IPC
253
+ - nng+push+tcp://host:port - NNG Push over TCP
252
254
  """
253
255
 
254
256
  value: str
255
- protocol: Optional[TransportProtocol] = None
256
- is_file: bool = False
257
- address: str = ""
257
+ protocol: TransportProtocol
258
+ address: str
258
259
  parameters: Dict[str, str] = field(default_factory=dict)
259
260
 
260
261
  @classmethod
@@ -296,32 +297,32 @@ class SegmentationConnectionString:
296
297
 
297
298
  # Parse protocol and address
298
299
  scheme_end = endpoint_part.find("://")
299
- if scheme_end > 0:
300
- protocol_str = endpoint_part[:scheme_end]
301
- path_part = endpoint_part[scheme_end + 3 :] # skip "://"
302
-
303
- if protocol_str.lower() == "file":
304
- address = "/" + path_part # Restore absolute path
305
- is_file = True
306
- protocol = None
307
- else:
308
- protocol = TransportProtocol.try_parse(protocol_str)
309
- if protocol is None:
310
- return None
311
- address = protocol.create_nng_address(path_part)
312
- is_file = False
313
- elif s.startswith("/"):
314
- # Assume absolute file path
315
- address = s
316
- is_file = True
317
- protocol = None
300
+ if scheme_end <= 0:
301
+ return None
302
+
303
+ schema_str = endpoint_part[:scheme_end]
304
+ path_part = endpoint_part[scheme_end + 3 :] # skip "://"
305
+
306
+ protocol = TransportProtocol.try_parse(schema_str)
307
+ if protocol is None:
308
+ return None
309
+
310
+ # Build address based on protocol type
311
+ if protocol.is_file:
312
+ # file:///absolute/path -> /absolute/path
313
+ address = path_part if path_part.startswith("/") else "/" + path_part
314
+ elif protocol.is_socket:
315
+ # socket:///tmp/sock -> /tmp/sock
316
+ address = path_part if path_part.startswith("/") else "/" + path_part
317
+ elif protocol.is_nng:
318
+ # NNG protocols need proper address format
319
+ address = protocol.create_nng_address(path_part)
318
320
  else:
319
321
  return None
320
322
 
321
323
  return cls(
322
324
  value=s,
323
325
  protocol=protocol,
324
- is_file=is_file,
325
326
  address=address,
326
327
  parameters=parameters,
327
328
  )
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  from rocket_welder_sdk.keypoints_protocol import IKeyPointsWriter
18
18
  from rocket_welder_sdk.segmentation_result import SegmentationResultWriter
19
19
 
20
- from .schema import KeyPoint, SegmentClass
20
+ from .schema import KeyPointDefinition, SegmentClass
21
21
 
22
22
  # Type aliases
23
23
  Point = Tuple[int, int]
@@ -37,12 +37,12 @@ class IKeyPointsDataContext(ABC):
37
37
  pass
38
38
 
39
39
  @abstractmethod
40
- def add(self, point: KeyPoint, x: int, y: int, confidence: float) -> None:
40
+ def add(self, point: KeyPointDefinition, x: int, y: int, confidence: float) -> None:
41
41
  """
42
42
  Add a keypoint detection for this frame.
43
43
 
44
44
  Args:
45
- point: KeyPoint from schema definition
45
+ point: KeyPointDefinition from schema definition
46
46
  x: X coordinate in pixels
47
47
  y: Y coordinate in pixels
48
48
  confidence: Detection confidence (0.0 to 1.0)
@@ -50,17 +50,22 @@ class IKeyPointsDataContext(ABC):
50
50
  pass
51
51
 
52
52
  @abstractmethod
53
- def add_point(self, point: KeyPoint, position: Point, confidence: float) -> None:
53
+ def add_point(self, point: KeyPointDefinition, position: Point, confidence: float) -> None:
54
54
  """
55
55
  Add a keypoint detection using a Point tuple.
56
56
 
57
57
  Args:
58
- point: KeyPoint from schema definition
58
+ point: KeyPointDefinition from schema definition
59
59
  position: (x, y) tuple
60
60
  confidence: Detection confidence (0.0 to 1.0)
61
61
  """
62
62
  pass
63
63
 
64
+ @abstractmethod
65
+ def commit(self) -> None:
66
+ """Commit the context (called automatically when delegate returns)."""
67
+ pass
68
+
64
69
 
65
70
  class ISegmentationDataContext(ABC):
66
71
  """
@@ -92,6 +97,11 @@ class ISegmentationDataContext(ABC):
92
97
  """
93
98
  pass
94
99
 
100
+ @abstractmethod
101
+ def commit(self) -> None:
102
+ """Commit the context (called automatically when delegate returns)."""
103
+ pass
104
+
95
105
 
96
106
  class KeyPointsDataContext(IKeyPointsDataContext):
97
107
  """Implementation of keypoints data context."""
@@ -101,8 +111,6 @@ class KeyPointsDataContext(IKeyPointsDataContext):
101
111
  frame_id: int,
102
112
  writer: IKeyPointsWriter,
103
113
  ) -> None:
104
- from .schema import KeyPoint # noqa: F401
105
-
106
114
  self._frame_id = frame_id
107
115
  self._writer = writer
108
116
 
@@ -110,11 +118,11 @@ class KeyPointsDataContext(IKeyPointsDataContext):
110
118
  def frame_id(self) -> int:
111
119
  return self._frame_id
112
120
 
113
- def add(self, point: KeyPoint, x: int, y: int, confidence: float) -> None:
121
+ def add(self, point: KeyPointDefinition, x: int, y: int, confidence: float) -> None:
114
122
  """Add a keypoint detection for this frame."""
115
123
  self._writer.append(point.id, x, y, confidence)
116
124
 
117
- def add_point(self, point: KeyPoint, position: Point, confidence: float) -> None:
125
+ def add_point(self, point: KeyPointDefinition, position: Point, confidence: float) -> None:
118
126
  """Add a keypoint detection using a Point tuple."""
119
127
  self._writer.append_point(point.id, position, confidence)
120
128
 
@@ -131,8 +139,6 @@ class SegmentationDataContext(ISegmentationDataContext):
131
139
  frame_id: int,
132
140
  writer: SegmentationResultWriter,
133
141
  ) -> None:
134
- from .schema import SegmentClass # noqa: F401
135
-
136
142
  self._frame_id = frame_id
137
143
  self._writer = writer
138
144