# Project export: Anim-Education

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Struggle to visualize your classes? Anim-Education has you covered
- Devpost: https://devpost.com/software/immersive-ed
- GitHub: https://github.com/msteele3/treehacksdockerdeploy
- Demo: https://github.com/Benler123/TreeHacks
- Team: 1 GitHub contributor(s) — msteele3 (1 commits)

## Devpost submission (written by the team)

### Inspiration

As a student with ADHD, one of our teammates often finds it challenging to focus during lectures, making it difficult to absorb and retain information effectively. Traditional learning environments are not always designed to accommodate neurodivergent students, which can lead to frustration and disengagement. Recognizing this struggle, we wanted to create a tool that empowers students like us to not only pay attention but also stay actively engaged with the material in a way that suits our learning styles. Our goal is to enhance comprehension, retention, and overall academic success by providing an aid that transforms lectures into interactive and accessible learning experiences.

### What it does

We developed an application that enhances lecture comprehension by generating real-time animations that visually reinforce the professor’s words. Unlike static slides, which can sometimes feel disengaging or overwhelming, our dynamic visuals provide an interactive and intuitive way to grasp complex concepts. By bridging the gap between auditory learning and visual understanding, our tool helps students stay focused, absorb information more effectively, and retain key concepts with greater clarity.

### How we built it

We utilized Perplexity Deep Research to strategize the development of generative animations and streamline our coding process. This involved conducting in-depth research on animation techniques, machine learning models, and optimization strategies to ensure smooth and efficient rendering. By leveraging OpenAI models and Groq, we were able to generate highly fluid and realistic animations with seamless transitions between frames, reducing visual artifacts and stuttering. Using natural language processing via the Groq API, the system analyzes incoming text in real-time, extracting relevant animation-worthy topics. When a concept is detected, the API dynamically generates Manim code to illustrate it. The animation pipeline operates asynchronously, leveraging a task queue to render animations efficiently while caching previously generated videos to optimize performance. We implemented multithreading, allowing different components of the animation pipeline—such as data structure animation rendering, and AI-driven code generation—to run concurrently. This significantly improved processing efficiency, reducing latency and ensuring that animations could run in real-time without noticeable delays. Additionally, we optimized buffer management techniques to minimize lag, enabling near-instantaneous transitions between animation states. Through this approach, we achieved high-quality, dynamic animations that responded quickly to real-time inputs while maintaining computational efficiency. Built with scalability in mind, it supports multiple concurrent WebSocket connections, allowing interactive applications to integrate it for real-time visualization. A caching mechanism also reduces redundant processing by reusing animations for previously encountered concepts. On the Unity side, we focused on setting up a robust real-time motion synchronization (RTMS) system to ensure responsiveness and accuracy in animation rendering. We also successfully created our Unity environment, carefully configuring assets, physics, and rendering settings to support high-performance generative animations. We worked on connecting Zoom RTMS to the Meta Quest and converting the live transcript from Zoom meetings to real time captions that scroll to avoid overflow.

### Challenges we ran into

We initially tried to make this integrate into a VR environment using the Zoom SDK but were unable to put it together. Meta Quest could not integrate mp3 audio files, which were essential for the video processing, making creating an integrated VR environment highly difficult.

### Accomplishments we're proud of

Created a self-contained generative animation agent that is ultra-efficient and performs in realtime in VR using Groq and OpenAI.

### What we learned

Integrating VR into our application presented significant challenges, from optimizing performance to ensuring seamless real-time interactions. However, through this process, we gained a deeper understanding of the complexities involved in VR development. Most of us had zero experience with Unity prior to this hackathon, so tackling the steep learning curve was challenging but deeply rewarding. Additionally, we discovered the immense potential of generative animations in education—these dynamic visuals have the power to revolutionize learning by making complex concepts more intuitive and engaging. This experience reinforced our belief that interactive and adaptive technologies are the future of education.

### What's next

for Immersive-Ed Our next step is to fully integrate Immersive-Ed into VR, creating an even more engaging and interactive learning experience. This will involve researching the best VR platforms, refining our real-time animation system for immersive environments, and overcoming the technical challenges of seamless integration. We aim to enhance accessibility and adaptability, ensuring that students can benefit from a truly immersive educational tool that caters to diverse learning styles.

## README (from the GitHub repository)

# Manim Animation Generator

This Docker-based workflow generates Manim animations based on text descriptions using the Groq API.

## Prerequisites

- Docker and Docker Compose installed on your system
- Groq API key

## Setup

1. Copy the `.env.example` file to `.env.local`:
   ```bash
   cp .env.example .env.local
   ```

2. Edit the `.env.local` file and add your Groq API key:
   ```
   GROQ_API_KEY=your_actual_groq_api_key
   ```

   Note: `.env.local` is gitignored to ensure your API key remains private.

## Usage

1. Build and run the container:
   ```bash
   docker-compose up --build
   ```

2. The script will prompt you for what you want to animate
3. It will generate the animation code using Groq
4. The animation will be rendered using Manim
5. The final video will be saved in the `media/videos` directory

## Output

The generated videos will be available in the `media/videos` directory on your host machine, thanks to the volume mounting in the Docker configuration.

## Customization

You can modify the following files to customize the behavior:
- `main.py`: The main script that handles the workflow
- `Dockerfile`: The container configuration
- `docker-compose.yml`: The service configuration

## Troubleshooting

If you encounter any issues:
1. Make sure your Groq API key is correctly set in the `.env.local` file
2. Check that Docker has sufficient resources allocated
3. Ensure all required ports are available
4. Check the Docker logs for any error messages # treehacksdockerdeploy


## Detected evidence (automated analysis)

Indexed codebase: 4 recognized source files, 18 KB.
- FastAPI (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- C# (language) — claimed on Devpost, not found in the code
- Docker (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (9 of 9)

```
.dockerignore
.env.example
.gitignore
animation_scene.py
docker-compose.yml
Dockerfile
main.py
README.md
requirements.txt
```

### Dependencies

- requirements.txt: fastapi@==0.109.2, groq@==0.18.0, manim@==0.17.0, openai, python-dotenv@==1.0.0, python-multipart@==0.0.9, uvicorn@==0.27.1, websockets

### Recent commits (newest first)

- first commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
manim==0.17.0
groq==0.18.0
python-dotenv==1.0.0
fastapi==0.109.2
uvicorn==0.27.1
python-multipart==0.0.9
openai
websockets
```

### docker-compose.yml

```yaml
version: '3.8'

services:
  manim-generator:
    build: .
    volumes:
      - /Users/mattsteele/Code/treehacks2025/manim_docker:/app
      - /Users/mattsteele/Code/treehacks2025/manim_docker/media:/app/media
    env_file:
      - .env.local
    ports:
      - "8080:8080"  # Map container port 8080 to host port 8080
    stdin_open: true  # Keep STDIN open
    tty: true        # Allocate a pseudo-TTY 
```

### Dockerfile

```
FROM python:3.11-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    ffmpeg \
    build-essential \
    python3-dev \
    libcairo2-dev \
    libpango1.0-dev \
    texlive \
    texlive-latex-extra \
    texlive-fonts-extra \
    texlive-latex-recommended \
    texlive-science \
    tipa \
    libcairo2 \
    && rm -rf /var/lib/apt/lists/*

# Set working directory
WORKDIR /app
# Copy requirements first to leverage Docker cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application
COPY . .

# Command to run the application
CMD ["python", "main.py"] 
```

### main.py

```python
import os
import subprocess
from groq import Groq
from openai import OpenAI
from dotenv import load_dotenv
import re
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from pydantic import BaseModel
import shutil
import asyncio
from typing import List, Dict, Optional
from datetime import datetime
import json
from queue import Queue
import threading
import base64
from contextlib import asynccontextmanager

# Load environment variables
load_dotenv()

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Start animation worker
    asyncio.create_task(animation_worker())
    yield

app = FastAPI(title="Manim Animation API", lifespan=lifespan)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Global state for managing animations and streaming
animation_queue = asyncio.Queue()
video_buffer: Dict[str, bytes] = {}
connected_clients: List[WebSocket] = []
text_buffer = ""
text_lock = threading.Lock()
animation_cache: Dict[str, str] = {}  # Maps normalized concepts to their animation IDs

class AnimationRequest(BaseModel):
    item_to_animate: str

def normalize_concept(concept: str) -> str:
    """Normalize a concept string for comparison."""
    # Remove special characters, convert to lowercase, and remove spaces
    return ''.join(c.lower() for c in concept if c.isalnum())

async def animation_worker():
    """Background worker that processes animation requests."""
    while True:
        animation_id, text = await animation_queue.get()
        try:
            # Normalize the concept
            normalized_concept = normalize_concept(text)
            
            # Check if we already have this animation
            if normalized_concept in animation_cache:
                cached_animation_id = animation_cache[normalized_concept]
                if cached_animation_id in video_buffer:
                    # Reuse existing video
                    video_data = video_buffer[cached_animation_id]
                    for client in connected_clients:
                        try:
                            await client.send_json({
                                "type": "animation_complete",
                                "animation_id": animation_id,
                                "video_data": base64.b64encode(video_data).decode('utf-8'),
                                "status": "success"
                            })
                        except Exception as e:
                            print(f"Error sending video: {e}")
                            connected_clients.remove(client)
                    continue

            # Generate and save the Manim code
            manim_code = generate_manim_code(text)
            save_manim_code(manim_code)
            
            # Render the animation
            if render_animation():
                video_path = get_latest_video_path()
                with open(video_path, 'rb') as f:
                    video_data = f.read()
                video_buffer[animation_id] = video_data
                # Store the normalized concept in the cache
                animation_cache[normalized_concept] = animation_id
                print("SENDOING VIDEO")
                for client in connected_clients:
                    try:
                        await client.send_json({
                            "type": "animation_complete",
                            "animation_id": animation_id,
                            "video_data": base64.b64encode(video_data).decode('utf-8'),
                            "status": "success"
                        })
                    except Exception as e:
                        print(f"Error sending video: {e}")
                        connected_clients.remove(client)
            else:
                for client in connected_clients:
                    try:
                        await client.send_json({
                            "type": "animation_error",
                            "animation_id": animation_id,
                            "error": "Failed to render animation"
                        })
                    except:
                        connected_clients.remove(client)
        except Exception as e:
            for client in connected_clients:
                try:
                    await client.send_json({
                        "type": "animation_error",
                        "animation_id": animation_id,
                        "error": str(e)
                    })
                except:
                    connected_clients.remove(client)
        finally:
            animation_queue.task_done()
            # Clean up the video buffer after sending
            if animation_id in video_buffer:
                del video_buffer[animation_id]

def analyze_text_segment(text: str) -> Optional[str]:
    """Analyze a segment of text to determine if it should trigger an animation."""
    try:
        client = Groq()
        completion = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=[
                {
                    "role": "system",
                    "content": """You are an expert at identifying concepts that can be visualized through animation. Your task is to analyze text and determine if it contains a concept that could be meaningfully animated.

Rules:
1. Respond with ONLY a single word or short phrase that can be animated
2. If no clear animation concept is found, respond with exactly "null"
3. Focus on:
   - Data structures (e.g., "linked list", "binary tree")
   - Algorithms (e.g., "bubble sort", "binary search")
   - Mathematical concepts (e.g., "sine wave", "vector addition")
   - Physical processes (e.g., "pendulum motion", "spring oscillation")
4. Ignore abstract or non-visual concepts
5. Keep 
[truncated — 8015 more characters]
```

### animation_scene.py

```python
from manim import *

config.pixel_width = 960
config.pixel_height = 540
config.frame_rate = 30


class BinaryTreeScene(MovingCameraScene):
    def create_node(self, position, label, primary_color):
        circle = Circle(radius=0.4, color=primary_color, fill_color=primary_color, fill_opacity=1)
        text = Text(label, font_size=24, color=WHITE)
        node = VGroup(circle, text)
        node.move_to(position)
        return node

    def create_edge(self, start_mobject, end_mobject, secondary_color):
        start = start_mobject.get_center()
        end = end_mobject.get_center()
        return Line(start, end, color=secondary_color)

    def construct(self):
        primary_color = "#003262"
        secondary_color = "#FDB515"

        # Create root node
        root = self.create_node(ORIGIN + UP * 2, "1", primary_color)
        self.play(FadeIn(root))
        self.wait(0.5)

        # Create left child
        left = self.create_node(ORIGIN + UP * 0 + LEFT * 2, "2", primary_color)
        edge_left = self.create_edge(root, left, secondary_color)
        self.play(FadeIn(left), Create(edge_left))
        self.play(self.camera.frame.animate.move_to(left.get_center()))
        self.wait(0.5)

        # Create right child
        right = self.create_node(ORIGIN + UP * 0 + RIGHT * 2, "3", primary_color)
        edge_right = self.create_edge(root, right, secondary_color)
        self.play(FadeIn(right), Create(edge_right))
        self.play(self.camera.frame.animate.move_to(right.get_center()))
        self.wait(0.5)

        # Create left grandchildren
        left_left = self.create_node(ORIGIN + DOWN * 2 + LEFT * 3, "4", primary_color)
        left_right = self.create_node(ORIGIN + DOWN * 2 + LEFT * 1, "5", primary_color)
        edge_left_left = self.create_edge(left, left_left, secondary_color)
        edge_left_right = self.create_edge(left, left_right, secondary_color)
        self.play(FadeIn(left_left), Create(edge_left_left))
        self.play(FadeIn(left_right), Create(edge_left_right))
        self.play(self.camera.frame.animate.move_to(left_left.get_center()))
        self.wait(0.5)

        # Create right grandchildren
        right_left = self.create_node(ORIGIN + DOWN * 2 + RIGHT * 1, "6", primary_color)
        right_right = self.create_node(ORIGIN + DOWN * 2 + RIGHT * 3, "7", primary_color)
        edge_right_left = self.create_edge(right, right_left, secondary_color)
        edge_right_right = self.create_edge(right, right_right, secondary_color)
        self.play(FadeIn(right_left), Create(edge_right_left))
        self.play(FadeIn(right_right), Create(edge_right_right))
        self.play(self.camera.frame.animate.move_to(right_right.get_center()))
        self.wait(2)
```