transcriber / app.py
asuilin's picture
Initial upload
3bb09ca verified
Raw
History Blame Contribute Delete
6.34 kB
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
import threading
from pathlib import Path
import gradio as gr
from dotenv import load_dotenv
load_dotenv()
MAIN = Path(__file__).parent / "main.py"
_ANSI = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]|\r")
_CLEANUP_DELAY = 30 * 60 # seconds to keep the output MKV available for download
def _strip(text: str) -> str:
return _ANSI.sub("", text)
def _delete_later(path: str, delay: float = _CLEANUP_DELAY) -> None:
"""Delete a file or directory after `delay` seconds on a daemon thread."""
def _do() -> None:
shutil.rmtree(path, ignore_errors=True)
threading.Timer(delay, _do).start()
def run(
url: str,
input_video: str | None,
language: str,
translate_to: str,
resolution: int,
max_length: int,
):
source = (input_video or "").strip()
url = (url or "").strip()
if not source and not url:
yield "Error: provide a YouTube URL or upload a video file.\n", gr.update(visible=False)
return
if source and url:
yield "Error: provide either a YouTube URL or a video file, not both.\n", gr.update(visible=False)
return
tmpdir = tempfile.mkdtemp(prefix="transcriber-")
cmd = [sys.executable, str(MAIN)]
if source:
cmd += ["--input-video", source]
else:
cmd += [url]
cmd += ["--output-dir", tmpdir]
if (language or "").strip():
cmd += ["--language", language.strip()]
if (translate_to or "").strip():
cmd += ["--translate-to", translate_to.strip()]
if resolution != 720:
cmd += ["--resolution", str(int(resolution))]
if max_length != 100:
cmd += ["--max-length", str(int(max_length))]
env = {**os.environ, "NO_COLOR": "1"}
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env,
)
lines: list[str] = []
try:
for line in proc.stdout: # type: ignore[union-attr]
clean = _strip(line)
if not clean.strip():
continue # skip blank lines and yt-dlp progress-bar clear lines
# Collapse consecutive yt-dlp [download] progress lines in-place
if clean.startswith("[download]") and lines and lines[-1].startswith("[download]"):
lines[-1] = clean
else:
lines.append(clean)
yield "".join(lines), gr.update(visible=False)
except GeneratorExit:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
shutil.rmtree(tmpdir, ignore_errors=True)
return
finally:
proc.wait()
mkv_files = list(Path(tmpdir).glob("*.mkv"))
if proc.returncode == 0 and mkv_files:
# Copy MKV into a named temp dir so the download filename matches the
# original title. Schedule the whole dir for deletion after the delay.
out_dir = tempfile.mkdtemp(prefix="transcriber-out-")
out_path = os.path.join(out_dir, mkv_files[0].name)
shutil.copy2(mkv_files[0], out_path)
shutil.rmtree(tmpdir, ignore_errors=True)
_delete_later(out_dir)
yield "".join(lines), gr.update(visible=True, value=out_path)
else:
shutil.rmtree(tmpdir, ignore_errors=True)
if proc.returncode != 0:
lines.append(f"\nProcess exited with code {proc.returncode}.\n")
else:
lines.append("\nNo output file produced.\n")
yield "".join(lines), gr.update(visible=False)
_CSS = """
#log textarea { font-family: monospace; font-size: 12px; }
"""
with gr.Blocks(title="transcriber", delete_cache=(_CLEANUP_DELAY, _CLEANUP_DELAY)) as demo:
browser_state = gr.BrowserState(
{"language": "", "translate_to": ""},
storage_key="transcriber-prefs",
secret="transcriber-2026",
)
gr.Markdown("## transcriber\nTranscribe a YouTube video and download it with embedded subtitles.")
with gr.Row():
url_box = gr.Textbox(
label="YouTube URL",
placeholder="https://www.youtube.com/watch?v=...",
scale=3,
)
file_input = gr.File(
label="Or upload a local video",
file_types=[".mp4", ".mkv", ".mov", ".avi", ".webm"],
scale=1,
)
with gr.Accordion("Options", open=False):
with gr.Row():
language = gr.Textbox(
label="Source language (ISO 639-3)",
placeholder="e.g. srb; blank = auto-detect",
)
translate_to = gr.Textbox(
label="Add subtitles translated to (ISO 639-3)",
placeholder="e.g. rus; blank = skip translation",
)
resolution = gr.Slider(144, 2160, step=1, value=720, label="Video size on shorter side (px)")
max_length = gr.Slider(40, 200, step=1, value=100, label="Max chars per subtitle cue")
with gr.Row():
run_btn = gr.Button("Run", variant="primary")
stop_btn = gr.Button("Stop", variant="stop")
log_box = gr.Textbox(
lines=15,
max_lines=40,
interactive=False,
autoscroll=True,
label="Progress",
elem_id="log",
)
output_file = gr.DownloadButton(label="Download MKV", visible=False, variant="primary", size="lg")
# Restore persisted language settings on page load
@demo.load(inputs=[browser_state], outputs=[language, translate_to])
def _restore(state: dict) -> tuple[str, str]:
return state.get("language", ""), state.get("translate_to", "")
# Save language settings whenever they change
@gr.on(
[language.change, translate_to.change],
inputs=[language, translate_to],
outputs=[browser_state],
)
def _save(lang: str, tl: str) -> dict:
return {"language": lang, "translate_to": tl}
run_event = run_btn.click(
fn=run,
inputs=[url_box, file_input, language, translate_to, resolution, max_length],
outputs=[log_box, output_file],
show_progress="full",
)
stop_btn.click(fn=None, cancels=[run_event])
demo.queue()
if __name__ == "__main__":
demo.launch(css=_CSS)