Update deformes4D_engine.py
Browse files- deformes4D_engine.py +124 -133
deformes4D_engine.py
CHANGED
|
@@ -26,6 +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 ltx_video.models.autoencoders.causal_video_autoencoder import CausalVideoAutoencoder
|
| 30 |
from ltx_video.models.autoencoders.vae_encode import vae_encode, vae_decode
|
| 31 |
|
|
@@ -80,7 +81,7 @@ class Deformes4DEngine:
|
|
| 80 |
video_tensor = video_tensor.squeeze(0).permute(1, 2, 3, 0)
|
| 81 |
video_tensor = (video_tensor.clamp(-1, 1) + 1) / 2.0
|
| 82 |
video_np = (video_tensor.detach().cpu().float().numpy() * 255).astype(np.uint8)
|
| 83 |
-
with imageio.get_writer(path, fps=fps, codec='libx264', quality=8) as writer:
|
| 84 |
for frame in video_np: writer.append_data(frame)
|
| 85 |
|
| 86 |
def _preprocess_image_for_latent_conversion(self, image: Image.Image, target_resolution: tuple) -> Image.Image:
|
|
@@ -94,56 +95,48 @@ class Deformes4DEngine:
|
|
| 94 |
tensor = (tensor * 2.0) - 1.0
|
| 95 |
return self.pixels_to_latents(tensor)
|
| 96 |
|
| 97 |
-
def
|
| 98 |
-
if not os.path.exists(video_path): return
|
| 99 |
-
cmd = ['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-count_frames',
|
| 100 |
-
'-show_entries', 'stream=nb_read_frames', '-of', 'default=nokey=1:noprint_wrappers=1', video_path]
|
| 101 |
-
try:
|
| 102 |
-
result = subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 103 |
-
return int(result.stdout.strip())
|
| 104 |
-
except Exception: return None
|
| 105 |
-
|
| 106 |
-
def _trim_last_frame_ffmpeg(self, input_path: str, output_path: str) -> bool:
|
| 107 |
-
frame_count = self._get_video_frame_count(input_path)
|
| 108 |
-
if frame_count is None or frame_count < 2:
|
| 109 |
-
if os.path.exists(input_path): os.rename(input_path, output_path)
|
| 110 |
-
return True
|
| 111 |
-
vf_filter = f"select='lt(n,{frame_count - 1})',setpts=PTS-STARTPTS"
|
| 112 |
-
cmd_list = ['ffmpeg', '-y', '-i', input_path, '-vf', vf_filter, '-an', output_path]
|
| 113 |
-
try:
|
| 114 |
-
subprocess.run(cmd_list, check=True, capture_output=True, text=True, encoding='utf-8')
|
| 115 |
-
return True
|
| 116 |
-
except subprocess.CalledProcessError: return False
|
| 117 |
-
|
| 118 |
-
def concatenate_videos_ffmpeg(self, video_paths: list[str], output_path: str) -> str:
|
| 119 |
-
if not video_paths: raise gr.Error("Nenhum fragmento de vídeo para montar.")
|
| 120 |
-
list_file_path = os.path.join(self.workspace_dir, "concat_list.txt")
|
| 121 |
-
with open(list_file_path, 'w', encoding='utf-8') as f:
|
| 122 |
-
for path in video_paths: f.write(f"file '{os.path.abspath(path)}'\n")
|
| 123 |
-
cmd_list = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', list_file_path, '-c', 'copy', output_path]
|
| 124 |
-
try:
|
| 125 |
-
subprocess.run(cmd_list, check=True, capture_output=True, text=True)
|
| 126 |
-
except subprocess.CalledProcessError as e:
|
| 127 |
-
raise gr.Error(f"Falha na montagem final do vídeo. Detalhes: {e.stderr}")
|
| 128 |
-
return output_path
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def _generate_video_and_audio(self, silent_video_path: str, audio_prompt: str, base_name: str) -> str:
|
| 133 |
try:
|
| 134 |
result = subprocess.run(
|
| 135 |
-
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1",
|
| 136 |
capture_output=True, text=True, check=True)
|
| 137 |
-
|
| 138 |
except Exception:
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
# NÚCLEO DA LÓGICA ADUC-SDR
|
| 149 |
def generate_full_movie(self, keyframes: list, global_prompt: str, storyboard: list,
|
|
@@ -152,6 +145,9 @@ class Deformes4DEngine:
|
|
| 152 |
video_resolution: int, use_continuity_director: bool,
|
| 153 |
progress: gr.Progress = gr.Progress()):
|
| 154 |
|
|
|
|
|
|
|
|
|
|
| 155 |
FPS = 24
|
| 156 |
FRAMES_PER_LATENT_CHUNK = 8
|
| 157 |
ECO_LATENT_CHUNKS = 2
|
|
@@ -171,18 +167,18 @@ class Deformes4DEngine:
|
|
| 171 |
keyframe_paths = [item[0] if isinstance(item, tuple) else item for item in keyframes]
|
| 172 |
story_history = ""
|
| 173 |
|
| 174 |
-
|
| 175 |
eco_latent_for_next_loop = None
|
| 176 |
dejavu_latent_for_next_loop = None
|
| 177 |
|
| 178 |
num_transitions_to_generate = len(keyframe_paths) - 1
|
| 179 |
-
|
| 180 |
|
| 181 |
-
|
| 182 |
for i in range(num_transitions_to_generate):
|
| 183 |
fragment_index = i + 1
|
| 184 |
-
|
|
|
|
| 185 |
|
|
|
|
| 186 |
past_keyframe_path = keyframe_paths[i - 1] if i > 0 else keyframe_paths[i]
|
| 187 |
start_keyframe_path = keyframe_paths[i]
|
| 188 |
destination_keyframe_path = keyframe_paths[i + 1]
|
|
@@ -193,16 +189,12 @@ class Deformes4DEngine:
|
|
| 193 |
transition_type, motion_prompt = decision["transition_type"], decision["motion_prompt"]
|
| 194 |
story_history += f"\n- Ato {fragment_index}: {motion_prompt}"
|
| 195 |
|
| 196 |
-
|
| 197 |
expected_height, expected_width = 768, 1152
|
| 198 |
downscale_factor = 2 / 3
|
| 199 |
downscaled_height = self._quantize_to_multiple(int(expected_height * downscale_factor), 8)
|
| 200 |
downscaled_width = self._quantize_to_multiple(int(expected_width * downscale_factor), 8)
|
| 201 |
target_resolution_tuple = (downscaled_height, downscaled_width)
|
| 202 |
-
final_resolution_tuple = (expected_height, expected_width)
|
| 203 |
-
|
| 204 |
|
| 205 |
-
|
| 206 |
conditioning_items = []
|
| 207 |
if eco_latent_for_next_loop is None:
|
| 208 |
img_start = self._preprocess_image_for_latent_conversion(Image.open(start_keyframe_path).convert("RGB"), target_resolution_tuple)
|
|
@@ -225,106 +217,105 @@ class Deformes4DEngine:
|
|
| 225 |
if transition_type == "cut":
|
| 226 |
eco_latent_for_next_loop, dejavu_latent_for_next_loop = None, None
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
list_latents_fragments.append(latent_upscale_fragment)
|
| 232 |
-
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
tensors_para_concatenar = []
|
| 238 |
-
target_device = self.device
|
| 239 |
-
for idx, tensor_frag in enumerate(upscaled_latent_fragments):
|
| 240 |
-
tensor_on_target_device = tensor_frag.to(target_device)
|
| 241 |
-
if idx < len(upscaled_latent_fragments) - 1:
|
| 242 |
-
tensors_para_concatenar.append(tensor_on_target_device[:, :, :-1, :, :])
|
| 243 |
-
else:
|
| 244 |
-
tensors_para_concatenar.append(tensor_on_target_device)
|
| 245 |
-
|
| 246 |
final_concatenated_latents = torch.cat(tensors_para_concatenar, dim=2)
|
| 247 |
-
|
| 248 |
-
progress((num_transitions_to_generate + 1) / (num_transitions_to_generate + 2), desc="Pós-produção (Upscale e Refinamento)...")
|
| 249 |
-
base_name = f"final_movie_hq_{int(time.time())}"
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
# Pós-produção: Upscale + Refine
|
| 253 |
-
#high_quality_video_path = self._render_and_post_process(
|
| 254 |
-
# final_concatenated_latents,
|
| 255 |
-
# base_name=base_name,
|
| 256 |
-
# expected_height=720,
|
| 257 |
-
# expected_width=720,
|
| 258 |
-
# fps=24
|
| 259 |
-
#)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
video_path = os.path.join(self.workspace_dir, f"{base_name}_HQ.mp4")
|
| 263 |
-
|
| 264 |
-
final_pixel_tensor = self.latents_to_pixels(final_concatenated_latents)
|
| 265 |
-
self.save_video_from_tensor(final_pixel_tensor, video_path, fps=24)
|
| 266 |
-
logger.info(f"Vídeo final salvo em: {video_path}")
|
| 267 |
|
|
|
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
#)
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
def refine_latents(self, latents: torch.Tensor,
|
| 281 |
fps: int = 24,
|
| 282 |
denoise_strength: float = 0.35,
|
| 283 |
refine_steps: int = 12,
|
| 284 |
motion_prompt: str = "refining video, improving details, cinematic quality") -> torch.Tensor:
|
| 285 |
-
"""
|
| 286 |
-
|
| 287 |
-
"""
|
| 288 |
-
logger.info(f"Recebido tensor latente com shape {latents.shape} para refinamento.")
|
| 289 |
-
|
| 290 |
-
# Extrai as dimensões do tensor latente.
|
| 291 |
_, _, num_frames, latent_h, latent_w = latents.shape
|
| 292 |
-
|
| 293 |
-
# Obtém o fator de escala do VAE (geralmente 8).
|
| 294 |
vae_scale_factor = self.vae.config.scaling_factor if hasattr(self.vae.config, 'scaling_factor') else 8
|
| 295 |
-
|
| 296 |
-
# Calcula as dimensões em PIXELS correspondentes.
|
| 297 |
-
pixel_height = latent_h * vae_scale_factor
|
| 298 |
-
pixel_width = latent_w * vae_scale_factor
|
| 299 |
|
| 300 |
-
# Chama o ltx_manager com os parâmetros corretos para evitar o AssertionError.
|
| 301 |
refined_latents_tensor, _ = self.ltx_manager.refine_latents(
|
| 302 |
-
latents,
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
video_total_frames=num_frames,
|
| 306 |
-
video_fps=fps,
|
| 307 |
-
motion_prompt=motion_prompt,
|
| 308 |
-
current_fragment_index=int(time.time()),
|
| 309 |
-
denoise_strength=denoise_strength,
|
| 310 |
-
refine_steps=refine_steps
|
| 311 |
-
)
|
| 312 |
|
| 313 |
-
logger.info(f"Retornando tensor latente refinado com shape: {refined_latents_tensor.shape}")
|
| 314 |
return refined_latents_tensor
|
| 315 |
-
|
| 316 |
|
| 317 |
def upscale_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
| 318 |
-
"""
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
"""
|
| 322 |
-
logger.info(f"Recebido tensor latente com shape {latents.shape} para upscale.")
|
| 323 |
-
upscaled_latents = upscaler_specialist_singleton.upscale(latents)
|
| 324 |
-
logger.info(f"Retornando tensor latente upscaled com novo shape: {upscaled_latents.shape}")
|
| 325 |
-
return upscaled_latents
|
| 326 |
|
| 327 |
-
|
| 328 |
def _generate_latent_tensor_internal(self, conditioning_items, ltx_params, target_resolution, total_frames_to_generate):
|
| 329 |
kwargs = {
|
| 330 |
**ltx_params, 'width': target_resolution[1], 'height': target_resolution[0],
|
|
|
|
| 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 # Importa o novo especialista
|
| 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 |
|
|
|
|
| 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:
|
|
|
|
| 95 |
tensor = (tensor * 2.0) - 1.0
|
| 96 |
return self.pixels_to_latents(tensor)
|
| 97 |
|
| 98 |
+
def _get_video_duration(self, video_path: str) -> float:
|
| 99 |
+
if not os.path.exists(video_path): return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
try:
|
| 101 |
result = subprocess.run(
|
| 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,
|
|
|
|
| 145 |
video_resolution: int, use_continuity_director: bool,
|
| 146 |
progress: gr.Progress = gr.Progress()):
|
| 147 |
|
| 148 |
+
TOTAL_STEPS = len(keyframes) - 1 + 5 # Fragmentos + 5 etapas de pós-produção
|
| 149 |
+
current_step = 0
|
| 150 |
+
|
| 151 |
FPS = 24
|
| 152 |
FRAMES_PER_LATENT_CHUNK = 8
|
| 153 |
ECO_LATENT_CHUNKS = 2
|
|
|
|
| 167 |
keyframe_paths = [item[0] if isinstance(item, tuple) else item for item in keyframes]
|
| 168 |
story_history = ""
|
| 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 |
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 = 768, 1152
|
| 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)
|
| 196 |
target_resolution_tuple = (downscaled_height, downscaled_width)
|
|
|
|
|
|
|
| 197 |
|
|
|
|
| 198 |
conditioning_items = []
|
| 199 |
if eco_latent_for_next_loop is None:
|
| 200 |
img_start = self._preprocess_image_for_latent_conversion(Image.open(start_keyframe_path).convert("RGB"), target_resolution_tuple)
|
|
|
|
| 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 base...")
|
| 236 |
+
refined_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_refined_silent.mp4")
|
| 237 |
+
final_pixel_tensor = self.latents_to_pixels(final_concatenated_latents)
|
| 238 |
+
self.save_video_from_tensor(final_pixel_tensor, refined_silent_video_path, fps=FPS)
|
|
|
|
| 239 |
|
| 240 |
+
# Limpeza de VRAM antes da próxima etapa pesada
|
| 241 |
+
del final_pixel_tensor, final_concatenated_latents, processed_latent_fragments, tensors_para_concatenar
|
| 242 |
+
gc.collect()
|
| 243 |
+
torch.cuda.empty_cache()
|
| 244 |
+
|
| 245 |
+
# --- ATO II: PRIMEIRA MASTERIZAÇÃO (VÍDEO + MÚSICA) ---
|
| 246 |
+
current_step += 1
|
| 247 |
+
progress(current_step / TOTAL_STEPS, desc="Gerando trilha sonora...")
|
| 248 |
+
try:
|
| 249 |
+
# Assume que seu audio_specialist tem um método que retorna o caminho do áudio
|
| 250 |
+
audio_path = audio_specialist_singleton.generate_standalone_audio(
|
| 251 |
+
prompt=global_prompt,
|
| 252 |
+
duration_seconds=self._get_video_duration(refined_silent_video_path),
|
| 253 |
+
output_dir=self.workspace_dir
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
refined_video_with_audio_path = os.path.join(self.workspace_dir, f"{base_name}_refined_with_audio.mp4")
|
| 257 |
+
self._combine_video_and_audio_ffmpeg(refined_silent_video_path, audio_path, refined_video_with_audio_path)
|
| 258 |
+
logger.info(f"Primeira masterização com áudio salva em: {refined_video_with_audio_path}")
|
| 259 |
+
# Você pode opcionalmente retornar este vídeo aqui como uma prévia
|
| 260 |
+
# yield {"preview_path": refined_video_with_audio_path}
|
| 261 |
+
except Exception as e:
|
| 262 |
+
logger.error(f"Falha na geração ou combinação de áudio: {e}. Prosseguindo sem áudio.")
|
| 263 |
+
audio_path = None # Garante que a variável exista
|
| 264 |
+
refined_video_with_audio_path = refined_silent_video_path # Usa o vídeo silencioso como fallback
|
| 265 |
+
|
| 266 |
+
# --- ATO III: MASTERIZAÇÃO FINAL (APLICAÇÃO DE HD) ---
|
| 267 |
+
current_step += 1
|
| 268 |
+
progress(current_step / TOTAL_STEPS, desc="Aprimoramento final (HD)...")
|
| 269 |
+
hq_silent_video_path = os.path.join(self.workspace_dir, f"{base_name}_hq_silent.mp4")
|
| 270 |
+
try:
|
| 271 |
+
# O Especialista HD processa o vídeo silencioso refinado
|
| 272 |
+
hd_specialist_singleton.process_video(
|
| 273 |
+
input_video_path=refined_silent_video_path,
|
| 274 |
+
output_video_path=hq_silent_video_path,
|
| 275 |
+
prompt=global_prompt
|
| 276 |
+
)
|
| 277 |
+
except Exception as e:
|
| 278 |
+
logger.error(f"Falha no processo de aprimoramento HD. Usando o vídeo refinado como fallback. Erro: {e}")
|
| 279 |
+
# Se o HD falhar, usamos o vídeo refinado (silencioso) como base para o final
|
| 280 |
+
os.rename(refined_silent_video_path, hq_silent_video_path)
|
| 281 |
+
|
| 282 |
+
current_step += 1
|
| 283 |
+
progress(current_step / TOTAL_STEPS, desc="Finalizando montagem...")
|
| 284 |
+
final_video_path = os.path.join(self.workspace_dir, f"{base_name}_FINAL.mp4")
|
| 285 |
+
|
| 286 |
+
#if audio_path and os.path.exists(audio_path):
|
| 287 |
+
# # Se o áudio foi gerado, combina o vídeo de ALTA QUALIDADE com ele
|
| 288 |
+
# self._combine_video_and_audio_ffmpeg(hq_silent_video_path, audio_path, final_video_path)
|
| 289 |
+
#else:
|
| 290 |
+
# # Se não houver áudio, apenas renomeia o vídeo de alta qualidade
|
| 291 |
+
# os.rename(hq_silent_video_path, final_video_path)
|
| 292 |
+
|
| 293 |
+
logger.info(f"Processo concluído! Vídeo final salvo em: {hq_silent_video_path}")
|
| 294 |
+
yield {"final_path": final_video_path}
|
| 295 |
|
| 296 |
def refine_latents(self, latents: torch.Tensor,
|
| 297 |
fps: int = 24,
|
| 298 |
denoise_strength: float = 0.35,
|
| 299 |
refine_steps: int = 12,
|
| 300 |
motion_prompt: str = "refining video, improving details, cinematic quality") -> torch.Tensor:
|
| 301 |
+
"""Aplica um passe de refinamento (denoise) em um tensor latente."""
|
| 302 |
+
logger.info(f"Refinando tensor latente com shape {latents.shape}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
_, _, num_frames, latent_h, latent_w = latents.shape
|
|
|
|
|
|
|
| 304 |
vae_scale_factor = self.vae.config.scaling_factor if hasattr(self.vae.config, 'scaling_factor') else 8
|
| 305 |
+
pixel_height, pixel_width = latent_h * vae_scale_factor, latent_w * vae_scale_factor
|
|
|
|
|
|
|
|
|
|
| 306 |
|
|
|
|
| 307 |
refined_latents_tensor, _ = self.ltx_manager.refine_latents(
|
| 308 |
+
latents, height=pixel_height, width=pixel_width, video_total_frames=num_frames,
|
| 309 |
+
video_fps=fps, motion_prompt=motion_prompt, current_fragment_index=int(time.time()),
|
| 310 |
+
denoise_strength=denoise_strength, refine_steps=refine_steps)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
|
|
|
| 312 |
return refined_latents_tensor
|
|
|
|
| 313 |
|
| 314 |
def upscale_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
| 315 |
+
"""Interface para o UpscalerSpecialist."""
|
| 316 |
+
logger.info(f"Realizando upscale em tensor latente com shape {latents.shape}.")
|
| 317 |
+
return upscaler_specialist_singleton.upscale(latents)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
|
|
|
| 319 |
def _generate_latent_tensor_internal(self, conditioning_items, ltx_params, target_resolution, total_frames_to_generate):
|
| 320 |
kwargs = {
|
| 321 |
**ltx_params, 'width': target_resolution[1], 'height': target_resolution[0],
|