Merge pull request #2 from xbol0/ext_i18n

[feature] i18n extension support
This commit is contained in:
xbol0 2023-08-08 15:58:42 +08:00 committed by GitHub
commit f4190fc9af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 3636 additions and 124 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ venv/
web/extensions/*
!web/extensions/logging.js.example
!web/extensions/core/
!web/extensions/i18n/

View File

@ -535,7 +535,7 @@ def should_use_fp16(device=None, model_params=0):
return False
#FP16 is just broken on these cards
nvidia_16_series = ["1660", "1650", "1630", "T500", "T550", "T600", "MX550", "MX450"]
nvidia_16_series = ["1660", "1650", "1630", "T500", "T550", "T600", "MX550", "MX450", "CMP 30HX"]
for x in nvidia_16_series:
if x in props.name:
return False

View File

@ -70,13 +70,22 @@ def load_lora(lora, to_load):
alpha = lora[alpha_name].item()
loaded_keys.add(alpha_name)
A_name = "{}.lora_up.weight".format(x)
B_name = "{}.lora_down.weight".format(x)
mid_name = "{}.lora_mid.weight".format(x)
regular_lora = "{}.lora_up.weight".format(x)
diffusers_lora = "{}_lora.up.weight".format(x)
A_name = None
if A_name in lora.keys():
if regular_lora in lora.keys():
A_name = regular_lora
B_name = "{}.lora_down.weight".format(x)
mid_name = "{}.lora_mid.weight".format(x)
elif diffusers_lora in lora.keys():
A_name = diffusers_lora
B_name = "{}_lora.down.weight".format(x)
mid_name = None
if A_name is not None:
mid = None
if mid_name in lora.keys():
if mid_name is not None and mid_name in lora.keys():
mid = lora[mid_name]
loaded_keys.add(mid_name)
patch_dict[to_load[x]] = (lora[A_name], lora[B_name], alpha, mid)
@ -202,6 +211,11 @@ def model_lora_keys_unet(model, key_map={}):
if k.endswith(".weight"):
key_lora = k[:-len(".weight")].replace(".", "_")
key_map["lora_unet_{}".format(key_lora)] = "diffusion_model.{}".format(diffusers_keys[k])
diffusers_lora_key = "unet.{}".format(k[:-len(".weight")].replace(".to_", ".processor.to_"))
if diffusers_lora_key.endswith(".to_out.0"):
diffusers_lora_key = diffusers_lora_key[:-2]
key_map[diffusers_lora_key] = "diffusion_model.{}".format(diffusers_keys[k])
return key_map
def set_attr(obj, attr, value):
@ -864,7 +878,7 @@ def load_controlnet(ckpt_path, model=None):
use_fp16 = model_management.should_use_fp16()
controlnet_config = model_detection.model_config_from_unet(controlnet_data, prefix, use_fp16).unet_config
controlnet_config.pop("out_channels")
controlnet_config["hint_channels"] = 3
controlnet_config["hint_channels"] = controlnet_data["{}input_hint_block.0.weight".format(prefix)].shape[1]
control_model = cldm.ControlNet(**controlnet_config)
if pth:

View File

@ -40,7 +40,8 @@ def cuda_malloc_supported():
blacklist = {"GeForce GTX TITAN X", "GeForce GTX 980", "GeForce GTX 970", "GeForce GTX 960", "GeForce GTX 950", "GeForce 945M",
"GeForce 940M", "GeForce 930M", "GeForce 920M", "GeForce 910M", "GeForce GTX 750", "GeForce GTX 745", "Quadro K620",
"Quadro K1200", "Quadro K2200", "Quadro M500", "Quadro M520", "Quadro M600", "Quadro M620", "Quadro M1000",
"Quadro M1200", "Quadro M2000", "Quadro M2200", "Quadro M3000", "Quadro M4000", "Quadro M5000", "Quadro M5500", "Quadro M6000"}
"Quadro M1200", "Quadro M2000", "Quadro M2200", "Quadro M3000", "Quadro M4000", "Quadro M5000", "Quadro M5500", "Quadro M6000",
"GeForce MX110", "GeForce MX130", "GeForce 830M", "GeForce 840M", "GeForce GTX 850M", "GeForce GTX 860M"}
try:
names = get_gpu_names()

View File

@ -159,13 +159,64 @@
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with cloudflared (Recommended Way)\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjjj"
},
"outputs": [],
"source": [
"!wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
"!dpkg -i cloudflared-linux-amd64.deb\n",
"\n",
"import subprocess\n",
"import threading\n",
"import time\n",
"import socket\n",
"import urllib.request\n",
"\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
"\n",
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
" for line in p.stderr:\n",
" l = line.decode()\n",
" if \"trycloudflare.com \" in l:\n",
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
" #print(l, end='')\n",
"\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with localtunnel (Recommended Way)\n",
"### Run ComfyUI with localtunnel\n",
"\n",
"\n"
]

View File

@ -345,6 +345,11 @@ class PromptServer():
vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
system_stats = {
"system": {
"os": os.name,
"python_version": sys.version,
"embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded"
},
"devices": [
{
"name": device_name,

View File

@ -435,7 +435,7 @@ app.registerExtension({
$el("td", [
$el("label", {
for: id.replaceAll(".", "-"),
textContent: "Color palette",
textContent: i18next.t("settings.Comfy.ColorPalette"),
}),
]),
$el("td", [
@ -449,7 +449,7 @@ app.registerExtension({
}, [
$el("input", {
type: "button",
value: "Export",
value: i18next.t("settings.Comfy.ColorPalette.export"),
onclick: async () => {
const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId));
@ -471,14 +471,14 @@ app.registerExtension({
}),
$el("input", {
type: "button",
value: "Import",
value: i18next.t("settings.Comfy.ColorPalette.import"),
onclick: () => {
fileInput.click();
}
}),
$el("input", {
type: "button",
value: "Template",
value: i18next.t("settings.Comfy.ColorPalette.template"),
onclick: async () => {
const colorPalette = await getColorPaletteTemplate();
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
@ -499,7 +499,7 @@ app.registerExtension({
}),
$el("input", {
type: "button",
value: "Delete",
value: i18next.t("settings.Comfy.ColorPalette.delete"),
onclick: async () => {
let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);

View File

@ -27,10 +27,13 @@ const ext = {
const clickedComboValue = currentNode.widgets
.filter(w => w.type === "combo" && w.options.values.length === values.length)
.find(w => w.options.values.every((v, i) => v === values[i]))
.value;
?.value;
let selectedIndex = values.findIndex(v => v === clickedComboValue);
let selectedItem = displayedItems?.[selectedIndex];
let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0;
if (selectedIndex < 0) {
selectedIndex = 0;
}
let selectedItem = displayedItems[selectedIndex];
updateSelected();
// Apply highlighting to the selected item

View File

@ -0,0 +1,46 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "i18next",
addCustomNodeDefs(defs) {
for (const k in defs) {
defs[k].display_name = i18next.t(`node.title.${k}`)
if ("input" in defs[k] && defs[k].input.required) {
for (const i in defs[k].input.required) {
if (defs[k].input.required[i].length > 1) {
defs[k].input.required[i][1].label = i18next.t(`node.input.${k}.${i}`)
} else {
defs[k].input.required[i].push({ label: i18next.t(`node.input.${k}.${i}`) })
}
}
}
}
},
nodeCreated(node) {
if ("inputs" in node) {
for (const item of node.inputs) {
item.label = i18next.t(`node.input.${node.comfyClass}.${item.name}`)
}
}
if ("widgets" in node) {
for (const item of node.widgets) {
item.label = i18next.t(`node.input.${node.comfyClass}.${item.name}`)
}
}
if ("outputs" in node) {
for (const item of node.outputs) {
item.label = i18next.t(`node.output.${node.comfyClass}.${item.name}`)
}
}
},
afterNodesRegistrations() {
const defs = LiteGraph.registered_node_types
for (const k in defs) {
defs[k].category = i18next.t(`category.${defs[k].category}`)
}
}
})

View File

@ -0,0 +1,25 @@
import { app } from "/scripts/app.js";
const id = "Comfy.LinkRenderMode";
const ext = {
name: id,
async setup(app) {
app.ui.settings.addSetting({
id,
name: "Link Render Mode",
defaultValue: 2,
type: "combo",
options: LiteGraph.LINK_RENDER_MODES.map((m, i) => ({
value: i,
text: m,
selected: i == app.canvas.links_render_mode,
})),
onChange(value) {
app.canvas.links_render_mode = +value;
app.graph.setDirtyCanvas(true);
},
});
},
};
app.registerExtension(ext);

View File

@ -57,6 +57,7 @@ function convertToInput(node, widget, config) {
const sz = node.size;
node.addInput(widget.name, linkType, {
widget: { name: widget.name, config },
label: config?.[1] ? i18next.t(config?.[1].label) : void 0,
});
for (const widget of node.widgets) {

125
web/i18n/en_US.js Normal file
View File

@ -0,0 +1,125 @@
export default {
translation: {
"ui.queue_btn": "Queue Prompt",
"ui.queue_front_btn": "Queue Front",
"ui.view_queue_btn": "View Queue",
"ui.view_history_btn": "View History",
"ui.save_btn": "Save",
"ui.load_btn": "Load",
"ui.refresh_btn": "Refresh",
"ui.clipspace_btn": "Clipspace",
"ui.clear_btn": "Clear",
"ui.load_default_btn": "Load Default",
"ui.close_btn": "Close",
"ui.queue_size": "Queue size: ",
"ui.extra_options": "Extra options",
"ui.settings.title": "Settings",
"ui.canvas_menu_add_node": "Add Node",
"ui.canvas_menu_add_group": "Add Group",
"ui.node_panel.header.properties": "Properties",
"ui.node_panel.header.title": "Title",
"ui.node_panel.header.mode": "Mode",
"ui.node_panel.header.color": "Color",
"node.title.KSampler": "KSampler",
"node.title.KSamplerAdvanced": "KSampler (Advanced)",
// Loaders
"node.title.CheckpointLoader": "Load Checkpoint (With Config)",
"node.title.CheckpointLoaderSimple": "Load Checkpoint",
"node.title.VAELoader": "Load VAE",
"node.title.LoraLoader": "Load LoRA",
"node.title.CLIPLoader": "Load CLIP",
"node.title.ControlNetLoader": "Load ControlNet Model",
"node.title.DiffControlNetLoader": "Load ControlNet Model (diff)",
"node.title.StyleModelLoader": "Load Style Model",
"node.title.CLIPVisionLoader": "Load CLIP Vision",
"node.title.UpscaleModelLoader": "Load Upscale Model",
// Conditioning
"node.title.CLIPVisionEncode": "CLIP Vision Encode",
"node.title.StyleModelApply": "Apply Style Model",
"node.title.CLIPTextEncode": "CLIP Text Encode (Prompt)",
"node.title.CLIPSetLastLayer": "CLIP Set Last Layer",
"node.title.ConditioningCombine": "Conditioning (Combine)",
"node.title.ConditioningAverage ": "Conditioning (Average)",
"node.title.ConditioningConcat": "Conditioning (Concat)",
"node.title.ConditioningSetArea": "Conditioning (Set Area)",
"node.title.ConditioningSetMask": "Conditioning (Set Mask)",
"node.title.ControlNetApply": "Apply ControlNet",
"node.title.ControlNetApplyAdvanced": "Apply ControlNet (Advanced)",
// Latent
"node.title.VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
"node.title.SetLatentNoiseMask": "Set Latent Noise Mask",
"node.title.VAEDecode": "VAE Decode",
"node.title.VAEEncode": "VAE Encode",
"node.title.LatentRotate": "Rotate Latent",
"node.title.LatentFlip": "Flip Latent",
"node.title.LatentCrop": "Crop Latent",
"node.title.EmptyLatentImage": "Empty Latent Image",
"node.title.LatentUpscale": "Upscale Latent",
"node.title.LatentUpscaleBy": "Upscale Latent By",
"node.title.LatentComposite": "Latent Composite",
"node.title.LatentBlend": "Latent Blend",
"LatentFromBatch": "Latent From Batch",
"node.title.RepeatLatentBatch": "Repeat Latent Batch",
// Image
"node.title.SaveImage": "Save Image",
"node.title.PreviewImage": "Preview Image",
"node.title.LoadImage": "Load Image",
"node.title.LoadImageMask": "Load Image (as Mask)",
"node.title.ImageScale": "Upscale Image",
"node.title.ImageScaleBy": "Upscale Image By",
"node.title.ImageUpscaleWithModel": "Upscale Image (using Model)",
"node.title.ImageInvert": "Invert Image",
"node.title.ImagePadForOutpaint": "Pad Image for Outpainting",
// _for_testing
"node.title.VAEDecodeTiled": "VAE Decode (Tiled)",
"node.title.VAEEncodeTiled": "VAE Encode (Tiled)",
"node.input.SaveImage.filename_prefix": "filename_prefix",
"node.input.SaveImage.images": "images",
"node.output.CheckpointLoaderSimple.MODEL": "MODEL",
"category.conditioning": "conditioning",
"category.loaders": "loaders",
"category.latent": "latent",
"category.latent/inpaint": "latent/inpaint",
"category.latent/batch": "latent/batch",
"category.image": "image",
"category.mask": "mask",
"category.image/upscaling": "image/upscaling",
"category.sampling": "sampling",
"category._for_testing": "_for_testing",
"category.latent/transform": "latent/transform",
"category.advanced/loaders": "advanced/loaders",
"category.conditioning/style_model": "conditioning/style_model",
"category.conditioning/gligen": "conditioning/gligen",
"category.advanced/loaders/deprecated": "advanced/loaders/deprecated",
"category.advanced/conditioning": "advanced/conditioning",
"category.image/postprocessing": "image/postprocessing",
"category.advanced/model_merging": "advanced/model_merging",
"category.image/preprocessors": "image/preprocessors",
"category.utils": "utils",
"settings.Comfy.ConfirmClear": "Require confirmation when clearing workflow",
"settings.Comfy.PromptFilename": "Prompt for filename when saving workflow",
"settings.Comfy.PreviewFormat": "When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.",
"settings.Comfy.DisableSliders": "Disable sliders.",
"settings.Comfy.DevMode": "Enable Dev mode Options",
"settings.Comfy.ColorPalette": "Color Palette",
"settings.Comfy.EditAttention.Delta": "Ctrl+up/down precision",
"settings.Comfy.InvertMenuScrolling": "Invert Menu Scrolling",
"settings.Comfy.LinkRenderMode": "Link Render Mode",
"settings.Comfy.NodeSuggestions.number": "Number of nodes suggestions",
"settings.Comfy.SnapToGrid.GridSize": "Grid Size",
"settings.Comfy.Logging.Enabled": "Comfy.Logging.Enabled",
"settings.Comfy.MenuPosition": "Save menu position",
"settings.Comfy.ColorPalette.export": "Export",
"settings.Comfy.ColorPalette.import": "Import",
"settings.Comfy.ColorPalette.template": "Template",
"settings.Comfy.ColorPalette.delete": "Delete",
}
}

125
web/i18n/zh_CN.js Normal file
View File

@ -0,0 +1,125 @@
export default {
translation: {
"ui.queue_btn": "冲冲冲",
"ui.queue_front_btn": "插队冲冲冲",
"ui.view_queue_btn": "查看队列",
"ui.view_history_btn": "查看历史",
"ui.save_btn": "保存",
"ui.load_btn": "加载",
"ui.refresh_btn": "刷新",
"ui.clipspace_btn": "Clipspace",
"ui.clear_btn": "清空",
"ui.load_default_btn": "加载默认配置",
"ui.close_btn": "关闭",
"ui.queue_size": "队列数量: ",
"ui.extra_options": "额外选项",
"ui.settings.title": "设置",
"ui.canvas_menu_add_node": "添加节点",
"ui.canvas_menu_add_group": "添加组",
"ui.node_panel.header.properties": "属性",
"ui.node_panel.header.title": "标题",
"ui.node_panel.header.mode": "模式",
"ui.node_panel.header.color": "颜色",
"node.title.KSampler": "采样器",
"node.title.KSamplerAdvanced": "采样器 (高级)",
// Loaders
"node.title.CheckpointLoader": "Load Checkpoint (With Config)",
"node.title.CheckpointLoaderSimple": "加载模型",
"node.title.VAELoader": "Load VAE",
"node.title.LoraLoader": "Load LoRA",
"node.title.CLIPLoader": "Load CLIP",
"node.title.ControlNetLoader": "Load ControlNet Model",
"node.title.DiffControlNetLoader": "Load ControlNet Model (diff)",
"node.title.StyleModelLoader": "Load Style Model",
"node.title.CLIPVisionLoader": "Load CLIP Vision",
"node.title.UpscaleModelLoader": "Load Upscale Model",
// Conditioning
"node.title.CLIPVisionEncode": "CLIP Vision Encode",
"node.title.StyleModelApply": "Apply Style Model",
"node.title.CLIPTextEncode": "CLIP Text Encode (Prompt)",
"node.title.CLIPSetLastLayer": "CLIP Set Last Layer",
"node.title.ConditioningCombine": "Conditioning (Combine)",
"node.title.ConditioningAverage ": "Conditioning (Average)",
"node.title.ConditioningConcat": "Conditioning (Concat)",
"node.title.ConditioningSetArea": "Conditioning (Set Area)",
"node.title.ConditioningSetMask": "Conditioning (Set Mask)",
"node.title.ControlNetApply": "Apply ControlNet",
"node.title.ControlNetApplyAdvanced": "Apply ControlNet (Advanced)",
// Latent
"node.title.VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
"node.title.SetLatentNoiseMask": "Set Latent Noise Mask",
"node.title.VAEDecode": "VAE Decode",
"node.title.VAEEncode": "VAE Encode",
"node.title.LatentRotate": "Rotate Latent",
"node.title.LatentFlip": "Flip Latent",
"node.title.LatentCrop": "Crop Latent",
"node.title.EmptyLatentImage": "Empty Latent Image",
"node.title.LatentUpscale": "Upscale Latent",
"node.title.LatentUpscaleBy": "Upscale Latent By",
"node.title.LatentComposite": "Latent Composite",
"node.title.LatentBlend": "Latent Blend",
"LatentFromBatch": "Latent From Batch",
"node.title.RepeatLatentBatch": "Repeat Latent Batch",
// Image
"node.title.SaveImage": "Save Image",
"node.title.PreviewImage": "Preview Image",
"node.title.LoadImage": "Load Image",
"node.title.LoadImageMask": "Load Image (as Mask)",
"node.title.ImageScale": "Upscale Image",
"node.title.ImageScaleBy": "Upscale Image By",
"node.title.ImageUpscaleWithModel": "Upscale Image (using Model)",
"node.title.ImageInvert": "Invert Image",
"node.title.ImagePadForOutpaint": "Pad Image for Outpainting",
// _for_testing
"node.title.VAEDecodeTiled": "VAE Decode (Tiled)",
"node.title.VAEEncodeTiled": "VAE Encode (Tiled)",
"node.input.SaveImage.filename_prefix": "文件名前缀",
"node.input.SaveImage.images": "图片",
"node.output.CheckpointLoaderSimple.MODEL": "模型",
"category.conditioning": "可调参数",
"category.loaders": "加载器",
"category.latent": "潜在",
"category.latent/inpaint": "潜在/修复",
"category.latent/batch": "潜在/批量",
"category.image": "图像",
"category.mask": "遮罩",
"category.image/upscaling": "图像/外扩",
"category.sampling": "采样",
"category._for_testing": "测试",
"category.latent/transform": "潜在/转换",
"category.advanced/loaders": "高级/加载器",
"category.conditioning/style_model": "可调参数/风格模型",
"category.conditioning/gligen": "可调参数/gligen",
"category.advanced/loaders/deprecated": "高级/加载器/已弃用",
"category.advanced/conditioning": "高级/可调参数",
"category.image/postprocessing": "图像/后期处理",
"category.advanced/model_merging": "高级/模型合并",
"category.image/preprocessors": "图像/前期处理",
"category.utils": "工具",
"settings.Comfy.ConfirmClear": "清空工作流需要确认",
"settings.Comfy.PromptFilename": "Prompt for filename when saving workflow",
"settings.Comfy.PreviewFormat": "预览图格式和压缩尺寸, e.g. webp, jpeg, webp;50, etc.",
"settings.Comfy.DisableSliders": "Disable sliders.",
"settings.Comfy.DevMode": "启用开发模式",
"settings.Comfy.ColorPalette": "主题",
"settings.Comfy.EditAttention.Delta": "Ctrl+up/down precision",
"settings.Comfy.InvertMenuScrolling": "反转滚动",
"settings.Comfy.LinkRenderMode": "链接渲染模式",
"settings.Comfy.NodeSuggestions.number": "Number of nodes suggestions",
"settings.Comfy.SnapToGrid.GridSize": "单元格尺寸",
"settings.Comfy.Logging.Enabled": "记录日志",
"settings.Comfy.MenuPosition": "保存菜单位置",
"settings.Comfy.ColorPalette.export": "导出",
"settings.Comfy.ColorPalette.import": "导入",
"settings.Comfy.ColorPalette.template": "模版",
"settings.Comfy.ColorPalette.delete": "删除",
}
}

View File

@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="./lib/litegraph.css" />
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="text/javascript" src="./lib/i18next.js"></script>
<script type="text/javascript" src="./lib/i18nextBrowserLanguageDetector.js"></script>
<script type="text/javascript" src="./lib/litegraph.core.js"></script>
<script type="text/javascript" src="./lib/litegraph.extensions.js" defer></script>
<script type="module">

2265
web/lib/i18next.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,422 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextBrowserLanguageDetector = factory());
})(this, (function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _typeof(obj) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
}, _typeof(obj);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
var arr = [];
var each = arr.forEach;
var slice = arr.slice;
function defaults(obj) {
each.call(slice.call(arguments, 1), function (source) {
if (source) {
for (var prop in source) {
if (obj[prop] === undefined) obj[prop] = source[prop];
}
}
});
return obj;
}
// eslint-disable-next-line no-control-regex
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
var serializeCookie = function serializeCookie(name, val, options) {
var opt = options || {};
opt.path = opt.path || '/';
var value = encodeURIComponent(val);
var str = "".concat(name, "=").concat(value);
if (opt.maxAge > 0) {
var maxAge = opt.maxAge - 0;
if (Number.isNaN(maxAge)) throw new Error('maxAge should be a Number');
str += "; Max-Age=".concat(Math.floor(maxAge));
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid');
}
str += "; Domain=".concat(opt.domain);
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError('option path is invalid');
}
str += "; Path=".concat(opt.path);
}
if (opt.expires) {
if (typeof opt.expires.toUTCString !== 'function') {
throw new TypeError('option expires is invalid');
}
str += "; Expires=".concat(opt.expires.toUTCString());
}
if (opt.httpOnly) str += '; HttpOnly';
if (opt.secure) str += '; Secure';
if (opt.sameSite) {
var sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite;
switch (sameSite) {
case true:
str += '; SameSite=Strict';
break;
case 'lax':
str += '; SameSite=Lax';
break;
case 'strict':
str += '; SameSite=Strict';
break;
case 'none':
str += '; SameSite=None';
break;
default:
throw new TypeError('option sameSite is invalid');
}
}
return str;
};
var cookie = {
create: function create(name, value, minutes, domain) {
var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
path: '/',
sameSite: 'strict'
};
if (minutes) {
cookieOptions.expires = new Date();
cookieOptions.expires.setTime(cookieOptions.expires.getTime() + minutes * 60 * 1000);
}
if (domain) cookieOptions.domain = domain;
document.cookie = serializeCookie(name, encodeURIComponent(value), cookieOptions);
},
read: function read(name) {
var nameEQ = "".concat(name, "=");
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
},
remove: function remove(name) {
this.create(name, '', -1);
}
};
var cookie$1 = {
name: 'cookie',
lookup: function lookup(options) {
var found;
if (options.lookupCookie && typeof document !== 'undefined') {
var c = cookie.read(options.lookupCookie);
if (c) found = c;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupCookie && typeof document !== 'undefined') {
cookie.create(options.lookupCookie, lng, options.cookieMinutes, options.cookieDomain, options.cookieOptions);
}
}
};
var querystring = {
name: 'querystring',
lookup: function lookup(options) {
var found;
if (typeof window !== 'undefined') {
var search = window.location.search;
if (!window.location.search && window.location.hash && window.location.hash.indexOf('?') > -1) {
search = window.location.hash.substring(window.location.hash.indexOf('?'));
}
var query = search.substring(1);
var params = query.split('&');
for (var i = 0; i < params.length; i++) {
var pos = params[i].indexOf('=');
if (pos > 0) {
var key = params[i].substring(0, pos);
if (key === options.lookupQuerystring) {
found = params[i].substring(pos + 1);
}
}
}
}
return found;
}
};
var hasLocalStorageSupport = null;
var localStorageAvailable = function localStorageAvailable() {
if (hasLocalStorageSupport !== null) return hasLocalStorageSupport;
try {
hasLocalStorageSupport = window !== 'undefined' && window.localStorage !== null;
var testKey = 'i18next.translate.boo';
window.localStorage.setItem(testKey, 'foo');
window.localStorage.removeItem(testKey);
} catch (e) {
hasLocalStorageSupport = false;
}
return hasLocalStorageSupport;
};
var localStorage = {
name: 'localStorage',
lookup: function lookup(options) {
var found;
if (options.lookupLocalStorage && localStorageAvailable()) {
var lng = window.localStorage.getItem(options.lookupLocalStorage);
if (lng) found = lng;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupLocalStorage && localStorageAvailable()) {
window.localStorage.setItem(options.lookupLocalStorage, lng);
}
}
};
var hasSessionStorageSupport = null;
var sessionStorageAvailable = function sessionStorageAvailable() {
if (hasSessionStorageSupport !== null) return hasSessionStorageSupport;
try {
hasSessionStorageSupport = window !== 'undefined' && window.sessionStorage !== null;
var testKey = 'i18next.translate.boo';
window.sessionStorage.setItem(testKey, 'foo');
window.sessionStorage.removeItem(testKey);
} catch (e) {
hasSessionStorageSupport = false;
}
return hasSessionStorageSupport;
};
var sessionStorage = {
name: 'sessionStorage',
lookup: function lookup(options) {
var found;
if (options.lookupSessionStorage && sessionStorageAvailable()) {
var lng = window.sessionStorage.getItem(options.lookupSessionStorage);
if (lng) found = lng;
}
return found;
},
cacheUserLanguage: function cacheUserLanguage(lng, options) {
if (options.lookupSessionStorage && sessionStorageAvailable()) {
window.sessionStorage.setItem(options.lookupSessionStorage, lng);
}
}
};
var navigator$1 = {
name: 'navigator',
lookup: function lookup(options) {
var found = [];
if (typeof navigator !== 'undefined') {
if (navigator.languages) {
// chrome only; not an array, so can't use .push.apply instead of iterating
for (var i = 0; i < navigator.languages.length; i++) {
found.push(navigator.languages[i]);
}
}
if (navigator.userLanguage) {
found.push(navigator.userLanguage);
}
if (navigator.language) {
found.push(navigator.language);
}
}
return found.length > 0 ? found : undefined;
}
};
var htmlTag = {
name: 'htmlTag',
lookup: function lookup(options) {
var found;
var htmlTag = options.htmlTag || (typeof document !== 'undefined' ? document.documentElement : null);
if (htmlTag && typeof htmlTag.getAttribute === 'function') {
found = htmlTag.getAttribute('lang');
}
return found;
}
};
var path = {
name: 'path',
lookup: function lookup(options) {
var found;
if (typeof window !== 'undefined') {
var language = window.location.pathname.match(/\/([a-zA-Z-]*)/g);
if (language instanceof Array) {
if (typeof options.lookupFromPathIndex === 'number') {
if (typeof language[options.lookupFromPathIndex] !== 'string') {
return undefined;
}
found = language[options.lookupFromPathIndex].replace('/', '');
} else {
found = language[0].replace('/', '');
}
}
}
return found;
}
};
var subdomain = {
name: 'subdomain',
lookup: function lookup(options) {
// If given get the subdomain index else 1
var lookupFromSubdomainIndex = typeof options.lookupFromSubdomainIndex === 'number' ? options.lookupFromSubdomainIndex + 1 : 1;
// get all matches if window.location. is existing
// first item of match is the match itself and the second is the first group macht which sould be the first subdomain match
// is the hostname no public domain get the or option of localhost
var language = typeof window !== 'undefined' && window.location && window.location.hostname && window.location.hostname.match(/^(\w{2,5})\.(([a-z0-9-]{1,63}\.[a-z]{2,6})|localhost)/i);
// if there is no match (null) return undefined
if (!language) return undefined;
// return the given group match
return language[lookupFromSubdomainIndex];
}
};
function getDefaults() {
return {
order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
lookupSessionStorage: 'i18nextLng',
// cache user language
caches: ['localStorage'],
excludeCacheFor: ['cimode'],
// cookieMinutes: 10,
// cookieDomain: 'myDomain'
convertDetectedLanguage: function convertDetectedLanguage(l) {
return l;
}
};
}
var Browser = /*#__PURE__*/function () {
function Browser(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Browser);
this.type = 'languageDetector';
this.detectors = {};
this.init(services, options);
}
_createClass(Browser, [{
key: "init",
value: function init(services) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
this.services = services || {
languageUtils: {}
}; // this way the language detector can be used without i18next
this.options = defaults(options, this.options || {}, getDefaults());
if (typeof this.options.convertDetectedLanguage === 'string' && this.options.convertDetectedLanguage.indexOf('15897') > -1) {
this.options.convertDetectedLanguage = function (l) {
return l.replace('-', '_');
};
}
// backwards compatibility
if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex;
this.i18nOptions = i18nOptions;
this.addDetector(cookie$1);
this.addDetector(querystring);
this.addDetector(localStorage);
this.addDetector(sessionStorage);
this.addDetector(navigator$1);
this.addDetector(htmlTag);
this.addDetector(path);
this.addDetector(subdomain);
}
}, {
key: "addDetector",
value: function addDetector(detector) {
this.detectors[detector.name] = detector;
}
}, {
key: "detect",
value: function detect(detectionOrder) {
var _this = this;
if (!detectionOrder) detectionOrder = this.options.order;
var detected = [];
detectionOrder.forEach(function (detectorName) {
if (_this.detectors[detectorName]) {
var lookup = _this.detectors[detectorName].lookup(_this.options);
if (lookup && typeof lookup === 'string') lookup = [lookup];
if (lookup) detected = detected.concat(lookup);
}
});
detected = detected.map(function (d) {
return _this.options.convertDetectedLanguage(d);
});
if (this.services.languageUtils.getBestMatchFromCodes) return detected; // new i18next v19.5.0
return detected.length > 0 ? detected[0] : null; // a little backward compatibility
}
}, {
key: "cacheUserLanguage",
value: function cacheUserLanguage(lng, caches) {
var _this2 = this;
if (!caches) caches = this.options.caches;
if (!caches) return;
if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return;
caches.forEach(function (cacheName) {
if (_this2.detectors[cacheName]) _this2.detectors[cacheName].cacheUserLanguage(lng, _this2.options);
});
}
}]);
return Browser;
}();
Browser.type = 'languageDetector';
return Browser;
}));

View File

@ -9835,7 +9835,11 @@ LGraphNode.prototype.executeAction = function(action)
ctx.textAlign = "center";
ctx.fillStyle = text_color;
ctx.fillText(
w.label || w.name + " " + Number(w.value).toFixed(3),
w.label || w.name + " " + Number(w.value).toFixed(
w.options.precision != null
? w.options.precision
: 3
),
widget_width * 0.5,
y + H * 0.7
);
@ -12518,7 +12522,7 @@ LGraphNode.prototype.executeAction = function(action)
panel.content.innerHTML = ""; //clear
panel.addHTML("<span class='node_type'>"+node.type+"</span><span class='node_desc'>"+(node.constructor.desc || "")+"</span><span class='separator'></span>");
panel.addHTML("<h3>Properties</h3>");
panel.addHTML(`<h3>${i18next.t("ui.node_panel.header.properties")}</h3>`);
var fUpdate = function(name,value){
graphcanvas.graph.beforeChange(node);
@ -12550,16 +12554,16 @@ LGraphNode.prototype.executeAction = function(action)
graphcanvas.dirty_canvas = true;
};
panel.addWidget( "string", "Title", node.title, {}, fUpdate);
panel.addWidget( "string", i18next.t("ui.node_panel.header.title"), node.title, {}, fUpdate);
panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate);
panel.addWidget( "combo", i18next.t("ui.node_panel.header.mode"), LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate);
var nodeCol = "";
if (node.color !== undefined){
nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; });
}
panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate);
panel.addWidget( "combo", i18next.t("ui.node_panel.header.color"), nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate);
for(var pName in node.properties)
{
@ -13835,7 +13839,7 @@ LGraphNode.prototype.executeAction = function(action)
if (!disabled) {
element.addEventListener("click", inner_onclick);
}
if (options.autoopen) {
if (!disabled && options.autoopen) {
LiteGraph.pointerListenerAdd(element,"enter",inner_over);
}

View File

@ -264,6 +264,15 @@ class ComfyApi extends EventTarget {
}
}
/**
* Gets system & device stats
* @returns System stats such as python version, OS, per device info
*/
async getSystemStats() {
const res = await this.fetchApi("/system_stats");
return await res.json();
}
/**
* Sends a POST request to the API
* @param {*} type The endpoint to post to

View File

@ -1,9 +1,23 @@
import { ComfyLogging } from "./logging.js";
import { ComfyWidgets } from "./widgets.js";
import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import en from "../i18n/en_US.js"
import cn from "../i18n/zh_CN.js"
i18next.use(i18nextBrowserLanguageDetector).init({
fallbackLng: 'en',
resources: {
"en-US": en,
"en": en,
"zh_CN": cn,
"cn": cn,
},
})
/**
* @typedef {import("types/comfy").ComfyExtension} ComfyExtension
*/
@ -31,6 +45,7 @@ export class ComfyApp {
constructor() {
this.ui = new ComfyUI(this);
this.logging = new ComfyLogging(this);
/**
* List of extensions that are registered with the app
@ -59,7 +74,7 @@ export class ComfyApp {
getPreviewFormatParam() {
let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat");
if(preview_format)
if (preview_format)
return `&preview=${preview_format}`;
else
return "";
@ -70,7 +85,7 @@ export class ComfyApp {
}
static onClipspaceEditorSave() {
if(ComfyApp.clipspace_return_node) {
if (ComfyApp.clipspace_return_node) {
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node);
}
}
@ -81,13 +96,13 @@ export class ComfyApp {
static copyToClipspace(node) {
var widgets = null;
if(node.widgets) {
if (node.widgets) {
widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value }));
}
var imgs = undefined;
var orig_imgs = undefined;
if(node.imgs != undefined) {
if (node.imgs != undefined) {
imgs = [];
orig_imgs = [];
@ -99,7 +114,7 @@ export class ComfyApp {
}
var selectedIndex = 0;
if(node.imageIndex) {
if (node.imageIndex) {
selectedIndex = node.imageIndex;
}
@ -114,30 +129,30 @@ export class ComfyApp {
ComfyApp.clipspace_return_node = null;
if(ComfyApp.clipspace_invalidate_handler) {
if (ComfyApp.clipspace_invalidate_handler) {
ComfyApp.clipspace_invalidate_handler();
}
}
static pasteFromClipspace(node) {
if(ComfyApp.clipspace) {
if (ComfyApp.clipspace) {
// image paste
if(ComfyApp.clipspace.imgs && node.imgs) {
if(node.images && ComfyApp.clipspace.images) {
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
if (ComfyApp.clipspace.imgs && node.imgs) {
if (node.images && ComfyApp.clipspace.images) {
if (ComfyApp.clipspace['img_paste_mode'] == 'selected') {
node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]];
}
else {
node.images = ComfyApp.clipspace.images;
}
if(app.nodeOutputs[node.id + ""])
if (app.nodeOutputs[node.id + ""])
app.nodeOutputs[node.id + ""].images = node.images;
}
if(ComfyApp.clipspace.imgs) {
if (ComfyApp.clipspace.imgs) {
// deep-copy to cut link with clipspace
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
if (ComfyApp.clipspace['img_paste_mode'] == 'selected') {
const img = new Image();
img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
node.imgs = [img];
@ -145,7 +160,7 @@ export class ComfyApp {
}
else {
const imgs = [];
for(let i=0; i<ComfyApp.clipspace.imgs.length; i++) {
for (let i = 0; i < ComfyApp.clipspace.imgs.length; i++) {
imgs[i] = new Image();
imgs[i].src = ComfyApp.clipspace.imgs[i].src;
node.imgs = imgs;
@ -154,25 +169,25 @@ export class ComfyApp {
}
}
if(node.widgets) {
if(ComfyApp.clipspace.images) {
if (node.widgets) {
if (ComfyApp.clipspace.images) {
const clip_image = ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']];
const index = node.widgets.findIndex(obj => obj.name === 'image');
if(index >= 0) {
if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) {
node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:'');
if (index >= 0) {
if (node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) {
node.widgets[index].value = (clip_image.subfolder ? clip_image.subfolder + '/' : '') + clip_image.filename + (clip_image.type ? ` [${clip_image.type}]` : '');
}
else {
node.widgets[index].value = clip_image;
}
}
}
if(ComfyApp.clipspace.widgets) {
if (ComfyApp.clipspace.widgets) {
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name);
if (prop && prop.type != 'button') {
if(prop.type != 'image' && typeof prop.value == "string" && value.filename) {
prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:'');
if (prop.type != 'image' && typeof prop.value == "string" && value.filename) {
prop.value = (value.subfolder ? value.subfolder + '/' : '') + value.filename + (value.type ? ` [${value.type}]` : '');
}
else {
prop.value = value;
@ -283,28 +298,28 @@ export class ComfyApp {
}
// prevent conflict of clipspace content
if(!ComfyApp.clipspace_return_node) {
if (!ComfyApp.clipspace_return_node) {
options.push({
content: "Copy (Clipspace)",
callback: (obj) => { ComfyApp.copyToClipspace(this); }
});
content: "Copy (Clipspace)",
callback: (obj) => { ComfyApp.copyToClipspace(this); }
});
if(ComfyApp.clipspace != null) {
if (ComfyApp.clipspace != null) {
options.push({
content: "Paste (Clipspace)",
callback: () => { ComfyApp.pasteFromClipspace(this); }
});
content: "Paste (Clipspace)",
callback: () => { ComfyApp.pasteFromClipspace(this); }
});
}
if(ComfyApp.isImageNode(this)) {
if (ComfyApp.isImageNode(this)) {
options.push({
content: "Open in MaskEditor",
callback: (obj) => {
ComfyApp.copyToClipspace(this);
ComfyApp.clipspace_return_node = this;
ComfyApp.open_maskeditor();
}
});
content: "Open in MaskEditor",
callback: (obj) => {
ComfyApp.copyToClipspace(this);
ComfyApp.clipspace_return_node = this;
ComfyApp.open_maskeditor();
}
});
}
}
};
@ -314,7 +329,7 @@ export class ComfyApp {
const app = this;
const origNodeOnKeyDown = node.prototype.onKeyDown;
node.prototype.onKeyDown = function(e) {
node.prototype.onKeyDown = function (e) {
if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) {
return false;
}
@ -369,7 +384,7 @@ export class ComfyApp {
if (w.computeSize) {
shiftY += w.computeSize()[1] + 4;
}
else if(w.computedHeight) {
else if (w.computedHeight) {
shiftY += w.computedHeight;
}
else {
@ -614,7 +629,7 @@ export class ComfyApp {
}
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") {
await this.handleFile(event.dataTransfer.files[0]);
await this.handleFile(event.dataTransfer.files[0]);
} else {
// Try loading the first URI in the transfer list
const validTypes = ["text/uri-list", "text/x-moz-url"];
@ -674,7 +689,7 @@ export class ComfyApp {
data = data.slice(data.indexOf("workflow\n"));
data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data);
} catch (error) {}
} catch (error) { }
}
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
@ -692,7 +707,7 @@ export class ComfyApp {
const self = this;
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function(e) {
LGraphCanvas.prototype.processMouseDown = function (e) {
const res = origProcessMouseDown.apply(this, arguments);
this.selected_group_moving = false;
@ -712,7 +727,7 @@ export class ComfyApp {
}
const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
LGraphCanvas.prototype.processMouseMove = function(e) {
LGraphCanvas.prototype.processMouseMove = function (e) {
const orig_selected_group = this.selected_group;
if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
@ -737,7 +752,7 @@ export class ComfyApp {
#addProcessKeyHandler() {
const self = this;
const origProcessKey = LGraphCanvas.prototype.processKey;
LGraphCanvas.prototype.processKey = function(e) {
LGraphCanvas.prototype.processKey = function (e) {
const res = origProcessKey.apply(this, arguments);
if (res === false) {
@ -802,7 +817,7 @@ export class ComfyApp {
const self = this;
const origDrawGroups = LGraphCanvas.prototype.drawGroups;
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
if (!this.graph) {
return;
}
@ -889,7 +904,7 @@ export class ComfyApp {
12 + size[0] + 1,
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
[this.round_radius * 2, this.round_radius * 2, 2, 2]
);
);
else if (shape == LiteGraph.CIRCLE_SHAPE)
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
ctx.strokeStyle = color;
@ -1023,6 +1038,7 @@ export class ComfyApp {
*/
async #loadExtensions() {
const extensions = await api.getExtensions();
this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions });
for (const ext of extensions) {
try {
await import(api.apiURL(ext));
@ -1114,9 +1130,10 @@ export class ComfyApp {
const defs = await api.getNodeDefs();
await this.registerNodesFromDefs(defs);
await this.#invokeExtensionsAsync("registerCustomNodes");
await this.#invokeExtensionsAsync("afterNodesRegistrations")
}
async registerNodesFromDefs(defs) {
async registerNodesFromDefs(defs) {
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
// Generate list of known widgets
@ -1132,15 +1149,15 @@ export class ComfyApp {
const node = Object.assign(
function ComfyNode() {
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined){
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
if (nodeData["input"]["optional"] != undefined) {
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
const config = { minWidth: 1, minHeight: 1 };
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
if(inputData[1]?.forceInput) {
if (inputData[1]?.forceInput) {
this.addInput(inputName, type);
} else {
if (Array.isArray(type)) {
@ -1162,7 +1179,7 @@ export class ComfyApp {
for (const o in nodeData["output"]) {
const output = nodeData["output"][o];
const outputName = nodeData["output_name"][o] || output;
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ;
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE;
this.addOutput(outputName, output, { shape: outputShape });
}
@ -1306,6 +1323,9 @@ export class ComfyApp {
(t) => `<li>${t}</li>`
).join("")}</ul>Nodes that have failed to load will show as red on the graph.`
);
this.logging.addEntry("Comfy.App", "warn", {
MissingNodes: missingNodeTypes,
});
}
}
@ -1356,7 +1376,7 @@ export class ComfyApp {
if (parent.isVirtualNode) {
link = parent.getInputLink(link.origin_slot);
if (link) {
parent = parent.getInputNode(link.origin_slot);
parent = parent.getInputNode(link.target_slot);
if (parent) {
found = true;
}
@ -1424,9 +1444,9 @@ export class ComfyApp {
else if (error.response) {
let message = error.response.error.message;
if (error.response.error.details)
message += ": " + error.response.error.details;
message += ": " + error.response.error.details;
for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) {
message += "\n" + nodeError.class_type + ":"
message += "\n" + nodeError.class_type + ":"
for (const errorReason of nodeError.errors) {
message += "\n - " + errorReason.message + ": " + errorReason.details
}
@ -1552,22 +1572,22 @@ export class ComfyApp {
async refreshComboInNodes() {
const defs = await api.getNodeDefs();
for(let nodeNum in this.graph._nodes) {
for (let nodeNum in this.graph._nodes) {
const node = this.graph._nodes[nodeNum];
const def = defs[node.type];
// HOTFIX: The current patch is designed to prevent the rest of the code from breaking due to primitive nodes,
// and additional work is needed to consider the primitive logic in the refresh logic.
if(!def)
if (!def)
continue;
for(const widgetNum in node.widgets) {
for (const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
if (widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(widget.name != 'image' && !widget.options.values.includes(widget.value)) {
if (widget.name != 'image' && !widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
widget.callback(widget.value);
}

367
web/scripts/logging.js Normal file
View File

@ -0,0 +1,367 @@
import { $el, ComfyDialog } from "./ui.js";
import { api } from "./api.js";
$el("style", {
textContent: `
.comfy-logging-logs {
display: grid;
color: var(--fg-color);
white-space: pre-wrap;
}
.comfy-logging-log {
display: contents;
}
.comfy-logging-title {
background: var(--tr-even-bg-color);
font-weight: bold;
margin-bottom: 5px;
text-align: center;
}
.comfy-logging-log div {
background: var(--row-bg);
padding: 5px;
}
`,
parent: document.body,
});
// Stringify function supporting max depth and removal of circular references
// https://stackoverflow.com/a/57193345
function stringify(val, depth, replacer, space, onGetObjID) {
depth = isNaN(+depth) ? 1 : depth;
var recursMap = new WeakMap();
function _build(val, depth, o, a, r) {
// (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
return !val || typeof val != "object"
? val
: ((r = recursMap.has(val)),
recursMap.set(val, true),
(a = Array.isArray(val)),
r
? (o = (onGetObjID && onGetObjID(val)) || null)
: JSON.stringify(val, function (k, v) {
if (a || depth > 0) {
if (replacer) v = replacer(k, v);
if (!k) return (a = Array.isArray(v)), (val = v);
!o && (o = a ? [] : {});
o[k] = _build(v, a ? depth : depth - 1);
}
}),
o === void 0 ? (a ? [] : {}) : o);
}
return JSON.stringify(_build(val, depth), null, space);
}
const jsonReplacer = (k, v, ui) => {
if (v instanceof Array && v.length === 1) {
v = v[0];
}
if (v instanceof Date) {
v = v.toISOString();
if (ui) {
v = v.split("T")[1];
}
}
if (v instanceof Error) {
let err = "";
if (v.name) err += v.name + "\n";
if (v.message) err += v.message + "\n";
if (v.stack) err += v.stack + "\n";
if (!err) {
err = v.toString();
}
v = err;
}
return v;
};
const fileInput = $el("input", {
type: "file",
accept: ".json",
style: { display: "none" },
parent: document.body,
});
class ComfyLoggingDialog extends ComfyDialog {
constructor(logging) {
super();
this.logging = logging;
}
clear() {
this.logging.clear();
this.show();
}
export() {
const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: `comfyui-logs-${Date.now()}.json`,
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
import() {
fileInput.onchange = () => {
const reader = new FileReader();
reader.onload = () => {
fileInput.remove();
try {
const obj = JSON.parse(reader.result);
if (obj instanceof Array) {
this.show(obj);
} else {
throw new Error("Invalid file selected.");
}
} catch (error) {
alert("Unable to load logs: " + error.message);
}
};
reader.readAsText(fileInput.files[0]);
};
fileInput.click();
}
createButtons() {
return [
$el("button", {
type: "button",
textContent: "Clear",
onclick: () => this.clear(),
}),
$el("button", {
type: "button",
textContent: "Export logs...",
onclick: () => this.export(),
}),
$el("button", {
type: "button",
textContent: "View exported logs...",
onclick: () => this.import(),
}),
...super.createButtons(),
];
}
getTypeColor(type) {
switch (type) {
case "error":
return "red";
case "warn":
return "orange";
case "debug":
return "dodgerblue";
}
}
show(entries) {
if (!entries) entries = this.logging.entries;
this.element.style.width = "100%";
const cols = {
source: "Source",
type: "Type",
timestamp: "Timestamp",
message: "Message",
};
const keys = Object.keys(cols);
const headers = Object.values(cols).map((title) =>
$el("div.comfy-logging-title", {
textContent: title,
})
);
const rows = entries.map((entry, i) => {
return $el(
"div.comfy-logging-log",
{
$: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`),
},
keys.map((key) => {
let v = entry[key];
let color;
if (key === "type") {
color = this.getTypeColor(v);
} else {
v = jsonReplacer(key, v, true);
if (typeof v === "object") {
v = stringify(v, 5, jsonReplacer, " ");
}
}
return $el("div", {
style: {
color,
},
textContent: v,
});
})
);
});
const grid = $el(
"div.comfy-logging-logs",
{
style: {
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
},
},
[...headers, ...rows]
);
const els = [grid];
if (!this.logging.enabled) {
els.unshift(
$el("h3", {
style: { textAlign: "center" },
textContent: "Logging is disabled",
})
);
}
super.show($el("div", els));
}
}
export class ComfyLogging {
/**
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
*/
entries = [];
#enabled;
#console = {};
get enabled() {
return this.#enabled;
}
set enabled(value) {
if (value === this.#enabled) return;
if (value) {
this.patchConsole();
} else {
this.unpatchConsole();
}
this.#enabled = value;
}
constructor(app) {
this.app = app;
this.dialog = new ComfyLoggingDialog(this);
this.addSetting();
this.catchUnhandled();
this.addInitData();
}
addSetting() {
const settingId = "Comfy.Logging.Enabled";
const htmlSettingId = settingId.replaceAll(".", "-");
const setting = this.app.ui.settings.addSetting({
id: settingId,
name: settingId,
defaultValue: true,
type: (name, setter, value) => {
return $el("tr", [
$el("td", [
$el("label", {
textContent: "Logging",
for: htmlSettingId,
}),
]),
$el("td", [
$el("input", {
id: htmlSettingId,
type: "checkbox",
checked: value,
onchange: (event) => {
setter((this.enabled = event.target.checked));
},
}),
$el("button", {
textContent: "View Logs",
onclick: () => {
this.app.ui.settings.element.close();
this.dialog.show();
},
style: {
fontSize: "14px",
display: "block",
marginTop: "5px",
},
}),
]),
]);
},
});
this.enabled = setting.value;
}
patchConsole() {
// Capture common console outputs
const self = this;
for (const type of ["log", "warn", "error", "debug"]) {
const orig = console[type];
this.#console[type] = orig;
console[type] = function () {
orig.apply(console, arguments);
self.addEntry("console", type, ...arguments);
};
}
}
unpatchConsole() {
// Restore original console functions
for (const type of Object.keys(this.#console)) {
console[type] = this.#console[type];
}
this.#console = {};
}
catchUnhandled() {
// Capture uncaught errors
window.addEventListener("error", (e) => {
this.addEntry("window", "error", e.error ?? "Unknown error");
return false;
});
window.addEventListener("unhandledrejection", (e) => {
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
});
}
clear() {
this.entries = [];
}
addEntry(source, type, ...args) {
if (this.enabled) {
this.entries.push({
source,
type,
timestamp: new Date(),
message: args,
});
}
}
log(source, ...args) {
this.addEntry(source, "log", ...args);
}
async addInitData() {
if (!this.enabled) return;
const source = "ComfyUI.Logging";
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
const systemStats = await api.getSystemStats();
this.addEntry(source, "debug", systemStats);
}
}

View File

@ -1,4 +1,4 @@
import {api} from "./api.js";
import { api } from "./api.js";
export function $el(tag, propsOrChildren, children) {
const split = tag.split(".");
@ -11,7 +11,7 @@ export function $el(tag, propsOrChildren, children) {
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const {parent, $: cb, dataset, style} = propsOrChildren;
const { parent, $: cb, dataset, style } = propsOrChildren;
delete propsOrChildren.parent;
delete propsOrChildren.$;
delete propsOrChildren.dataset;
@ -178,7 +178,7 @@ export class ComfyDialog {
return [
$el("button", {
type: "button",
textContent: "Close",
textContent: i18next.t("ui.close_btn"),
onclick: () => this.close(),
}),
];
@ -206,11 +206,11 @@ class ComfySettingsDialog extends ComfyDialog {
parent: document.body,
}, [
$el("table.comfy-modal-content.comfy-table", [
$el("caption", {textContent: "Settings"}),
$el("tbody", {$: (tbody) => (this.textElement = tbody)}),
$el("caption", { textContent: i18next.t("ui.settings.title") }),
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
$el("button", {
type: "button",
textContent: "Close",
textContent: i18next.t("ui.close_btn"),
style: {
cursor: "pointer",
},
@ -234,7 +234,7 @@ class ComfySettingsDialog extends ComfyDialog {
localStorage[settingId] = JSON.stringify(value);
}
addSetting({id, name, type, defaultValue, onChange, attrs = {}, tooltip = "",}) {
addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) {
if (!id) {
throw new Error("Settings must have an ID");
}
@ -270,7 +270,7 @@ class ComfySettingsDialog extends ComfyDialog {
$el("label", {
for: htmlID,
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
textContent: name,
textContent: i18next.t(`settings.${id}`),
})
]);
@ -337,7 +337,7 @@ class ComfySettingsDialog extends ComfyDialog {
value,
id: htmlID,
type: "number",
style: {maxWidth: "4rem"},
style: { maxWidth: "4rem" },
oninput: (e) => {
setter(e.target.value);
e.target.previousElementSibling.value = e.target.value;
@ -347,6 +347,32 @@ class ComfySettingsDialog extends ComfyDialog {
]),
]);
break;
case "combo":
element = $el("tr", [
labelCell,
$el("td", [
$el(
"select",
{
oninput: (e) => {
setter(e.target.value);
},
},
(typeof options === "function" ? options(value) : options || []).map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
const v = opt.value ?? opt.text;
return $el("option", {
value: v,
textContent: opt.text,
selected: value + "" === v + "",
});
})
),
]),
]);
break;
case "text":
default:
if (type !== "text") {
@ -391,10 +417,10 @@ class ComfySettingsDialog extends ComfyDialog {
show() {
this.textElement.replaceChildren(
$el("tr", {
style: {display: "none"},
style: { display: "none" },
}, [
$el("th"),
$el("th", {style: {width: "33%"}})
$el("th", { style: { width: "33%" } })
]),
...this.settings.map((s) => s.render()),
)
@ -431,7 +457,7 @@ class ComfyList {
name: "Delete",
cb: () => api.deleteItem(this.#type, item.prompt[1]),
};
return $el("div", {textContent: item.prompt[0] + ": "}, [
return $el("div", { textContent: item.prompt[0] + ": " }, [
$el("button", {
textContent: "Load",
onclick: () => {
@ -460,7 +486,7 @@ class ComfyList {
await this.load();
},
}),
$el("button", {textContent: "Refresh", onclick: () => this.load()}),
$el("button", { textContent: "Refresh", onclick: () => this.load() }),
])
);
}
@ -480,7 +506,7 @@ class ComfyList {
hide() {
this.element.style.display = "none";
this.button.textContent = "See " + this.#text;
this.button.textContent = "View " + this.#text;
}
toggle() {
@ -553,14 +579,14 @@ export class ComfyUI {
id: "comfy-file-input",
type: "file",
accept: ".json,image/png,.latent,.safetensors",
style: {display: "none"},
style: { display: "none" },
parent: document.body,
onchange: () => {
app.handleFile(fileInput.files[0]);
},
});
this.menuContainer = $el("div.comfy-menu", {parent: document.body}, [
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
$el("div.drag-handle", {
style: {
overflow: "hidden",
@ -570,16 +596,16 @@ export class ComfyUI {
}
}, [
$el("span.drag-handle"),
$el("span", {$: (q) => (this.queueSize = q)}),
$el("button.comfy-settings-btn", {textContent: "⚙️", onclick: () => this.settings.show()}),
$el("span", { $: (q) => (this.queueSize = q) }),
$el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }),
]),
$el("button.comfy-queue-btn", {
id: "queue-button",
textContent: "Queue Prompt",
textContent: i18next.t("ui.queue_btn"),
onclick: () => app.queuePrompt(0, this.batchCount),
}),
$el("div", {}, [
$el("label", {innerHTML: "Extra options"}, [
$el("label", { innerHTML: i18next.t("ui.extra_options") }, [
$el("input", {
type: "checkbox",
onchange: (i) => {
@ -590,14 +616,14 @@ export class ComfyUI {
}),
]),
]),
$el("div", {id: "extraOptions", style: {width: "100%", display: "none"}}, [
$el("label", {innerHTML: "Batch count"}, [
$el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [
$el("label", { innerHTML: "Batch count" }, [
$el("input", {
id: "batchCountInputNumber",
type: "number",
value: this.batchCount,
min: "1",
style: {width: "35%", "margin-left": "0.4em"},
style: { width: "35%", "margin-left": "0.4em" },
oninput: (i) => {
this.batchCount = i.target.value;
document.getElementById("batchCountInputRange").value = this.batchCount;
@ -625,13 +651,13 @@ export class ComfyUI {
$el("div.comfy-menu-btns", [
$el("button", {
id: "queue-front-button",
textContent: "Queue Front",
textContent: i18next.t("ui.queue_front_btn"),
onclick: () => app.queuePrompt(-1, this.batchCount)
}),
$el("button", {
$: (b) => (this.queue.button = b),
id: "comfy-view-queue-button",
textContent: "View Queue",
textContent: i18next.t("ui.view_queue_btn"),
onclick: () => {
this.history.hide();
this.queue.toggle();
@ -640,7 +666,7 @@ export class ComfyUI {
$el("button", {
$: (b) => (this.history.button = b),
id: "comfy-view-history-button",
textContent: "View History",
textContent: i18next.t("ui.view_history_btn"),
onclick: () => {
this.queue.hide();
this.history.toggle();
@ -651,7 +677,7 @@ export class ComfyUI {
this.history.element,
$el("button", {
id: "comfy-save-button",
textContent: "Save",
textContent: i18next.t("ui.save_btn"),
onclick: () => {
let filename = "workflow.json";
if (promptFilename.value) {
@ -662,12 +688,12 @@ export class ComfyUI {
}
}
const json = JSON.stringify(app.graph.serialize(), null, 2); // convert the data to a JSON string
const blob = new Blob([json], {type: "application/json"});
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: filename,
style: {display: "none"},
style: { display: "none" },
parent: document.body,
});
a.click();
@ -680,7 +706,7 @@ export class ComfyUI {
$el("button", {
id: "comfy-dev-save-api-button",
textContent: "Save (API Format)",
style: {width: "100%", display: "none"},
style: { width: "100%", display: "none" },
onclick: () => {
let filename = "workflow_api.json";
if (promptFilename.value) {
@ -690,14 +716,14 @@ export class ComfyUI {
filename += ".json";
}
}
app.graphToPrompt().then(p=>{
app.graphToPrompt().then(p => {
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
const blob = new Blob([json], {type: "application/json"});
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: filename,
style: {display: "none"},
style: { display: "none" },
parent: document.body,
});
a.click();
@ -708,15 +734,15 @@ export class ComfyUI {
});
},
}),
$el("button", {id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click()}),
$el("button", { id: "comfy-load-button", textContent: i18next.t("ui.load_btn"), onclick: () => fileInput.click() }),
$el("button", {
id: "comfy-refresh-button",
textContent: "Refresh",
textContent: i18next.t("ui.refresh_btn"),
onclick: () => app.refreshComboInNodes()
}),
$el("button", {id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace()}),
$el("button", { id: "comfy-clipspace-button", textContent: i18next.t("ui.clipspace_btn"), onclick: () => app.openClipspace() }),
$el("button", {
id: "comfy-clear-button", textContent: "Clear", onclick: () => {
id: "comfy-clear-button", textContent: i18next.t("ui.clear_btn"), onclick: () => {
if (!confirmClear.value || confirm("Clear workflow?")) {
app.clean();
app.graph.clear();
@ -724,7 +750,7 @@ export class ComfyUI {
}
}),
$el("button", {
id: "comfy-load-default-button", textContent: "Load Default", onclick: () => {
id: "comfy-load-default-button", textContent: i18next.t("ui.load_default_btn"), onclick: () => {
if (!confirmClear.value || confirm("Load default workflow?")) {
app.loadGraphData()
}
@ -737,16 +763,16 @@ export class ComfyUI {
name: "Enable Dev mode Options",
type: "boolean",
defaultValue: false,
onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "block" : "none"},
onChange: function (value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "block" : "none" },
});
dragElement(this.menuContainer, this.settings);
this.setStatus({exec_info: {queue_remaining: "X"}});
this.setStatus({ exec_info: { queue_remaining: "X" } });
}
setStatus(status) {
this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
this.queueSize.textContent = i18next.t("ui.queue_size") + (status ? status.exec_info.queue_remaining : "ERR");
if (status) {
if (
this.lastQueueSize != 0 &&