Eueuiaa commited on
Commit
21e9173
·
verified ·
1 Parent(s): 35be4e2

Update api/ltx_server.py

Browse files
Files changed (1) hide show
  1. api/ltx_server.py +189 -318
api/ltx_server.py CHANGED
@@ -25,6 +25,7 @@ import yaml
25
  from typing import List, Dict
26
  from pathlib import Path
27
  import imageio
 
28
  import tempfile
29
  from huggingface_hub import hf_hub_download
30
  import sys
@@ -40,9 +41,8 @@ from managers.vae_manager import vae_manager_singleton
40
  from tools.video_encode_tool import video_encode_tool_singleton
41
  DEPS_DIR = Path("/data")
42
  LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
43
- if not LTX_VIDEO_REPO_DIR.exists():
44
- print(f"[DEBUG] Repositório não encontrado em {LTX_VIDEO_REPO_DIR}. Rodando setup...")
45
- run_setup()
46
  def run_setup():
47
  setup_script_path = "setup.py"
48
  if not os.path.exists(setup_script_path):
@@ -55,6 +55,11 @@ def run_setup():
55
  except subprocess.CalledProcessError as e:
56
  print(f"[DEBUG] ERRO no setup.py (code {e.returncode}). Abortando.")
57
  sys.exit(1)
 
 
 
 
 
58
  def add_deps_to_path():
59
  repo_path = str(LTX_VIDEO_REPO_DIR.resolve())
60
  if str(LTX_VIDEO_REPO_DIR.resolve()) not in sys.path:
@@ -117,61 +122,39 @@ def _query_gpu_processes_via_nvidiasmi(device_index: int) -> List[Dict]:
117
  continue
118
  return results
119
  def calculate_new_dimensions(orig_w, orig_h, divisor=8):
120
- """
121
- Calcula novas dimensões mantendo a proporção, garantindo que ambos os
122
- lados sejam divisíveis pelo divisor especificado (padrão 8).
123
- """
124
  if orig_w == 0 or orig_h == 0:
125
- # Retorna um valor padrão seguro
126
  return 512, 512
127
-
128
- # Preserva a orientação (paisagem vs. retrato)
129
  if orig_w >= orig_h:
130
- # Paisagem ou quadrado
131
  aspect_ratio = orig_w / orig_h
132
- # Começa com uma altura base e calcula a largura
133
- new_h = 512 # Altura base para paisagem
134
  new_w = new_h * aspect_ratio
135
  else:
136
- # Retrato
137
  aspect_ratio = orig_h / orig_w
138
- # Começa com uma largura base e calcula a altura
139
- new_w = 512 # Largura base para retrato
140
  new_h = new_w * aspect_ratio
141
-
142
- # Arredonda AMBOS os valores para o múltiplo mais próximo do divisor
143
  final_w = int(round(new_w / divisor)) * divisor
144
  final_h = int(round(new_h / divisor)) * divisor
145
-
146
- # Garante que as dimensões não sejam zero após o arredondamento
147
  final_w = max(divisor, final_w)
148
  final_h = max(divisor, final_h)
149
-
150
  print(f"[Dimension Calc] Original: {orig_w}x{orig_h} -> Calculado: {new_w:.0f}x{new_h:.0f} -> Final (divisível por {divisor}): {final_w}x{final_h}")
151
- return final_h, final_w # Retorna (altura, largura)
152
  def handle_media_upload_for_dims(filepath, current_h, current_w):
153
- """
154
- Esta função agora usará o novo cálculo robusto.
155
- (O corpo desta função não precisa de alterações, pois ela já chama a função de cálculo)
156
- """
157
  if not filepath or not os.path.exists(str(filepath)):
158
- return gr.update(value=current_h), gr.update(value=current_w)
159
  try:
160
  if str(filepath).lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
161
  with Image.open(filepath) as img:
162
  orig_w, orig_h = img.size
163
- else: # Assumir que é um vídeo
164
  with imageio.get_reader(filepath) as reader:
165
  meta = reader.get_meta_data()
166
  orig_w, orig_h = meta.get('size', (current_w, current_h))
167
-
168
- # Chama a nova função corrigida
169
  new_h, new_w = calculate_new_dimensions(orig_w, orig_h)
170
-
171
- return gr.update(value=new_h), gr.update(value=new_w)
172
  except Exception as e:
173
  print(f"Erro ao processar mídia para dimensões: {e}")
174
- return gr.update(value=current_h), gr.update(value=current_w)
175
  def _gpu_process_table(processes: List[Dict], current_pid: int) -> str:
176
  if not processes:
177
  return " - Processos ativos: (nenhum)\n"
@@ -233,7 +216,6 @@ class VideoService:
233
  self._apply_precision_policy()
234
  print(f"[DEBUG] runtime_autocast_dtype = {getattr(self, 'runtime_autocast_dtype', None)}")
235
 
236
- # Injeta pipeline/vae no manager (impede vae=None)
237
  vae_manager_singleton.attach_pipeline(
238
  self.pipeline,
239
  device=self.device,
@@ -398,17 +380,10 @@ class VideoService:
398
  pass
399
  print(f"[DEBUG] FP8→BF16: params_promoted={p_cnt}, buffers_promoted={b_cnt}")
400
 
401
-
402
-
403
  @torch.no_grad()
404
  def _upsample_latents_internal(self, latents: torch.Tensor) -> torch.Tensor:
405
- """
406
- Lógica extraída diretamente da LTXMultiScalePipeline para upscale de latentes.
407
- """
408
  if not self.latent_upsampler:
409
  raise ValueError("Latent Upsampler não está carregado.")
410
-
411
- # Garante que os modelos estejam no dispositivo correto
412
  self.latent_upsampler.to(self.device)
413
  self.pipeline.vae.to(self.device)
414
  print(f"[DEBUG-UPSAMPLE] Shape de entrada: {tuple(latents.shape)}")
@@ -416,11 +391,8 @@ class VideoService:
416
  upsampled_latents = self.latent_upsampler(latents)
417
  upsampled_latents = normalize_latents(upsampled_latents, self.pipeline.vae, vae_per_channel_normalize=True)
418
  print(f"[DEBUG-UPSAMPLE] Shape de saída: {tuple(upsampled_latents.shape)}")
419
-
420
  return upsampled_latents
421
 
422
-
423
-
424
  def _apply_precision_policy(self):
425
  prec = str(self.config.get("precision", "")).lower()
426
  self.runtime_autocast_dtype = torch.float32
@@ -454,156 +426,124 @@ class VideoService:
454
  print(f"[DEBUG] Cond shape={tuple(out.shape)} dtype={out.dtype} device={out.device}")
455
  return out
456
 
457
-
458
  def _dividir_latentes_por_tamanho(self, latents_brutos, num_latente_por_chunk: int, overlap: int = 1):
459
- """
460
- Divide o tensor de latentes em chunks com tamanho definido em número de latentes.
461
-
462
- Args:
463
- latents_brutos: tensor [B, C, T, H, W]
464
- num_latente_por_chunk: número de latentes por chunk
465
- overlap: número de frames que se sobrepõem entre chunks
466
-
467
- Returns:
468
- List[tensor]: lista de chunks cloneados
469
- """
470
  sum_latent = latents_brutos.shape[2]
471
  chunks = []
472
-
473
  if num_latente_por_chunk >= sum_latent:
474
- return [latents_brutos]
475
-
476
- n_chunks = (sum_latent) // num_latente_por_chunk
477
- steps = sum_latent//n_chunks
478
- print("================PODA CAUSAL=================")
479
- print(f"[DEBUG] TOTAL LATENTES = {sum_latent}")
480
- print(f"[DEBUG] LATENTES min por chunk = {num_latente_por_chunk}")
481
- print(f"[DEBUG] Número de chunks = {n_chunks}")
482
- if n_chunks > 1:
483
- i=0
484
- while i < n_chunks:
485
- start = (num_latente_por_chunk*i)
486
- end = (start+num_latente_por_chunk+overlap)
487
- if i+1 < n_chunks:
488
- chunk = latents_brutos[:, :, start:end, :, :].clone().detach()
489
- print(f"[DEBUG] chunk{i+1}[:, :, {start}:{end}, :, :] = {chunk.shape[2]}")
490
- else:
491
- chunk = latents_brutos[:, :, start:, :, :].clone().detach()
492
- print(f"[DEBUG] chunk{i+1}[:, :, {start}:, :, :] = {chunk.shape[2]}")
493
- chunks.append(chunk)
494
- i+=1
495
- else:
496
- print(f"[DEBUG] numero chunks minimo ")
497
- print(f"[DEBUG] latents_brutos[:, :, :, :, :] = {latents_brutos.shape[2]}")
498
- chunks.append(latents_brutos)
499
- print("================PODA CAUSAL=================")
 
500
  return chunks
501
 
502
  def _get_total_frames(self, video_path: str) -> int:
503
  cmd = [
504
- "ffprobe",
505
- "-v", "error",
506
- "-select_streams", "v:0",
507
- "-count_frames",
508
- "-show_entries", "stream=nb_read_frames",
509
- "-of", "default=nokey=1:noprint_wrappers=1",
510
- video_path
511
  ]
512
  result = subprocess.run(cmd, capture_output=True, text=True, check=True)
513
  return int(result.stdout.strip())
514
 
515
  def _gerar_lista_com_transicoes(self, pasta: str, video_paths: list[str], crossfade_frames: int = 8) -> list[str]:
516
- """
517
- Gera uma nova lista de vídeos aplicando transições suaves (blend frame a frame)
518
- seguindo exatamente a lógica linear de Carlos.
519
- """
520
- import os, subprocess, shutil
521
-
522
- poda = crossfade_frames
523
- total_partes = len(video_paths)
524
- video_fade_fim = None
525
- video_fade_ini = None
526
- nova_lista = []
527
-
528
- print("===========CONCATECAO CAUSAL=============")
529
-
530
- print(f"[DEBUG] Iniciando pipeline com {total_partes} vídeos e {poda} frames de crossfade")
531
-
532
- for i in range(total_partes):
533
- base = video_paths[i]
534
-
535
- # --- PODA ---
536
- video_podado = os.path.join(pasta, f"{base}_podado_{i}.mp4")
537
 
538
-
539
- if i<total_partes-1:
540
- end_frame = self._get_total_frames(base) - poda
541
- else:
542
- end_frame = self._get_total_frames(base)
 
543
 
544
- if i>0:
545
- start_frame = poda
546
- else:
547
- start_frame = 0
548
-
549
- cmd_fim = (
550
- f'ffmpeg -y -hide_banner -loglevel error -i "{base}" '
551
- f'-vf "trim=start_frame={start_frame}:end_frame={end_frame},setpts=PTS-STARTPTS" '
552
- f'-an "{video_podado}"'
553
- )
554
- subprocess.run(cmd_fim, shell=True, check=True)
555
 
556
-
557
- # --- FADE_INI ---
558
- if i > 0:
559
- video_fade_ini = os.path.join(pasta, f"{base}_fade_ini_{i}.mp4")
560
- cmd_ini = (
561
- f'ffmpeg -y -hide_banner -loglevel error -i "{base}" '
562
- f'-vf "trim=end_frame={poda},setpts=PTS-STARTPTS" -an "{video_fade_ini}"'
563
- )
564
- subprocess.run(cmd_ini, shell=True, check=True)
565
-
566
- # --- TRANSIÇÃO ---
567
- if video_fade_fim and video_fade_ini:
568
- video_fade = os.path.join(pasta, f"transicao_{i}_{i+1}.mp4")
569
- cmd_blend = (
570
- f'ffmpeg -y -hide_banner -loglevel error '
571
- f'-i "{video_fade_fim}" -i "{video_fade_ini}" '
572
- f'-filter_complex "[0:v][1:v]blend=all_expr=\'A*(1-T/{poda})+B*(T/{poda})\',format=yuv420p" '
573
- f'-frames:v {poda} "{video_fade}"'
574
- )
575
- subprocess.run(cmd_blend, shell=True, check=True)
576
- print(f"[DEBUG] transicao adicionada {i}/{i+1} {self._get_total_frames(video_fade)} frames ✅")
577
- nova_lista.append(video_fade)
578
-
579
- # --- FADE_FIM ---
580
- if i<=total_partes-1:
581
- video_fade_fim = os.path.join(pasta, f"{base}_fade_fim_{i}.mp4")
582
- cmd_fim = (
583
- f'ffmpeg -y -hide_banner -loglevel error -i "{base}" '
584
- f'-vf "trim=start_frame={end_frame-poda},setpts=PTS-STARTPTS" -an "{video_fade_fim}"'
585
- )
586
- subprocess.run(cmd_fim, shell=True, check=True)
587
-
588
- nova_lista.append(video_podado)
589
- print(f"[DEBUG] Video podado {i+1} adicionado {self._get_total_frames(video_podado)} frames ✅")
590
-
591
-
592
 
593
- print("===========CONCATECAO CAUSAL=============")
594
- print(f"[DEBUG] {nova_lista}")
595
- return nova_lista
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
 
597
  def _concat_mp4s_no_reencode(self, mp4_list: List[str], out_path: str):
598
- """
599
- Concatena múltiplos MP4s sem reencode usando o demuxer do ffmpeg.
600
- ATENÇÃO: todos os arquivos precisam ter mesmo codec, fps, resolução etc.
601
- """
602
- if not mp4_list or len(mp4_list) < 2:
603
- raise ValueError("Forneça pelo menos dois arquivos MP4 para concatenar.")
604
-
605
-
606
- # Cria lista temporária para o ffmpeg
607
  with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
608
  for mp4 in mp4_list:
609
  f.write(f"file '{os.path.abspath(mp4)}'\n")
@@ -620,10 +560,6 @@ class VideoService:
620
  except Exception:
621
  pass
622
 
623
-
624
- # ==============================================================================
625
- # --- FUNÇÃO GENERATE COMPLETA E ATUALIZADA ---
626
- # ==============================================================================
627
  def generate(
628
  self,
629
  prompt,
@@ -639,21 +575,20 @@ class VideoService:
639
  height=512,
640
  width=704,
641
  duration=2.0,
642
- frames_to_use=9,
643
  seed=42,
644
  randomize_seed=True,
645
  guidance_scale=3.0,
646
  improve_texture=True,
647
  progress_callback=None,
648
- external_decode=True,
649
  ):
650
  t_all = time.perf_counter()
651
- print(f"[DEBUG] generate() begin mode={mode} external_decode={external_decode} improve_texture={improve_texture}")
652
  if self.device == "cuda":
653
  torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats()
654
  self._log_gpu_memory("Início da Geração")
655
 
656
- # --- Setup Inicial (como antes) ---
657
  if mode == "image-to-video" and not start_image_filepath:
658
  raise ValueError("A imagem de início é obrigatória para o modo image-to-video")
659
  used_seed = random.randint(0, 2**32 - 1) if randomize_seed else int(seed)
@@ -682,49 +617,33 @@ class VideoService:
682
  print(f"[DEBUG] Conditioning items: {len(conditioning_items)}")
683
 
684
  call_kwargs = {
685
- "prompt": prompt,
686
- "negative_prompt": negative_prompt,
687
- "height": height_padded,
688
- "width": width_padded,
689
- "num_frames": actual_num_frames,
690
- "frame_rate": int(FPS),
691
- "generator": generator,
692
- "output_type": "latent",
693
- "conditioning_items": conditioning_items if conditioning_items else None,
694
- "media_items": None,
695
- "decode_timestep": self.config["decode_timestep"],
696
- "decode_noise_scale": self.config["decode_noise_scale"],
697
- "stochastic_sampling": self.config["stochastic_sampling"],
698
- "image_cond_noise_scale": 0.01,
699
- "is_video": True,
700
- "vae_per_channel_normalize": True,
701
- "mixed_precision": (self.config["precision"] == "mixed_precision"),
702
- "offload_to_cpu": False,
703
- "enhance_prompt": False,
704
- "skip_layer_strategy": SkipLayerStrategy.AttentionValues,
705
  }
706
- latents = None
707
- latents_list[]
 
708
  temp_dir = tempfile.mkdtemp(prefix="ltxv_"); self._register_tmp_dir(temp_dir)
709
  results_dir = "/app/output"; os.makedirs(results_dir, exist_ok=True)
710
 
711
-
712
  try:
713
  if improve_texture:
714
  ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
715
  with ctx:
716
-
717
  if not self.latent_upsampler:
718
  raise ValueError("Upscaler espacial não carregado, mas 'improve_texture' está ativo.")
719
 
720
- # --- ETAPA 1: GERAÇÃO BASE (FIRST PASS) ---
721
  print("\n--- INICIANDO ETAPA 1: GERAÇÃO BASE (FIRST PASS) ---")
722
  t_pass1 = time.perf_counter()
723
-
724
  first_pass_config = self.config.get("first_pass", {}).copy()
725
  first_pass_config.pop("num_inference_steps", None)
726
  downscale_factor = self.config.get("downscale_factor", 0.6666666)
727
- vae_scale_factor = self.pipeline.vae_scale_factor # Geralmente 8
728
  x_width = int(width_padded * downscale_factor)
729
  downscaled_width = x_width - (x_width % vae_scale_factor)
730
  x_height = int(height_padded * downscale_factor)
@@ -733,155 +652,107 @@ class VideoService:
733
 
734
  first_pass_kwargs = call_kwargs.copy()
735
  first_pass_kwargs.update({
736
- "output_type": "latent",
737
- "width": downscaled_width,
738
- "height": downscaled_height,
739
- "guidance_scale": float(guidance_scale),
740
- **first_pass_config
741
  })
742
 
743
  print(f"[DEBUG] First Pass: Gerando em {downscaled_width}x{downscaled_height}...")
 
744
  latents = self.pipeline(**first_pass_kwargs).images
745
  log_tensor_info(latents, "Latentes Base (First Pass)")
746
  print(f"[DEBUG] First Pass concluída em {time.perf_counter() - t_pass1:.2f}s")
747
- del pipeline
748
 
749
- ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
750
  with ctx:
751
-
752
  print("\n--- INICIANDO ETAPA 2: UPSCALE DOS LATENTES ---")
753
  t_upscale = time.perf_counter()
754
  upsampled_latents = self._upsample_latents_internal(latents)
755
  upsampled_latents = adain_filter_latent(latents=upsampled_latents, reference_latents=latents)
756
  print(f"[DEBUG] Upscale de Latentes concluído em {time.perf_counter() - t_upscale:.2f}s")
757
- latents_cpu = upsampled_latents.detach().to("cpu", non_blocking=True)
758
- del upsampled_latents;
759
- del latents; gc.collect(); torch.cuda.empty_cache()
760
- del spatial_upscaler_path
761
- #latents_parts_up = self._dividir_latentes_por_tamanho(latents_cpu_up,4,1)
762
- latents_parts_up[latents_cpu]
763
- #del latents_cpu_up
 
 
764
 
765
- ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
766
- with ctx:
767
- for latents in latents_parts_up:
768
- latents = adain_filter_latent(latents=latents, reference_latents=latents_cpu_up)
769
-
770
- # # --- ETAPA 3: REFINAMENTO DE TEXTURA (SECOND PASS) ---
771
- print("\n--- INICIANDO ETAPA 3: REFINAMENTO DE TEXTURA (SECOND PASS) ---")
772
  second_pass_config = self.config.get("second_pass", {}).copy()
773
  second_pass_config.pop("num_inference_steps", None)
774
- second_pass_width = downscaled_width * 2
775
- second_pass_height = downscaled_height * 2
 
 
776
  print(f"[DEBUG] Second Pass Dims: Target ({second_pass_width}x{second_pass_height})")
777
  t_pass2 = time.perf_counter()
778
  second_pass_kwargs = call_kwargs.copy()
779
  second_pass_kwargs.update({
780
- "output_type": "latent",
781
- "width": second_pass_width,
782
- "height": second_pass_height,
783
- "latents": latents,
784
  "guidance_scale": float(guidance_scale),
 
785
  **second_pass_config
786
  })
787
- print(f"[DEBUG] Second Pass: Refinando em {width_padded}x{height_padded}...")
788
  final_latents = self.pipeline(**second_pass_kwargs).images
789
  log_tensor_info(final_latents, "Latentes Finais (Pós-Second Pass)")
790
  print(f"[DEBUG] Second part Pass concluída em {time.perf_counter() - t_pass2:.2f}s")
791
  latents_cpu = final_latents.detach().to("cpu", non_blocking=True)
792
  latents_list.append(latents_cpu)
793
- del final_latents; gc.collect(); torch.cuda.empty_cache()
794
- del pipeline
795
-
796
  else:
797
  ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
798
  with ctx:
799
  print("\n--- INICIANDO GERAÇÃO DE ETAPA ÚNICA ---")
800
  t_single = time.perf_counter()
801
  single_pass_call_kwargs = call_kwargs.copy()
802
- first_pass_config_from_yaml = self.config.get("first_pass", {})
803
- single_pass_call_kwargs["timesteps"] = first_pass_config_from_yaml.get("timesteps")
804
- single_pass_call_kwargs["guidance_scale"] = float(guidance_scale)
805
- single_pass_call_kwargs["stg_scale"] = first_pass_config_from_yaml.get("stg_scale")
806
- single_pass_call_kwargs["rescaling_scale"] = first_pass_config_from_yaml.get("rescaling_scale")
807
- single_pass_call_kwargs["skip_block_list"] = first_pass_config_from_yaml.get("skip_block_list")
808
- single_pass_call_kwargs.pop("num_inference_steps", None)
809
- single_pass_call_kwargs.pop("first_pass", None)
810
- single_pass_call_kwargs.pop("second_pass", None)
811
- single_pass_call_kwargs.pop("downscale_factor", None)
812
-
813
- latents_single_pass = pipeline_instance(**single_pass_call_kwargs).images
814
  log_tensor_info(latents_single_pass, "Latentes Finais (Etapa Única)")
815
  print(f"[DEBUG] Etapa única concluída em {time.perf_counter() - t_single:.2f}s")
816
  latents_cpu = latents_single_pass.detach().to("cpu", non_blocking=True)
817
- latents_list.append(latents_single_pass)
818
  del latents_single_pass; gc.collect(); torch.cuda.empty_cache()
819
- del pipeline
820
 
821
- ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
822
- with ctx:
823
- # --- ETAPA FINAL: DECODIFICAÇÃO E CODIFICAÇÃO MP4 ---
824
- print("\n--- INICIANDO ETAPA FINAL: DECODIFICAÇÃO E MONTAGEM ---")
825
-
826
- latents_parts[]
827
- for latents in latents_list:
828
- latents_parts.append(self._dividir_latentes_por_tamanho(latents_cpu,4,1))
829
 
830
- partes_mp4 = []
831
- par = 0
832
- for latents in latents_parts:
833
- latents = adain_filter_latent(latents=latents, reference_latents=latents_cpu)
834
- print(f"[DEBUG] Partição {par}: {tuple(latents.shape)}")
835
- par = par + 1
836
- output_video_path = os.path.join(temp_dir, f"output_{used_seed}_{par}.mp4")
837
- final_output_path = None
838
- print("[DEBUG] Decodificando bloco de latentes com VAE → tensor de pixels...")
839
- # Usar manager om timestep por item; previne target_shape e rota NoneType.decode
840
- pixel_tensor = vae_manager_singleton.decode(
841
- latents.to(self.device, non_blocking=True),
842
- decode_timestep=float(self.config.get("decode_timestep", 0.05))
843
- )
844
- log_tensor_info(pixel_tensor, "Pixel tensor (VAE saída)")
845
-
846
- print("[DEBUG] Codificando MP4 a partir do tensor de pixels (bloco inteiro)...")
847
- video_encode_tool_singleton.save_video_from_tensor(
848
- pixel_tensor,
849
- output_video_path,
850
- fps=call_kwargs["frame_rate"],
851
- progress_callback=progress_callback
852
- )
853
-
854
- candidate = os.path.join(results_dir, f"output_par_{par}.mp4")
855
- try:
856
- shutil.move(output_video_path, candidate)
857
- final_output_path = candidate
858
- print(f"[DEBUG] MP4 parte {par} movido para {final_output_path}")
859
- partes_mp4.append(final_output_path)
860
- except Exception as e:
861
- final_output_path = output_video_path
862
- print(f"[DEBUG] Falha no move; usando tmp como final: {e}")
863
-
864
- del pixel_tensor
865
- del latents; gc.collect(); torch.cuda.empty_cache()
866
- del candidate
867
-
868
- total_partes = len(partes_mp4)
869
- if (total_partes>1):
870
- final_vid = os.path.join(results_dir, f"concat_fim_{used_seed}.mp4")
871
- partes_mp4_fade = self._gerar_lista_com_transicoes(pasta=results_dir, video_paths=partes_mp4, crossfade_frames=8)
872
  self._concat_mp4s_no_reencode(partes_mp4_fade, final_vid)
873
  else:
874
- final_vid = partes_mp4[0]
875
-
876
- del partes_mp4_fade
877
- del latents_list
878
- del latents_parts
879
- del partes_mp4
880
-
881
  self._log_gpu_memory("Fim da Geração")
882
  return final_vid, used_seed
883
 
884
-
885
  except Exception as e:
886
  print("[DEBUG] EXCEÇÃO NA GERAÇÃO:")
887
  print("".join(traceback.format_exception(type(e), e, e.__traceback__)))
@@ -889,10 +760,10 @@ class VideoService:
889
 
890
  finally:
891
  gc.collect()
892
- torch.cuda.empty_cache()
893
- torch.cuda.ipc_collect()
894
- self.finalize(keep_paths=[])
 
895
 
896
-
897
  print("Criando instância do VideoService. O carregamento do modelo começará agora...")
898
  video_generation_service = VideoService()
 
25
  from typing import List, Dict
26
  from pathlib import Path
27
  import imageio
28
+ from PIL import Image # Import adicionado para handle_media_upload_for_dims
29
  import tempfile
30
  from huggingface_hub import hf_hub_download
31
  import sys
 
41
  from tools.video_encode_tool import video_encode_tool_singleton
42
  DEPS_DIR = Path("/data")
43
  LTX_VIDEO_REPO_DIR = DEPS_DIR / "LTX-Video"
44
+
45
+ # CORREÇÃO: Movido run_setup para o início para garantir que seja definido antes de ser chamado.
 
46
  def run_setup():
47
  setup_script_path = "setup.py"
48
  if not os.path.exists(setup_script_path):
 
55
  except subprocess.CalledProcessError as e:
56
  print(f"[DEBUG] ERRO no setup.py (code {e.returncode}). Abortando.")
57
  sys.exit(1)
58
+
59
+ if not LTX_VIDEO_REPO_DIR.exists():
60
+ print(f"[DEBUG] Repositório não encontrado em {LTX_VIDEO_REPO_DIR}. Rodando setup...")
61
+ run_setup()
62
+
63
  def add_deps_to_path():
64
  repo_path = str(LTX_VIDEO_REPO_DIR.resolve())
65
  if str(LTX_VIDEO_REPO_DIR.resolve()) not in sys.path:
 
122
  continue
123
  return results
124
  def calculate_new_dimensions(orig_w, orig_h, divisor=8):
 
 
 
 
125
  if orig_w == 0 or orig_h == 0:
 
126
  return 512, 512
 
 
127
  if orig_w >= orig_h:
 
128
  aspect_ratio = orig_w / orig_h
129
+ new_h = 512
 
130
  new_w = new_h * aspect_ratio
131
  else:
 
132
  aspect_ratio = orig_h / orig_w
133
+ new_w = 512
 
134
  new_h = new_w * aspect_ratio
 
 
135
  final_w = int(round(new_w / divisor)) * divisor
136
  final_h = int(round(new_h / divisor)) * divisor
 
 
137
  final_w = max(divisor, final_w)
138
  final_h = max(divisor, final_h)
 
139
  print(f"[Dimension Calc] Original: {orig_w}x{orig_h} -> Calculado: {new_w:.0f}x{new_h:.0f} -> Final (divisível por {divisor}): {final_w}x{final_h}")
140
+ return final_h, final_w
141
  def handle_media_upload_for_dims(filepath, current_h, current_w):
142
+ # CORREÇÃO: Gradio (`gr`) não deve ser usado no backend. Retornando tupla diretamente.
 
 
 
143
  if not filepath or not os.path.exists(str(filepath)):
144
+ return current_h, current_w
145
  try:
146
  if str(filepath).lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
147
  with Image.open(filepath) as img:
148
  orig_w, orig_h = img.size
149
+ else:
150
  with imageio.get_reader(filepath) as reader:
151
  meta = reader.get_meta_data()
152
  orig_w, orig_h = meta.get('size', (current_w, current_h))
 
 
153
  new_h, new_w = calculate_new_dimensions(orig_w, orig_h)
154
+ return new_h, new_w
 
155
  except Exception as e:
156
  print(f"Erro ao processar mídia para dimensões: {e}")
157
+ return current_h, current_w
158
  def _gpu_process_table(processes: List[Dict], current_pid: int) -> str:
159
  if not processes:
160
  return " - Processos ativos: (nenhum)\n"
 
216
  self._apply_precision_policy()
217
  print(f"[DEBUG] runtime_autocast_dtype = {getattr(self, 'runtime_autocast_dtype', None)}")
218
 
 
219
  vae_manager_singleton.attach_pipeline(
220
  self.pipeline,
221
  device=self.device,
 
380
  pass
381
  print(f"[DEBUG] FP8→BF16: params_promoted={p_cnt}, buffers_promoted={b_cnt}")
382
 
 
 
383
  @torch.no_grad()
384
  def _upsample_latents_internal(self, latents: torch.Tensor) -> torch.Tensor:
 
 
 
385
  if not self.latent_upsampler:
386
  raise ValueError("Latent Upsampler não está carregado.")
 
 
387
  self.latent_upsampler.to(self.device)
388
  self.pipeline.vae.to(self.device)
389
  print(f"[DEBUG-UPSAMPLE] Shape de entrada: {tuple(latents.shape)}")
 
391
  upsampled_latents = self.latent_upsampler(latents)
392
  upsampled_latents = normalize_latents(upsampled_latents, self.pipeline.vae, vae_per_channel_normalize=True)
393
  print(f"[DEBUG-UPSAMPLE] Shape de saída: {tuple(upsampled_latents.shape)}")
 
394
  return upsampled_latents
395
 
 
 
396
  def _apply_precision_policy(self):
397
  prec = str(self.config.get("precision", "")).lower()
398
  self.runtime_autocast_dtype = torch.float32
 
426
  print(f"[DEBUG] Cond shape={tuple(out.shape)} dtype={out.dtype} device={out.device}")
427
  return out
428
 
 
429
  def _dividir_latentes_por_tamanho(self, latents_brutos, num_latente_por_chunk: int, overlap: int = 1):
 
 
 
 
 
 
 
 
 
 
 
430
  sum_latent = latents_brutos.shape[2]
431
  chunks = []
 
432
  if num_latente_por_chunk >= sum_latent:
433
+ return [latents_brutos.clone().detach()] # CORREÇÃO: Retornar uma lista e clonar
434
+ # CORREÇÃO: Lógica de chunking simplificada e corrigida para evitar estouro de índice
435
+ start = 0
436
+ while start < sum_latent:
437
+ end = min(start + num_latente_por_chunk, sum_latent)
438
+ # Para o overlap, pegamos um pouco do chunk anterior, exceto para o primeiro
439
+ overlap_start = max(0, start - overlap)
440
+
441
+ # O chunk a ser processado vai de `overlap_start` até `end`
442
+ # mas o chunk "real" para junção posterior seria de `start` a `end`
443
+ # A lógica atual já faz um overlap simples, vamos refinar
444
+ effective_end = min(start + num_latente_por_chunk, sum_latent)
445
+ chunk = latents_brutos[:, :, start:effective_end, :, :].clone().detach()
446
+
447
+ # Adiciona overlap no final se não for o último chunk
448
+ if effective_end < sum_latent:
449
+ overlap_end = min(effective_end + overlap, sum_latent)
450
+ chunk = latents_brutos[:, :, start:overlap_end, :, :].clone().detach()
451
+
452
+ print(f"[DEBUG] Chunk: start={start}, end={chunk.shape[2]}, total_latents={sum_latent}")
453
+ chunks.append(chunk)
454
+
455
+ # Avança para o próximo chunk
456
+ if start + num_latente_por_chunk >= sum_latent:
457
+ break
458
+ start += num_latente_por_chunk
459
+
460
  return chunks
461
 
462
  def _get_total_frames(self, video_path: str) -> int:
463
  cmd = [
464
+ "ffprobe", "-v", "error", "-select_streams", "v:0", "-count_frames",
465
+ "-show_entries", "stream=nb_read_frames", "-of", "default=nokey=1:noprint_wrappers=1", video_path
 
 
 
 
 
466
  ]
467
  result = subprocess.run(cmd, capture_output=True, text=True, check=True)
468
  return int(result.stdout.strip())
469
 
470
  def _gerar_lista_com_transicoes(self, pasta: str, video_paths: list[str], crossfade_frames: int = 8) -> list[str]:
471
+ # Esta função parece complexa e propensa a erros com nomes de arquivo.
472
+ # Por segurança, mantendo a lógica original, mas corrigindo possíveis bugs de `shell=True`
473
+ # e garantindo que os arquivos existam.
474
+ if len(video_paths) <= 1:
475
+ return video_paths # Não há o que fazer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ nova_lista_intermediaria = []
478
+ # Primeiro, cria todos os vídeos podados
479
+ videos_podados = []
480
+ for i, base in enumerate(video_paths):
481
+ video_podado = os.path.join(pasta, f"podado_{i}.mp4")
482
+ total_frames = self._get_total_frames(base)
483
 
484
+ start_frame = crossfade_frames if i > 0 else 0
485
+ end_frame = total_frames - crossfade_frames if i < len(video_paths) - 1 else total_frames
 
 
 
 
 
 
 
 
 
486
 
487
+ # Pular poda se não houver frames suficientes
488
+ if start_frame >= end_frame:
489
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
+ cmd = [
492
+ 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', '-i', base,
493
+ '-vf', f'trim=start_frame={start_frame}:end_frame={end_frame},setpts=PTS-STARTPTS',
494
+ '-an', video_podado
495
+ ]
496
+ subprocess.run(cmd, check=True)
497
+ videos_podados.append(video_podado)
498
+
499
+ # Agora, cria as transições e monta a lista final
500
+ lista_final = [videos_podados[0]]
501
+ for i in range(len(video_paths) - 1):
502
+ video_anterior = video_paths[i]
503
+ video_seguinte = video_paths[i+1]
504
+
505
+ # Extrai fade_fim do anterior
506
+ fade_fim_path = os.path.join(pasta, f"fade_fim_{i}.mp4")
507
+ total_frames_anterior = self._get_total_frames(video_anterior)
508
+ cmd_fim = [
509
+ 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', '-i', video_anterior,
510
+ '-vf', f'trim=start_frame={total_frames_anterior - crossfade_frames},setpts=PTS-STARTPTS',
511
+ '-an', fade_fim_path
512
+ ]
513
+ subprocess.run(cmd_fim, check=True)
514
+
515
+ # Extrai fade_ini do seguinte
516
+ fade_ini_path = os.path.join(pasta, f"fade_ini_{i+1}.mp4")
517
+ cmd_ini = [
518
+ 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', '-i', video_seguinte,
519
+ '-vf', f'trim=end_frame={crossfade_frames},setpts=PTS-STARTPTS', '-an', fade_ini_path
520
+ ]
521
+ subprocess.run(cmd_ini, check=True)
522
+
523
+ # Cria a transição
524
+ transicao_path = os.path.join(pasta, f"transicao_{i}_{i+1}.mp4")
525
+ cmd_blend = [
526
+ 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error',
527
+ '-i', fade_fim_path, '-i', fade_ini_path,
528
+ '-filter_complex', f'[0:v][1:v]blend=all_expr=\'A*(1-T/{crossfade_frames})+B*(T/{crossfade_frames})\',format=yuv420p',
529
+ '-frames:v', str(crossfade_frames), transicao_path
530
+ ]
531
+ subprocess.run(cmd_blend, check=True)
532
+
533
+ lista_final.append(transicao_path)
534
+ lista_final.append(videos_podados[i+1])
535
+
536
+ return lista_final
537
 
538
  def _concat_mp4s_no_reencode(self, mp4_list: List[str], out_path: str):
539
+ if not mp4_list:
540
+ raise ValueError("A lista de MP4s para concatenar está vazia.")
541
+ # Se houver apenas um vídeo, apenas o copie/mova
542
+ if len(mp4_list) == 1:
543
+ shutil.move(mp4_list[0], out_path)
544
+ print(f"[DEBUG] Apenas um vídeo, movido para: {out_path}")
545
+ return
546
+
 
547
  with tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt") as f:
548
  for mp4 in mp4_list:
549
  f.write(f"file '{os.path.abspath(mp4)}'\n")
 
560
  except Exception:
561
  pass
562
 
 
 
 
 
563
  def generate(
564
  self,
565
  prompt,
 
575
  height=512,
576
  width=704,
577
  duration=2.0,
578
+ frames_to_use=9, # Parâmetro não utilizado, mas mantido por consistência
579
  seed=42,
580
  randomize_seed=True,
581
  guidance_scale=3.0,
582
  improve_texture=True,
583
  progress_callback=None,
584
+ external_decode=True, # Parâmetro não utilizado, mas mantido
585
  ):
586
  t_all = time.perf_counter()
587
+ print(f"[DEBUG] generate() begin mode={mode} improve_texture={improve_texture}")
588
  if self.device == "cuda":
589
  torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats()
590
  self._log_gpu_memory("Início da Geração")
591
 
 
592
  if mode == "image-to-video" and not start_image_filepath:
593
  raise ValueError("A imagem de início é obrigatória para o modo image-to-video")
594
  used_seed = random.randint(0, 2**32 - 1) if randomize_seed else int(seed)
 
617
  print(f"[DEBUG] Conditioning items: {len(conditioning_items)}")
618
 
619
  call_kwargs = {
620
+ "prompt": prompt, "negative_prompt": negative_prompt, "height": height_padded, "width": width_padded,
621
+ "num_frames": actual_num_frames, "frame_rate": int(FPS), "generator": generator, "output_type": "latent",
622
+ "conditioning_items": conditioning_items if conditioning_items else None, "media_items": None,
623
+ "decode_timestep": self.config["decode_timestep"], "decode_noise_scale": self.config["decode_noise_scale"],
624
+ "stochastic_sampling": self.config["stochastic_sampling"], "image_cond_noise_scale": 0.01, "is_video": True,
625
+ "vae_per_channel_normalize": True, "mixed_precision": (self.config["precision"] == "mixed_precision"),
626
+ "offload_to_cpu": False, "enhance_prompt": False, "skip_layer_strategy": SkipLayerStrategy.AttentionValues,
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  }
628
+
629
+ # CORREÇÃO: Inicialização de listas
630
+ latents_list = []
631
  temp_dir = tempfile.mkdtemp(prefix="ltxv_"); self._register_tmp_dir(temp_dir)
632
  results_dir = "/app/output"; os.makedirs(results_dir, exist_ok=True)
633
 
 
634
  try:
635
  if improve_texture:
636
  ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
637
  with ctx:
 
638
  if not self.latent_upsampler:
639
  raise ValueError("Upscaler espacial não carregado, mas 'improve_texture' está ativo.")
640
 
 
641
  print("\n--- INICIANDO ETAPA 1: GERAÇÃO BASE (FIRST PASS) ---")
642
  t_pass1 = time.perf_counter()
 
643
  first_pass_config = self.config.get("first_pass", {}).copy()
644
  first_pass_config.pop("num_inference_steps", None)
645
  downscale_factor = self.config.get("downscale_factor", 0.6666666)
646
+ vae_scale_factor = self.pipeline.vae_scale_factor
647
  x_width = int(width_padded * downscale_factor)
648
  downscaled_width = x_width - (x_width % vae_scale_factor)
649
  x_height = int(height_padded * downscale_factor)
 
652
 
653
  first_pass_kwargs = call_kwargs.copy()
654
  first_pass_kwargs.update({
655
+ "output_type": "latent", "width": downscaled_width, "height": downscaled_height,
656
+ "guidance_scale": float(guidance_scale), **first_pass_config
 
 
 
657
  })
658
 
659
  print(f"[DEBUG] First Pass: Gerando em {downscaled_width}x{downscaled_height}...")
660
+ # CORREÇÃO: Usar self.pipeline, não a variável deletada 'pipeline'
661
  latents = self.pipeline(**first_pass_kwargs).images
662
  log_tensor_info(latents, "Latentes Base (First Pass)")
663
  print(f"[DEBUG] First Pass concluída em {time.perf_counter() - t_pass1:.2f}s")
 
664
 
 
665
  with ctx:
 
666
  print("\n--- INICIANDO ETAPA 2: UPSCALE DOS LATENTES ---")
667
  t_upscale = time.perf_counter()
668
  upsampled_latents = self._upsample_latents_internal(latents)
669
  upsampled_latents = adain_filter_latent(latents=upsampled_latents, reference_latents=latents)
670
  print(f"[DEBUG] Upscale de Latentes concluído em {time.perf_counter() - t_upscale:.2f}s")
671
+
672
+ # CORREÇÃO: Manter latentes originais para AdaIN e passar latentes com upscale para o second pass
673
+ reference_latents_cpu = latents.detach().to("cpu", non_blocking=True)
674
+ latents_to_refine = upsampled_latents
675
+ del upsampled_latents; del latents; gc.collect(); torch.cuda.empty_cache()
676
+
677
+ # CORREÇÃO: Lógica de chunking para o second pass
678
+ latents_parts = self._dividir_latentes_por_tamanho(latents_to_refine, 32, 8) # Exemplo: chunks de 32 frames com 8 de overlap
679
+ del latents_to_refine
680
 
681
+ with ctx:
682
+ for i, latents_chunk in enumerate(latents_parts):
683
+ print(f"\n--- INICIANDO ETAPA 3.{i+1}: REFINAMENTO DE TEXTURA (SECOND PASS) ---")
684
+ # CORREÇÃO: AdaIN precisa de latents de referência com mesmo H/W, o que não é o caso aqui.
685
+ # Vamos aplicar AdaIN com o próprio chunk para normalização, ou pular. Pulando por simplicidade.
686
+
 
687
  second_pass_config = self.config.get("second_pass", {}).copy()
688
  second_pass_config.pop("num_inference_steps", None)
689
+
690
+ # O tamanho do second pass deve ser o tamanho do latente de entrada (após upscale)
691
+ second_pass_height, second_pass_width = latents_chunk.shape[3] * 8, latents_chunk.shape[4] * 8
692
+
693
  print(f"[DEBUG] Second Pass Dims: Target ({second_pass_width}x{second_pass_height})")
694
  t_pass2 = time.perf_counter()
695
  second_pass_kwargs = call_kwargs.copy()
696
  second_pass_kwargs.update({
697
+ "output_type": "latent", "width": second_pass_width, "height": second_pass_height,
698
+ "latents": latents_chunk.to(self.device), # Mover chunk para GPU
 
 
699
  "guidance_scale": float(guidance_scale),
700
+ "num_frames": latents_chunk.shape[2], # Usar o número de frames do chunk
701
  **second_pass_config
702
  })
703
+ print(f"[DEBUG] Second Pass: Refinando chunk {i+1}/{len(latents_parts)}...")
704
  final_latents = self.pipeline(**second_pass_kwargs).images
705
  log_tensor_info(final_latents, "Latentes Finais (Pós-Second Pass)")
706
  print(f"[DEBUG] Second part Pass concluída em {time.perf_counter() - t_pass2:.2f}s")
707
  latents_cpu = final_latents.detach().to("cpu", non_blocking=True)
708
  latents_list.append(latents_cpu)
709
+ del final_latents; del latents_chunk; gc.collect(); torch.cuda.empty_cache()
 
 
710
  else:
711
  ctx = torch.autocast(device_type="cuda", dtype=self.runtime_autocast_dtype) if self.device == "cuda" else contextlib.nullcontext()
712
  with ctx:
713
  print("\n--- INICIANDO GERAÇÃO DE ETAPA ÚNICA ---")
714
  t_single = time.perf_counter()
715
  single_pass_call_kwargs = call_kwargs.copy()
716
+ # CORREÇÃO: `pipeline_instance` não existe, usar `self.pipeline`.
717
+ latents_single_pass = self.pipeline(**single_pass_call_kwargs).images
 
 
 
 
 
 
 
 
 
 
718
  log_tensor_info(latents_single_pass, "Latentes Finais (Etapa Única)")
719
  print(f"[DEBUG] Etapa única concluída em {time.perf_counter() - t_single:.2f}s")
720
  latents_cpu = latents_single_pass.detach().to("cpu", non_blocking=True)
721
+ latents_list.append(latents_cpu) # CORREÇÃO: aqui deve ser latents_cpu, não latents_single_pass
722
  del latents_single_pass; gc.collect(); torch.cuda.empty_cache()
 
723
 
724
+ # --- ETAPA FINAL: DECODIFICAÇÃO E CODIFICAÇÃO MP4 ---
725
+ print("\n--- INICIANDO ETAPA FINAL: DECODIFICAÇÃO E MONTAGEM ---")
726
+ partes_mp4 = []
727
+ for i, latents in enumerate(latents_list):
728
+ print(f"[DEBUG] Decodificando partição {i+1}/{len(latents_list)}: {tuple(latents.shape)}")
729
+ output_video_path = os.path.join(temp_dir, f"output_{used_seed}_{i}.mp4")
 
 
730
 
731
+ pixel_tensor = vae_manager_singleton.decode(
732
+ latents.to(self.device, non_blocking=True),
733
+ decode_timestep=float(self.config.get("decode_timestep", 0.05))
734
+ )
735
+ log_tensor_info(pixel_tensor, "Pixel tensor (VAE saída)")
736
+
737
+ video_encode_tool_singleton.save_video_from_tensor(
738
+ pixel_tensor, output_video_path, fps=call_kwargs["frame_rate"], progress_callback=progress_callback
739
+ )
740
+ partes_mp4.append(output_video_path)
741
+ del pixel_tensor; del latents; gc.collect(); torch.cuda.empty_cache()
742
+
743
+ final_vid = os.path.join(results_dir, f"final_video_{used_seed}.mp4")
744
+ if len(partes_mp4) > 1:
745
+ # A função _gerar_lista_com_transicoes é complexa, usando uma concatenação direta como fallback robusto.
746
+ # Para usar a transição, a lógica de overlap na divisão de latentes precisa ser perfeita.
747
+ print("[DEBUG] Múltiplas partes geradas, concatenando...")
748
+ partes_mp4_fade = self._gerar_lista_com_transicoes(pasta=temp_dir, video_paths=partes_mp4, crossfade_frames=8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  self._concat_mp4s_no_reencode(partes_mp4_fade, final_vid)
750
  else:
751
+ shutil.move(partes_mp4[0], final_vid)
752
+
 
 
 
 
 
753
  self._log_gpu_memory("Fim da Geração")
754
  return final_vid, used_seed
755
 
 
756
  except Exception as e:
757
  print("[DEBUG] EXCEÇÃO NA GERAÇÃO:")
758
  print("".join(traceback.format_exception(type(e), e, e.__traceback__)))
 
760
 
761
  finally:
762
  gc.collect()
763
+ if torch.cuda.is_available():
764
+ torch.cuda.empty_cache()
765
+ torch.cuda.ipc_collect()
766
+ self.finalize(keep_paths=[]) # O resultado final já foi movido
767
 
 
768
  print("Criando instância do VideoService. O carregamento do modelo começará agora...")
769
  video_generation_service = VideoService()