rocket-welder-sdk 1.1.43__py3-none-any.whl → 1.1.45__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.
Files changed (31) hide show
  1. rocket_welder_sdk/__init__.py +44 -22
  2. rocket_welder_sdk/binary_frame_reader.py +222 -0
  3. rocket_welder_sdk/binary_frame_writer.py +213 -0
  4. rocket_welder_sdk/confidence.py +206 -0
  5. rocket_welder_sdk/delta_frame.py +150 -0
  6. rocket_welder_sdk/graphics/__init__.py +42 -0
  7. rocket_welder_sdk/graphics/layer_canvas.py +157 -0
  8. rocket_welder_sdk/graphics/protocol.py +72 -0
  9. rocket_welder_sdk/graphics/rgb_color.py +109 -0
  10. rocket_welder_sdk/graphics/stage.py +494 -0
  11. rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
  12. rocket_welder_sdk/high_level/__init__.py +8 -1
  13. rocket_welder_sdk/high_level/client.py +114 -3
  14. rocket_welder_sdk/high_level/connection_strings.py +88 -15
  15. rocket_welder_sdk/high_level/frame_sink_factory.py +2 -15
  16. rocket_welder_sdk/high_level/transport_protocol.py +4 -130
  17. rocket_welder_sdk/keypoints_protocol.py +520 -55
  18. rocket_welder_sdk/rocket_welder_client.py +210 -89
  19. rocket_welder_sdk/segmentation_result.py +387 -2
  20. rocket_welder_sdk/session_id.py +7 -182
  21. rocket_welder_sdk/transport/__init__.py +10 -3
  22. rocket_welder_sdk/transport/frame_sink.py +3 -3
  23. rocket_welder_sdk/transport/frame_source.py +2 -2
  24. rocket_welder_sdk/transport/websocket_transport.py +316 -0
  25. rocket_welder_sdk/varint.py +213 -0
  26. {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -4
  27. rocket_welder_sdk-1.1.45.dist-info/RECORD +51 -0
  28. {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
  29. rocket_welder_sdk/transport/nng_transport.py +0 -197
  30. rocket_welder_sdk-1.1.43.dist-info/RECORD +0 -40
  31. {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,12 @@ 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
18
+ from .graphics import ILayerCanvas, IStageSink, IStageWriter, RgbColor, StageSink
19
+ from .high_level.connection_strings import (
20
+ GraphicsConnectionString,
21
+ KeyPointsConnectionString,
22
+ SegmentationConnectionString,
23
+ )
19
24
  from .high_level.frame_sink_factory import FrameSinkFactory
20
25
  from .keypoints_protocol import IKeyPointsSink, IKeyPointsWriter, KeyPointsSink
21
26
  from .opencv_controller import OpenCvController
@@ -24,12 +29,6 @@ from .segmentation_result import (
24
29
  ISegmentationResultWriter,
25
30
  SegmentationResultSink,
26
31
  )
27
- from .session_id import (
28
- get_configured_nng_urls,
29
- get_nng_urls_from_env,
30
- has_explicit_nng_urls,
31
- )
32
- from .transport.nng_transport import NngFrameSink
33
32
 
34
33
  if TYPE_CHECKING:
35
34
  import numpy.typing as npt
@@ -67,9 +66,6 @@ class RocketWelderClient:
67
66
  self._controller: Optional[IController] = None
68
67
  self._lock = threading.Lock()
69
68
 
70
- # NNG publishers for streaming results (auto-created if SessionId env var is set)
71
- self._nng_publishers: dict[str, NngFrameSink] = {}
72
-
73
69
  # Preview support
74
70
  self._preview_enabled = (
75
71
  self._connection.parameters.get("preview", "false").lower() == "true"
@@ -89,50 +85,6 @@ class RocketWelderClient:
89
85
  with self._lock:
90
86
  return self._controller is not None and self._controller.is_running
91
87
 
92
- @property
93
- def nng_publishers(self) -> dict[str, NngFrameSink]:
94
- """Get NNG publishers for streaming results.
95
-
96
- Returns:
97
- Dictionary with 'segmentation', 'keypoints', 'actions' keys.
98
- Empty if SessionId env var was not set at startup.
99
-
100
- Example:
101
- client.nng_publishers["segmentation"].write_frame(seg_data)
102
- """
103
- return self._nng_publishers
104
-
105
- def _create_nng_publishers(self) -> None:
106
- """Create NNG publishers for result streaming.
107
-
108
- URLs are read from environment variables (preferred) or derived from SessionId (fallback).
109
-
110
- Priority:
111
- 1. Explicit URLs: SEGMENTATION_SINK_URL, KEYPOINTS_SINK_URL, ACTIONS_SINK_URL
112
- 2. Derived from SessionId environment variable (backwards compatibility)
113
- """
114
- try:
115
- urls = get_configured_nng_urls()
116
-
117
- for name, url in urls.items():
118
- sink = NngFrameSink.create_publisher(url)
119
- self._nng_publishers[name] = sink
120
- logger.info("NNG publisher ready: %s at %s", name, url)
121
-
122
- # Log configuration summary
123
- logger.info(
124
- "NNG publishers configured: seg=%s, kp=%s, actions=%s",
125
- urls.get("segmentation", "(not configured)"),
126
- urls.get("keypoints", "(not configured)"),
127
- urls.get("actions", "(not configured)"),
128
- )
129
- except ValueError as ex:
130
- # No URLs configured - this is expected for containers that don't publish results
131
- logger.debug("NNG publishers not configured: %s", ex)
132
- except Exception as ex:
133
- logger.warning("Failed to create NNG publishers: %s", ex)
134
- # Don't fail start() - NNG is optional for backwards compatibility
135
-
136
88
  def get_metadata(self) -> Optional[GstMetadata]:
137
89
  """
138
90
  Get the current GStreamer metadata.
@@ -180,21 +132,6 @@ class RocketWelderClient:
180
132
  else:
181
133
  raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
182
134
 
183
- # Auto-create NNG publishers if URLs are configured
184
- # (explicit URLs via SEGMENTATION_SINK_URL etc., or derived from SessionId)
185
- if has_explicit_nng_urls():
186
- self._create_nng_publishers()
187
- else:
188
- # Log that NNG is not configured (informational)
189
- urls = get_nng_urls_from_env()
190
- logger.info(
191
- "NNG sink URLs not configured (this is normal if not publishing AI results). "
192
- "seg=%s, kp=%s, actions=%s",
193
- urls.get("segmentation") or "(not set)",
194
- urls.get("keypoints") or "(not set)",
195
- urls.get("actions") or "(not set)",
196
- )
197
-
198
135
  # If preview is enabled, wrap the callback to capture frames
199
136
  if self._preview_enabled:
200
137
  self._original_callback = on_frame
@@ -256,25 +193,26 @@ class RocketWelderClient:
256
193
 
257
194
  def start_with_writers(
258
195
  self,
259
- on_frame: Callable[[Mat, ISegmentationResultWriter, IKeyPointsWriter, Mat], None], # type: ignore[valid-type]
196
+ on_frame: Callable[[Mat, ISegmentationResultWriter, IKeyPointsWriter, IStageWriter, Mat], None], # type: ignore[valid-type]
260
197
  cancellation_token: Optional[threading.Event] = None,
261
198
  ) -> None:
262
199
  """
263
- Start receiving frames with segmentation and keypoints output support.
200
+ Start receiving frames with segmentation, keypoints, and graphics output support.
264
201
 
265
202
  Creates sinks for streaming AI results to rocket-welder2.
266
203
 
267
204
  Configuration via environment variables:
268
205
  - SEGMENTATION_SINK_URL: URL for segmentation output (e.g., socket:///tmp/seg.sock)
269
206
  - KEYPOINTS_SINK_URL: URL for keypoints output (e.g., socket:///tmp/kp.sock)
207
+ - STAGE_SINK_URL: URL for graphics/stage output (e.g., socket:///tmp/stage.sock)
270
208
 
271
209
  Args:
272
- on_frame: Callback receiving (input_mat, seg_writer, kp_writer, output_mat).
210
+ on_frame: Callback receiving (input_mat, seg_writer, kp_writer, stage_writer, output_mat).
273
211
  The writers are created per-frame and auto-flush on context exit.
274
212
  cancellation_token: Optional cancellation token
275
213
 
276
214
  Example:
277
- def process_frame(input_mat, seg_writer, kp_writer, output_mat):
215
+ def process_frame(input_mat, seg_writer, kp_writer, stage_writer, output_mat):
278
216
  # Run AI inference
279
217
  result = ai_model.infer(input_mat)
280
218
 
@@ -286,6 +224,11 @@ class RocketWelderClient:
286
224
  for kp in result.keypoints:
287
225
  kp_writer.append(kp.id, kp.x, kp.y, kp.confidence)
288
226
 
227
+ # Draw graphics overlay
228
+ layer = stage_writer[0]
229
+ layer.set_font_size(24)
230
+ layer.draw_text("Detection count: 5", 10, 30)
231
+
289
232
  # Copy/draw to output
290
233
  output_mat[:] = input_mat
291
234
 
@@ -316,11 +259,13 @@ class RocketWelderClient:
316
259
  # Create sinks from environment
317
260
  seg_sink = self._get_or_create_segmentation_sink()
318
261
  kp_sink = self._get_or_create_keypoints_sink()
262
+ stage_sink = self._get_or_create_stage_sink()
319
263
 
320
264
  logger.info(
321
- "Starting RocketWelder client with AI output support: seg=%s, kp=%s",
265
+ "Starting RocketWelder client with AI output support: seg=%s, kp=%s, stage=%s",
322
266
  "configured" if seg_sink else "null",
323
267
  "configured" if kp_sink else "null",
268
+ "configured" if stage_sink else "null",
324
269
  )
325
270
 
326
271
  # Wrapper callback that creates per-frame writers
@@ -337,17 +282,26 @@ class RocketWelderClient:
337
282
  frame_metadata.frame_number,
338
283
  )
339
284
  # Use no-op writers
340
- on_frame(
341
- input_mat, _NoOpSegmentationWriter(), _NoOpKeyPointsWriter(), output_mat
342
- )
285
+ with stage_sink.create_writer(frame_metadata.frame_number) as stage_writer:
286
+ on_frame(
287
+ input_mat,
288
+ _NoOpSegmentationWriter(),
289
+ _NoOpKeyPointsWriter(),
290
+ stage_writer,
291
+ output_mat,
292
+ )
343
293
  return
344
294
 
345
- # Create per-frame writers from sinks
295
+ # Create per-frame writers from sinks (all auto-flush on context exit)
346
296
  with seg_sink.create_writer(
347
297
  frame_metadata.frame_number, caps.width, caps.height
348
- ) as seg_writer, kp_sink.create_writer(frame_metadata.frame_number) as kp_writer:
298
+ ) as seg_writer, kp_sink.create_writer(
299
+ frame_metadata.frame_number
300
+ ) as kp_writer, stage_sink.create_writer(
301
+ frame_metadata.frame_number
302
+ ) as stage_writer:
349
303
  # Call user callback with writers
350
- on_frame(input_mat, seg_writer, kp_writer, output_mat)
304
+ on_frame(input_mat, seg_writer, kp_writer, stage_writer, output_mat)
351
305
  # Writers auto-flush on context exit
352
306
 
353
307
  # Start the controller with our wrapper
@@ -392,6 +346,23 @@ class RocketWelderClient:
392
346
  logger.warning("Failed to create keypoints sink from %s: %s", url, ex)
393
347
  return _NullKeyPointsSink()
394
348
 
349
+ def _get_or_create_stage_sink(self) -> IStageSink:
350
+ """Get or create graphics stage sink from environment."""
351
+ import os
352
+
353
+ url = os.environ.get("GRAPHICS_SINK_URL")
354
+ if not url:
355
+ logger.debug("GRAPHICS_SINK_URL not set, using null sink")
356
+ return _NullStageSink()
357
+
358
+ try:
359
+ cs = GraphicsConnectionString.parse(url)
360
+ frame_sink = FrameSinkFactory.create(cs.protocol, cs.address)
361
+ return StageSink(frame_sink=frame_sink, owns_sink=True)
362
+ except Exception as ex:
363
+ logger.warning("Failed to create graphics stage sink from %s: %s", url, ex)
364
+ return _NullStageSink()
365
+
395
366
  def stop(self) -> None:
396
367
  """Stop the client and clean up resources."""
397
368
  with self._lock:
@@ -403,15 +374,6 @@ class RocketWelderClient:
403
374
  if self._preview_enabled:
404
375
  self._preview_queue.put(None) # Sentinel value
405
376
 
406
- # Clean up NNG publishers
407
- for name, sink in self._nng_publishers.items():
408
- try:
409
- sink.close()
410
- logger.debug("Closed NNG publisher: %s", name)
411
- except Exception as ex:
412
- logger.warning("Failed to close NNG publisher %s: %s", name, ex)
413
- self._nng_publishers.clear()
414
-
415
377
  logger.info("RocketWelder client stopped")
416
378
 
417
379
  def show(self, cancellation_token: Optional[threading.Event] = None) -> None:
@@ -697,3 +659,162 @@ class _NullSegmentationSink(ISegmentationResultSink):
697
659
  def close(self) -> None:
698
660
  """No-op close."""
699
661
  pass
662
+
663
+
664
+ class _NoOpLayerCanvas(ILayerCanvas):
665
+ """No-op layer canvas that discards all drawing operations."""
666
+
667
+ @property
668
+ def layer_id(self) -> int:
669
+ """The layer ID."""
670
+ return 0
671
+
672
+ # Frame type
673
+ def master(self) -> None:
674
+ """No-op."""
675
+ pass
676
+
677
+ def remain(self) -> None:
678
+ """No-op."""
679
+ pass
680
+
681
+ def clear(self) -> None:
682
+ """No-op."""
683
+ pass
684
+
685
+ # Context state - Styling
686
+ def set_stroke(self, color: RgbColor) -> None:
687
+ """No-op."""
688
+ pass
689
+
690
+ def set_fill(self, color: RgbColor) -> None:
691
+ """No-op."""
692
+ pass
693
+
694
+ def set_thickness(self, width: int) -> None:
695
+ """No-op."""
696
+ pass
697
+
698
+ def set_font_size(self, size: int) -> None:
699
+ """No-op."""
700
+ pass
701
+
702
+ def set_font_color(self, color: RgbColor) -> None:
703
+ """No-op."""
704
+ pass
705
+
706
+ # Context state - Transforms
707
+ def translate(self, dx: float, dy: float) -> None:
708
+ """No-op."""
709
+ pass
710
+
711
+ def rotate(self, degrees: float) -> None:
712
+ """No-op."""
713
+ pass
714
+
715
+ def scale(self, sx: float, sy: float) -> None:
716
+ """No-op."""
717
+ pass
718
+
719
+ def skew(self, kx: float, ky: float) -> None:
720
+ """No-op."""
721
+ pass
722
+
723
+ def set_matrix(
724
+ self,
725
+ scale_x: float,
726
+ skew_x: float,
727
+ trans_x: float,
728
+ skew_y: float,
729
+ scale_y: float,
730
+ trans_y: float,
731
+ ) -> None:
732
+ """No-op."""
733
+ pass
734
+
735
+ # Context stack
736
+ def save(self) -> None:
737
+ """No-op."""
738
+ pass
739
+
740
+ def restore(self) -> None:
741
+ """No-op."""
742
+ pass
743
+
744
+ def reset_context(self) -> None:
745
+ """No-op."""
746
+ pass
747
+
748
+ # Draw operations
749
+ def draw_polygon(self, points: Any) -> None:
750
+ """No-op."""
751
+ pass
752
+
753
+ def draw_text(self, text: str, x: int, y: int) -> None:
754
+ """No-op."""
755
+ pass
756
+
757
+ def draw_circle(self, center_x: int, center_y: int, radius: int) -> None:
758
+ """No-op."""
759
+ pass
760
+
761
+ def draw_rectangle(self, x: int, y: int, width: int, height: int) -> None:
762
+ """No-op."""
763
+ pass
764
+
765
+ def draw_line(self, x1: int, y1: int, x2: int, y2: int) -> None:
766
+ """No-op."""
767
+ pass
768
+
769
+ def draw_jpeg(self, jpeg_data: bytes, x: int, y: int, width: int, height: int) -> None:
770
+ """No-op."""
771
+ pass
772
+
773
+
774
+ # Singleton instance
775
+ _NO_OP_LAYER_CANVAS = _NoOpLayerCanvas()
776
+
777
+
778
+ class _NoOpStageWriter(IStageWriter):
779
+ """No-op stage writer that discards all graphics operations."""
780
+
781
+ @property
782
+ def frame_id(self) -> int:
783
+ """The frame ID."""
784
+ return 0
785
+
786
+ def __getitem__(self, layer_id: int) -> ILayerCanvas:
787
+ """Returns no-op layer canvas."""
788
+ return _NO_OP_LAYER_CANVAS
789
+
790
+ def layer(self, layer_id: int) -> ILayerCanvas:
791
+ """Returns no-op layer canvas."""
792
+ return _NO_OP_LAYER_CANVAS
793
+
794
+ def close(self) -> None:
795
+ """No-op close."""
796
+ pass
797
+
798
+ def __enter__(self) -> _NoOpStageWriter:
799
+ """Context manager entry."""
800
+ return self
801
+
802
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
803
+ """Context manager exit."""
804
+ pass
805
+
806
+
807
+ # Singleton instance
808
+ _NO_OP_STAGE_WRITER = _NoOpStageWriter()
809
+
810
+
811
+ class _NullStageSink(IStageSink):
812
+ """Null stage sink that creates no-op writers."""
813
+
814
+ def create_writer(self, frame_id: int) -> IStageWriter:
815
+ """Create a no-op writer."""
816
+ return _NO_OP_STAGE_WRITER
817
+
818
+ def close(self) -> None:
819
+ """No-op close."""
820
+ pass