videopython 0.35.0__tar.gz → 0.35.1__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.35.0 → videopython-0.35.1}/PKG-INFO +1 -1
- {videopython-0.35.0 → videopython-0.35.1}/pyproject.toml +1 -1
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/qwen3.py +124 -19
- {videopython-0.35.0 → videopython-0.35.1}/.gitignore +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/LICENSE +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/README.md +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/_device.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/config.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/dubber.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/expressiveness.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/loudness.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/models.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/pipeline.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/quality.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/remux.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/timing.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/dubbing/voice_sample.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/audio.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/image.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/translation.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/generation/video.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/transforms.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/audio.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/faces.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/image.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/separation.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/understanding/temporal.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/video_analysis/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/video_analysis/analyzer.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/video_analysis/models.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/video_analysis/sampling.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/ai/video_analysis/stages.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/audio/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/audio/analysis.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/audio/audio.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/_dimensions.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/_ffmpeg.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/_video_io.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/description.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/exceptions.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/fonts/DejaVuSans.ttf +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/fonts/LICENSE_DEJAVU +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/fonts/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/image_text.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/transcription.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/base/video.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/__init__.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/effects.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/operation.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/streaming.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/transcription_overlay.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/transforms.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/editing/video_edit.py +0 -0
- {videopython-0.35.0 → videopython-0.35.1}/src/videopython/py.typed +0 -0
|
@@ -92,6 +92,56 @@ _SPEECH_CHARS_DEFAULT = 12.0
|
|
|
92
92
|
_LOW_LOGPROB_HINT_THRESHOLD = -1.0
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
# Conservative chars-per-token used to size chunks without invoking the
|
|
96
|
+
# tokenizer. Morphologically rich languages land around 1.5-2.0
|
|
97
|
+
# chars/token; ASCII is ~3-4. We use the low end so chunks stay safe for
|
|
98
|
+
# any source language.
|
|
99
|
+
_CHARS_PER_TOKEN = 2.0
|
|
100
|
+
# Token reserve for the system prompt + user-prompt envelope ("Input
|
|
101
|
+
# segments:" / "Translations (...)" wrappers). Empirical upper bound.
|
|
102
|
+
_PROMPT_OVERHEAD_TOKENS = 300
|
|
103
|
+
# Per-segment JSON wrapper cost (keys, braces, commas, index). Added on
|
|
104
|
+
# top of len(seg.text) when sizing chunks.
|
|
105
|
+
_SEGMENT_ENVELOPE_CHARS = 40
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _chunk_segment_indices(
|
|
109
|
+
segments: list[TranscriptionSegment],
|
|
110
|
+
n_ctx: int,
|
|
111
|
+
max_tokens: int,
|
|
112
|
+
) -> list[list[int]]:
|
|
113
|
+
"""Group positions in ``segments`` into batches that fit one Qwen call.
|
|
114
|
+
|
|
115
|
+
Each batch must satisfy ``prompt_tokens + max_tokens <= n_ctx``, which
|
|
116
|
+
llama.cpp enforces. We approximate prompt token count from character
|
|
117
|
+
length using ``_CHARS_PER_TOKEN``; the conservative ratio means a chunk
|
|
118
|
+
estimated at the budget will tokenize to comfortably less.
|
|
119
|
+
|
|
120
|
+
A segment whose own serialized form exceeds the per-call budget goes in
|
|
121
|
+
its own chunk anyway — better to let llama.cpp report a clean overflow
|
|
122
|
+
on one giant segment than to silently swallow it.
|
|
123
|
+
"""
|
|
124
|
+
prompt_token_budget = n_ctx - max_tokens - _PROMPT_OVERHEAD_TOKENS
|
|
125
|
+
if prompt_token_budget <= 0:
|
|
126
|
+
return [[i] for i in range(len(segments))]
|
|
127
|
+
char_budget = int(prompt_token_budget * _CHARS_PER_TOKEN)
|
|
128
|
+
|
|
129
|
+
chunks: list[list[int]] = []
|
|
130
|
+
current: list[int] = []
|
|
131
|
+
current_chars = 0
|
|
132
|
+
for i, seg in enumerate(segments):
|
|
133
|
+
seg_chars = len(seg.text) + _SEGMENT_ENVELOPE_CHARS
|
|
134
|
+
if current and current_chars + seg_chars > char_budget:
|
|
135
|
+
chunks.append(current)
|
|
136
|
+
current = []
|
|
137
|
+
current_chars = 0
|
|
138
|
+
current.append(i)
|
|
139
|
+
current_chars += seg_chars
|
|
140
|
+
if current:
|
|
141
|
+
chunks.append(current)
|
|
142
|
+
return chunks
|
|
143
|
+
|
|
144
|
+
|
|
95
145
|
def _target_chars_for(duration_seconds: float, target_lang: str) -> int:
|
|
96
146
|
"""Character-count budget for a segment of ``duration_seconds`` in ``target_lang``."""
|
|
97
147
|
rate = _SPEECH_CHARS_PER_SEC.get(target_lang, _SPEECH_CHARS_DEFAULT)
|
|
@@ -170,9 +220,12 @@ class Qwen3Translator:
|
|
|
170
220
|
``DEFAULT_REPO_ID``; override for eval harnesses.
|
|
171
221
|
filename: GGUF filename within ``repo_id``. Defaults to
|
|
172
222
|
``DEFAULT_FILENAME``.
|
|
173
|
-
n_ctx: llama.cpp context window.
|
|
174
|
-
|
|
175
|
-
|
|
223
|
+
n_ctx: llama.cpp context window. ``translate_segments`` splits the
|
|
224
|
+
input across multiple calls when it doesn't fit, so 8192 stays
|
|
225
|
+
safe even for very long sources; raise to reduce the number of
|
|
226
|
+
calls (and gain cross-segment context per call) at the cost of
|
|
227
|
+
VRAM. Hard cap is the model's training context (262K for
|
|
228
|
+
Qwen3-4B-Instruct-2507).
|
|
176
229
|
max_tokens: Generation cap per call. 4× the input character count
|
|
177
230
|
is a safe upper bound for translation output.
|
|
178
231
|
temperature: Decoding temperature. 0.1 keeps output structurally
|
|
@@ -257,6 +310,52 @@ class Qwen3Translator:
|
|
|
257
310
|
raw = response["choices"][0]["text"]
|
|
258
311
|
return _parse_jsonl_response(raw)
|
|
259
312
|
|
|
313
|
+
def _qwen_translate_chunked(
|
|
314
|
+
self,
|
|
315
|
+
segments: list[TranscriptionSegment],
|
|
316
|
+
target_lang: str,
|
|
317
|
+
source_lang: str,
|
|
318
|
+
progress_callback: Callable[[float], None] | None = None,
|
|
319
|
+
progress_start: float = 0.0,
|
|
320
|
+
progress_end: float = 1.0,
|
|
321
|
+
) -> dict[int, str]:
|
|
322
|
+
"""Translate ``segments`` across one or more Qwen calls.
|
|
323
|
+
|
|
324
|
+
Returns a dict keyed by position in ``segments``. Splitting into
|
|
325
|
+
chunks keeps each call under llama.cpp's ``n_ctx`` cap — without
|
|
326
|
+
chunking, a long source with hundreds of dense segments easily
|
|
327
|
+
blows past the default 8192 token window.
|
|
328
|
+
|
|
329
|
+
Progress is reported as a linear ramp from ``progress_start`` to
|
|
330
|
+
``progress_end``, one tick per chunk completed.
|
|
331
|
+
"""
|
|
332
|
+
results: dict[int, str] = {}
|
|
333
|
+
if not segments:
|
|
334
|
+
if progress_callback is not None:
|
|
335
|
+
progress_callback(progress_end)
|
|
336
|
+
return results
|
|
337
|
+
|
|
338
|
+
chunks = _chunk_segment_indices(segments, self.n_ctx, self.max_tokens)
|
|
339
|
+
if len(chunks) > 1:
|
|
340
|
+
logger.info(
|
|
341
|
+
"Qwen3Translator: splitting %d segments into %d chunks (n_ctx=%d)",
|
|
342
|
+
len(segments),
|
|
343
|
+
len(chunks),
|
|
344
|
+
self.n_ctx,
|
|
345
|
+
)
|
|
346
|
+
for chunk_num, chunk_positions in enumerate(chunks):
|
|
347
|
+
chunk_segments = [segments[p] for p in chunk_positions]
|
|
348
|
+
chunk_result = self._qwen_translate(chunk_segments, target_lang, source_lang)
|
|
349
|
+
# chunk_result keys are 0..len(chunk_positions)-1; map back to
|
|
350
|
+
# positions in the caller-provided ``segments`` list.
|
|
351
|
+
for local_idx, text in chunk_result.items():
|
|
352
|
+
if 0 <= local_idx < len(chunk_positions):
|
|
353
|
+
results[chunk_positions[local_idx]] = text
|
|
354
|
+
if progress_callback is not None:
|
|
355
|
+
fraction = (chunk_num + 1) / len(chunks)
|
|
356
|
+
progress_callback(progress_start + (progress_end - progress_start) * fraction)
|
|
357
|
+
return results
|
|
358
|
+
|
|
260
359
|
def translate_segments(
|
|
261
360
|
self,
|
|
262
361
|
segments: list[TranscriptionSegment],
|
|
@@ -266,10 +365,10 @@ class Qwen3Translator:
|
|
|
266
365
|
) -> list[TranslatedSegment]:
|
|
267
366
|
"""Translate segments via Qwen with parse-retry + optional Marian fallback.
|
|
268
367
|
|
|
269
|
-
The progress_callback
|
|
270
|
-
Qwen
|
|
271
|
-
end.
|
|
272
|
-
|
|
368
|
+
The progress_callback ramps from 0 to 0.5 across the first-pass
|
|
369
|
+
Qwen chunks, hits 0.9 after the optional retry/fallback, and 1.0
|
|
370
|
+
at the end. Input larger than the model's context window is split
|
|
371
|
+
across multiple Qwen calls (see ``_qwen_translate_chunked``).
|
|
273
372
|
"""
|
|
274
373
|
effective_source = source_lang or "en"
|
|
275
374
|
self._failures_last_call = []
|
|
@@ -277,13 +376,15 @@ class Qwen3Translator:
|
|
|
277
376
|
translatable_indices = [i for i, seg in enumerate(segments) if _is_translatable_text(seg.text)]
|
|
278
377
|
translatable_segments = [segments[i] for i in translatable_indices]
|
|
279
378
|
|
|
280
|
-
# First attempt.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
379
|
+
# First attempt — chunked to fit n_ctx.
|
|
380
|
+
qwen_results = self._qwen_translate_chunked(
|
|
381
|
+
translatable_segments,
|
|
382
|
+
target_lang,
|
|
383
|
+
effective_source,
|
|
384
|
+
progress_callback=progress_callback,
|
|
385
|
+
progress_start=0.0,
|
|
386
|
+
progress_end=0.5,
|
|
387
|
+
)
|
|
287
388
|
|
|
288
389
|
# Identify segments Qwen failed (unparseable or missing index).
|
|
289
390
|
# Indices in qwen_results / translatable_segments are 0-based positions
|
|
@@ -299,11 +400,15 @@ class Qwen3Translator:
|
|
|
299
400
|
len(retry_segments),
|
|
300
401
|
len(translatable_segments),
|
|
301
402
|
)
|
|
302
|
-
retry_results = self.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
403
|
+
retry_results = self._qwen_translate_chunked(
|
|
404
|
+
retry_segments,
|
|
405
|
+
target_lang,
|
|
406
|
+
effective_source,
|
|
407
|
+
)
|
|
408
|
+
# retry_results keys are positions in retry_segments; map back to
|
|
409
|
+
# translatable_segments.
|
|
410
|
+
for retry_local, translation in retry_results.items():
|
|
411
|
+
qwen_results[missing_local_indices[retry_local]] = translation
|
|
307
412
|
if progress_callback is not None:
|
|
308
413
|
progress_callback(0.9)
|
|
309
414
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|