mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-09 13:50:49 +08:00
wip eval nodes, test tracing with full integration test, fix dockerfile barfing on flash_attn 2.8.3
This commit is contained in:
parent
69d8f1b120
commit
8700c4fadf
@ -33,7 +33,7 @@ RUN pip install uv && uv --version && \
|
|||||||
|
|
||||||
# install sageattention
|
# install sageattention
|
||||||
ADD pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl /workspace/pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl
|
ADD pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl /workspace/pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl
|
||||||
RUN uv pip install -U --no-deps --no-build-isolation spandrel timm tensorboard poetry flash-attn "xformers==0.0.31.post1" "file:./pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl"
|
RUN uv pip install -U --no-deps --no-build-isolation spandrel timm tensorboard poetry "flash-attn<=2.8.0" "xformers==0.0.31.post1" "file:./pkg/sageattention-2.2.0-cp312-cp312-linux_x86_64.whl"
|
||||||
# this exotic command will determine the correct torchaudio to install for the image
|
# this exotic command will determine the correct torchaudio to install for the image
|
||||||
RUN <<-EOF
|
RUN <<-EOF
|
||||||
python -c 'import torch, re, subprocess
|
python -c 'import torch, re, subprocess
|
||||||
@ -66,7 +66,7 @@ WORKDIR /workspace
|
|||||||
# addresses https://github.com/pytorch/pytorch/issues/104801
|
# addresses https://github.com/pytorch/pytorch/issues/104801
|
||||||
# and issues reported by importing nodes_canny
|
# and issues reported by importing nodes_canny
|
||||||
# smoke test
|
# smoke test
|
||||||
RUN python -c "import torch; import xformers; import sageattention; import cv2" && comfyui --quick-test-for-ci --cpu --cwd /workspace
|
RUN python -c "import torch; import xformers; import sageattention; import cv2; import diffusers.hooks" && comfyui --quick-test-for-ci --cpu --cwd /workspace
|
||||||
|
|
||||||
EXPOSE 8188
|
EXPOSE 8188
|
||||||
CMD ["python", "-m", "comfy.cmd.main", "--listen", "--use-sage-attention", "--reserve-vram=0", "--logging-level=INFO", "--enable-cors"]
|
CMD ["python", "-m", "comfy.cmd.main", "--listen", "--use-sage-attention", "--reserve-vram=0", "--logging-level=INFO", "--enable-cors"]
|
||||||
|
|||||||
@ -156,6 +156,7 @@ def _create_parser() -> EnhancedConfigArgParser:
|
|||||||
parser.add_argument("--whitelist-custom-nodes", type=str, action=FlattenAndAppendAction, nargs='+', default=[], help="Specify custom node folders to load even when --disable-all-custom-nodes is enabled.")
|
parser.add_argument("--whitelist-custom-nodes", type=str, action=FlattenAndAppendAction, nargs='+', default=[], help="Specify custom node folders to load even when --disable-all-custom-nodes is enabled.")
|
||||||
parser.add_argument("--blacklist-custom-nodes", type=str, action=FlattenAndAppendAction, nargs='+', default=[], help="Specify custom node folders to never load. Accepts shell-style globs.")
|
parser.add_argument("--blacklist-custom-nodes", type=str, action=FlattenAndAppendAction, nargs='+', default=[], help="Specify custom node folders to never load. Accepts shell-style globs.")
|
||||||
parser.add_argument("--disable-api-nodes", action="store_true", help="Disable loading all api nodes.")
|
parser.add_argument("--disable-api-nodes", action="store_true", help="Disable loading all api nodes.")
|
||||||
|
parser.add_argument("--enable-eval", action="store_true", help="Enable nodes that can evaluate Python code in workflows.")
|
||||||
|
|
||||||
parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.")
|
parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.")
|
||||||
parser.add_argument("--create-directories", action="store_true",
|
parser.add_argument("--create-directories", action="store_true",
|
||||||
|
|||||||
@ -169,6 +169,7 @@ class Configuration(dict):
|
|||||||
whitelist_custom_nodes (list[str]): Specify custom node folders to load even when --disable-all-custom-nodes is enabled.
|
whitelist_custom_nodes (list[str]): Specify custom node folders to load even when --disable-all-custom-nodes is enabled.
|
||||||
default_device (Optional[int]): Set the id of the default device, all other devices will stay visible.
|
default_device (Optional[int]): Set the id of the default device, all other devices will stay visible.
|
||||||
block_runtime_package_installation (Optional[bool]): When set, custom nodes like ComfyUI Manager, Easy Use, Nunchaku and others will not be able to use pip or uv to install packages at runtime (experimental).
|
block_runtime_package_installation (Optional[bool]): When set, custom nodes like ComfyUI Manager, Easy Use, Nunchaku and others will not be able to use pip or uv to install packages at runtime (experimental).
|
||||||
|
enable_eval (Optional[bool]): Enable nodes that can evaluate Python code in workflows.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@ -288,6 +289,7 @@ class Configuration(dict):
|
|||||||
self.database_url: str = db_config()
|
self.database_url: str = db_config()
|
||||||
self.default_device: Optional[int] = None
|
self.default_device: Optional[int] = None
|
||||||
self.block_runtime_package_installation = None
|
self.block_runtime_package_installation = None
|
||||||
|
self.enable_eval: Optional[bool] = False
|
||||||
|
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self[key] = value
|
self[key] = value
|
||||||
@ -420,6 +422,7 @@ class FlattenAndAppendAction(argparse.Action):
|
|||||||
Custom action to handle comma-separated values and multiple invocations
|
Custom action to handle comma-separated values and multiple invocations
|
||||||
of the same argument, flattening them into a single list.
|
of the same argument, flattening them into a single list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
items = getattr(namespace, self.dest, None)
|
items = getattr(namespace, self.dest, None)
|
||||||
if items is None:
|
if items is None:
|
||||||
|
|||||||
0
comfy_extras/eval_web/__init__.py
Normal file
0
comfy_extras/eval_web/__init__.py
Normal file
769
comfy_extras/eval_web/ace_utils.js
Normal file
769
comfy_extras/eval_web/ace_utils.js
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
/**
|
||||||
|
* Uses code adapted from https://github.com/yorkane/ComfyUI-KYNode
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 Kevin Yuan
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Make modal window
|
||||||
|
function makeModal({ title = "Message", text = "No text", type = "info", parent = null, stylePos = "fixed", classes = [] } = {}) {
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
Object.assign(overlay.style, {
|
||||||
|
display: "none",
|
||||||
|
position: stylePos,
|
||||||
|
background: "rgba(0 0 0 / 0.8)",
|
||||||
|
opacity: 0,
|
||||||
|
top: "0",
|
||||||
|
left: "0",
|
||||||
|
right: "0",
|
||||||
|
bottom: "0",
|
||||||
|
zIndex: "500",
|
||||||
|
transition: "all .8s",
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
|
const boxModal = document.createElement("div");
|
||||||
|
Object.assign(boxModal.style, {
|
||||||
|
transition: "all 0.5s",
|
||||||
|
opacity: 0,
|
||||||
|
display: "none",
|
||||||
|
position: stylePos,
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%,-50%)",
|
||||||
|
background: "#525252",
|
||||||
|
minWidth: "300px",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
zIndex: "501",
|
||||||
|
border: "1px solid rgb(255 255 255 / 45%)",
|
||||||
|
});
|
||||||
|
|
||||||
|
boxModal.className = "alekpet_modal_window";
|
||||||
|
boxModal.classList.add(...classes);
|
||||||
|
|
||||||
|
const boxModalBody = document.createElement("div");
|
||||||
|
Object.assign(boxModalBody.style, {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
textAlign: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
boxModalBody.className = "alekpet_modal_body";
|
||||||
|
|
||||||
|
const boxModalHtml = `
|
||||||
|
<div class="alekpet_modal_header" style="display: flex; align-items: center; background: #222; width: 100%;justify-content: center;">
|
||||||
|
<div class="alekpet_modal_title" style="flex-basis: 85%; text-align: center;padding: 5px;">${title}</div>
|
||||||
|
<div class="alekpet_modal_close">✕</div>
|
||||||
|
</div>
|
||||||
|
<div class="alekpet_modal_description" style="padding: 8px;">${text}</div>`;
|
||||||
|
boxModalBody.innerHTML = boxModalHtml;
|
||||||
|
|
||||||
|
const alekpet_modal_header = boxModalBody.querySelector(".alekpet_modal_header");
|
||||||
|
Object.assign(alekpet_modal_header.style, {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = boxModalBody.querySelector(".alekpet_modal_close");
|
||||||
|
Object.assign(close.style, {
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
|
let parentElement = document.body;
|
||||||
|
if (parent && parent.nodeType === 1) {
|
||||||
|
parentElement = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
boxModal.append(boxModalBody);
|
||||||
|
parentElement.append(overlay, boxModal);
|
||||||
|
|
||||||
|
const removeEvent = new Event("removeElements");
|
||||||
|
const remove = () => {
|
||||||
|
animateTransitionProps(boxModal, { opacity: 0 }).then(() =>
|
||||||
|
animateTransitionProps(overlay, { opacity: 0 }).then(() => {
|
||||||
|
parentElement.removeChild(boxModal);
|
||||||
|
parentElement.removeChild(overlay);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
boxModal.addEventListener("removeElements", remove);
|
||||||
|
overlay.addEventListener("removeElements", remove);
|
||||||
|
|
||||||
|
animateTransitionProps(overlay)
|
||||||
|
.then(() => {
|
||||||
|
overlay.addEventListener("click", () => {
|
||||||
|
overlay.dispatchEvent(removeEvent);
|
||||||
|
});
|
||||||
|
animateTransitionProps(boxModal);
|
||||||
|
})
|
||||||
|
.then(() => boxModal.querySelector(".alekpet_modal_close").addEventListener("click", () => boxModal.dispatchEvent(removeEvent)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWidget(node, value, attr = "name", func = "find") {
|
||||||
|
return node?.widgets ? node.widgets[func]((w) => (Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateTransitionProps(el, props = { opacity: 1 }, preStyles = { display: "block" }) {
|
||||||
|
Object.assign(el.style, preStyles);
|
||||||
|
|
||||||
|
el.style.transition = !el.style.transition || !window.getComputedStyle(el).getPropertyValue("transition") ? "all .8s" : el.style.transition;
|
||||||
|
|
||||||
|
return new Promise((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Object.assign(el.style, props);
|
||||||
|
|
||||||
|
const transstart = () => (el.isAnimating = true);
|
||||||
|
const transchancel = () => (el.isAnimating = false);
|
||||||
|
el.addEventListener("transitionstart", transstart);
|
||||||
|
el.addEventListener("transitioncancel", transchancel);
|
||||||
|
|
||||||
|
el.addEventListener("transitionend", function transend() {
|
||||||
|
el.isAnimating = false;
|
||||||
|
el.removeEventListener("transitionend", transend);
|
||||||
|
el.removeEventListener("transitionend", transchancel);
|
||||||
|
el.removeEventListener("transitionend", transstart);
|
||||||
|
res(el);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateClick(target, params = {}) {
|
||||||
|
const { opacityVal = 0.9, callback = () => {} } = params;
|
||||||
|
if (target?.isAnimating) return;
|
||||||
|
|
||||||
|
const hide = +target.style.opacity === 0;
|
||||||
|
return animateTransitionProps(target, {
|
||||||
|
opacity: hide ? opacityVal : 0,
|
||||||
|
}).then((el) => {
|
||||||
|
const isHide = hide || el.style.display === "none";
|
||||||
|
showHide({ elements: [target], hide: !hide });
|
||||||
|
callback();
|
||||||
|
return isHide;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHide({ elements = [], hide = null, displayProp = "block" } = {}) {
|
||||||
|
Array.from(elements).forEach((el) => {
|
||||||
|
if (hide !== null) {
|
||||||
|
el.style.display = !hide ? displayProp : "none";
|
||||||
|
} else {
|
||||||
|
el.style.display = !el.style.display || el.style.display === "none" ? displayProp : "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmptyObject(obj) {
|
||||||
|
if (!obj) return true;
|
||||||
|
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeElement(tag, attrs = {}) {
|
||||||
|
if (!tag) tag = "div";
|
||||||
|
const element = document.createElement(tag);
|
||||||
|
Object.keys(attrs).forEach((key) => {
|
||||||
|
const currValue = attrs[key];
|
||||||
|
if (key === "class") {
|
||||||
|
if (Array.isArray(currValue)) {
|
||||||
|
element.classList.add(...currValue);
|
||||||
|
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||||
|
element.className = currValue;
|
||||||
|
}
|
||||||
|
} else if (key === "dataset") {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(currValue)) {
|
||||||
|
currValue.forEach((datasetArr) => {
|
||||||
|
const [prop, propval] = Object.entries(datasetArr)[0];
|
||||||
|
element.dataset[prop] = propval;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.entries(currValue).forEach((datasetArr) => {
|
||||||
|
const [prop, propval] = datasetArr;
|
||||||
|
element.dataset[prop] = propval;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
} else if (key === "style") {
|
||||||
|
if (typeof currValue === "object" && !Array.isArray(currValue) && Object.keys(currValue).length) {
|
||||||
|
Object.assign(element[key], currValue);
|
||||||
|
} else if (typeof currValue === "object" && Array.isArray(currValue) && currValue.length) {
|
||||||
|
element[key] = [...currValue];
|
||||||
|
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||||
|
element[key] = currValue;
|
||||||
|
}
|
||||||
|
} else if (["for"].includes(key)) {
|
||||||
|
element.setAttribute(key, currValue);
|
||||||
|
} else if (key === "children") {
|
||||||
|
element.append(...(currValue instanceof Array ? currValue : [currValue]));
|
||||||
|
} else if (key === "parent") {
|
||||||
|
currValue.append(element);
|
||||||
|
} else {
|
||||||
|
element[key] = currValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidStyle(opt, strColor) {
|
||||||
|
let op = new Option().style;
|
||||||
|
if (!op.hasOwnProperty(opt)) return { result: false, color: "", color_hex: "" };
|
||||||
|
|
||||||
|
op[opt] = strColor;
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: op[opt] !== "",
|
||||||
|
color_rgb: op[opt],
|
||||||
|
color_hex: rgbToHex(op[opt]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(rgb) {
|
||||||
|
const regEx = new RegExp(/\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
||||||
|
if (regEx.test(rgb)) {
|
||||||
|
let [, r, g, b] = regEx.exec(rgb);
|
||||||
|
r = parseInt(r).toString(16);
|
||||||
|
g = parseInt(g).toString(16);
|
||||||
|
b = parseInt(b).toString(16);
|
||||||
|
|
||||||
|
r = r.length === 1 ? r + "0" : r;
|
||||||
|
g = g.length === 1 ? g + "0" : g;
|
||||||
|
b = b.length === 1 ? b + "0" : b;
|
||||||
|
|
||||||
|
return `#${r}${g}${b}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataJSON(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const jsonData = await response.json();
|
||||||
|
return jsonData;
|
||||||
|
} catch (err) {
|
||||||
|
return new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMerge(target, source) {
|
||||||
|
if (source?.nodeType) return;
|
||||||
|
for (let key in source) {
|
||||||
|
if (source[key] instanceof Object && key in target) {
|
||||||
|
Object.assign(source[key], deepMerge(target[key], source[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(target || {}, source);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const THEME_MODAL_WINDOW_BASE = {
|
||||||
|
stylesTitle: {
|
||||||
|
background: "auto",
|
||||||
|
padding: "5px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
marginBottom: "5px",
|
||||||
|
alignSelf: "stretch",
|
||||||
|
},
|
||||||
|
stylesWrapper: {
|
||||||
|
display: "none",
|
||||||
|
opacity: 0,
|
||||||
|
minWidth: "220px",
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
transition: "all .8s",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
zIndex: 99999,
|
||||||
|
},
|
||||||
|
stylesBox: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
background: "#0e0e0e",
|
||||||
|
padding: "6px",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "3px",
|
||||||
|
textAlign: "center",
|
||||||
|
borderRadius: "6px",
|
||||||
|
color: "white",
|
||||||
|
border: "2px solid silver",
|
||||||
|
boxShadow: "2px 2px 4px silver",
|
||||||
|
maxWidth: "300px",
|
||||||
|
},
|
||||||
|
stylesClose: {
|
||||||
|
position: "absolute",
|
||||||
|
top: "-10px",
|
||||||
|
right: "-10px",
|
||||||
|
background: "silver",
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const THEMES_MODAL_WINDOW = {
|
||||||
|
error: {
|
||||||
|
stylesTitle: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||||
|
background: "#8f210f",
|
||||||
|
},
|
||||||
|
stylesBox: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||||
|
background: "#3b2222",
|
||||||
|
boxShadow: "3px 3px 6px #141414",
|
||||||
|
border: "1px solid #f91b1b",
|
||||||
|
},
|
||||||
|
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||||
|
stylesClose: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||||
|
background: "#3b2222",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
stylesTitle: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||||
|
background: "#e99818",
|
||||||
|
},
|
||||||
|
stylesBox: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||||
|
background: "#594e32",
|
||||||
|
boxShadow: "3px 3px 6px #141414",
|
||||||
|
border: "1px solid #e99818",
|
||||||
|
},
|
||||||
|
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||||
|
stylesClose: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||||
|
background: "#594e32",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
normal: {
|
||||||
|
stylesTitle: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||||
|
background: "#108f0f",
|
||||||
|
},
|
||||||
|
stylesBox: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||||
|
background: "#223b2a",
|
||||||
|
boxShadow: "3px 3px 6px #141414",
|
||||||
|
border: "1px solid #108f0f",
|
||||||
|
},
|
||||||
|
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||||
|
stylesClose: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||||
|
background: "#223b2a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
auto: {
|
||||||
|
autohide: false,
|
||||||
|
autoshow: false,
|
||||||
|
autoremove: false,
|
||||||
|
propStyles: { opacity: 0 },
|
||||||
|
propPreStyles: {},
|
||||||
|
timewait: 2000,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
overlay_enabled: false,
|
||||||
|
overlayClasses: [],
|
||||||
|
overlayStyles: {},
|
||||||
|
},
|
||||||
|
close: { closeRemove: false, showClose: true },
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createWindowModal({ textTitle = "Message", textBody = "Hello world!", textFooter = null, classesWrapper = [], stylesWrapper = {}, classesBox = [], stylesBox = {}, classesTitle = [], stylesTitle = {}, classesBody = [], stylesBody = {}, classesClose = [], stylesClose = {}, classesFooter = [], stylesFooter = {}, options = defaultOptions } = {}) {
|
||||||
|
// Check all options exist
|
||||||
|
const _options = deepMerge(JSON.parse(JSON.stringify(defaultOptions)), options);
|
||||||
|
|
||||||
|
const {
|
||||||
|
parent,
|
||||||
|
overlay: { overlay_enabled, overlayClasses, overlayStyles },
|
||||||
|
close: { closeRemove, showClose },
|
||||||
|
auto: { autohide, autoshow, autoremove, timewait, propStyles, propPreStyles },
|
||||||
|
} = _options;
|
||||||
|
|
||||||
|
// Function past text(html)
|
||||||
|
function addText(text, parent) {
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
switch (typeof text) {
|
||||||
|
case "string":
|
||||||
|
if (/^\<.*\/?\>$/.test(text)) {
|
||||||
|
parent.innerHTML = text;
|
||||||
|
} else {
|
||||||
|
parent.textContent = text;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "object":
|
||||||
|
default:
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
text.forEach((element) => (element.nodeType === 1 || element.nodeType === 3) && parent.append(element));
|
||||||
|
} else if (text.nodeType === 1 || text.nodeType === 3) parent.append(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay
|
||||||
|
let overlayElement = null;
|
||||||
|
if (overlay_enabled) {
|
||||||
|
overlayElement = makeElement("div", {
|
||||||
|
class: [...overlayClasses],
|
||||||
|
style: {
|
||||||
|
display: "none",
|
||||||
|
position: "fixed",
|
||||||
|
background: "rgba(0 0 0 / 0.8)",
|
||||||
|
opacity: 0,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
transition: "all .8s",
|
||||||
|
cursor: "pointer",
|
||||||
|
...overlayStyles,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper
|
||||||
|
const wrapper_settings = makeElement("div", {
|
||||||
|
class: ["alekpet__wrapper__window", ...classesWrapper],
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(wrapper_settings.style, {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesWrapper,
|
||||||
|
...stylesWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Box
|
||||||
|
const box__settings = makeElement("div", {
|
||||||
|
class: ["alekpet__window__box", ...classesBox],
|
||||||
|
});
|
||||||
|
Object.assign(box__settings.style, {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||||
|
...stylesBox,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let box_settings_title = "";
|
||||||
|
if (textTitle) {
|
||||||
|
box_settings_title = makeElement("div", {
|
||||||
|
class: ["alekpet__window__title", ...classesTitle],
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(box_settings_title.style, {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||||
|
...stylesTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add text (html) to title
|
||||||
|
addText(textTitle, box_settings_title);
|
||||||
|
}
|
||||||
|
// Body
|
||||||
|
let box_settings_body = "";
|
||||||
|
if (textBody) {
|
||||||
|
box_settings_body = makeElement("div", {
|
||||||
|
class: ["alekpet__window__body", ...classesBody],
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(box_settings_body.style, {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: "5px",
|
||||||
|
textWrap: "wrap",
|
||||||
|
...stylesBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add text (html) to body
|
||||||
|
addText(textBody, box_settings_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
const close__box__button = makeElement("div", {
|
||||||
|
class: ["close__box__button", ...classesClose],
|
||||||
|
textContent: "✖",
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(close__box__button.style, {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||||
|
...stylesClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!showClose) close__box__button.style.display = "none";
|
||||||
|
|
||||||
|
const closeEvent = new Event("closeModal");
|
||||||
|
const closeModalWindow = function () {
|
||||||
|
overlay_enabled
|
||||||
|
? animateTransitionProps(overlayElement, {
|
||||||
|
opacity: 0,
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
animateTransitionProps(wrapper_settings, {
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (closeRemove) {
|
||||||
|
parent.removeChild(wrapper_settings);
|
||||||
|
parent.removeChild(overlayElement);
|
||||||
|
} else {
|
||||||
|
showHide({ elements: [wrapper_settings, overlayElement] });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: animateTransitionProps(wrapper_settings, {
|
||||||
|
opacity: 0,
|
||||||
|
}).then(() => {
|
||||||
|
showHide({ elements: [wrapper_settings] });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
close__box__button.addEventListener("closeModal", closeModalWindow);
|
||||||
|
|
||||||
|
close__box__button.addEventListener("click", () => close__box__button.dispatchEvent(closeEvent));
|
||||||
|
|
||||||
|
close__box__button.onmouseenter = () => {
|
||||||
|
close__box__button.style.opacity = 0.8;
|
||||||
|
};
|
||||||
|
|
||||||
|
close__box__button.onmouseleave = () => {
|
||||||
|
close__box__button.style.opacity = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
box__settings.append(box_settings_title, box_settings_body);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
if (textFooter) {
|
||||||
|
const box_settings_footer = makeElement("div", {
|
||||||
|
class: [...classesFooter],
|
||||||
|
});
|
||||||
|
Object.assign(box_settings_footer.style, {
|
||||||
|
...stylesFooter,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add text (html) to body
|
||||||
|
addText(textFooter, box_settings_footer);
|
||||||
|
|
||||||
|
box__settings.append(box_settings_footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper_settings.append(close__box__button, box__settings);
|
||||||
|
|
||||||
|
if (parent && parent.nodeType === 1) {
|
||||||
|
if (overlay_enabled) parent.append(overlayElement);
|
||||||
|
parent.append(wrapper_settings);
|
||||||
|
|
||||||
|
if (autoshow) {
|
||||||
|
overlay_enabled
|
||||||
|
? animateClick(overlayElement).then(() =>
|
||||||
|
animateClick(wrapper_settings).then(
|
||||||
|
() =>
|
||||||
|
autohide &&
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles })
|
||||||
|
.then(() => animateTransitionProps(overlayElement, { ...propStyles }, { ...propPreStyles }))
|
||||||
|
.then(() => {
|
||||||
|
if (autoremove) {
|
||||||
|
parent.removeChild(wrapper_settings);
|
||||||
|
parent.removeChild(overlayElement);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
timewait,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: animateClick(wrapper_settings).then(() => autohide && setTimeout(() => animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles }).then(() => autoremove && parent.removeChild(wrapper_settings)), timewait));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper_settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt
|
||||||
|
async function comfyuiDesktopPrompt(title, message, defaultValue) {
|
||||||
|
try {
|
||||||
|
return await app.extensionManager.dialog.prompt({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
defaultValue,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return prompt(title, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
function comfyuiDesktopAlert(message) {
|
||||||
|
try {
|
||||||
|
app.extensionManager.toast.addAlert(message);
|
||||||
|
} catch (err) {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
function confirmModal({ title, message }) {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const overlay = makeElement("div", {
|
||||||
|
class: ["alekpet_confOverlay"],
|
||||||
|
style: {
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = makeElement("div", {
|
||||||
|
class: ["alekpet_confModal"],
|
||||||
|
style: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||||
|
position: "fixed",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
background: "rgb(92 186 255 / 20%)",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
borderColor: "rgba(92, 186, 255, 0.63)",
|
||||||
|
boxShadow: "rgba(92, 186, 255, 0.63) 2px 2px 4px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleEl = makeElement("div", {
|
||||||
|
class: ["alekpet_confTitle"],
|
||||||
|
style: {
|
||||||
|
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||||
|
background: "rgba(92, 186, 255, 0.63)",
|
||||||
|
},
|
||||||
|
textContent: title,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageEl = makeElement("div", {
|
||||||
|
class: ["alekpet_confMessage"],
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: "5px",
|
||||||
|
textWrap: "wrap",
|
||||||
|
},
|
||||||
|
textContent: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
const action_box = makeElement("div", {
|
||||||
|
class: ["alekpet_confActions"],
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "5px",
|
||||||
|
width: "100%",
|
||||||
|
padding: "4px",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
modal.remove();
|
||||||
|
overlay.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ok = makeElement("div", {
|
||||||
|
class: ["alekpet_confButtons", "alekpet_confButtonOk"],
|
||||||
|
style: {
|
||||||
|
background: "linear-gradient(45deg, green, limegreen) rgb(21, 100, 6)",
|
||||||
|
},
|
||||||
|
textContent: "Ok",
|
||||||
|
onclick: (e) => {
|
||||||
|
res(true);
|
||||||
|
remove();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Cancel = makeElement("div", {
|
||||||
|
class: ["alekpet_confButtons", "alekpet_confButtonCancel"],
|
||||||
|
style: {
|
||||||
|
background: "linear-gradient(45deg, #b64396, #a52a8b) rgb(135 3 161)",
|
||||||
|
},
|
||||||
|
textContent: "Cancel",
|
||||||
|
onclick: (e) => {
|
||||||
|
res(false);
|
||||||
|
remove();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
action_box.append(ok, Cancel);
|
||||||
|
modal.append(titleEl, messageEl, action_box);
|
||||||
|
overlay.append(modal);
|
||||||
|
document.body.append(overlay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function comfyuiDesktopConfirm(message) {
|
||||||
|
try {
|
||||||
|
const result = await confirmModal({
|
||||||
|
title: "Confirm",
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait update comfyui frontend! Confirm Cancel not return value! Fixed in ComfyUI_frontend ver. v1.10.8
|
||||||
|
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2649
|
||||||
|
// const result = await app.extensionManager.dialog.confirm({
|
||||||
|
// title: "Confirm",
|
||||||
|
// message: message,
|
||||||
|
// });
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return confirm(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
makeModal,
|
||||||
|
createWindowModal,
|
||||||
|
animateTransitionProps,
|
||||||
|
animateClick,
|
||||||
|
showHide,
|
||||||
|
makeElement,
|
||||||
|
getDataJSON,
|
||||||
|
isEmptyObject,
|
||||||
|
isValidStyle,
|
||||||
|
rgbToHex,
|
||||||
|
findWidget,
|
||||||
|
THEMES_MODAL_WINDOW,
|
||||||
|
//
|
||||||
|
comfyuiDesktopConfirm,
|
||||||
|
comfyuiDesktopPrompt,
|
||||||
|
comfyuiDesktopAlert,
|
||||||
|
};
|
||||||
377
comfy_extras/eval_web/ky_eval_python.js
Normal file
377
comfy_extras/eval_web/ky_eval_python.js
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
/**
|
||||||
|
* Uses code adapted from https://github.com/yorkane/ComfyUI-KYNode
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 Kevin Yuan
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
import * as ace from "https://cdn.jsdelivr.net/npm/ace-code@1.43.4/+esm";
|
||||||
|
import { makeElement, findWidget } from "./ace_utils.js";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const varTypes = ["int", "boolean", "string", "float", "json", "list", "dict"];
|
||||||
|
const typeMap = {
|
||||||
|
int: "int",
|
||||||
|
boolean: "bool",
|
||||||
|
string: "str",
|
||||||
|
float: "float",
|
||||||
|
json: "json",
|
||||||
|
list: "list",
|
||||||
|
dict: "dict",
|
||||||
|
};
|
||||||
|
|
||||||
|
ace.config.setModuleLoader('ace/mode/python', () =>
|
||||||
|
import('https://cdn.jsdelivr.net/npm/ace-builds@1.43.4/src/mode-python.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
ace.config.setModuleLoader('ace/theme/monokai', () =>
|
||||||
|
import('https://cdn.jsdelivr.net/npm/ace-builds@1.43.4/src/theme-monokai.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
function getPostition(node, ctx, w_width, y, n_height) {
|
||||||
|
const margin = 5;
|
||||||
|
|
||||||
|
const rect = ctx.canvas.getBoundingClientRect();
|
||||||
|
const transform = new DOMMatrix()
|
||||||
|
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
|
||||||
|
.multiplySelf(ctx.getTransform())
|
||||||
|
.translateSelf(margin, margin + y);
|
||||||
|
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transformOrigin: "0 0",
|
||||||
|
transform: scale,
|
||||||
|
left: `${transform.a + transform.e + rect.left}px`,
|
||||||
|
top: `${transform.d + transform.f + rect.top}px`,
|
||||||
|
maxWidth: `${w_width - margin * 2}px`,
|
||||||
|
maxHeight: `${n_height - margin * 2 - y - 15}px`,
|
||||||
|
width: `${w_width - margin * 2}px`,
|
||||||
|
height: "90%",
|
||||||
|
position: "absolute",
|
||||||
|
scrollbarColor: "var(--descrip-text) var(--bg-color)",
|
||||||
|
scrollbarWidth: "thin",
|
||||||
|
zIndex: app.graph._nodes.indexOf(node),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create editor code
|
||||||
|
function codeEditor(node, inputName, inputData) {
|
||||||
|
const widget = {
|
||||||
|
type: "pycode",
|
||||||
|
name: inputName,
|
||||||
|
options: { hideOnZoom: true },
|
||||||
|
value:
|
||||||
|
inputData[1]?.default ||
|
||||||
|
`def my(a, b=1):
|
||||||
|
return a * b<br>
|
||||||
|
|
||||||
|
r0 = str(my(23, 9))`,
|
||||||
|
draw(ctx, node, widget_width, y, widget_height) {
|
||||||
|
const hidden = node.flags?.collapsed || (!!widget.options.hideOnZoom && app.canvas.ds.scale < 0.5) || widget.type === "converted-widget" || widget.type === "hidden";
|
||||||
|
|
||||||
|
widget.codeElement.hidden = hidden;
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
widget.options.onHide?.(widget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this.codeElement.style, getPostition(node, ctx, widget_width, y, node.size[1]));
|
||||||
|
},
|
||||||
|
computeSize(...args) {
|
||||||
|
return [500, 250];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.codeElement = makeElement("pre", {
|
||||||
|
innerHTML: widget.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.editor = ace.edit(widget.codeElement);
|
||||||
|
widget.editor.setTheme("ace/theme/monokai");
|
||||||
|
widget.editor.session.setMode("ace/mode/python");
|
||||||
|
widget.editor.setOptions({
|
||||||
|
enableAutoIndent: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
});
|
||||||
|
widget.codeElement.hidden = true;
|
||||||
|
|
||||||
|
document.body.appendChild(widget.codeElement);
|
||||||
|
|
||||||
|
const collapse = node.collapse;
|
||||||
|
node.collapse = function () {
|
||||||
|
collapse.apply(this, arguments);
|
||||||
|
if (this.flags?.collapsed) {
|
||||||
|
widget.codeElement.hidden = true;
|
||||||
|
} else {
|
||||||
|
if (this.flags?.collapsed === false) {
|
||||||
|
widget.codeElement.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save data to workflow forced!
|
||||||
|
function saveValue() {
|
||||||
|
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register extensions
|
||||||
|
app.registerExtension({
|
||||||
|
name: "KYNode.KY_Eval_Python",
|
||||||
|
getCustomWidgets(app) {
|
||||||
|
return {
|
||||||
|
PYCODE: (node, inputName, inputData, app) => {
|
||||||
|
const widget = codeEditor(node, inputName, inputData);
|
||||||
|
|
||||||
|
widget.editor.getSession().on("change", function (e) {
|
||||||
|
widget.value = widget.editor.getValue();
|
||||||
|
saveValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
const varTypeList = node.addWidget(
|
||||||
|
"combo",
|
||||||
|
"select_type",
|
||||||
|
"string",
|
||||||
|
(v) => {
|
||||||
|
// widget.editor.setTheme(`ace/theme/${varTypeList.value}`);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
values: varTypes,
|
||||||
|
serialize: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. 使用 addDOMWidget 将容器添加到节点上
|
||||||
|
// - 第一个参数是 widget 的名称,在节点内部需要是唯一的。
|
||||||
|
// - 第二个参数是 widget 的类型,对于自定义 DOM 元素,通常是 "div"。
|
||||||
|
// - 第三个参数是您创建的 DOM 元素。
|
||||||
|
// - 第四个参数是一个选项对象,可以用来配置 widget。
|
||||||
|
// node.addDOMWidget("rowOfButtons", "div", container, {
|
||||||
|
// });
|
||||||
|
node.addWidget("button", "Add Input variable", "add_input_variable", async () => {
|
||||||
|
// Input name variable and check
|
||||||
|
let nameInput = node?.inputs?.length ? `p${node.inputs.length - 1}` : "p0";
|
||||||
|
|
||||||
|
const currentWidth = node.size[0];
|
||||||
|
let tp = varTypeList.value;
|
||||||
|
nameInput = nameInput + "_" + typeMap[tp];
|
||||||
|
node.addInput(nameInput, "*");
|
||||||
|
node.setSize([currentWidth, node.size[1]]);
|
||||||
|
let cv = widget.editor.getValue();
|
||||||
|
if (tp === "json") {
|
||||||
|
cv = cv + "\n" + nameInput + " = json.loads(" + nameInput + ")";
|
||||||
|
} else if (tp === "list") {
|
||||||
|
cv = cv + "\n" + nameInput + " = []";
|
||||||
|
} else if (tp === "dict") {
|
||||||
|
cv = cv + "\n" + nameInput + " = {}";
|
||||||
|
} else {
|
||||||
|
cv = cv + "\n" + nameInput + " = " + typeMap[tp] + "(" + nameInput + ")";
|
||||||
|
}
|
||||||
|
widget.editor.setValue(cv);
|
||||||
|
saveValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addWidget("button", "Add Output variable", "add_output_variable", async () => {
|
||||||
|
const currentWidth = node.size[0];
|
||||||
|
// Output name variable
|
||||||
|
let nameOutput = node?.outputs?.length ? `r${node.outputs.length}` : "r0";
|
||||||
|
let tp = varTypeList.value;
|
||||||
|
nameOutput = nameOutput + "_" + typeMap[tp];
|
||||||
|
node.addOutput(nameOutput, tp);
|
||||||
|
node.setSize([currentWidth, node.size[1]]);
|
||||||
|
let cv = widget.editor.getValue();
|
||||||
|
if (tp === "json") {
|
||||||
|
cv = cv + "\n" + nameOutput + " = json.dumps(" + nameOutput + ")";
|
||||||
|
} else if (tp === "list") {
|
||||||
|
cv = cv + "\n" + nameOutput + " = []";
|
||||||
|
} else if (tp === "dict") {
|
||||||
|
cv = cv + "\n" + nameOutput + " = {}";
|
||||||
|
} else {
|
||||||
|
cv = cv + "\n" + nameOutput + " = " + typeMap[tp] + "(" + nameOutput + ")";
|
||||||
|
}
|
||||||
|
widget.editor.setValue(cv);
|
||||||
|
saveValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
node.onRemoved = function () {
|
||||||
|
for (const w of node?.widgets) {
|
||||||
|
if (w?.codeElement) w.codeElement.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
node.addCustomWidget(widget);
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
// --- IDENode
|
||||||
|
if (nodeData.name === "KY_Eval_Python") {
|
||||||
|
// Node Created
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = async function () {
|
||||||
|
const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||||
|
|
||||||
|
const node_title = await this.getTitle();
|
||||||
|
const nodeName = `${nodeData.name}_${this.id}`;
|
||||||
|
|
||||||
|
this.name = nodeName;
|
||||||
|
|
||||||
|
// Create default inputs, when first create node
|
||||||
|
if (this?.inputs?.length < 2) {
|
||||||
|
["p0_str"].forEach((inputName) => {
|
||||||
|
const currentWidth = this.size[0];
|
||||||
|
this.addInput(inputName, "*");
|
||||||
|
this.setSize([currentWidth, this.size[1]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetEditor = findWidget(this, "pycode", "type");
|
||||||
|
|
||||||
|
this.setSize([530, this.size[1]]);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||||
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||||
|
const r = onDrawForeground?.apply?.(this, arguments);
|
||||||
|
|
||||||
|
// if (this.flags?.collapsed) return r;
|
||||||
|
|
||||||
|
if (this?.outputs?.length) {
|
||||||
|
for (let o = 0; o < this.outputs.length; o++) {
|
||||||
|
const { name, type } = this.outputs[o];
|
||||||
|
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
|
||||||
|
const nameSize = ctx.measureText(name);
|
||||||
|
const typeSize = ctx.measureText(`[${type === "*" ? "any" : type.toLowerCase()}]`);
|
||||||
|
|
||||||
|
ctx.fillStyle = colorType === "" ? "#AAA" : colorType;
|
||||||
|
ctx.font = "12px Arial, sans-serif";
|
||||||
|
ctx.textAlign = "right";
|
||||||
|
ctx.fillText(`[${type === "*" ? "any" : type.toLowerCase()}]`, this.size[0] - nameSize.width - typeSize.width, o * 20 + 19);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this?.inputs?.length) {
|
||||||
|
const not_showing = ["select_type", "pycode"];
|
||||||
|
for (let i = 1; i < this.inputs.length; i++) {
|
||||||
|
const { name, type } = this.inputs[i];
|
||||||
|
if (not_showing.includes(name)) continue;
|
||||||
|
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
|
||||||
|
const nameSize = ctx.measureText(name);
|
||||||
|
|
||||||
|
ctx.fillStyle = !colorType || colorType === "" ? "#AAA" : colorType;
|
||||||
|
ctx.font = "12px Arial, sans-serif";
|
||||||
|
ctx.textAlign = "left";
|
||||||
|
ctx.fillText(`[${type === "*" ? "any" : type.toLowerCase()}]`, nameSize.width + 25, i * 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Node Configure
|
||||||
|
const onConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function (node) {
|
||||||
|
onConfigure?.apply(this, arguments);
|
||||||
|
if (node?.widgets_values?.length) {
|
||||||
|
const widget_code_id = findWidget(this, "pycode", "type", "findIndex");
|
||||||
|
const widget_theme_id = findWidget(this, "varTypeList", "name", "findIndex");
|
||||||
|
const widget_language_id = findWidget(this, "language", "name", "findIndex");
|
||||||
|
|
||||||
|
const editor = this.widgets[widget_code_id]?.editor;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
// editor.setTheme(
|
||||||
|
// `ace/theme/${this.widgets_values[widget_theme_id]}`
|
||||||
|
// );
|
||||||
|
// editor.session.setMode(
|
||||||
|
// `ace/mode/${this.widgets_values[widget_language_id]}`
|
||||||
|
// );
|
||||||
|
editor.setValue(this.widgets_values[widget_code_id]);
|
||||||
|
editor.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ExtraMenuOptions
|
||||||
|
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||||
|
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||||
|
getExtraMenuOptions?.apply(this, arguments);
|
||||||
|
|
||||||
|
const past_index = options.length - 1;
|
||||||
|
const past = options[past_index];
|
||||||
|
|
||||||
|
if (!!past) {
|
||||||
|
// Inputs remove
|
||||||
|
for (const input_idx in this.inputs) {
|
||||||
|
const input = this.inputs[input_idx];
|
||||||
|
|
||||||
|
if (["language", "select_type"].includes(input.name)) continue;
|
||||||
|
|
||||||
|
options.splice(past_index + 1, 0, {
|
||||||
|
content: `Remove Input ${input.name}`,
|
||||||
|
callback: (e) => {
|
||||||
|
const currentWidth = this.size[0];
|
||||||
|
if (input.link) {
|
||||||
|
app.graph.removeLink(input.link);
|
||||||
|
}
|
||||||
|
this.removeInput(input_idx);
|
||||||
|
this.setSize([80, this.size[1]]);
|
||||||
|
saveValue();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output remove
|
||||||
|
for (const output_idx in this.outputs) {
|
||||||
|
const output = this.outputs[output_idx];
|
||||||
|
|
||||||
|
if (output.name === "r0") continue;
|
||||||
|
|
||||||
|
options.splice(past_index + 1, 0, {
|
||||||
|
content: `Remove Output ${output.name}`,
|
||||||
|
callback: (e) => {
|
||||||
|
const currentWidth = this.size[0];
|
||||||
|
if (output.link) {
|
||||||
|
app.graph.removeLink(output.link);
|
||||||
|
}
|
||||||
|
this.removeOutput(output_idx);
|
||||||
|
this.setSize([currentWidth, this.size[1]]);
|
||||||
|
saveValue();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// end - ExtraMenuOptions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
110
comfy_extras/nodes/nodes_eval.py
Normal file
110
comfy_extras/nodes/nodes_eval.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
import types
|
||||||
|
|
||||||
|
from comfy.execution_context import current_execution_context
|
||||||
|
from comfy.node_helpers import export_package_as_web_directory, export_custom_nodes
|
||||||
|
from comfy.nodes.package_typing import CustomNode
|
||||||
|
|
||||||
|
remove_type_name = re.compile(r"(\{.*\})", re.I | re.M)
|
||||||
|
|
||||||
|
|
||||||
|
# Hack: string type that is always equal in not equal comparisons, thanks pythongosssss
|
||||||
|
class AnyType(str):
|
||||||
|
def __ne__(self, __value: object) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
PY_CODE = AnyType("*")
|
||||||
|
IDEs_DICT = {}
|
||||||
|
|
||||||
|
|
||||||
|
# - Thank you very much for the class -> Trung0246 -
|
||||||
|
# - https://github.com/Trung0246/ComfyUI-0246/blob/main/utils.py#L51
|
||||||
|
class TautologyStr(str):
|
||||||
|
def __ne__(self, other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ByPassTypeTuple(tuple):
|
||||||
|
def __getitem__(self, index):
|
||||||
|
if index > 0:
|
||||||
|
index = 0
|
||||||
|
item = super().__getitem__(index)
|
||||||
|
if isinstance(item, str):
|
||||||
|
return TautologyStr(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class KY_Eval_Python(CustomNode):
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"pycode": (
|
||||||
|
"PYCODE",
|
||||||
|
{
|
||||||
|
"default": """import re, json, os, traceback
|
||||||
|
from time import strftime
|
||||||
|
|
||||||
|
def runCode():
|
||||||
|
nowDataTime = strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return f"Hello ComfyUI with us today {nowDataTime}!"
|
||||||
|
r0_str = runCode() + unique_id
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ByPassTypeTuple((PY_CODE,))
|
||||||
|
RETURN_NAMES = ("r0_str",)
|
||||||
|
FUNCTION = "exec_py"
|
||||||
|
DESCRIPTION = "IDE Node is an node that allows you to run code written in Python or Javascript directly in the node."
|
||||||
|
CATEGORY = "KYNode/Code"
|
||||||
|
|
||||||
|
def exec_py(self, pycode, unique_id, extra_pnginfo, **kwargs):
|
||||||
|
ctx = current_execution_context()
|
||||||
|
if ctx.configuration.enable_eval is not True:
|
||||||
|
raise ValueError("Python eval is disabled")
|
||||||
|
|
||||||
|
if unique_id not in IDEs_DICT:
|
||||||
|
IDEs_DICT[unique_id] = self
|
||||||
|
|
||||||
|
outputs = {unique_id: unique_id}
|
||||||
|
if extra_pnginfo and 'workflow' in extra_pnginfo and extra_pnginfo['workflow']:
|
||||||
|
for node in extra_pnginfo['workflow']['nodes']:
|
||||||
|
if node['id'] == int(unique_id):
|
||||||
|
outputs_valid = [ouput for ouput in node.get('outputs', []) if ouput.get('name', '') != '' and ouput.get('type', '') != '']
|
||||||
|
outputs = {ouput['name']: None for ouput in outputs_valid}
|
||||||
|
self.RETURN_TYPES = ByPassTypeTuple(out["type"] for out in outputs_valid)
|
||||||
|
self.RETURN_NAMES = tuple(name for name in outputs.keys())
|
||||||
|
my_namespace = types.SimpleNamespace()
|
||||||
|
# 从 prompt 对象中提取 prompt_id
|
||||||
|
# if extra_data and 'extra_data' in extra_data and 'prompt_id' in extra_data['extra_data']:
|
||||||
|
# prompt_id = prompt['extra_data']['prompt_id']
|
||||||
|
# outputs['p0_str'] = p0_str
|
||||||
|
|
||||||
|
my_namespace.__dict__.update(outputs)
|
||||||
|
my_namespace.__dict__.update({prop: kwargs[prop] for prop in kwargs})
|
||||||
|
# my_namespace.__dict__.setdefault("r0_str", "The r0 variable is not assigned")
|
||||||
|
|
||||||
|
try:
|
||||||
|
exec(pycode, my_namespace.__dict__)
|
||||||
|
except Exception as e:
|
||||||
|
err = traceback.format_exc()
|
||||||
|
mc = re.search(r'line (\d+), in <module>([\w\W]+)$', err, re.MULTILINE)
|
||||||
|
msg = mc[1] + ':' + mc[2]
|
||||||
|
my_namespace.r0 = f"Error Line{msg}"
|
||||||
|
|
||||||
|
new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])}
|
||||||
|
return (*new_dict.values(),)
|
||||||
|
|
||||||
|
|
||||||
|
export_custom_nodes()
|
||||||
|
export_package_as_web_directory("comfy_extras.eval_web")
|
||||||
@ -100,7 +100,7 @@ def frontend_backend_worker_with_rabbitmq(request, tmp_path_factory, num_workers
|
|||||||
|
|
||||||
frontend_command = [
|
frontend_command = [
|
||||||
"comfyui",
|
"comfyui",
|
||||||
"--listen=127.0.0.1",
|
"--listen=0.0.0.0",
|
||||||
"--port=19001",
|
"--port=19001",
|
||||||
"--cpu",
|
"--cpu",
|
||||||
"--distributed-queue-frontend",
|
"--distributed-queue-frontend",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ full distributed trace.
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|||||||
from opentelemetry.semconv.attributes import service_attributes
|
from opentelemetry.semconv.attributes import service_attributes
|
||||||
from testcontainers.core.container import DockerContainer
|
from testcontainers.core.container import DockerContainer
|
||||||
from testcontainers.core.waiting_utils import wait_for_logs
|
from testcontainers.core.waiting_utils import wait_for_logs
|
||||||
|
from testcontainers.nginx import NginxContainer
|
||||||
|
|
||||||
from comfy.client.sdxl_with_refiner_workflow import sdxl_workflow_with_refiner
|
from comfy.client.sdxl_with_refiner_workflow import sdxl_workflow_with_refiner
|
||||||
|
|
||||||
@ -54,6 +56,102 @@ class JaegerContainer(DockerContainer):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def nginx_proxy(frontend_backend_worker_with_rabbitmq):
|
||||||
|
"""
|
||||||
|
Provide an nginx proxy in front of the ComfyUI frontend.
|
||||||
|
This tests if nginx is blocking W3C trace context propagation.
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Extract host and port from frontend address
|
||||||
|
frontend_url = frontend_backend_worker_with_rabbitmq
|
||||||
|
# frontend_url is like "http://127.0.0.1:19001"
|
||||||
|
import re
|
||||||
|
match = re.match(r'http://([^:]+):(\d+)', frontend_url)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Could not parse frontend URL: {frontend_url}")
|
||||||
|
|
||||||
|
frontend_host = match.group(1)
|
||||||
|
frontend_port = match.group(2)
|
||||||
|
nginx_port = 8085
|
||||||
|
|
||||||
|
# Get the Docker bridge gateway IP (this is how containers reach the host on Linux)
|
||||||
|
# Try to get the default Docker bridge gateway
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "network", "inspect", "bridge", "-f", "{{range .IPAM.Config}}{{.Gateway}}{{end}}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
docker_gateway = result.stdout.strip()
|
||||||
|
logger.info(f"Using Docker gateway IP: {docker_gateway}")
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback: try common gateway IPs
|
||||||
|
docker_gateway = "172.17.0.1" # Default Docker bridge gateway on Linux
|
||||||
|
logger.warning(f"Could not detect Docker gateway, using default: {docker_gateway}")
|
||||||
|
|
||||||
|
# Create nginx config that proxies to the frontend and passes trace headers
|
||||||
|
nginx_conf = f"""
|
||||||
|
events {{
|
||||||
|
worker_connections 1024;
|
||||||
|
}}
|
||||||
|
|
||||||
|
http {{
|
||||||
|
upstream backend {{
|
||||||
|
server {docker_gateway}:{frontend_port};
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen {nginx_port};
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Write config to a temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f:
|
||||||
|
f.write(nginx_conf)
|
||||||
|
nginx_conf_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start nginx container with the config
|
||||||
|
nginx = NginxContainer(port=nginx_port)
|
||||||
|
nginx.with_volume_mapping(nginx_conf_path, "/etc/nginx/nginx.conf")
|
||||||
|
nginx.start()
|
||||||
|
|
||||||
|
# Get the nginx URL
|
||||||
|
host = nginx.get_container_host_ip()
|
||||||
|
port = nginx.get_exposed_port(nginx_port)
|
||||||
|
nginx_url = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
logger.info(f"Nginx proxy started at {nginx_url} -> {frontend_url}")
|
||||||
|
|
||||||
|
# Wait for nginx to be ready
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
response = requests.get(nginx_url, timeout=1)
|
||||||
|
if response.status_code:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
yield nginx_url
|
||||||
|
finally:
|
||||||
|
nginx.stop()
|
||||||
|
os.unlink(nginx_conf_path)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def jaeger_container():
|
def jaeger_container():
|
||||||
"""
|
"""
|
||||||
@ -201,21 +299,21 @@ def verify_trace_continuity(trace: dict, expected_services: list[str]) -> bool:
|
|||||||
|
|
||||||
# order matters, execute jaeger_container first
|
# order matters, execute jaeger_container first
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_tracing_integration(jaeger_container, frontend_backend_worker_with_rabbitmq):
|
async def test_tracing_integration(jaeger_container, nginx_proxy):
|
||||||
"""
|
"""
|
||||||
Integration test for distributed tracing across services.
|
Integration test for distributed tracing across services with nginx proxy.
|
||||||
|
|
||||||
This test:
|
This test:
|
||||||
1. Starts ComfyUI frontend and worker with RabbitMQ
|
1. Starts ComfyUI frontend and worker with RabbitMQ
|
||||||
2. Configures OTLP export to Jaeger testcontainer
|
2. Starts nginx proxy in front of the frontend to test trace context propagation through nginx
|
||||||
3. Submits a workflow through the frontend
|
3. Configures OTLP export to Jaeger testcontainer
|
||||||
4. Queries Jaeger to verify trace propagation
|
4. Submits a workflow through the nginx proxy
|
||||||
5. Validates that the trace spans multiple services with proper relationships
|
5. Queries Jaeger to verify trace propagation
|
||||||
|
6. Validates that the trace spans multiple services with proper relationships
|
||||||
|
|
||||||
Note: The frontend_backend_worker_with_rabbitmq fixture is parameterized,
|
This specifically tests if nginx is blocking W3C trace context (traceparent/tracestate headers).
|
||||||
so this test will run with both ThreadPoolExecutor and ProcessPoolExecutor.
|
|
||||||
"""
|
"""
|
||||||
server_address = frontend_backend_worker_with_rabbitmq
|
server_address = nginx_proxy
|
||||||
jaeger_url = jaeger_container.get_query_url()
|
jaeger_url = jaeger_container.get_query_url()
|
||||||
otlp_endpoint = jaeger_container.get_otlp_endpoint()
|
otlp_endpoint = jaeger_container.get_otlp_endpoint()
|
||||||
|
|
||||||
@ -410,31 +508,27 @@ async def test_multiple_requests_different_traces(frontend_backend_worker_with_r
|
|||||||
# Query Jaeger and verify we have multiple distinct traces
|
# Query Jaeger and verify we have multiple distinct traces
|
||||||
jaeger_url = jaeger_container.get_query_url()
|
jaeger_url = jaeger_container.get_query_url()
|
||||||
|
|
||||||
try:
|
traces_response = query_jaeger_traces(jaeger_url, "comfyui", lookback="5m", limit=10)
|
||||||
traces_response = query_jaeger_traces(jaeger_url, "comfyui", lookback="5m", limit=10)
|
traces = traces_response.get("data", [])
|
||||||
traces = traces_response.get("data", [])
|
|
||||||
|
|
||||||
if len(traces) >= 2:
|
assert len(traces) >= 2
|
||||||
# Get trace IDs
|
# Get trace IDs
|
||||||
trace_ids = [trace.get("traceID") for trace in traces]
|
trace_ids = [trace.get("traceID") for trace in traces]
|
||||||
unique_trace_ids = set(trace_ids)
|
unique_trace_ids = set(trace_ids)
|
||||||
|
|
||||||
logger.info(f"Found {len(unique_trace_ids)} unique traces")
|
logger.info(f"Found {len(unique_trace_ids)} unique traces")
|
||||||
|
|
||||||
# Verify we have multiple distinct traces
|
# Verify we have multiple distinct traces
|
||||||
assert len(unique_trace_ids) >= 2, (
|
assert len(unique_trace_ids) >= 2, (
|
||||||
f"Expected at least 2 distinct traces, found {len(unique_trace_ids)}. "
|
f"Expected at least 2 distinct traces, found {len(unique_trace_ids)}. "
|
||||||
"Each request should create its own trace."
|
"Each request should create its own trace."
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("✓ Multiple requests created distinct traces")
|
logger.info("✓ Multiple requests created distinct traces")
|
||||||
else:
|
|
||||||
pytest.skip("Not enough traces to validate")
|
|
||||||
except Exception as e:
|
|
||||||
pytest.skip(f"Could not query Jaeger: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.skip(reason="rabbitmq has to be configured for observability?")
|
||||||
async def test_trace_contains_rabbitmq_operations(frontend_backend_worker_with_rabbitmq, jaeger_container):
|
async def test_trace_contains_rabbitmq_operations(frontend_backend_worker_with_rabbitmq, jaeger_container):
|
||||||
"""
|
"""
|
||||||
Test that traces include RabbitMQ publish/consume operations.
|
Test that traces include RabbitMQ publish/consume operations.
|
||||||
@ -455,43 +549,21 @@ async def test_trace_contains_rabbitmq_operations(frontend_backend_worker_with_r
|
|||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
try:
|
traces_response = query_jaeger_traces(jaeger_url, "comfyui", lookback="5m")
|
||||||
traces_response = query_jaeger_traces(jaeger_url, "comfyui", lookback="5m")
|
traces = traces_response.get("data", [])
|
||||||
traces = traces_response.get("data", [])
|
|
||||||
|
|
||||||
if traces:
|
# Look for RabbitMQ-related operations in any trace
|
||||||
# Look for RabbitMQ-related operations in any trace
|
rabbitmq_operations = [
|
||||||
rabbitmq_operations = [
|
"publish", "consume", "amq_queue_publish", "amq_queue_consume",
|
||||||
"publish", "consume", "amq_queue_publish", "amq_queue_consume",
|
"amq.basic.publish", "amq.basic.consume", "send", "receive"
|
||||||
"amq.basic.publish", "amq.basic.consume", "send", "receive"
|
]
|
||||||
]
|
|
||||||
|
|
||||||
found_rabbitmq_ops = []
|
found_rabbitmq_ops = []
|
||||||
for trace in traces:
|
for trace in traces:
|
||||||
for span in trace.get("spans", []):
|
for span in trace.get("spans", []):
|
||||||
op_name = span.get("operationName", "").lower()
|
op_name = span.get("operationName", "").lower()
|
||||||
for rmq_op in rabbitmq_operations:
|
for rmq_op in rabbitmq_operations:
|
||||||
if rmq_op in op_name:
|
if rmq_op in op_name:
|
||||||
found_rabbitmq_ops.append(op_name)
|
found_rabbitmq_ops.append(op_name)
|
||||||
|
|
||||||
if found_rabbitmq_ops:
|
assert found_rabbitmq_ops, "No RabbitMQ-related operations found in traces"
|
||||||
logger.info(f"✓ Found RabbitMQ operations in traces: {set(found_rabbitmq_ops)}")
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"No RabbitMQ operations found in traces. "
|
|
||||||
"This suggests that either:\n"
|
|
||||||
"1. AioPikaInstrumentor is not creating spans, or\n"
|
|
||||||
"2. The spans are being filtered out by the collector, or\n"
|
|
||||||
"3. The spans exist but use different operation names"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Log all operation names to help debug
|
|
||||||
all_ops = set()
|
|
||||||
for trace in traces[:3]: # First 3 traces
|
|
||||||
for span in trace.get("spans", []):
|
|
||||||
all_ops.add(span.get("operationName"))
|
|
||||||
logger.info(f"Sample operation names: {all_ops}")
|
|
||||||
else:
|
|
||||||
pytest.skip("No traces found")
|
|
||||||
except Exception as e:
|
|
||||||
pytest.skip(f"Could not query Jaeger: {e}")
|
|
||||||
Loading…
Reference in New Issue
Block a user