typed-ffmpeg-compatible 3.0.2a0__tar.gz → 3.2.2__tar.gz
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_compatible-3.2.2/MANIFEST.in +41 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/PKG-INFO +36 -17
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/README.md +22 -3
- typed_ffmpeg_compatible-3.2.2/pyproject.toml +125 -0
- typed_ffmpeg_compatible-3.2.2/setup.cfg +4 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/__init__.py +2 -1
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/_version.py +21 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/compile_cli.py +43 -35
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/compile_python.py +3 -4
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/ffprobe/parse.py +133 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/ffprobe/probe.py +272 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/ffprobe/schema.py +455 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/ffprobe/xml2json.py +70 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/PKG-INFO +201 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/SOURCES.txt +62 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/dependency_links.txt +1 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/entry_points.txt +2 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/requires.txt +3 -0
- typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg_compatible.egg-info/top_level.txt +1 -0
- typed_ffmpeg_compatible-3.0.2a0/pyproject.toml +0 -102
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/.gitignore +0 -3
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/acrossover.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/afir.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aiir.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ainterleave.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amerge.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amix.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amovie.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/anequalizer.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aphasemeter.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asegment.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aselect.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asplit.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/astreamselect.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/bm3d.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/channelsplit.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/concat.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/decimate.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ebur128.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/extractplanes.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/fieldmatch.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/guided.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/headphone.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/hstack.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/interleave.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/join.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/libplacebo.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/limitdiff.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mergeplanes.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mix.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/movie.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/premultiply.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/segment.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/select.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/signature.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/split.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/streamselect.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/unpremultiply.json +0 -6
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/vstack.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xmedian.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xstack.json +0 -9
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/list/filters.json +0 -90747
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/common/cache/list/options.json +0 -1694
- typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/probe.py +0 -75
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/base.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/common/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/common/cache.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/common/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/common/serialize.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/compile_json.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/context.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/compile/validate.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/factory.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/global_runnable/global_args.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/global_runnable/runnable.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/io/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/io/_input.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/io/_output.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/io/output_args.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/nodes.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/dag/utils.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/exceptions.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/utils → typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/ffprobe}/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/filters.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/info.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/py.typed +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/sources.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/streams/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/streams/audio.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/streams/av.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/streams/channel_layout.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/streams/video.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/types.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0/src/typed_ffmpeg/utils/lazy_eval → typed_ffmpeg_compatible-3.2.2/src/typed_ffmpeg/utils}/__init__.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/escaping.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/forzendict.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/lazy_eval/operator.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/lazy_eval/schema.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/run.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/snapshot.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/typing.py +0 -0
- {typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/utils/view.py +0 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
global-exclude **/tests/**
|
2
|
+
global-exclude **/scripts/**
|
3
|
+
global-exclude **/__snapshots__/**
|
4
|
+
global-exclude **/test_*
|
5
|
+
global-exclude **/*_test.py
|
6
|
+
global-exclude **/*.test.py
|
7
|
+
global-exclude **/conftest.py
|
8
|
+
|
9
|
+
# Exclude development and documentation files
|
10
|
+
global-exclude .coveragerc
|
11
|
+
global-exclude .eslintrc.json
|
12
|
+
global-exclude .gitignore
|
13
|
+
global-exclude .gitmodules
|
14
|
+
global-exclude .pre-commit-config.yaml
|
15
|
+
global-exclude .prettierignore
|
16
|
+
global-exclude .prettierrc
|
17
|
+
global-exclude blog.md
|
18
|
+
global-exclude codecov.yml
|
19
|
+
global-exclude mkdocs.yml
|
20
|
+
global-exclude pyproject-compatible.toml
|
21
|
+
global-exclude requirements.in
|
22
|
+
global-exclude requirements.txt
|
23
|
+
global-exclude tox.ini
|
24
|
+
global-exclude uv.lock
|
25
|
+
global-exclude README.ipynb
|
26
|
+
|
27
|
+
# Exclude development container files
|
28
|
+
global-exclude .devcontainer/**
|
29
|
+
global-exclude .github/**
|
30
|
+
|
31
|
+
# Exclude documentation
|
32
|
+
global-exclude docs/**
|
33
|
+
|
34
|
+
# Exclude uv configuration
|
35
|
+
global-exclude .uv/**
|
36
|
+
|
37
|
+
# Include only necessary files
|
38
|
+
include LICENSE
|
39
|
+
include README.md
|
40
|
+
include pyproject.toml
|
41
|
+
include MANIFEST.in
|
@@ -1,15 +1,13 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: typed-ffmpeg-compatible
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2.2
|
4
4
|
Summary: Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation.
|
5
|
-
|
6
|
-
License: MIT
|
5
|
+
Author-email: lucemia <lucemia@gmail.com>
|
6
|
+
License-Expression: MIT
|
7
|
+
Project-URL: Homepage, https://livingbio.github.io/typed-ffmpeg/
|
8
|
+
Project-URL: Repository, https://github.com/livingbio/typed-ffmpeg
|
7
9
|
Keywords: ffmpeg,video,audio,multimedia,filter
|
8
|
-
Author: lucemia
|
9
|
-
Author-email: lucemia@gmail.com
|
10
|
-
Requires-Python: >=3.10,<4.0
|
11
10
|
Classifier: Intended Audience :: Developers
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
13
11
|
Classifier: Natural Language :: English
|
14
12
|
Classifier: Operating System :: OS Independent
|
15
13
|
Classifier: Programming Language :: Python
|
@@ -17,15 +15,18 @@ Classifier: Programming Language :: Python :: 3
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.10
|
18
16
|
Classifier: Programming Language :: Python :: 3.11
|
19
17
|
Classifier: Programming Language :: Python :: 3.12
|
20
|
-
Classifier:
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
21
19
|
Classifier: Topic :: Multimedia :: Video
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
22
22
|
Classifier: Topic :: Software Development :: Libraries
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
24
|
-
|
25
|
-
Provides-Extra: graph
|
26
|
-
Requires-Dist: graphviz ; extra == "graph"
|
27
|
-
Project-URL: Repository, https://github.com/livingbio/typed-ffmpeg
|
24
|
+
Requires-Python: >=3.10
|
28
25
|
Description-Content-Type: text/markdown
|
26
|
+
License-File: LICENSE
|
27
|
+
Provides-Extra: graph
|
28
|
+
Requires-Dist: graphviz; extra == "graph"
|
29
|
+
Dynamic: license-file
|
29
30
|
|
30
31
|
## typed-ffmpeg
|
31
32
|
|
@@ -44,6 +45,7 @@ Description-Content-Type: text/markdown
|
|
44
45
|
- [Installation](#installation)
|
45
46
|
- [Quick Usage](#quick-usage)
|
46
47
|
- [Documentation](https://livingbio.github.io/typed-ffmpeg/)
|
48
|
+
- [Interactive Playground](#interactive-playground)
|
47
49
|
- [Acknowledgements](#acknowledgements)
|
48
50
|
|
49
51
|
---
|
@@ -63,6 +65,7 @@ Description-Content-Type: text/markdown
|
|
63
65
|
- **Validation and Auto-correction:** Assists in identifying and fixing errors within filter graphs.
|
64
66
|
- **Input and Output Options Support:** Provide a more comprehensive interface for input and output options, including support for additional codecs and formats.
|
65
67
|
- **Partial Evaluation:** Enhance the flexibility of filter graphs by enabling partial evaluation, allowing for modular construction and reuse.
|
68
|
+
- **Media File Analysis:** Built-in support for analyzing media files using FFmpeg's ffprobe utility, providing detailed metadata extraction with both dictionary and dataclass interfaces.
|
66
69
|
|
67
70
|
### Planned Features
|
68
71
|
|
@@ -101,12 +104,14 @@ Note: This requires Graphviz to be installed on your system.
|
|
101
104
|
|
102
105
|
Here's how to quickly start using `typed-ffmpeg`:
|
103
106
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
107
|
```python
|
108
108
|
import ffmpeg
|
109
109
|
|
110
|
+
# Analyze a media file
|
111
|
+
info = ffmpeg.probe("video.mp4")
|
112
|
+
print(f"Duration: {float(info['format']['duration']):.2f} seconds")
|
113
|
+
print(f"Streams: {len(info['streams'])}")
|
114
|
+
|
110
115
|
# Flip video horizontally and output
|
111
116
|
f = (
|
112
117
|
ffmpeg
|
@@ -163,6 +168,21 @@ f
|
|
163
168
|
|
164
169
|
See the [Usage](https://livingbio.github.io/typed-ffmpeg/usage/typed/) section in our documentation for more examples and detailed guides.
|
165
170
|
|
171
|
+
---
|
172
|
+
|
173
|
+
## Interactive Playground
|
174
|
+
|
175
|
+
Try out `typed-ffmpeg` directly in your browser with our [Interactive Playground](https://livingbio.github.io/typed-ffmpeg-playground/)! The playground provides a live environment where you can:
|
176
|
+
|
177
|
+

|
178
|
+
|
179
|
+
- Experiment with FFmpeg filters and commands
|
180
|
+
- Visualize filter graphs in real-time
|
181
|
+
- Test different input/output configurations
|
182
|
+
- Learn through interactive examples
|
183
|
+
- Share your filter graphs with others
|
184
|
+
|
185
|
+
The playground is perfect for learning and prototyping FFmpeg filter chains without setting up a local environment.
|
166
186
|
|
167
187
|
---
|
168
188
|
|
@@ -179,4 +199,3 @@ This project is dedicated to my son, Austin, on his seventh birthday (February 2
|
|
179
199
|
---
|
180
200
|
|
181
201
|
Feel free to check the [Documentation](https://livingbio.github.io/typed-ffmpeg/) for detailed information and more advanced features.
|
182
|
-
|
@@ -15,6 +15,7 @@
|
|
15
15
|
- [Installation](#installation)
|
16
16
|
- [Quick Usage](#quick-usage)
|
17
17
|
- [Documentation](https://livingbio.github.io/typed-ffmpeg/)
|
18
|
+
- [Interactive Playground](#interactive-playground)
|
18
19
|
- [Acknowledgements](#acknowledgements)
|
19
20
|
|
20
21
|
---
|
@@ -34,6 +35,7 @@
|
|
34
35
|
- **Validation and Auto-correction:** Assists in identifying and fixing errors within filter graphs.
|
35
36
|
- **Input and Output Options Support:** Provide a more comprehensive interface for input and output options, including support for additional codecs and formats.
|
36
37
|
- **Partial Evaluation:** Enhance the flexibility of filter graphs by enabling partial evaluation, allowing for modular construction and reuse.
|
38
|
+
- **Media File Analysis:** Built-in support for analyzing media files using FFmpeg's ffprobe utility, providing detailed metadata extraction with both dictionary and dataclass interfaces.
|
37
39
|
|
38
40
|
### Planned Features
|
39
41
|
|
@@ -72,12 +74,14 @@ Note: This requires Graphviz to be installed on your system.
|
|
72
74
|
|
73
75
|
Here's how to quickly start using `typed-ffmpeg`:
|
74
76
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
77
|
```python
|
79
78
|
import ffmpeg
|
80
79
|
|
80
|
+
# Analyze a media file
|
81
|
+
info = ffmpeg.probe("video.mp4")
|
82
|
+
print(f"Duration: {float(info['format']['duration']):.2f} seconds")
|
83
|
+
print(f"Streams: {len(info['streams'])}")
|
84
|
+
|
81
85
|
# Flip video horizontally and output
|
82
86
|
f = (
|
83
87
|
ffmpeg
|
@@ -134,6 +138,21 @@ f
|
|
134
138
|
|
135
139
|
See the [Usage](https://livingbio.github.io/typed-ffmpeg/usage/typed/) section in our documentation for more examples and detailed guides.
|
136
140
|
|
141
|
+
---
|
142
|
+
|
143
|
+
## Interactive Playground
|
144
|
+
|
145
|
+
Try out `typed-ffmpeg` directly in your browser with our [Interactive Playground](https://livingbio.github.io/typed-ffmpeg-playground/)! The playground provides a live environment where you can:
|
146
|
+
|
147
|
+

|
148
|
+
|
149
|
+
- Experiment with FFmpeg filters and commands
|
150
|
+
- Visualize filter graphs in real-time
|
151
|
+
- Test different input/output configurations
|
152
|
+
- Learn through interactive examples
|
153
|
+
- Share your filter graphs with others
|
154
|
+
|
155
|
+
The playground is perfect for learning and prototyping FFmpeg filter chains without setting up a local environment.
|
137
156
|
|
138
157
|
---
|
139
158
|
|
@@ -0,0 +1,125 @@
|
|
1
|
+
[project]
|
2
|
+
name = "typed-ffmpeg-compatible"
|
3
|
+
dynamic = ["version"]
|
4
|
+
description = "Modern Python FFmpeg wrappers offer comprehensive support for complex filters, complete with detailed typing and documentation."
|
5
|
+
authors = [
|
6
|
+
{name = "lucemia", email = "lucemia@gmail.com"}
|
7
|
+
]
|
8
|
+
readme = "README.md"
|
9
|
+
requires-python = ">=3.10"
|
10
|
+
license = "MIT"
|
11
|
+
keywords = [
|
12
|
+
"ffmpeg",
|
13
|
+
"video",
|
14
|
+
"audio",
|
15
|
+
"multimedia",
|
16
|
+
"filter",
|
17
|
+
]
|
18
|
+
classifiers = [
|
19
|
+
"Intended Audience :: Developers",
|
20
|
+
"Natural Language :: English",
|
21
|
+
"Operating System :: OS Independent",
|
22
|
+
"Programming Language :: Python",
|
23
|
+
"Programming Language :: Python :: 3",
|
24
|
+
"Programming Language :: Python :: 3.10",
|
25
|
+
"Programming Language :: Python :: 3.11",
|
26
|
+
"Programming Language :: Python :: 3.12",
|
27
|
+
"Programming Language :: Python :: 3.13",
|
28
|
+
"Topic :: Multimedia :: Video",
|
29
|
+
"Topic :: Multimedia :: Sound/Audio",
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
31
|
+
"Topic :: Software Development :: Libraries",
|
32
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
33
|
+
]
|
34
|
+
|
35
|
+
[project.scripts]
|
36
|
+
scripts = "scripts.main:app"
|
37
|
+
|
38
|
+
[project.urls]
|
39
|
+
Homepage = "https://livingbio.github.io/typed-ffmpeg/"
|
40
|
+
Repository = "https://github.com/livingbio/typed-ffmpeg"
|
41
|
+
|
42
|
+
[project.optional-dependencies]
|
43
|
+
graph = ["graphviz"]
|
44
|
+
|
45
|
+
[build-system]
|
46
|
+
requires = ["setuptools>=61.0", "setuptools-scm>=6.2"]
|
47
|
+
build-backend = "setuptools.build_meta"
|
48
|
+
|
49
|
+
[tool.setuptools]
|
50
|
+
package-dir = {"" = "src"}
|
51
|
+
|
52
|
+
[tool.setuptools.packages.find]
|
53
|
+
where = ["src"]
|
54
|
+
include = ["typed_ffmpeg*"]
|
55
|
+
exclude = [
|
56
|
+
"**/tests/**",
|
57
|
+
"**/scripts/**",
|
58
|
+
"**/__snapshots__/**",
|
59
|
+
"**/test_*",
|
60
|
+
"**/*_test.py",
|
61
|
+
"**/*.test.py",
|
62
|
+
"**/conftest.py"
|
63
|
+
]
|
64
|
+
|
65
|
+
[tool.setuptools.package-data]
|
66
|
+
"ffmpeg" = ["py.typed"]
|
67
|
+
"ffmpeg.common" = ["*"]
|
68
|
+
"ffmpeg.common.cache" = ["*"]
|
69
|
+
"ffmpeg.common.cache.list" = ["*"]
|
70
|
+
"ffmpeg.common.cache.FFMpegFilterManuallyDefined" = ["*"]
|
71
|
+
"ffmpeg.utils" = ["*"]
|
72
|
+
"ffmpeg.utils.lazy_eval" = ["*"]
|
73
|
+
exclude = [
|
74
|
+
"**/tests/**",
|
75
|
+
"**/scripts/**",
|
76
|
+
"**/__snapshots__/**",
|
77
|
+
"**/test_*",
|
78
|
+
"**/*_test.py",
|
79
|
+
"**/*.test.py",
|
80
|
+
"**/conftest.py"
|
81
|
+
]
|
82
|
+
|
83
|
+
[tool.setuptools_scm]
|
84
|
+
version_scheme = "post-release"
|
85
|
+
local_scheme = "node-and-timestamp"
|
86
|
+
write_to = "src/typed_ffmpeg/_version.py"
|
87
|
+
|
88
|
+
[tool.mypy]
|
89
|
+
exclude = "^(migrations|commands|sandbox|samples|sdk)/"
|
90
|
+
show_error_codes = true
|
91
|
+
implicit_optional = true
|
92
|
+
follow_imports = "silent"
|
93
|
+
warn_redundant_casts = true
|
94
|
+
warn_unused_ignores = true
|
95
|
+
disallow_any_generics = true
|
96
|
+
check_untyped_defs = true
|
97
|
+
no_implicit_reexport = true
|
98
|
+
disallow_untyped_defs = true
|
99
|
+
|
100
|
+
[tool.ruff]
|
101
|
+
line-length = 88
|
102
|
+
exclude = ["**/cassettes/", "**/__snapshots__/"]
|
103
|
+
src = ["src"]
|
104
|
+
|
105
|
+
[tool.ruff.lint]
|
106
|
+
select = [
|
107
|
+
"E", # pycodestyle errors
|
108
|
+
"W", # pycodestyle warnings
|
109
|
+
"F", # pyflakes
|
110
|
+
"I", # isort
|
111
|
+
"B006", # flake8-bugbear: Do not use mutable data structures for argument defaults
|
112
|
+
"C4", # flake8-comprehensions
|
113
|
+
"UP", # pyupgrade
|
114
|
+
]
|
115
|
+
ignore = [
|
116
|
+
"E741", # Ambiguous variable name: `I`
|
117
|
+
"E501", # Line too long
|
118
|
+
"E402", # Module level import not at top of file
|
119
|
+
"E712", # not work for pandas
|
120
|
+
"F811", # FIXME: we relay on it to share fixture across app
|
121
|
+
]
|
122
|
+
|
123
|
+
[tool.ruff.format]
|
124
|
+
quote-style = "double"
|
125
|
+
docstring-code-format = true
|
{typed_ffmpeg_compatible-3.0.2a0 → typed_ffmpeg_compatible-3.2.2}/src/typed_ffmpeg/__init__.py
RENAMED
@@ -26,8 +26,8 @@ from . import compile, dag, filters, sources
|
|
26
26
|
from .base import afilter, filter_multi_output, input, merge_outputs, output, vfilter
|
27
27
|
from .dag import Stream
|
28
28
|
from .exceptions import FFMpegExecuteError, FFMpegTypeError, FFMpegValueError
|
29
|
+
from .ffprobe.probe import probe, probe_obj
|
29
30
|
from .info import get_codecs, get_decoders, get_encoders
|
30
|
-
from .probe import probe
|
31
31
|
from .streams import AudioStream, AVStream, VideoStream
|
32
32
|
|
33
33
|
__all__ = [
|
@@ -41,6 +41,7 @@ __all__ = [
|
|
41
41
|
"FFMpegValueError",
|
42
42
|
"Stream",
|
43
43
|
"probe",
|
44
|
+
"probe_obj",
|
44
45
|
"compile",
|
45
46
|
"AudioStream",
|
46
47
|
"VideoStream",
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
6
|
+
TYPE_CHECKING = False
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
12
|
+
else:
|
13
|
+
VERSION_TUPLE = object
|
14
|
+
|
15
|
+
version: str
|
16
|
+
__version__: str
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
18
|
+
version_tuple: VERSION_TUPLE
|
19
|
+
|
20
|
+
__version__ = version = '3.2.2'
|
21
|
+
__version_tuple__ = version_tuple = (3, 2, 2)
|
@@ -21,13 +21,10 @@ import shlex
|
|
21
21
|
from collections import defaultdict
|
22
22
|
from collections.abc import Mapping
|
23
23
|
|
24
|
-
from ffmpeg.dag.factory import filter_node_factory
|
25
|
-
from ffmpeg.streams.audio import AudioStream
|
26
|
-
from ffmpeg.streams.video import VideoStream
|
27
|
-
|
28
24
|
from ..base import input, merge_outputs, output
|
29
25
|
from ..common.cache import load
|
30
26
|
from ..common.schema import FFMpegFilter, FFMpegFilterDef, FFMpegOption, StreamType
|
27
|
+
from ..dag.factory import filter_node_factory
|
31
28
|
from ..dag.nodes import (
|
32
29
|
FilterableStream,
|
33
30
|
FilterNode,
|
@@ -39,7 +36,9 @@ from ..dag.nodes import (
|
|
39
36
|
from ..dag.schema import Node, Stream
|
40
37
|
from ..exceptions import FFMpegValueError
|
41
38
|
from ..schema import Default
|
39
|
+
from ..streams.audio import AudioStream
|
42
40
|
from ..streams.av import AVStream
|
41
|
+
from ..streams.video import VideoStream
|
43
42
|
from ..utils.escaping import escape
|
44
43
|
from ..utils.lazy_eval.schema import LazyValue
|
45
44
|
from ..utils.run import command_line
|
@@ -153,6 +152,15 @@ def parse_output(
|
|
153
152
|
stream = parse_stream_selector(map_option, in_streams)
|
154
153
|
inputs.append(stream)
|
155
154
|
|
155
|
+
if not inputs:
|
156
|
+
# NOTE: if there is no inputs, and there is only one input node
|
157
|
+
if len([k for k in in_streams if isinstance(in_streams[k], AVStream)]) == 1:
|
158
|
+
inputs = [
|
159
|
+
in_streams[k]
|
160
|
+
for k in in_streams
|
161
|
+
if isinstance(in_streams[k], AVStream)
|
162
|
+
]
|
163
|
+
|
156
164
|
assert inputs, f"No inputs found for output {filename}"
|
157
165
|
export.append(output(*inputs, filename=filename, extra_options=options))
|
158
166
|
buffer = []
|
@@ -309,37 +317,21 @@ def parse_global(
|
|
309
317
|
For tokens like ['-y', '-loglevel', 'quiet', '-i', 'input.mp4']:
|
310
318
|
Returns ({'y': True, 'loglevel': 'quiet'}, ['-i', 'input.mp4'])
|
311
319
|
"""
|
312
|
-
|
313
|
-
remaining_tokens = tokens.
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
if not option.is_global_option:
|
325
|
-
continue
|
326
|
-
|
327
|
-
if len(remaining_tokens) > 1 and not remaining_tokens[1].startswith("-"):
|
328
|
-
# Option with value
|
329
|
-
global_params[option_name] = remaining_tokens[1]
|
330
|
-
remaining_tokens = remaining_tokens[2:]
|
320
|
+
options = parse_options(tokens[: tokens.index("-i")])
|
321
|
+
remaining_tokens = tokens[tokens.index("-i") :]
|
322
|
+
parameters: dict[str, str | bool] = {}
|
323
|
+
|
324
|
+
for key, value in options.items():
|
325
|
+
assert key in ffmpeg_options, f"Unknown option: {key}"
|
326
|
+
option = ffmpeg_options[key]
|
327
|
+
|
328
|
+
if option.is_global_option:
|
329
|
+
# Process only recognized global options
|
330
|
+
if value[-1] is None:
|
331
|
+
parameters[key] = True
|
331
332
|
else:
|
332
|
-
|
333
|
-
|
334
|
-
global_params[option_name[2:]] = False
|
335
|
-
else:
|
336
|
-
global_params[option_name] = True
|
337
|
-
remaining_tokens = remaining_tokens[1:]
|
338
|
-
else:
|
339
|
-
# Skip non-option tokens
|
340
|
-
remaining_tokens = remaining_tokens[1:]
|
341
|
-
|
342
|
-
return global_params, remaining_tokens
|
333
|
+
parameters[key] = value[-1]
|
334
|
+
return parameters, remaining_tokens
|
343
335
|
|
344
336
|
|
345
337
|
def parse(cli: str) -> Stream:
|
@@ -348,7 +340,7 @@ def parse(cli: str) -> Stream:
|
|
348
340
|
ffmpeg_filters = get_filter_dict()
|
349
341
|
|
350
342
|
tokens = shlex.split(cli)
|
351
|
-
assert tokens[0] == "ffmpeg"
|
343
|
+
assert tokens[0].lower().split(".")[0] == "ffmpeg"
|
352
344
|
tokens = tokens[1:]
|
353
345
|
|
354
346
|
# Parse global options first
|
@@ -677,6 +669,22 @@ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
|
|
677
669
|
if context:
|
678
670
|
for input in node.inputs:
|
679
671
|
if isinstance(input.node, InputNode):
|
672
|
+
# NOTE: specially rules,
|
673
|
+
# if there is only one input node,
|
674
|
+
# only one output node,
|
675
|
+
# the output node has only one input,
|
676
|
+
# and the stream selector is not specified,
|
677
|
+
# then the map can be ignore.
|
678
|
+
if (
|
679
|
+
input.index is None
|
680
|
+
and isinstance(input, AVStream)
|
681
|
+
and len([k for k in context.all_nodes if isinstance(k, InputNode)])
|
682
|
+
== 1
|
683
|
+
and len([k for k in context.all_nodes if isinstance(k, OutputNode)])
|
684
|
+
== 1
|
685
|
+
and len(node.inputs) == 1
|
686
|
+
):
|
687
|
+
continue
|
680
688
|
commands += ["-map", get_stream_label(input, context)]
|
681
689
|
else:
|
682
690
|
commands += ["-map", f"[{get_stream_label(input, context)}]"]
|
@@ -3,10 +3,6 @@ from __future__ import annotations
|
|
3
3
|
from collections.abc import Mapping
|
4
4
|
from typing import Any
|
5
5
|
|
6
|
-
from ffmpeg.streams.audio import AudioStream
|
7
|
-
from ffmpeg.streams.av import AVStream
|
8
|
-
from ffmpeg.streams.video import VideoStream
|
9
|
-
|
10
6
|
from ..common.cache import load
|
11
7
|
from ..common.schema import FFMpegFilter
|
12
8
|
from ..dag.nodes import (
|
@@ -19,6 +15,9 @@ from ..dag.nodes import (
|
|
19
15
|
OutputStream,
|
20
16
|
)
|
21
17
|
from ..dag.schema import Node, Stream
|
18
|
+
from ..streams.audio import AudioStream
|
19
|
+
from ..streams.av import AVStream
|
20
|
+
from ..streams.video import VideoStream
|
22
21
|
from .context import DAGContext
|
23
22
|
from .validate import validate
|
24
23
|
|
@@ -0,0 +1,133 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
import json
|
4
|
+
import types
|
5
|
+
from dataclasses import is_dataclass
|
6
|
+
from typing import (
|
7
|
+
Any,
|
8
|
+
TypeGuard,
|
9
|
+
TypeVar,
|
10
|
+
Union,
|
11
|
+
cast,
|
12
|
+
get_args,
|
13
|
+
get_origin,
|
14
|
+
get_type_hints,
|
15
|
+
)
|
16
|
+
|
17
|
+
from .schema import ffprobeType, registered_types
|
18
|
+
from .xml2json import xml_string_to_json
|
19
|
+
|
20
|
+
T = TypeVar("T")
|
21
|
+
|
22
|
+
|
23
|
+
def _get_actual_type(type_hint: Any) -> type[Any]:
|
24
|
+
"""
|
25
|
+
Get the actual type from a type hint, handling Optional and Union (including | syntax).
|
26
|
+
|
27
|
+
Args:
|
28
|
+
type_hint: The type hint to get the actual type from
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
The actual type
|
32
|
+
"""
|
33
|
+
# If type_hint is a string, evaluate it in the schema's module context
|
34
|
+
if isinstance(type_hint, str):
|
35
|
+
type_hint = registered_types[type_hint]
|
36
|
+
|
37
|
+
origin = get_origin(type_hint)
|
38
|
+
# Handle typing.Union and types.UnionType (for int | None in Python 3.10+)
|
39
|
+
if origin is Union or (hasattr(types, "UnionType") and origin is types.UnionType):
|
40
|
+
non_none_types = [t for t in get_args(type_hint) if t is not type(None)]
|
41
|
+
if non_none_types:
|
42
|
+
return _get_actual_type(non_none_types[0])
|
43
|
+
# Handle direct types.UnionType (for int | None in Python 3.10+)
|
44
|
+
if hasattr(types, "UnionType") and isinstance(type_hint, types.UnionType):
|
45
|
+
non_none_types = [t for t in type_hint.__args__ if t is not type(None)]
|
46
|
+
if non_none_types:
|
47
|
+
return _get_actual_type(non_none_types[0])
|
48
|
+
return type_hint
|
49
|
+
|
50
|
+
|
51
|
+
def is_dataclass_type(obj: type[Any]) -> TypeGuard[type[T]]:
|
52
|
+
"""
|
53
|
+
Check if an object is a dataclass type.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
obj: The object to check
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
True if the object is a dataclass type, False otherwise
|
60
|
+
"""
|
61
|
+
return is_dataclass(obj)
|
62
|
+
|
63
|
+
|
64
|
+
def _parse_obj_from_dict(data: Any, cls: type[T]) -> T | None:
|
65
|
+
"""
|
66
|
+
Parse a dictionary into a dataclass instance.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
data: The dictionary to parse
|
70
|
+
cls: The dataclass to parse into
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
The parsed dataclass instance
|
74
|
+
"""
|
75
|
+
if data is None:
|
76
|
+
return None
|
77
|
+
|
78
|
+
if isinstance(cls, type):
|
79
|
+
if cls is str:
|
80
|
+
return cast(T, str(data))
|
81
|
+
elif cls is int:
|
82
|
+
return cast(T, int(data))
|
83
|
+
elif cls is float:
|
84
|
+
return cast(T, float(data))
|
85
|
+
elif cls is bool:
|
86
|
+
return cast(T, bool(data))
|
87
|
+
|
88
|
+
if not isinstance(data, dict):
|
89
|
+
return cls()
|
90
|
+
|
91
|
+
if isinstance(cls, str): # NOTE: python 3.10
|
92
|
+
cls = registered_types[cls]
|
93
|
+
|
94
|
+
type_hints = get_type_hints(cls)
|
95
|
+
kwargs: dict[str, Any] = {}
|
96
|
+
|
97
|
+
for field_name, field_type in type_hints.items():
|
98
|
+
actual_type = _get_actual_type(field_type)
|
99
|
+
|
100
|
+
if get_origin(actual_type) is tuple:
|
101
|
+
tuple_args = get_args(actual_type)
|
102
|
+
if not tuple_args:
|
103
|
+
continue
|
104
|
+
item_type = tuple_args[0]
|
105
|
+
value = data.get(field_name, [])
|
106
|
+
# NOTE: if the value is a single item, convert it to a list (xml's issue)
|
107
|
+
if not isinstance(value, list):
|
108
|
+
value = [value]
|
109
|
+
kwargs[field_name] = tuple(
|
110
|
+
_parse_obj_from_dict(item, item_type) for item in value
|
111
|
+
)
|
112
|
+
continue
|
113
|
+
|
114
|
+
kwargs[field_name] = _parse_obj_from_dict(data.get(field_name), actual_type)
|
115
|
+
|
116
|
+
return cls(**kwargs)
|
117
|
+
|
118
|
+
|
119
|
+
def parse_ffprobe(xml_string: str) -> ffprobeType:
|
120
|
+
"""
|
121
|
+
Parse ffprobe XML output into ffprobeType dataclass using JSON dict.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
xml_string: The XML string to parse
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
The parsed ffprobeType instance
|
128
|
+
"""
|
129
|
+
json_str = xml_string_to_json(xml_string)
|
130
|
+
json_dict = json.loads(json_str)
|
131
|
+
# The root key is 'ffprobe'
|
132
|
+
root_data = json_dict.get("ffprobe", json_dict)
|
133
|
+
return _parse_obj_from_dict(root_data, ffprobeType) or ffprobeType()
|