jomasego commited on
Commit
7e2a742
·
1 Parent(s): b6e00fe

Fix: Add fn and outputs to gr.Examples to resolve ValueError

Browse files
Files changed (1) hide show
  1. app.py +119 -413
app.py CHANGED
@@ -1,438 +1,144 @@
1
  import gradio as gr
2
  import os
3
- import requests
4
- import tempfile
5
- import subprocess
6
- import re
7
- import shutil # Added for rmtree
8
- import modal
9
- from typing import Dict, Any, Optional # Added for type hinting
10
-
11
- def is_youtube_url(url_string: str) -> bool:
12
- """Checks if the given string is a YouTube URL."""
13
- # More robust regex to find YouTube video ID, accommodating various URL formats
14
- # and additional query parameters.
15
- youtube_regex = (
16
- r'(?:youtube(?:-nocookie)?\.com/(?:[^/\n\s]+/|watch(?:/|\?(?:[^&\n\s]+&)*v=)|embed(?:/|\?(?:[^&\n\s]+&)*feature=oembed)|shorts/|live/)|youtu\.be/)'
17
- r'([a-zA-Z0-9_-]{11})' # This captures the 11-character video ID
18
- )
19
- # We use re.search because the video ID might not be at the start of the query string part of the URL.
20
- # re.match only matches at the beginning of the string (or beginning of line in multiline mode).
21
- # The regex now directly looks for the 'v=VIDEO_ID' or youtu.be/VIDEO_ID structure.
22
- # The first part of the regex matches the domain and common paths, the second part captures the ID.
23
- return bool(re.search(youtube_regex, url_string))
24
-
25
- def download_video(url_string: str, temp_dir: str) -> str | None:
26
- """Downloads video from a URL (YouTube or direct link) to a temporary directory."""
27
- if is_youtube_url(url_string):
28
- print(f"Attempting to download YouTube video: {url_string}")
29
- # Define a fixed output filename pattern within the temp_dir
30
- output_filename_template = "downloaded_video.%(ext)s" # yt-dlp replaces %(ext)s
31
- output_path_template = os.path.join(temp_dir, output_filename_template)
32
-
33
- cmd = [
34
- "yt-dlp",
35
- "-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4/best", # Prefer mp4 format
36
- "--output", output_path_template,
37
- url_string
38
- ]
39
- print(f"Executing yt-dlp command: {' '.join(cmd)}")
40
-
41
- try:
42
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, check=False)
43
-
44
- print(f"yt-dlp STDOUT:\n{result.stdout}")
45
- print(f"yt-dlp STDERR:\n{result.stderr}")
46
-
47
- if result.returncode == 0:
48
- # Find the actual downloaded file based on the template
49
- downloaded_file_path = None
50
- for item in os.listdir(temp_dir):
51
- if item.startswith("downloaded_video."):
52
- potential_path = os.path.join(temp_dir, item)
53
- if os.path.isfile(potential_path):
54
- downloaded_file_path = potential_path
55
- print(f"YouTube video successfully downloaded to: {downloaded_file_path}")
56
- break
57
- if downloaded_file_path:
58
- return downloaded_file_path
59
- else:
60
- print(f"yt-dlp seemed to succeed (exit code 0) but the output file 'downloaded_video.*' was not found in {temp_dir}.")
61
- return None
62
- else:
63
- print(f"yt-dlp failed with return code {result.returncode}.")
64
- return None
65
- except subprocess.TimeoutExpired:
66
- print(f"yt-dlp command timed out after 300 seconds for URL: {url_string}")
67
- return None
68
- except Exception as e:
69
- print(f"An unexpected error occurred during yt-dlp execution for {url_string}: {e}")
70
- return None
71
-
72
- elif url_string.startswith(('http://', 'https://')) and url_string.lower().endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')):
73
- print(f"Attempting to download direct video link: {url_string}")
74
- try:
75
- response = requests.get(url_string, stream=True, timeout=300) # 5 min timeout
76
- response.raise_for_status() # Raises HTTPError for bad responses (4XX or 5XX)
77
-
78
- filename = os.path.basename(url_string) or "downloaded_video_direct.mp4"
79
- video_file_path = os.path.join(temp_dir, filename)
80
-
81
- with open(video_file_path, 'wb') as f:
82
- for chunk in response.iter_content(chunk_size=8192):
83
- f.write(chunk)
84
- print(f"Direct video downloaded successfully to: {video_file_path}")
85
- return video_file_path
86
- except requests.exceptions.RequestException as e:
87
- print(f"Error downloading direct video link {url_string}: {e}")
88
- return None
89
- except Exception as e:
90
- print(f"An unexpected error occurred during direct video download for {url_string}: {e}")
91
- return None
92
- else:
93
- print(f"Input '{url_string}' is not a recognized YouTube URL or direct video link for download.")
94
- return None
95
-
96
-
97
- def process_video_input(input_string: str) -> Dict[str, Any]:
98
- """
99
- Processes the video (from URL or local file path) and returns its transcription status as a JSON object.
100
- """
101
- if not input_string:
102
- return {
103
- "status": "error",
104
- "error_details": {
105
- "message": "No video URL or file path provided.",
106
- "input_received": input_string
107
- }
108
- }
109
-
110
- video_path_to_process = None
111
- # Get base_modal_url and construct modal_endpoint_url
112
- base_modal_url = os.getenv("MODAL_APP_BASE_URL")
113
- if not base_modal_url:
114
- print("ERROR: MODAL_APP_BASE_URL environment variable not set.")
115
- return {
116
- "status": "error",
117
- "error_details": {
118
- "message": "Modal application base URL is not configured. Please set the MODAL_APP_BASE_URL environment variable.",
119
- "input_received": input_string
120
- }
121
- }
122
- modal_endpoint_url = f"{base_modal_url.rstrip('/')}/analyze_video"
123
- print(f"Target Modal endpoint: {modal_endpoint_url}")
124
-
125
- response_json = None # Initialize to ensure it's always defined before return
126
-
127
- try:
128
- if input_string.startswith(('http://', 'https://')):
129
- print(f"Input is a URL: {input_string}. Sending URL to Modal endpoint as JSON.")
130
- payload = {"video_url": input_string}
131
- headers = {'Content-Type': 'application/json'}
132
- response = requests.post(modal_endpoint_url, json=payload, headers=headers, timeout=1860)
133
-
134
- elif os.path.exists(input_string):
135
- print(f"Input is a local file path: {input_string}. Sending file content to Modal endpoint.")
136
- video_path_to_process = input_string # Use input_string as the path
137
- try:
138
- with open(video_path_to_process, "rb") as video_file:
139
- video_bytes_content = video_file.read()
140
- print(f"Read {len(video_bytes_content)} bytes from video file '{video_path_to_process}'.")
141
- files = {'video_file': (os.path.basename(video_path_to_process), video_bytes_content, 'video/mp4')}
142
- response = requests.post(modal_endpoint_url, files=files, timeout=1860)
143
- except FileNotFoundError: # Catch if file disappears just before open
144
- print(f"Error: Video file not found at {video_path_to_process} when trying to read for upload.")
145
- return { # Return immediately
146
- "status": "error",
147
- "error_details": {
148
- "message": "Video file disappeared before it could be read for upload.",
149
- "path_attempted": video_path_to_process
150
- }
151
- }
152
- else:
153
- # This handles cases where input_string is neither a URL nor an existing file path
154
- print(f"Input '{input_string}' is not a valid URL or an existing file path.")
155
- return { # Return immediately
156
- "status": "error",
157
- "error_details": {
158
- "message": f"Input '{input_string}' is not a valid URL or an existing file path.",
159
- "input_received": input_string
160
- }
161
- }
162
-
163
- # Common response handling
164
- response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
165
- analysis_results = response.json()
166
- print(f"Received results from Modal endpoint: {str(analysis_results)[:200]}...")
167
- response_json = {
168
- "status": "success",
169
- "data": analysis_results
170
- }
171
-
172
- except requests.exceptions.Timeout:
173
- print(f"Request to Modal endpoint {modal_endpoint_url} timed out.")
174
- response_json = {
175
- "status": "error",
176
- "error_details": {
177
- "message": "Request to video analysis service timed out.",
178
- "endpoint_url": modal_endpoint_url
179
- }
180
- }
181
- except requests.exceptions.HTTPError as e:
182
- print(f"HTTP error calling Modal endpoint {modal_endpoint_url}: {e.response.status_code} - {e.response.text}")
183
- response_json = {
184
- "status": "error",
185
- "error_details": {
186
- "message": f"Video analysis service returned an error: {e.response.status_code}",
187
- "details": e.response.text,
188
- "endpoint_url": modal_endpoint_url
189
- }
190
- }
191
- except requests.exceptions.RequestException as e: # General request exception
192
- print(f"Error calling Modal endpoint {modal_endpoint_url}: {e}") # Corrected MODAL_ENDPOINT_URL to modal_endpoint_url
193
- response_json = {
194
- "status": "error",
195
- "error_details": {
196
- "message": "Failed to connect to video analysis service.",
197
- "details": str(e),
198
- "endpoint_url": modal_endpoint_url # Corrected MODAL_ENDPOINT_URL to modal_endpoint_url
199
- }
200
- }
201
- except Exception as e: # Catch-all for other unexpected errors
202
- print(f"An unexpected error occurred in process_video_input: {e}")
203
- import traceback
204
- traceback.print_exc()
205
- response_json = {
206
- "status": "error",
207
- "error_details": {
208
- "message": f"An unexpected error occurred: {str(e)}",
209
- "exception_type": type(e).__name__
210
- }
211
- }
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"),
275
- title="Video Interpretation Input",
276
- description="Provide a video URL or local file path to get its interpretation status as JSON.",
277
- flagging_options=None,
278
- examples=[
279
- ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
280
- ["https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]
281
- ]
282
- )
283
 
284
- # Gradio Interface for a simple user-facing demo
285
- def demo_process_video(input_string: str) -> tuple[str, Dict[str, Any]]:
286
- """
287
- A simple demo function for the Gradio UI.
288
- It calls process_video_input and unpacks its result for separate display.
289
- """
290
- result = process_video_input(input_string)
291
- status_str = result.get("status", "Unknown Status")
292
 
293
- # The second part of the tuple should be the 'data' if successful,
294
- # or the 'error_details' (or the whole result) if there was an error.
295
- if status_str == "success" and "data" in result:
296
- details_json = result["data"]
297
- elif "error_details" in result:
298
- details_json = result["error_details"]
299
- else: # Fallback, show the whole result
300
- details_json = result
301
-
302
- return status_str, details_json
303
-
304
-
305
- def call_topic_analysis_endpoint(topic_str: str, max_vids: int) -> Dict[str, Any]:
306
- """Calls the Modal FastAPI endpoint for topic-based video analysis."""
307
- if not topic_str:
308
- return {"status": "error", "error_details": {"message": "Topic cannot be empty."}}
309
- if not (1 <= max_vids <= 10): # Max 10 as defined in FastAPI endpoint, can adjust
310
- return {"status": "error", "error_details": {"message": "Max videos must be between 1 and 10."}}
311
-
312
- base_modal_url = os.getenv("MODAL_APP_BASE_URL")
313
- if not base_modal_url:
314
- print("ERROR: MODAL_APP_BASE_URL environment variable not set.")
315
- return {
316
- "status": "error",
317
- "error_details": {
318
- "message": "Modal application base URL is not configured. Please set the MODAL_APP_BASE_URL environment variable."
319
- }
320
- }
321
- topic_endpoint_url = f"{base_modal_url.rstrip('/')}/analyze_topic"
322
-
323
- params = {"topic": topic_str, "max_videos": max_vids}
324
- print(f"Calling Topic Analysis endpoint: {topic_endpoint_url} with params: {params}")
325
-
326
  try:
327
- # Using POST as defined in modal_whisper_app.py for /analyze_topic
328
- response = requests.post(topic_endpoint_url, params=params, timeout=3660) # Long timeout for multiple videos
329
- response.raise_for_status()
330
- results = response.json()
331
- print(f"Received results from Topic Analysis endpoint: {str(results)[:200]}...")
332
- return results # The endpoint should return the aggregated JSON directly
333
- except requests.exceptions.Timeout:
334
- print(f"Request to Topic Analysis endpoint {topic_endpoint_url} timed out.")
335
- return {"status": "error", "error_details": {"message": "Request to topic analysis service timed out."}}
336
- except requests.exceptions.HTTPError as e:
337
- print(f"HTTP error calling Topic Analysis endpoint {topic_endpoint_url}: {e.response.status_code} - {e.response.text}")
338
- return {"status": "error", "error_details": {"message": f"Topic analysis service returned an error: {e.response.status_code}", "details": e.response.text}}
339
- except requests.exceptions.RequestException as e:
340
- print(f"Error calling Topic Analysis endpoint {topic_endpoint_url}: {e}")
341
- return {"status": "error", "error_details": {"message": "Failed to connect to topic analysis service.", "details": str(e)}}
342
  except Exception as e:
343
- print(f"An unexpected error occurred: {e}")
344
- return {"status": "error", "error_details": {"message": "An unexpected error occurred during topic analysis call.", "details": str(e)}}
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.",
352
- flagging_options=None
353
- )
354
 
355
- js_code_for_head = """
356
- console.log('[MCP Script] Initializing script to change API link text...');
357
- let foundAndChangedGlobal = false; // Declare here to be accessible in setInterval
358
 
359
- function attemptChangeApiLinkText() {
360
- const links = document.querySelectorAll('a');
361
- // console.log('[MCP Script] Found ' + links.length + ' anchor tags on this attempt.');
362
- for (let i = 0; i < links.length; i++) {
363
- const linkText = links[i].textContent ? links[i].textContent.trim() : '';
364
- if (linkText === 'Use via API' || linkText === 'Share via Link') { // Target both possible texts
365
- links[i].textContent = 'Use as an MCP or via API';
366
- console.log('[MCP Script] Successfully changed link text from: ' + linkText);
367
- foundAndChangedGlobal = true;
368
- return true; // Indicate success
369
- }
370
- }
371
- return false; // Indicate not found/changed in this attempt
372
- }
373
 
374
- let attempts = 0;
375
- const maxAttempts = 50; // Try for up to 5 seconds (50 * 100ms)
376
- let initialScanDone = false;
 
 
 
 
 
 
 
377
 
378
- const intervalId = setInterval(() => {
379
- if (!initialScanDone && attempts === 0) {
380
- console.log('[MCP Script] Performing initial scan for API link text.');
381
- initialScanDone = true;
382
- }
383
 
384
- if (attemptChangeApiLinkText() || attempts >= maxAttempts) {
385
- clearInterval(intervalId);
386
- if (attempts >= maxAttempts && !foundAndChangedGlobal) {
387
- console.log('[MCP Script] Max attempts reached. Target link was not found or changed. It might not be rendered or has a different initial text.');
388
- }
389
- }
390
- attempts++;
391
- }, 100);
392
- """
393
 
394
- # Combine interfaces into a Blocks app
395
- with gr.Blocks(head=f"<script>{js_code_for_head}</script>") as app:
396
- gr.Markdown("# LLM Video interpretation MCP")
397
- gr.Markdown("This Hugging Face Space acts as a backend for processing video context for AI models.")
 
 
 
 
 
 
 
398
 
399
- with gr.Tab("API Endpoint (for AI Models)"):
400
- gr.Markdown("### Use this endpoint from another application (e.g., another Hugging Face Space).")
401
- gr.Markdown("The `process_video_input` function (for video interpretation) is exposed here.")
402
- api_interface.render()
403
- gr.Markdown("**Note:** Some YouTube videos may fail to download if they require login or cookie authentication due to YouTube's restrictions. Direct video links are generally more reliable for automated processing.")
404
 
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
 
438
  with gr.Tab("Demo (for Manual Testing)"):
 
1
  import gradio as gr
2
  import os
3
+ import httpx
4
+ from typing import Dict, Any
5
+
6
+ # --- Backend Client Functions ---
7
+ # These functions call the Modal/backend endpoints.
8
+
9
+ async def call_video_analysis_backend(video_url: str) -> Dict[str, Any]:
10
+ """Calls the backend to analyze a single video."""
11
+ # Default to a placeholder if the env var is not set, to avoid crashing.
12
+ backend_url = os.getenv("BACKEND_VIDEO_URL", "https://your-backend-hf-space-for-video/process_video_analysis")
13
+ if not video_url:
14
+ return {"status": "error", "message": "Video URL cannot be empty."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ print(f"Sending request to backend for video: {video_url}")
17
+ payload = {"video_url": video_url}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
19
+ async with httpx.AsyncClient(timeout=1800.0) as client:
20
+ response = await client.post(backend_url, json=payload)
21
+ response.raise_for_status()
22
+ return response.json()
23
+ except httpx.HTTPStatusError as e:
24
+ return {"status": "error", "message": f"Backend Error: {e.response.status_code}", "details": e.response.text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  except Exception as e:
26
+ return {"status": "error", "message": "Failed to connect to backend", "details": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ async def call_topic_analysis_backend(topic: str, max_videos: int) -> Dict[str, Any]:
29
+ """Calls the backend to analyze videos for a topic."""
30
+ backend_url = os.getenv("BACKEND_TOPIC_URL", "https://your-backend-hf-space-for-topic/analyze_topic")
31
+ if not topic:
32
+ return {"status": "error", "message": "Topic cannot be empty."}
 
 
 
33
 
34
+ print(f"Sending request to backend for topic: {topic} ({max_videos} videos)")
35
+ payload = {"topic": topic, "max_videos": max_videos}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  try:
37
+ async with httpx.AsyncClient(timeout=3600.0) as client:
38
+ response = await client.post(backend_url, json=payload)
39
+ response.raise_for_status()
40
+ return response.json()
41
+ except httpx.HTTPStatusError as e:
42
+ return {"status": "error", "message": f"Backend Error: {e.response.status_code}", "details": e.response.text}
 
 
 
 
 
 
 
 
 
43
  except Exception as e:
44
+ return {"status": "error", "message": "Failed to connect to backend", "details": str(e)}
 
45
 
46
+ # --- Gradio Tool Functions (Wrappers for MCP) ---
 
 
 
 
 
 
 
47
 
48
+ async def analyze_video(video_url: str):
49
+ """
50
+ Triggers a comprehensive analysis of a single video from a URL.
51
 
52
+ This tool calls a backend service to perform multiple analyses:
53
+ - Transcribes audio to text.
54
+ - Generates a descriptive caption for the video content.
55
+ - Recognizes main actions in the video.
56
+ - Detects objects in keyframes.
 
 
 
 
 
 
 
 
 
57
 
58
+ :param video_url: The public URL of the video to be processed (e.g., a YouTube link).
59
+ :return: A JSON object containing the full analysis results from the backend.
60
+ """
61
+ status_update = f"Analyzing video: {video_url}..."
62
+ results = await call_video_analysis_backend(video_url)
63
+ if isinstance(results, dict) and results.get("analysis") is None:
64
+ status_update = f"Error analyzing video: {results.get('error', 'Unknown error')}"
65
+ else:
66
+ status_update = "Video analysis complete."
67
+ return status_update, results
68
 
69
+ async def analyze_topic(topic: str, max_videos: int):
70
+ """
71
+ Finds and analyzes multiple videos based on a given topic.
 
 
72
 
73
+ This tool calls a backend service that searches for videos related to the topic,
74
+ then runs a comprehensive analysis on each video found.
 
 
 
 
 
 
 
75
 
76
+ :param topic: The topic to search for (e.g., 'latest AI advancements').
77
+ :param max_videos: The maximum number of videos to find and analyze (1-5).
78
+ :return: A JSON object with the aggregated analysis results for all videos.
79
+ """
80
+ status_update = f"Analyzing topic '{topic}' with {max_videos} videos... this can take a very long time."
81
+ results = await call_topic_analysis_backend(topic, max_videos)
82
+ if isinstance(results, dict) and results.get("results") is None:
83
+ status_update = f"Error analyzing topic: {results.get('error', 'Unknown error')}"
84
+ else:
85
+ status_update = "Topic analysis complete."
86
+ return status_update, results
87
 
88
+ # --- Gradio UI ---
 
 
 
 
89
 
90
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
91
+ gr.Markdown("# LLM Video Interpretation MCP")
92
+ gr.Markdown("This Hugging Face Space provides tools for processing video context for AI agents. Use the tools below to analyze videos by URL or by topic.")
93
+
94
+ with gr.Tab("Single Video Analysis"):
95
+ gr.Markdown("## Analyze a single video from a URL")
96
+ with gr.Row():
97
+ video_url_input = gr.Textbox(label="Video URL", placeholder="Enter a YouTube or direct video URL...", scale=4)
98
+ submit_button = gr.Button("Analyze Video", variant="primary")
99
+ status_text = gr.Textbox(label="Status", interactive=False)
100
+ json_output = gr.JSON(label="Analysis Results")
 
 
 
 
 
 
 
101
 
102
+ submit_button.click(
103
+ analyze_video,
104
+ inputs=[video_url_input],
105
+ outputs=[status_text, json_output],
106
+ api_name="analyze_video"
107
+ )
108
+ gr.Examples(
109
+ examples=["https://www.youtube.com/watch?v=3wLg_t_H2Xw", "https://www.youtube.com/watch?v=h42dDpgE7g8"],
110
+ inputs=video_url_input,
111
+ fn=analyze_video,
112
+ outputs=[status_text, json_output]
113
+ )
114
+
115
+ with gr.Tab("Topic Video Analysis"):
116
+ gr.Markdown("## Analyze multiple videos based on a topic")
117
+ with gr.Row():
118
+ topic_input = gr.Textbox(label="Enter a topic", placeholder="e.g., 'Apple Vision Pro review'", scale=3)
119
+ max_videos_slider = gr.Slider(minimum=1, maximum=5, value=2, step=1, label="Number of Videos to Analyze")
120
+ topic_submit_button = gr.Button("Analyze Topic", variant="primary")
121
+ topic_status_text = gr.Textbox(label="Status", interactive=False)
122
+ topic_json_output = gr.JSON(label="Analysis Results")
123
 
124
+ topic_submit_button.click(
125
+ analyze_topic,
126
+ inputs=[topic_input, max_videos_slider],
127
+ outputs=[topic_status_text, topic_json_output],
128
+ api_name="analyze_topic"
129
+ )
130
  gr.Examples(
131
+ examples=[["self-driving car technology", 2], ["open source large language models", 3]],
132
+ inputs=[topic_input, max_videos_slider],
133
+ fn=analyze_topic,
134
+ outputs=[topic_status_text, topic_json_output]
 
135
  )
136
+
137
+ # Set environment variables in your Hugging Face Space settings, not here.
138
+ # BACKEND_VIDEO_URL = "https://your-modal-or-backend-url/process_video_analysis"
139
+ # BACKEND_TOPIC_URL = "https://your-modal-or-backend-url/analyze_topic"
140
+
141
+ demo.launch()
142
  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.")
143
 
144
  with gr.Tab("Demo (for Manual Testing)"):