meta-sam-parser 0.0.2__tar.gz → 0.0.4__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.
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/PKG-INFO +54 -20
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/README.md +53 -19
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/pyproject.toml +1 -1
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/__init__.py +2 -2
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_segmentation.py +28 -189
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_stream.py +96 -39
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_types.py +13 -19
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/LICENSE +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_errors.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_mask_codec.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/_mask_conversion.py +0 -0
- {meta_sam_parser-0.0.2 → meta_sam_parser-0.0.4}/src/meta_sam_parser/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-sam-parser
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Native Python parsing primitives for the SAM 3 segmentation protocol
|
|
5
5
|
Project-URL: Homepage, https://github.com/meta-models/meta-sam
|
|
6
6
|
Project-URL: Repository, https://github.com/meta-models/meta-sam
|
|
@@ -106,24 +106,35 @@ python -m pip install .
|
|
|
106
106
|
|
|
107
107
|
Import supported APIs only from the package root. Deep imports are unsupported;
|
|
108
108
|
underscore-prefixed modules are implementation details and may change without notice.
|
|
109
|
-
Format factories are zero-argument and create isolated parser state
|
|
109
|
+
Format factories are zero-argument and create isolated parser state. The example
|
|
110
|
+
feeds the parser one frame of SAM 3.1 output exactly as the API emits it - one line
|
|
111
|
+
per frame, `<Nf>` then comma-separated `id<|box;...|><|mask;...|>` records:
|
|
110
112
|
|
|
111
113
|
```python
|
|
112
114
|
from meta_sam_parser import CompletedOutcome, video_segmentation_format
|
|
113
115
|
|
|
116
|
+
output_text = (
|
|
117
|
+
"<0f>0<|box;x1=211;y1=228;x2=270;y2=254;w=320;h=334|>"
|
|
118
|
+
"<|mask;x=0;y=0;data=27,60,~!!!!M!0c[0o91w?q1!pIH4pPRVp2B3'7`e.ioeAf6-k/#Xd8%dX9x(|>"
|
|
119
|
+
",1<|box;x1=155;y1=228;x2=202;y2=254;w=320;h=334|>"
|
|
120
|
+
"<|mask;x=0;y=0;data=27,48,~!!!!J!0c[q=Pj_zs=*4C4(/./x#:/`S_GnD`=o3?X{emCgO$y@|>\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
114
123
|
format_ = video_segmentation_format()
|
|
115
124
|
parser = format_.create_parser()
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
):
|
|
126
|
+
# Chunks may split anywhere, including inside a mask payload.
|
|
127
|
+
split = output_text.index(",1<|box")
|
|
128
|
+
for snapshot in parser.push(output_text[:split]):
|
|
121
129
|
print(snapshot.revision, len(snapshot.records))
|
|
130
|
+
for snapshot in parser.push(output_text[split:]):
|
|
131
|
+
print(snapshot.revision, len(snapshot.records)) # 1 4
|
|
122
132
|
|
|
123
133
|
finished = parser.finish(CompletedOutcome())
|
|
124
134
|
for snapshot in finished.events:
|
|
125
135
|
print(snapshot.revision, len(snapshot.records))
|
|
126
136
|
result = finished.result
|
|
137
|
+
# Two box records and two mask records, object IDs "0" and "1", frame 0.
|
|
127
138
|
```
|
|
128
139
|
|
|
129
140
|
`push()` accepts arbitrarily split text chunks and returns zero or one cumulative
|
|
@@ -140,7 +151,11 @@ can own lane validation, source cleanup, and terminal outcomes.
|
|
|
140
151
|
`parse_responses_stream(source, format)` accepts an async iterable of official
|
|
141
152
|
OpenAI Python SDK event objects, mappings with the same wire fields, or a mixture
|
|
142
153
|
of both. It has no runtime dependency on the OpenAI SDK. Field names stay in the
|
|
143
|
-
SDK/wire snake-case form (`item_id`, `output_index`, and `content_index`).
|
|
154
|
+
SDK/wire snake-case form (`item_id`, `output_index`, and `content_index`). An
|
|
155
|
+
output-text lane is finalized by either `response.output_text.done` or a
|
|
156
|
+
`response.content_part.done` event whose part type is `output_text`. The live SAM
|
|
157
|
+
Model API emits only the latter. If a stream emits both finalizers, their text
|
|
158
|
+
must match.
|
|
144
159
|
|
|
145
160
|
### Iterator-first
|
|
146
161
|
|
|
@@ -228,10 +243,10 @@ All public values are frozen, slotted dataclasses. Observable collections are
|
|
|
228
243
|
tuples. The package root exports:
|
|
229
244
|
|
|
230
245
|
- Geometry, masks, and aliases: `FrameReference`, `SegmentationMaskBounds`,
|
|
231
|
-
`SegmentationMask`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
246
|
+
`SegmentationMask`, `SegmentationMaskEncoding`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
232
247
|
`DiagnosticSeverity`, and `IncompleteReason`.
|
|
233
|
-
- Records: `SegmentationTextRecord`, `
|
|
234
|
-
`
|
|
248
|
+
- Records: `SegmentationTextRecord`, `SegmentationBoxRecord`,
|
|
249
|
+
`SegmentationMaskRecord`, and `SegmentationRecord`.
|
|
235
250
|
- Views: `SegmentationDiagnostic`, `ImageSegmentationSnapshot`,
|
|
236
251
|
`VideoSegmentationSnapshot`, `SegmentationSnapshot`,
|
|
237
252
|
`ImageSegmentationResult`, `VideoSegmentationResult`, and
|
|
@@ -258,15 +273,31 @@ the next revision.
|
|
|
258
273
|
|
|
259
274
|
## Parsing behavior
|
|
260
275
|
|
|
261
|
-
|
|
276
|
+
SAM 3.1 returns segmentation as special-token text in one `output_text` lane, one
|
|
277
|
+
line per frame:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
<Nf>id<|box;x1=..;y1=..;x2=..;y2=..;w=<frameW>;h=<frameH>|><|mask;x=0;y=0;data=<H>,<W>,<enc>payload|>,id<|box;...|><|mask;...|>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`<Nf>` is the zero-based frame index; frames without a visible object emit no
|
|
284
|
+
line, so indices can skip. Each comma-separated record is a bare integer object
|
|
285
|
+
id - stable for an object across the frames of one response and not a dense
|
|
286
|
+
sequence - followed by one box and one mask. The parser retains the id as a string
|
|
287
|
+
in `object_id`. Box corners and the `w`/`h` frame size are source pixels; the
|
|
288
|
+
inclusive wire `x2`/`y2` become half-open `right`/`bottom`. The mask tuple is
|
|
289
|
+
`height,width,payload`, where the payload's first character selects the encoding:
|
|
290
|
+
`~` for `lossless` (the API default) or `!` for `one_bit`. The payload is base85,
|
|
291
|
+
not base64: after the marker its digits are printable ASCII `!` through `{` minus
|
|
292
|
+
the wire delimiters `" \ , ; < > |`, so it contains `*`, `$`, brackets, and
|
|
293
|
+
backticks, a base64 regex will not match it, and only the first character after
|
|
294
|
+
`H,W,` is the marker (`!` is also digit zero). Pass it through unchanged. Each record becomes one `SegmentationBoxRecord` followed by
|
|
295
|
+
one `SegmentationMaskRecord` whose `bounds` is the same half-open box. Image streams require frame zero and omit the frame from
|
|
296
|
+
normalized records; video records retain `frame.frame_index`. Every mask is
|
|
297
|
+
strictly decoded before insertion; an empty lane is a valid completed response
|
|
298
|
+
with no records.
|
|
299
|
+
The grammar is specified in the
|
|
262
300
|
[SAM 3 protocol](https://github.com/meta-models/meta-sam/blob/main/protocol/sam3.md).
|
|
263
|
-
The parser separately accepts compact box-first SAM API output as production
|
|
264
|
-
compatibility input and normalizes it to the same records. Its object token is an
|
|
265
|
-
ASCII-decimal ID retained as a string, and IDs may be multi-digit or non-contiguous.
|
|
266
|
-
The compact input's inclusive `x2`/`y2` coordinates become half-open `right`/`bottom`
|
|
267
|
-
bounds. Image API
|
|
268
|
-
records require frame zero and omit it from normalized records; video records retain
|
|
269
|
-
frame indices. Every mask is strictly decoded before insertion.
|
|
270
301
|
|
|
271
302
|
Plain text remains an ordered text record. Malformed structured-looking lines
|
|
272
303
|
produce diagnostics and parsing continues. `raw_output` preserves every input
|
|
@@ -359,8 +390,11 @@ by the `release PyPI distribution` workflow
|
|
|
359
390
|
1. Bump `version` in `pyproject.toml`, run `node scripts/sync-compatibility` from
|
|
360
391
|
the repository root so the compatibility matrix records the new version, and
|
|
361
392
|
merge that change to `main`.
|
|
362
|
-
2. Push the tag `meta-sam-parser@<version>` at that commit
|
|
363
|
-
tag that does not match the manifest version
|
|
393
|
+
2. Push the tag `meta-sam-parser@<version>` at that merge commit on `main`. The
|
|
394
|
+
workflow refuses a tag that does not match the manifest version, a tag whose
|
|
395
|
+
commit is not on `main`, and a tag older than the latest `main` commit touching
|
|
396
|
+
`python/`, `conformance/`, or `protocol/`, so a tag created before later fixes
|
|
397
|
+
merged cannot publish stale source.
|
|
364
398
|
3. The workflow builds the reproducible wheel and sdist, runs the artifact and
|
|
365
399
|
clean-consumer audits and the cross-language conformance suite, and then waits
|
|
366
400
|
for approval in the `pypi` GitHub environment before uploading through PyPI
|
|
@@ -17,24 +17,35 @@ python -m pip install .
|
|
|
17
17
|
|
|
18
18
|
Import supported APIs only from the package root. Deep imports are unsupported;
|
|
19
19
|
underscore-prefixed modules are implementation details and may change without notice.
|
|
20
|
-
Format factories are zero-argument and create isolated parser state
|
|
20
|
+
Format factories are zero-argument and create isolated parser state. The example
|
|
21
|
+
feeds the parser one frame of SAM 3.1 output exactly as the API emits it - one line
|
|
22
|
+
per frame, `<Nf>` then comma-separated `id<|box;...|><|mask;...|>` records:
|
|
21
23
|
|
|
22
24
|
```python
|
|
23
25
|
from meta_sam_parser import CompletedOutcome, video_segmentation_format
|
|
24
26
|
|
|
27
|
+
output_text = (
|
|
28
|
+
"<0f>0<|box;x1=211;y1=228;x2=270;y2=254;w=320;h=334|>"
|
|
29
|
+
"<|mask;x=0;y=0;data=27,60,~!!!!M!0c[0o91w?q1!pIH4pPRVp2B3'7`e.ioeAf6-k/#Xd8%dX9x(|>"
|
|
30
|
+
",1<|box;x1=155;y1=228;x2=202;y2=254;w=320;h=334|>"
|
|
31
|
+
"<|mask;x=0;y=0;data=27,48,~!!!!J!0c[q=Pj_zs=*4C4(/./x#:/`S_GnD`=o3?X{emCgO$y@|>\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
25
34
|
format_ = video_segmentation_format()
|
|
26
35
|
parser = format_.create_parser()
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
):
|
|
37
|
+
# Chunks may split anywhere, including inside a mask payload.
|
|
38
|
+
split = output_text.index(",1<|box")
|
|
39
|
+
for snapshot in parser.push(output_text[:split]):
|
|
32
40
|
print(snapshot.revision, len(snapshot.records))
|
|
41
|
+
for snapshot in parser.push(output_text[split:]):
|
|
42
|
+
print(snapshot.revision, len(snapshot.records)) # 1 4
|
|
33
43
|
|
|
34
44
|
finished = parser.finish(CompletedOutcome())
|
|
35
45
|
for snapshot in finished.events:
|
|
36
46
|
print(snapshot.revision, len(snapshot.records))
|
|
37
47
|
result = finished.result
|
|
48
|
+
# Two box records and two mask records, object IDs "0" and "1", frame 0.
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
`push()` accepts arbitrarily split text chunks and returns zero or one cumulative
|
|
@@ -51,7 +62,11 @@ can own lane validation, source cleanup, and terminal outcomes.
|
|
|
51
62
|
`parse_responses_stream(source, format)` accepts an async iterable of official
|
|
52
63
|
OpenAI Python SDK event objects, mappings with the same wire fields, or a mixture
|
|
53
64
|
of both. It has no runtime dependency on the OpenAI SDK. Field names stay in the
|
|
54
|
-
SDK/wire snake-case form (`item_id`, `output_index`, and `content_index`).
|
|
65
|
+
SDK/wire snake-case form (`item_id`, `output_index`, and `content_index`). An
|
|
66
|
+
output-text lane is finalized by either `response.output_text.done` or a
|
|
67
|
+
`response.content_part.done` event whose part type is `output_text`. The live SAM
|
|
68
|
+
Model API emits only the latter. If a stream emits both finalizers, their text
|
|
69
|
+
must match.
|
|
55
70
|
|
|
56
71
|
### Iterator-first
|
|
57
72
|
|
|
@@ -139,10 +154,10 @@ All public values are frozen, slotted dataclasses. Observable collections are
|
|
|
139
154
|
tuples. The package root exports:
|
|
140
155
|
|
|
141
156
|
- Geometry, masks, and aliases: `FrameReference`, `SegmentationMaskBounds`,
|
|
142
|
-
`SegmentationMask`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
157
|
+
`SegmentationMask`, `SegmentationMaskEncoding`, `SegmentationMaskIdentity`, `SegmentationMedia`,
|
|
143
158
|
`DiagnosticSeverity`, and `IncompleteReason`.
|
|
144
|
-
- Records: `SegmentationTextRecord`, `
|
|
145
|
-
`
|
|
159
|
+
- Records: `SegmentationTextRecord`, `SegmentationBoxRecord`,
|
|
160
|
+
`SegmentationMaskRecord`, and `SegmentationRecord`.
|
|
146
161
|
- Views: `SegmentationDiagnostic`, `ImageSegmentationSnapshot`,
|
|
147
162
|
`VideoSegmentationSnapshot`, `SegmentationSnapshot`,
|
|
148
163
|
`ImageSegmentationResult`, `VideoSegmentationResult`, and
|
|
@@ -169,15 +184,31 @@ the next revision.
|
|
|
169
184
|
|
|
170
185
|
## Parsing behavior
|
|
171
186
|
|
|
172
|
-
|
|
187
|
+
SAM 3.1 returns segmentation as special-token text in one `output_text` lane, one
|
|
188
|
+
line per frame:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
<Nf>id<|box;x1=..;y1=..;x2=..;y2=..;w=<frameW>;h=<frameH>|><|mask;x=0;y=0;data=<H>,<W>,<enc>payload|>,id<|box;...|><|mask;...|>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`<Nf>` is the zero-based frame index; frames without a visible object emit no
|
|
195
|
+
line, so indices can skip. Each comma-separated record is a bare integer object
|
|
196
|
+
id - stable for an object across the frames of one response and not a dense
|
|
197
|
+
sequence - followed by one box and one mask. The parser retains the id as a string
|
|
198
|
+
in `object_id`. Box corners and the `w`/`h` frame size are source pixels; the
|
|
199
|
+
inclusive wire `x2`/`y2` become half-open `right`/`bottom`. The mask tuple is
|
|
200
|
+
`height,width,payload`, where the payload's first character selects the encoding:
|
|
201
|
+
`~` for `lossless` (the API default) or `!` for `one_bit`. The payload is base85,
|
|
202
|
+
not base64: after the marker its digits are printable ASCII `!` through `{` minus
|
|
203
|
+
the wire delimiters `" \ , ; < > |`, so it contains `*`, `$`, brackets, and
|
|
204
|
+
backticks, a base64 regex will not match it, and only the first character after
|
|
205
|
+
`H,W,` is the marker (`!` is also digit zero). Pass it through unchanged. Each record becomes one `SegmentationBoxRecord` followed by
|
|
206
|
+
one `SegmentationMaskRecord` whose `bounds` is the same half-open box. Image streams require frame zero and omit the frame from
|
|
207
|
+
normalized records; video records retain `frame.frame_index`. Every mask is
|
|
208
|
+
strictly decoded before insertion; an empty lane is a valid completed response
|
|
209
|
+
with no records.
|
|
210
|
+
The grammar is specified in the
|
|
173
211
|
[SAM 3 protocol](https://github.com/meta-models/meta-sam/blob/main/protocol/sam3.md).
|
|
174
|
-
The parser separately accepts compact box-first SAM API output as production
|
|
175
|
-
compatibility input and normalizes it to the same records. Its object token is an
|
|
176
|
-
ASCII-decimal ID retained as a string, and IDs may be multi-digit or non-contiguous.
|
|
177
|
-
The compact input's inclusive `x2`/`y2` coordinates become half-open `right`/`bottom`
|
|
178
|
-
bounds. Image API
|
|
179
|
-
records require frame zero and omit it from normalized records; video records retain
|
|
180
|
-
frame indices. Every mask is strictly decoded before insertion.
|
|
181
212
|
|
|
182
213
|
Plain text remains an ordered text record. Malformed structured-looking lines
|
|
183
214
|
produce diagnostics and parsing continues. `raw_output` preserves every input
|
|
@@ -270,8 +301,11 @@ by the `release PyPI distribution` workflow
|
|
|
270
301
|
1. Bump `version` in `pyproject.toml`, run `node scripts/sync-compatibility` from
|
|
271
302
|
the repository root so the compatibility matrix records the new version, and
|
|
272
303
|
merge that change to `main`.
|
|
273
|
-
2. Push the tag `meta-sam-parser@<version>` at that commit
|
|
274
|
-
tag that does not match the manifest version
|
|
304
|
+
2. Push the tag `meta-sam-parser@<version>` at that merge commit on `main`. The
|
|
305
|
+
workflow refuses a tag that does not match the manifest version, a tag whose
|
|
306
|
+
commit is not on `main`, and a tag older than the latest `main` commit touching
|
|
307
|
+
`python/`, `conformance/`, or `protocol/`, so a tag created before later fixes
|
|
308
|
+
merged cannot publish stale source.
|
|
275
309
|
3. The workflow builds the reproducible wheel and sdist, runs the artifact and
|
|
276
310
|
clean-consumer audits and the cross-language conformance suite, and then waits
|
|
277
311
|
for approval in the `pypi` GitHub environment before uploading through PyPI
|
|
@@ -36,10 +36,10 @@ from ._types import (
|
|
|
36
36
|
SegmentationDiagnostic,
|
|
37
37
|
SegmentationMask,
|
|
38
38
|
SegmentationMaskBounds,
|
|
39
|
+
SegmentationMaskEncoding,
|
|
39
40
|
SegmentationMaskIdentity,
|
|
40
41
|
SegmentationMaskRecord,
|
|
41
42
|
SegmentationMedia,
|
|
42
|
-
SegmentationPointRecord,
|
|
43
43
|
SegmentationRecord,
|
|
44
44
|
SegmentationResult,
|
|
45
45
|
SegmentationSnapshot,
|
|
@@ -80,10 +80,10 @@ __all__ = [
|
|
|
80
80
|
"SegmentationDiagnostic",
|
|
81
81
|
"SegmentationMask",
|
|
82
82
|
"SegmentationMaskBounds",
|
|
83
|
+
"SegmentationMaskEncoding",
|
|
83
84
|
"SegmentationMaskIdentity",
|
|
84
85
|
"SegmentationMaskRecord",
|
|
85
86
|
"SegmentationMedia",
|
|
86
|
-
"SegmentationPointRecord",
|
|
87
87
|
"SegmentationRecord",
|
|
88
88
|
"SegmentationResult",
|
|
89
89
|
"SegmentationSnapshot",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import math
|
|
6
5
|
import re
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from typing import cast
|
|
@@ -23,7 +22,6 @@ from ._types import (
|
|
|
23
22
|
SegmentationMaskIdentity,
|
|
24
23
|
SegmentationMaskRecord,
|
|
25
24
|
SegmentationMedia,
|
|
26
|
-
SegmentationPointRecord,
|
|
27
25
|
SegmentationRecord,
|
|
28
26
|
SegmentationResult,
|
|
29
27
|
SegmentationSnapshot,
|
|
@@ -40,65 +38,28 @@ _JS_WHITESPACE = (
|
|
|
40
38
|
_JS_WHITESPACE_CLASS = (
|
|
41
39
|
r"\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
42
40
|
)
|
|
43
|
-
_JS_WHITESPACE_PATTERN = rf"[{_JS_WHITESPACE_CLASS}]"
|
|
44
|
-
_JS_NON_WHITESPACE_PATTERN = rf"[^{_JS_WHITESPACE_CLASS}]"
|
|
45
41
|
_JS_DOT_PATTERN = r"[^\n\r\u2028\u2029]"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
rf"object=([A-Za-z0-9._-]+){_JS_WHITESPACE_PATTERN}+"
|
|
49
|
-
rf"(point|box|mask)=({_JS_DOT_PATTERN}+)$"
|
|
50
|
-
)
|
|
51
|
-
_COMPACT_API_HEADER_PATTERN = re.compile(rf"^<([0-9]+)f>({_JS_DOT_PATTERN}*)$")
|
|
52
|
-
_COMPACT_API_OBJECT_PATTERN = re.compile(
|
|
42
|
+
_API_HEADER_PATTERN = re.compile(rf"^<([0-9]+)f>({_JS_DOT_PATTERN}*)$")
|
|
43
|
+
_API_RECORD_PATTERN = re.compile(
|
|
53
44
|
r"^(?:,)?([0-9]+)"
|
|
54
45
|
r"<\|box;x1=(-?[0-9]+);y1=(-?[0-9]+);x2=(-?[0-9]+);"
|
|
55
46
|
r"y2=(-?[0-9]+);w=([0-9]+);h=([0-9]+)\|>"
|
|
56
47
|
r"<\|mask;x=0;y=0;data=([0-9]+),([0-9]+),([!~][^|]+)\|>"
|
|
57
48
|
)
|
|
58
|
-
_POINT_PATTERN = re.compile(r"^\(([^,]+),([^,]+)\)$")
|
|
59
|
-
_BOX_PATTERN = re.compile(r"^\(([^,]+),([^,]+),([^,]+),([^,]+)\)$")
|
|
60
|
-
_MASK_PATTERN = re.compile(
|
|
61
|
-
rf"^([^;{_JS_WHITESPACE_CLASS}]+);"
|
|
62
|
-
rf"size=([0-9]+)x([0-9]+);data=({_JS_NON_WHITESPACE_PATTERN}+)$"
|
|
63
|
-
)
|
|
64
|
-
_DECIMAL_NUMBER_PATTERN = re.compile(
|
|
65
|
-
r"^[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+))"
|
|
66
|
-
r"(?:[eE][+-]?[0-9]+)?$"
|
|
67
|
-
)
|
|
68
|
-
_HEXADECIMAL_NUMBER_PATTERN = re.compile(r"^0[xX][0-9A-Fa-f]+$")
|
|
69
|
-
_BINARY_NUMBER_PATTERN = re.compile(r"^0[bB][01]+$")
|
|
70
|
-
_OCTAL_NUMBER_PATTERN = re.compile(r"^0[oO][0-7]+$")
|
|
71
49
|
|
|
72
50
|
|
|
73
51
|
def _trim(value: str) -> str:
|
|
74
52
|
return value.strip(_JS_WHITESPACE)
|
|
75
53
|
|
|
76
54
|
|
|
77
|
-
def _javascript_number(value: str) -> float | None:
|
|
78
|
-
normalized = _trim(value)
|
|
79
|
-
if not normalized:
|
|
80
|
-
return None
|
|
81
|
-
try:
|
|
82
|
-
if _HEXADECIMAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
83
|
-
parsed = float(int(normalized[2:], 16))
|
|
84
|
-
elif _BINARY_NUMBER_PATTERN.fullmatch(normalized):
|
|
85
|
-
parsed = float(int(normalized[2:], 2))
|
|
86
|
-
elif _OCTAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
87
|
-
parsed = float(int(normalized[2:], 8))
|
|
88
|
-
elif _DECIMAL_NUMBER_PATTERN.fullmatch(normalized):
|
|
89
|
-
parsed = float(normalized)
|
|
90
|
-
else:
|
|
91
|
-
return None
|
|
92
|
-
except (OverflowError, ValueError):
|
|
93
|
-
return None
|
|
94
|
-
return parsed if math.isfinite(parsed) else None
|
|
95
|
-
|
|
96
|
-
|
|
97
55
|
def _integer_text(value: str) -> int | None:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
"""Parse a decimal integer token the way JavaScript's ``Number`` observes it.
|
|
57
|
+
|
|
58
|
+
Callers only pass regex captures of ``-?[0-9]+``; values beyond the
|
|
59
|
+
JavaScript safe-integer range are rejected so both parsers agree.
|
|
60
|
+
"""
|
|
61
|
+
parsed = int(value)
|
|
62
|
+
return parsed if abs(parsed) <= _MAXIMUM_SAFE_INTEGER else None
|
|
102
63
|
|
|
103
64
|
|
|
104
65
|
class _SegmentationParser:
|
|
@@ -180,50 +141,15 @@ class _SegmentationParser:
|
|
|
180
141
|
return False
|
|
181
142
|
|
|
182
143
|
if line.startswith("<"):
|
|
183
|
-
accepted = self.
|
|
144
|
+
accepted = self._accept_api_line(line, raw)
|
|
184
145
|
if accepted is not None:
|
|
185
146
|
return accepted
|
|
186
147
|
|
|
187
|
-
|
|
188
|
-
if match is None:
|
|
189
|
-
if line.startswith("[frame=") or line.startswith("object="):
|
|
190
|
-
self._diagnose("malformed_record", "Malformed structured record.", raw)
|
|
191
|
-
else:
|
|
192
|
-
self._add_record(
|
|
193
|
-
SegmentationTextRecord(order=len(self._records), text=raw)
|
|
194
|
-
)
|
|
195
|
-
return True
|
|
196
|
-
|
|
197
|
-
frame_index = None if match.group(1) is None else _integer_text(match.group(1))
|
|
198
|
-
if match.group(1) is not None and (frame_index is None or frame_index < 0):
|
|
199
|
-
self._diagnose(
|
|
200
|
-
"invalid_frame",
|
|
201
|
-
"Frame references must be non-negative safe integers.",
|
|
202
|
-
raw,
|
|
203
|
-
)
|
|
204
|
-
return True
|
|
205
|
-
if self._media == "image" and frame_index is not None:
|
|
206
|
-
self._diagnose(
|
|
207
|
-
"unexpected_frame",
|
|
208
|
-
"Image segmentation records cannot include a frame reference.",
|
|
209
|
-
raw,
|
|
210
|
-
)
|
|
211
|
-
return True
|
|
212
|
-
|
|
213
|
-
object_id = match.group(2)
|
|
214
|
-
kind = match.group(3)
|
|
215
|
-
value = match.group(4)
|
|
216
|
-
frame = None if frame_index is None else FrameReference(frame_index)
|
|
217
|
-
if kind == "point":
|
|
218
|
-
self._accept_point(object_id, frame, value, raw)
|
|
219
|
-
elif kind == "box":
|
|
220
|
-
self._accept_box(object_id, frame, value, raw)
|
|
221
|
-
else:
|
|
222
|
-
self._accept_mask(object_id, frame, value, raw)
|
|
148
|
+
self._add_record(SegmentationTextRecord(order=len(self._records), text=raw))
|
|
223
149
|
return True
|
|
224
150
|
|
|
225
|
-
def
|
|
226
|
-
header =
|
|
151
|
+
def _accept_api_line(self, line: str, raw: str) -> bool | None:
|
|
152
|
+
header = _API_HEADER_PATTERN.fullmatch(line)
|
|
227
153
|
if header is None:
|
|
228
154
|
return None
|
|
229
155
|
frame_index = _integer_text(header.group(1))
|
|
@@ -247,17 +173,17 @@ class _SegmentationParser:
|
|
|
247
173
|
if not remainder:
|
|
248
174
|
self._diagnose(
|
|
249
175
|
"malformed_record",
|
|
250
|
-
"Malformed
|
|
176
|
+
"Malformed SAM API object record.",
|
|
251
177
|
raw,
|
|
252
178
|
)
|
|
253
179
|
return True
|
|
254
180
|
accepted = False
|
|
255
181
|
while remainder:
|
|
256
|
-
match =
|
|
182
|
+
match = _API_RECORD_PATTERN.match(remainder)
|
|
257
183
|
if match is None:
|
|
258
184
|
self._diagnose(
|
|
259
185
|
"malformed_record",
|
|
260
|
-
"Malformed
|
|
186
|
+
"Malformed SAM API object record.",
|
|
261
187
|
raw,
|
|
262
188
|
)
|
|
263
189
|
return True
|
|
@@ -267,7 +193,7 @@ class _SegmentationParser:
|
|
|
267
193
|
)
|
|
268
194
|
if any(value is None for value in coordinate_values):
|
|
269
195
|
self._diagnose(
|
|
270
|
-
"invalid_box", "
|
|
196
|
+
"invalid_box", "SAM API box coordinates are invalid.", raw
|
|
271
197
|
)
|
|
272
198
|
return True
|
|
273
199
|
(
|
|
@@ -289,7 +215,7 @@ class _SegmentationParser:
|
|
|
289
215
|
or inclusive_bottom >= source_height
|
|
290
216
|
):
|
|
291
217
|
self._diagnose(
|
|
292
|
-
"invalid_box", "
|
|
218
|
+
"invalid_box", "SAM API box coordinates are invalid.", raw
|
|
293
219
|
)
|
|
294
220
|
return True
|
|
295
221
|
bounds = SegmentationMaskBounds(
|
|
@@ -309,14 +235,16 @@ class _SegmentationParser:
|
|
|
309
235
|
bottom=inclusive_bottom + 1,
|
|
310
236
|
)
|
|
311
237
|
)
|
|
312
|
-
mask_height = match.group(8)
|
|
313
|
-
mask_width = match.group(9)
|
|
314
238
|
payload = match.group(10)
|
|
315
|
-
encoding = "lossless" if payload.startswith("~") else "one_bit"
|
|
316
239
|
self._accept_mask(
|
|
317
240
|
object_id,
|
|
318
241
|
frame,
|
|
319
|
-
|
|
242
|
+
SegmentationMask(
|
|
243
|
+
encoding="lossless" if payload.startswith("~") else "one_bit",
|
|
244
|
+
payload=payload,
|
|
245
|
+
width=int(match.group(9)),
|
|
246
|
+
height=int(match.group(8)),
|
|
247
|
+
),
|
|
320
248
|
raw,
|
|
321
249
|
bounds,
|
|
322
250
|
)
|
|
@@ -324,109 +252,20 @@ class _SegmentationParser:
|
|
|
324
252
|
remainder = remainder[match.end() :]
|
|
325
253
|
return accepted
|
|
326
254
|
|
|
327
|
-
def _accept_point(
|
|
328
|
-
self,
|
|
329
|
-
object_id: str,
|
|
330
|
-
frame: FrameReference | None,
|
|
331
|
-
value: str,
|
|
332
|
-
raw: str,
|
|
333
|
-
) -> None:
|
|
334
|
-
match = _POINT_PATTERN.fullmatch(_trim(value))
|
|
335
|
-
x = None if match is None else _javascript_number(match.group(1))
|
|
336
|
-
y = None if match is None else _javascript_number(match.group(2))
|
|
337
|
-
if x is None or y is None:
|
|
338
|
-
self._diagnose(
|
|
339
|
-
"malformed_point", "Point coordinates must be finite numbers.", raw
|
|
340
|
-
)
|
|
341
|
-
return
|
|
342
|
-
self._add_record(
|
|
343
|
-
SegmentationPointRecord(
|
|
344
|
-
order=len(self._records),
|
|
345
|
-
object_id=object_id,
|
|
346
|
-
frame=frame,
|
|
347
|
-
x=x,
|
|
348
|
-
y=y,
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
def _accept_box(
|
|
353
|
-
self,
|
|
354
|
-
object_id: str,
|
|
355
|
-
frame: FrameReference | None,
|
|
356
|
-
value: str,
|
|
357
|
-
raw: str,
|
|
358
|
-
) -> None:
|
|
359
|
-
match = _BOX_PATTERN.fullmatch(_trim(value))
|
|
360
|
-
coordinates = (
|
|
361
|
-
None
|
|
362
|
-
if match is None
|
|
363
|
-
else tuple(_javascript_number(item) for item in match.group(1, 2, 3, 4))
|
|
364
|
-
)
|
|
365
|
-
if coordinates is None or any(item is None for item in coordinates):
|
|
366
|
-
self._diagnose(
|
|
367
|
-
"malformed_box", "Box coordinates must be finite numbers.", raw
|
|
368
|
-
)
|
|
369
|
-
return
|
|
370
|
-
left, top, right, bottom = cast(tuple[float, float, float, float], coordinates)
|
|
371
|
-
if right < left or bottom < top:
|
|
372
|
-
self._diagnose("invalid_box", "Box coordinates must be ordered.", raw)
|
|
373
|
-
return
|
|
374
|
-
self._add_record(
|
|
375
|
-
SegmentationBoxRecord(
|
|
376
|
-
order=len(self._records),
|
|
377
|
-
object_id=object_id,
|
|
378
|
-
frame=frame,
|
|
379
|
-
left=left,
|
|
380
|
-
top=top,
|
|
381
|
-
right=right,
|
|
382
|
-
bottom=bottom,
|
|
383
|
-
)
|
|
384
|
-
)
|
|
385
|
-
|
|
386
255
|
def _accept_mask(
|
|
387
256
|
self,
|
|
388
257
|
object_id: str,
|
|
389
258
|
frame: FrameReference | None,
|
|
390
|
-
|
|
259
|
+
mask: SegmentationMask,
|
|
391
260
|
raw: str,
|
|
392
|
-
bounds: SegmentationMaskBounds
|
|
261
|
+
bounds: SegmentationMaskBounds,
|
|
393
262
|
) -> None:
|
|
394
|
-
|
|
395
|
-
if
|
|
396
|
-
self._diagnose(
|
|
397
|
-
"malformed_mask",
|
|
398
|
-
"Mask records require encoding, size, and one complete payload.",
|
|
399
|
-
raw,
|
|
400
|
-
)
|
|
401
|
-
return
|
|
402
|
-
encoding = match.group(1)
|
|
403
|
-
width_number = _javascript_number(match.group(2))
|
|
404
|
-
height_number = _javascript_number(match.group(3))
|
|
405
|
-
width = int(width_number) if width_number is not None else 0
|
|
406
|
-
height = int(height_number) if height_number is not None else 0
|
|
407
|
-
payload = match.group(4)
|
|
408
|
-
if encoding not in ("one_bit", "lossless"):
|
|
409
|
-
self._diagnose(
|
|
410
|
-
"unsupported_mask_encoding",
|
|
411
|
-
f"Unsupported complete mask encoding: {encoding}.",
|
|
412
|
-
raw,
|
|
413
|
-
)
|
|
414
|
-
return
|
|
415
|
-
area = float(width) * float(height)
|
|
416
|
-
if (
|
|
417
|
-
width <= 0
|
|
418
|
-
or height <= 0
|
|
419
|
-
or not math.isfinite(area)
|
|
420
|
-
or not area.is_integer()
|
|
421
|
-
or abs(area) > _MAXIMUM_SAFE_INTEGER
|
|
422
|
-
):
|
|
263
|
+
area = mask.width * mask.height
|
|
264
|
+
if mask.width <= 0 or mask.height <= 0 or area > _MAXIMUM_SAFE_INTEGER:
|
|
423
265
|
self._diagnose(
|
|
424
266
|
"invalid_mask_size", "Mask dimensions must be positive.", raw
|
|
425
267
|
)
|
|
426
268
|
return
|
|
427
|
-
mask = SegmentationMask(
|
|
428
|
-
encoding=encoding, payload=payload, width=width, height=height
|
|
429
|
-
)
|
|
430
269
|
try:
|
|
431
270
|
decode_mask_to_raster(mask)
|
|
432
271
|
except InvalidSegmentationMaskError as error:
|
|
@@ -107,6 +107,7 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
107
107
|
self._error: BaseException | None = None
|
|
108
108
|
self._lane: OutputTextLane | None = None
|
|
109
109
|
self._lane_done = False
|
|
110
|
+
self._lane_finalizers: set[str] = set()
|
|
110
111
|
self._text = ""
|
|
111
112
|
self._saw_delta = False
|
|
112
113
|
|
|
@@ -169,6 +170,9 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
169
170
|
if self._consumption == "idle":
|
|
170
171
|
self._consumption = "final"
|
|
171
172
|
failure = self._start_failure(ResponsesStreamAbortedError())
|
|
173
|
+
if current is self._active_read:
|
|
174
|
+
# A source finalizer cannot join the read that is running it.
|
|
175
|
+
return
|
|
172
176
|
try:
|
|
173
177
|
await asyncio.shield(failure)
|
|
174
178
|
except asyncio.CancelledError:
|
|
@@ -274,30 +278,19 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
274
278
|
self._push_parser(delta, emit=emit)
|
|
275
279
|
return None
|
|
276
280
|
if event_type == "response.output_text.done":
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
self._lane,
|
|
289
|
-
lane,
|
|
290
|
-
)
|
|
291
|
-
if self._saw_delta and text != self._text:
|
|
292
|
-
raise ResponsesStreamLaneError(
|
|
293
|
-
"The finalized output text conflicts with its accumulated deltas.",
|
|
294
|
-
self._lane,
|
|
295
|
-
lane,
|
|
281
|
+
self._finalize_output_text(event, _field(event, "text"), emit=emit)
|
|
282
|
+
return None
|
|
283
|
+
if event_type == "response.content_part.done":
|
|
284
|
+
part = _field(event, "part")
|
|
285
|
+
part_type = _field(part, "type")
|
|
286
|
+
if part_type == "output_text":
|
|
287
|
+
self._finalize_output_text(event, _field(part, "text"), emit=emit)
|
|
288
|
+
elif part_type == "refusal":
|
|
289
|
+
refusal = _field(part, "refusal")
|
|
290
|
+
message = (
|
|
291
|
+
refusal if isinstance(refusal, str) else "The response was refused."
|
|
296
292
|
)
|
|
297
|
-
|
|
298
|
-
self._text = text
|
|
299
|
-
self._push_parser(text, emit=emit)
|
|
300
|
-
self._lane_done = True
|
|
293
|
+
raise ResponsesStreamRefusalError(message, event)
|
|
301
294
|
return None
|
|
302
295
|
if event_type in ("response.refusal.delta", "response.refusal.done"):
|
|
303
296
|
refusal = _field(event, "refusal")
|
|
@@ -342,6 +335,46 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
342
335
|
raise ResponsesStreamEventError(event_message, event)
|
|
343
336
|
return None
|
|
344
337
|
|
|
338
|
+
def _finalize_output_text(
|
|
339
|
+
self, event: ResponsesEventLike, text: object, *, emit: bool
|
|
340
|
+
) -> None:
|
|
341
|
+
lane = self._accept_lane(event)
|
|
342
|
+
event_type = _field(event, "type")
|
|
343
|
+
assert isinstance(event_type, str)
|
|
344
|
+
if self._lane_done and event_type in self._lane_finalizers:
|
|
345
|
+
raise ResponsesStreamLaneError(
|
|
346
|
+
"The response finalized its output text lane more than once.",
|
|
347
|
+
self._lane,
|
|
348
|
+
lane,
|
|
349
|
+
)
|
|
350
|
+
if not isinstance(text, str):
|
|
351
|
+
raise ResponsesStreamLaneError(
|
|
352
|
+
"The completed output text is missing its text.",
|
|
353
|
+
self._lane,
|
|
354
|
+
lane,
|
|
355
|
+
)
|
|
356
|
+
if self._lane_done:
|
|
357
|
+
if text != self._text:
|
|
358
|
+
raise ResponsesStreamLaneError(
|
|
359
|
+
"The response finalized its output text lane twice with "
|
|
360
|
+
"conflicting text.",
|
|
361
|
+
self._lane,
|
|
362
|
+
lane,
|
|
363
|
+
)
|
|
364
|
+
self._lane_finalizers.add(event_type)
|
|
365
|
+
return
|
|
366
|
+
if self._saw_delta and text != self._text:
|
|
367
|
+
raise ResponsesStreamLaneError(
|
|
368
|
+
"The finalized output text conflicts with its accumulated deltas.",
|
|
369
|
+
self._lane,
|
|
370
|
+
lane,
|
|
371
|
+
)
|
|
372
|
+
if not self._saw_delta:
|
|
373
|
+
self._text = text
|
|
374
|
+
self._push_parser(text, emit=emit)
|
|
375
|
+
self._lane_done = True
|
|
376
|
+
self._lane_finalizers.add(event_type)
|
|
377
|
+
|
|
345
378
|
def _accept_lane(self, event: ResponsesEventLike) -> OutputTextLane:
|
|
346
379
|
item_id = _field(event, "item_id")
|
|
347
380
|
output_index = _safe_nonnegative_integer(_field(event, "output_index"))
|
|
@@ -437,19 +470,41 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
437
470
|
if isinstance(error, ResponsesStreamError)
|
|
438
471
|
else ResponsesStreamParserError(error)
|
|
439
472
|
)
|
|
473
|
+
# Stop admitting reads before yielding, and hand the current read to
|
|
474
|
+
# the one cleanup task. Consumer cancellation must not cancel it again.
|
|
475
|
+
self._interrupt_reads(failure)
|
|
476
|
+
active = self._active_read
|
|
440
477
|
self._failure_task = self._ensure_loop().create_task(
|
|
441
|
-
self._fail_and_close(
|
|
478
|
+
self._fail_and_close(
|
|
479
|
+
failure, active, cancel_read=active is not asyncio.current_task()
|
|
480
|
+
)
|
|
442
481
|
)
|
|
443
482
|
self._observe(self._failure_task)
|
|
444
483
|
return self._failure_task
|
|
445
484
|
|
|
446
|
-
async def _fail_and_close(
|
|
447
|
-
self
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
485
|
+
async def _fail_and_close(
|
|
486
|
+
self,
|
|
487
|
+
failure: ResponsesStreamError,
|
|
488
|
+
active: asyncio.Future[ResponsesEventLike] | None,
|
|
489
|
+
*,
|
|
490
|
+
cancel_read: bool,
|
|
491
|
+
) -> None:
|
|
492
|
+
if active is not None:
|
|
493
|
+
# A read initiating its own close must unwind itself: cancellation
|
|
494
|
+
# could interrupt the finalizer that requested cleanup.
|
|
495
|
+
if cancel_read and not active.done():
|
|
496
|
+
active.cancel()
|
|
497
|
+
try:
|
|
452
498
|
await active
|
|
499
|
+
except (asyncio.CancelledError, StopAsyncIteration):
|
|
500
|
+
pass
|
|
501
|
+
except BaseException as error:
|
|
502
|
+
failure = ResponsesStreamSourceError(
|
|
503
|
+
"return", error, prior_error=failure
|
|
504
|
+
)
|
|
505
|
+
finally:
|
|
506
|
+
if self._active_read is active:
|
|
507
|
+
self._active_read = None
|
|
453
508
|
if (
|
|
454
509
|
isinstance(failure, ResponsesStreamSourceError)
|
|
455
510
|
and failure.operation == "return"
|
|
@@ -486,7 +541,8 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
486
541
|
failure = self._start_failure(ResponsesStreamAbortedError())
|
|
487
542
|
with suppress(asyncio.CancelledError):
|
|
488
543
|
await asyncio.shield(failure)
|
|
489
|
-
|
|
544
|
+
if failure.done():
|
|
545
|
+
self._consumer_done = True
|
|
490
546
|
|
|
491
547
|
def _ensure_source_iterator(self) -> AsyncIterator[ResponsesEventLike]:
|
|
492
548
|
if self._source_iterator is not None:
|
|
@@ -500,6 +556,9 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
500
556
|
return self._source_iterator
|
|
501
557
|
|
|
502
558
|
async def _read_source(self) -> tuple[bool, ResponsesEventLike | None]:
|
|
559
|
+
interrupted = self._ensure_read_interruption()
|
|
560
|
+
if interrupted.done():
|
|
561
|
+
raise interrupted.result()
|
|
503
562
|
iterator = self._ensure_source_iterator()
|
|
504
563
|
try:
|
|
505
564
|
awaitable = iterator.__anext__()
|
|
@@ -512,15 +571,12 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
512
571
|
raise ResponsesStreamSourceError("next", error) from error
|
|
513
572
|
self._active_read = reading
|
|
514
573
|
self._observe(reading)
|
|
515
|
-
interrupted = self._ensure_read_interruption()
|
|
516
574
|
try:
|
|
517
575
|
completed, _pending = await asyncio.wait(
|
|
518
576
|
(reading, interrupted), return_when=asyncio.FIRST_COMPLETED
|
|
519
577
|
)
|
|
520
578
|
if interrupted in completed:
|
|
521
|
-
|
|
522
|
-
reading.cancel()
|
|
523
|
-
raise interrupted_error
|
|
579
|
+
raise interrupted.result()
|
|
524
580
|
try:
|
|
525
581
|
event = reading.result()
|
|
526
582
|
except StopAsyncIteration:
|
|
@@ -540,16 +596,17 @@ class ParsedResponsesStream(AsyncIterator[EventT], Generic[EventT, ResultT]):
|
|
|
540
596
|
raise ResponsesStreamSourceError("next", shape_error) from shape_error
|
|
541
597
|
return False, event
|
|
542
598
|
except asyncio.CancelledError:
|
|
543
|
-
|
|
544
|
-
with suppress(BaseException):
|
|
545
|
-
await reading
|
|
599
|
+
self._start_failure(ResponsesStreamAbortedError())
|
|
546
600
|
raise
|
|
547
601
|
finally:
|
|
548
|
-
if self._active_read is reading:
|
|
602
|
+
if self._active_read is reading and self._failure_task is None:
|
|
549
603
|
self._active_read = None
|
|
550
604
|
|
|
551
605
|
async def _close_source(self) -> None:
|
|
552
606
|
current = asyncio.current_task()
|
|
607
|
+
if current is self._active_read:
|
|
608
|
+
self._start_failure(ResponsesStreamAbortedError())
|
|
609
|
+
return
|
|
553
610
|
if current is self._close_task:
|
|
554
611
|
return
|
|
555
612
|
if self._close_task is None:
|
|
@@ -48,11 +48,20 @@ class SegmentationMaskBounds:
|
|
|
48
48
|
bottom: float
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
SegmentationMaskEncoding: TypeAlias = Literal["lossless", "one_bit"]
|
|
52
|
+
"""The wire encodings of a mask payload: ``~`` selects lossless, ``!`` one_bit."""
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
@dataclass(frozen=True, slots=True)
|
|
52
56
|
class SegmentationMask:
|
|
53
|
-
"""One complete
|
|
57
|
+
"""One complete mask exactly as the SAM API emitted it.
|
|
58
|
+
|
|
59
|
+
``payload`` is the base85 text after the encoding marker and is never
|
|
60
|
+
partial; ``width`` and ``height`` are the raster's own dimensions, not the
|
|
61
|
+
frame's.
|
|
62
|
+
"""
|
|
54
63
|
|
|
55
|
-
encoding:
|
|
64
|
+
encoding: SegmentationMaskEncoding
|
|
56
65
|
payload: str
|
|
57
66
|
width: int
|
|
58
67
|
height: int
|
|
@@ -76,18 +85,6 @@ class SegmentationTextRecord:
|
|
|
76
85
|
text: str
|
|
77
86
|
|
|
78
87
|
|
|
79
|
-
@dataclass(frozen=True, slots=True)
|
|
80
|
-
class SegmentationPointRecord:
|
|
81
|
-
"""A point associated with one object and optional frame."""
|
|
82
|
-
|
|
83
|
-
kind: Literal["point"] = field(default="point", init=False)
|
|
84
|
-
order: int
|
|
85
|
-
object_id: str
|
|
86
|
-
x: float
|
|
87
|
-
y: float
|
|
88
|
-
frame: FrameReference | None = None
|
|
89
|
-
|
|
90
|
-
|
|
91
88
|
@dataclass(frozen=True, slots=True)
|
|
92
89
|
class SegmentationBoxRecord:
|
|
93
90
|
"""A half-open or explicitly supplied box in source-media coordinates."""
|
|
@@ -112,15 +109,12 @@ class SegmentationMaskRecord:
|
|
|
112
109
|
identity: SegmentationMaskIdentity
|
|
113
110
|
revision: int
|
|
114
111
|
mask: SegmentationMask
|
|
112
|
+
bounds: SegmentationMaskBounds
|
|
115
113
|
frame: FrameReference | None = None
|
|
116
|
-
bounds: SegmentationMaskBounds | None = None
|
|
117
114
|
|
|
118
115
|
|
|
119
116
|
SegmentationRecord: TypeAlias = (
|
|
120
|
-
SegmentationTextRecord
|
|
121
|
-
| SegmentationPointRecord
|
|
122
|
-
| SegmentationBoxRecord
|
|
123
|
-
| SegmentationMaskRecord
|
|
117
|
+
SegmentationTextRecord | SegmentationBoxRecord | SegmentationMaskRecord
|
|
124
118
|
)
|
|
125
119
|
|
|
126
120
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|