RBG Smart Seed Variance — major update 2.2.0
RBG Smart Seed Variance — major update 2.2.0: Krea2 support, variance scheduling, interactive token protection, and direction shift library expansion and more..
- Add Krea2 (SingleStream) model type with band-aware noise handling across
the 12-layer MMDiT tap stack, including a dedicated base-embedding
rebalance pass to boost expression, attention detail and prompt following.
- Add ERNIE-Image to model-specific adjustment presets, alongside
refined multipliers for Flux, Chroma HD, SDXL and Wan2.2.
- Add interactive token protection inspector (JS): click-to-toggle individual
tokens directly on a live visualisation of the protection mask, replacing
manual range typing as the primary workflow.
- Add "Custom Regions" and "Random Regions" protect modes — custom accepts
explicit token/range syntax (e.g. "0-5,15-20"), random seeds a procedural
protection mask.
- Add live protection mask feedback from backend to frontend via onExecuted,
so the inspector reflects the actual tokens used in the last run rather
than a static preview.
- Add Variance Schedule (Composition Lock 🔒) — control how noise intensity
changes across the sampling timeline with 5 modes: constant, decreasing
(fade out), step_cutoff (hard switch at a step), hard_lock (zero variance
until cutoff), and tiered_release (multi-phase staged unlock)
- Add cutoff_step, total_steps, and cutoff_strength controls — map a step
number to a percentage of the sampling timeline and set the noise
multiplier applied after the cutoff point, so composition can be locked
early while detail variance continues later.
- Expand direction shift library from the original artistic set to 26+
patterns, including Face-Variance Expansion, Visceral Expression & Grit
(Krea2-tuned), Semantic Drift, Structural Lock, Cinematic Framing, Texture
Lift, Landscape Depth, Group Diversity, Expression Variance, Motion Blur,
Microscopic, Cosmic, and Bone Anatomical Coherence.
- Add dynamic widget visibility — protect_regions field now shows/hides
automatically based on the selected protect_mode.
- Fix word-index vs BPE-index mismatch in token protection by adding
bidirectional BPE-to-word mapping, so protected ranges align correctly
with actual tokenizer output instead of word-level approximations.
This commit is contained in:
+576
-367
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,53 @@ app.registerExtension({
|
||||
container.appendChild(row1);
|
||||
container.appendChild(syncBtn);
|
||||
|
||||
// --- HELPER: widget visibility ---
|
||||
const widgetVisibilityMap = new Map();
|
||||
const setWidgetVisible = (widget, visible) => {
|
||||
if (!widget) return;
|
||||
widget.hidden = !visible;
|
||||
widget.computeSize = visible ? undefined : (w) => [w, -4];
|
||||
widgetVisibilityMap.set(widget.name, visible);
|
||||
};
|
||||
|
||||
const getWidget = (name) => node.widgets.find((w) => w.name === name);
|
||||
|
||||
const updateVisibility = () => {
|
||||
const preset = getWidget("variance_preset")?.value;
|
||||
const directionShift = getWidget("direction_shift")?.value;
|
||||
const schedule = getWidget("variance_schedule")?.value;
|
||||
const protectMode = getWidget("protect_mode")?.value;
|
||||
|
||||
setWidgetVisible(getWidget("fine_tune_variance"), preset === "⚙️ Custom");
|
||||
setWidgetVisible(getWidget("shift_strength"), directionShift && directionShift !== "🚫 None");
|
||||
setWidgetVisible(getWidget("cutoff_step"), schedule && schedule !== "constant");
|
||||
setWidgetVisible(getWidget("total_steps"), schedule && schedule !== "constant");
|
||||
setWidgetVisible(getWidget("cutoff_strength"), schedule && schedule !== "constant");
|
||||
setWidgetVisible(getWidget("protect_regions"), protectMode === "⚙️ Custom Regions");
|
||||
|
||||
const [w] = node.size;
|
||||
node.setSize([w, node.computeSize()[1]]);
|
||||
node.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Wrap callbacks to react to visibility changes.
|
||||
const wrapCallback = (widgetName) => {
|
||||
const widget = getWidget(widgetName);
|
||||
if (!widget) return;
|
||||
const original = widget.callback;
|
||||
widget.callback = (value, ...args) => {
|
||||
original?.call(widget, value, ...args);
|
||||
updateVisibility();
|
||||
};
|
||||
};
|
||||
|
||||
wrapCallback("variance_preset");
|
||||
wrapCallback("direction_shift");
|
||||
wrapCallback("variance_schedule");
|
||||
wrapCallback("protect_mode");
|
||||
|
||||
updateVisibility();
|
||||
|
||||
// --- LOGIC: EXPORT ---
|
||||
exportBtn.onclick = () => {
|
||||
const settings = {};
|
||||
@@ -79,12 +126,20 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
const presetName = getWidget("variance_preset")?.value || "custom";
|
||||
const saneName = presetName
|
||||
.toString()
|
||||
.replace(/[^a-zA-Z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase() || "preset";
|
||||
const dateStr = new Date().toISOString().slice(0, 16).replace("T", "_").replace(/:/g, "");
|
||||
|
||||
const data = JSON.stringify(settings, null, 2);
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `seed_variance_preset_${new Date().getTime()}.json`;
|
||||
a.download = `rbg_variance_${saneName}_${dateStr}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
@@ -94,7 +149,7 @@ app.registerExtension({
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = ".json";
|
||||
fileInput.style.display = "none";
|
||||
document.body.appendChild(fileInput);
|
||||
container.appendChild(fileInput);
|
||||
|
||||
importBtn.onclick = () => fileInput.click();
|
||||
|
||||
@@ -115,6 +170,7 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
}
|
||||
node.refreshFromWidgets();
|
||||
node.setDirtyCanvas(true, true);
|
||||
} catch (err) {
|
||||
console.error("[RBG Smart Seed Variance] Import failed:", err);
|
||||
@@ -184,26 +240,333 @@ app.registerExtension({
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// --- TOKEN INSPECTOR VISUALIZATION ---
|
||||
const inspectorContainer = document.createElement("div");
|
||||
inspectorContainer.style.width = "100%";
|
||||
inspectorContainer.style.height = "25px";
|
||||
inspectorContainer.style.marginTop = "5px";
|
||||
inspectorContainer.style.marginBottom = "5px";
|
||||
inspectorContainer.style.background = "#222";
|
||||
inspectorContainer.style.borderRadius = "4px";
|
||||
inspectorContainer.style.position = "relative";
|
||||
inspectorContainer.style.overflow = "hidden";
|
||||
inspectorContainer.title = "Token Inspector: Green = Varied, Red = Protected";
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 300; // Will be resized
|
||||
canvas.height = 25;
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
inspectorContainer.appendChild(canvas);
|
||||
|
||||
// Tooltip element
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.style.position = "absolute";
|
||||
tooltip.style.top = "0";
|
||||
tooltip.style.left = "0";
|
||||
tooltip.style.background = "rgba(0,0,0,0.8)";
|
||||
tooltip.style.color = "white";
|
||||
tooltip.style.padding = "2px 5px";
|
||||
tooltip.style.fontSize = "10px";
|
||||
tooltip.style.pointerEvents = "none";
|
||||
tooltip.style.display = "none";
|
||||
tooltip.style.whiteSpace = "nowrap";
|
||||
inspectorContainer.appendChild(tooltip);
|
||||
|
||||
container.appendChild(inspectorContainer);
|
||||
|
||||
// Data storage
|
||||
let protectionMask = [];
|
||||
|
||||
const drawInspector = () => {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (!protectionMask || protectionMask.length === 0) {
|
||||
// Draw "No Data" placeholder
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = "10px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Run to Inspect Tokens", w/2, h/2 + 3);
|
||||
return;
|
||||
}
|
||||
|
||||
const numTokens = protectionMask.length;
|
||||
const tokenWidth = w / numTokens;
|
||||
|
||||
for (let i = 0; i < numTokens; i++) {
|
||||
const isProtected = protectionMask[i];
|
||||
ctx.fillStyle = isProtected ? "#ff4444" : "#44ff44"; // Red for locked, Green for free
|
||||
|
||||
// Draw segment with slight gap
|
||||
const x = i * tokenWidth;
|
||||
const gap = numTokens > 50 ? 0 : 1; // Only show gaps if few tokens
|
||||
ctx.fillRect(x, 0, tokenWidth - gap, h);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial draw
|
||||
drawInspector();
|
||||
|
||||
// Sync with Widgets (for persistence/imports)
|
||||
node.refreshFromWidgets = () => {
|
||||
const modeWidget = node.widgets.find(w => w.name === "protect_mode");
|
||||
const regionsWidget = node.widgets.find(w => w.name === "protect_regions");
|
||||
|
||||
// We need a known token count to draw the bar
|
||||
const numTokens = node.properties.lastTokenCount || 0;
|
||||
if (numTokens <= 0) return;
|
||||
|
||||
const mode = modeWidget ? modeWidget.value : "🚫 None";
|
||||
const regionsStr = regionsWidget ? regionsWidget.value : "";
|
||||
|
||||
// Create a temporary mask based on current UI settings
|
||||
const newMask = new Array(numTokens).fill(0);
|
||||
|
||||
if (mode === "⚙️ Custom Regions") {
|
||||
const set = parseRegions(regionsStr);
|
||||
set.forEach(idx => {
|
||||
if (idx < numTokens) newMask[idx] = 1;
|
||||
});
|
||||
} else if (mode === "🎲 Random Regions") {
|
||||
const seedWidget = node.widgets.find(w => w.name === "seed");
|
||||
const seed = seedWidget ? seedWidget.value : 0;
|
||||
// Simple LCG for preview consistency (approximate backend logic)
|
||||
let s = seed ^ 0x5EED;
|
||||
for (let i = 0; i < numTokens; i++) {
|
||||
// Basic linear congruential generator
|
||||
s = (Math.imul(s, 1664525) + 1013904223) | 0;
|
||||
if (Math.abs(s % 100) < 30) newMask[i] = 1;
|
||||
}
|
||||
} else if (mode !== "🚫 None") {
|
||||
// Approximate legacy modes for UI preview
|
||||
// In Python: protect_config = self.PROTECT_OPTIONS.get(mode, (0.0, "start"))
|
||||
const opts = {
|
||||
"First Quarter": [0.25, "start"],
|
||||
"First Half": [0.5, "start"],
|
||||
"Last Quarter": [0.25, "end"],
|
||||
"Last Half": [0.5, "end"]
|
||||
};
|
||||
if (opts[mode]) {
|
||||
const [frac, pos] = opts[mode];
|
||||
const count = Math.floor(numTokens * frac);
|
||||
if (pos === "start") {
|
||||
for (let i = 0; i < count; i++) newMask[i] = 1;
|
||||
} else {
|
||||
for (let i = numTokens - count; i < numTokens; i++) newMask[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protectionMask = newMask;
|
||||
drawInspector();
|
||||
};
|
||||
|
||||
// Helper: Convert "0-5, 10" string to Set of indices
|
||||
const parseRegions = (str) => {
|
||||
const set = new Set();
|
||||
if (!str) return set;
|
||||
str.split(",").forEach(part => {
|
||||
part = part.trim();
|
||||
if (part.includes("-")) {
|
||||
const [start, end] = part.split("-").map(n => parseInt(n));
|
||||
if (!isNaN(start) && !isNaN(end)) {
|
||||
for (let i = start; i <= end; i++) set.add(i);
|
||||
}
|
||||
} else {
|
||||
const n = parseInt(part);
|
||||
if (!isNaN(n)) set.add(n);
|
||||
}
|
||||
});
|
||||
return set;
|
||||
};
|
||||
|
||||
// Helper: Convert Set of indices to optimized "0-5, 10" string
|
||||
const regionsToString = (set) => {
|
||||
const sorted = Array.from(set).sort((a, b) => a - b);
|
||||
const ranges = [];
|
||||
let start = null, prev = null;
|
||||
|
||||
for (const idx of sorted) {
|
||||
if (start === null) { start = idx; prev = idx; continue; }
|
||||
if (idx === prev + 1) { prev = idx; continue; }
|
||||
ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||
start = idx; prev = idx;
|
||||
}
|
||||
if (start !== null) ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||
return ranges.join(",");
|
||||
};
|
||||
|
||||
// Handle Click to Toggle Protection
|
||||
canvas.onclick = (e) => {
|
||||
if (!protectionMask.length) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const tokenIndex = Math.floor((x / rect.width) * protectionMask.length);
|
||||
|
||||
if (tokenIndex >= 0 && tokenIndex < protectionMask.length) {
|
||||
// 1. Get Widgets
|
||||
const modeWidget = node.widgets.find(w => w.name === "protect_mode");
|
||||
const regionsWidget = node.widgets.find(w => w.name === "protect_regions");
|
||||
|
||||
if (!regionsWidget) return;
|
||||
|
||||
// 2. Parse Current State
|
||||
// If we are NOT in Custom Mode, we should probably start from scratch or the visualized state?
|
||||
// Better to start from the *visualized* state (protectionMask) because that matches what the user sees.
|
||||
// But wait, protectionMask comes from the last run.
|
||||
// If user changes mode to "First Half" but hasn't run, bar is old.
|
||||
// Let's assume bar is current.
|
||||
// Actually, safer to read the protectionMask itself as the source of truth for the *current* set of protected tokens,
|
||||
// then toggle the clicked one.
|
||||
|
||||
const currentSet = new Set();
|
||||
protectionMask.forEach((isProtected, idx) => {
|
||||
if (isProtected) currentSet.add(idx);
|
||||
});
|
||||
|
||||
// 3. Toggle
|
||||
if (currentSet.has(tokenIndex)) {
|
||||
currentSet.delete(tokenIndex);
|
||||
protectionMask[tokenIndex] = 0; // Optimistic update
|
||||
} else {
|
||||
currentSet.add(tokenIndex);
|
||||
protectionMask[tokenIndex] = 1; // Optimistic update
|
||||
}
|
||||
|
||||
// 4. Update UI
|
||||
const newString = regionsToString(currentSet);
|
||||
regionsWidget.value = newString;
|
||||
|
||||
// Force Mode to Custom
|
||||
if (modeWidget && modeWidget.value !== "⚙️ Custom Regions") {
|
||||
modeWidget.value = "⚙️ Custom Regions";
|
||||
}
|
||||
|
||||
// Trigger updates
|
||||
drawInspector(); // Redraw bar immediately
|
||||
node.setDirtyCanvas(true, true); // Mark node as needing execution (optional, mostly for style)
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Mouse Hover for Tooltip
|
||||
canvas.onmousemove = (e) => {
|
||||
if (!protectionMask.length) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const tokenIndex = Math.floor((x / rect.width) * protectionMask.length);
|
||||
|
||||
if (tokenIndex >= 0 && tokenIndex < protectionMask.length) {
|
||||
const isProtected = protectionMask[tokenIndex];
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.left = `${Math.min(x + 10, rect.width - 80)}px`;
|
||||
// Tooltip text
|
||||
tooltip.textContent = `Token ${tokenIndex}: ${isProtected ? "Protected 🔒" : "Varied 🎲"} (Click to Toggle)`;
|
||||
tooltip.style.color = isProtected ? "#ffaaaa" : "#aaffaa";
|
||||
canvas.style.cursor = "pointer";
|
||||
}
|
||||
};
|
||||
|
||||
canvas.onmouseleave = () => {
|
||||
tooltip.style.display = "none";
|
||||
canvas.style.cursor = "default";
|
||||
};
|
||||
|
||||
// --- VIBE_BLEND VISIBILITY ---
|
||||
// vibe_blend is only meaningful when target_vibe is connected.
|
||||
// We watch the input slot connection state and show/hide accordingly.
|
||||
function updateVibeBlendVisibility() {
|
||||
const vibeBlendWidget = node.widgets?.find(w => w.name === "vibe_blend");
|
||||
if (!vibeBlendWidget) return;
|
||||
|
||||
// Find the target_vibe input slot by name
|
||||
const targetVibeInput = node.inputs?.find(inp => inp.name === "target_vibe");
|
||||
const isConnected = targetVibeInput?.link != null;
|
||||
|
||||
vibeBlendWidget.hidden = !isConnected;
|
||||
if (vibeBlendWidget.element) {
|
||||
vibeBlendWidget.element.style.display = isConnected ? "" : "none";
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// Hook into connection changes so visibility updates live
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
||||
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
||||
onConnectionsChange?.apply(this, arguments);
|
||||
updateVibeBlendVisibility();
|
||||
};
|
||||
|
||||
// Handle Execution Data
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
if (message && message.protection_data) {
|
||||
// protection_data is an array of masks (one per batch item). Take the first one.
|
||||
const mask = message.protection_data[0];
|
||||
if (mask) {
|
||||
protectionMask = mask;
|
||||
node.properties.lastTokenCount = mask.length;
|
||||
drawInspector();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Workflow load persistence
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function() {
|
||||
onConfigure?.apply(this, arguments);
|
||||
setTimeout(() => {
|
||||
this.refreshFromWidgets();
|
||||
updateVibeBlendVisibility();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const onRemoved = nodeType.prototype.onRemoved;
|
||||
nodeType.prototype.onRemoved = function() {
|
||||
onRemoved?.apply(this, arguments);
|
||||
importBtn.onclick = null;
|
||||
fileInput.onchange = null;
|
||||
if (fileInput.parentElement) {
|
||||
fileInput.parentElement.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
// Add to node
|
||||
const widget = this.addDOMWidget("presets_buttons", "div", container);
|
||||
widget.serialize = false;
|
||||
|
||||
// Ensure the node has enough height to contain the buttons
|
||||
// Ensure the node has enough height to contain the buttons + inspector
|
||||
const originalComputeSize = this.computeSize;
|
||||
this.computeSize = function (width) {
|
||||
const size = originalComputeSize ? originalComputeSize.apply(this, arguments) : [width || 300, 200];
|
||||
// Use a comfortable default width of 300px if it's smaller
|
||||
const finalWidth = Math.max(size[0], 300);
|
||||
// Tightened height: Using the user's preferred 50px offset
|
||||
size[1] += 45;
|
||||
|
||||
// Update canvas resolution on resize for crisp rendering
|
||||
if (canvas.width !== finalWidth) {
|
||||
canvas.width = finalWidth;
|
||||
drawInspector();
|
||||
}
|
||||
|
||||
size[1] += 80; // height for buttons + inspector (45 + 35)
|
||||
return [finalWidth, size[1]];
|
||||
};
|
||||
|
||||
// Trigger a resize to apply the new computeSize logic
|
||||
// Also run initial vibe_blend visibility check
|
||||
setTimeout(() => {
|
||||
this.setSize(this.computeSize());
|
||||
updateVibeBlendVisibility();
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user