@mcptoolshop/style-dataset-lab 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.es.md +96 -63
- package/README.fr.md +97 -64
- package/README.hi.md +108 -77
- package/README.it.md +97 -64
- package/README.ja.md +105 -72
- package/README.md +33 -0
- package/README.pt-BR.md +94 -61
- package/README.zh.md +98 -65
- package/bin/sdlab.js +71 -0
- package/lib/adapters/ai-toolkit.js +209 -0
- package/lib/adapters/comfyui-runner.js +36 -2
- package/lib/adapters/comfyui-workflows.js +133 -2
- package/lib/adapters/diffusers-lora.js +46 -66
- package/lib/adapters/generic-image-caption.js +22 -50
- package/lib/canon-build/build.js +450 -0
- package/lib/canon-build/emitters/base.js +122 -0
- package/lib/canon-build/emitters/character.js +167 -0
- package/lib/canon-build/emitters/deity.js +165 -0
- package/lib/canon-build/emitters/faction.js +161 -0
- package/lib/canon-build/emitters/index.js +38 -0
- package/lib/canon-build/emitters/location.js +153 -0
- package/lib/canon-build/emitters/monster.js +131 -0
- package/lib/canon-build/emitters/relic.js +142 -0
- package/lib/canon-build/emitters/ship.js +154 -0
- package/lib/canon-build/emitters/species.js +165 -0
- package/lib/canon-build/frontmatter.js +76 -0
- package/lib/canon-build/lane-resolve.js +64 -0
- package/lib/canon-build/load-config.js +117 -0
- package/lib/canon-build/load-entry.js +62 -0
- package/lib/canon-build/load-schema.js +46 -0
- package/lib/captions.js +197 -0
- package/lib/config.js +19 -0
- package/lib/critique-engine.js +46 -1
- package/lib/critique-render.js +45 -0
- package/lib/freeze-events.js +109 -0
- package/lib/freeze-gate.js +208 -0
- package/lib/freeze-stamp.js +161 -0
- package/lib/rows.js +228 -0
- package/lib/run-manifest.js +308 -0
- package/lib/snapshot.js +7 -1
- package/lib/training-adapters.js +5 -0
- package/lib/training-packages.js +24 -18
- package/lib/training-profiles.js +33 -0
- package/lib/verifier.js +67 -0
- package/package.json +12 -3
- package/scripts/_batch_cut.py +71 -0
- package/scripts/_seam_cutout.py +74 -0
- package/scripts/canon-build.js +88 -0
- package/scripts/canon-drift.js +193 -0
- package/scripts/canon-freeze-status.js +85 -0
- package/scripts/canon-freeze.js +211 -0
- package/scripts/canon-unfreeze.js +137 -0
- package/scripts/critique.js +32 -7
- package/scripts/curate.js +33 -0
- package/scripts/generate-controlnet.js +12 -0
- package/scripts/generate-identity.js +20 -4
- package/scripts/generate-ipadapter.js +11 -0
- package/scripts/generate.js +262 -102
- package/scripts/qwen_generate.py +327 -0
- package/scripts/run-generate.js +3 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Studio NON-ANIME generation on Qwen-Image (the verified base) via ComfyUI.
|
|
3
|
+
|
|
4
|
+
Reads a canon-derived WAVE json ({style_prefix, defaults, subjects:[{id,prompt,variations}]}) and
|
|
5
|
+
generates each subject on Qwen-Image, saving images + a provenance receipt (PIN_PER_STEP) to an
|
|
6
|
+
output dir. This is the bridge generator until sdlab's buildWorkflowGraph() gains native Qwen
|
|
7
|
+
support (tracked follow-on). It is the studio's NON-ANIME path — see memory/feedback_no_anime.md.
|
|
8
|
+
|
|
9
|
+
All Qwen-Image components are on the rig (diffusion_models/text_encoders/vae). Run with ComfyUI's
|
|
10
|
+
embedded python (has PIL): start the watchdog (E:/AI/training/_watchdog.ps1) before GPU runs.
|
|
11
|
+
|
|
12
|
+
python qwen_generate.py --wave <wave.json> --out <dir> [--variations N]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
import urllib.request
|
|
24
|
+
import uuid
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from PIL import Image
|
|
28
|
+
|
|
29
|
+
COMFY = "http://127.0.0.1:8188"
|
|
30
|
+
COMFY_OUT = Path(r"E:/AI-Models/ComfyUI_windows_portable/ComfyUI/output")
|
|
31
|
+
|
|
32
|
+
# Qwen-Image native components (on rig). NON-ANIME base — never an anime checkpoint.
|
|
33
|
+
QWEN_UNET = "qwen_image_fp8_e4m3fn.safetensors"
|
|
34
|
+
QWEN_CLIP = "qwen_2.5_vl_7b_fp8_scaled.safetensors"
|
|
35
|
+
QWEN_VAE = "qwen_image_vae.safetensors"
|
|
36
|
+
|
|
37
|
+
# ─── PIN_PER_STEP pinning contract ────────────────────────────────────────────
|
|
38
|
+
# Mirror of lib/run-manifest.js (the JS helper is the field-name authority). This
|
|
39
|
+
# receipt carries the IDENTICAL `pinning` block so a wave produced by this bridge
|
|
40
|
+
# generator is pinned the same way as one produced through the JS `sdlab generate`
|
|
41
|
+
# / `sdlab run generate` paths. Keep PINNING_VERSION + the field names in lockstep
|
|
42
|
+
# with lib/run-manifest.js.
|
|
43
|
+
PINNING_VERSION = "1.0.0"
|
|
44
|
+
MODELS_DIR = os.environ.get(
|
|
45
|
+
"SDLAB_MODELS_DIR", r"E:/AI-Models/ComfyUI_windows_portable/ComfyUI/models"
|
|
46
|
+
)
|
|
47
|
+
HASH_CACHE_FILE = os.environ.get("SDLAB_HASH_CACHE") or str(
|
|
48
|
+
Path(MODELS_DIR) / ".sdlab-hash-cache.json"
|
|
49
|
+
)
|
|
50
|
+
# kind → candidate subdirs under the models dir, in search order (mirror of JS).
|
|
51
|
+
_KIND_SUBDIRS = {
|
|
52
|
+
"unet": ["diffusion_models", "unet"],
|
|
53
|
+
"clip": ["text_encoders", "clip"],
|
|
54
|
+
"vae": ["vae"],
|
|
55
|
+
"checkpoint": ["checkpoints"],
|
|
56
|
+
"lora": ["loras"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _js_number(n) -> str:
|
|
61
|
+
"""Format a number the way JS String(n) does, so the graph hash matches the JS
|
|
62
|
+
helper: integer-valued floats drop the decimal (1.0 -> "1"), others use the
|
|
63
|
+
shortest round-trip repr (3.1 -> "3.1")."""
|
|
64
|
+
if isinstance(n, bool):
|
|
65
|
+
return "true" if n else "false"
|
|
66
|
+
if isinstance(n, int):
|
|
67
|
+
return str(n)
|
|
68
|
+
if isinstance(n, float):
|
|
69
|
+
if not math.isfinite(n):
|
|
70
|
+
return "null"
|
|
71
|
+
return str(int(n)) if n.is_integer() else repr(n)
|
|
72
|
+
return "null"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _stable_stringify(value) -> str:
|
|
76
|
+
"""Deterministic serialization mirroring lib/run-manifest.js stableStringify:
|
|
77
|
+
object keys sorted, arrays in order, JS-style number/string formatting."""
|
|
78
|
+
if value is None:
|
|
79
|
+
return "null"
|
|
80
|
+
if isinstance(value, bool):
|
|
81
|
+
return "true" if value else "false"
|
|
82
|
+
if isinstance(value, (int, float)):
|
|
83
|
+
return _js_number(value)
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
return json.dumps(value, ensure_ascii=False)
|
|
86
|
+
if isinstance(value, (list, tuple)):
|
|
87
|
+
return "[" + ",".join(_stable_stringify(v) for v in value) + "]"
|
|
88
|
+
if isinstance(value, dict):
|
|
89
|
+
# JS omits `undefined` keys; Python has no undefined, and None maps to JS
|
|
90
|
+
# `null` which stableStringify DOES serialize — so serialize every key.
|
|
91
|
+
parts = [
|
|
92
|
+
json.dumps(k, ensure_ascii=False) + ":" + _stable_stringify(value[k])
|
|
93
|
+
for k in sorted(value.keys())
|
|
94
|
+
]
|
|
95
|
+
return "{" + ",".join(parts) + "}"
|
|
96
|
+
return "null"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _normalize_graph_for_hash(graph: dict) -> dict:
|
|
100
|
+
"""Blank per-item nondeterminism (seed, prompt text, filename_prefix) so the
|
|
101
|
+
whole wave shares one comfy_workflow_sha. Mirror of the JS normalizer."""
|
|
102
|
+
out = {}
|
|
103
|
+
for nid, node in (graph or {}).items():
|
|
104
|
+
inputs = dict(node.get("inputs", {}))
|
|
105
|
+
cls = node.get("class_type")
|
|
106
|
+
if cls in ("KSampler", "KSamplerAdvanced"):
|
|
107
|
+
if "seed" in inputs:
|
|
108
|
+
inputs["seed"] = 0
|
|
109
|
+
if "noise_seed" in inputs:
|
|
110
|
+
inputs["noise_seed"] = 0
|
|
111
|
+
elif cls == "CLIPTextEncode":
|
|
112
|
+
if "text" in inputs:
|
|
113
|
+
inputs["text"] = ""
|
|
114
|
+
elif cls == "SaveImage":
|
|
115
|
+
if "filename_prefix" in inputs:
|
|
116
|
+
inputs["filename_prefix"] = ""
|
|
117
|
+
out[nid] = {"class_type": cls, "inputs": inputs}
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _comfy_workflow_sha(graph: dict):
|
|
122
|
+
if not graph:
|
|
123
|
+
return None
|
|
124
|
+
payload = _stable_stringify(_normalize_graph_for_hash(graph))
|
|
125
|
+
return "sha256:" + hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolve_model_path(name: str, kind: str):
|
|
129
|
+
if not name:
|
|
130
|
+
return None
|
|
131
|
+
subdirs = list(_KIND_SUBDIRS.get(kind, []))
|
|
132
|
+
for group in _KIND_SUBDIRS.values():
|
|
133
|
+
subdirs.extend(group)
|
|
134
|
+
subdirs.append(".")
|
|
135
|
+
seen = set()
|
|
136
|
+
for sub in subdirs:
|
|
137
|
+
cand = os.path.join(MODELS_DIR, sub, name)
|
|
138
|
+
if cand in seen:
|
|
139
|
+
continue
|
|
140
|
+
seen.add(cand)
|
|
141
|
+
if os.path.exists(cand):
|
|
142
|
+
return cand
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sha256_file(path: str) -> str:
|
|
147
|
+
h = hashlib.sha256()
|
|
148
|
+
with open(path, "rb") as f:
|
|
149
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
150
|
+
h.update(chunk)
|
|
151
|
+
return "sha256:" + h.hexdigest()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _load_hash_cache() -> dict:
|
|
155
|
+
try:
|
|
156
|
+
with open(HASH_CACHE_FILE, "r", encoding="utf-8") as f:
|
|
157
|
+
return json.load(f)
|
|
158
|
+
except Exception:
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _save_hash_cache(cache: dict) -> None:
|
|
163
|
+
try:
|
|
164
|
+
with open(HASH_CACHE_FILE, "w", encoding="utf-8") as f:
|
|
165
|
+
json.dump(cache, f, indent=2)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _hash_model_file(name: str, kind: str, do_hash: bool, cache: dict) -> dict:
|
|
171
|
+
"""Best-effort {name, size_bytes, sha256} for a model/LoRA file (mirror of the
|
|
172
|
+
JS hashModelFile). size_bytes is always filled when resolvable; sha256 is
|
|
173
|
+
gated behind do_hash and cached by (name, size, mtime)."""
|
|
174
|
+
if not name:
|
|
175
|
+
return {"name": name, "size_bytes": None, "sha256": None, "hash_note": "no filename"}
|
|
176
|
+
resolved = _resolve_model_path(name, kind)
|
|
177
|
+
if not resolved:
|
|
178
|
+
return {"name": name, "size_bytes": None, "sha256": None, "hash_note": f"unresolved under {MODELS_DIR}"}
|
|
179
|
+
try:
|
|
180
|
+
st = os.stat(resolved)
|
|
181
|
+
size_bytes = st.st_size
|
|
182
|
+
mtime_ms = round(st.st_mtime * 1000)
|
|
183
|
+
except OSError:
|
|
184
|
+
return {"name": name, "size_bytes": None, "sha256": None, "hash_note": "stat failed"}
|
|
185
|
+
if not do_hash:
|
|
186
|
+
return {"name": name, "size_bytes": size_bytes, "sha256": None, "hash_note": "hash skipped (pass --hash-models)"}
|
|
187
|
+
key = f"{name}:{size_bytes}:{mtime_ms}"
|
|
188
|
+
if key in cache:
|
|
189
|
+
return {"name": name, "size_bytes": size_bytes, "sha256": cache[key]}
|
|
190
|
+
try:
|
|
191
|
+
sha = _sha256_file(resolved)
|
|
192
|
+
except OSError:
|
|
193
|
+
return {"name": name, "size_bytes": size_bytes, "sha256": None, "hash_note": "hash failed (read error)"}
|
|
194
|
+
cache[key] = sha
|
|
195
|
+
return {"name": name, "size_bytes": size_bytes, "sha256": sha}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _build_pinning(graph, models, loras, seed_policy, do_hash, cache) -> dict:
|
|
199
|
+
"""Assemble the pinning block — identical shape to lib/run-manifest.js buildPinning."""
|
|
200
|
+
model_hashes = {}
|
|
201
|
+
for role, kind in (("unet", "unet"), ("clip", "clip"), ("vae", "vae"), ("checkpoint", "checkpoint")):
|
|
202
|
+
if models.get(role):
|
|
203
|
+
model_hashes[role] = _hash_model_file(models[role], kind, do_hash, cache)
|
|
204
|
+
lora_hashes = []
|
|
205
|
+
for lora in loras or []:
|
|
206
|
+
entry = _hash_model_file(lora.get("name"), "lora", do_hash, cache)
|
|
207
|
+
entry["weight"] = lora.get("weight", 1.0)
|
|
208
|
+
lora_hashes.append(entry)
|
|
209
|
+
return {
|
|
210
|
+
"pinning_version": PINNING_VERSION,
|
|
211
|
+
"comfy_workflow_sha": _comfy_workflow_sha(graph),
|
|
212
|
+
"seed_policy": seed_policy,
|
|
213
|
+
"models": model_hashes,
|
|
214
|
+
"loras": lora_hashes,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def qwen_graph(pos: str, neg: str, seed: int, w: int, h: int, steps: int, cfg: float, prefix: str,
|
|
219
|
+
loras: list | None = None) -> dict:
|
|
220
|
+
"""loras: [{name, weight}] chained as DiT-only LoraLoaderModelOnly nodes (ids from
|
|
221
|
+
"40", between UNETLoader and ModelSamplingAuraFlow) — mirrors sdlab buildQwenGraph.
|
|
222
|
+
Model-only because studio Qwen style LoRAs train the transformer with the text
|
|
223
|
+
encoder frozen."""
|
|
224
|
+
nodes = {
|
|
225
|
+
"37": {"class_type": "UNETLoader", "inputs": {"unet_name": QWEN_UNET, "weight_dtype": "default"}},
|
|
226
|
+
"38": {"class_type": "CLIPLoader", "inputs": {"clip_name": QWEN_CLIP, "type": "qwen_image"}},
|
|
227
|
+
"39": {"class_type": "VAELoader", "inputs": {"vae_name": QWEN_VAE}},
|
|
228
|
+
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": pos, "clip": ["38", 0]}},
|
|
229
|
+
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": neg, "clip": ["38", 0]}},
|
|
230
|
+
}
|
|
231
|
+
model_out = ["37", 0]
|
|
232
|
+
for i, lora in enumerate(loras or []):
|
|
233
|
+
lid = str(40 + i)
|
|
234
|
+
nodes[lid] = {"class_type": "LoraLoaderModelOnly", "inputs": {
|
|
235
|
+
"lora_name": lora["name"], "strength_model": lora.get("weight", 1.0), "model": model_out}}
|
|
236
|
+
model_out = [lid, 0]
|
|
237
|
+
nodes.update({
|
|
238
|
+
"66": {"class_type": "ModelSamplingAuraFlow", "inputs": {"shift": 3.1, "model": model_out}},
|
|
239
|
+
"5": {"class_type": "EmptySD3LatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}},
|
|
240
|
+
"3": {"class_type": "KSampler", "inputs": {"seed": seed, "steps": steps, "cfg": cfg, "sampler_name": "euler",
|
|
241
|
+
"scheduler": "simple", "denoise": 1.0, "model": ["66", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0]}},
|
|
242
|
+
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["39", 0]}},
|
|
243
|
+
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": prefix, "images": ["8", 0]}},
|
|
244
|
+
})
|
|
245
|
+
return nodes
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _post(path: str, payload: dict):
|
|
249
|
+
return json.loads(urllib.request.urlopen(urllib.request.Request(
|
|
250
|
+
f"{COMFY}{path}", json.dumps(payload).encode(), {"Content-Type": "application/json"}), timeout=90).read())
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def generate(pos: str, neg: str, seed: int, dst: Path, **kw) -> Path:
|
|
254
|
+
g = qwen_graph(pos, neg, seed, kw["w"], kw["h"], kw["steps"], kw["cfg"], dst.stem, loras=kw.get("loras"))
|
|
255
|
+
pid = _post("/prompt", {"prompt": g, "client_id": uuid.uuid4().hex})["prompt_id"]
|
|
256
|
+
for _ in range(1200):
|
|
257
|
+
hist = json.loads(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=30).read())
|
|
258
|
+
if pid in hist and hist[pid].get("outputs"):
|
|
259
|
+
img = hist[pid]["outputs"]["9"]["images"][0]
|
|
260
|
+
src = COMFY_OUT / img["subfolder"] / img["filename"]
|
|
261
|
+
for _w in range(120):
|
|
262
|
+
if src.exists():
|
|
263
|
+
Image.open(src).convert("RGB").save(dst)
|
|
264
|
+
return dst
|
|
265
|
+
time.sleep(0.5)
|
|
266
|
+
time.sleep(0.5)
|
|
267
|
+
raise RuntimeError("ComfyUI generation timed out")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main() -> None:
|
|
271
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
272
|
+
ap.add_argument("--wave", required=True, type=Path)
|
|
273
|
+
ap.add_argument("--out", required=True, type=Path)
|
|
274
|
+
ap.add_argument("--variations", type=int, default=None, help="override per-subject variation count")
|
|
275
|
+
ap.add_argument("--hash-models", action="store_true",
|
|
276
|
+
help="content-hash the unet/clip/vae/LoRA files into the pinning block (best-effort, cached)")
|
|
277
|
+
args = ap.parse_args()
|
|
278
|
+
|
|
279
|
+
wave = json.loads(args.wave.read_text(encoding="utf-8"))
|
|
280
|
+
prefix = wave.get("style_prefix", "")
|
|
281
|
+
d = wave.get("defaults", {})
|
|
282
|
+
neg = d.get("negative", "")
|
|
283
|
+
w, h = d.get("width", 1024), d.get("height", 1024)
|
|
284
|
+
steps, cfg = d.get("steps", 22), d.get("cfg", 3.5)
|
|
285
|
+
base_seed = d.get("base_seed", 1000)
|
|
286
|
+
loras = d.get("loras", [])
|
|
287
|
+
args.out.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
|
|
289
|
+
receipt = {"wave": wave.get("wave"), "base": "qwen-image", "unet": QWEN_UNET, "clip": QWEN_CLIP,
|
|
290
|
+
"vae": QWEN_VAE, "sampler": "euler", "scheduler": "simple", "shift": 3.1,
|
|
291
|
+
"steps": steps, "cfg": cfg, "size": [w, h], "style_prefix": prefix, "negative": neg,
|
|
292
|
+
"loras": loras, "items": []}
|
|
293
|
+
|
|
294
|
+
# PIN_PER_STEP: pin the exact graph + model/LoRA content identity + seed policy.
|
|
295
|
+
# The wave shares ONE comfy_workflow_sha (per-item seed + prompt are normalized
|
|
296
|
+
# out and recorded per item in items[]). seed_policy is "explicit-per-item"
|
|
297
|
+
# because each item's seed is written verbatim below. Mirror of the JS contract.
|
|
298
|
+
hash_cache = _load_hash_cache()
|
|
299
|
+
pin_graph = qwen_graph(prefix or "x", neg, base_seed, w, h, steps, cfg, "sdl", loras=loras)
|
|
300
|
+
receipt["pinning"] = _build_pinning(
|
|
301
|
+
pin_graph,
|
|
302
|
+
{"unet": QWEN_UNET, "clip": QWEN_CLIP, "vae": QWEN_VAE},
|
|
303
|
+
loras,
|
|
304
|
+
"explicit-per-item",
|
|
305
|
+
args.hash_models,
|
|
306
|
+
hash_cache,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
seed = base_seed
|
|
310
|
+
for subj in wave["subjects"]:
|
|
311
|
+
nvar = args.variations if args.variations is not None else subj.get("variations", 1)
|
|
312
|
+
pos = f"{prefix}, {subj['prompt']}" if prefix else subj["prompt"]
|
|
313
|
+
for v in range(nvar):
|
|
314
|
+
dst = args.out / f"{subj['id']}_v{v}.png"
|
|
315
|
+
generate(pos, neg, seed, dst, w=w, h=h, steps=steps, cfg=cfg, loras=loras)
|
|
316
|
+
print(f" {subj['id']} v{v} (seed {seed}) -> {dst.name}", flush=True)
|
|
317
|
+
receipt["items"].append({"id": subj["id"], "variation": v, "seed": seed, "file": dst.name, "prompt": pos})
|
|
318
|
+
seed += 1
|
|
319
|
+
|
|
320
|
+
if args.hash_models:
|
|
321
|
+
_save_hash_cache(hash_cache)
|
|
322
|
+
(args.out / "generation.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
323
|
+
print(f"\n{len(receipt['items'])} images -> {args.out} (+ generation.json provenance)")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
package/scripts/run-generate.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
24
24
|
brief: { type: 'string' },
|
|
25
25
|
seed: { type: 'string' },
|
|
26
26
|
'dry-run': { type: 'boolean' },
|
|
27
|
+
'hash-models': { type: 'boolean' },
|
|
27
28
|
json: { type: 'boolean' },
|
|
28
29
|
},
|
|
29
30
|
deprecated: { game: 'project' },
|
|
@@ -36,6 +37,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
36
37
|
console.log(' --brief <id> Compiled brief ID (required)');
|
|
37
38
|
console.log(' --seed <n> Override base seed');
|
|
38
39
|
console.log(' --dry-run Prepare run dir without submitting to ComfyUI');
|
|
40
|
+
console.log(' --hash-models Content-hash the model/LoRA files into the run manifest (best-effort, cached)');
|
|
39
41
|
console.log(' --json Output manifest as JSON');
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
@@ -59,6 +61,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
59
61
|
brief,
|
|
60
62
|
baseSeed: flags.seed ? parseNumberFlag('seed', flags.seed, { int: true, min: 0 }) : undefined,
|
|
61
63
|
dryRun: flags['dry-run'] || false,
|
|
64
|
+
hashModels: flags['hash-models'] || false,
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
// Save summary
|