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