mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into tc-global-banlist
This commit is contained in:
@@ -6,6 +6,10 @@ cardsCacheCapacity: 100
|
||||
# -- SERVER CONFIGURATION --
|
||||
# Listen for incoming connections
|
||||
listen: false
|
||||
# Listen on a specific address, supports IPv4 and IPv6
|
||||
listenAddress:
|
||||
ipv4: 0.0.0.0
|
||||
ipv6: '[::]'
|
||||
# Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled!
|
||||
# - Use option "auto" to automatically detect support
|
||||
# - Use true or false (no qoutes) to enable or disable each protocol
|
||||
@@ -183,6 +187,10 @@ ollama:
|
||||
# * 0: Unload the model immediately after the request
|
||||
# * N (any positive number): Keep the model loaded for N seconds after the request.
|
||||
keepAlive: -1
|
||||
# Controls the "num_batch" (batch size) parameter of the generation request
|
||||
# * -1: Use the default value of the model
|
||||
# * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc.
|
||||
batchSize: -1
|
||||
# -- ANTHROPIC CLAUDE API CONFIGURATION --
|
||||
claude:
|
||||
# Enables caching of the system prompt (if supported).
|
||||
|
@@ -671,10 +671,6 @@
|
||||
"filename": "presets/moving-ui/Default.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/moving-ui/Black Magic Time.json",
|
||||
"type": "moving_ui"
|
||||
},
|
||||
{
|
||||
"filename": "presets/quick-replies/Default.json",
|
||||
"type": "quick_replies"
|
||||
|
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "Black Magic Time",
|
||||
"movingUIState": {
|
||||
"sheld": {
|
||||
"top": 488,
|
||||
"left": 1407,
|
||||
"right": 1,
|
||||
"bottom": 4,
|
||||
"margin": "unset",
|
||||
"width": 471,
|
||||
"height": 439
|
||||
},
|
||||
"floatingPrompt": {
|
||||
"width": 369,
|
||||
"height": 441
|
||||
},
|
||||
"right-nav-panel": {
|
||||
"top": 0,
|
||||
"left": 1400,
|
||||
"right": 111,
|
||||
"bottom": 446,
|
||||
"margin": "unset",
|
||||
"width": 479,
|
||||
"height": 487
|
||||
},
|
||||
"WorldInfo": {
|
||||
"top": 41,
|
||||
"left": 369,
|
||||
"right": 642,
|
||||
"bottom": 51,
|
||||
"margin": "unset",
|
||||
"width": 1034,
|
||||
"height": 858
|
||||
},
|
||||
"left-nav-panel": {
|
||||
"top": 442,
|
||||
"left": 0,
|
||||
"right": 1546,
|
||||
"bottom": 25,
|
||||
"margin": "unset",
|
||||
"width": 368,
|
||||
"height": 483
|
||||
}
|
||||
}
|
||||
}
|
13
package-lock.json
generated
13
package-lock.json
generated
@@ -41,6 +41,7 @@
|
||||
"html-entities": "^2.5.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ip-matching": "^2.1.2",
|
||||
"ip-regex": "^5.0.0",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jimp": "^0.22.10",
|
||||
"localforage": "^1.10.0",
|
||||
@@ -4628,6 +4629,18 @@
|
||||
"integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==",
|
||||
"license": "LGPL-3.0-only"
|
||||
},
|
||||
"node_modules/ip-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
|
||||
|
@@ -31,6 +31,7 @@
|
||||
"html-entities": "^2.5.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ip-matching": "^2.1.2",
|
||||
"ip-regex": "^5.0.0",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jimp": "^0.22.10",
|
||||
"localforage": "^1.10.0",
|
||||
@@ -89,6 +90,7 @@
|
||||
"version": "1.12.11",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"debug": "node server.js --inspect",
|
||||
"start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
|
||||
"start:bun": "bun server.js",
|
||||
"start:no-csrf": "node server.js --disableCsrf",
|
||||
|
@@ -81,6 +81,7 @@ const sources = {
|
||||
huggingface: 'huggingface',
|
||||
nanogpt: 'nanogpt',
|
||||
bfl: 'bfl',
|
||||
falai: 'falai',
|
||||
};
|
||||
|
||||
const initiators = {
|
||||
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
|
||||
return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
|
||||
}
|
||||
|
||||
async function onFalaiKeyClick() {
|
||||
return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
|
||||
}
|
||||
|
||||
function onBflUpsamplingInput() {
|
||||
extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
@@ -1299,6 +1304,7 @@ async function onModelChange() {
|
||||
sources.huggingface,
|
||||
sources.nanogpt,
|
||||
sources.bfl,
|
||||
sources.falai,
|
||||
];
|
||||
|
||||
if (cloudSources.includes(extension_settings.sd.source)) {
|
||||
@@ -1707,6 +1713,9 @@ async function loadModels() {
|
||||
case sources.bfl:
|
||||
models = await loadBflModels();
|
||||
break;
|
||||
case sources.falai:
|
||||
models = await loadFalaiModels();
|
||||
break;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
|
||||
];
|
||||
}
|
||||
|
||||
async function loadFalaiModels() {
|
||||
$('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]);
|
||||
|
||||
const result = await fetch('/api/sd/falai/models', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
return await result.json();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadPollinationsModels() {
|
||||
const result = await fetch('/api/sd/pollinations/models', {
|
||||
method: 'POST',
|
||||
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
|
||||
case sources.bfl:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.falai:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
}
|
||||
|
||||
for (const scheduler of schedulers) {
|
||||
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
|
||||
case sources.bfl:
|
||||
result = await generateBflImage(prefixedPrompt, signal);
|
||||
break;
|
||||
case sources.falai:
|
||||
result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image using the FAL.AI API.
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The negative prompt used to guide the image generation.
|
||||
* @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateFalaiImage(prompt, negativePrompt, signal) {
|
||||
const result = await fetch('/api/sd/falai/generate', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
signal: signal,
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
model: extension_settings.sd.model,
|
||||
steps: clamp(extension_settings.sd.steps, 1, 50),
|
||||
guidance: clamp(extension_settings.sd.scale, 1.5, 5),
|
||||
width: clamp(extension_settings.sd.width, 256, 1440),
|
||||
height: clamp(extension_settings.sd.height, 256, 1440),
|
||||
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
return { format: 'jpg', data: data.image };
|
||||
} else {
|
||||
const text = await result.text();
|
||||
console.log(text);
|
||||
throw new Error(text);
|
||||
}
|
||||
}
|
||||
|
||||
async function onComfyOpenWorkflowEditorClick() {
|
||||
let workflow = await (await fetch('/api/sd/comfy/workflow', {
|
||||
method: 'POST',
|
||||
@@ -3782,6 +3846,8 @@ function isValidState() {
|
||||
return secret_state[SECRET_KEYS.NANOGPT];
|
||||
case sources.bfl:
|
||||
return secret_state[SECRET_KEYS.BFL];
|
||||
case sources.falai:
|
||||
return secret_state[SECRET_KEYS.FALAI];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4443,6 +4509,7 @@ jQuery(async () => {
|
||||
$('#sd_function_tool').on('input', onFunctionToolInput);
|
||||
$('#sd_bfl_key').on('click', onBflKeyClick);
|
||||
$('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
|
||||
$('#sd_falai_key').on('click', onFalaiKeyClick);
|
||||
|
||||
if (!CSS.supports('field-sizing', 'content')) {
|
||||
$('.sd_settings .inline-drawer-toggle').on('click', function () {
|
||||
|
@@ -42,6 +42,7 @@
|
||||
<option value="comfy">ComfyUI</option>
|
||||
<option value="drawthings">DrawThings HTTP API</option>
|
||||
<option value="extras">Extras API (deprecated)</option>
|
||||
<option value="falai">FAL.AI</option>
|
||||
<option value="huggingface">HuggingFace Inference API (serverless)</option>
|
||||
<option value="nanogpt">NanoGPT</option>
|
||||
<option value="novel">NovelAI Diffusion</option>
|
||||
@@ -256,6 +257,20 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-sd-source="falai">
|
||||
<div class="flex-container flexnowrap alignItemsBaseline marginBot5">
|
||||
<a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer">
|
||||
<strong data-i18n="API Key">API Key</strong>
|
||||
<i class="fa-solid fa-share-from-square"></i>
|
||||
</a>
|
||||
<span class="expander"></span>
|
||||
<div id="sd_falai_key" class="menu_button menu_button_icon">
|
||||
<i class="fa-fw fa-solid fa-key"></i>
|
||||
<span data-i18n="Click to set">Click to set</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1">
|
||||
<label for="sd_model" data-i18n="Model">Model</label>
|
||||
|
@@ -215,6 +215,10 @@ export class ReasoningHandler {
|
||||
}
|
||||
|
||||
this.updateDom(messageId);
|
||||
|
||||
if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) {
|
||||
this.messageReasoningDetailsDom.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -41,6 +41,7 @@ export const SECRET_KEYS = {
|
||||
GENERIC: 'api_key_generic',
|
||||
DEEPSEEK: 'api_key_deepseek',
|
||||
SERPER: 'api_key_serper',
|
||||
FALAI: 'api_key_falai',
|
||||
};
|
||||
|
||||
const INPUT_MAP = {
|
||||
|
@@ -362,9 +362,14 @@ input[type='checkbox']:focus-visible {
|
||||
}
|
||||
|
||||
.mes_reasoning_details .mes_reasoning_summary {
|
||||
list-style: none;
|
||||
margin-right: calc(var(--mes-right-spacing) * -1);
|
||||
}
|
||||
|
||||
.mes_reasoning_details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mes_reasoning *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -378,7 +383,7 @@ input[type='checkbox']:focus-visible {
|
||||
}
|
||||
|
||||
.mes_reasoning_details .mes_reasoning em {
|
||||
color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%)
|
||||
color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
|
||||
}
|
||||
|
||||
.mes_reasoning_header_block {
|
||||
@@ -403,7 +408,7 @@ input[type='checkbox']:focus-visible {
|
||||
}
|
||||
|
||||
/* TWIMC: Remove with custom CSS to show the icon */
|
||||
.mes_reasoning_header > .icon-svg {
|
||||
.mes_reasoning_header>.icon-svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
34
server.js
34
server.js
@@ -30,6 +30,7 @@ import bodyParser from 'body-parser';
|
||||
|
||||
// net related library imports
|
||||
import fetch from 'node-fetch';
|
||||
import ipRegex from 'ip-regex';
|
||||
|
||||
// Unrestrict console logs display limit
|
||||
util.inspect.defaultOptions.maxArrayLength = null;
|
||||
@@ -59,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js';
|
||||
import whitelistMiddleware from './src/middleware/whitelist.js';
|
||||
import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
|
||||
import initRequestProxy from './src/request-proxy.js';
|
||||
import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
|
||||
import {
|
||||
getVersion,
|
||||
getConfigValue,
|
||||
@@ -130,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
|
||||
const DEFAULT_PORT = 8000;
|
||||
const DEFAULT_AUTORUN = false;
|
||||
const DEFAULT_LISTEN = false;
|
||||
const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]';
|
||||
const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0';
|
||||
const DEFAULT_CORS_PROXY = false;
|
||||
const DEFAULT_WHITELIST = true;
|
||||
const DEFAULT_ACCOUNTS = false;
|
||||
@@ -185,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv))
|
||||
type: 'boolean',
|
||||
default: null,
|
||||
describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`,
|
||||
}).option('listenAddressIPv6', {
|
||||
type: 'string',
|
||||
default: null,
|
||||
describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]',
|
||||
}).option('listenAddressIPv4', {
|
||||
type: 'string',
|
||||
default: null,
|
||||
describe: 'Set SillyTavern to listen to a specific IPv4 address. If not set, it will fallback to listen to all.\n[config default: 0.0.0.0 ]',
|
||||
}).option('corsProxy', {
|
||||
type: 'boolean',
|
||||
default: null,
|
||||
@@ -254,6 +266,10 @@ const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getCon
|
||||
const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl;
|
||||
/** @type {boolean} */
|
||||
const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN);
|
||||
/** @type {string} */
|
||||
const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
|
||||
/** @type {string} */
|
||||
const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
|
||||
/** @type {boolean} */
|
||||
const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY);
|
||||
const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST);
|
||||
@@ -500,7 +516,7 @@ if (!disableCsrf) {
|
||||
|
||||
// Static files
|
||||
// Host index page
|
||||
app.get('/', (request, response) => {
|
||||
app.get('/', getCacheBusterMiddleware(), (request, response) => {
|
||||
if (shouldRedirectToLogin(request)) {
|
||||
const query = request.url.split('?')[1];
|
||||
const redirectUrl = query ? `/login?${query}` : '/login';
|
||||
@@ -708,13 +724,13 @@ app.use('/api/azure', azureRouter);
|
||||
|
||||
const tavernUrlV6 = new URL(
|
||||
(cliArguments.ssl ? 'https://' : 'http://') +
|
||||
(listen ? '[::]' : '[::1]') +
|
||||
(listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') +
|
||||
(':' + server_port),
|
||||
);
|
||||
|
||||
const tavernUrl = new URL(
|
||||
(cliArguments.ssl ? 'https://' : 'http://') +
|
||||
(listen ? '0.0.0.0' : '127.0.0.1') +
|
||||
(listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
|
||||
(':' + server_port),
|
||||
);
|
||||
|
||||
@@ -837,15 +853,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) {
|
||||
const plainGoToLog = removeColorFormatting(goToLog);
|
||||
|
||||
console.log(logListen);
|
||||
if (listen) {
|
||||
console.log();
|
||||
console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".');
|
||||
console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.');
|
||||
}
|
||||
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
|
||||
console.log(goToLog);
|
||||
console.log('\n' + getSeparator(plainGoToLog.length) + '\n');
|
||||
|
||||
if (listen) {
|
||||
console.log(
|
||||
'[::] or 0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n',
|
||||
);
|
||||
}
|
||||
|
||||
if (basicAuthMode) {
|
||||
if (perUserBasicAuth && !enableAccounts) {
|
||||
@@ -1083,7 +1099,7 @@ async function verifySecuritySettings() {
|
||||
}
|
||||
|
||||
if (!enableAccounts) {
|
||||
logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.');
|
||||
logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
|
||||
}
|
||||
|
||||
const users = await getAllEnabledUsers();
|
||||
|
@@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [
|
||||
export const OLLAMA_KEYS = [
|
||||
'num_predict',
|
||||
'num_ctx',
|
||||
'num_batch',
|
||||
'stop',
|
||||
'temperature',
|
||||
'repeat_penalty',
|
||||
|
@@ -373,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) {
|
||||
|
||||
if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
|
||||
const keepAlive = getConfigValue('ollama.keepAlive', -1);
|
||||
const numBatch = getConfigValue('ollama.batchSize', -1);
|
||||
if (numBatch > 0) {
|
||||
request.body['num_batch'] = numBatch;
|
||||
}
|
||||
args.body = JSON.stringify({
|
||||
model: request.body.model,
|
||||
prompt: request.body.prompt,
|
||||
|
@@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi
|
||||
invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url);
|
||||
await writeCharacterData(newAvatarPath, char, targetFile, request, crop);
|
||||
fs.unlinkSync(newAvatarPath);
|
||||
|
||||
// Bust cache to reload the new avatar
|
||||
response.setHeader('Clear-Site-Data', '"cache"');
|
||||
}
|
||||
|
||||
return response.sendStatus(200);
|
||||
|
@@ -50,6 +50,7 @@ export const SECRET_KEYS = {
|
||||
TAVILY: 'api_key_tavily',
|
||||
NANOGPT: 'api_key_nanogpt',
|
||||
BFL: 'api_key_bfl',
|
||||
FALAI: 'api_key_falai',
|
||||
GENERIC: 'api_key_generic',
|
||||
DEEPSEEK: 'api_key_deepseek',
|
||||
SERPER: 'api_key_serper',
|
||||
|
@@ -1228,6 +1228,131 @@ bfl.post('/generate', jsonParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
const falai = express.Router();
|
||||
|
||||
falai.post('/models', jsonParser, 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'))
|
||||
.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', jsonParser, 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,
|
||||
};
|
||||
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/comfy', comfy);
|
||||
router.use('/together', together);
|
||||
router.use('/drawthings', drawthings);
|
||||
@@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy);
|
||||
router.use('/huggingface', huggingface);
|
||||
router.use('/nanogpt', nanogpt);
|
||||
router.use('/bfl', bfl);
|
||||
router.use('/falai', falai);
|
||||
|
28
src/middleware/cacheBuster.js
Normal file
28
src/middleware/cacheBuster.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { DEFAULT_USER } from '../constants.js';
|
||||
|
||||
/**
|
||||
* Middleware to bust the browser cache for the current user.
|
||||
* @returns {import('express').RequestHandler}
|
||||
*/
|
||||
export default function getCacheBusterMiddleware() {
|
||||
/**
|
||||
* @type {Set<string>} Handles/User-Agents that have already been busted.
|
||||
*/
|
||||
const keys = new Set();
|
||||
|
||||
return (request, response, next) => {
|
||||
const handle = request.user?.profile?.handle || DEFAULT_USER.handle;
|
||||
const userAgent = request.headers['user-agent'] || '';
|
||||
const hash = crypto.createHash('sha256').update(userAgent).digest('hex');
|
||||
const key = `${handle}-${hash}`;
|
||||
|
||||
if (keys.has(key)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
keys.add(key);
|
||||
response.setHeader('Clear-Site-Data', '"cache"');
|
||||
next();
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user