Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -32,6 +32,10 @@ class TranslationJob(BaseModel):
|
|
| 32 |
supabase_anon_key: Optional[str] = None
|
| 33 |
gemini_api_key: Optional[str] = None
|
| 34 |
elevenlabs_api_key: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def load_env_local():
|
| 37 |
"""Load environment variables from local .env.local file if it exists."""
|
|
@@ -374,11 +378,18 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 374 |
# 1.5. Standardize video (H.264/AAC, 25 FPS, YUV420p) for OpenCV / Wav2Lip stability
|
| 375 |
print(f"[{video_id}] Standardizing downloaded video to MP4 format...")
|
| 376 |
cmd_standardize = [
|
| 377 |
-
"ffmpeg", "-y", "-i", downloaded_video_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-r", "25",
|
| 379 |
"-c:a", "aac", "-ar", "16000", "-ac", "1",
|
| 380 |
input_video_path
|
| 381 |
-
]
|
| 382 |
subprocess.run(cmd_standardize, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 383 |
|
| 384 |
# 2. Extract audio from video using FFmpeg
|
|
@@ -438,11 +449,53 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 438 |
|
| 439 |
# 4. Synthesize voice cloning using ElevenLabs API
|
| 440 |
print(f"[{video_id}] Calling ElevenLabs TTS Voice Cloning with tone '{job.voice_tone}'...")
|
| 441 |
-
# We can use the default standard voice (e.g. Rachel: 21m00Tcm4TlvDq8ikWAM)
|
| 442 |
-
voice_id = "21m00Tcm4TlvDq8ikWAM"
|
| 443 |
-
elevenlabs_url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
| 444 |
|
| 445 |
elevenlabs_headers = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
"xi-api-key": elevenlabs_key,
|
| 447 |
"Content-Type": "application/json"
|
| 448 |
}
|
|
@@ -466,12 +519,25 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 466 |
}
|
| 467 |
}
|
| 468 |
|
| 469 |
-
tts_res = requests.post(elevenlabs_url, headers=
|
| 470 |
tts_res.raise_for_status()
|
| 471 |
|
| 472 |
with open(synthesized_audio_path, "wb") as f:
|
| 473 |
f.write(tts_res.content)
|
| 474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
# 5. Merge synthesized audio back with the original video (or run Lip-Sync if selected)
|
| 476 |
lipsync_output_path = f"temp_{video_id}_lipsync.mp4"
|
| 477 |
|
|
|
|
| 32 |
supabase_anon_key: Optional[str] = None
|
| 33 |
gemini_api_key: Optional[str] = None
|
| 34 |
elevenlabs_api_key: Optional[str] = None
|
| 35 |
+
use_voice_cloning: Optional[bool] = True
|
| 36 |
+
default_voice_id: Optional[str] = 'Xb7hH8MSUJpSbSDYk0k2'
|
| 37 |
+
trim_start: Optional[float] = None
|
| 38 |
+
trim_end: Optional[float] = None
|
| 39 |
|
| 40 |
def load_env_local():
|
| 41 |
"""Load environment variables from local .env.local file if it exists."""
|
|
|
|
| 378 |
# 1.5. Standardize video (H.264/AAC, 25 FPS, YUV420p) for OpenCV / Wav2Lip stability
|
| 379 |
print(f"[{video_id}] Standardizing downloaded video to MP4 format...")
|
| 380 |
cmd_standardize = [
|
| 381 |
+
"ffmpeg", "-y", "-i", downloaded_video_path
|
| 382 |
+
]
|
| 383 |
+
if job.trim_start is not None and job.trim_start > 0:
|
| 384 |
+
cmd_standardize.extend(["-ss", str(job.trim_start)])
|
| 385 |
+
if job.trim_end is not None and job.trim_end > 0:
|
| 386 |
+
cmd_standardize.extend(["-to", str(job.trim_end)])
|
| 387 |
+
|
| 388 |
+
cmd_standardize.extend([
|
| 389 |
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-r", "25",
|
| 390 |
"-c:a", "aac", "-ar", "16000", "-ac", "1",
|
| 391 |
input_video_path
|
| 392 |
+
])
|
| 393 |
subprocess.run(cmd_standardize, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 394 |
|
| 395 |
# 2. Extract audio from video using FFmpeg
|
|
|
|
| 449 |
|
| 450 |
# 4. Synthesize voice cloning using ElevenLabs API
|
| 451 |
print(f"[{video_id}] Calling ElevenLabs TTS Voice Cloning with tone '{job.voice_tone}'...")
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
elevenlabs_headers = {
|
| 454 |
+
"xi-api-key": elevenlabs_key
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
voice_id = None
|
| 458 |
+
cloned_voice_created = False
|
| 459 |
+
|
| 460 |
+
# Try to dynamically clone the voice using the extracted audio if requested
|
| 461 |
+
if job.use_voice_cloning:
|
| 462 |
+
try:
|
| 463 |
+
print(f"[{video_id}] Attempting to create a temporary voice clone from extracted audio...")
|
| 464 |
+
add_voice_url = "https://api.elevenlabs.io/v1/voices/add"
|
| 465 |
+
|
| 466 |
+
# Open audio file for upload
|
| 467 |
+
with open(extracted_audio_path, "rb") as audio_file:
|
| 468 |
+
files = {
|
| 469 |
+
"files": (os.path.basename(extracted_audio_path), audio_file, "audio/mpeg")
|
| 470 |
+
}
|
| 471 |
+
data = {
|
| 472 |
+
"name": f"VoiceClips_{video_id}",
|
| 473 |
+
"description": f"Temporary cloned voice for job {video_id}"
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
add_res = requests.post(add_voice_url, headers=elevenlabs_headers, files=files, data=data)
|
| 477 |
+
|
| 478 |
+
if add_res.status_code == 200:
|
| 479 |
+
voice_id = add_res.json().get("voice_id")
|
| 480 |
+
cloned_voice_created = True
|
| 481 |
+
print(f"[{video_id}] Voice clone created successfully. Voice ID: {voice_id}")
|
| 482 |
+
else:
|
| 483 |
+
print(f"[{video_id}] Voice cloning failed with status code {add_res.status_code}: {add_res.text}")
|
| 484 |
+
print(f"[{video_id}] Falling back to standard pre-made voice ({job.default_voice_id}).")
|
| 485 |
+
except Exception as clone_err:
|
| 486 |
+
print(f"[{video_id}] Exception during voice cloning: {clone_err}")
|
| 487 |
+
print(f"[{video_id}] Falling back to standard pre-made voice ({job.default_voice_id}).")
|
| 488 |
+
else:
|
| 489 |
+
print(f"[{video_id}] Voice cloning disabled by user. Using standard pre-made voice ({job.default_voice_id}).")
|
| 490 |
+
|
| 491 |
+
# Fallback to default voice if cloning wasn't successful
|
| 492 |
+
if not voice_id:
|
| 493 |
+
voice_id = job.default_voice_id or "Xb7hH8MSUJpSbSDYk0k2" # Alice (pre-made voice, works on free tier)
|
| 494 |
+
|
| 495 |
+
elevenlabs_url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
| 496 |
+
|
| 497 |
+
# Content-Type header is needed for the JSON payload of the TTS request
|
| 498 |
+
tts_headers = {
|
| 499 |
"xi-api-key": elevenlabs_key,
|
| 500 |
"Content-Type": "application/json"
|
| 501 |
}
|
|
|
|
| 519 |
}
|
| 520 |
}
|
| 521 |
|
| 522 |
+
tts_res = requests.post(elevenlabs_url, headers=tts_headers, json=elevenlabs_payload)
|
| 523 |
tts_res.raise_for_status()
|
| 524 |
|
| 525 |
with open(synthesized_audio_path, "wb") as f:
|
| 526 |
f.write(tts_res.content)
|
| 527 |
|
| 528 |
+
# If we successfully created a temporary cloned voice, delete it now to free up slots
|
| 529 |
+
if cloned_voice_created and voice_id:
|
| 530 |
+
try:
|
| 531 |
+
print(f"[{video_id}] Deleting temporary cloned voice {voice_id}...")
|
| 532 |
+
delete_url = f"https://api.elevenlabs.io/v1/voices/{voice_id}"
|
| 533 |
+
del_res = requests.delete(delete_url, headers=elevenlabs_headers)
|
| 534 |
+
if del_res.status_code == 200:
|
| 535 |
+
print(f"[{video_id}] Temporary cloned voice deleted successfully.")
|
| 536 |
+
else:
|
| 537 |
+
print(f"[{video_id}] Failed to delete voice: {del_res.text}")
|
| 538 |
+
except Exception as del_err:
|
| 539 |
+
print(f"[{video_id}] Exception deleting voice: {del_err}")
|
| 540 |
+
|
| 541 |
# 5. Merge synthesized audio back with the original video (or run Lip-Sync if selected)
|
| 542 |
lipsync_output_path = f"temp_{video_id}_lipsync.mp4"
|
| 543 |
|