typed-ffmpeg-compatible 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. typed_ffmpeg/__init__.py +25 -0
  2. typed_ffmpeg/base.py +114 -0
  3. typed_ffmpeg/common/__init__.py +0 -0
  4. typed_ffmpeg/common/schema.py +308 -0
  5. typed_ffmpeg/common/serialize.py +132 -0
  6. typed_ffmpeg/dag/__init__.py +13 -0
  7. typed_ffmpeg/dag/compile.py +51 -0
  8. typed_ffmpeg/dag/context.py +221 -0
  9. typed_ffmpeg/dag/factory.py +31 -0
  10. typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
  11. typed_ffmpeg/dag/global_runnable/global_args.py +178 -0
  12. typed_ffmpeg/dag/global_runnable/runnable.py +174 -0
  13. typed_ffmpeg/dag/io/__init__.py +0 -0
  14. typed_ffmpeg/dag/io/_input.py +197 -0
  15. typed_ffmpeg/dag/io/_output.py +320 -0
  16. typed_ffmpeg/dag/io/output_args.py +327 -0
  17. typed_ffmpeg/dag/nodes.py +479 -0
  18. typed_ffmpeg/dag/schema.py +210 -0
  19. typed_ffmpeg/dag/utils.py +41 -0
  20. typed_ffmpeg/dag/validate.py +172 -0
  21. typed_ffmpeg/exceptions.py +42 -0
  22. typed_ffmpeg/filters.py +3572 -0
  23. typed_ffmpeg/probe.py +43 -0
  24. typed_ffmpeg/py.typed +0 -0
  25. typed_ffmpeg/schema.py +29 -0
  26. typed_ffmpeg/streams/__init__.py +5 -0
  27. typed_ffmpeg/streams/audio.py +7358 -0
  28. typed_ffmpeg/streams/av.py +22 -0
  29. typed_ffmpeg/streams/channel_layout.py +39 -0
  30. typed_ffmpeg/streams/video.py +13469 -0
  31. typed_ffmpeg/types.py +119 -0
  32. typed_ffmpeg/utils/__init__.py +0 -0
  33. typed_ffmpeg/utils/escaping.py +49 -0
  34. typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
  35. typed_ffmpeg/utils/lazy_eval/operator.py +134 -0
  36. typed_ffmpeg/utils/lazy_eval/schema.py +211 -0
  37. typed_ffmpeg/utils/run.py +27 -0
  38. typed_ffmpeg/utils/snapshot.py +26 -0
  39. typed_ffmpeg/utils/typing.py +17 -0
  40. typed_ffmpeg/utils/view.py +64 -0
  41. typed_ffmpeg_compatible-2.4.1.dist-info/LICENSE +21 -0
  42. typed_ffmpeg_compatible-2.4.1.dist-info/METADATA +182 -0
  43. typed_ffmpeg_compatible-2.4.1.dist-info/RECORD +45 -0
  44. typed_ffmpeg_compatible-2.4.1.dist-info/WHEEL +4 -0
  45. typed_ffmpeg_compatible-2.4.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+
5
+ # Another approach to determine if a graph is a DAG is to try to perform a topological sort.
6
+ # If the topological sort is successful (i.e., all vertices are visited exactly once),
7
+ # the graph is a DAG. If the topological sort cannot include all vertices (i.e., the graph has a cycle),
8
+ # it is not a DAG. Here is a basic implementation using Kahn's Algorithm:
9
+
10
+
11
+ def is_dag(graph: dict[str, set[str]]) -> bool:
12
+ """
13
+ Determine if a graph is a directed acyclic graph (DAG).
14
+
15
+ Args:
16
+ graph: The graph to check.
17
+
18
+ Returns:
19
+ Whether the graph is a DAG.
20
+ """
21
+
22
+ in_degree = {u: 0 for u in graph} # Initialize in-degree of each node to 0
23
+
24
+ # Calculate in-degree of each node
25
+ for u in graph:
26
+ for v in graph[u]:
27
+ in_degree[v] += 1
28
+
29
+ queue = deque([u for u in graph if in_degree[u] == 0])
30
+ count = 0
31
+
32
+ while queue:
33
+ u = queue.popleft()
34
+ count += 1
35
+
36
+ for v in graph[u]:
37
+ in_degree[v] -= 1
38
+ if in_degree[v] == 0:
39
+ queue.append(v)
40
+
41
+ return count == len(graph)
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+
5
+ from ..exceptions import FFMpegValueError
6
+ from ..streams.audio import AudioStream
7
+ from ..streams.video import VideoStream
8
+ from .context import DAGContext
9
+ from .nodes import FilterNode, InputNode
10
+ from .schema import Node, Stream
11
+
12
+
13
+ def remove_split(current_stream: Stream, mapping: dict[Stream, Stream] = None) -> tuple[Stream, dict[Stream, Stream]]:
14
+ """
15
+ Rebuild the graph with the given mapping.
16
+
17
+ Args:
18
+ current_stream: The stream to rebuild the graph with.
19
+ mapping: The mapping to rebuild the graph with.
20
+
21
+ Returns:
22
+ A tuple of the new node and the new mapping.
23
+ """
24
+
25
+ # remove all split nodes
26
+ # add split nodes to the graph
27
+ if mapping is None:
28
+ mapping = {}
29
+
30
+ if current_stream in mapping:
31
+ return mapping[current_stream], mapping
32
+
33
+ if not current_stream.node.inputs:
34
+ mapping[current_stream] = current_stream
35
+ return current_stream, mapping
36
+
37
+ if isinstance(current_stream.node, FilterNode):
38
+ # if the current node is a split node, we need to remove it
39
+ if current_stream.node.name in ("split", "asplit"):
40
+ new_stream, _mapping = remove_split(current_stream=current_stream.node.inputs[0], mapping=mapping)
41
+ mapping[current_stream] = mapping[current_stream.node.inputs[0]]
42
+ return mapping[current_stream.node.inputs[0]], mapping
43
+
44
+ inputs = {}
45
+ for idx, input_stream in sorted(
46
+ enumerate(current_stream.node.inputs), key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes)
47
+ ):
48
+ new_stream, _mapping = remove_split(current_stream=input_stream, mapping=mapping)
49
+ inputs[idx] = new_stream
50
+ mapping |= _mapping
51
+
52
+ new_node = replace(
53
+ current_stream.node, inputs=tuple(stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0]))
54
+ )
55
+ new_stream = replace(current_stream, node=new_node)
56
+
57
+ mapping[current_stream] = new_stream
58
+ return new_stream, mapping
59
+
60
+
61
+ def add_split(
62
+ current_stream: Stream,
63
+ down_node: Node = None,
64
+ down_index: int = None,
65
+ context: DAGContext = None,
66
+ mapping: dict[tuple[Stream, Node | None, int | None], Stream] = None,
67
+ ) -> tuple[Stream, dict[tuple[Stream, Node | None, int | None], Stream]]:
68
+ """
69
+ Add split nodes to the graph.
70
+
71
+ Args:
72
+ current_stream: The stream to add split nodes to.
73
+ down_node: The node use current_stream as input.
74
+ down_index: The index of the input stream in down_node.
75
+ context: The DAG context.
76
+ mapping: The mapping to add split nodes to.
77
+
78
+ Returns:
79
+ A tuple of the new node and the new mapping.
80
+ """
81
+
82
+ if not context:
83
+ context = DAGContext.build(current_stream.node)
84
+
85
+ if mapping is None:
86
+ mapping = {}
87
+
88
+ if (current_stream, down_node, down_index) in mapping:
89
+ return mapping[(current_stream, down_node, down_index)], mapping
90
+
91
+ inputs = {}
92
+
93
+ for idx, input_stream in sorted(
94
+ enumerate(current_stream.node.inputs), key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes)
95
+ ):
96
+ new_stream, _mapping = add_split(
97
+ current_stream=input_stream, down_node=current_stream.node, down_index=idx, mapping=mapping, context=context
98
+ )
99
+ inputs[idx] = new_stream
100
+ mapping |= _mapping
101
+
102
+ new_node = replace(
103
+ current_stream.node, inputs=tuple(stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0]))
104
+ )
105
+ new_stream = replace(current_stream, node=new_node)
106
+
107
+ num = len(context.get_outgoing_nodes(current_stream))
108
+ if num < 2:
109
+ mapping[(current_stream, down_node, down_index)] = new_stream
110
+ return new_stream, mapping
111
+
112
+ if isinstance(current_stream.node, InputNode):
113
+ for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
114
+ # if the current node is InputNode, we don't need to split it
115
+ mapping[(current_stream, node, index)] = new_stream
116
+ return new_stream, mapping
117
+
118
+ if isinstance(new_stream, VideoStream):
119
+ split_node = new_stream.split(outputs=num)
120
+ for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
121
+ mapping[(current_stream, node, index)] = split_node.video(idx)
122
+ return mapping[(current_stream, down_node, down_index)], mapping
123
+ elif isinstance(new_stream, AudioStream):
124
+ split_node = new_stream.asplit(outputs=num)
125
+ for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
126
+ mapping[(current_stream, node, index)] = split_node.audio(idx)
127
+ return mapping[(current_stream, down_node, down_index)], mapping
128
+ else:
129
+ raise FFMpegValueError(f"unsupported stream type: {current_stream}")
130
+
131
+
132
+ def fix_graph(stream: Stream) -> Stream:
133
+ """
134
+ Fix the graph by removing and adding split nodes.
135
+
136
+ Args:
137
+ stream: The stream to fix.
138
+
139
+ Returns:
140
+ The fixed stream.
141
+
142
+ Note:
143
+ Fix the graph by resetting split nodes.
144
+ This function is for internal use only.
145
+ """
146
+
147
+ stream, _ = remove_split(stream)
148
+ stream, _ = add_split(stream)
149
+ return stream
150
+
151
+
152
+ def validate(stream: Stream, auto_fix: bool = True) -> Stream:
153
+ """
154
+ Validate the given DAG. If auto_fix is True, the graph will be automatically fixed to follow ffmpeg's rule.
155
+
156
+ Args:
157
+ stream: The DAG to validate.
158
+ auto_fix: Whether to automatically fix the graph.
159
+
160
+ Returns:
161
+ The validated DAG context.
162
+ """
163
+ if auto_fix:
164
+ stream = fix_graph(stream)
165
+
166
+ # NOTE: we don't want to modify the original node
167
+ # validators: list[] = []
168
+
169
+ # for validator in validators:
170
+ # context = validator(context)
171
+
172
+ return stream
@@ -0,0 +1,42 @@
1
+ class FFMpegError(Exception):
2
+ """
3
+ Base exception for all ffmpeg errors.
4
+ """
5
+
6
+ ...
7
+
8
+
9
+ class FFMpegTypeError(FFMpegError, TypeError):
10
+ """
11
+ Base exception for all ffmpeg type errors.
12
+ """
13
+
14
+
15
+ class FFMpegValueError(FFMpegError, ValueError):
16
+ """
17
+ Base exception for all ffmpeg value errors.
18
+ """
19
+
20
+
21
+ class FFMpegExecuteError(FFMpegError):
22
+ """
23
+ FFmpeg error
24
+ """
25
+
26
+ def __init__(self, retcode: int | None, cmd: str, stdout: bytes, stderr: bytes):
27
+ """
28
+ Initialize the exception.
29
+
30
+ Args:
31
+ retcode: The return code of the command.
32
+ cmd: The command that was run.
33
+ stdout: The stdout of the command.
34
+ stderr: The stderr of the command.
35
+ """
36
+
37
+ self.stdout = stdout
38
+ self.stderr = stderr
39
+ self.cmd = cmd
40
+ self.retcode = retcode
41
+
42
+ super(FFMpegExecuteError, self).__init__(f"{cmd} error (see stderr output for detail) {stderr!r}")