devendergarg14 commited on
Commit
05c2df9
·
verified ·
1 Parent(s): f9aff34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -35
app.py CHANGED
@@ -3,55 +3,101 @@ import os
3
  import subprocess
4
  import re
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."""
@@ -73,17 +119,6 @@ def get_resolution_flags(orientation, quality):
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)
@@ -97,8 +132,7 @@ def run_manim(code_str, orientation, quality, timeout):
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')
@@ -124,30 +158,36 @@ def run_manim(code_str, orientation, quality, timeout):
124
  return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
125
 
126
  # ---------------------------------------------------------
127
- # 3. Main API Function
128
  # ---------------------------------------------------------
129
 
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:
147
  return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
148
 
149
  # ---------------------------------------------------------
150
- # 4. Gradio Interface (API Definition)
151
  # ---------------------------------------------------------
152
 
153
  DEFAULT_CODE = """from manim import *
@@ -165,6 +205,7 @@ with gr.Blocks(title="Manim Render API") as demo:
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)
 
3
  import subprocess
4
  import re
5
  import time
 
6
  import shutil
7
 
8
  # ---------------------------------------------------------
9
+ # 1. Helper Functions
10
  # ---------------------------------------------------------
11
 
12
+ def modify_animation_times(code: str, factor: float = 1.0, for_precheck: bool = False) -> str:
13
+ """
14
+ Modifies wait() and run_time in the code.
15
+
16
+ 1. If for_precheck=True:
17
+ - Sets all self.wait(...) to self.wait(0).
18
+ - Sets all run_time=... to run_time=0.
19
+ - This makes heavy simulation loops execute instantly during the 'manim -s' check.
20
+
21
+ 2. If for_precheck=False (Preview Mode):
22
+ - Scales animation speed by 'factor'.
23
+ - Enforces a minimum run_time of 0.1s to prevent rendering crashes.
24
+ """
25
+
26
+ # --- MODE 1: PRE-CHECK (Zero Latency) ---
27
+ if for_precheck:
28
+ print("⚡ Optimizing code for Pre-check (Zero Latency)...", flush=True)
29
+ # Regex: Find self.wait(ANYTHING) and replace with self.wait(0)
30
+ # We use [^)]* to match any content inside the parenthesis (numbers, math, variables)
31
+ code = re.sub(r"self\.wait\s*\([^)]*\)", "self.wait(0)", code)
32
+
33
+ # Regex: Find run_time=ANYTHING (until a comma or parenthesis) and set to 0
34
+ code = re.sub(r"run_time\s*=\s*[^,)]+", "run_time=0", code)
35
+ return code
36
+
37
+ # --- MODE 2: PREVIEW SCALING (Safe Speedup) ---
38
+ print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
39
+ MIN_RUN_TIME = 0.1
40
+
41
+ def scale_match(m, is_wait):
42
+ # m.group(1) is the prefix (e.g. "run_time=")
43
+ # m.group(2) is the number value
44
+ try:
45
+ val = float(m.group(2))
46
+ new_val = val * factor
47
+ # If it's a wait, we can go low. If it's an animation, keep 0.1 safety.
48
+ final_val = new_val if is_wait else max(new_val, MIN_RUN_TIME)
49
+ return f"{m.group(1)}{final_val:.3f}"
50
+ except ValueError:
51
+ return m.group(0)
52
+
53
+ # Scale run_time=1.5 -> run_time=0.75
54
+ code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale_match(m, False), code)
55
+ # Scale self.wait(2) -> self.wait(1.0)
56
+ code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale_match(m, True), code)
57
+
58
+ return code
59
+
60
  def run_manim_pre_check(code_str: str) -> (bool, str):
61
  """
62
  Runs Manim with the '-s' flag to perform a fast, reliable pre-check.
63
+ Uses the 'Speed Hack' (modify_animation_times) to ensure heavy loops
64
+ don't cause a timeout.
 
65
  """
66
  print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
67
 
68
+ # Apply the speed hack: waits become 0s
69
+ fast_code = modify_animation_times(code_str, for_precheck=True)
70
+
71
+ # Write the modified code to a temporary file
72
  with open("scene_pre_check.py", "w", encoding="utf-8") as f:
73
+ f.write(fast_code)
74
 
 
75
  cmd = [
76
  "manim",
77
  "-ql",
78
  "--progress_bar", "none",
79
  "--disable_caching",
80
  "scene_pre_check.py", "GenScene",
81
+ "-s", # Save last frame only (no video encoding)
82
+ "-o", "pre_check_output"
83
  ]
84
 
85
  try:
86
+ # 30s is now plenty of time because we removed all waits
87
  process = subprocess.run(cmd, capture_output=True, timeout=30, check=False)
88
 
89
  if process.returncode == 0:
90
  print("✅ Pre-check passed. Code is valid.", flush=True)
91
  return True, "Pre-check successful."
92
  else:
 
93
  stderr_log = process.stderr.decode('utf-8', 'ignore')
94
  print(f"❌ Pre-check failed.\n{stderr_log}", flush=True)
95
+ return False, f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
 
 
96
 
97
  except subprocess.TimeoutExpired:
98
  print("⌛ Pre-check timed out.", flush=True)
99
+ # Even with the speed hack, if it times out, it's likely a true infinite loop (while True)
100
+ return False, "❌ ERROR: The pre-check timed out. The code likely contains an infinite loop."
101
 
102
  def cleanup_media_directory():
103
  """Wipes the media directory to prevent caching issues."""
 
119
  width, height = make_even(base_h), make_even(base_h * (16/9))
120
  return f"{width},{height}"
121
 
 
 
 
 
 
 
 
 
 
 
 
122
  def run_manim(code_str, orientation, quality, timeout):
123
  timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
124
  print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
 
132
  frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
133
  cmd = ["manim", "--resolution", res_str, *frame_rate_flags, "--disable_caching",
134
  "--progress_bar", "none", "scene.py", "GenScene", "-o", output_filename]
135
+
 
136
  try:
137
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
138
  stdout_log, stderr_log = process.stdout.decode('utf-8', 'ignore'), process.stderr.decode('utf-8', 'ignore')
 
158
  return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
159
 
160
  # ---------------------------------------------------------
161
+ # 2. Main API Function
162
  # ---------------------------------------------------------
163
 
164
  def render_video_from_code(code, orientation, quality, timeout, preview_factor):
165
  """Renders a video from a given Manim code string."""
166
  try:
167
+ # --- 1. Pre-Check (With Speed Hack) ---
168
  is_valid, logs = run_manim_pre_check(code)
169
  if not is_valid:
170
  return None, logs, gr.Button(visible=True)
171
+
 
172
  cleanup_media_directory()
173
  if not code or "from manim import" not in code:
174
  return None, "Error: No valid code to render.", gr.Button(visible=False)
175
 
176
+ # --- 2. Prepare Code for Render ---
177
+ # If quality is Preview, we scale time. If not, we use original code.
178
+ if quality == "Preview (360p)":
179
+ code_to_render = modify_animation_times(code, factor=float(preview_factor) or 0.5, for_precheck=False)
180
+ else:
181
+ code_to_render = code
182
+
183
+ # --- 3. Execute Render ---
184
  video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
185
  return video_path, logs, gr.Button(visible=not success)
186
  except Exception as e:
187
  return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
188
 
189
  # ---------------------------------------------------------
190
+ # 3. Gradio Interface
191
  # ---------------------------------------------------------
192
 
193
  DEFAULT_CODE = """from manim import *
 
205
  quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
206
  timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
207
  preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
208
+
209
  video_output = gr.Video(label="Result")
210
  status_output = gr.Textbox(label="Status/Logs")
211
  fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop", visible=True)