#!/usr/bin/env python3 """ Submit each video segment to Replicate's heygen/video-translate model in parallel, poll until each prediction completes, and download the translated output. Usage: python3 translate.py """ from __future__ import annotations import json import os import pathlib import sys import time import urllib.request import urllib.error ROOT = pathlib.Path(__file__).resolve().parent def _path_env(var: str, default: pathlib.Path) -> pathlib.Path: raw = os.environ.get(var) if not raw: return default p = pathlib.Path(raw) return p if p.is_absolute() else ROOT / p SEG_DIR = _path_env("SEG_DIR", ROOT / "segments") OUT_DIR = _path_env("OUT_DIR", ROOT / "translated") STATE_FILE = _path_env("STATE_FILE", ROOT / "predictions.json") # replicate_upload: POST files to Replicate (works when ngrok fetch times out). # url: serve segments yourself and set NGROK_URL (or any HTTPS base). INPUT_DELIVERY = os.environ.get("INPUT_DELIVERY", "replicate_upload") NGROK_URL = os.environ.get("NGROK_URL", "").rstrip("/") TOKEN = os.environ["REPLICATE_API_TOKEN"] LANGUAGE = os.environ.get("OUTPUT_LANGUAGE", "English") MODE = os.environ.get("MODE", "precision") API = "https://api.replicate.com/v1" HEADERS = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", "User-Agent": "translate-script/1.0", } def upload_replicate_file(path: pathlib.Path) -> str: """Upload a local file to Replicate; return the authenticated file URL for model input.""" import subprocess abs_path = path.resolve() proc = subprocess.run( [ "curl", "-s", "-S", "-X", "POST", f"{API}/files", "-H", f"Authorization: Token {TOKEN}", "-F", f"content=@{abs_path};type=video/mp4;filename={path.name}", "-F", "metadata={};type=application/json", ], capture_output=True, text=True, timeout=1200, check=False, ) if proc.returncode != 0: raise RuntimeError(f"upload curl rc={proc.returncode} stderr={proc.stderr!r}") try: data = json.loads(proc.stdout) except json.JSONDecodeError as exc: raise RuntimeError(f"upload bad JSON: {proc.stdout[:800]}") from exc if proc.stderr: print(proc.stderr, file=sys.stderr) url = (data.get("urls") or {}).get("get") if not url: raise RuntimeError(f"upload missing urls.get: {proc.stdout[:800]}") return url def resolve_video_url(seg: pathlib.Path, existing: dict | None) -> str: if INPUT_DELIVERY == "url": if not NGROK_URL: print("NGROK_URL is required when INPUT_DELIVERY=url", file=sys.stderr) sys.exit(1) return f"{NGROK_URL}/{seg.name}" cached = (existing or {}).get("input_video_url") if cached: return cached print(f"[{seg.name}] uploading to Replicate ({seg.stat().st_size // (1024 * 1024)} MiB)...") return upload_replicate_file(seg) def http(method: str, url: str, body: dict | None = None) -> dict: data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, method=method, headers=HEADERS) try: with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: raise RuntimeError(f"HTTP {e.code} on {method} {url}: {e.read().decode()}") from None def submit(seg: pathlib.Path, video_url: str) -> dict: payload = { "input": { "video": video_url, "output_language": LANGUAGE, "mode": MODE, } } return http( "POST", f"{API}/models/heygen/video-translate/predictions", body=payload, ) def poll(prediction_id: str) -> dict: return http("GET", f"{API}/predictions/{prediction_id}") def download(url: str, dest: pathlib.Path) -> None: req = urllib.request.Request(url, headers={"User-Agent": HEADERS["User-Agent"]}) with urllib.request.urlopen(req, timeout=900) as resp, dest.open("wb") as f: while True: chunk = resp.read(1 << 20) if not chunk: break f.write(chunk) def load_state() -> dict: if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text()) return {} def save_state(state: dict) -> None: STATE_FILE.write_text(json.dumps(state, indent=2)) def main() -> int: OUT_DIR.mkdir(exist_ok=True) segments = sorted(SEG_DIR.glob("seg_*.mp4")) if not segments: print("No segments found.", file=sys.stderr) return 1 state = load_state() print(f"Language: {LANGUAGE} Mode: {MODE} Delivery: {INPUT_DELIVERY} Segments: {len(segments)}") if INPUT_DELIVERY == "url": print(f"Public base: {NGROK_URL}\n") else: print("Using Replicate file uploads (no public URL needed).\n") for seg in segments: if seg.name in state and state[seg.name].get("status") in ("starting", "processing", "succeeded"): print(f"[{seg.name}] reusing existing prediction {state[seg.name]['id']}") continue video_url = resolve_video_url(seg, state.get(seg.name)) pred = submit(seg, video_url) state[seg.name] = { "id": pred["id"], "status": pred["status"], "submitted_at": pred.get("created_at"), "input_video_url": video_url, } save_state(state) print(f"[{seg.name}] submitted -> {pred['id']} ({pred['status']})") print("\nPolling all predictions until done...\n") pending = {name for name, info in state.items() if info["status"] not in ("succeeded", "failed", "canceled")} started = time.time() while pending: for name in list(pending): info = state[name] try: pred = poll(info["id"]) except Exception as exc: print(f"[{name}] poll error: {exc}") continue if pred["status"] != info["status"]: print(f"[{name}] {info['status']} -> {pred['status']} (elapsed {int(time.time() - started)}s)") info["status"] = pred["status"] if pred["status"] == "succeeded": info["output"] = pred["output"] pending.discard(name) elif pred["status"] in ("failed", "canceled"): info["error"] = pred.get("error") or "(no error message)" info["logs"] = pred.get("logs", "")[-2000:] pending.discard(name) save_state(state) if pending: time.sleep(15) print("\nDownloading outputs...\n") failures = [] for name, info in state.items(): if info["status"] != "succeeded": failures.append((name, info.get("error", "unknown"))) continue out_path = OUT_DIR / name if out_path.exists() and out_path.stat().st_size > 0: print(f"[{name}] already downloaded ({out_path.stat().st_size} bytes)") continue print(f"[{name}] downloading {info['output']}") download(info["output"], out_path) print(f"[{name}] saved -> {out_path} ({out_path.stat().st_size} bytes)") if failures: print("\nFAILURES:") for name, err in failures: print(f" - {name}: {err}") return 2 print("\nAll segments translated and downloaded.") return 0 if __name__ == "__main__": sys.exit(main())