feat: implement vertical crop, local LLM deepseek-v4-flash, multi-threading, word-level pink box highlight subtitles, and Nextcloud scan sync
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user