""" Mazinger Dubber — Main Gradio UI Cinematic film-editing control panel aesthetic. DaVinci Resolve meets a broadcast dubbing suite. """ from __future__ import annotations import re from typing import Generator import gradio as gr from theme import MazingerTheme, CUSTOM_CSS from pipeline_runner import run_pipeline, STAGE_NAMES # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- VOICE_THEMES = [ "narrator-m", "narrator-f", "young-m", "young-f", "deep-m", "deep-f", "warm-m", "warm-f", "news-m", "news-f", "storyteller-m", "storyteller-f", "kid-m", "kid-f", "teen-m", "teen-f", ] VOICE_PROFILES = [ "abubakr", "daheeh-v1", "3b1b", "italian-v1", "morgan-freeman", "trump-v1", ] # Languages supported by voice themes (TTS generation) THEME_LANGUAGES = [ "Chinese", "English", "French", "German", "Italian", "Japanese", "Korean", "Portuguese", "Russian", "Spanish", ] # All languages supported by translation (34 from mazinger repo) LANGUAGES = [ "Arabic", "Bengali", "Chinese (Simplified)", "Chinese (Traditional)", "Czech", "Danish", "Dutch", "English", "Finnish", "French", "German", "Greek", "Hebrew", "Hindi", "Hungarian", "Indonesian", "Italian", "Japanese", "Korean", "Malay", "Norwegian", "Persian", "Polish", "Portuguese", "Romanian", "Russian", "Spanish", "Swedish", "Thai", "Turkish", "Ukrainian", "Urdu", "Vietnamese", ] # 3-letter abbreviations matching STAGE_NAMES order (10 stages) _STAGE_ABBREVS = ["DWN", "STT", "THB", "DSC", "REV", "TRN", "SEG", "TTS", "ASM", "SUB"] # --------------------------------------------------------------------------- # Pipeline HTML builder # --------------------------------------------------------------------------- def build_pipeline_html(active: int = -1, done_up_to: int = -1) -> str: """ Render a horizontal hex-node pipeline visualizer. active — index of the currently running stage (amber pulse) done_up_to — stages with index < done_up_to are marked done (teal) """ nodes_html = [] stage_count = len(STAGE_NAMES) for i, name in enumerate(STAGE_NAMES): abbrev = _STAGE_ABBREVS[i] if i < len(_STAGE_ABBREVS) else name[:3].upper() # Determine node state if i < done_up_to: node_cls = "pipeline-node done" elif i == active: node_cls = "pipeline-node active" else: node_cls = "pipeline-node" node_html = f"""
'
)
return ""
# ---------------------------------------------------------------------------
# Voice mode toggle helpers
# ---------------------------------------------------------------------------
def _voice_mode_change(mode: str) -> tuple[dict, dict, dict, dict]:
"""Show/hide voice control components based on selected mode."""
show_theme = mode == "theme"
show_profile = mode == "profile"
show_clone = mode == "clone"
return (
gr.update(visible=show_theme), # voice_theme dropdown
gr.update(visible=show_profile), # voice_profile dropdown
gr.update(visible=show_clone), # voice_sample audio
gr.update(visible=show_clone), # voice_script textbox
)
# ---------------------------------------------------------------------------
# Main pipeline processor (generator)
# ---------------------------------------------------------------------------
def process_video(
source_url: str,
source_upload,
target_lang: str,
source_lang: str,
voice_mode: str,
voice_theme: str,
voice_profile: str,
voice_sample,
voice_script: str,
output_type: str,
embed_subs: bool,
sub_font: str,
sub_size: int,
slice_start: str,
slice_end: str,
sub_position: str,
sub_color: str,
sub_bg_alpha: float,
sub_outline_width: int,
sub_bold: bool,
sub_line_spacing: int,
sub_source: str,
asr_review: bool,
tempo_mode: str,
max_tempo: float,
words_per_second: float,
duration_budget: float,
translate_technical: bool,
) -> Generator:
"""
Drive run_pipeline() and yield 6 UI updates per stage event:
[pipeline_html, log_text, video_output, audio_output, srt_preview, srt_download]
"""
# Resolve source: uploaded file takes priority over URL
source = source_url or ""
if source_upload is not None:
source = source_upload if isinstance(source_upload, str) else source_upload.name
if not source:
yield (
build_pipeline_html(),
"[ERROR] Please provide a video URL or upload a file.",
None, None, "", None,
)
return
log_lines: list[str] = []
video_path = None
# Validate theme + language compatibility
if voice_mode == "theme" and target_lang not in THEME_LANGUAGES:
yield (
build_pipeline_html(),
f"[ERROR] Voice themes don't support {target_lang}. Use auto-clone, a voice profile, or custom clone instead.",
None, None, "", None,
)
return
audio_path = None
srt_content = ""
srt_path = None
# Convert slider defaults to None when user hasn't changed them
wps_val = words_per_second if words_per_second != 0 else None
db_val = duration_budget if duration_budget != 0 else None
for stage_idx, log_msg, result in run_pipeline(
source=source,
target_language=target_lang,
source_language=source_lang,
voice_mode=voice_mode,
voice_theme=voice_theme,
voice_profile=voice_profile,
voice_sample=voice_sample,
voice_script=voice_script,
output_type=output_type,
embed_subtitles=embed_subs,
subtitle_font=sub_font,
subtitle_font_size=sub_size,
slice_start=slice_start,
slice_end=slice_end,
subtitle_position=sub_position,
subtitle_color=sub_color,
subtitle_bg_alpha=sub_bg_alpha,
subtitle_outline_width=sub_outline_width,
subtitle_bold=sub_bold,
subtitle_line_spacing=sub_line_spacing,
subtitle_source=sub_source,
asr_review=asr_review,
tempo_mode=tempo_mode,
max_tempo=max_tempo,
words_per_second=wps_val,
duration_budget=db_val,
translate_technical_terms=translate_technical,
):
log_lines.append(log_msg)
# Collect final results
if result.get("final_path"):
fpath = result["final_path"]
if fpath.endswith((".mp4", ".mkv", ".avi", ".mov", ".webm")):
video_path = fpath
else:
audio_path = fpath
if result.get("srt_path"):
srt_path = result["srt_path"]
try:
srt_content = open(srt_path).read()
except Exception:
pass
is_done = result.get("done", False)
done_up_to = stage_idx - 1 if not is_done else len(STAGE_NAMES)
active = stage_idx - 1 if not is_done and stage_idx <= len(STAGE_NAMES) else -1
yield (
build_pipeline_html(active=active, done_up_to=done_up_to),
"\n".join(log_lines),
video_path,
audio_path,
srt_content,
srt_path,
)
# ---------------------------------------------------------------------------
# Gradio Blocks layout
# ---------------------------------------------------------------------------
with gr.Blocks(
theme=MazingerTheme(),
css=CUSTOM_CSS,
title="Mazinger Dubber",
) as demo:
# ── Header ──────────────────────────────────────────────────────────────
gr.HTML("""
End-to-end video dubbing studio