#!/usr/bin/env python3 import inspect import os, sys, gc, subprocess from pathlib import Path from typing import List, Optional from omegaconf import OmegaConf, open_dict # Copiado/adaptado do vincie.py: uso de HF Hub from huggingface_hub import hf_hub_download, list_repo_files, HfApi VINCIE_DIR = Path(os.getenv("VINCIE_DIR", "/app/VINCIE")) VINCE_GIT_URL = os.getenv("VINCE_GIT_URL", "https://github.com/ByteDance-Seed/VINCIE") VINCE_REPO_ID = os.getenv("VINCE_REPO_ID", "ByteDance-Seed/VINCIE-3B") VINCE_CKPT = Path(os.getenv("VINCE_CKPT", "/app/ckpt/VINCIE-3B")) if str(VINCIE_DIR) not in sys.path: sys.path.insert(0, str(VINCIE_DIR)) # inclui 'models/' relativo (mantido) try: app_models = Path("/app/models"); vincie_models = VINCIE_DIR / "models" if not app_models.exists() and vincie_models.exists(): app_models.symlink_to(vincie_models, target_is_directory=True) except Exception as e: print("[vince_server] warn: link /app/models failed:", e) class VinceServer: def __init__(self, config_path: str="/app/VINCIE/configs/generate.yaml", *, base_overrides: Optional[List[str]]=None, output_root: str="/app/outputs", chdir_repo: bool=True): self.config_path = config_path self.output_root = Path(output_root); self.output_root.mkdir(parents=True, exist_ok=True) overrides = list(base_overrides or []) self.HF_HOME = Path(os.getenv("HF_HOME", "/data/.cache/huggingface")) self.HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") or None # 1) Clonar/garantir repo self.ensure_repo() # 2) Baixar TODOS os arquivos do modelo no ckpt target self.ensure_model_all() # 3) Symlink compatível repo/ckpt/VINCIE-3B -> /app/ckpt/VINCIE-3B self._ensure_ckpt_symlink() # 4) chdir para compatibilidade com caminhos relativos if chdir_repo: try: os.chdir(str(VINCIE_DIR)) except Exception as e: print("[vince_server] warn: chdir repo failed:", e) from common.config import load_config, create_object # type: ignore # 5) Carregar e bootstrap para manter GPU quente self.config = load_config(self.config_path, overrides) self.gen = create_object(self.config) self._bootstrap_models() # ==== Helpers de bootstrap copiados/adaptados do vincie.py ==== def ensure_repo(self) -> None: """Clona o repositório oficial quando ausente (idempotente).""" if not VINCIE_DIR.exists(): subprocess.run(["git", "clone", VINCE_GIT_URL, str(VINCIE_DIR)], check=True) def ensure_model_all(self, repo_revision: Optional[str]=None) -> None: """ Baixa TODOS os arquivos do modelo do Hub (VINCE_REPO_ID) para VINCE_CKPT, preservando subdiretórios. Critério de pular download: arquivo > 1MB. Cria pastas necessárias. """ VINCE_CKPT.mkdir(parents=True, exist_ok=True) token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") # Lista recursiva de arquivos do repositório try: _ = HfApi(token=token) # instanciado para compat; não é estritamente necessário all_files: List[str] = list_repo_files( repo_id=VINCE_REPO_ID, repo_type="model", revision=repo_revision, token=token, ) except Exception as e: print(f"[vince_server] warn: list_repo_files failed for '{VINCE_REPO_ID}': {e}") all_files = [] def _need(path: Path) -> bool: try: return not (path.exists() and path.stat().st_size > 1_000_000) except FileNotFoundError: return True downloaded = 0 for rel_path in all_files: if rel_path.endswith("/"): continue dst = VINCE_CKPT / rel_path dst.parent.mkdir(parents=True, exist_ok=True) if _need(dst): try: hf_hub_download( repo_id=VINCE_REPO_ID, filename=rel_path, cache_dir=str(self.HF_HOME), local_dir=str(VINCE_CKPT), #local_dir_use_symlinks=False, token=token, #force_download=False, #local_files_only=False, #revision=repo_revision, ) downloaded += 1 except Exception as de: print(f"[vince_server] warn: download failed '{rel_path}': {de}") print(f"[vince_server] model assets checked={len(all_files)} downloaded={downloaded}") def _ensure_ckpt_symlink(self) -> None: """ Cria symlink compatível para caminhos relativos do repo: /app/VINCIE/ckpt/VINCIE-3B -> /app/ckpt/VINCIE-3B. Não remove diretórios reais; apenas substitui symlink divergente. """ repo_ckpt_dir = VINCIE_DIR / "ckpt" repo_ckpt_dir.mkdir(parents=True, exist_ok=True) link = repo_ckpt_dir / "VINCIE-3B" try: if link.is_symlink(): # Reaponta se destino for diferente try: if link.resolve() != VINCE_CKPT: link.unlink() except Exception: # Se quebrado, remove e recria link.unlink(missing_ok=True) if not link.exists(): link.symlink_to(VINCE_CKPT, target_is_directory=True) except Exception as e: print("[vince_server] warn: ensure_ckpt_symlink failed:", e) # ==== Restante: mantido do servidor original ==== def _assert_ckpt_ready(self): ckpt = VINCE_CKPT dit = ckpt / "dit.pth"; vae = ckpt / "vae.pth"; llm = ckpt / "llm14b" missing = [] if not dit.exists(): missing.append("dit.pth") if not vae.exists(): missing.append("vae.pth") if not llm.exists(): missing.append("llm14b/") if missing: raise RuntimeError(f"[vince_server] missing checkpoint assets: {', '.join(missing)}") def _bootstrap_models(self): # Fail-fast: se alguma etapa obrigatória falhar, aborta com erro explícito for name in ( "configure_persistence", "configure_models", "configure_diffusion", #"configure_sampler", ): fn = getattr(self.gen, name, None) if not callable(fn): raise RuntimeError(f"[vince_server] missing bootstrap step: {name}") fn() def _apply_generation_overrides(self, cfg, *, output_dir: Path, image_path: Optional[str]=None, prompts: Optional[List[str]]=None, cfg_scale: Optional[float]=None, aspect_ratio: Optional[str]=None, resolution: Optional[int]=None, steps: Optional[int]=None): with open_dict(cfg): cfg["generation"]["output"]["dir"] = str(output_dir) if image_path is not None: cfg["generation"]["positive_prompt"]["image_path"] = [str(image_path)] if prompts is not None: cfg["generation"]["positive_prompt"]["prompts"] = list(prompts) if cfg_scale is not None: try: cfg["diffusion"]["cfg"]["scale"] = float(cfg_scale) except Exception: pass if aspect_ratio is not None: cfg["generation"]["aspect_ratio_input"] = str(aspect_ratio) if resolution is not None: cfg["generation"]["resolution_input"] = int(resolution) cfg["generation"]["resolution"] = int(resolution) if steps is not None: self._set_steps(steps) def _set_steps(self, steps: int): try: import torch s = getattr(self.gen, "sampler", None) if s is None: return if hasattr(s, "timesteps") and isinstance(s.timesteps, (list, tuple)): total = len(s.timesteps) if steps > 0 and steps < total: idx = torch.linspace(0, total - 1, steps).round().long().tolist() s.timesteps = [s.timesteps[i] for i in idx] except Exception as e: print("[vince_server] warn: set_steps failed:", e) def _infer_once(self): infer_methods = ["inference_loop", "entrypoint", "run"] for name in infer_methods: fn = getattr(self.gen, name, None) if callable(fn): print(f"[vince_server] using inference method: {name}") # ============================================================= # CORREÇÃO APLICADA AQUI # O método de inferência (ex: inference_loop) não espera # argumentos, pois acessa a configuração através do `self` # do seu próprio objeto (`self.gen`). return fn() # ============================================================= raise RuntimeError("[vince_server] no valid inference method found") def _cleanup(self): try: import torch torch.cuda.synchronize() torch.cuda.empty_cache() if hasattr(torch.cuda, "reset_peak_memory_stats"): torch.cuda.reset_peak_memory_stats() except Exception: pass gc.collect() # ===== APIs públicas mantidas ===== def generate_multi_turn(self, input_image: str, turns: List[str], out_dir_name: Optional[str]=None, *, cfg_scale: Optional[float]=None, aspect_ratio: Optional[str]=None, resolution: Optional[int]=None, steps: Optional[int]=None) -> Path: out_dir = self.output_root / (out_dir_name or f"multi_turn_{Path(input_image).stem}") cfg = OmegaConf.load(self.config_path) self._apply_generation_overrides(cfg, output_dir=out_dir, image_path=input_image, prompts=turns, cfg_scale=cfg_scale, aspect_ratio=aspect_ratio, resolution=resolution, steps=steps) self.config = cfg result = self._infer_once() self._cleanup() return out_dir def generate_multi_concept(self, concept_images: List[str], concept_prompts: List[str], final_prompt: str, out_dir_name: Optional[str]=None, *, cfg_scale: Optional[float]=None, aspect_ratio: Optional[str]=None, resolution: Optional[int]=None, steps: Optional[int]=None, pad_placeholder: bool=False) -> Path: out_dir = self.output_root / (out_dir_name or "multi_concept") prompts_all = concept_prompts + [final_prompt] cfg = OmegaConf.load(self.config_path) with open_dict(cfg): cfg["generation"]["positive_prompt"]["image_path"] = [str(p) for p in concept_images] cfg["generation"]["positive_prompt"]["prompts"] = list(prompts_all) cfg["generation"]["pad_img_placehoder"] = bool(pad_placeholder) self._apply_generation_overrides(cfg, output_dir=out_dir, cfg_scale=cfg_scale, aspect_ratio=aspect_ratio, resolution=resolution, steps=steps) self.config = cfg result = self._infer_once() self._cleanup() return out_dir