First commit
This commit is contained in:
13
.eslintignore
Normal file
13
.eslintignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
15
.eslintrc.cjs
Normal file
15
.eslintrc.cjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['eslint:recommended', 'prettier'],
|
||||||
|
plugins: ['svelte3'],
|
||||||
|
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
}
|
||||||
|
};
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
13
.prettierignore
Normal file
13
.prettierignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
17
jsconfig.json
Normal file
17
jsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
||||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "web2",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
|
"svelte-check": "svelte-check"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
"@sveltejs/kit": "^1.5.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
|
"svelte": "^3.54.0",
|
||||||
|
"svelte-check": "^3.2.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.2.0",
|
||||||
|
"vitest": "^0.25.3"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"litegraph.js": "^0.7.12",
|
||||||
|
"svelte-preprocess": "^5.0.3",
|
||||||
|
"vite-plugin-full-reload": "^1.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
patches/litegraph.js@0.7.12.patch
Normal file
14
patches/litegraph.js@0.7.12.patch
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
diff --git a/src/litegraph.d.ts b/src/litegraph.d.ts
|
||||||
|
index c9fbe0ced078548f95382eaa39be2b736be2a1bd..3ee29f82d614cd9f460f56228c56ea4eadc1b3fb 100644
|
||||||
|
--- a/src/litegraph.d.ts
|
||||||
|
+++ b/src/litegraph.d.ts
|
||||||
|
@@ -1260,6 +1260,9 @@ export declare class LGraphCanvas {
|
||||||
|
visible_nodes: LGraphNode[];
|
||||||
|
zoom_modify_alpha: boolean;
|
||||||
|
|
||||||
|
+ release_link_on_empty_shows_menu: boolean;
|
||||||
|
+ alt_drag_do_clone_nodes: boolean;
|
||||||
|
+
|
||||||
|
/** clears all the data inside */
|
||||||
|
clear(): void;
|
||||||
|
/** assigns a graph, you can reassign graphs to the same canvas */
|
||||||
1653
pnpm-lock.yaml
generated
Normal file
1653
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
src/index.test.js
Normal file
7
src/index.test.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('sum test', () => {
|
||||||
|
it('adds 1 + 2 to equal 3', () => {
|
||||||
|
expect(1 + 2).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
290
src/lib/api.ts
Normal file
290
src/lib/api.ts
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
type PromptRequestBody = {
|
||||||
|
client_id: string,
|
||||||
|
prompt: any,
|
||||||
|
extra_data: any,
|
||||||
|
front: boolean,
|
||||||
|
number: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueueItemType = "queue" | "history";
|
||||||
|
|
||||||
|
export default class ComfyAPI extends EventTarget {
|
||||||
|
private registered: Set<string> = new Set<string>();
|
||||||
|
|
||||||
|
socket: WebSocket | null = null;
|
||||||
|
clientId: string | null = null;
|
||||||
|
hostname: string | null = null;
|
||||||
|
port: number | null = 8188;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
override addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) {
|
||||||
|
super.addEventListener(type, callback, options);
|
||||||
|
this.registered.add(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll status for colab and other things that don't support websockets.
|
||||||
|
*/
|
||||||
|
private pollQueue() {
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(this.getBackendUrl() + "/prompt");
|
||||||
|
const status = await resp.json();
|
||||||
|
this.dispatchEvent(new CustomEvent("status", { detail: status }));
|
||||||
|
} catch (error) {
|
||||||
|
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBackendUrl(): string {
|
||||||
|
const hostname = this.hostname || location.hostname;
|
||||||
|
const port = this.port || location.port;
|
||||||
|
console.log(hostname)
|
||||||
|
console.log(port)
|
||||||
|
return `${window.location.protocol}//${hostname}:${port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and connects a WebSocket for realtime updates
|
||||||
|
* @param {boolean} isReconnect If the socket is connection is a reconnect attempt
|
||||||
|
*/
|
||||||
|
private createSocket(isReconnect: boolean = false) {
|
||||||
|
if (this.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let opened = false;
|
||||||
|
let existingSession = sessionStorage["Comfy.SessionId"] || "";
|
||||||
|
if (existingSession) {
|
||||||
|
existingSession = "/" + existingSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = this.hostname || location.host;
|
||||||
|
const port = this.port || location.port;
|
||||||
|
|
||||||
|
this.socket = new WebSocket(
|
||||||
|
`ws${window.location.protocol === "https:" ? "s" : ""}://${hostname}:${port}/ws${existingSession}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.socket.addEventListener("open", () => {
|
||||||
|
opened = true;
|
||||||
|
if (isReconnect) {
|
||||||
|
this.dispatchEvent(new CustomEvent("reconnected"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener("error", () => {
|
||||||
|
if (this.socket) this.socket.close();
|
||||||
|
if (!isReconnect && !opened) {
|
||||||
|
this.pollQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener("close", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.socket = null;
|
||||||
|
this.createSocket(true);
|
||||||
|
}, 300);
|
||||||
|
if (opened) {
|
||||||
|
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||||
|
this.dispatchEvent(new CustomEvent("reconnecting"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.addEventListener("message", (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
switch (msg.type) {
|
||||||
|
case "status":
|
||||||
|
if (msg.data.sid) {
|
||||||
|
this.clientId = msg.data.sid;
|
||||||
|
sessionStorage["Comfy.SessionId"] = this.clientId;
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
|
||||||
|
break;
|
||||||
|
case "progress":
|
||||||
|
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
|
||||||
|
break;
|
||||||
|
case "executing":
|
||||||
|
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node }));
|
||||||
|
break;
|
||||||
|
case "executed":
|
||||||
|
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (this.registered.has(msg.type)) {
|
||||||
|
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown message type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unhandled message:", event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises sockets and realtime updates
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.createSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of extension urls
|
||||||
|
* @returns An array of script urls to import
|
||||||
|
*/
|
||||||
|
async getExtensions() {
|
||||||
|
const resp = await fetch(this.getBackendUrl() + `/extensions`, { cache: "no-store" });
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of embedding names
|
||||||
|
* @returns An array of script urls to import
|
||||||
|
*/
|
||||||
|
async getEmbeddings() {
|
||||||
|
const resp = await fetch(this.getBackendUrl() + "/embeddings", { cache: "no-store" });
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads node object definitions for the graph
|
||||||
|
* @returns The node definitions
|
||||||
|
*/
|
||||||
|
async getNodeDefs() {
|
||||||
|
const resp = await fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" });
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
||||||
|
* @param {object} prompt The prompt data to queue
|
||||||
|
*/
|
||||||
|
async queuePrompt(number: number, { output, workflow }) {
|
||||||
|
const body: PromptRequestBody = {
|
||||||
|
client_id: this.clientId,
|
||||||
|
prompt: output,
|
||||||
|
extra_data: { extra_pnginfo: { workflow } },
|
||||||
|
front: false,
|
||||||
|
number: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (number === -1) {
|
||||||
|
body.front = true;
|
||||||
|
} else if (number != 0) {
|
||||||
|
body.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(this.getBackendUrl() + "/prompt", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw {
|
||||||
|
response: await res.text(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a list of items (queue or history)
|
||||||
|
* @param {string} type The type of items to load, queue or history
|
||||||
|
* @returns The items of the specified type grouped by their status
|
||||||
|
*/
|
||||||
|
async getItems(type: QueueItemType) {
|
||||||
|
if (type === "queue") {
|
||||||
|
return this.getQueue();
|
||||||
|
}
|
||||||
|
return this.getHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current state of the queue
|
||||||
|
* @returns The currently running and queued items
|
||||||
|
*/
|
||||||
|
async getQueue() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(this.getBackendUrl() + "/queue");
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
// Running action uses a different endpoint for cancelling
|
||||||
|
Running: data.queue_running.map((prompt) => ({
|
||||||
|
prompt,
|
||||||
|
remove: { name: "Cancel", cb: () => this.interrupt() },
|
||||||
|
})),
|
||||||
|
Pending: data.queue_pending.map((prompt) => ({ prompt })),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return { Running: [], Pending: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the prompt execution history
|
||||||
|
* @returns Prompt history including node outputs
|
||||||
|
*/
|
||||||
|
async getHistory() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(this.getBackendUrl() + "/history");
|
||||||
|
return { History: Object.values(await res.json()) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return { History: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a POST request to the API
|
||||||
|
* @param {*} type The endpoint to post to
|
||||||
|
* @param {*} body Optional POST data
|
||||||
|
*/
|
||||||
|
private async postItem(type: string, body: any) {
|
||||||
|
try {
|
||||||
|
await fetch("/" + type, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an item from the specified list
|
||||||
|
* @param {string} type The type of item to delete, queue or history
|
||||||
|
* @param {number} id The id of the item to delete
|
||||||
|
*/
|
||||||
|
async deleteItem(type: string, id: number) {
|
||||||
|
await this.postItem(type, { delete: [id] });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the specified list
|
||||||
|
* @param {string} type The type of list to clear, queue or history
|
||||||
|
*/
|
||||||
|
async clearItems(type: string) {
|
||||||
|
await this.postItem(type, { clear: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interrupts the execution of the running prompt
|
||||||
|
*/
|
||||||
|
async interrupt() {
|
||||||
|
await this.postItem("interrupt", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/lib/components/ComfyApp.svelte
Normal file
24
src/lib/components/ComfyApp.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import ComfyApp from "./ComfyApp";
|
||||||
|
|
||||||
|
let app: ComfyApp = undefined;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
app = new ComfyApp();
|
||||||
|
await app.setup();
|
||||||
|
|
||||||
|
(window as any).app = app;
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<canvas id="graph-canvas" tabIndex="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
406
src/lib/components/ComfyApp.ts
Normal file
406
src/lib/components/ComfyApp.ts
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode } from "litegraph.js";
|
||||||
|
import type { LGraphNodeBase } from "litegraph.js";
|
||||||
|
import ComfyAPI from "$lib/api"
|
||||||
|
import { ComfyWidgets } from "$lib/widgets"
|
||||||
|
import defaultGraph from "$lib/defaultGraph"
|
||||||
|
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||||
|
|
||||||
|
type QueueItem = { num: number, batchCount: number }
|
||||||
|
|
||||||
|
export default class ComfyApp {
|
||||||
|
api: ComfyAPI;
|
||||||
|
canvasEl: HTMLCanvasElement | null = null;
|
||||||
|
canvasCtx: CanvasRenderingContext2D | null = null;
|
||||||
|
lGraph: LGraph | null = null;
|
||||||
|
lCanvas: LGraphCanvas | null = null;
|
||||||
|
nodeOutputs: Record<string, any> = {};
|
||||||
|
|
||||||
|
private queueItems: QueueItem[] = [];
|
||||||
|
private processingQueue: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.api = new ComfyAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup(): Promise<void> {
|
||||||
|
this.addProcessMouseHandler();
|
||||||
|
this.addProcessKeyHandler();
|
||||||
|
|
||||||
|
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
||||||
|
this.lGraph = new LGraph();
|
||||||
|
this.lCanvas = new LGraphCanvas(this.canvasEl, this.lGraph);
|
||||||
|
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||||
|
|
||||||
|
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||||
|
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||||
|
|
||||||
|
this.lGraph.start();
|
||||||
|
|
||||||
|
// Ensure the canvas fills the window
|
||||||
|
this.resizeCanvas();
|
||||||
|
window.addEventListener("resize", this.resizeCanvas.bind(this));
|
||||||
|
|
||||||
|
// await this.#invokeExtensionsAsync("init");
|
||||||
|
await this.registerNodes();
|
||||||
|
|
||||||
|
// Load previous workflow
|
||||||
|
let restored = false;
|
||||||
|
try {
|
||||||
|
const json = localStorage.getItem("workflow");
|
||||||
|
if (json) {
|
||||||
|
const workflow = JSON.parse(json);
|
||||||
|
this.loadGraphData(workflow);
|
||||||
|
restored = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading previous workflow", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We failed to restore a workflow so load the default
|
||||||
|
if (!restored) {
|
||||||
|
this.loadGraphData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current workflow automatically
|
||||||
|
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.lGraph.serialize())), 1000);
|
||||||
|
|
||||||
|
// this.#addDrawNodeHandler();
|
||||||
|
// this.#addDrawGroupsHandler();
|
||||||
|
// this.#addApiUpdateHandlers();
|
||||||
|
// this.#addDropHandler();
|
||||||
|
// this.#addPasteHandler();
|
||||||
|
// this.#addKeyboardHandler();
|
||||||
|
|
||||||
|
// await this.#invokeExtensionsAsync("setup");
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeCanvas() {
|
||||||
|
this.canvasEl.width = window.innerWidth;
|
||||||
|
this.canvasEl.height = window.innerHeight;
|
||||||
|
this.lCanvas.draw(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addProcessMouseHandler() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private addProcessKeyHandler() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async registerNodes() {
|
||||||
|
const app = this;
|
||||||
|
|
||||||
|
// Load node definitions from the backend
|
||||||
|
const defs = await this.api.getNodeDefs();
|
||||||
|
// await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
|
||||||
|
|
||||||
|
// Generate list of known widgets
|
||||||
|
const widgets = ComfyWidgets;
|
||||||
|
// const widgets = Object.assign(
|
||||||
|
// {},
|
||||||
|
// ComfyWidgets,
|
||||||
|
// ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Register a node for each definition
|
||||||
|
for (const nodeId in defs) {
|
||||||
|
const nodeData = defs[nodeId];
|
||||||
|
const node: LGraphNodeBase = Object.assign(
|
||||||
|
function ComfyNode(this: LGraphNode) {
|
||||||
|
var inputs = nodeData["input"]["required"];
|
||||||
|
if (nodeData["input"]["optional"] != undefined){
|
||||||
|
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
||||||
|
}
|
||||||
|
const config = { minWidth: 1, minHeight: 1 };
|
||||||
|
for (const inputName in inputs) {
|
||||||
|
const inputData = inputs[inputName];
|
||||||
|
const type = inputData[0];
|
||||||
|
|
||||||
|
if(inputData[1]?.forceInput) {
|
||||||
|
this.addInput(inputName, type);
|
||||||
|
} else {
|
||||||
|
if (Array.isArray(type)) {
|
||||||
|
// Enums
|
||||||
|
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
|
||||||
|
} else if (`${type}:${inputName}` in widgets) {
|
||||||
|
// Support custom widgets by Type:Name
|
||||||
|
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||||
|
} else if (type in widgets) {
|
||||||
|
// Standard type widgets
|
||||||
|
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
|
||||||
|
} else {
|
||||||
|
// Node connection inputs
|
||||||
|
this.addInput(inputName, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const o in nodeData["output"]) {
|
||||||
|
const output = nodeData["output"][o];
|
||||||
|
const outputName = nodeData["output_name"][o] || output;
|
||||||
|
this.addOutput(outputName, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = this.computeSize();
|
||||||
|
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||||
|
s[1] = Math.max(config.minHeight, s[1]);
|
||||||
|
this.size = s;
|
||||||
|
this.serialize_widgets = true;
|
||||||
|
|
||||||
|
// app.#invokeExtensionsAsync("nodeCreated", this);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: nodeData.name,
|
||||||
|
comfyClass: nodeData.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
node.prototype.comfyClass = nodeData.name;
|
||||||
|
|
||||||
|
// this.#addNodeContextMenuHandler(node);
|
||||||
|
// this.#addDrawBackgroundHandler(node, app);
|
||||||
|
|
||||||
|
// await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
|
||||||
|
LiteGraph.registerNodeType(nodeId, node);
|
||||||
|
node.category = nodeData.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// await this.#invokeExtensionsAsync("registerCustomNodes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the graph with the specified workflow data
|
||||||
|
* @param {*} graphData A serialized graph object
|
||||||
|
*/
|
||||||
|
loadGraphData(graphData: any = null) {
|
||||||
|
this.clean();
|
||||||
|
|
||||||
|
if (!graphData) {
|
||||||
|
graphData = structuredClone(defaultGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||||
|
for (let n of graphData.nodes) {
|
||||||
|
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lGraph.configure(graphData);
|
||||||
|
|
||||||
|
for (const node of this.lGraph._nodes) {
|
||||||
|
const size = node.computeSize();
|
||||||
|
size[0] = Math.max(node.size[0], size[0]);
|
||||||
|
size[1] = Math.max(node.size[1], size[1]);
|
||||||
|
node.size = size;
|
||||||
|
|
||||||
|
if (node.widgets) {
|
||||||
|
// If you break something in the backend and want to patch workflows in the frontend
|
||||||
|
// This is the place to do this
|
||||||
|
for (let widget of node.widgets) {
|
||||||
|
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
||||||
|
if (widget.name == "sampler_name") {
|
||||||
|
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
|
||||||
|
widget.value = widget.value.slice(7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.#invokeExtensions("loadedGraphNode", node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the current graph workflow for sending to the API
|
||||||
|
* @returns The workflow and node links
|
||||||
|
*/
|
||||||
|
async graphToPrompt() {
|
||||||
|
const workflow = this.lGraph.serialize();
|
||||||
|
const output = {};
|
||||||
|
// Process nodes in order of execution
|
||||||
|
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
|
||||||
|
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||||
|
|
||||||
|
if (node.isVirtualNode) {
|
||||||
|
// Don't serialize frontend only nodes but let them make changes
|
||||||
|
if (node.applyToGraph) {
|
||||||
|
node.applyToGraph(workflow);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.mode === 2) {
|
||||||
|
// Don't serialize muted nodes
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = {};
|
||||||
|
const widgets = node.widgets;
|
||||||
|
|
||||||
|
// Store all widget values
|
||||||
|
if (widgets) {
|
||||||
|
for (const i in widgets) {
|
||||||
|
const widget = widgets[i];
|
||||||
|
if (!widget.options || widget.options.serialize !== false) {
|
||||||
|
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store all node links
|
||||||
|
for (let i in node.inputs) {
|
||||||
|
let parent = node.getInputNode(i);
|
||||||
|
if (parent) {
|
||||||
|
let link = node.getInputLink(i);
|
||||||
|
while (parent && parent.isVirtualNode) {
|
||||||
|
link = parent.getInputLink(link.origin_slot);
|
||||||
|
if (link) {
|
||||||
|
parent = parent.getInputNode(link.origin_slot);
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output[String(node.id)] = {
|
||||||
|
inputs,
|
||||||
|
class_type: node.comfyClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove inputs connected to removed nodes
|
||||||
|
|
||||||
|
for (const o in output) {
|
||||||
|
for (const i in output[o].inputs) {
|
||||||
|
if (Array.isArray(output[o].inputs[i])
|
||||||
|
&& output[o].inputs[i].length === 2
|
||||||
|
&& !output[output[o].inputs[i][0]]) {
|
||||||
|
delete output[o].inputs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workflow, output };
|
||||||
|
}
|
||||||
|
|
||||||
|
async queuePrompt(num: number, batchCount: number = 1) {
|
||||||
|
this.queueItems.push({ num, batchCount });
|
||||||
|
|
||||||
|
// Only have one action process the items so each one gets a unique seed correctly
|
||||||
|
if (this.processingQueue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processingQueue = true;
|
||||||
|
try {
|
||||||
|
while (this.queueItems.length) {
|
||||||
|
({ num, batchCount } = this.queueItems.pop());
|
||||||
|
console.log(`Queue get! ${num} ${batchCount}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < batchCount; i++) {
|
||||||
|
const p = await this.graphToPrompt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.queuePrompt(num, p);
|
||||||
|
} catch (error) {
|
||||||
|
// this.ui.dialog.show(error.response || error.toString());
|
||||||
|
console.error(error.response || error.toString())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of p.workflow.nodes) {
|
||||||
|
const node = this.lGraph.getNodeById(n.id);
|
||||||
|
if (node.widgets) {
|
||||||
|
for (const widget of node.widgets) {
|
||||||
|
// Allow widgets to run callbacks after a prompt has been queued
|
||||||
|
// e.g. random seed after every gen
|
||||||
|
// if (widget.afterQueued) {
|
||||||
|
// widget.afterQueued();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lCanvas.draw(true, true);
|
||||||
|
// await this.ui.queue.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
console.log("Queue finished!");
|
||||||
|
this.processingQueue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads workflow data from the specified file
|
||||||
|
*/
|
||||||
|
async handleFile(file: File) {
|
||||||
|
if (file.type === "image/png") {
|
||||||
|
const pngInfo = await getPngMetadata(file);
|
||||||
|
if (pngInfo) {
|
||||||
|
if (pngInfo.workflow) {
|
||||||
|
this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||||
|
} else if (pngInfo.parameters) {
|
||||||
|
importA1111(this.lGraph, pngInfo.parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
this.loadGraphData(JSON.parse(reader.result));
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerExtension(extension) {
|
||||||
|
// if (!extension.name) {
|
||||||
|
// throw new Error("Extensions must have a 'name' property.");
|
||||||
|
// }
|
||||||
|
// if (this.extensions.find((ext) => ext.name === extension.name)) {
|
||||||
|
// throw new Error(`Extension named '${extension.name}' already registered.`);
|
||||||
|
// }
|
||||||
|
// this.extensions.push(extension);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh combo list on whole nodes
|
||||||
|
*/
|
||||||
|
async refreshComboInNodes() {
|
||||||
|
const defs = await this.api.getNodeDefs();
|
||||||
|
|
||||||
|
for(let nodeNum in this.lGraph._nodes) {
|
||||||
|
const node = this.lGraph._nodes[nodeNum];
|
||||||
|
|
||||||
|
const def = defs[node.type];
|
||||||
|
|
||||||
|
for(const widgetNum in node.widgets) {
|
||||||
|
const widget = node.widgets[widgetNum]
|
||||||
|
|
||||||
|
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
||||||
|
widget.options.values = def["input"]["required"][widget.name][0];
|
||||||
|
|
||||||
|
if(!widget.options.values.includes(widget.value)) {
|
||||||
|
widget.value = widget.options.values[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean current state
|
||||||
|
*/
|
||||||
|
clean() {
|
||||||
|
this.nodeOutputs = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/lib/defaultGraph.ts
Normal file
119
src/lib/defaultGraph.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
export default {
|
||||||
|
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: "CheckpointLoaderSimple",
|
||||||
|
pos: [26, 474],
|
||||||
|
size: { 0: 315, 1: 98 },
|
||||||
|
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-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,
|
||||||
|
};
|
||||||
324
src/lib/pnginfo.ts
Normal file
324
src/lib/pnginfo.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import { LiteGraph, LGraph, LGraphNode } from "litegraph.js"
|
||||||
|
import type ComfyAPI from "$lib/api"
|
||||||
|
|
||||||
|
class PNGMetadataPromise extends Promise<Record<string, string>> {
|
||||||
|
public cancelMethod: () => void;
|
||||||
|
constructor(executor: (resolve: (value?: Record<string, string>) => void, reject: (reason?: any) => void) => void) {
|
||||||
|
super(executor);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//cancel the operation
|
||||||
|
public cancel() {
|
||||||
|
if (this.cancelMethod) {
|
||||||
|
this.cancelMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPngMetadata(file: File): PNGMetadataPromise {
|
||||||
|
return new PNGMetadataPromise((r, _) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event: Event) => {
|
||||||
|
// Get the PNG data as a Uint8Array
|
||||||
|
const pngData = new Uint8Array((event.target as any).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");
|
||||||
|
r();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += 12 + length;
|
||||||
|
}
|
||||||
|
|
||||||
|
r(txt_chunks);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeIndex = { node: LGraphNode, index: number }
|
||||||
|
|
||||||
|
export async function importA1111(graph: LGraph, parameters: string, api: ComfyAPI) {
|
||||||
|
const p = parameters.lastIndexOf("\nSteps:");
|
||||||
|
if (p > -1) {
|
||||||
|
const embeddings = await api.getEmbeddings();
|
||||||
|
const opts = parameters
|
||||||
|
.substr(p)
|
||||||
|
.split(",")
|
||||||
|
.reduce((p, n) => {
|
||||||
|
const s = n.split(":");
|
||||||
|
p[s[0].trim().toLowerCase()] = s[1].trim();
|
||||||
|
return p;
|
||||||
|
}, {});
|
||||||
|
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
|
||||||
|
if (p2 > -1) {
|
||||||
|
let positive = parameters.substr(0, p2).trim();
|
||||||
|
let negative = parameters.substring(p2 + 18, p).trim();
|
||||||
|
|
||||||
|
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
|
||||||
|
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
|
||||||
|
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
|
||||||
|
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
|
||||||
|
const samplerNode = LiteGraph.createNode("KSampler");
|
||||||
|
const imageNode = LiteGraph.createNode("EmptyLatentImage");
|
||||||
|
const vaeNode = LiteGraph.createNode("VAEDecode");
|
||||||
|
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
|
||||||
|
const saveNode = LiteGraph.createNode("SaveImage");
|
||||||
|
let hrSamplerNode = null;
|
||||||
|
|
||||||
|
const ceil64 = (v) => Math.ceil(v / 64) * 64;
|
||||||
|
|
||||||
|
function getWidget(node: LGraphNode, name: string) {
|
||||||
|
return node.widgets.find((w) => w.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWidgetValue(node: LGraphNode, name: string, value: any, isOptionPrefix: boolean = false) {
|
||||||
|
const w = getWidget(node, name);
|
||||||
|
if (isOptionPrefix) {
|
||||||
|
const o = w.options.values.find((w) => w.startsWith(value));
|
||||||
|
if (o) {
|
||||||
|
w.value = o;
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
|
||||||
|
w.value = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLoraNodes(clipNode: LGraphNode, text: string, prevClip: NodeIndex, prevModel: NodeIndex) {
|
||||||
|
const loras = [];
|
||||||
|
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
|
||||||
|
const s = c.split(":");
|
||||||
|
const weight = parseFloat(s[1]);
|
||||||
|
if (isNaN(weight)) {
|
||||||
|
console.warn("Invalid LORA", m);
|
||||||
|
} else {
|
||||||
|
loras.push({ name: s[0], weight });
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const l of loras) {
|
||||||
|
const loraNode = LiteGraph.createNode("LoraLoader");
|
||||||
|
graph.add(loraNode);
|
||||||
|
setWidgetValue(loraNode, "lora_name", l.name, true);
|
||||||
|
setWidgetValue(loraNode, "strength_model", l.weight);
|
||||||
|
setWidgetValue(loraNode, "strength_clip", l.weight);
|
||||||
|
prevModel.node.connect(prevModel.index, loraNode, 0);
|
||||||
|
prevClip.node.connect(prevClip.index, loraNode, 1);
|
||||||
|
prevModel = { node: loraNode, index: 0 };
|
||||||
|
prevClip = { node: loraNode, index: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
prevClip.node.connect(1, clipNode, 0);
|
||||||
|
prevModel.node.connect(0, samplerNode, 0);
|
||||||
|
if (hrSamplerNode) {
|
||||||
|
prevModel.node.connect(0, hrSamplerNode, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text, prevModel, prevClip };
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceEmbeddings(text: string) {
|
||||||
|
return text.replaceAll(
|
||||||
|
new RegExp(
|
||||||
|
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
|
||||||
|
"ig"
|
||||||
|
),
|
||||||
|
"embedding:$1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function popOpt(name: string) {
|
||||||
|
const v = opts[name];
|
||||||
|
delete opts[name];
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.clear();
|
||||||
|
graph.add(ckptNode);
|
||||||
|
graph.add(clipSkipNode);
|
||||||
|
graph.add(positiveNode);
|
||||||
|
graph.add(negativeNode);
|
||||||
|
graph.add(samplerNode);
|
||||||
|
graph.add(imageNode);
|
||||||
|
graph.add(vaeNode);
|
||||||
|
graph.add(vaeLoaderNode);
|
||||||
|
graph.add(saveNode);
|
||||||
|
|
||||||
|
ckptNode.connect(1, clipSkipNode, 0);
|
||||||
|
clipSkipNode.connect(0, positiveNode, 0);
|
||||||
|
clipSkipNode.connect(0, negativeNode, 0);
|
||||||
|
ckptNode.connect(0, samplerNode, 0);
|
||||||
|
positiveNode.connect(0, samplerNode, 1);
|
||||||
|
negativeNode.connect(0, samplerNode, 2);
|
||||||
|
imageNode.connect(0, samplerNode, 3);
|
||||||
|
vaeNode.connect(0, saveNode, 0);
|
||||||
|
samplerNode.connect(0, vaeNode, 0);
|
||||||
|
vaeLoaderNode.connect(0, vaeNode, 1);
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
model(v: string) {
|
||||||
|
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
||||||
|
},
|
||||||
|
"cfg scale"(v: number) {
|
||||||
|
setWidgetValue(samplerNode, "cfg", +v);
|
||||||
|
},
|
||||||
|
"clip skip"(v: number) {
|
||||||
|
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
|
||||||
|
},
|
||||||
|
sampler(v: string) {
|
||||||
|
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
|
||||||
|
if (name.includes("karras")) {
|
||||||
|
name = name.replace("karras", "").replace(/_+$/, "");
|
||||||
|
setWidgetValue(samplerNode, "scheduler", "karras");
|
||||||
|
} else {
|
||||||
|
setWidgetValue(samplerNode, "scheduler", "normal");
|
||||||
|
}
|
||||||
|
const w = getWidget(samplerNode, "sampler_name");
|
||||||
|
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
|
||||||
|
if (o) {
|
||||||
|
setWidgetValue(samplerNode, "sampler_name", o);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size(v: string) {
|
||||||
|
const wxh = v.split("x");
|
||||||
|
const w = ceil64(+wxh[0]);
|
||||||
|
const h = ceil64(+wxh[1]);
|
||||||
|
const hrUp = popOpt("hires upscale");
|
||||||
|
const hrSz = popOpt("hires resize");
|
||||||
|
let hrMethod = popOpt("hires upscaler");
|
||||||
|
|
||||||
|
setWidgetValue(imageNode, "width", w);
|
||||||
|
setWidgetValue(imageNode, "height", h);
|
||||||
|
|
||||||
|
if (hrUp || hrSz) {
|
||||||
|
let uw, uh;
|
||||||
|
if (hrUp) {
|
||||||
|
uw = w * hrUp;
|
||||||
|
uh = h * hrUp;
|
||||||
|
} else {
|
||||||
|
const s = hrSz.split("x");
|
||||||
|
uw = +s[0];
|
||||||
|
uh = +s[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let upscaleNode: LGraphNode;
|
||||||
|
let latentNode: LGraphNode;
|
||||||
|
|
||||||
|
if (hrMethod.startsWith("Latent")) {
|
||||||
|
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
|
||||||
|
graph.add(upscaleNode);
|
||||||
|
samplerNode.connect(0, upscaleNode, 0);
|
||||||
|
|
||||||
|
switch (hrMethod) {
|
||||||
|
case "Latent (nearest-exact)":
|
||||||
|
hrMethod = "nearest-exact";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
|
||||||
|
} else {
|
||||||
|
const decode = LiteGraph.createNode("VAEDecodeTiled");
|
||||||
|
graph.add(decode);
|
||||||
|
samplerNode.connect(0, decode, 0);
|
||||||
|
vaeLoaderNode.connect(0, decode, 1);
|
||||||
|
|
||||||
|
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
|
||||||
|
graph.add(upscaleLoaderNode);
|
||||||
|
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
||||||
|
|
||||||
|
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
|
||||||
|
graph.add(modelUpscaleNode);
|
||||||
|
decode.connect(0, modelUpscaleNode, 1);
|
||||||
|
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
||||||
|
|
||||||
|
upscaleNode = LiteGraph.createNode("ImageScale");
|
||||||
|
graph.add(upscaleNode);
|
||||||
|
modelUpscaleNode.connect(0, upscaleNode, 0);
|
||||||
|
|
||||||
|
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
|
||||||
|
graph.add(vaeEncodeNode);
|
||||||
|
upscaleNode.connect(0, vaeEncodeNode, 0);
|
||||||
|
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWidgetValue(upscaleNode, "width", ceil64(uw));
|
||||||
|
setWidgetValue(upscaleNode, "height", ceil64(uh));
|
||||||
|
|
||||||
|
hrSamplerNode = LiteGraph.createNode("KSampler");
|
||||||
|
graph.add(hrSamplerNode);
|
||||||
|
ckptNode.connect(0, hrSamplerNode, 0);
|
||||||
|
positiveNode.connect(0, hrSamplerNode, 1);
|
||||||
|
negativeNode.connect(0, hrSamplerNode, 2);
|
||||||
|
latentNode.connect(0, hrSamplerNode, 3);
|
||||||
|
hrSamplerNode.connect(0, vaeNode, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
steps(v: number) {
|
||||||
|
setWidgetValue(samplerNode, "steps", +v);
|
||||||
|
},
|
||||||
|
seed(v: number) {
|
||||||
|
setWidgetValue(samplerNode, "seed", +v);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const opt in opts) {
|
||||||
|
if (opt in handlers) {
|
||||||
|
handlers[opt](popOpt(opt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hrSamplerNode) {
|
||||||
|
setWidgetValue(hrSamplerNode, "steps", getWidget(samplerNode, "steps").value);
|
||||||
|
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
|
||||||
|
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
|
||||||
|
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
|
||||||
|
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
|
||||||
|
positive = n.text;
|
||||||
|
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
|
||||||
|
negative = n.text;
|
||||||
|
|
||||||
|
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
|
||||||
|
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
|
||||||
|
|
||||||
|
graph.arrange();
|
||||||
|
|
||||||
|
for (const opt of ["model hash", "ensd"]) {
|
||||||
|
delete opts[opt];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("Unhandled parameters:", opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/lib/widgets.ts
Normal file
137
src/lib/widgets.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { IWidget, LGraphNode } from "litegraph.js";
|
||||||
|
import type ComfyApp from "$lib/components/ComfyApp";
|
||||||
|
|
||||||
|
interface WidgetData {
|
||||||
|
widget: IWidget,
|
||||||
|
minWidth?: number,
|
||||||
|
minHeight?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp) => WidgetData;
|
||||||
|
|
||||||
|
|
||||||
|
type NumberConfig = { min: number, max: number, step: number, precision: number }
|
||||||
|
type NumberDefaults = { val: number, config: NumberConfig }
|
||||||
|
|
||||||
|
function getNumberDefaults(inputData: any, defaultStep: number): NumberDefaults {
|
||||||
|
let defaultVal = inputData[1]["default"];
|
||||||
|
let { min, max, step } = inputData[1];
|
||||||
|
|
||||||
|
if (defaultVal == undefined) defaultVal = 0;
|
||||||
|
if (min == undefined) min = 0;
|
||||||
|
if (max == undefined) max = 2048;
|
||||||
|
if (step == undefined) step = defaultStep;
|
||||||
|
|
||||||
|
return { val: defaultVal, config: { min, max, step: 10.0 * step, precision: 0 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||||
|
const { val, config } = getNumberDefaults(inputData, 0.5);
|
||||||
|
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||||
|
const { val, config } = getNumberDefaults(inputData, 1);
|
||||||
|
return {
|
||||||
|
widget: node.addWidget(
|
||||||
|
"number",
|
||||||
|
inputName,
|
||||||
|
val,
|
||||||
|
function (v) {
|
||||||
|
const s = this.options.step / 10;
|
||||||
|
this.value = Math.round(v / s) * s;
|
||||||
|
},
|
||||||
|
config
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp): WidgetData => {
|
||||||
|
const defaultVal = inputData[1].default || "";
|
||||||
|
const multiline = !!inputData[1].multiline;
|
||||||
|
|
||||||
|
// if (multiline) {
|
||||||
|
// return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||||
|
// } else {
|
||||||
|
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||||
|
const type = inputData[0];
|
||||||
|
let defaultValue = type[0];
|
||||||
|
if (inputData[1] && inputData[1].default) {
|
||||||
|
defaultValue = inputData[1].default;
|
||||||
|
}
|
||||||
|
return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app): WidgetData => {
|
||||||
|
const imageWidget = node.widgets.find((w) => w.name === "image");
|
||||||
|
let uploadWidget: IWidget;
|
||||||
|
|
||||||
|
// async function uploadFile(file: File, updateNode: boolean) {
|
||||||
|
// try {
|
||||||
|
// // Wrap file in formdata so it includes filename
|
||||||
|
// const body = new FormData();
|
||||||
|
// body.append("image", file);
|
||||||
|
// const resp = await fetch("/upload/image", {
|
||||||
|
// method: "POST",
|
||||||
|
// body,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (resp.status === 200) {
|
||||||
|
// const data = await resp.json();
|
||||||
|
// // Add the file as an option and update the widget value
|
||||||
|
// if (!imageWidget.options.values.includes(data.name)) {
|
||||||
|
// imageWidget.options.values.push(data.name);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (updateNode) {
|
||||||
|
// // showImage(data.name);
|
||||||
|
// imageWidget.value = data.name;
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// alert(resp.status + " - " + resp.statusText);
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// alert(error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const fileInput = document.createElement("input");
|
||||||
|
// Object.assign(fileInput, {
|
||||||
|
// type: "file",
|
||||||
|
// accept: "image/jpeg,image/png",
|
||||||
|
// style: "display: none",
|
||||||
|
// onchange: async () => {
|
||||||
|
// if (fileInput.files.length) {
|
||||||
|
// await uploadFile(fileInput.files[0], true);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// document.body.append(fileInput);
|
||||||
|
|
||||||
|
// Create the button widget for selecting the files
|
||||||
|
uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
|
||||||
|
// fileInput.click();
|
||||||
|
});
|
||||||
|
uploadWidget.options = { serialize: false };
|
||||||
|
|
||||||
|
return { widget: uploadWidget };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type WidgetRepository = Record<string, WidgetFactory>
|
||||||
|
|
||||||
|
export const ComfyWidgets: WidgetRepository = {
|
||||||
|
"INT:seed": INT,
|
||||||
|
"INT:noise_seed": INT,
|
||||||
|
FLOAT,
|
||||||
|
INT,
|
||||||
|
STRING,
|
||||||
|
COMBO,
|
||||||
|
IMAGEUPLOAD,
|
||||||
|
}
|
||||||
5
src/routes/+page.svelte
Normal file
5
src/routes/+page.svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ComfyApp from "$lib/components/ComfyApp.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ComfyApp/>
|
||||||
16
src/routes/+page.ts
Normal file
16
src/routes/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { PageLoad } from "./$types"
|
||||||
|
|
||||||
|
// `PageServerData` will contain everything from the layouts and also the
|
||||||
|
// `data` from the `+page.server.ts` file.
|
||||||
|
type OutputProps = {}
|
||||||
|
|
||||||
|
// We have imported the `PageLoad` type from the relative `./$types` folder that
|
||||||
|
// is hidden in the generated `.svelte-kit` folder. Those generated types
|
||||||
|
// contain a `PageLoad` type with a `params` and `data` object that matches our route.
|
||||||
|
// You need to run the dev server or `svelte-kit sync` to generate them.
|
||||||
|
export const load: PageLoad<OutputProps> = async ({
|
||||||
|
params,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
1575
src/types/litegraph.js/litegraph.d.ts
vendored
Normal file
1575
src/types/litegraph.js/litegraph.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
22
svelte.config.js
Normal file
22
svelte.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
},
|
||||||
|
preprocess: [
|
||||||
|
sveltePreprocess({
|
||||||
|
typescript: {
|
||||||
|
compilerOptions: {
|
||||||
|
debug: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"exclude": ["node_modules/litegraph.js/src/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"typeRoots": [
|
||||||
|
"types",
|
||||||
|
"../node_modules/@types"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"$lib": ["../src/lib"],
|
||||||
|
"$lib/*": ["../src/lib/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
vite.config.ts
Normal file
16
vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import FullReload from 'vite-plugin-full-reload'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
sveltekit(),
|
||||||
|
FullReload(["src/**/*.{js,ts,svelte}"])
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user