import streamlit as st import tempfile import os import logging from pathlib import Path from PIL import Image import io import numpy as np import sys import subprocess import json import re import shutil import time from datetime import datetime import streamlit.components.v1 as components import uuid import pandas as pd import plotly.express as px import zipfile import traceback # Set up logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s – %(message)s", handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) # Available quality presets and formats QUALITY_PRESETS = ["480p", "720p", "1080p", "4K", "8K"] FPS_OPTIONS = [24, 30, 60, 120] OUTPUT_FORMATS = { "MP4 Video": "mp4", "GIF Animation": "gif", "WebM Video": "webm", "PNG Sequence (ZIP)": "png_sequence", "SVG Image": "svg" } # Model configurations MODEL_CONFIGS = { "gpt-4o": {}, "gpt-4o-mini": {}, "gpt-4.1": {}, "gpt-4.1-mini": {}, "o1": {}, "o1-mini": {}, "default": {} } # Try to import st_ace try: from streamlit_ace import st_ace ACE_EDITOR_AVAILABLE = True except ImportError: ACE_EDITOR_AVAILABLE = False logger.warning("streamlit-ace not available, using text_area") def get_secret(env_var): val = os.environ.get(env_var) if not val: logger.warning(f"Secret '{env_var}' not set") return val def check_password(): pwd = get_secret("password") if not pwd: st.error("Admin password not configured") return False if "pwd_ok" not in st.session_state: st.session_state.pwd_ok = False if not st.session_state.pwd_ok: entry = st.text_input("Enter password", type="password") if entry: if entry == pwd: st.session_state.pwd_ok = True return True else: st.error("Incorrect password") return False return False return True def ensure_packages(): reqs = { "manim": "0.17.3", "Pillow": "9.0.0", "numpy": "1.22.0", "plotly": "5.14.0", "pandas": "2.0.0", "python-pptx": "0.6.21", "fpdf": "1.7.2", "matplotlib": "3.5.0", "seaborn": "0.11.2", "scipy": "1.7.3", "streamlit-ace": "0.1.1" } missing = {} for pkg, ver in reqs.items(): try: __import__(pkg if pkg != "Pillow" else "PIL") except ImportError: missing[pkg] = ver if not missing: return True bar = st.progress(0) txt = st.empty() for i, (pkg, ver) in enumerate(missing.items()): txt.text(f"Installing {pkg}...") res = subprocess.run([sys.executable, "-m", "pip", "install", f"{pkg}>={ver}"], capture_output=True, text=True) if res.returncode != 0: st.error(f"Failed to install {pkg}") return False bar.progress((i + 1) / len(missing)) txt.empty() return True def extract_scene_class_name(code): m = re.findall(r"class\s+(\w+)\s*\([^)]*Scene", code) return m[0] if m else "MyScene" def generate_manim_preview(code): icons = [] if "Circle" in code: icons.append("⭕") if "Square" in code: icons.append("🔲") if "MathTex" in code or "Tex" in code: icons.append("📊") if "Text" in code: icons.append("📝") if "Axes" in code: icons.append("📈") preview_icons = "".join(icons) or "🎬" return f"""

Preview

{preview_icons}

Full render for accurate output

""" def generate_manim_video(code, fmt, quality, fps, audio_path=None): temp_dir = tempfile.mkdtemp(prefix="manim_") scene = extract_scene_class_name(code) scene_file = os.path.join(temp_dir, "scene.py") with open(scene_file, "w") as f: f.write(code) qflags = {"480p":"-ql","720p":"-qm","1080p":"-qh","4K":"-qk","8K":"-qp"} qf = qflags.get(quality, "-qm") cmd = ["manim", scene_file, scene, qf, f"--format={fmt}", f"--fps={fps}"] process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) progress = st.progress(0) log = st.empty() total = None for line in process.stdout: log.code(line) m = re.search(r"(\d+)\s*/\s*(\d+)", line) if m: cur, tot = map(int, m.groups()) total = tot progress.progress(min(0.99, cur / tot)) else: p = re.search(r"(\d+)%", line) if p: progress.progress(min(0.99, int(p.group(1)) / 100)) process.wait() progress.progress(1.0) # locate output out_path = None for root, _, files in os.walk(temp_dir): for f in files: if f.endswith(f".{fmt}") or (fmt=="png_sequence" and f.endswith(".zip")): out_path = os.path.join(root, f) break data = None if out_path and os.path.exists(out_path): with open(out_path, "rb") as f: data = f.read() shutil.rmtree(temp_dir) if data: size_mb = len(data) / (1024 * 1024) return data, f"✅ Done ({size_mb:.1f} MB)" else: return None, "❌ No output generated" def main(): st.set_page_config("Manim Studio", "🎬", layout="wide") if "inited" not in st.session_state: st.session_state.update({ "inited": True, "code": "", "video_data": None, "status": "", "settings": {"quality":"720p","fps":30,"format":"mp4"}, "pwd_ok": False, "custom_model": "gpt-4o", "generated_code": "" }) if not st.session_state.get("packages_checked", False): if ensure_packages(): st.session_state.packages_checked = True else: return tabs = st.tabs([ "✨ Editor", "🤖 AI Assistant", "🎨 Assets", "🎞️ Timeline", "🎓 Export", "🐍 Python Runner" ]) # Sidebar settings with st.sidebar: st.markdown("## ⚙️ Render Settings") q = st.selectbox("Quality", QUALITY_PRESETS, index=QUALITY_PRESETS.index(st.session_state["settings"]["quality"])) f = st.selectbox("FPS", FPS_OPTIONS, index=FPS_OPTIONS.index(st.session_state["settings"]["fps"])) fmt_disp = st.selectbox("Format", list(OUTPUT_FORMATS.keys()), index=list(OUTPUT_FORMATS.values()).index(st.session_state["settings"]["format"])) st.session_state["settings"] = {"quality": q, "fps": f, "format": OUTPUT_FORMATS[fmt_disp]} # Editor tab with tabs[0]: c1, c2 = st.columns([3,2]) with c1: st.markdown("### 📝 Animation Editor") if ACE_EDITOR_AVAILABLE: st.session_state.code = st_ace( value=st.session_state.code, language="python", theme="monokai", min_lines=20, key="ace" ) else: st.session_state.code = st.text_area( "Manim Code", value=st.session_state.code, height=400 ) if st.button("🚀 Generate Animation"): if not st.session_state.code.strip(): st.error("Enter code first") else: data, msg = generate_manim_video( st.session_state.code, st.session_state["settings"]["format"], st.session_state["settings"]["quality"], st.session_state["settings"]["fps"] ) st.session_state.video_data = data st.session_state.status = msg with c2: if st.session_state.code: components.html(generate_manim_preview(st.session_state.code), height=250) if st.session_state.video_data: fmt = st.session_state["settings"]["format"] if fmt == "png_sequence": st.download_button("⬇️ Download ZIP", st.session_state.video_data, "animation.zip", "application/zip") elif fmt == "svg": try: svg = st.session_state.video_data.decode("utf-8") components.html(svg, height=400) except: st.error("Cannot display SVG") st.download_button("⬇️ Download SVG", st.session_state.video_data, "animation.svg", "image/svg+xml") else: st.video(st.session_state.video_data, format=fmt) st.download_button(f"⬇️ Download {fmt.upper()}", st.session_state.video_data, f"animation.{fmt}", f"video/{fmt}") if st.session_state.status: if st.session_state.status.startswith("❌"): st.error(st.session_state.status) else: st.success(st.session_state.status) # AI Assistant tab with tabs[1]: st.markdown("### 🤖 AI Assistant") if not check_password(): return model = st.selectbox("Select AI Model", list(MODEL_CONFIGS.keys()), index=list(MODEL_CONFIGS.keys()).index(st.session_state.custom_model)) st.session_state.custom_model = model token = get_secret("github_token_api") if st.button("Test Connection"): from azure.ai.inference import ChatCompletionsClient from azure.core.credentials import AzureKeyCredential client = ChatCompletionsClient( endpoint="https://models.inference.ai.azure.com", credential=AzureKeyCredential(token) ) from azure.ai.inference.models import UserMessage resp = client.complete(**{"messages":[UserMessage("Hello")], "model":model, "max_tokens":1000}) if resp.choices: st.success("✅ Connected") st.session_state.ai_client = client else: st.error("❌ No response") if "ai_client" in st.session_state: prompt = st.text_area("Describe animation or provide code") if st.button("Generate Code"): from azure.ai.inference.models import UserMessage msgs = [UserMessage(f"Write a complete Manim scene:\n{prompt}")] resp = st.session_state.ai_client.complete(**{"messages":msgs, "model":model, "max_tokens":8000}) if resp.choices: code = resp.choices[0].message.content if "```python" in code: code = code.split("```python")[1].split("```")[0] st.session_state.generated_code = code else: st.error("No code returned") if st.session_state.generated_code: st.code(st.session_state.generated_code, language="python") if st.button("Use This Code"): st.session_state.code = st.session_state.generated_code st.experimental_rerun() # Assets tab with tabs[2]: st.markdown("### 🎨 Assets") imgs = st.file_uploader("Upload Images", type=["png","jpg","jpeg","svg"], accept_multiple_files=True) if imgs: adir = os.path.join(os.getcwd(),"manim_assets","images") os.makedirs(adir, exist_ok=True) for up in imgs: ext = up.name.split(".")[-1] fn = f"img_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}" path = os.path.join(adir, fn) with open(path,"wb") as f: f.write(up.getvalue()) st.image(path, caption=up.name, width=150) if st.button(f"Use {up.name}"): snippet = f"""image = ImageMobject(r"{path}") self.play(FadeIn(image)) self.wait(1) """ st.session_state.code += "\n" + snippet st.experimental_rerun() aud = st.file_uploader("Upload Audio", type=["mp3","wav","ogg"]) if aud: adir = os.path.join(os.getcwd(),"manim_assets","audio") os.makedirs(adir, exist_ok=True) fn = f"audio_{int(time.time())}.{aud.name.split('.')[-1]}" path = os.path.join(adir, fn) with open(path,"wb") as f: f.write(aud.getvalue()) st.audio(aud) st.success("Audio uploaded") # Timeline tab with tabs[3]: st.markdown("### 🎞️ Timeline Editor") st.info("Adjust code manually – timeline UI coming soon.") # Export tab with tabs[4]: st.markdown("### 🎓 Export") st.warning("Export features coming soon.") # Python Runner tab with tabs[5]: st.markdown("### 🐍 Python Runner") code = st.text_area("Script", height=300, key="py_code") inputs = [] for i, line in enumerate(code.split("\n"), 1): if "input(" in line: prompt = re.search(r'input\(["\'](.+?)["\']\)', line) label = prompt.group(1) if prompt else f"Input line {i}" inputs.append(st.text_input(label, key=f"in_{i}")) timeout = st.slider("Timeout (s)", 5, 300, 30) if st.button("Run"): temp = tempfile.NamedTemporaryFile(delete=False, suffix=".py") temp.write(code.encode()) temp.flush() proc = subprocess.Popen([sys.executable, temp.name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # feed inputs out, err = proc.communicate(input="\n".join(inputs), timeout=timeout) if err: st.error(err) if out: st.code(out) temp.close() if __name__ == "__main__": main()