euler314 commited on
Commit
0ef6cca
Β·
verified Β·
1 Parent(s): 72d949d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -403
app.py CHANGED
@@ -9,11 +9,6 @@ import numpy as np
9
  import sys
10
  import subprocess
11
  import json
12
- from pygments import highlight
13
- from pygments.lexers import PythonLexer
14
- from pygments.formatters import HtmlFormatter
15
- import base64
16
- import torch
17
  import re
18
  import shutil
19
  import time
@@ -25,67 +20,62 @@ import plotly.express as px
25
  import zipfile
26
  import traceback
27
 
28
- # Set up enhanced logging
29
  logging.basicConfig(
30
  level=logging.INFO,
31
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
32
  handlers=[logging.StreamHandler()]
33
  )
34
  logger = logging.getLogger(__name__)
35
 
36
- # Model configuration mapping
 
 
 
 
 
 
 
 
 
 
 
37
  MODEL_CONFIGS = {
38
- "DeepSeek-V3-0324": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek"},
39
- "DeepSeek-R1": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek"},
40
- "Llama-4-Scout-17B-16E-Instruct": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Meta"},
41
- "Llama-4-Maverick-17B-128E-Instruct-FP8": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Meta"},
42
- "gpt-4o-mini": {"max_tokens": 15000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
43
- "gpt-4o": {"max_tokens": 16000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
44
- "gpt-4.1": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
45
- "gpt-4.1-mini": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
46
- "gpt-4.1-nano": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
47
- "o3-mini": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI"},
48
- "o1": {"max_completion_tokens": 100000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI"},
49
- "o1-mini": {"max_completion_tokens": 66000, "param_name": "max_completion_tokens", "api_version": "2024-12-01-preview", "category": "OpenAI"},
50
- "o1-preview": {"max_tokens": 33000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI"},
51
- "Phi-4-multimodal-instruct": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Microsoft"},
52
- "Mistral-large-2407": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral"},
53
- "Codestral-2501": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Mistral"},
54
- "default": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Other"}
55
  }
56
 
57
- # Try to import Streamlit Ace
58
  try:
59
  from streamlit_ace import st_ace
60
  ACE_EDITOR_AVAILABLE = True
61
  except ImportError:
62
  ACE_EDITOR_AVAILABLE = False
63
- logger.warning("streamlit-ace not available, falling back to text area")
64
-
65
- def prepare_api_params(messages, model_name):
66
- config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"])
67
- params = {"messages": messages, "model": model_name}
68
- params[config["param_name"]] = config.get(config["param_name"])
69
- return params, config
70
 
71
  def get_secret(env_var):
72
  val = os.environ.get(env_var)
73
  if not val:
74
- logger.warning(f"Secret '{env_var}' not found")
75
  return val
76
 
77
  def check_password():
78
- correct = get_secret("password")
79
- if not correct:
80
  st.error("Admin password not configured")
81
  return False
82
- if "password_entered" not in st.session_state:
83
- st.session_state.password_entered = False
84
- if not st.session_state.password_entered:
85
- pwd = st.text_input("Enter password to access AI features", type="password")
86
- if pwd:
87
- if pwd == correct:
88
- st.session_state.password_entered = True
89
  return True
90
  else:
91
  st.error("Incorrect password")
@@ -94,18 +84,23 @@ def check_password():
94
  return True
95
 
96
  def ensure_packages():
97
- required = {
98
- 'manim': '0.17.3', 'Pillow': '9.0.0', 'numpy': '1.22.0',
99
- 'transformers': '4.30.0', 'torch': '2.0.0', 'pygments': '2.15.1',
100
- 'streamlit-ace': '0.1.1', 'pydub': '0.25.1', 'plotly': '5.14.0',
101
- 'pandas': '2.0.0', 'python-pptx': '0.6.21', 'fpdf': '1.7.2',
102
- 'matplotlib': '3.5.0', 'seaborn': '0.11.2', 'scipy': '1.7.3',
103
- 'huggingface_hub': '0.16.0'
 
 
 
 
 
104
  }
105
  missing = {}
106
- for pkg, ver in required.items():
107
  try:
108
- __import__(pkg if pkg != 'Pillow' else 'PIL')
109
  except ImportError:
110
  missing[pkg] = ver
111
  if not missing:
@@ -113,88 +108,36 @@ def ensure_packages():
113
  bar = st.progress(0)
114
  txt = st.empty()
115
  for i, (pkg, ver) in enumerate(missing.items()):
116
- bar.progress(i / len(missing))
117
  txt.text(f"Installing {pkg}...")
118
  res = subprocess.run([sys.executable, "-m", "pip", "install", f"{pkg}>={ver}"], capture_output=True, text=True)
119
  if res.returncode != 0:
120
  st.error(f"Failed to install {pkg}")
121
  return False
122
- bar.progress(1.0)
123
  txt.empty()
124
  return True
125
 
126
- def install_custom_packages(pkgs):
127
- if not pkgs.strip():
128
- return True, "No packages specified"
129
- parts = [p.strip() for p in pkgs.split(",") if p.strip()]
130
- if not parts:
131
- return True, "No valid packages"
132
- sidebar_txt = st.sidebar.empty()
133
- bar = st.sidebar.progress(0)
134
- results, success = [], True
135
- for i, p in enumerate(parts):
136
- bar.progress(i / len(parts))
137
- sidebar_txt.text(f"Installing {p}...")
138
- res = subprocess.run([sys.executable, "-m", "pip", "install", p], capture_output=True, text=True)
139
- if res.returncode != 0:
140
- results.append(f"Failed {p}: {res.stderr}")
141
- success = False
142
- else:
143
- results.append(f"Installed {p}")
144
- bar.progress(1.0)
145
- sidebar_txt.empty()
146
- return success, "\n".join(results)
147
-
148
- @st.cache_resource(ttl=3600)
149
- def init_ai_models_direct():
150
- token = get_secret("github_token_api")
151
- if not token:
152
- st.error("API token not configured")
153
- return None
154
- try:
155
- from azure.ai.inference import ChatCompletionsClient
156
- from azure.ai.inference.models import UserMessage
157
- from azure.core.credentials import AzureKeyCredential
158
- client = ChatCompletionsClient(
159
- endpoint="https://models.inference.ai.azure.com",
160
- credential=AzureKeyCredential(token)
161
- )
162
- return {"client": client, "model_name": "gpt-4o", "last_loaded": datetime.now().isoformat()}
163
- except ImportError as e:
164
- st.error("Azure AI SDK not installed")
165
- logger.error(str(e))
166
- return None
167
 
168
  def generate_manim_preview(code):
169
- objects = []
170
- if "Circle" in code: objects.append("β­•")
171
- if "Square" in code: objects.append("πŸ”²")
172
- if "MathTex" in code or "Tex" in code: objects.append("πŸ“Š")
173
- if "Text" in code: objects.append("πŸ“")
174
- if "Axes" in code: objects.append("πŸ“ˆ")
175
- icons = "".join(objects) or "🎬"
176
  return f"""
177
- <div style="background:#000;color:#fff;padding:1rem;border-radius:10px;text-align:center;">
178
- <h3>Animation Preview</h3>
179
- <div style="font-size:2rem;">{icons}</div>
180
- <p>Full rendering required for accurate preview</p>
181
  </div>
182
  """
183
 
184
- def extract_scene_class_name(code):
185
- m = re.findall(r'class\s+(\w+)\s*\([^)]*Scene', code)
186
- return m[0] if m else "MyScene"
187
-
188
- def mp4_to_gif(mp4_path, gif_path, fps=15):
189
- cmd = [
190
- "ffmpeg", "-i", mp4_path,
191
- "-vf", f"fps={fps},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
192
- "-loop", "0", gif_path
193
- ]
194
- res = subprocess.run(cmd, capture_output=True, text=True)
195
- return gif_path if res.returncode == 0 else None
196
-
197
- def generate_manim_video(code, fmt, quality, speed=1.0, audio_path=None):
198
  temp_dir = tempfile.mkdtemp(prefix="manim_")
199
  scene = extract_scene_class_name(code)
200
  scene_file = os.path.join(temp_dir, "scene.py")
@@ -202,345 +145,240 @@ def generate_manim_video(code, fmt, quality, speed=1.0, audio_path=None):
202
  f.write(code)
203
  qflags = {"480p":"-ql","720p":"-qm","1080p":"-qh","4K":"-qk","8K":"-qp"}
204
  qf = qflags.get(quality, "-qm")
205
- cmd = ["manim", scene_file, scene, qf, f"--format={fmt}"]
206
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
207
- output, out_path, mp4_path = [], None, None
208
  log = st.empty()
209
- for line in proc.stdout:
210
- output.append(line)
211
- log.code("".join(output[-10:]))
212
- if "File ready at" in line:
213
- m = re.search(r'["\'](.+?\.(?:mp4|gif|webm|svg))["\']', line)
214
- if m:
215
- out_path = m.group(1)
216
- if out_path.endswith(".mp4"):
217
- mp4_path = out_path
218
- proc.wait()
219
- time.sleep(1)
220
- if fmt=="gif" and (not out_path or not os.path.exists(out_path)) and mp4_path:
221
- gif = os.path.join(temp_dir, "converted.gif")
222
- conv = mp4_to_gif(mp4_path, gif)
223
- if conv and os.path.exists(conv):
224
- out_path = conv
 
 
 
 
 
225
  data = None
226
  if out_path and os.path.exists(out_path):
227
  with open(out_path, "rb") as f:
228
  data = f.read()
229
  shutil.rmtree(temp_dir)
230
  if data:
231
- size_mb = len(data)/(1024*1024)
232
- return data, f"βœ… Generated ({size_mb:.1f} MB)"
233
  else:
234
- return None, "❌ No output generated. See logs."
235
-
236
- def detect_input_calls(code):
237
- calls=[]
238
- for i,line in enumerate(code.split("\n"),1):
239
- if "input(" in line and not line.strip().startswith("#"):
240
- m=re.search(r'input\(["\'](.+?)["\']\)', line)
241
- prompt=m.group(1) if m else f"Input at line {i}"
242
- calls.append({"line":i,"prompt":prompt})
243
- return calls
244
-
245
- def run_python_script(code, inputs=None, timeout=60):
246
- res={"stdout":"","stderr":"","exception":None,"plots":[],"dataframes":[],"execution_time":0}
247
- mod=""
248
- if inputs:
249
- mod=f"""
250
- __INPUTS={inputs}
251
- __IDX=0
252
- def input(prompt=''):
253
- global __IDX
254
- print(prompt,end='')
255
- if __IDX<len(__INPUTS):
256
- val=__INPUTS[__IDX]; __IDX+=1
257
- print(val)
258
- return val
259
- print()
260
- return ''
261
- """
262
- full_code=mod+code
263
- with tempfile.TemporaryDirectory() as td:
264
- path=os.path.join(td,"script.py")
265
- with open(path,"w") as f: f.write(full_code)
266
- outf, errf = os.path.join(td,"out.txt"), os.path.join(td,"err.txt")
267
- start=time.time()
268
- try:
269
- with open(outf,"w") as o, open(errf,"w") as e:
270
- proc=subprocess.Popen([sys.executable, path], stdout=o, stderr=e, cwd=td)
271
- proc.wait(timeout=timeout)
272
- except subprocess.TimeoutExpired:
273
- proc.kill()
274
- res["stderr"]+="\nTimed out"
275
- res["exception"]="Timeout"
276
- res["execution_time"]=time.time()-start
277
- res["stdout"]=open(outf).read()
278
- res["stderr"]+=open(errf).read()
279
- return res
280
-
281
- def display_python_script_results(r):
282
- st.info(f"Completed in {r['execution_time']:.2f}s")
283
- if r["exception"]:
284
- st.error(f"Exception: {r['exception']}")
285
- if r["stderr"]:
286
- st.error("Errors:")
287
- st.code(r["stderr"], language="bash")
288
- if r["plots"]:
289
- st.markdown("### Plots")
290
- cols=st.columns(min(3,len(r["plots"])))
291
- for i,p in enumerate(r["plots"]):
292
- cols[i%len(cols)].image(p,use_column_width=True)
293
- if r["dataframes"]:
294
- st.markdown("### DataFrames")
295
- for df in r["dataframes"]:
296
- with st.expander(f"{df['name']} {df['shape']}"):
297
- st.markdown(df["preview_html"], unsafe_allow_html=True)
298
- if r["stdout"]:
299
- st.markdown("### Output")
300
- st.code(r["stdout"], language="bash")
301
 
302
  def main():
303
- if 'init' not in st.session_state:
 
304
  st.session_state.update({
305
- 'init':True, 'video_data':None, 'status':None, 'ai_models':None,
306
- 'generated_code':"", 'code':"", 'temp_code':"", 'editor_key':str(uuid.uuid4()),
307
- 'packages_checked':False, 'audio_path':None, 'image_paths':[],
308
- 'custom_library_result':"", 'python_script':"", 'python_result':None,
309
- 'active_tab':0, 'settings':{"quality":"720p","format_type":"mp4","animation_speed":"Normal"},
310
- 'password_entered':False, 'custom_model':"gpt-4o", 'pending_tab_switch':None
 
 
311
  })
312
- st.set_page_config(page_title="Manim Animation Studio", page_icon="🎬", layout="wide")
313
 
314
- if not st.session_state.packages_checked:
315
  if ensure_packages():
316
- st.session_state.packages_checked=True
317
  else:
318
- st.error("Package installation failed")
319
  return
320
 
321
- tab_names=[
322
- "✨ Editor","πŸ€– AI Assistant","🎨 Assets",
323
- "🎞️ Timeline","πŸŽ“ Educational Export","🐍 Python Runner"
324
- ]
325
- tabs = st.tabs(tab_names)
 
 
 
 
 
 
 
 
326
 
327
- # Editor
328
  with tabs[0]:
329
- col1,col2 = st.columns([3,2])
330
- with col1:
331
  st.markdown("### πŸ“ Animation Editor")
332
- mode = st.radio("Code Input", ["Type Code","Upload File"], key="editor_mode")
333
- if mode=="Upload File":
334
- up = st.file_uploader("Upload .py", type=["py"])
335
- if up:
336
- txt=up.getvalue().decode()
337
- if txt.strip():
338
- st.session_state.code=txt
339
- st.session_state.temp_code=txt
340
  if ACE_EDITOR_AVAILABLE:
341
- st.session_state.temp_code = st_ace(
342
- value=st.session_state.code, language="python",
343
- theme="monokai", min_lines=20,
344
- key=f"ace_{st.session_state.editor_key}"
 
 
345
  )
346
  else:
347
- st.session_state.temp_code = st.text_area(
348
- "Code", st.session_state.code, height=400,
349
- key=f"ta_{st.session_state.editor_key}"
 
350
  )
351
- if st.session_state.temp_code!=st.session_state.code:
352
- st.session_state.code=st.session_state.temp_code
353
  if st.button("πŸš€ Generate Animation"):
354
- if not st.session_state.code:
355
  st.error("Enter code first")
356
  else:
357
  data, msg = generate_manim_video(
358
  st.session_state.code,
359
- st.session_state.settings["format_type"],
360
- st.session_state.settings["quality"],
361
- {"Slow":0.5,"Normal":1.0,"Fast":2.0,"Very Fast":3.0}[st.session_state.settings["animation_speed"]],
362
- st.session_state.audio_path
363
  )
364
- st.session_state.video_data=data
365
- st.session_state.status=msg
366
- with col2:
367
  if st.session_state.code:
368
- components.html(
369
- generate_manim_preview(st.session_state.code),
370
- height=250
371
- )
372
  if st.session_state.video_data:
373
- fmt=st.session_state.settings["format_type"]
374
- if fmt=="png_sequence":
375
- st.download_button(
376
- "⬇️ Download PNG ZIP", data=st.session_state.video_data,
377
- file_name=f"manim_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip",
378
- mime="application/zip"
379
- )
380
- elif fmt=="svg":
381
  try:
382
- svg=st.session_state.video_data.decode('utf-8')
383
  components.html(svg, height=400)
384
  except:
385
  st.error("Cannot display SVG")
386
- st.download_button(
387
- "⬇️ Download SVG", data=st.session_state.video_data,
388
- file_name="animation.svg", mime="image/svg+xml"
389
- )
390
  else:
391
  st.video(st.session_state.video_data, format=fmt)
392
- st.download_button(
393
- f"⬇️ Download {fmt.upper()}", st.session_state.video_data,
394
- file_name=f"animation.{fmt}", mime=f"video/{fmt}" if fmt!="gif" else "image/gif"
395
- )
396
  if st.session_state.status:
397
- if "❌" in st.session_state.status:
398
  st.error(st.session_state.status)
399
  else:
400
  st.success(st.session_state.status)
401
 
402
- # AI Assistant
403
  with tabs[1]:
404
- st.markdown("### πŸ€– AI Animation Assistant")
405
- if check_password():
406
- client_data = init_ai_models_direct()
407
- if client_data:
408
- if st.button("Test API Connection"):
409
- from azure.ai.inference.models import UserMessage
410
- params,_=prepare_api_params([UserMessage("Hello")], client_data["model_name"])
411
- resp=client_data["client"].complete(**params)
412
- if resp.choices:
413
- st.success("βœ… Connection successful!")
414
- st.session_state.ai_models=client_data
415
- else:
416
- st.error("❌ No response")
417
- if st.session_state.ai_models:
418
- st.info(f"Using model {st.session_state.ai_models['model_name']}")
419
- prompt = st.text_area("Describe animation or paste partial code", height=150)
420
- if st.button("Generate Animation Code"):
421
- if prompt.strip():
422
- from azure.ai.inference.models import UserMessage
423
- params,_=prepare_api_params(
424
- [UserMessage(f"Write a complete Manim scene for:\n{prompt}")],
425
- st.session_state.ai_models["model_name"]
426
- )
427
- resp=st.session_state.ai_models["client"].complete(**params)
428
- if resp.choices:
429
- code = resp.choices[0].message.content
430
- if "```python" in code:
431
- code=code.split("```python")[1].split("```")[0]
432
- st.session_state.generated_code=code
433
- else:
434
- st.error("No code generated")
435
- else:
436
- st.warning("Enter prompt first")
437
- if st.session_state.generated_code:
438
- st.code(st.session_state.generated_code, language="python")
439
- if st.button("Use This Code"):
440
- st.session_state.code=st.session_state.generated_code
441
- st.session_state.temp_code=st.session_state.generated_code
442
- st.session_state.pending_tab_switch=0
443
- st.rerun()
444
- else:
445
- st.info("Enter password to access AI")
446
 
447
- # Assets
448
  with tabs[2]:
449
- st.markdown("### 🎨 Asset Management")
450
- c1,c2 = st.columns(2)
451
- with c1:
452
- imgs = st.file_uploader(
453
- "Upload Images", type=["png","jpg","jpeg","svg"],
454
- accept_multiple_files=True
455
- )
456
- if imgs:
457
- idir = os.path.join(os.getcwd(),"manim_assets","images")
458
- os.makedirs(idir, exist_ok=True)
459
- for up in imgs:
460
- ext=up.name.split(".")[-1]
461
- fname=f"img_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
462
- path=os.path.join(idir,fname)
463
- with open(path,"wb") as f: f.write(up.getvalue())
464
- st.session_state.image_paths.append({"name":up.name,"path":path})
465
- for info in st.session_state.image_paths:
466
- img=Image.open(info["path"])
467
- st.image(img, caption=info["name"], width=100)
468
- if st.button(f"Use {info['name']}"):
469
- snippet=f"""
470
- # Image asset
471
- image = ImageMobject(r"{info['path']}")
472
- image.scale(2)
473
  self.play(FadeIn(image))
474
  self.wait(1)
475
  """
476
- st.session_state.code+=snippet
477
- st.session_state.temp_code=st.session_state.code
478
- st.success(f"Added {info['name']}")
479
- st.session_state.pending_tab_switch=0
480
- st.rerun()
481
- with c2:
482
- aud = st.file_uploader("Upload Audio", type=["mp3","wav","ogg"])
483
- if aud:
484
- adir = os.path.join(os.getcwd(),"manim_assets","audio")
485
- os.makedirs(adir, exist_ok=True)
486
- ext=aud.name.split(".")[-1]
487
- aname=f"audio_{int(time.time())}.{ext}"
488
- ap=os.path.join(adir,aname)
489
- with open(ap,"wb") as f: f.write(aud.getvalue())
490
- st.session_state.audio_path=ap
491
- st.audio(aud)
492
- st.success("Audio uploaded")
493
 
494
- # Timeline
495
  with tabs[3]:
496
  st.markdown("### 🎞️ Timeline Editor")
497
- st.info("Use code editor to adjust timing of self.play and self.wait calls.")
498
 
499
- # Educational Export
500
  with tabs[4]:
501
- st.markdown("### πŸŽ“ Educational Export")
502
- if not st.session_state.video_data:
503
- st.warning("Generate animation first")
504
- else:
505
- title=st.text_input("Title","Manim Animation")
506
- expl=st.text_area("Explanation (use ## to separate steps)",height=150)
507
- fmt=st.selectbox("Format",["PowerPoint","HTML","PDF Sequence"])
508
- if st.button("Export"):
509
- st.success(f"{fmt} export not implemented yet")
510
 
511
- # Python Runner
512
  with tabs[5]:
513
- st.markdown("### 🐍 Python Script Runner")
514
- examples={"Select...":"","Sine Plot":"""import matplotlib.pyplot as plt
515
- import numpy as np
516
- x=np.linspace(0,10,100)
517
- y=np.sin(x)
518
- plt.plot(x,y)
519
- print("Done")"""}
520
- sel=st.selectbox("Example",list(examples.keys()))
521
- code = examples.get(sel, st.session_state.python_script)
522
- if ACE_EDITOR_AVAILABLE:
523
- code=st_ace(value=code, language="python", theme="monokai", min_lines=15, key="pyace")
524
- else:
525
- code=st.text_area("Code", code, height=300, key="pyta")
526
- st.session_state.python_script=code
527
- inputs=detect_input_calls(code)
528
- vals=[]
529
- if inputs:
530
- st.info(f"{len(inputs)} input() calls detected")
531
- for i,c in enumerate(inputs):
532
- vals.append(st.text_input(f"{c['prompt']} (line {c['line']})", key=f"in{i}"))
533
- timeout=st.slider("Timeout",5,300,30)
534
- if st.button("▢️ Run"):
535
- res=run_python_script(code, inputs=vals, timeout=timeout)
536
- st.session_state.python_result=res
537
- if st.session_state.python_result:
538
- display_python_script_results(st.session_state.python_result)
539
-
540
- # Handle pending tab switch
541
- if st.session_state.pending_tab_switch is not None:
542
- st.session_state.active_tab = st.session_state.pending_tab_switch
543
- st.session_state.pending_tab_switch = None
544
 
545
  if __name__ == "__main__":
546
  main()
 
9
  import sys
10
  import subprocess
11
  import json
 
 
 
 
 
12
  import re
13
  import shutil
14
  import time
 
20
  import zipfile
21
  import traceback
22
 
23
+ # Set up logging
24
  logging.basicConfig(
25
  level=logging.INFO,
26
+ format="%(asctime)s %(levelname)s %(name)s – %(message)s",
27
  handlers=[logging.StreamHandler()]
28
  )
29
  logger = logging.getLogger(__name__)
30
 
31
+ # Available quality presets and formats
32
+ QUALITY_PRESETS = ["480p", "720p", "1080p", "4K", "8K"]
33
+ FPS_OPTIONS = [24, 30, 60, 120]
34
+ OUTPUT_FORMATS = {
35
+ "MP4 Video": "mp4",
36
+ "GIF Animation": "gif",
37
+ "WebM Video": "webm",
38
+ "PNG Sequence (ZIP)": "png_sequence",
39
+ "SVG Image": "svg"
40
+ }
41
+
42
+ # Model configurations
43
  MODEL_CONFIGS = {
44
+ "gpt-4o": {},
45
+ "gpt-4o-mini": {},
46
+ "gpt-4.1": {},
47
+ "gpt-4.1-mini": {},
48
+ "o1": {},
49
+ "o1-mini": {},
50
+ "default": {}
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
+ # Try to import st_ace
54
  try:
55
  from streamlit_ace import st_ace
56
  ACE_EDITOR_AVAILABLE = True
57
  except ImportError:
58
  ACE_EDITOR_AVAILABLE = False
59
+ logger.warning("streamlit-ace not available, using text_area")
 
 
 
 
 
 
60
 
61
  def get_secret(env_var):
62
  val = os.environ.get(env_var)
63
  if not val:
64
+ logger.warning(f"Secret '{env_var}' not set")
65
  return val
66
 
67
  def check_password():
68
+ pwd = get_secret("password")
69
+ if not pwd:
70
  st.error("Admin password not configured")
71
  return False
72
+ if "pwd_ok" not in st.session_state:
73
+ st.session_state.pwd_ok = False
74
+ if not st.session_state.pwd_ok:
75
+ entry = st.text_input("Enter password", type="password")
76
+ if entry:
77
+ if entry == pwd:
78
+ st.session_state.pwd_ok = True
79
  return True
80
  else:
81
  st.error("Incorrect password")
 
84
  return True
85
 
86
  def ensure_packages():
87
+ reqs = {
88
+ "manim": "0.17.3",
89
+ "Pillow": "9.0.0",
90
+ "numpy": "1.22.0",
91
+ "plotly": "5.14.0",
92
+ "pandas": "2.0.0",
93
+ "python-pptx": "0.6.21",
94
+ "fpdf": "1.7.2",
95
+ "matplotlib": "3.5.0",
96
+ "seaborn": "0.11.2",
97
+ "scipy": "1.7.3",
98
+ "streamlit-ace": "0.1.1"
99
  }
100
  missing = {}
101
+ for pkg, ver in reqs.items():
102
  try:
103
+ __import__(pkg if pkg != "Pillow" else "PIL")
104
  except ImportError:
105
  missing[pkg] = ver
106
  if not missing:
 
108
  bar = st.progress(0)
109
  txt = st.empty()
110
  for i, (pkg, ver) in enumerate(missing.items()):
 
111
  txt.text(f"Installing {pkg}...")
112
  res = subprocess.run([sys.executable, "-m", "pip", "install", f"{pkg}>={ver}"], capture_output=True, text=True)
113
  if res.returncode != 0:
114
  st.error(f"Failed to install {pkg}")
115
  return False
116
+ bar.progress((i + 1) / len(missing))
117
  txt.empty()
118
  return True
119
 
120
+ def extract_scene_class_name(code):
121
+ m = re.findall(r"class\s+(\w+)\s*\([^)]*Scene", code)
122
+ return m[0] if m else "MyScene"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def generate_manim_preview(code):
125
+ icons = []
126
+ if "Circle" in code: icons.append("β­•")
127
+ if "Square" in code: icons.append("πŸ”²")
128
+ if "MathTex" in code or "Tex" in code: icons.append("πŸ“Š")
129
+ if "Text" in code: icons.append("πŸ“")
130
+ if "Axes" in code: icons.append("πŸ“ˆ")
131
+ preview_icons = "".join(icons) or "🎬"
132
  return f"""
133
+ <div style="background:#000;color:#fff;padding:1rem;border-radius:8px;text-align:center;">
134
+ <h3>Preview</h3>
135
+ <div style="font-size:2rem;">{preview_icons}</div>
136
+ <p>Full render for accurate output</p>
137
  </div>
138
  """
139
 
140
+ def generate_manim_video(code, fmt, quality, fps, audio_path=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  temp_dir = tempfile.mkdtemp(prefix="manim_")
142
  scene = extract_scene_class_name(code)
143
  scene_file = os.path.join(temp_dir, "scene.py")
 
145
  f.write(code)
146
  qflags = {"480p":"-ql","720p":"-qm","1080p":"-qh","4K":"-qk","8K":"-qp"}
147
  qf = qflags.get(quality, "-qm")
148
+ cmd = ["manim", scene_file, scene, qf, f"--format={fmt}", f"--fps={fps}"]
149
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
150
+ progress = st.progress(0)
151
  log = st.empty()
152
+ total = None
153
+ for line in process.stdout:
154
+ log.code(line)
155
+ m = re.search(r"(\d+)\s*/\s*(\d+)", line)
156
+ if m:
157
+ cur, tot = map(int, m.groups())
158
+ total = tot
159
+ progress.progress(min(0.99, cur / tot))
160
+ else:
161
+ p = re.search(r"(\d+)%", line)
162
+ if p:
163
+ progress.progress(min(0.99, int(p.group(1)) / 100))
164
+ process.wait()
165
+ progress.progress(1.0)
166
+ # locate output
167
+ out_path = None
168
+ for root, _, files in os.walk(temp_dir):
169
+ for f in files:
170
+ if f.endswith(f".{fmt}") or (fmt=="png_sequence" and f.endswith(".zip")):
171
+ out_path = os.path.join(root, f)
172
+ break
173
  data = None
174
  if out_path and os.path.exists(out_path):
175
  with open(out_path, "rb") as f:
176
  data = f.read()
177
  shutil.rmtree(temp_dir)
178
  if data:
179
+ size_mb = len(data) / (1024 * 1024)
180
+ return data, f"βœ… Done ({size_mb:.1f} MB)"
181
  else:
182
+ return None, "❌ No output generated"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  def main():
185
+ st.set_page_config("Manim Studio", "🎬", layout="wide")
186
+ if "inited" not in st.session_state:
187
  st.session_state.update({
188
+ "inited": True,
189
+ "code": "",
190
+ "video_data": None,
191
+ "status": "",
192
+ "settings": {"quality":"720p","fps":30,"format":"mp4"},
193
+ "pwd_ok": False,
194
+ "custom_model": "gpt-4o",
195
+ "generated_code": ""
196
  })
 
197
 
198
+ if not st.session_state.get("packages_checked", False):
199
  if ensure_packages():
200
+ st.session_state.packages_checked = True
201
  else:
 
202
  return
203
 
204
+ tabs = st.tabs([
205
+ "✨ Editor", "πŸ€– AI Assistant", "🎨 Assets",
206
+ "🎞️ Timeline", "πŸŽ“ Export", "🐍 Python Runner"
207
+ ])
208
+
209
+ # Sidebar settings
210
+ with st.sidebar:
211
+ st.markdown("## βš™οΈ Render Settings")
212
+ q = st.selectbox("Quality", QUALITY_PRESETS, index=QUALITY_PRESETS.index(st.session_state["settings"]["quality"]))
213
+ f = st.selectbox("FPS", FPS_OPTIONS, index=FPS_OPTIONS.index(st.session_state["settings"]["fps"]))
214
+ fmt_disp = st.selectbox("Format", list(OUTPUT_FORMATS.keys()),
215
+ index=list(OUTPUT_FORMATS.values()).index(st.session_state["settings"]["format"]))
216
+ st.session_state["settings"] = {"quality": q, "fps": f, "format": OUTPUT_FORMATS[fmt_disp]}
217
 
218
+ # Editor tab
219
  with tabs[0]:
220
+ c1, c2 = st.columns([3,2])
221
+ with c1:
222
  st.markdown("### πŸ“ Animation Editor")
 
 
 
 
 
 
 
 
223
  if ACE_EDITOR_AVAILABLE:
224
+ st.session_state.code = st_ace(
225
+ value=st.session_state.code,
226
+ language="python",
227
+ theme="monokai",
228
+ min_lines=20,
229
+ key="ace"
230
  )
231
  else:
232
+ st.session_state.code = st.text_area(
233
+ "Manim Code",
234
+ value=st.session_state.code,
235
+ height=400
236
  )
 
 
237
  if st.button("πŸš€ Generate Animation"):
238
+ if not st.session_state.code.strip():
239
  st.error("Enter code first")
240
  else:
241
  data, msg = generate_manim_video(
242
  st.session_state.code,
243
+ st.session_state["settings"]["format"],
244
+ st.session_state["settings"]["quality"],
245
+ st.session_state["settings"]["fps"]
 
246
  )
247
+ st.session_state.video_data = data
248
+ st.session_state.status = msg
249
+ with c2:
250
  if st.session_state.code:
251
+ components.html(generate_manim_preview(st.session_state.code), height=250)
 
 
 
252
  if st.session_state.video_data:
253
+ fmt = st.session_state["settings"]["format"]
254
+ if fmt == "png_sequence":
255
+ st.download_button("⬇️ Download ZIP", st.session_state.video_data, "animation.zip", "application/zip")
256
+ elif fmt == "svg":
 
 
 
 
257
  try:
258
+ svg = st.session_state.video_data.decode("utf-8")
259
  components.html(svg, height=400)
260
  except:
261
  st.error("Cannot display SVG")
262
+ st.download_button("⬇️ Download SVG", st.session_state.video_data, "animation.svg", "image/svg+xml")
 
 
 
263
  else:
264
  st.video(st.session_state.video_data, format=fmt)
265
+ st.download_button(f"⬇️ Download {fmt.upper()}", st.session_state.video_data,
266
+ f"animation.{fmt}", f"video/{fmt}")
 
 
267
  if st.session_state.status:
268
+ if st.session_state.status.startswith("❌"):
269
  st.error(st.session_state.status)
270
  else:
271
  st.success(st.session_state.status)
272
 
273
+ # AI Assistant tab
274
  with tabs[1]:
275
+ st.markdown("### πŸ€– AI Assistant")
276
+ if not check_password():
277
+ return
278
+ model = st.selectbox("Select AI Model", list(MODEL_CONFIGS.keys()),
279
+ index=list(MODEL_CONFIGS.keys()).index(st.session_state.custom_model))
280
+ st.session_state.custom_model = model
281
+ token = get_secret("github_token_api")
282
+ if st.button("Test Connection"):
283
+ from azure.ai.inference import ChatCompletionsClient
284
+ from azure.core.credentials import AzureKeyCredential
285
+ client = ChatCompletionsClient(
286
+ endpoint="https://models.inference.ai.azure.com",
287
+ credential=AzureKeyCredential(token)
288
+ )
289
+ from azure.ai.inference.models import UserMessage
290
+ resp = client.complete(**{"messages":[UserMessage("Hello")], "model":model, "max_tokens":1000})
291
+ if resp.choices:
292
+ st.success("βœ… Connected")
293
+ st.session_state.ai_client = client
294
+ else:
295
+ st.error("❌ No response")
296
+ if "ai_client" in st.session_state:
297
+ prompt = st.text_area("Describe animation or provide code")
298
+ if st.button("Generate Code"):
299
+ from azure.ai.inference.models import UserMessage
300
+ msgs = [UserMessage(f"Write a complete Manim scene:\n{prompt}")]
301
+ resp = st.session_state.ai_client.complete(**{"messages":msgs, "model":model, "max_tokens":8000})
302
+ if resp.choices:
303
+ code = resp.choices[0].message.content
304
+ if "```python" in code:
305
+ code = code.split("```python")[1].split("```")[0]
306
+ st.session_state.generated_code = code
307
+ else:
308
+ st.error("No code returned")
309
+ if st.session_state.generated_code:
310
+ st.code(st.session_state.generated_code, language="python")
311
+ if st.button("Use This Code"):
312
+ st.session_state.code = st.session_state.generated_code
313
+ st.experimental_rerun()
 
 
 
314
 
315
+ # Assets tab
316
  with tabs[2]:
317
+ st.markdown("### 🎨 Assets")
318
+ imgs = st.file_uploader("Upload Images", type=["png","jpg","jpeg","svg"], accept_multiple_files=True)
319
+ if imgs:
320
+ adir = os.path.join(os.getcwd(),"manim_assets","images")
321
+ os.makedirs(adir, exist_ok=True)
322
+ for up in imgs:
323
+ ext = up.name.split(".")[-1]
324
+ fn = f"img_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
325
+ path = os.path.join(adir, fn)
326
+ with open(path,"wb") as f: f.write(up.getvalue())
327
+ st.image(path, caption=up.name, width=150)
328
+ if st.button(f"Use {up.name}"):
329
+ snippet = f"""image = ImageMobject(r"{path}")
 
 
 
 
 
 
 
 
 
 
 
330
  self.play(FadeIn(image))
331
  self.wait(1)
332
  """
333
+ st.session_state.code += "\n" + snippet
334
+ st.experimental_rerun()
335
+ aud = st.file_uploader("Upload Audio", type=["mp3","wav","ogg"])
336
+ if aud:
337
+ adir = os.path.join(os.getcwd(),"manim_assets","audio")
338
+ os.makedirs(adir, exist_ok=True)
339
+ fn = f"audio_{int(time.time())}.{aud.name.split('.')[-1]}"
340
+ path = os.path.join(adir, fn)
341
+ with open(path,"wb") as f: f.write(aud.getvalue())
342
+ st.audio(aud)
343
+ st.success("Audio uploaded")
 
 
 
 
 
 
344
 
345
+ # Timeline tab
346
  with tabs[3]:
347
  st.markdown("### 🎞️ Timeline Editor")
348
+ st.info("Adjust code manually – timeline UI coming soon.")
349
 
350
+ # Export tab
351
  with tabs[4]:
352
+ st.markdown("### πŸŽ“ Export")
353
+ st.warning("Export features coming soon.")
 
 
 
 
 
 
 
354
 
355
+ # Python Runner tab
356
  with tabs[5]:
357
+ st.markdown("### 🐍 Python Runner")
358
+ code = st.text_area("Script", height=300, key="py_code")
359
+ inputs = []
360
+ for i, line in enumerate(code.split("\n"), 1):
361
+ if "input(" in line:
362
+ prompt = re.search(r'input\(["\'](.+?)["\']\)', line)
363
+ label = prompt.group(1) if prompt else f"Input line {i}"
364
+ inputs.append(st.text_input(label, key=f"in_{i}"))
365
+ timeout = st.slider("Timeout (s)", 5, 300, 30)
366
+ if st.button("Run"):
367
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
368
+ temp.write(code.encode())
369
+ temp.flush()
370
+ proc = subprocess.Popen([sys.executable, temp.name],
371
+ stdin=subprocess.PIPE,
372
+ stdout=subprocess.PIPE,
373
+ stderr=subprocess.PIPE,
374
+ text=True)
375
+ # feed inputs
376
+ out, err = proc.communicate(input="\n".join(inputs), timeout=timeout)
377
+ if err:
378
+ st.error(err)
379
+ if out:
380
+ st.code(out)
381
+ temp.close()
 
 
 
 
 
 
382
 
383
  if __name__ == "__main__":
384
  main()