vidformer 1.2.0__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.

Potentially problematic release.


This version of vidformer might be problematic. Click here for more details.

vidformer/__init__.py ADDED
@@ -0,0 +1,900 @@
1
+ """
2
+ vidformer-py is a Python 🐍 interface for [vidformer](https://github.com/ixlab/vidformer).
3
+
4
+ **Quick links:**
5
+ * [📦 PyPI](https://pypi.org/project/vidformer/)
6
+ * [📘 Documentation - vidformer-py](https://ixlab.github.io/vidformer/vidformer-py/pdoc/)
7
+ * [📘 Documentation - vidformer.cv2](https://ixlab.github.io/vidformer/vidformer-py/pdoc/vidformer/cv2.html)
8
+ * [📘 Documentation - vidformer.supervision](https://ixlab.github.io/vidformer/vidformer-py/pdoc/vidformer/supervision.html)
9
+ * [🧑‍💻 Source Code](https://github.com/ixlab/vidformer/tree/main/vidformer-py/)
10
+ """
11
+
12
+ __version__ = "1.2.0"
13
+
14
+
15
+ import base64
16
+ import gzip
17
+ import json
18
+ import struct
19
+ import time
20
+ from fractions import Fraction
21
+ from urllib.parse import urlparse
22
+
23
+ import requests
24
+
25
+ _in_notebook = False
26
+ try:
27
+ from IPython import get_ipython
28
+
29
+ if "IPKernelApp" in get_ipython().config:
30
+ _in_notebook = True
31
+ except Exception:
32
+ pass
33
+
34
+
35
+ def _wait_for_url(url, max_attempts=150, delay=0.1):
36
+ for attempt in range(max_attempts):
37
+ try:
38
+ response = requests.get(url)
39
+ if response.status_code == 200:
40
+ return response.text.strip()
41
+ else:
42
+ time.sleep(delay)
43
+ except requests.exceptions.RequestException:
44
+ time.sleep(delay)
45
+ return None
46
+
47
+
48
+ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
49
+ # The namespace is so multiple videos in one tab don't conflict
50
+
51
+ if method == "html":
52
+ from IPython.display import HTML
53
+
54
+ if not status_url:
55
+ html_code = f"""
56
+ <!DOCTYPE html>
57
+ <html>
58
+ <head>
59
+ <title>HLS Video Player</title>
60
+ <!-- Include hls.js library -->
61
+ <script src="{hls_js_url}"></script>
62
+ </head>
63
+ <body>
64
+ <video id="video-{namespace}" controls width="640" height="360" autoplay></video>
65
+ <script>
66
+ var video = document.getElementById('video-{namespace}');
67
+ var videoSrc = '{hls_video_url}';
68
+
69
+ if (Hls.isSupported()) {{
70
+ var hls = new Hls();
71
+ hls.loadSource(videoSrc);
72
+ hls.attachMedia(video);
73
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {{
74
+ video.play();
75
+ }});
76
+ }} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{
77
+ video.src = videoSrc;
78
+ video.addEventListener('loadedmetadata', function() {{
79
+ video.play();
80
+ }});
81
+ }} else {{
82
+ console.error('This browser does not appear to support HLS.');
83
+ }}
84
+ </script>
85
+ </body>
86
+ </html>
87
+ """
88
+ return HTML(data=html_code)
89
+ else:
90
+ html_code = f"""
91
+ <!DOCTYPE html>
92
+ <html>
93
+ <head>
94
+ <title>HLS Video Player</title>
95
+ <script src="{hls_js_url}"></script>
96
+ </head>
97
+ <body>
98
+ <div id="container-{namespace}"></div>
99
+ <script>
100
+ var statusUrl = '{status_url}';
101
+ var videoSrc = '{hls_video_url}';
102
+ var videoNamespace = '{namespace}';
103
+
104
+ function showWaiting() {{
105
+ document.getElementById('container-{namespace}').textContent = 'Waiting...';
106
+ pollStatus();
107
+ }}
108
+
109
+ function pollStatus() {{
110
+ setTimeout(function() {{
111
+ fetch(statusUrl)
112
+ .then(r => r.json())
113
+ .then(res => {{
114
+ if (res.ready) {{
115
+ document.getElementById('container-{namespace}').textContent = '';
116
+ attachHls();
117
+ }} else {{
118
+ pollStatus();
119
+ }}
120
+ }})
121
+ .catch(e => {{
122
+ console.error(e);
123
+ pollStatus();
124
+ }});
125
+ }}, 250);
126
+ }}
127
+
128
+ function attachHls() {{
129
+ var container = document.getElementById('container-{namespace}');
130
+ container.textContent = '';
131
+ var video = document.createElement('video');
132
+ video.id = 'video-' + videoNamespace;
133
+ video.controls = true;
134
+ video.width = 640;
135
+ video.height = 360;
136
+ container.appendChild(video);
137
+ if (Hls.isSupported()) {{
138
+ var hls = new Hls();
139
+ hls.loadSource(videoSrc);
140
+ hls.attachMedia(video);
141
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {{
142
+ video.play();
143
+ }});
144
+ }} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{
145
+ video.src = videoSrc;
146
+ video.addEventListener('loadedmetadata', function() {{
147
+ video.play();
148
+ }});
149
+ }}
150
+ }}
151
+
152
+ fetch(statusUrl)
153
+ .then(r => r.json())
154
+ .then(res => {{
155
+ if (res.ready) {{
156
+ attachHls();
157
+ }} else {{
158
+ showWaiting();
159
+ }}
160
+ }})
161
+ .catch(e => {{
162
+ console.error(e);
163
+ showWaiting();
164
+ }});
165
+ </script>
166
+ </body>
167
+ </html>
168
+ """
169
+ return HTML(data=html_code)
170
+ elif method == "link":
171
+ return hls_video_url
172
+ else:
173
+ raise ValueError("Invalid method")
174
+
175
+
176
+ def _feb_expr_coded_as_scalar(expr) -> bool:
177
+ if type(expr) is tuple:
178
+ expr = list(expr)
179
+ if type(expr) is FilterExpr:
180
+ return False
181
+ if type(expr) is list:
182
+ if len(expr) > 3:
183
+ return False
184
+ else:
185
+ return all([type(x) is int and x >= -(2**15) and x < 2**15 for x in expr])
186
+ else:
187
+ assert type(expr) in [int, float, str, bytes, SourceExpr, bool, list]
188
+ return True
189
+
190
+
191
+ class _FrameExpressionBlock:
192
+ def __init__(self):
193
+ self._functions = []
194
+ self._literals = []
195
+ self._sources = []
196
+ self._kwarg_keys = []
197
+ self._source_fracs = []
198
+ self._exprs = []
199
+ self._frame_exprs = []
200
+
201
+ def __len__(self):
202
+ return len(self._frame_exprs)
203
+
204
+ def insert_expr(self, expr):
205
+ if type(expr) is SourceExpr or type(expr) is FilterExpr:
206
+ return self.insert_frame_expr(expr)
207
+ else:
208
+ return self.insert_data_expr(expr)
209
+
210
+ def insert_data_expr(self, data):
211
+ if type(data) is tuple:
212
+ data = list(data)
213
+ if type(data) is bool:
214
+ self._exprs.append(0x01000000_00000000 | int(data))
215
+ return len(self._exprs) - 1
216
+ elif type(data) is int:
217
+ if data >= -(2**31) and data < 2**31:
218
+ self._exprs.append(data & 0xFFFFFFFF)
219
+ else:
220
+ self._literals.append(_json_arg(data, skip_data_anot=True))
221
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
222
+ return len(self._exprs) - 1
223
+ elif type(data) is float:
224
+ self._exprs.append(
225
+ 0x02000000_00000000 | int.from_bytes(struct.pack("f", data)[::-1])
226
+ )
227
+ elif type(data) is str:
228
+ self._literals.append(_json_arg(data, skip_data_anot=True))
229
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
230
+ elif type(data) is bytes:
231
+ self._literals.append(_json_arg(data, skip_data_anot=True))
232
+ self._exprs.append(0x40000000_00000000 | len(self._literals) - 1)
233
+ elif type(data) is list:
234
+ if len(data) == 0:
235
+ self._exprs.append(0x03000000_00000000)
236
+ return len(self._exprs) - 1
237
+ if (
238
+ len(data) == 1
239
+ and type(data[0]) is int
240
+ and data[0] >= -(2**15)
241
+ and data[0] < 2**15
242
+ ):
243
+ self._exprs.append(0x04000000_00000000 | (data[0] & 0xFFFF))
244
+ return len(self._exprs) - 1
245
+ if (
246
+ len(data) == 2
247
+ and type(data[0]) is int
248
+ and data[0] >= -(2**15)
249
+ and data[0] < 2**15
250
+ and type(data[1]) is int
251
+ and data[1] >= -(2**15)
252
+ and data[1] < 2**15
253
+ ):
254
+ self._exprs.append(
255
+ 0x05000000_00000000
256
+ | ((data[0] & 0xFFFF) << 16)
257
+ | (data[1] & 0xFFFF)
258
+ )
259
+ return len(self._exprs) - 1
260
+ if (
261
+ len(data) == 3
262
+ and type(data[0]) is int
263
+ and data[0] >= -(2**15)
264
+ and data[0] < 2**15
265
+ and type(data[1]) is int
266
+ and data[1] >= -(2**15)
267
+ and data[1] < 2**15
268
+ and type(data[2]) is int
269
+ and data[2] >= -(2**15)
270
+ and data[2] < 2**15
271
+ ):
272
+ self._exprs.append(
273
+ 0x06000000_00000000
274
+ | ((data[0] & 0xFFFF) << 32)
275
+ | ((data[1] & 0xFFFF) << 16)
276
+ | (data[2] & 0xFFFF)
277
+ )
278
+ return len(self._exprs) - 1
279
+ out = len(self._exprs)
280
+ member_idxs = []
281
+ for member in data:
282
+ if _feb_expr_coded_as_scalar(member):
283
+ member_idxs.append(None)
284
+ else:
285
+ member_idxs.append(self.insert_data_expr(member))
286
+
287
+ self._exprs.append(0x42000000_00000000 | len(data))
288
+
289
+ for i in range(len(data)):
290
+ if member_idxs[i] is None:
291
+ self.insert_data_expr(data[i])
292
+ else:
293
+ self._exprs.append(0x45000000_00000000 | member_idxs[i])
294
+
295
+ return out
296
+ else:
297
+ raise Exception("Invalid data type")
298
+
299
+ def insert_frame_expr(self, frame):
300
+ if type(frame) is SourceExpr:
301
+ source = frame._source._name
302
+ if source in self._sources:
303
+ source_idx = self._sources.index(source)
304
+ else:
305
+ source_idx = len(self._sources)
306
+ self._sources.append(source)
307
+ if frame._is_iloc:
308
+ self._exprs.append(
309
+ 0x43000000_00000000 | (source_idx << 32) | frame._idx
310
+ )
311
+ else:
312
+ idx = len(self._source_fracs) // 2
313
+ self._source_fracs.append(frame._idx.numerator)
314
+ self._source_fracs.append(frame._idx.denominator)
315
+ self._exprs.append(0x44000000_00000000 | (source_idx << 32) | idx)
316
+ return len(self._exprs) - 1
317
+ elif type(frame) is FilterExpr:
318
+ func = frame._filter._func
319
+ if func in self._functions:
320
+ func_idx = self._functions.index(func)
321
+ else:
322
+ func_idx = len(self._functions)
323
+ self._functions.append(func)
324
+ len_args = len(frame._args)
325
+ len_kwargs = len(frame._kwargs)
326
+
327
+ arg_idxs = []
328
+ for arg in frame._args:
329
+ if _feb_expr_coded_as_scalar(arg):
330
+ arg_idxs.append(None)
331
+ else:
332
+ arg_idxs.append(self.insert_expr(arg))
333
+ kwarg_idxs = {}
334
+ for k, v in frame._kwargs.items():
335
+ if _feb_expr_coded_as_scalar(v):
336
+ kwarg_idxs[k] = None
337
+ else:
338
+ kwarg_idxs[k] = self.insert_expr(v)
339
+
340
+ out_idx = len(self._exprs)
341
+ self._exprs.append(
342
+ 0x41000000_00000000 | (len_args << 24) | (len_kwargs << 16) | func_idx
343
+ )
344
+ for i in range(len_args):
345
+ if arg_idxs[i] is None:
346
+ # It's a scalar
347
+ self.insert_expr(frame._args[i])
348
+ else:
349
+ # It's an expression pointer
350
+ self._exprs.append(0x45000000_00000000 | arg_idxs[i])
351
+ for k, v in frame._kwargs.items():
352
+ if k in self._kwarg_keys:
353
+ k_idx = self._kwarg_keys.index(k)
354
+ else:
355
+ k_idx = len(self._kwarg_keys)
356
+ self._kwarg_keys.append(k)
357
+ self._exprs.append(0x46000000_00000000 | k_idx)
358
+ if kwarg_idxs[k] is None:
359
+ # It's a scalar
360
+ self.insert_expr(v)
361
+ else:
362
+ # It's an expression pointer
363
+ self._exprs.append(0x45000000_00000000 | kwarg_idxs[k])
364
+ return out_idx
365
+ else:
366
+ raise Exception("Invalid frame type")
367
+
368
+ def insert_frame(self, frame):
369
+ idx = self.insert_frame_expr(frame)
370
+ self._frame_exprs.append(idx)
371
+
372
+ def as_dict(self):
373
+ return {
374
+ "functions": self._functions,
375
+ "literals": self._literals,
376
+ "sources": self._sources,
377
+ "kwarg_keys": self._kwarg_keys,
378
+ "source_fracs": self._source_fracs,
379
+ "exprs": self._exprs,
380
+ "frame_exprs": self._frame_exprs,
381
+ }
382
+
383
+
384
+ class Source:
385
+ def __init__(self, id: str, src):
386
+ self._name = id
387
+ self._fmt = {
388
+ "width": src["width"],
389
+ "height": src["height"],
390
+ "pix_fmt": src["pix_fmt"],
391
+ }
392
+ self._ts = [Fraction(x[0], x[1]) for x in src["ts"]]
393
+ self.iloc = _SourceILoc(self)
394
+
395
+ def id(self) -> str:
396
+ return self._name
397
+
398
+ def fmt(self):
399
+ return {**self._fmt}
400
+
401
+ def ts(self) -> list[Fraction]:
402
+ return self._ts.copy()
403
+
404
+ def __len__(self):
405
+ return len(self._ts)
406
+
407
+ def __getitem__(self, idx):
408
+ if type(idx) is not Fraction:
409
+ raise Exception("Source index must be a Fraction")
410
+ return SourceExpr(self, idx, False)
411
+
412
+ def __repr__(self):
413
+ return f"Source({self._name})"
414
+
415
+
416
+ class Spec:
417
+ def __init__(self, id: str, src):
418
+ self._id = id
419
+ self._fmt = {
420
+ "width": src["width"],
421
+ "height": src["height"],
422
+ "pix_fmt": src["pix_fmt"],
423
+ }
424
+ self._vod_endpoint = src["vod_endpoint"]
425
+ parsed_url = urlparse(self._vod_endpoint)
426
+ self._hls_js_url = f"{parsed_url.scheme}://{parsed_url.netloc}/hls.js"
427
+
428
+ def id(self) -> str:
429
+ return self._id
430
+
431
+ def play(self, method):
432
+ url = f"{self._vod_endpoint}playlist.m3u8"
433
+ status_url = f"{self._vod_endpoint}status"
434
+ hls_js_url = self._hls_js_url
435
+ return _play(self._id, url, hls_js_url, method=method, status_url=status_url)
436
+
437
+
438
+ class Server:
439
+ def __init__(self, endpoint: str, api_key: str):
440
+ if not endpoint.startswith("http://") and not endpoint.startswith("https://"):
441
+ raise Exception("Endpoint must start with http:// or https://")
442
+ if endpoint.endswith("/"):
443
+ raise Exception("Endpoint must not end with /")
444
+ self._endpoint = endpoint
445
+
446
+ self._api_key = api_key
447
+ self._session = requests.Session()
448
+ self._session.headers.update({"Authorization": f"Bearer {self._api_key}"})
449
+ response = self._session.get(
450
+ f"{self._endpoint}/v2/auth",
451
+ headers={"Authorization": f"Bearer {self._api_key}"},
452
+ )
453
+ if not response.ok:
454
+ raise Exception(response.text)
455
+ response = response.json()
456
+ assert response["status"] == "ok"
457
+
458
+ def get_source(self, id: str) -> Source:
459
+ assert type(id) is str
460
+ response = self._session.get(
461
+ f"{self._endpoint}/v2/source/{id}",
462
+ headers={"Authorization": f"Bearer {self._api_key}"},
463
+ )
464
+ if not response.ok:
465
+ raise Exception(response.text)
466
+ response = response.json()
467
+ return Source(response["id"], response)
468
+
469
+ def list_sources(self) -> list[str]:
470
+ response = self._session.get(
471
+ f"{self._endpoint}/v2/source",
472
+ headers={"Authorization": f"Bearer {self._api_key}"},
473
+ )
474
+ if not response.ok:
475
+ raise Exception(response.text)
476
+ response = response.json()
477
+ return response
478
+
479
+ def delete_source(self, id: str):
480
+ assert type(id) is str
481
+ response = self._session.delete(
482
+ f"{self._endpoint}/v2/source/{id}",
483
+ headers={"Authorization": f"Bearer {self._api_key}"},
484
+ )
485
+ if not response.ok:
486
+ raise Exception(response.text)
487
+ response = response.json()
488
+ assert response["status"] == "ok"
489
+
490
+ def search_source(
491
+ self, name, stream_idx, storage_service, storage_config
492
+ ) -> list[str]:
493
+ assert type(name) is str
494
+ assert type(stream_idx) is int
495
+ assert type(storage_service) is str
496
+ assert type(storage_config) is dict
497
+ for k, v in storage_config.items():
498
+ assert type(k) is str
499
+ assert type(v) is str
500
+ req = {
501
+ "name": name,
502
+ "stream_idx": stream_idx,
503
+ "storage_service": storage_service,
504
+ "storage_config": storage_config,
505
+ }
506
+ response = self._session.post(
507
+ f"{self._endpoint}/v2/source/search",
508
+ json=req,
509
+ headers={"Authorization": f"Bearer {self._api_key}"},
510
+ )
511
+ if not response.ok:
512
+ raise Exception(response.text)
513
+ response = response.json()
514
+ return response
515
+
516
+ def create_source(
517
+ self, name, stream_idx, storage_service, storage_config
518
+ ) -> Source:
519
+ assert type(name) is str
520
+ assert type(stream_idx) is int
521
+ assert type(storage_service) is str
522
+ assert type(storage_config) is dict
523
+ for k, v in storage_config.items():
524
+ assert type(k) is str
525
+ assert type(v) is str
526
+ req = {
527
+ "name": name,
528
+ "stream_idx": stream_idx,
529
+ "storage_service": storage_service,
530
+ "storage_config": storage_config,
531
+ }
532
+ response = self._session.post(
533
+ f"{self._endpoint}/v2/source",
534
+ json=req,
535
+ headers={"Authorization": f"Bearer {self._api_key}"},
536
+ )
537
+ if not response.ok:
538
+ raise Exception(response.text)
539
+ response = response.json()
540
+ assert response["status"] == "ok"
541
+ id = response["id"]
542
+ return self.get_source(id)
543
+
544
+ def source(self, name, stream_idx, storage_service, storage_config) -> Source:
545
+ """Convenience function for accessing sources.
546
+
547
+ Tries to find a source with the given name, stream_idx, storage_service, and storage_config.
548
+ If no source is found, creates a new source with the given parameters.
549
+ """
550
+
551
+ sources = self.search_source(name, stream_idx, storage_service, storage_config)
552
+ if len(sources) == 0:
553
+ return self.create_source(name, stream_idx, storage_service, storage_config)
554
+ return self.get_source(sources[0])
555
+
556
+ def get_spec(self, id: str) -> Spec:
557
+ assert type(id) is str
558
+ response = self._session.get(
559
+ f"{self._endpoint}/v2/spec/{id}",
560
+ headers={"Authorization": f"Bearer {self._api_key}"},
561
+ )
562
+ if not response.ok:
563
+ raise Exception(response.text)
564
+ response = response.json()
565
+ return Spec(response["id"], response)
566
+
567
+ def list_specs(self) -> list[str]:
568
+ response = self._session.get(
569
+ f"{self._endpoint}/v2/spec",
570
+ headers={"Authorization": f"Bearer {self._api_key}"},
571
+ )
572
+ if not response.ok:
573
+ raise Exception(response.text)
574
+ response = response.json()
575
+ return response
576
+
577
+ def create_spec(
578
+ self,
579
+ width,
580
+ height,
581
+ pix_fmt,
582
+ vod_segment_length,
583
+ frame_rate,
584
+ ready_hook=None,
585
+ steer_hook=None,
586
+ ttl=None,
587
+ ) -> Spec:
588
+ assert type(width) is int
589
+ assert type(height) is int
590
+ assert type(pix_fmt) is str
591
+ assert type(vod_segment_length) is Fraction
592
+ assert type(frame_rate) is Fraction
593
+ assert type(ready_hook) is str or ready_hook is None
594
+ assert type(steer_hook) is str or steer_hook is None
595
+ assert ttl is None or type(ttl) is int
596
+
597
+ req = {
598
+ "width": width,
599
+ "height": height,
600
+ "pix_fmt": pix_fmt,
601
+ "vod_segment_length": [
602
+ vod_segment_length.numerator,
603
+ vod_segment_length.denominator,
604
+ ],
605
+ "frame_rate": [frame_rate.numerator, frame_rate.denominator],
606
+ "ready_hook": ready_hook,
607
+ "steer_hook": steer_hook,
608
+ "ttl": ttl,
609
+ }
610
+ response = self._session.post(
611
+ f"{self._endpoint}/v2/spec",
612
+ json=req,
613
+ headers={"Authorization": f"Bearer {self._api_key}"},
614
+ )
615
+ if not response.ok:
616
+ raise Exception(response.text)
617
+ response = response.json()
618
+ assert response["status"] == "ok"
619
+ return self.get_spec(response["id"])
620
+
621
+ def delete_spec(self, id: str):
622
+ assert type(id) is str
623
+ response = self._session.delete(
624
+ f"{self._endpoint}/v2/spec/{id}",
625
+ headers={"Authorization": f"Bearer {self._api_key}"},
626
+ )
627
+ if not response.ok:
628
+ raise Exception(response.text)
629
+ response = response.json()
630
+ assert response["status"] == "ok"
631
+
632
+ def export_spec(
633
+ self, id: str, path: str, encoder=None, encoder_opts=None, format=None
634
+ ):
635
+ assert type(id) is str
636
+ assert type(path) is str
637
+ req = {
638
+ "path": path,
639
+ "encoder": encoder,
640
+ "encoder_opts": encoder_opts,
641
+ "format": format,
642
+ }
643
+ response = self._session.post(
644
+ f"{self._endpoint}/v2/spec/{id}/export",
645
+ json=req,
646
+ headers={"Authorization": f"Bearer {self._api_key}"},
647
+ )
648
+ if not response.ok:
649
+ raise Exception(response.text)
650
+ response = response.json()
651
+ assert response["status"] == "ok"
652
+
653
+ def push_spec_part(self, spec_id, pos, frames, terminal):
654
+ if type(spec_id) is Spec:
655
+ spec_id = spec_id._id
656
+ assert type(spec_id) is str
657
+ assert type(pos) is int
658
+ assert type(frames) is list
659
+ assert type(terminal) is bool
660
+
661
+ req_frames = []
662
+ for frame in frames:
663
+ assert type(frame) is tuple
664
+ assert len(frame) == 2
665
+ t = frame[0]
666
+ f = frame[1]
667
+ assert type(t) is Fraction
668
+ assert f is None or type(f) is SourceExpr or type(f) is FilterExpr
669
+ req_frames.append(
670
+ [
671
+ [t.numerator, t.denominator],
672
+ f._to_json_spec() if f is not None else None,
673
+ ]
674
+ )
675
+
676
+ req = {
677
+ "pos": pos,
678
+ "frames": req_frames,
679
+ "terminal": terminal,
680
+ }
681
+ response = self._session.post(
682
+ f"{self._endpoint}/v2/spec/{spec_id}/part",
683
+ json=req,
684
+ headers={"Authorization": f"Bearer {self._api_key}"},
685
+ )
686
+ if not response.ok:
687
+ raise Exception(response.text)
688
+ response = response.json()
689
+ assert response["status"] == "ok"
690
+
691
+ def push_spec_part_block(
692
+ self, spec_id: str, pos, blocks, terminal, compression="gzip"
693
+ ):
694
+ if type(spec_id) is Spec:
695
+ spec_id = spec_id._id
696
+ assert type(spec_id) is str
697
+ assert type(pos) is int
698
+ assert type(blocks) is list
699
+ assert type(terminal) is bool
700
+ assert compression is None or compression == "gzip"
701
+
702
+ req_blocks = []
703
+ for block in blocks:
704
+ assert type(block) is _FrameExpressionBlock
705
+ block_body = block.as_dict()
706
+ block_frames = len(block_body["frame_exprs"])
707
+ block_body = json.dumps(block_body).encode("utf-8")
708
+ if compression == "gzip":
709
+ block_body = gzip.compress(block_body, 1)
710
+ block_body = base64.b64encode(block_body).decode("utf-8")
711
+ req_blocks.append(
712
+ {
713
+ "frames": block_frames,
714
+ "compression": compression,
715
+ "body": block_body,
716
+ }
717
+ )
718
+
719
+ req = {
720
+ "pos": pos,
721
+ "terminal": terminal,
722
+ "blocks": req_blocks,
723
+ }
724
+ response = self._session.post(
725
+ f"{self._endpoint}/v2/spec/{spec_id}/part_block",
726
+ json=req,
727
+ headers={"Authorization": f"Bearer {self._api_key}"},
728
+ )
729
+ if not response.ok:
730
+ raise Exception(response.text)
731
+ response = response.json()
732
+ assert response["status"] == "ok"
733
+
734
+ def frame(self, width, height, pix_fmt, frame_expr, compression="gzip"):
735
+ assert type(frame_expr) is FilterExpr or type(frame_expr) is SourceExpr
736
+ assert compression is None or compression in ["gzip"]
737
+ feb = _FrameExpressionBlock()
738
+ feb.insert_frame(frame_expr)
739
+ feb_body = feb.as_dict()
740
+
741
+ feb_body = json.dumps(feb_body).encode("utf-8")
742
+ if compression == "gzip":
743
+ feb_body = gzip.compress(feb_body, 1)
744
+ feb_body = base64.b64encode(feb_body).decode("utf-8")
745
+ req = {
746
+ "width": width,
747
+ "height": height,
748
+ "pix_fmt": pix_fmt,
749
+ "compression": compression,
750
+ "block": {
751
+ "frames": 1,
752
+ "compression": compression,
753
+ "body": feb_body,
754
+ },
755
+ }
756
+ response = self._session.post(
757
+ f"{self._endpoint}/v2/frame",
758
+ json=req,
759
+ headers={"Authorization": f"Bearer {self._api_key}"},
760
+ )
761
+ if not response.ok:
762
+ raise Exception(response.text)
763
+ response_body = response.content
764
+ assert type(response_body) is bytes
765
+ if compression == "gzip":
766
+ response_body = gzip.decompress(response_body)
767
+ return response_body
768
+
769
+
770
+ class SourceExpr:
771
+ def __init__(self, source, idx, is_iloc):
772
+ self._source = source
773
+ self._idx = idx
774
+ self._is_iloc = is_iloc
775
+
776
+ def __repr__(self):
777
+ if self._is_iloc:
778
+ return f"{self._source._name}.iloc[{self._idx}]"
779
+ else:
780
+ return f"{self._source._name}[{self._idx}]"
781
+
782
+ def _to_json_spec(self):
783
+ if self._is_iloc:
784
+ return {
785
+ "Source": {
786
+ "video": self._source._name,
787
+ "index": {"ILoc": int(self._idx)},
788
+ }
789
+ }
790
+ else:
791
+ return {
792
+ "Source": {
793
+ "video": self._source._name,
794
+ "index": {"T": [self._idx.numerator, self._idx.denominator]},
795
+ }
796
+ }
797
+
798
+ def _sources(self):
799
+ return set([self._source])
800
+
801
+ def _filters(self):
802
+ return {}
803
+
804
+
805
+ class _SourceILoc:
806
+ def __init__(self, source):
807
+ self._source = source
808
+
809
+ def __getitem__(self, idx):
810
+ if type(idx) is not int:
811
+ raise Exception(f"Source iloc index must be an integer, got a {type(idx)}")
812
+ return SourceExpr(self._source, idx, True)
813
+
814
+
815
+ def _json_arg(arg, skip_data_anot=False):
816
+ if type(arg) is FilterExpr or type(arg) is SourceExpr:
817
+ return {"Frame": arg._to_json_spec()}
818
+ elif type(arg) is int:
819
+ if skip_data_anot:
820
+ return {"Int": arg}
821
+ return {"Data": {"Int": arg}}
822
+ elif type(arg) is str:
823
+ if skip_data_anot:
824
+ return {"String": arg}
825
+ return {"Data": {"String": arg}}
826
+ elif type(arg) is bytes:
827
+ arg = list(arg)
828
+ if skip_data_anot:
829
+ return {"Bytes": arg}
830
+ return {"Data": {"Bytes": arg}}
831
+ elif type(arg) is float:
832
+ if skip_data_anot:
833
+ return {"Float": arg}
834
+ return {"Data": {"Float": arg}}
835
+ elif type(arg) is bool:
836
+ if skip_data_anot:
837
+ return {"Bool": arg}
838
+ return {"Data": {"Bool": arg}}
839
+ elif type(arg) is tuple or type(arg) is list:
840
+ if skip_data_anot:
841
+ return {"List": [_json_arg(x, True) for x in list(arg)]}
842
+ return {"Data": {"List": [_json_arg(x, True) for x in list(arg)]}}
843
+ else:
844
+ raise Exception(f"Unknown arg type: {type(arg)}")
845
+
846
+
847
+ class Filter:
848
+ """A video filter."""
849
+
850
+ def __init__(self, func: str):
851
+ self._func = func
852
+
853
+ def __call__(self, *args, **kwargs):
854
+ return FilterExpr(self, args, kwargs)
855
+
856
+
857
+ class FilterExpr:
858
+ def __init__(self, filter: Filter, args, kwargs):
859
+ self._filter = filter
860
+ self._args = args
861
+ self._kwargs = kwargs
862
+
863
+ def __repr__(self):
864
+ args = []
865
+ for arg in self._args:
866
+ val = f'"{arg}"' if type(arg) is str else str(arg)
867
+ args.append(str(val))
868
+ for k, v in self._kwargs.items():
869
+ val = f'"{v}"' if type(v) is str else str(v)
870
+ args.append(f"{k}={val}")
871
+ return f"{self._filter._name}({', '.join(args)})"
872
+
873
+ def _to_json_spec(self):
874
+ args = []
875
+ for arg in self._args:
876
+ args.append(_json_arg(arg))
877
+ kwargs = {}
878
+ for k, v in self._kwargs.items():
879
+ kwargs[k] = _json_arg(v)
880
+ return {"Filter": {"name": self._filter._name, "args": args, "kwargs": kwargs}}
881
+
882
+ def _sources(self):
883
+ s = set()
884
+ for arg in self._args:
885
+ if type(arg) is FilterExpr or type(arg) is SourceExpr:
886
+ s = s.union(arg._sources())
887
+ for arg in self._kwargs.values():
888
+ if type(arg) is FilterExpr or type(arg) is SourceExpr:
889
+ s = s.union(arg._sources())
890
+ return s
891
+
892
+ def _filters(self):
893
+ f = {self._filter._name: self._filter}
894
+ for arg in self._args:
895
+ if type(arg) is FilterExpr:
896
+ f = {**f, **arg._filters()}
897
+ for arg in self._kwargs.values():
898
+ if type(arg) is FilterExpr:
899
+ f = {**f, **arg._filters()}
900
+ return f