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.
- rocket_welder_sdk/__init__.py +44 -22
- rocket_welder_sdk/binary_frame_reader.py +222 -0
- rocket_welder_sdk/binary_frame_writer.py +213 -0
- rocket_welder_sdk/confidence.py +206 -0
- rocket_welder_sdk/delta_frame.py +150 -0
- rocket_welder_sdk/graphics/__init__.py +42 -0
- rocket_welder_sdk/graphics/layer_canvas.py +157 -0
- rocket_welder_sdk/graphics/protocol.py +72 -0
- rocket_welder_sdk/graphics/rgb_color.py +109 -0
- rocket_welder_sdk/graphics/stage.py +494 -0
- rocket_welder_sdk/graphics/vector_graphics_encoder.py +575 -0
- rocket_welder_sdk/high_level/__init__.py +8 -1
- rocket_welder_sdk/high_level/client.py +114 -3
- rocket_welder_sdk/high_level/connection_strings.py +88 -15
- rocket_welder_sdk/high_level/frame_sink_factory.py +2 -15
- rocket_welder_sdk/high_level/transport_protocol.py +4 -130
- rocket_welder_sdk/keypoints_protocol.py +520 -55
- rocket_welder_sdk/rocket_welder_client.py +210 -89
- rocket_welder_sdk/segmentation_result.py +387 -2
- rocket_welder_sdk/session_id.py +7 -182
- rocket_welder_sdk/transport/__init__.py +10 -3
- rocket_welder_sdk/transport/frame_sink.py +3 -3
- rocket_welder_sdk/transport/frame_source.py +2 -2
- rocket_welder_sdk/transport/websocket_transport.py +316 -0
- rocket_welder_sdk/varint.py +213 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/METADATA +1 -4
- rocket_welder_sdk-1.1.45.dist-info/RECORD +51 -0
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/WHEEL +1 -1
- rocket_welder_sdk/transport/nng_transport.py +0 -197
- rocket_welder_sdk-1.1.43.dist-info/RECORD +0 -40
- {rocket_welder_sdk-1.1.43.dist-info → rocket_welder_sdk-1.1.45.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
rocket_welder_sdk/session_id.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""SessionId parsing utilities
|
|
1
|
+
"""SessionId parsing utilities.
|
|
2
2
|
|
|
3
3
|
SessionId format: ps-{guid} (e.g., ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890)
|
|
4
4
|
Prefix "ps" = PipelineSession.
|
|
@@ -6,33 +6,20 @@ Prefix "ps" = PipelineSession.
|
|
|
6
6
|
This module provides utilities to:
|
|
7
7
|
1. Parse SessionId from environment variable
|
|
8
8
|
2. Extract the Guid portion
|
|
9
|
-
3. Generate NNG IPC URLs for streaming results
|
|
10
|
-
4. Read explicit NNG URLs from environment variables (preferred)
|
|
11
9
|
|
|
12
|
-
## URL Configuration
|
|
10
|
+
## URL Configuration
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- KEYPOINTS_SINK_URL
|
|
19
|
-
- ACTIONS_SINK_URL
|
|
20
|
-
|
|
21
|
-
2. **Derived from SessionId (FALLBACK)** - For backwards compatibility:
|
|
22
|
-
- SessionId env var → parse GUID → generate URLs
|
|
23
|
-
|
|
24
|
-
Use `get_nng_urls_from_env()` for explicit URLs (preferred).
|
|
25
|
-
Use `get_nng_urls(session_id)` for SessionId-derived URLs (fallback).
|
|
12
|
+
Output URLs are configured via environment variables set by rocket-welder2:
|
|
13
|
+
- SEGMENTATION_SINK_URL: URL for segmentation output (e.g., socket:///tmp/seg.sock)
|
|
14
|
+
- KEYPOINTS_SINK_URL: URL for keypoints output (e.g., socket:///tmp/kp.sock)
|
|
15
|
+
- ACTIONS_SINK_URL: URL for actions output
|
|
26
16
|
"""
|
|
27
17
|
|
|
28
18
|
from __future__ import annotations
|
|
29
19
|
|
|
30
|
-
import logging
|
|
31
20
|
import os
|
|
32
21
|
import uuid
|
|
33
22
|
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
23
|
SESSION_ID_PREFIX = "ps-"
|
|
37
24
|
SESSION_ID_ENV_VAR = "SessionId"
|
|
38
25
|
|
|
@@ -40,6 +27,7 @@ SESSION_ID_ENV_VAR = "SessionId"
|
|
|
40
27
|
SEGMENTATION_SINK_URL_ENV = "SEGMENTATION_SINK_URL"
|
|
41
28
|
KEYPOINTS_SINK_URL_ENV = "KEYPOINTS_SINK_URL"
|
|
42
29
|
ACTIONS_SINK_URL_ENV = "ACTIONS_SINK_URL"
|
|
30
|
+
GRAPHICS_SINK_URL_ENV = "GRAPHICS_SINK_URL"
|
|
43
31
|
|
|
44
32
|
|
|
45
33
|
def parse_session_id(session_id: str) -> uuid.UUID:
|
|
@@ -73,166 +61,3 @@ def get_session_id_from_env() -> str | None:
|
|
|
73
61
|
SessionId string or None if not set
|
|
74
62
|
"""
|
|
75
63
|
return os.environ.get(SESSION_ID_ENV_VAR)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_nng_urls(session_id: str) -> dict[str, str]:
|
|
79
|
-
"""Generate NNG IPC URLs from SessionId.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
Dictionary with 'segmentation', 'keypoints', 'actions' URLs
|
|
86
|
-
|
|
87
|
-
Examples:
|
|
88
|
-
>>> urls = get_nng_urls("ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890")
|
|
89
|
-
>>> urls["segmentation"]
|
|
90
|
-
'ipc:///tmp/rw-a1b2c3d4-e5f6-7890-abcd-ef1234567890-seg.sock'
|
|
91
|
-
"""
|
|
92
|
-
guid = parse_session_id(session_id)
|
|
93
|
-
return {
|
|
94
|
-
"segmentation": f"ipc:///tmp/rw-{guid}-seg.sock",
|
|
95
|
-
"keypoints": f"ipc:///tmp/rw-{guid}-kp.sock",
|
|
96
|
-
"actions": f"ipc:///tmp/rw-{guid}-actions.sock",
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def get_segmentation_url(session_id: str) -> str:
|
|
101
|
-
"""Get NNG URL for segmentation stream.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
IPC URL for segmentation stream
|
|
108
|
-
"""
|
|
109
|
-
guid = parse_session_id(session_id)
|
|
110
|
-
return f"ipc:///tmp/rw-{guid}-seg.sock"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def get_keypoints_url(session_id: str) -> str:
|
|
114
|
-
"""Get NNG URL for keypoints stream.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
IPC URL for keypoints stream
|
|
121
|
-
"""
|
|
122
|
-
guid = parse_session_id(session_id)
|
|
123
|
-
return f"ipc:///tmp/rw-{guid}-kp.sock"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def get_actions_url(session_id: str) -> str:
|
|
127
|
-
"""Get NNG URL for actions stream.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
IPC URL for actions stream
|
|
134
|
-
"""
|
|
135
|
-
guid = parse_session_id(session_id)
|
|
136
|
-
return f"ipc:///tmp/rw-{guid}-actions.sock"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# ============================================================================
|
|
140
|
-
# Explicit URL functions (PREFERRED - URLs set by rocket-welder2)
|
|
141
|
-
# ============================================================================
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def get_nng_urls_from_env() -> dict[str, str | None]:
|
|
145
|
-
"""Get NNG URLs from explicit environment variables.
|
|
146
|
-
|
|
147
|
-
This is the PREFERRED method for getting NNG URLs. rocket-welder2
|
|
148
|
-
sets these environment variables when starting containers.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Dictionary with 'segmentation', 'keypoints', 'actions' URLs.
|
|
152
|
-
Values are None if not configured.
|
|
153
|
-
|
|
154
|
-
Examples:
|
|
155
|
-
>>> os.environ["SEGMENTATION_SINK_URL"] = "ipc:///tmp/rw-abc-seg.sock"
|
|
156
|
-
>>> urls = get_nng_urls_from_env()
|
|
157
|
-
>>> urls["segmentation"]
|
|
158
|
-
'ipc:///tmp/rw-abc-seg.sock'
|
|
159
|
-
"""
|
|
160
|
-
return {
|
|
161
|
-
"segmentation": os.environ.get(SEGMENTATION_SINK_URL_ENV),
|
|
162
|
-
"keypoints": os.environ.get(KEYPOINTS_SINK_URL_ENV),
|
|
163
|
-
"actions": os.environ.get(ACTIONS_SINK_URL_ENV),
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def get_segmentation_url_from_env() -> str | None:
|
|
168
|
-
"""Get segmentation NNG URL from environment variable.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
IPC URL for segmentation stream, or None if not configured.
|
|
172
|
-
"""
|
|
173
|
-
return os.environ.get(SEGMENTATION_SINK_URL_ENV)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def get_keypoints_url_from_env() -> str | None:
|
|
177
|
-
"""Get keypoints NNG URL from environment variable.
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
IPC URL for keypoints stream, or None if not configured.
|
|
181
|
-
"""
|
|
182
|
-
return os.environ.get(KEYPOINTS_SINK_URL_ENV)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def get_actions_url_from_env() -> str | None:
|
|
186
|
-
"""Get actions NNG URL from environment variable.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
IPC URL for actions stream, or None if not configured.
|
|
190
|
-
"""
|
|
191
|
-
return os.environ.get(ACTIONS_SINK_URL_ENV)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def has_explicit_nng_urls() -> bool:
|
|
195
|
-
"""Check if explicit NNG URLs are configured.
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
True if at least segmentation OR keypoints URL is configured.
|
|
199
|
-
"""
|
|
200
|
-
urls = get_nng_urls_from_env()
|
|
201
|
-
return bool(urls["segmentation"] or urls["keypoints"])
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def get_configured_nng_urls() -> dict[str, str]:
|
|
205
|
-
"""Get all configured NNG URLs (explicit or derived from SessionId).
|
|
206
|
-
|
|
207
|
-
Priority:
|
|
208
|
-
1. Explicit URLs from environment (SEGMENTATION_SINK_URL, etc.)
|
|
209
|
-
2. Derived from SessionId environment variable (fallback)
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Dictionary with 'segmentation', 'keypoints', 'actions' URLs.
|
|
213
|
-
Only includes URLs that are actually configured.
|
|
214
|
-
|
|
215
|
-
Raises:
|
|
216
|
-
ValueError: If no NNG URLs are configured (neither explicit nor SessionId).
|
|
217
|
-
"""
|
|
218
|
-
# Try explicit URLs first (preferred)
|
|
219
|
-
explicit_urls = get_nng_urls_from_env()
|
|
220
|
-
result: dict[str, str] = {}
|
|
221
|
-
|
|
222
|
-
for name, url in explicit_urls.items():
|
|
223
|
-
if url:
|
|
224
|
-
result[name] = url
|
|
225
|
-
|
|
226
|
-
# If we have at least one explicit URL, return what we have
|
|
227
|
-
if result:
|
|
228
|
-
return result
|
|
229
|
-
|
|
230
|
-
# Fallback: derive from SessionId
|
|
231
|
-
session_id = get_session_id_from_env()
|
|
232
|
-
if session_id:
|
|
233
|
-
return get_nng_urls(session_id)
|
|
234
|
-
|
|
235
|
-
raise ValueError(
|
|
236
|
-
"No NNG URLs configured. Set SEGMENTATION_SINK_URL/KEYPOINTS_SINK_URL "
|
|
237
|
-
"environment variables, or set SessionId for URL derivation."
|
|
238
|
-
)
|
|
@@ -6,7 +6,6 @@ Provides transport-agnostic frame sink/source abstractions for protocols.
|
|
|
6
6
|
|
|
7
7
|
from .frame_sink import IFrameSink, NullFrameSink
|
|
8
8
|
from .frame_source import IFrameSource
|
|
9
|
-
from .nng_transport import NngFrameSink, NngFrameSource
|
|
10
9
|
from .stream_transport import StreamFrameSink, StreamFrameSource
|
|
11
10
|
from .tcp_transport import TcpFrameSink, TcpFrameSource
|
|
12
11
|
from .unix_socket_transport import (
|
|
@@ -14,12 +13,16 @@ from .unix_socket_transport import (
|
|
|
14
13
|
UnixSocketFrameSource,
|
|
15
14
|
UnixSocketServer,
|
|
16
15
|
)
|
|
16
|
+
from .websocket_transport import (
|
|
17
|
+
WebSocketFrameSink,
|
|
18
|
+
WebSocketFrameSource,
|
|
19
|
+
connect_websocket_sink,
|
|
20
|
+
connect_websocket_source,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
__all__ = [
|
|
19
24
|
"IFrameSink",
|
|
20
25
|
"IFrameSource",
|
|
21
|
-
"NngFrameSink",
|
|
22
|
-
"NngFrameSource",
|
|
23
26
|
"NullFrameSink",
|
|
24
27
|
"StreamFrameSink",
|
|
25
28
|
"StreamFrameSource",
|
|
@@ -28,4 +31,8 @@ __all__ = [
|
|
|
28
31
|
"UnixSocketFrameSink",
|
|
29
32
|
"UnixSocketFrameSource",
|
|
30
33
|
"UnixSocketServer",
|
|
34
|
+
"WebSocketFrameSink",
|
|
35
|
+
"WebSocketFrameSource",
|
|
36
|
+
"connect_websocket_sink",
|
|
37
|
+
"connect_websocket_source",
|
|
31
38
|
]
|
|
@@ -9,7 +9,7 @@ class IFrameSink(ABC):
|
|
|
9
9
|
|
|
10
10
|
Transport-agnostic interface that handles the question: "where do frames go?"
|
|
11
11
|
This abstraction decouples protocol logic (KeyPoints, SegmentationResults) from
|
|
12
|
-
transport mechanisms (File, TCP, WebSocket,
|
|
12
|
+
transport mechanisms (File, TCP, WebSocket, Unix Socket). Each frame is written atomically.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
@abstractmethod
|
|
@@ -37,7 +37,7 @@ class IFrameSink(ABC):
|
|
|
37
37
|
"""
|
|
38
38
|
Flush any buffered data to the transport synchronously.
|
|
39
39
|
|
|
40
|
-
For message-based transports (
|
|
40
|
+
For message-based transports (WebSocket), this may be a no-op.
|
|
41
41
|
"""
|
|
42
42
|
pass
|
|
43
43
|
|
|
@@ -46,7 +46,7 @@ class IFrameSink(ABC):
|
|
|
46
46
|
"""
|
|
47
47
|
Flush any buffered data to the transport asynchronously.
|
|
48
48
|
|
|
49
|
-
For message-based transports (
|
|
49
|
+
For message-based transports (WebSocket), this may be a no-op.
|
|
50
50
|
"""
|
|
51
51
|
pass
|
|
52
52
|
|