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.
@@ -0,0 +1,5 @@
1
+ __version__ = "0.3.1"
2
+
3
+ # The version of the SDK specification this package implements, shared by every
4
+ # official DecisionNode SDK.
5
+ SPEC_VERSION = "1.3"
@@ -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