Files
BRADSEC 1ce4dc851f Use local RNG instance, widen seed to 64-bit, add age_mode tooltip (review R1-R4)
random.seed() on the shared module polluted process-wide state; a
per-call random.Random(seed) keeps same-seed output identical while
isolating other nodes. Seed max now matches the standard ComfyUI
0xffffffffffffffff range on both APIs. Bump to 1.3.1.
2026-07-03 10:50:47 +10:00

134 lines
6.0 KiB
JavaScript

import { app } from "../../scripts/app.js";
// ── Per-category labels that are sex-specific ─────────────────────────────────
// Mirrors the diff between data/male/ and data/female/ JSON files.
const MALE_ONLY = {
hair_style_fixed: ["braids short","buzz cut","crew cut","curly medium","locs long","locs short","messy medium","pompadour","quiff","side part","skin fade","slicked back","taper fade","undercut"],
hair_length_fixed: [],
body_type_fixed: ["barrel chested","broad shouldered","compact","dad bod"],
face_shape_fixed: ["rectangular"],
nose_shape_fixed: ["aquiline nose","curved nose bridge","roman nose","rounded nose tip"],
mouth_shape_fixed: [],
hair_color_fixed: [],
face_feature_fixed: ["birthmark on jaw","heavy brow","mole on cheek","strong jaw","weathered skin"],
facial_hair_fixed: ["circle beard","full beard","goatee","heavy stubble","light stubble","moustache","mutton chops","short beard","soul patch","van dyke"],
makeup_fixed: [],
clothing_fixed: ["bomber jacket","crew-neck sweater","flannel shirt","henley shirt","parka","polo shirt","three-piece suit","tracksuit","wool overcoat","work shirt"],
footwear_fixed: [],
shoulders_fixed: ["muscular"],
chest_fixed: ["average","barrel","broad","flat","muscular"],
bust_size_fixed: [],
bust_shape_fixed: [],
};
const FEMALE_ONLY = {
hair_style_fixed: ["braid over shoulder","braids","chin length bob","curtain bangs","half up","high ponytail","jaw length bob","layered","locs","loose curls","loose waves","messy","pixie","pulled back loose","space buns","tight curls","twist out"],
hair_length_fixed: ["chin length","very long"],
body_type_fixed: ["curvy","full figured","hourglass","pear","petite"],
face_shape_fixed: ["heart"],
nose_shape_fixed: ["button nose","upturned nose"],
mouth_shape_fixed: ["bow shaped lips","heart shaped lips","rosebud lips"],
hair_color_fixed: ["platinum blonde","strawberry blonde"],
face_feature_fixed: ["arched brows","beauty mark","birthmark","wide eyes"],
facial_hair_fixed: [],
makeup_fixed: ["bold lipstick","bold makeup","glam makeup","minimal makeup","natural makeup","red lipstick","smokey eye","soft makeup","winged eyeliner"],
clothing_fixed: ["blouse","camisole","cardigan","cocktail dress","jumpsuit","knit sweater","maxi dress","off-shoulder top","peplum top","sundress","wool coat","wrap dress"],
footwear_fixed: ["ballet flats","high boots","high heels","kitten heels","knee-high boots","mary janes","platform heels","stilettos","strappy heels","wedges"],
shoulders_fixed: ["soft"],
chest_fixed: [],
bust_size_fixed: ["average","extra large","full","large","petite","small"],
bust_shape_fixed: ["close-set","natural","round","teardrop","wide-set"],
};
const SEX_GATED_WIDGETS = Object.keys(MALE_ONLY);
const GATED_NODE_IDS = new Set([
"RandomPersonNode",
"RandomPersonFace",
"RandomPersonHair",
"RandomPersonBody",
"RandomPersonStyle",
]);
// ── Apply sex filter to all gated widgets on a node ───────────────────────────
function applyFilters(node, sex) {
for (const widgetName of SEX_GATED_WIDGETS) {
const w = node.widgets?.find(w => w.name === widgetName);
if (!w?.options?.values) continue;
// Store the full unfiltered list on first encounter
if (!w._allValues) w._allValues = [...w.options.values];
if (sex === "random") {
// Restore all options
w.options.values = [...w._allValues];
} else {
const excluded = new Set(
sex === "male" ? (FEMALE_ONLY[widgetName] || [])
: (MALE_ONLY[widgetName] || [])
);
w.options.values = w._allValues.filter(v => v === "(none)" || !excluded.has(v));
// If the selected value was just hidden, reset to (none)
if (excluded.has(w.value)) w.value = "(none)";
}
}
node.setDirtyCanvas?.(true, true);
}
// ── Extension ─────────────────────────────────────────────────────────────────
app.registerExtension({
name: "RandomPersonNode.SexAwareWidgets",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!GATED_NODE_IDS.has(nodeData.name)) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// Default width
if (this.size) this.size[0] = 420;
// Watch the sex widget
const sexWidget = this.widgets?.find(w => w.name === "sex");
if (sexWidget) {
// Apply immediately for the current value
applyFilters(this, sexWidget.value);
// Intercept changes
const node = this;
const origCallback = sexWidget.callback;
sexWidget.callback = function (value) {
origCallback?.call(this, value);
applyFilters(node, value);
};
}
return result;
};
// Nodes 2.0 width fallbacks
if (typeof nodeType.prototype.getDefaultSize === "function") {
nodeType.prototype.getDefaultSize = function () {
return [420, this.size?.[1] ?? 600];
};
} else {
nodeType.prototype.defaultWidth = 420;
}
},
// Re-apply after workflow load: widget values are restored before nodeCreated fires
async nodeCreated(node) {
if (!GATED_NODE_IDS.has(node.comfyClass)) return;
const sexWidget = node.widgets?.find(w => w.name === "sex");
if (sexWidget) {
setTimeout(() => applyFilters(node, sexWidget.value), 50);
}
},
});