Eueuiaa commited on
Commit
9ac7175
·
verified ·
1 Parent(s): d0d8baa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +436 -196
app.py CHANGED
@@ -1,222 +1,462 @@
1
- # app_refactored_with_postprod.py (com Presets de Guiagem e Opções LTX Completas)
 
 
2
 
3
- import gradio as gr
 
 
 
4
  import os
 
 
 
5
  import sys
 
 
6
  import traceback
 
7
  from pathlib import Path
 
8
 
9
- # --- Import dos Serviços de Backend ---
10
- try:
11
- from api.ltx_server_refactored import video_generation_service
12
- except ImportError:
13
- print("ERRO FATAL: Não foi possível importar 'video_generation_service' de 'api.ltx_server_refactored'.")
14
- sys.exit(1)
15
-
16
- try:
17
- from api.seedvr_server import SeedVRServer
18
- except ImportError:
19
- print("AVISO: Não foi possível importar SeedVRServer. A aba de upscaling SeedVR será desativada.")
20
- SeedVRServer = None
21
-
22
- seedvr_inference_server = SeedVRServer() if SeedVRServer else None
23
-
24
- # --- ESTADO DA SESSÃO ---
25
- def create_initial_state():
26
- return {"low_res_video": None, "low_res_latents": None, "used_seed": None}
27
-
28
- # --- FUNÇÕES WRAPPER PARA A UI ---
29
-
30
- def run_generate_base_video(
31
- # Parâmetros de Geração
32
- generation_mode, prompt, neg_prompt, start_img, height, width, duration, cfg, seed, randomize_seed,
33
- fp_guidance_preset, fp_guidance_scale_list, fp_stg_scale_list, fp_timesteps_list,
34
- progress=gr.Progress(track_tqdm=True)
35
- ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
37
- Função wrapper que decide qual pipeline de backend chamar, passando todas as configurações LTX.
 
 
38
  """
39
- print(f"UI: Iniciando geração no modo: {generation_mode}")
40
-
41
- try:
42
- initial_conditions = []
43
- if start_img:
44
- num_frames_estimate = int(duration * 24)
45
- items_list = [[start_img, 0, 1.0]]
46
- initial_conditions = video_generation_service.prepare_condition_items(items_list, height, width, num_frames_estimate)
47
-
48
- used_seed = None if randomize_seed else seed
49
-
50
- # Agrupa todas as configurações LTX em um único dicionário para o backend
51
- ltx_configs = {
52
- "guidance_preset": fp_guidance_preset,
53
- "guidance_scale_list": fp_guidance_scale_list,
54
- "stg_scale_list": fp_stg_scale_list,
55
- "timesteps_list": fp_timesteps_list,
56
- }
57
 
58
- # Decide qual função de backend chamar com base no modo
59
- if generation_mode == "Narrativa (Múltiplos Prompts)":
60
- video_path, tensor_path, final_seed = video_generation_service.generate_narrative_low(
61
- prompt=prompt, negative_prompt=neg_prompt,
62
- height=height, width=width, duration=duration,
63
- guidance_scale=cfg, seed=used_seed,
64
- initial_conditions=initial_conditions,
65
- ltx_configs_override=ltx_configs,
66
- )
67
- else: # Modo "Simples (Prompt Único)"
68
- video_path, tensor_path, final_seed = video_generation_service.generate_single_low(
69
- prompt=prompt, negative_prompt=neg_prompt,
70
- height=height, width=width, duration=duration,
71
- guidance_scale=cfg, seed=used_seed,
72
- initial_conditions=initial_conditions,
73
- ltx_configs_override=ltx_configs,
74
- )
75
-
76
- new_state = {"low_res_video": video_path, "low_res_latents": tensor_path, "used_seed": final_seed}
77
 
78
- return video_path, new_state, gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- except Exception as e:
81
- error_message = f"❌ Ocorreu um erro na Geração Base:\n{e}"
82
- print(f"{error_message}\nDetalhes: {traceback.format_exc()}")
83
- raise gr.Error(error_message)
 
 
 
 
 
 
 
84
 
85
- def run_ltx_refinement(state, prompt, neg_prompt, cfg, progress=gr.Progress(track_tqdm=True)):
86
- if not state or not state.get("low_res_latents"):
87
- raise gr.Error("Erro: Gere um vídeo base primeiro na Etapa 1.")
88
- try:
89
- video_path, tensor_path = video_generation_service.generate_upscale_denoise(
90
- latents_path=state["low_res_latents"], prompt=prompt,
91
- negative_prompt=neg_prompt, guidance_scale=cfg, seed=state["used_seed"]
 
 
92
  )
93
- state["refined_video_ltx"] = video_path; state["refined_latents_ltx"] = tensor_path
94
- return video_path, state
95
- except Exception as e:
96
- raise gr.Error(f"Erro no Refinamento LTX: {e}")
97
-
98
- def run_seedvr_upscaling(state, seed, resolution, batch_size, fps, progress=gr.Progress(track_tqdm=True)):
99
- if not state or not state.get("low_res_video"):
100
- raise gr.Error("Erro: Gere um vídeo base primeiro na Etapa 1.")
101
- if not seedvr_inference_server:
102
- raise gr.Error("Erro: O servidor SeedVR não está disponível.")
103
- try:
104
- def progress_wrapper(p, desc=""): progress(p, desc=desc)
105
- output_filepath = seedvr_inference_server.run_inference(
106
- file_path=state["low_res_video"], seed=seed, resolution=resolution,
107
- batch_size=batch_size, fps=fps, progress=progress_wrapper
108
  )
109
- return gr.update(value=output_filepath), gr.update(value=f"✅ Concluído!\nSalvo em: {output_filepath}")
110
- except Exception as e:
111
- return None, gr.update(value=f"❌ Erro no SeedVR:\n{e}")
 
 
 
 
 
 
 
 
112
 
113
- # --- DEFINIÇÃO DA INTERFACE GRADIO ---
114
- with gr.Blocks() as demo:
115
- gr.Markdown("# LTX Video - Geração e Pós-Produção por Etapas")
116
-
117
- app_state = gr.State(value=create_initial_state())
118
 
119
- with gr.Row():
120
- with gr.Column(scale=1):
121
- gr.Markdown("### Etapa 1: Configurações de Geração")
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- generation_mode_input = gr.Radio(
124
- label="Modo de Geração", choices=["Simples (Prompt Único)", "Narrativa (Múltiplos Prompts)"],
125
- value="Narrativa (Múltiplos Prompts)", info="Simples para uma ação, Narrativa para uma sequência (uma cena por linha)."
126
- )
127
- prompt_input = 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)
128
- neg_prompt_input = gr.Textbox(label="Negative Prompt", value="blurry, low quality, bad anatomy", lines=2)
129
- start_image = gr.Image(label="Imagem de Início (Opcional)", type="filepath", sources=["upload"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- with gr.Accordion("Parâmetros Principais", open=False):
132
- duration_input = gr.Slider(label="Duração Total (s)", value=1, step=1, minimum=1, maximum=40)
133
- with gr.Row():
134
- height_input = gr.Slider(label="Height", value=720, step=32, minimum=256, maximum=1024)
135
- width_input = gr.Slider(label="Width", value=720, step=32, minimum=256, maximum=1024)
136
- with gr.Row():
137
- seed_input = gr.Number(label="Seed", value=42, precision=0)
138
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
139
-
140
- with gr.Accordion("Opções Adicionais LTX (Avançado)", open=False):
141
- cfg_input = gr.Slider(label="Guidance Scale (CFG)", info="Afeta o refinamento (se usado) e não tem efeito no First Pass dos modelos 'distilled'.", value=0.0, step=1, minimum=0.0, maximum=10.0)
142
- fp_num_inference_steps = gr.Slider(label="Passos de Inferência (First Pass)", minimum=10, maximum=100, step=1, value=10)
143
- ship_initial_inference_steps = gr.Slider(label="Passos de Inferência (Ship First)", minimum=0, maximum=100, step=1, value=0)
144
- ship_final_inference_steps = gr.Slider(label="Passos de Inferência (Ship Last)", minimum=0, maximum=100, step=1, value=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- with gr.Tabs():
147
- with gr.TabItem("Guiagem (First Pass)"):
148
- fp_guidance_preset = gr.Dropdown(
149
- label="Preset de Guiagem",
150
- choices=["Padrão (Recomendado)", "Agressivo", "Suave", "Customizado"],
151
- value="Padrão (Recomendado)", info="Muda o comportamento da guiagem ao longo da difusão."
152
- )
153
- with gr.Group(visible=False) as custom_guidance_group:
154
- gr.Markdown("⚠️ Edite as listas em formato JSON. Ex: `[1, 2, 3]`")
155
- fp_guidance_scale_list = gr.Textbox(label="Lista de Guidance Scale", value="[1, 1, 6, 8, 6, 1, 1]")
156
- fp_stg_scale_list = gr.Textbox(label="Lista de STG Scale (Movimento)", value="[0, 0, 4, 4, 4, 2, 1]")
157
- fp_timesteps_list = gr.Textbox(label="Lista de Guidance Timesteps", value="[1.0, 0.996, 0.9933, 0.9850, 0.9767, 0.9008, 0.6180]")
158
-
159
- generate_low_btn = gr.Button("1. Gerar Vídeo Base", variant="primary")
160
-
161
- with gr.Column(scale=1):
162
- gr.Markdown("### Vídeo Base Gerado")
163
- low_res_video_output = gr.Video(label="O resultado da Etapa 1 aparecerá aqui", interactive=False)
164
-
165
- with gr.Group(visible=False) as post_prod_group:
166
- gr.Markdown("## Etapa 2: Pós-Produção")
167
-
168
- with gr.Tabs():
169
- with gr.TabItem("🚀 Upscaler Textura (LTX)"):
170
- with gr.Row():
171
- with gr.Column(scale=1):
172
- gr.Markdown("Reutiliza o prompt e CFG para refinar a textura.")
173
- ltx_refine_btn = gr.Button("Aplicar Refinamento LTX", variant="primary")
174
- with gr.Column(scale=1):
175
- ltx_refined_video_output = gr.Video(label="Vídeo com Textura Refinada", interactive=False)
176
 
177
- with gr.TabItem("✨ Upscaler SeedVR"):
178
- with gr.Row():
179
- with gr.Column(scale=1):
180
- seedvr_seed = gr.Slider(minimum=0, maximum=999999, value=42, step=1, label="Seed")
181
- seedvr_resolution = gr.Slider(minimum=720, maximum=1440, value=1072, step=8, label="Resolução Vertical")
182
- seedvr_batch_size = gr.Slider(minimum=1, maximum=16, value=4, step=1, label="Batch Size por GPU")
183
- seedvr_fps_output = gr.Number(label="FPS de Saída (0 = original)", value=0)
184
- run_seedvr_button = gr.Button("Iniciar Upscaling SeedVR", variant="primary", interactive=(seedvr_inference_server is not None))
185
- with gr.Column(scale=1):
186
- seedvr_video_output = gr.Video(label="Vídeo com Upscale SeedVR", interactive=False)
187
- seedvr_status_box = gr.Textbox(label="Status", value="Aguardando...", lines=3, interactive=False)
188
-
189
- # --- LÓGICA DE EVENTOS ---
190
- def update_custom_guidance_visibility(preset_choice):
191
- return gr.update(visible=(preset_choice == "Customizado"))
192
 
193
- fp_guidance_preset.change(fn=update_custom_guidance_visibility, inputs=fp_guidance_preset, outputs=custom_guidance_group)
194
 
195
- all_ltx_inputs = [
196
- fp_guidance_preset, fp_guidance_scale_list, fp_stg_scale_list, fp_timesteps_list
197
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- generate_low_btn.click(
200
- fn=run_generate_base_video,
201
- inputs=[
202
- generation_mode_input, prompt_input, neg_prompt_input, start_image, height_input, width_input,
203
- duration_input, cfg_input, seed_input, randomize_seed,
204
- *all_ltx_inputs
205
- ],
206
- outputs=[low_res_video_output, app_state, post_prod_group]
207
- )
 
 
 
 
 
 
 
 
208
 
209
- ltx_refine_btn.click(
210
- fn=run_ltx_refinement,
211
- inputs=[app_state, prompt_input, neg_prompt_input, cfg_input],
212
- outputs=[ltx_refined_video_output, app_state]
213
- )
 
 
 
214
 
215
- run_seedvr_button.click(
216
- fn=run_seedvr_upscaling,
217
- inputs=[app_state, seedvr_seed, seedvr_resolution, seedvr_batch_size, seedvr_fps_output],
218
- outputs=[seedvr_video_output, seedvr_status_box]
219
- )
 
 
 
 
 
 
 
 
 
220
 
221
- if __name__ == "__main__":
222
- demo.queue().launch(server_name="0.0.0.0", server_port=7860, debug=True, show_error=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FILE: ltx_server_refactored_complete.py
2
+ # DESCRIPTION: Backend service for video generation using LTX-Video pipeline.
3
+ # Features modular generation, narrative chunking, and resource management.
4
 
5
+ import gc
6
+ import io
7
+ import json
8
+ import logging
9
  import os
10
+ import random
11
+ import shutil
12
+ import subprocess
13
  import sys
14
+ import tempfile
15
+ import time
16
  import traceback
17
+ import warnings
18
  from pathlib import Path
19
+ from typing import Dict, List, Optional, Tuple
20
 
21
+ import torch
22
+ import yaml
23
+ from einops import rearrange
24
+ from huggingface_hub import hf_hub_download
25
+
26
+ # ==============================================================================
27
+ # --- INITIAL SETUP & CONFIGURATION ---
28
+ # ==============================================================================
29
+
30
+ # Suppress excessive logs from external libraries
31
+ warnings.filterwarnings("ignore")
32
+ logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
33
+ logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
34
+
35
+ # --- CONSTANTS ---
36
+ DEPS_DIR = Path("/data")
37
+ LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
38
+ BASE_CONFIG_PATH = LTX_VIDEO_REPO_DIR / "configs"
39
+ DEFAULT_CONFIG_FILE = BASE_CONFIG_PATH / "ltxv-13b-0.9.8-distilled-fp8.yaml"
40
+ LTX_REPO_ID = "Lightricks/LTX-Video"
41
+ RESULTS_DIR = Path("/app/output")
42
+ DEFAULT_FPS = 24.0
43
+ FRAMES_ALIGNMENT = 8
44
+
45
+ # --- DEPENDENCY PATH SETUP ---
46
+ # Ensures the LTX-Video library can be imported
47
+ def add_deps_to_path():
48
+ """Adds the LTX repository directory to the Python system path."""
49
+ repo_path = str(LTX_VIDEO_REPO_DIR.resolve())
50
+ if repo_path not in sys.path:
51
+ sys.path.insert(0, repo_path)
52
+ logging.info(f"Repo added to sys.path: {repo_path}")
53
+
54
+ add_deps_to_path()
55
+
56
+ # --- PROJECT IMPORTS ---
57
+ # These must come after the path setup
58
+ from api.gpu_manager import gpu_manager
59
+ from ltx_video.models.autoencoders.vae_encode import (normalize_latents, un_normalize_latents)
60
+ from ltx_video.pipelines.pipeline_ltx_video import (ConditioningItem, LTXMultiScalePipeline, adain_filter_latent)
61
+ from ltx_video.pipelines.pipeline_ltx_video import create_ltx_video_pipeline, create_latent_upsampler
62
+ from ltx_video.utils.inference_utils import load_image_to_tensor_with_resize_and_crop
63
+ from managers.vae_manager import vae_manager_singleton
64
+ from tools.video_encode_tool import video_encode_tool_singleton
65
+
66
+
67
+ # ==============================================================================
68
+ # --- UTILITY & HELPER FUNCTIONS ---
69
+ # ==============================================================================
70
+
71
+ def seed_everything(seed: int):
72
+ """Sets the seed for reproducibility across all relevant libraries."""
73
+ random.seed(seed)
74
+ os.environ['PYTHONHASHSEED'] = str(seed)
75
+ np.random.seed(seed)
76
+ torch.manual_seed(seed)
77
+ torch.cuda.manual_seed_all(seed)
78
+ # Potentially faster, but less reproducible
79
+ # torch.backends.cudnn.deterministic = False
80
+ # torch.backends.cudnn.benchmark = True
81
+
82
+ def calculate_padding(orig_h: int, orig_w: int, target_h: int, target_w: int) -> Tuple[int, int, int, int]:
83
+ """Calculates symmetric padding values to reach a target dimension."""
84
+ pad_h = target_h - orig_h
85
+ pad_w = target_w - orig_w
86
+ pad_top = pad_h // 2
87
+ pad_bottom = pad_h - pad_top
88
+ pad_left = pad_w // 2
89
+ pad_right = pad_w - pad_left
90
+ return (pad_left, pad_right, pad_top, pad_bottom)
91
+
92
+ def log_tensor_info(tensor: torch.Tensor, name: str = "Tensor"):
93
+ """Logs detailed information about a PyTorch tensor for debugging."""
94
+ if not isinstance(tensor, torch.Tensor):
95
+ logging.debug(f"'{name}' is not a tensor.")
96
+ return
97
+
98
+ info_str = (
99
+ f"--- Tensor: {name} ---\n"
100
+ f" - Shape: {tuple(tensor.shape)}\n"
101
+ f" - Dtype: {tensor.dtype}\n"
102
+ f" - Device: {tensor.device}\n"
103
+ )
104
+ if tensor.numel() > 0:
105
+ try:
106
+ info_str += (
107
+ f" - Min: {tensor.min().item():.4f} | "
108
+ f"Max: {tensor.max().item():.4f} | "
109
+ f"Mean: {tensor.mean().item():.4f}\n"
110
+ )
111
+ except Exception:
112
+ pass # Fails on some dtypes
113
+ logging.debug(info_str + "----------------------")
114
+
115
+
116
+ # ==============================================================================
117
+ # --- VIDEO SERVICE CLASS ---
118
+ # ==============================================================================
119
+
120
+ class VideoService:
121
  """
122
+ Backend service for orchestrating video generation using the LTX-Video pipeline.
123
+ Encapsulates model loading, state management, and the logic for multi-stage
124
+ video generation (low-resolution, upscale).
125
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ def __init__(self):
128
+ """Initializes the service, loads models, and configures the environment."""
129
+ t0 = time.perf_counter()
130
+ logging.info("Initializing VideoService...")
131
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
132
+
133
+ self.config = self._load_config(DEFAULT_CONFIG_FILE)
134
+ self._tmp_dirs = set()
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ self.pipeline, self.latent_upsampler = self._load_models_on_cpu()
137
+
138
+ target_device = gpu_manager.get_ltx_device()
139
+ self.device = torch.device("cpu") # Default device
140
+ self.move_to_device(target_device)
141
+
142
+ self._apply_precision_policy()
143
+ vae_manager_singleton.attach_pipeline(
144
+ self.pipeline,
145
+ device=self.device,
146
+ autocast_dtype=self.runtime_autocast_dtype
147
+ )
148
 
149
+ logging.info(f"VideoService ready. Startup time: {time.perf_counter()-t0:.2f}s")
150
+
151
+ # ==========================================================================
152
+ # --- LIFECYCLE & MODEL MANAGEMENT ---
153
+ # ==========================================================================
154
+
155
+ def _load_config(self, config_path: Path) -> Dict:
156
+ """Loads the YAML configuration file."""
157
+ logging.info(f"Loading config from: {config_path}")
158
+ with open(config_path, "r") as file:
159
+ return yaml.safe_load(file)
160
 
161
+ def _load_models_on_cpu(self) -> Tuple[LTXMultiScalePipeline, Optional[torch.nn.Module]]:
162
+ """Downloads and loads the pipeline and upsampler checkpoints onto the CPU."""
163
+ t0 = time.perf_counter()
164
+
165
+ logging.info("Downloading main checkpoint...")
166
+ distilled_model_path = hf_hub_download(
167
+ repo_id=LTX_REPO_ID,
168
+ filename=self.config["checkpoint_path"],
169
+ token=os.getenv("HF_TOKEN"),
170
  )
171
+ self.config["checkpoint_path"] = distilled_model_path
172
+
173
+ pipeline = create_ltx_video_pipeline(
174
+ ckpt_path=self.config["checkpoint_path"],
175
+ precision=self.config["precision"],
176
+ device="cpu", # Load on CPU first
177
+ # Pass other config values directly
178
+ **{k: v for k, v in self.config.items() if k in create_ltx_video_pipeline.__code__.co_varnames}
 
 
 
 
 
 
 
179
  )
180
+
181
+ latent_upsampler = None
182
+ if self.config.get("spatial_upscaler_model_path"):
183
+ logging.info("Downloading spatial upscaler checkpoint...")
184
+ spatial_upscaler_path = hf_hub_download(
185
+ repo_id=LTX_REPO_ID,
186
+ filename=self.config["spatial_upscaler_model_path"],
187
+ token=os.getenv("HF_TOKEN")
188
+ )
189
+ self.config["spatial_upscaler_model_path"] = spatial_upscaler_path
190
+ latent_upsampler = create_latent_upsampler(self.config["spatial_upscaler_model_path"], device="cpu")
191
 
192
+ logging.info(f"Models loaded on CPU in {time.perf_counter()-t0:.2f}s")
193
+ return pipeline, latent_upsampler
 
 
 
194
 
195
+ def move_to_device(self, device_str: str):
196
+ """Moves all relevant models to the specified device (e.g., 'cuda:0' or 'cpu')."""
197
+ target_device = torch.device(device_str)
198
+ if self.device == target_device:
199
+ logging.info(f"Models are already on the target device: {device_str}")
200
+ return
201
+
202
+ logging.info(f"Moving models to {device_str}...")
203
+ self.device = target_device
204
+ self.pipeline.to(self.device)
205
+ if self.latent_upsampler:
206
+ self.latent_upsampler.to(self.device)
207
+
208
+ if device_str == "cpu" and torch.cuda.is_available():
209
+ torch.cuda.empty_cache()
210
 
211
+ logging.info(f"Models successfully moved to {self.device}.")
212
+
213
+ def finalize(self, keep_paths: Optional[List[str]] = None):
214
+ """Cleans up GPU memory and temporary directories."""
215
+ logging.debug("Finalizing resources...")
216
+ gc.collect()
217
+ if torch.cuda.is_available():
218
+ torch.cuda.empty_cache()
219
+ try:
220
+ torch.cuda.ipc_collect()
221
+ except Exception:
222
+ pass
223
+
224
+ # Optional: Clean up temporary directories if needed (logic can be added here)
225
+
226
+
227
+ # ==========================================================================
228
+ # --- PUBLIC ORCHESTRATORS ---
229
+ # These are the main entry points called by the frontend.
230
+ # ==========================================================================
231
+
232
+ def generate_narrative_low(self, prompt: str, **kwargs) -> Tuple[Optional[str], Optional[str], Optional[int]]:
233
+ """
234
+ [ORCHESTRATOR] Generates a video from a multi-line prompt, creating a sequence of scenes.
235
+
236
+ Returns:
237
+ A tuple of (video_path, latents_path, used_seed).
238
+ """
239
+ logging.info("Starting narrative low-res generation...")
240
+ used_seed = self._resolve_seed(kwargs.get("seed"))
241
+ seed_everything(used_seed)
242
+
243
+ prompt_list = [p.strip() for p in prompt.splitlines() if p.strip()]
244
+ if not prompt_list:
245
+ raise ValueError("Prompt is empty or contains no valid lines.")
246
+
247
+ num_chunks = len(prompt_list)
248
+ total_frames = self._calculate_aligned_frames(kwargs.get("duration", 4.0))
249
+ frames_per_chunk = (total_frames // num_chunks // FRAMES_ALIGNMENT) * FRAMES_ALIGNMENT
250
+ overlap_frames = self.config.get("overlap_frames", 8)
251
+
252
+ all_latents_paths = []
253
+ overlap_condition_item = None
254
+
255
+ try:
256
+ for i, chunk_prompt in enumerate(prompt_list):
257
+ logging.info(f"Generating narrative chunk {i+1}/{num_chunks}: '{chunk_prompt[:50]}...'")
258
+
259
+ current_frames = frames_per_chunk
260
+ if i > 0:
261
+ current_frames += overlap_frames
262
+
263
+ # Use initial image conditions only for the first chunk
264
+ current_conditions = kwargs.get("initial_conditions", []) if i == 0 else []
265
+ if overlap_condition_item:
266
+ current_conditions.append(overlap_condition_item)
267
+
268
+ chunk_latents = self._generate_single_chunk_low(
269
+ prompt=chunk_prompt,
270
+ num_frames=current_frames,
271
+ seed=used_seed + i,
272
+ conditioning_items=current_conditions,
273
+ **kwargs
274
+ )
275
+
276
+ if chunk_latents is None:
277
+ raise RuntimeError(f"Failed to generate latents for chunk {i+1}.")
278
+
279
+ # Create overlap for the next chunk
280
+ if i < num_chunks - 1:
281
+ overlap_latents = chunk_latents[:, :, -overlap_frames:, :, :].clone()
282
+ log_tensor_info(overlap_latents, f"Overlap Latents from chunk {i+1}")
283
+ overlap_condition_item = ConditioningItem(
284
+ media_item=overlap_latents, media_frame_number=0, conditioning_strength=1.0
285
+ )
286
+
287
+ # Trim the overlap from the current chunk before saving
288
+ if i > 0:
289
+ chunk_latents = chunk_latents[:, :, overlap_frames:, :, :]
290
+
291
+ # Save chunk latents to disk to manage memory
292
+ chunk_path = RESULTS_DIR / f"chunk_{i}_{used_seed}.pt"
293
+ torch.save(chunk_latents.cpu(), chunk_path)
294
+ all_latents_paths.append(chunk_path)
295
 
296
+ # Concatenate, decode, and save the final video
297
+ return self._finalize_generation(all_latents_paths, "narrative_video", used_seed)
298
+
299
+ except Exception as e:
300
+ logging.error(f"Error during narrative generation: {e}")
301
+ traceback.print_exc()
302
+ return None, None, None
303
+ finally:
304
+ # Clean up intermediate chunk files
305
+ for path in all_latents_paths:
306
+ if os.path.exists(path):
307
+ os.remove(path)
308
+ self.finalize()
309
+
310
+
311
+ def generate_single_low(self, **kwargs) -> Tuple[Optional[str], Optional[str], Optional[int]]:
312
+ """
313
+ [ORCHESTRATOR] Generates a video from a single prompt in one go.
314
+
315
+ Returns:
316
+ A tuple of (video_path, latents_path, used_seed).
317
+ """
318
+ logging.info("Starting single-prompt low-res generation...")
319
+ used_seed = self._resolve_seed(kwargs.get("seed"))
320
+ seed_everything(used_seed)
321
+
322
+ try:
323
+ total_frames = self._calculate_aligned_frames(kwargs.get("duration", 4.0), min_frames=9)
324
 
325
+ final_latents = self._generate_single_chunk_low(
326
+ num_frames=total_frames,
327
+ seed=used_seed,
328
+ conditioning_items=kwargs.get("initial_conditions", []),
329
+ **kwargs
330
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ if final_latents is None:
333
+ raise RuntimeError("Failed to generate latents.")
334
+
335
+ # Save latents to a single file, then decode and save video
336
+ latents_path = RESULTS_DIR / f"single_{used_seed}.pt"
337
+ torch.save(final_latents.cpu(), latents_path)
338
+ return self._finalize_generation([latents_path], "single_video", used_seed)
339
+
340
+ except Exception as e:
341
+ logging.error(f"Error during single generation: {e}")
342
+ traceback.print_exc()
343
+ return None, None, None
344
+ finally:
345
+ self.finalize()
 
346
 
 
347
 
348
+ # ==========================================================================
349
+ # --- INTERNAL WORKER UNITS ---
350
+ # ==========================================================================
351
+
352
+ def _generate_single_chunk_low(
353
+ self, prompt: str, negative_prompt: str, height: int, width: int, num_frames: int, seed: int,
354
+ conditioning_items: List[ConditioningItem], ltx_configs_override: Optional[Dict], **kwargs
355
+ ) -> Optional[torch.Tensor]:
356
+ """
357
+ [WORKER] Generates a single chunk of latents. This is the core generation unit.
358
+ Returns the raw latents tensor on the target device, or None on failure.
359
+ """
360
+ height_padded, width_padded = (self._align(d) for d in (height, width))
361
+ downscale_factor = self.config.get("downscale_factor", 0.6666666)
362
+ vae_scale_factor = self.pipeline.vae_scale_factor
363
+
364
+ downscaled_height = self._align(int(height_padded * downscale_factor), vae_scale_factor)
365
+ downscaled_width = self._align(int(width_padded * downscale_factor), vae_scale_factor)
366
+
367
+ first_pass_config = self.config.get("first_pass", {}).copy()
368
+ if ltx_configs_override:
369
+ first_pass_config.update(self._prepare_guidance_overrides(ltx_configs_override))
370
+
371
+ pipeline_kwargs = {
372
+ "prompt": prompt,
373
+ "negative_prompt": negative_prompt,
374
+ "height": downscaled_height,
375
+ "width": downscaled_width,
376
+ "num_frames": num_frames,
377
+ "frame_rate": DEFAULT_FPS,
378
+ "generator": torch.Generator(device=self.device).manual_seed(seed),
379
+ "output_type": "latent",
380
+ "conditioning_items": conditioning_items,
381
+ **first_pass_config
382
+ }
383
+
384
+ logging.debug(f"Pipeline call args: { {k: v for k, v in pipeline_kwargs.items() if k != 'conditioning_items'} }")
385
+
386
+ with torch.autocast(device_type=self.device.type, dtype=self.runtime_autocast_dtype, enabled=self.device.type == 'cuda'):
387
+ latents_raw = self.pipeline(**pipeline_kwargs).images
388
+
389
+ log_tensor_info(latents_raw, f"Raw Latents for '{prompt[:40]}...'")
390
+ return latents_raw
391
+
392
+
393
+ # ==========================================================================
394
+ # --- HELPERS & UTILITY METHODS ---
395
+ # ==========================================================================
396
 
397
+ def _finalize_generation(self, latents_paths: List[Path], base_filename: str, seed: int) -> Tuple[str, str, int]:
398
+ """
399
+ Loads latents from paths, concatenates them, decodes to video, and saves both.
400
+ """
401
+ logging.info("Finalizing generation: decoding latents to video.")
402
+ # Load all tensors and concatenate them on the CPU first
403
+ all_tensors_cpu = [torch.load(p) for p in latents_paths]
404
+ final_latents_cpu = torch.cat(all_tensors_cpu, dim=2)
405
+
406
+ # Save final combined latents
407
+ final_latents_path = RESULTS_DIR / f"latents_{base_filename}_{seed}.pt"
408
+ torch.save(final_latents_cpu, final_latents_path)
409
+ logging.info(f"Final latents saved to: {final_latents_path}")
410
+
411
+ # Move to GPU for decoding
412
+ final_latents_gpu = final_latents_cpu.to(self.device)
413
+ log_tensor_info(final_latents_gpu, "Final Concatenated Latents")
414
 
415
+ with torch.autocast(device_type=self.device.type, dtype=self.runtime_autocast_dtype, enabled=self.device.type == 'cuda'):
416
+ pixel_tensor = vae_manager_singleton.decode(
417
+ final_latents_gpu,
418
+ decode_timestep=float(self.config.get("decode_timestep", 0.05))
419
+ )
420
+
421
+ video_path = self._save_and_log_video(pixel_tensor, f"{base_filename}_{seed}")
422
+ return str(video_path), str(final_latents_path), seed
423
 
424
+ def prepare_condition_items(self, items_list: List, height: int, width: int, num_frames: int) -> List[ConditioningItem]:
425
+ """Prepares a list of ConditioningItem objects from file paths or tensors."""
426
+ if not items_list:
427
+ return []
428
+
429
+ height_padded, width_padded = self._align(height), self._align(width)
430
+ padding_values = calculate_padding(height, width, height_padded, width_padded)
431
+
432
+ conditioning_items = []
433
+ for media, frame, weight in items_list:
434
+ tensor = self._prepare_conditioning_tensor(media, height, width, padding_values)
435
+ safe_frame = max(0, min(int(frame), num_frames - 1))
436
+ conditioning_items.append(ConditioningItem(tensor, safe_frame, float(weight)))
437
+ return conditioning_items
438
 
439
+ def _prepare_conditioning_tensor(self, media_path: str, height: int, width: int, padding: Tuple) -> torch.Tensor:
440
+ """Loads and processes an image to be a conditioning tensor."""
441
+ tensor = load_image_to_tensor_with_resize_and_crop(media_path, height, width)
442
+ tensor = torch.nn.functional.pad(tensor, padding)
443
+ log_tensor_info(tensor, f"Prepared Conditioning Tensor from {media_path}")
444
+ return tensor.to(self.device, dtype=self.runtime_autocast_dtype)
445
+
446
+ def _prepare_guidance_overrides(self, ltx_configs: Dict) -> Dict:
447
+ """Parses UI presets for guidance into pipeline-compatible arguments."""
448
+ overrides = {}
449
+ preset = ltx_configs.get("guidance_preset", "Padrão (Recomendado)")
450
+
451
+ # Default LTX values are used if preset is 'Padrão'
452
+ if preset == "Agressivo":
453
+ overrides["guidance_scale"] = [1, 2, 8, 12, 8, 2, 1]
454
+ overrides["stg_scale"] = [0, 0, 5, 6, 5, 3, 2]
455
+ elif preset == "Suave":
456
+ overrides["guidance_scale"] = [1, 1, 4, 5, 4, 1, 1]
457
+ overrides["stg_scale"] = [0, 0, 2, 2, 2, 1, 0]
458
+ elif preset == "Customizado":
459
+ try:
460
+ overrides["guidance_scale"] = json.loads(ltx_configs["guidance_scale_list"])
461
+ overrides["stg_scale"] = json.loads(ltx_configs["stg_scale_list"])
462
+ except (json.JSONDecodeError, KeyError