"use strict" // Opt in to a restricted variant of JavaScript
const SOUND_ENABLED_KEY = "soundEnabled"
const SAVE_TO_DISK_KEY = "saveToDisk"
const USE_CPU_KEY = "useCPU"
const USE_FULL_PRECISION_KEY = "useFullPrecision"
const USE_TURBO_MODE_KEY = "useTurboMode"
const DISK_PATH_KEY = "diskPath"
const ADVANCED_PANEL_OPEN_KEY = "advancedPanelOpen"
const MODIFIERS_PANEL_OPEN_KEY = "modifiersPanelOpen"
const NEGATIVE_PROMPT_PANEL_OPEN_KEY = "negativePromptPanelOpen"
const USE_FACE_CORRECTION_KEY = "useFaceCorrection"
const USE_UPSCALING_KEY = "useUpscaling"
const SHOW_ONLY_FILTERED_IMAGE_KEY = "showOnlyFilteredImage"
const STREAM_IMAGE_PROGRESS_KEY = "streamImageProgress"
const OUTPUT_FORMAT_KEY = "outputFormat"
const AUTO_SAVE_SETTINGS_KEY = "autoSaveSettings"
const HEALTH_PING_INTERVAL = 5 // seconds
const MAX_INIT_IMAGE_DIMENSION = 768
const INPAINTING_EDITOR_SIZE = 450
const IMAGE_REGEX = new RegExp('data:image/[A-Za-z]+;base64')
let sessionId = Date.now()
let promptField = document.querySelector('#prompt')
let promptsFromFileSelector = document.querySelector('#prompt_from_file')
let promptsFromFileBtn = document.querySelector('#promptsFromFileBtn')
let negativePromptField = document.querySelector('#negative_prompt')
let numOutputsTotalField = document.querySelector('#num_outputs_total')
let numOutputsParallelField = document.querySelector('#num_outputs_parallel')
let numInferenceStepsField = document.querySelector('#num_inference_steps')
let guidanceScaleSlider = document.querySelector('#guidance_scale_slider')
let guidanceScaleField = document.querySelector('#guidance_scale')
let randomSeedField = document.querySelector("#random_seed")
let seedField = document.querySelector('#seed')
let widthField = document.querySelector('#width')
let heightField = document.querySelector('#height')
let initImageSelector = document.querySelector("#init_image")
let initImagePreview = document.querySelector("#init_image_preview")
let initImageSizeBox = document.querySelector("#init_image_size_box")
let maskImageSelector = document.querySelector("#mask")
let maskImagePreview = document.querySelector("#mask_preview")
let turboField = document.querySelector('#turbo')
let useCPUField = document.querySelector('#use_cpu')
let useFullPrecisionField = document.querySelector('#use_full_precision')
let saveToDiskField = document.querySelector('#save_to_disk')
let diskPathField = document.querySelector('#diskPath')
let autoSaveSettingsField = document.querySelector('#auto_save_settings')
// let allowNSFWField = document.querySelector("#allow_nsfw")
let useBetaChannelField = document.querySelector("#use_beta_channel")
let promptStrengthSlider = document.querySelector('#prompt_strength_slider')
let promptStrengthField = document.querySelector('#prompt_strength')
let samplerField = document.querySelector('#sampler')
let samplerSelectionContainer = document.querySelector("#samplerSelection")
let useFaceCorrectionField = document.querySelector("#use_face_correction")
let useUpscalingField = document.querySelector("#use_upscale")
let upscaleModelField = document.querySelector("#upscale_model")
let stableDiffusionModelField = document.querySelector('#stable_diffusion_model')
let outputFormatField = document.querySelector('#output_format')
let showOnlyFilteredImageField = document.querySelector("#show_only_filtered_image")
let updateBranchLabel = document.querySelector("#updateBranchLabel")
let streamImageProgressField = document.querySelector("#stream_image_progress")
let makeImageBtn = document.querySelector('#makeImage')
let stopImageBtn = document.querySelector('#stopImage')
let imagesContainer = document.querySelector('#current-images')
let initImagePreviewContainer = document.querySelector('#init_image_preview_container')
let initImageClearBtn = document.querySelector('.init_image_clear')
let promptStrengthContainer = document.querySelector('#prompt_strength_container')
let initialText = document.querySelector("#initial-text")
let previewTools = document.querySelector("#preview-tools")
let clearAllPreviewsBtn = document.querySelector("#clear-all-previews")
// let maskSetting = document.querySelector('#editor-inputs-mask_setting')
// let maskImagePreviewContainer = document.querySelector('#mask_preview_container')
// let maskImageClearBtn = document.querySelector('#mask_clear')
let maskSetting = document.querySelector('#enable_mask')
let negativePromptPanelHandle = document.querySelector('#negative_prompt_handle')
let editorModifierEntries = document.querySelector('#editor-modifiers-entries')
let editorModifierTagsList = document.querySelector('#editor-inputs-tags-list')
let editorTagsContainer = document.querySelector('#editor-inputs-tags-container')
let imagePreview = document.querySelector("#preview")
let previewImageField = document.querySelector('#preview-image')
previewImageField.onchange = () => changePreviewImages(previewImageField.value)
let modifierCardSizeSlider = document.querySelector('#modifier-card-size-slider')
modifierCardSizeSlider.onchange = () => resizeModifierCards(modifierCardSizeSlider.value)
// let previewPrompt = document.querySelector('#preview-prompt')
let showConfigToggle = document.querySelector('#configToggleBtn')
// let configBox = document.querySelector('#config')
// let outputMsg = document.querySelector('#outputMsg')
// let progressBar = document.querySelector("#progressBar")
let soundToggle = document.querySelector('#sound_toggle')
let serverStatusColor = document.querySelector('#server-status-color')
let serverStatusMsg = document.querySelector('#server-status-msg')
let advancedPanelHandle = document.querySelector("#editor-settings .collapsible")
let modifiersPanelHandle = document.querySelector("#editor-modifiers .collapsible")
let inpaintingEditorContainer = document.querySelector('#inpaintingEditor')
let inpaintingEditor = new DrawingBoard.Board('inpaintingEditor', {
color: "#ffffff",
background: false,
size: 30,
webStorage: false,
controls: [{'DrawingMode': {'filler': false}}, 'Size', 'Navigation']
})
let inpaintingEditorCanvasBackground = document.querySelector('.drawing-board-canvas-wrapper')
document.querySelector('.drawing-board-control-navigation-back').innerHTML = ''
document.querySelector('.drawing-board-control-navigation-forward').innerHTML = ''
let maskResetButton = document.querySelector('.drawing-board-control-navigation-reset')
maskResetButton.innerHTML = 'Clear'
maskResetButton.style.fontWeight = 'normal'
maskResetButton.style.fontSize = '10pt'
let serverState = {'status': 'Offline', 'time': Date.now()}
let activeTags = []
let modifiers = []
let lastPromptUsed = ''
let bellPending = false
let taskQueue = []
let currentTask = null
const modifierThumbnailPath = 'media/modifier-thumbnails'
const activeCardClass = 'modifier-card-active'
function getLocalStorageItem(key, fallback) {
let item = localStorage.getItem(key)
if (item === null) {
return fallback
}
return item
}
function getLocalStorageBoolItem(key, fallback) {
let item = localStorage.getItem(key)
if (item === null) {
return fallback
}
return (item === 'true' ? true : false)
}
function handleBoolSettingChange(key) {
return function(e) {
localStorage.setItem(key, e.target.checked.toString())
}
}
function handleStringSettingChange(key) {
return function(e) {
localStorage.setItem(key, e.target.value.toString())
}
}
function isSoundEnabled() {
return getLocalStorageBoolItem(SOUND_ENABLED_KEY, true)
}
function isFaceCorrectionEnabled() {
return getLocalStorageBoolItem(USE_FACE_CORRECTION_KEY, false)
}
function isUpscalingEnabled() {
return getLocalStorageBoolItem(USE_UPSCALING_KEY, false)
}
function isShowOnlyFilteredImageEnabled() {
return getLocalStorageBoolItem(SHOW_ONLY_FILTERED_IMAGE_KEY, true)
}
function isSaveToDiskEnabled() {
return getLocalStorageBoolItem(SAVE_TO_DISK_KEY, false)
}
function isUseCPUEnabled() {
return getLocalStorageBoolItem(USE_CPU_KEY, false)
}
function isUseFullPrecisionEnabled() {
return getLocalStorageBoolItem(USE_FULL_PRECISION_KEY, false)
}
function isAutoSaveSettingsEnabled() {
return getLocalStorageBoolItem(AUTO_SAVE_SETTINGS_KEY, false)
}
function isUseTurboModeEnabled() {
return getLocalStorageBoolItem(USE_TURBO_MODE_KEY, true)
}
function getSavedDiskPath() {
return getLocalStorageItem(DISK_PATH_KEY, '')
}
function isAdvancedPanelOpenEnabled() {
return getLocalStorageBoolItem(ADVANCED_PANEL_OPEN_KEY, false)
}
function isModifiersPanelOpenEnabled() {
return getLocalStorageBoolItem(MODIFIERS_PANEL_OPEN_KEY, false)
}
function isNegativePromptPanelOpenEnabled() {
return getLocalStorageBoolItem(NEGATIVE_PROMPT_PANEL_OPEN_KEY, false)
}
function isStreamImageProgressEnabled() {
return getLocalStorageBoolItem(STREAM_IMAGE_PROGRESS_KEY, false)
}
function getOutputFormat() {
return getLocalStorageItem(OUTPUT_FORMAT_KEY, 'jpeg')
}
function setStatus(statusType, msg, msgType) {
}
function setServerStatus(msgType, msg) {
switch(msgType) {
case 'online':
serverStatusColor.style.color = 'green'
serverStatusMsg.style.color = 'green'
serverStatusMsg.innerText = 'Stable Diffusion is ' + msg
break
case 'busy':
serverStatusColor.style.color = 'yellow'
serverStatusMsg.style.color = 'yellow'
serverStatusMsg.innerText = 'Stable Diffusion is ' + msg
break
case 'error':
serverStatusColor.style.color = 'red'
serverStatusMsg.style.color = 'red'
serverStatusMsg.innerText = 'Stable Diffusion has stopped'
break
}
}
function isServerAvailable() {
if (typeof serverState !== 'object') {
return false
}
switch (serverState.status) {
case 'LoadingModel':
case 'Rendering':
case 'Online':
return true
default:
return false
}
}
function logMsg(msg, level, outputMsg) {
if (outputMsg.hasChildNodes()) {
outputMsg.appendChild(document.createElement('br'))
}
if (level === 'error') {
outputMsg.innerHTML += 'Error: ' + msg + ''
} else if (level === 'warn') {
outputMsg.innerHTML += 'Warning: ' + msg + ''
} else {
outputMsg.innerText += msg
}
console.log(level, msg)
}
function logError(msg, res, outputMsg) {
logMsg(msg, 'error', outputMsg)
console.log('request error', res)
setStatus('request', 'error', 'error')
}
function asyncDelay(timeout) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, timeout, true)
})
}
function playSound() {
const audio = new Audio('/media/ding.mp3')
audio.volume = 0.2
var promise = audio.play();
if (promise !== undefined) {
promise.then(_ => {}).catch(error => {
console.warn("browser blocked autoplay");
});
}
}
async function healthCheck() {
try {
let res = undefined
if (sessionId) {
res = await fetch('/ping?session_id=' + sessionId)
} else {
res = await fetch('/ping')
}
serverState = await res.json()
if (typeof serverState !== 'object' || typeof serverState.status !== 'string') {
serverState = {'status': 'Offline', 'time': Date.now()}
setServerStatus('error', 'offline')
return
}
// Set status
switch(serverState.status) {
case 'Init':
// Wait for init to complete before updating status.
break
case 'Online':
setServerStatus('online', 'ready')
break
case 'LoadingModel':
setServerStatus('busy', 'loading model')
break
case 'Rendering':
setServerStatus('busy', 'rendering')
break
default: // Unavailable
setServerStatus('error', serverState.status.toLowerCase())
break
}
serverState.time = Date.now()
} catch (e) {
serverState = {'status': 'Offline', 'time': Date.now()}
setServerStatus('error', 'offline')
}
}
function resizeInpaintingEditor() {
if (!maskSetting.checked) {
return
}
let widthValue = parseInt(widthField.value)
let heightValue = parseInt(heightField.value)
if (widthValue === heightValue) {
widthValue = INPAINTING_EDITOR_SIZE
heightValue = INPAINTING_EDITOR_SIZE
} else if (widthValue > heightValue) {
heightValue = (heightValue / widthValue) * INPAINTING_EDITOR_SIZE
widthValue = INPAINTING_EDITOR_SIZE
} else {
widthValue = (widthValue / heightValue) * INPAINTING_EDITOR_SIZE
heightValue = INPAINTING_EDITOR_SIZE
}
if (inpaintingEditor.opts.aspectRatio === (widthValue / heightValue).toFixed(3)) {
// Same ratio, don't reset the canvas.
return
}
inpaintingEditor.opts.aspectRatio = (widthValue / heightValue).toFixed(3)
inpaintingEditorContainer.style.width = widthValue + 'px'
inpaintingEditorContainer.style.height = heightValue + 'px'
inpaintingEditor.opts.enlargeYourContainer = true
inpaintingEditor.opts.size = inpaintingEditor.ctx.lineWidth
inpaintingEditor.resize()
inpaintingEditor.ctx.lineCap = "round"
inpaintingEditor.ctx.lineJoin = "round"
inpaintingEditor.ctx.lineWidth = inpaintingEditor.opts.size
inpaintingEditor.setColor(inpaintingEditor.opts.color)
}
function showImages(reqBody, res, outputContainer, livePreview) {
let imageItemElements = outputContainer.querySelectorAll('.imgItem')
if(typeof res != 'object') return
res.output.reverse()
res.output.forEach((result, index) => {
const imageData = result?.data || result?.path + '?t=' + Date.now(),
imageSeed = result?.seed,
imagePrompt = reqBody.prompt,
imageInferenceSteps = reqBody.num_inference_steps,
imageGuidanceScale = reqBody.guidance_scale,
imageWidth = reqBody.width,
imageHeight = reqBody.height;
if (!imageData.includes('/')) {
// res contained no data for the image, stop execution
setStatus('request', 'invalid image', 'error')
return
}
let imageItemElem = (index < imageItemElements.length ? imageItemElements[index] : null)
if(!imageItemElem) {
imageItemElem = document.createElement('div')
imageItemElem.className = 'imgItem'
imageItemElem.innerHTML = `
`
outputContainer.appendChild(imageItemElem)
}
const imageElem = imageItemElem.querySelector('img')
imageElem.src = imageData
imageElem.width = parseInt(imageWidth)
imageElem.height = parseInt(imageHeight)
imageElem.setAttribute('data-prompt', imagePrompt)
imageElem.setAttribute('data-steps', imageInferenceSteps)
imageElem.setAttribute('data-guidance', imageGuidanceScale)
const imageInfo = imageItemElem.querySelector('.imgItemInfo')
imageInfo.style.visibility = (livePreview ? 'hidden' : 'visible')
if ('seed' in result && !imageElem.hasAttribute('data-seed')) {
const req = Object.assign({}, reqBody, {
seed: result?.seed || reqBody.seed
})
imageElem.setAttribute('data-seed', req.seed)
const imageSeedLabel = imageItemElem.querySelector('.imgSeedLabel')
imageSeedLabel.innerText = 'Seed: ' + req.seed
const buttons = {
'imgUseBtn': { html: 'Use as Input', click: getUseAsInputHandler(imageItemElem) },
'imgSaveBtn': { html: 'Download', click: getSaveImageHandler(imageItemElem, req['output_format']) },
'imgX2Btn': { html: 'Double Size', click: getStartNewTaskHandler(req, imageItemElem, 'img2img_X2') },
'imgRedoBtn': { html: 'Redo', click: getStartNewTaskHandler(req, imageItemElem, 'img2img') },
}
if (!req.use_upscale) {
buttons.upscaleBtn = { html: 'Upscale', click: getStartNewTaskHandler(req, imageItemElem, 'upscale') }
}
const imgItemInfo = imageItemElem.querySelector('.imgItemInfo')
const createButton = function(name, btnInfo) {
const newButton = document.createElement('button')
newButton.classList.add(name)
newButton.classList.add('tasksBtns')
newButton.innerHTML = btnInfo.html
newButton.addEventListener('click', btnInfo.click)
imgItemInfo.appendChild(newButton)
}
Object.keys(buttons).forEach((name) => createButton(name, buttons[name]))
}
})
}
function getUseAsInputHandler(imageItemElem) {
return function() {
const imageElem = imageItemElem.querySelector('img')
const imgData = imageElem.src
const imageSeed = imageElem.getAttribute('data-seed')
initImageSelector.value = null
initImagePreview.src = imgData
initImagePreviewContainer.style.display = 'block'
inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'table-row'
maskSetting.checked = false
samplerSelectionContainer.style.display = 'none'
// maskSetting.style.display = 'block'
randomSeedField.checked = false
seedField.value = imageSeed
seedField.disabled = false
}
}
function getSaveImageHandler(imageItemElem, outputFormat) {
return function() {
const imageElem = imageItemElem.querySelector('img')
const imgData = imageElem.src
const imageSeed = imageElem.getAttribute('data-seed')
const imagePrompt = imageElem.getAttribute('data-prompt')
const imageInferenceSteps = imageElem.getAttribute('data-steps')
const imageGuidanceScale = imageElem.getAttribute('data-guidance')
const imgDownload = document.createElement('a')
imgDownload.download = createFileName(imagePrompt, imageSeed, imageInferenceSteps, imageGuidanceScale, outputFormat)
imgDownload.href = imgData
imgDownload.click()
}
}
function getStartNewTaskHandler(reqBody, imageItemElem, mode) {
return function() {
if (!isServerAvailable()) {
alert('The server is not available.')
return
}
const imageElem = imageItemElem.querySelector('img')
const newTaskRequest = getCurrentUserRequest()
switch (mode) {
case 'img2img':
case 'img2img_X2':
newTaskRequest.reqBody = Object.assign({}, reqBody, {
num_outputs: 1,
use_cpu: useCPUField.checked,
})
if (!newTaskRequest.reqBody.init_image || mode === 'img2img_X2') {
newTaskRequest.reqBody.sampler = 'ddim'
newTaskRequest.reqBody.prompt_strength = '0.5'
newTaskRequest.reqBody.init_image = imageElem.src
delete newTaskRequest.reqBody.mask
} else {
newTaskRequest.reqBody.seed = 1 + newTaskRequest.reqBody.seed
}
if (mode === 'img2img_X2') {
newTaskRequest.reqBody.width = reqBody.width * 2
newTaskRequest.reqBody.height = reqBody.height * 2
newTaskRequest.reqBody.num_inference_steps = Math.min(100, reqBody.num_inference_steps * 2)
if (useUpscalingField.checked) {
newTaskRequest.reqBody.use_upscale = upscaleModelField.value
} else {
delete newTaskRequest.reqBody.use_upscale
}
}
break
case 'upscale':
newTaskRequest.reqBody = Object.assign({}, reqBody, {
num_outputs: 1,
//use_face_correction: 'GFPGANv1.3',
use_upscale: upscaleModelField.value,
})
break
default:
throw new Error("Unknown upscale mode: " + mode)
}
newTaskRequest.seed = newTaskRequest.reqBody.seed
newTaskRequest.numOutputsTotal = 1
newTaskRequest.batchCount = 1
createTask(newTaskRequest)
}
}
// makes a single image. don't call this directly, use makeImage() instead
async function doMakeImage(task) {
if (task.stopped) {
return
}
const reqBody = task.reqBody
const batchCount = task.batchCount
const outputContainer = document.createElement('div')
outputContainer.className = 'img-batch'
task.outputContainer.insertBefore(outputContainer, task.outputContainer.firstChild)
const outputMsg = task['outputMsg']
const previewPrompt = task['previewPrompt']
const progressBar = task['progressBar']
let res = undefined
try {
const lastTask = serverState.task
let renderRequest = undefined
do {
res = await fetch('/render', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(reqBody)
})
renderRequest = await res.json()
// status_code 503, already a task running.
} while (renderRequest.status_code === 503 && await asyncDelay(30 * 1000))
if (typeof renderRequest?.stream !== 'string') {
console.log('Endpoint response: ', renderRequest)
throw new Error('Endpoint response does not contains a response stream url.')
}
task['taskStatusLabel'].innerText = "Busy/Waiting"
task['taskStatusLabel'].classList.add('waitingTaskLabel')
task['taskStatusLabel'].classList.remove('activeTaskLabel')
do { // Wait for server status to update.
await asyncDelay(250)
if (!isServerAvailable()) {
throw new Error('Connexion with server lost.')
}
} while (serverState.time > (Date.now() - (10 * 1000)) && serverState.task !== renderRequest.task)
if (serverState.session !== 'pending' && serverState.session !== 'running' && serverState.session !== 'buffer') {
throw new Error('Unexpected server task state: ' + serverState.session || 'Undefined')
}
while (serverState.task === renderRequest.task && serverState.session === 'pending') {
// Wait for task to start on server.
await asyncDelay(1500)
}
// Task started!
res = await fetch(renderRequest.stream, {
headers: {
'Content-Type': 'application/json'
},
})
task['taskStatusLabel'].innerText = "Processing"
task['taskStatusLabel'].classList.add('activeTaskLabel')
task['taskStatusLabel'].classList.remove('waitingTaskLabel')
let stepUpdate = undefined
let reader = res.body.getReader()
let textDecoder = new TextDecoder()
let finalJSON = ''
let prevTime = -1
let readComplete = false
while (!readComplete || finalJSON.length > 0) {
let t = Date.now()
let jsonStr = ''
if (!readComplete) {
const {value, done} = await reader.read()
if (done) {
readComplete = true
}
if (value) {
jsonStr = textDecoder.decode(value)
}
}
stepUpdate = undefined
try {
// hack for a middleman buffering all the streaming updates, and unleashing them on the poor browser in one shot.
// this results in having to parse JSON like {"step": 1}{"step": 2}{"step": 3}{"ste...
// which is obviously invalid and can happen at any point while rendering.
// So we need to extract only the next {} section
if (finalJSON.length > 0) {
// Append new data when required
if (jsonStr.length > 0) {
jsonStr = finalJSON + jsonStr
} else {
jsonStr = finalJSON
}
finalJSON = ''
}
// Find next delimiter
let lastChunkIdx = jsonStr.indexOf('}{')
if (lastChunkIdx !== -1) {
finalJSON = jsonStr.substring(0, lastChunkIdx + 1)
jsonStr = jsonStr.substring(lastChunkIdx + 1)
} else {
finalJSON = jsonStr
jsonStr = ''
}
// Try to parse
stepUpdate = (finalJSON.length > 0 ? JSON.parse(finalJSON) : undefined)
finalJSON = jsonStr
} catch (e) {
if (e instanceof SyntaxError && !readComplete) {
finalJSON += jsonStr
} else {
throw e
}
}
if (typeof stepUpdate === 'object' && 'step' in stepUpdate) {
let batchSize = stepUpdate.total_steps
let overallStepCount = stepUpdate.step + task.batchesDone * batchSize
let totalSteps = batchCount * batchSize
let percent = 100 * (overallStepCount / totalSteps)
percent = (percent > 100 ? 100 : percent)
percent = percent.toFixed(0)
let timeTaken = (prevTime === -1 ? -1 : t - prevTime)
let stepsRemaining = totalSteps - overallStepCount
stepsRemaining = (stepsRemaining < 0 ? 0 : stepsRemaining)
let timeRemaining = (timeTaken === -1 ? '' : stepsRemaining * timeTaken) // ms
outputMsg.innerHTML = `Batch ${task.batchesDone+1} of ${batchCount}`
outputMsg.innerHTML += `. Generating image(s): ${percent}%`
timeRemaining = (timeTaken !== -1 ? millisecondsToStr(timeRemaining) : '')
outputMsg.innerHTML += `. Time remaining (approx): ${timeRemaining}`
outputMsg.style.display = 'block'
if (stepUpdate.output !== undefined) {
showImages(reqBody, stepUpdate, outputContainer, true)
}
}
if (stepUpdate?.status) {
break
}
if (readComplete && finalJSON.length <= 0) {
if (res.status === 200) {
await asyncDelay(5000)
res = await fetch(renderRequest.stream, {
headers: {
'Content-Type': 'application/json'
},
})
reader = res.body.getReader()
readComplete = false
} else {
console.log('Stream stopped: ', res)
}
}
prevTime = t
}
if (typeof stepUpdate === 'object' && stepUpdate.status !== 'succeeded') {
let msg = ''
if ('detail' in stepUpdate && typeof stepUpdate.detail === 'string' && stepUpdate.detail.length > 0) {
msg = stepUpdate.detail
if (msg.toLowerCase().includes('out of memory')) {
msg += `
Suggestions:
1. If you have set an initial image, please try reducing its dimension to ${MAX_INIT_IMAGE_DIMENSION}x${MAX_INIT_IMAGE_DIMENSION} or smaller.
2. Try disabling the 'Turbo mode' under 'Advanced Settings'.
3. Try generating a smaller image. `
}
} else {
msg = `Unexpected Read Error: