Merge branch 'staging' into tc-global-banlist
| @@ -6,6 +6,10 @@ cardsCacheCapacity: 100 | |||
| 6 | # -- SERVER CONFIGURATION -- | 6 | # -- SERVER CONFIGURATION -- |
| 7 | # Listen for incoming connections | 7 | # Listen for incoming connections |
| 8 | listen: false | 8 | listen: false |
| 9 | # Listen on a specific address, supports IPv4 and IPv6 | ||
| 10 | listenAddress: | ||
| 11 | ipv4: 0.0.0.0 | ||
| 12 | ipv6: '[::]' | ||
| 9 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! | 13 | # Enables IPv6 and/or IPv4 protocols. Need to have at least one enabled! |
| 10 | # - Use option "auto" to automatically detect support | 14 | # - Use option "auto" to automatically detect support |
| 11 | # - Use true or false (no qoutes) to enable or disable each protocol | 15 | # - Use true or false (no qoutes) to enable or disable each protocol |
| @@ -183,6 +187,10 @@ ollama: | |||
| 183 | # * 0: Unload the model immediately after the request | 187 | # * 0: Unload the model immediately after the request |
| 184 | # * N (any positive number): Keep the model loaded for N seconds after the request. | 188 | # * N (any positive number): Keep the model loaded for N seconds after the request. |
| 185 | keepAlive: -1 | 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 | # -- ANTHROPIC CLAUDE API CONFIGURATION -- | 194 | # -- ANTHROPIC CLAUDE API CONFIGURATION -- |
| 187 | claude: | 195 | claude: |
| 188 | # Enables caching of the system prompt (if supported). | 196 | # Enables caching of the system prompt (if supported). |
| @@ -672,10 +672,6 @@ | |||
| 672 | "type": "moving_ui" | 672 | "type": "moving_ui" |
| 673 | }, | 673 | }, |
| 674 | { | 674 | { |
| 675 | "filename": "presets/moving-ui/Black Magic Time.json", | ||
| 676 | "type": "moving_ui" | ||
| 677 | }, | ||
| 678 | { | ||
| 679 | "filename": "presets/quick-replies/Default.json", | 675 | "filename": "presets/quick-replies/Default.json", |
| 680 | "type": "quick_replies" | 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 | \ No newline at end of file | |
| @@ -41,6 +41,7 @@ | |||
| 41 | "html-entities": "^2.5.2", | 41 | "html-entities": "^2.5.2", |
| 42 | "iconv-lite": "^0.6.3", | 42 | "iconv-lite": "^0.6.3", |
| 43 | "ip-matching": "^2.1.2", | 43 | "ip-matching": "^2.1.2", |
| 44 | "ip-regex": "^5.0.0", | ||
| 44 | "ipaddr.js": "^2.0.1", | 45 | "ipaddr.js": "^2.0.1", |
| 45 | "jimp": "^0.22.10", | 46 | "jimp": "^0.22.10", |
| 46 | "localforage": "^1.10.0", | 47 | "localforage": "^1.10.0", |
| @@ -4628,6 +4629,18 @@ | |||
| 4628 | "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==", | 4629 | "integrity": "sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==", |
| 4629 | "license": "LGPL-3.0-only" | 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 | "node_modules/ipaddr.js": { | 4644 | "node_modules/ipaddr.js": { |
| 4632 | "version": "2.1.0", | 4645 | "version": "2.1.0", |
| 4633 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", | 4646 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", |
| @@ -31,6 +31,7 @@ | |||
| 31 | "html-entities": "^2.5.2", | 31 | "html-entities": "^2.5.2", |
| 32 | "iconv-lite": "^0.6.3", | 32 | "iconv-lite": "^0.6.3", |
| 33 | "ip-matching": "^2.1.2", | 33 | "ip-matching": "^2.1.2", |
| 34 | "ip-regex": "^5.0.0", | ||
| 34 | "ipaddr.js": "^2.0.1", | 35 | "ipaddr.js": "^2.0.1", |
| 35 | "jimp": "^0.22.10", | 36 | "jimp": "^0.22.10", |
| 36 | "localforage": "^1.10.0", | 37 | "localforage": "^1.10.0", |
| @@ -89,6 +90,7 @@ | |||
| 89 | "version": "1.12.11", | 90 | "version": "1.12.11", |
| 90 | "scripts": { | 91 | "scripts": { |
| 91 | "start": "node server.js", | 92 | "start": "node server.js", |
| 93 | "debug": "node server.js --inspect", | ||
| 92 | "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", | 94 | "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js", |
| 93 | "start:bun": "bun server.js", | 95 | "start:bun": "bun server.js", |
| 94 | "start:no-csrf": "node server.js --disableCsrf", | 96 | "start:no-csrf": "node server.js --disableCsrf", |
| @@ -81,6 +81,7 @@ const sources = { | |||
| 81 | huggingface: 'huggingface', | 81 | huggingface: 'huggingface', |
| 82 | nanogpt: 'nanogpt', | 82 | nanogpt: 'nanogpt', |
| 83 | bfl: 'bfl', | 83 | bfl: 'bfl', |
| 84 | falai: 'falai', | ||
| 84 | }; | 85 | }; |
| 85 | 86 | ||
| 86 | const initiators = { | 87 | const initiators = { |
| @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { | |||
| 1169 | return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); | 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 | function onBflUpsamplingInput() { | 1177 | function onBflUpsamplingInput() { |
| 1173 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); | 1178 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 1174 | saveSettingsDebounced(); | 1179 | saveSettingsDebounced(); |
| @@ -1299,6 +1304,7 @@ async function onModelChange() { | |||
| 1299 | sources.huggingface, | 1304 | sources.huggingface, |
| 1300 | sources.nanogpt, | 1305 | sources.nanogpt, |
| 1301 | sources.bfl, | 1306 | sources.bfl, |
| 1307 | sources.falai, | ||
| 1302 | ]; | 1308 | ]; |
| 1303 | 1309 | ||
| 1304 | if (cloudSources.includes(extension_settings.sd.source)) { | 1310 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1707,6 +1713,9 @@ async function loadModels() { | |||
| 1707 | case sources.bfl: | 1713 | case sources.bfl: |
| 1708 | models = await loadBflModels(); | 1714 | models = await loadBflModels(); |
| 1709 | break; | 1715 | break; |
| 1716 | case sources.falai: | ||
| 1717 | models = await loadFalaiModels(); | ||
| 1718 | break; | ||
| 1710 | } | 1719 | } |
| 1711 | 1720 | ||
| 1712 | for (const model of models) { | 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 | async function loadPollinationsModels() { | 1771 | async function loadPollinationsModels() { |
| 1748 | const result = await fetch('/api/sd/pollinations/models', { | 1772 | const result = await fetch('/api/sd/pollinations/models', { |
| 1749 | method: 'POST', | 1773 | method: 'POST', |
| @@ -2081,6 +2105,9 @@ async function loadSchedulers() { | |||
| 2081 | case sources.bfl: | 2105 | case sources.bfl: |
| 2082 | schedulers = ['N/A']; | 2106 | schedulers = ['N/A']; |
| 2083 | break; | 2107 | break; |
| 2108 | case sources.falai: | ||
| 2109 | schedulers = ['N/A']; | ||
| 2110 | break; | ||
| 2084 | } | 2111 | } |
| 2085 | 2112 | ||
| 2086 | for (const scheduler of schedulers) { | 2113 | for (const scheduler of schedulers) { |
| @@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 2735 | case sources.bfl: | 2762 | case sources.bfl: |
| 2736 | result = await generateBflImage(prefixedPrompt, signal); | 2763 | result = await generateBflImage(prefixedPrompt, signal); |
| 2737 | break; | 2764 | break; |
| 2765 | case sources.falai: | ||
| 2766 | result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); | ||
| 2767 | break; | ||
| 2738 | } | 2768 | } |
| 2739 | 2769 | ||
| 2740 | if (!result.data) { | 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 | async function onComfyOpenWorkflowEditorClick() { | 3563 | async function onComfyOpenWorkflowEditorClick() { |
| 3500 | let workflow = await (await fetch('/api/sd/comfy/workflow', { | 3564 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 3501 | method: 'POST', | 3565 | method: 'POST', |
| @@ -3782,6 +3846,8 @@ function isValidState() { | |||
| 3782 | return secret_state[SECRET_KEYS.NANOGPT]; | 3846 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 3783 | case sources.bfl: | 3847 | case sources.bfl: |
| 3784 | return secret_state[SECRET_KEYS.BFL]; | 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 | $('#sd_function_tool').on('input', onFunctionToolInput); | 4509 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4444 | $('#sd_bfl_key').on('click', onBflKeyClick); | 4510 | $('#sd_bfl_key').on('click', onBflKeyClick); |
| 4445 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); | 4511 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4512 | $('#sd_falai_key').on('click', onFalaiKeyClick); | ||
| 4446 | 4513 | ||
| 4447 | if (!CSS.supports('field-sizing', 'content')) { | 4514 | if (!CSS.supports('field-sizing', 'content')) { |
| 4448 | $('.sd_settings .inline-drawer-toggle').on('click', function () { | 4515 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -42,6 +42,7 @@ | |||
| 42 | <option value="comfy">ComfyUI</option> | 42 | <option value="comfy">ComfyUI</option> |
| 43 | <option value="drawthings">DrawThings HTTP API</option> | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | <option value="extras">Extras API (deprecated)</option> | 44 | <option value="extras">Extras API (deprecated)</option> |
| 45 | <option value="falai">FAL.AI</option> | ||
| 45 | <option value="huggingface">HuggingFace Inference API (serverless)</option> | 46 | <option value="huggingface">HuggingFace Inference API (serverless)</option> |
| 46 | <option value="nanogpt">NanoGPT</option> | 47 | <option value="nanogpt">NanoGPT</option> |
| 47 | <option value="novel">NovelAI Diffusion</option> | 48 | <option value="novel">NovelAI Diffusion</option> |
| @@ -256,6 +257,20 @@ | |||
| 256 | </label> | 257 | </label> |
| 257 | </div> | 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 | <div class="flex-container"> | 274 | <div class="flex-container"> |
| 260 | <div class="flex1"> | 275 | <div class="flex1"> |
| 261 | <label for="sd_model" data-i18n="Model">Model</label> | 276 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -215,6 +215,10 @@ export class ReasoningHandler { | |||
| 215 | } | 215 | } |
| 216 | 216 | ||
| 217 | this.updateDom(messageId); | 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 | GENERIC: 'api_key_generic', | 41 | GENERIC: 'api_key_generic', |
| 42 | DEEPSEEK: 'api_key_deepseek', | 42 | DEEPSEEK: 'api_key_deepseek', |
| 43 | SERPER: 'api_key_serper', | 43 | SERPER: 'api_key_serper', |
| 44 | FALAI: 'api_key_falai', | ||
| 44 | }; | 45 | }; |
| 45 | 46 | ||
| 46 | const INPUT_MAP = { | 47 | const INPUT_MAP = { |
| @@ -362,9 +362,14 @@ input[type='checkbox']:focus-visible { | |||
| 362 | } | 362 | } |
| 363 | 363 | ||
| 364 | .mes_reasoning_details .mes_reasoning_summary { | 364 | .mes_reasoning_details .mes_reasoning_summary { |
| 365 | list-style: none; | ||
| 365 | margin-right: calc(var(--mes-right-spacing) * -1); | 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 | .mes_reasoning *:last-child { | 373 | .mes_reasoning *:last-child { |
| 369 | margin-bottom: 0; | 374 | margin-bottom: 0; |
| 370 | } | 375 | } |
| @@ -378,7 +383,7 @@ input[type='checkbox']:focus-visible { | |||
| 378 | } | 383 | } |
| 379 | 384 | ||
| 380 | .mes_reasoning_details .mes_reasoning em { | 385 | .mes_reasoning_details .mes_reasoning em { |
| 381 | color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%) | 386 | color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%); |
| 382 | } | 387 | } |
| 383 | 388 | ||
| 384 | .mes_reasoning_header_block { | 389 | .mes_reasoning_header_block { |
| @@ -403,7 +408,7 @@ input[type='checkbox']:focus-visible { | |||
| 403 | } | 408 | } |
| 404 | 409 | ||
| 405 | /* TWIMC: Remove with custom CSS to show the icon */ | 410 | /* TWIMC: Remove with custom CSS to show the icon */ |
| 406 | .mes_reasoning_header > .icon-svg { | 411 | .mes_reasoning_header>.icon-svg { |
| 407 | display: none; | 412 | display: none; |
| 408 | } | 413 | } |
| 409 | 414 | ||
| @@ -30,6 +30,7 @@ import bodyParser from 'body-parser'; | |||
| 30 | 30 | ||
| 31 | // net related library imports | 31 | // net related library imports |
| 32 | import fetch from 'node-fetch'; | 32 | import fetch from 'node-fetch'; |
| 33 | import ipRegex from 'ip-regex'; | ||
| 33 | 34 | ||
| 34 | // Unrestrict console logs display limit | 35 | // Unrestrict console logs display limit |
| 35 | util.inspect.defaultOptions.maxArrayLength = null; | 36 | util.inspect.defaultOptions.maxArrayLength = null; |
| @@ -59,6 +60,7 @@ import basicAuthMiddleware from './src/middleware/basicAuth.js'; | |||
| 59 | import whitelistMiddleware from './src/middleware/whitelist.js'; | 60 | import whitelistMiddleware from './src/middleware/whitelist.js'; |
| 60 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; | 61 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 61 | import initRequestProxy from './src/request-proxy.js'; | 62 | import initRequestProxy from './src/request-proxy.js'; |
| 63 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; | ||
| 62 | import { | 64 | import { |
| 63 | getVersion, | 65 | getVersion, |
| 64 | getConfigValue, | 66 | getConfigValue, |
| @@ -130,6 +132,8 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 | |||
| 130 | const DEFAULT_PORT = 8000; | 132 | const DEFAULT_PORT = 8000; |
| 131 | const DEFAULT_AUTORUN = false; | 133 | const DEFAULT_AUTORUN = false; |
| 132 | const DEFAULT_LISTEN = false; | 134 | const DEFAULT_LISTEN = false; |
| 135 | const DEFAULT_LISTEN_ADDRESS_IPV6 = '[::]'; | ||
| 136 | const DEFAULT_LISTEN_ADDRESS_IPV4 = '0.0.0.0'; | ||
| 133 | const DEFAULT_CORS_PROXY = false; | 137 | const DEFAULT_CORS_PROXY = false; |
| 134 | const DEFAULT_WHITELIST = true; | 138 | const DEFAULT_WHITELIST = true; |
| 135 | const DEFAULT_ACCOUNTS = false; | 139 | const DEFAULT_ACCOUNTS = false; |
| @@ -185,6 +189,14 @@ const cliArguments = yargs(hideBin(process.argv)) | |||
| 185 | type: 'boolean', | 189 | type: 'boolean', |
| 186 | default: null, | 190 | default: null, |
| 187 | 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}]`, | 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 | }).option('corsProxy', { | 200 | }).option('corsProxy', { |
| 189 | type: 'boolean', | 201 | type: 'boolean', |
| 190 | default: null, | 202 | default: null, |
| @@ -254,6 +266,10 @@ const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getCon | |||
| 254 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; | 266 | const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; |
| 255 | /** @type {boolean} */ | 267 | /** @type {boolean} */ |
| 256 | const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); | 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 | /** @type {boolean} */ | 273 | /** @type {boolean} */ |
| 258 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); | 274 | const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); |
| 259 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); | 275 | const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); |
| @@ -500,7 +516,7 @@ if (!disableCsrf) { | |||
| 500 | 516 | ||
| 501 | // Static files | 517 | // Static files |
| 502 | // Host index page | 518 | // Host index page |
| 503 | app.get('/', (request, response) => { | 519 | app.get('/', getCacheBusterMiddleware(), (request, response) => { |
| 504 | if (shouldRedirectToLogin(request)) { | 520 | if (shouldRedirectToLogin(request)) { |
| 505 | const query = request.url.split('?')[1]; | 521 | const query = request.url.split('?')[1]; |
| 506 | const redirectUrl = query ? `/login?${query}` : '/login'; | 522 | const redirectUrl = query ? `/login?${query}` : '/login'; |
| @@ -708,13 +724,13 @@ app.use('/api/azure', azureRouter); | |||
| 708 | 724 | ||
| 709 | const tavernUrlV6 = new URL( | 725 | const tavernUrlV6 = new URL( |
| 710 | (cliArguments.ssl ? 'https://' : 'http://') + | 726 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 711 | (listen ? '[::]' : '[::1]') + | 727 | (listen ? (ipRegex.v6({ exact: true }).test(listenAddressIPv6) ? listenAddressIPv6 : '[::]') : '[::1]') + |
| 712 | (':' + server_port), | 728 | (':' + server_port), |
| 713 | ); | 729 | ); |
| 714 | 730 | ||
| 715 | const tavernUrl = new URL( | 731 | const tavernUrl = new URL( |
| 716 | (cliArguments.ssl ? 'https://' : 'http://') + | 732 | (cliArguments.ssl ? 'https://' : 'http://') + |
| 717 | (listen ? '0.0.0.0' : '127.0.0.1') + | 733 | (listen ? (ipRegex.v4({ exact: true }).test(listenAddressIPv4) ? listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') + |
| 718 | (':' + server_port), | 734 | (':' + server_port), |
| 719 | ); | 735 | ); |
| 720 | 736 | ||
| @@ -837,15 +853,15 @@ const postSetupTasks = async function (v6Failed, v4Failed, useIPv6, useIPv4) { | |||
| 837 | const plainGoToLog = removeColorFormatting(goToLog); | 853 | const plainGoToLog = removeColorFormatting(goToLog); |
| 838 | 854 | ||
| 839 | console.log(logListen); | 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 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); | 861 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); |
| 841 | console.log(goToLog); | 862 | console.log(goToLog); |
| 842 | console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); | 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 | if (basicAuthMode) { | 866 | if (basicAuthMode) { |
| 851 | if (perUserBasicAuth && !enableAccounts) { | 867 | if (perUserBasicAuth && !enableAccounts) { |
| @@ -1083,7 +1099,7 @@ async function verifySecuritySettings() { | |||
| 1083 | } | 1099 | } |
| 1084 | 1100 | ||
| 1085 | if (!enableAccounts) { | 1101 | if (!enableAccounts) { |
| 1086 | logSecurityAlert('Your SillyTavern is currently insecurely open to the public. Enable whitelisting, basic authentication or user accounts.'); | 1102 | logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.'); |
| 1087 | } | 1103 | } |
| 1088 | 1104 | ||
| 1089 | const users = await getAllEnabledUsers(); | 1105 | const users = await getAllEnabledUsers(); |
| @@ -304,6 +304,7 @@ export const TOGETHERAI_KEYS = [ | |||
| 304 | export const OLLAMA_KEYS = [ | 304 | export const OLLAMA_KEYS = [ |
| 305 | 'num_predict', | 305 | 'num_predict', |
| 306 | 'num_ctx', | 306 | 'num_ctx', |
| 307 | 'num_batch', | ||
| 307 | 'stop', | 308 | 'stop', |
| 308 | 'temperature', | 309 | 'temperature', |
| 309 | 'repeat_penalty', | 310 | 'repeat_penalty', |
| @@ -373,6 +373,10 @@ router.post('/generate', jsonParser, async function (request, response) { | |||
| 373 | 373 | ||
| 374 | if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { | 374 | if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { |
| 375 | const keepAlive = getConfigValue('ollama.keepAlive', -1); | 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 | args.body = JSON.stringify({ | 380 | args.body = JSON.stringify({ |
| 377 | model: request.body.model, | 381 | model: request.body.model, |
| 378 | prompt: request.body.prompt, | 382 | prompt: request.body.prompt, |
| @@ -839,6 +839,9 @@ router.post('/edit', urlencodedParser, validateAvatarUrlMiddleware, async functi | |||
| 839 | invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url); | 839 | invalidateThumbnail(request.user.directories, 'avatar', request.body.avatar_url); |
| 840 | await writeCharacterData(newAvatarPath, char, targetFile, request, crop); | 840 | await writeCharacterData(newAvatarPath, char, targetFile, request, crop); |
| 841 | fs.unlinkSync(newAvatarPath); | 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 | return response.sendStatus(200); | 847 | return response.sendStatus(200); |
| @@ -50,6 +50,7 @@ export const SECRET_KEYS = { | |||
| 50 | TAVILY: 'api_key_tavily', | 50 | TAVILY: 'api_key_tavily', |
| 51 | NANOGPT: 'api_key_nanogpt', | 51 | NANOGPT: 'api_key_nanogpt', |
| 52 | BFL: 'api_key_bfl', | 52 | BFL: 'api_key_bfl', |
| 53 | FALAI: 'api_key_falai', | ||
| 53 | GENERIC: 'api_key_generic', | 54 | GENERIC: 'api_key_generic', |
| 54 | DEEPSEEK: 'api_key_deepseek', | 55 | DEEPSEEK: 'api_key_deepseek', |
| 55 | SERPER: 'api_key_serper', | 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 | router.use('/comfy', comfy); | 1356 | router.use('/comfy', comfy); |
| 1232 | router.use('/together', together); | 1357 | router.use('/together', together); |
| 1233 | router.use('/drawthings', drawthings); | 1358 | router.use('/drawthings', drawthings); |
| @@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy); | |||
| 1237 | router.use('/huggingface', huggingface); | 1362 | router.use('/huggingface', huggingface); |
| 1238 | router.use('/nanogpt', nanogpt); | 1363 | router.use('/nanogpt', nanogpt); |
| 1239 | router.use('/bfl', bfl); | 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 | } | ||