"use strict" // 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 randomSeedField.dispatchEvent(new Event('change')) // let plugins know that the state of the random seed toggle changed 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_lora_model: { name: 'LoRA model', setUI: (use_lora_model) => { const oldVal = loraModelField.value use_lora_model = (use_lora_model === undefined || use_lora_model === null || use_lora_model === 'None' ? '' : use_lora_model) if (use_lora_model !== '') { use_lora_model = getModelPath(use_lora_model, ['.ckpt', '.safetensors']) use_lora_model = use_lora_model !== '' ? use_lora_model : oldVal } loraModelField.value = use_lora_model }, readUI: () => loraModelField.value, parse: (val) => val }, lora_alpha: { name: 'LoRA Strength', setUI: (lora_alpha) => { loraAlphaField.value = lora_alpha updateLoraAlphaSlider() }, readUI: () => parseFloat(loraAlphaField.value), parse: (val) => parseFloat(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")) } if (!('use_lora_model' in task.reqBody)) { loraModelField.value = "" loraModelField.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 = `Paste Image Settings` 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 (paste != "" && 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 = `Copy Image Settings` 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"}) } })