tayyab415 commited on
Commit
ba7ebb7
Β·
1 Parent(s): 4471bb8

Add time estimate hint to Step 1 in Gradio UI (~3-4 mins)

Browse files
Files changed (1) hide show
  1. app.py +55 -45
app.py CHANGED
@@ -440,13 +440,13 @@ def reset_workflow():
440
 
441
  def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_intro_image, add_subtitles, progress=gr.Progress()):
442
  """Production via Modal - uploads video and processes with fresh job_id"""
443
-
444
  # Debug: Log what we received from Gradio
445
  logger.info(f"video_file type: {type(video_file)}")
446
  logger.info(f"video_file value: {video_file}")
447
-
448
  actual_video_path = None
449
-
450
  # Handle different Gradio 6 input formats
451
  if video_file is None:
452
  if workflow_state.get('final_video_path'):
@@ -460,7 +460,8 @@ def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_int
460
  actual_video_path = video_file
461
  elif isinstance(video_file, dict):
462
  # Gradio 6 may return dict with 'path' or 'name' key
463
- actual_video_path = video_file.get('path') or video_file.get('name') or video_file.get('video')
 
464
  logger.info(f"Extracted path from dict: {actual_video_path}")
465
  elif hasattr(video_file, 'name'):
466
  # File-like object
@@ -468,7 +469,7 @@ def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_int
468
  else:
469
  yield f"❌ Unexpected video input type: {type(video_file)}", None
470
  return
471
-
472
  if not actual_video_path:
473
  yield "❌ Could not determine video file path", None
474
  return
@@ -487,35 +488,37 @@ def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_int
487
  try:
488
  progress(0.1, desc="Preparing video...")
489
  yield "πŸ“€ Preparing video for processing...", None
490
-
491
  # Copy file to our temp directory to prevent Gradio cleanup issues
492
  import shutil
493
  import uuid
494
-
495
  temp_dir = os.path.join(os.getcwd(), "temp")
496
  os.makedirs(temp_dir, exist_ok=True)
497
-
498
  # Generate a unique filename
499
  temp_filename = f"upload_{uuid.uuid4().hex[:8]}_{os.path.basename(actual_video_path)}"
500
  local_video_path = os.path.join(temp_dir, temp_filename)
501
-
502
  logger.info(f"Copying uploaded file to: {local_video_path}")
503
  shutil.copy2(actual_video_path, local_video_path)
504
-
505
  # Get the file size for logging
506
  file_size = os.path.getsize(local_video_path) / 1024 / 1024
507
- logger.info(f"Processing video: {local_video_path} ({file_size:.1f} MB)")
 
508
 
509
  # Read video as base64 for transfer to Modal
510
  progress(0.2, desc="Reading video file...")
511
  yield "πŸ“¦ Reading video file...", None
512
-
513
  with open(local_video_path, 'rb') as f:
514
  video_bytes = f.read()
515
  video_base64 = base64.b64encode(video_bytes).decode('utf-8')
516
-
517
- logger.info(f"Video encoded: {len(video_base64) / 1024 / 1024:.1f} MB base64")
518
-
 
519
  progress(0.3, desc="Processing on Modal...")
520
  yield "🎬 Processing on Modal (this may take a few minutes)...", None
521
 
@@ -665,22 +668,22 @@ def add_production_to_video(
665
  ) -> str:
666
  """
667
  🎬 MAIN VIDEO PROCESSING TOOL - Transform any video into viral-ready content!
668
-
669
  ⚠️ IMPORTANT: This tool requires a WEB URL (http:// or https://), NOT a local file path!
670
  - βœ… YouTube URLs work: https://youtube.com/watch?v=...
671
  - βœ… Direct video URLs work: https://example.com/video.mp4
672
  - ❌ Local paths do NOT work: /mnt/data/file.mp4
673
-
674
  If the user uploads a file, tell them to:
675
  1. Upload the video to YouTube (unlisted) and provide the URL, OR
676
  2. Use a cloud storage link (Google Drive public link, Dropbox, etc.)
677
-
678
  This is the PRIMARY tool for video editing. Use this tool when the user wants to:
679
  - Process a YouTube video
680
  - Add professional production value to a video
681
  - Create short-form vertical content for TikTok/Reels/Shorts
682
  - Add intros, subtitles, smart crop, or background music
683
-
684
  The tool automatically:
685
  1. Downloads the video from the URL
686
  2. Applies AI-powered 9:16 smart crop for mobile viewing
@@ -688,7 +691,7 @@ def add_production_to_video(
688
  4. Adds auto-generated subtitles using Whisper
689
  5. Adds mood-matched background music
690
  6. Returns a download link for the finished video
691
-
692
  Parameters:
693
  video_url: The WEB URL of the video (must start with http:// or https://)
694
  - YouTube URL: https://youtube.com/watch?v=VIDEO_ID
@@ -697,7 +700,7 @@ def add_production_to_video(
697
  enable_smart_crop: If True, crops video to 9:16 vertical format for mobile
698
  add_intro: If True, generates AI intro with voiceover and title card
699
  add_subtitles: If True, adds auto-generated subtitles
700
-
701
  Returns:
702
  Processing result with a download URL for the produced video
703
  """
@@ -705,16 +708,17 @@ def add_production_to_video(
705
  # Validate URL - must be a web URL, not a local path
706
  if not video_url:
707
  return "❌ Error: No video URL provided"
708
-
709
  # Check for local file paths (reject these)
710
  if video_url.startswith('/mnt/data/') or video_url.startswith('/tmp/') or video_url.startswith('C:\\'):
711
  return f"❌ Error: Local file path detected.\n\n⚠️ This tool requires a web URL, not a local file.\n\n**Options:**\n1. Use the Video Studio widget to upload files directly\n2. Upload to YouTube (unlisted) and share the URL\n3. Use a cloud storage link (Google Drive, Dropbox)"
712
-
713
  if not video_url.startswith(('http://', 'https://')):
714
  return f"❌ Error: Invalid URL '{video_url[:50]}...'\n\n⚠️ This tool requires a web URL (http:// or https://), not a local file path.\n\n**Please provide:**\n- A YouTube URL: https://youtube.com/watch?v=...\n- Or a direct video URL: https://example.com/video.mp4\n\nIf you uploaded a file, please upload it to YouTube (unlisted) first and share that URL."
715
-
716
- logger.info(f"🎬 Production pipeline starting for: {video_url[:100]}...")
717
-
 
718
  # Call Modal Step 6 with video_url for standalone mode
719
  response = call_modal("step", data={
720
  "step": 6,
@@ -724,18 +728,18 @@ def add_production_to_video(
724
  "add_intro": add_intro,
725
  "add_subtitles": add_subtitles,
726
  }, timeout=2400) # 40 min timeout for large videos
727
-
728
  if response.get("error"):
729
  return f"❌ Error: {response['error']}"
730
-
731
  if response.get("success"):
732
  job_id = response.get('job_id')
733
  duration = response.get('duration', 0)
734
  detected_mood = response.get('mood', mood)
735
-
736
  # Build download URL
737
  download_url = f"https://tayyabkhn343--directors-cut-download.modal.run?job_id={job_id}"
738
-
739
  result = f"""βœ… Video produced successfully!
740
 
741
  πŸ“Š **Details:**
@@ -748,11 +752,11 @@ def add_production_to_video(
748
  - Background Music: {'βœ“' if response.get('has_music') else 'βœ—'}
749
 
750
  πŸ”— **Download:** {download_url}"""
751
-
752
  return result
753
  else:
754
  return f"❌ Processing failed: {response.get('error', 'Unknown error')}"
755
-
756
  except Exception as e:
757
  logger.error(f"Production pipeline error: {e}")
758
  return f"❌ Error: {str(e)}"
@@ -1251,19 +1255,19 @@ def video_studio_widget_html():
1251
  def open_video_studio() -> str:
1252
  """
1253
  🎬 Open the Director's Cut Video Studio interface.
1254
-
1255
  Use this tool when the user wants to:
1256
  - Process or edit a video
1257
  - Upload a video file
1258
  - Add production value to any video
1259
  - Create viral short-form content
1260
-
1261
  This opens an interactive studio where users can:
1262
  - Paste a YouTube URL OR upload a video file directly
1263
  - Choose mood (hype, chill, suspense, or auto-detect)
1264
  - Enable/disable smart crop, AI intro, and subtitles
1265
  - Process the video and download the result
1266
-
1267
  Returns:
1268
  str: Confirmation that the studio is ready
1269
  """
@@ -1291,6 +1295,7 @@ with gr.Blocks(title="Director's Cut") as app:
1291
  # Step 1
1292
  with gr.Group():
1293
  gr.Markdown("### Step 1: Analyze Video")
 
1294
  step1_btn = gr.Button(
1295
  "1️⃣ Analyze & Classify", variant="primary")
1296
  step1_output = gr.Markdown()
@@ -1376,35 +1381,40 @@ with gr.Blocks(title="Director's Cut") as app:
1376
  with gr.Tab("πŸ€– ChatGPT Integration", visible=False):
1377
  # This tab binds MCP tools and resources to Gradio events
1378
  # Required by Gradio MCP to expose them to ChatGPT
1379
-
1380
  # Bind the add_production_to_video tool
1381
  chatgpt_url_input = gr.Textbox(label="Video URL")
1382
- chatgpt_mood = gr.Dropdown(choices=["auto", "hype", "suspense", "chill"], value="auto")
 
1383
  chatgpt_crop = gr.Checkbox(value=True)
1384
  chatgpt_intro = gr.Checkbox(value=True)
1385
  chatgpt_subs = gr.Checkbox(value=True)
1386
  chatgpt_output = gr.Textbox(label="Result")
1387
  chatgpt_btn = gr.Button("Process Video")
1388
  chatgpt_btn.click(
1389
- add_production_to_video,
1390
- inputs=[chatgpt_url_input, chatgpt_mood, chatgpt_crop, chatgpt_intro, chatgpt_subs],
 
1391
  outputs=chatgpt_output
1392
  )
1393
-
1394
  # Bind the production widget resource
1395
- widget_code = gr.Code(label="Widget HTML", language="html", max_lines=5)
 
1396
  widget_btn = gr.Button("Show Widget")
1397
  widget_btn.click(production_widget_html, outputs=widget_code)
1398
-
1399
  # Bind the Video Studio tool
1400
  studio_output = gr.Textbox(label="Studio Status")
1401
  studio_btn = gr.Button("Open Video Studio")
1402
  studio_btn.click(open_video_studio, outputs=studio_output)
1403
-
1404
  # Bind the Video Studio widget resource
1405
- studio_widget_code = gr.Code(label="Video Studio Widget", language="html", max_lines=5)
 
1406
  studio_widget_btn = gr.Button("Get Video Studio Widget")
1407
- studio_widget_btn.click(video_studio_widget_html, outputs=studio_widget_code)
 
1408
 
1409
 
1410
  if __name__ == "__main__":
 
440
 
441
  def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_intro_image, add_subtitles, progress=gr.Progress()):
442
  """Production via Modal - uploads video and processes with fresh job_id"""
443
+
444
  # Debug: Log what we received from Gradio
445
  logger.info(f"video_file type: {type(video_file)}")
446
  logger.info(f"video_file value: {video_file}")
447
+
448
  actual_video_path = None
449
+
450
  # Handle different Gradio 6 input formats
451
  if video_file is None:
452
  if workflow_state.get('final_video_path'):
 
460
  actual_video_path = video_file
461
  elif isinstance(video_file, dict):
462
  # Gradio 6 may return dict with 'path' or 'name' key
463
+ actual_video_path = video_file.get('path') or video_file.get(
464
+ 'name') or video_file.get('video')
465
  logger.info(f"Extracted path from dict: {actual_video_path}")
466
  elif hasattr(video_file, 'name'):
467
  # File-like object
 
469
  else:
470
  yield f"❌ Unexpected video input type: {type(video_file)}", None
471
  return
472
+
473
  if not actual_video_path:
474
  yield "❌ Could not determine video file path", None
475
  return
 
488
  try:
489
  progress(0.1, desc="Preparing video...")
490
  yield "πŸ“€ Preparing video for processing...", None
491
+
492
  # Copy file to our temp directory to prevent Gradio cleanup issues
493
  import shutil
494
  import uuid
495
+
496
  temp_dir = os.path.join(os.getcwd(), "temp")
497
  os.makedirs(temp_dir, exist_ok=True)
498
+
499
  # Generate a unique filename
500
  temp_filename = f"upload_{uuid.uuid4().hex[:8]}_{os.path.basename(actual_video_path)}"
501
  local_video_path = os.path.join(temp_dir, temp_filename)
502
+
503
  logger.info(f"Copying uploaded file to: {local_video_path}")
504
  shutil.copy2(actual_video_path, local_video_path)
505
+
506
  # Get the file size for logging
507
  file_size = os.path.getsize(local_video_path) / 1024 / 1024
508
+ logger.info(
509
+ f"Processing video: {local_video_path} ({file_size:.1f} MB)")
510
 
511
  # Read video as base64 for transfer to Modal
512
  progress(0.2, desc="Reading video file...")
513
  yield "πŸ“¦ Reading video file...", None
514
+
515
  with open(local_video_path, 'rb') as f:
516
  video_bytes = f.read()
517
  video_base64 = base64.b64encode(video_bytes).decode('utf-8')
518
+
519
+ logger.info(
520
+ f"Video encoded: {len(video_base64) / 1024 / 1024:.1f} MB base64")
521
+
522
  progress(0.3, desc="Processing on Modal...")
523
  yield "🎬 Processing on Modal (this may take a few minutes)...", None
524
 
 
668
  ) -> str:
669
  """
670
  🎬 MAIN VIDEO PROCESSING TOOL - Transform any video into viral-ready content!
671
+
672
  ⚠️ IMPORTANT: This tool requires a WEB URL (http:// or https://), NOT a local file path!
673
  - βœ… YouTube URLs work: https://youtube.com/watch?v=...
674
  - βœ… Direct video URLs work: https://example.com/video.mp4
675
  - ❌ Local paths do NOT work: /mnt/data/file.mp4
676
+
677
  If the user uploads a file, tell them to:
678
  1. Upload the video to YouTube (unlisted) and provide the URL, OR
679
  2. Use a cloud storage link (Google Drive public link, Dropbox, etc.)
680
+
681
  This is the PRIMARY tool for video editing. Use this tool when the user wants to:
682
  - Process a YouTube video
683
  - Add professional production value to a video
684
  - Create short-form vertical content for TikTok/Reels/Shorts
685
  - Add intros, subtitles, smart crop, or background music
686
+
687
  The tool automatically:
688
  1. Downloads the video from the URL
689
  2. Applies AI-powered 9:16 smart crop for mobile viewing
 
691
  4. Adds auto-generated subtitles using Whisper
692
  5. Adds mood-matched background music
693
  6. Returns a download link for the finished video
694
+
695
  Parameters:
696
  video_url: The WEB URL of the video (must start with http:// or https://)
697
  - YouTube URL: https://youtube.com/watch?v=VIDEO_ID
 
700
  enable_smart_crop: If True, crops video to 9:16 vertical format for mobile
701
  add_intro: If True, generates AI intro with voiceover and title card
702
  add_subtitles: If True, adds auto-generated subtitles
703
+
704
  Returns:
705
  Processing result with a download URL for the produced video
706
  """
 
708
  # Validate URL - must be a web URL, not a local path
709
  if not video_url:
710
  return "❌ Error: No video URL provided"
711
+
712
  # Check for local file paths (reject these)
713
  if video_url.startswith('/mnt/data/') or video_url.startswith('/tmp/') or video_url.startswith('C:\\'):
714
  return f"❌ Error: Local file path detected.\n\n⚠️ This tool requires a web URL, not a local file.\n\n**Options:**\n1. Use the Video Studio widget to upload files directly\n2. Upload to YouTube (unlisted) and share the URL\n3. Use a cloud storage link (Google Drive, Dropbox)"
715
+
716
  if not video_url.startswith(('http://', 'https://')):
717
  return f"❌ Error: Invalid URL '{video_url[:50]}...'\n\n⚠️ This tool requires a web URL (http:// or https://), not a local file path.\n\n**Please provide:**\n- A YouTube URL: https://youtube.com/watch?v=...\n- Or a direct video URL: https://example.com/video.mp4\n\nIf you uploaded a file, please upload it to YouTube (unlisted) first and share that URL."
718
+
719
+ logger.info(
720
+ f"🎬 Production pipeline starting for: {video_url[:100]}...")
721
+
722
  # Call Modal Step 6 with video_url for standalone mode
723
  response = call_modal("step", data={
724
  "step": 6,
 
728
  "add_intro": add_intro,
729
  "add_subtitles": add_subtitles,
730
  }, timeout=2400) # 40 min timeout for large videos
731
+
732
  if response.get("error"):
733
  return f"❌ Error: {response['error']}"
734
+
735
  if response.get("success"):
736
  job_id = response.get('job_id')
737
  duration = response.get('duration', 0)
738
  detected_mood = response.get('mood', mood)
739
+
740
  # Build download URL
741
  download_url = f"https://tayyabkhn343--directors-cut-download.modal.run?job_id={job_id}"
742
+
743
  result = f"""βœ… Video produced successfully!
744
 
745
  πŸ“Š **Details:**
 
752
  - Background Music: {'βœ“' if response.get('has_music') else 'βœ—'}
753
 
754
  πŸ”— **Download:** {download_url}"""
755
+
756
  return result
757
  else:
758
  return f"❌ Processing failed: {response.get('error', 'Unknown error')}"
759
+
760
  except Exception as e:
761
  logger.error(f"Production pipeline error: {e}")
762
  return f"❌ Error: {str(e)}"
 
1255
  def open_video_studio() -> str:
1256
  """
1257
  🎬 Open the Director's Cut Video Studio interface.
1258
+
1259
  Use this tool when the user wants to:
1260
  - Process or edit a video
1261
  - Upload a video file
1262
  - Add production value to any video
1263
  - Create viral short-form content
1264
+
1265
  This opens an interactive studio where users can:
1266
  - Paste a YouTube URL OR upload a video file directly
1267
  - Choose mood (hype, chill, suspense, or auto-detect)
1268
  - Enable/disable smart crop, AI intro, and subtitles
1269
  - Process the video and download the result
1270
+
1271
  Returns:
1272
  str: Confirmation that the studio is ready
1273
  """
 
1295
  # Step 1
1296
  with gr.Group():
1297
  gr.Markdown("### Step 1: Analyze Video")
1298
+ gr.Markdown("*Downloads video & extracts transcript (~3-4 mins)*", elem_classes=["step-hint"])
1299
  step1_btn = gr.Button(
1300
  "1️⃣ Analyze & Classify", variant="primary")
1301
  step1_output = gr.Markdown()
 
1381
  with gr.Tab("πŸ€– ChatGPT Integration", visible=False):
1382
  # This tab binds MCP tools and resources to Gradio events
1383
  # Required by Gradio MCP to expose them to ChatGPT
1384
+
1385
  # Bind the add_production_to_video tool
1386
  chatgpt_url_input = gr.Textbox(label="Video URL")
1387
+ chatgpt_mood = gr.Dropdown(
1388
+ choices=["auto", "hype", "suspense", "chill"], value="auto")
1389
  chatgpt_crop = gr.Checkbox(value=True)
1390
  chatgpt_intro = gr.Checkbox(value=True)
1391
  chatgpt_subs = gr.Checkbox(value=True)
1392
  chatgpt_output = gr.Textbox(label="Result")
1393
  chatgpt_btn = gr.Button("Process Video")
1394
  chatgpt_btn.click(
1395
+ add_production_to_video,
1396
+ inputs=[chatgpt_url_input, chatgpt_mood,
1397
+ chatgpt_crop, chatgpt_intro, chatgpt_subs],
1398
  outputs=chatgpt_output
1399
  )
1400
+
1401
  # Bind the production widget resource
1402
+ widget_code = gr.Code(label="Widget HTML",
1403
+ language="html", max_lines=5)
1404
  widget_btn = gr.Button("Show Widget")
1405
  widget_btn.click(production_widget_html, outputs=widget_code)
1406
+
1407
  # Bind the Video Studio tool
1408
  studio_output = gr.Textbox(label="Studio Status")
1409
  studio_btn = gr.Button("Open Video Studio")
1410
  studio_btn.click(open_video_studio, outputs=studio_output)
1411
+
1412
  # Bind the Video Studio widget resource
1413
+ studio_widget_code = gr.Code(
1414
+ label="Video Studio Widget", language="html", max_lines=5)
1415
  studio_widget_btn = gr.Button("Get Video Studio Widget")
1416
+ studio_widget_btn.click(
1417
+ video_studio_widget_html, outputs=studio_widget_code)
1418
 
1419
 
1420
  if __name__ == "__main__":