1118 lines
45 KiB
HTML
1118 lines
45 KiB
HTML
<html>
|
|
<head>
|
|
<link rel="stylesheet" type="text/css" href="litegraph.css">
|
|
<script type="text/javascript" src="litegraph.core.js"></script>
|
|
</head>
|
|
<style>
|
|
.customtext_input {
|
|
background-color: #FFFFFF;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background-color: #202020;
|
|
}
|
|
.customtext_input {
|
|
background-color: #202020;
|
|
color: white;
|
|
}
|
|
}
|
|
</style>
|
|
<body style='width:100%; height:100%; overflow: hidden;'>
|
|
<style>
|
|
.modal {
|
|
display: none; /* Hidden by default */
|
|
position: fixed; /* Stay in place */
|
|
z-index: 100; /* Sit on top */
|
|
left: 20%; /* Center the modal horizontally */
|
|
right: 20%; /* Center the modal horizontally */
|
|
bottom: 30%; /* Center the modal vertically */
|
|
/* transform: translate(-50%, -50%); /* Use this to center the modal */ */
|
|
width: 50%; /* Set a width for the modal */
|
|
height: auto; /* Set a height for the modal */
|
|
padding: 30px;
|
|
background-color: #ff0000; /* Modal background */
|
|
box-shadow: 0px 0px 20px #888888;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.close {
|
|
color: #aaaaaa;
|
|
font-size: 24px; /* Decreased font-size */
|
|
font-weight: bold;
|
|
position: absolute;
|
|
bottom: 10px; /* move the close button up a bit */
|
|
left: 50%; /* center the close button horizontally */
|
|
transform: translateX(-50%); /* use this to center the close button horizontally */
|
|
width: 100%;
|
|
text-align: center; /* center the text inside the button */
|
|
}
|
|
|
|
.close:hover,
|
|
.close:focus {
|
|
color: #000;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#modal-text {
|
|
white-space: pre-line; /* This will respect line breaks */
|
|
margin-bottom: 20px; /* Add some margin between the text and the close button*/
|
|
}
|
|
|
|
@media only screen and (max-height: 850px) {
|
|
#menu {
|
|
margin-top: -70px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<div id="myErrorModal" class="modal">
|
|
<div class="modal-content">
|
|
<p id="modal-text"></p>
|
|
<span class="close">CLOSE</span>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id='mycanvas' width='1000' height='1000' style='width: 100%; height: 100%;'></canvas>
|
|
<script>
|
|
|
|
var graph = new LGraph();
|
|
|
|
var canvas = new LGraphCanvas("#mycanvas", graph);
|
|
|
|
const ccc = document.getElementById("mycanvas");
|
|
const ctx = ccc.getContext("2d");
|
|
let nodeOutputs = {}
|
|
|
|
// Resize the canvas to match the size of the canvas element
|
|
function resizeCanvas() {
|
|
ccc.width = ccc.offsetWidth;
|
|
ccc.height = ccc.offsetHeight;
|
|
canvas.draw(true, true);
|
|
}
|
|
// call the function when the page loads
|
|
resizeCanvas();
|
|
// call the function when the window is resized
|
|
window.addEventListener("resize", resizeCanvas);
|
|
|
|
var default_graph = {"last_node_id":9,"last_link_id":9,"nodes":[{"id":7,"type":"CLIPTextEncode","pos":[413,389],"size":{"0":425.27801513671875,"1":180.6060791015625},"flags":{},"order":3,"mode":0,"inputs":[{"name":"clip","type":"CLIP","link":5}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[6],"slot_index":0}],"properties":{},"widgets_values":["bad hands"]},{"id":6,"type":"CLIPTextEncode","pos":[415,186],"size":{"0":422.84503173828125,"1":164.31304931640625},"flags":{},"order":2,"mode":0,"inputs":[{"name":"clip","type":"CLIP","link":3}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[4],"slot_index":0}],"properties":{},"widgets_values":["masterpiece best quality girl"]},{"id":5,"type":"EmptyLatentImage","pos":[473,609],"size":{"0":315,"1":106},"flags":{},"order":1,"mode":0,"outputs":[{"name":"LATENT","type":"LATENT","links":[2],"slot_index":0}],"properties":{},"widgets_values":[512,512,1]},{"id":3,"type":"KSampler","pos":[863,186],"size":{"0":315,"1":262},"flags":{},"order":4,"mode":0,"inputs":[{"name":"model","type":"MODEL","link":1},{"name":"positive","type":"CONDITIONING","link":4},{"name":"negative","type":"CONDITIONING","link":6},{"name":"latent_image","type":"LATENT","link":2}],"outputs":[{"name":"LATENT","type":"LATENT","links":[7],"slot_index":0}],"properties":{},"widgets_values":[8566257,true,20,8,"euler","normal",1]},{"id":8,"type":"VAEDecode","pos":[1209,188],"size":{"0":210,"1":46},"flags":{},"order":5,"mode":0,"inputs":[{"name":"samples","type":"LATENT","link":7},{"name":"vae","type":"VAE","link":8}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[9],"slot_index":0}],"properties":{}},{"id":9,"type":"SaveImage","pos":[1451,189],"size":{"0":210,"1":26},"flags":{},"order":6,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":9}],"properties":{}},{"id":4,"type":"CheckpointLoader","pos":[26,474],"size":{"0":315,"1":122},"flags":{},"order":0,"mode":0,"outputs":[{"name":"MODEL","type":"MODEL","links":[1],"slot_index":0},{"name":"CLIP","type":"CLIP","links":[3,5],"slot_index":1},{"name":"VAE","type":"VAE","links":[8],"slot_index":2}],"properties":{},"widgets_values":["v1-inference.yaml","v1-5-pruned-emaonly.ckpt"]}],"links":[[1,4,0,3,0,"MODEL"],[2,5,0,3,3,"LATENT"],[3,4,1,6,0,"CLIP"],[4,6,0,3,1,"CONDITIONING"],[5,4,1,7,0,"CLIP"],[6,7,0,3,2,"CONDITIONING"],[7,3,0,8,0,"LATENT"],[8,4,2,8,1,"VAE"],[9,8,0,9,0,"IMAGE"]],"groups":[],"config":{},"extra":{},"version":0.4}
|
|
|
|
function loadGraphData(graph, graph_data)
|
|
{
|
|
graph.configure( graph_data);
|
|
for (let n in graph._nodes) {
|
|
n = graph._nodes[n];
|
|
s = n.computeSize();
|
|
s[0] = Math.max(n.size[0], s[0]);
|
|
s[1] = Math.max(n.size[1], s[1]);
|
|
n.size = s;
|
|
|
|
// If you break something in the backend and want to patch workflows in the frontend
|
|
// This is the place to do this
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (n.class_comfy == "KSampler" || n.class_comfy == "KSamplerAdvanced") {
|
|
if (wid.name == "sampler_name") {
|
|
if (wid.value.startsWith("sample_")) {
|
|
wid.value = wid.value.slice(7);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function afterLoadGraph()
|
|
{
|
|
let workflow = null;
|
|
try {
|
|
workflow = JSON.parse(localStorage.getItem("workflow"));
|
|
loadGraphData(graph, workflow);
|
|
} catch(err) {
|
|
}
|
|
|
|
if (!workflow) {
|
|
loadGraphData(graph, default_graph);
|
|
}
|
|
|
|
function saveGraph() {
|
|
localStorage.setItem("workflow", JSON.stringify(graph.serialize()));
|
|
}
|
|
|
|
setInterval(saveGraph, 1000);
|
|
|
|
}
|
|
|
|
function onObjectInfo(json) {
|
|
for (let key in json) {
|
|
function MyNode()
|
|
{
|
|
j = MyNode.__json_data;
|
|
inp = j['input']['required'];
|
|
this.class_comfy = MyNode.class_type_comfy;
|
|
this._widgets = []
|
|
min_height = 1;
|
|
min_width = 1;
|
|
|
|
for (let x in inp) {
|
|
let default_val = min_val = max_val = step_val = multiline = dynamic_prompt = undefined;
|
|
if (inp[x].length > 1) {
|
|
default_val = inp[x][1]['default'];
|
|
min_val = inp[x][1]['min'];
|
|
max_val = inp[x][1]['max'];
|
|
step_val = inp[x][1]['step'];
|
|
multiline = inp[x][1]['multiline'];
|
|
dynamic_prompt = inp[x][1]['dynamic_prompt'];
|
|
}
|
|
|
|
let type = inp[x][0];
|
|
if (Array.isArray(type)) {
|
|
w = this.addWidget("combo", x, type[0], function(v){}, { values: type } );
|
|
this._widgets += [w]
|
|
} else if (type == "INT") {
|
|
if (default_val == undefined) default_val = 0;
|
|
if (min_val == undefined) min_val = 0;
|
|
if (max_val == undefined) max_val = 2048;
|
|
if (step_val == undefined) step_val = 1;
|
|
w = this.addWidget("number", x, default_val, function(v){let s = this.options.step / 10;this.value = Math.round( v / s ) * s;}, { min: min_val, max: max_val, step: 10.0 * step_val} );
|
|
this._widgets += [w]
|
|
if (x == "seed" || x == "noise_seed") {
|
|
w1 = this.addWidget("toggle", "Random seed after every gen", true, function(v){}, { on: "enabled", off:"disabled"} );
|
|
w1.to_randomize = w;
|
|
this._widgets += [w1]
|
|
}
|
|
} else if (type == "FLOAT") {
|
|
if (default_val == undefined) default_val = 0;
|
|
if (min_val == undefined) min_val = 0;
|
|
if (max_val == undefined) max_val = 2048;
|
|
if (step_val == undefined) step_val = 0.5;
|
|
|
|
// if (min_val == 0.0 && max_val == 1.0) {
|
|
// w = this.slider = this.addWidget("slider", x, default_val, function(v){}, { min: min_val, max: max_val} );
|
|
// } else {
|
|
w = this.addWidget("number", x, default_val, function(v){}, { min: min_val, max: max_val, step: 10.0 * step_val} );
|
|
// }
|
|
this._widgets += [w]
|
|
} else if (type == "STRING") {
|
|
if (default_val == undefined) default_val = "";
|
|
if (multiline == undefined) multiline = false;
|
|
if (dynamic_prompt == undefined) dynamic_prompt = false;
|
|
|
|
if (multiline) {
|
|
var w = {
|
|
type: "customtext",
|
|
name: x,
|
|
dynamic_prompt: dynamic_prompt,
|
|
get value() { return this.input_div.value;},
|
|
set value(x) { this.input_div.value = x;},
|
|
callback: function(v){console.log(v);},
|
|
options: {},
|
|
draw: function(ctx, node, widget_width, y, H){
|
|
var show_text = canvas.ds.scale > 0.5;
|
|
// this.input_div.style.top = `${y}px`;
|
|
let t = ctx.getTransform();
|
|
let margin = 10;
|
|
let x_div = t.a * margin * 2 + t.e;
|
|
let y_div = t.d * (y + H) + t.f;
|
|
let width_div = (widget_width - margin * 2 - 3) * t.a;
|
|
let height_div = (this.parent.size[1] - (y + H) - 3)* t.d;
|
|
this.input_div.style.left = `${x_div}px`;
|
|
this.input_div.style.top = `${y_div}px`;
|
|
this.input_div.style.width = width_div;
|
|
this.input_div.style.height = height_div;
|
|
this.input_div.style.position = 'absolute';
|
|
this.input_div.style.zIndex = 1;
|
|
this.input_div.style.fontSize = t.d * 10.0;
|
|
|
|
if (show_text) {
|
|
this.input_div.hidden = false;
|
|
} else {
|
|
this.input_div.hidden = true;
|
|
}
|
|
|
|
ctx.save();
|
|
// ctx.fillText(String(this.value).substr(0,30), 0, y + H * 0.7);
|
|
ctx.restore();
|
|
},
|
|
};
|
|
w.input_div = document.createElement('textarea');
|
|
w.input_div.contentEditable = true;
|
|
w.input_div.className = "customtext_input";
|
|
w.input_div.style.overflow = 'hidden';
|
|
w.input_div.style.overflowY = 'auto';
|
|
w.input_div.style.padding = 2;
|
|
w.input_div.style.resize = 'none';
|
|
w.input_div.style.border = 'none';
|
|
w.input_div.value = default_val;
|
|
document.addEventListener('click', function(event) {
|
|
if (!w.input_div.contains(event.target)) {
|
|
w.input_div.blur();
|
|
}
|
|
});
|
|
w.parent = this;
|
|
min_height = Math.max(min_height, 200);
|
|
min_width = Math.max(min_width, 400);
|
|
ccc.parentNode.appendChild(w.input_div);
|
|
|
|
w = this.addCustomWidget(w);
|
|
canvas.onDrawBackground = function() {
|
|
for (let n in graph._nodes) {
|
|
n = graph._nodes[n];
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (Object.hasOwn(wid, 'input_div')) {
|
|
wid.input_div.style.left = -8000;
|
|
wid.input_div.style.position = 'absolute';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// w = this.addWidget("text", x, "", function(v){}, { multiline:true } );
|
|
console.log(w, this);
|
|
this._widgets += [w]
|
|
this.onRemoved = function() {
|
|
for (let y in this.widgets) {
|
|
if (this.widgets[y].input_div) {
|
|
this.widgets[y].input_div.remove();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
w = this.addWidget("text", x, default_val, function(v){}, { multiline:false } );
|
|
w.dynamic_prompt = dynamic_prompt;
|
|
this._widgets += [w];
|
|
}
|
|
} else {
|
|
this.addInput(x, type);
|
|
}
|
|
|
|
MyNode.prototype.getExtraMenuOptions = function(canvas, options) {
|
|
if(this.imgs) {
|
|
let img;
|
|
if(this.imageIndex != null) {
|
|
img = this.imgs[this.imageIndex];
|
|
} else if(this.overIndex != null) {
|
|
img = this.imgs[this.overIndex];
|
|
}
|
|
if(img) {
|
|
options.unshift({
|
|
content: "Open Image",
|
|
callback: () => window.open(img.src, "_blank")
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
MyNode.prototype.onDrawBackground = function(ctx) {
|
|
if(!this.flags.collapsed) {
|
|
const output = nodeOutputs[this.id + ""];
|
|
if(output && output.images) {
|
|
if(this.images !== output.images) {
|
|
this.images = output.images;
|
|
this.imgs = null;
|
|
this.imageIndex = null;
|
|
Promise.all(output.images.map(src => {
|
|
return new Promise(r => {
|
|
const img = new Image();
|
|
img.onload = () => r(img);
|
|
img.onerror = () => r(null);
|
|
img.src = "/view/" + src;
|
|
});
|
|
})).then(imgs => {
|
|
if(this.images === output.images) {
|
|
this.imgs = imgs.filter(Boolean);
|
|
if(this.size[1] < 100) {
|
|
this.size[1] = 250;
|
|
}
|
|
graph.setDirtyCanvas(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
if(this.imgs) {
|
|
const canvas = graph.list_of_graphcanvas[0];
|
|
const mouse = canvas.graph_mouse;
|
|
if(!canvas.pointer_is_down && this.pointerDown) {
|
|
if(mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
|
|
this.imageIndex = this.pointerDown.index;
|
|
}
|
|
this.pointerDown = null;
|
|
}
|
|
|
|
let w = this.imgs[0].naturalWidth;
|
|
let h = this.imgs[0].naturalHeight;
|
|
let imageIndex = this.imageIndex;
|
|
const numImages = this.imgs.length;
|
|
if(numImages === 1 && !imageIndex) {
|
|
this.imageIndex = imageIndex = 0;
|
|
}
|
|
let shiftY = this.type === "SaveImage" ? 55 : 0;
|
|
let dw = this.size[0];
|
|
let dh = this.size[1];
|
|
dh -= shiftY;
|
|
|
|
if(imageIndex == null) {
|
|
let best = 0;
|
|
let cellWidth;
|
|
let cellHeight;
|
|
let cols = 0;
|
|
let shiftX = 0;
|
|
for (let c = 1; c <= numImages; c++) {
|
|
const rows = Math.ceil(numImages / c);
|
|
const cW = dw / c;
|
|
const cH = dh / rows;
|
|
const scaleX = cW / w;
|
|
const scaleY = cH / h;
|
|
|
|
const scale = Math.min(scaleX, scaleY, 1);
|
|
const imageW = w * scale;
|
|
const imageH = h * scale;
|
|
const area = imageW * imageH * numImages;
|
|
|
|
if(area > best) {
|
|
best = area;
|
|
cellWidth = imageW;
|
|
cellHeight = imageH;
|
|
cols = c;
|
|
shiftX = c * ((cW - imageW) / 2);
|
|
}
|
|
}
|
|
|
|
let anyHovered = false;
|
|
this.imageRects = [];
|
|
for (let i = 0; i < numImages; i++) {
|
|
const img = this.imgs[i];
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
const x = col * cellWidth + shiftX;
|
|
const y = row * cellHeight + shiftY;
|
|
if(!anyHovered) {
|
|
anyHovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], cellWidth, cellHeight);
|
|
if(anyHovered) {
|
|
this.overIndex = i;
|
|
let value = 110;
|
|
if(canvas.pointer_is_down) {
|
|
if(!this.pointerDown || this.pointerDown.index !== i) {
|
|
this.pointerDown = {index: i, pos: [...mouse]};
|
|
}
|
|
value = 125;
|
|
}
|
|
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
|
|
canvas.canvas.style.cursor = "pointer";
|
|
}
|
|
}
|
|
this.imageRects.push([x, y, cellWidth, cellHeight]);
|
|
ctx.drawImage(img, x, y, cellWidth, cellHeight);
|
|
ctx.filter = "none";
|
|
}
|
|
|
|
if(!anyHovered) {
|
|
this.pointerDown = null;
|
|
this.overIndex = null;
|
|
}
|
|
} else {
|
|
// Draw individual
|
|
const scaleX = dw / w;
|
|
const scaleY = dh / h;
|
|
const scale = Math.min(scaleX, scaleY, 1);
|
|
|
|
w *= scale;
|
|
h *= scale;
|
|
|
|
let x = (dw - w) / 2;
|
|
let y = (dh - h) / 2 + shiftY;
|
|
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
|
|
|
|
const drawButton = (x, y, sz, text) => {
|
|
const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz);
|
|
let fill = "#333";
|
|
let textFill = "#fff";
|
|
let isClicking = false;
|
|
if(hovered) {
|
|
canvas.canvas.style.cursor = "pointer";
|
|
if(canvas.pointer_is_down) {
|
|
fill = "#1e90ff";
|
|
isClicking = true;
|
|
} else {
|
|
fill = "#eee";
|
|
textFill = "#000";
|
|
}
|
|
} else {
|
|
this.pointerWasDown = null;
|
|
}
|
|
|
|
ctx.fillStyle = fill;
|
|
ctx.beginPath();
|
|
ctx.roundRect(x, y, sz, sz, [4]);
|
|
ctx.fill();
|
|
ctx.fillStyle = textFill;
|
|
ctx.font = "12px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(text, x + 15, y + 20);
|
|
|
|
return isClicking;
|
|
}
|
|
|
|
if(numImages > 1) {
|
|
if(drawButton(x + w - 35, y + h - 35, 30, `${this.imageIndex+1}/${numImages}`)) {
|
|
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
|
|
if(!this.pointerDown || !this.pointerDown.index === i) {
|
|
this.pointerDown = {index: i, pos: [...mouse]};
|
|
}
|
|
}
|
|
|
|
if(drawButton(x + w - 35, y + 5, 30, `x`)) {
|
|
if(!this.pointerDown || !this.pointerDown.index === null) {
|
|
this.pointerDown = {index: null, pos: [...mouse]};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
out = j['output'];
|
|
for (let x in out) {
|
|
this.addOutput(out[x], out[x]);
|
|
}
|
|
s = this.computeSize();
|
|
s[0] *= 1.5;
|
|
s[0] = Math.max(min_width, s[0]);
|
|
s[1] = Math.max(min_height, s[1]);
|
|
this.size = s;
|
|
this.serialize_widgets = true;
|
|
}
|
|
MyNode.title = json[key]['name'];
|
|
MyNode.class_type_comfy = json[key]['name'];
|
|
MyNode.__json_data = json[key]
|
|
|
|
LiteGraph.registerNodeType(key, MyNode);
|
|
MyNode.category = json[key]['category'];
|
|
};
|
|
|
|
afterLoadGraph();
|
|
// loadGraphData(graph, JSON.parse(base_txt2img_graph));
|
|
}
|
|
|
|
fetch("object_info", {cache: "no-store"})
|
|
.then(response => response.json())
|
|
.then(json => onObjectInfo(json));
|
|
|
|
|
|
//register in the system
|
|
graph.start();
|
|
// LiteGraph.registerNodeType("testing", MyAddNode);
|
|
|
|
|
|
graph.onNodeRemoved = function(n) {
|
|
for (let y in n.widgets) {
|
|
if (n.widgets[y].input_div) {
|
|
n.widgets[y].input_div.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
function graphToPrompt() {
|
|
let s = graph.serialize();
|
|
let output = {};
|
|
// console.log(s['nodes']);
|
|
nodes = s['nodes']
|
|
|
|
for (let x in nodes) {
|
|
let n = graph.getNodeById(nodes[x].id);
|
|
let input_ = {};
|
|
|
|
// dynamic prompts handling
|
|
if (n.widgets && n.widgets.length > 0) {
|
|
// find widgets declared as supporting dynamic prompting
|
|
var supportedWidgets = n.widgets.filter(e => e.dynamic_prompt === true);
|
|
if (supportedWidgets.length > 0) {
|
|
|
|
// resolve dynamic prompts for all widgets supporting it in this node
|
|
for (let i in supportedWidgets)
|
|
{
|
|
var widget = supportedWidgets[i];
|
|
|
|
// store the unresolved prompt to restore it after sending the resolved prompt to the backend
|
|
// use of .innerText which keep \n symbols in order to not break multilines support
|
|
widget.value_initial = widget.input_div.value;
|
|
|
|
// resolve the string
|
|
var prompt = widget.input_div.value;
|
|
while (prompt.replace("\\{", "").includes('{') && prompt.replace("\\}", "").includes('}')) {
|
|
const startIndex = prompt.replace("\\{", "00").indexOf('{');
|
|
const endIndex = prompt.replace("\\}", "00").indexOf('}');
|
|
|
|
const optionsString = prompt.substring(startIndex + 1, endIndex);
|
|
const options = optionsString.split('|');
|
|
|
|
const randomIndex = Math.floor(Math.random() * options.length);
|
|
const randomOption = options[randomIndex];
|
|
|
|
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
|
|
}
|
|
|
|
widget.value = prompt;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let y in n.widgets) {
|
|
if (!Object.hasOwn(n.widgets[y], 'to_randomize')) { //don't include "Random seed after every gen" in prompt.
|
|
if (n.widgets[y].dynamic_prompt && n.widgets[y].dynamic_prompt === true) {
|
|
input_[n.widgets[y].name] = n.widgets[y].value.replace("\\{", "{").replace("\\}", "}");
|
|
} else {
|
|
input_[n.widgets[y].name] = n.widgets[y].value;
|
|
}
|
|
}
|
|
}
|
|
for (let y in n.inputs) {
|
|
let parent_node = n.getInputNode(y);
|
|
if (parent_node) {
|
|
for (let z in parent_node.outputs) {
|
|
let c_nodes = parent_node.getOutputNodes(z);
|
|
// console.log(c_nodes, z);
|
|
if (c_nodes) {
|
|
for (let zz in c_nodes) {
|
|
if (c_nodes[zz].id == n.id && parent_node.outputs[z].links.includes(n.inputs[y].link)) {
|
|
input_[n.inputs[y].name] = [String(parent_node.id), parseInt(z)];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let node = {}
|
|
node['inputs'] = input_;
|
|
node['class_type'] = n.class_comfy;
|
|
// inputs = x['inputs']
|
|
// inputs['name'], inputs['id']
|
|
// console.log(x, n);
|
|
// console.log(node);
|
|
output[String(n.id)] = node;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function closeModal() {
|
|
var modal = document.getElementById("myErrorModal");
|
|
modal.style.display = "none";
|
|
}
|
|
|
|
function showModal(text) {
|
|
var modal = document.getElementById("myErrorModal");
|
|
var modalText = document.getElementById("modal-text");
|
|
modalText.innerHTML = text;
|
|
modal.style.display = "block";
|
|
var closeBtn = modal.getElementsByClassName("close")[0];
|
|
closeBtn.onclick = function(event) {closeModal();}
|
|
}
|
|
|
|
function promptPosted(data)
|
|
{
|
|
if (data.status != 200) {
|
|
data.text().then(dt => showModal(dt));
|
|
return;
|
|
}
|
|
|
|
let s = graph.serialize();
|
|
let output = {};
|
|
// console.log(s['nodes']);
|
|
nodes = s['nodes']
|
|
|
|
for (let x in nodes) {
|
|
let n = graph.getNodeById(nodes[x].id);
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (Object.hasOwn(wid, 'to_randomize')) {
|
|
if (wid.value) {
|
|
wid.to_randomize.value = Math.floor(Math.random() * 1125899906842624);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
canvas.draw(true, true);
|
|
loadQueueIfVisible();
|
|
}
|
|
|
|
function postPrompt(number) {
|
|
let prompt = graphToPrompt();
|
|
let full_data = {client_id: clientId, prompt: prompt, extra_data: {extra_pnginfo: {workflow: graph.serialize()}}};
|
|
if (number == -1) {
|
|
full_data.front = true;
|
|
} else
|
|
if (number != 0) {
|
|
full_data.number = number;
|
|
}
|
|
|
|
fetch('/prompt', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(full_data)
|
|
})
|
|
.then(data => promptPosted(data))
|
|
.catch(error => console.error(error))
|
|
|
|
// console.log(JSON.stringify(prompt));
|
|
// console.log(JSON.stringify(graph.serialize()));
|
|
|
|
// restore initial values replaced by dynamic prompting
|
|
for (let x in graph._nodes_by_id) {
|
|
let n = graph._nodes_by_id[x];
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (wid.dynamic_prompt && wid.dynamic_prompt === true)
|
|
wid.value = wid.value_initial;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function promptToGraph(prompt) {
|
|
for (let x in prompt) {
|
|
|
|
}
|
|
}
|
|
|
|
function prompt_file_load(file)
|
|
{
|
|
if (file.type === 'image/png') {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
// Get the PNG data as a Uint8Array
|
|
const pngData = new Uint8Array(event.target.result);
|
|
const dataView = new DataView(pngData.buffer);
|
|
|
|
// Check that the PNG signature is present
|
|
if (dataView.getUint32(0) !== 0x89504e47) {
|
|
console.error('Not a valid PNG file');
|
|
return;
|
|
}
|
|
|
|
// Start searching for chunks after the PNG signature
|
|
let offset = 8;
|
|
let txt_chunks = {}
|
|
// Loop through the chunks in the PNG file
|
|
while (offset < pngData.length) {
|
|
// Get the length of the chunk
|
|
const length = dataView.getUint32(offset);
|
|
// Get the chunk type
|
|
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
|
if (type === 'tEXt') {
|
|
// Get the keyword
|
|
let keyword_end = offset + 8;
|
|
while (pngData[keyword_end] !== 0) {
|
|
keyword_end++;
|
|
}
|
|
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
|
|
// Get the text
|
|
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
|
|
txt_chunks[keyword] = text;
|
|
}
|
|
|
|
// Get the next chunk
|
|
offset += 12 + length;
|
|
}
|
|
console.log(txt_chunks);
|
|
// console.log(JSON.parse(txt_chunks["prompt"]));
|
|
loadGraphData(graph, JSON.parse(txt_chunks["workflow"]));
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
|
var reader = new FileReader();
|
|
reader.onload = function() {
|
|
console.log(reader.result);
|
|
var jsonData = JSON.parse(reader.result);
|
|
loadGraphData(graph, jsonData);
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
}
|
|
|
|
// Get prompt from dropped PNG or json
|
|
document.addEventListener('drop', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const file = event.dataTransfer.files[0];
|
|
console.log(file.type);
|
|
prompt_file_load(file);
|
|
});
|
|
|
|
let runningNodeId = null;
|
|
let progress = null;
|
|
let clientId = null;
|
|
const orig = LGraphCanvas.prototype.drawNodeShape;
|
|
LGraphCanvas.prototype.drawNodeShape = function(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
|
|
const res = orig.apply(this, arguments);
|
|
|
|
if(node.id + "" === runningNodeId) {
|
|
const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = 0.8;
|
|
ctx.beginPath();
|
|
if( shape == LiteGraph.BOX_SHAPE )
|
|
ctx.rect(-6,-6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT );
|
|
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) )
|
|
ctx.roundRect(-6,-6 - LiteGraph.NODE_TITLE_HEIGHT, 12 +size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT , this.round_radius * 2);
|
|
else if (shape == LiteGraph.CARD_SHAPE)
|
|
ctx.roundRect(-6,-6 + LiteGraph.NODE_TITLE_HEIGHT, 12 +size[0]+1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT , this.round_radius * 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 = "#0f0"
|
|
ctx.stroke();
|
|
ctx.strokeStyle = fgcolor;
|
|
ctx.globalAlpha = 1;
|
|
|
|
if(progress) {
|
|
ctx.fillStyle = "green";
|
|
ctx.fillRect(0, 0, size[0] * (progress.value / progress.max), 6);
|
|
ctx.fillStyle = bgcolor;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function updateNodeProgress(v) {
|
|
progress = v;
|
|
graph.setDirtyCanvas(true, false);
|
|
}
|
|
|
|
function setRunningNode(id) {
|
|
progress = null;
|
|
runningNodeId = id;
|
|
graph.setDirtyCanvas(true, false);
|
|
}
|
|
|
|
(() => {
|
|
function updateStatus(data) {
|
|
document.getElementById("queuesize").innerHTML = "Queue size: " + (data ? data.exec_info.queue_remaining : "ERR");
|
|
}
|
|
|
|
//fix for colab and other things that don't support websockets.
|
|
function manually_fetch_queue() {
|
|
fetch('/prompt')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
updateStatus(data);
|
|
}).catch((response) => {updateStatus(null)});
|
|
}
|
|
|
|
let ws;
|
|
function createSocket(isReconnect) {
|
|
if(ws) return;
|
|
|
|
let opened = false;
|
|
ws = new WebSocket(`ws${window.location.protocol === "https:"? "s" : ""}://${location.host}/ws`);
|
|
|
|
ws.addEventListener("open", () => {
|
|
opened = true;
|
|
if(isReconnect) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
ws.addEventListener("error", () => {
|
|
if(ws) ws.close();
|
|
manually_fetch_queue();
|
|
});
|
|
|
|
ws.addEventListener("close", () => {
|
|
setTimeout(() => {
|
|
ws = null;
|
|
createSocket(true);
|
|
}, 300);
|
|
if(opened) {
|
|
updateStatus(null);
|
|
showModal("Reconnecting...");
|
|
}
|
|
});
|
|
|
|
ws.addEventListener("message", (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data);
|
|
switch(msg.type) {
|
|
case "status":
|
|
if(msg.data.sid) {
|
|
clientId = msg.data.sid;
|
|
}
|
|
updateStatus(msg.data.status);
|
|
break;
|
|
case "progress":
|
|
updateNodeProgress(msg.data)
|
|
break;
|
|
case "executing":
|
|
setRunningNode(msg.data.node);
|
|
break;
|
|
case "executed":
|
|
nodeOutputs[msg.data.node] = msg.data.output;
|
|
break;
|
|
default:
|
|
throw new Error("Unknown message type")
|
|
}
|
|
} catch (error) {
|
|
console.warn("Unhandled message:", event.data)
|
|
}
|
|
});
|
|
}
|
|
createSocket();
|
|
})();
|
|
|
|
|
|
function clearGraph() {
|
|
graph.clear();
|
|
}
|
|
|
|
function loadTxt2Img() {
|
|
loadGraphData(graph, default_graph);
|
|
}
|
|
|
|
function saveGraph() {
|
|
var json = JSON.stringify(graph.serialize()); // convert the data to a JSON string
|
|
var blob = new Blob([json], {type: "application/json"});
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement("a");
|
|
a.style = "display: none";
|
|
a.href = url;
|
|
a.download = "workflow.json";
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
setTimeout(function() {
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
}, 0);
|
|
}
|
|
|
|
var input = document.createElement("input");
|
|
input.setAttribute("type", "file");
|
|
input.setAttribute("accept", ".json,image/png");
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
|
|
input.addEventListener('change', function() {
|
|
var file = input.files[0];
|
|
prompt_file_load(file);
|
|
|
|
});
|
|
|
|
function loadGraph() {
|
|
input.click();
|
|
}
|
|
|
|
document.addEventListener('paste', e=>{
|
|
let data = (e.clipboardData || window.clipboardData).getData('text/plain');
|
|
console.log(data);
|
|
|
|
try {
|
|
data = data.slice(data.indexOf('{'));
|
|
j = JSON.parse(data);
|
|
} catch(err) {
|
|
data = data.slice(data.indexOf('workflow\n'));
|
|
data = data.slice(data.indexOf('{'));
|
|
j = JSON.parse(data);
|
|
}
|
|
|
|
|
|
if (Object.hasOwn(j, 'version') && Object.hasOwn(j, 'nodes') && Object.hasOwn(j, 'extra')) {
|
|
loadGraphData(graph, j);
|
|
}
|
|
});
|
|
|
|
function deleteQueueElement(type, delete_id, then) {
|
|
fetch('/' + type, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({"delete":[delete_id]})
|
|
})
|
|
.then(data => {
|
|
console.log(data);
|
|
then();
|
|
})
|
|
.catch(error => console.error(error))
|
|
}
|
|
|
|
function loadQueue() {
|
|
loadItems("queue")
|
|
}
|
|
function loadHistory() {
|
|
loadItems("history")
|
|
}
|
|
function loadItems(type) {
|
|
fetch('/' + type)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
var queue_div = document.getElementById(type + "button-content");
|
|
queue_div.style.display = 'block';
|
|
var see_queue_button = document.getElementById("see" + type + "button");
|
|
let old_w = see_queue_button.style.width;
|
|
see_queue_button.innerHTML = "Close";
|
|
|
|
let runningcontents;
|
|
if(type === "queue") {
|
|
runningcontents = document.getElementById("runningcontents");
|
|
runningcontents.innerHTML = '';
|
|
}
|
|
let queuecontents = document.getElementById(type + "contents");
|
|
queuecontents.innerHTML = '';
|
|
function append_to_list(list_element, append_to_element, append_delete, state) {
|
|
let number = list_element[0];
|
|
let id = list_element[1];
|
|
let prompt = list_element[2];
|
|
let workflow = list_element[3].extra_pnginfo.workflow;
|
|
let a = document.createElement("a");
|
|
a.innerHTML = number + ": ";
|
|
append_to_element.appendChild(a);
|
|
let button = document.createElement("button");
|
|
button.innerHTML = "Load";
|
|
button.style.fontSize = "10px";
|
|
button.workflow = workflow;
|
|
button.onclick = function(event) {
|
|
loadGraphData(graph, event.target.workflow);
|
|
if(state) {
|
|
nodeOutputs = state;
|
|
}
|
|
};
|
|
|
|
append_to_element.appendChild(button);
|
|
if (append_delete) {
|
|
let button = document.createElement("button");
|
|
button.innerHTML = "Delete";
|
|
button.style.fontSize = "10px";
|
|
button.delete_id = id;
|
|
button.onclick = function(event) {
|
|
deleteQueueElement(type, event.target.delete_id, () => loadItems(type));
|
|
};
|
|
append_to_element.appendChild(button);
|
|
}
|
|
append_to_element.appendChild(document.createElement("br"));
|
|
}
|
|
|
|
if(runningcontents) {
|
|
for (let x in data.queue_running) {
|
|
append_to_list(data.queue_running[x], runningcontents, false);
|
|
}
|
|
}
|
|
|
|
let items;
|
|
if(type === "queue") {
|
|
items = data.queue_pending;
|
|
} else {
|
|
items = Object.values(data);
|
|
}
|
|
items.sort((a, b) => a[0] - b[0]);
|
|
for (let i of items) {
|
|
append_to_list(type === "queue" ? i : i.prompt, queuecontents, true, i.outputs);
|
|
}
|
|
}).catch((response) => {console.log(response)});
|
|
}
|
|
|
|
function loadQueueIfVisible()
|
|
{
|
|
var queue_div = document.getElementById("queuebutton-content");
|
|
if (queue_div.style.display == 'block') {
|
|
loadQueue();
|
|
}
|
|
}
|
|
|
|
function seeItems(type) {
|
|
var queue_div = document.getElementById(type + "button-content");
|
|
if (queue_div.style.display == 'block') {
|
|
closeItems(type)
|
|
} else {
|
|
loadItems(type);
|
|
}
|
|
}
|
|
|
|
function seeQueue() {
|
|
closeItems("history")
|
|
seeItems("queue")
|
|
}
|
|
|
|
function seeHistory() {
|
|
closeItems("queue")
|
|
seeItems("history")
|
|
}
|
|
|
|
function closeItems(type) {
|
|
var queue_div = document.getElementById(type + "button-content");
|
|
queue_div.style.display = 'none';
|
|
var see_queue_button = document.getElementById("see" + type + "button");
|
|
see_queue_button.innerHTML = "See " + type[0].toUpperCase() + type.substr(1)
|
|
}
|
|
|
|
function clearItems(type) {
|
|
fetch('/' + type, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({"clear":true})
|
|
}).then(data => {
|
|
loadItems(type);
|
|
})
|
|
.catch(error => console.error(error));
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<span id="menu" style="font-size: 15px;position: absolute; top: 50%; right: 0%; background-color: white; text-align: center; z-index: 100;width:170px">
|
|
<span id="queuesize">Queue size: X</span><br>
|
|
<button style="font-size: 20px;width: 100%;" id="queuebutton" onclick="postPrompt(0)">Queue Prompt</button><br>
|
|
<span style="left: 0%;">
|
|
<button style="font-size: 10px;" id="queuebutton" onclick="postPrompt(-1)">Queue Front</button>
|
|
<button style="font-size: 10px; width: 50%;" id="seequeuebutton" onclick="seeQueue()">See Queue</button>
|
|
<button style="font-size: 10px; width: 50%;" id="seehistorybutton" onclick="seeHistory()">See History</button>
|
|
<br>
|
|
</span>
|
|
<div id="queuebutton-content" style="background-color: #e1e1e1;min-width: 160px;display: none;z-index: 101;">
|
|
<span style="width:100%;padding: 3px;display:inline-block;">Running:</span>
|
|
<div id="runningcontents" style="background-color: #d0d0d0; padding: 5px;">
|
|
<a>1</a>
|
|
<button style="font-size: 10px;">Load</button>
|
|
<br>
|
|
</div>
|
|
<span style="left: 0%;padding: 3px;display:inline-block;">Queued:</span>
|
|
<div id="queuecontents" style="overflow-y: scroll;height: 100px;background-color: #d0d0d0;padding: 5px;">
|
|
<a>1</a>
|
|
<button style="font-size: 10px;">Load</button>
|
|
<button style="font-size: 10px;">Delete</button>
|
|
<br>
|
|
<br>
|
|
</div>
|
|
<span style="padding: 5px;display:inline-block;">
|
|
<button style="font-size: 12px;" onclick="clearItems('queue')">Clear Queue</button>
|
|
<button style="font-size: 12px;" onclick="loadQueue()">Refresh</button>
|
|
</span>
|
|
</div>
|
|
|
|
<div id="historybutton-content" style="background-color: #e1e1e1;min-width: 160px;display: none;z-index: 101;">
|
|
<span style="width:100%;padding: 3px;display:inline-block;">History:</span>
|
|
<div id="historycontents" style="overflow-y: scroll;height: 100px;background-color: #d0d0d0;padding: 5px;">
|
|
</div>
|
|
<span style="padding: 5px;display:inline-block;">
|
|
<button style="font-size: 12px;" onclick="clearItems('history')">Clear History</button>
|
|
<button style="font-size: 12px;" onclick="loadHistory()">Refresh</button>
|
|
</span>
|
|
</div>
|
|
<br>
|
|
<button style="font-size: 20px;" onclick="saveGraph()">Save</button><br>
|
|
<button style="font-size: 20px;" onclick="loadGraph()">Load</button>
|
|
<br>
|
|
<button style="font-size: 20px;" onclick="clearGraph()">Clear</button><br>
|
|
<button style="font-size: 20px;" onclick="loadTxt2Img()">Load Default</button><br>
|
|
</span>
|
|
</body>
|
|
</html>
|