faizr206 commited on
Commit
b93c8f2
·
1 Parent(s): 453eaf6

remove visual refinement for testing

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -2
  2. app/main.py +68 -154
Dockerfile CHANGED
@@ -3,10 +3,9 @@ FROM manimcommunity/manim:stable
3
  WORKDIR /app
4
  COPY app /app
5
 
6
- # Web + Google Gemini (new SDK) + dotenv (optional)
7
  RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic python-dotenv google-genai
8
 
9
- # Space default port
10
  ENV PORT=7860
11
  EXPOSE 7860
12
 
 
3
  WORKDIR /app
4
  COPY app /app
5
 
6
+ # Web server + Gemini SDK + dotenv (optional for local dev)
7
  RUN pip install --no-cache-dir fastapi uvicorn[standard] pydantic python-dotenv google-genai
8
 
 
9
  ENV PORT=7860
10
  EXPOSE 7860
11
 
app/main.py CHANGED
@@ -1,28 +1,27 @@
1
- import os, re, uuid, subprocess, sys, base64, traceback
2
  from pathlib import Path
3
- from typing import List, Tuple, Optional
4
 
5
  from fastapi import FastAPI, HTTPException, Response
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from pydantic import BaseModel
8
 
9
- # Optional .env support (local dev)
10
  from dotenv import load_dotenv
11
  load_dotenv()
12
 
13
- # ---------- Gemini (keep your original SDK & calls) ----------
14
  from google import genai
15
  from google.genai import types
16
 
17
  API_KEY = os.getenv("GEMINI_API_KEY", "")
18
- MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-pro")
19
- PORT = int(os.getenv("PORT", "7860"))
20
 
21
- # Configure Gemini client (exactly like your Flask code)
22
  client = genai.Client(api_key=API_KEY) if API_KEY else None
23
 
24
- # ---------- FastAPI ----------
25
- app = FastAPI(title="Manim Render API (Gemini → Manim → MP4)")
26
  app.add_middleware(
27
  CORSMiddleware,
28
  allow_origins=["*"], # tighten in prod
@@ -30,122 +29,43 @@ app.add_middleware(
30
  allow_headers=["*"],
31
  )
32
 
33
- # Work dirs (ephemeral per Space session is fine)
34
- RUNS = Path("runs"); RUNS.mkdir(parents=True, exist_ok=True)
35
 
36
- SYSTEM_PROMPT = """You are a Manim CE (0.19.x) code generator and refiner.
37
  Return ONLY valid Python code (no backticks, no prose).
38
  Define exactly one class: AutoScene(Scene).
39
- Keep it short (≤ ~120 s) and renderable.
40
 
41
  Use: from manim import *
42
  Allowed imports: manim, math, numpy.
43
- Forbidden: os, subprocess, sys, requests, pathlib, socket, shutil, psutil, file/network/OS access.
44
-
45
- Instructions for frame capture:
46
- - Capture a frame after each logical scene segment to help with automated evaluation.
47
- - Use:
48
- frame_image = self.camera.get_image()
49
- frame_image.save(FRAME_DIR / "frame_name.png")
50
- - Place this immediately after each self.play() that introduces or transforms major objects.
51
- - Choose descriptive names for frames, e.g., "frame_title", "frame_vector_addition".
52
- - Ensure all frames are saved inside the run-specific folder (FRAME_DIR) to allow automated refinement loops to detect them.
53
  """
54
 
55
  DEFAULT_SCENE = """from manim import *
56
 
57
  class AutoScene(Scene):
58
  def construct(self):
59
- t = Text("Hello").scale(1)
60
  self.play(Write(t))
61
- frame_image = self.camera.get_image()
62
- frame_image.save(FRAME_DIR / "hello.png")
63
  self.wait(0.5)
64
  """
65
 
66
- # ---------- helpers ----------
67
  def _clean_code(text: str) -> str:
68
- """Strip code fences and whitespace."""
69
  if not text:
70
  return ""
71
- code = re.sub(r"^```(?:python)?\n|```$", "", text, flags=re.MULTILINE).strip()
72
- return code
73
-
74
- def _prepare_frame_prefixes(frame_paths: List[str], prefix_len=300):
75
- """(Kept from original) Produce small b64 prefixes (not sent unless you want)."""
76
- mapping = []
77
- for p in frame_paths or []:
78
- try:
79
- b = Path(p).read_bytes()
80
- b64 = base64.b64encode(b).decode("utf-8")
81
- short = b64[:prefix_len]
82
- mapping.append((Path(p).name, short))
83
- except Exception as e:
84
- print("Failed reading frame", p, e, file=sys.stderr)
85
- return mapping
86
-
87
- def llm_generate_manim_code(prompt: str, previous_code: Optional[str] = None, frame_paths: Optional[List[str]] = None) -> str:
88
- """Generate or refine Manim AutoScene code using Gemini (multimodal with frames)."""
89
- if not client:
90
- return DEFAULT_SCENE
91
-
92
- try:
93
- contents = []
94
- if previous_code and frame_paths:
95
- # Refinement mode (use captured frames as image Parts)
96
- user_prompt = f"""Refine the following Manim CE (0.19.x) code to fix layout/overlap/readability issues.
97
- Keep the math concept and animations intact; make minimal, clear fixes.
98
-
99
- Original code:
100
- {previous_code}
101
-
102
- Notes:
103
- - Use the captured frames to identify overlapping objects or text outside the viewport.
104
- - Adjust positions, font sizes, or layout as needed.
105
- - Return ONLY the full corrected Python code that defines class AutoScene(Scene) and uses 'from manim import *'.
106
- """
107
- contents.append(user_prompt)
108
- for path in frame_paths:
109
- try:
110
- image_bytes = Path(path).read_bytes()
111
- contents.append(types.Part.from_bytes(image_bytes, mime_type="image/png"))
112
- except Exception as e:
113
- print("Skipping frame include:", path, e, file=sys.stderr)
114
- else:
115
- # First generation
116
- contents.append(f"Create AutoScene for: {prompt}")
117
-
118
- response = client.models.generate_content(
119
- model=MODEL,
120
- config=types.GenerateContentConfig(system_instruction=SYSTEM_PROMPT),
121
- contents=contents,
122
- )
123
- code = _clean_code(getattr(response, "text", str(response)))
124
-
125
- if "class AutoScene" not in code:
126
- code = previous_code or DEFAULT_SCENE
127
- return code
128
-
129
- except Exception:
130
- print("LLM error:", file=sys.stderr)
131
- traceback.print_exc()
132
- return previous_code or DEFAULT_SCENE
133
 
134
- def _run_manim(scene_code: str, run_id: Optional[str] = None) -> Tuple[bytes, List[str]]:
135
- """Render scene with Manim, return (mp4_bytes, captured_frame_paths)."""
136
  run_id = run_id or str(uuid.uuid4())[:8]
137
  work = RUNS / run_id; work.mkdir(parents=True, exist_ok=True)
138
  media = work / "media"; media.mkdir(parents=True, exist_ok=True)
139
  scene_path = work / "scene.py"
140
 
141
- # Inject FRAME_DIR so user code can save frames there
142
- scene_with_path = f"""
143
- import os
144
- from pathlib import Path
145
- FRAME_DIR = Path(r"{work.resolve()}")
146
- {scene_code}
147
- """
148
- scene_path.write_text(scene_with_path, encoding="utf-8")
149
 
150
  env = os.environ.copy()
151
  env["PYTHONPATH"] = str(work)
@@ -158,27 +78,42 @@ FRAME_DIR = Path(r"{work.resolve()}")
158
  ]
159
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
160
  if proc.returncode != 0:
161
- print("Manim stdout/stderr:\n", proc.stdout, file=sys.stderr)
162
  raise RuntimeError("Manim render failed")
163
 
164
- # Find output video
165
- mp4_path = None
166
  for p in media.rglob(f"{run_id}.mp4"):
167
- mp4_path = p; break
168
- if not mp4_path:
169
  for p in media.rglob("*.mp4"):
170
- mp4_path = p; break
171
- if not mp4_path:
172
  raise RuntimeError("Rendered video not found")
173
 
174
- mp4_bytes = Path(mp4_path).read_bytes()
175
 
176
- # Gather any saved frames by the scene
177
- frames = [str(p) for p in work.rglob("*.png")]
178
- return mp4_bytes, frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  def llm_refine_from_error(previous_code: str, error_message: str) -> str:
181
- """Refine code using error text if Manim failed."""
182
  if not client:
183
  return previous_code or DEFAULT_SCENE
184
  try:
@@ -187,14 +122,14 @@ def llm_refine_from_error(previous_code: str, error_message: str) -> str:
187
  Code:
188
  {previous_code}
189
 
190
- Error message / stack trace:
191
  {error_message}
192
 
193
  Task:
194
  - Fix the bug while preserving the math logic and animations.
195
- - Common mistakes: using .animate inside layout methods, invalid method chaining, etc.
196
  - Ensure the corrected code defines exactly one class AutoScene(Scene).
197
- - Return ONLY the corrected Python code.
198
  """
199
  response = client.models.generate_content(
200
  model=MODEL,
@@ -206,45 +141,26 @@ Task:
206
  return previous_code or DEFAULT_SCENE
207
  return code
208
  except Exception:
209
- print("LLM error during error refinement:", file=sys.stderr)
210
  traceback.print_exc()
211
  return previous_code or DEFAULT_SCENE
212
 
213
- def refine_loop(prompt: str, max_iters: int = 2, max_error_refines: int = 1) -> bytes:
214
- """Pipeline: generate → render → visual refine (frames) → final render; returns final MP4 bytes."""
215
  code = llm_generate_manim_code(prompt)
216
- last_video_bytes: Optional[bytes] = None
217
-
218
- for i in range(max_iters):
219
- try:
220
- video_bytes, frames = _run_manim(code, run_id=f"iter{i}")
221
- last_video_bytes = video_bytes
222
- except Exception:
223
- print("Render error (iter):", file=sys.stderr)
224
- traceback.print_exc()
225
-
226
- if i < max_error_refines:
227
- error_msg = traceback.format_exc()
228
- code = llm_refine_from_error(previous_code=code, error_message=error_msg)
229
- continue
230
- else:
231
- break
232
-
233
- if not frames:
234
- print("No captured frames detected; stopping refinement loop.")
235
- break
236
-
237
- # Visual refinement with frames
238
- code = llm_generate_manim_code(prompt, previous_code=code, frame_paths=frames)
239
-
240
- # Final render
241
  try:
242
- final_video, _ = _run_manim(code, run_id="final")
243
- return final_video
244
  except Exception:
245
- return last_video_bytes or b""
 
 
 
246
 
247
- # ---------- API models & routes ----------
 
 
 
 
248
  class PromptIn(BaseModel):
249
  prompt: str
250
 
@@ -254,16 +170,14 @@ def health():
254
 
255
  @app.post("/generate-and-render")
256
  def generate_and_render(inp: PromptIn):
257
- """
258
- Send: {"prompt": "..."} -> Returns: MP4 bytes (video/mp4)
259
- """
260
  if not inp.prompt or not inp.prompt.strip():
261
  raise HTTPException(400, "Missing prompt")
262
- video = refine_loop(inp.prompt.strip(), max_iters=2, max_error_refines=1)
263
- if not video:
264
- raise HTTPException(500, "Failed to produce video")
 
265
  return Response(
266
- content=video,
267
  media_type="video/mp4",
268
  headers={"Content-Disposition": 'inline; filename="result.mp4"'}
269
  )
 
1
+ import os, re, uuid, subprocess, sys, traceback
2
  from pathlib import Path
3
+ from typing import Optional, Tuple
4
 
5
  from fastapi import FastAPI, HTTPException, Response
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from pydantic import BaseModel
8
 
9
+ # Optional .env for local testing
10
  from dotenv import load_dotenv
11
  load_dotenv()
12
 
13
+ # -------- Gemini (same SDK style as your Flask app) --------
14
  from google import genai
15
  from google.genai import types
16
 
17
  API_KEY = os.getenv("GEMINI_API_KEY", "")
18
+ MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-pro")
19
+ PORT = int(os.getenv("PORT", "7860"))
20
 
 
21
  client = genai.Client(api_key=API_KEY) if API_KEY else None
22
 
23
+ # -------- FastAPI app --------
24
+ app = FastAPI(title="Manim Render API (error-refine only)")
25
  app.add_middleware(
26
  CORSMiddleware,
27
  allow_origins=["*"], # tighten in prod
 
29
  allow_headers=["*"],
30
  )
31
 
32
+ RUNS = Path("runs"); RUNS.mkdir(parents=True, exist_ok=True)
 
33
 
34
+ SYSTEM_PROMPT = """You are a Manim CE (0.19.x) code generator/refiner.
35
  Return ONLY valid Python code (no backticks, no prose).
36
  Define exactly one class: AutoScene(Scene).
37
+ Keep it short (preferably ≤ ~60 s) and quickly renderable.
38
 
39
  Use: from manim import *
40
  Allowed imports: manim, math, numpy.
41
+ Forbidden: os, subprocess, sys, requests, pathlib, socket, shutil, psutil, any file/network/OS access.
 
 
 
 
 
 
 
 
 
42
  """
43
 
44
  DEFAULT_SCENE = """from manim import *
45
 
46
  class AutoScene(Scene):
47
  def construct(self):
48
+ t = Text("Hello from Manim").scale(1)
49
  self.play(Write(t))
 
 
50
  self.wait(0.5)
51
  """
52
 
53
+ # ---------------- helpers ----------------
54
  def _clean_code(text: str) -> str:
 
55
  if not text:
56
  return ""
57
+ # strip code fences if present
58
+ return re.sub(r"^```(?:python)?\\n|```$", "", text, flags=re.MULTILINE).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ def _run_manim(scene_code: str, run_id: Optional[str] = None) -> bytes:
61
+ """Render with Manim (low quality, fast) and return MP4 bytes."""
62
  run_id = run_id or str(uuid.uuid4())[:8]
63
  work = RUNS / run_id; work.mkdir(parents=True, exist_ok=True)
64
  media = work / "media"; media.mkdir(parents=True, exist_ok=True)
65
  scene_path = work / "scene.py"
66
 
67
+ # Write scene code
68
+ scene_path.write_text(scene_code, encoding="utf-8")
 
 
 
 
 
 
69
 
70
  env = os.environ.copy()
71
  env["PYTHONPATH"] = str(work)
 
78
  ]
79
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
80
  if proc.returncode != 0:
81
+ print("Manim stdout/stderr:\\n", proc.stdout, file=sys.stderr)
82
  raise RuntimeError("Manim render failed")
83
 
84
+ # Locate output mp4
85
+ mp4 = None
86
  for p in media.rglob(f"{run_id}.mp4"):
87
+ mp4 = p; break
88
+ if not mp4:
89
  for p in media.rglob("*.mp4"):
90
+ mp4 = p; break
91
+ if not mp4:
92
  raise RuntimeError("Rendered video not found")
93
 
94
+ return mp4.read_bytes()
95
 
96
+ def llm_generate_manim_code(prompt: str, previous_code: Optional[str] = None) -> str:
97
+ """First-pass generation (no visual refinement)."""
98
+ if not client:
99
+ return DEFAULT_SCENE
100
+ try:
101
+ response = client.models.generate_content(
102
+ model=MODEL,
103
+ config=types.GenerateContentConfig(system_instruction=SYSTEM_PROMPT),
104
+ contents=f"Create AutoScene for: {prompt}",
105
+ )
106
+ code = _clean_code(getattr(response, "text", str(response)))
107
+ if "class AutoScene" not in code:
108
+ code = previous_code or DEFAULT_SCENE
109
+ return code
110
+ except Exception:
111
+ print("LLM generate error:", file=sys.stderr)
112
+ traceback.print_exc()
113
+ return previous_code or DEFAULT_SCENE
114
 
115
  def llm_refine_from_error(previous_code: str, error_message: str) -> str:
116
+ """Only used when Manim fails; no frames / visual step."""
117
  if not client:
118
  return previous_code or DEFAULT_SCENE
119
  try:
 
122
  Code:
123
  {previous_code}
124
 
125
+ Error / stack trace:
126
  {error_message}
127
 
128
  Task:
129
  - Fix the bug while preserving the math logic and animations.
130
+ - Common mistakes: invalid method chaining, wrong .animate usage, bad mobject usage, etc.
131
  - Ensure the corrected code defines exactly one class AutoScene(Scene).
132
+ - Return ONLY the corrected Python code (no backticks).
133
  """
134
  response = client.models.generate_content(
135
  model=MODEL,
 
141
  return previous_code or DEFAULT_SCENE
142
  return code
143
  except Exception:
144
+ print("LLM refine error:", file=sys.stderr)
145
  traceback.print_exc()
146
  return previous_code or DEFAULT_SCENE
147
 
148
+ def refine_loop(prompt: str, max_error_refines: int = 1) -> bytes:
149
+ """Generate → render; if render fails, LLM-refine once → re-render."""
150
  code = llm_generate_manim_code(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  try:
152
+ return _run_manim(code, run_id="iter0")
 
153
  except Exception:
154
+ print("Render failed (iter0), attempting error-based refinement...", file=sys.stderr)
155
+ traceback.print_exc()
156
+ if max_error_refines <= 0:
157
+ raise
158
 
159
+ refined = llm_refine_from_error(previous_code=code, error_message=traceback.format_exc())
160
+ # Try a second render
161
+ return _run_manim(refined, run_id="iter1")
162
+
163
+ # ---------------- API ----------------
164
  class PromptIn(BaseModel):
165
  prompt: str
166
 
 
170
 
171
  @app.post("/generate-and-render")
172
  def generate_and_render(inp: PromptIn):
 
 
 
173
  if not inp.prompt or not inp.prompt.strip():
174
  raise HTTPException(400, "Missing prompt")
175
+ try:
176
+ mp4 = refine_loop(inp.prompt.strip(), max_error_refines=1)
177
+ except Exception:
178
+ raise HTTPException(500, "Failed to produce video after refinement")
179
  return Response(
180
+ content=mp4,
181
  media_type="video/mp4",
182
  headers={"Content-Disposition": 'inline; filename="result.mp4"'}
183
  )