Spaces:
Sleeping
Sleeping
File size: 11,749 Bytes
aeaa761 ac02dae ad7a765 aeaa761 d8d0be0 aeaa761 ac02dae d8d0be0 e5644ac d2bb83f e5644ac aeaa761 e5644ac aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 ac02dae 4c45290 aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 e5644ac d2bb83f 2353c7d e5644ac 2353c7d d2bb83f e5644ac aeaa761 e5644ac 2353c7d d2bb83f d8d0be0 e5644ac d2bb83f d8d0be0 d2bb83f e5644ac d2bb83f d8d0be0 d2bb83f e5644ac 2353c7d d2bb83f e5644ac 2353c7d d8d0be0 e5644ac aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae aeaa761 ac02dae ad7a765 ac02dae 2353c7d ac02dae aeaa761 ac02dae |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# src/rendering/scene_renderer.py
import subprocess
import tempfile
import shutil
from pathlib import Path
from typing import Optional
from src.core.ai_debugger import AIDebugger
from src.utils.smart_logger import logger
import os
def render_and_combine_scene(project_config: dict, scene_name: str, code_file: str, manim_executable: str, renders_cache: Path, debugger: AIDebugger, retry_count: int = 0, max_retries: int = 2, regen_count: int = 0, max_regens: int = 1) -> Optional[str]:
"""Renders a scene with Manim and combines audio using subprocess calls."""
operation_id = f"render_{scene_name}"
if retry_count == 0 and regen_count == 0:
logger.start_operation(operation_id, f"π¬ Rendering {scene_name}")
logger.quick_log("info", f"π Using scene file: {Path(code_file).name}")
else:
logger.quick_log("info", f"π Retry #{retry_count} for {scene_name} (regen: {regen_count})")
if not code_file or not scene_name:
logger.end_operation(operation_id, "β Missing code file or scene name")
return None
output_dir = renders_cache / scene_name
output_dir.mkdir(exist_ok=True)
final_video_path = output_dir / f"{scene_name}_final.mp4"
absolute_code_file = Path(code_file).resolve()
manim_command = [
manim_executable, "render", "-o", f'{scene_name}.mp4', "-ql",
str(absolute_code_file), scene_name,
]
logger.quick_log("info", f"Manim cmd: {manim_executable} render {scene_name}")
try:
with tempfile.TemporaryDirectory() as temp_dir:
process = subprocess.run(
manim_command, check=True, capture_output=True, text=True,
cwd=temp_dir, timeout=300
)
# Find rendered file
possible_files = [
Path(temp_dir) / f"{scene_name}.mp4",
Path(temp_dir) / "media" / "videos" / "1080p60" / f"{scene_name}.mp4",
Path(temp_dir) / "media" / "videos" / f"{scene_name}.mp4",
]
rendered_file = None
for possible_file in possible_files:
if possible_file.exists():
rendered_file = possible_file
break
if not rendered_file:
for file_path in Path(temp_dir).rglob("*.mp4"):
if scene_name in file_path.name:
rendered_file = file_path
break
if not rendered_file:
logger.end_operation(operation_id, "No output file produced")
return None
scene_data = project_config["scenes"][scene_name]
audio_path = scene_data.get("audio_file")
if audio_path and Path(audio_path).exists():
# Get durations and combine with audio
try:
probe_cmd = ['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', str(audio_path)]
audio_duration_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
audio_duration = float(audio_duration_result.stdout.strip())
video_probe_cmd = ['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', str(rendered_file)]
video_duration_result = subprocess.run(video_probe_cmd, capture_output=True, text=True, check=True)
video_duration = float(video_duration_result.stdout.strip())
if audio_duration > video_duration:
# Extend video to match audio
ffmpeg_command = [
'ffmpeg', '-y', '-i', str(rendered_file), '-i', str(audio_path),
'-filter_complex', f'[0:v]tpad=stop_mode=clone:stop_duration={audio_duration - video_duration}[v]',
'-map', '[v]', '-map', '1:a', '-c:a', 'aac', '-r', '30', str(final_video_path)
]
else:
# Standard combine
ffmpeg_command = [
'ffmpeg', '-y', '-i', str(rendered_file), '-i', str(audio_path),
'-c:v', 'copy', '-c:a', 'aac', '-shortest', str(final_video_path)
]
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)
video_size = final_video_path.stat().st_size / (1024*1024)
logger.end_operation(operation_id, "Video+audio combined", f"{video_size:.1f}MB")
except Exception:
# Fallback to simple combination
ffmpeg_command = [
'ffmpeg', '-y', '-i', str(rendered_file), '-i', str(audio_path),
'-c:v', 'copy', '-c:a', 'aac', '-shortest', str(final_video_path)
]
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)
logger.end_operation(operation_id, "Video+audio combined (fallback)")
return str(final_video_path)
else:
# Video only
shutil.move(str(rendered_file), str(final_video_path))
video_size = final_video_path.stat().st_size / (1024*1024)
logger.end_operation(operation_id, "Video-only rendered", f"{video_size:.1f}MB")
return str(final_video_path)
except subprocess.TimeoutExpired:
logger.end_operation(operation_id, "Manim rendering timed out (5min)")
return None
except subprocess.CalledProcessError as e:
logger.error_with_context(f"β Manim failed for {scene_name}", f"Exit code: {e.returncode}")
# Quick exit - if we've already tried a lot, just skip
if retry_count >= max_retries or regen_count >= max_regens:
logger.quick_log("error", f"β Scene {scene_name} exhausted all attempts - SKIPPING")
logger.end_operation(operation_id, f"β {scene_name} SKIPPED")
return None
if "error" in e.stderr.lower() and retry_count < max_retries:
# Try AI debugging first (up to max_retries times)
logger.quick_log("info", f"π§ Attempting AI auto-debug ({retry_count + 1}/{max_retries})")
fixed_file_path = debugger.debug_and_fix(code_file, e.stderr, scene_name, project_config)
if fixed_file_path:
logger.quick_log("success", f"β
AI created NEW fixed file, retrying")
return render_and_combine_scene(project_config, scene_name, fixed_file_path, manim_executable, renders_cache, debugger, retry_count + 1, max_retries, regen_count, max_regens)
else:
logger.quick_log("error", "β AI debugger failed - SKIPPING")
return None
# If AI debugging failed or we've reached max retries, try code regeneration ONCE
if retry_count >= max_retries and regen_count < max_regens:
logger.quick_log("info", f"π Trying code regeneration - LAST ATTEMPT")
from src.generation.code_generator import regenerate_single_scene_code
# Get the gemini client from the environment
import os
from google import genai
api_key = os.environ.get("GEMINI_API_KEY")
if api_key:
try:
gemini_client = genai.Client(api_key=api_key)
temp_dir = Path(code_file).parent
if regenerate_single_scene_code(project_config, gemini_client, str(temp_dir), scene_name):
logger.quick_log("success", "β
NEW scene code generated - FINAL ATTEMPT")
new_code_file = project_config["scenes"][scene_name]["code_file"]
return render_and_combine_scene(project_config, scene_name, new_code_file, manim_executable, renders_cache, debugger, 0, max_retries, regen_count + 1, max_regens)
else:
logger.quick_log("error", "β Code regeneration failed - SKIPPING")
return None
except Exception as regen_e:
logger.error_with_context("β Code regeneration error - SKIPPING", str(regen_e))
return None
logger.end_operation(operation_id, f"β {scene_name} failed - SKIPPED")
return None
except Exception as e:
logger.error_with_context(f"Unexpected rendering error for {scene_name}", str(e))
logger.end_operation(operation_id, "Unexpected error")
return None
def render_scenes(project_config: dict, manim_executable: str, renders_cache: Path, debugger: AIDebugger):
"""Workflow Step 4: Render all scenes using Manim."""
logger.start_operation("render_scenes", "Starting scene rendering")
# Validate manim executable
if not manim_executable or manim_executable == "manim":
possible_paths = [
"/home/gokulbarath/anaconda3/bin/manim", "manim", "/usr/local/bin/manim",
"/usr/bin/manim", "/opt/anaconda3/bin/manim", "/conda/bin/manim", "/opt/conda/bin/manim"
]
for path in possible_paths:
if Path(path).exists():
manim_executable = path
break
else:
logger.end_operation("render_scenes", "Manim executable not found")
return
logger.quick_log("info", f"Using manim: {manim_executable}")
scenes_to_render = [
(name, data) for name, data in project_config.get("scenes", {}).items()
if data.get("status") not in ["rendered"]
]
if not scenes_to_render:
logger.end_operation("render_scenes", "All scenes already rendered")
return
logger.quick_log("info", f"Rendering {len(scenes_to_render)} scenes")
for i, (scene_name, scene_data) in enumerate(scenes_to_render, 1):
logger.progress_update(i, len(scenes_to_render), f"Rendering {scene_name}")
# Example: Before rendering
logger.quick_log("info", f"Rendering scene from file: {scene_data.get('code_file')}")
scene_file_path = scene_data.get('code_file')
if not os.path.exists(scene_file_path):
logger.quick_log("error", f"Scene file not found: {scene_file_path}")
# Skip rendering for this scene
continue
try:
video_path = render_and_combine_scene(
project_config, scene_name, scene_data.get("code_file"),
manim_executable, renders_cache, debugger, 0, 1, 0, 1 # VERY CONSERVATIVE: 1 AI retry, 1 code regen max
)
if video_path and Path(video_path).exists():
project_config["scenes"][scene_name]["final_video_path"] = str(video_path)
project_config["scenes"][scene_name]["status"] = "rendered"
else:
project_config["scenes"][scene_name]["status"] = "render_failed"
except Exception as e:
logger.error_with_context(f"Rendering {scene_name} failed", str(e))
project_config["scenes"][scene_name]["status"] = "render_failed"
success_count = sum(1 for _, data in project_config["scenes"].items() if data.get("status") == "rendered")
logger.end_operation("render_scenes", f"Rendering complete: {success_count}/{len(scenes_to_render)} successful")
|