demo: icon recipe pipeline + role-aware portraits + ComfyUI negative-prompt override
Adds two single-source-of-truth recipe files that drive both the
hot-path render server and the offline pre-render scripts:
- role_scenes.ts: per-role-band scene clauses (clothing + backdrop).
Forklift operators look like forklift operators instead of
collapsing to interchangeable studio shots. SCENES_VERSION mixes
into the headshot cache key so a coordinator tweak refreshes every
matching face on next view.
- icon_recipes.ts: cert / role-prop / status / hazard / empty icons
with deterministic per-recipe seeds + fuzzy text resolver.
ICONS_VERSION suffix on the cached file means edits don't
overwrite in place — misfires are recoverable.
Routes (mcp-server/index.ts):
- GET /headshots/_scenes — exposes SCENES + version to the
pre-render script so prompts don't drift between batch and hot-path.
- GET /icons/_recipes — same idea for icons.
- GET /icons/cert?text=... — resolves free-text cert names to a
recipe and 302s to the rendered icon. 404 (not 500) when no recipe
matches so the front-end can hang `onerror="this.remove()"`.
- GET /icons/render/{category}/{slug} — cache-or-render at 256² (8
steps) for crisper edges than 512² when downsampled to 14px.
ComfyUI portrait support (scripts/serve_imagegen.py):
The editorial workflow had `human, person, face` baked into its
negative prompt — actively sabotaging portraits. _comfyui_generate
now accepts negative_prompt/cfg/sampler/scheduler overrides, and
those mix into the cache key so portrait calls don't collapse into
hero-shot cache hits.
scripts/staffing/render_role_pool.py: pre-renders the role-aware
face pool by reading SCENES from /headshots/_scenes — single source
of truth verified at run time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,8 +46,15 @@ def _cache_put(key, img_bytes):
|
||||
(CACHE_DIR / f"{key}.webp").write_bytes(img_bytes)
|
||||
|
||||
|
||||
def _comfyui_generate(prompt, width=1024, height=512, steps=8, seed=None):
|
||||
"""Submit workflow to ComfyUI and wait for result."""
|
||||
def _comfyui_generate(prompt, width=1024, height=512, steps=8, seed=None,
|
||||
negative_prompt=None, cfg=None, sampler=None, scheduler=None):
|
||||
"""Submit workflow to ComfyUI and wait for result.
|
||||
|
||||
Optional overrides — when provided, replace the workflow's defaults.
|
||||
The workflow template at editorial_hero.json was tuned for product
|
||||
hero shots with a "no humans" negative prompt; portrait callers MUST
|
||||
pass `negative_prompt` to avoid the model fighting them on faces.
|
||||
"""
|
||||
# Load workflow template
|
||||
with open(WORKFLOW_PATH) as f:
|
||||
workflow = json.load(f)
|
||||
@@ -57,9 +64,21 @@ def _comfyui_generate(prompt, width=1024, height=512, steps=8, seed=None):
|
||||
seed = random.randint(0, 2**32)
|
||||
workflow["3"]["inputs"]["seed"] = seed
|
||||
workflow["3"]["inputs"]["steps"] = steps
|
||||
if cfg is not None:
|
||||
workflow["3"]["inputs"]["cfg"] = cfg
|
||||
if sampler:
|
||||
workflow["3"]["inputs"]["sampler_name"] = sampler
|
||||
if scheduler:
|
||||
workflow["3"]["inputs"]["scheduler"] = scheduler
|
||||
workflow["5"]["inputs"]["width"] = width
|
||||
workflow["5"]["inputs"]["height"] = height
|
||||
workflow["6"]["inputs"]["text"] = prompt
|
||||
# Node 7 is the negative-prompt CLIPTextEncode. The default is tuned
|
||||
# for product hero shots and contains "human, person, face, hand,
|
||||
# fingers, realistic photo of people" — actively sabotaging any
|
||||
# portrait render. Always overwrite when negative_prompt is given.
|
||||
if negative_prompt is not None:
|
||||
workflow["7"]["inputs"]["text"] = negative_prompt
|
||||
|
||||
# Submit to ComfyUI
|
||||
payload = json.dumps({"prompt": workflow}).encode()
|
||||
@@ -183,10 +202,20 @@ class ImageHandler(BaseHTTPRequestHandler):
|
||||
height = min(max(int(body.get("height", 720)), 256), 1080)
|
||||
steps = min(max(int(body.get("steps", 50)), 1), 80)
|
||||
seed = body.get("seed")
|
||||
# Portrait-friendly overrides — None means "use workflow default".
|
||||
# negative_prompt MUST be passed by portrait callers to avoid
|
||||
# the workflow's "no humans" baked-in negative.
|
||||
negative_prompt = body.get("negative_prompt")
|
||||
cfg = body.get("cfg")
|
||||
sampler = body.get("sampler")
|
||||
scheduler = body.get("scheduler")
|
||||
|
||||
# Cache check — seed is part of the key so per-worker requests
|
||||
# don't collapse to a single cached portrait.
|
||||
key = _cache_key(prompt, width, height, steps, seed)
|
||||
# Cache check — seed + negative + cfg are part of the key so per-
|
||||
# worker / per-config requests don't collapse to one cached image.
|
||||
key = _cache_key(
|
||||
f"{prompt}||neg={negative_prompt or ''}||cfg={cfg or ''}",
|
||||
width, height, steps, seed,
|
||||
)
|
||||
cached = _cache_get(key)
|
||||
if cached:
|
||||
self._json(200, {"image": cached, "format": "webp", "width": width, "height": height,
|
||||
@@ -199,7 +228,11 @@ class ImageHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
comfy_check = urllib.request.urlopen(f"{COMFYUI_URL}/system_stats", timeout=3)
|
||||
if comfy_check.status == 200:
|
||||
img_bytes, seed = _comfyui_generate(prompt, width, height, steps, seed)
|
||||
img_bytes, seed = _comfyui_generate(
|
||||
prompt, width, height, steps, seed,
|
||||
negative_prompt=negative_prompt, cfg=cfg,
|
||||
sampler=sampler, scheduler=scheduler,
|
||||
)
|
||||
backend = "comfyui"
|
||||
except:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user