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