SayedZahur786 commited on
Commit
a5e880f
·
0 Parent(s):

Added Project

Browse files
.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