Files
PozzettiAndrea c0208a18a9 Serve frontend JS from javascript/
Move web/ to javascript/ and declare it via [tool.comfy] web so the served
directory is explicit rather than positional.

comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no
"gpu" key -- the accelerator is named by backend, and the old spelling parsed to
an empty list, so CUDA runners silently ran the full unfiltered set), an
explicit [test.platforms] platforms allowlist (per-platform booleans are no
longer supported), and the javascript level.
2026-08-22 19:44:03 +02:00

725 lines
26 KiB
JavaScript

/**
* ComfyUI-Grounding Dynamic Parameter Management
*
* This extension manages the visibility of model-specific parameters
* based on the selected model in both GroundingModelLoader and GroundingDetector nodes.
*/
import { app } from "../../../scripts/app.js";
const DEBUG = true; // Set to true to enable console logging
function log(...args) {
if (DEBUG) {
console.log("[ComfyUI-Grounding]", ...args);
}
}
// Helper function to detect model type from model name
function getModelType(modelName) {
if (modelName.startsWith("GroundingDINO:") || modelName.startsWith("MM-GroundingDINO:")) {
return "grounding_dino";
}
if (modelName.startsWith("OWLv2:")) {
return "owlv2";
}
if (modelName.startsWith("Florence-2:")) {
return "florence2";
}
if (modelName.startsWith("YOLO-World:")) {
return "yolo_world";
}
return "unknown";
}
// Helper function to hide a widget (NUCLEAR OPTION: removes from array)
function hideWidget(node, widget, suffix = "") {
if (!widget) return;
// If already hidden, skip
if (widget._hidden) {
log("Widget already hidden:", widget.name);
return;
}
log("Hiding widget:", widget.name, "current type:", widget.type);
// Find widget's current index in the widgets array
const index = node.widgets.indexOf(widget);
if (index === -1) {
log(" ERROR: Widget not found in node.widgets array!");
return;
}
log(" Found widget at index:", index);
// Store original properties for restoration
if (!widget.origType) {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
}
// Store the original index and mark as hidden
widget._originalIndex = index;
widget._hidden = true;
// NUCLEAR OPTION: Remove widget from the array entirely
node.widgets.splice(index, 1);
log(" Widget removed from array at index:", index);
log(" Remaining widgets:", node.widgets.length);
// Hide linked widgets if any
if (widget.linkedWidgets) {
widget.linkedWidgets.forEach(w => hideWidget(node, w, ":hidden"));
}
}
// Helper function to show a widget (NUCLEAR OPTION: re-inserts into array)
function showWidget(node, widget) {
if (!widget) return;
// If not hidden, skip
if (!widget._hidden) {
log("Widget already visible:", widget.name);
return;
}
log("Showing widget:", widget.name);
// Restore original properties
if (widget.origType) {
widget.type = widget.origType;
widget.computeSize = widget.origComputeSize;
if (widget.origSerializeValue) {
widget.serializeValue = widget.origSerializeValue;
}
}
// NUCLEAR OPTION: Re-insert widget into array at original position
const targetIndex = widget._originalIndex;
// Make sure we don't insert beyond array bounds
const insertIndex = Math.min(targetIndex, node.widgets.length);
node.widgets.splice(insertIndex, 0, widget);
log(" Widget restored to array at index:", insertIndex);
log(" Total widgets:", node.widgets.length);
// Clear hidden flag
widget._hidden = false;
// Show linked widgets if any
if (widget.linkedWidgets) {
widget.linkedWidgets.forEach(w => showWidget(node, w));
}
}
// Main extension registration
app.registerExtension({
name: "grounding.dynamic_parameters",
async nodeCreated(node) {
// Handle GroundingModelLoader node
if (node.comfyClass === "GroundingModelLoader") {
// Use setTimeout to ensure widgets are fully initialized
// Increased delay to 100ms for better reliability across different systems
setTimeout(() => {
this.setupModelLoader(node);
}, 100);
}
// Handle GroundingDetector node
if (node.comfyClass === "GroundingDetector") {
setTimeout(() => {
this.setupDetector(node);
}, 100);
}
// Handle GroundingMaskModelLoader node
if (node.comfyClass === "GroundingMaskModelLoader") {
setTimeout(() => {
this.setupMaskModelLoader(node);
}, 100);
}
// Handle GroundingMaskDetector node
if (node.comfyClass === "GroundingMaskDetector") {
setTimeout(() => {
this.setupMaskDetector(node);
}, 100);
}
},
setupModelLoader(node) {
log("Setting up GroundingModelLoader node");
// Find the model widget
const modelWidget = node.widgets?.find(w => w.name === "model");
if (!modelWidget) {
log("ERROR: Model widget not found!");
log("Available widgets:", node.widgets?.map(w => w.name));
return;
}
log("Model widget found, current value:", modelWidget.value);
// Find all parameter widgets (load-time only)
const groundingDinoAttnWidget = node.widgets?.find(w => w.name === "grounding_dino_attn");
const florence2AttnWidget = node.widgets?.find(w => w.name === "florence2_attn");
log("Found parameter widgets:", {
grounding_dino_attn: !!groundingDinoAttnWidget,
florence2_attn: !!florence2AttnWidget
});
// Function to update widget visibility based on selected model
const updateWidgetVisibility = (selectedModel) => {
const modelType = getModelType(selectedModel);
log("Updating widget visibility for model:", selectedModel, "type:", modelType);
// GroundingDINO parameters (load-time)
if (groundingDinoAttnWidget) {
if (modelType === "grounding_dino") {
showWidget(node, groundingDinoAttnWidget);
} else {
hideWidget(node, groundingDinoAttnWidget);
}
}
// Florence-2 parameters (load-time)
if (florence2AttnWidget) {
if (modelType === "florence2") {
showWidget(node, florence2AttnWidget);
} else {
hideWidget(node, florence2AttnWidget);
}
}
// Force canvas redraw immediately to update hidden state
node.setDirtyCanvas(true, true);
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
// CRITICAL: Delay size recalculation to prevent last widget from re-appearing
// This ensures widget hiding completes before layout recalculation
requestAnimationFrame(() => {
// Recalculate node size after widgets are fully hidden
const newSize = node.computeSize();
node.setSize([node.size[0], newSize[1]]);
// Mark as dirty again after resize
node.setDirtyCanvas(true, true);
if (app.canvas) {
app.canvas.setDirty(true, true);
}
// Final redraw after everything settles
requestAnimationFrame(() => {
if (app.canvas) {
app.canvas.draw(true, true);
}
log("Widget visibility update complete");
});
});
};
// Store original callback
const origCallback = modelWidget.callback;
// Override callback to update visibility when model changes
modelWidget.callback = function(value) {
log("Model changed to:", value);
// Call original callback if it exists
const result = origCallback?.apply(this, arguments);
// Update widget visibility
updateWidgetVisibility(value);
return result;
};
// Initialize visibility on node creation
log("Initializing widget visibility...");
updateWidgetVisibility(modelWidget.value);
},
setupDetector(node) {
log("Setting up GroundingDetector node");
// Find all model-specific parameter widgets (inference-time only)
const textThresholdWidget = node.widgets?.find(w => w.name === "text_threshold");
const florence2MaxTokensWidget = node.widgets?.find(w => w.name === "florence2_max_tokens");
const florence2NumBeamsWidget = node.widgets?.find(w => w.name === "florence2_num_beams");
const yoloIouWidget = node.widgets?.find(w => w.name === "yolo_iou");
const yoloAgnosticNmsWidget = node.widgets?.find(w => w.name === "yolo_agnostic_nms");
const yoloMaxDetWidget = node.widgets?.find(w => w.name === "yolo_max_det");
log("Found detector parameter widgets:", {
text_threshold: !!textThresholdWidget,
florence2_max_tokens: !!florence2MaxTokensWidget,
florence2_num_beams: !!florence2NumBeamsWidget,
yolo_iou: !!yoloIouWidget,
yolo_agnostic_nms: !!yoloAgnosticNmsWidget,
yolo_max_det: !!yoloMaxDetWidget
});
// Function to update widget visibility based on connected model
const updateDetectorWidgets = (modelType) => {
log("Updating detector widgets for model type:", modelType);
// GroundingDINO parameters (text_threshold)
if (textThresholdWidget) {
if (modelType === "grounding_dino") {
textThresholdWidget.value = 0.25; // Reset to default
showWidget(node, textThresholdWidget);
} else {
hideWidget(node, textThresholdWidget);
}
}
// Florence-2 parameters (inference-time only)
if (florence2MaxTokensWidget) {
if (modelType === "florence2") {
florence2MaxTokensWidget.value = 1024; // Reset to default
showWidget(node, florence2MaxTokensWidget);
} else {
hideWidget(node, florence2MaxTokensWidget);
}
}
if (florence2NumBeamsWidget) {
if (modelType === "florence2") {
florence2NumBeamsWidget.value = 3; // Reset to default
showWidget(node, florence2NumBeamsWidget);
} else {
hideWidget(node, florence2NumBeamsWidget);
}
}
// YOLO-World parameters
if (yoloIouWidget) {
if (modelType === "yolo_world") {
yoloIouWidget.value = 0.45; // Reset to default
showWidget(node, yoloIouWidget);
} else {
hideWidget(node, yoloIouWidget);
}
}
if (yoloAgnosticNmsWidget) {
if (modelType === "yolo_world") {
yoloAgnosticNmsWidget.value = false; // Reset to default
showWidget(node, yoloAgnosticNmsWidget);
} else {
hideWidget(node, yoloAgnosticNmsWidget);
}
}
if (yoloMaxDetWidget) {
if (modelType === "yolo_world") {
yoloMaxDetWidget.value = 300; // Reset to default
showWidget(node, yoloMaxDetWidget);
} else {
hideWidget(node, yoloMaxDetWidget);
}
}
// Force canvas redraw
node.setDirtyCanvas(true, true);
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
requestAnimationFrame(() => {
const newSize = node.computeSize();
node.setSize([node.size[0], newSize[1]]);
node.setDirtyCanvas(true, true);
if (app.canvas) {
app.canvas.setDirty(true, true);
}
requestAnimationFrame(() => {
if (app.canvas) {
app.canvas.draw(true, true);
}
log("Detector widget visibility update complete");
});
});
};
// Function to get model type from connected loader
const getConnectedModelType = () => {
// Find the model input (slot 0)
const modelInput = node.inputs?.find(input => input.name === "model");
if (!modelInput || !modelInput.link) {
log("No model connected to detector");
return null;
}
// Get the connected link
const link = app.graph.links[modelInput.link];
if (!link) {
log("Link not found");
return null;
}
// Get the connected node (the loader)
const loaderNode = app.graph.getNodeById(link.origin_id);
if (!loaderNode || loaderNode.comfyClass !== "GroundingModelLoader") {
log("Connected node is not a GroundingModelLoader");
return null;
}
// Get the model widget value from the loader
const loaderModelWidget = loaderNode.widgets?.find(w => w.name === "model");
if (!loaderModelWidget) {
log("Model widget not found in loader");
return null;
}
const selectedModel = loaderModelWidget.value;
log("Connected loader has model:", selectedModel);
return getModelType(selectedModel);
};
// Update widgets when a connection is made
const origOnConnectionsChange = node.onConnectionsChange;
node.onConnectionsChange = function(type, index, connected, link_info) {
log("Detector connection changed:", type, index, connected);
if (origOnConnectionsChange) {
origOnConnectionsChange.apply(this, arguments);
}
// If model input was connected/disconnected
if (type === 1 && index === 0) { // type 1 = input, index 0 = model input
if (connected) {
const modelType = getConnectedModelType();
if (modelType) {
updateDetectorWidgets(modelType);
}
} else {
// Disconnected - show all widgets? Or hide all? Let's hide all for now
log("Model disconnected, hiding all model-specific widgets");
updateDetectorWidgets("unknown");
}
}
};
// Check if there's already a connection and update accordingly
setTimeout(() => {
const modelType = getConnectedModelType();
if (modelType) {
log("Initial detector setup with connected model type:", modelType);
updateDetectorWidgets(modelType);
} else {
log("No initial connection, hiding all model-specific widgets");
updateDetectorWidgets("unknown");
}
}, 150);
// Also listen for changes in the connected loader's model selection
// We need to hook into graph events for this
const checkLoaderChanges = setInterval(() => {
if (!node.graph) {
clearInterval(checkLoaderChanges);
return;
}
const modelType = getConnectedModelType();
if (modelType && modelType !== node._lastModelType) {
log("Detected model type change in connected loader:", modelType);
node._lastModelType = modelType;
updateDetectorWidgets(modelType);
}
}, 500);
// Clean up interval when node is removed
const origOnRemoved = node.onRemoved;
node.onRemoved = function() {
clearInterval(checkLoaderChanges);
if (origOnRemoved) {
origOnRemoved.apply(this, arguments);
}
};
},
setupMaskModelLoader(node) {
log("Setting up GroundingMaskModelLoader node");
// Find the model widget
const modelWidget = node.widgets?.find(w => w.name === "model");
if (!modelWidget) {
log("ERROR: Model widget not found!");
log("Available widgets:", node.widgets?.map(w => w.name));
return;
}
log("Mask model widget found, current value:", modelWidget.value);
// Find attention parameter widget
const florence2AttnWidget = node.widgets?.find(w => w.name === "florence2_attn");
log("Found parameter widgets:", {
florence2_attn: !!florence2AttnWidget
});
// Function to update widget visibility based on selected model
const updateWidgetVisibility = (selectedModel) => {
const modelType = getModelType(selectedModel);
log("Updating mask model widget visibility for model:", selectedModel, "type:", modelType);
// Florence-2 parameters (load-time)
if (florence2AttnWidget) {
if (modelType === "florence2") {
showWidget(node, florence2AttnWidget);
} else {
hideWidget(node, florence2AttnWidget);
}
}
// Force canvas redraw
node.setDirtyCanvas(true, true);
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
requestAnimationFrame(() => {
const newSize = node.computeSize();
node.setSize([node.size[0], newSize[1]]);
node.setDirtyCanvas(true, true);
if (app.canvas) {
app.canvas.setDirty(true, true);
}
requestAnimationFrame(() => {
if (app.canvas) {
app.canvas.draw(true, true);
}
log("Mask model widget visibility update complete");
});
});
};
// Store original callback
const origCallback = modelWidget.callback;
// Override callback to update visibility when model changes
modelWidget.callback = function(value) {
log("Mask model changed to:", value);
// Call original callback if it exists
const result = origCallback?.apply(this, arguments);
// Update widget visibility
updateWidgetVisibility(value);
return result;
};
// Initialize visibility on node creation
log("Initializing mask model widget visibility...");
updateWidgetVisibility(modelWidget.value);
},
setupMaskDetector(node) {
log("Setting up GroundingMaskDetector node");
// Find all model-specific parameter widgets
const florence2MaxTokensWidget = node.widgets?.find(w => w.name === "florence2_max_tokens");
const florence2NumBeamsWidget = node.widgets?.find(w => w.name === "florence2_num_beams");
const sa2vaMaxTokensWidget = node.widgets?.find(w => w.name === "sa2va_max_tokens");
const sa2vaNumBeamsWidget = node.widgets?.find(w => w.name === "sa2va_num_beams");
log("Found mask detector parameter widgets:", {
florence2_max_tokens: !!florence2MaxTokensWidget,
florence2_num_beams: !!florence2NumBeamsWidget,
sa2va_max_tokens: !!sa2vaMaxTokensWidget,
sa2va_num_beams: !!sa2vaNumBeamsWidget
});
// Function to update widget visibility based on connected model
const updateMaskDetectorWidgets = (modelType) => {
log("Updating mask detector widgets for model type:", modelType);
// Florence-2 Seg parameters
if (florence2MaxTokensWidget) {
if (modelType === "florence2") {
florence2MaxTokensWidget.value = 1024; // Reset to default
showWidget(node, florence2MaxTokensWidget);
} else {
hideWidget(node, florence2MaxTokensWidget);
}
}
if (florence2NumBeamsWidget) {
if (modelType === "florence2") {
florence2NumBeamsWidget.value = 3; // Reset to default
showWidget(node, florence2NumBeamsWidget);
} else {
hideWidget(node, florence2NumBeamsWidget);
}
}
// SA2VA parameters
if (sa2vaMaxTokensWidget) {
if (modelType === "sa2va") {
sa2vaMaxTokensWidget.value = 2048; // Reset to default
showWidget(node, sa2vaMaxTokensWidget);
} else {
hideWidget(node, sa2vaMaxTokensWidget);
}
}
if (sa2vaNumBeamsWidget) {
if (modelType === "sa2va") {
sa2vaNumBeamsWidget.value = 1; // Reset to default
showWidget(node, sa2vaNumBeamsWidget);
} else {
hideWidget(node, sa2vaNumBeamsWidget);
}
}
// Force canvas redraw
node.setDirtyCanvas(true, true);
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
requestAnimationFrame(() => {
const newSize = node.computeSize();
node.setSize([node.size[0], newSize[1]]);
node.setDirtyCanvas(true, true);
if (app.canvas) {
app.canvas.setDirty(true, true);
}
requestAnimationFrame(() => {
if (app.canvas) {
app.canvas.draw(true, true);
}
log("Mask detector widget visibility update complete");
});
});
};
// Helper function to detect SA2VA model type
const getMaskModelType = (modelName) => {
if (modelName.startsWith("Florence-2:")) {
return "florence2";
}
if (modelName.startsWith("SA2VA:")) {
return "sa2va";
}
return "unknown";
};
// Function to get model type from connected mask loader
const getConnectedMaskModelType = () => {
// Find the model input (slot 0)
const modelInput = node.inputs?.find(input => input.name === "model");
if (!modelInput || !modelInput.link) {
log("No model connected to mask detector");
return null;
}
// Get the connected link
const link = app.graph.links[modelInput.link];
if (!link) {
log("Link not found");
return null;
}
// Get the connected node (the loader)
const loaderNode = app.graph.getNodeById(link.origin_id);
if (!loaderNode || loaderNode.comfyClass !== "GroundingMaskModelLoader") {
log("Connected node is not a GroundingMaskModelLoader");
return null;
}
// Get the model widget value from the loader
const loaderModelWidget = loaderNode.widgets?.find(w => w.name === "model");
if (!loaderModelWidget) {
log("Model widget not found in loader");
return null;
}
const selectedModel = loaderModelWidget.value;
log("Connected mask loader has model:", selectedModel);
return getMaskModelType(selectedModel);
};
// Update widgets when a connection is made
const origOnConnectionsChange = node.onConnectionsChange;
node.onConnectionsChange = function(type, index, connected, link_info) {
log("Mask detector connection changed:", type, index, connected);
if (origOnConnectionsChange) {
origOnConnectionsChange.apply(this, arguments);
}
// If model input was connected/disconnected
if (type === 1 && index === 0) { // type 1 = input, index 0 = model input
if (connected) {
const modelType = getConnectedMaskModelType();
if (modelType) {
updateMaskDetectorWidgets(modelType);
}
} else {
// Disconnected - hide all model-specific widgets
log("Model disconnected, hiding all model-specific widgets");
updateMaskDetectorWidgets("unknown");
}
}
};
// Check if there's already a connection and update accordingly
setTimeout(() => {
const modelType = getConnectedMaskModelType();
if (modelType) {
log("Initial mask detector setup with connected model type:", modelType);
updateMaskDetectorWidgets(modelType);
} else {
log("No initial connection, hiding all model-specific widgets");
updateMaskDetectorWidgets("unknown");
}
}, 150);
// Also listen for changes in the connected loader's model selection
const checkLoaderChanges = setInterval(() => {
if (!node.graph) {
clearInterval(checkLoaderChanges);
return;
}
const modelType = getConnectedMaskModelType();
if (modelType && modelType !== node._lastModelType) {
log("Detected model type change in connected mask loader:", modelType);
node._lastModelType = modelType;
updateMaskDetectorWidgets(modelType);
}
}, 500);
// Clean up interval when node is removed
const origOnRemoved = node.onRemoved;
node.onRemoved = function() {
clearInterval(checkLoaderChanges);
if (origOnRemoved) {
origOnRemoved.apply(this, arguments);
}
};
log("GroundingMaskDetector setup complete");
}
});
log("ComfyUI-Grounding dynamic parameters extension loaded");