rocket-welder-sdk 1.1.42__py3-none-any.whl → 1.1.44__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.
@@ -24,12 +24,6 @@ from .segmentation_result import (
24
24
  ISegmentationResultWriter,
25
25
  SegmentationResultSink,
26
26
  )
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
27
 
34
28
  if TYPE_CHECKING:
35
29
  import numpy.typing as npt
@@ -67,9 +61,6 @@ class RocketWelderClient:
67
61
  self._controller: Optional[IController] = None
68
62
  self._lock = threading.Lock()
69
63
 
70
- # NNG publishers for streaming results (auto-created if SessionId env var is set)
71
- self._nng_publishers: dict[str, NngFrameSink] = {}
72
-
73
64
  # Preview support
74
65
  self._preview_enabled = (
75
66
  self._connection.parameters.get("preview", "false").lower() == "true"
@@ -89,50 +80,6 @@ class RocketWelderClient:
89
80
  with self._lock:
90
81
  return self._controller is not None and self._controller.is_running
91
82
 
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
83
  def get_metadata(self) -> Optional[GstMetadata]:
137
84
  """
138
85
  Get the current GStreamer metadata.
@@ -180,21 +127,6 @@ class RocketWelderClient:
180
127
  else:
181
128
  raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
182
129
 
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
130
  # If preview is enabled, wrap the callback to capture frames
199
131
  if self._preview_enabled:
200
132
  self._original_callback = on_frame
@@ -403,15 +335,6 @@ class RocketWelderClient:
403
335
  if self._preview_enabled:
404
336
  self._preview_queue.put(None) # Sentinel value
405
337
 
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
338
  logger.info("RocketWelder client stopped")
416
339
 
417
340
  def show(self, cancellation_token: Optional[threading.Event] = None) -> None:
@@ -23,13 +23,22 @@ import io
23
23
  import struct
24
24
  from abc import ABC, abstractmethod
25
25
  from dataclasses import dataclass
26
- from typing import BinaryIO, Iterator, List, Optional, Tuple, Union
26
+ from typing import (
27
+ AsyncIterator,
28
+ BinaryIO,
29
+ Iterator,
30
+ List,
31
+ Optional,
32
+ Sequence,
33
+ Tuple,
34
+ Union,
35
+ )
27
36
 
28
37
  import numpy as np
29
38
  import numpy.typing as npt
30
39
  from typing_extensions import TypeAlias
31
40
 
32
- from .transport import IFrameSink, StreamFrameSink
41
+ from .transport import IFrameSink, IFrameSource, StreamFrameSink
33
42
 
34
43
  # Type aliases
35
44
  Point = Tuple[int, int]
@@ -122,6 +131,40 @@ class SegmentationInstance:
122
131
  return [(int(x), int(y)) for x, y in self.points]
123
132
 
124
133
 
134
+ @dataclass(frozen=True)
135
+ class SegmentationFrame:
136
+ """
137
+ Represents a decoded segmentation frame containing instance segmentation results.
138
+
139
+ Matches C# SegmentationFrame record struct.
140
+ Used for round-trip testing of segmentation protocol encoding/decoding.
141
+
142
+ Attributes:
143
+ frame_id: Frame identifier for temporal ordering.
144
+ width: Frame width in pixels.
145
+ height: Frame height in pixels.
146
+ instances: Segmentation instances detected in this frame.
147
+ """
148
+
149
+ frame_id: int
150
+ width: int
151
+ height: int
152
+ instances: Sequence[SegmentationInstance]
153
+
154
+ @property
155
+ def count(self) -> int:
156
+ """Number of instances in the frame."""
157
+ return len(self.instances)
158
+
159
+ def find_by_class(self, class_id: int) -> List[SegmentationInstance]:
160
+ """Find all instances with the specified class ID."""
161
+ return [inst for inst in self.instances if inst.class_id == class_id]
162
+
163
+ def find_by_instance(self, instance_id: int) -> List[SegmentationInstance]:
164
+ """Find all instances with the specified instance ID."""
165
+ return [inst for inst in self.instances if inst.instance_id == instance_id]
166
+
167
+
125
168
  class SegmentationResultWriter:
126
169
  """
127
170
  Writes segmentation results for a single frame via IFrameSink.
@@ -550,3 +593,345 @@ class SegmentationResultSink(ISegmentationResultSink):
550
593
  """Close the sink and release resources."""
551
594
  if self._owns_sink:
552
595
  self._frame_sink.close()
596
+
597
+
598
+ class ISegmentationResultSource(ABC):
599
+ """
600
+ Interface for streaming segmentation frames from a source.
601
+
602
+ Mirrors the pattern from IKeyPointsSource for consistency.
603
+ """
604
+
605
+ @abstractmethod
606
+ def read_frames(self) -> Iterator[SegmentationFrame]:
607
+ """
608
+ Read frames synchronously as an iterator.
609
+
610
+ Yields:
611
+ SegmentationFrame for each frame in the source.
612
+ """
613
+ pass
614
+
615
+ def read_frames_async(self) -> AsyncIterator[SegmentationFrame]:
616
+ """
617
+ Read frames asynchronously as an async iterator.
618
+
619
+ Yields:
620
+ SegmentationFrame for each frame in the source.
621
+
622
+ Raises:
623
+ NotImplementedError: Subclass must implement for async support.
624
+ """
625
+ raise NotImplementedError("Subclass must implement read_frames_async")
626
+
627
+ def close(self) -> None: # noqa: B027
628
+ """Close the source and release resources."""
629
+ pass
630
+
631
+ async def close_async(self) -> None: # noqa: B027
632
+ """Close the source and release resources asynchronously."""
633
+ pass
634
+
635
+ def __enter__(self) -> "ISegmentationResultSource":
636
+ """Context manager entry."""
637
+ return self
638
+
639
+ def __exit__(self, *args: object) -> None:
640
+ """Context manager exit."""
641
+ self.close()
642
+
643
+ async def __aenter__(self) -> "ISegmentationResultSource":
644
+ """Async context manager entry."""
645
+ return self
646
+
647
+ async def __aexit__(self, *args: object) -> None:
648
+ """Async context manager exit."""
649
+ await self.close_async()
650
+
651
+
652
+ class SegmentationResultSource(ISegmentationResultSource):
653
+ """
654
+ High-level segmentation result source that reads from any IFrameSource.
655
+
656
+ Wraps IFrameSource to provide iterator-based access to SegmentationFrame objects.
657
+
658
+ Thread-safe: No (caller must synchronize)
659
+ """
660
+
661
+ def __init__(self, frame_source: IFrameSource) -> None:
662
+ """
663
+ Create a segmentation result source.
664
+
665
+ Args:
666
+ frame_source: Low-level frame source to read from.
667
+ """
668
+ self._frame_source = frame_source
669
+ self._closed = False
670
+
671
+ def read_frames(self) -> Iterator[SegmentationFrame]:
672
+ """
673
+ Read frames synchronously as an iterator.
674
+
675
+ Yields:
676
+ SegmentationFrame for each frame in the source.
677
+
678
+ Raises:
679
+ RuntimeError: If source is closed.
680
+ """
681
+ if self._closed:
682
+ raise RuntimeError("SegmentationResultSource is closed")
683
+
684
+ while True:
685
+ frame_data = self._frame_source.read_frame()
686
+ if frame_data is None or len(frame_data) == 0:
687
+ break
688
+
689
+ frame = SegmentationProtocol.read(frame_data)
690
+ yield frame
691
+
692
+ async def read_frames_async(self) -> AsyncIterator[SegmentationFrame]:
693
+ """
694
+ Read frames asynchronously as an async iterator.
695
+
696
+ Yields:
697
+ SegmentationFrame for each frame in the source.
698
+
699
+ Raises:
700
+ RuntimeError: If source is closed.
701
+ """
702
+ if self._closed:
703
+ raise RuntimeError("SegmentationResultSource is closed")
704
+
705
+ while True:
706
+ frame_data = await self._frame_source.read_frame_async()
707
+ if frame_data is None or len(frame_data) == 0:
708
+ break
709
+
710
+ frame = SegmentationProtocol.read(frame_data)
711
+ yield frame
712
+
713
+ def close(self) -> None:
714
+ """Close the source and release resources."""
715
+ if self._closed:
716
+ return
717
+ self._closed = True
718
+ self._frame_source.close()
719
+
720
+ async def close_async(self) -> None:
721
+ """Close the source and release resources asynchronously."""
722
+ if self._closed:
723
+ return
724
+ self._closed = True
725
+ await self._frame_source.close_async()
726
+
727
+
728
+ class SegmentationProtocol:
729
+ """
730
+ Static helpers for encoding and decoding segmentation protocol data.
731
+
732
+ Pure protocol logic with no transport or rendering dependencies.
733
+ Matches C# SegmentationProtocol static class.
734
+
735
+ Frame Format:
736
+ [FrameId: 8 bytes, little-endian uint64]
737
+ [Width: varint]
738
+ [Height: varint]
739
+ [Instances...]
740
+
741
+ Instance Format:
742
+ [ClassId: 1 byte]
743
+ [InstanceId: 1 byte]
744
+ [PointCount: varint]
745
+ [Point0: X zigzag-varint, Y zigzag-varint] (absolute)
746
+ [Point1+: deltaX zigzag-varint, deltaY zigzag-varint]
747
+ """
748
+
749
+ @staticmethod
750
+ def write(buffer: bytearray, frame: SegmentationFrame) -> int:
751
+ """
752
+ Write a complete segmentation frame to a buffer.
753
+
754
+ Args:
755
+ buffer: Pre-allocated buffer to write to.
756
+ frame: Frame to encode.
757
+
758
+ Returns:
759
+ Number of bytes written.
760
+ """
761
+ stream = io.BytesIO()
762
+
763
+ # Write header
764
+ stream.write(struct.pack("<Q", frame.frame_id))
765
+ _write_varint(stream, frame.width)
766
+ _write_varint(stream, frame.height)
767
+
768
+ # Write instances
769
+ for instance in frame.instances:
770
+ SegmentationProtocol._write_instance_core(stream, instance)
771
+
772
+ data = stream.getvalue()
773
+ buffer[: len(data)] = data
774
+ return len(data)
775
+
776
+ @staticmethod
777
+ def _write_instance_core(stream: BinaryIO, instance: SegmentationInstance) -> None:
778
+ """Write a single instance to the stream."""
779
+ stream.write(bytes([instance.class_id, instance.instance_id]))
780
+ point_count = len(instance.points)
781
+ _write_varint(stream, point_count)
782
+
783
+ if point_count == 0:
784
+ return
785
+
786
+ prev_x, prev_y = 0, 0
787
+ for i, point in enumerate(instance.points):
788
+ x, y = int(point[0]), int(point[1])
789
+ if i == 0:
790
+ # First point is absolute (but still zigzag encoded)
791
+ _write_varint(stream, _zigzag_encode(x))
792
+ _write_varint(stream, _zigzag_encode(y))
793
+ else:
794
+ # Subsequent points are deltas
795
+ _write_varint(stream, _zigzag_encode(x - prev_x))
796
+ _write_varint(stream, _zigzag_encode(y - prev_y))
797
+ prev_x, prev_y = x, y
798
+
799
+ @staticmethod
800
+ def write_header(buffer: bytearray, frame_id: int, width: int, height: int) -> int:
801
+ """
802
+ Write just the frame header (frameId, width, height).
803
+
804
+ Args:
805
+ buffer: Pre-allocated buffer to write to.
806
+ frame_id: Frame identifier.
807
+ width: Frame width.
808
+ height: Frame height.
809
+
810
+ Returns:
811
+ Number of bytes written.
812
+ """
813
+ stream = io.BytesIO()
814
+ stream.write(struct.pack("<Q", frame_id))
815
+ _write_varint(stream, width)
816
+ _write_varint(stream, height)
817
+ data = stream.getvalue()
818
+ buffer[: len(data)] = data
819
+ return len(data)
820
+
821
+ @staticmethod
822
+ def write_instance(
823
+ buffer: bytearray,
824
+ class_id: int,
825
+ instance_id: int,
826
+ points: Union[List[Point], PointArray],
827
+ ) -> int:
828
+ """
829
+ Write a single segmentation instance.
830
+
831
+ Points are delta-encoded for compression.
832
+
833
+ Args:
834
+ buffer: Pre-allocated buffer to write to.
835
+ class_id: Class identifier (0-255).
836
+ instance_id: Instance identifier (0-255).
837
+ points: Polygon points.
838
+
839
+ Returns:
840
+ Number of bytes written.
841
+ """
842
+ # Convert to numpy array if needed
843
+ if not isinstance(points, np.ndarray):
844
+ points_array = np.array(points, dtype=np.int32)
845
+ else:
846
+ points_array = points.astype(np.int32)
847
+
848
+ instance = SegmentationInstance(class_id, instance_id, points_array)
849
+
850
+ stream = io.BytesIO()
851
+ SegmentationProtocol._write_instance_core(stream, instance)
852
+ data = stream.getvalue()
853
+ buffer[: len(data)] = data
854
+ return len(data)
855
+
856
+ @staticmethod
857
+ def calculate_instance_size(point_count: int) -> int:
858
+ """
859
+ Calculate the maximum buffer size needed for an instance.
860
+
861
+ Args:
862
+ point_count: Number of polygon points.
863
+
864
+ Returns:
865
+ Maximum bytes needed.
866
+ """
867
+ # classId(1) + instanceId(1) + pointCount(varint, max 5) + points(max 10 bytes each)
868
+ return 1 + 1 + 5 + (point_count * 10)
869
+
870
+ @staticmethod
871
+ def read(data: bytes) -> SegmentationFrame:
872
+ """
873
+ Read a complete segmentation frame from a buffer.
874
+
875
+ Args:
876
+ data: Raw frame data.
877
+
878
+ Returns:
879
+ Decoded SegmentationFrame.
880
+ """
881
+ stream = io.BytesIO(data)
882
+
883
+ # Read header
884
+ frame_id_bytes = stream.read(8)
885
+ if len(frame_id_bytes) != 8:
886
+ raise EOFError("Failed to read FrameId")
887
+ frame_id = struct.unpack("<Q", frame_id_bytes)[0]
888
+ width = _read_varint(stream)
889
+ height = _read_varint(stream)
890
+
891
+ # Read all instances
892
+ instances: List[SegmentationInstance] = []
893
+ while True:
894
+ header = stream.read(2)
895
+ if len(header) == 0:
896
+ break
897
+ if len(header) != 2:
898
+ raise EOFError("Unexpected end of stream reading instance header")
899
+
900
+ class_id = header[0]
901
+ instance_id = header[1]
902
+ point_count = _read_varint(stream)
903
+
904
+ if point_count == 0:
905
+ points = np.empty((0, 2), dtype=np.int32)
906
+ else:
907
+ points = np.empty((point_count, 2), dtype=np.int32)
908
+ prev_x, prev_y = 0, 0
909
+
910
+ for i in range(point_count):
911
+ x = _zigzag_decode(_read_varint(stream))
912
+ y = _zigzag_decode(_read_varint(stream))
913
+ if i > 0:
914
+ x += prev_x
915
+ y += prev_y
916
+ points[i] = [x, y]
917
+ prev_x, prev_y = x, y
918
+
919
+ instances.append(SegmentationInstance(class_id, instance_id, points))
920
+
921
+ return SegmentationFrame(frame_id, width, height, instances)
922
+
923
+ @staticmethod
924
+ def try_read(data: bytes) -> Optional[SegmentationFrame]:
925
+ """
926
+ Try to read a segmentation frame, returning None if the data is invalid.
927
+
928
+ Args:
929
+ data: Raw frame data.
930
+
931
+ Returns:
932
+ SegmentationFrame if successful, None otherwise.
933
+ """
934
+ try:
935
+ return SegmentationProtocol.read(data)
936
+ except Exception:
937
+ return None