Release v2.0: Fix Prompt Machine technical input box, connector alignment, and preview masking
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# SATA Utility Node Suite for ComfyUI
|
||||
|
||||
**Author:** Satadal
|
||||
**Version:** 1.5.0
|
||||
**Version:** 2.0
|
||||
**License:** MIT
|
||||
**Repository:** https://github.com/SatadalAI/SATA_UtilityNode
|
||||
|
||||
@@ -13,7 +13,7 @@ SATA Utility Node Suite is a collection of custom nodes for [ComfyUI](https://gi
|
||||
It includes:
|
||||
|
||||
- **Upscale Machine:** High-quality model-based image upscaling with Frequency-Split SR and Blue Noise injection.
|
||||
- **Prompt Machine (Six-Slot):** Professional structured prompting with JSON output support.
|
||||
- **Prompt Machine (Eight-Category Framework):** Professional structured prompting with JSON output support.
|
||||
- **Prompt Style Machine:** Easy prompt selection from a CSV for positive/negative prompt workflows.
|
||||
- **Touchpad Pan & Zoom:** Seamless navigation for large images in the ComfyUI preview panel.
|
||||
- **Resolution Machine:** Quick selection of model-specific image resolutions with intelligent Megapixel scaling.
|
||||
@@ -35,12 +35,12 @@ It includes:
|
||||
- **CPU-based Rescaling:** Efficient resizing using OpenCV.
|
||||
- **Rounding Modulus:** Ensures output dimensions seamlessly align with UNet architectural constraints.
|
||||
|
||||
### 💬 Prompt Machine (Six-Slot Framework)
|
||||
### 💬 Prompt Machine (Eight-Category Framework)
|
||||
|
||||
- **Structured Prompting:** Dedicated input boxes for `subject`, `style`, `lighting`, `composition`, `mood`, and `technical` details to prevent "word salad".
|
||||
- **Structured Prompting:** Dedicated input boxes for `subject`, `pose_and_expression`, `secondary_subject`, `background_and_environment`, `lighting`, `style`, `creative_hook`, and `technical` details following an inside-out spatial sequence.
|
||||
- **JSON Output Mode:** Toggle `json_output` to compile the slots into a strict JSON string, perfect for piping into LLM rewriting nodes or API pipelines.
|
||||
- **Autocomplete Ready:** Fully integrated with the autocomplete system—just type `#` in any slot to trigger the smart tag popup!
|
||||
- **Natural Language & Comma Separated:** Dynamically formats outputs for FLUX (Natural Language) or SDXL (Comma Separated).
|
||||
- **Universal Model Compatibility:** Formats naturally for FLUX, SD3.5, SDXL, SD 1.5, PixArt, and LLM expansion pipelines.
|
||||
|
||||
### 💬 Prompt Style Machine (Legacy)
|
||||
|
||||
|
||||
@@ -665,16 +665,7 @@
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "technical",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "technical"
|
||||
},
|
||||
"link": 117
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "prompt",
|
||||
@@ -694,11 +685,13 @@
|
||||
},
|
||||
"widgets_values": [
|
||||
"a lady walking down the street with her little daughter in a rainy day. both are wearing multicolored dress.",
|
||||
"mother has smiling expression on her face and daughter has sprakling eyes with a cute smile.",
|
||||
"the little girl has jumped to a stagnent water. mother is holding one hand of her daughter. mother has a side bag on her shoulder and a closed umbrella holding in her hand. ",
|
||||
"a modern photograph of a rainy day.",
|
||||
"gloomy daylight.",
|
||||
"the little girl has jumped to a stagnent water. mother is holding one hand of her daughter. mother has a side bag on her shoulder and a closed umbrella holding in her hand. ",
|
||||
"mother has smiling expression on her face and daughter has sprakling eyes with a cute smile.",
|
||||
"",
|
||||
"Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.",
|
||||
"puddles reflecting neon streetlights and rainy sky.",
|
||||
"Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.",
|
||||
false
|
||||
]
|
||||
},
|
||||
@@ -721,9 +714,7 @@
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
117
|
||||
]
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -916,14 +907,6 @@
|
||||
11,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
117,
|
||||
68,
|
||||
0,
|
||||
67,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
|
||||
+308
-85
@@ -8,6 +8,96 @@ const SAVE_MACHINE_CLASS = "Save_Machine";
|
||||
// Track hover state
|
||||
const hoveredNodeIds = new Set();
|
||||
|
||||
// Inject stylesheet for Nodes 2.0 (Vue DOM) and Legacy V1 DOM preview masking
|
||||
function injectHiderCSS() {
|
||||
if (document.getElementById("sata-preview-hider-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "sata-preview-hider-styles";
|
||||
style.textContent = `
|
||||
/* Mask only the image container itself in-place */
|
||||
.sata-image-masked {
|
||||
position: relative !important;
|
||||
min-height: 100px !important;
|
||||
background-color: #1a1a1a !important;
|
||||
border-radius: 6px !important;
|
||||
border: 1px dashed #444 !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow: hidden !important;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
/* Hide all img/canvas/video children inside the masked image container */
|
||||
.sata-image-masked img,
|
||||
.sata-image-masked canvas,
|
||||
.sata-image-masked video {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
/* Center placeholder text cleanly over the masked image container */
|
||||
.sata-image-masked::after {
|
||||
content: "Hidden (Hover to Reveal)" !important;
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
color: #888 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: bold !important;
|
||||
font-family: sans-serif !important;
|
||||
text-align: center !important;
|
||||
white-space: nowrap !important;
|
||||
pointer-events: none !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
/* NATIVE CSS HOVER REVEAL: Works across Nodes 2.0 Vue Nodes & Legacy LiteGraph DOM */
|
||||
.sata-image-masked:hover img,
|
||||
.sata-image-masked:hover canvas,
|
||||
.sata-image-masked:hover video,
|
||||
[data-node-id]:hover .sata-image-masked img,
|
||||
[data-node-id]:hover .sata-image-masked canvas,
|
||||
[data-node-id]:hover .sata-image-masked video,
|
||||
.comfy-vue-node:hover .sata-image-masked img,
|
||||
.comfy-vue-node:hover .sata-image-masked canvas,
|
||||
.comfy-vue-node:hover .sata-image-masked video,
|
||||
.node:hover .sata-image-masked img,
|
||||
.node:hover .sata-image-masked canvas,
|
||||
.node:hover .sata-image-masked video,
|
||||
.lgraphnode:hover .sata-image-masked img,
|
||||
.lgraphnode:hover .sata-image-masked canvas,
|
||||
.lgraphnode:hover .sata-image-masked video {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.sata-image-masked:hover::after,
|
||||
[data-node-id]:hover .sata-image-masked::after,
|
||||
.comfy-vue-node:hover .sata-image-masked::after,
|
||||
.node:hover .sata-image-masked::after,
|
||||
.lgraphnode:hover .sata-image-masked::after {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.sata-image-masked:hover,
|
||||
[data-node-id]:hover .sata-image-masked,
|
||||
.comfy-vue-node:hover .sata-image-masked,
|
||||
.node:hover .sata-image-masked,
|
||||
.lgraphnode:hover .sata-image-masked {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Helper to get settings for both ComfyUI V1 and V2
|
||||
function getSettingValue(id, defaultValue) {
|
||||
if (app.extensionManager && app.extensionManager.setting) {
|
||||
@@ -35,6 +125,7 @@ app.registerExtension({
|
||||
],
|
||||
|
||||
async setup() {
|
||||
injectHiderCSS();
|
||||
setupGlobalMouseTracker();
|
||||
},
|
||||
|
||||
@@ -52,6 +143,7 @@ app.registerExtension({
|
||||
});
|
||||
|
||||
function isTargetNode(node) {
|
||||
if (!node || !node.comfyClass) return false;
|
||||
return node.comfyClass === "PreviewImage" ||
|
||||
node.comfyClass === "SaveImage" ||
|
||||
node.comfyClass === PREVIEW_MACHINE_CLASS ||
|
||||
@@ -60,26 +152,25 @@ function isTargetNode(node) {
|
||||
|
||||
function shouldHideNode(node) {
|
||||
const globalHide = getSettingValue(GLOBAL_HIDE_SETTING_ID, false);
|
||||
if (globalHide) return true;
|
||||
|
||||
if (node.comfyClass === PREVIEW_MACHINE_CLASS) {
|
||||
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
|
||||
if (hideWidget) {
|
||||
return !!hideWidget.value;
|
||||
}
|
||||
|
||||
if (node.comfyClass === PREVIEW_MACHINE_CLASS || node.comfyClass === SAVE_MACHINE_CLASS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.comfyClass === SAVE_MACHINE_CLASS) {
|
||||
const w = node.widgets?.find(w => w.name === "hide_preview");
|
||||
if (w) {
|
||||
if (w.value === true) return true;
|
||||
if (w.value === false && !globalHide) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return globalHide;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMouseOverNode(node) {
|
||||
if (!app.canvas || !app.canvas.graph_mouse) return false;
|
||||
if (!node || !node.pos || !node.size) return false;
|
||||
if (app.canvas && app.canvas.node_over === node) return true;
|
||||
|
||||
if (!app.canvas || !app.canvas.graph_mouse) return false;
|
||||
const mouse = app.canvas.graph_mouse;
|
||||
const x = mouse[0];
|
||||
const y = mouse[1];
|
||||
@@ -90,6 +181,89 @@ function isMouseOverNode(node) {
|
||||
y <= (node.pos[1] + node.size[1]);
|
||||
}
|
||||
|
||||
function isNodeHovered(node) {
|
||||
if (!node) return false;
|
||||
if (app.canvas && app.canvas.node_over === node) return true;
|
||||
if (hoveredNodeIds.has(node.id)) return true;
|
||||
return isMouseOverNode(node);
|
||||
}
|
||||
|
||||
function shouldHideNow(node) {
|
||||
const shouldHide = shouldHideNode(node);
|
||||
const hovered = isNodeHovered(node);
|
||||
return shouldHide && !hovered;
|
||||
}
|
||||
|
||||
function getNodeDOMElement(node) {
|
||||
if (!node) return null;
|
||||
if (node.DOMelement) return node.DOMelement;
|
||||
if (node.element) return node.element;
|
||||
|
||||
const id = String(node.id);
|
||||
return document.querySelector(`[data-node-id="${id}"]`) ||
|
||||
document.querySelector(`[data-id="${id}"]`) ||
|
||||
document.getElementById(`node-${id}`) ||
|
||||
document.querySelector(`.node-${id}`) ||
|
||||
document.querySelector(`.comfy-vue-node[data-id="${id}"]`);
|
||||
}
|
||||
|
||||
function hasImages(node) {
|
||||
if (!node) return false;
|
||||
if (node.imgs && node.imgs.length > 0) return true;
|
||||
|
||||
const el = getNodeDOMElement(node);
|
||||
if (el) {
|
||||
const img = el.querySelector("img, canvas.comfy-img-preview, .comfy-img-preview, .node-image-preview, .comfy-vue-image-preview");
|
||||
if (img) return true;
|
||||
}
|
||||
|
||||
if (node.widgets) {
|
||||
for (const w of node.widgets) {
|
||||
if (w.type === "image" || w.name === "preview" || w.imageEl || w.element?.querySelector?.("img")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getImageContainers(node) {
|
||||
const containers = new Set();
|
||||
const el = getNodeDOMElement(node);
|
||||
|
||||
if (el) {
|
||||
// Find existing image preview containers inside the node DOM
|
||||
const previewEls = el.querySelectorAll(
|
||||
".comfy-img-preview, .node-image-preview, .comfy-vue-image-preview, .node-widget-image, [class*='ImagePreview'], [class*='image-preview'], [class*='node-image']"
|
||||
);
|
||||
previewEls.forEach(p => containers.add(p));
|
||||
|
||||
// Direct img elements parent wrappers
|
||||
const imgs = el.querySelectorAll("img, canvas.comfy-img-preview");
|
||||
imgs.forEach(img => {
|
||||
if (img.parentElement && img.parentElement !== el) {
|
||||
containers.add(img.parentElement);
|
||||
} else {
|
||||
containers.add(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node.widgets) {
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.element && (widget.type === "image" || widget.name === "preview" || widget.element.querySelector("img"))) {
|
||||
containers.add(widget.element);
|
||||
}
|
||||
if (widget.imageEl) {
|
||||
containers.add(widget.imageEl.parentElement || widget.imageEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(containers);
|
||||
}
|
||||
|
||||
function setupGlobalMouseTracker() {
|
||||
let rafPending = false;
|
||||
|
||||
@@ -112,9 +286,11 @@ function setupGlobalMouseTracker() {
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
syncNodeDOMVisibility(node);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (changed && app.graph) {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
@@ -137,112 +313,159 @@ function setupGlobalMouseTracker() {
|
||||
});
|
||||
}
|
||||
|
||||
function syncNodeDOMVisibility(node) {
|
||||
if (!node) return;
|
||||
|
||||
const el = getNodeDOMElement(node);
|
||||
if (el && !el._sata_mouse_bound) {
|
||||
el._sata_mouse_bound = true;
|
||||
el.addEventListener("mouseenter", () => {
|
||||
hoveredNodeIds.add(node.id);
|
||||
syncNodeDOMVisibility(node);
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
hoveredNodeIds.delete(node.id);
|
||||
syncNodeDOMVisibility(node);
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply DOM masking when images exist AND hide_preview is active
|
||||
const nodeHasImages = hasImages(node);
|
||||
const hide = nodeHasImages && shouldHideNode(node);
|
||||
|
||||
const containers = getImageContainers(node);
|
||||
containers.forEach(container => {
|
||||
if (hide) {
|
||||
container.classList.add("sata-image-masked");
|
||||
} else {
|
||||
container.classList.remove("sata-image-masked");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupNodeHider(node) {
|
||||
if (node._sata_hider_v6) return;
|
||||
node._sata_hider_v6 = true;
|
||||
if (node._sata_hider_v12) return;
|
||||
node._sata_hider_v12 = true;
|
||||
|
||||
// Function to check if node should be hidden right now
|
||||
const shouldHideNow = () => {
|
||||
const shouldHide = shouldHideNode(node);
|
||||
const hovered = hoveredNodeIds.has(node.id);
|
||||
return shouldHide && !hovered;
|
||||
};
|
||||
injectHiderCSS();
|
||||
|
||||
// Find and patch all image-related widgets on the node
|
||||
const patchWidgets = () => {
|
||||
if (!node.widgets) return;
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
// Check if this is an image widget (they typically have draw/drawWidget methods)
|
||||
if (widget._sata_patched) continue;
|
||||
|
||||
// Patch the draw method if it exists
|
||||
if (widget.draw && typeof widget.draw === 'function') {
|
||||
const origDraw = widget.draw.bind(widget);
|
||||
widget.draw = function (ctx, node, widgetWidth, y, widgetHeight) {
|
||||
if (shouldHideNow()) {
|
||||
// Draw hidden placeholder instead
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(0, y, widgetWidth, widgetHeight);
|
||||
if (widget.type === "image" || widget.name === "preview") {
|
||||
if (widget.draw && typeof widget.draw === 'function') {
|
||||
const origDraw = widget.draw.bind(widget);
|
||||
widget.draw = function (ctx, nodeRef, widgetWidth, y, widgetHeight) {
|
||||
const targetNode = nodeRef || node;
|
||||
if (hasImages(targetNode) && shouldHideNow(targetNode)) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(0, y, widgetWidth, widgetHeight);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
return origDraw(ctx, targetNode, widgetWidth, y, widgetHeight);
|
||||
};
|
||||
widget._sata_patched = true;
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = "12px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("Hidden", widgetWidth / 2, y + widgetHeight / 2 - 8);
|
||||
ctx.font = "9px Arial";
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("(Hover to Reveal)", widgetWidth / 2, y + widgetHeight / 2 + 8);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
return origDraw(ctx, node, widgetWidth, y, widgetHeight);
|
||||
};
|
||||
widget._sata_patched = true;
|
||||
}
|
||||
|
||||
// Also patch drawWidget if it exists (some widgets use this)
|
||||
if (widget.drawWidget && typeof widget.drawWidget === 'function') {
|
||||
const origDrawWidget = widget.drawWidget.bind(widget);
|
||||
widget.drawWidget = function (...args) {
|
||||
if (shouldHideNow()) {
|
||||
// Skip drawing - the onDrawForeground will handle the overlay
|
||||
return;
|
||||
}
|
||||
return origDrawWidget(...args);
|
||||
};
|
||||
widget._sata_patched = true;
|
||||
if (widget.drawWidget && typeof widget.drawWidget === 'function') {
|
||||
const origDrawWidget = widget.drawWidget.bind(widget);
|
||||
widget.drawWidget = function (...args) {
|
||||
if (hasImages(node) && shouldHideNow(node)) {
|
||||
return;
|
||||
}
|
||||
return origDrawWidget(...args);
|
||||
};
|
||||
widget._sata_patched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
syncNodeDOMVisibility(node);
|
||||
};
|
||||
|
||||
// Patch widgets initially and on any update
|
||||
patchWidgets();
|
||||
|
||||
// Also intercept onDrawBackground to catch widget additions
|
||||
const origOnDrawBackground = node.onDrawBackground;
|
||||
node.onDrawBackground = function (ctx) {
|
||||
patchWidgets(); // Re-check for new widgets
|
||||
patchWidgets();
|
||||
syncNodeDOMVisibility(node);
|
||||
if (origOnDrawBackground) {
|
||||
origOnDrawBackground.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Draw overlay in onDrawForeground as backup
|
||||
const origOnDrawForeground = node.onDrawForeground;
|
||||
node.onDrawForeground = function (ctx) {
|
||||
if (origOnDrawForeground) {
|
||||
origOnDrawForeground.apply(this, arguments);
|
||||
}
|
||||
|
||||
if (shouldHideNow()) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(0, 0, this.size[0], this.size[1]);
|
||||
syncNodeDOMVisibility(this);
|
||||
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = "bold 14px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("Hidden", this.size[0] / 2, this.size[1] / 2 - 10);
|
||||
if (hasImages(this) && shouldHideNow(this)) {
|
||||
// Find where settings widgets end so we only mask the image area below widgets
|
||||
let startY = 30;
|
||||
if (this.widgets && this.widgets.length > 0) {
|
||||
const lastWidget = this.widgets[this.widgets.length - 1];
|
||||
if (lastWidget.last_y !== undefined) {
|
||||
const h = (lastWidget.computeSize ? lastWidget.computeSize(this.size[0])[1] : 20) || 20;
|
||||
startY = lastWidget.last_y + h + 6;
|
||||
} else {
|
||||
startY = 30 + this.widgets.length * 24;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.font = "10px Arial";
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("(Hover to Reveal)", this.size[0] / 2, this.size[1] / 2 + 10);
|
||||
ctx.restore();
|
||||
const maskHeight = Math.max(0, this.size[1] - startY);
|
||||
if (maskHeight > 10) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(4, startY, this.size[0] - 8, maskHeight - 4);
|
||||
|
||||
// Fallback for pure LiteGraph 2D Canvas rendering (when no DOM element exists)
|
||||
const el = getNodeDOMElement(this);
|
||||
if (!el || !el.querySelector(".sata-image-masked")) {
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = "bold 13px Arial";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("Hidden", this.size[0] / 2, startY + maskHeight / 2 - 8);
|
||||
|
||||
ctx.font = "10px Arial";
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("(Hover to Reveal)", this.size[0] / 2, startY + maskHeight / 2 + 10);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Widget callback for Save_Machine
|
||||
if (node.comfyClass === SAVE_MACHINE_CLASS) {
|
||||
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
|
||||
if (hideWidget) {
|
||||
const origCallback = hideWidget.callback;
|
||||
hideWidget.callback = function (value) {
|
||||
if (origCallback) origCallback.call(this, value);
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
const origOnExecuted = node.onExecuted;
|
||||
node.onExecuted = function (message) {
|
||||
if (origOnExecuted) {
|
||||
origOnExecuted.apply(this, arguments);
|
||||
}
|
||||
patchWidgets();
|
||||
syncNodeDOMVisibility(node);
|
||||
setTimeout(() => syncNodeDOMVisibility(node), 20);
|
||||
setTimeout(() => syncNodeDOMVisibility(node), 100);
|
||||
setTimeout(() => syncNodeDOMVisibility(node), 300);
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const hideWidget = node.widgets?.find(w => w.name === "hide_preview");
|
||||
if (hideWidget) {
|
||||
const origCallback = hideWidget.callback;
|
||||
hideWidget.callback = function (value) {
|
||||
if (origCallback) origCallback.call(this, value);
|
||||
syncNodeDOMVisibility(node);
|
||||
if (app.graph) app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,21 @@ app.registerExtension({
|
||||
}
|
||||
],
|
||||
|
||||
async setup() {
|
||||
const self = this;
|
||||
document.addEventListener("focusin", (e) => {
|
||||
if (e.target && (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT")) {
|
||||
if (e.target.dataset.hasAutocomplete) return;
|
||||
if (!self.snippetsCache) {
|
||||
self.loadSnippets();
|
||||
}
|
||||
e.target.dataset.hasAutocomplete = "true";
|
||||
e.target.addEventListener("keydown", (evt) => self.handleKeydown(evt, e.target, null));
|
||||
e.target.addEventListener("input", (evt) => self.handleInput(evt, e.target, null));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async initAutocompleteForNode(node) {
|
||||
const isTargetNode = node.comfyClass === "PromptAutocomplete" || node.comfyClass === "Prompt_Machine";
|
||||
const isGlobal = getSettingValue("SATA_UtilityNode.PromptAutocomplete.Global", false);
|
||||
|
||||
+170
-94
@@ -1,76 +1,128 @@
|
||||
// prompt_machine_frontend.js
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "SATA_UtilityNode.PromptMachineFrontend",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "Prompt_Style_Machine") return;
|
||||
const PROMPT_MACHINE_TOOLTIPS = {
|
||||
subject: "Core hero or subject and physical details (age, hair, eyes, outfit, accessories).\nExample: 'A young female astronomer with messy raven hair and grey eyes, wearing a fitted leather jacket.'",
|
||||
pose_and_expression: "Body posture, facial expression, action, and primary gestures.\nExample: 'Leaning over an ancient star map with a subtle smile, holding a brass compass in her right hand.'",
|
||||
secondary_subject: "Supporting characters, companion animals, or secondary focal objects.\nExample: 'A sleek white robotic cat sitting on the desk beside her, watching the holograms.'",
|
||||
background_and_environment: "Immediate physical surroundings, wider environment, and background setting.\nExample: 'Inside a high-tech glass dome observatory with scattered parchment maps, overlooking a star-filled galaxy.'",
|
||||
lighting: "Lighting sources, direction, intensity, reflections, shadows, and mood.\nExample: 'Soft cyan holographic glow illuminating her face, warm golden sunlight streaming through the side window.'",
|
||||
style: "Visual medium, photographic style, aesthetic textures, and dominant color palette.\nExample: 'Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.'",
|
||||
creative_hook: "One surreal, impossible, or striking visual concept.\nExample: 'Constellations floating like tiny glowing fireflies in the air inside the room.'",
|
||||
technical: "Camera equipment, lens specs, shot type, and framing (Optional).\nExample: 'Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.'",
|
||||
json_output: "Output structured JSON mapping instead of a formatted text paragraph."
|
||||
};
|
||||
|
||||
// Widgets
|
||||
const csvWidget = node.widgets.find(w => w.name === "csv_file");
|
||||
const oldNameWidget = node.widgets.find(w => w.name === "name");
|
||||
|
||||
if (!csvWidget || !oldNameWidget) {
|
||||
console.warn("[PromptMachineFrontend] Missing widgets, check backend definition.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the old text input for 'name'
|
||||
const index = node.widgets.indexOf(oldNameWidget);
|
||||
node.widgets.splice(index, 1);
|
||||
|
||||
// Add proper dropdown (this appends to the end)
|
||||
const nameWidget = node.addWidget("combo", "name", "None", () => { }, {
|
||||
values: ["None"]
|
||||
});
|
||||
|
||||
// Move the new widget back to the original position
|
||||
// Remove from the end (where addWidget put it)
|
||||
node.widgets.splice(node.widgets.indexOf(nameWidget), 1);
|
||||
// Insert at the correct index
|
||||
node.widgets.splice(index, 0, nameWidget);
|
||||
|
||||
// Store available names
|
||||
let namesConfig = [];
|
||||
|
||||
// Fetch names from backend for a given CSV
|
||||
async function loadNames(csvValue) {
|
||||
if (!csvValue) return;
|
||||
try {
|
||||
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
|
||||
const data = await resp.json();
|
||||
namesConfig = data.names || ["None"];
|
||||
|
||||
refreshNames();
|
||||
} catch (err) {
|
||||
console.error("[PromptMachineFrontend] Failed to load names:", err);
|
||||
function setupPromptMachine(node) {
|
||||
if (!node.widgets) return;
|
||||
node.widgets.forEach(w => {
|
||||
if (PROMPT_MACHINE_TOOLTIPS[w.name]) {
|
||||
w.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
|
||||
if (w.options) {
|
||||
w.options.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
|
||||
}
|
||||
const updateInputEl = () => {
|
||||
if (w.inputEl) {
|
||||
w.inputEl.title = PROMPT_MACHINE_TOOLTIPS[w.name];
|
||||
const firstLine = PROMPT_MACHINE_TOOLTIPS[w.name].split('\n')[0];
|
||||
if (!w.inputEl.placeholder) {
|
||||
w.inputEl.placeholder = firstLine;
|
||||
}
|
||||
w.inputEl.readOnly = false;
|
||||
w.inputEl.disabled = false;
|
||||
}
|
||||
};
|
||||
updateInputEl();
|
||||
// Retry if inputEl creates asynchronously
|
||||
if (!w.inputEl) {
|
||||
setTimeout(updateInputEl, 100);
|
||||
setTimeout(updateInputEl, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh dropdown with available names
|
||||
function refreshNames() {
|
||||
const available = namesConfig.length > 0 ? namesConfig : ["None"];
|
||||
nameWidget.options.values = available;
|
||||
function setupPromptStyleMachine(node) {
|
||||
if (node._sata_style_initialized) return;
|
||||
|
||||
if (!available.includes(nameWidget.value)) {
|
||||
nameWidget.value = available[0]; // safe fallback
|
||||
const csvWidget = node.widgets?.find(w => w.name === "csv_file");
|
||||
let nameWidget = node.widgets?.find(w => w.name === "name");
|
||||
|
||||
if (!csvWidget || !nameWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
node._sata_style_initialized = true;
|
||||
const initialSavedName = nameWidget.value || "None";
|
||||
|
||||
// Convert widget to combo dropdown gracefully if it's not already
|
||||
if (nameWidget.type !== "combo") {
|
||||
const index = node.widgets.indexOf(nameWidget);
|
||||
if (index !== -1) {
|
||||
node.widgets.splice(index, 1);
|
||||
nameWidget = node.addWidget("combo", "name", initialSavedName, () => { }, {
|
||||
values: [initialSavedName !== "None" ? initialSavedName : "None"]
|
||||
});
|
||||
const newIdx = node.widgets.indexOf(nameWidget);
|
||||
if (newIdx !== -1 && newIdx !== index) {
|
||||
node.widgets.splice(newIdx, 1);
|
||||
node.widgets.splice(index, 0, nameWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force update previews for the new name
|
||||
updatePreviews(nameWidget.value);
|
||||
if (!nameWidget.options) {
|
||||
nameWidget.options = { values: [initialSavedName] };
|
||||
}
|
||||
|
||||
app.graph.change();
|
||||
let namesConfig = [];
|
||||
|
||||
// Fetch names from backend for a given CSV
|
||||
async function loadNames(csvValue, preferredName) {
|
||||
if (!csvValue || csvValue === "None") return;
|
||||
try {
|
||||
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
|
||||
const data = await resp.json();
|
||||
namesConfig = (data.names && data.names.length > 0) ? data.names : ["None"];
|
||||
|
||||
refreshNames(preferredName);
|
||||
} catch (err) {
|
||||
console.error("[PromptMachineFrontend] Failed to load names:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh dropdown with available names while preserving target selection
|
||||
function refreshNames(preferredName) {
|
||||
const available = namesConfig.length > 0 ? namesConfig : ["None"];
|
||||
nameWidget.options.values = available;
|
||||
|
||||
const targetVal = preferredName !== undefined ? preferredName : nameWidget.value;
|
||||
if (available.includes(targetVal)) {
|
||||
nameWidget.value = targetVal;
|
||||
} else {
|
||||
nameWidget.value = available[0] || "None";
|
||||
}
|
||||
|
||||
// Hook CSV change
|
||||
const oldCsvCallback = csvWidget.callback;
|
||||
csvWidget.callback = function (value) {
|
||||
if (oldCsvCallback) oldCsvCallback(value);
|
||||
loadNames(value); // refresh dynamically
|
||||
};
|
||||
// Force update previews for current name
|
||||
updatePreviews(nameWidget.value);
|
||||
|
||||
// --- Preview Logic ---
|
||||
const previewEl = document.createElement("div");
|
||||
if (app.graph) {
|
||||
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
|
||||
if (app.graph.change) app.graph.change();
|
||||
}
|
||||
}
|
||||
|
||||
// Hook CSV change (user triggered)
|
||||
const oldCsvCallback = csvWidget.callback;
|
||||
csvWidget.callback = function (value) {
|
||||
if (oldCsvCallback) oldCsvCallback.apply(this, arguments);
|
||||
loadNames(value); // refresh without preferred name (defaults to first available)
|
||||
};
|
||||
|
||||
// --- Preview Logic ---
|
||||
let previewEl = node.widgets?.find(w => w.name === "preview_text")?.element;
|
||||
if (!previewEl) {
|
||||
previewEl = document.createElement("div");
|
||||
Object.assign(previewEl.style, {
|
||||
marginTop: "10px",
|
||||
padding: "8px",
|
||||
@@ -83,43 +135,67 @@ app.registerExtension({
|
||||
maxHeight: "400px",
|
||||
overflowY: "auto"
|
||||
});
|
||||
|
||||
|
||||
node.addDOMWidget("preview_text", "html", previewEl, {
|
||||
getValue: () => previewEl.innerHTML,
|
||||
setValue: (v) => { previewEl.innerHTML = v; }
|
||||
});
|
||||
|
||||
async function updatePreviews(name) {
|
||||
const csv = csvWidget.value;
|
||||
if (!csv || !name || name === "None") return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`);
|
||||
const data = await resp.json();
|
||||
let html = "";
|
||||
if (data.positive) html += `${data.positive}`;
|
||||
|
||||
previewEl.innerHTML = html || "<i>No preview available</i>";
|
||||
|
||||
// Adjust node size to fit content
|
||||
node.setSize(node.computeSize());
|
||||
|
||||
app.graph.change();
|
||||
} catch (err) {
|
||||
console.error("[PromptMachineFrontend] Failed to load preview:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Hook Name change
|
||||
const oldNameCallback = nameWidget.callback;
|
||||
nameWidget.callback = function (value) {
|
||||
if (oldNameCallback) oldNameCallback(value);
|
||||
updatePreviews(value);
|
||||
};
|
||||
|
||||
// Initial sync
|
||||
loadNames(csvWidget.value).then(() => {
|
||||
updatePreviews(nameWidget.value);
|
||||
setValue: (v) => { previewEl.innerHTML = v; },
|
||||
serialize: false
|
||||
});
|
||||
}
|
||||
|
||||
async function updatePreviews(name) {
|
||||
const csv = csvWidget.value;
|
||||
if (!csv || !name || name === "None") {
|
||||
previewEl.innerHTML = "<i>No preview available</i>";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`);
|
||||
const data = await resp.json();
|
||||
let html = "";
|
||||
if (data.positive) html += `${data.positive}`;
|
||||
|
||||
previewEl.innerHTML = html || "<i>No preview available</i>";
|
||||
|
||||
if (node.computeSize && node.setSize) {
|
||||
node.setSize(node.computeSize());
|
||||
}
|
||||
|
||||
if (app.graph) {
|
||||
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
|
||||
if (app.graph.change) app.graph.change();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[PromptMachineFrontend] Failed to load preview:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Hook Name change
|
||||
const oldNameCallback = nameWidget.callback;
|
||||
nameWidget.callback = function (value) {
|
||||
if (oldNameCallback) oldNameCallback.apply(this, arguments);
|
||||
updatePreviews(value);
|
||||
};
|
||||
|
||||
// Initial sync preserving saved value
|
||||
loadNames(csvWidget.value, initialSavedName);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "SATA_UtilityNode.PromptMachineFrontend",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "Prompt_Machine") {
|
||||
setupPromptMachine(node);
|
||||
} else if (node.comfyClass === "Prompt_Style_Machine") {
|
||||
setupPromptStyleMachine(node);
|
||||
}
|
||||
},
|
||||
async loadedGraphNode(node) {
|
||||
if (node.comfyClass === "Prompt_Machine") {
|
||||
setupPromptMachine(node);
|
||||
} else if (node.comfyClass === "Prompt_Style_Machine") {
|
||||
setupPromptStyleMachine(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
+164
-156
@@ -1,165 +1,173 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
function setupResolutionNode(node) {
|
||||
if (node._sata_res_initialized) return;
|
||||
if (node.comfyClass !== "Resolution_Machine" && node.comfyClass !== "Latent_Machine") return;
|
||||
|
||||
// Find widgets
|
||||
const modelWidget = node.widgets?.find(w => w.name === "model");
|
||||
const dimensionWidget = node.widgets?.find(w => w.name === "dimension");
|
||||
const resolutionWidget = node.widgets?.find(w => w.name === "resolution");
|
||||
const widthWidget = node.widgets?.find(w => w.name === "width") || node.widgets?.find(w => w.name === "custom_width");
|
||||
const heightWidget = node.widgets?.find(w => w.name === "height") || node.widgets?.find(w => w.name === "custom_height");
|
||||
|
||||
if (!modelWidget || !dimensionWidget || !resolutionWidget || !widthWidget || !heightWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
node._sata_res_initialized = true;
|
||||
|
||||
// Store resolution config
|
||||
let resolutionsConfig = {};
|
||||
|
||||
// Fetch resolution config from backend
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch("/SATA_UtilityNode/resolutions_config");
|
||||
resolutionsConfig = await resp.json();
|
||||
refreshResolutions(false);
|
||||
} catch (err) {
|
||||
console.error("[Resolution_Machine] Failed to load config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utility: refresh resolution dropdown when model/dimension changes ---
|
||||
function refreshResolutions(resetSelection = true) {
|
||||
const selectedModel = modelWidget.value;
|
||||
const selectedDimension = dimensionWidget.value;
|
||||
|
||||
let available = [];
|
||||
|
||||
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
|
||||
const buckets = resolutionsConfig.models[selectedModel];
|
||||
buckets.forEach(bucket => {
|
||||
if (resolutionsConfig.resolutions && resolutionsConfig.resolutions[bucket] && resolutionsConfig.resolutions[bucket][selectedDimension]) {
|
||||
available.push(...Object.keys(resolutionsConfig.resolutions[bucket][selectedDimension]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Always include "Custom" option
|
||||
if (!available.includes("Custom")) {
|
||||
available.push("Custom");
|
||||
}
|
||||
|
||||
resolutionWidget.options.values = available;
|
||||
|
||||
if (resetSelection && !available.includes(resolutionWidget.value)) {
|
||||
resolutionWidget.value = available[0] || "Custom";
|
||||
}
|
||||
|
||||
checkCustomMode();
|
||||
}
|
||||
|
||||
function checkCustomMode() {
|
||||
const isCustom = resolutionWidget.value === "Custom";
|
||||
if (isCustom) {
|
||||
widthWidget.type = "INT";
|
||||
heightWidget.type = "INT";
|
||||
delete widthWidget.computeSize;
|
||||
delete heightWidget.computeSize;
|
||||
widthWidget.disabled = false;
|
||||
heightWidget.disabled = false;
|
||||
} else {
|
||||
widthWidget.type = "hidden";
|
||||
heightWidget.type = "hidden";
|
||||
widthWidget.computeSize = () => [0, -4];
|
||||
heightWidget.computeSize = () => [0, -4];
|
||||
applyResolutionPreset();
|
||||
}
|
||||
|
||||
if (node.computeSize && node.setSize) {
|
||||
node.setSize(node.computeSize());
|
||||
}
|
||||
if (app.graph) {
|
||||
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
|
||||
if (app.graph.change) app.graph.change();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utility: apply resolution preset to width/height ---
|
||||
function applyResolutionPreset() {
|
||||
const selectedModel = modelWidget.value;
|
||||
const selectedDimension = dimensionWidget.value;
|
||||
const selectedResolution = resolutionWidget.value;
|
||||
|
||||
if (selectedResolution === "Custom") {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolutionData = null;
|
||||
|
||||
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
|
||||
const buckets = resolutionsConfig.models[selectedModel];
|
||||
for (const bucket of buckets) {
|
||||
if (resolutionsConfig.resolutions &&
|
||||
resolutionsConfig.resolutions[bucket] &&
|
||||
resolutionsConfig.resolutions[bucket][selectedDimension] &&
|
||||
resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution]) {
|
||||
|
||||
resolutionData = resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolutionData) {
|
||||
widthWidget.value = resolutionData.width;
|
||||
heightWidget.value = resolutionData.height;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hook model change ---
|
||||
const oldModelCallback = modelWidget.callback;
|
||||
modelWidget.callback = function (value) {
|
||||
if (oldModelCallback) oldModelCallback.apply(this, arguments);
|
||||
refreshResolutions(true);
|
||||
};
|
||||
|
||||
// --- Hook dimension change ---
|
||||
const oldDimensionCallback = dimensionWidget.callback;
|
||||
dimensionWidget.callback = function (value) {
|
||||
if (oldDimensionCallback) oldDimensionCallback.apply(this, arguments);
|
||||
refreshResolutions(true);
|
||||
};
|
||||
|
||||
// --- Hook resolution change ---
|
||||
const oldResolutionCallback = resolutionWidget.callback;
|
||||
resolutionWidget.callback = function (value) {
|
||||
if (oldResolutionCallback) oldResolutionCallback.apply(this, arguments);
|
||||
checkCustomMode();
|
||||
};
|
||||
|
||||
// --- Add Custom Button (once) ---
|
||||
if (!node.widgets.some(w => w.name === "Custom")) {
|
||||
node.addWidget("button", "Custom", null, () => {
|
||||
widthWidget.disabled = false;
|
||||
heightWidget.disabled = false;
|
||||
|
||||
if (resolutionWidget.options && resolutionWidget.options.values) {
|
||||
if (!resolutionWidget.options.values.includes("Custom")) {
|
||||
resolutionWidget.options.values.push("Custom");
|
||||
}
|
||||
}
|
||||
resolutionWidget.value = "Custom";
|
||||
|
||||
checkCustomMode();
|
||||
});
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "SATA_UtilityNode.Resolution_Machine",
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "Resolution_Machine" && node.comfyClass !== "Latent_Machine") return;
|
||||
setupResolutionNode(node);
|
||||
},
|
||||
|
||||
// Find widgets
|
||||
const modelWidget = node.widgets.find(w => w.name === "model");
|
||||
const dimensionWidget = node.widgets.find(w => w.name === "dimension");
|
||||
const resolutionWidget = node.widgets.find(w => w.name === "resolution");
|
||||
const widthWidget = node.widgets.find(w => w.name === "width") || node.widgets.find(w => w.name === "custom_width");
|
||||
const heightWidget = node.widgets.find(w => w.name === "height") || node.widgets.find(w => w.name === "custom_height");
|
||||
|
||||
if (!modelWidget || !dimensionWidget || !resolutionWidget || !widthWidget || !heightWidget) {
|
||||
console.warn("[Resolution_Machine] Missing widgets, check backend definition.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store resolution config
|
||||
let resolutionsConfig = {};
|
||||
|
||||
// Fetch resolution config from backend
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch("/SATA_UtilityNode/resolutions_config");
|
||||
resolutionsConfig = await resp.json();
|
||||
|
||||
} catch (err) {
|
||||
console.error("[Resolution_Machine] Failed to load config:", err);
|
||||
}
|
||||
}
|
||||
|
||||
await loadConfig();
|
||||
|
||||
// --- Utility: refresh resolution dropdown when model/dimension changes ---
|
||||
function refreshResolutions() {
|
||||
const selectedModel = modelWidget.value;
|
||||
const selectedDimension = dimensionWidget.value;
|
||||
|
||||
let available = [];
|
||||
|
||||
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
|
||||
const buckets = resolutionsConfig.models[selectedModel];
|
||||
buckets.forEach(bucket => {
|
||||
// Check if bucket exists and has the selected dimension
|
||||
if (resolutionsConfig.resolutions && resolutionsConfig.resolutions[bucket] && resolutionsConfig.resolutions[bucket][selectedDimension]) {
|
||||
available.push(...Object.keys(resolutionsConfig.resolutions[bucket][selectedDimension]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Always include "Custom" option so that switching model/dimension dropdowns doesn't kick the user out of custom mode
|
||||
if (!available.includes("Custom")) {
|
||||
available.push("Custom");
|
||||
}
|
||||
|
||||
resolutionWidget.options.values = available;
|
||||
|
||||
// Pick safe default if current value is not valid
|
||||
if (!available.includes(resolutionWidget.value)) {
|
||||
resolutionWidget.value = available[0];
|
||||
}
|
||||
|
||||
// If we switched to a preset (not Custom), lock fields
|
||||
// If we defaulted to Custom (e.g. empty available), unlock
|
||||
checkCustomMode();
|
||||
}
|
||||
|
||||
function checkCustomMode() {
|
||||
if (resolutionWidget.value === "Custom") {
|
||||
widthWidget.type = "INT";
|
||||
heightWidget.type = "INT";
|
||||
delete widthWidget.computeSize;
|
||||
delete heightWidget.computeSize;
|
||||
widthWidget.disabled = false;
|
||||
heightWidget.disabled = false;
|
||||
} else {
|
||||
widthWidget.type = "hidden";
|
||||
heightWidget.type = "hidden";
|
||||
widthWidget.computeSize = () => [0, -4];
|
||||
heightWidget.computeSize = () => [0, -4];
|
||||
applyResolutionPreset();
|
||||
}
|
||||
node.setSize(node.computeSize());
|
||||
app.graph.change();
|
||||
}
|
||||
|
||||
// --- Utility: apply resolution preset to width/height ---
|
||||
function applyResolutionPreset() {
|
||||
const selectedModel = modelWidget.value;
|
||||
const selectedDimension = dimensionWidget.value;
|
||||
const selectedResolution = resolutionWidget.value;
|
||||
|
||||
if (selectedResolution === "Custom") {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolutionData = null;
|
||||
|
||||
// Lookup
|
||||
if (resolutionsConfig.models && resolutionsConfig.models[selectedModel]) {
|
||||
const buckets = resolutionsConfig.models[selectedModel];
|
||||
for (const bucket of buckets) {
|
||||
if (resolutionsConfig.resolutions[bucket] &&
|
||||
resolutionsConfig.resolutions[bucket][selectedDimension] &&
|
||||
resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution]) {
|
||||
|
||||
resolutionData = resolutionsConfig.resolutions[bucket][selectedDimension][selectedResolution];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolutionData) {
|
||||
widthWidget.value = resolutionData.width;
|
||||
heightWidget.value = resolutionData.height;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hook model change ---
|
||||
const oldModelCallback = modelWidget.callback;
|
||||
modelWidget.callback = function (value) {
|
||||
if (oldModelCallback) oldModelCallback(value);
|
||||
refreshResolutions();
|
||||
};
|
||||
|
||||
// --- Hook dimension change ---
|
||||
const oldDimensionCallback = dimensionWidget.callback;
|
||||
dimensionWidget.callback = function (value) {
|
||||
if (oldDimensionCallback) oldDimensionCallback(value);
|
||||
refreshResolutions();
|
||||
};
|
||||
|
||||
// --- Hook resolution change ---
|
||||
const oldResolutionCallback = resolutionWidget.callback;
|
||||
resolutionWidget.callback = function (value) {
|
||||
if (oldResolutionCallback) oldResolutionCallback(value);
|
||||
checkCustomMode();
|
||||
};
|
||||
|
||||
// --- Add Custom Button ---
|
||||
// Rename "Swap Dimensions" -> "Custom"
|
||||
// Note: Node logic adds this button. Previous JS added it. We are replacing previous JS logic.
|
||||
|
||||
node.addWidget("button", "Custom", null, () => {
|
||||
// Unlock widgets
|
||||
widthWidget.disabled = false;
|
||||
heightWidget.disabled = false;
|
||||
|
||||
// Ensure "Custom" option exists and is selected
|
||||
if (!resolutionWidget.options.values.includes("Custom")) {
|
||||
resolutionWidget.options.values.push("Custom");
|
||||
}
|
||||
resolutionWidget.value = "Custom";
|
||||
|
||||
checkCustomMode();
|
||||
app.graph.change();
|
||||
});
|
||||
|
||||
// --- Initial sync ---
|
||||
refreshResolutions();
|
||||
async loadedGraphNode(node) {
|
||||
setupResolutionNode(node);
|
||||
}
|
||||
});
|
||||
+65
-56
@@ -20,8 +20,8 @@ app.registerExtension({
|
||||
}
|
||||
],
|
||||
async setup() {
|
||||
// Create a toggle button in the ComfyUI menu
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
// Create a toggle button in the ComfyUI menu (Legacy V1)
|
||||
const menu = document.querySelector(".comfy-menu") || document.querySelector(".comfyui-menu");
|
||||
if (menu) {
|
||||
const toggleBtn = document.createElement("button");
|
||||
toggleBtn.id = "sata-touchpad-toggle-btn";
|
||||
@@ -74,62 +74,71 @@ app.registerExtension({
|
||||
|
||||
/**
|
||||
* Enhanced smooth scrolling and zooming for touchpad gestures
|
||||
* Overrides LGraphCanvas.prototype.processMouseWheel
|
||||
* Overrides LGraphCanvas.prototype.processMouseWheel if available
|
||||
*/
|
||||
const originalProcessMouseWheel = LGraphCanvas.prototype.processMouseWheel;
|
||||
if (typeof LGraphCanvas !== "undefined" && LGraphCanvas.prototype) {
|
||||
const originalProcessMouseWheel = LGraphCanvas.prototype.processMouseWheel;
|
||||
|
||||
LGraphCanvas.prototype.processMouseWheel = function (/** @type {WheelEvent} */ event) {
|
||||
let isEnabled = true;
|
||||
|
||||
if (app.extensionManager && app.extensionManager.setting) {
|
||||
const val = app.extensionManager.setting.get(SETTING_ID);
|
||||
if (val !== undefined) isEnabled = val;
|
||||
} else if (app.ui && app.ui.settings) {
|
||||
isEnabled = app.ui.settings.getSettingValue(SETTING_ID, true);
|
||||
}
|
||||
LGraphCanvas.prototype.processMouseWheel = function (/** @type {WheelEvent} */ event) {
|
||||
let isEnabled = true;
|
||||
|
||||
if (app.extensionManager && app.extensionManager.setting) {
|
||||
const val = app.extensionManager.setting.get(SETTING_ID);
|
||||
if (val !== undefined) isEnabled = val;
|
||||
} else if (app.ui && app.ui.settings) {
|
||||
isEnabled = app.ui.settings.getSettingValue(SETTING_ID, true);
|
||||
}
|
||||
|
||||
if (isEnabled === false) {
|
||||
if (originalProcessMouseWheel) {
|
||||
return originalProcessMouseWheel.apply(this, arguments);
|
||||
if (isEnabled === false) {
|
||||
if (originalProcessMouseWheel) {
|
||||
return originalProcessMouseWheel.apply(this, arguments);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.graph || !this.allow_dragcanvas || !this.ds) return;
|
||||
|
||||
const rect = this.canvas ? this.canvas.getBoundingClientRect() : null;
|
||||
if (!rect) return;
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const { deltaX, deltaY, ctrlKey, metaKey } = event;
|
||||
|
||||
// Check if the event occurred inside the canvas viewport
|
||||
if (this.viewport) {
|
||||
const [vx, vy, vw, vh] = this.viewport;
|
||||
const insideX = x >= vx && x < vx + vw;
|
||||
const insideY = y >= vy && y < vy + vh;
|
||||
if (!(insideX && insideY)) return;
|
||||
}
|
||||
|
||||
const currentScale = this.ds.scale;
|
||||
|
||||
// Configurable sensitivity
|
||||
const ZOOM_SENSITIVITY = this.zoomSensitivity || 0.005;
|
||||
const PAN_SENSITIVITY = this.panSensitivity || 1.0;
|
||||
const MIN_SCALE = 0.1;
|
||||
const MAX_SCALE = 4.0;
|
||||
|
||||
if (ctrlKey || metaKey) {
|
||||
// Zoom mode
|
||||
const zoomDirection = Math.sign(deltaY);
|
||||
const zoomAmount = zoomDirection * Math.abs(deltaY) * ZOOM_SENSITIVITY;
|
||||
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, currentScale - zoomAmount));
|
||||
if (this.ds.changeScale) {
|
||||
this.ds.changeScale(newScale, [x, y]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Pan mode
|
||||
if (this.ds.mouseDrag) {
|
||||
this.ds.mouseDrag(-deltaX * PAN_SENSITIVITY, -deltaY * PAN_SENSITIVITY);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.graph || !this.allow_dragcanvas) return;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const { deltaX, deltaY, ctrlKey, metaKey } = event;
|
||||
|
||||
// Check if the event occurred inside the canvas viewport
|
||||
if (this.viewport) {
|
||||
const [vx, vy, vw, vh] = this.viewport;
|
||||
const insideX = x >= vx && x < vx + vw;
|
||||
const insideY = y >= vy && y < vy + vh;
|
||||
if (!(insideX && insideY)) return;
|
||||
}
|
||||
|
||||
const currentScale = this.ds.scale;
|
||||
|
||||
// Configurable sensitivity
|
||||
const ZOOM_SENSITIVITY = this.zoomSensitivity || 0.005;
|
||||
const PAN_SENSITIVITY = this.panSensitivity || 1.0;
|
||||
const MIN_SCALE = 0.1;
|
||||
const MAX_SCALE = 4.0;
|
||||
|
||||
if (ctrlKey || metaKey) {
|
||||
// Zoom mode
|
||||
const zoomDirection = Math.sign(deltaY);
|
||||
const zoomAmount = zoomDirection * Math.abs(deltaY) * ZOOM_SENSITIVITY;
|
||||
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, currentScale - zoomAmount));
|
||||
this.ds.changeScale(newScale, [x, y]);
|
||||
} else {
|
||||
// Pan mode
|
||||
this.ds.mouseDrag(-deltaX * PAN_SENSITIVITY, -deltaY * PAN_SENSITIVITY);
|
||||
}
|
||||
|
||||
this.graph.change();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
};
|
||||
if (this.graph.change) {
|
||||
this.graph.change();
|
||||
}
|
||||
event.preventDefault();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ class Preview_Machine(PreviewImage):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"hide_preview": ("BOOLEAN", {"default": True, "tooltip": "Hide image preview until mouse hovers over the node."}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
@@ -15,5 +16,5 @@ class Preview_Machine(PreviewImage):
|
||||
CATEGORY = "SATA_UtilityNode"
|
||||
FUNCTION = "save_images"
|
||||
|
||||
def save_images(self, images, prompt=None, extra_pnginfo=None):
|
||||
def save_images(self, images, hide_preview=True, prompt=None, extra_pnginfo=None):
|
||||
return super().save_images(images, filename_prefix="Preview_Machine", prompt=prompt, extra_pnginfo=extra_pnginfo)
|
||||
|
||||
+130
-43
@@ -18,7 +18,8 @@ def read_names_from_csv(filename):
|
||||
"""Return list of names (stripped) from CSV's 'name' column."""
|
||||
if not filename:
|
||||
return []
|
||||
path = os.path.join(CSV_DIR, filename)
|
||||
safe_filename = os.path.basename(str(filename).strip())
|
||||
path = os.path.join(CSV_DIR, safe_filename)
|
||||
if not os.path.exists(path):
|
||||
print(f"[PromptMachine] CSV not found: {path}")
|
||||
return []
|
||||
@@ -26,11 +27,11 @@ def read_names_from_csv(filename):
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
if not reader.fieldnames:
|
||||
print(f"[PromptMachine] CSV has no header: {filename}")
|
||||
print(f"[PromptMachine] CSV has no header: {safe_filename}")
|
||||
return []
|
||||
field_map = {fn.strip().lower(): fn for fn in reader.fieldnames}
|
||||
if "name" not in field_map:
|
||||
print(f"[PromptMachine] CSV missing 'name' column: {filename}")
|
||||
print(f"[PromptMachine] CSV missing 'name' column: {safe_filename}")
|
||||
return []
|
||||
name_field = field_map["name"]
|
||||
names = []
|
||||
@@ -38,12 +39,12 @@ def read_names_from_csv(filename):
|
||||
raw = row.get(name_field)
|
||||
if raw is None:
|
||||
continue
|
||||
val = raw.strip()
|
||||
val = str(raw).strip()
|
||||
if val:
|
||||
names.append(val)
|
||||
return names
|
||||
except Exception as e:
|
||||
print(f"[PromptMachine] read_names_from_csv error for {filename}: {e}")
|
||||
print(f"[PromptMachine] read_names_from_csv error for {safe_filename}: {e}")
|
||||
return []
|
||||
|
||||
def read_prompt_row(csv_file, name):
|
||||
@@ -53,7 +54,8 @@ def read_prompt_row(csv_file, name):
|
||||
return ("", "", "")
|
||||
if not name or not isinstance(name, str) or name == "None":
|
||||
return ("", "", "")
|
||||
path = os.path.join(CSV_DIR, csv_file)
|
||||
safe_csv_file = os.path.basename(str(csv_file).strip())
|
||||
path = os.path.join(CSV_DIR, safe_csv_file)
|
||||
if not os.path.exists(path):
|
||||
print(f"[PromptMachine] CSV file not found: {path}")
|
||||
return ("", "", "")
|
||||
@@ -61,11 +63,11 @@ def read_prompt_row(csv_file, name):
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
if not reader.fieldnames:
|
||||
print(f"[PromptMachine] CSV has no header: {csv_file}")
|
||||
print(f"[PromptMachine] CSV has no header: {safe_csv_file}")
|
||||
return ("", "", "")
|
||||
field_map = {fn.strip().lower(): fn for fn in reader.fieldnames}
|
||||
if "name" not in field_map:
|
||||
print(f"[PromptMachine] 'name' column missing in CSV: {csv_file}")
|
||||
print(f"[PromptMachine] 'name' column missing in CSV: {safe_csv_file}")
|
||||
return ("", "", "")
|
||||
name_field = field_map["name"]
|
||||
pos_field = field_map.get("positive", "positive")
|
||||
@@ -77,13 +79,13 @@ def read_prompt_row(csv_file, name):
|
||||
cell = row.get(name_field)
|
||||
if cell is None:
|
||||
continue
|
||||
if cell.strip() == target:
|
||||
pos = (row.get(pos_field) or "").strip()
|
||||
neg = (row.get(neg_field) or "").strip()
|
||||
note = (row.get(note_field) or "").strip()
|
||||
if str(cell).strip() == target:
|
||||
pos = str(row.get(pos_field) or "").strip()
|
||||
neg = str(row.get(neg_field) or "").strip()
|
||||
note = str(row.get(note_field) or "").strip()
|
||||
return (pos, neg, note)
|
||||
except Exception as e:
|
||||
print(f"[PromptMachine] read_prompt_row error for {csv_file}, {name}: {e}")
|
||||
print(f"[PromptMachine] read_prompt_row error for {safe_csv_file}, {name}: {e}")
|
||||
return ("", "", "")
|
||||
|
||||
|
||||
@@ -126,17 +128,66 @@ class Prompt_Style_Machine:
|
||||
|
||||
|
||||
class Prompt_Machine:
|
||||
"""
|
||||
Prompt Machine (8-Category Framework):
|
||||
Assembles information-dense scene descriptions in natural prose following an inside-out spatial sequence.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"subject": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"style": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"lighting": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"composition": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"mood": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"technical": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}),
|
||||
"json_output": ("BOOLEAN", {"default": False}),
|
||||
"subject": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Core hero or subject and physical details (age, hair, eyes, outfit, accessories).\nExample: 'A young female astronomer with messy raven hair and grey eyes, wearing a fitted leather jacket.'"
|
||||
}),
|
||||
"pose_and_expression": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Body posture, facial expression, action, and primary gestures.\nExample: 'Leaning over an ancient star map with a subtle smile, holding a brass compass in her right hand.'"
|
||||
}),
|
||||
"secondary_subject": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Supporting characters, companion animals, or secondary focal objects.\nExample: 'A sleek white robotic cat sitting on the desk beside her, watching the holograms.'"
|
||||
}),
|
||||
"background_and_environment": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Immediate physical surroundings, wider environment, and background setting.\nExample: 'Inside a high-tech glass dome observatory with scattered parchment maps, overlooking a star-filled galaxy.'"
|
||||
}),
|
||||
"lighting": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Lighting sources, direction, intensity, reflections, shadows, and mood.\nExample: 'Soft cyan holographic glow illuminating her face, warm golden sunlight streaming through the side window.'"
|
||||
}),
|
||||
"style": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Visual medium, photographic style, aesthetic textures, and dominant color palette.\nExample: 'Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.'"
|
||||
}),
|
||||
"creative_hook": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "One surreal, impossible, or striking visual concept.\nExample: 'Constellations floating like tiny glowing fireflies in the air inside the room.'"
|
||||
}),
|
||||
"technical": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"dynamicPrompts": True,
|
||||
"tooltip": "Camera equipment, lens specs, shot type, and framing (Optional).\nExample: 'Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.'"
|
||||
}),
|
||||
"json_output": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Output structured JSON mapping instead of a formatted text paragraph."
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,29 +196,63 @@ class Prompt_Machine:
|
||||
FUNCTION = "get_prompt"
|
||||
CATEGORY = "SATA_UtilityNode"
|
||||
|
||||
def get_prompt(self, subject, style, lighting, composition, mood, technical, json_output):
|
||||
def get_prompt(self, subject="", pose_and_expression="", secondary_subject="",
|
||||
background_and_environment="", lighting="", style="",
|
||||
creative_hook="", technical="", json_output=False, **kwargs):
|
||||
|
||||
# Support fallback for legacy parameter names if older workflows pass them
|
||||
if not subject and kwargs.get("appearance"):
|
||||
subject = kwargs["appearance"]
|
||||
if not secondary_subject and kwargs.get("secondary_subjects"):
|
||||
secondary_subject = kwargs["secondary_subjects"]
|
||||
if not background_and_environment:
|
||||
env_parts = [kwargs.get("surroundings", ""), kwargs.get("environment_and_background", "")]
|
||||
valid_parts = [" ".join(str(p).strip().split()) for p in env_parts if p is not None and str(p).strip()]
|
||||
if valid_parts:
|
||||
background_and_environment = " ".join(valid_parts)
|
||||
if not lighting and kwargs.get("lighting_and_atmosphere"):
|
||||
lighting = kwargs["lighting_and_atmosphere"]
|
||||
if kwargs.get("materials_and_colors"):
|
||||
mat = " ".join(str(kwargs["materials_and_colors"]).strip().split())
|
||||
if mat:
|
||||
style_str = " ".join(str(style).strip().split()) if style else ""
|
||||
style = f"{style_str} {mat}".strip() if style_str else mat
|
||||
|
||||
raw_fields = [
|
||||
("subject", subject),
|
||||
("pose_and_expression", pose_and_expression),
|
||||
("secondary_subject", secondary_subject),
|
||||
("background_and_environment", background_and_environment),
|
||||
("lighting", lighting),
|
||||
("style", style),
|
||||
("creative_hook", creative_hook),
|
||||
("technical", technical),
|
||||
]
|
||||
|
||||
cleaned_fields = []
|
||||
for name, val in raw_fields:
|
||||
if val is None:
|
||||
continue
|
||||
normalized = " ".join(str(val).strip().split())
|
||||
if normalized:
|
||||
cleaned_fields.append((name, normalized))
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
data = {
|
||||
"subject": subject.strip(),
|
||||
"style": style.strip(),
|
||||
"lighting": lighting.strip(),
|
||||
"composition": composition.strip(),
|
||||
"mood": mood.strip(),
|
||||
"technical": technical.strip()
|
||||
}
|
||||
# Remove empty fields to keep JSON clean
|
||||
data = {k: v for k, v in data.items() if v}
|
||||
data = {k: v for k, v in cleaned_fields}
|
||||
return (json.dumps(data, indent=2),)
|
||||
else:
|
||||
parts = []
|
||||
if subject.strip(): parts.append(subject.strip())
|
||||
if style.strip(): parts.append(style.strip())
|
||||
if lighting.strip(): parts.append(lighting.strip())
|
||||
if composition.strip(): parts.append(composition.strip())
|
||||
if mood.strip(): parts.append(mood.strip())
|
||||
if technical.strip(): parts.append(technical.strip())
|
||||
return (", ".join(parts),)
|
||||
|
||||
sentences = []
|
||||
for name, text in cleaned_fields:
|
||||
# Strip trailing separators like commas, semicolons, colons, or dashes before period
|
||||
trimmed = text.rstrip(" ,;:-")
|
||||
if trimmed:
|
||||
if trimmed[-1] not in ".!?":
|
||||
trimmed += "."
|
||||
sentences.append(trimmed)
|
||||
|
||||
paragraph = " ".join(sentences)
|
||||
return (paragraph,)
|
||||
|
||||
|
||||
# ---------------- REST API ----------------
|
||||
@@ -182,7 +267,8 @@ async def list_csvs(request):
|
||||
@PromptServer.instance.routes.get("/sata/prompt_machine/names")
|
||||
async def list_names(request):
|
||||
"""Return names from a selected CSV"""
|
||||
csv_file = request.query.get("csv")
|
||||
raw_csv = request.query.get("csv", "")
|
||||
csv_file = os.path.basename(raw_csv.strip()) if raw_csv else ""
|
||||
names = read_names_from_csv(csv_file) if csv_file else []
|
||||
return web.json_response({"names": names})
|
||||
|
||||
@@ -193,7 +279,8 @@ async def get_prompt(request):
|
||||
Return the prompt triple for immediate frontend consumption.
|
||||
Example: /sata/prompt_machine/get?csv=characters.csv&name=Bob
|
||||
"""
|
||||
csv_file = request.query.get("csv")
|
||||
name = request.query.get("name")
|
||||
raw_csv = request.query.get("csv", "")
|
||||
csv_file = os.path.basename(raw_csv.strip()) if raw_csv else ""
|
||||
name = request.query.get("name", "")
|
||||
pos, neg, note = read_prompt_row(csv_file, name)
|
||||
return web.json_response({"positive": pos, "negative": neg, "note": note})
|
||||
@@ -134,6 +134,7 @@ class Save_Machine:
|
||||
"images": ("IMAGE", ),
|
||||
"path_and_filename": ("STRING", {"default": "%time", "tooltip": "Supports %date, %time, and custom prompt %placeholders%"}),
|
||||
"extension": ((['png', 'jpeg', 'webp']),),
|
||||
"hide_preview": ("BOOLEAN", {"default": True, "tooltip": "Hide image preview until mouse hovers over the node."}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
@@ -147,7 +148,7 @@ class Save_Machine:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "SATA_UtilityNode"
|
||||
|
||||
def save_files(self, images, path_and_filename, extension, prompt=None, extra_pnginfo=None):
|
||||
def save_files(self, images, path_and_filename, extension, hide_preview=True, prompt=None, extra_pnginfo=None):
|
||||
# Resolve placeholders first (uses prompt)
|
||||
try:
|
||||
resolved = resolve_placeholders(str(path_and_filename), prompt, extra_pnginfo)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "sata_utilitynode"
|
||||
description = "A collection of utility nodes including image upscale,touchpad support and CSV-driven prompt machine for ComfyUI."
|
||||
version = "1.5.0"
|
||||
version = "2.0.0"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
dependencies = ["opencv-python>=4.5.0", "spandrel", "piexif"]
|
||||
|
||||
Reference in New Issue
Block a user