typed-ffmpeg-compatible 2.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- typed_ffmpeg/__init__.py +25 -0
- typed_ffmpeg/base.py +114 -0
- typed_ffmpeg/common/__init__.py +0 -0
- typed_ffmpeg/common/schema.py +308 -0
- typed_ffmpeg/common/serialize.py +132 -0
- typed_ffmpeg/dag/__init__.py +13 -0
- typed_ffmpeg/dag/compile.py +51 -0
- typed_ffmpeg/dag/context.py +221 -0
- typed_ffmpeg/dag/factory.py +31 -0
- typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
- typed_ffmpeg/dag/global_runnable/global_args.py +178 -0
- typed_ffmpeg/dag/global_runnable/runnable.py +174 -0
- typed_ffmpeg/dag/io/__init__.py +0 -0
- typed_ffmpeg/dag/io/_input.py +197 -0
- typed_ffmpeg/dag/io/_output.py +318 -0
- typed_ffmpeg/dag/io/output_args.py +327 -0
- typed_ffmpeg/dag/nodes.py +479 -0
- typed_ffmpeg/dag/schema.py +210 -0
- typed_ffmpeg/dag/utils.py +41 -0
- typed_ffmpeg/dag/validate.py +172 -0
- typed_ffmpeg/exceptions.py +42 -0
- typed_ffmpeg/filters.py +3510 -0
- typed_ffmpeg/probe.py +43 -0
- typed_ffmpeg/py.typed +0 -0
- typed_ffmpeg/schema.py +29 -0
- typed_ffmpeg/streams/__init__.py +5 -0
- typed_ffmpeg/streams/audio.py +6955 -0
- typed_ffmpeg/streams/av.py +22 -0
- typed_ffmpeg/streams/channel_layout.py +39 -0
- typed_ffmpeg/streams/video.py +12974 -0
- typed_ffmpeg/types.py +119 -0
- typed_ffmpeg/utils/__init__.py +0 -0
- typed_ffmpeg/utils/escaping.py +49 -0
- typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
- typed_ffmpeg/utils/lazy_eval/operator.py +134 -0
- typed_ffmpeg/utils/lazy_eval/schema.py +211 -0
- typed_ffmpeg/utils/run.py +27 -0
- typed_ffmpeg/utils/snapshot.py +26 -0
- typed_ffmpeg/utils/typing.py +17 -0
- typed_ffmpeg/utils/view.py +64 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/LICENSE +21 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/METADATA +183 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/RECORD +45 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/WHEEL +4 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,479 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import os.path
|
5
|
+
from dataclasses import dataclass, replace
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
|
9
|
+
from ..exceptions import FFMpegTypeError, FFMpegValueError
|
10
|
+
from ..schema import Default, StreamType
|
11
|
+
from ..utils.escaping import escape
|
12
|
+
from ..utils.lazy_eval.schema import LazyValue
|
13
|
+
from ..utils.typing import override
|
14
|
+
from .global_runnable.runnable import GlobalRunable
|
15
|
+
from .io.output_args import OutputArgs
|
16
|
+
from .schema import Node, Stream
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from ..streams.audio import AudioStream
|
20
|
+
from ..streams.av import AVStream
|
21
|
+
from ..streams.video import VideoStream
|
22
|
+
from .context import DAGContext
|
23
|
+
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass(frozen=True, kw_only=True)
|
29
|
+
class FilterNode(Node):
|
30
|
+
"""
|
31
|
+
A filter node that can be used to apply filters to streams
|
32
|
+
"""
|
33
|
+
|
34
|
+
name: str
|
35
|
+
"""
|
36
|
+
The name of the filter
|
37
|
+
"""
|
38
|
+
|
39
|
+
inputs: tuple[FilterableStream, ...] = ()
|
40
|
+
"""
|
41
|
+
The input streams
|
42
|
+
"""
|
43
|
+
|
44
|
+
input_typings: tuple[StreamType, ...] = ()
|
45
|
+
"""
|
46
|
+
The input typings
|
47
|
+
"""
|
48
|
+
|
49
|
+
output_typings: tuple[StreamType, ...] = ()
|
50
|
+
"""
|
51
|
+
The output typings
|
52
|
+
"""
|
53
|
+
|
54
|
+
@override
|
55
|
+
def repr(self) -> str:
|
56
|
+
return self.name
|
57
|
+
|
58
|
+
def video(self, index: int) -> "VideoStream":
|
59
|
+
"""
|
60
|
+
Return the video stream at the specified index
|
61
|
+
|
62
|
+
Args:
|
63
|
+
index: the index of the video stream
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
the video stream at the specified index
|
67
|
+
"""
|
68
|
+
from ..streams.video import VideoStream
|
69
|
+
|
70
|
+
video_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.video]
|
71
|
+
if not len(video_outputs) > index:
|
72
|
+
raise FFMpegValueError(f"Specified index {index} is out of range for video outputs {len(video_outputs)}")
|
73
|
+
return VideoStream(node=self, index=video_outputs[index])
|
74
|
+
|
75
|
+
def audio(self, index: int) -> "AudioStream":
|
76
|
+
"""
|
77
|
+
Return the audio stream at the specified index
|
78
|
+
|
79
|
+
Args:
|
80
|
+
index: the index of the audio stream
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
the audio stream at the specified index
|
84
|
+
"""
|
85
|
+
from ..streams.audio import AudioStream
|
86
|
+
|
87
|
+
audio_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.audio]
|
88
|
+
if not len(audio_outputs) > index:
|
89
|
+
raise FFMpegValueError(f"Specified index {index} is out of range for audio outputs {len(audio_outputs)}")
|
90
|
+
|
91
|
+
return AudioStream(node=self, index=audio_outputs[index])
|
92
|
+
|
93
|
+
def __post_init__(self) -> None:
|
94
|
+
from ..streams.audio import AudioStream
|
95
|
+
from ..streams.video import VideoStream
|
96
|
+
|
97
|
+
super().__post_init__()
|
98
|
+
|
99
|
+
if len(self.inputs) != len(self.input_typings):
|
100
|
+
raise FFMpegValueError(f"Expected {len(self.input_typings)} inputs, got {len(self.inputs)}")
|
101
|
+
|
102
|
+
stream: FilterableStream
|
103
|
+
expected_type: StreamType
|
104
|
+
|
105
|
+
for i, (stream, expected_type) in enumerate(zip(self.inputs, self.input_typings)):
|
106
|
+
if expected_type == StreamType.video:
|
107
|
+
if not isinstance(stream, VideoStream):
|
108
|
+
raise FFMpegTypeError(
|
109
|
+
f"Expected input {i} to have video component, got {stream.__class__.__name__}"
|
110
|
+
)
|
111
|
+
if expected_type == StreamType.audio:
|
112
|
+
if not isinstance(stream, AudioStream):
|
113
|
+
raise FFMpegTypeError(
|
114
|
+
f"Expected input {i} to have audio component, got {stream.__class__.__name__}"
|
115
|
+
)
|
116
|
+
|
117
|
+
@override
|
118
|
+
def get_args(self, context: DAGContext = None) -> list[str]:
|
119
|
+
from .context import DAGContext
|
120
|
+
|
121
|
+
if not context:
|
122
|
+
context = DAGContext.build(self)
|
123
|
+
|
124
|
+
incoming_labels = "".join(f"[{k.label(context)}]" for k in self.inputs)
|
125
|
+
outputs = context.get_outgoing_streams(self)
|
126
|
+
|
127
|
+
outgoing_labels = ""
|
128
|
+
for output in sorted(outputs, key=lambda stream: stream.index or 0):
|
129
|
+
# NOTE: all outgoing streams must be filterable
|
130
|
+
assert isinstance(output, FilterableStream)
|
131
|
+
outgoing_labels += f"[{output.label(context)}]"
|
132
|
+
|
133
|
+
commands = []
|
134
|
+
for key, value in self.kwargs:
|
135
|
+
assert not isinstance(value, LazyValue), f"LazyValue should have been evaluated: {key}={value}"
|
136
|
+
|
137
|
+
# Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
|
138
|
+
if isinstance(value, bool):
|
139
|
+
value = str(int(value))
|
140
|
+
|
141
|
+
if not isinstance(value, Default):
|
142
|
+
commands += [f"{key}={escape(value)}"]
|
143
|
+
|
144
|
+
if commands:
|
145
|
+
return [incoming_labels] + [f"{self.name}="] + [escape(":".join(commands), "\\'[],;")] + [outgoing_labels]
|
146
|
+
return [incoming_labels] + [f"{self.name}"] + [outgoing_labels]
|
147
|
+
|
148
|
+
|
149
|
+
@dataclass(frozen=True, kw_only=True)
|
150
|
+
class FilterableStream(Stream, OutputArgs):
|
151
|
+
"""
|
152
|
+
A stream that can be used as input to a filter
|
153
|
+
"""
|
154
|
+
|
155
|
+
node: "FilterNode | InputNode"
|
156
|
+
|
157
|
+
@override
|
158
|
+
def _output_node(self, *streams: FilterableStream, filename: str | Path, **kwargs: Any) -> OutputNode:
|
159
|
+
"""
|
160
|
+
Output the streams to a file URL
|
161
|
+
|
162
|
+
Args:
|
163
|
+
*streams: the other streams to output
|
164
|
+
filename: the filename to output to
|
165
|
+
**kwargs: the arguments for the output
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
the output stream
|
169
|
+
"""
|
170
|
+
return OutputNode(inputs=(self, *streams), filename=str(filename), kwargs=tuple(kwargs.items()))
|
171
|
+
|
172
|
+
def vfilter(
|
173
|
+
self,
|
174
|
+
*streams: "FilterableStream",
|
175
|
+
name: str,
|
176
|
+
input_typings: tuple[StreamType, ...] = (StreamType.video,),
|
177
|
+
**kwargs: Any,
|
178
|
+
) -> "VideoStream":
|
179
|
+
"""
|
180
|
+
Apply a custom video filter which has only one output to this stream
|
181
|
+
|
182
|
+
Args:
|
183
|
+
*streams (FilterableStream): the streams to apply the filter to
|
184
|
+
name: the name of the filter
|
185
|
+
input_typings: the input typings
|
186
|
+
**kwargs: the arguments for the filter
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
the output stream
|
190
|
+
"""
|
191
|
+
return self.filter_multi_output(
|
192
|
+
*streams,
|
193
|
+
name=name,
|
194
|
+
input_typings=input_typings,
|
195
|
+
output_typings=(StreamType.video,),
|
196
|
+
**kwargs,
|
197
|
+
).video(0)
|
198
|
+
|
199
|
+
def afilter(
|
200
|
+
self,
|
201
|
+
*streams: "FilterableStream",
|
202
|
+
name: str,
|
203
|
+
input_typings: tuple[StreamType, ...] = (StreamType.audio,),
|
204
|
+
**kwargs: Any,
|
205
|
+
) -> "AudioStream":
|
206
|
+
"""
|
207
|
+
Apply a custom audio filter which has only one output to this stream
|
208
|
+
|
209
|
+
Args:
|
210
|
+
*streams (FilterableStream): the streams to apply the filter to
|
211
|
+
name: the name of the filter
|
212
|
+
input_typings: the input typings
|
213
|
+
**kwargs: the arguments for the filter
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
the output stream
|
217
|
+
"""
|
218
|
+
return self.filter_multi_output(
|
219
|
+
*streams,
|
220
|
+
name=name,
|
221
|
+
input_typings=input_typings,
|
222
|
+
output_typings=(StreamType.audio,),
|
223
|
+
**kwargs,
|
224
|
+
).audio(0)
|
225
|
+
|
226
|
+
def filter_multi_output(
|
227
|
+
self,
|
228
|
+
*streams: "FilterableStream",
|
229
|
+
name: str,
|
230
|
+
input_typings: tuple[StreamType, ...] = (),
|
231
|
+
output_typings: tuple[StreamType, ...] = (),
|
232
|
+
**kwargs: Any,
|
233
|
+
) -> "FilterNode":
|
234
|
+
"""
|
235
|
+
Apply a custom filter which has multiple outputs to this stream
|
236
|
+
|
237
|
+
Args:
|
238
|
+
*streams (FilterableStream): the streams to apply the filter to
|
239
|
+
name: the name of the filter
|
240
|
+
input_typings: the input typings
|
241
|
+
output_typings: the output typings
|
242
|
+
**kwargs: the arguments for the filter
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
the FilterNode
|
246
|
+
"""
|
247
|
+
return FilterNode(
|
248
|
+
name=name,
|
249
|
+
kwargs=tuple(kwargs.items()),
|
250
|
+
inputs=(self, *streams),
|
251
|
+
input_typings=input_typings,
|
252
|
+
output_typings=output_typings,
|
253
|
+
)
|
254
|
+
|
255
|
+
def label(self, context: DAGContext = None) -> str:
|
256
|
+
"""
|
257
|
+
Return the label for this stream
|
258
|
+
|
259
|
+
Args:
|
260
|
+
context: the DAG context
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
the label for this stream
|
264
|
+
"""
|
265
|
+
from ..streams.audio import AudioStream
|
266
|
+
from ..streams.av import AVStream
|
267
|
+
from ..streams.video import VideoStream
|
268
|
+
from .context import DAGContext
|
269
|
+
|
270
|
+
if not context:
|
271
|
+
context = DAGContext.build(self.node)
|
272
|
+
|
273
|
+
if isinstance(self.node, InputNode):
|
274
|
+
if isinstance(self, AVStream):
|
275
|
+
return f"{context.get_node_label(self.node)}"
|
276
|
+
elif isinstance(self, VideoStream):
|
277
|
+
return f"{context.get_node_label(self.node)}:v"
|
278
|
+
elif isinstance(self, AudioStream):
|
279
|
+
return f"{context.get_node_label(self.node)}:a"
|
280
|
+
raise FFMpegValueError(f"Unknown stream type: {self.__class__.__name__}") # pragma: no cover
|
281
|
+
|
282
|
+
if isinstance(self.node, FilterNode):
|
283
|
+
if len(self.node.output_typings) > 1:
|
284
|
+
return f"{context.get_node_label(self.node)}#{self.index}"
|
285
|
+
return f"{context.get_node_label(self.node)}"
|
286
|
+
raise FFMpegValueError(f"Unknown node type: {self.node.__class__.__name__}") # pragma: no cover
|
287
|
+
|
288
|
+
def __post_init__(self) -> None:
|
289
|
+
if isinstance(self.node, InputNode):
|
290
|
+
assert self.index is None, "Input streams cannot have an index"
|
291
|
+
else:
|
292
|
+
assert self.index is not None, "Filter streams must have an index"
|
293
|
+
|
294
|
+
|
295
|
+
@dataclass(frozen=True, kw_only=True)
|
296
|
+
class InputNode(Node):
|
297
|
+
"""
|
298
|
+
A node that can be used to read from files
|
299
|
+
"""
|
300
|
+
|
301
|
+
filename: str
|
302
|
+
"""
|
303
|
+
The filename to read from
|
304
|
+
"""
|
305
|
+
|
306
|
+
inputs: tuple[()] = ()
|
307
|
+
|
308
|
+
@override
|
309
|
+
def repr(self) -> str:
|
310
|
+
return os.path.basename(self.filename)
|
311
|
+
|
312
|
+
@property
|
313
|
+
def video(self) -> "VideoStream":
|
314
|
+
"""
|
315
|
+
Return the video stream of this node
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
the video stream
|
319
|
+
"""
|
320
|
+
from ..streams.video import VideoStream
|
321
|
+
|
322
|
+
return VideoStream(node=self)
|
323
|
+
|
324
|
+
@property
|
325
|
+
def audio(self) -> "AudioStream":
|
326
|
+
"""
|
327
|
+
Return the audio stream of this node
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
the audio stream
|
331
|
+
"""
|
332
|
+
from ..streams.audio import AudioStream
|
333
|
+
|
334
|
+
return AudioStream(node=self)
|
335
|
+
|
336
|
+
def stream(self) -> "AVStream":
|
337
|
+
"""
|
338
|
+
Return the output stream of this node
|
339
|
+
|
340
|
+
Returns:
|
341
|
+
the output stream
|
342
|
+
"""
|
343
|
+
from ..streams.av import AVStream
|
344
|
+
|
345
|
+
return AVStream(node=self)
|
346
|
+
|
347
|
+
@override
|
348
|
+
def get_args(self, context: DAGContext = None) -> list[str]:
|
349
|
+
commands = []
|
350
|
+
for key, value in self.kwargs:
|
351
|
+
if isinstance(value, bool):
|
352
|
+
if value is True:
|
353
|
+
commands += [f"-{key}"]
|
354
|
+
# NOTE: the -nooption is not supported
|
355
|
+
else:
|
356
|
+
commands += [f"-{key}", str(value)]
|
357
|
+
commands += ["-i", self.filename]
|
358
|
+
return commands
|
359
|
+
|
360
|
+
|
361
|
+
@dataclass(frozen=True, kw_only=True)
|
362
|
+
class OutputNode(Node):
|
363
|
+
filename: str
|
364
|
+
"""
|
365
|
+
The filename to output to
|
366
|
+
"""
|
367
|
+
inputs: tuple[FilterableStream, ...]
|
368
|
+
|
369
|
+
@override
|
370
|
+
def repr(self) -> str:
|
371
|
+
return os.path.basename(self.filename)
|
372
|
+
|
373
|
+
def stream(self) -> "OutputStream":
|
374
|
+
"""
|
375
|
+
Return the output stream of this node
|
376
|
+
|
377
|
+
Returns:
|
378
|
+
the output stream
|
379
|
+
"""
|
380
|
+
return OutputStream(node=self)
|
381
|
+
|
382
|
+
@override
|
383
|
+
def get_args(self, context: DAGContext = None) -> list[str]:
|
384
|
+
# !handle mapping
|
385
|
+
commands = []
|
386
|
+
|
387
|
+
if context and (
|
388
|
+
any(isinstance(k.node, FilterNode) for k in self.inputs)
|
389
|
+
or len([k for k in context.all_nodes if isinstance(k, OutputNode)]) > 1
|
390
|
+
):
|
391
|
+
for input in self.inputs:
|
392
|
+
if isinstance(input.node, InputNode):
|
393
|
+
commands += ["-map", input.label(context)]
|
394
|
+
else:
|
395
|
+
commands += ["-map", f"[{input.label(context)}]"]
|
396
|
+
|
397
|
+
for key, value in self.kwargs:
|
398
|
+
if isinstance(value, bool):
|
399
|
+
if value is True:
|
400
|
+
commands += [f"-{key}"]
|
401
|
+
# NOTE: the -nooption is not supported
|
402
|
+
else:
|
403
|
+
commands += [f"-{key}", str(value)]
|
404
|
+
commands += [self.filename]
|
405
|
+
return commands
|
406
|
+
|
407
|
+
|
408
|
+
@dataclass(frozen=True, kw_only=True)
|
409
|
+
class OutputStream(Stream, GlobalRunable):
|
410
|
+
node: OutputNode
|
411
|
+
|
412
|
+
@override
|
413
|
+
def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
|
414
|
+
"""
|
415
|
+
Add extra global command-line argument
|
416
|
+
|
417
|
+
Args:
|
418
|
+
**kwargs: the extra arguments
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
the output stream
|
422
|
+
"""
|
423
|
+
return GlobalNode(inputs=(self, *streams), kwargs=tuple(kwargs.items()))
|
424
|
+
|
425
|
+
|
426
|
+
@dataclass(frozen=True, kw_only=True)
|
427
|
+
class GlobalNode(Node):
|
428
|
+
"""
|
429
|
+
A node that can be used to set global options
|
430
|
+
"""
|
431
|
+
|
432
|
+
inputs: tuple[OutputStream, ...]
|
433
|
+
|
434
|
+
@override
|
435
|
+
def repr(self) -> str:
|
436
|
+
return " ".join(self.get_args())
|
437
|
+
|
438
|
+
def stream(self) -> "GlobalStream":
|
439
|
+
"""
|
440
|
+
Return the output stream of this node
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
the output stream
|
444
|
+
"""
|
445
|
+
return GlobalStream(node=self)
|
446
|
+
|
447
|
+
@override
|
448
|
+
def get_args(self, context: DAGContext = None) -> list[str]:
|
449
|
+
commands = []
|
450
|
+
for key, value in self.kwargs:
|
451
|
+
if isinstance(value, bool):
|
452
|
+
if value is True:
|
453
|
+
commands += [f"-{key}"]
|
454
|
+
# NOTE: the -no{key} format since not really for global options
|
455
|
+
else:
|
456
|
+
commands += [f"-{key}", str(value)]
|
457
|
+
return commands
|
458
|
+
|
459
|
+
|
460
|
+
@dataclass(frozen=True, kw_only=True)
|
461
|
+
class GlobalStream(Stream, GlobalRunable):
|
462
|
+
node: GlobalNode
|
463
|
+
|
464
|
+
@override
|
465
|
+
def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
|
466
|
+
"""
|
467
|
+
Add extra global command-line argument
|
468
|
+
|
469
|
+
Args:
|
470
|
+
**kwargs: the extra arguments
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
the output stream
|
474
|
+
"""
|
475
|
+
inputs = (*self.node.inputs, *streams)
|
476
|
+
kwargs = dict(self.node.kwargs) | kwargs
|
477
|
+
|
478
|
+
new_node = replace(self.node, inputs=inputs, kwargs=tuple(kwargs.items()))
|
479
|
+
return new_node
|
@@ -0,0 +1,210 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from dataclasses import dataclass, replace
|
5
|
+
from functools import cached_property
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
7
|
+
|
8
|
+
from ..utils.lazy_eval.schema import LazyValue
|
9
|
+
from .utils import is_dag
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from .context import DAGContext
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass(frozen=True, kw_only=True)
|
16
|
+
class HashableBaseModel:
|
17
|
+
"""
|
18
|
+
A base class for hashable dataclasses.
|
19
|
+
"""
|
20
|
+
|
21
|
+
@cached_property
|
22
|
+
def hex(self) -> str:
|
23
|
+
"""
|
24
|
+
Get the hexadecimal hash of the object.
|
25
|
+
"""
|
26
|
+
return hex(abs(hash(self)))[2:]
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass(frozen=True, kw_only=True)
|
30
|
+
class Stream(HashableBaseModel):
|
31
|
+
"""
|
32
|
+
A 'Stream' represents a sequence of data flow in the Directed Acyclic Graph (DAG).
|
33
|
+
|
34
|
+
Note:
|
35
|
+
Each stream in the DAG is a sequence of operations that transforms the data from its input form to its output form. The stream is an essential component of the DAG, as it defines the order and the nature of the operations that are performed on the data.
|
36
|
+
"""
|
37
|
+
|
38
|
+
node: Node
|
39
|
+
"""
|
40
|
+
Represents the node that the stream is connected to in the upstream direction.
|
41
|
+
|
42
|
+
Note:
|
43
|
+
In the context of a data stream, the 'upstream' refers to the source of the data, or where the data is coming from. Therefore, the 'upstream node' is the node that is providing the data to the current stream.
|
44
|
+
"""
|
45
|
+
|
46
|
+
index: int | None = None # the nth child of the node
|
47
|
+
"""
|
48
|
+
Represents the index of the stream in the node's output streams.
|
49
|
+
|
50
|
+
Note:
|
51
|
+
See Also: [Stream specifiers](https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1) `stream_index`
|
52
|
+
"""
|
53
|
+
|
54
|
+
def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
|
55
|
+
"""
|
56
|
+
Visualize the stream.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
format: The format of the view.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
The file path of the visualization.
|
63
|
+
"""
|
64
|
+
from ..utils.view import view
|
65
|
+
|
66
|
+
return view(self.node, format=format)
|
67
|
+
|
68
|
+
def _repr_png_(self) -> bytes: # pragma: no cover
|
69
|
+
with open(self.view(format="png"), "rb") as f:
|
70
|
+
return f.read()
|
71
|
+
|
72
|
+
def _repr_svg_(self) -> str: # pragma: no cover
|
73
|
+
with open(self.view(format="svg"), "r") as f:
|
74
|
+
return f.read()
|
75
|
+
|
76
|
+
|
77
|
+
@dataclass(frozen=True, kw_only=True)
|
78
|
+
class Node(HashableBaseModel, ABC):
|
79
|
+
"""
|
80
|
+
A 'Node' represents a single operation in the Directed Acyclic Graph (DAG).
|
81
|
+
|
82
|
+
Note:
|
83
|
+
Each node in the DAG represents a single operation that transforms the data from its input form to its output form. The node is an essential component of the DAG, as it defines the nature of the operations that are performed on the data.
|
84
|
+
"""
|
85
|
+
|
86
|
+
# Filter_Node_Option_Type
|
87
|
+
kwargs: tuple[tuple[str, str | int | float | bool | LazyValue], ...] = ()
|
88
|
+
"""
|
89
|
+
Represents the keyword arguments of the node.
|
90
|
+
"""
|
91
|
+
|
92
|
+
inputs: tuple[Stream, ...] = ()
|
93
|
+
"""
|
94
|
+
Represents the input streams of the node.
|
95
|
+
"""
|
96
|
+
|
97
|
+
def __post_init__(self) -> None:
|
98
|
+
# Validate the DAG
|
99
|
+
passed = set()
|
100
|
+
nodes = [self]
|
101
|
+
output = {}
|
102
|
+
|
103
|
+
while nodes:
|
104
|
+
node = nodes.pop()
|
105
|
+
|
106
|
+
if node in passed:
|
107
|
+
continue
|
108
|
+
passed.add(node)
|
109
|
+
|
110
|
+
nodes.extend(k.node for k in node.inputs)
|
111
|
+
|
112
|
+
output[node.hex] = set(k.node.hex for k in node.inputs)
|
113
|
+
|
114
|
+
if not is_dag(output):
|
115
|
+
raise ValueError(f"Graph is not a DAG: {output}") # pragma: no cover
|
116
|
+
|
117
|
+
@abstractmethod
|
118
|
+
def get_args(self, context: DAGContext = None) -> list[str]:
|
119
|
+
"""
|
120
|
+
Get the arguments of the node.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
context: The DAG context.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
The arguments of the node.
|
127
|
+
"""
|
128
|
+
|
129
|
+
def repr(self) -> str:
|
130
|
+
"""
|
131
|
+
Get the representation of the node.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
The representation of the node.
|
135
|
+
"""
|
136
|
+
return repr(self)
|
137
|
+
|
138
|
+
def replace(self, old_node: Node, new_node: Node) -> Node:
|
139
|
+
"""
|
140
|
+
Replace the old node in the graph with the new node.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
old_node: The old node to replace.
|
144
|
+
new_node: The new node to replace with.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
The new graph with the replaced node.
|
148
|
+
"""
|
149
|
+
if self == old_node:
|
150
|
+
return new_node
|
151
|
+
|
152
|
+
inputs = []
|
153
|
+
|
154
|
+
for stream in self.inputs:
|
155
|
+
new_stream_node = stream.node.replace(old_node, new_node)
|
156
|
+
|
157
|
+
if new_stream_node != stream.node:
|
158
|
+
# need to create a new stream
|
159
|
+
new_stream = replace(stream, node=new_stream_node)
|
160
|
+
inputs.append(new_stream)
|
161
|
+
else:
|
162
|
+
inputs.append(stream)
|
163
|
+
|
164
|
+
return replace(self, inputs=tuple(inputs))
|
165
|
+
|
166
|
+
@property
|
167
|
+
def max_depth(self) -> int:
|
168
|
+
"""
|
169
|
+
Get the maximum depth of the node.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
The maximum depth of the node.
|
173
|
+
"""
|
174
|
+
return max((i.node.max_depth for i in self.inputs), default=0) + 1
|
175
|
+
|
176
|
+
@property
|
177
|
+
def upstream_nodes(self) -> set[Node]:
|
178
|
+
"""
|
179
|
+
Get all upstream nodes of the node.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
The upstream nodes of the node.
|
183
|
+
"""
|
184
|
+
output = {self}
|
185
|
+
for input in self.inputs:
|
186
|
+
output |= input.node.upstream_nodes
|
187
|
+
|
188
|
+
return output
|
189
|
+
|
190
|
+
def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
|
191
|
+
"""
|
192
|
+
Visualize the Node.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
format: The format of the view.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
The file path of the visualization.
|
199
|
+
"""
|
200
|
+
from ..utils.view import view
|
201
|
+
|
202
|
+
return view(self, format=format)
|
203
|
+
|
204
|
+
def _repr_png_(self) -> bytes: # pragma: no cover
|
205
|
+
with open(self.view(format="png"), "rb") as f:
|
206
|
+
return f.read()
|
207
|
+
|
208
|
+
def _repr_svg_(self) -> str: # pragma: no cover
|
209
|
+
with open(self.view(format="svg"), "r") as f:
|
210
|
+
return f.read()
|