Integrate Stable Diffusion with the Imaginate layer (#784)

* Add AI Artist layer

* WIP add a button to download the rendered folder under an AI Artist layer

* Successfully download the correct image

* Break out image downloading JS into helper function

* Change file download from using data URLs to blob URLs

* WIP rasterize to blob

* Remove dimensions from AI Artist layer

* Successfully draw rasterized image on layer after calculation

* Working txt2img generation based on user prompt

* Add img2img and the main parameters

* Fix ability to rasterize multi-depth documents with blob URL images by switching them to base64

* Fix test

* Rasterize with artboard background color

* Allow aspect ratio stretch of AI Artist images

* Add automatic resolution choosing

* Add a terminate button, and make the lifecycle more robust

* Add negative prompt

* Add range bounds for parameter inputs

* Add seed

* Add tiling and restore faces

* Add server status check, server hostname customization, and resizing layer to fit AI Artist resolution

* Fix background color of infinite canvas rasterization

* Escape prompt text sent in the JSON

* Revoke blob URLs when cleared/replaced to reduce memory leak

* Fix welcome screen logo color

* Add PreferencesMessageHandler

* Add persistent storage of preferences

* Fix crash introduced in previous commit when moving mouse on page load

* Add tooltips to the AI Artist layer properties

* Integrate AI Artist tool into the raster section of the tool shelf

* Add a refresh button to the connection status

* Fix crash when generating and switching to a different document tab

* Add persistent image storage to AI Artist layers and fix duplication bugs

* Add a generate with random seed button

* Simplify and standardize message names

* Majorly improve robustness of networking code

* Fix race condition causing default server hostname to show disconnected when app loads with AI Artist layer selected (probably, not confirmed fixed)

* Clean up messages and function calls by changing arguments into structs

* Update API to more recent server commit

* Add support for picking the sampling method

* Add machinery for filtering selected layers with type

* Replace placeholder button icons

* Improve the random icon by tilting the dice

* Use selected_layers() instead of repeating that code

* Fix borrow error

* Change message flow in progress towards fixing #797

* Allow loading image on non-active document (fixes #797)

* Reduce code duplication with rasterization

* Add AI Artist tool and layer icons, and remove ugly node layer icon style

* Rename "AI Artist" codename to "Imaginate" feature name

Co-authored-by: otdavies <oliver@psyfer.io>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2022-10-18 22:33:27 -07:00
committed by GitHub
co-authored by otdavies 0hypercube
parent 06acd45a81
commit 30719bdc72
118 changed files with 3767 additions and 678 deletions
-20
View File
@@ -1,20 +0,0 @@
import { type Editor } from "@/wasm-communication/editor";
import { UpdateImageData } from "@/wasm-communication/messages";
export function createBlobManager(editor: Editor): void {
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.imageData.forEach(async (element) => {
// Using updateImageData.imageData.buffer returns undefined for some reason?
const buffer = new Uint8Array(element.imageData.values()).buffer;
const blob = new Blob([buffer], { type: element.mime });
// TODO: Call `URL.revokeObjectURL` at the appropriate time to avoid a memory leak
const blobURL = URL.createObjectURL(blob);
const image = await createImageBitmap(blob);
editor.instance.setImageBlobUrl(element.path, blobURL, image.width, image.height);
});
});
}
+2 -2
View File
@@ -25,8 +25,8 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
const widgets: WidgetLayout = {
layout: [
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, multiline: false }, 0n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, multiline: true }, 1n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" }, 0n)] },
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
],
layoutTarget: undefined,
};
+131 -75
View File
@@ -1,99 +1,155 @@
import { type PortfolioState } from "@/state-providers/portfolio";
import { stripIndents } from "@/utility-functions/strip-indents";
import { type Editor } from "@/wasm-communication/editor";
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument } from "@/wasm-communication/messages";
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument, TriggerSavePreferences, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences } from "@/wasm-communication/messages";
const GRAPHITE_INDEXED_DB_VERSION = 2;
const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db";
const GRAPHITE_AUTO_SAVE_STORE = "auto-save-documents";
const GRAPHITE_AUTO_SAVE_STORE = { name: "auto-save-documents", keyPath: "details.id" };
const GRAPHITE_EDITOR_PREFERENCES_STORE = { name: "editor-preferences", keyPath: "key" };
const GRAPHITE_INDEXEDDB_STORES = [GRAPHITE_AUTO_SAVE_STORE, GRAPHITE_EDITOR_PREFERENCES_STORE];
const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function createPersistenceManager(editor: Editor, portfolio: PortfolioState): Promise<() => void> {
function storeDocumentOrder(): void {
// Make sure to store as string since JSON does not play nice with BigInt
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
}
export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): () => void {
async function initialize(): Promise<IDBDatabase> {
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
return new Promise<IDBDatabase>((resolve) => {
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
async function removeDocument(id: string): Promise<void> {
const db = await databaseConnection;
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).delete(id);
storeDocumentOrder();
}
// Handle a version mismatch if `GRAPHITE_INDEXED_DB_VERSION` is now higher than what was saved in the database
dbOpenRequest.onupgradeneeded = (): void => {
const db = dbOpenRequest.result;
async function closeDatabaseConnection(): Promise<void> {
const db = await databaseConnection;
db.close();
}
// Wipe out all stores when a request is made to upgrade the database version to a newer one
GRAPHITE_INDEXEDDB_STORES.forEach((store) => {
if (db.objectStoreNames.contains(store.name)) db.deleteObjectStore(store.name);
// Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => {
const db = await databaseConnection;
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).put(autoSaveDocument);
storeDocumentOrder();
});
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
removeDocument(removeAutoSaveDocument.documentId);
});
db.createObjectStore(store.name, { keyPath: store.keyPath });
});
};
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
const databaseConnection = new Promise<IDBDatabase>((resolve) => {
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
dbOpenRequest.onupgradeneeded = (): void => {
const db = dbOpenRequest.result;
// Wipes out all auto-save data on upgrade
if (db.objectStoreNames.contains(GRAPHITE_AUTO_SAVE_STORE)) {
db.deleteObjectStore(GRAPHITE_AUTO_SAVE_STORE);
}
db.createObjectStore(GRAPHITE_AUTO_SAVE_STORE, { keyPath: "details.id" });
};
dbOpenRequest.onerror = (): void => {
const errorText = stripIndents`
// Handle some other error by presenting it to the user
dbOpenRequest.onerror = (): void => {
const errorText = stripIndents`
Documents won't be saved across reloads and later visits.
This may be caused by Firefox's private browsing mode.
Error on opening IndexDB:
${dbOpenRequest.error}
`;
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
};
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
};
dbOpenRequest.onsuccess = (): void => {
resolve(dbOpenRequest.result);
};
});
databaseConnection.then(async (db) => {
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readonly");
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).getAll();
await new Promise((resolve): void => {
request.onsuccess = (): void => {
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
const orderedSavedDocuments = documentOrder
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
orderedSavedDocuments.forEach((doc: TriggerIndexedDbWriteDocument) => {
if (doc.version === currentDocumentVersion) {
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
} else {
removeDocument(doc.details.id);
}
});
resolve(undefined);
// Resolve the promise on a successful opening of the database connection
dbOpenRequest.onsuccess = (): void => {
resolve(dbOpenRequest.result);
};
});
}
function storeDocumentOrder(): void {
// Make sure to store as string since JSON does not play nice with BigInt
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
}
async function removeDocument(id: string, db: IDBDatabase): Promise<void> {
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).delete(id);
storeDocumentOrder();
}
async function loadAutoSaveDocuments(db: IDBDatabase): Promise<void> {
let promiseResolve: (value: void | PromiseLike<void>) => void;
const promise = new Promise<void>((resolve): void => {
promiseResolve = resolve;
});
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readonly");
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).getAll();
request.onsuccess = (): void => {
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
const orderedSavedDocuments = documentOrder
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
orderedSavedDocuments.forEach(async (doc: TriggerIndexedDbWriteDocument) => {
if (doc.version === currentDocumentVersion) {
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
} else {
await removeDocument(doc.details.id, db);
}
});
promiseResolve();
};
await promise;
}
async function loadPreferences(db: IDBDatabase): Promise<void> {
let promiseResolve: (value: void | PromiseLike<void>) => void;
const promise = new Promise<void>((resolve): void => {
promiseResolve = resolve;
});
// Open auto-save documents
const transaction = db.transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readonly");
const request = transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).getAll();
request.onsuccess = (): void => {
const preferenceEntries: { key: string; value: unknown }[] = request.result;
const preferences: Record<string, unknown> = {};
preferenceEntries.forEach(({ key, value }) => {
preferences[key] = value;
});
editor.instance.loadPreferences(JSON.stringify(preferences));
promiseResolve();
};
await promise;
}
// Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => {
const transaction = (await databaseConnection).transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).put(autoSaveDocument);
storeDocumentOrder();
});
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
await removeDocument(removeAutoSaveDocument.documentId, await databaseConnection);
});
editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => {
await loadAutoSaveDocuments(await databaseConnection);
});
editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (preferences) => {
Object.entries(preferences.preferences).forEach(async ([key, value]) => {
const storedObject = { key, value };
const transaction = (await databaseConnection).transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readwrite");
transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).put(storedObject);
});
});
editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => {
await loadPreferences(await databaseConnection);
});
return closeDatabaseConnection;
const databaseConnection = initialize();
// Destructor
return () => {
databaseConnection.then((connection) => connection.close());
};
}