Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,6 +7,59 @@ import shutil
|
|
| 7 |
import glob
|
| 8 |
import base64
|
| 9 |
import tempfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# ---------------------------------------------------------
|
| 12 |
# 1. Helper Functions
|
|
@@ -15,37 +68,28 @@ import tempfile
|
|
| 15 |
def modify_animation_times(code: str, factor: float) -> str:
|
| 16 |
"""
|
| 17 |
Safely scales all numeric run_time and wait() values by a given factor.
|
| 18 |
-
This single, safe function is now used for both Pre-check and Preview modes.
|
| 19 |
"""
|
| 20 |
print(f"⚡ Scaling animation times by a factor of {factor}...", flush=True)
|
| 21 |
-
# A safe minimum to prevent Manim from crashing with run_time=0
|
| 22 |
MIN_RUN_TIME = 0.01
|
| 23 |
|
| 24 |
def scale_match(m, is_wait):
|
| 25 |
try:
|
| 26 |
val = float(m.group(2))
|
| 27 |
new_val = val * factor
|
| 28 |
-
# For waits, we can go low. For run_time, we must stay above zero.
|
| 29 |
final_val = new_val if is_wait else max(new_val, MIN_RUN_TIME)
|
| 30 |
return f"{m.group(1)}{final_val:.3f}"
|
| 31 |
except ValueError:
|
| 32 |
-
# This happens if run_time is a variable. We safely ignore it.
|
| 33 |
return m.group(0)
|
| 34 |
|
| 35 |
-
# This safe regex only matches explicit numbers, preventing SyntaxErrors.
|
| 36 |
code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale_match(m, False), code)
|
| 37 |
code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale_match(m, True), code)
|
| 38 |
return code
|
| 39 |
|
| 40 |
def run_manim_pre_check(code_str: str) -> (bool, str):
|
| 41 |
"""
|
| 42 |
-
Runs Manim with '-s'.
|
| 43 |
-
- Uses the safe `modify_animation_times` function, so no special error handling is needed.
|
| 44 |
-
- Any error that occurs is now considered a real, critical error.
|
| 45 |
-
- Soft Pass on Timeout remains as a fallback.
|
| 46 |
"""
|
| 47 |
print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
|
| 48 |
-
# Using a factor of 0.2 provides a good speedup without being overly aggressive.
|
| 49 |
fast_code = modify_animation_times(code_str, factor=0.1)
|
| 50 |
|
| 51 |
with open("scene_pre_check.py", "w", encoding="utf-8") as f:
|
|
@@ -59,8 +103,10 @@ def run_manim_pre_check(code_str: str) -> (bool, str):
|
|
| 59 |
print("✅ Pre-check passed.", flush=True)
|
| 60 |
return True, "Pre-check successful."
|
| 61 |
else:
|
| 62 |
-
# With our safe regex, any error should be treated as a real problem.
|
| 63 |
stderr_log = process.stderr.decode('utf-8', 'ignore')
|
|
|
|
|
|
|
|
|
|
| 64 |
print(f"❌ Pre-check failed with a critical error.\n{stderr_log}", flush=True)
|
| 65 |
return False, f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
|
| 66 |
|
|
@@ -89,7 +135,7 @@ def get_resolution_flags(orientation, quality):
|
|
| 89 |
|
| 90 |
def run_manim(code_str, orientation, quality, timeout):
|
| 91 |
"""
|
| 92 |
-
Executes Manim. Partial stitching on Timeout.
|
| 93 |
"""
|
| 94 |
timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
|
| 95 |
print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
|
|
@@ -158,6 +204,10 @@ 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 |
return None, f"❌ Failure: Video file was not created.\n\n{full_logs}", False
|
| 162 |
|
| 163 |
# ---------------------------------------------------------
|
|
@@ -182,6 +232,8 @@ def render_video_from_code(code, orientation, quality, timeout, preview_factor):
|
|
| 182 |
video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
|
| 183 |
return video_path, logs, gr.Button(visible=not success)
|
| 184 |
except Exception as e:
|
|
|
|
|
|
|
| 185 |
return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
|
| 186 |
|
| 187 |
# ---------------------------------------------------------
|
|
|
|
| 7 |
import glob
|
| 8 |
import base64
|
| 9 |
import tempfile
|
| 10 |
+
import requests
|
| 11 |
+
import json
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
# ---------------------------------------------------------
|
| 15 |
+
# 0. Error Logging Helper (GitHub Gist)
|
| 16 |
+
# ---------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
def log_to_gist(content):
|
| 19 |
+
"""
|
| 20 |
+
Silently appends the error content to a GitHub Gist file.
|
| 21 |
+
Uses environment variables for security.
|
| 22 |
+
"""
|
| 23 |
+
token = os.getenv("GITHUB_TOKEN")
|
| 24 |
+
gist_id = os.getenv("GIST_ID")
|
| 25 |
+
|
| 26 |
+
if not token or not gist_id:
|
| 27 |
+
print("⚠️ GitHub Token or Gist ID not set. Skipping error logging.", flush=True)
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
filename = "manim_error_history.txt"
|
| 31 |
+
url = f"https://api.github.com/gists/{gist_id}"
|
| 32 |
+
headers = {
|
| 33 |
+
"Authorization": f"token {token}",
|
| 34 |
+
"Accept": "application/vnd.github.v3+json"
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# 1. Fetch existing content
|
| 39 |
+
resp = requests.get(url, headers=headers, timeout=5)
|
| 40 |
+
if resp.status_code != 200:
|
| 41 |
+
print(f"⚠️ Failed to fetch Gist history: {resp.status_code}", flush=True)
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
current_data = resp.json()
|
| 45 |
+
old_content = current_data.get('files', {}).get(filename, {}).get('content', "")
|
| 46 |
+
|
| 47 |
+
# 2. Format new entry (Newest at the top)
|
| 48 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 49 |
+
new_entry = f"{'='*40}\nDATE: {timestamp}\n{content}\n\n"
|
| 50 |
+
updated_content = new_entry + old_content
|
| 51 |
+
|
| 52 |
+
# 3. Update Gist
|
| 53 |
+
payload = {"files": {filename: {"content": updated_content}}}
|
| 54 |
+
patch_resp = requests.patch(url, headers=headers, data=json.dumps(payload), timeout=5)
|
| 55 |
+
|
| 56 |
+
if patch_resp.status_code == 200:
|
| 57 |
+
print(f"📋 Error logged to GitHub history. URL: https://gist.github.com/{gist_id}", flush=True)
|
| 58 |
+
else:
|
| 59 |
+
print(f"⚠️ Failed to update Gist: {patch_resp.text}", flush=True)
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"⚠️ Gist Logging Exception: {str(e)}", flush=True)
|
| 63 |
|
| 64 |
# ---------------------------------------------------------
|
| 65 |
# 1. Helper Functions
|
|
|
|
| 68 |
def modify_animation_times(code: str, factor: float) -> str:
|
| 69 |
"""
|
| 70 |
Safely scales all numeric run_time and wait() values by a given factor.
|
|
|
|
| 71 |
"""
|
| 72 |
print(f"⚡ Scaling animation times by a factor of {factor}...", flush=True)
|
|
|
|
| 73 |
MIN_RUN_TIME = 0.01
|
| 74 |
|
| 75 |
def scale_match(m, is_wait):
|
| 76 |
try:
|
| 77 |
val = float(m.group(2))
|
| 78 |
new_val = val * factor
|
|
|
|
| 79 |
final_val = new_val if is_wait else max(new_val, MIN_RUN_TIME)
|
| 80 |
return f"{m.group(1)}{final_val:.3f}"
|
| 81 |
except ValueError:
|
|
|
|
| 82 |
return m.group(0)
|
| 83 |
|
|
|
|
| 84 |
code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale_match(m, False), code)
|
| 85 |
code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale_match(m, True), code)
|
| 86 |
return code
|
| 87 |
|
| 88 |
def run_manim_pre_check(code_str: str) -> (bool, str):
|
| 89 |
"""
|
| 90 |
+
Runs Manim with '-s'.
|
|
|
|
|
|
|
|
|
|
| 91 |
"""
|
| 92 |
print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
|
|
|
|
| 93 |
fast_code = modify_animation_times(code_str, factor=0.1)
|
| 94 |
|
| 95 |
with open("scene_pre_check.py", "w", encoding="utf-8") as f:
|
|
|
|
| 103 |
print("✅ Pre-check passed.", flush=True)
|
| 104 |
return True, "Pre-check successful."
|
| 105 |
else:
|
|
|
|
| 106 |
stderr_log = process.stderr.decode('utf-8', 'ignore')
|
| 107 |
+
# --- LOGGING TO GITHUB ---
|
| 108 |
+
log_to_gist(f"PRE-CHECK FAILED:\n{stderr_log}")
|
| 109 |
+
|
| 110 |
print(f"❌ Pre-check failed with a critical error.\n{stderr_log}", flush=True)
|
| 111 |
return False, f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
|
| 112 |
|
|
|
|
| 135 |
|
| 136 |
def run_manim(code_str, orientation, quality, timeout):
|
| 137 |
"""
|
| 138 |
+
Executes Manim. Partial stitching on Timeout.
|
| 139 |
"""
|
| 140 |
timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
|
| 141 |
print(f"🎬 Starting Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
|
|
|
|
| 204 |
print(f"✅ Video Render Success: {found_video_path}", flush=True)
|
| 205 |
return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", True
|
| 206 |
|
| 207 |
+
# --- LOGGING TO GITHUB ---
|
| 208 |
+
# If we reached here, the video was not found or failed completely.
|
| 209 |
+
log_to_gist(f"RENDER FAILED:\n{full_logs}")
|
| 210 |
+
|
| 211 |
return None, f"❌ Failure: Video file was not created.\n\n{full_logs}", False
|
| 212 |
|
| 213 |
# ---------------------------------------------------------
|
|
|
|
| 232 |
video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
|
| 233 |
return video_path, logs, gr.Button(visible=not success)
|
| 234 |
except Exception as e:
|
| 235 |
+
# --- LOGGING TO GITHUB ---
|
| 236 |
+
log_to_gist(f"CRITICAL APP EXCEPTION:\n{str(e)}")
|
| 237 |
return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
|
| 238 |
|
| 239 |
# ---------------------------------------------------------
|