mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 23:38:06 +08:00
* 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: 3195833e0512cb24Author: 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: 2b3db45a8fe8896cAuthor: 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: 26eefc432cf4ee0fAuthor: 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
367 lines
13 KiB
TypeScript
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;
|
|
}
|