Update aduc_framework/managers/seedvr_manager.py
Browse files
aduc_framework/managers/seedvr_manager.py
CHANGED
|
@@ -2,13 +2,11 @@
|
|
| 2 |
#
|
| 3 |
# Copyright (C) 2025 Carlos Rodrigues dos Santos
|
| 4 |
#
|
| 5 |
-
# Version:
|
| 6 |
#
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
# application root. It also handles the pip installation of the Apex dependency.
|
| 11 |
-
# This ensures that the SeedVR code runs in the exact file structure it expects.
|
| 12 |
|
| 13 |
import torch
|
| 14 |
import torch.distributed as dist
|
|
@@ -17,6 +15,7 @@ import gc
|
|
| 17 |
import logging
|
| 18 |
import sys
|
| 19 |
import subprocess
|
|
|
|
| 20 |
from pathlib import Path
|
| 21 |
from urllib.parse import urlparse
|
| 22 |
from torch.hub import download_url_to_file
|
|
@@ -25,6 +24,10 @@ import mediapy
|
|
| 25 |
from einops import rearrange
|
| 26 |
import shutil
|
| 27 |
from omegaconf import OmegaConf
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
|
|
@@ -34,200 +37,289 @@ DEPS_DIR = APP_ROOT / "deps"
|
|
| 34 |
SEEDVR_SPACE_DIR = DEPS_DIR / "SeedVR_Space"
|
| 35 |
SEEDVR_SPACE_URL = "https://huggingface.co/spaces/ByteDance-Seed/SeedVR2-3B"
|
| 36 |
|
| 37 |
-
class
|
| 38 |
-
|
| 39 |
-
|
|
|
|
| 40 |
self.runner = None
|
| 41 |
-
self.workspace_dir = workspace_dir
|
| 42 |
self.is_initialized = False
|
| 43 |
-
self.
|
| 44 |
-
|
| 45 |
-
logger.info("SeedVrManager initialized. Setup will run on first use.")
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
Executa
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
| 53 |
|
| 54 |
-
logger.info("---
|
| 55 |
|
| 56 |
-
# 1. Clonar o repositório
|
| 57 |
if not SEEDVR_SPACE_DIR.exists():
|
| 58 |
-
logger.info(f"
|
| 59 |
DEPS_DIR.mkdir(exist_ok=True, parents=True)
|
| 60 |
-
subprocess.run(
|
| 61 |
-
["git", "clone", "--depth", "1", SEEDVR_SPACE_URL, str(SEEDVR_SPACE_DIR)],
|
| 62 |
-
check=True, capture_output=True, text=True
|
| 63 |
-
)
|
| 64 |
|
| 65 |
-
# 2. Copiar as pastas necessárias
|
| 66 |
required_dirs = ["projects", "common", "models", "configs_3b", "configs_7b"]
|
| 67 |
for dirname in required_dirs:
|
| 68 |
source = SEEDVR_SPACE_DIR / dirname
|
| 69 |
target = APP_ROOT / dirname
|
| 70 |
if not target.exists():
|
| 71 |
-
logger.info(f"
|
| 72 |
shutil.copytree(source, target)
|
| 73 |
|
| 74 |
-
# 3. Adicionar a raiz ao sys.path
|
| 75 |
if str(APP_ROOT) not in sys.path:
|
| 76 |
sys.path.insert(0, str(APP_ROOT))
|
| 77 |
-
logger.info(f"
|
| 78 |
|
| 79 |
-
# 4. Instalar
|
| 80 |
try:
|
| 81 |
import apex
|
| 82 |
-
logger.info("Apex is already installed.")
|
| 83 |
except ImportError:
|
| 84 |
-
logger.info("
|
| 85 |
apex_url = 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/apex-0.1-cp310-cp310-linux_x86_64.whl'
|
| 86 |
apex_wheel_path = _load_file_from_url(url=apex_url, model_dir=str(DEPS_DIR))
|
| 87 |
subprocess.run(f"pip install {apex_wheel_path}", check=True, shell=True)
|
| 88 |
-
logger.info("Apex installed successfully.")
|
| 89 |
|
| 90 |
-
# 5. Baixar os modelos
|
| 91 |
ckpt_dir = APP_ROOT / 'ckpts'
|
| 92 |
ckpt_dir.mkdir(exist_ok=True)
|
| 93 |
-
|
| 94 |
'vae': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/ema_vae.pth',
|
| 95 |
'dit_3b': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/seedvr2_ema_3b.pth',
|
| 96 |
-
|
| 97 |
'pos_emb': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/pos_emb.pt',
|
| 98 |
'neg_emb': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/neg_emb.pt'
|
| 99 |
}
|
| 100 |
-
for name, url in
|
| 101 |
_load_file_from_url(url=url, model_dir=str(ckpt_dir))
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
logger.info("---
|
|
|
|
| 105 |
|
| 106 |
-
def
|
|
|
|
| 107 |
if self.runner is not None: return
|
| 108 |
|
| 109 |
-
# Garante que todo o ambiente está configurado antes de prosseguir
|
| 110 |
-
self._full_setup()
|
| 111 |
-
|
| 112 |
# Agora que o setup está feito, podemos importar os módulos
|
| 113 |
from projects.video_diffusion_sr.infer import VideoDiffusionInfer
|
| 114 |
from common.config import load_config
|
| 115 |
-
from common.seed import set_seed
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
#if model_version == '3B':
|
| 127 |
-
config_path = APP_ROOT / 'configs_3b' / 'main.yaml'
|
| 128 |
-
checkpoint_path = APP_ROOT / 'ckpts' / 'seedvr2_ema_3b.pth'
|
| 129 |
-
#else: # Assumimos 7B
|
| 130 |
-
# config_path = APP_ROOT / 'configs_7b' / 'main.yaml'
|
| 131 |
-
# checkpoint_path = APP_ROOT / 'ckpts' / 'seedvr2_ema_7b.pth'
|
| 132 |
|
| 133 |
config = load_config(str(config_path))
|
| 134 |
|
| 135 |
self.runner = VideoDiffusionInfer(config)
|
| 136 |
OmegaConf.set_readonly(self.runner.config, False)
|
| 137 |
|
| 138 |
-
self.runner.configure_dit_model(device=self.device, checkpoint=str(checkpoint_path))
|
| 139 |
self.runner.configure_vae_model()
|
| 140 |
|
| 141 |
-
if hasattr(self.runner.vae, "set_memory_limit"):
|
| 142 |
-
self.runner.vae.set_memory_limit(**self.runner.config.vae.memory_limit)
|
| 143 |
-
|
| 144 |
self.is_initialized = True
|
| 145 |
-
logger.info(f"Runner
|
| 146 |
|
| 147 |
-
def
|
|
|
|
| 148 |
if self.runner is not None:
|
|
|
|
|
|
|
|
|
|
| 149 |
del self.runner
|
| 150 |
self.runner = None
|
| 151 |
gc.collect()
|
| 152 |
torch.cuda.empty_cache()
|
| 153 |
self.is_initialized = False
|
| 154 |
-
logger.info("Runner do SeedVR2 descarregado da VRAM.")
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
def process_video(self, input_video_path: str, output_video_path: str, prompt: str,
|
| 160 |
-
model_version: str = '
|
| 161 |
progress: gr.Progress = None) -> str:
|
|
|
|
| 162 |
try:
|
| 163 |
-
self.
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
video_tensor = read_video(input_video_path, output_format="TCHW")[0] / 255.0
|
| 179 |
-
res_h, res_w = video_tensor.shape[-2:]
|
| 180 |
-
video_transform = Compose([
|
| 181 |
-
NaResize(resolution=(res_h * res_w) ** 0.5, mode="area", downsample_only=False),
|
| 182 |
-
Lambda(lambda x: torch.clamp(x, 0.0, 1.0)),
|
| 183 |
-
DivisibleCrop((16, 16)),
|
| 184 |
-
Normalize(0.5, 0.5),
|
| 185 |
-
Rearrange("t c h w -> c t h w"),
|
| 186 |
-
])
|
| 187 |
-
cond_latents = [video_transform(video_tensor.to(self.device))]
|
| 188 |
-
input_videos = cond_latents
|
| 189 |
-
self.runner.dit.to("cpu")
|
| 190 |
-
self.runner.vae.to(self.device)
|
| 191 |
-
cond_latents = self.runner.vae_encode(cond_latents)
|
| 192 |
-
self.runner.vae.to("cpu"); gc.collect(); torch.cuda.empty_cache()
|
| 193 |
-
self.runner.dit.to(self.device)
|
| 194 |
-
|
| 195 |
-
pos_emb = torch.load(APP_ROOT / 'pos_emb.pt').to(self.device)
|
| 196 |
-
neg_emb = torch.load(APP_ROOT / 'neg_emb.pt').to(self.device)
|
| 197 |
-
text_embeds_dict = {"texts_pos": [pos_emb], "texts_neg": [neg_emb]}
|
| 198 |
-
|
| 199 |
-
noises = [torch.randn_like(latent) for latent in cond_latents]
|
| 200 |
-
conditions = [self.runner.get_condition(noise, latent_blur=latent, task="sr") for noise, latent in zip(noises, cond_latents)]
|
| 201 |
-
|
| 202 |
-
with torch.no_grad(), torch.autocast("cuda", torch.bfloat16, enabled=True):
|
| 203 |
-
video_tensors = self.runner.inference(noises=noises, conditions=conditions, dit_offload=True, **text_embeds_dict)
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
samples = self.runner.vae_decode(video_tensors)
|
| 208 |
-
final_sample = samples[0]
|
| 209 |
-
input_video_sample = input_videos[0]
|
| 210 |
-
if final_sample.shape[1] < input_video_sample.shape[1]:
|
| 211 |
-
input_video_sample = input_video_sample[:, :final_sample.shape[1]]
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
final_sample = final_sample.clip(-1, 1).mul_(0.5).add_(0.5).mul_(255).round()
|
| 216 |
-
final_sample_np = final_sample.to(torch.uint8).cpu().numpy()
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
finally:
|
| 222 |
-
|
|
|
|
| 223 |
|
| 224 |
def _load_file_from_url(url, model_dir='./', file_name=None):
|
| 225 |
os.makedirs(model_dir, exist_ok=True)
|
| 226 |
filename = file_name or os.path.basename(urlparse(url).path)
|
| 227 |
cached_file = os.path.abspath(os.path.join(model_dir, filename))
|
| 228 |
if not os.path.exists(cached_file):
|
| 229 |
-
logger.info(f'
|
| 230 |
download_url_to_file(url, cached_file, hash_prefix=None, progress=True)
|
| 231 |
return cached_file
|
| 232 |
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
#
|
| 3 |
# Copyright (C) 2025 Carlos Rodrigues dos Santos
|
| 4 |
#
|
| 5 |
+
# Version: 5.0.0 (GPU Pool Manager)
|
| 6 |
#
|
| 7 |
+
# Esta versão transforma o SeedVrManager em um Pool Manager, similar ao LTX,
|
| 8 |
+
# para gerenciar múltiplos workers em GPUs dedicadas e fazer rodízio,
|
| 9 |
+
# otimizando o uso de VRAM e o tempo de carregamento/descarregamento.
|
|
|
|
|
|
|
| 10 |
|
| 11 |
import torch
|
| 12 |
import torch.distributed as dist
|
|
|
|
| 15 |
import logging
|
| 16 |
import sys
|
| 17 |
import subprocess
|
| 18 |
+
import threading
|
| 19 |
from pathlib import Path
|
| 20 |
from urllib.parse import urlparse
|
| 21 |
from torch.hub import download_url_to_file
|
|
|
|
| 24 |
from einops import rearrange
|
| 25 |
import shutil
|
| 26 |
from omegaconf import OmegaConf
|
| 27 |
+
import yaml
|
| 28 |
+
|
| 29 |
+
# Imports relativos para o hardware_manager
|
| 30 |
+
from ..tools.hardware_manager import hardware_manager
|
| 31 |
|
| 32 |
logger = logging.getLogger(__name__)
|
| 33 |
|
|
|
|
| 37 |
SEEDVR_SPACE_DIR = DEPS_DIR / "SeedVR_Space"
|
| 38 |
SEEDVR_SPACE_URL = "https://huggingface.co/spaces/ByteDance-Seed/SeedVR2-3B"
|
| 39 |
|
| 40 |
+
class SeedVrWorker:
|
| 41 |
+
"""Representa uma única instância do pipeline SeedVR em um dispositivo."""
|
| 42 |
+
def __init__(self, device_id: str):
|
| 43 |
+
self.device = torch.device(device_id)
|
| 44 |
self.runner = None
|
|
|
|
| 45 |
self.is_initialized = False
|
| 46 |
+
self.setup_complete = self._check_and_run_global_setup()
|
| 47 |
+
logger.info(f"SeedVR Worker inicializado para o dispositivo {self.device}.")
|
|
|
|
| 48 |
|
| 49 |
+
@staticmethod
|
| 50 |
+
def _check_and_run_global_setup():
|
| 51 |
+
"""Executa o setup de arquivos uma única vez para toda a aplicação."""
|
| 52 |
+
# Usa um arquivo de flag para garantir que o setup só rode uma vez.
|
| 53 |
+
setup_flag = DEPS_DIR / "seedvr.setup.complete"
|
| 54 |
+
if setup_flag.exists():
|
| 55 |
+
return True
|
| 56 |
|
| 57 |
+
logger.info("--- Iniciando Setup Global do SeedVR (primeira execução) ---")
|
| 58 |
|
| 59 |
+
# 1. Clonar o repositório
|
| 60 |
if not SEEDVR_SPACE_DIR.exists():
|
| 61 |
+
logger.info(f"Clonando repositório SeedVR Space para {SEEDVR_SPACE_DIR}...")
|
| 62 |
DEPS_DIR.mkdir(exist_ok=True, parents=True)
|
| 63 |
+
subprocess.run(["git", "clone", "--depth", "1", SEEDVR_SPACE_URL, str(SEEDVR_SPACE_DIR)], check=True)
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
# 2. Copiar as pastas necessárias
|
| 66 |
required_dirs = ["projects", "common", "models", "configs_3b", "configs_7b"]
|
| 67 |
for dirname in required_dirs:
|
| 68 |
source = SEEDVR_SPACE_DIR / dirname
|
| 69 |
target = APP_ROOT / dirname
|
| 70 |
if not target.exists():
|
| 71 |
+
logger.info(f"Copiando '{dirname}' para a raiz da aplicação...")
|
| 72 |
shutil.copytree(source, target)
|
| 73 |
|
| 74 |
+
# 3. Adicionar a raiz ao sys.path
|
| 75 |
if str(APP_ROOT) not in sys.path:
|
| 76 |
sys.path.insert(0, str(APP_ROOT))
|
| 77 |
+
logger.info(f"Adicionado '{APP_ROOT}' ao sys.path.")
|
| 78 |
|
| 79 |
+
# 4. Instalar Apex
|
| 80 |
try:
|
| 81 |
import apex
|
|
|
|
| 82 |
except ImportError:
|
| 83 |
+
logger.info("Instalando dependência Apex...")
|
| 84 |
apex_url = 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/apex-0.1-cp310-cp310-linux_x86_64.whl'
|
| 85 |
apex_wheel_path = _load_file_from_url(url=apex_url, model_dir=str(DEPS_DIR))
|
| 86 |
subprocess.run(f"pip install {apex_wheel_path}", check=True, shell=True)
|
|
|
|
| 87 |
|
| 88 |
+
# 5. Baixar os modelos
|
| 89 |
ckpt_dir = APP_ROOT / 'ckpts'
|
| 90 |
ckpt_dir.mkdir(exist_ok=True)
|
| 91 |
+
model_urls = {
|
| 92 |
'vae': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/ema_vae.pth',
|
| 93 |
'dit_3b': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/seedvr2_ema_3b.pth',
|
| 94 |
+
'dit_7b': 'https://huggingface.co/ByteDance-Seed/SeedVR2-7B/resolve/main/seedvr2_ema_7b.pth',
|
| 95 |
'pos_emb': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/pos_emb.pt',
|
| 96 |
'neg_emb': 'https://huggingface.co/ByteDance-Seed/SeedVR2-3B/resolve/main/neg_emb.pt'
|
| 97 |
}
|
| 98 |
+
for name, url in model_urls.items():
|
| 99 |
_load_file_from_url(url=url, model_dir=str(ckpt_dir))
|
| 100 |
+
|
| 101 |
+
setup_flag.touch() # Cria o arquivo de flag
|
| 102 |
+
logger.info("--- Setup Global do SeedVR Concluído ---")
|
| 103 |
+
return True
|
| 104 |
|
| 105 |
+
def initialize_runner(self, model_version: str):
|
| 106 |
+
"""Carrega os modelos para a VRAM do dispositivo deste worker."""
|
| 107 |
if self.runner is not None: return
|
| 108 |
|
|
|
|
|
|
|
|
|
|
| 109 |
# Agora que o setup está feito, podemos importar os módulos
|
| 110 |
from projects.video_diffusion_sr.infer import VideoDiffusionInfer
|
| 111 |
from common.config import load_config
|
|
|
|
| 112 |
|
| 113 |
+
# O torch.distributed é complicado em multi-GPU, vamos desativá-lo para SeedVR
|
| 114 |
+
# já que cada worker é independente.
|
| 115 |
+
logger.info(f"Worker {self.device}: Inicializando runner SeedVR2 {model_version}...")
|
| 116 |
+
if model_version == '3B':
|
| 117 |
+
config_path = APP_ROOT / 'configs_3b' / 'main.yaml'
|
| 118 |
+
checkpoint_path = APP_ROOT / 'ckpts' / 'seedvr2_ema_3b.pth'
|
| 119 |
+
else: # 7B
|
| 120 |
+
config_path = APP_ROOT / 'configs_7b' / 'main.yaml'
|
| 121 |
+
checkpoint_path = APP_ROOT / 'ckpts' / 'seedvr2_ema_7b.pth'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
config = load_config(str(config_path))
|
| 124 |
|
| 125 |
self.runner = VideoDiffusionInfer(config)
|
| 126 |
OmegaConf.set_readonly(self.runner.config, False)
|
| 127 |
|
| 128 |
+
self.runner.configure_dit_model(device=str(self.device), checkpoint=str(checkpoint_path))
|
| 129 |
self.runner.configure_vae_model()
|
| 130 |
|
|
|
|
|
|
|
|
|
|
| 131 |
self.is_initialized = True
|
| 132 |
+
logger.info(f"Worker {self.device}: Runner para SeedVR2 {model_version} inicializado e pronto na VRAM.")
|
| 133 |
|
| 134 |
+
def unload_runner(self):
|
| 135 |
+
"""Descarrega os modelos da VRAM."""
|
| 136 |
if self.runner is not None:
|
| 137 |
+
# Mova todos os componentes para a CPU antes de deletar
|
| 138 |
+
self.runner.dit.to('cpu')
|
| 139 |
+
self.runner.vae.to('cpu')
|
| 140 |
del self.runner
|
| 141 |
self.runner = None
|
| 142 |
gc.collect()
|
| 143 |
torch.cuda.empty_cache()
|
| 144 |
self.is_initialized = False
|
| 145 |
+
logger.info(f"Worker {self.device}: Runner do SeedVR2 descarregado da VRAM.")
|
| 146 |
+
|
| 147 |
+
def process_video_internal(self, *args, **kwargs):
|
| 148 |
+
# A lógica de processamento de vídeo em si.
|
| 149 |
+
# Precisamos importar aqui, pois o sys.path é modificado no setup
|
| 150 |
+
from common.seed import set_seed
|
| 151 |
+
from data.image.transforms.divisible_crop import DivisibleCrop
|
| 152 |
+
from data.image.transforms.na_resize import NaResize
|
| 153 |
+
from data.video.transforms.rearrange import Rearrange
|
| 154 |
+
from projects.video_diffusion_sr.color_fix import wavelet_reconstruction
|
| 155 |
+
from torchvision.transforms import Compose, Lambda, Normalize
|
| 156 |
+
from torchvision.io.video import read_video
|
| 157 |
+
|
| 158 |
+
input_video_path, output_video_path, prompt, model_version, steps, seed = args
|
| 159 |
+
|
| 160 |
+
set_seed(seed, same_across_ranks=True)
|
| 161 |
+
self.runner.config.diffusion.timesteps.sampling.steps = steps
|
| 162 |
+
self.runner.configure_diffusion()
|
| 163 |
+
|
| 164 |
+
video_tensor = read_video(input_video_path, output_format="TCHW")[0] / 255.0
|
| 165 |
+
res_h, res_w = video_tensor.shape[-2:]
|
| 166 |
+
video_transform = Compose([
|
| 167 |
+
NaResize(resolution=(res_h * res_w) ** 0.5, mode="area", downsample_only=False),
|
| 168 |
+
Lambda(lambda x: torch.clamp(x, 0.0, 1.0)),
|
| 169 |
+
DivisibleCrop((16, 16)),
|
| 170 |
+
Normalize(0.5, 0.5),
|
| 171 |
+
Rearrange("t c h w -> c t h w"),
|
| 172 |
+
])
|
| 173 |
+
cond_latents = [video_transform(video_tensor.to(self.device))]
|
| 174 |
+
input_videos = cond_latents
|
| 175 |
+
self.runner.dit.to("cpu")
|
| 176 |
+
self.runner.vae.to(self.device)
|
| 177 |
+
cond_latents = self.runner.vae_encode(cond_latents)
|
| 178 |
+
self.runner.vae.to("cpu"); gc.collect(); torch.cuda.empty_cache()
|
| 179 |
+
self.runner.dit.to(self.device)
|
| 180 |
+
|
| 181 |
+
pos_emb = torch.load(APP_ROOT / 'ckpts' / 'pos_emb.pt').to(self.device)
|
| 182 |
+
neg_emb = torch.load(APP_ROOT / 'ckpts' / 'neg_emb.pt').to(self.device)
|
| 183 |
+
text_embeds_dict = {"texts_pos": [pos_emb], "texts_neg": [neg_emb]}
|
| 184 |
+
|
| 185 |
+
noises = [torch.randn_like(latent) for latent in cond_latents]
|
| 186 |
+
conditions = [self.runner.get_condition(noise, latent_blur=latent, task="sr") for noise, latent in zip(noises, cond_latents)]
|
| 187 |
+
|
| 188 |
+
with torch.no_grad(), torch.autocast("cuda", torch.bfloat16, enabled=True):
|
| 189 |
+
video_tensors = self.runner.inference(noises=noises, conditions=conditions, dit_offload=True, **text_embeds_dict)
|
| 190 |
+
|
| 191 |
+
self.runner.dit.to("cpu"); gc.collect(); torch.cuda.empty_cache()
|
| 192 |
+
self.runner.vae.to(self.device)
|
| 193 |
+
samples = self.runner.vae_decode(video_tensors)
|
| 194 |
+
final_sample = samples[0]
|
| 195 |
+
input_video_sample = input_videos[0]
|
| 196 |
+
if final_sample.shape[1] < input_video_sample.shape[1]:
|
| 197 |
+
input_video_sample = input_video_sample[:, :final_sample.shape[1]]
|
| 198 |
+
|
| 199 |
+
final_sample = wavelet_reconstruction(rearrange(final_sample, "c t h w -> t c h w"), rearrange(input_video_sample, "c t h w -> t c h w"))
|
| 200 |
+
final_sample = rearrange(final_sample, "t c h w -> t h w c")
|
| 201 |
+
final_sample = final_sample.clip(-1, 1).mul_(0.5).add_(0.5).mul_(255).round()
|
| 202 |
+
final_sample_np = final_sample.to(torch.uint8).cpu().numpy()
|
| 203 |
+
|
| 204 |
+
mediapy.write_video(output_video_path, final_sample_np, fps=24)
|
| 205 |
+
return output_video_path
|
| 206 |
+
|
| 207 |
+
class SeedVrPoolManager:
|
| 208 |
+
def __init__(self, device_ids: list[str]):
|
| 209 |
+
logger.info(f"SEEDVR POOL MANAGER: Criando workers para os dispositivos: {device_ids}")
|
| 210 |
+
if not device_ids or device_ids == ['cpu']:
|
| 211 |
+
raise ValueError("SeedVrPoolManager requer GPUs dedicadas. Verifique a configuração.")
|
| 212 |
+
self.workers = [SeedVrWorker(device_id) for device_id in device_ids]
|
| 213 |
+
self.current_worker_index = 0
|
| 214 |
+
self.lock = threading.Lock()
|
| 215 |
+
self.last_cleanup_thread = None
|
| 216 |
+
|
| 217 |
+
def _cleanup_worker_thread(self, worker: SeedVrWorker):
|
| 218 |
+
"""Thread para descarregar o worker em segundo plano."""
|
| 219 |
+
logger.info(f"SEEDVR CLEANUP THREAD: Iniciando limpeza de {worker.device} em background...")
|
| 220 |
+
worker.unload_runner()
|
| 221 |
|
| 222 |
def process_video(self, input_video_path: str, output_video_path: str, prompt: str,
|
| 223 |
+
model_version: str = '7B', steps: int = 100, seed: int = 666,
|
| 224 |
progress: gr.Progress = None) -> str:
|
| 225 |
+
worker_to_use = None
|
| 226 |
try:
|
| 227 |
+
with self.lock:
|
| 228 |
+
# Espera a limpeza do worker anterior terminar, se houver
|
| 229 |
+
if self.last_cleanup_thread and self.last_cleanup_thread.is_alive():
|
| 230 |
+
logger.info("Aguardando a thread de limpeza anterior terminar...")
|
| 231 |
+
self.last_cleanup_thread.join()
|
| 232 |
+
|
| 233 |
+
# Seleciona o worker atual e o próximo a ser limpo
|
| 234 |
+
worker_to_use = self.workers[self.current_worker_index]
|
| 235 |
+
previous_worker_index = (self.current_worker_index - 1 + len(self.workers)) % len(self.workers)
|
| 236 |
+
worker_to_cleanup = self.workers[previous_worker_index]
|
| 237 |
+
|
| 238 |
+
# Inicia a limpeza do worker anterior em uma nova thread
|
| 239 |
+
cleanup_thread = threading.Thread(target=self._cleanup_worker_thread, args=(worker_to_cleanup,))
|
| 240 |
+
cleanup_thread.start()
|
| 241 |
+
self.last_cleanup_thread = cleanup_thread
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
+
# Carrega o modelo no worker atual
|
| 244 |
+
worker_to_use.initialize_runner(model_version)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
# Avança o índice para o próximo rodízio
|
| 247 |
+
self.current_worker_index = (self.current_worker_index + 1) % len(self.workers)
|
|
|
|
|
|
|
| 248 |
|
| 249 |
+
logger.info(f"SEEDVR POOL MANAGER: Processando vídeo em {worker_to_use.device}...")
|
| 250 |
+
return worker_to_use.process_video_internal(
|
| 251 |
+
input_video_path, output_video_path, prompt, model_version, steps, seed
|
| 252 |
+
)
|
| 253 |
+
except Exception as e:
|
| 254 |
+
logger.error(f"SEEDVR POOL MANAGER: Erro durante o processamento de vídeo: {e}", exc_info=True)
|
| 255 |
+
raise e
|
| 256 |
finally:
|
| 257 |
+
# A limpeza agora é feita pela thread, não mais aqui.
|
| 258 |
+
pass
|
| 259 |
|
| 260 |
def _load_file_from_url(url, model_dir='./', file_name=None):
|
| 261 |
os.makedirs(model_dir, exist_ok=True)
|
| 262 |
filename = file_name or os.path.basename(urlparse(url).path)
|
| 263 |
cached_file = os.path.abspath(os.path.join(model_dir, filename))
|
| 264 |
if not os.path.exists(cached_file):
|
| 265 |
+
logger.info(f'Baixando: "{url}" para {cached_file}')
|
| 266 |
download_url_to_file(url, cached_file, hash_prefix=None, progress=True)
|
| 267 |
return cached_file
|
| 268 |
|
| 269 |
+
# --- Instanciação Singleton ---
|
| 270 |
+
with open("config.yaml", 'r') as f:
|
| 271 |
+
config = yaml.safe_load(f)
|
| 272 |
+
|
| 273 |
+
seedvr_gpus_required = config['specialists'].get('seedvr', {}).get('gpus_required', 0)
|
| 274 |
+
seedvr_device_ids = hardware_manager.allocate_gpus('SeedVR', seedvr_gpus_required)
|
| 275 |
+
|
| 276 |
+
# Verificação crucial para garantir que não tente instanciar um pool sem GPUs
|
| 277 |
+
if seedvr_gpus_required > 0 and 'cpu' not in seedvr_device_ids:
|
| 278 |
+
seedvr_manager_singleton = SeedVrPoolManager(device_ids=seedvr_device_ids)
|
| 279 |
+
logger.info("Especialista de Masterização HD (SeedVR Pool) pronto.")
|
| 280 |
+
else:
|
| 281 |
+
# Se nenhuma GPU for alocada, criamos um placeholder para evitar erros.
|
| 282 |
+
class SeedVrPlaceholder:
|
| 283 |
+
def process_video(self, *args, **kwargs):
|
| 284 |
+
logger.error("SeedVR não foi inicializado pois nenhuma GPU foi alocada na config.yaml. Pulando etapa de masterização.")
|
| 285 |
+
# Retorna o vídeo de entrada para não quebrar o fluxo
|
| 286 |
+
return kwargs.get('input_video_path') or args[0]
|
| 287 |
+
|
| 288 |
+
seedvr_manager_singleton = SeedVrPlaceholder()
|
| 289 |
+
logger.warning("SeedVR Pool Manager não foi inicializado. Nenhuma GPU foi requisitada na config.yaml.")```
|
| 290 |
+
|
| 291 |
+
### Passo 3: Ajustar o `AducOrchestrator`
|
| 292 |
+
|
| 293 |
+
Agora, o `orchestrator` não precisa mais se preocupar em descarregar os modelos LTX, pois eles estão em GPUs diferentes do SeedVR. A chamada para a masterização HD se torna mais limpa.
|
| 294 |
+
|
| 295 |
+
No arquivo `aduc_framework/orchestrator.py`:
|
| 296 |
+
|
| 297 |
+
```python
|
| 298 |
+
# Em aduc_framework/orchestrator.py
|
| 299 |
+
|
| 300 |
+
# ... (outros imports)
|
| 301 |
+
from .managers.seedvr_manager import seedvr_manager_singleton # Garanta que está importando o novo singleton
|
| 302 |
+
|
| 303 |
+
class AducOrchestrator:
|
| 304 |
+
# ... (outras funções)
|
| 305 |
+
|
| 306 |
+
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]:
|
| 307 |
+
if not self.director.workspace_dir: raise RuntimeError("Orchestrator não inicializado corretamente.")
|
| 308 |
+
logger.info(f"--- ORQUESTRADOR: Tarefa de Masterização HD com SeedVR {model_version} ---")
|
| 309 |
+
|
| 310 |
+
# <<< --- REMOVA O BLOCO DE DESCARREGAMENTO DE MODELOS DAQUI --- >>>
|
| 311 |
+
# Não precisamos mais descarregar LTX e VAE, pois estão em GPUs diferentes.
|
| 312 |
+
|
| 313 |
+
run_timestamp = int(time.time())
|
| 314 |
+
output_path = os.path.join(self.director.workspace_dir, f"hd_mastered_movie_{model_version}_{run_timestamp}.mp4")
|
| 315 |
+
|
| 316 |
+
# A chamada para process_video agora usa o Pool Manager, que gerencia o rodízio internamente.
|
| 317 |
+
final_path = seedvr_manager_singleton.process_video(
|
| 318 |
+
input_video_path=source_video_path,
|
| 319 |
+
output_video_path=output_path,
|
| 320 |
+
prompt=prompt,
|
| 321 |
+
model_version=model_version,
|
| 322 |
+
steps=steps
|
| 323 |
+
)
|
| 324 |
+
logger.info(f"Masterização HD completa! Vídeo final em: {final_path}")
|
| 325 |
+
yield {"final_path": final_path}
|