| import os |
| import re |
| from google import genai |
| from google.genai import types as genai_types |
| import logging |
| from .gemini import base_prompt_instructions |
|
|
| logging.basicConfig( |
| level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" |
| ) |
|
|
| FALLBACK_SYSTEM_PROMPT = """You are an expert Manim programmer specializing in fixing broken Manim code and creating visually striking 60-second animations, strictly following Manim Community v0.19.0 standards. |
| |
| CRITICAL TIMING REQUIREMENTS: |
| - **Total Duration:** Exactly 60 seconds (1 minute) |
| - **Narration:** Exactly 150-160 words (average speaking pace: 2.5 words per second) |
| - **Animation Structure:** Use this timing framework: |
| * Introduction: 8-10 seconds |
| * Main content: 40-45 seconds (3-4 major segments) |
| * Conclusion/summary: 7-10 seconds |
| - **Synchronization:** Each narration sentence should correspond to 3-5 seconds of animation |
| |
| Core Requirements: |
| - **API Version:** Use only Manim Community v0.19.0 API |
| - **Vectors & Math:** Use 3D vectors (np.array([x, y, 0])) and ensure correct math operations |
| - **Matrix Visualization:** Use MathTex for matrices: r'\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix}' |
| - **Star Usage:** Use Star(n=5, ...) not n_points |
| - **Error Prevention:** Always validate Scene class exists; avoid 3D scenes |
| - **Visual Style:** Create vibrant, dynamic animations with smooth transitions |
| |
| IMPORTANT: Your response must be formatted with clear delimiters: |
| - Start Manim code with: ### MANIM CODE: |
| - Start narration with: ### NARRATION: |
| - End response after narration (no additional text) |
| """ |
|
|
|
|
| def fix_manim_code(faulty_code: str, error_message: str, original_context: str): |
| """ |
| Enhanced fallback function with Google Search integration. |
| """ |
| api_key = os.getenv("GEMINI_API_KEY") |
| if not api_key: |
| logging.error("GEMINI_API_KEY not found in environment variables for fallback.") |
| return None, None |
|
|
| client = genai.Client(api_key=api_key) |
|
|
| |
| fix_prompt_text = f""" |
| TASK: Fix the broken Manim code that failed with a specific error. |
| |
| ### ORIGINAL REQUEST: |
| {original_context} |
| |
| ### BROKEN MANIM CODE: |
| ```python |
| {faulty_code} |
| ``` |
| |
| ### ERROR ENCOUNTERED: |
| ``` |
| {error_message} |
| ``` |
| |
| ### ANALYSIS INSTRUCTIONS: |
| 1. **Error Analysis**: Examine the error message carefully. Common issues include: |
| - Import errors (missing 'from manim import *' or 'import numpy as np') |
| - Scene class not found (class must inherit from Scene) |
| - Invalid Manim methods or syntax |
| - Vector dimension mismatches (use np.array([x, y, 0])) |
| - Animation object validation errors |
| - Timing issues (ensure total duration = 60 seconds) |
| |
| 2. **Google Search**: Use Google Search to find: |
| - Recent Manim Community v0.19.0 API changes |
| - Specific error message solutions |
| - Updated method signatures or deprecated features |
| - Working examples of similar animations |
| |
| 3. **Code Fixing Strategy**: |
| - Keep the original animation concept intact |
| - Fix only what's necessary to resolve the error |
| - Maintain 60-second duration and 120-150 word narration |
| - Ensure all imports are present |
| - Validate Scene class exists and is properly named |
| - Use only verified Manim methods from the allowed list |
| |
| 4. **Quality Checks**: |
| - Verify vector operations use 3D format: np.array([x, y, 0]) |
| - Check all self.play() calls have valid animation objects |
| - Ensure run_time and self.wait() sum to exactly 60 seconds |
| - Count narration words (must be 120-150) |
| |
| ### OUTPUT FORMAT: |
| Provide your response in exactly this format: |
| |
| ### MANIM CODE: |
| [Insert the complete, fixed Manim code here - include all imports and Scene class] |
| |
| ### NARRATION: |
| [Insert the narration script here - exactly 120-150 words, synchronized with animations] |
| |
| ### REQUIREMENTS TO FOLLOW: |
| {base_prompt_instructions} |
| """ |
|
|
| contents = [fix_prompt_text] |
|
|
| logging.info("Attempting to fix Manim code via fallback...") |
| try: |
| grounding_tool = genai_types.Tool(google_search=genai_types.GoogleSearch()) |
|
|
| generation_config = genai_types.GenerateContentConfig( |
| tools=[grounding_tool], |
| temperature=0.4, |
| system_instruction=FALLBACK_SYSTEM_PROMPT, |
| ) |
|
|
| response = client.models.generate_content( |
| model="gemini-2.5-pro", |
| contents=contents, |
| config=generation_config, |
| ) |
| if response: |
| |
| try: |
| content = response.text |
| logging.info("Received response from fallback attempt.") |
|
|
| if "### NARRATION:" in content: |
| manim_code, narration = content.split("### NARRATION:", 1) |
| manim_code = ( |
| re.sub(r"```python", "", manim_code).replace("```", "").strip() |
| ) |
| narration = narration.strip() |
|
|
| if "from manim import *" not in manim_code: |
| logging.warning( |
| "Adding missing 'from manim import *' (fallback fix)." |
| ) |
| manim_code = ( |
| "from manim import *\nimport numpy as np\n" + manim_code |
| ) |
| elif "import numpy as np" not in manim_code: |
| logging.warning( |
| "Adding missing 'import numpy as np' (fallback fix)." |
| ) |
| lines = manim_code.splitlines() |
| for i, line in enumerate(lines): |
| if "from manim import *" in line: |
| lines.insert(i + 1, "import numpy as np") |
| manim_code = "\n".join(lines) |
| break |
|
|
| logging.info( |
| "Successfully parsed fixed code and narration from fallback." |
| ) |
| return { |
| "manim_code": manim_code, |
| "output_file": "output.mp4", |
| }, narration |
| else: |
| logging.warning( |
| "Delimiter '### NARRATION:' not found in fallback response. Attempting fallback extraction." |
| ) |
| code_match = re.search(r"```python(.*?)```", content, re.DOTALL) |
| if code_match: |
| manim_code = code_match.group(1).strip() |
| narration_part = content.split("```", 2)[-1].strip() |
| narration = narration_part if len(narration_part) > 20 else "" |
| if not narration: |
| logging.warning( |
| "Fallback narration extraction resulted in empty or very short text (fallback fix)." |
| ) |
| else: |
| logging.info( |
| "Successfully parsed code and narration using fallback regex (fallback fix)." |
| ) |
|
|
| if "from manim import *" not in manim_code: |
| logging.warning( |
| "Adding missing 'from manim import *' (fallback fix, regex path)." |
| ) |
| manim_code = ( |
| "from manim import *\nimport numpy as np\n" + manim_code |
| ) |
| elif "import numpy as np" not in manim_code: |
| logging.warning( |
| "Adding missing 'import numpy as np' (fallback fix, regex path)." |
| ) |
| lines = manim_code.splitlines() |
| for i, line in enumerate(lines): |
| if "from manim import *" in line: |
| lines.insert(i + 1, "import numpy as np") |
| manim_code = "\n".join(lines) |
| break |
|
|
| logging.info( |
| "Successfully parsed fixed code using fallback extraction." |
| ) |
| return { |
| "manim_code": manim_code, |
| "output_file": "output.mp4", |
| }, narration |
| else: |
| logging.error( |
| "Fallback extraction failed: No Python code block found in fallback response." |
| ) |
| logging.debug( |
| f"Fallback content without code block:\n{content}" |
| ) |
| return None, None |
|
|
| except ValueError: |
| logging.error("Could not extract text from the fallback response.") |
| if response.prompt_feedback and response.prompt_feedback.block_reason: |
| logging.error( |
| f"Fallback content generation blocked. Reason: {response.prompt_feedback.block_reason.name}" |
| ) |
| return None, None |
| except Exception as e: |
| logging.exception(f"Error processing fallback response: {e}") |
| return None, None |
| else: |
| logging.error("No response received from Gemini during fallback attempt.") |
| return None, None |
|
|
| except Exception as e: |
| logging.exception(f"Error calling Gemini API during fallback: {e}") |
| return None, None |
|
|