typed-ffmpeg-compatible 2.6.3__py3-none-any.whl → 2.7.0__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.
Files changed (35) hide show
  1. typed_ffmpeg/__init__.py +24 -0
  2. typed_ffmpeg/base.py +87 -29
  3. typed_ffmpeg/common/schema.py +285 -4
  4. typed_ffmpeg/common/serialize.py +149 -34
  5. typed_ffmpeg/dag/__init__.py +13 -0
  6. typed_ffmpeg/dag/compile.py +39 -4
  7. typed_ffmpeg/dag/context.py +137 -29
  8. typed_ffmpeg/dag/factory.py +27 -0
  9. typed_ffmpeg/dag/global_runnable/global_args.py +11 -0
  10. typed_ffmpeg/dag/global_runnable/runnable.py +143 -34
  11. typed_ffmpeg/dag/io/_input.py +2 -1
  12. typed_ffmpeg/dag/io/_output.py +2 -1
  13. typed_ffmpeg/dag/nodes.py +403 -71
  14. typed_ffmpeg/dag/schema.py +5 -2
  15. typed_ffmpeg/dag/utils.py +29 -8
  16. typed_ffmpeg/dag/validate.py +83 -20
  17. typed_ffmpeg/exceptions.py +42 -9
  18. typed_ffmpeg/info.py +137 -16
  19. typed_ffmpeg/probe.py +31 -6
  20. typed_ffmpeg/schema.py +32 -5
  21. typed_ffmpeg/streams/channel_layout.py +13 -0
  22. typed_ffmpeg/utils/escaping.py +47 -7
  23. typed_ffmpeg/utils/forzendict.py +108 -0
  24. typed_ffmpeg/utils/lazy_eval/operator.py +43 -1
  25. typed_ffmpeg/utils/lazy_eval/schema.py +122 -6
  26. typed_ffmpeg/utils/run.py +44 -7
  27. typed_ffmpeg/utils/snapshot.py +36 -1
  28. typed_ffmpeg/utils/typing.py +29 -4
  29. typed_ffmpeg/utils/view.py +46 -4
  30. {typed_ffmpeg_compatible-2.6.3.dist-info → typed_ffmpeg_compatible-2.7.0.dist-info}/METADATA +1 -1
  31. typed_ffmpeg_compatible-2.7.0.dist-info/RECORD +48 -0
  32. typed_ffmpeg_compatible-2.6.3.dist-info/RECORD +0 -47
  33. {typed_ffmpeg_compatible-2.6.3.dist-info → typed_ffmpeg_compatible-2.7.0.dist-info}/LICENSE +0 -0
  34. {typed_ffmpeg_compatible-2.6.3.dist-info → typed_ffmpeg_compatible-2.7.0.dist-info}/WHEEL +0 -0
  35. {typed_ffmpeg_compatible-2.6.3.dist-info → typed_ffmpeg_compatible-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,14 @@
1
+ """
2
+ Serialization utilities for FFmpeg filter graphs and components.
3
+
4
+ This module provides functions for serializing and deserializing FFmpeg filter
5
+ graph components to and from JSON. It handles dataclasses, enums, and other
6
+ custom types used in the typed-ffmpeg library, enabling filter graphs to be
7
+ saved to disk and loaded back.
8
+ """
9
+
1
10
  from __future__ import annotations
2
11
 
3
- import importlib
4
12
  import json
5
13
  from dataclasses import fields, is_dataclass
6
14
  from enum import Enum
@@ -8,60 +16,128 @@ from functools import partial
8
16
  from pathlib import Path
9
17
  from typing import Any
10
18
 
19
+ from ..utils.forzendict import FrozenDict
20
+
21
+ CLASS_REGISTRY: dict[str, type[Serializable | Enum]] = {}
22
+ """
23
+ A registry of classes that have been loaded, and can be deserialized.
24
+ """
25
+
11
26
 
12
- def load_class(path: str, strict: bool = True) -> Any:
27
+ def serializable(
28
+ cls: type[Serializable] | type[Enum],
29
+ ) -> type[Serializable] | type[Enum]:
13
30
  """
14
- Load a class from a string path
31
+ Register a class with the serialization system.
32
+ """
33
+ assert cls.__name__ not in CLASS_REGISTRY, (
34
+ f"Class {cls.__name__} already registered"
35
+ )
36
+ CLASS_REGISTRY[cls.__name__] = cls
37
+
38
+ return cls
39
+
40
+
41
+ class Serializable:
42
+ """
43
+ A base class for all serializable classes.
44
+ """
45
+
46
+ def __init_subclass__(cls, **kwargs: Any) -> None:
47
+ super().__init_subclass__(**kwargs)
48
+ serializable(cls)
49
+
50
+
51
+ def load_class(name: str) -> type[Serializable] | type[Enum]:
52
+ """
53
+ Load a class from its name.
54
+
55
+ This function looks up a class by its name in the CLASS_REGISTRY. It's used during
56
+ deserialization to reconstruct objects from their class names.
15
57
 
16
58
  Args:
17
- path: The path to the class.
18
- strict: If True, raise an error if the class is not in ffmpeg package.
59
+ name: The simple class name (e.g., 'FilterNode')
19
60
 
20
61
  Returns:
21
- The class.
62
+ The class object that can be instantiated
63
+
64
+ Raises:
65
+ AssertionError: If the class name is not found in the registry
66
+
67
+ Example:
68
+ ```python
69
+ # Load the FilterNode class
70
+ FilterNode = load_class('FilterNode')
71
+ # Create an instance
72
+ node = FilterNode(name='scale', ...)
73
+ ```
22
74
  """
23
- if strict:
24
- assert path.startswith("ffmpeg."), (
25
- f"Only support loading class from ffmpeg package: {path}"
26
- )
27
-
28
- module_path, class_name = path.rsplit(".", 1)
29
- module = importlib.import_module(module_path)
30
- return getattr(module, class_name)
75
+ assert name in CLASS_REGISTRY, f"Class {name} not registered"
76
+ return CLASS_REGISTRY[name]
31
77
 
32
78
 
33
79
  def frozen(v: Any) -> Any:
34
80
  """
35
- Convert the instance to a frozen instance
81
+ Convert mutable data structures to immutable (frozen) equivalents.
82
+
83
+ This function recursively converts lists to tuples and dictionaries to
84
+ FrozenDict instances, ensuring that the resulting data structure is
85
+ completely immutable. This is important for dataclasses that are marked
86
+ as frozen, as they can only contain immutable data.
36
87
 
37
88
  Args:
38
- v: The instance to convert.
89
+ v: The value to convert, which may be a list, dict, or any other type
39
90
 
40
91
  Returns:
41
- The frozen instance.
92
+ An immutable version of the input value
93
+
94
+ Example:
95
+ ```python
96
+ # Convert a nested structure to immutable form
97
+ frozen_data = frozen(
98
+ {"options": ["option1", "option2"], "settings": {"key": "value"}}
99
+ )
100
+ # Result: FrozenDict with tuple instead of list and nested FrozenDict
101
+ ```
42
102
  """
43
103
  if isinstance(v, list):
44
104
  return tuple(frozen(i) for i in v)
45
105
 
46
106
  if isinstance(v, dict):
47
- return tuple((key, frozen(value)) for key, value in v.items())
107
+ return FrozenDict({k: frozen(v) for k, v in v.items()})
48
108
 
49
109
  return v
50
110
 
51
111
 
52
112
  def object_hook(obj: Any, strict: bool = True) -> Any:
53
113
  """
54
- Convert the dictionary to an instance
114
+ Custom JSON object hook for deserializing FFmpeg objects.
115
+
116
+ This function is used by the JSON decoder to convert dictionaries into
117
+ appropriate Python objects during deserialization. It looks for a special
118
+ '__class__' key that indicates the type of object to create.
55
119
 
56
120
  Args:
57
- obj: The dictionary to convert.
121
+ obj: A dictionary from the JSON parser
122
+ strict: If True, only allow loading classes from the ffmpeg package
58
123
 
59
124
  Returns:
60
- The instance.
125
+ Either the original dictionary or an instance of the specified class
126
+
127
+ Example:
128
+ ```python
129
+ # A JSON object with class information
130
+ json_obj = {
131
+ "__class__": "FilterNode",
132
+ "name": "scale",
133
+ "kwargs": {"width": 1280, "height": 720},
134
+ }
135
+ # Will be converted to a FilterNode instance
136
+ ```
61
137
  """
62
138
  if isinstance(obj, dict):
63
139
  if obj.get("__class__"):
64
- cls = load_class(obj.pop("__class__"), strict=strict)
140
+ cls = load_class(obj.pop("__class__"))
65
141
 
66
142
  if is_dataclass(cls):
67
143
  # NOTE: in our application, the dataclass is always frozen
@@ -74,13 +150,26 @@ def object_hook(obj: Any, strict: bool = True) -> Any:
74
150
 
75
151
  def loads(raw: str, strict: bool = True) -> Any:
76
152
  """
77
- Deserialize the JSON string to an instance
153
+ Deserialize a JSON string into Python objects with proper class types.
154
+
155
+ This function parses a JSON string and reconstructs the original Python
156
+ objects, including dataclasses and enums, based on class information
157
+ embedded in the JSON.
78
158
 
79
159
  Args:
80
- raw: The JSON string to deserialize.
160
+ raw: The JSON string to deserialize
161
+ strict: If True, only allow loading classes from the ffmpeg package
81
162
 
82
163
  Returns:
83
- The deserialized instance.
164
+ The deserialized Python object with proper types
165
+
166
+ Example:
167
+ ```python
168
+ # Deserialize a filter graph from JSON
169
+ json_str = '{"__class__": "FilterNode", "name": "scale", ...}'
170
+ filter_node = loads(json_str)
171
+ # filter_node is now a FilterNode instance
172
+ ```
84
173
  """
85
174
  object_hook_strict = partial(object_hook, strict=strict)
86
175
 
@@ -89,16 +178,29 @@ def loads(raw: str, strict: bool = True) -> Any:
89
178
 
90
179
  def to_dict_with_class_info(instance: Any) -> Any:
91
180
  """
92
- Convert the instance to a dictionary with class information
181
+ Convert Python objects to dictionaries with embedded class information.
182
+
183
+ This function recursively converts Python objects to dictionaries, lists,
184
+ and primitive types suitable for JSON serialization. For dataclasses and
185
+ enums, it adds a '__class__' key with the fully qualified class name,
186
+ allowing them to be reconstructed during deserialization.
93
187
 
94
188
  Args:
95
- instance: The instance to convert.
189
+ instance: The Python object to convert
96
190
 
97
191
  Returns:
98
- The dictionary with class information
192
+ A JSON-serializable representation with embedded class information
193
+
194
+ Example:
195
+ ```python
196
+ # Convert a FilterNode to a serializable dict
197
+ filter_node = FilterNode(name='scale', ...)
198
+ serializable = to_dict_with_class_info(filter_node)
199
+ # serializable now contains class information and all attributes
200
+ ```
99
201
  """
100
202
 
101
- if isinstance(instance, dict):
203
+ if isinstance(instance, dict | FrozenDict):
102
204
  return {k: to_dict_with_class_info(v) for k, v in instance.items()}
103
205
  elif isinstance(instance, list):
104
206
  return [to_dict_with_class_info(v) for v in instance]
@@ -108,7 +210,7 @@ def to_dict_with_class_info(instance: Any) -> Any:
108
210
  return str(instance)
109
211
  elif is_dataclass(instance):
110
212
  return {
111
- "__class__": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
213
+ "__class__": instance.__class__.__name__,
112
214
  **{
113
215
  k.name: to_dict_with_class_info(getattr(instance, k.name))
114
216
  for k in fields(instance)
@@ -116,7 +218,7 @@ def to_dict_with_class_info(instance: Any) -> Any:
116
218
  }
117
219
  elif isinstance(instance, Enum):
118
220
  return {
119
- "__class__": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
221
+ "__class__": instance.__class__.__name__,
120
222
  "value": instance.value,
121
223
  }
122
224
  return instance
@@ -125,13 +227,26 @@ def to_dict_with_class_info(instance: Any) -> Any:
125
227
  # Serialization
126
228
  def dumps(instance: Any) -> str:
127
229
  """
128
- Serialize the instance to a JSON string
230
+ Serialize a Python object to a JSON string with class information.
231
+
232
+ This function converts a Python object (including dataclasses, enums,
233
+ and other custom types) to a JSON string that includes class information,
234
+ allowing it to be deserialized back into the original object types.
129
235
 
130
236
  Args:
131
- instance: The instance to serialize.
237
+ instance: The Python object to serialize
132
238
 
133
239
  Returns:
134
- The serialized instance.
240
+ A JSON string representation of the object with class information
241
+
242
+ Example:
243
+ ```python
244
+ # Serialize a filter graph to JSON
245
+ filter_node = FilterNode(name='scale', ...)
246
+ json_str = dumps(filter_node)
247
+ # json_str can be saved to a file and later deserialized
248
+ # with loads() to reconstruct the original object
249
+ ```
135
250
  """
136
251
  obj = to_dict_with_class_info(instance)
137
252
  return json.dumps(obj, indent=2)
@@ -1,3 +1,16 @@
1
+ """
2
+ Directed Acyclic Graph (DAG) implementation for FFmpeg filter chains.
3
+
4
+ This package provides the core components for representing FFmpeg filter chains
5
+ as directed acyclic graphs. It includes classes for different types of nodes
6
+ (inputs, filters, outputs) and streams, as well as utilities for validating,
7
+ compiling, and manipulating these graphs.
8
+
9
+ The DAG structure enables type-safe construction and validation of complex
10
+ FFmpeg filter chains, ensuring that the resulting FFmpeg commands are valid
11
+ and correctly structured.
12
+ """
13
+
1
14
  from .nodes import (
2
15
  FilterableStream,
3
16
  FilterNode,
@@ -1,3 +1,12 @@
1
+ """
2
+ Compiles FFmpeg filter graphs into command-line arguments.
3
+
4
+ This module provides functionality to convert the internal DAG (Directed Acyclic Graph)
5
+ representation of an FFmpeg filter chain into the actual command-line arguments
6
+ that would be passed to FFmpeg. It traverses the graph in the correct order,
7
+ handling global options, inputs, complex filtergraphs, and outputs.
8
+ """
9
+
1
10
  from __future__ import annotations
2
11
 
3
12
  from .context import DAGContext
@@ -8,14 +17,40 @@ from .validate import validate
8
17
 
9
18
  def compile(stream: Stream, auto_fix: bool = True) -> list[str]:
10
19
  """
11
- Compile the stream into a list of arguments.
20
+ Compile a stream into a list of FFmpeg command-line arguments.
21
+
22
+ This function takes a Stream object representing an FFmpeg filter graph
23
+ and converts it into a list of command-line arguments that can be passed
24
+ to FFmpeg. It processes the graph in the correct order:
25
+ 1. Global nodes (general FFmpeg options)
26
+ 2. Input nodes (input files and their options)
27
+ 3. Filter nodes (combined into a -filter_complex argument)
28
+ 4. Output nodes (output files and their options)
29
+
30
+ The function validates the graph before compilation to ensure it's properly
31
+ formed. If auto_fix is enabled, it will attempt to fix common issues.
12
32
 
13
33
  Args:
14
- stream: The stream to compile.
15
- auto_fix: Whether to automatically fix the stream.
34
+ stream: The Stream object to compile into arguments
35
+ auto_fix: Whether to automatically fix issues in the stream
36
+ (e.g., reconnecting disconnected nodes)
16
37
 
17
38
  Returns:
18
- The list of arguments.
39
+ A list of strings representing FFmpeg command-line arguments
40
+
41
+ Example:
42
+ ```python
43
+ # Create a simple video scaling filter graph
44
+ input_stream = ffmpeg.input("input.mp4")
45
+ scaled = input_stream.filter("scale", 1280, 720)
46
+ output_stream = scaled.output("output.mp4")
47
+
48
+ # Compile to FFmpeg arguments
49
+ args = ffmpeg.dag.compile(output_stream)
50
+ print(
51
+ args
52
+ ) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
53
+ ```
19
54
  """
20
55
 
21
56
  stream = validate(stream, auto_fix=auto_fix)
@@ -1,3 +1,12 @@
1
+ """
2
+ Context management for FFmpeg filter graph traversal and manipulation.
3
+
4
+ This module provides the DAGContext class, which represents the context
5
+ of a Directed Acyclic Graph (DAG) of FFmpeg filter nodes. It provides methods
6
+ for traversing, manipulating, and rendering the graph structure, and is used
7
+ during graph validation and command-line compilation.
8
+ """
9
+
1
10
  from __future__ import annotations
2
11
 
3
12
  from collections import defaultdict
@@ -14,13 +23,17 @@ T = TypeVar("T")
14
23
 
15
24
  def _remove_duplicates(seq: list[T]) -> list[T]:
16
25
  """
17
- Remove duplicates from a list while preserving order.
26
+ Remove duplicates from a list while preserving the original order.
27
+
28
+ This helper function processes a list and removes any duplicate elements
29
+ while maintaining the relative ordering of elements. The first occurrence
30
+ of each element is kept, subsequent duplicates are removed.
18
31
 
19
32
  Args:
20
- seq: The list to remove duplicates from.
33
+ seq: The list to remove duplicates from
21
34
 
22
35
  Returns:
23
- The list with duplicates removed.
36
+ A new list with duplicates removed, preserving the original order
24
37
  """
25
38
  seen = set()
26
39
  output = []
@@ -35,13 +48,19 @@ def _remove_duplicates(seq: list[T]) -> list[T]:
35
48
 
36
49
  def _collect(node: Node) -> tuple[list[Node], list[Stream]]:
37
50
  """
38
- Collect all nodes and streams that are upstreamed to the given node
51
+ Recursively collect all nodes and streams in the upstream path of a given node.
52
+
53
+ This function traverses the graph starting from the given node and collects
54
+ all nodes and streams that are upstream (input sources) to the node. The
55
+ traversal is performed recursively to ensure all dependencies are captured.
39
56
 
40
57
  Args:
41
- node: The node to collect from.
58
+ node: The starting node to collect dependencies from
42
59
 
43
60
  Returns:
44
- A tuple of all nodes and streams that are upstreamed to the given node.
61
+ A tuple containing two lists:
62
+ - A list of all nodes in the upstream path (including the starting node)
63
+ - A list of all streams connecting these nodes
45
64
  """
46
65
  nodes, streams = [node], [*node.inputs]
47
66
 
@@ -56,34 +75,53 @@ def _collect(node: Node) -> tuple[list[Node], list[Stream]]:
56
75
  @dataclass(frozen=True, kw_only=True)
57
76
  class DAGContext:
58
77
  """
59
- A context for a directed acyclic graph (DAG).
78
+ Context class for working with a Directed Acyclic Graph (DAG) of FFmpeg filter nodes.
79
+
80
+ This immutable class provides methods and properties for analyzing, traversing,
81
+ and manipulating a filter graph. It maintains information about nodes and streams
82
+ in the graph, their relationships, and provides efficient lookups for graph operations.
83
+
84
+ The context is built from a "root" node (typically an output node) and captures all
85
+ upstream dependencies (input nodes, filter nodes, and connecting streams).
60
86
  """
61
87
 
62
88
  node: Node
63
89
  """
64
90
  The root node (the destination) of the DAG.
91
+
92
+ This is typically an output node where the graph traversal begins.
93
+ All nodes collected in the context are upstream from this node.
65
94
  """
66
95
 
67
96
  nodes: tuple[Node, ...]
68
97
  """
69
- All nodes in the graph.
98
+ All nodes in the graph as an immutable tuple.
99
+
100
+ This includes the root node and all upstream nodes (inputs, filters)
101
+ that contribute to the filter graph.
70
102
  """
71
103
 
72
104
  streams: tuple[Stream, ...]
73
105
  """
74
- All streams in the graph.
106
+ All streams in the graph as an immutable tuple.
107
+
108
+ These streams represent the connections between nodes in the filter graph.
75
109
  """
76
110
 
77
111
  @classmethod
78
112
  def build(cls, node: Node) -> DAGContext:
79
113
  """
80
- create a DAG context based on the given node
114
+ Create a DAG context by traversing the graph from the specified root node.
115
+
116
+ This factory method builds a complete DAGContext by recursively collecting
117
+ all nodes and streams that are upstream from the specified node. It removes
118
+ duplicates to ensure each node and stream is represented only once in the context.
81
119
 
82
120
  Args:
83
- node: The root node of the DAG.
121
+ node: The root node to build the context from (typically an output node)
84
122
 
85
123
  Returns:
86
- A DAG context based on the given node.
124
+ A fully initialized DAGContext containing all nodes and streams in the graph
87
125
  """
88
126
  nodes, streams = _collect(node)
89
127
 
@@ -96,14 +134,29 @@ class DAGContext:
96
134
  @cached_property
97
135
  def all_nodes(self) -> list[Node]:
98
136
  """
99
- All nodes in the graph sorted by the number of upstream nodes.
137
+ Get all nodes in the graph sorted by their position in the processing chain.
138
+
139
+ This property returns a list of all nodes in the graph, sorted by the number
140
+ of upstream nodes. This ensures that nodes earlier in the processing chain
141
+ (closer to inputs) come before nodes later in the chain (closer to outputs).
142
+
143
+ Returns:
144
+ A sorted list of all nodes in the graph
100
145
  """
101
146
  return sorted(self.nodes, key=lambda node: len(node.upstream_nodes))
102
147
 
103
148
  @cached_property
104
149
  def all_streams(self) -> list[Stream]:
105
150
  """
106
- All streams in the graph sorted by the number of upstream nodes and the index of the stream.
151
+ Get all streams in the graph sorted by their position in the processing chain.
152
+
153
+ This property returns a list of all streams in the graph, sorted first by the
154
+ number of upstream nodes of the source node, and then by the stream index.
155
+ This ensures a consistent and logical ordering of streams based on their
156
+ position in the processing pipeline.
157
+
158
+ Returns:
159
+ A sorted list of all streams in the graph
107
160
  """
108
161
  return sorted(
109
162
  self.streams,
@@ -113,7 +166,15 @@ class DAGContext:
113
166
  @cached_property
114
167
  def outgoing_nodes(self) -> dict[Stream, list[tuple[Node, int]]]:
115
168
  """
116
- A dictionary of outgoing nodes for each stream.
169
+ Get a mapping of streams to the nodes they connect to.
170
+
171
+ This property builds a dictionary that maps each stream to a list of
172
+ tuples containing (node, input_index) pairs. Each tuple represents a node
173
+ that receives this stream as input, along with the index position where
174
+ the stream connects to that node.
175
+
176
+ Returns:
177
+ A dictionary mapping streams to their destination nodes and connection indices
117
178
  """
118
179
  outgoing_nodes: dict[Stream, list[tuple[Node, int]]] = defaultdict(list)
119
180
 
@@ -126,7 +187,14 @@ class DAGContext:
126
187
  @cached_property
127
188
  def outgoing_streams(self) -> dict[Node, list[Stream]]:
128
189
  """
129
- A dictionary of outgoing streams for each node.
190
+ Get a mapping of nodes to the streams they output.
191
+
192
+ This property builds a dictionary that maps each node to a list of streams
193
+ that originate from it. This is particularly useful for determining all the
194
+ outputs from a specific filter or input node.
195
+
196
+ Returns:
197
+ A dictionary mapping nodes to their output streams
130
198
  """
131
199
 
132
200
  outgoing_streams: dict[Node, list[Stream]] = defaultdict(list)
@@ -139,7 +207,18 @@ class DAGContext:
139
207
  @cached_property
140
208
  def node_labels(self) -> dict[Node, str]:
141
209
  """
142
- A dictionary of outgoing streams for each node.
210
+ Get a mapping of nodes to their string labels used in FFmpeg filter graphs.
211
+
212
+ This property assigns a unique label to each node in the graph, following
213
+ the FFmpeg filter graph labeling conventions:
214
+ - Input nodes are labeled with sequential numbers (0, 1, 2...)
215
+ - Filter nodes are labeled with 's' followed by a number (s0, s1, s2...)
216
+ - Output nodes are labeled as 'out'
217
+
218
+ These labels are used when generating the filter_complex argument for FFmpeg.
219
+
220
+ Returns:
221
+ A dictionary mapping nodes to their string labels
143
222
  """
144
223
 
145
224
  input_node_index = 0
@@ -161,26 +240,38 @@ class DAGContext:
161
240
  @override
162
241
  def get_outgoing_nodes(self, stream: Stream) -> list[tuple[Node, int]]:
163
242
  """
164
- Get all outgoing nodes of the stream.
243
+ Get all nodes that receive a specific stream as input.
244
+
245
+ This method returns a list of (node, index) tuples representing the nodes
246
+ that receive the given stream as input, along with the input index position
247
+ where the stream connects to each node.
165
248
 
166
249
  Args:
167
- stream: The stream to get the outgoing nodes of.
250
+ stream: The stream to get the destination nodes for
168
251
 
169
252
  Returns:
170
- The outgoing nodes of the stream.
253
+ A list of (node, input_index) tuples for nodes that receive this stream
171
254
  """
172
255
  return self.outgoing_nodes[stream]
173
256
 
174
257
  @override
175
258
  def get_node_label(self, node: Node) -> str:
176
259
  """
177
- Get the label of the node.
260
+ Get the string label for a specific node in the filter graph.
261
+
262
+ This method returns the label assigned to the node, which is used in FFmpeg
263
+ filter graph notation. The label format depends on the node type:
264
+ - Input nodes: sequential numbers (0, 1, 2...)
265
+ - Filter nodes: 's' prefix followed by a number (s0, s1, s2...)
178
266
 
179
267
  Args:
180
- node: The node to get the label of.
268
+ node: The node to get the label for (must be an InputNode or FilterNode)
181
269
 
182
270
  Returns:
183
- The label of the node.
271
+ The string label for the node
272
+
273
+ Raises:
274
+ AssertionError: If the node is not an InputNode or FilterNode
184
275
  """
185
276
 
186
277
  assert isinstance(node, (InputNode, FilterNode)), (
@@ -191,25 +282,42 @@ class DAGContext:
191
282
  @override
192
283
  def get_outgoing_streams(self, node: Node) -> list[Stream]:
193
284
  """
194
- Extract all node's outgoing streams from the given set of streams, Because a node only know its incoming streams.
285
+ Get all streams that originate from a specific node.
286
+
287
+ This method returns all streams where the given node is the source.
288
+ It's particularly useful because nodes natively only track their inputs,
289
+ not their outputs, so this context method provides a way to look up
290
+ a node's outputs.
195
291
 
196
292
  Args:
197
- node: The node to get the outgoing streams of.
293
+ node: The node to get the output streams for
198
294
 
199
295
  Returns:
200
- The outgoing streams of the node.
296
+ A list of streams that originate from this node
201
297
  """
202
298
  return self.outgoing_streams[node]
203
299
 
204
300
  def render(self, obj: Any) -> Any:
205
301
  """
206
- Render the object to a string.
302
+ Recursively convert graph objects to a human-readable representation.
303
+
304
+ This method processes arbitrary objects, with special handling for graph
305
+ elements like nodes and streams. It converts them to a readable string format
306
+ that includes node labels. It recursively handles nested structures like
307
+ lists, tuples, and dictionaries.
308
+
309
+ This is primarily used for debugging, logging, and visualization purposes.
207
310
 
208
311
  Args:
209
- obj: The object to render.
312
+ obj: The object to render, which may be a Node, Stream, or a container
313
+ with these objects nested inside
210
314
 
211
315
  Returns:
212
- The rendered object.
316
+ The rendered representation of the object:
317
+ - For nodes: "Node(repr#label)"
318
+ - For streams: "Stream(node_repr#label#index)"
319
+ - For containers: recursively rendered contents
320
+ - For other objects: the original object unchanged
213
321
  """
214
322
 
215
323
  if isinstance(obj, (list, tuple)):
@@ -1,3 +1,11 @@
1
+ """
2
+ Factory functions for creating FFmpeg filter nodes.
3
+
4
+ This module provides factory functions that create filter nodes based on
5
+ FFmpeg filter definitions. These factories handle the evaluation of automatic
6
+ parameters and the conversion of input/output typing specifications.
7
+ """
8
+
1
9
  import re
2
10
  from typing import Any
3
11
 
@@ -10,6 +18,25 @@ from .nodes import FilterableStream, FilterNode
10
18
  def filter_node_factory(
11
19
  ffmpeg_filter_def: FFMpegFilterDef, *inputs: FilterableStream, **kwargs: Any
12
20
  ) -> FilterNode:
21
+ """
22
+ Create a FilterNode from an FFmpeg filter definition.
23
+
24
+ This function creates a FilterNode based on the provided FFmpeg filter definition.
25
+ It handles the evaluation of Auto parameters and the conversion of input/output
26
+ typing specifications from the filter definition.
27
+
28
+ Args:
29
+ ffmpeg_filter_def: The FFmpeg filter definition to create a node from
30
+ *inputs: The input streams to connect to the filter
31
+ **kwargs: Filter-specific parameters as keyword arguments
32
+
33
+ Returns:
34
+ A FilterNode configured according to the filter definition
35
+
36
+ Note:
37
+ This function is primarily used internally by the filter generation system
38
+ to create filter nodes from the FFmpeg filter definitions.
39
+ """
13
40
  for k, v in kwargs.items():
14
41
  if isinstance(v, Auto):
15
42
  kwargs[k] = eval(