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:
Ramon Guthrie
2026-07-01 11:48:08 +01:00
committed by GitHub
parent 1506b69391
commit 8f8621464a
3 changed files with 947 additions and 374 deletions
File diff suppressed because it is too large Load Diff
+370 -7
View File
@@ -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