The main pipeline class. Orchestrates all nine stages.
from mazinger import MazingerDubberMazingerDubber(
openai_api_key=None, # str — or set OPENAI_API_KEY env var
openai_base_url=None, # str — or set OPENAI_BASE_URL env var
llm_model=None, # str — default: OPENAI_MODEL env var or "gpt-4.1"
base_dir="./mazinger_output",
llm_think=None, # bool | None — True/False to control LLM thinking mode;
# None (default) sends no preference.
# Set to False for Ollama models like Qwen3 that
# have thinking enabled by default.
)Runs the full pipeline and returns a ProjectPaths object.
proj = dubber.dub(
source, # str — URL or local file path (required)
voice_sample=None, # str | None — path to reference voice audio
voice_script=None, # str | None — path to reference transcript
*,
voice_theme=None, # str | None — pre-defined theme (alternative to sample+script)
slug=None, # str — project directory name (auto-generated if omitted)
device="cuda", # str — "cuda", "cpu", or "auto"
transcribe_method="faster-whisper", # str — "openai", "faster-whisper", "whisperx", "coherex", "mlx-whisper"
whisper_model=None, # str — model name override
mlx_whisper_model="mlx-community/whisper-large-v3-turbo", # str — MLX Whisper model
tts_model_name="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
tts_dtype="bfloat16", # str — "bfloat16", "float16", "float32"
tts_language=None, # str — TTS language hint (defaults to target_language)
tts_engine="qwen", # str — "qwen", "chatterbox", "mlx", or "omnivoice"
mlx_model="mlx-community/Qwen3-TTS-12Hz-0.6B-Base-bf16", # str — MLX TTS model
source_language="auto", # str — source language or "auto"
target_language="English", # str — one of 33 supported languages
chatterbox_model="ResembleAI/chatterbox",
chatterbox_exaggeration=0.5, # float — 0.0–1.0, emotion intensity
chatterbox_cfg=0.5, # float — 0.0–1.0, pacing control
omnivoice_model="k2-fsa/OmniVoice", # str — OmniVoice model ID
cookies_from_browser=None, # str — browser name for yt-dlp cookie extraction
cookies=None, # str — path to cookies.txt
quality=None, # str — "low", "medium", "high", or numeric
start=None, # str — start timestamp for slicing (e.g. "00:01:30" or "90")
end=None, # str — end timestamp for slicing (e.g. "00:05:00" or "300")
skip_existing=True, # bool — skip stages with existing output
force_reset=False, # bool — delete cache and re-run everything
use_resegmented=False, # bool — use resegmented SRT for TTS input
tempo_mode="sync", # str — "sync" (exact fit to the original speech),
# "auto", "dynamic", "fixed", "off"
fixed_tempo=None, # float — constant speed multiplier
max_tempo=1.5, # float — speed-up cap
min_tempo=0.8, # float — slow-down floor in sync mode
words_per_second=None, # float — speech rate for word budgets (auto-estimated)
duration_budget=None, # float — fraction of time for speech (0.85; sync: 0.95)
translate_technical_terms=False, # bool — translate tech terms vs. keep in English
asr_review=False, # bool — review ASR transcript (fix typos, punctuation)
keep_technical_english=False, # bool — convert technical terms to English (requires asr_review)
use_youtube_subs=False, # bool — download YouTube captions and compare with ASR
loudness_match=True, # bool — normalise dubbed loudness to original
mix_background=True, # bool — mix original background audio under dub
background_volume=None, # float — background layer gain (0.0–1.0);
# None: 1.0 in sync mode with Demucs, else 0.15
output_type="audio", # str — "audio" (WAV) or "video" (MP4)
subtitle_style=None, # SubtitleStyle — styling for burned subtitles
subtitle_source="translated", # str — "translated", "original", or file path
)Returns a ProjectPaths instance with all output paths populated.
When neither
voice_sample/voice_scriptnorvoice_themeis provided, the pipeline automatically clones the speaker's voice from the source audio (auto-clone). A 20–60 s segment with the best word coverage is selected from the transcript.When
voice_themeis provided,voice_sampleandvoice_scriptare optional. The theme generates a voice profile automatically and saves it to the project'svoice_profile/directory.
from mazinger import MazingerDubber
# Auto-clone the speaker's voice (simplest — no voice configuration)
dubber = MazingerDubber(openai_api_key="sk-...", base_dir="./output")
proj = dubber.dub(
source="https://youtube.com/watch?v=VIDEO_ID",
target_language="Spanish",
output_type="video",
)
print(proj.final_video)# Using a voice theme
dubber = MazingerDubber(openai_api_key="sk-...", base_dir="./output")
proj = dubber.dub(
source="https://youtube.com/watch?v=VIDEO_ID",
voice_theme="narrator-m",
target_language="Spanish",
output_type="video",
)
print(proj.final_video)# Using a voice profile
from mazinger import MazingerDubber
from mazinger.profiles import fetch_profile
from mazinger.subtitle import SubtitleStyle
dubber = MazingerDubber(
openai_api_key="sk-...",
llm_model="gpt-4.1",
base_dir="./output",
)
voice, script = fetch_profile("abubakr")
style = SubtitleStyle(
font_size=24,
font_color="yellow",
bg_color="black",
bg_alpha=0.8,
bold=True,
)
proj = dubber.dub(
source="https://youtube.com/watch?v=VIDEO_ID",
voice_sample=voice,
voice_script=script,
target_language="Arabic",
output_type="video",
subtitle_style=style,
subtitle_source="translated",
tts_engine="qwen",
transcribe_method="faster-whisper",
)
print(proj.summary())Manages all file paths for a single project.
from mazinger import ProjectPathsProjectPaths(slug, base_dir="./mazinger_output", target_language=None)With target_language, the per-language paths are under
lang/<target_language>/, which is how the pipeline lays out projects. Without
it, they fall back to the project root (the legacy flat layout).
Paths are relative to <base_dir>/projects/<slug>/. <lang> is
lang/<target_language>/.
| Property | Path |
|---|---|
root |
<base_dir>/projects/<slug>/ |
video |
source/video.mp4 |
audio |
source/audio.mp3 |
source_loudness |
source/loudness.json |
source_srt |
transcription/source.srt |
source_raw_srt |
transcription/source.raw.srt |
reviewed_srt |
transcription/source.reviewed.srt |
thumbs_meta |
thumbnails/meta.json |
description |
analysis/description.json |
translated_raw_srt |
<lang>/transcription/translated.raw.srt |
final_srt |
<lang>/subtitles/translated.srt |
final_audio |
<lang>/tts/dubbed.wav |
final_video |
<lang>/tts/dubbed.mp4 |
tts_segments_dir |
<lang>/tts/segments/ |
voice_profile_dir |
<lang>/voice_profile/ |
voice_reference_dir |
<lang>/voice_profile/reference/ |
run_info |
<lang>/run.json |
editor_dir |
<lang>/editor/ |
proj.ensure_dirs() # Create all subdirectories (idempotent). Returns self.
proj.summary() # Human-readable overview of which files exist.
proj.background_audio(sr) # source/background.<sr>.wav — the cached background stemMazingerDubber.dub() writes the settings it used to <lang>/run.json
(never API keys or tokens), so a single step can be redone later with the
same settings:
from mazinger.runinfo import load_run_info
info = load_run_info(proj) # dict, or None for projects dubbed before 2.3
print(info["tts"]["engine"], info["voice"]["kind"], info["assembly"]["tempo_mode"])Each pipeline stage is available as a standalone function.
from mazinger.download import resolve_slug, download_video, extract_audio
slug, info = resolve_slug("https://youtube.com/watch?v=VIDEO_ID")
download_video("https://youtube.com/watch?v=VIDEO_ID", "video.mp4")
extract_audio("video.mp4", "audio.mp3")Slice a downloaded file to a time range:
from mazinger.download import slice_media, slice_project
# Slice a media file directly
slice_media("video.mp4", "clip.mp4", start="00:01:30", end="00:05:00")
# Slice a project’s video/audio in-place
slice_project(proj, start="90", end="300")from mazinger.transcribe import transcribe
# Cloud
transcribe("audio.mp3", "subtitles.srt")
# Local
transcribe("audio.mp3", "subtitles.srt", method="faster-whisper", device="cuda")
transcribe("audio.mp3", "subtitles.srt", method="whisperx", device="cuda")
# CohereX — Cohere Transcribe + wav2vec2 alignment (14 languages).
# The source language is required; Cohere Transcribe cannot detect it.
transcribe("audio.mp3", "subtitles.srt", method="coherex", language="ar")
# MLX (Apple Silicon)
transcribe("audio.mp3", "subtitles.srt", method="mlx-whisper")from openai import OpenAI
from mazinger.thumbnails import select_timestamps, extract_frames
client = OpenAI()
with open("subtitles.srt") as f:
srt_text = f.read()
timestamps = select_timestamps(srt_text, client)
frames = extract_frames("video.mp4", timestamps, "./thumbs")from mazinger.describe import describe_content
from mazinger.utils import load_json
thumbs = load_json("./thumbs/meta.json")
desc = describe_content(srt_text, thumbs, client)
# Returns: {"title": "...", "summary": "...", "keypoints": [...], "keywords": [...]}from mazinger.review import review_srt
reviewed = review_srt(
srt_text,
description=desc,
client=client,
source_language="auto",
keep_technical_english=False,
)Full signature:
review_srt(
srt_text, # str — source SRT content
description, # dict — from describe_content
client, # OpenAI client
*,
llm_model="gpt-4.1",
source_language="auto",
keep_technical_english=False,
blocks_per_batch=30,
overlap_size=6,
usage_tracker=None, # LLMUsageTracker
)from mazinger.translate import translate_srt
translated = translate_srt(
srt_text,
description=desc,
thumb_paths=thumbs,
client=client,
target_language="Spanish",
)
with open("translated.srt", "w") as f:
f.write(translated)Full signature:
translate_srt(
srt_text, # str — source SRT content
description, # dict — from describe_content
thumb_paths, # list[dict] — from thumbnails meta.json
client, # OpenAI client
*,
llm_model="gpt-4.1",
source_language="auto",
target_language="English",
blocks_per_batch=24,
overlap_size=8,
words_per_second=None, # auto: TTS speech rate of the target language
duration_budget=0.85,
translate_technical_terms=False,
usage_tracker=None, # LLMUsageTracker
)from mazinger.resegment import resegment_srt
final = resegment_srt(srt_text, client=client, max_chars=84, max_dur=4.0)The client parameter is optional. Without it, the function uses rule-based merging instead of LLM-powered merging.
from mazinger import tts
from mazinger.srt import parse_file
# Load model
model = tts.load_model(engine="qwen", device="cuda:0")
# Create voice prompt from reference recording
wrapper = tts.create_voice_prompt(
model,
ref_audio="speaker.m4a",
ref_text="Transcript of the voice sample.",
engine="qwen",
)
# Synthesize all segments
entries = parse_file("translated.srt")
segments = tts.synthesize_segments(model, wrapper, entries, "./segments")
# Clean up
tts.unload_model(model)For Chatterbox:
model = tts.load_model(engine="chatterbox")
wrapper = tts.create_voice_prompt(
model,
ref_audio="speaker.m4a",
ref_text="", # not needed for Chatterbox
engine="chatterbox",
chatterbox_exaggeration=0.5,
chatterbox_cfg=0.5,
)
segments = tts.synthesize_segments(model, wrapper, entries, "./segments", language="Spanish")For OmniVoice (24 languages):
model = tts.load_model(engine="omnivoice")
wrapper = tts.create_voice_prompt(
model,
ref_audio="speaker.wav",
ref_text="Transcript of the reference audio.",
engine="omnivoice",
)
segments = tts.synthesize_segments(model, wrapper, entries, "./segments", language="Hindi")To re-synthesize all segments instead of resuming:
segments = tts.synthesize_segments(model, wrapper, entries, "./segments", force_reset=True)from mazinger.assemble import assemble_timeline, mux_video
from mazinger.utils import get_audio_duration
duration = get_audio_duration("audio.mp3")
# No tempo adjustment
assemble_timeline(segments, duration, "dubbed.wav")
# Fixed tempo
assemble_timeline(segments, duration, "dubbed.wav", tempo_mode="fixed", fixed_tempo=1.1)
# Dynamic tempo
assemble_timeline(segments, duration, "dubbed.wav", tempo_mode="dynamic", max_tempo=1.3)
# Exact sync: fit every line to the original speech it replaces
from mazinger.speech import build_speech_map, line_targets
smap = build_speech_map("audio.mp3", "speech_map.json", vocals_path="vocals.wav")
targets, unvoiced = line_targets(entries, smap) # seconds per line; lines with no speech
segments = tts.synthesize_segments(model, wrapper, entries, "./segments", targets=targets)
assemble_timeline(segments, smap.duration, "dubbed.wav", tempo_mode="sync",
speech_map=smap, vocals_path="vocals.wav")
post_process("dubbed.wav", "audio.mp3", "dubbed_final.wav",
background_volume=1.0, voice_reference="vocals.wav", exact=True)
# Background and vocals stems from one Demucs pass, cached
from mazinger.assemble import extract_stems_cached
bg, vocals, method = extract_stems_cached(
"audio.mp3", proj.background_audio(), proj.vocals_audio())
# Mux audio into video
mux_video("video.mp4", "dubbed.wav", "dubbed.mp4")
# Post-process: loudness matching + background mixing
from mazinger.assemble import post_process
post_process("dubbed.wav", "audio.mp3", "dubbed_final.wav")
# Skip background mixing, only normalise loudness
post_process("dubbed.wav", "audio.mp3", "dubbed_final.wav", mix_background=False)
# Keep the background stem and the source loudness for later runs. Both are
# slow on long videos (Demucs, and a loudness pass of ~1 min per hour) and
# depend only on the source, so they are reused while audio.mp3 is unchanged.
post_process(
"dubbed.wav", "audio.mp3", "dubbed_final.wav",
background_cache=proj.background_audio(),
loudness_cache=proj.source_loudness,
)In sync mode, assemble_timeline also writes a per-line report beside the
output (dubbed.sync.json: onset, target, natural and placed length, stretch
rate and outcome of every line), and python -m mazinger.testing.bench_sync SOURCE DUB_VOICE SRT measures a finished dub's timing against its source
(onset error, length ratio, speech overlap, total length difference).
assemble_timeline prepares segments (load, tempo change, trim) on
assemble.ASSEMBLE_WORKERS threads (at most 8) and places them in order, so
the result is the same as a sequential run.
Redo one step for one segment, with the same prompts and settings as the full pipeline. The Editor uses these.
from mazinger.translate import translate_chunk
text = translate_chunk(
"and that is why the gradient vanishes",
prev_ctx=["We multiply many small numbers."], # source text of the chunks before
next_ctx=["Let's fix it with residual links."], # and after (context only)
duration=3.2, # the chunk's slot in seconds; sets the word budget
description=desc, # from describe_content (optional)
client=client,
llm_model="gpt-4.1",
source_language="English",
target_language="Spanish",
)Full signature:
translate_chunk(
source_text,
*,
prev_ctx=None, next_ctx=None, # list[str]
duration, # float, seconds
description=None, # dict
client,
llm_model="gpt-4.1",
source_language="auto",
target_language="English",
words_per_second=None, # auto: TTS speech rate of the target language
duration_budget=0.85,
translate_technical_terms=False,
user_instructions="",
video_meta=None, # dict — the video's title, description and channel, as context
thumb_paths=None, start=None, # screenshots inside the chunk's time range are attached
usage_tracker=None,
)It raises ValueError if the source text is empty or no translation can be
parsed from the reply. It never returns the source text as the translation.
For dubs made with a template translation model (such as translategemma), use
translate.translate_text_simple(text, client, llm_model=..., source_language=..., target_language=...).
from mazinger import tts
model = tts.load_model(engine="qwen", device="cuda:0")
voice = tts.create_voice_prompt(model, "speaker.wav", "Transcript.", engine="qwen")
path, seconds = tts.synthesize_one(voice, "Hola a todos.", "./seg_0042.wav", language="Spanish")It always overwrites out_path. The audio is written to <out_path>.part and
then renamed, so an interrupted run never leaves a truncated WAV behind.
synthesize_segments uses it for each entry.
from mazinger.transcribe import transcribe_clip
text = transcribe_clip(
"audio.mp3", 83.4, 87.9, # the range to transcribe, in seconds
pad=0.3, # extra audio on each side, so edge words are not cut
method="faster-whisper", device="cuda", language="en",
)Keyword arguments go to transcribe(). Recognized segments whose midpoint
falls inside the padding belong to the neighboring chunks and are dropped.
Returns "" for silence.
mazinger.editor holds the Editor's logic without the UI. See
Editor → Python API.
from mazinger.subtitle import SubtitleStyle, burn_subtitles, download_google_font
style = SubtitleStyle(
font="DejaVu Sans",
font_size=28,
font_color="yellow",
position="bottom",
bold=True,
)
# Burn subtitles (keep original audio)
burn_subtitles("video.mp4", "output.mp4", "translated.srt", style)
# Burn subtitles and replace audio
burn_subtitles("video.mp4", "output.mp4", "translated.srt", style, audio_path="dubbed.wav")
# Use a Google Font
font_path = download_google_font("Noto Sans Arabic")
style = SubtitleStyle(font_file=font_path, font_size=24)
burn_subtitles("video.mp4", "output.mp4", "translated.srt", style)from mazinger.profiles import fetch_profile
# From HuggingFace
voice_path, script_path = fetch_profile("abubakr")
# voice_path → /tmp/mazinger-dubber-profiles/abubakr/voice.wav
# script_path → /tmp/mazinger-dubber-profiles/abubakr/script.txt
# From a local directory
voice_path, script_path = fetch_profile("./my-profile")Files are cached in the system temp directory. Non-WAV voice files are converted to 16-kHz mono WAV automatically.
from mazinger.profiles import list_themes, generate_profile
# List all 16 pre-defined themes
themes = list_themes()
for t in themes:
print(f"{t['name']:20s} {t['gender']:8s} {', '.join(t['languages'])}")
# Generate a reusable profile from a theme
voice_path, script_path = generate_profile(
"narrator-m", "Spanish", "./my-profile",
device="cuda:0", dtype="bfloat16",
)
# Creates: ./my-profile/voice.wav and ./my-profile/script.txt
# Use with: --clone-profile ./my-profileTracks token usage across LLM calls.
from mazinger import LLMUsageTracker
tracker = LLMUsageTracker()
# Pass to any LLM-calling function
translated = translate_srt(srt_text, desc, thumbs, client, usage_tracker=tracker)
# Inspect results
print(tracker.report()) # Formatted usage report
print(tracker.total_input) # Total input tokens
print(tracker.total_output) # Total output tokens
print(tracker.total_tokens) # Combined total
print(tracker.summary_by_stage()) # Dict grouped by stageRecords are also saved to <project>/llm_usage.json when running through the pipeline.
from mazinger.srt import parse_file, parse_string, format_srt
entries = parse_file("subtitles.srt")
# Returns: [{"idx": "1", "start": 5.0, "end": 10.5, "text": "Hello world"}, ...]
entries = parse_string(srt_content)
srt_text = format_srt(entries)from mazinger.utils import (
sanitize_filename,
get_audio_duration,
save_json,
load_json,
image_to_base64,
make_image_content,
)
slug = sanitize_filename("My Video Title! (2024)") # "my-video-title-2024"
duration = get_audio_duration("audio.mp3") # seconds as float
save_json(data, "output.json")
data = load_json("input.json")
b64 = image_to_base64("thumb.jpg")
content = make_image_content("thumb.jpg", detail="low") # OpenAI vision block