typed-ffmpeg-compatible 3.2.4__py3-none-any.whl → 3.3__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/_version.py +2 -2
- typed_ffmpeg/compile/compile_cli.py +48 -20
- typed_ffmpeg/compile/compile_python.py +9 -10
- typed_ffmpeg/dag/schema.py +8 -0
- typed_ffmpeg/streams/av.py +36 -4
- typed_ffmpeg/streams/subtitle.py +7 -0
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/METADATA +1 -1
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/RECORD +12 -11
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/entry_points.txt +0 -0
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/licenses/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/top_level.txt +0 -0
typed_ffmpeg/_version.py
CHANGED
@@ -20,6 +20,7 @@ import re
|
|
20
20
|
import shlex
|
21
21
|
from collections import defaultdict
|
22
22
|
from collections.abc import Mapping
|
23
|
+
from dataclasses import replace
|
23
24
|
|
24
25
|
from ..base import input, merge_outputs, output
|
25
26
|
from ..common.cache import load
|
@@ -38,6 +39,7 @@ from ..exceptions import FFMpegValueError
|
|
38
39
|
from ..schema import Default
|
39
40
|
from ..streams.audio import AudioStream
|
40
41
|
from ..streams.av import AVStream
|
42
|
+
from ..streams.subtitle import SubtitleStream
|
41
43
|
from ..streams.video import VideoStream
|
42
44
|
from ..utils.escaping import escape
|
43
45
|
from ..utils.lazy_eval.schema import LazyValue
|
@@ -140,20 +142,31 @@ def parse_stream_selector(
|
|
140
142
|
stream = mapping[stream_label]
|
141
143
|
|
142
144
|
if isinstance(stream, AVStream):
|
143
|
-
if
|
144
|
-
|
145
|
-
|
146
|
-
elif selector.count(":") == 2:
|
147
|
-
stream_label, stream_type, stream_index = selector.split(":", 2)
|
148
|
-
return (
|
149
|
-
stream.video_stream(int(stream_index))
|
150
|
-
if stream_type == "v"
|
151
|
-
else stream.audio_stream(int(stream_index))
|
152
|
-
)
|
145
|
+
if "?" in selector:
|
146
|
+
optional = True
|
147
|
+
selector = selector.strip("?")
|
153
148
|
else:
|
154
|
-
|
155
|
-
|
156
|
-
|
149
|
+
optional = False
|
150
|
+
|
151
|
+
if ":" in selector:
|
152
|
+
if selector.count(":") == 1:
|
153
|
+
stream_label, stream_type = selector.split(":", 1)
|
154
|
+
stream_index = None
|
155
|
+
elif selector.count(":") == 2:
|
156
|
+
stream_label, stream_type, _stream_index = selector.split(":", 2)
|
157
|
+
stream_index = int(_stream_index)
|
158
|
+
|
159
|
+
match stream_type:
|
160
|
+
case "v":
|
161
|
+
return stream.video_stream(stream_index, optional)
|
162
|
+
case "a":
|
163
|
+
return stream.audio_stream(stream_index, optional)
|
164
|
+
case "s":
|
165
|
+
return stream.subtitle_stream(stream_index, optional)
|
166
|
+
case _:
|
167
|
+
raise FFMpegValueError(f"Unknown stream type: {stream_type}")
|
168
|
+
return replace(stream, optional=optional)
|
169
|
+
return stream
|
157
170
|
|
158
171
|
|
159
172
|
def _is_filename(token: str) -> bool:
|
@@ -371,12 +384,17 @@ def parse_filter_complex(
|
|
371
384
|
for idx, (output_label, output_typing) in enumerate(
|
372
385
|
zip(output_labels, filter_node.output_typings)
|
373
386
|
):
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
387
|
+
match output_typing:
|
388
|
+
case StreamType.video:
|
389
|
+
stream_mapping[output_label] = VideoStream(
|
390
|
+
node=filter_node, index=idx
|
391
|
+
)
|
392
|
+
case StreamType.audio:
|
393
|
+
stream_mapping[output_label] = AudioStream(
|
394
|
+
node=filter_node, index=idx
|
395
|
+
)
|
396
|
+
case _:
|
397
|
+
raise FFMpegValueError(f"Unknown stream type: {output_typing}")
|
380
398
|
|
381
399
|
return stream_mapping
|
382
400
|
|
@@ -608,6 +626,7 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
|
608
626
|
For input streams, labels follow FFmpeg's stream specifier syntax:
|
609
627
|
- Video streams: "0:v" (first input, video stream)
|
610
628
|
- Audio streams: "0:a" (first input, audio stream)
|
629
|
+
- Subtitle streams: "0:s" (first input, subtitle stream)
|
611
630
|
- AV streams: "0" (first input, all streams)
|
612
631
|
|
613
632
|
For filter outputs, labels use the filter's label:
|
@@ -649,6 +668,12 @@ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
|
649
668
|
f"{get_node_label(stream.node, context)}:a:{stream.index}"
|
650
669
|
)
|
651
670
|
return f"{get_node_label(stream.node, context)}:a"
|
671
|
+
case SubtitleStream():
|
672
|
+
if stream.index is not None:
|
673
|
+
return (
|
674
|
+
f"{get_node_label(stream.node, context)}:s:{stream.index}"
|
675
|
+
)
|
676
|
+
return f"{get_node_label(stream.node, context)}:s"
|
652
677
|
case _:
|
653
678
|
raise FFMpegValueError(
|
654
679
|
f"Unknown stream type: {stream.__class__.__name__}"
|
@@ -795,7 +820,10 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
|
|
795
820
|
and len(node.inputs) == 1
|
796
821
|
):
|
797
822
|
continue
|
798
|
-
|
823
|
+
if not input.optional:
|
824
|
+
commands += ["-map", get_stream_label(input, context)]
|
825
|
+
else:
|
826
|
+
commands += ["-map", f"{get_stream_label(input, context)}?"]
|
799
827
|
else:
|
800
828
|
commands += ["-map", f"[{get_stream_label(input, context)}]"]
|
801
829
|
|
@@ -17,6 +17,7 @@ from ..dag.nodes import (
|
|
17
17
|
from ..dag.schema import Node, Stream
|
18
18
|
from ..streams.audio import AudioStream
|
19
19
|
from ..streams.av import AVStream
|
20
|
+
from ..streams.subtitle import SubtitleStream
|
20
21
|
from ..streams.video import VideoStream
|
21
22
|
from .context import DAGContext
|
22
23
|
from .validate import validate
|
@@ -76,10 +77,7 @@ def get_input_var_name(
|
|
76
77
|
case VideoStream():
|
77
78
|
match stream.node:
|
78
79
|
case InputNode():
|
79
|
-
|
80
|
-
return f"{get_output_var_name(stream.node, context)}.video_stream({stream.index})"
|
81
|
-
else:
|
82
|
-
return f"{get_output_var_name(stream.node, context)}.video"
|
80
|
+
return f"{get_output_var_name(stream.node, context)}.video_stream({stream.index}, optional={stream.optional})"
|
83
81
|
case FilterNode():
|
84
82
|
if filter_data_dict[stream.node.name].is_dynamic_output:
|
85
83
|
return f"{get_output_var_name(stream.node, context)}.video({filter_stream_typed_index(stream, context)})"
|
@@ -93,10 +91,7 @@ def get_input_var_name(
|
|
93
91
|
case AudioStream():
|
94
92
|
match stream.node:
|
95
93
|
case InputNode():
|
96
|
-
|
97
|
-
return f"{get_output_var_name(stream.node, context)}.audio_stream({stream.index})"
|
98
|
-
else:
|
99
|
-
return f"{get_output_var_name(stream.node, context)}.audio"
|
94
|
+
return f"{get_output_var_name(stream.node, context)}.audio_stream({stream.index}, optional={stream.optional})"
|
100
95
|
case FilterNode():
|
101
96
|
if filter_data_dict[stream.node.name].is_dynamic_output:
|
102
97
|
return f"{get_output_var_name(stream.node, context)}.audio({filter_stream_typed_index(stream, context)})"
|
@@ -107,12 +102,16 @@ def get_input_var_name(
|
|
107
102
|
return f"{get_output_var_name(stream.node, context)}[{stream.index}]"
|
108
103
|
else:
|
109
104
|
return f"{get_output_var_name(stream.node, context)}"
|
105
|
+
case SubtitleStream():
|
106
|
+
match stream.node:
|
107
|
+
case InputNode():
|
108
|
+
return f"{get_output_var_name(stream.node, context)}.subtitle_stream({stream.index}, optional={stream.optional})"
|
110
109
|
case OutputStream():
|
111
110
|
return f"{get_output_var_name(stream.node, context)}"
|
112
111
|
case GlobalStream():
|
113
112
|
return f"{get_output_var_name(stream.node, context)}"
|
114
|
-
|
115
|
-
|
113
|
+
|
114
|
+
raise ValueError(f"Unknown stream type: {type(stream)}") # pragma: no cover
|
116
115
|
|
117
116
|
|
118
117
|
def get_output_var_name(node: Node, context: DAGContext) -> str:
|
typed_ffmpeg/dag/schema.py
CHANGED
@@ -49,6 +49,14 @@ class Stream(HashableBaseModel):
|
|
49
49
|
See Also: [Stream specifiers](https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1) `stream_index`
|
50
50
|
"""
|
51
51
|
|
52
|
+
optional: bool = False
|
53
|
+
"""
|
54
|
+
Represents whether the stream is optional.
|
55
|
+
|
56
|
+
Note:
|
57
|
+
See Also: [Advanced options](https://ffmpeg.org/ffmpeg.html#Advanced-options)
|
58
|
+
"""
|
59
|
+
|
52
60
|
def view(self, format: Literal["png", "svg", "dot"] = "png") -> str:
|
53
61
|
"""
|
54
62
|
Visualize the stream.
|
typed_ffmpeg/streams/av.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from ..dag.nodes import InputNode
|
2
2
|
from .audio import AudioStream
|
3
|
+
from .subtitle import SubtitleStream
|
3
4
|
from .video import VideoStream
|
4
5
|
|
5
6
|
|
@@ -30,26 +31,57 @@ class AVStream(AudioStream, VideoStream):
|
|
30
31
|
"""
|
31
32
|
return AudioStream(node=self.node, index=self.index)
|
32
33
|
|
33
|
-
|
34
|
+
@property
|
35
|
+
def subtitle(self) -> SubtitleStream:
|
36
|
+
"""
|
37
|
+
Get the subtitle stream from the input node.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
SubtitleStream: The subtitle stream from the input node.
|
41
|
+
"""
|
42
|
+
return SubtitleStream(node=self.node, index=self.index)
|
43
|
+
|
44
|
+
def video_stream(
|
45
|
+
self, index: int | None = None, optional: bool = False
|
46
|
+
) -> VideoStream:
|
34
47
|
"""
|
35
48
|
Get the video stream from the input node with a specified index.
|
36
49
|
|
37
50
|
Args:
|
38
51
|
index: The index of the video stream.
|
52
|
+
optional: Whether the video stream is optional.
|
39
53
|
|
40
54
|
Returns:
|
41
55
|
VideoStream: The video stream from the input node.
|
42
56
|
"""
|
43
|
-
return VideoStream(node=self.node, index=index)
|
57
|
+
return VideoStream(node=self.node, index=index, optional=optional)
|
44
58
|
|
45
|
-
def audio_stream(
|
59
|
+
def audio_stream(
|
60
|
+
self, index: int | None = None, optional: bool = False
|
61
|
+
) -> AudioStream:
|
46
62
|
"""
|
47
63
|
Get the audio stream from the input node with a specified index.
|
48
64
|
|
49
65
|
Args:
|
50
66
|
index: The index of the audio stream.
|
67
|
+
optional: Whether the audio stream is optional.
|
51
68
|
|
52
69
|
Returns:
|
53
70
|
AudioStream: The audio stream from the input node.
|
54
71
|
"""
|
55
|
-
return AudioStream(node=self.node, index=index)
|
72
|
+
return AudioStream(node=self.node, index=index, optional=optional)
|
73
|
+
|
74
|
+
def subtitle_stream(
|
75
|
+
self, index: int | None = None, optional: bool = False
|
76
|
+
) -> SubtitleStream:
|
77
|
+
"""
|
78
|
+
Get the subtitle stream from the input node with a specified index.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
index: The index of the subtitle stream.
|
82
|
+
optional: Whether the subtitle stream is optional.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
SubtitleStream: The subtitle stream from the input node.
|
86
|
+
"""
|
87
|
+
return SubtitleStream(node=self.node, index=index, optional=optional)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: typed-ffmpeg-compatible
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.3
|
4
4
|
Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
|
5
5
|
Author-email: lucemia <lucemia@gmail.com>
|
6
6
|
License-Expression: MIT
|
@@ -1,5 +1,5 @@
|
|
1
1
|
typed_ffmpeg/__init__.py,sha256=n1SZfENc9xhZ0eA3ZzkBNPuIY-Pt3-rOwB8-uUj5olU,1592
|
2
|
-
typed_ffmpeg/_version.py,sha256=
|
2
|
+
typed_ffmpeg/_version.py,sha256=lgTPmPhRGLqWrHHDFV9z5QiSuA5Dn5Dch5YbYnpAWj4,506
|
3
3
|
typed_ffmpeg/base.py,sha256=C5Tqbx2I0c-09D7aXKZoGkspu-lAAeAhuOns5zr3PXQ,6304
|
4
4
|
typed_ffmpeg/exceptions.py,sha256=D4SID6WOwkjVV8O8mAjrEDHWn-8BRDnK_jteaDof1SY,2474
|
5
5
|
typed_ffmpeg/filters.py,sha256=_lBpGZgPHK3KgGVrw-TCdQEsBRuEXVIgwggYNGd80MU,110076
|
@@ -13,15 +13,15 @@ typed_ffmpeg/common/cache.py,sha256=j0JvfX7jewLpdJWxgo7Pwze0BkUJdYGHX2uGR8BZ-9M,
|
|
13
13
|
typed_ffmpeg/common/schema.py,sha256=qM8yfMX9UU3EAQSNsTrr-SAmyqKx8eQCXTtu3RJWkEk,19673
|
14
14
|
typed_ffmpeg/common/serialize.py,sha256=dLim0DBP5CdJ1JiMV9xEmmh1XMSIhBOWs61EopAL15s,7719
|
15
15
|
typed_ffmpeg/compile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
typed_ffmpeg/compile/compile_cli.py,sha256=
|
16
|
+
typed_ffmpeg/compile/compile_cli.py,sha256=Xuz4gAp4kqeywOxBNgPXH_Ie8lrmwfEdAl-HxMGo9kI,32892
|
17
17
|
typed_ffmpeg/compile/compile_json.py,sha256=YCiTyfAnUVSbFr7BiQpmJYs13K5sa-xo77Iih33mb6I,992
|
18
|
-
typed_ffmpeg/compile/compile_python.py,sha256=
|
18
|
+
typed_ffmpeg/compile/compile_python.py,sha256=YnnRRHE8TEUiqFF9DsqkYOwIcA2ejCYw12k-O5n825A,11506
|
19
19
|
typed_ffmpeg/compile/context.py,sha256=macQ3HhEJ73j_WbWYtU9GCQCzcB_KQGAPimcuU-WOac,10946
|
20
20
|
typed_ffmpeg/compile/validate.py,sha256=QsWksdvlRwWw6hnatFo-ABakms1qDXRbEmvMQGRLrD8,9579
|
21
21
|
typed_ffmpeg/dag/__init__.py,sha256=qAApSNqjbZ1DtUaV5bSku9RwG7MpMPa1HJO764cSBt4,849
|
22
22
|
typed_ffmpeg/dag/factory.py,sha256=2IMVKP_2UaTrlGXBg8YDx5KXBqhpScJiJQ87PRrppzY,3147
|
23
23
|
typed_ffmpeg/dag/nodes.py,sha256=lfHChT8JqRs3UUDWtgrWnnXn845HXSD5S1QmHpIUT4U,20526
|
24
|
-
typed_ffmpeg/dag/schema.py,sha256=
|
24
|
+
typed_ffmpeg/dag/schema.py,sha256=xsVT46XgT3Usgb4N9OWdjP74MG9dBrXgZGbjsYhsUEM,5916
|
25
25
|
typed_ffmpeg/dag/utils.py,sha256=hydh7_kjpOCw8WEGhXMxIXR4Ek-3DeoOt6esInuK2Xw,1941
|
26
26
|
typed_ffmpeg/dag/global_runnable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
27
|
typed_ffmpeg/dag/global_runnable/global_args.py,sha256=ehLtx4v-XxqkmODhpE_gik0r79hs4Sa8TJnRsH9Fj1o,8043
|
@@ -37,8 +37,9 @@ typed_ffmpeg/ffprobe/schema.py,sha256=PBgBWYGO8wKnI0Lae9oCJ1Nprhv2ciPkdLrumzPVll
|
|
37
37
|
typed_ffmpeg/ffprobe/xml2json.py,sha256=1TSuxR7SYc-M_-JmE-1khHGbXCapgW0Oap9kzL0nwNg,2455
|
38
38
|
typed_ffmpeg/streams/__init__.py,sha256=Nt9uWpcVI1sQLl5Qt_kBCBcWOGZA1vczCQ0qvFbSko0,141
|
39
39
|
typed_ffmpeg/streams/audio.py,sha256=2oNRhb5UetWtlPG3NW73AjUZoFPl303yMv-6W1sGWt0,259054
|
40
|
-
typed_ffmpeg/streams/av.py,sha256=
|
40
|
+
typed_ffmpeg/streams/av.py,sha256=qrsO692BG6OVMWVwIeuCKPntBGTR685lc2ULbGPTSkQ,2594
|
41
41
|
typed_ffmpeg/streams/channel_layout.py,sha256=hGagaoc1tnWS8K1yiITp4f_92qq5e7C_zB15bHOL0DI,1162
|
42
|
+
typed_ffmpeg/streams/subtitle.py,sha256=zhbRKUYH0C972Hurm4hK_dqLvT4Y6v_VNZFzQrkCm9A,141
|
42
43
|
typed_ffmpeg/streams/video.py,sha256=cQwHfew75YO_dbZjmpUYd-nXt1JHN0M7suFKKe5UH5s,453576
|
43
44
|
typed_ffmpeg/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
44
45
|
typed_ffmpeg/utils/escaping.py,sha256=m6CTEBwWZTFdtZHTHW-3pQCgkpdZb9f9ynoO-gsD7uM,2937
|
@@ -50,9 +51,9 @@ typed_ffmpeg/utils/view.py,sha256=QCSlQoQkRBI-T0sWjiywGgM9DlKd8Te3CB2ZYX-pEVU,34
|
|
50
51
|
typed_ffmpeg/utils/lazy_eval/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
52
|
typed_ffmpeg/utils/lazy_eval/operator.py,sha256=QWybd-UH3VdDa8kgWkqAMi3WV0b0WF1d1JixQr6is2E,4136
|
52
53
|
typed_ffmpeg/utils/lazy_eval/schema.py,sha256=WSg-E3MS3itN1AT6Dq4Z9btnRHEReuN3o6zruXou7h4,9623
|
53
|
-
typed_ffmpeg_compatible-3.
|
54
|
-
typed_ffmpeg_compatible-3.
|
55
|
-
typed_ffmpeg_compatible-3.
|
56
|
-
typed_ffmpeg_compatible-3.
|
57
|
-
typed_ffmpeg_compatible-3.
|
58
|
-
typed_ffmpeg_compatible-3.
|
54
|
+
typed_ffmpeg_compatible-3.3.dist-info/licenses/LICENSE,sha256=8Aaya5i_09Cou2i3QMxTwz6uHGzi_fGA4uhkco07-A4,1066
|
55
|
+
typed_ffmpeg_compatible-3.3.dist-info/METADATA,sha256=FtrDI-yit60_yKnw5CfgYotZX9MLBIMiNtqCXBnDPmA,8383
|
56
|
+
typed_ffmpeg_compatible-3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
57
|
+
typed_ffmpeg_compatible-3.3.dist-info/entry_points.txt,sha256=kUQvZ27paV-07qtkIFV-emKsYtjFOTw9kknBRSXPs04,45
|
58
|
+
typed_ffmpeg_compatible-3.3.dist-info/top_level.txt,sha256=vuASJGVRQiNmhWY1pt0RXESWSNkknWXqWLIRAU7H_L4,13
|
59
|
+
typed_ffmpeg_compatible-3.3.dist-info/RECORD,,
|
File without changes
|
{typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/entry_points.txt
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{typed_ffmpeg_compatible-3.2.4.dist-info → typed_ffmpeg_compatible-3.3.dist-info}/top_level.txt
RENAMED
File without changes
|