-
Type a prompt and press the "Make Image" button. You can set an "Initial Image" if you want to guide the AI. You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section and selecting the desired modifiers. Click "Advanced Settings" for additional settings like seed, image size, number of images to generate etc. Enjoy! :)
+
+
+
+ Type a prompt and press the "Make Image" button. You can set an "Initial Image" if you want to guide the AI. You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section and selecting the desired modifiers. Click "Advanced Settings" for additional settings like seed, image size, number of images to generate etc. Enjoy! :)
+
+
+
@@ -428,11 +599,14 @@ const MODIFIERS_PANEL_OPEN_KEY = "modifiersPanelOpen"
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 HEALTH_PING_INTERVAL = 5 // seconds
const MAX_INIT_IMAGE_DIMENSION = 768
const IMAGE_REGEX = new RegExp('data:image/[A-Za-z]+;base64')
+let sessionId = new Date().getTime()
+
let promptField = document.querySelector('#prompt')
let numOutputsTotalField = document.querySelector('#num_outputs_total')
let numOutputsParallelField = document.querySelector('#num_outputs_parallel')
@@ -445,8 +619,8 @@ let widthField = document.querySelector('#width')
let heightField = document.querySelector('#height')
let initImageSelector = document.querySelector("#init_image")
let initImagePreview = document.querySelector("#init_image_preview")
-// let maskImageSelector = document.querySelector("#mask")
-// let maskImagePreview = document.querySelector("#mask_preview")
+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')
@@ -456,23 +630,27 @@ let diskPathField = document.querySelector('#diskPath')
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 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 initImageClearBtn = document.querySelector('.init_image_clear')
let promptStrengthContainer = document.querySelector('#prompt_strength_container')
-// let maskSetting = document.querySelector('#mask_setting')
+// 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 editorModifierEntries = document.querySelector('#editor-modifiers-entries')
let editorModifierTagsList = document.querySelector('#editor-inputs-tags-list')
@@ -483,6 +661,7 @@ 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')
@@ -491,11 +670,35 @@ 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')
+// let inpaintingEditorControls = document.querySelector('.drawing-board-controls')
+
+// let inpaintingEditorMetaControl = document.createElement('div')
+// inpaintingEditorMetaControl.className = 'drawing-board-control'
+// let initImageClearBtnToolbar = document.createElement('button')
+// initImageClearBtnToolbar.className = 'init_image_clear'
+// initImageClearBtnToolbar.innerHTML = 'Remove Image'
+// inpaintingEditorMetaControl.appendChild(initImageClearBtnToolbar)
+// inpaintingEditorControls.appendChild(inpaintingEditorMetaControl)
+
+let maskResetButton = document.querySelector('.drawing-board-control-navigation-reset')
+maskResetButton.innerHTML = 'Clear'
+maskResetButton.style.fontWeight = 'normal'
+maskResetButton.style.fontSize = '10pt'
let serverStatus = 'offline'
let activeTags = []
let lastPromptUsed = ''
let taskStopped = true
+let batchesDone = 0
function getLocalStorageItem(key, fallback) {
let item = localStorage.getItem(key)
@@ -571,6 +774,10 @@ function isModifiersPanelOpenEnabled() {
return getLocalStorageBoolItem(MODIFIERS_PANEL_OPEN_KEY, false)
}
+function isStreamImageProgressEnabled() {
+ return getLocalStorageBoolItem(STREAM_IMAGE_PROGRESS_KEY, false)
+}
+
function setStatus(statusType, msg, msgType) {
if (statusType !== 'server') {
return;
@@ -578,12 +785,12 @@ function setStatus(statusType, msg, msgType) {
if (msgType == 'error') {
// msg = '
' + msg + ''
- serverStatusColor.style.backgroundColor = 'red'
+ serverStatusColor.style.color = 'red'
serverStatusMsg.style.color = 'red'
serverStatusMsg.innerHTML = 'Stable Diffusion has stopped'
} else if (msgType == 'success') {
// msg = '' + msg + ''
- serverStatusColor.style.backgroundColor = 'green'
+ serverStatusColor.style.color = 'green'
serverStatusMsg.style.color = 'green'
serverStatusMsg.innerHTML = 'Stable Diffusion is ready'
serverStatus = 'online'
@@ -630,14 +837,37 @@ async function healthCheck() {
}
}
+function makeImageElement(width, height) {
+ let imgItem = document.createElement('div')
+ imgItem.className = 'imgItem'
+
+ let img = document.createElement('img')
+ img.width = parseInt(width)
+ img.height = parseInt(height)
+
+ imgItem.appendChild(img)
+ imagesContainer.appendChild(imgItem)
+
+ return imgItem
+}
+
// makes a single image. don't call this directly, use makeImage() instead
-async function doMakeImage(reqBody) {
+async function doMakeImage(reqBody, batchCount) {
if (taskStopped) {
return
}
let res = ''
let seed = reqBody['seed']
+ let numOutputs = parseInt(reqBody['num_outputs'])
+
+ let images = []
+
+ function makeImageContainers(numImages) {
+ for (let i = images.length; i < numImages; i++) {
+ images.push(makeImageElement(reqBody.width, reqBody.height))
+ }
+ }
try {
res = await fetch('/image', {
@@ -648,15 +878,82 @@ async function doMakeImage(reqBody) {
body: JSON.stringify(reqBody)
})
+ let reader = res.body.getReader()
+ let textDecoder = new TextDecoder()
+ let finalJSON = ''
+ let prevTime = -1
+ while (true) {
+ try {
+ let t = new Date().getTime()
+
+ const {value, done} = await reader.read()
+ if (done) {
+ break
+ }
+
+ let timeTaken = (prevTime === -1 ? -1 : t - prevTime)
+
+ let jsonStr = textDecoder.decode(value)
+
+ try {
+ let stepUpdate = JSON.parse(jsonStr)
+
+ if (stepUpdate.step === undefined) {
+ finalJSON += jsonStr
+ } else {
+ let batchSize = stepUpdate.total_steps
+ let overallStepCount = stepUpdate.step + batchesDone * batchSize
+ let totalSteps = batchCount * batchSize
+ let percent = 100 * (overallStepCount / totalSteps)
+ percent = (percent > 100 ? 100 : percent)
+ percent = percent.toFixed(0)
+
+ stepsRemaining = totalSteps - overallStepCount
+ stepsRemaining = (stepsRemaining < 0 ? 0 : stepsRemaining)
+ timeRemaining = (timeTaken === -1 ? '' : stepsRemaining * timeTaken) // ms
+
+ outputMsg.innerHTML = `Batch ${batchesDone+1} of ${batchCount}`
+ progressBar.innerHTML = `Generating image(s): ${percent}%`
+
+ if (timeTaken !== -1) {
+ progressBar.innerHTML += ` Time remaining (approx): ${millisecondsToStr(timeRemaining)}`
+ }
+ progressBar.style.display = 'block'
+
+ if (stepUpdate.output !== undefined) {
+ makeImageContainers(numOutputs)
+
+ for (idx in stepUpdate.output) {
+ let imgItem = images[idx]
+ let img = imgItem.firstChild
+ let tmpImageData = stepUpdate.output[idx]
+ img.src = tmpImageData['path'] + '?t=' + new Date().getTime()
+ }
+ }
+ }
+ } catch (e) {
+ finalJSON += jsonStr
+ }
+
+ prevTime = t
+ } catch (e) {
+ logError('Stable Diffusion had an error. Please check the logs in the command-line window.', res)
+ res = undefined
+ throw e
+ }
+ }
+
if (res.status != 200) {
if (serverStatus === 'online') {
- logError('Stable Diffusion had an error: ' + await res.text() + '. This happens sometimes. Maybe modify the prompt or seed a little bit?', res)
+ logError('Stable Diffusion had an error: ' + await res.text(), res)
} else {
- logError("Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed.", res)
+ logError("Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.", res)
}
res = undefined
+ progressBar.style.display = 'none'
} else {
- res = await res.json()
+ res = JSON.parse(finalJSON)
+ progressBar.style.display = 'none'
if (res.status !== 'succeeded') {
let msg = ''
@@ -680,7 +977,10 @@ async function doMakeImage(reqBody) {
}
} catch (e) {
console.log('request error', e)
+ logError('Stable Diffusion had an error. Please check the logs in the command-line window. ' + e + '' + e.stack + ' ', res)
setStatus('request', 'error', 'error')
+ progressBar.style.display = 'none'
+ res = undefined
}
if (!res) {
@@ -689,6 +989,8 @@ async function doMakeImage(reqBody) {
lastPromptUsed = reqBody['prompt']
+ makeImageContainers(res.output.length)
+
for (let idx in res.output) {
let imgBody = ''
let seed = 0
@@ -703,12 +1005,9 @@ async function doMakeImage(reqBody) {
continue
}
- let imgItem = document.createElement('div')
- imgItem.className = 'imgItem'
+ let imgItem = images[idx]
+ let img = imgItem.firstChild
- let img = document.createElement('img')
- img.width = parseInt(reqBody.width)
- img.height = parseInt(reqBody.height)
img.src = imgBody
let imgItemInfo = document.createElement('span')
@@ -726,19 +1025,19 @@ async function doMakeImage(reqBody) {
imgSaveBtn.className = 'imgSaveBtn'
imgSaveBtn.innerHTML = 'Download'
- imgItem.appendChild(img)
imgItem.appendChild(imgItemInfo)
imgItemInfo.appendChild(imgSeedLabel)
imgItemInfo.appendChild(imgUseBtn)
imgItemInfo.appendChild(imgSaveBtn)
- imagesContainer.appendChild(imgItem)
imgUseBtn.addEventListener('click', function() {
initImageSelector.value = null
initImagePreview.src = imgBody
initImagePreviewContainer.style.display = 'block'
+ inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'block'
+ maskSetting.checked = false
// maskSetting.style.display = 'block'
@@ -789,7 +1088,7 @@ async function makeImage() {
let validation = validateInput()
if (validation['isValid']) {
- outputMsg.innerHTML = 'Fetching..'
+ outputMsg.innerHTML = 'Starting..'
} else {
if (validation['error']) {
logError(validation['error'])
@@ -802,10 +1101,12 @@ async function makeImage() {
setStatus('request', 'fetching..')
makeImageBtn.innerHTML = 'Processing..'
+ makeImageBtn.disabled = true
makeImageBtn.style.display = 'none'
stopImageBtn.style.display = 'block'
taskStopped = false
+ batchesDone = 0
let seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value))
let numOutputsTotal = parseInt(numOutputsTotalField.value)
@@ -813,6 +1114,8 @@ async function makeImage() {
let batchCount = Math.ceil(numOutputsTotal / numOutputsParallel)
let batchSize = numOutputsParallel
+ let streamImageProgress = (numOutputsTotal > 50 ? false : streamImageProgressField.checked)
+
let prompt = promptField.value
if (activeTags.length > 0) {
let promptTags = activeTags.join(", ")
@@ -822,6 +1125,7 @@ async function makeImage() {
previewPrompt.innerHTML = prompt
let reqBody = {
+ session_id: sessionId,
prompt: prompt,
num_outputs: batchSize,
num_inference_steps: numInferenceStepsField.value,
@@ -831,7 +1135,10 @@ async function makeImage() {
// allow_nsfw: allowNSFWField.checked,
turbo: turboField.checked,
use_cpu: useCPUField.checked,
- use_full_precision: useFullPrecisionField.checked
+ use_full_precision: useFullPrecisionField.checked,
+ stream_progress_updates: true,
+ stream_image_progress: streamImageProgress,
+ show_only_filtered_image: showOnlyFilteredImageField.checked
}
if (IMAGE_REGEX.test(initImagePreview.src)) {
@@ -841,6 +1148,13 @@ async function makeImage() {
// if (IMAGE_REGEX.test(maskImagePreview.src)) {
// reqBody['mask'] = maskImagePreview.src
// }
+ if (maskSetting.checked) {
+ reqBody['mask'] = inpaintingEditor.getImg()
+ }
+
+ reqBody['sampler'] = 'ddim'
+ } else {
+ reqBody['sampler'] = samplerField.value
}
if (saveToDiskField.checked && diskPathField.value.trim() !== '') {
@@ -855,10 +1169,6 @@ async function makeImage() {
reqBody['use_upscale'] = upscaleModelField.value
}
- if (showOnlyFilteredImageField.checked && (useUpscalingField.checked || useFaceCorrectionField.checked)) {
- reqBody['show_only_filtered_image'] = showOnlyFilteredImageField.checked
- }
-
let time = new Date().getTime()
imagesContainer.innerHTML = ''
@@ -867,7 +1177,8 @@ async function makeImage() {
for (let i = 0; i < batchCount; i++) {
reqBody['seed'] = seed + (i * batchSize)
- let success = await doMakeImage(reqBody)
+ let success = await doMakeImage(reqBody, batchCount)
+ batchesDone++
if (success) {
outputMsg.innerHTML = 'Processed batch ' + (i+1) + '/' + batchCount
@@ -971,6 +1282,9 @@ useFullPrecisionField.checked = isUseFullPrecisionEnabled()
turboField.addEventListener('click', handleBoolSettingChange(USE_TURBO_MODE_KEY))
turboField.checked = isUseTurboModeEnabled()
+streamImageProgressField.addEventListener('click', handleBoolSettingChange(STREAM_IMAGE_PROGRESS_KEY))
+streamImageProgressField.checked = isStreamImageProgressEnabled()
+
diskPathField.addEventListener('change', handleStringSettingChange(DISK_PATH_KEY))
saveToDiskField.addEventListener('click', function(e) {
@@ -1007,8 +1321,8 @@ function updateGuidanceScale() {
function updateGuidanceScaleSlider() {
if (guidanceScaleField.value < 0) {
guidanceScaleField.value = 0
- } else if (guidanceScaleField.value > 20) {
- guidanceScaleField.value = 20
+ } else if (guidanceScaleField.value > 50) {
+ guidanceScaleField.value = 50
}
guidanceScaleSlider.value = guidanceScaleField.value * 10
@@ -1094,6 +1408,7 @@ checkRandomSeed()
function showInitImagePreview() {
if (initImageSelector.files.length === 0) {
initImagePreviewContainer.style.display = 'none'
+ // inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'none'
// maskSetting.style.display = 'none'
return
@@ -1106,9 +1421,10 @@ function showInitImagePreview() {
// console.log(file.name, reader.result)
initImagePreview.src = reader.result
initImagePreviewContainer.style.display = 'block'
+ inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'block'
-
- // maskSetting.style.display = 'block'
+ samplerSelectionContainer.style.display = 'none'
+ // maskSetting.checked = false
})
if (file) {
@@ -1118,24 +1434,37 @@ function showInitImagePreview() {
initImageSelector.addEventListener('change', showInitImagePreview)
showInitImagePreview()
+initImagePreview.addEventListener('load', function() {
+ inpaintingEditorCanvasBackground.style.backgroundImage = "url('" + this.src + "')"
+ // maskSetting.style.display = 'block'
+ // inpaintingEditorContainer.style.display = 'block'
+})
+
initImageClearBtn.addEventListener('click', function() {
initImageSelector.value = null
// maskImageSelector.value = null
initImagePreview.src = ''
// maskImagePreview.src = ''
+ maskSetting.checked = false
initImagePreviewContainer.style.display = 'none'
+ // inpaintingEditorContainer.style.display = 'none'
// maskImagePreviewContainer.style.display = 'none'
// maskSetting.style.display = 'none'
promptStrengthContainer.style.display = 'none'
+ samplerSelectionContainer.style.display = 'block'
+})
+
+maskSetting.addEventListener('click', function() {
+ inpaintingEditorContainer.style.display = (this.checked ? 'block' : 'none')
})
// function showMaskImagePreview() {
// if (maskImageSelector.files.length === 0) {
-// maskImagePreviewContainer.style.display = 'none'
+// // maskImagePreviewContainer.style.display = 'none'
// return
// }
@@ -1143,8 +1472,8 @@ initImageClearBtn.addEventListener('click', function() {
// let file = maskImageSelector.files[0]
// reader.addEventListener('load', function() {
-// maskImagePreview.src = reader.result
-// maskImagePreviewContainer.style.display = 'block'
+// // maskImagePreview.src = reader.result
+// // maskImagePreviewContainer.style.display = 'block'
// })
// if (file) {
@@ -1157,8 +1486,32 @@ initImageClearBtn.addEventListener('click', function() {
// maskImageClearBtn.addEventListener('click', function() {
// maskImageSelector.value = null
// maskImagePreview.src = ''
-// maskImagePreviewContainer.style.display = 'none'
+// // maskImagePreviewContainer.style.display = 'none'
// })
+
+// https://stackoverflow.com/a/8212878
+function millisecondsToStr(milliseconds) {
+ function numberEnding (number) {
+ return (number > 1) ? 's' : '';
+ }
+
+ var temp = Math.floor(milliseconds / 1000);
+ var hours = Math.floor((temp %= 86400) / 3600);
+ var s = ''
+ if (hours) {
+ s += hours + ' hour' + numberEnding(hours) + ' ';
+ }
+ var minutes = Math.floor((temp %= 3600) / 60);
+ if (minutes) {
+ s += minutes + ' minute' + numberEnding(minutes) + ' ';
+ }
+ var seconds = temp % 60;
+ if (!hours && minutes < 4 && seconds) {
+ s += seconds + ' second' + numberEnding(seconds);
+ }
+
+ return s;
+}
-
diff --git a/ui/media/drawingboard.min.css b/ui/media/drawingboard.min.css
new file mode 100644
index 00000000..7d804787
--- /dev/null
+++ b/ui/media/drawingboard.min.css
@@ -0,0 +1,5 @@
+/* drawingboard.js v0.4.6 - https://github.com/Leimi/drawingboard.js
+* Copyright (c) 2015 Emmanuel Pelletier
+* Licensed MIT */
+
+.drawing-board,.drawing-board *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.drawing-board-controls-hidden,.drawing-board-utils-hidden{display:none!important}.drawing-board{position:relative;display:block}.drawing-board-canvas-wrapper{position:relative;margin:0;border:1px solid #ddd}.drawing-board-canvas{position:absolute;top:0;left:0;width:auto;cursor:crosshair;z-index:20}.drawing-board-cursor{position:absolute;top:0;left:0;pointer-events:none;border-radius:50%;background:#ccc;background:rgba(0,0,0,.2);z-index:30}.drawing-board-control-colors-rainbows,.drawing-board-control-size .drawing-board-control-inner,.drawing-board-control-size-dropdown,.drawing-board-control>button{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;overflow:hidden;background-color:#eee;padding:2px 4px;border:1px solid #ccc;box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);height:28px}.drawing-board-control>button{cursor:pointer;min-width:28px;line-height:14px}.drawing-board-control>button:focus,.drawing-board-control>button:hover{background-color:#ddd}.drawing-board-control>button.active,.drawing-board-control>button:active{box-shadow:inset 0 1px 2px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 2px 0 rgba(0,0,0,.2);background-color:#ddd}.drawing-board-control>button[disabled]{color:gray}.drawing-board-control>button[disabled].active,.drawing-board-control>button[disabled]:active,.drawing-board-control>button[disabled]:focus,.drawing-board-control>button[disabled]:hover{background-color:#eee;box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 3px -2px #121212,inset 0 2px 5px 0 rgba(255,255,255,.3);cursor:default}.drawing-board-controls{margin:0 auto;text-align:center;font-size:0;display:table;border-spacing:9.33px 0;position:relative;min-height:28px}.drawing-board-controls[data-align=left]{margin:0;left:-9.33px}.drawing-board-controls[data-align=right]{margin:0 0 0 auto;right:-9.33px}.drawing-board-canvas-wrapper+.drawing-board-controls,.drawing-board-controls+.drawing-board-canvas-wrapper{margin-top:5px}.drawing-board-controls-hidden{height:0;min-height:0;padding:0;margin:0;border:0}.drawing-board-control{display:table-cell;border-collapse:separate;vertical-align:middle;font-size:16px;height:100%}.drawing-board-control-inner{position:relative;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.drawing-board-control>button{margin:0;vertical-align:middle}.drawing-board-control-colors{font-size:0;line-height:0}.drawing-board-control-colors-current{border:1px solid #ccc;cursor:pointer;display:inline-block;width:26px;height:26px}.drawing-board-control-colors-rainbows{display:inline-block;position:absolute;left:0;top:33px;margin-left:0;z-index:100;width:250px;height:auto;padding:4px}.drawing-board-control-colors-rainbow{height:18px}.drawing-board-control-colors-picker:first-child{margin-right:5px}.drawing-board-control-colors-picker{display:inline-block;width:18px;height:18px;cursor:pointer}.drawing-board-control-colors-picker[data-color="rgba(255, 255, 255, 1)"]{width:16px;height:17px;border:1px solid #ccc;border-bottom:none}.drawing-board-control-colors-picker:hover{width:16px;height:16px;border:1px solid #555}.drawing-board-control-drawingmode>button{margin-right:2px}.drawing-board-control-drawingmode>button:last-child{margin-right:0}.drawing-board-control-drawingmode-pencil-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe9JREFUeNpiZAACVlFRBhYREQZcQPnbNwa3N28YlL5+ZfgLFfvPwGD9m4FhIgsDHuAO0gTUDNKIBvyBmqt/MTDMY8Gl0f31azD7L6oUIxCnAzWmAPHBfwwM01AMUAV6JfPQIVwOYgVqqPnFyOjz6///O38YGKpAgmAD1OXlGdTk5PD5hgeouZudj8/uy9evP/78/dsFFPsJNiAoKIiBABAHap4oLi9v8fTNm48//v7NBwbgWZgkE7rqt8DY+A8JZRBW+cfIuEDT0NDlzadP3z98/doPFDuCrB7TAGFhBqCNIGwM9OcKUzs7+xdv3355+f79VqDYAiTDwZgJh7ONgYpnOvn4GL949erT7UePdgL5JVCD4fgBLBBxaX74+PG789evnwby0/8jKXgExIeB+CG6Af///1e9Ki9vFSAkZPzoyZPPJy9evA9MB77/sWiEARZkzV+/fvXYtGnTpG3btj28EBT0BqjZ5D8OjXCwPksUhA1Wpggf/PHjx/9169Y9EBERaUlgZmaIAcrLE4rk5sIqBqDmlefnRPzfWGX5EaSZm5ubgRloADGA5QZ3RgK7gESY4PMNn9ZtObPpzZvfU4DiYkiB/RcHG+S7fyxAMH/lFU2GOZd2bLx18/cEUMoD4j9I+DcS/RtJHGTYf4AAAwAxaOMYHjxKFwAAAABJRU5ErkJggg==);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-pencil-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-drawingmode-eraser-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkpJREFUeNp0kk+IElEcx39vFBc9+OfQRTAwzFt4CaYOKStj6MoeculStzoIQSB4kCVckmDx4iGCXWYJIqjoVOzO1l4qT1F7WfBWHvxzDPyTB3XUmXn93suRybUffHmP997n9/cRsFgwGARJkiAcDsPlwgEIeEZQAhCRAkgAlOD6SQP4rgMFDWVnYCAQgFgsBqFQCBwOByzZNQOotPHx1RNCCCipu6bfb+zSnslkeOQVILPrBkAirbws9btdTEWAzZPXpfepOzaeGMBXwe/3w3+MwTc3Dl+UeghTiskbBvR6Pbh18mZHB0jjmxvCKhIfR37s3r+Sevf8ca/T4TBF2HTSODuDxP7uNjrZFFbBk8lEzOVyspa4ykGYw2zfbTb/7ilvok1YhlVVFfP5vDydTkHXdXDdlhZOOnPY4/HA0YPtp3h6LFjh8XgsFgoFGTPgsKm1zDr8ajTQh8Fh5eGjZzjGI8yjKlgjF4tFGdd/YKYmRja24hw+zu3sYe2HiH3hYzQjl8tleTQanWtou93G6Qngdrth6+1+9h6hTULJZ/PeziJXKhV5OByeg1ut1gJOp9NZTdNOcQ419ot+ggp1qoLdBFmqVmNpm3A8Huewy+Wq1RH8QH9zmBlJJpMRdCIqiiIPBgN+2MCGsW/r8/kgGo1m0fmpzWarseayHlmNeL1eFiWC0cRqtSr3+/3FpSiKHMZtjU1glbFyfKgLTqfzEka9OJvNeDnzz1JnCaFmqOl8ZdJY1SiDOXCiXKg1NtG5DIt0y6ov3dE/AgwAENFWYYLj4mYAAAAASUVORK5CYII=);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-eraser-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-drawingmode-filler-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnNJREFUeNp0k0trE1EUx89MJpNJooYYXBgDNtCKdRPwlbqoCKUtaNVNA0Uo7UbMxoVPEARTXEi+QWfnwn6DEAlEkrSLttTGRiULEQlJ8yChmbzI++E50yTUJA78uMy953/u/557LmOz2WDEZ2m1WrckSRJSqdR2tVrdHQyYebwHtVoNuFHqTqczhQnWKaBYLDoKhcIuzgHDMKBSqeD20qd+LNdsNocSoFhRr9ctpVLJigl4xIIJQizLAmG4cAPa7bYcy9Iug5TL5UYikbD6/X7Rbre/IUcYe3WUW5ZsnQQzW9LpNOPz+UQc5aBM5mgdh7vI9FCCAesW2tnr9YqZTAby+bw8f3AQRP6853n+Ph5hemSCntjj8YjZbFYWx2IxeS2RSEMwuA87O79eqdXquVolK+GxnP0EPbHb7RZJSGABIR6PA11zJHKIR2MhHA5DIPDj7eH3j95KpfK60Wg8Yntil8slkqgnpioLghacTidoNDpEC3q9HnheCc3s1jZeLcW943pirPw/4lKpBkqlDubnl/riycnLsLy88EKj0fhzuRyZv8RFo1E6wpBYkiqy7Z54YmIcVlYeyOKC4mYwJ0nHRaQuM5vNT6hB/iceG7sIq6sPnwmC4MerDkby40AOCCoiddie1Wp92W7zQ2KTyQSLizNP8T0EsPLBbxEDnCj0GkM2qIEwyZRCobizsfH5A1ZXFhuN52F29vpz3HkL574mk8lj24Y5wsHkvjjoX0BOIWc5jruHzbK2ufmzEwpFO3jnDhQv4JoROYdoERVyGjEgZ8iBDlF3FzXo4go6utZ9lftY4N/dXisjR0i1G0ublv8KMAA0ZoUlicxrhwAAAABJRU5ErkJggg==);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-drawingmode-filler-button:before{content:"";display:block;width:0;height:100%}.drawing-board-control-navigation>button{font-family:Helvetica,Arial,sans-serif;font-size:14px;font-weight:700;margin-right:2px}.drawing-board-control-navigation>button:last-child{margin-right:0}.drawing-board-control-size[data-drawing-board-type=range] .drawing-board-control-inner{width:75px}.drawing-board-control-size[data-drawing-board-type=dropdown] .drawing-board-control-inner{overflow:visible}.drawing-board-control-size-range-input{position:relative;width:100%;z-index:100;margin:0;padding:0;border:0}.drawing-board-control-size-dropdown span,.drawing-board-control-size-dropdown-current span,.drawing-board-control-size-range-current{display:block;background:#333;opacity:.8}.drawing-board-control-size-range-current{display:inline-block;opacity:.15;position:absolute;pointer-events:none;left:50%;top:50%;z-index:50}.drawing-board-control-size-dropdown-current{display:block;height:100%;width:40px;overflow:hidden;position:relative}.drawing-board-control-size-dropdown-current span{position:absolute;left:50%;top:50%}.drawing-board-control-size-dropdown{position:absolute;left:-6px;top:33px;height:auto;list-style-type:none;margin:0;padding:0;z-index:100}.drawing-board-control-size-dropdown li{display:block;padding:4px;margin:3px 0;min-height:16px}.drawing-board-control-size-dropdown li:hover{background:#ccc}.drawing-board-control-size-dropdown span{margin:0 auto}.drawing-board-control-download-button{overflow:hidden;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoBJREFUeNqMkr9PU1EUx7/vR1tQ3yu10hAmTawtBSYSy2YccFOcnDQm/gewOLnj5GYMg4sdXFxkMca4OBAwdUBe/ZkIGFp+9tHXvh/3/fTcAm01aLzJybnn3nM+95tzrnDl6Tb+sibuTmWUWj3C6/Juk+LySUmyvt0FCKKA02ryOCy6LBiu15ngMbZ5DDCNBqIw6gKM+n4nECUJru3glKry8CpjQaHVYmC2rVH82DIMMMdGGARdwJ+SPNdFS9chx+MXDNMp/NzagWNatk/nQU/hiYAoih6FYTBCBs9zUXMCbAhx2OYOv351lPOJ3EwH4LteL6Dcp/Rfu3FrstDyIizt+agpaYxNDU0M9gl4v7Ck+TYrCYLQqZHUyTtdQBiutPSGUflczSXHs5lVKwZdSOBMvwztxVvN0RtzsiyXBFHsAvL5PBSnCpXV2getILFiE2SjspYbuZzPiDSZ2vOXmlvX5yQqTmMfg9ZXqtls1wnT09OHEyAq0aFLg/gSXsSWq9wWk+p9PrCoYTwcijdLOfE7UsEufN9HGIYnT4EnTGIXe1KqtNNIvuNnGamxfi7SgQD/nIJCTbzOPQ/SQh1pud7T4M6W/8qFIw/5WAr5m7Ozsw9UVc069Fls2yJzSC5/lnc9RhaHZVnfSqUnEgXP2oBqtYqBgYG2+mKxmOVADnAcB4yxHgD1RzehKKns/LyV4gUHBweQy+UyRkdH6UKJ6fQDFxcXoWkaXJeRuTgUGCdLQJ9bx72lGZimGWs2m+083oN+2iiFQiGxvLy8RrDzudyltgrG3N8U2G8CrPz4sGYYRqJSqWR4H/jNWbJhUjAWi8XG8R/L87yPpGCVttVfAgwAVpZR+8tZC08AAAAASUVORK5CYII=);background-position:50% 50%;background-repeat:no-repeat}.drawing-board-control-download-button:before{content:"";display:block;width:0;height:100%}
\ No newline at end of file
diff --git a/ui/media/drawingboard.min.js b/ui/media/drawingboard.min.js
new file mode 100644
index 00000000..289d40ae
--- /dev/null
+++ b/ui/media/drawingboard.min.js
@@ -0,0 +1,4 @@
+/* drawingboard.js v0.4.6 - https://github.com/Leimi/drawingboard.js
+* Copyright (c) 2015 Emmanuel Pelletier
+* Licensed MIT */
+!function(){"use strict";function a(a,b){for(;a.length>b;)a.shift()}var b=function(a){var b=a?a:{},c={provider:function(){throw new Error("No provider!")},maxLength:30,onUpdate:function(){}};this.provider="undefined"!=typeof b.provider?b.provider:c.provider,this.maxLength="undefined"!=typeof b.maxLength?b.maxLength:c.maxLength,this.onUpdate="undefined"!=typeof b.onUpdate?b.onUpdate:c.onUpdate,this.initialItem=null,this.clear()};b.prototype.initialize=function(a){this.stack[0]=a,this.initialItem=a},b.prototype.clear=function(){this.stack=[this.initialItem],this.position=0,this.onUpdate()},b.prototype.save=function(){this.provider(function(b){a(this.stack,this.maxLength),this.position=Math.min(this.position,this.stack.length-1),this.stack=this.stack.slice(0,this.position+1),this.stack.push(b),this.position++,this.onUpdate()}.bind(this))},b.prototype.undo=function(a){if(this.canUndo()){var b=this.stack[--this.position];this.onUpdate(),a&&a(b)}},b.prototype.redo=function(a){if(this.canRedo()){var b=this.stack[++this.position];this.onUpdate(),a&&a(b)}},b.prototype.canUndo=function(){return this.position>0},b.prototype.canRedo=function(){return this.positionh;h++){if(g=g[e[h]],g===a)throw"tim: '"+e[h]+"' not found in "+b;if(h===f-1)return g}})}}(),DrawingBoard.Utils.MicroEvent=function(){},DrawingBoard.Utils.MicroEvent.prototype={bind:function(a,b){this._events=this._events||{},this._events[a]=this._events[a]||[],this._events[a].push(b)},unbind:function(a,b){this._events=this._events||{},a in this._events!=!1&&this._events[a].splice(this._events[a].indexOf(b),1)},trigger:function(a){if(this._events=this._events||{},a in this._events!=!1)for(var b=0;b=0;g--)f+=parseInt(a.css(e[g]).replace("px",""),10);return f},DrawingBoard.Utils.boxBorderWidth=function(a,b,c){return DrawingBoard.Utils._boxBorderSize(a,b,c,"width")},DrawingBoard.Utils.boxBorderHeight=function(a,b,c){return DrawingBoard.Utils._boxBorderSize(a,b,c,"height")},DrawingBoard.Utils.isColor=function(a){return a&&a.length?/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a)||-1!==$.inArray(a.substring(0,3),["rgb","hsl"]):!1},DrawingBoard.Utils.RGBToInt=function(a,b,c){var d=0;return d|=(255&a)<<16,d|=(255&b)<<8,d|=255&c},DrawingBoard.Utils.pixelAt=function(a,b,c){var d=4*(c*a.width+b),e=DrawingBoard.Utils.RGBToInt(a.data[d],a.data[d+1],a.data[d+2]);return[d,b,c,e]},DrawingBoard.Utils.compareColors=function(a,b,c){if(0===c)return a===b;var d=a>>16&255,e=b>>16&255,f=a>>8&255,g=b>>8&255,h=255&a,i=255&b;return Math.abs(d-e)<=c&&Math.abs(f-g)<=c&&Math.abs(h-i)<=c},function(){for(var a=["ms","moz","webkit","o"],b=0;b-1?c+='
':c='
'+c,this.$el.addClass("drawing-board").append(c),this.dom={$canvasWrapper:this.$el.find(".drawing-board-canvas-wrapper"),$canvas:this.$el.find(".drawing-board-canvas"),$cursor:this.$el.find(".drawing-board-cursor"),$controls:this.$el.find(".drawing-board-controls")},$.each(["left","right","center"],$.proxy(function(a,b){return this.opts.controlsPosition.indexOf(b)>-1?(this.dom.$controls.attr("data-align",b),!1):void 0},this)),this.canvas=this.dom.$canvas.get(0),this.ctx=this.canvas&&this.canvas.getContext&&this.canvas.getContext("2d")?this.canvas.getContext("2d"):null,this.color=this.opts.color,this.ctx?(this.storage=this._getStorage(),this.initHistory(),this.reset({webStorage:!1,history:!1,background:!1}),this.initControls(),this.resize(),this.reset({webStorage:!1,history:!1,background:!0}),this.restoreWebStorage(),this.initDropEvents(),void this.initDrawEvents()):(this.opts.errorMessage&&this.$el.html(this.opts.errorMessage),!1)},DrawingBoard.Board.defaultOpts={controls:["Color","DrawingMode","Size","Navigation"],controlsPosition:"top left",color:"#000000",size:1,background:"#fff",eraserColor:"background",fillTolerance:100,fillHack:!0,webStorage:"session",droppable:!1,enlargeYourContainer:!1,errorMessage:'It seems you use an obsolete browser. Update it to start drawing.
',stretchImg:!1},DrawingBoard.Board.prototype={mergeOptions:function(a){return a=$.extend({},DrawingBoard.Board.defaultOpts,a),a.background||"background"!==a.eraserColor||(a.eraserColor="transparent"),a},reset:function(a){a=$.extend({color:this.opts.color,size:this.opts.size,webStorage:!0,history:!0,background:!1},a),this.setMode("pencil"),a.background&&this.resetBackground(this.opts.background,$.proxy(function(){a.history&&this.saveHistory()},this)),a.color&&this.setColor(a.color),a.size&&(this.ctx.lineWidth=a.size),this.ctx.lineCap="round",this.ctx.lineJoin="round",a.webStorage&&this.saveWebStorage(),a.history&&!a.background&&this.saveHistory(),this.blankCanvas=this.getImg(),this.ev.trigger("board:reset",a)},resetBackground:function(a,b){a=a||this.opts.background;var c=DrawingBoard.Utils.isColor(a),d=this.getMode();this.setMode("pencil"),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),c?(this.ctx.fillStyle=a,this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.history.initialize(this.getImg()),b&&b()):a&&this.setImg(a,{callback:$.proxy(function(){this.history.initialize(this.getImg()),b&&b()},this)}),this.setMode(d)},resize:function(){this.dom.$controls.toggleClass("drawing-board-controls-hidden",!this.controls||!this.controls.length);var a,b,c=[this.$el.width(),DrawingBoard.Utils.boxBorderWidth(this.$el),DrawingBoard.Utils.boxBorderWidth(this.dom.$canvasWrapper,!0,!0)],d=[this.$el.height(),DrawingBoard.Utils.boxBorderHeight(this.$el),this.dom.$controls.height(),DrawingBoard.Utils.boxBorderHeight(this.dom.$controls,!1,!0),DrawingBoard.Utils.boxBorderHeight(this.dom.$canvasWrapper,!0,!0)],e=function(a,b){b=b||1;for(var c=a[0],d=1;d0&&q.push(DrawingBoard.Utils.pixelAt(c,p[e]-1,p[f])),p[e]0&&q.push(DrawingBoard.Utils.pixelAt(c,p[e],p[f]-1)),p[f]10&&this.isMouseHovering){this.dom.$cursor.css({width:this.ctx.lineWidth+"px",height:this.ctx.lineWidth+"px"});var a=DrawingBoard.Utils.tpl("translateX({{x}}px) translateY({{y}}px)",{x:this.coords.current.x-this.ctx.lineWidth/2,y:this.coords.current.y-this.ctx.lineWidth/2});this.dom.$cursor.css({transform:a,"-webkit-transform":a,"-ms-transform":a}),this.dom.$cursor.removeClass("drawing-board-utils-hidden")}else this.dom.$cursor.addClass("drawing-board-utils-hidden");if(this.isDrawing){var b=this._getMidInputCoords(this.coords.current);this.ctx.beginPath(),this.ctx.moveTo(b.x,b.y),this.ctx.quadraticCurveTo(this.coords.old.x,this.coords.old.y,this.coords.oldMid.x,this.coords.oldMid.y),this.ctx.stroke(),this.coords.old=this.coords.current,this.coords.oldMid=b}window.requestAnimationFrame&&requestAnimationFrame($.proxy(function(){this.draw()},this))},_onInputStart:function(a,b){this.coords.current=this.coords.old=b,this.coords.oldMid=this._getMidInputCoords(b),this.isDrawing=!0,window.requestAnimationFrame||this.draw(),this.ev.trigger("board:startDrawing",{e:a,coords:b}),a.stopPropagation(),a.preventDefault()},_onInputMove:function(a,b){this.coords.current=b,this.ev.trigger("board:drawing",{e:a,coords:b}),window.requestAnimationFrame||this.draw(),a.stopPropagation(),a.preventDefault()},_onInputStop:function(a,b){!this.isDrawing||a.touches&&0!==a.touches.length||(this.isDrawing=!1,this.saveWebStorage(),this.saveHistory(),this.ev.trigger("board:stopDrawing",{e:a,coords:b}),this.ev.trigger("board:userAction"),a.stopPropagation(),a.preventDefault())},_onMouseOver:function(a,b){this.isMouseHovering=!0,this.coords.old=this._getInputCoords(a),this.coords.oldMid=this._getMidInputCoords(this.coords.old),this.ev.trigger("board:mouseOver",{e:a,coords:b})},_onMouseOut:function(a,b){this.isMouseHovering=!1,this.ev.trigger("board:mouseOut",{e:a,coords:b})},_getInputCoords:function(a){a=a.originalEvent?a.originalEvent:a;var b,c,d=this.canvas.getBoundingClientRect(),e=this.dom.$canvas.width(),f=this.dom.$canvas.height();return a.touches&&1==a.touches.length?(b=a.touches[0].pageX,c=a.touches[0].pageY):(b=a.pageX,c=a.pageY),b-=this.dom.$canvas.offset().left,c-=this.dom.$canvas.offset().top,b*=e/d.width,c*=f/d.height,{x:b,y:c}},_getMidInputCoords:function(a){return{x:this.coords.old.x+a.x>>1,y:this.coords.old.y+a.y>>1}}},DrawingBoard.Control=function(a,b){return this.board=a,this.opts=$.extend({},this.defaults,b),this.$el=$(document.createElement("div")).addClass("drawing-board-control"),this.name&&this.$el.addClass("drawing-board-control-"+this.name),this.board.ev.bind("board:reset",$.proxy(this.onBoardReset,this)),this.initialize.apply(this,arguments),this},DrawingBoard.Control.prototype={name:"",defaults:{},initialize:function(){},addToBoard:function(){this.board.addControl(this)},onBoardReset:function(){}},DrawingBoard.Control.extend=function(a,b){var c,d=this;c=a&&a.hasOwnProperty("constructor")?a.constructor:function(){return d.apply(this,arguments)},$.extend(c,d,b);var e=function(){this.constructor=c};return e.prototype=d.prototype,c.prototype=new e,a&&$.extend(c.prototype,a),c.__super__=d.prototype,c},DrawingBoard.Control.Color=DrawingBoard.Control.extend({name:"colors",initialize:function(){this.initTemplate();var a=this;this.$el.on("click",".drawing-board-control-colors-picker",function(b){var c=$(this).attr("data-color");a.board.setColor(c),a.$el.find(".drawing-board-control-colors-current").css("background-color",c).attr("data-color",c),a.board.ev.trigger("color:changed",c),a.$el.find(".drawing-board-control-colors-rainbows").addClass("drawing-board-utils-hidden"),b.preventDefault()}),this.$el.on("click",".drawing-board-control-colors-current",function(b){a.$el.find(".drawing-board-control-colors-rainbows").toggleClass("drawing-board-utils-hidden"),b.preventDefault()}),$("body").on("click",function(b){var c=$(b.target),d=c.hasClass("drawing-board-control-colors-current")?c:c.closest(".drawing-board-control-colors-current"),e=a.$el.find(".drawing-board-control-colors-current"),f=a.$el.find(".drawing-board-control-colors-rainbows");d.length&&d.get(0)===e.get(0)||f.hasClass("drawing-board-utils-hidden")||f.addClass("drawing-board-utils-hidden")})},initTemplate:function(){var a='',b='
',c="";$.each([.75,.5,.25],$.proxy(function(a,d){var e=0,f=null;for(c+='',.25==d&&(f=this._rgba(0,0,0,1)),.5==d&&(f=this._rgba(150,150,150,1)),.75==d&&(f=this._rgba(255,255,255,1)),c+=DrawingBoard.Utils.tpl(b,{color:f.toString()});330>=e;)c+=DrawingBoard.Utils.tpl(b,{color:this._hsl2Rgba(this._hsl(e-60,1,d)).toString()}),e+=30;c+="
"},this)),this.$el.append($(DrawingBoard.Utils.tpl(a,{color:this.board.color,rainbows:c}))),this.$el.find(".drawing-board-control-colors-rainbows").addClass("drawing-board-utils-hidden")},onBoardReset:function(){this.board.setColor(this.$el.find(".drawing-board-control-colors-current").attr("data-color"))},_rgba:function(a,b,c,d){return{r:a,g:b,b:c,a:d,toString:function(){return"rgba("+a+", "+b+", "+c+", "+d+")"}}},_hsl:function(a,b,c){return{h:a,s:b,l:c,toString:function(){return"hsl("+a+", "+100*b+"%, "+100*c+"%)"}}},_hex2Rgba:function(a){var b=parseInt(a.substring(1),16);return this._rgba(b>>16,b>>8&255,255&b,1)},_hsl2Rgba:function(a){function b(a,b,c){return 0>c&&(c+=1),c>1&&(c-=1),1/6>c?a+6*(b-a)*c:.5>c?b:2/3>c?a+(b-a)*(2/3-c)*6:a}var c,d,e,f=a.h/360,g=a.s,h=a.l;if(0===g)c=d=e=h;else{var i=.5>h?h*(1+g):h+g-h*g,j=2*h-i;c=Math.floor(255*b(j,i,f+1/3)),d=Math.floor(255*b(j,i,f)),e=Math.floor(255*b(j,i,f-1/3))}return this._rgba(c,d,e,1)}}),DrawingBoard.Control.DrawingMode=DrawingBoard.Control.extend({name:"drawingmode",defaults:{pencil:!0,eraser:!0,filler:!0},initialize:function(){this.prevMode=this.board.getMode(),$.each(["pencil","eraser","filler"],$.proxy(function(a,b){this.opts[b]&&this.$el.append(' ')},this)),this.$el.on("click","button[data-mode]",$.proxy(function(a){var b=$(a.currentTarget).attr("data-mode"),c=this.board.getMode();c!==b&&(this.prevMode=c);var d=c===b?this.prevMode:b;this.board.setMode(d),a.preventDefault()},this)),this.board.ev.bind("board:mode",$.proxy(function(a){this.toggleButtons(a)},this)),this.toggleButtons(this.board.getMode())},toggleButtons:function(a){this.$el.find("button[data-mode]").each(function(b,c){var d=$(c);d.toggleClass("active",a===d.attr("data-mode"))})}}),DrawingBoard.Control.Navigation=DrawingBoard.Control.extend({name:"navigation",defaults:{back:!0,forward:!0,reset:!0},initialize:function(){var a="";if(this.opts.back&&(a+='← '),this.opts.forward&&(a+='→ '),this.opts.reset&&(a+='× '),this.$el.append(a),this.opts.back){var b=this.$el.find(".drawing-board-control-navigation-back");this.board.ev.bind("historyNavigation",$.proxy(this.updateBack,this,b)),this.$el.on("click",".drawing-board-control-navigation-back",$.proxy(function(a){this.board.goBackInHistory(),a.preventDefault()},this)),this.updateBack(b)}if(this.opts.forward){var c=this.$el.find(".drawing-board-control-navigation-forward");this.board.ev.bind("historyNavigation",$.proxy(this.updateForward,this,c)),this.$el.on("click",".drawing-board-control-navigation-forward",$.proxy(function(a){this.board.goForthInHistory(),a.preventDefault()},this)),this.updateForward(c)}this.opts.reset&&this.$el.on("click",".drawing-board-control-navigation-reset",$.proxy(function(a){this.board.reset({background:!0}),a.preventDefault()},this))},updateBack:function(a){this.board.history.canUndo()?a.removeAttr("disabled"):a.attr("disabled","disabled")},updateForward:function(a){this.board.history.canRedo()?a.removeAttr("disabled"):a.attr("disabled","disabled")}}),DrawingBoard.Control.Size=DrawingBoard.Control.extend({name:"size",defaults:{type:"auto",dropdownValues:[1,3,6,10,20,30,40,50],min:1,max:50},types:["dropdown","range"],initialize:function(){"auto"==this.opts.type&&(this.opts.type=this._iHasRangeInput()?"range":"dropdown");var a=$.inArray(this.opts.type,this.types)>-1?this["_"+this.opts.type+"Template"]():!1;if(!a)return!1;this.val=this.board.opts.size,this.$el.append($(a)),this.$el.attr("data-drawing-board-type",this.opts.type),this.updateView();var b=this;"range"==this.opts.type&&this.$el.on("change",".drawing-board-control-size-range-input",function(a){b.val=$(this).val(),b.updateView(),b.board.ev.trigger("size:changed",b.val),a.preventDefault()}),"dropdown"==this.opts.type&&(this.$el.on("click",".drawing-board-control-size-dropdown-current",$.proxy(function(){this.$el.find(".drawing-board-control-size-dropdown").toggleClass("drawing-board-utils-hidden")},this)),this.$el.on("click","[data-size]",function(a){b.val=parseInt($(this).attr("data-size"),0),b.updateView(),b.board.ev.trigger("size:changed",b.val),a.preventDefault()}))},_rangeTemplate:function(){var a='
';return DrawingBoard.Utils.tpl(a,{min:this.opts.min,max:this.opts.max,size:this.board.opts.size})},_dropdownTemplate:function(){var a='
';return $.each(this.opts.dropdownValues,function(b,c){a+=DrawingBoard.Utils.tpl(' ',{size:c})}),a+=" "},onBoardReset:function(){this.updateView()},updateView:function(){var a=this.val;if(this.board.ctx.lineWidth=a,this.$el.find(".drawing-board-control-size-range-current, .drawing-board-control-size-dropdown-current span").css({width:a+"px",height:a+"px",borderRadius:a+"px",marginLeft:-1*a/2+"px",marginTop:-1*a/2+"px"}),this.$el.find(".drawing-board-control-inner").attr("title",a),"dropdown"==this.opts.type){var b=null;$.each(this.opts.dropdownValues,function(c,d){(null===b||Math.abs(d-a)'),this.$el.on("click",".drawing-board-control-download-button",$.proxy(function(a){this.board.downloadImg(),a.preventDefault()},this))}});
\ No newline at end of file
diff --git a/ui/media/favicon-16x16.png b/ui/media/favicon-16x16.png
new file mode 100644
index 00000000..7a33c0b1
Binary files /dev/null and b/ui/media/favicon-16x16.png differ
diff --git a/ui/media/favicon-32x32.png b/ui/media/favicon-32x32.png
new file mode 100644
index 00000000..3b21168b
Binary files /dev/null and b/ui/media/favicon-32x32.png differ
diff --git a/ui/media/jquery-3.6.1.min.js b/ui/media/jquery-3.6.1.min.js
new file mode 100644
index 00000000..2c69bc90
--- /dev/null
+++ b/ui/media/jquery-3.6.1.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",v.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0= 4:
+ old_eps.pop(0)
+- if callback: callback(i)
+- if img_callback: img_callback(pred_x0, i)
++ if callback: yield from callback(i)
++ if img_callback: yield from img_callback(pred_x0, i)
+
+- return img
++ yield from img_callback(img, len(iterator)-1)
+
+ @torch.no_grad()
+ def p_sample_plms(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False,
+@@ -706,7 +715,8 @@ class UNet(DDPM):
@torch.no_grad()
def ddim_sampling(self, x_latent, cond, t_start, unconditional_guidance_scale=1.0, unconditional_conditioning=None,
@@ -22,13 +100,233 @@ index dcf7901..1f99adc 100644
timesteps = self.ddim_timesteps
timesteps = timesteps[:t_start]
-@@ -710,6 +712,9 @@ class UNet(DDPM):
- x_dec = self.p_sample_ddim(x_dec, cond, ts, index=index, use_original_steps=use_original_steps,
+@@ -730,10 +740,13 @@ class UNet(DDPM):
unconditional_guidance_scale=unconditional_guidance_scale,
unconditional_conditioning=unconditional_conditioning)
-+
-+ if callback: callback(i)
-+ if img_callback: img_callback(x_dec, i)
++ if callback: yield from callback(i)
++ if img_callback: yield from img_callback(x_dec, i)
++
if mask is not None:
- return x0 * mask + (1. - mask) * x_dec
+- return x0 * mask + (1. - mask) * x_dec
++ x_dec = x0 * mask + (1. - mask) * x_dec
+
+- return x_dec
++ yield from img_callback(x_dec, len(iterator)-1)
+
+
+ @torch.no_grad()
+@@ -779,13 +792,16 @@ class UNet(DDPM):
+
+
+ @torch.no_grad()
+- def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
++ def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
++ img_callback=None):
+ """Implements Algorithm 2 (Euler steps) from Karras et al. (2022)."""
+ extra_args = {} if extra_args is None else extra_args
+ cvd = CompVisDenoiser(ac)
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running Euler Sampling with {len(sigmas) - 1} timesteps")
++
+ s_in = x.new_ones([x.shape[0]]).half()
+ for i in trange(len(sigmas) - 1, disable=disable):
+ gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
+@@ -807,13 +823,18 @@ class UNet(DDPM):
+ d = to_d(x, sigma_hat, denoised)
+ if callback is not None:
+ callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
++
++ if img_callback: yield from img_callback(x, i)
++
+ dt = sigmas[i + 1] - sigma_hat
+ # Euler method
+ x = x + d * dt
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+
+ @torch.no_grad()
+- def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None):
++ def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None,
++ img_callback=None):
+ """Ancestral sampling with Euler method steps."""
+ extra_args = {} if extra_args is None else extra_args
+
+@@ -822,6 +843,8 @@ class UNet(DDPM):
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running Euler Ancestral Sampling with {len(sigmas) - 1} timesteps")
++
+ s_in = x.new_ones([x.shape[0]]).half()
+ for i in trange(len(sigmas) - 1, disable=disable):
+
+@@ -837,17 +860,22 @@ class UNet(DDPM):
+ sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1])
+ if callback is not None:
+ callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
++
++ if img_callback: yield from img_callback(x, i)
++
+ d = to_d(x, sigmas[i], denoised)
+ # Euler method
+ dt = sigma_down - sigmas[i]
+ x = x + d * dt
+ x = x + torch.randn_like(x) * sigma_up
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+
+
+
+ @torch.no_grad()
+- def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
++ def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
++ img_callback=None):
+ """Implements Algorithm 2 (Heun steps) from Karras et al. (2022)."""
+ extra_args = {} if extra_args is None else extra_args
+
+@@ -855,6 +883,8 @@ class UNet(DDPM):
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running Heun Sampling with {len(sigmas) - 1} timesteps")
++
+
+ s_in = x.new_ones([x.shape[0]]).half()
+ for i in trange(len(sigmas) - 1, disable=disable):
+@@ -876,6 +906,9 @@ class UNet(DDPM):
+ d = to_d(x, sigma_hat, denoised)
+ if callback is not None:
+ callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
++
++ if img_callback: yield from img_callback(x, i)
++
+ dt = sigmas[i + 1] - sigma_hat
+ if sigmas[i + 1] == 0:
+ # Euler method
+@@ -895,11 +928,13 @@ class UNet(DDPM):
+ d_2 = to_d(x_2, sigmas[i + 1], denoised_2)
+ d_prime = (d + d_2) / 2
+ x = x + d_prime * dt
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+
+
+ @torch.no_grad()
+- def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
++ def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
++ img_callback=None):
+ """A sampler inspired by DPM-Solver-2 and Algorithm 2 from Karras et al. (2022)."""
+ extra_args = {} if extra_args is None else extra_args
+
+@@ -907,6 +942,8 @@ class UNet(DDPM):
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running DPM2 Sampling with {len(sigmas) - 1} timesteps")
++
+ s_in = x.new_ones([x.shape[0]]).half()
+ for i in trange(len(sigmas) - 1, disable=disable):
+ gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
+@@ -924,7 +961,7 @@ class UNet(DDPM):
+ e_t_uncond, e_t = (x_in + eps * c_out).chunk(2)
+ denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)
+
+-
++ if img_callback: yield from img_callback(x, i)
+
+ d = to_d(x, sigma_hat, denoised)
+ # Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule
+@@ -945,11 +982,13 @@ class UNet(DDPM):
+
+ d_2 = to_d(x_2, sigma_mid, denoised_2)
+ x = x + d_2 * dt_2
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+
+
+ @torch.no_grad()
+- def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None):
++ def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None,
++ img_callback=None):
+ """Ancestral sampling with DPM-Solver inspired second-order steps."""
+ extra_args = {} if extra_args is None else extra_args
+
+@@ -957,6 +996,8 @@ class UNet(DDPM):
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running DPM2 Ancestral Sampling with {len(sigmas) - 1} timesteps")
++
+ s_in = x.new_ones([x.shape[0]]).half()
+ for i in trange(len(sigmas) - 1, disable=disable):
+
+@@ -973,6 +1014,9 @@ class UNet(DDPM):
+ sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1])
+ if callback is not None:
+ callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
++
++ if img_callback: yield from img_callback(x, i)
++
+ d = to_d(x, sigmas[i], denoised)
+ # Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule
+ sigma_mid = ((sigmas[i] ** (1 / 3) + sigma_down ** (1 / 3)) / 2) ** 3
+@@ -993,11 +1037,13 @@ class UNet(DDPM):
+ d_2 = to_d(x_2, sigma_mid, denoised_2)
+ x = x + d_2 * dt_2
+ x = x + torch.randn_like(x) * sigma_up
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+
+
+ @torch.no_grad()
+- def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4):
++ def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4,
++ img_callback=None):
+ extra_args = {} if extra_args is None else extra_args
+ s_in = x.new_ones([x.shape[0]])
+
+@@ -1005,6 +1051,8 @@ class UNet(DDPM):
+ sigmas = cvd.get_sigmas(S)
+ x = x*sigmas[0]
+
++ print(f"Running LMS Sampling with {len(sigmas) - 1} timesteps")
++
+ ds = []
+ for i in trange(len(sigmas) - 1, disable=disable):
+
+@@ -1017,6 +1065,7 @@ class UNet(DDPM):
+ e_t_uncond, e_t = (x_in + eps * c_out).chunk(2)
+ denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)
+
++ if img_callback: yield from img_callback(x, i)
+
+ d = to_d(x, sigmas[i], denoised)
+ ds.append(d)
+@@ -1027,4 +1076,5 @@ class UNet(DDPM):
+ cur_order = min(i + 1, order)
+ coeffs = [linear_multistep_coeff(cur_order, sigmas.cpu(), i, j) for j in range(cur_order)]
+ x = x + sum(coeff * d for coeff, d in zip(coeffs, reversed(ds)))
+- return x
++
++ yield from img_callback(x, len(sigmas)-1)
+diff --git a/optimizedSD/openaimodelSplit.py b/optimizedSD/openaimodelSplit.py
+index abc3098..7a32ffe 100644
+--- a/optimizedSD/openaimodelSplit.py
++++ b/optimizedSD/openaimodelSplit.py
+@@ -13,7 +13,7 @@ from ldm.modules.diffusionmodules.util import (
+ normalization,
+ timestep_embedding,
+ )
+-from splitAttention import SpatialTransformer
++from .splitAttention import SpatialTransformer
+
+
+ class AttentionPool2d(nn.Module):
diff --git a/ui/sd_internal/runtime.py b/ui/sd_internal/runtime.py
index 0fe21ee6..f03a0d34 100644
--- a/ui/sd_internal/runtime.py
+++ b/ui/sd_internal/runtime.py
@@ -1,9 +1,10 @@
+import json
import os, re
import traceback
import torch
import numpy as np
from omegaconf import OmegaConf
-from PIL import Image
+from PIL import Image, ImageOps
from tqdm import tqdm, trange
from itertools import islice
from einops import rearrange
@@ -32,10 +33,11 @@ filename_regex = re.compile('[^a-zA-Z0-9]')
from . import Request, Response, Image as ResponseImage
import base64
from io import BytesIO
+#from colorama import Fore
# local
-session_id = str(uuid.uuid4())[-8:]
stop_processing = False
+temp_images = {}
ckpt_file = None
gfpgan_file = None
@@ -184,23 +186,47 @@ def load_model_real_esrgan(real_esrgan_to_use):
print('loaded ', real_esrgan_to_use, 'to', device, 'precision', precision)
def mk_img(req: Request):
- global modelFS, device
+ try:
+ yield from do_mk_img(req)
+ except Exception as e:
+ print(traceback.format_exc())
+
+ gc()
+
+ if device != "cpu":
+ modelFS.to("cpu")
+ modelCS.to("cpu")
+
+ model.model1.to("cpu")
+ model.model2.to("cpu")
+
+ gc()
+
+ yield json.dumps({
+ "status": 'failed',
+ "detail": str(e)
+ })
+
+def do_mk_img(req: Request):
+ global model, modelCS, modelFS, device
global model_gfpgan, model_real_esrgan
global stop_processing
stop_processing = False
res = Response()
- res.session_id = session_id
res.request = req
res.images = []
+ temp_images.clear()
+
model.turbo = req.turbo
if req.use_cpu:
if device != 'cpu':
device = 'cpu'
if model_is_half:
+ del model, modelCS, modelFS
load_model_ckpt(ckpt_file, device)
load_model_gfpgan(gfpgan_file)
@@ -215,7 +241,8 @@ def mk_img(req: Request):
(req.init_image is None and model_fs_is_half) or \
(req.init_image is not None and not model_fs_is_half and not force_full_precision):
- load_model_ckpt(ckpt_file, device, model.turbo, unet_bs, ('full' if req.use_full_precision else 'autocast'), half_model_fs=(req.init_image is not None and not req.use_full_precision))
+ del model, modelCS, modelFS
+ load_model_ckpt(ckpt_file, device, req.turbo, unet_bs, ('full' if req.use_full_precision else 'autocast'), half_model_fs=(req.init_image is not None and not req.use_full_precision))
if prev_device != device:
load_model_gfpgan(gfpgan_file)
@@ -248,6 +275,7 @@ def mk_img(req: Request):
opt_use_upscale = req.use_upscale
opt_show_only_filtered = req.show_only_filtered_image
opt_format = 'png'
+ opt_sampler_name = req.sampler
print(req.to_string(), '\n device', device)
@@ -265,6 +293,8 @@ def mk_img(req: Request):
else:
precision_scope = nullcontext
+ mask = None
+
if req.init_image is None:
handler = _txt2img
@@ -284,18 +314,22 @@ def mk_img(req: Request):
init_image = repeat(init_image, '1 ... -> b ...', b=batch_size)
init_latent = modelFS.get_first_stage_encoding(modelFS.encode_first_stage(init_image)) # move to latent space
- if device != "cpu":
- mem = torch.cuda.memory_allocated() / 1e6
- modelFS.to("cpu")
- while torch.cuda.memory_allocated() / 1e6 >= mem:
- time.sleep(1)
+ if req.mask is not None:
+ mask = load_mask(req.mask, opt_W, opt_H, init_latent.shape[2], init_latent.shape[3], True).to(device)
+ mask = mask[0][0].unsqueeze(0).repeat(4, 1, 1).unsqueeze(0)
+ mask = repeat(mask, '1 ... -> b ...', b=batch_size)
+
+ if device != "cpu" and precision == "autocast":
+ mask = mask.half()
+
+ move_fs_to_cpu()
assert 0. <= opt_strength <= 1., 'can only work with strength in [0.0, 1.0]'
t_enc = int(opt_strength * opt_ddim_steps)
print(f"target t_enc is {t_enc} steps")
if opt_save_to_disk_path is not None:
- session_out_path = os.path.join(opt_save_to_disk_path, session_id)
+ session_out_path = os.path.join(opt_save_to_disk_path, req.session_id)
os.makedirs(session_out_path, exist_ok=True)
else:
session_out_path = None
@@ -326,29 +360,60 @@ def mk_img(req: Request):
else:
c = modelCS.get_learned_conditioning(prompts)
+ modelFS.to(device)
+
partial_x_samples = None
def img_callback(x_samples, i):
nonlocal partial_x_samples
partial_x_samples = x_samples
+ if req.stream_progress_updates:
+ n_steps = opt_ddim_steps if req.init_image is None else t_enc
+ progress = {"step": i, "total_steps": n_steps}
+
+ if req.stream_image_progress and i % 5 == 0:
+ partial_images = []
+
+ for i in range(batch_size):
+ x_samples_ddim = modelFS.decode_first_stage(x_samples[i].unsqueeze(0))
+ x_sample = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0)
+ x_sample = 255.0 * rearrange(x_sample[0].cpu().numpy(), "c h w -> h w c")
+ x_sample = x_sample.astype(np.uint8)
+ img = Image.fromarray(x_sample)
+ buf = BytesIO()
+ img.save(buf, format='JPEG')
+ buf.seek(0)
+
+ del img, x_sample, x_samples_ddim
+ # don't delete x_samples, it is used in the code that called this callback
+
+ temp_images[str(req.session_id) + '/' + str(i)] = buf
+ partial_images.append({'path': f'/image/tmp/{req.session_id}/{i}'})
+
+ progress['output'] = partial_images
+
+ yield json.dumps(progress)
+
if stop_processing:
raise UserInitiatedStop("User requested that we stop processing")
# run the handler
try:
if handler == _txt2img:
- x_samples = _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, None, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback)
+ x_samples = _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, None, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, opt_sampler_name)
else:
- x_samples = _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback)
+ x_samples = _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback, mask)
+
+ yield from x_samples
+
+ x_samples = partial_x_samples
except UserInitiatedStop:
if partial_x_samples is None:
continue
x_samples = partial_x_samples
- modelFS.to(device)
-
print("saving images")
for i in range(batch_size):
@@ -358,6 +423,14 @@ def mk_img(req: Request):
x_sample = x_sample.astype(np.uint8)
img = Image.fromarray(x_sample)
+ has_filters = (opt_use_face_correction is not None and opt_use_face_correction.startswith('GFPGAN')) or \
+ (opt_use_upscale is not None and opt_use_upscale.startswith('RealESRGAN'))
+
+ return_orig_img = not has_filters or not opt_show_only_filtered
+
+ if stop_processing:
+ return_orig_img = True
+
if opt_save_to_disk_path is not None:
prompt_flattened = filename_regex.sub('_', prompts[0])
prompt_flattened = prompt_flattened[:50]
@@ -368,12 +441,12 @@ def mk_img(req: Request):
img_out_path = os.path.join(session_out_path, f"{file_path}.{opt_format}")
meta_out_path = os.path.join(session_out_path, f"{file_path}.txt")
- if not opt_show_only_filtered:
+ if return_orig_img:
save_image(img, img_out_path)
- save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_strength, opt_use_face_correction, opt_use_upscale)
+ save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_strength, opt_use_face_correction, opt_use_upscale, opt_sampler_name)
- if not opt_show_only_filtered:
+ if return_orig_img:
img_data = img_to_base64_str(img)
res_image_orig = ResponseImage(data=img_data, seed=opt_seed)
res.images.append(res_image_orig)
@@ -381,8 +454,10 @@ def mk_img(req: Request):
if opt_save_to_disk_path is not None:
res_image_orig.path_abs = img_out_path
- if (opt_use_face_correction is not None and opt_use_face_correction.startswith('GFPGAN')) or \
- (opt_use_upscale is not None and opt_use_upscale.startswith('RealESRGAN')):
+ del img
+
+ if has_filters and not stop_processing:
+ print('Applying filters..')
gc()
filters_applied = []
@@ -410,18 +485,19 @@ def mk_img(req: Request):
save_image(filtered_image, filtered_img_out_path)
res_image_filtered.path_abs = filtered_img_out_path
+ del filtered_image
+
seeds += str(opt_seed) + ","
opt_seed += 1
- if device != "cpu":
- mem = torch.cuda.memory_allocated() / 1e6
- modelFS.to("cpu")
- while torch.cuda.memory_allocated() / 1e6 >= mem:
- time.sleep(1)
- del x_samples
+ move_fs_to_cpu()
+ gc()
+ del x_samples, x_samples_ddim, x_sample
print("memory_final = ", torch.cuda.memory_allocated() / 1e6)
- return res
+ print('Task completed')
+
+ yield json.dumps(res.json())
def save_image(img, img_out_path):
try:
@@ -429,8 +505,8 @@ def save_image(img, img_out_path):
except:
print('could not save the file', traceback.format_exc())
-def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_prompt_strength, opt_correct_face, opt_upscale):
- metadata = f"{prompts[0]}\nWidth: {opt_W}\nHeight: {opt_H}\nSeed: {opt_seed}\nSteps: {opt_ddim_steps}\nGuidance Scale: {opt_scale}\nPrompt Strength: {opt_prompt_strength}\nUse Face Correction: {opt_correct_face}\nUse Upscaling: {opt_upscale}"
+def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps, opt_scale, opt_prompt_strength, opt_correct_face, opt_upscale, sampler_name):
+ metadata = f"{prompts[0]}\nWidth: {opt_W}\nHeight: {opt_H}\nSeed: {opt_seed}\nSteps: {opt_ddim_steps}\nGuidance Scale: {opt_scale}\nPrompt Strength: {opt_prompt_strength}\nUse Face Correction: {opt_correct_face}\nUse Upscaling: {opt_upscale}\nSampler: {sampler_name}"
try:
with open(meta_out_path, 'w') as f:
@@ -438,7 +514,7 @@ def save_metadata(meta_out_path, prompts, opt_seed, opt_W, opt_H, opt_ddim_steps
except:
print('could not save the file', traceback.format_exc())
-def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback):
+def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, sampler_name):
shape = [opt_n_samples, opt_C, opt_H // opt_f, opt_W // opt_f]
if device != "cpu":
@@ -458,12 +534,13 @@ def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code,
eta=opt_ddim_eta,
x_T=start_code,
img_callback=img_callback,
- sampler = 'plms',
+ mask=mask,
+ sampler = sampler_name,
)
- return samples_ddim
+ yield from samples_ddim
-def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback):
+def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback, mask):
# encode (scaled latent)
z_enc = model.stochastic_encode(
init_latent,
@@ -472,6 +549,8 @@ def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, o
opt_ddim_eta,
opt_ddim_steps,
)
+ x_T = None if mask is None else init_latent
+
# decode it
samples_ddim = model.sample(
t_enc,
@@ -480,10 +559,19 @@ def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, o
unconditional_guidance_scale=opt_scale,
unconditional_conditioning=uc,
img_callback=img_callback,
+ mask=mask,
+ x_T=x_T,
sampler = 'ddim'
)
- return samples_ddim
+ yield from samples_ddim
+
+def move_fs_to_cpu():
+ if device != "cpu":
+ mem = torch.cuda.memory_allocated() / 1e6
+ modelFS.to("cpu")
+ while torch.cuda.memory_allocated() / 1e6 >= mem:
+ time.sleep(1)
def gc():
if device == 'cpu':
@@ -525,6 +613,31 @@ def load_img(img_str, w0, h0):
image = torch.from_numpy(image)
return 2.*image - 1.
+def load_mask(mask_str, h0, w0, newH, newW, invert=False):
+ image = base64_str_to_img(mask_str).convert("RGB")
+ w, h = image.size
+ print(f"loaded input mask of size ({w}, {h})")
+
+ if invert:
+ print("inverted")
+ image = ImageOps.invert(image)
+ # where_0, where_1 = np.where(image == 0), np.where(image == 255)
+ # image[where_0], image[where_1] = 255, 0
+
+ if h0 is not None and w0 is not None:
+ h, w = h0, w0
+
+ w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64
+
+ print(f"New mask size ({w}, {h})")
+ image = image.resize((newW, newH), resample=Image.Resampling.LANCZOS)
+ image = np.array(image)
+
+ image = image.astype(np.float32) / 255.0
+ image = image[None].transpose(0, 3, 1, 2)
+ image = torch.from_numpy(image)
+ return image
+
# https://stackoverflow.com/a/61114178
def img_to_base64_str(img):
buffered = BytesIO()
diff --git a/ui/server.py b/ui/server.py
index f55c1091..ad3b1ae0 100644
--- a/ui/server.py
+++ b/ui/server.py
@@ -15,7 +15,8 @@ CONFIG_DIR = os.path.join(SD_UI_DIR, '..', 'scripts')
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
from fastapi import FastAPI, HTTPException
-from starlette.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+from starlette.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
import logging
@@ -31,6 +32,7 @@ outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME)
# defaults from https://huggingface.co/blog/stable_diffusion
class ImageRequest(BaseModel):
+ session_id: str = "session"
prompt: str = ""
init_image: str = None # base64
mask: str = None # base64
@@ -41,6 +43,7 @@ class ImageRequest(BaseModel):
height: int = 512
seed: int = 42
prompt_strength: float = 0.8
+ sampler: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
# allow_nsfw: bool = False
save_to_disk_path: str = None
turbo: bool = True
@@ -50,9 +53,14 @@ class ImageRequest(BaseModel):
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
show_only_filtered_image: bool = False
+ stream_progress_updates: bool = False
+ stream_image_progress: bool = False
+
class SetAppConfigRequest(BaseModel):
update_branch: str = "main"
+app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media/')), name="media")
+
@app.get('/')
def read_root():
headers = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
@@ -87,6 +95,7 @@ def image(req : ImageRequest):
from sd_internal import runtime
r = Request()
+ r.session_id = req.session_id
r.prompt = req.prompt
r.init_image = req.init_image
r.mask = req.mask
@@ -97,6 +106,7 @@ def image(req : ImageRequest):
r.height = req.height
r.seed = req.seed
r.prompt_strength = req.prompt_strength
+ r.sampler = req.sampler
# r.allow_nsfw = req.allow_nsfw
r.turbo = req.turbo
r.use_cpu = req.use_cpu
@@ -106,10 +116,24 @@ def image(req : ImageRequest):
r.use_face_correction = req.use_face_correction
r.show_only_filtered_image = req.show_only_filtered_image
- try:
- res: Response = runtime.mk_img(r)
+ r.stream_progress_updates = True # the underlying implementation only supports streaming
+ r.stream_image_progress = req.stream_image_progress
- return res.json()
+ try:
+ if not req.stream_progress_updates:
+ r.stream_image_progress = False
+
+ res = runtime.mk_img(r)
+
+ if req.stream_progress_updates:
+ return StreamingResponse(res, media_type='application/json')
+ else: # compatibility mode: buffer the streaming responses, and return the last one
+ last_result = None
+
+ for result in res:
+ last_result = result
+
+ return json.loads(last_result)
except Exception as e:
print(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
@@ -128,6 +152,13 @@ def stop():
print(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
+@app.get('/image/tmp/{session_id}/{img_id}')
+def get_image(session_id, img_id):
+ from sd_internal import runtime
+ buf = runtime.temp_images[session_id + '/' + img_id]
+ buf.seek(0)
+ return StreamingResponse(buf, media_type='image/jpeg')
+
@app.post('/app_config')
async def setAppConfig(req : SetAppConfigRequest):
try:
@@ -173,14 +204,6 @@ def getAppConfig():
print(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
-@app.get('/media/ding.mp3')
-def read_ding():
- return FileResponse(os.path.join(SD_UI_DIR, 'media/ding.mp3'))
-
-@app.get('/media/kofi.png')
-def read_modifiers():
- return FileResponse(os.path.join(SD_UI_DIR, 'media/kofi.png'))
-
@app.get('/modifiers.json')
def read_modifiers():
return FileResponse(os.path.join(SD_UI_DIR, 'modifiers.json'))