typed-ffmpeg-compatible 2.4.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.
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