videopython 0.48.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.
- {videopython-0.48.0 → videopython-0.49.0}/PKG-INFO +1 -4
- {videopython-0.48.0 → videopython-0.49.0}/pyproject.toml +1 -7
- videopython-0.49.0/src/videopython/ai/_ollama.py +71 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_revisions.py +0 -11
- videopython-0.49.0/src/videopython/ai/auto_edit/local.py +39 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/__init__.py +5 -6
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/config.py +7 -8
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/dubber.py +2 -2
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/models.py +3 -5
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/pipeline.py +6 -64
- videopython-0.49.0/src/videopython/ai/generation/translation.py +375 -0
- videopython-0.49.0/src/videopython/ai/understanding/image.py +79 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/analyzer.py +1 -1
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/models.py +1 -1
- videopython-0.48.0/src/videopython/ai/auto_edit/local.py +0 -67
- videopython-0.48.0/src/videopython/ai/generation/qwen3.py +0 -504
- videopython-0.48.0/src/videopython/ai/generation/translation.py +0 -347
- videopython-0.48.0/src/videopython/ai/understanding/image.py +0 -400
- {videopython-0.48.0 → videopython-0.49.0}/.gitignore +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/LICENSE +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/README.md +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_device.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_optional.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_predictor.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/backend.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/editor.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/models.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/effects.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/objects.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/audio.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/description.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/draw_detections.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/transcription.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/video.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_ass.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_easing.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_schema.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/audio_ops.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/effects.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/operation.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.48.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.
|
|
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,13 +28,10 @@ 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'
|
|
32
31
|
Requires-Dist: ollama>=0.5.0; extra == 'ai'
|
|
33
32
|
Requires-Dist: openai-whisper>=20240930; extra == 'ai'
|
|
34
33
|
Requires-Dist: pyannote-audio>=4.0.0; extra == 'ai'
|
|
35
34
|
Requires-Dist: pyloudnorm>=0.1.1; extra == 'ai'
|
|
36
|
-
Requires-Dist: qwen-vl-utils>=0.0.10; extra == 'ai'
|
|
37
|
-
Requires-Dist: sentencepiece>=0.1.99; extra == 'ai'
|
|
38
35
|
Requires-Dist: silero-vad>=5.1; extra == 'ai'
|
|
39
36
|
Requires-Dist: torch>=2.8.0; extra == 'ai'
|
|
40
37
|
Requires-Dist: torchaudio>=2.8.0; extra == 'ai'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "videopython"
|
|
3
|
-
version = "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" },
|
|
@@ -72,13 +72,9 @@ ai = [
|
|
|
72
72
|
"ultralytics>=8.0.0",
|
|
73
73
|
"transnetv2-pytorch>=1.0.5",
|
|
74
74
|
"transformers>=5.2.0",
|
|
75
|
-
"qwen-vl-utils>=0.0.10",
|
|
76
75
|
"imagehash>=4.3",
|
|
77
76
|
# Audio source separation (understanding/separation.py)
|
|
78
77
|
"demucs>=4.0.0",
|
|
79
|
-
# Translation backends (generation/translation.py MarianMT, generation/qwen3.py GGUF)
|
|
80
|
-
"sentencepiece>=0.1.99",
|
|
81
|
-
"llama-cpp-python>=0.3.0",
|
|
82
78
|
# Voice cloning TTS (generation/audio.py — Chatterbox); its strict pins drive
|
|
83
79
|
# the [tool.uv].override-dependencies block below.
|
|
84
80
|
"chatterbox-tts>=0.1.7",
|
|
@@ -120,9 +116,7 @@ module = [
|
|
|
120
116
|
"pyannote", "pyannote.*",
|
|
121
117
|
"silero_vad", "silero_vad.*",
|
|
122
118
|
"cv2", "cv2.*",
|
|
123
|
-
"llama_cpp", "llama_cpp.*",
|
|
124
119
|
"pyloudnorm", "pyloudnorm.*",
|
|
125
|
-
"qwen_vl_utils", "qwen_vl_utils.*",
|
|
126
120
|
"imagehash", "imagehash.*",
|
|
127
121
|
]
|
|
128
122
|
ignore_missing_imports = true
|
|
@@ -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,39 @@
|
|
|
1
|
+
"""A local, model-agnostic planner backed by an Ollama server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from videopython.ai._ollama import OllamaError, OllamaStructuredClient
|
|
8
|
+
|
|
9
|
+
from .backend import ImagePart, Part, PlannerError, TextPart
|
|
10
|
+
|
|
11
|
+
DEFAULT_OLLAMA_MODEL = "gemma3:27b"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OllamaVisionLLM:
|
|
15
|
+
"""A StructuredVisionLLM backed by a local Ollama server.
|
|
16
|
+
|
|
17
|
+
The model must be vision-capable (it is sent keyframes) AND support Ollama's
|
|
18
|
+
structured-output ``format`` (the EditPlan schema constrains the decode). Not
|
|
19
|
+
every model supports schema conditioning -- ``gemma3:27b`` is verified working;
|
|
20
|
+
some builds (e.g. certain MLX ones) fail it. ``ollama pull <model>`` first;
|
|
21
|
+
``options`` are extra generation options merged over ``temperature=0``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
model: str = DEFAULT_OLLAMA_MODEL,
|
|
27
|
+
*,
|
|
28
|
+
host: str | None = None,
|
|
29
|
+
options: dict[str, Any] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._client = OllamaStructuredClient(model=model, host=host, options=options)
|
|
32
|
+
|
|
33
|
+
def generate_json(self, *, system: str, parts: list[Part], schema: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
text = "\n\n".join(part.text for part in parts if isinstance(part, TextPart))
|
|
35
|
+
images = [part.image for part in parts if isinstance(part, ImagePart)]
|
|
36
|
+
try:
|
|
37
|
+
return self._client.generate_json(system=system, text=text, schema=schema, images=images or None)
|
|
38
|
+
except OllamaError as exc:
|
|
39
|
+
raise PlannerError(str(exc)) from exc
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Local video dubbing functionality, lazily re-exported (PEP 562).
|
|
2
2
|
|
|
3
|
-
The pipeline
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
``VideoDubber`` access loads the heavy chain. The ``TYPE_CHECKING`` block keeps
|
|
3
|
+
The pipeline pulls heavy stages (Whisper, Demucs, Chatterbox), so eagerly
|
|
4
|
+
chaining ``pipeline`` here would drag them in just to read ``DubbingConfig``
|
|
5
|
+
(pydantic-only). Lazy re-exports keep the lightweight config/model symbols
|
|
6
|
+
importable without that; only ``LocalDubbingPipeline`` / ``VideoDubber`` access
|
|
7
|
+
loads the heavy chain. The ``TYPE_CHECKING`` block keeps
|
|
9
8
|
symbols visible to mypy and IDEs.
|
|
10
9
|
"""
|
|
11
10
|
|
|
@@ -6,7 +6,7 @@ from typing import Literal
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict
|
|
8
8
|
|
|
9
|
-
TranslatorChoice = Literal["auto", "
|
|
9
|
+
TranslatorChoice = Literal["auto", "ollama"]
|
|
10
10
|
WhisperModel = Literal["tiny", "base", "small", "medium", "large", "turbo"]
|
|
11
11
|
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ class DubbingConfig(BaseModel):
|
|
|
18
18
|
|
|
19
19
|
Attributes:
|
|
20
20
|
device: Execution device (``cpu``, ``cuda``, ``mps``, or ``None`` for auto).
|
|
21
|
-
low_memory: When True, each pipeline stage (Whisper, Demucs,
|
|
21
|
+
low_memory: When True, each pipeline stage (Whisper, Demucs, translation,
|
|
22
22
|
Chatterbox TTS) is unloaded from memory after it runs, so only one
|
|
23
23
|
model is resident at a time. Trades per-run latency (~10-30s of
|
|
24
24
|
extra model loads) for a much lower memory ceiling. Recommended
|
|
@@ -47,12 +47,9 @@ class DubbingConfig(BaseModel):
|
|
|
47
47
|
WARNING but processing continues. Either way the
|
|
48
48
|
:class:`TranscriptQuality` is exposed on ``DubbingResult`` for
|
|
49
49
|
inspection.
|
|
50
|
-
translator: Translation backend
|
|
51
|
-
|
|
52
|
-
the
|
|
53
|
-
:class:`videopython.ai.generation.qwen3.Qwen3Translator` for
|
|
54
|
-
tradeoffs (Qwen3 is slower on CPU but produces context-aware,
|
|
55
|
-
length-budgeted output).
|
|
50
|
+
translator: Translation backend; ``"auto"`` and ``"ollama"`` both use the
|
|
51
|
+
local Ollama text model. ``translator_model`` sets the Ollama tag and
|
|
52
|
+
``translator_host`` the server URL.
|
|
56
53
|
"""
|
|
57
54
|
|
|
58
55
|
model_config = ConfigDict(frozen=True)
|
|
@@ -66,6 +63,8 @@ class DubbingConfig(BaseModel):
|
|
|
66
63
|
vocabulary: list[str] | None = None
|
|
67
64
|
strict_quality: bool = False
|
|
68
65
|
translator: TranslatorChoice = "auto"
|
|
66
|
+
translator_model: str | None = None
|
|
67
|
+
translator_host: str | None = None
|
|
69
68
|
|
|
70
69
|
def init_log_fields(self) -> dict[str, object]:
|
|
71
70
|
"""Subset of fields surfaced in the init-log line.
|
|
@@ -247,6 +247,6 @@ class VideoDubber:
|
|
|
247
247
|
|
|
248
248
|
@staticmethod
|
|
249
249
|
def get_supported_languages() -> dict[str, str]:
|
|
250
|
-
from videopython.ai.generation.translation import
|
|
250
|
+
from videopython.ai.generation.translation import OllamaTranslator
|
|
251
251
|
|
|
252
|
-
return
|
|
252
|
+
return OllamaTranslator.get_supported_languages()
|
|
@@ -218,11 +218,9 @@ class DubbingResult(BaseModel):
|
|
|
218
218
|
timing_summary: Aggregate stats over per-segment timing adjustments.
|
|
219
219
|
transcript_quality: Heuristic quality assessment of the transcription
|
|
220
220
|
(None when the pipeline returned early on an empty transcription).
|
|
221
|
-
translation_failures: Indices of segments
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
with empty text. Empty list under MarianTranslator (Marian has
|
|
225
|
-
no failure mode that drops segments).
|
|
221
|
+
translation_failures: Indices of segments the translator could not
|
|
222
|
+
translate (missing after its parse-retry pass); those segments are
|
|
223
|
+
dubbed with empty text.
|
|
226
224
|
"""
|
|
227
225
|
|
|
228
226
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -7,18 +7,12 @@ import tempfile
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TYPE_CHECKING, Any, Callable
|
|
9
9
|
|
|
10
|
-
from videopython.ai._device import select_device
|
|
11
10
|
from videopython.ai.dubbing import expressiveness, loudness, voice_sample
|
|
12
11
|
from videopython.ai.dubbing.config import DubbingConfig
|
|
13
12
|
from videopython.ai.dubbing.models import DubbingResult, Expressiveness, RevoiceResult, SeparatedAudio, TimingSummary
|
|
14
13
|
from videopython.ai.dubbing.quality import GarbageTranscriptError, assess_transcript
|
|
15
14
|
from videopython.ai.dubbing.timing import TimingSynchronizer
|
|
16
|
-
from videopython.ai.generation.
|
|
17
|
-
from videopython.ai.generation.translation import (
|
|
18
|
-
MarianTranslator,
|
|
19
|
-
TranslationBackend,
|
|
20
|
-
UnsupportedLanguageError,
|
|
21
|
-
)
|
|
15
|
+
from videopython.ai.generation.translation import DEFAULT_TRANSLATION_MODEL, OllamaTranslator
|
|
22
16
|
|
|
23
17
|
if TYPE_CHECKING:
|
|
24
18
|
from videopython.ai.dubbing.models import TranslatedSegment
|
|
@@ -205,63 +199,11 @@ class LocalDubbingPipeline:
|
|
|
205
199
|
)
|
|
206
200
|
|
|
207
201
|
def _init_translator(self, source_lang: str, target_lang: str) -> None:
|
|
208
|
-
"""Initialize the translation backend.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
is a no-op when ``self._translator`` is already a matching instance
|
|
214
|
-
for the same language pair (handled at call sites via the existing
|
|
215
|
-
``self._translator is None`` gate).
|
|
216
|
-
"""
|
|
217
|
-
if self.config.translator == "marian":
|
|
218
|
-
self._translator = MarianTranslator(device=self.config.device)
|
|
219
|
-
elif self.config.translator == "qwen3":
|
|
220
|
-
self._translator = Qwen3Translator(device=self.config.device)
|
|
221
|
-
else: # "auto"
|
|
222
|
-
self._translator = self._resolve_translator_auto(source_lang, target_lang)
|
|
223
|
-
|
|
224
|
-
def _resolve_translator_auto(self, source_lang: str, target_lang: str) -> TranslationBackend:
|
|
225
|
-
"""Pick a backend based on language coverage AND device.
|
|
226
|
-
|
|
227
|
-
Qwen3-4B Q4_K_M on CPU is roughly 10-15x slower than MarianMT (M2.1
|
|
228
|
-
spike on dreams_15min.mp4). The resolver picks Marian on CPU
|
|
229
|
-
whenever it covers the language pair and only escalates to Qwen
|
|
230
|
-
when a GPU is available or Marian doesn't cover the pair.
|
|
231
|
-
"""
|
|
232
|
-
device = select_device(self.config.device, mps_allowed=True)
|
|
233
|
-
has_gpu = device in ("cuda", "mps")
|
|
234
|
-
|
|
235
|
-
# 1. GPU + Qwen covers the pair → Qwen wins (best quality).
|
|
236
|
-
if has_gpu and Qwen3Translator.supports(source_lang, target_lang):
|
|
237
|
-
logger.info(
|
|
238
|
-
"translator: auto-selected qwen3 (device=%s, supports %s->%s)",
|
|
239
|
-
device,
|
|
240
|
-
source_lang,
|
|
241
|
-
target_lang,
|
|
242
|
-
)
|
|
243
|
-
return Qwen3Translator(device=self.config.device)
|
|
244
|
-
|
|
245
|
-
# 2. Marian covers the pair → Marian (fast).
|
|
246
|
-
if MarianTranslator.has_model_for(source_lang, target_lang):
|
|
247
|
-
if has_gpu:
|
|
248
|
-
reason = f"Qwen does not cover {source_lang}->{target_lang}"
|
|
249
|
-
else:
|
|
250
|
-
reason = f"device={device} (Qwen would be ~10-15x slower; pass translator='qwen3' to override)"
|
|
251
|
-
logger.info("translator: auto-selected marian (%s)", reason)
|
|
252
|
-
return MarianTranslator(device=self.config.device)
|
|
253
|
-
|
|
254
|
-
# 3. CPU + only Qwen covers it: warn loudly and use Qwen anyway.
|
|
255
|
-
if Qwen3Translator.supports(source_lang, target_lang):
|
|
256
|
-
logger.warning(
|
|
257
|
-
"translator: auto-selected qwen3 on CPU (%s->%s not in Marian); "
|
|
258
|
-
"translation will be slow (~10-15x MarianMT). Consider GPU.",
|
|
259
|
-
source_lang,
|
|
260
|
-
target_lang,
|
|
261
|
-
)
|
|
262
|
-
return Qwen3Translator(device=self.config.device)
|
|
263
|
-
|
|
264
|
-
raise UnsupportedLanguageError(source_lang, target_lang)
|
|
202
|
+
"""Initialize the Ollama translation backend."""
|
|
203
|
+
self._translator = OllamaTranslator(
|
|
204
|
+
model=self.config.translator_model or DEFAULT_TRANSLATION_MODEL,
|
|
205
|
+
host=self.config.translator_host,
|
|
206
|
+
)
|
|
265
207
|
|
|
266
208
|
def _init_tts(self, language: str = "en") -> None:
|
|
267
209
|
"""Resolve the text-to-speech backend for ``language``.
|