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
typed_ffmpeg/__init__.py
ADDED
@@ -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
|