Spaces:
Configuration error
Configuration error
Commit ·
a5e880f
0
Parent(s):
Added Project
Browse files- .env.example +22 -0
- .gitignore +52 -0
- CONTRIBUTING.md +93 -0
- DEPLOYMENT.md +216 -0
- Dockerfile +27 -0
- HF_README.md +70 -0
- LICENSE +21 -0
- README.md +367 -0
- app.py +213 -0
- backend/compiler.py +290 -0
- backend/main.py +155 -0
- backend/models/Modelfile +6 -0
- backend/narrator.py +92 -0
- backend/requirements.txt +9 -0
- backend/runner.py +40 -0
- backend/static/index.html +724 -0
- backend/teacher.py +65 -0
- docker-compose.yml +19 -0
.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sikho Environment Configuration
|
| 2 |
+
# Copy this file to .env and fill in your actual values
|
| 3 |
+
|
| 4 |
+
# ====================================
|
| 5 |
+
# REQUIRED: Google Gemini API Key
|
| 6 |
+
# ====================================
|
| 7 |
+
|
| 8 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
| 9 |
+
|
| 10 |
+
# ====================================
|
| 11 |
+
# OPTIONAL: Additional Configuration
|
| 12 |
+
# ====================================
|
| 13 |
+
|
| 14 |
+
# Server Configuration (uncomment to override defaults)
|
| 15 |
+
# HOST=0.0.0.0
|
| 16 |
+
# PORT=8000
|
| 17 |
+
|
| 18 |
+
# Manim Quality Settings (low, medium, high)
|
| 19 |
+
# MANIM_QUALITY=medium
|
| 20 |
+
|
| 21 |
+
# Maximum concurrent video generations
|
| 22 |
+
# MAX_WORKERS=2
|
.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
.venv/
|
| 10 |
+
pip-log.txt
|
| 11 |
+
pip-delete-this-directory.txt
|
| 12 |
+
|
| 13 |
+
# Environment Variables
|
| 14 |
+
.env
|
| 15 |
+
|
| 16 |
+
# Manim / Media Output
|
| 17 |
+
media/
|
| 18 |
+
backend/media/
|
| 19 |
+
backend/images/
|
| 20 |
+
backend/videos/
|
| 21 |
+
backend/texts/
|
| 22 |
+
|
| 23 |
+
# Logs and Debug Output
|
| 24 |
+
*.log
|
| 25 |
+
error.log
|
| 26 |
+
merge_error.log
|
| 27 |
+
reproduction_output.txt
|
| 28 |
+
models.txt
|
| 29 |
+
models_clean.txt
|
| 30 |
+
*_log.txt
|
| 31 |
+
*_log_*.txt
|
| 32 |
+
|
| 33 |
+
# Generated Files
|
| 34 |
+
backend/scene_*.py
|
| 35 |
+
backend/step_*_narration.mp3
|
| 36 |
+
backend/narration_*.mp3
|
| 37 |
+
|
| 38 |
+
# OS generated files
|
| 39 |
+
.DS_Store
|
| 40 |
+
Thumbs.db
|
| 41 |
+
*.swp
|
| 42 |
+
*.swo
|
| 43 |
+
|
| 44 |
+
# IDEs
|
| 45 |
+
.vscode/
|
| 46 |
+
.idea/
|
| 47 |
+
*.code-workspace
|
| 48 |
+
|
| 49 |
+
# Models
|
| 50 |
+
backend/models/*.gguf
|
| 51 |
+
RENDER_DEPLOY.md
|
| 52 |
+
HUGGINGFACE_DEPLOY.md
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to Sikho
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in contributing to Sikho! We welcome contributions from the community.
|
| 4 |
+
|
| 5 |
+
## How to Contribute
|
| 6 |
+
|
| 7 |
+
### Reporting Bugs
|
| 8 |
+
|
| 9 |
+
If you find a bug, please create an issue with:
|
| 10 |
+
|
| 11 |
+
- Clear description of the problem
|
| 12 |
+
- Steps to reproduce
|
| 13 |
+
- Expected vs actual behavior
|
| 14 |
+
- Environment details (OS, Python version, etc.)
|
| 15 |
+
|
| 16 |
+
### Suggesting Features
|
| 17 |
+
|
| 18 |
+
Feature suggestions are welcome! Please:
|
| 19 |
+
|
| 20 |
+
- Check existing issues first
|
| 21 |
+
- Provide clear use case
|
| 22 |
+
- Explain expected behavior
|
| 23 |
+
- Include examples if possible
|
| 24 |
+
|
| 25 |
+
### Pull Requests
|
| 26 |
+
|
| 27 |
+
1. **Fork the repository**
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
git clone https://github.com/MdZaheen/sikho-scaler.git
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
2. **Create a feature branch**
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
git checkout -b feature/your-feature-name
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
3. **Make your changes**
|
| 40 |
+
|
| 41 |
+
- Follow existing code style
|
| 42 |
+
- Add comments for complex logic
|
| 43 |
+
- Update documentation if needed
|
| 44 |
+
|
| 45 |
+
4. **Test your changes**
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# Run the application
|
| 49 |
+
cd backend
|
| 50 |
+
uvicorn main:app --reload
|
| 51 |
+
|
| 52 |
+
# Test with various prompts
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
5. **Commit with clear messages**
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
git commit -m "Add: description of your changes"
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
6. **Push and create PR**
|
| 62 |
+
```bash
|
| 63 |
+
git push origin feature/your-feature-name
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Code Style
|
| 67 |
+
|
| 68 |
+
- Follow PEP 8 for Python code
|
| 69 |
+
- Use meaningful variable names
|
| 70 |
+
- Add docstrings for functions
|
| 71 |
+
- Keep functions focused and small
|
| 72 |
+
|
| 73 |
+
## Areas for Contribution
|
| 74 |
+
|
| 75 |
+
- 🎨 Frontend improvements
|
| 76 |
+
- 🤖 Additional AI model integrations
|
| 77 |
+
- 🎬 New animation effects
|
| 78 |
+
- 📚 Documentation enhancements
|
| 79 |
+
- 🐛 Bug fixes
|
| 80 |
+
- ⚡ Performance optimizations
|
| 81 |
+
- 🧪 Test coverage
|
| 82 |
+
|
| 83 |
+
## Development Setup
|
| 84 |
+
|
| 85 |
+
See [README.md](README.md) for setup instructions.
|
| 86 |
+
|
| 87 |
+
## Questions?
|
| 88 |
+
|
| 89 |
+
Feel free to open an issue for any questions!
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
Thank you for making Sikho better! 🙏
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide will help you deploy Sikho to production.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- Python 3.8+
|
| 8 |
+
- FFmpeg installed
|
| 9 |
+
- Google Gemini API key
|
| 10 |
+
- Server with at least 2GB RAM
|
| 11 |
+
|
| 12 |
+
## Quick Deployment Steps
|
| 13 |
+
|
| 14 |
+
### 1. Environment Setup
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
# Clone repository
|
| 18 |
+
git clone https://github.com/MdZaheen/sikho-scaler.git
|
| 19 |
+
cd sikho-scaler
|
| 20 |
+
|
| 21 |
+
# Create virtual environment
|
| 22 |
+
python -m venv sikho
|
| 23 |
+
source sikho/bin/activate # Linux/Mac
|
| 24 |
+
# sikho\Scripts\activate # Windows
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 2. Install Dependencies
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
cd backend
|
| 31 |
+
pip install -r requirements.txt
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 3. Configure Environment Variables
|
| 35 |
+
|
| 36 |
+
Create a `.env` file in the project root:
|
| 37 |
+
|
| 38 |
+
```env
|
| 39 |
+
GEMINI_API_KEY=your_actual_gemini_api_key
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### 4. Configure FFmpeg Path
|
| 43 |
+
|
| 44 |
+
Edit `backend/main.py` lines 13-18 and set your FFmpeg path:
|
| 45 |
+
|
| 46 |
+
**Windows:**
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
ffmpeg_path = r"C:\ffmpeg\bin"
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**Linux/Mac:**
|
| 53 |
+
FFmpeg should be in your system PATH. You can comment out the FFmpeg configuration block.
|
| 54 |
+
|
| 55 |
+
### 5. Test Locally
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
cd backend
|
| 59 |
+
uvicorn main:app --reload
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
Visit `http://localhost:8000` to verify everything works.
|
| 63 |
+
|
| 64 |
+
## Production Deployment
|
| 65 |
+
|
| 66 |
+
### Option 1: Using Gunicorn (Linux/Mac)
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
pip install gunicorn
|
| 70 |
+
|
| 71 |
+
# Run with 4 workers
|
| 72 |
+
gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:8000
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### Option 2: Using Uvicorn in Production
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### Option 3: Docker Deployment
|
| 82 |
+
|
| 83 |
+
Create `Dockerfile`:
|
| 84 |
+
|
| 85 |
+
```dockerfile
|
| 86 |
+
FROM python:3.10-slim
|
| 87 |
+
|
| 88 |
+
# Install FFmpeg
|
| 89 |
+
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
| 90 |
+
|
| 91 |
+
WORKDIR /app
|
| 92 |
+
|
| 93 |
+
# Copy requirements and install dependencies
|
| 94 |
+
COPY backend/requirements.txt .
|
| 95 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 96 |
+
|
| 97 |
+
# Copy application
|
| 98 |
+
COPY backend/ .
|
| 99 |
+
COPY .env .
|
| 100 |
+
|
| 101 |
+
EXPOSE 8000
|
| 102 |
+
|
| 103 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
Build and run:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
docker build -t sikho-scaler .
|
| 110 |
+
docker run -p 8000:8000 --env-file .env sikho-scaler
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Option 4: Nginx Reverse Proxy
|
| 114 |
+
|
| 115 |
+
Configure Nginx:
|
| 116 |
+
|
| 117 |
+
```nginx
|
| 118 |
+
server {
|
| 119 |
+
listen 80;
|
| 120 |
+
server_name your-domain.com;
|
| 121 |
+
|
| 122 |
+
location / {
|
| 123 |
+
proxy_pass http://127.0.0.1:8000;
|
| 124 |
+
proxy_set_header Host $host;
|
| 125 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 126 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 127 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 128 |
+
|
| 129 |
+
# Increase timeout for video generation
|
| 130 |
+
proxy_read_timeout 300s;
|
| 131 |
+
proxy_connect_timeout 300s;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## Performance Optimization
|
| 137 |
+
|
| 138 |
+
### 1. Enable Caching
|
| 139 |
+
|
| 140 |
+
Add response caching for static files in `main.py`:
|
| 141 |
+
|
| 142 |
+
```python
|
| 143 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### 2. Cleanup Generated Files
|
| 147 |
+
|
| 148 |
+
Add a cleanup task to remove old videos:
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
# Cron job to clean files older than 24 hours
|
| 152 |
+
0 */6 * * * find /path/to/sikho-scaler/backend/media -name "*.mp4" -mtime +1 -delete
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
### 3. Resource Limits
|
| 156 |
+
|
| 157 |
+
Monitor RAM usage during rendering. Manim can be memory-intensive for complex animations.
|
| 158 |
+
|
| 159 |
+
## Security Checklist
|
| 160 |
+
|
| 161 |
+
- ✅ Keep `.env` file secure (never commit to git)
|
| 162 |
+
- ✅ Use HTTPS in production (Let's Encrypt/Certbot)
|
| 163 |
+
- ✅ Set rate limiting for API endpoints
|
| 164 |
+
- ✅ Regularly update dependencies
|
| 165 |
+
- ✅ Use environment-based configuration
|
| 166 |
+
|
| 167 |
+
## Monitoring
|
| 168 |
+
|
| 169 |
+
### Health Check Endpoint
|
| 170 |
+
|
| 171 |
+
The API includes a health check at `/status`:
|
| 172 |
+
|
| 173 |
+
```bash
|
| 174 |
+
curl http://localhost:8000/status
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### Logs
|
| 178 |
+
|
| 179 |
+
Monitor application logs:
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
# With uvicorn
|
| 183 |
+
uvicorn main:app --log-level info
|
| 184 |
+
|
| 185 |
+
# With Docker
|
| 186 |
+
docker logs -f container_id
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
## Troubleshooting
|
| 190 |
+
|
| 191 |
+
| Issue | Solution |
|
| 192 |
+
| ------------------------- | ---------------------------------------------------------------- |
|
| 193 |
+
| FFmpeg not found | Install FFmpeg: `apt install ffmpeg` or download from ffmpeg.org |
|
| 194 |
+
| Gemini API quota exceeded | Check your API usage at console.cloud.google.com |
|
| 195 |
+
| Out of memory | Reduce concurrent workers or increase server RAM |
|
| 196 |
+
| Slow rendering | Use lower quality settings or optimize prompts |
|
| 197 |
+
|
| 198 |
+
## Scaling
|
| 199 |
+
|
| 200 |
+
For high-traffic deployments:
|
| 201 |
+
|
| 202 |
+
1. **Use a task queue** (Celery + Redis) for async video generation
|
| 203 |
+
2. **CDN for videos** - Store generated videos on S3/Cloud Storage
|
| 204 |
+
3. **Load balancer** - Distribute requests across multiple instances
|
| 205 |
+
4. **Database** - Track generation history and cache results
|
| 206 |
+
|
| 207 |
+
## Support
|
| 208 |
+
|
| 209 |
+
For issues or questions:
|
| 210 |
+
|
| 211 |
+
- GitHub Issues: https://github.com/MdZaheen/sikho-scaler/issues
|
| 212 |
+
- Email: [Your contact email]
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
**Happy Deploying! 🎉**
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies including FFmpeg
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
ffmpeg \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
# Set working directory
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Copy requirements and install Python dependencies
|
| 12 |
+
COPY backend/requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY backend/ .
|
| 17 |
+
COPY .env .env
|
| 18 |
+
|
| 19 |
+
# Expose port
|
| 20 |
+
EXPOSE 8000
|
| 21 |
+
|
| 22 |
+
# Health check
|
| 23 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 24 |
+
CMD python -c "import requests; requests.get('http://localhost:8000/status')"
|
| 25 |
+
|
| 26 |
+
# Run the application
|
| 27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
HF_README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Animetrix AI
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: orange
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.50.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
python_version: 3.10
|
| 12 |
+
models:
|
| 13 |
+
- google/gemini-2.0-flash-exp
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# 🎬 Animetrix AI - Educational Animation Generator
|
| 17 |
+
|
| 18 |
+
Transform text descriptions into professional educational animations using AI and Manim.
|
| 19 |
+
|
| 20 |
+
## ✨ Features
|
| 21 |
+
|
| 22 |
+
- 🤖 **AI-Powered**: Uses Google Gemini for intelligent content generation
|
| 23 |
+
- 🎨 **Professional Quality**: Manim Community Edition for stunning visuals
|
| 24 |
+
- 🎙️ **Synchronized Narration**: Auto-generated voiceover with perfect timing
|
| 25 |
+
- ⚡ **Fast Rendering**: Optimized pipeline for quick results
|
| 26 |
+
- 🎯 **Easy to Use**: Just describe your concept in plain English
|
| 27 |
+
|
| 28 |
+
## 🚀 How It Works
|
| 29 |
+
|
| 30 |
+
1. **Enter your prompt** - Describe the concept you want to visualize
|
| 31 |
+
2. **AI plans the animation** - Gemini creates a structured outline
|
| 32 |
+
3. **Code generation** - Automatic Manim script creation
|
| 33 |
+
4. **Rendering** - Professional video with narration
|
| 34 |
+
5. **Done!** - Download or share your animation
|
| 35 |
+
|
| 36 |
+
## 📝 Example Prompts
|
| 37 |
+
|
| 38 |
+
- "Explain the Pythagorean theorem with a visual proof"
|
| 39 |
+
- "Show how binary search works step by step"
|
| 40 |
+
- "Visualize how compound interest grows over time"
|
| 41 |
+
- "Demonstrate the structure of an atom"
|
| 42 |
+
|
| 43 |
+
## 🛠️ Tech Stack
|
| 44 |
+
|
| 45 |
+
- **AI Model**: Google Gemini 2.0 Flash
|
| 46 |
+
- **Animation**: Manim Community Edition
|
| 47 |
+
- **Text-to-Speech**: gTTS
|
| 48 |
+
- **Video Processing**: MoviePy + FFmpeg
|
| 49 |
+
- **Interface**: Gradio
|
| 50 |
+
|
| 51 |
+
## 🔗 Links
|
| 52 |
+
|
| 53 |
+
- [GitHub Repository](https://github.com/MdZaheen/sikho-scaler)
|
| 54 |
+
- [Documentation](https://github.com/MdZaheen/sikho-scaler#readme)
|
| 55 |
+
- [Report Issues](https://github.com/MdZaheen/sikho-scaler/issues)
|
| 56 |
+
|
| 57 |
+
## 📄 License
|
| 58 |
+
|
| 59 |
+
MIT License - See [LICENSE](LICENSE) for details
|
| 60 |
+
|
| 61 |
+
## 👨💻 Author
|
| 62 |
+
|
| 63 |
+
**Md Zaheen**
|
| 64 |
+
|
| 65 |
+
- GitHub: [@MdZaheen](https://github.com/MdZaheen)
|
| 66 |
+
- Repository: [sikho-scaler](https://github.com/MdZaheen/sikho-scaler)
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
**Made with ❤️ using AI and Manim**
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Sayed Zahur
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# � Animetrix AI - AI Educational Animation Generator
|
| 2 |
+
|
| 3 |
+
> Create professional educational animations from natural language using AI and Manim
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📋 Overview
|
| 8 |
+
|
| 9 |
+
**Animetrix AI** is an intelligent animation generation platform that transforms simple text descriptions into high-quality educational videos. Powered by Google Gemini AI and Manim Community Edition, it automatically interprets concepts, generates animation code, and renders professional videos with synchronized voiceovers.
|
| 10 |
+
|
| 11 |
+
### Key Features
|
| 12 |
+
|
| 13 |
+
- 🤖 **AI-Powered Generation** — Natural language to animated video pipeline
|
| 14 |
+
- 🎬 **Synchronized Narration** — Step-by-step voiceover perfectly timed with animations
|
| 15 |
+
- 🎨 **Professional Quality** — Clean Manim animations with smart post-processing
|
| 16 |
+
- 🔧 **Self-Correcting** — Automatic error detection and code sanitization
|
| 17 |
+
- 🚀 **Production Ready** — FastAPI backend with modern web interface
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 🏗 Architecture
|
| 22 |
+
|
| 23 |
+
```mermaid
|
| 24 |
+
graph TD
|
| 25 |
+
A[User Prompt] --> B[Teacher Module]
|
| 26 |
+
B --> C[Gemini AI - Outline Generation]
|
| 27 |
+
C --> D[Structured JSON with Steps]
|
| 28 |
+
D --> E[Narrator - Per-Step Audio]
|
| 29 |
+
E --> F[Compiler - Code Generation]
|
| 30 |
+
F --> G[Gemini AI - Manim Code]
|
| 31 |
+
G --> H[Code Sanitization]
|
| 32 |
+
H --> I[Runner - Manim Rendering]
|
| 33 |
+
I --> J[Video + Audio Merge]
|
| 34 |
+
J --> K[Final MP4 Output]
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### Pipeline Stages
|
| 38 |
+
|
| 39 |
+
1. **Teacher** (`teacher.py`) - Converts prompts into structured educational outlines
|
| 40 |
+
2. **Narrator** (`narrator.py`) - Generates per-step narration audio with gTTS
|
| 41 |
+
3. **Compiler** (`compiler.py`) - Creates clean Manim code with audio synchronization
|
| 42 |
+
4. **Runner** (`runner.py`) - Executes rendering and merges audio/video
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 🛠 Tech Stack
|
| 47 |
+
|
| 48 |
+
### Backend
|
| 49 |
+
|
| 50 |
+
- **FastAPI** - High-performance async API framework
|
| 51 |
+
- **Google Gemini 2.5 Flash** - AI model for outline and code generation
|
| 52 |
+
- **Manim Community Edition** - Professional animation engine
|
| 53 |
+
- **gTTS** - Text-to-speech for narration
|
| 54 |
+
- **MoviePy** - Audio/video processing
|
| 55 |
+
|
| 56 |
+
### Frontend
|
| 57 |
+
|
| 58 |
+
- **Tailwind CSS** - Modern, responsive design
|
| 59 |
+
- **Three.js** - Animated particle background
|
| 60 |
+
- **Vanilla JavaScript** - Lightweight, no framework overhead
|
| 61 |
+
|
| 62 |
+
### Infrastructure
|
| 63 |
+
|
| 64 |
+
- Python 3.8+
|
| 65 |
+
- FFmpeg for video encoding
|
| 66 |
+
- Environment-based configuration
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
## 📁 Project Structure
|
| 71 |
+
|
| 72 |
+
```
|
| 73 |
+
animetrix-ai/
|
| 74 |
+
├── backend/
|
| 75 |
+
│ ├── main.py # FastAPI application & pipeline orchestration
|
| 76 |
+
│ ├── teacher.py # Outline generation (Gemini)
|
| 77 |
+
│ ├── compiler.py # Code generation & sanitization (Gemini)
|
| 78 |
+
│ ├── runner.py # Manim rendering
|
| 79 |
+
│ ├── narrator.py # Audio generation & merging
|
| 80 |
+
│ ├── requirements.txt # Python dependencies
|
| 81 |
+
│ ├── static/
|
| 82 |
+
│ │ └── index.html # Web interface
|
| 83 |
+
│ └── media/ # Generated videos & audio
|
| 84 |
+
├── .env # Environment variables
|
| 85 |
+
├── .gitignore
|
| 86 |
+
└── README.md
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 🚀 Quick Start
|
| 92 |
+
|
| 93 |
+
### Prerequisites
|
| 94 |
+
|
| 95 |
+
- Python 3.8 or higher
|
| 96 |
+
- FFmpeg installed and in PATH
|
| 97 |
+
- Google Gemini API key
|
| 98 |
+
|
| 99 |
+
### Installation
|
| 100 |
+
|
| 101 |
+
1. **Clone the repository**
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
git clone https://github.com/MdZaheen/sikho-scaler.git
|
| 105 |
+
cd sikho-scaler
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
2. **Create virtual environment**
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
python -m venv venv
|
| 112 |
+
venv\Scripts\activate # Windows
|
| 113 |
+
# source venv/bin/activate # Linux/Mac
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
3. **Install dependencies**
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
cd backend
|
| 120 |
+
pip install -r requirements.txt
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
4. **Configure environment**
|
| 124 |
+
|
| 125 |
+
Create `.env` file in the root directory:
|
| 126 |
+
|
| 127 |
+
```env
|
| 128 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
5. **Update FFmpeg path** (Windows)
|
| 132 |
+
|
| 133 |
+
Edit [main.py](backend/main.py#L13) and set your FFmpeg path:
|
| 134 |
+
|
| 135 |
+
```python
|
| 136 |
+
ffmpeg_path = r"C:\path\to\ffmpeg\bin"
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
### Running the Application
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
cd backend
|
| 143 |
+
uvicorn main:app --reload
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
Open your browser and navigate to:
|
| 147 |
+
|
| 148 |
+
```
|
| 149 |
+
http://localhost:8000
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
## 🎯 Usage
|
| 155 |
+
|
| 156 |
+
1. **Enter a prompt** in the text area (e.g., "Explain the Pythagorean theorem with a visual proof")
|
| 157 |
+
2. **Click Generate** to start the pipeline
|
| 158 |
+
3. **Monitor progress** through real-time status updates:
|
| 159 |
+
- Planning (outline generation)
|
| 160 |
+
- Coding (Manim script creation)
|
| 161 |
+
- Executing (animation rendering)
|
| 162 |
+
4. **Watch the result** - video automatically plays when ready
|
| 163 |
+
|
| 164 |
+
### Example Prompts
|
| 165 |
+
|
| 166 |
+
- "Show how compound interest grows over time"
|
| 167 |
+
- "Visualize the concept of derivatives with a tangent line"
|
| 168 |
+
- "Demonstrate bubble sort algorithm step by step"
|
| 169 |
+
- "Explain photosynthesis with animated diagrams"
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## 🔧 Configuration
|
| 174 |
+
|
| 175 |
+
### Environment Variables
|
| 176 |
+
|
| 177 |
+
| Variable | Description | Required |
|
| 178 |
+
| ---------------- | --------------------- | -------- |
|
| 179 |
+
| `GEMINI_API_KEY` | Google Gemini API key | Yes |
|
| 180 |
+
|
| 181 |
+
### FFmpeg Configuration
|
| 182 |
+
|
| 183 |
+
Update the FFmpeg path in [main.py](backend/main.py) line 13:
|
| 184 |
+
|
| 185 |
+
```python
|
| 186 |
+
ffmpeg_path = r"M:\Ap\ffmpeg-7.1.1-essentials_build\ffmpeg-7.1.1-essentials_build\bin"
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## 📡 API Reference
|
| 192 |
+
|
| 193 |
+
### POST `/generate`
|
| 194 |
+
|
| 195 |
+
Generate an educational animation from a text prompt.
|
| 196 |
+
|
| 197 |
+
**Request Body:**
|
| 198 |
+
|
| 199 |
+
```json
|
| 200 |
+
{
|
| 201 |
+
"prompt": "Explain Newton's first law of motion"
|
| 202 |
+
}
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
**Response:**
|
| 206 |
+
|
| 207 |
+
```json
|
| 208 |
+
{
|
| 209 |
+
"status": "started"
|
| 210 |
+
}
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### GET `/status`
|
| 214 |
+
|
| 215 |
+
Check the current generation status.
|
| 216 |
+
|
| 217 |
+
**Response:**
|
| 218 |
+
|
| 219 |
+
```json
|
| 220 |
+
{
|
| 221 |
+
"stage": "executing",
|
| 222 |
+
"message": "Rendering Animation Frames...",
|
| 223 |
+
"video_path": null,
|
| 224 |
+
"error": null
|
| 225 |
+
}
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
### GET `/video/{path}`
|
| 229 |
+
|
| 230 |
+
Retrieve the generated video file.
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
## 🐛 Troubleshooting
|
| 235 |
+
|
| 236 |
+
| Issue | Solution |
|
| 237 |
+
| --------------------- | ----------------------------------------------------------------- |
|
| 238 |
+
| FFmpeg not found | Ensure FFmpeg is installed and path is correctly set in `main.py` |
|
| 239 |
+
| Gemini API errors | Verify your API key is valid and has sufficient quota |
|
| 240 |
+
| Import errors | Run `pip install -r requirements.txt` in the backend directory |
|
| 241 |
+
| Manim rendering fails | Check that Manim CE is properly installed: `pip install manim` |
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## 🌐 Free Deployment Options
|
| 246 |
+
|
| 247 |
+
### Option 1: Render.com (Recommended)
|
| 248 |
+
|
| 249 |
+
**Best for: Simple, automated deployments**
|
| 250 |
+
|
| 251 |
+
- ✅ Free tier with 750 hours/month
|
| 252 |
+
- ✅ Auto-deploy from GitHub
|
| 253 |
+
- ✅ Built-in SSL
|
| 254 |
+
- ⚠️ Spins down after inactivity (cold starts ~30s)
|
| 255 |
+
|
| 256 |
+
**Steps:**
|
| 257 |
+
|
| 258 |
+
1. Push code to GitHub
|
| 259 |
+
2. Connect to [Render.com](https://render.com)
|
| 260 |
+
3. Create new Web Service
|
| 261 |
+
4. Add environment variable: `GEMINI_API_KEY`
|
| 262 |
+
5. Build command: `cd backend && pip install -r requirements.txt`
|
| 263 |
+
6. Start command: `cd backend && uvicorn main:app --host 0.0.0.0 --port $PORT`
|
| 264 |
+
|
| 265 |
+
### Option 2: Railway.app
|
| 266 |
+
|
| 267 |
+
**Best for: Quick deployments with generous free tier**
|
| 268 |
+
|
| 269 |
+
- ✅ $5 free credit/month
|
| 270 |
+
- ✅ No sleep/cold starts
|
| 271 |
+
- ✅ GitHub integration
|
| 272 |
+
|
| 273 |
+
**Steps:**
|
| 274 |
+
|
| 275 |
+
1. Connect GitHub repo to [Railway.app](https://railway.app)
|
| 276 |
+
2. Add `GEMINI_API_KEY` in Variables
|
| 277 |
+
3. Set start command: `cd backend && uvicorn main:app --host 0.0.0.0 --port $PORT`
|
| 278 |
+
|
| 279 |
+
### Option 3: Fly.io
|
| 280 |
+
|
| 281 |
+
**Best for: Global edge deployment**
|
| 282 |
+
|
| 283 |
+
- ✅ Free tier: 3 shared VMs
|
| 284 |
+
- ✅ Global CDN
|
| 285 |
+
- ✅ Fast performance
|
| 286 |
+
|
| 287 |
+
**Steps:**
|
| 288 |
+
|
| 289 |
+
1. Install Fly CLI: `curl -L https://fly.io/install.sh | sh`
|
| 290 |
+
2. Login: `fly auth login`
|
| 291 |
+
3. Launch: `fly launch`
|
| 292 |
+
4. Set secrets: `fly secrets set GEMINI_API_KEY=your_key`
|
| 293 |
+
|
| 294 |
+
### Option 4: Google Cloud Run
|
| 295 |
+
|
| 296 |
+
**Best for: Pay-per-use, scales to zero**
|
| 297 |
+
|
| 298 |
+
- ✅ 2 million requests/month free
|
| 299 |
+
- ✅ No cold start issues
|
| 300 |
+
- ✅ Integrated with Google services
|
| 301 |
+
|
| 302 |
+
**Steps:**
|
| 303 |
+
|
| 304 |
+
1. Create `Dockerfile` in project root:
|
| 305 |
+
```dockerfile
|
| 306 |
+
FROM python:3.10-slim
|
| 307 |
+
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
| 308 |
+
WORKDIR /app
|
| 309 |
+
COPY backend/requirements.txt .
|
| 310 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 311 |
+
COPY backend/ .
|
| 312 |
+
EXPOSE 8080
|
| 313 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
| 314 |
+
```
|
| 315 |
+
2. Deploy: `gcloud run deploy animetrix-ai --source .`
|
| 316 |
+
|
| 317 |
+
### Option 5: Hugging Face Spaces
|
| 318 |
+
|
| 319 |
+
**Best for: ML/AI community exposure**
|
| 320 |
+
|
| 321 |
+
- ✅ Free GPU access (limited)
|
| 322 |
+
- ✅ Great for demos
|
| 323 |
+
- ✅ Built-in community
|
| 324 |
+
|
| 325 |
+
**Steps:**
|
| 326 |
+
|
| 327 |
+
1. Create new Space at [huggingface.co/spaces](https://huggingface.co/spaces)
|
| 328 |
+
2. Select "Gradio" or "Streamlit" SDK
|
| 329 |
+
3. Push code to Space repository
|
| 330 |
+
4. Add `GEMINI_API_KEY` in Space secrets
|
| 331 |
+
|
| 332 |
+
### Comparison Table
|
| 333 |
+
|
| 334 |
+
| Platform | Free Tier | Cold Starts | Deployment | Best For |
|
| 335 |
+
| -------------------- | -------------- | ----------- | ---------- | --------------- |
|
| 336 |
+
| **Render.com** | ✅ 750h/month | Yes (~30s) | Easiest | Beginners |
|
| 337 |
+
| **Railway.app** | ✅ $5 credit | No | Easy | Active projects |
|
| 338 |
+
| **Fly.io** | ✅ 3 VMs | Minimal | Moderate | Performance |
|
| 339 |
+
| **Google Cloud Run** | ✅ 2M requests | Minimal | Advanced | Scale |
|
| 340 |
+
| **Hugging Face** | ✅ Free GPU | No | Easy | ML Community |
|
| 341 |
+
|
| 342 |
+
**💡 Recommendation:** Start with **Render.com** for easiest setup, then migrate to **Railway.app** or **Fly.io** for production use.
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## 🤝 Contributing
|
| 347 |
+
|
| 348 |
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
| 349 |
+
|
| 350 |
+
---
|
| 351 |
+
|
| 352 |
+
## 📄 License
|
| 353 |
+
|
| 354 |
+
This project is open source and available under the MIT License.
|
| 355 |
+
|
| 356 |
+
---
|
| 357 |
+
|
| 358 |
+
## 👨💻 Author
|
| 359 |
+
|
| 360 |
+
**Md Zaheen**
|
| 361 |
+
|
| 362 |
+
- GitHub: [@MdZaheen](https://github.com/MdZaheen)
|
| 363 |
+
- Repository: [sikho-scaler](https://github.com/MdZaheen/sikho-scaler)
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
**Made with ❤️ using AI and Manim**
|
app.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Animetrix AI - Gradio Interface for Hugging Face Spaces
|
| 3 |
+
Educational Animation Generator powered by AI and Manim
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import asyncio
|
| 11 |
+
import shutil
|
| 12 |
+
|
| 13 |
+
# Add backend to path
|
| 14 |
+
backend_path = Path(__file__).parent / "backend"
|
| 15 |
+
sys.path.insert(0, str(backend_path))
|
| 16 |
+
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# Configure FFmpeg for Hugging Face Spaces (already available in system PATH)
|
| 21 |
+
if shutil.which('ffmpeg') is None:
|
| 22 |
+
print("⚠️ FFmpeg not found - installing...")
|
| 23 |
+
os.system("apt-get update && apt-get install -y ffmpeg")
|
| 24 |
+
|
| 25 |
+
# Import backend functions
|
| 26 |
+
from teacher import generate_outline
|
| 27 |
+
from compiler import generate_manim_code
|
| 28 |
+
from runner import render_scene
|
| 29 |
+
from narrator import generate_narration_audio, merge_audio_video
|
| 30 |
+
|
| 31 |
+
async def generate_animation(prompt, progress=gr.Progress()):
|
| 32 |
+
"""Generate educational animation from text prompt"""
|
| 33 |
+
try:
|
| 34 |
+
# Validate input
|
| 35 |
+
if not prompt or len(prompt.strip()) < 10:
|
| 36 |
+
return None, "❌ Please enter a more detailed prompt (at least 10 characters)"
|
| 37 |
+
|
| 38 |
+
progress(0, desc="🎯 Initializing...")
|
| 39 |
+
|
| 40 |
+
# Step 1: Generate outline
|
| 41 |
+
progress(0.1, desc="📚 Planning animation structure...")
|
| 42 |
+
outline = await generate_outline(prompt)
|
| 43 |
+
|
| 44 |
+
# Step 2: Generate per-step narration
|
| 45 |
+
progress(0.3, desc="🎙️ Generating narration audio...")
|
| 46 |
+
steps = outline.get("steps", [])
|
| 47 |
+
step_audio_paths = []
|
| 48 |
+
|
| 49 |
+
for idx, step in enumerate(steps):
|
| 50 |
+
narration = step.get("narration", "")
|
| 51 |
+
if narration:
|
| 52 |
+
audio_filename = f"step_{idx+1}_narration.mp3"
|
| 53 |
+
audio_path = generate_narration_audio(narration, filename=audio_filename)
|
| 54 |
+
step_audio_paths.append(audio_path)
|
| 55 |
+
progress(0.3 + (0.1 * (idx+1) / len(steps)), desc=f"🎙️ Narration {idx+1}/{len(steps)}...")
|
| 56 |
+
else:
|
| 57 |
+
step_audio_paths.append(None)
|
| 58 |
+
|
| 59 |
+
# Step 3: Generate Manim code
|
| 60 |
+
progress(0.5, desc="💻 Generating animation code...")
|
| 61 |
+
code = await generate_manim_code(outline, step_audio_paths=step_audio_paths)
|
| 62 |
+
|
| 63 |
+
# Step 4: Render video
|
| 64 |
+
progress(0.7, desc="🎬 Rendering video (this takes 30-60s)...")
|
| 65 |
+
video_path = await render_scene(code)
|
| 66 |
+
|
| 67 |
+
# Step 5: Merge audio
|
| 68 |
+
progress(0.9, desc="🔊 Finalizing with audio...")
|
| 69 |
+
if any(step_audio_paths):
|
| 70 |
+
# For now, merge the first available audio
|
| 71 |
+
first_audio = next((a for a in step_audio_paths if a), None)
|
| 72 |
+
if first_audio and os.path.exists(first_audio):
|
| 73 |
+
video_path = merge_audio_video(video_path, first_audio)
|
| 74 |
+
|
| 75 |
+
progress(1.0, desc="✅ Complete!")
|
| 76 |
+
|
| 77 |
+
# Return video and success message
|
| 78 |
+
if os.path.exists(video_path):
|
| 79 |
+
return video_path, f"✅ Animation generated successfully!\n\n📊 Stats:\n- Steps: {len(steps)}\n- Topic: {outline.get('topic', 'N/A')}"
|
| 80 |
+
else:
|
| 81 |
+
return None, "❌ Video file not found after rendering"
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
error_msg = f"❌ Error: {str(e)}\n\nPlease try:\n1. A simpler prompt\n2. Check if GEMINI_API_KEY is set\n3. Report this issue on GitHub"
|
| 85 |
+
print(f"Error in generate_animation: {e}")
|
| 86 |
+
import traceback
|
| 87 |
+
traceback.print_exc()
|
| 88 |
+
return None, error_msg
|
| 89 |
+
|
| 90 |
+
def generate_sync(prompt):
|
| 91 |
+
"""Synchronous wrapper for Gradio"""
|
| 92 |
+
return asyncio.run(generate_animation(prompt))
|
| 93 |
+
|
| 94 |
+
# Example prompts
|
| 95 |
+
EXAMPLES = [
|
| 96 |
+
["Explain the Pythagorean theorem with a right triangle and show a^2 + b^2 = c^2"],
|
| 97 |
+
["Show how binary search algorithm works with a sorted array"],
|
| 98 |
+
["Visualize the structure of an atom with nucleus and orbiting electrons"],
|
| 99 |
+
["Demonstrate how compound interest grows over time with a graph"],
|
| 100 |
+
["Explain Newton's first law of motion with a simple example"],
|
| 101 |
+
["Show bubble sort algorithm sorting an array step by step"],
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
# Custom CSS
|
| 105 |
+
custom_css = """
|
| 106 |
+
.gradio-container {
|
| 107 |
+
font-family: 'Inter', sans-serif;
|
| 108 |
+
}
|
| 109 |
+
.contain {
|
| 110 |
+
max-width: 1200px;
|
| 111 |
+
margin: auto;
|
| 112 |
+
}
|
| 113 |
+
footer {
|
| 114 |
+
display: none !important;
|
| 115 |
+
}
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
# Create Gradio interface
|
| 119 |
+
with gr.Blocks(
|
| 120 |
+
theme=gr.themes.Soft(
|
| 121 |
+
primary_hue="orange",
|
| 122 |
+
secondary_hue="gray",
|
| 123 |
+
neutral_hue="slate",
|
| 124 |
+
),
|
| 125 |
+
css=custom_css,
|
| 126 |
+
title="Animetrix AI - Educational Animation Generator"
|
| 127 |
+
) as demo:
|
| 128 |
+
|
| 129 |
+
gr.Markdown(
|
| 130 |
+
"""
|
| 131 |
+
# 🎬 Animetrix AI
|
| 132 |
+
## AI-Powered Educational Animation Generator
|
| 133 |
+
|
| 134 |
+
Transform your ideas into professional educational animations using AI and Manim.
|
| 135 |
+
Powered by Google Gemini and Manim Community Edition.
|
| 136 |
+
"""
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
with gr.Row():
|
| 140 |
+
with gr.Column(scale=3):
|
| 141 |
+
prompt_input = gr.Textbox(
|
| 142 |
+
label="💡 Describe the concept you want to animate",
|
| 143 |
+
placeholder="e.g., Explain how photosynthesis works in plants...",
|
| 144 |
+
lines=4,
|
| 145 |
+
max_lines=6
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
with gr.Row():
|
| 149 |
+
clear_btn = gr.ClearButton([prompt_input])
|
| 150 |
+
submit_btn = gr.Button("🎬 Generate Animation", variant="primary", size="lg")
|
| 151 |
+
|
| 152 |
+
with gr.Column(scale=2):
|
| 153 |
+
gr.Markdown(
|
| 154 |
+
"""
|
| 155 |
+
### 💡 Tips for Best Results
|
| 156 |
+
|
| 157 |
+
- **Be specific**: Include what you want to see
|
| 158 |
+
- **Use simple language**: Avoid complex jargon
|
| 159 |
+
- **Mention visuals**: Circles, arrows, graphs, etc.
|
| 160 |
+
- **Keep it focused**: One concept per animation
|
| 161 |
+
|
| 162 |
+
### ⏱️ Processing Time
|
| 163 |
+
- Planning: ~5 seconds
|
| 164 |
+
- Narration: ~10 seconds
|
| 165 |
+
- Rendering: ~30-60 seconds
|
| 166 |
+
|
| 167 |
+
**Total: 1-2 minutes**
|
| 168 |
+
"""
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
video_output = gr.Video(label="📹 Generated Animation", height=400)
|
| 172 |
+
status_output = gr.Textbox(label="Status", lines=4, show_label=True)
|
| 173 |
+
|
| 174 |
+
gr.Markdown("### 📚 Example Prompts (Click to try)")
|
| 175 |
+
gr.Examples(
|
| 176 |
+
examples=EXAMPLES,
|
| 177 |
+
inputs=[prompt_input],
|
| 178 |
+
label="Try these examples:"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
gr.Markdown(
|
| 182 |
+
"""
|
| 183 |
+
---
|
| 184 |
+
### 🛠️ Tech Stack
|
| 185 |
+
- **AI**: Google Gemini 2.0 Flash
|
| 186 |
+
- **Animation**: Manim Community Edition
|
| 187 |
+
- **Narration**: gTTS (Google Text-to-Speech)
|
| 188 |
+
- **Video Processing**: MoviePy + FFmpeg
|
| 189 |
+
|
| 190 |
+
### 🔗 Links
|
| 191 |
+
- [GitHub Repository](https://github.com/MdZaheen/sikho-scaler)
|
| 192 |
+
- [Report Issues](https://github.com/MdZaheen/sikho-scaler/issues)
|
| 193 |
+
- [Documentation](https://github.com/MdZaheen/sikho-scaler#readme)
|
| 194 |
+
|
| 195 |
+
**Made with ❤️ by Md Zaheen**
|
| 196 |
+
"""
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Event handlers
|
| 200 |
+
submit_btn.click(
|
| 201 |
+
fn=generate_sync,
|
| 202 |
+
inputs=[prompt_input],
|
| 203 |
+
outputs=[video_output, status_output],
|
| 204 |
+
api_name="generate"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Launch configuration
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
demo.queue(max_size=5).launch(
|
| 210 |
+
server_name="0.0.0.0",
|
| 211 |
+
server_port=7860,
|
| 212 |
+
show_error=True
|
| 213 |
+
)
|
backend/compiler.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
from google import genai
|
| 6 |
+
from google.genai import types
|
| 7 |
+
|
| 8 |
+
COMPILER_SYSTEM_PROMPT = """
|
| 9 |
+
You are a deterministic Manim script generator for a text-to-educational-animation engine.
|
| 10 |
+
|
| 11 |
+
Your job:
|
| 12 |
+
Convert the user prompt into a single clean Manim Community Edition Python script.
|
| 13 |
+
|
| 14 |
+
The script will be executed automatically by a backend system, so the structure must be strict.
|
| 15 |
+
|
| 16 |
+
────────────────────────────
|
| 17 |
+
|
| 18 |
+
ABSOLUTE REQUIREMENTS
|
| 19 |
+
|
| 20 |
+
────────────────────────────
|
| 21 |
+
|
| 22 |
+
Always output ONLY Python code.
|
| 23 |
+
|
| 24 |
+
No markdown
|
| 25 |
+
No explanations
|
| 26 |
+
No comments
|
| 27 |
+
No triple quotes
|
| 28 |
+
|
| 29 |
+
Exactly 1 Scene class:
|
| 30 |
+
|
| 31 |
+
class GenScene(Scene):
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
Never use any other scene name.
|
| 35 |
+
|
| 36 |
+
Imports:
|
| 37 |
+
|
| 38 |
+
from manim import *
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
No LaTeX
|
| 42 |
+
Never use:
|
| 43 |
+
MathTex
|
| 44 |
+
Tex
|
| 45 |
+
Matrix
|
| 46 |
+
TexTemplate
|
| 47 |
+
|
| 48 |
+
Use only:
|
| 49 |
+
Text("expression or label")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
────────────────────────────
|
| 53 |
+
|
| 54 |
+
ASCII RULES (CRITICAL)
|
| 55 |
+
|
| 56 |
+
────────────────────────────
|
| 57 |
+
|
| 58 |
+
1. NEVER output Unicode mathematical characters or subscript/superscript glyphs.
|
| 59 |
+
❌ Do not use: ² ₁ ₓ α β γ θ π σ → ∞ × ÷
|
| 60 |
+
✔️ Instead use ONLY plain ASCII text:
|
| 61 |
+
"x^2" instead of "x²"
|
| 62 |
+
"x_1" instead of "x₁"
|
| 63 |
+
"pi" instead of "π"
|
| 64 |
+
"velocity" instead of "→v"
|
| 65 |
+
|
| 66 |
+
2. The script must be compatible with Windows console and UTF-8 only.
|
| 67 |
+
- No special glyphs, emoji, arrows, smart quotes, curly quotes, or accents.
|
| 68 |
+
|
| 69 |
+
────────────────────────────
|
| 70 |
+
|
| 71 |
+
VISUAL RULES (CRITICAL)
|
| 72 |
+
|
| 73 |
+
────────────────────────────
|
| 74 |
+
|
| 75 |
+
1. NEVER allow visuals to go outside the video frame.
|
| 76 |
+
- Keep all objects centered or inside safe boundaries.
|
| 77 |
+
- Do not let squares, shapes, or arrows clip off-screen.
|
| 78 |
+
- Standard frame is [-7, 7] horizontally and [-4, 4] vertically. Keep well within this.
|
| 79 |
+
|
| 80 |
+
2. No overlapping elements.
|
| 81 |
+
- All text must be positioned with next_to(), move_to(), or buff>=0.4.
|
| 82 |
+
- All shapes must have spacing.
|
| 83 |
+
- If a square represents a², show the label inside or beside — never over other shapes.
|
| 84 |
+
|
| 85 |
+
3. Visual accuracy FIRST.
|
| 86 |
+
- Show geometry clearly.
|
| 87 |
+
- Avoid rotating or stretching objects unnecessarily.
|
| 88 |
+
- Avoid random effects.
|
| 89 |
+
|
| 90 |
+
────────────────────────────
|
| 91 |
+
|
| 92 |
+
ANIMATION RULES
|
| 93 |
+
|
| 94 |
+
────────────────────────────
|
| 95 |
+
|
| 96 |
+
1. Slow down animations & make them educational.
|
| 97 |
+
- Use 0.5–1 second durations for Create(), Write(), FadeIn().
|
| 98 |
+
- Avoid sudden transitions.
|
| 99 |
+
- Avoid instant scaling or teleporting.
|
| 100 |
+
|
| 101 |
+
2. Only use these animations:
|
| 102 |
+
Create
|
| 103 |
+
FadeIn
|
| 104 |
+
FadeOut
|
| 105 |
+
Write
|
| 106 |
+
Transform
|
| 107 |
+
MoveTo
|
| 108 |
+
Scale
|
| 109 |
+
Rotate
|
| 110 |
+
|
| 111 |
+
3. No 3D, no camera zoom, no cinematic effects, no physics.
|
| 112 |
+
|
| 113 |
+
────────────────────────────
|
| 114 |
+
|
| 115 |
+
STRUCTURE & PACING
|
| 116 |
+
|
| 117 |
+
────────────────────────────
|
| 118 |
+
|
| 119 |
+
1. Follow step-by-step logic:
|
| 120 |
+
- Introduce main idea
|
| 121 |
+
- Draw objects (one-by-one, not overlapping)
|
| 122 |
+
- Highlight key components
|
| 123 |
+
- Explain or show the formula visually
|
| 124 |
+
- Conclude cleanly
|
| 125 |
+
|
| 126 |
+
2. Keep total runtime 12–18 seconds.
|
| 127 |
+
- Use self.wait(1) or self.wait(2) to pace the video.
|
| 128 |
+
|
| 129 |
+
────────────────────────────
|
| 130 |
+
|
| 131 |
+
OUTPUT FORMAT EXAMPLE
|
| 132 |
+
|
| 133 |
+
────────────────────────────
|
| 134 |
+
|
| 135 |
+
from manim import *
|
| 136 |
+
|
| 137 |
+
class GenScene(Scene):
|
| 138 |
+
def construct(self):
|
| 139 |
+
# 1. Introduce
|
| 140 |
+
title = Text("Concept Name").scale(0.8).to_edge(UP)
|
| 141 |
+
self.play(Write(title), run_time=1)
|
| 142 |
+
self.wait(0.5)
|
| 143 |
+
|
| 144 |
+
# 2. Draw Objects
|
| 145 |
+
box = Square(side_length=2, color=BLUE)
|
| 146 |
+
self.play(Create(box), run_time=1)
|
| 147 |
+
self.wait(0.5)
|
| 148 |
+
|
| 149 |
+
# 3. Label (No overlap)
|
| 150 |
+
label = Text("Side = 2").next_to(box, DOWN, buff=0.5)
|
| 151 |
+
self.play(Write(label), run_time=1)
|
| 152 |
+
self.wait(1)
|
| 153 |
+
|
| 154 |
+
# 4. Conclude
|
| 155 |
+
self.play(FadeOut(box), FadeOut(label), run_time=1)
|
| 156 |
+
self.wait(1)
|
| 157 |
+
|
| 158 |
+
────────────────────────────
|
| 159 |
+
|
| 160 |
+
FINAL OUTPUT RULE
|
| 161 |
+
|
| 162 |
+
────────────────────────────
|
| 163 |
+
|
| 164 |
+
➡️ Return ONLY Python code.
|
| 165 |
+
➡️ No formatting, no text, no explanations.
|
| 166 |
+
➡️ Only 1 Scene class named GenScene.
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
async def generate_manim_code(outline: dict, step_audio_paths=None):
|
| 170 |
+
outline_str = json.dumps(outline, indent=2)
|
| 171 |
+
api_key = os.environ.get("GEMINI_API_KEY")
|
| 172 |
+
if not api_key:
|
| 173 |
+
raise ValueError("GEMINI_API_KEY not found in environment variables.")
|
| 174 |
+
|
| 175 |
+
client = genai.Client(api_key=api_key)
|
| 176 |
+
prompt = f"{COMPILER_SYSTEM_PROMPT}\n\nINPUT OUTLINE:\n{outline_str}\n\nPYTHON CODE:"
|
| 177 |
+
|
| 178 |
+
try:
|
| 179 |
+
response = client.models.generate_content(
|
| 180 |
+
model='gemini-2.0-flash-exp',
|
| 181 |
+
contents=prompt
|
| 182 |
+
)
|
| 183 |
+
code = response.text.strip()
|
| 184 |
+
# Cleanup markdown if present
|
| 185 |
+
if code.startswith("```python"):
|
| 186 |
+
code = code[9:]
|
| 187 |
+
elif code.startswith("```"):
|
| 188 |
+
code = code[3:]
|
| 189 |
+
if code.endswith("```"):
|
| 190 |
+
code = code[:-3]
|
| 191 |
+
# --- POST-PROCESSING SANITIZATION ---
|
| 192 |
+
if "MathTex" in code or "Tex(" in code:
|
| 193 |
+
print("WARNING: Model used LaTeX despite instructions. Sanitizing code...")
|
| 194 |
+
code = code.replace("MathTex", "Text")
|
| 195 |
+
code = code.replace("Tex(", "Text(")
|
| 196 |
+
replacements = {
|
| 197 |
+
r"^\\circ": " degrees", r"\\circ": " degrees", "°": " degrees",
|
| 198 |
+
r"\\theta": "theta", "θ": "theta",
|
| 199 |
+
r"\\pi": "pi", "π": "pi",
|
| 200 |
+
r"\\alpha": "alpha", "α": "alpha",
|
| 201 |
+
r"\\beta": "beta", "β": "beta",
|
| 202 |
+
r"\\gamma": "gamma", "γ": "gamma",
|
| 203 |
+
r"\\sigma": "sigma", "σ": "sigma",
|
| 204 |
+
r"\\Delta": "Delta", "Δ": "Delta",
|
| 205 |
+
r"\\times": "x", "×": "x",
|
| 206 |
+
r"\\cdot": "*", "·": "*",
|
| 207 |
+
r"\\div": "/", "÷": "/",
|
| 208 |
+
r"\\pm": "+/-", "±": "+/-",
|
| 209 |
+
r"\\approx": "~", "≈": "~",
|
| 210 |
+
r"\\neq": "!=", "≠": "!=",
|
| 211 |
+
r"\\le": "<=", "≤": "<=",
|
| 212 |
+
r"\\ge": ">=", "≥": ">=",
|
| 213 |
+
r"\\infty": "infinity", "∞": "infinity",
|
| 214 |
+
r"\\Rightarrow": "->", "⇒": "->",
|
| 215 |
+
r"\\rightarrow": "->", "→": "->",
|
| 216 |
+
r"\\leftarrow": "<-", "←": "<-",
|
| 217 |
+
"²": "^2", "³": "^3", "₁": "_1", "₂": "_2", "ₓ": "_x",
|
| 218 |
+
r"\\\\": "\n", # Double backslash to newline
|
| 219 |
+
"–": "-", # En dash to hyphen
|
| 220 |
+
"—": "-", # Em dash to hyphen
|
| 221 |
+
"’": "'", # Smart quotes
|
| 222 |
+
"“": '"',
|
| 223 |
+
"”": '"',
|
| 224 |
+
}
|
| 225 |
+
for pattern, replacement in replacements.items():
|
| 226 |
+
code = code.replace(pattern, replacement)
|
| 227 |
+
# Remove any model-generated self.add_sound calls (we'll insert our own)
|
| 228 |
+
lines = code.split('\n')
|
| 229 |
+
code = '\n'.join([line for line in lines if "self.add_sound" not in line])
|
| 230 |
+
# Fix .center usage (replace .center with .get_center() only when used as an argument)
|
| 231 |
+
code = re.sub(r'([\w\)\]]+)\.center(\s*\))', r'\1.get_center()\2', code)
|
| 232 |
+
code = re.sub(r'class\s+\w+\(Scene\):', 'class GenScene(Scene):', code)
|
| 233 |
+
|
| 234 |
+
# --- Insert self.add_sound and self.wait for each step ---
|
| 235 |
+
if step_audio_paths and isinstance(step_audio_paths, list):
|
| 236 |
+
# Try to find the construct() method and insert after each animation step
|
| 237 |
+
import ast
|
| 238 |
+
try:
|
| 239 |
+
# Parse the code to find the construct() method
|
| 240 |
+
class ConstructVisitor(ast.NodeVisitor):
|
| 241 |
+
def __init__(self):
|
| 242 |
+
self.construct_node = None
|
| 243 |
+
def visit_FunctionDef(self, node):
|
| 244 |
+
if node.name == "construct":
|
| 245 |
+
self.construct_node = node
|
| 246 |
+
|
| 247 |
+
tree = ast.parse(code)
|
| 248 |
+
visitor = ConstructVisitor()
|
| 249 |
+
visitor.visit(tree)
|
| 250 |
+
if visitor.construct_node:
|
| 251 |
+
# Get the lines of the code
|
| 252 |
+
code_lines = code.split('\n')
|
| 253 |
+
func = visitor.construct_node
|
| 254 |
+
# Find the start and end of the construct() method
|
| 255 |
+
start = func.lineno - 1
|
| 256 |
+
end = func.end_lineno if hasattr(func, 'end_lineno') else None
|
| 257 |
+
if end is None:
|
| 258 |
+
# Fallback: find the next function/class or end of file
|
| 259 |
+
for i in range(start+1, len(code_lines)):
|
| 260 |
+
if code_lines[i].startswith(' def ') or code_lines[i].startswith('class '):
|
| 261 |
+
end = i
|
| 262 |
+
break
|
| 263 |
+
if end is None:
|
| 264 |
+
end = len(code_lines)
|
| 265 |
+
# Insert self.add_sound and self.wait after each animation step
|
| 266 |
+
new_func_lines = []
|
| 267 |
+
step_idx = 0
|
| 268 |
+
for line in code_lines[start:end]:
|
| 269 |
+
new_func_lines.append(line)
|
| 270 |
+
# Heuristic: insert after self.play( or self.wait( lines
|
| 271 |
+
if ("self.play(" in line or "self.wait(" in line) and step_idx < len(step_audio_paths):
|
| 272 |
+
audio_path = step_audio_paths[step_idx]
|
| 273 |
+
if audio_path:
|
| 274 |
+
# Indent matches the line
|
| 275 |
+
indent = ' ' * (len(line) - len(line.lstrip()))
|
| 276 |
+
new_func_lines.append(f'{indent}self.add_sound(r"{audio_path}")')
|
| 277 |
+
# Optionally, add a wait (e.g., 1.5s)
|
| 278 |
+
new_func_lines.append(f'{indent}self.wait(1.5)')
|
| 279 |
+
step_idx += 1
|
| 280 |
+
# Replace the function in the code
|
| 281 |
+
code_lines[start:end] = new_func_lines
|
| 282 |
+
code = '\n'.join(code_lines)
|
| 283 |
+
except Exception as e:
|
| 284 |
+
print(f"Error inserting per-step audio: {e}")
|
| 285 |
+
# Fallback: do nothing
|
| 286 |
+
|
| 287 |
+
return code
|
| 288 |
+
except Exception as e:
|
| 289 |
+
print(f"Error generating code with Gemini: {e}")
|
| 290 |
+
raise e
|
backend/main.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
| 2 |
+
from fastapi.staticfiles import StaticFiles
|
| 3 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
import os
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import asyncio
|
| 8 |
+
|
| 9 |
+
# Load environment variables FIRST
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# ⚙️ DEPLOYMENT CONFIGURATION
|
| 13 |
+
# FFmpeg Configuration - Tries to find FFmpeg automatically
|
| 14 |
+
import shutil
|
| 15 |
+
|
| 16 |
+
# Check if ffmpeg is already in system PATH
|
| 17 |
+
if shutil.which('ffmpeg') is None:
|
| 18 |
+
# If not in PATH, try custom path (update this for your system)
|
| 19 |
+
# Common Windows path: r"C:\ffmpeg\bin"
|
| 20 |
+
# Linux/Mac: Usually already in PATH, no need to set
|
| 21 |
+
custom_ffmpeg_path = os.environ.get('FFMPEG_PATH', r"M:\Ap\ffmpeg-7.1.1-essentials_build\ffmpeg-7.1.1-essentials_build\bin")
|
| 22 |
+
|
| 23 |
+
if os.path.exists(custom_ffmpeg_path):
|
| 24 |
+
os.environ["PATH"] += os.pathsep + custom_ffmpeg_path
|
| 25 |
+
print(f"✅ FFmpeg found at: {custom_ffmpeg_path}")
|
| 26 |
+
else:
|
| 27 |
+
print("⚠️ FFmpeg not found in system PATH")
|
| 28 |
+
print(" Options:")
|
| 29 |
+
print(" 1. Install FFmpeg and add to system PATH")
|
| 30 |
+
print(" 2. Set FFMPEG_PATH environment variable")
|
| 31 |
+
print(f" 3. Update custom_ffmpeg_path in main.py (currently: {custom_ffmpeg_path})")
|
| 32 |
+
else:
|
| 33 |
+
print("✅ FFmpeg found in system PATH")
|
| 34 |
+
|
| 35 |
+
from teacher import generate_outline
|
| 36 |
+
from compiler import generate_manim_code
|
| 37 |
+
from runner import render_scene
|
| 38 |
+
from narrator import generate_narration_audio
|
| 39 |
+
|
| 40 |
+
app = FastAPI()
|
| 41 |
+
|
| 42 |
+
# Get the directory of the current script (backend/)
|
| 43 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 44 |
+
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
| 45 |
+
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
| 46 |
+
|
| 47 |
+
# Ensure the media directory exists
|
| 48 |
+
os.makedirs(MEDIA_DIR, exist_ok=True)
|
| 49 |
+
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
| 50 |
+
|
| 51 |
+
# Serve static frontend files
|
| 52 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 53 |
+
|
| 54 |
+
# Global Job Status
|
| 55 |
+
job_status = {
|
| 56 |
+
"stage": "idle", # idle, planning, coding, executing, success, failed
|
| 57 |
+
"message": "System Ready",
|
| 58 |
+
"video_path": None,
|
| 59 |
+
"error": None
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
class PromptRequest(BaseModel):
|
| 63 |
+
prompt: str
|
| 64 |
+
|
| 65 |
+
async def process_video_generation(prompt: str):
|
| 66 |
+
global job_status
|
| 67 |
+
try:
|
| 68 |
+
# 1. Planning
|
| 69 |
+
job_status["stage"] = "planning"
|
| 70 |
+
job_status["message"] = "Analyzing Prompt & Generating Outline..."
|
| 71 |
+
outline = await generate_outline(prompt)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# 2. Narrator: Generate per-step narration audio files
|
| 75 |
+
steps = outline.get("steps", [])
|
| 76 |
+
step_audio_paths = []
|
| 77 |
+
from narrator import generate_narration_audio
|
| 78 |
+
for idx, step in enumerate(steps):
|
| 79 |
+
narration = step.get("narration", "")
|
| 80 |
+
if narration:
|
| 81 |
+
audio_filename = f"step_{idx+1}_narration.mp3"
|
| 82 |
+
audio_path = generate_narration_audio(narration, filename=audio_filename)
|
| 83 |
+
step_audio_paths.append(audio_path)
|
| 84 |
+
else:
|
| 85 |
+
step_audio_paths.append(None)
|
| 86 |
+
|
| 87 |
+
# 3. Coding
|
| 88 |
+
job_status["stage"] = "coding"
|
| 89 |
+
job_status["message"] = "Generating Manim Script..."
|
| 90 |
+
code = await generate_manim_code(outline, step_audio_paths=step_audio_paths)
|
| 91 |
+
|
| 92 |
+
# 4. Executing
|
| 93 |
+
job_status["stage"] = "executing"
|
| 94 |
+
job_status["message"] = "Rendering Animation Frames..."
|
| 95 |
+
video_path = await render_scene(code)
|
| 96 |
+
|
| 97 |
+
# 5. Merge audio if available
|
| 98 |
+
if audio_path:
|
| 99 |
+
from narrator import merge_audio_video
|
| 100 |
+
video_path = merge_audio_video(video_path, audio_path)
|
| 101 |
+
|
| 102 |
+
# Success
|
| 103 |
+
relative_path = os.path.relpath(video_path, start=MEDIA_DIR).replace("\\", "/")
|
| 104 |
+
job_status["stage"] = "success"
|
| 105 |
+
job_status["message"] = "Render Complete!"
|
| 106 |
+
job_status["video_path"] = relative_path
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
error_msg = str(e)
|
| 110 |
+
print(f"Error generating video: {error_msg}")
|
| 111 |
+
job_status["stage"] = "failed"
|
| 112 |
+
job_status["message"] = "Process Failed"
|
| 113 |
+
job_status["error"] = error_msg
|
| 114 |
+
|
| 115 |
+
# Log error
|
| 116 |
+
with open("error.log", "w") as f:
|
| 117 |
+
f.write(error_msg)
|
| 118 |
+
import traceback
|
| 119 |
+
traceback.print_exc(file=f)
|
| 120 |
+
|
| 121 |
+
@app.get("/")
|
| 122 |
+
async def read_index():
|
| 123 |
+
return FileResponse(os.path.join(STATIC_DIR, 'index.html'))
|
| 124 |
+
|
| 125 |
+
@app.post("/generate")
|
| 126 |
+
async def generate_video(request: PromptRequest, background_tasks: BackgroundTasks):
|
| 127 |
+
global job_status
|
| 128 |
+
|
| 129 |
+
# Reset status
|
| 130 |
+
job_status = {
|
| 131 |
+
"stage": "planning",
|
| 132 |
+
"message": "Initializing...",
|
| 133 |
+
"video_path": None,
|
| 134 |
+
"error": None
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
# Start background task
|
| 138 |
+
background_tasks.add_task(process_video_generation, request.prompt)
|
| 139 |
+
|
| 140 |
+
return {"status": "started"}
|
| 141 |
+
|
| 142 |
+
@app.get("/status")
|
| 143 |
+
async def get_status():
|
| 144 |
+
return job_status
|
| 145 |
+
|
| 146 |
+
@app.get("/video/{path:path}")
|
| 147 |
+
async def get_video(path: str):
|
| 148 |
+
video_path = os.path.join(MEDIA_DIR, path)
|
| 149 |
+
if os.path.exists(video_path):
|
| 150 |
+
return FileResponse(video_path)
|
| 151 |
+
raise HTTPException(status_code=404, detail="Video not found")
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
import uvicorn
|
| 155 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
backend/models/Modelfile
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM ./Qwen2.5-Manim-3B-Instruct-q4_k_m.gguf
|
| 2 |
+
|
| 3 |
+
PARAMETER temperature 0.1
|
| 4 |
+
PARAMETER top_p 0.1
|
| 5 |
+
PARAMETER top_k 1
|
| 6 |
+
PARAMETER repeat_penalty 1.1
|
backend/narrator.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from gtts import gTTS
|
| 3 |
+
from moviepy import VideoFileClip, AudioFileClip, CompositeAudioClip
|
| 4 |
+
import uuid
|
| 5 |
+
|
| 6 |
+
# Get the directory of the current script (backend/)
|
| 7 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
+
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
| 9 |
+
AUDIO_DIR = os.path.join(MEDIA_DIR, "audio")
|
| 10 |
+
|
| 11 |
+
# Ensure audio directory exists
|
| 12 |
+
os.makedirs(AUDIO_DIR, exist_ok=True)
|
| 13 |
+
|
| 14 |
+
def generate_narration_audio(text: str, filename: str = None) -> str:
|
| 15 |
+
"""
|
| 16 |
+
Generates an MP3 audio file from the given text using gTTS.
|
| 17 |
+
Returns the absolute path to the generated audio file.
|
| 18 |
+
"""
|
| 19 |
+
try:
|
| 20 |
+
if not filename:
|
| 21 |
+
run_id = str(uuid.uuid4())
|
| 22 |
+
filename = f"narration_{run_id}.mp3"
|
| 23 |
+
|
| 24 |
+
# Save in BASE_DIR (backend/) so it's accessible to the script
|
| 25 |
+
filepath = os.path.join(BASE_DIR, filename)
|
| 26 |
+
|
| 27 |
+
tts = gTTS(text=text, lang='en', slow=False)
|
| 28 |
+
tts.save(filepath)
|
| 29 |
+
|
| 30 |
+
return filepath
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Error generating audio: {e}")
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
def merge_audio_video(video_path: str, audio_path: str) -> str:
|
| 36 |
+
"""
|
| 37 |
+
Merges the given audio file into the video file.
|
| 38 |
+
Returns the path to the new video file with audio.
|
| 39 |
+
If merging fails, returns the original video path.
|
| 40 |
+
"""
|
| 41 |
+
try:
|
| 42 |
+
if not audio_path or not os.path.exists(audio_path):
|
| 43 |
+
print("Audio file not found, skipping merge.")
|
| 44 |
+
return video_path
|
| 45 |
+
|
| 46 |
+
if not video_path or not os.path.exists(video_path):
|
| 47 |
+
print("Video file not found, skipping merge.")
|
| 48 |
+
return video_path
|
| 49 |
+
|
| 50 |
+
# Generate output path
|
| 51 |
+
video_dir = os.path.dirname(video_path)
|
| 52 |
+
video_filename = os.path.basename(video_path)
|
| 53 |
+
output_filename = f"narrated_{video_filename}"
|
| 54 |
+
output_path = os.path.join(video_dir, output_filename)
|
| 55 |
+
|
| 56 |
+
# Load clips
|
| 57 |
+
video_clip = VideoFileClip(video_path)
|
| 58 |
+
audio_clip = AudioFileClip(audio_path)
|
| 59 |
+
|
| 60 |
+
# Handle duration mismatch
|
| 61 |
+
# If audio is longer, we might need to loop video or cut audio.
|
| 62 |
+
# For simplicity, we'll let the video dictate duration, but if audio is longer,
|
| 63 |
+
# we might lose the end. Ideally, we'd extend the last frame of video.
|
| 64 |
+
# Here, we'll just set audio to video duration (cut off) or loop video?
|
| 65 |
+
# Let's just set the audio to the video.
|
| 66 |
+
|
| 67 |
+
# Better approach: If audio is longer, extend video? No, that's hard with compiled video.
|
| 68 |
+
# Let's just set the audio. If it's too long, it gets cut.
|
| 69 |
+
|
| 70 |
+
final_audio = audio_clip
|
| 71 |
+
|
| 72 |
+
# Set audio to video
|
| 73 |
+
final_video = video_clip.with_audio(final_audio)
|
| 74 |
+
|
| 75 |
+
# Write output
|
| 76 |
+
# codec='libx264' is standard. audio_codec='aac'
|
| 77 |
+
final_video.write_videofile(output_path, codec='libx264', audio_codec='aac', logger=None)
|
| 78 |
+
|
| 79 |
+
# Cleanup
|
| 80 |
+
video_clip.close()
|
| 81 |
+
audio_clip.close()
|
| 82 |
+
|
| 83 |
+
return output_path
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f"Error merging audio and video: {e}")
|
| 87 |
+
with open("merge_error.log", "w") as f:
|
| 88 |
+
f.write(str(e))
|
| 89 |
+
import traceback
|
| 90 |
+
traceback.print_exc(file=f)
|
| 91 |
+
# Return original video on failure
|
| 92 |
+
return video_path
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
manim
|
| 4 |
+
google-genai
|
| 5 |
+
pydantic
|
| 6 |
+
python-dotenv
|
| 7 |
+
gTTS
|
| 8 |
+
moviepy
|
| 9 |
+
ollama
|
backend/runner.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import uuid
|
| 4 |
+
|
| 5 |
+
# Get the directory of the current script (backend/)
|
| 6 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
+
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
| 8 |
+
|
| 9 |
+
async def render_scene(code: str):
|
| 10 |
+
# Create a unique ID for this run
|
| 11 |
+
run_id = str(uuid.uuid4())
|
| 12 |
+
filename = f"scene_{run_id}.py"
|
| 13 |
+
|
| 14 |
+
# Save code to file in the backend directory
|
| 15 |
+
filepath = os.path.join(BASE_DIR, filename)
|
| 16 |
+
with open(filepath, "w") as f:
|
| 17 |
+
f.write(code)
|
| 18 |
+
|
| 19 |
+
# Run Manim
|
| 20 |
+
# manim -qm --media_dir MEDIA_DIR filename.py GenScene
|
| 21 |
+
|
| 22 |
+
cmd = ["manim", "-qm", "--media_dir", MEDIA_DIR, filepath, "GenScene"]
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 26 |
+
print("Manim Output:", result.stdout)
|
| 27 |
+
except subprocess.CalledProcessError as e:
|
| 28 |
+
print("Manim Error:", e.stderr)
|
| 29 |
+
raise Exception(f"Manim failed: {e.stderr}")
|
| 30 |
+
|
| 31 |
+
# Construct expected output path
|
| 32 |
+
# Manim structure with --media_dir: {MEDIA_DIR}/videos/{filename_without_extension}/720p30/GenScene.mp4
|
| 33 |
+
|
| 34 |
+
video_folder = filename.replace(".py", "")
|
| 35 |
+
video_path = os.path.join(MEDIA_DIR, "videos", video_folder, "720p30", "GenScene.mp4")
|
| 36 |
+
|
| 37 |
+
if not os.path.exists(video_path):
|
| 38 |
+
raise Exception(f"Video file not found at {video_path}")
|
| 39 |
+
|
| 40 |
+
return video_path
|
backend/static/index.html
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta
|
| 6 |
+
name="viewport"
|
| 7 |
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
| 8 |
+
/>
|
| 9 |
+
<title>Animetrix AI | AI Animation Engine</title>
|
| 10 |
+
|
| 11 |
+
<!-- Fonts: Inter (Modern, Clean) -->
|
| 12 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 13 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 14 |
+
<link
|
| 15 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
| 16 |
+
rel="stylesheet"
|
| 17 |
+
/>
|
| 18 |
+
|
| 19 |
+
<!-- Tailwind CSS -->
|
| 20 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 21 |
+
|
| 22 |
+
<!-- Three.js -->
|
| 23 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
|
| 24 |
+
|
| 25 |
+
<!-- Custom Config -->
|
| 26 |
+
<script>
|
| 27 |
+
tailwind.config = {
|
| 28 |
+
theme: {
|
| 29 |
+
extend: {
|
| 30 |
+
fontFamily: {
|
| 31 |
+
sans: ["Inter", "sans-serif"],
|
| 32 |
+
},
|
| 33 |
+
colors: {
|
| 34 |
+
black: "#000000",
|
| 35 |
+
obsidian: "#0A0A0A",
|
| 36 |
+
orange: {
|
| 37 |
+
50: "#FFF7ED",
|
| 38 |
+
100: "#FFEDD5",
|
| 39 |
+
200: "#FED7AA",
|
| 40 |
+
300: "#FDBA74",
|
| 41 |
+
400: "#FB923C",
|
| 42 |
+
500: "#F97316",
|
| 43 |
+
600: "#EA580C",
|
| 44 |
+
700: "#C2410C",
|
| 45 |
+
800: "#9A3412",
|
| 46 |
+
900: "#7C2D12",
|
| 47 |
+
950: "#431407",
|
| 48 |
+
brand: "#FF6B00", // Vibrant Neon Orange
|
| 49 |
+
},
|
| 50 |
+
},
|
| 51 |
+
animation: {
|
| 52 |
+
glow: "glow 4s ease-in-out infinite",
|
| 53 |
+
float: "float 6s ease-in-out infinite",
|
| 54 |
+
},
|
| 55 |
+
keyframes: {
|
| 56 |
+
glow: {
|
| 57 |
+
"0%, 100%": { opacity: 0.3 },
|
| 58 |
+
"50%": { opacity: 0.6 },
|
| 59 |
+
},
|
| 60 |
+
float: {
|
| 61 |
+
"0%, 100%": { transform: "translateY(0)" },
|
| 62 |
+
"50%": { transform: "translateY(-10px)" },
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
},
|
| 66 |
+
},
|
| 67 |
+
};
|
| 68 |
+
</script>
|
| 69 |
+
|
| 70 |
+
<style>
|
| 71 |
+
body {
|
| 72 |
+
background-color: #000000;
|
| 73 |
+
color: #ffffff;
|
| 74 |
+
overflow-x: hidden;
|
| 75 |
+
width: 100vw;
|
| 76 |
+
min-height: 100vh;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
#canvas-container {
|
| 80 |
+
position: fixed;
|
| 81 |
+
top: 0;
|
| 82 |
+
left: 0;
|
| 83 |
+
width: 100%;
|
| 84 |
+
height: 100%;
|
| 85 |
+
z-index: 0;
|
| 86 |
+
pointer-events: none;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.glass {
|
| 90 |
+
background: rgba(10, 10, 10, 0.7);
|
| 91 |
+
backdrop-filter: blur(20px);
|
| 92 |
+
-webkit-backdrop-filter: blur(20px);
|
| 93 |
+
border: 1px solid rgba(255, 107, 0, 0.1);
|
| 94 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.glass-hover:hover {
|
| 98 |
+
border-color: rgba(255, 107, 0, 0.3);
|
| 99 |
+
background: rgba(15, 15, 15, 0.8);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.orange-gradient-text {
|
| 103 |
+
background: linear-gradient(135deg, #ff6b00 0%, #ffa500 100%);
|
| 104 |
+
-webkit-background-clip: text;
|
| 105 |
+
-webkit-text-fill-color: transparent;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn-orange {
|
| 109 |
+
background: linear-gradient(135deg, #ff6b00 0%, #ea580c 100%);
|
| 110 |
+
color: white;
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 113 |
+
position: relative;
|
| 114 |
+
overflow: hidden;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.btn-orange::after {
|
| 118 |
+
content: "";
|
| 119 |
+
position: absolute;
|
| 120 |
+
top: 0;
|
| 121 |
+
left: -100%;
|
| 122 |
+
width: 100%;
|
| 123 |
+
height: 100%;
|
| 124 |
+
background: linear-gradient(
|
| 125 |
+
90deg,
|
| 126 |
+
transparent,
|
| 127 |
+
rgba(255, 255, 255, 0.2),
|
| 128 |
+
transparent
|
| 129 |
+
);
|
| 130 |
+
transition: 0.5s;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.btn-orange:hover::after {
|
| 134 |
+
left: 100%;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.btn-orange:hover {
|
| 138 |
+
transform: translateY(-2px);
|
| 139 |
+
box-shadow: 0 10px 20px rgba(255, 107, 0, 0.3);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.input-dark {
|
| 143 |
+
background: rgba(0, 0, 0, 0.4);
|
| 144 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 145 |
+
transition: all 0.3s ease;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.input-dark:focus {
|
| 149 |
+
border-color: #ff6b00;
|
| 150 |
+
background: rgba(0, 0, 0, 0.6);
|
| 151 |
+
outline: none;
|
| 152 |
+
box-shadow: 0 0 0 2px rgba(255, 107, 0, 0.1);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Custom Scrollbar */
|
| 156 |
+
::-webkit-scrollbar {
|
| 157 |
+
width: 4px;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
::-webkit-scrollbar-track {
|
| 161 |
+
background: #000;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
::-webkit-scrollbar-thumb {
|
| 165 |
+
background: #333;
|
| 166 |
+
border-radius: 10px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
::-webkit-scrollbar-thumb:hover {
|
| 170 |
+
background: #ff6b00;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.mesh-gradient {
|
| 174 |
+
position: fixed;
|
| 175 |
+
top: 0;
|
| 176 |
+
left: 0;
|
| 177 |
+
width: 100%;
|
| 178 |
+
height: 100%;
|
| 179 |
+
background: radial-gradient(
|
| 180 |
+
at 0% 0%,
|
| 181 |
+
rgba(255, 107, 0, 0.15) 0px,
|
| 182 |
+
transparent 50%
|
| 183 |
+
),
|
| 184 |
+
radial-gradient(
|
| 185 |
+
at 100% 100%,
|
| 186 |
+
rgba(255, 107, 0, 0.1) 0px,
|
| 187 |
+
transparent 50%
|
| 188 |
+
);
|
| 189 |
+
z-index: -1;
|
| 190 |
+
}
|
| 191 |
+
</style>
|
| 192 |
+
</head>
|
| 193 |
+
|
| 194 |
+
<body class="antialiased font-sans">
|
| 195 |
+
<div class="mesh-gradient"></div>
|
| 196 |
+
<div id="canvas-container"></div>
|
| 197 |
+
|
| 198 |
+
<!-- Header -->
|
| 199 |
+
<nav
|
| 200 |
+
class="fixed top-0 w-full z-50 border-b border-white/5 bg-black/50 backdrop-blur-xl"
|
| 201 |
+
>
|
| 202 |
+
<div
|
| 203 |
+
class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"
|
| 204 |
+
>
|
| 205 |
+
<div class="flex items-center gap-3 group cursor-pointer">
|
| 206 |
+
<div
|
| 207 |
+
class="w-8 h-8 rounded bg-orange-brand flex items-center justify-center transition-transform group-hover:rotate-12"
|
| 208 |
+
>
|
| 209 |
+
<svg
|
| 210 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 211 |
+
class="h-5 w-5 text-black"
|
| 212 |
+
fill="none"
|
| 213 |
+
viewBox="0 0 24 24"
|
| 214 |
+
stroke="currentColor"
|
| 215 |
+
stroke-width="3"
|
| 216 |
+
>
|
| 217 |
+
<path
|
| 218 |
+
stroke-linecap="round"
|
| 219 |
+
stroke-linejoin="round"
|
| 220 |
+
d="M13 10V3L4 14h7v7l9-11h-7z"
|
| 221 |
+
/>
|
| 222 |
+
</svg>
|
| 223 |
+
</div>
|
| 224 |
+
<span class="font-extrabold text-xl tracking-tighter text-white"
|
| 225 |
+
>ANIMETRIX AI</span
|
| 226 |
+
>
|
| 227 |
+
</div>
|
| 228 |
+
<div class="flex items-center gap-6">
|
| 229 |
+
<div
|
| 230 |
+
class="hidden md:flex items-center gap-2 px-3 py-1 rounded-full bg-orange-950/30 border border-orange-900/50"
|
| 231 |
+
>
|
| 232 |
+
<span
|
| 233 |
+
class="w-2 h-2 rounded-full bg-orange-brand animate-pulse"
|
| 234 |
+
></span>
|
| 235 |
+
<span
|
| 236 |
+
class="text-[10px] font-bold text-orange-400 uppercase tracking-widest"
|
| 237 |
+
>Engine Online</span
|
| 238 |
+
>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
</nav>
|
| 243 |
+
|
| 244 |
+
<main class="relative z-10 pt-24 pb-20 px-6 max-w-7xl mx-auto">
|
| 245 |
+
<div class="flex flex-col lg:flex-row gap-12">
|
| 246 |
+
<!-- Left: Controls -->
|
| 247 |
+
<div class="w-full lg:w-5/12 space-y-8">
|
| 248 |
+
<div class="space-y-4">
|
| 249 |
+
<h1
|
| 250 |
+
class="text-5xl md:text-6xl font-black tracking-tighter leading-[0.9]"
|
| 251 |
+
>
|
| 252 |
+
ANIMATE <br />
|
| 253 |
+
<span class="orange-gradient-text">IDEAS.</span>
|
| 254 |
+
</h1>
|
| 255 |
+
<p class="text-zinc-400 text-lg font-medium max-w-sm leading-snug">
|
| 256 |
+
The next generation of educational content. Powered by AI,
|
| 257 |
+
rendered with Manim.
|
| 258 |
+
</p>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<div class="glass rounded-3xl p-8 space-y-6">
|
| 262 |
+
<div class="space-y-2">
|
| 263 |
+
<label
|
| 264 |
+
class="text-[10px] font-black text-orange-brand uppercase tracking-[0.2em]"
|
| 265 |
+
>Prompt Input</label
|
| 266 |
+
>
|
| 267 |
+
<textarea
|
| 268 |
+
id="promptInput"
|
| 269 |
+
rows="4"
|
| 270 |
+
class="w-full input-dark rounded-2xl p-5 text-white placeholder-zinc-600 text-base font-medium resize-none"
|
| 271 |
+
placeholder="Describe the concept you want to visualize..."
|
| 272 |
+
></textarea>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<div class="space-y-4">
|
| 276 |
+
<div class="flex flex-wrap gap-2">
|
| 277 |
+
<button
|
| 278 |
+
onclick="setPrompt('Pythagorean theorem visualization')"
|
| 279 |
+
class="px-4 py-2 rounded-xl bg-zinc-900/50 border border-white/5 text-xs font-semibold text-zinc-400 hover:text-orange-400 hover:border-orange-900/50 transition-all"
|
| 280 |
+
>
|
| 281 |
+
📐 Geometry
|
| 282 |
+
</button>
|
| 283 |
+
<button
|
| 284 |
+
onclick="setPrompt('Binary search algorithm step by step')"
|
| 285 |
+
class="px-4 py-2 rounded-xl bg-zinc-900/50 border border-white/5 text-xs font-semibold text-zinc-400 hover:text-orange-400 hover:border-orange-900/50 transition-all"
|
| 286 |
+
>
|
| 287 |
+
💻 Algorithms
|
| 288 |
+
</button>
|
| 289 |
+
<button
|
| 290 |
+
onclick="setPrompt('Structure of an atom with orbiting electrons')"
|
| 291 |
+
class="px-4 py-2 rounded-xl bg-zinc-900/50 border border-white/5 text-xs font-semibold text-zinc-400 hover:text-orange-400 hover:border-orange-900/50 transition-all"
|
| 292 |
+
>
|
| 293 |
+
⚛️ Physics
|
| 294 |
+
</button>
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
<button
|
| 298 |
+
onclick="generate()"
|
| 299 |
+
id="generateBtn"
|
| 300 |
+
class="w-full btn-orange py-5 rounded-2xl text-sm uppercase tracking-[0.15em] flex items-center justify-center gap-3"
|
| 301 |
+
>
|
| 302 |
+
<span>Generate Animation</span>
|
| 303 |
+
<svg
|
| 304 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 305 |
+
class="h-5 w-5"
|
| 306 |
+
viewBox="0 0 20 20"
|
| 307 |
+
fill="currentColor"
|
| 308 |
+
>
|
| 309 |
+
<path
|
| 310 |
+
fill-rule="evenodd"
|
| 311 |
+
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
|
| 312 |
+
clip-rule="evenodd"
|
| 313 |
+
/>
|
| 314 |
+
</svg>
|
| 315 |
+
</button>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
|
| 319 |
+
<div class="grid grid-cols-3 gap-4">
|
| 320 |
+
<div class="glass p-4 rounded-2xl text-center">
|
| 321 |
+
<div class="text-xl font-black text-white">4K</div>
|
| 322 |
+
<div
|
| 323 |
+
class="text-[8px] text-zinc-500 uppercase font-bold tracking-widest"
|
| 324 |
+
>
|
| 325 |
+
Ready
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
<div class="glass p-4 rounded-2xl text-center">
|
| 329 |
+
<div class="text-xl font-black text-white">AI</div>
|
| 330 |
+
<div
|
| 331 |
+
class="text-[8px] text-zinc-500 uppercase font-bold tracking-widest"
|
| 332 |
+
>
|
| 333 |
+
Driven
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
<div class="glass p-4 rounded-2xl text-center">
|
| 337 |
+
<div class="text-xl font-black text-white">FAST</div>
|
| 338 |
+
<div
|
| 339 |
+
class="text-[8px] text-zinc-500 uppercase font-bold tracking-widest"
|
| 340 |
+
>
|
| 341 |
+
Render
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
|
| 347 |
+
<!-- Right: Preview -->
|
| 348 |
+
<div class="w-full lg:w-7/12">
|
| 349 |
+
<div
|
| 350 |
+
class="relative aspect-video rounded-[2rem] overflow-hidden glass border-2 border-white/5 group"
|
| 351 |
+
>
|
| 352 |
+
<!-- Standby -->
|
| 353 |
+
<div
|
| 354 |
+
id="standbyScreen"
|
| 355 |
+
class="absolute inset-0 flex flex-col items-center justify-center bg-black/40"
|
| 356 |
+
>
|
| 357 |
+
<div class="relative">
|
| 358 |
+
<div
|
| 359 |
+
class="absolute inset-0 bg-orange-brand blur-3xl opacity-10 animate-pulse"
|
| 360 |
+
></div>
|
| 361 |
+
<div
|
| 362 |
+
class="w-20 h-20 rounded-full border border-orange-brand/20 flex items-center justify-center animate-float"
|
| 363 |
+
>
|
| 364 |
+
<svg
|
| 365 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 366 |
+
class="h-10 w-10 text-orange-brand"
|
| 367 |
+
fill="none"
|
| 368 |
+
viewBox="0 0 24 24"
|
| 369 |
+
stroke="currentColor"
|
| 370 |
+
>
|
| 371 |
+
<path
|
| 372 |
+
stroke-linecap="round"
|
| 373 |
+
stroke-linejoin="round"
|
| 374 |
+
stroke-width="1"
|
| 375 |
+
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
| 376 |
+
/>
|
| 377 |
+
<path
|
| 378 |
+
stroke-linecap="round"
|
| 379 |
+
stroke-linejoin="round"
|
| 380 |
+
stroke-width="1"
|
| 381 |
+
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
| 382 |
+
/>
|
| 383 |
+
</svg>
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
<p
|
| 387 |
+
class="mt-6 text-zinc-500 font-bold text-xs uppercase tracking-[0.3em]"
|
| 388 |
+
>
|
| 389 |
+
System Idle
|
| 390 |
+
</p>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
+
<!-- Video -->
|
| 394 |
+
<video
|
| 395 |
+
id="videoPlayer"
|
| 396 |
+
class="absolute inset-0 w-full h-full object-cover hidden"
|
| 397 |
+
controls
|
| 398 |
+
playsinline
|
| 399 |
+
>
|
| 400 |
+
<source id="videoSource" src="" type="video/mp4" />
|
| 401 |
+
</video>
|
| 402 |
+
|
| 403 |
+
<!-- Status Overlay -->
|
| 404 |
+
<div
|
| 405 |
+
id="statusSection"
|
| 406 |
+
class="hidden absolute inset-0 bg-black/90 backdrop-blur-2xl flex flex-col items-center justify-center p-12 z-20"
|
| 407 |
+
>
|
| 408 |
+
<div class="w-full max-w-sm space-y-8">
|
| 409 |
+
<div class="flex justify-center">
|
| 410 |
+
<div class="relative">
|
| 411 |
+
<div
|
| 412 |
+
class="absolute inset-0 bg-orange-brand blur-2xl opacity-20 animate-ping"
|
| 413 |
+
></div>
|
| 414 |
+
<div
|
| 415 |
+
class="w-16 h-16 rounded-2xl bg-orange-brand flex items-center justify-center shadow-2xl shadow-orange-brand/50"
|
| 416 |
+
>
|
| 417 |
+
<svg
|
| 418 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 419 |
+
class="h-8 w-8 text-black animate-spin"
|
| 420 |
+
fill="none"
|
| 421 |
+
viewBox="0 0 24 24"
|
| 422 |
+
stroke="currentColor"
|
| 423 |
+
>
|
| 424 |
+
<path
|
| 425 |
+
stroke-linecap="round"
|
| 426 |
+
stroke-linejoin="round"
|
| 427 |
+
stroke-width="3"
|
| 428 |
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
| 429 |
+
/>
|
| 430 |
+
</svg>
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<div class="text-center space-y-2">
|
| 436 |
+
<h3
|
| 437 |
+
class="text-2xl font-black text-white tracking-tight"
|
| 438 |
+
id="statusTitle"
|
| 439 |
+
>
|
| 440 |
+
Processing
|
| 441 |
+
</h3>
|
| 442 |
+
<p
|
| 443 |
+
class="text-orange-brand/60 text-xs font-bold uppercase tracking-widest"
|
| 444 |
+
id="statusMessage"
|
| 445 |
+
>
|
| 446 |
+
Initializing...
|
| 447 |
+
</p>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="space-y-4">
|
| 451 |
+
<div
|
| 452 |
+
class="h-1 w-full bg-zinc-900 rounded-full overflow-hidden"
|
| 453 |
+
>
|
| 454 |
+
<div
|
| 455 |
+
id="progressFill"
|
| 456 |
+
class="h-full bg-orange-brand w-0 transition-all duration-500 shadow-[0_0_15px_rgba(255,107,0,0.5)]"
|
| 457 |
+
></div>
|
| 458 |
+
</div>
|
| 459 |
+
<div
|
| 460 |
+
class="flex justify-between text-[8px] font-black text-zinc-600 uppercase tracking-[0.2em]"
|
| 461 |
+
>
|
| 462 |
+
<span>Plan</span>
|
| 463 |
+
<span>Code</span>
|
| 464 |
+
<span>Render</span>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
</div>
|
| 469 |
+
|
| 470 |
+
<!-- Error -->
|
| 471 |
+
<div
|
| 472 |
+
id="errorSection"
|
| 473 |
+
class="hidden absolute inset-0 bg-red-950/95 backdrop-blur-xl flex flex-col items-center justify-center p-12 text-center z-30"
|
| 474 |
+
>
|
| 475 |
+
<div
|
| 476 |
+
class="w-16 h-16 rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center mb-6"
|
| 477 |
+
>
|
| 478 |
+
<svg
|
| 479 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 480 |
+
class="h-8 w-8 text-red-500"
|
| 481 |
+
fill="none"
|
| 482 |
+
viewBox="0 0 24 24"
|
| 483 |
+
stroke="currentColor"
|
| 484 |
+
>
|
| 485 |
+
<path
|
| 486 |
+
stroke-linecap="round"
|
| 487 |
+
stroke-linejoin="round"
|
| 488 |
+
stroke-width="2"
|
| 489 |
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
| 490 |
+
/>
|
| 491 |
+
</svg>
|
| 492 |
+
</div>
|
| 493 |
+
<h3 class="text-xl font-black text-white mb-2">System Failure</h3>
|
| 494 |
+
<p
|
| 495 |
+
id="errorMessage"
|
| 496 |
+
class="text-red-400/60 text-xs font-mono bg-black/40 p-4 rounded-xl border border-red-500/10 max-w-md overflow-auto max-h-32"
|
| 497 |
+
>
|
| 498 |
+
Error details...
|
| 499 |
+
</p>
|
| 500 |
+
<button
|
| 501 |
+
onclick="resetUI()"
|
| 502 |
+
class="mt-8 px-8 py-3 bg-white text-black text-xs font-black uppercase tracking-widest rounded-xl hover:bg-zinc-200 transition-colors"
|
| 503 |
+
>
|
| 504 |
+
Reset Engine
|
| 505 |
+
</button>
|
| 506 |
+
</div>
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
</div>
|
| 510 |
+
</main>
|
| 511 |
+
|
| 512 |
+
<!-- Three.js Background -->
|
| 513 |
+
<script>
|
| 514 |
+
const scene = new THREE.Scene();
|
| 515 |
+
const camera = new THREE.PerspectiveCamera(
|
| 516 |
+
75,
|
| 517 |
+
window.innerWidth / window.innerHeight,
|
| 518 |
+
0.1,
|
| 519 |
+
1000
|
| 520 |
+
);
|
| 521 |
+
const renderer = new THREE.WebGLRenderer({
|
| 522 |
+
alpha: true,
|
| 523 |
+
antialias: true,
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 527 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 528 |
+
document
|
| 529 |
+
.getElementById("canvas-container")
|
| 530 |
+
.appendChild(renderer.domElement);
|
| 531 |
+
|
| 532 |
+
// Particles
|
| 533 |
+
const particlesGeometry = new THREE.BufferGeometry();
|
| 534 |
+
const count = 1500;
|
| 535 |
+
const positions = new Float32Array(count * 3);
|
| 536 |
+
const colors = new Float32Array(count * 3);
|
| 537 |
+
|
| 538 |
+
for (let i = 0; i < count * 3; i++) {
|
| 539 |
+
positions[i] = (Math.random() - 0.5) * 15;
|
| 540 |
+
// Orange to Black gradient for particles
|
| 541 |
+
const mixedColor = new THREE.Color("#FF6B00").lerp(
|
| 542 |
+
new THREE.Color("#000000"),
|
| 543 |
+
Math.random() * 0.8
|
| 544 |
+
);
|
| 545 |
+
colors[i] = mixedColor.r;
|
| 546 |
+
colors[i + 1] = mixedColor.g;
|
| 547 |
+
colors[i + 2] = mixedColor.b;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
particlesGeometry.setAttribute(
|
| 551 |
+
"position",
|
| 552 |
+
new THREE.BufferAttribute(positions, 3)
|
| 553 |
+
);
|
| 554 |
+
particlesGeometry.setAttribute(
|
| 555 |
+
"color",
|
| 556 |
+
new THREE.BufferAttribute(colors, 3)
|
| 557 |
+
);
|
| 558 |
+
|
| 559 |
+
const particlesMaterial = new THREE.PointsMaterial({
|
| 560 |
+
size: 0.015,
|
| 561 |
+
vertexColors: true,
|
| 562 |
+
transparent: true,
|
| 563 |
+
opacity: 0.4,
|
| 564 |
+
blending: THREE.AdditiveBlending,
|
| 565 |
+
});
|
| 566 |
+
|
| 567 |
+
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
|
| 568 |
+
scene.add(particles);
|
| 569 |
+
|
| 570 |
+
camera.position.z = 5;
|
| 571 |
+
|
| 572 |
+
let mouseX = 0;
|
| 573 |
+
let mouseY = 0;
|
| 574 |
+
|
| 575 |
+
document.addEventListener("mousemove", (e) => {
|
| 576 |
+
mouseX = e.clientX / window.innerWidth - 0.5;
|
| 577 |
+
mouseY = e.clientY / window.innerHeight - 0.5;
|
| 578 |
+
});
|
| 579 |
+
|
| 580 |
+
function animate() {
|
| 581 |
+
requestAnimationFrame(animate);
|
| 582 |
+
|
| 583 |
+
particles.rotation.y += 0.001;
|
| 584 |
+
particles.rotation.x += 0.0005;
|
| 585 |
+
|
| 586 |
+
camera.position.x += (mouseX * 2 - camera.position.x) * 0.05;
|
| 587 |
+
camera.position.y += (-mouseY * 2 - camera.position.y) * 0.05;
|
| 588 |
+
camera.lookAt(scene.position);
|
| 589 |
+
|
| 590 |
+
renderer.render(scene, camera);
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
animate();
|
| 594 |
+
|
| 595 |
+
window.addEventListener("resize", () => {
|
| 596 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 597 |
+
camera.updateProjectionMatrix();
|
| 598 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 599 |
+
});
|
| 600 |
+
</script>
|
| 601 |
+
|
| 602 |
+
<!-- Logic -->
|
| 603 |
+
<script>
|
| 604 |
+
let statusCheckInterval = null;
|
| 605 |
+
|
| 606 |
+
function setPrompt(text) {
|
| 607 |
+
const input = document.getElementById("promptInput");
|
| 608 |
+
input.value = text;
|
| 609 |
+
input.focus();
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
function resetUI() {
|
| 613 |
+
document.getElementById("errorSection").classList.add("hidden");
|
| 614 |
+
document.getElementById("statusSection").classList.add("hidden");
|
| 615 |
+
document.getElementById("standbyScreen").classList.remove("hidden");
|
| 616 |
+
document.getElementById("videoPlayer").classList.add("hidden");
|
| 617 |
+
document.getElementById("videoPlayer").pause();
|
| 618 |
+
|
| 619 |
+
const btn = document.getElementById("generateBtn");
|
| 620 |
+
btn.disabled = false;
|
| 621 |
+
btn.querySelector("span").textContent = "Generate Animation";
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
async function generate() {
|
| 625 |
+
const prompt = document.getElementById("promptInput").value.trim();
|
| 626 |
+
if (!prompt) return;
|
| 627 |
+
|
| 628 |
+
document.getElementById("statusSection").classList.remove("hidden");
|
| 629 |
+
document.getElementById("errorSection").classList.add("hidden");
|
| 630 |
+
document.getElementById("standbyScreen").classList.add("hidden");
|
| 631 |
+
|
| 632 |
+
const btn = document.getElementById("generateBtn");
|
| 633 |
+
btn.disabled = true;
|
| 634 |
+
btn.querySelector("span").textContent = "Processing...";
|
| 635 |
+
|
| 636 |
+
try {
|
| 637 |
+
const response = await fetch("/generate", {
|
| 638 |
+
method: "POST",
|
| 639 |
+
headers: { "Content-Type": "application/json" },
|
| 640 |
+
body: JSON.stringify({ prompt: prompt }),
|
| 641 |
+
});
|
| 642 |
+
if (!response.ok) throw new Error("Network response was not ok");
|
| 643 |
+
startStatusPolling();
|
| 644 |
+
} catch (error) {
|
| 645 |
+
showError(error.message);
|
| 646 |
+
}
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
function startStatusPolling() {
|
| 650 |
+
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
| 651 |
+
statusCheckInterval = setInterval(checkStatus, 1000);
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
async function checkStatus() {
|
| 655 |
+
try {
|
| 656 |
+
const response = await fetch("/status");
|
| 657 |
+
const status = await response.json();
|
| 658 |
+
updateUI(status);
|
| 659 |
+
if (status.stage === "success" || status.stage === "failed") {
|
| 660 |
+
clearInterval(statusCheckInterval);
|
| 661 |
+
}
|
| 662 |
+
} catch (error) {
|
| 663 |
+
console.error(error);
|
| 664 |
+
}
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
function updateUI(status) {
|
| 668 |
+
const titles = {
|
| 669 |
+
idle: "System Ready",
|
| 670 |
+
planning: "Strategizing",
|
| 671 |
+
coding: "Synthesizing Code",
|
| 672 |
+
executing: "Rendering Reality",
|
| 673 |
+
success: "Generation Complete",
|
| 674 |
+
failed: "System Error",
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
document.getElementById("statusTitle").textContent =
|
| 678 |
+
titles[status.stage] || "Processing";
|
| 679 |
+
document.getElementById("statusMessage").textContent = status.message;
|
| 680 |
+
|
| 681 |
+
const progressMap = {
|
| 682 |
+
idle: 0,
|
| 683 |
+
planning: 25,
|
| 684 |
+
coding: 50,
|
| 685 |
+
executing: 80,
|
| 686 |
+
success: 100,
|
| 687 |
+
failed: 100,
|
| 688 |
+
};
|
| 689 |
+
document.getElementById("progressFill").style.width =
|
| 690 |
+
(progressMap[status.stage] || 0) + "%";
|
| 691 |
+
|
| 692 |
+
if (status.stage === "success" && status.video_path) {
|
| 693 |
+
setTimeout(() => {
|
| 694 |
+
document.getElementById("statusSection").classList.add("hidden");
|
| 695 |
+
const video = document.getElementById("videoPlayer");
|
| 696 |
+
video.classList.remove("hidden");
|
| 697 |
+
document.getElementById("videoSource").src =
|
| 698 |
+
"/video/" + status.video_path;
|
| 699 |
+
video.load();
|
| 700 |
+
video.play();
|
| 701 |
+
|
| 702 |
+
const btn = document.getElementById("generateBtn");
|
| 703 |
+
btn.disabled = false;
|
| 704 |
+
btn.querySelector("span").textContent = "Generate New";
|
| 705 |
+
}, 800);
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
if (status.stage === "failed") {
|
| 709 |
+
showError(status.error || "Unknown error occurred");
|
| 710 |
+
}
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
function showError(msg) {
|
| 714 |
+
document.getElementById("statusSection").classList.add("hidden");
|
| 715 |
+
document.getElementById("errorSection").classList.remove("hidden");
|
| 716 |
+
document.getElementById("errorMessage").textContent = msg;
|
| 717 |
+
|
| 718 |
+
const btn = document.getElementById("generateBtn");
|
| 719 |
+
btn.disabled = false;
|
| 720 |
+
btn.querySelector("span").textContent = "Retry";
|
| 721 |
+
}
|
| 722 |
+
</script>
|
| 723 |
+
</body>
|
| 724 |
+
</html>
|
backend/teacher.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from google import genai
|
| 2 |
+
from google.genai import types
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
# Configure Gemini inside the function to ensure env vars are loaded
|
| 7 |
+
|
| 8 |
+
TEACHER_SYSTEM_PROMPT = """
|
| 9 |
+
You are the "Teacher Brain" of an educational video generator.
|
| 10 |
+
Your goal is to take a raw user prompt and convert it into a structured educational OUTLINE JSON.
|
| 11 |
+
|
| 12 |
+
Your output must be a valid JSON object with the following structure:
|
| 13 |
+
{
|
| 14 |
+
"topic": "The topic of the video",
|
| 15 |
+
"mode": "education",
|
| 16 |
+
"steps": [
|
| 17 |
+
{
|
| 18 |
+
"action": "draw shape",
|
| 19 |
+
"shape": "triangle|circle|square|line|arrow",
|
| 20 |
+
"scale": 1.0,
|
| 21 |
+
"label": "optional label",
|
| 22 |
+
"position": "optional position (e.g., LEFT, RIGHT, UP, DOWN)",
|
| 23 |
+
"narration": "A short, clear sentence describing this step, to be spoken as the animation happens."
|
| 24 |
+
},
|
| 25 |
+
// ... more steps
|
| 26 |
+
]
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
Rules:
|
| 30 |
+
- Each step MUST have a "narration" field: a short, clear sentence describing what is happening in that step, to be spoken exactly as the animation for that step occurs.
|
| 31 |
+
- Do NOT write any code.
|
| 32 |
+
- Do NOT write formulas in LaTeX (use simple text representation like a^2 + b^2 = c^2).
|
| 33 |
+
- Keep steps simple and sequential.
|
| 34 |
+
- Focus on visual explanation.
|
| 35 |
+
- The "action" field determines what happens. Supported actions: "draw shape", "show text", "wait", "clear".
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
async def generate_outline(prompt: str):
|
| 39 |
+
api_key = os.environ.get("GEMINI_API_KEY")
|
| 40 |
+
if not api_key:
|
| 41 |
+
raise ValueError("GEMINI_API_KEY not found in environment variables.")
|
| 42 |
+
|
| 43 |
+
client = genai.Client(api_key=api_key)
|
| 44 |
+
|
| 45 |
+
full_prompt = f"{TEACHER_SYSTEM_PROMPT}\n\nUSER PROMPT: {prompt}\n\nOUTPUT JSON:"
|
| 46 |
+
|
| 47 |
+
response = client.models.generate_content(
|
| 48 |
+
model='gemini-2.0-flash-exp',
|
| 49 |
+
contents=full_prompt
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
# cleanup response text to ensure it's valid JSON
|
| 54 |
+
text = response.text.strip()
|
| 55 |
+
if text.startswith("```json"):
|
| 56 |
+
text = text[7:]
|
| 57 |
+
if text.endswith("```"):
|
| 58 |
+
text = text[:-3]
|
| 59 |
+
|
| 60 |
+
return json.loads(text)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Error parsing Gemini response: {e}")
|
| 63 |
+
print(f"Raw response: {response.text}")
|
| 64 |
+
# Fallback or re-raise
|
| 65 |
+
raise e
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
animetrix-ai:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: animetrix-ai
|
| 7 |
+
ports:
|
| 8 |
+
- "8000:8000"
|
| 9 |
+
environment:
|
| 10 |
+
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 11 |
+
volumes:
|
| 12 |
+
- ./backend/media:/app/media
|
| 13 |
+
restart: unless-stopped
|
| 14 |
+
healthcheck:
|
| 15 |
+
test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
|
| 16 |
+
interval: 30s
|
| 17 |
+
timeout: 10s
|
| 18 |
+
retries: 3
|
| 19 |
+
start_period: 40s
|