Spaces:
Runtime error
Runtime error
// Opt in to a restricted variant of JavaScript | |
const EXT_REGEX = /(?:\.([^.]+))?$/ | |
const TEXT_EXTENSIONS = ['txt', 'json'] | |
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga', 'webp'] | |
function parseBoolean(stringValue) { | |
if (typeof stringValue === 'boolean') { | |
return stringValue | |
} | |
if (typeof stringValue === 'number') { | |
return stringValue !== 0 | |
} | |
if (typeof stringValue !== 'string') { | |
return false | |
} | |
switch(stringValue?.toLowerCase()?.trim()) { | |
case "true": | |
case "yes": | |
case "on": | |
case "1": | |
return true; | |
case "false": | |
case "no": | |
case "off": | |
case "0": | |
case "none": | |
case null: | |
case undefined: | |
return false; | |
} | |
try { | |
return Boolean(JSON.parse(stringValue)); | |
} catch { | |
return Boolean(stringValue) | |
} | |
} | |
const TASK_MAPPING = { | |
prompt: { name: 'Prompt', | |
setUI: (prompt) => { | |
promptField.value = prompt | |
}, | |
readUI: () => promptField.value, | |
parse: (val) => val | |
}, | |
negative_prompt: { name: 'Negative Prompt', | |
setUI: (negative_prompt) => { | |
negativePromptField.value = negative_prompt | |
}, | |
readUI: () => negativePromptField.value, | |
parse: (val) => val | |
}, | |
active_tags: { name: "Image Modifiers", | |
setUI: (active_tags) => { | |
refreshModifiersState(active_tags) | |
}, | |
readUI: () => activeTags.map(x => x.name), | |
parse: (val) => val | |
}, | |
inactive_tags: { name: "Inactive Image Modifiers", | |
setUI: (inactive_tags) => { | |
refreshInactiveTags(inactive_tags) | |
}, | |
readUI: () => activeTags.filter(tag => tag.inactive === true).map(x => x.name), | |
parse: (val) => val | |
}, | |
width: { name: 'Width', | |
setUI: (width) => { | |
const oldVal = widthField.value | |
widthField.value = width | |
if (!widthField.value) { | |
widthField.value = oldVal | |
} | |
}, | |
readUI: () => parseInt(widthField.value), | |
parse: (val) => parseInt(val) | |
}, | |
height: { name: 'Height', | |
setUI: (height) => { | |
const oldVal = heightField.value | |
heightField.value = height | |
if (!heightField.value) { | |
heightField.value = oldVal | |
} | |
}, | |
readUI: () => parseInt(heightField.value), | |
parse: (val) => parseInt(val) | |
}, | |
seed: { name: 'Seed', | |
setUI: (seed) => { | |
if (!seed) { | |
randomSeedField.checked = true | |
seedField.disabled = true | |
seedField.value = 0 | |
return | |
} | |
randomSeedField.checked = false | |
seedField.disabled = false | |
seedField.value = seed | |
}, | |
readUI: () => parseInt(seedField.value), // just return the value the user is seeing in the UI | |
parse: (val) => parseInt(val) | |
}, | |
num_inference_steps: { name: 'Steps', | |
setUI: (num_inference_steps) => { | |
numInferenceStepsField.value = num_inference_steps | |
}, | |
readUI: () => parseInt(numInferenceStepsField.value), | |
parse: (val) => parseInt(val) | |
}, | |
guidance_scale: { name: 'Guidance Scale', | |
setUI: (guidance_scale) => { | |
guidanceScaleField.value = guidance_scale | |
updateGuidanceScaleSlider() | |
}, | |
readUI: () => parseFloat(guidanceScaleField.value), | |
parse: (val) => parseFloat(val) | |
}, | |
prompt_strength: { name: 'Prompt Strength', | |
setUI: (prompt_strength) => { | |
promptStrengthField.value = prompt_strength | |
updatePromptStrengthSlider() | |
}, | |
readUI: () => parseFloat(promptStrengthField.value), | |
parse: (val) => parseFloat(val) | |
}, | |
init_image: { name: 'Initial Image', | |
setUI: (init_image) => { | |
initImagePreview.src = init_image | |
}, | |
readUI: () => initImagePreview.src, | |
parse: (val) => val | |
}, | |
mask: { name: 'Mask', | |
setUI: (mask) => { | |
setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter) | |
imageInpainter.setImg(mask) | |
}, 250) | |
maskSetting.checked = Boolean(mask) | |
}, | |
readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined), | |
parse: (val) => val | |
}, | |
preserve_init_image_color_profile: { name: 'Preserve Color Profile', | |
setUI: (preserve_init_image_color_profile) => { | |
applyColorCorrectionField.checked = parseBoolean(preserve_init_image_color_profile) | |
}, | |
readUI: () => applyColorCorrectionField.checked, | |
parse: (val) => parseBoolean(val) | |
}, | |
use_face_correction: { name: 'Use Face Correction', | |
setUI: (use_face_correction) => { | |
const oldVal = gfpganModelField.value | |
gfpganModelField.value = getModelPath(use_face_correction, ['.pth']) | |
if (gfpganModelField.value) { // Is a valid value for the field. | |
useFaceCorrectionField.checked = true | |
gfpganModelField.disabled = false | |
} else { // Not a valid value, restore the old value and disable the filter. | |
gfpganModelField.disabled = true | |
gfpganModelField.value = oldVal | |
useFaceCorrectionField.checked = false | |
} | |
//useFaceCorrectionField.checked = parseBoolean(use_face_correction) | |
}, | |
readUI: () => (useFaceCorrectionField.checked ? gfpganModelField.value : undefined), | |
parse: (val) => val | |
}, | |
use_upscale: { name: 'Use Upscaling', | |
setUI: (use_upscale) => { | |
const oldVal = upscaleModelField.value | |
upscaleModelField.value = getModelPath(use_upscale, ['.pth']) | |
if (upscaleModelField.value) { // Is a valid value for the field. | |
useUpscalingField.checked = true | |
upscaleModelField.disabled = false | |
upscaleAmountField.disabled = false | |
} else { // Not a valid value, restore the old value and disable the filter. | |
upscaleModelField.disabled = true | |
upscaleAmountField.disabled = true | |
upscaleModelField.value = oldVal | |
useUpscalingField.checked = false | |
} | |
}, | |
readUI: () => (useUpscalingField.checked ? upscaleModelField.value : undefined), | |
parse: (val) => val | |
}, | |
upscale_amount: { name: 'Upscale By', | |
setUI: (upscale_amount) => { | |
upscaleAmountField.value = upscale_amount | |
}, | |
readUI: () => upscaleAmountField.value, | |
parse: (val) => val | |
}, | |
sampler_name: { name: 'Sampler', | |
setUI: (sampler_name) => { | |
samplerField.value = sampler_name | |
}, | |
readUI: () => samplerField.value, | |
parse: (val) => val | |
}, | |
use_stable_diffusion_model: { name: 'Stable Diffusion model', | |
setUI: (use_stable_diffusion_model) => { | |
const oldVal = stableDiffusionModelField.value | |
use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, ['.ckpt', '.safetensors']) | |
stableDiffusionModelField.value = use_stable_diffusion_model | |
if (!stableDiffusionModelField.value) { | |
stableDiffusionModelField.value = oldVal | |
} | |
}, | |
readUI: () => stableDiffusionModelField.value, | |
parse: (val) => val | |
}, | |
use_vae_model: { name: 'VAE model', | |
setUI: (use_vae_model) => { | |
const oldVal = vaeModelField.value | |
use_vae_model = (use_vae_model === undefined || use_vae_model === null || use_vae_model === 'None' ? '' : use_vae_model) | |
if (use_vae_model !== '') { | |
use_vae_model = getModelPath(use_vae_model, ['.vae.pt', '.ckpt']) | |
use_vae_model = use_vae_model !== '' ? use_vae_model : oldVal | |
} | |
vaeModelField.value = use_vae_model | |
}, | |
readUI: () => vaeModelField.value, | |
parse: (val) => val | |
}, | |
use_hypernetwork_model: { name: 'Hypernetwork model', | |
setUI: (use_hypernetwork_model) => { | |
const oldVal = hypernetworkModelField.value | |
use_hypernetwork_model = (use_hypernetwork_model === undefined || use_hypernetwork_model === null || use_hypernetwork_model === 'None' ? '' : use_hypernetwork_model) | |
if (use_hypernetwork_model !== '') { | |
use_hypernetwork_model = getModelPath(use_hypernetwork_model, ['.pt']) | |
use_hypernetwork_model = use_hypernetwork_model !== '' ? use_hypernetwork_model : oldVal | |
} | |
hypernetworkModelField.value = use_hypernetwork_model | |
hypernetworkModelField.dispatchEvent(new Event('change')) | |
}, | |
readUI: () => hypernetworkModelField.value, | |
parse: (val) => val | |
}, | |
hypernetwork_strength: { name: 'Hypernetwork Strength', | |
setUI: (hypernetwork_strength) => { | |
hypernetworkStrengthField.value = hypernetwork_strength | |
updateHypernetworkStrengthSlider() | |
}, | |
readUI: () => parseFloat(hypernetworkStrengthField.value), | |
parse: (val) => parseFloat(val) | |
}, | |
num_outputs: { name: 'Parallel Images', | |
setUI: (num_outputs) => { | |
numOutputsParallelField.value = num_outputs | |
}, | |
readUI: () => parseInt(numOutputsParallelField.value), | |
parse: (val) => val | |
}, | |
use_cpu: { name: 'Use CPU', | |
setUI: (use_cpu) => { | |
useCPUField.checked = use_cpu | |
}, | |
readUI: () => useCPUField.checked, | |
parse: (val) => val | |
}, | |
stream_image_progress: { name: 'Stream Image Progress', | |
setUI: (stream_image_progress) => { | |
streamImageProgressField.checked = (parseInt(numOutputsTotalField.value) > 50 ? false : stream_image_progress) | |
}, | |
readUI: () => streamImageProgressField.checked, | |
parse: (val) => Boolean(val) | |
}, | |
show_only_filtered_image: { name: 'Show only the corrected/upscaled image', | |
setUI: (show_only_filtered_image) => { | |
showOnlyFilteredImageField.checked = show_only_filtered_image | |
}, | |
readUI: () => showOnlyFilteredImageField.checked, | |
parse: (val) => Boolean(val) | |
}, | |
output_format: { name: 'Output Format', | |
setUI: (output_format) => { | |
outputFormatField.value = output_format | |
}, | |
readUI: () => outputFormatField.value, | |
parse: (val) => val | |
}, | |
save_to_disk_path: { name: 'Save to disk path', | |
setUI: (save_to_disk_path) => { | |
saveToDiskField.checked = Boolean(save_to_disk_path) | |
diskPathField.value = save_to_disk_path | |
}, | |
readUI: () => diskPathField.value, | |
parse: (val) => val | |
} | |
} | |
function restoreTaskToUI(task, fieldsToSkip) { | |
fieldsToSkip = fieldsToSkip || [] | |
if ('numOutputsTotal' in task) { | |
numOutputsTotalField.value = task.numOutputsTotal | |
} | |
if ('seed' in task) { | |
randomSeedField.checked = false | |
seedField.value = task.seed | |
} | |
if (!('reqBody' in task)) { | |
return | |
} | |
for (const key in TASK_MAPPING) { | |
if (key in task.reqBody && !fieldsToSkip.includes(key)) { | |
TASK_MAPPING[key].setUI(task.reqBody[key]) | |
} | |
} | |
// properly reset fields not present in the task | |
if (!('use_hypernetwork_model' in task.reqBody)) { | |
hypernetworkModelField.value = "" | |
hypernetworkModelField.dispatchEvent(new Event("change")) | |
} | |
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d) | |
promptField.value = task.reqBody.original_prompt | |
if (!('original_prompt' in task.reqBody)) { | |
promptField.value = task.reqBody.prompt | |
} | |
// properly reset checkboxes | |
if (!('use_face_correction' in task.reqBody)) { | |
useFaceCorrectionField.checked = false | |
gfpganModelField.disabled = true | |
} | |
if (!('use_upscale' in task.reqBody)) { | |
useUpscalingField.checked = false | |
} | |
if (!('mask' in task.reqBody) && maskSetting.checked) { | |
maskSetting.checked = false | |
maskSetting.dispatchEvent(new Event("click")) | |
} | |
upscaleModelField.disabled = !useUpscalingField.checked | |
upscaleAmountField.disabled = !useUpscalingField.checked | |
// hide/show source picture as needed | |
if (IMAGE_REGEX.test(initImagePreview.src) && task.reqBody.init_image == undefined) { | |
// hide source image | |
initImageClearBtn.dispatchEvent(new Event("click")) | |
} | |
else if (task.reqBody.init_image !== undefined) { | |
// listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpainter) | |
initImagePreview.addEventListener('load', function() { | |
if (Boolean(task.reqBody.mask)) { | |
imageInpainter.setImg(task.reqBody.mask) | |
maskSetting.checked = true | |
} | |
}, { once: true }) | |
initImagePreview.src = task.reqBody.init_image | |
} | |
} | |
function readUI() { | |
const reqBody = {} | |
for (const key in TASK_MAPPING) { | |
reqBody[key] = TASK_MAPPING[key].readUI() | |
} | |
return { | |
'numOutputsTotal': parseInt(numOutputsTotalField.value), | |
'seed': TASK_MAPPING['seed'].readUI(), | |
'reqBody': reqBody | |
} | |
} | |
function getModelPath(filename, extensions) | |
{ | |
if (typeof filename !== "string") { | |
return | |
} | |
let pathIdx | |
if (filename.includes('/models/stable-diffusion/')) { | |
pathIdx = filename.indexOf('/models/stable-diffusion/') + 25 // Linux, Mac paths | |
} | |
else if (filename.includes('\\models\\stable-diffusion\\')) { | |
pathIdx = filename.indexOf('\\models\\stable-diffusion\\') + 25 // Linux, Mac paths | |
} | |
if (pathIdx >= 0) { | |
filename = filename.slice(pathIdx) | |
} | |
extensions.forEach(ext => { | |
if (filename.endsWith(ext)) { | |
filename = filename.slice(0, filename.length - ext.length) | |
} | |
}) | |
return filename | |
} | |
const TASK_TEXT_MAPPING = { | |
prompt: 'Prompt', | |
width: 'Width', | |
height: 'Height', | |
seed: 'Seed', | |
num_inference_steps: 'Steps', | |
guidance_scale: 'Guidance Scale', | |
prompt_strength: 'Prompt Strength', | |
use_face_correction: 'Use Face Correction', | |
use_upscale: 'Use Upscaling', | |
upscale_amount: 'Upscale By', | |
sampler_name: 'Sampler', | |
negative_prompt: 'Negative Prompt', | |
use_stable_diffusion_model: 'Stable Diffusion model', | |
use_hypernetwork_model: 'Hypernetwork model', | |
hypernetwork_strength: 'Hypernetwork Strength' | |
} | |
function parseTaskFromText(str) { | |
const taskReqBody = {} | |
const lines = str.split('\n') | |
if (lines.length === 0) { | |
return | |
} | |
// Prompt | |
let knownKeyOnFirstLine = false | |
for (let key in TASK_TEXT_MAPPING) { | |
if (lines[0].startsWith(TASK_TEXT_MAPPING[key] + ':')) { | |
knownKeyOnFirstLine = true | |
break | |
} | |
} | |
if (!knownKeyOnFirstLine) { | |
taskReqBody.prompt = lines[0] | |
console.log('Prompt:', taskReqBody.prompt) | |
} | |
for (const key in TASK_TEXT_MAPPING) { | |
if (key in taskReqBody) { | |
continue | |
} | |
const name = TASK_TEXT_MAPPING[key]; | |
let val = undefined | |
const reName = new RegExp(`${name}\\ *:\\ *(.*)(?:\\r\\n|\\r|\\n)*`, 'igm') | |
const match = reName.exec(str); | |
if (match) { | |
str = str.slice(0, match.index) + str.slice(match.index + match[0].length) | |
val = match[1] | |
} | |
if (val !== undefined) { | |
taskReqBody[key] = TASK_MAPPING[key].parse(val.trim()) | |
console.log(TASK_MAPPING[key].name + ':', taskReqBody[key]) | |
if (!str) { | |
break | |
} | |
} | |
} | |
if (Object.keys(taskReqBody).length <= 0) { | |
return undefined | |
} | |
const task = { reqBody: taskReqBody } | |
if ('seed' in taskReqBody) { | |
task.seed = taskReqBody.seed | |
} | |
return task | |
} | |
async function parseContent(text) { | |
text = text.trim(); | |
if (text.startsWith('{') && text.endsWith('}')) { | |
try { | |
const task = JSON.parse(text) | |
if (!('reqBody' in task)) { // support the format saved to the disk, by the UI | |
task.reqBody = Object.assign({}, task) | |
} | |
restoreTaskToUI(task) | |
return true | |
} catch (e) { | |
console.warn(`JSON text content couldn't be parsed.`, e) | |
} | |
return false | |
} | |
// Normal txt file. | |
const task = parseTaskFromText(text) | |
if (text.toLowerCase().includes('seed:') && task) { // only parse valid task content | |
restoreTaskToUI(task) | |
return true | |
} else { | |
console.warn(`Raw text content couldn't be parsed.`) | |
promptField.value = text | |
return false | |
} | |
} | |
async function readFile(file, i) { | |
console.log(`Event %o reading file[${i}]:${file.name}...`) | |
const fileContent = (await file.text()).trim() | |
return await parseContent(fileContent) | |
} | |
function dropHandler(ev) { | |
console.log('Content dropped...') | |
let items = [] | |
if (ev?.dataTransfer?.items) { // Use DataTransferItemList interface | |
items = Array.from(ev.dataTransfer.items) | |
items = items.filter(item => item.kind === 'file') | |
items = items.map(item => item.getAsFile()) | |
} else if (ev?.dataTransfer?.files) { // Use DataTransfer interface | |
items = Array.from(ev.dataTransfer.files) | |
} | |
items.forEach(item => {item.file_ext = EXT_REGEX.exec(item.name.toLowerCase())[1]}) | |
let text_items = items.filter(item => TEXT_EXTENSIONS.includes(item.file_ext)) | |
let image_items = items.filter(item => IMAGE_EXTENSIONS.includes(item.file_ext)) | |
if (image_items.length > 0 && ev.target == initImageSelector) { | |
return // let the event bubble up, so that the Init Image filepicker can receive this | |
} | |
ev.preventDefault() // Prevent default behavior (Prevent file/content from being opened) | |
text_items.forEach(readFile) | |
} | |
function dragOverHandler(ev) { | |
console.log('Content in drop zone') | |
// Prevent default behavior (Prevent file/content from being opened) | |
ev.preventDefault() | |
ev.dataTransfer.dropEffect = "copy" | |
let img = new Image() | |
img.src = '//' + location.host + '/media/images/favicon-32x32.png' | |
ev.dataTransfer.setDragImage(img, 16, 16) | |
} | |
document.addEventListener("drop", dropHandler) | |
document.addEventListener("dragover", dragOverHandler) | |
const TASK_REQ_NO_EXPORT = [ | |
"use_cpu", | |
"save_to_disk_path" | |
] | |
const resetSettings = document.getElementById('reset-image-settings') | |
function checkReadTextClipboardPermission (result) { | |
if (result.state != "granted" && result.state != "prompt") { | |
return | |
} | |
// PASTE ICON | |
const pasteIcon = document.createElement('i') | |
pasteIcon.className = 'fa-solid fa-paste section-button' | |
pasteIcon.innerHTML = `<span class="simple-tooltip top-left">Paste Image Settings</span>` | |
pasteIcon.addEventListener('click', async (event) => { | |
event.stopPropagation() | |
// Add css class 'active' | |
pasteIcon.classList.add('active') | |
// In 350 ms remove the 'active' class | |
asyncDelay(350).then(() => pasteIcon.classList.remove('active')) | |
// Retrieve clipboard content and try to parse it | |
const text = await navigator.clipboard.readText(); | |
await parseContent(text) | |
}) | |
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings) | |
} | |
navigator.permissions.query({ name: "clipboard-read" }).then(checkReadTextClipboardPermission, (reason) => console.log('clipboard-read is not available. %o', reason)) | |
document.addEventListener('paste', async (event) => { | |
if (event.target) { | |
const targetTag = event.target.tagName.toLowerCase() | |
// Disable when targeting input elements. | |
if (targetTag === 'input' || targetTag === 'textarea') { | |
return | |
} | |
} | |
const paste = (event.clipboardData || window.clipboardData).getData('text') | |
const selection = window.getSelection() | |
if (selection.toString().trim().length <= 0 && await parseContent(paste)) { | |
event.preventDefault() | |
return | |
} | |
}) | |
// Adds a copy and a paste icon if the browser grants permission to write to clipboard. | |
function checkWriteToClipboardPermission (result) { | |
if (result.state != "granted" && result.state != "prompt") { | |
return | |
} | |
// COPY ICON | |
const copyIcon = document.createElement('i') | |
copyIcon.className = 'fa-solid fa-clipboard section-button' | |
copyIcon.innerHTML = `<span class="simple-tooltip top-left">Copy Image Settings</span>` | |
copyIcon.addEventListener('click', (event) => { | |
event.stopPropagation() | |
// Add css class 'active' | |
copyIcon.classList.add('active') | |
// In 350 ms remove the 'active' class | |
asyncDelay(350).then(() => copyIcon.classList.remove('active')) | |
const uiState = readUI() | |
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key]) | |
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) { | |
delete uiState.reqBody.init_image | |
delete uiState.reqBody.prompt_strength | |
} | |
navigator.clipboard.writeText(JSON.stringify(uiState, undefined, 4)) | |
}) | |
resetSettings.parentNode.insertBefore(copyIcon, resetSettings) | |
} | |
// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS. | |
navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => { | |
if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === 'function') { | |
// Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373 | |
checkWriteToClipboardPermission({state:"granted"}) | |
} | |
}) | |