Merge branch 'staging' into tc-global-banlist
| @@ -6,6 +6,10 @@ cardsCacheCapacity: 100 | ||
| 6 | 6 | # -- SERVER CONFIGURATION -- |
| 7 | 7 | # Listen for incoming connections |
| 8 | 8 | listen: false |
| 9 | +# Listen on a specific address, supports IPv4 and IPv6 | |
| 10 | +listenAddress: | |
| 11 | + ipv4: 0.0.0.0 | |
| 12 | + ipv6: '[::]' | |
| 9 | 13 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! |
| 10 | 14 | # - Use option "auto" to automatically detect support |
| 11 | 15 | # - Use true or false (no qoutes) to enable or disable each protocol |
| @@ -183,6 +187,10 @@ ollama: | ||
| 183 | 187 | # * 0: Unload the model immediately after the request |
| 184 | 188 | # * N (any positive number): Keep the model loaded for N seconds after the request. |
| 185 | 189 | keepAlive: -1 |
| 190 | + # Controls the "num_batch" (batch size) parameter of the generation request | |
| 191 | + # * -1: Use the default value of the model | |
| 192 | + # * N (positive number): Use the specified value. Must be a power of 2, e.g. 128, 256, 512, etc. | |
| 193 | + batchSize: -1 | |
| 186 | 194 | # -- ANTHROPIC CLAUDE API CONFIGURATION -- |
| 187 | 195 | claude: |
| 188 | 196 | # Enables caching of the system prompt (if supported). |
| @@ -672,10 +672,6 @@ | ||
| 672 | 672 | "type": "moving_ui" |
| 673 | 673 | }, |
| 674 | 674 | { |
| 675 | - "filename": "presets/moving-ui/Black Magic Time.json", | |
| 676 | - "type": "moving_ui" | |
| 677 | - }, | |
| 678 | - { | |
| 679 | 675 | "filename": "presets/quick-replies/Default.json", |
| 680 | 676 | "type": "quick_replies" |
| 681 | 677 | }, |
| @@ -1,45 +0,0 @@ | ||
| 1 | -{ | |
| 2 | - "name": "Black Magic Time", | |
| 3 | - "movingUIState": { | |
| 4 | - "sheld": { | |
| 5 | - "top": 488, | |
| 6 | - "left": 1407, | |
| 7 | - "right": 1, | |
| 8 | - "bottom": 4, | |
| 9 | - "margin": "unset", | |
| 10 | - "width": 471, | |
| 11 | - "height": 439 | |
| 12 | - }, | |
| 13 | - "floatingPrompt": { | |
| 14 | - "width": 369, | |
| 15 | - "height": 441 | |
| 16 | - }, | |
| 17 | - "right-nav-panel": { | |
| 18 | - "top": 0, | |
| 19 | - "left": 1400, | |
| 20 | - "right": 111, | |
| 21 | - "bottom": 446, | |
| 22 | - "margin": "unset", | |
| 23 | - "width": 479, | |
| 24 | - "height": 487 | |
| 25 | - }, | |
| 26 | - "WorldInfo": { | |
| 27 | - "top": 41, | |
| 28 | - "left": 369, | |
| 29 | - "right": 642, | |
| 30 | - "bottom": 51, | |
| 31 | - "margin": "unset", | |
| 32 | - "width": 1034, | |
| 33 | - "height": 858 | |
| 34 | - }, | |
| 35 | - "left-nav-panel": { | |
| 36 | - "top": 442, | |
| 37 | - "left": 0, | |
| 38 | - "right": 1546, | |
| 39 | - "bottom": 25, | |
| 40 | - "margin": "unset", | |
| 41 | - "width": 368, | |
| 42 | - "height": 483 | |
| 43 | - } | |
| 44 | - } | |
| 45 | -} | |
| 45 | \ No newline at end of file | |
| @@ -41,6 +41,7 @@ | ||
| 41 | 41 | "html-entities": "^2.5.2", |
| 42 | 42 | "iconv-lite": "^0.6.3", |
| 43 | 43 | "ip-matching": "^2.1.2", |
| 44 | + "ip-regex": "^5.0.0", | |
| 44 | 45 | "ipaddr.js": "^2.0.1", |
| 45 | 46 | "jimp": "^0.22.10", |
| 46 | 47 | "localforage": "^1.10.0", |
| @@ -4628,6 +4629,18 @@ | ||
| 4628 | 4629 | "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==", |
| 4629 | 4630 | "license": "LGPL-3.0-only" |
| 4630 | 4631 | }, |
| 4632 | + "node_modules/ip-regex": { | |
| 4633 | + "version": "5.0.0", | |
| 4634 | + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", | |
| 4635 | + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", | |
| 4636 | + "license": "MIT", | |
| 4637 | + "engines": { | |
| 4638 | + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | |
| 4639 | + }, | |
| 4640 | + "funding": { | |
| 4641 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 4642 | + } | |
| 4643 | + }, | |
| 4631 | 4644 | "node_modules/ipaddr.js": { |
| 4632 | 4645 | "version": "2.1.0", |
| 4633 | 4646 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", |
| @@ -31,6 +31,7 @@ | ||
| 31 | 31 | "html-entities": "^2.5.2", |
| 32 | 32 | "iconv-lite": "^0.6.3", |
| 33 | 33 | "ip-matching": "^2.1.2", |
| 34 | + "ip-regex": "^5.0.0", | |
| 34 | 35 | "ipaddr.js": "^2.0.1", |
| 35 | 36 | "jimp": "^0.22.10", |
| 36 | 37 | "localforage": "^1.10.0", |
| @@ -89,6 +90,7 @@ | ||
| 89 | 90 | "version": "1.12.11", |
| 90 | 91 | "scripts": { |
| 91 | 92 | "start": "node server.js", |
| 93 | + "debug": "node server.js --inspect", | |
| 92 | 94 | "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", |
| 93 | 95 | "start:bun": "bun server.js", |
| 94 | 96 | "start:no-csrf": "node server.js --disableCsrf", |
| @@ -81,6 +81,7 @@ const sources = { | ||
| 81 | 81 | huggingface: 'huggingface', |
| 82 | 82 | nanogpt: 'nanogpt', |
| 83 | 83 | bfl: 'bfl', |
| 84 | + falai: 'falai', | |
| 84 | 85 | }; |
| 85 | 86 | |
| 86 | 87 | const initiators = { |
| @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { | ||
| 1169 | 1170 | return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); |
| 1170 | 1171 | } |
| 1171 | 1172 | |
| 1173 | +async function onFalaiKeyClick() { | |
| 1174 | + return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI); | |
| 1175 | +} | |
| 1176 | + | |
| 1172 | 1177 | function onBflUpsamplingInput() { |
| 1173 | 1178 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 1174 | 1179 | saveSettingsDebounced(); |
| @@ -1299,6 +1304,7 @@ async function onModelChange() { | ||
| 1299 | 1304 | sources.huggingface, |
| 1300 | 1305 | sources.nanogpt, |
| 1301 | 1306 | sources.bfl, |
| 1307 | + sources.falai, | |
| 1302 | 1308 | ]; |
| 1303 | 1309 | |
| 1304 | 1310 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1707,6 +1713,9 @@ async function loadModels() { | ||
| 1707 | 1713 | case sources.bfl: |
| 1708 | 1714 | models = await loadBflModels(); |
| 1709 | 1715 | break; |
| 1716 | + case sources.falai: | |
| 1717 | + models = await loadFalaiModels(); | |
| 1718 | + break; | |
| 1710 | 1719 | } |
| 1711 | 1720 | |
| 1712 | 1721 | for (const model of models) { |
| @@ -1744,6 +1753,21 @@ async function loadBflModels() { | ||
| 1744 | 1753 | ]; |
| 1745 | 1754 | } |
| 1746 | 1755 | |
| 1756 | +async function loadFalaiModels() { | |
| 1757 | + $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); | |
| 1758 | + | |
| 1759 | + const result = await fetch('/api/sd/falai/models', { | |
| 1760 | + method: 'POST', | |
| 1761 | + headers: getRequestHeaders(), | |
| 1762 | + }); | |
| 1763 | + | |
| 1764 | + if (result.ok) { | |
| 1765 | + return await result.json(); | |
| 1766 | + } | |
| 1767 | + | |
| 1768 | + return []; | |
| 1769 | +} | |
| 1770 | + | |
| 1747 | 1771 | async function loadPollinationsModels() { |
| 1748 | 1772 | const result = await fetch('/api/sd/pollinations/models', { |
| 1749 | 1773 | method: 'POST', |
| @@ -2081,6 +2105,9 @@ async function loadSchedulers() { | ||
| 2081 | 2105 | case sources.bfl: |
| 2082 | 2106 | schedulers = ['N/A']; |
| 2083 | 2107 | break; |
| 2108 | + case sources.falai: | |
| 2109 | + schedulers = ['N/A']; | |
| 2110 | + break; | |
| 2084 | 2111 | } |
| 2085 | 2112 | |
| 2086 | 2113 | for (const scheduler of schedulers) { |
| @@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2735 | 2762 | case sources.bfl: |
| 2736 | 2763 | result = await generateBflImage(prefixedPrompt, signal); |
| 2737 | 2764 | break; |
| 2765 | + case sources.falai: | |
| 2766 | + result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); | |
| 2767 | + break; | |
| 2738 | 2768 | } |
| 2739 | 2769 | |
| 2740 | 2770 | if (!result.data) { |
| @@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) { | ||
| 3496 | 3526 | } |
| 3497 | 3527 | } |
| 3498 | 3528 | |
| 3529 | +/** | |
| 3530 | + * Generates an image using the FAL.AI API. | |
| 3531 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 3532 | + * @param {string} negativePrompt - The negative prompt used to guide the image generation. | |
| 3533 | + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | |
| 3534 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 3535 | + */ | |
| 3536 | +async function generateFalaiImage(prompt, negativePrompt, signal) { | |
| 3537 | + const result = await fetch('/api/sd/falai/generate', { | |
| 3538 | + method: 'POST', | |
| 3539 | + headers: getRequestHeaders(), | |
| 3540 | + signal: signal, | |
| 3541 | + body: JSON.stringify({ | |
| 3542 | + prompt: prompt, | |
| 3543 | + negative_prompt: negativePrompt, | |
| 3544 | + model: extension_settings.sd.model, | |
| 3545 | + steps: clamp(extension_settings.sd.steps, 1, 50), | |
| 3546 | + guidance: clamp(extension_settings.sd.scale, 1.5, 5), | |
| 3547 | + width: clamp(extension_settings.sd.width, 256, 1440), | |
| 3548 | + height: clamp(extension_settings.sd.height, 256, 1440), | |
| 3549 | + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | |
| 3550 | + }), | |
| 3551 | + }); | |
| 3552 | + | |
| 3553 | + if (result.ok) { | |
| 3554 | + const data = await result.json(); | |
| 3555 | + return { format: 'jpg', data: data.image }; | |
| 3556 | + } else { | |
| 3557 | + const text = await result.text(); | |
| 3558 | + console.log(text); | |
| 3559 | + throw new Error(text); | |
| 3560 | + } | |
| 3561 | +} | |
| 3562 | + | |
| 3499 | 3563 | async function onComfyOpenWorkflowEditorClick() { |
| 3500 | 3564 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 3501 | 3565 | method: 'POST', |
| @@ -3782,6 +3846,8 @@ function isValidState() { | ||
| 3782 | 3846 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 3783 | 3847 | case sources.bfl: |
| 3784 | 3848 | return secret_state[SECRET_KEYS.BFL]; |
| 3849 | + case sources.falai: | |
| 3850 | + return secret_state[SECRET_KEYS.FALAI]; | |
| 3785 | 3851 | } |
| 3786 | 3852 | } |
| 3787 | 3853 | |
| @@ -4443,6 +4509,7 @@ jQuery(async () => { | ||
| 4443 | 4509 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4444 | 4510 | $('#sd_bfl_key').on('click', onBflKeyClick); |
| 4445 | 4511 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4512 | + $('#sd_falai_key').on('click', onFalaiKeyClick); | |
| 4446 | 4513 | |
| 4447 | 4514 | if (!CSS.supports('field-sizing', 'content')) { |
| 4448 | 4515 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -42,6 +42,7 @@ | ||
| 42 | 42 | <option value="comfy">ComfyUI</option> |
| 43 | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | 44 | <option value="extras">Extras API (deprecated)</option> |
| 45 | + <option value="falai">FAL.AI</option> | |
| 45 | 46 | <option value="huggingface">HuggingFace Inference API (serverless)</option> |
| 46 | 47 | <option value="nanogpt">NanoGPT</option> |
| 47 | 48 | <option value="novel">NovelAI Diffusion</option> |
| @@ -256,6 +257,20 @@ | ||
| 256 | 257 | </label> |
| 257 | 258 | </div> |
| 258 | 259 | |
| 260 | + <div data-sd-source="falai"> | |
| 261 | + <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | |
| 262 | + <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer"> | |
| 263 | + <strong data-i18n="API Key">API Key</strong> | |
| 264 | + <i class="fa-solid fa-share-from-square"></i> | |
| 265 | + </a> | |
| 266 | + <span class="expander"></span> | |
| 267 | + <div id="sd_falai_key" class="menu_button menu_button_icon"> | |
| 268 | + <i class="fa-fw fa-solid fa-key"></i> | |
| 269 | + <span data-i18n="Click to set">Click to set</span> | |
| 270 | + </div> | |
| 271 | + </div> | |
| 272 | + </div> | |
| 273 | + | |
| 259 | 274 | <div class="flex-container"> |
| 260 | 275 | <div class="flex1"> |
| 261 | 276 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -215,6 +215,10 @@ export class ReasoningHandler { | ||
| 215 | 215 | } |
| 216 | 216 | |
| 217 | 217 | this.updateDom(messageId); |
| 218 | + | |
| 219 | + if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) { | |
| 220 | + this.messageReasoningDetailsDom.open = true; | |
| 221 | + } | |
| 218 | 222 | } |
| 219 | 223 | |
| 220 | 224 | /** |
| @@ -41,6 +41,7 @@ export const SECRET_KEYS = { | ||
| 41 | 41 | GENERIC: 'api_key_generic', |
| 42 | 42 | DEEPSEEK: 'api_key_deepseek', |
| 43 | 43 | SERPER: 'api_key_serper', |
| 44 | + FALAI: 'api_key_falai', | |
| 44 | 45 | }; |
| 45 | 46 | |
| 46 | 47 | const INPUT_MAP = { |
| @@ -362,9 +362,14 @@ input[type='checkbox']:focus-visible { | ||
| 362 | 362 | } |
| 363 | 363 | |
| 364 | 364 | .mes_reasoning_details .mes_reasoning_summary { |
| 365 | + list-style: none; | |
| 365 | 366 | margin-right: calc(var(--mes-right-spacing) * -1); |
| 366 | 367 | } |
| 367 | 368 | |
| 369 | +.mes_reasoning_details summary::-webkit-details-marker { | |
| 370 | + display: none; | |
| 371 | +} | |
| 372 | + | |
| 368 | 373 | .mes_reasoning *:last-child { |
| 369 | 374 | margin-bottom: 0; |
| 370 | 375 | } |
| @@ -378,7 +383,7 @@ input[type='checkbox']:focus-visible { | ||
| 378 | 383 | } |
| 379 | 384 | |
| 380 | 385 | .mes_reasoning_details .mes_reasoning em { |
| 381 | 386 | color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%); |
| 382 | 387 | } |
| 383 | 388 | |
| 384 | 389 | .mes_reasoning_header_block { |
| @@ -403,7 +408,7 @@ input[type='checkbox']:focus-visible { | ||
| 403 | 408 | } |
| 404 | 409 | |
| 405 | 410 | /* TWIMC: Remove with custom CSS to show the icon */ |
| 406 | 411 | .mes_reasoning_header > .icon-svg { |
| 407 | 412 | display: none; |
| 408 | 413 | } |
| 409 | 414 | |
| @@ -30,6 +30,7 @@ import bodyParser from 'body-parser'; | ||
| 30 | 30 | |
| 31 | 31 | // net related library imports |
| 32 | 32 | import fetch from 'node-fetch'; |
| 33 | +import ipRegex from 'ip-regex'; | |
| 33 | 34 | |
| 34 | 35 | // Unrestrict console logs display limit |
| 35 | 36 | util.inspect.defaultOptions.maxArrayLength = null; |
| @@ -59,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js'; | ||
| 59 | 60 | import whitelistMiddleware from './src/middleware/whitelist.js'; |
| 60 | 61 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 61 | 62 | import initRequestProxy from './src/request-proxy.js'; |
| 63 | +import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; | |
| 62 | 64 | import { |
| 63 | 65 | getVersion, |
| 64 | 66 | getConfigValue, |
| @@ -130,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 | ||
| 130 | 132 | const DEFAULT_PORT = 8000; |
| 131 | 133 | const DEFAULT_AUTORUN = false; |
| 132 | 134 | const DEFAULT_LISTEN = false; |
| 135 | +const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]'; | |
| 136 | +const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0'; | |
| 133 | 137 | const DEFAULT_CORS_PROXY = false; |
| 134 | 138 | const DEFAULT_WHITELIST = true; |
| 135 | 139 | const DEFAULT_ACCOUNTS = false; |
| @@ -185,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv)) | ||
| 185 | 189 | type: 'boolean', |
| 186 | 190 | default: null, |
| 187 | 191 | 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}]`, |
| 192 | + }).option('listenAddressIPv6', { | |
| 193 | + type: 'string', | |
| 194 | + default: null, | |
| 195 | + describe: 'Set SillyTavern to listen to a specific IPv6 address. If not set, it will fallback to listen to all.\n[config default: [::] ]', | |
| 196 | + }).option('listenAddressIPv4', { | |
| 197 | + type: 'string', | |
| 198 | + default: null, | |
| 199 | + 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 ]', | |
| 188 | 200 | }).option('corsProxy', { |
| 189 | 201 | type: 'boolean', |
| 190 | 202 | default: null, |
| @@ -254,6 +266,10 @@ const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getCon | ||
| 254 | 266 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 255 | 267 | /** @type {boolean} */ |
| 256 | 268 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); |
| 269 | +/** @type {string} */ | |
| 270 | +const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6); | |
| 271 | +/** @type {string} */ | |
| 272 | +const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4); | |
| 257 | 273 | /** @type {boolean} */ |
| 258 | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 259 | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| @@ -500,7 +516,7 @@ if (!disableCsrf) { | ||
| 500 | 516 | |
| 501 | 517 | // Static files |
| 502 | 518 | // Host index page |
| 503 | 519 | app.get('/', getCacheBusterMiddleware(), (request, response) => { |
| 504 | 520 | if (shouldRedirectToLogin(request)) { |
| 505 | 521 | const query = request.url.split('?')[1]; |
| 506 | 522 | const redirectUrl = query ? `/login?${query}` : '/login'; |
| @@ -708,13 +724,13 @@ app.use('/api/azure', azureRouter); | ||
| 708 | 724 | |
| 709 | 725 | const tavernUrlV6 = new URL( |
| 710 | 726 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 711 | 727 | (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') + |
| 712 | 728 | (':' + server_port), |
| 713 | 729 | ); |
| 714 | 730 | |
| 715 | 731 | const tavernUrl = new URL( |
| 716 | 732 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 717 | 733 | (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') + |
| 718 | 734 | (':' + server_port), |
| 719 | 735 | ); |
| 720 | 736 | |
| @@ -837,15 +853,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { | ||
| 837 | 853 | const plainGoToLog = removeColorFormatting(goToLog); |
| 838 | 854 | |
| 839 | 855 | console.log(logListen); |
| 856 | + if (listen) { | |
| 857 | + console.log(); | |
| 858 | + console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".'); | |
| 859 | + console.log('Check the "access.log" file in the SillyTavern directory to inspect incoming connections.'); | |
| 860 | + } | |
| 840 | 861 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); |
| 841 | 862 | console.log(goToLog); |
| 842 | 863 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); |
| 843 | 864 | |
| 844 | - if (listen) { | |
| 845 | - console.log( | |
| 846 | - '[::] 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', | |
| 847 | - ); | |
| 848 | - } | |
| 849 | 865 | |
| 850 | 866 | if (basicAuthMode) { |
| 851 | 867 | if (perUserBasicAuth && !enableAccounts) { |
| @@ -1083,7 +1099,7 @@ async function verifySecuritySettings() { | ||
| 1083 | 1099 | } |
| 1084 | 1100 | |
| 1085 | 1101 | if (!enableAccounts) { |
| 1086 | 1102 | logSecurityAlert('Your current SillyTavern isconfiguration currentlyis insecurelyinsecure open(listening to the publicnon-localhost). Enable whitelisting, basic authentication or user accounts.'); |
| 1087 | 1103 | } |
| 1088 | 1104 | |
| 1089 | 1105 | const users = await getAllEnabledUsers(); |
| @@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [ | ||
| 304 | 304 | export const OLLAMA_KEYS = [ |
| 305 | 305 | 'num_predict', |
| 306 | 306 | 'num_ctx', |
| 307 | + 'num_batch', | |
| 307 | 308 | 'stop', |
| 308 | 309 | 'temperature', |
| 309 | 310 | 'repeat_penalty', |
| @@ -373,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) { | ||
| 373 | 373 | |
| 374 | 374 | if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { |
| 375 | 375 | const keepAlive = getConfigValue('ollama.keepAlive', -1); |
| 376 | + const numBatch = getConfigValue('ollama.batchSize', -1); | |
| 377 | + if (numBatch > 0) { | |
| 378 | + request.body['num_batch'] = numBatch; | |
| 379 | + } | |
| 376 | 380 | args.body = JSON.stringify({ |
| 377 | 381 | model: request.body.model, |
| 378 | 382 | prompt: request.body.prompt, |
| @@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi | ||
| 839 | 839 | invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url); |
| 840 | 840 | await writeCharacterData(newAvatarPath, char, targetFile, request, crop); |
| 841 | 841 | fs.unlinkSync(newAvatarPath); |
| 842 | + | |
| 843 | + // Bust cache to reload the new avatar | |
| 844 | + response.setHeader('Clear-Site-Data', '"cache"'); | |
| 842 | 845 | } |
| 843 | 846 | |
| 844 | 847 | return response.sendStatus(200); |
| @@ -50,6 +50,7 @@ export const SECRET_KEYS = { | ||
| 50 | 50 | TAVILY: 'api_key_tavily', |
| 51 | 51 | NANOGPT: 'api_key_nanogpt', |
| 52 | 52 | BFL: 'api_key_bfl', |
| 53 | + FALAI: 'api_key_falai', | |
| 53 | 54 | GENERIC: 'api_key_generic', |
| 54 | 55 | DEEPSEEK: 'api_key_deepseek', |
| 55 | 56 | SERPER: 'api_key_serper', |
| @@ -1228,6 +1228,131 @@ bfl.post('/generate', jsonParser, async (request, response) => { | ||
| 1228 | 1228 | } |
| 1229 | 1229 | }); |
| 1230 | 1230 | |
| 1231 | +const falai = express.Router(); | |
| 1232 | + | |
| 1233 | +falai.post('/models', jsonParser, async (_request, response) => { | |
| 1234 | + try { | |
| 1235 | + const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image'); | |
| 1236 | + const result = await fetch(modelsUrl); | |
| 1237 | + | |
| 1238 | + if (!result.ok) { | |
| 1239 | + console.warn('FAL.AI returned an error.', result.status, result.statusText); | |
| 1240 | + throw new Error('FAL.AI request failed.'); | |
| 1241 | + } | |
| 1242 | + | |
| 1243 | + const data = await result.json(); | |
| 1244 | + | |
| 1245 | + if (!Array.isArray(data)) { | |
| 1246 | + console.warn('FAL.AI returned invalid data.'); | |
| 1247 | + throw new Error('FAL.AI request failed.'); | |
| 1248 | + } | |
| 1249 | + | |
| 1250 | + const models = data | |
| 1251 | + .filter(x => !x.title.toLowerCase().includes('inpainting') && | |
| 1252 | + !x.title.toLowerCase().includes('control') && | |
| 1253 | + !x.title.toLowerCase().includes('upscale')) | |
| 1254 | + .sort((a, b) => a.title.localeCompare(b.title)) | |
| 1255 | + .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); | |
| 1256 | + return response.send(models); | |
| 1257 | + } catch (error) { | |
| 1258 | + console.error(error); | |
| 1259 | + return response.sendStatus(500); | |
| 1260 | + } | |
| 1261 | +}); | |
| 1262 | + | |
| 1263 | +falai.post('/generate', jsonParser, async (request, response) => { | |
| 1264 | + try { | |
| 1265 | + const key = readSecret(request.user.directories, SECRET_KEYS.FALAI); | |
| 1266 | + | |
| 1267 | + if (!key) { | |
| 1268 | + console.warn('FAL.AI key not found.'); | |
| 1269 | + return response.sendStatus(400); | |
| 1270 | + } | |
| 1271 | + | |
| 1272 | + const requestBody = { | |
| 1273 | + prompt: request.body.prompt, | |
| 1274 | + image_size: { 'width': request.body.width, 'height': request.body.height }, | |
| 1275 | + num_inference_steps: request.body.steps, | |
| 1276 | + seed: request.body.seed ?? null, | |
| 1277 | + guidance_scale: request.body.guidance, | |
| 1278 | + enable_safety_checker: false, | |
| 1279 | + }; | |
| 1280 | + | |
| 1281 | + console.debug('FAL.AI request:', requestBody); | |
| 1282 | + | |
| 1283 | + const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, { | |
| 1284 | + method: 'POST', | |
| 1285 | + body: JSON.stringify(requestBody), | |
| 1286 | + headers: { | |
| 1287 | + 'Content-Type': 'application/json', | |
| 1288 | + 'Authorization': `Key ${key}`, | |
| 1289 | + }, | |
| 1290 | + }); | |
| 1291 | + | |
| 1292 | + if (!result.ok) { | |
| 1293 | + console.warn('FAL.AI returned an error.'); | |
| 1294 | + return response.sendStatus(500); | |
| 1295 | + } | |
| 1296 | + | |
| 1297 | + /** @type {any} */ | |
| 1298 | + const taskData = await result.json(); | |
| 1299 | + const { status_url } = taskData; | |
| 1300 | + | |
| 1301 | + const MAX_ATTEMPTS = 100; | |
| 1302 | + for (let i = 0; i < MAX_ATTEMPTS; i++) { | |
| 1303 | + await delay(2500); | |
| 1304 | + | |
| 1305 | + const statusResult = await fetch(status_url, { | |
| 1306 | + headers: { | |
| 1307 | + 'Authorization': `Key ${key}`, | |
| 1308 | + }, | |
| 1309 | + }); | |
| 1310 | + | |
| 1311 | + if (!statusResult.ok) { | |
| 1312 | + const text = await statusResult.text(); | |
| 1313 | + console.warn('FAL.AI returned an error.', text); | |
| 1314 | + return response.sendStatus(500); | |
| 1315 | + } | |
| 1316 | + | |
| 1317 | + /** @type {any} */ | |
| 1318 | + const statusData = await statusResult.json(); | |
| 1319 | + | |
| 1320 | + if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') { | |
| 1321 | + continue; | |
| 1322 | + } | |
| 1323 | + | |
| 1324 | + if (statusData?.status === 'COMPLETED') { | |
| 1325 | + const resultFetch = await fetch(statusData?.response_url, { | |
| 1326 | + method: 'GET', | |
| 1327 | + headers: { | |
| 1328 | + 'Authorization': `Key ${key}`, | |
| 1329 | + }, | |
| 1330 | + }); | |
| 1331 | + const resultData = await resultFetch.json(); | |
| 1332 | + | |
| 1333 | + if (resultData.detail !== null && resultData.detail !== undefined) { | |
| 1334 | + throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` }); | |
| 1335 | + } | |
| 1336 | + | |
| 1337 | + const imageFetch = await fetch(resultData?.images[0].url, { | |
| 1338 | + headers: { | |
| 1339 | + 'Authorization': `Key ${key}`, | |
| 1340 | + }, | |
| 1341 | + }); | |
| 1342 | + | |
| 1343 | + const fetchData = await imageFetch.arrayBuffer(); | |
| 1344 | + const image = Buffer.from(fetchData).toString('base64'); | |
| 1345 | + return response.send({ image: image }); | |
| 1346 | + } | |
| 1347 | + | |
| 1348 | + throw new Error('FAL.AI failed to generate image.', { cause: statusData }); | |
| 1349 | + } | |
| 1350 | + } catch (error) { | |
| 1351 | + console.error(error); | |
| 1352 | + return response.status(500).send(error.cause || error.message); | |
| 1353 | + } | |
| 1354 | +}); | |
| 1355 | + | |
| 1231 | 1356 | router.use('/comfy', comfy); |
| 1232 | 1357 | router.use('/together', together); |
| 1233 | 1358 | router.use('/drawthings', drawthings); |
| @@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy); | ||
| 1237 | 1362 | router.use('/huggingface', huggingface); |
| 1238 | 1363 | router.use('/nanogpt', nanogpt); |
| 1239 | 1364 | router.use('/bfl', bfl); |
| 1365 | +router.use('/falai', falai); | |
| @@ -0,0 +1,28 @@ | ||
| 1 | +import crypto from 'node:crypto'; | |
| 2 | +import { DEFAULT_USER } from '../constants.js'; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Middleware to bust the browser cache for the current user. | |
| 6 | + * @returns {import('express').RequestHandler} | |
| 7 | + */ | |
| 8 | +export default function getCacheBusterMiddleware() { | |
| 9 | + /** | |
| 10 | + * @type {Set<string>} Handles/User-Agents that have already been busted. | |
| 11 | + */ | |
| 12 | + const keys = new Set(); | |
| 13 | + | |
| 14 | + return (request, response, next) => { | |
| 15 | + const handle = request.user?.profile?.handle || DEFAULT_USER.handle; | |
| 16 | + const userAgent = request.headers['user-agent'] || ''; | |
| 17 | + const hash = crypto.createHash('sha256').update(userAgent).digest('hex'); | |
| 18 | + const key = `${handle}-${hash}`; | |
| 19 | + | |
| 20 | + if (keys.has(key)) { | |
| 21 | + return next(); | |
| 22 | + } | |
| 23 | + | |
| 24 | + keys.add(key); | |
| 25 | + response.setHeader('Clear-Site-Data', '"cache"'); | |
| 26 | + next(); | |
| 27 | + }; | |
| 28 | +} | |