Adityahulk commited on
Commit
6fc3143
·
0 Parent(s):

Restoring repo state for deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +10 -0
  2. .gitignore +67 -0
  3. DEPLOYMENT.md +38 -0
  4. Dockerfile +45 -0
  5. LICENSE +21 -0
  6. README.md +7 -0
  7. api_client.py +364 -0
  8. api_server.py +811 -0
  9. assets/manimator.png +0 -0
  10. docs/IMPLEMENTATION_COMPLETE.md +162 -0
  11. docs/IMPLEMENTATION_PLAN.md +293 -0
  12. ensure_rpc.sql +33 -0
  13. fix_columns_and_trigger.sql +52 -0
  14. fix_rls.sql +24 -0
  15. frontend/.env.example +14 -0
  16. frontend/.gitignore +30 -0
  17. frontend/README.md +35 -0
  18. frontend/eslint.config.mjs +18 -0
  19. frontend/next-env.d.ts +5 -0
  20. frontend/next.config.mjs +17 -0
  21. frontend/package-lock.json +0 -0
  22. frontend/package.json +39 -0
  23. frontend/postcss.config.mjs +7 -0
  24. frontend/public/file.svg +1 -0
  25. frontend/public/globe.svg +1 -0
  26. frontend/public/logo-full.jpg +0 -0
  27. frontend/public/logo-icon.jpg +0 -0
  28. frontend/public/logo.svg +11 -0
  29. frontend/public/next.svg +1 -0
  30. frontend/public/robots.txt +4 -0
  31. frontend/public/vercel.svg +1 -0
  32. frontend/public/window.svg +1 -0
  33. frontend/src/app/api-docs/page.tsx +166 -0
  34. frontend/src/app/api/auth/sync/route.ts +98 -0
  35. frontend/src/app/api/generate/route.ts +149 -0
  36. frontend/src/app/api/jobs/[id]/route.ts +77 -0
  37. frontend/src/app/api/videos/[id]/route.ts +44 -0
  38. frontend/src/app/app/page.tsx +12 -0
  39. frontend/src/app/auth/callback/route.ts +99 -0
  40. frontend/src/app/billing/page.tsx +157 -0
  41. frontend/src/app/favicon.ico +0 -0
  42. frontend/src/app/globals.css +74 -0
  43. frontend/src/app/layout.tsx +35 -0
  44. frontend/src/app/page.tsx +105 -0
  45. frontend/src/app/pricing/page.tsx +123 -0
  46. frontend/src/app/profile/page.tsx +136 -0
  47. frontend/src/components/app/VideoGenerator.tsx +602 -0
  48. frontend/src/components/auth/LoginButton.tsx +40 -0
  49. frontend/src/components/auth/UserAuth.tsx +124 -0
  50. frontend/src/components/landing/Footer.tsx +72 -0
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Our selected models, replace these with your own choices if you want to modify the models used
2
+ PROMPT_SCENE_GEN_MODEL=groq/llama-3.3-70b-versatile
3
+ PDF_SCENE_GEN_MODEL=gemini/gemini-1.5-flash
4
+ PDF_RETRY_MODEL=gemini/gemini-2.0-flash-exp #Optional, only if you want to retry the PDF generation
5
+ CODE_GEN_MODEL=openrouter/deepseek/deepseek-chat:free
6
+
7
+ # Use the LiteLLM convention of naming the API keys depending on the models you choose
8
+ GROQ_API_KEY=
9
+ OPENROUTER_API_KEY=
10
+ GEMINI_API_KEY=
.gitignore ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ /lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual environments
25
+ venv/
26
+ ENV/
27
+ env/
28
+ .venv
29
+
30
+ # IDE
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+
37
+ # OS
38
+ .DS_Store
39
+ .DS_Store?
40
+ ._*
41
+ .Spotlight-V100
42
+ .Trashes
43
+ ehthumbs.db
44
+ Thumbs.db
45
+
46
+ # Test output files
47
+ *.mp4
48
+ test_*.py
49
+
50
+ # Generated files
51
+ scene_*.py
52
+ scaling_*.py
53
+ generated_*.py
54
+ *.log
55
+
56
+ # Generated directories
57
+ media/
58
+ /jobs/
59
+ jobs_3d/
60
+
61
+ # Environment variables
62
+ .env
63
+ .env.local
64
+
65
+ # Project-specific
66
+ packages.txt
67
+ Error_free_videos.zip
DEPLOYMENT.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Configuration Guide
2
+
3
+ ## Environment Variables
4
+
5
+ For a secure production deployment, you must set the following environment variables.
6
+
7
+ ### 1. Frontend (Next.js)
8
+ These variables should be set in your Vercel project settings or `.env.production`.
9
+
10
+ | Variable | Description | Example Value |
11
+ |----------|-------------|---------------|
12
+ | `NEXTAUTH_URL` | The canonical URL of your site | `https://your-app.com` |
13
+ | `NEXTAUTH_SECRET` | A random string used to hash tokens | `openssl rand -base64 32` |
14
+ | `GOOGLE_CLIENT_ID` | OAuth Client ID from Google Cloud | `123...apps.googleusercontent.com` |
15
+ | `GOOGLE_CLIENT_SECRET` | OAuth Client Secret from Google Cloud | `GOCSPX-...` |
16
+ | `INTERNAL_API_KEY` | **CRITICAL**: Shared secret to talk to Python backend | `long-random-string-shared-with-backend` |
17
+ | `PYTHON_API_URL` | URL of your deployed Python backend | `https://api.your-app.com` |
18
+ | `DATABASE_URL` | Connection string for your production DB (e.g., Postgres) | `postgresql://user:pass@host:5432/db` |
19
+
20
+ > **Note on Database**: Currently, the app uses SQLite (`file:./dev.db`). For production, you should switch the `provider` in `prisma/schema.prisma` to `postgresql` or `mysql` and use a real database URL.
21
+
22
+ ### 2. Backend (Python / FastAPI)
23
+ These variables should be set in your backend hosting service (e.g., Railway, Render, AWS).
24
+
25
+ | Variable | Description | Example Value |
26
+ |----------|-------------|---------------|
27
+ | `INTERNAL_API_KEY` | **CRITICAL**: Must match the Frontend key exactly | `long-random-string-shared-with-backend` |
28
+ | `OPENAI_API_KEY` | For generating animation code | `sk-...` |
29
+ | `ELEVENLABS_API_KEY` | For generating voiceovers | `...` |
30
+ | `ANTHROPIC_API_KEY` | (Optional) If using Claude models | `sk-ant-...` |
31
+ | `CODE_GEN_MODEL` | Model to use for code generation | `gpt-4o` or `claude-3-5-sonnet-20240620` |
32
+
33
+ ## Security Checklist
34
+
35
+ 1. [ ] **Generate a Strong `INTERNAL_API_KEY`**: Use `openssl rand -hex 32` to generate a secure key. Set this on BOTH frontend and backend.
36
+ 2. [ ] **HTTPS Everywhere**: Ensure both your frontend and backend are served over HTTPS.
37
+ 3. [ ] **Database**: Do not use SQLite in production if you have multiple server instances (serverless). Use a managed Postgres database (e.g., Supabase, Neon, Railway).
38
+ 4. [ ] **CORS**: In `api_server.py`, update `allow_origins` to only allow your production frontend domain, not `*` or `localhost`.
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies for Manim and general usage
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ build-essential \
6
+ ffmpeg \
7
+ libcairo2-dev \
8
+ libpango1.0-dev \
9
+ texlive \
10
+ texlive-latex-extra \
11
+ texlive-fonts-recommended \
12
+ dvisvgm \
13
+ pkg-config \
14
+ python3-dev \
15
+ sudo \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Create a non-root user (Hugging Face requirement)
19
+ RUN useradd -m -u 1000 user
20
+ USER user
21
+ ENV HOME=/home/user \
22
+ PATH=/home/user/.local/bin:$PATH
23
+
24
+ WORKDIR $HOME/app
25
+
26
+ # Copy requirements first to leverage Docker cache
27
+ COPY --chown=user requirements.txt .
28
+
29
+ # Install Python dependencies
30
+ RUN pip install --no-cache-dir -r requirements.txt
31
+
32
+ # Copy the rest of the application
33
+ COPY --chown=user . .
34
+
35
+ # Make start script executable
36
+ RUN chmod +x start.sh
37
+
38
+ # Expose the port (Hugging Face Spaces uses 7860 by default)
39
+ EXPOSE 7860
40
+
41
+ # Set environment variables
42
+ ENV PYTHONUNBUFFERED=1
43
+
44
+ # Run the start script
45
+ CMD ["./start.sh"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 HyperCluster
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,7 @@
 
 
 
 
 
 
 
 
1
+ title: Vidsimplify AI Video Generator
2
+ emoji: 🎬
3
+ colorFrom: red
4
+ colorTo: purple
5
+ sdk: docker
6
+ pinned: false
7
+ app_port: 7860
api_client.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Python client library for Manim Video Generation API
3
+ """
4
+
5
+ import requests
6
+ import time
7
+ from typing import Optional, Dict, Any
8
+ from enum import Enum
9
+
10
+
11
+ class QualityLevel(str, Enum):
12
+ """Video quality levels"""
13
+ LOW = "low"
14
+ MEDIUM = "medium"
15
+ HIGH = "high"
16
+ ULTRA = "ultra"
17
+
18
+
19
+ class JobStatus(str, Enum):
20
+ """Job status"""
21
+ PENDING = "pending"
22
+ GENERATING_CODE = "generating_code"
23
+ RENDERING = "rendering"
24
+ COMPLETED = "completed"
25
+ FAILED = "failed"
26
+
27
+
28
+ class ManimVideoClient:
29
+ """Client for Manim Video Generation API"""
30
+
31
+ def __init__(self, base_url: str = "http://localhost:8000"):
32
+ """
33
+ Initialize client
34
+
35
+ Args:
36
+ base_url: Base URL of the API server
37
+ """
38
+ self.base_url = base_url.rstrip('/')
39
+ self.session = requests.Session()
40
+
41
+ def create_video(
42
+ self,
43
+ prompt: str,
44
+ quality: QualityLevel = QualityLevel.HIGH,
45
+ scene_name: Optional[str] = None
46
+ ) -> Dict[str, Any]:
47
+ """
48
+ Create a new video generation job
49
+
50
+ Args:
51
+ prompt: Detailed animation prompt
52
+ quality: Video quality level
53
+ scene_name: Optional custom scene name
54
+
55
+ Returns:
56
+ Job information including job_id
57
+
58
+ Example:
59
+ >>> client = ManimVideoClient()
60
+ >>> job = client.create_video(
61
+ ... prompt="Explain bubble sort with animations",
62
+ ... quality=QualityLevel.HIGH
63
+ ... )
64
+ >>> print(job['job_id'])
65
+ """
66
+ response = self.session.post(
67
+ f"{self.base_url}/api/videos",
68
+ json={
69
+ "prompt": prompt,
70
+ "quality": quality.value,
71
+ "scene_name": scene_name
72
+ }
73
+ )
74
+ response.raise_for_status()
75
+ return response.json()
76
+
77
+ def get_status(self, job_id: str) -> Dict[str, Any]:
78
+ """
79
+ Get job status
80
+
81
+ Args:
82
+ job_id: Job ID returned from create_video()
83
+
84
+ Returns:
85
+ Job status information
86
+
87
+ Example:
88
+ >>> status = client.get_status(job_id)
89
+ >>> print(status['status'])
90
+ >>> print(status['progress']['percentage'])
91
+ """
92
+ response = self.session.get(f"{self.base_url}/api/jobs/{job_id}")
93
+ response.raise_for_status()
94
+ return response.json()
95
+
96
+ def wait_for_completion(
97
+ self,
98
+ job_id: str,
99
+ poll_interval: int = 5,
100
+ timeout: Optional[int] = None,
101
+ callback: Optional[callable] = None
102
+ ) -> Dict[str, Any]:
103
+ """
104
+ Wait for job to complete
105
+
106
+ Args:
107
+ job_id: Job ID
108
+ poll_interval: Seconds between status checks
109
+ timeout: Maximum seconds to wait (None = no timeout)
110
+ callback: Optional function called with status on each poll
111
+
112
+ Returns:
113
+ Final job status
114
+
115
+ Example:
116
+ >>> def progress_callback(status):
117
+ ... print(f"{status['progress']['percentage']}%: {status['progress']['message']}")
118
+ >>>
119
+ >>> result = client.wait_for_completion(
120
+ ... job_id,
121
+ ... callback=progress_callback
122
+ ... )
123
+ """
124
+ start_time = time.time()
125
+
126
+ while True:
127
+ status = self.get_status(job_id)
128
+
129
+ if callback:
130
+ callback(status)
131
+
132
+ if status['status'] in [JobStatus.COMPLETED, JobStatus.FAILED]:
133
+ return status
134
+
135
+ if timeout and (time.time() - start_time > timeout):
136
+ raise TimeoutError(f"Job did not complete within {timeout} seconds")
137
+
138
+ time.sleep(poll_interval)
139
+
140
+ def download_video(self, job_id: str, output_path: str) -> str:
141
+ """
142
+ Download completed video
143
+
144
+ Args:
145
+ job_id: Job ID
146
+ output_path: Path to save video file
147
+
148
+ Returns:
149
+ Path to downloaded file
150
+
151
+ Example:
152
+ >>> client.download_video(job_id, "my_animation.mp4")
153
+ 'my_animation.mp4'
154
+ """
155
+ response = self.session.get(
156
+ f"{self.base_url}/api/videos/{job_id}",
157
+ stream=True
158
+ )
159
+ response.raise_for_status()
160
+
161
+ with open(output_path, 'wb') as f:
162
+ for chunk in response.iter_content(chunk_size=8192):
163
+ f.write(chunk)
164
+
165
+ return output_path
166
+
167
+ def list_jobs(self, limit: int = 50) -> Dict[str, Any]:
168
+ """
169
+ List all jobs
170
+
171
+ Args:
172
+ limit: Maximum number of jobs to return
173
+
174
+ Returns:
175
+ Dictionary with job list
176
+
177
+ Example:
178
+ >>> jobs = client.list_jobs(limit=10)
179
+ >>> for job in jobs['jobs']:
180
+ ... print(f"{job['job_id']}: {job['status']}")
181
+ """
182
+ response = self.session.get(
183
+ f"{self.base_url}/api/jobs",
184
+ params={"limit": limit}
185
+ )
186
+ response.raise_for_status()
187
+ return response.json()
188
+
189
+ def delete_job(self, job_id: str) -> Dict[str, Any]:
190
+ """
191
+ Delete a job and its files
192
+
193
+ Args:
194
+ job_id: Job ID
195
+
196
+ Returns:
197
+ Deletion confirmation
198
+
199
+ Example:
200
+ >>> client.delete_job(job_id)
201
+ {'message': 'Job deleted successfully', 'job_id': '...'}
202
+ """
203
+ response = self.session.delete(f"{self.base_url}/api/jobs/{job_id}")
204
+ response.raise_for_status()
205
+ return response.json()
206
+
207
+ def health_check(self) -> Dict[str, Any]:
208
+ """
209
+ Check API health
210
+
211
+ Returns:
212
+ Health status and statistics
213
+
214
+ Example:
215
+ >>> health = client.health_check()
216
+ >>> print(f"Total jobs: {health['jobs']['total']}")
217
+ """
218
+ response = self.session.get(f"{self.base_url}/health")
219
+ response.raise_for_status()
220
+ return response.json()
221
+
222
+ def generate_and_download(
223
+ self,
224
+ prompt: str,
225
+ output_path: str,
226
+ quality: QualityLevel = QualityLevel.HIGH,
227
+ poll_interval: int = 5,
228
+ progress_callback: Optional[callable] = None
229
+ ) -> Dict[str, Any]:
230
+ """
231
+ Complete workflow: create, wait, and download
232
+
233
+ Args:
234
+ prompt: Animation prompt
235
+ output_path: Where to save video
236
+ quality: Video quality
237
+ poll_interval: Status check interval
238
+ progress_callback: Optional progress callback
239
+
240
+ Returns:
241
+ Final job status
242
+
243
+ Example:
244
+ >>> def show_progress(status):
245
+ ... pct = status['progress']['percentage']
246
+ ... msg = status['progress']['message']
247
+ ... print(f"[{pct}%] {msg}")
248
+ >>>
249
+ >>> client.generate_and_download(
250
+ ... prompt="Explain merge sort",
251
+ ... output_path="merge_sort.mp4",
252
+ ... progress_callback=show_progress
253
+ ... )
254
+ """
255
+ # Create job
256
+ job = self.create_video(prompt, quality)
257
+ job_id = job['job_id']
258
+
259
+ print(f"✓ Job created: {job_id}")
260
+
261
+ # Wait for completion
262
+ result = self.wait_for_completion(
263
+ job_id,
264
+ poll_interval=poll_interval,
265
+ callback=progress_callback
266
+ )
267
+
268
+ if result['status'] == JobStatus.COMPLETED:
269
+ # Download video
270
+ self.download_video(job_id, output_path)
271
+ print(f"✓ Video downloaded: {output_path}")
272
+ return result
273
+ else:
274
+ raise Exception(f"Job failed: {result.get('error', 'Unknown error')}")
275
+
276
+
277
+ # ============================================================================
278
+ # CLI Tool
279
+ # ============================================================================
280
+
281
+ def main():
282
+ """Command-line interface"""
283
+ import argparse
284
+
285
+ parser = argparse.ArgumentParser(
286
+ description="Manim Video Generation API Client"
287
+ )
288
+ parser.add_argument(
289
+ "command",
290
+ choices=["create", "status", "download", "list", "generate"],
291
+ help="Command to execute"
292
+ )
293
+ parser.add_argument("--prompt", help="Animation prompt")
294
+ parser.add_argument("--job-id", help="Job ID")
295
+ parser.add_argument("--output", "-o", help="Output file path")
296
+ parser.add_argument(
297
+ "--quality",
298
+ choices=["low", "medium", "high", "ultra"],
299
+ default="high",
300
+ help="Video quality"
301
+ )
302
+ parser.add_argument(
303
+ "--url",
304
+ default="http://localhost:8000",
305
+ help="API base URL"
306
+ )
307
+
308
+ args = parser.parse_args()
309
+ client = ManimVideoClient(args.url)
310
+
311
+ if args.command == "create":
312
+ if not args.prompt:
313
+ print("Error: --prompt required")
314
+ return
315
+
316
+ job = client.create_video(args.prompt, QualityLevel(args.quality))
317
+ print(f"Job created: {job['job_id']}")
318
+ print(f"Status: {job['status']}")
319
+
320
+ elif args.command == "status":
321
+ if not args.job_id:
322
+ print("Error: --job-id required")
323
+ return
324
+
325
+ status = client.get_status(args.job_id)
326
+ print(f"Job ID: {status['job_id']}")
327
+ print(f"Status: {status['status']}")
328
+ print(f"Progress: {status['progress']['percentage']}%")
329
+ print(f"Message: {status['progress']['message']}")
330
+
331
+ elif args.command == "download":
332
+ if not args.job_id or not args.output:
333
+ print("Error: --job-id and --output required")
334
+ return
335
+
336
+ path = client.download_video(args.job_id, args.output)
337
+ print(f"Downloaded: {path}")
338
+
339
+ elif args.command == "list":
340
+ jobs = client.list_jobs()
341
+ print(f"Total jobs: {jobs['total']}")
342
+ for job in jobs['jobs']:
343
+ print(f" {job['job_id']}: {job['status']}")
344
+
345
+ elif args.command == "generate":
346
+ if not args.prompt or not args.output:
347
+ print("Error: --prompt and --output required")
348
+ return
349
+
350
+ def progress(status):
351
+ pct = status['progress']['percentage']
352
+ msg = status['progress']['message']
353
+ print(f"[{pct:3d}%] {msg}")
354
+
355
+ client.generate_and_download(
356
+ args.prompt,
357
+ args.output,
358
+ QualityLevel(args.quality),
359
+ progress_callback=progress
360
+ )
361
+
362
+
363
+ if __name__ == "__main__":
364
+ main()
api_server.py ADDED
@@ -0,0 +1,811 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unified FastAPI Video Generation Server
3
+ Supports text prompts, PDFs, and URLs for all animation categories.
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
7
+ from fastapi.responses import FileResponse, JSONResponse
8
+ from pydantic import BaseModel, Field
9
+ from typing import Optional, List, Dict, Any, Literal, Union
10
+ from enum import Enum
11
+ import uuid
12
+ import os
13
+ import json
14
+ import subprocess
15
+ import shutil
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ import asyncio
19
+ import logging
20
+ import base64
21
+
22
+ # Load environment variables from .env file
23
+ from dotenv import load_dotenv
24
+ load_dotenv()
25
+
26
+ from manimator.api.animation_generation import generate_animation_response
27
+ from manimator.utils.code_fixer import CodeFixer
28
+ # from manimator.api.input_processor import process_input
29
+
30
+ # Configure logging
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
34
+ )
35
+ logger = logging.getLogger("api_server_unified")
36
+
37
+ # ============================================================================
38
+ # Configuration
39
+ # ============================================================================
40
+
41
+ class Config:
42
+ """Application configuration"""
43
+ BASE_DIR = Path(__file__).parent
44
+ JOBS_DIR = BASE_DIR / "jobs"
45
+ VIDEOS_DIR = BASE_DIR / "media" / "videos"
46
+ MAX_JOB_AGE_DAYS = 7
47
+
48
+ # Ensure directories exist
49
+ JOBS_DIR.mkdir(exist_ok=True)
50
+ VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
51
+
52
+
53
+ # ============================================================================
54
+ # Models
55
+ # ============================================================================
56
+
57
+ class JobStatus(str, Enum):
58
+ """Job status enumeration"""
59
+ PENDING = "pending"
60
+ GENERATING_CODE = "generating_code"
61
+ RENDERING = "rendering"
62
+ COMPLETED = "completed"
63
+ FAILED = "failed"
64
+
65
+
66
+ class QualityLevel(str, Enum):
67
+ """Video quality levels"""
68
+ LOW = "low" # 480p15
69
+ MEDIUM = "medium" # 720p30
70
+ HIGH = "high" # 1080p60
71
+ ULTRA = "ultra" # 4K60
72
+
73
+
74
+ class AnimationCategory(str, Enum):
75
+ """Animation categories"""
76
+ TECH_SYSTEM = "tech_system"
77
+ PRODUCT_STARTUP = "product_startup"
78
+ MATHEMATICAL = "mathematical"
79
+
80
+
81
+ QUALITY_FLAGS = {
82
+ QualityLevel.LOW: "-pql",
83
+ QualityLevel.MEDIUM: "-pqm",
84
+ QualityLevel.HIGH: "-pqh",
85
+ QualityLevel.ULTRA: "-pqk",
86
+ }
87
+
88
+
89
+ class VideoRequest(BaseModel):
90
+ """Request model for video generation"""
91
+ input_type: Literal["text", "pdf", "url"] = Field(..., description="Type of input")
92
+ input_data: Union[str, bytes] = Field(..., description="Input data (text prompt, base64 PDF, or URL)")
93
+ quality: QualityLevel = Field(default=QualityLevel.HIGH, description="Video quality level")
94
+ category: AnimationCategory = Field(default=AnimationCategory.MATHEMATICAL, description="Animation category")
95
+ scene_name: Optional[str] = Field(default=None, description="Custom scene class name")
96
+
97
+ class Config:
98
+ json_schema_extra = {
99
+ "example": {
100
+ "input_type": "text",
101
+ "input_data": "Explain how a distributed system handles requests",
102
+ "quality": "high",
103
+ "category": "tech_system"
104
+ }
105
+ }
106
+
107
+
108
+ class JobResponse(BaseModel):
109
+ """Response model for job creation"""
110
+ job_id: str
111
+ status: JobStatus
112
+ message: str
113
+ created_at: str
114
+
115
+
116
+ class JobStatusResponse(BaseModel):
117
+ """Response model for job status"""
118
+ job_id: str
119
+ status: JobStatus
120
+ category: str
121
+ progress: Dict[str, Any]
122
+ created_at: str
123
+ updated_at: str
124
+ error: Optional[str] = None
125
+ video_url: Optional[str] = None
126
+ duration: Optional[float] = None
127
+
128
+
129
+ # ============================================================================
130
+ # Job Manager
131
+ # ============================================================================
132
+
133
+ class JobManager:
134
+ """Manages video generation jobs"""
135
+
136
+ def __init__(self):
137
+ self.jobs: Dict[str, Dict] = {}
138
+ self._load_existing_jobs()
139
+ self._cleanup_task = None
140
+
141
+ def _load_existing_jobs(self):
142
+ """Load existing jobs from disk"""
143
+ for job_file in Config.JOBS_DIR.glob("*.json"):
144
+ try:
145
+ with open(job_file) as f:
146
+ job_data = json.load(f)
147
+ self.jobs[job_data["job_id"]] = job_data
148
+ except Exception as e:
149
+ logger.error(f"Error loading job {job_file}: {e}")
150
+
151
+ def create_job(
152
+ self,
153
+ input_type: str,
154
+ input_data: Union[str, bytes],
155
+ quality: QualityLevel,
156
+ category: AnimationCategory,
157
+ scene_name: Optional[str] = None
158
+ ) -> str:
159
+ """Create a new job"""
160
+ job_id = str(uuid.uuid4())
161
+
162
+ if not scene_name:
163
+ scene_name = f"Scene_{uuid.uuid4().hex[:8]}"
164
+
165
+ job_data = {
166
+ "job_id": job_id,
167
+ "status": JobStatus.PENDING,
168
+ "input_type": input_type,
169
+ "quality": quality,
170
+ "category": category.value,
171
+ "scene_name": scene_name,
172
+ "created_at": datetime.now().isoformat(),
173
+ "updated_at": datetime.now().isoformat(),
174
+ "progress": {
175
+ "stage": "queued",
176
+ "percentage": 0,
177
+ "message": "Job queued for processing"
178
+ },
179
+ "error": None,
180
+ "video_path": None,
181
+ "code_path": None,
182
+ }
183
+
184
+ # Store input data (truncate if too long for display)
185
+ if input_type == "text":
186
+ job_data["input_preview"] = str(input_data)[:200] + "..." if len(str(input_data)) > 200 else str(input_data)
187
+ elif input_type == "url":
188
+ job_data["input_preview"] = str(input_data)
189
+ else:
190
+ job_data["input_preview"] = "[PDF file]"
191
+
192
+ self.jobs[job_id] = job_data
193
+ self._save_job(job_id)
194
+ return job_id
195
+
196
+ def update_job(self, job_id: str, **kwargs):
197
+ """Update job data"""
198
+ if job_id not in self.jobs:
199
+ return
200
+
201
+ self.jobs[job_id].update(kwargs)
202
+ self.jobs[job_id]["updated_at"] = datetime.now().isoformat()
203
+ self._save_job(job_id)
204
+
205
+ def get_job(self, job_id: str) -> Optional[Dict]:
206
+ """Get job by ID"""
207
+ return self.jobs.get(job_id)
208
+
209
+ def _save_job(self, job_id: str):
210
+ """Save job to disk"""
211
+ # Ensure directory exists
212
+ Config.JOBS_DIR.mkdir(parents=True, exist_ok=True)
213
+ job_file = Config.JOBS_DIR / f"{job_id}.json"
214
+ with open(job_file, 'w') as f:
215
+ json.dump(self.jobs[job_id], f, indent=2)
216
+
217
+ def list_jobs(self, limit: int = 50) -> List[Dict]:
218
+ """List recent jobs"""
219
+ jobs = sorted(
220
+ self.jobs.values(),
221
+ key=lambda x: x["created_at"],
222
+ reverse=True
223
+ )
224
+ return jobs[:limit]
225
+
226
+ def start_periodic_cleanup(self):
227
+ """Start periodic cleanup task (call this after event loop is running)"""
228
+ if self._cleanup_task is None:
229
+ self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
230
+
231
+ async def _periodic_cleanup(self):
232
+ """Periodic cleanup of old jobs and voiceover cache"""
233
+ import time
234
+ # Wait a bit before starting cleanup
235
+ await asyncio.sleep(60)
236
+ while True:
237
+ try:
238
+ await asyncio.sleep(3600) # Run every hour
239
+ await self._cleanup_old_jobs()
240
+ await self._cleanup_old_voiceovers()
241
+ except Exception as e:
242
+ logger.warning(f"Periodic cleanup error: {e}")
243
+
244
+ async def _cleanup_old_jobs(self):
245
+ """Remove old job files and their associated data"""
246
+ cutoff_date = datetime.now().timestamp() - (Config.MAX_JOB_AGE_DAYS * 24 * 3600)
247
+ removed_count = 0
248
+
249
+ for job_id, job_data in list(self.jobs.items()):
250
+ try:
251
+ job_time = datetime.fromisoformat(job_data["created_at"]).timestamp()
252
+ if job_time < cutoff_date:
253
+ # Remove job file
254
+ job_file = Config.JOBS_DIR / f"{job_id}.json"
255
+ if job_file.exists():
256
+ job_file.unlink()
257
+
258
+ # Remove from memory
259
+ del self.jobs[job_id]
260
+ removed_count += 1
261
+ except Exception as e:
262
+ logger.warning(f"Error cleaning up job {job_id[:8]}: {e}")
263
+
264
+ if removed_count > 0:
265
+ logger.info(f"🧹 Cleaned up {removed_count} old jobs")
266
+
267
+ async def _cleanup_old_voiceovers(self):
268
+ """Clean up old voiceover cache files (keep recent ones)"""
269
+ import time
270
+ try:
271
+ # Clean both ElevenLabs and gTTS cache
272
+ for service_dir in ["elevenlabs", "gtts"]:
273
+ voiceover_dir = Config.BASE_DIR / "media" / "voiceover" / service_dir
274
+ if not voiceover_dir.exists():
275
+ continue
276
+
277
+ # Keep voiceover files from last 7 days
278
+ cutoff_time = time.time() - (7 * 24 * 3600)
279
+ removed_count = 0
280
+
281
+ for voice_file in voiceover_dir.glob("*.mp3"):
282
+ try:
283
+ if voice_file.stat().st_mtime < cutoff_time:
284
+ voice_file.unlink()
285
+ removed_count += 1
286
+ except Exception:
287
+ pass
288
+
289
+ if removed_count > 0:
290
+ logger.info(f"🧹 Cleaned up {removed_count} old {service_dir} voiceover files")
291
+ except Exception as e:
292
+ logger.warning(f"Error cleaning up voiceovers: {e}")
293
+
294
+
295
+ # ============================================================================
296
+ # Video Generator
297
+ # ============================================================================
298
+
299
+ class VideoGenerator:
300
+ """Handles video generation workflow"""
301
+
302
+ def __init__(self, job_manager: JobManager):
303
+ self.job_manager = job_manager
304
+
305
+ async def generate_video(self, job_id: str):
306
+ """Generate video for a job"""
307
+ job = self.job_manager.get_job(job_id)
308
+ if not job:
309
+ return
310
+
311
+ logger.info(f"🎬 Starting video generation for job {job_id[:8]}...")
312
+
313
+ try:
314
+ # Stage 2: Generate Manim code
315
+ logger.info(f"🤖 Generating Manim code for job {job_id[:8]}...")
316
+ self.job_manager.update_job(
317
+ job_id,
318
+ status=JobStatus.GENERATING_CODE,
319
+ progress={
320
+ "stage": "generating_code",
321
+ "percentage": 30,
322
+ "message": "Generating Manim code using AI..."
323
+ }
324
+ )
325
+
326
+ # Pass raw input to generation function which now handles processing
327
+ code = generate_animation_response(
328
+ input_data=job.get("input_data", ""),
329
+ input_type=job["input_type"],
330
+ category=job["category"]
331
+ )
332
+
333
+ logger.info(f"✅ Code generation complete for job {job_id[:8]}...")
334
+
335
+ # Save code
336
+ code_file = Config.BASE_DIR / f"scene_{job_id}.py"
337
+ with open(code_file, 'w') as f:
338
+ f.write(code)
339
+
340
+ logger.info(f"💾 Code saved to {code_file.name}")
341
+
342
+ # Ensure voiceover directories exist
343
+ voiceover_dir = Config.BASE_DIR / "media" / "voiceover"
344
+ (voiceover_dir / "elevenlabs").mkdir(parents=True, exist_ok=True)
345
+ (voiceover_dir / "gtts").mkdir(parents=True, exist_ok=True)
346
+
347
+ self.job_manager.update_job(
348
+ job_id,
349
+ code_path=str(code_file),
350
+ progress={
351
+ "stage": "code_generated",
352
+ "percentage": 50,
353
+ "message": "Code generated successfully"
354
+ }
355
+ )
356
+
357
+ # Stage 3: Render video with Self-Healing Loop
358
+ fixer = CodeFixer()
359
+ max_retries = 3
360
+ video_path = None
361
+
362
+ for attempt in range(max_retries):
363
+ try:
364
+ logger.info(f"🎥 Starting Manim rendering for job {job_id[:8]} (Attempt {attempt+1}/{max_retries})...")
365
+
366
+ self.job_manager.update_job(
367
+ job_id,
368
+ status=JobStatus.RENDERING,
369
+ progress={
370
+ "stage": "rendering",
371
+ "percentage": 60 + (attempt * 5),
372
+ "message": f"Rendering video (Attempt {attempt+1})..."
373
+ }
374
+ )
375
+
376
+ video_path = await self._render_video(
377
+ code_file,
378
+ job["scene_name"],
379
+ QualityLevel(job["quality"])
380
+ )
381
+
382
+ # If successful, break the loop
383
+ break
384
+
385
+ except Exception as e:
386
+ error_msg = str(e)
387
+ logger.warning(f"⚠️ Rendering failed on attempt {attempt+1}: {error_msg}")
388
+
389
+ if attempt < max_retries - 1:
390
+ # Try to fix the code
391
+ logger.info(f"🔧 Attempting to auto-fix code for job {job_id[:8]}...")
392
+
393
+ self.job_manager.update_job(
394
+ job_id,
395
+ progress={
396
+ "stage": "fixing_code",
397
+ "percentage": 60 + (attempt * 5),
398
+ "message": f"Fixing rendering error..."
399
+ }
400
+ )
401
+
402
+ # Read current code
403
+ with open(code_file, 'r') as f:
404
+ current_code = f.read()
405
+
406
+ # Fix code using LLM
407
+ fixed_code = fixer.fix_runtime_error(current_code, error_msg)
408
+
409
+ # Save fixed code
410
+ with open(code_file, 'w') as f:
411
+ f.write(fixed_code)
412
+
413
+ logger.info(f"💾 Saved fixed code for job {job_id[:8]}")
414
+ else:
415
+ # Out of retries, re-raise exception
416
+ raise e
417
+
418
+ logger.info(f"✅ Video rendering complete for job {job_id[:8]}...")
419
+
420
+ # Stage 4: Complete
421
+ self.job_manager.update_job(
422
+ job_id,
423
+ status=JobStatus.COMPLETED,
424
+ video_path=str(video_path),
425
+ progress={
426
+ "stage": "completed",
427
+ "percentage": 100,
428
+ "message": "Video generation completed successfully!"
429
+ }
430
+ )
431
+
432
+ # Stage 5: Cleanup intermediate files in background
433
+ asyncio.create_task(self._cleanup_intermediate_files(job_id, code_file, video_path))
434
+
435
+ except Exception as e:
436
+ logger.error(f"❌ Error generating video for job {job_id[:8]}: {str(e)}")
437
+ self.job_manager.update_job(
438
+ job_id,
439
+ status=JobStatus.FAILED,
440
+ error=str(e),
441
+ progress={
442
+ "stage": "failed",
443
+ "percentage": 0,
444
+ "message": f"Error: {str(e)}"
445
+ }
446
+ )
447
+
448
+ async def _render_video(self, code_file: Path, scene_name: str, quality: QualityLevel) -> Path:
449
+ """Render Manim scene to video"""
450
+ quality_flag = QUALITY_FLAGS[quality]
451
+
452
+ # Ensure ALL media directories exist before rendering
453
+ media_dir = Config.BASE_DIR / "media"
454
+ voiceover_dir = media_dir / "voiceover" / "elevenlabs"
455
+ voiceover_dir.mkdir(parents=True, exist_ok=True)
456
+
457
+ # Also create gTTS cache directory in case of fallback
458
+ gtts_dir = media_dir / "voiceover" / "gtts"
459
+ gtts_dir.mkdir(parents=True, exist_ok=True)
460
+
461
+ # Create videos directory structure
462
+ Config.VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
463
+
464
+ cmd = [
465
+ "manim",
466
+ quality_flag,
467
+ "--media_dir",
468
+ str(Config.BASE_DIR / "media"),
469
+ str(code_file),
470
+ scene_name,
471
+ ]
472
+
473
+ # Set working directory to base dir to ensure relative paths work
474
+ env = os.environ.copy()
475
+ # Set MEDIA_DIR as absolute path to help voiceover services find cache directory
476
+ env["MEDIA_DIR"] = str(media_dir.resolve())
477
+
478
+ process = await asyncio.create_subprocess_exec(
479
+ *cmd,
480
+ stdout=asyncio.subprocess.PIPE,
481
+ stderr=asyncio.subprocess.PIPE,
482
+ cwd=str(Config.BASE_DIR),
483
+ env=env
484
+ )
485
+
486
+ stdout, stderr = await process.communicate()
487
+
488
+ if process.returncode != 0:
489
+ error_output = stderr.decode()[-500:] if stderr else "Unknown error"
490
+ raise Exception(f"Manim rendering failed: {error_output}")
491
+
492
+ # Find generated video
493
+ quality_dir = {
494
+ QualityLevel.LOW: "480p15",
495
+ QualityLevel.MEDIUM: "720p30",
496
+ QualityLevel.HIGH: "1080p60",
497
+ QualityLevel.ULTRA: "2160p60"
498
+ }[quality]
499
+
500
+ video_dir = Config.VIDEOS_DIR / code_file.stem / quality_dir
501
+ video_files = list(video_dir.glob("*.mp4"))
502
+
503
+ if not video_files:
504
+ raise Exception(f"No video file found in {video_dir}")
505
+
506
+ video_path = video_files[0]
507
+ logger.info(f"📹 Found video: {video_path.name}")
508
+
509
+ return video_path
510
+
511
+ async def _cleanup_intermediate_files(self, job_id: str, code_file: Path, final_video_path: Path):
512
+ """
513
+ Clean up intermediate files in background after video is successfully created.
514
+ Removes: scene code files, partial videos, voiceover files (keeps final video).
515
+
516
+ Args:
517
+ job_id: Job ID
518
+ code_file: Path to generated scene code file
519
+ final_video_path: Path to final rendered video (keep this)
520
+ """
521
+ try:
522
+ logger.info(f"🧹 Starting cleanup for job {job_id[:8]}...")
523
+
524
+ # 1. Remove scene code file
525
+ if code_file.exists():
526
+ try:
527
+ code_file.unlink()
528
+ logger.info(f" ✅ Removed scene code: {code_file.name}")
529
+ except Exception as e:
530
+ logger.warning(f" ⚠️ Could not remove scene code: {e}")
531
+
532
+ # 2. Remove partial video files (keep only final video)
533
+ # Find all video files in the scene directory
534
+ scene_video_dir = Config.VIDEOS_DIR / code_file.stem
535
+ if scene_video_dir.exists():
536
+ # Keep only the final video, remove all other quality versions and partial files
537
+ final_video_name = final_video_path.name
538
+ for quality_dir in scene_video_dir.iterdir():
539
+ if quality_dir.is_dir():
540
+ for video_file in quality_dir.glob("*.mp4"):
541
+ # Keep only the final video file
542
+ if video_file.name != final_video_name:
543
+ try:
544
+ video_file.unlink()
545
+ logger.info(f" ✅ Removed partial video: {video_file.name}")
546
+ except Exception as e:
547
+ logger.warning(f" ⚠️ Could not remove partial video: {e}")
548
+
549
+ # Remove partial movie files directory if exists
550
+ partial_dir = quality_dir / "partial_movie_files"
551
+ if partial_dir.exists():
552
+ try:
553
+ shutil.rmtree(partial_dir)
554
+ logger.info(f" ✅ Removed partial movie files directory")
555
+ except Exception as e:
556
+ logger.warning(f" ⚠️ Could not remove partial files: {e}")
557
+
558
+ # 3. Remove voiceover files for this job (they're cached, so safe to remove)
559
+ # Voiceover files are cached by text hash, so we can't easily identify job-specific ones
560
+ # Instead, we'll clean up old voiceover files periodically (not per-job)
561
+ # This is handled by a separate cleanup task
562
+
563
+ # 4. Remove any temporary files in media directory for this scene
564
+ scene_media_dir = Config.BASE_DIR / "media" / "videos" / code_file.stem
565
+ if scene_media_dir.exists():
566
+ # Remove text SVGs, images, etc. but keep the final video directory structure
567
+ for item in scene_media_dir.iterdir():
568
+ if item.is_file() and item.suffix in ['.svg', '.png', '.jpg', '.txt', '.srt']:
569
+ try:
570
+ item.unlink()
571
+ logger.info(f" ✅ Removed temporary file: {item.name}")
572
+ except Exception as e:
573
+ logger.warning(f" ⚠️ Could not remove temp file: {e}")
574
+
575
+ logger.info(f"✅ Cleanup completed for job {job_id[:8]}")
576
+
577
+ except Exception as e:
578
+ # Don't fail the job if cleanup fails
579
+ logger.warning(f"⚠️ Cleanup error for job {job_id[:8]}: {e}")
580
+
581
+
582
+ # ============================================================================
583
+ # FastAPI Application
584
+ # ============================================================================
585
+
586
+ app = FastAPI(
587
+ title="Unified Manim Video Generation API",
588
+ description="Generate educational animation videos from text, PDFs, or URLs",
589
+ version="2.0.0",
590
+ docs_url="/docs",
591
+ redoc_url="/redoc"
592
+ )
593
+
594
+ from fastapi.middleware.cors import CORSMiddleware
595
+
596
+ app.add_middleware(
597
+ CORSMiddleware,
598
+ allow_origins=["http://localhost:3000", "http://localhost:8000"],
599
+ allow_credentials=True,
600
+ allow_methods=["*"],
601
+ allow_headers=["*"],
602
+ )
603
+
604
+ # Security
605
+ from fastapi import Security, HTTPException, status
606
+ from fastapi.security.api_key import APIKeyHeader
607
+
608
+ API_KEY_NAME = "X-API-KEY"
609
+ API_KEY = os.getenv("INTERNAL_API_KEY")
610
+
611
+ if not API_KEY:
612
+ logger.warning("⚠️ INTERNAL_API_KEY is not set. API is insecure or will fail auth checks.")
613
+
614
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)
615
+
616
+ async def get_api_key(api_key_header: str = Security(api_key_header)):
617
+ if not API_KEY:
618
+ raise HTTPException(
619
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
620
+ detail="Server security configuration error"
621
+ )
622
+
623
+ if api_key_header != API_KEY:
624
+ raise HTTPException(
625
+ status_code=status.HTTP_403_FORBIDDEN,
626
+ detail="Could not validate credentials"
627
+ )
628
+ return api_key_header
629
+
630
+ # Initialize managers
631
+ job_manager = JobManager()
632
+ video_generator = VideoGenerator(job_manager)
633
+
634
+
635
+ @app.on_event("startup")
636
+ async def startup_event():
637
+ """Startup event - initialize background tasks"""
638
+ job_manager.start_periodic_cleanup()
639
+ logger.info("✅ Background cleanup tasks started")
640
+
641
+
642
+ # ============================================================================
643
+ # Endpoints
644
+ # ============================================================================
645
+
646
+ @app.get("/")
647
+ async def root():
648
+ """API root endpoint"""
649
+ return {
650
+ "name": "Unified Manim Video Generation API",
651
+ "version": "2.0.0",
652
+ "description": "Supports text prompts, PDFs, and URLs",
653
+ "categories": ["tech_system", "product_startup", "mathematical"],
654
+ "input_types": ["text", "pdf", "url"],
655
+ "endpoints": {
656
+ "docs": "/docs",
657
+ "create_video": "POST /api/videos",
658
+ "get_status": "GET /api/jobs/{job_id}",
659
+ "download_video": "GET /api/videos/{job_id}",
660
+ "list_jobs": "GET /api/jobs"
661
+ }
662
+ }
663
+
664
+
665
+ @app.post("/api/videos", response_model=JobResponse)
666
+ async def create_video(request: VideoRequest, background_tasks: BackgroundTasks):
667
+ """
668
+ Create a new video generation job
669
+
670
+ Supports three input types:
671
+ - text: Plain text prompt
672
+ - pdf: Base64 encoded PDF file
673
+ - url: URL to scrape content from
674
+ """
675
+ # Store input data in job
676
+ input_data = request.input_data
677
+
678
+ # Create job
679
+ job_id = job_manager.create_job(
680
+ input_type=request.input_type,
681
+ input_data=input_data,
682
+ quality=request.quality,
683
+ category=request.category,
684
+ scene_name=request.scene_name
685
+ )
686
+
687
+ # Store full input data for processing
688
+ job_manager.jobs[job_id]["input_data"] = input_data
689
+
690
+ logger.info(f"📝 New job created: {job_id} (type: {request.input_type}, category: {request.category})")
691
+
692
+ # Start generation in background
693
+ background_tasks.add_task(video_generator.generate_video, job_id)
694
+
695
+ return JobResponse(
696
+ job_id=job_id,
697
+ status=JobStatus.PENDING,
698
+ message="Job created successfully. Video generation started.",
699
+ created_at=datetime.now().isoformat()
700
+ )
701
+
702
+
703
+ @app.get("/api/jobs/{job_id}", response_model=JobStatusResponse)
704
+ async def get_job_status(job_id: str):
705
+ """Get the status of a video generation job"""
706
+ job = job_manager.get_job(job_id)
707
+
708
+ if not job:
709
+ raise HTTPException(status_code=404, detail="Job not found")
710
+
711
+ video_url = None
712
+ duration = None
713
+
714
+ if job["status"] == JobStatus.COMPLETED and job.get("video_path"):
715
+ video_url = f"/api/videos/{job_id}"
716
+
717
+ # Get video duration if available
718
+ try:
719
+ video_path = Path(job["video_path"])
720
+ if video_path.exists():
721
+ result = subprocess.run(
722
+ ["ffprobe", "-v", "error", "-show_entries", "format=duration",
723
+ "-of", "default=noprint_wrappers=1:nokey=1", str(video_path)],
724
+ capture_output=True,
725
+ text=True
726
+ )
727
+ duration = float(result.stdout.strip())
728
+ except:
729
+ pass
730
+
731
+ return JobStatusResponse(
732
+ job_id=job_id,
733
+ status=job["status"],
734
+ category=job["category"],
735
+ progress=job["progress"],
736
+ created_at=job["created_at"],
737
+ updated_at=job["updated_at"],
738
+ error=job.get("error"),
739
+ video_url=video_url,
740
+ duration=duration
741
+ )
742
+
743
+
744
+ @app.get("/api/videos/{job_id}")
745
+ async def download_video(job_id: str):
746
+ """Download the generated video file"""
747
+ job = job_manager.get_job(job_id)
748
+
749
+ if not job:
750
+ raise HTTPException(status_code=404, detail="Job not found")
751
+
752
+ if job["status"] != JobStatus.COMPLETED:
753
+ raise HTTPException(
754
+ status_code=400,
755
+ detail=f"Video not ready. Status: {job['status']}"
756
+ )
757
+
758
+ video_path = Path(job["video_path"])
759
+ if not video_path.exists():
760
+ raise HTTPException(status_code=404, detail="Video file not found")
761
+
762
+ return FileResponse(
763
+ video_path,
764
+ media_type="video/mp4",
765
+ filename=f"animation_{job_id[:8]}.mp4"
766
+ )
767
+
768
+
769
+ @app.get("/api/jobs")
770
+ async def list_jobs(limit: int = 50):
771
+ """List recent jobs"""
772
+ jobs = job_manager.list_jobs(limit)
773
+ return {"jobs": jobs, "total": len(jobs)}
774
+
775
+
776
+ @app.get("/health")
777
+ async def health_check():
778
+ """Health check endpoint"""
779
+ return {
780
+ "status": "healthy",
781
+ "version": "2.0.0",
782
+ "jobs": {
783
+ "total": len(job_manager.jobs),
784
+ "pending": sum(1 for j in job_manager.jobs.values() if j["status"] == JobStatus.PENDING),
785
+ "processing": sum(1 for j in job_manager.jobs.values() if j["status"] in [JobStatus.GENERATING_CODE, JobStatus.RENDERING]),
786
+ "completed": sum(1 for j in job_manager.jobs.values() if j["status"] == JobStatus.COMPLETED),
787
+ "failed": sum(1 for j in job_manager.jobs.values() if j["status"] == JobStatus.FAILED)
788
+ }
789
+ }
790
+
791
+
792
+ # ============================================================================
793
+ # Main
794
+ # ============================================================================
795
+
796
+ if __name__ == "__main__":
797
+ import uvicorn
798
+
799
+ print("🚀 Starting Unified Manim Video Generation API Server...")
800
+ print("📚 API Documentation: http://localhost:8000/docs")
801
+ print("🔍 ReDoc Documentation: http://localhost:8000/redoc")
802
+ print("✨ Supports: Text, PDF, and URL inputs")
803
+ print("🎨 Categories: Tech System, Product Startup, Mathematical")
804
+
805
+ uvicorn.run(
806
+ "api_server:app",
807
+ host="0.0.0.0",
808
+ port=8003,
809
+ reload=False,
810
+ log_level="info"
811
+ )
assets/manimator.png ADDED
docs/IMPLEMENTATION_COMPLETE.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Complete ✅
2
+
3
+ All phases of the enhanced video generation system have been implemented.
4
+
5
+ ## What's New
6
+
7
+ ### 1. **Unified Input Support**
8
+ - ✅ Text prompts
9
+ - ✅ PDF files (base64 encoded)
10
+ - ✅ URL scraping (blogs, documentation, etc.)
11
+
12
+ ### 2. **Category-Specific Visual Themes**
13
+ - ✅ **Tech System**: Dark blue background (#0a0e27), professional diagrams
14
+ - ✅ **Product Startup**: White background (#ffffff), modern gradients
15
+ - ✅ **Research/Mathematical**: Dark background (#1e1e1e), educational style
16
+
17
+ ### 3. **Robust Code Validation & Auto-Fix**
18
+ - ✅ Pre-render syntax validation
19
+ - ✅ Import checking
20
+ - ✅ Structure validation
21
+ - ✅ Auto-fix common issues
22
+ - ✅ Retry with fallback models
23
+
24
+ ### 4. **Unified API Server**
25
+ - ✅ Single endpoint for all input types
26
+ - ✅ Async job processing
27
+ - ✅ Progress tracking
28
+ - ✅ Error handling
29
+
30
+ ## New Files Created
31
+
32
+ ### Core Components
33
+ - `manimator/services/web_scraper.py` - Web content scraping
34
+ - `manimator/api/input_processor.py` - Unified input handler
35
+ - `manimator/utils/visual_themes.py` - Theme configurations
36
+ - `manimator/utils/theme_injector.py` - Theme code injection
37
+ - `manimator/utils/code_validator.py` - Code validation
38
+ - `manimator/utils/code_fixer.py` - Auto-fix functionality
39
+ - `manimator/utils/validation_pipeline.py` - Complete validation pipeline
40
+ - `api_server_unified.py` - New unified API server
41
+
42
+ ### Modified Files
43
+ - `manimator/api/scene_description.py` - Added URL processing
44
+ - `manimator/api/animation_generation.py` - Added validation & retry logic
45
+ - `manimator/utils/system_prompts.py` - Enhanced with theme instructions
46
+ - `pyproject.toml` - Added new dependencies
47
+
48
+ ## Dependencies Added
49
+
50
+ ```toml
51
+ beautifulsoup4 = "^4.12.0"
52
+ requests = "^2.31.0"
53
+ readability-lxml = "^0.8.1"
54
+ ```
55
+
56
+ ## How to Use
57
+
58
+ ### Start the Unified API Server
59
+
60
+ ```bash
61
+ python api_server_unified.py
62
+ ```
63
+
64
+ Server runs on `http://localhost:8000`
65
+
66
+ ### API Endpoints
67
+
68
+ #### Create Video
69
+ ```bash
70
+ POST /api/videos
71
+ {
72
+ "input_type": "text|pdf|url",
73
+ "input_data": "...",
74
+ "quality": "low|medium|high|ultra",
75
+ "category": "tech_system|product_startup|mathematical"
76
+ }
77
+ ```
78
+
79
+ #### Check Job Status
80
+ ```bash
81
+ GET /api/jobs/{job_id}
82
+ ```
83
+
84
+ #### Download Video
85
+ ```bash
86
+ GET /api/videos/{job_id}
87
+ ```
88
+
89
+ ### Example Requests
90
+
91
+ **Text Input:**
92
+ ```json
93
+ {
94
+ "input_type": "text",
95
+ "input_data": "Explain how a distributed system handles requests",
96
+ "category": "tech_system",
97
+ "quality": "high"
98
+ }
99
+ ```
100
+
101
+ **URL Input:**
102
+ ```json
103
+ {
104
+ "input_type": "url",
105
+ "input_data": "https://example.com/blog/post",
106
+ "category": "product_startup",
107
+ "quality": "medium"
108
+ }
109
+ ```
110
+
111
+ **PDF Input:**
112
+ ```json
113
+ {
114
+ "input_type": "pdf",
115
+ "input_data": "base64_encoded_pdf_string",
116
+ "category": "mathematical",
117
+ "quality": "high"
118
+ }
119
+ ```
120
+
121
+ ## Features
122
+
123
+ ### Visual Themes
124
+ Each category has distinct visual styling:
125
+ - **Tech**: Dark professional backgrounds, architecture diagrams
126
+ - **Product**: Light modern backgrounds, UI elements, gradients
127
+ - **Research**: Dark educational backgrounds, mathematical equations
128
+
129
+ ### Error Handling
130
+ - Automatic code validation before rendering
131
+ - Auto-fix for common issues (missing imports, undefined colors, etc.)
132
+ - Retry with fallback models if generation fails
133
+ - Detailed error messages
134
+
135
+ ### Input Processing
136
+ - **Text**: Direct prompt processing
137
+ - **PDF**: Base64 decoding and content extraction
138
+ - **URL**: Web scraping with content extraction
139
+
140
+ ## Next Steps
141
+
142
+ 1. Install new dependencies:
143
+ ```bash
144
+ poetry install
145
+ ```
146
+
147
+ 2. Test the unified server:
148
+ ```bash
149
+ python api_server_unified.py
150
+ ```
151
+
152
+ 3. Try different input types and categories
153
+
154
+ 4. Monitor job status and video generation
155
+
156
+ ## Notes
157
+
158
+ - The unified server replaces the need for separate 2D/3D servers
159
+ - All categories use the same code generation pipeline with different themes
160
+ - Web scraping respects robots.txt and handles authentication errors gracefully
161
+ - Code validation prevents most rendering failures
162
+
docs/IMPLEMENTATION_PLAN.md ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Plan: Enhanced Video Generation System
2
+
3
+ ## Phase 1: Unified Input Processor
4
+
5
+ ### 1.1 Create Web Content Scraper
6
+ **File**: `manimator/utils/web_scraper.py`
7
+ - Use `requests` + `beautifulsoup4` for HTML parsing
8
+ - Extract main content, remove navigation/ads
9
+ - Support: blogs, documentation sites, Medium, Dev.to, etc.
10
+ - Handle authentication-required pages (return error with clear message)
11
+ - Extract text content and structure (headings, paragraphs, code blocks)
12
+ - Convert to structured format similar to PDF processing
13
+
14
+ **Dependencies to add**:
15
+ ```python
16
+ beautifulsoup4>=4.12.0
17
+ requests>=2.31.0
18
+ readability-lxml>=0.8.1 # For cleaner content extraction
19
+ ```
20
+
21
+ ### 1.2 Unified Input Handler
22
+ **File**: `manimator/api/input_processor.py`
23
+ - Single entry point: `process_input(input_type, input_data, category)`
24
+ - Input types: `text`, `pdf`, `url`
25
+ - Route to appropriate processor:
26
+ - Text → `process_prompt_scene()`
27
+ - PDF → `process_pdf_prompt()`
28
+ - URL → new `process_url_content()`
29
+ - Return standardized scene description format
30
+
31
+ ### 1.3 URL Content Processor
32
+ **File**: `manimator/api/scene_description.py` (extend existing)
33
+ - Function: `process_url_content(url: str) -> str`
34
+ - Scrape web content
35
+ - Extract main text (similar to PDF processing)
36
+ - Generate scene description using LLM with web content context
37
+ - Handle errors: invalid URLs, access denied, parsing failures
38
+
39
+ ---
40
+
41
+ ## Phase 2: Category-Specific Visual Themes
42
+
43
+ ### 2.1 Theme Configuration System
44
+ **File**: `manimator/utils/visual_themes.py`
45
+ - Define theme configs for each category:
46
+ ```python
47
+ TECH_THEME = {
48
+ "background_color": "#0a0e27", # Dark blue
49
+ "accent_colors": [BLUE, GREEN, ORANGE, RED, PURPLE],
50
+ "text_color": WHITE,
51
+ "component_style": "rounded_rectangles",
52
+ "animation_style": "professional",
53
+ "voice_id": "Adam"
54
+ }
55
+
56
+ PRODUCT_THEME = {
57
+ "background_color": "#ffffff", # White/light
58
+ "accent_colors": [ORANGE, BLUE, PURPLE, GREEN],
59
+ "text_color": "#1a1a1a",
60
+ "component_style": "modern_gradients",
61
+ "animation_style": "engaging",
62
+ "voice_id": "Bella"
63
+ }
64
+
65
+ RESEARCH_THEME = {
66
+ "background_color": "#1e1e1e", # Dark
67
+ "accent_colors": [BLUE, GREEN, YELLOW, RED],
68
+ "text_color": WHITE,
69
+ "component_style": "mathematical",
70
+ "animation_style": "educational",
71
+ "voice_id": "Rachel"
72
+ }
73
+ ```
74
+
75
+ ### 2.2 Theme Injection into System Prompts
76
+ **File**: `manimator/utils/system_prompts.py` (modify)
77
+ - Update `get_system_prompt(category)` to include theme instructions
78
+ - Add theme-specific code snippets to each prompt:
79
+ - Tech: Dark background setup, component colors
80
+ - Product: Light background, gradient examples
81
+ - Research: Dark background, equation styling
82
+ - Include background setup code in each prompt template
83
+
84
+ ### 2.3 Background Setup Code Generator
85
+ **File**: `manimator/utils/theme_injector.py`
86
+ - Function: `inject_theme_setup(code: str, category: str) -> str`
87
+ - Parse generated code
88
+ - Insert background setup at start of `construct()` method:
89
+ ```python
90
+ # Tech theme
91
+ self.camera.background_color = "#0a0e27"
92
+
93
+ # Product theme
94
+ self.camera.background_color = "#ffffff"
95
+
96
+ # Research theme
97
+ self.camera.background_color = "#1e1e1e"
98
+ ```
99
+ - Ensure theme colors are used consistently
100
+
101
+ ---
102
+
103
+ ## Phase 3: Enhanced Code Validation & Error Handling
104
+
105
+ ### 3.1 Pre-Render Code Validator
106
+ **File**: `manimator/utils/code_validator.py`
107
+ - Function: `validate_code(code: str) -> Tuple[bool, List[str]]`
108
+ - Checks:
109
+ - Valid Python syntax (use `ast.parse()`)
110
+ - Required imports present (`from manim import *`, `VoiceoverScene`, `ElevenLabsService`)
111
+ - Scene class inherits from `VoiceoverScene`
112
+ - `construct()` method exists
113
+ - Voiceover service initialized
114
+ - No undefined variables/colors
115
+ - No overlapping object warnings (spatial analysis)
116
+ - Return: (is_valid, list_of_errors)
117
+
118
+ ### 3.2 Code Fixer
119
+ **File**: `manimator/utils/code_fixer.py`
120
+ - Function: `auto_fix_code(code: str, errors: List[str]) -> str`
121
+ - Auto-fixes:
122
+ - Missing imports (add if not present)
123
+ - Undefined colors (use existing `fix_undefined_colors()`)
124
+ - Missing voiceover setup (inject if missing)
125
+ - Syntax errors (try to fix common issues)
126
+ - Use existing `code_postprocessor.py` functions
127
+ - Chain fixes until valid or max attempts
128
+
129
+ ### 3.3 Retry Logic with Model Fallback
130
+ **File**: `manimator/api/animation_generation.py` (modify)
131
+ - Enhanced `generate_animation_response()`:
132
+ - Try generation with primary model
133
+ - Validate code
134
+ - If invalid, try auto-fix
135
+ - If still invalid, retry with different model (fallback)
136
+ - Max 3 attempts total
137
+ - Return best valid code or raise clear error
138
+
139
+ ### 3.4 Render Error Handler
140
+ **File**: `manimator/utils/schema.py` (modify `ManimProcessor`)
141
+ - Enhanced `render_scene()`:
142
+ - Capture full error output
143
+ - Parse common Manim errors:
144
+ - LaTeX errors → suggest fixes
145
+ - Import errors → auto-add imports
146
+ - Scene not found → validate class name
147
+ - Return detailed error messages
148
+ - Attempt auto-fix and re-render if possible
149
+
150
+ ---
151
+
152
+ ## Phase 4: Unified API Server
153
+
154
+ ### 4.1 New Unified API Server
155
+ **File**: `api_server_unified.py`
156
+ - Single server handling all input types and categories
157
+ - Endpoints:
158
+ - `POST /api/videos` - Create video (text/PDF/URL)
159
+ - `GET /api/jobs/{job_id}` - Check status
160
+ - `GET /api/videos/{job_id}` - Download video
161
+ - `GET /api/jobs` - List jobs
162
+ - Request model:
163
+ ```python
164
+ class VideoRequest(BaseModel):
165
+ input_type: Literal["text", "pdf", "url"]
166
+ input_data: str # text prompt, PDF bytes (base64), or URL
167
+ category: Literal["tech_system", "product_startup", "mathematical"]
168
+ quality: QualityLevel
169
+ scene_name: Optional[str] = None
170
+ ```
171
+
172
+ ### 4.2 Input Router
173
+ **File**: `manimator/api/input_router.py`
174
+ - Route based on `input_type`:
175
+ - `text` → `process_prompt_scene()`
176
+ - `pdf` → `process_pdf_prompt()` (decode base64)
177
+ - `url` → `process_url_content()`
178
+ - All return scene description → pass to `generate_animation_response()`
179
+
180
+ ### 4.3 Job Manager Enhancement
181
+ **File**: `api_server_unified.py` (extend existing JobManager)
182
+ - Track input type and category
183
+ - Store theme used
184
+ - Better error messages with category context
185
+
186
+ ---
187
+
188
+ ## Phase 5: System Prompts Enhancement
189
+
190
+ ### 5.1 Category-Specific Prompt Templates
191
+ **File**: `manimator/utils/system_prompts.py` (enhance existing)
192
+ - **Tech System Prompt**:
193
+ - Emphasize architecture diagrams
194
+ - Component-based visuals
195
+ - Dark background setup
196
+ - Professional color scheme
197
+ - Data flow animations
198
+
199
+ - **Product Startup Prompt**:
200
+ - Modern UI elements
201
+ - Gradient backgrounds
202
+ - Light/colorful theme
203
+ - Feature showcases
204
+ - Statistics displays
205
+
206
+ - **Research/Mathematical Prompt**:
207
+ - Equation-heavy
208
+ - Dark background
209
+ - Step-by-step proofs
210
+ - Graph visualizations
211
+ - Educational pacing
212
+
213
+ ### 5.2 Few-Shot Examples Update
214
+ **File**: `manimator/few_shot/few_shot_prompts.py`
215
+ - Add category-specific examples:
216
+ - Tech: System architecture example
217
+ - Product: Feature demo example
218
+ - Research: Mathematical proof example
219
+ - Include theme setup in examples
220
+
221
+ ---
222
+
223
+ ## Phase 6: Testing & Validation Pipeline
224
+
225
+ ### 6.1 Code Validation Pipeline
226
+ **File**: `manimator/utils/validation_pipeline.py`
227
+ - Pre-render checks:
228
+ 1. Syntax validation
229
+ 2. Import validation
230
+ 3. Structure validation
231
+ 4. Theme compliance
232
+ 5. Auto-fix attempts
233
+ - Post-render checks:
234
+ 1. Video file exists
235
+ 2. Video duration > 0
236
+ 3. Video is playable
237
+
238
+ ### 6.2 Error Recovery
239
+ - If validation fails → auto-fix → re-validate
240
+ - If auto-fix fails → retry generation with different model
241
+ - If all fails → return detailed error to user
242
+
243
+ ---
244
+
245
+ ## Implementation Order
246
+
247
+ 1. **Week 1**: Phase 1 (Input Processor) + Phase 2 (Themes)
248
+ 2. **Week 2**: Phase 3 (Validation) + Phase 5 (Prompts)
249
+ 3. **Week 3**: Phase 4 (Unified API) + Phase 6 (Testing)
250
+
251
+ ---
252
+
253
+ ## File Structure Changes
254
+
255
+ ```
256
+ manimator/
257
+ ├── api/
258
+ │ ├── animation_generation.py (existing, enhance)
259
+ │ ├── scene_description.py (existing, extend)
260
+ │ ├── input_processor.py (NEW)
261
+ │ └── input_router.py (NEW)
262
+ ├── utils/
263
+ │ ├── code_postprocessor.py (existing)
264
+ │ ├── code_validator.py (NEW)
265
+ │ ├── code_fixer.py (NEW)
266
+ │ ├── visual_themes.py (NEW)
267
+ │ ├── theme_injector.py (NEW)
268
+ │ └── validation_pipeline.py (NEW)
269
+ └── services/
270
+ └── web_scraper.py (NEW)
271
+
272
+ api_server_unified.py (NEW - main server)
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Dependencies to Add
278
+
279
+ ```toml
280
+ beautifulsoup4 = "^4.12.0"
281
+ requests = "^2.31.0"
282
+ readability-lxml = "^0.8.1"
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Key Success Metrics
288
+
289
+ 1. **Reliability**: < 5% code generation failures
290
+ 2. **Visual Differentiation**: Clear visual distinction between categories
291
+ 3. **Error Recovery**: 80%+ of errors auto-fixed
292
+ 4. **Input Support**: All 3 input types working (text/PDF/URL)
293
+
ensure_rpc.sql ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ create or replace function public.handle_user_login(
2
+ user_email text,
3
+ user_full_name text,
4
+ user_avatar_url text
5
+ )
6
+ returns void
7
+ language plpgsql
8
+ security definer
9
+ as $$
10
+ declare
11
+ current_user_id uuid;
12
+ begin
13
+ current_user_id := auth.uid();
14
+ if current_user_id is null then
15
+ raise exception 'Not authenticated';
16
+ end if;
17
+
18
+ insert into public.users (id, email, full_name, avatar_url, credits)
19
+ values (
20
+ current_user_id,
21
+ user_email,
22
+ user_full_name,
23
+ user_avatar_url,
24
+ 5
25
+ )
26
+ on conflict (id) do update set
27
+ email = excluded.email,
28
+ full_name = excluded.full_name,
29
+ avatar_url = excluded.avatar_url;
30
+ end;
31
+ $$;
32
+
33
+ grant execute on function public.handle_user_login to authenticated;
fix_columns_and_trigger.sql ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 1. Add missing columns if they don't exist
2
+ DO $$
3
+ BEGIN
4
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'full_name') THEN
5
+ ALTER TABLE public.users ADD COLUMN full_name text;
6
+ END IF;
7
+
8
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'avatar_url') THEN
9
+ ALTER TABLE public.users ADD COLUMN avatar_url text;
10
+ END IF;
11
+
12
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'credits') THEN
13
+ ALTER TABLE public.users ADD COLUMN credits integer default 5;
14
+ END IF;
15
+ END $$;
16
+
17
+ -- 2. Create the Trigger Function (Updated to be safe)
18
+ create or replace function public.handle_new_user()
19
+ returns trigger as $$
20
+ begin
21
+ insert into public.users (id, email, full_name, avatar_url, credits)
22
+ values (
23
+ new.id,
24
+ new.email,
25
+ new.raw_user_meta_data->>'full_name',
26
+ new.raw_user_meta_data->>'avatar_url',
27
+ 5
28
+ )
29
+ ON CONFLICT (id) DO UPDATE SET
30
+ email = EXCLUDED.email,
31
+ full_name = EXCLUDED.full_name,
32
+ avatar_url = EXCLUDED.avatar_url;
33
+ return new;
34
+ end;
35
+ $$ language plpgsql security definer;
36
+
37
+ -- 3. Create the Trigger
38
+ drop trigger if exists on_auth_user_created on auth.users;
39
+ create trigger on_auth_user_created
40
+ after insert on auth.users
41
+ for each row execute procedure public.handle_new_user();
42
+
43
+ -- 4. Backfill existing users
44
+ insert into public.users (id, email, full_name, avatar_url, credits)
45
+ select
46
+ id,
47
+ email,
48
+ raw_user_meta_data->>'full_name',
49
+ raw_user_meta_data->>'avatar_url',
50
+ 5
51
+ from auth.users
52
+ where id not in (select id from public.users);
fix_rls.sql ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Enable RLS
2
+ ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
3
+
4
+ -- Allow users to insert their own profile
5
+ CREATE POLICY "Users can insert their own profile"
6
+ ON public.users
7
+ FOR INSERT
8
+ WITH CHECK (auth.uid() = id);
9
+
10
+ -- Allow users to view their own profile
11
+ CREATE POLICY "Users can view their own profile"
12
+ ON public.users
13
+ FOR SELECT
14
+ USING (auth.uid() = id);
15
+
16
+ -- Allow users to update their own profile
17
+ CREATE POLICY "Users can update their own profile"
18
+ ON public.users
19
+ FOR UPDATE
20
+ USING (auth.uid() = id);
21
+
22
+ -- Grant access to authenticated users
23
+ GRANT ALL ON public.users TO authenticated;
24
+ GRANT ALL ON public.users TO service_role;
frontend/.env.example ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Google Authentication
2
+ GOOGLE_CLIENT_ID=your_google_client_id_here
3
+ GOOGLE_CLIENT_SECRET=your_google_client_secret_here
4
+ NEXTAUTH_URL=http://localhost:3000
5
+ NEXTAUTH_SECRET=generate_a_random_string_here
6
+
7
+ # Internal Security
8
+ INTERNAL_API_KEY=secure-internal-key-123
9
+
10
+ # Database (Supabase / Postgres)
11
+ # Connect to port 6543 (Transaction Pooler) for DATABASE_URL
12
+ DATABASE_URL="postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true"
13
+ # Connect to port 5432 (Session) for DIRECT_URL (Used for migrations)
14
+ DIRECT_URL="postgresql://postgres.[project-ref]:[password]@aws-0-[region].supabase.co:5432/postgres"
frontend/.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # testing
7
+ /coverage
8
+
9
+ # next.js
10
+ /.next/
11
+ /out/
12
+
13
+ # production
14
+ /build
15
+
16
+ # misc
17
+ .DS_Store
18
+ *.pem
19
+
20
+ # debug
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ .pnpm-debug.log*
25
+
26
+ # local env files
27
+ .env*.local
28
+
29
+ # vercel
30
+ .vercel
frontend/README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VidSimplify Frontend
2
+
3
+ This is the Next.js frontend for the VidSimplify video generation platform.
4
+
5
+ ## 🚀 Getting Started
6
+
7
+ 1. **Install Dependencies**:
8
+ ```bash
9
+ npm install
10
+ ```
11
+
12
+ 2. **Run Development Server**:
13
+ ```bash
14
+ npm run dev
15
+ ```
16
+
17
+ 3. **Open in Browser**:
18
+ Navigate to [http://localhost:3000](http://localhost:3000).
19
+
20
+ ## 🏗️ Architecture
21
+
22
+ - **Framework**: Next.js 14 (App Router)
23
+ - **Styling**: Tailwind CSS
24
+ - **Icons**: Lucide React
25
+ - **Animations**: Framer Motion
26
+ - **API Client**: `src/lib/api.ts` connects to the Python backend at `http://localhost:8000`.
27
+
28
+ ## 📄 Pages
29
+
30
+ - `/` - Landing page with features and demo.
31
+ - `/app` - Main application for generating videos.
32
+
33
+ ## 🔧 Configuration
34
+
35
+ The API URL is configured in `src/lib/api.ts`. Default is `http://localhost:8000`.
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
frontend/next.config.mjs ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ images: {
4
+ remotePatterns: [
5
+ {
6
+ protocol: 'https',
7
+ hostname: 'lh3.googleusercontent.com',
8
+ },
9
+ {
10
+ protocol: 'https',
11
+ hostname: 'googleusercontent.com',
12
+ }
13
+ ],
14
+ },
15
+ };
16
+
17
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.11",
13
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
14
+ "@radix-ui/react-slot": "^1.2.4",
15
+ "@radix-ui/react-tabs": "^1.1.13",
16
+ "@supabase/auth-helpers-nextjs": "^0.15.0",
17
+ "@supabase/ssr": "^0.8.0",
18
+ "@supabase/supabase-js": "^2.86.0",
19
+ "class-variance-authority": "^0.7.1",
20
+ "clsx": "^2.1.1",
21
+ "framer-motion": "^12.23.24",
22
+ "lucide-react": "^0.554.0",
23
+ "next": "^14.2.3",
24
+ "react": "^18.3.1",
25
+ "react-dom": "^18.3.1",
26
+ "tailwind-merge": "^3.4.0"
27
+ },
28
+ "devDependencies": {
29
+ "@tailwindcss/postcss": "^4",
30
+ "@types/node": "^20",
31
+ "@types/react": "^18.3.27",
32
+ "@types/react-dom": "^18.3.7",
33
+ "babel-plugin-react-compiler": "1.0.0",
34
+ "eslint": "^9",
35
+ "eslint-config-next": "16.0.3",
36
+ "tailwindcss": "^4",
37
+ "typescript": "^5"
38
+ }
39
+ }
frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/logo-full.jpg ADDED
frontend/public/logo-icon.jpg ADDED
frontend/public/logo.svg ADDED
frontend/public/next.svg ADDED
frontend/public/robots.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /app/
4
+ Disallow: /api/
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/src/app/api-docs/page.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Navbar } from "@/components/landing/Navbar";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { Check, Copy, Terminal, Play, BarChart3, Coins } from "lucide-react";
8
+
9
+ export default function ApiDocs() {
10
+ return (
11
+ <main className="min-h-screen bg-slate-950">
12
+ <Navbar />
13
+
14
+ <div className="pt-24 pb-16 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
15
+ <div className="text-center mb-16">
16
+ <h1 className="text-4xl font-bold text-white mb-4">VidSimplify API</h1>
17
+ <p className="text-xl text-slate-400 max-w-2xl mx-auto">
18
+ Integrate precision animation generation directly into your product.
19
+ Simple, RESTful, and scalable.
20
+ </p>
21
+ </div>
22
+
23
+ <div className="grid lg:grid-cols-3 gap-8">
24
+ {/* Main Documentation Area */}
25
+ <div className="lg:col-span-2 space-y-8">
26
+ <Card className="bg-slate-900 border-slate-800">
27
+ <CardHeader className="p-4 sm:p-6">
28
+ <CardTitle className="text-white flex items-center gap-2">
29
+ <Terminal className="h-5 w-5 text-blue-400" />
30
+ Quick Start
31
+ </CardTitle>
32
+ <CardDescription className="text-slate-400">
33
+ Generate your first video in 3 steps
34
+ </CardDescription>
35
+ </CardHeader>
36
+ <CardContent className="p-4 sm:p-6">
37
+ <Tabs defaultValue="python" className="w-full">
38
+ <TabsList className="bg-slate-950 border border-slate-800 mb-4">
39
+ <TabsTrigger value="python">Python</TabsTrigger>
40
+ <TabsTrigger value="curl">cURL</TabsTrigger>
41
+ <TabsTrigger value="node">Node.js</TabsTrigger>
42
+ </TabsList>
43
+
44
+ <TabsContent value="python" className="space-y-4">
45
+ <div className="bg-slate-950 p-4 rounded-lg border border-slate-800 font-mono text-sm text-slate-300 overflow-x-auto">
46
+ <div className="flex justify-between items-start mb-2">
47
+ <span className="text-slate-500"># 1. Create a video job</span>
48
+ <Copy className="h-4 w-4 text-slate-600 cursor-pointer hover:text-white" />
49
+ </div>
50
+ <pre className="text-blue-300">import <span className="text-white">requests</span></pre>
51
+ <pre className="mt-2">
52
+ {`response = requests.post(
53
+ "https://api.vidsimplify.com/v1/videos",
54
+ headers={"Authorization": "Bearer YOUR_API_KEY"},
55
+ json={
56
+ "prompt": "Explain database sharding",
57
+ "category": "tech_system",
58
+ "duration_minutes": 5
59
+ }
60
+ )
61
+
62
+ job_id = response.json()["job_id"]
63
+ print(f"Job started: {job_id}")`}
64
+ </pre>
65
+ </div>
66
+ </TabsContent>
67
+
68
+ <TabsContent value="curl">
69
+ <div className="bg-slate-950 p-4 rounded-lg border border-slate-800 font-mono text-sm text-slate-300 overflow-x-auto">
70
+ <pre>
71
+ {`curl -X POST https://api.vidsimplify.com/v1/videos \\
72
+ -H "Authorization: Bearer YOUR_API_KEY" \\
73
+ -H "Content-Type: application/json" \\
74
+ -d '{
75
+ "prompt": "Explain database sharding",
76
+ "category": "tech_system"
77
+ }'`}
78
+ </pre>
79
+ </div>
80
+ </TabsContent>
81
+ </Tabs>
82
+ </CardContent>
83
+ </Card>
84
+
85
+ <Card className="bg-slate-900 border-slate-800">
86
+ <CardHeader className="p-4 sm:p-6">
87
+ <CardTitle className="text-white">Endpoints</CardTitle>
88
+ </CardHeader>
89
+ <CardContent className="space-y-4 p-4 sm:p-6">
90
+ {[
91
+ { method: "POST", path: "/v1/videos", desc: "Create a new video generation job" },
92
+ { method: "GET", path: "/v1/jobs/{job_id}", desc: "Check generation status and progress" },
93
+ { method: "GET", path: "/v1/videos/{video_id}/download", desc: "Get secure download URL" },
94
+ { method: "GET", path: "/v1/usage", desc: "Get current billing period usage" }
95
+ ].map((ep, i) => (
96
+ <div key={i} className="flex flex-col md:flex-row md:items-center justify-between p-3 rounded bg-slate-950/50 border border-slate-800/50 gap-2 md:gap-0">
97
+ <div className="flex items-center gap-3">
98
+ <span className={`px-2 py-1 rounded text-xs font-bold ${ep.method === "POST" ? "bg-blue-900/30 text-blue-400" : "bg-green-900/30 text-green-400"
99
+ }`}>
100
+ {ep.method}
101
+ </span>
102
+ <span className="font-mono text-sm text-slate-300 break-all">{ep.path}</span>
103
+ </div>
104
+ <span className="text-sm text-slate-500">{ep.desc}</span>
105
+ </div>
106
+ ))}
107
+ </CardContent>
108
+ </Card>
109
+ </div>
110
+
111
+ {/* Sidebar / Dashboard Simulation */}
112
+ <div className="space-y-6">
113
+ <Card className="bg-slate-900 border-slate-800">
114
+ <CardHeader>
115
+ <CardTitle className="text-white flex items-center gap-2">
116
+ <BarChart3 className="h-5 w-5 text-green-400" />
117
+ Usage & Cost
118
+ </CardTitle>
119
+ </CardHeader>
120
+ <CardContent>
121
+ <div className="space-y-6">
122
+ <div>
123
+ <div className="flex justify-between text-sm mb-2">
124
+ <span className="text-slate-400">API Calls</span>
125
+ <span className="text-white font-medium">8,432 / 10,000</span>
126
+ </div>
127
+ <div className="h-2 bg-slate-800 rounded-full overflow-hidden">
128
+ <div className="h-full w-[84%] bg-blue-500"></div>
129
+ </div>
130
+ </div>
131
+
132
+ <div>
133
+ <div className="flex justify-between text-sm mb-2">
134
+ <span className="text-slate-400">Video Minutes</span>
135
+ <span className="text-white font-medium">450 / 500</span>
136
+ </div>
137
+ <div className="h-2 bg-slate-800 rounded-full overflow-hidden">
138
+ <div className="h-full w-[90%] bg-purple-500"></div>
139
+ </div>
140
+ </div>
141
+
142
+ <div className="pt-4 border-t border-slate-800">
143
+ <div className="flex justify-between items-center">
144
+ <span className="text-slate-400">Current Bill</span>
145
+ <span className="text-2xl font-bold text-white">$45.00</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+
152
+ <div className="bg-gradient-to-br from-blue-900/20 to-violet-900/20 border border-blue-500/20 rounded-xl p-6">
153
+ <h3 className="text-lg font-semibold text-white mb-2">Enterprise Plan</h3>
154
+ <p className="text-sm text-slate-400 mb-4">
155
+ Need higher limits or dedicated rendering clusters?
156
+ </p>
157
+ <Button className="w-full bg-white text-slate-900 hover:bg-slate-200">
158
+ Contact Sales
159
+ </Button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </main>
165
+ );
166
+ }
frontend/src/app/api/auth/sync/route.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr'
2
+ import { cookies } from 'next/headers'
3
+ import { NextResponse } from 'next/server'
4
+
5
+ export async function POST() {
6
+ console.log("\n🔄 === SYNC API CALLED ===");
7
+ const cookieStore = cookies()
8
+
9
+ // Debug: Log all cookies to see if auth token is present
10
+ const allCookies = cookieStore.getAll().map(c => c.name);
11
+ console.log("Cookies received:", allCookies);
12
+
13
+ // Create authenticated Supabase client
14
+ const supabase = createServerClient(
15
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
16
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
17
+ {
18
+ cookies: {
19
+ get(name: string) {
20
+ return cookieStore.get(name)?.value
21
+ },
22
+ set(name: string, value: string, options: CookieOptions) {
23
+ try {
24
+ cookieStore.set({ name, value, ...options })
25
+ } catch (error) {
26
+ // Route handlers can't set cookies in some Next.js versions/contexts,
27
+ // but we only need to READ them for auth here.
28
+ }
29
+ },
30
+ remove(name: string, options: CookieOptions) {
31
+ try {
32
+ cookieStore.delete({ name, ...options })
33
+ } catch (error) {
34
+ // Ignore
35
+ }
36
+ },
37
+ },
38
+ }
39
+ )
40
+
41
+ // Get current user
42
+ console.log("Getting current user...");
43
+ const { data: { user }, error: authError } = await supabase.auth.getUser()
44
+
45
+ if (authError || !user) {
46
+ console.error("❌ Not authenticated:", authError?.message);
47
+ return NextResponse.json({ error: 'Not authenticated', details: authError?.message }, { status: 401 })
48
+ }
49
+
50
+ console.log("✅ User authenticated:", user.id);
51
+
52
+ try {
53
+ // Check if user exists
54
+ console.log("Checking if user exists in DB...");
55
+ const { data: existingUser, error: fetchError } = await supabase
56
+ .from('users')
57
+ .select('id')
58
+ .eq('id', user.id)
59
+ .single()
60
+
61
+ console.log("Fetch result:", { existingUser, fetchError });
62
+
63
+ if (!existingUser) {
64
+ console.log(`🔄 User ${user.id} not found. Inserting...`)
65
+
66
+ const insertData = {
67
+ id: user.id,
68
+ email: user.email,
69
+ full_name: user.user_metadata.full_name || null,
70
+ avatar_url: user.user_metadata.avatar_url || null,
71
+ credits: 5
72
+ };
73
+
74
+ console.log("Insert data:", insertData);
75
+
76
+ // Insert user using the AUTHENTICATED client (acting as the user)
77
+ const { data: insertResult, error: insertError } = await supabase
78
+ .from('users')
79
+ .insert(insertData)
80
+ .select()
81
+
82
+ if (insertError) {
83
+ console.error('❌ Sync insert error:', insertError)
84
+ return NextResponse.json({ error: insertError.message }, { status: 500 })
85
+ }
86
+
87
+ console.log("✅ User created successfully:", insertResult);
88
+ return NextResponse.json({ status: 'created', user: insertResult })
89
+ }
90
+
91
+ console.log("✅ User already exists");
92
+ return NextResponse.json({ status: 'exists' })
93
+
94
+ } catch (err) {
95
+ console.error('❌ Sync error:', err)
96
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
97
+ }
98
+ }
frontend/src/app/api/generate/route.ts ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { NextResponse } from "next/server";
3
+ import { createServerClient, type CookieOptions } from '@supabase/ssr'
4
+ import { cookies } from "next/headers";
5
+ import { supabase as supabaseAdmin } from "@/lib/supabase";
6
+
7
+ const PYTHON_API_URL = process.env.PYTHON_API_URL || "http://127.0.0.1:8000";
8
+
9
+ const SHOWCASE_KEYWORDS = [
10
+ "database sharding",
11
+ "kafka",
12
+ "transformers",
13
+ "quantum entanglement",
14
+ "netflix",
15
+ "black hole",
16
+ "sorting algorithms",
17
+ "uber",
18
+ "quicksort",
19
+ "bubble sort"
20
+ ];
21
+
22
+ export async function POST(req: Request) {
23
+ try {
24
+ // Get request body first to check for demo mode
25
+ const body = await req.json();
26
+ const { input_data, input_type, category, quality } = body;
27
+
28
+ // Check for showcase prompt (Demo Mode) - Bypass Auth and Credits
29
+ const lowerInput = (input_data || "").toLowerCase();
30
+ const isShowcase = SHOWCASE_KEYWORDS.some(keyword => lowerInput.includes(keyword));
31
+
32
+ if (isShowcase) {
33
+ console.log("🎨 Demo Mode activated for prompt:", input_data);
34
+
35
+ // Return a fake job ID encoded with timestamp to track progress
36
+ const timestamp = Date.now();
37
+ return NextResponse.json({
38
+ job_id: `demo-${timestamp}`,
39
+ status: "pending",
40
+ message: "Job started (Demo)"
41
+ });
42
+ }
43
+
44
+ const cookieStore = cookies() // Removed await for Next.js 14 compatibility
45
+
46
+ // Create authenticated Supabase client
47
+ const supabase = createServerClient(
48
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
49
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
50
+ {
51
+ cookies: {
52
+ getAll() {
53
+ return cookieStore.getAll()
54
+ },
55
+ setAll(cookiesToSet) {
56
+ cookiesToSet.forEach(({ name, value, options }) => {
57
+ cookieStore.set(name, value, options)
58
+ })
59
+ },
60
+ },
61
+ }
62
+ )
63
+
64
+ // Get current user
65
+ const { data: { user }, error: authError } = await supabase.auth.getUser()
66
+
67
+ if (authError || !user) {
68
+ return NextResponse.json(
69
+ { error: "Unauthorized" },
70
+ { status: 401 }
71
+ );
72
+ }
73
+
74
+ // Check credits
75
+ const { data: userData, error: userError } = await supabaseAdmin
76
+ .from("users")
77
+ .select("credits")
78
+ .eq("id", user.id)
79
+ .single();
80
+
81
+ if (userError || !userData) {
82
+ return NextResponse.json(
83
+ { error: "User not found" },
84
+ { status: 404 }
85
+ );
86
+ }
87
+
88
+ if (userData.credits < 1) {
89
+ return NextResponse.json(
90
+ { error: "Insufficient credits" },
91
+ { status: 402 }
92
+ );
93
+ }
94
+
95
+ // Real Generation Flow
96
+ // Deduct credit
97
+ const { error: updateError } = await supabaseAdmin
98
+ .from("users")
99
+ .update({ credits: userData.credits - 1 })
100
+ .eq("id", user.id);
101
+
102
+ if (updateError) {
103
+ return NextResponse.json(
104
+ { error: "Failed to deduct credits" },
105
+ { status: 500 }
106
+ );
107
+ }
108
+
109
+ // Call Python API
110
+ const response = await fetch(`${PYTHON_API_URL}/api/generate`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ "X-API-Key": process.env.INTERNAL_API_KEY || "",
115
+ },
116
+ body: JSON.stringify({
117
+ input_data,
118
+ input_type,
119
+ category,
120
+ quality,
121
+ user_id: user.id
122
+ }),
123
+ });
124
+
125
+ if (!response.ok) {
126
+ // Refund credit on failure
127
+ await supabaseAdmin
128
+ .from("users")
129
+ .update({ credits: userData.credits })
130
+ .eq("id", user.id);
131
+
132
+ const error = await response.json();
133
+ return NextResponse.json(
134
+ { error: error.detail || "Generation failed" },
135
+ { status: response.status }
136
+ );
137
+ }
138
+
139
+ const data = await response.json();
140
+ return NextResponse.json(data);
141
+
142
+ } catch (error) {
143
+ console.error("Generate error:", error);
144
+ return NextResponse.json(
145
+ { error: "Internal server error" },
146
+ { status: 500 }
147
+ );
148
+ }
149
+ }
frontend/src/app/api/jobs/[id]/route.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ const PYTHON_API_URL = process.env.PYTHON_API_URL || "http://127.0.0.1:8000";
4
+ const DEMO_VIDEO_URL = "https://storage.googleapis.com/vidsimplify/GeneratedScene.mp4";
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: { id: string } }
9
+ ) {
10
+ const jobId = params.id;
11
+
12
+ // Handle Demo Jobs
13
+ if (jobId.startsWith("demo-")) {
14
+ const timestamp = parseInt(jobId.split("-")[1]);
15
+ const now = Date.now();
16
+ const elapsed = (now - timestamp) / 1000; // seconds
17
+
18
+ if (elapsed < 3) {
19
+ return NextResponse.json({
20
+ job_id: jobId,
21
+ status: "pending",
22
+ progress: { percentage: 10, message: "Initializing system..." },
23
+ created_at: new Date(timestamp).toISOString()
24
+ });
25
+ } else if (elapsed < 6) {
26
+ return NextResponse.json({
27
+ job_id: jobId,
28
+ status: "generating_code",
29
+ progress: { percentage: 40, message: "Generating Manim script..." },
30
+ created_at: new Date(timestamp).toISOString()
31
+ });
32
+ } else if (elapsed < 10) {
33
+ return NextResponse.json({
34
+ job_id: jobId,
35
+ status: "rendering",
36
+ progress: { percentage: 70, message: "Rendering animation frames..." },
37
+ created_at: new Date(timestamp).toISOString()
38
+ });
39
+ } else if (elapsed < 14) {
40
+ return NextResponse.json({
41
+ job_id: jobId,
42
+ status: "rendering",
43
+ progress: { percentage: 90, message: "Finalizing video..." },
44
+ created_at: new Date(timestamp).toISOString()
45
+ });
46
+ } else {
47
+ return NextResponse.json({
48
+ job_id: jobId,
49
+ status: "completed",
50
+ progress: { percentage: 100, message: "Completed" },
51
+ output_url: DEMO_VIDEO_URL,
52
+ created_at: new Date(timestamp).toISOString()
53
+ });
54
+ }
55
+ }
56
+
57
+ // Handle Real Jobs - Proxy to Python Backend
58
+ try {
59
+ const response = await fetch(`${PYTHON_API_URL}/api/jobs/${jobId}`);
60
+
61
+ if (!response.ok) {
62
+ return NextResponse.json(
63
+ { error: "Failed to fetch job status" },
64
+ { status: response.status }
65
+ );
66
+ }
67
+
68
+ const data = await response.json();
69
+ return NextResponse.json(data);
70
+ } catch (error) {
71
+ console.error("Proxy error:", error);
72
+ return NextResponse.json(
73
+ { error: "Internal server error" },
74
+ { status: 500 }
75
+ );
76
+ }
77
+ }
frontend/src/app/api/videos/[id]/route.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ const PYTHON_API_URL = process.env.PYTHON_API_URL || "http://127.0.0.1:8000";
4
+ const DEMO_VIDEO_URL = "https://storage.googleapis.com/vidsimplify/GeneratedScene.mp4";
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: { id: string } }
9
+ ) {
10
+ const jobId = params.id;
11
+
12
+ // Handle Demo Jobs
13
+ if (jobId.startsWith("demo-")) {
14
+ return NextResponse.redirect(DEMO_VIDEO_URL);
15
+ }
16
+
17
+ // Handle Real Jobs - Proxy to Python Backend
18
+ try {
19
+ const response = await fetch(`${PYTHON_API_URL}/api/videos/${jobId}`);
20
+
21
+ if (!response.ok) {
22
+ return NextResponse.json(
23
+ { error: "Failed to fetch video" },
24
+ { status: response.status }
25
+ );
26
+ }
27
+
28
+ // Forward the video content
29
+ const blob = await response.blob();
30
+ const headers = new Headers(response.headers);
31
+
32
+ return new NextResponse(blob, {
33
+ status: 200,
34
+ headers: headers
35
+ });
36
+
37
+ } catch (error) {
38
+ console.error("Proxy error:", error);
39
+ return NextResponse.json(
40
+ { error: "Internal server error" },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
frontend/src/app/app/page.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Suspense } from "react";
2
+ import { VideoGenerator } from "@/components/app/VideoGenerator";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export default function AppPage() {
7
+ return (
8
+ <Suspense fallback={<div className="flex items-center justify-center h-screen bg-slate-950 text-slate-400">Loading...</div>}>
9
+ <VideoGenerator />
10
+ </Suspense>
11
+ );
12
+ }
frontend/src/app/auth/callback/route.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createServerClient, type CookieOptions } from '@supabase/ssr'
2
+ import { createClient } from '@supabase/supabase-js'
3
+ import { cookies } from 'next/headers'
4
+ import { NextResponse } from 'next/server'
5
+
6
+ export async function GET(request: Request) {
7
+ console.log("\n=== AUTH CALLBACK START ===");
8
+ const { searchParams, origin } = new URL(request.url)
9
+ const code = searchParams.get('code')
10
+ const next = searchParams.get('next') ?? '/'
11
+
12
+ console.log("Code present:", !!code);
13
+ console.log("Redirect target:", next);
14
+
15
+ if (code) {
16
+ const cookieStore = await cookies()
17
+
18
+ const supabase = createServerClient(
19
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
20
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
21
+ {
22
+ cookies: {
23
+ getAll() {
24
+ return cookieStore.getAll()
25
+ },
26
+ setAll(cookiesToSet) {
27
+ cookiesToSet.forEach(({ name, value, options }) => {
28
+ cookieStore.set(name, value, options)
29
+ })
30
+ },
31
+ },
32
+ }
33
+ )
34
+
35
+ console.log("Exchanging code for session...");
36
+ const { data: { session }, error } = await supabase.auth.exchangeCodeForSession(code)
37
+
38
+ if (error) {
39
+ console.error('❌ Auth exchange error:', error)
40
+ return NextResponse.redirect(`${origin}/auth/error?error=${encodeURIComponent(error.message)}`)
41
+ }
42
+
43
+ if (session?.user) {
44
+ console.log("✅ Auth successful. User ID:", session.user.id);
45
+ console.log("User email:", session.user.email);
46
+
47
+ // SYNC USER TO DATABASE
48
+ console.log("Checking if user exists in DB...");
49
+
50
+ // Use the authenticated supabase client (has the session)
51
+ const { data: existingUser, error: fetchError } = await supabase
52
+ .from('users')
53
+ .select('id')
54
+ .eq('id', session.user.id)
55
+ .single();
56
+
57
+ console.log("Fetch result:", { existingUser, fetchError });
58
+
59
+ if (fetchError && fetchError.code !== 'PGRST116') {
60
+ console.error("❌ Error checking user existence:", fetchError);
61
+ }
62
+
63
+ if (!existingUser) {
64
+ console.log("🔄 User not found in DB. Attempting insert...");
65
+
66
+ const insertData = {
67
+ id: session.user.id,
68
+ email: session.user.email,
69
+ full_name: session.user.user_metadata.full_name || null,
70
+ avatar_url: session.user.user_metadata.avatar_url || null,
71
+ credits: 5
72
+ };
73
+
74
+ console.log("Insert data:", insertData);
75
+
76
+ const { data: insertResult, error: insertError } = await supabase
77
+ .from('users')
78
+ .insert(insertData)
79
+ .select();
80
+
81
+ if (insertError) {
82
+ console.error("❌ INSERT FAILED:", insertError);
83
+ console.error("Error code:", insertError.code);
84
+ console.error("Error message:", insertError.message);
85
+ console.error("Error details:", insertError.details);
86
+ console.error("Error hint:", insertError.hint);
87
+ } else {
88
+ console.log("✅ Successfully inserted user:", insertResult);
89
+ }
90
+ } else {
91
+ console.log("✅ User already exists in DB");
92
+ }
93
+ }
94
+ }
95
+
96
+ console.log("Redirecting to:", `${origin}${next}`);
97
+ console.log("=== AUTH CALLBACK END ===\n");
98
+ return NextResponse.redirect(`${origin}${next}`)
99
+ }
frontend/src/app/billing/page.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { supabaseClient } from "@/lib/supabase-client";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Loader2, CreditCard, Zap, CheckCircle, AlertCircle } from "lucide-react";
8
+ import { useRouter } from "next/navigation";
9
+ import Link from "next/link";
10
+
11
+ export default function BillingPage() {
12
+ const [user, setUser] = useState<any>(null);
13
+ const [credits, setCredits] = useState(0);
14
+ const [loading, setLoading] = useState(true);
15
+ const router = useRouter();
16
+ const supabase = supabaseClient;
17
+
18
+ useEffect(() => {
19
+ const getData = async () => {
20
+ const { data: { user } } = await supabase.auth.getUser();
21
+ if (!user) {
22
+ router.push("/auth/signin");
23
+ return;
24
+ }
25
+ setUser(user);
26
+
27
+ // Fetch user credits only
28
+ const { data: userData } = await supabase
29
+ .from("users")
30
+ .select("credits")
31
+ .eq("id", user.id)
32
+ .single();
33
+
34
+ setCredits(userData?.credits ?? 0);
35
+ setLoading(false);
36
+ };
37
+ getData();
38
+ }, [supabase, router]);
39
+
40
+ if (loading) {
41
+ return (
42
+ <div className="flex h-screen items-center justify-center bg-slate-950">
43
+ <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
44
+ </div>
45
+ );
46
+ }
47
+
48
+ // Static plan info for now
49
+ const plan = {
50
+ name: "Free",
51
+ price: "$0",
52
+ features: [
53
+ "5 video credits per month",
54
+ "720p video quality",
55
+ "Standard generation speed"
56
+ ]
57
+ };
58
+
59
+ return (
60
+ <div className="min-h-screen bg-slate-950 text-slate-200 py-12 px-4 sm:px-6 lg:px-8">
61
+ <div className="max-w-4xl mx-auto space-y-8">
62
+ <div>
63
+ <h1 className="text-3xl font-bold text-white">Billing & Usage</h1>
64
+ <p className="text-slate-400 mt-2">Manage your subscription and view credit usage.</p>
65
+ </div>
66
+
67
+ <div className="grid gap-6 md:grid-cols-2">
68
+ {/* Current Plan */}
69
+ <Card className="bg-slate-900 border-slate-800">
70
+ <CardHeader>
71
+ <CardTitle className="text-white flex items-center gap-2">
72
+ <CreditCard className="h-5 w-5 text-blue-500" />
73
+ Current Plan
74
+ </CardTitle>
75
+ <CardDescription className="text-slate-400">
76
+ You are currently on the <span className="text-white font-medium">{plan.name} Tier</span>.
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardContent className="space-y-6">
80
+ <div className="flex items-center justify-between p-4 bg-slate-950/50 rounded-lg border border-slate-800">
81
+ <div>
82
+ <p className="text-sm font-medium text-slate-300">{plan.name} Plan</p>
83
+ <p className="text-2xl font-bold text-white">{plan.price}<span className="text-sm text-slate-500 font-normal">/month</span></p>
84
+ </div>
85
+ <div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
86
+ </div>
87
+
88
+ <div className="space-y-2">
89
+ {plan.features.map((feature: string) => (
90
+ <div key={feature} className="flex items-center gap-2 text-sm text-slate-300">
91
+ <CheckCircle className="h-4 w-4 text-green-500" />
92
+ <span>{feature}</span>
93
+ </div>
94
+ ))}
95
+ </div>
96
+
97
+ <Button asChild className="w-full bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 text-white border-0">
98
+ <Link href="/pricing">Upgrade Plan</Link>
99
+ </Button>
100
+ </CardContent>
101
+ </Card>
102
+
103
+ {/* Credit Usage */}
104
+ <Card className="bg-slate-900 border-slate-800">
105
+ <CardHeader>
106
+ <CardTitle className="text-white flex items-center gap-2">
107
+ <Zap className="h-5 w-5 text-yellow-500" />
108
+ Credit Usage
109
+ </CardTitle>
110
+ <CardDescription className="text-slate-400">
111
+ Your monthly generation credits.
112
+ </CardDescription>
113
+ </CardHeader>
114
+ <CardContent className="space-y-6">
115
+ <div className="space-y-2">
116
+ <div className="flex justify-between text-sm">
117
+ <span className="text-slate-300">Credits Remaining</span>
118
+ <span className="text-white font-mono">{credits} / 5</span>
119
+ </div>
120
+ <div className="h-3 bg-slate-950 rounded-full overflow-hidden border border-slate-800">
121
+ <div
122
+ className="h-full bg-gradient-to-r from-blue-500 to-violet-500 transition-all duration-500"
123
+ style={{ width: `${Math.min((credits / 5) * 100, 100)}%` }}
124
+ />
125
+ </div>
126
+ <p className="text-xs text-slate-500 pt-1">
127
+ Credits reset on the 1st of every month.
128
+ </p>
129
+ </div>
130
+
131
+ {credits === 0 && (
132
+ <div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex gap-3">
133
+ <AlertCircle className="h-5 w-5 text-red-400 shrink-0" />
134
+ <p className="text-sm text-red-300">
135
+ You have run out of credits. Upgrade your plan to continue generating videos.
136
+ </p>
137
+ </div>
138
+ )}
139
+ </CardContent>
140
+ </Card>
141
+ </div>
142
+
143
+ {/* Invoice History Placeholder */}
144
+ <Card className="bg-slate-900 border-slate-800">
145
+ <CardHeader>
146
+ <CardTitle className="text-white">Billing History</CardTitle>
147
+ </CardHeader>
148
+ <CardContent>
149
+ <div className="text-center py-8 text-slate-500 text-sm">
150
+ No invoices found.
151
+ </div>
152
+ </CardContent>
153
+ </Card>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
frontend/src/app/favicon.ico ADDED
frontend/src/app/globals.css ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #020617;
5
+ --foreground: #f8fafc;
6
+ --card: #020617;
7
+ --card-foreground: #f8fafc;
8
+ --popover: #020617;
9
+ --popover-foreground: #f8fafc;
10
+ --primary: #2563eb;
11
+ --primary-foreground: #f8fafc;
12
+ --secondary: #1e293b;
13
+ --secondary-foreground: #f8fafc;
14
+ --muted: #1e293b;
15
+ --muted-foreground: #94a3b8;
16
+ --accent: #1e293b;
17
+ --accent-foreground: #f8fafc;
18
+ --destructive: #7f1d1d;
19
+ --destructive-foreground: #f8fafc;
20
+ --border: #1e293b;
21
+ --input: #1e293b;
22
+ --ring: #1d4ed8;
23
+ --radius: 0.5rem;
24
+ }
25
+
26
+ @theme inline {
27
+ --color-background: var(--background);
28
+ --color-foreground: var(--foreground);
29
+ --color-card: var(--card);
30
+ --color-card-foreground: var(--card-foreground);
31
+ --color-popover: var(--popover);
32
+ --color-popover-foreground: var(--popover-foreground);
33
+ --color-primary: var(--primary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-secondary: var(--secondary);
36
+ --color-secondary-foreground: var(--secondary-foreground);
37
+ --color-muted: var(--muted);
38
+ --color-muted-foreground: var(--muted-foreground);
39
+ --color-accent: var(--accent);
40
+ --color-accent-foreground: var(--accent-foreground);
41
+ --color-destructive: var(--destructive);
42
+ --color-destructive-foreground: var(--destructive-foreground);
43
+ --color-border: var(--border);
44
+ --color-input: var(--input);
45
+ --color-ring: var(--ring);
46
+ --radius-sm: calc(var(--radius) - 2px);
47
+ --radius-md: calc(var(--radius) - 2px);
48
+ --radius-lg: var(--radius);
49
+ --font-sans: var(--font-inter);
50
+ --font-mono: monospace;
51
+ }
52
+
53
+ body {
54
+ background: var(--background);
55
+ color: var(--foreground);
56
+ font-family: Arial, Helvetica, sans-serif;
57
+ }
58
+
59
+ /* Strictly hide scrollbars for everything */
60
+ ::-webkit-scrollbar {
61
+ display: none !important;
62
+ width: 0 !important;
63
+ height: 0 !important;
64
+ background: transparent !important;
65
+ }
66
+
67
+ html,
68
+ body,
69
+ * {
70
+ -ms-overflow-style: none !important;
71
+ /* IE and Edge */
72
+ scrollbar-width: none !important;
73
+ /* Firefox */
74
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const inter = Inter({
7
+ subsets: ["latin"],
8
+ variable: "--font-inter",
9
+ });
10
+
11
+ export const metadata: Metadata = {
12
+ title: "VidSimplify - AI-Powered Animation Generator",
13
+ description: "Transform text, PDFs, and URLs into stunning educational animations powered by AI and Manim",
14
+ };
15
+
16
+
17
+
18
+ // ... imports
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en">
27
+ <body className={cn(
28
+ "min-h-screen bg-slate-950 font-sans antialiased",
29
+ inter.variable
30
+ )}>
31
+ {children}
32
+ </body>
33
+ </html>
34
+ );
35
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Navbar } from "@/components/landing/Navbar";
2
+ import { Hero } from "@/components/landing/Hero";
3
+ import { Showcase } from "@/components/landing/Showcase";
4
+ import { Footer } from "@/components/landing/Footer";
5
+ import { Check, Edit, Layers, Zap } from "lucide-react";
6
+
7
+ export default function Home() {
8
+ return (
9
+ <main className="min-h-screen bg-slate-950">
10
+ <Navbar />
11
+ <Hero />
12
+ <Showcase />
13
+
14
+ {/* Problem vs Solution Section */}
15
+ <section className="py-24 bg-slate-900">
16
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
17
+ <div className="grid md:grid-cols-2 gap-16 items-center">
18
+ <div>
19
+ <h2 className="text-3xl font-bold text-white mb-6">
20
+ The Problem with <span className="text-red-400">Random AI Video</span>
21
+ </h2>
22
+ <ul className="space-y-4">
23
+ {[
24
+ "Hallucinated facts and visuals",
25
+ "Inconsistent style and branding",
26
+ "Impossible to edit specific details",
27
+ "Black box generation process"
28
+ ].map((item, i) => (
29
+ <li key={i} className="flex items-center text-slate-400">
30
+ <div className="h-2 w-2 rounded-full bg-red-500 mr-3" />
31
+ {item}
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </div>
36
+ <div>
37
+ <h2 className="text-3xl font-bold text-white mb-6">
38
+ The <span className="text-blue-400">VidSimplify</span> Solution
39
+ </h2>
40
+ <ul className="space-y-4">
41
+ {[
42
+ "Mathematically precise animations",
43
+ "Granular control to edit every detail",
44
+ "Consistent, professional styling",
45
+ "Explainable and transparent generation"
46
+ ].map((item, i) => (
47
+ <li key={i} className="flex items-center text-slate-300">
48
+ <div className="h-6 w-6 rounded-full bg-blue-500/20 text-blue-400 flex items-center justify-center mr-3">
49
+ <Check className="h-4 w-4" />
50
+ </div>
51
+ {item}
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </section>
59
+
60
+ {/* Features Grid */}
61
+ <section id="features" className="py-24 bg-slate-950">
62
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
63
+ <div className="text-center mb-16">
64
+ <h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
65
+ Engineered for Complexity
66
+ </h2>
67
+ <p className="mt-4 text-lg text-slate-400 max-w-2xl mx-auto">
68
+ In a growing complex world, we need tools that can explain the working of any system, research, or product in depth.
69
+ </p>
70
+ </div>
71
+
72
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
73
+ {[
74
+ {
75
+ title: "Multi-Dimensional",
76
+ description: "Generate 2D and 3D visualizations that capture every angle of your concept.",
77
+ icon: <Layers className="h-8 w-8 text-blue-400" />
78
+ },
79
+ {
80
+ title: "Fully Editable",
81
+ description: "Don't like a color? Want to change a speed? Tweak parameters instantly.",
82
+ icon: <Edit className="h-8 w-8 text-violet-400" />
83
+ },
84
+ {
85
+ title: "Instant Repurposing",
86
+ description: "Generate once, then tweak for different formats, audiences, and platforms.",
87
+ icon: <Zap className="h-8 w-8 text-indigo-400" />
88
+ }
89
+ ].map((feature, i) => (
90
+ <div key={i} className="bg-slate-900/50 border border-white/5 p-8 rounded-2xl hover:bg-slate-900 transition-colors">
91
+ <div className="mb-6 bg-slate-800/50 w-16 h-16 rounded-xl flex items-center justify-center border border-white/5">
92
+ {feature.icon}
93
+ </div>
94
+ <h3 className="text-xl font-semibold text-white mb-3">{feature.title}</h3>
95
+ <p className="text-slate-400 leading-relaxed">{feature.description}</p>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ </div>
100
+ </section>
101
+
102
+ <Footer />
103
+ </main>
104
+ );
105
+ }
frontend/src/app/pricing/page.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Check } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import Link from "next/link";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export default function PricingPage() {
9
+ const tiers = [
10
+ {
11
+ name: "Free",
12
+ price: "$0",
13
+ description: "Perfect for experimenting and personal projects.",
14
+ features: [
15
+ "5 video credits per month",
16
+ "720p video quality",
17
+ "Standard generation speed",
18
+ "Public community access",
19
+ ],
20
+ cta: "Get Started",
21
+ href: "/app",
22
+ popular: false,
23
+ },
24
+ {
25
+ name: "Pro",
26
+ price: "$29",
27
+ description: "For content creators and professionals.",
28
+ features: [
29
+ "50 video credits per month",
30
+ "1080p HD video quality",
31
+ "Fast generation speed",
32
+ "Priority support",
33
+ "Commercial usage rights",
34
+ "Remove watermarks",
35
+ ],
36
+ cta: "Upgrade to Pro",
37
+ href: "/auth/signin",
38
+ popular: true,
39
+ },
40
+ {
41
+ name: "Enterprise",
42
+ price: "Custom",
43
+ description: "For teams and high-volume needs.",
44
+ features: [
45
+ "Unlimited video credits",
46
+ "4K video quality",
47
+ "Instant generation",
48
+ "Dedicated account manager",
49
+ "Custom branding & templates",
50
+ "API access",
51
+ ],
52
+ cta: "Contact Sales",
53
+ href: "mailto:sales@vidsimplify.com",
54
+ popular: false,
55
+ },
56
+ ];
57
+
58
+ return (
59
+ <div className="min-h-screen bg-slate-950 text-slate-200 py-24 px-4 sm:px-6 lg:px-8">
60
+ <div className="max-w-7xl mx-auto">
61
+ <div className="text-center mb-16">
62
+ <h1 className="text-4xl font-bold tracking-tight text-white sm:text-5xl mb-4">
63
+ Simple, transparent pricing
64
+ </h1>
65
+ <p className="text-xl text-slate-400 max-w-2xl mx-auto">
66
+ Choose the plan that best fits your needs. All plans include access to our core AI generation features.
67
+ </p>
68
+ </div>
69
+
70
+ <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
71
+ {tiers.map((tier) => (
72
+ <div
73
+ key={tier.name}
74
+ className={cn(
75
+ "relative flex flex-col p-8 bg-slate-900/50 backdrop-blur-sm border rounded-2xl transition-all duration-200 hover:scale-105",
76
+ tier.popular
77
+ ? "border-blue-500/50 shadow-2xl shadow-blue-500/10 z-10 scale-105"
78
+ : "border-white/10 hover:border-white/20"
79
+ )}
80
+ >
81
+ {tier.popular && (
82
+ <div className="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-gradient-to-r from-blue-600 to-violet-600 text-white text-sm font-medium rounded-full shadow-lg">
83
+ Most Popular
84
+ </div>
85
+ )}
86
+
87
+ <div className="mb-8">
88
+ <h3 className="text-lg font-semibold text-white mb-2">{tier.name}</h3>
89
+ <div className="flex items-baseline gap-1">
90
+ <span className="text-4xl font-bold text-white">{tier.price}</span>
91
+ {tier.price !== "Custom" && <span className="text-slate-500">/month</span>}
92
+ </div>
93
+ <p className="mt-4 text-sm text-slate-400">{tier.description}</p>
94
+ </div>
95
+
96
+ <ul className="space-y-4 mb-8 flex-1">
97
+ {tier.features.map((feature) => (
98
+ <li key={feature} className="flex items-start gap-3 text-sm text-slate-300">
99
+ <Check className="h-5 w-5 text-blue-500 shrink-0" />
100
+ <span>{feature}</span>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+
105
+ <Button
106
+ asChild
107
+ className={cn(
108
+ "w-full",
109
+ tier.popular
110
+ ? "bg-blue-600 hover:bg-blue-700 text-white"
111
+ : "bg-slate-800 hover:bg-slate-700 text-white"
112
+ )}
113
+ variant={tier.popular ? "default" : "outline"}
114
+ >
115
+ <Link href={tier.href}>{tier.cta}</Link>
116
+ </Button>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
frontend/src/app/profile/page.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { supabaseClient } from "@/lib/supabase-client";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { Loader2, User, Mail, Save } from "lucide-react";
10
+ import { useRouter } from "next/navigation";
11
+
12
+ export default function ProfilePage() {
13
+ const [user, setUser] = useState<any>(null);
14
+ const [loading, setLoading] = useState(true);
15
+ const [fullName, setFullName] = useState("");
16
+ const [saving, setSaving] = useState(false);
17
+ const router = useRouter();
18
+ const supabase = supabaseClient;
19
+
20
+ useEffect(() => {
21
+ const getUser = async () => {
22
+ const { data: { user } } = await supabase.auth.getUser();
23
+ if (!user) {
24
+ router.push("/auth/signin");
25
+ return;
26
+ }
27
+ setUser(user);
28
+ setFullName(user.user_metadata?.full_name || "");
29
+ setLoading(false);
30
+ };
31
+ getUser();
32
+ }, [supabase, router]);
33
+
34
+ const handleUpdateProfile = async () => {
35
+ setSaving(true);
36
+ const { error } = await supabase.auth.updateUser({
37
+ data: { full_name: fullName },
38
+ });
39
+
40
+ if (error) {
41
+ alert("Error updating profile");
42
+ } else {
43
+ alert("Profile updated successfully");
44
+ router.refresh();
45
+ }
46
+ setSaving(false);
47
+ };
48
+
49
+ if (loading) {
50
+ return (
51
+ <div className="flex h-screen items-center justify-center bg-slate-950">
52
+ <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <div className="min-h-screen bg-slate-950 text-slate-200 py-12 px-4 sm:px-6 lg:px-8">
59
+ <div className="max-w-2xl mx-auto space-y-8">
60
+ <div>
61
+ <h1 className="text-3xl font-bold text-white">Profile Settings</h1>
62
+ <p className="text-slate-400 mt-2">Manage your account settings and preferences.</p>
63
+ </div>
64
+
65
+ <Card className="bg-slate-900 border-slate-800">
66
+ <CardHeader>
67
+ <CardTitle className="text-white">Personal Information</CardTitle>
68
+ <CardDescription className="text-slate-400">
69
+ Update your personal details here.
70
+ </CardDescription>
71
+ </CardHeader>
72
+ <CardContent className="space-y-6">
73
+ <div className="flex items-center gap-6">
74
+ <Avatar className="h-20 w-20 border-2 border-slate-700">
75
+ <AvatarImage src={user?.user_metadata?.avatar_url} />
76
+ <AvatarFallback className="text-lg bg-slate-800 text-slate-300">
77
+ {user?.email?.charAt(0).toUpperCase()}
78
+ </AvatarFallback>
79
+ </Avatar>
80
+ <div>
81
+ <p className="text-sm font-medium text-slate-300 mb-1">Profile Picture</p>
82
+ <p className="text-xs text-slate-500">
83
+ Managed by your identity provider (Google).
84
+ </p>
85
+ </div>
86
+ </div>
87
+
88
+ <div className="space-y-2">
89
+ <label className="text-sm font-medium text-slate-300">Full Name</label>
90
+ <div className="relative">
91
+ <User className="absolute left-3 top-3 h-4 w-4 text-slate-500" />
92
+ <Input
93
+ value={fullName}
94
+ onChange={(e) => setFullName(e.target.value)}
95
+ className="pl-9 bg-slate-950 border-slate-700 text-slate-200 focus:border-blue-500"
96
+ />
97
+ </div>
98
+ </div>
99
+
100
+ <div className="space-y-2">
101
+ <label className="text-sm font-medium text-slate-300">Email Address</label>
102
+ <div className="relative">
103
+ <Mail className="absolute left-3 top-3 h-4 w-4 text-slate-500" />
104
+ <Input
105
+ value={user?.email}
106
+ disabled
107
+ className="pl-9 bg-slate-950/50 border-slate-800 text-slate-500 cursor-not-allowed"
108
+ />
109
+ </div>
110
+ </div>
111
+
112
+ <div className="pt-4">
113
+ <Button
114
+ onClick={handleUpdateProfile}
115
+ disabled={saving}
116
+ className="bg-blue-600 hover:bg-blue-700 text-white"
117
+ >
118
+ {saving ? (
119
+ <>
120
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
121
+ Saving...
122
+ </>
123
+ ) : (
124
+ <>
125
+ <Save className="mr-2 h-4 w-4" />
126
+ Save Changes
127
+ </>
128
+ )}
129
+ </Button>
130
+ </div>
131
+ </CardContent>
132
+ </Card>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
frontend/src/components/app/VideoGenerator.tsx ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { motion, AnimatePresence } from "framer-motion";
6
+ import { Loader2, Upload, Link as LinkIcon, FileText, CheckCircle, AlertCircle, Download, Play, Layout, Clock, Settings, Menu, X, Video, ChevronRight, Sparkles } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input, Textarea } from "@/components/ui/input";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { api, type InputType, type Category, type JobStatus } from "@/lib/api";
11
+ import { cn } from "@/lib/utils";
12
+ import Link from "next/link";
13
+ import { supabaseClient } from "@/lib/supabase-client";
14
+
15
+ export function VideoGenerator() {
16
+ const searchParams = useSearchParams();
17
+ const [inputType, setInputType] = useState<InputType>("text");
18
+ const [category, setCategory] = useState<Category>("tech_system");
19
+ const [content, setContent] = useState("");
20
+ const [isGenerating, setIsGenerating] = useState(false);
21
+ const [currentJob, setCurrentJob] = useState<JobStatus | null>(null);
22
+ const [error, setError] = useState<string | null>(null);
23
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
24
+ const [credits, setCredits] = useState(0);
25
+ const [demoVideoUrl, setDemoVideoUrl] = useState<string | null>(null);
26
+
27
+ const supabase = supabaseClient;
28
+
29
+ // Fetch credits
30
+ useEffect(() => {
31
+ const fetchCredits = async () => {
32
+ const { data: { user } } = await supabase.auth.getUser();
33
+ if (user) {
34
+ const { data } = await supabase
35
+ .from("users")
36
+ .select("credits")
37
+ .eq("id", user.id)
38
+ .single();
39
+ setCredits(data?.credits ?? 0);
40
+ }
41
+ };
42
+ fetchCredits();
43
+ }, [supabase]);
44
+
45
+ // Initialize from URL params
46
+ useEffect(() => {
47
+ const promptParam = searchParams.get("prompt");
48
+ const categoryParam = searchParams.get("category");
49
+ const demoUrlParam = searchParams.get("demoVideoUrl");
50
+
51
+ if (promptParam) {
52
+ setContent(promptParam);
53
+ setInputType("text");
54
+ }
55
+
56
+ if (categoryParam && ["tech_system", "product_startup", "mathematical"].includes(categoryParam)) {
57
+ setCategory(categoryParam as Category);
58
+ }
59
+
60
+ if (demoUrlParam) {
61
+ setDemoVideoUrl(demoUrlParam);
62
+ // Removed auto-start: simulateGeneration(demoUrlParam);
63
+ } else {
64
+ setDemoVideoUrl(null);
65
+ }
66
+ }, [searchParams]);
67
+
68
+ const simulateGeneration = (videoUrl: string) => {
69
+ setIsGenerating(true);
70
+ const jobId = "demo-" + Date.now();
71
+
72
+ const steps = [
73
+ { percentage: 10, message: "Analyzing Input..." },
74
+ { percentage: 30, message: "Generating Script..." },
75
+ { percentage: 60, message: "Validating Code..." },
76
+ { percentage: 85, message: "Rendering Frames..." },
77
+ { percentage: 100, message: "Finalizing..." }
78
+ ];
79
+
80
+ let step = 0;
81
+ // Clear any existing job
82
+ setCurrentJob({
83
+ job_id: jobId,
84
+ status: "pending",
85
+ progress: { percentage: 0, message: "Initializing..." },
86
+ created_at: new Date().toISOString()
87
+ });
88
+
89
+ const interval = setInterval(() => {
90
+ if (step >= steps.length) {
91
+ clearInterval(interval);
92
+ setCurrentJob({
93
+ job_id: jobId,
94
+ status: "completed",
95
+ progress: { percentage: 100, message: "Completed" },
96
+ created_at: new Date().toISOString()
97
+ });
98
+ setIsGenerating(false);
99
+ return;
100
+ }
101
+
102
+ setCurrentJob({
103
+ job_id: jobId,
104
+ status: "rendering",
105
+ progress: steps[step],
106
+ created_at: new Date().toISOString()
107
+ });
108
+ step++;
109
+ }, 1000); // 1 second per step for a nice flow
110
+ };
111
+
112
+ // Poll for job status
113
+ useEffect(() => {
114
+ let interval: NodeJS.Timeout;
115
+
116
+ // Only poll if it's NOT a demo job (demo jobs start with "demo-")
117
+ if (currentJob && ["pending", "generating_code", "rendering"].includes(currentJob.status) && !currentJob.job_id.startsWith("demo-")) {
118
+ interval = setInterval(async () => {
119
+ try {
120
+ const status = await api.getJobStatus(currentJob.job_id);
121
+ setCurrentJob(status);
122
+
123
+ if (status.status === "failed") {
124
+ setError(status.error || "Job failed");
125
+ setIsGenerating(false);
126
+ } else if (status.status === "completed") {
127
+ setIsGenerating(false);
128
+ }
129
+ } catch (e) {
130
+ console.error("Polling error", e);
131
+ }
132
+ }, 2000);
133
+ }
134
+
135
+ return () => clearInterval(interval);
136
+ }, [currentJob]);
137
+
138
+ const handleSubmit = async (e: React.FormEvent) => {
139
+ e.preventDefault();
140
+ if (!content) return;
141
+
142
+ // If we have a demo video URL, simulate the generation instead of calling the API
143
+ if (demoVideoUrl) {
144
+ simulateGeneration(demoVideoUrl);
145
+ return;
146
+ }
147
+
148
+ setIsGenerating(true);
149
+ setError(null);
150
+ setCurrentJob(null);
151
+ // setDemoVideoUrl(null); // Don't clear it here, we might want to keep it if user retries?
152
+ // Actually, if it's a real generation, we should probably clear it.
153
+ // But wait, if we are here, demoVideoUrl is NULL (because of the check above).
154
+ // So we don't need to clear it.
155
+
156
+ try {
157
+ const job = await api.createVideo(content, inputType, category);
158
+ setCurrentJob({
159
+ job_id: job.job_id,
160
+ status: "pending",
161
+ progress: { percentage: 0, message: "Initializing system..." },
162
+ created_at: new Date().toISOString()
163
+ });
164
+ // Optimistically update credits
165
+ setCredits(prev => Math.max(0, prev - 1));
166
+ } catch (e: any) {
167
+ setError(e.message);
168
+ setIsGenerating(false);
169
+ }
170
+ };
171
+
172
+ const categories: { value: Category; label: string; description: string }[] = [
173
+ { value: "tech_system", label: "Tech & Systems", description: "Architecture, Data Flow, APIs" },
174
+ { value: "product_startup", label: "Product Demo", description: "Features, Value Prop, UI/UX" },
175
+ { value: "mathematical", label: "Math & Research", description: "Equations, Graphs, Concepts" },
176
+ ];
177
+
178
+ const SHOWCASE_KEYWORDS = [
179
+ "database sharding",
180
+ "kafka",
181
+ "transformers",
182
+ "quantum entanglement",
183
+ "netflix",
184
+ "black hole",
185
+ "sorting algorithms",
186
+ "uber",
187
+ "sorting",
188
+ "bubble sort",
189
+ "url shortener"
190
+ ];
191
+
192
+ const isShowcasePrompt = SHOWCASE_KEYWORDS.some(keyword => (content || "").toLowerCase().includes(keyword));
193
+ const canEdit = credits > 0;
194
+ const canGenerate = canEdit || isShowcasePrompt;
195
+
196
+ return (
197
+ <div className="flex h-screen bg-slate-950 overflow-hidden">
198
+ {/* Mobile Sidebar Overlay */}
199
+ <AnimatePresence>
200
+ {isSidebarOpen && (
201
+ <motion.div
202
+ initial={{ opacity: 0 }}
203
+ animate={{ opacity: 1 }}
204
+ exit={{ opacity: 0 }}
205
+ onClick={() => setIsSidebarOpen(false)}
206
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 md:hidden"
207
+ />
208
+ )}
209
+ </AnimatePresence>
210
+
211
+ {/* Sidebar */}
212
+ <motion.div
213
+ className={cn(
214
+ "fixed inset-y-0 left-0 z-50 w-72 bg-slate-900/50 backdrop-blur-xl border-r border-white/5 flex flex-col transition-transform duration-300 md:translate-x-0 md:static",
215
+ isSidebarOpen ? "translate-x-0" : "-translate-x-full"
216
+ )}
217
+ >
218
+ <div className="p-6 border-b border-white/5 flex items-center justify-between">
219
+ <Link href="/" className="flex items-center gap-2">
220
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-violet-600">
221
+ <Video className="h-4 w-4 text-white" />
222
+ </div>
223
+ <span className="text-lg font-bold text-white">VidSimplify</span>
224
+ </Link>
225
+ <button onClick={() => setIsSidebarOpen(false)} className="md:hidden text-slate-400 hover:text-white">
226
+ <X className="h-5 w-5" />
227
+ </button>
228
+ </div>
229
+
230
+ <div className="flex-1 py-6 px-4 space-y-2 overflow-y-auto custom-scrollbar">
231
+ <Button variant="ghost" className="w-full justify-start text-blue-400 bg-blue-500/10 hover:bg-blue-500/20 hover:text-blue-300 font-medium">
232
+ <Layout className="mr-3 h-4 w-4" />
233
+ Create New
234
+ </Button>
235
+ <Button variant="ghost" className="w-full justify-start text-slate-400 hover:text-white hover:bg-white/5">
236
+ <Clock className="mr-3 h-4 w-4" />
237
+ History
238
+ </Button>
239
+ <Button variant="ghost" className="w-full justify-start text-slate-400 hover:text-white hover:bg-white/5">
240
+ <Settings className="mr-3 h-4 w-4" />
241
+ Settings
242
+ </Button>
243
+ </div>
244
+
245
+ <div className="p-4 border-t border-white/5 bg-slate-900/50">
246
+ <div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-4 border border-white/5">
247
+ <div className="flex justify-between items-center mb-2">
248
+ <div className="text-xs font-medium text-slate-300">Credits</div>
249
+ <div className="text-xs font-mono text-blue-400">{credits} / 5</div>
250
+ </div>
251
+ <div className="h-1.5 bg-slate-700/50 rounded-full overflow-hidden mb-3">
252
+ <div
253
+ className="h-full bg-gradient-to-r from-blue-500 to-violet-500 rounded-full transition-all duration-500"
254
+ style={{ width: `${Math.min((credits / 5) * 100, 100)}%` }}
255
+ ></div>
256
+ </div>
257
+ <Button
258
+ size="sm"
259
+ variant="outline"
260
+ className="w-full text-xs h-8 border-slate-700 hover:bg-slate-800 text-slate-300"
261
+ onClick={() => window.open("https://calendly.com/aditya-vidsimplify/demo-call", "_blank")}
262
+ >
263
+ Upgrade Plan
264
+ </Button>
265
+ </div>
266
+ </div>
267
+ </motion.div>
268
+
269
+ {/* Main Content */}
270
+ <div className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
271
+ {/* App Header */}
272
+ <header className="h-16 border-b border-white/5 bg-slate-950/50 backdrop-blur-md flex items-center justify-between px-4 sm:px-8 z-30">
273
+ <div className="flex items-center gap-4">
274
+ <button onClick={() => setIsSidebarOpen(true)} className="md:hidden text-slate-400 hover:text-white p-1">
275
+ <Menu className="h-6 w-6" />
276
+ </button>
277
+ <h1 className="text-lg font-semibold text-white flex items-center gap-2">
278
+ <span className="text-slate-500 font-normal">Project /</span> Untitled Animation
279
+ </h1>
280
+ </div>
281
+ <div className="flex items-center gap-4">
282
+ <Button
283
+ variant="ghost"
284
+ size="sm"
285
+ className="hidden sm:flex text-slate-400 hover:text-white font-medium"
286
+ onClick={() => window.open("https://calendly.com/aditya-vidsimplify/demo-call", "_blank")}
287
+ >
288
+ Book a Call
289
+ </Button>
290
+ <div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-green-500/10 border border-green-500/20 text-xs font-medium text-green-400">
291
+ <div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
292
+ System Operational
293
+ </div>
294
+ <div className="w-8 h-8 rounded-full bg-gradient-to-tr from-blue-500 to-violet-500 border border-white/20 shadow-lg" />
295
+ </div>
296
+ </header>
297
+
298
+ <main className="flex-1 overflow-y-auto p-4 sm:p-8 custom-scrollbar">
299
+ <div className="max-w-6xl mx-auto">
300
+ <div className="grid lg:grid-cols-12 gap-8">
301
+
302
+ {/* Left Column: Input Configuration */}
303
+ <div className="lg:col-span-7 space-y-8">
304
+ <div>
305
+ <h2 className="text-2xl font-bold text-white mb-2">Configure Generation</h2>
306
+ <p className="text-slate-400">Define the parameters for your AI-generated animation.</p>
307
+ </div>
308
+
309
+ <form onSubmit={handleSubmit} className="space-y-8">
310
+ {/* Input Source */}
311
+ <div className="space-y-4">
312
+ <label className="text-sm font-medium text-slate-300 uppercase tracking-wider">Input Source</label>
313
+ <div className="grid grid-cols-3 gap-4">
314
+ {[
315
+ { id: "text", icon: FileText, label: "Text Prompt" },
316
+ { id: "url", icon: LinkIcon, label: "URL / Blog" },
317
+ { id: "pdf", icon: Upload, label: "PDF Document" }
318
+ ].map((type) => (
319
+ <button
320
+ key={type.id}
321
+ type="button"
322
+ onClick={() => setInputType(type.id as InputType)}
323
+ className={cn(
324
+ "flex flex-col items-center justify-center p-4 rounded-xl border transition-all duration-200",
325
+ inputType === type.id
326
+ ? "bg-blue-600/10 border-blue-500/50 text-blue-400 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
327
+ : "bg-slate-900/50 border-white/5 text-slate-400 hover:bg-slate-800 hover:border-white/10"
328
+ )}
329
+ >
330
+ <type.icon className="w-6 h-6 mb-2" />
331
+ <span className="text-sm font-medium">{type.label}</span>
332
+ </button>
333
+ ))}
334
+ </div>
335
+ </div>
336
+
337
+ {/* Animation Style */}
338
+ <div className="space-y-4">
339
+ <label className="text-sm font-medium text-slate-300 uppercase tracking-wider">Visual Style</label>
340
+ <div className="grid gap-3">
341
+ {categories.map((cat) => (
342
+ <button
343
+ key={cat.value}
344
+ type="button"
345
+ onClick={() => setCategory(cat.value)}
346
+ className={cn(
347
+ "flex items-center p-4 rounded-xl border text-left transition-all duration-200",
348
+ category === cat.value
349
+ ? "bg-violet-600/10 border-violet-500/50 shadow-[0_0_20px_rgba(139,92,246,0.15)]"
350
+ : "bg-slate-900/50 border-white/5 hover:bg-slate-800 hover:border-white/10"
351
+ )}
352
+ >
353
+ <div className={cn(
354
+ "w-10 h-10 rounded-lg flex items-center justify-center mr-4 transition-colors",
355
+ category === cat.value ? "bg-violet-500/20 text-violet-400" : "bg-slate-800 text-slate-400"
356
+ )}>
357
+ {cat.value === 'tech_system' && <Layout className="w-5 h-5" />}
358
+ {cat.value === 'product_startup' && <Sparkles className="w-5 h-5" />}
359
+ {cat.value === 'mathematical' && <Clock className="w-5 h-5" />}
360
+ </div>
361
+ <div>
362
+ <div className={cn("font-medium", category === cat.value ? "text-violet-300" : "text-slate-200")}>
363
+ {cat.label}
364
+ </div>
365
+ <div className="text-xs text-slate-500 mt-0.5">{cat.description}</div>
366
+ </div>
367
+ {category === cat.value && (
368
+ <div className="ml-auto text-violet-400">
369
+ <CheckCircle className="w-5 h-5" />
370
+ </div>
371
+ )}
372
+ </button>
373
+ ))}
374
+ </div>
375
+ </div>
376
+
377
+ {/* Content Input */}
378
+ <div className="space-y-4">
379
+ <label className="text-sm font-medium text-slate-300 uppercase tracking-wider">
380
+ {inputType === "text" ? "Description" : inputType === "url" ? "Source URL" : "Upload File"}
381
+ </label>
382
+
383
+ <div className="relative group">
384
+ <div className="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-violet-500 rounded-xl opacity-0 group-hover:opacity-20 transition duration-500 blur"></div>
385
+ {inputType === "text" ? (
386
+ <Textarea
387
+ placeholder="Explain the concept of neural networks using a simple analogy..."
388
+ className="relative min-h-[200px] text-base resize-none bg-slate-900/80 border-white/10 text-slate-200 focus:border-blue-500/50 focus:ring-blue-500/20 rounded-xl p-4"
389
+ value={content}
390
+ onChange={(e) => setContent(e.target.value)}
391
+ disabled={!canEdit}
392
+ />
393
+ ) : inputType === "url" ? (
394
+ <Input
395
+ placeholder="https://example.com/article"
396
+ value={content}
397
+ onChange={(e) => setContent(e.target.value)}
398
+ className="relative h-12 bg-slate-900/80 border-white/10 text-slate-200 focus:border-blue-500/50 focus:ring-blue-500/20 rounded-xl px-4"
399
+ disabled={!canEdit}
400
+ />
401
+ ) : (
402
+ <div className={cn(
403
+ "relative border-2 border-dashed border-slate-700 rounded-xl p-12 text-center transition-all",
404
+ canEdit ? "hover:border-blue-500/50 hover:bg-slate-900/50 cursor-pointer" : "opacity-50 cursor-not-allowed"
405
+ )}>
406
+ <Input
407
+ type="file"
408
+ accept=".pdf"
409
+ onChange={async (e) => {
410
+ const file = e.target.files?.[0];
411
+ if (file) {
412
+ const reader = new FileReader();
413
+ reader.onload = (event) => {
414
+ const base64 = event.target?.result as string;
415
+ const base64Content = base64.split(',')[1];
416
+ setContent(base64Content);
417
+ };
418
+ reader.readAsDataURL(file);
419
+ }
420
+ }}
421
+ className="hidden"
422
+ id="pdf-upload"
423
+ disabled={!canEdit}
424
+ />
425
+ <label htmlFor="pdf-upload" className={cn("w-full h-full block", canEdit ? "cursor-pointer" : "cursor-not-allowed")}>
426
+ <div className="w-16 h-16 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
427
+ <Upload className="h-8 w-8 text-slate-400 group-hover:text-blue-400 transition-colors" />
428
+ </div>
429
+ <p className="text-lg font-medium text-slate-300 mb-2">Click to upload PDF</p>
430
+ <p className="text-sm text-slate-500">Maximum file size 10MB</p>
431
+ </label>
432
+ {content && (
433
+ <div className="absolute top-4 right-4 flex items-center gap-2 text-green-400 text-xs bg-green-500/10 py-1.5 px-3 rounded-full border border-green-500/20">
434
+ <CheckCircle className="h-3 w-3" />
435
+ <span>Ready</span>
436
+ </div>
437
+ )}
438
+ </div>
439
+ )}
440
+ </div>
441
+ </div>
442
+
443
+ <Button
444
+ type="submit"
445
+ size="lg"
446
+ className="w-full h-16 text-lg font-semibold bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 text-white shadow-lg shadow-blue-500/25 rounded-xl transition-all hover:scale-[1.02] active:scale-[0.98]"
447
+ disabled={isGenerating || !content || !canGenerate}
448
+ >
449
+ {isGenerating ? (
450
+ <div className="flex items-center gap-3">
451
+ <Loader2 className="h-6 w-6 animate-spin" />
452
+ <span>Processing Request...</span>
453
+ </div>
454
+ ) : (
455
+ <div className="flex items-center gap-3">
456
+ <Play className="h-6 w-6 fill-current" />
457
+ <span>Generate Animation</span>
458
+ </div>
459
+ )}
460
+ </Button>
461
+ </form>
462
+ </div>
463
+
464
+ {/* Right Column: Preview & Status */}
465
+ <div className="lg:col-span-5 space-y-8">
466
+ <div>
467
+ <h2 className="text-2xl font-bold text-white mb-2">Live Preview</h2>
468
+ <p className="text-slate-400">Real-time generation status and output.</p>
469
+ </div>
470
+
471
+ <div className="sticky top-8">
472
+ <AnimatePresence mode="wait">
473
+ {currentJob ? (
474
+ <motion.div
475
+ initial={{ opacity: 0, y: 20 }}
476
+ animate={{ opacity: 1, y: 0 }}
477
+ exit={{ opacity: 0, y: -20 }}
478
+ className="space-y-6"
479
+ >
480
+ <Card className="bg-slate-900 border-white/10 overflow-hidden shadow-2xl">
481
+ <CardHeader className="border-b border-white/5 bg-slate-950/30 py-4">
482
+ <div className="flex items-center justify-between">
483
+ <CardTitle className="text-sm font-medium text-slate-300 flex items-center gap-2">
484
+ {currentJob.status === "completed" ? (
485
+ <span className="flex items-center gap-2 text-green-400">
486
+ <CheckCircle className="h-4 w-4" /> Completed
487
+ </span>
488
+ ) : currentJob.status === "failed" ? (
489
+ <span className="flex items-center gap-2 text-red-400">
490
+ <AlertCircle className="h-4 w-4" /> Failed
491
+ </span>
492
+ ) : (
493
+ <span className="flex items-center gap-2 text-blue-400">
494
+ <Loader2 className="h-4 w-4 animate-spin" /> Processing
495
+ </span>
496
+ )}
497
+ </CardTitle>
498
+ <div className="text-xs font-mono text-slate-500">{currentJob.job_id.slice(0, 8)}</div>
499
+ </div>
500
+ </CardHeader>
501
+
502
+ <CardContent className="p-0">
503
+ {/* Video Player or Progress State */}
504
+ <div className="aspect-video bg-black relative group">
505
+ {currentJob.status === "completed" ? (
506
+ <video
507
+ key={demoVideoUrl || currentJob.job_id}
508
+ src={demoVideoUrl || api.getVideoUrl(currentJob.job_id)}
509
+ controls
510
+ className="w-full h-full"
511
+ poster="/placeholder-video.jpg"
512
+ />
513
+ ) : (
514
+ <div className="absolute inset-0 flex flex-col items-center justify-center p-8 text-center">
515
+ <div className="relative w-24 h-24 mb-6">
516
+ <div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
517
+ <div className="absolute inset-0 rounded-full border-4 border-t-blue-500 border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
518
+ <div className="absolute inset-4 rounded-full bg-slate-800/50 backdrop-blur flex items-center justify-center">
519
+ <span className="text-sm font-bold text-white">{currentJob.progress.percentage}%</span>
520
+ </div>
521
+ </div>
522
+ <h3 className="text-lg font-medium text-white mb-2">Generating Animation</h3>
523
+ <p className="text-sm text-slate-400 max-w-xs mx-auto animate-pulse">
524
+ {currentJob.progress.message}
525
+ </p>
526
+ </div>
527
+ )}
528
+ </div>
529
+
530
+ {/* Actions */}
531
+ {currentJob.status === "completed" && (
532
+ <div className="p-4 bg-slate-900 border-t border-white/5">
533
+ <Button className="w-full bg-white text-slate-900 hover:bg-slate-200 font-medium" asChild>
534
+ <a href={demoVideoUrl || api.getVideoUrl(currentJob.job_id)} download>
535
+ <Download className="mr-2 h-4 w-4" />
536
+ Download MP4 (1080p)
537
+ </a>
538
+ </Button>
539
+ </div>
540
+ )}
541
+
542
+ {/* Error Message */}
543
+ {error && (
544
+ <div className="p-4 bg-red-500/10 border-t border-red-500/20">
545
+ <p className="text-sm text-red-400 flex items-start gap-2">
546
+ <AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
547
+ {error}
548
+ </p>
549
+ </div>
550
+ )}
551
+ </CardContent>
552
+ </Card>
553
+
554
+ {/* Process Steps (Visual Decoration) */}
555
+ {currentJob.status !== "completed" && currentJob.status !== "failed" && (
556
+ <div className="space-y-3">
557
+ {["Analyzing Input", "Generating Script", "Validating Code", "Rendering Frames"].map((step, i) => {
558
+ const currentStepIndex = Math.floor((currentJob.progress.percentage / 100) * 4);
559
+ const isActive = i === currentStepIndex;
560
+ const isCompleted = i < currentStepIndex;
561
+
562
+ return (
563
+ <div key={step} className="flex items-center gap-3 text-sm">
564
+ <div className={cn(
565
+ "w-6 h-6 rounded-full flex items-center justify-center border transition-colors",
566
+ isCompleted ? "bg-green-500 border-green-500 text-slate-900" :
567
+ isActive ? "border-blue-500 text-blue-500" : "border-slate-700 text-slate-700"
568
+ )}>
569
+ {isCompleted ? <CheckCircle className="w-4 h-4" /> : <div className={cn("w-2 h-2 rounded-full", isActive ? "bg-blue-500 animate-pulse" : "bg-slate-700")} />}
570
+ </div>
571
+ <span className={cn(
572
+ "transition-colors",
573
+ isCompleted ? "text-slate-300" :
574
+ isActive ? "text-white font-medium" : "text-slate-600"
575
+ )}>{step}</span>
576
+ </div>
577
+ );
578
+ })}
579
+ </div>
580
+ )}
581
+ </motion.div>
582
+ ) : (
583
+ <div className="h-[400px] rounded-2xl border-2 border-dashed border-slate-800 bg-slate-900/30 flex flex-col items-center justify-center text-slate-500 p-8 text-center">
584
+ <div className="w-20 h-20 rounded-full bg-slate-800/50 flex items-center justify-center mb-6">
585
+ <Video className="h-10 w-10 opacity-50" />
586
+ </div>
587
+ <h3 className="text-lg font-medium text-slate-300 mb-2">Ready to Generate</h3>
588
+ <p className="max-w-xs text-sm">
589
+ Configure your animation parameters on the left and click generate to see the magic happen.
590
+ </p>
591
+ </div>
592
+ )}
593
+ </AnimatePresence>
594
+ </div>
595
+ </div>
596
+ </div>
597
+ </div>
598
+ </main>
599
+ </div>
600
+ </div>
601
+ );
602
+ }
frontend/src/components/auth/LoginButton.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { supabaseClient } from "@/lib/supabase-client";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ interface LoginButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8
+ text?: string;
9
+ variant?: "default" | "outline" | "secondary" | "ghost";
10
+ redirectTo?: string;
11
+ }
12
+
13
+ export function LoginButton({
14
+ text = "Sign In",
15
+ variant = "default",
16
+ className,
17
+ redirectTo = "/",
18
+ ...props
19
+ }: LoginButtonProps) {
20
+
21
+ const handleLogin = async () => {
22
+ await supabaseClient.auth.signInWithOAuth({
23
+ provider: "google",
24
+ options: {
25
+ redirectTo: `${location.origin}/auth/callback?next=${redirectTo}`,
26
+ },
27
+ });
28
+ };
29
+
30
+ return (
31
+ <Button
32
+ variant={variant}
33
+ className={cn(className)}
34
+ onClick={handleLogin}
35
+ {...props}
36
+ >
37
+ {text}
38
+ </Button>
39
+ );
40
+ }
frontend/src/components/auth/UserAuth.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from "@/components/ui/dropdown-menu";
13
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
14
+ import { LogOut, User, CreditCard } from "lucide-react";
15
+ import Link from "next/link";
16
+ import { useRouter } from "next/navigation";
17
+ import { supabaseClient } from "@/lib/supabase-client";
18
+ import { LoginButton } from "./LoginButton";
19
+ import { cn } from "@/lib/utils";
20
+
21
+ export function UserAuth() {
22
+ const [user, setUser] = useState<any>(null);
23
+ const supabase = supabaseClient;
24
+ const router = useRouter();
25
+
26
+ useEffect(() => {
27
+ const getUser = async () => {
28
+ const { data: { user } } = await supabase.auth.getUser();
29
+ setUser(user);
30
+ if (user) {
31
+ console.log("🔄 UserAuth: Calling RPC handle_user_login on mount");
32
+ await supabase.rpc('handle_user_login', {
33
+ user_email: user.email,
34
+ user_full_name: user.user_metadata.full_name || '',
35
+ user_avatar_url: user.user_metadata.avatar_url || ''
36
+ }).then(({ error }) => {
37
+ if (error) console.error("❌ RPC Error:", error);
38
+ else console.log("✅ RPC Success: User synced");
39
+ });
40
+ }
41
+ };
42
+ getUser();
43
+
44
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(async (_event, session) => {
45
+ setUser(session?.user ?? null);
46
+ if (session?.user) {
47
+ console.log("🔄 UserAuth: Calling RPC handle_user_login on auth change");
48
+ await supabase.rpc('handle_user_login', {
49
+ user_email: session.user.email,
50
+ user_full_name: session.user.user_metadata.full_name || '',
51
+ user_avatar_url: session.user.user_metadata.avatar_url || ''
52
+ }).then(({ error }) => {
53
+ if (error) console.error("❌ RPC Error:", error);
54
+ else {
55
+ console.log("✅ RPC Success: User synced");
56
+ router.refresh();
57
+ }
58
+ });
59
+ }
60
+ router.refresh();
61
+ });
62
+
63
+ return () => subscription.unsubscribe();
64
+ }, [supabase, router]);
65
+
66
+ const handleLogin = async () => {
67
+ await supabase.auth.signInWithOAuth({
68
+ provider: "google",
69
+ options: {
70
+ redirectTo: `${location.origin}/auth/callback`,
71
+ },
72
+ });
73
+ };
74
+
75
+ const handleLogout = async () => {
76
+ await supabase.auth.signOut();
77
+ router.refresh();
78
+ };
79
+
80
+ if (!user) {
81
+ return (
82
+ <LoginButton text="Sign In" className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-6" />
83
+ );
84
+ }
85
+
86
+ return (
87
+ <DropdownMenu>
88
+ <DropdownMenuTrigger asChild>
89
+ <Button variant="ghost" className="relative h-10 w-10 rounded-full">
90
+ <Avatar className="h-10 w-10 border border-slate-700">
91
+ <AvatarImage src={user.user_metadata?.avatar_url} alt={user.user_metadata?.full_name} />
92
+ <AvatarFallback>{user.email?.charAt(0).toUpperCase()}</AvatarFallback>
93
+ </Avatar>
94
+ </Button>
95
+ </DropdownMenuTrigger>
96
+ <DropdownMenuContent className="w-56 bg-slate-900 border-slate-800 text-slate-200" align="end" forceMount>
97
+ <DropdownMenuLabel className="font-normal">
98
+ <div className="flex flex-col space-y-1">
99
+ <p className="text-sm font-medium leading-none text-white">{user.user_metadata?.full_name}</p>
100
+ <p className="text-xs leading-none text-slate-400">{user.email}</p>
101
+ </div>
102
+ </DropdownMenuLabel>
103
+ <DropdownMenuSeparator className="bg-slate-800" />
104
+ <DropdownMenuItem asChild>
105
+ <Link href="/profile" className="cursor-pointer hover:bg-slate-800 focus:bg-slate-800">
106
+ <User className="mr-2 h-4 w-4" />
107
+ <span>Profile</span>
108
+ </Link>
109
+ </DropdownMenuItem>
110
+ <DropdownMenuItem asChild>
111
+ <Link href="/billing" className="cursor-pointer hover:bg-slate-800 focus:bg-slate-800">
112
+ <CreditCard className="mr-2 h-4 w-4" />
113
+ <span>Billing</span>
114
+ </Link>
115
+ </DropdownMenuItem>
116
+ <DropdownMenuSeparator className="bg-slate-800" />
117
+ <DropdownMenuItem onClick={handleLogout} className="cursor-pointer text-red-400 hover:bg-red-950/30 focus:bg-red-950/30 hover:text-red-300 focus:text-red-300">
118
+ <LogOut className="mr-2 h-4 w-4" />
119
+ <span>Log out</span>
120
+ </DropdownMenuItem>
121
+ </DropdownMenuContent>
122
+ </DropdownMenu>
123
+ );
124
+ }
frontend/src/components/landing/Footer.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { Video, Github, Twitter, Linkedin } from "lucide-react";
3
+
4
+ export function Footer() {
5
+ return (
6
+ <footer className="bg-slate-950 border-t border-white/5 pt-16 pb-8">
7
+ <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
8
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
9
+ <div className="col-span-1 md:col-span-1">
10
+ <Link href="/" className="flex items-center gap-2 mb-6">
11
+ <img src="/logo.svg" alt="Logo" className="h-8 w-8" />
12
+ <span className="text-lg font-bold text-white">VidSimplify</span>
13
+ </Link>
14
+ <p className="text-slate-400 text-sm leading-relaxed mb-6">
15
+ Precision animation engine for the modern web. Explain complex systems with mathematical accuracy.
16
+ </p>
17
+ <div className="flex gap-4">
18
+ <Link href="#" className="text-slate-400 hover:text-white transition-colors">
19
+ <Twitter className="h-5 w-5" />
20
+ </Link>
21
+ <Link href="#" className="text-slate-400 hover:text-white transition-colors">
22
+ <Github className="h-5 w-5" />
23
+ </Link>
24
+ <Link href="#" className="text-slate-400 hover:text-white transition-colors">
25
+ <Linkedin className="h-5 w-5" />
26
+ </Link>
27
+ </div>
28
+ </div>
29
+
30
+ <div>
31
+ <h3 className="text-white font-semibold mb-4">Product</h3>
32
+ <ul className="space-y-3 text-sm text-slate-400">
33
+ <li><Link href="#features" className="hover:text-blue-400 transition-colors">Features</Link></li>
34
+ <li><Link href="#showcase" className="hover:text-blue-400 transition-colors">Showcase</Link></li>
35
+ <li><Link href="/api-docs" className="hover:text-blue-400 transition-colors">API</Link></li>
36
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Pricing</Link></li>
37
+ </ul>
38
+ </div>
39
+
40
+ <div>
41
+ <h3 className="text-white font-semibold mb-4">Resources</h3>
42
+ <ul className="space-y-3 text-sm text-slate-400">
43
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Documentation</Link></li>
44
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Blog</Link></li>
45
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Community</Link></li>
46
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Help Center</Link></li>
47
+ </ul>
48
+ </div>
49
+
50
+ <div>
51
+ <h3 className="text-white font-semibold mb-4">Legal</h3>
52
+ <ul className="space-y-3 text-sm text-slate-400">
53
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Privacy Policy</Link></li>
54
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Terms of Service</Link></li>
55
+ <li><Link href="#" className="hover:text-blue-400 transition-colors">Cookie Policy</Link></li>
56
+ </ul>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="border-t border-white/5 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
61
+ <p className="text-slate-500 text-sm">
62
+ © {new Date().getFullYear()} VidSimplify Inc. All rights reserved.
63
+ </p>
64
+ <div className="flex items-center gap-2 text-sm text-slate-500">
65
+ <div className="h-2 w-2 rounded-full bg-green-500"></div>
66
+ All Systems Operational
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </footer>
71
+ );
72
+ }