Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

319 lines
12 KiB
JavaScript

// Tag Builder utilities — text building and tag formatting.
// Shared between Svelte components and the bridge.
// Defensive strip for natlang DB fields. If a curator pasted a leading
// `// Section:` line into the prose body, drop it — the assembly path
// emits its own header and the doubled line would otherwise leak into
// the editor.
export function stripLeadingSectionHeader(text) {
if (!text) return text;
return text.replace(/^\s*\/\/\s*[A-Za-z][^\n]*\n+/, "");
}
export function escapeTagParens(tags) {
if (!tags) return tags;
let result = "";
let i = 0;
while (i < tags.length) {
if (tags[i] === "(") {
let depth = 1;
let j = i + 1;
let colonPos = -1;
while (j < tags.length && depth > 0) {
if (tags[j] === "(") depth++;
else if (tags[j] === ")") depth--;
else if (tags[j] === ":" && depth === 1) colonPos = j;
j++;
}
if (colonPos > 0 && depth === 0) {
const afterColon = tags.slice(colonPos + 1, j - 1);
if (/^\d+\.?\d*$/.test(afterColon)) {
const content = tags.slice(i + 1, colonPos);
const weight = afterColon;
const escapedContent = content.replace(/\(/g, "\\(").replace(/\)/g, "\\)");
result += `(${escapedContent}:${weight})`;
i = j;
continue;
}
}
result += "\\(";
i++;
continue;
}
if (tags[i] === ")") {
result += "\\)";
i++;
continue;
}
result += tags[i];
i++;
}
return result;
}
export function formatTagsForModel(tags, tagSourceConfig) {
if (!tags) return "";
if (tagSourceConfig?.format === "spaces") {
return tags
.split(",")
.map(tag => tag.trim().replace(/_/g, " "))
.join(", ");
}
return tags;
}
export function buildInsertText(state, isAtLineStart, tagSourceConfig) {
if (isAtLineStart) {
return state.isNaturalMode
? buildStructuredNaturalLanguage(state)
: buildStructuredTags(state, tagSourceConfig);
}
return buildInlineText(state, tagSourceConfig);
}
function buildStructuredNaturalLanguage(state) {
const sections = [];
const characters = state.selections.characters || [];
for (const char of characters) {
const charParts = [];
const cachedChar = char.tag ? state.cache?.[`char:${char.tag}`] : null;
const series = cachedChar?.series || "";
let charName = char.display;
if (series && charName.endsWith(`(${series})`)) {
charName = charName.slice(0, -(series.length + 3)).trim();
}
let header = series ? `// Character: ${charName} (${series})` : `// Character: ${charName}`;
if (char.base && char.baseNatlang) {
let natlang = stripLeadingSectionHeader(char.baseNatlang);
if (char.outfit?.overridesNatlang) {
const toRemove = char.outfit.overridesNatlang.split("|").map(p => p.trim()).filter(Boolean);
for (const phrase of toRemove) natlang = natlang.replace(phrase, "");
natlang = natlang.replace(/\s+/g, " ").trim();
}
charParts.push(natlang);
}
if (!char.outfit && char.pose?.natlang) charParts.push(stripLeadingSectionHeader(char.pose.natlang));
if (char.outfit?.natlang) {
if (charParts.length > 0) {
let charText = charParts.join(" ");
if (!charText.endsWith(".")) charText += ".";
sections.push(header + "\n" + charText);
charParts.length = 0;
}
const outfitHeader = `// Outfit: ${char.outfit.display} from Character: ${charName}`;
const outfitBody = stripLeadingSectionHeader(char.outfit.natlang);
sections.push(outfitHeader + "\n" + (outfitBody.endsWith(".") ? outfitBody : outfitBody + "."));
} else if (charParts.length > 0) {
let charText = charParts.join(" ");
if (!charText.endsWith(".")) charText += ".";
sections.push(header + "\n" + charText);
}
if (char.outfit && char.pose?.natlang) {
const poseHeader = `// Pose: ${char.pose.display} from Character: ${charName}`;
const poseBody = stripLeadingSectionHeader(char.pose.natlang);
sections.push(poseHeader + "\n" + (poseBody.endsWith(".") ? poseBody : poseBody + "."));
}
}
const clothingItems = [];
if (state.selections.clothing) {
for (const groupName in state.selections.clothing) {
const sel = state.selections.clothing[groupName];
const items = Array.isArray(sel) ? sel : [sel];
for (const item of items) {
if (item.natlang) clothingItems.push(item.natlang);
else if (item.tags) clothingItems.push(item.tags);
}
}
}
if (clothingItems.length > 0) {
let clothingText = "Wearing " + clothingItems.join(", ");
if (!clothingText.endsWith(".")) clothingText += ".";
sections.push("// Clothing\n" + clothingText);
}
const otherParts = [];
for (const bucket of ["cast", "appearance", "pose", "scene", "expression", "action", "nsfw_action"]) {
const selections = state.selections[bucket];
if (!selections) continue;
for (const groupName in selections) {
const sel = selections[groupName];
const items = Array.isArray(sel) ? sel : [sel];
for (const item of items) {
if (item.natlang) otherParts.push(item.natlang);
else if (item.tags) otherParts.push(item.tags);
}
}
}
if (otherParts.length > 0) {
let otherText = otherParts.join(" ");
if (!otherText.endsWith(".")) otherText += ".";
sections.push("// Additional Details\n" + otherText);
}
const props = state.selections.props || [];
if (props.length > 0) {
const propTexts = props.map(p => {
let text = p.natlang || p.tags;
if (text && !text.endsWith(".")) text += ".";
return text;
}).filter(Boolean);
if (propTexts.length > 0) sections.push("// Props\n" + propTexts.join(" "));
}
return sections.join("\n\n");
}
function buildStructuredTags(state, tagSourceConfig) {
const sections = [];
const characters = state.selections.characters || [];
for (const char of characters) {
const charParts = [];
const cachedChar = char.tag ? state.cache?.[`char:${char.tag}`] : null;
const series = cachedChar?.series || "";
let charName = char.display;
let header = series ? `// Character: ${charName} (${series})` : `// Character: ${charName}`;
if (char.base && char.baseTags) {
let tags = char.baseTags;
if (char.outfit?.overridesTags) {
const toRemove = char.outfit.overridesTags.split(",").map(t => t.trim()).filter(Boolean);
for (const tag of toRemove) {
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
tags = tags.replace(new RegExp(`\\(${escapedTag}:[^)]+\\),?\\s*`, "gi"), "");
tags = tags.replace(new RegExp(`${escapedTag},?\\s*`, "gi"), "");
}
tags = tags.replace(/,\s*$/, "").replace(/,\s*,/g, ",");
}
charParts.push(escapeTagParens(tags));
}
if (!char.outfit && char.pose?.tags) charParts.push(escapeTagParens(char.pose.tags));
if (char.outfit?.tags) {
if (charParts.length > 0) {
sections.push(header + "\n" + charParts.join(", "));
charParts.length = 0;
}
const outfitHeader = `// Outfit: ${char.outfit.display} from Character: ${charName}`;
sections.push(outfitHeader + "\n" + escapeTagParens(char.outfit.tags));
} else if (charParts.length > 0) {
sections.push(header + "\n" + charParts.join(", "));
}
if (char.outfit && char.pose?.tags) {
const poseHeader = `// Pose: ${char.pose.display} from Character: ${charName}`;
sections.push(poseHeader + "\n" + escapeTagParens(char.pose.tags));
}
}
const clothingItems = [];
if (state.selections.clothing) {
for (const groupName in state.selections.clothing) {
const sel = state.selections.clothing[groupName];
const items = Array.isArray(sel) ? sel : [sel];
for (const item of items) if (item.tags) clothingItems.push(escapeTagParens(item.tags));
}
}
if (clothingItems.length > 0) sections.push("// Clothing\n" + clothingItems.join(", "));
const otherParts = [];
for (const bucket of ["cast", "appearance", "pose", "scene", "expression", "action", "nsfw_action"]) {
const selections = state.selections[bucket];
if (!selections) continue;
for (const groupName in selections) {
const sel = selections[groupName];
const items = Array.isArray(sel) ? sel : [sel];
for (const item of items) if (item.tags) otherParts.push(escapeTagParens(item.tags));
}
}
if (otherParts.length > 0) sections.push("// Additional Details\n" + otherParts.join(", "));
const props = state.selections.props || [];
if (props.length > 0) {
const propTags = props.map(p => escapeTagParens(p.tags)).filter(Boolean);
if (propTags.length > 0) sections.push("// Props\n" + propTags.join(", "));
}
return sections.map(section => {
const lines = section.split("\n");
if (lines.length === 2) return lines[0] + "\n" + formatTagsForModel(lines[1], tagSourceConfig);
return section;
}).join("\n\n");
}
function buildInlineText(state, tagSourceConfig) {
const parts = [];
const characters = state.selections.characters || [];
for (const char of characters) {
if (state.isNaturalMode) {
if (char.base && char.baseNatlang) {
let natlang = stripLeadingSectionHeader(char.baseNatlang);
if (char.outfit?.overridesNatlang) {
const toRemove = char.outfit.overridesNatlang.split("|").map(p => p.trim()).filter(Boolean);
for (const phrase of toRemove) natlang = natlang.replace(phrase, "");
natlang = natlang.replace(/\s+/g, " ").trim();
}
parts.push(natlang);
}
if (char.outfit?.natlang) parts.push(stripLeadingSectionHeader(char.outfit.natlang));
if (char.pose?.natlang) parts.push(stripLeadingSectionHeader(char.pose.natlang));
} else {
if (char.base && char.baseTags) {
let tags = char.baseTags;
if (char.outfit?.overridesTags) {
const toRemove = char.outfit.overridesTags.split(",").map(t => t.trim()).filter(Boolean);
for (const tag of toRemove) {
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
tags = tags.replace(new RegExp(`\\(${escapedTag}:[^)]+\\),?\\s*`, "gi"), "");
tags = tags.replace(new RegExp(`${escapedTag},?\\s*`, "gi"), "");
}
tags = tags.replace(/,\s*$/, "").replace(/,\s*,/g, ",");
}
parts.push(escapeTagParens(tags));
}
if (char.outfit?.tags) parts.push(escapeTagParens(char.outfit.tags));
if (char.pose?.tags) parts.push(escapeTagParens(char.pose.tags));
}
}
const props = state.selections.props || [];
for (const prop of props) {
if (state.isNaturalMode && prop.natlang) parts.push(prop.natlang);
else if (prop.tags) parts.push(escapeTagParens(prop.tags));
}
const clothingItems = [];
for (const bucket of ["cast", "appearance", "clothing", "pose", "scene", "expression", "action", "nsfw_action"]) {
const selections = state.selections[bucket];
for (const groupName in selections) {
const sel = selections[groupName];
const items = Array.isArray(sel) ? sel : [sel];
for (const item of items) {
if (bucket === "clothing" && state.isNaturalMode) {
if (item.natlang) clothingItems.push(item.natlang);
else if (item.tags) clothingItems.push(item.tags);
} else {
if (state.isNaturalMode && item.natlang) parts.push(item.natlang);
else if (item.tags) parts.push(escapeTagParens(item.tags));
}
}
}
}
if (clothingItems.length > 0 && state.isNaturalMode) {
parts.push("wearing " + clothingItems.join(", "));
}
if (parts.length === 0) return "";
const separator = state.isNaturalMode ? " " : ", ";
let result = parts.join(separator);
if (!state.isNaturalMode) result = formatTagsForModel(result, tagSourceConfig);
return result;
}