240 lines
6.5 KiB
Python
240 lines
6.5 KiB
Python
"""
|
|
ElevenLabs Video Translation/Dubbing Module
|
|
|
|
Uses ElevenLabs Dubbing API to translate video audio to different languages.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import httpx
|
|
from typing import Optional
|
|
|
|
ELEVENLABS_API_BASE = "https://api.elevenlabs.io/v1"
|
|
|
|
# Supported target languages for dubbing
|
|
SUPPORTED_LANGUAGES = {
|
|
"en": "English",
|
|
"es": "Spanish",
|
|
"fr": "French",
|
|
"de": "German",
|
|
"it": "Italian",
|
|
"pt": "Portuguese",
|
|
"pl": "Polish",
|
|
"hi": "Hindi",
|
|
"ja": "Japanese",
|
|
"ko": "Korean",
|
|
"zh": "Chinese",
|
|
"ar": "Arabic",
|
|
"ru": "Russian",
|
|
"tr": "Turkish",
|
|
"nl": "Dutch",
|
|
"sv": "Swedish",
|
|
"id": "Indonesian",
|
|
"fil": "Filipino",
|
|
"ms": "Malay",
|
|
"vi": "Vietnamese",
|
|
"th": "Thai",
|
|
"uk": "Ukrainian",
|
|
"el": "Greek",
|
|
"cs": "Czech",
|
|
"fi": "Finnish",
|
|
"ro": "Romanian",
|
|
"da": "Danish",
|
|
"bg": "Bulgarian",
|
|
"hr": "Croatian",
|
|
"sk": "Slovak",
|
|
"ta": "Tamil",
|
|
}
|
|
|
|
|
|
def create_dubbing_project(
|
|
video_path: str,
|
|
target_language: str,
|
|
api_key: str,
|
|
source_language: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
Create a new dubbing project with ElevenLabs.
|
|
|
|
Args:
|
|
video_path: Path to the video file
|
|
target_language: Target language code (e.g., 'es', 'fr', 'de')
|
|
api_key: ElevenLabs API key
|
|
source_language: Source language code (auto-detected if None)
|
|
|
|
Returns:
|
|
dict with dubbing_id and expected_duration_sec
|
|
"""
|
|
url = f"{ELEVENLABS_API_BASE}/dubbing"
|
|
|
|
headers = {
|
|
"xi-api-key": api_key,
|
|
}
|
|
|
|
# Prepare form data
|
|
data = {
|
|
"target_lang": target_language,
|
|
"mode": "automatic",
|
|
"num_speakers": "0",
|
|
"watermark": "false",
|
|
}
|
|
|
|
if source_language:
|
|
data["source_lang"] = source_language
|
|
|
|
# Open and send the video file
|
|
with open(video_path, "rb") as video_file:
|
|
files = {
|
|
"file": (os.path.basename(video_path), video_file, "video/mp4")
|
|
}
|
|
|
|
print(f"[ElevenLabs] Creating dubbing project for {target_language}...")
|
|
with httpx.Client(timeout=300.0) as client:
|
|
response = client.post(url, headers=headers, data=data, files=files)
|
|
|
|
if response.status_code not in [200, 201]:
|
|
error_msg = response.text
|
|
try:
|
|
error_data = response.json()
|
|
error_msg = error_data.get("detail", {}).get("message", response.text)
|
|
except:
|
|
pass
|
|
raise Exception(f"ElevenLabs API error: {error_msg}")
|
|
|
|
result = response.json()
|
|
print(f"[ElevenLabs] Dubbing project created: {result.get('dubbing_id')}")
|
|
return result
|
|
|
|
|
|
def get_dubbing_status(dubbing_id: str, api_key: str) -> dict:
|
|
"""
|
|
Check the status of a dubbing project.
|
|
|
|
Returns:
|
|
dict with status ('dubbing', 'dubbed', 'failed') and other metadata
|
|
"""
|
|
url = f"{ELEVENLABS_API_BASE}/dubbing/{dubbing_id}"
|
|
|
|
headers = {
|
|
"xi-api-key": api_key,
|
|
}
|
|
|
|
with httpx.Client(timeout=30.0) as client:
|
|
response = client.get(url, headers=headers)
|
|
|
|
if response.status_code != 200:
|
|
raise Exception(f"Failed to get dubbing status: {response.text}")
|
|
|
|
return response.json()
|
|
|
|
|
|
def download_dubbed_video(
|
|
dubbing_id: str,
|
|
target_language: str,
|
|
output_path: str,
|
|
api_key: str
|
|
) -> str:
|
|
"""
|
|
Download the dubbed video file.
|
|
|
|
Args:
|
|
dubbing_id: The dubbing project ID
|
|
target_language: Target language code
|
|
output_path: Where to save the dubbed video
|
|
api_key: ElevenLabs API key
|
|
|
|
Returns:
|
|
Path to the downloaded file
|
|
"""
|
|
url = f"{ELEVENLABS_API_BASE}/dubbing/{dubbing_id}/audio/{target_language}"
|
|
|
|
headers = {
|
|
"xi-api-key": api_key,
|
|
}
|
|
|
|
print(f"[ElevenLabs] Downloading dubbed video...")
|
|
with httpx.Client(timeout=120.0) as client:
|
|
with client.stream("GET", url, headers=headers) as response:
|
|
if response.status_code != 200:
|
|
raise Exception(f"Failed to download dubbed video: {response.text}")
|
|
|
|
with open(output_path, "wb") as f:
|
|
for chunk in response.iter_bytes(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
print(f"[ElevenLabs] Dubbed video saved to: {output_path}")
|
|
return output_path
|
|
|
|
|
|
def translate_video(
|
|
video_path: str,
|
|
output_path: str,
|
|
target_language: str,
|
|
api_key: str,
|
|
source_language: Optional[str] = None,
|
|
max_wait_seconds: int = 600,
|
|
poll_interval: int = 5,
|
|
) -> str:
|
|
"""
|
|
Translate a video to a target language using ElevenLabs dubbing.
|
|
|
|
This is a blocking call that waits for the dubbing to complete.
|
|
|
|
Args:
|
|
video_path: Path to input video
|
|
output_path: Path to save translated video
|
|
target_language: Target language code
|
|
api_key: ElevenLabs API key
|
|
source_language: Source language code (auto-detected if None)
|
|
max_wait_seconds: Maximum time to wait for dubbing (default 10 min)
|
|
poll_interval: Seconds between status checks
|
|
|
|
Returns:
|
|
Path to the translated video
|
|
"""
|
|
# Create dubbing project
|
|
project = create_dubbing_project(
|
|
video_path=video_path,
|
|
target_language=target_language,
|
|
api_key=api_key,
|
|
source_language=source_language,
|
|
)
|
|
|
|
dubbing_id = project["dubbing_id"]
|
|
expected_duration = project.get("expected_duration_sec", 60)
|
|
|
|
print(f"[ElevenLabs] Dubbing ID: {dubbing_id}, Expected duration: {expected_duration}s")
|
|
|
|
# Poll for completion
|
|
start_time = time.time()
|
|
while True:
|
|
elapsed = time.time() - start_time
|
|
if elapsed > max_wait_seconds:
|
|
raise Exception(f"Dubbing timed out after {max_wait_seconds} seconds")
|
|
|
|
status = get_dubbing_status(dubbing_id, api_key)
|
|
current_status = status.get("status", "unknown")
|
|
|
|
print(f"[ElevenLabs] Status: {current_status} (elapsed: {int(elapsed)}s)")
|
|
|
|
if current_status == "dubbed":
|
|
# Download the result
|
|
return download_dubbed_video(
|
|
dubbing_id=dubbing_id,
|
|
target_language=target_language,
|
|
output_path=output_path,
|
|
api_key=api_key,
|
|
)
|
|
|
|
elif current_status == "failed":
|
|
error = status.get("error", "Unknown error")
|
|
raise Exception(f"Dubbing failed: {error}")
|
|
|
|
# Still processing, wait and poll again
|
|
time.sleep(poll_interval)
|
|
|
|
|
|
def get_supported_languages() -> dict:
|
|
"""Return dict of supported language codes and names."""
|
|
return SUPPORTED_LANGUAGES.copy()
|