diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -13,35 +13,37 @@ from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter import base64 -from transformers import pipeline -import torch import re import shutil import time from datetime import datetime, timedelta import streamlit.components.v1 as components import uuid -import platform import pandas as pd import plotly.express as px import markdown import zipfile -import contextlib -import threading +from azure.ai.inference import ChatCompletionsClient +from azure.ai.inference.models import SystemMessage, UserMessage +from azure.core.credentials import AzureKeyCredential +from openai import OpenAI +from transformers import pipeline +import torch import traceback -from io import StringIO, BytesIO -# Set up enhanced logging +# ────────────────────────────────────────────────────────────────────────────── +# Logging +# ────────────────────────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler() - ] + format="%(asctime)s • %(name)s • %(levelname)s • %(message)s", + handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) -# Model configuration mapping for different API requirements and limits +# ────────────────────────────────────────────────────────────────────────────── +# Model & Render Configuration +# ────────────────────────────────────────────────────────────────────────────── MODEL_CONFIGS = { "DeepSeek-V3-0324": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None}, "DeepSeek-R1": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None}, @@ -59,3340 +61,652 @@ MODEL_CONFIGS = { "Phi-4-multimodal-instruct": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Microsoft", "warning": None}, "Mistral-large-2407": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None}, "Codestral-2501": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None}, - # Default configuration for other models "default": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Other", "warning": None} } -# Try to import Streamlit Ace -try: - from streamlit_ace import st_ace - ACE_EDITOR_AVAILABLE = True -except ImportError: - ACE_EDITOR_AVAILABLE = False - logger.warning("streamlit-ace not available, falling back to standard text editor") +QUALITY_PRESETS = { + "480p": {"flag": "-ql", "fps": 30}, + "720p": {"flag": "-qm", "fps": 30}, + "1080p": {"flag": "-qh", "fps": 60}, + "4K": {"flag": "-qk", "fps": 60}, + "8K": {"flag": "-qp", "fps": 60}, +} +ANIMATION_SPEEDS = { + "Slow": 0.5, + "Normal": 1.0, + "Fast": 2.0, + "Very Fast": 3.0 +} + +EXPORT_FORMATS = { + "MP4 Video": "mp4", + "GIF Animation": "gif", + "WebM Video": "webm", + "PNG Sequence": "png_sequence", + "SVG": "svg" +} + +# ────────────────────────────────────────────────────────────────────────────── +# 1. prepare_api_params +# ────────────────────────────────────────────────────────────────────────────── def prepare_api_params(messages, model_name): - """Create appropriate API parameters based on model configuration""" - # Get model configuration + """Lookup MODEL_CONFIGS and build API call parameters.""" config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"]) - - # Base parameters common to all models - api_params = { + params = { "messages": messages, - "model": model_name + "model": model_name, + config["param_name"]: config.get(config["param_name"]) } - - # Add the appropriate token parameter based on model's parameter name - token_param = config["param_name"] - token_value = config[token_param] # Get the actual value from the config - - # Add the parameter to the API params - api_params[token_param] = token_value - - return api_params, config - -# New functions for accessing secrets and password verification -def get_secret(github_token_api): - """Retrieve a secret from HuggingFace Spaces environment variables""" - secret_value = os.environ.get(github_token_api) - if not secret_value: - logger.warning(f"Secret '{github_token_api}' not found") - return None - return secret_value - + return params, config + +# ────────────────────────────────────────────────────────────────────────────── +# 2. get_secret +# ────────────────────────────────────────────────────────────────────────────── +def get_secret(key): + """Read an environment variable (e.g. password, API token).""" + val = os.environ.get(key) + if not val: + logger.warning(f"Secret '{key}' not found") + return val or "" + +# ────────────────────────────────────────────────────────────────────────────── +# 3. check_password +# ────────────────────────────────────────────────────────────────────────────── def check_password(): - """Returns True if the user entered the correct password""" - # Get the password from secrets - correct_password = get_secret("password") - if not correct_password: - st.error("Admin password not configured in HuggingFace Spaces secrets") + """Prompt for admin password and gate AI features.""" + correct = get_secret("password") + if not correct: + st.error("Admin password not configured in secrets") return False - - # Password input - if "password_entered" not in st.session_state: - st.session_state.password_entered = False - - if not st.session_state.password_entered: - password = st.text_input("Enter password to access AI features", type="password") - if password: - if password == correct_password: - st.session_state.password_entered = True - return True + if "auth_ok" not in st.session_state: + st.session_state.auth_ok = False + if not st.session_state.auth_ok: + pwd = st.text_input("🔒 Enter admin password", type="password", help="Protects AI assistant") + if pwd: + if pwd == correct: + st.session_state.auth_ok = True + st.success("Access granted") else: st.error("Incorrect password") - return False return False return True +# ────────────────────────────────────────────────────────────────────────────── +# 4. ensure_packages +# ────────────────────────────────────────────────────────────────────────────── def ensure_packages(): - required_packages = { - 'manim': '0.17.3', - 'Pillow': '9.0.0', - 'numpy': '1.22.0', - 'transformers': '4.30.0', - 'torch': '2.0.0', - 'pygments': '2.15.1', - 'streamlit-ace': '0.1.1', - 'pydub': '0.25.1', # For audio processing - 'plotly': '5.14.0', # For timeline editor - 'pandas': '2.0.0', # For data manipulation - 'python-pptx': '0.6.21', # For PowerPoint export - 'markdown': '3.4.3', # For markdown processing - 'fpdf': '1.7.2', # For PDF generation - 'matplotlib': '3.5.0', # For Python script runner - 'seaborn': '0.11.2', # For enhanced visualizations - 'scipy': '1.7.3', # For scientific computations - 'huggingface_hub': '0.16.0', # For Hugging Face API + """Check & install core dependencies on first run.""" + required = { + 'streamlit':'1.25.0','manim':'0.17.3','numpy':'1.22.0','Pillow':'9.0.0', + 'transformers':'4.30.0','torch':'2.0.0','plotly':'5.14.0','pandas':'2.0.0', + 'python-pptx':'0.6.21','markdown':'3.4.3','fpdf':'1.7.2','matplotlib':'3.5.0', + 'seaborn':'0.11.2','scipy':'1.7.3','huggingface_hub':'0.16.0', + 'azure-ai-inference':'1.0.0b9','azure-core':'1.33.0','openai':'' } - - with st.spinner("Checking required packages..."): - # First, quickly check if packages are already installed - missing_packages = {} - for package, version in required_packages.items(): - try: - # Try to import the package to check if it's available - if package == 'manim': - import manim - elif package == 'Pillow': - import PIL - elif package == 'numpy': - import numpy - elif package == 'transformers': - import transformers - elif package == 'torch': - import torch - elif package == 'pygments': - import pygments - elif package == 'streamlit-ace': - # This one is trickier, we already handle it with ACE_EDITOR_AVAILABLE flag - pass - elif package == 'pydub': - import pydub - elif package == 'plotly': - import plotly - elif package == 'pandas': - import pandas - elif package == 'python-pptx': - import pptx - elif package == 'markdown': - import markdown - elif package == 'fpdf': - import fpdf - elif package == 'matplotlib': - import matplotlib - elif package == 'seaborn': - import seaborn - elif package == 'scipy': - import scipy - elif package == 'huggingface_hub': - import huggingface_hub - except ImportError: - missing_packages[package] = version - - # If no packages are missing, return success immediately - if not missing_packages: - logger.info("All required packages already installed.") - return True - - # If there are missing packages, install them with progress reporting - progress_bar = st.progress(0) - status_text = st.empty() - - for i, (package, version) in enumerate(missing_packages.items()): - try: - progress = (i / len(missing_packages)) - progress_bar.progress(progress) - status_text.text(f"Installing {package}...") - - result = subprocess.run( - [sys.executable, "-m", "pip", "install", f"{package}>={version}"], - capture_output=True, - text=True - ) - - if result.returncode != 0: - st.error(f"Failed to install {package}: {result.stderr}") - logger.error(f"Package installation failed: {package}") - return False - - except Exception as e: - st.error(f"Error installing {package}: {str(e)}") - logger.error(f"Package installation error: {str(e)}") - return False - - progress_bar.progress(1.0) - status_text.text("All packages installed successfully!") - time.sleep(0.5) - progress_bar.empty() - status_text.empty() - return True - + missing = [] + for pkg, ver in required.items(): + try: + __import__(pkg if pkg!='Pillow' else 'PIL') + except ImportError: + missing.append(f"{pkg}>={ver}" if ver else pkg) + if missing: + st.sidebar.info("Installing required packages...") + prog = st.sidebar.progress(0) + for i, pkg in enumerate(missing, 1): + subprocess.run([sys.executable, "-m", "pip", "install", pkg], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + prog.progress(i/len(missing)) + st.sidebar.success("All packages installed") + +# ────────────────────────────────────────────────────────────────────────────── +# 5. install_custom_packages +# ────────────────────────────────────────────────────────────────────────────── def install_custom_packages(package_list): - """Install custom packages specified by the user without page refresh""" - if not package_list.strip(): - return True, "No packages specified" - - # Split and clean package list - packages = [pkg.strip() for pkg in package_list.split(',') if pkg.strip()] - + """Install user-specified pip packages on the fly.""" + packages = [p.strip() for p in package_list.split(",") if p.strip()] if not packages: - return True, "No valid packages specified" - - status_placeholder = st.sidebar.empty() - progress_bar = st.sidebar.progress(0) - + return True, "No packages specified" results = [] success = True - - for i, package in enumerate(packages): - try: - progress = (i / len(packages)) - progress_bar.progress(progress) - status_placeholder.text(f"Installing {package}...") - - result = subprocess.run( - [sys.executable, "-m", "pip", "install", package], - capture_output=True, - text=True - ) - - if result.returncode != 0: - error_msg = f"Failed to install {package}: {result.stderr}" - results.append(error_msg) - logger.error(error_msg) - success = False - else: - results.append(f"Successfully installed {package}") - logger.info(f"Successfully installed custom package: {package}") - - except Exception as e: - error_msg = f"Error installing {package}: {str(e)}" - results.append(error_msg) - logger.error(error_msg) - success = False - - progress_bar.progress(1.0) - status_placeholder.text("Installation complete!") - time.sleep(0.5) - progress_bar.empty() - status_placeholder.empty() - + for pkg in packages: + res = subprocess.run([sys.executable, "-m", "pip", "install", pkg], capture_output=True, text=True) + ok = (res.returncode == 0) + results.append(f"{pkg}: {'✅' if ok else '❌'}") + if not ok: success = False return success, "\n".join(results) +# ────────────────────────────────────────────────────────────────────────────── +# 6. init_ai_models_direct +# ────────────────────────────────────────────────────────────────────────────── @st.cache_resource(ttl=3600) def init_ai_models_direct(): - """Direct implementation using the exact pattern from the example code""" - try: - # Get token from secrets - token = get_secret("github_token_api") - if not token: - st.error("GitHub token not found in secrets. Please add 'github_token_api' to your HuggingFace Spaces secrets.") - return None - - # Log what we're doing - for debugging - logger.info(f"Initializing AI model with token: {token[:5]}...") - - # Use exact imports as in your example - import os - from azure.ai.inference import ChatCompletionsClient - from azure.ai.inference.models import SystemMessage, UserMessage - from azure.core.credentials import AzureKeyCredential - - # Use exact endpoint as in your example - endpoint = "https://models.inference.ai.azure.com" - - # Use default model - model_name = "gpt-4o" - - # Create client exactly as in your example - client = ChatCompletionsClient( - endpoint=endpoint, - credential=AzureKeyCredential(token), - ) - - # Return the necessary information - return { - "client": client, - "model_name": model_name, - "endpoint": endpoint - } - except ImportError as ie: - st.error(f"Import error: {str(ie)}. Please make sure azure-ai-inference is installed.") - logger.error(f"Import error: {str(ie)}") + """Initialize Azure ChatCompletionsClient for AI code generation.""" + token = get_secret("github_token_api") + if not token: + st.error("GitHub token not found in secrets") return None - except Exception as e: - st.error(f"Error initializing AI model: {str(e)}") - logger.error(f"Initialization error: {str(e)}") - return None - + endpoint = "https://models.inference.ai.azure.com" + client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token)) + return {"client": client, "model_name": "gpt-4o", "endpoint": endpoint} + +# ────────────────────────────────────────────────────────────────────────────── +# 7. suggest_code_completion +# ────────────────────────────────────────────────────────────────────────────── def suggest_code_completion(code_snippet, models): - """Generate code completion using the AI model""" + """Use the initialized AI model to generate complete Manim code.""" if not models: - st.error("AI models not properly initialized.") + st.error("AI models not initialized") return None - - try: - # Create the prompt - prompt = f"""Write a complete Manim animation scene based on this code or idea: + prompt = f"""Write a complete Manim animation scene based on this code or idea: {code_snippet} -The code should be a complete, working Manim animation that includes: -- Proper Scene class definition -- Constructor with animations -- Proper use of self.play() for animations -- Proper wait times between animations - -Here's the complete Manim code: +The code should include: +- A Scene subclass +- self.play() animations +- wait times +Return only valid Python code. """ - - with st.spinner("AI is generating your animation code..."): - # Get the current model name and base URL - model_name = models["model_name"] - - # Handle model name - extract base name if it has a prefix - base_model_name = model_name.split('/')[-1] if '/' in model_name else model_name - - # Convert message to the appropriate format based on model category - config = MODEL_CONFIGS.get(base_model_name, MODEL_CONFIGS["default"]) - category = config.get("category", "Other") - - if category == "OpenAI": - # Import OpenAI client - from openai import OpenAI - - # Get token - token = get_secret("github_token_api") - - # Create or get client - if "openai_client" not in models: - client = OpenAI( - base_url="https://models.github.ai/inference", - api_key=token - ) - models["openai_client"] = client - else: - client = models["openai_client"] - - # Add openai/ prefix if not present - if "/" not in model_name: - full_model_name = f"openai/{model_name}" - else: - full_model_name = model_name - - # For OpenAI models, use developer role instead of system - messages = [ - {"role": "developer", "content": "You are an expert in Manim animations."}, - {"role": "user", "content": prompt} - ] - - # Create params - params = { - "messages": messages, - "model": full_model_name - } - - # Add token parameter - token_param = config["param_name"] - params[token_param] = config[token_param] - - # Make API call - response = client.chat.completions.create(**params) - completed_code = response.choices[0].message.content - - else: - # Use Azure client - from azure.ai.inference.models import UserMessage - - # Convert message format for Azure - messages = [UserMessage(prompt)] - api_params, _ = prepare_api_params(messages, model_name) - - # Make API call with Azure client - response = models["client"].complete(**api_params) - completed_code = response.choices[0].message.content - - # Process the code - if "```python" in completed_code: - completed_code = completed_code.split("```python")[1].split("```")[0] - elif "```" in completed_code: - completed_code = completed_code.split("```")[1].split("```")[0] - - # Add Scene class if missing - if "Scene" not in completed_code: - completed_code = f"""from manim import * - -class MyScene(Scene): - def construct(self): - {completed_code}""" - - return completed_code - - except Exception as e: - st.error(f"Error generating code: {str(e)}") - st.code(traceback.format_exc()) - return None - + config = MODEL_CONFIGS.get(models["model_name"].split("/")[-1], MODEL_CONFIGS["default"]) + if config["category"] == "OpenAI": + client = models.get("openai_client") or OpenAI(base_url="https://models.github.ai/inference", api_key=get_secret("github_token_api")) + models["openai_client"] = client + messages = [{"role":"developer","content":"Expert in Manim."}, {"role":"user","content":prompt}] + params = {"messages": messages, "model": models["model_name"], config["param_name"]: config.get(config["param_name"])} + resp = client.chat.completions.create(**params) + content = resp.choices[0].message.content + else: + client = models["client"] + msgs = [UserMessage(prompt)] + params, _ = prepare_api_params(msgs, models["model_name"]) + resp = client.complete(**params) + content = resp.choices[0].message.content + # extract code block + if "```python" in content: + content = content.split("```python")[1].split("```")[0] + elif "```" in content: + content = content.split("```")[1].split("```")[0] + if "class" not in content: + content = f"from manim import *\n\nclass MyScene(Scene):\n def construct(self):\n {content}" + return content + +# ────────────────────────────────────────────────────────────────────────────── +# 8. check_model_freshness +# ────────────────────────────────────────────────────────────────────────────── def check_model_freshness(): - """Check if models need to be reloaded based on TTL""" - if 'ai_models' not in st.session_state or st.session_state.ai_models is None: - return False - - if 'last_loaded' not in st.session_state.ai_models: - return False - - last_loaded = datetime.fromisoformat(st.session_state.ai_models['last_loaded']) - ttl_hours = 1 # 1 hour TTL - - return datetime.now() - last_loaded < timedelta(hours=ttl_hours) - + """Return True if AI client was loaded within the past hour.""" + if not st.session_state.get("ai_models"): return False + last = st.session_state.ai_models.get("last_loaded") + if not last: return False + return datetime.fromisoformat(last) + timedelta(hours=1) > datetime.now() + +# ────────────────────────────────────────────────────────────────────────────── +# 9. extract_scene_class_name +# ────────────────────────────────────────────────────────────────────────────── def extract_scene_class_name(python_code): - """Extract the scene class name from Python code.""" - import re - scene_classes = re.findall(r'class\s+(\w+)\s*\([^)]*Scene[^)]*\)', python_code) - - if scene_classes: - # Return the first scene class found - return scene_classes[0] - else: - # If no scene class is found, use a default name - return "MyScene" - -def suggest_code_completion(code_snippet, models): - if not models or "code_model" not in models: - st.error("AI models not properly initialized") - return None - - try: - prompt = f"""Write a complete Manim animation scene based on this code or idea: -{code_snippet} - -The code should be a complete, working Manim animation that includes: -- Proper Scene class definition -- Constructor with animations -- Proper use of self.play() for animations -- Proper wait times between animations - -Here's the complete Manim code: -```python -""" - with st.spinner("AI is generating your animation code..."): - response = models["code_model"]( - prompt, - max_length=1024, - do_sample=True, - temperature=0.2, - top_p=0.95, - top_k=50, - num_return_sequences=1, - truncation=True, - pad_token_id=50256 - ) - - if not response or not response[0].get('generated_text'): - st.error("No valid completion generated") - return None - - completed_code = response[0]['generated_text'] - if "```python" in completed_code: - completed_code = completed_code.split("```python")[1].split("```")[0] - - if "Scene" not in completed_code: - completed_code = f"""from manim import * - -class MyScene(Scene): - def construct(self): - {completed_code}""" - - return completed_code - except Exception as e: - st.error(f"Error suggesting code: {str(e)}") - logger.error(f"Code suggestion error: {str(e)}") - return None - -# Quality presets -QUALITY_PRESETS = { - "480p": {"resolution": "480p", "fps": "30"}, - "720p": {"resolution": "720p", "fps": "30"}, - "1080p": {"resolution": "1080p", "fps": "60"}, - "4K": {"resolution": "2160p", "fps": "60"}, - "8K": {"resolution": "4320p", "fps": "60"} # Added 8K option -} - -# Animation speeds -ANIMATION_SPEEDS = { - "Slow": 0.5, - "Normal": 1.0, - "Fast": 2.0, - "Very Fast": 3.0 -} - -# Export formats -EXPORT_FORMATS = { - "MP4 Video": "mp4", - "GIF Animation": "gif", - "WebM Video": "webm", - "PNG Image Sequence": "png_sequence", - "SVG Image": "svg" -} + """Regex for the first class inheriting from Scene.""" + m = re.findall(r"class\s+(\w+)\s*\([^)]*Scene[^)]*\)", python_code) + return m[0] if m else "MyScene" +# ────────────────────────────────────────────────────────────────────────────── +# 10. highlight_code +# ────────────────────────────────────────────────────────────────────────────── def highlight_code(code): - formatter = HtmlFormatter(style='monokai') - highlighted = highlight(code, PythonLexer(), formatter) - return highlighted, formatter.get_style_defs() + """Return HTML+CSS highlighted Python code.""" + formatter = HtmlFormatter(style="monokai", full=True, noclasses=True) + return highlight(code, PythonLexer(), formatter) +# ────────────────────────────────────────────────────────────────────────────── +# 11. generate_manim_preview +# ────────────────────────────────────────────────────────────────────────────── def generate_manim_preview(python_code): - """Generate a lightweight preview of the Manim animation""" - try: - # Extract scene components for preview - scene_objects = [] - if "Circle" in python_code: - scene_objects.append("circle") - if "Square" in python_code: - scene_objects.append("square") - if "MathTex" in python_code or "Tex" in python_code: - scene_objects.append("equation") - if "Text" in python_code: - scene_objects.append("text") - if "Axes" in python_code: - scene_objects.append("graph") - if "ThreeDScene" in python_code or "ThreeDAxes" in python_code: - scene_objects.append("3D scene") - if "Sphere" in python_code: - scene_objects.append("sphere") - if "Cube" in python_code: - scene_objects.append("cube") - - # Generate a more detailed visual preview based on extracted objects - object_icons = { - "circle": "⭕", - "square": "🔲", - "equation": "📊", - "text": "📝", - "graph": "📈", - "3D scene": "🧊", - "sphere": "🌐", - "cube": "🧊" - } - - icon_html = "" - for obj in scene_objects: - if obj in object_icons: - icon_html += f'{object_icons[obj]}' - - preview_html = f""" -
Scene contains: {', '.join(scene_objects) if scene_objects else 'No detected objects'}
-{str(e)}
-Accurate preview requires full render
No explanation provided.
" - - # Format the HTML template - html_content = html_template.format( - title=animation_title, - video_base64=video_base64, - explanation_html=explanation_html - ) - - # Save to file - output_path = os.path.join(temp_dir, f"{animation_title.replace(' ', '_')}.html") - with open(output_path, "w", encoding="utf-8") as f: - f.write(html_content) - - # Read the file to return it - with open(output_path, "rb") as f: - return f.read(), "html" - - elif format_type == "sequence": - # Generate animation sequence with explanatory text - # Make sure FPDF is installed - try: - from fpdf import FPDF - except ImportError: - logger.error("fpdf not installed") - subprocess.run([sys.executable, "-m", "pip", "install", "fpdf"], check=True) - from fpdf import FPDF - - # Save video temporarily - temp_video_path = os.path.join(temp_dir, "temp_video.mp4") - with open(temp_video_path, "wb") as f: - f.write(video_data) - - # Create frames directory - frames_dir = os.path.join(temp_dir, "frames") - os.makedirs(frames_dir, exist_ok=True) - - # Extract frames using ffmpeg (assuming it's installed) - frame_count = 5 # Number of key frames to extract - try: - subprocess.run([ - "ffmpeg", - "-i", temp_video_path, - "-vf", f"select=eq(n\\,0)+eq(n\\,{frame_count//4})+eq(n\\,{frame_count//2})+eq(n\\,{frame_count*3//4})+eq(n\\,{frame_count-1})", - "-vsync", "0", - os.path.join(frames_dir, "frame_%03d.png") - ], check=True) - except Exception as e: - logger.error(f"Error extracting frames: {str(e)}") - # Try a simpler approach - subprocess.run([ - "ffmpeg", - "-i", temp_video_path, - "-r", "1", # 1 frame per second - os.path.join(frames_dir, "frame_%03d.png") - ], check=True) - - # Parse explanation text into segments (assuming sections divided by ##) - explanation_segments = explanation_text.split("##") if explanation_text else ["No explanation provided."] - - # Create a PDF with frames and explanations - pdf = FPDF() - pdf.set_auto_page_break(auto=True, margin=15) - - # Title page + """Export the existing video_data to PPTX, HTML, or PDF sequence.""" + if format_type=="powerpoint": + from pptx import Presentation + from pptx.util import Inches + prs = Presentation() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + slide.shapes.title.text = animation_title + video_path = os.path.join(temp_dir,"video.mp4") + with open(video_path,"wb") as f: f.write(video_data) + slide2 = prs.slides.add_slide(prs.slide_layouts[5]) + slide2.shapes.title.text="Animation" + slide2.shapes.add_movie(video_path, Inches(1),Inches(1.5),Inches(8),Inches(4.5)) + if explanation_text: + txt_sl = prs.slides.add_slide(prs.slide_layouts[1]) + txt_sl.shapes.title.text="Explanation" + txt_sl.placeholders[1].text=explanation_text + out = os.path.join(temp_dir,f"{animation_title}.pptx") + prs.save(out) + return open(out,"rb").read(), "pptx" + + elif format_type=="html": + html_template = """Create mathematical animations with Manim
- """, unsafe_allow_html=True) + # Ensure packages installed once + if 'packages_checked' not in st.session_state: + ensure_packages() + st.session_state.packages_checked = True - # Check for packages ONLY ONCE per session - if not st.session_state.packages_checked: - if ensure_packages(): - st.session_state.packages_checked = True - else: - st.error("Failed to install required packages. Please try again.") - st.stop() - - # Create main tabs - tab_names = ["✨ Editor", "🤖 AI Assistant", "📚 LaTeX Formulas", "🎨 Assets", "🎞️ Timeline", "🎓 Educational Export", "🐍 Python Runner"] - tabs = st.tabs(tab_names) - - # Sidebar for rendering settings and custom libraries + # Sidebar with st.sidebar: - # Rendering settings section - st.markdown("## ⚙️ Rendering Settings") - - col1, col2 = st.columns(2) - with col1: - quality = st.selectbox( - "🎯 Quality", - options=list(QUALITY_PRESETS.keys()), - index=list(QUALITY_PRESETS.keys()).index(st.session_state.settings["quality"]), - key="quality_select" - ) - - with col2: - format_type_display = st.selectbox( - "📦 Format", - options=list(EXPORT_FORMATS.keys()), - index=list(EXPORT_FORMATS.values()).index(st.session_state.settings["format_type"]) - if st.session_state.settings["format_type"] in EXPORT_FORMATS.values() else 0, - key="format_select_display" - ) - # Convert display name to actual format value - format_type = EXPORT_FORMATS[format_type_display] - - animation_speed = st.selectbox( - "⚡ Speed", - options=list(ANIMATION_SPEEDS.keys()), - index=list(ANIMATION_SPEEDS.keys()).index(st.session_state.settings["animation_speed"]), - key="speed_select" - ) - - # Apply the settings without requiring a button - st.session_state.settings = { - "quality": quality, - "format_type": format_type, - "animation_speed": animation_speed - } - - # Custom libraries section - st.markdown("## 📚 Custom Libraries") - st.markdown("Enter additional Python packages needed for your animations (comma-separated):") - - custom_libraries = st.text_area( - "Libraries to install", - placeholder="e.g., scipy, networkx, matplotlib", - key="custom_libraries" - ) - - if st.button("Install Libraries", key="install_libraries_btn"): - success, result = install_custom_packages(custom_libraries) - st.session_state.custom_library_result = result - - if success: - st.success("Installation complete!") - else: - st.error("Installation failed for some packages.") - - if st.session_state.custom_library_result: - with st.expander("Installation Results"): - st.code(st.session_state.custom_library_result) - - # EDITOR TAB + st.header("⚙️ Settings") + with st.expander("Render Settings", True): + st.selectbox("Quality", list(QUALITY_PRESETS.keys()), key="quality") + st.selectbox("Format", list(EXPORT_FORMATS.keys()), key="format") + st.selectbox("Speed", list(ANIMATION_SPEEDS.keys()), key="speed") + with st.expander("Custom Libraries"): + txt = st.text_area("pip install …", help="e.g. scipy,networkx") + if st.button("Install"): + ok,msg = install_custom_packages(txt) + st.code(msg) + st.markdown("---") + st.markdown("Manim Studio • Powered by Streamlit") + + # Tabs + tabs = st.tabs(["✨ Editor","🤖 AI","📚 LaTeX","🎨 Assets","🎞️ Timeline","🎓 Export","🐍 Python"]) + + # --- Editor Tab --- with tabs[0]: - col1, col2 = st.columns([3, 2]) - - with col1: - st.markdown("### 📝 Animation Editor") - - # Toggle between upload and type - editor_mode = st.radio( - "Choose how to input your code:", - ["Type Code", "Upload File"], - key="editor_mode" + st.markdown("⚠️ {warning}
' if warning else "" - - st.markdown(f""" -Max Tokens: {config.get(config['param_name'], 'Unknown')}
-Category: {config['category']}
-API Version: {config['api_version'] if config['api_version'] else 'Default'}
- {warning_html} -Audio: {uploaded_audio.name}
-Path: {audio_path}
-