Spaces:
Running
Running
import fs from 'node:fs'; | |
import path from 'node:path'; | |
import express from 'express'; | |
import fetch from 'node-fetch'; | |
import sanitize from 'sanitize-filename'; | |
import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |
import FormData from 'form-data'; | |
import urlJoin from 'url-join'; | |
import _ from 'lodash'; | |
import { delay, getBasicAuthHeader, tryParse } from '../util.js'; | |
import { readSecret, SECRET_KEYS } from './secrets.js'; | |
import { AIMLAPI_HEADERS } from '../constants.js'; | |
/** | |
* Gets the comfy workflows. | |
* @param {import('../users.js').UserDirectoryList} directories | |
* @returns {string[]} List of comfy workflows | |
*/ | |
function getComfyWorkflows(directories) { | |
return fs | |
.readdirSync(directories.comfyWorkflows) | |
.filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json')) | |
.sort(Intl.Collator().compare); | |
} | |
export const router = express.Router(); | |
router.post('/ping', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/options'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
return response.sendStatus(200); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/upscalers', async (request, response) => { | |
try { | |
async function getUpscalerModels() { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/upscalers'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
return data.map(x => x.name); | |
} | |
async function getLatentUpscalers() { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/latent-upscale-modes'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
return data.map(x => x.name); | |
} | |
const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]); | |
// 0 = None, then Latent Upscalers, then Upscalers | |
upscalers.splice(1, 0, ...latentUpscalers); | |
return response.send(upscalers); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/vaes', async (request, response) => { | |
try { | |
const autoUrl = new URL(request.body.url); | |
autoUrl.pathname = '/sdapi/v1/sd-vae'; | |
const forgeUrl = new URL(request.body.url); | |
forgeUrl.pathname = '/sdapi/v1/sd-modules'; | |
const requestInit = { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}; | |
const results = await Promise.allSettled([ | |
fetch(autoUrl, requestInit).then(r => r.ok ? r.json() : Promise.reject(r.statusText)), | |
fetch(forgeUrl, requestInit).then(r => r.ok ? r.json() : Promise.reject(r.statusText)), | |
]); | |
const data = results.find(r => r.status === 'fulfilled')?.value; | |
if (!Array.isArray(data)) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
const names = data.map(x => x.model_name); | |
return response.send(names); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/samplers', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/samplers'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const names = data.map(x => x.name); | |
return response.send(names); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/schedulers', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/schedulers'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const names = data.map(x => x.name); | |
return response.send(names); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/models', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/sd-models'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const models = data.map(x => ({ value: x.title, text: x.title })); | |
return response.send(models); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/get-model', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/options'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data['sd_model_checkpoint']); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/set-model', async (request, response) => { | |
try { | |
async function getProgress() { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/progress'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
return await result.json(); | |
} | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/options'; | |
const options = { | |
sd_model_checkpoint: request.body.model, | |
}; | |
const result = await fetch(url, { | |
method: 'POST', | |
body: JSON.stringify(options), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
const MAX_ATTEMPTS = 10; | |
const CHECK_INTERVAL = 2000; | |
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { | |
/** @type {any} */ | |
const progressState = await getProgress(); | |
const progress = progressState['progress']; | |
const jobCount = progressState['state']['job_count']; | |
if (progress === 0.0 && jobCount === 0) { | |
break; | |
} | |
console.info(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`); | |
await delay(CHECK_INTERVAL); | |
} | |
return response.sendStatus(200); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/generate', async (request, response) => { | |
try { | |
try { | |
const optionsUrl = new URL(request.body.url); | |
optionsUrl.pathname = '/sdapi/v1/options'; | |
const optionsResult = await fetch(optionsUrl, { headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } }); | |
if (optionsResult.ok) { | |
const optionsData = /** @type {any} */ (await optionsResult.json()); | |
const isForge = 'forge_preset' in optionsData; | |
if (!isForge) { | |
_.unset(request.body, 'override_settings.forge_additional_modules'); | |
} | |
} | |
} catch (error) { | |
console.error('SD WebUI failed to get options:', error); | |
} | |
const controller = new AbortController(); | |
request.socket.removeAllListeners('close'); | |
request.socket.on('close', function () { | |
if (!response.writableEnded) { | |
const interruptUrl = new URL(request.body.url); | |
interruptUrl.pathname = '/sdapi/v1/interrupt'; | |
fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } }); | |
} | |
controller.abort(); | |
}); | |
console.debug('SD WebUI request:', request.body); | |
const txt2imgUrl = new URL(request.body.url); | |
txt2imgUrl.pathname = '/sdapi/v1/txt2img'; | |
const result = await fetch(txt2imgUrl, { | |
method: 'POST', | |
body: JSON.stringify(request.body), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
signal: controller.signal, | |
}); | |
if (!result.ok) { | |
const text = await result.text(); | |
throw new Error('SD WebUI returned an error.', { cause: text }); | |
} | |
const data = await result.json(); | |
return response.send(data); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
router.post('/sd-next/upscalers', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/upscalers'; | |
const result = await fetch(url, { | |
method: 'GET', | |
headers: { | |
'Authorization': getBasicAuthHeader(request.body.auth), | |
}, | |
}); | |
if (!result.ok) { | |
throw new Error('SD WebUI returned an error.'); | |
} | |
// Vlad doesn't provide Latent Upscalers in the API, so we have to hardcode them here | |
const latentUpscalers = ['Latent', 'Latent (antialiased)', 'Latent (bicubic)', 'Latent (bicubic antialiased)', 'Latent (nearest)', 'Latent (nearest-exact)']; | |
/** @type {any} */ | |
const data = await result.json(); | |
const names = data.map(x => x.name); | |
// 0 = None, then Latent Upscalers, then Upscalers | |
names.splice(1, 0, ...latentUpscalers); | |
return response.send(names); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const comfy = express.Router(); | |
comfy.post('/ping', async (request, response) => { | |
try { | |
const url = new URL(urlJoin(request.body.url, '/system_stats')); | |
const result = await fetch(url); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
return response.sendStatus(200); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/samplers', async (request, response) => { | |
try { | |
const url = new URL(urlJoin(request.body.url, '/object_info')); | |
const result = await fetch(url); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data.KSampler.input.required.sampler_name[0]); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/models', async (request, response) => { | |
try { | |
const url = new URL(urlJoin(request.body.url, '/object_info')); | |
const result = await fetch(url); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const ckpts = data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })) || []; | |
const unets = data.UNETLoader.input.required.unet_name[0].map(it => ({ value: it, text: `UNet: ${it}` })) || []; | |
// load list of GGUF unets from diffusion_models if the loader node is available | |
const ggufs = data.UnetLoaderGGUF?.input.required.unet_name[0].map(it => ({ value: it, text: `GGUF: ${it}` })) || []; | |
const models = [...ckpts, ...unets, ...ggufs]; | |
// make the display names of the models somewhat presentable | |
models.forEach(it => it.text = it.text.replace(/\.[^.]*$/, '').replace(/_/g, ' ')); | |
return response.send(models); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/schedulers', async (request, response) => { | |
try { | |
const url = new URL(urlJoin(request.body.url, '/object_info')); | |
const result = await fetch(url); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data.KSampler.input.required.scheduler[0]); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/vaes', async (request, response) => { | |
try { | |
const url = new URL(urlJoin(request.body.url, '/object_info')); | |
const result = await fetch(url); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data.VAELoader.input.required.vae_name[0]); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/workflows', async (request, response) => { | |
try { | |
const data = getComfyWorkflows(request.user.directories); | |
return response.send(data); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/workflow', async (request, response) => { | |
try { | |
let filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name))); | |
if (!fs.existsSync(filePath)) { | |
filePath = path.join(request.user.directories.comfyWorkflows, 'Default_Comfy_Workflow.json'); | |
} | |
const data = fs.readFileSync(filePath, { encoding: 'utf-8' }); | |
return response.send(JSON.stringify(data)); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/save-workflow', async (request, response) => { | |
try { | |
const filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name))); | |
writeFileAtomicSync(filePath, request.body.workflow, 'utf8'); | |
const data = getComfyWorkflows(request.user.directories); | |
return response.send(data); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/delete-workflow', async (request, response) => { | |
try { | |
const filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name))); | |
if (fs.existsSync(filePath)) { | |
fs.unlinkSync(filePath); | |
} | |
return response.sendStatus(200); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
comfy.post('/generate', async (request, response) => { | |
try { | |
let item; | |
const url = new URL(urlJoin(request.body.url, '/prompt')); | |
const controller = new AbortController(); | |
request.socket.removeAllListeners('close'); | |
request.socket.on('close', function () { | |
if (!response.writableEnded && !item) { | |
const interruptUrl = new URL(urlJoin(request.body.url, '/interrupt')); | |
fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } }); | |
} | |
controller.abort(); | |
}); | |
const promptResult = await fetch(url, { | |
method: 'POST', | |
body: request.body.prompt, | |
}); | |
if (!promptResult.ok) { | |
const text = await promptResult.text(); | |
throw new Error('ComfyUI returned an error.', { cause: tryParse(text) }); | |
} | |
/** @type {any} */ | |
const data = await promptResult.json(); | |
const id = data.prompt_id; | |
const historyUrl = new URL(urlJoin(request.body.url, '/history')); | |
while (true) { | |
const result = await fetch(historyUrl); | |
if (!result.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
/** @type {any} */ | |
const history = await result.json(); | |
item = history[id]; | |
if (item) { | |
break; | |
} | |
await delay(100); | |
} | |
if (item.status.status_str === 'error') { | |
// Report node tracebacks if available | |
const errorMessages = item.status?.messages | |
?.filter(it => it[0] === 'execution_error') | |
.map(it => it[1]) | |
.map(it => `${it.node_type} [${it.node_id}] ${it.exception_type}: ${it.exception_message}`) | |
.join('\n') || ''; | |
throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim()); | |
} | |
const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; | |
const imgUrl = new URL(urlJoin(request.body.url, '/view')); | |
imgUrl.search = `?filename=${imgInfo.filename}&subfolder=${imgInfo.subfolder}&type=${imgInfo.type}`; | |
const imgResponse = await fetch(imgUrl); | |
if (!imgResponse.ok) { | |
throw new Error('ComfyUI returned an error.'); | |
} | |
const imgBuffer = await imgResponse.arrayBuffer(); | |
return response.send(Buffer.from(imgBuffer).toString('base64')); | |
} catch (error) { | |
console.error('ComfyUI error:', error); | |
response.status(500).send(error.message); | |
return response; | |
} | |
}); | |
const together = express.Router(); | |
together.post('/models', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI); | |
if (!key) { | |
console.warn('TogetherAI key not found.'); | |
return response.sendStatus(400); | |
} | |
const modelsResponse = await fetch('https://api.together.xyz/api/models', { | |
method: 'GET', | |
headers: { | |
'Authorization': `Bearer ${key}`, | |
}, | |
}); | |
if (!modelsResponse.ok) { | |
console.warn('TogetherAI returned an error.'); | |
return response.sendStatus(500); | |
} | |
const data = await modelsResponse.json(); | |
if (!Array.isArray(data)) { | |
console.warn('TogetherAI returned invalid data.'); | |
return response.sendStatus(500); | |
} | |
const models = data | |
.filter(x => x.type === 'image') | |
.map(x => ({ value: x.id, text: x.display_name })); | |
return response.send(models); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
together.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI); | |
if (!key) { | |
console.warn('TogetherAI key not found.'); | |
return response.sendStatus(400); | |
} | |
console.debug('TogetherAI request:', request.body); | |
const result = await fetch('https://api.together.xyz/v1/images/generations', { | |
method: 'POST', | |
body: JSON.stringify({ | |
prompt: request.body.prompt, | |
negative_prompt: request.body.negative_prompt, | |
height: request.body.height, | |
width: request.body.width, | |
model: request.body.model, | |
steps: request.body.steps, | |
n: 1, | |
// Limited to 10000 on playground, works fine with more. | |
seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000), | |
}), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${key}`, | |
}, | |
}); | |
if (!result.ok) { | |
console.warn('TogetherAI returned an error.', { body: await result.text() }); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
console.debug('TogetherAI response:', data); | |
const choice = data?.data?.[0]; | |
let b64_json = choice.b64_json; | |
if (!b64_json) { | |
const buffer = await (await fetch(choice.url)).arrayBuffer(); | |
b64_json = Buffer.from(buffer).toString('base64'); | |
} | |
return response.send({ format: 'jpg', data: b64_json }); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const drawthings = express.Router(); | |
drawthings.post('/ping', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/'; | |
const result = await fetch(url, { | |
method: 'HEAD', | |
}); | |
if (!result.ok) { | |
throw new Error('SD DrawThings API returned an error.'); | |
} | |
return response.sendStatus(200); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
drawthings.post('/get-model', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/'; | |
const result = await fetch(url, { | |
method: 'GET', | |
}); | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data['model']); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
drawthings.post('/get-upscaler', async (request, response) => { | |
try { | |
const url = new URL(request.body.url); | |
url.pathname = '/'; | |
const result = await fetch(url, { | |
method: 'GET', | |
}); | |
/** @type {any} */ | |
const data = await result.json(); | |
return response.send(data['upscaler']); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
drawthings.post('/generate', async (request, response) => { | |
try { | |
console.debug('SD DrawThings API request:', request.body); | |
const url = new URL(request.body.url); | |
url.pathname = '/sdapi/v1/txt2img'; | |
const body = { ...request.body }; | |
const auth = getBasicAuthHeader(request.body.auth); | |
delete body.url; | |
delete body.auth; | |
const result = await fetch(url, { | |
method: 'POST', | |
body: JSON.stringify(body), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': auth, | |
}, | |
}); | |
if (!result.ok) { | |
const text = await result.text(); | |
throw new Error('SD DrawThings API returned an error.', { cause: text }); | |
} | |
const data = await result.json(); | |
return response.send(data); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const pollinations = express.Router(); | |
pollinations.post('/models', async (_request, response) => { | |
try { | |
const modelsUrl = new URL('https://image.pollinations.ai/models'); | |
const result = await fetch(modelsUrl); | |
if (!result.ok) { | |
console.warn('Pollinations returned an error.', result.status, result.statusText); | |
throw new Error('Pollinations request failed.'); | |
} | |
const data = await result.json(); | |
if (!Array.isArray(data)) { | |
console.warn('Pollinations returned invalid data.'); | |
throw new Error('Pollinations request failed.'); | |
} | |
const models = data.map(x => ({ value: x, text: x })); | |
return response.send(models); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
pollinations.post('/generate', async (request, response) => { | |
try { | |
const promptUrl = new URL(`https://image.pollinations.ai/prompt/${encodeURIComponent(request.body.prompt)}`); | |
const params = new URLSearchParams({ | |
model: String(request.body.model), | |
negative_prompt: String(request.body.negative_prompt), | |
seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)), | |
width: String(request.body.width ?? 1024), | |
height: String(request.body.height ?? 1024), | |
nologo: String(true), | |
nofeed: String(true), | |
private: String(true), | |
referrer: 'sillytavern', | |
}); | |
if (request.body.enhance) { | |
params.set('enhance', String(true)); | |
} | |
promptUrl.search = params.toString(); | |
console.info('Pollinations request URL:', promptUrl.toString()); | |
const result = await fetch(promptUrl); | |
if (!result.ok) { | |
const text = await result.text(); | |
console.warn('Pollinations returned an error.', text); | |
throw new Error('Pollinations request failed.'); | |
} | |
const buffer = await result.arrayBuffer(); | |
const base64 = Buffer.from(buffer).toString('base64'); | |
return response.send({ image: base64 }); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const stability = express.Router(); | |
stability.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY); | |
if (!key) { | |
console.warn('Stability AI key not found.'); | |
return response.sendStatus(400); | |
} | |
const { payload, model } = request.body; | |
console.debug('Stability AI request:', model, payload); | |
const formData = new FormData(); | |
for (const [key, value] of Object.entries(payload)) { | |
if (value !== undefined) { | |
formData.append(key, String(value)); | |
} | |
} | |
let apiUrl; | |
switch (model) { | |
case 'stable-image-ultra': | |
apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/ultra'; | |
break; | |
case 'stable-image-core': | |
apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/core'; | |
break; | |
case 'stable-diffusion-3': | |
apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/sd3'; | |
break; | |
default: | |
throw new Error('Invalid Stability AI model selected'); | |
} | |
const result = await fetch(apiUrl, { | |
method: 'POST', | |
headers: { | |
'Authorization': `Bearer ${key}`, | |
'Accept': 'image/*', | |
}, | |
body: formData, | |
}); | |
if (!result.ok) { | |
const text = await result.text(); | |
console.warn('Stability AI returned an error.', result.status, result.statusText, text); | |
return response.sendStatus(500); | |
} | |
const buffer = await result.arrayBuffer(); | |
return response.send(Buffer.from(buffer).toString('base64')); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const huggingface = express.Router(); | |
huggingface.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE); | |
if (!key) { | |
console.warn('Hugging Face key not found.'); | |
return response.sendStatus(400); | |
} | |
console.debug('Hugging Face request:', request.body); | |
const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, { | |
method: 'POST', | |
body: JSON.stringify({ | |
inputs: request.body.prompt, | |
}), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${key}`, | |
}, | |
}); | |
if (!result.ok) { | |
console.warn('Hugging Face returned an error.'); | |
return response.sendStatus(500); | |
} | |
const buffer = await result.arrayBuffer(); | |
return response.send({ | |
image: Buffer.from(buffer).toString('base64'), | |
}); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const nanogpt = express.Router(); | |
nanogpt.post('/models', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); | |
if (!key) { | |
console.warn('NanoGPT key not found.'); | |
return response.sendStatus(400); | |
} | |
const modelsResponse = await fetch('https://nano-gpt.com/api/models', { | |
method: 'GET', | |
headers: { | |
'x-api-key': key, | |
'Content-Type': 'application/json', | |
}, | |
}); | |
if (!modelsResponse.ok) { | |
console.warn('NanoGPT returned an error.'); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const data = await modelsResponse.json(); | |
const imageModels = data?.models?.image; | |
if (!imageModels || typeof imageModels !== 'object') { | |
console.warn('NanoGPT returned invalid data.'); | |
return response.sendStatus(500); | |
} | |
const models = Object.values(imageModels).map(x => ({ value: x.model, text: x.name })); | |
return response.send(models); | |
} | |
catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
nanogpt.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); | |
if (!key) { | |
console.warn('NanoGPT key not found.'); | |
return response.sendStatus(400); | |
} | |
console.debug('NanoGPT request:', request.body); | |
const result = await fetch('https://nano-gpt.com/api/generate-image', { | |
method: 'POST', | |
body: JSON.stringify(request.body), | |
headers: { | |
'x-api-key': key, | |
'Content-Type': 'application/json', | |
}, | |
}); | |
if (!result.ok) { | |
console.warn('NanoGPT returned an error.'); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const image = data?.data?.[0]?.b64_json; | |
if (!image) { | |
console.warn('NanoGPT returned invalid data.'); | |
return response.sendStatus(500); | |
} | |
return response.send({ image }); | |
} | |
catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const bfl = express.Router(); | |
bfl.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.BFL); | |
if (!key) { | |
console.warn('BFL key not found.'); | |
return response.sendStatus(400); | |
} | |
const requestBody = { | |
prompt: request.body.prompt, | |
steps: request.body.steps, | |
guidance: request.body.guidance, | |
width: request.body.width, | |
height: request.body.height, | |
prompt_upsampling: request.body.prompt_upsampling, | |
seed: request.body.seed ?? null, | |
safety_tolerance: 6, // being least strict | |
output_format: 'jpeg', | |
}; | |
function getClosestAspectRatio(width, height) { | |
const minAspect = 9 / 21; | |
const maxAspect = 21 / 9; | |
const currentAspect = width / height; | |
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b); | |
const simplifyRatio = (w, h) => { | |
const divisor = gcd(w, h); | |
return `${w / divisor}:${h / divisor}`; | |
}; | |
if (currentAspect < minAspect) { | |
const adjustedHeight = Math.round(width / minAspect); | |
return simplifyRatio(width, adjustedHeight); | |
} else if (currentAspect > maxAspect) { | |
const adjustedWidth = Math.round(height * maxAspect); | |
return simplifyRatio(adjustedWidth, height); | |
} else { | |
return simplifyRatio(width, height); | |
} | |
} | |
if (String(request.body.model).endsWith('-ultra')) { | |
requestBody.aspect_ratio = getClosestAspectRatio(request.body.width, request.body.height); | |
delete requestBody.steps; | |
delete requestBody.guidance; | |
delete requestBody.width; | |
delete requestBody.height; | |
delete requestBody.prompt_upsampling; | |
} | |
if (String(request.body.model).endsWith('-pro-1.1')) { | |
delete requestBody.steps; | |
delete requestBody.guidance; | |
} | |
console.debug('BFL request:', requestBody); | |
const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, { | |
method: 'POST', | |
body: JSON.stringify(requestBody), | |
headers: { | |
'Content-Type': 'application/json', | |
'x-key': key, | |
}, | |
}); | |
if (!result.ok) { | |
console.warn('BFL returned an error.'); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const taskData = await result.json(); | |
const { id } = taskData; | |
const MAX_ATTEMPTS = 100; | |
for (let i = 0; i < MAX_ATTEMPTS; i++) { | |
await delay(2500); | |
const statusResult = await fetch(`https://api.bfl.ml/v1/get_result?id=${id}`); | |
if (!statusResult.ok) { | |
const text = await statusResult.text(); | |
console.warn('BFL returned an error.', text); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const statusData = await statusResult.json(); | |
if (statusData?.status === 'Pending') { | |
continue; | |
} | |
if (statusData?.status === 'Ready') { | |
const { sample } = statusData.result; | |
const fetchResult = await fetch(sample); | |
const fetchData = await fetchResult.arrayBuffer(); | |
const image = Buffer.from(fetchData).toString('base64'); | |
return response.send({ image: image }); | |
} | |
throw new Error('BFL failed to generate image.', { cause: statusData }); | |
} | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
const falai = express.Router(); | |
falai.post('/models', async (_request, response) => { | |
try { | |
const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image'); | |
const result = await fetch(modelsUrl); | |
if (!result.ok) { | |
console.warn('FAL.AI returned an error.', result.status, result.statusText); | |
throw new Error('FAL.AI request failed.'); | |
} | |
const data = await result.json(); | |
if (!Array.isArray(data)) { | |
console.warn('FAL.AI returned invalid data.'); | |
throw new Error('FAL.AI request failed.'); | |
} | |
const models = data | |
.filter(x => !x.title.toLowerCase().includes('inpainting') && | |
!x.title.toLowerCase().includes('control') && | |
!x.title.toLowerCase().includes('upscale') && | |
!x.title.toLowerCase().includes('lora')) | |
.sort((a, b) => a.title.localeCompare(b.title)) | |
.map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); | |
return response.send(models); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
falai.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.FALAI); | |
if (!key) { | |
console.warn('FAL.AI key not found.'); | |
return response.sendStatus(400); | |
} | |
const requestBody = { | |
prompt: request.body.prompt, | |
image_size: { 'width': request.body.width, 'height': request.body.height }, | |
num_inference_steps: request.body.steps, | |
seed: request.body.seed ?? null, | |
guidance_scale: request.body.guidance, | |
enable_safety_checker: false, // Disable general safety checks | |
safety_tolerance: 6, // Make Flux the least strict | |
}; | |
console.debug('FAL.AI request:', requestBody); | |
const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, { | |
method: 'POST', | |
body: JSON.stringify(requestBody), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Key ${key}`, | |
}, | |
}); | |
if (!result.ok) { | |
console.warn('FAL.AI returned an error.'); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const taskData = await result.json(); | |
const { status_url } = taskData; | |
const MAX_ATTEMPTS = 100; | |
for (let i = 0; i < MAX_ATTEMPTS; i++) { | |
await delay(2500); | |
const statusResult = await fetch(status_url, { | |
headers: { | |
'Authorization': `Key ${key}`, | |
}, | |
}); | |
if (!statusResult.ok) { | |
const text = await statusResult.text(); | |
console.warn('FAL.AI returned an error.', text); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const statusData = await statusResult.json(); | |
if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') { | |
continue; | |
} | |
if (statusData?.status === 'COMPLETED') { | |
const resultFetch = await fetch(statusData?.response_url, { | |
method: 'GET', | |
headers: { | |
'Authorization': `Key ${key}`, | |
}, | |
}); | |
/** @type {any} */ | |
const resultData = await resultFetch.json(); | |
if (resultData.detail !== null && resultData.detail !== undefined) { | |
throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` }); | |
} | |
const imageFetch = await fetch(resultData?.images[0].url, { | |
headers: { | |
'Authorization': `Key ${key}`, | |
}, | |
}); | |
const fetchData = await imageFetch.arrayBuffer(); | |
const image = Buffer.from(fetchData).toString('base64'); | |
return response.send({ image: image }); | |
} | |
throw new Error('FAL.AI failed to generate image.', { cause: statusData }); | |
} | |
} catch (error) { | |
console.error(error); | |
return response.status(500).send(error.cause || error.message); | |
} | |
}); | |
const xai = express.Router(); | |
xai.post('/generate', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.XAI); | |
if (!key) { | |
console.warn('xAI key not found.'); | |
return response.sendStatus(400); | |
} | |
const requestBody = { | |
prompt: request.body.prompt, | |
model: request.body.model, | |
response_format: 'b64_json', | |
}; | |
console.debug('xAI request:', requestBody); | |
const result = await fetch('https://api.x.ai/v1/images/generations', { | |
method: 'POST', | |
body: JSON.stringify(requestBody), | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${key}`, | |
}, | |
}); | |
if (!result.ok) { | |
const text = await result.text(); | |
console.warn('xAI returned an error.', text); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const data = await result.json(); | |
const image = data?.data?.[0]?.b64_json; | |
if (!image) { | |
console.warn('xAI returned invalid data.'); | |
return response.sendStatus(500); | |
} | |
return response.send({ image }); | |
} catch (error) { | |
console.error('Error communicating with xAI', error); | |
return response.sendStatus(500); | |
} | |
}); | |
const aimlapi = express.Router(); | |
aimlapi.post('/models', async (request, response) => { | |
try { | |
const key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI); | |
if (!key) { | |
console.warn('AI/ML API key not found.'); | |
return response.sendStatus(400); | |
} | |
const modelsResponse = await fetch('https://api.aimlapi.com/v1/models', { | |
method: 'GET', | |
headers: { | |
Authorization: `Bearer ${key}`, | |
}, | |
}); | |
if (!modelsResponse.ok) { | |
console.warn('AI/ML API returned an error.'); | |
return response.sendStatus(500); | |
} | |
/** @type {any} */ | |
const data = await modelsResponse.json(); | |
const models = (data.data || []) | |
.filter(model => | |
model.type === 'image' && | |
model.id !== 'triposr' && | |
model.id !== 'flux/dev/image-to-image', | |
) | |
.map(model => ({ | |
value: model.id, | |
text: model.info?.name || model.id, | |
})); | |
return response.send({ data: models }); | |
} catch (error) { | |
console.error(error); | |
return response.sendStatus(500); | |
} | |
}); | |
aimlapi.post('/generate-image', async (req, res) => { | |
try { | |
const key = readSecret(req.user.directories, SECRET_KEYS.AIMLAPI); | |
if (!key) return res.sendStatus(400); | |
console.debug('AI/ML API image request:', req.body); | |
const apiRes = await fetch('https://api.aimlapi.com/v1/images/generations', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}`, ...AIMLAPI_HEADERS }, | |
body: JSON.stringify(req.body), | |
}); | |
if (!apiRes.ok) { | |
const err = await apiRes.text(); | |
return res.status(500).send(err); | |
} | |
/** @type {any} */ | |
const data = await apiRes.json(); | |
const imgObj = Array.isArray(data.images) ? data.images[0] : data.data?.[0]; | |
if (!imgObj) return res.status(500).send('No image returned'); | |
let base64; | |
if (imgObj.b64_json || imgObj.base64) { | |
base64 = imgObj.b64_json || imgObj.base64; | |
} else if (imgObj.url) { | |
const blobRes = await fetch(imgObj.url); | |
if (!blobRes.ok) throw new Error('Failed to fetch image URL'); | |
const buffer = await blobRes.arrayBuffer(); | |
base64 = Buffer.from(buffer).toString('base64'); | |
} else { | |
throw new Error('Unsupported image format'); | |
} | |
return res.json({ format: 'png', data: base64 }); | |
} catch (e) { | |
console.error(e); | |
res.status(500).send('Internal error'); | |
} | |
}); | |
router.use('/comfy', comfy); | |
router.use('/together', together); | |
router.use('/drawthings', drawthings); | |
router.use('/pollinations', pollinations); | |
router.use('/stability', stability); | |
router.use('/huggingface', huggingface); | |
router.use('/nanogpt', nanogpt); | |
router.use('/bfl', bfl); | |
router.use('/falai', falai); | |
router.use('/xai', xai); | |
router.use('/aimlapi', aimlapi); | |