eeuuia commited on
Commit
4f4406c
·
verified ·
1 Parent(s): 1cacf10

Update api/ltx/ltx_aduc_pipeline.py

Browse files
Files changed (1) hide show
  1. api/ltx/ltx_aduc_pipeline.py +155 -334
api/ltx/ltx_aduc_pipeline.py CHANGED
@@ -1,375 +1,196 @@
1
  # FILE: api/ltx/ltx_aduc_pipeline.py
2
- # DESCRIPTION: Final orchestrator for LTX-Video generation.
3
- # This version internalizes conditioning item preparation, accepting a raw
4
- # list of media items directly in its main generation function for maximum simplicity and encapsulation.
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, Union
16
-
17
  import torch
18
- import yaml
19
- import numpy as np
20
- from PIL import Image
21
- from huggingface_hub import hf_hub_download
 
 
 
 
 
 
 
 
22
 
23
  # ==============================================================================
24
- # --- SETUP E IMPORTAÇÕES DO PROJETO ---
25
  # ==============================================================================
26
 
27
- # Configuração de logging e supressão de warnings
28
- import warnings
29
- warnings.filterwarnings("ignore")
30
- logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
31
- log_level = os.environ.get("ADUC_LOG_LEVEL", "INFO").upper()
32
- logging.basicConfig(level=log_level, format='[%(levelname)s] [%(name)s] %(message)s')
33
-
34
- # --- Constantes de Configuração ---
35
- DEPS_DIR = Path("/data")
36
- LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
37
- RESULTS_DIR = Path("/app/output")
38
- DEFAULT_FPS = 24.0
39
- FRAMES_ALIGNMENT = 8
40
- LTX_REPO_ID = "Lightricks/LTX-Video"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
 
 
42
 
43
- # --- Módulos da nossa Arquitetura ---
44
- try:
45
- from managers.gpu_manager import gpu_manager
46
- from api.ltx.vae_aduc_pipeline import vae_ltx_aduc_pipeline
47
- from tools.video_encode_tool import video_encode_tool_singleton
48
- from api.ltx.ltx_utils import build_ltx_pipeline_on_cpu, seed_everything
49
- from api.ltx.ltx_aduc_manager import LatentConditioningItem, ltx_aduc_manager
50
- from utils.debug_utils import log_function_io
51
- except ImportError as e:
52
- logging.critical(f"A crucial import from the local API/architecture failed. Error: {e}", exc_info=True)
53
- sys.exit(1)
54
 
55
  # ==============================================================================
56
- # --- CLASSE DE SERVIÇO (O ORQUESTRADOR) ---
57
  # ==============================================================================
58
 
59
-
60
  class LtxAducPipeline:
61
  """
62
- Orchestrates the high-level logic of video generation, with internalized
63
- conditioning item preparation.
64
  """
65
-
66
- @log_function_io
67
  def __init__(self):
68
- t0 = time.time()
69
- logging.info("Initializing VideoService Orchestrator...")
70
- RESULTS_DIR.mkdir(parents=True, exist_ok=True)
71
-
72
- target_main_device_str = str(gpu_manager.get_ltx_device())
73
- target_vae_device_str = str(gpu_manager.get_ltx_vae_device())
74
- logging.info(f"LTX allocated to devices: Main='{target_main_device_str}', VAE='{target_vae_device_str}'")
75
 
76
- self.config = self._load_config()
77
- self.pipeline, self.latent_upsampler = build_ltx_pipeline_on_cpu(self.config)
78
-
79
- self.main_device = torch.device("cpu")
80
- self.vae_device = torch.device("cpu")
81
- self.move_to_device(main_device_str=target_main_device_str, vae_device_str=target_vae_device_str)
82
-
83
- self._apply_precision_policy()
84
- logging.info(f"VideoService ready. Startup time: {time.time() - t0:.2f}s")
85
-
86
- def _load_config(self) -> Dict:
87
- """Loads the YAML configuration file."""
88
- config_path = LTX_VIDEO_REPO_DIR / "configs" / "ltxv-13b-0.9.8-distilled-fp8.yaml"
89
- with open(config_path, "r") as file:
90
- return yaml.safe_load(file)
91
-
92
- def _resolve_model_paths_from_cache(self):
93
- """Finds the absolute paths to model files in the cache and updates the in-memory config."""
94
- logging.info("Resolving model paths from Hugging Face cache...")
95
- cache_dir = os.environ.get("HF_HOME")
96
- try:
97
- main_ckpt_path = hf_hub_download(repo_id=LTX_REPO_ID, filename=self.config["checkpoint_path"], cache_dir=cache_dir)
98
- self.config["checkpoint_path"] = main_ckpt_path
99
- if self.config.get("spatial_upscaler_model_path"):
100
- upscaler_path = hf_hub_download(repo_id=LTX_REPO_ID, filename=self.config["spatial_upscaler_model_path"], cache_dir=cache_dir)
101
- self.config["spatial_upscaler_model_path"] = upscaler_path
102
- except Exception as e:
103
- logging.critical(f"Failed to resolve model paths. Ensure setup.py ran correctly. Error: {e}", exc_info=True)
104
- sys.exit(1)
105
-
106
- @log_function_io
107
- def move_to_device(self, main_device_str: str, vae_device_str: str):
108
- """Moves pipeline components to their designated target devices."""
109
- target_main_device = torch.device(main_device_str)
110
- target_vae_device = torch.device(vae_device_str)
111
- self.main_device = target_main_device
112
- self.vae_device = target_vae_device
113
- self.pipeline.to(self.main_device)
114
- self.pipeline.vae.to(self.vae_device)
115
- if self.latent_upsampler: self.latent_upsampler.to(self.main_device)
116
- logging.info("LTX models successfully moved to target devices.")
117
-
118
- def move_to_cpu(self):
119
- """Moves all LTX components to CPU to free VRAM for other services."""
120
- self.move_to_device(main_device_str="cpu", vae_device_str="cpu")
121
- if torch.cuda.is_available(): torch.cuda.empty_cache()
122
-
123
- def finalize(self):
124
- """Cleans up GPU memory after a generation task."""
125
- gc.collect()
126
- if torch.cuda.is_available(): torch.cuda.empty_cache()
127
- try: torch.cuda.ipc_collect();
128
- except Exception: pass
129
 
130
- # ==========================================================================
131
- # --- LÓGICA DE NEGÓCIO: ORQUESTRADOR PÚBLICO UNIFICADO ---
132
- # ==========================================================================
133
 
134
- @log_function_io
135
- def generate_low_resolution(
136
  self,
137
  prompt_list: List[str],
138
- initial_media_items: Optional[List[Tuple[Union[str, Image.Image, torch.Tensor], int, float]]] = None,
139
- **kwargs
140
- ) -> Tuple[Optional[str], Optional[str], Optional[int]]:
 
 
 
141
  """
142
- [UNIFIED ORCHESTRATOR] Generates a low-resolution video from a prompt and a raw list of media items.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  """
144
- logging.info("Starting unified low-resolution generation...")
 
 
 
 
 
145
  used_seed = self._get_random_seed()
146
- seed_everything(used_seed)
147
- logging.info(f"Using randomly generated seed: {used_seed}")
148
 
149
- if not prompt_list: raise ValueError("Prompt is empty or contains no valid lines.")
150
-
151
- is_narrative = len(prompt_list) > 1
152
  num_chunks = len(prompt_list)
153
- total_frames = self._calculate_aligned_frames(kwargs.get("duration", 4.0))
154
- frames_per_chunk = max(FRAMES_ALIGNMENT, (total_frames // num_chunks // FRAMES_ALIGNMENT) * FRAMES_ALIGNMENT)
155
- overlap_frames = 9 if is_narrative else 0
156
-
157
- initial_conditions = []
158
- if initial_media_items:
159
- logging.info("Preparing initial conditioning items from raw media list...")
160
- initial_conditions = vae_ltx_aduc_pipeline.generate_conditioning_items(
161
- media_items=[item[0] for item in initial_media_items],
162
- target_frames=[item[1] for item in initial_media_items],
163
- strengths=[item[2] for item in initial_media_items],
164
- target_resolution=(kwargs['height'], kwargs['width'])
165
- )
166
-
167
- temp_latent_paths = []
168
- overlap_condition_item: Optional[LatentConditioningItem] = None
169
-
170
- try:
171
- for i, chunk_prompt in enumerate(prompt_list):
172
- logging.info(f"Processing scene {i+1}/{num_chunks}: '{chunk_prompt[:50]}...'")
173
-
174
- if i < num_chunks - 1:
175
- current_frames_base = frames_per_chunk
176
- else:
177
- processed_frames_base = (num_chunks - 1) * frames_per_chunk
178
- current_frames_base = total_frames - processed_frames_base
179
-
180
- current_frames = current_frames_base + (overlap_frames if i > 0 else 0)
181
- current_frames = self._align(current_frames, alignment_rule='n*8+1')
182
 
183
- current_conditions = initial_conditions if i == 0 else []
184
- if overlap_condition_item: current_conditions.append(overlap_condition_item)
185
-
186
- chunk_latents = self._generate_single_chunk_low(
187
- prompt=chunk_prompt, num_frames=current_frames, seed=used_seed + i,
188
- conditioning_items=current_conditions, **kwargs
189
- )
190
- if chunk_latents is None: raise RuntimeError(f"Failed to generate latents for scene {i+1}.")
191
 
192
- if is_narrative and i < num_chunks - 1:
193
- overlap_latents = chunk_latents[:, :, -overlap_frames:, :, :].clone()
194
- overlap_condition_item = LatentConditioningItem(
195
- latent_tensor=overlap_latents.cpu(),
196
- media_frame_number=0,
197
- conditioning_strength=1.0
198
- )
199
-
200
- if i > 0: chunk_latents = chunk_latents[:, :, overlap_frames:, :, :]
201
-
202
- chunk_path = RESULTS_DIR / f"temp_chunk_{i}_{used_seed}.pt"
203
- torch.save(chunk_latents.cpu(), chunk_path)
204
- temp_latent_paths.append(chunk_path)
205
 
206
- base_filename = "narrative_video" if is_narrative else "single_video"
207
- all_tensors_cpu = [torch.load(p) for p in temp_latent_paths]
208
- final_latents = torch.cat(all_tensors_cpu, dim=2)
209
-
210
- video_path, latents_path = self._finalize_generation(final_latents, base_filename, used_seed)
211
- return video_path, latents_path, used_seed
212
- except Exception as e:
213
- logging.error(f"Error during unified generation: {e}", exc_info=True)
214
- return None, None, None
215
- finally:
216
- for path in temp_latent_paths:
217
- if path.exists(): path.unlink()
218
- self.finalize()
219
-
220
- # ==========================================================================
221
- # --- UNIDADES DE TRABALHO E HELPERS INTERNOS ---
222
- # ==========================================================================
223
-
224
- def _log_conditioning_items(self, items: List[LatentConditioningItem]):
225
- """
226
- Logs detailed information about a list of ConditioningItem objects.
227
- This is a dedicated debug helper function.
228
- """
229
- # Só imprime o log se o nível de logging for DEBUG
230
- if logging.getLogger().isEnabledFor(logging.INFO):
231
- log_str = ["\n" + "="*25 + " INFO: Conditioning Items " + "="*25]
232
- if not items:
233
- log_str.append(" -> Lista de conditioning_items está vazia.")
234
- else:
235
- for i, item in enumerate(items):
236
- if hasattr(item, 'media_item') and isinstance(item.media_item, torch.Tensor):
237
- t = item.media_item
238
- log_str.append(
239
- f" -> Item [{i}]: "
240
- f"Tensor(shape={list(t.shape)}, "
241
- f"device='{t.device}', "
242
- f"dtype={t.dtype}), "
243
- f"Target Frame = {item.media_frame_number}, "
244
- f"Strength = {item.conditioning_strength:.2f}"
245
- )
246
- else:
247
- log_str.append(f" -> Item [{i}]: Não contém um tensor válido.")
248
- log_str.append("="*75 + "\n")
249
 
250
- # Usa o logger de debug para imprimir a mensagem completa
251
- logging.info("\n".join(log_str))
252
-
253
- @log_function_io
254
- def _generate_single_chunk_low(self, **kwargs) -> Optional[torch.Tensor]:
255
- """[WORKER] Calls the pipeline to generate a single chunk of latents."""
256
- height_padded, width_padded = (self._align(d) for d in (kwargs['height'], kwargs['width']))
257
- downscale_factor = self.config.get("downscale_factor", 0.6666666)
258
- vae_scale_factor = self.pipeline.vae_scale_factor
259
- downscaled_height = self._align(int(height_padded * downscale_factor), vae_scale_factor)
260
- downscaled_width = self._align(int(width_padded * downscale_factor), vae_scale_factor)
261
-
262
- # 1. Começa com a configuração padrão
263
- first_pass_config = self.config.get("first_pass", {}).copy()
264
-
265
- # 2. Aplica os overrides da UI, se existirem
266
- if kwargs.get("ltx_configs_override"):
267
- self._apply_ui_overrides(first_pass_config, kwargs.get("ltx_configs_override"))
268
-
269
- # 3. Monta o dicionário de argumentos SEM conditioning_items primeiro
270
- pipeline_kwargs = {
271
- "prompt": kwargs['prompt'],
272
- "negative_prompt": kwargs['negative_prompt'],
273
- "height": downscaled_height,
274
- "width": downscaled_width,
275
- "num_frames": kwargs['num_frames'],
276
- "frame_rate": int(DEFAULT_FPS),
277
- "generator": torch.Generator(device=self.main_device).manual_seed(kwargs['seed']),
278
- "output_type": "latent",
279
- #"conditioning_items": conditioning_items if conditioning_items else None,
280
- "media_items": None,
281
- "decode_timestep": self.config["decode_timestep"],
282
- "decode_noise_scale": self.config["decode_noise_scale"],
283
- "stochastic_sampling": self.config["stochastic_sampling"],
284
- "image_cond_noise_scale": 0.01,
285
- "is_video": True,
286
- "vae_per_channel_normalize": True,
287
- "mixed_precision": (self.config["precision"] == "mixed_precision"),
288
- "offload_to_cpu": False,
289
- "enhance_prompt": False,
290
- #"skip_layer_strategy": SkipLayerStrategy.AttentionValues,
291
- **first_pass_config
292
- }
293
-
294
- # --- Bloco de Logging para Depuração ---
295
- # 4. Loga os argumentos do pipeline (sem os tensores de condição)
296
- logging.info(f"\n[Info] Pipeline Arguments (BASE):\n {json.dumps(pipeline_kwargs, indent=2, default=str)}\n")
297
-
298
- # Loga os conditioning_items separadamente com a nossa função helper
299
- conditioning_items_list = kwargs.get('conditioning_items')
300
- self._log_conditioning_items(conditioning_items_list)
301
- # --- Fim do Bloco de Logging ---
302
 
303
- # 5. Adiciona os conditioning_items ao dicionário
304
- pipeline_kwargs['conditioning_items'] = conditioning_items_list
305
-
306
- # 6. Executa o pipeline com o dicionário completo
307
- with torch.autocast(device_type=self.main_device.type, dtype=self.runtime_autocast_dtype, enabled="cuda" in self.main_device.type):
308
- latents_raw = self.pipeline(**pipeline_kwargs).images
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
- return latents_raw.to(self.main_device)
311
-
312
- @log_function_io
313
- def _finalize_generation(self, final_latents: torch.Tensor, base_filename: str, seed: int) -> Tuple[str, str]:
314
- """Consolidates latents, decodes them to video, and saves final artifacts."""
315
- logging.info("Finalizing generation: decoding latents to video.")
316
- final_latents_path = RESULTS_DIR / f"latents_{base_filename}_{seed}.pt"
317
- torch.save(final_latents, final_latents_path)
318
- logging.info(f"Final latents saved to: {final_latents_path}")
319
 
320
- pixel_tensor = vae_ltx_aduc_pipeline.decode_to_pixels(
321
- final_latents, decode_timestep=float(self.config.get("decode_timestep", 0.05))
322
- )
323
- video_path = self._save_and_log_video(pixel_tensor, f"{base_filename}_{seed}")
324
- return str(video_path), str(final_latents_path)
325
-
326
- def _apply_ui_overrides(self, config_dict: Dict, overrides: Dict):
327
- """Applies advanced settings from the UI to a config dictionary."""
328
- # Override step counts
329
- for key in ["num_inference_steps", "skip_initial_inference_steps", "skip_final_inference_steps"]:
330
- ui_value = overrides.get(key)
331
- if ui_value and ui_value > 0:
332
- config_dict[key] = ui_value
333
- logging.info(f"Override: '{key}' set to {ui_value} by UI.")
334
 
335
- def _save_and_log_video(self, pixel_tensor: torch.Tensor, base_filename: str) -> Path:
336
- with tempfile.TemporaryDirectory() as temp_dir:
337
- temp_path = os.path.join(temp_dir, f"{base_filename}.mp4")
338
- video_encode_tool_singleton.save_video_from_tensor(pixel_tensor, temp_path, fps=DEFAULT_FPS)
339
- final_path = RESULTS_DIR / f"{base_filename}.mp4"
340
- shutil.move(temp_path, final_path)
341
- logging.info(f"Video saved successfully to: {final_path}")
342
- return final_path
343
-
344
- def _apply_precision_policy(self):
345
- precision = str(self.config.get("precision", "bfloat16")).lower()
346
- if precision in ["float8_e4m3fn", "bfloat16"]: self.runtime_autocast_dtype = torch.bfloat16
347
- elif precision == "mixed_precision": self.runtime_autocast_dtype = torch.float16
348
- else: self.runtime_autocast_dtype = torch.float32
349
- logging.info(f"Runtime precision policy set for autocast: {self.runtime_autocast_dtype}")
350
-
351
- def _align(self, dim: int, alignment: int = FRAMES_ALIGNMENT, alignment_rule: str = 'default') -> int:
352
- """Aligns a dimension to the nearest multiple of `alignment`."""
353
- if alignment_rule == 'n*8+1':
354
- return ((dim - 1) // alignment) * alignment + 1
355
- return ((dim - 1) // alignment + 1) * alignment
356
-
357
- def _calculate_aligned_frames(self, duration_s: float, min_frames: int = 1) -> int:
358
- num_frames = int(round(duration_s * DEFAULT_FPS))
359
- # Para a duração total, sempre arredondamos para cima para o múltiplo de 8 mais próximo
360
- aligned_frames = self._align(num_frames, alignment=FRAMES_ALIGNMENT)
361
- return max(aligned_frames, min_frames)
362
-
363
- def _get_random_seed(self) -> int:
364
- """Always generates and returns a new random seed."""
365
- return random.randint(0, 2**32 - 1)
366
 
367
- # ==============================================================================
368
- # --- INSTANCIAÇÃO SINGLETON ---
369
- # ==============================================================================
370
  try:
371
  ltx_aduc_pipeline = LtxAducPipeline()
372
- logging.info("Global VideoService orchestrator instance created successfully.")
373
  except Exception as e:
374
- logging.critical(f"Failed to initialize VideoService: {e}", exc_info=True)
375
- sys.exit(1)
 
1
  # FILE: api/ltx/ltx_aduc_pipeline.py
2
+ # DESCRIPTION: A high-level client for submitting LTX video generation jobs to the pool manager.
3
+ # Its sole responsibility is to orchestrate the generation of a final LATENT tensor from prompts
4
+ # and initial conditions, without handling pixel decoding.
5
 
 
 
6
  import logging
 
 
 
 
7
  import time
 
 
 
8
  import torch
9
+ import random
10
+ import json
11
+ from typing import List, Optional, Tuple, Union, Dict
12
+
13
+ # O cliente importa o MANAGER para submeter trabalhos
14
+ from api.ltx.ltx_aduc_manager import ltx_aduc_manager
15
+
16
+ # O cliente precisa da definição de LatentConditioningItem para os seus inputs
17
+ from api.ltx.vae_aduc_pipeline import LatentConditioningItem
18
+
19
+ # Importa o tipo do pipeline para anotações claras nas funções de trabalho
20
+ from ltx_video.pipelines.pipeline_ltx_video import LTXVideoPipeline
21
 
22
  # ==============================================================================
23
+ # --- FUNÇÕES DE TRABALHO (Jobs a serem executados no Pool LTX) ---
24
  # ==============================================================================
25
 
26
+ def _job_generate_latent_chunk(
27
+ pipeline: LTXVideoPipeline,
28
+ prompt: str,
29
+ negative_prompt: str,
30
+ height: int,
31
+ width: int,
32
+ num_frames: int,
33
+ seed: int,
34
+ conditioning_items: Optional[List[LatentConditioningItem]],
35
+ ltx_configs: Dict
36
+ ) -> torch.Tensor:
37
+ """
38
+ Função de trabalho que executa a geração de um único chunk (cena) de vídeo latente.
39
+ Esta função é executada DENTRO de um LTXMainWorker.
40
+ """
41
+ generator = torch.Generator(device=pipeline.device).manual_seed(seed)
42
+
43
+ # Monta os argumentos para a chamada do pipeline
44
+ pipeline_kwargs = {
45
+ "prompt": prompt,
46
+ "negative_prompt": negative_prompt,
47
+ "height": height,
48
+ "width": width,
49
+ "num_frames": num_frames,
50
+ "frame_rate": 24, # Padrão, pode ser parametrizado se necessário
51
+ "generator": generator,
52
+ "output_type": "latent", # Ponto chave: sempre pedimos latentes
53
+ "conditioning_items": conditioning_items if conditioning_items else None,
54
+ **ltx_configs # Aplica configurações avançadas (guidance, steps, etc.)
55
+ }
56
+
57
+ logging.info(f"[LTX Job] Gerando chunk com {num_frames} frames para o prompt: '{prompt[:50]}...'")
58
+
59
+ # O pipeline já está na GPU correta dentro do worker
60
+ with torch.autocast(device_type=pipeline.device.type, dtype=torch.bfloat16):
61
+ latents_raw = pipeline(**pipeline_kwargs).images
62
 
63
+ # Retorna o tensor latente na CPU para liberar VRAM do worker para o próximo job
64
+ return latents_raw.cpu()
65
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  # ==============================================================================
68
+ # --- A CLASSE CLIENTE (Interface Pública para Geração de Vídeo Latente) ---
69
  # ==============================================================================
70
 
 
71
  class LtxAducPipeline:
72
  """
73
+ Cliente de alto nível para orquestrar a geração de vídeo latente.
74
+ Submete trabalhos de geração de chunks de vídeo ao LTXAducManager.
75
  """
 
 
76
  def __init__(self):
77
+ logging.info("✅ LTX ADUC Pipeline (Client) initialized and ready to submit jobs.")
78
+ # O __init__ é limpo, sem carregar modelos.
79
+ self.FRAMES_ALIGNMENT = 8
80
+ pass
 
 
 
81
 
82
+ def _get_random_seed(self) -> int:
83
+ """Sempre gera e retorna uma nova semente aleatória."""
84
+ return random.randint(0, 2**32 - 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ def _align(self, dim: int, alignment: int = 8) -> int:
87
+ """Alinha uma dimensão para o múltiplo mais próximo."""
88
+ return ((dim + alignment - 1) // alignment) * alignment
89
 
90
+ def __call__(
 
91
  self,
92
  prompt_list: List[str],
93
+ initial_conditioning_items: Optional[List[LatentConditioningItem]] = None,
94
+ height: int = 432,
95
+ width: int = 768,
96
+ duration_in_seconds: float = 4.0,
97
+ ltx_configs: Optional[Dict] = None
98
+ ) -> Tuple[Optional[torch.Tensor], Optional[int]]:
99
  """
100
+ Ponto de entrada principal para gerar um vídeo latente completo.
101
+
102
+ Args:
103
+ prompt_list: Lista de prompts, onde cada prompt é uma cena.
104
+ initial_conditioning_items: Lista de `LatentConditioningItem` para condicionar
105
+ a primeira cena.
106
+ height: Altura do vídeo.
107
+ width: Largura do vídeo.
108
+ duration_in_seconds: Duração total desejada do vídeo.
109
+ ltx_configs: Dicionário com configurações avançadas para o pipeline LTX
110
+ (guidance_scale, num_inference_steps, etc.).
111
+
112
+ Returns:
113
+ Uma tupla contendo:
114
+ - O tensor latente final completo (na CPU).
115
+ - A semente principal usada para a geração.
116
  """
117
+ t0 = time.time()
118
+ logging.info(f"LTX Client received a generation job with {len(prompt_list)} scenes.")
119
+
120
+ if not prompt_list:
121
+ raise ValueError("A lista de prompts não pode estar vazia.")
122
+
123
  used_seed = self._get_random_seed()
124
+ logging.info(f"Generation seed set to: {used_seed}")
 
125
 
126
+ # --- Lógica de Divisão de Chunks e Sobreposição ---
 
 
127
  num_chunks = len(prompt_list)
128
+ total_frames = self._align(int(duration_in_seconds * 24))
129
+ frames_per_chunk_base = total_frames // num_chunks
130
+ overlap_frames = self._align(9) if num_chunks > 1 else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ final_latents_list = []
133
+ overlap_condition_item: Optional[LatentConditioningItem] = None
 
 
 
 
 
 
134
 
135
+ for i, chunk_prompt in enumerate(prompt_list):
136
+ current_conditions = []
137
+ if i == 0 and initial_conditioning_items:
138
+ current_conditions.extend(initial_conditioning_items)
139
+ if overlap_condition_item:
140
+ current_conditions.append(overlap_condition_item)
 
 
 
 
 
 
 
141
 
142
+ # Calcula o número de frames para o chunk atual
143
+ num_frames_for_chunk = frames_per_chunk_base
144
+ if i == num_chunks - 1: # Último chunk pega o resto
145
+ processed_frames = sum(f.shape[2] for f in final_latents_list)
146
+ num_frames_for_chunk = total_frames - processed_frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ num_frames_for_chunk = self._align(num_frames_for_chunk)
149
+
150
+ # --- Submissão do Job para o Chunk Atual ---
151
+ chunk_latents = ltx_aduc_manager.submit_job(
152
+ job_type='ltx',
153
+ job_func=_job_generate_latent_chunk,
154
+ # Passa todos os argumentos necessários para a função de trabalho
155
+ prompt=chunk_prompt,
156
+ negative_prompt="blurry, low quality, bad anatomy, deformed", # Pode ser parametrizado
157
+ height=height,
158
+ width=width,
159
+ num_frames=num_frames_for_chunk,
160
+ seed=used_seed + i, # Semente diferente para cada chunk para variedade
161
+ conditioning_items=current_conditions,
162
+ ltx_configs=ltx_configs or {}
163
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ if chunk_latents is None:
166
+ logging.error(f"Failed to generate latents for scene {i+1}. Aborting generation.")
167
+ return None, used_seed
168
+
169
+ # --- Gerenciamento do "Eco Cinético" (Sobreposição) ---
170
+ if i < num_chunks - 1:
171
+ # Salva os últimos frames do chunk atual para condicionar o próximo
172
+ overlap_latents = chunk_latents[:, :, -overlap_frames:, :, :].clone()
173
+ overlap_condition_item = LatentConditioningItem(
174
+ latent_tensor=overlap_latents,
175
+ media_frame_number=0, # Sempre condiciona o início do próximo chunk
176
+ conditioning_strength=1.0 # Condicionamento forte
177
+ )
178
+ # Adiciona o chunk atual sem a sobreposição
179
+ final_latents_list.append(chunk_latents[:, :, :-overlap_frames, :, :])
180
+ else:
181
+ # Adiciona o último chunk completo
182
+ final_latents_list.append(chunk_latents)
183
 
184
+ # Concatena todos os chunks de latentes em um único tensor
185
+ final_latents = torch.cat(final_latents_list, dim=2)
 
 
 
 
 
 
 
186
 
187
+ logging.info(f"LTX Client job finished in {time.time() - t0:.2f}s. Final latent shape: {final_latents.shape}")
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ return final_latents, used_seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ # --- INSTÂNCIA SINGLETON DO CLIENTE ---
 
 
192
  try:
193
  ltx_aduc_pipeline = LtxAducPipeline()
 
194
  except Exception as e:
195
+ logging.critical("CRITICAL: Failed to initialize the LtxAducPipeline client.", exc_info=True)
196
+ ltx_aduc_pipeline = None```