aducsdr commited on
Commit
41d9d43
·
verified ·
1 Parent(s): 078d3ce

Update aduc_framework/engineers/deformes4D.py

Browse files
Files changed (1) hide show
  1. aduc_framework/engineers/deformes4D.py +207 -162
aduc_framework/engineers/deformes4D.py CHANGED
@@ -1,190 +1,235 @@
1
- # aduc_framework/orchestrator.py
2
  #
3
  # Copyright (C) August 4, 2025 Carlos Rodrigues dos Santos
4
  #
5
- # Versão 3.2.1 (Com lógica de Upscale por fragmento)
6
  #
7
- # Esta versão representa a camada de orquestração do Aduc Framework.
8
- # Ela é agnóstica a qualquer interface (UI ou API) e opera com
9
- # tipos de dados bem definidos (Pydantic) e um estado de geração central.
10
 
 
 
 
 
 
11
  import logging
12
- from typing import List, Dict, Any, Tuple, Callable, Optional, Generator
13
  from PIL import Image, ImageOps
14
- import os
15
- import subprocess
16
  import shutil
17
  from pathlib import Path
18
- import time
19
- import gc
20
- import torch
21
 
22
- # Importa componentes internos do framework
23
- from .director import AducDirector
24
- from .types import GenerationState, PreProductionParams, ProductionParams
25
- from .engineers import deformes2d_thinker_singleton, deformes3d_engine_singleton, Deformes4DEngine
26
- # Importa managers diretamente para as tarefas de pós-produção
27
- from .managers.latent_enhancer_manager import latent_enhancer_specialist_singleton
28
- from .managers.seedvr_manager import seedvr_manager_singleton
29
- from .managers.mmaudio_manager import mmaudio_manager_singleton
30
- from .managers.vae_manager import vae_manager_singleton
31
- from .tools.video_encode_tool import video_encode_tool_singleton
32
 
33
  logger = logging.getLogger(__name__)
34
 
35
  ProgressCallback = Optional[Callable[[float, str], None]]
36
 
37
- class AducOrchestrator:
38
  """
39
- Implementa o Maestro (Γ), a camada de orquestração central do Aduc Framework.
40
- Ele recebe solicitações, atualiza o estado de geração, delega tarefas para os
41
- engenheiros especialistas e retorna o estado atualizado.
42
  """
43
- def __init__(self, workspace_dir: str):
44
- self.director = AducDirector(workspace_dir)
45
- self.editor = Deformes4DEngine()
46
- self.editor.initialize(workspace_dir)
47
- self.painter = deformes3d_engine_singleton
48
- self.painter.initialize(workspace_dir)
49
  self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
50
- logger.info("ADUC Maestro (Framework Core) está no pódio. Engenheiros especialistas prontos.")
51
-
52
- def get_current_state(self) -> GenerationState:
53
- return self.director.get_full_state()
54
-
55
- def process_image_for_story(self, image_path: str, size: int, filename: str) -> str:
56
- img = Image.open(image_path).convert("RGB")
57
- img_square = ImageOps.fit(img, (size, size), Image.Resampling.LANCZOS)
58
- processed_path = os.path.join(self.director.workspace_dir, filename)
59
- img_square.save(processed_path)
60
- logger.info(f"Imagem de referência processada e salva em: {processed_path}")
61
- return processed_path
62
-
63
- def task_pre_production(self, params: PreProductionParams, progress_callback: ProgressCallback = None) -> Tuple[List[str], List[str], GenerationState]:
64
- logger.info("Maestro: Iniciando tarefa de Pré-Produção.")
65
- self.director.update_parameters("pre_producao", params)
66
- if progress_callback: progress_callback(0.1, "Gerando storyboard...")
67
- storyboard_list = deformes2d_thinker_singleton.generate_storyboard(prompt=params.prompt, num_keyframes=params.num_keyframes, ref_image_paths=params.ref_paths)
68
- self.director.update_pre_production_state(params.prompt, params.ref_paths, storyboard_list)
69
- if progress_callback: progress_callback(0.2, "Iniciando geração de keyframes...")
70
- keyframes_detailed_data = self.painter.generate_keyframes_from_storyboard(generation_state=self.director.get_full_state_as_dict(), progress_callback=progress_callback)
71
- self.director.update_keyframes_state(keyframes_detailed_data)
72
- final_keyframe_paths = [kf["caminho_pixel"] for kf in keyframes_detailed_data]
73
- final_state = self.director.get_full_state()
74
- logger.info("Maestro: Tarefa de Pré-Produção concluída.")
75
- return storyboard_list, final_keyframe_paths, final_state
76
-
77
- def task_produce_original_movie(self, params: ProductionParams, progress_callback: ProgressCallback = None) -> Tuple[str, List[str], GenerationState]:
78
- logger.info("Maestro: Iniciando tarefa de Produção do Filme Original.")
79
- self.director.update_parameters("producao", params)
80
- result_data = self.editor.generate_original_movie(full_generation_state=self.director.get_full_state_as_dict(), progress_callback=progress_callback)
81
- self.director.update_video_state(result_data["video_data"])
82
- final_video_path = result_data["final_path"]
83
- latent_paths = result_data["latent_paths"]
84
- final_state = self.director.get_full_state()
85
- logger.info("Maestro: Tarefa de Produção do Filme Original concluída.")
86
- return final_video_path, latent_paths, final_state
87
-
88
- # --- TAREFAS DE PÓS-PRODUÇÃO ---
89
-
90
- def task_run_latent_upscaler(self, latent_paths: List[str], chunk_size: int, progress_callback: ProgressCallback = None) -> Generator[Dict[str, Any], None, None]:
91
- if not self.director.workspace_dir: raise RuntimeError("Orchestrator não inicializado corretamente.")
92
- if not latent_paths:
93
- raise ValueError("Não é possível fazer o upscale: nenhum caminho de latente foi fornecido.")
94
 
95
- logger.info("--- ORQUESTRADOR: Tarefa de Upscaling de Latentes (Estratégia por Fragmento) ---")
96
- run_timestamp = int(time.time())
97
- temp_dir = os.path.join(self.director.workspace_dir, f"temp_upscaled_clips_{run_timestamp}")
98
- os.makedirs(temp_dir, exist_ok=True)
99
 
100
- final_upscaled_clip_paths = []
101
- total_latents = len(latent_paths)
102
 
103
- for i, latent_path in enumerate(latent_paths):
104
- if progress_callback:
105
- progress_fraction = i / total_latents
106
- progress_callback(progress_fraction, f"Processando fragmento latente {i+1}/{total_latents}")
107
-
108
- try:
109
- logger.info(f"Carregando latente: {os.path.basename(latent_path)}")
110
- latent_tensor = torch.load(latent_path, map_location=self.device)
111
-
112
- logger.info(f"Fazendo upscale do latente shape: {latent_tensor.shape}")
113
- upscaled_latent = latent_enhancer_specialist_singleton.upscale(latent_tensor)
114
- del latent_tensor; gc.collect(); torch.cuda.empty_cache()
115
-
116
- logger.info(f"Decodificando latente com upscale shape: {upscaled_latent.shape}")
117
- pixel_tensor = vae_manager_singleton.decode(upscaled_latent)
118
- del upscaled_latent; gc.collect(); torch.cuda.empty_cache()
119
-
120
- clip_filename = f"upscaled_clip_{i:04d}.mp4"
121
- current_clip_path = os.path.join(temp_dir, clip_filename)
122
- self.editor.save_video_from_tensor(pixel_tensor, current_clip_path, fps=24)
123
- final_upscaled_clip_paths.append(current_clip_path)
124
- logger.info(f"Clipe com upscale salvo em: {clip_filename}")
125
- del pixel_tensor; gc.collect(); torch.cuda.empty_cache()
126
-
127
- yield {"progress": (i + 1) / total_latents, "desc": f"Fragmento {i+1}/{total_latents} concluído."}
128
-
129
- except Exception as e:
130
- logger.error(f"Falha ao processar o fragmento latente {latent_path}: {e}", exc_info=True)
131
- continue
132
-
133
- if progress_callback: progress_callback(0.98, "Montando vídeo final com upscale...")
134
- final_video_path = os.path.join(self.director.workspace_dir, f"upscaled_movie_{run_timestamp}.mp4")
135
- video_encode_tool_singleton.concatenate_videos(final_upscaled_clip_paths, final_video_path, self.director.workspace_dir)
136
 
137
- try:
138
- shutil.rmtree(temp_dir)
139
- except OSError as e:
140
- logger.warning(f"Não foi possível remover o diretório temporário de clipes com upscale: {e}")
141
-
142
- logger.info(f"Upscaling de latentes completo! Vídeo final em: {final_video_path}")
143
- yield {"final_path": final_video_path}
144
 
145
- def task_run_hd_mastering(self, source_video_path: str, model_version: str, steps: int, prompt: str, progress_callback: ProgressCallback = None) -> Generator[Dict[str, Any], None, None]:
146
- if not self.director.workspace_dir: raise RuntimeError("Orchestrator não inicializado corretamente.")
147
- logger.info(f"--- ORQUESTRADOR: Tarefa de Masterização HD com SeedVR {model_version} ---")
148
-
149
  run_timestamp = int(time.time())
150
- output_path = os.path.join(self.director.workspace_dir, f"hd_mastered_movie_{model_version}_{run_timestamp}.mp4")
151
-
152
- final_path = seedvr_manager_singleton.process_video(
153
- input_video_path=source_video_path,
154
- output_video_path=output_path,
155
- prompt=prompt,
156
- model_version=model_version,
157
- steps=steps,
158
- progress=progress_callback
159
- )
160
- logger.info(f"Masterização HD completa! Vídeo final em: {final_path}")
161
- yield {"final_path": final_path}
162
-
163
- def task_run_audio_generation(self, source_video_path: str, audio_prompt: str, progress_callback: ProgressCallback = None) -> Generator[Dict[str, Any], None, None]:
164
- if not self.director.workspace_dir: raise RuntimeError("Orchestrator não inicializado corretamente.")
165
- logger.info(f"--- ORQUESTRADOR: Tarefa de Geração de Áudio ---")
166
-
167
- if progress_callback: progress_callback(0.1, "Preparando para geração de áudio...")
168
-
169
- run_timestamp = int(time.time())
170
- source_name = Path(source_video_path).stem
171
- output_path = os.path.join(self.director.workspace_dir, f"{source_name}_with_audio_{run_timestamp}.mp4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- result = subprocess.run(
174
- ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", source_video_path],
175
- capture_output=True, text=True, check=True
176
- )
177
- duration = float(result.stdout.strip())
178
 
179
- if progress_callback: progress_callback(0.5, "Gerando trilha de áudio...")
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- final_path = mmaudio_manager_singleton.generate_audio_for_video(
182
- video_path=source_video_path,
183
- prompt=audio_prompt,
184
- duration_seconds=duration,
185
- output_path_override=output_path
186
- )
 
187
 
188
- logger.info(f"Geração de áudio completa! Vídeo com áudio em: {final_path}")
189
- if progress_callback: progress_callback(1.0, "Geração de áudio completa!")
190
- yield {"final_path": final_path}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # aduc_framework/engineers/deformes4D.py
2
  #
3
  # Copyright (C) August 4, 2025 Carlos Rodrigues dos Santos
4
  #
5
+ # Versão 3.1.1 (Com correção de limpeza de arquivos)
6
  #
7
+ # Este engenheiro implementa a Câmera (Ψ) e o Destilador (Δ) da arquitetura
8
+ # ADUC-SDR. Sua única responsabilidade é a geração sequencial de fragmentos de
9
+ # vídeo com base em um conjunto de keyframes pré-definido.
10
 
11
+ import os
12
+ import time
13
+ import imageio
14
+ import numpy as np
15
+ import torch
16
  import logging
 
17
  from PIL import Image, ImageOps
18
+ import gc
 
19
  import shutil
20
  from pathlib import Path
21
+ from typing import List, Tuple, Dict, Any, Callable, Optional
 
 
22
 
23
+ # --- Imports Relativos Corrigidos ---
24
+ from ..types import LatentConditioningItem
25
+ from ..managers.ltx_manager import ltx_manager_singleton
26
+ from ..managers.vae_manager import vae_manager_singleton
27
+ from .deformes2D_thinker import deformes2d_thinker_singleton
28
+ from ..tools.video_encode_tool import video_encode_tool_singleton
 
 
 
 
29
 
30
  logger = logging.getLogger(__name__)
31
 
32
  ProgressCallback = Optional[Callable[[float, str], None]]
33
 
34
+ class Deformes4DEngine:
35
  """
36
+ Orquestra a geração e concatenação de fragmentos de vídeo.
 
 
37
  """
38
+ def __init__(self):
39
+ """O construtor é leve e não recebe argumentos."""
40
+ self.workspace_dir: Optional[str] = None
 
 
 
41
  self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
42
+ logger.info("Deformes4DEngine instanciado (não inicializado).")
43
+
44
+ def initialize(self, workspace_dir: str):
45
+ """Inicializa o engenheiro com as configurações necessárias."""
46
+ if self.workspace_dir is not None:
47
+ return # Evita reinicialização
48
+ self.workspace_dir = workspace_dir
49
+ os.makedirs(self.workspace_dir, exist_ok=True)
50
+ logger.info(f"Deformes4D Specialist (Executor) inicializado com workspace: {self.workspace_dir}.")
51
+
52
+ def generate_original_movie(
53
+ self,
54
+ full_generation_state: Dict[str, Any],
55
+ progress_callback: ProgressCallback = None
56
+ ) -> Dict[str, Any]:
57
+ """
58
+ Gera o filme principal lendo todos os parâmetros do estado de geração.
59
+ """
60
+ if not self.workspace_dir:
61
+ raise RuntimeError("Deformes4DEngine não foi inicializado. Chame o método initialize() antes de usar.")
62
+
63
+ # 1. Extrai todos os parâmetros do estado de geração
64
+ pre_prod_params = full_generation_state.get("parametros_geracao", {}).get("pre_producao", {})
65
+ prod_params = full_generation_state.get("parametros_geracao", {}).get("producao", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ keyframes_data = full_generation_state.get("Keyframe_atos", [])
68
+ global_prompt = full_generation_state.get("Promt_geral", "")
69
+ storyboard = [ato["resumo_ato"] for ato in full_generation_state.get("Atos", [])]
70
+ keyframe_paths = [kf["caminho_pixel"] for kf in keyframes_data]
71
 
72
+ seconds_per_fragment = pre_prod_params.get('duration_per_fragment', 4.0)
73
+ video_resolution = pre_prod_params.get('resolution', 480)
74
 
75
+ trim_percent = prod_params.get('trim_percent', 50)
76
+ handler_strength = prod_params.get('handler_strength', 0.5)
77
+ destination_convergence_strength = prod_params.get('destination_convergence_strength', 0.75)
78
+ guidance_scale = prod_params.get('guidance_scale', 2.0)
79
+ stg_scale = prod_params.get('stg_scale', 0.025)
80
+ num_inference_steps = prod_params.get('inference_steps', 20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ # 2. Inicia o processo de geração
83
+ FPS = 24
84
+ FRAMES_PER_LATENT_CHUNK = 8
85
+ LATENT_PROCESSING_CHUNK_SIZE = 4
 
 
 
86
 
 
 
 
 
87
  run_timestamp = int(time.time())
88
+ temp_latent_dir = os.path.join(self.workspace_dir, f"temp_latents_{run_timestamp}")
89
+ temp_video_clips_dir = os.path.join(self.workspace_dir, f"temp_clips_{run_timestamp}")
90
+ os.makedirs(temp_latent_dir, exist_ok=True)
91
+ os.makedirs(temp_video_clips_dir, exist_ok=True)
92
+
93
+ total_frames_brutos = self._quantize_to_multiple(int(seconds_per_fragment * FPS), FRAMES_PER_LATENT_CHUNK)
94
+ frames_a_podar = self._quantize_to_multiple(int(total_frames_brutos * (trim_percent / 100)), FRAMES_PER_LATENT_CHUNK)
95
+ latents_a_podar = frames_a_podar // FRAMES_PER_LATENT_CHUNK
96
+ DEJAVU_FRAME_TARGET = frames_a_podar - 1 if frames_a_podar > 0 else 0
97
+ DESTINATION_FRAME_TARGET = total_frames_brutos - 1
98
+
99
+ base_ltx_params = {"guidance_scale": guidance_scale, "stg_scale": stg_scale, "num_inference_steps": num_inference_steps}
100
+ story_history = ""
101
+ target_resolution_tuple = (video_resolution, video_resolution)
102
+ eco_latent_for_next_loop, dejavu_latent_for_next_loop = None, None
103
+ latent_fragment_paths = []
104
+ video_fragments_data = []
105
+
106
+ if len(keyframe_paths) < 2:
107
+ raise ValueError(f"A geração requer pelo menos 2 keyframes. Fornecidos: {len(keyframe_paths)}.")
108
+ num_transitions_to_generate = len(keyframe_paths) - 1
109
+
110
+ logger.info("--- ESTÁGIO 1: Geração de Fragmentos Latentes ---")
111
+ for i in range(num_transitions_to_generate):
112
+ fragment_index = i + 1
113
+ if progress_callback:
114
+ progress_fraction = (i / num_transitions_to_generate) * 0.7
115
+ progress_callback(progress_fraction, f"Gerando Latente {fragment_index}/{num_transitions_to_generate}")
116
+
117
+ past_keyframe_path = keyframe_paths[i - 1] if i > 0 else keyframe_paths[i]
118
+ start_keyframe_path = keyframe_paths[i]
119
+ destination_keyframe_path = keyframe_paths[i + 1]
120
+ future_story_prompt = storyboard[i + 1] if (i + 1) < len(storyboard) else "A cena final."
121
+ decision = deformes2d_thinker_singleton.get_cinematic_decision(
122
+ global_prompt, story_history, past_keyframe_path, start_keyframe_path,
123
+ destination_keyframe_path, storyboard[i - 1] if i > 0 else "O início.",
124
+ storyboard[i], future_story_prompt
125
+ )
126
+ motion_prompt = decision["motion_prompt"]
127
+ story_history += f"\n- Ato {fragment_index}: {motion_prompt}"
128
+
129
+ conditioning_items = []
130
+ if eco_latent_for_next_loop is None:
131
+ img_start = self._preprocess_image_for_latent_conversion(Image.open(start_keyframe_path).convert("RGB"), target_resolution_tuple)
132
+ conditioning_items.append(LatentConditioningItem(self._pil_to_latent(img_start), 0, 1.0))
133
+ else:
134
+ conditioning_items.append(LatentConditioningItem(eco_latent_for_next_loop, 0, 1.0))
135
+ conditioning_items.append(LatentConditioningItem(dejavu_latent_for_next_loop, DEJAVU_FRAME_TARGET, handler_strength))
136
+
137
+ img_dest = self._preprocess_image_for_latent_conversion(Image.open(destination_keyframe_path).convert("RGB"), target_resolution_tuple)
138
+ conditioning_items.append(LatentConditioningItem(self._pil_to_latent(img_dest), DESTINATION_FRAME_TARGET, destination_convergence_strength))
139
+
140
+ latents_brutos, _ = ltx_manager_singleton.generate_latent_fragment(
141
+ height=video_resolution, width=video_resolution,
142
+ conditioning_items_data=conditioning_items, motion_prompt=motion_prompt,
143
+ video_total_frames=total_frames_brutos, video_fps=FPS,
144
+ **base_ltx_params
145
+ )
146
+
147
+ last_trim = latents_brutos[:, :, -(latents_a_podar+1):, :, :].clone()
148
+ eco_latent_for_next_loop = last_trim[:, :, :2, :, :].clone()
149
+ dejavu_latent_for_next_loop = last_trim[:, :, -1:, :, :].clone()
150
+ latents_video = latents_brutos[:, :, :-(latents_a_podar-1), :, :].clone()
151
+ del last_trim, latents_brutos; gc.collect(); torch.cuda.empty_cache()
152
+
153
+ cpu_latent = latents_video.cpu()
154
+ latent_path = os.path.join(temp_latent_dir, f"latent_fragment_{i:04d}.pt")
155
+ torch.save(cpu_latent, latent_path)
156
+ latent_fragment_paths.append(latent_path)
157
+
158
+ video_fragments_data.append({"id": i, "prompt_video": motion_prompt})
159
+ del latents_video, cpu_latent; gc.collect()
160
+
161
+ del eco_latent_for_next_loop, dejavu_latent_for_next_loop; gc.collect(); torch.cuda.empty_cache()
162
+
163
+ logger.info(f"--- ESTÁGIO 2: Processando {len(latent_fragment_paths)} latentes ---")
164
+ final_video_clip_paths = []
165
+ num_chunks = -(-len(latent_fragment_paths) // LATENT_PROCESSING_CHUNK_SIZE) if LATENT_PROCESSING_CHUNK_SIZE > 0 else 0
166
+ for i in range(num_chunks):
167
+ chunk_start_index = i * LATENT_PROCESSING_CHUNK_SIZE
168
+ chunk_end_index = chunk_start_index + LATENT_PROCESSING_CHUNK_SIZE
169
+ chunk_paths = latent_fragment_paths[chunk_start_index:chunk_end_index]
170
+
171
+ if progress_callback:
172
+ progress_fraction = 0.7 + (i / num_chunks * 0.28)
173
+ progress_callback(progress_fraction, f"Processando & Decodificando Lote {i+1}/{num_chunks}")
174
 
175
+ tensors_in_chunk = [torch.load(p, map_location=self.device) for p in chunk_paths]
176
+ sub_group_latent = torch.cat(tensors_in_chunk, dim=2)
177
+ del tensors_in_chunk; gc.collect(); torch.cuda.empty_cache()
 
 
178
 
179
+ pixel_tensor = vae_manager_singleton.decode(sub_group_latent)
180
+ del sub_group_latent; gc.collect(); torch.cuda.empty_cache()
181
+
182
+ base_name = f"clip_{i:04d}_{run_timestamp}"
183
+ current_clip_path = os.path.join(temp_video_clips_dir, f"{base_name}.mp4")
184
+ self.save_video_from_tensor(pixel_tensor, current_clip_path, fps=FPS)
185
+ final_video_clip_paths.append(current_clip_path)
186
+ del pixel_tensor; gc.collect(); torch.cuda.empty_cache()
187
+
188
+ if progress_callback: progress_callback(0.98, "Montando o filme final...")
189
+ final_video_path = os.path.join(self.workspace_dir, f"original_movie_{run_timestamp}.mp4")
190
+ video_encode_tool_singleton.concatenate_videos(final_video_clip_paths, final_video_path, self.workspace_dir)
191
 
192
+ try:
193
+ shutil.rmtree(temp_video_clips_dir)
194
+ # A linha que apagava 'temp_latent_dir' foi removida para persistir os latentes.
195
+ except OSError as e:
196
+ logger.warning(f"Não foi possível remover o diretório de clipes temporários: {e}")
197
+
198
+ logger.info(f"Processo completo! Vídeo original salvo em: {final_video_path}")
199
 
200
+ final_video_data_for_state = {
201
+ "id": 0, "caminho_pixel": final_video_path,
202
+ "caminhos_latentes_fragmentos": latent_fragment_paths,
203
+ "fragmentos_componentes": video_fragments_data
204
+ }
205
+
206
+ return {
207
+ "final_path": final_video_path,
208
+ "latent_paths": latent_fragment_paths,
209
+ "video_data": final_video_data_for_state
210
+ }
211
+
212
+ # --- FUNÇÕES HELPER ---
213
+ def save_video_from_tensor(self, video_tensor: torch.Tensor, path: str, fps: int = 24):
214
+ if video_tensor is None or video_tensor.ndim != 5 or video_tensor.shape[2] == 0: return
215
+ video_tensor = video_tensor.squeeze(0).permute(1, 2, 3, 0)
216
+ video_tensor = (video_tensor.clamp(-1, 1) + 1) / 2.0
217
+ video_np = (video_tensor.detach().cpu().float().numpy() * 255).astype(np.uint8)
218
+ with imageio.get_writer(path, fps=fps, codec='libx264', quality=8, output_params=['-pix_fmt', 'yuv420p']) as writer:
219
+ for frame in video_np: writer.append_data(frame)
220
+
221
+ def _preprocess_image_for_latent_conversion(self, image: Image.Image, target_resolution: tuple) -> Image.Image:
222
+ if image.size != target_resolution:
223
+ return ImageOps.fit(image, target_resolution, Image.Resampling.LANCZOS)
224
+ return image
225
+
226
+ def _pil_to_latent(self, pil_image: Image.Image) -> torch.Tensor:
227
+ image_np = np.array(pil_image).astype(np.float32) / 255.0
228
+ tensor = torch.from_numpy(image_np).permute(2, 0, 1).unsqueeze(0).unsqueeze(2)
229
+ tensor = (tensor * 2.0) - 1.0
230
+ return vae_manager_singleton.encode(tensor)
231
+
232
+ def _quantize_to_multiple(self, n: int, m: int) -> int:
233
+ if m == 0: return n
234
+ quantized = int(round(n / m) * m)
235
+ return m if n > 0 and quantized == 0 else quantized