VoiceClips commited on
Commit
3a7b55e
·
verified ·
1 Parent(s): ae2e0e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +119 -41
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, supabase_anon_key = get_supabase_config()
 
 
 
 
139
  if not supabase_url or not supabase_anon_key:
140
  print("Error: Supabase config is missing.")
141
  return
142
 
143
- url = f"{supabase_url}/rest/v1/videos?id=eq.{video_id}"
 
 
 
 
 
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
- gemini_key, elevenlabs_key, is_mock_mode = get_api_keys()
 
 
 
 
 
 
 
 
 
 
 
 
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
- time.sleep(2)
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
- print(f"[{video_id}] Step 3: Simulating ElevenLabs Voice synthesis...")
 
 
 
 
 
 
 
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
- print(f"[{video_id}] Simulation complete. Updating DB to completed.")
290
- update_db_status(video_id, user_token, "completed", translated_url=mock_output_video)
 
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
- # Gelen URL'den uzantıyı tespit edip kaydediyoruz (.mp4, .mov, .webm vb.)
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
- # Geçici çalışma dosyaları
310
  downloaded_video_path = f"temp_{video_id}_downloaded.{file_ext}"
311
- input_video_path = f"temp_{video_id}_input.mp4" # Standartlaştırılmış MP4 dosyası
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. Orijinal dosyayı indir
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. Video standardizasyon (H.264/AAC, 25 FPS, YUV420p)
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. Ses Ayıklama
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 ile Transkript & Çeviri
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
- {"inline_data": {"mime_type": "audio/mp3", "data": audio_data}},
364
- {"text": gemini_prompt}
 
 
 
 
 
 
 
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. ElevenLabs Yapay Zeka Ses Sentezleme
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 & Video Birleştirme
 
 
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
- raise Exception(f"Wav2Lip inference failed with exit code {result.returncode}")
 
 
 
 
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
- # 6. Sonuç Videosunu Supabase Storage'a Yükle
476
- print(f"[{video_id}] Uploading output to Supabase Storage...")
477
- storage_upload_url = f"{supabase_url}/storage/v1/object/videos/{video_id}_translated.mp4"
 
 
 
 
 
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
- public_video_url = f"{supabase_url}/storage/v1/object/public/videos/{video_id}_translated.mp4"
 
491
  print(f"[{video_id}] Video successfully uploaded. Public URL: {public_video_url}")
492
- update_db_status(video_id, user_token, "completed", translated_url=public_video_url)
 
 
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
- # Geçici dosyaların temizlenmesi
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)