euler314 commited on
Commit
6181a36
Β·
verified Β·
1 Parent(s): 02a25f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +755 -569
app.py CHANGED
@@ -13,37 +13,35 @@ from pygments import highlight
13
  from pygments.lexers import PythonLexer
14
  from pygments.formatters import HtmlFormatter
15
  import base64
 
 
16
  import re
17
  import shutil
18
  import time
19
  from datetime import datetime, timedelta
20
  import streamlit.components.v1 as components
21
  import uuid
 
22
  import pandas as pd
23
  import plotly.express as px
24
  import markdown
25
  import zipfile
26
- from azure.ai.inference import ChatCompletionsClient
27
- from azure.ai.inference.models import SystemMessage, UserMessage
28
- from azure.core.credentials import AzureKeyCredential
29
- from openai import OpenAI
30
- from transformers import pipeline
31
- import torch
32
  import traceback
 
33
 
34
- # ──────────────────────────────────────────────────────────────────────────────
35
- # Logging
36
- # ──────────────────────────────────────────────────────────────────────────────
37
  logging.basicConfig(
38
  level=logging.INFO,
39
- format="%(asctime)s β€’ %(name)s β€’ %(levelname)s β€’ %(message)s",
40
- handlers=[logging.StreamHandler()]
 
 
41
  )
42
  logger = logging.getLogger(__name__)
43
 
44
- # ──────────────────────────────────────────────────────────────────────────────
45
- # Model & Render Configuration
46
- # ──────────────────────────────────────────────────────────────────────────────
47
  MODEL_CONFIGS = {
48
  "DeepSeek-V3-0324": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None},
49
  "DeepSeek-R1": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None},
@@ -61,652 +59,840 @@ MODEL_CONFIGS = {
61
  "Phi-4-multimodal-instruct": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Microsoft", "warning": None},
62
  "Mistral-large-2407": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None},
63
  "Codestral-2501": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None},
 
64
  "default": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Other", "warning": None}
65
  }
66
 
67
- QUALITY_PRESETS = {
68
- "480p": {"flag": "-ql", "fps": 30},
69
- "720p": {"flag": "-qm", "fps": 30},
70
- "1080p": {"flag": "-qh", "fps": 60},
71
- "4K": {"flag": "-qk", "fps": 60},
72
- "8K": {"flag": "-qp", "fps": 60},
73
- }
74
-
75
- ANIMATION_SPEEDS = {
76
- "Slow": 0.5,
77
- "Normal": 1.0,
78
- "Fast": 2.0,
79
- "Very Fast": 3.0
80
- }
81
-
82
- EXPORT_FORMATS = {
83
- "MP4 Video": "mp4",
84
- "GIF Animation": "gif",
85
- "WebM Video": "webm",
86
- "PNG Sequence": "png_sequence",
87
- "SVG": "svg"
88
- }
89
 
90
- # ──────────────────────────────────────────────────────────────────────────────
91
- # 1. prepare_api_params
92
- # ──────────────────────────────────────────────────────────────────────────────
93
  def prepare_api_params(messages, model_name):
94
- """Lookup MODEL_CONFIGS and build API call parameters."""
95
  config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"])
96
- params = {
97
- "messages": messages,
98
- "model": model_name,
99
- config["param_name"]: config.get(config["param_name"])
100
- }
101
- return params, config
102
 
103
- # ────────────────���─────────────────────────────────────────────────────────────
104
- # 2. get_secret
105
- # ──────────────────────────────────────────────────────────────────────────────
106
  def get_secret(key):
107
- """Read an environment variable (e.g. password, API token)."""
108
- val = os.environ.get(key)
109
- if not val:
110
- logger.warning(f"Secret '{key}' not found")
111
- return val or ""
112
-
113
- # ──────────────────────────────────────────────────────────────────────────────
114
- # 3. check_password
115
- # ──────────────────────────────────────────────────────────────────────────────
116
  def check_password():
117
- """Prompt for admin password and gate AI features."""
118
- correct = get_secret("password")
119
- if not correct:
120
- st.error("Admin password not configured in secrets")
121
  return False
122
- if "auth_ok" not in st.session_state:
123
- st.session_state.auth_ok = False
124
- if not st.session_state.auth_ok:
125
- pwd = st.text_input("πŸ”’ Enter admin password", type="password", help="Protects AI assistant")
126
  if pwd:
127
- if pwd == correct:
128
- st.session_state.auth_ok = True
129
- st.success("Access granted")
130
  else:
131
  st.error("Incorrect password")
 
132
  return False
133
  return True
134
 
135
- # ──────────────────────────────────────────────────────────────────────────────
136
- # 4. ensure_packages
137
- # ──────────────────────────────────────────────────────────────────────────────
138
  def ensure_packages():
139
- """Check & install core dependencies on first run."""
140
- required = {
141
- 'streamlit':'1.25.0','manim':'0.17.3','numpy':'1.22.0','Pillow':'9.0.0',
142
- 'transformers':'4.30.0','torch':'2.0.0','plotly':'5.14.0','pandas':'2.0.0',
143
- 'python-pptx':'0.6.21','markdown':'3.4.3','fpdf':'1.7.2','matplotlib':'3.5.0',
144
- 'seaborn':'0.11.2','scipy':'1.7.3','huggingface_hub':'0.16.0',
145
- 'azure-ai-inference':'1.0.0b9','azure-core':'1.33.0','openai':''
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
- missing = []
148
- for pkg, ver in required.items():
149
  try:
150
- __import__(pkg if pkg!='Pillow' else 'PIL')
151
  except ImportError:
152
- missing.append(f"{pkg}>={ver}" if ver else pkg)
153
- if missing:
154
- st.sidebar.info("Installing required packages...")
155
- prog = st.sidebar.progress(0)
156
- for i, pkg in enumerate(missing, 1):
157
- subprocess.run([sys.executable, "-m", "pip", "install", pkg], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
158
- prog.progress(i/len(missing))
159
- st.sidebar.success("All packages installed")
160
-
161
- # ──────────────────────────────────────────────────────────────────────────────
162
- # 5. install_custom_packages
163
- # ──────────────────────────────────────────────────────────────────────────────
164
- def install_custom_packages(package_list):
165
- """Install user-specified pip packages on the fly."""
166
- packages = [p.strip() for p in package_list.split(",") if p.strip()]
167
- if not packages:
168
- return True, "No packages specified"
169
- results = []
170
- success = True
171
- for pkg in packages:
172
- res = subprocess.run([sys.executable, "-m", "pip", "install", pkg], capture_output=True, text=True)
173
- ok = (res.returncode == 0)
174
- results.append(f"{pkg}: {'βœ…' if ok else '❌'}")
175
- if not ok: success = False
176
- return success, "\n".join(results)
177
-
178
- # ──────────────────────────────────────────────────────────────────────────────
179
- # 6. init_ai_models_direct
180
- # ──────────────────────────────────────────────────────────────────────────────
181
  @st.cache_resource(ttl=3600)
182
  def init_ai_models_direct():
183
- """Initialize Azure ChatCompletionsClient for AI code generation."""
184
- token = get_secret("github_token_api")
185
- if not token:
186
- st.error("GitHub token not found in secrets")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return None
188
- endpoint = "https://models.inference.ai.azure.com"
189
- client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token))
190
- return {"client": client, "model_name": "gpt-4o", "endpoint": endpoint}
191
 
192
- # ──────────────────────────────────────────────────────────────────────────────
193
- # 7. suggest_code_completion
194
- # ──────────────────────────────────────────────────────────────────────────────
195
  def suggest_code_completion(code_snippet, models):
196
- """Use the initialized AI model to generate complete Manim code."""
197
  if not models:
198
  st.error("AI models not initialized")
199
  return None
200
- prompt = f"""Write a complete Manim animation scene based on this code or idea:
 
201
  {code_snippet}
202
 
203
- The code should include:
204
- - A Scene subclass
205
- - self.play() animations
206
- - wait times
207
- Return only valid Python code.
 
 
208
  """
209
- config = MODEL_CONFIGS.get(models["model_name"].split("/")[-1], MODEL_CONFIGS["default"])
210
- if config["category"] == "OpenAI":
211
- client = models.get("openai_client") or OpenAI(base_url="https://models.github.ai/inference", api_key=get_secret("github_token_api"))
212
- models["openai_client"] = client
213
- messages = [{"role":"developer","content":"Expert in Manim."}, {"role":"user","content":prompt}]
214
- params = {"messages": messages, "model": models["model_name"], config["param_name"]: config.get(config["param_name"])}
215
- resp = client.chat.completions.create(**params)
216
- content = resp.choices[0].message.content
217
- else:
218
- client = models["client"]
219
- msgs = [UserMessage(prompt)]
220
- params, _ = prepare_api_params(msgs, models["model_name"])
221
- resp = client.complete(**params)
222
- content = resp.choices[0].message.content
223
- # extract code block
224
- if "```python" in content:
225
- content = content.split("```python")[1].split("```")[0]
226
- elif "```" in content:
227
- content = content.split("```")[1].split("```")[0]
228
- if "class" not in content:
229
- content = f"from manim import *\n\nclass MyScene(Scene):\n def construct(self):\n {content}"
230
- return content
231
-
232
- # ──────────────────────────────────────────────────────────────────────────────
233
- # 8. check_model_freshness
234
- # ──────────────────────────────────────────────────────────────────────────────
235
- def check_model_freshness():
236
- """Return True if AI client was loaded within the past hour."""
237
- if not st.session_state.get("ai_models"): return False
238
- last = st.session_state.ai_models.get("last_loaded")
239
- if not last: return False
240
- return datetime.fromisoformat(last) + timedelta(hours=1) > datetime.now()
241
-
242
- # ──────────────────────────────────────────────────────────────────────────────
243
- # 9. extract_scene_class_name
244
- # ──────────────────────────────────────────────────────────────────────────────
245
- def extract_scene_class_name(python_code):
246
- """Regex for the first class inheriting from Scene."""
247
- m = re.findall(r"class\s+(\w+)\s*\([^)]*Scene[^)]*\)", python_code)
248
- return m[0] if m else "MyScene"
 
 
 
249
 
250
- # ──────────────────────────────────────────────────────────────────────────────
251
- # 10. highlight_code
252
- # ──────────────────────────────────────────────────────────────────────────────
253
  def highlight_code(code):
254
- """Return HTML+CSS highlighted Python code."""
255
- formatter = HtmlFormatter(style="monokai", full=True, noclasses=True)
256
- return highlight(code, PythonLexer(), formatter)
257
 
258
- # ───────────────────────────���──────────────────────────────────────────────────
259
- # 11. generate_manim_preview
260
- # ──────────────────────────────────────────────────────────────────────────────
261
  def generate_manim_preview(python_code):
262
- """Show icons for detected Manim objects in code."""
263
- icons = []
264
- mapping = {
265
- "Circle":"β­•","Square":"πŸ”²","MathTex":"πŸ“Š","Tex":"πŸ“Š",
266
- "Text":"πŸ“","Axes":"πŸ“ˆ","ThreeDScene":"🧊","Sphere":"🌐","Cube":"🧊"
267
- }
268
- for key,icon in mapping.items():
269
- if key in python_code: icons.append(icon)
270
- icons = icons or ["🎬"]
271
- html = f"""
272
- <div style="background:#000;color:#fff;padding:1rem;border-radius:8px;text-align:center;">
273
- <h4>Animation Preview</h4>
274
- <div style="font-size:2.5rem">{''.join(icons)}</div>
275
- <p style="opacity:0.7">Accurate preview requires full render</p>
 
 
 
276
  </div>
277
  """
278
- return html
279
-
280
- # ──────────────────────────────────────────────────────────────────────────────
281
- # 12. render_latex_preview
282
- # ──────────────────────────────────────────────────────────────────────────────
283
- def render_latex_preview(latex_formula):
284
- """Return HTML snippet with MathJax preview for LaTeX."""
285
- if not latex_formula:
286
  return """
287
- <div style="background:#f8f9fa;padding:1rem;border-radius:6px;text-align:center;color:#777;">
288
- Enter a LaTeX formula above.
289
- </div>"""
 
290
  return f"""
291
- <div style="background:#202124;color:#fff;padding:1rem;border-radius:6px;text-align:center;">
292
- <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
293
- <script async id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
294
- <h4>LaTeX Preview</h4>
295
- <div>$$ {latex_formula} $$</div>
296
- </div>"""
297
-
298
- # ──────────────────────────────────────────────────────────────────────────────
299
- # 13. prepare_audio_for_manim
300
- # ──────────────────────────────────────────────────────────────────────────────
 
301
  def prepare_audio_for_manim(audio_file, target_dir):
302
- """Save uploaded audio and return filesystem path."""
303
- os.makedirs(target_dir, exist_ok=True)
304
  filename = f"audio_{int(time.time())}.mp3"
305
- out = os.path.join(target_dir, filename)
306
- with open(out, "wb") as f:
307
- f.write(audio_file.getvalue())
308
- return out
309
-
310
- # ──────────────────────────────────────────────────────────────────────────────
311
- # 14. mp4_to_gif
312
- # ──────────────────────────────────────────────────────────────────────────────
313
- def mp4_to_gif(mp4_path, output_path, fps=15):
314
- """Use ffmpeg to convert an MP4 to a looping GIF."""
315
- cmd = [
316
- "ffmpeg","-i",mp4_path,
317
- "-vf",f"fps={fps},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
318
- "-loop","0",output_path
319
- ]
320
- res = subprocess.run(cmd, capture_output=True, text=True)
321
- return output_path if res.returncode==0 else None
322
-
323
- # ──────────────────────────────────────────────────────────────────────────────
324
- # 15. generate_manim_video
325
- # ──────────────────────────────────────────────────────────────────────────────
326
- def generate_manim_video(python_code, format_type, quality_preset, animation_speed=1.0, audio_path=None):
327
- """Render code via Manim CLI; fallback for GIF via ffmpeg."""
328
- temp_dir = tempfile.mkdtemp(prefix="manim_")
329
  try:
330
- scene = extract_scene_class_name(python_code)
331
- scene_file = os.path.join(temp_dir, "scene.py")
332
- with open(scene_file, "w", encoding="utf-8") as f:
333
- f.write(python_code)
334
- flag = QUALITY_PRESETS[quality_preset]["flag"]
335
- cmd = ["manim", scene_file, scene, flag, f"--format={format_type}"]
 
 
 
 
 
 
 
 
 
 
 
 
336
  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
337
- output = []
 
338
  while True:
339
  line = proc.stdout.readline()
340
- if not line and proc.poll() is not None:
341
- break
342
  output.append(line)
 
 
 
 
 
 
 
 
 
 
343
  proc.wait()
344
- # find output file
345
- matches = list(Path(temp_dir).rglob(f"*.{format_type}"))
346
- if format_type == "gif" and not matches:
347
- # try ffmpeg fallback
348
- mp4s = list(Path(temp_dir).rglob("*.mp4"))
349
- if mp4s:
350
- gif = os.path.join(temp_dir, f"{scene}.gif")
351
- mp4_to_gif(str(mp4s[-1]), gif)
352
- matches = [Path(gif)]
353
- if not matches:
354
- return None, "❌ No output file found"
355
- data = matches[-1].read_bytes()
356
- return data, f"βœ… Generated ({len(data)/(1024*1024):.1f} MB)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  finally:
358
- shutil.rmtree(temp_dir, ignore_errors=True)
 
359
 
360
- # ──────────────────────────────────────────────────────────────────────────────
361
- # 16. detect_input_calls
362
- # ──────────────────────────────────────────────────────────────────────────────
363
  def detect_input_calls(code):
364
- """Scan for input() calls and extract prompts."""
365
- calls = []
366
- for i, line in enumerate(code.splitlines(), 1):
367
- if "input(" in line and not line.strip().startswith("#"):
368
- m = re.search(r'input\(["\'](.+?)["\']\)', line)
369
- prompt = m.group(1) if m else f"Input at line {i}"
370
- calls.append({"line": i, "prompt": prompt})
371
  return calls
372
 
373
- # ──────────────────────────────────────────────────────────────────────────────
374
- # 17. run_python_script
375
- # ──────────────────────────────────────────────────────────────────────────────
376
  def run_python_script(code, inputs=None, timeout=60):
377
- """Execute arbitrary Python code, capturing stdout/stderr, plots, DataFrames."""
378
- tmp = tempfile.mkdtemp(prefix="run_")
379
- result = {"stdout":"", "stderr":"", "exception":None, "plots":[], "dataframes":[], "execution_time":0}
380
- # override input()
381
  if inputs:
382
- wrapper = (
383
- "__INPUTS="+json.dumps(inputs)+"\n"
384
- "__IDX=0\n"
385
- "def input(prompt=''):\n"
386
- " global __IDX\n"
387
- " val = __INPUTS[__IDX] if __IDX<len(__INPUTS) else ''\n"
388
- " __IDX +=1\n"
389
- " print(prompt+val)\n"
390
- " return val\n\n"
391
- )
392
- code = wrapper + code
393
- # ensure matplotlib & pandas imports
394
- if "plt" in code and "import matplotlib" not in code:
395
- code = "import matplotlib.pyplot as plt\n" + code
396
- if "pd." in code and "import pandas" not in code:
397
- code = "import pandas as pd\n" + code
398
- script_path = os.path.join(tmp, "script.py")
399
- with open(script_path, "w") as f:
400
- f.write(code)
401
- start = time.time()
402
- try:
403
- proc = subprocess.Popen([sys.executable, script_path],
404
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
405
- cwd=tmp, text=True)
406
- out, err = proc.communicate(timeout=timeout)
407
- result["stdout"] = out
408
- result["stderr"] = err
409
- except subprocess.TimeoutExpired:
410
- proc.kill()
411
- result["stderr"] += f"\n⏱️ Execution timed out after {timeout}s"
412
- finally:
413
- result["execution_time"] = time.time() - start
414
- # plots & dataframes capture omitted for brevity
415
- shutil.rmtree(tmp, ignore_errors=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  return result
417
 
418
- # ──────────────────────────────────────────────────────────────────────────────
419
- # 18. display_python_script_results
420
- # ──────────────────────────────────────────────────────────────────────────────
421
- def display_python_script_results(res):
422
- """Render the result dict from run_python_script() in Streamlit."""
423
- if res["exception"]:
424
- st.error(f"Exception: {res['exception']}")
425
- if res["stderr"]:
426
  st.error("Errors:")
427
- st.code(res["stderr"])
428
- if res["stdout"]:
429
- st.markdown("### Output:")
430
- st.code(res["stdout"])
431
- st.info(f"Execution time: {res['execution_time']:.2f}s")
432
- # plots & dataframes display omitted for brevity
433
-
434
- # ──────────────────────────────────────────────────────────────────────────────
435
- # 19. parse_animation_steps
436
- # ─────────────────────────────────────────────────────────────────────────���────
437
- def parse_animation_steps(python_code):
438
- """Extract self.play() and self.wait() steps into a list of dicts."""
439
- plays = re.findall(r"self\.play\((.*?)\)", python_code, re.DOTALL)
440
- waits = re.findall(r"self\.wait\((.*?)\)", python_code, re.DOTALL)
441
- steps = []
442
- current = 0.0
443
- for i, play in enumerate(plays):
444
- anims = [a.strip() for a in play.split(",")]
445
- dur = float(waits[i]) if i < len(waits) and re.match(r"[\d\.]+", waits[i]) else 1.0
446
- steps.append({"id": i+1, "animations": anims, "duration": dur, "start_time": current, "code": f"self.play({play})"})
447
- current += dur
 
 
 
 
 
 
 
448
  return steps
449
 
450
- # ──────────────────────────────────────────────────────────────────────────────
451
- # 20. generate_code_from_timeline
452
- # ──────────────────────────────────────────────────────────────────────────────
453
- def generate_code_from_timeline(animation_steps, original_code):
454
- """Regenerate the construct() method body from timeline steps."""
455
- class_match = re.search(r"(class\s+\w+\s*\([^)]*\)\s*:\s*.*?def\s+construct\s*\(self\)\s*:)", original_code, re.DOTALL)
456
- if not class_match:
457
- return original_code
458
- header = class_match.group(1)
459
- indent = " " * (len(header) - len(header.lstrip())) + " "
460
- body = [header]
461
- for step in animation_steps:
462
- body.append(f"{indent}{step['code']}")
463
- body.append(f"{indent}self.wait({step['duration']})")
464
- return "\n".join(body)
465
-
466
- # ──────────────────────────────────────────────────────────────────────────────
467
- # 21. create_timeline_editor
468
- # ──────────────────────────────────────────────────────────────────────────────
469
  def create_timeline_editor(code):
470
- """Interactive timeline editor tab to reorder/update animation steps."""
471
- st.markdown("### 🎞 Animation Timeline")
472
- steps = parse_animation_steps(code)
 
 
473
  if not steps:
474
- st.warning("No animation steps detected.")
475
  return code
476
- df = pd.DataFrame(steps)
477
- fig = px.timeline(df, x_start="start_time", x_end=df["start_time"]+df["duration"],
478
- y="id", color="id", hover_name="animations")
479
- fig.update_layout(height=300, showlegend=False, xaxis_title="Time (s)", yaxis_title="Step")
480
- st.plotly_chart(fig, use_container_width=True)
481
- cols = st.columns(3)
482
- step_id = cols[0].selectbox("Select Step", df["id"])
483
- new_dur = cols[1].number_input("New Duration (s)", min_value=0.1, step=0.1, value=float(df[df["id"]==step_id]["duration"]))
484
- action = cols[2].selectbox("Action", ["Update Duration","Delete Step","Move Up","Move Down"])
485
  if st.button("Apply"):
486
- idx = df[df["id"]==step_id].index[0]
487
  if action=="Update Duration":
488
  df.at[idx,"duration"]=new_dur
489
- elif action=="Delete Step":
490
- df = df[df["id"]!=step_id]
491
- elif action=="Move Up" and step_id>1:
492
- other = df[df["id"]==step_id-1].index[0]
493
- df.at[idx,"id"],df.at[other,"id"]=df.at[other,"id"],df.at[idx,"id"]
494
- elif action=="Move Down" and step_id<len(df):
495
- other = df[df["id"]==step_id+1].index[0]
496
- df.at[idx,"id"],df.at[other,"id"]=df.at[other,"id"],df.at[idx,"id"]
497
- df = df.sort_values("id").reset_index(drop=True)
498
- current=0.0
499
- for i,row in df.iterrows():
500
- df.at[i,"start_time"]=current
501
- current+=row["duration"]
502
- new_code = generate_code_from_timeline(df.to_dict("records"), code)
503
- st.success("Timeline updated!")
504
  return new_code
505
  return code
506
 
507
- # ──────────────────────────────────────────────────────────────────────────────
508
- # 22. export_to_educational_format
509
- # ──────────────────────────────────────────────────────────────────────────────
510
- def export_to_educational_format(video_data, format_type, animation_title, explanation_text, temp_dir):
511
- """Export the existing video_data to PPTX, HTML, or PDF sequence."""
512
- if format_type=="powerpoint":
513
- from pptx import Presentation
514
- from pptx.util import Inches
515
- prs = Presentation()
516
- slide = prs.slides.add_slide(prs.slide_layouts[0])
517
- slide.shapes.title.text = animation_title
518
- video_path = os.path.join(temp_dir,"video.mp4")
519
- with open(video_path,"wb") as f: f.write(video_data)
520
- slide2 = prs.slides.add_slide(prs.slide_layouts[5])
521
- slide2.shapes.title.text="Animation"
522
- slide2.shapes.add_movie(video_path, Inches(1),Inches(1.5),Inches(8),Inches(4.5))
523
- if explanation_text:
524
- txt_sl = prs.slides.add_slide(prs.slide_layouts[1])
525
- txt_sl.shapes.title.text="Explanation"
526
- txt_sl.placeholders[1].text=explanation_text
527
- out = os.path.join(temp_dir,f"{animation_title}.pptx")
528
- prs.save(out)
529
- return open(out,"rb").read(), "pptx"
530
-
531
- elif format_type=="html":
532
- html_template = """<!DOCTYPE html><html><head><meta charset="utf-8"><title>{title}</title></head><body>
533
- <h1>{title}</h1><video controls width="100%"><source src="data:video/mp4;base64,{b64}"></video>
534
- <div>{explanation}</div></body></html>"""
535
- b64 = base64.b64encode(video_data).decode()
536
- expl = markdown.markdown(explanation_text or "")
537
- content = html_template.format(title=animation_title,b64=b64,explanation=expl)
538
- out = os.path.join(temp_dir,f"{animation_title}.html")
539
- with open(out,"w",encoding="utf-8") as f: f.write(content)
540
- return open(out,"rb").read(), "html"
541
-
542
- elif format_type=="sequence":
543
- from fpdf import FPDF
544
- video_path = os.path.join(temp_dir,"video.mp4")
545
- with open(video_path,"wb") as f: f.write(video_data)
546
- frames_dir = os.path.join(temp_dir,"frames")
547
- os.makedirs(frames_dir, exist_ok=True)
548
- # extract 5 key frames
549
- subprocess.run(["ffmpeg","-i",video_path,"-vf","select=not(mod(n\\,10))","-vsync","vfr",
550
- os.path.join(frames_dir,"frame_%03d.png")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
551
- pdf = FPDF()
552
- segments = explanation_text.split("##") if explanation_text else []
553
- for i, img in enumerate(sorted(os.listdir(frames_dir))):
554
- pdf.add_page()
555
- pdf.image(os.path.join(frames_dir,img), x=10,y=10,w=190)
556
- pdf.ln(100)
557
- txt = segments[i] if i<len(segments) else ""
558
- pdf.multi_cell(0, 5, txt)
559
- out = os.path.join(temp_dir,f"{animation_title}.pdf")
560
- pdf.output(out)
561
- return open(out,"rb").read(), "pdf"
562
- return None, None
563
-
564
- # ──────────────────────────────────────────────────────────────────────────────
565
- # 23. main
566
- # ──────────────────────────────────────────────────────────────────────────────
567
  def main():
568
- st.set_page_config(page_title="🎬 Manim Animation Studio", layout="wide")
569
- # Custom CSS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  st.markdown("""
571
  <style>
572
- .main-header { font-size:2.5rem; text-align:center; background:linear-gradient(90deg,#4F46E5,#818CF8); -webkit-background-clip:text; -webkit-text-fill-color:transparent; margin-bottom:1rem; }
573
- .card { background:#fff; padding:1rem; border-radius:8px; box-shadow:0 2px 6px rgba(0,0,0,0.1); margin-bottom:1rem; }
574
  </style>
575
  """, unsafe_allow_html=True)
576
-
577
- # Ensure packages installed once
578
- if 'packages_checked' not in st.session_state:
579
- ensure_packages()
580
- st.session_state.packages_checked = True
581
-
582
- # Sidebar
583
- with st.sidebar:
584
- st.header("βš™οΈ Settings")
585
- with st.expander("Render Settings", True):
586
- st.selectbox("Quality", list(QUALITY_PRESETS.keys()), key="quality")
587
- st.selectbox("Format", list(EXPORT_FORMATS.keys()), key="format")
588
- st.selectbox("Speed", list(ANIMATION_SPEEDS.keys()), key="speed")
589
- with st.expander("Custom Libraries"):
590
- txt = st.text_area("pip install …", help="e.g. scipy,networkx")
591
- if st.button("Install"):
592
- ok,msg = install_custom_packages(txt)
593
- st.code(msg)
594
- st.markdown("---")
595
- st.markdown("Manim Studio β€’ Powered by Streamlit")
596
-
597
- # Tabs
598
- tabs = st.tabs(["✨ Editor","πŸ€– AI","πŸ“š LaTeX","🎨 Assets","🎞️ Timeline","πŸŽ“ Export","🐍 Python"])
599
-
600
  # --- Editor Tab ---
601
  with tabs[0]:
602
- st.markdown("<div class='main-header'>✨ Animation Editor</div>", unsafe_allow_html=True)
603
- code = st.text_area("Python code", height=300, key="editor_code")
604
- st.markdown(generate_manim_preview(code), unsafe_allow_html=True)
605
- if st.button("πŸš€ Generate Animation"):
606
- data, status = generate_manim_video(
607
- code,
608
- EXPORT_FORMATS[st.session_state.format],
609
- st.session_state.quality,
610
- ANIMATION_SPEEDS[st.session_state.speed]
611
- )
612
- if data:
613
- st.video(data)
614
- st.success(status)
615
- st.session_state.last_video = data
616
  else:
617
- st.error(status)
618
- if st.session_state.get("last_video"):
619
- st.download_button("⬇️ Download Animation", st.session_state.last_video,
620
- f"manim_animation.{EXPORT_FORMATS[st.session_state.format]}", use_container_width=True)
621
-
622
- # --- AI Tab ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  with tabs[1]:
624
- st.markdown("<div class='main-header'>πŸ€– AI Animation Assistant</div>", unsafe_allow_html=True)
625
- if not check_password():
626
- return
627
- if "ai_models" not in st.session_state or not check_model_freshness():
628
- models = init_ai_models_direct()
629
- if models:
630
- st.session_state.ai_models = {**models, "last_loaded": datetime.now().isoformat()}
631
- st.markdown("### Describe your animation or paste code stub")
632
- prompt = st.text_area("Prompt / stub", height=150)
633
- if st.button("✨ Generate Code"):
634
- models = st.session_state.ai_models
635
- gen = suggest_code_completion(prompt, models)
636
- if gen:
637
- st.code(gen, language="python")
638
- if st.button("Use This Code"):
639
- st.session_state.editor_code = gen
640
- st.experimental_rerun()
641
-
642
- # --- LaTeX Tab ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  with tabs[2]:
644
- st.markdown("<div class='main-header'>πŸ“š LaTeX Formula Builder</div>", unsafe_allow_html=True)
645
- latex_input = st.text_input("LaTeX", key="latex_input", help="Raw string, e.g. r\"e^{i\\pi}+1=0\"")
646
- st.markdown(render_latex_preview(latex_input), unsafe_allow_html=True)
647
- if latex_input and st.button("Insert into Editor"):
648
- snippet = f"""formula = MathTex(r"{latex_input}")\nself.play(Write(formula))\nself.wait(2)"""
649
- st.session_state.editor_code += "\n " + snippet
650
- st.success("Inserted into editor")
651
- st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
  # --- Assets Tab ---
654
  with tabs[3]:
655
- st.markdown("<div class='main-header'>🎨 Asset Management</div>", unsafe_allow_html=True)
656
- imgs = st.file_uploader("Upload images", accept_multiple_files=True)
657
- for img in imgs:
658
- st.image(img, width=150, caption=img.name)
659
- if st.button(f"Use {img.name}"):
660
- code_snip = f"""from manim import ImageMobject\nimg = ImageMobject(r"{img.name}")\nself.play(FadeIn(img))"""
661
- st.session_state.editor_code += "\n " + code_snip
662
- st.success(f"Added {img.name} to code")
663
- st.experimental_rerun()
664
- audio = st.file_uploader("Upload audio", type=["mp3","wav"])
665
- if audio:
666
- path = prepare_audio_for_manim(audio, "manim_assets/audio")
667
- st.audio(audio)
668
- st.code(f"@with_sound(r\"{path}\")\nclass YourScene(Scene):\n ...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
  # --- Timeline Tab ---
671
  with tabs[4]:
672
- st.markdown("<div class='main-header'>🎞️ Timeline Editor</div>", unsafe_allow_html=True)
673
- new_code = create_timeline_editor(st.session_state.get("editor_code",""))
674
- if new_code != st.session_state.get("editor_code",""):
675
- st.session_state.editor_code = new_code
676
 
677
- # --- Export Tab ---
678
  with tabs[5]:
679
- st.markdown("<div class='main-header'>πŸŽ“ Educational Export</div>", unsafe_allow_html=True)
680
- if not st.session_state.get("last_video"):
681
- st.warning("Generate an animation first")
682
  else:
683
- title = st.text_input("Animation Title", "My Animation")
684
- expl = st.text_area("Explanation (use ## for steps)")
685
- fmt = st.selectbox("Export Format", ["PowerPoint","HTML","PDF Sequence"])
686
- if st.button("πŸ“€ Export"):
687
- fmt_key = {"PowerPoint":"powerpoint","HTML":"html","PDF Sequence":"sequence"}[fmt]
688
- data,ft = export_to_educational_format(
689
- st.session_state.last_video, fmt_key, title, expl, tempfile.mkdtemp()
690
- )
691
  if data:
692
- ext = {"pptx":"pptx","html":"html","pdf":"pdf"}[ft]
693
- st.success(f"{fmt} created")
694
- st.download_button(f"⬇️ Download {fmt}", data, f"{title}.{ext}")
695
 
696
- # --- Python Tab ---
697
  with tabs[6]:
698
- st.markdown("<div class='main-header'>🐍 Python Script Runner</div>", unsafe_allow_html=True)
699
- script = st.text_area("Script", height=200, key="python_script")
700
- calls = detect_input_calls(script)
701
- inputs = []
 
 
 
 
 
 
 
 
 
 
 
702
  if calls:
703
- st.info("Detected input() calls – please provide values:")
704
- for c in calls:
705
- v = st.text_input(f"{c['prompt']} (line {c['line']})")
706
- inputs.append(v)
707
- if st.button("▢️ Run Script"):
708
- res = run_python_script(script, inputs)
709
- display_python_script_results(res)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
  if __name__ == "__main__":
712
  main()
 
13
  from pygments.lexers import PythonLexer
14
  from pygments.formatters import HtmlFormatter
15
  import base64
16
+ from transformers import pipeline
17
+ import torch
18
  import re
19
  import shutil
20
  import time
21
  from datetime import datetime, timedelta
22
  import streamlit.components.v1 as components
23
  import uuid
24
+ import platform
25
  import pandas as pd
26
  import plotly.express as px
27
  import markdown
28
  import zipfile
29
+ import contextlib
30
+ import threading
 
 
 
 
31
  import traceback
32
+ from io import StringIO, BytesIO
33
 
34
+ # Set up enhanced logging
 
 
35
  logging.basicConfig(
36
  level=logging.INFO,
37
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
38
+ handlers=[
39
+ logging.StreamHandler()
40
+ ]
41
  )
42
  logger = logging.getLogger(__name__)
43
 
44
+ # Model configuration mapping for different API requirements and limits
 
 
45
  MODEL_CONFIGS = {
46
  "DeepSeek-V3-0324": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None},
47
  "DeepSeek-R1": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None},
 
59
  "Phi-4-multimodal-instruct": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Microsoft", "warning": None},
60
  "Mistral-large-2407": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None},
61
  "Codestral-2501": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral", "warning": None},
62
+ # Default configuration for other models
63
  "default": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Other", "warning": None}
64
  }
65
 
66
+ # Try to import Streamlit Ace
67
+ try:
68
+ from streamlit_ace import st_ace
69
+ ACE_EDITOR_AVAILABLE = True
70
+ except ImportError:
71
+ ACE_EDITOR_AVAILABLE = False
72
+ logger.warning("streamlit-ace not available, falling back to standard text editor")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
 
 
 
74
  def prepare_api_params(messages, model_name):
75
+ """Create appropriate API parameters based on model configuration"""
76
  config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"])
77
+ api_params = {"messages": messages, "model": model_name}
78
+ token_param = config["param_name"]
79
+ token_value = config[token_param]
80
+ api_params[token_param] = token_value
81
+ return api_params, config
 
82
 
 
 
 
83
  def get_secret(key):
84
+ """Retrieve a secret from environment or Streamlit secrets."""
85
+ if hasattr(st, "secrets") and key in st.secrets:
86
+ return st.secrets[key]
87
+ return os.environ.get(key)
88
+
 
 
 
 
89
  def check_password():
90
+ correct_password = get_secret("password")
91
+ if not correct_password:
92
+ st.error("Admin password not configured in secrets or env var 'password'")
 
93
  return False
94
+ if "password_entered" not in st.session_state:
95
+ st.session_state.password_entered = False
96
+ if not st.session_state.password_entered:
97
+ pwd = st.text_input("Enter password to access AI features", type="password")
98
  if pwd:
99
+ if pwd == correct_password:
100
+ st.session_state.password_entered = True
101
+ return True
102
  else:
103
  st.error("Incorrect password")
104
+ return False
105
  return False
106
  return True
107
 
 
 
 
108
  def ensure_packages():
109
+ required_packages = {
110
+ 'manim': '0.17.3',
111
+ 'Pillow': '9.0.0',
112
+ 'numpy': '1.22.0',
113
+ 'transformers': '4.30.0',
114
+ 'torch': '2.0.0',
115
+ 'pygments': '2.15.1',
116
+ 'streamlit-ace': '0.1.1',
117
+ 'pydub': '0.25.1',
118
+ 'plotly': '5.14.0',
119
+ 'pandas': '2.0.0',
120
+ 'python-pptx': '0.6.21',
121
+ 'markdown': '3.4.3',
122
+ 'fpdf': '1.7.2',
123
+ 'matplotlib': '3.5.0',
124
+ 'seaborn': '0.11.2',
125
+ 'scipy': '1.7.3',
126
+ 'huggingface_hub': '0.16.0',
127
  }
128
+ missing = {}
129
+ for pkg, ver in required_packages.items():
130
  try:
131
+ __import__(pkg if pkg != 'Pillow' else 'PIL')
132
  except ImportError:
133
+ missing[pkg] = ver
134
+ if not missing:
135
+ return True
136
+ progress = st.progress(0)
137
+ status = st.empty()
138
+ for i, (pkg, ver) in enumerate(missing.items()):
139
+ status.text(f"Installing {pkg}...")
140
+ res = subprocess.run([sys.executable, "-m", "pip", "install", f"{pkg}>={ver}"], capture_output=True, text=True)
141
+ if res.returncode != 0:
142
+ st.error(f"Failed to install {pkg}: {res.stderr}")
143
+ return False
144
+ progress.progress((i + 1) / len(missing))
145
+ return True
146
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  @st.cache_resource(ttl=3600)
148
  def init_ai_models_direct():
149
+ try:
150
+ token = get_secret("github_token_api")
151
+ if not token:
152
+ st.error("GitHub token not found in secrets or env var 'github_token_api'")
153
+ return None
154
+ from azure.ai.inference import ChatCompletionsClient
155
+ from azure.ai.inference.models import SystemMessage, UserMessage
156
+ from azure.core.credentials import AzureKeyCredential
157
+ endpoint = "https://models.inference.ai.azure.com"
158
+ model_name = "gpt-4o"
159
+ client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token))
160
+ return {
161
+ "client": client,
162
+ "model_name": model_name,
163
+ "endpoint": endpoint,
164
+ "last_loaded": datetime.now().isoformat(),
165
+ "category": MODEL_CONFIGS[model_name]["category"],
166
+ "api_version": MODEL_CONFIGS[model_name].get("api_version")
167
+ }
168
+ except Exception as e:
169
+ st.error(f"Error initializing AI model: {e}")
170
+ logger.error(str(e))
171
  return None
 
 
 
172
 
 
 
 
173
  def suggest_code_completion(code_snippet, models):
 
174
  if not models:
175
  st.error("AI models not initialized")
176
  return None
177
+ try:
178
+ prompt = f"""Write a complete Manim animation scene based on this code or idea:
179
  {code_snippet}
180
 
181
+ The code should be a complete, working Manim animation that includes:
182
+ - Proper Scene class definition
183
+ - Constructor with animations
184
+ - Proper use of self.play() for animations
185
+ - Proper wait times between animations
186
+
187
+ Here's the complete Manim code:
188
  """
189
+ from openai import OpenAI
190
+ token = get_secret("github_token_api")
191
+ client = OpenAI(base_url="https://models.github.ai/inference", api_key=token)
192
+ messages = [{"role": "system", "content": "You are an expert in Manim animations."},
193
+ {"role": "user", "content": prompt}]
194
+ config = MODEL_CONFIGS.get(models["model_name"], MODEL_CONFIGS["default"])
195
+ params = {"messages": messages, "model": models["model_name"], config["param_name"]: config[config["param_name"]]}
196
+ response = client.chat.completions.create(**params)
197
+ content = response.choices[0].message.content
198
+ if "```python" in content:
199
+ content = content.split("```python")[1].split("```")[0]
200
+ elif "```" in content:
201
+ content = content.split("```")[1].split("```")[0]
202
+ if "Scene" not in content:
203
+ content = f"from manim import *\n\nclass MyScene(Scene):\n def construct(self):\n {content}"
204
+ return content
205
+ except Exception as e:
206
+ st.error(f"Error generating code: {e}")
207
+ logger.error(traceback.format_exc())
208
+ return None
209
+
210
+ QUALITY_PRESETS = {
211
+ "480p": {"resolution": "480p", "fps": "30"},
212
+ "720p": {"resolution": "720p", "fps": "30"},
213
+ "1080p": {"resolution": "1080p", "fps": "60"},
214
+ "4K": {"resolution": "2160p", "fps": "60"},
215
+ "8K": {"resolution": "4320p", "fps": "60"}
216
+ }
217
+
218
+ ANIMATION_SPEEDS = {
219
+ "Slow": 0.5,
220
+ "Normal": 1.0,
221
+ "Fast": 2.0,
222
+ "Very Fast": 3.0
223
+ }
224
+
225
+ EXPORT_FORMATS = {
226
+ "MP4 Video": "mp4",
227
+ "GIF Animation": "gif",
228
+ "WebM Video": "webm",
229
+ "PNG Image Sequence": "png_sequence",
230
+ "SVG Image": "svg"
231
+ }
232
 
 
 
 
233
  def highlight_code(code):
234
+ formatter = HtmlFormatter(style='monokai')
235
+ highlighted = highlight(code, PythonLexer(), formatter)
236
+ return highlighted, formatter.get_style_defs()
237
 
 
 
 
238
  def generate_manim_preview(python_code):
239
+ scene_objects = []
240
+ if "Circle" in python_code: scene_objects.append("circle")
241
+ if "Square" in python_code: scene_objects.append("square")
242
+ if "MathTex" in python_code or "Tex" in python_code: scene_objects.append("equation")
243
+ if "Text" in python_code: scene_objects.append("text")
244
+ if "Axes" in python_code: scene_objects.append("graph")
245
+ if "ThreeDScene" in python_code or "ThreeDAxes" in python_code: scene_objects.append("3D scene")
246
+ if "Sphere" in python_code: scene_objects.append("sphere")
247
+ if "Cube" in python_code: scene_objects.append("cube")
248
+ icons = {"circle":"β­•","square":"πŸ”²","equation":"πŸ“Š","text":"πŸ“","graph":"πŸ“ˆ","3D scene":"🧊","sphere":"🌐","cube":"🧊"}
249
+ icon_html = "".join(f'<span style="font-size:2rem; margin:0.3rem;">{icons[o]}</span>' for o in scene_objects)
250
+ preview_html = f"""
251
+ <div style="background-color:#000; width:100%; height:220px; border-radius:10px; display:flex; flex-direction:column; align-items:center; justify-content:center; color:white; text-align:center;">
252
+ <h3>Animation Preview</h3>
253
+ <div>{icon_html if icon_html else '<span style="font-size:2rem;">🎬</span>'}</div>
254
+ <p>Scene contains: {', '.join(scene_objects) if scene_objects else 'No detected objects'}</p>
255
+ <p style="font-size:0.8rem; opacity:0.7;">Full rendering required for accurate preview</p>
256
  </div>
257
  """
258
+ return preview_html
259
+
260
+ def render_latex_preview(latex):
261
+ if not latex:
 
 
 
 
262
  return """
263
+ <div style="background:#f8f9fa; width:100%; height:100px; border-radius:5px; display:flex; align-items:center; justify-content:center; color:#6c757d;">
264
+ Enter LaTeX formula to see preview
265
+ </div>
266
+ """
267
  return f"""
268
+ <div style="background:#202124; width:100%; padding:20px; border-radius:5px; color:white; text-align:center;">
269
+ <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
270
+ <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
271
+ <div><h3>LaTeX Preview</h3><div id="math-preview">$$ {latex} $$</div><p style="font-size:0.8rem; opacity:0.7;">Use MathTex(r"{latex}") in Manim</p></div>
272
+ </div>
273
+ """
274
+
275
+ def extract_scene_class_name(python_code):
276
+ match = re.search(r'class\s+(\w+)\s*\([^)]*Scene[^)]*\)', python_code)
277
+ return match.group(1) if match else "MyScene"
278
+
279
  def prepare_audio_for_manim(audio_file, target_dir):
280
+ audio_dir = os.path.join(target_dir, "audio")
281
+ os.makedirs(audio_dir, exist_ok=True)
282
  filename = f"audio_{int(time.time())}.mp3"
283
+ path = os.path.join(audio_dir, filename)
284
+ with open(path, "wb") as f: f.write(audio_file.getvalue())
285
+ return path
286
+
287
+ def mp4_to_gif(mp4, out, fps=15):
288
+ cmd = ["ffmpeg","-i",mp4,"-vf",f"fps={fps},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse","-loop","0",out]
289
+ res = subprocess.run(cmd,capture_output=True,text=True)
290
+ return out if res.returncode==0 else None
291
+
292
+ def generate_manim_video(code, fmt, quality, speed, audio_path=None):
293
+ temp_dir = tempfile.mkdtemp(prefix="manim_render_")
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  try:
295
+ scene = extract_scene_class_name(code)
296
+ if audio_path and "with_sound" not in code:
297
+ code = "from manim.scene.scene_file_writer import SceneFileWriter\n" + code
298
+ pat = re.search(f"class {scene}\\(.*?\\):", code)
299
+ if pat:
300
+ decor = f"@with_sound(\"{audio_path}\")\n"
301
+ code = code[:pat.start()] + decor + code[pat.start():]
302
+ path_py = os.path.join(temp_dir, "scene.py")
303
+ with open(path_py, "w", encoding="utf-8") as f: f.write(code)
304
+ qmap = {"480p":"-ql","720p":"-qm","1080p":"-qh","4K":"-qk","8K":"-qp"}
305
+ qflag = qmap.get(quality,"-qm")
306
+ if fmt=="png_sequence":
307
+ farg="--format=png"; extra=["--save_pngs"]
308
+ elif fmt=="svg":
309
+ farg="--format=svg"; extra=[]
310
+ else:
311
+ farg=f"--format={fmt}"; extra=[]
312
+ cmd = ["manim", path_py, scene, qflag, farg] + extra
313
  proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
314
+ output=[]
315
+ out_path=None; mp4_path=None
316
  while True:
317
  line = proc.stdout.readline()
318
+ if not line and proc.poll() is not None: break
 
319
  output.append(line)
320
+ if "%" in line:
321
+ try:
322
+ p=float(line.split("%")[0].strip().split()[-1]);
323
+ except: pass
324
+ if "File ready at" in line:
325
+ chunk = line.split("File ready at")[-1].strip()
326
+ m=re.search(r'([\'"]?)(.*?\.(mp4|gif|webm|svg))\1',chunk)
327
+ if m:
328
+ out_path=m.group(2)
329
+ if out_path.endswith(".mp4"): mp4_path=out_path
330
  proc.wait()
331
+ time.sleep(2)
332
+ data=None
333
+ if fmt=="gif" and (not out_path or not os.path.exists(out_path)) and mp4_path:
334
+ gif=os.path.join(temp_dir,f"{scene}_converted.gif")
335
+ if mp4_to_gif(mp4_path,gif): out_path=gif
336
+ if fmt=="png_sequence":
337
+ dirs=[os.path.join(temp_dir,"media","images",scene,"Animations")]
338
+ pngs=[]
339
+ for d in dirs:
340
+ if os.path.isdir(d):
341
+ pngs+= [os.path.join(d,f) for f in os.listdir(d) if f.endswith(".png")]
342
+ if pngs:
343
+ zipf=os.path.join(temp_dir,f"{scene}_pngs.zip")
344
+ with zipfile.ZipFile(zipf,"w") as z:
345
+ for p in pngs: z.write(p,os.path.basename(p))
346
+ data=open(zipf,"rb").read()
347
+ elif out_path and os.path.exists(out_path):
348
+ data=open(out_path,"rb").read()
349
+ else:
350
+ # fallback search
351
+ files=[]
352
+ for root,_,fs in os.walk(temp_dir):
353
+ for f in fs:
354
+ if f.endswith(f".{fmt}") and "partial" not in f:
355
+ files.append(os.path.join(root,f))
356
+ if files:
357
+ latest=max(files,key=os.path.getctime)
358
+ data=open(latest,"rb").read()
359
+ if fmt=="gif" and latest.endswith(".mp4"):
360
+ gif=os.path.join(temp_dir,f"{scene}_converted.gif")
361
+ if mp4_to_gif(latest,gif): data=open(gif,"rb").read()
362
+ if data:
363
+ size=len(data)/(1024*1024)
364
+ return data, f"βœ… Animation generated successfully! ({size:.1f} MB)"
365
+ else:
366
+ return None, "❌ Error: No output files generated.\n" + "".join(output)[:500]
367
+ except Exception as e:
368
+ logger.error(traceback.format_exc())
369
+ return None, f"❌ Error: {e}"
370
  finally:
371
+ try: shutil.rmtree(temp_dir)
372
+ except: pass
373
 
 
 
 
374
  def detect_input_calls(code):
375
+ calls=[]
376
+ for i,line in enumerate(code.splitlines(),1):
377
+ if 'input(' in line and not line.strip().startswith('#'):
378
+ m=re.search(r'input\([\'"](.+?)[\'"]\)',line)
379
+ prompt=m.group(1) if m else f"Input for line {i}"
380
+ calls.append({"line":i,"prompt":prompt})
 
381
  return calls
382
 
 
 
 
383
  def run_python_script(code, inputs=None, timeout=60):
384
+ result={"stdout":"","stderr":"","exception":None,"plots":[],"dataframes":[],"execution_time":0}
 
 
 
385
  if inputs:
386
+ inject = f"""
387
+ __INPUT_VALUES={inputs}
388
+ __INPUT_INDEX=0
389
+ def input(prompt=''):
390
+ global __INPUT_INDEX
391
+ print(prompt,end='')
392
+ if __INPUT_INDEX<len(__INPUT_VALUES):
393
+ v=__INPUT_VALUES[__INPUT_INDEX]; __INPUT_INDEX+=1
394
+ print(v); return v
395
+ print(); return ''
396
+ """
397
+ code = inject + code
398
+ with tempfile.TemporaryDirectory() as td:
399
+ plot_dir=os.path.join(td,'plots'); os.makedirs(plot_dir,exist_ok=True)
400
+ stdout_f=os.path.join(td,'stdout.txt')
401
+ stderr_f=os.path.join(td,'stderr.txt')
402
+ if 'plt' in code or 'matplotlib' in code:
403
+ if 'import matplotlib.pyplot as plt' not in code:
404
+ code="import matplotlib.pyplot as plt\n"+code
405
+ save_plots=f"""
406
+ import matplotlib.pyplot as plt,os
407
+ for i,num in enumerate(plt.get_fignums()):
408
+ plt.figure(num).savefig(os.path.join(r'{plot_dir}','plot_{{i}}.png'))
409
+ """
410
+ code+=save_plots
411
+ if 'pd.' in code or 'import pandas' in code:
412
+ if 'import pandas as pd' not in code:
413
+ code="import pandas as pd\n"+code
414
+ dfcap=f"""
415
+ import pandas as pd, json,os
416
+ for name,val in globals().items():
417
+ if isinstance(val,pd.DataFrame):
418
+ info={{"name":name,"shape":val.shape,"columns":list(val.columns),"preview":val.head().to_html()}}
419
+ open(os.path.join(r'{td}',f'df_{{name}}.json'),'w').write(json.dumps(info))
420
+ """
421
+ code+=dfcap
422
+ script=os.path.join(td,'script.py')
423
+ open(script,'w').write(code)
424
+ start=time.time()
425
+ try:
426
+ with open(stdout_f,'w') as so, open(stderr_f,'w') as se:
427
+ p=subprocess.Popen([sys.executable,script],stdout=so,stderr=se,cwd=td)
428
+ p.wait(timeout=timeout)
429
+ except subprocess.TimeoutExpired:
430
+ p.kill()
431
+ result["stderr"]+="\nTimeout"
432
+ result["exception"]="Timeout"
433
+ return result
434
+ result["execution_time"]=time.time()-start
435
+ result["stdout"]=open(stdout_f).read()
436
+ result["stderr"]=open(stderr_f).read()
437
+ for f in sorted(os.listdir(plot_dir)):
438
+ if f.endswith('.png'):
439
+ result["plots"].append(open(os.path.join(plot_dir,f),'rb').read())
440
+ for f in os.listdir(td):
441
+ if f.startswith('df_') and f.endswith('.json'):
442
+ result["dataframes"].append(json.load(open(os.path.join(td,f))))
443
  return result
444
 
445
+ def display_python_script_results(result):
446
+ if not result: return
447
+ st.info(f"Execution completed in {result['execution_time']:.2f}s")
448
+ if result["exception"]:
449
+ st.error(f"Exception: {result['exception']}")
450
+ if result["stderr"]:
 
 
451
  st.error("Errors:")
452
+ st.code(result["stderr"], language="bash")
453
+ if result["plots"]:
454
+ st.markdown("### Plots")
455
+ cols=st.columns(min(3,len(result["plots"])))
456
+ for i,p in enumerate(result["plots"]):
457
+ cols[i%len(cols)].image(p,use_column_width=True)
458
+ if result["dataframes"]:
459
+ st.markdown("### DataFrames")
460
+ for df in result["dataframes"]:
461
+ with st.expander(f"{df['name']} {df['shape']}"):
462
+ st.write(pd.read_html(df["preview"])[0])
463
+ if result["stdout"]:
464
+ st.markdown("### Stdout")
465
+ st.code(result["stdout"], language="bash")
466
+
467
+ def parse_animation_steps(code):
468
+ steps=[]
469
+ plays=re.findall(r'self\.play\((.*?)\)',code,re.DOTALL)
470
+ waits=re.findall(r'self\.wait\((.*?)\)',code,re.DOTALL)
471
+ cum=0
472
+ for i,pc in enumerate(plays):
473
+ anims=[a.strip() for a in pc.split(',')]
474
+ dur=1.0
475
+ if i<len(waits):
476
+ m=re.search(r'(\d+\.?\d*)',waits[i])
477
+ if m: dur=float(m.group(1))
478
+ steps.append({"id":i+1,"type":"play","animations":anims,"duration":dur,"start_time":cum,"code":f"self.play({pc})"})
479
+ cum+=dur
480
  return steps
481
 
482
+ def generate_code_from_timeline(steps,orig):
483
+ m=re.search(r'(class\s+\w+\s*\([^)]*\)\s*:.*?def\s+construct\s*\(\s*self\s*\)\s*:)',orig,re.DOTALL)
484
+ if not m: return orig
485
+ header=m.group(1)
486
+ new=[header]
487
+ indent=" "
488
+ for s in sorted(steps,key=lambda x:x["id"]):
489
+ new.append(f"{indent}{s['code']}")
490
+ if s["duration"]>0:
491
+ new.append(f"{indent}self.wait({s['duration']})")
492
+ return "\n".join(new)
493
+
 
 
 
 
 
 
 
494
  def create_timeline_editor(code):
495
+ st.markdown("### 🎞️ Animation Timeline Editor")
496
+ if not code:
497
+ st.warning("Add animation code first")
498
+ return code
499
+ steps=parse_animation_steps(code)
500
  if not steps:
501
+ st.warning("No steps detected")
502
  return code
503
+ df=pd.DataFrame(steps)
504
+ st.markdown("#### Animation Timeline")
505
+ fig=px.timeline(df,x_start="start_time",x_end=df["start_time"]+df["duration"],y="id",color="type",hover_name="animations",labels={"id":"Step","start_time":"Time(s)"})
506
+ fig.update_layout(height=300,xaxis=dict(title="Time(s)",rangeslider_visible=True))
507
+ st.plotly_chart(fig,use_container_width=True)
508
+ sel=st.selectbox("Select Step:",options=df["id"],format_func=lambda x:f"Step {x}")
509
+ new_dur=st.number_input("Duration(s):",min_value=0.1,max_value=10.0,value=float(df[df["id"]==sel]["duration"].iloc[0]),step=0.1)
510
+ action=st.selectbox("Action:",["Update Duration","Move Up","Move Down","Delete"])
 
511
  if st.button("Apply"):
512
+ idx=df[df["id"]==sel].index[0]
513
  if action=="Update Duration":
514
  df.at[idx,"duration"]=new_dur
515
+ elif action=="Move Up" and sel>1:
516
+ j=df[df["id"]==sel-1].index[0]
517
+ df.at[idx,"id"],df.at[j,"id"]=sel-1,sel
518
+ elif action=="Move Down" and sel<len(df):
519
+ j=df[df["id"]==sel+1].index[0]
520
+ df.at[idx,"id"],df.at[j,"id"]=sel+1,sel
521
+ elif action=="Delete":
522
+ df=df[df["id"]!=sel]
523
+ df["id"]=range(1,len(df)+1)
524
+ cum=0
525
+ for i in df.sort_values("id").index:
526
+ df.at[i,"start_time"]=cum; cum+=df.at[i,"duration"]
527
+ new_code=generate_code_from_timeline(df.to_dict('records'),code)
528
+ st.success("Timeline updated, code regenerated.")
 
529
  return new_code
530
  return code
531
 
532
+ def export_to_educational_format(video_data,fmt,title,explanation,temp_dir):
533
+ try:
534
+ if fmt=="powerpoint":
535
+ import pptx
536
+ from pptx.util import Inches
537
+ prs=pptx.Presentation()
538
+ s0=prs.slides.add_slide(prs.slide_layouts[0]); s0.shapes.title.text=title; s0.placeholders[1].text="Created with Manim"
539
+ s1=prs.slides.add_slide(prs.slide_layouts[5]); s1.shapes.title.text="Animation"
540
+ vid_path=os.path.join(temp_dir,"anim.mp4"); open(vid_path,"wb").write(video_data)
541
+ try:
542
+ s1.shapes.add_movie(vid_path,Inches(1),Inches(1.5),Inches(8),Inches(4.5))
543
+ except:
544
+ thumb=os.path.join(temp_dir,"thumb.png")
545
+ subprocess.run(["ffmpeg","-i",vid_path,"-ss","00:00:01","-vframes","1",thumb],check=True)
546
+ s1.shapes.add_picture(thumb,Inches(1),Inches(1.5),Inches(8),Inches(4.5))
547
+ if explanation:
548
+ s2=prs.slides.add_slide(prs.slide_layouts[1]); s2.shapes.title.text="Explanation"; s2.placeholders[1].text=explanation
549
+ out=os.path.join(temp_dir,f"{title.replace(' ','_')}.pptx"); prs.save(out)
550
+ return open(out,"rb").read(),"pptx"
551
+ if fmt=="html":
552
+ html=f"""<!DOCTYPE html><html><head><title>{title}</title>
553
+ <style>body{{font-family:Arial;max-width:800px;margin:auto;padding:20px}}
554
+ .controls button{{margin-right:10px;padding:5px 10px}}</style>
555
+ <script>window.onload=function(){{const v=document.getElementById('anim');
556
+ document.getElementById('play').onclick=()=>v.play();
557
+ document.getElementById('pause').onclick=()=>v.pause();
558
+ document.getElementById('restart').onclick=()=>{{v.currentTime=0;v.play()}};
559
+ }};</script>
560
+ </head><body><h1>{title}</h1>
561
+ <video id="anim" width="100%" controls><source src="data:video/mp4;base64,{base64.b64encode(video_data).decode()}" type="video/mp4"></video>
562
+ <div class="controls"><button id="play">Play</button><button id="pause">Pause</button><button id="restart">Restart</button></div>
563
+ <div class="explanation">{markdown.markdown(explanation)}</div>
564
+ </body></html>"""
565
+ out=os.path.join(temp_dir,f"{title.replace(' ','_')}.html"); open(out,"w").write(html)
566
+ return open(out,"rb").read(),"html"
567
+ if fmt=="sequence":
568
+ from fpdf import FPDF
569
+ vid=os.path.join(temp_dir,"anim.mp4"); open(vid,"wb").write(video_data)
570
+ fr_dir=os.path.join(temp_dir,"frames"); os.makedirs(fr_dir,exist_ok=True)
571
+ subprocess.run(["ffmpeg","-i",vid,"-r","1",os.path.join(fr_dir,"frame_%03d.png")],check=True)
572
+ pdf=FPDF(); pdf.set_auto_page_break(True,15)
573
+ pdf.add_page(); pdf.set_font("Arial","B",20); pdf.cell(190,10,title,0,1,"C")
574
+ segs=explanation.split("##") if explanation else ["No explanation"]
575
+ imgs=sorted([f for f in os.listdir(fr_dir) if f.endswith(".png")])
576
+ for i,img in enumerate(imgs):
577
+ pdf.add_page(); pdf.image(os.path.join(fr_dir,img),10,10,190)
578
+ pdf.ln(100); pdf.set_font("Arial","B",12); pdf.cell(190,10,f"Step {i+1}",0,1)
579
+ pdf.set_font("Arial","",10); pdf.multi_cell(190,5,segs[min(i,len(segs)-1)].strip())
580
+ out=os.path.join(temp_dir,f"{title.replace(' ','_')}_seq.pdf"); pdf.output(out)
581
+ return open(out,"rb").read(),"pdf"
582
+ except Exception as e:
583
+ logger.error(traceback.format_exc())
584
+ return None,None
585
+
 
 
 
 
 
 
586
  def main():
587
+ if 'init' not in st.session_state:
588
+ st.session_state.init=True
589
+ st.session_state.video_data=None
590
+ st.session_state.status=None
591
+ st.session_state.ai_models=None
592
+ st.session_state.generated_code=""
593
+ st.session_state.code=""
594
+ st.session_state.temp_code=""
595
+ st.session_state.editor_key=str(uuid.uuid4())
596
+ st.session_state.packages_checked=False
597
+ st.session_state.latex_formula=""
598
+ st.session_state.audio_path=None
599
+ st.session_state.image_paths=[]
600
+ st.session_state.custom_library_result=""
601
+ st.session_state.python_script="""import matplotlib.pyplot as plt
602
+ import numpy as np
603
+
604
+ # Example: Create a simple plot
605
+ x = np.linspace(0, 10, 100)
606
+ y = np.sin(x)
607
+
608
+ plt.figure(figsize=(10, 6))
609
+ plt.plot(x, y, 'b-', label='sin(x)')
610
+ plt.title('Sine Wave')
611
+ plt.xlabel('x')
612
+ plt.ylabel('sin(x)')
613
+ plt.grid(True)
614
+ plt.legend()
615
+ """
616
+ st.session_state.python_result=None
617
+ st.session_state.settings={"quality":"720p","format_type":"mp4","animation_speed":"Normal"}
618
+ st.session_state.password_entered=False
619
+ st.set_page_config(page_title="Manim Animation Studio", page_icon="🎬", layout="wide")
620
  st.markdown("""
621
  <style>
622
+ /* custom CSS */
 
623
  </style>
624
  """, unsafe_allow_html=True)
625
+ st.markdown("<h1 style='text-align:center;'>🎬 Manim Animation Studio</h1>", unsafe_allow_html=True)
626
+ if not st.session_state.packages_checked:
627
+ if ensure_packages():
628
+ st.session_state.packages_checked=True
629
+ else:
630
+ st.error("Failed to install packages"); st.stop()
631
+ if not ACE_EDITOR_AVAILABLE:
632
+ try:
633
+ from streamlit_ace import st_ace
634
+ ACE_EDITOR_AVAILABLE=True
635
+ except ImportError:
636
+ pass
637
+ tabs = st.tabs(["✨ Editor","πŸ€– AI Assistant","πŸ“š LaTeX Formulas","🎨 Assets","🎞️ Timeline","πŸŽ“ Educational Export","🐍 Python Runner"])
 
 
 
 
 
 
 
 
 
 
 
638
  # --- Editor Tab ---
639
  with tabs[0]:
640
+ col1,col2=st.columns([3,2])
641
+ with col1:
642
+ st.markdown("### πŸ“ Animation Editor")
643
+ mode=st.radio("Input code:",["Type Code","Upload File"],key="editor_mode")
644
+ if mode=="Upload File":
645
+ up=st.file_uploader("Upload .py",type=["py"],key="file_up")
646
+ if up:
647
+ txt=up.getvalue().decode("utf-8")
648
+ st.session_state.code=txt; st.session_state.temp_code=txt
649
+ if ACE_EDITOR_AVAILABLE:
650
+ code_in=st_ace(value=st.session_state.code,language="python",theme="monokai",min_lines=20,key=f"ace_{st.session_state.editor_key}")
 
 
 
651
  else:
652
+ code_in=st.text_area("Code",value=st.session_state.code,height=400,key=f"ta_{st.session_state.editor_key}")
653
+ if code_in!=st.session_state.code:
654
+ st.session_state.code=code_in; st.session_state.temp_code=code_in
655
+ if st.button("πŸš€ Generate Animation",key="gen"):
656
+ if not st.session_state.code.strip():
657
+ st.error("Enter code first")
658
+ else:
659
+ sc=extract_scene_class_name(st.session_state.code)
660
+ if sc=="MyScene" and "class MyScene" not in st.session_state.code:
661
+ df="""\nclass MyScene(Scene):\n def construct(self):\n text=Text("Default Scene"); self.play(Write(text)); self.wait(2)\n"""
662
+ st.session_state.code+=df; st.warning("No scene class; added default")
663
+ with st.spinner("Rendering..."):
664
+ d,s=generate_manim_video(st.session_state.code,st.session_state.settings["format_type"],st.session_state.settings["quality"],ANIMATION_SPEEDS[st.session_state.settings["animation_speed"]],st.session_state.audio_path)
665
+ st.session_state.video_data=d; st.session_state.status=s
666
+ with col2:
667
+ st.markdown("### πŸ–₯️ Preview & Output")
668
+ if st.session_state.code:
669
+ st.markdown("<div style='border:1px solid #ccc;padding:10px;'>",unsafe_allow_html=True)
670
+ st.components.v1.html(generate_manim_preview(st.session_state.code),height=250)
671
+ st.markdown("</div>",unsafe_allow_html=True)
672
+ if st.session_state.video_data:
673
+ fmt=st.session_state.settings["format_type"]
674
+ if fmt=="png_sequence":
675
+ st.download_button("⬇️ Download PNG Zip",st.session_state.video_data,file_name=f"frames_{int(time.time())}.zip")
676
+ elif fmt=="svg":
677
+ try: st.components.v1.html(st.session_state.video_data.decode('utf-8'),height=400)
678
+ except: pass
679
+ st.download_button("⬇️ Download SVG",st.session_state.video_data,file_name=f"anim.svg")
680
+ else:
681
+ st.video(st.session_state.video_data,format=fmt)
682
+ st.download_button(f"⬇️ Download {fmt.upper()}",st.session_state.video_data,file_name=f"anim.{fmt}")
683
+ if st.session_state.status:
684
+ if "❌" in st.session_state.status: st.error(st.session_state.status)
685
+ else: st.success(st.session_state.status)
686
+ # --- AI Assistant Tab ---
687
  with tabs[1]:
688
+ st.markdown("### πŸ€– AI Animation Assistant")
689
+ if check_password():
690
+ if not st.session_state.ai_models:
691
+ st.session_state.ai_models=init_ai_models_direct()
692
+ # Debug & selection & generation (as in original)
693
+ with st.expander("πŸ”§ Debug Connection"):
694
+ if st.button("Test API Connection"):
695
+ with st.spinner("Testing..."):
696
+ try:
697
+ token=get_secret("github_token_api")
698
+ if not token: st.error("Token missing"); st.stop()
699
+ model=st.session_state.ai_models["model_name"]
700
+ from openai import OpenAI
701
+ client=OpenAI(base_url="https://models.github.ai/inference",api_key=token)
702
+ params={"messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"Hi"}],"model":model}
703
+ params[MODEL_CONFIGS[model]["param_name"]]=MODEL_CONFIGS[model][MODEL_CONFIGS[model]["param_name"]]
704
+ resp=client.chat.completions.create(**params)
705
+ if resp and resp.choices:
706
+ st.success("βœ… Connected")
707
+ else: st.error("No response")
708
+ except Exception as e:
709
+ st.error(f"Error: {e}")
710
+ st.markdown("### πŸ€– Model Selection")
711
+ cats={}
712
+ for m,cfg in MODEL_CONFIGS.items():
713
+ if m!="default":
714
+ cats.setdefault(cfg["category"],[]).append(m)
715
+ cat_tabs=st.tabs(sorted(cats.keys()))
716
+ for i,cat in enumerate(sorted(cats.keys())):
717
+ with cat_tabs[i]:
718
+ for m in sorted(cats[cat]):
719
+ cfg=MODEL_CONFIGS[m]
720
+ sel=(m==st.session_state.ai_models["model_name"])
721
+ st.markdown(f"<div style='background:#f8f9fa;padding:10px;border-left:4px solid {'#0d6efd' if sel else '#4F46E5'};margin-bottom:8px;'>"
722
+ f"<h4>{m}</h4><p>Max Tokens: {cfg.get(cfg['param_name'],'?')}</p><p>API Ver: {cfg['api_version'] or 'default'}</p></div>",
723
+ unsafe_allow_html=True)
724
+ if st.button("Select" if not sel else "Selected βœ“",key=f"sel_{m}",disabled=sel):
725
+ st.session_state.ai_models["model_name"]=m
726
+ st.experimental_rerun()
727
+ if st.session_state.ai_models:
728
+ st.info(f"Using model: {st.session_state.ai_models['model_name']}")
729
+
730
+ if st.session_state.ai_models and "client" in st.session_state.ai_models:
731
+ st.markdown("#### Generate Animation from Description")
732
+ ideas=["...","3D sphere to torus","Pythagorean proof","Fourier transform","Neural network propagation","Integration area"]
733
+ sel=st.selectbox("Try idea",ideas)
734
+ prompt=sel if sel!="..." else ""
735
+ inp=st.text_area("Your prompt or code",value=prompt,height=150)
736
+ if st.button("Generate Animation Code"):
737
+ if inp:
738
+ with st.spinner("Generating..."):
739
+ code=suggest_code_completion(inp,st.session_state.ai_models)
740
+ if code:
741
+ st.session_state.generated_code=code
742
+ else: st.error("Failed")
743
+ else: st.warning("Enter prompt")
744
+ if st.session_state.generated_code:
745
+ st.code(st.session_state.generated_code,language="python")
746
+ c1,c2=st.columns(2)
747
+ if c1.button("Use This Code"):
748
+ st.session_state.code=st.session_state.generated_code
749
+ st.experimental_rerun()
750
+ if c2.button("Render Preview"):
751
+ vd,stt=generate_manim_video(st.session_state.generated_code,"mp4","480p",1.0)
752
+ if vd: st.video(vd); st.download_button("Download Preview",vd,file_name="preview.mp4")
753
+ else: st.error(f"Error: {stt}")
754
+ else:
755
+ st.info("Enter password to access AI")
756
+
757
+ # --- LaTeX Formulas Tab ---
758
  with tabs[2]:
759
+ st.markdown("### πŸ“š LaTeX Formula Builder")
760
+ c1,c2=st.columns([3,2])
761
+ with c1:
762
+ lt=st.text_area("LaTeX Formula",value=st.session_state.latex_formula,placeholder=r"e^{i\pi}+1=0",height=100)
763
+ st.session_state.latex_formula=lt
764
+ categories={
765
+ "Basic Math":[{"name":"Fraction","latex":r"\frac{a}{b}"},...],
766
+ # fill in as original categories...
767
+ }
768
+ tab_cats=st.tabs(list(categories.keys()))
769
+ for i,(cat,forms) in enumerate(categories.items()):
770
+ with tab_cats[i]:
771
+ for f in forms:
772
+ if st.button(f["name"],key=f"lt_{f['name']}"):
773
+ st.session_state.latex_formula=f["latex"]; st.experimental_rerun()
774
+ if lt:
775
+ snippet=f"""
776
+ formula=MathTex(r"{lt}")
777
+ self.play(Write(formula))
778
+ self.wait(2)
779
+ """
780
+ st.code(snippet,language="python")
781
+ if st.button("Insert into Editor"):
782
+ if "def construct" in st.session_state.code:
783
+ lines=st.session_state.code.split("\n")
784
+ idx=[i for i,l in enumerate(lines) if "def construct" in l][0]
785
+ indent=re.match(r"(\s*)",lines[idx+1]).group(1) if idx+1<len(lines) else " "
786
+ insert="\n".join(indent+line for line in snippet.strip().split("\n"))
787
+ lines.insert(idx+2,insert)
788
+ st.session_state.code="\n".join(lines)
789
+ st.experimental_rerun()
790
+ else:
791
+ base=f"""from manim import *\n\nclass LatexScene(Scene):\n def construct(self):\n {snippet.strip().replace('\n','\n ')}\n"""
792
+ st.session_state.code=base; st.experimental_rerun()
793
+ with c2:
794
+ st.components.v1.html(render_latex_preview(st.session_state.latex_formula),height=300)
795
 
796
  # --- Assets Tab ---
797
  with tabs[3]:
798
+ st.markdown("### 🎨 Asset Management")
799
+ a1,a2=st.columns(2)
800
+ with a1:
801
+ imgs=st.file_uploader("Upload Images",type=["png","jpg","jpeg","svg"],accept_multiple_files=True)
802
+ if imgs:
803
+ d="manim_assets/images";os.makedirs(d,exist_ok=True)
804
+ for up in imgs:
805
+ ext=up.name.split(".")[-1]
806
+ fn=f"img_{int(time.time())}_{uuid.uuid4().hex[:8]}.{ext}"
807
+ p=os.path.join(d,fn)
808
+ open(p,"wb").write(up.getvalue())
809
+ st.session_state.image_paths.append({"name":up.name,"path":p})
810
+ st.success("Images uploaded")
811
+ if st.session_state.image_paths:
812
+ for ip in st.session_state.image_paths:
813
+ st.image(Image.open(ip["path"]),caption=ip["name"],width=100)
814
+ if st.button(f"Use {ip['name']}",key=f"use_img_{ip['name']}"):
815
+ code=f"""
816
+ image=ImageMobject(r"{ip['path']}")
817
+ self.play(FadeIn(image))
818
+ self.wait(1)
819
+ """
820
+ st.session_state.code+=code; st.experimental_rerun()
821
+ with a2:
822
+ au=st.file_uploader("Upload Audio",type=["mp3","wav","ogg"])
823
+ if au:
824
+ d="manim_assets/audio";os.makedirs(d,exist_ok=True)
825
+ fn=f"audio_{int(time.time())}.{au.name.split('.')[-1]}"
826
+ p=os.path.join(d,fn)
827
+ open(p,"wb").write(au.getvalue())
828
+ st.session_state.audio_path=p
829
+ st.audio(au)
830
+ st.success("Audio uploaded")
831
 
832
  # --- Timeline Tab ---
833
  with tabs[4]:
834
+ updated=create_timeline_editor(st.session_state.code)
835
+ if updated!=st.session_state.code:
836
+ st.session_state.code=updated; st.experimental_rerun()
 
837
 
838
+ # --- Educational Export Tab ---
839
  with tabs[5]:
840
+ st.markdown("### πŸŽ“ Educational Export")
841
+ if not st.session_state.video_data:
842
+ st.warning("Generate animation first")
843
  else:
844
+ title=st.text_input("Animation Title","Manim Animation")
845
+ expl=st.text_area("Explanation",height=150)
846
+ fmt=st.selectbox("Format",["PowerPoint Presentation","Interactive HTML","Explanation Sequence PDF"])
847
+ if st.button("Export"):
848
+ mp={"PowerPoint Presentation":"powerpoint","Interactive HTML":"html","Explanation Sequence PDF":"sequence"}
849
+ data,typ=export_to_educational_format(st.session_state.video_data,mp[fmt],title,expl,tempfile.mkdtemp())
 
 
850
  if data:
851
+ ext={"powerpoint":"pptx","html":"html","sequence":"pdf"}[typ]
852
+ st.download_button("Download",data,file_name=f"{title.replace(' ','_')}.{ext}")
853
+ else: st.error("Export failed")
854
 
855
+ # --- Python Runner Tab ---
856
  with tabs[6]:
857
+ st.markdown("### 🐍 Python Script Runner")
858
+ examples={
859
+ "Basic Plot":st.session_state.python_script,
860
+ "Input Example":"""# input demo...""",
861
+ "DataFrame":"""import pandas as pd...""",
862
+ }
863
+ choice=st.selectbox("Examples",list(examples.keys()))
864
+ code=examples[choice] if choice in examples else st.session_state.python_script
865
+ if ACE_EDITOR_AVAILABLE:
866
+ code_in=st_ace(value=code,language="python",theme="monokai",min_lines=15,key=f"pyace_{st.session_state.editor_key}")
867
+ else:
868
+ code_in=st.text_area("Code",value=code,height=400,key=f"pyta_{st.session_state.editor_key}")
869
+ st.session_state.python_script=code_in
870
+ calls=detect_input_calls(code_in)
871
+ vals=[]
872
  if calls:
873
+ st.markdown("Provide inputs:")
874
+ for i,c in enumerate(calls):
875
+ v=st.text_input(c["prompt"],key=f"inp_{i}")
876
+ vals.append(v)
877
+ timeout=st.slider("Timeout",5,300,30)
878
+ if st.button("▢️ Run"):
879
+ res=run_python_script(code_in,vals,timeout)
880
+ st.session_state.python_result=res
881
+ if st.session_state.python_result:
882
+ display_python_script_results(st.session_state.python_result)
883
+ if st.session_state.python_result["plots"]:
884
+ st.markdown("Add plot to animation:")
885
+ for i,p in enumerate(st.session_state.python_result["plots"]):
886
+ st.image(p);
887
+ if st.button(f"Use Plot {i+1}",key=f"use_plot_{i}"):
888
+ path=tempfile.NamedTemporaryFile(delete=False,suffix=".png").name
889
+ open(path,"wb").write(p)
890
+ code=f"""
891
+ plot_img=ImageMobject(r"{path}")
892
+ self.play(FadeIn(plot_img))
893
+ self.wait(1)
894
+ """
895
+ st.session_state.code+=code; st.experimental_rerun()
896
 
897
  if __name__ == "__main__":
898
  main()