API reference¶
Generated from the docstrings of the public modules, classes, and functions.
The vendored Look2Hear tree under dubber/third_party is not included.
dubber
¶
Voxprint AI Movie Dubber - early pre-release with a built-in diagnostic / benchmark.
appinfo
¶
Application metadata.
BUILD.json at the repository root is the offset and the codename.
voxprint_common.buildinfo reads that file and a sibling build_info.json.
The release build number is GITHUB_RUN_NUMBER plus the offset. The installer
workflow writes the result into build_info.json. An installed copy reads that
stamp and does not need the git checkout.
A recorded build of 0 stays 0. A missing recorded build is dev, never 0.
VOXPRINT_BUILD set to digits forces that number.
The version line keeps a middle dot: 1.0.0 RC · build N "Codename".
The splash uses :func:voxprint_common.buildinfo.label, which has no middle dot.
A codename is one Biblical Hebrew word in ASCII transliteration. It describes the state of that build (readiness, a milestone), not a theme of the product. It is shown only when it is not empty.
resource_dir()
¶
Folder with bundled resources: the repository or installation folder.
build_info()
cached
¶
build_info.json written by the installer workflow; {} when the stamp is absent.
The build number and codename come from :func:voxprint_common.buildinfo.load.
This dict keeps the extra stamp fields (commit, built, flavour) for :func:version_line.
clear_build_cache()
¶
Drop the cached read of build_info.json (tests).
app_build()
¶
Build number for this process, or the marker dev when none was recorded.
app_codename()
¶
Codename for this process.
format_version(version=None, build=None, codename=None)
¶
Human version, e.g. 1.0.0 RC · build 1001 "Bochan". The codename is omitted when it is empty.
version_label()
¶
The version line shown in the window, About, --version and the installer.
release_title()
¶
GitHub release title, e.g. VOXPrint AI Movie Dubber 1.0.0 RC · build 1001 "Bochan".
version_line()
¶
One line for About, --version and the diagnostics header.
cli
¶
Headless commands for other programs and scripts.
python main.py version|diagnose|fetch-models|run-project|info ...
python main.py --dry-run --json
--json prints one JSON object on stdout. Logs and errors go to stderr.
--dry-run only discovers the machine and the job: no model load and no download.
Exit codes are :data:EXIT_CODES. See docs/CLI.md.
UsageError
¶
Bases: Exception
The command line is not valid. Exit :data:EXIT_USAGE.
InputError
¶
Bases: Exception
The file or project cannot be used. Exit :data:EXIT_INPUT.
Request(command, json_mode, dry_run, path='', values=dict(), switches=set(), argv=list())
dataclass
¶
One parsed headless command: the name, whether to print JSON, and the flags that were set.
build_parser()
¶
Headless command line. parse_args and the generated reference both read this parser.
build_window_parser()
¶
Window and installer entry points. Not used to parse the headless commands.
flag_sets(parser)
¶
Switches, options that take a value, and command names (including aliases) declared on parser.
parse_args(argv)
¶
Split argv (no program name) into a command. Raises :class:UsageError.
main(argv=None)
¶
Run one headless command. argv is the argument list without the program name.
core
¶
Pure logic of the dubbing pipeline (no Qt, no model imports): project files, subtitles, timing, mixing, voices.
actor_voice
¶
'Actor-like voice' (multi-voice mode option): a temporary per-project voice that sounds like the actor speaking the dub language.
For one speaker it blends
* the actor's own clean reference clip (built from his lines; Qwen3-TTS clones cross-lingually from it), and
* the closest voice of the shared library (cosine similarity of speaker embeddings: the actor's x-vector against each library
voice's speaker_centroid.safetensors; voices of the other gender are skipped when the gender is known),
by mixing the two speaker embeddings (weight = share of the actor, default 0.5) and running the library LoRA adapter at a
reduced strength (library scale x (1 - weight)). When the actor clip is too short or too noisy the library voice is used
as is (fallback).
The temporary voice lives in <project>/voices/actor_<speaker>/ (record + embeddings) and disappears with the project;
:func:save_to_library turns it into a normal library voice (Audiobook Builder format) only when the user asks for it and
confirms that he may use this person's voice (EULA: the user is responsible for consent / rights).
The model-dependent part (the actor's x-vector, the prompt) runs in the TTS engine (dubber.engines.tts); this module is numpy
only. NEEDS VALIDATION ON A GPU: blend weight, adapter strength and the quality thresholds are first guesses.
Candidate(id, path, gender='', adapter_scale=1.0)
dataclass
¶
Library voice that can be blended with an actor. A known gender skips voices of the other gender.
folder(project_folder, speaker_id)
¶
Directory of the temporary actor voice for one speaker.
ref_quality(x, sr)
¶
(seconds, rough SNR in dB, good enough). SNR = loud frames vs quiet frames (speech vs the floor between words).
cosine(a, b)
¶
Cosine similarity of two embeddings, or -1 when the lengths differ or either vector is zero.
load_centroid(path)
¶
Speaker embedding from speaker_centroid.safetensors in path, or None when it cannot be read.
pick_closest(actor, candidates, gender='')
¶
Library voice with the most similar embedding (same gender first when known); without an embedding: the first of that gender.
blend(actor, library, weight)
¶
Weighted mix of two speaker embeddings, rescaled to the actor's norm (the model expects that magnitude).
adapter_scale(library_scale, weight)
¶
library_scale * (1 - weight), rounded to 3 decimals and never below zero.
write_record(dirpath, data, actor_emb=None, blended=None)
¶
Write actor_voice.json and, when given, the actor and blended embedding arrays. Creates the folder.
read_record(dirpath)
¶
Parsed actor_voice.json, or an empty dict when the file is missing or not JSON.
save_to_library(dirpath, name, consent_note, library_root, language='')
¶
Turn a made actor voice into a library voice (Audiobook Builder format). consent_note is required (EULA).
candidates_from_library(lib_voices)
¶
Library voices that can serve as the blend partner (they need a centroid to be compared; others still count as fallback).
audio
¶
Audio helpers (numpy / soundfile only): read, write, resample, energy speech detection, MFCC features, Signalsmith stretch.
read(path, sr=None, mono=True)
¶
Float32 samples and their sample rate. Resamples when sr is set and differs from the file.
read_range(path, start, end, mono=False)
¶
Samples from start to end seconds, zero-padded when the file ends first.
write(path, x, sr)
¶
Write samples as 16-bit PCM, creating parent directories. Values outside [-1, 1] are clipped.
duration(path)
¶
Length of a sound file in seconds.
resample(x, sr_from, sr_to)
¶
Resample along axis 0. Uses scipy when it imports, otherwise linear interpolation.
energy_vad(x, sr, frame_s=0.03, thresh_db=-38.0, min_speech_s=0.25, min_silence_s=0.3)
¶
Speech-like regions by frame energy relative to the loudest frames (fallback when Silero VAD is not installed).
mfcc_stats(x, sr, n_mels=32, n_mfcc=13)
¶
Mean + std of MFCCs (a small speaker fingerprint for the fallback speaker clustering).
stretch(x, sr, factor)
¶
Make x factor times faster without changing the pitch (Signalsmith Stretch, MIT; fallback: ffmpeg atempo).
trim_silence(x, sr, thresh_db=-45.0, pad_s=0.04)
¶
Cut leading / trailing silence of a synthesised line (TTS output usually has some).
media
¶
What is inside the film: duration, audio tracks, embedded subtitles (ffprobe); extraction of text subtitles and audio.
Track(index, stream, codec, lang='', title='', channels=None)
dataclass
¶
MediaInfo(path, duration=0.0, video_codec='', audio=list(), subtitles=list())
dataclass
¶
probe(path)
¶
Duration, video codec, and audio and subtitle tracks from ffprobe.
extract_subtitle(path, track, out_srt)
¶
Convert one subtitle stream to SRT at out_srt and return its cues.
embedded_for(info, lang)
¶
Best text subtitle track in lang (non-"forced"/"SDH" titles first).
pick_original_track(info, target_lang='')
¶
Audio track to dub from, chosen automatically: the original-language track. A track already in the dub language, a commentary or an audio-description track is skipped; a title saying "original" wins; otherwise the first track left.
extract_audio(path, out, sr, channels, track=0, start=None, length=None)
¶
Extract one audio track as 16-bit PCM. start and length, when set, are seconds.
poster_frame(src, at_s, dest)
¶
Write one JPEG of the frame at at_s seconds.
The player shows this still until Play. A decode failure returns False and leaves the player on its previous picture.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src
|
Path
|
Film path. |
required |
at_s
|
float
|
Time in seconds. |
required |
dest
|
Path
|
JPEG path to write. Parent directories are created. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when |
mux_dub(src, dub_wav, out, lang, title)
¶
Same file + the dub as a new audio track (video stream-copied, never re-encoded). The dub is AAC (plays on TVs) and becomes the default audio track, the original stays selectable.
MKV (default): every stream of the film is copied. MP4 (remux only): video + audio + text subtitles (as mov_text); an original audio track whose codec MP4 cannot hold (TrueHD, DTS, FLAC, PCM, Vorbis, ...) is converted to AAC - only that audio; picture-based subtitles and attachments (fonts) are left out because MP4 cannot carry them.
preview_clip(src, start, length, dub_wav, out)
¶
A short file with the dubbed fragment for an external player (only used when the built-in player cannot play the film).
The video is re-encoded here (native mpeg4, no GPL encoder needed) so it starts exactly at start and stays in sync.
mixing
¶
Mixing the dub track (research notes 01/06): outside dialogue windows the original soundtrack is kept untouched; inside the
windows the separated background + the original speech at original_volume (voice-over style) + the dub lines. Lines marked
"keep original" (songs) keep the full original. Crossfades at window edges; the result is peak-limited.
Works on any time range, so the same code makes the whole track, the 1-minute preview and the Watch-mode chunks.
mix_range(t0, t1, original, windows, lines, line_audio, background=None, speech=None, original_volume=0.15, dub_gain_db=0.0)
¶
Stereo float32 mix of [t0, t1) at the sample rate of original.
loudness_match_gain(dub, reference)
¶
dB gain that brings the dub lines to the RMS of the original speech (a simple stand-in for LUFS matching).
chunk_bounds(total, chunk_s)
¶
Ranges of at most chunk_s seconds from 0 to total. The last one ends at total.
profanity
¶
Profanity mode of the translation (per project): keep (as in the original, the default) or soften.
soften replaces obscene words (Russian mat) with non-obscene equivalents that may still be rude ("пиздец" -> "капец",
"охуел" -> "обалдел", "блядь" -> "чёрт"). It runs on the final translation - downloaded subtitles and Opus-MT output alike -
before the speech synthesis; every changed line keeps its previous text in Line.softened so the Script table can highlight
it and the user can edit it. Switching back to keep restores the lines the user did not edit.
Filters are pluggable per language (:func:register): a dictionary + rules filter for Russian now; another language or an LLM
rewrite later only needs a (text) -> text function.
register(lang, fn)
¶
Install fn as the soften filter for lang, replacing any previous one.
supported(lang)
¶
Whether lang has a soften filter.
soften(text, lang)
¶
Apply the soften filter for lang, or return text when that language has none.
soften_ru(text)
¶
Replace Russian mat with non-obscene wording, keeping each word's letter case.
apply_to_lines(lines, mode, lang)
¶
Soften (or restore) the translations; returns how many lines are softened now. Lines the user edited stay as they are.
project
¶
A dubbing project: one folder per film, resumable, with a cache per stage.
Layout::
<project>/project.json settings (source, languages, multi-voice, voices, volumes) + stage states (input hash, timing)
<project>/lines.json the script: one entry per line (time, original, translation, speaker, flags)
<project>/speakers.json characters and their voices
<project>/audio/ extracted soundtrack (16 kHz mono for analysis, 48 kHz stereo for the mix)
<project>/stems/ separated speech / background
<project>/tts/ synthesised lines (line_<id>.wav, cached by text+voice hash)
<project>/watch/ mixed chunks for Watch mode (chunk_<n>.wav, CHUNK_S seconds each)
<project>/out/ results (preview.wav, dub_track.wav)
Every write goes through a temp file with a unique name and os.replace (an interrupted run never leaves a half-written JSON).
A stage is skipped when it is marked done with the same input hash (:meth:Project.stage_fresh).
Line(id, start, end, text='', translation='', spoken='', speaker='S1', keep_original=False, kind='speech', source='', audio='', audio_s=0.0, place_start=-1.0, stretch=1.0, fit='', edited=False, tag='', softened='', review='')
dataclass
¶
Voice(kind='clone', id='')
dataclass
¶
How a character is spoken: a clone, a library voice, an actor blend, or automatic.
from_dict(d)
classmethod
¶
Build a voice from a JSON object. A missing object becomes a clone with an empty id.
Speaker(id, name='', voice=(lambda: Voice('auto'))(), ref_audio='', ref_text='', seconds=0.0, key=None, actor_weight=None)
dataclass
¶
One character: the chosen voice, the reference clip, and how much of the dialogue they speak.
from_dict(d)
classmethod
¶
Build a speaker from a JSON object. A blank or missing actor_weight stays unset.
Project(folder)
¶
One film's project folder: settings, stage cache, lines, and speakers.
source
property
¶
Film path stored in the settings.
create(folder, source, **settings)
classmethod
¶
Create a project in folder for source and save it. Extra keywords overwrite settings.
folder_for(source, root)
staticmethod
¶
<root>/<film name>-<short hash of the full path> - the same film always maps to the same (resumable) project.
path(*parts)
¶
Join parts under the project folder and create the parent directory.
rel(p)
¶
p relative to the project folder, with forward slashes. Raises ValueError when p is outside it.
abs(rel)
¶
Project path for a relative path stored in the project.
save()
¶
Write project.json, lines.json, and speakers.json.
stage_fresh(key, inputs)
¶
Whether stage key is marked done for this exact inputs hash.
mark_done(key, inputs, seconds, **extra)
¶
Mark stage key done for inputs and save. seconds is wall-clock time; extra keywords are stored on the stage.
invalidate(*keys)
¶
Drop the named stages from the cache and save.
line(line_id)
¶
The script line with this id, or None.
speaker(sid)
¶
The character with this id, or None.
dub_lines()
¶
Lines to synthesise: a translation is set, and the original is not kept.
merge_speakers(keep, absorb)
¶
Move lines from absorb onto keep, drop those speakers, and invalidate voices through mux.
reassign(line_id, speaker)
¶
Give one line another speaker and clear its synthesised audio. Creates the speaker when it is new.
is_key(sp)
¶
Key character (gets an actor-like voice in "auto"): set by the user, else >= key_share of all dialogue time.
voice_for(line)
¶
Voice for this line: the single project voice, or the speaker's voice when multi-voice is on.
atomic_write_text(path, text)
¶
Replace path with text through a temp file. Raises PermissionError when the destination stays locked.
write_json(path, data)
¶
Write data as UTF-8 JSON, replacing path atomically.
read_json(path, default=None)
¶
Parse a JSON file, or return default when it is missing or invalid.
write_json_gz(path, data)
¶
Reproducible gzip (mtime=0, no file name in the header): the same data gives the same bytes.
read_json_gz(path, default=None)
¶
Parse gzip-compressed JSON, or return default when the file is missing or invalid.
hash_of(*parts)
¶
First 16 hex characters of SHA-256 over the JSON form of each part.
file_fingerprint(path)
¶
size:mtime of path, or missing when the file cannot be stated.
review
¶
What deserves a look after the automatic run - shown as small, non-blocking badges on the Characters and Lines tabs.
Qt-free: returns {"characters": [(i18n key, params)], "lines": [...]}; an empty list means the tab needs no attention. Nothing here
ever stops a dub: the program always finishes with its best choice; the badges only point at what a person may want to check.
too_long_count(p)
¶
Lines that could not be fitted (after the dub: the time-fitting verdict; before it: the length estimate).
diarized(p)
¶
Whether diarization is marked done and its summary does not say it was skipped.
attention(p)
¶
Badges for the Characters and Lines tabs. An empty list means that tab needs no attention.
script
¶
Building the script: dialogue windows, lines from recognition or subtitles, translations, speakers, length estimates.
dialogue_windows(segments, merge_gap=0.6, pad=0.15, min_len=0.3, total=None)
¶
Merge VAD segments closer than merge_gap into windows, pad them; only these windows are separated and dubbed
(everything outside stays the original soundtrack - research decision 2, "fail-open").
overlap(a0, a1, b0, b1)
¶
Seconds shared by the intervals [a0, a1] and [b0, b1].
lines_from_asr(segments, max_len=8.0, lang=None, turns=None)
¶
ASR segments -> lines rebuilt from the words (:mod:dubber.core.segment): sentence ends, pauses > 0.6 s and speaker
turns (turns from diarization, when known) break a line; long sentences are split at their best pause/comma.
lines_from_cues(cues, target)
¶
Subtitle cues -> lines. target: the cue text is already the translation (subtitles in the dub language).
attach_text(lines, cues, attr)
¶
Fill attr (text / translation) of each line from the cues it overlaps most; returns how many got text.
speakers_from_tags(lines, min_cover=0.6)
¶
Speaker per line from subtitle speaker tags ("JOHN: ...") when most speech lines carry one (>= min_cover and at
least two names); untagged lines take the nearest tagged line before them (else after). {} = not enough tags.
snap_to_windows(lines, windows, max_move=0.6)
¶
Subtitle timing is approximate: snap line edges to the nearest speech window edge when it is close.
assign_speakers(lines, turns)
¶
Each line gets the diarization speaker that covers most of it (turns: {"start","end","speaker"}).
cluster_speakers(features, threshold=0.35, max_speakers=8)
¶
Agglomerative clustering (cosine, average linkage) of per-line voice fingerprints - the fallback when pyannote is not available. Returns a cluster index per row (0 = the most frequent).
syllables(text, lang)
¶
Syllable count for the length estimate: vowel groups in English and German, vowel letters otherwise. At least 1.
estimate_seconds(text, lang)
¶
Spoken length of text at a normal dubbing pace (+ pauses at punctuation).
too_long(line, lang, slack=1.15, next_start=None)
¶
Flag for the Script table: the translation will not fit its slot even after squeezing (shown before any synthesis).
A short interjection (Whoa!, Yes!, No., Wait., Boy.) may run into the following pause, up to min(next start - gap, end + 0.8 s), before it is stretched. It is flagged only when it would still overlap the next line at 1.25x. With no following line the bare slot is judged at that same 1.25x.
kept_share(variant, text)
¶
Share of the letters of text still in variant (1.0 = complete).
acceptable_variant(variant, text, min_keep=MIN_KEEP)
¶
Whether variant keeps min_keep of the letters of text, and two words unless text is shorter.
shorten(text, lang, min_keep=MIN_KEEP)
¶
Shorter variants of a translation, most conservative first: drop parentheticals, filler words, repeated words,
then the last clause. Only variants that keep >= min_keep of the letters and two or more words are returned, so the
meaning survives; the time fitter tries them only after shifting into pauses and stretching up to 1.15x.
is_short_for_context(line)
¶
A line Opus-MT tends to mistranslate on its own: under ~1.2 s, or three words or fewer.
breath_groups(lines)
¶
Consecutive lines of the same speaker with a gap under BREATH_GAP_S.
context_units(lines, todo_ids=None)
¶
Translation units. A short line is joined with the next line of its breath group (the previous, if it is last).
split_translation(sources, translated)
¶
Split translated back into one piece per source line, by sentence, in proportion to the sources.
None when the sentence counts do not match (the caller merges the short line into the next one).
fit_text_to_slot(text, lang, slot_s)
¶
The shortest acceptable wording of text that the estimate can say inside slot_s, or the shortest one.
prepare_for_voice(lines, lang, total=None)
¶
Shorten lines that will not fit their slot, then merge one that is still too long into the next breath.
lines is updated in place. Absorbed ids are removed from the list and returned. A second call does not
shorten a line whose estimate already fits, and does not merge it again.
merge_short_into_next(short, nxt, translation)
¶
One line spanning both, texts joined. Used when a joint translation cannot be split back apart.
translate_with_context(lines, translate, todo_ids=None)
¶
Translate lines that still need it. Short lines share one translation with their neighbour in the breath group.
translate receives source strings (a short line already joined with its neighbour) and returns one translation
each. When that translation does not split back into the same number of sentences, the short line is merged into
the next line.
segment
¶
Lines from recognised words: sentence boundaries + pauses + speaker turns, length capped at natural break points.
Speech recognition gives a stream of words with timestamps; its own segments are cut by its 30 s decoding windows, not by meaning, so they often end mid-sentence and run across two speakers (real case: a two-person dialogue came out as 4-8 s chunks like "...well i got a hot day" / "coming over later so good for you man and i know..."). Lines are rebuilt here from words:
- hard breaks: a pause longer than
hard_pause(0.6 s) or a change of speaker (diarization turns, when known) - a line never spans either; - sentence breaks: a word ending in
. ? ! …(not an abbreviation, "A.I." inside a sentence is not an end); - a piece still longer than
max_s/max_wordsis split at its best inner point: the longest pause, a comma, a conjunction, near the middle (recursively). Without punctuation (some recognisers drop it) the cap is lower and the split points are the pauses alone.
Junk tokens of the recogniser (letters of another script inside an English text, =# debris) are removed first.
clean_words(words, lang=None)
¶
Drop recogniser debris and glue stray punctuation or word pieces onto the previous word.
words_of(segments, lang=None)
¶
All words of the recognised segments, in time order (a segment without word timings becomes one pseudo word).
collapse_loops(words, max_repeat=3)
¶
A word repeated more than max_repeat times in a row is a decoding loop of the recogniser (real case: "B.I." x6 over a
laugh at the end of clip 1): the run is reduced to one word. "Yeah, yeah, yeah" (3) stays.
is_sentence_end(word, next_word)
¶
Whether word ends a sentence. Abbreviations and a dotted acronym before a lower-case word do not.
punctuated(words)
¶
True when the recogniser wrote sentence punctuation (at least one end mark per ~30 words).
impossible_rate(start, end, text)
¶
Whether text has at least six letters and more than 40 per second over [start, end].
tidy_punctuation(text)
¶
Stray marks the recogniser leaves (real case: "...at your place.).,"): unmatched brackets go, runs of marks shrink to one.
segment(words, turns=None, hard_pause=HARD_PAUSE, max_s=MAX_S, max_words=MAX_WORDS)
¶
(start, end, text) per line.
subtitles
¶
Subtitles: parsing (SRT / WebVTT / ASS), clean-up, song detection, sidecar search and the online services.
Search order (research note 05): subtitles inside the file -> files next to the film -> SubDL (the user's API key) ->
OpenSubtitles (the user's API key, optional login). The HTTP layer is injectable (http) so everything is testable offline.
Only text and timing are used; downloaded files stay inside the project folder.
Cue(start, end, text)
dataclass
¶
One subtitle cue.
Found(source, cues, label='')
dataclass
¶
Subtitles from one search hit: embedded, sidecar, subdl, or opensubtitles.
parse_srt(text)
¶
Cues from an SRT document. A block with a bad timestamp is skipped.
parse_vtt(text)
¶
Cues from a WebVTT document, parsed as SRT after the header.
parse_ass(text)
¶
Dialogue cues from an ASS or SSA document, in start-time order. Override tags are removed.
parse_any(text, name='')
¶
Parse ASS, WebVTT, or SRT, chosen from name and the start of text.
decode_bytes(data)
¶
Decode subtitle bytes as UTF-8, then Windows-1251, then Latin-1.
load_file(path)
¶
Read a subtitle file and parse its cues.
write_srt(cues, path)
¶
Write cues as a UTF-8 SRT file.
is_music(text)
¶
Song lyrics / music cue: the line keeps the original audio (decision: songs are never dubbed).
clean_text(text)
¶
Remove formatting tags, SDH descriptions [door slams], speaker labels JOHN: and dialogue dashes.
sdh_speaker(text)
¶
JOHN: ... -> JOHN (a hint for the speaker detection), else ''.
estimate_offset(cues, windows, search_s=8.0, step=0.1)
¶
Shift (seconds) that best aligns the cues with the detected speech windows (subtitles from another release are often offset by a constant). 0 when nothing is better.
shift(cues, offset)
¶
Copy of cues moved by offset seconds. Times are kept at or above zero.
lang_matches(name, lang)
¶
Whether name contains a language token for lang, including aliases such as rus.
sidecar_candidates(video, lang)
¶
Subtitle files next to the film (same stem first), the ones naming lang first; also a Subs/Subtitles folder.
default_http(method, url, headers, body=None, timeout=30.0)
¶
Send one HTTP request and return the body. timeout is seconds; urlopen errors propagate.
guess_title(video)
¶
The.Movie.2019.1080p.mkv -> ("The Movie", 2019, None); Show.S01E02 -> ("Show", None, (1, 2)).
subdl_search(api_key, video, lang, http=default_http)
¶
SubDL API v1 (api.subdl.com; its terms allow a user's own key inside an application).
opensubtitles_search(api_key, video, lang, http=default_http, username='', password='')
¶
OpenSubtitles REST API (api.opensubtitles.com): search, then /download for a link (login optional, raises quota).
find_online(video, lang, settings, http=default_http, log=lambda m: None)
¶
SubDL, then OpenSubtitles, with the user's keys from Settings; network errors only log.
timefit
¶
Time fitting of synthesised lines (research note 04): shift into pauses -> start a little earlier -> Signalsmith Stretch up to 1.15x -> (runner) shorter translation and best-of-N takes -> otherwise the line may run up to 1.25x and is flagged.
Placement(id, start, stretch, verdict)
dataclass
¶
Where one dubbed line sits: start in seconds, stretch factor, and a fit verdict.
needs_retry
property
¶
Whether the verdict is too_long and a shorter take should be tried.
is_short_interjection(line)
¶
Whoa!, Yes!, No., Wait., Boy.: a source slot of at most 1 s, or at most two words.
interjection_end(line, next_start)
¶
How far a short interjection may run before any stretch: min(next start - GAP, end + 0.8 s).
place(lines, seconds, total=None)
¶
Placement of every dubbed line (seconds: synthesised length by line id). Lines are processed in time order and never
overlap each other.
slot_for(line, lines, total=None)
¶
Room for a line: up to the next line (minus the gap) plus the early start allowance.
best_take(durations, slot, completeness=None)
¶
Best-of-N, meaning first: among the takes that fit slot the most complete wording (completeness: share of the
full translation kept, 1.0 = all; default all 1.0), then the longest (most natural pace). When none fits: the most complete
wording again, its shortest take - a full line read a little fast is better than a line that lost its meaning (the placement
may still squeeze it up to HARD_STRETCH and flags it).
voice_catalog
¶
Open voices published by Voxprint on Hugging Face (e.g. Mitroshenkov87/voxprint-voices-ru: Levi, Natan, Shimon, Miriam,
Rivka, Noa - CC0) - listed next to the local library and installed into the SHARED voice library, in the same format the
Audiobook Builder uses (<voxprint home>/voices/<id>/: LoRA adapter, ref_sample.wav, training_meta.json,
speaker_centroid.safetensors, voice.json with repo_id). Both programs then see the same voice once.
Repository layout: <id>.zip per voice, SHA256SUMS.txt (<sha256> <id>.zip) and <id>/voice.json for the catalog.
Download: lock file voices/.<id>.lock (same OS lock as the model store), .part resumed with HTTP Range, SHA-256 must
match before anything is extracted, only known file names are extracted, the folder appears atomically (rename).
VoiceCatalogError
¶
Bases: RuntimeError
Listing or installing a remote voice failed.
RemoteVoice(id, name, repo, sha256, language='', gender='', license='', description='', size_bytes=0)
dataclass
¶
One voice published as a zip in a Hugging Face repository.
url
property
¶
Hugging Face download URL of this voice's zip.
repos()
¶
Repository ids to list. A non-empty VOXPRINT_VOICE_REPOS replaces the built-in default.
parse_sums(text)
¶
Map voice id to lowercase SHA-256. Only <64 hex> <id>.zip lines are kept.
list_remote(repo, fetch=_fetch)
¶
Voices of one repository (network errors raise :class:VoiceCatalogError).
installed_ids(root=None)
¶
repo_id (else folder name) -> folder, for voices already in the shared library.
extract_voice(zip_path, dest)
¶
Only the known files, flat (a single wrapping folder is accepted); no paths outside dest.
install(voice, root=None, progress=lambda f: None, cancel=lambda: False, downloader=None)
¶
Download, verify and add one voice to the shared library; returns its folder (an installed voice is not downloaded again).
voices
¶
Voices: the shared Voxprint voice library (read-only) and reference clips cloned from a character's own lines.
The library is the one of Voxprint AI Audiobook Builder: <Voxprint home>/voices/<id>/ with a LoRA adapter
(adapter_model.safetensors + adapter_config.json), ref_sample.wav (+ its text in training_meta.json
ref_sample_text) and voice.json (name, language, gender ...). The dubber never writes there.
A library voice is used with its adapter on the Qwen3-TTS base model it was trained on (voice.json base_model).
LibraryVoice(id, name, path, language='', gender='', base_model='', license='', ref_text='', adapter_scale=1.0, repo_id='')
dataclass
¶
list_library(root=None)
¶
Usable voices of the shared library (complete adapter + reference clip), sorted by name. Missing folder -> [].
default_single_voice(target_lang, root=None)
¶
The library voice used when one voice reads the whole film and the user chose none: a voice in the dub language with a reference clip (first by name; one recorded for this language beats a multilingual one). None -> clone the film's voice.
get_library_voice(voice_id, root=None)
¶
The installed voice whose folder name is voice_id, or None.
pick_reference_lines(lines, speaker, max_s=REF_MAX_S, wav=None, sr=24000)
¶
Lines for the cloning reference: one voice, longest first, up to max_s.
The anchor is the longest candidate. A line stays only when its speaker embedding is at least REF_SIM_MIN
(cosine) from that anchor. Lines of REF_PREF_S (3 s) and more are preferred. A line of 1.5-3 s is used only
when it matches the anchor and is not a shout, and only to reach REF_MIN_S — the 15 s budget is left
unfilled rather than filled with short or shouted lines. Mixing two voices is never worth those 5 s.
speaker None = any speaker (single-voice mode clones the main character). wav is the speech stem;
without it every candidate is treated as the anchor's voice (tests that only check duration).
build_reference(lines, speech_wav, out_wav, sr=24000, bounds=None)
¶
Concatenate the chosen lines from the separated speech stem -> out_wav; returns (seconds, joined text).
Each chunk is padded by REF_PAD_S on both sides (clamped to the neighbouring line and to the file) and faded
in and out over REF_FADE_S so the join does not click. REF_GAP_S of silence stays between chunks.
bounds are the lines whose edges limit the pad (the whole script); omitted, only lines themselves.
reference_log(n_lines, seconds, status)
¶
One log line for a built reference, e.g. reference: 2 lines, 12.3 s, check OK.
assess_reference(lines, joined, hypothesis)
¶
Compare a re-transcription of the reference with the joined line text.
Returns (ok, drop_index, ref_text). ok is false when the normalised word error rate is above
REF_WER_MAX or the hypothesis starts with words the reference text does not. drop_index is then the
worst chunk (the first chunk when those extra leading words are there). ref_text is the re-transcription
when it is the closer match, otherwise the joined line text.
vxdub
¶
The .vxdub project file: one dub in a ZIP, without the video.
The container follows the ODF/EPUB rule: the first entry is an uncompressed mimetype
file whose bytes are exactly :data:MIME. The other members are JSON documents plus
optional voice-reference clips under assets/. See docs/formats/VXDUB.md.
A save writes a temporary file in the destination directory and publishes it with
os.replace, so a crash never leaves a half-written project under the real name.
Opening a file again keeps JSON keys and zip members this version does not know.
VxdubError
¶
Bases: Exception
A project file cannot be read, linked to its video, or written.
SchemaTooNew(found)
¶
SourceMissing
¶
Bases: VxdubError
The video is not at a saved path and was not located.
FingerprintMismatch()
¶
Document(manifest, transcript, translation, voices, job, assets=dict(), extras=dict())
dataclass
¶
One project file in memory. extras and unknown JSON keys are kept for the next save.
utc_now()
¶
Current time as ISO 8601 UTC with a Z suffix and whole seconds.
fingerprint(path)
¶
SHA-256 of the first 16 MiB, the last 16 MiB, and the size.
A file of at most 16 MiB is hashed twice (the whole file is both the head and the tail).
A longer file seeks to size - 16 MiB for the tail, so a file shorter than 32 MiB
overlaps the head. The size is an unsigned 64-bit big-endian integer after those bytes.
sha256_file(path)
¶
SHA-256 of every byte. Optional in the manifest; a movie is not hashed unless asked.
relative_hint(video, package)
¶
Path of the video relative to the project file's directory, or "" when there is none.
read(path)
¶
Read a project file. Raises :class:SchemaTooNew when schema is newer than :data:SCHEMA.
resolve_source(source, package, locate=None)
¶
Find the video by absolute path, then by the relative hint, checking the fingerprint.
When neither candidate matches, locate is called with the saved absolute path and the
file it returns is accepted only when the fingerprint matches.
open_project(path, root, locate=None)
¶
Open a project file, relink its video, and load it into the local project folder.
materialise(doc, video, root)
¶
Write the document into the project folder for video.
The folder is the same one a dub of this path already uses, so a save on this computer
resumes the cached stages. A new folder (the video was relinked to a different path)
keeps the script but clears done: the stage outputs are not inside the .vxdub.
write_project(project, dest, *, previous=None, include_sha256=False)
¶
Write project to dest. An existing .vxdub keeps unknown keys and members.
previous is the document already in memory (for example the file the user opened).
When it is omitted and dest is already a project file, that file is read first.
A newer schema is never overwritten.
watch
¶
Watch mode logic (no Qt): when the "Watch" button becomes available, and when the player must wait for the dubbing.
Decision (2026-10-08): not strict real time. The first READY_AHEAD_S (5 min) are dubbed before Watch becomes available; the
user starts it by hand; the dubbing keeps running ahead; if playback catches up with the dubbed part the player pauses ("buffering")
and resumes once RESUME_AHEAD_S are ready again (or the film is finished).
WatchState(total, dubbed_until=0.0, finished=False, playing=False, buffering=False)
dataclass
¶
Playback position against how far the dub has been mixed.
ready
property
¶
The Watch button is enabled.
update(position, dubbed_until, finished=False)
¶
Feed the player position and the dubbing progress; returns the action for the player: play | pause | none.
start()
¶
Begin playback when enough of the film is dubbed. Returns False while Watch stays disabled.
stop()
¶
Stop playback and clear buffering.
diag
¶
Built-in diagnostic / benchmark: collects facts about the machine and times every pipeline model, then writes a plain-text report.
checks_system
¶
In-process checks that need no heavy libraries (the GUI process never imports torch): OS, hardware, power, disk, Python and package
versions, network reachability, ffmpeg, nvidia-smi and the Windows video controllers. Each function returns a :class:CheckResult;
:func:dubber.diag.runner.guarded wraps them so an exception cannot stop the report.
check_app()
¶
Record the program version, data folders, registered model users, and whether the shared GPU lock is held.
check_os()
¶
Record the operating system and Python build, and warn when Windows is older than the supported build.
check_hardware()
¶
Record CPU, memory, and power source, and warn on battery power or under 6 GB of free RAM.
check_disk(extra=None)
¶
Record free space for the models, temp, and desktop folders, and warn when models or temp have under 12 GB free.
check_packages()
¶
List installed versions of the tracked packages and name the ones that are absent, without importing them.
check_network(timeout=5.0)
¶
Reach the places models come from. Only a TCP connect + HEAD request; nothing is uploaded.
check_ffmpeg()
¶
Locate ffmpeg and ffprobe and record encoders, build licence, and hardware acceleration, failing when ffmpeg is missing.
query_nvidia_smi()
¶
Static facts from nvidia-smi: name, compute capability, driver branch, VRAM, power and clocks. {} when absent.
check_gpu_smi()
¶
Record NVIDIA name, compute capability, driver branch, and VRAM, failing the check when no qualifying GPU is found.
check_ctranslate2()
¶
CTranslate2 (faster-whisper) must see a CUDA device. The CUDA 12 DLLs are registered first.
check_gpu_wmi()
¶
Windows: every video controller (hybrid ROG laptops have an integrated GPU + the NVIDIA one).
clip_stages
¶
Stages of the diagnostic run on the bundled test clip (extract, timing plan, mix, mux) - the product pipeline is
dubber.pipeline (project folders, caching, Watch mode); this small set measures the same operations on a few seconds of audio.
StageContext(source, target_lang, work, out=None, log=lambda m: None, data=dict())
dataclass
¶
Shared state of one dubbing run.
StageResult(key, ok, message='', seconds=0.0)
dataclass
¶
Outcome of one diagnostic clip stage.
Stage
¶
One step of the diagnostic clip. Subclasses set key and implement :meth:run.
run(ctx)
¶
Run this stage and store its outputs on ctx. The base stage raises NotImplementedError.
ExtractStage
¶
FitStage
¶
MixStage
¶
MuxStage
¶
fit_plan(slots, lines)
¶
For each line: ratio = synthesised seconds / slot seconds. <=1 fits; <=1.15 can be squeezed; more = translation must be shortened.
mix_track(original_wav, slots, lines, out_wav, duck_db=-14.0)
¶
Overlay the dubbed lines on the original soundtrack; the original is ducked by duck_db under each line (50 ms ramps).
This version keeps the original under the dub (a later version will use the separated background stem instead). Lines longer than their slot are placed at the slot start and may run into the next pause (fit/stretch comes later).
constants
¶
Fixed inputs of the benchmark (so that reports from different runs/machines are comparable).
test_clip_dir()
¶
Folder of the bundled synthetic clip (assets/test_clip).
procs
¶
Run a heavy step in a separate process ("worker") and watch it.
Why a process per model: a crash (access violation in a CUDA DLL, driver reset, out-of-memory kill) or a hang kills only the worker; the parent turns that into a FAIL entry with the exit code and the tail of stderr, and the report goes on. A process also gives clean VRAM numbers (everything is freed when it exits) and avoids the transformers-version conflicts between libraries. It is also the architecture decided for the product (research note 06, section 3).
Protocol: the worker prints lines @@VX@@ {json} on stdout (anything else on stdout is ignored, stderr is kept as a log):
{"t":"log","msg":...} progress text, {"t":"result", ...} the final result (status/summary/details/metrics/traceback).
GpuSampler(interval=1.0)
¶
Bases: Thread
Polls nvidia-smi once a second while a worker runs; :meth:summary gives max/min values. Silent if nvidia-smi is absent.
run()
¶
Poll nvidia-smi about once a second until :meth:stop is called.
stop()
¶
Stop sampling and wait up to 12 seconds for this thread to finish.
summary()
¶
{"n": samples, "util_max", "mem_used_max_gb", "power_max_w", "temp_max_c", "sm_clock_min/max", "throttle": text}.
describe(s)
staticmethod
¶
One line for the report, e.g. GPU during step: util max 92%, VRAM max 7.1 GB, ... ('' when nothing was sampled).
WorkerOutcome(name, result=None, returncode=None, timed_out=False, cancelled=False, seconds=0.0, stderr_tail='', gpu=dict(), start_error='')
dataclass
¶
What came back from one worker process.
worker_command(name, args_file)
¶
Command line that starts worker name (frozen exe: exe --worker ...; source: python main.py --worker ...).
find_nvidia_smi()
¶
nvidia-smi from PATH or its usual Windows locations.
decode_throttle(mask)
¶
Human text for the nvidia-smi throttle-reason bit mask (0x0 -> none).
describe_exit_code(rc)
¶
Readable meaning of a worker exit code (Windows NTSTATUS values appear as negative/large numbers).
run_worker(name, args, timeout=600.0, env=None, on_log=None, cancel=None, sample_gpu=True, command=None)
¶
Start worker name with args and wait for it (at most timeout seconds). Never raises.
report
¶
The diagnostic report: data model, plain-text renderer, redaction and a crash-safe writer.
Design goals¶
- Readable and pasteable: ASCII layout (the only non-ASCII text is real data such as a Russian test phrase), fixed-width tables, a SUMMARY block first, details below, all tracebacks collected at the end.
- One failing check never stops the report: every check yields a :class:
CheckResult; exceptions become FAIL entries carrying the traceback (see :func:dubber.diag.runner.guarded). - Crash-safe: the runner re-saves the file after every check (atomic replace), so even if the program, the GPU driver or
Windows dies half-way the Desktop already has a report saying
INCOMPLETEand everything measured so far. - No secrets / personal data: :func:
redacthides Hugging Face tokens, bearer tokens and the Windows user name.
Status
¶
Bases: str, Enum
Outcome of one check.
CheckResult(id, title, status=Status.INFO, summary='', details=list(), seconds=None, traceback='', metrics=dict())
dataclass
¶
Report(app_line='', options=dict(), header=list(), results=list(), notes=list(), started=time.time(), finished=None, complete=False, max_traceback_lines=60, secrets=list(), current_step='')
dataclass
¶
A diagnostic report under construction. Thread-compatible when only one thread calls :meth:add.
add(result)
¶
Add a result (a result with the same id replaces the old one, keeping its position).
get(check_id)
¶
Return the check with this id, or None when it has not been added.
note(text)
¶
Append a note printed under the summary totals.
finish(complete=True)
¶
Record the finish time. complete False leaves the rendered state as INCOMPLETE.
counts()
¶
Count checks for each status, including statuses that did not occur.
render()
¶
The whole report as one string (already redacted).
redact(text, extra_secrets=())
¶
Hide tokens and the user name in text (applied to the whole report just before it is written).
fmt_seconds(s)
¶
0.42 s / 12.3 s / 3 min 05 s / -.
fmt_gb(x)
¶
Gigabytes with one decimal, - when unknown.
fmt_num(x, digits=2)
¶
Format x to digits decimal places, or - when x is missing.
table(headers, rows, indent=' ')
¶
Fixed-width text table (left aligned, two spaces between columns).
key_numbers(results)
¶
The few lines worth pasting into a chat: GPU, TTS speed per mode, per-stage times. Built from CheckResult.metrics.
report_filename(ts=None)
¶
Voxprint-MovieDubber-Diagnostics-20261004-143012.txt
write_text_atomic(path, text, retries=3)
¶
Write text (UTF-8) to path through a temp file + os.replace; raises OSError after retries failures.
save_report(report, primary, fallback_dir=None)
¶
Write the rendered report to primary; if that fails, to fallback_dir under the same name. Returns the real path.
runner
¶
The diagnostic runner: plans the checks, runs each one guarded, saves the report after every step.
Order (cheap and safe first, expensive last; the report file is rewritten after every step, so a crash keeps everything measured so far):
- system facts (OS, hardware, disk, packages, network, ffmpeg), nvidia-smi, Windows adapters
gpu.torch: PyTorch/CUDA, FlashAttention, CUDA Graphs micro-tests (worker process)models.fetch: make sure the models are on disk (download time reported separately from load time)tts.*: Qwen3-TTS speed in 4 modes (standard/graphs x SDPA/FlashAttention-2), one worker process eachstage.*: the pipeline on the bundled test clip: extract -> VAD -> separation -> ASR -> diarization -> translation -> TTS -> fit -> mix -> mux
DiagOptions(allow_download=True, quick=False, target_lang='ru', hf_token='', clip=None, out_path=None, tts_model='tts_1_7b', tts_modes=ALL_TTS_MODES, skip=(), cpu_tts=False, cpu_sep=False, asr_repo='', timeout_scale=1.0)
dataclass
¶
What to run. Model downloads are anonymous; hf_token is unused and stays out of the report.
as_report_options()
¶
Header fields for the report. The token is recorded only as given or none.
Step(id, title, fn, weight=1.0, group='misc')
dataclass
¶
One diagnostic check in the run plan. group is the skip name DiagOptions.skip drops before it runs.
DiagnosticRunner(options=None, on_progress=None, on_result=None)
¶
Runs the diagnostics; thread-safe to cancel from another thread (cancel()).
cancelled
property
¶
True after :meth:cancel has been called.
cancel()
¶
Stop before the next check and cancel a worker that is still running.
run()
¶
Run everything, write the report and return its path (never raises).
result_from_outcome(check_id, title, out)
staticmethod
¶
Turn a finished, crashed, timed-out, or cancelled worker into a check result, and warn when a finished worker then exits non-zero.
guarded(check_id, title, fn)
¶
Run one check; an exception becomes a FAIL result with the traceback (the report goes on).
engines
¶
Model engines behind small interfaces; each has a light stand-in so the whole pipeline runs (and is tested) on a CPU.
============== =========================================== ========================================== stage real engine stand-in (CPU, no model) ============== =========================================== ========================================== VAD Silero VAD frame-energy detector separation TIGER-DnR / Mel-Band RoFormer "none": no stems, the original is ducked ASR faster-whisper large-v3-turbo "none" (subtitles only) / "mock" diarization pyannote community-1 (ungated mirror) MFCC clustering of the lines translation Opus-MT (Marian) "mock": marks the text TTS Qwen3-TTS (FA2 -> CUDA Graphs -> SDPA) "mock": speech-like tone of the right length ============== =========================================== ==========================================
asr
¶
Speech recognition with faster-whisper on the separated speech stem (word timestamps).
Punctuation matters: lines are rebuilt from sentence ends (:mod:dubber.core.segment). Without context Whisper sometimes writes
a lower-case stream without any punctuation (real case: a 93 s dialogue); a short punctuated prompt in the film's language and
condition_on_previous_text keep the punctuation for the whole film. The language is detected first so the prompt is in
the film's language, and the temperature fallback stops at 0.4 (higher temperatures produced stray tokens such as "Wellсем"
or "Choi443buz"). Measured on both user clips (CPU int8): every sentence punctuated, no stray tokens, no lost sentence, and
not slower than the old settings. If the text still comes back unpunctuated, one more pass with a different prompt is tried
and the better-punctuated result is kept.
Holes: with condition_on_previous_text Whisper can jump over a stretch of speech (real case, clip 2 without separation:
nothing between 9.3 s and 24.2 s). Speech found by the VAD but not covered by any recognised word for 2 s or more is a hole;
then one pass without the previous-text condition is made and its words inside the holes are taken.
AsrCudaFallback
¶
Bases: RuntimeError
faster-whisper / CTranslate2 would run on the CPU while an NVIDIA GPU is available.
cuda_gpu_present()
¶
True when this machine has an NVIDIA GPU, even if this process's PyTorch build is CPU-only.
ensure_cuda_asr(requested, resolved)
¶
Stop when recognition would use the CPU while a CUDA GPU is present.
An explicit cpu request is kept (the user asked for it). A resolved CUDA device is kept too.
is_cuda_fallback_failure(message)
¶
True when a pipeline error is the hard stop above (the window shows a warning for it).
decode_options(language, retry=False)
¶
Build one faster-whisper pass: word timestamps, temperatures from 0.0 to 0.4, and a punctuated prompt. retry uses the alternate prompt when one exists.
find_holes(speech, segments, min_s=HOLE_MIN_S, pad=0.3, bridge=1.0)
¶
Stretches of VAD speech (seconds) that no recognised word covers (words count pad seconds wider).
fill_holes(segments, extra, holes, margin=0.3)
¶
Words of extra (a second pass) whose middle lies in a hole become new segments; returns (segments, words added).
load_whisper(folder, device)
¶
The faster-whisper model itself (no transcription). CUDA uses float16; the CPU uses int8.
load_whisper_cached(device, role, allow_download, log)
¶
Load (or reuse) the ASR model bound to role (dubber.model_resolver). The resident cache keeps it for the next clip.
allow_download and log are kept for callers; the app never downloads (VOXPrint MDL does).
transcribe_faster_whisper(wav16, language, role, device, allow_download, log, requested=None)
¶
Transcribe a 16 kHz WAV with the model bound to role, recover skipped speech, and retry once if punctuation is missing.
Raises:
| Type | Description |
|---|---|
ModelUnavailable
|
The ASR model is not on disk (checked before any GPU work, so it is not mistaken for a CUDA problem). |
AsrCudaFallback
|
An NVIDIA GPU is present but recognition would run on the CPU. |
diarization
¶
Who speaks when: the ungated pyannote community-1 mirror, or MFCC clustering per line.
rewrite_pipeline_config(text, folder)
¶
Point gated Hugging Face ids in a pipeline config at local files under folder.
$model/... is left unchanged. pyannote binds that prefix to the pipeline directory,
which is the diarization mirror, so embedding, segmentation and PLDA load from that folder.
The checksummed config.yaml is not modified by this function.
pipeline_checkpoint(folder)
¶
Directory or rewritten config to pass to Pipeline.from_pretrained.
When the config already uses $model/..., the directory is returned and pyannote
resolves those subfolders locally. A gated repo id is rewritten into config.local.yaml
in the same folder; config.yaml itself stays byte-identical for the checksum.
load_diar_pipeline(folder)
¶
Load the community-1 pipeline from the local mirror. No Hugging Face token is sent.
pyannote.audio is imported here because a partial install does not have it.
pyannote_turns(wav16, device, allow_download, log, num_speakers=None)
¶
Diarize a 16 kHz WAV with pyannote and return each turn's start, end, and speaker, in seconds. num_speakers asks for exactly that many when set.
try_speaker_embeddings(segments, sr)
¶
One pyannote speaker embedding per waveform, or None when that model is not available.
The reference picker uses this and falls back to an MFCC + pitch vector, so tests run without the model.
cluster_lines(lines, speech_wav, threshold=0.35)
¶
Fallback: one MFCC fingerprint per line, clustered; returns line id -> S1, S2... (S1 = most lines).
separation
¶
Speech / background separation on the dialogue windows only (outside them the original is used as is).
chunk_batch_size(total_gb, budget_gb=None)
¶
How many separation chunks to run at once. 4 on a 16 GB card, 8 on a 24 GB card.
A tight VRAM budget (under 3 GB) halves that. Cards below the 16 GB class take fewer. This sizes RoFormer copies. TIGER-DnR is always one block: its forward indexes tracks, not items.
stems_are_per_item(dialog, effect, music, n_items)
¶
True when each stem's leading axis is one row per mixture, not TIGER's track axis.
TIGER-DnR squeezes a batch of 1 into [ntrack, nch, T] (ntrack is 3) and forward
indexes that axis. A stack of N mixtures still comes back track-major, so stem[i] reads
a track: N greater than 3 raises IndexError, and N of 3 or less silently swaps stems.
A real batch has leading size n_items. When n_items is 3 that shape is also the
track axis, so a leading 3 is not treated as a batch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dialog
|
object
|
Dialogue stem, array or tensor. |
required |
effect
|
object
|
Effects stem. |
required |
music
|
object
|
Music stem. |
required |
n_items
|
int
|
How many mixtures were stacked. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when it is safe to index each stem with the mixture index. |
separate_with_forward(forward, segs)
¶
Separate each segment. Stack only when the model returns one row per item.
A stacked call is kept only when :func:stems_are_per_item accepts it. Anything else,
including an exception from the model, is separated one segment at a time. TIGER-DnR
never passes the check, so it cannot swap stems or raise on the track axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward
|
Callable
|
|
required |
segs
|
Sequence[ndarray]
|
One waveform per mixture. |
required |
Returns:
| Type | Description |
|---|---|
List[Tuple[ndarray, ndarray]]
|
|
safe_compute_dtype(cuda, bf16)
¶
Network dtype. Half precision only as bf16 on CUDA. RTX 40 / Ada and newer always have bf16.
Callers cast the waveform back to fp32. The half-precision math stays inside the network.
separate_windows(mix44, windows, out_speech, out_bg, model_fn, log=lambda m: None, batch_size=1)
¶
Run model_fn(segment, sr) -> (speech, background) on each block; stems are full length (silence outside blocks).
batch_size blocks are computed together when model_fn has separate_batch. Copies into the stem
buffers run on a side thread, and the final files are written there too, so the next batch is not waiting on disk.
build_tiger(device, allow_download, log)
¶
Construct TIGER-DnR (not cached). Half precision stays inside the network; waveforms come back as fp32.
One block at a time. wav_chunk_inference squeezes a batch of 1 into [ntrack, nch, T]
and forward indexes tracks. A stack of N blocks is still track-major, so a batched call
swaps stems or raises. There is no separate_batch on the returned function.
tiger_model(device, allow_download, log)
¶
TIGER-DnR (Apache-2.0 weights, vendored MIT code): dialogue vs effects+music at 44.1 kHz.
The resident cache keeps the network loaded across stages and clips while it fits in the VRAM budget.
roformer_model(device, allow_download, log, model_file='vocals_mel_band_roformer.ckpt')
¶
Mel-Band RoFormer (MIT, KimberleyJSN/melbandroformer at the 2026-04-22 relicence) through audio-separator.
The checkpoint is the pinned file in the shared model store. audio-separator still loads its own yaml for that filename
and skips the weight download when the file is already there. device is unused: the package picks the device.
translation
¶
Offline translation with Opus-MT (Marian models through transformers).
Direct pairs among en/ru/de are en<->ru, en<->de and ru<->de. A pair of two non-English languages with no direct model still pivots through English.
Completeness first (real case: "Yeah, that sounds cool! Learning sucks!" came back as "Да, звучит круто!" and "...Destroy it all.
Destroy." lost "Destroy"): Marian models tend to drop a short trailing sentence when given several at once. So every line is
split into sentences, all sentences are translated in one batch, and each result is checked (:func:suspicious: empty, much
shorter than the source, or left untranslated). Suspicious sentences are retried (greedy decoding, then clause by clause) and
the most complete candidate wins. The sentences of a line are then joined again.
OpusMT(src, tgt, device, allow_download, log)
¶
One Opus-MT model loaded for a single language pair. Raises RuntimeError when no model exists for that pair.
split_sentences(text)
¶
Split text on the same sentence-ending tokens the script uses.
suspicious(src, out, tgt='')
¶
True when out looks like an incomplete (or missing) translation of src.
complete_translate(texts, fn, tgt='', retry_fn=None, log=lambda m: None)
¶
Translate texts sentence by sentence with fn (one batch); retry suspicious sentences. Returns
(translations, number of sentences that needed a retry).
source_has_ai(text)
¶
True when text says AI / A.I. / A. I. as a word (not inside AIM, not "A. I. Surname").
protect_ai(text)
¶
Replace the acronym with AI_PLACEHOLDER so Opus-MT copies it instead of guessing.
restore_ai(text, tgt)
¶
Put the acronym back: "ИИ" for Russian, "AI" otherwise. Inflected leftovers (XQZ1а) count too.
lowercase_mid_title(text)
¶
Lowercase mid-sentence Title Case common nouns ("Artificial Intelligence" -> "artificial intelligence").
The first word and a word after .!? stay, as do ALL CAPS and a small proper-noun list. Opus-MT otherwise
reads the capitals as a name ("Искусственная Разведка" for Artificial Intelligence).
collapse_letter_runs(text)
¶
Collapse a run of 3 or more of the same letter to 2 ("Ewwwww." -> "Eww.") before MT sees it.
drop_latin_leftovers(text, source, tgt)
¶
Drop Latin-only tokens a Cyrillic translation kept ("Фу, www." -> "Фу."), except names and brands from the source.
prepare_mt(text)
¶
Source text as Opus-MT should see it: letter runs collapsed, AI protected, mid-sentence capitals lowered.
finish_mt(text, source, tgt)
¶
Translation after Opus-MT: AI restored, then stray Latin tokens removed for a Cyrillic target.
opus_translate(texts, src, tgt, device, allow_download, log, batch=16)
¶
Translate lines with Opus-MT, retrying incomplete sentences and pivoting through English when no direct model exists. Raises RuntimeError when the pair cannot pivot.
mock_translate(texts, src, tgt)
¶
Stand-in for tests and CPU-only trials: keeps the text, marks the language.
tts
¶
Qwen3-TTS synthesis (logic taken from Voxprint AI Audiobook Builder core/tts_engine.py and core/narration.py).
Backend order on an NVIDIA GPU: CUDA Graphs via faster-qwen3-tts (MIT) first, then FlashAttention-2 if that
package exists, then standard SDPA. Graphs is also used for a library adapter or an actor blend when the installed
package accepts a voice-clone prompt; otherwise the standard backend is used and the reason is logged (a missing
package, not the triton not found warning). auto tries Graphs first; the batched standard backend is used when
tts_backend is standard/sdpa (or standard/flash_attention_2) and stays the fallback when Graphs cannot load. Clip 3
(16 short lines) measured graphs/sdpa at 67.5 s against batched standard/sdpa at 208.9 s. On the CPU: SDPA. Every step falls back
to the next one if loading fails.
Batching: CUDA Graphs stay the default and synthesise one line at a time (static shapes). The batched standard backend is used
only for a group of at least four long lines (about 8 seconds or 120 characters), sorted by length. Up to MAX_BATCH (12)
lines per generate call, sized by the VRAM budget in dubber.infra.resources (free memory minus a headroom, re-measured
before each batch; about 1.2 GB per batched line). Out-of-memory halves the batch.
Library voices (LoRA adapters) stay loaded side by side while the budget allows, else they are swapped one after another.
Voices: clone = reference clip + its text (ICL; without a text the x-vector-only mode is used); library = a LoRA adapter
from the shared Voxprint voice library, applied to the talker with peft (several adapters can be loaded and switched).
SynthesisTimeout
¶
Bases: RuntimeError
One generate call produced no result before its wall-clock limit.
The call itself may still be running: a CUDA kernel cannot be cancelled from this thread. The queue then uses one line at a time, or stops when a single line does not finish.
VoiceSpec(key, kind, ref_audio='', ref_text='', adapter_dir='', adapter_scale=1.0, actor_weight=0.0, actor_ok=True, gender='', candidates=(), record_dir='')
dataclass
¶
How one speaker is synthesized: a clone clip, a library adapter, or an actor blend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
|
required |
ref_text
|
str
|
Empty selects x-vector-only cloning. |
''
|
adapter_scale
|
float
|
LoRA strength. New library voices use 0.5; older ones use 1.0. |
1.0
|
actor_weight
|
float
|
Share of the actor's timbre; the rest is the closest library voice. |
0.0
|
actor_ok
|
bool
|
False when the actor clip is not long or clean enough, so the library voice is used as is. |
True
|
record_dir
|
str
|
Folder where the chosen blend is written, or empty to skip that record. |
''
|
tag()
¶
Return a 12-character hash of the voice files and settings for the synthesis cache key.
BaseTTS
¶
Shared speech engine that resolves an actor voice once, then halves the batch when synthesis runs out of memory.
resolve(voice)
¶
An "actor" voice becomes a concrete one (library adapter + blended embedding, or a fallback) - once per voice.
actor_embedding(voice)
¶
Speaker embedding for an actor clip, or None when this engine cannot compute one.
synthesize_batch(texts, voice, seed=None)
¶
Synthesize texts in one voice and return one waveform per text. Subclasses implement this; the base raises NotImplementedError.
max_batch()
¶
Maximum lines in one generate call. This base engine returns 1.
synthesis_timeout(texts)
¶
Seconds to wait for one generate call. 0 waits without a limit (the mock engine).
close()
¶
Release model memory. This base engine holds none.
run_queue(items, voice, on_done)
¶
Synthesize items (id, text) for one voice in batches; OOM halves the batch size; a failing batch goes one by one.
MockTTS(lang='ru', rate=1.0)
¶
Bases: BaseTTS
Speech-like tone with the length a speaker would need (deterministic per text): lets the whole pipeline run on a CPU.
QwenTTS(base_dir, language, device='auto', prefer='auto', need_adapters=False, log_fn=lambda m: None)
¶
Bases: BaseTTS
Qwen3-TTS Base (voice cloning). auto tries CUDA Graphs, then FA2, then SDPA.
graphs
property
¶
True when the loaded backend captures CUDA Graphs.
candidates(use_cuda, prefer='auto', need_adapters=False)
staticmethod
¶
Backends to try, first choice first.
auto on CUDA puts graphs/sdpa first when faster-qwen3-tts is installed. A film that needs a
library adapter or an actor blend still tries Graphs when :func:graphs_accepts_prompt is true; otherwise
Graphs is left out and :func:graphs_skip_reason says why. Clip 3 (16 short lines) took 67.5 s on Graphs
and 208.9 s on batched standard/sdpa. The batched backend is selected when prefer is standard/sdpa
and remains the last fallback when Graphs cannot load.
max_batch()
¶
Lines per generate call: 1 with CUDA Graphs, otherwise what free VRAM or RAM allows, at most batch_cap.
synthesis_timeout(texts)
¶
Seconds to wait for one generate call before treating it as stuck.
About twelve times the estimated speech, and at least 60 seconds. A missing
end-of-speech token used to run until max_tokens_for with no log line.
actor_embedding(voice)
¶
The actor's x-vector from his reference clip (the model's own speaker encoder).
synthesize_batch(texts, voice, seed=None)
¶
Synthesize texts in the resolved voice and return one float32 waveform per line. seed reseeds PyTorch so a retry can differ. Raises RuntimeError when the weights are not loaded.
close()
¶
Drop the model and cached voice prompts, then release CUDA memory.
adapter_name(key)
¶
PEFT / torch module name for a voice key.
Torch module names allow letters, digits, and underscore. A short hash of the original key is appended so two keys that sanitize to the same text do not share one adapter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
Voice key, which may contain |
required |
Returns:
| Type | Description |
|---|---|
str
|
A module name of at most 120 characters. |
graphs_accepts_prompt()
¶
True when the installed CUDA Graphs backend can take a voice-clone prompt.
Library adapters and actor blends need that prompt. A package that only accepts
ref_audio / ref_text cannot apply them, so Graphs stays off for those films.
The import stays inside this function: faster-qwen3-tts is optional and missing
on a machine that has not installed it.
Returns:
| Type | Description |
|---|---|
bool
|
True when |
graphs_skip_reason(use_cuda, need_adapters)
¶
Why CUDA Graphs is not the dialogue backend, or an empty string when it should be tried.
triton not found from torch is unrelated: Graphs does not need Triton, and Windows has
no official Triton wheel. A missing faster-qwen3-tts package, or a build that cannot
take a voice-clone prompt while this film uses an adapter or an actor blend, is why the
standard backend is selected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
use_cuda
|
bool
|
Whether this process can run on an NVIDIA GPU. |
required |
need_adapters
|
bool
|
True when a line uses a library adapter or an actor blend. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A sentence for the log, or |
max_tokens_for(text)
¶
Upper bound of codec frames (without it a missing end-of-speech token babbles for minutes).
next_group(queue, texts, limit)
¶
Take the next batch off queue (in place): limit texts of similar length from a look-ahead window, always containing
the earliest pending item, so the order stays close to the film order.
is_long_line(text, seconds=None)
¶
A line long enough to batch: about 8 seconds of speech, or 120 characters.
next_dialogue_group(queue, texts, limit, seconds=None, graphs=False)
¶
Next synthesis group. Graphs, a batch of one, or fewer than four long lines: the earliest line alone.
Otherwise up to limit long lines, shortest first. Short dialogue stays one line even on the standard backend.
is_oom(exc)
¶
True when exc is an out-of-memory error from CUDA or from the process.
set_lora_scale(peft_model, adapter, scale)
¶
Strength of one LoRA adapter on every layer (peft LoraLayer.set_scale: scaling = scale x alpha / r).
load_centroid(folder)
¶
The voice's averaged speaker embedding (speaker_centroid.safetensors from the Audiobook Builder) or None.
use_centroid(prompt, centroid)
¶
Replace the x-vector of every voice-clone prompt item by the voice's centroid (the reference codes + text stay).
ffmpeg
¶
ffmpeg / ffprobe discovery and the few operations the pipeline needs (extract audio, probe, add a dubbed track).
Search order: VOXPRINT_FFMPEG (file or folder) -> the shared suite ffmpeg (shared/ffmpeg/<version>) -> PATH
(only if -version really runs) -> the imageio-ffmpeg wheel (a GPL build; offline fallback, no ffprobe).
Older per-app copies are moved into the shared folder on launch and are not searched. The video stream is NEVER
re-encoded (-c:v copy).
FfmpegError
¶
Bases: RuntimeError
ffmpeg is missing or a command failed; the message carries the tail of ffmpeg's stderr.
FfmpegInfo(ffmpeg=None, ffprobe=None, version='', source='', problems=list())
dataclass
¶
ffmpeg and ffprobe that were found, the version line, and candidates that did not run.
StreamInfo
¶
Bases: TypedDict
One media stream: index, type, codec, language, channel count, sample rate in hertz, and title.
ProbeResult
¶
Bases: TypedDict
Media duration in seconds and the streams :func:probe reported.
find_ffmpeg()
¶
Locate ffmpeg (and ffprobe next to it or on PATH). Never raises.
ffmpeg_info(refresh=False)
¶
Return the cached :func:find_ffmpeg result, searching on the first call and when refresh is true.
require_ffmpeg()
¶
Return the path of a working ffmpeg executable, or raise FfmpegError when none is found.
run(args, timeout=3600)
¶
Run ffmpeg with args (without the program name); raises :class:FfmpegError on failure.
probe(path)
¶
Streams and duration of a media file: {"duration": s, "streams": [{"index","type","codec","lang","channels","rate"}]}.
extract_audio(src, dst, sample_rate=16000, channels=1, audio_index=0)
¶
Decode the audio_index-th audio stream of src to a PCM16 WAV (resampled / downmixed).
add_dub_track(src, dub_audio, out, language='rus', title='AI dub', codec='aac', bitrate='192k')
¶
Copy everything from src and append dub_audio as a new audio track. Returns the index of the new audio track
(among the output audio streams). Video and the original audio are stream-copied (never re-encoded).
i18n
¶
Localisation: flat JSON catalogs dubber/locales/<code>.json and tr(key, **params) (never raises).
Languages: ru and en (the audiobook program also has de; add it the same way - copy en.json, translate, add the code to
LANGS). Order: VOXPRINT_LANG, the shared Voxprint choice (suite.json ui_language, when it is a language we
have), our saved choice (state/language), the OS language, English.
The diagnostic REPORT is always English (so it can be pasted anywhere and read by anyone); only the UI is localised.
locales_dir()
¶
Directory of the <code>.json catalogs, next to this file or inside a frozen build.
load_catalog(lang)
¶
Load and cache a catalog; a missing or broken file gives an empty dict.
normalize_code(value)
¶
'ru-RU' / 'en_US.UTF-8' -> 'ru' / 'en'; unsupported -> None.
system_language()
¶
UI language of the OS if supported.
saved_language()
¶
Return the language code saved in state/language, or None when it is missing or unsupported.
detect_language()
¶
Pick a supported UI language from VOXPRINT_LANG, the shared suite choice, the saved choice, the OS, then English.
current()
¶
Return the active UI language, detecting and caching it on the first call.
set_language(lang, save=True)
¶
Switch the UI language (and remember it).
reset()
¶
Forget the cached language and catalogs (tests).
tr(key, **params)
¶
Translate key; missing -> English -> the key itself. {name} placeholders are filled from params.
infra
¶
Infrastructure shared with the other Voxprint programs: data folders, model store, GPU lock, process helpers.
components
¶
Movie Dubber's references in the shared runtime manifest.
The file format, the lock (manifest.lock, 30 seconds), the atomic publish
and the Canvas / Enlarge migration live in voxprint_common.runtime_manifest.
This module only chooses component ids, turns this app's older list-shaped
manifest into the object that library reads, and deletes a directory when no
component still points at it. The shared models directory is not removed while
another program's weights are inside it. The app id stored in refs is movie-dubber.
A path must stay inside <home>/shared, except a model component, which
may be an absolute path under voxprint_common.paths.models_dir
(<home>/models by default) or VOXPRINT_MODEL_CACHE.
OutsideShared
¶
Bases: ValueError
The directory is not an allowed component path.
Runtime, wheel and tool paths stay inside <home>/shared. A model path
stays under the shared models directory or VOXPRINT_MODEL_CACHE.
ReleaseResult(deleted, pending)
dataclass
¶
Directories removed, and directories the uninstaller still has to remove.
manifest_file(home)
¶
Return <home>/shared/manifest.json. The file need not exist yet.
shared_root(home)
¶
Return <home>/shared.
component_id_for(piece_id, version)
¶
Return (component id, kind) for a self-test piece id.
python-3.14 is the user CUDA runtime. The CI CPU runtime uses
python-3.14-cpu so the two copies sit side by side.
digest_for(path, fallback)
¶
Hash a file, or runtime-key.json inside a directory, or the fallback text.
relative_path(home, path)
¶
Return path relative to <home>/shared.
Raises:
| Type | Description |
|---|---|
OutsideShared
|
|
component_path(home, path, kind)
¶
Return the path stored for one component.
A model is an absolute path under the shared models directory. Every other
kind is relative to <home>/shared.
Raises:
| Type | Description |
|---|---|
OutsideShared
|
|
running_from(path)
¶
True when this process's prefix is path or a directory inside it.
guard_cpu_onnxruntime()
¶
Return the refusal text when the CPU onnxruntime distribution is installed.
prepare(home)
¶
Rewrite a list-shaped manifest (this app's previous file) into the object shape.
voxprint_common migrates a resources object and holders. The file
written by the shared-deps branch was a resources array with apps.
That array is converted here, once, before the library reads the file.
add_component(home, component_id, *, kind, version, path, app=APP_ID, sha256='')
¶
Add app to one component. The same id, version and digest is reused.
The shared models directory itself is never given a ref. voxprint-common 0.2.4 refuses that path, and a ref there could delete every app's weights. Per-model folders under that directory are recorded as usual.
Raises:
| Type | Description |
|---|---|
OutsideShared
|
|
ManifestError
|
The id, kind, or digest is not valid. |
release_component(home, component_id, app=APP_ID)
¶
Drop app from one component and delete its directory when nothing still uses it.
Other components that share the directory (the runtime and its wheels) keep
the directory. A directory this process is running from is left in pending.
component_refs(home, component_id)
¶
Return the apps that reference component_id, or an empty list.
release_app(home, app=APP_ID)
¶
Drop app from every component and delete directories nobody still uses.
A directory this process is running from is left on disk and named in
pending. A component record is removed only when its refs list is
empty. A second component is not added on a path another component already
uses. An older manifest that still has two ids on one directory keeps that
directory while any of them remains.
main(argv=None)
¶
Installer entry: add one component, or release an app's references.
cuda_dlls
¶
Make the CUDA 12 libraries visible to CTranslate2 (faster-whisper) on Windows.
CTranslate2 4.x is built for CUDA 12 and loads cublas64_12.dll with a plain LoadLibrary. The cu130 PyTorch build
does not ship those CUDA 12 libraries (it ships CUDA 13). The installer therefore installs the nvidia-cublas-cu12 and
nvidia-cudnn-cu12 wheels, and :func:expose puts their bin folders on the DLL search path with
os.add_dll_directory before CTranslate2 is imported.
candidate_dirs()
¶
Folders that may hold cublas64_12.dll / cuDNN 9: torch\lib and the NVIDIA wheel bin folders.
has_cublas12(dirs)
¶
Return whether any directory in dirs contains cublas64_12.dll.
expose()
¶
Idempotent. On Windows, register every candidate folder with os.add_dll_directory and PATH.
cuda_device_count()
¶
How many CUDA devices CTranslate2 sees, after :func:expose. None when the package is not installed.
cuda_device_summary(count)
¶
Pass only when CTranslate2 reports at least one CUDA device.
gpu_policy
¶
Minimum GPU for VOXPrint AI Movie Dubber: NVIDIA compute capability 8.9 (RTX 40 / Ada) and driver branch 600.
The installer asks nvidia-smi before it downloads anything. The window asks PyTorch before it opens.
Diagnostics report the same facts and fail the check when the machine is below the minimum.
VOXPRINT_SKIP_GPU_GATE=1 skips only the window check, so the CI smoke run can open the UI on a GPU-less runner.
The CPU build of PyTorch is not a user install; CI passes -Backend cpu to the installer on its own.
GpuFact(name, compute, driver, driver_branch)
dataclass
¶
One NVIDIA GPU from nvidia-smi: name, compute capability, driver version, and driver branch.
GateResult(ok, gpus, qualifying, summary)
dataclass
¶
Minimum-GPU check: whether any card qualifies, which cards were seen, and a one-line summary.
StartupBlock(code, name='', capability='', driver='')
dataclass
¶
Why the window stays closed: no CUDA device, compute capability below 8.9, or a driver older than branch 600.
parse_compute(text)
¶
Parse a compute capability such as 8.9 into (8, 9), or return None.
parse_driver_branch(version)
¶
Major number of an nvidia-smi driver version (560.94 -> 560, 617.42 -> 617).
parse_nvidia_smi_query(text)
¶
Parse nvidia-smi --query-gpu=name,compute_cap,driver_version --format=csv,noheader.
meets_policy(gpu)
¶
Return whether the GPU is compute 8.9 or newer and the driver branch is 600 or newer.
found_text(gpus)
¶
Return one sentence describing the NVIDIA GPUs that were found.
failure_summary(gpus)
¶
Return why the machine is below the minimum, including the case of no NVIDIA GPU.
evaluate_gpus(gpus)
¶
Pass when at least one GPU is RTX 40 / Ada or newer and the driver branch is 600 or newer.
evaluate_nvidia_smi_query(text)
¶
Check nvidia-smi CSV text against the RTX 40 and driver-branch 600 minimum.
installer_message(found)
¶
English text the setup wizard shows when the PC is below the minimum. Nothing is installed.
installer_message_ru(found)
¶
Russian text written to the setup log. The wizard itself has no language catalog.
startup_block(available, capability, name)
¶
Window gate: torch.cuda.is_available() and get_device_capability() >= (8, 9).
None means the window may open. Driver branch is enforced by the installer and by diagnostics;
this process can see the capability PyTorch reports.
startup_detail_key(block)
¶
Locale key and placeholders for the sentence under the shared requirement.
with_driver_link(body)
¶
Append the NVIDIA driver download page when the message does not already name it.
driver_branch()
¶
Driver branch from nvidia-smi, or None when the tool is missing or the version cannot be read.
launch_block(available, capability, name, driver)
¶
Window and self-test gate. A driver branch below 600 fails even when PyTorch reports a new GPU.
gate_skipped(environ=None)
¶
True when VOXPRINT_SKIP_GPU_GATE is set. Tests and the CI smoke run set it; users do not.
probe_torch()
¶
(available, capability, device name) from PyTorch.
Torch is imported here, not at module import: a machine without it is "no CUDA", and the CLI commands (version, installer helpers) must stay light.
model_guard
¶
Refuse to delete shared model folders this app did not install or does not solely own.
.users.json and .installed.json are updated under a file lock. Unknown keys and
other programs' entries stay. A publish is a temporary file plus os.replace.
users_path(root)
¶
Return <models>/.users.json.
ledger_path(root)
¶
Return <models>/.installed.json.
is_models_root(folder)
¶
True when folder is the shared models directory itself.
is_under_models(folder)
¶
True when folder is inside the shared models directory and is not the directory itself.
load_users_document(path)
¶
Read a users document. A missing file is {}. A list of names becomes flags plus legacy_users.
Raises:
| Type | Description |
|---|---|
OSError
|
The file exists and is not a JSON object or list. The caller must not replace it. |
user_flags(document)
¶
Return program name to in-use flag. Bookkeeping keys are not programs.
active_users(document)
¶
Sorted names whose flag is true.
other_users(document, app=APP_ID)
¶
True when some other program still uses the shared models directory.
with_user(document, app)
¶
Copy document, mark app in use, and append it to apps when it is absent.
without_user(document, app)
¶
Copy document without app. An empty name list is omitted so a last unregister leaves {}.
update_users(root, change)
¶
Read-modify-write .users.json under .users.lock. Unknown keys are the caller's to keep.
Raises:
| Type | Description |
|---|---|
OSError
|
The current file cannot be parsed. It is left unchanged. |
load_ledger(path)
¶
Read .installed.json. A missing file is an empty ledger. A bad file raises OSError.
safe_rel(relative)
¶
Return a relative model path that cannot escape the models directory.
Raises:
| Type | Description |
|---|---|
ValueError
|
The path is absolute or contains |
record_installed(root, relative, app=APP_ID)
¶
Remember that app installed relative under root. Other apps' entries stay.
forget_installed(root, relative, app=APP_ID)
¶
Drop one installed path for app. A missing ledger is left missing.
may_delete_model(folder, root=None)
¶
True only for a folder this app recorded and solely owns, and only when no other program uses the store.
delete_owned_models(root)
¶
Delete model folders this app recorded and solely owns. The models directory itself stays while anything else is inside it.
An empty store that only this app uses is removed, because that directory is the models component this app recorded.
replace_downloaded(part, target, wholesale)
¶
Move a finished .partial folder onto target.
A wholesale replace removes target first. A shared folder is updated file by file and the partial
directory is the only tree removed. The shared models directory is never the target.
Raises:
| Type | Description |
|---|---|
OSError
|
|
publish_download(part, target)
¶
Install a finished download and remember the folder when this app solely owns it.
A folder another program still uses is updated file by file. The shared models directory is never replaced.
voxprint-mdl replaces the download itself and must keep calling this.
Raises:
| Type | Description |
|---|---|
OSError
|
|
model_store
¶
Shared model store: locate a model that VOXPrint MDL has already fetched.
Downloads belong to voxprint-mdl. This module does not fetch, verify-by-redownload,
or call ensure_ready. That helper downloads inside the process, so the app never calls it.
- Layout:
<models>/<owner>--<name>/holds the files an engine opens. MDL stores the bytes at<models>/blobs/<sha256>. :func:link_blobshardlinks (then symlinks, then copies) those blobs into the named folder. - Manifest:
mdl.jsonat the repository root. :func:load_manifestturns it back into{repo: {revision, license, files}}. - Users:
<models>/.users.json. The installer addsmovie-dubber. Unknown keys and other programs' entries stay. A folder is deleted only when this app recorded it and no other program still uses the store. The models directory itself is not a manifest ref. - :class:
ModelLockis the voice-library lock. It is not a model download.
ModelUnavailable
¶
Bases: RuntimeError
The model is not on disk. VOXPrint MDL is what fetches it.
ModelLock(path)
¶
Cross-process lock (an OS file lock). The voice library uses it; model downloads do not.
models_root()
¶
Return the shared models folder, creating the configured directory when that is possible.
folder_name(repo)
¶
Turn owner/name into the on-disk folder name owner--name.
local_dir_for(repo, root=None, weights=DEFAULT_WEIGHTS, require_config=True)
¶
Qwen/X -> <models>/Qwen--X; a model complete only in the default folder is used from there.
verify_structure(path, weights=DEFAULT_WEIGHTS, require_config=True)
¶
At least one weights file, and a config file unless the model is a checkpoint with no config.
load_manifest(path=None)
¶
Load pinned models as {repo: {revision, license, files}}.
mdl.json (schema 1) is the file on disk. A document whose models value is
already that dict is returned unchanged. An unreadable file or another schema
yields an empty dict.
pinned_revision(repo)
¶
Return the pinned revision for repo, or None when the manifest has no entry.
sha256_file(path, bufsize=1 << 20)
¶
Return the hex SHA-256 of path. bufsize is accepted and ignored; the shared hasher streams the file.
manifest_bad_files(folder, repo, patterns=None, manifest=None, full_hash=True)
¶
Missing or wrong files of folder against the manifest (size first, then SHA-256). None means no entry.
write_verified_marker(folder, repo)
¶
.verified records that the hashes were checked, so later starts only compare sizes.
is_ready(folder, repo, weights=DEFAULT_WEIGHTS, patterns=None, require_config=True)
¶
Return whether the folder has the expected weights and matches the manifest.
Files are hashed only when .verified is absent. A blob MDL stored under its
SHA-256 name is treated as already checked once :func:link_blobs writes the marker.
folder_bytes(path)
¶
Return the total size in bytes of the files under path.
link_blobs(repo, weights=DEFAULT_WEIGHTS, patterns=None, require_config=True, root=None)
¶
Link MDL blobs for repo into <models>/owner--name when every file is present.
A size match against the manifest is enough. The blob file name is the SHA-256, which MDL checked when it stored the file. Nothing is downloaded.
read_users(root=None)
¶
Read .users.json as a map of program name to in-use flag, or return an empty dict.
register_user(key=USER_KEY, root=None)
¶
Mark key as using the models folder and return the updated program map.
The write keeps unknown keys and other programs. A file that cannot be parsed is left unchanged.
Raises:
| Type | Description |
|---|---|
OSError
|
|
unregister_user(key=USER_KEY, root=None)
¶
Remove key and return the other programs still using the models folder.
Raises:
| Type | Description |
|---|---|
OSError
|
|
quiet
¶
Mute known harmless noise of the ML libraries (lesson from Voxprint AI Audiobook Builder): the "SoX could not be found"
message of qwen-tts, triton "not found" warnings on Windows, and transformers' "Setting pad_token_id to eos_token_id" line printed
for every generate call. Call :func:mute_library_noise in each worker process before importing the libraries.
mute_library_noise()
¶
Silence known harmless warnings from transformers, qwen-tts, torch, SoX, and triton in this process.
resident
¶
Keep stage models loaded while they fit in the VRAM budget.
The budget is the memory that is free right now minus max(2 GB, 8 % of the card)
(dubber.infra.resources.vram_allowance_gb). A model stays resident across stages and clips in this process.
Another model is loaded beside it when the sum still fits. Under pressure the largest other model is unloaded
first, and only then. With no NVIDIA GPU there is no VRAM pressure, so models stay until :func:reset.
enable()
¶
Turn the cache on for this process (the dubbing session worker).
enabled()
¶
Return whether the resident-model cache is on in this process.
disable()
¶
Turn the cache off. Tests use this so a later test does not keep a model.
reset()
¶
Unload everything held here. Used at the end of a session and by tests.
empty_cuda_cache()
¶
Collect garbage and return cached CUDA blocks to the driver, so the next stage measures real free VRAM.
release_for(stage)
¶
Unload resident models the next stage does not use, then empty the CUDA cache. Returns the names dropped.
Separation, ASR and diarization weights would otherwise stay on the card during speech synthesis and keep the
TTS batch at one line. A model prefetched for stage itself stays.
loaded()
¶
Return the names of models currently held in this process.
noted()
¶
Models a stage asked to prefetch, whether or not a loader ran.
register_loader(name, factory)
¶
Remember factory as the way to build name for a later :func:prefetch.
vram_now()
¶
(free GB, total GB) of the first GPU, or (0, 0) when there is none.
unload_plan(loaded_sizes, incoming, need_gb, free_gb, total_gb)
¶
Names to drop so incoming fits. Empty when it already fits, or when there is no GPU.
Largest residents go first. The incoming name is never listed. If it still does not fit after every other model is listed, the list is every other model (the new one still has to run).
slot(name, need_gb, build, free_gb=None, total_gb=None)
¶
Return the resident object named name, building it once. A disabled cache just calls build.
Under pressure the largest other model is unloaded first. The build runs while the cache lock is held so two stages cannot construct the same name twice.
note(name)
¶
Remember a non-empty name as a prefetch request without loading it.
prefetch(name, need_gb, free_gb=None, total_gb=None)
¶
Load name if a loader was registered and it fits beside what is already loaded.
Always records the request. Unlike :func:slot, this never unloads a model: the stage that is running still
needs its weights. A miss returns None.
resources
¶
Resource policy for every model stage: which GPU there is, how much VRAM and RAM is free right now, and how much a run may take.
Suite rule (agreed with Voxprint AI Audiobook Builder) for system RAM: a run uses at most RAM_SHARE of the free RAM.
VRAM is not a share of free memory. The budget is whatever is free right now minus a headroom of max(2 GB, 8 % of the card),
measured again before each TTS batch, so the driver is not pushed into system RAM. An optional fraction
(VOXPRINT_VRAM_FRACTION, or vram_fraction on the shared gpu value) can cap that budget; it is off unless set.
What the budget decides:
- TTS batch size (lines per generate call): what fits in the budget after the model is loaded (measured, not guessed);
- whether several voices (LoRA adapters) stay loaded at once or are swapped one after another;
- per stage: a model that does not fit the GPU budget runs on the CPU (light stages), or the lighter TTS model is used.
Model stages of one dub share one worker process. A model stays loaded while it fits in the VRAM budget and is unloaded
only under pressure (dubber.infra.resident). The GUI process never imports torch: there the numbers come from nvidia-smi.
Snapshot(gpu='', vram_total_gb=0.0, vram_free_gb=0.0, ram_total_gb=0.0, ram_free_gb=0.0, source='')
dataclass
¶
VRAM and system RAM free at one moment, in gigabytes, and where the numbers came from.
has_gpu
property
¶
Return whether a GPU with non-zero total VRAM was measured.
vram_budget_gb
property
¶
Return free VRAM minus headroom, in gigabytes, optionally capped by :func:vram_fraction.
ram_budget_gb
property
¶
Return RAM_SHARE of free system RAM, in gigabytes.
describe()
¶
Return one log line of GPU name, VRAM budget, and RAM budget.
StagePlan(device, tts_model='', notes=list())
dataclass
¶
Device and TTS model chosen for one stage, plus notes when the plan stepped down to fit the budget.
VramBudget()
¶
In a worker process: how much VRAM a batch may use, from the memory that is free now.
budget = max(0, free_now - tts_headroom_gb(total)) (1.5 to 2.5 GB), optionally capped by :func:vram_fraction. Re-measure with
:meth:remeasure before each TTS batch group. free_now already includes this process, so the result is not reduced
a second time by what we allocated.
used_by_us()
¶
Return how many gigabytes of free VRAM have disappeared since this budget was created.
remeasure()
¶
Set budget from the VRAM that is free at this moment and return it.
left()
¶
The budget from a fresh measurement (free memory already reflects what this process holds).
describe()
¶
Measure free VRAM again and return one line of the VRAM budget, what is left, and the RAM budget.
ram_gb()
¶
(total, available) system RAM in GB; (0, 0) if unknown.
gpu_from_torch()
¶
(name, total GB, free GB) of cuda:0 when torch is already loaded in this process (never imports it).
gpu_from_smi()
¶
(name, total GB, free GB) of the first NVIDIA GPU from nvidia-smi (no torch needed).
snapshot(use_torch=True)
¶
What is there and what is free right now (torch if it is already loaded in this process, else nvidia-smi).
The window process passes use_torch=False: asking torch for free memory would create a CUDA context there (~0.3 GB held).
plan_stage(stage, snap, device, tts_model='tts_1_7b', lighter_ok=True)
¶
Device (and TTS model) for one model stage, so that it fits the VRAM budget (free minus headroom).
Light stages that do not fit go to the CPU (a few minutes slower, never a stall). TTS on the CPU is far too slow, so it first switches to the 0.6B model; if even that does not fit it stays on the GPU at batch 1 (the batch logic halves on out-of-memory).
tts_batch(device, budget_left_gb, ram_budget_gb=0.0, graphs=False)
¶
Lines per TTS generate call: what fits in what is left of the budget after the model (and its voices) were loaded.
keep_voices_loaded(budget_left_gb, batch=1)
¶
True: one more voice adapter fits next to the loaded ones and a batch; False: swap voices one after another.
tts_headroom_gb(total_gb)
¶
VRAM the TTS batch leaves unused: 8 % of the card, at least 1.5 GB and at most 2.5 GB.
tts_allowance_gb(free_gb, total_gb, fraction=None)
¶
max(0, free_now - tts_headroom_gb(total)), then min with fraction * total when a cap is set.
vram_headroom_gb(total_gb)
¶
VRAM left unused on purpose: at least 2 GB, or 8 % of the card when that is larger.
vram_fraction(cfg=None)
¶
Optional user cap. VOXPRINT_VRAM_FRACTION wins, then cfg, then suite.json (gpu.vram_fraction or vram_fraction).
Missing, empty, and 0 mean off.
vram_allowance_gb(free_gb, total_gb, fraction=None)
¶
max(0, free_now - max(2 GB, 0.08 * total)), then min with fraction * total when a cap is set.
runtime
¶
The Python runtime: pinned versions shared with Voxprint AI Audiobook Builder, side-by-side folders, a users file.
Agreed rules (Audiobook Builder, project-notes/voxprint/DECISIONS.md):
- Pins:
runtime_lock.jsonnext to this file keeps the Audiobook Builder lock schema (so the two files can be aligned later). The sibling lock on main is still Python 3.11 / torch 2.11 cu128, which this app does not copy. This lock is Python 3.14, win_amd64, torch 2.11.0. The only GPU flavor is cu130. The cu130 index and the PyTorch previous-versions install commands have no torchaudio for 2.12, 2.13, or 2.14, so the newest consistent set is torch 2.11.0+cu130 with torchaudio 2.11.0+cu130. torchcodec 0.17.0+cu130 is the newest codec whose table allows torch >= 2.11. The cpu flavor remains only so CI can pass-Backend cpu. CTranslate2 still needs CUDA 12, so the lock also pinsnvidia-cublas-cu12andnvidia-cudnn-cu12.installer/runtime-constraints.txtrecords that those torch pins are applied by the installer. - Key: :func:
runtime_key= SHA-256 over Python version, platform, torch version + flavor, our requirements and constraints (comments and blank lines ignored), 12 hex digits.installer/install-runtime.ps1computes the same key. - The installed copy lives at
<Voxprint home>/shared/runtimes/py3.14-torch2.11-<flavor>/(cu130for a user install,cpuonly when CI asks for it). A folder that already matches the pin is reused. It is never upgraded in place to a different Python or torch line: that line has its own directory name.<home>/runtimebelongs to the Audiobook Builder and is never touched. An olderruntime-<key>directory is moved into this layout by :func:dubber.infra.shared_deps.migrate. - Who uses it is recorded in
shared/manifest.json(schema 1), not in a private users file. The uninstaller drops this app's reference and deletes the directory only when no app remains.
Layout of one runtime: python (the uv-managed CPython), env (the virtual environment), runtime-key.json,
.install-complete. The program folder has a junction <app>\runtime -> env, so shortcuts keep using
<app>\runtime\Scripts\python.exe.
load_lock(path=None)
¶
Return the parsed runtime lock (runtime_lock.json unless path is given).
normalize_requirements(text)
¶
Requirement lines without comments / blanks / surrounding spaces, in file order, joined by LF.
key_material(flavor, requirements, constraints, lock=None)
¶
Return the canonical string hashed into a runtime key: Python, platform, torch flavor, and requirement hashes.
runtime_key(flavor, requirements, constraints, lock=None)
¶
Return the first 12 hex digits of the SHA-256 of :func:key_material.
choose_flavor(lock, driver_cuda)
¶
Newest CUDA flavor of the lock that the driver supports (driver_cuda = (13, 0) from nvidia-smi).
cu130 needs a driver that can run CUDA 13.0 (driver branch 580 or newer). The hardware gate already requires branch 600, which is above that line.
None when the driver cannot run one. The CPU flavor is never chosen here: a user install does not
offer it, and CI passes -Backend cpu to the installer on its own.
runtime_dir(flavor)
¶
Return <Voxprint home>/shared/runtimes/py3.14-torch2.11-<flavor>.
is_runtime_dir(path)
¶
A versioned runtime folder (never the Audiobook Builder's runtime).
current_runtime_dir(app_dir=None)
¶
The versioned runtime folder of the running interpreter, or None. First the installer's note
<app>\runtime-dir.txt, then the interpreter prefix with the <app>\runtime junction resolved.
installed_runtimes()
¶
Return versioned runtime folders that contain runtime-key.json, sorted by path.
register_user(root=None)
¶
Add this program's reference to the runtime component in shared/manifest.json.
unregister_user(root=None)
¶
Drop this program's runtime reference. Returns the other apps still using that runtime.
unregister_cli(out, root=None)
¶
--unregister-runtime-user [--out FILE]: FILE gets <number of other apps>\n<runtime folder>\n. Exit code 0 unless writing failed.
shared_deps
¶
Shared suite dependencies: layout, migration, check and repair.
The installer puts the Python 3.14 runtime, the torch 2.11.0 / torchaudio / torchcodec cu130 wheels, the
CTranslate2 CUDA 12 libraries and ffmpeg into <Voxprint home>/shared, and the
default-pipeline models into <Voxprint home>/models, before
the first launch. Every launch and --selftest call :func:live_selftest, which checks those pieces and
repairs a missing or broken one. An old per-app runtime or ffmpeg directory is moved once by :func:migrate
and is not consulted again.
Progress events are dictionaries {"progress": true, "resource", "status", "message"}. The window shows
them in a dialog. --selftest prints each one as a JSON line on stderr.
RepairError(resource_id, message)
¶
Bases: RuntimeError
A shared piece could not be restored. resource_id selects the process exit code.
GpuStatus(ok, code, message)
dataclass
¶
Result of the launch-time graphics check.
Piece(id, version, path, ok, detail='', sha256=None, size=None)
dataclass
¶
One shared component the self-test reports.
Report(outcome, exit_code, message, repaired=list(), resources=list(), optional_missing=list())
dataclass
¶
Self-test result. outcome is ok, repair, gpu, models or failed.
as_dict()
¶
Fields merged into the command's JSON object.
runtime_version(flavor)
¶
Directory name py3.14-torch2.11-<flavor> for one pin set.
runtime_path(home, flavor)
¶
shared/runtimes/py3.14-torch2.11-<flavor>.
ffmpeg_path(home, version=FFMPEG_VERSION)
¶
shared/ffmpeg/<version>.
ffmpeg_executable(home=None, version=FFMPEG_VERSION)
¶
Path of the shared ffmpeg binary for this platform (the file need not exist yet).
model_fetch_skipped()
¶
True when VOXPRINT_SKIP_MODEL_FETCH is set. CI sets it; a user install does not.
active_flavor()
¶
cpu or cu130. VOXPRINT_TORCH_FLAVOR wins, otherwise the installed torch build, otherwise cu130.
live_gpu()
¶
Graphics check used by launch and --selftest. A skipped gate (CI) is a pass.
live_pieces(home)
¶
Runtime, wheels, CTranslate2, ffmpeg and models as they are on disk right now.
install_shared_runtime(home, progress, flavor=None)
¶
Install or repair the versioned runtime with uv. Raises :class:RepairError when uv is not there.
download_resumable(url, dest, progress, resource, opener=urllib.request.urlopen)
¶
Download url to dest, resuming a partial file. Each attempt uses a per-read timeout and backs off.
install_shared_ffmpeg(home, progress, opener=urllib.request.urlopen)
¶
Install the LGPL ffmpeg build into shared/ffmpeg/n8.1. An existing working binary is kept.
install_shared_models(progress)
¶
Check the models the default pipeline needs. This does not download them.
A missing model is a repair error that names voxprint-mdl --gui --app movie-dubber.
ensure_ready(app='movie-dubber') is not called, because that function downloads.
missing_optional_models()
¶
Speaker-model keys that are not on disk. An empty list means multi-voice can run.
live_fix(home, resource_id, progress)
¶
Repair one piece. Runtime, torch and CTranslate2 are restored together, because they share one environment.
migrate(home=None, app_dir=None)
¶
Move this app's old runtime and ffmpeg into shared/ and remove the old directories.
Weights stay at <home>/models. A copy that an older installer moved into shared/models is moved
back when <home>/models does not exist yet. After this returns, discovery does not read the old runtime path.
run_selftest(progress, *, gpu, inspect, fix, register=None, repair=True)
¶
Check every piece. Repair the ones that fail when repair is true, then check those again.
A graphics failure returns immediately with exit code 3 and does not download anything. outcome is
repair when at least one piece was restored and the second check passed.
live_selftest(progress, *, repair=True, check_gpu=True)
¶
Migrate old folders, then check and repair the shared pieces on this machine.
stdio_guard
¶
Stdout / stderr that cannot raise (copied from Voxprint AI Audiobook Builder infra/stdio_guard.py).
The installed program starts with pythonw.exe (GUI subsystem): sys.stdout may be None or a handle whose write fails
with OSError errno 22. Such an error must never escape (it would end the program with a traceback window).
thermal
¶
Cooling pauses between blocks, and the GPU line written for each stage.
Suite thermal rule (project-notes suite/COLLABORATION.md, section 3): full speed for the first ~2.5 h of a job; after that, pause between batches while the GPU 5-min median is >= 83 C or it keeps throttling for > 60 s; resume at <= 75 C. Hardware longevity beats speed.
The 2.5 hours count from the start of the whole dub (job_started in the stage config), not from the start of
the speech stage. The median is taken over the temperature samples of the last five minutes (one per block and
one every few seconds during a pause). There is no user-facing mode: the rule runs on its own. A missing sensor
never pauses the job.
Guard(now=time.monotonic, started_ago_s=0.0)
¶
Tracks one job from its start and decides the pause between blocks.
started_ago_s: how long the dub has already been running when this stage starts (its full-speed window
is shared with the stages before).
elapsed(now=None)
¶
Return seconds since the dub started, including time before this stage.
update(temp_c, power_w, mask, now=None)
¶
Record one sample: temperature in Celsius, power in watts, and the nvidia-smi throttle bit mask.
median_temp()
¶
Median of the temperature samples of the last five minutes; None when there is none.
throttle_seconds(now=None)
¶
Return how many seconds the thermal or power-brake bits have stayed set.
should_pause(now=None)
¶
True only after the full-speed window, and only for a hot 5-min median or a throttle that has lasted.
cool_enough()
¶
Resume point: 75 C or below, and the throttle reason gone. Unknown temperature follows the throttle bit.
pause_after_block(sample=read_nvidia, sleep=time.sleep, log=lambda _m: None, stage='tts')
¶
Sample, log temperature / power / throttle, and pause between blocks when the rule says so.
Returns how many seconds were spent paused. One pause stops at 10 minutes so a stuck sensor cannot hold the job.
stage_telemetry_line(stage, temp_c, power_w, throttle)
¶
One log line: temperature, power and the throttle reason for a stage.
telemetry_from_summary(stage, gpu)
¶
The same line from a :class:dubber.diag.procs.GpuSampler summary. Empty when nothing was sampled.
read_nvidia()
¶
(temp C, power W, throttle mask) from nvidia-smi, or (None, None, 0) when it is not there.
vram_tier
¶
VRAM-aware model tiers (suite rule, project-notes suite/COLLABORATION.md section 3).
The installer and the program detect the card's VRAM (at install and at every start) and pick the matching tier on their own:
8gb: below 15.5 GiB;16gb: from 15.5 GiB up to but not including 23.5 GiB (RTX 4060 Ti 16 GB, 4080, RTX 4090 Laptop ...);24gb: 23.5 GiB and above (RTX 4090, RTX 5090 ...).
The cut points come from voxprint_common.gpu_probe. Batch caps stay in this module.
A tier is a set of caps, not a different pipeline: how many lines one TTS generate call may take (the measured VRAM
budget still decides below that cap) and which models are offered at all. A model is offered only when it fits the
card with the suite headroom (max(2 GB, 8 %), :mod:dubber.infra.resources) left free, so options the card cannot
run are disabled, never offered. Nothing here is a user mode: Settings has an optional advanced override that can only
pick a tier the card supports.
Tier(key, title, tts_batch_cap)
dataclass
¶
One VRAM tier and the maximum number of TTS lines allowed in a single generate call.
detect(total_gb)
¶
The tier for a card with total_gb of VRAM. Unknown, zero, or NaN is unknown.
choices(total_gb)
¶
Tiers the advanced override may pick: the detected one and the ones below it.
An unknown card may pick any tier.
resolve(total_gb, override=AUTO)
¶
The tier in use: the override when the card supports it, otherwise the detected tier.
batch_cap(tier)
¶
Return the TTS line cap for tier, using the 16 GB cap when the tier is unknown.
tts_need_gb(model)
¶
VRAM one TTS model needs to speak at least one line: weights + workspace + one line.
tts_models(total_gb)
¶
TTS models this card can run with the headroom left free (all of them when the VRAM is unknown).
separation_choices(total_gb)
¶
Separation models this card can run (none is always possible).
tts_model_for(total_gb, preferred)
¶
preferred when the card can run it, else the best model it can run (the lightest one as the last resort).
install_models(total_gb, keys)
¶
The installer's model list with the TTS model swapped for one the card can run.
describe(total_gb, override=AUTO)
¶
Return one line naming the tier in use, the card size in gigabytes, the TTS batch cap, and the TTS models that fit.
model_resolver
¶
Model roles: role id -> MDL entry in mdl.json -> local folder.
Every pipeline stage asks this module for its weights. Stage code names a role such
as asr or tts.1_7b; it never names a repository, a revision or a path. The
repository, the pinned revision, the licence and the files live in mdl.json, which
VOXPrint MDL reads to download them. This module never downloads and never calls
voxprint_common.mdl.ensure_ready (that helper downloads inside the process).
Placeholders: Model Forge will publish the models the suite ships for most roles. Until
those outputs land, mdl.json points at the current upstream weights and marks those
entries with "placeholder": true (voxprint-common 0.2.4 ignores unknown fields).
Swapping a placeholder is a mdl.json change only; no stage code changes.
A missing model raises :class:ModelUnavailable. The CLI maps it to exit code 4
(models) and the window shows its Manage models notice
(:func:is_model_missing recognises the message after it crossed the worker process).
Role(mdl_id, tier=None)
dataclass
¶
Which mdl.json variant a role uses: the model id and, for sized models, the VRAM tier.
ModelEntry(role, mdl_id, version, tier, repo, license, display_name, placeholder)
dataclass
¶
One resolved mdl.json variant for a role.
role_for_key(key)
¶
Map a registry key such as tts_1_7b or mt_en_ru to its role id.
translation_role(src, tgt)
¶
Role id of the direct Opus-MT model for one language pair.
manifest_path()
¶
mdl.json next to the program, the same file voxprint-mdl --manifest gets.
entries(path=None)
¶
Every mdl.json variant, cached until the file changes.
Raises:
| Type | Description |
|---|---|
ModelUnavailable
|
|
entry(role, path=None)
¶
The mdl.json variant bound to role.
Raises:
| Type | Description |
|---|---|
ModelUnavailable
|
The role is unknown or |
locate(role)
¶
The complete local folder for role, or None when it is not on disk (or not declared).
resolve(role)
¶
The local folder of the model bound to role. Does not download.
Raises:
| Type | Description |
|---|---|
ModelUnavailable
|
The model is not declared or not on disk yet. |
is_model_missing(message)
¶
True when a stage error is a missing model (the text crosses the worker process as ModelUnavailable: ...).
models
¶
Model registry. VOXPrint MDL downloads the weights; this module only finds them on disk.
The files live in the suite model store (VOXPRINT_MODELS_DIR, otherwise the shared
models folder). mdl.json at the install root is the app manifest. Qwen3-TTS is
declared there with the Audiobook Builder repository and revision because
voxprint-common 0.2.4 cannot resolve a shared-component id into files.
ModelSpec(key, repo, title, size_gb, license, gated=False, patterns=(), weights=('*.safetensors', '*.bin'), target_token='', require_config=True)
dataclass
¶
One Hugging Face model in the dubber registry: repo, size in gigabytes, license, and completeness rules.
EnsureInfo
¶
Bases: TypedDict
How :func:ensure resolved a model: a source label and the download time in seconds.
mt_spec(source, target)
¶
Opus-MT tc-big model for a direct pair (None when that pair has no tc-big model).
mt_inputs(token, texts)
¶
Sentences as a multi-target Opus-MT model expects them: >>rus<< text when token is set.
total_download_gb(keys, missing_only=True)
¶
Approximate size for keys (missing models only by default). MDL does the download.
manifest_path()
¶
Return mdl.json next to the program (the MDL app manifest).
mdl_id(key)
¶
Map a registry key to the MDL model id (tts_1_7b and tts_0_6b share qwen3-tts).
plan_keys(total_gb=None)
¶
Required models for this card. Diarization and embedding stay optional.
A 24 GB card gets Qwen3-TTS 1.7B. Every other reading, including unknown and 8 GB,
gets the 0.6B model. This is the set voxprint-mdl enqueue fetches by default.
missing_required(total_gb=None)
¶
Registry keys from :func:plan_keys that are not on disk. This does not download.
mdl_argv(command, *extra, model_ids=None)
¶
Build a voxprint-mdl command for this app, including --manifest.
Discovery only scans the suite install folders. --manifest points at this
program's mdl.json so a source checkout and a non-default prefix still work.
mdl_env(base=None)
¶
Return a copy of the environment with Hugging Face tokens removed.
gui_argv()
¶
Build voxprint-mdl --gui --app movie-dubber.
--gui is a top-level flag in voxprint-common 0.2.4. It opens the
localized Manage models window and is not a download subcommand.
launch_mdl()
¶
Open voxprint-mdl --gui --app movie-dubber.
voxprint_common.mdl.ensure_ready(app='movie-dubber') is not called. That
function downloads inside the process, and this app has no downloader.
Raises:
| Type | Description |
|---|---|
OSError
|
The MDL window could not be started. |
local_dir(repo, root=None)
¶
owner/name -> <models>/owner--name.
verify_dir(path, spec=None)
¶
A folder counts as a complete model when it has the expected weights (+ manifest checksums, if pinned).
locate(repo)
¶
Complete copy in the shared models folder, including a folder linked from MDL blobs.
ensure(repo, allow_download=True, token=None, log=lambda m: None)
¶
Return (folder, info) when the model is already on disk. Raises ModelUnavailable otherwise.
allow_download and token are ignored. This function does not download and
does not call ensure_ready(app='movie-dubber'). Ask VOXPrint MDL:
voxprint-mdl enqueue --app movie-dubber.
paths
¶
Application folders and the user's Desktop.
- data root:
%LOCALAPPDATA%\VoxprintMovieDubber(Windows) /$XDG_DATA_HOME/voxprint-movie-dubber(Linux); override withVOXPRINT_DUBBER_HOME. Sub-folders:logs,state,reports,projects. - Desktop: the real Desktop folder (Windows
SHGetKnownFolderPath: follows OneDrive "Known Folder Move"), with fallbacks; override withVOXPRINT_DESKTOP(tests). If nothing is writable the report goes toreports. - Models and voices live in the suite home from
voxprint_common.paths(<home>/modelsand<home>/voices). A folder that holds a Voxprint backup is never used as the models folder.
app_home()
¶
Root data directory (created on demand).
default_models_dir()
¶
<suite home>/models. VOXPRINT_MODELS_DIR still wins in :func:models_dir.
voices_dir()
¶
The shared voice library <suite home>/voices (written by the Audiobook Builder; not created here).
legacy_models_dir()
¶
<app home>/models used by builds before the shared store (read-only fallback; not created).
backup_root_of(folder)
¶
The Voxprint backup behind folder (the folder, a Voxprint-backup inside it, or its parent), else None.
set_models_dir(folder)
¶
Remember folder as the models folder. None, or the default, forgets the choice.
Written to suite.json and to the older state/models_dir.txt that earlier builds still read.
models_dir()
¶
Where models are stored: the suite choice when it can be created, else :func:default_models_dir.
A backup folder is refused. Creating the directory is this program's job; voxprint_common.paths does not.
projects_dir()
¶
Default parent of dubbing projects (cache of every stage, resumable).
logs_dir()
¶
Log folder under the app home, created on demand.
state_dir()
¶
State folder under the app home, created on demand.
reports_dir()
¶
Copy of every diagnostic report and the fallback place when the Desktop is not writable.
new_work_dir(prefix='vmd-')
¶
A fresh scratch folder in the OS temp dir (callers delete it when done).
desktop_candidates()
¶
Possible Desktop folders, best first (not filtered by existence).
desktop_dir()
¶
The first existing, writable Desktop candidate; otherwise the home folder; finally the app's reports folder.
pipeline
¶
The dubbing pipeline: stage functions (:mod:.stages) and the runner (:mod:.runner: caching, worker processes, GPU lock,
Watch-mode chunks, preview fragment).
prefetch
¶
Overlap the next stage's model load and CPU work with the GPU stage that is already running.
CPU work is VAD and ffmpeg extraction, and only when that work has not been done yet. The next model is recorded (and loaded, when the resident cache is on and a loader was registered) on a side thread. A failure there is logged and does not fail the stage.
Plan(next_model, cpu, need_gb)
dataclass
¶
Model load and CPU work that can overlap the GPU stage already running.
plan(stage)
¶
Return the next model, the CPU jobs that can overlap stage, and the VRAM that model needs.
overlap(gpu, cpu)
¶
Run cpu on a side thread for the whole of gpu. cpu errors are kept off the GPU result.
run_cpu(names, project, cfg, emit)
¶
VAD and ffmpeg for this project, skipped when the files are already there.
start_model(stage)
¶
Ask the resident cache to load the next stage's model. No loader means the request is only noted.
runner
¶
Runs a project through the stages: cache check, one worker process for the model stages, the shared GPU lock, progress + ETA, Watch-mode chunks while the dub is being made, and the 1-minute preview fragment.
The runner is Qt-free; the window drives it from a QThread and gets callbacks. main.py run-project (see dubber.cli)
runs it without a window.
Cancelled
¶
Bases: Exception
Raised when the user cancels a stage that is running in this process.
Callbacks(stage=lambda key, status, msg: None, log=lambda text: None, progress=lambda frac, eta_s: None, dubbed_until=lambda seconds: None)
dataclass
¶
Progress hooks the window or the CLI receives while a project runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stage
|
Callable[[str, str, str], None]
|
|
lambda key, status, msg: None
|
progress
|
Callable[[float, float], None]
|
|
lambda frac, eta_s: None
|
dubbed_until
|
Callable[[float], None]
|
Seconds of film whose dub is already final. |
lambda seconds: None
|
RunResult(ok, message='', stages=dict(), debug='')
dataclass
¶
Outcome of one pipeline run.
message is the output path, cancelled, or the failing stage and its short error.
stages maps each key to cached, done, or failed. debug is the traceback
for a failure and stays empty on success. The user-facing message does not include it.
Runner(project, cfg=None, cb=None, cancel=None)
¶
Runs one project through the stage order and reports progress on the callbacks. cancel stops before the next stage, and during a stage in this process.
run(until_stage='mux', from_stage=None)
¶
Run every stage from probe through until_stage, re-running from_stage even when its cache is fresh, and release the GPU lock if this run held it.
stage_error(summary, debug='')
¶
A stage failure. summary is shown to the user; debug keeps the traceback.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
summary
|
str
|
Short error, usually |
required |
debug
|
str
|
Full traceback from the worker, or empty when the caller will format one. |
''
|
Returns:
| Type | Description |
|---|---|
RuntimeError
|
RuntimeError with a |
block_lines(p)
¶
Placed lines from the TTS block records (valid while the TTS worker still owns lines.json).
reuse_preview(parent, sub, cfg)
¶
Copy a finished film's stage outputs into a preview and mark that prefix fresh.
The preview range is already on sub. Audio, windows, subtitles, recognition and the
script are sliced to that range and shifted to start at zero. Voice references are copied
as they are (they are short clips, not a timeline). TTS, mix and mux stay undone so only
the fragment is synthesised. A second run of the same preview leaves edits in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent
|
Project
|
The full film, with stages already marked done. |
required |
sub
|
Project
|
The preview project. |
required |
cfg
|
Dict[str, Any]
|
The engine config the preview run will use, so the cache hashes match. |
required |
best_preview_start(p, length=60.0)
¶
Start of the minute with the most dialogue (from the windows, if known), else 10 % into the film.
preview_project(p, start, length=60.0)
¶
A sub-project <project>/preview for [start, start+length) with the same settings and voices.
session
¶
One worker process for every model stage of a dub.
The process stays up across stages and across films started from this program, so the resident model cache
(dubber.infra.resident) can keep weights loaded. The parent holds the shared GPU lock for the whole run and
releases it when the run ends. Diagnostics still use one process per check.
stages
¶
Pipeline stages. Each stage is fn(project, cfg, emit) -> summary and only reads/writes files of the project folder, so it
can run in the GUI process (light stages, tests) or in a worker process (models). emit(kind, **payload) reports
log (text), progress (0..1 of the stage) and until (seconds of film whose dub is final - Watch mode).
Order (research note 06): probe -> extract -> subtitles -> vad -> separation -> asr -> script -> diarization -> translation ->
voices -> tts (+ time fitting, block by block in film order) -> mix -> mux. Model stages of one dub share one worker
process and keep a model loaded while it fits the VRAM budget (free memory minus max(2 GB, 8 % of the card);
dubber.infra.resources).
requested_source_lang(p)
¶
The source language the user asked for (auto unless chosen). Only this goes into the cache keys: the language found
by probing / recognition is stored separately (source_lang_detected) - otherwise "auto" resolved to "en" after the
recognition changed the keys and a later MP4 remux re-ran the whole pipeline from the subtitles (real case, run 2).
source_lang(p)
¶
The language to work with: the user's choice, else the detected one (recognition, else the audio track tag), else en.
inputs_for(key, p, cfg, prev)
¶
Hash the inputs that decide whether this stage's cache is fresh, chained to the previous stage's key.
st_probe(p, cfg, emit)
¶
Read the container and store duration, track labels, and a known audio-track language. Raises RuntimeError when the file has no audio track.
st_extract(p, cfg, emit)
¶
Decode the chosen audio track to 16 kHz mono, 44.1 kHz mono, and 48 kHz stereo, and store the duration.
find_subs(p, lang, cfg, emit, online=True)
¶
Embedded text track -> file next to the film -> SubDL -> OpenSubtitles.
st_subtitles(p, cfg, emit)
¶
Find target and source subtitles, crop them to the project range, and write the SRT files.
st_script(p, cfg, emit)
¶
Build dialogue lines from target subtitles, else source subtitles, else recognition, snapping subtitle times to the speech windows. Raises RuntimeError when there are no subtitles and no recognition result.
st_voices(p, cfg, emit)
¶
Reference clips for cloned voices (from the separated speech, else the mix).
st_vad(p, cfg, emit)
¶
Find speech with Silero VAD, or the energy detector when Silero is missing, and write the dialogue windows.
st_separation(p, cfg, emit)
¶
Separate speech and background on the dialogue windows, or skip when separation is off.
st_asr(p, cfg, emit)
¶
Transcribe the speech stem, or the mix, unless recognition is off or source subtitles already supply the text and ASR was not forced.
st_diarization(p, cfg, emit)
¶
Assign each line a speaker from subtitle tags, pyannote, or voice clustering, or one voice when multi-voice is off.
resplit_at_turns(p, turns, emit)
¶
Lines made from recognition are rebuilt with the speaker turns as hard breaks, so no line mixes two voices (only while nobody edited the script; subtitle lines keep their timing).
st_translation(p, cfg, emit)
¶
Translate lines that still have no dub text, then soften profanity when that filter is on.
tidy_translation(text, tgt, source='')
¶
Fix machine-translation leftovers the voice cannot read: Latin acronyms in a Russian line ("A.I." / "AI" / "Эй.И." -> "ИИ").
note_ai_review(line, tgt)
¶
Mark a Russian line whose source says AI and whose translation still has no "ИИ".
voice_spec(p, ln)
¶
Resolve the library adapter, actor blend, or clone reference used to speak this line.
actor_spec(p, sp, library_only=False)
¶
Actor-like voice of one speaker: his clip + the library voices to compare with (the engine picks and blends).
make_tts(p, cfg, need_adapters, emit)
¶
Load Qwen3-TTS for the dub language, reusing a resident model when that cache is on, or the mock engine when TTS is mock.
Library and actor voices set need_adapters. CUDA Graphs is used for them only when the installed
package can take a voice-clone prompt; otherwise the standard backend is used and the reason is logged.
st_tts(p, cfg, emit)
¶
Synthesize dubbed lines in 60-second blocks, time-fit each take into its slot, and publish Watch-mode progress. Raises RuntimeError when a real engine has no library voice and no reference clip to clone.
line_audio_map(p, lines)
¶
Load each line's placed WAV that exists on disk, keyed by line id.
mix_into(p, t0, t1, lines, cache)
¶
Mix the dub from t0 to t1 seconds and return the waveform with its sample rate. Missing clips are read into cache.
st_mix(p, cfg, emit)
¶
Write the full 48 kHz stereo dub track, ducking the original voice under the new lines.
output_path(p)
¶
Path of the finished movie: the configured output, or a .dub-<lang> file beside the source.
st_mux(p, cfg, emit)
¶
Add the dub as the default audio track, copying video and the original tracks, unless this run is only a preview.
platform_win
¶
Windows specifics (guarded by sys.platform): minimum build check and the Acrylic/dark window backdrop.
Same approach as Voxprint Audiobook Builder: only native DWM through ctypes (no GPL frameless-window libraries). Every function is a safe no-op on other platforms so the Linux port keeps working.
OsCheck(ok, build, message)
dataclass
¶
Whether this Windows build meets the 24H2 minimum, with the build number and a short reason.
windows_build()
¶
Return the Windows build number, or None when this is not Windows or the version cannot be read.
check_os(build=None, is_windows=None)
¶
Soft check, never raises.
apply_backdrop(hwnd, dark=True)
¶
Enable Acrylic through DwmSetWindowAttribute; returns 'acrylic' or 'plain' (older Windows / any error).
settings
¶
Application settings (<app home>/state/settings.json): service keys, engine choices, folders.
Secrets (subtitle keys and the OpenSubtitles password) stay in the user's profile folder only; they are passed to worker processes through the argument file and never written to logs or diagnostic reports.
load()
¶
Load known settings from state/settings.json, filling gaps from :data:DEFAULTS.
save(values)
¶
Merge known keys from values into state/settings.json.
output_format()
¶
mkv (default) | mp4; the old "same as the film" choice reads as MKV (the video is only ever remuxed).
engine_cfg()
¶
Return engine, device, and secret settings for a worker, with device normalized.
device()
¶
auto | cuda. A stored cpu (an older choice or the sibling app) is read as auto.
set_device(value)
¶
Remember auto or cuda in the shared suite file. cpu is not a choice this program offers.
sync_suite_settings()
¶
Write the models folder and UI language into suite.json when those keys are still absent.
Exit code 0 when done, 1 on any error. The installer ignores the error.
redacted(values)
¶
Return values with secret keys replaced by "set" when present and "" when empty.
ui
¶
Qt user interface (PySide6) in the Voxprint 'glass' design language.
dialogs
¶
Settings and diagnostics dialogs.
SettingsDialog(parent=None)
¶
DiagThread(options)
¶
DiagnosticsDialog(parent=None, target_lang=lambda: 'ru', options_hook=None)
¶
Bases: QDialog
Hardware and model checks with a plain-text report on the Desktop.
build_options()
¶
Build the diagnostic options from this dialog.
start()
¶
Start the checks and show their progress.
cancel()
¶
Stop the running checks.
running()
¶
Return whether the checks are still running.
open_report()
¶
Open the report in the default application.
show_in_folder()
¶
Show the report file in its folder.
copy_report()
¶
Copy the report text to the clipboard.
closeEvent(e)
¶
Stop any running checks and close the dialog.
CatalogThread(voices_to_install=None)
¶
Bases: QThread
Lists (ids empty) or installs voices of the Voxprint voice repositories off the GUI thread.
run()
¶
List the catalog, or install the queued voices.
VoiceCatalogDialog(parent=None)
¶
show_in_folder(path)
¶
Show the file in Explorer, or open its folder on other systems.
dub_audio
¶
Where the player takes the dubbed sound from (no Qt): the Watch-mode chunks written while dubbing, or one finished WAV.
Both sources answer read(t, n): n stereo float32 frames at 48 kHz starting at film time t. Parts that are not dubbed
yet (or outside the file) come back as silence - the Watch logic pauses the film before that happens.
ChunkSource(folder, keep=4)
¶
easter_egg
¶
Click-burst trigger for a short hidden picture.
Six clicks inside two seconds fire it once. The sibling Voxprint app can copy this module: it depends only on the standard library, draws nothing, and does not know about the splash screen.
icons
¶
Suite icons for Save, Save As, and model download.
The SVG files stroke with currentColor. Qt does not map that to the widget palette, so each
pixmap is painted in a theme colour: the normal text colour, the disabled colour, and white for
the selected menu row.
icons_dir()
¶
dubber/assets/icons in a checkout and in an installed copy.
load_svg(name)
¶
Return the SVG source for an icon file name.
suite_icon(name)
¶
A theme-tinted icon. name is the file stem (save, save-as, download, ...).
render_icon(svg, *, normal=TEXT, disabled=TEXT_DISABLED, active=_ACTIVE, size=_SIZE)
¶
Build an icon from SVG, tinted for the normal, disabled, and selected states.
jobs
¶
Background jobs of the window: the dubbing pipeline (and the preview fragment) in a QThread around the Qt-free runner.
Model stages still run as separate worker processes (see dubber.pipeline.runner); this thread only waits for them and turns
the runner's callbacks into Qt signals.
pages
¶
The four steps of the window: Film, Characters, Lines, Dub.
Pages only show and edit the project (dubber.core.project.Project); the window owns the project, starts the pipeline and
switches pages. Every edit is written to the project folder at once, so a closed window loses nothing.
AutoLabel
¶
Bases: QLabel
A status label that takes no room while it has nothing to say.
setText(text)
¶
Set the text and hide the label when it is empty.
FilmPage()
¶
Bases: QWidget
Drop a film, press Dub: the whole pipeline runs with the best settings and the dubbed file appears next to the film.
Everything that can be changed sits in the collapsed Options (preset, remembered for the next film); "Prepare and review" is the optional path for people who want to check the characters and lines before the dub.
auto_track
property
¶
Return the original audio track for the selected dub language.
retranslate()
¶
Refresh this page's labels for the current language.
set_options_open(on)
¶
Open or close Options without notifying listeners.
target_lang()
¶
Return the selected dub language.
set_media(path, info, error='')
¶
Show the film path, its tracks, and the probe error when probing failed.
set_subs_file(path)
¶
Offer this subtitle file in the combo and select it.
subtitle_choice()
¶
Return the subtitle source: auto, none, or a file path.
set_multi_voice_available(available)
¶
Enable or disable a different voice per character. Turning it off clears the checkbox.
multi()
¶
Return whether each character gets its own voice.
load(p)
¶
Show this project's options on the Film page.
store(p)
¶
Copy the Film options into the project settings.
set_plan(p, out_name='')
¶
One line under the button: what pressing Dub will make.
running(on)
¶
Enable or disable the Film actions while a job runs.
set_attention(items)
¶
Show review notes, or hide them when there are none.
show_subtitle_status(p, online_keys=False)
¶
Where the translation will come from (before the run: a local look; after: what the subtitles stage found).
SpeakerCard(page, sp, samples, items)
¶
Bases: QFrame
One character: name, voice, likeness, and a few of their lines.
CharactersPage()
¶
Bases: QWidget
Optional review of the characters.
Multi-voice shows one card per speaker. One-voice shows the single voice, its actor-likeness slider, and a few sample lines.
retranslate()
¶
Refresh this page's labels for the current language.
count_lines(sid)
¶
Return how many lines this speaker has.
diarized()
¶
Return whether speaker detection finished without being skipped.
load(p)
¶
Rebuild the character cards from the project.
set_actor_weight(sid, percent)
¶
Save how closely this character should match the actor.
set_voice(sid, data)
¶
Save the voice chosen for this character.
set_key(sid, on)
¶
Save whether this character is a key speaker.
rename(sid, name)
¶
Save a new name for this character.
selected()
¶
Return the ids of the checked characters.
merge_selected()
¶
Merge the checked characters into the one with the most speech.
LinesPage()
¶
Bases: QWidget
Optional review of the lines (made automatically): edit a translation, keep a line in the original, find the long ones.
DubPage(player)
¶
Bases: QWidget
Dub step: run the pipeline, set the original volume, and play the result.
card(primary=False)
¶
Return a card frame, marked primary when it holds the main action.
label(obj='', wrap=True)
¶
Return a label, using obj as its object name when one is given.
fmt_time(t, ms=False)
¶
Format a duration as a clock time, with milliseconds when asked.
fmt_eta(seconds)
¶
Format a remaining time in minutes, or as unknown when it is negative.
voice_items()
¶
(Voice, label) choices: a clone from the film, then the voices of the shared Voxprint voice library (read-only).
voice_key(v)
¶
Return the combo-box value for a voice.
voice_from_key(key)
¶
Return the voice stored in a combo-box value.
fill_voice_combo(cmb, current, items=None)
¶
Fill a voice combo and select the current voice.
player
¶
Built-in player for the preview fragment and Watch mode (Qt Multimedia, FFmpeg backend - no VLC needed).
The film's video plays without its own sound; the dubbed sound (Watch chunks or the preview WAV) is pushed into a QAudioSink
in step with the video position. If the two drift apart by more than RESYNC_S the sound is restarted at the video position.
In Watch mode a :class:~dubber.core.watch.WatchState decides when the film must wait for the dubbing ("buffering").
If Qt Multimedia cannot play the file, failed is emitted and the window offers the external player instead.
Player(parent=None)
¶
Bases: QWidget
Silent film player that plays the dubbed sound in step with the picture.
retranslate()
¶
Set the play, pause, and stop labels for the current language.
open(film, source, start=0.0, end=None, watch=None)
¶
Load the film and the dub source; start/end limit playback (preview); watch enables the buffering logic.
show_poster(image)
¶
Show a still frame until playback starts. A missing or unreadable image leaves the video widget as it is.
play()
¶
Start playback unless Watch mode is still buffering.
pause()
¶
Pause the picture and the dubbed sound.
toggle()
¶
Pause if the film is playing, otherwise start it.
stop()
¶
Stop playback and clear the Watch state.
position()
¶
Return the current film time in seconds.
dubbed_until(seconds, finished=False)
¶
Watch mode: the dubbing moved on (called from the window).
setup_progress
¶
Progress dialog for the shared-component check that runs before the window opens.
startup_repair_dialog(report)
¶
Title and body for a failed startup repair. A healthy report returns None.
The title is the model-download title. It is never the graphics-card gate title.
fetch_optional_with_dialog()
¶
Ask VOXPrint MDL for the speaker models and return the keys still missing.
The dialog is not modal. A failure leaves the window open. This does not download inside the process.
ensure_with_dialog()
¶
Check the shared runtime, wheels, ffmpeg and models, and repair a missing piece.
The dialog stays hidden on a healthy launch and appears while a piece is being repaired or has failed. The window path already refused an unsupported GPU, so this check does not repeat that gate.
splash_hook
¶
Movie Dubber splash: the shared window plus the click-burst picture.
voxprint_common.ui.splash.Splash paints the 960 by 540 window. Clicks arrive
through on_click. The burst picture is an overlay on that window. A click
on the picture hides it and leaves the splash up.
splash_build()
¶
Build identity for the splash.
The painted name is :data:dubber.appinfo.APP_DISPLAY_NAME. voxprint-common maps the
suite id movie-dubber to an older spelling, and :func:voxprint_common.ui.splash.display_name
keeps a name it does not know. The second line is :func:voxprint_common.buildinfo.label.
show_splash()
¶
Show the shared splash and count clicks with on_click.
The returned object is the shared :class:Splash. _easter is the overlay
widget, so a test can see that a burst did not close the window.
window
¶
The main window: one dark glass window with four steps - Film, Characters, Lines, Dub.
Film is all most people need: drop a movie, press Dub, get <name>.dub-<lang>.mkv next to it. Characters and
Lines are optional review tabs; one voice still shows its voice and likeness. After the analysis they get a badge only when something deserves a look.
Same shell as the Voxprint Audiobook Builder: translucent root, Acrylic backdrop on Windows 11, cards with rounded corners,
a gear menu for the UI language. Heavy work never runs in the GUI thread: the pipeline runs in a QThread that starts one worker
process for the model stages (dubber.pipeline.runner); diagnostics live in their own dialog.
MainWindow(autorun=False, options_hook=None, cfg=None)
¶
Bases: QWidget
Main window for choosing a film, reviewing the dub, and running the pipeline. autorun opens diagnostics and starts them.
retranslate()
¶
Refresh the window and every page for the current language.
manage_models()
¶
Open the VOXPrint MDL window for this app.
show_optional_models(missing)
¶
Warn once that multi-voice is off, and offer another download. An empty list clears the banner.
retry_optional_models()
¶
Download the speaker models again. The banner stays up while any of them are still missing.
set_language(code)
¶
Switch the interface language and refresh the window.
go(index)
¶
Show a wizard step, staying on Film until the project is prepared.
target_lang()
¶
Return the dub language selected on the Film page.
dragEnterEvent(e)
¶
Accept a dragged video, subtitle, or project file.
dropEvent(e)
¶
Open a dropped film or project, or attach a dropped subtitle file.
choose_file()
¶
Ask for a film file and open it.
choose_vxdub()
¶
Ask for a project file and open it.
save_vxdub_as()
¶
Ask where to save the project file, then save it.
save_vxdub(path=None)
¶
Write the open dub to path (or the current project file). Returns whether a file was written.
open_vxdub(path, locate=None, notify=None)
¶
Open a .vxdub file, relink its video, and show that dub.
locate(saved_path) asks for the video when the saved path and the relative hint both miss.
notify(title, text) reports a problem. Both default to dialogs.
choose_subtitles()
¶
Ask for a subtitle file and use it for this film.
projects_root()
¶
Return the folder that holds film projects.
set_source(path)
¶
Open a film: read what is inside, open (or resume) its project folder.
default_target(info)
staticmethod
¶
Remembered dub language, else the UI language (Russian when the UI language is not a dub language), never the film's own.
new_project_settings(info)
¶
A new film starts with the remembered Options (or the defaults: original track, subtitles automatic, profanity as in the original, one voice, voice-over volume, MKV).
apply_auto(p)
¶
Automatic choices the user has not overridden: the original audio track, and one library voice in the dub language (a voice made or downloaded with the Audiobook Builder) instead of a clone, when there is one.
prepare()
¶
The optional review path: make the lines (and speakers), then open the review tabs.
dub_all()
¶
The main action: the whole pipeline with the current (preset) Options, ending with the dubbed file next to the film.
listen(sid)
¶
Play what a voice will sound like: the library sample, the reference clip, or a few of the speaker's lines.
cfg()
¶
Return the engine settings for a pipeline run.
run_pipeline(kind, until, preview=None)
¶
Start a background pipeline job and return whether it started. prepare, find, and auto use the Film page.
cancel_job()
¶
Ask the running pipeline job to stop.
set_original_volume(value)
¶
Save how loud the original audio stays under the dub.
start_preview()
¶
Dub a one-minute fragment and play it.
watch_source()
¶
Finished dub -> the whole dub track; while dubbing -> the Watch chunks.
start_watch()
¶
Play the film alongside the dubbed audio that is already ready.
show_result()
¶
Show the finished dubbed file in its folder.
open_external()
¶
External player fallback: the finished file, else a short clip of the preview fragment.
save_actor_voice(sid)
¶
Keep an actor-like voice in the shared library - only after the user confirms he may use this person's voice.
open_voice_catalog()
¶
Open the shared voice catalog and reload characters after an install.
open_settings()
¶
Open the settings dialog and refresh the plan when they are saved.
open_diagnostics(start=False)
¶
Show the diagnostics dialog and start the checks when asked.
showEvent(e)
¶
Acrylic backdrop once the native window exists (Windows 11), else the plain dark look.
closeEvent(e)
¶
Stop playback and background work, then close the window.
engine_cfg()
¶
Engines from Settings; VOXPRINT_ENGINES=mock runs the whole flow with the CPU stand-ins (no models needed).
workers
¶
Worker processes: one heavy model (or one micro-benchmark) per process. Entry point: main.py --worker NAME ARGS.json.
common
¶
Shared helpers of the worker processes: the stdout protocol, timing, CUDA/VRAM helpers, model location and download.
A worker module w_<name>.py defines run(args: dict, ctx: WorkerContext) -> dict returning
{"status": "OK|WARN|FAIL|SKIP", "summary": str, "details": [str], "metrics": {...}}. Exceptions are caught by
:func:worker_main and become FAIL with the traceback, so a worker module can stay simple.
WorkerContext()
¶
Passed to run: log(msg) reports progress to the parent (shown in the UI), t0 is the start time.
log(msg)
¶
Send one progress line to the parent on the worker protocol.
worker_main(name, args_path)
¶
Run worker name. Always prints exactly one result line; returns 0 unless the process itself is broken.
torch_device(prefer='auto')
¶
cuda when available (and not disabled by prefer='cpu'), else cpu.
cuda_sync()
¶
Wait until this process's queued CUDA work has finished.
reset_peak()
¶
Reset PyTorch's peak CUDA-allocation counter for this process.
peak_vram_gb()
¶
Peak memory allocated by PyTorch in this process (GB); None without CUDA.
reserved_vram_gb()
¶
GPU memory reserved by PyTorch, in GB, or None when CUDA is unavailable.
free_gpu()
¶
Run garbage collection and release PyTorch's cached CUDA blocks.
read_wav_mono(path, target_sr=None)
¶
(float32 mono samples, sample rate) from a WAV/FLAC; resampled to target_sr (linear interpolation or scipy) if given.
resample(x, sr_from, sr_to)
¶
Polyphase resampling with scipy (falls back to linear interpolation).
rms_db(x)
¶
Root-mean-square level of x in dBFS, or -120 for an empty buffer.
wer(reference, hypothesis)
¶
Word error rate (lower-case, punctuation removed) via edit distance; 0.0 = identical.
apply_qwen_tts_compat()
¶
Work around qwen-tts-hf 0.1.1 vs transformers >= 5.18: its "default" RoPE initialiser (qwen_tts._transformers_compat)
reads config.rope_theta, which Mimi/other configs no longer have (they keep it in config.rope_parameters) ->
AttributeError: ... 'rope_theta' while the speech tokenizer loads. Installs a tolerant initialiser in the transformers
registry AND in qwen_tts's own module (faster-qwen3-tts imports qwen_tts lazily inside from_pretrained, and qwen_tts only
setdefaults its function, so both places must hold the tolerant one). Returns a note for the report ("" if nothing was needed).
model_folder(override, role, log=None)
¶
Diagnostics: (folder, info, label) for an explicit repo override (tests use a tiny model) or the folder bound to role.
Raises:
| Type | Description |
|---|---|
ModelUnavailable
|
The model is not on disk. Nothing is downloaded. |
w_asr
¶
Worker asr: faster-whisper (CTranslate2) speech recognition with word timestamps and language detection.
Input (args): wav16 (preferably the dialogue stem), repo (default large-v3-turbo CT2), expected_text (for the WER check),
out_json, allow_download, device. GPU first (float16); if CUDA cannot be used (e.g. cuBLAS/cuDNN DLLs missing) it
falls back to CPU int8 and says so (status WARN) - that fallback is exactly the kind of Windows problem the report should reveal.
run(args, ctx)
¶
Transcribe wav16 with faster-whisper, write out_json when set, and return segments, timing, and word-error rate.
w_diar
¶
Worker diar: speaker diarization with the ungated pyannote community-1 mirror.
Input (args): wav16, expected_speakers (int, optional), out_json, allow_download, device.
Without pyannote.audio installed the result is SKIP with the exact reason - not a failure of the app.
The audio is passed to pyannote as an in-memory waveform, so torchcodec/ffmpeg shared libraries (a frequent Windows problem) are not needed.
run(args, ctx)
¶
Diarize wav16 with pyannote and write turns to out_json when set, or skip when the model cannot be loaded.
w_fetch
¶
Worker fetch: report models that are already on disk. It does not download.
Args: keys (model keys from dubber.models.SPECS), repos (optional {key: repo} overrides).
allow_download is accepted and ignored.
run(args, ctx)
¶
Ensure each requested model is on disk and report its source, size, and download time.
w_gpu
¶
Worker gpu: what PyTorch (and the other CUDA libraries) can do on this machine.
Checks, each wrapped on its own so one failure never hides the others:
PyTorch / CUDA / cuDNN versions and devices, a bf16 matmul and a memory-copy micro-benchmark, the three PyTorch SDPA attention
backends (FlashAttention kernel built into PyTorch, memory-efficient, math), the separate flash_attn package (FlashAttention-2:
imported AND executed), CUDA Graphs (captured and replayed, speed-up measured), triton, CTranslate2 and onnxruntime CUDA support.
run(args, ctx)
¶
Measure PyTorch, CUDA, attention backends, and CUDA Graphs, and return one combined result.
w_mt
¶
Worker mt: offline machine translation with Opus-MT tc-big (Marian).
Input (args): source, target (en/ru/de), sentences (list of str), out_json, allow_download, device.
Direct pairs among en/ru/de are en<->ru, en<->de and ru<->de. Any other pair is SKIP here
(the pipeline pivots through English only when both sides are non-English and a direct model is missing).
run(args, ctx)
¶
Translate sentences with the direct Opus-MT pair, or skip when that pair does not exist.
w_sep
¶
Worker sep: dialogue / effects / music separation with TIGER-DnR (vendored code, Apache-2.0 weights).
Input (args): wav 44.1 kHz mono WAV (the mix), out_dir, repo (model repo), allow_download, device (auto|cpu).
Output files in out_dir: dialog.wav (44.1 kHz), dialog16k.wav (for ASR / diarization), music.wav, effect.wav.
Status OK when the three stems were produced; the details tell how much dialogue leaked into the pauses (a quality hint).
run(args, ctx)
¶
Separate the mix with TIGER-DnR and write dialogue, effect, music, and 16 kHz dialogue WAVs under out_dir.
w_session
¶
Worker session: several model stages, one after another, in this process.
The parent writes one JSON command per line on stdin ({"stage", "project", "cfg"}) and {"cmd": "quit"} to stop.
Each stage answers with the usual @@VX@@ result line, including a stage field, so the parent can keep the
process. Models stay in :mod:dubber.infra.resident until VRAM pressure or the quit.
run(args, ctx)
¶
Run JSON stage commands from stdin in this process until quit, keeping loaded models between stages.
w_stage
¶
Worker stage: one model stage of a dubbing project in its own process (see dubber.pipeline.stages).
Args: project (folder), stage (key), cfg (engine settings). Progress goes to the parent as JSON log lines
({"kind": "log|progress|until", ...}); the stage saves the project files itself before the result line.
run(args, ctx)
¶
Run one pipeline stage on the project folder and save the project.
w_tts
¶
Worker tts: Qwen3-TTS speed and (optionally) synthesis of the clip's lines, in one of four modes.
====================== ========================================= ===============================================
mode code path what it measures
====================== ========================================= ===============================================
standard_sdpa qwen_tts.Qwen3TTSModel + SDPA the baseline the audiobook program uses (3.7x slower than real time)
standard_fa2 same + attn_implementation=flash_attention_2 effect of the flash_attn package
graphs_sdpa faster_qwen3_tts.FasterQwen3TTS (CUDA Graphs + static KV cache) + SDPA the candidate fix (research note 03/06)
graphs_fa2 same + flash_attention_2 both speed-ups together
====================== ========================================= ===============================================
Args: mode, model_repo, ref_audio, ref_text, language ("Russian"), phrases (list of str, timed),
runs (repeats per phrase), lines (optional: [{"id","text"}] synthesised into out_dir as line_<id>.wav),
allow_download, device (auto|cpu). RTF = generation seconds / audio seconds (< 1 means faster than playback).
Every phase (import, model load, voice prompt, warm-up/graph capture, each run) is timed separately.
w_vad
¶
Worker vad: Silero VAD (MIT, ~2 MB, runs on CPU) - finds the speech windows of the soundtrack.
Input (args): wav16 mono 16 kHz WAV; optional expected (list of {start,end}) for a sanity check; out_json.
Output: out_json = {"segments": [{"start","end"}...]} (seconds).
run(args, ctx)
¶
Find speech windows in wav16 with Silero VAD, writing out_json when that path is set.
main
¶
Voxprint AI Movie Dubber - entry point.
main.py start the window
main.py PROJECT.vxdub start the window and open that dubbing project
main.py --diagnose start the window and run the diagnostics at once (the report goes to the Desktop)
main.py --diagnose-cli run the diagnostics without a window (console output), same report
main.py version|diagnose|fetch-models|run-project|info headless commands (docs/CLI.md)
main.py --dry-run --json discovery only: one JSON object, no model load or download
main.py --worker NAME ARGS internal: one heavy step in its own process (used by the diagnostics and the pipeline)
main.py --fetch-models ask VOXPrint MDL to fetch the models; --models KEY,KEY limits the set
main.py --register-models-user (installer) add this program to the shared models folder's .users.json
main.py --unregister-models-user --out FILE (uninstaller) remove it; FILE gets "<other users>\n<models folder>"
main.py --register-runtime-user / --unregister-runtime-user --out FILE the same for the shared runtime in shared/manifest.json
main.py --sync-suite-settings (installer) write the shared suite.json (models folder, UI language) if it has none yet
main.py --run-project DIR [--stages a,b] run (or resume) a dubbing project without a window (detached long run)
main.py --selftest check and repair shared components, then open the window briefly (smoke test)
main.py --selftest --json same check, one JSON object on stdout, progress JSON lines on stderr, no window
main.py selftest [--json] the same check as a headless command
main.py --release-shared drop this app's shared-resource references and delete unreferenced directories
main.py --version
Options for the diagnostics: --out FILE --quick --no-download --target ru|en|de --skip system,gpu,network,models,tts,stages --tts-model tts_1_7b|tts_0_6b --tts-modes standard_sdpa,graphs_sdpa,... --cpu-tts --cpu-sep --clip FILE --asr-repo REPO