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.
Files changed (89) hide show
  1. {videopython-0.48.0 → videopython-0.49.0}/PKG-INFO +1 -4
  2. {videopython-0.48.0 → videopython-0.49.0}/pyproject.toml +1 -7
  3. videopython-0.49.0/src/videopython/ai/_ollama.py +71 -0
  4. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_revisions.py +0 -11
  5. videopython-0.49.0/src/videopython/ai/auto_edit/local.py +39 -0
  6. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/__init__.py +5 -6
  7. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/config.py +7 -8
  8. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/dubber.py +2 -2
  9. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/models.py +3 -5
  10. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/pipeline.py +6 -64
  11. videopython-0.49.0/src/videopython/ai/generation/translation.py +375 -0
  12. videopython-0.49.0/src/videopython/ai/understanding/image.py +79 -0
  13. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/analyzer.py +1 -1
  14. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/models.py +1 -1
  15. videopython-0.48.0/src/videopython/ai/auto_edit/local.py +0 -67
  16. videopython-0.48.0/src/videopython/ai/generation/qwen3.py +0 -504
  17. videopython-0.48.0/src/videopython/ai/generation/translation.py +0 -347
  18. videopython-0.48.0/src/videopython/ai/understanding/image.py +0 -400
  19. {videopython-0.48.0 → videopython-0.49.0}/.gitignore +0 -0
  20. {videopython-0.48.0 → videopython-0.49.0}/LICENSE +0 -0
  21. {videopython-0.48.0 → videopython-0.49.0}/README.md +0 -0
  22. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/__init__.py +0 -0
  23. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/__init__.py +0 -0
  24. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_device.py +0 -0
  25. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_optional.py +0 -0
  26. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/_predictor.py +0 -0
  27. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/__init__.py +0 -0
  28. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/backend.py +0 -0
  29. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/catalog.py +0 -0
  30. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/editor.py +0 -0
  31. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/models.py +0 -0
  32. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/auto_edit/resolve.py +0 -0
  33. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/expressiveness.py +0 -0
  34. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/loudness.py +0 -0
  35. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/quality.py +0 -0
  36. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/remux.py +0 -0
  37. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/timing.py +0 -0
  38. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/dubbing/voice_sample.py +0 -0
  39. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/effects.py +0 -0
  40. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/__init__.py +0 -0
  41. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/_tts_backend.py +0 -0
  42. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/audio.py +0 -0
  43. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/image.py +0 -0
  44. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/generation/video.py +0 -0
  45. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/transforms.py +0 -0
  46. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/__init__.py +0 -0
  47. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/audio.py +0 -0
  48. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/faces.py +0 -0
  49. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/objects.py +0 -0
  50. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/separation.py +0 -0
  51. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/understanding/temporal.py +0 -0
  52. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/__init__.py +0 -0
  53. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/sampling.py +0 -0
  54. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/ai/video_analysis/stages.py +0 -0
  55. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/__init__.py +0 -0
  56. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/analysis.py +0 -0
  57. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/audio/audio.py +0 -0
  58. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/__init__.py +0 -0
  59. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_dimensions.py +0 -0
  60. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_ffmpeg.py +0 -0
  61. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/_video_io.py +0 -0
  62. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/description.py +0 -0
  63. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/draw_detections.py +0 -0
  64. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/exceptions.py +0 -0
  65. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-OFL.txt +0 -0
  66. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Anton-Regular.ttf +0 -0
  67. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-OFL.txt +0 -0
  68. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/BebasNeue-Regular.ttf +0 -0
  69. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
  70. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
  71. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-Bold.ttf +0 -0
  72. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Lato-OFL.txt +0 -0
  73. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-Bold.ttf +0 -0
  74. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/Poppins-OFL.txt +0 -0
  75. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/fonts/__init__.py +0 -0
  76. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/transcription.py +0 -0
  77. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/base/video.py +0 -0
  78. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/__init__.py +0 -0
  79. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_ass.py +0 -0
  80. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_easing.py +0 -0
  81. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/_schema.py +0 -0
  82. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/audio_ops.py +0 -0
  83. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/effects.py +0 -0
  84. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/operation.py +0 -0
  85. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/streaming.py +0 -0
  86. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/transcription_overlay.py +0 -0
  87. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/transforms.py +0 -0
  88. {videopython-0.48.0 → videopython-0.49.0}/src/videopython/editing/video_edit.py +0 -0
  89. {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.48.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.48.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's translation backends (``MarianTranslator``/``Qwen3Translator``)
4
- top-level import transformers/llama-cpp/torch, so eagerly chaining ``pipeline``
5
- here would drag the ``[dub]`` translation stack in just to read
6
- ``DubbingConfig`` (pydantic-only). Lazy re-exports keep the lightweight
7
- config/model symbols importable without that; only ``LocalDubbingPipeline`` /
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", "marian", "qwen3"]
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, MarianMT,
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 to use. ``"auto"`` (default) picks
51
- Qwen3 on GPU, MarianMT on CPU; ``"marian"`` and ``"qwen3"`` force
52
- the named backend regardless of device. See
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 TextTranslator
250
+ from videopython.ai.generation.translation import OllamaTranslator
251
251
 
252
- return TextTranslator.get_supported_languages()
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 where translation failed
222
- entirely. Used by Qwen3Translator when both the primary call and
223
- the per-segment Marian fallback fail; those segments are dubbed
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.qwen3 import Qwen3Translator
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
- Resolves the configured ``self.config.translator`` choice into a concrete
211
- backend. ``"auto"`` uses :meth:`_resolve_translator_auto`; explicit
212
- choices instantiate the named backend directly. Re-initialization
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``.