Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -21,10 +21,13 @@ class TranslationJob(BaseModel):
|
|
| 21 |
caption_style: str
|
| 22 |
resolution: str
|
| 23 |
user_token: str
|
| 24 |
-
# Null (boş) değerlerin 422 hatası vermemesi için Optional yapıldı
|
| 25 |
custom_font: Optional[str] = None
|
| 26 |
custom_size: Optional[int] = None
|
| 27 |
custom_color: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def load_env_local():
|
| 30 |
"""Load environment variables from local .env.local file if it exists."""
|
|
@@ -119,6 +122,7 @@ def setup_wav2lip():
|
|
| 119 |
print("Wav2Lip environment setup completed successfully.")
|
| 120 |
return True
|
| 121 |
|
|
|
|
| 122 |
def get_supabase_config():
|
| 123 |
supabase_url = os.environ.get("NEXT_PUBLIC_SUPABASE_URL", "")
|
| 124 |
supabase_anon_key = os.environ.get("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
|
|
@@ -134,13 +138,22 @@ def get_api_keys():
|
|
| 134 |
|
| 135 |
return gemini_key, elevenlabs_key, (is_gemini_mock or is_eleven_mock)
|
| 136 |
|
| 137 |
-
def update_db_status(video_id: str, user_token: str, status: str, translated_url: str = None, error_message: str = None):
|
| 138 |
-
supabase_url
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
if not supabase_url or not supabase_anon_key:
|
| 140 |
print("Error: Supabase config is missing.")
|
| 141 |
return
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
headers = {
|
| 145 |
"apikey": supabase_anon_key,
|
| 146 |
"Authorization": f"Bearer {user_token}",
|
|
@@ -226,6 +239,7 @@ def get_ffmpeg_style(caption_style: str, custom_font: str = None, custom_size: i
|
|
| 226 |
if custom_size:
|
| 227 |
style["Fontsize"] = str(custom_size)
|
| 228 |
if custom_color:
|
|
|
|
| 229 |
color_map = {
|
| 230 |
"yellow": "&H0000FFFF",
|
| 231 |
"green": "&H0000FF00",
|
|
@@ -258,57 +272,86 @@ def process_video_task(job: TranslationJob):
|
|
| 258 |
user_token = job.user_token
|
| 259 |
|
| 260 |
print(f"Starting background process for video ID: {video_id}")
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
if is_mock_mode:
|
| 264 |
-
run_simulation_pipeline(job)
|
| 265 |
else:
|
| 266 |
-
run_production_pipeline(job, gemini_key, elevenlabs_key)
|
| 267 |
|
| 268 |
-
def run_simulation_pipeline(job: TranslationJob):
|
| 269 |
video_id = job.video_id
|
| 270 |
user_token = job.user_token
|
| 271 |
|
| 272 |
print(f"[{video_id}] Running in SIMULATION MODE...")
|
|
|
|
| 273 |
try:
|
| 274 |
-
|
| 275 |
print(f"[{video_id}] Step 1: Simulating audio extraction...")
|
| 276 |
-
time.sleep(3)
|
| 277 |
-
print(f"[{video_id}] Step 2: Simulating Gemini translation...")
|
| 278 |
-
update_db_status(video_id, user_token, "processing")
|
| 279 |
time.sleep(2)
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
time.sleep(2)
|
| 282 |
|
|
|
|
| 283 |
if job.has_captions:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
time.sleep(2)
|
|
|
|
| 285 |
if job.has_lip_sync:
|
|
|
|
| 286 |
time.sleep(2)
|
|
|
|
|
|
|
|
|
|
| 287 |
|
|
|
|
|
|
|
| 288 |
mock_output_video = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"
|
| 289 |
-
|
| 290 |
-
|
|
|
|
| 291 |
|
| 292 |
except Exception as e:
|
| 293 |
print(f"[{video_id}] Simulation Error: {e}")
|
| 294 |
-
update_db_status(video_id, user_token, "failed", error_message=f"Simülasyon Hatası: {str(e)}")
|
| 295 |
|
| 296 |
-
def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key: str):
|
| 297 |
video_id = job.video_id
|
| 298 |
user_token = job.user_token
|
| 299 |
-
supabase_url, supabase_anon_key = get_supabase_config()
|
| 300 |
|
| 301 |
print(f"[{video_id}] Running in PRODUCTION MODE...")
|
| 302 |
|
| 303 |
-
#
|
| 304 |
url_without_params = job.original_video_url.split('?')[0]
|
| 305 |
file_ext = url_without_params.split('.')[-1].lower() if '.' in url_without_params else 'mp4'
|
| 306 |
if file_ext not in ['mp4', 'mov', 'webm']:
|
| 307 |
file_ext = 'mp4'
|
| 308 |
|
| 309 |
-
#
|
| 310 |
downloaded_video_path = f"temp_{video_id}_downloaded.{file_ext}"
|
| 311 |
-
input_video_path = f"temp_{video_id}_input.mp4" #
|
| 312 |
extracted_audio_path = f"temp_{video_id}_audio.mp3"
|
| 313 |
synthesized_audio_path = f"temp_{video_id}_tts.mp3"
|
| 314 |
srt_file_path = f"temp_{video_id}.srt"
|
|
@@ -316,7 +359,7 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 316 |
lipsync_output_path = f"temp_{video_id}_lipsync.mp4"
|
| 317 |
|
| 318 |
try:
|
| 319 |
-
# 1.
|
| 320 |
print(f"[{video_id}] Downloading original video from: {job.original_video_url}")
|
| 321 |
res = requests.get(job.original_video_url, stream=True)
|
| 322 |
res.raise_for_status()
|
|
@@ -324,8 +367,7 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 324 |
for chunk in res.iter_content(chunk_size=8192):
|
| 325 |
f.write(chunk)
|
| 326 |
|
| 327 |
-
# 1.5.
|
| 328 |
-
# Webm ve Mov formatlarını OpenCV ve Wav2Lip ile %100 uyumlu hale getirmek için standart MP4 formatına dönüştürüyoruz.
|
| 329 |
print(f"[{video_id}] Standardizing downloaded video to MP4 format...")
|
| 330 |
cmd_standardize = [
|
| 331 |
"ffmpeg", "-y", "-i", downloaded_video_path,
|
|
@@ -335,7 +377,7 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 335 |
]
|
| 336 |
subprocess.run(cmd_standardize, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 337 |
|
| 338 |
-
# 2.
|
| 339 |
print(f"[{video_id}] Extracting audio using FFmpeg...")
|
| 340 |
cmd_extract = [
|
| 341 |
"ffmpeg", "-y", "-i", input_video_path,
|
|
@@ -344,8 +386,8 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 344 |
]
|
| 345 |
subprocess.run(cmd_extract, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 346 |
|
| 347 |
-
# 3. Gemini 2.0
|
| 348 |
-
print(f"[{video_id}] Calling Gemini 2.0 Flash...")
|
| 349 |
with open(extracted_audio_path, "rb") as audio_file:
|
| 350 |
audio_data = base64.b64encode(audio_file.read()).decode("utf-8")
|
| 351 |
|
|
@@ -360,8 +402,15 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 360 |
"contents": [
|
| 361 |
{
|
| 362 |
"parts": [
|
| 363 |
-
{
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
]
|
| 366 |
}
|
| 367 |
]
|
|
@@ -377,11 +426,14 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 377 |
with open(srt_file_path, "w", encoding="utf-8") as srt_file:
|
| 378 |
srt_file.write(srt_content)
|
| 379 |
translated_text = srt_to_plain_text(srt_content)
|
|
|
|
| 380 |
else:
|
| 381 |
translated_text = gemini_output
|
|
|
|
| 382 |
|
| 383 |
-
# 4.
|
| 384 |
-
print(f"[{video_id}] Calling ElevenLabs TTS Voice Cloning...")
|
|
|
|
| 385 |
voice_id = "21m00Tcm4TlvDq8ikWAM"
|
| 386 |
elevenlabs_url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
| 387 |
|
|
@@ -390,6 +442,7 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 390 |
"Content-Type": "application/json"
|
| 391 |
}
|
| 392 |
|
|
|
|
| 393 |
stability = 0.5
|
| 394 |
similarity_boost = 0.75
|
| 395 |
if job.voice_tone == 'excited':
|
|
@@ -414,8 +467,11 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 414 |
with open(synthesized_audio_path, "wb") as f:
|
| 415 |
f.write(tts_res.content)
|
| 416 |
|
| 417 |
-
# 5. Lip-Sync
|
|
|
|
|
|
|
| 418 |
if job.has_lip_sync:
|
|
|
|
| 419 |
setup_success = setup_wav2lip()
|
| 420 |
if not setup_success:
|
| 421 |
raise Exception("Wav2Lip model files setup failed. Please check server logs.")
|
|
@@ -428,15 +484,21 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 428 |
"--audio", f"../{synthesized_audio_path}",
|
| 429 |
"--outfile", f"../{lipsync_output_path}"
|
| 430 |
]
|
|
|
|
|
|
|
| 431 |
result = subprocess.run(cmd_lipsync, cwd="Wav2Lip", capture_output=True, text=True)
|
| 432 |
if result.returncode != 0:
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
video_src_for_subtitles = lipsync_output_path
|
| 435 |
else:
|
| 436 |
video_src_for_subtitles = input_video_path
|
| 437 |
|
| 438 |
if job.has_captions:
|
| 439 |
-
print(f"[{video_id}] Burning subtitles using FFmpeg...")
|
| 440 |
style_str = get_ffmpeg_style(
|
| 441 |
job.caption_style,
|
| 442 |
custom_font=job.custom_font,
|
|
@@ -445,6 +507,8 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 445 |
)
|
| 446 |
|
| 447 |
if job.has_lip_sync:
|
|
|
|
|
|
|
| 448 |
cmd_merge = [
|
| 449 |
"ffmpeg", "-y", "-i", video_src_for_subtitles,
|
| 450 |
"-vf", f"subtitles={srt_file_path}:force_style='{style_str}'",
|
|
@@ -453,6 +517,7 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 453 |
output_video_path
|
| 454 |
]
|
| 455 |
else:
|
|
|
|
| 456 |
cmd_merge = [
|
| 457 |
"ffmpeg", "-y", "-i", video_src_for_subtitles, "-i", synthesized_audio_path,
|
| 458 |
"-vf", f"subtitles={srt_file_path}:force_style='{style_str}'",
|
|
@@ -463,8 +528,10 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 463 |
subprocess.run(cmd_merge, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 464 |
else:
|
| 465 |
if job.has_lip_sync:
|
|
|
|
| 466 |
shutil.copy2(lipsync_output_path, output_video_path)
|
| 467 |
else:
|
|
|
|
| 468 |
cmd_merge = [
|
| 469 |
"ffmpeg", "-y", "-i", video_src_for_subtitles, "-i", synthesized_audio_path,
|
| 470 |
"-map", "0:v", "-map", "1:a", "-c:v", "copy", "-c:a", "aac", "-shortest",
|
|
@@ -472,9 +539,14 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 472 |
]
|
| 473 |
subprocess.run(cmd_merge, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 474 |
|
| 475 |
-
#
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
with open(output_video_path, "rb") as out_file:
|
| 480 |
upload_headers = {
|
|
@@ -483,20 +555,25 @@ def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key
|
|
| 483 |
"Content-Type": "video/mp4"
|
| 484 |
}
|
| 485 |
upload_res = requests.post(storage_upload_url, headers=upload_headers, data=out_file)
|
|
|
|
| 486 |
if upload_res.status_code == 400 and "AlreadyExists" in upload_res.text:
|
| 487 |
upload_res = requests.put(storage_upload_url, headers=upload_headers, data=out_file)
|
|
|
|
| 488 |
upload_res.raise_for_status()
|
| 489 |
|
| 490 |
-
|
|
|
|
| 491 |
print(f"[{video_id}] Video successfully uploaded. Public URL: {public_video_url}")
|
| 492 |
-
|
|
|
|
|
|
|
| 493 |
|
| 494 |
except Exception as e:
|
| 495 |
print(f"[{video_id}] Production Pipeline Error: {e}")
|
| 496 |
-
update_db_status(video_id, user_token, "failed", error_message=f"Hata: {str(e)}")
|
| 497 |
|
| 498 |
finally:
|
| 499 |
-
#
|
| 500 |
for temp_file in [downloaded_video_path, input_video_path, extracted_audio_path, synthesized_audio_path, srt_file_path, output_video_path, lipsync_output_path]:
|
| 501 |
if os.path.exists(temp_file):
|
| 502 |
try:
|
|
@@ -511,5 +588,6 @@ def process_video(job: TranslationJob, background_tasks: BackgroundTasks):
|
|
| 511 |
|
| 512 |
if __name__ == "__main__":
|
| 513 |
import uvicorn
|
|
|
|
| 514 |
load_env_local()
|
| 515 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 21 |
caption_style: str
|
| 22 |
resolution: str
|
| 23 |
user_token: str
|
|
|
|
| 24 |
custom_font: Optional[str] = None
|
| 25 |
custom_size: Optional[int] = None
|
| 26 |
custom_color: Optional[str] = None
|
| 27 |
+
supabase_url: Optional[str] = None
|
| 28 |
+
supabase_anon_key: Optional[str] = None
|
| 29 |
+
gemini_api_key: Optional[str] = None
|
| 30 |
+
elevenlabs_api_key: Optional[str] = None
|
| 31 |
|
| 32 |
def load_env_local():
|
| 33 |
"""Load environment variables from local .env.local file if it exists."""
|
|
|
|
| 122 |
print("Wav2Lip environment setup completed successfully.")
|
| 123 |
return True
|
| 124 |
|
| 125 |
+
|
| 126 |
def get_supabase_config():
|
| 127 |
supabase_url = os.environ.get("NEXT_PUBLIC_SUPABASE_URL", "")
|
| 128 |
supabase_anon_key = os.environ.get("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
|
|
|
|
| 138 |
|
| 139 |
return gemini_key, elevenlabs_key, (is_gemini_mock or is_eleven_mock)
|
| 140 |
|
| 141 |
+
def update_db_status(video_id: str, user_token: str, status: str, translated_url: str = None, error_message: str = None, supabase_url: str = None, supabase_anon_key: str = None):
|
| 142 |
+
if not supabase_url or not supabase_anon_key:
|
| 143 |
+
env_url, env_key = get_supabase_config()
|
| 144 |
+
supabase_url = supabase_url or env_url
|
| 145 |
+
supabase_anon_key = supabase_anon_key or env_key
|
| 146 |
+
|
| 147 |
if not supabase_url or not supabase_anon_key:
|
| 148 |
print("Error: Supabase config is missing.")
|
| 149 |
return
|
| 150 |
|
| 151 |
+
# Normalize Supabase base URL (remove trailing slash and rest/v1 path if present)
|
| 152 |
+
base_url = supabase_url.rstrip("/")
|
| 153 |
+
if base_url.endswith("/rest/v1"):
|
| 154 |
+
base_url = base_url[:-8].rstrip("/")
|
| 155 |
+
|
| 156 |
+
url = f"{base_url}/rest/v1/videos?id=eq.{video_id}"
|
| 157 |
headers = {
|
| 158 |
"apikey": supabase_anon_key,
|
| 159 |
"Authorization": f"Bearer {user_token}",
|
|
|
|
| 239 |
if custom_size:
|
| 240 |
style["Fontsize"] = str(custom_size)
|
| 241 |
if custom_color:
|
| 242 |
+
# BGR (Blue-Green-Red) hex format for ASS style colors
|
| 243 |
color_map = {
|
| 244 |
"yellow": "&H0000FFFF",
|
| 245 |
"green": "&H0000FF00",
|
|
|
|
| 272 |
user_token = job.user_token
|
| 273 |
|
| 274 |
print(f"Starting background process for video ID: {video_id}")
|
| 275 |
+
|
| 276 |
+
# Resolve Supabase config (prioritize request body, fallback to env)
|
| 277 |
+
supabase_url = job.supabase_url or os.environ.get("NEXT_PUBLIC_SUPABASE_URL", "")
|
| 278 |
+
supabase_anon_key = job.supabase_anon_key or os.environ.get("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
|
| 279 |
+
|
| 280 |
+
# Resolve API keys (prioritize request body, fallback to env)
|
| 281 |
+
gemini_key = job.gemini_api_key or os.environ.get("GEMINI_API_KEY", "")
|
| 282 |
+
elevenlabs_key = job.elevenlabs_api_key or os.environ.get("ELEVENLABS_API_KEY", "")
|
| 283 |
+
|
| 284 |
+
# Check if they are valid or placeholders
|
| 285 |
+
is_gemini_mock = not gemini_key or "your-gemini" in gemini_key or "here" in gemini_key
|
| 286 |
+
is_eleven_mock = not elevenlabs_key or "your-eleven" in elevenlabs_key or "here" in elevenlabs_key
|
| 287 |
+
is_mock_mode = is_gemini_mock or is_eleven_mock
|
| 288 |
|
| 289 |
if is_mock_mode:
|
| 290 |
+
run_simulation_pipeline(job, supabase_url, supabase_anon_key)
|
| 291 |
else:
|
| 292 |
+
run_production_pipeline(job, gemini_key, elevenlabs_key, supabase_url, supabase_anon_key)
|
| 293 |
|
| 294 |
+
def run_simulation_pipeline(job: TranslationJob, supabase_url: str, supabase_anon_key: str):
|
| 295 |
video_id = job.video_id
|
| 296 |
user_token = job.user_token
|
| 297 |
|
| 298 |
print(f"[{video_id}] Running in SIMULATION MODE...")
|
| 299 |
+
|
| 300 |
try:
|
| 301 |
+
# Step 1: Simulate Audio Extraction (Status is already processing)
|
| 302 |
print(f"[{video_id}] Step 1: Simulating audio extraction...")
|
|
|
|
|
|
|
|
|
|
| 303 |
time.sleep(2)
|
| 304 |
+
|
| 305 |
+
# Step 2: Simulate Gemini Translation
|
| 306 |
+
print(f"[{video_id}] Step 2: Simulating Gemini 2.0 Flash transcription & translation...")
|
| 307 |
+
update_db_status(video_id, user_token, "processing", supabase_url=supabase_url, supabase_anon_key=supabase_anon_key) # Just refresh database connection
|
| 308 |
+
time.sleep(3)
|
| 309 |
+
|
| 310 |
+
# Step 3: Simulate ElevenLabs Voice Clone Synthesis
|
| 311 |
+
print(f"[{video_id}] Step 3: Simulating ElevenLabs voice clone synthesis with '{job.voice_tone}' tone...")
|
| 312 |
time.sleep(2)
|
| 313 |
|
| 314 |
+
# Step 4: Simulate Subtitle Burning and Audio-Video Merge
|
| 315 |
if job.has_captions:
|
| 316 |
+
override_msg = ""
|
| 317 |
+
if job.custom_font or job.custom_size or job.custom_color:
|
| 318 |
+
override_msg = f" (Overrides: font={job.custom_font}, size={job.custom_size}, color={job.custom_color})"
|
| 319 |
+
print(f"[{video_id}] Step 4: Simulating FFmpeg subtitle burning in '{job.caption_style}' style{override_msg}...")
|
| 320 |
time.sleep(2)
|
| 321 |
+
|
| 322 |
if job.has_lip_sync:
|
| 323 |
+
print(f"[{video_id}] Step 5: Simulating Wav2Lip alignment (LipSync active)...")
|
| 324 |
time.sleep(2)
|
| 325 |
+
else:
|
| 326 |
+
print(f"[{video_id}] Step 5: Simulating FFmpeg video & audio track merge...")
|
| 327 |
+
time.sleep(1)
|
| 328 |
|
| 329 |
+
# Target Lang Mock URL selector
|
| 330 |
+
# To make it feel premium, we can point to a high quality public MP4 file
|
| 331 |
mock_output_video = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"
|
| 332 |
+
|
| 333 |
+
print(f"[{video_id}] Simulation complete. Updating database to completed.")
|
| 334 |
+
update_db_status(video_id, user_token, "completed", translated_url=mock_output_video, supabase_url=supabase_url, supabase_anon_key=supabase_anon_key)
|
| 335 |
|
| 336 |
except Exception as e:
|
| 337 |
print(f"[{video_id}] Simulation Error: {e}")
|
| 338 |
+
update_db_status(video_id, user_token, "failed", error_message=f"Simülasyon Hatası: {str(e)}", supabase_url=supabase_url, supabase_anon_key=supabase_anon_key)
|
| 339 |
|
| 340 |
+
def run_production_pipeline(job: TranslationJob, gemini_key: str, elevenlabs_key: str, supabase_url: str, supabase_anon_key: str):
|
| 341 |
video_id = job.video_id
|
| 342 |
user_token = job.user_token
|
|
|
|
| 343 |
|
| 344 |
print(f"[{video_id}] Running in PRODUCTION MODE...")
|
| 345 |
|
| 346 |
+
# Detect extension from URL or default to mp4
|
| 347 |
url_without_params = job.original_video_url.split('?')[0]
|
| 348 |
file_ext = url_without_params.split('.')[-1].lower() if '.' in url_without_params else 'mp4'
|
| 349 |
if file_ext not in ['mp4', 'mov', 'webm']:
|
| 350 |
file_ext = 'mp4'
|
| 351 |
|
| 352 |
+
# Temporary filenames
|
| 353 |
downloaded_video_path = f"temp_{video_id}_downloaded.{file_ext}"
|
| 354 |
+
input_video_path = f"temp_{video_id}_input.mp4" # This will hold the standardized version
|
| 355 |
extracted_audio_path = f"temp_{video_id}_audio.mp3"
|
| 356 |
synthesized_audio_path = f"temp_{video_id}_tts.mp3"
|
| 357 |
srt_file_path = f"temp_{video_id}.srt"
|
|
|
|
| 359 |
lipsync_output_path = f"temp_{video_id}_lipsync.mp4"
|
| 360 |
|
| 361 |
try:
|
| 362 |
+
# 1. Download original video
|
| 363 |
print(f"[{video_id}] Downloading original video from: {job.original_video_url}")
|
| 364 |
res = requests.get(job.original_video_url, stream=True)
|
| 365 |
res.raise_for_status()
|
|
|
|
| 367 |
for chunk in res.iter_content(chunk_size=8192):
|
| 368 |
f.write(chunk)
|
| 369 |
|
| 370 |
+
# 1.5. Standardize video (H.264/AAC, 25 FPS, YUV420p) for OpenCV / Wav2Lip stability
|
|
|
|
| 371 |
print(f"[{video_id}] Standardizing downloaded video to MP4 format...")
|
| 372 |
cmd_standardize = [
|
| 373 |
"ffmpeg", "-y", "-i", downloaded_video_path,
|
|
|
|
| 377 |
]
|
| 378 |
subprocess.run(cmd_standardize, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 379 |
|
| 380 |
+
# 2. Extract audio from video using FFmpeg
|
| 381 |
print(f"[{video_id}] Extracting audio using FFmpeg...")
|
| 382 |
cmd_extract = [
|
| 383 |
"ffmpeg", "-y", "-i", input_video_path,
|
|
|
|
| 386 |
]
|
| 387 |
subprocess.run(cmd_extract, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 388 |
|
| 389 |
+
# 3. Request Transcription & Translation from Gemini 2.0 Flash
|
| 390 |
+
print(f"[{video_id}] Calling Gemini 2.0 Flash for transcription/translation to '{job.target_lang}'...")
|
| 391 |
with open(extracted_audio_path, "rb") as audio_file:
|
| 392 |
audio_data = base64.b64encode(audio_file.read()).decode("utf-8")
|
| 393 |
|
|
|
|
| 402 |
"contents": [
|
| 403 |
{
|
| 404 |
"parts": [
|
| 405 |
+
{
|
| 406 |
+
"inline_data": {
|
| 407 |
+
"mime_type": "audio/mp3",
|
| 408 |
+
"data": audio_data
|
| 409 |
+
}
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"text": gemini_prompt
|
| 413 |
+
}
|
| 414 |
]
|
| 415 |
}
|
| 416 |
]
|
|
|
|
| 426 |
with open(srt_file_path, "w", encoding="utf-8") as srt_file:
|
| 427 |
srt_file.write(srt_content)
|
| 428 |
translated_text = srt_to_plain_text(srt_content)
|
| 429 |
+
print(f"[{video_id}] Gemini SRT translation generated. Plain text length: {len(translated_text)}")
|
| 430 |
else:
|
| 431 |
translated_text = gemini_output
|
| 432 |
+
print(f"[{video_id}] Gemini Plain text translation: '{translated_text[:100]}...'")
|
| 433 |
|
| 434 |
+
# 4. Synthesize voice cloning using ElevenLabs API
|
| 435 |
+
print(f"[{video_id}] Calling ElevenLabs TTS Voice Cloning with tone '{job.voice_tone}'...")
|
| 436 |
+
# We can use the default standard voice (e.g. Rachel: 21m00Tcm4TlvDq8ikWAM)
|
| 437 |
voice_id = "21m00Tcm4TlvDq8ikWAM"
|
| 438 |
elevenlabs_url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
| 439 |
|
|
|
|
| 442 |
"Content-Type": "application/json"
|
| 443 |
}
|
| 444 |
|
| 445 |
+
# Optional stability configurations based on voice tone
|
| 446 |
stability = 0.5
|
| 447 |
similarity_boost = 0.75
|
| 448 |
if job.voice_tone == 'excited':
|
|
|
|
| 467 |
with open(synthesized_audio_path, "wb") as f:
|
| 468 |
f.write(tts_res.content)
|
| 469 |
|
| 470 |
+
# 5. Merge synthesized audio back with the original video (or run Lip-Sync if selected)
|
| 471 |
+
lipsync_output_path = f"temp_{video_id}_lipsync.mp4"
|
| 472 |
+
|
| 473 |
if job.has_lip_sync:
|
| 474 |
+
print(f"[{video_id}] Setting up Wav2Lip model checkpoint files...")
|
| 475 |
setup_success = setup_wav2lip()
|
| 476 |
if not setup_success:
|
| 477 |
raise Exception("Wav2Lip model files setup failed. Please check server logs.")
|
|
|
|
| 484 |
"--audio", f"../{synthesized_audio_path}",
|
| 485 |
"--outfile", f"../{lipsync_output_path}"
|
| 486 |
]
|
| 487 |
+
|
| 488 |
+
# Execute in the Wav2Lip directory to resolve relative imports
|
| 489 |
result = subprocess.run(cmd_lipsync, cwd="Wav2Lip", capture_output=True, text=True)
|
| 490 |
if result.returncode != 0:
|
| 491 |
+
print(f"Wav2Lip stdout: {result.stdout}")
|
| 492 |
+
print(f"Wav2Lip stderr: {result.stderr}")
|
| 493 |
+
raise Exception(f"Wav2Lip inference failed with exit code {result.returncode}: {result.stderr}")
|
| 494 |
+
|
| 495 |
+
print(f"[{video_id}] Wav2Lip alignment completed successfully.")
|
| 496 |
video_src_for_subtitles = lipsync_output_path
|
| 497 |
else:
|
| 498 |
video_src_for_subtitles = input_video_path
|
| 499 |
|
| 500 |
if job.has_captions:
|
| 501 |
+
print(f"[{video_id}] Burning subtitles with style '{job.caption_style}' using FFmpeg...")
|
| 502 |
style_str = get_ffmpeg_style(
|
| 503 |
job.caption_style,
|
| 504 |
custom_font=job.custom_font,
|
|
|
|
| 507 |
)
|
| 508 |
|
| 509 |
if job.has_lip_sync:
|
| 510 |
+
# Lip-synced video already has the synthesized audio merged inside it.
|
| 511 |
+
# So we map audio track from the same file (0:a)
|
| 512 |
cmd_merge = [
|
| 513 |
"ffmpeg", "-y", "-i", video_src_for_subtitles,
|
| 514 |
"-vf", f"subtitles={srt_file_path}:force_style='{style_str}'",
|
|
|
|
| 517 |
output_video_path
|
| 518 |
]
|
| 519 |
else:
|
| 520 |
+
# Map audio from input 1 (synthesized audio path)
|
| 521 |
cmd_merge = [
|
| 522 |
"ffmpeg", "-y", "-i", video_src_for_subtitles, "-i", synthesized_audio_path,
|
| 523 |
"-vf", f"subtitles={srt_file_path}:force_style='{style_str}'",
|
|
|
|
| 528 |
subprocess.run(cmd_merge, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 529 |
else:
|
| 530 |
if job.has_lip_sync:
|
| 531 |
+
print(f"[{video_id}] Copying lip-synced video to final output path...")
|
| 532 |
shutil.copy2(lipsync_output_path, output_video_path)
|
| 533 |
else:
|
| 534 |
+
print(f"[{video_id}] Merging audio track into original video using FFmpeg...")
|
| 535 |
cmd_merge = [
|
| 536 |
"ffmpeg", "-y", "-i", video_src_for_subtitles, "-i", synthesized_audio_path,
|
| 537 |
"-map", "0:v", "-map", "1:a", "-c:v", "copy", "-c:a", "aac", "-shortest",
|
|
|
|
| 539 |
]
|
| 540 |
subprocess.run(cmd_merge, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 541 |
|
| 542 |
+
# Normalize Supabase base URL (remove trailing slash and rest/v1 path if present)
|
| 543 |
+
base_url = supabase_url.rstrip("/")
|
| 544 |
+
if base_url.endswith("/rest/v1"):
|
| 545 |
+
base_url = base_url[:-8].rstrip("/")
|
| 546 |
+
|
| 547 |
+
# 6. Upload output video to Supabase Storage
|
| 548 |
+
print(f"[{video_id}] Uploading output video to Supabase Storage...")
|
| 549 |
+
storage_upload_url = f"{base_url}/storage/v1/object/videos/{video_id}_translated.mp4"
|
| 550 |
|
| 551 |
with open(output_video_path, "rb") as out_file:
|
| 552 |
upload_headers = {
|
|
|
|
| 555 |
"Content-Type": "video/mp4"
|
| 556 |
}
|
| 557 |
upload_res = requests.post(storage_upload_url, headers=upload_headers, data=out_file)
|
| 558 |
+
# If exists, we can try to PUT (overwrite)
|
| 559 |
if upload_res.status_code == 400 and "AlreadyExists" in upload_res.text:
|
| 560 |
upload_res = requests.put(storage_upload_url, headers=upload_headers, data=out_file)
|
| 561 |
+
|
| 562 |
upload_res.raise_for_status()
|
| 563 |
|
| 564 |
+
# Get public url
|
| 565 |
+
public_video_url = f"{base_url}/storage/v1/object/public/videos/{video_id}_translated.mp4"
|
| 566 |
print(f"[{video_id}] Video successfully uploaded. Public URL: {public_video_url}")
|
| 567 |
+
|
| 568 |
+
# 7. Update database to completed
|
| 569 |
+
update_db_status(video_id, user_token, "completed", translated_url=public_video_url, supabase_url=supabase_url, supabase_anon_key=supabase_anon_key)
|
| 570 |
|
| 571 |
except Exception as e:
|
| 572 |
print(f"[{video_id}] Production Pipeline Error: {e}")
|
| 573 |
+
update_db_status(video_id, user_token, "failed", error_message=f"Hata: {str(e)}", supabase_url=supabase_url, supabase_anon_key=supabase_anon_key)
|
| 574 |
|
| 575 |
finally:
|
| 576 |
+
# Cleanup temporary files
|
| 577 |
for temp_file in [downloaded_video_path, input_video_path, extracted_audio_path, synthesized_audio_path, srt_file_path, output_video_path, lipsync_output_path]:
|
| 578 |
if os.path.exists(temp_file):
|
| 579 |
try:
|
|
|
|
| 588 |
|
| 589 |
if __name__ == "__main__":
|
| 590 |
import uvicorn
|
| 591 |
+
# Make sure we load local variables before starting
|
| 592 |
load_env_local()
|
| 593 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|