eeuuia commited on
Commit
ebef213
·
verified ·
1 Parent(s): 1dc8d3d

Update api/ltx_server_refactored_complete.py

Browse files
Files changed (1) hide show
  1. api/ltx_server_refactored_complete.py +346 -230
api/ltx_server_refactored_complete.py CHANGED
@@ -1,262 +1,378 @@
1
- # FILE: app.py
2
- # DESCRIPTION: Final Gradio web interface for the ADUC-SDR Video Suite.
3
- # Features dimension sliders locked to multiples of 8, a unified LTX workflow,
4
- # advanced controls, integrated SeedVR upscaling, and detailed debug logging.
5
 
6
- import gradio as gr
7
- import traceback
8
- import sys
9
- import os
10
  import logging
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # ==============================================================================
13
- # --- IMPORTAÇÃO DOS SERVIÇOS DE BACKEND E UTILS ---
14
  # ==============================================================================
15
 
16
- try:
17
- # Serviço principal para geração LTX
18
- from api.ltx_server_refactored_complete import video_generation_service
19
-
20
- # Nosso decorador de logging para depuração
21
- from api.utils.debug_utils import log_function_io
22
 
23
- # Serviço especialista para upscaling de resolução (SeedVR)
24
- from api.seedvr_server import seedvr_server_singleton as seedvr_inference_server
25
-
26
- logging.info("All backend services (LTX, SeedVR) and debug utils imported successfully.")
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  except ImportError as e:
29
- def log_function_io(func): return func
30
- logging.warning(f"Could not import a module, debug logger might be disabled. SeedVR might be unavailable. Details: {e}")
31
- if 'video_generation_service' not in locals():
32
- logging.critical(f"FATAL: Main LTX service failed to import.", exc_info=True)
33
- sys.exit(1)
34
- if 'seedvr_inference_server' not in locals():
35
- seedvr_inference_server = None
36
- logging.warning("SeedVR server could not be initialized. The SeedVR upscaling tab will be disabled.")
37
- except Exception as e:
38
- logging.critical(f"FATAL ERROR: An unexpected error occurred during backend initialization. Details: {e}", exc_info=True)
39
  sys.exit(1)
40
 
41
  # ==============================================================================
42
- # --- FUNÇÕES WRAPPER (PONTE ENTRE UI E BACKEND) ---
43
  # ==============================================================================
44
 
45
  @log_function_io
46
- def run_generate_base_video(
47
- generation_mode: str, prompt: str, neg_prompt: str, start_img: str,
48
- height: int, width: int, duration: float,
49
- fp_guidance_preset: str, fp_guidance_scale_list: str, fp_stg_scale_list: str,
50
- fp_num_inference_steps: int, fp_skip_initial_steps: int, fp_skip_final_steps: int,
51
- progress=gr.Progress(track_tqdm=True)
52
- ) -> tuple:
53
- """Wrapper para a geração do vídeo base LTX."""
54
- try:
55
- logging.info(f"[UI] Request received. Selected mode: {generation_mode}")
56
-
57
- initial_conditions = []
58
- if start_img:
59
- num_frames_estimate = int(duration * 24)
60
- items_list = [[start_img, 0, 1.0]]
61
- initial_conditions = video_generation_service.prepare_condition_items(
62
- items_list, height, width, num_frames_estimate
63
- )
64
-
65
- ltx_configs = {
66
- "guidance_preset": fp_guidance_preset,
67
- "guidance_scale_list": fp_guidance_scale_list,
68
- "stg_scale_list": fp_stg_scale_list,
69
- "num_inference_steps": fp_num_inference_steps,
70
- "skip_initial_inference_steps": fp_skip_initial_steps,
71
- "skip_final_inference_steps": fp_skip_final_steps,
72
- }
73
 
74
- video_path, tensor_path, final_seed = video_generation_service.generate_low_resolution(
75
- prompt=prompt, negative_prompt=neg_prompt,
76
- height=height, width=width, duration=duration,
77
- initial_conditions=initial_conditions, ltx_configs_override=ltx_configs
78
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- if not video_path: raise RuntimeError("Backend failed to return a valid video path.")
 
81
 
82
- new_state = {"low_res_video": video_path, "low_res_latents": tensor_path, "used_seed": final_seed}
83
- logging.info(f"[UI] Base video generation successful. Seed used: {final_seed}, Path: {video_path}")
84
- return video_path, new_state, gr.update(visible=True)
 
 
 
 
 
 
 
 
85
 
86
- except Exception as e:
87
- error_message = f"❌ An error occurred during base generation:\n{e}"
88
- logging.error(f"{error_message}\nDetails: {traceback.format_exc()}", exc_info=True)
89
- raise gr.Error(error_message)
 
 
 
 
 
 
 
 
 
90
 
91
- @log_function_io
92
- def run_ltx_refinement(state: dict, prompt: str, neg_prompt: str, progress=gr.Progress(track_tqdm=True)) -> tuple:
93
- """Wrapper para o refinamento de textura LTX."""
94
- if not state or not state.get("low_res_latents"):
95
- raise gr.Error("Error: Please generate a base video in Step 1 before refining.")
96
-
97
- try:
98
- logging.info(f"[UI] Requesting LTX refinement for latents: {state.get('low_res_latents')}")
99
- video_path, tensor_path = video_generation_service.generate_upscale_denoise(
100
- latents_path=state["low_res_latents"],
101
- prompt=prompt,
102
- negative_prompt=neg_prompt,
103
- seed=state["used_seed"]
104
- )
105
- state["refined_video_ltx"] = video_path
106
- state["refined_latents_ltx"] = tensor_path
107
- logging.info(f"[UI] LTX refinement successful. Path: {video_path}")
108
- return video_path, state
109
- except Exception as e:
110
- error_message = f"❌ An error occurred during LTX Refinement:\n{e}"
111
- logging.error(f"{error_message}\nDetails: {traceback.format_exc()}", exc_info=True)
112
- raise gr.Error(error_message)
113
 
114
- @log_function_io
115
- def run_seedvr_upscaling(state: dict, seed: int, resolution: int, batch_size: int, fps: int, progress=gr.Progress(track_tqdm=True)) -> tuple:
116
- """Wrapper para o upscale de resolução SeedVR."""
117
- if not state or not state.get("low_res_video"):
118
- raise gr.Error("Error: Please generate a base video in Step 1 before upscaling.")
119
- if not seedvr_inference_server:
120
- raise gr.Error("Error: The SeedVR upscaling server is not available.")
121
-
122
- try:
123
- logging.info(f"[UI] Requesting SeedVR upscaling for video: {state.get('low_res_video')}")
124
- def progress_wrapper(p, desc=""): progress(p, desc=desc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- output_filepath = seedvr_inference_server.run_inference(
127
- file_path=state["low_res_video"], seed=int(seed), resolution=int(resolution),
128
- batch_size=int(batch_size), fps=float(fps), progress=progress_wrapper
129
- )
130
 
131
- status_message = f"✅ Upscaling complete!\nSaved to: {output_filepath}"
132
- logging.info(f"[UI] SeedVR upscaling successful. Path: {output_filepath}")
133
- return gr.update(value=output_filepath), gr.update(value=status_message)
134
- except Exception as e:
135
- error_message = f"❌ An error occurred during SeedVR Upscaling:\n{e}"
136
- logging.error(f"{error_message}\nDetails: {traceback.format_exc()}", exc_info=True)
137
- return None, gr.update(value=error_message)
138
 
139
- # ==============================================================================
140
- # --- CONSTRUÇÃO DA INTERFACE GRADIO ---
141
- # ==============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- def build_ui():
144
- """Constrói a interface completa do Gradio."""
145
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo")) as demo:
146
- app_state = gr.State(value={"low_res_video": None, "low_res_latents": None, "used_seed": None})
147
- ui_components = {}
148
- gr.Markdown("# ADUC-SDR Video Suite - LTX & SeedVR Workflow", elem_id="main-title")
149
- with gr.Row():
150
- with gr.Column(scale=1): _build_generation_controls(ui_components)
151
- with gr.Column(scale=1):
152
- gr.Markdown("### Etapa 1: Vídeo Base Gerado")
153
- ui_components['low_res_video_output'] = gr.Video(label="O resultado aparecerá aqui", interactive=False)
154
- ui_components['used_seed_display'] = gr.Textbox(label="Seed Utilizada", interactive=False)
155
- _build_postprod_controls(ui_components)
156
- _register_event_handlers(app_state, ui_components)
157
- return demo
158
-
159
- def _build_generation_controls(ui: dict):
160
- """Constrói os componentes da UI para a Etapa 1: Geração."""
161
- gr.Markdown("### Configurações de Geração")
162
- ui['generation_mode'] = gr.Radio(label="Modo de Geração", choices=["Simples (Prompt Único)", "Narrativa (Múltiplos Prompts)"], value="Narrativa (Múltiplos Prompts)", info="Simples para uma ação contínua, Narrativa para uma sequência (uma cena por linha).")
163
- ui['prompt'] = gr.Textbox(label="Prompt(s)", value="Um leão majestoso caminha pela savana\nEle sobe em uma grande pedra e olha o horizonte", lines=4)
164
- ui['neg_prompt'] = gr.Textbox(label="Negative Prompt", value="blurry, low quality, bad anatomy, deformed", lines=2)
165
- ui['start_image'] = gr.Image(label="Imagem de Início (Opcional)", type="filepath", sources=["upload"])
166
-
167
- with gr.Accordion("Parâmetros Principais", open=True):
168
- ui['duration'] = gr.Slider(label="Duração Total (s)", value=4, step=1, minimum=1, maximum=30)
169
- with gr.Row():
170
- ui['height'] = gr.Slider(label="Height", value=432, step=8, minimum=256, maximum=1024)
171
- ui['width'] = gr.Slider(label="Width", value=768, step=8, minimum=256, maximum=1024)
172
-
173
- with gr.Accordion("Opções Avançadas LTX", open=False):
174
- gr.Markdown("#### Configurações de Passos de Inferência (First Pass)")
175
- gr.Markdown("*Deixe o valor padrão (ex: 20) ou 0 para usar a configuração do `config.yaml`.*")
176
- ui['fp_num_inference_steps'] = gr.Slider(label="Número de Passos", minimum=0, maximum=100, step=1, value=20, info="Padrão LTX: 20.")
177
- ui['fp_skip_initial_steps'] = gr.Slider(label="Pular Passos Iniciais", minimum=0, maximum=100, step=1, value=0)
178
- ui['fp_skip_final_steps'] = gr.Slider(label="Pular Passos Finais", minimum=0, maximum=100, step=1, value=0)
179
- with gr.Tabs():
180
- with gr.TabItem("Configurações de Guiagem (First Pass)"):
181
- ui['fp_guidance_preset'] = gr.Dropdown(label="Preset de Guiagem", choices=["Padrão (Recomendado)", "Agressivo", "Suave", "Customizado"], value="Padrão (Recomendado)", info="Controla o comportamento da guiagem durante a difusão.")
182
- with gr.Group(visible=False) as ui['custom_guidance_group']:
183
- gr.Markdown("⚠️ Edite as listas em formato JSON. Ex: `[1.0, 2.5, 3.0]`")
184
- ui['fp_guidance_scale_list'] = gr.Textbox(label="Lista de Guidance Scale", value="[1, 1, 6, 8, 6, 1, 1]")
185
- ui['fp_stg_scale_list'] = gr.Textbox(label="Lista de STG Scale (Movimento)", value="[0, 0, 4, 4, 4, 2, 1]")
186
-
187
- ui['generate_low_btn'] = gr.Button("1. Gerar Vídeo Base", variant="primary")
188
-
189
- def _build_postprod_controls(ui: dict):
190
- """Constrói os componentes da UI para a Etapa 2: Pós-Produção."""
191
- with gr.Group(visible=False) as ui['post_prod_group']:
192
- gr.Markdown("--- \n## Etapa 2: Pós-Produção")
193
- with gr.Tabs():
194
- with gr.TabItem("🚀 Upscaler de Textura (LTX)"):
195
- with gr.Row():
196
- with gr.Column(scale=1):
197
- gr.Markdown("Usa o prompt e a semente originais para refinar o vídeo, adicionando detalhes e texturas de alta qualidade.")
198
- ui['ltx_refine_btn'] = gr.Button("2. Aplicar Refinamento LTX", variant="primary")
199
- with gr.Column(scale=1):
200
- ui['ltx_refined_video_output'] = gr.Video(label="Vídeo com Textura Refinada", interactive=False)
201
 
202
- with gr.TabItem("✨ Upscaler de Resolução (SeedVR)"):
203
- is_seedvr_available = seedvr_inference_server is not None
204
- if not is_seedvr_available:
205
- gr.Markdown("🔴 **AVISO: O serviço SeedVR não está disponível.**")
206
- with gr.Row():
207
- with gr.Column(scale=1):
208
- ui['seedvr_seed'] = gr.Slider(minimum=0, maximum=999999, value=42, step=1, label="Seed")
209
- ui['seedvr_resolution'] = gr.Slider(minimum=720, maximum=2160, value=1080, step=8, label="Resolução Vertical Alvo")
210
- ui['seedvr_batch_size'] = gr.Slider(minimum=1, maximum=16, value=4, step=1, label="Batch Size por GPU")
211
- ui['seedvr_fps'] = gr.Number(label="FPS de Saída (0 = original)", value=0)
212
- ui['run_seedvr_btn'] = gr.Button("2. Iniciar Upscaling SeedVR", variant="primary", interactive=is_seedvr_available)
213
- with gr.Column(scale=1):
214
- ui['seedvr_video_output'] = gr.Video(label="Vídeo com Upscale SeedVR", interactive=False)
215
- ui['seedvr_status_box'] = gr.Textbox(label="Status do SeedVR", value="Aguardando...", lines=3, interactive=False)
216
-
217
- def _register_event_handlers(app_state: gr.State, ui: dict):
218
- """Registra todos os manipuladores de eventos do Gradio."""
219
- def toggle_custom_guidance(preset_choice: str) -> gr.update:
220
- return gr.update(visible=(preset_choice == "Customizado"))
221
-
222
- ui['fp_guidance_preset'].change(fn=toggle_custom_guidance, inputs=ui['fp_guidance_preset'], outputs=ui['custom_guidance_group'])
223
-
224
- def update_seed_display(state):
225
- return state.get("used_seed", "N/A")
226
-
227
- gen_inputs = [
228
- ui['generation_mode'], ui['prompt'], ui['neg_prompt'], ui['start_image'],
229
- ui['height'], ui['width'], ui['duration'],
230
- ui['fp_guidance_preset'], ui['fp_guidance_scale_list'], ui['fp_stg_scale_list'],
231
- ui['fp_num_inference_steps'], ui['fp_skip_initial_steps'], ui['fp_skip_final_steps'],
232
- ]
233
- gen_outputs = [ui['low_res_video_output'], app_state, ui['post_prod_group']]
 
 
 
 
 
 
 
234
 
235
- (ui['generate_low_btn'].click(fn=run_generate_base_video, inputs=gen_inputs, outputs=gen_outputs)
236
- .then(fn=update_seed_display, inputs=[app_state], outputs=[ui['used_seed_display']]))
 
 
 
 
237
 
238
- refine_inputs = [app_state, ui['prompt'], ui['neg_prompt']]
239
- refine_outputs = [ui['ltx_refined_video_output'], app_state]
240
- ui['ltx_refine_btn'].click(fn=run_ltx_refinement, inputs=refine_inputs, outputs=refine_outputs)
 
 
241
 
242
- if 'run_seedvr_btn' in ui and ui['run_seedvr_btn'].interactive:
243
- seedvr_inputs = [app_state, ui['seedvr_seed'], ui['seedvr_resolution'], ui['seedvr_batch_size'], ui['seedvr_fps']]
244
- seedvr_outputs = [ui['seedvr_video_output'], ui['seedvr_status_box']]
245
- ui['run_seedvr_btn'].click(fn=run_seedvr_upscaling, inputs=seedvr_inputs, outputs=seedvr_outputs)
 
 
 
 
 
246
 
247
  # ==============================================================================
248
- # --- PONTO DE ENTRADA DA APLICAÇÃO ---
249
  # ==============================================================================
250
-
251
- if __name__ == "__main__":
252
- log_level = os.environ.get("ADUC_LOG_LEVEL", "INFO").upper()
253
- logging.basicConfig(level=log_level, format='[%(levelname)s] [%(name)s] %(message)s')
254
-
255
- print("Building Gradio UI...")
256
- gradio_app = build_ui()
257
- print("Launching Gradio app...")
258
- gradio_app.queue().launch(
259
- server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
260
- server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
261
- show_error=True
262
- )
 
1
+ # FILE: api/ltx_server_refactored_complete.py
2
+ # DESCRIPTION: Final orchestrator for LTX-Video generation.
3
+ # This version includes the fix for the narrative generation overlap bug and
4
+ # consolidates all previous refactoring and debugging improvements.
5
 
6
+ import gc
7
+ import json
 
 
8
  import logging
9
+ import os
10
+ import shutil
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Tuple
16
+ import random
17
+ import torch
18
+ import yaml
19
+ import numpy as np
20
+ from huggingface_hub import hf_hub_download
21
 
22
  # ==============================================================================
23
+ # --- SETUP E IMPORTAÇÕES DO PROJETO ---
24
  # ==============================================================================
25
 
26
+ # Configuração de logging e supressão de warnings
27
+ import warnings
28
+ warnings.filterwarnings("ignore")
29
+ logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
30
+ log_level = os.environ.get("ADUC_LOG_LEVEL", "INFO").upper()
31
+ logging.basicConfig(level=log_level, format='[%(levelname)s] [%(name)s] %(message)s')
32
 
33
+ # --- Constantes de Configuração ---
34
+ DEPS_DIR = Path("/data")
35
+ LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
36
+ RESULTS_DIR = Path("/app/output")
37
+ DEFAULT_FPS = 24.0
38
+ FRAMES_ALIGNMENT = 8
39
+ LTX_REPO_ID = "Lightricks/LTX-Video"
40
+
41
+ # Garante que a biblioteca LTX-Video seja importável
42
+ def add_deps_to_path():
43
+ repo_path = str(LTX_VIDEO_REPO_DIR.resolve())
44
+ if repo_path not in sys.path:
45
+ sys.path.insert(0, repo_path)
46
+ logging.info(f"[ltx_server] LTX-Video repository added to sys.path: {repo_path}")
47
 
48
+ add_deps_to_path()
49
+
50
+ # --- Módulos da nossa Arquitetura ---
51
+ try:
52
+ from api.gpu_manager import gpu_manager
53
+ from managers.vae_manager import vae_manager_singleton
54
+ from tools.video_encode_tool import video_encode_tool_singleton
55
+ from api.ltx.ltx_utils import (
56
+ build_ltx_pipeline_on_cpu,
57
+ seed_everything,
58
+ load_image_to_tensor_with_resize_and_crop,
59
+ ConditioningItem,
60
+ )
61
+ from api.utils.debug_utils import log_function_io
62
  except ImportError as e:
63
+ logging.critical(f"A crucial import from the local API/architecture failed. Error: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
64
  sys.exit(1)
65
 
66
  # ==============================================================================
67
+ # --- FUNÇÕES AUXILIARES DO ORQUESTRADOR ---
68
  # ==============================================================================
69
 
70
  @log_function_io
71
+ def calculate_padding(orig_h: int, orig_w: int, target_h: int, target_w: int) -> Tuple[int, int, int, int]:
72
+ """Calculates symmetric padding required to meet target dimensions."""
73
+ pad_h = target_h - orig_h
74
+ pad_w = target_w - orig_w
75
+ pad_top = pad_h // 2
76
+ pad_bottom = pad_h - pad_top
77
+ pad_left = pad_w // 2
78
+ pad_right = pad_w - pad_left
79
+ return (pad_left, pad_right, pad_top, pad_bottom)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # ==============================================================================
82
+ # --- CLASSE DE SERVIÇO (O ORQUESTRADOR) ---
83
+ # ==============================================================================
84
+
85
+ class VideoService:
86
+ """
87
+ Orchestrates the high-level logic of video generation, delegating low-level
88
+ tasks to specialized managers and utility modules.
89
+ """
90
+
91
+ @log_function_io
92
+ def __init__(self):
93
+ t0 = time.perf_counter()
94
+ logging.info("Initializing VideoService Orchestrator...")
95
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
96
+
97
+ target_main_device_str = str(gpu_manager.get_ltx_device())
98
+ target_vae_device_str = str(gpu_manager.get_ltx_vae_device())
99
+ logging.info(f"LTX allocated to devices: Main='{target_main_device_str}', VAE='{target_vae_device_str}'")
100
+
101
+ self.config = self._load_config()
102
+ self._resolve_model_paths_from_cache()
103
+
104
+ self.pipeline, self.latent_upsampler = build_ltx_pipeline_on_cpu(self.config)
105
+
106
+ self.main_device = torch.device("cpu")
107
+ self.vae_device = torch.device("cpu")
108
+ self.move_to_device(main_device_str=target_main_device_str, vae_device_str=target_vae_device_str)
109
+
110
+ self._apply_precision_policy()
111
+ vae_manager_singleton.attach_pipeline(self.pipeline, device=self.vae_device, autocast_dtype=self.runtime_autocast_dtype)
112
+ logging.info(f"VideoService ready. Startup time: {time.perf_counter()-t0:.2f}s")
113
+
114
+ def _load_config(self) -> Dict:
115
+ """Loads the YAML configuration file."""
116
+ config_path = LTX_VIDEO_REPO_DIR / "configs" / "ltxv-13b-0.9.8-distilled-fp8.yaml"
117
+ logging.info(f"Loading config from: {config_path}")
118
+ with open(config_path, "r") as file:
119
+ return yaml.safe_load(file)
120
+
121
+ def _resolve_model_paths_from_cache(self):
122
+ """Finds the absolute paths to model files in the cache and updates the in-memory config."""
123
+ logging.info("Resolving model paths from Hugging Face cache...")
124
+ cache_dir = os.environ.get("HF_HOME")
125
+ try:
126
+ main_ckpt_path = hf_hub_download(repo_id=LTX_REPO_ID, filename=self.config["checkpoint_path"], cache_dir=cache_dir)
127
+ self.config["checkpoint_path"] = main_ckpt_path
128
+ logging.info(f" -> Main checkpoint resolved to: {main_ckpt_path}")
129
+
130
+ if self.config.get("spatial_upscaler_model_path"):
131
+ upscaler_path = hf_hub_download(repo_id=LTX_REPO_ID, filename=self.config["spatial_upscaler_model_path"], cache_dir=cache_dir)
132
+ self.config["spatial_upscaler_model_path"] = upscaler_path
133
+ logging.info(f" -> Spatial upscaler resolved to: {upscaler_path}")
134
+ except Exception as e:
135
+ logging.critical(f"Failed to resolve model paths. Ensure setup.py ran correctly. Error: {e}", exc_info=True)
136
+ sys.exit(1)
137
+
138
+ @log_function_io
139
+ def move_to_device(self, main_device_str: str, vae_device_str: str):
140
+ """Moves pipeline components to their designated target devices."""
141
+ target_main_device = torch.device(main_device_str)
142
+ target_vae_device = torch.device(vae_device_str)
143
+ logging.info(f"Moving LTX models -> Main Pipeline: {target_main_device}, VAE: {target_vae_device}")
144
+
145
+ self.main_device = target_main_device
146
+ self.pipeline.to(self.main_device)
147
+ self.vae_device = target_vae_device
148
+ self.pipeline.vae.to(self.vae_device)
149
+ if self.latent_upsampler: self.latent_upsampler.to(self.main_device)
150
+ logging.info("LTX models successfully moved to target devices.")
151
+
152
+ def move_to_cpu(self):
153
+ """Moves all LTX components to CPU to free VRAM for other services."""
154
+ self.move_to_device(main_device_str="cpu", vae_device_str="cpu")
155
+ if torch.cuda.is_available(): torch.cuda.empty_cache()
156
+
157
+ def finalize(self):
158
+ """Cleans up GPU memory after a generation task."""
159
+ gc.collect()
160
+ if torch.cuda.is_available():
161
+ torch.cuda.empty_cache()
162
+ try: torch.cuda.ipc_collect();
163
+ except Exception: pass
164
+
165
+ # ==========================================================================
166
+ # --- LÓGICA DE NEGÓCIO: ORQUESTRADOR PÚBLICO UNIFICADO ---
167
+ # ==========================================================================
168
+
169
+ @log_function_io
170
+ def generate_low_resolution(self, prompt: str, **kwargs) -> Tuple[Optional[str], Optional[str], Optional[int]]:
171
+ """
172
+ [UNIFIED ORCHESTRATOR] Generates a low-resolution video from a prompt.
173
+ Handles both single-line and multi-line prompts transparently.
174
+ """
175
+ logging.info("Starting unified low-resolution generation (random seed)...")
176
+ used_seed = self._get_random_seed()
177
+ seed_everything(used_seed)
178
+ logging.info(f"Using randomly generated seed: {used_seed}")
179
+
180
+ prompt_list = [p.strip() for p in prompt.splitlines() if p.strip()]
181
+ if not prompt_list: raise ValueError("Prompt is empty or contains no valid lines.")
182
 
183
+ is_narrative = len(prompt_list) > 1
184
+ logging.info(f"Generation mode detected: {'Narrative' if is_narrative else 'Simple'} ({len(prompt_list)} scene(s)).")
185
 
186
+ num_chunks = len(prompt_list)
187
+ total_frames = self._calculate_aligned_frames(kwargs.get("duration", 4.0))
188
+ frames_per_chunk = max(FRAMES_ALIGNMENT, (total_frames // num_chunks // FRAMES_ALIGNMENT) * FRAMES_ALIGNMENT)
189
+
190
+ # Overlap must be N*8+1 frames. 9 is the smallest practical value.
191
+ overlap_frames = 9 if is_narrative else 0
192
+ if is_narrative:
193
+ logging.info(f"Narrative mode: Using overlap of {overlap_frames} frames between chunks.")
194
+
195
+ temp_latent_paths = []
196
+ overlap_condition_item = None
197
 
198
+ try:
199
+ for i, chunk_prompt in enumerate(prompt_list):
200
+ logging.info(f"Processing scene {i+1}/{num_chunks}: '{chunk_prompt[:50]}...'")
201
+
202
+ if i < num_chunks - 1:
203
+ current_frames_base = frames_per_chunk
204
+ else: # Last chunk takes all remaining frames
205
+ processed_frames_base = (num_chunks - 1) * frames_per_chunk
206
+ current_frames_base = total_frames - processed_frames_base
207
+
208
+ current_frames = current_frames_base + (overlap_frames if i > 0 else 0)
209
+ # Ensure final frame count for generation is N*8+1
210
+ current_frames = self._align(current_frames, alignment_rule='n*8+1')
211
 
212
+ current_conditions = kwargs.get("initial_conditions", []) if i == 0 else []
213
+ if overlap_condition_item: current_conditions.append(overlap_condition_item)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ chunk_latents = self._generate_single_chunk_low(
216
+ prompt=chunk_prompt, num_frames=current_frames, seed=used_seed + i,
217
+ conditioning_items=current_conditions, **kwargs
218
+ )
219
+ if chunk_latents is None: raise RuntimeError(f"Failed to generate latents for scene {i+1}.")
220
+
221
+ if is_narrative and i < num_chunks - 1:
222
+ overlap_latents = chunk_latents[:, :, -overlap_frames:, :, :].clone()
223
+ overlap_condition_item = ConditioningItem(media_item=overlap_latents, media_frame_number=0, conditioning_strength=1.0)
224
+
225
+ if i > 0:
226
+ chunk_latents = chunk_latents[:, :, overlap_frames:, :, :]
227
+
228
+ chunk_path = RESULTS_DIR / f"temp_chunk_{i}_{used_seed}.pt"
229
+ torch.save(chunk_latents.cpu(), chunk_path)
230
+ temp_latent_paths.append(chunk_path)
231
+
232
+ base_filename = "narrative_video" if is_narrative else "single_video"
233
+ return self._finalize_generation(temp_latent_paths, base_filename, used_seed)
234
+ except Exception as e:
235
+ logging.error(f"Error during unified generation: {e}", exc_info=True)
236
+ return None, None, None
237
+ finally:
238
+ for path in temp_latent_paths:
239
+ if path.exists(): path.unlink()
240
+ self.finalize()
241
+
242
+ # ==========================================================================
243
+ # --- UNIDADES DE TRABALHO E HELPERS INTERNOS ---
244
+ # ==========================================================================
245
+
246
+ @log_function_io
247
+ def _generate_single_chunk_low(self, **kwargs) -> Optional[torch.Tensor]:
248
+ """[WORKER] Calls the pipeline to generate a single chunk of latents."""
249
+ height_padded, width_padded = (self._align(d) for d in (kwargs['height'], kwargs['width']))
250
+ downscale_factor = self.config.get("downscale_factor", 0.6666666)
251
+ vae_scale_factor = self.pipeline.vae_scale_factor
252
+ downscaled_height = self._align(int(height_padded * downscale_factor), vae_scale_factor)
253
+ downscaled_width = self._align(int(width_padded * downscale_factor), vae_scale_factor)
254
+
255
+ first_pass_config = self.config.get("first_pass", {}).copy()
256
+ if kwargs.get("ltx_configs_override"):
257
+ self._apply_ui_overrides(first_pass_config, kwargs["ltx_configs_override"])
258
+
259
+ pipeline_kwargs = {
260
+ "prompt": kwargs['prompt'], "negative_prompt": kwargs['negative_prompt'],
261
+ "height": downscaled_height, "width": downscaled_width, "num_frames": kwargs['num_frames'],
262
+ "frame_rate": DEFAULT_FPS, "generator": torch.Generator(device=self.main_device).manual_seed(kwargs['seed']),
263
+ "output_type": "latent", "conditioning_items": kwargs['conditioning_items'], **first_pass_config
264
+ }
265
 
266
+ with torch.autocast(device_type=self.main_device.type, dtype=self.runtime_autocast_dtype, enabled="cuda" in self.main_device.type):
267
+ latents_raw = self.pipeline(**pipeline_kwargs).images
 
 
268
 
269
+ return latents_raw.to(self.main_device)
 
 
 
 
 
 
270
 
271
+ @log_function_io
272
+ def _finalize_generation(self, temp_latent_paths: List[Path], base_filename: str, seed: int) -> Tuple[str, str, int]:
273
+ """Consolidates latents, decodes them to video, and saves final artifacts."""
274
+ logging.info("Finalizing generation: decoding latents to video.")
275
+ all_tensors_cpu = [torch.load(p) for p in temp_latent_paths]
276
+ final_latents = torch.cat(all_tensors_cpu, dim=2)
277
+
278
+ final_latents_path = RESULTS_DIR / f"latents_{base_filename}_{seed}.pt"
279
+ torch.save(final_latents, final_latents_path)
280
+ logging.info(f"Final latents saved to: {final_latents_path}")
281
+
282
+ pixel_tensor = vae_manager_singleton.decode(
283
+ final_latents, decode_timestep=float(self.config.get("decode_timestep", 0.05))
284
+ )
285
+ video_path = self._save_and_log_video(pixel_tensor, f"{base_filename}_{seed}")
286
+ return str(video_path), str(final_latents_path), seed
287
 
288
+ @log_function_io
289
+ def prepare_condition_items(self, items_list: List, height: int, width: int, num_frames: int) -> List[ConditioningItem]:
290
+ """[UNIFIED] Prepares ConditioningItems from a mixed list of file paths and tensors."""
291
+ if not items_list: return []
292
+ height_padded, width_padded = self._align(height), self._align(width)
293
+ padding_values = calculate_padding(height, width, height_padded, width_padded)
294
+
295
+ conditioning_items = []
296
+ for media_item, frame, weight in items_list:
297
+ if isinstance(media_item, str):
298
+ tensor = load_image_to_tensor_with_resize_and_crop(media_item, height, width)
299
+ tensor = torch.nn.functional.pad(tensor, padding_values)
300
+ tensor = tensor.to(self.main_device, dtype=self.runtime_autocast_dtype)
301
+ elif isinstance(media_item, torch.Tensor):
302
+ tensor = media_item.to(self.main_device, dtype=self.runtime_autocast_dtype)
303
+ else:
304
+ logging.warning(f"Unknown conditioning media type: {type(media_item)}. Skipping.")
305
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ safe_frame = max(0, min(int(frame), num_frames - 1))
308
+ conditioning_items.append(ConditioningItem(tensor, safe_frame, float(weight)))
309
+ return conditioning_items
310
+
311
+ def _apply_ui_overrides(self, config_dict: Dict, overrides: Dict):
312
+ """Applies advanced settings from the UI to a config dictionary."""
313
+ # Override step counts
314
+ for key in ["num_inference_steps", "skip_initial_inference_steps", "skip_final_inference_steps"]:
315
+ ui_value = overrides.get(key)
316
+ if ui_value and ui_value > 0:
317
+ config_dict[key] = ui_value
318
+ logging.info(f"Override: '{key}' set to {ui_value} by UI.")
319
+
320
+ # Override guidance settings
321
+ preset = overrides.get("guidance_preset", "Padrão (Recomendado)")
322
+ guidance_overrides = {}
323
+ if preset == "Agressivo":
324
+ guidance_overrides = {"guidance_scale": [1, 2, 8, 12, 8, 2, 1], "stg_scale": [0, 0, 5, 6, 5, 3, 2]}
325
+ elif preset == "Suave":
326
+ guidance_overrides = {"guidance_scale": [1, 1, 4, 5, 4, 1, 1], "stg_scale": [0, 0, 2, 2, 2, 1, 0]}
327
+ elif preset == "Customizado":
328
+ try:
329
+ guidance_overrides["guidance_scale"] = json.loads(overrides["guidance_scale_list"])
330
+ guidance_overrides["stg_scale"] = json.loads(overrides["stg_scale_list"])
331
+ except Exception as e:
332
+ logging.warning(f"Failed to parse custom guidance values: {e}. Using defaults.")
333
+
334
+ if guidance_overrides:
335
+ config_dict.update(guidance_overrides)
336
+ logging.info(f"Applying '{preset}' guidance preset overrides.")
337
+
338
+ def _save_and_log_video(self, pixel_tensor: torch.Tensor, base_filename: str) -> Path:
339
+ with tempfile.TemporaryDirectory() as temp_dir:
340
+ temp_path = os.path.join(temp_dir, f"{base_filename}.mp4")
341
+ video_encode_tool_singleton.save_video_from_tensor(pixel_tensor, temp_path, fps=DEFAULT_FPS)
342
+ final_path = RESULTS_DIR / f"{base_filename}.mp4"
343
+ shutil.move(temp_path, final_path)
344
+ logging.info(f"Video saved successfully to: {final_path}")
345
+ return final_path
346
 
347
+ def _apply_precision_policy(self):
348
+ precision = str(self.config.get("precision", "bfloat16")).lower()
349
+ if precision in ["float8_e4m3fn", "bfloat16"]: self.runtime_autocast_dtype = torch.bfloat16
350
+ elif precision == "mixed_precision": self.runtime_autocast_dtype = torch.float16
351
+ else: self.runtime_autocast_dtype = torch.float32
352
+ logging.info(f"Runtime precision policy set for autocast: {self.runtime_autocast_dtype}")
353
 
354
+ def _align(self, dim: int, alignment: int = FRAMES_ALIGNMENT, alignment_rule: str = 'default') -> int:
355
+ """Aligns a dimension to the nearest multiple of `alignment`."""
356
+ if alignment_rule == 'n*8+1':
357
+ return ((dim - 1) // alignment) * alignment + 1
358
+ return ((dim - 1) // alignment + 1) * alignment
359
 
360
+ def _calculate_aligned_frames(self, duration_s: float, min_frames: int = 1) -> int:
361
+ num_frames = int(round(duration_s * DEFAULT_FPS))
362
+ # Para a duração total, sempre arredondamos para cima para o múltiplo de 8 mais próximo
363
+ aligned_frames = self._align(num_frames, alignment=FRAMES_ALIGNMENT)
364
+ return max(aligned_frames, min_frames)
365
+
366
+ def _get_random_seed(self) -> int:
367
+ """Always generates and returns a new random seed."""
368
+ return random.randint(0, 2**32 - 1)
369
 
370
  # ==============================================================================
371
+ # --- INSTANCIAÇÃO SINGLETON ---
372
  # ==============================================================================
373
+ try:
374
+ video_generation_service = VideoService()
375
+ logging.info("Global VideoService orchestrator instance created successfully.")
376
+ except Exception as e:
377
+ logging.critical(f"Failed to initialize VideoService: {e}", exc_info=True)
378
+ sys.exit(1)