Skip to main content

Log in to comment and vote

Comments5

  • Julius Marminge

    Team

    Can you send the chat id or the text in full? I can’t reproduce it from what I can see in the image:

    • Rose Beaver

      I believe the ID in the URL is refered sa chatId :

      b47970fc-c976-46d6-a81d-63ca6982de56

      full text below:

      Okay, creating a chat interface where an AI generates Manim video animations on a server and serves them to a React frontend is an ambitious and exciting project! It involves several components working together. Here's a step-by-step approach to help you achieve this:

      Core Idea:

      1. Frontend (React): User types a request for an animation (e.g., "Create a red circle that moves to the right").

      2. Backend (Python - Flask/FastAPI):

      * Receives the request.

      * An "AI" component (which can range from simple parsing to a sophisticated LLM) interprets the request and translates it into Manim Python code.

      * Executes the Manim script to generate a video.

      * Makes the video file available.

      3. Frontend (React): Receives the video (or a link to it) and displays it.

      Let's break this down into manageable phases and steps:

      Phase 1: Backend Setup (Python with Flask/FastAPI and Manim)

      This phase focuses on getting Manim to run via an API call.

      * Step 1: Set Up Your Python Environment

      * Create a project directory.

      * Set up a virtual environment:

      ```bash

      python -m venv venv

      source venv/bin/activate # On Windows: venv\Scripts\activate

      ```

      * Install necessary libraries:

      * Manim: pip install manim

      * A web framework (Flask or FastAPI):

      * Flask: pip install Flask flask-cors

      * FastAPI: pip install fastapi uvicorn[standard] python-multipart

      * python-dotenv for managing environment variables.

      * Step 2: Basic Manim Script Generation Logic

      * For now, let's assume the "AI" part will translate the user's request into a structured format or directly into Manim code.

      * Create a Python module (e.g., manim_generator.py) that can take parameters and generate a simple Manim scene script.

      Example (`manim_generator.py` - very basic):

      ```python

      from manim import *

      import os

      import uuid

      # Ensure a directory for outputs exists

      OUTPUT_DIRECTORY = "media/videos"

      os.makedirs(OUTPUT_DIRECTORY, exist_ok=True)

      def create_basic_scene_code(text_to_display="Hello Manim"):

      """

      Generates the Python code for a simple Manim scene that displays text.

      """

      scene_name = f"GeneratedScene_{uuid.uuid4().hex[:8]}"

      code = f"""

      from manim import *

      class {scene_name}(Scene):

      def construct(self):

      text = Text("{text_to_display}")

      self.play(Write(text))

      self.wait(2)

      """

      return code, scene_name

      def render_manim_script(script_code, scene_name, output_filename_base):

      """

      Saves the script to a temporary file and runs Manim.

      Returns the path to the output video file.

      """

      script_path = os.path.join("temp_manim_scripts", f"{scene_name}.py")

      os.makedirs("temp_manim_scripts", exist_ok=True)

      with open(script_path, "w") as f:

      f.write(script_code)

      # Adjust Manim command as needed (quality, output dir)

      # Note: Manim's programmatic execution can be complex.

      # Running as a subprocess is often more straightforward for server environments.

      # The manim render command is preferred.

      # manim <script_path> <SceneName> -ql (low quality for speed)

      # The output path will be relative to where manim is run, or configured.

      # Manim's default output is media/videos/<script_name>/<quality>/<SceneName>.mp4

      # We want to control the output filename and location more directly.

      output_file = os.path.join(OUTPUT_DIRECTORY, f"{output_filename_base}.mp4")

      # Command to render the specific scene to a specific output file

      # Manim Community v0.18.0+ allows specifying output file directly

      # manim render -o <output_file_name_with_extension> <script_file_path> <SceneName>

      # For older versions, you might need to move/rename the file after rendering.

      # Example using subprocess:

      import subprocess

      try:

      # Ensure MANIM_OPTS is not overriding output directory if you want to use -o

      # You might need to configure Manim's output_file or media_dir

      # or use the -o flag if your Manim version supports it well.

      # For simplicity, let's assume Manim outputs to a predictable location

      # or we can use a config.

      # Simplest approach: run manim and then find the file.

      # A more robust way is to use Manim's configuration system or direct output flags.

      # Let's try to make Manim output to our desired OUTPUT_DIRECTORY

      # This might require a custom manim.cfg or programmatic configuration.

      # For now, let's use a simpler command and then move the file.

      # This is a common pattern if direct output control is tricky.

      # manim render <script_path> <SceneName> -ql --media_dir .

      # This would output to ./media/videos/<script_name (no .py)>/<quality>/<SceneName>.mp4

      # Let's assume we can configure Manim to output where we want or use -o

      # If using manim render script.py Scene -o output/file.mp4

      # This is cleaner if available and working correctly.

      # For this example, let's assume Manim outputs to its default structure

      # and we then locate the file.

      # A more direct approach:

      # manim_cmd = [

      # "manim", "render",

      # "-ql", # low quality for speed

      # "-o", output_filename_base + ".mp4", # desired output filename

      # "--media_dir", OUTPUT_DIRECTORY, # desired output directory

      # script_path,

      # scene_name

      # ]

      # This syntax for -o and --media_dir might vary slightly or have interactions.

      # Test this command directly in your terminal first.

      # A common way to run Manim:

      # This will output to media/videos/temp_manim_scripts/<scene_name>/<quality>/<scene_name>.mp4

      # We need to adjust this.

      # Let's try to make Manim output to a specific file.

      # The Manim CLI has evolved. Check manim render --help.

      # As of recent versions, manim render script.py SceneName -o file.mp4 should work.

      # Let's use a temporary output directory for Manim's default structure

      # and then move the file. This is often more reliable across Manim versions

      # if direct output flags are problematic.

      temp_media_dir = os.path.join("temp_manim_output", scene_name)

      os.makedirs(temp_media_dir, exist_ok=True)

      manim_process = subprocess.run(

      [

      "manim",

      script_path,

      scene_name,

      "-ql", # Low quality for faster rendering

      "--media_dir", temp_media_dir # Output to a temp location

      ],

      capture_output=True,

      text=True,

      check=True # Raise an exception for non-zero exit codes

      )

      print("Manim STDOUT:", manim_process.stdout)

      print("Manim STDERR:", manim_process.stderr)

      # Find the generated .mp4 file in the temp_media_dir structure

      # e.g., temp_media_dir/videos/SceneName/480p15/SceneName.mp4 (Manim CE structure)

      # Or temp_media_dir/videos/<script_name_no_ext>/<quality>/<SceneName>.mp4

      # This part is crucial and depends on your Manim version's output structure.

      # Let's assume a common structure:

      # For Manim Community Edition, it's often:

      # <media_dir>/videos/<script_name_without_extension>/<quality_dir>/<SceneName>.mp4

      # Since script_name is scene_name here:

      # <temp_media_dir>/videos/<scene_name>/480p15/<scene_name>.mp4 (for -ql)

      # This path needs to be robustly determined.

      # For simplicity, let's assume it's directly in temp_media_dir for this example,

      # which is not standard Manim behavior without specific config.

      # A better way: search for the .mp4 file.

      source_video_path = None

      for root, dirs, files in os.walk(temp_media_dir):

      for file in files:

      if file.endswith(f"{scene_name}.mp4"):

      source_video_path = os.path.join(root, file)

      break

      if source_video_path:

      break

      if not source_video_path:

      raise FileNotFoundError(f"Manim output video for {scene_name} not found in {temp_media_dir}")

      # Move the found video to our desired final output path

      import shutil

      shutil.move(source_video_path, output_file)

      print(f"Video moved to {output_file}")

      # Clean up temporary script and output directory

      os.remove(script_path)

      shutil.rmtree(os.path.dirname(script_path)) # remove temp_manim_scripts if empty

      shutil.rmtree(temp_media_dir) # remove temp_manim_output/<scene_name>

      return output_file

      except subprocess.CalledProcessError as e:

      print(f"Manim execution failed for {scene_name}:")

      print(e.stdout)

      print(e.stderr)

      # Clean up failed attempts

      if os.path.exists(script_path):

      os.remove(script_path)

      if os.path.exists(temp_media_dir):

      shutil.rmtree(temp_media_dir)

      return None

      except FileNotFoundError as e:

      print(e)

      return None

      ```

      * Important: Manim's output directory structure can be tricky. You might need to adjust the render_manim_script function to correctly locate the output .mp4 file based on your Manim version and configuration. Using manim render script.py SceneName -o desired/path/video.mp4 is the ideal if your Manim version supports it robustly. Otherwise, rendering to a default/temporary location and then moving the file is a common workaround.

      * Step 3: Create a Flask/FastAPI Backend

      * This backend will have an endpoint that:

      1. Receives a description or parameters for the animation.

      2. Uses manim_generator.py to create Manim code.

      3. Calls Manim to render the video.

      4. Returns a URL to the generated video.

      Example (using Flask - app.py):

      ```python

      from flask import Flask, request, jsonify, send_from_directory

      from flask_cors import CORS

      import os

      import uuid

      from manim_generator import create_basic_scene_code, render_manim_script

      import threading # For background tasks

      app = Flask(__name__)

      CORS(app) # Enable CORS for local development

      VIDEO_DIR = "media/videos" # Should match OUTPUT_DIRECTORY in manim_generator

      os.makedirs(VIDEO_DIR, exist_ok=True)

      # In-memory store for job statuses (can be replaced with Redis, DB, etc.)

      jobs = {}

      def process_manim_generation(job_id, text_to_display):

      try:

      script_code, scene_name = create_basic_scene_code(text_to_display)

      output_filename_base = f"animation_{job_id}"

      # Update job status

      jobs[job_id]["status"] = "rendering"

      video_path = render_manim_script(script_code, scene_name, output_filename_base)

      if video_path:

      relative_video_path = os.path.join(

      "videos", os.path.basename(video_path)

      )

      jobs[job_id]["status"] = "completed"

      jobs[job_id]["video_url"] = f"/serve/{os.path.basename(video_path)}"

      jobs[job_id]["message"] = "Video generated successfully."

      else:

      jobs[job_id]["status"] = "failed"

      jobs[job_id]["message"] = "Failed to render Manim script."

      except Exception as e:

      print(f"Error in Manim generation thread for job {job_id}: {e}")

      jobs[job_id]["status"] = "failed"

      jobs[job_id]["message"] = str(e)

      @app.route("/generate-video", methods=["POST"])

      def generate_video_endpoint():

      data = request.get_json()

      if not data or "prompt" not in data:

      return jsonify({"error": "No prompt provided"}), 400

      prompt = data["prompt"]

      # In a real AI, this prompt would be processed.

      # For now, we'll use it directly as text_to_display.

      text_to_display = prompt

      job_id = str(uuid.uuid4())

      jobs[job_id] = {"status": "queued", "prompt": prompt}

      # Run Manim rendering in a background thread

      thread = threading.Thread(

      target=process_manim_generation,

      args=(job_id, text_to_display)

      )

      thread.start()

      return jsonify({

      "message": "Video generation started.",

      "job_id": job_id,

      "status_url": f"/status/{job_id}"

      }), 202

      @app.route("/status/<job_id>", methods=["GET"])

      def get_status_endpoint(job_id):

      job = jobs.get(job_id)

      if not job:

      return jsonify({"error": "Job not found"}), 404

      return jsonify(job)

      @app.route("/serve/<filename>", methods=["GET"])

      def serve_video(filename):

      return send_from_directory(VIDEO_DIR, filename)

      if name == "__main__":

      # Make sure to run with a production server like Gunicorn for real deployment

      # For development:

      app.run(debug=True, port=5001) # Use a different port than React dev server

      ```

      * Note on Asynchronous Tasks: Manim rendering can take time. The example above uses threading for simplicity. For production, you'd want a more robust task queue system like Celery with Redis or RabbitMQ. This prevents HTTP requests from timing out.

      Phase 2: Frontend Setup (React)

      This phase focuses on creating the chat interface.

      * Step 4: Set Up Your React Project

      * Use Create React App or Vite:

      ```bash

      npx create-react-app manim-chat-frontend

      # or

      # npm create vite@latest manim-chat-frontend -- --template react

      cd manim-chat-frontend

      npm install axios # For API calls

      ```

      * Step 5: Create Chat Components

      * ChatInterface.js: Main component to hold input and messages.

      * Message.js: Component to display individual messages (user or AI/video).

      Example (`src/ChatInterface.js`):

      ```javascript

      import React, { useState, useEffect, useRef } from "react";

      import axios from "axios";

      import "./ChatInterface.css"; // Create this for styling

      const API_BASE_URL = "http://localhost:5001"; // Your backend URL

      function Message({ sender, text, videoUrl }) {

      return (

      <div className={`message ${sender}`}>

      {text && <p>{text}</p>}

      {videoUrl && (

      <video controls width="320" height="240">

      <source src={`${API_BASE_URL}${videoUrl}`} type="video/mp4" />

      Your browser does not support the video tag.

      </video>

      )}

      </div>

      );

      }

      function ChatInterface() {

      const [messages, setMessages] = useState([

      { sender: "ai", text: "Hi! How can I help you create an animation?" },

      ]);

      const [input, setInput] = useState("");

      const messagesEndRef = useRef(null);

      const scrollToBottom = () => {

      messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });

      };

      useEffect(scrollToBottom, [messages]);

      const checkJobStatus = async (jobId, attempt = 1) => {

      const maxAttempts = 20; // Poll for ~2 minutes (20 * 6s)

      const delay = 6000; // 6 seconds

      if (attempt > maxAttempts) {

      setMessages((prev) => [

      ...prev,

      {

      sender: "ai",

      text: Video generation for job ${jobId} timed out. Please check server logs.,

      },

      ]);

      return;

      }

      try {

      const response = await axios.get(`${API_BASE_URL}/status/${jobId}`);

      const job = response.data;

      if (job.status === "completed") {

      setMessages((prev) => [

      ...prev,

      {

      sender: "ai",

      text: job.message || "Here's your video:",

      videoUrl: job.video_url,

      },

      ]);

      } else if (job.status === "failed") {

      setMessages((prev) => [

      ...prev,

      {

      sender: "ai",

      text: Video generation failed: ${job.message || "Unknown error."},

      },

      ]);

      } else {

      // Still processing (queued or rendering)

      setTimeout(() => checkJobStatus(jobId, attempt + 1), delay);

      }

      } catch (error) {

      console.error("Error checking job status:", error);

      setMessages((prev) => [

      ...prev,

      {

      sender: "ai",

      text: Error checking status for job ${jobId}.,

      },

      ]);

      // Optionally retry on network errors too

      }

      };

      const handleSubmit = async (e) => {

      e.preventDefault();

      if (!input.trim()) return;

      const userMessage = { sender: "user", text: input };

      setMessages((prev) => [...prev, userMessage]);

      setInput("");

      try {

      const response = await axios.post(

      ${API_BASE_URL}/generate-video,

      {

      prompt: input,

      },

      );

      const { job_id, message, status_url } = response.data;

      setMessages((prev) => [

      ...prev,

      { sender: "ai", text: message || "Processing your request..." },

      ]);

      if (job_id && status_url) {

      // Start polling for status

      setTimeout(() => checkJobStatus(job_id), 3000); // Initial delay before first poll

      }

      } catch (error) {

      console.error("Error sending message:", error);

      setMessages((prev) => [

      ...prev,

      {

      sender: "ai",

      text: "Sorry, I couldn't process your request.",

      },

      ]);

      }

      };

      return (

      <div className="chat-container">

      <div className="messages-area">

      {messages.map((msg, index) => (

      <Message

      key={index}

      sender={msg.sender}

      text={msg.text}

      videoUrl={msg.videoUrl}

      />

      ))}

      <div ref={messagesEndRef} />

      </div>

      <form onSubmit={handleSubmit} className="input-area">

      <input

      type="text"

      value={input}

      onChange={(e) => setInput(e.target.value)}

      placeholder="Describe the animation you want..."

      />

      <button type="submit">Send</button>

      </form>

      </div>

      );

      }

      export default ChatInterface;

      ```

      * Add basic CSS in src/ChatInterface.css for styling.

      * Update src/App.js to use ChatInterface.

      Phase 3: The "AI" - Translating Requests to Manim Code

      This is the most complex part. Here are a few approaches, from simple to advanced:

      * Step 6: Option A - Predefined Commands / Simple Parser (Easiest to Start)

      * Define a set of commands your "AI" can understand.

      * Example: "text: Hello World", "shape: circle, color: blue, animation: grow"

      * In your Python backend, parse these commands and map them to specific Manim scene constructions.

      * Modify manim_generator.py to accept more parameters and generate more varied scenes based on this structured input.

      Example modification in app.py (conceptual):

      ```python

      # ... (inside generate_video_endpoint)

      prompt = data["prompt"]

      # Simple parsing logic (replace with more sophisticated parsing)

      text_to_display = "Generated from prompt" # Default

      if "text:" in prompt.lower():

      try:

      text_to_display = prompt.split("text:")[1].strip()

      except:

      pass # Keep default

      # job_id = ...

      # thread = threading.Thread(target=process_manim_generation, args=(job_id, text_to_display))

      # ...

      ```

      * Step 7: Option B - Rule-Based Natural Language Understanding (NLU)

      * Use libraries like spaCy or NLTK to parse the user's sentence, identify entities (objects, colors, actions), and map them to Manim elements and animations.

      * This requires defining grammar rules and mappings. It's more flexible than predefined commands but still limited.

      * Step 8: Option C - Using a Large Language Model (LLM) (Most Advanced & Flexible)

      * This is where a true "AI" comes in. You could use a model like Gemini (which powers me!), GPT-4, or others.

      * How it would work:

      1. Send the user's natural language prompt to the LLM.

      2. Prompt Engineering is Key: Instruct the LLM to output only valid Python code for a Manim scene based on the user's request. You'll need to provide examples in your prompt (few-shot prompting) and clear instructions about the desired output format and Manim version/syntax.

      3. The LLM returns a string of Python code.

      4. CRITICAL SECURITY RISK: Directly executing LLM-generated code is dangerous. You MUST sandbox the execution environment heavily. Consider:

      * Running Manim in a Docker container with restricted permissions.

      * Analyzing the generated code for malicious patterns (very hard to do perfectly).

      * Limiting the scope of Manim modules and Python built-ins available to the generated code.

      5. Your backend then takes this LLM-generated code and passes it to render_manim_script.

      Example LLM interaction (conceptual, using a hypothetical LLM client):

      ```python

      # In your backend, when you get a prompt:

      # from your_llm_client import get_manim_code_from_llm

      # user_natural_language_prompt = "Create a blue square that fades in and then rotates."

      # generated_manim_code_string = get_manim_code_from_llm(user_natural_language_prompt)

      # if generated_manim_code_string:

      # # !!! SECURITY: Validate and sandbox before execution !!!

      # # For now, assuming it's safe for this conceptual step

      # script_code = generated_manim_code_string

      # scene_name = extract_scene_name_from_code(script_code) # You'd need a helper for this

      # # ... then call render_manim_script

      # else:

      # # Handle LLM failure

      ```

      * To implement this, you'd use the API of your chosen LLM provider.

      Phase 4: Refinements and Production-Readiness

      * Step 9: Robust Task Queue

      * Replace threading with Celery and a message broker (Redis/RabbitMQ) for handling Manim rendering tasks. This is crucial for scalability and reliability.

      * Step 10: Improved Error Handling and Feedback

      * Provide more granular feedback to the user about the animation generation process (e.g., "Parsing request," "Generating Manim script," "Rendering video," "Almost done!").

      * Log errors from Manim execution comprehensively.

      * Step 11: Security (Especially if using LLM for code generation)

      * Input Sanitization: Clean user inputs.

      * Sandboxing: If executing LLM-generated code, this is paramount. Docker, nsjail, or WebAssembly runtimes could be options. Restrict network access, file system access, and available Python modules for the Manim execution environment.

      * Resource Limits: Limit CPU, memory, and execution time for Manim processes to prevent abuse.

      * Step 12: Configuration

      * Use environment variables for API keys, Manim paths, quality settings, etc.

      * Step 13: Scalability

      * Consider how you'll scale the backend if many users generate videos simultaneously. This might involve multiple worker processes for Celery and potentially load balancing.

      * Step 14: User Interface Enhancements

      * Loading indicators.

      * Ability to cancel requests.

      * History of generated videos.

      Running the Project:

      1. Start Backend:

      ```bash

      cd your-backend-directory

      source venv/bin/activate

      python app.py # Or use gunicorn for production: gunicorn -w 4 -k uvicorn.workers.UvicornWorker app:app

      ```

      2. Start Frontend:

      ```bash

      cd manim-chat-frontend

      npm start

      ```

      3. Open your browser to the React app (usually http://localhost:3000).

      This is a complex project, so tackle it step-by-step. Start with the basic Flask/FastAPI backend and Manim execution, then build the React frontend, and finally, iterate on the "AI" component, starting simple. Good luck!

      • Julius Marminge

        Team

        Thanks for the repro!

        Upon inspection, I believe this is invalid LLM output. Here’s part of the message that failed (I removed some content but all the indentations etc is the same as your original message:

        The code block dedents beyond itself.

        Here’s what I believe is the valid output


        Which does render correctly:


        Will see what we can do with the system prompt to get the LLM to output valid indented code blocks in cases like yours!

        • Rose Beaver

          Hi Julius,

          Thanks for swift response, hope it will be sorted out soon.

  • Mark Florkowski

    Team

    Probably output invalid code. Unfortunate side effect of next token prediction.