ladybug11 commited on
Commit
5b510b1
Β·
1 Parent(s): 7922c49
Files changed (1) hide show
  1. app.py +340 -391
app.py CHANGED
@@ -1,35 +1,32 @@
1
- import gradio as gr
2
  import os
3
- import requests
4
- import random
5
- import tempfile
6
- import json
7
  import time
 
8
  import shutil
 
 
 
9
 
 
10
  from openai import OpenAI
11
  from smolagents import CodeAgent, MCPClient, tool
12
- from huggingface_hub import InferenceClient # still imported if you need it elsewhere
13
- from moviepy.editor import VideoFileClip, ImageClip, CompositeVideoClip, AudioFileClip
14
- from PIL import Image, ImageDraw, ImageFont
15
- import numpy as np
16
- from elevenlabs import ElevenLabs, VoiceSettings
17
 
18
- # Import our new Gemini quote generator
19
  from quote_generator_gemini import HybridQuoteGenerator
20
 
21
- # Initialize clients
 
 
 
22
  openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
23
  PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
24
- elevenlabs_client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
25
 
26
- # Initialize Hybrid Quote Generator (Gemini + OpenAI fallback)
27
  hybrid_quote_generator = HybridQuoteGenerator(
28
  gemini_key=os.getenv("GEMINI_API_KEY"),
29
- openai_client=openai_client
30
  )
31
 
32
- # Initialize MCP Client (connecting to existing MCP server)
33
  try:
34
  mcp_client = MCPClient("https://abidlabs-mcp-tools.hf.space")
35
  mcp_enabled = True
@@ -37,42 +34,43 @@ except Exception as e:
37
  print(f"MCP initialization warning: {e}")
38
  mcp_enabled = False
39
 
40
- # -----------------------
 
41
  # TOOLS
42
- # -----------------------
43
 
44
  @tool
45
  def generate_quote_tool(niche: str, style: str) -> str:
46
  """
47
- Generate a powerful inspirational quote using Gemini AI with variety tracking.
48
- Falls back to OpenAI if Gemini is unavailable.
49
-
50
  Args:
51
- niche: The category of quote (Motivation, Business, Fitness, etc.)
52
- style: The visual style (Cinematic, Nature, Urban, Minimal, Abstract)
53
-
54
  Returns:
55
- A powerful, unique quote string
56
  """
57
  try:
58
- result = hybrid_quote_generator.generate_quote(niche, style, prefer_gemini=True)
59
-
60
- if result["success"]:
 
 
 
61
  quote = result["quote"]
62
- source = result["source"]
63
-
64
- # Log which generator was used
65
  if source == "gemini":
66
  stats = result.get("stats", {})
67
- print(f"✨ Generated with Gemini (Total: {stats.get('total_quotes_generated', 0)})")
 
 
 
68
  else:
69
- print(f"✨ Generated with OpenAI (fallback)")
70
-
71
  return quote
72
  else:
73
- error_msg = result.get("error", "Unknown error")
74
- return f"Error generating quote: {error_msg}"
75
-
76
  except Exception as e:
77
  return f"Error generating quote: {str(e)}"
78
 
@@ -80,322 +78,253 @@ def generate_quote_tool(niche: str, style: str) -> str:
80
  @tool
81
  def search_pexels_video_tool(style: str, niche: str) -> dict:
82
  """
83
- Search and fetch a matching video from Pexels based on style and niche.
84
-
85
  Args:
86
- style: Visual style (Cinematic, Nature, Urban, Minimal, Abstract)
87
- niche: Content niche (Motivation, Business, Fitness, etc.)
88
-
89
  Returns:
90
- Dictionary with video_url, search_query, and pexels_url
 
 
 
 
 
91
  """
92
-
93
- # Intelligent search strategy mapping
94
  search_strategies = {
95
  "Motivation": {
96
  "Cinematic": ["person climbing mountain", "running sunrise", "achievement success"],
97
  "Nature": ["sunrise mountain peak", "ocean waves powerful", "forest light"],
98
  "Urban": ["city skyline dawn", "person running city", "urban success"],
99
  "Minimal": ["minimal motivation", "single person silhouette", "clean inspiring"],
100
- "Abstract": ["light rays hope", "particles rising", "abstract energy"]
101
  },
102
  "Business/Entrepreneurship": {
103
  "Cinematic": ["business cityscape", "office modern", "handshake deal"],
104
  "Nature": ["growth plant", "river flowing", "sunrise new beginning"],
105
  "Urban": ["city business", "office skyline", "modern workspace"],
106
  "Minimal": ["desk minimal", "workspace clean", "simple office"],
107
- "Abstract": ["network connections", "growth chart", "abstract progress"]
108
  },
109
  "Fitness": {
110
  "Cinematic": ["athlete training", "gym workout", "running outdoor"],
111
  "Nature": ["outdoor workout", "mountain hiking", "beach running"],
112
  "Urban": ["city running", "urban fitness", "street workout"],
113
  "Minimal": ["gym minimal", "simple workout", "clean fitness"],
114
- "Abstract": ["energy motion", "strength power", "dynamic movement"]
115
  },
116
  "Mindfulness": {
117
  "Cinematic": ["meditation sunset", "peaceful landscape", "calm water"],
118
  "Nature": ["forest peaceful", "calm lake", "zen garden"],
119
  "Urban": ["city peaceful morning", "quiet street", "urban calm"],
120
  "Minimal": ["minimal zen", "simple meditation", "clean peaceful"],
121
- "Abstract": ["calm waves", "gentle motion", "soft particles"]
122
  },
123
  "Stoicism": {
124
  "Cinematic": ["ancient architecture", "statue philosopher", "timeless landscape"],
125
  "Nature": ["mountain strong", "oak tree", "stone nature"],
126
  "Urban": ["classical building", "statue city", "ancient modern"],
127
  "Minimal": ["stone minimal", "simple strong", "pillar minimal"],
128
- "Abstract": ["marble texture", "stone abstract", "timeless pattern"]
129
  },
130
  "Leadership": {
131
  "Cinematic": ["team meeting", "leader speaking", "group collaboration"],
132
  "Nature": ["eagle flying", "lion pride", "mountain top"],
133
  "Urban": ["office leadership", "boardroom", "city leadership"],
134
  "Minimal": ["chess pieces", "simple leadership", "clean professional"],
135
- "Abstract": ["network leader", "connection points", "guiding light"]
136
  },
137
  "Love & Relationships": {
138
  "Cinematic": ["couple sunset", "romance beautiful", "love cinematic"],
139
  "Nature": ["couple nature", "romantic sunset", "peaceful together"],
140
  "Urban": ["couple city", "romance urban", "love city lights"],
141
  "Minimal": ["hands holding", "simple love", "minimal romance"],
142
- "Abstract": ["hearts flowing", "love particles", "connection abstract"]
143
- }
144
  }
145
-
146
- # Get queries for this niche + style combination
147
  queries = search_strategies.get(niche, {}).get(style, ["aesthetic nature"])
148
-
149
  try:
150
- headers = {"Authorization": PEXELS_API_KEY}
151
-
152
- # Pick a random query for variety
153
  query = random.choice(queries)
154
-
155
- url = f"https://api.pexels.com/videos/search?query={query}&per_page=15&orientation=portrait"
 
 
 
156
  response = requests.get(url, headers=headers)
157
  data = response.json()
158
-
159
  if "videos" in data and len(data["videos"]) > 0:
160
- # Pick a random video from results
161
  video = random.choice(data["videos"][:10])
162
  video_files = video.get("video_files", [])
163
-
164
- # Find portrait/vertical video
165
- portrait_videos = [vf for vf in video_files if vf.get("width", 0) < vf.get("height", 0)]
166
-
 
 
 
167
  if portrait_videos:
168
  selected = random.choice(portrait_videos)
169
  return {
 
170
  "video_url": selected.get("link"),
171
  "search_query": query,
172
  "pexels_url": video.get("url"),
173
- "success": True
174
  }
175
-
176
- # Fallback to any HD video
177
  if video_files:
178
  return {
 
179
  "video_url": video_files[0].get("link"),
180
  "search_query": query,
181
  "pexels_url": video.get("url"),
182
- "success": True
183
  }
184
-
185
  return {
 
186
  "video_url": None,
187
  "search_query": query,
188
  "pexels_url": None,
189
- "success": False,
190
- "error": "No suitable videos found"
191
  }
192
-
193
  except Exception as e:
194
  return {
 
195
  "video_url": None,
196
  "search_query": "",
197
  "pexels_url": None,
198
- "success": False,
199
- "error": str(e)
200
  }
201
 
202
 
203
  @tool
204
- def generate_voice_commentary_tool(quote_text: str, niche: str, output_path: str) -> dict:
205
  """
206
- Generate insightful voice commentary explaining the deeper meaning of the quote.
207
- Uses Gemini to create thoughtful explanation, then ElevenLabs to voice it.
208
- This adds VALUE - not just reading what's already on screen.
 
 
 
 
 
 
 
 
 
209
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  try:
211
- import google.generativeai as genai
212
-
213
- explanation_prompt = f"""Given this {niche} quote:
214
-
215
- "{quote_text}"
216
-
217
- Write a brief, insightful voice-over commentary that explains the deeper meaning or practical wisdom.
218
-
219
- Requirements:
220
- - 2-3 sentences maximum
221
- - Around 25-35 words total
222
- - Spoken naturally (like a wise mentor)
223
- - Add insight that isn't obvious from reading
224
- - Make it thought-provoking
225
- - Don't start with "This quote..." - dive into the insight
226
-
227
- Return ONLY the commentary, nothing else."""
228
- genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
229
- model = genai.GenerativeModel('gemini-1.5-flash')
230
-
231
- response = model.generate_content(
232
- explanation_prompt,
233
- generation_config={
234
- "temperature": 0.7,
235
- "max_output_tokens": 100
236
- }
237
  )
238
-
239
- explanation = response.text.strip().strip('"').strip("'")
240
- print(f"πŸ“ Commentary: {explanation}")
241
-
242
- audio = elevenlabs_client.text_to_speech.convert(
243
- text=explanation,
244
- voice_id="pNInz6obpgDQGcFmaJgB",
245
- model_id="eleven_multilingual_v2",
246
- voice_settings=VoiceSettings(
247
- stability=0.6,
248
- similarity_boost=0.8,
249
- style=0.6,
250
- use_speaker_boost=True
251
- )
252
- )
253
-
254
- with open(output_path, 'wb') as f:
255
- for chunk in audio:
256
- f.write(chunk)
257
-
 
 
 
 
 
 
 
258
  return {
259
  "success": True,
260
  "output_path": output_path,
261
- "explanation": explanation,
262
- "message": "Voice commentary created!"
263
  }
264
-
265
  except Exception as e:
266
  return {
267
  "success": False,
268
  "output_path": None,
269
- "explanation": None,
270
- "message": f"Error creating commentary: {str(e)}"
271
  }
272
 
273
 
274
- @tool
275
- def create_quote_video_tool(video_url: str, quote_text: str, output_path: str, audio_path: str = None) -> dict:
276
- """
277
- Create a final quote video by overlaying text on the background video.
278
- Uses Modal for fast processing (4-8x faster) with local fallback.
279
- Optionally adds voice narration audio.
280
- """
281
- modal_endpoint = os.getenv("MODAL_ENDPOINT_URL")
282
-
283
- if modal_endpoint:
284
- try:
285
- import base64
286
-
287
- print("πŸš€ Processing on Modal (fast!)...")
288
-
289
- audio_b64 = None
290
- if audio_path and os.path.exists(audio_path):
291
- with open(audio_path, 'rb') as f:
292
- audio_bytes = f.read()
293
- audio_b64 = base64.b64encode(audio_bytes).decode()
294
- print(f" 🎀 Including voice commentary audio ({len(audio_bytes)} bytes)")
295
-
296
- response = requests.post(
297
- modal_endpoint,
298
- json={
299
- "video_url": video_url,
300
- "quote_text": quote_text,
301
- "audio_b64": audio_b64
302
- },
303
- timeout=120
304
- )
305
-
306
- if response.status_code == 200:
307
- result = response.json()
308
-
309
- if result.get("success"):
310
- video_b64 = result["video"]
311
- video_bytes = base64.b64decode(video_b64)
312
-
313
- with open(output_path, 'wb') as f:
314
- f.write(video_bytes)
315
-
316
- print(f"βœ… Modal processing complete! {result['size_mb']:.2f}MB")
317
-
318
- return {
319
- "success": True,
320
- "output_path": output_path,
321
- "message": f"Video created via Modal in ~20s ({result['size_mb']:.2f}MB)"
322
- }
323
- else:
324
- print(f"⚠️ Modal returned error: {result.get('error', 'Unknown')}")
325
- else:
326
- print(f"⚠️ Modal HTTP error: {response.status_code}")
327
-
328
- print("⚠️ Modal failed, falling back to local processing...")
329
-
330
- except requests.Timeout:
331
- print(f"⚠️ Modal timeout after 120s, falling back to local...")
332
- except Exception as e:
333
- print(f"⚠️ Modal error: {e}, falling back to local processing...")
334
- else:
335
- print("ℹ️ MODAL_ENDPOINT_URL not configured, using local processing")
336
-
337
- # For hackathon deploy: avoid heavy local MoviePy on Spaces to prevent hangs
338
- print("πŸ”§ Local processing disabled on this deployment.")
339
- return {
340
- "success": False,
341
- "output_path": None,
342
- "message": "Local processing disabled - please configure Modal for video generation. Deploy Modal with: modal deploy modal_video_processing.py"
343
- }
344
-
345
- # -----------------------
346
  # AGENT INITIALIZATION
347
- # -----------------------
348
 
349
  def initialize_agent():
350
- """Initialize the CodeAgent with MCP capabilities"""
351
  try:
352
- # Use Hugging Face Inference API for the agent's LLM
353
  hf_token = os.getenv("HF_TOKEN")
354
- # If you have a specific model, you can set HF_MODEL_ID in your Space secrets
355
- hf_model_id = os.getenv("HF_MODEL_ID") # e.g. "mistralai/Mixtral-8x7B-Instruct-v0.1"
356
-
357
- if hf_model_id:
358
- model = InferenceClient(model=hf_model_id, token=hf_token)
359
- else:
360
- # Fallback: rely on default model configured on the Space / org
361
- model = InferenceClient(token=hf_token)
362
-
363
  agent = CodeAgent(
364
- tools=[
365
- generate_quote_tool,
366
- search_pexels_video_tool,
367
- generate_voice_commentary_tool,
368
- create_quote_video_tool,
369
- ],
370
  model=model,
371
  additional_authorized_imports=[
372
- "requests",
373
- "openai",
374
- "random",
375
- "tempfile",
376
  "os",
377
- "google.generativeai",
378
  "json",
 
 
 
379
  ],
380
  max_steps=15,
381
  )
382
-
383
  if mcp_enabled:
384
  agent.mcp_clients = [mcp_client]
385
-
386
  return agent, None
387
  except Exception as e:
388
  return None, f"Agent initialization error: {str(e)}"
389
 
 
390
  agent, agent_error = initialize_agent()
391
 
392
- # -----------------------
 
393
  # PIPELINES
394
- # -----------------------
395
 
396
- def mcp_agent_pipeline(niche, style, num_variations=1):
397
  """
398
  MAIN PIPELINE: uses smolagents CodeAgent.run to plan & call tools.
 
399
  The agent:
400
  - calls generate_quote_tool
401
  - calls search_pexels_video_tool multiple times
@@ -403,22 +332,22 @@ def mcp_agent_pipeline(niche, style, num_variations=1):
403
  - returns JSON with status_log + video_paths
404
  """
405
  base_log = ["πŸ€– **MCP AGENT RUN**"]
406
-
407
  if agent_error or agent is None:
408
  base_log.append(f"❌ Agent initialization failed: {agent_error}")
409
  base_log.append("πŸ”„ Falling back to direct tool pipeline...")
410
  status, vids = fallback_pipeline(niche, style, num_variations)
411
  return "\n".join(base_log + [status]), vids
412
-
413
  try:
414
  output_dir = "/tmp/quote_videos"
415
  gallery_dir = "/data/gallery_videos"
416
  os.makedirs(output_dir, exist_ok=True)
417
  os.makedirs(gallery_dir, exist_ok=True)
418
-
419
  timestamp = int(time.time())
420
- base_prefix = f"{output_dir}/agent_{timestamp}_v"
421
-
422
  user_task = f"""
423
  You are an autonomous Python agent helping creators generate short vertical quote videos.
424
 
@@ -426,81 +355,84 @@ Niche: {niche}
426
  Style: {style}
427
  Number of variations: {num_variations}
428
 
429
- You have these TOOLS already available in this environment:
430
 
431
  1. generate_quote_tool(niche: str, style: str) -> str
432
- - Returns a unique quote as plain text.
433
 
434
  2. search_pexels_video_tool(style: str, niche: str) -> dict
435
- - Returns a dict with at least:
436
- - "video_url": str or None
437
  - "success": bool
 
438
 
439
- 3. create_quote_video_tool(video_url: str, quote_text: str, output_path: str, audio_path: str | None = None) -> dict
440
- - Downloads a video, overlays the quote, and writes a video file to output_path.
441
- - Returns a dict with at least:
442
  - "success": bool
443
- - "output_path": str | None
444
-
445
- You MAY also have access to external MCP tools through your mcp_clients attribute; you can call them if helpful (e.g. logging, inspiration, etc.), but they are optional.
446
 
447
  Your job:
448
 
449
- 1. Call generate_quote_tool once with the given niche and style to obtain quote_text.
450
- 2. For each variation i from 1 to {num_variations}, call search_pexels_video_tool(style, niche) to get a background video.
451
- 3. For each successful search result, create an output path EXACTLY as:
452
- "{base_prefix}{{i}}.mp4" where i is the variation index (1-based).
453
- 4. Call create_quote_video_tool(video_url, quote_text, output_path) for each variation.
454
- 5. Only keep variations where create_quote_video_tool returns success == True and a non-empty output_path.
455
- 6. Build a human-readable status_log string summarizing what you did (which tools were called, success/failures).
456
- 7. Return ONLY a valid JSON object of the form:
 
 
 
457
 
458
  {{
459
  "status_log": "multi-line human readable description of what you did",
460
  "video_paths": [
461
  "{base_prefix}1.mp4",
462
- "... only include paths that actually succeeded ..."
463
  ]
464
  }}
465
 
466
  CRITICAL:
467
- - Do not wrap the JSON in markdown or backticks.
468
- - Do not add extra keys.
469
- - Do not print anything besides the JSON.
470
  """
 
471
  agent_result = agent.run(user_task)
472
-
473
  try:
474
  parsed = json.loads(agent_result)
475
  except Exception as parse_err:
476
- raise ValueError(f"Agent output was not valid JSON: {parse_err}\nRaw: {agent_result[:500]}")
477
-
 
 
 
478
  status_log = parsed.get("status_log", "")
479
  video_paths = parsed.get("video_paths", [])
480
-
481
- # Keep only existing paths
482
  valid_paths = [
483
- p for p in video_paths
484
- if isinstance(p, str) and os.path.exists(p)
485
  ]
486
-
487
  if not valid_paths:
488
- raise ValueError("Agent returned no valid video paths or files do not exist on disk.")
489
-
490
- # Copy to gallery directory
491
  for idx, path in enumerate(valid_paths):
492
  try:
493
  filename = os.path.basename(path)
494
- gallery_path = os.path.join(gallery_dir, f"gallery_{timestamp}_v{idx+1}_{filename}")
 
 
 
495
  shutil.copy2(path, gallery_path)
496
  except Exception as e:
497
  print(f"⚠️ Failed to copy to gallery for {path}: {e}")
498
-
499
  full_status = "\n".join(base_log + [status_log])
500
  return full_status, valid_paths[:3]
501
-
502
  except Exception as e:
503
- # Hard fallback if anything goes wrong
504
  fallback_status, fallback_videos = fallback_pipeline(niche, style, num_variations)
505
  combined_status = "\n".join(
506
  base_log
@@ -509,57 +441,54 @@ CRITICAL:
509
  return combined_status, fallback_videos
510
 
511
 
512
- def fallback_pipeline(niche, style, num_variations=1):
513
- """Fallback pipeline if MCP agent fails: direct tool calls."""
514
  status_log = []
515
  status_log.append("πŸ”„ **FALLBACK MODE (Direct Tool Execution)**\n")
516
-
517
- # Generate quote
518
- status_log.append("🧠 Generating quote with Gemini...")
519
  quote = generate_quote_tool(niche, style)
520
-
521
- if "Error" in quote:
522
  return "\n".join(status_log) + f"\n❌ {quote}", []
523
-
524
  status_log.append(" βœ… Quote generated\n")
525
-
526
- # Search videos
527
  status_log.append(f"πŸ” Searching for {num_variations} videos...")
528
  video_results = []
529
- for i in range(num_variations):
530
- video_result = search_pexels_video_tool(style, niche)
531
- if video_result["success"]:
532
- video_results.append(video_result)
533
-
534
  if not video_results:
535
- return "\n".join(status_log) + "\n❌ No videos found", []
536
-
 
537
  status_log.append(f" βœ… Found {len(video_results)} videos\n")
538
-
539
- # Create videos
540
- status_log.append("🎬 Creating videos...")
541
  output_dir = "/tmp/quote_videos"
542
  gallery_dir = "/data/gallery_videos"
543
  os.makedirs(output_dir, exist_ok=True)
544
  os.makedirs(gallery_dir, exist_ok=True)
545
-
546
  timestamp = int(time.time())
547
  created_videos = []
548
-
549
- for i, video_result in enumerate(video_results):
550
  output_filename = f"quote_video_v{i+1}_{timestamp}.mp4"
551
  output_path = os.path.join(output_dir, output_filename)
552
-
553
  creation_result = create_quote_video_tool(
554
- video_result["video_url"],
555
- quote,
556
- output_path,
557
- None
558
  )
559
-
560
- if creation_result["success"]:
561
  created_videos.append(creation_result["output_path"])
562
-
563
  gallery_filename = f"gallery_{timestamp}_v{i+1}.mp4"
564
  gallery_path = os.path.join(gallery_dir, gallery_filename)
565
  try:
@@ -569,162 +498,182 @@ def fallback_pipeline(niche, style, num_variations=1):
569
  else:
570
  error_msg = creation_result.get("message", "Unknown error")
571
  status_log.append(f" ❌ Video {i+1} error: {error_msg}")
572
-
573
  if not created_videos:
574
- return "\n".join(status_log) + "\n❌ Video creation failed", []
575
-
 
576
  status_log.append(f" βœ… Created {len(created_videos)} videos!\n")
577
  status_log.append("🎬 **COMPLETE!**")
578
-
579
  return "\n".join(status_log), created_videos
580
 
581
- # -----------------------
582
- # GRADIO UI
583
- # -----------------------
584
 
585
- with gr.Blocks(title="AIQuoteClipGenerator - MCP + Gemini Edition", theme=gr.themes.Soft()) as demo:
586
- gr.Markdown("""
 
 
 
 
 
 
 
 
587
  # 🎬 AIQuoteClipGenerator
588
  ### MCP-Powered with Gemini AI
589
-
590
  **Key Features:**
591
- - 🌟 **Gemini AI:** No more repetitive quotes! Smart variety tracking
592
- - πŸ”— **MCP Server Usage:** smolagents CodeAgent + MCP client
593
- - πŸ› οΈ **4 Custom Tools:** Quote + Video search + Voice (optional) + Video creation
594
- - πŸ€– **Agent Reasoning:** Autonomous task execution via CodeAgent.run
595
- - ⚑ **Modal Processing:** 4-8x faster video creation
596
- - 🎨 **Multiple Variations:** Get different video styles
597
- """)
598
-
599
- # Example Gallery
600
  with gr.Accordion("πŸ“Έ Example Gallery - Recent Videos", open=True):
601
- gr.Markdown("See what others have created! Updates automatically after generation.")
602
-
 
 
603
  with gr.Row():
604
- gallery_video1 = gr.Video(label="", height=300, show_label=False, interactive=False)
605
- gallery_video2 = gr.Video(label="", height=300, show_label=False, interactive=False)
606
- gallery_video3 = gr.Video(label="", height=300, show_label=False, interactive=False)
607
-
608
  with gr.Row():
609
- gallery_video4 = gr.Video(label="", height=300, show_label=False, interactive=False)
610
- gallery_video5 = gr.Video(label="", height=300, show_label=False, interactive=False)
611
- gallery_video6 = gr.Video(label="", height=300, show_label=False, interactive=False)
612
-
613
  def load_gallery_videos():
614
  gallery_output_dir = "/data/gallery_videos"
615
  os.makedirs(gallery_output_dir, exist_ok=True)
616
-
617
  import glob
 
618
  existing_videos = sorted(
619
- glob.glob(f"{gallery_output_dir}/*.mp4"),
620
  key=os.path.getmtime,
621
- reverse=True
622
  )[:6]
623
-
624
  videos = [None] * 6
625
- for i, video_path in enumerate(existing_videos):
626
  if i < 6:
627
- videos[i] = video_path
628
-
629
  return videos
630
-
631
  gr.Markdown("---")
632
  gr.Markdown("## 🎯 Generate Your Own Quote Video")
633
-
634
  with gr.Row():
635
  with gr.Column():
636
  gr.Markdown("### 🎯 Input")
637
  niche = gr.Dropdown(
638
  choices=[
639
  "Motivation",
640
- "Business/Entrepreneurship",
641
  "Fitness",
642
  "Mindfulness",
643
  "Stoicism",
644
  "Leadership",
645
- "Love & Relationships"
646
  ],
647
  label="πŸ“‚ Select Niche",
648
- value="Motivation"
649
  )
650
-
651
  style = gr.Dropdown(
652
- choices=[
653
- "Cinematic",
654
- "Nature",
655
- "Urban",
656
- "Minimal",
657
- "Abstract"
658
- ],
659
  label="🎨 Visual Style",
660
- value="Cinematic"
661
  )
662
-
663
  num_variations = gr.Slider(
664
  minimum=1,
665
  maximum=3,
666
- value=1,
667
  step=1,
 
668
  label="🎬 Number of Video Variations",
669
- info="Generate multiple versions to choose from"
670
  )
671
-
672
- generate_btn = gr.Button("πŸ€– Run MCP Agent with Gemini", variant="primary", size="lg")
673
-
 
 
674
  with gr.Column():
675
  gr.Markdown("### πŸ“Š MCP Agent Activity Log")
676
- output = gr.Textbox(label="Agent Status", lines=20, show_label=False)
677
-
678
  with gr.Row():
679
  gr.Markdown("### ✨ Your Quote Videos")
680
-
681
  with gr.Row():
682
  video1 = gr.Video(label="Video 1", visible=True, height=500)
683
  video2 = gr.Video(label="Video 2", visible=False, height=500)
684
  video3 = gr.Video(label="Video 3", visible=False, height=500)
685
-
686
- gr.Markdown("""
 
687
  ---
688
  ### ✨ Features
689
- - 🌟 **Gemini AI** - Eliminates repetitive quotes with smart history tracking
690
- - 🎨 **Multiple Variations** - Get 1-3 different videos to choose from
691
- - ⚑ **Modal Processing** - 4-8x faster with serverless compute
692
- - πŸ€– **Real Agent** - smolagents CodeAgent orchestrates tool calls
693
- - πŸ”— **MCP Usage** - Agent wired with MCP client for external tools
694
-
695
  ### πŸ† Hackathon: MCP 1st Birthday
696
  **Track:** Track 2 - MCP in Action
697
- **Category:** Productivity Tools
698
- **Built with:** Gradio + smolagents + Gemini + OpenAI + Pexels + Modal + ElevenLabs + MCP
699
- """)
700
-
 
701
  def process_and_display(niche, style, num_variations):
702
- status, videos = mcp_agent_pipeline(niche, style, int(num_variations))
703
-
 
 
 
 
704
  v1 = videos[0] if len(videos) > 0 else None
705
  v2 = videos[1] if len(videos) > 1 else None
706
  v3 = videos[2] if len(videos) > 2 else None
707
-
708
  gallery_vids = load_gallery_videos()
709
-
710
  return [status, v1, v2, v3] + gallery_vids
711
-
712
  generate_btn.click(
713
- process_and_display,
714
- inputs=[niche, style, num_variations],
715
  outputs=[
716
- output, video1, video2, video3,
717
- gallery_video1, gallery_video2, gallery_video3,
718
- gallery_video4, gallery_video5, gallery_video6
719
- ]
 
 
 
 
 
 
 
720
  )
721
-
722
  demo.load(
723
  load_gallery_videos,
724
  outputs=[
725
- gallery_video1, gallery_video2, gallery_video3,
726
- gallery_video4, gallery_video5, gallery_video6
727
- ]
 
 
 
 
728
  )
729
 
730
  if __name__ == "__main__":
 
 
1
  import os
 
 
 
 
2
  import time
3
+ import json
4
  import shutil
5
+ import random
6
+ import tempfile
7
+ import requests
8
 
9
+ import gradio as gr
10
  from openai import OpenAI
11
  from smolagents import CodeAgent, MCPClient, tool
12
+ from huggingface_hub import InferenceClient
 
 
 
 
13
 
 
14
  from quote_generator_gemini import HybridQuoteGenerator
15
 
16
+ # -------------------------------------------------
17
+ # GLOBAL CLIENTS & CONFIG
18
+ # -------------------------------------------------
19
+
20
  openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
21
  PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
 
22
 
23
+ # Hybrid Gemini + OpenAI quote generator
24
  hybrid_quote_generator = HybridQuoteGenerator(
25
  gemini_key=os.getenv("GEMINI_API_KEY"),
26
+ openai_client=openai_client,
27
  )
28
 
29
+ # Optional MCP client (non-fatal if not installed)
30
  try:
31
  mcp_client = MCPClient("https://abidlabs-mcp-tools.hf.space")
32
  mcp_enabled = True
 
34
  print(f"MCP initialization warning: {e}")
35
  mcp_enabled = False
36
 
37
+
38
+ # -------------------------------------------------
39
  # TOOLS
40
+ # -------------------------------------------------
41
 
42
  @tool
43
  def generate_quote_tool(niche: str, style: str) -> str:
44
  """
45
+ Generate a unique inspirational quote using the HybridQuoteGenerator.
46
+
 
47
  Args:
48
+ niche: The category of the quote (e.g. Motivation, Fitness, Mindfulness).
49
+ style: The visual style or aesthetic (e.g. Cinematic, Nature, Urban).
50
+
51
  Returns:
52
+ A single quote string. If an error occurs, returns a human-readable error message.
53
  """
54
  try:
55
+ result = hybrid_quote_generator.generate_quote(
56
+ niche=niche,
57
+ style=style,
58
+ prefer_gemini=True,
59
+ )
60
+ if result.get("success"):
61
  quote = result["quote"]
62
+ source = result.get("source")
 
 
63
  if source == "gemini":
64
  stats = result.get("stats", {})
65
+ print(
66
+ f"✨ Generated with Gemini. Total quotes: "
67
+ f"{stats.get('total_quotes_generated', 0)}"
68
+ )
69
  else:
70
+ print("✨ Generated with OpenAI fallback")
 
71
  return quote
72
  else:
73
+ return f"Error generating quote: {result.get('error', 'Unknown error')}"
 
 
74
  except Exception as e:
75
  return f"Error generating quote: {str(e)}"
76
 
 
78
  @tool
79
  def search_pexels_video_tool(style: str, niche: str) -> dict:
80
  """
81
+ Search and fetch a portrait video from Pexels that matches a style and niche.
82
+
83
  Args:
84
+ style: Visual style (e.g. Cinematic, Nature, Urban, Minimal, Abstract).
85
+ niche: Content niche (e.g. Motivation, Business/Entrepreneurship, Fitness).
86
+
87
  Returns:
88
+ A dictionary with:
89
+ - success: Whether a suitable video was found.
90
+ - video_url: The direct link to the video file (or None).
91
+ - search_query: The query used to search.
92
+ - pexels_url: The Pexels page URL (or None).
93
+ - error: Optional error message on failure.
94
  """
 
 
95
  search_strategies = {
96
  "Motivation": {
97
  "Cinematic": ["person climbing mountain", "running sunrise", "achievement success"],
98
  "Nature": ["sunrise mountain peak", "ocean waves powerful", "forest light"],
99
  "Urban": ["city skyline dawn", "person running city", "urban success"],
100
  "Minimal": ["minimal motivation", "single person silhouette", "clean inspiring"],
101
+ "Abstract": ["light rays hope", "particles rising", "abstract energy"],
102
  },
103
  "Business/Entrepreneurship": {
104
  "Cinematic": ["business cityscape", "office modern", "handshake deal"],
105
  "Nature": ["growth plant", "river flowing", "sunrise new beginning"],
106
  "Urban": ["city business", "office skyline", "modern workspace"],
107
  "Minimal": ["desk minimal", "workspace clean", "simple office"],
108
+ "Abstract": ["network connections", "growth chart", "abstract progress"],
109
  },
110
  "Fitness": {
111
  "Cinematic": ["athlete training", "gym workout", "running outdoor"],
112
  "Nature": ["outdoor workout", "mountain hiking", "beach running"],
113
  "Urban": ["city running", "urban fitness", "street workout"],
114
  "Minimal": ["gym minimal", "simple workout", "clean fitness"],
115
+ "Abstract": ["energy motion", "strength power", "dynamic movement"],
116
  },
117
  "Mindfulness": {
118
  "Cinematic": ["meditation sunset", "peaceful landscape", "calm water"],
119
  "Nature": ["forest peaceful", "calm lake", "zen garden"],
120
  "Urban": ["city peaceful morning", "quiet street", "urban calm"],
121
  "Minimal": ["minimal zen", "simple meditation", "clean peaceful"],
122
+ "Abstract": ["calm waves", "gentle motion", "soft particles"],
123
  },
124
  "Stoicism": {
125
  "Cinematic": ["ancient architecture", "statue philosopher", "timeless landscape"],
126
  "Nature": ["mountain strong", "oak tree", "stone nature"],
127
  "Urban": ["classical building", "statue city", "ancient modern"],
128
  "Minimal": ["stone minimal", "simple strong", "pillar minimal"],
129
+ "Abstract": ["marble texture", "stone abstract", "timeless pattern"],
130
  },
131
  "Leadership": {
132
  "Cinematic": ["team meeting", "leader speaking", "group collaboration"],
133
  "Nature": ["eagle flying", "lion pride", "mountain top"],
134
  "Urban": ["office leadership", "boardroom", "city leadership"],
135
  "Minimal": ["chess pieces", "simple leadership", "clean professional"],
136
+ "Abstract": ["network leader", "connection points", "guiding light"],
137
  },
138
  "Love & Relationships": {
139
  "Cinematic": ["couple sunset", "romance beautiful", "love cinematic"],
140
  "Nature": ["couple nature", "romantic sunset", "peaceful together"],
141
  "Urban": ["couple city", "romance urban", "love city lights"],
142
  "Minimal": ["hands holding", "simple love", "minimal romance"],
143
+ "Abstract": ["hearts flowing", "love particles", "connection abstract"],
144
+ },
145
  }
146
+
 
147
  queries = search_strategies.get(niche, {}).get(style, ["aesthetic nature"])
148
+
149
  try:
150
+ headers = {"Authorization": PEXELS_API_KEY} if PEXELS_API_KEY else {}
 
 
151
  query = random.choice(queries)
152
+
153
+ url = (
154
+ f"https://api.pexels.com/videos/search"
155
+ f"?query={query}&per_page=15&orientation=portrait"
156
+ )
157
  response = requests.get(url, headers=headers)
158
  data = response.json()
159
+
160
  if "videos" in data and len(data["videos"]) > 0:
 
161
  video = random.choice(data["videos"][:10])
162
  video_files = video.get("video_files", [])
163
+
164
+ portrait_videos = [
165
+ vf
166
+ for vf in video_files
167
+ if vf.get("width", 0) < vf.get("height", 0)
168
+ ]
169
+
170
  if portrait_videos:
171
  selected = random.choice(portrait_videos)
172
  return {
173
+ "success": True,
174
  "video_url": selected.get("link"),
175
  "search_query": query,
176
  "pexels_url": video.get("url"),
 
177
  }
178
+
 
179
  if video_files:
180
  return {
181
+ "success": True,
182
  "video_url": video_files[0].get("link"),
183
  "search_query": query,
184
  "pexels_url": video.get("url"),
 
185
  }
186
+
187
  return {
188
+ "success": False,
189
  "video_url": None,
190
  "search_query": query,
191
  "pexels_url": None,
192
+ "error": "No suitable videos found",
 
193
  }
194
+
195
  except Exception as e:
196
  return {
197
+ "success": False,
198
  "video_url": None,
199
  "search_query": "",
200
  "pexels_url": None,
201
+ "error": str(e),
 
202
  }
203
 
204
 
205
  @tool
206
+ def create_quote_video_tool(video_url: str, quote_text: str, output_path: str) -> dict:
207
  """
208
+ Create a quote video by calling a Modal endpoint that overlays text on a background video.
209
+
210
+ Args:
211
+ video_url: Direct URL of the background video (e.g. from Pexels).
212
+ quote_text: The quote text to be overlaid on the video.
213
+ output_path: Local file path where the resulting video should be saved.
214
+
215
+ Returns:
216
+ A dictionary with:
217
+ - success: Whether the generation succeeded.
218
+ - output_path: The saved video path on disk (or None).
219
+ - message: A human-readable status message.
220
  """
221
+ modal_endpoint = os.getenv("MODAL_ENDPOINT_URL")
222
+
223
+ if not modal_endpoint:
224
+ print("ℹ️ MODAL_ENDPOINT_URL not configured, cannot generate video.")
225
+ return {
226
+ "success": False,
227
+ "output_path": None,
228
+ "message": (
229
+ "Modal endpoint not configured. Set MODAL_ENDPOINT_URL to use remote "
230
+ "video generation (modal deploy modal_video_processing.py)."
231
+ ),
232
+ }
233
+
234
  try:
235
+ print("πŸš€ Processing on Modal (fast!)...")
236
+ response = requests.post(
237
+ modal_endpoint,
238
+ json={
239
+ "video_url": video_url,
240
+ "quote_text": quote_text,
241
+ },
242
+ timeout=120,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  )
244
+
245
+ if response.status_code != 200:
246
+ return {
247
+ "success": False,
248
+ "output_path": None,
249
+ "message": f"Modal HTTP error: {response.status_code}",
250
+ }
251
+
252
+ result = response.json()
253
+ if not result.get("success"):
254
+ return {
255
+ "success": False,
256
+ "output_path": None,
257
+ "message": f"Modal error: {result.get('error', 'Unknown error')}",
258
+ }
259
+
260
+ import base64
261
+
262
+ video_b64 = result["video"]
263
+ video_bytes = base64.b64decode(video_b64)
264
+
265
+ with open(output_path, "wb") as f:
266
+ f.write(video_bytes)
267
+
268
+ size_mb = result.get("size_mb", len(video_bytes) / 1024 / 1024)
269
+ print(f"βœ… Modal processing complete! {size_mb:.2f}MB")
270
+
271
  return {
272
  "success": True,
273
  "output_path": output_path,
274
+ "message": f"Video created via Modal (~{size_mb:.2f}MB).",
 
275
  }
276
+
277
  except Exception as e:
278
  return {
279
  "success": False,
280
  "output_path": None,
281
+ "message": f"Error calling Modal: {str(e)}",
 
282
  }
283
 
284
 
285
+ # -------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  # AGENT INITIALIZATION
287
+ # -------------------------------------------------
288
 
289
  def initialize_agent():
290
+ """Initialize the CodeAgent with optional MCP client."""
291
  try:
 
292
  hf_token = os.getenv("HF_TOKEN")
293
+ model = InferenceClient(token=hf_token)
294
+
 
 
 
 
 
 
 
295
  agent = CodeAgent(
296
+ tools=[generate_quote_tool, search_pexels_video_tool, create_quote_video_tool],
 
 
 
 
 
297
  model=model,
298
  additional_authorized_imports=[
 
 
 
 
299
  "os",
300
+ "time",
301
  "json",
302
+ "random",
303
+ "tempfile",
304
+ "requests",
305
  ],
306
  max_steps=15,
307
  )
308
+
309
  if mcp_enabled:
310
  agent.mcp_clients = [mcp_client]
311
+
312
  return agent, None
313
  except Exception as e:
314
  return None, f"Agent initialization error: {str(e)}"
315
 
316
+
317
  agent, agent_error = initialize_agent()
318
 
319
+
320
+ # -------------------------------------------------
321
  # PIPELINES
322
+ # -------------------------------------------------
323
 
324
+ def mcp_agent_pipeline(niche: str, style: str, num_variations: int = 1):
325
  """
326
  MAIN PIPELINE: uses smolagents CodeAgent.run to plan & call tools.
327
+
328
  The agent:
329
  - calls generate_quote_tool
330
  - calls search_pexels_video_tool multiple times
 
332
  - returns JSON with status_log + video_paths
333
  """
334
  base_log = ["πŸ€– **MCP AGENT RUN**"]
335
+
336
  if agent_error or agent is None:
337
  base_log.append(f"❌ Agent initialization failed: {agent_error}")
338
  base_log.append("πŸ”„ Falling back to direct tool pipeline...")
339
  status, vids = fallback_pipeline(niche, style, num_variations)
340
  return "\n".join(base_log + [status]), vids
341
+
342
  try:
343
  output_dir = "/tmp/quote_videos"
344
  gallery_dir = "/data/gallery_videos"
345
  os.makedirs(output_dir, exist_ok=True)
346
  os.makedirs(gallery_dir, exist_ok=True)
347
+
348
  timestamp = int(time.time())
349
+ base_prefix = os.path.join(output_dir, f"agent_{timestamp}_v")
350
+
351
  user_task = f"""
352
  You are an autonomous Python agent helping creators generate short vertical quote videos.
353
 
 
355
  Style: {style}
356
  Number of variations: {num_variations}
357
 
358
+ You have these TOOLS available:
359
 
360
  1. generate_quote_tool(niche: str, style: str) -> str
361
+ - Returns a single quote as plain text.
362
 
363
  2. search_pexels_video_tool(style: str, niche: str) -> dict
364
+ - Returns a dict with:
 
365
  - "success": bool
366
+ - "video_url": str or None
367
 
368
+ 3. create_quote_video_tool(video_url: str, quote_text: str, output_path: str) -> dict
369
+ - Writes a video file to output_path and returns a dict with:
 
370
  - "success": bool
371
+ - "output_path": str or None
 
 
372
 
373
  Your job:
374
 
375
+ 1. Call generate_quote_tool once to obtain quote_text.
376
+ 2. For each variation i from 1 to {num_variations}:
377
+ - Call search_pexels_video_tool(style, niche).
378
+ - If it succeeds, compute output_path exactly as:
379
+ "{base_prefix}{{i}}.mp4"
380
+ - Call create_quote_video_tool(video_url, quote_text, output_path).
381
+ 3. Collect only variations where create_quote_video_tool returns success == True and a non-empty output_path.
382
+ 4. Build a human-readable status_log string summarizing:
383
+ - Which tools you called
384
+ - How many videos succeeded or failed
385
+ 5. Return ONLY a valid JSON object of the form:
386
 
387
  {{
388
  "status_log": "multi-line human readable description of what you did",
389
  "video_paths": [
390
  "{base_prefix}1.mp4",
391
+ "... only paths that actually succeeded ..."
392
  ]
393
  }}
394
 
395
  CRITICAL:
396
+ - Do NOT wrap the JSON in markdown or backticks.
397
+ - Do NOT add extra keys.
398
+ - Do NOT print anything except the JSON object as your final answer.
399
  """
400
+
401
  agent_result = agent.run(user_task)
402
+
403
  try:
404
  parsed = json.loads(agent_result)
405
  except Exception as parse_err:
406
+ raise ValueError(
407
+ f"Agent output was not valid JSON: {parse_err}\n"
408
+ f"Raw agent output (first 500 chars): {agent_result[:500]}"
409
+ )
410
+
411
  status_log = parsed.get("status_log", "")
412
  video_paths = parsed.get("video_paths", [])
413
+
 
414
  valid_paths = [
415
+ p for p in video_paths if isinstance(p, str) and os.path.exists(p)
 
416
  ]
417
+
418
  if not valid_paths:
419
+ raise ValueError("Agent returned no valid video paths or files not found.")
420
+
 
421
  for idx, path in enumerate(valid_paths):
422
  try:
423
  filename = os.path.basename(path)
424
+ gallery_path = os.path.join(
425
+ gallery_dir,
426
+ f"gallery_{timestamp}_v{idx+1}_{filename}",
427
+ )
428
  shutil.copy2(path, gallery_path)
429
  except Exception as e:
430
  print(f"⚠️ Failed to copy to gallery for {path}: {e}")
431
+
432
  full_status = "\n".join(base_log + [status_log])
433
  return full_status, valid_paths[:3]
434
+
435
  except Exception as e:
 
436
  fallback_status, fallback_videos = fallback_pipeline(niche, style, num_variations)
437
  combined_status = "\n".join(
438
  base_log
 
441
  return combined_status, fallback_videos
442
 
443
 
444
+ def fallback_pipeline(niche: str, style: str, num_variations: int = 1):
445
+ """Fallback pipeline: direct tool calls without agent planning."""
446
  status_log = []
447
  status_log.append("πŸ”„ **FALLBACK MODE (Direct Tool Execution)**\n")
448
+
449
+ status_log.append("🧠 Generating quote with HybridQuoteGenerator...")
 
450
  quote = generate_quote_tool(niche, style)
451
+
452
+ if isinstance(quote, str) and quote.startswith("Error generating quote"):
453
  return "\n".join(status_log) + f"\n❌ {quote}", []
454
+
455
  status_log.append(" βœ… Quote generated\n")
456
+
 
457
  status_log.append(f"πŸ” Searching for {num_variations} videos...")
458
  video_results = []
459
+ for _ in range(num_variations):
460
+ vr = search_pexels_video_tool(style, niche)
461
+ if vr.get("success"):
462
+ video_results.append(vr)
463
+
464
  if not video_results:
465
+ status_log.append("❌ No videos found\n")
466
+ return "\n".join(status_log), []
467
+
468
  status_log.append(f" βœ… Found {len(video_results)} videos\n")
469
+
470
+ status_log.append("🎬 Creating videos via Modal...")
 
471
  output_dir = "/tmp/quote_videos"
472
  gallery_dir = "/data/gallery_videos"
473
  os.makedirs(output_dir, exist_ok=True)
474
  os.makedirs(gallery_dir, exist_ok=True)
475
+
476
  timestamp = int(time.time())
477
  created_videos = []
478
+
479
+ for i, vr in enumerate(video_results):
480
  output_filename = f"quote_video_v{i+1}_{timestamp}.mp4"
481
  output_path = os.path.join(output_dir, output_filename)
482
+
483
  creation_result = create_quote_video_tool(
484
+ video_url=vr["video_url"],
485
+ quote_text=quote,
486
+ output_path=output_path,
 
487
  )
488
+
489
+ if creation_result.get("success"):
490
  created_videos.append(creation_result["output_path"])
491
+
492
  gallery_filename = f"gallery_{timestamp}_v{i+1}.mp4"
493
  gallery_path = os.path.join(gallery_dir, gallery_filename)
494
  try:
 
498
  else:
499
  error_msg = creation_result.get("message", "Unknown error")
500
  status_log.append(f" ❌ Video {i+1} error: {error_msg}")
501
+
502
  if not created_videos:
503
+ status_log.append("❌ Video creation failed\n")
504
+ return "\n".join(status_log), []
505
+
506
  status_log.append(f" βœ… Created {len(created_videos)} videos!\n")
507
  status_log.append("🎬 **COMPLETE!**")
508
+
509
  return "\n".join(status_log), created_videos
510
 
 
 
 
511
 
512
+ # -------------------------------------------------
513
+ # GRADIO UI
514
+ # -------------------------------------------------
515
+
516
+ with gr.Blocks(
517
+ title="AIQuoteClipGenerator - MCP + Gemini Edition",
518
+ theme=gr.themes.Soft(),
519
+ ) as demo:
520
+ gr.Markdown(
521
+ """
522
  # 🎬 AIQuoteClipGenerator
523
  ### MCP-Powered with Gemini AI
524
+
525
  **Key Features:**
526
+ - 🌟 **Gemini AI** with quote-history to avoid repetition
527
+ - πŸ€– **smolagents CodeAgent** for planning & tool-use
528
+ - πŸ”— **MCP Client Ready** (uses external MCP tools if available)
529
+ - πŸŽ₯ **Modal** for fast video rendering
530
+ - 🎨 Generate multiple vertical quote video variations
531
+ """
532
+ )
533
+
 
534
  with gr.Accordion("πŸ“Έ Example Gallery - Recent Videos", open=True):
535
+ gr.Markdown(
536
+ "See what others (or you) have generated. Auto-updates after each run."
537
+ )
538
+
539
  with gr.Row():
540
+ gallery_video1 = gr.Video(height=300, show_label=False, interactive=False)
541
+ gallery_video2 = gr.Video(height=300, show_label=False, interactive=False)
542
+ gallery_video3 = gr.Video(height=300, show_label=False, interactive=False)
543
+
544
  with gr.Row():
545
+ gallery_video4 = gr.Video(height=300, show_label=False, interactive=False)
546
+ gallery_video5 = gr.Video(height=300, show_label=False, interactive=False)
547
+ gallery_video6 = gr.Video(height=300, show_label=False, interactive=False)
548
+
549
  def load_gallery_videos():
550
  gallery_output_dir = "/data/gallery_videos"
551
  os.makedirs(gallery_output_dir, exist_ok=True)
552
+
553
  import glob
554
+
555
  existing_videos = sorted(
556
+ glob.glob(os.path.join(gallery_output_dir, "*.mp4")),
557
  key=os.path.getmtime,
558
+ reverse=True,
559
  )[:6]
560
+
561
  videos = [None] * 6
562
+ for i, path in enumerate(existing_videos):
563
  if i < 6:
564
+ videos[i] = path
 
565
  return videos
566
+
567
  gr.Markdown("---")
568
  gr.Markdown("## 🎯 Generate Your Own Quote Video")
569
+
570
  with gr.Row():
571
  with gr.Column():
572
  gr.Markdown("### 🎯 Input")
573
  niche = gr.Dropdown(
574
  choices=[
575
  "Motivation",
576
+ "Business/Entrepreneurship",
577
  "Fitness",
578
  "Mindfulness",
579
  "Stoicism",
580
  "Leadership",
581
+ "Love & Relationships",
582
  ],
583
  label="πŸ“‚ Select Niche",
584
+ value="Motivation",
585
  )
586
+
587
  style = gr.Dropdown(
588
+ choices=["Cinematic", "Nature", "Urban", "Minimal", "Abstract"],
 
 
 
 
 
 
589
  label="🎨 Visual Style",
590
+ value="Cinematic",
591
  )
592
+
593
  num_variations = gr.Slider(
594
  minimum=1,
595
  maximum=3,
 
596
  step=1,
597
+ value=1,
598
  label="🎬 Number of Video Variations",
599
+ info="Generate multiple versions to choose from",
600
  )
601
+
602
+ generate_btn = gr.Button(
603
+ "πŸ€– Run MCP Agent with Gemini", variant="primary", size="lg"
604
+ )
605
+
606
  with gr.Column():
607
  gr.Markdown("### πŸ“Š MCP Agent Activity Log")
608
+ output = gr.Textbox(lines=20, show_label=False)
609
+
610
  with gr.Row():
611
  gr.Markdown("### ✨ Your Quote Videos")
612
+
613
  with gr.Row():
614
  video1 = gr.Video(label="Video 1", visible=True, height=500)
615
  video2 = gr.Video(label="Video 2", visible=False, height=500)
616
  video3 = gr.Video(label="Video 3", visible=False, height=500)
617
+
618
+ gr.Markdown(
619
+ """
620
  ---
621
  ### ✨ Features
622
+ - 🌟 **Gemini-powered** quote variety (history-aware)
623
+ - 🎨 Multiple aesthetic video variations
624
+ - ⚑ **Modal**-accelerated rendering
625
+ - πŸ€– **smolagents** CodeAgent for autonomous tool-calling
626
+ - πŸ”— Optional MCP integration via MCPClient
627
+
628
  ### πŸ† Hackathon: MCP 1st Birthday
629
  **Track:** Track 2 - MCP in Action
630
+ **Category:** Productivity / Creator Tools
631
+ **Stack:** Gradio Β· smolagents Β· Gemini Β· OpenAI Β· Pexels Β· Modal Β· MCP
632
+ """
633
+ )
634
+
635
  def process_and_display(niche, style, num_variations):
636
+ status, videos = mcp_agent_pipeline(
637
+ niche=str(niche),
638
+ style=str(style),
639
+ num_variations=int(num_variations),
640
+ )
641
+
642
  v1 = videos[0] if len(videos) > 0 else None
643
  v2 = videos[1] if len(videos) > 1 else None
644
  v3 = videos[2] if len(videos) > 2 else None
645
+
646
  gallery_vids = load_gallery_videos()
647
+
648
  return [status, v1, v2, v3] + gallery_vids
649
+
650
  generate_btn.click(
651
+ process_and_display,
652
+ inputs=[niche, style, num_variations],
653
  outputs=[
654
+ output,
655
+ video1,
656
+ video2,
657
+ video3,
658
+ gallery_video1,
659
+ gallery_video2,
660
+ gallery_video3,
661
+ gallery_video4,
662
+ gallery_video5,
663
+ gallery_video6,
664
+ ],
665
  )
666
+
667
  demo.load(
668
  load_gallery_videos,
669
  outputs=[
670
+ gallery_video1,
671
+ gallery_video2,
672
+ gallery_video3,
673
+ gallery_video4,
674
+ gallery_video5,
675
+ gallery_video6,
676
+ ],
677
  )
678
 
679
  if __name__ == "__main__":