videopython 0.53.0__tar.gz → 0.54.1__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 (94) hide show
  1. {videopython-0.53.0 → videopython-0.54.1}/PKG-INFO +7 -4
  2. {videopython-0.53.0 → videopython-0.54.1}/pyproject.toml +49 -14
  3. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/_revisions.py +8 -5
  4. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/generation/__init__.py +1 -1
  5. videopython-0.54.1/src/videopython/ai/generation/image.py +93 -0
  6. videopython-0.54.1/src/videopython/ai/generation/video.py +207 -0
  7. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/faces.py +1 -1
  8. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/audio/audio.py +5 -9
  9. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/video.py +3 -2
  10. videopython-0.53.0/src/videopython/ai/generation/image.py +0 -59
  11. videopython-0.53.0/src/videopython/ai/generation/video.py +0 -131
  12. {videopython-0.53.0 → videopython-0.54.1}/.gitignore +0 -0
  13. {videopython-0.53.0 → videopython-0.54.1}/LICENSE +0 -0
  14. {videopython-0.53.0 → videopython-0.54.1}/README.md +0 -0
  15. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/__init__.py +0 -0
  16. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/__init__.py +0 -0
  17. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/_device.py +0 -0
  18. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/_ollama.py +0 -0
  19. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/_optional.py +0 -0
  20. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/_predictor.py +0 -0
  21. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/__init__.py +0 -0
  22. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/backend.py +0 -0
  23. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/catalog.py +0 -0
  24. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/editor.py +0 -0
  25. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/local.py +0 -0
  26. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/models.py +0 -0
  27. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/auto_edit/resolve.py +0 -0
  28. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/__init__.py +0 -0
  29. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/_tts_backend.py +0 -0
  30. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/audio_ops.py +0 -0
  31. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/config.py +0 -0
  32. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/dubber.py +0 -0
  33. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/models.py +0 -0
  34. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/pipeline.py +0 -0
  35. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/quality.py +0 -0
  36. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/remux.py +0 -0
  37. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/separation.py +0 -0
  38. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/timing.py +0 -0
  39. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/translation.py +0 -0
  40. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  41. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/effects.py +0 -0
  42. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/errors.py +0 -0
  43. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/generation/audio.py +0 -0
  44. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/keyframe.py +0 -0
  45. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/ops.py +0 -0
  46. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/transforms.py +0 -0
  47. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/__init__.py +0 -0
  48. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/_detector.py +0 -0
  49. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/audio.py +0 -0
  50. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/classification.py +0 -0
  51. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/image.py +0 -0
  52. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/objects.py +0 -0
  53. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/understanding/temporal.py +0 -0
  54. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/__init__.py +0 -0
  55. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  56. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/detectors.py +0 -0
  57. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/models.py +0 -0
  58. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/sampling.py +0 -0
  59. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/ai/video_analysis/source_metadata.py +0 -0
  60. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/audio/__init__.py +0 -0
  61. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/audio/analysis.py +0 -0
  62. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/__init__.py +0 -0
  63. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/_dimensions.py +0 -0
  64. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/_ffmpeg.py +0 -0
  65. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/_video_io.py +0 -0
  66. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/description.py +0 -0
  67. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/draw_detections.py +0 -0
  68. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/exceptions.py +0 -0
  69. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  70. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  71. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  72. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  73. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  74. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  75. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  76. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  77. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  78. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  79. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/fonts/__init__.py +0 -0
  80. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/base/transcription.py +0 -0
  81. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/__init__.py +0 -0
  82. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/_ass.py +0 -0
  83. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/_easing.py +0 -0
  84. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/_schema.py +0 -0
  85. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/audio_ops.py +0 -0
  86. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/effects.py +0 -0
  87. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/operation.py +0 -0
  88. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/streaming.py +0 -0
  89. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/transcription_overlay.py +0 -0
  90. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/transforms.py +0 -0
  91. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/editing/video_edit.py +0 -0
  92. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/mcp/__init__.py +0 -0
  93. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/mcp/server.py +0 -0
  94. {videopython-0.53.0 → videopython-0.54.1}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.53.0
3
+ Version: 0.54.1
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/
@@ -24,10 +24,11 @@ Requires-Dist: resvg-py>=0.3.2
24
24
  Requires-Dist: tqdm>=4.66.3
25
25
  Provides-Extra: ai
26
26
  Requires-Dist: accelerate>=0.29.2; extra == 'ai'
27
- Requires-Dist: chatterbox-tts>=0.1.7; extra == 'ai'
28
27
  Requires-Dist: demucs>=4.0.0; extra == 'ai'
29
- Requires-Dist: diffusers>=0.30.0; extra == 'ai'
28
+ Requires-Dist: diffusers>=0.35.0; extra == 'ai'
29
+ Requires-Dist: ftfy>=6.1; extra == 'ai'
30
30
  Requires-Dist: imagehash>=4.3; extra == 'ai'
31
+ Requires-Dist: numba>=0.62; extra == 'ai'
31
32
  Requires-Dist: ollama>=0.5.0; extra == 'ai'
32
33
  Requires-Dist: openai-whisper>=20240930; extra == 'ai'
33
34
  Requires-Dist: pyannote-audio>=4.0.0; extra == 'ai'
@@ -35,9 +36,11 @@ 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'
38
- Requires-Dist: torchvision<0.24.0,>=0.23.0; extra == 'ai'
39
+ Requires-Dist: torchcodec>=0.7.0; extra == 'ai'
40
+ Requires-Dist: torchvision>=0.23.0; extra == 'ai'
39
41
  Requires-Dist: transformers>=5.2.0; extra == 'ai'
40
42
  Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
43
+ Requires-Dist: videopython-chatterbox>=0.1.7.post1; extra == 'ai'
41
44
  Provides-Extra: mcp
42
45
  Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
43
46
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "videopython"
3
- version = "0.53.0"
3
+ version = "0.54.1"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -66,6 +66,13 @@ ai = ["videopython[ai]"]
66
66
  ai = [
67
67
  # Speech recognition / diarization (understanding/audio.py)
68
68
  "openai-whisper>=20240930",
69
+ # whisper imports numba (timing.py), and numba is version-coupled to numpy:
70
+ # each release declares a `numpy<X` cap that moves forward (0.62 -> <2.4,
71
+ # 0.66 -> <2.5). Without a floor a resolver can satisfy a newer numpy by
72
+ # backtracking numba to an ancient release instead, which then fails at
73
+ # `import whisper`. Floor it so numba's own cap does the work; 0.62 is the
74
+ # oldest release covering our full Python range (3.11-3.13).
75
+ "numba>=0.62",
69
76
  "pyannote-audio>=4.0.0",
70
77
  "silero-vad>=5.1",
71
78
  # Visual understanding: detection (D-FINE/YuNet via transformers + opencv core
@@ -75,11 +82,21 @@ ai = [
75
82
  "imagehash>=4.3",
76
83
  # Audio source separation (understanding/separation.py)
77
84
  "demucs>=4.0.0",
78
- # Voice cloning TTS (generation/audio.py — Chatterbox); its strict pins drive
79
- # the [tool.uv].override-dependencies block below.
80
- "chatterbox-tts>=0.1.7",
81
- # Local media generation: SDXL/CogVideoX + MusicGen (generation/*)
82
- "diffusers>=0.30.0",
85
+ # Voice cloning TTS (generation/audio.py — Chatterbox). Upstream chatterbox-tts
86
+ # 0.1.7 pins diffusers==0.29.0 / torch==2.6.0 / transformers==5.2.0 with `==`,
87
+ # which is unsatisfiable against pyannote-audio (torch>=2.8) and our
88
+ # diffusers>=0.35 — it made `pip install "videopython[ai]"` impossible for
89
+ # consumers. videopython-chatterbox is upstream's source with corrected
90
+ # metadata (identical apart from its self-version lookup); the import name is
91
+ # still `chatterbox`. See https://github.com/BartWojtowicz/videopython-chatterbox
92
+ "videopython-chatterbox>=0.1.7.post1",
93
+ # Local media generation: Qwen-Image/Wan2.2 + MusicGen (generation/*).
94
+ # >=0.35 is the floor that ships QwenImagePipeline + Wan2.2 support (tested on 0.37.1).
95
+ "diffusers>=0.35.0",
96
+ # Wan2.2 i2v pipeline cleans prompts with ftfy via a hard import (diffusers'
97
+ # pipeline_wan_i2v); the t2v pipeline guards it with is_ftfy_available(), i2v
98
+ # does not, so it is a required dep for ImageToVideo.
99
+ "ftfy>=6.1",
83
100
  "accelerate>=0.29.2",
84
101
  # Dubbing loudness matching (dubbing/loudness.py)
85
102
  "pyloudnorm>=0.1.1",
@@ -89,9 +106,17 @@ ai = [
89
106
  # processor used by the D-FINE object detector (understanding/objects.py).
90
107
  "torch>=2.8.0",
91
108
  "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",
109
+ # No upper bound: each torchvision release declares an exact `torch==X.Y.Z`
110
+ # pin, so resolvers co-select a valid pair unaided. Capping torchvision alone
111
+ # is actively harmful — it transitively pinned torch to 2.8 (cu12) while
112
+ # torchaudio floated to 2.11 (cu13), resolving cleanly and then dying at
113
+ # import with `OSError: libcudart.so.13`. Cap every member or none.
114
+ "torchvision>=0.23.0",
115
+ # torchaudio 2.11 routes save()/load() through torchcodec, and torchcodec is a
116
+ # native extension linked against libtorch that declares no torch requirement
117
+ # of its own. It arrives transitively via pyannote-audio; declare it so it is
118
+ # visible rather than incidental.
119
+ "torchcodec>=0.7.0",
95
120
  ]
96
121
  # MCP server (videopython/mcp/). Pin <2 — v2 is pre-release/breaking.
97
122
  mcp = ["mcp>=1.27,<2"]
@@ -131,12 +156,22 @@ module = [
131
156
  ignore_missing_imports = true
132
157
 
133
158
  [tool.uv]
134
- # chatterbox-tts 0.1.7 (in [ai]) pins strict torch/torchaudio/numpy/diffusers
135
- # that conflict with pyannote-audio (torch>=2.8) and CogVideoX (diffusers>=0.30).
136
- # These overrides let the combined [ai] resolve pick compatible versions; the
137
- # floors in [ai] are aligned with them so pip and uv resolve similar versions.
159
+ # NOTE: overrides are a uv workspace feature — they do NOT ship in the built
160
+ # wheel. Anything reconciled only here is invisible to `pip install videopython`,
161
+ # so [ai] must be resolvable without this block. Keep it minimal.
162
+ #
163
+ # The torch/torchaudio/torchvision/diffusers overrides that used to live here
164
+ # existed solely to paper over chatterbox-tts's `==` pins. They masked the
165
+ # breakage from CI while every downstream consumer hit it, and an override
166
+ # replaces a requirement *everywhere* — including torchvision's own exact
167
+ # `torch==2.8.0` pin, which would have produced an ABI-invalid pair on the next
168
+ # `uv lock --upgrade`. [ai] now depends on videopython-chatterbox, whose metadata
169
+ # is correct, so none of them are needed.
170
+ # The numpy>=2.0.0 override is gone for the same reason: it existed to counter
171
+ # chatterbox-tts's `numpy<2.0.0` pin, and it also replaced numba's `numpy<2.5`
172
+ # cap, resolving numpy 2.5 into an environment where `import whisper` dies with
173
+ # "Numba needs NumPy 2.4 or less". numba IS version-coupled to numpy.
138
174
  override-dependencies = [
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
175
  # Some transitive deps pull opencv-python, which conflicts with our
141
176
  # opencv-python-headless (both provide cv2). Exclude opencv-python so
142
177
  # only the headless variant is installed.
@@ -71,11 +71,14 @@ MODEL_REVISIONS: dict[str, str] = {
71
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
 
@@ -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)
@@ -236,7 +236,7 @@ class FaceSmoothingTracker(_FaceTrackerBase):
236
236
  frame_center = (0.5, 0.5)
237
237
  _, bbox = min(
238
238
  faces_with_box,
239
- key=lambda fb: ((fb[1].center[0] - frame_center[0]) ** 2 + (fb[1].center[1] - frame_center[1]) ** 2),
239
+ key=lambda fb: (fb[1].center[0] - frame_center[0]) ** 2 + (fb[1].center[1] - frame_center[1]) ** 2,
240
240
  )
241
241
  elif self.selection_strategy == "index":
242
242
  idx = self.face_index if self.face_index < len(faces_with_box) else 0
@@ -224,16 +224,12 @@ class Audio:
224
224
  if dtype is None:
225
225
  raise AudioLoadError(f"Unsupported sample width: {sample_width}")
226
226
 
227
- data = np.frombuffer(raw_data, dtype=dtype)
227
+ # Explicitly annotated: numpy>=2.5 shape-types ndarray, so the
228
+ # 1-D frombuffer result cannot be rebound to a 2-D view below.
229
+ data: np.ndarray[Any, np.dtype[np.float32]]
230
+ data = np.frombuffer(raw_data, dtype=dtype).astype(np.float32)
228
231
 
229
- # Reshape if stereo
230
- if channels == 2:
231
- data = data.reshape(-1, 2)
232
-
233
- # Convert to float32
234
- data = data.astype(np.float32)
235
-
236
- # Reshape before normalization if stereo
232
+ # Reshape to (frames, channels) if stereo
237
233
  if channels == 2:
238
234
  data = data.reshape(-1, 2)
239
235
 
@@ -427,8 +427,9 @@ def extract_frames_at_indices(
427
427
  # Truncate to complete frames only
428
428
  raw_data = raw_data[: actual_frames * frame_size]
429
429
 
430
- frames = np.frombuffer(raw_data, dtype=np.uint8).copy()
431
- frames = frames.reshape(-1, metadata.height, metadata.width, 3)
430
+ # Chained rather than reshaped in place: numpy>=2.5 shape-types ndarray, so
431
+ # rebinding a 1-D frombuffer result to a 4-D view is an assignment error.
432
+ frames = np.frombuffer(raw_data, dtype=np.uint8).copy().reshape(-1, metadata.height, metadata.width, 3)
432
433
 
433
434
  # Reorder to match original frame_indices order if needed
434
435
  if unique_sorted_indices != frame_indices:
@@ -1,59 +0,0 @@
1
- """Image generation using local diffusion models."""
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
-
14
- class TextToImage(ManagedPredictor):
15
- """Generates images from text descriptions using local models."""
16
-
17
- _model_attrs = ("_pipeline",)
18
-
19
- def __init__(self, device: str | None = None):
20
- self.device = device
21
- self._pipeline: Any = None
22
-
23
- def _init_local(self) -> None:
24
- """Initialize local diffusion pipeline."""
25
- import torch
26
-
27
- from videopython.ai._optional import require
28
-
29
- DiffusionPipeline = require("diffusers", feature="TextToImage").DiffusionPipeline
30
-
31
- requested_device = self.device
32
- device = select_device(self.device, mps_allowed=True)
33
- dtype = torch.float16 if device == "cuda" else torch.float32
34
- variant = "fp16" if device == "cuda" else None
35
-
36
- model_name = "stabilityai/stable-diffusion-xl-base-1.0"
37
- self._pipeline = DiffusionPipeline.from_pretrained(
38
- model_name,
39
- revision=pinned(model_name),
40
- torch_dtype=dtype,
41
- variant=variant,
42
- use_safetensors=True,
43
- )
44
- self._pipeline.to(device)
45
- self.device = device
46
- log_device_initialization(
47
- "TextToImage",
48
- requested_device=requested_device,
49
- resolved_device=device,
50
- )
51
-
52
- if device == "mps":
53
- self._pipeline.enable_attention_slicing()
54
-
55
- def generate_image(self, prompt: str) -> Image.Image:
56
- """Generate an image from a text prompt."""
57
- if self._pipeline is None:
58
- self._init_local()
59
- return self._pipeline(prompt=prompt).images[0]
@@ -1,131 +0,0 @@
1
- """Video generation using local diffusion models."""
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
-
18
- def _get_torch_device_and_dtype(device: str | None) -> tuple[str, Any]:
19
- """Get the best available torch device and dtype for CogVideoX."""
20
- import torch
21
-
22
- selected_device = select_device(device, mps_allowed=False)
23
- if selected_device == "cuda":
24
- return selected_device, torch.bfloat16
25
- return selected_device, torch.float32
26
-
27
-
28
- class TextToVideo(ManagedPredictor):
29
- """Generates videos from text descriptions using local diffusion models."""
30
-
31
- _model_attrs = ("_pipeline",)
32
-
33
- def __init__(self, device: str | None = None):
34
- self.device = device
35
- self._pipeline: Any = None
36
-
37
- def _init_local(self) -> None:
38
- from videopython.ai._optional import require
39
-
40
- CogVideoXPipeline = require("diffusers", feature="TextToVideo").CogVideoXPipeline
41
-
42
- requested_device = self.device
43
- device, dtype = _get_torch_device_and_dtype(self.device)
44
-
45
- model_name = "THUDM/CogVideoX1.5-5B"
46
- self._pipeline = CogVideoXPipeline.from_pretrained(model_name, revision=pinned(model_name), torch_dtype=dtype)
47
- self._pipeline.to(device)
48
- self.device = device
49
- log_device_initialization(
50
- "TextToVideo",
51
- requested_device=requested_device,
52
- resolved_device=device,
53
- )
54
-
55
- def generate_video(
56
- self,
57
- prompt: str,
58
- num_steps: int = 50,
59
- num_frames: int = 81,
60
- guidance_scale: float = 6.0,
61
- ) -> Video:
62
- """Generate video from text prompt."""
63
- import torch
64
-
65
- if self._pipeline is None:
66
- self._init_local()
67
-
68
- video_frames = self._pipeline(
69
- prompt=prompt,
70
- num_inference_steps=num_steps,
71
- num_frames=num_frames,
72
- guidance_scale=guidance_scale,
73
- generator=torch.Generator(device=self.device).manual_seed(42),
74
- ).frames[0]
75
- video_frames = np.asarray(video_frames, dtype=np.uint8)
76
- return Video.from_frames(video_frames, fps=16.0)
77
-
78
-
79
- class ImageToVideo(ManagedPredictor):
80
- """Generates videos from static images using local video diffusion."""
81
-
82
- _model_attrs = ("_pipeline",)
83
-
84
- def __init__(self, device: str | None = None):
85
- self.device = device
86
- self._pipeline: Any = None
87
-
88
- def _init_local(self) -> None:
89
- from videopython.ai._optional import require
90
-
91
- CogVideoXImageToVideoPipeline = require("diffusers", feature="ImageToVideo").CogVideoXImageToVideoPipeline
92
-
93
- requested_device = self.device
94
- device, dtype = _get_torch_device_and_dtype(self.device)
95
-
96
- model_name = "THUDM/CogVideoX1.5-5B-I2V"
97
- self._pipeline = CogVideoXImageToVideoPipeline.from_pretrained(
98
- model_name, revision=pinned(model_name), torch_dtype=dtype
99
- )
100
- self._pipeline.to(device)
101
- self.device = device
102
- log_device_initialization(
103
- "ImageToVideo",
104
- requested_device=requested_device,
105
- resolved_device=device,
106
- )
107
-
108
- def generate_video(
109
- self,
110
- image: Image,
111
- prompt: str = "",
112
- num_steps: int = 50,
113
- num_frames: int = 81,
114
- guidance_scale: float = 6.0,
115
- ) -> Video:
116
- """Generate video animation from a static image."""
117
- import torch
118
-
119
- if self._pipeline is None:
120
- self._init_local()
121
-
122
- video_frames = self._pipeline(
123
- prompt=prompt,
124
- image=image,
125
- num_inference_steps=num_steps,
126
- num_frames=num_frames,
127
- guidance_scale=guidance_scale,
128
- generator=torch.Generator(device=self.device).manual_seed(42),
129
- ).frames[0]
130
- video_frames = np.asarray(video_frames, dtype=np.uint8)
131
- return Video.from_frames(video_frames, fps=16.0)
File without changes
File without changes
File without changes