sequence-base 0.4.0__tar.gz → 0.4.2__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 (106) hide show
  1. {sequence_base-0.4.0 → sequence_base-0.4.2}/PKG-INFO +1 -1
  2. {sequence_base-0.4.0 → sequence_base-0.4.2}/pyproject.toml +1 -1
  3. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/codec/__init__.py +6 -2
  4. sequence_base-0.4.2/sequence_base/codec/image.py +210 -0
  5. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/constants/version.py +2 -1
  6. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/deployments.py +8 -2
  7. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/io.py +5 -1
  8. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/presets/robot_arm.py +68 -2
  9. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ActRequest.json +1 -1
  10. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ActResponse.json +1 -1
  11. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ActionChunk.json +1 -1
  12. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ActionContract.json +17 -1
  13. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ActionStep.json +1 -1
  14. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/AdapterTransform.json +1 -1
  15. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/AddLocal.json +1 -1
  16. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Autoscaler.json +1 -1
  17. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/BenchmarkConditionSpec.json +1 -1
  18. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/BenchmarkRegisterRequest.json +17 -1
  19. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/BenchmarkSpec.json +2 -2
  20. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/BenchmarkStatusResponse.json +1 -1
  21. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/BootstrapResponse.json +1 -1
  22. sequence_base-0.4.2/sequence_base/schemas/Camera.json +76 -0
  23. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ClampInfo.json +1 -1
  24. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/CodeBundle.json +1 -1
  25. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Concurrency.json +1 -1
  26. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Conditions.json +1 -1
  27. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ConnectRequest.json +1 -1
  28. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ConnectResponse.json +56 -2
  29. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/DeploymentAccepted.json +1 -1
  30. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/DeploymentPolicy.json +56 -2
  31. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/DeploymentRequest.json +60 -2
  32. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/DeploymentStatusResponse.json +1 -1
  33. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Embodiment.json +56 -2
  34. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ErrorBody.json +1 -1
  35. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ErrorCodes.json +1 -1
  36. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Estimate.json +1 -1
  37. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/EvalDryRunResponse.json +1 -1
  38. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/EvalReport.json +1 -1
  39. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/EvalRequest.json +1 -1
  40. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/EvalResponse.json +1 -1
  41. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageFrame.json +1 -1
  42. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageSpec.json +1 -1
  43. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageStepAddLocal.json +1 -1
  44. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageStepApt.json +1 -1
  45. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageStepEnv.json +1 -1
  46. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageStepRun.json +1 -1
  47. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ImageStepUvPip.json +1 -1
  48. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/InferParam.json +1 -1
  49. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Lease.json +1 -1
  50. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/LossyTransform.json +1 -1
  51. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/MetricAgg.json +1 -1
  52. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Observation.json +1 -1
  53. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/ObservationContract.json +40 -2
  54. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/PolicyRegistration.json +57 -3
  55. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/PolicySpec.json +56 -2
  56. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Proprioception.json +1 -1
  57. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/RenderSpec.json +1 -1
  58. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Resources.json +1 -1
  59. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/RolloutResult.json +1 -1
  60. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/SecretCreateRequest.json +1 -1
  61. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/SecretInfo.json +1 -1
  62. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/SecretRef.json +1 -1
  63. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/SeqFlags.json +1 -1
  64. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Step.json +1 -1
  65. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/SuiteAgg.json +1 -1
  66. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/TaskAgg.json +1 -1
  67. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Timeouts.json +5 -1
  68. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/UploadTarget.json +1 -1
  69. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/Variant.json +1 -1
  70. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeAttach.json +1 -1
  71. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeFile.json +1 -1
  72. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeInfo.json +1 -1
  73. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeMount.json +1 -1
  74. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeRef.json +1 -1
  75. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeUploadRequest.json +1 -1
  76. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/VolumeUploadResponse.json +1 -1
  77. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schemas/WarmingError.json +1 -1
  78. sequence_base-0.4.0/sequence_base/codec/image.py +0 -119
  79. sequence_base-0.4.0/sequence_base/schemas/Camera.json +0 -38
  80. {sequence_base-0.4.0 → sequence_base-0.4.2}/.gitignore +0 -0
  81. {sequence_base-0.4.0 → sequence_base-0.4.2}/README.md +0 -0
  82. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/__init__.py +0 -0
  83. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/auth.py +0 -0
  84. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/constants/__init__.py +0 -0
  85. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/constants/headers.py +0 -0
  86. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/__init__.py +0 -0
  87. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/accounts.py +0 -0
  88. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/act.py +0 -0
  89. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/benchmarks.py +0 -0
  90. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/connect.py +0 -0
  91. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/errors.py +0 -0
  92. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/eval.py +0 -0
  93. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/flags.py +0 -0
  94. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/policy.py +0 -0
  95. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/templates.py +0 -0
  96. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/volumes.py +0 -0
  97. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/contract/wire.py +0 -0
  98. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/errors/__init__.py +0 -0
  99. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/errors/transport.py +0 -0
  100. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/obs/__init__.py +0 -0
  101. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/obs/log.py +0 -0
  102. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/obs/rid.py +0 -0
  103. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/presets/__init__.py +0 -0
  104. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/schema_export.py +0 -0
  105. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/telemetry/__init__.py +0 -0
  106. {sequence_base-0.4.0 → sequence_base-0.4.2}/sequence_base/telemetry/events.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sequence-base
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pydantic>=2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sequence-base"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = ["pydantic>=2.0"]
@@ -9,11 +9,15 @@ decodes, installs the light core and never imports this.
9
9
  """
10
10
  from sequence_base.codec.image import (
11
11
  DEFAULT_JPEG_QUALITY,
12
- MAX_UPLOAD_EDGE,
12
+ FILTERS,
13
+ MODES,
13
14
  decode,
14
15
  decode_depth,
15
16
  encode,
16
17
  encode_depth,
18
+ image_size,
19
+ resize_image,
17
20
  )
18
21
 
19
- __all__ = ["encode", "decode", "encode_depth", "decode_depth", "DEFAULT_JPEG_QUALITY", "MAX_UPLOAD_EDGE"]
22
+ __all__ = ["encode", "decode", "encode_depth", "decode_depth", "resize_image", "image_size", "DEFAULT_JPEG_QUALITY",
23
+ "MODES", "FILTERS"]
@@ -0,0 +1,210 @@
1
+ """
2
+ The image codec: one camera frame <-> the base64 string the wire carries.
3
+
4
+ `encode` (frame -> base64 JPEG) is the SDK's client-side path; `decode` (base64 -> uint8 RGB array)
5
+ is the container/worker's server-side path. They live together so the wire format — JPEG, quality
6
+ 95, RGB — is defined once and provable by a round-trip test, and so the SDK and the worker cannot
7
+ drift on what they put on and take off the wire.
8
+
9
+ Pillow is imported lazily inside each function, never at module scope: a caller who only passes
10
+ pre-encoded strings must not need it installed.
11
+
12
+ Nothing here fits a frame to a policy behind the caller's back (contract 0.5.2): `encode` sends the frame it is given,
13
+ `resize_image` is the tool the robot calls to fit one the way a camera's contract says the policy was trained, and
14
+ `image_size` reads a frame's width and height from its header - what the worker checks a declared camera against.
15
+
16
+
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import binascii
22
+ import io
23
+ import struct
24
+ from typing import Any
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ DEFAULT_JPEG_QUALITY = 95
34
+
35
+ def encode(frame: Any, *, quality: int = DEFAULT_JPEG_QUALITY) -> str:
36
+ """
37
+ One camera frame to the base64 image that `ImageFrame.data` carries.
38
+
39
+ `frame` may be a numpy array, a PIL image, raw `bytes` already encoded, or a `str` already base64
40
+ — the last two pass through untouched, so a caller with their own pipeline is not forced through
41
+ this one. Codec is JPEG at `quality`, unconditionally, at the frame's own size: fitting it to a
42
+ policy is `resize_image`, called by the robot. (Before contract 0.5.2 a frame over 640 px on its
43
+ long edge was scaled down to it here.)
44
+ """
45
+ if isinstance(frame, str):
46
+ return frame
47
+ if isinstance(frame, (bytes, bytearray)):
48
+ return base64.b64encode(bytes(frame)).decode()
49
+
50
+ try:
51
+ from PIL import Image
52
+ except ImportError as e:
53
+ raise ImportError(
54
+ "encoding an array or PIL image needs Pillow: pip install 'sequence-base[codec]'. "
55
+ "Pass an already-encoded base64 str or bytes to avoid it."
56
+ ) from e
57
+
58
+ img = frame if hasattr(frame, "save") else Image.fromarray(frame)
59
+ if img.mode != "RGB":
60
+ img = img.convert("RGB")
61
+ buf = io.BytesIO()
62
+ img.save(buf, format="JPEG", quality=quality)
63
+ return base64.b64encode(buf.getvalue()).decode()
64
+
65
+
66
+ def decode(data: str) -> "Any":
67
+ """
68
+ base64 JPEG/PNG -> HxWx3 uint8 RGB array — the worker's server-side inverse of `encode`.
69
+
70
+ This is what turns `ImageFrame.data` back into pixels for the model. Send JPEG q95: 16x smaller
71
+ than a JSON int array, ~0.5% action delta. {sequences-models worker handler decode_image}
72
+
73
+ Returns a numpy array; numpy is part of the `[codec]` extra alongside Pillow.
74
+ """
75
+ import numpy as np
76
+ from PIL import Image
77
+
78
+ raw = base64.b64decode(data, validate=False)
79
+ return np.asarray(Image.open(io.BytesIO(raw)).convert("RGB"), dtype=np.uint8)
80
+
81
+
82
+
83
+
84
+
85
+
86
+ MODES = ("pad", "stretch")
87
+ FILTERS = ("nearest", "box", "bilinear", "hamming", "bicubic", "lanczos")
88
+
89
+
90
+ def _to_image(frame: Any) -> "Any":
91
+ """A frame as an RGB PIL image: a PIL image, encoded bytes (JPEG / PNG), base64 text (a data: URI too), or an
92
+ HxWx3 uint8 array (anything Image.fromarray takes)."""
93
+ from PIL import Image
94
+
95
+ if isinstance(frame, str):
96
+ frame = base64.b64decode(frame.split(",", 1)[1] if frame.startswith("data:") else frame, validate=False)
97
+ if isinstance(frame, (bytes, bytearray)):
98
+ img = Image.open(io.BytesIO(bytes(frame)))
99
+ elif hasattr(frame, "save"):
100
+ img = frame
101
+ else:
102
+ img = Image.fromarray(frame)
103
+ return img if img.mode == "RGB" else img.convert("RGB")
104
+
105
+
106
+ def resize_image(frame: Any, width: int, height: int, *, mode: str, filter: str) -> "Any":
107
+ """
108
+ One camera frame fitted to `width` x `height`, as the robot sends it to a camera that reads that size: an RGB PIL
109
+ image, for `encode` next.
110
+
111
+ `mode` "pad" scales it into the box keeping its aspect ratio and pads the rest black, centred - openpi's
112
+ resize_with_pad exactly (both edges truncated with int()), what the templates' client.py ran; "stretch" scales it
113
+ to the box. `filter` is the resampling filter, one of FILTERS. Neither has a default: they are how the policy was
114
+ trained, and a policy's contract names them (Camera.resize, Camera.filter). A frame already that size is returned
115
+ as it is. `frame` is a PIL image, encoded JPEG / PNG bytes, base64 text, or an HxWx3 uint8 array.
116
+ """
117
+ if mode not in MODES:
118
+ raise ValueError(f"mode must be one of {MODES}, got {mode!r}")
119
+ if filter not in FILTERS:
120
+ raise ValueError(f"filter must be one of {FILTERS}, got {filter!r}")
121
+ if int(width) <= 0 or int(height) <= 0:
122
+ raise ValueError(f"the size must be positive, got {width}x{height}")
123
+ from PIL import Image
124
+
125
+ img = _to_image(frame)
126
+ if img.size == (width, height):
127
+ return img
128
+ resample = Image.Resampling[filter.upper()]
129
+ if mode == "stretch":
130
+ return img.resize((width, height), resample=resample)
131
+ cur_width, cur_height = img.size
132
+ ratio = max(cur_width / width, cur_height / height)
133
+ resized_width, resized_height = int(cur_width / ratio), int(cur_height / ratio)
134
+ resized = img.resize((resized_width, resized_height), resample=resample)
135
+ canvas = Image.new(resized.mode, (width, height), 0)
136
+ canvas.paste(resized, (max(0, int((width - resized_width) / 2)), max(0, int((height - resized_height) / 2))))
137
+ return canvas
138
+
139
+
140
+ def image_size(data: str) -> tuple[int, int]:
141
+ """
142
+ (width, height) of the frame `ImageFrame.data` carries - base64 JPEG or PNG, a data: URI's prefix taken off -
143
+ read from its header alone. ValueError for anything else.
144
+
145
+ No Pillow: the SDK checks a frame with it before sending, where Pillow may not be installed, and Pillow refuses a
146
+ header that declares a huge size (DecompressionBombError), which is then not "not an image" at all. A PNG's size
147
+ is in its IHDR; a JPEG's in the first start-of-frame segment (SOF0-SOF15, but not C4 / C8 / CC, which are not
148
+ frames).
149
+ """
150
+ text = data.split(",", 1)[1] if isinstance(data, str) and data.startswith("data:") else data
151
+ try:
152
+ raw = base64.b64decode(text, validate=False)
153
+ except (binascii.Error, ValueError, TypeError) as e:
154
+ raise ValueError("not base64") from e
155
+ if raw[:8] == b"\x89PNG\r\n\x1a\n" and raw[12:16] == b"IHDR" and len(raw) >= 24:
156
+ return struct.unpack(">II", raw[16:24])
157
+ if raw[:2] != b"\xff\xd8":
158
+ raise ValueError("not a JPEG or PNG image")
159
+ i = 2
160
+ while i + 4 <= len(raw):
161
+ if raw[i] != 0xFF:
162
+ raise ValueError("a JPEG whose segments do not line up")
163
+ marker = raw[i + 1]
164
+ if marker == 0xFF:
165
+ i += 1
166
+ continue
167
+ if marker == 0x01 or 0xD0 <= marker <= 0xD8:
168
+ i += 2
169
+ continue
170
+ if marker in (0xD9, 0xDA):
171
+ break
172
+ if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC) and i + 9 <= len(raw):
173
+ height, width = struct.unpack(">HH", raw[i + 5:i + 9])
174
+ return width, height
175
+ i += 2 + int.from_bytes(raw[i + 2:i + 4], "big")
176
+ raise ValueError("a JPEG with no frame header")
177
+
178
+
179
+
180
+ _DEPTH_UNITS = {"m": 1000.0, "mm": 1.0}
181
+
182
+
183
+ def encode_depth(depth: Any, *, unit: str = "m") -> str:
184
+ """A depth map -> the base64 16-bit PNG a depth frame (ImageFrame kind="depth") carries: millimetres as uint16, 0
185
+ where there is no reading (NaN, inf, <= 0), clipped at 65.535 m. `unit` is what the array holds: "m" (float metres,
186
+ the default) or "mm". Lossless, unlike the JPEG colour frames take — a depth error is a geometry error.
187
+
188
+ """
189
+ import numpy as np
190
+ from PIL import Image
191
+
192
+ if unit not in _DEPTH_UNITS:
193
+ raise ValueError(f"unit must be one of {sorted(_DEPTH_UNITS)}, got {unit!r}")
194
+ arr = np.asarray(depth, dtype=np.float64)
195
+ if arr.ndim != 2:
196
+ raise ValueError(f"a depth map is HxW, got shape {arr.shape}")
197
+ mm = arr * _DEPTH_UNITS[unit]
198
+ mm = np.where(np.isfinite(mm) & (mm > 0), mm, 0.0)
199
+ buf = io.BytesIO()
200
+ Image.fromarray(np.clip(np.rint(mm), 0, 65535).astype(np.uint16)).save(buf, format="PNG")
201
+ return base64.b64encode(buf.getvalue()).decode()
202
+
203
+
204
+ def decode_depth(data: str) -> "Any":
205
+ """A depth frame's data -> HxW uint16 millimetres (0: no reading) — the worker's inverse of `encode_depth`."""
206
+ import numpy as np
207
+ from PIL import Image
208
+
209
+ raw = base64.b64decode(data, validate=False)
210
+ return np.asarray(Image.open(io.BytesIO(raw)), dtype=np.uint16)
@@ -4,7 +4,8 @@
4
4
 
5
5
 
6
6
 
7
- CONTRACT_VERSION = "0.5.0"
7
+ CONTRACT_VERSION = "0.5.2"
8
+
8
9
 
9
10
 
10
11
 
@@ -93,8 +93,14 @@ class Autoscaler(BaseModel):
93
93
  class Timeouts(BaseModel):
94
94
  model_config = ConfigDict(extra="forbid")
95
95
 
96
- request: int
97
- startup: int
96
+
97
+
98
+
99
+ request: int = Field(..., ge=1, le=3600)
100
+
101
+
102
+
103
+ startup: int = Field(..., ge=60, le=18000)
98
104
 
99
105
 
100
106
  class VolumeAttach(BaseModel):
@@ -7,11 +7,15 @@ Declaring one is optional since contract 0.4.14: a deployment that declares none
7
7
  handed to the robot at connect.
8
8
  """
9
9
  from sequence_base.presets.robot_arm import (
10
+ PAYLOAD_HINT,
10
11
  PROPRIO_FIELDS,
11
12
  ActionContract,
12
13
  Camera,
13
14
  Embodiment,
14
15
  ObservationContract,
16
+ fit_hint,
17
+ frame_size_problem,
15
18
  )
16
19
 
17
- __all__ = ["ActionContract", "PROPRIO_FIELDS", "Camera", "ObservationContract", "Embodiment"]
20
+ __all__ = ["ActionContract", "PROPRIO_FIELDS", "Camera", "ObservationContract", "Embodiment", "frame_size_problem",
21
+ "fit_hint", "PAYLOAD_HINT"]
@@ -12,6 +12,7 @@ resolving to these same classes.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import math
15
16
  from enum import Enum
16
17
  from typing import Any, Literal
17
18
 
@@ -126,8 +127,19 @@ class ActionContract(BaseModel):
126
127
 
127
128
  on_violation: Literal["clip", "refuse"] | None = None
128
129
 
130
+
131
+
132
+
133
+ binarize: dict[int, float] | None = Field(
134
+ default=None, description="dimension -> threshold: the client gives the robot 1.0 above it, 0.0 otherwise")
135
+
129
136
  @model_validator(mode="after")
130
137
  def _limits_fit(self) -> "ActionContract":
138
+ for d, threshold in (self.binarize or {}).items():
139
+ if not 0 <= d < self.action_dim:
140
+ raise ValueError(f"binarize names dimension {d}; action_dim is {self.action_dim}")
141
+ if not math.isfinite(threshold):
142
+ raise ValueError(f"binarize[{d}] is {threshold}, not a finite threshold")
131
143
  if self.bounds is not None:
132
144
  if len(self.bounds) != self.action_dim:
133
145
  raise ValueError(f"bounds has {len(self.bounds)} [low, high] pairs; action_dim is {self.action_dim}")
@@ -148,8 +160,10 @@ PROPRIO_FIELDS = ("joint_positions", "joint_velocities", "end_effector_pose", "g
148
160
 
149
161
 
150
162
  class Camera(BaseModel):
151
- """One camera view a policy reads: the size it expects. Informational at the worker (the policy resizes);
152
- the client resizes and letterboxes to it before sending, so the bytes on the wire are what is used."""
163
+ """One camera view a policy reads: the size its colour frames arrive at. The worker refuses a frame of another size
164
+ (400 invalid_request naming the view, the size it got and the SDK call that fits it) and never resizes one: the
165
+ robot fits its frames before sending - with `sequence_ai.resize_image`, or `policy.prepare_image`, which fits a frame
166
+ the way `resize` and `filter` say the policy was trained."""
153
167
 
154
168
  model_config = ConfigDict(extra="forbid")
155
169
 
@@ -160,6 +174,58 @@ class Camera(BaseModel):
160
174
  depth: bool | None = Field(default=None, description="the view also sends a depth frame (ImageFrame kind=depth)")
161
175
 
162
176
 
177
+
178
+
179
+
180
+
181
+ resize: Literal["pad", "stretch"] | None = Field(
182
+ default=None, description='how a frame is fitted to width x height: "pad" (letterbox) or "stretch"')
183
+
184
+
185
+
186
+ filter: Literal["nearest", "box", "bilinear", "hamming", "bicubic", "lanczos"] | None = Field(
187
+ default=None, description="the resampling filter a frame is fitted with (Pillow's names)")
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+ def frame_size_problem(view: str, index: int, got: tuple[int, int] | None, camera: dict[str, Any]) -> str | None:
196
+ """What is wrong with frame `index` of camera `view` (counted in the order the request carries that view's frames,
197
+ from 0), read as `got` (width, height; None: not a JPEG or PNG image), against the camera the contract declares;
198
+ None when it fits."""
199
+ if got is None:
200
+ return f"camera {view!r} frame {index} is not a JPEG or PNG image"
201
+ want = (int(camera["width"]), int(camera["height"]))
202
+ if tuple(got) == want:
203
+ return None
204
+ return f"camera {view!r} frame {index} is {got[0]}x{got[1]}; the policy reads {want[0]}x{want[1]}"
205
+
206
+
207
+ def fit_hint(view: str, camera: dict[str, Any], *, more: bool = False) -> str:
208
+ """The next step for a frame of camera `view` that does not fit: the SDK call that fits it as the contract says
209
+ the policy was trained, or, when the contract does not say, the call with the two choices left to the caller -
210
+ never a guess. `more`: other views did not fit either (details.sizes lists them)."""
211
+ width, height = int(camera["width"]), int(camera["height"])
212
+ mode, flt = camera.get("resize"), camera.get("filter")
213
+ if mode and flt:
214
+ hint = (f'fit each frame on the robot before sending: policy.prepare_image("{view}", frame), or '
215
+ f'sequence_ai.resize_image(frame, {width}, {height}, mode="{mode}", filter="{flt}")')
216
+ else:
217
+ hint = (f"fit each frame on the robot before sending: sequence_ai.resize_image(frame, {width}, {height}, "
218
+ f"mode=..., filter=...) with the mode and filter the policy was trained with; this deployment does "
219
+ f"not declare them")
220
+ return hint + (" (the same for each view in details.sizes)" if more else "")
221
+
222
+
223
+
224
+
225
+ PAYLOAD_HINT = ("fit each camera frame on the robot before sending (sequence_ai.resize_image, or policy.prepare_image "
226
+ "for a deployment that declares how): a frame at the size a policy reads is a few kB")
227
+
228
+
163
229
  class ObservationContract(BaseModel):
164
230
  """What a policy reads, declared once: the worker checks every /act against it before inference, the
165
231
  client assembles to it. An observation that does not match is refused with what is missing — it is never
@@ -65,5 +65,5 @@
65
65
  ],
66
66
  "title": "ActRequest",
67
67
  "type": "object",
68
- "x-contract-version": "0.5.0"
68
+ "x-contract-version": "0.5.2"
69
69
  }
@@ -75,5 +75,5 @@
75
75
  ],
76
76
  "title": "ActResponse",
77
77
  "type": "object",
78
- "x-contract-version": "0.5.0"
78
+ "x-contract-version": "0.5.2"
79
79
  }
@@ -99,5 +99,5 @@
99
99
  ],
100
100
  "title": "ActionChunk",
101
101
  "type": "object",
102
- "x-contract-version": "0.5.0"
102
+ "x-contract-version": "0.5.2"
103
103
  }
@@ -23,6 +23,22 @@
23
23
  "action_space": {
24
24
  "$ref": "#/$defs/ActionSpace"
25
25
  },
26
+ "binarize": {
27
+ "anyOf": [
28
+ {
29
+ "additionalProperties": {
30
+ "type": "number"
31
+ },
32
+ "type": "object"
33
+ },
34
+ {
35
+ "type": "null"
36
+ }
37
+ ],
38
+ "default": null,
39
+ "description": "dimension -> threshold: the client gives the robot 1.0 above it, 0.0 otherwise",
40
+ "title": "Binarize"
41
+ },
26
42
  "bounds": {
27
43
  "anyOf": [
28
44
  {
@@ -100,5 +116,5 @@
100
116
  ],
101
117
  "title": "ActionContract",
102
118
  "type": "object",
103
- "x-contract-version": "0.5.0"
119
+ "x-contract-version": "0.5.2"
104
120
  }
@@ -19,5 +19,5 @@
19
19
  ],
20
20
  "title": "ActionStep",
21
21
  "type": "object",
22
- "x-contract-version": "0.5.0"
22
+ "x-contract-version": "0.5.2"
23
23
  }
@@ -38,5 +38,5 @@
38
38
  ],
39
39
  "title": "AdapterTransform",
40
40
  "type": "object",
41
- "x-contract-version": "0.5.0"
41
+ "x-contract-version": "0.5.2"
42
42
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "AddLocal",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.0"
26
+ "x-contract-version": "0.5.2"
27
27
  }
@@ -69,5 +69,5 @@
69
69
  },
70
70
  "title": "Autoscaler",
71
71
  "type": "object",
72
- "x-contract-version": "0.5.0"
72
+ "x-contract-version": "0.5.2"
73
73
  }
@@ -62,5 +62,5 @@
62
62
  ],
63
63
  "title": "BenchmarkConditionSpec",
64
64
  "type": "object",
65
- "x-contract-version": "0.5.0"
65
+ "x-contract-version": "0.5.2"
66
66
  }
@@ -11,6 +11,22 @@
11
11
  "action_space": {
12
12
  "$ref": "#/$defs/ActionSpace"
13
13
  },
14
+ "binarize": {
15
+ "anyOf": [
16
+ {
17
+ "additionalProperties": {
18
+ "type": "number"
19
+ },
20
+ "type": "object"
21
+ },
22
+ {
23
+ "type": "null"
24
+ }
25
+ ],
26
+ "default": null,
27
+ "description": "dimension -> threshold: the client gives the robot 1.0 above it, 0.0 otherwise",
28
+ "title": "Binarize"
29
+ },
14
30
  "bounds": {
15
31
  "anyOf": [
16
32
  {
@@ -849,5 +865,5 @@
849
865
  ],
850
866
  "title": "BenchmarkRegisterRequest",
851
867
  "type": "object",
852
- "x-contract-version": "0.5.0"
868
+ "x-contract-version": "0.5.2"
853
869
  }
@@ -297,7 +297,7 @@
297
297
  ],
298
298
  "title": "SeqFlags",
299
299
  "type": "integer",
300
- "x-contract-version": "0.5.0",
300
+ "x-contract-version": "0.5.2",
301
301
  "x-flag-names": {
302
302
  "CHECK": 65536,
303
303
  "INFER": 16,
@@ -518,5 +518,5 @@
518
518
  ],
519
519
  "title": "BenchmarkSpec",
520
520
  "type": "object",
521
- "x-contract-version": "0.5.0"
521
+ "x-contract-version": "0.5.2"
522
522
  }
@@ -40,5 +40,5 @@
40
40
  ],
41
41
  "title": "BenchmarkStatusResponse",
42
42
  "type": "object",
43
- "x-contract-version": "0.5.0"
43
+ "x-contract-version": "0.5.2"
44
44
  }
@@ -79,5 +79,5 @@
79
79
  ],
80
80
  "title": "BootstrapResponse",
81
81
  "type": "object",
82
- "x-contract-version": "0.5.0"
82
+ "x-contract-version": "0.5.2"
83
83
  }
@@ -0,0 +1,76 @@
1
+ {
2
+ "additionalProperties": false,
3
+ "description": "One camera view a policy reads: the size its colour frames arrive at. The worker refuses a frame of another size\n(400 invalid_request naming the view, the size it got and the SDK call that fits it) and never resizes one: the\nrobot fits its frames before sending - with `sequence_ai.resize_image`, or `policy.prepare_image`, which fits a frame\nthe way `resize` and `filter` say the policy was trained.",
4
+ "properties": {
5
+ "depth": {
6
+ "anyOf": [
7
+ {
8
+ "type": "boolean"
9
+ },
10
+ {
11
+ "type": "null"
12
+ }
13
+ ],
14
+ "default": null,
15
+ "description": "the view also sends a depth frame (ImageFrame kind=depth)",
16
+ "title": "Depth"
17
+ },
18
+ "filter": {
19
+ "anyOf": [
20
+ {
21
+ "enum": [
22
+ "nearest",
23
+ "box",
24
+ "bilinear",
25
+ "hamming",
26
+ "bicubic",
27
+ "lanczos"
28
+ ],
29
+ "type": "string"
30
+ },
31
+ {
32
+ "type": "null"
33
+ }
34
+ ],
35
+ "default": null,
36
+ "description": "the resampling filter a frame is fitted with (Pillow's names)",
37
+ "title": "Filter"
38
+ },
39
+ "height": {
40
+ "exclusiveMinimum": 0,
41
+ "maximum": 8192,
42
+ "title": "Height",
43
+ "type": "integer"
44
+ },
45
+ "resize": {
46
+ "anyOf": [
47
+ {
48
+ "enum": [
49
+ "pad",
50
+ "stretch"
51
+ ],
52
+ "type": "string"
53
+ },
54
+ {
55
+ "type": "null"
56
+ }
57
+ ],
58
+ "default": null,
59
+ "description": "how a frame is fitted to width x height: \"pad\" (letterbox) or \"stretch\"",
60
+ "title": "Resize"
61
+ },
62
+ "width": {
63
+ "exclusiveMinimum": 0,
64
+ "maximum": 8192,
65
+ "title": "Width",
66
+ "type": "integer"
67
+ }
68
+ },
69
+ "required": [
70
+ "width",
71
+ "height"
72
+ ],
73
+ "title": "Camera",
74
+ "type": "object",
75
+ "x-contract-version": "0.5.2"
76
+ }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "ClampInfo",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.0"
26
+ "x-contract-version": "0.5.2"
27
27
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "CodeBundle",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.0"
26
+ "x-contract-version": "0.5.2"
27
27
  }
@@ -77,5 +77,5 @@
77
77
  },
78
78
  "title": "Concurrency",
79
79
  "type": "object",
80
- "x-contract-version": "0.5.0"
80
+ "x-contract-version": "0.5.2"
81
81
  }
@@ -55,5 +55,5 @@
55
55
  ],
56
56
  "title": "Conditions",
57
57
  "type": "object",
58
- "x-contract-version": "0.5.0"
58
+ "x-contract-version": "0.5.2"
59
59
  }