videopython 0.52.1__tar.gz → 0.54.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.1 → videopython-0.54.0}/PKG-INFO +6 -5
- {videopython-0.52.1 → videopython-0.54.0}/README.md +2 -2
- {videopython-0.52.1 → videopython-0.54.0}/pyproject.toml +17 -9
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/__init__.py +1 -1
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_optional.py +1 -1
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_predictor.py +3 -3
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_revisions.py +18 -15
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/local.py +5 -4
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/translation.py +1 -1
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/effects.py +4 -4
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/generation/__init__.py +1 -1
- videopython-0.54.0/src/videopython/ai/generation/image.py +93 -0
- videopython-0.54.0/src/videopython/ai/generation/video.py +207 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/transforms.py +0 -4
- videopython-0.52.1/src/videopython/ai/understanding/_yolo.py → videopython-0.54.0/src/videopython/ai/understanding/_detector.py +27 -43
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/faces.py +76 -127
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/image.py +1 -1
- videopython-0.54.0/src/videopython/ai/understanding/objects.py +141 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/models.py +1 -1
- videopython-0.52.1/src/videopython/ai/generation/image.py +0 -59
- videopython-0.52.1/src/videopython/ai/generation/video.py +0 -131
- videopython-0.52.1/src/videopython/ai/understanding/objects.py +0 -96
- {videopython-0.52.1 → videopython-0.54.0}/.gitignore +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/LICENSE +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_ollama.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/backend.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/editor.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/_tts_backend.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/audio_ops.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/separation.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/errors.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/keyframe.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/ops.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/classification.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/detectors.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/source_metadata.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/description.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/video.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/mcp/__init__.py +0 -0
- {videopython-0.52.1 → videopython-0.54.0}/src/videopython/mcp/server.py +0 -0
- {videopython-0.52.1 → videopython-0.54.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.54.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/
|
|
@@ -26,7 +26,8 @@ Provides-Extra: ai
|
|
|
26
26
|
Requires-Dist: accelerate>=0.29.2; extra == 'ai'
|
|
27
27
|
Requires-Dist: chatterbox-tts>=0.1.7; extra == 'ai'
|
|
28
28
|
Requires-Dist: demucs>=4.0.0; extra == 'ai'
|
|
29
|
-
Requires-Dist: diffusers>=0.
|
|
29
|
+
Requires-Dist: diffusers>=0.35.0; extra == 'ai'
|
|
30
|
+
Requires-Dist: ftfy>=6.1; extra == 'ai'
|
|
30
31
|
Requires-Dist: imagehash>=4.3; extra == 'ai'
|
|
31
32
|
Requires-Dist: ollama>=0.5.0; extra == 'ai'
|
|
32
33
|
Requires-Dist: openai-whisper>=20240930; extra == 'ai'
|
|
@@ -35,9 +36,9 @@ Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
|
|
|
35
36
|
Requires-Dist: silero-vad>=5.1; extra == 'ai'
|
|
36
37
|
Requires-Dist: torch>=2.8.0; extra == 'ai'
|
|
37
38
|
Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
39
|
+
Requires-Dist: torchvision<0.24.0,>=0.23.0; extra == 'ai'
|
|
38
40
|
Requires-Dist: transformers>=5.2.0; extra == 'ai'
|
|
39
41
|
Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
|
|
40
|
-
Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
|
|
41
42
|
Provides-Extra: mcp
|
|
42
43
|
Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
|
|
43
44
|
Description-Content-Type: text/markdown
|
|
@@ -63,7 +64,7 @@ pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
|
|
|
63
64
|
pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
|
|
64
65
|
```
|
|
65
66
|
|
|
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
|
|
67
|
+
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
68
|
|
|
68
69
|
## Quick Start
|
|
69
70
|
|
|
@@ -99,7 +100,7 @@ Give `AutoEditor` your clips and a brief; a local Ollama vision model selects an
|
|
|
99
100
|
```python
|
|
100
101
|
from videopython.ai import AutoEditor, OllamaVisionLLM
|
|
101
102
|
|
|
102
|
-
editor = AutoEditor(planner=OllamaVisionLLM(model="
|
|
103
|
+
editor = AutoEditor(planner=OllamaVisionLLM(model="qwen3.6:27b")) # ollama pull qwen3.6:27b
|
|
103
104
|
edit = editor.edit(
|
|
104
105
|
["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
|
|
105
106
|
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.54.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",
|
|
@@ -78,16 +78,25 @@ ai = [
|
|
|
78
78
|
# Voice cloning TTS (generation/audio.py — Chatterbox); its strict pins drive
|
|
79
79
|
# the [tool.uv].override-dependencies block below.
|
|
80
80
|
"chatterbox-tts>=0.1.7",
|
|
81
|
-
# Local media generation:
|
|
82
|
-
|
|
81
|
+
# Local media generation: Qwen-Image/Wan2.2 + MusicGen (generation/*).
|
|
82
|
+
# >=0.35 is the floor that ships QwenImagePipeline + Wan2.2 support (tested on 0.37.1).
|
|
83
|
+
"diffusers>=0.35.0",
|
|
84
|
+
# Wan2.2 i2v pipeline cleans prompts with ftfy via a hard import (diffusers'
|
|
85
|
+
# pipeline_wan_i2v); the t2v pipeline guards it with is_ftfy_available(), i2v
|
|
86
|
+
# does not, so it is a required dep for ImageToVideo.
|
|
87
|
+
"ftfy>=6.1",
|
|
83
88
|
"accelerate>=0.29.2",
|
|
84
89
|
# Dubbing loudness matching (dubbing/loudness.py)
|
|
85
90
|
"pyloudnorm>=0.1.1",
|
|
86
91
|
# LLM-authored editing planner (auto_edit/local.py OllamaVisionLLM)
|
|
87
92
|
"ollama>=0.5.0",
|
|
88
|
-
# Shared torch stack
|
|
93
|
+
# Shared torch stack. torchvision backs the transformers "fast" image
|
|
94
|
+
# processor used by the D-FINE object detector (understanding/objects.py).
|
|
89
95
|
"torch>=2.8.0",
|
|
90
96
|
"torchaudio>=2.8.0",
|
|
97
|
+
# Capped to the torch-2.8-compatible line; torchvision<->torch are ABI-coupled
|
|
98
|
+
# and uv does not know it (0.23.x pairs with torch 2.8.x).
|
|
99
|
+
"torchvision>=0.23.0,<0.24.0",
|
|
91
100
|
]
|
|
92
101
|
# MCP server (videopython/mcp/). Pin <2 — v2 is pre-release/breaking.
|
|
93
102
|
mcp = ["mcp>=1.27,<2"]
|
|
@@ -112,7 +121,6 @@ module = [
|
|
|
112
121
|
"torch", "torch.*",
|
|
113
122
|
"diffusers", "diffusers.*",
|
|
114
123
|
"ollama", "ollama.*",
|
|
115
|
-
"ultralytics", "ultralytics.*",
|
|
116
124
|
"transformers", "transformers.*",
|
|
117
125
|
"transnetv2_pytorch", "transnetv2_pytorch.*",
|
|
118
126
|
"chatterbox", "chatterbox.*",
|
|
@@ -133,8 +141,8 @@ ignore_missing_imports = true
|
|
|
133
141
|
# These overrides let the combined [ai] resolve pick compatible versions; the
|
|
134
142
|
# floors in [ai] are aligned with them so pip and uv resolve similar versions.
|
|
135
143
|
override-dependencies = [
|
|
136
|
-
"torch>=2.8.0", "torchaudio>=2.8.0", "numpy>=2.0.0", "diffusers>=0.
|
|
137
|
-
#
|
|
144
|
+
"torch>=2.8.0", "torchaudio>=2.8.0", "torchvision>=0.23.0,<0.24.0", "numpy>=2.0.0", "diffusers>=0.35.0",
|
|
145
|
+
# Some transitive deps pull opencv-python, which conflicts with our
|
|
138
146
|
# opencv-python-headless (both provide cv2). Exclude opencv-python so
|
|
139
147
|
# only the headless variant is installed.
|
|
140
148
|
"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,15 +59,26 @@ 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
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
# Text-to-image — Qwen-Image (ai/generation/image.py: TextToImage), Apache-2.0;
|
|
75
|
+
# replaced the OpenRAIL++-licensed SDXL.
|
|
76
|
+
"Qwen/Qwen-Image-2512": "25468b98e3276ca6700de15c6628e51b7de54a26",
|
|
77
|
+
# Text/image-to-video — Wan2.2 (ai/generation/video.py: TextToVideo / ImageToVideo),
|
|
78
|
+
# Apache-2.0; replaced the custom-licensed CogVideoX. The fp32 VAE is loaded from the
|
|
79
|
+
# same repo (subfolder="vae"), so revision=pinned(repo) covers both from_pretrained calls.
|
|
80
|
+
"Wan-AI/Wan2.2-T2V-A14B-Diffusers": "5be7df9619b54f4e2667b2755bc6a756675b5cd7",
|
|
81
|
+
"Wan-AI/Wan2.2-I2V-A14B-Diffusers": "596658fd9ca6b7b71d5057529bbf319ecbc61d74",
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Each symbol's backing leaf module is imported only on first access, so
|
|
4
4
|
``from videopython.ai.generation import TextToImage`` does not pull in
|
|
5
|
-
``audio`` (chatterbox/musicgen) or ``video`` (diffusers/
|
|
5
|
+
``audio`` (chatterbox/musicgen) or ``video`` (diffusers/Wan2.2). The
|
|
6
6
|
``TYPE_CHECKING`` block keeps the symbols visible to mypy and IDEs.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Image generation using local diffusion models (Qwen-Image)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
from videopython.ai._device import log_device_initialization, select_device
|
|
10
|
+
from videopython.ai._predictor import ManagedPredictor
|
|
11
|
+
from videopython.ai._revisions import pinned
|
|
12
|
+
|
|
13
|
+
_MODEL_NAME = "Qwen/Qwen-Image-2512"
|
|
14
|
+
|
|
15
|
+
# Qwen-Image recommends appending a quality "magic" suffix to the prompt
|
|
16
|
+
# (verbatim from the model card).
|
|
17
|
+
_POSITIVE_MAGIC = ", Ultra HD, 4K, cinematic composition."
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TextToImage(ManagedPredictor):
|
|
21
|
+
"""Generates images from text descriptions using local models (Qwen-Image, Apache-2.0)."""
|
|
22
|
+
|
|
23
|
+
_model_attrs = ("_pipeline",)
|
|
24
|
+
|
|
25
|
+
def __init__(self, device: str | None = None):
|
|
26
|
+
self.device = device
|
|
27
|
+
self._pipeline: Any = None
|
|
28
|
+
|
|
29
|
+
def _init_local(self) -> None:
|
|
30
|
+
"""Initialize the local Qwen-Image diffusion pipeline (CUDA-only)."""
|
|
31
|
+
import torch
|
|
32
|
+
|
|
33
|
+
from videopython.ai._optional import require
|
|
34
|
+
|
|
35
|
+
requested_device = self.device
|
|
36
|
+
device = select_device(self.device, mps_allowed=False)
|
|
37
|
+
if device != "cuda":
|
|
38
|
+
raise RuntimeError("TextToImage requires a CUDA GPU; Qwen-Image (~20B) is impractical on CPU/MPS.")
|
|
39
|
+
|
|
40
|
+
QwenImagePipeline = require("diffusers", feature="TextToImage").QwenImagePipeline
|
|
41
|
+
self._pipeline = QwenImagePipeline.from_pretrained(
|
|
42
|
+
_MODEL_NAME,
|
|
43
|
+
revision=pinned(_MODEL_NAME),
|
|
44
|
+
torch_dtype=torch.bfloat16,
|
|
45
|
+
use_safetensors=True,
|
|
46
|
+
)
|
|
47
|
+
# ~20B params (Qwen2.5-VL text encoder + transformer + VAE). Offload submodules
|
|
48
|
+
# to the GPU on demand so it fits a single GPU; offload manages device placement,
|
|
49
|
+
# so we must NOT also call .to("cuda").
|
|
50
|
+
self._pipeline.enable_model_cpu_offload()
|
|
51
|
+
self._pipeline.enable_vae_tiling()
|
|
52
|
+
|
|
53
|
+
self.device = device
|
|
54
|
+
log_device_initialization(
|
|
55
|
+
"TextToImage",
|
|
56
|
+
requested_device=requested_device,
|
|
57
|
+
resolved_device=device,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def generate_image(
|
|
61
|
+
self,
|
|
62
|
+
prompt: str,
|
|
63
|
+
*,
|
|
64
|
+
negative_prompt: str = " ",
|
|
65
|
+
true_cfg_scale: float = 4.0,
|
|
66
|
+
num_inference_steps: int = 50,
|
|
67
|
+
width: int = 1328,
|
|
68
|
+
height: int = 1328,
|
|
69
|
+
add_magic: bool = True,
|
|
70
|
+
seed: int = 42,
|
|
71
|
+
) -> Image.Image:
|
|
72
|
+
"""Generate an image from a text prompt.
|
|
73
|
+
|
|
74
|
+
Qwen-Image uses ``true_cfg_scale`` (not ``guidance_scale``) for
|
|
75
|
+
classifier-free guidance; a non-empty ``negative_prompt`` (default a single
|
|
76
|
+
space) is required to enable it. ``add_magic`` appends the model's
|
|
77
|
+
recommended quality suffix to ``prompt``.
|
|
78
|
+
"""
|
|
79
|
+
import torch
|
|
80
|
+
|
|
81
|
+
if self._pipeline is None:
|
|
82
|
+
self._init_local()
|
|
83
|
+
|
|
84
|
+
full_prompt = prompt + _POSITIVE_MAGIC if add_magic else prompt
|
|
85
|
+
return self._pipeline(
|
|
86
|
+
prompt=full_prompt,
|
|
87
|
+
negative_prompt=negative_prompt,
|
|
88
|
+
true_cfg_scale=true_cfg_scale,
|
|
89
|
+
num_inference_steps=num_inference_steps,
|
|
90
|
+
width=width,
|
|
91
|
+
height=height,
|
|
92
|
+
generator=torch.Generator(device=self.device).manual_seed(seed),
|
|
93
|
+
).images[0]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Video generation using local diffusion models (Wan2.2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from videopython.ai._device import log_device_initialization, select_device
|
|
10
|
+
from videopython.ai._predictor import ManagedPredictor
|
|
11
|
+
from videopython.ai._revisions import pinned
|
|
12
|
+
from videopython.base.video import Video
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from PIL.Image import Image
|
|
16
|
+
|
|
17
|
+
# Canonical Wan2.2 negative prompt (from the diffusers WanPipeline docstring example).
|
|
18
|
+
_WAN_NEGATIVE_PROMPT = (
|
|
19
|
+
"Bright tones, overexposed, static, blurred details, subtitles, style, works, "
|
|
20
|
+
"paintings, images, static, overall gray, worst quality, low quality, JPEG "
|
|
21
|
+
"compression residue, ugly, incomplete, extra fingers, poorly drawn hands, "
|
|
22
|
+
"poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, "
|
|
23
|
+
"still picture, messy background, three legs, many people in the background, "
|
|
24
|
+
"walking backwards"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _retie_umt5_embeddings(pipeline: Any) -> None:
|
|
29
|
+
"""Restore Wan's text conditioning under transformers>=5.2.
|
|
30
|
+
|
|
31
|
+
Wan's checkpoint ships the UMT5 token embedding only as ``shared.weight``, but
|
|
32
|
+
transformers 5.2's ``UMT5EncoderModel`` has ``tie_word_embeddings=False`` and reads a
|
|
33
|
+
*separate* ``encoder.embed_tokens`` that the checkpoint never populates -- so it stays
|
|
34
|
+
zero-initialized. The encoder then emits an all-zero embedding for every prompt and
|
|
35
|
+
generation silently ignores the text (verified on GPU: prompt has no effect). Point the
|
|
36
|
+
embedding lookup back at the loaded ``shared`` weight to make conditioning live again.
|
|
37
|
+
"""
|
|
38
|
+
text_encoder = pipeline.text_encoder
|
|
39
|
+
text_encoder.encoder.embed_tokens = text_encoder.shared
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_cuda_device(device: str | None, component: str) -> str:
|
|
43
|
+
"""Resolve to a CUDA device or raise.
|
|
44
|
+
|
|
45
|
+
Wan2.2-A14B (~28B MoE) is too large to run on CPU/MPS, so generation requires
|
|
46
|
+
a CUDA GPU rather than silently falling back to an unusable device.
|
|
47
|
+
"""
|
|
48
|
+
selected_device = select_device(device, mps_allowed=False)
|
|
49
|
+
if selected_device != "cuda":
|
|
50
|
+
raise RuntimeError(f"{component} requires a CUDA GPU; Wan2.2-A14B (~28B) is impractical on CPU/MPS.")
|
|
51
|
+
return selected_device
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TextToVideo(ManagedPredictor):
|
|
55
|
+
"""Generates videos from text descriptions using Wan2.2-T2V (Apache-2.0)."""
|
|
56
|
+
|
|
57
|
+
_model_attrs = ("_pipeline",)
|
|
58
|
+
|
|
59
|
+
def __init__(self, device: str | None = None):
|
|
60
|
+
self.device = device
|
|
61
|
+
self._pipeline: Any = None
|
|
62
|
+
|
|
63
|
+
def _init_local(self) -> None:
|
|
64
|
+
import torch
|
|
65
|
+
|
|
66
|
+
from videopython.ai._optional import require
|
|
67
|
+
|
|
68
|
+
diffusers = require("diffusers", feature="TextToVideo")
|
|
69
|
+
WanPipeline = diffusers.WanPipeline
|
|
70
|
+
AutoencoderKLWan = diffusers.AutoencoderKLWan
|
|
71
|
+
|
|
72
|
+
requested_device = self.device
|
|
73
|
+
device = _require_cuda_device(self.device, "TextToVideo")
|
|
74
|
+
|
|
75
|
+
model_name = "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
|
|
76
|
+
revision = pinned(model_name)
|
|
77
|
+
# VAE stays float32 for decode quality; the transformer/pipeline use bf16.
|
|
78
|
+
vae = AutoencoderKLWan.from_pretrained(
|
|
79
|
+
model_name, subfolder="vae", revision=revision, torch_dtype=torch.float32
|
|
80
|
+
)
|
|
81
|
+
self._pipeline = WanPipeline.from_pretrained(model_name, vae=vae, revision=revision, torch_dtype=torch.bfloat16)
|
|
82
|
+
_retie_umt5_embeddings(self._pipeline)
|
|
83
|
+
# A14B is a MoE (high+low-noise experts), too large for a single-GPU .to("cuda");
|
|
84
|
+
# offload submodules on demand (offload manages placement, so do NOT also call .to).
|
|
85
|
+
self._pipeline.enable_model_cpu_offload()
|
|
86
|
+
|
|
87
|
+
self.device = device
|
|
88
|
+
log_device_initialization(
|
|
89
|
+
"TextToVideo",
|
|
90
|
+
requested_device=requested_device,
|
|
91
|
+
resolved_device=device,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def generate_video(
|
|
95
|
+
self,
|
|
96
|
+
prompt: str,
|
|
97
|
+
num_steps: int = 40,
|
|
98
|
+
num_frames: int = 81,
|
|
99
|
+
guidance_scale: float = 4.0,
|
|
100
|
+
) -> Video:
|
|
101
|
+
"""Generate video from text prompt."""
|
|
102
|
+
import torch
|
|
103
|
+
|
|
104
|
+
if self._pipeline is None:
|
|
105
|
+
self._init_local()
|
|
106
|
+
|
|
107
|
+
# output_type="pil" is required: the "np" default returns float32 in [0, 1],
|
|
108
|
+
# which the uint8 cast below would floor to an all-black video.
|
|
109
|
+
video_frames = self._pipeline(
|
|
110
|
+
prompt=prompt,
|
|
111
|
+
negative_prompt=_WAN_NEGATIVE_PROMPT,
|
|
112
|
+
height=720,
|
|
113
|
+
width=1280,
|
|
114
|
+
num_frames=num_frames,
|
|
115
|
+
num_inference_steps=num_steps,
|
|
116
|
+
guidance_scale=guidance_scale,
|
|
117
|
+
guidance_scale_2=3.0, # low-noise MoE expert; None would reuse guidance_scale
|
|
118
|
+
output_type="pil",
|
|
119
|
+
generator=torch.Generator(device=self.device).manual_seed(42),
|
|
120
|
+
).frames[0]
|
|
121
|
+
video_frames = np.asarray(video_frames, dtype=np.uint8)
|
|
122
|
+
return Video.from_frames(video_frames, fps=16.0)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ImageToVideo(ManagedPredictor):
|
|
126
|
+
"""Generates videos from static images using Wan2.2-I2V (Apache-2.0)."""
|
|
127
|
+
|
|
128
|
+
_model_attrs = ("_pipeline",)
|
|
129
|
+
|
|
130
|
+
def __init__(self, device: str | None = None):
|
|
131
|
+
self.device = device
|
|
132
|
+
self._pipeline: Any = None
|
|
133
|
+
|
|
134
|
+
def _init_local(self) -> None:
|
|
135
|
+
import torch
|
|
136
|
+
|
|
137
|
+
from videopython.ai._optional import require
|
|
138
|
+
|
|
139
|
+
diffusers = require("diffusers", feature="ImageToVideo")
|
|
140
|
+
WanImageToVideoPipeline = diffusers.WanImageToVideoPipeline
|
|
141
|
+
AutoencoderKLWan = diffusers.AutoencoderKLWan
|
|
142
|
+
|
|
143
|
+
requested_device = self.device
|
|
144
|
+
device = _require_cuda_device(self.device, "ImageToVideo")
|
|
145
|
+
|
|
146
|
+
model_name = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
|
|
147
|
+
revision = pinned(model_name)
|
|
148
|
+
vae = AutoencoderKLWan.from_pretrained(
|
|
149
|
+
model_name, subfolder="vae", revision=revision, torch_dtype=torch.float32
|
|
150
|
+
)
|
|
151
|
+
self._pipeline = WanImageToVideoPipeline.from_pretrained(
|
|
152
|
+
model_name, vae=vae, revision=revision, torch_dtype=torch.bfloat16
|
|
153
|
+
)
|
|
154
|
+
_retie_umt5_embeddings(self._pipeline)
|
|
155
|
+
# A14B MoE is too large for a single-GPU .to("cuda"); offload submodules on demand.
|
|
156
|
+
self._pipeline.enable_model_cpu_offload()
|
|
157
|
+
|
|
158
|
+
self.device = device
|
|
159
|
+
log_device_initialization(
|
|
160
|
+
"ImageToVideo",
|
|
161
|
+
requested_device=requested_device,
|
|
162
|
+
resolved_device=device,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _resize_to_model_grid(self, image: Image) -> tuple[Image, int, int]:
|
|
166
|
+
"""Resize ``image`` to Wan's area budget, snapped to the model's spatial grid.
|
|
167
|
+
|
|
168
|
+
Returns the resized image plus the ``(height, width)`` to request, derived
|
|
169
|
+
from the input aspect ratio against Wan's 480x832 area (per the model card).
|
|
170
|
+
"""
|
|
171
|
+
max_area = 480 * 832
|
|
172
|
+
aspect_ratio = image.height / image.width
|
|
173
|
+
mod_value = self._pipeline.vae_scale_factor_spatial * self._pipeline.transformer.config.patch_size[1]
|
|
174
|
+
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
|
|
175
|
+
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
|
|
176
|
+
return image.resize((width, height)), height, width
|
|
177
|
+
|
|
178
|
+
def generate_video(
|
|
179
|
+
self,
|
|
180
|
+
image: Image,
|
|
181
|
+
prompt: str = "",
|
|
182
|
+
num_steps: int = 40,
|
|
183
|
+
num_frames: int = 81,
|
|
184
|
+
guidance_scale: float = 3.5,
|
|
185
|
+
) -> Video:
|
|
186
|
+
"""Generate video animation from a static image."""
|
|
187
|
+
import torch
|
|
188
|
+
|
|
189
|
+
if self._pipeline is None:
|
|
190
|
+
self._init_local()
|
|
191
|
+
|
|
192
|
+
image, height, width = self._resize_to_model_grid(image)
|
|
193
|
+
video_frames = self._pipeline(
|
|
194
|
+
image=image,
|
|
195
|
+
prompt=prompt,
|
|
196
|
+
negative_prompt=_WAN_NEGATIVE_PROMPT,
|
|
197
|
+
height=height,
|
|
198
|
+
width=width,
|
|
199
|
+
num_frames=num_frames,
|
|
200
|
+
num_inference_steps=num_steps,
|
|
201
|
+
guidance_scale=guidance_scale,
|
|
202
|
+
# No guidance_scale_2: the low-noise MoE expert reuses guidance_scale (its None default).
|
|
203
|
+
output_type="pil",
|
|
204
|
+
generator=torch.Generator(device=self.device).manual_seed(42),
|
|
205
|
+
).frames[0]
|
|
206
|
+
video_frames = np.asarray(video_frames, dtype=np.uint8)
|
|
207
|
+
return Video.from_frames(video_frames, fps=16.0)
|
|
@@ -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
|