#!/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("""

šŸŽ“ Theorem Explanation Agent

Generate Educational Videos with AI

Powered by Gemini 2.0 Flash & Manim

""") # Status and setup information with gr.Row(): with gr.Column(): gr.HTML(f"""

šŸ” API Setup Status

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"}

""") with gr.Column(): system_status = gr.Textbox( label="šŸ”§ System Status", value=init_status, interactive=False, lines=2 ) # API Configuration Help if not has_api_keys or DEMO_MODE: gr.HTML("""

šŸš€ Enable Full Functionality

To generate actual videos instead of simulations:

Single API Key:
GEMINI_API_KEY=your-gemini-api-key

Multiple Keys (Recommended):
GEMINI_API_KEY=key1,key2,key3,key4

Disable Demo Mode:
DEMO_MODE=false

Multiple API keys enable automatic failover and load distribution across different billing accounts.

""") # Main interface with gr.Row(): with gr.Column(scale=2): topic_input = gr.Textbox( label="šŸ“š Educational Topic", placeholder="e.g., Pythagorean Theorem, Newton's Laws, Derivatives...", lines=1 ) context_input = gr.Textbox( label="šŸ“ Additional Context (Optional)", placeholder="Specify focus areas, target audience, or particular aspects to emphasize...", lines=3 ) max_scenes_slider = gr.Slider( label="šŸŽ¬ Maximum Scenes", minimum=1, maximum=6, value=3, step=1, info="More scenes = longer videos but more API usage" ) generate_btn = gr.Button("šŸš€ Generate Educational Video", variant="primary", size="lg") with gr.Column(scale=1): gr.HTML("""

šŸ’” Tips for Best Results

""") # Examples examples = gr.Examples( examples=get_examples(), inputs=[topic_input, context_input], label="šŸ“– Example Topics" ) # Output section with gr.Row(): with gr.Column(scale=2): output_display = gr.Markdown( value="šŸ‘‹ **Ready to generate!** Enter an educational topic above and click 'Generate Educational Video' to begin.", label="šŸ“‹ Generation Results" ) with gr.Column(scale=1): video_output = gr.Video( label="šŸŽ„ Generated Video", visible=True ) # Wire up the interface generate_btn.click( fn=generate_video_gradio, inputs=[topic_input, context_input, max_scenes_slider], outputs=[output_display, system_status, video_output], show_progress=True ) # Launch configuration if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, share=False, show_error=True )