mirror of
https://github.com/Stability-AI/StableStudio.git
synced 2026-07-13 19:24:30 +08:00
settings system & movable comfy path
This commit is contained in:
+14
-1
@@ -85,6 +85,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-upload",
|
||||
"tiny_http",
|
||||
"zip 0.6.6",
|
||||
@@ -3157,10 +3158,22 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c3a6dab24ccbaeada2d15caaf79e97064730e54f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-upload"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#0863f800b81925884a6a37c042f3d92d433d4a37"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#c3a6dab24ccbaeada2d15caaf79e97064730e54f"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
|
||||
@@ -19,6 +19,7 @@ serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.4.1", features = [ "os-all", "fs-all", "path-all", "process-command-api", "devtools"] }
|
||||
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
zip = "0.6.6"
|
||||
zip-extensions = "0.6.1"
|
||||
reqwest = { version = "0.11.6", features = ["blocking"] }
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
export const defaultGraph = {
|
||||
last_node_id: 28,
|
||||
last_link_id: 69,
|
||||
nodes: [
|
||||
{
|
||||
id: 9,
|
||||
type: "SaveImage",
|
||||
pos: [730, 362],
|
||||
size: {
|
||||
0: 404.3651428222656,
|
||||
1: 451.4970703125,
|
||||
},
|
||||
flags: {},
|
||||
order: 7,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
name: "images",
|
||||
type: "IMAGE",
|
||||
link: 9,
|
||||
},
|
||||
],
|
||||
properties: {},
|
||||
widgets_values: ["ComfyUI"],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "KSampler",
|
||||
pos: [374, 555],
|
||||
size: {
|
||||
0: 315,
|
||||
1: 262,
|
||||
},
|
||||
flags: {},
|
||||
order: 5,
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "cfg",
|
||||
type: "FLOAT",
|
||||
link: 62,
|
||||
widget: {
|
||||
name: "cfg",
|
||||
config: [
|
||||
"FLOAT",
|
||||
{
|
||||
default: 8,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 5,
|
||||
},
|
||||
{
|
||||
name: "seed",
|
||||
type: "INT",
|
||||
link: 67,
|
||||
widget: {
|
||||
name: "seed",
|
||||
config: [
|
||||
"INT",
|
||||
{
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 18446744073709552000,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 6,
|
||||
},
|
||||
{
|
||||
name: "steps",
|
||||
type: "INT",
|
||||
link: 68,
|
||||
widget: {
|
||||
name: "steps",
|
||||
config: [
|
||||
"INT",
|
||||
{
|
||||
default: 20,
|
||||
min: 1,
|
||||
max: 10000,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 7,
|
||||
},
|
||||
{
|
||||
name: "sampler_name",
|
||||
type: "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
|
||||
link: 61,
|
||||
widget: {
|
||||
name: "sampler_name",
|
||||
config: [
|
||||
[
|
||||
"euler",
|
||||
"euler_ancestral",
|
||||
"heun",
|
||||
"dpm_2",
|
||||
"dpm_2_ancestral",
|
||||
"lms",
|
||||
"dpm_fast",
|
||||
"dpm_adaptive",
|
||||
"dpmpp_2s_ancestral",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_sde_gpu",
|
||||
"dpmpp_2m",
|
||||
"dpmpp_2m_sde",
|
||||
"dpmpp_2m_sde_gpu",
|
||||
"ddim",
|
||||
"uni_pc",
|
||||
"uni_pc_bh2",
|
||||
],
|
||||
],
|
||||
},
|
||||
slot_index: 7,
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "LATENT",
|
||||
type: "LATENT",
|
||||
links: [7],
|
||||
slot_index: 0,
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
"Node name for S&R": "KSampler",
|
||||
},
|
||||
widgets_values: [
|
||||
983092648072402,
|
||||
"randomize",
|
||||
50,
|
||||
7,
|
||||
"dpmpp_sde",
|
||||
"normal",
|
||||
1,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: "CLIPTextEncode",
|
||||
pos: [22, 503],
|
||||
size: {
|
||||
0: 210,
|
||||
1: 54,
|
||||
},
|
||||
flags: {
|
||||
collapsed: false,
|
||||
},
|
||||
order: 3,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
name: "clip",
|
||||
type: "CLIP",
|
||||
link: 3,
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
type: "STRING",
|
||||
link: 64,
|
||||
widget: {
|
||||
name: "text",
|
||||
config: [
|
||||
"STRING",
|
||||
{
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 1,
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "CONDITIONING",
|
||||
type: "CONDITIONING",
|
||||
links: [4],
|
||||
slot_index: 0,
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
},
|
||||
widgets_values: [
|
||||
"Surreal carnival scene with bright lights, strange creatures, and a full moon, dreamlike, vibrant, stylized, surreal, high detail",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "CheckpointLoaderSimple",
|
||||
pos: [23, 355],
|
||||
size: {
|
||||
0: 315,
|
||||
1: 98,
|
||||
},
|
||||
flags: {
|
||||
collapsed: false,
|
||||
},
|
||||
order: 2,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
name: "ckpt_name",
|
||||
type: "v2-1_768-ema-pruned.safetensors",
|
||||
link: 60,
|
||||
widget: {
|
||||
name: "ckpt_name",
|
||||
config: [["v2-1_768-ema-pruned.safetensors"]],
|
||||
},
|
||||
},
|
||||
],
|
||||
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: {
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
},
|
||||
widgets_values: ["v2-1_768-ema-pruned.safetensors"],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: "CLIPTextEncode",
|
||||
pos: [29, 606],
|
||||
size: {
|
||||
0: 210,
|
||||
1: 54,
|
||||
},
|
||||
flags: {
|
||||
collapsed: false,
|
||||
},
|
||||
order: 4,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
name: "clip",
|
||||
type: "CLIP",
|
||||
link: 5,
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
type: "STRING",
|
||||
link: 63,
|
||||
widget: {
|
||||
name: "text",
|
||||
config: [
|
||||
"STRING",
|
||||
{
|
||||
multiline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 1,
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "CONDITIONING",
|
||||
type: "CONDITIONING",
|
||||
links: [6],
|
||||
slot_index: 0,
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
"Node name for S&R": "CLIPTextEncode",
|
||||
},
|
||||
widgets_values: [""],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: "EmptyLatentImage",
|
||||
pos: [32, 705],
|
||||
size: {
|
||||
0: 315,
|
||||
1: 106,
|
||||
},
|
||||
flags: {},
|
||||
order: 1,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
name: "batch_size",
|
||||
type: "INT",
|
||||
link: 69,
|
||||
widget: {
|
||||
name: "batch_size",
|
||||
config: [
|
||||
"INT",
|
||||
{
|
||||
default: 1,
|
||||
min: 1,
|
||||
max: 64,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "width",
|
||||
type: "INT",
|
||||
link: 66,
|
||||
widget: {
|
||||
name: "width",
|
||||
config: [
|
||||
"INT",
|
||||
{
|
||||
default: 512,
|
||||
min: 64,
|
||||
max: 8192,
|
||||
step: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 1,
|
||||
},
|
||||
{
|
||||
name: "height",
|
||||
type: "INT",
|
||||
link: 65,
|
||||
widget: {
|
||||
name: "height",
|
||||
config: [
|
||||
"INT",
|
||||
{
|
||||
default: 512,
|
||||
min: 64,
|
||||
max: 8192,
|
||||
step: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
slot_index: 2,
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "LATENT",
|
||||
type: "LATENT",
|
||||
links: [2],
|
||||
slot_index: 0,
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
"Node name for S&R": "EmptyLatentImage",
|
||||
},
|
||||
widgets_values: [768, 768, 4],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: "VAEDecode",
|
||||
pos: [428, 417],
|
||||
size: {
|
||||
0: 210,
|
||||
1: 46,
|
||||
},
|
||||
flags: {},
|
||||
order: 6,
|
||||
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: {
|
||||
"Node name for S&R": "VAEDecode",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
type: "StableStudioNode",
|
||||
pos: [-311, 506],
|
||||
size: {
|
||||
0: 151.1999969482422,
|
||||
1: 206,
|
||||
},
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
outputs: [
|
||||
{
|
||||
name: "batch_size",
|
||||
type: "INT",
|
||||
links: [69],
|
||||
slot_index: 0,
|
||||
},
|
||||
{
|
||||
name: "steps",
|
||||
type: "INT",
|
||||
links: [68],
|
||||
slot_index: 1,
|
||||
},
|
||||
{
|
||||
name: "seed",
|
||||
type: "INT",
|
||||
links: [67],
|
||||
slot_index: 2,
|
||||
},
|
||||
{
|
||||
name: "width",
|
||||
type: "INT",
|
||||
links: [66],
|
||||
slot_index: 3,
|
||||
},
|
||||
{
|
||||
name: "height",
|
||||
type: "INT",
|
||||
links: [65],
|
||||
slot_index: 4,
|
||||
},
|
||||
{
|
||||
name: "positive_prompt",
|
||||
type: "STRING",
|
||||
links: [64],
|
||||
slot_index: 5,
|
||||
},
|
||||
{
|
||||
name: "negative_prompt",
|
||||
type: "STRING",
|
||||
links: [63],
|
||||
slot_index: 6,
|
||||
},
|
||||
{
|
||||
name: "cfg",
|
||||
type: "FLOAT",
|
||||
links: [62],
|
||||
slot_index: 7,
|
||||
},
|
||||
{
|
||||
name: "sampler_name",
|
||||
type: "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
|
||||
links: [61],
|
||||
slot_index: 8,
|
||||
},
|
||||
{
|
||||
name: "ckpt_name",
|
||||
type: "v2-1_768-ema-pruned.safetensors",
|
||||
links: [60],
|
||||
slot_index: 9,
|
||||
},
|
||||
],
|
||||
properties: {},
|
||||
color: "#323",
|
||||
bgcolor: "#535",
|
||||
},
|
||||
],
|
||||
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"],
|
||||
[60, 28, 9, 4, 0, "v2-1_768-ema-pruned.safetensors"],
|
||||
[
|
||||
61,
|
||||
28,
|
||||
8,
|
||||
3,
|
||||
7,
|
||||
"euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2",
|
||||
],
|
||||
[62, 28, 7, 3, 4, "FLOAT"],
|
||||
[63, 28, 6, 7, 1, "STRING"],
|
||||
[64, 28, 5, 6, 1, "STRING"],
|
||||
[65, 28, 4, 5, 2, "INT"],
|
||||
[66, 28, 3, 5, 1, "INT"],
|
||||
[67, 28, 2, 3, 5, "INT"],
|
||||
[68, 28, 1, 3, 6, "INT"],
|
||||
[69, 28, 0, 5, 0, "INT"],
|
||||
],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4,
|
||||
};
|
||||
@@ -1,286 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
import { app } from "../scripts/app.js";
|
||||
|
||||
function getWidgetType(config) {
|
||||
// Special handling for COMBO so we restrict links based on the entries
|
||||
let type = config[0];
|
||||
let linkType = type;
|
||||
if (type instanceof Array) {
|
||||
type = "COMBO";
|
||||
linkType = linkType.join(",");
|
||||
}
|
||||
return { type, linkType };
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "StableStudio.Inputs",
|
||||
registerCustomNodes() {
|
||||
class StableStudioNode {
|
||||
color = LGraphCanvas.node_colors.purple.color;
|
||||
bgcolor = LGraphCanvas.node_colors.purple.bgcolor;
|
||||
constructor() {
|
||||
this.addOutput("batch_size", "INT");
|
||||
this.addOutput("steps", "INT");
|
||||
this.addOutput("seed", "INT");
|
||||
this.addOutput("width", "INT");
|
||||
this.addOutput("height", "INT");
|
||||
this.addOutput("positive_prompt", "STRING");
|
||||
this.addOutput("negative_prompt", "STRING");
|
||||
this.addOutput("cfg", "FLOAT");
|
||||
this.addOutput(
|
||||
"sampler_name",
|
||||
"euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,ddim,uni_pc,uni_pc_bh2"
|
||||
);
|
||||
this.addOutput("ckpt_name", null);
|
||||
this.serialize_widgets = false;
|
||||
this.isVirtualNode = true;
|
||||
|
||||
this.updateLoop = setInterval(async () => {
|
||||
const resp = await fetch("/object_info", { cache: "no-cache" });
|
||||
const data = await resp.json();
|
||||
if (data) {
|
||||
const models =
|
||||
data.CheckpointLoader?.input?.required?.ckpt_name?.[0] ?? [];
|
||||
const samplers =
|
||||
data.KSampler?.input?.required?.sampler_name?.[0] ?? [];
|
||||
|
||||
if (!this.stableValues.ckpt_name) {
|
||||
this.stableValues.ckpt_name = models[0];
|
||||
}
|
||||
if (!this.stableValues.sampler_name) {
|
||||
this.stableValues.sampler_name = samplers[0];
|
||||
}
|
||||
|
||||
this.outputs[8].type = samplers.join(",");
|
||||
this.outputs[9].type = models.join(",");
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
this.stableValues = {
|
||||
batch_size: 4,
|
||||
steps: 50,
|
||||
seed: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
positive_prompt: "",
|
||||
negative_prompt: "",
|
||||
cfg: 7,
|
||||
sampler_name: "",
|
||||
ckpt_name: "",
|
||||
};
|
||||
}
|
||||
|
||||
applyToGraph() {
|
||||
if (!this.outputs.some((o) => o.links?.length)) return;
|
||||
|
||||
function get_links(node) {
|
||||
let links = [];
|
||||
for (const o of node.outputs) {
|
||||
if (!o.links?.length) continue;
|
||||
for (const l of o.links) {
|
||||
const linkInfo = app.graph.links[l];
|
||||
const n = node.graph.getNodeById(linkInfo.target_id);
|
||||
if (n.type == "Reroute") {
|
||||
links = links.concat(get_links(n));
|
||||
} else {
|
||||
links.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
let links = get_links(this);
|
||||
// For each output link copy our value over the original widget value
|
||||
for (const l of links) {
|
||||
const linkInfo = app.graph.links[l];
|
||||
|
||||
const node = this.graph.getNodeById(linkInfo.target_id);
|
||||
const input = node.inputs[linkInfo.target_slot];
|
||||
const widgetName = input.widget.name;
|
||||
if (widgetName) {
|
||||
const widget = node.widgets.find((w) => w.name === widgetName);
|
||||
if (widget) {
|
||||
let stableValue = this.outputs.find(
|
||||
(o) => o.slot_index === linkInfo.origin_slot
|
||||
)?.name;
|
||||
|
||||
if (!stableValue) {
|
||||
// try to match with name
|
||||
stableValue = this.outputs.find(
|
||||
(o) => o.name === input.name
|
||||
)?.name;
|
||||
}
|
||||
|
||||
if (!stableValue) continue;
|
||||
widget.value = this.stableValues[stableValue] ?? widget.value;
|
||||
if (widget.callback) {
|
||||
widget.callback(
|
||||
widget.value,
|
||||
app.canvas,
|
||||
node,
|
||||
app.canvas.graph_mouse,
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConnectionsChange(_, index, connected) {
|
||||
console.log("onConnectionsChange", index, connected);
|
||||
// if (connected) {
|
||||
// if (this.outputs[0].links?.length) {
|
||||
// if (!this.widgets?.length) {
|
||||
// this.#onFirstConnection();
|
||||
// }
|
||||
// if (!this.widgets?.length && this.outputs[0].widget) {
|
||||
// // On first load it often cant recreate the widget as the other node doesnt exist yet
|
||||
// // Manually recreate it from the output info
|
||||
// this.#createWidget(this.outputs[0].widget.config);
|
||||
// }
|
||||
// }
|
||||
// } else if (!this.outputs[0].links?.length) {
|
||||
// this.#onLastDisconnect();
|
||||
// }
|
||||
}
|
||||
|
||||
onConnectOutput(slot, type, input) {
|
||||
// Fires before the link is made allowing us to reject it if it isn't valid
|
||||
// No widget, we cant connect
|
||||
// if (!input.widget) {
|
||||
// if (!(input.type in ComfyWidgets)) return false;
|
||||
// }
|
||||
// if (this.outputs[slot].links?.length) {
|
||||
// return this.#isValidConnection(input);
|
||||
// }
|
||||
|
||||
// let a STRING connect to a ckpt_name
|
||||
if (
|
||||
this.outputs[slot].type === "STRING" &&
|
||||
input.name === "ckpt_name"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#onFirstConnection() {
|
||||
// First connection can fire before the graph is ready on initial load so random things can be missing
|
||||
const linkId = this.outputs[0].links[0];
|
||||
const link = this.graph.links[linkId];
|
||||
if (!link) return;
|
||||
const theirNode = this.graph.getNodeById(link.target_id);
|
||||
if (!theirNode || !theirNode.inputs) return;
|
||||
const input = theirNode.inputs[link.target_slot];
|
||||
if (!input) return;
|
||||
var _widget;
|
||||
if (!input.widget) {
|
||||
if (!(input.type in ComfyWidgets)) return;
|
||||
_widget = { name: input.name, config: [input.type, {}] }; //fake widget
|
||||
} else {
|
||||
_widget = input.widget;
|
||||
}
|
||||
const widget = _widget;
|
||||
const { type, linkType } = getWidgetType(widget.config);
|
||||
// Update our output to restrict to the widget type
|
||||
this.outputs[0].type = linkType;
|
||||
this.outputs[0].name = type;
|
||||
this.outputs[0].widget = widget;
|
||||
this.#createWidget(widget.config, theirNode, widget.name);
|
||||
}
|
||||
|
||||
#createWidget(inputData, node, widgetName) {
|
||||
let type = inputData[0];
|
||||
|
||||
if (type instanceof Array) {
|
||||
type = "COMBO";
|
||||
}
|
||||
|
||||
let widget;
|
||||
if (type in ComfyWidgets) {
|
||||
widget = (ComfyWidgets[type](this, "value", inputData, app) || {})
|
||||
.widget;
|
||||
} else {
|
||||
widget = this.addWidget(type, "value", null, () => {}, {});
|
||||
}
|
||||
|
||||
if (node?.widgets && widget) {
|
||||
const theirWidget = node.widgets.find((w) => w.name === widgetName);
|
||||
if (theirWidget) {
|
||||
widget.value = theirWidget.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.type === "number" || widget.type === "combo") {
|
||||
addValueControlWidget(this, widget, "fixed");
|
||||
}
|
||||
|
||||
// When our value changes, update other widgets to reflect our changes
|
||||
// e.g. so LoadImage shows correct image
|
||||
const callback = widget.callback;
|
||||
const self = this;
|
||||
widget.callback = function () {
|
||||
const r = callback ? callback.apply(this, arguments) : undefined;
|
||||
self.applyToGraph();
|
||||
return r;
|
||||
};
|
||||
|
||||
// Grow our node if required
|
||||
const sz = this.computeSize();
|
||||
if (this.size[0] < sz[0]) {
|
||||
this.size[0] = sz[0];
|
||||
}
|
||||
if (this.size[1] < sz[1]) {
|
||||
this.size[1] = sz[1];
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.onResize) {
|
||||
this.onResize(this.size);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#isValidConnection(input) {
|
||||
// Only allow connections where the configs match
|
||||
const config1 = this.outputs[0].widget.config;
|
||||
const config2 = input.widget.config;
|
||||
|
||||
if (config1[0] instanceof Array) {
|
||||
// These checks shouldnt actually be necessary as the types should match
|
||||
// but double checking doesn't hurt
|
||||
|
||||
// New input isnt a combo
|
||||
if (!(config2[0] instanceof Array)) return false;
|
||||
// New imput combo has a different size
|
||||
if (config1[0].length !== config2[0].length) return false;
|
||||
// New input combo has different elements
|
||||
if (config1[0].find((v, i) => config2[0][i] !== v)) return false;
|
||||
} else if (config1[0] !== config2[0]) {
|
||||
// Configs dont match
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const k in config1[1]) {
|
||||
if (k !== "default") {
|
||||
if (config1[1][k] !== config2[1][k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
"StableStudioNode",
|
||||
Object.assign(StableStudioNode, {
|
||||
title: "StableStudio Input",
|
||||
})
|
||||
);
|
||||
StableStudioNode.category = "stablestudio";
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use settings::Settings;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::sync::OnceLock;
|
||||
@@ -15,6 +16,7 @@ use tauri::{RunEvent, Window, WindowBuilder, WindowUrl};
|
||||
use tauri_plugin_upload;
|
||||
|
||||
mod server;
|
||||
mod settings;
|
||||
mod show_path;
|
||||
|
||||
static WINDOW: OnceLock<Window> = OnceLock::new();
|
||||
@@ -54,6 +56,23 @@ fn main() {
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let app_data_dir = app.path_resolver().app_data_dir().unwrap();
|
||||
let _ = std::fs::create_dir_all(&app_data_dir);
|
||||
|
||||
let mut store_location = app_data_dir.clone();
|
||||
store_location.push("settings.json");
|
||||
|
||||
let comfy_location = app_data_dir.clone();
|
||||
|
||||
settings::create_settings(
|
||||
store_location,
|
||||
Settings {
|
||||
comfyui_location: Box::new(comfy_location.to_str().unwrap().to_string()),
|
||||
comfyui_url: Box::new("http://localhost:5000".to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
_ = WINDOW.set(window);
|
||||
|
||||
Ok(())
|
||||
@@ -62,7 +81,9 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
extract_comfy,
|
||||
launch_comfy,
|
||||
show_path::show_in_folder
|
||||
show_path::show_in_folder,
|
||||
settings::get_setting,
|
||||
settings::set_setting,
|
||||
])
|
||||
.build(context)
|
||||
.expect("error while building tauri application")
|
||||
@@ -78,12 +99,18 @@ fn main() {
|
||||
|
||||
// tauri command to extract a zip from an arbitrary file path
|
||||
#[tauri::command]
|
||||
fn extract_comfy(
|
||||
handle: tauri::AppHandle,
|
||||
path: String,
|
||||
target_dir: String,
|
||||
) -> Result<String, String> {
|
||||
println!("extracting zip from {} to {}", path, target_dir);
|
||||
fn extract_comfy(handle: tauri::AppHandle) -> Result<String, String> {
|
||||
let mut path = handle.path_resolver().app_data_dir().unwrap();
|
||||
path.push("comfyui.zip");
|
||||
|
||||
let target_dir = *settings::SETTINGS
|
||||
.get()
|
||||
.unwrap()
|
||||
.data
|
||||
.comfyui_location
|
||||
.clone();
|
||||
|
||||
println!("extracting zip from {:?} to {}", path, target_dir);
|
||||
let file = File::open(path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(file).unwrap();
|
||||
|
||||
@@ -96,25 +123,6 @@ fn extract_comfy(
|
||||
// dont block the main thread
|
||||
extract_thread.join().unwrap();
|
||||
|
||||
let default_graph = handle
|
||||
.path_resolver()
|
||||
.resolve_resource("resources/defaultGraph.js")
|
||||
.expect("failed to resolve resource");
|
||||
let stable_inputs = handle
|
||||
.path_resolver()
|
||||
.resolve_resource("resources/stableStudioInputs.js")
|
||||
.expect("failed to resolve resource");
|
||||
|
||||
let mut dest = std::path::PathBuf::from(&target_dir);
|
||||
dest.push("ComfyUI/ComfyUI/web/scripts/defaultGraph.js");
|
||||
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
|
||||
std::fs::copy(default_graph, dest).unwrap();
|
||||
|
||||
let mut dest = std::path::PathBuf::from(&target_dir);
|
||||
dest.push("ComfyUI/ComfyUI/web/extensions/stableStudioInputs.js");
|
||||
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
|
||||
std::fs::copy(stable_inputs, dest).unwrap();
|
||||
|
||||
println!("extracted zip");
|
||||
Ok("completed".to_string())
|
||||
}
|
||||
@@ -168,7 +176,17 @@ async fn watch_comfy(
|
||||
|
||||
// tauri command to launch python process
|
||||
#[tauri::command]
|
||||
async fn launch_comfy(path: String) -> Result<String, String> {
|
||||
async fn launch_comfy() -> Result<String, String> {
|
||||
let comfyui_location = *settings::SETTINGS
|
||||
.get()
|
||||
.unwrap()
|
||||
.data
|
||||
.comfyui_location
|
||||
.clone();
|
||||
let mut path = std::path::PathBuf::from(comfyui_location.clone());
|
||||
path.push("ComfyUI");
|
||||
let url = *settings::SETTINGS.get().unwrap().data.comfyui_url.clone();
|
||||
|
||||
// set working directory
|
||||
std::env::set_current_dir(path.clone()).unwrap();
|
||||
|
||||
@@ -178,7 +196,7 @@ async fn launch_comfy(path: String) -> Result<String, String> {
|
||||
.connect_timeout(std::time::Duration::from_secs(1))
|
||||
.build()
|
||||
.unwrap();
|
||||
let resp = client.get("http://localhost:5000").send().await;
|
||||
let resp = client.get(url.clone()).send().await;
|
||||
if resp.is_ok() {
|
||||
println!("Comfy already running, skipping launch.");
|
||||
return Ok("completed".to_string());
|
||||
@@ -198,7 +216,14 @@ async fn launch_comfy(path: String) -> Result<String, String> {
|
||||
panic!("Unsupported platform")
|
||||
}
|
||||
})
|
||||
.args(["-s", "ComfyUI/main.py", "--port", "5000"])
|
||||
.args([
|
||||
"-s",
|
||||
"ComfyUI/main.py",
|
||||
"--port",
|
||||
url.split(":").collect::<Vec<&str>>().last().unwrap(),
|
||||
"--preview-method",
|
||||
"auto",
|
||||
])
|
||||
.envs(HashMap::from([("PYTHONUNBUFFERED".into(), "1".into())]))
|
||||
.spawn()
|
||||
.expect("Failed to spawn ComfyUI process");
|
||||
|
||||
@@ -7,6 +7,8 @@ use tauri::{
|
||||
};
|
||||
use tiny_http::{Header, Response as HttpResponse, Server};
|
||||
|
||||
use crate::settings;
|
||||
|
||||
pub struct Response {
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
@@ -36,14 +38,10 @@ impl Builder {
|
||||
let server = Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
|
||||
let server = Arc::new(server);
|
||||
let mut guards = Vec::with_capacity(4);
|
||||
|
||||
let path_resolver = app.path_resolver();
|
||||
let appdata_path = path_resolver.app_data_dir().unwrap().as_os_str().to_str().unwrap().to_string();
|
||||
|
||||
for _ in 0..5 {
|
||||
let server = server.clone();
|
||||
let asset_resolver = app.asset_resolver();
|
||||
let appdata_path = appdata_path.clone();
|
||||
let guard = std::thread::spawn(move || {
|
||||
// aquire a copy of appdata_path
|
||||
for mut req in server.incoming_requests() {
|
||||
@@ -84,7 +82,7 @@ impl Builder {
|
||||
None,
|
||||
None,
|
||||
Some(
|
||||
"http://localhost:5000/".to_string() + &sliced_path.join("/") + "?" + &req.url().split("?").collect::<Vec<&str>>()[1..].join("?"),
|
||||
settings::SETTINGS.get().unwrap().data.comfyui_url.to_string() + "/" + &sliced_path.join("/") + "?" + &req.url().split("?").collect::<Vec<&str>>()[1..].join("?"),
|
||||
),
|
||||
),
|
||||
[] | [_, _, ..] | [&_] => (None, None, None),
|
||||
@@ -93,7 +91,7 @@ impl Builder {
|
||||
if let Some(file_name) = file_name {
|
||||
let path_name = format!(
|
||||
"{}/ComfyUI/ComfyUI/web/{}",
|
||||
appdata_path.clone(),
|
||||
settings::SETTINGS.get().unwrap().data.comfyui_location.to_string(),
|
||||
file_name
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub comfyui_location: Box<String>,
|
||||
pub comfyui_url: Box<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Store {
|
||||
pub path: PathBuf,
|
||||
pub data: Settings,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn save(&self) -> Result<(), Box<dyn Error>> {
|
||||
let file = File::create(&self.path)?;
|
||||
serde_json::to_writer(file, &self.data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Settings {
|
||||
fn clone(&self) -> Self {
|
||||
Settings {
|
||||
comfyui_location: self.comfyui_location.clone(),
|
||||
comfyui_url: self.comfyui_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static SETTINGS: OnceLock<Store> = OnceLock::new();
|
||||
|
||||
pub fn create_settings(path: PathBuf, default: Settings) -> Result<&'static Store, Box<dyn Error>> {
|
||||
let file = match File::open(&path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
let file = File::create(&path.as_path())?;
|
||||
serde_json::to_writer(file, &default)?;
|
||||
return create_settings(path, default);
|
||||
}
|
||||
};
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let u = match serde_json::from_reader::<_, Settings>(reader) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
let file = File::create(&path.as_path())?;
|
||||
serde_json::to_writer(file, &default)?;
|
||||
default
|
||||
}
|
||||
};
|
||||
|
||||
let s = Store { path, data: u };
|
||||
|
||||
let _ = SETTINGS.set(s);
|
||||
|
||||
Ok(SETTINGS.get().unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_setting(key: String) -> Result<String, String> {
|
||||
let s = SETTINGS.get().unwrap();
|
||||
match key.as_str() {
|
||||
"comfyui_location" => Ok(s.data.comfyui_location.to_string()),
|
||||
"comfyui_url" => Ok(s.data.comfyui_url.to_string()),
|
||||
_ => Err("Invalid key".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_setting(key: String, value: String) -> Result<(), String> {
|
||||
let mut s = SETTINGS.get().unwrap().clone();
|
||||
match key.as_str() {
|
||||
"comfyui_location" => {
|
||||
let path = Path::new(&value);
|
||||
if !path.exists() {
|
||||
return Err("Invalid path".to_string());
|
||||
}
|
||||
s.data.comfyui_location = Box::new(value);
|
||||
}
|
||||
"comfyui_url" => {
|
||||
s.data.comfyui_url = Box::new(value);
|
||||
}
|
||||
_ => return Err("Invalid key".to_string()),
|
||||
}
|
||||
|
||||
let _ = s.save();
|
||||
let _ = SETTINGS.set(s);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"allowlist": {
|
||||
"fs": {
|
||||
"all": true,
|
||||
"scope": ["$APPDATA/**", "$RESOURCE/**"]
|
||||
"scope": ["$APPDATA/**"]
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
@@ -46,9 +46,7 @@
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [
|
||||
"resources/*"
|
||||
],
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
|
||||
@@ -117,38 +117,40 @@ export namespace App {
|
||||
if (isSetup !== SetupState.NotStarted || nonce.current !== 0) return;
|
||||
nonce.current++;
|
||||
|
||||
const appDataPath = await appDataDir();
|
||||
const comfyui_location = await invoke("get_setting", {
|
||||
key: "comfyui_location",
|
||||
})
|
||||
.then((res) => res)
|
||||
.catch(() => appDataPath);
|
||||
|
||||
let entries: FileEntry[] = await listAppDataDir(
|
||||
"comfyui/ComfyUI/models/checkpoints"
|
||||
`${comfyui_location}/ComfyUI/ComfyUI/models/checkpoints`
|
||||
);
|
||||
|
||||
// filter for actual files (not directories or symlinks or "put_checkpoints_here" files)
|
||||
entries = entries.filter((entry) => entry.name?.includes("."));
|
||||
|
||||
console.log(entries);
|
||||
const appDataPath = await appDataDir();
|
||||
|
||||
if (entries.length === 0) {
|
||||
const comfyExists = await exists("comdyui/ComfyUI/main.py", {
|
||||
dir: BaseDirectory.AppData,
|
||||
});
|
||||
const comfyExists = await exists(
|
||||
`${comfyui_location}/ComfyUI/ComfyUI/main.py`
|
||||
);
|
||||
|
||||
if (!comfyExists) {
|
||||
setIsSetup(SetupState.NotStarted);
|
||||
setMessage("Downloading ComfyUI...");
|
||||
|
||||
// delete the old comfyui zip if it exists
|
||||
if (
|
||||
!(await exists("comfyui.zip", {
|
||||
dir: BaseDirectory.AppData,
|
||||
}))
|
||||
) {
|
||||
if (!(await exists(`${comfyui_location}/comfyui.zip`))) {
|
||||
let comulativeProgress = 0;
|
||||
|
||||
console.log("downloading comfyui");
|
||||
|
||||
await download(
|
||||
"https://pub-5e5adf378ed14628a527d735b7743e4e.r2.dev/stability-downloads/ComfyUI/ComfyUI_windows_portable.zip",
|
||||
`${appDataPath}\\comfyui.zip`,
|
||||
`${comfyui_location}\\comfyui.zip`,
|
||||
(p, total) => {
|
||||
comulativeProgress += p;
|
||||
setProgress(comulativeProgress / total);
|
||||
@@ -159,10 +161,7 @@ export namespace App {
|
||||
setMessage("Extracting ComfyUI...");
|
||||
setProgress(null);
|
||||
try {
|
||||
const result = await invoke("extract_comfy", {
|
||||
path: `${appDataPath}/comfyui.zip`,
|
||||
targetDir: `${appDataPath}`,
|
||||
});
|
||||
const result = await invoke("extract_comfy");
|
||||
|
||||
if (result !== "completed") {
|
||||
throw new Error("Failed to extract comfyui.zip");
|
||||
@@ -174,9 +173,7 @@ export namespace App {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeFile("comfyui.zip", {
|
||||
dir: BaseDirectory.AppData,
|
||||
});
|
||||
await removeFile(`${comfyui_location}/comfyui.zip`);
|
||||
}
|
||||
|
||||
setIsSetup(SetupState.ComfyInstalled);
|
||||
@@ -188,7 +185,7 @@ export namespace App {
|
||||
|
||||
await download(
|
||||
"https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors",
|
||||
`${appDataPath}/comfyui/ComfyUI/models/checkpoints/v2-1_768-ema-pruned.safetensors`,
|
||||
`${comfyui_location}/ComfyUI/ComfyUI/models/checkpoints/v2-1_768-ema-pruned.safetensors`,
|
||||
(p, total) => {
|
||||
comulativeProgress += p;
|
||||
setProgress(comulativeProgress / total);
|
||||
@@ -211,9 +208,7 @@ export namespace App {
|
||||
setUnlisteners([unlisten]);
|
||||
|
||||
// start comfy
|
||||
const result = await invoke("launch_comfy", {
|
||||
path: `${appDataPath}/comfyui`,
|
||||
});
|
||||
const result = await invoke("launch_comfy");
|
||||
|
||||
if (result !== "completed") {
|
||||
setMessage(`Error launching ComfyUI: ${result}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PluginStatus } from "@stability/stablestudio-plugin";
|
||||
import { getTauriVersion, getVersion } from "@tauri-apps/api/app";
|
||||
import { version as getOsVerison, platform } from "@tauri-apps/api/os";
|
||||
import { appDataDir } from "@tauri-apps/api/path";
|
||||
import { appDataDir, sep } from "@tauri-apps/api/path";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -60,8 +60,12 @@ export function Settings() {
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchComfyLocation() {
|
||||
const path = await appDataDir();
|
||||
setComfyLocation(`${path}ComfyUI`);
|
||||
const path = await invoke("get_setting", {
|
||||
key: "comfyui_location",
|
||||
})
|
||||
.then((res) => res)
|
||||
.catch(() => appDataDir());
|
||||
setComfyLocation(`${path}${sep}ComfyUI`);
|
||||
}
|
||||
|
||||
async function fetchVersion() {
|
||||
|
||||
Reference in New Issue
Block a user