carlex3321 commited on
Commit
08508e4
·
verified ·
1 Parent(s): 7a8b8f7

Update services/vincie.py

Browse files
Files changed (1) hide show
  1. services/vincie.py +92 -153
services/vincie.py CHANGED
@@ -1,35 +1,11 @@
1
  #!/usr/bin/env python3
2
- """
3
- VincieService - Orquestrador de Backend para o VINCIE com Suporte a Multi-GPU
4
-
5
- Responsabilidades:
6
- - Garante que o repositório VINCIE upstream está presente.
7
- - Baixa os arquivos de checkpoint (dit.pth, vae.pth) via hf_hub.
8
- - Cria um link simbólico de compatibilidade para os caminhos de checkpoint.
9
- - Executa o script main.py do VINCIE usando 'torchrun' para inferência distribuída.
10
- - Lida com a passagem de parâmetros dinâmicos da UI (GPU, Batch Size, etc.) para a linha de comando.
11
-
12
- Referência Upstream: https://github.com/ByteDance-Seed/VINCIE
13
- Desenvolvido por: [email protected] | https://github.com/carlex22
14
- Versão: 3.0.0
15
- """
16
-
17
- import os
18
- import sys
19
- import json
20
- import subprocess
21
  from pathlib import Path
22
  from typing import List, Optional
23
-
24
- from huggingface_hub import hf_hub_download
25
-
26
 
27
  class VincieService:
28
- """
29
- Serviço de alto nível para preparar os ativos de tempo de execução do VINCIE e invocar a geração,
30
- com foco em maximizar o desempenho em hardware multi-GPU.
31
- """
32
-
33
  def __init__(
34
  self,
35
  repo_dir: str = "/app/VINCIE",
@@ -37,161 +13,124 @@ class VincieService:
37
  python_bin: str = "python3",
38
  repo_id: str = "ByteDance-Seed/VINCIE-3B",
39
  ):
40
- """
41
- Inicializa o serviço com caminhos e configurações de tempo de execução.
42
- """
43
  self.repo_dir = Path(repo_dir)
44
  self.ckpt_dir = Path(ckpt_dir)
45
  self.python = python_bin
46
  self.repo_id = repo_id
47
-
48
- # Caminhos para os diferentes arquivos de configuração
49
  self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
50
- self.generate_yaml_distributed = self.repo_dir / "configs" / "generate_distributed.yaml"
51
-
52
  self.output_root = Path("/app/outputs")
53
  self.output_root.mkdir(parents=True, exist_ok=True)
54
-
55
  (self.repo_dir / "ckpt").mkdir(parents=True, exist_ok=True)
56
 
57
- # ---------- Funções de Configuração (Setup) ----------
58
-
59
  def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
60
- """Clona o repositório oficial do VINCIE, se ausente."""
61
  if not self.repo_dir.exists():
62
- print(f"Clonando o repositório VINCIE de {git_url}...")
63
- # Clona com --depth 1 para baixar apenas a versão mais recente
64
  subprocess.run(["git", "clone", "--depth", "1", git_url, str(self.repo_dir)], check=True)
65
 
 
 
 
 
66
  def ensure_model(self, hf_token: Optional[str] = None) -> None:
67
- """Baixa os arquivos de checkpoint e cria o link simbólico necessário."""
68
  self.ckpt_dir.mkdir(parents=True, exist_ok=True)
69
  token = hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
70
 
71
- def _need_download(p: Path) -> bool:
72
- try: return not (p.exists() and p.stat().st_size > 1_000_000)
73
- except FileNotFoundError: return True
74
-
75
- for fname in ["dit.pth", "vae.pth"]:
76
- dst = self.ckpt_dir / fname
77
- if _need_download(dst):
78
- print(f"Baixando {fname} de {self.repo_id}...")
79
- hf_hub_download(repo_id=self.repo_id, filename=fname, local_dir=str(self.ckpt_dir), token=token)
80
-
81
- link_path = self.repo_dir / "ckpt" / "VINCIE-3B"
82
- if not link_path.exists():
83
- try:
84
- if link_path.is_symlink(): link_path.unlink()
85
- link_path.symlink_to(self.ckpt_dir, target_is_directory=True)
86
- except Exception as e:
87
- print(f"Aviso: falha ao criar o link simbólico do checkpoint: {e}")
88
-
89
- # ---------- Executor Principal com Suporte a Multi-GPU ----------
90
-
91
- def _run_vincie(self, overrides: List[str], work_output: Path, video_mode: bool = False, num_gpus: int = 1) -> None:
92
- """
93
- Invoca o main.py do VINCIE, usando torchrun para execução distribuída se num_gpus > 1.
94
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  work_output.mkdir(parents=True, exist_ok=True)
96
-
97
- # O modo distribuído usa um script de geração diferente
98
- config_file = self.generate_yaml_distributed if num_gpus > 1 else self.generate_yaml
99
- entry_script = "generate_distributed.py" if num_gpus > 1 else "generate.py"
100
-
101
- base_cmd = [
102
  "main.py",
103
- str(config_file),
104
  *overrides,
105
  f"generation.output.dir={str(work_output)}",
106
  ]
 
 
 
 
 
107
 
108
- if num_gpus > 1:
109
- cmd = ["torchrun", f"--nproc_per_node={num_gpus}", *base_cmd]
110
- else:
111
- cmd = [self.python, *base_cmd]
112
-
113
- env = os.environ.copy()
114
- if video_mode:
115
- env["VINCIE_GENERATE_VIDEO"] = "1"
116
- elif "VINCIE_GENERATE_VIDEO" in env:
117
- del env["VINCIE_GENERATE_VIDEO"]
118
-
119
- print(f"Executando comando: {' '.join(cmd)}")
120
- subprocess.run(cmd, cwd=self.repo_dir, check=True, env=env)
121
-
122
- # ---------- Pipelines de Geração ----------
123
-
124
  def multi_turn_edit(self, input_image: str, turns: List[str], **kwargs) -> Path:
125
- """Executa a pipeline de edição multi-turno com configurações dinâmicas e suporte a multi-GPU."""
126
- num_gpus = kwargs.get('num_gpus', 1)
127
- batch_size = kwargs.get('batch_size', 1) if num_gpus == 1 else num_gpus
128
-
129
- out_dir = self.output_root / f"multi_turn_{self._slug(input_image)}"
130
-
131
  overrides = [
132
- f"generation.positive_prompt.image_path={json.dumps([str(input_image)])}",
133
  f"generation.positive_prompt.prompts={json.dumps(turns)}",
134
- f"generation.seed={kwargs.get('seed', 1)}",
135
- f"diffusion.timesteps.sampling.steps={kwargs.get('steps', 50)}",
136
- f"diffusion.cfg.scale={kwargs.get('cfg_scale', 7.5)}",
137
- f"generation.negative_prompt={json.dumps(kwargs.get('negative_prompt', ''))}",
138
- f"generation.resolution={kwargs.get('resolution', 512)}",
139
- f"generation.batch_size={batch_size}",
140
  ]
141
-
142
- if kwargs.get('use_vae_slicing', True):
143
- overrides.extend(["vae.slicing.split_size=1", "vae.slicing.memory_device=same"])
144
-
145
- self._run_vincie(overrides, out_dir, video_mode=False, num_gpus=num_gpus)
146
  return out_dir
147
 
148
- def text_to_video(self, input_image: str, prompt: str, **kwargs) -> Path:
149
- """Executa a pipeline de texto-para-vídeo com configurações dinâmicas e suporte a multi-GPU."""
150
- num_gpus = kwargs.get('num_gpus', 1)
151
- batch_size = kwargs.get('batch_size', 1) if num_gpus == 1 else num_gpus
152
-
153
- out_dir = self.output_root / f"txt2vid_{self._slug(prompt)}"
154
-
155
  overrides = [
156
- f"generation.positive_prompt.image_path={json.dumps([str(input_image)])}",
157
- f"generation.positive_prompt.prompts={json.dumps([prompt])}",
158
- f"generation.seed={kwargs.get('seed', 1)}",
159
- f"diffusion.timesteps.sampling.steps={kwargs.get('steps', 50)}",
160
- f"diffusion.cfg.scale={kwargs.get('cfg_scale', 7.5)}",
161
- f"generation.negative_prompt={json.dumps(kwargs.get('negative_prompt', ''))}",
162
- f"generation.resolution={kwargs.get('resolution', 512)}",
163
- f"generation.fps={kwargs.get('fps', 2)}",
164
- f"generation.batch_size={batch_size}",
165
  ]
166
-
167
- if kwargs.get('use_vae_slicing', True):
168
- overrides.extend(["vae.slicing.split_size=1", "vae.slicing.memory_device=same"])
169
-
170
- self._run_vincie(overrides, out_dir, video_mode=True, num_gpus=num_gpus)
171
  return out_dir
172
-
173
- def multi_concept_compose(self, concept_images: List[str], concept_prompts: List[str], final_prompt: str) -> Path:
174
- """Executa a pipeline de composição multi-conceito."""
175
- out_dir = self.output_root / "multi_concept"
176
- prompts_all = concept_prompts + [final_prompt]
177
-
178
- overrides = [
179
- f"generation.positive_prompt.image_path={json.dumps([str(p) for p in concept_images])}",
180
- f"generation.positive_prompt.prompts={json.dumps(prompts_all)}",
181
- "generation.pad_img_placehoder=False",
182
- ]
183
-
184
- # O modo multi-conceito é mais complexo e pode não escalar bem com FSDP da mesma forma.
185
- # Por padrão, vamos mantê-lo em uma única GPU para garantir estabilidade.
186
- # Pode ser adaptado para multi-GPU se necessário, mas requer testes mais aprofundados.
187
- self._run_vincie(overrides, out_dir, video_mode=False, num_gpus=1)
188
- return out_dir
189
-
190
- # ---------- Helper ----------
191
-
192
- @staticmethod
193
- def _slug(path_or_text: str) -> str:
194
- """Produz um nome curto e seguro para o sistema de arquivos."""
195
- base_name = Path(path_or_text).stem if Path(path_or_text).exists() else str(path_or_text)
196
- safe_name = "".join(c if c.isalnum() or c in "-_." else "_" for c in base_name)
197
- return safe_name[:64]
 
1
  #!/usr/bin/env python3
2
+ import os, sys, json, subprocess
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from pathlib import Path
4
  from typing import List, Optional
5
+ from time import time, sleep
6
+ from huggingface_hub import snapshot_download
 
7
 
8
  class VincieService:
 
 
 
 
 
9
  def __init__(
10
  self,
11
  repo_dir: str = "/app/VINCIE",
 
13
  python_bin: str = "python3",
14
  repo_id: str = "ByteDance-Seed/VINCIE-3B",
15
  ):
 
 
 
16
  self.repo_dir = Path(repo_dir)
17
  self.ckpt_dir = Path(ckpt_dir)
18
  self.python = python_bin
19
  self.repo_id = repo_id
 
 
20
  self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
 
 
21
  self.output_root = Path("/app/outputs")
22
  self.output_root.mkdir(parents=True, exist_ok=True)
 
23
  (self.repo_dir / "ckpt").mkdir(parents=True, exist_ok=True)
24
 
25
+ # ---------- Repo ----------
 
26
  def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
 
27
  if not self.repo_dir.exists():
 
 
28
  subprocess.run(["git", "clone", "--depth", "1", git_url, str(self.repo_dir)], check=True)
29
 
30
+ # ---------- Model ----------
31
+ def _have_core_files(self) -> bool:
32
+ return (self.ckpt_dir / "dit.pth").exists() and (self.ckpt_dir / "vae.pth").exists()
33
+
34
  def ensure_model(self, hf_token: Optional[str] = None) -> None:
 
35
  self.ckpt_dir.mkdir(parents=True, exist_ok=True)
36
  token = hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
37
 
38
+ # Reutiliza cache persistente se HF_HOME estiver definido
39
+ hf_home = Path(os.environ.get("HF_HOME", "/app/.cache/huggingface"))
40
+ cache_dir = str(hf_home / "hub")
41
+ os.makedirs(cache_dir, exist_ok=True)
42
+
43
+ need_full = True
44
+ if any(self.ckpt_dir.iterdir()):
45
+ need_full = not self._have_core_files()
46
+
47
+ if need_full:
48
+ print(f"[vince] snapshot_download {self.repo_id} -> {self.ckpt_dir} (cache_dir={cache_dir})")
49
+ snapshot_download(
50
+ repo_id=self.repo_id,
51
+ cache_dir=cache_dir,
52
+ local_dir=str(self.ckpt_dir),
53
+ # local_dir_use_symlinks está deprecado; removível em versões futuras do hub
54
+ local_dir_use_symlinks=False,
55
+ resume_download=True,
56
+ token=token,
57
+ )
58
+ else:
59
+ print("[vince] modelo já presente; pulando download")
60
+
61
+ if not self._have_core_files():
62
+ raise FileNotFoundError("Modelo baixado, mas dit.pth/vae.pth não encontrados em ckpt_dir")
63
+
64
+ # Symlink de compatibilidade: /app/VINCIE/ckpt/VINCIE-3B -> /app/ckpt/VINCIE-3B
65
+ link = self.repo_dir / "ckpt" / "VINCIE-3B"
66
+ try:
67
+ if link.is_symlink() or link.exists():
68
+ try:
69
+ link.unlink()
70
+ except IsADirectoryError:
71
+ pass
72
+ if not link.exists():
73
+ link.symlink_to(self.ckpt_dir, target_is_directory=True)
74
+ except Exception as e:
75
+ print("[vince] symlink warning:", e)
76
+
77
+ def ready(self) -> bool:
78
+ have_repo = self.repo_dir.exists() and self.generate_yaml.exists()
79
+ return bool(have_repo and self._have_core_files())
80
+
81
+ # ---------- Execução ----------
82
+ def _wait_until_outputs(self, out_dir: Path, timeout_s: int = 300) -> None:
83
+ exts = (".png", ".jpg", ".jpeg", ".gif", ".mp4")
84
+ deadline = time() + timeout_s
85
+ while time() < deadline:
86
+ if any(p.is_file() and p.suffix.lower() in exts for p in out_dir.rglob("*")):
87
+ print(f"[vince] outputs detected in {out_dir}")
88
+ return
89
+ sleep(1)
90
+ print(f"[vince] warning: no outputs detected in {out_dir} within {timeout_s}s")
91
+
92
+ def _run_vincie(self, overrides: List[str], work_output: Path, wait_outputs: bool = True) -> None:
93
  work_output.mkdir(parents=True, exist_ok=True)
94
+ cmd = [
95
+ self.python,
 
 
 
 
96
  "main.py",
97
+ str(self.generate_yaml),
98
  *overrides,
99
  f"generation.output.dir={str(work_output)}",
100
  ]
101
+ print("[vince] CWD=", self.repo_dir)
102
+ print("[vince] CMD=", " ".join(cmd))
103
+ subprocess.run(cmd, cwd=self.repo_dir, check=True, env=os.environ.copy())
104
+ if wait_outputs:
105
+ self._wait_until_outputs(work_output, timeout_s=int(os.getenv("VINCIE_WAIT_OUTPUTS_SEC", "300")))
106
 
107
+ # ---------- Pipelines ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  def multi_turn_edit(self, input_image: str, turns: List[str], **kwargs) -> Path:
109
+ out_dir = self.output_root / f"multi_turn_{Path(input_image).stem}"
 
 
 
 
 
110
  overrides = [
111
+ f'generation.positive_prompt.image_path="{str(input_image)}"',
112
  f"generation.positive_prompt.prompts={json.dumps(turns)}",
113
+ f"generation.seed={int(kwargs.get('seed', 1))}",
114
+ f"diffusion.timesteps.sampling.steps={int(kwargs.get('steps', 50))}",
115
+ f"diffusion.cfg.scale={float(kwargs.get('cfg_scale', 7.5))}",
116
+ f'generation.negative_prompt="{kwargs.get("negative_prompt","")}"',
117
+ f"generation.resolution={int(kwargs.get('resolution', 512))}",
118
+ f"generation.batch_size={int(kwargs.get('batch_size', 1))}",
119
  ]
120
+ self._run_vincie(overrides, out_dir, wait_outputs=True)
 
 
 
 
121
  return out_dir
122
 
123
+ def multi_concept_compose(self, files: List[str], descs: List[str], final_prompt: str, **kwargs) -> Path:
124
+ out_dir = self.output_root / f"multi_concept_{len(files)}"
 
 
 
 
 
125
  overrides = [
126
+ f"generation.concepts.files={json.dumps(files)}",
127
+ f"generation.concepts.descs={json.dumps(descs)}",
128
+ f'generation.final_prompt="{final_prompt}"',
129
+ f"generation.seed={int(kwargs.get('seed', 1))}",
130
+ f"diffusion.timesteps.sampling.steps={int(kwargs.get('steps', 50))}",
131
+ f"diffusion.cfg.scale={float(kwargs.get('cfg_scale', 7.5))}",
132
+ f"generation.resolution={int(kwargs.get('resolution', 512))}",
133
+ f"generation.batch_size={int(kwargs.get('batch_size', 1))}",
 
134
  ]
135
+ self._run_vincie(overrides, out_dir, wait_outputs=True)
 
 
 
 
136
  return out_dir