Update deformes4D_engine.py
Browse files- deformes4D_engine.py +81 -135
deformes4D_engine.py
CHANGED
|
@@ -26,7 +26,7 @@ from audio_specialist import audio_specialist_singleton
|
|
| 26 |
from ltx_manager_helpers import ltx_manager_singleton
|
| 27 |
from gemini_helpers import gemini_singleton
|
| 28 |
from upscaler_specialist import upscaler_specialist_singleton
|
| 29 |
-
from hd_specialist import hd_specialist_singleton
|
| 30 |
from ltx_video.models.autoencoders.causal_video_autoencoder import CausalVideoAutoencoder
|
| 31 |
from ltx_video.models.autoencoders.vae_encode import vae_encode, vae_decode
|
| 32 |
|
|
@@ -58,17 +58,28 @@ class Deformes4DEngine:
|
|
| 58 |
self._vae.to(self.device); self._vae.eval()
|
| 59 |
return self._vae
|
| 60 |
|
| 61 |
-
# MÉTODOS AUXILIARES
|
| 62 |
-
def save_latent_tensor(self, tensor: torch.Tensor, path: str):
|
| 63 |
-
torch.save(tensor.cpu(), path)
|
| 64 |
|
| 65 |
-
def
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
@torch.no_grad()
|
| 74 |
def latents_to_pixels(self, latent_tensor: torch.Tensor, decode_timestep: float = 0.05) -> torch.Tensor:
|
|
@@ -76,14 +87,6 @@ class Deformes4DEngine:
|
|
| 76 |
timestep_tensor = torch.tensor([decode_timestep] * latent_tensor.shape[0], device=self.device, dtype=latent_tensor.dtype)
|
| 77 |
return vae_decode(latent_tensor, self.vae, is_video=True, timestep=timestep_tensor, vae_per_channel_normalize=True)
|
| 78 |
|
| 79 |
-
def save_video_from_tensor(self, video_tensor: torch.Tensor, path: str, fps: int = 24):
|
| 80 |
-
if video_tensor is None or video_tensor.ndim != 5 or video_tensor.shape[2] == 0: return
|
| 81 |
-
video_tensor = video_tensor.squeeze(0).permute(1, 2, 3, 0)
|
| 82 |
-
video_tensor = (video_tensor.clamp(-1, 1) + 1) / 2.0
|
| 83 |
-
video_np = (video_tensor.detach().cpu().float().numpy() * 255).astype(np.uint8)
|
| 84 |
-
with imageio.get_writer(path, fps=fps, codec='libx264', quality=8, output_params=['-pix_fmt', 'yuv420p']) as writer:
|
| 85 |
-
for frame in video_np: writer.append_data(frame)
|
| 86 |
-
|
| 87 |
def _preprocess_image_for_latent_conversion(self, image: Image.Image, target_resolution: tuple) -> Image.Image:
|
| 88 |
if image.size != target_resolution:
|
| 89 |
return ImageOps.fit(image, target_resolution, Image.Resampling.LANCZOS)
|
|
@@ -95,70 +98,32 @@ class Deformes4DEngine:
|
|
| 95 |
tensor = (tensor * 2.0) - 1.0
|
| 96 |
return self.pixels_to_latents(tensor)
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video_path],
|
| 103 |
-
capture_output=True, text=True, check=True)
|
| 104 |
-
return float(result.stdout.strip())
|
| 105 |
-
except Exception:
|
| 106 |
-
return 0.0
|
| 107 |
-
|
| 108 |
-
def _combine_video_and_audio_ffmpeg(self, video_path: str, audio_path: str, output_path: str):
|
| 109 |
-
"""Combina um arquivo de vídeo com um arquivo de áudio usando ffmpeg."""
|
| 110 |
-
cmd = [
|
| 111 |
-
'ffmpeg', '-y',
|
| 112 |
-
'-i', video_path,
|
| 113 |
-
'-i', audio_path,
|
| 114 |
-
'-c:v', 'copy', # Copia o stream de vídeo sem re-codificar
|
| 115 |
-
'-c:a', 'aac', # Re-codifica o áudio para o formato AAC, padrão para MP4
|
| 116 |
-
'-shortest', # Termina a codificação quando o stream mais curto terminar
|
| 117 |
-
output_path
|
| 118 |
-
]
|
| 119 |
-
try:
|
| 120 |
-
subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 121 |
-
logger.info(f"Áudio e vídeo combinados com sucesso em {output_path}")
|
| 122 |
-
except subprocess.CalledProcessError as e:
|
| 123 |
-
logger.error(f"Falha ao combinar áudio e vídeo. Detalhes: {e.stderr}")
|
| 124 |
-
raise gr.Error(f"Falha ao combinar áudio e vídeo: {e.stderr}")
|
| 125 |
-
|
| 126 |
-
def _generate_standalone_audio(self, video_for_duration_path: str, audio_prompt: str) -> str:
|
| 127 |
-
"""Gera um arquivo de áudio e retorna seu caminho."""
|
| 128 |
-
duration = self._get_video_duration(video_for_duration_path)
|
| 129 |
-
if duration == 0:
|
| 130 |
-
raise gr.Error("Não foi possível determinar a duração do vídeo para gerar o áudio.")
|
| 131 |
-
|
| 132 |
-
# Esta função agora deve retornar apenas o caminho do arquivo de áudio gerado
|
| 133 |
-
# (pode exigir uma pequena modificação no seu audio_specialist)
|
| 134 |
-
audio_path = audio_specialist_singleton.generate_audio(
|
| 135 |
-
prompt=audio_prompt,
|
| 136 |
-
duration_seconds=duration,
|
| 137 |
-
output_dir=self.workspace_dir
|
| 138 |
-
)
|
| 139 |
-
return audio_path
|
| 140 |
|
| 141 |
-
# NÚCLEO DA LÓGICA ADUC-SDR
|
| 142 |
def generate_full_movie(self, keyframes: list, global_prompt: str, storyboard: list,
|
| 143 |
seconds_per_fragment: float, trim_percent: int,
|
| 144 |
handler_strength: float, destination_convergence_strength: float,
|
| 145 |
video_resolution: int, use_continuity_director: bool,
|
| 146 |
progress: gr.Progress = gr.Progress()):
|
| 147 |
|
| 148 |
-
|
|
|
|
| 149 |
current_step = 0
|
| 150 |
-
|
| 151 |
FPS = 24
|
| 152 |
FRAMES_PER_LATENT_CHUNK = 8
|
| 153 |
ECO_LATENT_CHUNKS = 2
|
| 154 |
|
| 155 |
total_frames_brutos = self._quantize_to_multiple(int(seconds_per_fragment * FPS), FRAMES_PER_LATENT_CHUNK)
|
| 156 |
-
total_latents_brutos = total_frames_brutos // FRAMES_PER_LATENT_CHUNK
|
| 157 |
frames_a_podar = self._quantize_to_multiple(int(total_frames_brutos * (trim_percent / 100)), FRAMES_PER_LATENT_CHUNK)
|
| 158 |
latents_a_podar = frames_a_podar // FRAMES_PER_LATENT_CHUNK
|
| 159 |
|
| 160 |
-
if
|
| 161 |
-
raise gr.Error(
|
| 162 |
|
| 163 |
DEJAVU_FRAME_TARGET = frames_a_podar - 1 if frames_a_podar > 0 else 0
|
| 164 |
DESTINATION_FRAME_TARGET = total_frames_brutos - 1
|
|
@@ -169,16 +134,14 @@ class Deformes4DEngine:
|
|
| 169 |
|
| 170 |
eco_latent_for_next_loop = None
|
| 171 |
dejavu_latent_for_next_loop = None
|
| 172 |
-
|
| 173 |
-
num_transitions_to_generate = len(keyframe_paths) - 1
|
| 174 |
processed_latent_fragments = []
|
| 175 |
|
|
|
|
| 176 |
for i in range(num_transitions_to_generate):
|
| 177 |
fragment_index = i + 1
|
| 178 |
current_step += 1
|
| 179 |
progress(current_step / TOTAL_STEPS, desc=f"Gerando Fragmento {fragment_index}/{num_transitions_to_generate}")
|
| 180 |
|
| 181 |
-
# ... (Lógica de decisão do Gemini e configuração de parâmetros - sem alterações)
|
| 182 |
past_keyframe_path = keyframe_paths[i - 1] if i > 0 else keyframe_paths[i]
|
| 183 |
start_keyframe_path = keyframe_paths[i]
|
| 184 |
destination_keyframe_path = keyframe_paths[i + 1]
|
|
@@ -189,7 +152,7 @@ class Deformes4DEngine:
|
|
| 189 |
transition_type, motion_prompt = decision["transition_type"], decision["motion_prompt"]
|
| 190 |
story_history += f"\n- Ato {fragment_index}: {motion_prompt}"
|
| 191 |
|
| 192 |
-
expected_height, expected_width =
|
| 193 |
downscale_factor = 2 / 3
|
| 194 |
downscaled_height = self._quantize_to_multiple(int(expected_height * downscale_factor), 8)
|
| 195 |
downscaled_width = self._quantize_to_multiple(int(expected_width * downscale_factor), 8)
|
|
@@ -217,82 +180,76 @@ class Deformes4DEngine:
|
|
| 217 |
if transition_type == "cut":
|
| 218 |
eco_latent_for_next_loop, dejavu_latent_for_next_loop = None, None
|
| 219 |
|
| 220 |
-
# --- ATO I: PÓS-PRODUÇÃO LATENTE ---
|
| 221 |
upscaled_latents = self.upscale_latents(latents_video)
|
| 222 |
refined_latents = self.refine_latents(upscaled_latents, motion_prompt=f"refining scene: {motion_prompt}")
|
| 223 |
processed_latent_fragments.append(refined_latents)
|
| 224 |
-
|
| 225 |
-
# --- FIM DO LOOP DE GERAÇÃO ---
|
| 226 |
-
|
| 227 |
-
current_step += 1
|
| 228 |
-
progress(current_step / TOTAL_STEPS, desc="Concatenando fragmentos...")
|
| 229 |
-
tensors_para_concatenar = [frag.to(self.device) for frag in processed_latent_fragments]
|
| 230 |
-
final_concatenated_latents = torch.cat(tensors_para_concatenar, dim=2)
|
| 231 |
-
|
| 232 |
-
base_name = f"movie_{int(time.time())}"
|
| 233 |
|
|
|
|
|
|
|
| 234 |
current_step += 1
|
| 235 |
-
progress(current_step / TOTAL_STEPS, desc="Renderizando vídeo
|
| 236 |
refined_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_refined_silent.mp4")
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
-
|
| 241 |
-
del
|
| 242 |
gc.collect()
|
| 243 |
torch.cuda.empty_cache()
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
current_step += 1
|
| 248 |
progress(current_step / TOTAL_STEPS, desc="Aprimoramento final (HD)...")
|
| 249 |
hq_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_hq_silent.mp4")
|
| 250 |
try:
|
| 251 |
-
# O Especialista HD processa o vídeo silencioso refinado
|
| 252 |
hd_specialist_singleton.process_video(
|
| 253 |
input_video_path=refined_silent_video_path,
|
| 254 |
output_video_path=hq_silent_video_path,
|
| 255 |
prompt=global_prompt
|
| 256 |
)
|
| 257 |
except Exception as e:
|
| 258 |
-
logger.error(f"Falha no
|
| 259 |
-
# Se o HD falhar, usamos o vídeo refinado (silencioso) como base para o final
|
| 260 |
os.rename(refined_silent_video_path, hq_silent_video_path)
|
| 261 |
|
| 262 |
current_step += 1
|
| 263 |
progress(current_step / TOTAL_STEPS, desc="Finalizando montagem...")
|
| 264 |
final_video_path = os.path.join(self.workspace_dir, f"{base_name}_FINAL.mp4")
|
| 265 |
-
|
| 266 |
-
#if audio_path and os.path.exists(audio_path):
|
| 267 |
-
# # Se o áudio foi gerado, combina o vídeo de ALTA QUALIDADE com ele
|
| 268 |
-
# self._combine_video_and_audio_ffmpeg(hq_silent_video_path, audio_path, final_video_path)
|
| 269 |
-
#else:
|
| 270 |
-
# # Se não houver áudio, apenas renomeia o vídeo de alta qualidade
|
| 271 |
-
# os.rename(hq_silent_video_path, final_video_path)
|
| 272 |
-
|
| 273 |
-
logger.info(f"Processo concluído! Vídeo final salvo em: {hq_silent_video_path}")
|
| 274 |
-
yield {"final_path": hq_silent_video_path}
|
| 275 |
-
|
| 276 |
-
def refine_latents1(self, latents: torch.Tensor,
|
| 277 |
-
fps: int = 24,
|
| 278 |
-
denoise_strength: float = 0.35,
|
| 279 |
-
refine_steps: int = 12,
|
| 280 |
-
motion_prompt: str = "refining video, improving details, cinematic quality") -> torch.Tensor:
|
| 281 |
-
"""Aplica um passe de refinamento (denoise) em um tensor latente."""
|
| 282 |
-
logger.info(f"Refinando tensor latente com shape {latents.shape}.")
|
| 283 |
-
_, _, num_frames, latent_h, latent_w = latents.shape
|
| 284 |
-
vae_scale_factor = self.vae.config.scaling_factor if hasattr(self.vae.config, 'scaling_factor') else 8
|
| 285 |
-
pixel_height, pixel_width = latent_h * vae_scale_factor, latent_w * vae_scale_factor
|
| 286 |
-
|
| 287 |
-
refined_latents_tensor, _ = self.ltx_manager.refine_latents(
|
| 288 |
-
latents, height=pixel_height, width=pixel_width, video_total_frames=num_frames,
|
| 289 |
-
video_fps=fps, motion_prompt=motion_prompt, current_fragment_index=int(time.time()),
|
| 290 |
-
denoise_strength=denoise_strength, refine_steps=refine_steps)
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
|
|
|
| 295 |
|
|
|
|
|
|
|
| 296 |
|
| 297 |
def refine_latents(self, latents: torch.Tensor,
|
| 298 |
fps: int = 24,
|
|
@@ -305,26 +262,17 @@ class Deformes4DEngine:
|
|
| 305 |
"""
|
| 306 |
logger.info(f"Refinando tensor latente com shape {latents.shape} para refinamento.")
|
| 307 |
|
| 308 |
-
# Extrai as dimensões do tensor latente de ENTRADA.
|
| 309 |
_, _, num_latent_frames, latent_h, latent_w = latents.shape
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
video_scale_factor = getattr(self.vae, 'temporal_downscale_factor', 8)
|
| 314 |
-
vae_scale_factor = getattr(self.vae, 'spatial_downscale_factor', 8)
|
| 315 |
|
| 316 |
-
# Converte as dimensões latentes para as dimensões de pixel correspondentes.
|
| 317 |
pixel_height = latent_h * vae_scale_factor
|
| 318 |
pixel_width = latent_w * vae_scale_factor
|
| 319 |
-
|
| 320 |
-
#
|
| 321 |
-
# Para que a pipeline espere um latente com 'num_latent_frames', precisamos
|
| 322 |
-
# fornecer um número de frames de pixel que, após a divisão e a adição de 1
|
| 323 |
-
# (devido ao VAE causal), resulte no número original de frames latentes.
|
| 324 |
-
# A fórmula inversa é: (num_latent_frames - 1) * video_scale_factor
|
| 325 |
pixel_frames = (num_latent_frames - 1) * video_scale_factor
|
| 326 |
|
| 327 |
-
# Chama o ltx_manager com os parâmetros corretos.
|
| 328 |
refined_latents_tensor, _ = self.ltx_manager.refine_latents(
|
| 329 |
latents,
|
| 330 |
height=pixel_height,
|
|
@@ -339,9 +287,7 @@ class Deformes4DEngine:
|
|
| 339 |
|
| 340 |
logger.info(f"Retornando tensor latente refinado com shape: {refined_latents_tensor.shape}")
|
| 341 |
return refined_latents_tensor
|
| 342 |
-
|
| 343 |
|
| 344 |
-
|
| 345 |
def upscale_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
| 346 |
"""Interface para o UpscalerSpecialist."""
|
| 347 |
logger.info(f"Realizando upscale em tensor latente com shape {latents.shape}.")
|
|
@@ -349,7 +295,7 @@ class Deformes4DEngine:
|
|
| 349 |
|
| 350 |
def _generate_latent_tensor_internal(self, conditioning_items, ltx_params, target_resolution, total_frames_to_generate):
|
| 351 |
kwargs = {
|
| 352 |
-
**ltx_params, 'width': target_resolution[
|
| 353 |
'video_total_frames': total_frames_to_generate, 'video_fps': 24,
|
| 354 |
'current_fragment_index': int(time.time()), 'conditioning_items_data': conditioning_items
|
| 355 |
}
|
|
|
|
| 26 |
from ltx_manager_helpers import ltx_manager_singleton
|
| 27 |
from gemini_helpers import gemini_singleton
|
| 28 |
from upscaler_specialist import upscaler_specialist_singleton
|
| 29 |
+
from hd_specialist import hd_specialist_singleton
|
| 30 |
from ltx_video.models.autoencoders.causal_video_autoencoder import CausalVideoAutoencoder
|
| 31 |
from ltx_video.models.autoencoders.vae_encode import vae_encode, vae_decode
|
| 32 |
|
|
|
|
| 58 |
self._vae.to(self.device); self._vae.eval()
|
| 59 |
return self._vae
|
| 60 |
|
| 61 |
+
# --- MÉTODOS AUXILIARES ---
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
def _extract_audio_ffmpeg(self, video_path: str, output_audio_path: str) -> str | None:
|
| 64 |
+
"""Extrai a trilha sonora de um vídeo para uso posterior."""
|
| 65 |
+
if not os.path.exists(video_path): return None
|
| 66 |
+
cmd = ['ffmpeg', '-y', '-i', video_path, '-vn', '-acodec', 'copy', output_audio_path]
|
| 67 |
+
try:
|
| 68 |
+
subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 69 |
+
logger.info(f"Áudio extraído com sucesso para {output_audio_path}")
|
| 70 |
+
return output_audio_path
|
| 71 |
+
except subprocess.CalledProcessError:
|
| 72 |
+
logger.warning(f"Não foi possível extrair o áudio de {os.path.basename(video_path)}. O vídeo pode ser silencioso.")
|
| 73 |
+
return None
|
| 74 |
|
| 75 |
+
def _combine_video_and_audio_ffmpeg(self, video_path: str, audio_path: str, output_path: str):
|
| 76 |
+
"""Combina um vídeo (sem som) com um arquivo de áudio."""
|
| 77 |
+
cmd = ['ffmpeg', '-y', '-i', video_path, '-i', audio_path, '-c:v', 'copy', '-c:a', 'aac', '-shortest', output_path]
|
| 78 |
+
try:
|
| 79 |
+
subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 80 |
+
logger.info(f"Áudio e vídeo combinados com sucesso em {output_path}")
|
| 81 |
+
except subprocess.CalledProcessError as e:
|
| 82 |
+
raise gr.Error(f"Falha ao combinar áudio e vídeo: {e.stderr}")
|
| 83 |
|
| 84 |
@torch.no_grad()
|
| 85 |
def latents_to_pixels(self, latent_tensor: torch.Tensor, decode_timestep: float = 0.05) -> torch.Tensor:
|
|
|
|
| 87 |
timestep_tensor = torch.tensor([decode_timestep] * latent_tensor.shape[0], device=self.device, dtype=latent_tensor.dtype)
|
| 88 |
return vae_decode(latent_tensor, self.vae, is_video=True, timestep=timestep_tensor, vae_per_channel_normalize=True)
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
def _preprocess_image_for_latent_conversion(self, image: Image.Image, target_resolution: tuple) -> Image.Image:
|
| 91 |
if image.size != target_resolution:
|
| 92 |
return ImageOps.fit(image, target_resolution, Image.Resampling.LANCZOS)
|
|
|
|
| 98 |
tensor = (tensor * 2.0) - 1.0
|
| 99 |
return self.pixels_to_latents(tensor)
|
| 100 |
|
| 101 |
+
@torch.no_grad()
|
| 102 |
+
def pixels_to_latents(self, tensor: torch.Tensor) -> torch.Tensor:
|
| 103 |
+
tensor = tensor.to(self.device, dtype=self.vae.dtype)
|
| 104 |
+
return vae_encode(tensor, self.vae, vae_per_channel_normalize=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
# --- NÚCLEO DA LÓGICA ADUC-SDR ---
|
| 107 |
def generate_full_movie(self, keyframes: list, global_prompt: str, storyboard: list,
|
| 108 |
seconds_per_fragment: float, trim_percent: int,
|
| 109 |
handler_strength: float, destination_convergence_strength: float,
|
| 110 |
video_resolution: int, use_continuity_director: bool,
|
| 111 |
progress: gr.Progress = gr.Progress()):
|
| 112 |
|
| 113 |
+
num_transitions_to_generate = len(keyframes) - 1
|
| 114 |
+
TOTAL_STEPS = num_transitions_to_generate + 4 # Fragmentos + etapas de pós-produção
|
| 115 |
current_step = 0
|
| 116 |
+
|
| 117 |
FPS = 24
|
| 118 |
FRAMES_PER_LATENT_CHUNK = 8
|
| 119 |
ECO_LATENT_CHUNKS = 2
|
| 120 |
|
| 121 |
total_frames_brutos = self._quantize_to_multiple(int(seconds_per_fragment * FPS), FRAMES_PER_LATENT_CHUNK)
|
|
|
|
| 122 |
frames_a_podar = self._quantize_to_multiple(int(total_frames_brutos * (trim_percent / 100)), FRAMES_PER_LATENT_CHUNK)
|
| 123 |
latents_a_podar = frames_a_podar // FRAMES_PER_LATENT_CHUNK
|
| 124 |
|
| 125 |
+
if total_frames_brutos // FRAMES_PER_LATENT_CHUNK <= latents_a_podar + 1:
|
| 126 |
+
raise gr.Error("A combinação de duração e poda é muito agressiva.")
|
| 127 |
|
| 128 |
DEJAVU_FRAME_TARGET = frames_a_podar - 1 if frames_a_podar > 0 else 0
|
| 129 |
DESTINATION_FRAME_TARGET = total_frames_brutos - 1
|
|
|
|
| 134 |
|
| 135 |
eco_latent_for_next_loop = None
|
| 136 |
dejavu_latent_for_next_loop = None
|
|
|
|
|
|
|
| 137 |
processed_latent_fragments = []
|
| 138 |
|
| 139 |
+
# --- ATO I: GERAÇÃO LATENTE (LOOP DE FRAGMENTOS) ---
|
| 140 |
for i in range(num_transitions_to_generate):
|
| 141 |
fragment_index = i + 1
|
| 142 |
current_step += 1
|
| 143 |
progress(current_step / TOTAL_STEPS, desc=f"Gerando Fragmento {fragment_index}/{num_transitions_to_generate}")
|
| 144 |
|
|
|
|
| 145 |
past_keyframe_path = keyframe_paths[i - 1] if i > 0 else keyframe_paths[i]
|
| 146 |
start_keyframe_path = keyframe_paths[i]
|
| 147 |
destination_keyframe_path = keyframe_paths[i + 1]
|
|
|
|
| 152 |
transition_type, motion_prompt = decision["transition_type"], decision["motion_prompt"]
|
| 153 |
story_history += f"\n- Ato {fragment_index}: {motion_prompt}"
|
| 154 |
|
| 155 |
+
expected_height, expected_width = video_resolution, video_resolution
|
| 156 |
downscale_factor = 2 / 3
|
| 157 |
downscaled_height = self._quantize_to_multiple(int(expected_height * downscale_factor), 8)
|
| 158 |
downscaled_width = self._quantize_to_multiple(int(expected_width * downscale_factor), 8)
|
|
|
|
| 180 |
if transition_type == "cut":
|
| 181 |
eco_latent_for_next_loop, dejavu_latent_for_next_loop = None, None
|
| 182 |
|
|
|
|
| 183 |
upscaled_latents = self.upscale_latents(latents_video)
|
| 184 |
refined_latents = self.refine_latents(upscaled_latents, motion_prompt=f"refining scene: {motion_prompt}")
|
| 185 |
processed_latent_fragments.append(refined_latents)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
# --- ATO II: RENDERIZAÇÃO PRIMÁRIA (COM CORREÇÃO DE OOM) ---
|
| 188 |
+
base_name = f"movie_{int(time.time())}"
|
| 189 |
current_step += 1
|
| 190 |
+
progress(current_step / TOTAL_STEPS, desc="Renderizando vídeo (em lotes)...")
|
| 191 |
refined_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_refined_silent.mp4")
|
| 192 |
+
|
| 193 |
+
with imageio.get_writer(refined_silent_video_path, fps=FPS, codec='libx264', quality=8, output_params=['-pix_fmt', 'yuv420p']) as writer:
|
| 194 |
+
for i, latent_fragment in enumerate(processed_latent_fragments):
|
| 195 |
+
logger.info(f"Decodificando fragmento {i+1}/{len(processed_latent_fragments)} para pixels...")
|
| 196 |
+
pixel_tensor_fragment = self.latents_to_pixels(latent_fragment)
|
| 197 |
+
|
| 198 |
+
pixel_tensor_fragment = pixel_tensor_fragment.squeeze(0).permute(1, 2, 3, 0)
|
| 199 |
+
pixel_tensor_fragment = (pixel_tensor_fragment.clamp(-1, 1) + 1) / 2.0
|
| 200 |
+
video_np_fragment = (pixel_tensor_fragment.detach().cpu().float().numpy() * 255).astype(np.uint8)
|
| 201 |
+
|
| 202 |
+
for frame in video_np_fragment:
|
| 203 |
+
writer.append_data(frame)
|
| 204 |
+
|
| 205 |
+
del pixel_tensor_fragment, video_np_fragment
|
| 206 |
+
gc.collect()
|
| 207 |
+
torch.cuda.empty_cache()
|
| 208 |
|
| 209 |
+
logger.info(f"Vídeo base renderizado com sucesso em: {refined_silent_video_path}")
|
| 210 |
+
del processed_latent_fragments
|
| 211 |
gc.collect()
|
| 212 |
torch.cuda.empty_cache()
|
| 213 |
|
| 214 |
+
# --- ATO III: MASTERIZAÇÃO FINAL (ÁUDIO E HD) ---
|
| 215 |
+
current_step += 1
|
| 216 |
+
progress(current_step / TOTAL_STEPS, desc="Gerando trilha sonora...")
|
| 217 |
+
try:
|
| 218 |
+
video_with_audio_path = audio_specialist_singleton.generate_audio_for_video(
|
| 219 |
+
video_path=refined_silent_video_path,
|
| 220 |
+
prompt=global_prompt,
|
| 221 |
+
duration_seconds=self._get_video_duration(refined_silent_video_path)
|
| 222 |
+
)
|
| 223 |
+
temp_audio_path = os.path.join(self.workspace_dir, f"{base_name}_extracted_audio.aac")
|
| 224 |
+
extracted_audio_path = self._extract_audio_ffmpeg(video_with_audio_path, temp_audio_path)
|
| 225 |
+
except Exception as e:
|
| 226 |
+
logger.error(f"Falha na geração de áudio: {e}. O vídeo final será silencioso.")
|
| 227 |
+
extracted_audio_path = None
|
| 228 |
+
|
| 229 |
current_step += 1
|
| 230 |
progress(current_step / TOTAL_STEPS, desc="Aprimoramento final (HD)...")
|
| 231 |
hq_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_hq_silent.mp4")
|
| 232 |
try:
|
|
|
|
| 233 |
hd_specialist_singleton.process_video(
|
| 234 |
input_video_path=refined_silent_video_path,
|
| 235 |
output_video_path=hq_silent_video_path,
|
| 236 |
prompt=global_prompt
|
| 237 |
)
|
| 238 |
except Exception as e:
|
| 239 |
+
logger.error(f"Falha no aprimoramento HD: {e}. Usando vídeo de qualidade padrão.")
|
|
|
|
| 240 |
os.rename(refined_silent_video_path, hq_silent_video_path)
|
| 241 |
|
| 242 |
current_step += 1
|
| 243 |
progress(current_step / TOTAL_STEPS, desc="Finalizando montagem...")
|
| 244 |
final_video_path = os.path.join(self.workspace_dir, f"{base_name}_FINAL.mp4")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
if extracted_audio_path and os.path.exists(hq_silent_video_path):
|
| 247 |
+
self._combine_video_and_audio_ffmpeg(hq_silent_video_path, extracted_audio_path, final_video_path)
|
| 248 |
+
else:
|
| 249 |
+
os.rename(hq_silent_video_path, final_video_path)
|
| 250 |
|
| 251 |
+
logger.info(f"Processo concluído! Vídeo final salvo em: {final_video_path}")
|
| 252 |
+
yield {"final_path": final_video_path}
|
| 253 |
|
| 254 |
def refine_latents(self, latents: torch.Tensor,
|
| 255 |
fps: int = 24,
|
|
|
|
| 262 |
"""
|
| 263 |
logger.info(f"Refinando tensor latente com shape {latents.shape} para refinamento.")
|
| 264 |
|
|
|
|
| 265 |
_, _, num_latent_frames, latent_h, latent_w = latents.shape
|
| 266 |
|
| 267 |
+
video_scale_factor = getattr(self.vae.config, 'temporal_scale_factor', 8)
|
| 268 |
+
vae_scale_factor = getattr(self.vae.config, 'spatial_downscale_factor', 8)
|
|
|
|
|
|
|
| 269 |
|
|
|
|
| 270 |
pixel_height = latent_h * vae_scale_factor
|
| 271 |
pixel_width = latent_w * vae_scale_factor
|
| 272 |
+
|
| 273 |
+
# A fórmula inversa para o VAE causal: (N_latente - 1) * FatorDeEscala
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
pixel_frames = (num_latent_frames - 1) * video_scale_factor
|
| 275 |
|
|
|
|
| 276 |
refined_latents_tensor, _ = self.ltx_manager.refine_latents(
|
| 277 |
latents,
|
| 278 |
height=pixel_height,
|
|
|
|
| 287 |
|
| 288 |
logger.info(f"Retornando tensor latente refinado com shape: {refined_latents_tensor.shape}")
|
| 289 |
return refined_latents_tensor
|
|
|
|
| 290 |
|
|
|
|
| 291 |
def upscale_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
| 292 |
"""Interface para o UpscalerSpecialist."""
|
| 293 |
logger.info(f"Realizando upscale em tensor latente com shape {latents.shape}.")
|
|
|
|
| 295 |
|
| 296 |
def _generate_latent_tensor_internal(self, conditioning_items, ltx_params, target_resolution, total_frames_to_generate):
|
| 297 |
kwargs = {
|
| 298 |
+
**ltx_params, 'width': target_resolution[1], 'height': target_resolution[0],
|
| 299 |
'video_total_frames': total_frames_to_generate, 'video_fps': 24,
|
| 300 |
'current_fragment_index': int(time.time()), 'conditioning_items_data': conditioning_items
|
| 301 |
}
|