euler314 commited on
Commit
97cd083
Β·
verified Β·
1 Parent(s): 6181a36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +380 -656
app.py CHANGED
@@ -52,7 +52,7 @@ MODEL_CONFIGS = {
52
  "gpt-4.1": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
53
  "gpt-4.1-mini": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
54
  "gpt-4.1-nano": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
55
- "o3-mini": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
56
  "o1": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
57
  "o1-mini": {"max_completion_tokens": 66000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
58
  "o1-preview": {"max_tokens": 33000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
@@ -74,29 +74,33 @@ except ImportError:
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:
@@ -106,7 +110,7 @@ def check_password():
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',
@@ -126,115 +130,69 @@ def ensure_packages():
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")
@@ -242,657 +200,423 @@ def generate_manim_preview(python_code):
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()
 
52
  "gpt-4.1": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
53
  "gpt-4.1-mini": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
54
  "gpt-4.1-nano": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
55
+ "o4-mini": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
56
  "o1": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
57
  "o1-mini": {"max_completion_tokens": 66000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI", "warning": None},
58
  "o1-preview": {"max_tokens": 33000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None},
 
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 = {
78
+ "messages": messages,
79
+ "model": model_name
80
+ }
81
  token_param = config["param_name"]
82
+ api_params[token_param] = config.get(token_param)
 
83
  return api_params, config
84
 
85
+ def get_secret(env_var):
86
+ """Retrieve a secret from environment variables"""
87
+ val = os.environ.get(env_var)
88
+ if not val:
89
+ logger.warning(f"Secret '{env_var}' not found")
90
+ return val
91
 
92
  def check_password():
93
+ """Verify password entered against secret"""
94
+ correct = get_secret("password")
95
+ if not correct:
96
+ st.error("Admin password not configured")
97
  return False
98
  if "password_entered" not in st.session_state:
99
  st.session_state.password_entered = False
100
  if not st.session_state.password_entered:
101
  pwd = st.text_input("Enter password to access AI features", type="password")
102
  if pwd:
103
+ if pwd == correct:
104
  st.session_state.password_entered = True
105
  return True
106
  else:
 
110
  return True
111
 
112
  def ensure_packages():
113
+ required = {
114
  'manim': '0.17.3',
115
  'Pillow': '9.0.0',
116
  'numpy': '1.22.0',
 
130
  'huggingface_hub': '0.16.0',
131
  }
132
  missing = {}
133
+ for pkg, ver in required.items():
134
  try:
135
  __import__(pkg if pkg != 'Pillow' else 'PIL')
136
  except ImportError:
137
  missing[pkg] = ver
138
  if not missing:
139
  return True
140
+ bar = st.progress(0)
141
+ txt = st.empty()
142
  for i, (pkg, ver) in enumerate(missing.items()):
143
+ bar.progress(i / len(missing))
144
+ txt.text(f"Installing {pkg}...")
145
  res = subprocess.run([sys.executable, "-m", "pip", "install", f"{pkg}>={ver}"], capture_output=True, text=True)
146
  if res.returncode != 0:
147
+ st.error(f"Failed to install {pkg}")
148
  return False
149
+ bar.progress(1.0)
150
+ txt.empty()
151
  return True
152
 
153
+ def install_custom_packages(pkgs):
154
+ if not pkgs.strip():
155
+ return True, "No packages specified"
156
+ parts = [p.strip() for p in pkgs.split(",") if p.strip()]
157
+ if not parts:
158
+ return True, "No valid packages"
159
+ sidebar_txt = st.sidebar.empty()
160
+ bar = st.sidebar.progress(0)
161
+ results = []
162
+ success = True
163
+ for i, p in enumerate(parts):
164
+ bar.progress(i / len(parts))
165
+ sidebar_txt.text(f"Installing {p}...")
166
+ res = subprocess.run([sys.executable, "-m", "pip", "install", p], capture_output=True, text=True)
167
+ if res.returncode != 0:
168
+ results.append(f"Failed {p}: {res.stderr}")
169
+ success = False
170
+ else:
171
+ results.append(f"Installed {p}")
172
+ bar.progress(1.0)
173
+ sidebar_txt.empty()
174
+ return success, "\n".join(results)
175
+
176
  @st.cache_resource(ttl=3600)
177
  def init_ai_models_direct():
178
+ token = get_secret("github_token_api")
179
+ if not token:
180
+ st.error("API token not configured")
181
+ return None
182
  try:
 
 
 
 
183
  from azure.ai.inference import ChatCompletionsClient
184
+ from azure.ai.inference.models import UserMessage
185
  from azure.core.credentials import AzureKeyCredential
186
+ client = ChatCompletionsClient(
187
+ endpoint="https://models.inference.ai.azure.com",
188
+ credential=AzureKeyCredential(token)
189
+ )
190
+ return {"client": client, "model_name": "gpt-4o", "last_loaded": datetime.now().isoformat()}
191
+ except ImportError as e:
192
+ st.error("Azure AI SDK not installed")
 
 
 
 
 
 
193
  logger.error(str(e))
194
  return None
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  def generate_manim_preview(python_code):
197
  scene_objects = []
198
  if "Circle" in python_code: scene_objects.append("circle")
 
200
  if "MathTex" in python_code or "Tex" in python_code: scene_objects.append("equation")
201
  if "Text" in python_code: scene_objects.append("text")
202
  if "Axes" in python_code: scene_objects.append("graph")
203
+ icons = {"circle":"β­•","square":"πŸ”²","equation":"πŸ“Š","text":"πŸ“","graph":"πŸ“ˆ"}
204
+ icon_html = "".join(f'<span style="font-size:2rem;margin:0.3rem;">{icons[o]}</span>' for o in scene_objects if o in icons)
205
+ html = f"""
206
+ <div style="background:#000;color:#fff;padding:1rem;border-radius:10px;text-align:center;">
207
+ <h3>Animation Preview</h3>
208
+ <div>{icon_html or '🎬'}</div>
209
+ <p>Contains: {', '.join(scene_objects) or 'none'}</p>
210
+ <p style="opacity:0.7;">Full rendering required for accurate preview</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </div>
212
  """
213
+ return html
214
 
215
  def extract_scene_class_name(python_code):
216
+ names = re.findall(r'class\s+(\w+)\s*\([^)]*Scene', python_code)
217
+ return names[0] if names else "MyScene"
 
 
 
 
 
 
 
 
218
 
219
  def mp4_to_gif(mp4, out, fps=15):
220
+ cmd = [
221
+ "ffmpeg","-i",mp4,
222
+ "-vf",f"fps={fps},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
223
+ "-loop","0",out
224
+ ]
225
+ r = subprocess.run(cmd, capture_output=True, text=True)
226
+ return out if r.returncode==0 else None
227
+
228
+ def generate_manim_video(code, format_type, quality_preset, speed=1.0, audio_path=None):
229
+ temp_dir = tempfile.mkdtemp(prefix="manim_")
230
+ scene_class = extract_scene_class_name(code)
231
+ file_py = os.path.join(temp_dir, "scene.py")
232
+ with open(file_py, "w", encoding="utf-8") as f:
233
+ f.write(code)
234
+ quality_flags = {"480p":"-ql","720p":"-qm","1080p":"-qh","4K":"-qk","8K":"-qp"}
235
+ qf = quality_flags.get(quality_preset, "-qm")
236
+ fmt_arg = f"--format={format_type}"
237
+ cmd = ["manim", file_py, scene_class, qf, fmt_arg]
238
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
239
+ output = []
240
+ out_path = None
241
+ mp4_path = None
242
+ bar = st.empty()
243
+ log = st.empty()
244
+ while True:
245
+ line = proc.stdout.readline()
246
+ if not line and proc.poll() is not None:
247
+ break
248
+ if line:
 
 
249
  output.append(line)
250
+ log.code("".join(output[-10:]))
 
 
 
251
  if "File ready at" in line:
252
+ m = re.search(r'([\'"])?(.+?\.(?:mp4|gif|webm|svg))\1', line)
253
+ if m:
254
+ out_path = m.group(2)
255
+ if out_path.endswith(".mp4"):
256
+ mp4_path = out_path
257
+ proc.wait()
258
+ time.sleep(1)
259
+ data = None
260
+ if format_type=="gif" and (not out_path or not os.path.exists(out_path)) and mp4_path and os.path.exists(mp4_path):
261
+ gif = os.path.join(temp_dir, scene_class+"_conv.gif")
262
+ conv = mp4_to_gif(mp4_path, gif)
263
+ if conv and os.path.exists(conv):
264
+ out_path = conv
265
+ if out_path and os.path.exists(out_path):
266
+ with open(out_path,"rb") as f: data = f.read()
267
+ shutil.rmtree(temp_dir)
268
+ if data:
269
+ return data, f"βœ… Generated successfully ({len(data)/(1024*1024):.1f} MB)"
270
+ else:
271
+ return None, "❌ No output generated. Check logs."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  def detect_input_calls(code):
274
+ calls = []
275
+ for i, line in enumerate(code.split("\n"),1):
276
+ if "input(" in line and not line.strip().startswith("#"):
277
+ m = re.search(r'input\(["\'](.+?)["\']\)', line)
278
+ prompt = m.group(1) if m else f"Input at line {i}"
279
  calls.append({"line":i,"prompt":prompt})
280
  return calls
281
 
282
  def run_python_script(code, inputs=None, timeout=60):
283
+ result = {"stdout":"","stderr":"","exception":None,"plots":[],"dataframes":[],"execution_time":0}
284
+ mod = ""
285
  if inputs:
286
+ mod = f"""
287
+ __INPUTS={inputs}
288
+ __IDX=0
289
  def input(prompt=''):
290
+ global __IDX
291
  print(prompt,end='')
292
+ if __IDX<len(__INPUTS):
293
+ val=__INPUTS[__IDX]; __IDX+=1
294
+ print(val)
295
+ return val
296
+ print()
297
+ return ''
298
  """
299
+ code_full = mod + code
300
  with tempfile.TemporaryDirectory() as td:
301
+ script = os.path.join(td,"script.py")
302
+ with open(script,"w") as f: f.write(code_full)
303
+ outf = os.path.join(td,"out.txt")
304
+ errf = os.path.join(td,"err.txt")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  start=time.time()
306
  try:
307
+ with open(outf,"w") as o, open(errf,"w") as e:
308
+ proc=subprocess.Popen([sys.executable, script], stdout=o, stderr=e, cwd=td)
309
+ proc.wait(timeout=timeout)
310
  except subprocess.TimeoutExpired:
311
+ proc.kill()
312
+ result["stderr"] += f"\nTimed out after {timeout}s"
313
+ result["exception"] = "Timeout"
 
314
  result["execution_time"]=time.time()-start
315
+ result["stdout"]=open(outf).read()
316
+ result["stderr"]+=open(errf).read()
 
 
 
 
 
 
317
  return result
318
 
319
+ def display_python_script_results(res):
320
+ st.info(f"Completed in {res['execution_time']:.2f}s")
321
+ if res["exception"]:
322
+ st.error(f"Exception: {res['exception']}")
323
+ if res["stderr"]:
 
324
  st.error("Errors:")
325
+ st.code(res["stderr"], language="bash")
326
+ if res["plots"]:
327
  st.markdown("### Plots")
328
+ cols = st.columns(min(3,len(res["plots"])))
329
+ for i,p in enumerate(res["plots"]):
330
  cols[i%len(cols)].image(p,use_column_width=True)
331
+ if res["dataframes"]:
332
  st.markdown("### DataFrames")
333
+ for df in res["dataframes"]:
334
+ with st.expander(f"{df['name']} ({df['shape'][0]}Γ—{df['shape'][1]})"):
335
+ st.markdown(df["preview_html"], unsafe_allow_html=True)
336
+ if res["stdout"]:
337
+ st.markdown("### Output")
338
+ st.code(res["stdout"], language="bash")
339
+
340
+ # Main app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  def main():
342
  if 'init' not in st.session_state:
343
+ st.session_state.update({
344
+ 'init':True, 'video_data':None, 'status':None, 'ai_models':None,
345
+ 'generated_code':"", 'code':"", 'temp_code':"", 'editor_key':str(uuid.uuid4()),
346
+ 'packages_checked':False, 'latex_formula':"", 'audio_path':None,
347
+ 'image_paths':[], 'custom_library_result':"", 'python_script':"",
348
+ 'python_result':None, 'active_tab':0,
349
+ 'settings':{"quality":"720p","format_type":"mp4","animation_speed":"Normal"},
350
+ 'password_entered':False, 'custom_model':"gpt-4o", 'first_load_complete':False,
351
+ 'pending_tab_switch':None
352
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  st.set_page_config(page_title="Manim Animation Studio", page_icon="🎬", layout="wide")
 
 
 
 
 
 
354
  if not st.session_state.packages_checked:
355
  if ensure_packages():
356
  st.session_state.packages_checked=True
357
  else:
358
+ st.error("Failed to install packages")
359
+ return
360
+
361
+ tab_names=["✨ Editor","πŸ€– AI Assistant","πŸ“š LaTeX Formulas","🎨 Assets","🎞️ Timeline","πŸŽ“ Educational Export","🐍 Python Runner"]
362
+ tabs = st.tabs(tab_names)
363
+
364
+ # Editor Tab
 
 
365
  with tabs[0]:
366
+ col1,col2 = st.columns([3,2])
367
  with col1:
368
  st.markdown("### πŸ“ Animation Editor")
369
+ mode = st.radio("Code Input",["Type Code","Upload File"], key="editor_mode")
370
  if mode=="Upload File":
371
+ up=st.file_uploader("Upload .py file", type=["py"])
372
  if up:
373
+ txt=up.getvalue().decode()
374
+ if txt.strip():
375
+ st.session_state.code=txt
376
+ st.session_state.temp_code=txt
377
  if ACE_EDITOR_AVAILABLE:
378
+ st.session_state.temp_code = st_ace(value=st.session_state.code, language="python", theme="monokai", min_lines=20, key=f"ace_{st.session_state.editor_key}")
379
  else:
380
+ st.session_state.temp_code = st.text_area("Code", st.session_state.code, height=400, key=f"ta_{st.session_state.editor_key}")
381
+ if st.session_state.temp_code!=st.session_state.code:
382
+ st.session_state.code=st.session_state.temp_code
383
+ if st.button("πŸš€ Generate Animation"):
384
+ if not st.session_state.code:
385
  st.error("Enter code first")
386
  else:
387
+ vc,stt = generate_manim_video(
388
+ st.session_state.code,
389
+ st.session_state.settings["format_type"],
390
+ st.session_state.settings["quality"],
391
+ {"Slow":0.5,"Normal":1.0,"Fast":2.0,"Very Fast":3.0}[st.session_state.settings["animation_speed"]],
392
+ st.session_state.audio_path
393
+ )
394
+ st.session_state.video_data=vc
395
+ st.session_state.status=stt
396
  with col2:
 
397
  if st.session_state.code:
398
+ st.markdown("<div style='border:1px solid #ccc;padding:1rem;border-radius:8px;'>", unsafe_allow_html=True)
399
+ components.html(generate_manim_preview(st.session_state.code), height=250)
400
+ st.markdown("</div>", unsafe_allow_html=True)
401
  if st.session_state.video_data:
402
  fmt=st.session_state.settings["format_type"]
403
  if fmt=="png_sequence":
404
+ st.download_button("⬇️ Download PNG ZIP", data=st.session_state.video_data, file_name=f"manim_pngs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip", mime="application/zip")
405
  elif fmt=="svg":
406
+ try:
407
+ svg=st.session_state.video_data.decode('utf-8')
408
+ components.html(svg, height=400)
409
+ except:
410
+ st.error("Cannot display SVG")
411
+ st.download_button("⬇️ Download SVG", data=st.session_state.video_data, file_name="animation.svg", mime="image/svg+xml")
412
  else:
413
+ st.video(st.session_state.video_data, format=fmt)
414
+ st.download_button(f"⬇️ Download {fmt.upper()}", st.session_state.video_data, file_name=f"animation.{fmt}", mime=f"video/{fmt}" if fmt!="gif" else "image/gif")
415
  if st.session_state.status:
416
+ if "Error" in st.session_state.status:
417
+ st.error(st.session_state.status)
418
+ else:
419
+ st.success(st.session_state.status)
420
+
421
+ # AI Assistant Tab
422
  with tabs[1]:
423
  st.markdown("### πŸ€– AI Animation Assistant")
424
  if check_password():
425
+ client_data = init_ai_models_direct()
426
+ if client_data:
 
 
427
  if st.button("Test API Connection"):
428
  with st.spinner("Testing..."):
429
+ from azure.ai.inference.models import UserMessage
430
+ api_params,_=prepare_api_params([UserMessage("Hello")], client_data["model_name"])
431
+ resp=client_data["client"].complete(**api_params)
432
+ if resp.choices:
433
+ st.success("βœ… Connection successful!")
434
+ st.session_state.ai_models=client_data
435
+ else:
436
+ st.error("❌ No response")
437
+ if st.session_state.ai_models:
438
+ st.info(f"Using model {st.session_state.ai_models['model_name']}")
439
+ prompt = st.text_area("Describe animation or paste partial code", height=150)
440
+ if st.button("Generate Animation Code"):
441
+ if prompt.strip():
442
+ from azure.ai.inference.models import UserMessage
443
+ api_params,_=prepare_api_params([UserMessage(f"Write a complete Manim scene for:\n{prompt}")], st.session_state.ai_models["model_name"])
444
+ resp=st.session_state.ai_models["client"].complete(**api_params)
445
+ if resp.choices:
446
+ code = resp.choices[0].message.content
447
+ if "```python" in code:
448
+ code=code.split("```python")[1].split("```")[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  st.session_state.generated_code=code
450
+ else:
451
+ st.error("No code generated")
452
+ else:
453
+ st.warning("Enter prompt first")
454
+ if st.session_state.generated_code:
455
+ st.code(st.session_state.generated_code, language="python")
456
+ if st.button("Use This Code"):
457
+ st.session_state.code=st.session_state.generated_code
458
+ st.session_state.temp_code=st.session_state.generated_code
459
+ st.session_state.pending_tab_switch=0
460
+ st.rerun()
 
461
  else:
462
+ st.info("Enter password to access")
463
 
464
+ # LaTeX Formulas Tab
465
  with tabs[2]:
466
  st.markdown("### πŸ“š LaTeX Formula Builder")
467
+ col1,col2=st.columns([3,2])
468
+ with col1:
469
+ latex_input = st.text_area("LaTeX Formula", value=st.session_state.latex_formula, height=100, placeholder=r"e^{i\pi}+1=0")
470
+ st.session_state.latex_formula=latex_input
471
+ if latex_input:
472
+ manim_latex_code = f"""
473
+ # LaTeX formula
474
+ formula = MathTex(r"{latex_input}")
 
 
 
 
 
 
 
 
 
475
  self.play(Write(formula))
476
  self.wait(2)
477
  """
478
+ st.code(manim_latex_code, language="python")
479
  if st.button("Insert into Editor"):
480
+ if st.session_state.code:
481
+ if "def construct(self):" in st.session_state.code:
482
+ lines=st.session_state.code.split("\n")
483
+ idx=-1
484
+ for i,l in enumerate(lines):
485
+ if "def construct(self):" in l:
486
+ idx=i; break
487
+ if idx>=0:
488
+ for j in range(idx+1,len(lines)):
489
+ if lines[j].strip() and not lines[j].strip().startswith("#"):
490
+ indent=re.match(r"(\s*)",lines[j]).group(1)
491
+ new_block="\n".join(indent+ln for ln in manim_latex_code.strip().split("\n"))
492
+ lines.insert(j,new_block)
493
+ break
494
+ else:
495
+ lines.append(" "+ "\n ".join(manim_latex_code.strip().split("\n")))
496
+ st.session_state.code="\n".join(lines)
497
+ st.session_state.temp_code=st.session_state.code
498
+ st.success("Inserted LaTeX into editor")
499
+ st.session_state.pending_tab_switch=0
500
+ st.rerun()
501
+ else:
502
+ st.warning("No construct() found")
503
  else:
504
+ basic_scene = f"""from manim import *
505
+
506
+ class LatexScene(Scene):
507
+ def construct(self):
508
+ # LaTeX formula
509
+ formula = MathTex(r"{latex_input}")
510
+ self.play(Write(formula))
511
+ self.wait(2)
512
+ """
513
+ st.session_state.code=basic_scene
514
+ st.session_state.temp_code=basic_scene
515
+ st.success("Created new scene with LaTeX")
516
+ st.session_state.pending_tab_switch=0
517
+ st.rerun()
518
+ with col2:
519
+ components.html(render_latex_preview(latex_input), height=300)
520
 
521
+ # Assets Tab
522
  with tabs[3]:
523
  st.markdown("### 🎨 Asset Management")
524
+ c1,c2 = st.columns(2)
525
+ with c1:
526
+ imgs=st.file_uploader("Upload Images", type=["png","jpg","jpeg","svg"], accept_multiple_files=True)
527
  if imgs:
528
+ img_dir=os.path.join(os.getcwd(),"manim_assets","images")
529
+ os.makedirs(img_dir, exist_ok=True)
530
  for up in imgs:
531
  ext=up.name.split(".")[-1]
532
+ fname=f"img_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
533
+ path=os.path.join(img_dir,fname)
534
+ with open(path,"wb") as f: f.write(up.getvalue())
535
+ st.session_state.image_paths.append({"name":up.name,"path":path})
 
536
  if st.session_state.image_paths:
537
+ for info in st.session_state.image_paths:
538
+ img=Image.open(info["path"])
539
+ st.image(img, caption=info["name"], width=100)
540
+ if st.button(f"Use {info['name']}"):
541
+ code_snippet=f"""
542
+ # Image asset
543
+ image = ImageMobject(r"{info['path']}")
544
+ image.scale(2)
545
  self.play(FadeIn(image))
546
  self.wait(1)
547
  """
548
+ st.session_state.code+=code_snippet
549
+ st.session_state.temp_code=st.session_state.code
550
+ st.success(f"Added {info['name']} to code")
551
+ st.session_state.pending_tab_switch=0
552
+ st.rerun()
553
+ with c2:
554
+ aud=st.file_uploader("Upload Audio", type=["mp3","wav","ogg"])
555
+ if aud:
556
+ adir=os.path.join(os.getcwd(),"manim_assets","audio")
557
+ os.makedirs(adir,exist_ok=True)
558
+ ext=aud.name.split(".")[-1]
559
+ aname=f"audio_{int(time.time())}.{ext}"
560
+ ap=os.path.join(adir,aname)
561
+ with open(ap,"wb") as f: f.write(aud.getvalue())
562
+ st.session_state.audio_path=ap
563
+ st.audio(aud)
564
  st.success("Audio uploaded")
565
 
566
+ # Timeline Tab
567
  with tabs[4]:
568
+ st.markdown("### 🎞️ Timeline Editor")
569
+ st.info("Drag and adjust steps in code directly for now.")
 
570
 
571
+ # Educational Export Tab
572
  with tabs[5]:
573
  st.markdown("### πŸŽ“ Educational Export")
574
  if not st.session_state.video_data:
575
+ st.warning("Generate an animation first")
576
  else:
577
+ title = st.text_input("Title", "Manim Animation")
578
+ expl = st.text_area("Explanation (use ## to separate steps)", height=150)
579
+ fmt = st.selectbox("Format", ["PowerPoint","HTML","PDF Sequence"])
580
  if st.button("Export"):
581
+ # Simplified, reuse generate_manim_video logic or placeholder
582
+ st.success(f"{fmt} export not yet implemented.")
 
 
 
 
583
 
584
+ # Python Runner Tab
585
  with tabs[6]:
586
  st.markdown("### 🐍 Python Script Runner")
587
+ examples = {
588
+ "Select...":"",
589
+ "Sine Plot":"""import matplotlib.pyplot as plt
590
+ import numpy as np
591
+ x=np.linspace(0,10,100)
592
+ y=np.sin(x)
593
+ plt.plot(x,y)
594
+ print("Done plotting")"""
595
  }
596
+ sel=st.selectbox("Example", list(examples.keys()))
597
+ code = examples.get(sel, st.session_state.python_script)
598
  if ACE_EDITOR_AVAILABLE:
599
+ code = st_ace(value=code, language="python", theme="monokai", min_lines=15, key="pyace")
600
  else:
601
+ code = st.text_area("Code", code, height=300, key="pyta")
602
+ st.session_state.python_script=code
603
+ inputs = detect_input_calls(code)
604
  vals=[]
605
+ if inputs:
606
+ st.info(f"{len(inputs)} input() calls detected")
607
+ for i,c in enumerate(inputs):
608
+ vals.append(st.text_input(f"{c['prompt']} (line {c['line']})", key=f"inp{i}"))
609
+ timeout = st.slider("Timeout", 5,300,30)
 
610
  if st.button("▢️ Run"):
611
+ res=run_python_script(code, inputs=vals, timeout=timeout)
612
  st.session_state.python_result=res
613
  if st.session_state.python_result:
614
  display_python_script_results(st.session_state.python_result)
615
+
616
+ # Handle tab switch after actions
617
+ if st.session_state.pending_tab_switch is not None:
618
+ st.session_state.active_tab = st.session_state.pending_tab_switch
619
+ st.session_state.pending_tab_switch=None
 
 
 
 
 
 
 
 
620
 
621
  if __name__ == "__main__":
622
  main()