typed-ffmpeg-compatible 2.6.0__tar.gz → 2.6.2__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/PKG-INFO +1 -1
  2. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/pyproject.toml +1 -1
  3. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/base.py +24 -5
  4. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/common/schema.py +25 -9
  5. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/common/serialize.py +9 -4
  6. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/__init__.py +8 -1
  7. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/context.py +7 -2
  8. typed_ffmpeg_compatible-2.6.2/src/typed_ffmpeg/dag/factory.py +51 -0
  9. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/global_runnable/global_args.py +1 -2
  10. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/global_runnable/runnable.py +9 -3
  11. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/io/_input.py +3 -1
  12. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/io/_output.py +3 -1
  13. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/io/output_args.py +7 -4
  14. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/nodes.py +55 -26
  15. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/schema.py +3 -3
  16. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/validate.py +26 -8
  17. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/exceptions.py +1 -1
  18. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/filters.py +304 -72
  19. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/info.py +9 -3
  20. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/probe.py +9 -2
  21. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/schema.py +0 -1
  22. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/streams/audio.py +697 -214
  23. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/streams/video.py +1400 -401
  24. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/escaping.py +6 -5
  25. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/run.py +1 -1
  26. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/snapshot.py +6 -1
  27. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/view.py +7 -2
  28. typed_ffmpeg_compatible-2.6.0/src/typed_ffmpeg/dag/factory.py +0 -31
  29. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/LICENSE +0 -0
  30. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/README.md +0 -0
  31. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/__init__.py +0 -0
  32. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/common/__init__.py +0 -0
  33. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/compile.py +0 -0
  34. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
  35. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/io/__init__.py +0 -0
  36. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/dag/utils.py +0 -0
  37. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/py.typed +0 -0
  38. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/streams/__init__.py +0 -0
  39. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/streams/av.py +0 -0
  40. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/streams/channel_layout.py +0 -0
  41. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/types.py +0 -0
  42. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/__init__.py +0 -0
  43. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
  44. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/lazy_eval/operator.py +0 -0
  45. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/lazy_eval/schema.py +0 -0
  46. {typed_ffmpeg_compatible-2.6.0 → typed_ffmpeg_compatible-2.6.2}/src/typed_ffmpeg/utils/typing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: typed-ffmpeg-compatible
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
5
5
  Home-page: https://livingbio.github.io/typed-ffmpeg/
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "typed-ffmpeg-compatible"
3
- version = "2.6.0"
3
+ version = "2.6.2"
4
4
  description = "Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation."
5
5
  authors = ["lucemia <lucemia@gmail.com>"]
6
6
  readme = "README.md"
@@ -6,7 +6,13 @@ from typing import Any
6
6
 
7
7
  from .dag.io._input import input
8
8
  from .dag.io._output import output
9
- from .dag.nodes import FilterableStream, FilterNode, GlobalNode, GlobalStream, OutputStream
9
+ from .dag.nodes import (
10
+ FilterableStream,
11
+ FilterNode,
12
+ GlobalNode,
13
+ GlobalStream,
14
+ OutputStream,
15
+ )
10
16
  from .schema import StreamType
11
17
  from .streams.audio import AudioStream
12
18
  from .streams.video import VideoStream
@@ -26,7 +32,10 @@ def merge_outputs(*streams: OutputStream) -> GlobalStream:
26
32
 
27
33
 
28
34
  def vfilter(
29
- *streams: FilterableStream, name: str, input_typings: tuple[StreamType, ...] = (StreamType.video,), **kwargs: Any
35
+ *streams: FilterableStream,
36
+ name: str,
37
+ input_typings: tuple[StreamType, ...] = (StreamType.video,),
38
+ **kwargs: Any,
30
39
  ) -> VideoStream:
31
40
  """
32
41
  Apply a custom video filter which has only one output to this stream
@@ -53,7 +62,10 @@ def vfilter(
53
62
 
54
63
 
55
64
  def afilter(
56
- *streams: FilterableStream, name: str, input_typings: tuple[StreamType, ...] = (StreamType.audio,), **kwargs: Any
65
+ *streams: FilterableStream,
66
+ name: str,
67
+ input_typings: tuple[StreamType, ...] = (StreamType.audio,),
68
+ **kwargs: Any,
57
69
  ) -> AudioStream:
58
70
  """
59
71
  Apply a custom audio filter which has only one output to this stream
@@ -84,7 +96,7 @@ def filter_multi_output(
84
96
  name: str,
85
97
  input_typings: tuple[StreamType, ...] = (),
86
98
  output_tyings: tuple[StreamType, ...] = (),
87
- **kwargs: Any
99
+ **kwargs: Any,
88
100
  ) -> FilterNode:
89
101
  """
90
102
  Apply a custom filter which has multiple outputs to this stream
@@ -111,4 +123,11 @@ def filter_multi_output(
111
123
  )
112
124
 
113
125
 
114
- __all__ = ["input", "output", "merge_outputs", "vfilter", "afilter", "filter_multi_output"]
126
+ __all__ = [
127
+ "input",
128
+ "output",
129
+ "merge_outputs",
130
+ "vfilter",
131
+ "afilter",
132
+ "filter_multi_output",
133
+ ]
@@ -118,8 +118,10 @@ class FFMpegFilter:
118
118
  def to_def(self) -> FFMpegFilterDef:
119
119
  return FFMpegFilterDef(
120
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),
121
+ typings_input=self.formula_typings_input
122
+ or tuple(k.type.value for k in self.stream_typings_input),
123
+ typings_output=self.formula_typings_output
124
+ or tuple(k.type.value for k in self.stream_typings_output),
123
125
  )
124
126
 
125
127
  @property
@@ -131,13 +133,18 @@ class FFMpegFilter:
131
133
  else:
132
134
  assert self.formula_typings_input, f"{self.name} has no input"
133
135
  if "video" not in self.formula_typings_input:
134
- assert "audio" in self.formula_typings_input, f"{self.name} has no video input"
136
+ assert "audio" in self.formula_typings_input, (
137
+ f"{self.name} has no video input"
138
+ )
135
139
  return {StreamType.audio}
136
140
  elif "audio" not in self.formula_typings_input:
137
- assert "video" in self.formula_typings_input, f"{self.name} has no audio input"
141
+ assert "video" in self.formula_typings_input, (
142
+ f"{self.name} has no audio input"
143
+ )
138
144
  return {StreamType.video}
139
145
  assert (
140
- "video" in self.formula_typings_input and "audio" in self.formula_typings_input
146
+ "video" in self.formula_typings_input
147
+ and "audio" in self.formula_typings_input
141
148
  ), f"{self.name} has no video or audio input"
142
149
  return {StreamType.video, StreamType.audio}
143
150
 
@@ -150,13 +157,18 @@ class FFMpegFilter:
150
157
  else:
151
158
  assert self.formula_typings_output, f"{self.name} has no output"
152
159
  if "video" not in self.formula_typings_output:
153
- assert "audio" in self.formula_typings_output, f"{self.name} has no video output"
160
+ assert "audio" in self.formula_typings_output, (
161
+ f"{self.name} has no video output"
162
+ )
154
163
  return {StreamType.audio}
155
164
  elif "audio" not in self.formula_typings_output:
156
- assert "video" in self.formula_typings_output, f"{self.name} has no audio output"
165
+ assert "video" in self.formula_typings_output, (
166
+ f"{self.name} has no audio output"
167
+ )
157
168
  return {StreamType.video}
158
169
  assert (
159
- "video" in self.formula_typings_output and "audio" in self.formula_typings_output
170
+ "video" in self.formula_typings_output
171
+ and "audio" in self.formula_typings_output
160
172
  ), f"{self.name} has no video or audio output"
161
173
  return {StreamType.video, StreamType.audio}
162
174
 
@@ -301,7 +313,11 @@ class FFMpegOption:
301
313
 
302
314
  @property
303
315
  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)
316
+ return (
317
+ not self.is_input_option
318
+ and not self.is_output_option
319
+ and not (self.flags & FFMpegOptionFlag.OPT_EXIT)
320
+ )
305
321
 
306
322
  @property
307
323
  def is_support_stream_specifier(self) -> bool:
@@ -1,4 +1,4 @@
1
- from __future__ import absolute_import, annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import importlib
4
4
  import json
@@ -21,7 +21,9 @@ def load_class(path: str, strict: bool = True) -> Any:
21
21
  The class.
22
22
  """
23
23
  if strict:
24
- assert path.startswith("ffmpeg."), f"Only support loading class from ffmpeg package: {path}"
24
+ assert path.startswith("ffmpeg."), (
25
+ f"Only support loading class from ffmpeg package: {path}"
26
+ )
25
27
 
26
28
  module_path, class_name = path.rsplit(".", 1)
27
29
  module = importlib.import_module(module_path)
@@ -65,7 +67,7 @@ def object_hook(obj: Any, strict: bool = True) -> Any:
65
67
  # NOTE: in our application, the dataclass is always frozen
66
68
  return cls(**{k: frozen(v) for k, v in obj.items()})
67
69
 
68
- return cls(**{k: v for k, v in obj.items()})
70
+ return cls(**dict(obj.items()))
69
71
 
70
72
  return obj
71
73
 
@@ -107,7 +109,10 @@ def to_dict_with_class_info(instance: Any) -> Any:
107
109
  elif is_dataclass(instance):
108
110
  return {
109
111
  "__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)},
112
+ **{
113
+ k.name: to_dict_with_class_info(getattr(instance, k.name))
114
+ for k in fields(instance)
115
+ },
111
116
  }
112
117
  elif isinstance(instance, Enum):
113
118
  return {
@@ -1,4 +1,11 @@
1
- from .nodes import FilterableStream, FilterNode, GlobalNode, InputNode, OutputNode, OutputStream
1
+ from .nodes import (
2
+ FilterableStream,
3
+ FilterNode,
4
+ GlobalNode,
5
+ InputNode,
6
+ OutputNode,
7
+ OutputStream,
8
+ )
2
9
  from .schema import Node, Stream
3
10
 
4
11
  __all__ = [
@@ -105,7 +105,10 @@ class DAGContext:
105
105
  """
106
106
  All streams in the graph sorted by the number of upstream nodes and the index of the stream.
107
107
  """
108
- return sorted(self.streams, key=lambda stream: (len(stream.node.upstream_nodes), stream.index))
108
+ return sorted(
109
+ self.streams,
110
+ key=lambda stream: (len(stream.node.upstream_nodes), stream.index),
111
+ )
109
112
 
110
113
  @cached_property
111
114
  def outgoing_nodes(self) -> dict[Stream, list[tuple[Node, int]]]:
@@ -180,7 +183,9 @@ class DAGContext:
180
183
  The label of the node.
181
184
  """
182
185
 
183
- assert isinstance(node, (InputNode, FilterNode)), "Only input and filter nodes have labels"
186
+ assert isinstance(node, (InputNode, FilterNode)), (
187
+ "Only input and filter nodes have labels"
188
+ )
184
189
  return self.node_labels[node]
185
190
 
186
191
  @override
@@ -0,0 +1,51 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ from ..common.schema import FFMpegFilterDef, StreamType
5
+ from ..schema import Auto
6
+ from ..utils.run import ignore_default
7
+ from .nodes import FilterableStream, FilterNode
8
+
9
+
10
+ def filter_node_factory(
11
+ ffmpeg_filter_def: FFMpegFilterDef, *inputs: FilterableStream, **kwargs: Any
12
+ ) -> FilterNode:
13
+ for k, v in kwargs.items():
14
+ if isinstance(v, Auto):
15
+ kwargs[k] = eval(
16
+ v, {"StreamType": StreamType, "re": re, **kwargs, "streams": inputs}
17
+ )
18
+
19
+ if isinstance(ffmpeg_filter_def.typings_input, str):
20
+ input_typings = tuple(
21
+ eval(
22
+ ffmpeg_filter_def.typings_input,
23
+ {"StreamType": StreamType, "re": re, **kwargs},
24
+ )
25
+ )
26
+ else:
27
+ input_typings = tuple(
28
+ StreamType.video if k == "video" else StreamType.audio
29
+ for k in ffmpeg_filter_def.typings_input
30
+ )
31
+
32
+ if isinstance(ffmpeg_filter_def.typings_output, str):
33
+ output_typings = tuple(
34
+ eval(
35
+ ffmpeg_filter_def.typings_output,
36
+ {"StreamType": StreamType, "re": re, **kwargs},
37
+ )
38
+ )
39
+ else:
40
+ output_typings = tuple(
41
+ StreamType.video if k == "video" else StreamType.audio
42
+ for k in ffmpeg_filter_def.typings_output
43
+ )
44
+
45
+ return FilterNode(
46
+ name=ffmpeg_filter_def.name,
47
+ input_typings=input_typings,
48
+ output_typings=output_typings,
49
+ inputs=inputs,
50
+ kwargs=ignore_default(kwargs),
51
+ )
@@ -12,8 +12,7 @@ if TYPE_CHECKING:
12
12
 
13
13
  class GlobalArgs(ABC):
14
14
  @abstractmethod
15
- def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
16
- ...
15
+ def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode: ...
17
16
 
18
17
  def global_args(
19
18
  self,
@@ -82,7 +82,9 @@ class GlobalRunable(GlobalArgs):
82
82
  Returns:
83
83
  the command-line
84
84
  """
85
- return command_line(self.compile(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix))
85
+ return command_line(
86
+ self.compile(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix)
87
+ )
86
88
 
87
89
  def run_async(
88
90
  self,
@@ -115,7 +117,9 @@ class GlobalRunable(GlobalArgs):
115
117
  stdout_stream = subprocess.PIPE if pipe_stdout or quiet else None
116
118
  stderr_stream = subprocess.PIPE if pipe_stderr or quiet else None
117
119
 
118
- logger.info(f"Running command: {self.compile_line(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix)}")
120
+ logger.info(
121
+ f"Running command: {self.compile_line(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix)}"
122
+ )
119
123
 
120
124
  return subprocess.Popen(
121
125
  args,
@@ -166,7 +170,9 @@ class GlobalRunable(GlobalArgs):
166
170
  if retcode:
167
171
  raise FFMpegExecuteError(
168
172
  retcode=retcode,
169
- cmd=self.compile_line(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix),
173
+ cmd=self.compile_line(
174
+ cmd, overwrite_output=overwrite_output, auto_fix=auto_fix
175
+ ),
170
176
  stdout=stdout,
171
177
  stderr=stderr,
172
178
  )
@@ -194,4 +194,6 @@ def input(
194
194
  if v is not None
195
195
  }
196
196
 
197
- return InputNode(filename=str(filename), kwargs=tuple((options | (extra_options or {})).items())).stream()
197
+ return InputNode(
198
+ filename=str(filename), kwargs=tuple((options | (extra_options or {})).items())
199
+ ).stream()
@@ -316,5 +316,7 @@ def output(
316
316
  }
317
317
 
318
318
  return OutputNode(
319
- inputs=streams, filename=str(filename), kwargs=tuple((options | (extra_options or {})).items())
319
+ inputs=streams,
320
+ filename=str(filename),
321
+ kwargs=tuple((options | (extra_options or {})).items()),
320
322
  ).stream()
@@ -13,12 +13,13 @@ if TYPE_CHECKING:
13
13
 
14
14
  class OutputArgs(ABC):
15
15
  @abstractmethod
16
- def _output_node(self, *streams: FilterableStream, filename: str | Path, **kwargs: Any) -> OutputNode:
17
- ...
16
+ def _output_node(
17
+ self, *streams: FilterableStream, filename: str | Path, **kwargs: Any
18
+ ) -> OutputNode: ...
18
19
 
19
20
  def output(
20
21
  self,
21
- *streams: "FilterableStream",
22
+ *streams: FilterableStream,
22
23
  filename: str | Path,
23
24
  f: String = None,
24
25
  c: String = None,
@@ -324,4 +325,6 @@ class OutputArgs(ABC):
324
325
  if v is not None
325
326
  }
326
327
 
327
- return self._output_node(*streams, filename=filename, **options, **(extra_options or {})).stream()
328
+ return self._output_node(
329
+ *streams, filename=filename, **options, **(extra_options or {})
330
+ ).stream()
@@ -55,7 +55,7 @@ class FilterNode(Node):
55
55
  def repr(self) -> str:
56
56
  return self.name
57
57
 
58
- def video(self, index: int) -> "VideoStream":
58
+ def video(self, index: int) -> VideoStream:
59
59
  """
60
60
  Return the video stream at the specified index
61
61
 
@@ -67,12 +67,16 @@ class FilterNode(Node):
67
67
  """
68
68
  from ..streams.video import VideoStream
69
69
 
70
- video_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.video]
70
+ video_outputs = [
71
+ i for i, k in enumerate(self.output_typings) if k == StreamType.video
72
+ ]
71
73
  if not len(video_outputs) > index:
72
- raise FFMpegValueError(f"Specified index {index} is out of range for video outputs {len(video_outputs)}")
74
+ raise FFMpegValueError(
75
+ f"Specified index {index} is out of range for video outputs {len(video_outputs)}"
76
+ )
73
77
  return VideoStream(node=self, index=video_outputs[index])
74
78
 
75
- def audio(self, index: int) -> "AudioStream":
79
+ def audio(self, index: int) -> AudioStream:
76
80
  """
77
81
  Return the audio stream at the specified index
78
82
 
@@ -84,9 +88,13 @@ class FilterNode(Node):
84
88
  """
85
89
  from ..streams.audio import AudioStream
86
90
 
87
- audio_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.audio]
91
+ audio_outputs = [
92
+ i for i, k in enumerate(self.output_typings) if k == StreamType.audio
93
+ ]
88
94
  if not len(audio_outputs) > index:
89
- raise FFMpegValueError(f"Specified index {index} is out of range for audio outputs {len(audio_outputs)}")
95
+ raise FFMpegValueError(
96
+ f"Specified index {index} is out of range for audio outputs {len(audio_outputs)}"
97
+ )
90
98
 
91
99
  return AudioStream(node=self, index=audio_outputs[index])
92
100
 
@@ -97,12 +105,16 @@ class FilterNode(Node):
97
105
  super().__post_init__()
98
106
 
99
107
  if len(self.inputs) != len(self.input_typings):
100
- raise FFMpegValueError(f"Expected {len(self.input_typings)} inputs, got {len(self.inputs)}")
108
+ raise FFMpegValueError(
109
+ f"Expected {len(self.input_typings)} inputs, got {len(self.inputs)}"
110
+ )
101
111
 
102
112
  stream: FilterableStream
103
113
  expected_type: StreamType
104
114
 
105
- for i, (stream, expected_type) in enumerate(zip(self.inputs, self.input_typings)):
115
+ for i, (stream, expected_type) in enumerate(
116
+ zip(self.inputs, self.input_typings)
117
+ ):
106
118
  if expected_type == StreamType.video:
107
119
  if not isinstance(stream, VideoStream):
108
120
  raise FFMpegTypeError(
@@ -132,7 +144,9 @@ class FilterNode(Node):
132
144
 
133
145
  commands = []
134
146
  for key, value in self.kwargs:
135
- assert not isinstance(value, LazyValue), f"LazyValue should have been evaluated: {key}={value}"
147
+ assert not isinstance(value, LazyValue), (
148
+ f"LazyValue should have been evaluated: {key}={value}"
149
+ )
136
150
 
137
151
  # Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
138
152
  if isinstance(value, bool):
@@ -142,7 +156,12 @@ class FilterNode(Node):
142
156
  commands += [f"{key}={escape(value)}"]
143
157
 
144
158
  if commands:
145
- return [incoming_labels] + [f"{self.name}="] + [escape(":".join(commands), "\\'[],;")] + [outgoing_labels]
159
+ return (
160
+ [incoming_labels]
161
+ + [f"{self.name}="]
162
+ + [escape(":".join(commands), "\\'[],;")]
163
+ + [outgoing_labels]
164
+ )
146
165
  return [incoming_labels] + [f"{self.name}"] + [outgoing_labels]
147
166
 
148
167
 
@@ -152,10 +171,12 @@ class FilterableStream(Stream, OutputArgs):
152
171
  A stream that can be used as input to a filter
153
172
  """
154
173
 
155
- node: "FilterNode | InputNode"
174
+ node: FilterNode | InputNode
156
175
 
157
176
  @override
158
- def _output_node(self, *streams: FilterableStream, filename: str | Path, **kwargs: Any) -> OutputNode:
177
+ def _output_node(
178
+ self, *streams: FilterableStream, filename: str | Path, **kwargs: Any
179
+ ) -> OutputNode:
159
180
  """
160
181
  Output the streams to a file URL
161
182
 
@@ -167,15 +188,19 @@ class FilterableStream(Stream, OutputArgs):
167
188
  Returns:
168
189
  the output stream
169
190
  """
170
- return OutputNode(inputs=(self, *streams), filename=str(filename), kwargs=tuple(kwargs.items()))
191
+ return OutputNode(
192
+ inputs=(self, *streams),
193
+ filename=str(filename),
194
+ kwargs=tuple(kwargs.items()),
195
+ )
171
196
 
172
197
  def vfilter(
173
198
  self,
174
- *streams: "FilterableStream",
199
+ *streams: FilterableStream,
175
200
  name: str,
176
201
  input_typings: tuple[StreamType, ...] = (StreamType.video,),
177
202
  **kwargs: Any,
178
- ) -> "VideoStream":
203
+ ) -> VideoStream:
179
204
  """
180
205
  Apply a custom video filter which has only one output to this stream
181
206
 
@@ -198,11 +223,11 @@ class FilterableStream(Stream, OutputArgs):
198
223
 
199
224
  def afilter(
200
225
  self,
201
- *streams: "FilterableStream",
226
+ *streams: FilterableStream,
202
227
  name: str,
203
228
  input_typings: tuple[StreamType, ...] = (StreamType.audio,),
204
229
  **kwargs: Any,
205
- ) -> "AudioStream":
230
+ ) -> AudioStream:
206
231
  """
207
232
  Apply a custom audio filter which has only one output to this stream
208
233
 
@@ -225,12 +250,12 @@ class FilterableStream(Stream, OutputArgs):
225
250
 
226
251
  def filter_multi_output(
227
252
  self,
228
- *streams: "FilterableStream",
253
+ *streams: FilterableStream,
229
254
  name: str,
230
255
  input_typings: tuple[StreamType, ...] = (),
231
256
  output_typings: tuple[StreamType, ...] = (),
232
257
  **kwargs: Any,
233
- ) -> "FilterNode":
258
+ ) -> FilterNode:
234
259
  """
235
260
  Apply a custom filter which has multiple outputs to this stream
236
261
 
@@ -277,13 +302,17 @@ class FilterableStream(Stream, OutputArgs):
277
302
  return f"{context.get_node_label(self.node)}:v"
278
303
  elif isinstance(self, AudioStream):
279
304
  return f"{context.get_node_label(self.node)}:a"
280
- raise FFMpegValueError(f"Unknown stream type: {self.__class__.__name__}") # pragma: no cover
305
+ raise FFMpegValueError(
306
+ f"Unknown stream type: {self.__class__.__name__}"
307
+ ) # pragma: no cover
281
308
 
282
309
  if isinstance(self.node, FilterNode):
283
310
  if len(self.node.output_typings) > 1:
284
311
  return f"{context.get_node_label(self.node)}#{self.index}"
285
312
  return f"{context.get_node_label(self.node)}"
286
- raise FFMpegValueError(f"Unknown node type: {self.node.__class__.__name__}") # pragma: no cover
313
+ raise FFMpegValueError(
314
+ f"Unknown node type: {self.node.__class__.__name__}"
315
+ ) # pragma: no cover
287
316
 
288
317
  def __post_init__(self) -> None:
289
318
  if isinstance(self.node, InputNode):
@@ -310,7 +339,7 @@ class InputNode(Node):
310
339
  return os.path.basename(self.filename)
311
340
 
312
341
  @property
313
- def video(self) -> "VideoStream":
342
+ def video(self) -> VideoStream:
314
343
  """
315
344
  Return the video stream of this node
316
345
 
@@ -322,7 +351,7 @@ class InputNode(Node):
322
351
  return VideoStream(node=self)
323
352
 
324
353
  @property
325
- def audio(self) -> "AudioStream":
354
+ def audio(self) -> AudioStream:
326
355
  """
327
356
  Return the audio stream of this node
328
357
 
@@ -333,7 +362,7 @@ class InputNode(Node):
333
362
 
334
363
  return AudioStream(node=self)
335
364
 
336
- def stream(self) -> "AVStream":
365
+ def stream(self) -> AVStream:
337
366
  """
338
367
  Return the output stream of this node
339
368
 
@@ -371,7 +400,7 @@ class OutputNode(Node):
371
400
  def repr(self) -> str:
372
401
  return os.path.basename(self.filename)
373
402
 
374
- def stream(self) -> "OutputStream":
403
+ def stream(self) -> OutputStream:
375
404
  """
376
405
  Return the output stream of this node
377
406
 
@@ -437,7 +466,7 @@ class GlobalNode(Node):
437
466
  def repr(self) -> str:
438
467
  return " ".join(self.get_args())
439
468
 
440
- def stream(self) -> "GlobalStream":
469
+ def stream(self) -> GlobalStream:
441
470
  """
442
471
  Return the output stream of this node
443
472
 
@@ -70,7 +70,7 @@ class Stream(HashableBaseModel):
70
70
  return f.read()
71
71
 
72
72
  def _repr_svg_(self) -> str: # pragma: no cover
73
- with open(self.view(format="svg"), "r") as f:
73
+ with open(self.view(format="svg")) as f:
74
74
  return f.read()
75
75
 
76
76
 
@@ -109,7 +109,7 @@ class Node(HashableBaseModel, ABC):
109
109
 
110
110
  nodes.extend(k.node for k in node.inputs)
111
111
 
112
- output[node.hex] = set(k.node.hex for k in node.inputs)
112
+ output[node.hex] = {k.node.hex for k in node.inputs}
113
113
 
114
114
  if not is_dag(output):
115
115
  raise ValueError(f"Graph is not a DAG: {output}") # pragma: no cover
@@ -206,5 +206,5 @@ class Node(HashableBaseModel, ABC):
206
206
  return f.read()
207
207
 
208
208
  def _repr_svg_(self) -> str: # pragma: no cover
209
- with open(self.view(format="svg"), "r") as f:
209
+ with open(self.view(format="svg")) as f:
210
210
  return f.read()