videopython 0.47.0__tar.gz → 0.49.0__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.
- {videopython-0.47.0 → videopython-0.49.0}/PKG-INFO +2 -48
- {videopython-0.47.0 → videopython-0.49.0}/README.md +0 -2
- {videopython-0.47.0 → videopython-0.49.0}/pyproject.toml +23 -79
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/__init__.py +21 -0
- videopython-0.49.0/src/videopython/ai/_ollama.py +71 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/_revisions.py +0 -11
- videopython-0.49.0/src/videopython/ai/auto_edit/__init__.py +29 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/backend.py +37 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/catalog.py +90 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/editor.py +140 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/local.py +39 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/models.py +105 -0
- videopython-0.49.0/src/videopython/ai/auto_edit/resolve.py +34 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/__init__.py +5 -6
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/config.py +7 -8
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/dubber.py +2 -2
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/loudness.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/models.py +3 -5
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/pipeline.py +6 -64
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/effects.py +8 -2
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/generation/audio.py +2 -2
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/generation/image.py +1 -1
- videopython-0.49.0/src/videopython/ai/generation/translation.py +375 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/generation/video.py +2 -4
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/audio.py +5 -5
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/faces.py +1 -1
- videopython-0.49.0/src/videopython/ai/understanding/image.py +79 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/objects.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/separation.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/temporal.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/analyzer.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/models.py +1 -1
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/sampling.py +1 -1
- videopython-0.49.0/src/videopython/editing/_schema.py +42 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/video_edit.py +11 -44
- videopython-0.47.0/src/videopython/ai/generation/qwen3.py +0 -504
- videopython-0.47.0/src/videopython/ai/generation/translation.py +0 -347
- videopython-0.47.0/src/videopython/ai/understanding/image.py +0 -400
- {videopython-0.47.0 → videopython-0.49.0}/.gitignore +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/LICENSE +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/_optional.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/_predictor.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/description.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/base/video.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.47.0 → videopython-0.49.0}/src/videopython/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: videopython
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.49.0
|
|
4
4
|
Summary: Minimal video generation and processing library.
|
|
5
5
|
Project-URL: Homepage, https://videopython.com
|
|
6
6
|
Project-URL: Repository, https://github.com/bartwojtowicz/videopython/
|
|
@@ -28,60 +28,16 @@ Requires-Dist: chatterbox-tts>=0.1.7; extra == 'ai'
|
|
|
28
28
|
Requires-Dist: demucs>=4.0.0; extra == 'ai'
|
|
29
29
|
Requires-Dist: diffusers>=0.30.0; extra == 'ai'
|
|
30
30
|
Requires-Dist: imagehash>=4.3; extra == 'ai'
|
|
31
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: ollama>=0.5.0; extra == 'ai'
|
|
32
32
|
Requires-Dist: openai-whisper>=20240930; extra == 'ai'
|
|
33
33
|
Requires-Dist: pyannote-audio>=4.0.0; extra == 'ai'
|
|
34
34
|
Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
|
|
35
|
-
Requires-Dist: qwen-vl-utils>=0.0.10; extra == 'ai'
|
|
36
|
-
Requires-Dist: sentencepiece>=0.1.99; extra == 'ai'
|
|
37
35
|
Requires-Dist: silero-vad>=5.1; extra == 'ai'
|
|
38
36
|
Requires-Dist: torch>=2.8.0; extra == 'ai'
|
|
39
37
|
Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
40
38
|
Requires-Dist: transformers>=5.2.0; extra == 'ai'
|
|
41
39
|
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
|
|
42
40
|
Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
|
|
43
|
-
Provides-Extra: asr
|
|
44
|
-
Requires-Dist: openai-whisper>=20240930; extra == 'asr'
|
|
45
|
-
Requires-Dist: pyannote-audio>=4.0.0; extra == 'asr'
|
|
46
|
-
Requires-Dist: silero-vad>=5.1; extra == 'asr'
|
|
47
|
-
Requires-Dist: torch>=2.8.0; extra == 'asr'
|
|
48
|
-
Requires-Dist: torchaudio>=2.8.0; extra == 'asr'
|
|
49
|
-
Provides-Extra: dub
|
|
50
|
-
Requires-Dist: demucs>=4.0.0; extra == 'dub'
|
|
51
|
-
Requires-Dist: llama-cpp-python>=0.3.0; extra == 'dub'
|
|
52
|
-
Requires-Dist: openai-whisper>=20240930; extra == 'dub'
|
|
53
|
-
Requires-Dist: pyannote-audio>=4.0.0; extra == 'dub'
|
|
54
|
-
Requires-Dist: pyloudnorm>=0.1.1; extra == 'dub'
|
|
55
|
-
Requires-Dist: sentencepiece>=0.1.99; extra == 'dub'
|
|
56
|
-
Requires-Dist: silero-vad>=5.1; extra == 'dub'
|
|
57
|
-
Requires-Dist: torch>=2.8.0; extra == 'dub'
|
|
58
|
-
Requires-Dist: torchaudio>=2.8.0; extra == 'dub'
|
|
59
|
-
Requires-Dist: transformers>=5.2.0; extra == 'dub'
|
|
60
|
-
Provides-Extra: generation
|
|
61
|
-
Requires-Dist: accelerate>=0.29.2; extra == 'generation'
|
|
62
|
-
Requires-Dist: diffusers>=0.30.0; extra == 'generation'
|
|
63
|
-
Requires-Dist: torch>=2.8.0; extra == 'generation'
|
|
64
|
-
Requires-Dist: torchaudio>=2.8.0; extra == 'generation'
|
|
65
|
-
Requires-Dist: transformers>=5.2.0; extra == 'generation'
|
|
66
|
-
Provides-Extra: separation
|
|
67
|
-
Requires-Dist: demucs>=4.0.0; extra == 'separation'
|
|
68
|
-
Requires-Dist: torch>=2.8.0; extra == 'separation'
|
|
69
|
-
Requires-Dist: torchaudio>=2.8.0; extra == 'separation'
|
|
70
|
-
Provides-Extra: translation
|
|
71
|
-
Requires-Dist: llama-cpp-python>=0.3.0; extra == 'translation'
|
|
72
|
-
Requires-Dist: sentencepiece>=0.1.99; extra == 'translation'
|
|
73
|
-
Requires-Dist: torch>=2.8.0; extra == 'translation'
|
|
74
|
-
Requires-Dist: transformers>=5.2.0; extra == 'translation'
|
|
75
|
-
Provides-Extra: tts
|
|
76
|
-
Requires-Dist: chatterbox-tts>=0.1.7; extra == 'tts'
|
|
77
|
-
Provides-Extra: vision
|
|
78
|
-
Requires-Dist: imagehash>=4.3; extra == 'vision'
|
|
79
|
-
Requires-Dist: qwen-vl-utils>=0.0.10; extra == 'vision'
|
|
80
|
-
Requires-Dist: torch>=2.8.0; extra == 'vision'
|
|
81
|
-
Requires-Dist: torchaudio>=2.8.0; extra == 'vision'
|
|
82
|
-
Requires-Dist: transformers>=5.2.0; extra == 'vision'
|
|
83
|
-
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'vision'
|
|
84
|
-
Requires-Dist: ultralytics>=8.0.0; extra == 'vision'
|
|
85
41
|
Description-Content-Type: text/markdown
|
|
86
42
|
|
|
87
43
|
# videopython
|
|
@@ -104,8 +60,6 @@ pip install videopython # core video/audio editing
|
|
|
104
60
|
pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
105
61
|
```
|
|
106
62
|
|
|
107
|
-
AI deps are also split into granular extras so you can install only what you need: `[asr]` (transcription), `[vision]` (detection/scene/VLM), `[separation]`, `[translation]`, `[tts]` (voice cloning), `[generation]` (image/video/music), and `[dub]` (the dubbing pipeline). `[dub]` excludes chatterbox — add `[tts]` for local synthesis (`pip install "videopython[dub,tts]"`) or inject a `SpeechBackend`. See the [Installation Guide](https://videopython.com/getting-started/installation/) for the full table.
|
|
108
|
-
|
|
109
63
|
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
110
64
|
|
|
111
65
|
## Quick Start
|
|
@@ -18,8 +18,6 @@ pip install videopython # core video/audio editing
|
|
|
18
18
|
pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
AI deps are also split into granular extras so you can install only what you need: `[asr]` (transcription), `[vision]` (detection/scene/VLM), `[separation]`, `[translation]`, `[tts]` (voice cloning), `[generation]` (image/video/music), and `[dub]` (the dubbing pipeline). `[dub]` excludes chatterbox — add `[tts]` for local synthesis (`pip install "videopython[dub,tts]"`) or inject a `SpeechBackend`. See the [Installation Guide](https://videopython.com/getting-started/installation/) for the full table.
|
|
22
|
-
|
|
23
21
|
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use.
|
|
24
22
|
|
|
25
23
|
## Quick Start
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "videopython"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.49.0"
|
|
4
4
|
description = "Minimal video generation and processing library."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
|
|
@@ -53,90 +53,42 @@ dev = [
|
|
|
53
53
|
"mkdocstrings>=1.0.0",
|
|
54
54
|
"mkdocstrings-python>=2.0.1",
|
|
55
55
|
]
|
|
56
|
-
# Thin dev-workflow group so `uv sync` / uv.lock keep working.
|
|
57
|
-
#
|
|
58
|
-
# there is a single source of truth (the granular extras) and no hand-sync.
|
|
59
|
-
# `test_packaging_extras.py` asserts this stays consistent with the extra.
|
|
56
|
+
# Thin dev-workflow group so `uv sync` / uv.lock keep working. References the
|
|
57
|
+
# `[ai]` extra so dev installs every AI capability (single source of truth).
|
|
60
58
|
ai = ["videopython[ai]"]
|
|
61
59
|
|
|
62
|
-
# pip install videopython[
|
|
60
|
+
# pip install "videopython[ai]" - pip uses optional-dependencies, not dependency-groups.
|
|
63
61
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# `test_packaging_extras.py` enforces union(granular) == ai as a drift guard.
|
|
62
|
+
# A single [ai] extra installs every AI capability. The heavy ML deps still load
|
|
63
|
+
# lazily at first use (no top-level imports under ai/), so importing videopython
|
|
64
|
+
# stays light even with [ai] installed.
|
|
68
65
|
[project.optional-dependencies]
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
ai = [
|
|
67
|
+
# Speech recognition / diarization (understanding/audio.py)
|
|
71
68
|
"openai-whisper>=20240930",
|
|
72
69
|
"pyannote-audio>=4.0.0",
|
|
73
|
-
# Voice activity detection (used by AudioToText to gate Whisper language detection)
|
|
74
70
|
"silero-vad>=5.1",
|
|
75
|
-
|
|
76
|
-
"torchaudio>=2.8.0",
|
|
77
|
-
]
|
|
78
|
-
# Visual understanding: detection, scene/temporal, VLM (understanding/{faces,
|
|
79
|
-
# objects,temporal,image}.py + video_analysis). transformers also backs the AST
|
|
80
|
-
# AudioClassifier and the SceneVLM; qwen-vl-utils is the SceneVLM preprocessor;
|
|
81
|
-
# imagehash is the SceneVLM frame-dedup; transnetv2 is scene detection.
|
|
82
|
-
vision = [
|
|
71
|
+
# Visual understanding: detection, scene/temporal, VLM (understanding/*, video_analysis)
|
|
83
72
|
"ultralytics>=8.0.0",
|
|
84
73
|
"transnetv2-pytorch>=1.0.5",
|
|
85
74
|
"transformers>=5.2.0",
|
|
86
|
-
"qwen-vl-utils>=0.0.10",
|
|
87
75
|
"imagehash>=4.3",
|
|
88
|
-
|
|
89
|
-
"torchaudio>=2.8.0",
|
|
90
|
-
]
|
|
91
|
-
# Audio source separation (understanding/separation.py).
|
|
92
|
-
separation = [
|
|
76
|
+
# Audio source separation (understanding/separation.py)
|
|
93
77
|
"demucs>=4.0.0",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
]
|
|
97
|
-
# Text translation backends (generation/translation.py MarianMT +
|
|
98
|
-
# generation/qwen3.py Qwen3 GGUF). MarianTokenizer requires sentencepiece.
|
|
99
|
-
translation = [
|
|
100
|
-
"transformers>=5.2.0",
|
|
101
|
-
"sentencepiece>=0.1.99",
|
|
102
|
-
"llama-cpp-python>=0.3.0",
|
|
103
|
-
"torch>=2.8.0",
|
|
104
|
-
]
|
|
105
|
-
# Voice cloning TTS (generation/audio.py — Chatterbox Multilingual by Resemble
|
|
106
|
-
# AI). ISOLATED: chatterbox-tts 0.1.7 carries the strict pins that force the
|
|
107
|
-
# [tool.uv].override-dependencies block. This is the ONLY extra that drags them.
|
|
108
|
-
# Left to chatterbox's own torch==2.6.0 / torchaudio==2.6.0 pins (no floors
|
|
109
|
-
# here) so `pip install videopython[tts]` resolves STANDALONE in its own
|
|
110
|
-
# image/Modal function -- the whole point of running TTS behind SpeechBackend.
|
|
111
|
-
# The all-in [ai] resolve forces torch>=2.8.0 over this via override-dependencies.
|
|
112
|
-
tts = [
|
|
78
|
+
# Voice cloning TTS (generation/audio.py — Chatterbox); its strict pins drive
|
|
79
|
+
# the [tool.uv].override-dependencies block below.
|
|
113
80
|
"chatterbox-tts>=0.1.7",
|
|
114
|
-
|
|
115
|
-
# Local media generation: SDXL/CogVideoX (generation/{image,video}.py) +
|
|
116
|
-
# MusicGen (generation/audio.py TextToMusic, via transformers).
|
|
117
|
-
generation = [
|
|
81
|
+
# Local media generation: SDXL/CogVideoX + MusicGen (generation/*)
|
|
118
82
|
"diffusers>=0.30.0",
|
|
119
|
-
"transformers>=5.2.0",
|
|
120
83
|
"accelerate>=0.29.2",
|
|
84
|
+
# Dubbing loudness matching (dubbing/loudness.py)
|
|
85
|
+
"pyloudnorm>=0.1.1",
|
|
86
|
+
# LLM-authored editing planner (auto_edit/local.py OllamaVisionLLM)
|
|
87
|
+
"ollama>=0.5.0",
|
|
88
|
+
# Shared torch stack
|
|
121
89
|
"torch>=2.8.0",
|
|
122
90
|
"torchaudio>=2.8.0",
|
|
123
91
|
]
|
|
124
|
-
# Dubbing pipeline's hard top-level deps: transcription (asr) + source
|
|
125
|
-
# separation (separation) + translation, plus BS.1770 loudness matching
|
|
126
|
-
# (dubbing/loudness.py). Deliberately EXCLUDES tts/chatterbox: local synthesis
|
|
127
|
-
# runs through the SpeechBackend protocol against a separately-installed [tts]
|
|
128
|
-
# extra or an injected remote backend, so a [dub] image co-resolves WITHOUT
|
|
129
|
-
# chatterbox's override-forcing pin. A bare [dub] install that hits local
|
|
130
|
-
# synthesis raises a clear [tts]-pointing ImportError.
|
|
131
|
-
dub = [
|
|
132
|
-
"videopython[asr,separation,translation]",
|
|
133
|
-
# Loudness measurement (BS.1770) for dub-vs-source loudness matching (M3)
|
|
134
|
-
"pyloudnorm>=0.1.1",
|
|
135
|
-
]
|
|
136
|
-
# Convenience aggregate: installs every capability. PEP 685 self-references make
|
|
137
|
-
# this the single "everything" install with no hand-duplicated dep list. This
|
|
138
|
-
# combined resolve is what the [tool.uv].override-dependencies block exists for.
|
|
139
|
-
ai = ["videopython[asr,vision,separation,translation,tts,generation,dub]"]
|
|
140
92
|
|
|
141
93
|
[project.urls]
|
|
142
94
|
Homepage = "https://videopython.com"
|
|
@@ -164,24 +116,16 @@ module = [
|
|
|
164
116
|
"pyannote", "pyannote.*",
|
|
165
117
|
"silero_vad", "silero_vad.*",
|
|
166
118
|
"cv2", "cv2.*",
|
|
167
|
-
"llama_cpp", "llama_cpp.*",
|
|
168
119
|
"pyloudnorm", "pyloudnorm.*",
|
|
169
|
-
"qwen_vl_utils", "qwen_vl_utils.*",
|
|
170
120
|
"imagehash", "imagehash.*",
|
|
171
121
|
]
|
|
172
122
|
ignore_missing_imports = true
|
|
173
123
|
|
|
174
124
|
[tool.uv]
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# `[generation]`). This override lets the combined resolver pick compatible
|
|
180
|
-
# versions. A consumer installing a SUBSET that excludes chatterbox — notably
|
|
181
|
-
# `[dub]` (asr+separation+translation+pyloudnorm, no tts) — co-resolves cleanly
|
|
182
|
-
# WITHOUT this override; that clean `[dub]` resolve is the whole point of
|
|
183
|
-
# isolating `[tts]`. The ai dependency floors are aligned with these overrides
|
|
184
|
-
# to keep pip and uv resolving similar versions.
|
|
125
|
+
# chatterbox-tts 0.1.7 (in [ai]) pins strict torch/torchaudio/numpy/diffusers
|
|
126
|
+
# that conflict with pyannote-audio (torch>=2.8) and CogVideoX (diffusers>=0.30).
|
|
127
|
+
# These overrides let the combined [ai] resolve pick compatible versions; the
|
|
128
|
+
# floors in [ai] are aligned with them so pip and uv resolve similar versions.
|
|
185
129
|
override-dependencies = [
|
|
186
130
|
"torch>=2.8.0", "torchaudio>=2.8.0", "numpy>=2.0.0", "diffusers>=0.30.0",
|
|
187
131
|
# ultralytics depends on opencv-python which conflicts with our
|
|
@@ -24,6 +24,16 @@ if TYPE_CHECKING:
|
|
|
24
24
|
# Redundant aliases mark these as intentional re-exports (mypy/IDE see the
|
|
25
25
|
# symbols; ruff doesn't flag them as unused). Runtime resolution is lazy via
|
|
26
26
|
# __getattr__ below.
|
|
27
|
+
from .auto_edit import AutoEditError as AutoEditError
|
|
28
|
+
from .auto_edit import AutoEditor as AutoEditor
|
|
29
|
+
from .auto_edit import EditCatalog as EditCatalog
|
|
30
|
+
from .auto_edit import EditPlan as EditPlan
|
|
31
|
+
from .auto_edit import ImagePart as ImagePart
|
|
32
|
+
from .auto_edit import OllamaVisionLLM as OllamaVisionLLM
|
|
33
|
+
from .auto_edit import PlannerError as PlannerError
|
|
34
|
+
from .auto_edit import StructuredVisionLLM as StructuredVisionLLM
|
|
35
|
+
from .auto_edit import TextPart as TextPart
|
|
36
|
+
from .auto_edit import build_catalog as build_catalog
|
|
27
37
|
from .effects import ObjectDetectionOverlay as ObjectDetectionOverlay
|
|
28
38
|
from .generation import ImageToVideo as ImageToVideo
|
|
29
39
|
from .generation import TextToImage as TextToImage
|
|
@@ -66,6 +76,17 @@ _exports: dict[str, str] = {
|
|
|
66
76
|
"VideoAnalysis": ".video_analysis",
|
|
67
77
|
"VideoAnalysisConfig": ".video_analysis",
|
|
68
78
|
"VideoAnalyzer": ".video_analysis",
|
|
79
|
+
# Auto-editing (LLM-authored edits)
|
|
80
|
+
"AutoEditor": ".auto_edit",
|
|
81
|
+
"AutoEditError": ".auto_edit",
|
|
82
|
+
"OllamaVisionLLM": ".auto_edit",
|
|
83
|
+
"StructuredVisionLLM": ".auto_edit",
|
|
84
|
+
"PlannerError": ".auto_edit",
|
|
85
|
+
"TextPart": ".auto_edit",
|
|
86
|
+
"ImagePart": ".auto_edit",
|
|
87
|
+
"EditCatalog": ".auto_edit",
|
|
88
|
+
"EditPlan": ".auto_edit",
|
|
89
|
+
"build_catalog": ".auto_edit",
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
__all__ = list(_exports)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Shared structured-generation client over a local Ollama server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
from videopython.ai._optional import require
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OllamaError(RuntimeError):
|
|
17
|
+
"""Ollama returned unusable output (non-JSON or an unexpected shape)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OllamaStructuredClient:
|
|
21
|
+
"""Generate schema-constrained JSON from text + optional images via Ollama.
|
|
22
|
+
|
|
23
|
+
Shared by the auto-edit planner, scene captioner, and translator. The model
|
|
24
|
+
must be served by a local Ollama daemon and support structured-output
|
|
25
|
+
``format`` (and vision, when images are passed); ``options`` are extra Ollama
|
|
26
|
+
generation options merged over ``temperature=0``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, model: str, *, host: str | None = None, options: dict[str, Any] | None = None) -> None:
|
|
30
|
+
self.model = model
|
|
31
|
+
self.host = host
|
|
32
|
+
self.options: dict[str, Any] = {"temperature": 0.0, **(options or {})}
|
|
33
|
+
self._client: Any = None
|
|
34
|
+
|
|
35
|
+
def _get_client(self) -> Any:
|
|
36
|
+
if self._client is None:
|
|
37
|
+
ollama = require("ollama", "ai", feature="Ollama")
|
|
38
|
+
self._client = ollama.Client(host=self.host)
|
|
39
|
+
return self._client
|
|
40
|
+
|
|
41
|
+
def generate_json(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
system: str,
|
|
45
|
+
text: str,
|
|
46
|
+
schema: dict[str, Any],
|
|
47
|
+
images: list[np.ndarray] | None = None,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Return the parsed JSON object Ollama generates under ``schema``."""
|
|
50
|
+
user: dict[str, Any] = {"role": "user", "content": text}
|
|
51
|
+
if images:
|
|
52
|
+
user["images"] = [_encode_png_b64(image) for image in images]
|
|
53
|
+
messages = [{"role": "system", "content": system}, user]
|
|
54
|
+
response = self._get_client().chat(model=self.model, messages=messages, format=schema, options=self.options)
|
|
55
|
+
content = response.message.content
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(content)
|
|
58
|
+
except (ValueError, TypeError) as exc:
|
|
59
|
+
raise OllamaError(f"Ollama returned non-JSON output: {content!r}") from exc
|
|
60
|
+
if not isinstance(data, dict):
|
|
61
|
+
raise OllamaError(f"Ollama returned a non-object JSON value: {type(data).__name__}")
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
def unload(self) -> None:
|
|
65
|
+
self._client = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _encode_png_b64(frame: np.ndarray) -> str:
|
|
69
|
+
buffer = io.BytesIO()
|
|
70
|
+
Image.fromarray(frame).save(buffer, format="PNG")
|
|
71
|
+
return base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
@@ -41,11 +41,6 @@ Usage:
|
|
|
41
41
|
# from the HF repo ``arnabdhar/YOLOv8-Face-Detection`` via
|
|
42
42
|
# hf_hub_download, and that one IS pinned below.)
|
|
43
43
|
#
|
|
44
|
-
# * Dynamic Marian pairs -- ai/generation/translation.py builds the model
|
|
45
|
-
# name per language pair at runtime (e.g. "Helsinki-NLP/opus-mt-en-de").
|
|
46
|
-
# The set of repos is open-ended (one per direction), so pinning each pair
|
|
47
|
-
# to a SHA is impractical and out of scope; left unpinned by design.
|
|
48
|
-
#
|
|
49
44
|
# * Chatterbox internal load -- ai/generation/audio.py calls
|
|
50
45
|
# ``ChatterboxMultilingualTTS.from_pretrained(device=...)`` with no repo
|
|
51
46
|
# argument. The repo id + revision are resolved internally by the
|
|
@@ -67,10 +62,6 @@ from __future__ import annotations
|
|
|
67
62
|
# ``pinned(model_id)`` resolves with a plain dict lookup. SHAs captured from
|
|
68
63
|
# the HuggingFace model API (see module docstring for refresh instructions).
|
|
69
64
|
MODEL_REVISIONS: dict[str, str] = {
|
|
70
|
-
# SceneVLM (ai/understanding/image.py: SCENE_VLM_MODEL_IDS)
|
|
71
|
-
"Qwen/Qwen3.5-4B": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a",
|
|
72
|
-
"Qwen/Qwen3.5-9B": "c202236235762e1c871ad0ccb60c8ee5ba337b9a",
|
|
73
|
-
"Qwen/Qwen3.5-27B": "fc05daec18b0a78c049392ed2e771dde82bdf654",
|
|
74
65
|
# Speaker diarization (ai/understanding/audio.py: PYANNOTE_DIARIZATION_MODEL).
|
|
75
66
|
# Gated repo (auto-approved); from_pretrained needs an accepted HF token.
|
|
76
67
|
"pyannote/speaker-diarization-community-1": "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee",
|
|
@@ -78,8 +69,6 @@ MODEL_REVISIONS: dict[str, str] = {
|
|
|
78
69
|
"MIT/ast-finetuned-audioset-10-10-0.4593": "f826b80d28226b62986cc218e5cec390b1096902",
|
|
79
70
|
# Face detection checkpoint (ai/understanding/faces.py: hf_hub_download)
|
|
80
71
|
"arnabdhar/YOLOv8-Face-Detection": "52fa54977207fa4f021de949b515fb19dcab4488",
|
|
81
|
-
# Qwen3 translation GGUF (ai/generation/qwen3.py: DEFAULT_REPO_ID)
|
|
82
|
-
"unsloth/Qwen3-4B-Instruct-2507-GGUF": "a06e946bb6b655725eafa393f4a9745d460374c9",
|
|
83
72
|
# MusicGen (ai/generation/audio.py: TextToMusic)
|
|
84
73
|
"facebook/musicgen-small": "4c8334b02c6ec4e8664a91979669a501ec497792",
|
|
85
74
|
# SDXL (ai/generation/image.py: TextToImage)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""LLM-authored editing: build a scene catalog and plan a VideoEdit from it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .backend import ImagePart, Part, PlannerError, StructuredVisionLLM, TextPart
|
|
6
|
+
from .catalog import build_catalog
|
|
7
|
+
from .editor import AutoEditError, AutoEditor
|
|
8
|
+
from .local import OllamaVisionLLM
|
|
9
|
+
from .models import CatalogBundle, CatalogScene, EditCatalog, EditPlan, PlanSegment
|
|
10
|
+
from .resolve import UnknownSceneIdsError, resolve_plan
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AutoEditError",
|
|
14
|
+
"AutoEditor",
|
|
15
|
+
"CatalogBundle",
|
|
16
|
+
"CatalogScene",
|
|
17
|
+
"EditCatalog",
|
|
18
|
+
"EditPlan",
|
|
19
|
+
"ImagePart",
|
|
20
|
+
"OllamaVisionLLM",
|
|
21
|
+
"Part",
|
|
22
|
+
"PlanSegment",
|
|
23
|
+
"PlannerError",
|
|
24
|
+
"StructuredVisionLLM",
|
|
25
|
+
"TextPart",
|
|
26
|
+
"UnknownSceneIdsError",
|
|
27
|
+
"build_catalog",
|
|
28
|
+
"resolve_plan",
|
|
29
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The SDK-free seam between the editor and a structured-vision model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TextPart:
|
|
13
|
+
"""A text chunk in a planner prompt."""
|
|
14
|
+
|
|
15
|
+
text: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ImagePart:
|
|
20
|
+
"""A keyframe in a planner prompt: an RGB (H, W, 3) uint8 array."""
|
|
21
|
+
|
|
22
|
+
image: np.ndarray
|
|
23
|
+
label: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Part = TextPart | ImagePart
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PlannerError(RuntimeError):
|
|
30
|
+
"""A backend produced unusable output; the editor retries (infra errors should propagate instead)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class StructuredVisionLLM(Protocol):
|
|
35
|
+
"""Returns schema-shaped JSON from interleaved text + images; raises PlannerError on bad output."""
|
|
36
|
+
|
|
37
|
+
def generate_json(self, *, system: str, parts: list[Part], schema: dict[str, Any]) -> dict[str, Any]: ...
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Build the LLM-facing edit catalog from VideoAnalysis results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from videopython.base.video import extract_frames_at_times
|
|
12
|
+
|
|
13
|
+
from .models import CatalogBundle, CatalogScene, EditCatalog
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from videopython.ai.video_analysis import SceneAnalysisSample, VideoAnalysis
|
|
17
|
+
from videopython.base.transcription import Transcription
|
|
18
|
+
|
|
19
|
+
DEFAULT_TRANSCRIPT_CHARS = 280
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_catalog(
|
|
23
|
+
analyses: Sequence[VideoAnalysis],
|
|
24
|
+
*,
|
|
25
|
+
keyframes: bool = True,
|
|
26
|
+
max_transcript_chars: int = DEFAULT_TRANSCRIPT_CHARS,
|
|
27
|
+
) -> CatalogBundle:
|
|
28
|
+
"""Project VideoAnalysis results into a flat scene catalog with one midpoint keyframe per scene."""
|
|
29
|
+
scenes: list[CatalogScene] = []
|
|
30
|
+
frames: dict[str, np.ndarray] = {}
|
|
31
|
+
used_ids: set[str] = set()
|
|
32
|
+
|
|
33
|
+
for analysis in analyses:
|
|
34
|
+
source_path = analysis.source.path
|
|
35
|
+
samples = analysis.scenes.samples if analysis.scenes else []
|
|
36
|
+
transcription = analysis.audio.transcription if analysis.audio else None
|
|
37
|
+
stem = Path(source_path).stem if source_path else "clip"
|
|
38
|
+
|
|
39
|
+
for sample in samples:
|
|
40
|
+
scene_id = _unique_id(stem, sample.scene_index, used_ids)
|
|
41
|
+
caption, shot_type = _description(sample)
|
|
42
|
+
transcript = _transcript_excerpt(
|
|
43
|
+
transcription, sample.start_second, sample.end_second, max_transcript_chars
|
|
44
|
+
)
|
|
45
|
+
scenes.append(
|
|
46
|
+
CatalogScene(
|
|
47
|
+
id=scene_id,
|
|
48
|
+
source=Path(source_path) if source_path else Path(stem),
|
|
49
|
+
start=sample.start_second,
|
|
50
|
+
end=sample.end_second,
|
|
51
|
+
duration=max(0.0, sample.end_second - sample.start_second),
|
|
52
|
+
shot_type=shot_type,
|
|
53
|
+
caption=caption,
|
|
54
|
+
transcript=transcript,
|
|
55
|
+
has_speech=bool(transcript),
|
|
56
|
+
has_faces=bool(sample.faces),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
if keyframes:
|
|
60
|
+
if source_path is None:
|
|
61
|
+
raise ValueError(f"Scene {scene_id!r} has no source path to extract a keyframe from.")
|
|
62
|
+
midpoint = (sample.start_second + sample.end_second) / 2.0
|
|
63
|
+
frames[scene_id] = extract_frames_at_times(source_path, [midpoint])[0]
|
|
64
|
+
|
|
65
|
+
return CatalogBundle(catalog=EditCatalog(scenes=scenes), keyframes=frames)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _unique_id(stem: str, scene_index: int, used: set[str]) -> str:
|
|
69
|
+
base = f"{stem}#{scene_index}"
|
|
70
|
+
candidate, suffix = base, 2
|
|
71
|
+
while candidate in used:
|
|
72
|
+
candidate = f"{base}-{suffix}"
|
|
73
|
+
suffix += 1
|
|
74
|
+
used.add(candidate)
|
|
75
|
+
return candidate
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _description(sample: SceneAnalysisSample) -> tuple[str, str | None]:
|
|
79
|
+
desc = sample.scene_description
|
|
80
|
+
return (desc.caption or "", desc.shot_type) if desc else ("", None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _transcript_excerpt(transcription: Transcription | None, start: float, end: float, max_chars: int) -> str:
|
|
84
|
+
sliced = transcription.slice(start, end) if transcription else None
|
|
85
|
+
if sliced is None:
|
|
86
|
+
return ""
|
|
87
|
+
text = " ".join(" ".join(segment.text.split()) for segment in sliced.segments).strip()
|
|
88
|
+
if len(text) > max_chars:
|
|
89
|
+
text = text[: max_chars - 3].rstrip() + "..."
|
|
90
|
+
return text
|