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.
- {videopython-0.26.2 → videopython-0.26.3}/PKG-INFO +1 -1
- {videopython-0.26.2 → videopython-0.26.3}/pyproject.toml +1 -1
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/dubber.py +82 -2
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/pipeline.py +13 -10
- videopython-0.26.3/src/videopython/ai/dubbing/remux.py +73 -0
- {videopython-0.26.2 → videopython-0.26.3}/.gitignore +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/LICENSE +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/README.md +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/_device.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/registry.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/inpainter.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/models.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/segmenter.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/swapping/swapper.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/ai/video_analysis.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/analysis.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/audio/audio.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/combine.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/description.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/effects.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/progress.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/registry.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/scene.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/streaming.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/overlay.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/text/transcription.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/transforms.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/transitions.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/utils.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/base/video.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/multicam.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/premiere_xml.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.26.2 → videopython-0.26.3}/src/videopython/py.typed +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|