jomasego commited on
Commit
b046b1d
·
1 Parent(s): 50ca028

Fix Modal deployment using correct FastAPI endpoint pattern and update frontend

Browse files
Files changed (2) hide show
  1. app.py +78 -24
  2. modal_whisper_app.py +60 -158
app.py CHANGED
@@ -212,9 +212,63 @@ def process_video_input(input_string: str) -> Dict[str, Any]:
212
 
213
  return response_json
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  # Gradio Interface for the API endpoint
216
  api_interface = gr.Interface(
217
- fn=process_video_input,
218
  inputs=gr.Textbox(lines=1, label="Video URL or Local File Path for Interpretation",
219
  placeholder="Enter YouTube URL, direct video URL (.mp4, .mov, etc.), or local file path..."),
220
  outputs=gr.JSON(label="API Response"),
@@ -291,7 +345,7 @@ def call_topic_analysis_endpoint(topic_str: str, max_vids: int) -> Dict[str, Any
291
 
292
  demo_interface = gr.Interface(
293
  fn=demo_process_video,
294
- inputs=gr.Textbox(lines=1, label="Video URL or Local File Path", placeholder="Enter YouTube URL, direct video URL, or local file path..."),
295
  outputs=[gr.Textbox(label="Status"), gr.JSON(label="Comprehensive Analysis Output", scale=2)],
296
  title="Video Interpretation Demo",
297
  description="Provide a video URL or local file path to see its transcription status.",
@@ -351,33 +405,33 @@ with gr.Blocks(head=f"<script>{js_code_for_head}</script>") as app:
351
  with gr.Tab("Interactive Demo"):
352
  gr.Markdown("### Test the Full Video Analysis Pipeline")
353
  gr.Markdown("Enter a video URL or local file path to get a comprehensive JSON output including transcription, caption, actions, and objects.")
354
- with gr.Row():
355
- text_input = gr.Textbox(lines=1, label="Video URL or Local File Path", placeholder="Enter YouTube URL, direct video URL, or local file path...", scale=3)
356
 
357
- analysis_output = gr.JSON(label="Comprehensive Analysis Output", scale=2)
 
 
358
 
359
- with gr.Row():
360
- submit_button = gr.Button("Get Comprehensive Analysis", variant="primary", scale=1)
361
- clear_button = gr.Button("Clear", scale=1)
362
-
363
- # The 'process_video_input' function returns a single dictionary.
364
- submit_button.click(fn=process_video_input, inputs=[text_input], outputs=[analysis_output])
365
-
366
- def clear_all():
367
- return [None, None] # Clears text_input and analysis_output
368
- clear_button.click(fn=clear_all, inputs=[], outputs=[text_input, analysis_output])
369
-
 
 
 
370
  gr.Examples(
371
  examples=[
372
- ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
373
- ["http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"],
374
- # Add a local file path example if you have a common test video, e.g.:
375
- # ["./sample_video.mp4"] # User would need this file locally
376
  ],
377
- inputs=text_input,
378
- outputs=analysis_output,
379
- fn=process_video_input,
380
- cache_examples=False,
381
  )
382
  gr.Markdown("**Processing can take several minutes** depending on video length and model inference times. The cache on the Modal backend will speed up repeated requests for the same video.")
383
 
 
212
 
213
  return response_json
214
 
215
+ def process_video_input_new(input_string: str) -> Dict[str, Any]:
216
+ """
217
+ Processes the video (from URL or local file path) and returns its transcription status as a JSON object.
218
+ """
219
+ if not input_string:
220
+ return {
221
+ "status": "error",
222
+ "error_details": {
223
+ "message": "No video URL or file path provided.",
224
+ "input_received": input_string
225
+ }
226
+ }
227
+
228
+ video_path_to_process = None
229
+ # Get base_modal_url and construct modal_endpoint_url
230
+ base_modal_url = os.getenv("MODAL_APP_BASE_URL")
231
+ if not base_modal_url:
232
+ print("ERROR: MODAL_APP_BASE_URL environment variable not set.")
233
+ return {
234
+ "status": "error",
235
+ "error_details": {
236
+ "message": "Modal application base URL is not configured. Please set the MODAL_APP_BASE_URL environment variable.",
237
+ "input_received": input_string
238
+ }
239
+ }
240
+ modal_endpoint_url = base_modal_url.rstrip('/')
241
+ print(f"Using Modal endpoint URL: {modal_endpoint_url}")
242
+
243
+ try:
244
+ if input_string.startswith("http://") or input_string.startswith("https://"):
245
+ # Send URL as JSON payload to the Modal backend
246
+ payload = {"video_url": input_string}
247
+ print(f"Sending video URL as JSON payload: {payload}")
248
+ response = requests.post(modal_endpoint_url, json=payload, timeout=1860)
249
+ else:
250
+ # Local file path - still need to send as JSON for now (until we support file uploads)
251
+ return {"status": "error", "error_details": {"message": "Local file upload not yet supported. Please provide a video URL."}}
252
+
253
+ response.raise_for_status()
254
+ result = response.json()
255
+ print(f"Modal backend response: {result}")
256
+ return result
257
+
258
+ except requests.exceptions.HTTPError as e:
259
+ error_msg = f"HTTP {e.response.status_code}: {e.response.text[:200] if e.response else 'Unknown error'}"
260
+ print(f"HTTP error: {error_msg}")
261
+ return {"status": "error", "error_details": {"message": f"Video analysis service returned an error: {e.response.status_code}", "details": error_msg, "endpoint_url": modal_endpoint_url}}
262
+ except requests.exceptions.RequestException as e:
263
+ print(f"Request error: {e}")
264
+ return {"status": "error", "error_details": {"message": "Failed to connect to video analysis service", "details": str(e), "endpoint_url": modal_endpoint_url}}
265
+ except Exception as e:
266
+ print(f"Unexpected error: {e}")
267
+ return {"status": "error", "error_details": {"message": "Unexpected error during video analysis", "details": str(e), "endpoint_url": modal_endpoint_url}}
268
+
269
  # Gradio Interface for the API endpoint
270
  api_interface = gr.Interface(
271
+ fn=process_video_input_new,
272
  inputs=gr.Textbox(lines=1, label="Video URL or Local File Path for Interpretation",
273
  placeholder="Enter YouTube URL, direct video URL (.mp4, .mov, etc.), or local file path..."),
274
  outputs=gr.JSON(label="API Response"),
 
345
 
346
  demo_interface = gr.Interface(
347
  fn=demo_process_video,
348
+ inputs=gr.Textbox(lines=1, label="Video URL or Local File Path", placeholder="Enter YouTube URL, direct video URL, or local file path...", scale=3),
349
  outputs=[gr.Textbox(label="Status"), gr.JSON(label="Comprehensive Analysis Output", scale=2)],
350
  title="Video Interpretation Demo",
351
  description="Provide a video URL or local file path to see its transcription status.",
 
405
  with gr.Tab("Interactive Demo"):
406
  gr.Markdown("### Test the Full Video Analysis Pipeline")
407
  gr.Markdown("Enter a video URL or local file path to get a comprehensive JSON output including transcription, caption, actions, and objects.")
408
+ input_text = gr.Textbox(lines=1, label="Video URL or Local File Path", placeholder="Enter YouTube URL, direct video URL, or local file path...", scale=3)
409
+ output_json = gr.JSON(label="Comprehensive Analysis Output", scale=2)
410
 
411
+ with gr.Column(scale=1):
412
+ submit_btn = gr.Button("Submit", variant="primary")
413
+ clear_btn = gr.Button("Clear")
414
 
415
+ # Define functions for button actions
416
+ def handle_submit(input_text):
417
+ if not input_text.strip():
418
+ return "Please enter a video URL or file path."
419
+ return process_video_input_new(input_text.strip())
420
+
421
+ def handle_clear():
422
+ return "", ""
423
+
424
+ # Connect button events
425
+ submit_btn.click(fn=handle_submit, inputs=input_text, outputs=output_json)
426
+ clear_btn.click(fn=handle_clear, outputs=[input_text, output_json])
427
+
428
+ # Example inputs
429
  gr.Examples(
430
  examples=[
431
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
432
+ "https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4"
 
 
433
  ],
434
+ inputs=input_text
 
 
 
435
  )
436
  gr.Markdown("**Processing can take several minutes** depending on video length and model inference times. The cache on the Modal backend will speed up repeated requests for the same video.")
437
 
modal_whisper_app.py CHANGED
@@ -5,21 +5,21 @@ from starlette.routing import Mount
5
  import os
6
  import tempfile
7
  import io # Used by Whisper for BytesIO
8
- import hashlib # For generating cache keys
9
- import httpx # For downloading video from URL if needed by endpoint
10
- import gradio as gr
11
- import gradio.routes
12
- from typing import Dict, List, Any, Optional # For type hinting results and Optional in Pydantic
13
- from fastapi.responses import JSONResponse # For FastAPI endpoint
14
- from fastapi import File, Body, UploadFile, Query # For FastAPI file uploads, request body parts, and query parameters
15
  from fastapi.middleware.cors import CORSMiddleware
16
- from pydantic import BaseModel # For FastAPI request body validation
17
- import re # For parsing search results
18
  import asyncio # For concurrent video processing
19
 
20
- # --- Constants for Model Names ---
21
- WHISPER_MODEL_NAME = "openai/whisper-large-v3"
22
- CAPTION_MODEL_NAME = "Neleac/SpaceTimeGPT"
 
 
23
  CAPTION_PROCESSOR_NAME = "MCG-NJU/videomae-base" # For SpaceTimeGPT's video encoder
24
  # CAPTION_TOKENIZER_NAME = "gpt2" # For SpaceTimeGPT's text decoder (usually part of processor)
25
  ACTION_MODEL_NAME = "MCG-NJU/videomae-base-finetuned-kinetics"
@@ -49,29 +49,19 @@ video_analysis_image = (
49
  # --- Modal App Definition ---
50
  app = modal.App(name="video-analysis-gradio-pipeline") # New app name, using App
51
 
52
- fastapi_app = FastAPI() # Initialize FastAPI app
53
-
54
- @fastapi_app.get("/")
55
- async def read_root():
56
- return {"message": "FastAPI root is alive!"}
57
-
58
- # Define allowed origins for CORS
59
- origins = [
60
- "https://agents-mcp-hackathon-video-mcp.hf.space", # Your Hugging Face Space
61
- "http://localhost",
62
- "http://localhost:7860", # Default Gradio local port
63
- "http://127.0.0.1:7860", # Default Gradio local port
64
- # You can add other origins if needed, e.g., your Modal app's direct URL if you access it directly
65
- "https://jomasego--video-analysis-gradio-pipeline-main-asgi.modal.run"
66
- ]
67
-
68
- fastapi_app.add_middleware(
69
- CORSMiddleware,
70
- allow_origins=origins,
71
- allow_credentials=True,
72
- allow_methods=["*"], # Allows all methods
73
- allow_headers=["*"], # Allows all headers
74
- )
75
 
76
  # --- Modal Distributed Dictionary for Caching ---
77
  video_analysis_cache = modal.Dict.from_name("video_analysis_cache", create_if_missing=True)
@@ -231,6 +221,7 @@ def generate_captions_with_spacetimegpt(video_bytes: bytes) -> str:
231
  if video_path and os.path.exists(video_path):
232
  os.remove(video_path)
233
 
 
234
  # === 3. Action Recognition with VideoMAE ===
235
  @app.function(
236
  image=video_analysis_image,
@@ -447,115 +438,45 @@ async def analyze_video_comprehensive(video_bytes: bytes) -> Dict[str, Any]:
447
  return results
448
 
449
 
450
- # --- Pydantic model for FastAPI request ---
451
- class VideoAnalysisRequestPayload(BaseModel):
452
- video_url: Optional[str] = None # Reverted to optional
453
-
454
-
455
- # === FastAPI Endpoint for Comprehensive Analysis ===
456
- @fastapi_app.post("/analyze_video")
457
- async def process_video_for_analysis(
458
- payload: Optional[VideoAnalysisRequestPayload] = Body(None),
459
- video_file: Optional[UploadFile] = File(None) # Use UploadFile for type hint and async read
460
- ):
461
- print("[FastAPI Endpoint] Received request for comprehensive analysis.")
462
- video_bytes_content: Optional[bytes] = None
463
- video_source_description: str = "Unknown"
464
-
465
- if video_file: # Prioritize file upload
466
- print(f"[FastAPI Endpoint] Processing uploaded video file: {video_file.filename}")
467
- video_bytes_content = await video_file.read()
468
- await video_file.close() # Important to close the file
469
- if not video_bytes_content:
470
- print(f"[FastAPI Endpoint] Uploaded video file {video_file.filename} is empty.")
471
- return JSONResponse(status_code=400, content={"error": "Uploaded video file is empty."})
472
- video_source_description = f"direct file upload: {video_file.filename}"
473
- print(f"[FastAPI Endpoint] Read {len(video_bytes_content)} bytes from uploaded file {video_file.filename}.")
474
-
475
- elif payload and payload.video_url:
476
- video_url = str(payload.video_url) # Ensure it's a string
477
- print(f"[FastAPI Endpoint] Processing video_url: {video_url}")
478
- video_source_description = f"URL: {video_url}"
479
- try:
480
- async with httpx.AsyncClient() as client:
481
- response = await client.get(video_url, follow_redirects=True, timeout=60.0) # Download timeout
482
- response.raise_for_status()
483
- video_bytes_content = await response.aread()
484
- if not video_bytes_content:
485
- print(f"[FastAPI Endpoint] Download failed: content was empty for URL: {video_url}")
486
- return JSONResponse(status_code=400, content={"error": f"Failed to download video from URL: {video_url}. Content was empty."})
487
- print(f"[FastAPI Endpoint] Successfully downloaded {len(video_bytes_content)} bytes from {video_url}")
488
- except httpx.TimeoutException: # Specific timeout exception
489
- print(f"[FastAPI Endpoint] Timeout downloading video from URL: {video_url}")
490
- return JSONResponse(status_code=408, content={"error": f"Timeout downloading video from URL: {video_url}."})
491
- except httpx.RequestError as e:
492
- print(f"[FastAPI Endpoint] httpx.RequestError downloading video: {e}")
493
- return JSONResponse(status_code=400, content={"error": f"Error downloading video from URL: {video_url}. Details: {str(e)}"})
494
- except Exception as e: # Catch any other unexpected error during download
495
- print(f"[FastAPI Endpoint] Unexpected Exception downloading video: {e}")
496
- return JSONResponse(status_code=500, content={"error": f"Unexpected error downloading video. Details: {str(e)}"})
497
- else:
498
- # Neither file nor URL provided
499
- print("[FastAPI Endpoint] No video_url in payload and no video_file uploaded.")
500
- return JSONResponse(status_code=400, content={"error": "Either 'video_url' in JSON payload or a 'video_file' in form-data must be provided."})
501
-
502
- if not video_bytes_content:
503
- print("[FastAPI Endpoint] Critical error: video_bytes_content is not populated after input processing.")
504
- return JSONResponse(status_code=500, content={"error": "Internal server error: video data could not be obtained."})
505
-
506
- print(f"[FastAPI Endpoint] Calling analyze_video_comprehensive for video from {video_source_description} ({len(video_bytes_content)} bytes).")
507
  try:
508
- # Since process_video_for_analysis is an @app.function, it can .call() another @app.function
509
- analysis_results = await analyze_video_comprehensive.call(video_bytes_content)
 
 
 
 
 
 
 
 
 
 
510
  print("[FastAPI Endpoint] Comprehensive analysis finished.")
511
  return JSONResponse(status_code=200, content=analysis_results)
512
- except modal.exception.ModalError as e:
513
- print(f"[FastAPI Endpoint] ModalError during comprehensive analysis: {e}")
514
- return JSONResponse(status_code=500, content={"error": f"Modal processing error: {str(e)}"})
515
- except Exception as e:
516
- print(f"[FastAPI Endpoint] Unexpected Exception during comprehensive analysis: {e}")
517
- # import traceback # Uncomment for detailed server-side stack trace
518
- # traceback.print_exc() # Uncomment for detailed server-side stack trace
519
- return JSONResponse(status_code=500, content={"error": f"Unexpected server error during analysis: {str(e)}"})
520
-
521
-
522
- @fastapi_app.post("/analyze_topic")
523
- async def analyze_topic_endpoint(topic: str = Query(..., min_length=3, description="The topic to search videos for."),
524
- max_videos: Optional[int] = Query(3, ge=1, le=10, description="Maximum number of videos to find and analyze.")):
525
- """Endpoint to find videos for a topic, analyze them, and return aggregated results."""
526
- print(f"[FastAPI /analyze_topic] Received request for topic: '{topic}', max_videos: {max_videos}")
527
- try:
528
- print(f"[FastAPI /analyze_topic] Calling find_video_urls_for_topic for topic: '{topic}', max_videos: {max_videos}")
529
- # find_video_urls_for_topic is a Modal function that internally calls search_web and extracts URLs
530
- video_urls_dict = await find_video_urls_for_topic.call(topic=topic, max_results=max_videos)
531
-
532
- if "error" in video_urls_dict:
533
- print(f"[FastAPI /analyze_topic] Error from find_video_urls_for_topic: {video_urls_dict}")
534
- # Use status_code from the error dict if available, otherwise default (e.g., 500)
535
- return JSONResponse(status_code=video_urls_dict.get("status_code", 500), content=video_urls_dict)
536
-
537
- video_urls = video_urls_dict.get("video_urls", [])
538
-
539
- if not video_urls:
540
- print(f"[FastAPI /analyze_topic] No video URLs found by find_video_urls_for_topic for topic: '{topic}'")
541
- return JSONResponse(status_code=404, content={"error": "No relevant video URLs found for the topic after search.", "details": video_urls_dict.get("search_provider_log", "No additional search details.")})
542
 
543
- print(f"[FastAPI /analyze_topic] Found {len(video_urls)} URLs: {video_urls}. Calling analyze_videos_by_topic.")
544
- # analyze_videos_by_topic is another Modal function
545
- analysis_results = await analyze_videos_by_topic.call(video_urls=video_urls, topic=topic)
546
- print(f"[FastAPI /analyze_topic] Analysis complete for topic: '{topic}'.")
547
- return JSONResponse(status_code=200, content=analysis_results)
548
-
549
- except modal.exception.ModalError as e:
550
- print(f"[FastAPI /analyze_topic] ModalError during topic analysis: {e}")
551
- return JSONResponse(status_code=500, content={"error": f"Modal processing error during topic analysis: {str(e)}"})
552
  except Exception as e:
553
- print(f"[FastAPI /analyze_topic] Unexpected Exception during topic analysis: {e}")
554
- # import traceback # For server-side debugging
555
- # traceback.print_exc() # For server-side debugging
556
- return JSONResponse(status_code=500, content={"error": f"Unexpected server error during topic analysis: {str(e)}"})
557
-
558
-
559
 
560
  # === 6. Topic-Based Video Search ===
561
  @app.function(
@@ -823,22 +744,3 @@ def video_analyzer_gradio_ui():
823
 
824
  print("[Gradio] UI definition complete.")
825
  return gr.routes.App.create_app(demo)
826
-
827
-
828
- # === Main ASGI App (FastAPI + Gradio) ===
829
- @modal.asgi_app()
830
- def main_asgi():
831
- gradio_asgi_app = video_analyzer_gradio_ui()
832
-
833
- # fastapi_app is already defined globally and has its routes (@fastapi_app.post, @fastapi_app.get)
834
- # We should not mount Gradio inside fastapi_app if we are mounting fastapi_app itself into a Starlette app.
835
- # Ensure fastapi_app is 'clean' of the /gradio mount if it was done elsewhere,
836
- # or ensure it's not mounted if we are doing it at the Starlette level.
837
- # For this change, we assume fastapi_app does NOT have /gradio mounted internally.
838
-
839
- # Create a new top-level Starlette app
840
- top_level_app = Starlette(routes=[
841
- Mount("/api", app=fastapi_app), # Mount FastAPI app under /api
842
- Mount("/gradio", app=gradio_asgi_app) # Mount Gradio app under /gradio
843
- ])
844
- return top_level_app
 
5
  import os
6
  import tempfile
7
  import io # Used by Whisper for BytesIO
8
+ import httpx # For downloading videos from URLs
9
+ from typing import Optional, List, Dict, Any
10
+ import json
11
+ import hashlib
12
+ from fastapi.responses import JSONResponse
 
 
13
  from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel
15
+ import re # For parsing search results
16
  import asyncio # For concurrent video processing
17
 
18
+ import gradio as gr
19
+
20
+ # Global Configuration (should be at the top of the file)
21
+ WHISPER_MODEL_NAME = "openai/whisper-large-v3" # Use latest Whisper model
22
+ CAPTION_MODEL_NAME = "microsoft/xclip-base-patch16" # For SpaceTimeGPT alternative
23
  CAPTION_PROCESSOR_NAME = "MCG-NJU/videomae-base" # For SpaceTimeGPT's video encoder
24
  # CAPTION_TOKENIZER_NAME = "gpt2" # For SpaceTimeGPT's text decoder (usually part of processor)
25
  ACTION_MODEL_NAME = "MCG-NJU/videomae-base-finetuned-kinetics"
 
49
  # --- Modal App Definition ---
50
  app = modal.App(name="video-analysis-gradio-pipeline") # New app name, using App
51
 
52
+ # --- Pydantic model for web endpoint request ---
53
+ class VideoAnalysisRequestPayload(BaseModel):
54
+ video_url: str
55
+
56
+ # --- Constants for Model Names ---
57
+ # WHISPER_MODEL_NAME = "openai/whisper-large-v3"
58
+ # CAPTION_MODEL_NAME = "Neleac/SpaceTimeGPT"
59
+ # CAPTION_PROCESSOR_NAME = "MCG-NJU/videomae-base" # For SpaceTimeGPT's video encoder
60
+ # # CAPTION_TOKENIZER_NAME = "gpt2" # For SpaceTimeGPT's text decoder (usually part of processor)
61
+ # ACTION_MODEL_NAME = "MCG-NJU/videomae-base-finetuned-kinetics"
62
+ # ACTION_PROCESSOR_NAME = "MCG-NJU/videomae-base" # Or VideoMAEImageProcessor.from_pretrained(ACTION_MODEL_NAME)
63
+ # OBJECT_DETECTION_MODEL_NAME = "facebook/detr-resnet-50"
64
+ # OBJECT_DETECTION_PROCESSOR_NAME = "facebook/detr-resnet-50"
 
 
 
 
 
 
 
 
 
 
65
 
66
  # --- Modal Distributed Dictionary for Caching ---
67
  video_analysis_cache = modal.Dict.from_name("video_analysis_cache", create_if_missing=True)
 
221
  if video_path and os.path.exists(video_path):
222
  os.remove(video_path)
223
 
224
+
225
  # === 3. Action Recognition with VideoMAE ===
226
  @app.function(
227
  image=video_analysis_image,
 
438
  return results
439
 
440
 
441
+ # === FastAPI Endpoint for Video Analysis ===
442
+ @app.function(
443
+ image=video_analysis_image,
444
+ secrets=[HF_TOKEN_SECRET],
445
+ gpu="any",
446
+ timeout=1800,
447
+ )
448
+ @modal.fastapi_endpoint(method="POST")
449
+ def process_video_analysis(payload: VideoAnalysisRequestPayload):
450
+ """FastAPI endpoint for comprehensive video analysis."""
451
+ print(f"[FastAPI Endpoint] Received request for video analysis")
452
+
453
+ video_url = payload.video_url
454
+ if not video_url:
455
+ return JSONResponse(status_code=400, content={"error": "video_url must be provided in JSON payload."})
456
+
457
+ print(f"[FastAPI Endpoint] Processing video_url: {video_url}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  try:
459
+ # Download video
460
+ import httpx
461
+ with httpx.Client() as client:
462
+ response = client.get(video_url, follow_redirects=True, timeout=60.0)
463
+ response.raise_for_status()
464
+ video_bytes = response.content
465
+ if not video_bytes:
466
+ return JSONResponse(status_code=400, content={"error": f"Failed to download video from URL: {video_url}. Content was empty."})
467
+ print(f"[FastAPI Endpoint] Successfully downloaded {len(video_bytes)} bytes from {video_url}")
468
+
469
+ # Call comprehensive analysis
470
+ analysis_results = analyze_video_comprehensive.call(video_bytes)
471
  print("[FastAPI Endpoint] Comprehensive analysis finished.")
472
  return JSONResponse(status_code=200, content=analysis_results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
+ except httpx.RequestError as e:
475
+ print(f"[FastAPI Endpoint] httpx.RequestError downloading video: {e}")
476
+ return JSONResponse(status_code=400, content={"error": f"Error downloading video from URL: {video_url}. Details: {str(e)}"})
 
 
 
 
 
 
477
  except Exception as e:
478
+ print(f"[FastAPI Endpoint] Unexpected Exception during analysis: {e}")
479
+ return JSONResponse(status_code=500, content={"error": f"Unexpected server error during analysis: {str(e)}"})
 
 
 
 
480
 
481
  # === 6. Topic-Based Video Search ===
482
  @app.function(
 
744
 
745
  print("[Gradio] UI definition complete.")
746
  return gr.routes.App.create_app(demo)