diff --git a/README.md b/README.md index 1de9d4c3b..84c10bfe2 100644 --- a/README.md +++ b/README.md @@ -87,13 +87,13 @@ Put your SD checkpoints (the huge ckpt/safetensors files) in: models/checkpoints Put your VAE in: models/vae -At the time of writing this pytorch has issues with python versions higher than 3.10 so make sure your python/pip versions are 3.10. - ### AMD GPUs (Linux only) AMD users can install rocm and pytorch with pip if you don't have it already installed, this is the command to install the stable version: ```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/rocm5.4.2``` +This is the command to install the nightly with ROCm 5.5 that supports the 7000 series and might have some performance improvements: +```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm5.5 -r requirements.txt``` ### NVIDIA @@ -178,16 +178,6 @@ To use a textual inversion concepts/embeddings in a text prompt put them in the ```embedding:embedding_filename.pt``` -### Fedora - -To get python 3.10 on fedora: -```dnf install python3.10``` - -Then you can: - -```python3.10 -m ensurepip``` - -This will let you use: pip3.10 to install all the dependencies. ## How to increase generation speed? diff --git a/comfy/k_diffusion/external.py b/comfy/k_diffusion/external.py index 49ce5ae39..d15eb5951 100644 --- a/comfy/k_diffusion/external.py +++ b/comfy/k_diffusion/external.py @@ -134,7 +134,7 @@ class CompVisDenoiser(DiscreteEpsDDPMDenoiser): """A wrapper for CompVis diffusion models.""" def __init__(self, model, quantize=False, device='cpu'): - super().__init__(model, model.alphas_cumprod, quantize=quantize) + super().__init__(model, model.alphas_cumprod.float(), quantize=quantize) def get_eps(self, *args, **kwargs): return self.inner_model.apply_model(*args, **kwargs) @@ -173,7 +173,7 @@ class CompVisVDenoiser(DiscreteVDDPMDenoiser): """A wrapper for CompVis diffusion models that output v.""" def __init__(self, model, quantize=False, device='cpu'): - super().__init__(model, model.alphas_cumprod, quantize=quantize) + super().__init__(model, model.alphas_cumprod.float(), quantize=quantize) def get_v(self, x, t, cond, **kwargs): return self.inner_model.apply_model(x, t, cond) diff --git a/comfy/ldm/models/diffusion/ddim.py b/comfy/ldm/models/diffusion/ddim.py index c279f2c18..d5649089a 100644 --- a/comfy/ldm/models/diffusion/ddim.py +++ b/comfy/ldm/models/diffusion/ddim.py @@ -284,7 +284,7 @@ class DDIMSampler(object): model_output = model_uncond + unconditional_guidance_scale * (model_t - model_uncond) if self.model.parameterization == "v": - e_t = self.model.predict_eps_from_z_and_v(x, t, model_output) + e_t = extract_into_tensor(self.sqrt_alphas_cumprod, t, x.shape) * model_output + extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x.shape) * x else: e_t = model_output @@ -306,7 +306,7 @@ class DDIMSampler(object): if self.model.parameterization != "v": pred_x0 = (x - sqrt_one_minus_at * e_t) / a_t.sqrt() else: - pred_x0 = self.model.predict_start_from_z_and_v(x, t, model_output) + pred_x0 = extract_into_tensor(self.sqrt_alphas_cumprod, t, x.shape) * x - extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x.shape) * model_output if quantize_denoised: pred_x0, _, *_ = self.model.first_stage_model.quantize(pred_x0) diff --git a/comfy/ldm/modules/attention.py b/comfy/ldm/modules/attention.py index 62248f79e..62707dfd2 100644 --- a/comfy/ldm/modules/attention.py +++ b/comfy/ldm/modules/attention.py @@ -51,9 +51,9 @@ def init_(tensor): # feedforward class GEGLU(nn.Module): - def __init__(self, dim_in, dim_out): + def __init__(self, dim_in, dim_out, dtype=None): super().__init__() - self.proj = comfy.ops.Linear(dim_in, dim_out * 2) + self.proj = comfy.ops.Linear(dim_in, dim_out * 2, dtype=dtype) def forward(self, x): x, gate = self.proj(x).chunk(2, dim=-1) @@ -68,7 +68,7 @@ class FeedForward(nn.Module): project_in = nn.Sequential( comfy.ops.Linear(dim, inner_dim, dtype=dtype), nn.GELU() - ) if not glu else GEGLU(dim, inner_dim) + ) if not glu else GEGLU(dim, inner_dim, dtype=dtype) self.net = nn.Sequential( project_in, @@ -89,8 +89,8 @@ def zero_module(module): return module -def Normalize(in_channels): - return torch.nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True) +def Normalize(in_channels, dtype=None): + return torch.nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True, dtype=dtype) class SpatialSelfAttention(nn.Module): @@ -594,7 +594,7 @@ class SpatialTransformer(nn.Module): context_dim = [context_dim] self.in_channels = in_channels inner_dim = n_heads * d_head - self.norm = Normalize(in_channels) + self.norm = Normalize(in_channels, dtype=dtype) if not use_linear: self.proj_in = nn.Conv2d(in_channels, inner_dim, diff --git a/comfy/ldm/modules/diffusionmodules/openaimodel.py b/comfy/ldm/modules/diffusionmodules/openaimodel.py index 0307831d1..e170f6779 100644 --- a/comfy/ldm/modules/diffusionmodules/openaimodel.py +++ b/comfy/ldm/modules/diffusionmodules/openaimodel.py @@ -111,14 +111,14 @@ class Upsample(nn.Module): upsampling occurs in the inner-two dimensions. """ - def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1): + def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1, dtype=None): super().__init__() self.channels = channels self.out_channels = out_channels or channels self.use_conv = use_conv self.dims = dims if use_conv: - self.conv = conv_nd(dims, self.channels, self.out_channels, 3, padding=padding) + self.conv = conv_nd(dims, self.channels, self.out_channels, 3, padding=padding, dtype=dtype) def forward(self, x, output_shape=None): assert x.shape[1] == self.channels @@ -160,7 +160,7 @@ class Downsample(nn.Module): downsampling occurs in the inner-two dimensions. """ - def __init__(self, channels, use_conv, dims=2, out_channels=None,padding=1): + def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1, dtype=None): super().__init__() self.channels = channels self.out_channels = out_channels or channels @@ -169,7 +169,7 @@ class Downsample(nn.Module): stride = 2 if dims != 3 else (1, 2, 2) if use_conv: self.op = conv_nd( - dims, self.channels, self.out_channels, 3, stride=stride, padding=padding + dims, self.channels, self.out_channels, 3, stride=stride, padding=padding, dtype=dtype ) else: assert self.channels == self.out_channels @@ -220,7 +220,7 @@ class ResBlock(TimestepBlock): self.use_scale_shift_norm = use_scale_shift_norm self.in_layers = nn.Sequential( - normalization(channels), + normalization(channels, dtype=dtype), nn.SiLU(), conv_nd(dims, channels, self.out_channels, 3, padding=1, dtype=dtype), ) @@ -228,11 +228,11 @@ class ResBlock(TimestepBlock): self.updown = up or down if up: - self.h_upd = Upsample(channels, False, dims) - self.x_upd = Upsample(channels, False, dims) + self.h_upd = Upsample(channels, False, dims, dtype=dtype) + self.x_upd = Upsample(channels, False, dims, dtype=dtype) elif down: - self.h_upd = Downsample(channels, False, dims) - self.x_upd = Downsample(channels, False, dims) + self.h_upd = Downsample(channels, False, dims, dtype=dtype) + self.x_upd = Downsample(channels, False, dims, dtype=dtype) else: self.h_upd = self.x_upd = nn.Identity() @@ -240,11 +240,11 @@ class ResBlock(TimestepBlock): nn.SiLU(), linear( emb_channels, - 2 * self.out_channels if use_scale_shift_norm else self.out_channels, + 2 * self.out_channels if use_scale_shift_norm else self.out_channels, dtype=dtype ), ) self.out_layers = nn.Sequential( - normalization(self.out_channels), + normalization(self.out_channels, dtype=dtype), nn.SiLU(), nn.Dropout(p=dropout), zero_module( @@ -604,6 +604,7 @@ class UNetModel(nn.Module): dims=dims, use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, + dtype=self.dtype ) ] ch = mult * model_channels @@ -651,10 +652,11 @@ class UNetModel(nn.Module): use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, down=True, + dtype=self.dtype ) if resblock_updown else Downsample( - ch, conv_resample, dims=dims, out_channels=out_ch + ch, conv_resample, dims=dims, out_channels=out_ch, dtype=self.dtype ) ) ) @@ -679,6 +681,7 @@ class UNetModel(nn.Module): dims=dims, use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, + dtype=self.dtype ), AttentionBlock( ch, @@ -698,6 +701,7 @@ class UNetModel(nn.Module): dims=dims, use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, + dtype=self.dtype ), ) self._feature_size += ch @@ -715,6 +719,7 @@ class UNetModel(nn.Module): dims=dims, use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, + dtype=self.dtype ) ] ch = model_channels * mult @@ -758,18 +763,19 @@ class UNetModel(nn.Module): use_checkpoint=use_checkpoint, use_scale_shift_norm=use_scale_shift_norm, up=True, + dtype=self.dtype ) if resblock_updown - else Upsample(ch, conv_resample, dims=dims, out_channels=out_ch) + else Upsample(ch, conv_resample, dims=dims, out_channels=out_ch, dtype=self.dtype) ) ds //= 2 self.output_blocks.append(TimestepEmbedSequential(*layers)) self._feature_size += ch self.out = nn.Sequential( - normalization(ch), + normalization(ch, dtype=self.dtype), nn.SiLU(), - zero_module(conv_nd(dims, model_channels, out_channels, 3, padding=1)), + zero_module(conv_nd(dims, model_channels, out_channels, 3, padding=1, dtype=self.dtype)), ) if self.predict_codebook_ids: self.id_predictor = nn.Sequential( diff --git a/comfy/ldm/modules/diffusionmodules/util.py b/comfy/ldm/modules/diffusionmodules/util.py index d6a4778e4..d890c8044 100644 --- a/comfy/ldm/modules/diffusionmodules/util.py +++ b/comfy/ldm/modules/diffusionmodules/util.py @@ -206,13 +206,13 @@ def mean_flat(tensor): return tensor.mean(dim=list(range(1, len(tensor.shape)))) -def normalization(channels): +def normalization(channels, dtype=None): """ Make a standard normalization layer. :param channels: number of input channels. :return: an nn.Module for normalization. """ - return GroupNorm32(32, channels) + return GroupNorm32(32, channels, dtype=dtype) # PyTorch 1.7 has SiLU, but we support PyTorch 1.5. diff --git a/comfy/sd.py b/comfy/sd.py index 24806dd01..7f04ae3a7 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -1159,9 +1159,6 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o else: model = model_base.BaseModel(unet_config, v_prediction=v_prediction) - if fp16: - model = model.half() - model = load_model_weights(model, sd, verbose=False, load_state_dict_to=load_state_dict_to) return (ModelPatcher(model), clip, vae, clipvision) diff --git a/nodes.py b/nodes.py index 658e32dad..cbb7d69ea 100644 --- a/nodes.py +++ b/nodes.py @@ -756,7 +756,7 @@ class RepeatLatentBatch: return (s,) class LatentUpscale: - upscale_methods = ["nearest-exact", "bilinear", "area", "bislerp"] + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"] crop_methods = ["disabled", "center"] @classmethod @@ -776,7 +776,7 @@ class LatentUpscale: return (s,) class LatentUpscaleBy: - upscale_methods = ["nearest-exact", "bilinear", "area", "bislerp"] + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"] @classmethod def INPUT_TYPES(s): @@ -1172,7 +1172,7 @@ class LoadImageMask: return True class ImageScale: - upscale_methods = ["nearest-exact", "bilinear", "area"] + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic"] crop_methods = ["disabled", "center"] @classmethod @@ -1193,7 +1193,7 @@ class ImageScale: return (s,) class ImageScaleBy: - upscale_methods = ["nearest-exact", "bilinear", "area"] + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic"] @classmethod def INPUT_TYPES(s): diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 592dfd2d1..9836143d3 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -56,7 +56,9 @@ const colorPalettes = { "descrip-text": "#999", "drag-text": "#ccc", "error-text": "#ff4444", - "border-color": "#4e4e4e" + "border-color": "#4e4e4e", + "tr-even-bg-color": "#222", + "tr-odd-bg-color": "#353535", } }, }, @@ -111,7 +113,9 @@ const colorPalettes = { "descrip-text": "#444", "drag-text": "#555", "error-text": "#F44336", - "border-color": "#888" + "border-color": "#888", + "tr-even-bg-color": "#f9f9f9", + "tr-odd-bg-color": "#fff", } }, }, @@ -165,7 +169,9 @@ const colorPalettes = { "descrip-text": "#586e75", // Base01 "drag-text": "#839496", // Base0 "error-text": "#dc322f", // Solarized Red - "border-color": "#657b83" // Base00 + "border-color": "#657b83", // Base00 + "tr-even-bg-color": "#002b36", + "tr-odd-bg-color": "#073642", } }, } @@ -194,7 +200,7 @@ app.registerExtension({ const nodeData = defs[nodeId]; var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] != undefined) { + if (nodeData["input"]["optional"] !== undefined) { inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) } @@ -214,7 +220,7 @@ app.registerExtension({ } return types; - }; + } function completeColorPalette(colorPalette) { var types = getSlotTypes(); @@ -228,7 +234,7 @@ app.registerExtension({ colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot); return colorPalette; - }; + } const getColorPaletteTemplate = async () => { let colorPalette = { @@ -267,31 +273,31 @@ app.registerExtension({ const addCustomColorPalette = async (colorPalette) => { if (typeof (colorPalette) !== "object") { - app.ui.dialog.show("Invalid color palette"); + alert("Invalid color palette."); return; } if (!colorPalette.id) { - app.ui.dialog.show("Color palette missing id"); + alert("Color palette missing id."); return; } if (!colorPalette.name) { - app.ui.dialog.show("Color palette missing name"); + alert("Color palette missing name."); return; } if (!colorPalette.colors) { - app.ui.dialog.show("Color palette missing colors"); + alert("Color palette missing colors."); return; } if (colorPalette.colors.node_slot && typeof (colorPalette.colors.node_slot) !== "object") { - app.ui.dialog.show("Invalid color palette colors.node_slot"); + alert("Invalid color palette colors.node_slot."); return; } - let customColorPalettes = getCustomColorPalettes(); + const customColorPalettes = getCustomColorPalettes(); customColorPalettes[colorPalette.id] = colorPalette; setCustomColorPalettes(customColorPalettes); @@ -312,7 +318,7 @@ app.registerExtension({ }; const deleteCustomColorPalette = async (colorPaletteId) => { - let customColorPalettes = getCustomColorPalettes(); + const customColorPalettes = getCustomColorPalettes(); delete customColorPalettes[colorPaletteId]; setCustomColorPalettes(customColorPalettes); @@ -387,8 +393,7 @@ app.registerExtension({ style: {display: "none"}, parent: document.body, onchange: () => { - let file = fileInput.files[0]; - + const file = fileInput.files[0]; if (file.type === "application/json" || file.name.endsWith(".json")) { const reader = new FileReader(); reader.onload = async () => { @@ -403,104 +408,116 @@ app.registerExtension({ id, name: "Color Palette", type: (name, setter, value) => { - let options = []; + const options = [ + ...Object.values(colorPalettes).map(c=> $el("option", { + textContent: c.name, + value: c.id, + selected: c.id === value + })), + ...Object.values(getCustomColorPalettes()).map(c=>$el("option", { + textContent: `${c.name} (custom)`, + value: `custom_${c.id}`, + selected: `custom_${c.id}` === value + })) , + ]; - for (const c in colorPalettes) { - const colorPalette = colorPalettes[c]; - options.push($el("option", { - textContent: colorPalette.name, - value: colorPalette.id, - selected: colorPalette.id === value - })); - } + els.select = $el("select", { + style: { + marginBottom: "0.15rem", + width: "100%", + }, + onchange: (e) => { + setter(e.target.value); + } + }, options) - let customColorPalettes = getCustomColorPalettes(); - for (const c in customColorPalettes) { - const colorPalette = customColorPalettes[c]; - options.push($el("option", { - textContent: colorPalette.name + " (custom)", - value: "custom_" + colorPalette.id, - selected: "custom_" + colorPalette.id === value - })); - } - - return $el("div", [ - $el("label", {textContent: name || id}, [ - els.select = $el("select", { - onchange: (e) => { - setter(e.target.value); - } - }, options) + return $el("tr", [ + $el("td", [ + $el("label", { + for: id.replaceAll(".", "-"), + textContent: "Color palette:", + }), ]), - $el("input", { - type: "button", - value: "Export", - onclick: async () => { - const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId)); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: colorPaletteId + ".json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }, - }), - $el("input", { - type: "button", - value: "Import", - onclick: () => { - fileInput.click(); - } - }), - $el("input", { - type: "button", - value: "Template", - onclick: async () => { - const colorPalette = await getColorPaletteTemplate(); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: "color_palette.json", - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - }), - $el("input", { - type: "button", - value: "Delete", - onclick: async () => { - let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); + $el("td", [ + els.select, + $el("div", { + style: { + display: "grid", + gap: "4px", + gridAutoFlow: "column", + }, + }, [ + $el("input", { + type: "button", + value: "Export", + onclick: async () => { + const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); + const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId)); + const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: colorPaletteId + ".json", + style: {display: "none"}, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("input", { + type: "button", + value: "Import", + onclick: () => { + fileInput.click(); + } + }), + $el("input", { + type: "button", + value: "Template", + onclick: async () => { + const colorPalette = await getColorPaletteTemplate(); + const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: "color_palette.json", + style: {display: "none"}, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + } + }), + $el("input", { + type: "button", + value: "Delete", + onclick: async () => { + let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - if (colorPalettes[colorPaletteId]) { - app.ui.dialog.show("You cannot delete built-in color palette"); - return; - } + if (colorPalettes[colorPaletteId]) { + alert("You cannot delete a built-in color palette."); + return; + } - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - } + if (colorPaletteId.startsWith("custom_")) { + colorPaletteId = colorPaletteId.substr(7); + } - await deleteCustomColorPalette(colorPaletteId); - } - }), - ]); + await deleteCustomColorPalette(colorPaletteId); + } + }), + ]), + ]), + ]) }, defaultValue: defaultColorPaletteId, async onChange(value) { diff --git a/web/extensions/core/slotDefaults.js b/web/extensions/core/slotDefaults.js index 9401678b0..5b8304711 100644 --- a/web/extensions/core/slotDefaults.js +++ b/web/extensions/core/slotDefaults.js @@ -10,7 +10,7 @@ app.registerExtension({ LiteGraph.middle_click_slot_add_default_node = true; this.suggestionsNumber = app.ui.settings.addSetting({ id: "Comfy.NodeSuggestions.number", - name: "number of nodes suggestions", + name: "Number of nodes suggestions", type: "slider", attrs: { min: 1, diff --git a/web/scripts/ui.js b/web/scripts/ui.js index a26eedec3..fe81984d3 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -1,19 +1,26 @@ -import { api } from "./api.js"; +import {api} from "./api.js"; export function $el(tag, propsOrChildren, children) { const split = tag.split("."); const element = document.createElement(split.shift()); - element.classList.add(...split); + if (split.length > 0) { + element.classList.add(...split); + } + if (propsOrChildren) { 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; delete propsOrChildren.style; + if (Object.hasOwn(propsOrChildren, "for")) { + element.setAttribute("for", propsOrChildren.for) + } + if (style) { Object.assign(element.style, style); } @@ -119,6 +126,7 @@ function dragElement(dragEl, settings) { savePos = value; }, }); + function dragMouseDown(e) { e = e || window.event; e.preventDefault(); @@ -161,8 +169,8 @@ function dragElement(dragEl, settings) { export class ComfyDialog { constructor() { - this.element = $el("div.comfy-modal", { parent: document.body }, [ - $el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]), + this.element = $el("div.comfy-modal", {parent: document.body}, [ + $el("div.comfy-modal-content", [$el("p", {$: (p) => (this.textElement = p)}), ...this.createButtons()]), ]); } @@ -193,7 +201,22 @@ export class ComfyDialog { class ComfySettingsDialog extends ComfyDialog { constructor() { super(); - this.element.classList.add("comfy-settings"); + this.element = $el("dialog", { + id: "comfy-settings-dialog", + parent: document.body, + }, [ + $el("table.comfy-modal-content.comfy-table", [ + $el("caption", {textContent: "Settings"}), + $el("tbody", {$: (tbody) => (this.textElement = tbody)}), + $el("button", { + type: "button", + textContent: "Close", + onclick: () => { + this.element.close(); + }, + }), + ]), + ]); this.settings = []; } @@ -208,15 +231,16 @@ 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 = "",}) { if (!id) { throw new Error("Settings must have an ID"); } + if (this.settings.find((s) => s.id === id)) { - throw new Error("Setting IDs must be unique"); + throw new Error(`Setting ${id} of type ${type} must have a unique ID.`); } - const settingId = "Comfy.Settings." + id; + const settingId = `Comfy.Settings.${id}`; const v = localStorage[settingId]; let value = v == null ? defaultValue : JSON.parse(v); @@ -234,34 +258,50 @@ class ComfySettingsDialog extends ComfyDialog { localStorage[settingId] = JSON.stringify(v); value = v; }; + value = this.getSettingValue(id, defaultValue); let element; - value = this.getSettingValue(id, defaultValue); + const htmlID = id.replaceAll(".", "-"); + + const labelCell = $el("td", [ + $el("label", { + for: htmlID, + classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""], + textContent: name.endsWith(":") ? name : `${name}:`, + }) + ]); if (typeof type === "function") { element = type(name, setter, value, attrs); } else { switch (type) { case "boolean": - element = $el("div", [ - $el("label", { textContent: name || id }, [ + element = $el("tr", [ + labelCell, + $el("td", [ $el("input", { + id: htmlID, type: "checkbox", - checked: !!value, - oninput: (e) => { - setter(e.target.checked); + checked: value, + onchange: (event) => { + const isChecked = event.target.checked; + if (onChange !== undefined) { + onChange(isChecked) + } + this.setSettingValue(id, isChecked); }, - ...attrs }), ]), - ]); + ]) break; case "number": - element = $el("div", [ - $el("label", { textContent: name || id }, [ + element = $el("tr", [ + labelCell, + $el("td", [ $el("input", { type, value, + id: htmlID, oninput: (e) => { setter(e.target.value); }, @@ -271,46 +311,62 @@ class ComfySettingsDialog extends ComfyDialog { ]); break; case "slider": - element = $el("div", [ - $el("label", { textContent: name }, [ - $el("input", { - type: "range", - value, - oninput: (e) => { - setter(e.target.value); - e.target.nextElementSibling.value = e.target.value; + element = $el("tr", [ + labelCell, + $el("td", [ + $el("div", { + style: { + display: "grid", + gridAutoFlow: "column", }, - ...attrs - }), - $el("input", { - type: "number", - value, - oninput: (e) => { - setter(e.target.value); - e.target.previousElementSibling.value = e.target.value; - }, - ...attrs - }), + }, [ + $el("input", { + ...attrs, + value, + type: "range", + oninput: (e) => { + setter(e.target.value); + e.target.nextElementSibling.value = e.target.value; + }, + }), + $el("input", { + ...attrs, + value, + id: htmlID, + type: "number", + style: {maxWidth: "4rem"}, + oninput: (e) => { + setter(e.target.value); + e.target.previousElementSibling.value = e.target.value; + }, + }), + ]), ]), ]); break; + case "text": default: - console.warn("Unsupported setting type, defaulting to text"); - element = $el("div", [ - $el("label", { textContent: name || id }, [ + if (type !== "text") { + console.warn(`Unsupported setting type '${type}, defaulting to text`); + } + + element = $el("tr", [ + labelCell, + $el("td", [ $el("input", { value, + id: htmlID, oninput: (e) => { setter(e.target.value); }, - ...attrs + ...attrs, }), ]), ]); break; } } - if(tooltip) { + if (tooltip) { element.title = tooltip; } @@ -330,13 +386,16 @@ class ComfySettingsDialog extends ComfyDialog { } show() { - super.show(); - Object.assign(this.textElement.style, { - display: "flex", - flexDirection: "column", - gap: "10px" - }); - this.textElement.replaceChildren(...this.settings.map((s) => s.render())); + this.textElement.replaceChildren( + $el("tr", { + style: {display: "none"}, + }, [ + $el("th"), + $el("th", {style: {width: "33%"}}) + ]), + ...this.settings.map((s) => s.render()), + ) + this.element.showModal(); } } @@ -369,7 +428,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: () => { @@ -398,7 +457,7 @@ class ComfyList { await this.load(); }, }), - $el("button", { textContent: "Refresh", onclick: () => this.load() }), + $el("button", {textContent: "Refresh", onclick: () => this.load()}), ]) ); } @@ -475,8 +534,8 @@ export class ComfyUI { */ const previewImage = this.settings.addSetting({ id: "Comfy.PreviewFormat", - name: "When displaying a preview in the image widget, convert it to a lightweight image. (webp, jpeg, webp;50, ...)", - type: "string", + name: "When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.", + type: "text", defaultValue: "", }); @@ -484,18 +543,25 @@ export class ComfyUI { id: "comfy-file-input", type: "file", accept: ".json,image/png,.latent", - style: { display: "none" }, + style: {display: "none"}, parent: document.body, onchange: () => { app.handleFile(fileInput.files[0]); }, }); - this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ - $el("div.drag-handle", { style: { overflow: "hidden", position: "relative", width: "100%", cursor: "default" } }, [ + this.menuContainer = $el("div.comfy-menu", {parent: document.body}, [ + $el("div.drag-handle", { + style: { + overflow: "hidden", + position: "relative", + width: "100%", + cursor: "default" + } + }, [ $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", @@ -503,7 +569,7 @@ export class ComfyUI { onclick: () => app.queuePrompt(0, this.batchCount), }), $el("div", {}, [ - $el("label", { innerHTML: "Extra options" }, [ + $el("label", {innerHTML: "Extra options"}, [ $el("input", { type: "checkbox", onchange: (i) => { @@ -514,14 +580,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; @@ -547,7 +613,11 @@ export class ComfyUI { ]), ]), $el("div.comfy-menu-btns", [ - $el("button", { id: "queue-front-button", textContent: "Queue Front", onclick: () => app.queuePrompt(-1, this.batchCount) }), + $el("button", { + id: "queue-front-button", + textContent: "Queue Front", + onclick: () => app.queuePrompt(-1, this.batchCount) + }), $el("button", { $: (b) => (this.queue.button = b), id: "comfy-view-queue-button", @@ -582,12 +652,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(); @@ -597,25 +667,33 @@ export class ComfyUI { }, 0); }, }), - $el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }), - $el("button", { id: "comfy-refresh-button", textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), - $el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }), - $el("button", { id: "comfy-clear-button", textContent: "Clear", onclick: () => { - if (!confirmClear.value || confirm("Clear workflow?")) { - app.clean(); - app.graph.clear(); + $el("button", {id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click()}), + $el("button", { + id: "comfy-refresh-button", + textContent: "Refresh", + onclick: () => app.refreshComboInNodes() + }), + $el("button", {id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace()}), + $el("button", { + id: "comfy-clear-button", textContent: "Clear", onclick: () => { + if (!confirmClear.value || confirm("Clear workflow?")) { + app.clean(); + app.graph.clear(); + } } - }}), - $el("button", { id: "comfy-load-default-button", textContent: "Load Default", onclick: () => { - if (!confirmClear.value || confirm("Load default workflow?")) { - app.loadGraphData() + }), + $el("button", { + id: "comfy-load-default-button", textContent: "Load Default", onclick: () => { + if (!confirmClear.value || confirm("Load default workflow?")) { + app.loadGraphData() + } } - }}), + }), ]); dragElement(this.menuContainer, this.settings); - this.setStatus({ exec_info: { queue_remaining: "X" } }); + this.setStatus({exec_info: {queue_remaining: "X"}}); } setStatus(status) { diff --git a/web/style.css b/web/style.css index 5fea5bba8..5b6b9ec57 100644 --- a/web/style.css +++ b/web/style.css @@ -8,6 +8,8 @@ --drag-text: #ccc; --error-text: #ff4444; --border-color: #4e4e4e; + --tr-even-bg-color: #222; + --tr-odd-bg-color: #353535; } @media (prefers-color-scheme: dark) { @@ -220,7 +222,7 @@ button.comfy-queue-btn { margin: 6px 0 !important; } -.comfy-modal.comfy-settings, +.comfy-modal.comfy-settings, .comfy-modal.comfy-manage-templates { text-align: center; font-family: sans-serif; @@ -246,6 +248,11 @@ button.comfy-queue-btn { font-size: inherit; } +.comfy-tooltip-indicator { + text-decoration: underline; + text-decoration-style: dashed; +} + @media only screen and (max-height: 850px) { .comfy-menu { top: 0 !important; @@ -254,8 +261,9 @@ button.comfy-queue-btn { right: 0 !important; border-radius: 0; } + .comfy-menu span.drag-handle { - visibility:hidden + visibility: hidden } } @@ -287,11 +295,75 @@ button.comfy-queue-btn { border-radius: 12px 0 0 12px; } +/* Dialogs */ + +dialog { + box-shadow: 0 0 20px #888888; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +#comfy-settings-dialog { + padding: 0; + width: 41rem; +} + +#comfy-settings-dialog tr > td:first-child { + text-align: right; +} + +#comfy-settings-dialog button { + background-color: var(--bg-color); + border: 1px var(--border-color) solid; + border-radius: 0; + color: var(--input-text); + font-size: 1rem; + padding: 0.5rem; +} + +#comfy-settings-dialog button:hover { + background-color: var(--tr-odd-bg-color); +} + +/* General CSS for tables */ + +.comfy-table { + border-collapse: collapse; + color: var(--input-text); + font-family: Arial, sans-serif; + width: 100%; +} + +.comfy-table caption { + background-color: var(--bg-color); + color: var(--input-text); + font-size: 1rem; + font-weight: bold; + padding: 8px; + text-align: center; +} + +.comfy-table tr:nth-child(even) { + background-color: var(--tr-even-bg-color); +} + +.comfy-table tr:nth-child(odd) { + background-color: var(--tr-odd-bg-color); +} + +.comfy-table td, +.comfy-table th { + border: 1px solid var(--border-color); + padding: 8px; +} + /* Context menu */ .litegraph .dialog { - z-index: 1; - font-family: Arial, sans-serif; + z-index: 1; + font-family: Arial, sans-serif; } .litegraph .litemenu-entry.has_submenu {