#!/usr/bin/env python3 """ Theorem Explanation Agent - Hugging Face Spaces App Generates educational videos using Gemini 2.0 Flash and Manim """ import os import sys import asyncio import time import random from typing import Dict, Any, Tuple, Optional from pathlib import Path import gradio as gr # Environment setup DEMO_MODE = os.getenv("DEMO_MODE", "false").lower() == "true" video_generator = None CAN_IMPORT_DEPENDENCIES = True GRADIO_OUTPUT_DIR = "gradio_outputs" def setup_environment(): """Setup environment for HF Spaces.""" print("š Setting up Theorem Explanation Agent...") # Create output directory os.makedirs(GRADIO_OUTPUT_DIR, exist_ok=True) gemini_keys = os.getenv("GEMINI_API_KEY", "") if gemini_keys: key_count = len([k.strip() for k in gemini_keys.split(',') if k.strip()]) print(f"ā Found {key_count} Gemini API key(s)") return True else: print("ā ļø No Gemini API keys found - running in demo mode") return False def initialize_video_generator(): """Initialize video generator with proper dependencies.""" global video_generator, CAN_IMPORT_DEPENDENCIES try: if DEMO_MODE: return "ā ļø Demo mode enabled - No video generation" gemini_keys = os.getenv("GEMINI_API_KEY", "") if not gemini_keys: return "ā ļø No API keys found - Set GEMINI_API_KEY environment variable" # Import dependencies try: from generate_video import VideoGenerator from mllm_tools.litellm import LiteLLMWrapper print("ā Successfully imported video generation dependencies") except ImportError as e: CAN_IMPORT_DEPENDENCIES = False print(f"ā Import error: {e}") return f"ā ļø Missing dependencies: {str(e)}" # Initialize models with comma-separated API key support planner_model = LiteLLMWrapper( model_name="gemini/gemini-2.0-flash-exp", temperature=0.7, print_cost=True, verbose=False, use_langfuse=False ) # Initialize video generator video_generator = VideoGenerator( planner_model=planner_model, helper_model=planner_model, scene_model=planner_model, output_dir=GRADIO_OUTPUT_DIR, use_rag=False, use_context_learning=False, use_visual_fix_code=False, verbose=True ) return "ā Video generator initialized successfully" except Exception as e: CAN_IMPORT_DEPENDENCIES = False print(f"ā Error initializing video generator: {e}") return f"ā Initialization failed: {str(e)}" def simulate_video_generation(topic: str, context: str, max_scenes: int, progress_callback=None): """Simulate video generation for demo mode.""" stages = [ ("š Analyzing topic", 15), ("š Planning scenes", 30), ("š¬ Generating content", 50), ("⨠Creating animations", 75), ("š„ Rendering video", 90), ("ā Finalizing", 100) ] results = [] for stage, progress in stages: if progress_callback: progress_callback(progress, stage) time.sleep(random.uniform(0.5, 1.0)) results.append(f"⢠{stage}") return { "success": True, "message": f"Demo simulation completed for: {topic}", "scenes_created": max_scenes, "processing_steps": results, "demo_note": "This is a simulation - set GEMINI_API_KEY and DEMO_MODE=false for real generation" } async def generate_video_async(topic: str, context: str, max_scenes: int, progress_callback=None): """Generate video asynchronously using the actual VideoGenerator.""" global video_generator if not topic.strip(): return {"success": False, "error": "Please enter a topic"} try: if DEMO_MODE or not CAN_IMPORT_DEPENDENCIES or video_generator is None: return simulate_video_generation(topic, context, max_scenes, progress_callback) if progress_callback: progress_callback(10, "š Starting video generation...") # Use the actual video generation pipeline result = await video_generator.generate_video_pipeline( topic=topic, description=context or f"Educational video about {topic}", max_retries=3, only_plan=False, specific_scenes=list(range(1, max_scenes + 1)) if max_scenes > 0 else None ) if progress_callback: progress_callback(100, "ā Video generation completed!") # Check for generated video files file_prefix = topic.lower().replace(' ', '_') file_prefix = ''.join(c for c in file_prefix if c.isalnum() or c == '_') output_folder = os.path.join(GRADIO_OUTPUT_DIR, file_prefix) video_files = [] if os.path.exists(output_folder): # Look for combined video combined_video = os.path.join(output_folder, f"{file_prefix}_combined.mp4") if os.path.exists(combined_video): video_files.append(combined_video) # Look for individual scene videos for i in range(1, max_scenes + 1): scene_video = os.path.join(output_folder, f"scene{i}", f"{file_prefix}_scene{i}.mp4") if os.path.exists(scene_video): video_files.append(scene_video) return { "success": True, "message": f"Video generated successfully for: {topic}", "video_files": video_files, "output_folder": output_folder, "result": result } except Exception as e: print(f"ā Error in video generation: {e}") return {"success": False, "error": str(e)} def generate_video_gradio(topic: str, context: str, max_scenes: int, progress=gr.Progress()) -> Tuple[str, str, Optional[str]]: """Main Gradio function that handles video generation and returns results.""" def progress_callback(percent, message): progress(percent / 100, desc=message) # Create new event loop for this generation loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: result = loop.run_until_complete( generate_video_async(topic, context, max_scenes, progress_callback) ) finally: loop.close() if result["success"]: output = f"""# š Video Generation Complete! **Topic:** {topic} **Context:** {context if context else "None"} **Scenes:** {max_scenes} ## ā Result {result["message"]} """ # Add processing steps if available if "processing_steps" in result: output += "\n## š Processing Steps\n" for step in result["processing_steps"]: output += f"{step}\n" # Add demo note if in demo mode if "demo_note" in result: output += f"\nā ļø **{result['demo_note']}**" # Add video file information video_path = None if "video_files" in result and result["video_files"]: output += f"\n## š„ Generated Videos\n" for video_file in result["video_files"]: output += f"⢠{os.path.basename(video_file)}\n" video_path = result["video_files"][0] # Return first video for display elif "output_folder" in result: output += f"\nš **Output folder:** {result['output_folder']}\n" status = "š® Demo completed" if DEMO_MODE else "ā Generation completed" return output, status, video_path else: error_output = f"""# ā Video Generation Failed **Error:** {result.get("error", "Unknown error")} ## š” Troubleshooting Tips 1. **Check API Keys:** Ensure GEMINI_API_KEY is set with valid keys 2. **Topic Clarity:** Use specific, educational topics 3. **Dependencies:** Make sure all required packages are installed 4. **Demo Mode:** Set DEMO_MODE=false for real generation ## š§ Environment Setup ```bash export GEMINI_API_KEY="your-key-1,your-key-2,your-key-3" export DEMO_MODE=false ``` """ return error_output, "ā Generation failed", None def get_examples(): """Educational example topics.""" return [ ["Pythagorean Theorem", "Mathematical proof with geometric visualization"], ["Newton's Second Law", "F=ma with real-world examples and demonstrations"], ["Derivatives in Calculus", "Rate of change with graphical interpretation"], ["Photosynthesis Process", "Cellular process with chemical equations"], ["Wave-Particle Duality", "Quantum physics concept with experiments"], ["Quadratic Formula", "Step-by-step derivation and applications"], ["DNA Replication", "Biological process with molecular details"], ["Ohm's Law", "Electrical relationship with circuit examples"] ] # Initialize the system has_api_keys = setup_environment() init_status = initialize_video_generator() # Create Gradio interface with gr.Blocks( title="š Theorem Explanation Agent", theme=gr.themes.Soft(), css="footer {visibility: hidden}" ) as demo: gr.HTML("""
Generate Educational Videos with AI
Powered by Gemini 2.0 Flash & Manim
Status: {"ā API keys configured" if has_api_keys else "ā ļø No API keys found"}
{"Ready for video generation" if has_api_keys else "Running in demo mode"}
To generate actual videos instead of simulations:
GEMINI_API_KEY=your-gemini-api-keyGEMINI_API_KEY=key1,key2,key3,key4DEMO_MODE=false
Multiple API keys enable automatic failover and load distribution across different billing accounts.