Spaces:
Running
Running
Update app.py
Browse files
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 |
-
"
|
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 = {
|
|
|
|
|
|
|
78 |
token_param = config["param_name"]
|
79 |
-
|
80 |
-
api_params[token_param] = token_value
|
81 |
return api_params, config
|
82 |
|
83 |
-
def get_secret(
|
84 |
-
"""Retrieve a secret from environment
|
85 |
-
|
86 |
-
|
87 |
-
|
|
|
88 |
|
89 |
def check_password():
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
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 ==
|
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 |
-
|
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
|
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 |
-
|
137 |
-
|
138 |
for i, (pkg, ver) in enumerate(missing.items()):
|
139 |
-
|
|
|
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}
|
143 |
return False
|
144 |
-
|
|
|
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
|
156 |
from azure.core.credentials import AzureKeyCredential
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
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 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
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 |
-
|
277 |
-
return
|
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 = [
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
line = proc.stdout.readline()
|
318 |
-
if not line and proc.poll() is not None: break
|
319 |
output.append(line)
|
320 |
-
|
321 |
-
try:
|
322 |
-
p=float(line.split("%")[0].strip().split()[-1]);
|
323 |
-
except: pass
|
324 |
if "File ready at" in line:
|
325 |
-
|
326 |
-
m
|
327 |
-
|
328 |
-
out_path
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
if
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
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.
|
377 |
-
if
|
378 |
-
m=re.search(r'input\([\'
|
379 |
-
prompt=m.group(1) if m else f"Input
|
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 |
-
|
387 |
-
|
388 |
-
|
389 |
def input(prompt=''):
|
390 |
-
global
|
391 |
print(prompt,end='')
|
392 |
-
if
|
393 |
-
|
394 |
-
print(
|
395 |
-
|
|
|
|
|
396 |
"""
|
397 |
-
|
398 |
with tempfile.TemporaryDirectory() as td:
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
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(
|
427 |
-
|
428 |
-
|
429 |
except subprocess.TimeoutExpired:
|
430 |
-
|
431 |
-
result["stderr"]+="\
|
432 |
-
result["exception"]="Timeout"
|
433 |
-
return result
|
434 |
result["execution_time"]=time.time()-start
|
435 |
-
result["stdout"]=open(
|
436 |
-
result["stderr"]
|
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(
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
if result["stderr"]:
|
451 |
st.error("Errors:")
|
452 |
-
st.code(
|
453 |
-
if
|
454 |
st.markdown("### Plots")
|
455 |
-
cols=st.columns(min(3,len(
|
456 |
-
for i,p in enumerate(
|
457 |
cols[i%len(cols)].image(p,use_column_width=True)
|
458 |
-
if
|
459 |
st.markdown("### DataFrames")
|
460 |
-
for df in
|
461 |
-
with st.expander(f"{df['name']} {df['shape']}"):
|
462 |
-
st.
|
463 |
-
if
|
464 |
-
st.markdown("###
|
465 |
-
st.code(
|
466 |
-
|
467 |
-
|
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.
|
589 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
|
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")
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
|
636 |
-
|
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
|
644 |
if mode=="Upload File":
|
645 |
-
up=st.file_uploader("Upload .py",type=["py"]
|
646 |
if up:
|
647 |
-
txt=up.getvalue().decode(
|
648 |
-
|
|
|
|
|
649 |
if ACE_EDITOR_AVAILABLE:
|
650 |
-
|
651 |
else:
|
652 |
-
|
653 |
-
if
|
654 |
-
st.session_state.code=
|
655 |
-
if st.button("π Generate Animation"
|
656 |
-
if not st.session_state.code
|
657 |
st.error("Enter code first")
|
658 |
else:
|
659 |
-
|
660 |
-
|
661 |
-
|
662 |
-
st.session_state.
|
663 |
-
|
664 |
-
|
665 |
-
|
|
|
|
|
666 |
with col2:
|
667 |
-
st.markdown("### π₯οΈ Preview & Output")
|
668 |
if st.session_state.code:
|
669 |
-
st.markdown("<div style='border:1px solid #ccc;padding:
|
670 |
-
|
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
|
676 |
elif fmt=="svg":
|
677 |
-
try:
|
678 |
-
|
679 |
-
|
|
|
|
|
|
|
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"
|
683 |
if st.session_state.status:
|
684 |
-
if "
|
685 |
-
|
686 |
-
|
|
|
|
|
|
|
687 |
with tabs[1]:
|
688 |
st.markdown("### π€ AI Animation Assistant")
|
689 |
if check_password():
|
690 |
-
|
691 |
-
|
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 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
715 |
-
|
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:
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
st.
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
else: st.error(f"Error: {stt}")
|
754 |
else:
|
755 |
-
st.info("Enter password to access
|
756 |
|
757 |
-
#
|
758 |
with tabs[2]:
|
759 |
st.markdown("### π LaTeX Formula Builder")
|
760 |
-
|
761 |
-
with
|
762 |
-
|
763 |
-
st.session_state.latex_formula=
|
764 |
-
|
765 |
-
|
766 |
-
|
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(
|
781 |
if st.button("Insert into Editor"):
|
782 |
-
if
|
783 |
-
|
784 |
-
|
785 |
-
|
786 |
-
|
787 |
-
|
788 |
-
|
789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
790 |
else:
|
791 |
-
|
792 |
-
|
793 |
-
|
794 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
795 |
|
796 |
-
#
|
797 |
with tabs[3]:
|
798 |
st.markdown("### π¨ Asset Management")
|
799 |
-
|
800 |
-
with
|
801 |
-
imgs=st.file_uploader("Upload Images",type=["png","jpg","jpeg","svg"],accept_multiple_files=True)
|
802 |
if imgs:
|
803 |
-
|
|
|
804 |
for up in imgs:
|
805 |
ext=up.name.split(".")[-1]
|
806 |
-
|
807 |
-
|
808 |
-
open(
|
809 |
-
st.session_state.image_paths.append({"name":up.name,"path":
|
810 |
-
st.success("Images uploaded")
|
811 |
if st.session_state.image_paths:
|
812 |
-
for
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
|
|
|
|
|
|
817 |
self.play(FadeIn(image))
|
818 |
self.wait(1)
|
819 |
"""
|
820 |
-
st.session_state.code+=
|
821 |
-
|
822 |
-
|
823 |
-
|
824 |
-
|
825 |
-
|
826 |
-
|
827 |
-
|
828 |
-
|
829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
830 |
st.success("Audio uploaded")
|
831 |
|
832 |
-
#
|
833 |
with tabs[4]:
|
834 |
-
|
835 |
-
|
836 |
-
st.session_state.code=updated; st.experimental_rerun()
|
837 |
|
838 |
-
#
|
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("
|
845 |
-
expl=st.text_area("Explanation",height=150)
|
846 |
-
fmt=st.selectbox("Format",["PowerPoint
|
847 |
if st.button("Export"):
|
848 |
-
|
849 |
-
|
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 |
-
#
|
856 |
with tabs[6]:
|
857 |
st.markdown("### π Python Script Runner")
|
858 |
-
examples={
|
859 |
-
"
|
860 |
-
"
|
861 |
-
|
|
|
|
|
|
|
|
|
862 |
}
|
863 |
-
|
864 |
-
code=
|
865 |
if ACE_EDITOR_AVAILABLE:
|
866 |
-
|
867 |
else:
|
868 |
-
|
869 |
-
st.session_state.python_script=
|
870 |
-
|
871 |
vals=[]
|
872 |
-
if
|
873 |
-
st.
|
874 |
-
for i,c in enumerate(
|
875 |
-
|
876 |
-
|
877 |
-
timeout=st.slider("Timeout",5,300,30)
|
878 |
if st.button("βΆοΈ Run"):
|
879 |
-
res=run_python_script(
|
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 |
-
|
884 |
-
|
885 |
-
|
886 |
-
|
887 |
-
|
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()
|