carlex3321 commited on
Commit
63fecbf
·
verified ·
1 Parent(s): 8e139e3

Update services/vincie.py

Browse files
Files changed (1) hide show
  1. services/vincie.py +174 -189
services/vincie.py CHANGED
@@ -1,17 +1,16 @@
1
  #!/usr/bin/env python3
 
2
  """
3
- VincieService
4
- - Ensures the upstream VINCIE repository is present.
5
- - Fetches the minimal checkpoint files (dit.pth, vae.pth) via hf_hub_download into /app/ckpt/VINCIE-3B.
6
- - Creates a compatibility symlink /app/VINCIE/ckpt/VINCIE-3B -> /app/ckpt/VINCIE-3B for repo-relative paths.
7
- - Runs the official VINCIE main.py with Hydra/YACS overrides for both multi-turn and multi-concept generation.
8
- - Optionally injects a minimal 'apex.normalization' shim when NVIDIA Apex is not available (to avoid import errors).
9
- Upstream reference: https://github.com/ByteDance-Seed/VINCIE
10
 
11
- Developed by carlex22@gmail.com
12
- https://github.com/carlex22
 
 
13
 
14
- Version 1.0.0
 
 
15
  """
16
 
17
  import os
@@ -21,24 +20,24 @@ 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
- High-level service for preparing VINCIE runtime assets and invoking generation.
30
-
31
- Responsibilities:
32
- - Repository management: clone the official VINCIE repository when missing.
33
- - Checkpoint management: download dit.pth and vae.pth from the VINCIE-3B checkpoint on the Hub.
34
- - Path compatibility: ensure /app/VINCIE/ckpt/VINCIE-3B points to /app/ckpt/VINCIE-3B.
35
- - Runners: execute main.py with generate.yaml overrides for multi-turn edits and multi-concept composition.
36
- - Apex shim: provide a minimal fallback for apex.normalization if Apex isn’t installed.
37
-
38
- Defaults assume the Docker/container layout used by the Space:
39
- - Repository directory: /app/VINCIE
40
- - Checkpoint directory: /app/ckpt/VINCIE-3B
41
- - Output root: /app/outputs
42
  """
43
 
44
  def __init__(
@@ -46,165 +45,154 @@ class VincieService:
46
  repo_dir: str = "/app/VINCIE",
47
  ckpt_dir: str = "/app/ckpt/VINCIE-3B",
48
  python_bin: str = "python",
49
- repo_id: str = "ByteDance-Seed/VINCIE-3B",
50
  ):
51
  """
52
- Initialize the service with paths and runtime settings.
53
-
54
  Args:
55
- repo_dir: Filesystem location of the upstream VINCIE repository clone.
56
- ckpt_dir: Filesystem location where dit.pth and vae.pth are stored.
57
- python_bin: Python executable to invoke for main.py (e.g., 'python' or a full path).
58
- repo_id: Hugging Face Hub repo id for the VINCIE-3B checkpoint.
59
-
60
- Side-effects:
61
- - Ensures the output root directory exists.
62
- - Ensures the repo ckpt/ directory exists (for symlink placement).
63
  """
64
  self.repo_dir = Path(repo_dir)
65
  self.ckpt_dir = Path(ckpt_dir)
66
  self.python = python_bin
67
- self.repo_id = repo_id
68
-
69
- # Canonical config and paths within the upstream repo
70
  self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
71
  self.assets_dir = self.repo_dir / "assets"
72
-
73
- # Output root for generated media
74
  self.output_root = Path("/app/outputs")
75
  self.output_root.mkdir(parents=True, exist_ok=True)
76
-
77
- # Ensure ckpt/ exists in the repo (symlink target lives here)
78
  (self.repo_dir / "ckpt").mkdir(parents=True, exist_ok=True)
79
 
80
  # ---------- Setup ----------
81
 
82
  def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
83
  """
84
- Clone the official VINCIE repository when missing.
85
-
86
  Args:
87
- git_url: Source URL of the official VINCIE repo.
88
-
89
  Raises:
90
- subprocess.CalledProcessError on git clone failure.
91
  """
92
  if not self.repo_dir.exists():
 
93
  subprocess.run(["git", "clone", git_url, str(self.repo_dir)], check=True)
94
-
95
- def ensure_model(self, hf_token: Optional[str] = None) -> None:
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
- Download the minimal VINCIE-3B checkpoint files if missing and create a repo-compatible symlink.
98
-
99
- Files fetched from the Hub (repo_id):
100
- - dit.pth
101
- - vae.pth
102
-
103
- The files are placed under self.ckpt_dir (default /app/ckpt/VINCIE-3B) and a symlink
104
- /app/VINCIE/ckpt/VINCIE-3B -> /app/ckpt/VINCIE-3B is created to match upstream relative paths.
105
-
106
  Args:
107
- hf_token: Optional Hugging Face token; defaults to env HF_TOKEN or HUGGINGFACE_TOKEN.
108
-
109
- Notes:
110
- - Uses hf_hub_download with local_dir, so files are placed directly in the target directory.
111
- - A basic size check (> 1MB) is used to decide whether to refetch a file.
 
 
112
  """
113
- self.ckpt_dir.mkdir(parents=True, exist_ok=True)
114
  token = hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
115
-
116
- def _need(p: Path) -> bool:
117
- try:
118
- return not (p.exists() and p.stat().st_size > 1_000_000)
119
- except FileNotFoundError:
120
- return True
121
-
122
- for fname in ["dit.pth", "vae.pth"]:
123
- dst = self.ckpt_dir / fname
124
- if _need(dst):
125
- print(f"Downloading {fname} from {self.repo_id} ...")
126
- hf_hub_download(
127
- repo_id=self.repo_id,
128
- filename=fname,
129
- local_dir=str(self.ckpt_dir),
130
- local_dir_use_symlinks=False,
131
- token=token,
132
- force_download=False,
133
- local_files_only=False,
134
- )
135
-
136
- # Compatibility symlink for repo-relative ckpt paths
137
- link = self.repo_dir / "ckpt" / "VINCIE-3B"
138
  try:
139
- if link.is_symlink() or link.exists():
140
- try:
141
- link.unlink()
142
- except IsADirectoryError:
143
- # If a directory sits at that path, we leave it as-is or replace as needed
144
- pass
145
- if not link.exists():
146
- link.symlink_to(self.ckpt_dir, target_is_directory=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  except Exception as e:
148
- print("Warning: failed to create checkpoint symlink:", e)
149
-
150
- def ensure_apex(self, enable_shim: bool = True) -> None:
151
- """
152
- Ensure apex.normalization importability.
153
-
154
- If NVIDIA Apex is not installed, and enable_shim=True, inject a minimal shim implementing:
155
- - FusedRMSNorm via torch.nn.RMSNorm
156
- - FusedLayerNorm via torch.nn.LayerNorm
157
-
158
- This prevents import-time failures in code that references apex.normalization while
159
- sacrificing any Apex-specific kernel benefits.
160
-
161
- Args:
162
- enable_shim: Whether to install a local shim when 'apex.normalization' is missing.
163
- """
164
- try:
165
- import importlib
166
- importlib.import_module("apex.normalization")
167
- return
168
- except Exception:
169
- if not enable_shim:
170
- return
171
-
172
- shim_root = Path("/app/shims")
173
- apex_pkg = shim_root / "apex"
174
- apex_pkg.mkdir(parents=True, exist_ok=True)
175
-
176
- (apex_pkg / "__init__.py").write_text("from .normalization import *\n")
177
- (apex_pkg / "normalization.py").write_text(
178
- "import torch\n"
179
- "import torch.nn as nn\n"
180
- "\n"
181
- "class FusedRMSNorm(nn.Module):\n"
182
- " def __init__(self, normalized_shape, eps=1e-6, elementwise_affine=True):\n"
183
- " super().__init__()\n"
184
- " self.mod = nn.RMSNorm(normalized_shape, eps=eps, elementwise_affine=elementwise_affine)\n"
185
- " def forward(self, x):\n"
186
- " return self.mod(x)\n"
187
- "\n"
188
- "class FusedLayerNorm(nn.Module):\n"
189
- " def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):\n"
190
- " super().__init__()\n"
191
- " self.mod = nn.LayerNorm(normalized_shape, eps=eps, elementwise_affine=elementwise_affine)\n"
192
- " def forward(self, x):\n"
193
- " return self.mod(x)\n"
194
- )
195
-
196
- # Make shim importable in this process and child processes
197
- sys.path.insert(0, str(shim_root))
198
- os.environ["PYTHONPATH"] = f"{str(shim_root)}:{os.environ.get('PYTHONPATH','')}"
199
 
200
  def ready(self) -> bool:
201
  """
202
- Quick readiness probe for UI:
203
- - The repository and generate.yaml exist.
204
- - Minimal checkpoint files (dit.pth, vae.pth) exist.
205
-
206
  Returns:
207
- True if the environment is ready to run generation tasks; otherwise False.
208
  """
209
  have_repo = self.repo_dir.exists() and self.generate_yaml.exists()
210
  dit_ok = (self.ckpt_dir / "dit.pth").exists()
@@ -215,16 +203,17 @@ class VincieService:
215
 
216
  def _run_vincie(self, overrides: List[str], work_output: Path) -> None:
217
  """
218
- Invoke VINCIE's main.py with Hydra/YACS overrides inside the upstream repo directory.
219
-
220
  Args:
221
- overrides: A list of CLI overrides (e.g., generation.positive_prompt.*).
222
- work_output: Output directory path for generated assets.
223
-
224
  Raises:
225
- subprocess.CalledProcessError if the underlying process fails.
226
  """
227
  work_output.mkdir(parents=True, exist_ok=True)
 
228
  cmd = [
229
  self.python,
230
  "main.py",
@@ -232,6 +221,8 @@ class VincieService:
232
  *overrides,
233
  f"generation.output.dir={str(work_output)}",
234
  ]
 
 
235
  env = os.environ.copy()
236
  subprocess.run(cmd, cwd=self.repo_dir, check=True, env=env)
237
 
@@ -244,29 +235,27 @@ class VincieService:
244
  out_dir_name: Optional[str] = None,
245
  ) -> Path:
246
  """
247
- Run the official 'multi-turn' generation equivalent.
248
-
249
- This wraps generate.yaml using overrides:
250
- - generation.positive_prompt.image_path = [ "<input-image-path>" ]
251
- - generation.positive_prompt.prompts = [ "<turn1>", "<turn2>", ... ]
252
-
253
  Args:
254
- input_image: Path to the single input image on disk.
255
- turns: A list of editing instructions, in the order they should be applied.
256
- out_dir_name: Optional name for the output subdirectory; auto-generated if omitted.
257
-
258
  Returns:
259
- Path to the output directory containing images and, if produced, a video.
260
  """
261
  out_dir = self.output_root / (out_dir_name or f"multi_turn_{self._slug(input_image)}")
 
262
  image_json = json.dumps([str(input_image)])
263
  prompts_json = json.dumps(turns)
264
-
265
  overrides = [
266
  f"generation.positive_prompt.image_path={image_json}",
267
  f"generation.positive_prompt.prompts={prompts_json}",
268
  f"ckpt.path={str(self.ckpt_dir)}",
269
  ]
 
270
  self._run_vincie(overrides, out_dir)
271
  return out_dir
272
 
@@ -280,34 +269,30 @@ class VincieService:
280
  out_dir_name: Optional[str] = None,
281
  ) -> Path:
282
  """
283
- Run the 'multi-concept' composition pipeline.
284
-
285
- The service forms:
286
- - generation.positive_prompt.image_path = [ <concept-img-1>, ..., <concept-img-N> ]
287
- - generation.positive_prompt.prompts = [ <desc-1>, ..., <desc-N>, <final-prompt> ]
288
- - generation.pad_img_placehoder = False (preserves input shapes)
289
- - ckpt.path = /app/ckpt/VINCIE-3B (by default)
290
-
291
  Args:
292
- concept_images: Paths to concept images on disk.
293
- concept_prompts: Per-image descriptions in the same order as concept_images.
294
- final_prompt: Composition prompt appended after all per-image descriptions.
295
- out_dir_name: Optional name for the output subdirectory; defaults to 'multi_concept'.
296
-
297
  Returns:
298
- Path to the output directory containing images and, if produced, a video.
299
  """
300
  out_dir = self.output_root / (out_dir_name or "multi_concept")
 
301
  imgs_json = json.dumps([str(p) for p in concept_images])
302
  prompts_all = concept_prompts + [final_prompt]
303
  prompts_json = json.dumps(prompts_all)
304
-
305
  overrides = [
306
  f"generation.positive_prompt.image_path={imgs_json}",
307
  f"generation.positive_prompt.prompts={prompts_json}",
308
  "generation.pad_img_placehoder=False",
309
  f"ckpt.path={str(self.ckpt_dir)}",
310
  ]
 
311
  self._run_vincie(overrides, out_dir)
312
  return out_dir
313
 
@@ -316,13 +301,13 @@ class VincieService:
316
  @staticmethod
317
  def _slug(path_or_text: str) -> str:
318
  """
319
- Produce a filesystem-friendly short name (max 64 chars) from a path or text.
320
-
321
  Args:
322
- path_or_text: An input path or arbitrary string.
323
-
324
  Returns:
325
- A sanitized string consisting of [A-Za-z0-9._-] with non-matching chars converted to underscores.
326
  """
327
  p = Path(path_or_text)
328
  base = p.stem if p.exists() else str(path_or_text)
 
1
  #!/usr/bin/env python3
2
+
3
  """
4
+ VincieService - VINCIE-3B High-Performance Service
 
 
 
 
 
 
5
 
6
+ - Ensures upstream VINCIE repository is present.
7
+ - Downloads checkpoint using HF structured cache (persistent, no duplication).
8
+ - Creates compatibility symlink for repo-relative paths.
9
+ - Runs official VINCIE main.py with Hydra/YACS overrides for multi-turn and multi-concept.
10
 
11
+ Upstream: https://github.com/ByteDance-Seed/VINCIE
12
+ Developed by: [email protected] | https://github.com/carlex22
13
+ Version: 2.0.0 (Cache-optimized for 8× L40S)
14
  """
15
 
16
  import os
 
20
  from pathlib import Path
21
  from typing import List, Optional
22
 
23
+ from huggingface_hub import snapshot_download
24
 
25
 
26
  class VincieService:
27
  """
28
+ High-level service for VINCIE runtime with persistent cache support.
29
+
30
+ Optimizations:
31
+ - Uses HF_HUB_CACHE structured cache (no file duplication)
32
+ - Persistent across container restarts when /data volume is mounted
33
+ - Parallel downloads with max_workers
34
+ - Resume capability for interrupted downloads
35
+
36
+ Paths (Docker/container layout):
37
+ - Repository: /app/VINCIE
38
+ - Cache: $HF_HUB_CACHE/models--ByteDance-Seed--VINCIE-3B/
39
+ - Symlink: /app/ckpt/VINCIE-3B -> cached snapshot
40
+ - Output: /app/outputs
41
  """
42
 
43
  def __init__(
 
45
  repo_dir: str = "/app/VINCIE",
46
  ckpt_dir: str = "/app/ckpt/VINCIE-3B",
47
  python_bin: str = "python",
48
+ model_repo: str = "ByteDance-Seed/VINCIE-3B",
49
  ):
50
  """
51
+ Initialize service with paths and runtime settings.
52
+
53
  Args:
54
+ repo_dir: VINCIE repository clone location
55
+ ckpt_dir: Checkpoint symlink location (points to cache)
56
+ python_bin: Python executable for main.py
57
+ model_repo: HuggingFace Hub repo ID for VINCIE-3B
 
 
 
 
58
  """
59
  self.repo_dir = Path(repo_dir)
60
  self.ckpt_dir = Path(ckpt_dir)
61
  self.python = python_bin
62
+ self.model_repo = model_repo
63
+
64
+ # Config paths within upstream repo
65
  self.generate_yaml = self.repo_dir / "configs" / "generate.yaml"
66
  self.assets_dir = self.repo_dir / "assets"
67
+
68
+ # Output root
69
  self.output_root = Path("/app/outputs")
70
  self.output_root.mkdir(parents=True, exist_ok=True)
71
+
72
+ # Ensure repo ckpt/ dir exists (for symlink)
73
  (self.repo_dir / "ckpt").mkdir(parents=True, exist_ok=True)
74
 
75
  # ---------- Setup ----------
76
 
77
  def ensure_repo(self, git_url: str = "https://github.com/ByteDance-Seed/VINCIE") -> None:
78
  """
79
+ Clone official VINCIE repository if missing.
80
+
81
  Args:
82
+ git_url: Source URL of VINCIE repo
83
+
84
  Raises:
85
+ subprocess.CalledProcessError: On git clone failure
86
  """
87
  if not self.repo_dir.exists():
88
+ print(f"[VincieService] Cloning {git_url} to {self.repo_dir}")
89
  subprocess.run(["git", "clone", git_url, str(self.repo_dir)], check=True)
90
+ print("[VincieService] Clone complete")
91
+ else:
92
+ print(f"[VincieService] Repository exists: {self.repo_dir}")
93
+
94
+ # Validate main.py exists
95
+ main_py = self.repo_dir / "main.py"
96
+ if not main_py.exists():
97
+ raise FileNotFoundError(
98
+ f"main.py not found in {self.repo_dir}. "
99
+ f"Repository may be incomplete or corrupted."
100
+ )
101
+
102
+ def ensure_model(self, hf_token: Optional[str] = None) -> Path:
103
  """
104
+ Download VINCIE-3B checkpoint to structured HF cache (persistent).
105
+
106
+ Uses snapshot_download with cache_dir (not local_dir) to:
107
+ - Store files once in $HF_HUB_CACHE/models--ByteDance-Seed--VINCIE-3B/
108
+ - Create symlink at self.ckpt_dir pointing to cached snapshot
109
+ - Enable automatic deduplication and resume across restarts
110
+
 
 
111
  Args:
112
+ hf_token: Optional HF token (defaults to env HF_TOKEN)
113
+
114
+ Returns:
115
+ Path to the cached snapshot directory
116
+
117
+ Raises:
118
+ Exception: On download or validation failure
119
  """
 
120
  token = hf_token or os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
121
+ cache_dir = os.environ.get("HF_HUB_CACHE",
122
+ os.path.expanduser("~/.cache/huggingface/hub"))
123
+
124
+ print(f"[VincieService] Ensuring model in cache: {cache_dir}")
125
+ print(f"[VincieService] Model repo: {self.model_repo}")
126
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  try:
128
+ # Download to structured cache (persistent, deduplicated)
129
+ model_path = snapshot_download(
130
+ repo_id=self.model_repo,
131
+ cache_dir=cache_dir,
132
+ token=token,
133
+ resume_download=True, # Resume interrupted downloads
134
+ max_workers=8, # Parallel downloads
135
+ allow_patterns=["*.pth", "*.json", "*.txt", "*.yaml"], # Skip unused files
136
+ )
137
+
138
+ print(f"[VincieService] Model cached at: {model_path}")
139
+
140
+ # Create/update symlink for compatibility
141
+ self.ckpt_dir.parent.mkdir(parents=True, exist_ok=True)
142
+
143
+ if self.ckpt_dir.is_symlink():
144
+ self.ckpt_dir.unlink()
145
+ elif self.ckpt_dir.exists():
146
+ # If it's a directory with old files, remove it
147
+ import shutil
148
+ shutil.rmtree(self.ckpt_dir)
149
+
150
+ self.ckpt_dir.symlink_to(model_path, target_is_directory=True)
151
+ print(f"[VincieService] Symlink: {self.ckpt_dir} -> {model_path}")
152
+
153
+ # Validate critical files
154
+ dit_pth = Path(model_path) / "dit.pth"
155
+ vae_pth = Path(model_path) / "vae.pth"
156
+
157
+ if not dit_pth.exists():
158
+ raise FileNotFoundError(f"dit.pth missing in {model_path}")
159
+ if not vae_pth.exists():
160
+ raise FileNotFoundError(f"vae.pth missing in {model_path}")
161
+
162
+ dit_size = dit_pth.stat().st_size / (1024**3) # GB
163
+ vae_size = vae_pth.stat().st_size / (1024**3)
164
+ print(f"[VincieService] Checkpoint sizes: dit.pth={dit_size:.2f}GB, vae.pth={vae_size:.2f}GB")
165
+
166
+ # Create repo-relative symlink
167
+ repo_link = self.repo_dir / "ckpt" / "VINCIE-3B"
168
+ try:
169
+ if repo_link.is_symlink() or repo_link.exists():
170
+ try:
171
+ repo_link.unlink()
172
+ except IsADirectoryError:
173
+ import shutil
174
+ shutil.rmtree(repo_link)
175
+
176
+ if not repo_link.exists():
177
+ repo_link.symlink_to(self.ckpt_dir, target_is_directory=True)
178
+ print(f"[VincieService] Repo symlink: {repo_link} -> {self.ckpt_dir}")
179
+ except Exception as e:
180
+ print(f"[VincieService] Warning: repo symlink failed: {e}")
181
+
182
+ return Path(model_path)
183
+
184
  except Exception as e:
185
+ print(f"[VincieService] Model download error: {e}")
186
+ import traceback
187
+ traceback.print_exc()
188
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  def ready(self) -> bool:
191
  """
192
+ Readiness check for UI/health probes.
193
+
 
 
194
  Returns:
195
+ True if repo and checkpoints are ready
196
  """
197
  have_repo = self.repo_dir.exists() and self.generate_yaml.exists()
198
  dit_ok = (self.ckpt_dir / "dit.pth").exists()
 
203
 
204
  def _run_vincie(self, overrides: List[str], work_output: Path) -> None:
205
  """
206
+ Invoke VINCIE main.py with Hydra/YACS overrides.
207
+
208
  Args:
209
+ overrides: CLI overrides list
210
+ work_output: Output directory for generated assets
211
+
212
  Raises:
213
+ subprocess.CalledProcessError: If main.py fails
214
  """
215
  work_output.mkdir(parents=True, exist_ok=True)
216
+
217
  cmd = [
218
  self.python,
219
  "main.py",
 
221
  *overrides,
222
  f"generation.output.dir={str(work_output)}",
223
  ]
224
+
225
+ print(f"[VincieService] Running: {' '.join(cmd)}")
226
  env = os.environ.copy()
227
  subprocess.run(cmd, cwd=self.repo_dir, check=True, env=env)
228
 
 
235
  out_dir_name: Optional[str] = None,
236
  ) -> Path:
237
  """
238
+ Run multi-turn image editing pipeline.
239
+
 
 
 
 
240
  Args:
241
+ input_image: Path to input image
242
+ turns: List of editing instructions (applied sequentially)
243
+ out_dir_name: Optional output dir name
244
+
245
  Returns:
246
+ Path to output directory with results
247
  """
248
  out_dir = self.output_root / (out_dir_name or f"multi_turn_{self._slug(input_image)}")
249
+
250
  image_json = json.dumps([str(input_image)])
251
  prompts_json = json.dumps(turns)
252
+
253
  overrides = [
254
  f"generation.positive_prompt.image_path={image_json}",
255
  f"generation.positive_prompt.prompts={prompts_json}",
256
  f"ckpt.path={str(self.ckpt_dir)}",
257
  ]
258
+
259
  self._run_vincie(overrides, out_dir)
260
  return out_dir
261
 
 
269
  out_dir_name: Optional[str] = None,
270
  ) -> Path:
271
  """
272
+ Run multi-concept composition pipeline.
273
+
 
 
 
 
 
 
274
  Args:
275
+ concept_images: List of concept image paths
276
+ concept_prompts: Per-image descriptions (same order)
277
+ final_prompt: Final composition prompt
278
+ out_dir_name: Optional output dir name
279
+
280
  Returns:
281
+ Path to output directory with results
282
  """
283
  out_dir = self.output_root / (out_dir_name or "multi_concept")
284
+
285
  imgs_json = json.dumps([str(p) for p in concept_images])
286
  prompts_all = concept_prompts + [final_prompt]
287
  prompts_json = json.dumps(prompts_all)
288
+
289
  overrides = [
290
  f"generation.positive_prompt.image_path={imgs_json}",
291
  f"generation.positive_prompt.prompts={prompts_json}",
292
  "generation.pad_img_placehoder=False",
293
  f"ckpt.path={str(self.ckpt_dir)}",
294
  ]
295
+
296
  self._run_vincie(overrides, out_dir)
297
  return out_dir
298
 
 
301
  @staticmethod
302
  def _slug(path_or_text: str) -> str:
303
  """
304
+ Create filesystem-friendly name (max 64 chars).
305
+
306
  Args:
307
+ path_or_text: Input path or text
308
+
309
  Returns:
310
+ Sanitized string with [A-Za-z0-9._-] only
311
  """
312
  p = Path(path_or_text)
313
  base = p.stem if p.exists() else str(path_or_text)