videopython 0.52.0__tar.gz → 0.53.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {videopython-0.52.0 → videopython-0.53.0}/PKG-INFO +4 -4
  2. {videopython-0.52.0 → videopython-0.53.0}/README.md +2 -2
  3. {videopython-0.52.0 → videopython-0.53.0}/pyproject.toml +10 -7
  4. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/__init__.py +1 -1
  5. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_optional.py +1 -1
  6. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_predictor.py +3 -3
  7. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_revisions.py +10 -10
  8. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/local.py +5 -4
  9. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/pipeline.py +41 -40
  10. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/separation.py +1 -6
  11. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/translation.py +1 -1
  12. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/effects.py +4 -4
  13. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/transforms.py +0 -4
  14. videopython-0.52.0/src/videopython/ai/understanding/_yolo.py → videopython-0.53.0/src/videopython/ai/understanding/_detector.py +27 -43
  15. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/classification.py +0 -4
  16. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/faces.py +76 -127
  17. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/image.py +1 -1
  18. videopython-0.53.0/src/videopython/ai/understanding/objects.py +141 -0
  19. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/models.py +1 -1
  20. videopython-0.52.0/src/videopython/ai/understanding/objects.py +0 -96
  21. {videopython-0.52.0 → videopython-0.53.0}/.gitignore +0 -0
  22. {videopython-0.52.0 → videopython-0.53.0}/LICENSE +0 -0
  23. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/__init__.py +0 -0
  24. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_device.py +0 -0
  25. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/_ollama.py +0 -0
  26. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
  27. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/backend.py +0 -0
  28. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
  29. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/editor.py +0 -0
  30. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/models.py +0 -0
  31. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
  32. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/__init__.py +0 -0
  33. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/_tts_backend.py +0 -0
  34. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/audio_ops.py +0 -0
  35. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/config.py +0 -0
  36. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/dubber.py +0 -0
  37. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/models.py +0 -0
  38. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/quality.py +0 -0
  39. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/remux.py +0 -0
  40. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/timing.py +0 -0
  41. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  42. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/errors.py +0 -0
  43. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/__init__.py +0 -0
  44. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/audio.py +0 -0
  45. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/image.py +0 -0
  46. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/generation/video.py +0 -0
  47. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/keyframe.py +0 -0
  48. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/ops.py +0 -0
  49. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/__init__.py +0 -0
  50. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/audio.py +0 -0
  51. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/understanding/temporal.py +0 -0
  52. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  53. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/analyzer.py +0 -0
  54. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/detectors.py +0 -0
  55. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  56. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/ai/video_analysis/source_metadata.py +0 -0
  57. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/__init__.py +0 -0
  58. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/analysis.py +0 -0
  59. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/audio/audio.py +0 -0
  60. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/__init__.py +0 -0
  61. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_dimensions.py +0 -0
  62. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_ffmpeg.py +0 -0
  63. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/_video_io.py +0 -0
  64. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/description.py +0 -0
  65. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/draw_detections.py +0 -0
  66. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/exceptions.py +0 -0
  67. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  68. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  69. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  70. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  71. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  72. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  73. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  74. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  75. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  76. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  77. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/fonts/__init__.py +0 -0
  78. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/transcription.py +0 -0
  79. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/base/video.py +0 -0
  80. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/__init__.py +0 -0
  81. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_ass.py +0 -0
  82. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_easing.py +0 -0
  83. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/_schema.py +0 -0
  84. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/audio_ops.py +0 -0
  85. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/effects.py +0 -0
  86. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/operation.py +0 -0
  87. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/streaming.py +0 -0
  88. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/transcription_overlay.py +0 -0
  89. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/transforms.py +0 -0
  90. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/editing/video_edit.py +0 -0
  91. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/mcp/__init__.py +0 -0
  92. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/mcp/server.py +0 -0
  93. {videopython-0.52.0 → videopython-0.53.0}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.52.0
3
+ Version: 0.53.0
4
4
  Summary: Minimal video generation and processing library.
5
5
  Project-URL: Homepage, https://videopython.com
6
6
  Project-URL: Repository, https://github.com/bartwojtowicz/videopython/
@@ -35,9 +35,9 @@ Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
35
35
  Requires-Dist: silero-vad>=5.1; extra == 'ai'
36
36
  Requires-Dist: torch>=2.8.0; extra == 'ai'
37
37
  Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
38
+ Requires-Dist: torchvision<0.24.0,>=0.23.0; extra == 'ai'
38
39
  Requires-Dist: transformers>=5.2.0; extra == 'ai'
39
40
  Requires-Dist: transnetv2-pytorch>=1.0.5; extra == 'ai'
40
- Requires-Dist: ultralytics>=8.0.0; extra == 'ai'
41
41
  Provides-Extra: mcp
42
42
  Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
43
43
  Description-Content-Type: text/markdown
@@ -63,7 +63,7 @@ pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
63
63
  pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
64
64
  ```
65
65
 
66
- Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull gemma3:27b`).
66
+ Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull qwen3.6:27b`).
67
67
 
68
68
  ## Quick Start
69
69
 
@@ -99,7 +99,7 @@ Give `AutoEditor` your clips and a brief; a local Ollama vision model selects an
99
99
  ```python
100
100
  from videopython.ai import AutoEditor, OllamaVisionLLM
101
101
 
102
- editor = AutoEditor(planner=OllamaVisionLLM(model="gemma3:27b")) # ollama pull gemma3:27b
102
+ editor = AutoEditor(planner=OllamaVisionLLM(model="qwen3.6:27b")) # ollama pull qwen3.6:27b
103
103
  edit = editor.edit(
104
104
  ["clip_a.mp4", "clip_b.mp4", "clip_c.mp4"],
105
105
  brief="A punchy 15-second teaser; lead with the most dynamic shot.",
@@ -19,7 +19,7 @@ pip install "videopython[ai]" # + ALL local AI features (GPU recommended)
19
19
  pip install "videopython[ai,mcp]" # + MCP server for agent-driven editing
20
20
  ```
21
21
 
22
- Python `>=3.11, <3.14`. AI features run locally — no cloud API keys required, but model weights are downloaded on first use. LLM-driven editing and scene captioning use a local [Ollama](https://ollama.com) server (`ollama pull 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.0"
3
+ version = "0.53.0"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -68,8 +68,8 @@ ai = [
68
68
  "openai-whisper>=20240930",
69
69
  "pyannote-audio>=4.0.0",
70
70
  "silero-vad>=5.1",
71
- # Visual understanding: detection, 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",
@@ -85,9 +85,13 @@ ai = [
85
85
  "pyloudnorm>=0.1.1",
86
86
  # LLM-authored editing planner (auto_edit/local.py OllamaVisionLLM)
87
87
  "ollama>=0.5.0",
88
- # Shared torch stack
88
+ # Shared torch stack. torchvision backs the transformers "fast" image
89
+ # processor used by the D-FINE object detector (understanding/objects.py).
89
90
  "torch>=2.8.0",
90
91
  "torchaudio>=2.8.0",
92
+ # Capped to the torch-2.8-compatible line; torchvision<->torch are ABI-coupled
93
+ # and uv does not know it (0.23.x pairs with torch 2.8.x).
94
+ "torchvision>=0.23.0,<0.24.0",
91
95
  ]
92
96
  # MCP server (videopython/mcp/). Pin <2 — v2 is pre-release/breaking.
93
97
  mcp = ["mcp>=1.27,<2"]
@@ -112,7 +116,6 @@ module = [
112
116
  "torch", "torch.*",
113
117
  "diffusers", "diffusers.*",
114
118
  "ollama", "ollama.*",
115
- "ultralytics", "ultralytics.*",
116
119
  "transformers", "transformers.*",
117
120
  "transnetv2_pytorch", "transnetv2_pytorch.*",
118
121
  "chatterbox", "chatterbox.*",
@@ -133,8 +136,8 @@ ignore_missing_imports = true
133
136
  # These overrides let the combined [ai] resolve pick compatible versions; the
134
137
  # floors in [ai] are aligned with them so pip and uv resolve similar versions.
135
138
  override-dependencies = [
136
- "torch>=2.8.0", "torchaudio>=2.8.0", "numpy>=2.0.0", "diffusers>=0.30.0",
137
- # ultralytics depends on opencv-python which conflicts with our
139
+ "torch>=2.8.0", "torchaudio>=2.8.0", "torchvision>=0.23.0,<0.24.0", "numpy>=2.0.0", "diffusers>=0.30.0",
140
+ # Some transitive deps pull opencv-python, which conflicts with our
138
141
  # opencv-python-headless (both provide cv2). Exclude opencv-python so
139
142
  # only the headless variant is installed.
140
143
  "opencv-python ; sys_platform == '_'",
@@ -5,7 +5,7 @@ Every public symbol is re-exported lazily via PEP 562 ``__getattr__`` so that
5
5
  leaf module backing the requested symbol — not every sibling. All AI capabilities
6
6
  ship in the single ``[ai]`` extra; the laziness keeps ``import videopython`` (and
7
7
  importing one leaf class) light by deferring the heavy ML imports (torch /
8
- transformers / diffusers / 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,8 +59,16 @@ MODEL_REVISIONS: dict[str, str] = {
67
59
  "pyannote/speaker-diarization-community-1": "3533c8cf8e369892e6b79ff1bf80f7b0286a54ee",
68
60
  # Audio event classifier (ai/understanding/classification.py: AudioClassifier)
69
61
  "MIT/ast-finetuned-audioset-10-10-0.4593": "f826b80d28226b62986cc218e5cec390b1096902",
70
- # 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
74
  # SDXL (ai/generation/image.py: TextToImage)
@@ -11,16 +11,17 @@ from .backend import PlannerError
11
11
  if TYPE_CHECKING:
12
12
  import numpy as np
13
13
 
14
- DEFAULT_OLLAMA_MODEL = "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
@@ -229,6 +229,43 @@ class LocalDubbingPipeline:
229
229
  """Initialize the timing synchronizer."""
230
230
  self._synchronizer = TimingSynchronizer()
231
231
 
232
+ def _separate(
233
+ self,
234
+ source_audio: Audio,
235
+ transcription: Transcription,
236
+ progress_fraction: float,
237
+ report_progress: Callable[[str, float], None],
238
+ ) -> tuple[SeparatedAudio | None, Audio, Audio | None]:
239
+ """Demucs source separation over the speech regions; shared by process()/revoice().
240
+
241
+ Limits Demucs to the speech-bearing portion of the audio: the transcription
242
+ has already located every speech region, and separating outside those is pure
243
+ overhead (no vocals to isolate). On talk-heavy sources with silence/music gaps
244
+ this roughly halves separation time; when speech covers most of the track,
245
+ separate_regions falls back to a full-track separate().
246
+
247
+ Returns ``(separated_audio, vocal_audio, background_audio)``. In low_memory
248
+ mode the ``SeparatedAudio`` container is dropped (returned as ``None``) so
249
+ vocals and background release as soon as their last local reference goes.
250
+ """
251
+ report_progress("Separating audio", progress_fraction)
252
+ if self._separator is None:
253
+ self._init_separator()
254
+
255
+ from videopython.ai.dubbing.separation import merge_regions
256
+
257
+ speech_regions = merge_regions(
258
+ [(s.start, s.end) for s in transcription.segments],
259
+ audio_duration=source_audio.metadata.duration_seconds,
260
+ )
261
+ separated_audio = self._separator.separate_regions(source_audio, speech_regions)
262
+ self._maybe_unload("_separator")
263
+ vocal_audio = separated_audio.vocals
264
+ background_audio = separated_audio.background
265
+ if self.config.low_memory:
266
+ separated_audio = None
267
+ return separated_audio, vocal_audio, background_audio
268
+
232
269
  def _finalise_audio(
233
270
  self,
234
271
  dubbed_speech: Audio,
@@ -355,32 +392,9 @@ class LocalDubbingPipeline:
355
392
  background_audio: Audio | None = None
356
393
 
357
394
  if preserve_background:
358
- report_progress("Separating audio", 0.15)
359
- if self._separator is None:
360
- self._init_separator()
361
-
362
- # Limit Demucs to the speech-bearing portion of the audio. The
363
- # transcription has already located every speech region; running
364
- # source separation outside those is pure overhead (no vocals to
365
- # isolate). On talk-heavy sources with silence/music gaps this
366
- # roughly halves separation time. When speech covers most of the
367
- # track separate_regions falls back to a full-track separate().
368
- from videopython.ai.dubbing.separation import merge_regions
369
-
370
- speech_regions = merge_regions(
371
- [(s.start, s.end) for s in transcription.segments],
372
- audio_duration=source_audio.metadata.duration_seconds,
395
+ separated_audio, vocal_audio, background_audio = self._separate(
396
+ source_audio, transcription, 0.15, report_progress
373
397
  )
374
- separated_audio = self._separator.separate_regions(source_audio, speech_regions)
375
- self._maybe_unload("_separator")
376
- vocal_audio = separated_audio.vocals
377
- background_audio = separated_audio.background
378
- # In low_memory mode, drop the SeparatedAudio container so vocals
379
- # and background can be released as soon as their last local
380
- # reference goes (after voice-sample extraction and final overlay
381
- # respectively). The result will report separated_audio=None.
382
- if self.config.low_memory:
383
- separated_audio = None
384
398
 
385
399
  voice_samples: dict[str, Audio] = {}
386
400
  if voice_clone:
@@ -527,22 +541,9 @@ class LocalDubbingPipeline:
527
541
  background_audio: Audio | None = None
528
542
 
529
543
  if preserve_background:
530
- report_progress("Separating audio", 0.20)
531
- if self._separator is None:
532
- self._init_separator()
533
-
534
- from videopython.ai.dubbing.separation import merge_regions
535
-
536
- speech_regions = merge_regions(
537
- [(s.start, s.end) for s in transcription.segments],
538
- audio_duration=source_audio.metadata.duration_seconds,
544
+ separated_audio, vocal_audio, background_audio = self._separate(
545
+ source_audio, transcription, 0.20, report_progress
539
546
  )
540
- separated_audio = self._separator.separate_regions(source_audio, speech_regions)
541
- self._maybe_unload("_separator")
542
- vocal_audio = separated_audio.vocals
543
- background_audio = separated_audio.background
544
- if self.config.low_memory:
545
- separated_audio = None
546
547
 
547
548
  report_progress("Extracting voice sample", 0.40)
548
549
  chosen_sample: Audio | None = None
@@ -59,14 +59,9 @@ def merge_regions(
59
59
  class AudioSeparator(ManagedPredictor):
60
60
  """Separates audio into vocals and background components using Demucs."""
61
61
 
62
- SUPPORTED_MODELS: list[str] = ["htdemucs", "htdemucs_ft", "htdemucs_6s", "mdx_extra"]
63
62
  STEM_NAMES = ["drums", "bass", "other", "vocals"]
64
- STEM_NAMES_6S = ["drums", "bass", "other", "vocals", "guitar", "piano"]
65
63
 
66
64
  def __init__(self, model_name: str = "htdemucs", device: str | None = None):
67
- if model_name not in self.SUPPORTED_MODELS:
68
- raise ValueError(f"Model '{model_name}' not supported. Supported: {self.SUPPORTED_MODELS}")
69
-
70
65
  self.model_name = model_name
71
66
  self.device = device
72
67
  self._model: Any = None
@@ -129,7 +124,7 @@ class AudioSeparator(ManagedPredictor):
129
124
  sources_np = sources[0].cpu().numpy()
130
125
  del sources
131
126
 
132
- stem_names = self.STEM_NAMES_6S if self.model_name == "htdemucs_6s" else self.STEM_NAMES
127
+ stem_names = self.STEM_NAMES
133
128
  vocals_idx = stem_names.index("vocals")
134
129
  non_vocal_indices = [i for i in range(len(stem_names)) if i != vocals_idx]
135
130
 
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
24
24
 
25
25
  # Default Ollama text model for translation; override via the `model` arg (and
26
26
  # `ollama pull` it first). Any instruct model that supports structured output works.
27
- DEFAULT_TRANSLATION_MODEL = "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,
@@ -74,8 +74,6 @@ class FaceTrackingCrop(Operation):
74
74
  ),
75
75
  )
76
76
  detection_interval: int = Field(3, ge=1, description="Frames between face detections.")
77
- backend: Literal["cpu", "gpu", "auto"] = Field("auto", description='Detection backend - "cpu", "gpu", or "auto".')
78
- sample_rate: int = Field(1, ge=1, description="For GPU backend, detect every Nth frame and interpolate.")
79
77
 
80
78
  def _apply_framing_offset(self, face_cx: float, face_cy: float, face_h: float) -> tuple[float, float]:
81
79
  if self.framing_rule == "offset":
@@ -139,8 +137,6 @@ class FaceTrackingCrop(Operation):
139
137
  face_index=self.face_index,
140
138
  smoothing=self.smoothing,
141
139
  detection_interval=self.detection_interval,
142
- backend=self.backend,
143
- sample_rate=self.sample_rate,
144
140
  )
145
141
  default = ((frame_w - out_w) // 2, (frame_h - out_h) // 2)
146
142
  last = default
@@ -1,21 +1,21 @@
1
- """Shared lazy YOLOv8 detector wrapper for the understanding layer.
2
-
3
- ``ObjectDetector`` (COCO objects) and ``_FaceDetector`` (faces) were nearly
4
- identical YOLO wrappers: same ``cpu``/``gpu``/``auto`` device resolution, same
5
- lazy load + ``.to("cuda")``, same ``detect`` / ``detect_batch`` shape, differing
6
- only in how the model is loaded and how a result is parsed into detections.
7
- :class:`YoloDetector` holds all the shared machinery; subclasses supply just
8
- ``_load_model`` (build ``self._yolo_model``), ``_parse`` (one result -> list of
9
- detections), and ``_conf`` (the confidence passed to YOLO).
10
-
11
- The heavy ``ultralytics`` import is deferred to :meth:`_build_yolo` (guarded by
12
- ``require``), so constructing a detector pulls in no torch until it actually runs.
1
+ """Shared lazy detector base for the understanding layer.
2
+
3
+ ``ObjectDetector`` (COCO objects, D-FINE via transformers) and ``_FaceDetector``
4
+ (faces, OpenCV YuNet) share the same machinery: ``cpu``/``gpu``/``auto`` device
5
+ resolution, lazy load, the ``detect`` / ``detect_batch`` shape, and ``unload``.
6
+ :class:`DetectorBase` holds that machinery; subclasses supply just ``_load_model``
7
+ (populate the ``_model_attrs`` fields) and ``_infer`` (map a batch of frames to a
8
+ list of per-frame detections).
9
+
10
+ The two backends are completely different (transformers vs OpenCV DNN), so the
11
+ base imports no model library itself; each subclass defers its heavy import into
12
+ ``_load_model`` (guarded by ``require`` where the dep lives in the ``[ai]`` extra).
13
13
  """
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
17
  import logging
18
- from typing import Any, Generic, Literal, TypeVar
18
+ from typing import Generic, Literal, TypeVar
19
19
 
20
20
  import numpy as np
21
21
 
@@ -28,26 +28,25 @@ T = TypeVar("T")
28
28
  Backend = Literal["cpu", "gpu", "auto"]
29
29
 
30
30
 
31
- class YoloDetector(ManagedPredictor, Generic[T]):
32
- """Lazy YOLOv8 wrapper: device resolution, lazy load, detect/detect_batch, unload.
31
+ class DetectorBase(ManagedPredictor, Generic[T]):
32
+ """Lazy detector base: device resolution, lazy load, detect/detect_batch, unload.
33
33
 
34
34
  Context-managed via :class:`ManagedPredictor`; ``unload()`` drops the model
35
- and frees the resolved device. Subclass contract:
35
+ reference(s) named in ``_model_attrs`` and frees the resolved device. Subclass
36
+ contract:
36
37
  * ``_FEATURE`` -- name surfaced in the ``[ai]``-extra ``ImportError``.
37
- * ``_load_model()`` -- set ``self._yolo_model`` (call :meth:`_build_yolo`)
38
- plus any label state.
39
- * ``_parse(result)`` -- map one ultralytics result to a list of detections.
40
- * ``_conf()`` -- the ``conf`` threshold passed to the model.
38
+ * ``_model_attrs`` -- attribute(s) holding loaded model state; the first one
39
+ doubles as the "is loaded" sentinel.
40
+ * ``_load_model()`` -- populate the ``_model_attrs`` fields plus any label state.
41
+ * ``_infer(images)`` -- map a list of frames to a list of per-frame detections.
41
42
  """
42
43
 
43
- _FEATURE = "YoloDetector"
44
- _model_attrs = ("_yolo_model",)
44
+ _FEATURE = "detector"
45
45
  _device_attr = "_resolved_device"
46
46
 
47
47
  def __init__(self, *, backend: Backend = "auto") -> None:
48
48
  self.backend: Backend = backend
49
49
  self._resolved_device: Literal["cpu", "cuda"] | None = None
50
- self._yolo_model: Any = None
51
50
 
52
51
  def _resolve_device(self) -> Literal["cpu", "cuda"]:
53
52
  if self._resolved_device is not None:
@@ -67,37 +66,23 @@ class YoloDetector(ManagedPredictor, Generic[T]):
67
66
  """Resolved execution device for this detector."""
68
67
  return self._resolve_device()
69
68
 
70
- def _build_yolo(self, model_path: str) -> None:
71
- """Construct ``self._yolo_model`` from a model id/path and move it to the device."""
72
- from videopython.ai._optional import require
73
-
74
- YOLO = require("ultralytics", feature=self._FEATURE).YOLO
75
- self._yolo_model = YOLO(model_path)
76
- if self._resolve_device() == "cuda":
77
- self._yolo_model.to("cuda")
78
-
79
69
  # --- subclass hooks -----------------------------------------------------
80
70
  def _load_model(self) -> None:
81
71
  raise NotImplementedError
82
72
 
83
- def _conf(self) -> float:
84
- raise NotImplementedError
85
-
86
- def _parse(self, result: Any) -> list[T]:
73
+ def _infer(self, images: list[np.ndarray]) -> list[list[T]]:
87
74
  raise NotImplementedError
88
75
 
89
76
  # --- public API ---------------------------------------------------------
90
77
  def _ensure_loaded(self) -> None:
91
- if self._yolo_model is None:
78
+ if getattr(self, self._model_attrs[0], None) is None:
92
79
  self._load_model()
93
80
 
94
81
  def detect(self, image: np.ndarray) -> list[T]:
95
82
  """Detect in a single ``(H, W, 3)`` frame."""
96
83
  self._ensure_loaded()
97
- results = self._yolo_model(image, conf=self._conf(), verbose=False)
98
- if not results:
99
- return []
100
- return self._parse(results[0])
84
+ results = self._infer([image])
85
+ return results[0] if results else []
101
86
 
102
87
  def detect_batch(self, images: list[np.ndarray] | np.ndarray) -> list[list[T]]:
103
88
  """Detect in a batch of frames (list or stacked ``(N, H, W, 3)``)."""
@@ -107,5 +92,4 @@ class YoloDetector(ManagedPredictor, Generic[T]):
107
92
  return []
108
93
 
109
94
  self._ensure_loaded()
110
- results = self._yolo_model(images, conf=self._conf(), verbose=False)
111
- return [self._parse(result) for result in results]
95
+ return self._infer(images)
@@ -23,7 +23,6 @@ class AudioClassifier(ManagedPredictor):
23
23
  """Audio event and sound classification using AST."""
24
24
 
25
25
  _model_attrs = ("_model", "_processor")
26
- SUPPORTED_MODELS: list[str] = ["MIT/ast-finetuned-audioset-10-10-0.4593"]
27
26
  AST_SAMPLE_RATE: int = 16000
28
27
  AST_CHUNK_SECONDS: float = 10.0
29
28
  AST_HOP_SECONDS: float = 5.0
@@ -35,9 +34,6 @@ class AudioClassifier(ManagedPredictor):
35
34
  top_k: int = 10,
36
35
  device: str | None = None,
37
36
  ):
38
- if model_name not in self.SUPPORTED_MODELS:
39
- raise ValueError(f"Model '{model_name}' not supported. Supported: {self.SUPPORTED_MODELS}")
40
-
41
37
  self.model_name = model_name
42
38
  self.confidence_threshold = confidence_threshold
43
39
  self.top_k = top_k