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>
93 lines
4.5 KiB
TypeScript
93 lines
4.5 KiB
TypeScript
// Server-side mirror of search.html's ROLE_BANDS regex table.
|
||
// Each band carries a *visual scene* — clothing + immediate backdrop —
|
||
// so ComfyUI produces role-coherent headshots instead of interchangeable
|
||
// studio portraits. The front-end sends the raw role string in the
|
||
// query (?role=Forklift%20Operator); the server resolves it to a band
|
||
// and looks up the scene here.
|
||
|
||
export type RoleBand =
|
||
| "warehouse"
|
||
| "production"
|
||
| "trades"
|
||
| "driver"
|
||
| "lead";
|
||
|
||
export interface SceneDef {
|
||
band: RoleBand;
|
||
// Free-form clause inserted into the diffusion prompt AFTER
|
||
// "[age]-year-old [race] [gender] [role], ". Should describe what
|
||
// they're wearing and what is immediately behind them. Keep under
|
||
// ~25 words — SDXL Turbo loses focus on longer prompts and starts
|
||
// hallucinating cartoon hands.
|
||
scene: string;
|
||
}
|
||
|
||
const RE_BANDS: { re: RegExp; band: RoleBand }[] = [
|
||
{ re: /forklift|warehouse|associate|material\s*handler|loader|loading|packag|shipping|logistics|inventory|sanitation|janit/i, band: "warehouse" },
|
||
{ re: /production|assembl|quality/i, band: "production" },
|
||
{ re: /welder|weld|electric|maint(enance)?\s*tech|cnc|machine\s*op|hvac|plumb|carpenter|mason|tool\s*&\s*die/i, band: "trades" },
|
||
{ re: /driver|truck|haul|cdl/i, band: "driver" },
|
||
{ re: /line\s*lead|supervisor|foreman|coordinator|lead\b/i, band: "lead" },
|
||
];
|
||
|
||
export function roleBand(role: string): RoleBand {
|
||
const r = (role || "").trim();
|
||
if (!r) return "warehouse";
|
||
for (const b of RE_BANDS) if (b.re.test(r)) return b.band;
|
||
return "warehouse";
|
||
}
|
||
|
||
// TODO J — refine these. Each `scene` string lands directly in the
|
||
// diffusion prompt. Tone target: a coordinator glances at the card
|
||
// and recognizes the role from the photo before reading the role pill.
|
||
//
|
||
// Things that work well in SDXL Turbo at 8 steps:
|
||
// - One concrete clothing item ("high-visibility yellow vest")
|
||
// - One concrete prop ("hard hat hanging from belt", "tablet in hand")
|
||
// - One blurred background element ("warehouse pallet aisle behind",
|
||
// "factory machinery softly out of focus")
|
||
// - Avoid: text/logos (rendered as scribble), specific brands, hands
|
||
// holding tools (often distorts), full-body language ("standing",
|
||
// "leaning") — model is trained on portrait crops.
|
||
//
|
||
// Each scene now bakes "monochrome black and white photography" into
|
||
// the prompt so the model produces native B&W output rather than us
|
||
// applying CSS grayscale post-hoc. SDXL Turbo handles B&W natively
|
||
// with strong tonal range — better than desaturating a color render.
|
||
export const SCENES: Record<RoleBand, SceneDef> = {
|
||
warehouse: {
|
||
band: "warehouse",
|
||
scene: "wearing a high-visibility safety vest over a t-shirt, hard hat visible, blurred warehouse pallet aisle behind, soft natural light, monochrome black and white photography, fine film grain, documentary portrait style",
|
||
},
|
||
production: {
|
||
band: "production",
|
||
scene: "wearing a work shirt with safety glasses on forehead, blurred factory machinery softly out of focus behind, fluorescent overhead lighting, monochrome black and white photography, fine film grain, documentary portrait style",
|
||
},
|
||
trades: {
|
||
band: "trades",
|
||
scene: "wearing a heavy-duty work shirt with rolled sleeves, blurred workshop tool wall behind, focused tungsten lighting, monochrome black and white photography, fine film grain, documentary portrait style",
|
||
},
|
||
driver: {
|
||
band: "driver",
|
||
scene: "wearing a polo shirt, lanyard with ID badge visible, blurred truck cab or loading dock behind, daylight, monochrome black and white photography, fine film grain, documentary portrait style",
|
||
},
|
||
lead: {
|
||
band: "lead",
|
||
scene: "wearing a button-down shirt, tablet held casually at chest level, blurred warehouse floor in soft focus behind, professional lighting, monochrome black and white photography, fine film grain, documentary portrait style",
|
||
},
|
||
};
|
||
|
||
// v2 — baked B&W + 1024×1024 render canvas (4× pixels of v1). Larger
|
||
// source means downsampling to a 40px avatar packs more detail per
|
||
// displayed pixel, hiding the diffusion-y micro-textures that read as
|
||
// "AI generated" at small sizes. Server route reads pool from
|
||
// data/headshots_role_pool/{SCENES_VERSION}/... so v1 stays available
|
||
// for rollback / A-B comparison.
|
||
export const SCENES_VERSION = "v2";
|
||
|
||
// Default render dimensions used by both the on-demand /headshots/
|
||
// generate/:key route and the offline render_role_pool.py script. v1
|
||
// used 512²; v2 doubles to 1024² (linear 2× = 4× pixels = ~3× GPU
|
||
// time on SDXL Turbo).
|
||
export const FACE_RENDER_DIM = 1024;
|