Files
Graphite/frontend/src/utility-functions/imaginate.ts
Dennis Kobert b55e233fff Update GPU execution and quantization to new node system (#1070)
* Update GPU and quantization to new node system

Squashed commit of the following:

commit 3b69bdafed79f0bb1279609537a8eeead3f06830
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Sun Mar 5 11:37:17 2023 +0100

    Disable dev tools by default

commit dbbbedd68e48d1162442574ad8877c9922d40e4a
Merge: b1018eb5 a8f6e11e
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Sun Mar 5 10:45:00 2023 +0100

    Merge branch 'vite' into tauri-restructure-lite

commit b1018eb5ee56c2d23f9d5a4f034608ec684bd746
Merge: 3195833e 0512cb24
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 17:06:21 2023 +0100

    Merge branch 'master' into tauri-restructure-lite

commit 3195833e4088a4ed7984955c72617b27b7e39bfc
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 17:06:02 2023 +0100

    Bump number of samples

commit 3e57e1e3280759cf4f75726635e31d2b8e9387f9
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 16:55:52 2023 +0100

    Move part of quantization code to gcore

commit 10c15b0bc6ffb51e2bf2d94cd4eb0e24d761fb6f
Merge: 2b3db45a 8fe8896c
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 14:28:56 2023 +0100

    Merge remote-tracking branch 'origin/master' into tauri-restructure-lite

commit 2b3db45aee44a20660f0b1204666bb81e5a7e4b6
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 14:17:11 2023 +0100

    Fix types in node registry

commit 9122f35c0ba9a86255709680d744a48d3c7dcac4
Merge: 26eefc43 2cf4ee0f
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Fri Mar 3 01:04:55 2023 +0100

    Merge remote-tracking branch 'origin/master' into tauri-restructure-lite

commit 26eefc437eaad873f8d38fdb1fae0a1e3ec189e4
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Mar 2 23:05:53 2023 +0100

    Add Quantize node to document_node_types

commit 3f7606a91329200b2c025010d4a0cffee840a11c
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Mar 2 17:47:51 2023 +0100

    Add quantization nodes to node registry

commit 22d8e477ef79eef5b57b1dc9805e41bbf81cae43
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Mar 2 17:13:28 2023 +0100

    Introduce scopes (#1053)

    * Implement let binding

    * Add lambda inputs

    * Fix tests

    * Fix proto network formatting

    * Generate a template Scoped network by default

    * Add comment to explain the lambda parameter

    * Move binding wrapping out of the template

    * Fix errors cause by image frames

commit 9e0c29d92a164d4a4063e93480e1e289ef5243fe
Author: Alexandru Ică <alexandru@seyhanlee.com>
Date:   Thu Mar 2 15:55:10 2023 +0200

    Make use of ImageFrame in the node system more extensively (#1055) (#1062)

    Make the node system use ImageFrame more extensively (#1055)

commit 5912ef9a1a807917eeb90c1f4835bd8a5de9c821
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Wed Mar 1 16:15:21 2023 +0100

    Split quantization into multiple nodes

commit 285d7b76c176b3e2679ea24eecb38ef867a79f3b
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Mon Feb 27 12:35:57 2023 +0100

    Fix gpu support

commit e0b6327eebba8caf7545c4fedc6670abc4c3652e
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Feb 16 22:08:53 2023 +0100

    Don't watch frontend files when using tauri

commit 58ae146f6da935cfd37afbd25e1c331b615252da
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Feb 16 21:48:54 2023 +0100

    Migrate vue code base to vite

commit f996390cc312618a60f98ccb9cd515f1bae5006d
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Feb 16 19:34:33 2023 +0100

    Start migrating vue to use vite

commit 29d752f47cfd1c74ee51fac6f3d75557a378471c
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Feb 16 19:00:53 2023 +0100

    Kill cargo watch process automatically

commit 4d1c76b07acadbf609dbab7d57d9a7769b81d4b5
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Feb 16 17:37:27 2023 +0100

    Start playing around with vite infrastructure

commit 8494f5e9227aa433fd5ca75b268a6a96b2706b36
Author: Locria Cyber <74560659+locriacyber@users.noreply.github.com>
Date:   Thu Jan 19 18:40:46 2023 +0000

    Fix import style and eslint rules

commit 92490f7774a7351bb40091bcec78f79c28704768
Author: Locria Cyber <74560659+locriacyber@users.noreply.github.com>
Date:   Thu Jan 19 18:25:09 2023 +0000

    Fix icons

commit dc67821abad87f8ff780b12ae96668af2f7bb355
Author: Locria Cyber <74560659+locriacyber@users.noreply.github.com>
Date:   Thu Jan 19 18:20:48 2023 +0000

    Add license generator with rollup

commit 441e339d31b76dac4f91321d39a39900b5a79bc1
Author: Locria Cyber <74560659+locriacyber@users.noreply.github.com>
Date:   Thu Jan 19 18:14:22 2023 +0000

    Use eslint --fix to fix TS-in-svelte type imports. Now it compiles.

commit 2e847d339e7dcd51ed4c4677ed337c1e20636724
Author: Locria Cyber <74560659+locriacyber@users.noreply.github.com>
Date:   Thu Jan 19 17:31:49 2023 +0000

    Remove webpack and plugins

commit 3adab1b7f40ff17b91163e7ca47a403ef3c02fbc
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Thu Mar 2 16:10:19 2023 +0100

    Fix errors cause by image frames

commit 4e5f838995e213b4696225a473b9c56c0084e7a8
Author: Alexandru Ică <alexandru@seyhanlee.com>
Date:   Thu Mar 2 15:55:10 2023 +0200

    Make use of ImageFrame in the node system more extensively (#1055) (#1062)

    Make the node system use ImageFrame more extensively (#1055)

commit 1d4b0e29c693a53c068f1a30f0e857a9c1a59587
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Wed Mar 1 15:13:51 2023 +0100

    Update node graph guide readme with new syntax (#1061)

commit 6735d8c61f5709e22d2b22abd037bab417e868d6
Author: Rob Nadal <Robnadal44@gmail.com>
Date:   Tue Feb 28 18:59:06 2023 -0500

    Bezier-rs: Add function to smoothly join bezier curves (#1037)

    * Added bezier join

    * Stylistic changes per review

commit cd1d7aa7fbcce39fbbf7762d131ee16ad9cb46dd
Author: Dennis Kobert <dennis@kobert.dev>
Date:   Wed Feb 22 23:42:32 2023 +0100

    Implement let binding

    Add lambda inputs

    Fix tests

    Fix proto network formatting

    Generate a template Scoped network by default

    Add comment to explain the lambda parameter

    Move binding wrapping out of the template

* Update package-lock.json

* Regenerate package-lock.json and fix lint errors

* Readd git keep dir

* Revert change to panic.ts

* Fix clippy warnings

* Apply code review

* Clean up node_registry

* Fix test / spriv -> spirv typos
2023-03-05 13:22:14 +01:00

367 lines
13 KiB
TypeScript

/* eslint-disable camelcase */
// import { escapeJSON } from "@/utility-functions/escape";
import { blobToBase64 } from "@/utility-functions/files";
import { type RequestResult, requestWithUploadDownloadProgress } from "@/utility-functions/network";
import { type Editor } from "@/wasm-communication/editor";
import { type ImaginateGenerationParameters, type XY } from "@/wasm-communication/messages";
const MAX_POLLING_RETRIES = 4;
const SERVER_STATUS_CHECK_TIMEOUT = 5000;
const PROGRESS_EVERY_N_STEPS = 5;
let timer: NodeJS.Timeout | undefined;
let terminated = false;
let generatingAbortRequest: XMLHttpRequest | undefined;
let pollingAbortController = new AbortController();
let statusAbortController = new AbortController();
// PUBLICLY CALLABLE FUNCTIONS
export async function imaginateGenerate(
parameters: ImaginateGenerationParameters,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
hostname: string,
refreshFrequency: number,
documentId: bigint,
layerPath: BigUint64Array,
nodePath: BigUint64Array,
editor: Editor
): Promise<void> {
// Ignore a request to generate a new image while another is already being generated
if (generatingAbortRequest !== undefined) return;
terminated = false;
// Immediately set the progress to 0% so the backend knows to update its layout
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, 0, "Beginning");
// Initiate a request to the computation server
const discloseUploadingProgress = (progress: number): void => {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, progress * 100, "Uploading");
};
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, mask, maskPaintMode, maskBlurPx, maskFillContent, parameters);
generatingAbortRequest = xhr;
try {
// Wait until the request is fully uploaded, which could be slow if the img2img source is large and the user is on a slow connection
await uploaded;
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, 0, "Generating");
// Begin polling for updates to the in-progress image generation at the specified interval
// Don't poll if the chosen interval is 0, or if the chosen sampling method does not support polling
if (refreshFrequency > 0) {
const interval = Math.max(refreshFrequency * 1000, 500);
scheduleNextPollingUpdate(interval, Date.now(), 0, editor, hostname, documentId, layerPath, nodePath, parameters.resolution);
}
// Wait for the final image to be returned by the initial request containing either the full image or the last frame if it was terminated by the user
const { body, status } = await result;
if (status < 200 || status > 299) {
throw new Error(`Request to server failed to return a 200-level status code (${status})`);
}
// Extract the final image from the response and convert it to a data blob
const base64Data = JSON.parse(body)?.images?.[0] as string | undefined;
const base64 = typeof base64Data === "string" && base64Data.length > 0 ? `data:image/png;base64,${base64Data}` : undefined;
if (!base64) throw new Error("Could not read final image result from server response");
const blob = await (await fetch(base64)).blob();
// Send the backend an updated status
const percent = terminated ? undefined : 100;
const newStatus = terminated ? "Terminated" : "Idle";
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, percent, newStatus);
// Send the backend a blob URL for the final image
updateBackendImage(editor, blob, documentId, layerPath, nodePath);
} catch {
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminated");
await imaginateCheckConnection(hostname, editor);
}
abortAndResetGenerating();
abortAndResetPolling();
}
export async function imaginateTerminate(hostname: string, documentId: bigint, layerPath: BigUint64Array, nodePath: BigUint64Array, editor: Editor): Promise<void> {
terminated = true;
abortAndResetPolling();
try {
await terminate(hostname);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminating");
} catch {
abortAndResetGenerating();
abortAndResetPolling();
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, undefined, "Terminated");
await imaginateCheckConnection(hostname, editor);
}
}
export async function imaginateCheckConnection(hostname: string, editor: Editor): Promise<void> {
const serverReached = await checkConnection(hostname);
editor.instance.setImaginateServerStatus(serverReached);
}
// Converts the blob image into a list of pixels using an invisible canvas.
export async function updateBackendImage(editor: Editor, blob: Blob, documentId: bigint, layerPath: BigUint64Array, nodePath: BigUint64Array): Promise<void> {
const image = await createImageBitmap(blob);
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not create canvas context");
ctx.drawImage(image, 0, 0);
// Send the backend the blob data to be stored persistently in the layer
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const u8Array = new Uint8Array(imageData.data);
editor.instance.setImaginateImageData(documentId, layerPath, nodePath, u8Array, imageData.width, imageData.height);
}
// ABORTING AND RESETTING HELPERS
function abortAndResetGenerating(): void {
generatingAbortRequest?.abort();
generatingAbortRequest = undefined;
}
function abortAndResetPolling(): void {
pollingAbortController.abort();
pollingAbortController = new AbortController();
clearTimeout(timer);
}
// POLLING IMPLEMENTATION DETAILS
function scheduleNextPollingUpdate(
interval: number,
timeoutBegan: number,
pollingRetries: number,
editor: Editor,
hostname: string,
documentId: bigint,
layerPath: BigUint64Array,
nodePath: BigUint64Array,
resolution: XY
): void {
// Pick a future time that keeps to the user-requested interval if possible, but on slower connections will go as fast as possible without overlapping itself
const nextPollTimeGoal = timeoutBegan + interval;
const timeFromNow = Math.max(0, nextPollTimeGoal - Date.now());
timer = setTimeout(async () => {
const nextTimeoutBegan = Date.now();
try {
const [blob, percentComplete] = await pollImage(hostname);
// After waiting for the polling result back from the server, if during that intervening time the user has terminated the generation, exit so we don't overwrite that terminated status
if (terminated) return;
if (blob) updateBackendImage(editor, blob, documentId, layerPath, nodePath);
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, nodePath, percentComplete, "Generating");
scheduleNextPollingUpdate(interval, nextTimeoutBegan, 0, editor, hostname, documentId, layerPath, nodePath, resolution);
} catch {
if (generatingAbortRequest === undefined) return;
if (pollingRetries + 1 > MAX_POLLING_RETRIES) {
abortAndResetGenerating();
abortAndResetPolling();
await imaginateCheckConnection(hostname, editor);
} else {
scheduleNextPollingUpdate(interval, nextTimeoutBegan, pollingRetries + 1, editor, hostname, documentId, layerPath, nodePath, resolution);
}
}
}, timeFromNow);
}
// API COMMUNICATION FUNCTIONS
async function pollImage(hostname: string): Promise<[Blob | undefined, number]> {
// Fetch the percent progress and in-progress image from the API
const result = await fetch(`${hostname}sdapi/v1/progress`, { signal: pollingAbortController.signal, method: "GET" });
const { current_image, progress } = await result.json();
// Convert to a usable format
const progressPercent = progress * 100;
const base64 = typeof current_image === "string" && current_image.length > 0 ? `data:image/png;base64,${current_image}` : undefined;
// Deal with a missing image
if (!base64) {
// The image is not ready yet (because it's only had a few samples since generation began), but we do have a progress percentage
if (!Number.isNaN(progressPercent) && progressPercent >= 0 && progressPercent <= 100) {
return [undefined, progressPercent];
}
// Something else is wrong and the image wasn't provided as expected
return Promise.reject();
}
// The image was provided so we turn it into a data blob
const blob = await (await fetch(base64)).blob();
return [blob, progressPercent];
}
async function generate(
discloseUploadingProgress: (progress: number) => void,
hostname: string,
image: Blob | undefined,
mask: Blob | undefined,
maskPaintMode: string,
maskBlurPx: number,
maskFillContent: string,
parameters: ImaginateGenerationParameters
): Promise<{
uploaded: Promise<void>;
result: Promise<RequestResult>;
xhr?: XMLHttpRequest;
}> {
let body;
let endpoint;
if (image === undefined || parameters.denoisingStrength === undefined) {
endpoint = `${hostname}sdapi/v1/txt2img`;
body = {
// enable_hr: false,
// denoising_strength: 0,
// firstphase_width: 0,
// firstphase_height: 0,
prompt: parameters.prompt,
// styles: [],
seed: Number(parameters.seed),
// subseed: -1,
// subseed_strength: 0,
// seed_resize_from_h: -1,
// seed_resize_from_w: -1,
// batch_size: 1,
// n_iter: 1,
steps: parameters.samples,
cfg_scale: parameters.cfgScale,
width: parameters.resolution.x,
height: parameters.resolution.y,
restore_faces: parameters.restoreFaces,
tiling: parameters.tiling,
negative_prompt: parameters.negativePrompt,
// eta: 0,
// s_churn: 0,
// s_tmax: 0,
// s_tmin: 0,
// s_noise: 1,
override_settings: {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
},
sampler_index: parameters.samplingMethod,
};
} else {
const sourceImageBase64 = await blobToBase64(image);
const maskImageBase64 = mask ? await blobToBase64(mask) : "";
const maskFillContentIndexes = ["Fill", "Original", "LatentNoise", "LatentNothing"];
const maskFillContentIndexFound = maskFillContentIndexes.indexOf(maskFillContent);
const maskFillContentIndex = maskFillContentIndexFound === -1 ? undefined : maskFillContentIndexFound;
const maskInvert = maskPaintMode === "Inpaint" ? 1 : 0;
endpoint = `${hostname}sdapi/v1/img2img`;
body = {
init_images: [sourceImageBase64],
// resize_mode: 0,
denoising_strength: parameters.denoisingStrength,
mask: mask && maskImageBase64,
mask_blur: mask && maskBlurPx,
inpainting_fill: mask && maskFillContentIndex,
inpaint_full_res: mask && false,
// inpaint_full_res_padding: 0,
inpainting_mask_invert: mask && maskInvert,
prompt: parameters.prompt,
// styles: [],
seed: Number(parameters.seed),
// subseed: -1,
// subseed_strength: 0,
// seed_resize_from_h: -1,
// seed_resize_from_w: -1,
// batch_size: 1,
// n_iter: 1,
steps: parameters.samples,
cfg_scale: parameters.cfgScale,
width: parameters.resolution.x,
height: parameters.resolution.y,
restore_faces: parameters.restoreFaces,
tiling: parameters.tiling,
negative_prompt: parameters.negativePrompt,
// eta: 0,
// s_churn: 0,
// s_tmax: 0,
// s_tmin: 0,
// s_noise: 1,
override_settings: {
show_progress_every_n_steps: PROGRESS_EVERY_N_STEPS,
img2img_fix_steps: true,
},
sampler_index: parameters.samplingMethod,
// include_init_images: false,
};
}
// Prepare a promise that will resolve after the outbound request upload is complete
let uploadedResolve: () => void;
let uploadedReject: () => void;
const uploaded = new Promise<void>((resolve, reject): void => {
uploadedResolve = resolve;
uploadedReject = reject;
});
// Fire off the request and, once the outbound request upload is complete, resolve the promise we defined above
const uploadProgress = (progress: number): void => {
if (progress < 1) {
discloseUploadingProgress(progress);
} else {
uploadedResolve();
}
};
const [result, xhr] = requestWithUploadDownloadProgress(endpoint, "POST", JSON.stringify(body), uploadProgress, abortAndResetPolling);
result.catch(() => uploadedReject());
// Return the promise that resolves when the request upload is complete, the promise that resolves when the response download is complete, and the XHR so it can be aborted
return { uploaded, result, xhr };
}
async function terminate(hostname: string): Promise<void> {
await fetch(`${hostname}sdapi/v1/interrupt`, { method: "POST" });
}
async function checkConnection(hostname: string): Promise<boolean> {
statusAbortController.abort();
statusAbortController = new AbortController();
const timeout = setTimeout(() => statusAbortController.abort(), SERVER_STATUS_CHECK_TIMEOUT);
try {
// Intentionally misuse this API endpoint by using it just to check for a code 200 response, regardless of what the result is
const { status } = await fetch(`${hostname}sdapi/v1/progress?skip_current_image=true`, { signal: statusAbortController.signal, method: "GET" });
// This code means the server has indeed responded and the endpoint exists (otherwise it would be 404)
if (status === 200) {
clearTimeout(timeout);
return true;
}
} catch {
// Do nothing here
}
return false;
}