typed-ffmpeg-compatible 2.4.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. typed_ffmpeg/__init__.py +25 -0
  2. typed_ffmpeg/base.py +114 -0
  3. typed_ffmpeg/common/__init__.py +0 -0
  4. typed_ffmpeg/common/schema.py +308 -0
  5. typed_ffmpeg/common/serialize.py +132 -0
  6. typed_ffmpeg/dag/__init__.py +13 -0
  7. typed_ffmpeg/dag/compile.py +51 -0
  8. typed_ffmpeg/dag/context.py +221 -0
  9. typed_ffmpeg/dag/factory.py +31 -0
  10. typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
  11. typed_ffmpeg/dag/global_runnable/global_args.py +178 -0
  12. typed_ffmpeg/dag/global_runnable/runnable.py +174 -0
  13. typed_ffmpeg/dag/io/__init__.py +0 -0
  14. typed_ffmpeg/dag/io/_input.py +197 -0
  15. typed_ffmpeg/dag/io/_output.py +320 -0
  16. typed_ffmpeg/dag/io/output_args.py +327 -0
  17. typed_ffmpeg/dag/nodes.py +479 -0
  18. typed_ffmpeg/dag/schema.py +210 -0
  19. typed_ffmpeg/dag/utils.py +41 -0
  20. typed_ffmpeg/dag/validate.py +172 -0
  21. typed_ffmpeg/exceptions.py +42 -0
  22. typed_ffmpeg/filters.py +3572 -0
  23. typed_ffmpeg/probe.py +43 -0
  24. typed_ffmpeg/py.typed +0 -0
  25. typed_ffmpeg/schema.py +29 -0
  26. typed_ffmpeg/streams/__init__.py +5 -0
  27. typed_ffmpeg/streams/audio.py +7358 -0
  28. typed_ffmpeg/streams/av.py +22 -0
  29. typed_ffmpeg/streams/channel_layout.py +39 -0
  30. typed_ffmpeg/streams/video.py +13469 -0
  31. typed_ffmpeg/types.py +119 -0
  32. typed_ffmpeg/utils/__init__.py +0 -0
  33. typed_ffmpeg/utils/escaping.py +49 -0
  34. typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
  35. typed_ffmpeg/utils/lazy_eval/operator.py +134 -0
  36. typed_ffmpeg/utils/lazy_eval/schema.py +211 -0
  37. typed_ffmpeg/utils/run.py +27 -0
  38. typed_ffmpeg/utils/snapshot.py +26 -0
  39. typed_ffmpeg/utils/typing.py +17 -0
  40. typed_ffmpeg/utils/view.py +64 -0
  41. typed_ffmpeg_compatible-2.4.1.dist-info/LICENSE +21 -0
  42. typed_ffmpeg_compatible-2.4.1.dist-info/METADATA +182 -0
  43. typed_ffmpeg_compatible-2.4.1.dist-info/RECORD +45 -0
  44. typed_ffmpeg_compatible-2.4.1.dist-info/WHEEL +4 -0
  45. typed_ffmpeg_compatible-2.4.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,25 @@
1
+ from . import dag, filters
2
+ from .base import afilter, filter_multi_output, input, merge_outputs, output, vfilter
3
+ from .dag import Stream
4
+ from .exceptions import FFMpegExecuteError, FFMpegTypeError, FFMpegValueError
5
+ from .probe import probe
6
+ from .streams import AudioStream, AVStream, VideoStream
7
+
8
+ __all__ = [
9
+ "filters",
10
+ "input",
11
+ "output",
12
+ "merge_outputs",
13
+ "FFMpegExecuteError",
14
+ "FFMpegTypeError",
15
+ "FFMpegValueError",
16
+ "Stream",
17
+ "probe",
18
+ "AudioStream",
19
+ "VideoStream",
20
+ "AVStream",
21
+ "vfilter",
22
+ "afilter",
23
+ "filter_multi_output",
24
+ "dag",
25
+ ]
typed_ffmpeg/base.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ This module defined the basic functions for creating the ffmpeg filter graph.
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from .dag.io._input import input
8
+ from .dag.io._output import output
9
+ from .dag.nodes import FilterableStream, FilterNode, GlobalNode, GlobalStream, OutputStream
10
+ from .schema import StreamType
11
+ from .streams.audio import AudioStream
12
+ from .streams.video import VideoStream
13
+
14
+
15
+ def merge_outputs(*streams: OutputStream) -> GlobalStream:
16
+ """
17
+ Merge multiple output streams into one.
18
+
19
+ Args:
20
+ *streams: The output streams to merge.
21
+
22
+ Returns:
23
+ The merged output stream.
24
+ """
25
+ return GlobalNode(inputs=streams).stream()
26
+
27
+
28
+ def vfilter(
29
+ *streams: FilterableStream, name: str, input_typings: tuple[StreamType, ...] = (StreamType.video,), **kwargs: Any
30
+ ) -> VideoStream:
31
+ """
32
+ Apply a custom video filter which has only one output to this stream
33
+
34
+ Args:
35
+ *streams: the streams to apply the filter to
36
+ name: the name of the filter
37
+ input_typings: the input typings of the filter
38
+ **kwargs: the arguments for the filter
39
+
40
+ Returns:
41
+ the output stream
42
+
43
+ Note:
44
+ This function is for custom filter which is not implemented in typed-ffmpeg
45
+ """
46
+ return FilterNode(
47
+ name=name,
48
+ inputs=streams,
49
+ output_typings=(StreamType.video,),
50
+ input_typings=input_typings,
51
+ kwargs=tuple(kwargs.items()),
52
+ ).video(0)
53
+
54
+
55
+ def afilter(
56
+ *streams: FilterableStream, name: str, input_typings: tuple[StreamType, ...] = (StreamType.audio,), **kwargs: Any
57
+ ) -> AudioStream:
58
+ """
59
+ Apply a custom audio filter which has only one output to this stream
60
+
61
+ Args:
62
+ *streams: the streams to apply the filter to
63
+ name: the name of the filter
64
+ input_typings: the input typings of the filter
65
+ **kwargs: the arguments for the filter
66
+
67
+ Returns:
68
+ the output stream
69
+
70
+ Note:
71
+ This function is for custom filter which is not implemented in typed-ffmpeg
72
+ """
73
+ return FilterNode(
74
+ name=name,
75
+ inputs=streams,
76
+ output_typings=(StreamType.audio,),
77
+ input_typings=input_typings,
78
+ kwargs=tuple(kwargs.items()),
79
+ ).audio(0)
80
+
81
+
82
+ def filter_multi_output(
83
+ *streams: FilterableStream,
84
+ name: str,
85
+ input_typings: tuple[StreamType, ...] = (),
86
+ output_tyings: tuple[StreamType, ...] = (),
87
+ **kwargs: Any
88
+ ) -> FilterNode:
89
+ """
90
+ Apply a custom filter which has multiple outputs to this stream
91
+
92
+ Args:
93
+ *streams: the streams to apply the filter to
94
+ name: the name of the filter
95
+ input_typings: the input typings of the filter
96
+ output_tyings: the output typings of the filter
97
+ **kwargs: the arguments for the filter
98
+
99
+ Returns:
100
+ the FilterNode
101
+
102
+ Note:
103
+ This function is for custom filter which is not implemented in typed-ffmpeg
104
+ """
105
+ return FilterNode(
106
+ name=name,
107
+ kwargs=tuple(kwargs.items()),
108
+ inputs=streams,
109
+ input_typings=input_typings,
110
+ output_typings=output_tyings,
111
+ )
112
+
113
+
114
+ __all__ = ["input", "output", "merge_outputs", "vfilter", "afilter", "filter_multi_output"]
File without changes
@@ -0,0 +1,308 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Literal
6
+
7
+
8
+ class StreamType(str, Enum):
9
+ """
10
+ The type of a stream. (audio or video)
11
+ """
12
+
13
+ audio = "audio"
14
+ """it is an audio stream"""
15
+ video = "video"
16
+ """it is a video stream"""
17
+
18
+
19
+ class FFMpegFilterOptionType(str, Enum):
20
+ boolean = "boolean"
21
+ duration = "duration"
22
+ color = "color"
23
+ flags = "flags"
24
+ dictionary = "dictionary"
25
+ pix_fmt = "pix_fmt"
26
+ int = "int"
27
+ int64 = "int64"
28
+ double = "double"
29
+ float = "float"
30
+ string = "string"
31
+ video_rate = "video_rate"
32
+ image_size = "image_size"
33
+ rational = "rational"
34
+ sample_fmt = "sample_fmt"
35
+ binary = "binary"
36
+
37
+
38
+ class FFMpegFilterType(str, Enum):
39
+ af = "af"
40
+ asrc = "asrc"
41
+ asink = "asink"
42
+ vf = "vf"
43
+ vsrc = "vsrc"
44
+ vsink = "vsink"
45
+ avsrc = "avsrc"
46
+ avf = "avf"
47
+ vaf = "vaf"
48
+
49
+
50
+ @dataclass(frozen=True, kw_only=True)
51
+ class FFMpegFilterOptionChoice:
52
+ name: str
53
+ help: str
54
+ value: str | int
55
+ flags: str | None = None
56
+
57
+
58
+ @dataclass(frozen=True, kw_only=True)
59
+ class FFMpegFilterOption:
60
+ name: str
61
+ alias: tuple[str, ...] = ()
62
+ description: str
63
+ type: FFMpegFilterOptionType
64
+ min: str | None = None
65
+ max: str | None = None
66
+ default: bool | int | float | str | None = None
67
+ required: bool = False
68
+ choices: tuple[FFMpegFilterOptionChoice, ...] = ()
69
+ flags: str | None = None
70
+
71
+
72
+ @dataclass(frozen=True, kw_only=True)
73
+ class FFMpegIOType:
74
+ name: str | None = None
75
+ type: StreamType
76
+
77
+
78
+ @dataclass(frozen=True, kw_only=True)
79
+ class FFMpegFilterDef:
80
+ name: str
81
+
82
+ typings_input: str | tuple[Literal["video", "audio"], ...] = ()
83
+ typings_output: str | tuple[Literal["video", "audio"], ...] = ()
84
+
85
+
86
+ @dataclass(frozen=True, kw_only=True)
87
+ class FFMpegFilter:
88
+ id: str | None = None
89
+
90
+ name: str
91
+ description: str
92
+ ref: str | None = None
93
+
94
+ # Flags
95
+ is_support_slice_threading: bool | None = None
96
+ is_support_timeline: bool | None = None
97
+ is_support_framesync: bool | None = None
98
+ is_support_command: bool | None = None
99
+ is_filter_sink: bool | None = None
100
+ is_filter_source: bool | None = None
101
+
102
+ # IO Typing
103
+ is_dynamic_input: bool = False
104
+ is_dynamic_output: bool = False
105
+ stream_typings_input: tuple[FFMpegIOType, ...] = ()
106
+ stream_typings_output: tuple[FFMpegIOType, ...] = ()
107
+ formula_typings_input: str | None = None
108
+ formula_typings_output: str | None = None
109
+
110
+ pre: tuple[tuple[str, str], ...] = ()
111
+ options: tuple[FFMpegFilterOption, ...] = ()
112
+
113
+ @property
114
+ def pre_dict(self) -> dict[str, str]:
115
+ return dict(self.pre)
116
+
117
+ @property
118
+ def to_def(self) -> FFMpegFilterDef:
119
+ return FFMpegFilterDef(
120
+ name=self.name,
121
+ typings_input=self.formula_typings_input or tuple(k.type.value for k in self.stream_typings_input),
122
+ typings_output=self.formula_typings_output or tuple(k.type.value for k in self.stream_typings_output),
123
+ )
124
+
125
+ @property
126
+ def input_typings(self) -> set[StreamType]:
127
+ if self.is_filter_source:
128
+ return set()
129
+ if not self.is_dynamic_input:
130
+ return {i.type for i in self.stream_typings_input}
131
+ else:
132
+ assert self.formula_typings_input, f"{self.name} has no input"
133
+ if "video" not in self.formula_typings_input:
134
+ assert "audio" in self.formula_typings_input, f"{self.name} has no video input"
135
+ return {StreamType.audio}
136
+ elif "audio" not in self.formula_typings_input:
137
+ assert "video" in self.formula_typings_input, f"{self.name} has no audio input"
138
+ return {StreamType.video}
139
+ assert (
140
+ "video" in self.formula_typings_input and "audio" in self.formula_typings_input
141
+ ), f"{self.name} has no video or audio input"
142
+ return {StreamType.video, StreamType.audio}
143
+
144
+ @property
145
+ def output_typings(self) -> set[StreamType]:
146
+ if self.is_filter_sink:
147
+ return set()
148
+ if not self.is_dynamic_output:
149
+ return {i.type for i in self.stream_typings_output}
150
+ else:
151
+ assert self.formula_typings_output, f"{self.name} has no output"
152
+ if "video" not in self.formula_typings_output:
153
+ assert "audio" in self.formula_typings_output, f"{self.name} has no video output"
154
+ return {StreamType.audio}
155
+ elif "audio" not in self.formula_typings_output:
156
+ assert "video" in self.formula_typings_output, f"{self.name} has no audio output"
157
+ return {StreamType.video}
158
+ assert (
159
+ "video" in self.formula_typings_output and "audio" in self.formula_typings_output
160
+ ), f"{self.name} has no video or audio output"
161
+ return {StreamType.video, StreamType.audio}
162
+
163
+ @property
164
+ def filter_type(self) -> FFMpegFilterType:
165
+ if self.is_filter_sink:
166
+ assert len(self.input_typings) == 1
167
+ if {StreamType.video} == self.input_typings:
168
+ return FFMpegFilterType.vsink
169
+ if {StreamType.audio} == self.input_typings:
170
+ return FFMpegFilterType.asink
171
+ elif self.is_filter_source:
172
+ if {StreamType.video, StreamType.audio} == self.output_typings:
173
+ return FFMpegFilterType.avsrc
174
+ if {StreamType.video} == self.output_typings:
175
+ return FFMpegFilterType.vsrc
176
+ if {StreamType.audio} == self.output_typings:
177
+ return FFMpegFilterType.asrc
178
+
179
+ assert self.input_typings
180
+
181
+ if self.input_typings == {StreamType.video}:
182
+ if StreamType.audio in self.output_typings:
183
+ return FFMpegFilterType.vaf
184
+ if self.output_typings == {StreamType.video}:
185
+ return FFMpegFilterType.vf
186
+
187
+ if self.input_typings == {StreamType.audio}:
188
+ if self.output_typings == {StreamType.audio}:
189
+ return FFMpegFilterType.af
190
+ if StreamType.video in self.output_typings:
191
+ return FFMpegFilterType.avf
192
+
193
+ if self.input_typings == {StreamType.video, StreamType.audio}:
194
+ return FFMpegFilterType.avf
195
+
196
+ raise ValueError(f"Unknown filter type for {self.name}")
197
+
198
+
199
+ class FFMpegOptionFlag(int, Enum):
200
+ OPT_FUNC_ARG = 1 << 0
201
+ """
202
+ The OPT_TYPE_FUNC option takes an argument.
203
+ Must not be used with other option types, as for those it holds:
204
+ - OPT_TYPE_BOOL do not take an argument
205
+ - all other types do
206
+ """
207
+
208
+ OPT_EXIT = 1 << 1
209
+ """
210
+ Program will immediately exit after processing this option
211
+ """
212
+
213
+ OPT_EXPERT = 1 << 2
214
+ """
215
+ Option is intended for advanced users. Only affects help output.
216
+ """
217
+
218
+ OPT_VIDEO = 1 << 3
219
+ OPT_AUDIO = 1 << 4
220
+ OPT_SUBTITLE = 1 << 5
221
+ OPT_DATA = 1 << 6
222
+
223
+ OPT_PERFILE = 1 << 7
224
+ """
225
+ The option is per-file (currently ffmpeg-only). At least one of OPT_INPUT or OPT_OUTPUT must be set when this flag is in use.
226
+ """
227
+
228
+ OPT_FLAG_OFFSET = 1 << 8
229
+ """
230
+ Option is specified as an offset in a passed optctx.
231
+ Always use as OPT_OFFSET in option definitions.
232
+ """
233
+
234
+ OPT_OFFSET = OPT_FLAG_OFFSET | OPT_PERFILE
235
+ """
236
+ Option is to be stored in a SpecifierOptList.
237
+ Always use as OPT_SPEC in option definitions.
238
+ """
239
+ OPT_FLAG_SPEC = 1 << 9
240
+ """
241
+ Option is to be stored in a SpecifierOptList.
242
+ Always use as OPT_SPEC in option definitions.
243
+ """
244
+
245
+ OPT_SPEC = OPT_FLAG_SPEC | OPT_OFFSET
246
+ """
247
+ Option applies per-stream (implies OPT_SPEC).
248
+ """
249
+ OPT_FLAG_PERSTREAM = 1 << 10
250
+ """
251
+ Option applies per-stream (implies OPT_SPEC).
252
+ """
253
+ OPT_PERSTREAM = OPT_FLAG_PERSTREAM | OPT_SPEC
254
+
255
+ OPT_INPUT = 1 << 11
256
+ """
257
+ ffmpeg-only - specifies whether an OPT_PERFILE option applies to input, output, or both.
258
+ """
259
+ OPT_OUTPUT = 1 << 12
260
+ """
261
+ ffmpeg-only - specifies whether an OPT_PERFILE option applies to input, output, or both.
262
+ """
263
+
264
+ OPT_HAS_ALT = 1 << 13
265
+ """
266
+ This option is a "canonical" form, to which one or more alternatives exist. These alternatives are listed in u1.names_alt.
267
+ """
268
+ OPT_HAS_CANON = 1 << 14
269
+ """
270
+ This option is an alternative form of some other option, whose name is stored in u1.name_canon
271
+ """
272
+
273
+
274
+ class FFMpegOptionType(str, Enum):
275
+ OPT_TYPE_FUNC = "OPT_TYPE_FUNC"
276
+ OPT_TYPE_BOOL = "OPT_TYPE_BOOL"
277
+ OPT_TYPE_STRING = "OPT_TYPE_STRING"
278
+ OPT_TYPE_INT = "OPT_TYPE_INT"
279
+ OPT_TYPE_INT64 = "OPT_TYPE_INT64"
280
+ OPT_TYPE_FLOAT = "OPT_TYPE_FLOAT"
281
+ OPT_TYPE_DOUBLE = "OPT_TYPE_DOUBLE"
282
+ OPT_TYPE_TIME = "OPT_TYPE_TIME"
283
+
284
+
285
+ @dataclass(frozen=True, kw_only=True)
286
+ class FFMpegOption:
287
+ name: str
288
+ type: FFMpegOptionType
289
+ flags: int
290
+ help: str
291
+ argname: str | None = None
292
+ canon: str | None = None
293
+
294
+ @property
295
+ def is_input_option(self) -> bool:
296
+ return bool(self.flags & FFMpegOptionFlag.OPT_INPUT)
297
+
298
+ @property
299
+ def is_output_option(self) -> bool:
300
+ return bool(self.flags & FFMpegOptionFlag.OPT_OUTPUT)
301
+
302
+ @property
303
+ def is_global_option(self) -> bool:
304
+ return not self.is_input_option and not self.is_output_option and not (self.flags & FFMpegOptionFlag.OPT_EXIT)
305
+
306
+ @property
307
+ def is_support_stream_specifier(self) -> bool:
308
+ return bool(self.flags & FFMpegOptionFlag.OPT_SPEC)
@@ -0,0 +1,132 @@
1
+ from __future__ import absolute_import, annotations
2
+
3
+ import importlib
4
+ import json
5
+ from dataclasses import fields, is_dataclass
6
+ from enum import Enum
7
+ from functools import partial
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def load_class(path: str, strict: bool = True) -> Any:
13
+ """
14
+ Load a class from a string path
15
+
16
+ Args:
17
+ path: The path to the class.
18
+ strict: If True, raise an error if the class is not in ffmpeg package.
19
+
20
+ Returns:
21
+ The class.
22
+ """
23
+ if strict:
24
+ assert path.startswith("ffmpeg."), f"Only support loading class from ffmpeg package: {path}"
25
+
26
+ module_path, class_name = path.rsplit(".", 1)
27
+ module = importlib.import_module(module_path)
28
+ return getattr(module, class_name)
29
+
30
+
31
+ def frozen(v: Any) -> Any:
32
+ """
33
+ Convert the instance to a frozen instance
34
+
35
+ Args:
36
+ v: The instance to convert.
37
+
38
+ Returns:
39
+ The frozen instance.
40
+ """
41
+ if isinstance(v, list):
42
+ return tuple(frozen(i) for i in v)
43
+
44
+ if isinstance(v, dict):
45
+ return tuple((key, frozen(value)) for key, value in v.items())
46
+
47
+ return v
48
+
49
+
50
+ def object_hook(obj: Any, strict: bool = True) -> Any:
51
+ """
52
+ Convert the dictionary to an instance
53
+
54
+ Args:
55
+ obj: The dictionary to convert.
56
+
57
+ Returns:
58
+ The instance.
59
+ """
60
+ if isinstance(obj, dict):
61
+ if obj.get("__class__"):
62
+ cls = load_class(obj.pop("__class__"), strict=strict)
63
+
64
+ if is_dataclass(cls):
65
+ # NOTE: in our application, the dataclass is always frozen
66
+ return cls(**{k: frozen(v) for k, v in obj.items()})
67
+
68
+ return cls(**{k: v for k, v in obj.items()})
69
+
70
+ return obj
71
+
72
+
73
+ def loads(raw: str, strict: bool = True) -> Any:
74
+ """
75
+ Deserialize the JSON string to an instance
76
+
77
+ Args:
78
+ raw: The JSON string to deserialize.
79
+
80
+ Returns:
81
+ The deserialized instance.
82
+ """
83
+ object_hook_strict = partial(object_hook, strict=strict)
84
+
85
+ return json.loads(raw, object_hook=object_hook_strict)
86
+
87
+
88
+ def to_dict_with_class_info(instance: Any) -> Any:
89
+ """
90
+ Convert the instance to a dictionary with class information
91
+
92
+ Args:
93
+ instance: The instance to convert.
94
+
95
+ Returns:
96
+ The dictionary with class information
97
+ """
98
+
99
+ if isinstance(instance, dict):
100
+ return {k: to_dict_with_class_info(v) for k, v in instance.items()}
101
+ elif isinstance(instance, list):
102
+ return [to_dict_with_class_info(v) for v in instance]
103
+ elif isinstance(instance, tuple):
104
+ return tuple(to_dict_with_class_info(v) for v in instance)
105
+ elif isinstance(instance, Path):
106
+ return str(instance)
107
+ elif is_dataclass(instance):
108
+ return {
109
+ "__class__": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
110
+ **{k.name: to_dict_with_class_info(getattr(instance, k.name)) for k in fields(instance)},
111
+ }
112
+ elif isinstance(instance, Enum):
113
+ return {
114
+ "__class__": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
115
+ "value": instance.value,
116
+ }
117
+ return instance
118
+
119
+
120
+ # Serialization
121
+ def dumps(instance: Any) -> str:
122
+ """
123
+ Serialize the instance to a JSON string
124
+
125
+ Args:
126
+ instance: The instance to serialize.
127
+
128
+ Returns:
129
+ The serialized instance.
130
+ """
131
+ obj = to_dict_with_class_info(instance)
132
+ return json.dumps(obj, indent=2)
@@ -0,0 +1,13 @@
1
+ from .nodes import FilterableStream, FilterNode, GlobalNode, InputNode, OutputNode, OutputStream
2
+ from .schema import Node, Stream
3
+
4
+ __all__ = [
5
+ "Node",
6
+ "Stream",
7
+ "FilterableStream",
8
+ "FilterNode",
9
+ "GlobalNode",
10
+ "InputNode",
11
+ "OutputNode",
12
+ "OutputStream",
13
+ ]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from .context import DAGContext
4
+ from .nodes import FilterNode, GlobalNode, InputNode, OutputNode
5
+ from .schema import Stream
6
+ from .validate import validate
7
+
8
+
9
+ def compile(stream: Stream, auto_fix: bool = True) -> list[str]:
10
+ """
11
+ Compile the stream into a list of arguments.
12
+
13
+ Args:
14
+ stream: The stream to compile.
15
+ auto_fix: Whether to automatically fix the stream.
16
+
17
+ Returns:
18
+ The list of arguments.
19
+ """
20
+
21
+ stream = validate(stream, auto_fix=auto_fix)
22
+ node = stream.node
23
+ context = DAGContext.build(node)
24
+
25
+ # compile the global nodes
26
+ commands = []
27
+ global_nodes = [node for node in context.all_nodes if isinstance(node, GlobalNode)]
28
+ for node in global_nodes:
29
+ commands += node.get_args(context)
30
+
31
+ # compile the input nodes
32
+ input_nodes = [node for node in context.all_nodes if isinstance(node, InputNode)]
33
+ for node in input_nodes:
34
+ commands += node.get_args(context)
35
+
36
+ # compile the filter nodes
37
+ vf_commands = []
38
+ filter_nodes = [node for node in context.all_nodes if isinstance(node, FilterNode)]
39
+
40
+ for node in sorted(filter_nodes, key=lambda node: len(node.upstream_nodes)):
41
+ vf_commands += ["".join(node.get_args(context))]
42
+
43
+ if vf_commands:
44
+ commands += ["-filter_complex", ";".join(vf_commands)]
45
+
46
+ # compile the output nodes
47
+ output_nodes = [node for node in context.all_nodes if isinstance(node, OutputNode)]
48
+ for node in output_nodes:
49
+ commands += node.get_args(context)
50
+
51
+ return commands