VideoTranslate / translate.py
sushilideaclan01's picture
first commit
a5142b1
Raw
History Blame Contribute Delete
7.62 kB
#!/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())