forked from extern/easydiffusion
commit
73ace121a4
@ -2,10 +2,12 @@
|
||||
|
||||
## v2.4
|
||||
### Major Changes
|
||||
- **Allow reordering the task queue** (by dragging and dropping tasks). Thanks @madrang
|
||||
- **Automatic scanning for malicious model files** - using `picklescan`, and support for `safetensor` model format. Thanks @JeLuf
|
||||
- **Image Editor** - for drawing simple images for guiding the AI. Thanks @mdiller
|
||||
- **Use pre-trained hypernetworks** - for improving the quality of images. Thanks @C0bra5
|
||||
- **Support for custom VAE models**. You can place your VAE files in the `models/vae` folder, and refresh the browser page to use them. More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder
|
||||
- **Experimental support for multiple GPUs!** It should work automatically. Just open one browser tab per GPU, and spread your tasks across your GPUs. For e.g. open our UI in two browser tabs if you have two GPUs. You can customize which GPUs it should use in the "Settings" tab, otherwise let it automatically pick the best GPUs. Thanks @madrang . More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/Run-on-Multiple-GPUs
|
||||
- **Image Editor** - for drawing simple images for guiding the AI. Thanks @mdiller
|
||||
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
|
||||
- **Progress bar.** Thanks @mdiller
|
||||
- **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
|
||||
@ -25,6 +27,11 @@
|
||||
- Support loading models in the safetensor format, for improved safety
|
||||
|
||||
### Detailed changelog
|
||||
* 2.4.19 - 10 Dec 2022 - Show init img in task list
|
||||
* 2.4.19 - 7 Dec 2022 - Use pre-trained hypernetworks while generating images. Thanks @C0bra5
|
||||
* 2.4.19 - 6 Dec 2022 - Allow processing new tasks first. Thanks @madrang
|
||||
* 2.4.19 - 6 Dec 2022 - Allow reordering the task queue (by dragging tasks). Thanks @madrang
|
||||
* 2.4.19 - 6 Dec 2022 - Re-organize the code, to make it easier to write user plugins. Thanks @madrang
|
||||
* 2.4.18 - 5 Dec 2022 - Make JPEG Output quality user controllable. Thanks @JeLuf
|
||||
* 2.4.18 - 5 Dec 2022 - Support loading models in the safetensor format, for improved safety. Thanks @JeLuf
|
||||
* 2.4.18 - 1 Dec 2022 - Image Editor, for drawing simple images for guiding the AI. Thanks @mdiller
|
||||
|
@ -44,7 +44,7 @@ if NOT DEFINED test_sd2 set test_sd2=N
|
||||
@call git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a
|
||||
)
|
||||
if "%test_sd2%" == "Y" (
|
||||
@call git -c advice.detachedHead=false checkout b1a80dfc75388914252ce363f923103185eaf48f
|
||||
@call git -c advice.detachedHead=false checkout 733a1f6f9cae9b9a9b83294bf3281b123378cb1f
|
||||
)
|
||||
|
||||
@cd ..
|
||||
@ -201,8 +201,10 @@ call WHERE uvicorn > .tmp
|
||||
|
||||
if not exist "..\models\stable-diffusion" mkdir "..\models\stable-diffusion"
|
||||
if not exist "..\models\vae" mkdir "..\models\vae"
|
||||
if not exist "..\models\hypernetwork" mkdir "..\models\hypernetwork"
|
||||
echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
|
||||
echo. > "..\models\vae\Put your VAE files here.txt"
|
||||
echo. > "..\models\hypernetwork\Put your hypernetwork files here.txt"
|
||||
|
||||
@if exist "sd-v1-4.ckpt" (
|
||||
for %%I in ("sd-v1-4.ckpt") do if "%%~zI" EQU "4265380512" (
|
||||
|
@ -38,7 +38,7 @@ if [ -e "scripts/install_status.txt" ] && [ `grep -c sd_git_cloned scripts/insta
|
||||
if [ "$test_sd2" == "N" ]; then
|
||||
git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a
|
||||
elif [ "$test_sd2" == "Y" ]; then
|
||||
git -c advice.detachedHead=false checkout b1a80dfc75388914252ce363f923103185eaf48f
|
||||
git -c advice.detachedHead=false checkout 733a1f6f9cae9b9a9b83294bf3281b123378cb1f
|
||||
fi
|
||||
|
||||
cd ..
|
||||
@ -161,8 +161,10 @@ fi
|
||||
|
||||
mkdir -p "../models/stable-diffusion"
|
||||
mkdir -p "../models/vae"
|
||||
mkdir -p "../models/hypernetwork"
|
||||
echo "" > "../models/stable-diffusion/Put your custom ckpt files here.txt"
|
||||
echo "" > "../models/vae/Put your VAE files here.txt"
|
||||
echo "" > "../models/hypernetwork/Put your hypernetwork files here.txt"
|
||||
|
||||
if [ -f "sd-v1-4.ckpt" ]; then
|
||||
model_size=`find "sd-v1-4.ckpt" -printf "%s"`
|
||||
|
@ -25,7 +25,7 @@
|
||||
<div id="logo">
|
||||
<h1>
|
||||
Stable Diffusion UI
|
||||
<small>v2.4.18 <span id="updateBranchLabel"></span></small>
|
||||
<small>v2.4.19 <span id="updateBranchLabel"></span></small>
|
||||
</h1>
|
||||
</div>
|
||||
<div id="server-status">
|
||||
@ -55,7 +55,7 @@
|
||||
<input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden -->
|
||||
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle">
|
||||
Negative Prompt
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about Negative Prompts</span></i></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about Negative Prompts</span></i></a>
|
||||
<small>(optional)</small>
|
||||
</label>
|
||||
<div class="collapsible-content">
|
||||
@ -95,7 +95,7 @@
|
||||
</div>
|
||||
|
||||
<div id="editor-inputs-tags-container" class="row">
|
||||
<label>Image Modifiers <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">click an Image Modifier to remove it, use Ctrl+Mouse Wheel to adjust its weight</span></i>:</label>
|
||||
<label>Image Modifiers <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">click an Image Modifier to remove it, use Ctrl+Mouse Wheel to adjust its weight</span></i>:</label>
|
||||
<div id="editor-inputs-tags-list"></div>
|
||||
</div>
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
<h4 class="collapsible">
|
||||
Image Settings
|
||||
<i id="reset-image-settings" class="fa-solid fa-arrow-rotate-left section-button">
|
||||
<span class="simple-tooltip right">
|
||||
<span class="simple-tooltip top-left">
|
||||
Reset Image Settings
|
||||
</span>
|
||||
</i>
|
||||
@ -123,13 +123,13 @@
|
||||
<select id="stable_diffusion_model" name="stable_diffusion_model">
|
||||
<!-- <option value="sd-v1-4" selected>sd-v1-4</option> -->
|
||||
</select>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about custom models</span></i></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about custom models</span></i></a>
|
||||
</td></tr>
|
||||
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td>
|
||||
<select id="vae_model" name="vae_model">
|
||||
<!-- <option value="" selected>None</option> -->
|
||||
</select>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about VAEs</span></i></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about VAEs</span></i></a>
|
||||
</td></tr>
|
||||
<tr id="samplerSelection" class="pl-5"><td><label for="sampler">Sampler:</label></td><td>
|
||||
<select id="sampler" name="sampler">
|
||||
@ -142,7 +142,7 @@
|
||||
<option value="dpm2_a">dpm2_a</option>
|
||||
<option value="lms">lms</option>
|
||||
</select>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about samplers</span></i></a>
|
||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about samplers</span></i></a>
|
||||
</td></tr>
|
||||
<tr class="pl-5"><td><label>Image Size: </label></td><td>
|
||||
<select id="width" name="width" value="512">
|
||||
@ -192,7 +192,16 @@
|
||||
</td></tr>
|
||||
<tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" size="4" value="25" onkeypress="preventNonNumericalInput(event)"></td></tr>
|
||||
<tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="10" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
|
||||
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr></span>
|
||||
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr>
|
||||
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td>
|
||||
<select id="hypernetwork_model" name="hypernetwork_model">
|
||||
<!-- <option value="" selected>None</option> -->
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr id="hypernetwork_strength_container" class="pl-5">
|
||||
<td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td>
|
||||
<td> <input id="hypernetwork_strength_slider" name="hypernetwork_strength_slider" class="editor-slider" value="100" type="range" min="0" max="100"> <input id="hypernetwork_strength" name="hypernetwork_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td>
|
||||
</tr>
|
||||
<tr class="pl-5"><td><label for="output_format">Output Format:</label></td><td>
|
||||
<select id="output_format" name="output_format">
|
||||
<option value="jpeg" selected>jpeg</option>
|
||||
@ -200,8 +209,8 @@
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr class="pl-5" id="output_quality_row"><td><label for="output_quality">JPEG Quality:</label></td><td>
|
||||
<input id="output_quality_slider" name="output_quality" class="editor-slider" value="75" type="range" min="10" max="95"> <input id="output_quality" name="output_quality" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)">
|
||||
</td></tr>
|
||||
<input id="output_quality_slider" name="output_quality" class="editor-slider" value="75" type="range" min="10" max="95"> <input id="output_quality" name="output_quality" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)">
|
||||
</td></tr>
|
||||
</table></div>
|
||||
|
||||
<div><ul>
|
||||
@ -272,7 +281,7 @@
|
||||
<tr><td><label>Compatible Graphics Cards (all):</label></td><td id="system-info-gpus-all" class="value"></td></tr>
|
||||
<tr><td></td><td> </td></tr>
|
||||
<tr><td><label>Used for rendering 🔥:</label></td><td id="system-info-rendering-devices" class="value"></td></tr>
|
||||
<tr><td><label>Server Addresses <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">You can access Stable Diffusion UI from other devices using these addresses</span></i> :</label></td><td id="system-info-server-hosts" class="value"></td></tr>
|
||||
<tr><td><label>Server Addresses <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">You can access Stable Diffusion UI from other devices using these addresses</span></i> :</label></td><td id="system-info-server-hosts" class="value"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -388,10 +397,13 @@
|
||||
</div>
|
||||
</body>
|
||||
<script src="media/js/utils.js"></script>
|
||||
<script src="media/js/engine.js"></script>
|
||||
<script src="media/js/parameters.js"></script>
|
||||
<script src="media/js/plugins.js"></script>
|
||||
|
||||
<script src="media/js/image-modifiers.js"></script>
|
||||
<script src="media/js/auto-save.js"></script>
|
||||
|
||||
<script src="media/js/main.js"></script>
|
||||
<script src="media/js/themes.js"></script>
|
||||
<script src="media/js/dnd.js"></script>
|
||||
@ -406,8 +418,12 @@ async function init() {
|
||||
await loadModifiers()
|
||||
await getSystemInfo()
|
||||
|
||||
setInterval(healthCheck, HEALTH_PING_INTERVAL * 1000)
|
||||
healthCheck()
|
||||
SD.init({
|
||||
events: {
|
||||
statusChange: setServerStatus
|
||||
, idle: onIdle
|
||||
}
|
||||
})
|
||||
|
||||
playSound()
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ code {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 370pt;
|
||||
flex: 0 0 380pt;
|
||||
}
|
||||
#editor label {
|
||||
font-weight: normal;
|
||||
@ -893,6 +893,15 @@ input::file-selector-button {
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
|
||||
.simple-tooltip.top-left {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
transform: translate(calc(-100% + 15%), calc(-100% + 15%));
|
||||
}
|
||||
:hover > .simple-tooltip.top-left {
|
||||
transform: translate(-80%, -100%);
|
||||
}
|
||||
|
||||
/* PROGRESS BAR */
|
||||
.progress-bar {
|
||||
background: var(--background-color3);
|
||||
@ -901,6 +910,7 @@ input::file-selector-button {
|
||||
height: 16px;
|
||||
position: relative;
|
||||
transition: 0.25s 1s border, 0.25s 1s height;
|
||||
clear: both;
|
||||
}
|
||||
.progress-bar > div {
|
||||
background: var(--accent-color);
|
||||
@ -1052,6 +1062,15 @@ button:active {
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
div.task-initimg > img {
|
||||
margin-right: 6px;
|
||||
display: block;
|
||||
}
|
||||
div.task-fs-initimage {
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
button#save-system-settings-btn {
|
||||
padding: 4pt 8pt;
|
||||
}
|
||||
|
@ -14,12 +14,14 @@ const SETTINGS_IDS_LIST = [
|
||||
"num_outputs_parallel",
|
||||
"stable_diffusion_model",
|
||||
"vae_model",
|
||||
"hypernetwork_model",
|
||||
"sampler",
|
||||
"width",
|
||||
"height",
|
||||
"num_inference_steps",
|
||||
"guidance_scale",
|
||||
"prompt_strength",
|
||||
"hypernetwork_strength",
|
||||
"output_format",
|
||||
"output_quality",
|
||||
"negative_prompt",
|
||||
@ -129,7 +131,7 @@ function loadSettings() {
|
||||
var saved_settings_text = localStorage.getItem(SETTINGS_KEY)
|
||||
if (saved_settings_text) {
|
||||
var saved_settings = JSON.parse(saved_settings_text)
|
||||
if (saved_settings.find(s => s.key == "auto_save_settings").value == false) {
|
||||
if (saved_settings.find(s => s.key == "auto_save_settings")?.value == false) {
|
||||
setSetting("auto_save_settings", false)
|
||||
return
|
||||
}
|
||||
|
@ -194,6 +194,28 @@ const TASK_MAPPING = {
|
||||
readUI: () => vaeModelField.value,
|
||||
parse: (val) => val
|
||||
},
|
||||
use_hypernetwork_model: { name: 'Hypernetwork model',
|
||||
setUI: (use_hypernetwork_model) => {
|
||||
const oldVal = hypernetworkModelField.value
|
||||
|
||||
if (use_hypernetwork_model !== '') {
|
||||
use_hypernetwork_model = getModelPath(use_hypernetwork_model, ['.pt'])
|
||||
use_hypernetwork_model = use_hypernetwork_model !== '' ? use_hypernetwork_model : oldVal
|
||||
}
|
||||
hypernetworkModelField.value = use_hypernetwork_model
|
||||
hypernetworkModelField.dispatchEvent(new Event('change'))
|
||||
},
|
||||
readUI: () => hypernetworkModelField.value,
|
||||
parse: (val) => val
|
||||
},
|
||||
hypernetwork_strength: { name: 'Hypernetwork Strength',
|
||||
setUI: (hypernetwork_strength) => {
|
||||
hypernetworkStrengthField.value = hypernetwork_strength
|
||||
updateHypernetworkStrengthSlider()
|
||||
},
|
||||
readUI: () => parseFloat(hypernetworkStrengthField.value),
|
||||
parse: (val) => parseFloat(val)
|
||||
},
|
||||
|
||||
num_outputs: { name: 'Parallel Images',
|
||||
setUI: (num_outputs) => {
|
||||
@ -338,7 +360,9 @@ const TASK_TEXT_MAPPING = {
|
||||
use_upscale: 'Use Upscaling',
|
||||
sampler: 'Sampler',
|
||||
negative_prompt: 'Negative Prompt',
|
||||
use_stable_diffusion_model: 'Stable Diffusion model'
|
||||
use_stable_diffusion_model: 'Stable Diffusion model',
|
||||
use_hypernetwork_model: 'Hypernetwork model',
|
||||
hypernetwork_strength: 'Hypernetwork Strength'
|
||||
}
|
||||
const afterPromptRe = /^\s*Width\s*:\s*\d+\s*(?:\r\n|\r|\n)+\s*Height\s*:\s*\d+\s*(\r\n|\r|\n)+Seed\s*:\s*\d+\s*$/igm
|
||||
function parseTaskFromText(str) {
|
||||
@ -465,7 +489,7 @@ function checkReadTextClipboardPermission (result) {
|
||||
// PASTE ICON
|
||||
const pasteIcon = document.createElement('i')
|
||||
pasteIcon.className = 'fa-solid fa-paste section-button'
|
||||
pasteIcon.innerHTML = `<span class="simple-tooltip right">Paste Image Settings</span>`
|
||||
pasteIcon.innerHTML = `<span class="simple-tooltip top-left">Paste Image Settings</span>`
|
||||
pasteIcon.addEventListener('click', async (event) => {
|
||||
event.stopPropagation()
|
||||
// Add css class 'active'
|
||||
@ -505,7 +529,7 @@ function checkWriteToClipboardPermission (result) {
|
||||
// COPY ICON
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.className = 'fa-solid fa-clipboard section-button'
|
||||
copyIcon.innerHTML = `<span class="simple-tooltip right">Copy Image Settings</span>`
|
||||
copyIcon.innerHTML = `<span class="simple-tooltip top-left">Copy Image Settings</span>`
|
||||
copyIcon.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
// Add css class 'active'
|
||||
|
1308
ui/media/js/engine.js
Normal file
1308
ui/media/js/engine.js
Normal file
File diff suppressed because it is too large
Load Diff
1046
ui/media/js/main.js
1046
ui/media/js/main.js
File diff suppressed because it is too large
Load Diff
@ -61,6 +61,13 @@ var PARAMETERS = [
|
||||
icon: "fa-volume-low",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: "process_order_toggle",
|
||||
type: ParameterType.checkbox,
|
||||
label: "Process newest jobs first",
|
||||
note: "reverse the normal processing order",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: "ui_open_browser_on_start",
|
||||
type: ParameterType.checkbox,
|
||||
@ -368,73 +375,69 @@ function setHostInfo(hosts) {
|
||||
|
||||
async function getSystemInfo() {
|
||||
try {
|
||||
let res = await fetch('/get/system_info')
|
||||
if (res.status === 200) {
|
||||
res = await res.json()
|
||||
let devices = res['devices']
|
||||
let hosts = res['hosts']
|
||||
const res = await SD.getSystemInfo()
|
||||
let devices = res['devices']
|
||||
|
||||
let allDeviceIds = Object.keys(devices['all']).filter(d => d !== 'cpu')
|
||||
let activeDeviceIds = Object.keys(devices['active']).filter(d => d !== 'cpu')
|
||||
let allDeviceIds = Object.keys(devices['all']).filter(d => d !== 'cpu')
|
||||
let activeDeviceIds = Object.keys(devices['active']).filter(d => d !== 'cpu')
|
||||
|
||||
if (activeDeviceIds.length === 0) {
|
||||
useCPUField.checked = true
|
||||
}
|
||||
|
||||
if (allDeviceIds.length < MIN_GPUS_TO_SHOW_SELECTION || useCPUField.checked) {
|
||||
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
|
||||
gpuSettingEntry.style.display = 'none'
|
||||
let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus')
|
||||
autoPickGPUSettingEntry.style.display = 'none'
|
||||
}
|
||||
|
||||
if (allDeviceIds.length === 0) {
|
||||
useCPUField.checked = true
|
||||
useCPUField.disabled = true // no compatible GPUs, so make the CPU mandatory
|
||||
}
|
||||
|
||||
autoPickGPUsField.checked = (devices['config'] === 'auto')
|
||||
|
||||
useGPUsField.innerHTML = ''
|
||||
allDeviceIds.forEach(device => {
|
||||
let deviceName = devices['all'][device]['name']
|
||||
let deviceOption = `<option value="${device}">${deviceName} (${device})</option>`
|
||||
useGPUsField.insertAdjacentHTML('beforeend', deviceOption)
|
||||
})
|
||||
|
||||
if (autoPickGPUsField.checked) {
|
||||
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
|
||||
gpuSettingEntry.style.display = 'none'
|
||||
} else {
|
||||
$('#use_gpus').val(activeDeviceIds)
|
||||
}
|
||||
|
||||
setDeviceInfo(devices)
|
||||
setHostInfo(hosts)
|
||||
if (activeDeviceIds.length === 0) {
|
||||
useCPUField.checked = true
|
||||
}
|
||||
|
||||
if (allDeviceIds.length < MIN_GPUS_TO_SHOW_SELECTION || useCPUField.checked) {
|
||||
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
|
||||
gpuSettingEntry.style.display = 'none'
|
||||
let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus')
|
||||
autoPickGPUSettingEntry.style.display = 'none'
|
||||
}
|
||||
|
||||
if (allDeviceIds.length === 0) {
|
||||
useCPUField.checked = true
|
||||
useCPUField.disabled = true // no compatible GPUs, so make the CPU mandatory
|
||||
}
|
||||
|
||||
autoPickGPUsField.checked = (devices['config'] === 'auto')
|
||||
|
||||
useGPUsField.innerHTML = ''
|
||||
allDeviceIds.forEach(device => {
|
||||
let deviceName = devices['all'][device]['name']
|
||||
let deviceOption = `<option value="${device}">${deviceName} (${device})</option>`
|
||||
useGPUsField.insertAdjacentHTML('beforeend', deviceOption)
|
||||
})
|
||||
|
||||
if (autoPickGPUsField.checked) {
|
||||
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
|
||||
gpuSettingEntry.style.display = 'none'
|
||||
} else {
|
||||
$('#use_gpus').val(activeDeviceIds)
|
||||
}
|
||||
|
||||
setDeviceInfo(devices)
|
||||
setHostInfo(res['hosts'])
|
||||
} catch (e) {
|
||||
console.log('error fetching devices', e)
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
let updateBranch = (useBetaChannelField.checked ? 'beta' : 'main')
|
||||
|
||||
if (listenPortField.value == '') {
|
||||
alert('The network port field must not be empty.')
|
||||
} else if (listenPortField.value<1 || listenPortField.value>65535) {
|
||||
alert('The network port must be a number from 1 to 65535')
|
||||
} else {
|
||||
changeAppConfig({
|
||||
'render_devices': getCurrentRenderDeviceSelection(),
|
||||
'update_branch': updateBranch,
|
||||
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked,
|
||||
'listen_to_network': listenToNetworkField.checked,
|
||||
'listen_port': listenPortField.value,
|
||||
'test_sd2': testSD2Field.checked
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (listenPortField.value < 1 || listenPortField.value > 65535) {
|
||||
alert('The network port must be a number from 1 to 65535')
|
||||
return
|
||||
}
|
||||
let updateBranch = (useBetaChannelField.checked ? 'beta' : 'main')
|
||||
changeAppConfig({
|
||||
'render_devices': getCurrentRenderDeviceSelection(),
|
||||
'update_branch': updateBranch,
|
||||
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked,
|
||||
'listen_to_network': listenToNetworkField.checked,
|
||||
'listen_port': listenPortField.value,
|
||||
'test_sd2': testSD2Field.checked
|
||||
})
|
||||
saveSettingsBtn.classList.add('active')
|
||||
asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active'))
|
||||
})
|
||||
|
@ -25,23 +25,47 @@ const PLUGINS = {
|
||||
* })
|
||||
*/
|
||||
IMAGE_INFO_BUTTONS: [],
|
||||
MODIFIERS_LOAD: []
|
||||
MODIFIERS_LOAD: [],
|
||||
TASK_CREATE: [],
|
||||
OUTPUTS_FORMATS: new ServiceContainer(
|
||||
function png() { return (reqBody) => new SD.RenderTask(reqBody) }
|
||||
, function jpeg() { return (reqBody) => new SD.RenderTask(reqBody) }
|
||||
),
|
||||
}
|
||||
PLUGINS.OUTPUTS_FORMATS.register = function(...args) {
|
||||
const service = ServiceContainer.prototype.register.apply(this, args)
|
||||
if (typeof outputFormatField !== 'undefined') {
|
||||
const newOption = document.createElement("option")
|
||||
newOption.setAttribute("value", service.name)
|
||||
newOption.innerText = service.name
|
||||
outputFormatField.appendChild(newOption)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
function loadScript(url) {
|
||||
const script = document.createElement('script')
|
||||
const promiseSrc = new PromiseSource()
|
||||
script.addEventListener('error', () => promiseSrc.reject(new Error(`Script "${url}" couldn't be loaded.`)))
|
||||
script.addEventListener('load', () => promiseSrc.resolve(url))
|
||||
script.src = url + '?t=' + Date.now()
|
||||
|
||||
console.log('loading script', url)
|
||||
document.head.appendChild(script)
|
||||
|
||||
return promiseSrc.promise
|
||||
}
|
||||
|
||||
async function loadUIPlugins() {
|
||||
try {
|
||||
let res = await fetch('/get/ui_plugins')
|
||||
if (res.status === 200) {
|
||||
res = await res.json()
|
||||
res.forEach(pluginPath => {
|
||||
let script = document.createElement('script')
|
||||
script.src = pluginPath + '?t=' + Date.now()
|
||||
|
||||
console.log('loading plugin', pluginPath)
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
const res = await fetch('/get/ui_plugins')
|
||||
if (!res.ok) {
|
||||
console.error(`Error HTTP${res.status} while loading plugins list. - ${res.statusText}`)
|
||||
return
|
||||
}
|
||||
const plugins = await res.json()
|
||||
const loadingPromises = plugins.map(loadScript)
|
||||
return await Promise.allSettled(loadingPromises)
|
||||
} catch (e) {
|
||||
console.log('error fetching plugin paths', e)
|
||||
}
|
||||
|
@ -1,32 +1,37 @@
|
||||
"use strict";
|
||||
|
||||
// https://gomakethings.com/finding-the-next-and-previous-sibling-elements-that-match-a-selector-with-vanilla-js/
|
||||
function getNextSibling(elem, selector) {
|
||||
// Get the next sibling element
|
||||
var sibling = elem.nextElementSibling
|
||||
let sibling = elem.nextElementSibling
|
||||
|
||||
// If there's no selector, return the first sibling
|
||||
if (!selector) return sibling
|
||||
if (!selector) {
|
||||
return sibling
|
||||
}
|
||||
|
||||
// If the sibling matches our selector, use it
|
||||
// If not, jump to the next sibling and continue the loop
|
||||
while (sibling) {
|
||||
if (sibling.matches(selector)) return sibling
|
||||
if (sibling.matches(selector)) {
|
||||
return sibling
|
||||
}
|
||||
sibling = sibling.nextElementSibling
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Panel Stuff */
|
||||
|
||||
// true = open
|
||||
var COLLAPSIBLES_INITIALIZED = false;
|
||||
let COLLAPSIBLES_INITIALIZED = false;
|
||||
const COLLAPSIBLES_KEY = "collapsibles";
|
||||
const COLLAPSIBLE_PANELS = []; // filled in by createCollapsibles with all the elements matching .collapsible
|
||||
|
||||
// on-init call this for any panels that are marked open
|
||||
function toggleCollapsible(element) {
|
||||
var collapsibleHeader = element.querySelector(".collapsible");
|
||||
var handle = element.querySelector(".collapsible-handle");
|
||||
const collapsibleHeader = element.querySelector(".collapsible");
|
||||
const handle = element.querySelector(".collapsible-handle");
|
||||
collapsibleHeader.classList.toggle("active")
|
||||
let content = getNextSibling(collapsibleHeader, '.collapsible-content')
|
||||
if (!collapsibleHeader.classList.contains("active")) {
|
||||
@ -47,16 +52,16 @@ function toggleCollapsible(element) {
|
||||
}
|
||||
|
||||
function saveCollapsibles() {
|
||||
var values = {}
|
||||
let values = {}
|
||||
COLLAPSIBLE_PANELS.forEach(element => {
|
||||
var value = element.querySelector(".collapsible").className.indexOf("active") !== -1
|
||||
let value = element.querySelector(".collapsible").className.indexOf("active") !== -1
|
||||
values[element.id] = value
|
||||
})
|
||||
localStorage.setItem(COLLAPSIBLES_KEY, JSON.stringify(values))
|
||||
}
|
||||
|
||||
function createCollapsibles(node) {
|
||||
var save = false
|
||||
let save = false
|
||||
if (!node) {
|
||||
node = document
|
||||
save = true
|
||||
@ -81,7 +86,7 @@ function createCollapsibles(node) {
|
||||
})
|
||||
})
|
||||
if (save) {
|
||||
var saved = localStorage.getItem(COLLAPSIBLES_KEY)
|
||||
let saved = localStorage.getItem(COLLAPSIBLES_KEY)
|
||||
if (!saved) {
|
||||
saved = tryLoadOldCollapsibles();
|
||||
}
|
||||
@ -89,9 +94,9 @@ function createCollapsibles(node) {
|
||||
saveCollapsibles()
|
||||
saved = localStorage.getItem(COLLAPSIBLES_KEY)
|
||||
}
|
||||
var values = JSON.parse(saved)
|
||||
let values = JSON.parse(saved)
|
||||
COLLAPSIBLE_PANELS.forEach(element => {
|
||||
var value = element.querySelector(".collapsible").className.indexOf("active") !== -1
|
||||
let value = element.querySelector(".collapsible").className.indexOf("active") !== -1
|
||||
if (values[element.id] != value) {
|
||||
toggleCollapsible(element)
|
||||
}
|
||||
@ -101,17 +106,17 @@ function createCollapsibles(node) {
|
||||
}
|
||||
|
||||
function tryLoadOldCollapsibles() {
|
||||
var old_map = {
|
||||
const old_map = {
|
||||
"advancedPanelOpen": "editor-settings",
|
||||
"modifiersPanelOpen": "editor-modifiers",
|
||||
"negativePromptPanelOpen": "editor-inputs-prompt"
|
||||
};
|
||||
if (localStorage.getItem(Object.keys(old_map)[0])) {
|
||||
var result = {};
|
||||
let result = {};
|
||||
Object.keys(old_map).forEach(key => {
|
||||
var value = localStorage.getItem(key);
|
||||
const value = localStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
result[old_map[key]] = value == true || value == "true"
|
||||
result[old_map[key]] = (value == true || value == "true")
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
});
|
||||
@ -150,17 +155,17 @@ function millisecondsToStr(milliseconds) {
|
||||
return (number > 1) ? 's' : ''
|
||||
}
|
||||
|
||||
var temp = Math.floor(milliseconds / 1000)
|
||||
var hours = Math.floor((temp %= 86400) / 3600)
|
||||
var s = ''
|
||||
let temp = Math.floor(milliseconds / 1000)
|
||||
let hours = Math.floor((temp %= 86400) / 3600)
|
||||
let s = ''
|
||||
if (hours) {
|
||||
s += hours + ' hour' + numberEnding(hours) + ' '
|
||||
}
|
||||
var minutes = Math.floor((temp %= 3600) / 60)
|
||||
let minutes = Math.floor((temp %= 3600) / 60)
|
||||
if (minutes) {
|
||||
s += minutes + ' minute' + numberEnding(minutes) + ' '
|
||||
}
|
||||
var seconds = temp % 60
|
||||
let seconds = temp % 60
|
||||
if (!hours && minutes < 4 && seconds) {
|
||||
s += seconds + ' second' + numberEnding(seconds)
|
||||
}
|
||||
@ -178,7 +183,7 @@ function BraceExpander() {
|
||||
function bracePair(tkns, iPosn, iNest, lstCommas) {
|
||||
if (iPosn >= tkns.length || iPosn < 0) return null;
|
||||
|
||||
var t = tkns[iPosn],
|
||||
let t = tkns[iPosn],
|
||||
n = (t === '{') ? (
|
||||
iNest + 1
|
||||
) : (t === '}' ? (
|
||||
@ -198,7 +203,7 @@ function BraceExpander() {
|
||||
function andTree(dctSofar, tkns) {
|
||||
if (!tkns.length) return [dctSofar, []];
|
||||
|
||||
var dctParse = dctSofar ? dctSofar : {
|
||||
let dctParse = dctSofar ? dctSofar : {
|
||||
fn: and,
|
||||
args: []
|
||||
},
|
||||
@ -231,14 +236,14 @@ function BraceExpander() {
|
||||
// Parse of a PARADIGM subtree
|
||||
function orTree(dctSofar, tkns, lstCommas) {
|
||||
if (!tkns.length) return [dctSofar, []];
|
||||
var iLast = lstCommas.length;
|
||||
let iLast = lstCommas.length;
|
||||
|
||||
return {
|
||||
fn: or,
|
||||
args: splitsAt(
|
||||
lstCommas, tkns
|
||||
).map(function (x, i) {
|
||||
var ts = x.slice(
|
||||
let ts = x.slice(
|
||||
1, i === iLast ? (
|
||||
-1
|
||||
) : void 0
|
||||
@ -256,7 +261,7 @@ function BraceExpander() {
|
||||
// List of unescaped braces and commas, and remaining strings
|
||||
function tokens(str) {
|
||||
// Filter function excludes empty splitting artefacts
|
||||
var toS = function (x) {
|
||||
let toS = function (x) {
|
||||
return x.toString();
|
||||
};
|
||||
|
||||
@ -270,7 +275,7 @@ function BraceExpander() {
|
||||
// PARSE TREE OPERATOR (1 of 2)
|
||||
// Each possible head * each possible tail
|
||||
function and(args) {
|
||||
var lng = args.length,
|
||||
let lng = args.length,
|
||||
head = lng ? args[0] : null,
|
||||
lstHead = "string" === typeof head ? (
|
||||
[head]
|
||||
@ -330,7 +335,7 @@ function BraceExpander() {
|
||||
// s -> [s]
|
||||
this.expand = function(s) {
|
||||
// BRACE EXPRESSION PARSED
|
||||
var dctParse = andTree(null, tokens(s))[0];
|
||||
let dctParse = andTree(null, tokens(s))[0];
|
||||
|
||||
// ABSTRACT SYNTAX TREE LOGGED
|
||||
// console.log(pp(dctParse));
|
||||
@ -341,21 +346,75 @@ function BraceExpander() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Pause the execution of an async function until timer elapse.
|
||||
* @Returns a promise that will resolve after the specified timeout.
|
||||
*/
|
||||
function asyncDelay(timeout) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve, timeout, true)
|
||||
})
|
||||
}
|
||||
|
||||
/* Simple debounce function, placeholder for the one in engine.js for simple use cases */
|
||||
function debounce(func, timeout = 300){
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||
};
|
||||
function PromiseSource() {
|
||||
const srcPromise = new Promise((resolve, reject) => {
|
||||
Object.defineProperties(this, {
|
||||
resolve: { value: resolve, writable: false }
|
||||
, reject: { value: reject, writable: false }
|
||||
})
|
||||
})
|
||||
Object.defineProperties(this, {
|
||||
promise: {value: makeQuerablePromise(srcPromise), writable: false}
|
||||
})
|
||||
}
|
||||
|
||||
/** A debounce is a higher-order function, which is a function that returns another function
|
||||
* that, as long as it continues to be invoked, will not be triggered.
|
||||
* The function will be called after it stops being called for N milliseconds.
|
||||
* If `immediate` is passed, trigger the function on the leading edge, instead of the trailing.
|
||||
* @Returns a promise that will resolve to func return value.
|
||||
*/
|
||||
function debounce (func, wait, immediate) {
|
||||
if (typeof wait === "undefined") {
|
||||
wait = 40
|
||||
}
|
||||
if (typeof wait !== "number") {
|
||||
throw new Error("wait is not an number.")
|
||||
}
|
||||
let timeout = null
|
||||
let lastPromiseSrc = new PromiseSource()
|
||||
const applyFn = function(context, args) {
|
||||
let result = undefined
|
||||
try {
|
||||
result = func.apply(context, args)
|
||||
} catch (err) {
|
||||
lastPromiseSrc.reject(err)
|
||||
}
|
||||
if (result instanceof Promise) {
|
||||
result.then(lastPromiseSrc.resolve, lastPromiseSrc.reject)
|
||||
} else {
|
||||
lastPromiseSrc.resolve(result)
|
||||
}
|
||||
}
|
||||
return function(...args) {
|
||||
const callNow = Boolean(immediate && !timeout)
|
||||
const context = this;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(function () {
|
||||
if (!immediate) {
|
||||
applyFn(context, args)
|
||||
}
|
||||
lastPromiseSrc = new PromiseSource()
|
||||
timeout = null
|
||||
}, wait)
|
||||
if (callNow) {
|
||||
applyFn(context, args)
|
||||
}
|
||||
return lastPromiseSrc.promise
|
||||
}
|
||||
}
|
||||
|
||||
function preventNonNumericalInput(e) {
|
||||
e = e || window.event;
|
||||
@ -369,6 +428,83 @@ function preventNonNumericalInput(e) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the global object for the current execution environement.
|
||||
* @Returns window in a browser, global in node and self in a ServiceWorker.
|
||||
* @Notes Allows unit testing and use of the engine outside of a browser.
|
||||
*/
|
||||
function getGlobal() {
|
||||
if (typeof globalThis === 'object') {
|
||||
return globalThis
|
||||
} else if (typeof global === 'object') {
|
||||
return global
|
||||
} else if (typeof self === 'object') {
|
||||
return self
|
||||
}
|
||||
try {
|
||||
return Function('return this')()
|
||||
} catch {
|
||||
// If the Function constructor fails, we're in a browser with eval disabled by CSP headers.
|
||||
return window
|
||||
} // Returns undefined if global can't be found.
|
||||
}
|
||||
|
||||
/** Check if x is an Array or a TypedArray.
|
||||
* @Returns true if x is an Array or a TypedArray, false otherwise.
|
||||
*/
|
||||
function isArrayOrTypedArray(x) {
|
||||
return Boolean(typeof x === 'object' && (Array.isArray(x) || (ArrayBuffer.isView(x) && !(x instanceof DataView))))
|
||||
}
|
||||
|
||||
function makeQuerablePromise(promise) {
|
||||
if (typeof promise !== 'object') {
|
||||
throw new Error('promise is not an object.')
|
||||
}
|
||||
if (!(promise instanceof Promise)) {
|
||||
throw new Error('Argument is not a promise.')
|
||||
}
|
||||
// Don't modify a promise that's been already modified.
|
||||
if ('isResolved' in promise || 'isRejected' in promise || 'isPending' in promise) {
|
||||
return promise
|
||||
}
|
||||
let isPending = true
|
||||
let isRejected = false
|
||||
let rejectReason = undefined
|
||||
let isResolved = false
|
||||
let resolvedValue = undefined
|
||||
const qurPro = promise.then(
|
||||
function(val){
|
||||
isResolved = true
|
||||
isPending = false
|
||||
resolvedValue = val
|
||||
return val
|
||||
}
|
||||
, function(reason) {
|
||||
rejectReason = reason
|
||||
isRejected = true
|
||||
isPending = false
|
||||
throw reason
|
||||
}
|
||||
)
|
||||
Object.defineProperties(qurPro, {
|
||||
'isResolved': {
|
||||
get: () => isResolved
|
||||
}
|
||||
, 'resolvedValue': {
|
||||
get: () => resolvedValue
|
||||
}
|
||||
, 'isPending': {
|
||||
get: () => isPending
|
||||
}
|
||||
, 'isRejected': {
|
||||
get: () => isRejected
|
||||
}
|
||||
, 'rejectReason': {
|
||||
get: () => rejectReason
|
||||
}
|
||||
})
|
||||
return qurPro
|
||||
}
|
||||
|
||||
/* inserts custom html to allow prettifying of inputs */
|
||||
function prettifyInputs(root_element) {
|
||||
root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => {
|
||||
@ -384,3 +520,156 @@ function prettifyInputs(root_element) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class GenericEventSource {
|
||||
#events = {};
|
||||
#types = []
|
||||
constructor(...eventsTypes) {
|
||||
if (Array.isArray(eventsTypes) && eventsTypes.length === 1 && Array.isArray(eventsTypes[0])) {
|
||||
eventsTypes = eventsTypes[0]
|
||||
}
|
||||
this.#types.push(...eventsTypes)
|
||||
}
|
||||
get eventTypes() {
|
||||
return this.#types
|
||||
}
|
||||
/** Add a new event listener
|
||||
*/
|
||||
addEventListener(name, handler) {
|
||||
if (!this.#types.includes(name)) {
|
||||
throw new Error('Invalid event name.')
|
||||
}
|
||||
if (this.#events.hasOwnProperty(name)) {
|
||||
this.#events[name].push(handler)
|
||||
} else {
|
||||
this.#events[name] = [handler]
|
||||
}
|
||||
}
|
||||
/** Remove the event listener
|
||||
*/
|
||||
removeEventListener(name, handler) {
|
||||
if (!this.#events.hasOwnProperty(name)) {
|
||||
return
|
||||
}
|
||||
const index = this.#events[name].indexOf(handler)
|
||||
if (index != -1) {
|
||||
this.#events[name].splice(index, 1)
|
||||
}
|
||||
}
|
||||
fireEvent(name, ...args) {
|
||||
if (!this.#types.includes(name)) {
|
||||
throw new Error(`Event ${String(name)} missing from Events.types`)
|
||||
}
|
||||
if (!this.#events.hasOwnProperty(name)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!args || !args.length) {
|
||||
args = []
|
||||
}
|
||||
const evs = this.#events[name]
|
||||
if (evs.length <= 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.allSettled(evs.map((callback) => {
|
||||
try {
|
||||
return Promise.resolve(callback.apply(SD, args))
|
||||
} catch (ex) {
|
||||
return Promise.reject(ex)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceContainer {
|
||||
#services = new Map()
|
||||
#singletons = new Map()
|
||||
constructor(...servicesParams) {
|
||||
servicesParams.forEach(this.register.bind(this))
|
||||
}
|
||||
get services () {
|
||||
return this.#services
|
||||
}
|
||||
get singletons() {
|
||||
return this.#singletons
|
||||
}
|
||||
register(params) {
|
||||
if (ServiceContainer.isConstructor(params)) {
|
||||
if (typeof params.name !== 'string') {
|
||||
throw new Error('params.name is not a string.')
|
||||
}
|
||||
params = {name:params.name, definition:params}
|
||||
}
|
||||
if (typeof params !== 'object') {
|
||||
throw new Error('params is not an object.')
|
||||
}
|
||||
[ 'name',
|
||||
'definition',
|
||||
].forEach((key) => {
|
||||
if (!(key in params)) {
|
||||
console.error('Invalid service %o registration.', params)
|
||||
throw new Error(`params.${key} is not defined.`)
|
||||
}
|
||||
})
|
||||
const opts = {definition: params.definition}
|
||||
if ('dependencies' in params) {
|
||||
if (Array.isArray(params.dependencies)) {
|
||||
params.dependencies.forEach((dep) => {
|
||||
if (typeof dep !== 'string') {
|
||||
throw new Error('dependency name is not a string.')
|
||||
}
|
||||
})
|
||||
opts.dependencies = params.dependencies
|
||||
} else {
|
||||
throw new Error('params.dependencies is not an array.')
|
||||
}
|
||||
}
|
||||
if (params.singleton) {
|
||||
opts.singleton = true
|
||||
}
|
||||
this.#services.set(params.name, opts)
|
||||
return Object.assign({name: params.name}, opts)
|
||||
}
|
||||
get(name) {
|
||||
const ctorInfos = this.#services.get(name)
|
||||
if (!ctorInfos) {
|
||||
return
|
||||
}
|
||||
if(!ServiceContainer.isConstructor(ctorInfos.definition)) {
|
||||
return ctorInfos.definition
|
||||
}
|
||||
if(!ctorInfos.singleton) {
|
||||
return this._createInstance(ctorInfos)
|
||||
}
|
||||
const singletonInstance = this.#singletons.get(name)
|
||||
if(singletonInstance) {
|
||||
return singletonInstance
|
||||
}
|
||||
const newSingletonInstance = this._createInstance(ctorInfos)
|
||||
this.#singletons.set(name, newSingletonInstance)
|
||||
return newSingletonInstance
|
||||
}
|
||||
|
||||
_getResolvedDependencies(service) {
|
||||
let classDependencies = []
|
||||
if(service.dependencies) {
|
||||
classDependencies = service.dependencies.map(this.get.bind(this))
|
||||
}
|
||||
return classDependencies
|
||||
}
|
||||
|
||||
_createInstance(service) {
|
||||
if (!ServiceContainer.isClass(service.definition)) {
|
||||
// Call as normal function.
|
||||
return service.definition(...this._getResolvedDependencies(service))
|
||||
}
|
||||
// Use new
|
||||
return new service.definition(...this._getResolvedDependencies(service))
|
||||
}
|
||||
|
||||
static isClass(definition) {
|
||||
return typeof definition === 'function' && Boolean(definition.prototype) && definition.prototype.constructor === definition
|
||||
}
|
||||
static isConstructor(definition) {
|
||||
return typeof definition === 'function'
|
||||
}
|
||||
}
|
||||
|
@ -17,17 +17,11 @@
|
||||
prettifyInputs(document);
|
||||
let autoScroll = document.querySelector("#auto_scroll")
|
||||
|
||||
/**
|
||||
* the use of initSettings() in the autoscroll plugin seems to be breaking the models dropdown and the save-to-disk folder field
|
||||
* in the settings tab. They're both blank, because they're being re-initialized. Their earlier values came from the API call,
|
||||
* but those values aren't stored in localStorage, since they aren't user-specified.
|
||||
* So when initSettings() is called a second time, it overwrites the values with an empty string.
|
||||
*
|
||||
* We could either rework how new components can register themselves to be auto-saved, without having to call initSettings() again.
|
||||
* Or we could move the autoscroll code into the main code, and include it in the list of fields in auto-save.js
|
||||
*/
|
||||
// SETTINGS_IDS_LIST.push("auto_scroll")
|
||||
// initSettings()
|
||||
// save/restore the toggle state
|
||||
autoScroll.addEventListener('click', (e) => {
|
||||
localStorage.setItem('auto_scroll', autoScroll.checked)
|
||||
})
|
||||
autoScroll.checked = localStorage.getItem('auto_scroll') == "true"
|
||||
|
||||
// observe for changes in the preview pane
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
|
@ -1,7 +1,10 @@
|
||||
(function () {
|
||||
"use strict"
|
||||
(function () { "use strict"
|
||||
if (typeof editorModifierTagsList !== 'object') {
|
||||
console.error('editorModifierTagsList missing...')
|
||||
return
|
||||
}
|
||||
|
||||
var styleSheet = document.createElement("style");
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.textContent = `
|
||||
.modifier-card-tiny.drag-sort-active {
|
||||
background: transparent;
|
||||
@ -12,7 +15,7 @@
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
// observe for changes in tag list
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
// mutations.forEach(function (mutation) {
|
||||
if (editorModifierTagsList.childNodes.length > 0) {
|
||||
ModifierDragAndDrop(editorModifierTagsList)
|
||||
|
@ -1,8 +1,11 @@
|
||||
(function () {
|
||||
"use strict"
|
||||
(function () { "use strict"
|
||||
if (typeof editorModifierTagsList !== 'object') {
|
||||
console.error('editorModifierTagsList missing...')
|
||||
return
|
||||
}
|
||||
|
||||
// observe for changes in tag list
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
// mutations.forEach(function (mutation) {
|
||||
if (editorModifierTagsList.childNodes.length > 0) {
|
||||
ModifierMouseWheel(editorModifierTagsList)
|
||||
|
29
ui/plugins/ui/SpecRunner.html
Normal file
29
ui/plugins/ui/SpecRunner.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Jasmine Spec Runner v4.5.0</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="./jasmine/jasmine_favicon.png">
|
||||
<link rel="stylesheet" href="./jasmine/jasmine.css">
|
||||
|
||||
<script src="./jasmine/jasmine.js"></script>
|
||||
<script src="./jasmine/jasmine-html.js"></script>
|
||||
<script src="./jasmine/boot0.js"></script>
|
||||
<!-- optional: include a file here that configures the Jasmine env -->
|
||||
<script src="./jasmine/boot1.js"></script>
|
||||
|
||||
<!-- include source files here... -->
|
||||
<script src="/media/js/utils.js?v=4"></script>
|
||||
<script src="/media/js/engine.js?v=1"></script>
|
||||
<!-- <script src="./engine.js?v=1"></script> -->
|
||||
<script src="/media/js/plugins.js?v=1"></script>
|
||||
|
||||
<!-- include spec files here... -->
|
||||
<script src="./jasmineSpec.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
64
ui/plugins/ui/jasmine/boot0.js
Normal file
64
ui/plugins/ui/jasmine/boot0.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright (c) 2008-2022 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
/**
|
||||
This file starts the process of "booting" Jasmine. It initializes Jasmine,
|
||||
makes its globals available, and creates the env. This file should be loaded
|
||||
after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project
|
||||
source files or spec files are loaded.
|
||||
*/
|
||||
(function() {
|
||||
const jasmineRequire = window.jasmineRequire || require('./jasmine.js');
|
||||
|
||||
/**
|
||||
* ## Require & Instantiate
|
||||
*
|
||||
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
|
||||
*/
|
||||
const jasmine = jasmineRequire.core(jasmineRequire),
|
||||
global = jasmine.getGlobal();
|
||||
global.jasmine = jasmine;
|
||||
|
||||
/**
|
||||
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
|
||||
*/
|
||||
jasmineRequire.html(jasmine);
|
||||
|
||||
/**
|
||||
* Create the Jasmine environment. This is used to run all specs in a project.
|
||||
*/
|
||||
const env = jasmine.getEnv();
|
||||
|
||||
/**
|
||||
* ## The Global Interface
|
||||
*
|
||||
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
|
||||
*/
|
||||
const jasmineInterface = jasmineRequire.interface(jasmine, env);
|
||||
|
||||
/**
|
||||
* Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
|
||||
*/
|
||||
for (const property in jasmineInterface) {
|
||||
global[property] = jasmineInterface[property];
|
||||
}
|
||||
})();
|
132
ui/plugins/ui/jasmine/boot1.js
Normal file
132
ui/plugins/ui/jasmine/boot1.js
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
Copyright (c) 2008-2022 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
/**
|
||||
This file finishes 'booting' Jasmine, performing all of the necessary
|
||||
initialization before executing the loaded environment and all of a project's
|
||||
specs. This file should be loaded after `boot0.js` but before any project
|
||||
source files or spec files are loaded. Thus this file can also be used to
|
||||
customize Jasmine for a project.
|
||||
|
||||
If a project is using Jasmine via the standalone distribution, this file can
|
||||
be customized directly. If you only wish to configure the Jasmine env, you
|
||||
can load another file that calls `jasmine.getEnv().configure({...})`
|
||||
after `boot0.js` is loaded and before this file is loaded.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
const env = jasmine.getEnv();
|
||||
|
||||
/**
|
||||
* ## Runner Parameters
|
||||
*
|
||||
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
|
||||
*/
|
||||
|
||||
const queryString = new jasmine.QueryString({
|
||||
getWindowLocation: function() {
|
||||
return window.location;
|
||||
}
|
||||
});
|
||||
|
||||
const filterSpecs = !!queryString.getParam('spec');
|
||||
|
||||
const config = {
|
||||
stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'),
|
||||
stopSpecOnExpectationFailure: queryString.getParam(
|
||||
'stopSpecOnExpectationFailure'
|
||||
),
|
||||
hideDisabled: queryString.getParam('hideDisabled')
|
||||
};
|
||||
|
||||
const random = queryString.getParam('random');
|
||||
|
||||
if (random !== undefined && random !== '') {
|
||||
config.random = random;
|
||||
}
|
||||
|
||||
const seed = queryString.getParam('seed');
|
||||
if (seed) {
|
||||
config.seed = seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Reporters
|
||||
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
|
||||
*/
|
||||
const htmlReporter = new jasmine.HtmlReporter({
|
||||
env: env,
|
||||
navigateWithNewParam: function(key, value) {
|
||||
return queryString.navigateWithNewParam(key, value);
|
||||
},
|
||||
addToExistingQueryString: function(key, value) {
|
||||
return queryString.fullStringWithNewParam(key, value);
|
||||
},
|
||||
getContainer: function() {
|
||||
return document.body;
|
||||
},
|
||||
createElement: function() {
|
||||
return document.createElement.apply(document, arguments);
|
||||
},
|
||||
createTextNode: function() {
|
||||
return document.createTextNode.apply(document, arguments);
|
||||
},
|
||||
timer: new jasmine.Timer(),
|
||||
filterSpecs: filterSpecs
|
||||
});
|
||||
|
||||
/**
|
||||
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
|
||||
*/
|
||||
env.addReporter(jsApiReporter);
|
||||
env.addReporter(htmlReporter);
|
||||
|
||||
/**
|
||||
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
|
||||
*/
|
||||
const specFilter = new jasmine.HtmlSpecFilter({
|
||||
filterString: function() {
|
||||
return queryString.getParam('spec');
|
||||
}
|
||||
});
|
||||
|
||||
config.specFilter = function(spec) {
|
||||
return specFilter.matches(spec.getFullName());
|
||||
};
|
||||
|
||||
env.configure(config);
|
||||
|
||||
/**
|
||||
* ## Execution
|
||||
*
|
||||
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
|
||||
*/
|
||||
const currentWindowOnload = window.onload;
|
||||
|
||||
window.onload = function() {
|
||||
if (currentWindowOnload) {
|
||||
currentWindowOnload();
|
||||
}
|
||||
htmlReporter.initialize();
|
||||
env.execute();
|
||||
};
|
||||
})();
|
964
ui/plugins/ui/jasmine/jasmine-html.js
Normal file
964
ui/plugins/ui/jasmine/jasmine-html.js
Normal file
@ -0,0 +1,964 @@
|
||||
/*
|
||||
Copyright (c) 2008-2022 Pivotal Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
// eslint-disable-next-line no-var
|
||||
var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
|
||||
|
||||
jasmineRequire.html = function(j$) {
|
||||
j$.ResultsNode = jasmineRequire.ResultsNode();
|
||||
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
|
||||
j$.QueryString = jasmineRequire.QueryString();
|
||||
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
|
||||
};
|
||||
|
||||
jasmineRequire.HtmlReporter = function(j$) {
|
||||
function ResultsStateBuilder() {
|
||||
this.topResults = new j$.ResultsNode({}, '', null);
|
||||
this.currentParent = this.topResults;
|
||||
this.specsExecuted = 0;
|
||||
this.failureCount = 0;
|
||||
this.pendingSpecCount = 0;
|
||||
}
|
||||
|
||||
ResultsStateBuilder.prototype.suiteStarted = function(result) {
|
||||
this.currentParent.addChild(result, 'suite');
|
||||
this.currentParent = this.currentParent.last();
|
||||
};
|
||||
|
||||
ResultsStateBuilder.prototype.suiteDone = function(result) {
|
||||
this.currentParent.updateResult(result);
|
||||
if (this.currentParent !== this.topResults) {
|
||||
this.currentParent = this.currentParent.parent;
|
||||
}
|
||||
|
||||
if (result.status === 'failed') {
|
||||
this.failureCount++;
|
||||
}
|
||||
};
|
||||
|
||||
ResultsStateBuilder.prototype.specStarted = function(result) {};
|
||||
|
||||
ResultsStateBuilder.prototype.specDone = function(result) {
|
||||
this.currentParent.addChild(result, 'spec');
|
||||
|
||||
if (result.status !== 'excluded') {
|
||||
this.specsExecuted++;
|
||||
}
|
||||
|
||||
if (result.status === 'failed') {
|
||||
this.failureCount++;
|
||||
}
|
||||
|
||||
if (result.status == 'pending') {
|
||||
this.pendingSpecCount++;
|
||||
}
|
||||
};
|
||||
|
||||
ResultsStateBuilder.prototype.jasmineDone = function(result) {
|
||||
if (result.failedExpectations) {
|
||||
this.failureCount += result.failedExpectations.length;
|
||||
}
|
||||
};
|
||||
|
||||
function HtmlReporter(options) {
|
||||
function config() {
|
||||
return (options.env && options.env.configuration()) || {};
|
||||
}
|
||||
|
||||
const getContainer = options.getContainer;
|
||||
const createElement = options.createElement;
|
||||
const createTextNode = options.createTextNode;
|
||||
const navigateWithNewParam = options.navigateWithNewParam || function() {};
|
||||
const addToExistingQueryString =
|
||||
options.addToExistingQueryString || defaultQueryString;
|
||||
const filterSpecs = options.filterSpecs;
|
||||
let htmlReporterMain;
|
||||
let symbols;
|
||||
const deprecationWarnings = [];
|
||||
const failures = [];
|
||||
|
||||
this.initialize = function() {
|
||||
clearPrior();
|
||||
htmlReporterMain = createDom(
|
||||
'div',
|
||||
{ className: 'jasmine_html-reporter' },
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-banner' },
|
||||
createDom('a', {
|
||||
className: 'jasmine-title',
|
||||
href: 'http://jasmine.github.io/',
|
||||
target: '_blank'
|
||||
}),
|
||||
createDom('span', { className: 'jasmine-version' }, j$.version)
|
||||
),
|
||||
createDom('ul', { className: 'jasmine-symbol-summary' }),
|
||||
createDom('div', { className: 'jasmine-alert' }),
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-results' },
|
||||
createDom('div', { className: 'jasmine-failures' })
|
||||
)
|
||||
);
|
||||
getContainer().appendChild(htmlReporterMain);
|
||||
};
|
||||
|
||||
let totalSpecsDefined;
|
||||
this.jasmineStarted = function(options) {
|
||||
totalSpecsDefined = options.totalSpecsDefined || 0;
|
||||
};
|
||||
|
||||
const summary = createDom('div', { className: 'jasmine-summary' });
|
||||
|
||||
const stateBuilder = new ResultsStateBuilder();
|
||||
|
||||
this.suiteStarted = function(result) {
|
||||
stateBuilder.suiteStarted(result);
|
||||
};
|
||||
|
||||
this.suiteDone = function(result) {
|
||||
stateBuilder.suiteDone(result);
|
||||
|
||||
if (result.status === 'failed') {
|
||||
failures.push(failureDom(result));
|
||||
}
|
||||
addDeprecationWarnings(result, 'suite');
|
||||
};
|
||||
|
||||
this.specStarted = function(result) {
|
||||
stateBuilder.specStarted(result);
|
||||
};
|
||||
|
||||
this.specDone = function(result) {
|
||||
stateBuilder.specDone(result);
|
||||
|
||||
if (noExpectations(result)) {
|
||||
const noSpecMsg = "Spec '" + result.fullName + "' has no expectations.";
|
||||
if (result.status === 'failed') {
|
||||
console.error(noSpecMsg);
|
||||
} else {
|
||||
console.warn(noSpecMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!symbols) {
|
||||
symbols = find('.jasmine-symbol-summary');
|
||||
}
|
||||
|
||||
symbols.appendChild(
|
||||
createDom('li', {
|
||||
className: this.displaySpecInCorrectFormat(result),
|
||||
id: 'spec_' + result.id,
|
||||
title: result.fullName
|
||||
})
|
||||
);
|
||||
|
||||
if (result.status === 'failed') {
|
||||
failures.push(failureDom(result));
|
||||
}
|
||||
|
||||
addDeprecationWarnings(result, 'spec');
|
||||
};
|
||||
|
||||
this.displaySpecInCorrectFormat = function(result) {
|
||||
return noExpectations(result) && result.status === 'passed'
|
||||
? 'jasmine-empty'
|
||||
: this.resultStatus(result.status);
|
||||
};
|
||||
|
||||
this.resultStatus = function(status) {
|
||||
if (status === 'excluded') {
|
||||
return config().hideDisabled
|
||||
? 'jasmine-excluded-no-display'
|
||||
: 'jasmine-excluded';
|
||||
}
|
||||
return 'jasmine-' + status;
|
||||
};
|
||||
|
||||
this.jasmineDone = function(doneResult) {
|
||||
stateBuilder.jasmineDone(doneResult);
|
||||
const banner = find('.jasmine-banner');
|
||||
const alert = find('.jasmine-alert');
|
||||
const order = doneResult && doneResult.order;
|
||||
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-duration' },
|
||||
'finished in ' + doneResult.totalTime / 1000 + 's'
|
||||
)
|
||||
);
|
||||
|
||||
banner.appendChild(optionsMenu(config()));
|
||||
|
||||
if (stateBuilder.specsExecuted < totalSpecsDefined) {
|
||||
const skippedMessage =
|
||||
'Ran ' +
|
||||
stateBuilder.specsExecuted +
|
||||
' of ' +
|
||||
totalSpecsDefined +
|
||||
' specs - run all';
|
||||
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
|
||||
const skippedLink =
|
||||
(window.location.pathname || '') +
|
||||
addToExistingQueryString('spec', '');
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-bar jasmine-skipped' },
|
||||
createDom(
|
||||
'a',
|
||||
{ href: skippedLink, title: 'Run all specs' },
|
||||
skippedMessage
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
let statusBarMessage = '';
|
||||
let statusBarClassName = 'jasmine-overall-result jasmine-bar ';
|
||||
const globalFailures =
|
||||
(doneResult && doneResult.failedExpectations) || [];
|
||||
const failed = stateBuilder.failureCount + globalFailures.length > 0;
|
||||
|
||||
if (totalSpecsDefined > 0 || failed) {
|
||||
statusBarMessage +=
|
||||
pluralize('spec', stateBuilder.specsExecuted) +
|
||||
', ' +
|
||||
pluralize('failure', stateBuilder.failureCount);
|
||||
if (stateBuilder.pendingSpecCount) {
|
||||
statusBarMessage +=
|
||||
', ' + pluralize('pending spec', stateBuilder.pendingSpecCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (doneResult.overallStatus === 'passed') {
|
||||
statusBarClassName += ' jasmine-passed ';
|
||||
} else if (doneResult.overallStatus === 'incomplete') {
|
||||
statusBarClassName += ' jasmine-incomplete ';
|
||||
statusBarMessage =
|
||||
'Incomplete: ' +
|
||||
doneResult.incompleteReason +
|
||||
', ' +
|
||||
statusBarMessage;
|
||||
} else {
|
||||
statusBarClassName += ' jasmine-failed ';
|
||||
}
|
||||
|
||||
let seedBar;
|
||||
if (order && order.random) {
|
||||
seedBar = createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-seed-bar' },
|
||||
', randomized with seed ',
|
||||
createDom(
|
||||
'a',
|
||||
{
|
||||
title: 'randomized with seed ' + order.seed,
|
||||
href: seedHref(order.seed)
|
||||
},
|
||||
order.seed
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: statusBarClassName },
|
||||
statusBarMessage,
|
||||
seedBar
|
||||
)
|
||||
);
|
||||
|
||||
const errorBarClassName = 'jasmine-bar jasmine-errored';
|
||||
const afterAllMessagePrefix = 'AfterAll ';
|
||||
|
||||
for (let i = 0; i < globalFailures.length; i++) {
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: errorBarClassName },
|
||||
globalFailureMessage(globalFailures[i])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function globalFailureMessage(failure) {
|
||||
if (failure.globalErrorType === 'load') {
|
||||
const prefix = 'Error during loading: ' + failure.message;
|
||||
|
||||
if (failure.filename) {
|
||||
return (
|
||||
prefix + ' in ' + failure.filename + ' line ' + failure.lineno
|
||||
);
|
||||
} else {
|
||||
return prefix;
|
||||
}
|
||||
} else if (failure.globalErrorType === 'afterAll') {
|
||||
return afterAllMessagePrefix + failure.message;
|
||||
} else {
|
||||
return failure.message;
|
||||
}
|
||||
}
|
||||
|
||||
addDeprecationWarnings(doneResult);
|
||||
|
||||
for (let i = 0; i < deprecationWarnings.length; i++) {
|
||||
const children = [];
|
||||
let context;
|
||||
|
||||
switch (deprecationWarnings[i].runnableType) {
|
||||
case 'spec':
|
||||
context = '(in spec: ' + deprecationWarnings[i].runnableName + ')';
|
||||
break;
|
||||
case 'suite':
|
||||
context = '(in suite: ' + deprecationWarnings[i].runnableName + ')';
|
||||
break;
|
||||
default:
|
||||
context = '';
|
||||
}
|
||||
|
||||
deprecationWarnings[i].message.split('\n').forEach(function(line) {
|
||||
children.push(line);
|
||||
children.push(createDom('br'));
|
||||
});
|
||||
|
||||
children[0] = 'DEPRECATION: ' + children[0];
|
||||
children.push(context);
|
||||
|
||||
if (deprecationWarnings[i].stack) {
|
||||
children.push(createExpander(deprecationWarnings[i].stack));
|
||||
}
|
||||
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-bar jasmine-warning' },
|
||||
children
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = find('.jasmine-results');
|
||||
results.appendChild(summary);
|
||||
|
||||
summaryList(stateBuilder.topResults, summary);
|
||||
|
||||
if (failures.length) {
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
|
||||
createDom('span', {}, 'Spec List | '),
|
||||
createDom(
|
||||
'a',
|
||||
{ className: 'jasmine-failures-menu', href: '#' },
|
||||
'Failures'
|
||||
)
|
||||
)
|
||||
);
|
||||
alert.appendChild(
|
||||
createDom(
|
||||
'span',
|
||||
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
|
||||
createDom(
|
||||
'a',
|
||||
{ className: 'jasmine-spec-list-menu', href: '#' },
|
||||
'Spec List'
|
||||
),
|
||||
createDom('span', {}, ' | Failures ')
|
||||
)
|
||||
);
|
||||
|
||||
find('.jasmine-failures-menu').onclick = function() {
|
||||
setMenuModeTo('jasmine-failure-list');
|
||||
return false;
|
||||
};
|
||||
find('.jasmine-spec-list-menu').onclick = function() {
|
||||
setMenuModeTo('jasmine-spec-list');
|
||||
return false;
|
||||
};
|
||||
|
||||
setMenuModeTo('jasmine-failure-list');
|
||||
|
||||
const failureNode = find('.jasmine-failures');
|
||||
for (let i = 0; i < failures.length; i++) {
|
||||
failureNode.appendChild(failures[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function failureDom(result) {
|
||||
const failure = createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-spec-detail jasmine-failed' },
|
||||
failureDescription(result, stateBuilder.currentParent),
|
||||
createDom('div', { className: 'jasmine-messages' })
|
||||
);
|
||||
const messages = failure.childNodes[1];
|
||||
|
||||
for (let i = 0; i < result.failedExpectations.length; i++) {
|
||||
const expectation = result.failedExpectations[i];
|
||||
messages.appendChild(
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-result-message' },
|
||||
expectation.message
|
||||
)
|
||||
);
|
||||
messages.appendChild(
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-stack-trace' },
|
||||
expectation.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (result.failedExpectations.length === 0) {
|
||||
messages.appendChild(
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-result-message' },
|
||||
'Spec has no expectations'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (result.debugLogs) {
|
||||
messages.appendChild(debugLogTable(result.debugLogs));
|
||||
}
|
||||
|
||||
return failure;
|
||||
}
|
||||
|
||||
function debugLogTable(debugLogs) {
|
||||
const tbody = createDom('tbody');
|
||||
|
||||
debugLogs.forEach(function(entry) {
|
||||
tbody.appendChild(
|
||||
createDom(
|
||||
'tr',
|
||||
{},
|
||||
createDom('td', {}, entry.timestamp.toString()),
|
||||
createDom('td', {}, entry.message)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-debug-log' },
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-debug-log-header' },
|
||||
'Debug logs'
|
||||
),
|
||||
createDom(
|
||||
'table',
|
||||
{},
|
||||
createDom(
|
||||
'thead',
|
||||
{},
|
||||
createDom(
|
||||
'tr',
|
||||
{},
|
||||
createDom('th', {}, 'Time (ms)'),
|
||||
createDom('th', {}, 'Message')
|
||||
)
|
||||
),
|
||||
tbody
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function summaryList(resultsTree, domParent) {
|
||||
let specListNode;
|
||||
for (let i = 0; i < resultsTree.children.length; i++) {
|
||||
const resultNode = resultsTree.children[i];
|
||||
if (filterSpecs && !hasActiveSpec(resultNode)) {
|
||||
continue;
|
||||
}
|
||||
if (resultNode.type === 'suite') {
|
||||
const suiteListNode = createDom(
|
||||
'ul',
|
||||
{ className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
|
||||
createDom(
|
||||
'li',
|
||||
{
|
||||
className:
|
||||
'jasmine-suite-detail jasmine-' + resultNode.result.status
|
||||
},
|
||||
createDom(
|
||||
'a',
|
||||
{ href: specHref(resultNode.result) },
|
||||
resultNode.result.description
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
summaryList(resultNode, suiteListNode);
|
||||
domParent.appendChild(suiteListNode);
|
||||
}
|
||||
if (resultNode.type === 'spec') {
|
||||
if (domParent.getAttribute('class') !== 'jasmine-specs') {
|
||||
specListNode = createDom('ul', { className: 'jasmine-specs' });
|
||||
domParent.appendChild(specListNode);
|
||||
}
|
||||
let specDescription = resultNode.result.description;
|
||||
if (noExpectations(resultNode.result)) {
|
||||
specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
|
||||
}
|
||||
if (
|
||||
resultNode.result.status === 'pending' &&
|
||||
resultNode.result.pendingReason !== ''
|
||||
) {
|
||||
specDescription =
|
||||
specDescription +
|
||||
' PENDING WITH MESSAGE: ' +
|
||||
resultNode.result.pendingReason;
|
||||
}
|
||||
specListNode.appendChild(
|
||||
createDom(
|
||||
'li',
|
||||
{
|
||||
className: 'jasmine-' + resultNode.result.status,
|
||||
id: 'spec-' + resultNode.result.id
|
||||
},
|
||||
createDom(
|
||||
'a',
|
||||
{ href: specHref(resultNode.result) },
|
||||
specDescription
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function optionsMenu(config) {
|
||||
const optionsMenuDom = createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-run-options' },
|
||||
createDom('span', { className: 'jasmine-trigger' }, 'Options'),
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-payload' },
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-stop-on-failure' },
|
||||
createDom('input', {
|
||||
className: 'jasmine-fail-fast',
|
||||
id: 'jasmine-fail-fast',
|
||||
type: 'checkbox'
|
||||
}),
|
||||
createDom(
|
||||
'label',
|
||||
{ className: 'jasmine-label', for: 'jasmine-fail-fast' },
|
||||
'stop execution on spec failure'
|
||||
)
|
||||
),
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-throw-failures' },
|
||||
createDom('input', {
|
||||
className: 'jasmine-throw',
|
||||
id: 'jasmine-throw-failures',
|
||||
type: 'checkbox'
|
||||
}),
|
||||
createDom(
|
||||
'label',
|
||||
{ className: 'jasmine-label', for: 'jasmine-throw-failures' },
|
||||
'stop spec on expectation failure'
|
||||
)
|
||||
),
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-random-order' },
|
||||
createDom('input', {
|
||||
className: 'jasmine-random',
|
||||
id: 'jasmine-random-order',
|
||||
type: 'checkbox'
|
||||
}),
|
||||
createDom(
|
||||
'label',
|
||||
{ className: 'jasmine-label', for: 'jasmine-random-order' },
|
||||
'run tests in random order'
|
||||
)
|
||||
),
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-hide-disabled' },
|
||||
createDom('input', {
|
||||
className: 'jasmine-disabled',
|
||||
id: 'jasmine-hide-disabled',
|
||||
type: 'checkbox'
|
||||
}),
|
||||
createDom(
|
||||
'label',
|
||||
{ className: 'jasmine-label', for: 'jasmine-hide-disabled' },
|
||||
'hide disabled tests'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const failFastCheckbox = optionsMenuDom.querySelector(
|
||||
'#jasmine-fail-fast'
|
||||
);
|
||||
failFastCheckbox.checked = config.stopOnSpecFailure;
|
||||
failFastCheckbox.onclick = function() {
|
||||
navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure);
|
||||
};
|
||||
|
||||
const throwCheckbox = optionsMenuDom.querySelector(
|
||||
'#jasmine-throw-failures'
|
||||
);
|
||||
throwCheckbox.checked = config.stopSpecOnExpectationFailure;
|
||||
throwCheckbox.onclick = function() {
|
||||
navigateWithNewParam(
|
||||
'stopSpecOnExpectationFailure',
|
||||
!config.stopSpecOnExpectationFailure
|
||||
);
|
||||
};
|
||||
|
||||
const randomCheckbox = optionsMenuDom.querySelector(
|
||||
'#jasmine-random-order'
|
||||
);
|
||||
randomCheckbox.checked = config.random;
|
||||
randomCheckbox.onclick = function() {
|
||||
navigateWithNewParam('random', !config.random);
|
||||
};
|
||||
|
||||
const hideDisabled = optionsMenuDom.querySelector(
|
||||
'#jasmine-hide-disabled'
|
||||
);
|
||||
hideDisabled.checked = config.hideDisabled;
|
||||
hideDisabled.onclick = function() {
|
||||
navigateWithNewParam('hideDisabled', !config.hideDisabled);
|
||||
};
|
||||
|
||||
const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'),
|
||||
optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'),
|
||||
isOpen = /\bjasmine-open\b/;
|
||||
|
||||
optionsTrigger.onclick = function() {
|
||||
if (isOpen.test(optionsPayload.className)) {
|
||||
optionsPayload.className = optionsPayload.className.replace(
|
||||
isOpen,
|
||||
''
|
||||
);
|
||||
} else {
|
||||
optionsPayload.className += ' jasmine-open';
|
||||
}
|
||||
};
|
||||
|
||||
return optionsMenuDom;
|
||||
}
|
||||
|
||||
function failureDescription(result, suite) {
|
||||
const wrapper = createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-description' },
|
||||
createDom(
|
||||
'a',
|
||||
{ title: result.description, href: specHref(result) },
|
||||
result.description
|
||||
)
|
||||
);
|
||||
let suiteLink;
|
||||
|
||||
while (suite && suite.parent) {
|
||||
wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild);
|
||||
suiteLink = createDom(
|
||||
'a',
|
||||
{ href: suiteHref(suite) },
|
||||
suite.result.description
|
||||
);
|
||||
wrapper.insertBefore(suiteLink, wrapper.firstChild);
|
||||
|
||||
suite = suite.parent;
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function suiteHref(suite) {
|
||||
const els = [];
|
||||
|
||||
while (suite && suite.parent) {
|
||||
els.unshift(suite.result.description);
|
||||
suite = suite.parent;
|
||||
}
|
||||
|
||||
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
|
||||
return (
|
||||
(window.location.pathname || '') +
|
||||
addToExistingQueryString('spec', els.join(' '))
|
||||
);
|
||||
}
|
||||
|
||||
function addDeprecationWarnings(result, runnableType) {
|
||||
if (result && result.deprecationWarnings) {
|
||||
for (let i = 0; i < result.deprecationWarnings.length; i++) {
|
||||
const warning = result.deprecationWarnings[i].message;
|
||||
deprecationWarnings.push({
|
||||
message: warning,
|
||||
stack: result.deprecationWarnings[i].stack,
|
||||
runnableName: result.fullName,
|
||||
runnableType: runnableType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createExpander(stackTrace) {
|
||||
const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
|
||||
const root = createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-expander' },
|
||||
expandLink,
|
||||
createDom(
|
||||
'div',
|
||||
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
|
||||
stackTrace
|
||||
)
|
||||
);
|
||||
|
||||
expandLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (root.classList.contains('jasmine-expanded')) {
|
||||
root.classList.remove('jasmine-expanded');
|
||||
expandLink.textContent = 'Show stack trace';
|
||||
} else {
|
||||
root.classList.add('jasmine-expanded');
|
||||
expandLink.textContent = 'Hide stack trace';
|
||||
}
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function find(selector) {
|
||||
return getContainer().querySelector('.jasmine_html-reporter ' + selector);
|
||||
}
|
||||
|
||||
function clearPrior() {
|
||||
const oldReporter = find('');
|
||||
|
||||
if (oldReporter) {
|
||||
getContainer().removeChild(oldReporter);
|
||||
}
|
||||
}
|
||||
|
||||
function createDom(type, attrs, childrenArrayOrVarArgs) {
|
||||
const el = createElement(type);
|
||||
let children;
|
||||
|
||||
if (j$.isArray_(childrenArrayOrVarArgs)) {
|
||||
children = childrenArrayOrVarArgs;
|
||||
} else {
|
||||
children = [];
|
||||
|
||||
for (let i = 2; i < arguments.length; i++) {
|
||||
children.push(arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
|
||||
if (typeof child === 'string') {
|
||||
el.appendChild(createTextNode(child));
|
||||
} else {
|
||||
if (child) {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr in attrs) {
|
||||
if (attr == 'className') {
|
||||
el[attr] = attrs[attr];
|
||||
} else {
|
||||
el.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function pluralize(singular, count) {
|
||||
const word = count == 1 ? singular : singular + 's';
|
||||
|
||||
return '' + count + ' ' + word;
|
||||
}
|
||||
|
||||
function specHref(result) {
|
||||
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
|
||||
return (
|
||||
(window.location.pathname || '') +
|
||||
addToExistingQueryString('spec', result.fullName)
|
||||
);
|
||||
}
|
||||
|
||||
function seedHref(seed) {
|
||||
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
|
||||
return (
|
||||
(window.location.pathname || '') +
|
||||
addToExistingQueryString('seed', seed)
|
||||
);
|
||||
}
|
||||
|
||||
function defaultQueryString(key, value) {
|
||||
return '?' + key + '=' + value;
|
||||
}
|
||||
|
||||
function setMenuModeTo(mode) {
|
||||
htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
|
||||
}
|
||||
|
||||
function noExpectations(result) {
|
||||
const allExpectations =
|
||||
result.failedExpectations.length + result.passedExpectations.length;
|
||||
|
||||
return (
|
||||
allExpectations === 0 &&
|
||||
(result.status === 'passed' || result.status === 'failed')
|
||||
);
|
||||
}
|
||||
|
||||
function hasActiveSpec(resultNode) {
|
||||
if (resultNode.type == 'spec' && resultNode.result.status != 'excluded') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resultNode.type == 'suite') {
|
||||
for (let i = 0, j = resultNode.children.length; i < j; i++) {
|
||||
if (hasActiveSpec(resultNode.children[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return HtmlReporter;
|
||||
};
|
||||
|
||||
jasmineRequire.HtmlSpecFilter = function() {
|
||||
function HtmlSpecFilter(options) {
|
||||
const filterString =
|
||||
options &&
|
||||
options.filterString() &&
|
||||
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||
const filterPattern = new RegExp(filterString);
|
||||
|
||||
this.matches = function(specName) {
|
||||
return filterPattern.test(specName);
|
||||
};
|
||||
}
|
||||
|
||||
return HtmlSpecFilter;
|
||||
};
|
||||
|
||||
jasmineRequire.ResultsNode = function() {
|
||||
function ResultsNode(result, type, parent) {
|
||||
this.result = result;
|
||||
this.type = type;
|
||||
this.parent = parent;
|
||||
|
||||
this.children = [];
|
||||
|
||||
this.addChild = function(result, type) {
|
||||
this.children.push(new ResultsNode(result, type, this));
|
||||
};
|
||||
|
||||
this.last = function() {
|
||||
return this.children[this.children.length - 1];
|
||||
};
|
||||
|
||||
this.updateResult = function(result) {
|
||||
this.result = result;
|
||||
};
|
||||
}
|
||||
|
||||
return ResultsNode;
|
||||
};
|
||||
|
||||
jasmineRequire.QueryString = function() {
|
||||
function QueryString(options) {
|
||||
this.navigateWithNewParam = function(key, value) {
|
||||
options.getWindowLocation().search = this.fullStringWithNewParam(
|
||||
key,
|
||||
value
|
||||
);
|
||||
};
|
||||
|
||||
this.fullStringWithNewParam = function(key, value) {
|
||||
const paramMap = queryStringToParamMap();
|
||||
paramMap[key] = value;
|
||||
return toQueryString(paramMap);
|
||||
};
|
||||
|
||||
this.getParam = function(key) {
|
||||
return queryStringToParamMap()[key];
|
||||
};
|
||||
|
||||
return this;
|
||||
|
||||
function toQueryString(paramMap) {
|
||||
const qStrPairs = [];
|
||||
for (const prop in paramMap) {
|
||||
qStrPairs.push(
|
||||
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
|
||||
);
|
||||
}
|
||||
return '?' + qStrPairs.join('&');
|
||||
}
|
||||
|
||||
function queryStringToParamMap() {
|
||||
const paramStr = options.getWindowLocation().search.substring(1);
|
||||
let params = [];
|
||||
const paramMap = {};
|
||||
|
||||
if (paramStr.length > 0) {
|
||||
params = paramStr.split('&');
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const p = params[i].split('=');
|
||||
let value = decodeURIComponent(p[1]);
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = JSON.parse(value);
|
||||
}
|
||||
paramMap[decodeURIComponent(p[0])] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
}
|
||||
|
||||
return QueryString;
|
||||
};
|
301
ui/plugins/ui/jasmine/jasmine.css
Normal file
301
ui/plugins/ui/jasmine/jasmine.css
Normal file
File diff suppressed because one or more lines are too long
10468
ui/plugins/ui/jasmine/jasmine.js
Normal file
10468
ui/plugins/ui/jasmine/jasmine.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
ui/plugins/ui/jasmine/jasmine_favicon.png
Normal file
BIN
ui/plugins/ui/jasmine/jasmine_favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
412
ui/plugins/ui/jasmineSpec.js
Normal file
412
ui/plugins/ui/jasmineSpec.js
Normal file
@ -0,0 +1,412 @@
|
||||
"use strict"
|
||||
|
||||
const JASMINE_SESSION_ID = `jasmine-${String(Date.now()).slice(8)}`
|
||||
|
||||
beforeEach(function () {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 60 * 1000 // Test timeout after 15 minutes
|
||||
jasmine.addMatchers({
|
||||
toBeOneOf: function () {
|
||||
return {
|
||||
compare: function (actual, expected) {
|
||||
return {
|
||||
pass: expected.includes(actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('stable-diffusion-ui', function() {
|
||||
beforeEach(function() {
|
||||
expect(typeof SD).toBe('object')
|
||||
expect(typeof SD.serverState).toBe('object')
|
||||
expect(typeof SD.serverState.status).toBe('string')
|
||||
})
|
||||
it('should be able to reach the backend', async function() {
|
||||
expect(SD.serverState.status).toBe(SD.ServerStates.unavailable)
|
||||
SD.sessionId = JASMINE_SESSION_ID
|
||||
await SD.init()
|
||||
expect(SD.isServerAvailable()).toBeTrue()
|
||||
})
|
||||
|
||||
it('enfore the current task state', function() {
|
||||
const task = new SD.Task()
|
||||
expect(task.status).toBe(SD.TaskStatus.init)
|
||||
expect(task.isPending).toBeTrue()
|
||||
|
||||
task._setStatus(SD.TaskStatus.pending)
|
||||
expect(task.status).toBe(SD.TaskStatus.pending)
|
||||
expect(task.isPending).toBeTrue()
|
||||
expect(function() {
|
||||
task._setStatus(SD.TaskStatus.init)
|
||||
}).toThrowError()
|
||||
|
||||
task._setStatus(SD.TaskStatus.waiting)
|
||||
expect(task.status).toBe(SD.TaskStatus.waiting)
|
||||
expect(task.isPending).toBeTrue()
|
||||
expect(function() {
|
||||
task._setStatus(SD.TaskStatus.pending)
|
||||
}).toThrowError()
|
||||
|
||||
task._setStatus(SD.TaskStatus.processing)
|
||||
expect(task.status).toBe(SD.TaskStatus.processing)
|
||||
expect(task.isPending).toBeTrue()
|
||||
expect(function() {
|
||||
task._setStatus(SD.TaskStatus.pending)
|
||||
}).toThrowError()
|
||||
|
||||
task._setStatus(SD.TaskStatus.failed)
|
||||
expect(task.status).toBe(SD.TaskStatus.failed)
|
||||
expect(task.isPending).toBeFalse()
|
||||
expect(function() {
|
||||
task._setStatus(SD.TaskStatus.processing)
|
||||
}).toThrowError()
|
||||
expect(function() {
|
||||
task._setStatus(SD.TaskStatus.completed)
|
||||
}).toThrowError()
|
||||
})
|
||||
it('should be able to run tasks', async function() {
|
||||
expect(typeof SD.Task.run).toBe('function')
|
||||
const promiseGenerator = (function*(val) {
|
||||
expect(val).toBe('start')
|
||||
expect(yield 1 + 1).toBe(4)
|
||||
expect(yield 2 + 2).toBe(8)
|
||||
yield asyncDelay(500)
|
||||
expect(yield 3 + 3).toBe(12)
|
||||
expect(yield 4 + 4).toBe(16)
|
||||
return 8 + 8
|
||||
})('start')
|
||||
const callback = function({value, done}) {
|
||||
return {value: 2 * value, done}
|
||||
}
|
||||
expect(await SD.Task.run(promiseGenerator, {callback})).toBe(32)
|
||||
})
|
||||
it('should be able to queue tasks', async function() {
|
||||
expect(typeof SD.Task.enqueue).toBe('function')
|
||||
const promiseGenerator = (function*(val) {
|
||||
expect(val).toBe('start')
|
||||
expect(yield 1 + 1).toBe(4)
|
||||
expect(yield 2 + 2).toBe(8)
|
||||
yield asyncDelay(500)
|
||||
expect(yield 3 + 3).toBe(12)
|
||||
expect(yield 4 + 4).toBe(16)
|
||||
return 8 + 8
|
||||
})('start')
|
||||
const callback = function({value, done}) {
|
||||
return {value: 2 * value, done}
|
||||
}
|
||||
const gen = SD.Task.asGenerator({generator: promiseGenerator, callback})
|
||||
expect(await SD.Task.enqueue(gen)).toBe(32)
|
||||
})
|
||||
it('should be able to chain handlers', async function() {
|
||||
expect(typeof SD.Task.enqueue).toBe('function')
|
||||
const promiseGenerator = (function*(val) {
|
||||
expect(val).toBe('start')
|
||||
expect(yield {test: '1'}).toEqual({test: '1', foo: 'bar'})
|
||||
expect(yield 2 + 2).toEqual(8)
|
||||
yield asyncDelay(500)
|
||||
expect(yield 3 + 3).toEqual(12)
|
||||
expect(yield {test: 4}).toEqual({test: 8, foo: 'bar'})
|
||||
return {test: 8}
|
||||
})('start')
|
||||
const gen1 = SD.Task.asGenerator({generator: promiseGenerator, callback: function({value, done}) {
|
||||
if (typeof value === "object") {
|
||||
value['foo'] = 'bar'
|
||||
}
|
||||
return {value, done}
|
||||
}})
|
||||
const gen2 = SD.Task.asGenerator({generator: gen1, callback: function({value, done}) {
|
||||
if (typeof value === 'number') {
|
||||
value = 2 * value
|
||||
}
|
||||
if (typeof value === 'object' && typeof value.test === 'number') {
|
||||
value.test = 2 * value.test
|
||||
}
|
||||
return {value, done}
|
||||
}})
|
||||
expect(await SD.Task.enqueue(gen2)).toEqual({test:32, foo: 'bar'})
|
||||
})
|
||||
describe('ServiceContainer', function() {
|
||||
it('should be able to register providers', function() {
|
||||
const cont = new ServiceContainer(
|
||||
function foo() {
|
||||
this.bar = ''
|
||||
},
|
||||
function bar() {
|
||||
return () => 0
|
||||
},
|
||||
{ name: 'zero', definition: 0 },
|
||||
{ name: 'ctx', definition: () => Object.create(null), singleton: true },
|
||||
{ name: 'test',
|
||||
definition: (ctx, missing, one, foo) => {
|
||||
expect(ctx).toEqual({ran: true})
|
||||
expect(one).toBe(1)
|
||||
expect(typeof foo).toBe('object')
|
||||
expect(foo.bar).toBeDefined()
|
||||
expect(typeof missing).toBe('undefined')
|
||||
return {foo: 'bar'}
|
||||
}, dependencies: ['ctx', 'missing', 'one', 'foo']
|
||||
}
|
||||
)
|
||||
const fooObj = cont.get('foo')
|
||||
expect(typeof fooObj).toBe('object')
|
||||
fooObj.ran = true
|
||||
|
||||
const ctx = cont.get('ctx')
|
||||
expect(ctx).toEqual({})
|
||||
ctx.ran = true
|
||||
|
||||
const bar = cont.get('bar')
|
||||
expect(typeof bar).toBe('function')
|
||||
expect(bar()).toBe(0)
|
||||
|
||||
cont.register({name: 'one', definition: 1})
|
||||
const test = cont.get('test')
|
||||
expect(typeof test).toBe('object')
|
||||
expect(test.foo).toBe('bar')
|
||||
})
|
||||
})
|
||||
it('should be able to stream data in chunks', async function() {
|
||||
expect(SD.isServerAvailable()).toBeTrue()
|
||||
const nbr_steps = 15
|
||||
let res = await fetch('/render', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"prompt": "a photograph of an astronaut riding a horse",
|
||||
"negative_prompt": "",
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"seed": Math.floor(Math.random() * 10000000),
|
||||
|
||||
"sampler": "plms",
|
||||
"use_stable_diffusion_model": "sd-v1-4",
|
||||
"num_inference_steps": nbr_steps,
|
||||
"guidance_scale": 7.5,
|
||||
|
||||
"numOutputsParallel": 1,
|
||||
"stream_image_progress": true,
|
||||
"show_only_filtered_image": true,
|
||||
"output_format": "jpeg",
|
||||
|
||||
"session_id": JASMINE_SESSION_ID,
|
||||
}),
|
||||
})
|
||||
expect(res.ok).toBeTruthy()
|
||||
const renderRequest = await res.json()
|
||||
expect(typeof renderRequest.stream).toBe('string')
|
||||
expect(renderRequest.task).toBeDefined()
|
||||
|
||||
// Wait for server status to update.
|
||||
await SD.waitUntil(() => {
|
||||
console.log('Waiting for %s to be received...', renderRequest.task)
|
||||
return (!SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)])
|
||||
}, 250, 10 * 60 * 1000)
|
||||
// Wait for task to start on server.
|
||||
await SD.waitUntil(() => {
|
||||
console.log('Waiting for %s to start...', renderRequest.task)
|
||||
return !SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)] !== 'pending'
|
||||
}, 250)
|
||||
|
||||
const reader = new SD.ChunkedStreamReader(renderRequest.stream)
|
||||
const parseToString = reader.parse
|
||||
reader.parse = function(value) {
|
||||
value = parseToString.call(this, value)
|
||||
if (!value || value.length <= 0) {
|
||||
return
|
||||
}
|
||||
return reader.readStreamAsJSON(value.join(''))
|
||||
}
|
||||
reader.onNext = function({done, value}) {
|
||||
console.log(value)
|
||||
if (typeof value === 'object' && 'status' in value) {
|
||||
done = true
|
||||
}
|
||||
return {done, value}
|
||||
}
|
||||
let lastUpdate = undefined
|
||||
let stepCount = 0
|
||||
let complete = false
|
||||
//for await (const stepUpdate of reader) {
|
||||
for await (const stepUpdate of reader.open()) {
|
||||
console.log('ChunkedStreamReader received ', stepUpdate)
|
||||
lastUpdate = stepUpdate
|
||||
if (complete) {
|
||||
expect(stepUpdate.status).toBe('succeeded')
|
||||
expect(stepUpdate.output).toHaveSize(1)
|
||||
} else {
|
||||
expect(stepUpdate.total_steps).toBe(nbr_steps)
|
||||
expect(stepUpdate.step).toBe(stepCount)
|
||||
if (stepUpdate.step === stepUpdate.total_steps) {
|
||||
complete = true
|
||||
} else {
|
||||
stepCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
for(let i=1; i <= 5; ++i) {
|
||||
res = await fetch(renderRequest.stream)
|
||||
expect(res.ok).toBeTruthy()
|
||||
const cachedResponse = await res.json()
|
||||
console.log('Cache test %s received %o', i, cachedResponse)
|
||||
expect(lastUpdate).toEqual(cachedResponse)
|
||||
}
|
||||
})
|
||||
|
||||
describe('should be able to make renders', function() {
|
||||
beforeEach(function() {
|
||||
expect(SD.isServerAvailable()).toBeTrue()
|
||||
})
|
||||
it('basic inline request', async function() {
|
||||
let stepCount = 0
|
||||
let complete = false
|
||||
const result = await SD.render({
|
||||
"prompt": "a photograph of an astronaut riding a horse",
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"num_inference_steps": 10,
|
||||
"show_only_filtered_image": false,
|
||||
//"use_face_correction": 'GFPGANv1.3',
|
||||
"use_upscale": "RealESRGAN_x4plus",
|
||||
"session_id": JASMINE_SESSION_ID,
|
||||
}, function(event) {
|
||||
console.log(this, event)
|
||||
if ('update' in event) {
|
||||
const stepUpdate = event.update
|
||||
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
|
||||
expect(stepUpdate.status).toBe('succeeded')
|
||||
expect(stepUpdate.output).toHaveSize(2)
|
||||
} else {
|
||||
expect(stepUpdate.step).toBe(stepCount)
|
||||
if (stepUpdate.step === stepUpdate.total_steps) {
|
||||
complete = true
|
||||
} else {
|
||||
stepCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(result)
|
||||
expect(result.status).toBe('succeeded')
|
||||
expect(result.output).toHaveSize(2)
|
||||
})
|
||||
it('post and reader request', async function() {
|
||||
const renderTask = new SD.RenderTask({
|
||||
"prompt": "a photograph of an astronaut riding a horse",
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"seed": SD.MAX_SEED_VALUE,
|
||||
"num_inference_steps": 10,
|
||||
"session_id": JASMINE_SESSION_ID,
|
||||
})
|
||||
expect(renderTask.status).toBe(SD.TaskStatus.init)
|
||||
|
||||
const timeout = -1
|
||||
const renderRequest = await renderTask.post(timeout)
|
||||
expect(typeof renderRequest.stream).toBe('string')
|
||||
expect(renderTask.status).toBe(SD.TaskStatus.waiting)
|
||||
expect(renderTask.streamUrl).toBe(renderRequest.stream)
|
||||
|
||||
await renderTask.waitUntil({state: SD.TaskStatus.processing, callback: () => console.log('Waiting for render task to start...') })
|
||||
expect(renderTask.status).toBe(SD.TaskStatus.processing)
|
||||
|
||||
let stepCount = 0
|
||||
let complete = false
|
||||
//for await (const stepUpdate of renderTask.reader) {
|
||||
for await (const stepUpdate of renderTask.reader.open()) {
|
||||
console.log(stepUpdate)
|
||||
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
|
||||
expect(stepUpdate.status).toBe('succeeded')
|
||||
expect(stepUpdate.output).toHaveSize(1)
|
||||
} else {
|
||||
expect(stepUpdate.step).toBe(stepCount)
|
||||
if (stepUpdate.step === stepUpdate.total_steps) {
|
||||
complete = true
|
||||
} else {
|
||||
stepCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(renderTask.status).toBe(SD.TaskStatus.completed)
|
||||
expect(renderTask.result.status).toBe('succeeded')
|
||||
expect(renderTask.result.output).toHaveSize(1)
|
||||
})
|
||||
it('queued request', async function() {
|
||||
let stepCount = 0
|
||||
let complete = false
|
||||
const renderTask = new SD.RenderTask({
|
||||
"prompt": "a photograph of an astronaut riding a horse",
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"num_inference_steps": 10,
|
||||
"show_only_filtered_image": false,
|
||||
//"use_face_correction": 'GFPGANv1.3',
|
||||
"use_upscale": "RealESRGAN_x4plus",
|
||||
"session_id": JASMINE_SESSION_ID,
|
||||
})
|
||||
await renderTask.enqueue(function(event) {
|
||||
console.log(this, event)
|
||||
if ('update' in event) {
|
||||
const stepUpdate = event.update
|
||||
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
|
||||
expect(stepUpdate.status).toBe('succeeded')
|
||||
expect(stepUpdate.output).toHaveSize(2)
|
||||
} else {
|
||||
expect(stepUpdate.step).toBe(stepCount)
|
||||
if (stepUpdate.step === stepUpdate.total_steps) {
|
||||
complete = true
|
||||
} else {
|
||||
stepCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(renderTask.result)
|
||||
expect(renderTask.result.status).toBe('succeeded')
|
||||
expect(renderTask.result.output).toHaveSize(2)
|
||||
})
|
||||
})
|
||||
describe('# Special cases', function() {
|
||||
it('should throw an exception on set for invalid sessionId', function() {
|
||||
expect(function() {
|
||||
SD.sessionId = undefined
|
||||
}).toThrowError("Can't set sessionId to undefined.")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const loadCompleted = window.onload
|
||||
let loadEvent = undefined
|
||||
window.onload = function(evt) {
|
||||
loadEvent = evt
|
||||
}
|
||||
if (!PLUGINS.SELFTEST) {
|
||||
PLUGINS.SELFTEST = {}
|
||||
}
|
||||
loadUIPlugins().then(function() {
|
||||
console.log('loadCompleted', loadEvent)
|
||||
describe('@Plugins', function() {
|
||||
it('exposes hooks to overide', function() {
|
||||
expect(typeof PLUGINS.IMAGE_INFO_BUTTONS).toBe('object')
|
||||
expect(typeof PLUGINS.TASK_CREATE).toBe('object')
|
||||
})
|
||||
describe('supports selftests', function() { // Hook to allow plugins to define tests.
|
||||
const pluginsTests = Object.keys(PLUGINS.SELFTEST).filter((key) => PLUGINS.SELFTEST.hasOwnProperty(key))
|
||||
if (!pluginsTests || pluginsTests.length <= 0) {
|
||||
it('but nothing loaded...', function() {
|
||||
expect(true).toBeTruthy()
|
||||
})
|
||||
return
|
||||
}
|
||||
for (const pTest of pluginsTests) {
|
||||
describe(pTest, function() {
|
||||
const testFn = PLUGINS.SELFTEST[pTest]
|
||||
return Promise.resolve(testFn.call(jasmine, pTest))
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
loadCompleted.call(window, loadEvent)
|
||||
})
|
@ -1,11 +1,21 @@
|
||||
(function() {
|
||||
document.querySelector('#tab-container').insertAdjacentHTML('beforeend', `
|
||||
// Register selftests when loaded by jasmine.
|
||||
if (typeof PLUGINS?.SELFTEST === 'object') {
|
||||
PLUGINS.SELFTEST["release-notes"] = function() {
|
||||
it('should be able to fetch CHANGES.md', async function() {
|
||||
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/main/CHANGES.md`)
|
||||
expect(releaseNotes.status).toBe(200)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('#tab-container')?.insertAdjacentHTML('beforeend', `
|
||||
<span id="tab-news" class="tab">
|
||||
<span><i class="fa fa-bolt icon"></i> What's new?</span>
|
||||
</span>
|
||||
`)
|
||||
|
||||
document.querySelector('#tab-content-wrapper').insertAdjacentHTML('beforeend', `
|
||||
document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', `
|
||||
<div id="tab-content-news" class="tab-content">
|
||||
<div id="news" class="tab-content-inner">
|
||||
Loading..
|
||||
@ -13,6 +23,16 @@
|
||||
</div>
|
||||
`)
|
||||
|
||||
const tabNews = document.querySelector('#tab-news')
|
||||
if (tabNews) {
|
||||
linkTabContents(tabNews)
|
||||
}
|
||||
const news = document.querySelector('#news')
|
||||
if (!news) {
|
||||
// news tab not found, dont exec plugin code.
|
||||
return
|
||||
}
|
||||
|
||||
document.querySelector('body').insertAdjacentHTML('beforeend', `
|
||||
<style>
|
||||
#tab-content-news .tab-content-inner {
|
||||
@ -23,9 +43,7 @@
|
||||
</style>
|
||||
`)
|
||||
|
||||
linkTabContents(document.querySelector('#tab-news'))
|
||||
|
||||
let markedScript = document.createElement('script')
|
||||
const markedScript = document.createElement('script')
|
||||
markedScript.src = '/media/js/marked.min.js'
|
||||
|
||||
markedScript.onload = async function() {
|
||||
@ -34,7 +52,6 @@
|
||||
|
||||
let updateBranch = appConfig.update_branch || 'main'
|
||||
|
||||
let news = document.querySelector('#news')
|
||||
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`)
|
||||
if (releaseNotes.status != 200) {
|
||||
return
|
||||
|
25
ui/plugins/ui/selftest.plugin.js
Normal file
25
ui/plugins/ui/selftest.plugin.js
Normal file
@ -0,0 +1,25 @@
|
||||
/* SD-UI Selftest Plugin.js
|
||||
*/
|
||||
(function() { "use strict"
|
||||
const ID_PREFIX = "selftest-plugin"
|
||||
|
||||
const links = document.getElementById("community-links")
|
||||
if (!links) {
|
||||
console.error('%s the ID "community-links" cannot be found.', ID_PREFIX)
|
||||
return
|
||||
}
|
||||
|
||||
// Add link to Jasmine SpecRunner
|
||||
const pluginLink = document.createElement('li')
|
||||
const options = {
|
||||
'stopSpecOnExpectationFailure': "true",
|
||||
'stopOnSpecFailure': 'false',
|
||||
'random': 'false',
|
||||
'hideDisabled': 'false'
|
||||
}
|
||||
const optStr = Object.entries(options).map(([key, val]) => `${key}=${val}`).join('&')
|
||||
pluginLink.innerHTML = `<a id="${ID_PREFIX}-starttest" href="${location.protocol}/plugins/core/SpecRunner.html?${optStr}" target="_blank"><i class="fa-solid fa-vial-circle-check"></i> Start SelfTest</a>`
|
||||
links.appendChild(pluginLink)
|
||||
|
||||
console.log('%s loaded!', ID_PREFIX)
|
||||
})()
|
@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
class Request:
|
||||
request_id: str = None
|
||||
session_id: str = "session"
|
||||
prompt: str = ""
|
||||
negative_prompt: str = ""
|
||||
@ -23,6 +24,8 @@ class Request:
|
||||
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
|
||||
use_stable_diffusion_model: str = "sd-v1-4"
|
||||
use_vae_model: str = None
|
||||
use_hypernetwork_model: str = None
|
||||
hypernetwork_strength: float = 1
|
||||
show_only_filtered_image: bool = False
|
||||
output_format: str = "jpeg" # or "png"
|
||||
output_quality: int = 75
|
||||
@ -38,6 +41,7 @@ class Request:
|
||||
"num_outputs": self.num_outputs,
|
||||
"num_inference_steps": self.num_inference_steps,
|
||||
"guidance_scale": self.guidance_scale,
|
||||
"hypernetwork_strengtgh": self.guidance_scale,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"seed": self.seed,
|
||||
@ -47,6 +51,8 @@ class Request:
|
||||
"use_upscale": self.use_upscale,
|
||||
"use_stable_diffusion_model": self.use_stable_diffusion_model,
|
||||
"use_vae_model": self.use_vae_model,
|
||||
"use_hypernetwork_model": self.use_hypernetwork_model,
|
||||
"hypernetwork_strength": self.hypernetwork_strength,
|
||||
"output_format": self.output_format,
|
||||
"output_quality": self.output_quality,
|
||||
}
|
||||
@ -70,6 +76,8 @@ class Request:
|
||||
use_upscale: {self.use_upscale}
|
||||
use_stable_diffusion_model: {self.use_stable_diffusion_model}
|
||||
use_vae_model: {self.use_vae_model}
|
||||
use_hypernetwork_model: {self.use_hypernetwork_model}
|
||||
hypernetwork_strength: {self.hypernetwork_strength}
|
||||
show_only_filtered_image: {self.show_only_filtered_image}
|
||||
output_format: {self.output_format}
|
||||
output_quality: {self.output_quality}
|
||||
|
198
ui/sd_internal/hypernetwork.py
Normal file
198
ui/sd_internal/hypernetwork.py
Normal file
@ -0,0 +1,198 @@
|
||||
# this is basically a cut down version of https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/c9a2cfdf2a53d37c2de1908423e4f548088667ef/modules/hypernetworks/hypernetwork.py, mostly for feature parity
|
||||
# I, c0bra5, don't really understand how deep learning works. I just know how to port stuff.
|
||||
|
||||
import inspect
|
||||
import torch
|
||||
import optimizedSD.splitAttention
|
||||
from . import runtime
|
||||
from einops import rearrange
|
||||
|
||||
optimizer_dict = {optim_name : cls_obj for optim_name, cls_obj in inspect.getmembers(torch.optim, inspect.isclass) if optim_name != "Optimizer"}
|
||||
|
||||
loaded_hypernetwork = None
|
||||
|
||||
class HypernetworkModule(torch.nn.Module):
|
||||
multiplier = 0.5
|
||||
activation_dict = {
|
||||
"linear": torch.nn.Identity,
|
||||
"relu": torch.nn.ReLU,
|
||||
"leakyrelu": torch.nn.LeakyReLU,
|
||||
"elu": torch.nn.ELU,
|
||||
"swish": torch.nn.Hardswish,
|
||||
"tanh": torch.nn.Tanh,
|
||||
"sigmoid": torch.nn.Sigmoid,
|
||||
}
|
||||
activation_dict.update({cls_name.lower(): cls_obj for cls_name, cls_obj in inspect.getmembers(torch.nn.modules.activation) if inspect.isclass(cls_obj) and cls_obj.__module__ == 'torch.nn.modules.activation'})
|
||||
|
||||
def __init__(self, dim, state_dict=None, layer_structure=None, activation_func=None, weight_init='Normal',
|
||||
add_layer_norm=False, use_dropout=False, activate_output=False, last_layer_dropout=False):
|
||||
super().__init__()
|
||||
|
||||
assert layer_structure is not None, "layer_structure must not be None"
|
||||
assert layer_structure[0] == 1, "Multiplier Sequence should start with size 1!"
|
||||
assert layer_structure[-1] == 1, "Multiplier Sequence should end with size 1!"
|
||||
|
||||
linears = []
|
||||
for i in range(len(layer_structure) - 1):
|
||||
|
||||
# Add a fully-connected layer
|
||||
linears.append(torch.nn.Linear(int(dim * layer_structure[i]), int(dim * layer_structure[i+1])))
|
||||
|
||||
# Add an activation func except last layer
|
||||
if activation_func == "linear" or activation_func is None or (i >= len(layer_structure) - 2 and not activate_output):
|
||||
pass
|
||||
elif activation_func in self.activation_dict:
|
||||
linears.append(self.activation_dict[activation_func]())
|
||||
else:
|
||||
raise RuntimeError(f'hypernetwork uses an unsupported activation function: {activation_func}')
|
||||
|
||||
# Add layer normalization
|
||||
if add_layer_norm:
|
||||
linears.append(torch.nn.LayerNorm(int(dim * layer_structure[i+1])))
|
||||
|
||||
# Add dropout except last layer
|
||||
if use_dropout and (i < len(layer_structure) - 3 or last_layer_dropout and i < len(layer_structure) - 2):
|
||||
linears.append(torch.nn.Dropout(p=0.3))
|
||||
|
||||
self.linear = torch.nn.Sequential(*linears)
|
||||
|
||||
self.fix_old_state_dict(state_dict)
|
||||
self.load_state_dict(state_dict)
|
||||
|
||||
self.to(runtime.thread_data.device)
|
||||
|
||||
def fix_old_state_dict(self, state_dict):
|
||||
changes = {
|
||||
'linear1.bias': 'linear.0.bias',
|
||||
'linear1.weight': 'linear.0.weight',
|
||||
'linear2.bias': 'linear.1.bias',
|
||||
'linear2.weight': 'linear.1.weight',
|
||||
}
|
||||
|
||||
for fr, to in changes.items():
|
||||
x = state_dict.get(fr, None)
|
||||
if x is None:
|
||||
continue
|
||||
|
||||
del state_dict[fr]
|
||||
state_dict[to] = x
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
return x + self.linear(x) * runtime.thread_data.hypernetwork_strength
|
||||
|
||||
def apply_hypernetwork(hypernetwork, context, layer=None):
|
||||
hypernetwork_layers = hypernetwork.get(context.shape[2], None)
|
||||
|
||||
if hypernetwork_layers is None:
|
||||
return context, context
|
||||
|
||||
if layer is not None:
|
||||
layer.hyper_k = hypernetwork_layers[0]
|
||||
layer.hyper_v = hypernetwork_layers[1]
|
||||
|
||||
context_k = hypernetwork_layers[0](context)
|
||||
context_v = hypernetwork_layers[1](context)
|
||||
return context_k, context_v
|
||||
|
||||
def get_kv(context, hypernetwork):
|
||||
if hypernetwork is None:
|
||||
return context, context
|
||||
else:
|
||||
return apply_hypernetwork(runtime.thread_data.hypernetwork, context)
|
||||
|
||||
# This might need updating as the optimisedSD code changes
|
||||
# I think yall have a system for this (patch files in sd_internal) but idk how it works and no amount of searching gave me any clue
|
||||
# just in case for attribution https://github.com/easydiffusion/diffusion-kit/blob/e8ea0cadd543056059cd951e76d4744de76327d2/optimizedSD/splitAttention.py#L171
|
||||
def new_cross_attention_forward(self, x, context=None, mask=None):
|
||||
h = self.heads
|
||||
|
||||
q = self.to_q(x)
|
||||
# default context
|
||||
context = context if context is not None else x() if inspect.isfunction(x) else x
|
||||
# hypernetwork!
|
||||
context_k, context_v = get_kv(context, runtime.thread_data.hypernetwork)
|
||||
k = self.to_k(context_k)
|
||||
v = self.to_v(context_v)
|
||||
del context, x
|
||||
|
||||
q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> (b h) n d', h=h), (q, k, v))
|
||||
|
||||
|
||||
limit = k.shape[0]
|
||||
att_step = self.att_step
|
||||
q_chunks = list(torch.tensor_split(q, limit//att_step, dim=0))
|
||||
k_chunks = list(torch.tensor_split(k, limit//att_step, dim=0))
|
||||
v_chunks = list(torch.tensor_split(v, limit//att_step, dim=0))
|
||||
|
||||
q_chunks.reverse()
|
||||
k_chunks.reverse()
|
||||
v_chunks.reverse()
|
||||
sim = torch.zeros(q.shape[0], q.shape[1], v.shape[2], device=q.device)
|
||||
del k, q, v
|
||||
for i in range (0, limit, att_step):
|
||||
|
||||
q_buffer = q_chunks.pop()
|
||||
k_buffer = k_chunks.pop()
|
||||
v_buffer = v_chunks.pop()
|
||||
sim_buffer = torch.einsum('b i d, b j d -> b i j', q_buffer, k_buffer) * self.scale
|
||||
|
||||
del k_buffer, q_buffer
|
||||
# attention, what we cannot get enough of, by chunks
|
||||
|
||||
sim_buffer = sim_buffer.softmax(dim=-1)
|
||||
|
||||
sim_buffer = torch.einsum('b i j, b j d -> b i d', sim_buffer, v_buffer)
|
||||
del v_buffer
|
||||
sim[i:i+att_step,:,:] = sim_buffer
|
||||
|
||||
del sim_buffer
|
||||
sim = rearrange(sim, '(b h) n d -> b n (h d)', h=h)
|
||||
return self.to_out(sim)
|
||||
|
||||
|
||||
def load_hypernetwork(path: str):
|
||||
|
||||
state_dict = torch.load(path, map_location='cpu')
|
||||
|
||||
layer_structure = state_dict.get('layer_structure', [1, 2, 1])
|
||||
activation_func = state_dict.get('activation_func', None)
|
||||
weight_init = state_dict.get('weight_initialization', 'Normal')
|
||||
add_layer_norm = state_dict.get('is_layer_norm', False)
|
||||
use_dropout = state_dict.get('use_dropout', False)
|
||||
activate_output = state_dict.get('activate_output', True)
|
||||
last_layer_dropout = state_dict.get('last_layer_dropout', False)
|
||||
# this is a bit verbose so leaving it commented out for the poor soul who ever has to debug this
|
||||
# print(f"layer_structure: {layer_structure}")
|
||||
# print(f"activation_func: {activation_func}")
|
||||
# print(f"weight_init: {weight_init}")
|
||||
# print(f"add_layer_norm: {add_layer_norm}")
|
||||
# print(f"use_dropout: {use_dropout}")
|
||||
# print(f"activate_output: {activate_output}")
|
||||
# print(f"last_layer_dropout: {last_layer_dropout}")
|
||||
|
||||
layers = {}
|
||||
for size, sd in state_dict.items():
|
||||
if type(size) == int:
|
||||
layers[size] = (
|
||||
HypernetworkModule(size, sd[0], layer_structure, activation_func, weight_init, add_layer_norm,
|
||||
use_dropout, activate_output, last_layer_dropout=last_layer_dropout),
|
||||
HypernetworkModule(size, sd[1], layer_structure, activation_func, weight_init, add_layer_norm,
|
||||
use_dropout, activate_output, last_layer_dropout=last_layer_dropout),
|
||||
)
|
||||
print(f"hypernetwork loaded")
|
||||
return layers
|
||||
|
||||
|
||||
|
||||
# overriding of original function
|
||||
old_cross_attention_forward = optimizedSD.splitAttention.CrossAttention.forward
|
||||
# hijacks the cross attention forward function to add hyper network support
|
||||
def hijack_cross_attention():
|
||||
print("hypernetwork functionality added to cross attention")
|
||||
optimizedSD.splitAttention.CrossAttention.forward = new_cross_attention_forward
|
||||
# there was a cop on board
|
||||
def unhijack_cross_attention_forward():
|
||||
print("hypernetwork functionality removed from cross attention")
|
||||
optimizedSD.splitAttention.CrossAttention.forward = old_cross_attention_forward
|
||||
|
||||
hijack_cross_attention()
|
@ -28,6 +28,8 @@ from gfpgan import GFPGANer
|
||||
from basicsr.archs.rrdbnet_arch import RRDBNet
|
||||
from realesrgan import RealESRGANer
|
||||
|
||||
from server import HYPERNETWORK_MODEL_EXTENSIONS# , STABLE_DIFFUSION_MODEL_EXTENSIONS, VAE_MODEL_EXTENSIONS
|
||||
|
||||
from threading import Lock
|
||||
from safetensors.torch import load_file
|
||||
|
||||
@ -57,12 +59,15 @@ def thread_init(device):
|
||||
|
||||
thread_data.ckpt_file = None
|
||||
thread_data.vae_file = None
|
||||
thread_data.hypernetwork_file = None
|
||||
thread_data.gfpgan_file = None
|
||||
thread_data.real_esrgan_file = None
|
||||
|
||||
thread_data.model = None
|
||||
thread_data.modelCS = None
|
||||
thread_data.modelFS = None
|
||||
thread_data.hypernetwork = None
|
||||
thread_data.hypernetwork_strength = 1
|
||||
thread_data.model_gfpgan = None
|
||||
thread_data.model_real_esrgan = None
|
||||
|
||||
@ -72,6 +77,8 @@ def thread_init(device):
|
||||
thread_data.device_name = None
|
||||
thread_data.unet_bs = 1
|
||||
thread_data.precision = 'autocast'
|
||||
thread_data.sampler_plms = None
|
||||
thread_data.sampler_ddim = None
|
||||
|
||||
thread_data.turbo = False
|
||||
thread_data.force_full_precision = False
|
||||
@ -121,7 +128,7 @@ def load_model_ckpt():
|
||||
load_model_ckpt_sd1()
|
||||
|
||||
def load_model_ckpt_sd1():
|
||||
sd = load_model_from_config(thread_data.ckpt_file)
|
||||
sd, model_ver = load_model_from_config(thread_data.ckpt_file)
|
||||
li, lo = [], []
|
||||
for key, value in sd.items():
|
||||
sp = key.split(".")
|
||||
@ -215,12 +222,12 @@ def load_model_ckpt_sd1():
|
||||
using precision: {thread_data.precision}''')
|
||||
|
||||
def load_model_ckpt_sd2():
|
||||
config_file = 'configs/stable-diffusion/v2-inference-v.yaml' if 'sd2_' in thread_data.ckpt_file else "configs/stable-diffusion/v1-inference.yaml"
|
||||
sd, model_ver = load_model_from_config(thread_data.ckpt_file)
|
||||
|
||||
config_file = 'configs/stable-diffusion/v2-inference-v.yaml' if model_ver == 'sd2' else "configs/stable-diffusion/v1-inference.yaml"
|
||||
config = OmegaConf.load(config_file)
|
||||
verbose = False
|
||||
|
||||
sd = load_model_from_config(thread_data.ckpt_file)
|
||||
|
||||
thread_data.model = instantiate_from_config(config.model)
|
||||
m, u = thread_data.model.load_state_dict(sd, strict=False)
|
||||
if len(m) > 0 and verbose:
|
||||
@ -433,6 +440,54 @@ def reload_model():
|
||||
unload_filters()
|
||||
load_model_ckpt()
|
||||
|
||||
def is_hypernetwork_reload_necessary(req: Request):
|
||||
needs_model_reload = False
|
||||
if thread_data.hypernetwork_file != req.use_hypernetwork_model:
|
||||
thread_data.hypernetwork_file = req.use_hypernetwork_model
|
||||
needs_model_reload = True
|
||||
|
||||
return needs_model_reload
|
||||
|
||||
def load_hypernetwork():
|
||||
if thread_data.test_sd2:
|
||||
# Not yet supported in SD2
|
||||
return
|
||||
|
||||
from . import hypernetwork
|
||||
if thread_data.hypernetwork_file is not None:
|
||||
try:
|
||||
loaded = False
|
||||
for model_extension in HYPERNETWORK_MODEL_EXTENSIONS:
|
||||
if os.path.exists(thread_data.hypernetwork_file + model_extension):
|
||||
print(f"Loading hypernetwork weights from: {thread_data.hypernetwork_file}{model_extension}")
|
||||
thread_data.hypernetwork = hypernetwork.load_hypernetwork(thread_data.hypernetwork_file + model_extension)
|
||||
loaded = True
|
||||
break
|
||||
|
||||
if not loaded:
|
||||
print(f'Cannot find hypernetwork: {thread_data.hypernetwork_file}')
|
||||
thread_data.hypernetwork_file = None
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
print(f'Could not load hypernetwork: {thread_data.hypernetwork_file}')
|
||||
thread_data.hypernetwork_file = None
|
||||
|
||||
def unload_hypernetwork():
|
||||
if thread_data.hypernetwork is not None:
|
||||
print('Unloading hypernetwork...')
|
||||
if thread_data.device != 'cpu':
|
||||
for i in thread_data.hypernetwork:
|
||||
thread_data.hypernetwork[i][0].to('cpu')
|
||||
thread_data.hypernetwork[i][1].to('cpu')
|
||||
del thread_data.hypernetwork
|
||||
thread_data.hypernetwork = None
|
||||
|
||||
gc()
|
||||
|
||||
def reload_hypernetwork():
|
||||
unload_hypernetwork()
|
||||
load_hypernetwork()
|
||||
|
||||
def mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, step_callback):
|
||||
try:
|
||||
return do_mk_img(req, data_queue, task_temp_images, step_callback)
|
||||
@ -468,15 +523,16 @@ def update_temp_img(req, x_samples, task_temp_images: list):
|
||||
del img, x_sample, x_sample_ddim
|
||||
# don't delete x_samples, it is used in the code that called this callback
|
||||
|
||||
thread_data.temp_images[str(req.session_id) + '/' + str(i)] = buf
|
||||
thread_data.temp_images[f'{req.request_id}/{i}'] = buf
|
||||
task_temp_images[i] = buf
|
||||
partial_images.append({'path': f'/image/tmp/{req.session_id}/{i}'})
|
||||
partial_images.append({'path': f'/image/tmp/{req.request_id}/{i}'})
|
||||
return partial_images
|
||||
|
||||
# Build and return the apropriate generator for do_mk_img
|
||||
def get_image_progress_generator(req, data_queue: queue.Queue, task_temp_images: list, step_callback, extra_props=None):
|
||||
if not req.stream_progress_updates:
|
||||
def empty_callback(x_samples, i): return x_samples
|
||||
def empty_callback(x_samples, i):
|
||||
step_callback()
|
||||
return empty_callback
|
||||
|
||||
thread_data.partial_x_samples = None
|
||||
@ -509,6 +565,7 @@ def do_mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, ste
|
||||
res = Response()
|
||||
res.request = req
|
||||
res.images = []
|
||||
thread_data.hypernetwork_strength = req.hypernetwork_strength
|
||||
|
||||
thread_data.temp_images.clear()
|
||||
|
||||
@ -583,11 +640,6 @@ def do_mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, ste
|
||||
t_enc = int(req.prompt_strength * req.num_inference_steps)
|
||||
print(f"target t_enc is {t_enc} steps")
|
||||
|
||||
if req.save_to_disk_path is not None:
|
||||
session_out_path = get_session_out_path(req.save_to_disk_path, req.session_id)
|
||||
else:
|
||||
session_out_path = None
|
||||
|
||||
with torch.no_grad():
|
||||
for n in trange(opt_n_iter, desc="Sampling"):
|
||||
for prompts in tqdm(data, desc="data"):
|
||||
@ -751,6 +803,8 @@ Sampler: {req.sampler}
|
||||
Negative Prompt: {req.negative_prompt}
|
||||
Stable Diffusion model: {req.use_stable_diffusion_model + '.ckpt'}
|
||||
VAE model: {req.use_vae_model}
|
||||
Hypernetwork Model: {req.use_hypernetwork_model}
|
||||
Hypernetwork Strength: {req.hypernetwork_strength}
|
||||
'''
|
||||
try:
|
||||
with open(meta_out_path, 'w', encoding='utf-8') as f:
|
||||
@ -883,6 +937,7 @@ def chunk(it, size):
|
||||
|
||||
def load_model_from_config(ckpt, verbose=False):
|
||||
print(f"Loading model from {ckpt}")
|
||||
model_ver = 'sd1'
|
||||
|
||||
if ckpt.endswith(".safetensors"):
|
||||
print("Loading from safetensors")
|
||||
@ -894,9 +949,13 @@ def load_model_from_config(ckpt, verbose=False):
|
||||
print(f"Global Step: {pl_sd['global_step']}")
|
||||
|
||||
if "state_dict" in pl_sd:
|
||||
return pl_sd["state_dict"]
|
||||
# check for a key that only seems to be present in SD2 models
|
||||
if 'cond_stage_model.model.ln_final.bias' in pl_sd['state_dict'].keys():
|
||||
model_ver = 'sd2'
|
||||
|
||||
return pl_sd["state_dict"], model_ver
|
||||
else:
|
||||
return pl_sd
|
||||
return pl_sd, model_ver
|
||||
|
||||
class UserInitiatedStop(Exception):
|
||||
pass
|
||||
|
@ -37,7 +37,8 @@ class ServerStates:
|
||||
|
||||
class RenderTask(): # Task with output queue and completion lock.
|
||||
def __init__(self, req: Request):
|
||||
self.request: Request = req # Initial Request
|
||||
req.request_id = id(self)
|
||||
self.request: Request = req # Initial Request
|
||||
self.response: Any = None # Copy of the last reponse
|
||||
self.render_device = None # Select the task affinity. (Not used to change active devices).
|
||||
self.temp_images:list = [None] * req.num_outputs * (1 if req.show_only_filtered_image else 2)
|
||||
@ -51,6 +52,22 @@ class RenderTask(): # Task with output queue and completion lock.
|
||||
self.buffer_queue.task_done()
|
||||
yield res
|
||||
except queue.Empty as e: yield
|
||||
@property
|
||||
def status(self):
|
||||
if self.lock.locked():
|
||||
return 'running'
|
||||
if isinstance(self.error, StopAsyncIteration):
|
||||
return 'stopped'
|
||||
if self.error:
|
||||
return 'error'
|
||||
if not self.buffer_queue.empty():
|
||||
return 'buffer'
|
||||
if self.response:
|
||||
return 'completed'
|
||||
return 'pending'
|
||||
@property
|
||||
def is_pending(self):
|
||||
return bool(not self.response and not self.error)
|
||||
|
||||
# defaults from https://huggingface.co/blog/stable_diffusion
|
||||
class ImageRequest(BaseModel):
|
||||
@ -77,6 +94,8 @@ class ImageRequest(BaseModel):
|
||||
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
|
||||
use_stable_diffusion_model: str = "sd-v1-4"
|
||||
use_vae_model: str = None
|
||||
use_hypernetwork_model: str = None
|
||||
hypernetwork_strength: float = None
|
||||
show_only_filtered_image: bool = False
|
||||
output_format: str = "jpeg" # or "png"
|
||||
output_quality: int = 75
|
||||
@ -99,7 +118,7 @@ class FilterRequest(BaseModel):
|
||||
output_quality: int = 75
|
||||
|
||||
# Temporary cache to allow to query tasks results for a short time after they are completed.
|
||||
class TaskCache():
|
||||
class DataCache():
|
||||
def __init__(self):
|
||||
self._base = dict()
|
||||
self._lock: threading.Lock = threading.Lock()
|
||||
@ -108,7 +127,7 @@ class TaskCache():
|
||||
def _is_expired(self, timestamp: int) -> bool:
|
||||
return int(time.time()) >= timestamp
|
||||
def clean(self) -> None:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.clean' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.clean' + ERR_LOCK_FAILED)
|
||||
try:
|
||||
# Create a list of expired keys to delete
|
||||
to_delete = []
|
||||
@ -118,16 +137,22 @@ class TaskCache():
|
||||
to_delete.append(key)
|
||||
# Remove Items
|
||||
for key in to_delete:
|
||||
(_, val) = self._base[key]
|
||||
if isinstance(val, RenderTask):
|
||||
print(f'RenderTask {key} expired. Data removed.')
|
||||
elif isinstance(val, SessionState):
|
||||
print(f'Session {key} expired. Data removed.')
|
||||
else:
|
||||
print(f'Key {key} expired. Data removed.')
|
||||
del self._base[key]
|
||||
print(f'Session {key} expired. Data removed.')
|
||||
finally:
|
||||
self._lock.release()
|
||||
def clear(self) -> None:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.clear' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.clear' + ERR_LOCK_FAILED)
|
||||
try: self._base.clear()
|
||||
finally: self._lock.release()
|
||||
def delete(self, key: Hashable) -> bool:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.delete' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.delete' + ERR_LOCK_FAILED)
|
||||
try:
|
||||
if key not in self._base:
|
||||
return False
|
||||
@ -136,7 +161,7 @@ class TaskCache():
|
||||
finally:
|
||||
self._lock.release()
|
||||
def keep(self, key: Hashable, ttl: int) -> bool:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.keep' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.keep' + ERR_LOCK_FAILED)
|
||||
try:
|
||||
if key in self._base:
|
||||
_, value = self._base.get(key)
|
||||
@ -146,7 +171,7 @@ class TaskCache():
|
||||
finally:
|
||||
self._lock.release()
|
||||
def put(self, key: Hashable, value: Any, ttl: int) -> bool:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.put' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.put' + ERR_LOCK_FAILED)
|
||||
try:
|
||||
self._base[key] = (
|
||||
self._get_ttl_time(ttl), value
|
||||
@ -160,7 +185,7 @@ class TaskCache():
|
||||
finally:
|
||||
self._lock.release()
|
||||
def tryGet(self, key: Hashable) -> Any:
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('TaskCache.tryGet' + ERR_LOCK_FAILED)
|
||||
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.tryGet' + ERR_LOCK_FAILED)
|
||||
try:
|
||||
ttl, value = self._base.get(key, (None, None))
|
||||
if ttl is not None and self._is_expired(ttl):
|
||||
@ -177,28 +202,61 @@ current_state = ServerStates.Init
|
||||
current_state_error:Exception = None
|
||||
current_model_path = None
|
||||
current_vae_path = None
|
||||
current_hypernetwork_path = None
|
||||
tasks_queue = []
|
||||
task_cache = TaskCache()
|
||||
session_cache = DataCache()
|
||||
task_cache = DataCache()
|
||||
default_model_to_load = None
|
||||
default_vae_to_load = None
|
||||
default_hypernetwork_to_load = None
|
||||
weak_thread_data = weakref.WeakKeyDictionary()
|
||||
idle_event: threading.Event = threading.Event()
|
||||
|
||||
def preload_model(ckpt_file_path=None, vae_file_path=None):
|
||||
global current_state, current_state_error, current_model_path, current_vae_path
|
||||
class SessionState():
|
||||
def __init__(self, id: str):
|
||||
self._id = id
|
||||
self._tasks_ids = []
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
@property
|
||||
def tasks(self):
|
||||
tasks = []
|
||||
for task_id in self._tasks_ids:
|
||||
task = task_cache.tryGet(task_id)
|
||||
if task:
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
def put(self, task, ttl=TASK_TTL):
|
||||
task_id = id(task)
|
||||
self._tasks_ids.append(task_id)
|
||||
if not task_cache.put(task_id, task, ttl):
|
||||
return False
|
||||
while len(self._tasks_ids) > len(render_threads) * 2:
|
||||
self._tasks_ids.pop(0)
|
||||
return True
|
||||
|
||||
def preload_model(ckpt_file_path=None, vae_file_path=None, hypernetwork_file_path=None):
|
||||
global current_state, current_state_error, current_model_path, current_vae_path, current_hypernetwork_path
|
||||
if ckpt_file_path == None:
|
||||
ckpt_file_path = default_model_to_load
|
||||
if vae_file_path == None:
|
||||
vae_file_path = default_vae_to_load
|
||||
if hypernetwork_file_path == None:
|
||||
hypernetwork_file_path = default_hypernetwork_to_load
|
||||
if ckpt_file_path == current_model_path and vae_file_path == current_vae_path:
|
||||
return
|
||||
current_state = ServerStates.LoadingModel
|
||||
try:
|
||||
from . import runtime
|
||||
runtime.thread_data.hypernetwork_file = hypernetwork_file_path
|
||||
runtime.thread_data.ckpt_file = ckpt_file_path
|
||||
runtime.thread_data.vae_file = vae_file_path
|
||||
runtime.load_model_ckpt()
|
||||
runtime.load_hypernetwork()
|
||||
current_model_path = ckpt_file_path
|
||||
current_vae_path = vae_file_path
|
||||
current_hypernetwork_path = hypernetwork_file_path
|
||||
current_state_error = None
|
||||
current_state = ServerStates.Online
|
||||
except Exception as e:
|
||||
@ -240,7 +298,7 @@ def thread_get_next_task():
|
||||
manager_lock.release()
|
||||
|
||||
def thread_render(device):
|
||||
global current_state, current_state_error, current_model_path, current_vae_path
|
||||
global current_state, current_state_error, current_model_path, current_vae_path, current_hypernetwork_path
|
||||
from . import runtime
|
||||
try:
|
||||
runtime.thread_init(device)
|
||||
@ -259,6 +317,7 @@ def thread_render(device):
|
||||
preload_model()
|
||||
current_state = ServerStates.Online
|
||||
while True:
|
||||
session_cache.clean()
|
||||
task_cache.clean()
|
||||
if not weak_thread_data[threading.current_thread()]['alive']:
|
||||
print(f'Shutting down thread for device {runtime.thread_data.device}')
|
||||
@ -270,7 +329,8 @@ def thread_render(device):
|
||||
return
|
||||
task = thread_get_next_task()
|
||||
if task is None:
|
||||
time.sleep(0.05)
|
||||
idle_event.clear()
|
||||
idle_event.wait(timeout=1)
|
||||
continue
|
||||
if task.error is not None:
|
||||
print(task.error)
|
||||
@ -285,6 +345,10 @@ def thread_render(device):
|
||||
print(f'Session {task.request.session_id} starting task {id(task)} on {runtime.thread_data.device_name}')
|
||||
if not task.lock.acquire(blocking=False): raise Exception('Got locked task from queue.')
|
||||
try:
|
||||
if runtime.is_hypernetwork_reload_necessary(task.request):
|
||||
runtime.reload_hypernetwork()
|
||||
current_hypernetwork_path = task.request.use_hypernetwork_model
|
||||
|
||||
if runtime.is_model_reload_necessary(task.request):
|
||||
current_state = ServerStates.LoadingModel
|
||||
runtime.reload_model()
|
||||
@ -301,10 +365,11 @@ def thread_render(device):
|
||||
current_state_error = None
|
||||
print(f'Session {task.request.session_id} sent cancel signal for task {id(task)}')
|
||||
|
||||
task_cache.keep(task.request.session_id, TASK_TTL)
|
||||
|
||||
current_state = ServerStates.Rendering
|
||||
task.response = runtime.mk_img(task.request, task.buffer_queue, task.temp_images, step_callback)
|
||||
# Before looping back to the generator, mark cache as still alive.
|
||||
task_cache.keep(id(task), TASK_TTL)
|
||||
session_cache.keep(task.request.session_id, TASK_TTL)
|
||||
except Exception as e:
|
||||
task.error = e
|
||||
print(traceback.format_exc())
|
||||
@ -312,7 +377,8 @@ def thread_render(device):
|
||||
finally:
|
||||
# Task completed
|
||||
task.lock.release()
|
||||
task_cache.keep(task.request.session_id, TASK_TTL)
|
||||
task_cache.keep(id(task), TASK_TTL)
|
||||
session_cache.keep(task.request.session_id, TASK_TTL)
|
||||
if isinstance(task.error, StopAsyncIteration):
|
||||
print(f'Session {task.request.session_id} task {id(task)} cancelled!')
|
||||
elif task.error is not None:
|
||||
@ -321,12 +387,21 @@ def thread_render(device):
|
||||
print(f'Session {task.request.session_id} task {id(task)} completed by {runtime.thread_data.device_name}.')
|
||||
current_state = ServerStates.Online
|
||||
|
||||
def get_cached_task(session_id:str, update_ttl:bool=False):
|
||||
def get_cached_task(task_id:str, update_ttl:bool=False):
|
||||
# By calling keep before tryGet, wont discard if was expired.
|
||||
if update_ttl and not task_cache.keep(session_id, TASK_TTL):
|
||||
if update_ttl and not task_cache.keep(task_id, TASK_TTL):
|
||||
# Failed to keep task, already gone.
|
||||
return None
|
||||
return task_cache.tryGet(session_id)
|
||||
return task_cache.tryGet(task_id)
|
||||
|
||||
def get_cached_session(session_id:str, update_ttl:bool=False):
|
||||
if update_ttl:
|
||||
session_cache.keep(session_id, TASK_TTL)
|
||||
session = session_cache.tryGet(session_id)
|
||||
if not session:
|
||||
session = SessionState(session_id)
|
||||
session_cache.put(session_id, session, TASK_TTL)
|
||||
return session
|
||||
|
||||
def get_devices():
|
||||
devices = {
|
||||
@ -473,14 +548,16 @@ def shutdown_event(): # Signal render thread to close on shutdown
|
||||
current_state_error = SystemExit('Application shutting down.')
|
||||
|
||||
def render(req : ImageRequest):
|
||||
if is_alive() <= 0: # Render thread is dead
|
||||
current_thread_count = is_alive()
|
||||
if current_thread_count <= 0: # Render thread is dead
|
||||
raise ChildProcessError('Rendering thread has died.')
|
||||
|
||||
# Alive, check if task in cache
|
||||
task = task_cache.tryGet(req.session_id)
|
||||
if task and not task.response and not task.error and not task.lock.locked():
|
||||
# Unstarted task pending, deny queueing more than one.
|
||||
raise ConnectionRefusedError(f'Session {req.session_id} has an already pending task.')
|
||||
#
|
||||
session = get_cached_session(req.session_id, update_ttl=True)
|
||||
pending_tasks = list(filter(lambda t: t.is_pending, session.tasks))
|
||||
if current_thread_count < len(pending_tasks):
|
||||
raise ConnectionRefusedError(f'Session {req.session_id} already has {len(pending_tasks)} pending tasks out of {current_thread_count}.')
|
||||
|
||||
from . import runtime
|
||||
r = Request()
|
||||
r.session_id = req.session_id
|
||||
@ -504,6 +581,8 @@ def render(req : ImageRequest):
|
||||
r.use_face_correction = req.use_face_correction
|
||||
r.use_stable_diffusion_model = req.use_stable_diffusion_model
|
||||
r.use_vae_model = req.use_vae_model
|
||||
r.use_hypernetwork_model = req.use_hypernetwork_model
|
||||
r.hypernetwork_strength = req.hypernetwork_strength
|
||||
r.show_only_filtered_image = req.show_only_filtered_image
|
||||
r.output_format = req.output_format
|
||||
r.output_quality = req.output_quality
|
||||
@ -515,13 +594,13 @@ def render(req : ImageRequest):
|
||||
r.stream_image_progress = False
|
||||
|
||||
new_task = RenderTask(r)
|
||||
|
||||
if task_cache.put(r.session_id, new_task, TASK_TTL):
|
||||
if session.put(new_task, TASK_TTL):
|
||||
# Use twice the normal timeout for adding user requests.
|
||||
# Tries to force task_cache.put to fail before tasks_queue.put would.
|
||||
# Tries to force session.put to fail before tasks_queue.put would.
|
||||
if manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT * 2):
|
||||
try:
|
||||
tasks_queue.append(new_task)
|
||||
idle_event.set()
|
||||
return new_task
|
||||
finally:
|
||||
manager_lock.release()
|
||||
|
78
ui/server.py
78
ui/server.py
@ -26,6 +26,7 @@ UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, 'core'), (USER_UI_PLUGINS_DIR, 'user
|
||||
|
||||
STABLE_DIFFUSION_MODEL_EXTENSIONS = ['.ckpt', '.safetensors']
|
||||
VAE_MODEL_EXTENSIONS = ['.vae.pt', '.ckpt']
|
||||
HYPERNETWORK_MODEL_EXTENSIONS = ['.pt']
|
||||
|
||||
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
|
||||
TASK_TTL = 15 * 60 # Discard last session's task timeout
|
||||
@ -48,14 +49,12 @@ from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
#import queue, threading, time
|
||||
from typing import Any, Generator, Hashable, List, Optional, Union
|
||||
|
||||
from sd_internal import Request, Response, task_manager
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
modifiers_cache = None
|
||||
outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME)
|
||||
|
||||
os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True)
|
||||
@ -193,6 +192,12 @@ def resolve_vae_to_use(model_name:str=None):
|
||||
except:
|
||||
return None
|
||||
|
||||
def resolve_hypernetwork_to_use(model_name:str=None):
|
||||
try:
|
||||
return resolve_model_to_use(model_name, model_type='hypernetwork', model_dir='hypernetwork', model_extensions=HYPERNETWORK_MODEL_EXTENSIONS, default_models=[])
|
||||
except:
|
||||
return None
|
||||
|
||||
class SetAppConfigRequest(BaseModel):
|
||||
update_branch: str = None
|
||||
render_devices: Union[List[str], List[int], str, int] = None
|
||||
@ -253,10 +258,12 @@ def getModels():
|
||||
'active': {
|
||||
'stable-diffusion': 'sd-v1-4',
|
||||
'vae': '',
|
||||
'hypernetwork': '',
|
||||
},
|
||||
'options': {
|
||||
'stable-diffusion': ['sd-v1-4'],
|
||||
'vae': [],
|
||||
'hypernetwork': [],
|
||||
},
|
||||
}
|
||||
|
||||
@ -288,7 +295,7 @@ def getModels():
|
||||
# custom models
|
||||
listModels(models_dirname='stable-diffusion', model_type='stable-diffusion', model_extensions=STABLE_DIFFUSION_MODEL_EXTENSIONS)
|
||||
listModels(models_dirname='vae', model_type='vae', model_extensions=VAE_MODEL_EXTENSIONS)
|
||||
|
||||
listModels(models_dirname='hypernetwork', model_type='hypernetwork', model_extensions=HYPERNETWORK_MODEL_EXTENSIONS)
|
||||
# legacy
|
||||
custom_weight_path = os.path.join(SD_DIR, 'custom-model.ckpt')
|
||||
if os.path.exists(custom_weight_path):
|
||||
@ -345,34 +352,24 @@ def ping(session_id:str=None):
|
||||
# Alive
|
||||
response = {'status': str(task_manager.current_state)}
|
||||
if session_id:
|
||||
task = task_manager.get_cached_task(session_id, update_ttl=True)
|
||||
if task:
|
||||
response['task'] = id(task)
|
||||
if task.lock.locked():
|
||||
response['session'] = 'running'
|
||||
elif isinstance(task.error, StopAsyncIteration):
|
||||
response['session'] = 'stopped'
|
||||
elif task.error:
|
||||
response['session'] = 'error'
|
||||
elif not task.buffer_queue.empty():
|
||||
response['session'] = 'buffer'
|
||||
elif task.response:
|
||||
response['session'] = 'completed'
|
||||
else:
|
||||
response['session'] = 'pending'
|
||||
session = task_manager.get_cached_session(session_id, update_ttl=True)
|
||||
response['tasks'] = {id(t): t.status for t in session.tasks}
|
||||
response['devices'] = task_manager.get_devices()
|
||||
return JSONResponse(response, headers=NOCACHE_HEADERS)
|
||||
|
||||
def save_model_to_config(ckpt_model_name, vae_model_name):
|
||||
def save_model_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name):
|
||||
config = getConfig()
|
||||
if 'model' not in config:
|
||||
config['model'] = {}
|
||||
|
||||
config['model']['stable-diffusion'] = ckpt_model_name
|
||||
config['model']['vae'] = vae_model_name
|
||||
config['model']['hypernetwork'] = hypernetwork_model_name
|
||||
|
||||
if vae_model_name is None or vae_model_name == "":
|
||||
del config['model']['vae']
|
||||
if hypernetwork_model_name is None or hypernetwork_model_name == "":
|
||||
del config['model']['hypernetwork']
|
||||
|
||||
setConfig(config)
|
||||
|
||||
@ -388,30 +385,33 @@ def update_render_devices_in_config(config, render_devices):
|
||||
@app.post('/render')
|
||||
def render(req : task_manager.ImageRequest):
|
||||
try:
|
||||
save_model_to_config(req.use_stable_diffusion_model, req.use_vae_model)
|
||||
save_model_to_config(req.use_stable_diffusion_model, req.use_vae_model, req.use_hypernetwork_model)
|
||||
req.use_stable_diffusion_model = resolve_ckpt_to_use(req.use_stable_diffusion_model)
|
||||
req.use_vae_model = resolve_vae_to_use(req.use_vae_model)
|
||||
req.use_hypernetwork_model = resolve_hypernetwork_to_use(req.use_hypernetwork_model)
|
||||
new_task = task_manager.render(req)
|
||||
response = {
|
||||
'status': str(task_manager.current_state),
|
||||
'queue': len(task_manager.tasks_queue),
|
||||
'stream': f'/image/stream/{req.session_id}/{id(new_task)}',
|
||||
'stream': f'/image/stream/{id(new_task)}',
|
||||
'task': id(new_task)
|
||||
}
|
||||
return JSONResponse(response, headers=NOCACHE_HEADERS)
|
||||
except ChildProcessError as e: # Render thread is dead
|
||||
raise HTTPException(status_code=500, detail=f'Rendering thread has died.') # HTTP500 Internal Server Error
|
||||
except ConnectionRefusedError as e: # Unstarted task pending, deny queueing more than one.
|
||||
raise HTTPException(status_code=503, detail=f'Session {req.session_id} has an already pending task.') # HTTP503 Service Unavailable
|
||||
except ConnectionRefusedError as e: # Unstarted task pending limit reached, deny queueing too many.
|
||||
raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get('/image/stream/{session_id:str}/{task_id:int}')
|
||||
def stream(session_id:str, task_id:int):
|
||||
@app.get('/image/stream/{task_id:int}')
|
||||
def stream(task_id:int):
|
||||
#TODO Move to WebSockets ??
|
||||
task = task_manager.get_cached_task(session_id, update_ttl=True)
|
||||
if not task: raise HTTPException(status_code=410, detail='No request received.') # HTTP410 Gone
|
||||
if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict
|
||||
task = task_manager.get_cached_task(task_id, update_ttl=True)
|
||||
if not task: raise HTTPException(status_code=404, detail=f'Request {task_id} not found.') # HTTP404 NotFound
|
||||
#if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict
|
||||
if task.buffer_queue.empty() and not task.lock.locked():
|
||||
if task.response:
|
||||
#print(f'Session {session_id} sending cached response')
|
||||
@ -421,22 +421,23 @@ def stream(session_id:str, task_id:int):
|
||||
return StreamingResponse(task.read_buffer_generator(), media_type='application/json')
|
||||
|
||||
@app.get('/image/stop')
|
||||
def stop(session_id:str=None):
|
||||
if not session_id:
|
||||
def stop(task: int):
|
||||
if not task:
|
||||
if task_manager.current_state == task_manager.ServerStates.Online or task_manager.current_state == task_manager.ServerStates.Unavailable:
|
||||
raise HTTPException(status_code=409, detail='Not currently running any tasks.') # HTTP409 Conflict
|
||||
task_manager.current_state_error = StopAsyncIteration('')
|
||||
return {'OK'}
|
||||
task = task_manager.get_cached_task(session_id, update_ttl=False)
|
||||
if not task: raise HTTPException(status_code=404, detail=f'Session {session_id} has no active task.') # HTTP404 Not Found
|
||||
if isinstance(task.error, StopAsyncIteration): raise HTTPException(status_code=409, detail=f'Session {session_id} task is already stopped.') # HTTP409 Conflict
|
||||
task.error = StopAsyncIteration('')
|
||||
task_id = task
|
||||
task = task_manager.get_cached_task(task_id, update_ttl=False)
|
||||
if not task: raise HTTPException(status_code=404, detail=f'Task {task_id} was not found.') # HTTP404 Not Found
|
||||
if isinstance(task.error, StopAsyncIteration): raise HTTPException(status_code=409, detail=f'Task {task_id} is already stopped.') # HTTP409 Conflict
|
||||
task.error = StopAsyncIteration(f'Task {task_id} stop requested.')
|
||||
return {'OK'}
|
||||
|
||||
@app.get('/image/tmp/{session_id}/{img_id:int}')
|
||||
def get_image(session_id, img_id):
|
||||
task = task_manager.get_cached_task(session_id, update_ttl=True)
|
||||
if not task: raise HTTPException(status_code=410, detail=f'Session {session_id} has not submitted a task.') # HTTP410 Gone
|
||||
@app.get('/image/tmp/{task_id:int}/{img_id:int}')
|
||||
def get_image(task_id: int, img_id: int):
|
||||
task = task_manager.get_cached_task(task_id, update_ttl=True)
|
||||
if not task: raise HTTPException(status_code=410, detail=f'Task {task_id} could not be found.') # HTTP404 NotFound
|
||||
if not task.temp_images[img_id]: raise HTTPException(status_code=425, detail='Too Early, task data is not available yet.') # HTTP425 Too Early
|
||||
try:
|
||||
img_data = task.temp_images[img_id]
|
||||
@ -469,6 +470,7 @@ getModels()
|
||||
# Start the task_manager
|
||||
task_manager.default_model_to_load = resolve_ckpt_to_use()
|
||||
task_manager.default_vae_to_load = resolve_vae_to_use()
|
||||
task_manager.default_hypernetwork_to_load = resolve_hypernetwork_to_use()
|
||||
|
||||
def update_render_threads():
|
||||
config = getConfig()
|
||||
|
Loading…
Reference in New Issue
Block a user