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.
Files changed (95) hide show
  1. {videopython-0.52.1 → videopython-0.54.0}/PKG-INFO +6 -5
  2. {videopython-0.52.1 → videopython-0.54.0}/README.md +2 -2
  3. {videopython-0.52.1 → videopython-0.54.0}/pyproject.toml +17 -9
  4. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/__init__.py +1 -1
  5. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_optional.py +1 -1
  6. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_predictor.py +3 -3
  7. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_revisions.py +18 -15
  8. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/local.py +5 -4
  9. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/translation.py +1 -1
  10. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/effects.py +4 -4
  11. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/generation/__init__.py +1 -1
  12. videopython-0.54.0/src/videopython/ai/generation/image.py +93 -0
  13. videopython-0.54.0/src/videopython/ai/generation/video.py +207 -0
  14. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/transforms.py +0 -4
  15. videopython-0.52.1/src/videopython/ai/understanding/_yolo.py → videopython-0.54.0/src/videopython/ai/understanding/_detector.py +27 -43
  16. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/faces.py +76 -127
  17. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/image.py +1 -1
  18. videopython-0.54.0/src/videopython/ai/understanding/objects.py +141 -0
  19. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/models.py +1 -1
  20. videopython-0.52.1/src/videopython/ai/generation/image.py +0 -59
  21. videopython-0.52.1/src/videopython/ai/generation/video.py +0 -131
  22. videopython-0.52.1/src/videopython/ai/understanding/objects.py +0 -96
  23. {videopython-0.52.1 → videopython-0.54.0}/.gitignore +0 -0
  24. {videopython-0.52.1 → videopython-0.54.0}/LICENSE +0 -0
  25. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/__init__.py +0 -0
  26. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_device.py +0 -0
  27. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/_ollama.py +0 -0
  28. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
  29. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/backend.py +0 -0
  30. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
  31. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/editor.py +0 -0
  32. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/models.py +0 -0
  33. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
  34. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/__init__.py +0 -0
  35. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/_tts_backend.py +0 -0
  36. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/audio_ops.py +0 -0
  37. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/config.py +0 -0
  38. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/dubber.py +0 -0
  39. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/models.py +0 -0
  40. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/pipeline.py +0 -0
  41. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/quality.py +0 -0
  42. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/remux.py +0 -0
  43. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/separation.py +0 -0
  44. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/timing.py +0 -0
  45. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  46. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/errors.py +0 -0
  47. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/generation/audio.py +0 -0
  48. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/keyframe.py +0 -0
  49. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/ops.py +0 -0
  50. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/__init__.py +0 -0
  51. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/audio.py +0 -0
  52. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/classification.py +0 -0
  53. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/understanding/temporal.py +0 -0
  54. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  55. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  56. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/detectors.py +0 -0
  57. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  58. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/ai/video_analysis/source_metadata.py +0 -0
  59. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/__init__.py +0 -0
  60. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/analysis.py +0 -0
  61. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/audio/audio.py +0 -0
  62. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/__init__.py +0 -0
  63. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_dimensions.py +0 -0
  64. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_ffmpeg.py +0 -0
  65. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/_video_io.py +0 -0
  66. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/description.py +0 -0
  67. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/draw_detections.py +0 -0
  68. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/exceptions.py +0 -0
  69. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  70. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  71. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  72. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  73. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  74. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  75. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  76. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  77. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  78. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  79. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/fonts/__init__.py +0 -0
  80. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/transcription.py +0 -0
  81. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/base/video.py +0 -0
  82. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/__init__.py +0 -0
  83. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_ass.py +0 -0
  84. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_easing.py +0 -0
  85. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/_schema.py +0 -0
  86. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/audio_ops.py +0 -0
  87. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/effects.py +0 -0
  88. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/operation.py +0 -0
  89. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/streaming.py +0 -0
  90. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/transcription_overlay.py +0 -0
  91. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/transforms.py +0 -0
  92. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/editing/video_edit.py +0 -0
  93. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/mcp/__init__.py +0 -0
  94. {videopython-0.52.1 → videopython-0.54.0}/src/videopython/mcp/server.py +0 -0
  95. {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.52.1
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.30.0; extra == 'ai'
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 gemma3:27b`).
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="gemma3:27b")) # ollama pull gemma3:27b
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 gemma3:27b`).
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="gemma3:27b")) # ollama pull gemma3:27b
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.52.1"
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, scene/temporal, VLM (understanding/*, video_analysis)
72
- "ultralytics>=8.0.0",
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: SDXL/CogVideoX + MusicGen (generation/*)
82
- "diffusers>=0.30.0",
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.30.0",
137
- # ultralytics depends on opencv-python which conflicts with our
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 / ultralytics) until a symbol is actually used. When
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 / ultralytics) until a symbol is used.
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 / ultralytics (``release_device_memory``
23
- defers its torch import), so it stays safe to mix into any predictor regardless
24
- of how it is constructed.
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
- # Face detection checkpoint (ai/understanding/faces.py: hf_hub_download)
71
- "arnabdhar/YOLOv8-Face-Detection": "52fa54977207fa4f021de949b515fb19dcab4488",
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
- # SDXL (ai/generation/image.py: TextToImage)
75
- "stabilityai/stable-diffusion-xl-base-1.0": "462165984030d82259a11f4367a4eed129e94a7b",
76
- # CogVideoX (ai/generation/video.py: TextToVideo / ImageToVideo)
77
- "THUDM/CogVideoX1.5-5B": "fdc5267c90b5c06492985b966e43aae984e189e0",
78
- "THUDM/CogVideoX1.5-5B-I2V": "46c90528707aebbe69066390b4fe7e7d24c9c2a4",
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 = "gemma3:27b"
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). Not
22
- every model supports schema conditioning -- ``gemma3:27b`` is verified working;
23
- some builds (e.g. certain MLX ones) fail it. ``ollama pull <model>`` first;
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 = "gemma3:27b"
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 YOLOv8-COCO detector and composites tidy, colour-coded boxes with
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
- "YOLOv8 model size: 'n' (nano, fastest), 's' (small), 'm' (medium, most accurate). "
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=f"yolov8{self.model_size}.pt",
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/CogVideoX). The
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