devendergarg14 commited on
Commit
12e6c81
·
verified ·
1 Parent(s): eb2b92e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -70
app.py CHANGED
@@ -4,6 +4,7 @@ import subprocess
4
  import re
5
  import time
6
  import shutil
 
7
 
8
  # ---------------------------------------------------------
9
  # 1. Helper Functions
@@ -12,107 +13,67 @@ import shutil
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 self.wait(...) to self.wait(0).
18
- - Sets run_time=... to run_time=0.01.
19
- - This attempts to make the validation run instantly.
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
  code = re.sub(r"self\.wait\s*\([^)]*\)", "self.wait(0)", code)
31
-
32
- # Regex: Find run_time=ANYTHING and set to 0.01 to avoid "run_time <= 0" error
33
  code = re.sub(r"run_time\s*=\s*[^,)]+", "run_time=0.01", code)
34
  return code
35
 
36
- # --- MODE 2: PREVIEW SCALING (Safe Speedup) ---
37
  print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
38
  MIN_RUN_TIME = 0.1
39
 
40
  def scale_match(m, is_wait):
41
- # m.group(1) is the prefix (e.g. "run_time=")
42
- # m.group(2) is the number value
43
  try:
44
  val = float(m.group(2))
45
  new_val = val * factor
46
- # If it's a wait, we can go low. If it's an animation, keep 0.1 safety.
47
  final_val = new_val if is_wait else max(new_val, MIN_RUN_TIME)
48
  return f"{m.group(1)}{final_val:.3f}"
49
  except ValueError:
50
  return m.group(0)
51
 
52
- # Scale run_time=1.5 -> run_time=0.75
53
  code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale_match(m, False), code)
54
- # Scale self.wait(2) -> self.wait(1.0)
55
  code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale_match(m, True), code)
56
-
57
  return code
58
 
59
  def run_manim_pre_check(code_str: str) -> (bool, str):
60
  """
61
- Runs Manim with the '-s' flag to perform a fast pre-check.
62
-
63
- CRITICAL BEHAVIOR:
64
- 1. Tries to run quickly using 'modify_animation_times'.
65
- 2. If it catches a SyntaxError or NameError, it FAILS (Returns False).
66
- 3. If it runs for >30 seconds (Timeout), it PASSES (Returns True), assuming
67
- the code is valid but just computationally heavy.
68
  """
69
  print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
70
-
71
- # Apply the speed hack
72
  fast_code = modify_animation_times(code_str, for_precheck=True)
73
 
74
- # Write to temp file
75
  with open("scene_pre_check.py", "w", encoding="utf-8") as f:
76
  f.write(fast_code)
77
 
78
- cmd = [
79
- "manim",
80
- "-ql",
81
- "--progress_bar", "none",
82
- "--disable_caching",
83
- "scene_pre_check.py", "GenScene",
84
- "-s", # Save last frame only
85
- "-o", "pre_check_output"
86
- ]
87
 
88
  try:
89
- # Run for max 30 seconds
90
  process = subprocess.run(cmd, capture_output=True, timeout=30, check=False)
91
-
92
  if process.returncode == 0:
93
- print("✅ Pre-check passed. Code is valid.", flush=True)
94
  return True, "Pre-check successful."
95
  else:
96
- # Code finished but failed (e.g. Syntax Error)
97
  stderr_log = process.stderr.decode('utf-8', 'ignore')
98
  print(f"❌ Pre-check failed.\n{stderr_log}", flush=True)
99
  return False, f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
100
-
101
  except subprocess.TimeoutExpired:
102
- # Code ran for 30s without crashing. likely valid but heavy.
103
- print(" Pre-check timed out (30s). Assuming code is valid but heavy.", flush=True)
104
- return True, "⚠️ Warning: Pre-check timed out (30s). The code seems valid but is very heavy. Proceeding to full render..."
105
 
106
  def cleanup_media_directory():
107
- """Wipes the media directory to prevent caching issues."""
108
  media_dir = 'media'
109
  if os.path.exists(media_dir):
110
  try: shutil.rmtree(media_dir)
111
- except OSError as e: print(f"⚠️ Warning during cleanup: {e}", flush=True)
112
 
113
  def make_even(n):
114
- n = int(n)
115
- return n if n % 2 == 0 else n + 1
116
 
117
  def get_resolution_flags(orientation, quality):
118
  qual_map = {"Preview (360p)": 360, "480p": 480, "720p": 720, "1080p": 1080, "4k": 2160}
@@ -124,6 +85,11 @@ def get_resolution_flags(orientation, quality):
124
  return f"{width},{height}"
125
 
126
  def run_manim(code_str, orientation, quality, timeout):
 
 
 
 
 
127
  timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
128
  print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
129
 
@@ -132,24 +98,69 @@ def run_manim(code_str, orientation, quality, timeout):
132
  timestamp = int(time.time())
133
  output_filename = f"video_{timestamp}.mp4"
134
  res_str = get_resolution_flags(orientation, quality)
135
-
136
  frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
 
137
  cmd = ["manim", "--resolution", res_str, *frame_rate_flags, "--disable_caching",
138
  "--progress_bar", "none", "scene.py", "GenScene", "-o", output_filename]
139
 
 
 
140
  try:
 
 
141
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
142
- stdout_log, stderr_log = process.stdout.decode('utf-8', 'ignore'), process.stderr.decode('utf-8', 'ignore')
 
 
143
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
144
 
 
145
  if process.returncode != 0:
146
- print(f"❌ Render Failed.\n{stderr_log}", flush=True)
147
- return None, f"⚠️ ERROR: Manim failed to render.\n{full_logs}", False
 
148
  except subprocess.TimeoutExpired as e:
149
- print(f"⌛ Render timed out after {timeout_sec} seconds.", flush=True)
150
- stdout_log, stderr_log = (e.stdout.decode('utf-8', 'ignore') if e.stdout else ""), (e.stderr.decode('utf-8', 'ignore') if e.stderr else "")
151
- return None, f"❌ ERROR: Timed out.\n--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}", False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
 
 
153
  media_video_base = os.path.join("media", "videos", "scene")
154
  if os.path.exists(media_video_base):
155
  for root, _, files in os.walk(media_video_base):
@@ -158,36 +169,27 @@ def run_manim(code_str, orientation, quality, timeout):
158
  print(f"✅ Video Render Success: {found_video_path}", flush=True)
159
  return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", True
160
 
161
- print(f"❌ Final output file '{output_filename}' not found.", flush=True)
162
- return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
163
 
164
  # ---------------------------------------------------------
165
  # 2. Main API Function
166
  # ---------------------------------------------------------
167
 
168
  def render_video_from_code(code, orientation, quality, timeout, preview_factor):
169
- """Renders a video from a given Manim code string."""
170
  try:
171
- # --- 1. Pre-Check (With Speed Hack & Soft Pass) ---
172
  is_valid, logs = run_manim_pre_check(code)
173
-
174
- # If is_valid is False, it means there was a real Error (Syntax, NameError, etc.)
175
  if not is_valid:
176
  return None, logs, gr.Button(visible=True)
177
 
178
- # If is_valid is True, we proceed (even if it timed out)
179
-
180
  cleanup_media_directory()
181
  if not code or "from manim import" not in code:
182
- return None, "Error: No valid code to render.", gr.Button(visible=False)
183
 
184
- # --- 2. Prepare Code for Real Render ---
185
  if quality == "Preview (360p)":
186
  code_to_render = modify_animation_times(code, factor=float(preview_factor) or 0.5, for_precheck=False)
187
  else:
188
  code_to_render = code
189
 
190
- # --- 3. Execute Render ---
191
  video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
192
  return video_path, logs, gr.Button(visible=not success)
193
  except Exception as e:
 
4
  import re
5
  import time
6
  import shutil
7
+ import glob
8
 
9
  # ---------------------------------------------------------
10
  # 1. Helper Functions
 
13
  def modify_animation_times(code: str, factor: float = 1.0, for_precheck: bool = False) -> str:
14
  """
15
  Modifies wait() and run_time in the code.
16
+ 1. Pre-Check: Zero latency (wait(0), run_time=0.01).
17
+ 2. Preview: Scales time by factor.
 
 
 
 
 
 
 
18
  """
 
 
19
  if for_precheck:
20
  print("⚡ Optimizing code for Pre-check (Zero Latency)...", flush=True)
 
21
  code = re.sub(r"self\.wait\s*\([^)]*\)", "self.wait(0)", code)
 
 
22
  code = re.sub(r"run_time\s*=\s*[^,)]+", "run_time=0.01", code)
23
  return code
24
 
 
25
  print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
26
  MIN_RUN_TIME = 0.1
27
 
28
  def scale_match(m, is_wait):
 
 
29
  try:
30
  val = float(m.group(2))
31
  new_val = val * factor
 
32
  final_val = new_val if is_wait else max(new_val, MIN_RUN_TIME)
33
  return f"{m.group(1)}{final_val:.3f}"
34
  except ValueError:
35
  return m.group(0)
36
 
 
37
  code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale_match(m, False), code)
 
38
  code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale_match(m, True), code)
 
39
  return code
40
 
41
  def run_manim_pre_check(code_str: str) -> (bool, str):
42
  """
43
+ Runs Manim with '-s'.
44
+ - Speed Hack applied.
45
+ - Soft Pass: Timeouts return True.
46
+ - Errors return False.
 
 
 
47
  """
48
  print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
 
 
49
  fast_code = modify_animation_times(code_str, for_precheck=True)
50
 
 
51
  with open("scene_pre_check.py", "w", encoding="utf-8") as f:
52
  f.write(fast_code)
53
 
54
+ cmd = ["manim", "-ql", "--progress_bar", "none", "--disable_caching", "scene_pre_check.py", "GenScene", "-s", "-o", "pre_check_output"]
 
 
 
 
 
 
 
 
55
 
56
  try:
 
57
  process = subprocess.run(cmd, capture_output=True, timeout=30, check=False)
 
58
  if process.returncode == 0:
59
+ print("✅ Pre-check passed.", flush=True)
60
  return True, "Pre-check successful."
61
  else:
 
62
  stderr_log = process.stderr.decode('utf-8', 'ignore')
63
  print(f"❌ Pre-check failed.\n{stderr_log}", flush=True)
64
  return False, f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
 
65
  except subprocess.TimeoutExpired:
66
+ print("⌛ Pre-check timed out (30s). Soft Pass.", flush=True)
67
+ return True, "⚠️ Warning: Pre-check timed out. Proceeding to full render..."
 
68
 
69
  def cleanup_media_directory():
 
70
  media_dir = 'media'
71
  if os.path.exists(media_dir):
72
  try: shutil.rmtree(media_dir)
73
+ except OSError: pass
74
 
75
  def make_even(n):
76
+ return int(n) if int(n) % 2 == 0 else int(n) + 1
 
77
 
78
  def get_resolution_flags(orientation, quality):
79
  qual_map = {"Preview (360p)": 360, "480p": 480, "720p": 720, "1080p": 1080, "4k": 2160}
 
85
  return f"{width},{height}"
86
 
87
  def run_manim(code_str, orientation, quality, timeout):
88
+ """
89
+ Executes Manim.
90
+ - RECOVERY: Partial stitching ONLY happens inside 'except subprocess.TimeoutExpired'.
91
+ - ERRORS: Standard errors do NOT trigger recovery.
92
+ """
93
  timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
94
  print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
95
 
 
98
  timestamp = int(time.time())
99
  output_filename = f"video_{timestamp}.mp4"
100
  res_str = get_resolution_flags(orientation, quality)
 
101
  frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
102
+
103
  cmd = ["manim", "--resolution", res_str, *frame_rate_flags, "--disable_caching",
104
  "--progress_bar", "none", "scene.py", "GenScene", "-o", output_filename]
105
 
106
+ full_logs = ""
107
+
108
  try:
109
+ # check=False prevents Python from raising exception on error code automatically.
110
+ # We handle returncode manually below.
111
  process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, check=False)
112
+
113
+ stdout_log = process.stdout.decode('utf-8', 'ignore')
114
+ stderr_log = process.stderr.decode('utf-8', 'ignore')
115
  full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
116
 
117
+ # --- STANDARD ERROR CHECK ---
118
  if process.returncode != 0:
119
+ print(f"❌ Render Failed (Process Error). Return Code: {process.returncode}", flush=True)
120
+ # We do NOT stitch here. We just fall through to see if a file exists (unlikely).
121
+
122
  except subprocess.TimeoutExpired as e:
123
+ # --- TIMEOUT LOGIC ONLY ---
124
+ print(f"⌛ Render timed out after {timeout_sec} seconds. Attempting recovery...", flush=True)
125
+ stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
126
+ stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
127
+ timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
128
+ full_logs = timeout_logs
129
+
130
+ # 1. Parse logs for directory
131
+ combined_log = stdout_log + "\n" + stderr_log
132
+ path_matches = re.findall(r"movie file written in\s*'([^']+?)'", combined_log, flags=re.DOTALL)
133
+
134
+ if path_matches:
135
+ partial_files_dir = os.path.dirname("".join(path_matches[-1].split()))
136
+
137
+ if os.path.exists(partial_files_dir):
138
+ # 2. Find partial chunks
139
+ partial_files = sorted(glob.glob(os.path.join(partial_files_dir, 'uncached_*.mp4')),
140
+ key=lambda f: int(re.search(r'(\d+)\.mp4$', f).group(1)))
141
+
142
+ if partial_files:
143
+ print(f"⚡ Found {len(partial_files)} partial chunks. Stitching...", flush=True)
144
+ list_file_path = os.path.join(partial_files_dir, "file_list.txt")
145
+ with open(list_file_path, 'w') as f:
146
+ for pf in partial_files:
147
+ f.write(f"file '{os.path.abspath(pf)}'\n")
148
+
149
+ # 3. Stitch
150
+ combined_video_path = os.path.join(os.path.dirname(partial_files_dir), f"combined_partial_{timestamp}.mp4")
151
+ ffmpeg_cmd = ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file_path, "-c", "copy", combined_video_path]
152
+
153
+ ffmpeg_process = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, check=False)
154
+
155
+ if ffmpeg_process.returncode == 0 and os.path.exists(combined_video_path):
156
+ print(f"✅ Recovery Successful: {combined_video_path}", flush=True)
157
+ return combined_video_path, f"⚠️ WARNING: Render Timed Out. Recovered partial video.\n\n{timeout_logs}", True
158
+
159
+ print("❌ Recovery failed or no partial files found.", flush=True)
160
+ # End of Timeout Logic. Falls through to final check.
161
 
162
+ # --- FINAL FILE CHECK ---
163
+ # Only returns success if the actual complete file exists.
164
  media_video_base = os.path.join("media", "videos", "scene")
165
  if os.path.exists(media_video_base):
166
  for root, _, files in os.walk(media_video_base):
 
169
  print(f"✅ Video Render Success: {found_video_path}", flush=True)
170
  return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", True
171
 
172
+ return None, f"❌ Failure: Video file was not created.\n\n{full_logs}", False
 
173
 
174
  # ---------------------------------------------------------
175
  # 2. Main API Function
176
  # ---------------------------------------------------------
177
 
178
  def render_video_from_code(code, orientation, quality, timeout, preview_factor):
 
179
  try:
 
180
  is_valid, logs = run_manim_pre_check(code)
 
 
181
  if not is_valid:
182
  return None, logs, gr.Button(visible=True)
183
 
 
 
184
  cleanup_media_directory()
185
  if not code or "from manim import" not in code:
186
+ return None, "Error: No valid code.", gr.Button(visible=False)
187
 
 
188
  if quality == "Preview (360p)":
189
  code_to_render = modify_animation_times(code, factor=float(preview_factor) or 0.5, for_precheck=False)
190
  else:
191
  code_to_render = code
192
 
 
193
  video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
194
  return video_path, logs, gr.Button(visible=not success)
195
  except Exception as e: