Spaces:
Sleeping
Sleeping
Download app.py from Chand11/Math_video_explainer_2: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/spaces/Chand11/Math_video_explainer_2/resolve/main/app.py
- Command line
-
hf download hf://spaces/Chand11/Math_video_explainer_2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Chand11/Math_video_explainer_2/resolve/main/app.py
17.1 kB
| import gradio as gr | |
| import subprocess | |
| import os | |
| import re | |
| from pathlib import Path | |
| import tempfile | |
| try: | |
| from google import genai | |
| from google.genai import types | |
| GENAI_AVAILABLE = True | |
| except ImportError: | |
| GENAI_AVAILABLE = False | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") | |
| # Video templates for different content types | |
| TEMPLATES = { | |
| "Problem Solver": { | |
| "structure": "Title β Problem Statement β Step-by-Step Solution β Final Answer", | |
| "style": "Clean, focused, educational", | |
| "duration": "Focus on clear problem-solving steps" | |
| }, | |
| "Concept Explainer": { | |
| "structure": "Title β What is it? β Visual Example β Key Points β Summary", | |
| "style": "Visual, intuitive, smooth transitions", | |
| "duration": "Build understanding gradually" | |
| }, | |
| "Formula Breakdown": { | |
| "structure": "Title β Formula Display β What Each Part Means β Example β Application", | |
| "style": "Clear labeling, color-coded parts", | |
| "duration": "Thorough explanation of components" | |
| }, | |
| "Quick Trick": { | |
| "structure": "Title β Normal Method β Shortcut Method β Why It Works β Practice", | |
| "style": "Fast-paced, attention-grabbing", | |
| "duration": "Efficient and memorable" | |
| } | |
| } | |
| def fix_all_known_errors(code): | |
| """Fix ALL known Manim errors""" | |
| # Fix colors to base 10 only | |
| color_map = { | |
| 'ORANGE_A': 'ORANGE', 'ORANGE_B': 'ORANGE', 'ORANGE_C': 'ORANGE', | |
| 'GRAY_A': 'WHITE', 'GRAY_B': 'WHITE', 'GREY': 'WHITE', 'GRAY': 'WHITE', | |
| 'PURPLE_A': 'PURPLE', 'RED_A': 'RED', 'BLUE_A': 'BLUE', 'GREEN_A': 'GREEN', | |
| 'BROWN': 'ORANGE', 'GOLD': 'YELLOW', 'CYAN': 'TEAL', 'MAGENTA': 'PINK', | |
| } | |
| for old, new in color_map.items(): | |
| code = re.sub(rf'\b{old}\b', new, code) | |
| # Fix invalid objects | |
| code = re.sub(r'Checkmark\([^)]*\)', 'Circle(radius=0.3, color=GREEN, fill_opacity=1)', code) | |
| code = re.sub(r'SVGMobject\([^)]+\)', 'Circle(radius=0.8, color=PINK, fill_opacity=0.8)', code) | |
| code = re.sub(r'ImageMobject\([^)]+\)', 'Square(side_length=2, color=BLUE, fill_opacity=0.5)', code) | |
| # Remove problematic methods | |
| code = re.sub(r'\.add_prefix\([^)]+\)', '', code) | |
| code = re.sub(r'\.add_suffix\([^)]+\)', '', code) | |
| code = re.sub(r'\.get_x_axis_label\([^)]+\)', '', code) | |
| code = re.sub(r'\.get_y_axis_label\([^)]+\)', '', code) | |
| # Remove Table usage - too error-prone | |
| if 'Table(' in code: | |
| code = re.sub(r'(\s+)(.*Table\(.*)', r'\1# \2 # Removed', code) | |
| code = re.sub(r'(\s+)(.*\.get_entries\(.*)', r'\1# \2', code) | |
| # STRICT limits | |
| code = re.sub(r'font_size=(\d+)', lambda m: f'font_size={min(int(m.group(1)), 32)}', code) | |
| code = re.sub(r'\.shift\(UP\s*\*\s*\d+\.?\d*\)', '.shift(UP*1)', code) | |
| code = re.sub(r'\.shift\(DOWN\s*\*\s*\d+\.?\d*\)', '.shift(DOWN*1)', code) | |
| code = re.sub(r'\.shift\(LEFT\s*\*\s*\d+\.?\d*\)', '.shift(LEFT*2)', code) | |
| code = re.sub(r'\.shift\(RIGHT\s*\*\s*\d+\.?\d*\)', '.shift(RIGHT*2)', code) | |
| code = re.sub(r'\.to_edge\((UP|DOWN|LEFT|RIGHT)\)(?!\s*,)', r'.to_edge(\1, buff=0.8)', code) | |
| # Remove unsupported parameters | |
| code = re.sub(r',\s*weight=\w+', '', code) # Remove weight parameter | |
| code = re.sub(r'weight=\w+,\s*', '', code) # Remove weight at start | |
| # Add color=BLACK to Text/MathTex without color | |
| def add_black(match): | |
| text = match.group(0) | |
| if 'color=' not in text: | |
| text = text[:-1] + ', color=BLACK)' | |
| return text | |
| code = re.sub(r'Text\([^)]+\)', add_black, code) | |
| code = re.sub(r'MathTex\([^)]+\)', add_black, code) | |
| return code | |
| def generate_code_with_gemini(prompt, template, duration, quality): | |
| """Generate Manim code using Gemini API""" | |
| if not GENAI_AVAILABLE or not GEMINI_API_KEY: | |
| return None, "β Setup GEMINI_API_KEY in Settings β Secrets" | |
| try: | |
| client = genai.Client(api_key=GEMINI_API_KEY) | |
| # Duration guidance | |
| duration_guide = { | |
| "Short (30-60s)": "Keep it concise - 3-4 key points max, quick transitions", | |
| "Medium (2-5 min)": "Detailed explanation - 5-8 steps, smooth pacing", | |
| "Long (5-10 min)": "Comprehensive - 10+ steps, thorough breakdown, multiple examples" | |
| } | |
| template_info = TEMPLATES[template] | |
| duration_info = duration_guide[duration] | |
| full_prompt = f"""You are creating a YOUTUBE MATH VIDEO for government exam preparation (SSC, Banking, Railways). | |
| TEMPLATE: {template} | |
| STRUCTURE: {template_info['structure']} | |
| STYLE: {template_info['style']} (inspired by 3Blue1Brown) | |
| TARGET DURATION: {duration} - {duration_info} | |
| USER REQUEST: {prompt} | |
| CRITICAL RULES: | |
| 1. YOUTUBE QUALITY: | |
| - Professional, clean, smooth animations | |
| - Clear visual hierarchy (title β content β conclusion) | |
| - Engaging pacing with proper timing | |
| - Everything centered and visible | |
| 2. STRUCTURE (Follow {template} template): | |
| - Start with BOLD title card (2 seconds) | |
| - Present problem/concept clearly | |
| - Step-by-step visual explanation | |
| - Highlight key insights/shortcuts | |
| - End with summary or practice prompt | |
| 3. COLORS & VISIBILITY: | |
| - Background: WHITE | |
| - ALL Text/MathTex: color=BLACK (MANDATORY!) | |
| - Use vibrant accent colors: BLUE (trust), GREEN (correct), RED (important), ORANGE (highlight) | |
| - Color-code different elements (formulas=BLUE, steps=GREEN, answers=RED) | |
| 4. PREVENT OVERLAP (CRITICAL): | |
| - ALWAYS FadeOut before new content | |
| - Pattern: | |
| ```python | |
| step1 = Text("Step 1: ...", font_size=28, color=BLACK).to_edge(UP, buff=0.8) | |
| self.play(Write(step1), run_time=1) | |
| self.wait(2) # Let viewers read | |
| self.play(FadeOut(step1)) | |
| self.wait(0.3) # Small pause | |
| step2 = Text("Step 2: ...", font_size=28, color=BLACK).to_edge(UP, buff=0.8) | |
| self.play(Write(step2), run_time=1) | |
| self.wait(2) | |
| self.play(FadeOut(step2)) | |
| ``` | |
| 5. TIMING (YouTube pacing): | |
| - Title card: 2 seconds | |
| - Each explanation step: 2-3 seconds | |
| - Complex calculations: 3-4 seconds | |
| - Final answer highlight: 2 seconds | |
| - Total should match {duration} | |
| 6. VISUAL ELEMENTS: | |
| - Use boxes/rectangles to group related info | |
| - Arrows to show relationships | |
| - Underlines for emphasis | |
| - Fade transitions between sections | |
| - Scale/rotate for dynamic interest | |
| 7. STAY IN FRAME: | |
| - Font: MAX 32 (title), 28 (content), 24 (details) | |
| - Safe zone: X[-2.5, 2.5], Y[-1.5, 1.5] | |
| - Use .to_edge() with buff=0.8 | |
| - Center main content with .move_to(ORIGIN) | |
| 8. NEVER use: | |
| - Table, DecimalTable (use VGroup instead) | |
| - get_x_axis_label/get_y_axis_label | |
| - Checkmark, SVGMobject, ImageMobject | |
| - Complex grids | |
| 9. QUALITY SETTINGS: | |
| - Use run_time for smooth animations (1-2 seconds) | |
| - Add fill_opacity=0.8 for shapes | |
| - Use stroke_width=2 for emphasis | |
| 10. CODE STRUCTURE: | |
| ```python | |
| from manim import * | |
| class MathExplainer(Scene): | |
| def construct(self): | |
| self.camera.background_color = WHITE | |
| # Title Card (2 sec) | |
| title = Text("Topic Title", font_size=32, color=BLACK) | |
| self.play(Write(title), run_time=1) | |
| self.wait(2) | |
| self.play(FadeOut(title)) | |
| # Main Content (follow template structure) | |
| # ... your explanation with proper FadeOuts | |
| # Conclusion | |
| conclusion = Text("Key Takeaway", font_size=28, color=GREEN) | |
| self.play(Write(conclusion), run_time=1) | |
| self.wait(2) | |
| self.play(FadeOut(conclusion)) | |
| ``` | |
| Generate professional, YouTube-ready code following the {template} template for: {prompt}""" | |
| response = client.models.generate_content( | |
| model='gemini-2.5-flash-lite', | |
| contents=full_prompt | |
| ) | |
| code = response.text | |
| return code, None | |
| except Exception as e: | |
| return None, f"β Error: {str(e)}" | |
| def render_video_from_code(code, quality): | |
| """Render video from code""" | |
| if not code or not code.strip(): | |
| return "β No code!", None | |
| try: | |
| temp_dir = Path(tempfile.mkdtemp(prefix="manim_")) | |
| code = fix_all_known_errors(code) | |
| match = re.search(r'class\s+(\w+)\s*\(Scene\)', code) | |
| if not match: | |
| return "β No Scene class!", None | |
| class_name = match.group(1) | |
| code_file = temp_dir / "animation.py" | |
| code_file.write_text(code) | |
| quality_map = {'Low (fast)': '-ql', 'Medium': '-qm', 'High (1080p)': '-qh'} | |
| quality_flag = quality_map.get(quality, '-qm') | |
| cmd = f"manim {code_file} {class_name} {quality_flag} --disable_caching --media_dir {temp_dir}/media" | |
| result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300) | |
| if result.returncode == 0: | |
| videos = list((temp_dir / "media").rglob("*.mp4")) | |
| if videos: | |
| return "β Rendered!", str(videos[0]) | |
| return f"β Failed:\n{result.stderr[-500:]}", None | |
| except Exception as e: | |
| return f"β {e}", None | |
| def render_video(prompt, template, duration, quality): | |
| """Generate and render video with auto-retry""" | |
| if not prompt: | |
| yield "β Enter a prompt!", None, None | |
| return | |
| MAX_ATTEMPTS = 3 | |
| for attempt in range(1, MAX_ATTEMPTS + 1): | |
| try: | |
| temp_dir = Path(tempfile.mkdtemp(prefix="manim_")) | |
| if attempt == 1: | |
| yield f"π€ Generating {template} video ({duration})...", None, None | |
| else: | |
| yield f"π Attempt {attempt}/{MAX_ATTEMPTS} - Simplifying code...", None, None | |
| # Adjust prompt complexity based on attempt | |
| if attempt == 1: | |
| current_prompt = prompt | |
| elif attempt == 2: | |
| current_prompt = f"KEEP IT VERY SIMPLE. {prompt}. Use only basic shapes and text, no complex arrangements." | |
| else: | |
| current_prompt = f"EXTREMELY SIMPLE VERSION: {prompt}. Only 2-3 simple elements, no arrays, no loops, no complex positioning." | |
| code, error = generate_code_with_gemini(current_prompt, template, duration, quality) | |
| if error: | |
| if attempt < MAX_ATTEMPTS: | |
| continue | |
| yield error, None, None | |
| return | |
| if "```python" in code: | |
| code = code.split("```python")[1].split("```")[0] | |
| elif "```" in code: | |
| code = code.split("```")[1].split("```")[0] | |
| code = code.strip() | |
| code = fix_all_known_errors(code) | |
| # Extra safety fixes for common index errors | |
| # Remove array/list indexing that might fail | |
| code = re.sub(r'\[(\d+)\]\.', '[0].', code) # Replace [N] with [0] | |
| code = re.sub(r'for\s+\w+\s+in\s+range\([^)]+\):', '# Loop removed', code) # Comment out loops | |
| match = re.search(r'class\s+(\w+)\s*\(Scene\)', code) | |
| if not match: | |
| if attempt < MAX_ATTEMPTS: | |
| continue | |
| yield "β No Scene class!", code, None | |
| return | |
| class_name = match.group(1) | |
| code_file = temp_dir / "animation.py" | |
| code_file.write_text(code) | |
| yield f"π¬ Rendering (attempt {attempt})...", code, None | |
| quality_map = {'Low (fast)': '-ql', 'Medium': '-qm', 'High (1080p)': '-qh'} | |
| quality_flag = quality_map.get(quality, '-qm') | |
| cmd = f"manim {code_file} {class_name} {quality_flag} --disable_caching --media_dir {temp_dir}/media" | |
| result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300) | |
| if result.returncode == 0: | |
| videos = list((temp_dir / "media").rglob("*.mp4")) | |
| if videos: | |
| if attempt > 1: | |
| yield f"β Success on attempt {attempt}! (Simplified version) π", code, str(videos[0]) | |
| else: | |
| yield "β YouTube-ready video rendered! π", code, str(videos[0]) | |
| return | |
| else: | |
| if attempt < MAX_ATTEMPTS: | |
| continue | |
| yield "β Video not found", code, None | |
| return | |
| else: | |
| # Rendering failed, try again with simpler code | |
| if attempt < MAX_ATTEMPTS: | |
| error_snippet = result.stderr[-200:] if result.stderr else "Unknown error" | |
| print(f"Attempt {attempt} failed: {error_snippet}") | |
| continue | |
| else: | |
| yield f"β All {MAX_ATTEMPTS} attempts failed. Last error:\n{result.stderr[-500:]}", code, None | |
| return | |
| except Exception as e: | |
| if attempt < MAX_ATTEMPTS: | |
| print(f"Attempt {attempt} crashed: {e}") | |
| continue | |
| yield f"β Error after {MAX_ATTEMPTS} attempts: {e}", None, None | |
| return | |
| # Gradio Interface | |
| with gr.Blocks(title="YouTube Math Explainer") as demo: | |
| gr.Markdown(""" | |
| # π¬ YouTube Math Video Generator (Govt Exam Prep) | |
| **Professional Manim animations for your YouTube channel!** | |
| Perfect for SSC, Banking, Railways exam preparation videos. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| prompt_input = gr.Textbox( | |
| label="π Video Topic", | |
| placeholder="e.g., Solve percentage problem: If 20% of 500 is increased by 40%, find the result", | |
| lines=4 | |
| ) | |
| template_input = gr.Dropdown( | |
| choices=list(TEMPLATES.keys()), | |
| value="Problem Solver", | |
| label="π¨ Video Template", | |
| info="Choose content structure" | |
| ) | |
| duration_input = gr.Radio( | |
| choices=["Short (30-60s)", "Medium (2-5 min)", "Long (5-10 min)"], | |
| value="Short (30-60s)", | |
| label="β±οΈ Target Duration" | |
| ) | |
| quality_input = gr.Radio( | |
| choices=["Low (fast)", "Medium", "High (1080p)"], | |
| value="Medium", | |
| label="π₯ Quality" | |
| ) | |
| with gr.Row(): | |
| generate_btn = gr.Button("π Generate Video", variant="primary", size="lg") | |
| render_btn = gr.Button("π Render Edited", variant="secondary") | |
| with gr.Column(scale=1): | |
| status_output = gr.Textbox(label="π Status", lines=3) | |
| video_output = gr.Video(label="π₯ Your Video") | |
| code_output = gr.Code( | |
| label="π» Generated Code (Editable)", | |
| language="python", | |
| lines=15, | |
| interactive=True | |
| ) | |
| gr.Markdown(""" | |
| ### π Templates Explained: | |
| - **Problem Solver**: Step-by-step solution for math problems | |
| - **Concept Explainer**: Visual explanation of mathematical concepts | |
| - **Formula Breakdown**: Detailed formula explanation with examples | |
| - **Quick Trick**: Shortcuts and time-saving techniques | |
| ### π― Tips for YouTube Success: | |
| - Use **Problem Solver** for numerical questions | |
| - Use **Quick Trick** for shortcuts (great for Shorts!) | |
| - Use **Concept Explainer** for theory topics | |
| - Start with **Short videos** to test, then go longer | |
| - **High (1080p)** quality for main channel, **Low** for testing | |
| ### β¨ Features: | |
| β 3Blue1Brown inspired style | |
| β Professional animations | |
| β No text overlap | |
| β Everything stays in frame | |
| β Exam-focused content | |
| β Edit & re-render support | |
| """) | |
| generate_btn.click( | |
| fn=render_video, | |
| inputs=[prompt_input, template_input, duration_input, quality_input], | |
| outputs=[status_output, code_output, video_output] | |
| ) | |
| render_btn.click( | |
| fn=render_video_from_code, | |
| inputs=[code_output, quality_input], | |
| outputs=[status_output, video_output] | |
| ) | |
| gr.Examples( | |
| examples=[ | |
| ["Solve: If 25% of a number is 75, find the number", "Problem Solver", "Short (30-60s)", "Medium"], | |
| ["Explain Pythagoras theorem with visual proof", "Concept Explainer", "Medium (2-5 min)", "Medium"], | |
| ["Quick trick: Calculate 15% of any number mentally", "Quick Trick", "Short (30-60s)", "Medium"], | |
| ["Break down the quadratic formula: x = (-b Β± β(bΒ²-4ac))/2a", "Formula Breakdown", "Medium (2-5 min)", "Medium"], | |
| ], | |
| inputs=[prompt_input, template_input, duration_input, quality_input] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |