fix error
Browse files
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
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 27 |
hybrid_quote_generator = HybridQuoteGenerator(
|
| 28 |
gemini_key=os.getenv("GEMINI_API_KEY"),
|
| 29 |
-
openai_client=openai_client
|
| 30 |
)
|
| 31 |
|
| 32 |
-
#
|
| 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
|
| 48 |
-
|
| 49 |
-
|
| 50 |
Args:
|
| 51 |
-
niche: The category of quote (Motivation,
|
| 52 |
-
style: The visual style (Cinematic, Nature, Urban
|
| 53 |
-
|
| 54 |
Returns:
|
| 55 |
-
A
|
| 56 |
"""
|
| 57 |
try:
|
| 58 |
-
result = hybrid_quote_generator.generate_quote(
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
quote = result["quote"]
|
| 62 |
-
source = result
|
| 63 |
-
|
| 64 |
-
# Log which generator was used
|
| 65 |
if source == "gemini":
|
| 66 |
stats = result.get("stats", {})
|
| 67 |
-
print(
|
|
|
|
|
|
|
|
|
|
| 68 |
else:
|
| 69 |
-
print(
|
| 70 |
-
|
| 71 |
return quote
|
| 72 |
else:
|
| 73 |
-
|
| 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
|
| 84 |
-
|
| 85 |
Args:
|
| 86 |
-
style: Visual style (Cinematic, Nature, Urban, Minimal, Abstract)
|
| 87 |
-
niche: Content niche (Motivation, Business, Fitness
|
| 88 |
-
|
| 89 |
Returns:
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 165 |
-
|
| 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 |
-
"
|
| 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 |
-
"
|
| 199 |
-
"error": str(e)
|
| 200 |
}
|
| 201 |
|
| 202 |
|
| 203 |
@tool
|
| 204 |
-
def
|
| 205 |
"""
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
try:
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
"
|
| 216 |
-
|
| 217 |
-
|
| 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 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
return {
|
| 259 |
"success": True,
|
| 260 |
"output_path": output_path,
|
| 261 |
-
"
|
| 262 |
-
"message": "Voice commentary created!"
|
| 263 |
}
|
| 264 |
-
|
| 265 |
except Exception as e:
|
| 266 |
return {
|
| 267 |
"success": False,
|
| 268 |
"output_path": None,
|
| 269 |
-
"
|
| 270 |
-
"message": f"Error creating commentary: {str(e)}"
|
| 271 |
}
|
| 272 |
|
| 273 |
|
| 274 |
-
|
| 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
|
| 351 |
try:
|
| 352 |
-
# Use Hugging Face Inference API for the agent's LLM
|
| 353 |
hf_token = os.getenv("HF_TOKEN")
|
| 354 |
-
|
| 355 |
-
|
| 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 |
-
"
|
| 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"
|
| 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
|
| 430 |
|
| 431 |
1. generate_quote_tool(niche: str, style: str) -> str
|
| 432 |
-
- Returns a
|
| 433 |
|
| 434 |
2. search_pexels_video_tool(style: str, niche: str) -> dict
|
| 435 |
-
- Returns a dict with
|
| 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
|
| 440 |
-
-
|
| 441 |
-
- Returns a dict with at least:
|
| 442 |
- "success": bool
|
| 443 |
-
- "output_path": str
|
| 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
|
| 450 |
-
2. For each variation i from 1 to {num_variations}
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
| 457 |
|
| 458 |
{{
|
| 459 |
"status_log": "multi-line human readable description of what you did",
|
| 460 |
"video_paths": [
|
| 461 |
"{base_prefix}1.mp4",
|
| 462 |
-
"... only
|
| 463 |
]
|
| 464 |
}}
|
| 465 |
|
| 466 |
CRITICAL:
|
| 467 |
-
- Do
|
| 468 |
-
- Do
|
| 469 |
-
- Do
|
| 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(
|
| 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
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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
|
| 514 |
status_log = []
|
| 515 |
status_log.append("π **FALLBACK MODE (Direct Tool Execution)**\n")
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
status_log.append("π§ Generating quote with Gemini...")
|
| 519 |
quote = generate_quote_tool(niche, style)
|
| 520 |
-
|
| 521 |
-
if "Error
|
| 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
|
| 530 |
-
|
| 531 |
-
if
|
| 532 |
-
video_results.append(
|
| 533 |
-
|
| 534 |
if not video_results:
|
| 535 |
-
|
| 536 |
-
|
|
|
|
| 537 |
status_log.append(f" β
Found {len(video_results)} videos\n")
|
| 538 |
-
|
| 539 |
-
|
| 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,
|
| 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 |
-
|
| 555 |
-
quote,
|
| 556 |
-
output_path,
|
| 557 |
-
None
|
| 558 |
)
|
| 559 |
-
|
| 560 |
-
if creation_result
|
| 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 |
-
|
| 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 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
# π¬ AIQuoteClipGenerator
|
| 588 |
### MCP-Powered with Gemini AI
|
| 589 |
-
|
| 590 |
**Key Features:**
|
| 591 |
-
- π **Gemini AI
|
| 592 |
-
-
|
| 593 |
-
-
|
| 594 |
-
-
|
| 595 |
-
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
# Example Gallery
|
| 600 |
with gr.Accordion("πΈ Example Gallery - Recent Videos", open=True):
|
| 601 |
-
gr.Markdown(
|
| 602 |
-
|
|
|
|
|
|
|
| 603 |
with gr.Row():
|
| 604 |
-
gallery_video1 = gr.Video(
|
| 605 |
-
gallery_video2 = gr.Video(
|
| 606 |
-
gallery_video3 = gr.Video(
|
| 607 |
-
|
| 608 |
with gr.Row():
|
| 609 |
-
gallery_video4 = gr.Video(
|
| 610 |
-
gallery_video5 = gr.Video(
|
| 611 |
-
gallery_video6 = gr.Video(
|
| 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(
|
| 620 |
key=os.path.getmtime,
|
| 621 |
-
reverse=True
|
| 622 |
)[:6]
|
| 623 |
-
|
| 624 |
videos = [None] * 6
|
| 625 |
-
for i,
|
| 626 |
if i < 6:
|
| 627 |
-
videos[i] =
|
| 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(
|
| 673 |
-
|
|
|
|
|
|
|
| 674 |
with gr.Column():
|
| 675 |
gr.Markdown("### π MCP Agent Activity Log")
|
| 676 |
-
output = gr.Textbox(
|
| 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
|
| 690 |
-
- π¨
|
| 691 |
-
- β‘ **Modal
|
| 692 |
-
- π€ **
|
| 693 |
-
- π
|
| 694 |
-
|
| 695 |
### π Hackathon: MCP 1st Birthday
|
| 696 |
**Track:** Track 2 - MCP in Action
|
| 697 |
-
**Category:** Productivity Tools
|
| 698 |
-
**
|
| 699 |
-
"""
|
| 700 |
-
|
|
|
|
| 701 |
def process_and_display(niche, style, num_variations):
|
| 702 |
-
status, videos = mcp_agent_pipeline(
|
| 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,
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
)
|
| 721 |
-
|
| 722 |
demo.load(
|
| 723 |
load_gallery_videos,
|
| 724 |
outputs=[
|
| 725 |
-
gallery_video1,
|
| 726 |
-
|
| 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__":
|