Spaces:
Runtime error
Runtime error
/** SD-UI Backend control and classes. | |
*/ | |
(function () { "use strict"; | |
const RETRY_DELAY_IF_BUFFER_IS_EMPTY = 1000 // ms | |
const RETRY_DELAY_IF_SERVER_IS_BUSY = 30 * 1000 // ms, status_code 503, already a task running | |
const RETRY_DELAY_ON_ERROR = 4000 // ms | |
const TASK_STATE_SERVER_UPDATE_DELAY = 1500 // ms | |
const SERVER_STATE_VALIDITY_DURATION = 90 * 1000 // ms - 90 seconds to allow ping to timeout more than once before killing tasks. | |
const HEALTH_PING_INTERVAL = 5000 // ms | |
const IDLE_COOLDOWN = 2500 // ms | |
const CONCURRENT_TASK_INTERVAL = 100 // ms | |
/** Connects to an endpoint and resumes connection after reaching end of stream until all data is received. | |
* Allows closing the connection while the server buffers more data. | |
*/ | |
class ChunkedStreamReader { | |
#bufferedString = '' // Data received waiting to be read. | |
#url | |
#fetchOptions | |
#response | |
constructor(url, initialContent='', options={}) { | |
if (typeof url !== 'string' && !(url instanceof String)) { | |
throw new Error('url is not a string.') | |
} | |
if (typeof initialContent !== 'undefined' && typeof initialContent !== 'string') { | |
throw new Error('initialContent is not a string.') | |
} | |
this.#bufferedString = initialContent | |
this.#url = url | |
this.#fetchOptions = Object.assign({ | |
headers: { | |
'Content-Type': 'application/json' | |
} | |
}, options) | |
this.onNext = undefined | |
} | |
get url() { | |
if (this.#response.redirected) { | |
return this.#response.url | |
} | |
return this.#url | |
} | |
get bufferedString() { | |
return this.#bufferedString | |
} | |
get status() { | |
this.#response?.status | |
} | |
get statusText() { | |
this.#response?.statusText | |
} | |
parse(value) { | |
if (typeof value === 'undefined') { | |
return | |
} | |
if (!isArrayOrTypedArray(value)) { | |
return [value] | |
} | |
if (value.length === 0) { | |
return value | |
} | |
if (typeof this.textDecoder === 'undefined') { | |
this.textDecoder = new TextDecoder() | |
} | |
return [this.textDecoder.decode(value)] | |
} | |
onComplete(value) { | |
return value | |
} | |
onError(response) { | |
throw new Error(response.statusText) | |
} | |
onNext({value, done}, response) { | |
return {value, done} | |
} | |
async *[Symbol.asyncIterator]() { | |
return this.open() | |
} | |
async *open() { | |
let value = undefined | |
let done = undefined | |
do { | |
if (this.#response) { | |
await asyncDelay(RETRY_DELAY_IF_BUFFER_IS_EMPTY) | |
} | |
this.#response = await fetch(this.#url, this.#fetchOptions) | |
if (!this.#response.ok) { | |
if (this.#response.status === 425) { | |
continue | |
} | |
// Request status indicate failure | |
console.warn('Stream %o stopped unexpectedly.', this.#response) | |
value = await Promise.resolve(this.onError(this.#response)) | |
if (typeof value === 'boolean' && value) { | |
continue | |
} | |
return value | |
} | |
const reader = this.#response.body.getReader() | |
done = false | |
do { | |
const readState = await reader.read() | |
value = this.parse(readState.value) | |
if (value) { | |
for(let sVal of value) { | |
({value: sVal, done} = await Promise.resolve(this.onNext({value:sVal, done:readState.done}))) | |
yield sVal | |
if (done) { | |
return this.onComplete(sVal) | |
} | |
} | |
} | |
if (done) { | |
return | |
} | |
} while(value && !done) | |
} while (!done && (this.#response.ok || this.#response.status === 425)) | |
} | |
*readStreamAsJSON(jsonStr, throwOnError) { | |
if (typeof jsonStr !== 'string') { | |
throw new Error('jsonStr is not a string.') | |
} | |
do { | |
if (this.#bufferedString.length > 0) { | |
// Append new data when required | |
if (jsonStr.length > 0) { | |
jsonStr = this.#bufferedString + jsonStr | |
} else { | |
jsonStr = this.#bufferedString | |
} | |
this.#bufferedString = '' | |
} | |
if (!jsonStr) { | |
return | |
} | |
// Find next delimiter | |
let lastChunkIdx = jsonStr.indexOf('}{') | |
if (lastChunkIdx >= 0) { | |
this.#bufferedString = jsonStr.substring(0, lastChunkIdx + 1) | |
jsonStr = jsonStr.substring(lastChunkIdx + 1) | |
} else { | |
this.#bufferedString = jsonStr | |
jsonStr = '' | |
} | |
if (this.#bufferedString.length <= 0) { | |
return | |
} | |
// hack for a middleman buffering all the streaming updates, and unleashing them on the poor browser in one shot. | |
// this results in having to parse JSON like {"step": 1}{"step": 2}{"step": 3}{"ste... | |
// which is obviously invalid and can happen at any point while rendering. | |
// So we need to extract only the next {} section | |
try { // Try to parse | |
const jsonObj = JSON.parse(this.#bufferedString) | |
this.#bufferedString = jsonStr | |
jsonStr = '' | |
yield jsonObj | |
} catch (e) { | |
if (throwOnError) { | |
console.error(`Parsing: "${this.#bufferedString}", Buffer: "${jsonStr}"`) | |
} | |
this.#bufferedString += jsonStr | |
if (e instanceof SyntaxError && !throwOnError) { | |
return | |
} | |
throw e | |
} | |
} while (this.#bufferedString.length > 0 && this.#bufferedString.indexOf('}') >= 0) | |
} | |
} | |
const EVENT_IDLE = 'idle' | |
const EVENT_STATUS_CHANGED = 'statusChange' | |
const EVENT_UNHANDLED_REJECTION = 'unhandledRejection' | |
const EVENT_TASK_QUEUED = 'taskQueued' | |
const EVENT_TASK_START = 'taskStart' | |
const EVENT_TASK_END = 'taskEnd' | |
const EVENT_TASK_ERROR = 'task_error' | |
const EVENT_UNEXPECTED_RESPONSE = 'unexpectedResponse' | |
const EVENTS_TYPES = [ | |
EVENT_IDLE, | |
EVENT_STATUS_CHANGED, | |
EVENT_UNHANDLED_REJECTION, | |
EVENT_TASK_QUEUED, | |
EVENT_TASK_START, | |
EVENT_TASK_END, | |
EVENT_TASK_ERROR, | |
EVENT_UNEXPECTED_RESPONSE, | |
] | |
Object.freeze(EVENTS_TYPES) | |
const eventSource = new GenericEventSource(EVENTS_TYPES) | |
function setServerStatus(msgType, msg) { | |
return eventSource.fireEvent(EVENT_STATUS_CHANGED, {type: msgType, message: msg}) | |
} | |
const ServerStates = { | |
init: 'Init', | |
loadingModel: 'LoadingModel', | |
online: 'Online', | |
rendering: 'Rendering', | |
unavailable: 'Unavailable', | |
} | |
Object.freeze(ServerStates) | |
let sessionId = Date.now() | |
let serverState = {'status': ServerStates.unavailable, 'time': Date.now()} | |
async function healthCheck() { | |
if (Date.now() < serverState.time + (HEALTH_PING_INTERVAL / 2) && isServerAvailable()) { | |
// Ping confirmed online less than half of HEALTH_PING_INTERVAL ago. | |
return true | |
} | |
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { | |
console.warn('WARNING! SERVER_STATE_VALIDITY_DURATION has elapsed since the last Ping completed.') | |
} | |
try { | |
let res = undefined | |
if (typeof sessionId !== 'undefined') { | |
res = await fetch('/ping?session_id=' + sessionId) | |
} else { | |
res = await fetch('/ping') | |
} | |
serverState = await res.json() | |
if (typeof serverState !== 'object' || typeof serverState.status !== 'string') { | |
console.error(`Server reply didn't contain a state value.`) | |
serverState = {'status': ServerStates.unavailable, 'time': Date.now()} | |
setServerStatus('error', 'offline') | |
return false | |
} | |
// Set status | |
switch(serverState.status) { | |
case ServerStates.init: | |
// Wait for init to complete before updating status. | |
break | |
case ServerStates.online: | |
setServerStatus('online', 'ready') | |
break | |
case ServerStates.loadingModel: | |
setServerStatus('busy', 'loading..') | |
break | |
case ServerStates.rendering: | |
setServerStatus('busy', 'rendering..') | |
break | |
default: // Unavailable | |
console.error('Ping received an unexpected server status. Status: %s', serverState.status) | |
setServerStatus('error', serverState.status.toLowerCase()) | |
break | |
} | |
serverState.time = Date.now() | |
return true | |
} catch (e) { | |
console.error(e) | |
serverState = {'status': ServerStates.unavailable, 'time': Date.now()} | |
setServerStatus('error', 'offline') | |
} | |
return false | |
} | |
function isServerAvailable() { | |
if (typeof serverState !== 'object') { | |
console.error('serverState not set to a value. Connection to server could be lost...') | |
return false | |
} | |
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { | |
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connection to server could be lost...') | |
return false | |
} | |
switch (serverState.status) { | |
case ServerStates.loadingModel: | |
case ServerStates.rendering: | |
case ServerStates.online: | |
return true | |
default: | |
console.warn('Unexpected server status. Server could be unavailable... Status: %s', serverState.status) | |
return false | |
} | |
} | |
async function waitUntil(isReadyFn, delay, timeout) { | |
if (typeof delay === 'number') { | |
const msDelay = delay | |
delay = () => asyncDelay(msDelay) | |
} | |
if (typeof delay !== 'function') { | |
throw new Error('delay is not a number or a function.') | |
} | |
if (typeof timeout !== 'undefined' && typeof timeout !== 'number') { | |
throw new Error('timeout is not a number.') | |
} | |
if (typeof timeout === 'undefined' || timeout < 0) { | |
timeout = Number.MAX_SAFE_INTEGER | |
} | |
timeout = Date.now() + timeout | |
while (timeout > Date.now() | |
&& Date.now() < serverState.time + SERVER_STATE_VALIDITY_DURATION | |
&& !Boolean(await Promise.resolve(isReadyFn())) | |
) { | |
await delay() | |
if (!isServerAvailable()) { // Can fail if ping got frozen/suspended... | |
if (await healthCheck() && isServerAvailable()) { // Force a recheck of server status before failure... | |
continue // Continue waiting if last healthCheck confirmed the server is still alive. | |
} | |
throw new Error('Connection with server lost.') | |
} | |
} | |
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { | |
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Released waitUntil on stale server state.') | |
} | |
} | |
const TaskStatus = { | |
init: 'init', | |
pending: 'pending', // Queued locally, not yet posted to server | |
waiting: 'waiting', // Waiting to run on server | |
processing: 'processing', | |
stopped: 'stopped', | |
completed: 'completed', | |
failed: 'failed', | |
} | |
Object.freeze(TaskStatus) | |
const TASK_STATUS_ORDER = [ | |
TaskStatus.init, | |
TaskStatus.pending, | |
TaskStatus.waiting, | |
TaskStatus.processing, | |
//Don't add status that are final. | |
] | |
const task_queue = new Map() | |
const concurrent_generators = new Map() | |
const weak_results = new WeakMap() | |
class Task { | |
// Private properties... | |
_reqBody = {} // request body of this task. | |
#reader = undefined | |
#status = TaskStatus.init | |
#id = undefined | |
#exception = undefined | |
constructor(options={}) { | |
this._reqBody = Object.assign({}, options) | |
if (typeof this._reqBody.session_id === 'undefined') { | |
this._reqBody.session_id = sessionId | |
} else if (this._reqBody.session_id !== SD.sessionId && String(this._reqBody.session_id) !== String(SD.sessionId)) { | |
throw new Error('Use SD.sessionId to set the request session_id.') | |
} | |
this._reqBody.session_id = String(this._reqBody.session_id) | |
} | |
get id() { | |
return this.#id | |
} | |
_setId(id) { | |
if (typeof this.#id !== 'undefined') { | |
throw new Error('The task ID can only be set once.') | |
} | |
this.#id = id | |
} | |
get exception() { | |
return this.#exception | |
} | |
async abort(exception) { | |
if (this.isCompleted || this.isStopped || this.hasFailed) { | |
return | |
} | |
if (typeof exception !== 'undefined') { | |
if (typeof exception === 'string') { | |
exception = new Error(exception) | |
} | |
if (typeof exception !== 'object') { | |
throw new Error('exception is not an object.') | |
} | |
if (!(exception instanceof Error)) { | |
throw new Error('exception is not an Error or a string.') | |
} | |
} | |
const res = await fetch('/image/stop?task=' + this.id) | |
if (!res.ok) { | |
console.log('Stop response:', res) | |
throw new Error(res.statusText) | |
} | |
task_queue.delete(this) | |
this.#exception = exception | |
this.#status = (exception ? TaskStatus.failed : TaskStatus.stopped) | |
} | |
get reqBody() { | |
if (this.#status === TaskStatus.init) { | |
return this._reqBody | |
} | |
console.warn('Task reqBody cannot be changed after the init state.') | |
return Object.assign({}, this._reqBody) | |
} | |
get isPending() { | |
return TASK_STATUS_ORDER.indexOf(this.#status) >= 0 | |
} | |
get isCompleted() { | |
return this.#status === TaskStatus.completed | |
} | |
get hasFailed() { | |
return this.#status === TaskStatus.failed | |
} | |
get isStopped() { | |
return this.#status === TaskStatus.stopped | |
} | |
get status() { | |
return this.#status | |
} | |
_setStatus(status) { | |
if (status === this.#status) { | |
return | |
} | |
const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status) | |
if (currentIdx < 0) { | |
throw Error(`The task status ${this.#status} is final and can't be changed.`) | |
} | |
const newIdx = TASK_STATUS_ORDER.indexOf(status) | |
if (newIdx >= 0 && newIdx < currentIdx) { | |
throw Error(`The task status ${status} can't replace ${this.#status}.`) | |
} | |
this.#status = status | |
} | |
/** Send current task to server. | |
* @param {*} [timeout=-1] Optional timeout value in ms | |
* @returns the response from the render request. | |
* @memberof Task | |
*/ | |
async post(url, timeout=-1) { | |
if(this.status !== TaskStatus.init && this.status !== TaskStatus.pending) { | |
throw new Error(`Task status ${this.status} is not valid for post.`) | |
} | |
this._setStatus(TaskStatus.pending) | |
Object.freeze(this._reqBody) | |
const abortSignal = (timeout >= 0 ? AbortSignal.timeout(timeout) : undefined) | |
let res = undefined | |
try { | |
this.checkReqBody() | |
do { | |
abortSignal?.throwIfAborted() | |
res = await fetch(url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(this._reqBody), | |
signal: abortSignal | |
}) | |
// status_code 503, already a task running. | |
} while (res.status === 503 && await asyncDelay(RETRY_DELAY_IF_SERVER_IS_BUSY)) | |
} catch (err) { | |
this.abort(err) | |
throw err | |
} | |
if (!res.ok) { | |
const err = new Error(`Unexpected response HTTP${res.status}. Details: ${res.statusText}`) | |
this.abort(err) | |
throw err | |
} | |
return await res.json() | |
} | |
static getReader(url) { | |
const reader = new ChunkedStreamReader(url) | |
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}) { | |
// By default is completed when the return value has a status defined. | |
if (typeof value === 'object' && 'status' in value) { | |
done = true | |
} | |
return {done, value} | |
} | |
return reader | |
} | |
_setReader(reader) { | |
if (typeof this.#reader !== 'undefined') { | |
throw new Error('The task reader can only be set once.') | |
} | |
this.#reader = reader | |
} | |
get reader() { | |
if (this.#reader) { | |
return this.#reader | |
} | |
if (!this.streamUrl) { | |
throw new Error('The task has no stream Url defined.') | |
} | |
this.#reader = Task.getReader(this.streamUrl) | |
const task = this | |
const onNext = this.#reader.onNext | |
this.#reader.onNext = function({done, value}) { | |
if (value && typeof value === 'object') { | |
if (task.status === TaskStatus.init | |
|| task.status === TaskStatus.pending | |
|| task.status === TaskStatus.waiting | |
) { | |
task._setStatus(TaskStatus.processing) | |
} | |
if ('step' in value && 'total_steps' in value) { | |
task.step = value.step | |
task.total_steps = value.total_steps | |
} | |
} | |
return onNext.call(this, {done, value}) | |
} | |
this.#reader.onComplete = function(value) { | |
task.result = value | |
if (task.isPending) { | |
task._setStatus(TaskStatus.completed) | |
} | |
return value | |
} | |
this.#reader.onError = function(response) { | |
const err = new Error(response.statusText) | |
task.abort(err) | |
throw err | |
} | |
return this.#reader | |
} | |
async waitUntil({timeout=-1, callback, status, signal}) { | |
const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status) | |
if (currentIdx <= 0) { | |
return false | |
} | |
const stIdx = (status ? TASK_STATUS_ORDER.indexOf(status) : currentIdx + 1) | |
if (stIdx >= 0 && stIdx <= currentIdx) { | |
return true | |
} | |
if (stIdx < 0 && currentIdx < 0) { | |
return this.#status === (status || TaskStatus.completed) | |
} | |
if (signal?.aborted) { | |
return false | |
} | |
const task = this | |
switch(this.#status) { | |
case TaskStatus.pending: | |
case TaskStatus.waiting: | |
// Wait for server status to include this task. | |
await waitUntil( | |
async () => { | |
if (task.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) { | |
return true | |
} | |
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { | |
return true | |
} | |
}, | |
TASK_STATE_SERVER_UPDATE_DELAY, | |
timeout, | |
) | |
if (this.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) { | |
this._setStatus(TaskStatus.waiting) | |
} | |
if (await Promise.resolve(callback?.call(this)) || signal?.aborted) { | |
return false | |
} | |
if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.waiting)) { | |
return true | |
} | |
// Wait for task to start on server. | |
await waitUntil( | |
async () => { | |
if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'pending') { | |
return true | |
} | |
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { | |
return true | |
} | |
}, | |
TASK_STATE_SERVER_UPDATE_DELAY, | |
timeout, | |
) | |
const state = (typeof serverState.tasks === 'object' ? serverState.tasks[String(task.#id)] : undefined) | |
if (state === 'running' || state === 'buffer' || state === 'completed') { | |
this._setStatus(TaskStatus.processing) | |
} | |
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { | |
return false | |
} | |
if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.processing)) { | |
return true | |
} | |
case TaskStatus.processing: | |
await waitUntil( | |
async () => { | |
if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'running') { | |
return true | |
} | |
if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { | |
return true | |
} | |
}, | |
TASK_STATE_SERVER_UPDATE_DELAY, | |
timeout, | |
) | |
await Promise.resolve(callback?.call(this)) | |
default: | |
return this.#status === (status || TaskStatus.completed) | |
} | |
} | |
async enqueue(promiseGenerator, ...args) { | |
if (this.status !== TaskStatus.init) { | |
throw new Error(`Task is in an invalid status ${this.status} to add to queue.`) | |
} | |
this._setStatus(TaskStatus.pending) | |
task_queue.set(this, promiseGenerator) | |
await eventSource.fireEvent(EVENT_TASK_QUEUED, {task:this}) | |
await Task.enqueue(promiseGenerator, ...args) | |
await this.waitUntil({status: TaskStatus.completed}) | |
if (this.exception) { | |
throw this.exception | |
} | |
return this.result | |
} | |
static async enqueue(promiseGenerator, ...args) { | |
if (typeof promiseGenerator === 'undefined') { | |
throw new Error('To enqueue a concurrent task, a *Promise Generator is needed but undefined was found.') | |
} | |
//if (Symbol.asyncIterator in result || Symbol.iterator in result) { | |
//concurrent_generators.set(result, Promise.resolve(args)) | |
if (typeof promiseGenerator === 'function') { | |
concurrent_generators.set(asGenerator({callback: promiseGenerator}), Promise.resolve(args)) | |
} else { | |
concurrent_generators.set(promiseGenerator, Promise.resolve(args)) | |
} | |
await waitUntil(() => !concurrent_generators.has(promiseGenerator), CONCURRENT_TASK_INTERVAL) | |
return weak_results.get(promiseGenerator) | |
} | |
static enqueueNew(task, classCtor, progressCallback) { | |
if (task.status !== TaskStatus.init) { | |
throw new Error('Task has an invalid status to add to queue.') | |
} | |
if (!(task instanceof classCtor)) { | |
throw new Error('Task is not a instance of classCtor.') | |
} | |
let promiseGenerator = undefined | |
if (typeof progressCallback === 'undefined') { | |
promiseGenerator = classCtor.start(task) | |
} else if (typeof progressCallback === 'function') { | |
promiseGenerator = classCtor.start(task, progressCallback) | |
} else { | |
throw new Error('progressCallback is not a function.') | |
} | |
return Task.prototype.enqueue.call(task, promiseGenerator) | |
} | |
static async run(promiseGenerator, {callback, signal, timeout=-1}={}) { | |
let value = undefined | |
let done = undefined | |
if (timeout < 0) { | |
timeout = Number.MAX_SAFE_INTEGER | |
} | |
timeout = Date.now() + timeout | |
do { | |
({value, done} = await Promise.resolve(promiseGenerator.next(value))) | |
if (value instanceof Promise) { | |
value = await value | |
} | |
if (callback) { | |
({value, done} = await Promise.resolve(callback.call(promiseGenerator, {value, done}))) | |
} | |
if (value instanceof Promise) { | |
value = await value | |
} | |
} while(!done && !signal?.aborted && timeout > Date.now()) | |
return value | |
} | |
static async *asGenerator({callback, generator, signal, timeout=-1}={}) { | |
let value = undefined | |
let done = undefined | |
if (timeout < 0) { | |
timeout = Number.MAX_SAFE_INTEGER | |
} | |
timeout = Date.now() + timeout | |
do { | |
({value, done} = await Promise.resolve(generator.next(value))) | |
if (value instanceof Promise) { | |
value = await value | |
} | |
if (callback) { | |
({value, done} = await Promise.resolve(callback.call(generator, {value, done}))) | |
if (value instanceof Promise) { | |
value = await value | |
} | |
} | |
value = yield value | |
} while(!done && !signal?.aborted && timeout > Date.now()) | |
return value | |
} | |
} | |
const TASK_REQUIRED = { | |
"session_id": 'string', | |
"prompt": 'string', | |
"negative_prompt": 'string', | |
"width": 'number', | |
"height": 'number', | |
"seed": 'number', | |
"sampler_name": 'string', | |
"use_stable_diffusion_model": 'string', | |
"num_inference_steps": 'number', | |
"guidance_scale": 'number', | |
"num_outputs": 'number', | |
"stream_progress_updates": 'boolean', | |
"stream_image_progress": 'boolean', | |
"show_only_filtered_image": 'boolean', | |
"output_format": 'string', | |
"output_quality": 'number', | |
} | |
const TASK_DEFAULTS = { | |
"sampler_name": "plms", | |
"use_stable_diffusion_model": "sd-v1-4", | |
"num_inference_steps": 50, | |
"guidance_scale": 7.5, | |
"negative_prompt": "", | |
"num_outputs": 1, | |
"stream_progress_updates": true, | |
"stream_image_progress": true, | |
"show_only_filtered_image": true, | |
"block_nsfw": false, | |
"output_format": "png", | |
"output_quality": 75, | |
} | |
const TASK_OPTIONAL = { | |
"device": 'string', | |
"init_image": 'string', | |
"mask": 'string', | |
"save_to_disk_path": 'string', | |
"use_face_correction": 'string', | |
"use_upscale": 'string', | |
"use_vae_model": 'string', | |
"use_hypernetwork_model": 'string', | |
"hypernetwork_strength": 'number', | |
} | |
// Higer values will result in... | |
// pytorch_lightning/utilities/seed.py:60: UserWarning: X is not in bounds, numpy accepts from 0 to 4294967295 | |
const MAX_SEED_VALUE = 4294967295 | |
class RenderTask extends Task { | |
constructor(options={}) { | |
super(options) | |
if (typeof this._reqBody.seed === 'undefined') { | |
this._reqBody.seed = Math.floor(Math.random() * (MAX_SEED_VALUE + 1)) | |
} | |
if (typeof typeof this._reqBody.seed === 'number' && (this._reqBody.seed > MAX_SEED_VALUE || this._reqBody.seed < 0)) { | |
throw new Error(`seed must be in range 0 to ${MAX_SEED_VALUE}.`) | |
} | |
if ('use_cpu' in this._reqBody) { | |
if (this._reqBody.use_cpu) { | |
this._reqBody.device = 'cpu' | |
} | |
delete this._reqBody.use_cpu | |
} | |
if (this._reqBody.init_image) { | |
if (typeof this._reqBody.prompt_strength === 'undefined') { | |
this._reqBody.prompt_strength = 0.8 | |
} else if (typeof this._reqBody.prompt_strength !== 'number') { | |
throw new Error(`prompt_strength need to be of type number but ${typeof this._reqBody.prompt_strength} was found.`) | |
} | |
} | |
if ('modifiers' in this._reqBody) { | |
if (Array.isArray(this._reqBody.modifiers) && this._reqBody.modifiers.length > 0) { | |
this._reqBody.modifiers = this._reqBody.modifiers.filter((val) => val.trim()) | |
if (this._reqBody.modifiers.length > 0) { | |
this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers.join(', ')}` | |
} | |
} | |
if (typeof this._reqBody.modifiers === 'string' && this._reqBody.modifiers.length > 0) { | |
this._reqBody.modifiers = this._reqBody.modifiers.trim() | |
if (this._reqBody.modifiers.length > 0) { | |
this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers}` | |
} | |
} | |
delete this._reqBody.modifiers | |
} | |
this.checkReqBody() | |
} | |
checkReqBody() { | |
for (const key in TASK_DEFAULTS) { | |
if (typeof this._reqBody[key] === 'undefined') { | |
this._reqBody[key] = TASK_DEFAULTS[key] | |
} | |
} | |
for (const key in TASK_REQUIRED) { | |
if (typeof this._reqBody[key] !== TASK_REQUIRED[key]) { | |
throw new Error(`${key} need to be of type ${TASK_REQUIRED[key]} but ${typeof this._reqBody[key]} was found.`) | |
} | |
} | |
for (const key in this._reqBody) { | |
if (key in TASK_REQUIRED) { | |
continue | |
} | |
if (key in TASK_OPTIONAL) { | |
if (typeof this._reqBody[key] == "undefined") { | |
delete this._reqBody[key] | |
console.warn(`reqBody[${key}] was set to undefined. Removing optional key without value...`) | |
continue | |
} | |
if (typeof this._reqBody[key] !== TASK_OPTIONAL[key]) { | |
throw new Error(`${key} need to be of type ${TASK_OPTIONAL[key]} but ${typeof this._reqBody[key]} was found.`) | |
} | |
} | |
} | |
} | |
/** Send current task to server. | |
* @param {*} [timeout=-1] Optional timeout value in ms | |
* @returns the response from the render request. | |
* @memberof Task | |
*/ | |
async post(timeout=-1) { | |
performance.mark('make-render-request') | |
if (performance.getEntriesByName('click-makeImage', 'mark').length > 0) { | |
performance.measure('diff', 'click-makeImage', 'make-render-request') | |
console.log('delay between clicking and making the server request:', performance.getEntriesByName('diff', 'measure')[0].duration + ' ms') | |
} | |
let jsonResponse = await super.post('/render', timeout) | |
if (typeof jsonResponse?.task !== 'number') { | |
console.warn('Endpoint error response: ', jsonResponse) | |
const event = Object.assign({task:this}, jsonResponse) | |
await eventSource.fireEvent(EVENT_UNEXPECTED_RESPONSE, event) | |
if ('continueWith' in event) { | |
jsonResponse = await Promise.resolve(event.continueWith) | |
} | |
if (typeof jsonResponse?.task !== 'number') { | |
const err = new Error(jsonResponse?.detail || 'Endpoint response does not contains a task ID.') | |
this.abort(err) | |
throw err | |
} | |
} | |
this._setId(jsonResponse.task) | |
if (jsonResponse.stream) { | |
this.streamUrl = jsonResponse.stream | |
} | |
this._setStatus(TaskStatus.waiting) | |
return jsonResponse | |
} | |
enqueue(progressCallback) { | |
return Task.enqueueNew(this, RenderTask, progressCallback) | |
} | |
*start(progressCallback) { | |
if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') { | |
throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback) | |
} | |
if (this.isStopped) { | |
return | |
} | |
this._setStatus(TaskStatus.pending) | |
progressCallback?.call(this, {reqBody: this._reqBody}) | |
Object.freeze(this._reqBody) | |
// Post task request to backend | |
let renderRequest = undefined | |
try { | |
renderRequest = yield this.post() | |
yield progressCallback?.call(this, {renderResponse: renderRequest}) | |
} catch (e) { | |
yield progressCallback?.call(this, { detail: e.message }) | |
throw e | |
} | |
try { // Wait for task to start on server. | |
yield this.waitUntil({ | |
callback: function() { return progressCallback?.call(this, {}) }, | |
status: TaskStatus.processing, | |
}) | |
} catch (e) { | |
this.abort(err) | |
throw e | |
} | |
// Update class status and callback. | |
const taskState = (typeof serverState.tasks === 'object' ? serverState.tasks[String(this.id)] : undefined) | |
switch(taskState) { | |
case 'pending': // Session has pending tasks. | |
console.error('Server %o render request %o is still waiting.', serverState, renderRequest) | |
//Only update status if not already set by waitUntil | |
if (this.status === TaskStatus.init | |
|| this.status === TaskStatus.pending | |
) { | |
// Set status as Waiting in backend. | |
this._setStatus(TaskStatus.waiting) | |
} | |
break | |
case 'running': | |
case 'buffer': | |
// Normal expected messages. | |
this._setStatus(TaskStatus.processing) | |
break | |
case 'completed': | |
if (this.isPending) { | |
// Set state to processing until we read the reply | |
this._setStatus(TaskStatus.processing) | |
} | |
console.warn('Server %o render request %o completed unexpectedly', serverState, renderRequest) | |
break // Continue anyway to try to read cached result. | |
case 'error': | |
this._setStatus(TaskStatus.failed) | |
console.error('Server %o render request %o has failed', serverState, renderRequest) | |
break // Still valid, Update UI with error message | |
case 'stopped': | |
this._setStatus(TaskStatus.stopped) | |
console.log('Server %o render request %o was stopped', serverState, renderRequest) | |
return false | |
default: | |
if (!progressCallback) { | |
const err = new Error('Unexpected server task state: ' + taskState || 'Undefined') | |
this.abort(err) | |
throw err | |
} | |
const response = yield progressCallback.call(this, {}) | |
if (response instanceof Error) { | |
this.abort(response) | |
throw response | |
} | |
if (!response) { | |
return false | |
} | |
} | |
// Task started! | |
// Open the reader. | |
const reader = this.reader | |
const task = this | |
reader.onError = function(response) { | |
if (progressCallback) { | |
task.abort(new Error(response.statusText)) | |
return progressCallback.call(task, { response, reader }) | |
} | |
return Task.prototype.onError.call(task, response) | |
} | |
yield progressCallback?.call(this, { reader }) | |
//Start streaming the results. | |
const streamGenerator = reader.open() | |
let value = undefined | |
let done = undefined | |
yield progressCallback?.call(this, { stream: streamGenerator }) | |
do { | |
({value, done} = yield streamGenerator.next()) | |
if (typeof value !== 'object') { | |
continue | |
} | |
yield progressCallback?.call(this, { update: value }) | |
} while(!done) | |
return value | |
} | |
static start(task, progressCallback) { | |
if (typeof task !== 'object') { | |
throw new Error ('task is not an object. task type: ' + typeof task) | |
} | |
if (!(task instanceof Task)) { | |
if (task.reqBody) { | |
task = new RenderTask(task.reqBody) | |
} else { | |
task = new RenderTask(task) | |
} | |
} | |
return task.start(progressCallback) | |
} | |
static run(task, progressCallback) { | |
const promiseGenerator = RenderTask.start(task, progressCallback) | |
return Task.run(promiseGenerator) | |
} | |
} | |
class FilterTask extends Task { | |
constructor(options={}) { | |
} | |
/** Send current task to server. | |
* @param {*} [timeout=-1] Optional timeout value in ms | |
* @returns the response from the render request. | |
* @memberof Task | |
*/ | |
async post(timeout=-1) { | |
let jsonResponse = await super.post('/filter', timeout) | |
//this._setId(jsonResponse.task) | |
this._setStatus(TaskStatus.waiting) | |
} | |
enqueue(progressCallback) { | |
return Task.enqueueNew(this, FilterTask, progressCallback) | |
} | |
*start(progressCallback) { | |
if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') { | |
throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback) | |
} | |
if (this.isStopped) { | |
return | |
} | |
} | |
static start(task, progressCallback) { | |
if (typeof task !== 'object') { | |
throw new Error ('task is not an object. task type: ' + typeof task) | |
} | |
if (!(task instanceof Task)) { | |
if (task.reqBody) { | |
task = new FilterTask(task.reqBody) | |
} else { | |
task = new FilterTask(task) | |
} | |
} | |
return task.start(progressCallback) | |
} | |
static run(task, progressCallback) { | |
const promiseGenerator = FilterTask.start(task, progressCallback) | |
return Task.run(promiseGenerator) | |
} | |
} | |
const getSystemInfo = debounce(async function() { | |
let systemInfo = { | |
devices: { | |
all: {}, | |
active: {}, | |
}, | |
hosts: [] | |
} | |
try { | |
const res = await fetch('/get/system_info') | |
if (!res.ok) { | |
console.error('Invalid response fetching devices', res.statusText) | |
return systemInfo | |
} | |
systemInfo = await res.json() | |
} catch (e) { | |
console.error('error fetching system info', e) | |
} | |
return systemInfo | |
}, 250, true) | |
async function getDevices() { | |
let systemInfo = getSystemInfo() | |
return systemInfo.devices | |
} | |
async function getHosts() { | |
let systemInfo = getSystemInfo() | |
return systemInfo.hosts | |
} | |
async function getModels() { | |
let models = { | |
'stable-diffusion': [], | |
'vae': [], | |
} | |
try { | |
const res = await fetch('/get/models') | |
if (!res.ok) { | |
console.error('Invalid response fetching models', res.statusText) | |
return models | |
} | |
models = await res.json() | |
console.log('get models response', models) | |
} catch (e) { | |
console.log('get models error', e) | |
} | |
return models | |
} | |
function getServerCapacity() { | |
let activeDevicesCount = Object.keys(serverState?.devices?.active || {}).length | |
if (typeof window === "object" && window.document.visibilityState === 'hidden') { | |
activeDevicesCount = 1 + activeDevicesCount | |
} | |
return activeDevicesCount | |
} | |
let idleEventPromise = undefined | |
function continueTasks() { | |
if (typeof navigator?.scheduling?.isInputPending === 'function') { | |
const inputPendingOptions = { | |
// Report mouse/pointer move events when queue is empty. | |
// Delay idle after mouse moves stops. | |
includeContinuous: Boolean(task_queue.size <= 0 && concurrent_generators.size <= 0) | |
} | |
if (navigator.scheduling.isInputPending(inputPendingOptions)) { | |
// Browser/User still active. | |
return asyncDelay(CONCURRENT_TASK_INTERVAL) | |
} | |
} | |
const serverCapacity = getServerCapacity() | |
if (task_queue.size <= 0 && concurrent_generators.size <= 0) { | |
if (!idleEventPromise?.isPending) { | |
idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity, idle: true})) | |
} | |
// Calling idle could result in task being added to queue. | |
// if (task_queue.size <= 0 && concurrent_generators.size <= 0) { | |
// return asyncDelay(IDLE_COOLDOWN).then(() => idleEventPromise) | |
// } | |
} | |
if (task_queue.size < serverCapacity) { | |
if (!idleEventPromise?.isPending) { | |
idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity - task_queue.size})) | |
} | |
} | |
const completedTasks = [] | |
for (let [generator, promise] of concurrent_generators.entries()) { | |
if (promise.isPending) { | |
continue | |
} | |
let value = promise.resolvedValue?.value || promise.resolvedValue | |
if (promise.isRejected) { | |
console.error(promise.rejectReason) | |
const event = {generator, reason: promise.rejectReason} | |
eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, event) | |
if ('continueWith' in event) { | |
value = Promise.resolve(event.continueWith) | |
} else { | |
concurrent_generators.delete(generator) | |
completedTasks.push({generator, promise}) | |
continue | |
} | |
} | |
if (value instanceof Promise) { | |
promise = makeQuerablePromise(value.then((val) => ({done: promise.resolvedValue?.done, value: val}))) | |
concurrent_generators.set(generator, promise) | |
continue | |
} | |
weak_results.set(generator, value) | |
if (promise.resolvedValue?.done) { | |
concurrent_generators.delete(generator) | |
completedTasks.push({generator, promise}) | |
continue | |
} | |
promise = generator.next(value) | |
if (!(promise instanceof Promise)) { | |
promise = Promise.resolve(promise) | |
} | |
promise = makeQuerablePromise(promise) | |
concurrent_generators.set(generator, promise) | |
} | |
for (let [task, generator] of task_queue.entries()) { | |
const cTsk = completedTasks.find((item) => item.generator === generator) | |
if (cTsk?.promise?.rejectReason || task.hasFailed) { | |
eventSource.fireEvent(EVENT_TASK_ERROR, {task, generator, reason: cTsk?.promise?.rejectReason || task.exception }) | |
task_queue.delete(task) | |
continue | |
} | |
if (task.isCompleted || task.isStopped || cTsk) { | |
const eventEndArgs = {task, generator} | |
if (task.isStopped) { | |
eventEndArgs.stopped = true | |
} | |
eventSource.fireEvent(EVENT_TASK_END, eventEndArgs) | |
task_queue.delete(task) | |
continue | |
} | |
if (concurrent_generators.size > serverCapacity) { | |
break | |
} | |
if (!generator) { | |
if (typeof task.start === 'function') { | |
generator = task.start() | |
} | |
} else if (concurrent_generators.has(generator)) { | |
continue | |
} | |
const event = {task, generator}; | |
const beforeStart = eventSource.fireEvent(EVENT_TASK_START, event) // optional beforeStart promise to wait on before starting task. | |
const promise = makeQuerablePromise(beforeStart.then(() => Promise.resolve(event.beforeStart))) | |
concurrent_generators.set(event.generator, promise) | |
task_queue.set(task, event.generator) | |
} | |
const promises = Array.from(concurrent_generators.values()) | |
if (promises.length <= 0) { | |
return asyncDelay(CONCURRENT_TASK_INTERVAL) | |
} | |
return Promise.race(promises).finally(continueTasks) | |
} | |
let taskPromise = undefined | |
function startCheck() { | |
if (taskPromise?.isPending) { | |
return | |
} | |
do { | |
if (taskPromise?.resolvedValue instanceof Promise) { | |
taskPromise = makeQuerablePromise(taskPromise.resolvedValue) | |
continue | |
} | |
if (typeof navigator?.scheduling?.isInputPending === 'function' && navigator.scheduling.isInputPending()) { | |
return | |
} | |
const continuePromise = continueTasks().catch(async function(err) { | |
console.error(err) | |
await eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, {reason: err}) | |
await asyncDelay(RETRY_DELAY_ON_ERROR) | |
}) | |
taskPromise = makeQuerablePromise(continuePromise) | |
} while(taskPromise?.isResolved) | |
} | |
const SD = { | |
ChunkedStreamReader, | |
ServerStates, | |
TaskStatus, | |
Task, | |
RenderTask, | |
FilterTask, | |
Events: EVENTS_TYPES, | |
init: async function(options={}) { | |
if ('events' in options) { | |
for (const key in options.events) { | |
eventSource.addEventListener(key, options.events[key]) | |
} | |
} | |
await healthCheck() | |
setInterval(healthCheck, HEALTH_PING_INTERVAL) | |
setInterval(startCheck, CONCURRENT_TASK_INTERVAL) | |
}, | |
/** Add a new event listener | |
*/ | |
addEventListener: (...args) => eventSource.addEventListener(...args), | |
/** Remove the event listener | |
*/ | |
removeEventListener: (...args) => eventSource.removeEventListener(...args), | |
isServerAvailable, | |
getServerCapacity, | |
getSystemInfo, | |
getDevices, | |
getHosts, | |
getModels, | |
render: (...args) => RenderTask.run(...args), | |
filter: (...args) => FilterTask.run(...args), | |
waitUntil, | |
}; | |
Object.defineProperties(SD, { | |
serverState: { | |
configurable: false, | |
get: () => serverState, | |
}, | |
isAvailable: { | |
configurable: false, | |
get: () => isServerAvailable(), | |
}, | |
serverCapacity: { | |
configurable: false, | |
get: () => getServerCapacity(), | |
}, | |
sessionId: { | |
configurable: false, | |
get: () => sessionId, | |
set: (val) => { | |
if (typeof val === 'undefined') { | |
throw new Error("Can't set sessionId to undefined.") | |
} | |
sessionId = val | |
}, | |
}, | |
MAX_SEED_VALUE: { | |
configurable: false, | |
get: () => MAX_SEED_VALUE, | |
}, | |
activeTasks: { | |
configurable: false, | |
get: () => task_queue, | |
}, | |
}) | |
Object.defineProperties(getGlobal(), { | |
SD: { | |
configurable: false, | |
get: () => SD, | |
}, | |
sessionId: { //TODO Remove in the future in favor of SD.sessionId | |
configurable: false, | |
get: () => { | |
console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.') | |
console.trace() | |
return SD.sessionId | |
}, | |
set: (val) => { | |
console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.') | |
console.trace() | |
SD.sessionId = val | |
} | |
} | |
}) | |
})() | |