Spaces:
Sleeping
Sleeping
Fix Modal deployment using correct FastAPI endpoint pattern and update frontend
Browse files- app.py +78 -24
- 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=
|
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 |
-
|
355 |
-
|
356 |
|
357 |
-
|
|
|
|
|
358 |
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
|
|
|
|
|
|
370 |
gr.Examples(
|
371 |
examples=[
|
372 |
-
|
373 |
-
|
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=
|
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
|
9 |
-
|
10 |
-
import
|
11 |
-
import
|
12 |
-
from
|
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
|
17 |
-
import re # For parsing search results
|
18 |
import asyncio # For concurrent video processing
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
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 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
#
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
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 |
-
#
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
)
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
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 |
-
#
|
509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
544 |
-
|
545 |
-
|
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
|
554 |
-
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|