Spaces:
Sleeping
Sleeping
Add Hugging Face Spaces frontend with API key fallback system
Browse files- .cursor/rules/rules.mdc +9 -0
- .specstory/history/.what-is-this.md +65 -0
- DEPLOYMENT_GUIDE.md +230 -0
- Dockerfile.hf +32 -0
- README.md +2 -0
- README_HUGGINGFACE.md +135 -0
- app.py +11 -1
- app_backup.py +253 -0
- app_broken.py +94 -0
- generate_video.py +36 -16
- huggingface_app.py +421 -0
- huggingface_spaces_app.py +282 -0
- requirements.txt +1 -0
- requirements_hf.txt +25 -0
- src/core/code_generator.py +78 -0
- src/core/video_planner.py +17 -8
- src/utils/utils.py +16 -0
- test_api_keys.py +121 -0
- test_gemini_only.py +20 -0
.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
|
| 205 |
|
| 206 |
Returns:
|
| 207 |
-
str: Generated scene outline
|
| 208 |
"""
|
| 209 |
-
|
|
|
|
| 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
|
| 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(
|
| 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
|
| 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
|
| 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 |
-
|
| 166 |
-
|
| 167 |
-
scene_outline =
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
file_prefix = topic.lower()
|
| 171 |
file_prefix = re.sub(r'[^a-z0-9_]+', '_', file_prefix)
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()}')
|