typed-ffmpeg-compatible 3.5.1__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 +55 -8
- 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 +9 -8
- 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 +51 -11
- 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 +3 -2
- typed_ffmpeg/utils/typing.py +2 -1
- typed_ffmpeg/utils/view.py +2 -1
- {typed_ffmpeg_compatible-3.5.1.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.1.dist-info/RECORD +0 -67
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/entry_points.txt +0 -0
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/licenses/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.5.1.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."""
|
@@ -16,8 +16,10 @@ filter graph syntax, and escaping of special characters in FFmpeg commands.
|
|
16
16
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
|
+
import logging
|
19
20
|
import re
|
20
21
|
import shlex
|
22
|
+
import tempfile
|
21
23
|
from collections import defaultdict
|
22
24
|
from collections.abc import Mapping
|
23
25
|
from dataclasses import replace
|
@@ -47,6 +49,8 @@ from ..utils.run import command_line
|
|
47
49
|
from .context import DAGContext
|
48
50
|
from .validate import validate
|
49
51
|
|
52
|
+
logger = logging.getLogger(__name__)
|
53
|
+
|
50
54
|
|
51
55
|
def get_options_dict() -> dict[str, FFMpegOption]:
|
52
56
|
"""
|
@@ -54,6 +58,7 @@ def get_options_dict() -> dict[str, FFMpegOption]:
|
|
54
58
|
|
55
59
|
Returns:
|
56
60
|
Dictionary mapping option names to their FFMpegOption definitions
|
61
|
+
|
57
62
|
"""
|
58
63
|
options = load(list[FFMpegOption], "options")
|
59
64
|
return {option.name: option for option in options}
|
@@ -65,6 +70,7 @@ def get_filter_dict() -> dict[str, FFMpegFilter]:
|
|
65
70
|
|
66
71
|
Returns:
|
67
72
|
Dictionary mapping filter names to their FFMpegFilter definitions
|
73
|
+
|
68
74
|
"""
|
69
75
|
filters = load(list[FFMpegFilter], "filters")
|
70
76
|
return {filter.name: filter for filter in filters}
|
@@ -85,6 +91,7 @@ def parse_options(tokens: list[str]) -> dict[str, list[str | None | bool]]:
|
|
85
91
|
|
86
92
|
Returns:
|
87
93
|
Dictionary mapping option names to lists of their values
|
94
|
+
|
88
95
|
"""
|
89
96
|
parsed_options: dict[str, list[str | None | bool]] = defaultdict(list)
|
90
97
|
|
@@ -130,6 +137,8 @@ def parse_stream_selector(
|
|
130
137
|
|
131
138
|
Raises:
|
132
139
|
AssertionError: If the stream label is not found in the mapping
|
140
|
+
FFMpegValueError: If the stream type is unknown
|
141
|
+
|
133
142
|
"""
|
134
143
|
selector = selector.strip("[]")
|
135
144
|
|
@@ -178,6 +187,7 @@ def _is_filename(token: str) -> bool:
|
|
178
187
|
|
179
188
|
Returns:
|
180
189
|
True if the token is a filename, False otherwise
|
190
|
+
|
181
191
|
"""
|
182
192
|
# not start with - and has ext
|
183
193
|
return not token.startswith("-") and len(token.split(".")) > 1
|
@@ -201,6 +211,7 @@ def parse_output(
|
|
201
211
|
|
202
212
|
Returns:
|
203
213
|
List of OutputStream objects representing the output specifications
|
214
|
+
|
204
215
|
"""
|
205
216
|
tokens = source.copy()
|
206
217
|
|
@@ -268,6 +279,7 @@ def parse_input(
|
|
268
279
|
|
269
280
|
Returns:
|
270
281
|
Dictionary mapping input indices to their FilterableStream objects
|
282
|
+
|
271
283
|
"""
|
272
284
|
output: list[AVStream] = []
|
273
285
|
|
@@ -321,6 +333,10 @@ def parse_filter_complex(
|
|
321
333
|
|
322
334
|
Returns:
|
323
335
|
Updated stream mapping with new filter outputs added
|
336
|
+
|
337
|
+
Raises:
|
338
|
+
FFMpegValueError: If the stream type is unknown
|
339
|
+
|
324
340
|
"""
|
325
341
|
# Use re.split with negative lookbehind to handle escaped semicolons
|
326
342
|
filter_units = re.split(r"(?<!\\);", filter_complex)
|
@@ -421,6 +437,7 @@ def parse_global(
|
|
421
437
|
Example:
|
422
438
|
For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
|
423
439
|
Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
|
440
|
+
|
424
441
|
"""
|
425
442
|
options = parse_options(tokens[: tokens.index("-i")])
|
426
443
|
remaining_tokens = tokens[tokens.index("-i") :]
|
@@ -462,6 +479,7 @@ def parse(cli: str) -> Stream:
|
|
462
479
|
"ffmpeg -i input.mp4 -filter_complex '[0:v]scale=1280:720[v]' -map '[v]' output.mp4"
|
463
480
|
)
|
464
481
|
```
|
482
|
+
|
465
483
|
"""
|
466
484
|
# ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
|
467
485
|
ffmpeg_options = get_options_dict()
|
@@ -502,7 +520,9 @@ def parse(cli: str) -> Stream:
|
|
502
520
|
return result
|
503
521
|
|
504
522
|
|
505
|
-
def compile(
|
523
|
+
def compile(
|
524
|
+
stream: Stream, auto_fix: bool = True, use_filter_complex_script: bool = False
|
525
|
+
) -> str:
|
506
526
|
"""
|
507
527
|
Compile a stream into a command-line string.
|
508
528
|
|
@@ -512,14 +532,21 @@ def compile(stream: Stream, auto_fix: bool = True) -> str:
|
|
512
532
|
Args:
|
513
533
|
stream: The Stream object to compile into a command-line string
|
514
534
|
auto_fix: Whether to automatically fix issues in the stream
|
535
|
+
use_filter_complex_script: If True, use -filter_complex_script with a
|
536
|
+
temporary file instead of -filter_complex
|
515
537
|
|
516
538
|
Returns:
|
517
539
|
A command-line string that can be passed to FFmpeg
|
540
|
+
|
518
541
|
"""
|
519
|
-
return "ffmpeg " + command_line(
|
542
|
+
return "ffmpeg " + command_line(
|
543
|
+
compile_as_list(stream, auto_fix, use_filter_complex_script)
|
544
|
+
)
|
520
545
|
|
521
546
|
|
522
|
-
def compile_as_list(
|
547
|
+
def compile_as_list(
|
548
|
+
stream: Stream, auto_fix: bool = True, use_filter_complex_script: bool = False
|
549
|
+
) -> list[str]:
|
523
550
|
"""
|
524
551
|
Compile a stream into a list of FFmpeg command-line arguments.
|
525
552
|
|
@@ -545,6 +572,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
|
545
572
|
- Properly labels all streams
|
546
573
|
- Maintains correct filter chain order
|
547
574
|
- Handles stream splitting and merging
|
575
|
+
- If use_filter_complex_script is True, creates a temporary file
|
576
|
+
with the filter complex content and uses -filter_complex_script
|
548
577
|
|
549
578
|
5. Output Files: Processes destination files
|
550
579
|
- File paths and output options
|
@@ -560,6 +589,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
|
560
589
|
stream: The Stream object to compile into arguments
|
561
590
|
auto_fix: Whether to automatically fix issues in the stream
|
562
591
|
(e.g., reconnecting disconnected nodes)
|
592
|
+
use_filter_complex_script: If True, use -filter_complex_script with a
|
593
|
+
temporary file instead of -filter_complex
|
563
594
|
|
564
595
|
Returns:
|
565
596
|
A list of strings representing FFmpeg command-line arguments
|
@@ -580,8 +611,8 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
|
580
611
|
args
|
581
612
|
) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
|
582
613
|
```
|
583
|
-
"""
|
584
614
|
|
615
|
+
"""
|
585
616
|
stream = validate(stream, auto_fix=auto_fix)
|
586
617
|
node = stream.node
|
587
618
|
context = DAGContext.build(node)
|
@@ -605,7 +636,19 @@ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
|
|
605
636
|
vf_commands += ["".join(get_args(node, context))]
|
606
637
|
|
607
638
|
if vf_commands:
|
608
|
-
|
639
|
+
filter_complex_content = ";".join(vf_commands)
|
640
|
+
|
641
|
+
if use_filter_complex_script:
|
642
|
+
# Create a temporary file with the filter complex content
|
643
|
+
with tempfile.NamedTemporaryFile(
|
644
|
+
mode="w", suffix=".txt", delete=False
|
645
|
+
) as f:
|
646
|
+
f.write(filter_complex_content)
|
647
|
+
temp_filename = f.name
|
648
|
+
|
649
|
+
commands += ["-filter_complex_script", temp_filename]
|
650
|
+
else:
|
651
|
+
commands += ["-filter_complex", filter_complex_content]
|
609
652
|
|
610
653
|
# compile the output nodes
|
611
654
|
output_nodes = [node for node in context.all_nodes if isinstance(node, OutputNode)]
|
@@ -643,6 +686,7 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
|
643
686
|
|
644
687
|
Raises:
|
645
688
|
FFMpegValueError: If the stream has an unknown type or node type
|
689
|
+
|
646
690
|
"""
|
647
691
|
from ..streams.audio import AudioStream
|
648
692
|
from ..streams.av import AVStream
|
@@ -713,8 +757,8 @@ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
|
|
713
757
|
Example:
|
714
758
|
For a scale filter with width=1280 and height=720, this might return:
|
715
759
|
['[0:v]', 'scale=', 'width=1280:height=720', '[s0]']
|
716
|
-
"""
|
717
760
|
|
761
|
+
"""
|
718
762
|
incoming_labels = "".join(f"[{get_stream_label(k, context)}]" for k in node.inputs)
|
719
763
|
outputs = context.get_outgoing_streams(node)
|
720
764
|
|
@@ -766,6 +810,7 @@ def get_args_input_node(node: InputNode, context: DAGContext) -> list[str]:
|
|
766
810
|
Example:
|
767
811
|
For an input file "input.mp4" with options like seeking to 10 seconds:
|
768
812
|
['-ss', '10', '-i', 'input.mp4']
|
813
|
+
|
769
814
|
"""
|
770
815
|
commands = []
|
771
816
|
for key, value in node.kwargs.items():
|
@@ -799,6 +844,7 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
|
|
799
844
|
Example:
|
800
845
|
For an output file "output.mp4" with H.264 video codec:
|
801
846
|
['-map', '[v0]', '-c:v', 'libx264', 'output.mp4']
|
847
|
+
|
802
848
|
"""
|
803
849
|
# !handle mapping
|
804
850
|
commands = []
|
@@ -860,6 +906,7 @@ def get_args_global_node(node: GlobalNode, context: DAGContext) -> list[str]:
|
|
860
906
|
Example:
|
861
907
|
For global options like overwrite and quiet logging:
|
862
908
|
['-y', '-loglevel', 'quiet']
|
909
|
+
|
863
910
|
"""
|
864
911
|
commands = []
|
865
912
|
for key, value in node.kwargs.items():
|
@@ -891,8 +938,8 @@ def get_args(node: Node, context: DAGContext | None = None) -> list[str]:
|
|
891
938
|
|
892
939
|
Raises:
|
893
940
|
FFMpegValueError: If the node type is not recognized
|
894
|
-
"""
|
895
941
|
|
942
|
+
"""
|
896
943
|
context = context or DAGContext.build(node)
|
897
944
|
|
898
945
|
match node:
|
@@ -927,8 +974,8 @@ def get_node_label(node: Node, context: DAGContext) -> str:
|
|
927
974
|
|
928
975
|
Raises:
|
929
976
|
AssertionError: If the node is not an InputNode or FilterNode
|
930
|
-
"""
|
931
977
|
|
978
|
+
"""
|
932
979
|
node_id = context.node_ids[node]
|
933
980
|
match node:
|
934
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]
|