File size: 10,116 Bytes
db7de06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import asyncio
import os
import shutil
import subprocess
import tempfile
from typing import List
import logging

from moviepy import concatenate_videoclips, VideoFileClip
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.gemini import GeminiModel
from pydantic_ai.providers.google_gla import GoogleGLAProvider
from config import api_key
import nest_asyncio
nest_asyncio.apply()

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Configure your Gemini API key
gemini_llm = GeminiModel(
    'gemini-2.0-flash', provider=GoogleGLAProvider(api_key=api_key)
)


class ChapterDescription(BaseModel):
    """Describes a chapter in the video."""
    title: str = Field(description="Title of the chapter.")
    explanation: str = Field(description="Detailed explanation of the chapter's content, including how Manim should visualize it. Be very specific with Manim instructions, including animations, shapes, positions, colors, and timing. Include LaTeX for mathematical formulas. Specify scene transitions. Example: 'Create a number line. Animate a point moving along the number line to illustrate addition. Use Transform to show the equation changing. Transition to a new Scene.'")

class VideoOutline(BaseModel):
    """Describes the outline of the video."""
    title: str = Field(description="Title of the entire video.")
    chapters: List[ChapterDescription] = Field(description="List of chapters in the video.")

class ManimCode(BaseModel):
    """Describes the Manim code for a chapter."""
    code: str = Field(description="Complete Manim code for the chapter. Include all necessary imports. The code should create a single scene. Add comments to explain the code. Do not include any comments that are not valid Python comments. Ensure the code is runnable.")

outline_agent = Agent(
    model=gemini_llm,
    result_type=VideoOutline,
    system_prompt="""
    You are a video script writer. Your job is to create a clear and concise outline for an educational video explaining a concept.
    The video should have a title and a list of chapters (maximum 3). Each chapter should have a title and a detailed explanation.
    The explanation should be very specific about how the concept should be visualized using Manim. Include detailed instructions
    for animations, shapes, positions, colors, and timing. Use LaTeX for mathematical formulas. Specify scene transitions.
    Do not include code, only explanations.
    """
)

manim_agent = Agent(
    model=gemini_llm,
    result_type=ManimCode,
    system_prompt="""
    You are a Manim code generator. Your job is to create Manim code for a single chapter of a video, given a detailed explanation of the chapter's content and how it should be visualized.
    The code should be complete and runnable. Include all necessary imports. The code should create a single scene. Add comments to explain the code.
    Do not include any comments that are not valid Python comments. Ensure the code is runnable. Do not include any text outside of the code block.

    """
)

code_fixer_agent = Agent(
    model=gemini_llm,
    result_type=ManimCode,
    system_prompt="""
    You are a Manim code debugging expert. You will receive Manim code that failed to execute and the error message.
    Your task is to analyze the code and the error, identify the issue, and provide corrected, runnable Manim code.
    Ensure the corrected code addresses the error and still aims to achieve the visualization described in the original code.
    Include all necessary imports and ensure the code creates a single scene. Add comments to explain the changes you made.
    Do not include any comments that are not valid Python comments. Ensure the code is runnable. Do not include any text outside of the code block.
    """
)

def generate_manim_code(chapter_description: ChapterDescription) -> str:
    """Generates initial Manim code for a single chapter."""
    logging.info(f"Generating Manim code for chapter: {chapter_description.title}")
    result = manim_agent.run_sync(f"title: {chapter_description.title}. Explanation: {chapter_description.explanation}")
    return result.data.code

def fix_manim_code(error: str, current_code: str) -> str:
    """Attempts to fix the Manim code that resulted in an error."""
    logging.info(f"Attempting to fix Manim code due to error: {error}")
    result = code_fixer_agent.run_sync(f"Error: {error}\nCurrent Code: {current_code}")
    return result.data.code

def generate_video_outline(concept: str) -> VideoOutline:
    """Generates the video outline."""
    logging.info(f"Generating video outline for concept: {concept}")
    result = outline_agent.run_sync(concept)
    return result.data

def create_video_from_code(code: str, chapter_number: int) -> str:
    """Creates a video from Manim code and returns the video file path using subprocess.Popen."""
    with open("temp.py", "w") as temp_file:
        temp_file.write(code)
        temp_file_name = temp_file.name

    process = None
    try:
        command = ["manim", temp_file_name, "-ql", "--disable_caching"]
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True)
        stdout, stderr = process.communicate(timeout=60)  # Add a timeout to prevent indefinite blocking

        if process.returncode == 0:
            logging.info(f"Manim execution successful for chapter {chapter_number}.")
            logging.debug(f"Manim stdout:\n{stdout}")
            logging.debug(f"Manim stderr:\n{stderr}")
        else:
            error_msg = f"Manim execution failed for chapter {chapter_number} with return code {process.returncode}:\nStdout:\n{stdout}\nStderr:\n{stderr}"
            logging.error(str(error_msg).split('\n')[-1])
            raise subprocess.CalledProcessError(process.returncode, command, output=stdout.encode(), stderr=stderr.encode())

    except subprocess.TimeoutExpired:
        logging.error(f"Manim process timed out for chapter {chapter_number}.")
        if process:
            process.kill()
        raise
    except FileNotFoundError:
        logging.error("Error: The 'manim' command was not found. Ensure Manim is installed and in your system's PATH.")
        raise
    finally:
        pass
        # if os.path.exists(temp_file_name):
        #     os.remove(temp_file_name)

    # Construct the video file name. Manim names the file based on the class name.
    # Extract the class name from the python code.
    import re
    match = re.search(r"class\s+(\w+)\(Scene\):", code)
    if match:
        class_name = match.group(1)
        video_file_name = f"{class_name}.mp4"
        return video_file_name
    else:
        raise ValueError(f"Could not extract class name from Manim code for chapter {chapter_number}")

async def main(concept: str):
    """Generates a video explanation for a given concept using Manim with error correction."""
    logging.info(f"Generating video for concept: {concept}")
    outline = generate_video_outline(concept)
    logging.info(f"Video outline: {outline}")

    video_files = []
    for i, chapter in enumerate(outline.chapters):
        logging.info(f"Processing chapter {i + 1}: {chapter.title}")
        manim_code = generate_manim_code(chapter)
        logging.debug(f"Generated Manim code for chapter {i + 1}:\n{manim_code}")

        success = False
        attempts = 0
        max_attempts = 2  # Try fixing the code once

        while attempts < max_attempts and not success:
            try:
                video_file = create_video_from_code(manim_code, i + 1)
                video_files.append(video_file)
                logging.info(f"Video file created for chapter {i + 1}: {video_file}")
                success = True
            except subprocess.CalledProcessError as e:
                attempts += 1
                logging.error(f"Manim execution failed for chapter {i + 1} (Attempt {attempts}): {e}")
                logging.info(f"Attempting to fix the code...")
                manim_code = fix_manim_code(str(e), manim_code)
                logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
            except ValueError as e:
                logging.error(f"Error processing Manim code for chapter {i + 1}: {e}")
                return  # Stop processing if a critical error occurs with code structure
            except FileNotFoundError:
                logging.error("Manim not found. Please ensure it's installed and in your PATH.")
                return
            except subprocess.TimeoutExpired:
                logging.error(f"Manim process timed out for chapter {i + 1}. Attempting to fix...")
                manim_code = fix_manim_code(f"Manim process timed out.", manim_code)
                logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")

        if not success:
            logging.error(f"Failed to generate video for chapter {i + 1} after {max_attempts} attempts. Skipping chapter.")
            continue

    # Combine the video files
    if video_files:
        logging.info("Combining video files...")
        clips = [VideoFileClip("./media/videos/temp/480p15/"+video_file) for video_file in video_files]
        final_video_path = f"final.mp4"
        final_clip = concatenate_videoclips(clips)
        final_clip.write_videofile(final_video_path, codec="libx264", audio_codec="aac")
        final_clip.close()

        logging.info(f"Final video created: {final_video_path}")

        # Clean up intermediate video files
        for video_file in video_files:
            try:
                os.remove(video_file)
                logging.info(f"Deleted intermediate video file: {video_file}")
            except Exception as e:
                logging.error(f"Error deleting intermediate video file {video_file}: {e}")
    else:
        logging.warning("No video files to combine.")

if __name__ == "__main__":
    concept = input("Enter your Prompt: ")  # Replace with your concept
    asyncio.run(main(concept))