bynn-decisionnode 0.3.1__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.
- bynn_decisionnode-0.3.1.dist-info/METADATA +939 -0
- bynn_decisionnode-0.3.1.dist-info/RECORD +19 -0
- bynn_decisionnode-0.3.1.dist-info/WHEEL +4 -0
- bynn_decisionnode-0.3.1.dist-info/licenses/LICENSE +21 -0
- decisionnode/__init__.py +195 -0
- decisionnode/_base.py +548 -0
- decisionnode/_batches.py +333 -0
- decisionnode/_boosters.py +553 -0
- decisionnode/_client.py +2269 -0
- decisionnode/_errors.py +218 -0
- decisionnode/_images.py +283 -0
- decisionnode/_questions.py +262 -0
- decisionnode/_sessions.py +711 -0
- decisionnode/_types.py +764 -0
- decisionnode/_uploads.py +265 -0
- decisionnode/_validate.py +934 -0
- decisionnode/_version.py +5 -0
- decisionnode/_videos.py +197 -0
- decisionnode/py.typed +0 -0
decisionnode/_version.py
ADDED
decisionnode/_videos.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Videos sent as frames: the frames the caller sampled, each with its time.
|
|
2
|
+
|
|
3
|
+
The API reads the frames of a video as one video, in order and with their times. The
|
|
4
|
+
client sends frames only, inline as base64: never a video file, a URL or sound, and it
|
|
5
|
+
never uploads, resizes or re-encodes a frame. The checks here need no decoding; frame
|
|
6
|
+
counts, a frame's bytes, sizes and pixels, the window and the token budget are the
|
|
7
|
+
server's.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import math
|
|
14
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Union
|
|
15
|
+
|
|
16
|
+
from ._images import MEDIA_TYPES, Image, sniff_media_type
|
|
17
|
+
from ._validate import MAX_VIDEOS
|
|
18
|
+
from ._validate import usage_error as _usage_error
|
|
19
|
+
|
|
20
|
+
__all__ = ["VideoFrame", "Video", "VideoInput", "video_list", "check_videos", "MAX_VIDEOS", "MAX_FPS"]
|
|
21
|
+
|
|
22
|
+
MAX_FPS = 4
|
|
23
|
+
|
|
24
|
+
Number = Union[int, float]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VideoFrame:
|
|
28
|
+
"""One frame of a video: its time in seconds and its image::
|
|
29
|
+
|
|
30
|
+
VideoFrame(30.0, jpeg_bytes) # the type read from the bytes
|
|
31
|
+
VideoFrame(30.5, "iVBORw0KGgo...", "image/png") # base64 text needs its type
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
t: The frame's time in seconds: at least 0, and larger than the time of the frame
|
|
35
|
+
before it in the same video. A camera's own clock works.
|
|
36
|
+
data: The image as bytes (JPEG, PNG or WebP), or as base64 text.
|
|
37
|
+
media_type: ``image/jpeg``, ``image/png`` or ``image/webp``. Read from the first
|
|
38
|
+
bytes when bytes are given without one.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("t", "data", "media_type", "_b64")
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
t: Number,
|
|
46
|
+
data: Union[bytes, bytearray, memoryview, str],
|
|
47
|
+
media_type: Optional[str] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if isinstance(data, (bytearray, memoryview)):
|
|
50
|
+
data = bytes(data)
|
|
51
|
+
if not isinstance(data, (bytes, str)):
|
|
52
|
+
raise ValueError("VideoFrame data must be bytes or base64 text, got %s" % type(data).__name__)
|
|
53
|
+
if media_type is None and isinstance(data, bytes):
|
|
54
|
+
media_type = sniff_media_type(data)
|
|
55
|
+
self.t = t
|
|
56
|
+
self.data = data
|
|
57
|
+
self.media_type = media_type
|
|
58
|
+
self._b64: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
"""The frame as the API takes it: ``{"t", "media_type", "data"}``."""
|
|
62
|
+
if self._b64 is None:
|
|
63
|
+
data = self.data
|
|
64
|
+
self._b64 = base64.b64encode(data).decode("ascii") if isinstance(data, bytes) else data
|
|
65
|
+
return {"t": self.t, "media_type": self.media_type, "data": self._b64}
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return "VideoFrame(t=%r, media_type=%r)" % (self.t, self.media_type)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Video:
|
|
72
|
+
"""A video every question in the request can see, as the frames you sampled::
|
|
73
|
+
|
|
74
|
+
Video("dock", [VideoFrame(30.0, f0), VideoFrame(30.5, f1)], fps=2)
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
id: The video's id, unique across the request's images and videos. The model
|
|
78
|
+
sees the video as "Video <n>" (its place in the request's videos, from 1)
|
|
79
|
+
with this id beside it, so text in the state or the instructions can refer
|
|
80
|
+
to it by either.
|
|
81
|
+
frames: The frames in time order, as :class:`VideoFrame` objects or wire-form mappings.
|
|
82
|
+
fps: The most frames a second the API reads (above 0, at most 4; the API's
|
|
83
|
+
default is 2). Frames closer together than that are skipped by the API.
|
|
84
|
+
start: The start of the window read, in seconds on the frames' own times.
|
|
85
|
+
end: The end of the window read (not included); after ``start``.
|
|
86
|
+
|
|
87
|
+
``fps``, ``start`` and ``end`` are left out of the request when not given.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
__slots__ = ("id", "frames", "fps", "start", "end")
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
id: str, # noqa: A002 - the wire name
|
|
95
|
+
frames: Iterable[Union[VideoFrame, Mapping[str, Any]]],
|
|
96
|
+
fps: Optional[Number] = None,
|
|
97
|
+
start: Optional[Number] = None,
|
|
98
|
+
end: Optional[Number] = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
if isinstance(frames, (str, bytes, Mapping)) or not isinstance(frames, Iterable):
|
|
101
|
+
raise ValueError("Video frames must be a list of VideoFrame objects or wire-form mappings")
|
|
102
|
+
items = list(frames)
|
|
103
|
+
for frame in items:
|
|
104
|
+
if not isinstance(frame, (VideoFrame, Mapping)):
|
|
105
|
+
raise ValueError("Video frames must be VideoFrame objects or wire-form mappings, got %r" % (frame,))
|
|
106
|
+
self.id = id
|
|
107
|
+
self.frames = items
|
|
108
|
+
self.fps = fps
|
|
109
|
+
self.start = start
|
|
110
|
+
self.end = end
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
113
|
+
"""The video as the API takes it, ``fps``, ``start`` and ``end`` only when given."""
|
|
114
|
+
out: Dict[str, Any] = {"id": self.id}
|
|
115
|
+
for name in ("fps", "start", "end"):
|
|
116
|
+
value = getattr(self, name)
|
|
117
|
+
if value is not None:
|
|
118
|
+
out[name] = value
|
|
119
|
+
out["frames"] = [f.to_dict() if isinstance(f, VideoFrame) else dict(f) for f in self.frames]
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
def __repr__(self) -> str:
|
|
123
|
+
return "Video(id=%r, frames=%d)" % (self.id, len(self.frames))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
VideoInput = Union[Video, Mapping[str, Any]]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def video_list(videos: Any) -> List[Dict[str, Any]]:
|
|
130
|
+
"""The videos argument in wire form, or ``ValueError``."""
|
|
131
|
+
if not videos:
|
|
132
|
+
return []
|
|
133
|
+
if isinstance(videos, (Video, Mapping)):
|
|
134
|
+
videos = [videos]
|
|
135
|
+
elif isinstance(videos, (str, bytes)) or not isinstance(videos, Iterable):
|
|
136
|
+
raise ValueError("videos must be a list of Video objects or wire-form mappings")
|
|
137
|
+
out = []
|
|
138
|
+
for video in videos:
|
|
139
|
+
if isinstance(video, Video):
|
|
140
|
+
out.append(video.to_dict())
|
|
141
|
+
elif isinstance(video, Mapping):
|
|
142
|
+
out.append(dict(video))
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError("videos must be Video objects or wire-form mappings, got %r" % (video,))
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _finite(value: Any) -> bool:
|
|
149
|
+
"""A JSON number (never a boolean), finite."""
|
|
150
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_videos(videos: Sequence[Mapping[str, Any]], images: Sequence[Any] = ()) -> None:
|
|
154
|
+
"""Raise what the API would answer for these videos (in wire form), in its order and
|
|
155
|
+
words: the count, each id, each frame's time and media type, ``fps`` and the window."""
|
|
156
|
+
if not videos:
|
|
157
|
+
return
|
|
158
|
+
seen: Set[str] = set()
|
|
159
|
+
for img in images:
|
|
160
|
+
iid = img.id if isinstance(img, Image) else img.get("id") if isinstance(img, Mapping) else None
|
|
161
|
+
if isinstance(iid, str):
|
|
162
|
+
seen.add(iid)
|
|
163
|
+
for i, video in enumerate(videos):
|
|
164
|
+
vid = video.get("id")
|
|
165
|
+
if not isinstance(vid, str) or vid == "":
|
|
166
|
+
raise _usage_error("Video %d needs an id." % i)
|
|
167
|
+
if vid in seen:
|
|
168
|
+
raise _usage_error("Video id %r is used twice (ids are unique across images and videos)." % vid)
|
|
169
|
+
seen.add(vid)
|
|
170
|
+
where = "Video %r: " % vid
|
|
171
|
+
frames = video.get("frames")
|
|
172
|
+
if not isinstance(frames, list) or not frames:
|
|
173
|
+
raise _usage_error(where + "frames must be a non-empty list of frames.")
|
|
174
|
+
before: Optional[Number] = None
|
|
175
|
+
for n, frame in enumerate(frames):
|
|
176
|
+
if not isinstance(frame, Mapping):
|
|
177
|
+
raise _usage_error(where + "frame %d must be an object with t, media_type and data." % n)
|
|
178
|
+
t: Any = frame.get("t")
|
|
179
|
+
if not _finite(t) or t < 0:
|
|
180
|
+
raise _usage_error(where + "frame %d: t must be a number of seconds, at least 0." % n)
|
|
181
|
+
if before is not None and t <= before:
|
|
182
|
+
raise _usage_error(where + "frame %d: t must be larger than the frame before it (%g)." % (n, before))
|
|
183
|
+
before = t
|
|
184
|
+
if not isinstance(frame.get("data"), str):
|
|
185
|
+
raise _usage_error(where + "frame %d: data must be base64 text." % n)
|
|
186
|
+
if frame.get("media_type") not in MEDIA_TYPES:
|
|
187
|
+
raise _usage_error(where + "frame %d: media_type must be one of %s." % (n, ", ".join(MEDIA_TYPES)))
|
|
188
|
+
fps: Any = video.get("fps")
|
|
189
|
+
if fps is not None and (not _finite(fps) or not 0 < fps <= MAX_FPS):
|
|
190
|
+
raise _usage_error(where + "fps must be a number above 0 and at most %d." % MAX_FPS)
|
|
191
|
+
for name in ("start", "end"):
|
|
192
|
+
value: Any = video.get(name)
|
|
193
|
+
if value is not None and (not _finite(value) or value < 0):
|
|
194
|
+
raise _usage_error(where + "%s must be a number of seconds, at least 0." % name)
|
|
195
|
+
end: Any = video.get("end")
|
|
196
|
+
if end is not None and end <= (video.get("start") or 0):
|
|
197
|
+
raise _usage_error(where + "end must be after start.")
|
decisionnode/py.typed
ADDED
|
File without changes
|