File size: 26,413 Bytes
0805c5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
#!/usr/bin/env python3
"""
NeuroAnim Gradio Web Interface

A comprehensive web UI for generating educational STEM animations with:
- Topic input and configuration
- Real-time progress tracking
- Video preview and download
- Generated content display (narration, code, quiz)
- Error handling and logging
"""

import asyncio
import logging
import os
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, Tuple

import gradio as gr
from dotenv import load_dotenv

from orchestrator import NeuroAnimOrchestrator

load_dotenv()

# Set up logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


def format_quiz_markdown(quiz_text: str) -> str:
    """Format quiz text into a nice markdown display."""
    if not quiz_text or quiz_text == "Not available":
        return "โ“ No quiz generated yet."

    # If it's already formatted or looks good, return as is with some styling
    formatted = f"## ๐Ÿ“ Assessment Questions\n\n{quiz_text}"

    # Try to add some structure if it's plain text
    lines = quiz_text.split("\n")
    formatted_lines = []
    question_num = 0

    for line in lines:
        line = line.strip()
        if not line:
            formatted_lines.append("")
            continue

        # Detect question patterns
        if line.lower().startswith(("q:", "question", "q.", f"{question_num + 1}.")):
            question_num += 1
            formatted_lines.append(f"\n### Question {question_num}")
            # Remove the question prefix
            clean_line = line.split(":", 1)[-1].strip() if ":" in line else line
            formatted_lines.append(f"**{clean_line}**\n")
        elif line.lower().startswith(("a)", "b)", "c)", "d)", "a.", "b.", "c.", "d.")):
            # Format multiple choice options
            formatted_lines.append(f"- {line}")
        elif line.lower().startswith(("answer:", "a:", "correct:")):
            # Format answers
            formatted_lines.append(f"\n> โœ… {line}\n")
        else:
            formatted_lines.append(line)

    # If we detected structure, use the formatted version
    if question_num > 0:
        return "## ๐Ÿ“ Assessment Questions\n\n" + "\n".join(formatted_lines)

    # Otherwise return with basic formatting
    return formatted


class NeuroAnimApp:
    """Main application class for Gradio interface."""

    def __init__(self):
        self.orchestrator: Optional[NeuroAnimOrchestrator] = None
        self.current_task: Optional[asyncio.Task] = None
        self.is_generating = False
        self.event_loop: Optional[asyncio.AbstractEventLoop] = None
        self.current_progress = None  # Store progress callback for dynamic updates

    async def initialize_orchestrator(self):
        """Initialize the orchestrator if not already done."""
        if self.orchestrator is None:
            self.orchestrator = NeuroAnimOrchestrator()
            await self.orchestrator.initialize()
            logger.info("Orchestrator initialized successfully")

    async def cleanup_orchestrator(self):
        """Clean up orchestrator resources."""
        if self.orchestrator is not None:
            await self.orchestrator.cleanup()
            self.orchestrator = None
            logger.info("Orchestrator cleaned up")
    
    def cleanup_event_loop(self):
        """Clean up the event loop on application shutdown."""
        if self.event_loop is not None and not self.event_loop.is_closed():
            self.event_loop.close()
            self.event_loop = None
            logger.info("Event loop closed")

    async def generate_animation_async(
        self, topic: str, audience: str, duration: float, quality: str, progress=gr.Progress()
    ) -> Dict[str, Any]:
        """
        Generate animation with progress tracking.

        Args:
            topic: STEM topic to animate
            audience: Target audience level
            duration: Animation duration in minutes
            quality: Video quality (low, medium, high, production_quality)
            progress: Gradio progress tracker

        Returns:
            Results dictionary with generated content
        """
        try:
            self.is_generating = True

            # Validate inputs
            if not topic or len(topic.strip()) < 3:
                return {
                    "success": False,
                    "error": "Please provide a valid topic (at least 3 characters)",
                }

            if duration < 0.5 or duration > 10:
                return {
                    "success": False,
                    "error": "Duration must be between 0.5 and 10 minutes",
                }

            # Initialize orchestrator
            progress(0.05, desc="Initializing system...")
            await self.initialize_orchestrator()

            # Generate unique filename
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            safe_topic = "".join(c if c.isalnum() else "_" for c in topic)[:30]
            output_filename = f"{safe_topic}_{timestamp}.mp4"

            # Map quality from UI to orchestrator format
            quality_map = {
                "Low (480p, faster)": "low",
                "Medium (720p, balanced)": "medium",
                "High (1080p, slower)": "high",
                "Production (4K, slowest)": "production_quality",
            }
            quality_param = quality_map.get(quality, "medium")

            # Map audience from UI to orchestrator format
            audience_map = {
                "elementary": "elementary",
                "middle_school": "middle_school",
                "high_school": "high_school",
                "undergraduate": "college",  # Map to 'college' for LLM compatibility
                "phd": "graduate",  # Map to 'graduate' for LLM compatibility
                "general": "general",
            }
            audience_param = audience_map.get(audience, audience)

            # Dynamic progress tracking with step-based updates
            step_times = {}  # Track step start times
            step_index = [0]  # Current step index
            
            steps = [
                (0.1, "Planning concept"),
                (0.25, "Generating narration script"),
                (0.40, "Creating Manim animation code"),
                (0.55, "Rendering animation video"),
                (0.75, "Generating audio narration"),
                (0.90, "Merging video and audio"),
                (0.95, "Creating quiz questions"),
            ]
            
            import time
            
            def progress_callback(step_name: str, step_progress: float):
                """Callback for orchestrator to report progress."""
                # Find matching step
                for idx, (prog, desc) in enumerate(steps):
                    if desc.lower() in step_name.lower():
                        step_index[0] = idx
                        
                        # Track timing
                        current_time = time.time()
                        if step_name not in step_times:
                            step_times[step_name] = current_time
                        elapsed = current_time - step_times[step_name]
                        
                        # Add timing info for long steps
                        if elapsed > 30:  # Show message if step takes more than 30s
                            desc_with_time = f"{desc} (taking longer than usual, please wait...)"
                        else:
                            desc_with_time = f"{desc}..."
                        
                        progress(prog, desc=desc_with_time)
                        return
                
                # If no match, use the provided progress directly
                progress(step_progress, desc=f"{step_name}...")

            # Start generation with dynamic progress
            result = await self.orchestrator.generate_animation(
                topic=topic,
                target_audience=audience_param,
                animation_length_minutes=duration,
                output_filename=output_filename,
                quality=quality_param,
                progress_callback=progress_callback,
            )
            
            progress(1.0, desc="Complete!")
            logger.info("Async generation completed, returning result")

            return result

        except Exception as e:
            logger.error(f"Generation failed: {e}", exc_info=True)
            return {"success": False, "error": str(e)}
        finally:
            self.is_generating = False

    def generate_animation_sync(
        self, topic: str, audience: str, duration: float, quality: str, progress=gr.Progress()
    ) -> Tuple[str, str, str, str, str, str]:
        """
        Synchronous wrapper for Gradio interface.

        Returns:
            Tuple of (video_path, status, narration, code, quiz, concept_plan)
        """
        try:
            # Reuse existing event loop or create a persistent one
            if self.event_loop is None or self.event_loop.is_closed():
                self.event_loop = asyncio.new_event_loop()
                asyncio.set_event_loop(self.event_loop)
                logger.info("Created new persistent event loop")
            else:
                asyncio.set_event_loop(self.event_loop)
                logger.info("Reusing existing event loop")

            logger.info("Starting event loop execution...")
            result = self.event_loop.run_until_complete(
                self.generate_animation_async(topic, audience, duration, quality, progress)
            )
            logger.info("Event loop execution completed")
            # DO NOT close the loop - keep it for subsequent generations

            if result["success"]:
                logger.info("Processing successful result...")
                video_path = result["output_file"]
                status = f"โœ… **Animation Generated Successfully!**\n\n**Topic:** {result['topic']}\n**Audience:** {result['target_audience']}\n**Output:** {os.path.basename(video_path)}"
                narration = result.get("narration", "Not available")
                code = result.get("manim_code", "Not available")
                quiz_raw = result.get("quiz", "Not available")
                quiz = format_quiz_markdown(quiz_raw)
                concept = result.get("concept_plan", "Not available")

                logger.info(f"Returning result to Gradio: {video_path}")
                return video_path, video_path, status, narration, code, quiz, concept
            else:
                error_msg = result.get("error", "Unknown error")
                status = f"โŒ **Generation Failed**\n\n{error_msg}"
                return None, None, status, "", "", "", ""

        except Exception as e:
            logger.error(f"Sync wrapper error: {e}", exc_info=True)
            status = f"๐Ÿ’ฅ **Unexpected Error**\n\n{str(e)}"
            return None, None, status, "", "", "", ""


def create_interface() -> gr.Blocks:
    """Create the Gradio interface."""

    app = NeuroAnimApp()

    # Custom CSS for better styling
    custom_css = """
    .main-title {
        text-align: center;
        color: #2563eb;
        font-size: 2.5em;
        font-weight: bold;
        margin-bottom: 0.5em;
    }
    .subtitle {
        text-align: center;
        color: #64748b;
        font-size: 1.2em;
        margin-bottom: 2em;
    }
    .status-box {
        padding: 1em;
        border-radius: 8px;
        margin: 1em 0;
    }
    .gradio-container {
        max-width: 1400px !important;
    }
    /* Video player styling */
    video {
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }
    /* Quiz and content styling */
    .markdown-text h2 {
        color: #1e40af;
        border-bottom: 2px solid #3b82f6;
        padding-bottom: 0.5em;
        margin-top: 1em;
    }
    .markdown-text h3 {
        color: #1e293b;
        margin-top: 1em;
    }
    .markdown-text blockquote {
        background-color: #f0fdf4;
        border-left: 4px solid #22c55e;
        padding: 0.5em 1em;
        margin: 1em 0;
    }
    /* Button styling */
    .primary {
        background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
    }
    /* Code block styling */
    .code-container {
        border-radius: 8px;
        margin: 1em 0;
    }
    """

    with gr.Blocks(title="NeuroAnim - STEM Animation Generator") as interface:
        # Apply custom CSS
        interface.css = custom_css
        # Header
        gr.HTML("""
        <div class="main-title">๐Ÿง  NeuroAnim</div>
        <div class="subtitle">AI-Powered Educational Animation Generator</div>
        """)

        with gr.Tabs() as tabs:
            # Main Generation Tab
            with gr.TabItem("๐ŸŽฌ Generate Animation", id=0):
                gr.Markdown("""
                ### Create Your Educational Animation
                Enter a mathematical or scientific concept, and NeuroAnim will generate a complete animated video with narration and quiz questions.
                """)

                with gr.Row():
                    with gr.Column(scale=1):
                        # Input Section
                        gr.Markdown("#### ๐Ÿ“ Animation Configuration")

                        topic_input = gr.Textbox(
                            label="Topic / Concept",
                            placeholder="e.g., Pythagorean Theorem, Photosynthesis, Newton's Laws, etc.",
                            lines=2,
                            info="Enter the STEM concept you want to explain",
                        )

                        with gr.Row():
                            audience_input = gr.Dropdown(
                                label="Target Audience",
                                choices=[
                                    "elementary",
                                    "middle_school",
                                    "high_school",
                                    "undergraduate",
                                    "phd",
                                    "general",
                                ],
                                value="high_school",
                                info="Select the appropriate education level",
                            )

                            duration_input = gr.Slider(
                                label="Duration (minutes)",
                                minimum=0.5,
                                maximum=10,
                                value=2.0,
                                step=0.5,
                                info="Animation length",
                            )

                        quality_input = gr.Dropdown(
                            label="Video Quality",
                            choices=[
                                "Low (480p, faster)",
                                "Medium (720p, balanced)",
                                "High (1080p, slower)",
                                "Production (4K, slowest)",
                            ],
                            value="Medium (720p, balanced)",
                            info="Higher quality takes longer to render",
                        )

                        generate_btn = gr.Button(
                            "๐Ÿš€ Generate Animation", variant="primary", size="lg"
                        )

                        status_output = gr.Markdown(
                            label="Status",
                            value="Ready to generate...",
                            elem_classes=["status-box"],
                        )

                        # Example inputs
                        gr.Markdown("#### ๐Ÿ’ก Example Topics")
                        gr.Examples(
                            examples=[
                                ["Pythagorean Theorem", "high_school", 2.0, "Medium (720p, balanced)"],
                                ["Laws of Motion", "middle_school", 2.5, "Low (480p, faster)"],
                                ["Binary Numbers", "middle_school", 1.5, "Medium (720p, balanced)"],
                                ["Photosynthesis Process", "elementary", 2.0, "Low (480p, faster)"],
                                ["Quadratic Formula", "high_school", 3.0, "Medium (720p, balanced)"],
                                ["Circle Area Derivation", "undergraduate", 2.5, "High (1080p, slower)"],
                            ],
                            inputs=[topic_input, audience_input, duration_input, quality_input],
                        )

                    with gr.Column(scale=1):
                        # Output Section
                        gr.Markdown("#### ๐ŸŽฅ Generated Animation")

                        video_output = gr.Video(
                            label="Animation Video", height=400, interactive=False
                        )

                        # Download button
                        download_file = gr.File(
                            label="๐Ÿ“ฅ Download Animation",
                            interactive=False,
                            visible=True,
                        )

                        gr.Markdown(
                            "**Tip:** Click the download button above or use the โ‹ฎ menu on the video player"
                        )

                # Additional outputs in accordion
                with gr.Accordion("๐Ÿ“„ View Generated Content", open=True):
                    with gr.Tabs():
                        with gr.TabItem("๐Ÿ“– Narration Script"):
                            narration_output = gr.Textbox(
                                label="Narration Text",
                                lines=8,
                                interactive=False,
                            )

                        with gr.TabItem("๐Ÿ’ป Manim Code"):
                            code_output = gr.Code(
                                label="Generated Python Code",
                                language="python",
                                interactive=False,
                                lines=15,
                            )

                        with gr.TabItem("โ“ Quiz Questions"):
                            quiz_output = gr.Markdown(
                                label="Assessment Questions",
                                value="Quiz will appear here after generation...",
                            )

                        with gr.TabItem("๐Ÿ“‹ Concept Plan"):
                            concept_output = gr.Textbox(
                                label="Educational Plan",
                                lines=10,
                                interactive=False,
                            )

                # Connect the generate button
                generate_btn.click(
                    fn=app.generate_animation_sync,
                    inputs=[topic_input, audience_input, duration_input, quality_input],
                    outputs=[
                        video_output,
                        download_file,
                        status_output,
                        narration_output,
                        code_output,
                        quiz_output,
                        concept_output,
                    ],
                    api_name="generate",
                )

            # About Tab
            with gr.TabItem("โ„น๏ธ About", id=1):
                gr.Markdown("""
                # About NeuroAnim

                NeuroAnim is an AI-powered educational animation generator that creates engaging STEM content automatically.

                ## ๐ŸŽฏ Features

                - **๐ŸŽจ Automatic Animation Generation**: Creates professional Manim animations from topic descriptions
                - **๐Ÿ—ฃ๏ธ AI Narration**: Generates educational narration scripts tailored to your audience
                - **๐Ÿ”Š Text-to-Speech**: Converts narration to high-quality audio with ElevenLabs or Hugging Face
                - **๐Ÿ“น Video Production**: Renders and merges video with synchronized audio
                - **โ“ Quiz Generation**: Creates assessment questions to test understanding
                - **๐ŸŽ“ Multi-Level Support**: Content appropriate for elementary through undergraduate levels

                ## ๐Ÿ”ง Technology Stack

                - **Manim Community Edition**: Mathematical animation engine
                - **Hugging Face Models**: AI-powered content generation
                - **ElevenLabs**: High-quality text-to-speech synthesis
                - **MCP (Model Context Protocol)**: Modular server architecture
                - **Gradio**: Interactive web interface

                ## ๐Ÿš€ How It Works

                1. **Concept Planning**: AI analyzes your topic and creates an educational plan
                2. **Script Writing**: Generates age-appropriate narration aligned with learning objectives
                3. **Code Generation**: Creates Manim Python code for visual representation
                4. **Rendering**: Executes Manim to produce the base animation
                5. **Audio Synthesis**: Converts narration to speech using TTS
                6. **Final Production**: Merges video and audio into complete animation
                7. **Assessment**: Generates quiz questions for the content

                ## ๐Ÿ“ Tips for Best Results

                - **Be Specific**: Instead of "math", try "solving linear equations" or "area of a circle"
                - **Choose Right Audience**: Match the complexity level to your target viewers
                - **Optimal Duration**: 1.5-3 minutes works best for most concepts
                - **Review Generated Content**: Check the narration and code tabs to see what was created
                - **Iterate**: If results aren't perfect, try rewording your topic or adjusting parameters

                ## ๐Ÿ”‘ Setup Requirements

                To use NeuroAnim, you need:
                - **Hugging Face API Key**: For AI content generation (required)
                - **ElevenLabs API Key**: For high-quality TTS (optional, falls back to HF TTS)

                Set these in your `.env` file:
                ```bash
                HUGGINGFACE_API_KEY=your_key_here
                ELEVENLABS_API_KEY=your_key_here  # Optional
                ```

                ## ๐Ÿ“š Example Use Cases

                - **Teachers**: Create engaging lesson materials
                - **Students**: Visualize complex concepts for better understanding
                - **Content Creators**: Produce educational YouTube/social media content
                - **Tutors**: Generate custom explanations for specific topics
                - **Course Developers**: Build comprehensive educational video libraries

                ## ๐Ÿค Contributing

                NeuroAnim is open source! Contributions are welcome:
                - Report bugs or suggest features via GitHub Issues
                - Submit pull requests with improvements
                - Share your generated animations with the community

                ## ๐Ÿ“„ License

                MIT License - Free to use for educational and commercial purposes

                ---

                Made with โค๏ธ for educational content creation
                """)

            # Settings Tab
            with gr.TabItem("โš™๏ธ Settings", id=2):
                gr.Markdown("""
                # System Configuration

                Configure API keys and system settings here.
                """)

                with gr.Group():
                    gr.Markdown("### ๐Ÿ”‘ API Keys")

                    hf_key_status = gr.Textbox(
                        label="Hugging Face API Key Status",
                        value="โœ… Configured"
                        if os.getenv("HUGGINGFACE_API_KEY")
                        else "โŒ Not Set",
                        interactive=False,
                    )

                    eleven_key_status = gr.Textbox(
                        label="ElevenLabs API Key Status",
                        value="โœ… Configured"
                        if os.getenv("ELEVENLABS_API_KEY")
                        else "โš ๏ธ Not Set (will use fallback TTS)",
                        interactive=False,
                    )

                    gr.Markdown("""
                    **To configure API keys:**
                    1. Create a `.env` file in the project root
                    2. Add your keys:
                       ```
                       HUGGINGFACE_API_KEY=your_hf_key
                       ELEVENLABS_API_KEY=your_elevenlabs_key
                       ```
                    3. Restart the application
                    """)

                with gr.Group():
                    gr.Markdown("### ๐Ÿ“Š System Info")

                    system_info = gr.Textbox(
                        label="System Status",
                        value=f"""
Output Directory: {Path("outputs").absolute()}
Working Directory: Temporary (auto-created)
Manim Version: Community Edition
Default Quality: Medium (720p, 30fps)
                        """.strip(),
                        interactive=False,
                        lines=6,
                    )

    return interface


def main():
    """Launch the Gradio application."""

    # Check for API keys
    if not os.getenv("HUGGINGFACE_API_KEY"):
        logger.warning("HUGGINGFACE_API_KEY not set! Generation will fail.")
        print("\nโš ๏ธ  WARNING: HUGGINGFACE_API_KEY environment variable not set!")
        print("Please set it in your .env file or environment.\n")

    if not os.getenv("ELEVENLABS_API_KEY"):
        logger.info("ELEVENLABS_API_KEY not set, will use fallback TTS")
        print(
            "\nโ„น๏ธ  Note: ELEVENLABS_API_KEY not set. Using fallback TTS (may have lower quality).\n"
        )

    # Create outputs directory
    Path("outputs").mkdir(exist_ok=True)

    # Build and launch interface
    interface = create_interface()

    logger.info("Launching Gradio interface...")

    interface.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
    )


if __name__ == "__main__":
    main()