Merge pull request #2471 from WBlair1/staging Stable image from StabilityAI api
Signed| @@ -22,7 +22,7 @@ import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, rend | ||
| 22 | 22 | import { selected_group } from '../../group-chats.js'; |
| 23 | 23 | import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js'; |
| 24 | 24 | import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; |
| 25 | 25 | import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; |
| 26 | 26 | import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js'; |
| 27 | 27 | import { getMultimodalCaption } from '../shared.js'; |
| 28 | 28 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -49,6 +49,7 @@ const sources = { | ||
| 49 | 49 | togetherai: 'togetherai', |
| 50 | 50 | drawthings: 'drawthings', |
| 51 | 51 | pollinations: 'pollinations', |
| 52 | + stability: 'stability', | |
| 52 | 53 | }; |
| 53 | 54 | |
| 54 | 55 | const initiators = { |
| @@ -282,6 +283,9 @@ const defaultSettings = { | ||
| 282 | 283 | wand_visible: false, |
| 283 | 284 | command_visible: false, |
| 284 | 285 | interactive_visible: false, |
| 286 | + | |
| 287 | + // Stability AI settings | |
| 288 | + stability_style_preset: 'anime', | |
| 285 | 289 | }; |
| 286 | 290 | |
| 287 | 291 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| @@ -444,6 +448,7 @@ async function loadSettings() { | ||
| 444 | 448 | $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible); |
| 445 | 449 | $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible); |
| 446 | 450 | $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible); |
| 451 | + $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset); | |
| 447 | 452 | |
| 448 | 453 | for (const style of extension_settings.sd.styles) { |
| 449 | 454 | const option = document.createElement('option'); |
| @@ -671,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) { | ||
| 671 | 676 | const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' }); |
| 672 | 677 | |
| 673 | 678 | if (refinedPrompt) { |
| 674 | 679 | return String(refinedPrompt); |
| 675 | 680 | } else { |
| 676 | 681 | throw new Error('Generation aborted by user.'); |
| 677 | 682 | } |
| @@ -1084,6 +1089,26 @@ function onComfyWorkflowChange() { | ||
| 1084 | 1089 | extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val(); |
| 1085 | 1090 | saveSettingsDebounced(); |
| 1086 | 1091 | } |
| 1092 | + | |
| 1093 | +async function onStabilityKeyClick() { | |
| 1094 | + const popupText = 'Stability AI API Key:'; | |
| 1095 | + const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT); | |
| 1096 | + | |
| 1097 | + if (!key) { | |
| 1098 | + return; | |
| 1099 | + } | |
| 1100 | + | |
| 1101 | + await writeSecret(SECRET_KEYS.STABILITY, String(key)); | |
| 1102 | + | |
| 1103 | + toastr.success('API Key saved'); | |
| 1104 | + await loadSettingOptions(); | |
| 1105 | +} | |
| 1106 | + | |
| 1107 | +function onStabilityStylePresetChange() { | |
| 1108 | + extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val()); | |
| 1109 | + saveSettingsDebounced(); | |
| 1110 | +} | |
| 1111 | + | |
| 1087 | 1112 | async function changeComfyWorkflow(_, name) { |
| 1088 | 1113 | name = name.replace(/(\.json)?$/i, '.json'); |
| 1089 | 1114 | if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) { |
| @@ -1193,7 +1218,7 @@ async function onModelChange() { | ||
| 1193 | 1218 | extension_settings.sd.model = $('#sd_model').find(':selected').val(); |
| 1194 | 1219 | saveSettingsDebounced(); |
| 1195 | 1220 | |
| 1196 | 1221 | const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability]; |
| 1197 | 1222 | |
| 1198 | 1223 | if (cloudSources.includes(extension_settings.sd.source)) { |
| 1199 | 1224 | return; |
| @@ -1402,6 +1427,9 @@ async function loadSamplers() { | ||
| 1402 | 1427 | case sources.pollinations: |
| 1403 | 1428 | samplers = ['N/A']; |
| 1404 | 1429 | break; |
| 1430 | + case sources.stability: | |
| 1431 | + samplers = ['N/A']; | |
| 1432 | + break; | |
| 1405 | 1433 | } |
| 1406 | 1434 | |
| 1407 | 1435 | for (const sampler of samplers) { |
| @@ -1585,6 +1613,9 @@ async function loadModels() { | ||
| 1585 | 1613 | case sources.pollinations: |
| 1586 | 1614 | models = await loadPollinationsModels(); |
| 1587 | 1615 | break; |
| 1616 | + case sources.stability: | |
| 1617 | + models = await loadStabilityModels(); | |
| 1618 | + break; | |
| 1588 | 1619 | } |
| 1589 | 1620 | |
| 1590 | 1621 | for (const model of models) { |
| @@ -1601,6 +1632,16 @@ async function loadModels() { | ||
| 1601 | 1632 | } |
| 1602 | 1633 | } |
| 1603 | 1634 | |
| 1635 | +async function loadStabilityModels() { | |
| 1636 | + $('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]); | |
| 1637 | + | |
| 1638 | + return [ | |
| 1639 | + { value: 'stable-image-ultra', text: 'Stable Image Ultra' }, | |
| 1640 | + { value: 'stable-image-core', text: 'Stable Image Core' }, | |
| 1641 | + { value: 'stable-diffusion-3', text: 'Stable Diffusion 3' }, | |
| 1642 | + ]; | |
| 1643 | +} | |
| 1644 | + | |
| 1604 | 1645 | async function loadPollinationsModels() { |
| 1605 | 1646 | return [ |
| 1606 | 1647 | { |
| @@ -1932,6 +1973,9 @@ async function loadSchedulers() { | ||
| 1932 | 1973 | case sources.comfy: |
| 1933 | 1974 | schedulers = await loadComfySchedulers(); |
| 1934 | 1975 | break; |
| 1976 | + case sources.stability: | |
| 1977 | + schedulers = ['N/A']; | |
| 1978 | + break; | |
| 1935 | 1979 | } |
| 1936 | 1980 | |
| 1937 | 1981 | for (const scheduler of schedulers) { |
| @@ -2005,6 +2049,9 @@ async function loadVaes() { | ||
| 2005 | 2049 | case sources.comfy: |
| 2006 | 2050 | vaes = await loadComfyVaes(); |
| 2007 | 2051 | break; |
| 2052 | + case sources.stability: | |
| 2053 | + vaes = ['N/A']; | |
| 2054 | + break; | |
| 2008 | 2055 | } |
| 2009 | 2056 | |
| 2010 | 2057 | for (const vae of vaes) { |
| @@ -2485,6 +2532,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2485 | 2532 | case sources.pollinations: |
| 2486 | 2533 | result = await generatePollinationsImage(prefixedPrompt, negativePrompt); |
| 2487 | 2534 | break; |
| 2535 | + case sources.stability: | |
| 2536 | + result = await generateStabilityImage(prefixedPrompt, negativePrompt); | |
| 2537 | + break; | |
| 2488 | 2538 | } |
| 2489 | 2539 | |
| 2490 | 2540 | if (!result.data) { |
| @@ -2508,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2508 | 2558 | return base64Image; |
| 2509 | 2559 | } |
| 2510 | 2560 | |
| 2561 | +/** | |
| 2562 | + * Generates an image using the TogetherAI API. | |
| 2563 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 2564 | + * @param {string} negativePrompt - The instruction used to restrict the image generation. | |
| 2565 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 2566 | + */ | |
| 2511 | 2567 | async function generateTogetherAIImage(prompt, negativePrompt) { |
| 2512 | 2568 | const result = await fetch('/api/sd/together/generate', { |
| 2513 | 2569 | method: 'POST', |
| @@ -2532,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) { | ||
| 2532 | 2588 | } |
| 2533 | 2589 | } |
| 2534 | 2590 | |
| 2591 | +/** | |
| 2592 | + * Generates an image using the Pollinations API. | |
| 2593 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 2594 | + * @param {string} negativePrompt - The instruction used to restrict the image generation. | |
| 2595 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 2596 | + */ | |
| 2535 | 2597 | async function generatePollinationsImage(prompt, negativePrompt) { |
| 2536 | 2598 | const result = await fetch('/api/sd/pollinations/generate', { |
| 2537 | 2599 | method: 'POST', |
| @@ -2601,6 +2663,84 @@ async function generateExtrasImage(prompt, negativePrompt) { | ||
| 2601 | 2663 | } |
| 2602 | 2664 | |
| 2603 | 2665 | /** |
| 2666 | + * Gets an aspect ratio for Stability that is the closest to the given width and height. | |
| 2667 | + * @param {number} width Target width | |
| 2668 | + * @param {number} height Target height | |
| 2669 | + * @returns {string} Closest aspect ratio as a string | |
| 2670 | + */ | |
| 2671 | +function getClosestAspectRatio(width, height) { | |
| 2672 | + const aspectRatios = { | |
| 2673 | + '16:9': 16 / 9, | |
| 2674 | + '1:1': 1, | |
| 2675 | + '21:9': 21 / 9, | |
| 2676 | + '2:3': 2 / 3, | |
| 2677 | + '3:2': 3 / 2, | |
| 2678 | + '4:5': 4 / 5, | |
| 2679 | + '5:4': 5 / 4, | |
| 2680 | + '9:16': 9 / 16, | |
| 2681 | + '9:21': 9 / 21, | |
| 2682 | + }; | |
| 2683 | + | |
| 2684 | + const aspectRatio = width / height; | |
| 2685 | + | |
| 2686 | + let closestAspectRatio = Object.keys(aspectRatios)[0]; | |
| 2687 | + let minDiff = Math.abs(aspectRatio - aspectRatios[closestAspectRatio]); | |
| 2688 | + | |
| 2689 | + for (const key in aspectRatios) { | |
| 2690 | + const diff = Math.abs(aspectRatio - aspectRatios[key]); | |
| 2691 | + if (diff < minDiff) { | |
| 2692 | + minDiff = diff; | |
| 2693 | + closestAspectRatio = key; | |
| 2694 | + } | |
| 2695 | + } | |
| 2696 | + | |
| 2697 | + return closestAspectRatio; | |
| 2698 | +} | |
| 2699 | + | |
| 2700 | +/** | |
| 2701 | + * Generates an image using Stability AI. | |
| 2702 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 2703 | + * @param {string} negativePrompt - The instruction used to restrict the image generation. | |
| 2704 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 2705 | + */ | |
| 2706 | +async function generateStabilityImage(prompt, negativePrompt) { | |
| 2707 | + const IMAGE_FORMAT = 'png'; | |
| 2708 | + const PROMPT_LIMIT = 10000; | |
| 2709 | + | |
| 2710 | + try { | |
| 2711 | + const response = await fetch('/api/sd/stability/generate', { | |
| 2712 | + method: 'POST', | |
| 2713 | + headers: getRequestHeaders(), | |
| 2714 | + body: JSON.stringify({ | |
| 2715 | + model: extension_settings.sd.model, | |
| 2716 | + payload: { | |
| 2717 | + prompt: prompt.slice(0, PROMPT_LIMIT), | |
| 2718 | + negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT), | |
| 2719 | + aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height), | |
| 2720 | + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | |
| 2721 | + style_preset: extension_settings.sd.stability_style_preset, | |
| 2722 | + output_format: IMAGE_FORMAT, | |
| 2723 | + }, | |
| 2724 | + }), | |
| 2725 | + }); | |
| 2726 | + | |
| 2727 | + if (!response.ok) { | |
| 2728 | + throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| 2729 | + } | |
| 2730 | + | |
| 2731 | + const base64Image = await response.text(); | |
| 2732 | + | |
| 2733 | + return { | |
| 2734 | + format: IMAGE_FORMAT, | |
| 2735 | + data: base64Image, | |
| 2736 | + }; | |
| 2737 | + } catch (error) { | |
| 2738 | + console.error('Error generating image with Stability AI:', error); | |
| 2739 | + throw error; | |
| 2740 | + } | |
| 2741 | +} | |
| 2742 | + | |
| 2743 | +/** | |
| 2604 | 2744 | * Generates a "horde" image using the provided prompt and configuration settings. |
| 2605 | 2745 | * |
| 2606 | 2746 | * @param {string} prompt - The main instruction used to guide the image generation. |
| @@ -3228,6 +3368,8 @@ function isValidState() { | ||
| 3228 | 3368 | return secret_state[SECRET_KEYS.TOGETHERAI]; |
| 3229 | 3369 | case sources.pollinations: |
| 3230 | 3370 | return true; |
| 3371 | + case sources.stability: | |
| 3372 | + return secret_state[SECRET_KEYS.STABILITY]; | |
| 3231 | 3373 | } |
| 3232 | 3374 | } |
| 3233 | 3375 | |
| @@ -3455,6 +3597,8 @@ jQuery(async () => { | ||
| 3455 | 3597 | $('#sd_command_visible').on('input', onCommandVisibleInput); |
| 3456 | 3598 | $('#sd_interactive_visible').on('input', onInteractiveVisibleInput); |
| 3457 | 3599 | $('#sd_swap_dimensions').on('click', onSwapDimensionsClick); |
| 3600 | + $('#sd_stability_key').on('click', onStabilityKeyClick); | |
| 3601 | + $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange); | |
| 3458 | 3602 | |
| 3459 | 3603 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| 3460 | 3604 | initScrollHeight($('#sd_prompt_prefix')); |
| @@ -44,6 +44,7 @@ | ||
| 44 | 44 | <option value="openai">OpenAI (DALL-E)</option> |
| 45 | 45 | <option value="pollinations">Pollinations</option> |
| 46 | 46 | <option value="vlad">SD.Next (vladmandic)</option> |
| 47 | + <option value="stability">Stability AI</option> | |
| 47 | 48 | <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> |
| 48 | 49 | <option value="horde">Stable Horde</option> |
| 49 | 50 | <option value="togetherai">TogetherAI</option> |
| @@ -189,7 +190,45 @@ | ||
| 189 | 190 | </label> |
| 190 | 191 | </div> |
| 191 | 192 | </div> |
| 193 | + <div data-sd-source="stability"> | |
| 194 | + <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | |
| 195 | + <strong class="flex1" data-i18n="API Key">API Key</strong> | |
| 196 | + <div id="sd_stability_key" class="menu_button menu_button_icon"> | |
| 197 | + <i class="fa-fw fa-solid fa-key"></i> | |
| 198 | + <span data-i18n="Click to set">Click to set</span> | |
| 199 | + </div> | |
| 200 | + </div> | |
| 201 | + <div class="marginBot5"> | |
| 202 | + <i data-i18n="You can find your API key in the Stability AI dashboard."> | |
| 203 | + You can find your API key in the Stability AI dashboard. | |
| 204 | + </i> | |
| 205 | + </div> | |
| 192 | 206 | |
| 207 | + <div class="flex-container"> | |
| 208 | + <div class="flex1"> | |
| 209 | + <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label> | |
| 210 | + <select id="sd_stability_style_preset"> | |
| 211 | + <option value="anime">Anime</option> | |
| 212 | + <option value="3d-model">3D Model</option> | |
| 213 | + <option value="analog-film">Analog Film</option> | |
| 214 | + <option value="cinematic">Cinematic</option> | |
| 215 | + <option value="comic-book">Comic Book</option> | |
| 216 | + <option value="digital-art">Digital Art</option> | |
| 217 | + <option value="enhance">Enhance</option> | |
| 218 | + <option value="fantasy-art">Fantasy Art</option> | |
| 219 | + <option value="isometric">Isometric</option> | |
| 220 | + <option value="line-art">Line Art</option> | |
| 221 | + <option value="low-poly">Low Poly</option> | |
| 222 | + <option value="modeling-compound">Modeling Compound</option> | |
| 223 | + <option value="neon-punk">Neon Punk</option> | |
| 224 | + <option value="origami">Origami</option> | |
| 225 | + <option value="photographic">Photographic</option> | |
| 226 | + <option value="pixel-art">Pixel Art</option> | |
| 227 | + <option value="tile-texture">Tile Texture</option> | |
| 228 | + </select> | |
| 229 | + </div> | |
| 230 | + </div> | |
| 231 | + </div> | |
| 193 | 232 | <div class="flex-container"> |
| 194 | 233 | <div class="flex1"> |
| 195 | 234 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -339,7 +378,7 @@ | ||
| 339 | 378 | </label> |
| 340 | 379 | </div> |
| 341 | 380 | |
| 342 | 381 | <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5"> |
| 343 | 382 | <label for="sd_seed"> |
| 344 | 383 | <span data-i18n="Seed">Seed</span> |
| 345 | 384 | <small data-i18n="(-1 for random)">(-1 for random)</small> |
| @@ -31,6 +31,7 @@ export const SECRET_KEYS = { | ||
| 31 | 31 | FEATHERLESS: 'api_key_featherless', |
| 32 | 32 | ZEROONEAI: 'api_key_01ai', |
| 33 | 33 | HUGGINGFACE: 'api_key_huggingface', |
| 34 | + STABILITY: 'api_key_stability', | |
| 34 | 35 | }; |
| 35 | 36 | |
| 36 | 37 | const INPUT_MAP = { |
| @@ -43,6 +43,7 @@ const SECRET_KEYS = { | ||
| 43 | 43 | FEATHERLESS: 'api_key_featherless', |
| 44 | 44 | ZEROONEAI: 'api_key_01ai', |
| 45 | 45 | HUGGINGFACE: 'api_key_huggingface', |
| 46 | + STABILITY: 'api_key_stability', | |
| 46 | 47 | }; |
| 47 | 48 | |
| 48 | 49 | // These are the keys that are safe to expose, even if allowKeysExposure is false |
| @@ -7,6 +7,7 @@ const path = require('path'); | ||
| 7 | 7 | const writeFileAtomicSync = require('write-file-atomic').sync; |
| 8 | 8 | const { jsonParser } = require('../express-common'); |
| 9 | 9 | const { readSecret, SECRET_KEYS } = require('./secrets.js'); |
| 10 | +const FormData = require('form-data'); | |
| 10 | 11 | |
| 11 | 12 | /** |
| 12 | 13 | * Sanitizes a string. |
| @@ -793,9 +794,69 @@ pollinations.post('/generate', jsonParser, async (request, response) => { | ||
| 793 | 794 | } |
| 794 | 795 | }); |
| 795 | 796 | |
| 797 | +const stability = express.Router(); | |
| 798 | + | |
| 799 | +stability.post('/generate', jsonParser, async (request, response) => { | |
| 800 | + try { | |
| 801 | + const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY); | |
| 802 | + | |
| 803 | + if (!key) { | |
| 804 | + console.log('Stability AI key not found.'); | |
| 805 | + return response.sendStatus(400); | |
| 806 | + } | |
| 807 | + | |
| 808 | + const { payload, model } = request.body; | |
| 809 | + | |
| 810 | + const formData = new FormData(); | |
| 811 | + for (const [key, value] of Object.entries(payload)) { | |
| 812 | + if (value !== undefined) { | |
| 813 | + formData.append(key, String(value)); | |
| 814 | + } | |
| 815 | + } | |
| 816 | + | |
| 817 | + let apiUrl; | |
| 818 | + switch (model) { | |
| 819 | + case 'stable-image-ultra': | |
| 820 | + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/ultra'; | |
| 821 | + break; | |
| 822 | + case 'stable-image-core': | |
| 823 | + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/core'; | |
| 824 | + break; | |
| 825 | + case 'stable-diffusion-3': | |
| 826 | + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/sd3'; | |
| 827 | + break; | |
| 828 | + default: | |
| 829 | + throw new Error('Invalid Stability AI model selected'); | |
| 830 | + } | |
| 831 | + | |
| 832 | + const result = await fetch(apiUrl, { | |
| 833 | + method: 'POST', | |
| 834 | + headers: { | |
| 835 | + 'Authorization': `Bearer ${key}`, | |
| 836 | + 'Accept': 'image/*', | |
| 837 | + }, | |
| 838 | + body: formData, | |
| 839 | + timeout: 0, | |
| 840 | + }); | |
| 841 | + | |
| 842 | + if (!result.ok) { | |
| 843 | + const text = await result.text(); | |
| 844 | + console.log('Stability AI returned an error.', result.status, result.statusText, text); | |
| 845 | + return response.sendStatus(500); | |
| 846 | + } | |
| 847 | + | |
| 848 | + const buffer = await result.buffer(); | |
| 849 | + return response.send(buffer.toString('base64')); | |
| 850 | + } catch (error) { | |
| 851 | + console.log(error); | |
| 852 | + return response.sendStatus(500); | |
| 853 | + } | |
| 854 | +}); | |
| 855 | + | |
| 796 | 856 | router.use('/comfy', comfy); |
| 797 | 857 | router.use('/together', together); |
| 798 | 858 | router.use('/drawthings', drawthings); |
| 799 | 859 | router.use('/pollinations', pollinations); |
| 860 | +router.use('/stability', stability); | |
| 800 | 861 | |
| 801 | 862 | module.exports = { router }; |