From 9e063711789336ab16a07e1873b753eee1b094c9 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 18 Oct 2023 13:39:21 +1100
Subject: [PATCH] feat(api): serve app via route & add cache-control: no-store
This should prevent `index.html` from *ever* being cached, so UIs will never be out of date.
Minor organisation to accomodate this.
Deleting old unused files from the early days
---
invokeai/app/api_app.py | 24 +-
.../static/{dream_web => docs}/favicon.ico | Bin
.../frontend/web/static/dream_web/index.css | 179 --------
.../frontend/web/static/dream_web/index.html | 187 --------
.../frontend/web/static/dream_web/index.js | 409 ------------------
.../frontend/web/static/dream_web/test.html | 246 -----------
.../web/static/legacy_web/favicon.ico | Bin 1150 -> 0 bytes
.../frontend/web/static/legacy_web/index.css | 152 -------
.../frontend/web/static/legacy_web/index.html | 137 ------
.../frontend/web/static/legacy_web/index.js | 234 ----------
10 files changed, 17 insertions(+), 1551 deletions(-)
rename invokeai/frontend/web/static/{dream_web => docs}/favicon.ico (100%)
delete mode 100644 invokeai/frontend/web/static/dream_web/index.css
delete mode 100644 invokeai/frontend/web/static/dream_web/index.html
delete mode 100644 invokeai/frontend/web/static/dream_web/index.js
delete mode 100644 invokeai/frontend/web/static/dream_web/test.html
delete mode 100644 invokeai/frontend/web/static/legacy_web/favicon.ico
delete mode 100644 invokeai/frontend/web/static/legacy_web/index.css
delete mode 100644 invokeai/frontend/web/static/legacy_web/index.html
delete mode 100644 invokeai/frontend/web/static/legacy_web/index.js
diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py
index e07b037dd1..f45541e63b 100644
--- a/invokeai/app/api_app.py
+++ b/invokeai/app/api_app.py
@@ -23,6 +23,7 @@ if True: # hack to make flake8 happy with imports coming after setting up the c
from fastapi_events.handlers.local import local_handler
from fastapi_events.middleware import EventHandlerASGIMiddleware
from pydantic.json_schema import models_json_schema
+ from fastapi.responses import FileResponse
# noinspection PyUnresolvedReferences
import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import)
@@ -173,16 +174,13 @@ def custom_openapi():
app.openapi = custom_openapi # type: ignore [method-assign] # this is a valid assignment
-# Override API doc favicons
-app.mount("/static", StaticFiles(directory=Path(web_dir.__path__[0], "static/dream_web")), name="static")
-
@app.get("/docs", include_in_schema=False)
def overridden_swagger():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title,
- swagger_favicon_url="/static/favicon.ico",
+ swagger_favicon_url="/static/docs/favicon.ico",
)
@@ -191,12 +189,24 @@ def overridden_redoc():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title,
- redoc_favicon_url="/static/favicon.ico",
+ redoc_favicon_url="/static/docs/favicon.ico",
)
-# Must mount *after* the other routes else it borks em
-app.mount("/", StaticFiles(directory=Path(web_dir.__path__[0], "dist"), html=True), name="ui")
+web_root_path = Path(list(web_dir.__path__)[0])
+
+
+# Cannot add headers to StaticFiles, so we must serve index.html with a custom route
+# Add cache-control: no-store header to prevent caching of index.html, which leads to broken UIs at release
+@app.get("/", include_in_schema=False, name="ui_root")
+def get_index() -> FileResponse:
+ return FileResponse(Path(web_root_path, "dist/index.html"), headers={"Cache-Control": "no-store"})
+
+
+# # Must mount *after* the other routes else it borks em
+app.mount("/static", StaticFiles(directory=Path(web_root_path, "static/")), name="static") # docs favicon is in here
+app.mount("/assets", StaticFiles(directory=Path(web_root_path, "dist/assets/")), name="assets")
+app.mount("/locales", StaticFiles(directory=Path(web_root_path, "dist/locales/")), name="locales")
def invoke_api():
diff --git a/invokeai/frontend/web/static/dream_web/favicon.ico b/invokeai/frontend/web/static/docs/favicon.ico
similarity index 100%
rename from invokeai/frontend/web/static/dream_web/favicon.ico
rename to invokeai/frontend/web/static/docs/favicon.ico
diff --git a/invokeai/frontend/web/static/dream_web/index.css b/invokeai/frontend/web/static/dream_web/index.css
deleted file mode 100644
index 25a0994a3d..0000000000
--- a/invokeai/frontend/web/static/dream_web/index.css
+++ /dev/null
@@ -1,179 +0,0 @@
-:root {
- --fields-dark:#DCDCDC;
- --fields-light:#F5F5F5;
-}
-
-* {
- font-family: 'Arial';
- font-size: 100%;
-}
-body {
- font-size: 1em;
-}
-textarea {
- font-size: 0.95em;
-}
-header, form, #progress-section {
- margin-left: auto;
- margin-right: auto;
- max-width: 1024px;
- text-align: center;
-}
-fieldset {
- border: none;
- line-height: 2.2em;
-}
-fieldset > legend {
- width: auto;
- margin-left: 0;
- margin-right: auto;
- font-weight:bold;
-}
-select, input {
- margin-right: 10px;
- padding: 2px;
-}
-input:disabled {
- cursor:auto;
-}
-input[type=submit] {
- cursor: pointer;
- background-color: #666;
- color: white;
-}
-input[type=checkbox] {
- cursor: pointer;
- margin-right: 0px;
- width: 20px;
- height: 20px;
- vertical-align: middle;
-}
-input#seed {
- margin-right: 0px;
-}
-div {
- padding: 10px 10px 10px 10px;
-}
-header {
- margin-bottom: 16px;
-}
-header h1 {
- margin-bottom: 0;
- font-size: 2em;
-}
-#search-box {
- display: flex;
-}
-#scaling-inprocess-message {
- font-weight: bold;
- font-style: italic;
- display: none;
-}
-#prompt {
- flex-grow: 1;
- padding: 5px 10px 5px 10px;
- border: 1px solid #999;
- outline: none;
-}
-#submit {
- padding: 5px 10px 5px 10px;
- border: 1px solid #999;
-}
-#reset-all, #remove-image {
- margin-top: 12px;
- font-size: 0.8em;
- background-color: pink;
- border: 1px solid #999;
- border-radius: 4px;
-}
-#results {
- text-align: center;
- margin: auto;
- padding-top: 10px;
-}
-#results figure {
- display: inline-block;
- margin: 10px;
-}
-#results figcaption {
- font-size: 0.8em;
- padding: 3px;
- color: #888;
- cursor: pointer;
-}
-#results img {
- border-radius: 5px;
- object-fit: contain;
- background-color: var(--fields-dark);
-}
-#fieldset-config {
- line-height:2em;
-}
-input[type="number"] {
- width: 60px;
-}
-#seed {
- width: 150px;
-}
-button#reset-seed {
- font-size: 1.7em;
- background: #efefef;
- border: 1px solid #999;
- border-radius: 4px;
- line-height: 0.8;
- margin: 0 10px 0 0;
- padding: 0 5px 3px;
- vertical-align: middle;
-}
-label {
- white-space: nowrap;
-}
-#progress-section {
- display: none;
-}
-#progress-image {
- width: 30vh;
- height: 30vh;
- object-fit: contain;
- background-color: var(--fields-dark);
-}
-#cancel-button {
- cursor: pointer;
- color: red;
-}
-#txt2img {
- background-color: var(--fields-dark);
-}
-#variations {
- background-color: var(--fields-light);
-}
-#initimg {
- background-color: var(--fields-dark);
-}
-#img2img {
- background-color: var(--fields-light);
-}
-#initimg > :not(legend) {
- background-color: var(--fields-light);
- margin: .5em;
-}
-
-#postprocess, #initimg {
- display:flex;
- flex-wrap:wrap;
- padding: 0;
- margin-top: 1em;
- background-color: var(--fields-dark);
-}
-#postprocess > fieldset, #initimg > * {
- flex-grow: 1;
-}
-#postprocess > fieldset {
- background-color: var(--fields-dark);
-}
-#progress-section {
- background-color: var(--fields-light);
-}
-#no-results-message:not(:only-child) {
- display: none;
-}
diff --git a/invokeai/frontend/web/static/dream_web/index.html b/invokeai/frontend/web/static/dream_web/index.html
deleted file mode 100644
index feb542adb2..0000000000
--- a/invokeai/frontend/web/static/dream_web/index.html
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
- Stable Diffusion Dream Server
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
✖
-
-

-
- Postprocessing...1/3
-
-
-
-
-
-
-
-
-
-
diff --git a/invokeai/frontend/web/static/dream_web/index.js b/invokeai/frontend/web/static/dream_web/index.js
deleted file mode 100644
index 438232f0c7..0000000000
--- a/invokeai/frontend/web/static/dream_web/index.js
+++ /dev/null
@@ -1,409 +0,0 @@
-const socket = io();
-
-var priorResultsLoadState = {
- page: 0,
- pages: 1,
- per_page: 10,
- total: 20,
- offset: 0, // number of items generated since last load
- loading: false,
- initialized: false,
-};
-
-function loadPriorResults() {
- // Fix next page by offset
- let offsetPages =
- priorResultsLoadState.offset / priorResultsLoadState.per_page;
- priorResultsLoadState.page += offsetPages;
- priorResultsLoadState.pages += offsetPages;
- priorResultsLoadState.total += priorResultsLoadState.offset;
- priorResultsLoadState.offset = 0;
-
- if (priorResultsLoadState.loading) {
- return;
- }
-
- if (priorResultsLoadState.page >= priorResultsLoadState.pages) {
- return; // Nothing more to load
- }
-
- // Load
- priorResultsLoadState.loading = true;
- let url = new URL('/api/images', document.baseURI);
- url.searchParams.append(
- 'page',
- priorResultsLoadState.initialized
- ? priorResultsLoadState.page + 1
- : priorResultsLoadState.page
- );
- url.searchParams.append('per_page', priorResultsLoadState.per_page);
- fetch(url.href, {
- method: 'GET',
- headers: new Headers({ 'content-type': 'application/json' }),
- })
- .then((response) => response.json())
- .then((data) => {
- priorResultsLoadState.page = data.page;
- priorResultsLoadState.pages = data.pages;
- priorResultsLoadState.per_page = data.per_page;
- priorResultsLoadState.total = data.total;
-
- data.items.forEach(function (dreamId, index) {
- let src = 'api/images/' + dreamId;
- fetch('/api/images/' + dreamId + '/metadata', {
- method: 'GET',
- headers: new Headers({ 'content-type': 'application/json' }),
- })
- .then((response) => response.json())
- .then((metadata) => {
- let seed = metadata.seed || 0; // TODO: Parse old metadata
- appendOutput(src, seed, metadata, true);
- });
- });
-
- // Load until page is full
- if (!priorResultsLoadState.initialized) {
- if (document.body.scrollHeight <= window.innerHeight) {
- loadPriorResults();
- }
- }
- })
- .finally(() => {
- priorResultsLoadState.loading = false;
- priorResultsLoadState.initialized = true;
- });
-}
-
-function resetForm() {
- var form = document.getElementById('generate-form');
- form.querySelector('fieldset').removeAttribute('disabled');
-}
-
-function initProgress(totalSteps, showProgressImages) {
- // TODO: Progress could theoretically come from multiple jobs at the same time (in the future)
- let progressSectionEle = document.querySelector('#progress-section');
- progressSectionEle.style.display = 'initial';
- let progressEle = document.querySelector('#progress-bar');
- progressEle.setAttribute('max', totalSteps);
-
- let progressImageEle = document.querySelector('#progress-image');
- progressImageEle.src = BLANK_IMAGE_URL;
- progressImageEle.style.display = showProgressImages ? 'initial' : 'none';
-}
-
-function setProgress(step, totalSteps, src) {
- let progressEle = document.querySelector('#progress-bar');
- progressEle.setAttribute('value', step);
-
- if (src) {
- let progressImageEle = document.querySelector('#progress-image');
- progressImageEle.src = src;
- }
-}
-
-function resetProgress(hide = true) {
- if (hide) {
- let progressSectionEle = document.querySelector('#progress-section');
- progressSectionEle.style.display = 'none';
- }
- let progressEle = document.querySelector('#progress-bar');
- progressEle.setAttribute('value', 0);
-}
-
-function toBase64(file) {
- return new Promise((resolve, reject) => {
- const r = new FileReader();
- r.readAsDataURL(file);
- r.onload = () => resolve(r.result);
- r.onerror = (error) => reject(error);
- });
-}
-
-function ondragdream(event) {
- let dream = event.target.dataset.dream;
- event.dataTransfer.setData('dream', dream);
-}
-
-function seedClick(event) {
- // Get element
- var image = event.target.closest('figure').querySelector('img');
- var dream = JSON.parse(decodeURIComponent(image.dataset.dream));
-
- let form = document.querySelector('#generate-form');
- for (const [k, v] of new FormData(form)) {
- if (k == 'initimg') {
- continue;
- }
- let formElem = form.querySelector(`*[name=${k}]`);
- formElem.value = dream[k] !== undefined ? dream[k] : formElem.defaultValue;
- }
-
- document.querySelector('#seed').value = dream.seed;
- document.querySelector('#iterations').value = 1; // Reset to 1 iteration since we clicked a single image (not a full job)
-
- // NOTE: leaving this manual for the user for now - it was very confusing with this behavior
- // document.querySelector("#with_variations").value = variations || '';
- // if (document.querySelector("#variation_amount").value <= 0) {
- // document.querySelector("#variation_amount").value = 0.2;
- // }
-
- saveFields(document.querySelector('#generate-form'));
-}
-
-function appendOutput(src, seed, config, toEnd = false) {
- let outputNode = document.createElement('figure');
- let altText = seed.toString() + ' | ' + config.prompt;
-
- // img needs width and height for lazy loading to work
- // TODO: store the full config in a data attribute on the image?
- const figureContents = `
-
-
-
- ${seed}
- `;
-
- outputNode.innerHTML = figureContents;
-
- if (toEnd) {
- document.querySelector('#results').append(outputNode);
- } else {
- document.querySelector('#results').prepend(outputNode);
- }
- document.querySelector('#no-results-message')?.remove();
-}
-
-function saveFields(form) {
- for (const [k, v] of new FormData(form)) {
- if (typeof v !== 'object') {
- // Don't save 'file' type
- localStorage.setItem(k, v);
- }
- }
-}
-
-function loadFields(form) {
- for (const [k, v] of new FormData(form)) {
- const item = localStorage.getItem(k);
- if (item != null) {
- form.querySelector(`*[name=${k}]`).value = item;
- }
- }
-}
-
-function clearFields(form) {
- localStorage.clear();
- let prompt = form.prompt.value;
- form.reset();
- form.prompt.value = prompt;
-}
-
-const BLANK_IMAGE_URL =
- 'data:image/svg+xml,';
-async function generateSubmit(form) {
- // Convert file data to base64
- // TODO: Should probably uplaod files with formdata or something, and store them in the backend?
- let formData = Object.fromEntries(new FormData(form));
- if (!formData.enable_generate && !formData.enable_init_image) {
- gen_label = document.querySelector('label[for=enable_generate]').innerHTML;
- initimg_label = document.querySelector(
- 'label[for=enable_init_image]'
- ).innerHTML;
- alert(`Error: one of "${gen_label}" or "${initimg_label}" must be set`);
- }
-
- formData.initimg_name = formData.initimg.name;
- formData.initimg =
- formData.initimg.name !== '' ? await toBase64(formData.initimg) : null;
-
- // Evaluate all checkboxes
- let checkboxes = form.querySelectorAll('input[type=checkbox]');
- checkboxes.forEach(function (checkbox) {
- if (checkbox.checked) {
- formData[checkbox.name] = 'true';
- }
- });
-
- let strength = formData.strength;
- let totalSteps = formData.initimg
- ? Math.floor(strength * formData.steps)
- : formData.steps;
- let showProgressImages = formData.progress_images;
-
- // Set enabling flags
-
- // Initialize the progress bar
- initProgress(totalSteps, showProgressImages);
-
- // POST, use response to listen for events
- fetch(form.action, {
- method: form.method,
- headers: new Headers({ 'content-type': 'application/json' }),
- body: JSON.stringify(formData),
- })
- .then((response) => response.json())
- .then((data) => {
- var jobId = data.jobId;
- socket.emit('join_room', { room: jobId });
- });
-
- form.querySelector('fieldset').setAttribute('disabled', '');
-}
-
-function fieldSetEnableChecked(event) {
- cb = event.target;
- fields = cb.closest('fieldset');
- fields.disabled = !cb.checked;
-}
-
-// Socket listeners
-socket.on('job_started', (data) => {});
-
-socket.on('dream_result', (data) => {
- var jobId = data.jobId;
- var dreamId = data.dreamId;
- var dreamRequest = data.dreamRequest;
- var src = 'api/images/' + dreamId;
-
- priorResultsLoadState.offset += 1;
- appendOutput(src, dreamRequest.seed, dreamRequest);
-
- resetProgress(false);
-});
-
-socket.on('dream_progress', (data) => {
- // TODO: it'd be nice if we could get a seed reported here, but the generator would need to be updated
- var step = data.step;
- var totalSteps = data.totalSteps;
- var jobId = data.jobId;
- var dreamId = data.dreamId;
-
- var progressType = data.progressType;
- if (progressType === 'GENERATION') {
- var src = data.hasProgressImage
- ? 'api/intermediates/' + dreamId + '/' + step
- : null;
- setProgress(step, totalSteps, src);
- } else if (progressType === 'UPSCALING_STARTED') {
- // step and totalSteps are used for upscale count on this message
- document.getElementById('processing_cnt').textContent = step;
- document.getElementById('processing_total').textContent = totalSteps;
- document.getElementById('scaling-inprocess-message').style.display =
- 'block';
- } else if (progressType == 'UPSCALING_DONE') {
- document.getElementById('scaling-inprocess-message').style.display = 'none';
- }
-});
-
-socket.on('job_canceled', (data) => {
- resetForm();
- resetProgress();
-});
-
-socket.on('job_done', (data) => {
- jobId = data.jobId;
- socket.emit('leave_room', { room: jobId });
-
- resetForm();
- resetProgress();
-});
-
-window.onload = async () => {
- document.querySelector('#prompt').addEventListener('keydown', (e) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- const form = e.target.form;
- generateSubmit(form);
- }
- });
- document.querySelector('#generate-form').addEventListener('submit', (e) => {
- e.preventDefault();
- const form = e.target;
-
- generateSubmit(form);
- });
- document.querySelector('#generate-form').addEventListener('change', (e) => {
- saveFields(e.target.form);
- });
- document.querySelector('#reset-seed').addEventListener('click', (e) => {
- document.querySelector('#seed').value = 0;
- saveFields(e.target.form);
- });
- document.querySelector('#reset-all').addEventListener('click', (e) => {
- clearFields(e.target.form);
- });
- document.querySelector('#remove-image').addEventListener('click', (e) => {
- initimg.value = null;
- });
- loadFields(document.querySelector('#generate-form'));
-
- document.querySelector('#cancel-button').addEventListener('click', () => {
- fetch('/api/cancel').catch((e) => {
- console.error(e);
- });
- });
- document.documentElement.addEventListener('keydown', (e) => {
- if (e.key === 'Escape')
- fetch('/api/cancel').catch((err) => {
- console.error(err);
- });
- });
-
- if (!config.gfpgan_model_exists) {
- document.querySelector('#gfpgan').style.display = 'none';
- }
-
- window.addEventListener('scroll', () => {
- if (window.innerHeight + window.pageYOffset >= document.body.offsetHeight) {
- loadPriorResults();
- }
- });
-
- // Enable/disable forms by checkboxes
- document
- .querySelectorAll('legend > input[type=checkbox]')
- .forEach(function (cb) {
- cb.addEventListener('change', fieldSetEnableChecked);
- fieldSetEnableChecked({ target: cb });
- });
-
- // Load some of the previous results
- loadPriorResults();
-
- // Image drop/upload WIP
- /*
- let drop = document.getElementById('dropper');
- function ondrop(event) {
- let dreamData = event.dataTransfer.getData('dream');
- if (dreamData) {
- var dream = JSON.parse(decodeURIComponent(dreamData));
- alert(dream.dreamId);
- }
- };
-
- function ondragenter(event) {
- event.preventDefault();
- };
-
- function ondragover(event) {
- event.preventDefault();
- };
-
- function ondragleave(event) {
-
- }
-
- drop.addEventListener('drop', ondrop);
- drop.addEventListener('dragenter', ondragenter);
- drop.addEventListener('dragover', ondragover);
- drop.addEventListener('dragleave', ondragleave);
- */
-};
diff --git a/invokeai/frontend/web/static/dream_web/test.html b/invokeai/frontend/web/static/dream_web/test.html
deleted file mode 100644
index cbb746a5a1..0000000000
--- a/invokeai/frontend/web/static/dream_web/test.html
+++ /dev/null
@@ -1,246 +0,0 @@
-
-
- InvokeAI Test
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/invokeai/frontend/web/static/legacy_web/favicon.ico b/invokeai/frontend/web/static/legacy_web/favicon.ico
deleted file mode 100644
index 51eb844a6a4a9d4b13e17e38b0fc915e7e97d4b5..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1150
zcmaiy%TE(g6vi*n1a-yAr5H_2eSt+l!2}h8?$p@n=nPJTglL%pit>^TL`+1D5hx&N
z)!<{Tc1e&lvO-)*Ow^TsgK$#zJKYFEA;2&@TN?6A5C9Q()1;lGF^Sd
zF~GSouqjvv->jVh^vZ3gw#sUXZQHSqR>WSmwCOtUf;BK6W$k#wMKX$aiq1TKiY)i0
zVAh_I80S)!qiamC2k7>K9QPINuKnap%uv%}j+#E^Jur4AXDJpbkvT6Ctz07yN&)Z7
znrGHFe)vUp?-<1^k5RnhDB0a3h^>+{H77oj<%hM0acGw^T{k?>wWp=8-IJ2<;2zkW
z55$XEACugh&R(wZ1^nba=DC(TD08@HP|IVZ?1<#7_S=$s)|_Dd@;ZI;mZvYT`CA{Y
z_Vq(y{pYvZf8ANnKfH$f+a32rZ=N(I_xgGd_x}n~fRYte5_cZWQRBiY+1KuqaiB`D
zuiiy$g`D(znbUIcklw#ZXiGqz&xFs
-
- Stable Diffusion Dream Server
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
✖
-
-

-
- Postprocessing...1/3
-
-
-
-
-
-
-
-