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.
- 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 +320 -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 +3572 -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 +7358 -0
- typed_ffmpeg/streams/av.py +22 -0
- typed_ffmpeg/streams/channel_layout.py +39 -0
- typed_ffmpeg/streams/video.py +13469 -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.4.1.dist-info/LICENSE +21 -0
- typed_ffmpeg_compatible-2.4.1.dist-info/METADATA +182 -0
- typed_ffmpeg_compatible-2.4.1.dist-info/RECORD +45 -0
- typed_ffmpeg_compatible-2.4.1.dist-info/WHEEL +4 -0
- typed_ffmpeg_compatible-2.4.1.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
|