videopython 0.26.4__tar.gz → 0.26.5__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 (56) hide show
  1. {videopython-0.26.4 → videopython-0.26.5}/PKG-INFO +1 -1
  2. {videopython-0.26.4 → videopython-0.26.5}/pyproject.toml +1 -1
  3. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/dubber.py +23 -3
  4. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/pipeline.py +64 -19
  5. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/understanding/separation.py +27 -40
  6. {videopython-0.26.4 → videopython-0.26.5}/.gitignore +0 -0
  7. {videopython-0.26.4 → videopython-0.26.5}/LICENSE +0 -0
  8. {videopython-0.26.4 → videopython-0.26.5}/README.md +0 -0
  9. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/__init__.py +0 -0
  10. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/__init__.py +0 -0
  11. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/_device.py +0 -0
  12. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/__init__.py +0 -0
  13. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/models.py +0 -0
  14. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/remux.py +0 -0
  15. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/dubbing/timing.py +0 -0
  16. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/generation/__init__.py +0 -0
  17. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/generation/audio.py +0 -0
  18. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/generation/image.py +0 -0
  19. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/generation/translation.py +0 -0
  20. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/generation/video.py +0 -0
  21. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/registry.py +0 -0
  22. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/swapping/__init__.py +0 -0
  23. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/swapping/inpainter.py +0 -0
  24. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/swapping/models.py +0 -0
  25. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/swapping/segmenter.py +0 -0
  26. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/swapping/swapper.py +0 -0
  27. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/transforms.py +0 -0
  28. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/understanding/__init__.py +0 -0
  29. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/understanding/audio.py +0 -0
  30. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/understanding/image.py +0 -0
  31. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/understanding/temporal.py +0 -0
  32. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/ai/video_analysis.py +0 -0
  33. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/__init__.py +0 -0
  34. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/audio/__init__.py +0 -0
  35. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/audio/analysis.py +0 -0
  36. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/audio/audio.py +0 -0
  37. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/combine.py +0 -0
  38. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/description.py +0 -0
  39. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/effects.py +0 -0
  40. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/exceptions.py +0 -0
  41. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/progress.py +0 -0
  42. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/registry.py +0 -0
  43. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/scene.py +0 -0
  44. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/streaming.py +0 -0
  45. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/text/__init__.py +0 -0
  46. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/text/overlay.py +0 -0
  47. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/text/transcription.py +0 -0
  48. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/transforms.py +0 -0
  49. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/transitions.py +0 -0
  50. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/utils.py +0 -0
  51. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/base/video.py +0 -0
  52. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/editing/__init__.py +0 -0
  53. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/editing/multicam.py +0 -0
  54. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/editing/premiere_xml.py +0 -0
  55. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/editing/video_edit.py +0 -0
  56. {videopython-0.26.4 → videopython-0.26.5}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.26.4
3
+ Version: 0.26.5
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "videopython"
3
- version = "0.26.4"
3
+ version = "0.26.5"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Any, Callable
9
9
 
10
10
  from videopython.ai.dubbing.models import DubbingResult, RevoiceResult
11
+ from videopython.ai.dubbing.pipeline import WhisperModel
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from videopython.base.video import Video
@@ -25,19 +26,38 @@ class VideoDubber:
25
26
  model is resident at a time. Trades per-run latency (~10-30s of
26
27
  extra model loads) for a much lower memory ceiling. Recommended for
27
28
  GPUs with <=12GB VRAM or hosts with <32GB RAM. Default False.
29
+ whisper_model: Whisper model size used for transcription. Larger models
30
+ give better accuracy at the cost of VRAM and latency. One of
31
+ ``tiny``, ``base``, ``small``, ``medium``, ``large``, ``turbo``.
32
+ Default ``small``.
28
33
  """
29
34
 
30
- def __init__(self, device: str | None = None, low_memory: bool = False):
35
+ def __init__(
36
+ self,
37
+ device: str | None = None,
38
+ low_memory: bool = False,
39
+ whisper_model: WhisperModel = "small",
40
+ ):
31
41
  self.device = device
32
42
  self.low_memory = low_memory
43
+ self.whisper_model = whisper_model
33
44
  self._local_pipeline: Any = None
34
45
  requested = device.lower() if isinstance(device, str) else "auto"
35
- logger.info("VideoDubber initialized with device=%s low_memory=%s", requested, low_memory)
46
+ logger.info(
47
+ "VideoDubber initialized with device=%s low_memory=%s whisper_model=%s",
48
+ requested,
49
+ low_memory,
50
+ whisper_model,
51
+ )
36
52
 
37
53
  def _init_local_pipeline(self) -> None:
38
54
  from videopython.ai.dubbing.pipeline import LocalDubbingPipeline
39
55
 
40
- self._local_pipeline = LocalDubbingPipeline(device=self.device, low_memory=self.low_memory)
56
+ self._local_pipeline = LocalDubbingPipeline(
57
+ device=self.device,
58
+ low_memory=self.low_memory,
59
+ whisper_model=self.whisper_model,
60
+ )
41
61
 
42
62
  def dub(
43
63
  self,
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from typing import TYPE_CHECKING, Any, Callable
6
+ from typing import TYPE_CHECKING, Any, Callable, Literal
7
7
 
8
8
  from videopython.ai.dubbing.models import DubbingResult, RevoiceResult, SeparatedAudio
9
9
  from videopython.ai.dubbing.timing import TimingSynchronizer
@@ -11,6 +11,8 @@ from videopython.ai.dubbing.timing import TimingSynchronizer
11
11
  if TYPE_CHECKING:
12
12
  from videopython.base.audio import Audio
13
13
 
14
+ WhisperModel = Literal["tiny", "base", "small", "medium", "large", "turbo"]
15
+
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
 
@@ -23,14 +25,21 @@ class LocalDubbingPipeline:
23
25
  with <=12GB VRAM or hosts with <32GB RAM.
24
26
  """
25
27
 
26
- def __init__(self, device: str | None = None, low_memory: bool = False):
28
+ def __init__(
29
+ self,
30
+ device: str | None = None,
31
+ low_memory: bool = False,
32
+ whisper_model: WhisperModel = "small",
33
+ ):
27
34
  self.device = device
28
35
  self.low_memory = low_memory
36
+ self.whisper_model = whisper_model
29
37
  requested = device.lower() if isinstance(device, str) else "auto"
30
38
  logger.info(
31
- "LocalDubbingPipeline initialized with device=%s low_memory=%s",
39
+ "LocalDubbingPipeline initialized with device=%s low_memory=%s whisper_model=%s",
32
40
  requested,
33
41
  low_memory,
42
+ whisper_model,
34
43
  )
35
44
 
36
45
  self._transcriber: Any = None
@@ -62,7 +71,11 @@ class LocalDubbingPipeline:
62
71
  """Initialize the transcription model."""
63
72
  from videopython.ai.understanding.audio import AudioToText
64
73
 
65
- self._transcriber = AudioToText(device=self.device, enable_diarization=enable_diarization)
74
+ self._transcriber = AudioToText(
75
+ model_name=self.whisper_model,
76
+ device=self.device,
77
+ enable_diarization=enable_diarization,
78
+ )
66
79
 
67
80
  def _init_translator(self) -> None:
68
81
  """Initialize the translation model."""
@@ -94,6 +107,7 @@ class LocalDubbingPipeline:
94
107
  max_duration: float = 10.0,
95
108
  ) -> dict[str, Any]:
96
109
  """Extract voice samples for each speaker from the audio."""
110
+ from videopython.base.audio import Audio
97
111
 
98
112
  voice_samples: dict[str, Audio] = {}
99
113
 
@@ -120,7 +134,11 @@ class LocalDubbingPipeline:
120
134
  if best_segment is not None:
121
135
  start = best_segment.start
122
136
  end = min(best_segment.end, start + max_duration)
123
- voice_samples[speaker] = audio.slice(start, end)
137
+ sliced = audio.slice(start, end)
138
+ # Audio.slice returns a numpy view into the source. Copy so the
139
+ # short voice sample doesn't keep the full vocals array (~1.3 GB
140
+ # for 2h sources) alive across translate + TTS.
141
+ voice_samples[speaker] = Audio(sliced.data.copy(), sliced.metadata)
124
142
 
125
143
  return voice_samples
126
144
 
@@ -175,6 +193,7 @@ class LocalDubbingPipeline:
175
193
 
176
194
  separated_audio: SeparatedAudio | None = None
177
195
  vocal_audio = source_audio
196
+ background_audio: Audio | None = None
178
197
 
179
198
  if preserve_background:
180
199
  report_progress("Separating audio", 0.15)
@@ -184,12 +203,24 @@ class LocalDubbingPipeline:
184
203
  separated_audio = self._separator.separate(source_audio)
185
204
  self._maybe_unload("_separator")
186
205
  vocal_audio = separated_audio.vocals
206
+ background_audio = separated_audio.background
207
+ # In low_memory mode, drop the SeparatedAudio container so vocals
208
+ # and background can be released as soon as their last local
209
+ # reference goes (after voice-sample extraction and final overlay
210
+ # respectively). The result will report separated_audio=None.
211
+ if self.low_memory:
212
+ separated_audio = None
187
213
 
188
214
  voice_samples: dict[str, Audio] = {}
189
215
  if voice_clone:
190
216
  report_progress("Extracting voice samples", 0.25)
191
217
  voice_samples = self._extract_voice_samples(vocal_audio, transcription)
192
218
 
219
+ # vocals is no longer needed; voice_samples are independent copies.
220
+ # In low_memory mode this is the only ref keeping the buffer alive
221
+ # (separated_audio was dropped above), so dropping the local frees it.
222
+ del vocal_audio
223
+
193
224
  report_progress("Translating text", 0.35)
194
225
  if self._translator is None:
195
226
  self._init_translator()
@@ -237,17 +268,23 @@ class LocalDubbingPipeline:
237
268
  assert self._synchronizer is not None
238
269
 
239
270
  synchronized_segments, _ = self._synchronizer.synchronize_segments(dubbed_segments, target_durations)
271
+ del dubbed_segments
240
272
 
241
273
  report_progress("Assembling final audio", 0.90)
242
274
  total_duration = source_audio.metadata.duration_seconds
243
275
  dubbed_speech = self._synchronizer.assemble_with_timing(synchronized_segments, start_times, total_duration)
276
+ del synchronized_segments
244
277
 
245
- if separated_audio is not None:
246
- background_sr = separated_audio.background.metadata.sample_rate
278
+ if background_audio is not None:
279
+ background_sr = background_audio.metadata.sample_rate
247
280
  if dubbed_speech.metadata.sample_rate != background_sr:
248
281
  dubbed_speech = dubbed_speech.resample(background_sr)
249
282
 
250
- final_audio = separated_audio.background.overlay(dubbed_speech, position=0.0)
283
+ final_audio = background_audio.overlay(dubbed_speech, position=0.0)
284
+ # Drop the local; in low_memory this releases the background
285
+ # buffer (~1.3 GB for 2h sources). In non-low_memory the same
286
+ # array is still held by separated_audio.background.
287
+ del background_audio
251
288
  else:
252
289
  final_audio = dubbed_speech
253
290
 
@@ -294,6 +331,7 @@ class LocalDubbingPipeline:
294
331
 
295
332
  separated_audio: SeparatedAudio | None = None
296
333
  vocal_audio = source_audio
334
+ background_audio: Audio | None = None
297
335
 
298
336
  if preserve_background:
299
337
  report_progress("Separating audio", 0.20)
@@ -303,6 +341,9 @@ class LocalDubbingPipeline:
303
341
  separated_audio = self._separator.separate(source_audio)
304
342
  self._maybe_unload("_separator")
305
343
  vocal_audio = separated_audio.vocals
344
+ background_audio = separated_audio.background
345
+ if self.low_memory:
346
+ separated_audio = None
306
347
 
307
348
  report_progress("Extracting voice sample", 0.40)
308
349
  voice_sample: Audio | None = None
@@ -314,7 +355,11 @@ class LocalDubbingPipeline:
314
355
 
315
356
  if voice_sample is None:
316
357
  sample_duration = min(6.0, original_duration)
317
- voice_sample = vocal_audio.slice(0, sample_duration)
358
+ sliced = vocal_audio.slice(0, sample_duration)
359
+ # Copy so the short sample doesn't pin the full vocals array.
360
+ voice_sample = Audio(sliced.data.copy(), sliced.metadata)
361
+
362
+ del vocal_audio
318
363
 
319
364
  report_progress("Generating speech", 0.60)
320
365
  if self._tts is None or self._tts_language != "en":
@@ -327,24 +372,24 @@ class LocalDubbingPipeline:
327
372
 
328
373
  report_progress("Assembling audio", 0.85)
329
374
 
330
- if separated_audio is not None:
331
- background_sr = separated_audio.background.metadata.sample_rate
375
+ if background_audio is not None:
376
+ background_sr = background_audio.metadata.sample_rate
332
377
  if generated_speech.metadata.sample_rate != background_sr:
333
378
  generated_speech = generated_speech.resample(background_sr)
334
379
 
335
- background = separated_audio.background
336
- if background.metadata.duration_seconds > speech_duration:
337
- background = background.slice(0, speech_duration)
338
- elif background.metadata.duration_seconds < speech_duration:
339
- silence_duration = speech_duration - background.metadata.duration_seconds
380
+ if background_audio.metadata.duration_seconds > speech_duration:
381
+ background_audio = background_audio.slice(0, speech_duration)
382
+ elif background_audio.metadata.duration_seconds < speech_duration:
383
+ silence_duration = speech_duration - background_audio.metadata.duration_seconds
340
384
  silence = Audio.silence(
341
385
  duration=silence_duration,
342
386
  sample_rate=background_sr,
343
- channels=background.metadata.channels,
387
+ channels=background_audio.metadata.channels,
344
388
  )
345
- background = background.concat(silence)
389
+ background_audio = background_audio.concat(silence)
346
390
 
347
- final_audio = background.overlay(generated_speech, position=0.0)
391
+ final_audio = background_audio.overlay(generated_speech, position=0.0)
392
+ del background_audio
348
393
  else:
349
394
  final_audio = generated_speech
350
395
 
@@ -42,7 +42,15 @@ class AudioSeparator:
42
42
  )
43
43
 
44
44
  def _separate_local(self, audio: Audio) -> SeparatedAudio:
45
- """Separate audio using local Demucs model."""
45
+ """Separate audio using local Demucs model.
46
+
47
+ Keeps the input tensor on CPU and passes ``device=self.device`` to
48
+ ``apply_model`` so per-chunk compute runs on GPU while the full
49
+ ``(stems, channels, samples)`` output is stored in CPU RAM. For long
50
+ sources this is the difference between OOM-on-GPU and running cleanly:
51
+ a 2h stereo @ 44.1kHz output is ~10 GB — too big for an 8 GB card but
52
+ comfortable on a 32 GB host.
53
+ """
46
54
  import numpy as np
47
55
  import torch
48
56
  from demucs.apply import apply_model
@@ -65,61 +73,40 @@ class AudioSeparator:
65
73
  audio_data = audio_data.T
66
74
 
67
75
  wav = torch.tensor(audio_data, dtype=torch.float32).unsqueeze(0)
68
- wav = wav.to(self.device)
69
76
 
70
77
  with torch.no_grad():
71
78
  sources = apply_model(self._model, wav, device=self.device)
72
79
 
73
80
  sources_np = sources[0].cpu().numpy()
81
+ del sources
74
82
 
75
83
  stem_names = self.STEM_NAMES_6S if self.model_name == "htdemucs_6s" else self.STEM_NAMES
84
+ vocals_idx = stem_names.index("vocals")
85
+ non_vocal_indices = [i for i in range(len(stem_names)) if i != vocals_idx]
76
86
 
77
- stems: dict[str, Audio] = {}
78
- for i, name in enumerate(stem_names):
79
- stem_data = sources_np[i].T
80
-
81
- metadata = AudioMetadata(
82
- sample_rate=target_sr,
83
- channels=2,
84
- sample_width=2,
85
- duration_seconds=stem_data.shape[0] / target_sr,
86
- frame_count=stem_data.shape[0],
87
- )
88
- stems[name] = Audio(stem_data.astype(np.float32), metadata)
89
-
90
- vocals = stems["vocals"]
91
-
92
- non_vocal_stems = [stems[name] for name in stem_names if name != "vocals"]
93
- background_data = np.zeros_like(vocals.data)
94
- for stem in non_vocal_stems:
95
- background_data += stem.data
87
+ vocals_data = sources_np[vocals_idx].T
88
+ background_data = sources_np[non_vocal_indices].sum(axis=0).T
89
+ del sources_np
96
90
 
97
91
  max_val = np.max(np.abs(background_data))
98
92
  if max_val > 1.0:
99
- background_data = background_data / max_val
100
-
101
- background = Audio(background_data.astype(np.float32), vocals.metadata)
102
-
103
- music_stems = ["drums", "bass", "other"]
104
- if self.model_name == "htdemucs_6s":
105
- music_stems.extend(["guitar", "piano"])
106
-
107
- music_data = np.zeros_like(vocals.data)
108
- for name in music_stems:
109
- if name in stems:
110
- music_data += stems[name].data
111
-
112
- max_val = np.max(np.abs(music_data))
113
- if max_val > 1.0:
114
- music_data = music_data / max_val
115
-
116
- music = Audio(music_data.astype(np.float32), vocals.metadata)
93
+ background_data /= max_val
94
+
95
+ metadata = AudioMetadata(
96
+ sample_rate=target_sr,
97
+ channels=2,
98
+ sample_width=2,
99
+ duration_seconds=vocals_data.shape[0] / target_sr,
100
+ frame_count=vocals_data.shape[0],
101
+ )
102
+ vocals = Audio(np.ascontiguousarray(vocals_data, dtype=np.float32), metadata)
103
+ background = Audio(np.ascontiguousarray(background_data, dtype=np.float32), metadata)
117
104
 
118
105
  return SeparatedAudio(
119
106
  vocals=vocals,
120
107
  background=background,
121
108
  original=audio,
122
- music=music,
109
+ music=None,
123
110
  effects=None,
124
111
  )
125
112
 
File without changes
File without changes
File without changes