dfdfdsfgs commited on
Commit
4588d9f
·
1 Parent(s): dd65f93

Add Hugging Face Spaces frontend with API key fallback system

Browse files
.cursor/rules/rules.mdc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+ always use the powershell commands don't use the command prompt command and always use the .venv virual environ ment.
7
+
8
+
9
+ At last always use the rewiew-gate mcp to get review from user about your work form that you have to fix/ do what user told if it fails because of any reson call terminal with {echo"user review"}.
.specstory/history/.what-is-this.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # SpecStory Artifacts Directory
3
+
4
+ This directory is automatically created and maintained by the SpecStory extension to preserve your Cursor composer and chat history.
5
+
6
+ ## What's Here?
7
+
8
+ - `.specstory/history`: Contains markdown files of your AI coding sessions
9
+ - Each file represents a separate chat or composer session
10
+ - Files are automatically updated as you work
11
+ - `.specstory/cursor_rules_backups`: Contains backups of the `.cursor/rules/derived-cursor-rules.mdc` file
12
+ - Backups are automatically created each time the `.cursor/rules/derived-cursor-rules.mdc` file is updated
13
+ - You can enable/disable the Cursor Rules feature in the SpecStory settings
14
+
15
+ ## Valuable Uses
16
+
17
+ - Capture: Keep your context window up-to-date when starting new Chat/Composer sessions via @ references
18
+ - Search: For previous prompts and code snippets
19
+ - Learn: Meta-analyze your patterns and learn from your past experiences
20
+ - Derive: Keep Cursor on course with your past decisions by automatically deriving Cursor rules from your AI interactions
21
+
22
+ ## Version Control
23
+
24
+ We recommend keeping this directory under version control to maintain a history of your AI interactions. However, if you prefer not to version these files, you can exclude them by adding this to your `.gitignore`:
25
+
26
+ ```
27
+ .specstory
28
+ ```
29
+
30
+ We recommend not keeping the `.specstory/cursor_rules_backups` directory under version control if you are already using git to version the `.cursor/rules` directory, and committing regularly. You can exclude it by adding this to your `.gitignore`:
31
+
32
+ ```
33
+ .specstory/cursor_rules_backups
34
+ ```
35
+
36
+ ## Searching Your Codebase
37
+
38
+ When searching your codebase in Cursor, search results may include your previous AI coding interactions. To focus solely on your actual code files, you can exclude the AI interaction history from search results.
39
+
40
+ To exclude AI interaction history:
41
+
42
+ 1. Open the "Find in Files" search in Cursor (Cmd/Ctrl + Shift + F)
43
+ 2. Navigate to the "files to exclude" section
44
+ 3. Add the following pattern:
45
+
46
+ ```
47
+ .specstory/*
48
+ ```
49
+
50
+ This will ensure your searches only return results from your working codebase files.
51
+
52
+ ## Notes
53
+
54
+ - Auto-save only works when Cursor/sqlite flushes data to disk. This results in a small delay after the AI response is complete before SpecStory can save the history.
55
+ - Auto-save does not yet work on remote WSL workspaces.
56
+
57
+ ## Settings
58
+
59
+ You can control auto-saving behavior in Cursor:
60
+
61
+ 1. Open Cursor → Settings → VS Code Settings (Cmd/Ctrl + ,)
62
+ 2. Search for "SpecStory"
63
+ 3. Find "Auto Save" setting to enable/disable
64
+
65
+ Auto-save occurs when changes are detected in Cursor's sqlite database, or every 2 minutes as a safety net.
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Hugging Face Spaces Deployment Guide
2
+
3
+ This guide will walk you through deploying the Theorem Explanation Agent to Hugging Face Spaces.
4
+
5
+ ## 📋 Prerequisites
6
+
7
+ 1. **Hugging Face Account**: Create an account at [huggingface.co](https://huggingface.co)
8
+ 2. **Gemini API Key(s)**: Get from [Google AI Studio](https://makersuite.google.com/app/apikey)
9
+ 3. **Optional**: ElevenLabs API key for text-to-speech
10
+
11
+ ## 🔧 Step-by-Step Deployment
12
+
13
+ ### Step 1: Create a New Space
14
+
15
+ 1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
16
+ 2. Click "Create new Space"
17
+ 3. Fill in details:
18
+ - **Space name**: `theorem-explanation-agent` (or your choice)
19
+ - **License**: MIT
20
+ - **SDK**: Gradio
21
+ - **Hardware**: CPU Basic (can upgrade later)
22
+ - **Visibility**: Public
23
+
24
+ ### Step 2: Upload Your Code
25
+
26
+ You have two options:
27
+
28
+ #### Option A: Git Clone (Recommended)
29
+ ```bash
30
+ git clone https://github.com/yourusername/theorem-explanation-agent
31
+ cd theorem-explanation-agent
32
+ git remote add hf https://huggingface.co/spaces/yourusername/theorem-explanation-agent
33
+ git push hf main
34
+ ```
35
+
36
+ #### Option B: Upload Files Manually
37
+ 1. Upload these key files to your Space:
38
+ - `app.py`
39
+ - `huggingface_spaces_app.py`
40
+ - `requirements_hf.txt`
41
+ - `README_HUGGINGFACE.md`
42
+ - All folders: `src/`, `mllm_tools/`, etc.
43
+
44
+ ### Step 3: Configure API Keys
45
+
46
+ 1. Go to your Space's **Settings** tab
47
+ 2. Scroll down to **Repository secrets**
48
+ 3. Add these secrets:
49
+
50
+ #### Required Secrets
51
+
52
+ **Multiple API Keys (Recommended)**
53
+ ```
54
+ Name: GEMINI_API_KEY
55
+ Value: AIzaSyA1...,AIzaSyB2...,AIzaSyC3...,AIzaSyD4...
56
+ ```
57
+
58
+ **Single API Key**
59
+ ```
60
+ Name: GEMINI_API_KEY
61
+ Value: AIzaSyA1...
62
+ ```
63
+
64
+ #### Optional Secrets
65
+
66
+ **Enable Full Mode**
67
+ ```
68
+ Name: DEMO_MODE
69
+ Value: false
70
+ ```
71
+
72
+ **Text-to-Speech (Optional)**
73
+ ```
74
+ Name: ELEVENLABS_API_KEY
75
+ Value: your_elevenlabs_api_key
76
+ ```
77
+
78
+ ### Step 4: Configure App Settings
79
+
80
+ In your Space settings:
81
+
82
+ 1. **Hardware**:
83
+ - Start with CPU Basic (free)
84
+ - Upgrade to CPU Optimized for better performance
85
+ - Use GPU only if you enable advanced features
86
+
87
+ 2. **Environment**:
88
+ - Python version: 3.9+
89
+ - Gradio SDK will be automatically detected
90
+
91
+ 3. **Persistent Storage** (Optional):
92
+ - Enable if you want to keep generated videos
93
+ - Useful for caching and avoiding regeneration
94
+
95
+ ## 🔑 API Key Setup Details
96
+
97
+ ### How the Fallback System Works
98
+
99
+ The app uses a smart API key rotation system:
100
+
101
+ ```python
102
+ # Your GEMINI_API_KEY can be:
103
+ # Single key: "AIzaSyA..."
104
+ # Multiple keys: "AIzaSyA...,AIzaSyB...,AIzaSyC..."
105
+
106
+ # The system will:
107
+ # 1. Parse comma-separated keys
108
+ # 2. Randomly select one for each request
109
+ # 3. Automatically retry with different keys if one fails
110
+ # 4. Log which key is being used (first 20 chars only)
111
+ ```
112
+
113
+ ### Benefits of Multiple Keys
114
+
115
+ 1. **Rate Limit Avoidance**: Distributes requests across keys
116
+ 2. **Higher Throughput**: Can handle more concurrent requests
117
+ 3. **Fault Tolerance**: If one key fails, others continue working
118
+ 4. **Cost Distribution**: Spreads usage across multiple billing accounts
119
+
120
+ ### Recommended Setup
121
+
122
+ For production use, we recommend **4 API keys**:
123
+
124
+ ```
125
+ GEMINI_API_KEY=key1,key2,key3,key4
126
+ ```
127
+
128
+ This provides good balance between rate limit avoidance and key management complexity.
129
+
130
+ ## 📊 Monitoring and Costs
131
+
132
+ ### Cost Estimation
133
+
134
+ - **Demo Mode**: Free (no API calls)
135
+ - **Single Scene**: ~$0.001-0.01 per generation
136
+ - **Full Video (3-6 scenes)**: ~$0.01-0.05 per generation
137
+ - **Multiple API Keys**: Costs distributed across accounts
138
+
139
+ ### Monitoring Usage
140
+
141
+ 1. **Google Cloud Console**: Monitor API usage per key
142
+ 2. **Hugging Face Metrics**: View Space usage and performance
143
+ 3. **App Logs**: Check which API keys are being selected
144
+
145
+ ### Usage Patterns
146
+
147
+ ```
148
+ Selected random Gemini API key from 4 available keys: AIzaSyDuKKriNMayoPwn...
149
+ Selected random Gemini API key from 4 available keys: AIzaSyDa44QjZES9qp8L...
150
+ ```
151
+
152
+ ## 🔧 Troubleshooting
153
+
154
+ ### Common Issues
155
+
156
+ 1. **"Demo mode active"**
157
+ - Check `GEMINI_API_KEY` is set in Secrets
158
+ - Verify key format (comma-separated if multiple)
159
+ - Ensure `DEMO_MODE=false` if you want full functionality
160
+
161
+ 2. **"Rate limit exceeded"**
162
+ - Add more API keys to your `GEMINI_API_KEY`
163
+ - Wait a few minutes before retrying
164
+ - Consider upgrading to paid Gemini tier
165
+
166
+ 3. **"Generation failed"**
167
+ - Try simpler topics first
168
+ - Check API key validity
169
+ - Verify topic is educational/mathematical
170
+
171
+ 4. **Slow performance**
172
+ - Upgrade Hardware tier in Space settings
173
+ - Use multiple API keys for better throughput
174
+ - Enable persistent storage for caching
175
+
176
+ ### Error Messages and Solutions
177
+
178
+ | Error | Solution |
179
+ |-------|----------|
180
+ | `No API_KEY found` | Set `GEMINI_API_KEY` in Secrets |
181
+ | `Model is overloaded` | Add more API keys or retry later |
182
+ | `Invalid API key` | Check key format and validity |
183
+ | `Demo mode active` | Set API keys and `DEMO_MODE=false` |
184
+
185
+ ## 🚀 Performance Optimization
186
+
187
+ ### Hardware Recommendations
188
+
189
+ - **CPU Basic**: Good for demo and light usage
190
+ - **CPU Optimized**: Better for regular usage
191
+ - **GPU**: Only needed for advanced video processing
192
+
193
+ ### API Key Strategy
194
+
195
+ ```python
196
+ # Optimal setup for production:
197
+ GEMINI_API_KEY=key1,key2,key3,key4
198
+ DEMO_MODE=false
199
+ ELEVENLABS_API_KEY=optional_tts_key
200
+ ```
201
+
202
+ ### Scaling Considerations
203
+
204
+ 1. **Multiple Keys**: 4-6 keys for high usage
205
+ 2. **Persistent Storage**: Cache results to avoid regeneration
206
+ 3. **Hardware Upgrade**: CPU Optimized or GPU for faster processing
207
+ 4. **Rate Limiting**: Implement user request limiting if needed
208
+
209
+ ## 📞 Support and Resources
210
+
211
+ ### Hugging Face Resources
212
+ - [Spaces Documentation](https://huggingface.co/docs/hub/spaces)
213
+ - [Gradio Documentation](https://gradio.app/docs/)
214
+ - [Community Forums](https://discuss.huggingface.co/)
215
+
216
+ ### API Resources
217
+ - [Google AI Studio](https://makersuite.google.com/)
218
+ - [Gemini API Documentation](https://ai.google.dev/)
219
+ - [ElevenLabs API](https://elevenlabs.io/docs/)
220
+
221
+ ### Getting Help
222
+
223
+ 1. **Check app logs** in Space settings
224
+ 2. **Test with simple topics** first
225
+ 3. **Verify API keys** in Google Cloud Console
226
+ 4. **Monitor costs** and usage patterns
227
+
228
+ ---
229
+
230
+ **Ready to deploy?** Follow these steps and you'll have your Theorem Explanation Agent running on Hugging Face Spaces with automatic API key rotation!
Dockerfile.hf ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies for video processing
7
+ RUN apt-get update && apt-get install -y \
8
+ ffmpeg \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first for better caching
13
+ COPY requirements_hf.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements_hf.txt
17
+
18
+ # Copy the application code
19
+ COPY . .
20
+
21
+ # Create output directory
22
+ RUN mkdir -p output
23
+
24
+ # Expose port
25
+ EXPOSE 7860
26
+
27
+ # Set environment variables
28
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
29
+ ENV GRADIO_SERVER_PORT=7860
30
+
31
+ # Run the application
32
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -4,9 +4,11 @@ emoji: 🎓
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
 
7
  app_file: app.py
8
  pinned: false
9
  license: mit
 
10
  ---
11
 
12
  # 🎓 Theorem Explanation Agent
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ python_version: 3.11
12
  ---
13
 
14
  # 🎓 Theorem Explanation Agent
README_HUGGINGFACE.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎓 Theorem Explanation Agent - Hugging Face Spaces
2
+
3
+ This is a Gradio web interface for generating educational videos that explain mathematical theorems and scientific concepts using AI.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **AI-Powered Video Generation**: Uses Gemini 2.0 Flash to create educational content
8
+ - **Automatic API Key Rotation**: Supports multiple Gemini API keys with fallback mechanism
9
+ - **Educational Focus**: Specializes in mathematical and scientific explanations
10
+ - **Interactive Interface**: Clean, user-friendly Gradio interface
11
+ - **Demo Mode**: Works without API keys for demonstration purposes
12
+
13
+ ## 🔑 Setting Up API Keys on Hugging Face Spaces
14
+
15
+ ### Step 1: Get Your Gemini API Keys
16
+
17
+ 1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey)
18
+ 2. Create one or more API keys
19
+ 3. Copy the keys (they look like: `AIzaSyA...`)
20
+
21
+ ### Step 2: Configure Secrets on Hugging Face Spaces
22
+
23
+ 1. Go to your Space's **Settings** tab
24
+ 2. Click on **Repository secrets**
25
+ 3. Add the following secrets:
26
+
27
+ #### For Multiple API Keys (Recommended)
28
+ ```
29
+ Name: GEMINI_API_KEY
30
+ Value: AIzaSyA...,AIzaSyB...,AIzaSyC...,AIzaSyD...
31
+ ```
32
+
33
+ #### For Single API Key
34
+ ```
35
+ Name: GEMINI_API_KEY
36
+ Value: AIzaSyA...
37
+ ```
38
+
39
+ #### Optional: Text-to-Speech
40
+ ```
41
+ Name: ELEVENLABS_API_KEY
42
+ Value: your_elevenlabs_api_key
43
+ ```
44
+
45
+ #### Enable Full Mode
46
+ ```
47
+ Name: DEMO_MODE
48
+ Value: false
49
+ ```
50
+
51
+ ### Step 3: How the Fallback System Works
52
+
53
+ The app automatically handles multiple API keys:
54
+
55
+ 1. **Comma-Separated Keys**: When you provide multiple keys separated by commas, the system randomly selects one for each request
56
+ 2. **Automatic Rotation**: This helps avoid rate limits and distributes load
57
+ 3. **Error Handling**: If one key fails, the system continues working
58
+ 4. **Usage Tracking**: Each key selection is logged for monitoring
59
+
60
+ ## 📝 Example Usage
61
+
62
+ ### Basic Topics
63
+ - "Velocity" - Physics concepts with examples
64
+ - "Pythagorean Theorem" - Mathematical proofs
65
+ - "Derivatives" - Calculus with geometric interpretation
66
+
67
+ ### Advanced Topics
68
+ - "Newton's Laws" - Physics with demonstrations
69
+ - "Chemical Bonding" - Chemistry with molecular examples
70
+ - "Probability" - Statistics with practical examples
71
+
72
+ ## 🛠️ Configuration Options
73
+
74
+ ### Environment Variables
75
+
76
+ | Variable | Description | Example |
77
+ |----------|-------------|---------|
78
+ | `GEMINI_API_KEY` | Gemini API key(s) - supports comma-separated multiple keys | `key1,key2,key3` |
79
+ | `ELEVENLABS_API_KEY` | Optional: ElevenLabs API key for TTS | `your_elevenlabs_key` |
80
+ | `DEMO_MODE` | Set to "false" to enable full functionality | `false` |
81
+
82
+ ### Video Generation Settings
83
+
84
+ - **Max Scenes**: 1-6 scenes per video (more scenes = longer videos, higher cost)
85
+ - **Context**: Optional additional requirements or focus areas
86
+ - **Topic**: Any mathematical or scientific concept
87
+
88
+ ## 💡 Tips for Best Results
89
+
90
+ 1. **Use Clear Topics**: "velocity in physics" works better than just "motion"
91
+ 2. **Provide Context**: Specify difficulty level, target audience, or focus areas
92
+ 3. **Start Simple**: Try basic concepts first, then move to advanced topics
93
+ 4. **Multiple Keys**: Use 3-4 API keys for better rate limit handling
94
+
95
+ ## 🔧 Troubleshooting
96
+
97
+ ### Common Issues
98
+
99
+ 1. **"No API keys found"**: Check that `GEMINI_API_KEY` is set in Secrets
100
+ 2. **Rate limit errors**: Add more API keys separated by commas
101
+ 3. **Generation fails**: Try simpler topics or shorter context
102
+ 4. **Slow response**: Normal for complex topics, progress bar shows status
103
+
104
+ ### Error Messages
105
+
106
+ - **"Demo mode active"**: API keys not configured properly
107
+ - **"Generation failed"**: Check topic validity and API key status
108
+ - **"Rate limit exceeded"**: Add more API keys for rotation
109
+
110
+ ## 📊 Cost Considerations
111
+
112
+ - Each scene costs approximately $0.001-0.01 in API calls
113
+ - Multiple API keys help distribute costs
114
+ - Demo mode is completely free but only simulates generation
115
+ - Monitor usage through Google Cloud Console
116
+
117
+ ## 🚀 Deployment Checklist
118
+
119
+ - [ ] Fork/clone the repository
120
+ - [ ] Set up Gemini API key(s) in Secrets
121
+ - [ ] Set `DEMO_MODE=false` if you want full functionality
122
+ - [ ] Test with simple topics first
123
+ - [ ] Monitor API usage and costs
124
+
125
+ ## 📞 Support
126
+
127
+ For issues with:
128
+ - **API Keys**: Check Google AI Studio documentation
129
+ - **Rate Limits**: Add more API keys or wait before retrying
130
+ - **Video Generation**: Verify topic is educational/mathematical
131
+ - **Hugging Face**: Check Spaces documentation
132
+
133
+ ---
134
+
135
+ **Note**: This app is designed for educational content generation. The AI models work best with mathematical, scientific, and educational topics.
app.py CHANGED
@@ -1 +1,11 @@
1
-
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theorem Explanation Agent - Main entry point for Hugging Face Spaces
4
+ """
5
+
6
+ # Import from our Hugging Face Spaces app
7
+ from huggingface_spaces_app import create_interface
8
+
9
+ if __name__ == "__main__":
10
+ demo = create_interface()
11
+ demo.launch()
app_backup.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theorem Explanation Agent - Gradio Interface
4
+ A web interface for generating educational videos explaining mathematical theorems and concepts.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import traceback
11
+ import tempfile
12
+ import shutil
13
+ from typing import Optional, List, Dict, Any
14
+ import gradio as gr
15
+ from pathlib import Path
16
+ import asyncio
17
+ import threading
18
+ from datetime import datetime
19
+
20
+ # Add the project root to Python path
21
+ project_root = Path(__file__).parent
22
+ sys.path.insert(0, str(project_root))
23
+
24
+ # Demo mode flag - set to True for deployment environments with limited resources
25
+ DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() == "true" # Default to demo mode for HF Spaces
26
+
27
+ # Global variables for managing video generation
28
+ video_generator = None
29
+ generation_status = {}
30
+
31
+ # Flag to track if we can import all dependencies
32
+ CAN_IMPORT_DEPENDENCIES = True
33
+
34
+ def initialize_video_generator():
35
+ """Initialize the video generator with default settings."""
36
+ global video_generator, CAN_IMPORT_DEPENDENCIES
37
+ try:
38
+ if DEMO_MODE:
39
+ return "✅ Demo mode - Video generator simulation enabled"
40
+
41
+ if not CAN_IMPORT_DEPENDENCIES:
42
+ return "⚠️ Running in fallback mode due to missing dependencies"
43
+
44
+ from generate_video import VideoGenerator
45
+
46
+ video_generator = VideoGenerator(
47
+ planner_model="gemini/gemini-2.0-flash",
48
+ helper_model="gemini/gemini-2.0-flash",
49
+ scene_model="gemini/gemini-2.0-flash",
50
+ output_dir="output",
51
+ use_rag=False,
52
+ use_context_learning=False,
53
+ use_visual_fix_code=False,
54
+ print_response=False
55
+ )
56
+ return "✅ Video generator initialized successfully"
57
+ except Exception as e:
58
+ CAN_IMPORT_DEPENDENCIES = False
59
+ return f"❌ Failed to initialize video generator: {str(e)}\n\n🔧 Running in demo mode"
60
+
61
+ def simulate_video_generation(topic: str, context: str, max_scenes: int) -> Dict[str, Any]:
62
+ """Simulate video generation for demo purposes."""
63
+ import time
64
+ import random
65
+
66
+ # Simulate different stages
67
+ stages = [
68
+ ("Planning video structure", 20),
69
+ ("Generating scene outlines", 40),
70
+ ("Creating animations", 60),
71
+ ("Rendering videos", 80),
72
+ ("Finalizing output", 100)
73
+ ]
74
+
75
+ for stage, progress in stages:
76
+ time.sleep(random.uniform(0.1, 0.3)) # Faster for demo
77
+
78
+ return {
79
+ "success": True,
80
+ "message": f"Demo video generated for topic: {topic}",
81
+ "scenes_created": max_scenes,
82
+ "total_duration": "2.5 minutes",
83
+ "demo_note": "This is a simulated result. In production, actual Manim videos would be generated."
84
+ }
85
+
86
+ def generate_video_demo(topic: str, context: str = "", max_scenes: int = 3) -> str:
87
+ """Generate a video explanation for the given topic (demo version)."""
88
+ if not topic.strip():
89
+ return "❌ Please enter a topic to explain"
90
+
91
+ try:
92
+ # Simulate video generation
93
+ result = simulate_video_generation(topic, context, max_scenes)
94
+
95
+ output = f"""🎓 **Theorem Explanation Agent**
96
+
97
+ **Topic:** {topic}
98
+ **Context:** {context if context else "None provided"}
99
+ **Max Scenes:** {max_scenes}
100
+
101
+ **✅ Demo Generation Complete!**
102
+
103
+ 📊 **Results:**
104
+ - Scenes Created: {result['scenes_created']}
105
+ - Total Duration: {result['total_duration']}
106
+ - Status: {result['message']}
107
+
108
+ ⚠️ **Demo Mode Note:**
109
+ {result['demo_note']}
110
+
111
+ 🚀 **To enable full video generation:**
112
+ 1. Set up API keys (GEMINI_API_KEY, etc.)
113
+ 2. Install full dependencies (Manim, FFmpeg, etc.)
114
+ 3. Set DEMO_MODE=false
115
+
116
+ 📝 **Example topics to try:**
117
+ - Pythagorean Theorem
118
+ - Velocity in Physics
119
+ - Derivatives in Calculus
120
+ - Newton's Laws of Motion
121
+ """
122
+ return output
123
+
124
+ except Exception as e:
125
+ return f"❌ Error during generation: {str(e)}\n\nPlease try with a simpler topic."
126
+
127
+ def get_example_topics() -> List[List[str]]:
128
+ """Get example topics for the interface."""
129
+ return [
130
+ ["Pythagorean Theorem", "Explain with visual proof and real-world applications"],
131
+ ["Velocity", "Explain velocity in physics with detailed examples"],
132
+ ["Derivatives", "Explain derivatives in calculus with geometric interpretation"],
133
+ ["Newton's Laws", "Explain Newton's three laws of motion with examples"],
134
+ ["Quadratic Formula", "Derive and explain the quadratic formula step by step"],
135
+ ["Logarithms", "Explain logarithms and their properties with examples"],
136
+ ["Probability", "Explain basic probability concepts with practical examples"],
137
+ ["Trigonometry", "Explain basic trigonometric functions and their uses"]
138
+ ]
139
+
140
+ # Create the main interface
141
+ with gr.Blocks(
142
+ title="🎓 Theorem Explanation Agent",
143
+ theme=gr.themes.Soft(),
144
+ css="""
145
+ .main-header {
146
+ text-align: center;
147
+ margin-bottom: 30px;
148
+ padding: 20px;
149
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
150
+ border-radius: 10px;
151
+ color: white;
152
+ }
153
+ .demo-warning {
154
+ background-color: #fff3cd;
155
+ border: 1px solid #ffeaa7;
156
+ border-radius: 5px;
157
+ padding: 15px;
158
+ margin: 10px 0;
159
+ color: #856404;
160
+ }
161
+ """
162
+ ) as demo:
163
+
164
+ # Header
165
+ gr.HTML(f"""
166
+ <div class="main-header">
167
+ <h1>🎓 Theorem Explanation Agent</h1>
168
+ <p>Generate educational videos explaining mathematical theorems and concepts using AI</p>
169
+ {'<div class="demo-warning">⚠️ <strong>Demo Mode Active</strong> - This is a simulation for demonstration purposes.</div>' if DEMO_MODE else ''}
170
+ </div>
171
+ """)
172
+
173
+ with gr.Row():
174
+ with gr.Column(scale=2):
175
+ gr.HTML("<h3>📝 Video Generation Settings</h3>")
176
+
177
+ # Topic input
178
+ topic_input = gr.Textbox(
179
+ label="Topic to Explain",
180
+ placeholder="Enter the topic you want to explain (e.g., 'velocity', 'pythagorean theorem')",
181
+ lines=1
182
+ )
183
+
184
+ # Context input
185
+ context_input = gr.Textbox(
186
+ label="Additional Context (Optional)",
187
+ placeholder="Provide additional context or specific requirements for the explanation",
188
+ lines=3
189
+ )
190
+
191
+ # Max scenes
192
+ max_scenes_slider = gr.Slider(
193
+ label="Maximum Number of Scenes",
194
+ minimum=1,
195
+ maximum=5,
196
+ value=3,
197
+ step=1
198
+ )
199
+
200
+ # Example topics
201
+ gr.HTML("<h4>💡 Example Topics</h4>")
202
+ examples = gr.Examples(
203
+ examples=get_example_topics(),
204
+ inputs=[topic_input, context_input]
205
+ )
206
+
207
+ # Generate button
208
+ generate_btn = gr.Button(
209
+ "🚀 Generate Educational Video",
210
+ variant="primary",
211
+ size="lg"
212
+ )
213
+
214
+ with gr.Column(scale=1):
215
+ gr.HTML("<h3>📊 System Status</h3>")
216
+
217
+ # Initialization status
218
+ init_status = gr.Textbox(
219
+ label="System Status",
220
+ value="Click 'Initialize System' to check status",
221
+ interactive=False,
222
+ lines=3
223
+ )
224
+ init_btn = gr.Button("🔧 Initialize System")
225
+
226
+ # Generation result
227
+ gr.HTML("<h3>📋 Generation Results</h3>")
228
+ result_display = gr.Textbox(
229
+ label="Generation Output",
230
+ lines=15,
231
+ interactive=False
232
+ )
233
+
234
+ # Event handlers
235
+ init_btn.click(
236
+ fn=initialize_video_generator,
237
+ outputs=init_status
238
+ )
239
+
240
+ generate_btn.click(
241
+ fn=generate_video_demo,
242
+ inputs=[topic_input, context_input, max_scenes_slider],
243
+ outputs=result_display
244
+ )
245
+
246
+ # Launch the interface if run directly
247
+ if __name__ == "__main__":
248
+ demo.launch(
249
+ server_name="0.0.0.0",
250
+ server_port=7860,
251
+ share=False,
252
+ show_error=True
253
+ )
app_broken.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theorem Explanation Agent - Gradio Interface
4
+ A web interface for generating educational videos explaining mathematical theorems and concepts.
5
+ """
6
+
7
+ import gradio as gr
8
+
9
+ def generate_explanation(topic, context, max_scenes):
10
+ """Generate educational content explanation."""
11
+ if not topic.strip():
12
+ return "❌ Please enter a topic to explain"
13
+
14
+ result = f"""🎓 **Theorem Explanation Agent**
15
+
16
+ 📚 **Topic:** {topic}
17
+ 📋 **Context:** {context if context else "None specified"}
18
+ 🎬 **Scenes:** {max_scenes}
19
+
20
+ ✅ **Demo Generation Complete!**
21
+
22
+ 🎯 **Generated Educational Content:**
23
+ • Introduction to {topic}
24
+ • Fundamental concepts and definitions
25
+ • Step-by-step mathematical derivation
26
+ • Visual demonstrations and proofs
27
+ • Real-world applications and examples
28
+ • Practice problems and solutions
29
+
30
+ 📊 **Video Specifications:**
31
+ • Duration: ~{max_scenes * 0.8:.1f} minutes
32
+ • Scene count: {max_scenes}
33
+ • Style: Mathematical animations
34
+ • Voiceover: AI-generated narration
35
+
36
+ ⚠️ **Demo Mode Active**
37
+ This is a simulation showing the interface capabilities.
38
+ In production mode, actual Manim animations would be generated.
39
+
40
+ 🚀 **Production Features:**
41
+ ✓ Manim mathematical animations
42
+ ✓ AI-powered script generation
43
+ ✓ Professional voiceover synthesis
44
+ ✓ Multiple output formats
45
+ ✓ Custom styling and branding
46
+ """
47
+ return result
48
+
49
+ # Define the interface explicitly
50
+ iface = gr.Interface(
51
+ fn=generate_explanation,
52
+ inputs=[
53
+ gr.Textbox(
54
+ label="🎯 Mathematical Topic",
55
+ placeholder="Enter any mathematical concept (e.g., Pythagorean Theorem, Derivatives, etc.)",
56
+ value=""
57
+ ),
58
+ gr.Textbox(
59
+ label="📝 Additional Context",
60
+ placeholder="Specify learning level, focus areas, or special requirements (optional)",
61
+ value=""
62
+ ),
63
+ gr.Slider(
64
+ label="🎬 Number of Video Scenes",
65
+ minimum=1,
66
+ maximum=8,
67
+ value=4,
68
+ step=1,
69
+ info="More scenes = more detailed explanation"
70
+ )
71
+ ],
72
+ outputs=gr.Textbox(
73
+ label="📊 Generated Educational Content",
74
+ lines=20,
75
+ show_copy_button=True
76
+ ),
77
+ title="🎓 Theorem Explanation Agent",
78
+ description="🚀 Generate educational videos explaining mathematical theorems and concepts using AI-powered animations",
79
+ examples=[
80
+ ["Pythagorean Theorem", "Include visual proof and real-world applications", 4],
81
+ ["Calculus Derivatives", "Focus on geometric interpretation for beginners", 5],
82
+ ["Newton's Laws of Motion", "Physics applications with practical examples", 3],
83
+ ["Quadratic Formula", "Step-by-step derivation with examples", 4],
84
+ ["Probability Distributions", "Visual explanations with real-world data", 5]
85
+ ],
86
+ theme=gr.themes.Soft(),
87
+ css="footer {visibility: hidden}"
88
+ )
89
+
90
+ # Export for HF Spaces
91
+ demo = iface
92
+
93
+ if __name__ == "__main__":
94
+ demo.launch()
generate_video.py CHANGED
@@ -18,7 +18,7 @@ from mllm_tools.utils import _prepare_text_inputs # Keep _prepare_text_inputs if
18
  from src.core.video_planner import VideoPlanner
19
  from src.core.code_generator import CodeGenerator
20
  from src.core.video_renderer import VideoRenderer
21
- from src.utils.utils import _print_response, _extract_code, extract_xml # Import utility functions
22
  from src.config.config import Config # Import Config class
23
 
24
  # Video parsing
@@ -96,9 +96,12 @@ class VideoGenerator:
96
  self.scene_semaphore = asyncio.Semaphore(max_scene_concurrency)
97
  self.banned_reasonings = get_banned_reasonings()
98
 
 
 
 
99
  # Initialize separate modules
100
  self.planner = VideoPlanner(
101
- planner_model=planner_model,
102
  helper_model=helper_model,
103
  output_dir=output_dir,
104
  print_response=verbose,
@@ -112,8 +115,8 @@ class VideoGenerator:
112
  use_langfuse=use_langfuse
113
  )
114
  self.code_generator = CodeGenerator(
115
- scene_model=scene_model if scene_model is not None else planner_model,
116
- helper_model=helper_model if helper_model is not None else planner_model,
117
  output_dir=output_dir,
118
  print_response=verbose,
119
  use_rag=use_rag,
@@ -201,12 +204,13 @@ class VideoGenerator:
201
  Args:
202
  topic (str): The topic of the video
203
  description (str): Description of the video content
204
- session_id (str): Session identifier for tracking
205
 
206
  Returns:
207
- str: Generated scene outline
208
  """
209
- return self.planner.generate_scene_outline(topic, description, session_id)
 
210
 
211
  async def generate_scene_implementation(self,
212
  topic: str,
@@ -405,7 +409,7 @@ class VideoGenerator:
405
 
406
  curr_version += 1
407
  # if program runs this, it means that the code is not rendered successfully
408
- code, log = self.code_generator.fix_code_errors(
409
  implementation_plan=scene_implementation,
410
  code=code,
411
  error=error_message,
@@ -417,7 +421,7 @@ class VideoGenerator:
417
  )
418
 
419
  with open(os.path.join(code_dir, f"{file_prefix}_scene{curr_scene}_v{curr_version}_fix_log.txt"), "w", encoding='utf-8') as f:
420
- f.write(log)
421
  with open(os.path.join(code_dir, f"{file_prefix}_scene{curr_scene}_v{curr_version}.py"), "w", encoding='utf-8') as f:
422
  f.write(code)
423
 
@@ -475,7 +479,7 @@ class VideoGenerator:
475
  """
476
  return await self.planner._generate_scene_implementation_single(topic, description, scene_outline_i, i, file_prefix, session_id, scene_trace_id)
477
 
478
- async def generate_video_pipeline(self, topic: str, description: str, max_retries: int, only_plan: bool = False, specific_scenes: List[int] = None):
479
  """
480
  Modified pipeline to handle partial scene completions and option to only generate plans for specific scenes.
481
 
@@ -485,6 +489,8 @@ class VideoGenerator:
485
  max_retries (int): Maximum number of code fix attempts
486
  only_plan (bool, optional): Whether to only generate plans without rendering. Defaults to False.
487
  specific_scenes (List[int], optional): List of specific scenes to process. Defaults to None.
 
 
488
  """
489
  session_id = self._load_or_create_session_id()
490
  self._save_topic_session_id(topic, session_id)
@@ -495,7 +501,11 @@ class VideoGenerator:
495
  # Load or generate scene outline
496
  scene_outline_path = os.path.join(self.output_dir, file_prefix, f"{file_prefix}_scene_outline.txt")
497
  if not os.path.exists(scene_outline_path):
498
- scene_outline = self.generate_scene_outline(topic, description, session_id)
 
 
 
 
499
  os.makedirs(os.path.join(self.output_dir, file_prefix), exist_ok=True)
500
  with open(scene_outline_path, "w", encoding='utf-8') as f:
501
  f.write(scene_outline)
@@ -504,6 +514,14 @@ class VideoGenerator:
504
  with open(scene_outline_path, "r", encoding='utf-8') as f:
505
  scene_outline = f.read()
506
 
 
 
 
 
 
 
 
 
507
  # Load or generate implementation plans
508
  implementation_plans_dict = self.load_implementation_plans(topic)
509
  if not implementation_plans_dict:
@@ -553,12 +571,10 @@ class VideoGenerator:
553
  has_code = True
554
 
555
  # For only_render mode, only process scenes without code
556
- if args.only_render:
557
  if not has_code:
558
  scenes_to_process.append((i+1, implementation_plan))
559
  print(f"Scene {i+1} has no code, will process")
560
- else:
561
- print(f"Scene {i+1} already has code, skipping")
562
  # For normal mode, process scenes that haven't been successfully rendered
563
  elif not os.path.exists(os.path.join(scene_dir, "succ_rendered.txt")):
564
  scenes_to_process.append((i+1, implementation_plan))
@@ -576,7 +592,7 @@ class VideoGenerator:
576
  await self.render_video_fix_code(topic, description, scene_outline, filtered_implementation_plans,
577
  max_retries=max_retries, session_id=session_id)
578
 
579
- if not args.only_render: # Skip video combination in only_render mode
580
  print(f"Video rendering completed for topic '{topic}'.")
581
 
582
  def check_theorem_status(self, theorem: Dict) -> Dict[str, bool]:
@@ -896,7 +912,9 @@ if __name__ == "__main__":
896
  description,
897
  max_retries=args.max_retries,
898
  only_plan=args.only_plan,
899
- specific_scenes=args.scenes
 
 
900
  )
901
  if not args.only_plan and not args.only_render: # Add condition for only_render
902
  video_generator.combine_videos(topic)
@@ -941,6 +959,8 @@ if __name__ == "__main__":
941
  args.context,
942
  max_retries=args.max_retries,
943
  only_plan=args.only_plan,
 
 
944
  ))
945
  if not args.only_plan and not args.only_render:
946
  video_generator.combine_videos(args.topic)
 
18
  from src.core.video_planner import VideoPlanner
19
  from src.core.code_generator import CodeGenerator
20
  from src.core.video_renderer import VideoRenderer
21
+ from src.utils.utils import _print_response, _extract_code, extract_xml, extract_xml_tag # Import utility functions
22
  from src.config.config import Config # Import Config class
23
 
24
  # Video parsing
 
96
  self.scene_semaphore = asyncio.Semaphore(max_scene_concurrency)
97
  self.banned_reasonings = get_banned_reasonings()
98
 
99
+ # Initialize the planner with the model instance, not a string
100
+ self.planner_model = planner_model
101
+
102
  # Initialize separate modules
103
  self.planner = VideoPlanner(
104
+ planner_model=self.planner_model, # Pass the initialized model
105
  helper_model=helper_model,
106
  output_dir=output_dir,
107
  print_response=verbose,
 
115
  use_langfuse=use_langfuse
116
  )
117
  self.code_generator = CodeGenerator(
118
+ scene_model=scene_model if scene_model is not None else self.planner_model, # Pass the model
119
+ helper_model=helper_model if helper_model is not None else self.planner_model, # Pass the model
120
  output_dir=output_dir,
121
  print_response=verbose,
122
  use_rag=use_rag,
 
204
  Args:
205
  topic (str): The topic of the video
206
  description (str): Description of the video content
207
+ session_id (str): Session identifier
208
 
209
  Returns:
210
+ str: Generated and extracted scene outline
211
  """
212
+ scene_outline = self.planner.generate_scene_outline(topic, description, session_id)
213
+ return scene_outline
214
 
215
  async def generate_scene_implementation(self,
216
  topic: str,
 
409
 
410
  curr_version += 1
411
  # if program runs this, it means that the code is not rendered successfully
412
+ code = self.code_generator.fix_code_errors(
413
  implementation_plan=scene_implementation,
414
  code=code,
415
  error=error_message,
 
421
  )
422
 
423
  with open(os.path.join(code_dir, f"{file_prefix}_scene{curr_scene}_v{curr_version}_fix_log.txt"), "w", encoding='utf-8') as f:
424
+ f.write(error_message)
425
  with open(os.path.join(code_dir, f"{file_prefix}_scene{curr_scene}_v{curr_version}.py"), "w", encoding='utf-8') as f:
426
  f.write(code)
427
 
 
479
  """
480
  return await self.planner._generate_scene_implementation_single(topic, description, scene_outline_i, i, file_prefix, session_id, scene_trace_id)
481
 
482
+ async def generate_video_pipeline(self, topic: str, description: str, max_retries: int, only_plan: bool = False, specific_scenes: List[int] = None, only_render: bool = False, only_combine: bool = False):
483
  """
484
  Modified pipeline to handle partial scene completions and option to only generate plans for specific scenes.
485
 
 
489
  max_retries (int): Maximum number of code fix attempts
490
  only_plan (bool, optional): Whether to only generate plans without rendering. Defaults to False.
491
  specific_scenes (List[int], optional): List of specific scenes to process. Defaults to None.
492
+ only_render (bool, optional): Whether to only render scenes without combining videos. Defaults to False.
493
+ only_combine (bool, optional): Whether to only combine videos. Defaults to False.
494
  """
495
  session_id = self._load_or_create_session_id()
496
  self._save_topic_session_id(topic, session_id)
 
501
  # Load or generate scene outline
502
  scene_outline_path = os.path.join(self.output_dir, file_prefix, f"{file_prefix}_scene_outline.txt")
503
  if not os.path.exists(scene_outline_path):
504
+ scene_outline = self.planner.generate_scene_outline(topic, description, session_id)
505
+ if not scene_outline or not extract_xml(scene_outline):
506
+ print(f"❌ Failed to generate a valid scene outline for topic: {topic}. Aborting.")
507
+ raise ValueError("Failed to generate a valid scene outline from the AI model. Please try a different topic or model.")
508
+
509
  os.makedirs(os.path.join(self.output_dir, file_prefix), exist_ok=True)
510
  with open(scene_outline_path, "w", encoding='utf-8') as f:
511
  f.write(scene_outline)
 
514
  with open(scene_outline_path, "r", encoding='utf-8') as f:
515
  scene_outline = f.read()
516
 
517
+ # After loading or generating, verify the outline has content
518
+ scene_outline_content = extract_xml(scene_outline)
519
+ if not scene_outline_content or len(re.findall(r'<SCENE_(\d+)>[^<]', scene_outline_content)) == 0:
520
+ print(f"❌ Scene outline for '{topic}' is empty or invalid. Deleting and aborting.")
521
+ if os.path.exists(scene_outline_path):
522
+ os.remove(scene_outline_path)
523
+ raise ValueError(f"The generated scene outline for '{topic}' was empty or invalid. The process cannot continue.")
524
+
525
  # Load or generate implementation plans
526
  implementation_plans_dict = self.load_implementation_plans(topic)
527
  if not implementation_plans_dict:
 
571
  has_code = True
572
 
573
  # For only_render mode, only process scenes without code
574
+ if only_render:
575
  if not has_code:
576
  scenes_to_process.append((i+1, implementation_plan))
577
  print(f"Scene {i+1} has no code, will process")
 
 
578
  # For normal mode, process scenes that haven't been successfully rendered
579
  elif not os.path.exists(os.path.join(scene_dir, "succ_rendered.txt")):
580
  scenes_to_process.append((i+1, implementation_plan))
 
592
  await self.render_video_fix_code(topic, description, scene_outline, filtered_implementation_plans,
593
  max_retries=max_retries, session_id=session_id)
594
 
595
+ if not only_render: # Skip video combination in only_render mode
596
  print(f"Video rendering completed for topic '{topic}'.")
597
 
598
  def check_theorem_status(self, theorem: Dict) -> Dict[str, bool]:
 
912
  description,
913
  max_retries=args.max_retries,
914
  only_plan=args.only_plan,
915
+ specific_scenes=args.scenes,
916
+ only_render=args.only_render,
917
+ only_combine=args.only_combine
918
  )
919
  if not args.only_plan and not args.only_render: # Add condition for only_render
920
  video_generator.combine_videos(topic)
 
959
  args.context,
960
  max_retries=args.max_retries,
961
  only_plan=args.only_plan,
962
+ only_render=args.only_render,
963
+ only_combine=args.only_combine
964
  ))
965
  if not args.only_plan and not args.only_render:
966
  video_generator.combine_videos(args.topic)
huggingface_app.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theorem Explanation Agent - Gradio Interface for Hugging Face Spaces
4
+ A web interface for generating educational videos explaining mathematical theorems and concepts.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import traceback
11
+ import tempfile
12
+ import shutil
13
+ import asyncio
14
+ import threading
15
+ import time
16
+ import random
17
+ from typing import Optional, List, Dict, Any, Tuple
18
+ from pathlib import Path
19
+ from datetime import datetime
20
+ import gradio as gr
21
+ import zipfile
22
+
23
+ # Add the project root to Python path
24
+ project_root = Path(__file__).parent
25
+ sys.path.insert(0, str(project_root))
26
+
27
+ # Environment setup for Hugging Face Spaces
28
+ SPACE_ID = os.getenv("SPACE_ID", "")
29
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
30
+ DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() == "true"
31
+
32
+ # Global variables
33
+ video_generator = None
34
+ generation_status = {}
35
+ CAN_IMPORT_DEPENDENCIES = True
36
+
37
+ def setup_environment():
38
+ """Setup environment variables and dependencies for Hugging Face Spaces."""
39
+ print("🚀 Setting up Theorem Explanation Agent...")
40
+
41
+ # Check for API keys
42
+ gemini_keys = os.getenv("GEMINI_API_KEY", "")
43
+ if gemini_keys:
44
+ key_count = len([k.strip() for k in gemini_keys.split(',') if k.strip()])
45
+ print(f"✅ Found {key_count} Gemini API key(s)")
46
+ else:
47
+ print("⚠️ No Gemini API keys found - running in demo mode")
48
+
49
+ # Check for optional environment variables
50
+ elevenlabs_key = os.getenv("ELEVENLABS_API_KEY", "")
51
+ if elevenlabs_key:
52
+ print("✅ ElevenLabs API key found")
53
+ else:
54
+ print("⚠️ No ElevenLabs API key found - TTS will be disabled")
55
+
56
+ return True
57
+
58
+ def initialize_video_generator():
59
+ """Initialize the video generator with error handling."""
60
+ global video_generator, CAN_IMPORT_DEPENDENCIES
61
+
62
+ try:
63
+ if DEMO_MODE:
64
+ return "✅ Demo mode enabled - Video generation will be simulated"
65
+
66
+ # Try importing dependencies
67
+ from generate_video import VideoGenerator
68
+ from mllm_tools.litellm import LiteLLMWrapper
69
+
70
+ # Initialize models
71
+ planner_model = LiteLLMWrapper(
72
+ model_name="gemini/gemini-2.0-flash",
73
+ temperature=0.7,
74
+ print_cost=True,
75
+ verbose=False,
76
+ use_langfuse=False
77
+ )
78
+
79
+ helper_model = LiteLLMWrapper(
80
+ model_name="gemini/gemini-2.0-flash",
81
+ temperature=0.7,
82
+ print_cost=True,
83
+ verbose=False,
84
+ use_langfuse=False
85
+ )
86
+
87
+ video_generator = VideoGenerator(
88
+ planner_model=planner_model,
89
+ helper_model=helper_model,
90
+ scene_model=helper_model,
91
+ output_dir="output",
92
+ use_rag=False,
93
+ use_context_learning=False,
94
+ use_visual_fix_code=False,
95
+ verbose=False
96
+ )
97
+
98
+ return "✅ Video generator initialized successfully with Gemini models"
99
+
100
+ except ImportError as e:
101
+ CAN_IMPORT_DEPENDENCIES = False
102
+ print(f"Import error: {e}")
103
+ return f"⚠️ Missing dependencies - running in demo mode: {str(e)}"
104
+ except Exception as e:
105
+ CAN_IMPORT_DEPENDENCIES = False
106
+ print(f"Initialization error: {e}")
107
+ return f"⚠️ Failed to initialize - running in demo mode: {str(e)}"
108
+
109
+ def simulate_video_generation(topic: str, context: str, max_scenes: int, progress_callback=None) -> Dict[str, Any]:
110
+ """Simulate video generation for demo purposes with progress updates."""
111
+ stages = [
112
+ ("🔍 Analyzing topic and context", 10),
113
+ ("📝 Planning video structure", 25),
114
+ ("🎬 Generating scene outlines", 45),
115
+ ("✨ Creating animations", 70),
116
+ ("🎥 Rendering videos", 85),
117
+ ("🔗 Combining scenes", 95),
118
+ ("✅ Finalizing output", 100)
119
+ ]
120
+
121
+ results = []
122
+ for stage, progress in stages:
123
+ if progress_callback:
124
+ progress_callback(progress, stage)
125
+ time.sleep(random.uniform(0.2, 0.8)) # Simulate processing time
126
+ results.append(f"• {stage}")
127
+
128
+ return {
129
+ "success": True,
130
+ "message": f"Demo video generated for topic: {topic}",
131
+ "scenes_created": max_scenes,
132
+ "total_duration": f"{max_scenes * 45} seconds",
133
+ "processing_steps": results,
134
+ "output_files": [
135
+ f"scene_{i+1}.mp4" for i in range(max_scenes)
136
+ ] + ["combined_video.mp4"],
137
+ "demo_note": "This is a simulated result for demonstration purposes."
138
+ }
139
+
140
+ async def generate_video_async(topic: str, context: str, max_scenes: int, progress_callback=None):
141
+ """Asynchronously generate video with progress updates."""
142
+ global video_generator
143
+
144
+ if not topic.strip():
145
+ return {"success": False, "error": "Please enter a topic to explain"}
146
+
147
+ try:
148
+ if DEMO_MODE or not CAN_IMPORT_DEPENDENCIES:
149
+ return simulate_video_generation(topic, context, max_scenes, progress_callback)
150
+
151
+ # Real video generation
152
+ if progress_callback:
153
+ progress_callback(10, "🚀 Starting video generation...")
154
+
155
+ result = await video_generator.generate_video_pipeline(
156
+ topic=topic,
157
+ description=context,
158
+ max_retries=3,
159
+ only_plan=False,
160
+ specific_scenes=list(range(1, max_scenes + 1)),
161
+ only_render=False,
162
+ only_combine=False
163
+ )
164
+
165
+ if progress_callback:
166
+ progress_callback(100, "✅ Video generation completed!")
167
+
168
+ return {
169
+ "success": True,
170
+ "message": f"Video generated successfully for topic: {topic}",
171
+ "result": result
172
+ }
173
+
174
+ except Exception as e:
175
+ error_msg = f"Error during generation: {str(e)}"
176
+ print(f"Generation error: {traceback.format_exc()}")
177
+ return {"success": False, "error": error_msg}
178
+
179
+ def generate_video_gradio(topic: str, context: str, max_scenes: int, progress=gr.Progress()) -> Tuple[str, str]:
180
+ """Main function called by Gradio interface."""
181
+ def progress_callback(percent, message):
182
+ progress(percent / 100, desc=message)
183
+
184
+ # Run async function in sync context
185
+ loop = asyncio.new_event_loop()
186
+ asyncio.set_event_loop(loop)
187
+
188
+ try:
189
+ result = loop.run_until_complete(
190
+ generate_video_async(topic, context, max_scenes, progress_callback)
191
+ )
192
+ finally:
193
+ loop.close()
194
+
195
+ if result["success"]:
196
+ output = f"""# 🎓 Video Generation Complete!
197
+
198
+ ## 📋 Generation Details
199
+ - **Topic:** {topic}
200
+ - **Context:** {context if context else "None provided"}
201
+ - **Max Scenes:** {max_scenes}
202
+
203
+ ## ✅ Results
204
+ {result["message"]}
205
+
206
+ """
207
+
208
+ if "processing_steps" in result:
209
+ output += "## 🔄 Processing Steps\n"
210
+ for step in result["processing_steps"]:
211
+ output += f"{step}\n"
212
+ output += "\n"
213
+
214
+ if "output_files" in result:
215
+ output += "## 📁 Generated Files\n"
216
+ for file in result["output_files"]:
217
+ output += f"• {file}\n"
218
+ output += "\n"
219
+
220
+ if "demo_note" in result:
221
+ output += f"## ⚠️ Demo Mode\n{result['demo_note']}\n\n"
222
+
223
+ status = "🎮 Demo mode - Simulation completed successfully" if DEMO_MODE else "✅ Video generation completed successfully"
224
+
225
+ return output, status
226
+
227
+ else:
228
+ error_output = f"""# ❌ Generation Failed
229
+
230
+ ## Error Details
231
+ {result.get("error", "Unknown error occurred")}
232
+
233
+ ## 💡 Troubleshooting Tips
234
+ 1. **Check your topic**: Make sure it's a valid mathematical or scientific concept
235
+ 2. **Verify API keys**: Ensure your Gemini API keys are properly set
236
+ 3. **Try simpler topics**: Start with basic concepts like "velocity" or "pythagorean theorem"
237
+ 4. **Check context**: Make sure additional context is relevant and not too complex
238
+
239
+ ## 🔧 Common Issues
240
+ - **API Rate Limits**: If using multiple API keys, the system will automatically rotate between them
241
+ - **Complex Topics**: Very advanced topics might require more specific context
242
+ - **Long Context**: Try shortening the additional context if it's very long
243
+ """
244
+ return error_output, "❌ Generation failed - Check the output for details"
245
+
246
+ def get_example_topics():
247
+ """Get example topics with contexts for the interface."""
248
+ return [
249
+ ["Velocity", "Explain velocity in physics with detailed examples and real-world applications"],
250
+ ["Pythagorean Theorem", "Explain with visual proof and practical applications in construction and navigation"],
251
+ ["Derivatives", "Explain derivatives in calculus with geometric interpretation and rate of change examples"],
252
+ ["Newton's Laws", "Explain Newton's three laws of motion with everyday examples and demonstrations"],
253
+ ["Quadratic Formula", "Derive the quadratic formula step by step and show how to apply it"],
254
+ ["Logarithms", "Explain logarithms, their properties, and applications in science and engineering"],
255
+ ["Probability", "Explain basic probability concepts with coin flips, dice, and card examples"],
256
+ ["Trigonometry", "Explain sine, cosine, and tangent functions with unit circle visualization"],
257
+ ["Limits", "Explain the concept of limits in calculus with graphical examples"],
258
+ ["Chemical Bonding", "Explain ionic, covalent, and metallic bonding with molecular examples"]
259
+ ]
260
+
261
+ def create_interface():
262
+ """Create and configure the Gradio interface."""
263
+
264
+ setup_status = setup_environment()
265
+ init_status = initialize_video_generator()
266
+
267
+ custom_css = """
268
+ .main-header {
269
+ text-align: center;
270
+ margin-bottom: 30px;
271
+ padding: 25px;
272
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
273
+ border-radius: 15px;
274
+ color: white;
275
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
276
+ }
277
+ .status-box {
278
+ padding: 15px;
279
+ border-radius: 10px;
280
+ margin: 10px 0;
281
+ border-left: 4px solid #007bff;
282
+ background-color: #f8f9fa;
283
+ }
284
+ .demo-warning {
285
+ background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
286
+ border: none;
287
+ border-radius: 10px;
288
+ padding: 20px;
289
+ margin: 15px 0;
290
+ color: #2d3436;
291
+ font-weight: 500;
292
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
293
+ }
294
+ """
295
+
296
+ with gr.Blocks(
297
+ title="🎓 Theorem Explanation Agent",
298
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
299
+ css=custom_css
300
+ ) as demo:
301
+
302
+ gr.HTML(f"""
303
+ <div class="main-header">
304
+ <h1>🎓 Theorem Explanation Agent</h1>
305
+ <p style="font-size: 18px; margin: 10px 0;">Generate educational videos explaining mathematical theorems and concepts using AI</p>
306
+ <p style="font-size: 14px; opacity: 0.9;">Powered by Gemini 2.0 Flash with automatic API key rotation</p>
307
+ </div>
308
+ """)
309
+
310
+ if DEMO_MODE:
311
+ gr.HTML("""
312
+ <div class="demo-warning">
313
+ <h3>⚠️ Demo Mode Active</h3>
314
+ <p>This is a demonstration version. To enable full video generation:</p>
315
+ <ul>
316
+ <li>Set your <code>GEMINI_API_KEY</code> in the Secrets tab (supports comma-separated multiple keys)</li>
317
+ <li>Optionally set <code>ELEVENLABS_API_KEY</code> for voice narration</li>
318
+ <li>Set <code>DEMO_MODE=false</code> in environment variables</li>
319
+ </ul>
320
+ </div>
321
+ """)
322
+
323
+ with gr.Row():
324
+ with gr.Column(scale=3):
325
+ gr.HTML("<h3>📝 Video Generation Settings</h3>")
326
+
327
+ topic_input = gr.Textbox(
328
+ label="🎯 Topic to Explain",
329
+ placeholder="Enter the topic you want to explain (e.g., 'velocity', 'pythagorean theorem')",
330
+ lines=1,
331
+ max_lines=2
332
+ )
333
+
334
+ context_input = gr.Textbox(
335
+ label="📝 Additional Context (Optional)",
336
+ placeholder="Provide specific requirements, difficulty level, or focus areas for the explanation",
337
+ lines=3,
338
+ max_lines=5
339
+ )
340
+
341
+ max_scenes_slider = gr.Slider(
342
+ label="🎬 Maximum Number of Scenes",
343
+ minimum=1,
344
+ maximum=6,
345
+ value=3,
346
+ step=1,
347
+ info="More scenes = longer video but higher cost"
348
+ )
349
+
350
+ generate_btn = gr.Button(
351
+ "🚀 Generate Video",
352
+ variant="primary",
353
+ size="lg"
354
+ )
355
+
356
+ with gr.Column(scale=2):
357
+ gr.HTML("<h3>📊 Status & Information</h3>")
358
+
359
+ status_display = gr.Textbox(
360
+ label="🔄 Current Status",
361
+ value=init_status,
362
+ interactive=False,
363
+ lines=2
364
+ )
365
+
366
+ gr.HTML("""
367
+ <div class="status-box">
368
+ <h4>🔑 API Key Setup for Hugging Face Spaces</h4>
369
+ <p><strong>Multiple Gemini Keys (Recommended):</strong></p>
370
+ <code>GEMINI_API_KEY=key1,key2,key3,key4</code>
371
+ <p><strong>Single Key:</strong></p>
372
+ <code>GEMINI_API_KEY=your_single_api_key</code>
373
+ <p><strong>Optional TTS:</strong></p>
374
+ <code>ELEVENLABS_API_KEY=your_elevenlabs_key</code>
375
+ <br><br>
376
+ <small>💡 The system automatically rotates between multiple keys to avoid rate limits</small>
377
+ </div>
378
+ """)
379
+
380
+ examples = gr.Examples(
381
+ examples=get_example_topics(),
382
+ inputs=[topic_input, context_input],
383
+ label="📚 Example Topics & Contexts"
384
+ )
385
+
386
+ output_display = gr.Markdown(
387
+ label="📋 Generation Results",
388
+ value="Ready to generate your first video! Enter a topic above and click 'Generate Video'."
389
+ )
390
+
391
+ generate_btn.click(
392
+ fn=generate_video_gradio,
393
+ inputs=[topic_input, context_input, max_scenes_slider],
394
+ outputs=[output_display, status_display],
395
+ show_progress=True
396
+ )
397
+
398
+ gr.HTML("""
399
+ <div style="text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid #eee;">
400
+ <p>🎓 <strong>Theorem Explanation Agent</strong></p>
401
+ <p>Built with ❤️ using Gradio, Gemini 2.0 Flash, and Manim</p>
402
+ <p style="font-size: 12px; color: #666;">
403
+ For support and updates, visit the project repository •
404
+ Rate limits automatically managed with multi-key rotation
405
+ </p>
406
+ </div>
407
+ """)
408
+
409
+ return demo
410
+
411
+ if __name__ == "__main__":
412
+ demo = create_interface()
413
+ demo.launch(
414
+ server_name="0.0.0.0",
415
+ server_port=7860,
416
+ share=False,
417
+ show_error=True,
418
+ show_tips=True,
419
+ enable_queue=True,
420
+ max_threads=10
421
+ )
huggingface_spaces_app.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Theorem Explanation Agent - Gradio Interface for Hugging Face Spaces
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import asyncio
10
+ import time
11
+ import random
12
+ from typing import Dict, Any, Tuple
13
+ from pathlib import Path
14
+ import gradio as gr
15
+
16
+ # Add project root to path
17
+ project_root = Path(__file__).parent
18
+ sys.path.insert(0, str(project_root))
19
+
20
+ # Environment setup
21
+ DEMO_MODE = os.getenv("DEMO_MODE", "true").lower() == "true"
22
+ video_generator = None
23
+ CAN_IMPORT_DEPENDENCIES = True
24
+
25
+ def setup_environment():
26
+ """Setup environment for HF Spaces."""
27
+ print("🚀 Setting up Theorem Explanation Agent...")
28
+
29
+ gemini_keys = os.getenv("GEMINI_API_KEY", "")
30
+ if gemini_keys:
31
+ key_count = len([k.strip() for k in gemini_keys.split(',') if k.strip()])
32
+ print(f"✅ Found {key_count} Gemini API key(s)")
33
+ return True
34
+ else:
35
+ print("⚠️ No Gemini API keys found - running in demo mode")
36
+ return False
37
+
38
+ def initialize_video_generator():
39
+ """Initialize video generator."""
40
+ global video_generator, CAN_IMPORT_DEPENDENCIES
41
+
42
+ try:
43
+ if DEMO_MODE:
44
+ return "✅ Demo mode enabled"
45
+
46
+ from generate_video import VideoGenerator
47
+ from mllm_tools.litellm import LiteLLMWrapper
48
+
49
+ planner_model = LiteLLMWrapper(
50
+ model_name="gemini/gemini-2.0-flash",
51
+ temperature=0.7,
52
+ print_cost=True,
53
+ verbose=False,
54
+ use_langfuse=False
55
+ )
56
+
57
+ video_generator = VideoGenerator(
58
+ planner_model=planner_model,
59
+ helper_model=planner_model,
60
+ scene_model=planner_model,
61
+ output_dir="output",
62
+ use_rag=False,
63
+ use_context_learning=False,
64
+ use_visual_fix_code=False,
65
+ verbose=False
66
+ )
67
+
68
+ return "✅ Video generator initialized"
69
+
70
+ except Exception as e:
71
+ CAN_IMPORT_DEPENDENCIES = False
72
+ return f"⚠️ Running in demo mode: {str(e)}"
73
+
74
+ def simulate_video_generation(topic: str, context: str, max_scenes: int, progress_callback=None):
75
+ """Simulate video generation."""
76
+ stages = [
77
+ ("🔍 Analyzing topic", 15),
78
+ ("📝 Planning structure", 30),
79
+ ("🎬 Generating scenes", 50),
80
+ ("✨ Creating animations", 75),
81
+ ("🎥 Rendering video", 90),
82
+ ("✅ Finalizing", 100)
83
+ ]
84
+
85
+ results = []
86
+ for stage, progress in stages:
87
+ if progress_callback:
88
+ progress_callback(progress, stage)
89
+ time.sleep(random.uniform(0.3, 0.7))
90
+ results.append(f"• {stage}")
91
+
92
+ return {
93
+ "success": True,
94
+ "message": f"Demo video generated for: {topic}",
95
+ "scenes_created": max_scenes,
96
+ "processing_steps": results,
97
+ "demo_note": "This is a simulation for demo purposes."
98
+ }
99
+
100
+ async def generate_video_async(topic: str, context: str, max_scenes: int, progress_callback=None):
101
+ """Generate video asynchronously."""
102
+ global video_generator
103
+
104
+ if not topic.strip():
105
+ return {"success": False, "error": "Please enter a topic"}
106
+
107
+ try:
108
+ if DEMO_MODE or not CAN_IMPORT_DEPENDENCIES:
109
+ return simulate_video_generation(topic, context, max_scenes, progress_callback)
110
+
111
+ if progress_callback:
112
+ progress_callback(10, "🚀 Starting generation...")
113
+
114
+ result = await video_generator.generate_video_pipeline(
115
+ topic=topic,
116
+ description=context,
117
+ max_retries=3,
118
+ only_plan=False,
119
+ specific_scenes=list(range(1, max_scenes + 1))
120
+ )
121
+
122
+ if progress_callback:
123
+ progress_callback(100, "✅ Completed!")
124
+
125
+ return {"success": True, "message": f"Video generated for: {topic}", "result": result}
126
+
127
+ except Exception as e:
128
+ return {"success": False, "error": str(e)}
129
+
130
+ def generate_video_gradio(topic: str, context: str, max_scenes: int, progress=gr.Progress()) -> Tuple[str, str]:
131
+ """Main Gradio function."""
132
+ def progress_callback(percent, message):
133
+ progress(percent / 100, desc=message)
134
+
135
+ loop = asyncio.new_event_loop()
136
+ asyncio.set_event_loop(loop)
137
+
138
+ try:
139
+ result = loop.run_until_complete(
140
+ generate_video_async(topic, context, max_scenes, progress_callback)
141
+ )
142
+ finally:
143
+ loop.close()
144
+
145
+ if result["success"]:
146
+ output = f"""# 🎓 Video Generation Complete!
147
+
148
+ **Topic:** {topic}
149
+ **Context:** {context if context else "None"}
150
+ **Scenes:** {max_scenes}
151
+
152
+ ## ✅ Result
153
+ {result["message"]}
154
+
155
+ """
156
+ if "processing_steps" in result:
157
+ output += "## 🔄 Steps\n"
158
+ for step in result["processing_steps"]:
159
+ output += f"{step}\n"
160
+
161
+ if "demo_note" in result:
162
+ output += f"\n⚠️ **{result['demo_note']}**"
163
+
164
+ status = "🎮 Demo completed" if DEMO_MODE else "✅ Generation completed"
165
+ return output, status
166
+
167
+ else:
168
+ error_output = f"""# ❌ Generation Failed
169
+
170
+ {result.get("error", "Unknown error")}
171
+
172
+ ## 💡 Tips
173
+ - Check topic validity
174
+ - Verify API keys
175
+ - Try simpler topics
176
+ """
177
+ return error_output, "❌ Failed"
178
+
179
+ def get_examples():
180
+ """Example topics."""
181
+ return [
182
+ ["Velocity", "Physics concept with real-world examples"],
183
+ ["Pythagorean Theorem", "Mathematical proof with applications"],
184
+ ["Derivatives", "Calculus concept with geometric interpretation"],
185
+ ["Newton's Laws", "Three laws of motion with demonstrations"],
186
+ ["Quadratic Formula", "Step-by-step derivation and usage"]
187
+ ]
188
+
189
+ def create_interface():
190
+ """Create Gradio interface."""
191
+ setup_environment()
192
+ init_status = initialize_video_generator()
193
+
194
+ with gr.Blocks(
195
+ title="🎓 Theorem Explanation Agent",
196
+ theme=gr.themes.Soft()
197
+ ) as demo:
198
+
199
+ gr.HTML("""
200
+ <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white; margin-bottom: 20px;">
201
+ <h1>🎓 Theorem Explanation Agent</h1>
202
+ <p>Generate educational videos using AI</p>
203
+ </div>
204
+ """)
205
+
206
+ if DEMO_MODE:
207
+ gr.HTML("""
208
+ <div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin: 10px 0;">
209
+ <h3>⚠️ Demo Mode</h3>
210
+ <p>To enable full functionality:</p>
211
+ <ul>
212
+ <li>Set <code>GEMINI_API_KEY</code> (supports comma-separated keys)</li>
213
+ <li>Set <code>DEMO_MODE=false</code></li>
214
+ </ul>
215
+ </div>
216
+ """)
217
+
218
+ with gr.Row():
219
+ with gr.Column():
220
+ topic_input = gr.Textbox(
221
+ label="Topic",
222
+ placeholder="e.g., velocity, pythagorean theorem"
223
+ )
224
+
225
+ context_input = gr.Textbox(
226
+ label="Context (Optional)",
227
+ placeholder="Additional details or requirements",
228
+ lines=3
229
+ )
230
+
231
+ max_scenes_slider = gr.Slider(
232
+ label="Max Scenes",
233
+ minimum=1,
234
+ maximum=6,
235
+ value=3,
236
+ step=1
237
+ )
238
+
239
+ generate_btn = gr.Button("🚀 Generate Video", variant="primary")
240
+
241
+ with gr.Column():
242
+ status_display = gr.Textbox(
243
+ label="Status",
244
+ value=init_status,
245
+ interactive=False
246
+ )
247
+
248
+ gr.HTML("""
249
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 5px;">
250
+ <h4>🔑 API Setup</h4>
251
+ <p><strong>Multiple keys:</strong></p>
252
+ <code>GEMINI_API_KEY=key1,key2,key3</code>
253
+ <p><strong>Single key:</strong></p>
254
+ <code>GEMINI_API_KEY=your_key</code>
255
+ </div>
256
+ """)
257
+
258
+ examples = gr.Examples(
259
+ examples=get_examples(),
260
+ inputs=[topic_input, context_input]
261
+ )
262
+
263
+ output_display = gr.Markdown(
264
+ value="Ready to generate! Enter a topic and click Generate."
265
+ )
266
+
267
+ generate_btn.click(
268
+ fn=generate_video_gradio,
269
+ inputs=[topic_input, context_input, max_scenes_slider],
270
+ outputs=[output_display, status_display],
271
+ show_progress=True
272
+ )
273
+
274
+ return demo
275
+
276
+ if __name__ == "__main__":
277
+ demo = create_interface()
278
+ demo.launch(
279
+ server_name="0.0.0.0",
280
+ server_port=7860,
281
+ share=False
282
+ )
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  # Essential dependencies for Gradio interface
 
2
  python-dotenv>=0.19.0
3
  requests>=2.25.0
4
  numpy>=1.21.0
 
1
  # Essential dependencies for Gradio interface
2
+ gradio==4.44.0
3
  python-dotenv>=0.19.0
4
  requests>=2.25.0
5
  numpy>=1.21.0
requirements_hf.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies for Hugging Face Spaces
2
+ gradio==4.44.0
3
+ python-dotenv>=0.19.0
4
+ requests>=2.25.0
5
+ numpy>=1.21.0
6
+
7
+ # AI/ML dependencies
8
+ litellm>=1.0.0
9
+ google-generativeai>=0.8.0
10
+
11
+ # Image processing (required for video generation)
12
+ pillow>=8.3.0
13
+
14
+ # Audio processing (optional, for TTS)
15
+ elevenlabs>=1.0.0
16
+
17
+ # Utility libraries
18
+ tqdm>=4.62.0
19
+
20
+ # Optional dependencies for full functionality
21
+ # These will be installed if available, but the app will work without them
22
+ manim; python_version>="3.8"
23
+ pydub>=0.25.0; python_version>="3.8"
24
+ moviepy>=1.0.3; python_version>="3.8"
25
+ soundfile>=0.12.0; python_version>="3.8"
src/core/code_generator.py CHANGED
@@ -469,6 +469,84 @@ class CodeGenerator:
469
  fixed_code
470
  )
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  return fixed_code
473
 
474
  def visual_self_reflection(self, code: str, media_path: Union[str, Image.Image], scene_trace_id: str, topic: str, scene_number: int, session_id: str) -> str:
 
469
  fixed_code
470
  )
471
 
472
+ # Fix 5: Array comparison ambiguity with get_bottom(), get_top(), etc.
473
+ if "The truth value of an array with more than one element is ambiguous" in error:
474
+ import re
475
+ # Fix comparisons like obj.get_bottom() < value to use numpy array indexing
476
+ patterns = [
477
+ (r'(\w+)\.get_bottom\(\)\s*([<>]=?)\s*(-?\d+\.?\d*)', r'\1.get_bottom()[1] \2 \3'),
478
+ (r'(\w+)\.get_top\(\)\s*([<>]=?)\s*(-?\d+\.?\d*)', r'\1.get_top()[1] \2 \3'),
479
+ (r'(\w+)\.get_left\(\)\s*([<>]=?)\s*(-?\d+\.?\d*)', r'\1.get_left()[0] \2 \3'),
480
+ (r'(\w+)\.get_right\(\)\s*([<>]=?)\s*(-?\d+\.?\d*)', r'\1.get_right()[0] \2 \3'),
481
+ ]
482
+ for pattern, replacement in patterns:
483
+ fixed_code = re.sub(pattern, replacement, fixed_code)
484
+
485
+ # Fix 6: Missing SVG files - replace with basic shapes
486
+ if "could not find" in error and ".svg" in error:
487
+ import re
488
+ # Replace SVGMobject with Rectangle for missing SVG files
489
+ svg_patterns = [
490
+ (r'SVGMobject\("car\.svg"\)', 'Rectangle(height=0.5, width=1.0, color=BLUE)'),
491
+ (r'SVGMobject\("arrow\.svg"\)', 'Arrow(start=ORIGIN, end=RIGHT, color=RED)'),
492
+ (r'SVGMobject\("([^"]+\.svg)"\)', r'Rectangle(height=0.5, width=0.5, color=YELLOW) # Replaced missing \1'),
493
+ ]
494
+ for pattern, replacement in svg_patterns:
495
+ fixed_code = re.sub(pattern, replacement, fixed_code)
496
+
497
+ # Fix 7: Non-existent Manim classes/functions
498
+ if "is not defined" in error or "cannot import name" in error:
499
+ import re
500
+ # Replace non-existent Manim functions with working alternatives
501
+ replacements = [
502
+ (r'Surround\(([^)]+)\)', r'Circumscribe(\1)'), # Surround doesn't exist, use Circumscribe
503
+ (r'from manim import \*, Surround', 'from manim import *'), # Remove invalid import
504
+ (r'from manim import Surround[^\n]*\n', ''), # Remove Surround import line
505
+ ]
506
+ for pattern, replacement in replacements:
507
+ fixed_code = re.sub(pattern, replacement, fixed_code)
508
+
509
+ # Fix 8: Config frame attribute errors
510
+ if "'ManimMLConfig' object has no attribute 'frame_width'" in error or \
511
+ "'ManimMLConfig' object has no attribute 'frame_height'" in error:
512
+ # Replace config frame access with hardcoded values
513
+ fixed_code = fixed_code.replace(
514
+ 'config.frame_width', '14.0'
515
+ ).replace(
516
+ 'config.frame_height', '8.0'
517
+ ).replace(
518
+ '-config.frame_width / 2', '-7.0'
519
+ ).replace(
520
+ 'config.frame_width / 2', '7.0'
521
+ ).replace(
522
+ '-config.frame_height / 2', '-4.0'
523
+ ).replace(
524
+ 'config.frame_height / 2', '4.0'
525
+ )
526
+
527
+ # Fix 9: Transform animation issues with function objects
528
+ if "object of type 'function' has no len()" in error and "Transform" in code:
529
+ import re
530
+ # Fix Transform calls with .animate that should use the object directly
531
+ fixed_code = re.sub(
532
+ r'Transform\((\w+), \1\.animate\.([^)]+)\)',
533
+ r'self.play(\1.animate.\2)',
534
+ fixed_code
535
+ )
536
+ # Also fix incorrect Transform usage
537
+ fixed_code = re.sub(
538
+ r'self\.play\(Transform\(([^,]+), ([^)]+)\.animate\.([^)]+)\)\)',
539
+ r'self.play(\2.animate.\3)',
540
+ fixed_code
541
+ )
542
+
543
+ # Fix 10: Syntax errors with import statements
544
+ if "invalid syntax" in error and "import" in error:
545
+ import re
546
+ # Fix malformed import statements
547
+ fixed_code = re.sub(r'from manim import \*, (\w+)', r'from manim import *', fixed_code)
548
+ fixed_code = re.sub(r'from manim import \*,', 'from manim import *', fixed_code)
549
+
550
  return fixed_code
551
 
552
  def visual_self_reflection(self, code: str, media_path: Union[str, Image.Image], scene_trace_id: str, topic: str, scene_number: int, session_id: str) -> str:
src/core/video_planner.py CHANGED
@@ -7,7 +7,7 @@ import uuid
7
  import asyncio
8
 
9
  from mllm_tools.utils import _prepare_text_inputs
10
- from src.utils.utils import extract_xml
11
  from task_generator import (
12
  get_prompt_scene_plan,
13
  get_prompt_scene_vision_storyboard,
@@ -162,16 +162,25 @@ class VideoPlanner:
162
  _prepare_text_inputs(prompt),
163
  metadata={"generation_name": "scene_outline", "tags": [topic, "scene-outline"], "session_id": session_id}
164
  )
165
- # extract scene outline <SCENE_OUTLINE> ... </SCENE_OUTLINE>
166
- scene_outline_match = re.search(r'(<SCENE_OUTLINE>.*?</SCENE_OUTLINE>)', response_text, re.DOTALL)
167
- scene_outline = scene_outline_match.group(1) if scene_outline_match else response_text
168
-
169
- # replace all spaces and special characters with underscores for file path compatibility
 
 
 
 
170
  file_prefix = topic.lower()
171
  file_prefix = re.sub(r'[^a-z0-9_]+', '_', file_prefix)
172
- outline_path = os.path.join(self.output_dir, file_prefix, "scene_outline.txt")
 
 
 
 
 
173
 
174
- # Save the scene outline to a file
175
  with open(outline_path, 'w', encoding='utf-8') as f:
176
  f.write(scene_outline)
177
 
 
7
  import asyncio
8
 
9
  from mllm_tools.utils import _prepare_text_inputs
10
+ from src.utils.utils import extract_xml, extract_xml_tag
11
  from task_generator import (
12
  get_prompt_scene_plan,
13
  get_prompt_scene_vision_storyboard,
 
162
  _prepare_text_inputs(prompt),
163
  metadata={"generation_name": "scene_outline", "tags": [topic, "scene-outline"], "session_id": session_id}
164
  )
165
+
166
+ # Extract scene outline <SCENE_OUTLINE> ... </SCENE_OUTLINE>
167
+ scene_outline = extract_xml_tag(response_text, "SCENE_OUTLINE")
168
+ if not scene_outline:
169
+ print("⚠️ Warning: Could not find <SCENE_OUTLINE> tags. Returning full response.")
170
+ # Fallback to returning the full text if tags are missing, after logging a warning.
171
+ scene_outline = response_text
172
+
173
+ # Create a file-safe prefix for the topic
174
  file_prefix = topic.lower()
175
  file_prefix = re.sub(r'[^a-z0-9_]+', '_', file_prefix)
176
+
177
+ # Ensure the directory exists
178
+ topic_dir = os.path.join(self.output_dir, file_prefix)
179
+ os.makedirs(topic_dir, exist_ok=True)
180
+
181
+ outline_path = os.path.join(topic_dir, "scene_outline.txt")
182
 
183
+ # Save the processed (extracted) scene outline to a file
184
  with open(outline_path, 'w', encoding='utf-8') as f:
185
  f.write(scene_outline)
186
 
src/utils/utils.py CHANGED
@@ -126,3 +126,19 @@ def extract_xml(response: str) -> str:
126
  return re.search(r'```xml\n(.*?)\n```', response, re.DOTALL).group(1)
127
  except:
128
  return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  return re.search(r'```xml\n(.*?)\n```', response, re.DOTALL).group(1)
127
  except:
128
  return response
129
+
130
+ def extract_xml_tag(response: str, tag_name: str) -> str:
131
+ """Extract content between specific XML tags from a text response.
132
+
133
+ Extracts content between XML tags like <TAG_NAME>...</TAG_NAME>.
134
+
135
+ Args:
136
+ response (str): The text response containing XML content
137
+ tag_name (str): The name of the XML tag to extract (without angle brackets)
138
+
139
+ Returns:
140
+ str: The extracted content between the XML tags, or empty string if not found
141
+ """
142
+ pattern = f'<{tag_name}>(.*?)</{tag_name}>'
143
+ match = re.search(pattern, response, re.DOTALL)
144
+ return match.group(1).strip() if match else ""
test_api_keys.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to validate API keys for TheoremExplainAgent
4
+ Usage: python test_api_keys.py
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from dotenv import load_dotenv
10
+
11
+ def test_gemini_api():
12
+ """Test Gemini API key(s)"""
13
+ print("Testing Gemini API...")
14
+
15
+ try:
16
+ import google.generativeai as genai
17
+ import random
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # Get API key with fallback support
23
+ gemini_key_env = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
24
+ if not gemini_key_env:
25
+ print("❌ No GEMINI_API_KEY found in environment")
26
+ print(" Please set GEMINI_API_KEY in your .env file")
27
+ print(" Get your API key from: https://aistudio.google.com/app/apikey")
28
+ return False
29
+
30
+ # Handle multiple keys
31
+ if ',' in gemini_key_env:
32
+ keys = [key.strip() for key in gemini_key_env.split(',') if key.strip()]
33
+ print(f" Found {len(keys)} API keys to test")
34
+ api_key = random.choice(keys)
35
+ print(f" Testing random key: {api_key[:20]}...")
36
+ else:
37
+ api_key = gemini_key_env
38
+ print(f" Testing key: {api_key[:20]}...")
39
+
40
+ # Configure and test
41
+ genai.configure(api_key=api_key)
42
+ model = genai.GenerativeModel('gemini-1.5-pro')
43
+
44
+ # Simple test
45
+ response = model.generate_content("Say hello in one word")
46
+ print(f"✅ Gemini API works! Response: {response.text.strip()}")
47
+ return True
48
+
49
+ except Exception as e:
50
+ print(f"❌ Gemini API test failed: {e}")
51
+ return False
52
+
53
+ def test_elevenlabs_api():
54
+ """Test ElevenLabs API key"""
55
+ print("\nTesting ElevenLabs API...")
56
+
57
+ try:
58
+ import requests
59
+
60
+ # Load environment variables
61
+ load_dotenv()
62
+
63
+ api_key = os.getenv("ELEVENLABS_API_KEY")
64
+ if not api_key:
65
+ print("❌ No ELEVENLABS_API_KEY found in environment")
66
+ print(" Please set ELEVENLABS_API_KEY in your .env file")
67
+ print(" Get your API key from: https://elevenlabs.io/app/settings/api-keys")
68
+ return False
69
+
70
+ print(f" Testing key: {api_key[:20]}...")
71
+
72
+ # Test API with a simple request
73
+ headers = {
74
+ "Accept": "application/json",
75
+ "xi-api-key": api_key
76
+ }
77
+
78
+ response = requests.get("https://api.elevenlabs.io/v1/user", headers=headers)
79
+
80
+ if response.status_code == 200:
81
+ user_data = response.json()
82
+ print(f"✅ ElevenLabs API works! User: {user_data.get('email', 'Unknown')}")
83
+ return True
84
+ else:
85
+ print(f"❌ ElevenLabs API test failed: {response.status_code} - {response.text}")
86
+ return False
87
+
88
+ except Exception as e:
89
+ print(f"❌ ElevenLabs API test failed: {e}")
90
+ return False
91
+
92
+ def main():
93
+ """Main test function"""
94
+ print("🔍 Testing API Keys for TheoremExplainAgent\n")
95
+
96
+ # Check if .env file exists
97
+ if not os.path.exists('.env'):
98
+ print("❌ No .env file found!")
99
+ print(" Please create a .env file based on .env.template")
100
+ print(" Run: cp .env.template .env")
101
+ return
102
+
103
+ gemini_ok = test_gemini_api()
104
+ elevenlabs_ok = test_elevenlabs_api()
105
+
106
+ print("\n" + "="*50)
107
+ if gemini_ok and elevenlabs_ok:
108
+ print("✅ All API keys are working correctly!")
109
+ print(" You can now run generate_video.py")
110
+ elif gemini_ok:
111
+ print("⚠️ Gemini API works, but ElevenLabs API failed")
112
+ print(" Video generation will work but TTS might fail")
113
+ elif elevenlabs_ok:
114
+ print("⚠️ ElevenLabs API works, but Gemini API failed")
115
+ print(" TTS will work but video generation will fail")
116
+ else:
117
+ print("❌ Both API keys failed")
118
+ print(" Please check your .env file and API keys")
119
+
120
+ if __name__ == "__main__":
121
+ main()
test_gemini_only.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import google.generativeai as genai
4
+ import random
5
+
6
+ load_dotenv()
7
+
8
+ gemini_key_env = os.getenv('GEMINI_API_KEY')
9
+ if ',' in gemini_key_env:
10
+ keys = [key.strip() for key in gemini_key_env.split(',') if key.strip()]
11
+ api_key = random.choice(keys)
12
+ print(f"Selected random key: {api_key[:20]}...")
13
+ else:
14
+ api_key = gemini_key_env
15
+ print(f"Using single key: {api_key[:20]}...")
16
+
17
+ genai.configure(api_key=api_key)
18
+ model = genai.GenerativeModel('gemini-1.5-pro')
19
+ response = model.generate_content('Say hello in one word')
20
+ print(f'Success! Response: {response.text.strip()}')