Cnet (#22)
* ini * remove shit * Create control_model.py * i * i * Update controlnet_supported.py * Update controlnet_supported.py * Update controlnet_supported.py * i * i * Update controlnet_supported.py * i * Update controlnet_supported.py * remove shits * remove shit * Update global_state.py * i * i * Update legacy_preprocessors.py * Update legacy_preprocessors.py * remove shit * Update batch_hijack.py * remove shit * remove shit * i * i * i * Update external_code.py * Update global_state.py * Update infotext.py * Update utils.py * Update external_code.py * i * i * i * Update controlnet_ui_group.py * remove shit * remove shit * i * Update controlnet.py * Update controlnet.py * Update controlnet.py * Update controlnet.py * Update controlnet.py * i * Update global_state.py * Update global_state.py * i * Update global_state.py * Update global_state.py * Update global_state.py * Update global_state.py * Update controlnet_ui_group.py * i * Update global_state.py * Update controlnet_ui_group.py * Update controlnet_ui_group.py * i * Update controlnet_ui_group.py * Update controlnet_ui_group.py * Update controlnet_ui_group.py * Update controlnet_ui_group.py
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
(function () {
|
||||
/*
|
||||
MIT LICENSE
|
||||
Copyright 2011 Jon Leighton
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
// From: https://gist.github.com/jonleighton/958841
|
||||
function base64ArrayBuffer(arrayBuffer) {
|
||||
var base64 = ''
|
||||
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
var bytes = new Uint8Array(arrayBuffer)
|
||||
var byteLength = bytes.byteLength
|
||||
var byteRemainder = byteLength % 3
|
||||
var mainLength = byteLength - byteRemainder
|
||||
|
||||
var a, b, c, d
|
||||
var chunk
|
||||
|
||||
// Main loop deals with bytes in chunks of 3
|
||||
for (var i = 0; i < mainLength; i = i + 3) {
|
||||
// Combine the three bytes into a single integer
|
||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
|
||||
|
||||
// Use bitmasks to extract 6-bit segments from the triplet
|
||||
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
|
||||
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
|
||||
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
|
||||
d = chunk & 63 // 63 = 2^6 - 1
|
||||
|
||||
// Convert the raw binary segments to the appropriate ASCII encoding
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
|
||||
}
|
||||
|
||||
// Deal with the remaining bytes and padding
|
||||
if (byteRemainder == 1) {
|
||||
chunk = bytes[mainLength]
|
||||
|
||||
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
|
||||
|
||||
// Set the 4 least significant bits to zero
|
||||
b = (chunk & 3) << 4 // 3 = 2^2 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + '=='
|
||||
} else if (byteRemainder == 2) {
|
||||
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
|
||||
|
||||
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
|
||||
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
|
||||
|
||||
// Set the 2 least significant bits to zero
|
||||
c = (chunk & 15) << 2 // 15 = 2^4 - 1
|
||||
|
||||
base64 += encodings[a] + encodings[b] + encodings[c] + '='
|
||||
}
|
||||
|
||||
return base64
|
||||
}
|
||||
|
||||
// Turn a base64 string into a blob.
|
||||
// From https://gist.github.com/gauravmehla/7a7dfd87dd7d1b13697b6e894426615f
|
||||
function b64toBlob(b64Data, contentType, sliceSize) {
|
||||
var contentType = contentType || '';
|
||||
var sliceSize = sliceSize || 512;
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
var byteNumbers = new Array(slice.length);
|
||||
for (var i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
}
|
||||
|
||||
function createBlackImageBase64(width, height) {
|
||||
// Create a canvas element
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Get the context of the canvas
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
// Fill the canvas with black color
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Get the base64 encoded string
|
||||
var base64Image = canvas.toDataURL('image/png');
|
||||
|
||||
return base64Image;
|
||||
}
|
||||
|
||||
// Functions to be called within photopea context.
|
||||
// Start of photopea functions
|
||||
function pasteImage(base64image) {
|
||||
app.open(base64image, null, /* asSmart */ true);
|
||||
app.echoToOE("success");
|
||||
}
|
||||
|
||||
function setLayerNames(names) {
|
||||
const layers = app.activeDocument.layers;
|
||||
if (layers.length !== names.length) {
|
||||
console.error("layer length does not match names length");
|
||||
echoToOE("error");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const layer = layers[i];
|
||||
layer.name = names[i];
|
||||
}
|
||||
app.echoToOE("success");
|
||||
}
|
||||
|
||||
function removeLayersWithNames(names) {
|
||||
const layers = app.activeDocument.layers;
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
if (names.includes(layer.name)) {
|
||||
layer.remove();
|
||||
}
|
||||
}
|
||||
app.echoToOE("success");
|
||||
}
|
||||
|
||||
function getAllLayerNames() {
|
||||
const layers = app.activeDocument.layers;
|
||||
const names = [];
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
names.push(layer.name);
|
||||
}
|
||||
app.echoToOE(JSON.stringify(names));
|
||||
}
|
||||
|
||||
// Hides all layers except the current one, outputs the whole image, then restores the previous
|
||||
// layers state.
|
||||
function exportSelectedLayerOnly(format, layerName) {
|
||||
// Gets all layers recursively, including the ones inside folders.
|
||||
function getAllArtLayers(document) {
|
||||
let allArtLayers = [];
|
||||
|
||||
for (let i = 0; i < document.layers.length; i++) {
|
||||
const currentLayer = document.layers[i];
|
||||
allArtLayers.push(currentLayer);
|
||||
if (currentLayer.typename === "LayerSet") {
|
||||
allArtLayers = allArtLayers.concat(getAllArtLayers(currentLayer));
|
||||
}
|
||||
}
|
||||
return allArtLayers;
|
||||
}
|
||||
|
||||
function makeLayerVisible(layer) {
|
||||
let currentLayer = layer;
|
||||
while (currentLayer != app.activeDocument) {
|
||||
currentLayer.visible = true;
|
||||
if (currentLayer.parent.typename != 'Document') {
|
||||
currentLayer = currentLayer.parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const allLayers = getAllArtLayers(app.activeDocument);
|
||||
// Make all layers except the currently selected one invisible, and store
|
||||
// their initial state.
|
||||
const layerStates = [];
|
||||
for (let i = 0; i < allLayers.length; i++) {
|
||||
const layer = allLayers[i];
|
||||
layerStates.push(layer.visible);
|
||||
}
|
||||
// Hide all layers to begin with
|
||||
for (let i = 0; i < allLayers.length; i++) {
|
||||
const layer = allLayers[i];
|
||||
layer.visible = false;
|
||||
}
|
||||
for (let i = 0; i < allLayers.length; i++) {
|
||||
const layer = allLayers[i];
|
||||
const selected = layer.name === layerName;
|
||||
if (selected) {
|
||||
makeLayerVisible(layer);
|
||||
}
|
||||
}
|
||||
app.activeDocument.saveToOE(format);
|
||||
|
||||
for (let i = 0; i < allLayers.length; i++) {
|
||||
const layer = allLayers[i];
|
||||
layer.visible = layerStates[i];
|
||||
}
|
||||
}
|
||||
|
||||
function hasActiveDocument() {
|
||||
app.echoToOE(app.documents.length > 0 ? "true" : "false");
|
||||
}
|
||||
// End of photopea functions
|
||||
|
||||
const MESSAGE_END_ACK = "done";
|
||||
const MESSAGE_ERROR = "error";
|
||||
const PHOTOPEA_URL = "https://www.photopea.com/";
|
||||
class PhotopeaContext {
|
||||
constructor(photopeaIframe) {
|
||||
this.photopeaIframe = photopeaIframe;
|
||||
this.timeout = 1000;
|
||||
}
|
||||
|
||||
navigateIframe() {
|
||||
const iframe = this.photopeaIframe;
|
||||
const editorURL = PHOTOPEA_URL;
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
if (iframe.src !== editorURL) {
|
||||
iframe.src = editorURL;
|
||||
// Stop waiting after 10s.
|
||||
setTimeout(resolve, 10000);
|
||||
|
||||
// Testing whether photopea is able to accept message.
|
||||
while (true) {
|
||||
try {
|
||||
await this.invoke(hasActiveDocument);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log("Keep waiting for photopea to accept message.");
|
||||
}
|
||||
}
|
||||
this.timeout = 5000; // Restore to a longer timeout in normal messaging.
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// From https://github.com/huchenlei/stable-diffusion-ps-pea/blob/main/src/Photopea.ts
|
||||
postMessageToPhotopea(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const responseDataPieces = [];
|
||||
let hasError = false;
|
||||
const photopeaMessageHandle = (event) => {
|
||||
if (event.source !== this.photopeaIframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
// Filter out the ping messages
|
||||
if (typeof event.data === 'string' && event.data.includes('MSFAPI#')) {
|
||||
return;
|
||||
}
|
||||
// Ignore "done" when no data has been received. The "done" can come from
|
||||
// MSFAPI ping.
|
||||
if (event.data === MESSAGE_END_ACK && responseDataPieces.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (event.data === MESSAGE_END_ACK) {
|
||||
window.removeEventListener("message", photopeaMessageHandle);
|
||||
if (hasError) {
|
||||
reject('Photopea Error.');
|
||||
} else {
|
||||
resolve(responseDataPieces.length === 1 ? responseDataPieces[0] : responseDataPieces);
|
||||
}
|
||||
} else if (event.data === MESSAGE_ERROR) {
|
||||
responseDataPieces.push(event.data);
|
||||
hasError = true;
|
||||
} else {
|
||||
responseDataPieces.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", photopeaMessageHandle);
|
||||
setTimeout(() => reject("Photopea message timeout"), this.timeout);
|
||||
this.photopeaIframe.contentWindow.postMessage(message, "*");
|
||||
});
|
||||
}
|
||||
|
||||
// From https://github.com/huchenlei/stable-diffusion-ps-pea/blob/main/src/Photopea.ts
|
||||
async invoke(func, ...args) {
|
||||
await this.navigateIframe();
|
||||
const message = `${func.toString()} ${func.name}(${args.map(arg => JSON.stringify(arg)).join(',')});`;
|
||||
try {
|
||||
return await this.postMessageToPhotopea(message);
|
||||
} catch (e) {
|
||||
throw `Failed to invoke ${func.name}. ${e}.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch detected maps from each ControlNet units.
|
||||
* Create a new photopea document.
|
||||
* Add those detected maps to the created document.
|
||||
*/
|
||||
async fetchFromControlNet(tabs) {
|
||||
if (tabs.length === 0) return;
|
||||
const isImg2Img = tabs[0].querySelector('.cnet-unit-enabled').id.includes('img2img');
|
||||
const generationType = isImg2Img ? 'img2img' : 'txt2img';
|
||||
const width = gradioApp().querySelector(`#${generationType}_width input[type=number]`).value;
|
||||
const height = gradioApp().querySelector(`#${generationType}_height input[type=number]`).value;
|
||||
|
||||
const layerNames = ["background"];
|
||||
await this.invoke(pasteImage, createBlackImageBase64(width, height));
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
for (const [i, tab] of tabs.entries()) {
|
||||
const generatedImage = tab.querySelector('.cnet-generated-image-group .cnet-image img');
|
||||
if (!generatedImage) continue;
|
||||
await this.invoke(pasteImage, generatedImage.src);
|
||||
// Wait 200ms for pasting to fully complete so that we do not ended up with 2 separate
|
||||
// documents.
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
layerNames.push(`unit-${i}`);
|
||||
}
|
||||
await this.invoke(removeLayersWithNames, layerNames);
|
||||
await this.invoke(setLayerNames, layerNames.reverse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the images in the active photopea document back to each ControlNet units.
|
||||
*/
|
||||
async sendToControlNet(tabs) {
|
||||
// Gradio's image widgets are inputs. To set the image in one, we set the image on the input and
|
||||
// force it to refresh.
|
||||
function setImageOnInput(imageInput, file) {
|
||||
// Createa a data transfer element to set as the data in the input.
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
const list = dt.files;
|
||||
|
||||
// Actually set the image in the image widget.
|
||||
imageInput.files = list;
|
||||
|
||||
// Foce the image widget to update with the new image, after setting its source files.
|
||||
const event = new Event('change', {
|
||||
'bubbles': true,
|
||||
"composed": true
|
||||
});
|
||||
imageInput.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function sendToControlNetUnit(b64Image, index) {
|
||||
const tab = tabs[index];
|
||||
// Upload image to output image element.
|
||||
const outputImage = tab.querySelector('.cnet-photopea-output');
|
||||
const outputImageUpload = outputImage.querySelector('input[type="file"]');
|
||||
setImageOnInput(outputImageUpload, new File([b64toBlob(b64Image, "image/png")], "photopea_output.png"));
|
||||
|
||||
// Make sure `UsePreviewAsInput` checkbox is checked.
|
||||
const checkbox = tab.querySelector('.cnet-preview-as-input input[type="checkbox"]');
|
||||
if (!checkbox.checked) {
|
||||
checkbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
const layerNames =
|
||||
JSON.parse(await this.invoke(getAllLayerNames))
|
||||
.filter(name => /unit-\d+/.test(name));
|
||||
|
||||
for (const layerName of layerNames) {
|
||||
const arrayBuffer = await this.invoke(exportSelectedLayerOnly, 'PNG', layerName);
|
||||
const b64Image = base64ArrayBuffer(arrayBuffer);
|
||||
const layerIndex = Number.parseInt(layerName.split('-')[1]);
|
||||
sendToControlNetUnit(b64Image, layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let photopeaWarningShown = false;
|
||||
|
||||
function firstTimeUserPrompt() {
|
||||
if (opts.controlnet_photopea_warning){
|
||||
const photopeaPopupMsg = "you are about to connect to https://photopea.com\n" +
|
||||
"- Click OK: proceed.\n" +
|
||||
"- Click Cancel: abort.\n" +
|
||||
"Photopea integration can be disabled in Settings > ControlNet > Disable photopea edit.\n" +
|
||||
"This popup can be disabled in Settings > ControlNet > Photopea popup warning.";
|
||||
if (photopeaWarningShown || confirm(photopeaPopupMsg)) photopeaWarningShown = true;
|
||||
else return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const cnetRegisteredAccordions = new Set();
|
||||
function loadPhotopea() {
|
||||
function registerCallbacks(accordion) {
|
||||
const photopeaMainTrigger = accordion.querySelector('.cnet-photopea-main-trigger');
|
||||
// Photopea edit feature disabled.
|
||||
if (!photopeaMainTrigger) {
|
||||
console.log("ControlNet photopea edit disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const closeModalButton = accordion.querySelector('.cnet-photopea-edit .cnet-modal-close');
|
||||
const tabs = accordion.querySelectorAll('.cnet-unit-tab');
|
||||
const photopeaIframe = accordion.querySelector('.photopea-iframe');
|
||||
const photopeaContext = new PhotopeaContext(photopeaIframe, tabs);
|
||||
|
||||
tabs.forEach(tab => {
|
||||
const photopeaChildTrigger = tab.querySelector('.cnet-photopea-child-trigger');
|
||||
photopeaChildTrigger.addEventListener('click', async () => {
|
||||
if (!firstTimeUserPrompt()) return;
|
||||
|
||||
photopeaMainTrigger.click();
|
||||
if (await photopeaContext.invoke(hasActiveDocument) === "false") {
|
||||
await photopeaContext.fetchFromControlNet(tabs);
|
||||
}
|
||||
});
|
||||
});
|
||||
accordion.querySelector('.photopea-fetch').addEventListener('click', () => photopeaContext.fetchFromControlNet(tabs));
|
||||
accordion.querySelector('.photopea-send').addEventListener('click', () => {
|
||||
photopeaContext.sendToControlNet(tabs)
|
||||
closeModalButton.click();
|
||||
});
|
||||
}
|
||||
|
||||
const accordions = gradioApp().querySelectorAll('#controlnet');
|
||||
accordions.forEach(accordion => {
|
||||
if (cnetRegisteredAccordions.has(accordion)) return;
|
||||
registerCallbacks(accordion);
|
||||
cnetRegisteredAccordions.add(accordion);
|
||||
});
|
||||
}
|
||||
|
||||
onUiUpdate(loadPhotopea);
|
||||
})();
|
||||
Reference in New Issue
Block a user