File size: 8,259 Bytes
fff13d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""
NeuroAnim - STEM Animation Generator with LangGraph

Main entry point for the NeuroAnim system using LangGraph for workflow orchestration.
This version uses a single unified Manim MCP server and LangGraph for better modularity.
"""

import asyncio
import logging
import os
import sys
from pathlib import Path

from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from neuroanim import run_animation_pipeline
from utils.tts import TTSGenerator

# Load environment variables
load_dotenv()

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


class NeuroAnimApp:
    """Main application for NeuroAnim animation generation."""

    def __init__(
        self,
        hf_api_key: str = None,
        elevenlabs_api_key: str = None,
    ):
        """
        Initialize the NeuroAnim application.

        Args:
            hf_api_key: HuggingFace API key (optional, falls back to env var)
            elevenlabs_api_key: ElevenLabs API key (optional, falls back to env var)
        """
        self.hf_api_key = hf_api_key or os.getenv("HUGGINGFACE_API_KEY")
        self.elevenlabs_api_key = elevenlabs_api_key or os.getenv("ELEVENLABS_API_KEY")

        # Initialize TTS generator
        self.tts_generator = TTSGenerator(
            elevenlabs_api_key=self.elevenlabs_api_key,
            hf_api_key=self.hf_api_key,
            fallback_enabled=True,
        )

        # MCP session components
        self.mcp_session = None
        self._mcp_cm = None
        self._mcp_streams = None

    async def initialize(self):
        """Initialize the MCP server connection."""
        logger.info("πŸš€ Initializing NeuroAnim...")

        # Initialize Manim MCP server
        mcp_params = StdioServerParameters(
            command="python",
            args=["manim_mcp/server.py"],
            env=({"HUGGINGFACE_API_KEY": self.hf_api_key} if self.hf_api_key else None),
        )

        self._mcp_cm = stdio_client(mcp_params)
        self._mcp_streams = await self._mcp_cm.__aenter__()
        read_stream, write_stream = self._mcp_streams
        self.mcp_session = ClientSession(read_stream, write_stream)
        await self.mcp_session.__aenter__()
        await self.mcp_session.initialize()

        logger.info("βœ… Manim MCP server connected")

    async def cleanup(self):
        """Clean up resources."""
        logger.info("🧹 Cleaning up...")

        # Close MCP session
        if self.mcp_session:
            try:
                await self.mcp_session.__aexit__(None, None, None)
            except (Exception, asyncio.CancelledError) as e:
                logger.debug(f"Error closing MCP session: {e}")

        # Close stdio client context manager
        if self._mcp_cm:
            try:
                async with asyncio.timeout(2):
                    await self._mcp_cm.__aexit__(None, None, None)
            except (Exception, asyncio.CancelledError, TimeoutError) as e:
                logger.debug(f"Error closing MCP context manager: {e}")

        logger.info("βœ… Cleanup complete")

    async def generate_animation(
        self,
        topic: str,
        target_audience: str = "general",
        animation_length_minutes: float = 2.0,
        output_filename: str = "animation.mp4",
        rendering_quality: str = "medium",
        max_retries: int = 3,
    ):
        """
        Generate an educational animation.

        Args:
            topic: STEM topic to animate
            target_audience: Target audience level (elementary, middle_school, high_school, college, general)
            animation_length_minutes: Desired animation length in minutes
            output_filename: Name for the output file
            rendering_quality: Manim rendering quality (low, medium, high, production_quality)
            max_retries: Maximum retry attempts per step

        Returns:
            Dictionary with pipeline results
        """
        logger.info(f"🎬 Generating animation for topic: '{topic}'")

        # Run the LangGraph pipeline
        result = await run_animation_pipeline(
            mcp_session=self.mcp_session,
            tts_generator=self.tts_generator,
            topic=topic,
            target_audience=target_audience,
            animation_length_minutes=animation_length_minutes,
            output_filename=output_filename,
            rendering_quality=rendering_quality,
            max_retries=max_retries,
        )

        return result


async def main():
    """Main entry point for the application."""
    print("🎨 NeuroAnim - STEM Animation Generator")
    print("=" * 50)
    print()

    # Get user input
    topic = input("πŸ“š Enter a STEM topic to animate: ").strip()
    if not topic:
        print("❌ Topic cannot be empty")
        return

    # Optional: Get target audience
    print("\n🎯 Target Audience:")
    print("  1. Elementary")
    print("  2. Middle School")
    print("  3. High School")
    print("  4. College")
    print("  5. General")
    audience_choice = input("Select (1-5) [default: 5]: ").strip() or "5"

    audience_map = {
        "1": "elementary",
        "2": "middle_school",
        "3": "high_school",
        "4": "college",
        "5": "general",
    }
    target_audience = audience_map.get(audience_choice, "general")

    # Optional: Get animation length
    length_input = input("\n⏱️  Animation length in minutes [default: 2.0]: ").strip()
    try:
        animation_length = float(length_input) if length_input else 2.0
    except ValueError:
        animation_length = 2.0

    # Optional: Get quality
    print("\n🎬 Rendering Quality:")
    print("  1. Low (fast, 480p)")
    print("  2. Medium (balanced, 720p)")
    print("  3. High (slow, 1080p)")
    print("  4. Production (very slow, 4K)")
    quality_choice = input("Select (1-4) [default: 2]: ").strip() or "2"

    quality_map = {
        "1": "low",
        "2": "medium",
        "3": "high",
        "4": "production_quality",
    }
    rendering_quality = quality_map.get(quality_choice, "medium")

    print()
    print("=" * 50)
    print(f"πŸ“ Configuration:")
    print(f"  Topic: {topic}")
    print(f"  Audience: {target_audience}")
    print(f"  Length: {animation_length} minutes")
    print(f"  Quality: {rendering_quality}")
    print("=" * 50)
    print()

    # Initialize the app
    app = NeuroAnimApp()

    try:
        # Initialize MCP connection
        await app.initialize()

        # Generate animation
        result = await app.generate_animation(
            topic=topic,
            target_audience=target_audience,
            animation_length_minutes=animation_length,
            rendering_quality=rendering_quality,
        )

        # Display results
        print()
        print("=" * 50)
        if result["success"]:
            print("βœ… ANIMATION GENERATION SUCCESSFUL!")
            print(f"πŸ“Ή Output: {result['final_output_path']}")
            print(f"⏱️  Time: {result.get('total_duration', 0):.2f}s")
            print(f"βœ“ Steps completed: {len(result['completed_steps'])}")

            if result.get("warnings"):
                print(f"\n⚠️  Warnings ({len(result['warnings'])}):")
                for warning in result["warnings"]:
                    print(f"  - {warning}")

            if result.get("quiz"):
                print("\n❓ Quiz Questions:")
                print(result["quiz"][:500])  # Print first 500 chars

        else:
            print("❌ ANIMATION GENERATION FAILED")
            print(f"Errors: {len(result.get('errors', []))}")
            for error in result.get("errors", []):
                print(f"  - {error}")

        print("=" * 50)

    except KeyboardInterrupt:
        print("\n⚠️  Process interrupted by user")
        sys.exit(1)

    except Exception as e:
        logger.error(f"Unexpected error: {e}", exc_info=True)
        print(f"\nπŸ’₯ Unexpected error: {str(e)}")
        sys.exit(1)

    finally:
        # Clean up
        await app.cleanup()


if __name__ == "__main__":
    asyncio.run(main())