devendergarg14 commited on
Commit
a1e136d
·
verified ·
1 Parent(s): e7f450f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -166
app.py CHANGED
@@ -5,206 +5,121 @@ import re
5
  import time
6
  import glob
7
  import shutil
8
- # --- START: New Imports for Pyflakes Integration ---
9
- from pyflakes.api import check
10
- from pyflakes.reporter import Reporter
11
- import io
12
- # --- END: New Imports ---
13
 
14
  # ---------------------------------------------------------
15
  # 1. Helper Functions (Dependencies for Rendering)
16
  # ---------------------------------------------------------
17
 
18
- class ErrorReporter(Reporter):
19
- """Custom reporter to capture pyflakes errors into a list."""
20
- def __init__(self):
21
- self.errors = []
22
- # Suppress the default print-to-stdout behavior
23
- super().__init__(io.StringIO(), io.StringIO())
24
-
25
- def unexpectedError(self, filename, msg):
26
- self.errors.append(f"Unexpected Error in {filename}: {msg}")
27
-
28
- def syntaxError(self, filename, msg, lineno, offset, text):
29
- self.errors.append(f"Syntax Error in {filename} (Line {lineno}): {msg}")
30
-
31
- def flake(self, message):
32
- self.errors.append(str(message))
33
-
34
- def check_code_with_pyflakes(code_str: str) -> str:
35
  """
36
- Analyzes a string of Python code using pyflakes and returns a formatted error report.
37
- This version uses a robust blocklist to filter out all warnings related to star imports.
38
- Returns an empty string if no definite errors are found.
 
39
  """
40
- reporter = ErrorReporter()
41
- check(code_str, "scene.py", reporter=reporter)
42
 
43
- # A blocklist of warning substrings to completely ignore.
44
- IGNORE_SUBSTRINGS = [
45
- "may be undefined, or defined from star imports",
46
- "undefined names"
 
 
 
 
 
 
 
 
 
47
  ]
48
-
49
- filtered_errors = []
50
- for err in reporter.errors:
51
- err_str = str(err)
52
- # If the error message contains any of the substrings in our blocklist, we ignore it.
53
- if not any(sub in err_str for sub in IGNORE_SUBSTRINGS):
54
- filtered_errors.append(err_str)
55
 
56
- if not filtered_errors:
57
- return "" # No real errors found
 
58
 
59
- error_report = "Static analysis found potential errors:\n"
60
- for err in filtered_errors:
61
- # Use a simpler, more robust formatting logic
62
- parts = err.split(':', 2)
63
- if len(parts) == 3:
64
- try:
65
- line_num = parts[1]
66
- message = parts[2].strip()
67
- error_report += f"- Line {line_num}: {message}\n"
68
- except:
69
- error_report += f"- {err}\n" # Fallback for unexpected format
70
  else:
71
- error_report += f"- {err}\n" # Fallback
 
 
 
 
 
72
 
73
- return error_report
 
 
74
 
75
  def cleanup_media_directory():
76
  """Wipes the media directory to prevent caching issues."""
77
  media_dir = 'media'
78
  if os.path.exists(media_dir):
79
- try:
80
- shutil.rmtree(media_dir)
81
- except OSError as e:
82
- print(f"⚠️ Warning during cleanup: {e}", flush=True)
83
 
84
  def make_even(n):
85
  n = int(n)
86
  return n if n % 2 == 0 else n + 1
87
 
88
  def get_resolution_flags(orientation, quality):
89
- qual_map = {
90
- "Preview (360p)": 360, "480p": 480, "720p": 720, "1080p": 1080, "4k": 2160
91
- }
92
  base_h = qual_map.get(quality, 1080)
93
  if orientation == "Landscape (16:9)":
94
- width = make_even(base_h * (16/9)); height = make_even(base_h)
95
- else: # Portrait (9:16)
96
- width = make_even(base_h); height = make_even(base_h * (16/9))
97
  return f"{width},{height}"
98
 
99
  def scale_animation_times(code, factor):
100
- """
101
- Scales run_time and wait() calls, enforcing a minimum run_time to prevent render errors.
102
- """
103
  print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
104
- MIN_RUN_TIME = 0.1 # A safe minimum duration for any animation clip.
105
-
106
- def scale_runtime(m):
107
- new_time = max(float(m.group(2)) * factor, MIN_RUN_TIME)
108
- return f"{m.group(1)}{new_time}"
109
-
110
- def scale_wait(m):
111
- new_time = float(m.group(2)) * factor
112
- return f"{m.group(1)}{new_time}"
113
-
114
- code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", scale_runtime, code)
115
- code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", scale_wait, code)
116
  return code
117
 
118
  def run_manim(code_str, orientation, quality, timeout):
119
  timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
120
- print(f"🎬 Starting Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
121
 
122
- # Write code to file
123
- with open("scene.py", "w", encoding="utf-8") as f:
124
- f.write(code_str)
125
 
126
  timestamp = int(time.time())
127
  output_filename = f"video_{timestamp}.mp4"
128
  res_str = get_resolution_flags(orientation, quality)
129
 
130
- # Manim command
131
  frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
132
- cmd = [
133
- "manim",
134
- "--resolution", res_str,
135
- *frame_rate_flags,
136
- "--disable_caching",
137
- "--progress_bar", "none",
138
- "scene.py", "GenScene",
139
- "-o", output_filename
140
- ]
141
  print(f"⚙️ Running command: {' '.join(cmd)}", flush=True)
142
 
143
  try:
144
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
145
- stdout_log = process.stdout.decode('utf-8', 'ignore')
146
- stderr_log = process.stderr.decode('utf-8', 'ignore')
147
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
148
 
149
  if process.returncode != 0:
150
  print(f"❌ Render Failed.\n{stderr_log}", flush=True)
151
  return None, f"⚠️ ERROR: Manim failed to render.\n{full_logs}", False
152
-
153
  except subprocess.TimeoutExpired as e:
154
  print(f"⌛ Render timed out after {timeout_sec} seconds.", flush=True)
155
- stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
156
- stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
157
- timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
158
- return None, f"❌ ERROR: Timed out.\n{timeout_logs}", False
159
 
160
- # ---------------------------------------------------------
161
- # 2. LOCATE OUTPUT FILE
162
- # ---------------------------------------------------------
163
-
164
- # Strategy A: Look for the expected Video File (.mp4)
165
  media_video_base = os.path.join("media", "videos", "scene")
166
- found_video_path = None
167
-
168
  if os.path.exists(media_video_base):
169
  for root, _, files in os.walk(media_video_base):
170
  if output_filename in files:
171
  found_video_path = os.path.join(root, output_filename)
172
- break
173
-
174
- if found_video_path:
175
- print(f"✅ Video Render Success: {found_video_path}", flush=True)
176
- return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", True
177
-
178
- # Strategy B: Look for a Static Image (.png) and convert to Video
179
- media_image_base = os.path.join("media", "images", "scene")
180
- expected_image_name = output_filename.replace('.mp4', '.png')
181
- found_image_path = None
182
-
183
- if os.path.exists(media_image_base):
184
- for root, _, files in os.walk(media_image_base):
185
- # Manim Community v0.18.0 changed naming for -o. Let's find the single png.
186
- png_files = [f for f in files if f.endswith(".png")]
187
- if len(png_files) == 1:
188
- found_image_path = os.path.join(root, png_files[0])
189
- break
190
-
191
- if found_image_path:
192
- print(f"🖼️ Static scene detected (0 animations). converting image to video: {found_image_path}", flush=True)
193
- converted_video_path = os.path.join(os.path.dirname(found_image_path), output_filename)
194
- ffmpeg_cmd = [
195
- "ffmpeg", "-y", "-loop", "1",
196
- "-i", found_image_path,
197
- "-t", "1",
198
- "-c:v", "libx264",
199
- "-pix_fmt", "yuv420p",
200
- converted_video_path
201
- ]
202
- ffmpeg_proc = subprocess.run(ffmpeg_cmd, capture_output=True, check=False)
203
- if ffmpeg_proc.returncode == 0 and os.path.exists(converted_video_path):
204
- return converted_video_path, f"✅ Static Scene Rendered (Converted to 1s Video)\n\n{full_logs}", True
205
- else:
206
- print(f"❌ FFMPEG Conversion Failed: {ffmpeg_proc.stderr.decode()}", flush=True)
207
-
208
  print(f"❌ Final output file '{output_filename}' not found.", flush=True)
209
  return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
210
 
@@ -215,30 +130,17 @@ def run_manim(code_str, orientation, quality, timeout):
215
  def render_video_from_code(code, orientation, quality, timeout, preview_factor):
216
  """Renders a video from a given Manim code string."""
217
  try:
218
- # --- START: Integrated Pyflakes Pre-Check ---
219
- print("🕵️ Running static analysis with pyflakes...", flush=True)
220
- static_errors = check_code_with_pyflakes(code)
221
- if static_errors:
222
- print(f"❌ Static analysis failed:\n{static_errors}", flush=True)
223
- # Return the error report immediately without trying to render
224
- return None, static_errors, gr.Button(visible=True)
225
- print("✅ Static analysis passed.", flush=True)
226
- # --- END: Integrated Pyflakes Pre-Check ---
227
 
228
  cleanup_media_directory()
229
-
230
  if not code or "from manim import" not in code:
231
  return None, "Error: No valid code to render.", gr.Button(visible=False)
232
 
233
- code_to_render = code
234
- if quality == "Preview (360p)":
235
- try:
236
- factor = float(preview_factor)
237
- if factor <= 0: factor = 0.5
238
- except (ValueError, TypeError):
239
- factor = 0.5
240
- code_to_render = scale_animation_times(code, factor)
241
-
242
  video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
243
  return video_path, logs, gr.Button(visible=not success)
244
  except Exception as e:
@@ -253,22 +155,19 @@ DEFAULT_CODE = """from manim import *
253
  class GenScene(Scene):
254
  def construct(self):
255
  c = Circle(color=BLUE, fill_opacity=0.5)
256
- self.play(Create(c)) # Animation adds runtime
257
- self.wait(1) # Pause adds runtime
258
  """
259
 
260
  with gr.Blocks(title="Manim Render API") as demo:
261
- # Components are now visible for direct interaction
262
  code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE, visible=True)
263
  orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation", visible=True)
264
  quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
265
  timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
266
  preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
267
-
268
  video_output = gr.Video(label="Result")
269
  status_output = gr.Textbox(label="Status/Logs")
270
  fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop", visible=True)
271
-
272
  render_btn = gr.Button("Render")
273
 
274
  render_btn.click(
 
5
  import time
6
  import glob
7
  import shutil
 
 
 
 
 
8
 
9
  # ---------------------------------------------------------
10
  # 1. Helper Functions (Dependencies for Rendering)
11
  # ---------------------------------------------------------
12
 
13
+ def run_manim_pre_check(code_str: str) -> (bool, str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
+ Runs Manim with the '-s' flag to perform a fast, reliable pre-check.
16
+ This executes the Python code without rendering the full video.
17
+ Returns (True, "Success") if the code is valid.
18
+ Returns (False, "Error Log") if any Python error (like NameError) occurs.
19
  """
20
+ print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
 
21
 
22
+ # Write the code to a temporary file for the pre-check
23
+ with open("scene_pre_check.py", "w", encoding="utf-8") as f:
24
+ f.write(code_str)
25
+
26
+ # Use low quality and '-s' flag for maximum speed. Output is discarded.
27
+ cmd = [
28
+ "manim",
29
+ "-ql",
30
+ "--progress_bar", "none",
31
+ "--disable_caching",
32
+ "scene_pre_check.py", "GenScene",
33
+ "-s", # This is the key: save last frame only.
34
+ "-o", "pre_check_output" # Output is temporary and will be overwritten
35
  ]
 
 
 
 
 
 
 
36
 
37
+ try:
38
+ # We don't need a long timeout for a pre-check
39
+ process = subprocess.run(cmd, capture_output=True, timeout=30, check=False)
40
 
41
+ if process.returncode == 0:
42
+ print("✅ Pre-check passed. Code is valid.", flush=True)
43
+ return True, "Pre-check successful."
 
 
 
 
 
 
 
 
44
  else:
45
+ # An error occurred, capture the logs
46
+ stderr_log = process.stderr.decode('utf-8', 'ignore')
47
+ print(f"❌ Pre-check failed.\n{stderr_log}", flush=True)
48
+ # Format a clean error message for the user
49
+ error_message = f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
50
+ return False, error_message
51
 
52
+ except subprocess.TimeoutExpired:
53
+ print("⌛ Pre-check timed out.", flush=True)
54
+ return False, "❌ ERROR: The pre-check timed out. The code may contain an infinite loop."
55
 
56
  def cleanup_media_directory():
57
  """Wipes the media directory to prevent caching issues."""
58
  media_dir = 'media'
59
  if os.path.exists(media_dir):
60
+ try: shutil.rmtree(media_dir)
61
+ except OSError as e: print(f"⚠️ Warning during cleanup: {e}", flush=True)
 
 
62
 
63
  def make_even(n):
64
  n = int(n)
65
  return n if n % 2 == 0 else n + 1
66
 
67
  def get_resolution_flags(orientation, quality):
68
+ qual_map = {"Preview (360p)": 360, "480p": 480, "720p": 720, "1080p": 1080, "4k": 2160}
 
 
69
  base_h = qual_map.get(quality, 1080)
70
  if orientation == "Landscape (16:9)":
71
+ width, height = make_even(base_h * (16/9)), make_even(base_h)
72
+ else:
73
+ width, height = make_even(base_h), make_even(base_h * (16/9))
74
  return f"{width},{height}"
75
 
76
  def scale_animation_times(code, factor):
77
+ """Scales run_time and wait() calls."""
 
 
78
  print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
79
+ MIN_RUN_TIME = 0.1
80
+ def scale(m, is_wait):
81
+ val = float(m.group(2)) * factor
82
+ return f"{m.group(1)}{val if is_wait else max(val, MIN_RUN_TIME)}"
83
+ code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale(m, False), code)
84
+ code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale(m, True), code)
 
 
 
 
 
 
85
  return code
86
 
87
  def run_manim(code_str, orientation, quality, timeout):
88
  timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
89
+ print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
90
 
91
+ with open("scene.py", "w", encoding="utf-8") as f: f.write(code_str)
 
 
92
 
93
  timestamp = int(time.time())
94
  output_filename = f"video_{timestamp}.mp4"
95
  res_str = get_resolution_flags(orientation, quality)
96
 
 
97
  frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
98
+ cmd = ["manim", "--resolution", res_str, *frame_rate_flags, "--disable_caching",
99
+ "--progress_bar", "none", "scene.py", "GenScene", "-o", output_filename]
 
 
 
 
 
 
 
100
  print(f"⚙️ Running command: {' '.join(cmd)}", flush=True)
101
 
102
  try:
103
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
104
+ stdout_log, stderr_log = process.stdout.decode('utf-8', 'ignore'), process.stderr.decode('utf-8', 'ignore')
 
105
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
106
 
107
  if process.returncode != 0:
108
  print(f"❌ Render Failed.\n{stderr_log}", flush=True)
109
  return None, f"⚠️ ERROR: Manim failed to render.\n{full_logs}", False
 
110
  except subprocess.TimeoutExpired as e:
111
  print(f"⌛ Render timed out after {timeout_sec} seconds.", flush=True)
112
+ stdout_log, stderr_log = (e.stdout.decode('utf-8', 'ignore') if e.stdout else ""), (e.stderr.decode('utf-8', 'ignore') if e.stderr else "")
113
+ return None, f"❌ ERROR: Timed out.\n--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}", False
 
 
114
 
 
 
 
 
 
115
  media_video_base = os.path.join("media", "videos", "scene")
 
 
116
  if os.path.exists(media_video_base):
117
  for root, _, files in os.walk(media_video_base):
118
  if output_filename in files:
119
  found_video_path = os.path.join(root, output_filename)
120
+ print(f"✅ Video Render Success: {found_video_path}", flush=True)
121
+ return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", True
122
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  print(f"❌ Final output file '{output_filename}' not found.", flush=True)
124
  return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
125
 
 
130
  def render_video_from_code(code, orientation, quality, timeout, preview_factor):
131
  """Renders a video from a given Manim code string."""
132
  try:
133
+ # --- START: New '-s' Pre-Check ---
134
+ is_valid, logs = run_manim_pre_check(code)
135
+ if not is_valid:
136
+ return None, logs, gr.Button(visible=True)
137
+ # --- END: New '-s' Pre-Check ---
 
 
 
 
138
 
139
  cleanup_media_directory()
 
140
  if not code or "from manim import" not in code:
141
  return None, "Error: No valid code to render.", gr.Button(visible=False)
142
 
143
+ code_to_render = code if quality != "Preview (360p)" else scale_animation_times(code, float(preview_factor) or 0.5)
 
 
 
 
 
 
 
 
144
  video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
145
  return video_path, logs, gr.Button(visible=not success)
146
  except Exception as e:
 
155
  class GenScene(Scene):
156
  def construct(self):
157
  c = Circle(color=BLUE, fill_opacity=0.5)
158
+ self.play(Create(c))
159
+ self.wait(1)
160
  """
161
 
162
  with gr.Blocks(title="Manim Render API") as demo:
 
163
  code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE, visible=True)
164
  orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation", visible=True)
165
  quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
166
  timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
167
  preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
 
168
  video_output = gr.Video(label="Result")
169
  status_output = gr.Textbox(label="Status/Logs")
170
  fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop", visible=True)
 
171
  render_btn = gr.Button("Render")
172
 
173
  render_btn.click(