videopython 0.47.0__tar.gz → 0.49.0__tar.gz

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