typed-ffmpeg-compatible 3.5.2__py3-none-any.whl → 3.6__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 +4 -1
- typed_ffmpeg/_version.py +2 -2
- typed_ffmpeg/base.py +4 -1
- typed_ffmpeg/codecs/__init__.py +2 -0
- typed_ffmpeg/codecs/decoders.py +1852 -1853
- typed_ffmpeg/codecs/encoders.py +2001 -1782
- typed_ffmpeg/codecs/schema.py +6 -12
- typed_ffmpeg/common/__init__.py +1 -0
- typed_ffmpeg/common/cache.py +9 -6
- typed_ffmpeg/common/schema.py +11 -0
- typed_ffmpeg/common/serialize.py +13 -7
- typed_ffmpeg/compile/__init__.py +1 -0
- typed_ffmpeg/compile/compile_cli.py +23 -4
- typed_ffmpeg/compile/compile_json.py +4 -0
- typed_ffmpeg/compile/compile_python.py +15 -0
- typed_ffmpeg/compile/context.py +15 -4
- typed_ffmpeg/compile/validate.py +4 -3
- typed_ffmpeg/dag/factory.py +2 -0
- typed_ffmpeg/dag/global_runnable/__init__.py +1 -0
- typed_ffmpeg/dag/global_runnable/global_args.py +2 -2
- typed_ffmpeg/dag/global_runnable/runnable.py +6 -2
- typed_ffmpeg/dag/io/__init__.py +1 -0
- typed_ffmpeg/dag/io/_input.py +20 -5
- typed_ffmpeg/dag/io/_output.py +24 -9
- typed_ffmpeg/dag/io/output_args.py +21 -7
- typed_ffmpeg/dag/nodes.py +20 -0
- typed_ffmpeg/dag/schema.py +19 -6
- typed_ffmpeg/dag/utils.py +2 -2
- typed_ffmpeg/exceptions.py +2 -1
- typed_ffmpeg/expressions.py +884 -0
- typed_ffmpeg/ffprobe/__init__.py +1 -0
- typed_ffmpeg/ffprobe/parse.py +7 -1
- typed_ffmpeg/ffprobe/probe.py +3 -1
- typed_ffmpeg/ffprobe/schema.py +83 -1
- typed_ffmpeg/ffprobe/xml2json.py +8 -2
- typed_ffmpeg/filters.py +540 -631
- typed_ffmpeg/formats/__init__.py +2 -0
- typed_ffmpeg/formats/demuxers.py +1869 -1921
- typed_ffmpeg/formats/muxers.py +1382 -1107
- typed_ffmpeg/formats/schema.py +6 -12
- typed_ffmpeg/info.py +8 -0
- typed_ffmpeg/options/__init__.py +15 -0
- typed_ffmpeg/options/codec.py +711 -0
- typed_ffmpeg/options/format.py +196 -0
- typed_ffmpeg/options/framesync.py +43 -0
- typed_ffmpeg/options/timeline.py +22 -0
- typed_ffmpeg/schema.py +15 -0
- typed_ffmpeg/sources.py +392 -381
- typed_ffmpeg/streams/__init__.py +2 -0
- typed_ffmpeg/streams/audio.py +1071 -882
- typed_ffmpeg/streams/av.py +9 -3
- typed_ffmpeg/streams/subtitle.py +3 -3
- typed_ffmpeg/streams/video.py +1873 -1725
- typed_ffmpeg/types.py +3 -2
- typed_ffmpeg/utils/__init__.py +1 -0
- typed_ffmpeg/utils/escaping.py +8 -4
- typed_ffmpeg/utils/frozendict.py +31 -1
- typed_ffmpeg/utils/lazy_eval/__init__.py +1 -0
- typed_ffmpeg/utils/lazy_eval/operator.py +75 -27
- typed_ffmpeg/utils/lazy_eval/schema.py +176 -4
- typed_ffmpeg/utils/run.py +2 -0
- typed_ffmpeg/utils/snapshot.py +1 -0
- typed_ffmpeg/utils/typing.py +2 -1
- typed_ffmpeg/utils/view.py +2 -1
- {typed_ffmpeg_compatible-3.5.2.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/METADATA +1 -1
- typed_ffmpeg_compatible-3.6.dist-info/RECORD +73 -0
- typed_ffmpeg_compatible-3.5.2.dist-info/RECORD +0 -67
- {typed_ffmpeg_compatible-3.5.2.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-3.5.2.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/entry_points.txt +0 -0
- {typed_ffmpeg_compatible-3.5.2.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/licenses/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.5.2.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/top_level.txt +0 -0
typed_ffmpeg/codecs/schema.py
CHANGED
@@ -1,17 +1,11 @@
|
|
1
|
-
|
1
|
+
"""FFmpeg codec schema definitions."""
|
2
2
|
|
3
|
-
from ..
|
4
|
-
from ..utils.frozendict import FrozenDict
|
3
|
+
from ..schema import FFMpegOptionGroup
|
5
4
|
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
kwargs: FrozenDict[str, str | int | float | bool] = FrozenDict({})
|
6
|
+
class FFMpegEncoderOption(FFMpegOptionGroup):
|
7
|
+
"""FFmpeg encoder option group."""
|
10
8
|
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@dataclass(frozen=True, kw_only=True)
|
17
|
-
class FFMpegDecoderOption(FFMpegCodecOption): ...
|
10
|
+
class FFMpegDecoderOption(FFMpegOptionGroup):
|
11
|
+
"""FFmpeg decoder option group."""
|
typed_ffmpeg/common/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""FFmpeg common utilities package."""
|
typed_ffmpeg/common/cache.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Cache utilities for FFmpeg operations."""
|
2
|
+
|
1
3
|
from pathlib import Path
|
2
4
|
from typing import TypeVar
|
3
5
|
|
@@ -11,7 +13,7 @@ cache_path.mkdir(exist_ok=True)
|
|
11
13
|
|
12
14
|
def load(cls: type[T], id: str) -> T:
|
13
15
|
"""
|
14
|
-
Load an object from the cache
|
16
|
+
Load an object from the cache.
|
15
17
|
|
16
18
|
Args:
|
17
19
|
cls: The class of the object
|
@@ -19,6 +21,7 @@ def load(cls: type[T], id: str) -> T:
|
|
19
21
|
|
20
22
|
Returns:
|
21
23
|
The loaded object
|
24
|
+
|
22
25
|
"""
|
23
26
|
path = cache_path / f"{cls.__name__}/{id}.json"
|
24
27
|
|
@@ -29,11 +32,12 @@ def load(cls: type[T], id: str) -> T:
|
|
29
32
|
|
30
33
|
def save(obj: T, id: str) -> None:
|
31
34
|
"""
|
32
|
-
Save an object to the cache
|
35
|
+
Save an object to the cache.
|
33
36
|
|
34
37
|
Args:
|
35
38
|
obj: The object to save
|
36
39
|
id: The id of the object
|
40
|
+
|
37
41
|
"""
|
38
42
|
schema_path = cache_path / f"{obj.__class__.__name__}"
|
39
43
|
schema_path.mkdir(exist_ok=True)
|
@@ -44,13 +48,14 @@ def save(obj: T, id: str) -> None:
|
|
44
48
|
|
45
49
|
def list_all(cls: type[T]) -> list[T]:
|
46
50
|
"""
|
47
|
-
List all objects of a class in the cache
|
51
|
+
List all objects of a class in the cache.
|
48
52
|
|
49
53
|
Args:
|
50
54
|
cls: The class of the objects
|
51
55
|
|
52
56
|
Returns:
|
53
57
|
A list of all objects of the class in the cache
|
58
|
+
|
54
59
|
"""
|
55
60
|
path = cache_path / f"{cls.__name__}"
|
56
61
|
|
@@ -58,9 +63,7 @@ def list_all(cls: type[T]) -> list[T]:
|
|
58
63
|
|
59
64
|
|
60
65
|
def clean(cls: type[T]) -> None:
|
61
|
-
"""
|
62
|
-
Clean the cache for a class
|
63
|
-
"""
|
66
|
+
"""Clean the cache for a class."""
|
64
67
|
path = cache_path / f"{cls.__name__}"
|
65
68
|
for i in path.glob("*.json"):
|
66
69
|
i.unlink()
|
typed_ffmpeg/common/schema.py
CHANGED
@@ -275,6 +275,7 @@ class FFMpegFilter(Serializable):
|
|
275
275
|
|
276
276
|
Returns:
|
277
277
|
A dictionary mapping parameter names to their values
|
278
|
+
|
278
279
|
"""
|
279
280
|
return dict(self.pre)
|
280
281
|
|
@@ -288,6 +289,7 @@ class FFMpegFilter(Serializable):
|
|
288
289
|
|
289
290
|
Returns:
|
290
291
|
A simplified FFMpegFilterDef representation of this filter
|
292
|
+
|
291
293
|
"""
|
292
294
|
return FFMpegFilterDef(
|
293
295
|
name=self.name,
|
@@ -310,6 +312,7 @@ class FFMpegFilter(Serializable):
|
|
310
312
|
|
311
313
|
Raises:
|
312
314
|
AssertionError: If a dynamic input filter has no input formula
|
315
|
+
|
313
316
|
"""
|
314
317
|
if self.is_filter_source:
|
315
318
|
return set()
|
@@ -346,6 +349,7 @@ class FFMpegFilter(Serializable):
|
|
346
349
|
|
347
350
|
Raises:
|
348
351
|
AssertionError: If a dynamic output filter has no output formula
|
352
|
+
|
349
353
|
"""
|
350
354
|
if self.is_filter_sink:
|
351
355
|
return set()
|
@@ -386,6 +390,7 @@ class FFMpegFilter(Serializable):
|
|
386
390
|
any known filter type
|
387
391
|
AssertionError: If a sink filter has multiple input types or
|
388
392
|
if a filter has no input types
|
393
|
+
|
389
394
|
"""
|
390
395
|
if self.is_filter_sink:
|
391
396
|
assert len(self.input_typings) == 1
|
@@ -423,6 +428,8 @@ class FFMpegFilter(Serializable):
|
|
423
428
|
|
424
429
|
@serializable
|
425
430
|
class FFMpegOptionFlag(int, Enum):
|
431
|
+
"""FFmpeg option flags that define option behavior and characteristics."""
|
432
|
+
|
426
433
|
OPT_FUNC_ARG = 1 << 0
|
427
434
|
"""
|
428
435
|
The OPT_TYPE_FUNC option takes an argument.
|
@@ -567,6 +574,7 @@ class FFMpegOption(Serializable):
|
|
567
574
|
|
568
575
|
Returns:
|
569
576
|
True if this option is meant to be used with input files
|
577
|
+
|
570
578
|
"""
|
571
579
|
return bool(self.flags & FFMpegOptionFlag.OPT_INPUT)
|
572
580
|
|
@@ -577,6 +585,7 @@ class FFMpegOption(Serializable):
|
|
577
585
|
|
578
586
|
Returns:
|
579
587
|
True if this option is meant to be used with output files
|
588
|
+
|
580
589
|
"""
|
581
590
|
return bool(self.flags & FFMpegOptionFlag.OPT_OUTPUT)
|
582
591
|
|
@@ -588,6 +597,7 @@ class FFMpegOption(Serializable):
|
|
588
597
|
Returns:
|
589
598
|
True if this option is a global option that doesn't apply to
|
590
599
|
specific input or output files
|
600
|
+
|
591
601
|
"""
|
592
602
|
return (
|
593
603
|
not self.is_input_option
|
@@ -605,5 +615,6 @@ class FFMpegOption(Serializable):
|
|
605
615
|
|
606
616
|
Returns:
|
607
617
|
True if this option can be used with stream specifiers
|
618
|
+
|
608
619
|
"""
|
609
620
|
return bool(self.flags & FFMpegOptionFlag.OPT_SPEC)
|
typed_ffmpeg/common/serialize.py
CHANGED
@@ -37,6 +37,7 @@ def serializable(
|
|
37
37
|
|
38
38
|
Returns:
|
39
39
|
The class itself
|
40
|
+
|
40
41
|
"""
|
41
42
|
assert cls.__name__ not in CLASS_REGISTRY, (
|
42
43
|
f"Class {cls.__name__} already registered"
|
@@ -47,11 +48,10 @@ def serializable(
|
|
47
48
|
|
48
49
|
|
49
50
|
class Serializable:
|
50
|
-
"""
|
51
|
-
A base class for all serializable classes.
|
52
|
-
"""
|
51
|
+
"""A base class for all serializable classes."""
|
53
52
|
|
54
53
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
54
|
+
"""Register the subclass in the serialization registry."""
|
55
55
|
super().__init_subclass__(**kwargs)
|
56
56
|
serializable(cls)
|
57
57
|
|
@@ -79,6 +79,7 @@ def load_class(name: str) -> type[Serializable] | type[Enum]:
|
|
79
79
|
# Create an instance
|
80
80
|
node = FilterNode(name='scale', ...)
|
81
81
|
```
|
82
|
+
|
82
83
|
"""
|
83
84
|
assert name in CLASS_REGISTRY, f"Class {name} not registered"
|
84
85
|
return CLASS_REGISTRY[name]
|
@@ -102,11 +103,13 @@ def frozen(v: Any) -> Any:
|
|
102
103
|
Example:
|
103
104
|
```python
|
104
105
|
# Convert a nested structure to immutable form
|
105
|
-
frozen_data = frozen(
|
106
|
-
|
107
|
-
|
106
|
+
frozen_data = frozen({
|
107
|
+
"options": ["option1", "option2"],
|
108
|
+
"settings": {"key": "value"},
|
109
|
+
})
|
108
110
|
# Result: FrozenDict with tuple instead of list and nested FrozenDict
|
109
111
|
```
|
112
|
+
|
110
113
|
"""
|
111
114
|
if isinstance(v, list):
|
112
115
|
return tuple(frozen(i) for i in v)
|
@@ -141,6 +144,7 @@ def object_hook(obj: Any) -> Any:
|
|
141
144
|
}
|
142
145
|
# Will be converted to a FilterNode instance
|
143
146
|
```
|
147
|
+
|
144
148
|
"""
|
145
149
|
if isinstance(obj, dict):
|
146
150
|
if obj.get("__class__"):
|
@@ -176,6 +180,7 @@ def loads(raw: str) -> Any:
|
|
176
180
|
filter_node = loads(json_str)
|
177
181
|
# filter_node is now a FilterNode instance
|
178
182
|
```
|
183
|
+
|
179
184
|
"""
|
180
185
|
return json.loads(raw, object_hook=object_hook)
|
181
186
|
|
@@ -202,8 +207,8 @@ def to_dict_with_class_info(instance: Any) -> Any:
|
|
202
207
|
serializable = to_dict_with_class_info(filter_node)
|
203
208
|
# serializable now contains class information and all attributes
|
204
209
|
```
|
205
|
-
"""
|
206
210
|
|
211
|
+
"""
|
207
212
|
if isinstance(instance, dict | FrozenDict):
|
208
213
|
return {k: to_dict_with_class_info(v) for k, v in instance.items()}
|
209
214
|
elif isinstance(instance, list):
|
@@ -251,6 +256,7 @@ def dumps(instance: Any) -> str:
|
|
251
256
|
# json_str can be saved to a file and later deserialized
|
252
257
|
# with loads() to reconstruct the original object
|
253
258
|
```
|
259
|
+
|
254
260
|
"""
|
255
261
|
obj = to_dict_with_class_info(instance)
|
256
262
|
return json.dumps(obj, indent=2)
|
typed_ffmpeg/compile/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""FFmpeg compilation utilities package."""
|
@@ -58,6 +58,7 @@ def get_options_dict() -> dict[str, FFMpegOption]:
|
|
58
58
|
|
59
59
|
Returns:
|
60
60
|
Dictionary mapping option names to their FFMpegOption definitions
|
61
|
+
|
61
62
|
"""
|
62
63
|
options = load(list[FFMpegOption], "options")
|
63
64
|
return {option.name: option for option in options}
|
@@ -69,6 +70,7 @@ def get_filter_dict() -> dict[str, FFMpegFilter]:
|
|
69
70
|
|
70
71
|
Returns:
|
71
72
|
Dictionary mapping filter names to their FFMpegFilter definitions
|
73
|
+
|
72
74
|
"""
|
73
75
|
filters = load(list[FFMpegFilter], "filters")
|
74
76
|
return {filter.name: filter for filter in filters}
|
@@ -89,6 +91,7 @@ def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
|
|
89
91
|
|
90
92
|
Returns:
|
91
93
|
Dictionary mapping option names to lists of their values
|
94
|
+
|
92
95
|
"""
|
93
96
|
parsed_options: dict[str, list[str | None | bool]] = defaultdict(list)
|
94
97
|
|
@@ -134,6 +137,8 @@ def parse_stream_selector(
|
|
134
137
|
|
135
138
|
Raises:
|
136
139
|
AssertionError: If the stream label is not found in the mapping
|
140
|
+
FFMpegValueError: If the stream type is unknown
|
141
|
+
|
137
142
|
"""
|
138
143
|
selector = selector.strip("[]")
|
139
144
|
|
@@ -182,6 +187,7 @@ def _is_filename(token: str) -> bool:
|
|
182
187
|
|
183
188
|
Returns:
|
184
189
|
True if the token is a filename, False otherwise
|
190
|
+
|
185
191
|
"""
|
186
192
|
# not start with - and has ext
|
187
193
|
return not token.startswith("-") and len(token.split(".")) > 1
|
@@ -205,6 +211,7 @@ def parse_output(
|
|
205
211
|
|
206
212
|
Returns:
|
207
213
|
List of OutputStream objects representing the output specifications
|
214
|
+
|
208
215
|
"""
|
209
216
|
tokens = source.copy()
|
210
217
|
|
@@ -272,6 +279,7 @@ def parse_input(
|
|
272
279
|
|
273
280
|
Returns:
|
274
281
|
Dictionary mapping input indices to their FilterableStream objects
|
282
|
+
|
275
283
|
"""
|
276
284
|
output: list[AVStream] = []
|
277
285
|
|
@@ -325,6 +333,10 @@ def parse_filter_complex(
|
|
325
333
|
|
326
334
|
Returns:
|
327
335
|
Updated stream mapping with new filter outputs added
|
336
|
+
|
337
|
+
Raises:
|
338
|
+
FFMpegValueError: If the stream type is unknown
|
339
|
+
|
328
340
|
"""
|
329
341
|
# Use re.split with negative lookbehind to handle escaped semicolons
|
330
342
|
filter_units = re.split(r"(?<!\\);", filter_complex)
|
@@ -425,6 +437,7 @@ def parse_global(
|
|
425
437
|
Example:
|
426
438
|
For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
|
427
439
|
Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
|
440
|
+
|
428
441
|
"""
|
429
442
|
options = parse_options(tokens[: tokens.index("-i")])
|
430
443
|
remaining_tokens = tokens[tokens.index("-i") :]
|
@@ -466,6 +479,7 @@ def parse(cli: str) -> Stream:
|
|
466
479
|
"ffmpeg -i input.mp4 -filter_complex '[0:v]scale=1280:720[v]' -map '[v]' output.mp4"
|
467
480
|
)
|
468
481
|
```
|
482
|
+
|
469
483
|
"""
|
470
484
|
# ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
|
471
485
|
ffmpeg_options = get_options_dict()
|
@@ -523,6 +537,7 @@ def compile(
|
|
523
537
|
|
524
538
|
Returns:
|
525
539
|
A command-line string that can be passed to FFmpeg
|
540
|
+
|
526
541
|
"""
|
527
542
|
return "ffmpeg " + command_line(
|
528
543
|
compile_as_list(stream, auto_fix, use_filter_complex_script)
|
@@ -596,8 +611,8 @@ def compile_as_list(
|
|
596
611
|
args
|
597
612
|
) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
|
598
613
|
```
|
599
|
-
"""
|
600
614
|
|
615
|
+
"""
|
601
616
|
stream = validate(stream, auto_fix=auto_fix)
|
602
617
|
node = stream.node
|
603
618
|
context = DAGContext.build(node)
|
@@ -671,6 +686,7 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
|
671
686
|
|
672
687
|
Raises:
|
673
688
|
FFMpegValueError: If the stream has an unknown type or node type
|
689
|
+
|
674
690
|
"""
|
675
691
|
from ..streams.audio import AudioStream
|
676
692
|
from ..streams.av import AVStream
|
@@ -741,8 +757,8 @@ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
|
|
741
757
|
Example:
|
742
758
|
For a scale filter with width=1280 and height=720, this might return:
|
743
759
|
['[0:v]', 'scale=', 'width=1280:height=720', '[s0]']
|
744
|
-
"""
|
745
760
|
|
761
|
+
"""
|
746
762
|
incoming_labels = "".join(f"[{get_stream_label(k, context)}]" for k in node.inputs)
|
747
763
|
outputs = context.get_outgoing_streams(node)
|
748
764
|
|
@@ -794,6 +810,7 @@ def get_args_input_node(node: InputNode, context: DAGContext) -> list[str]:
|
|
794
810
|
Example:
|
795
811
|
For an input file "input.mp4" with options like seeking to 10 seconds:
|
796
812
|
['-ss', '10', '-i', 'input.mp4']
|
813
|
+
|
797
814
|
"""
|
798
815
|
commands = []
|
799
816
|
for key, value in node.kwargs.items():
|
@@ -827,6 +844,7 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
|
|
827
844
|
Example:
|
828
845
|
For an output file "output.mp4" with H.264 video codec:
|
829
846
|
['-map', '[v0]', '-c:v', 'libx264', 'output.mp4']
|
847
|
+
|
830
848
|
"""
|
831
849
|
# !handle mapping
|
832
850
|
commands = []
|
@@ -888,6 +906,7 @@ def get_args_global_node(node: GlobalNode, context: DAGContext) -> list[str]:
|
|
888
906
|
Example:
|
889
907
|
For global options like overwrite and quiet logging:
|
890
908
|
['-y', '-loglevel', 'quiet']
|
909
|
+
|
891
910
|
"""
|
892
911
|
commands = []
|
893
912
|
for key, value in node.kwargs.items():
|
@@ -919,8 +938,8 @@ def get_args(node: Node, context: DAGContext | None = None) -> list[str]:
|
|
919
938
|
|
920
939
|
Raises:
|
921
940
|
FFMpegValueError: If the node type is not recognized
|
922
|
-
"""
|
923
941
|
|
942
|
+
"""
|
924
943
|
context = context or DAGContext.build(node)
|
925
944
|
|
926
945
|
match node:
|
@@ -955,8 +974,8 @@ def get_node_label(node: Node, context: DAGContext) -> str:
|
|
955
974
|
|
956
975
|
Raises:
|
957
976
|
AssertionError: If the node is not an InputNode or FilterNode
|
958
|
-
"""
|
959
977
|
|
978
|
+
"""
|
960
979
|
node_id = context.node_ids[node]
|
961
980
|
match node:
|
962
981
|
case InputNode():
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""JSON compilation utilities for FFmpeg streams."""
|
2
|
+
|
1
3
|
from ..common.serialize import dumps, loads
|
2
4
|
from ..dag.schema import Stream
|
3
5
|
from .validate import validate
|
@@ -16,6 +18,7 @@ def compile(stream: Stream, auto_fix: bool = True) -> str:
|
|
16
18
|
|
17
19
|
Returns:
|
18
20
|
A JSON string that can be passed to FFmpeg
|
21
|
+
|
19
22
|
"""
|
20
23
|
stream = validate(stream, auto_fix=auto_fix)
|
21
24
|
|
@@ -34,5 +37,6 @@ def parse(json: str) -> Stream:
|
|
34
37
|
|
35
38
|
Returns:
|
36
39
|
A Stream object
|
40
|
+
|
37
41
|
"""
|
38
42
|
return loads(json)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Python code compilation utilities for FFmpeg streams."""
|
2
|
+
|
1
3
|
from __future__ import annotations
|
2
4
|
|
3
5
|
from collections.abc import Mapping
|
@@ -39,6 +41,7 @@ def filter_stream_typed_index(
|
|
39
41
|
|
40
42
|
Returns:
|
41
43
|
The index of the matched stream in the outgoing streams of the node.
|
44
|
+
|
42
45
|
"""
|
43
46
|
matched_outgoing_streams = [
|
44
47
|
k
|
@@ -69,6 +72,10 @@ def get_input_var_name(
|
|
69
72
|
|
70
73
|
Returns:
|
71
74
|
The input variable name for the stream.
|
75
|
+
|
76
|
+
Raises:
|
77
|
+
ValueError: If the stream type is unknown
|
78
|
+
|
72
79
|
"""
|
73
80
|
match stream:
|
74
81
|
case AVStream():
|
@@ -127,6 +134,10 @@ def get_output_var_name(node: Node, context: DAGContext) -> str:
|
|
127
134
|
|
128
135
|
Returns:
|
129
136
|
The output variable name for the node.
|
137
|
+
|
138
|
+
Raises:
|
139
|
+
ValueError: If the node type is unknown
|
140
|
+
|
130
141
|
"""
|
131
142
|
match node:
|
132
143
|
case InputNode():
|
@@ -153,6 +164,7 @@ def compile_kwargs(kwargs: Mapping[str, Any]) -> str:
|
|
153
164
|
|
154
165
|
Returns:
|
155
166
|
The compiled kwargs.
|
167
|
+
|
156
168
|
"""
|
157
169
|
return ", ".join(f"{k}={repr(v)}" for k, v in kwargs.items())
|
158
170
|
|
@@ -169,6 +181,7 @@ def compile_fluent(code: list[str]) -> list[str]:
|
|
169
181
|
|
170
182
|
Returns:
|
171
183
|
The compiled code.
|
184
|
+
|
172
185
|
"""
|
173
186
|
buffer = [k.split("=", 1)[:2] for k in code]
|
174
187
|
|
@@ -209,6 +222,7 @@ def compile(stream: Stream, auto_fix: bool = True, fluent: bool = True) -> str:
|
|
209
222
|
|
210
223
|
Returns:
|
211
224
|
The compiled python code.
|
225
|
+
|
212
226
|
"""
|
213
227
|
stream = validate(stream, auto_fix=auto_fix)
|
214
228
|
node = stream.node
|
@@ -314,6 +328,7 @@ def parse(code: str) -> Stream:
|
|
314
328
|
|
315
329
|
Returns:
|
316
330
|
The parsed stream.
|
331
|
+
|
317
332
|
"""
|
318
333
|
local_vars: dict[str, Any] = {}
|
319
334
|
exec(code, {}, local_vars)
|
typed_ffmpeg/compile/context.py
CHANGED
@@ -35,6 +35,7 @@ def _remove_duplicates(seq: Iterable[T]) -> list[T]:
|
|
35
35
|
|
36
36
|
Returns:
|
37
37
|
A new list with duplicates removed, preserving the original order
|
38
|
+
|
38
39
|
"""
|
39
40
|
seen = set()
|
40
41
|
output: list[T] = []
|
@@ -62,6 +63,7 @@ def _collect(node: Node) -> tuple[list[Node], list[Stream]]:
|
|
62
63
|
A tuple containing two lists:
|
63
64
|
- A list of all nodes in the upstream path (including the starting node)
|
64
65
|
- A list of all streams connecting these nodes
|
66
|
+
|
65
67
|
"""
|
66
68
|
nodes: list[Node] = [node]
|
67
69
|
streams: list[Stream] = list(node.inputs)
|
@@ -124,6 +126,7 @@ class DAGContext:
|
|
124
126
|
|
125
127
|
Returns:
|
126
128
|
A fully initialized DAGContext containing all nodes and streams in the graph
|
129
|
+
|
127
130
|
"""
|
128
131
|
nodes, streams = _collect(node)
|
129
132
|
|
@@ -144,6 +147,7 @@ class DAGContext:
|
|
144
147
|
|
145
148
|
Returns:
|
146
149
|
A sorted list of all nodes in the graph
|
150
|
+
|
147
151
|
"""
|
148
152
|
return sorted(self.nodes, key=lambda node: len(node.upstream_nodes))
|
149
153
|
|
@@ -159,6 +163,7 @@ class DAGContext:
|
|
159
163
|
|
160
164
|
Returns:
|
161
165
|
A sorted list of all streams in the graph
|
166
|
+
|
162
167
|
"""
|
163
168
|
return sorted(
|
164
169
|
self.streams,
|
@@ -177,6 +182,7 @@ class DAGContext:
|
|
177
182
|
|
178
183
|
Returns:
|
179
184
|
A dictionary mapping streams to their destination nodes and connection indices
|
185
|
+
|
180
186
|
"""
|
181
187
|
outgoing_nodes: dict[Stream, list[tuple[Node, int]]] = defaultdict(list)
|
182
188
|
|
@@ -197,8 +203,8 @@ class DAGContext:
|
|
197
203
|
|
198
204
|
Returns:
|
199
205
|
A dictionary mapping nodes to their output streams
|
200
|
-
"""
|
201
206
|
|
207
|
+
"""
|
202
208
|
outgoing_streams: dict[Node, list[Stream]] = defaultdict(list)
|
203
209
|
|
204
210
|
for stream in self.streams:
|
@@ -210,10 +216,13 @@ class DAGContext:
|
|
210
216
|
def node_ids(self) -> dict[Node, int]:
|
211
217
|
"""
|
212
218
|
Get a mapping of nodes to their unique integer IDs.
|
219
|
+
|
213
220
|
This property assigns a unique integer ID to each node in the graph,
|
214
221
|
based on the node type and its position in the processing chain.
|
222
|
+
|
215
223
|
Returns:
|
216
|
-
A dictionary mapping nodes to their unique integer IDs
|
224
|
+
A dictionary mapping nodes to their unique integer IDs.
|
225
|
+
|
217
226
|
"""
|
218
227
|
node_index: dict[type[Node], int] = defaultdict(int)
|
219
228
|
node_ids: dict[Node, int] = {}
|
@@ -239,8 +248,8 @@ class DAGContext:
|
|
239
248
|
|
240
249
|
Returns:
|
241
250
|
A dictionary mapping nodes to their string labels
|
242
|
-
"""
|
243
251
|
|
252
|
+
"""
|
244
253
|
input_node_index = 0
|
245
254
|
filter_node_index = 0
|
246
255
|
node_labels: dict[Node, str] = {}
|
@@ -271,6 +280,7 @@ class DAGContext:
|
|
271
280
|
|
272
281
|
Returns:
|
273
282
|
A list of (node, input_index) tuples for nodes that receive this stream
|
283
|
+
|
274
284
|
"""
|
275
285
|
return self.outgoing_nodes[stream]
|
276
286
|
|
@@ -292,8 +302,8 @@ class DAGContext:
|
|
292
302
|
|
293
303
|
Raises:
|
294
304
|
AssertionError: If the node is not an InputNode or FilterNode
|
295
|
-
"""
|
296
305
|
|
306
|
+
"""
|
297
307
|
return self.node_labels[node]
|
298
308
|
|
299
309
|
@override
|
@@ -311,5 +321,6 @@ class DAGContext:
|
|
311
321
|
|
312
322
|
Returns:
|
313
323
|
A list of streams that originate from this node
|
324
|
+
|
314
325
|
"""
|
315
326
|
return self.outgoing_streams[node]
|
typed_ffmpeg/compile/validate.py
CHANGED
@@ -43,8 +43,8 @@ def remove_split(
|
|
43
43
|
A tuple containing:
|
44
44
|
- The new stream corresponding to the input stream but with splits removed
|
45
45
|
- A mapping dictionary relating original streams to their new versions
|
46
|
-
"""
|
47
46
|
|
47
|
+
"""
|
48
48
|
# remove all split nodes
|
49
49
|
# add split nodes to the graph
|
50
50
|
if mapping is None:
|
@@ -122,8 +122,8 @@ def add_split(
|
|
122
122
|
|
123
123
|
Raises:
|
124
124
|
FFMpegValueError: If an unsupported stream type is encountered
|
125
|
-
"""
|
126
125
|
|
126
|
+
"""
|
127
127
|
if not context:
|
128
128
|
context = DAGContext.build(current_stream.node)
|
129
129
|
|
@@ -202,8 +202,8 @@ def fix_graph(stream: Stream) -> Stream:
|
|
202
202
|
Note:
|
203
203
|
This function creates a new graph structure rather than modifying the
|
204
204
|
existing one, preserving the original graph.
|
205
|
-
"""
|
206
205
|
|
206
|
+
"""
|
207
207
|
stream, _ = remove_split(stream)
|
208
208
|
stream, _ = add_split(stream)
|
209
209
|
return stream
|
@@ -240,6 +240,7 @@ def validate(stream: Stream, auto_fix: bool = True) -> Stream:
|
|
240
240
|
# Validate will automatically insert a split filter
|
241
241
|
valid_stream = ffmpeg.dag.validate(scaled.output("output.mp4"))
|
242
242
|
```
|
243
|
+
|
243
244
|
"""
|
244
245
|
if auto_fix:
|
245
246
|
stream = fix_graph(stream)
|
typed_ffmpeg/dag/factory.py
CHANGED
@@ -28,6 +28,7 @@ def eval_formula(formula: str, **kwargs: Any) -> list[StreamType]:
|
|
28
28
|
|
29
29
|
Returns:
|
30
30
|
The result of the formula evaluation
|
31
|
+
|
31
32
|
"""
|
32
33
|
# Convert formula to Python code
|
33
34
|
return eval(
|
@@ -57,6 +58,7 @@ def filter_node_factory(
|
|
57
58
|
Note:
|
58
59
|
This function is primarily used internally by the filter generation system
|
59
60
|
to create filter nodes from the FFmpeg filter definitions.
|
61
|
+
|
60
62
|
"""
|
61
63
|
for k, v in kwargs.items():
|
62
64
|
if isinstance(v, Auto):
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Global runnable utilities for FFmpeg DAG operations."""
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# NOTE: this file is auto-generated, do not modify
|
2
|
-
|
2
|
+
"""Global arguments."""
|
3
3
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
@@ -136,8 +136,8 @@ class GlobalArgs(ABC):
|
|
136
136
|
|
137
137
|
Returns:
|
138
138
|
GlobalStream: GlobalStream instance
|
139
|
-
"""
|
140
139
|
|
140
|
+
"""
|
141
141
|
return self._global_node(
|
142
142
|
**merge(
|
143
143
|
{
|