jomasego commited on
Commit
0036cd7
·
1 Parent(s): b180948

Ensure app.py uses MODAL_APP_BASE_URL for frontend

Browse files
Files changed (1) hide show
  1. app.py +187 -44
app.py CHANGED
@@ -6,7 +6,7 @@ import subprocess
6
  import re
7
  import shutil # Added for rmtree
8
  import modal
9
- from typing import Dict, Any # Added for type hinting
10
 
11
  def is_youtube_url(url_string: str) -> bool:
12
  """Checks if the given string is a YouTube URL."""
@@ -158,41 +158,67 @@ def process_video_input(input_string: str) -> Dict[str, Any]:
158
  # os.environ["MODAL_TOKEN_ID"] = "your_modal_token_id" # Replace or set in HF Space
159
  # os.environ["MODAL_TOKEN_SECRET"] = "your_modal_token_secret" # Replace or set in HF Space
160
 
161
- print("Looking up Modal function 'whisper-transcriber/transcribe_video_audio'...")
162
- # The function name should match what was deployed.
163
- # It's typically 'AppName/FunctionName' or just 'FunctionName' if app is default.
164
- # Based on your deployment log, app name is 'whisper-transcriber'
165
- # and function is 'transcribe_video_audio'
166
- try:
167
- f = modal.Function.from_name("whisper-transcriber", "transcribe_video_audio")
168
- print("Modal function looked up successfully.")
169
- except modal.Error as e:
170
- print("Modal function 'whisper-transcriber/transcribe_video_audio' not found. Trying with just function name.")
171
- # Fallback or alternative lookup, though the above should be correct for named apps.
172
- # This might be needed if the app name context is implicit.
173
- # For a named app 'whisper-transcriber' and function 'transcribe_video_audio',
174
- # the lookup `modal.Function.lookup("whisper-transcriber", "transcribe_video_audio")` is standard.
175
- # If it was deployed as part of the default app, then just "transcribe_video_audio" might work.
176
- # Given the deployment log, the first lookup should be correct.
177
  return {
178
  "status": "error",
179
  "error_details": {
180
- "message": "Could not find the deployed Modal function. Please check deployment status and name.",
181
- "modal_function_name": "whisper-transcriber/transcribe_video_audio"
182
  }
183
  }
 
184
 
185
- print("Calling Modal function for transcription...")
186
- # Using .remote() for asynchronous execution, .call() for synchronous
187
- # For Gradio, synchronous (.call()) might be simpler to handle the response directly.
188
- transcription = f.remote(video_bytes_content) # Use .remote() for Modal function call
189
- print(f"Received transcription from Modal: {transcription[:100]}...")
190
- return {
191
- "status": "success",
192
- "data": {
193
- "transcription": transcription
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
195
- }
196
  except FileNotFoundError:
197
  print(f"Error: Video file not found at {video_path_to_process} before sending to Modal.")
198
  return {
@@ -249,7 +275,7 @@ api_interface = gr.Interface(
249
  outputs=gr.JSON(label="API Response"),
250
  title="Video Interpretation Input",
251
  description="Provide a video URL or local file path to get its interpretation status as JSON.",
252
- allow_flagging="never",
253
  examples=[
254
  ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
255
  ["https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]
@@ -257,29 +283,76 @@ api_interface = gr.Interface(
257
  )
258
 
259
  # Gradio Interface for a simple user-facing demo
260
- def demo_process_video(input_string: str) -> str:
261
  """
262
  A simple demo function for the Gradio UI.
263
- It calls the same backend logic as the API.
264
  """
265
- print(f"Demo received input: {input_string}")
266
- result = process_video_input(input_string) # Call the core logic
267
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  demo_interface = gr.Interface(
270
  fn=demo_process_video,
271
- inputs=gr.Textbox(label="Upload Video URL or Local File Path for Demo",
272
- placeholder="Enter YouTube URL, direct video URL (.mp4, .mov, etc.), or local file path..."),
273
- outputs="text",
274
  title="Video Interpretation Demo",
275
  description="Provide a video URL or local file path to see its transcription status.",
276
- allow_flagging="never"
277
  )
278
 
279
- # JavaScript to find and replace the 'Use via API' link text
280
- # This attempts to change the text a few times in case Gradio renders elements late.
281
- js_code_for_head = """
282
- (function() {
283
  console.log('[MCP Script] Initializing script to change API link text...');
284
  let foundAndChangedGlobal = false; // Declare here to be accessible in setInterval
285
 
@@ -330,10 +403,80 @@ with gr.Blocks(head=f"<script>{js_code_for_head}</script>") as app:
330
  api_interface.render()
331
  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.")
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  with gr.Tab("Demo (for Manual Testing)"):
334
  gr.Markdown("### Manually test video URLs or paths for interpretation and observe the JSON response.")
335
  demo_interface.render()
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  # Launch the Gradio application
338
  if __name__ == "__main__":
339
- app.launch(server_name="0.0.0.0")
 
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."""
 
158
  # os.environ["MODAL_TOKEN_ID"] = "your_modal_token_id" # Replace or set in HF Space
159
  # os.environ["MODAL_TOKEN_SECRET"] = "your_modal_token_secret" # Replace or set in HF Space
160
 
161
+ print("Preparing to call Modal FastAPI endpoint for comprehensive analysis...")
162
+ # IMPORTANT: Replace this with your actual Modal app's deployed FastAPI endpoint URL
163
+ # This URL is typically found in your Modal deployment logs or dashboard.
164
+ # It will look something like: https://YOUR_MODAL_WORKSPACE--video-analysis-gradio-pipeline-process-video-for-analysis.modal.run/analyze_video
165
+ # Or, if the FastAPI endpoint function itself is not separately deployed but part of the main app deployment:
166
+ # https://YOUR_MODAL_WORKSPACE--video-analysis-gradio-pipeline-fastapi-app.modal.run/analyze_video
167
+ # (The exact name depends on how Modal names the deployed web endpoint for the FastAPI app)
168
+ # For now, using a placeholder. This MUST be configured.
169
+ base_modal_url = os.getenv("MODAL_APP_BASE_URL")
170
+ if not base_modal_url:
171
+ print("ERROR: MODAL_APP_BASE_URL environment variable not set.")
 
 
 
 
 
172
  return {
173
  "status": "error",
174
  "error_details": {
175
+ "message": "Modal application base URL is not configured. Please set the MODAL_APP_BASE_URL environment variable.",
176
+ "input_received": input_string
177
  }
178
  }
179
+ modal_endpoint_url = f"{base_modal_url.rstrip('/')}/analyze_video"
180
 
181
+ files = {'video_file': (os.path.basename(video_path_to_process), video_bytes_content, 'video/mp4')}
182
+
183
+ print(f"Calling Modal endpoint: {modal_endpoint_url}")
184
+ try:
185
+ response = requests.post(modal_endpoint_url, files=files, timeout=1860) # Timeout slightly longer than Modal function
186
+ response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
187
+ analysis_results = response.json()
188
+ print(f"Received results from Modal endpoint: {str(analysis_results)[:200]}...")
189
+ return {
190
+ "status": "success",
191
+ "data": analysis_results
192
+ }
193
+ except requests.exceptions.Timeout:
194
+ print(f"Request to Modal endpoint {MODAL_ENDPOINT_URL} timed out.")
195
+ return {
196
+ "status": "error",
197
+ "error_details": {
198
+ "message": "Request to video analysis service timed out.",
199
+ "endpoint_url": MODAL_ENDPOINT_URL
200
+ }
201
+ }
202
+ except requests.exceptions.HTTPError as e:
203
+ print(f"HTTP error calling Modal endpoint {MODAL_ENDPOINT_URL}: {e.response.status_code} - {e.response.text}")
204
+ return {
205
+ "status": "error",
206
+ "error_details": {
207
+ "message": f"Video analysis service returned an error: {e.response.status_code}",
208
+ "details": e.response.text,
209
+ "endpoint_url": MODAL_ENDPOINT_URL
210
+ }
211
+ }
212
+ except requests.exceptions.RequestException as e:
213
+ print(f"Error calling Modal endpoint {MODAL_ENDPOINT_URL}: {e}")
214
+ return {
215
+ "status": "error",
216
+ "error_details": {
217
+ "message": "Failed to connect to video analysis service.",
218
+ "details": str(e),
219
+ "endpoint_url": MODAL_ENDPOINT_URL
220
+ }
221
  }
 
222
  except FileNotFoundError:
223
  print(f"Error: Video file not found at {video_path_to_process} before sending to Modal.")
224
  return {
 
275
  outputs=gr.JSON(label="API Response"),
276
  title="Video Interpretation Input",
277
  description="Provide a video URL or local file path to get its interpretation status as JSON.",
278
+ flagging_options=None,
279
  examples=[
280
  ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
281
  ["https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]
 
283
  )
284
 
285
  # Gradio Interface for a simple user-facing demo
286
+ def demo_process_video(input_string: str) -> tuple[str, Dict[str, Any]]:
287
  """
288
  A simple demo function for the Gradio UI.
289
+ It calls process_video_input and unpacks its result for separate display.
290
  """
291
+ result = process_video_input(input_string)
292
+ status_str = result.get("status", "Unknown Status")
293
+
294
+ # The second part of the tuple should be the 'data' if successful,
295
+ # or the 'error_details' (or the whole result) if there was an error.
296
+ if status_str == "success" and "data" in result:
297
+ details_json = result["data"]
298
+ elif "error_details" in result:
299
+ details_json = result["error_details"]
300
+ else: # Fallback, show the whole result
301
+ details_json = result
302
+
303
+ return status_str, details_json
304
+
305
+
306
+ def call_topic_analysis_endpoint(topic_str: str, max_vids: int) -> Dict[str, Any]:
307
+ """Calls the Modal FastAPI endpoint for topic-based video analysis."""
308
+ if not topic_str:
309
+ return {"status": "error", "error_details": {"message": "Topic cannot be empty."}}
310
+ if not (1 <= max_vids <= 10): # Max 10 as defined in FastAPI endpoint, can adjust
311
+ return {"status": "error", "error_details": {"message": "Max videos must be between 1 and 10."}}
312
+
313
+ base_modal_url = os.getenv("MODAL_APP_BASE_URL")
314
+ if not base_modal_url:
315
+ print("ERROR: MODAL_APP_BASE_URL environment variable not set.")
316
+ return {
317
+ "status": "error",
318
+ "error_details": {
319
+ "message": "Modal application base URL is not configured. Please set the MODAL_APP_BASE_URL environment variable."
320
+ }
321
+ }
322
+ topic_endpoint_url = f"{base_modal_url.rstrip('/')}/analyze_topic"
323
+
324
+ params = {"topic": topic_str, "max_videos": max_vids}
325
+ print(f"Calling Topic Analysis endpoint: {topic_endpoint_url} with params: {params}")
326
+
327
+ try:
328
+ # Using POST as defined in modal_whisper_app.py for /analyze_topic
329
+ response = requests.post(topic_endpoint_url, params=params, timeout=3660) # Long timeout for multiple videos
330
+ response.raise_for_status()
331
+ results = response.json()
332
+ print(f"Received results from Topic Analysis endpoint: {str(results)[:200]}...")
333
+ return results # The endpoint should return the aggregated JSON directly
334
+ except requests.exceptions.Timeout:
335
+ print(f"Request to Topic Analysis endpoint {topic_endpoint_url} timed out.")
336
+ return {"status": "error", "error_details": {"message": "Request to topic analysis service timed out."}}
337
+ except requests.exceptions.HTTPError as e:
338
+ print(f"HTTP error calling Topic Analysis endpoint {topic_endpoint_url}: {e.response.status_code} - {e.response.text}")
339
+ return {"status": "error", "error_details": {"message": f"Topic analysis service returned an error: {e.response.status_code}", "details": e.response.text}}
340
+ except requests.exceptions.RequestException as e:
341
+ print(f"Error calling Topic Analysis endpoint {topic_endpoint_url}: {e}")
342
+ return {"status": "error", "error_details": {"message": "Failed to connect to topic analysis service.", "details": str(e)}}
343
+ except Exception as e:
344
+ print(f"An unexpected error occurred: {e}")
345
+ return {"status": "error", "error_details": {"message": "An unexpected error occurred during topic analysis call.", "details": str(e)}}
346
 
347
  demo_interface = gr.Interface(
348
  fn=demo_process_video,
349
+ inputs=gr.Textbox(lines=1, label="Video URL or Local File Path", placeholder="Enter YouTube URL, direct video URL, or local file path..."),
350
+ outputs=[gr.Textbox(label="Status"), gr.JSON(label="Comprehensive Analysis Output", scale=2)],
 
351
  title="Video Interpretation Demo",
352
  description="Provide a video URL or local file path to see its transcription status.",
353
+ flagging_options=None
354
  )
355
 
 
 
 
 
356
  console.log('[MCP Script] Initializing script to change API link text...');
357
  let foundAndChangedGlobal = false; // Declare here to be accessible in setInterval
358
 
 
403
  api_interface.render()
404
  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.")
405
 
406
+ with gr.Tab("Interactive Demo"):
407
+ gr.Markdown("### Test the Full Video Analysis Pipeline")
408
+ gr.Markdown("Enter a video URL or local file path to get a comprehensive JSON output including transcription, caption, actions, and objects.")
409
+ with gr.Row():
410
+ 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)
411
+
412
+ analysis_output = gr.JSON(label="Comprehensive Analysis Output", scale=2)
413
+
414
+ with gr.Row():
415
+ submit_button = gr.Button("Get Comprehensive Analysis", variant="primary", scale=1)
416
+ clear_button = gr.Button("Clear", scale=1)
417
+
418
+ # The 'process_video_input' function returns a single dictionary.
419
+ submit_button.click(fn=process_video_input, inputs=[text_input], outputs=[analysis_output])
420
+
421
+ def clear_all():
422
+ return [None, None] # Clears text_input and analysis_output
423
+ clear_button.click(fn=clear_all, inputs=[], outputs=[text_input, analysis_output])
424
+
425
+ gr.Examples(
426
+ examples=[
427
+ ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
428
+ ["http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"],
429
+ # Add a local file path example if you have a common test video, e.g.:
430
+ # ["./sample_video.mp4"] # User would need this file locally
431
+ ],
432
+ inputs=text_input,
433
+ outputs=analysis_output,
434
+ fn=process_video_input,
435
+ cache_examples=False,
436
+ )
437
+ 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.")
438
+
439
  with gr.Tab("Demo (for Manual Testing)"):
440
  gr.Markdown("### Manually test video URLs or paths for interpretation and observe the JSON response.")
441
  demo_interface.render()
442
 
443
+ with gr.Tab("Topic Video Analysis"):
444
+ gr.Markdown("### Analyze Multiple Videos Based on a Topic")
445
+ gr.Markdown("Enter a topic, and the system will search for relevant videos, analyze them, and provide an aggregated JSON output.")
446
+
447
+ with gr.Row():
448
+ topic_input = gr.Textbox(label="Enter Topic", placeholder="e.g., 'best cat videos', 'Python programming tutorials'", scale=3)
449
+ max_videos_input = gr.Number(label="Max Videos to Analyze", value=3, minimum=1, maximum=5, step=1, scale=1) # Max 5 for UI, backend might support more
450
+
451
+ topic_analysis_output = gr.JSON(label="Topic Analysis Results")
452
+
453
+ with gr.Row():
454
+ topic_submit_button = gr.Button("Analyze Topic Videos", variant="primary")
455
+ topic_clear_button = gr.Button("Clear")
456
+
457
+ topic_submit_button.click(
458
+ fn=call_topic_analysis_endpoint,
459
+ inputs=[topic_input, max_videos_input],
460
+ outputs=[topic_analysis_output]
461
+ )
462
+
463
+ def clear_topic_outputs():
464
+ return [None, 3, None] # topic_input, max_videos_input (reset to default), topic_analysis_output
465
+ topic_clear_button.click(fn=clear_topic_outputs, inputs=[], outputs=[topic_input, max_videos_input, topic_analysis_output])
466
+
467
+ gr.Examples(
468
+ examples=[
469
+ ["AI in healthcare", 2],
470
+ ["sustainable energy solutions", 3],
471
+ ["how to make sourdough bread", 1]
472
+ ],
473
+ inputs=[topic_input, max_videos_input],
474
+ outputs=topic_analysis_output,
475
+ fn=call_topic_analysis_endpoint,
476
+ cache_examples=False
477
+ )
478
+ gr.Markdown("**Note:** This process involves searching for videos and then analyzing each one. It can take a significant amount of time, especially for multiple videos. The backend has a long timeout, but please be patient.")
479
+
480
  # Launch the Gradio application
481
  if __name__ == "__main__":
482
+ app.launch(debug=True, server_name="0.0.0.0")