videopython 0.26.2__tar.gz → 0.26.3__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.2 → videopython-0.26.3}/PKG-INFO +1 -1
  2. {videopython-0.26.2 → videopython-0.26.3}/pyproject.toml +1 -1
  3. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/dubber.py +82 -2
  4. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/pipeline.py +13 -10
  5. videopython-0.26.3/src/videopython/ai/dubbing/remux.py +73 -0
  6. {videopython-0.26.2 → videopython-0.26.3}/.gitignore +0 -0
  7. {videopython-0.26.2 → videopython-0.26.3}/LICENSE +0 -0
  8. {videopython-0.26.2 → videopython-0.26.3}/README.md +0 -0
  9. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/__init__.py +0 -0
  10. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/__init__.py +0 -0
  11. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/_device.py +0 -0
  12. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/__init__.py +0 -0
  13. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/models.py +0 -0
  14. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/timing.py +0 -0
  15. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/__init__.py +0 -0
  16. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/audio.py +0 -0
  17. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/image.py +0 -0
  18. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/translation.py +0 -0
  19. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/video.py +0 -0
  20. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/registry.py +0 -0
  21. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/__init__.py +0 -0
  22. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/inpainter.py +0 -0
  23. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/models.py +0 -0
  24. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/segmenter.py +0 -0
  25. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/swapper.py +0 -0
  26. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/transforms.py +0 -0
  27. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/__init__.py +0 -0
  28. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/audio.py +0 -0
  29. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/image.py +0 -0
  30. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/separation.py +0 -0
  31. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/temporal.py +0 -0
  32. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/video_analysis.py +0 -0
  33. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/__init__.py +0 -0
  34. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/__init__.py +0 -0
  35. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/analysis.py +0 -0
  36. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/audio.py +0 -0
  37. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/combine.py +0 -0
  38. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/description.py +0 -0
  39. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/effects.py +0 -0
  40. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/exceptions.py +0 -0
  41. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/progress.py +0 -0
  42. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/registry.py +0 -0
  43. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/scene.py +0 -0
  44. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/streaming.py +0 -0
  45. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/__init__.py +0 -0
  46. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/overlay.py +0 -0
  47. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/transcription.py +0 -0
  48. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/transforms.py +0 -0
  49. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/transitions.py +0 -0
  50. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/utils.py +0 -0
  51. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/video.py +0 -0
  52. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/__init__.py +0 -0
  53. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/multicam.py +0 -0
  54. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/premiere_xml.py +0 -0
  55. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/video_edit.py +0 -0
  56. {videopython-0.26.2 → videopython-0.26.3}/src/videopython/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: videopython
3
- Version: 0.26.2
3
+ Version: 0.26.3
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.2"
3
+ version = "0.26.3"
4
4
  description = "Minimal video generation and processing library."
5
5
  authors = [
6
6
  { name = "Bartosz Wójtowicz", email = "bartoszwojtowicz@outlook.com" },
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import tempfile
7
+ from pathlib import Path
6
8
  from typing import TYPE_CHECKING, Any, Callable
7
9
 
8
10
  from videopython.ai.dubbing.models import DubbingResult, RevoiceResult
@@ -60,7 +62,7 @@ class VideoDubber:
60
62
  self._init_local_pipeline()
61
63
 
62
64
  return self._local_pipeline.process(
63
- video=video,
65
+ source_audio=video.audio,
64
66
  target_lang=target_lang,
65
67
  source_lang=source_lang,
66
68
  preserve_background=preserve_background,
@@ -99,6 +101,84 @@ class VideoDubber:
99
101
  )
100
102
  return video.add_audio(result.dubbed_audio, overlay=False)
101
103
 
104
+ def dub_file(
105
+ self,
106
+ input_path: str | Path,
107
+ output_path: str | Path,
108
+ target_lang: str,
109
+ source_lang: str | None = None,
110
+ preserve_background: bool = True,
111
+ voice_clone: bool = True,
112
+ enable_diarization: bool = False,
113
+ progress_callback: Callable[[str, float], None] | None = None,
114
+ transcription: Any = None,
115
+ ) -> DubbingResult:
116
+ """Dub a video file in place on disk without loading video frames into memory.
117
+
118
+ Extracts the audio track via ffmpeg, runs the dubbing pipeline on the
119
+ audio only, then muxes the dubbed audio back into the source video
120
+ using ffmpeg stream-copy (no video re-encode). Peak memory is bounded
121
+ by model weights and the audio track — independent of video length and
122
+ resolution.
123
+
124
+ Use this instead of ``dub_and_replace`` when the source video is long
125
+ or high-resolution and you don't need frame-level access in Python.
126
+
127
+ Args:
128
+ input_path: Path to the source video file.
129
+ output_path: Path to write the dubbed video. Overwritten if it exists.
130
+ target_lang: Target language code (e.g. ``"es"``, ``"fr"``).
131
+ source_lang: Source language code, or ``None`` to auto-detect.
132
+ preserve_background: Preserve background music/effects via source separation.
133
+ voice_clone: Clone the source speaker's voice for the dubbed track.
134
+ enable_diarization: Enable speaker diarization for per-speaker voice cloning.
135
+ progress_callback: Optional callback ``(stage: str, progress: float) -> None``.
136
+ transcription: Optional pre-computed ``Transcription`` to skip the Whisper step.
137
+
138
+ Returns:
139
+ ``DubbingResult`` with the dubbed audio, translated segments, and
140
+ source transcription. The output video is written to ``output_path``.
141
+ """
142
+ from videopython.ai.dubbing.remux import replace_audio_stream
143
+ from videopython.base.audio import Audio
144
+
145
+ input_path = Path(input_path)
146
+ output_path = Path(output_path)
147
+
148
+ if not input_path.exists():
149
+ raise FileNotFoundError(f"Input video not found: {input_path}")
150
+
151
+ logger.info("dub_file: loading audio from %s", input_path)
152
+ source_audio = Audio.from_path(input_path)
153
+
154
+ if self._local_pipeline is None:
155
+ self._init_local_pipeline()
156
+
157
+ result = self._local_pipeline.process(
158
+ source_audio=source_audio,
159
+ target_lang=target_lang,
160
+ source_lang=source_lang,
161
+ preserve_background=preserve_background,
162
+ voice_clone=voice_clone,
163
+ enable_diarization=enable_diarization,
164
+ progress_callback=progress_callback,
165
+ transcription=transcription,
166
+ )
167
+
168
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
169
+ dubbed_audio_path = Path(tmp.name)
170
+ try:
171
+ result.dubbed_audio.save(dubbed_audio_path)
172
+ replace_audio_stream(
173
+ video_path=input_path,
174
+ audio_path=dubbed_audio_path,
175
+ output_path=output_path,
176
+ )
177
+ finally:
178
+ dubbed_audio_path.unlink(missing_ok=True)
179
+
180
+ return result
181
+
102
182
  def revoice(
103
183
  self,
104
184
  video: Video,
@@ -111,7 +191,7 @@ class VideoDubber:
111
191
  self._init_local_pipeline()
112
192
 
113
193
  return self._local_pipeline.revoice(
114
- video=video,
194
+ source_audio=video.audio,
115
195
  text=text,
116
196
  preserve_background=preserve_background,
117
197
  progress_callback=progress_callback,
@@ -9,7 +9,7 @@ from videopython.ai.dubbing.models import DubbingResult, RevoiceResult, Separate
9
9
  from videopython.ai.dubbing.timing import TimingSynchronizer
10
10
 
11
11
  if TYPE_CHECKING:
12
- from videopython.base.video import Video
12
+ from videopython.base.audio import Audio
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -102,7 +102,6 @@ class LocalDubbingPipeline:
102
102
  max_duration: float = 10.0,
103
103
  ) -> dict[str, Any]:
104
104
  """Extract voice samples for each speaker from the audio."""
105
- from videopython.base.audio import Audio
106
105
 
107
106
  voice_samples: dict[str, Audio] = {}
108
107
 
@@ -135,7 +134,7 @@ class LocalDubbingPipeline:
135
134
 
136
135
  def process(
137
136
  self,
138
- video: Video,
137
+ source_audio: Audio,
139
138
  target_lang: str,
140
139
  source_lang: str | None = None,
141
140
  preserve_background: bool = True,
@@ -144,22 +143,22 @@ class LocalDubbingPipeline:
144
143
  progress_callback: Callable[[str, float], None] | None = None,
145
144
  transcription: Any | None = None,
146
145
  ) -> DubbingResult:
147
- """Process a video through the local dubbing pipeline.
146
+ """Run the dubbing pipeline against the given source audio.
148
147
 
149
148
  Args:
149
+ source_audio: Source audio track to dub. Callers with a ``Video``
150
+ object should pass ``video.audio``; callers with only a file path
151
+ can use ``Audio.from_path(path)`` to avoid loading video frames.
150
152
  transcription: Optional pre-computed Transcription object. When provided,
151
153
  the internal Whisper transcription step is skipped (saving time and VRAM).
152
154
  Must be a ``videopython.base.text.transcription.Transcription`` instance
153
155
  with populated ``segments``.
154
156
  """
155
- from videopython.base.audio import Audio
156
157
 
157
158
  def report_progress(stage: str, progress: float) -> None:
158
159
  if progress_callback:
159
160
  progress_callback(stage, progress)
160
161
 
161
- source_audio = video.audio
162
-
163
162
  if transcription is not None:
164
163
  report_progress("Using provided transcription", 0.05)
165
164
  else:
@@ -275,19 +274,23 @@ class LocalDubbingPipeline:
275
274
 
276
275
  def revoice(
277
276
  self,
278
- video: Video,
277
+ source_audio: Audio,
279
278
  text: str,
280
279
  preserve_background: bool = True,
281
280
  progress_callback: Callable[[str, float], None] | None = None,
282
281
  ) -> RevoiceResult:
283
- """Replace speech in a video with new text using voice cloning."""
282
+ """Replace speech in audio with new text using voice cloning.
283
+
284
+ Args:
285
+ source_audio: Source audio track to revoice. Callers with a ``Video``
286
+ object should pass ``video.audio``.
287
+ """
284
288
  from videopython.base.audio import Audio
285
289
 
286
290
  def report_progress(stage: str, progress: float) -> None:
287
291
  if progress_callback:
288
292
  progress_callback(stage, progress)
289
293
 
290
- source_audio = video.audio
291
294
  original_duration = source_audio.metadata.duration_seconds
292
295
 
293
296
  report_progress("Analyzing audio", 0.05)
@@ -0,0 +1,73 @@
1
+ """ffmpeg helper for replacing a video file's audio track without re-encoding video."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class RemuxError(RuntimeError):
13
+ """ffmpeg failed while replacing an audio stream."""
14
+
15
+
16
+ def replace_audio_stream(
17
+ video_path: str | Path,
18
+ audio_path: str | Path,
19
+ output_path: str | Path,
20
+ audio_codec: str = "aac",
21
+ audio_bitrate: str = "192k",
22
+ ) -> None:
23
+ """Copy ``video_path``'s video stream and mux in ``audio_path`` as the audio track.
24
+
25
+ Uses ffmpeg stream-copy for video (no re-encode) and encodes audio to AAC.
26
+ ``-shortest`` trims to the shorter of the two streams so the output duration
27
+ matches the source video when the dubbed audio is slightly longer.
28
+
29
+ Args:
30
+ video_path: Source video file (video stream is copied unchanged).
31
+ audio_path: Audio file to use as the new audio track.
32
+ output_path: Destination file. Overwritten if it exists.
33
+ audio_codec: ffmpeg audio codec name. Defaults to ``aac`` (MP4-compatible).
34
+ audio_bitrate: Audio bitrate passed to ffmpeg (``-b:a``).
35
+
36
+ Raises:
37
+ FileNotFoundError: If ``video_path`` or ``audio_path`` does not exist.
38
+ RemuxError: If ffmpeg returns a non-zero exit code.
39
+ """
40
+ video_path = Path(video_path)
41
+ audio_path = Path(audio_path)
42
+ output_path = Path(output_path)
43
+
44
+ if not video_path.exists():
45
+ raise FileNotFoundError(f"Video file not found: {video_path}")
46
+ if not audio_path.exists():
47
+ raise FileNotFoundError(f"Audio file not found: {audio_path}")
48
+
49
+ cmd = [
50
+ "ffmpeg",
51
+ "-y",
52
+ "-i",
53
+ str(video_path),
54
+ "-i",
55
+ str(audio_path),
56
+ "-map",
57
+ "0:v:0",
58
+ "-map",
59
+ "1:a:0",
60
+ "-c:v",
61
+ "copy",
62
+ "-c:a",
63
+ audio_codec,
64
+ "-b:a",
65
+ audio_bitrate,
66
+ "-shortest",
67
+ str(output_path),
68
+ ]
69
+
70
+ logger.info("replace_audio_stream: %s + %s -> %s", video_path, audio_path, output_path)
71
+ result = subprocess.run(cmd, capture_output=True)
72
+ if result.returncode != 0:
73
+ raise RemuxError(f"ffmpeg failed (exit {result.returncode}): {result.stderr.decode(errors='replace')}")
File without changes
File without changes
File without changes