- Export V3 comfy_entrypoint only when comfy_api V3 symbols exist, else V1 mappings; narrow the V3 import guard so real import errors surface - Reject age_min > age_max with an input-named ValueError - Validate data file structure at load with path and field in errors - Sync JS sex filter lists with bundled data; test fails on drift - Filter incompatible hair length/style and build/chest/shoulders pairs; user-fixed or allow-listed values win when nothing compatible remains - Join bust as "<size> <shape> bust" - Vocabulary: fix malformed phrases (softly built build, hair worn buzz cut), remove duplicate and conflicting entries, rename Maori to Maori with macron, add skin tones, body types, hair styles, clothing, footwear, accessories, female facial hair and male makeup options - Bump version to 1.4.0
133 lines
6.0 KiB
JavaScript
133 lines
6.0 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
// Per-category differences are checked against bundled JSON by the unit tests.
|
|
|
|
const MALE_ONLY = {
|
|
face_shape_fixed: ["rectangular"],
|
|
nose_shape_fixed: ["curved nose bridge", "snub nose"],
|
|
mouth_shape_fixed: [],
|
|
face_feature_fixed: ["birthmark on jaw", "heavy brow", "mole on cheek", "strong jaw"],
|
|
hair_color_fixed: ["red"],
|
|
hair_style_fixed: ["bowl cut", "braids short", "buzz cut", "caesar cut", "crew cut", "curly", "curly medium", "french crop", "locs long", "locs short", "messy medium", "pompadour", "quiff", "side part", "skin fade", "slicked back", "taper fade", "undercut", "wavy"],
|
|
hair_length_fixed: [],
|
|
facial_hair_fixed: ["circle beard", "full beard", "goatee", "heavy stubble", "light stubble", "moustache", "mutton chops", "short beard", "soul patch", "van dyke"],
|
|
body_type_fixed: ["compact", "dad bod", "medium build", "slight"],
|
|
shoulders_fixed: ["muscular"],
|
|
chest_fixed: ["average", "barrel", "broad", "flat", "muscular"],
|
|
bust_size_fixed: ["none"],
|
|
bust_shape_fixed: ["none"],
|
|
makeup_fixed: ["eyeliner", "subtle concealer"],
|
|
clothing_fixed: ["bomber jacket", "crew-neck sweater", "flannel shirt", "henley shirt", "parka", "polo shirt", "three-piece suit", "tracksuit", "wool overcoat", "work shirt"],
|
|
footwear_fixed: [],
|
|
};
|
|
|
|
const FEMALE_ONLY = {
|
|
face_shape_fixed: ["heart"],
|
|
nose_shape_fixed: ["button nose"],
|
|
mouth_shape_fixed: ["bow shaped lips", "heart shaped lips", "rosebud lips"],
|
|
face_feature_fixed: ["beauty mark", "birthmark"],
|
|
hair_color_fixed: ["platinum blonde", "strawberry blonde"],
|
|
hair_style_fixed: ["blunt cut", "braid over shoulder", "braids", "chignon", "chin length bob", "cropped short", "curtain bangs", "fishtail braid", "french braid", "half up", "high ponytail", "jaw length bob", "layered", "locs", "loose curls", "loose waves", "messy", "pixie", "pulled back loose", "shag", "space buns", "tight curls", "twist out", "updo", "wispy", "wolf cut"],
|
|
hair_length_fixed: ["chin length", "very long"],
|
|
facial_hair_fixed: ["chin hair", "light facial hair"],
|
|
body_type_fixed: ["curvy", "full figured", "hourglass", "pear", "petite"],
|
|
shoulders_fixed: ["soft"],
|
|
chest_fixed: ["none"],
|
|
bust_size_fixed: ["average", "extra large", "full", "large", "petite", "small"],
|
|
bust_shape_fixed: ["close-set", "round", "teardrop", "wide-set"],
|
|
makeup_fixed: ["bold lipstick", "bold makeup", "minimal makeup", "natural makeup", "smokey eye", "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 heels", "kitten heels", "knee-high boots", "mary janes", "platform heels", "stilettos", "strappy heels", "wedges"],
|
|
};
|
|
|
|
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);
|
|
}
|
|
},
|
|
});
|