euiiiia commited on
Commit
1497820
·
verified ·
1 Parent(s): c78a34d

Update api/ltx_server_refactored.py

Browse files
Files changed (1) hide show
  1. api/ltx_server_refactored.py +300 -454
api/ltx_server_refactored.py CHANGED
@@ -1,96 +1,101 @@
1
- # ltx_server_clean_refactor.py — VideoService (Modular Version with Simple Overlap Chunking)
2
 
3
- # ==============================================================================
4
- # 0. CONFIGURAÇÃO DE AMBIENTE E IMPORTAÇÕES
5
- # ==============================================================================
6
- import os
7
- import sys
8
- import gc
9
- import cv2
10
- import yaml
11
- import time
12
- import json
13
- import random
14
- import shutil
15
  import warnings
16
- import tempfile
17
- import traceback
18
- import subprocess
19
- from pathlib import Path
20
- from typing import List, Dict, Optional, Tuple, Union
21
-
22
- # --- Configurações de Logging e Avisos ---
23
  warnings.filterwarnings("ignore", category=UserWarning)
24
  warnings.filterwarnings("ignore", category=FutureWarning)
25
- from huggingface_hub import logging as hf_logging
26
- hf_logging.set_verbosity_error()
27
-
28
- # --- Importações de Bibliotecas de ML/Processamento ---
 
 
 
 
 
29
  import torch
30
- import torch.nn.functional as F
31
  import numpy as np
 
 
 
 
 
 
 
32
  from PIL import Image
33
- from einops import rearrange
34
  from huggingface_hub import hf_hub_download
35
- from safetensors import safe_open
36
-
 
 
 
 
 
 
 
37
  from managers.vae_manager import vae_manager_singleton
38
  from tools.video_encode_tool import video_encode_tool_singleton
39
-
40
-
41
- # --- Constantes Globais ---
42
- LTXV_DEBUG = True # Mude para False para desativar logs de debug
43
- LTXV_FRAME_LOG_EVERY = 8
44
  DEPS_DIR = Path("/data")
45
  LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
46
- RESULTS_DIR = Path("/app/output")
47
- DEFAULT_FPS = 24.0
48
-
49
- # ==============================================================================
50
- # 1. SETUP E FUNÇÕES AUXILIARES DE AMBIENTE
51
- # ==============================================================================
52
 
53
- def _run_setup_script():
54
- """Executa o script setup.py se o repositório LTX-Video não existir."""
 
55
  setup_script_path = "setup.py"
56
  if not os.path.exists(setup_script_path):
57
  print("[DEBUG] 'setup.py' não encontrado. Pulando clonagem de dependências.")
58
  return
59
-
60
- print(f"[DEBUG] Repositório não encontrado em {LTX_VIDEO_REPO_DIR}. Executando setup.py...")
61
  try:
62
- subprocess.run([sys.executable, setup_script_path], check=True, capture_output=True, text=True)
63
- print("[DEBUG] Script 'setup.py' concluído com sucesso.")
 
64
  except subprocess.CalledProcessError as e:
65
- print(f"[ERROR] Falha ao executar 'setup.py' (código {e.returncode}).\nOutput:\n{e.stdout}\n{e.stderr}")
66
  sys.exit(1)
67
-
68
- def add_deps_to_path(repo_path: Path):
69
- """Adiciona o diretório do repositório ao sys.path para importações locais."""
70
- resolved_path = str(repo_path.resolve())
71
- if resolved_path not in sys.path:
72
- sys.path.insert(0, resolved_path)
73
- if LTXV_DEBUG:
74
- print(f"[DEBUG] Adicionado ao sys.path: {resolved_path}")
75
-
76
- # --- Execução da configuração inicial ---
77
  if not LTX_VIDEO_REPO_DIR.exists():
78
- _run_setup_script()
79
- add_deps_to_path(LTX_VIDEO_REPO_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- # --- Importações Dependentes do Path Adicionado ---
 
 
82
  from ltx_video.models.autoencoders.vae_encode import un_normalize_latents, normalize_latents
83
  from ltx_video.pipelines.pipeline_ltx_video import adain_filter_latent
84
- from ltx_video.models.autoencoders.latent_upsampler import LatentUpsampler
85
- from ltx_video.pipelines.pipeline_ltx_video import ConditioningItem, LTXVideoPipeline
86
- from transformers import T5EncoderModel, T5Tokenizer, AutoModelForCausalLM, AutoProcessor, AutoTokenizer
87
- from ltx_video.models.autoencoders.causal_video_autoencoder import CausalVideoAutoencoder
88
- from ltx_video.models.transformers.symmetric_patchifier import SymmetricPatchifier
89
- from ltx_video.models.transformers.transformer3d import Transformer3DModel
90
- from ltx_video.schedulers.rf import RectifiedFlowScheduler
91
- from ltx_video.utils.skip_layer_strategy import SkipLayerStrategy
92
- import ltx_video.pipelines.crf_compressor as crf_compressor
93
-
94
 
95
 
96
  def load_image_to_tensor_with_resize_and_crop(
@@ -141,451 +146,292 @@ def load_image_to_tensor_with_resize_and_crop(
141
  # Create 5D tensor: (batch_size=1, channels=3, num_frames=1, height, width)
142
  return frame_tensor.unsqueeze(0).unsqueeze(2)
143
 
144
- def create_latent_upsampler(latent_upsampler_model_path: str, device: str):
145
- latent_upsampler = LatentUpsampler.from_pretrained(latent_upsampler_model_path)
146
- latent_upsampler.to(device)
147
- latent_upsampler.eval()
148
- return latent_upsampler
149
-
150
- def create_ltx_video_pipeline(
151
- ckpt_path: str,
152
- precision: str,
153
- text_encoder_model_name_or_path: str,
154
- sampler: Optional[str] = None,
155
- device: Optional[str] = None,
156
- enhance_prompt: bool = False,
157
- prompt_enhancer_image_caption_model_name_or_path: Optional[str] = None,
158
- prompt_enhancer_llm_model_name_or_path: Optional[str] = None,
159
- ) -> LTXVideoPipeline:
160
- ckpt_path = Path(ckpt_path)
161
- assert os.path.exists(
162
- ckpt_path
163
- ), f"Ckpt path provided (--ckpt_path) {ckpt_path} does not exist"
164
 
165
- with safe_open(ckpt_path, framework="pt") as f:
166
- metadata = f.metadata()
167
- config_str = metadata.get("config")
168
- configs = json.loads(config_str)
169
- allowed_inference_steps = configs.get("allowed_inference_steps", None)
170
-
171
- vae = CausalVideoAutoencoder.from_pretrained(ckpt_path)
172
- transformer = Transformer3DModel.from_pretrained(ckpt_path)
173
-
174
- # Use constructor if sampler is specified, otherwise use from_pretrained
175
- if sampler == "from_checkpoint" or not sampler:
176
- scheduler = RectifiedFlowScheduler.from_pretrained(ckpt_path)
177
- else:
178
- scheduler = RectifiedFlowScheduler(
179
- sampler=("Uniform" if sampler.lower() == "uniform" else "LinearQuadratic")
180
- )
181
-
182
- text_encoder = T5EncoderModel.from_pretrained(
183
- text_encoder_model_name_or_path, subfolder="text_encoder"
184
- )
185
- patchifier = SymmetricPatchifier(patch_size=1)
186
- tokenizer = T5Tokenizer.from_pretrained(
187
- text_encoder_model_name_or_path, subfolder="tokenizer"
188
- )
189
-
190
- transformer = transformer.to(device)
191
- vae = vae.to(device)
192
- text_encoder = text_encoder.to(device)
193
-
194
- if enhance_prompt:
195
- prompt_enhancer_image_caption_model = AutoModelForCausalLM.from_pretrained(
196
- prompt_enhancer_image_caption_model_name_or_path, trust_remote_code=True
197
- )
198
- prompt_enhancer_image_caption_processor = AutoProcessor.from_pretrained(
199
- prompt_enhancer_image_caption_model_name_or_path, trust_remote_code=True
200
- )
201
- prompt_enhancer_llm_model = AutoModelForCausalLM.from_pretrained(
202
- prompt_enhancer_llm_model_name_or_path,
203
- torch_dtype="bfloat16",
204
- )
205
- prompt_enhancer_llm_tokenizer = AutoTokenizer.from_pretrained(
206
- prompt_enhancer_llm_model_name_or_path,
207
- )
208
- else:
209
- prompt_enhancer_image_caption_model = None
210
- prompt_enhancer_image_caption_processor = None
211
- prompt_enhancer_llm_model = None
212
- prompt_enhancer_llm_tokenizer = None
213
-
214
- vae = vae.to(torch.bfloat16)
215
- if precision == "bfloat16" and transformer.dtype != torch.bfloat16:
216
- transformer = transformer.to(torch.bfloat16)
217
- text_encoder = text_encoder.to(torch.bfloat16)
218
-
219
- # Use submodels for the pipeline
220
- submodel_dict = {
221
- "transformer": transformer,
222
- "patchifier": patchifier,
223
- "text_encoder": text_encoder,
224
- "tokenizer": tokenizer,
225
- "scheduler": scheduler,
226
- "vae": vae,
227
- "prompt_enhancer_image_caption_model": prompt_enhancer_image_caption_model,
228
- "prompt_enhancer_image_caption_processor": prompt_enhancer_image_caption_processor,
229
- "prompt_enhancer_llm_model": prompt_enhancer_llm_model,
230
- "prompt_enhancer_llm_tokenizer": prompt_enhancer_llm_tokenizer,
231
- "allowed_inference_steps": allowed_inference_steps,
232
- }
233
-
234
- pipeline = LTXVideoPipeline(**submodel_dict)
235
- pipeline = pipeline.to(device)
236
- return pipeline
237
-
238
- # ==============================================================================
239
- # 2. FUNÇÕES AUXILIARES DE PROCESSAMENTO
240
- # ==============================================================================
241
-
242
- def calculate_padding(orig_h: int, orig_w: int, target_h: int, target_w: int) -> Tuple[int, int, int, int]:
243
- """Calcula o preenchimento para centralizar uma imagem em uma nova dimensão."""
244
- pad_h = target_h - orig_h
245
- pad_w = target_w - orig_w
246
- pad_top = pad_h // 2
247
- pad_bottom = pad_h - pad_top
248
- pad_left = pad_w // 2
249
- pad_right = pad_w - pad_left
250
- return (pad_left, pad_right, pad_top, pad_bottom)
251
-
252
- def log_tensor_info(tensor: torch.Tensor, name: str = "Tensor"):
253
- """Exibe informações detalhadas sobre um tensor para depuração."""
254
- if not isinstance(tensor, torch.Tensor):
255
- print(f"\n[INFO] '{name}' não é um tensor.")
256
- return
257
- print(f"\n--- Tensor Info: {name} ---")
258
- print(f" - Shape: {tuple(tensor.shape)}")
259
- print(f" - Dtype: {tensor.dtype}")
260
- print(f" - Device: {tensor.device}")
261
- if tensor.numel() > 0:
262
- try:
263
- print(f" - Stats: Min={tensor.min().item():.4f}, Max={tensor.max().item():.4f}, Mean={tensor.mean().item():.4f}")
264
- except RuntimeError:
265
- print(" - Stats: Não foi possível calcular (ex: tensores bool).")
266
- print("-" * 30)
267
-
268
- # ==============================================================================
269
- # 3. CLASSE PRINCIPAL DO SERVIÇO DE VÍDEO
270
- # ==============================================================================
271
 
272
  class VideoService:
273
- """
274
- Serviço encapsulado para gerar vídeos usando a pipeline LTX-Video.
275
- Gerencia o carregamento de modelos, pré-processamento, geração em múltiplos
276
- passos (baixa resolução, upscale com denoise) e pós-processamento.
277
- """
278
  def __init__(self):
279
- """Inicializa o serviço, carregando configurações e modelos."""
280
  t0 = time.perf_counter()
281
- print("[INFO] Inicializando VideoService...")
282
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
283
- self.config = self._load_config("ltxv-13b-0.9.8-dev-fp8.yaml")
284
-
285
- self.pipeline, self.latent_upsampler = self._load_models_from_hub()
286
- self._move_models_to_device()
287
-
288
- self.runtime_autocast_dtype = self._get_precision_dtype()
289
  vae_manager_singleton.attach_pipeline(
290
  self.pipeline,
291
  device=self.device,
292
  autocast_dtype=self.runtime_autocast_dtype
293
  )
294
  self._tmp_dirs = set()
295
- RESULTS_DIR.mkdir(exist_ok=True)
296
- print(f"[INFO] VideoService pronto. Tempo de inicialização: {time.perf_counter()-t0:.2f}s")
297
 
298
- # --------------------------------------------------------------------------
299
- # --- Métodos Públicos (API do Serviço) ---
300
- # --------------------------------------------------------------------------
301
-
302
- def _prepare_condition_items(self, items_list: List[Tuple], height: int, width: int, num_frames: int) -> List[ConditioningItem]:
303
- """Prepara os tensores de condicionamento a partir de imagens ou tensores."""
304
- if not items_list:
305
- return []
306
-
307
- height, width = self._calculate_downscaled_dims(height, width)
308
-
309
- height_padded = ((height - 1) // 8 + 1) * 8
310
- width_padded = ((width - 1) // 8 + 1) * 8
311
- padding_values = calculate_padding(height, width, height_padded, width_padded)
312
-
313
- conditioning_items = []
314
- for media, frame_idx, weight in items_list:
315
- if isinstance(media, str):
316
- tensor = self._prepare_conditioning_tensor_from_path(media, height, width, padding_values)
317
- else: # Assume que é um tensor
318
- tensor = media.to(self.device, dtype=self.runtime_autocast_dtype)
319
-
320
- # Garante que o frame de condicionamento esteja dentro dos limites do vídeo
321
- safe_frame_idx = max(0, min(int(frame_idx), num_frames - 1))
322
- conditioning_items.append(ConditioningItem(tensor, safe_frame_idx, float(weight)))
323
-
324
- return conditioning_items
325
-
326
- def generate_low_resolution(
327
- self, prompt: str, negative_prompt: str,
328
- height: int, width: int, duration_secs: float,
329
- guidance_scale: float, seed: Optional[int] = None,
330
- conditioning_items: Optional[List[ConditioningItem]] = None
331
- ) -> Tuple[str, str, int]:
332
- """
333
- Gera um vídeo de baixa resolução e retorna os caminhos para o vídeo e os latentes.
334
- """
335
- used_seed = random.randint(0, 2**32 - 1) if seed is None else int(seed)
336
- self._seed_everething(used_seed)
337
-
338
- actual_num_frames = int(duration_secs * DEFAULT_FPS)
339
-
340
- downscaled_height, downscaled_width = self._calculate_downscaled_dims(height, width)
341
-
342
- first_pass_kwargs = {
343
- "prompt": prompt,
344
- "negative_prompt": negative_prompt,
345
- "height": downscaled_height,
346
- "width": downscaled_width,
347
- "num_frames": max(3, (actual_num_frames//8)*8)+1,
348
- "frame_rate": int(DEFAULT_FPS),
349
- "generator": torch.Generator(device=self.device).manual_seed(used_seed),
350
- "output_type": "latent",
351
- "is_video": True,
352
- "vae_per_channel_normalize": True,
353
- "conditioning_items": conditioning_items,
354
- "guidance_scale": float(guidance_scale),
355
- **(self.config.get("first_pass", {}))
356
- }
357
 
358
- temp_dir = tempfile.mkdtemp(prefix="ltxv_low_")
359
- self._register_tmp_dir(temp_dir)
360
-
 
361
  try:
362
- with torch.autocast(device_type=self.device.split(':')[0], dtype=self.runtime_autocast_dtype, enabled=(self.device == 'cuda')):
363
- latents = self.pipeline(**first_pass_kwargs).images
364
- pixel_tensor = vae_manager_singleton.decode(latents.clone(), decode_timestep=float(self.config.get("decode_timestep", 0.05)))
365
- video_path = self._save_video_from_tensor(pixel_tensor, "low_res_video", used_seed, temp_dir)
366
- latents_path = self._save_latents_to_disk(latents, "latents_low_res", used_seed)
367
-
368
-
369
- return video_path, latents_path, used_seed
370
-
371
  except Exception as e:
372
- print(f"[ERROR] Falha na geração de baixa resolução: {e}")
373
- traceback.print_exc()
374
- raise
375
- finally:
376
- self._finalize()
377
-
378
-
379
- def encode_latents_to_mp4(self, latents_path: str, fps: int = int(DEFAULT_FPS)) -> str:
380
- """Decodifica um tensor de latentes salvo e o salva como um vídeo MP4."""
381
- latents = torch.load(latents_path)
382
- temp_dir = tempfile.mkdtemp(prefix="ltxv_enc_")
383
- self._register_tmp_dir(temp_dir)
384
- seed = random.randint(0, 99999) # Seed apenas para nome do arquivo
385
-
386
  try:
387
- chunks = self._split_latents_with_overlap(latents)
388
- pixel_chunks = []
389
-
390
- with torch.autocast(device_type=self.device.split(':')[0], dtype=self.runtime_autocast_dtype, enabled=(self.device == 'cuda')):
391
- for chunk in chunks:
392
- if chunk.shape[2] == 0: continue
393
- pixel_chunk = vae_manager_singleton.decode(chunk.to(self.device), decode_timestep=float(self.config.get("decode_timestep", 0.05)))
394
- pixel_chunks.append(pixel_chunk)
395
-
396
- final_pixel_tensor = self._merge_chunks_with_overlap(pixel_chunks)
397
- final_video_path = self._save_video_from_tensor(final_pixel_tensor, f"final_video_{seed}", seed, temp_dir, fps=fps)
398
- return final_video_path
399
-
400
  except Exception as e:
401
- print(f"[ERROR] Falha ao encodar latentes para MP4: {e}")
402
- traceback.print_exc()
403
- raise
404
- finally:
405
- self._finalize()
406
 
407
- # --------------------------------------------------------------------------
408
- # --- Métodos Internos e Auxiliares ---
409
- # --------------------------------------------------------------------------
410
-
411
- def _finalize(self):
412
- """Limpa a memória da GPU e os diretórios temporários."""
413
- if LTXV_DEBUG:
414
- print("[DEBUG] Finalize: iniciando limpeza...")
415
-
416
- gc.collect()
417
- if torch.cuda.is_available():
418
- torch.cuda.empty_cache()
419
- torch.cuda.ipc_collect()
420
-
421
- # Limpa todos os diretórios temporários registrados
422
- for d in list(self._tmp_dirs):
423
- shutil.rmtree(d, ignore_errors=True)
424
- self._tmp_dirs.remove(d)
425
- if LTXV_DEBUG:
426
- print(f"[DEBUG] Diretório temporário removido: {d}")
427
-
428
- def _load_config(self, config_filename: str) -> Dict:
429
- """Carrega o arquivo de configuração YAML."""
430
- config_path = LTX_VIDEO_REPO_DIR / "configs" / config_filename
431
- print(f"[INFO] Carregando configuração de: {config_path}")
432
- with open(config_path, "r") as file:
433
- return yaml.safe_load(file)
434
-
435
- def _load_models_from_hub(self):
436
- """Baixa e cria as instâncias da pipeline e do upsampler."""
437
  t0 = time.perf_counter()
438
  LTX_REPO = "Lightricks/LTX-Video"
439
-
440
- print("[INFO] Baixando checkpoint principal...")
441
- self.config["checkpoint_path"] = hf_hub_download(
442
- repo_id=LTX_REPO, filename=self.config["checkpoint_path"],
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  token=os.getenv("HF_TOKEN")
444
  )
445
- print(f"[INFO] Checkpoint principal em: {self.config['checkpoint_path']}")
 
446
 
447
- print("[INFO] Construindo pipeline...")
448
  pipeline = create_ltx_video_pipeline(
449
  ckpt_path=self.config["checkpoint_path"],
450
  precision=self.config["precision"],
451
  text_encoder_model_name_or_path=self.config["text_encoder_model_name_or_path"],
452
  sampler=self.config["sampler"],
453
- device="cpu", # Carrega em CPU primeiro
454
- enhance_prompt=False
 
 
455
  )
456
- print("[INFO] Pipeline construída.")
457
 
458
  latent_upsampler = None
459
  if self.config.get("spatial_upscaler_model_path"):
460
- print("[INFO] Baixando upscaler espacial...")
461
- self.config["spatial_upscaler_model_path"] = hf_hub_download(
462
- repo_id=LTX_REPO, filename=self.config["spatial_upscaler_model_path"],
463
- token=os.getenv("HF_TOKEN")
464
- )
465
- print(f"[INFO] Upscaler em: {self.config['spatial_upscaler_model_path']}")
466
-
467
- print("[INFO] Construindo latent_upsampler...")
468
  latent_upsampler = create_latent_upsampler(self.config["spatial_upscaler_model_path"], device="cpu")
469
- print("[INFO] Latent upsampler construído.")
470
-
471
- print(f"[INFO] Carregamento de modelos concluído em {time.perf_counter()-t0:.2f}s")
472
  return pipeline, latent_upsampler
473
-
474
- def _move_models_to_device(self):
475
- """Move os modelos carregados para o dispositivo de computação (GPU/CPU)."""
476
- print(f"[INFO] Movendo modelos para o dispositivo: {self.device}")
477
- self.pipeline.to(self.device)
478
- if self.latent_upsampler:
479
- self.latent_upsampler.to(self.device)
480
 
481
- def _get_precision_dtype(self) -> torch.dtype:
482
- """Determina o dtype para autocast com base na configuração de precisão."""
483
  prec = str(self.config.get("precision", "")).lower()
 
484
  if prec in ["float8_e4m3fn", "bfloat16"]:
485
- return torch.bfloat16
486
  elif prec == "mixed_precision":
487
- return torch.float16
488
- return torch.float32
 
 
 
489
 
490
  @torch.no_grad()
491
- def _upsample_and_filter_latents(self, latents: torch.Tensor) -> torch.Tensor:
492
- """Aplica o upsample espacial e o filtro AdaIN aos latentes."""
493
- if not self.latent_upsampler:
494
- raise ValueError("Latent Upsampler não está carregado para a operação de upscale.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
- latents_unnormalized = un_normalize_latents(latents, self.pipeline.vae, vae_per_channel_normalize=True)
497
- upsampled_latents_unnormalized = self.latent_upsampler(latents_unnormalized)
498
- upsampled_latents_normalized = normalize_latents(upsampled_latents_unnormalized, self.pipeline.vae, vae_per_channel_normalize=True)
 
 
 
 
 
 
 
 
 
499
 
500
- # Filtro AdaIN para manter consistência de cor/estilo com o vídeo de baixa resolução
501
- return adain_filter_latent(latents=upsampled_latents_normalized, reference_latents=latents)
502
-
503
  def _prepare_conditioning_tensor_from_path(self, filepath: str, height: int, width: int, padding: Tuple) -> torch.Tensor:
504
  """Carrega uma imagem, redimensiona, aplica padding e move para o dispositivo."""
505
  tensor = load_image_to_tensor_with_resize_and_crop(filepath, height, width)
506
  tensor = F.pad(tensor, padding)
507
  return tensor.to(self.device, dtype=self.runtime_autocast_dtype)
 
508
 
509
- def _calculate_downscaled_dims(self, height: int, width: int) -> Tuple[int, int]:
510
- """Calcula as dimensões para o primeiro passo (baixa resolução)."""
 
 
 
511
  height_padded = ((height - 1) // 8 + 1) * 8
512
  width_padded = ((width - 1) // 8 + 1) * 8
513
-
 
514
  downscale_factor = self.config.get("downscale_factor", 0.6666666)
515
  vae_scale_factor = self.pipeline.vae_scale_factor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- target_w = int(width_padded * downscale_factor)
518
- downscaled_width = target_w - (target_w % vae_scale_factor)
519
-
520
- target_h = int(height_padded * downscale_factor)
521
- downscaled_height = target_h - (target_h % vae_scale_factor)
522
-
523
- return downscaled_height, downscaled_width
524
-
525
- def _split_latents_with_overlap(self, latents: torch.Tensor, overlap: int = 1) -> List[torch.Tensor]:
526
- """Divide um tensor de latentes em dois chunks com sobreposição."""
527
- total_frames = latents.shape[2]
528
- if total_frames <= overlap:
529
- return [latents]
530
 
531
- mid_point = max(overlap, total_frames // 2)
532
- chunk1 = latents[:, :, :mid_point, :, :]
533
- # O segundo chunk começa 'overlap' frames antes para criar a sobreposição
534
- chunk2 = latents[:, :, mid_point - overlap:, :, :]
535
-
536
- return [c for c in [chunk1, chunk2] if c.shape[2] > 0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
- def _merge_chunks_with_overlap(self, chunks: List[torch.Tensor], overlap: int = 1) -> torch.Tensor:
539
- """Junta uma lista de chunks, removendo a sobreposição."""
540
- if not chunks:
541
- return torch.empty(0)
542
- if len(chunks) == 1:
543
- return chunks[0]
544
 
545
- # Pega o primeiro chunk sem o frame de sobreposição final
546
- merged_list = [chunks[0][:, :, :-overlap, :, :]]
547
- # Adiciona os chunks restantes
548
- merged_list.extend(chunks[1:])
 
 
549
 
550
- return torch.cat(merged_list, dim=2)
 
 
 
 
551
 
552
- def _save_latents_to_disk(self, latents_tensor: torch.Tensor, base_filename: str, seed: int) -> str:
553
- """Salva um tensor de latentes em um arquivo .pt."""
554
- latents_cpu = latents_tensor.detach().to("cpu")
555
- tensor_path = RESULTS_DIR / f"{base_filename}_{seed}.pt"
556
- torch.save(latents_cpu, tensor_path)
557
- if LTXV_DEBUG:
558
- print(f"[DEBUG] Latentes salvos em: {tensor_path}")
559
- return str(tensor_path)
560
-
561
- def _save_video_from_tensor(self, pixel_tensor: torch.Tensor, base_filename: str, seed: int, temp_dir: str, fps: int = int(DEFAULT_FPS)) -> str:
562
- """Salva um tensor de pixels como um arquivo de vídeo MP4."""
563
- temp_path = os.path.join(temp_dir, f"{base_filename}_{seed}.mp4")
564
- video_encode_tool_singleton.save_video_from_tensor(pixel_tensor, temp_path, fps=fps)
565
 
566
- final_path = RESULTS_DIR / f"{base_filename}_{seed}.mp4"
567
- shutil.move(temp_path, final_path)
568
- print(f"[INFO] Vídeo final salvo em: {final_path}")
569
- return str(final_path)
570
 
571
- def _register_tmp_dir(self, dir_path: str):
572
- """Registra um diretório temporário para limpeza posterior."""
573
- if dir_path and os.path.isdir(dir_path):
574
- self._tmp_dirs.add(dir_path)
575
- if LTXV_DEBUG:
576
- print(f"[DEBUG] Diretório temporário registrado: {dir_path}")
577
-
578
- def _seed_everething(self, seed: int):
579
- random.seed(seed)
580
- np.random.seed(seed)
581
- torch.manual_seed(seed)
582
- if torch.cuda.is_available():
583
- torch.cuda.manual_seed(seed)
584
- if torch.backends.mps.is_available():
585
- torch.mps.manual_seed(seed)
586
 
587
- # ==============================================================================
588
- # 4. INSTANCIAÇÃO E PONTO DE ENTRADA (Exemplo)
589
- # ==============================================================================
590
  print("Criando instância do VideoService. O carregamento do modelo começará agora...")
591
- video_generation_service = VideoService()
 
 
1
+ # ltx_server_refactored.py — VideoService (Modular Version with Simple Overlap Chunking)
2
 
3
+ # --- 0. WARNINGS E AMBIENTE ---
 
 
 
 
 
 
 
 
 
 
 
4
  import warnings
 
 
 
 
 
 
 
5
  warnings.filterwarnings("ignore", category=UserWarning)
6
  warnings.filterwarnings("ignore", category=FutureWarning)
7
+ warnings.filterwarnings("ignore", message=".*")
8
+ from huggingface_hub import logging
9
+ logging.set_verbosity_error()
10
+ logging.set_verbosity_warning()
11
+ logging.set_verbosity_info()
12
+ logging.set_verbosity_debug()
13
+ LTXV_DEBUG=1
14
+ LTXV_FRAME_LOG_EVERY=8
15
+ import os, subprocess, shlex, tempfile
16
  import torch
17
+ import json
18
  import numpy as np
19
+ import random
20
+ import os
21
+ import shlex
22
+ import yaml
23
+ from typing import List, Dict
24
+ from pathlib import Path
25
+ import imageio
26
  from PIL import Image
27
+ import tempfile
28
  from huggingface_hub import hf_hub_download
29
+ import sys
30
+ import subprocess
31
+ import gc
32
+ import shutil
33
+ import contextlib
34
+ import time
35
+ import traceback
36
+ from einops import rearrange
37
+ import torch.nn.functional as F
38
  from managers.vae_manager import vae_manager_singleton
39
  from tools.video_encode_tool import video_encode_tool_singleton
 
 
 
 
 
40
  DEPS_DIR = Path("/data")
41
  LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
 
 
 
 
 
 
42
 
43
+ # (Todas as funções de setup, helpers e inicialização da classe permanecem inalteradas)
44
+ # ... (run_setup, add_deps_to_path, _query_gpu_processes_via_nvml, etc.)
45
+ def run_setup():
46
  setup_script_path = "setup.py"
47
  if not os.path.exists(setup_script_path):
48
  print("[DEBUG] 'setup.py' não encontrado. Pulando clonagem de dependências.")
49
  return
 
 
50
  try:
51
+ print("[DEBUG] Executando setup.py para dependências...")
52
+ subprocess.run([sys.executable, setup_script_path], check=True)
53
+ print("[DEBUG] Setup concluído com sucesso.")
54
  except subprocess.CalledProcessError as e:
55
+ print(f"[DEBUG] ERRO no setup.py (code {e.returncode}). Abortando.")
56
  sys.exit(1)
 
 
 
 
 
 
 
 
 
 
57
  if not LTX_VIDEO_REPO_DIR.exists():
58
+ print(f"[DEBUG] Repositório não encontrado em {LTX_VIDEO_REPO_DIR}. Rodando setup...")
59
+ run_setup()
60
+ def add_deps_to_path():
61
+ repo_path = str(LTX_VIDEO_REPO_DIR.resolve())
62
+ if str(LTX_VIDEO_REPO_DIR.resolve()) not in sys.path:
63
+ sys.path.insert(0, repo_path)
64
+ print(f"[DEBUG] Repo adicionado ao sys.path: {repo_path}")
65
+ def calculate_padding(orig_h, orig_w, target_h, target_w):
66
+ pad_h = target_h - orig_h
67
+ pad_w = target_w - orig_w
68
+ pad_top = pad_h // 2
69
+ pad_bottom = pad_h - pad_top
70
+ pad_left = pad_w // 2
71
+ pad_right = pad_w - pad_left
72
+ return (pad_left, pad_right, pad_top, pad_bottom)
73
+ def log_tensor_info(tensor, name="Tensor"):
74
+ if not isinstance(tensor, torch.Tensor):
75
+ print(f"\n[INFO] '{name}' não é tensor.")
76
+ return
77
+ print(f"\n--- Tensor: {name} ---")
78
+ print(f" - Shape: {tuple(tensor.shape)}")
79
+ print(f" - Dtype: {tensor.dtype}")
80
+ print(f" - Device: {tensor.device}")
81
+ if tensor.numel() > 0:
82
+ try:
83
+ print(f" - Min: {tensor.min().item():.4f} Max: {tensor.max().item():.4f} Mean: {tensor.mean().item():.4f}")
84
+ except Exception:
85
+ pass
86
+ print("------------------------------------------\n")
87
 
88
+ add_deps_to_path()
89
+ from ltx_video.pipelines.pipeline_ltx_video import ConditioningItem, LTXMultiScalePipeline
90
+ from ltx_video.utils.skip_layer_strategy import SkipLayerStrategy
91
  from ltx_video.models.autoencoders.vae_encode import un_normalize_latents, normalize_latents
92
  from ltx_video.pipelines.pipeline_ltx_video import adain_filter_latent
93
+ from api.ltx.inference import (
94
+ create_ltx_video_pipeline,
95
+ create_latent_upsampler,
96
+ load_image_to_tensor_with_resize_and_crop,
97
+ seed_everething,
98
+ )
 
 
 
 
99
 
100
 
101
  def load_image_to_tensor_with_resize_and_crop(
 
146
  # Create 5D tensor: (batch_size=1, channels=3, num_frames=1, height, width)
147
  return frame_tensor.unsqueeze(0).unsqueeze(2)
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  class VideoService:
 
 
 
 
 
152
  def __init__(self):
 
153
  t0 = time.perf_counter()
154
+ print("[DEBUG] Inicializando VideoService...")
155
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
156
+ self.config = self._load_config()
157
+ self.pipeline, self.latent_upsampler = self._load_models()
158
+ self.pipeline.to(self.device)
159
+ if self.latent_upsampler:
160
+ self.latent_upsampler.to(self.device)
161
+ self._apply_precision_policy()
162
  vae_manager_singleton.attach_pipeline(
163
  self.pipeline,
164
  device=self.device,
165
  autocast_dtype=self.runtime_autocast_dtype
166
  )
167
  self._tmp_dirs = set()
168
+ print(f"[DEBUG] VideoService pronto. boot_time={time.perf_counter()-t0:.3f}s")
 
169
 
170
+ def _load_config(self):
171
+ base = LTX_VIDEO_REPO_DIR / "configs"
172
+ config_path = base / "ltxv-13b-0.9.8-distilled-fp8.yaml"
173
+ with open(config_path, "r") as file:
174
+ return yaml.safe_load(file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ def finalize(self, keep_paths=None, extra_paths=None, clear_gpu=True):
177
+ print("[DEBUG] Finalize: iniciando limpeza...")
178
+ keep = set(keep_paths or []); extras = set(extra_paths or [])
179
+ gc.collect()
180
  try:
181
+ if clear_gpu and torch.cuda.is_available():
182
+ torch.cuda.empty_cache()
183
+ try:
184
+ torch.cuda.ipc_collect()
185
+ except Exception:
186
+ pass
 
 
 
187
  except Exception as e:
188
+ print(f"[DEBUG] Finalize: limpeza GPU falhou: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  try:
190
+ self._log_gpu_memory("Após finalize")
 
 
 
 
 
 
 
 
 
 
 
 
191
  except Exception as e:
192
+ print(f"[DEBUG] Log GPU pós-finalize falhou: {e}")
 
 
 
 
193
 
194
+ def _load_models(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  t0 = time.perf_counter()
196
  LTX_REPO = "Lightricks/LTX-Video"
197
+ print("[DEBUG] Baixando checkpoint principal...")
198
+ distilled_model_path = hf_hub_download(
199
+ repo_id=LTX_REPO,
200
+ filename=self.config["checkpoint_path"],
201
+ local_dir=os.getenv("HF_HOME"),
202
+ cache_dir=os.getenv("HF_HOME_CACHE"),
203
+ token=os.getenv("HF_TOKEN"),
204
+ )
205
+ self.config["checkpoint_path"] = distilled_model_path
206
+ print(f"[DEBUG] Checkpoint em: {distilled_model_path}")
207
+
208
+ print("[DEBUG] Baixando upscaler espacial...")
209
+ spatial_upscaler_path = hf_hub_download(
210
+ repo_id=LTX_REPO,
211
+ filename=self.config["spatial_upscaler_model_path"],
212
+ local_dir=os.getenv("HF_HOME"),
213
+ cache_dir=os.getenv("HF_HOME_CACHE"),
214
  token=os.getenv("HF_TOKEN")
215
  )
216
+ self.config["spatial_upscaler_model_path"] = spatial_upscaler_path
217
+ print(f"[DEBUG] Upscaler em: {spatial_upscaler_path}")
218
 
219
+ print("[DEBUG] Construindo pipeline...")
220
  pipeline = create_ltx_video_pipeline(
221
  ckpt_path=self.config["checkpoint_path"],
222
  precision=self.config["precision"],
223
  text_encoder_model_name_or_path=self.config["text_encoder_model_name_or_path"],
224
  sampler=self.config["sampler"],
225
+ device="cpu",
226
+ enhance_prompt=False,
227
+ prompt_enhancer_image_caption_model_name_or_path=self.config["prompt_enhancer_image_caption_model_name_or_path"],
228
+ prompt_enhancer_llm_model_name_or_path=self.config["prompt_enhancer_llm_model_name_or_path"],
229
  )
230
+ print("[DEBUG] Pipeline pronto.")
231
 
232
  latent_upsampler = None
233
  if self.config.get("spatial_upscaler_model_path"):
234
+ print("[DEBUG] Construindo latent_upsampler...")
 
 
 
 
 
 
 
235
  latent_upsampler = create_latent_upsampler(self.config["spatial_upscaler_model_path"], device="cpu")
236
+ print("[DEBUG] Upsampler pronto.")
237
+ print(f"[DEBUG] _load_models() tempo total={time.perf_counter()-t0:.3f}s")
 
238
  return pipeline, latent_upsampler
 
 
 
 
 
 
 
239
 
240
+ def _apply_precision_policy(self):
 
241
  prec = str(self.config.get("precision", "")).lower()
242
+ self.runtime_autocast_dtype = torch.float32
243
  if prec in ["float8_e4m3fn", "bfloat16"]:
244
+ self.runtime_autocast_dtype = torch.bfloat16
245
  elif prec == "mixed_precision":
246
+ self.runtime_autocast_dtype = torch.float16
247
+
248
+ def _register_tmp_dir(self, d: str):
249
+ if d and os.path.isdir(d):
250
+ self._tmp_dirs.add(d); print(f"[DEBUG] Registrado tmp dir: {d}")
251
 
252
  @torch.no_grad()
253
+ def _upsample_latents_internal(self, latents: torch.Tensor) -> torch.Tensor:
254
+ try:
255
+ if not self.latent_upsampler:
256
+ raise ValueError("Latent Upsampler não está carregado.")
257
+ latents_unnormalized = un_normalize_latents(latents, self.pipeline.vae, vae_per_channel_normalize=True)
258
+ upsampled_latents = self.latent_upsampler(latents_unnormalized)
259
+ return normalize_latents(upsampled_latents, self.pipeline.vae, vae_per_channel_normalize=True)
260
+ except Exception as e:
261
+ pass
262
+ finally:
263
+ torch.cuda.empty_cache()
264
+ torch.cuda.ipc_collect()
265
+ self.finalize(keep_paths=[])
266
+
267
+ def _prepare_conditioning_tensor(self, filepath, height, width, padding_values):
268
+ tensor = load_image_to_tensor_with_resize_and_crop(filepath, height, width)
269
+ tensor = torch.nn.functional.pad(tensor, padding_values)
270
+ return tensor.to(self.device, dtype=self.runtime_autocast_dtype)
271
+
272
+
273
+ def _save_and_log_video(self, pixel_tensor, base_filename, fps, temp_dir, results_dir, used_seed, progress_callback=None):
274
+ output_path = os.path.join(temp_dir, f"{base_filename}_{used_seed}.mp4")
275
+ video_encode_tool_singleton.save_video_from_tensor(
276
+ pixel_tensor, output_path, fps=fps, progress_callback=progress_callback
277
+ )
278
+ final_path = os.path.join(results_dir, f"{base_filename}_{used_seed}.mp4")
279
+ shutil.move(output_path, final_path)
280
+ print(f"[DEBUG] Vídeo salvo em: {final_path}")
281
+ return final_path
282
+
283
+ # ==============================================================================
284
+ # --- FUNÇÕES MODULARES COM A LÓGICA DE CHUNKING SIMPLIFICADA ---
285
+ # ==============================================================================
286
+
287
+ def _prepare_condition_items(self, items_list: List[Tuple], height: int, width: int, num_frames: int) -> List[ConditioningItem]:
288
+ """Prepara os tensores de condicionamento a partir de imagens ou tensores."""
289
+ if not items_list:
290
+ return []
291
+
292
+ height, width = self._calculate_downscaled_dims(height, width)
293
+
294
+ height_padded = ((height - 1) // 8 + 1) * 8
295
+ width_padded = ((width - 1) // 8 + 1) * 8
296
+ padding_values = calculate_padding(height, width, height_padded, width_padded)
297
 
298
+ conditioning_items = []
299
+ for media, frame_idx, weight in items_list:
300
+ if isinstance(media, str):
301
+ tensor = self._prepare_conditioning_tensor_from_path(media, height, width, padding_values)
302
+ else: # Assume que é um tensor
303
+ tensor = media.to(self.device, dtype=self.runtime_autocast_dtype)
304
+
305
+ # Garante que o frame de condicionamento esteja dentro dos limites do vídeo
306
+ safe_frame_idx = max(0, min(int(frame_idx), num_frames - 1))
307
+ conditioning_items.append(ConditioningItem(tensor, safe_frame_idx, float(weight)))
308
+
309
+ return conditioning_items
310
 
 
 
 
311
  def _prepare_conditioning_tensor_from_path(self, filepath: str, height: int, width: int, padding: Tuple) -> torch.Tensor:
312
  """Carrega uma imagem, redimensiona, aplica padding e move para o dispositivo."""
313
  tensor = load_image_to_tensor_with_resize_and_crop(filepath, height, width)
314
  tensor = F.pad(tensor, padding)
315
  return tensor.to(self.device, dtype=self.runtime_autocast_dtype)
316
+
317
 
318
+ def generate_low(self, prompt, negative_prompt, height, width, duration, guidance_scale, seed, conditioning_items=None):
319
+ used_seed = random.randint(0, 2**32 - 1) if seed is None else int(seed)
320
+ seed_everething(used_seed)
321
+ FPS = 24.0
322
+ actual_num_frames = max(9, int(round((round(duration * FPS) - 1) / 8.0) * 8 + 1))
323
  height_padded = ((height - 1) // 8 + 1) * 8
324
  width_padded = ((width - 1) // 8 + 1) * 8
325
+ temp_dir = tempfile.mkdtemp(prefix="ltxv_low_"); self._register_tmp_dir(temp_dir)
326
+ results_dir = "/app/output"; os.makedirs(results_dir, exist_ok=True)
327
  downscale_factor = self.config.get("downscale_factor", 0.6666666)
328
  vae_scale_factor = self.pipeline.vae_scale_factor
329
+ x_width = int(width_padded * downscale_factor)
330
+ downscaled_width = x_width - (x_width % vae_scale_factor)
331
+ x_height = int(height_padded * downscale_factor)
332
+ downscaled_height = x_height - (x_height % vae_scale_factor)
333
+ first_pass_kwargs = {
334
+ "prompt": prompt, "negative_prompt": negative_prompt, "height": downscaled_height, "width": downscaled_width,
335
+ "num_frames": actual_num_frames, "frame_rate": int(FPS), "generator": torch.Generator(device=self.device).manual_seed(used_seed),
336
+ "output_type": "latent", "conditioning_items": conditioning_items, "guidance_scale": float(guidance_scale),
337
+ **(self.config.get("first_pass", {}))
338
+ }
339
+ try:
340
+ with torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype, enabled=self.device == 'cuda'):
341
+ latents = self.pipeline(**first_pass_kwargs).images
342
+ pixel_tensor = vae_manager_singleton.decode(latents.clone(), decode_timestep=float(self.config.get("decode_timestep", 0.05)))
343
+ video_path = self._save_and_log_video(pixel_tensor, "low_res_video", FPS, temp_dir, results_dir, used_seed)
344
+ latents_cpu = latents.detach().to("cpu")
345
+ tensor_path = os.path.join(results_dir, f"latents_low_res_{used_seed}.pt")
346
+ torch.save(latents_cpu, tensor_path)
347
+ return video_path, tensor_path, used_seed
348
 
349
+ except Exception as e:
350
+ pass
351
+ finally:
352
+ torch.cuda.empty_cache()
353
+ torch.cuda.ipc_collect()
354
+ self.finalize(keep_paths=[])
 
 
 
 
 
 
 
355
 
356
+ def generate_upscale_denoise(self, latents_path, prompt, negative_prompt, guidance_scale, seed):
357
+ used_seed = random.randint(0, 2**32 - 1) if seed is None else int(seed)
358
+ seed_everething(used_seed)
359
+ temp_dir = tempfile.mkdtemp(prefix="ltxv_up_"); self._register_tmp_dir(temp_dir)
360
+ results_dir = "/app/output"; os.makedirs(results_dir, exist_ok=True)
361
+ latents_low = torch.load(latents_path).to(self.device)
362
+ with torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype, enabled=self.device == 'cuda'):
363
+ upsampled_latents = self._upsample_latents_internal(latents_low)
364
+ upsampled_latents = adain_filter_latent(latents=upsampled_latents, reference_latents=latents_low)
365
+ del latents_low; torch.cuda.empty_cache()
366
+
367
+ # --- LÓGICA DE DIVISÃO SIMPLES COM OVERLAP ---
368
+ total_frames = upsampled_latents.shape[2]
369
+ # Garante que mid_point seja pelo menos 1 para evitar um segundo chunk vazio se houver poucos frames
370
+ mid_point = max(1, total_frames // 2)
371
+ chunk1 = upsampled_latents[:, :, :mid_point, :, :]
372
+ # O segundo chunk começa um frame antes para criar o overlap
373
+ chunk2 = upsampled_latents[:, :, mid_point - 1:, :, :]
374
+
375
+ final_latents_list = []
376
+ for i, chunk in enumerate([chunk1, chunk2]):
377
+ if chunk.shape[2] <= 1: continue # Pula chunks inválidos ou vazios
378
+ second_pass_height = chunk.shape[3] * self.pipeline.vae_scale_factor
379
+ second_pass_width = chunk.shape[4] * self.pipeline.vae_scale_factor
380
+ second_pass_kwargs = {
381
+ "prompt": prompt, "negative_prompt": negative_prompt, "height": second_pass_height, "width": second_pass_width,
382
+ "num_frames": chunk.shape[2], "latents": chunk, "guidance_scale": float(guidance_scale),
383
+ "output_type": "latent", "generator": torch.Generator(device=self.device).manual_seed(used_seed),
384
+ **(self.config.get("second_pass", {}))
385
+ }
386
+ refined_chunk = self.pipeline(**second_pass_kwargs).images
387
+ # Remove o overlap do primeiro chunk refinado antes de juntar
388
+ if i == 0:
389
+ final_latents_list.append(refined_chunk[:, :, :-1, :, :])
390
+ else:
391
+ final_latents_list.append(refined_chunk)
392
+
393
+ final_latents = torch.cat(final_latents_list, dim=2)
394
+ log_tensor_info(final_latents, "Latentes Upscaled/Refinados Finais")
395
+
396
+ latents_cpu = final_latents.detach().to("cpu")
397
+ tensor_path = os.path.join(results_dir, f"latents_refined_{used_seed}.pt")
398
+ torch.save(latents_cpu, tensor_path)
399
+ pixel_tensor = vae_manager_singleton.decode(final_latents, decode_timestep=float(self.config.get("decode_timestep", 0.05)))
400
+ video_path = self._save_and_log_video(pixel_tensor, "refined_video", 24.0, temp_dir, results_dir, used_seed)
401
+ return video_path, tensor_path
402
 
 
 
 
 
 
 
403
 
404
+
405
+ def encode_mp4(self, latents_path: str, fps: int = 24):
406
+ latents = torch.load(latents_path)
407
+ seed = random.randint(0, 99999)
408
+ temp_dir = tempfile.mkdtemp(prefix="ltxv_enc_"); self._register_tmp_dir(temp_dir)
409
+ results_dir = "/app/output"; os.makedirs(results_dir, exist_ok=True)
410
 
411
+ # --- LÓGICA DE DIVISÃO SIMPLES COM OVERLAP ---
412
+ total_frames = latents.shape[2]
413
+ mid_point = max(1, total_frames // 2)
414
+ chunk1_latents = latents[:, :, :mid_point, :, :]
415
+ chunk2_latents = latents[:, :, mid_point - 1:, :, :]
416
 
417
+ video_parts = []
418
+ pixel_chunks_to_concat = []
419
+ with torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype, enabled=self.device == 'cuda'):
420
+ for i, chunk in enumerate([chunk1_latents, chunk2_latents]):
421
+ if chunk.shape[2] == 0: continue
422
+ pixel_chunk = vae_manager_singleton.decode(chunk.to(self.device), decode_timestep=float(self.config.get("decode_timestep", 0.05)))
423
+ # Remove o overlap do primeiro chunk de pixels
424
+ if i == 0:
425
+ pixel_chunks_to_concat.append(pixel_chunk[:, :, :-1, :, :])
426
+ else:
427
+ pixel_chunks_to_concat.append(pixel_chunk)
 
 
428
 
429
+ final_pixel_tensor = torch.cat(pixel_chunks_to_concat, dim=2)
430
+ final_video_path = self._save_and_log_video(final_pixel_tensor, f"final_concatenated_{seed}", fps, temp_dir, results_dir, seed)
431
+ return final_video_path
 
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
+ # --- INSTANCIAÇÃO DO SERVIÇO ---
 
 
435
  print("Criando instância do VideoService. O carregamento do modelo começará agora...")
436
+ video_generation_service = VideoService()
437
+ print("Instância do VideoService pronta para uso.")