videopython 0.52.0__tar.gz → 0.53.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.52.0 → videopython-0.53.0}/PKG-INFO +4 -4
- {videopython-0.52.0 → videopython-0.53.0}/README.md +2 -2
- {videopython-0.52.0 → videopython-0.53.0}/pyproject.toml +10 -7
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/__init__.py +1 -1
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_optional.py +1 -1
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_predictor.py +3 -3
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_revisions.py +10 -10
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/local.py +5 -4
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/pipeline.py +41 -40
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/separation.py +1 -6
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/translation.py +1 -1
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/effects.py +4 -4
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/transforms.py +0 -4
- videopython-0.52.0/src/videopython/ai/understanding/_yolo.py → videopython-0.53.0/src/videopython/ai/understanding/_detector.py +27 -43
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/classification.py +0 -4
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/faces.py +76 -127
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/image.py +1 -1
- videopython-0.53.0/src/videopython/ai/understanding/objects.py +141 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/models.py +1 -1
- videopython-0.52.0/src/videopython/ai/understanding/objects.py +0 -96
- {videopython-0.52.0 → videopython-0.53.0}/.gitignore +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/LICENSE +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_ollama.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/backend.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/editor.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/_tts_backend.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/audio_ops.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/errors.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/keyframe.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/ops.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/detectors.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/source_metadata.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/description.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/video.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/mcp/__init__.py +0 -0
- {videopython-0.52.0 → videopython-0.53.0}/src/videopython/mcp/server.py +0 -0
- {videopython-0.52.0 → videopython-0.53.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.53.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/
|
|
@@ -35,9 +35,9 @@ Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
|
|
|
35
35
|
Requires-Dist: silero-vad>=5.1; extra == 'ai'
|
|
36
36
|
Requires-Dist: torch>=2.8.0; extra == 'ai'
|
|
37
37
|
Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
38
|
+
Requires-Dist: torchvision<0.24.0,>=0.23.0; extra == 'ai'
|
|
38
39
|
Requires-Dist: transformers>=5.2.0; extra == 'ai'
|
|
39
40
|
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
|
|
40
|
-
Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
|
|
41
41
|
Provides-Extra: mcp
|
|
42
42
|
Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
@@ -63,7 +63,7 @@ pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
|
63
63
|
pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull
|
|
66
|
+
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull qwen3.6:27b`).
|
|
67
67
|
|
|
68
68
|
## Quick Start
|
|
69
69
|
|
|
@@ -99,7 +99,7 @@ Give `AutoEditor` your clips and a brief; a local Ollama vision model selects an
|
|
|
99
99
|
```python
|
|
100
100
|
from videopython.ai import AutoEditor, OllamaVisionLLM
|
|
101
101
|
|
|
102
|
-
editor = AutoEditor(planner=OllamaVisionLLM(model="
|
|
102
|
+
editor = AutoEditor(planner=OllamaVisionLLM(model="qwen3.6:27b")) # ollama pull qwen3.6:27b
|
|
103
103
|
edit = editor.edit(
|
|
104
104
|
["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
|
|
105
105
|
brief="A punchy 15-second teaser; lead with the most dynamic shot.",
|
|
@@ -19,7 +19,7 @@ pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
|
19
19
|
pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull
|
|
22
|
+
Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull qwen3.6:27b`).
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
@@ -55,7 +55,7 @@ Give `AutoEditor` your clips and a brief; a local Ollama vision model selects an
|
|
|
55
55
|
```python
|
|
56
56
|
from videopython.ai import AutoEditor, OllamaVisionLLM
|
|
57
57
|
|
|
58
|
-
editor = AutoEditor(planner=OllamaVisionLLM(model="
|
|
58
|
+
editor = AutoEditor(planner=OllamaVisionLLM(model="qwen3.6:27b")) # ollama pull qwen3.6:27b
|
|
59
59
|
edit = editor.edit(
|
|
60
60
|
["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
|
|
61
61
|
brief="A punchy 15-second teaser; lead with the most dynamic shot.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "videopython"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.53.0"
|
|
4
4
|
description = "Minimal video generation and processing library."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
|
|
@@ -68,8 +68,8 @@ ai = [
|
|
|
68
68
|
"openai-whisper>=20240930",
|
|
69
69
|
"pyannote-audio>=4.0.0",
|
|
70
70
|
"silero-vad>=5.1",
|
|
71
|
-
# Visual understanding: detection
|
|
72
|
-
|
|
71
|
+
# Visual understanding: detection (D-FINE/YuNet via transformers + opencv core
|
|
72
|
+
# dep), scene/temporal, VLM (understanding/*, video_analysis)
|
|
73
73
|
"transnetv2-pytorch>=1.0.5",
|
|
74
74
|
"transformers>=5.2.0",
|
|
75
75
|
"imagehash>=4.3",
|
|
@@ -85,9 +85,13 @@ ai = [
|
|
|
85
85
|
"pyloudnorm>=0.1.1",
|
|
86
86
|
# LLM-authored editing planner (auto_edit/local.py OllamaVisionLLM)
|
|
87
87
|
"ollama>=0.5.0",
|
|
88
|
-
# Shared torch stack
|
|
88
|
+
# Shared torch stack. torchvision backs the transformers "fast" image
|
|
89
|
+
# processor used by the D-FINE object detector (understanding/objects.py).
|
|
89
90
|
"torch>=2.8.0",
|
|
90
91
|
"torchaudio>=2.8.0",
|
|
92
|
+
# Capped to the torch-2.8-compatible line; torchvision<->torch are ABI-coupled
|
|
93
|
+
# and uv does not know it (0.23.x pairs with torch 2.8.x).
|
|
94
|
+
"torchvision>=0.23.0,<0.24.0",
|
|
91
95
|
]
|
|
92
96
|
# MCP server (videopython/mcp/). Pin <2 — v2 is pre-release/breaking.
|
|
93
97
|
mcp = ["mcp>=1.27,<2"]
|
|
@@ -112,7 +116,6 @@ module = [
|
|
|
112
116
|
"torch", "torch.*",
|
|
113
117
|
"diffusers", "diffusers.*",
|
|
114
118
|
"ollama", "ollama.*",
|
|
115
|
-
"ultralytics", "ultralytics.*",
|
|
116
119
|
"transformers", "transformers.*",
|
|
117
120
|
"transnetv2_pytorch", "transnetv2_pytorch.*",
|
|
118
121
|
"chatterbox", "chatterbox.*",
|
|
@@ -133,8 +136,8 @@ ignore_missing_imports = true
|
|
|
133
136
|
# These overrides let the combined [ai] resolve pick compatible versions; the
|
|
134
137
|
# floors in [ai] are aligned with them so pip and uv resolve similar versions.
|
|
135
138
|
override-dependencies = [
|
|
136
|
-
"torch>=2.8.0", "torchaudio>=2.8.0", "numpy>=2.0.0", "diffusers>=0.30.0",
|
|
137
|
-
#
|
|
139
|
+
"torch>=2.8.0", "torchaudio>=2.8.0", "torchvision>=0.23.0,<0.24.0", "numpy>=2.0.0", "diffusers>=0.30.0",
|
|
140
|
+
# Some transitive deps pull opencv-python, which conflicts with our
|
|
138
141
|
# opencv-python-headless (both provide cv2). Exclude opencv-python so
|
|
139
142
|
# only the headless variant is installed.
|
|
140
143
|
"opencv-python ; sys_platform == '_'",
|
|
@@ -5,7 +5,7 @@ Every public symbol is re-exported lazily via PEP 562 ``__getattr__`` so that
|
|
|
5
5
|
leaf module backing the requested symbol — not every sibling. All AI capabilities
|
|
6
6
|
ship in the single ``[ai]`` extra; the laziness keeps ``import videopython`` (and
|
|
7
7
|
importing one leaf class) light by deferring the heavy ML imports (torch /
|
|
8
|
-
transformers / diffusers /
|
|
8
|
+
transformers / diffusers / chatterbox) until a symbol is actually used. When
|
|
9
9
|
``[ai]`` is not installed, touching a symbol raises a clear
|
|
10
10
|
``pip install 'videopython[ai]'``-pointing ``ImportError`` at attribute access
|
|
11
11
|
instead of failing the whole package import.
|
|
@@ -52,7 +52,7 @@ def lazy_exports(package: str, exports: dict[str, str]) -> tuple[Callable[[str],
|
|
|
52
52
|
imported only on first attribute access, so importing the package does not
|
|
53
53
|
pull in any sibling leaf module. This keeps ``import videopython`` (and
|
|
54
54
|
importing a single leaf class) light by deferring the heavy ML imports
|
|
55
|
-
(torch / transformers / diffusers /
|
|
55
|
+
(torch / transformers / diffusers / chatterbox) until a symbol is used.
|
|
56
56
|
|
|
57
57
|
Args:
|
|
58
58
|
package: The ``__name__`` of the package defining the re-exports. Used
|
|
@@ -19,9 +19,9 @@ Subclasses with non-default model fields just declare ``_model_attrs``; those
|
|
|
19
19
|
whose teardown isn't "null the fields + release" (e.g. delegating to a client's
|
|
20
20
|
own ``unload()``) override ``unload()`` instead.
|
|
21
21
|
|
|
22
|
-
The base imports no torch / transformers
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
The base imports no torch / transformers (``release_device_memory`` defers its
|
|
23
|
+
torch import), so it stays safe to mix into any predictor regardless of how it is
|
|
24
|
+
constructed.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
@@ -33,14 +33,6 @@ Usage:
|
|
|
33
33
|
# documented here rather than registered above. ``pinned()`` returns None for
|
|
34
34
|
# them, which leaves the caller on its current (unpinned) behavior.
|
|
35
35
|
#
|
|
36
|
-
# * ultralytics YOLO asset -- ai/understanding/objects.py loads
|
|
37
|
-
# ``YOLO(self.model_name)`` (e.g. "yolov8n.pt"). Ultralytics resolves and
|
|
38
|
-
# downloads this from its own GitHub release assets, not from a HF repo,
|
|
39
|
-
# so there is no HF revision to pin. (The face detector in
|
|
40
|
-
# ai/understanding/faces.py is different: it pulls a YOLOv8 *checkpoint*
|
|
41
|
-
# from the HF repo ``arnabdhar/YOLOv8-Face-Detection`` via
|
|
42
|
-
# hf_hub_download, and that one IS pinned below.)
|
|
43
|
-
#
|
|
44
36
|
# * Chatterbox internal load -- ai/generation/audio.py calls
|
|
45
37
|
# ``ChatterboxMultilingualTTS.from_pretrained(device=...)`` with no repo
|
|
46
38
|
# argument. The repo id + revision are resolved internally by the
|
|
@@ -67,8 +59,16 @@ MODEL_REVISIONS: dict[str, str] = {
|
|
|
67
59
|
"pyannote/speaker-diarization-community-1": "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee",
|
|
68
60
|
# Audio event classifier (ai/understanding/classification.py: AudioClassifier)
|
|
69
61
|
"MIT/ast-finetuned-audioset-10-10-0.4593": "f826b80d28226b62986cc218e5cec390b1096902",
|
|
70
|
-
#
|
|
71
|
-
|
|
62
|
+
# Object detection — D-FINE COCO (ai/understanding/objects.py: ObjectDetector).
|
|
63
|
+
# Apache-2.0; replaced the AGPL Ultralytics YOLO weights. Loaded by AutoImageProcessor
|
|
64
|
+
# AND DFineForObjectDetection.from_pretrained, both keyed on the full repo id.
|
|
65
|
+
"ustc-community/dfine-nano-coco": "066438d3d8f0da137a37b38fdf3368fd4afceced",
|
|
66
|
+
"ustc-community/dfine-small-coco": "f79e65b5fbb33ceb9d3ebba042955d7410c608f8",
|
|
67
|
+
"ustc-community/dfine-medium-coco": "4ab9f5e466432f4fcf9a9a023da6aa5ecc9c9829",
|
|
68
|
+
"ustc-community/dfine-large-coco": "aa8f8211545e4cff5adf52ea512f9ac74a9824ec",
|
|
69
|
+
# Face detection checkpoint — OpenCV YuNet ONNX, MIT (ai/understanding/faces.py:
|
|
70
|
+
# hf_hub_download). Replaced the AGPL arnabdhar/YOLOv8-Face-Detection weights.
|
|
71
|
+
"opencv/face_detection_yunet": "3cc26e7f1014a5ee5d74a42acee58bafc9d0a310",
|
|
72
72
|
# MusicGen (ai/generation/audio.py: TextToMusic)
|
|
73
73
|
"facebook/musicgen-small": "4c8334b02c6ec4e8664a91979669a501ec497792",
|
|
74
74
|
# SDXL (ai/generation/image.py: TextToImage)
|
|
@@ -11,16 +11,17 @@ from .backend import PlannerError
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
import numpy as np
|
|
13
13
|
|
|
14
|
-
DEFAULT_OLLAMA_MODEL = "
|
|
14
|
+
DEFAULT_OLLAMA_MODEL = "qwen3.6:27b"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class OllamaVisionLLM:
|
|
18
18
|
"""A StructuredVisionLLM backed by a local Ollama server.
|
|
19
19
|
|
|
20
20
|
The model must be vision-capable (it is sent keyframes) AND support Ollama's
|
|
21
|
-
structured-output ``format`` (the EditPlan schema constrains the decode).
|
|
22
|
-
|
|
23
|
-
some builds
|
|
21
|
+
structured-output ``format`` (the EditPlan schema constrains the decode). The
|
|
22
|
+
default ``qwen3.6:27b`` is an Apache-2.0 vision model; not every model supports
|
|
23
|
+
schema conditioning (some builds, e.g. certain MLX ones, fail it), so confirm
|
|
24
|
+
``format`` works for a custom model locally. ``ollama pull <model>`` first;
|
|
24
25
|
``options`` are extra generation options merged over ``temperature=0``.
|
|
25
26
|
|
|
26
27
|
Thin wrapper over the shared :class:`OllamaStructuredClient`: its only job is
|
|
@@ -229,6 +229,43 @@ class LocalDubbingPipeline:
|
|
|
229
229
|
"""Initialize the timing synchronizer."""
|
|
230
230
|
self._synchronizer = TimingSynchronizer()
|
|
231
231
|
|
|
232
|
+
def _separate(
|
|
233
|
+
self,
|
|
234
|
+
source_audio: Audio,
|
|
235
|
+
transcription: Transcription,
|
|
236
|
+
progress_fraction: float,
|
|
237
|
+
report_progress: Callable[[str, float], None],
|
|
238
|
+
) -> tuple[SeparatedAudio | None, Audio, Audio | None]:
|
|
239
|
+
"""Demucs source separation over the speech regions; shared by process()/revoice().
|
|
240
|
+
|
|
241
|
+
Limits Demucs to the speech-bearing portion of the audio: the transcription
|
|
242
|
+
has already located every speech region, and separating outside those is pure
|
|
243
|
+
overhead (no vocals to isolate). On talk-heavy sources with silence/music gaps
|
|
244
|
+
this roughly halves separation time; when speech covers most of the track,
|
|
245
|
+
separate_regions falls back to a full-track separate().
|
|
246
|
+
|
|
247
|
+
Returns ``(separated_audio, vocal_audio, background_audio)``. In low_memory
|
|
248
|
+
mode the ``SeparatedAudio`` container is dropped (returned as ``None``) so
|
|
249
|
+
vocals and background release as soon as their last local reference goes.
|
|
250
|
+
"""
|
|
251
|
+
report_progress("Separating audio", progress_fraction)
|
|
252
|
+
if self._separator is None:
|
|
253
|
+
self._init_separator()
|
|
254
|
+
|
|
255
|
+
from videopython.ai.dubbing.separation import merge_regions
|
|
256
|
+
|
|
257
|
+
speech_regions = merge_regions(
|
|
258
|
+
[(s.start, s.end) for s in transcription.segments],
|
|
259
|
+
audio_duration=source_audio.metadata.duration_seconds,
|
|
260
|
+
)
|
|
261
|
+
separated_audio = self._separator.separate_regions(source_audio, speech_regions)
|
|
262
|
+
self._maybe_unload("_separator")
|
|
263
|
+
vocal_audio = separated_audio.vocals
|
|
264
|
+
background_audio = separated_audio.background
|
|
265
|
+
if self.config.low_memory:
|
|
266
|
+
separated_audio = None
|
|
267
|
+
return separated_audio, vocal_audio, background_audio
|
|
268
|
+
|
|
232
269
|
def _finalise_audio(
|
|
233
270
|
self,
|
|
234
271
|
dubbed_speech: Audio,
|
|
@@ -355,32 +392,9 @@ class LocalDubbingPipeline:
|
|
|
355
392
|
background_audio: Audio | None = None
|
|
356
393
|
|
|
357
394
|
if preserve_background:
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
self._init_separator()
|
|
361
|
-
|
|
362
|
-
# Limit Demucs to the speech-bearing portion of the audio. The
|
|
363
|
-
# transcription has already located every speech region; running
|
|
364
|
-
# source separation outside those is pure overhead (no vocals to
|
|
365
|
-
# isolate). On talk-heavy sources with silence/music gaps this
|
|
366
|
-
# roughly halves separation time. When speech covers most of the
|
|
367
|
-
# track separate_regions falls back to a full-track separate().
|
|
368
|
-
from videopython.ai.dubbing.separation import merge_regions
|
|
369
|
-
|
|
370
|
-
speech_regions = merge_regions(
|
|
371
|
-
[(s.start, s.end) for s in transcription.segments],
|
|
372
|
-
audio_duration=source_audio.metadata.duration_seconds,
|
|
395
|
+
separated_audio, vocal_audio, background_audio = self._separate(
|
|
396
|
+
source_audio, transcription, 0.15, report_progress
|
|
373
397
|
)
|
|
374
|
-
separated_audio = self._separator.separate_regions(source_audio, speech_regions)
|
|
375
|
-
self._maybe_unload("_separator")
|
|
376
|
-
vocal_audio = separated_audio.vocals
|
|
377
|
-
background_audio = separated_audio.background
|
|
378
|
-
# In low_memory mode, drop the SeparatedAudio container so vocals
|
|
379
|
-
# and background can be released as soon as their last local
|
|
380
|
-
# reference goes (after voice-sample extraction and final overlay
|
|
381
|
-
# respectively). The result will report separated_audio=None.
|
|
382
|
-
if self.config.low_memory:
|
|
383
|
-
separated_audio = None
|
|
384
398
|
|
|
385
399
|
voice_samples: dict[str, Audio] = {}
|
|
386
400
|
if voice_clone:
|
|
@@ -527,22 +541,9 @@ class LocalDubbingPipeline:
|
|
|
527
541
|
background_audio: Audio | None = None
|
|
528
542
|
|
|
529
543
|
if preserve_background:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
self._init_separator()
|
|
533
|
-
|
|
534
|
-
from videopython.ai.dubbing.separation import merge_regions
|
|
535
|
-
|
|
536
|
-
speech_regions = merge_regions(
|
|
537
|
-
[(s.start, s.end) for s in transcription.segments],
|
|
538
|
-
audio_duration=source_audio.metadata.duration_seconds,
|
|
544
|
+
separated_audio, vocal_audio, background_audio = self._separate(
|
|
545
|
+
source_audio, transcription, 0.20, report_progress
|
|
539
546
|
)
|
|
540
|
-
separated_audio = self._separator.separate_regions(source_audio, speech_regions)
|
|
541
|
-
self._maybe_unload("_separator")
|
|
542
|
-
vocal_audio = separated_audio.vocals
|
|
543
|
-
background_audio = separated_audio.background
|
|
544
|
-
if self.config.low_memory:
|
|
545
|
-
separated_audio = None
|
|
546
547
|
|
|
547
548
|
report_progress("Extracting voice sample", 0.40)
|
|
548
549
|
chosen_sample: Audio | None = None
|
|
@@ -59,14 +59,9 @@ def merge_regions(
|
|
|
59
59
|
class AudioSeparator(ManagedPredictor):
|
|
60
60
|
"""Separates audio into vocals and background components using Demucs."""
|
|
61
61
|
|
|
62
|
-
SUPPORTED_MODELS: list[str] = ["htdemucs", "htdemucs_ft", "htdemucs_6s", "mdx_extra"]
|
|
63
62
|
STEM_NAMES = ["drums", "bass", "other", "vocals"]
|
|
64
|
-
STEM_NAMES_6S = ["drums", "bass", "other", "vocals", "guitar", "piano"]
|
|
65
63
|
|
|
66
64
|
def __init__(self, model_name: str = "htdemucs", device: str | None = None):
|
|
67
|
-
if model_name not in self.SUPPORTED_MODELS:
|
|
68
|
-
raise ValueError(f"Model '{model_name}' not supported. Supported: {self.SUPPORTED_MODELS}")
|
|
69
|
-
|
|
70
65
|
self.model_name = model_name
|
|
71
66
|
self.device = device
|
|
72
67
|
self._model: Any = None
|
|
@@ -129,7 +124,7 @@ class AudioSeparator(ManagedPredictor):
|
|
|
129
124
|
sources_np = sources[0].cpu().numpy()
|
|
130
125
|
del sources
|
|
131
126
|
|
|
132
|
-
stem_names = self.
|
|
127
|
+
stem_names = self.STEM_NAMES
|
|
133
128
|
vocals_idx = stem_names.index("vocals")
|
|
134
129
|
non_vocal_indices = [i for i in range(len(stem_names)) if i != vocals_idx]
|
|
135
130
|
|
|
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
|
|
25
25
|
# Default Ollama text model for translation; override via the `model` arg (and
|
|
26
26
|
# `ollama pull` it first). Any instruct model that supports structured output works.
|
|
27
|
-
DEFAULT_TRANSLATION_MODEL = "
|
|
27
|
+
DEFAULT_TRANSLATION_MODEL = "qwen3.6:27b"
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _is_translatable_text(text: str) -> bool:
|
|
@@ -14,7 +14,7 @@ from typing import Any, Literal
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from pydantic import Field, PrivateAttr
|
|
16
16
|
|
|
17
|
-
from videopython.ai.understanding.objects import ObjectDetector
|
|
17
|
+
from videopython.ai.understanding.objects import MODEL_SIZES, ObjectDetector
|
|
18
18
|
from videopython.base.description import DetectedObject
|
|
19
19
|
from videopython.base.draw_detections import DetectionStyle, draw_detections
|
|
20
20
|
from videopython.editing.operation import Effect
|
|
@@ -25,7 +25,7 @@ __all__ = ["ObjectDetectionOverlay"]
|
|
|
25
25
|
class ObjectDetectionOverlay(Effect):
|
|
26
26
|
"""Detect objects per frame and overlay labelled bounding boxes.
|
|
27
27
|
|
|
28
|
-
Runs a
|
|
28
|
+
Runs a D-FINE COCO detector and composites tidy, colour-coded boxes with
|
|
29
29
|
class labels (and optional confidence) onto every frame in the window.
|
|
30
30
|
|
|
31
31
|
Detection runs on a ``detection_interval`` cadence in the streaming path and
|
|
@@ -70,7 +70,7 @@ class ObjectDetectionOverlay(Effect):
|
|
|
70
70
|
model_size: Literal["n", "s", "m"] = Field(
|
|
71
71
|
"n",
|
|
72
72
|
description=(
|
|
73
|
-
"
|
|
73
|
+
"D-FINE detector size: 'n' (nano, fastest), 's' (small), 'm' (medium, most accurate). "
|
|
74
74
|
"Larger detects better but is slower."
|
|
75
75
|
),
|
|
76
76
|
)
|
|
@@ -96,7 +96,7 @@ class ObjectDetectionOverlay(Effect):
|
|
|
96
96
|
"""Build the detector lazily. Single patch point for tests."""
|
|
97
97
|
if self._detector is None:
|
|
98
98
|
self._detector = ObjectDetector(
|
|
99
|
-
model_name=
|
|
99
|
+
model_name=MODEL_SIZES[self.model_size],
|
|
100
100
|
confidence_threshold=self.confidence_threshold,
|
|
101
101
|
class_filter=tuple(self.class_filter or ()),
|
|
102
102
|
backend=self.backend,
|
|
@@ -74,8 +74,6 @@ class FaceTrackingCrop(Operation):
|
|
|
74
74
|
),
|
|
75
75
|
)
|
|
76
76
|
detection_interval: int = Field(3, ge=1, description="Frames between face detections.")
|
|
77
|
-
backend: Literal["cpu", "gpu", "auto"] = Field("auto", description='Detection backend - "cpu", "gpu", or "auto".')
|
|
78
|
-
sample_rate: int = Field(1, ge=1, description="For GPU backend, detect every Nth frame and interpolate.")
|
|
79
77
|
|
|
80
78
|
def _apply_framing_offset(self, face_cx: float, face_cy: float, face_h: float) -> tuple[float, float]:
|
|
81
79
|
if self.framing_rule == "offset":
|
|
@@ -139,8 +137,6 @@ class FaceTrackingCrop(Operation):
|
|
|
139
137
|
face_index=self.face_index,
|
|
140
138
|
smoothing=self.smoothing,
|
|
141
139
|
detection_interval=self.detection_interval,
|
|
142
|
-
backend=self.backend,
|
|
143
|
-
sample_rate=self.sample_rate,
|
|
144
140
|
)
|
|
145
141
|
default = ((frame_w - out_w) // 2, (frame_h - out_h) // 2)
|
|
146
142
|
last = default
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
"""Shared lazy
|
|
2
|
-
|
|
3
|
-
``ObjectDetector`` (COCO objects) and ``_FaceDetector``
|
|
4
|
-
|
|
5
|
-
lazy load
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
``
|
|
1
|
+
"""Shared lazy detector base for the understanding layer.
|
|
2
|
+
|
|
3
|
+
``ObjectDetector`` (COCO objects, D-FINE via transformers) and ``_FaceDetector``
|
|
4
|
+
(faces, OpenCV YuNet) share the same machinery: ``cpu``/``gpu``/``auto`` device
|
|
5
|
+
resolution, lazy load, the ``detect`` / ``detect_batch`` shape, and ``unload``.
|
|
6
|
+
:class:`DetectorBase` holds that machinery; subclasses supply just ``_load_model``
|
|
7
|
+
(populate the ``_model_attrs`` fields) and ``_infer`` (map a batch of frames to a
|
|
8
|
+
list of per-frame detections).
|
|
9
|
+
|
|
10
|
+
The two backends are completely different (transformers vs OpenCV DNN), so the
|
|
11
|
+
base imports no model library itself; each subclass defers its heavy import into
|
|
12
|
+
``_load_model`` (guarded by ``require`` where the dep lives in the ``[ai]`` extra).
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import Generic, Literal, TypeVar
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
@@ -28,26 +28,25 @@ T = TypeVar("T")
|
|
|
28
28
|
Backend = Literal["cpu", "gpu", "auto"]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class
|
|
32
|
-
"""Lazy
|
|
31
|
+
class DetectorBase(ManagedPredictor, Generic[T]):
|
|
32
|
+
"""Lazy detector base: device resolution, lazy load, detect/detect_batch, unload.
|
|
33
33
|
|
|
34
34
|
Context-managed via :class:`ManagedPredictor`; ``unload()`` drops the model
|
|
35
|
-
and frees the resolved device. Subclass
|
|
35
|
+
reference(s) named in ``_model_attrs`` and frees the resolved device. Subclass
|
|
36
|
+
contract:
|
|
36
37
|
* ``_FEATURE`` -- name surfaced in the ``[ai]``-extra ``ImportError``.
|
|
37
|
-
* ``
|
|
38
|
-
|
|
39
|
-
* ``
|
|
40
|
-
* ``
|
|
38
|
+
* ``_model_attrs`` -- attribute(s) holding loaded model state; the first one
|
|
39
|
+
doubles as the "is loaded" sentinel.
|
|
40
|
+
* ``_load_model()`` -- populate the ``_model_attrs`` fields plus any label state.
|
|
41
|
+
* ``_infer(images)`` -- map a list of frames to a list of per-frame detections.
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
|
-
_FEATURE = "
|
|
44
|
-
_model_attrs = ("_yolo_model",)
|
|
44
|
+
_FEATURE = "detector"
|
|
45
45
|
_device_attr = "_resolved_device"
|
|
46
46
|
|
|
47
47
|
def __init__(self, *, backend: Backend = "auto") -> None:
|
|
48
48
|
self.backend: Backend = backend
|
|
49
49
|
self._resolved_device: Literal["cpu", "cuda"] | None = None
|
|
50
|
-
self._yolo_model: Any = None
|
|
51
50
|
|
|
52
51
|
def _resolve_device(self) -> Literal["cpu", "cuda"]:
|
|
53
52
|
if self._resolved_device is not None:
|
|
@@ -67,37 +66,23 @@ class YoloDetector(ManagedPredictor, Generic[T]):
|
|
|
67
66
|
"""Resolved execution device for this detector."""
|
|
68
67
|
return self._resolve_device()
|
|
69
68
|
|
|
70
|
-
def _build_yolo(self, model_path: str) -> None:
|
|
71
|
-
"""Construct ``self._yolo_model`` from a model id/path and move it to the device."""
|
|
72
|
-
from videopython.ai._optional import require
|
|
73
|
-
|
|
74
|
-
YOLO = require("ultralytics", feature=self._FEATURE).YOLO
|
|
75
|
-
self._yolo_model = YOLO(model_path)
|
|
76
|
-
if self._resolve_device() == "cuda":
|
|
77
|
-
self._yolo_model.to("cuda")
|
|
78
|
-
|
|
79
69
|
# --- subclass hooks -----------------------------------------------------
|
|
80
70
|
def _load_model(self) -> None:
|
|
81
71
|
raise NotImplementedError
|
|
82
72
|
|
|
83
|
-
def
|
|
84
|
-
raise NotImplementedError
|
|
85
|
-
|
|
86
|
-
def _parse(self, result: Any) -> list[T]:
|
|
73
|
+
def _infer(self, images: list[np.ndarray]) -> list[list[T]]:
|
|
87
74
|
raise NotImplementedError
|
|
88
75
|
|
|
89
76
|
# --- public API ---------------------------------------------------------
|
|
90
77
|
def _ensure_loaded(self) -> None:
|
|
91
|
-
if self.
|
|
78
|
+
if getattr(self, self._model_attrs[0], None) is None:
|
|
92
79
|
self._load_model()
|
|
93
80
|
|
|
94
81
|
def detect(self, image: np.ndarray) -> list[T]:
|
|
95
82
|
"""Detect in a single ``(H, W, 3)`` frame."""
|
|
96
83
|
self._ensure_loaded()
|
|
97
|
-
results = self.
|
|
98
|
-
if
|
|
99
|
-
return []
|
|
100
|
-
return self._parse(results[0])
|
|
84
|
+
results = self._infer([image])
|
|
85
|
+
return results[0] if results else []
|
|
101
86
|
|
|
102
87
|
def detect_batch(self, images: list[np.ndarray] | np.ndarray) -> list[list[T]]:
|
|
103
88
|
"""Detect in a batch of frames (list or stacked ``(N, H, W, 3)``)."""
|
|
@@ -107,5 +92,4 @@ class YoloDetector(ManagedPredictor, Generic[T]):
|
|
|
107
92
|
return []
|
|
108
93
|
|
|
109
94
|
self._ensure_loaded()
|
|
110
|
-
|
|
111
|
-
return [self._parse(result) for result in results]
|
|
95
|
+
return self._infer(images)
|
{videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/classification.py
RENAMED
|
@@ -23,7 +23,6 @@ class AudioClassifier(ManagedPredictor):
|
|
|
23
23
|
"""Audio event and sound classification using AST."""
|
|
24
24
|
|
|
25
25
|
_model_attrs = ("_model", "_processor")
|
|
26
|
-
SUPPORTED_MODELS: list[str] = ["MIT/ast-finetuned-audioset-10-10-0.4593"]
|
|
27
26
|
AST_SAMPLE_RATE: int = 16000
|
|
28
27
|
AST_CHUNK_SECONDS: float = 10.0
|
|
29
28
|
AST_HOP_SECONDS: float = 5.0
|
|
@@ -35,9 +34,6 @@ class AudioClassifier(ManagedPredictor):
|
|
|
35
34
|
top_k: int = 10,
|
|
36
35
|
device: str | None = None,
|
|
37
36
|
):
|
|
38
|
-
if model_name not in self.SUPPORTED_MODELS:
|
|
39
|
-
raise ValueError(f"Model '{model_name}' not supported. Supported: {self.SUPPORTED_MODELS}")
|
|
40
|
-
|
|
41
37
|
self.model_name = model_name
|
|
42
38
|
self.confidence_threshold = confidence_threshold
|
|
43
39
|
self.top_k = top_k
|