Merge pull request #2471 from WBlair1/staging Stable image from StabilityAI api

ce71c0ef86837f7223289d36f983caee59a8f64d

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +250 -4Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+147 -3
@@ -22,7 +22,7 @@ import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, rend
22import { selected_group } from '../../group-chats.js';22import { selected_group } from '../../group-chats.js';
23import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';23import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
24import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';24import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
25import { SECRET_KEYS, secret_state } from '../../secrets.js';25import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
26import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';26import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
27import { getMultimodalCaption } from '../shared.js';27import { getMultimodalCaption } from '../shared.js';
28import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';28import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -49,6 +49,7 @@ const sources = {
49 togetherai: 'togetherai',49 togetherai: 'togetherai',
50 drawthings: 'drawthings',50 drawthings: 'drawthings',
51 pollinations: 'pollinations',51 pollinations: 'pollinations',
52 stability: 'stability',
52};53};
5354
54const initiators = {55const initiators = {
@@ -282,6 +283,9 @@ const defaultSettings = {
282 wand_visible: false,283 wand_visible: false,
283 command_visible: false,284 command_visible: false,
284 interactive_visible: false,285 interactive_visible: false,
286
287 // Stability AI settings
288 stability_style_preset: 'anime',
285};289};
286290
287const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);291const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -444,6 +448,7 @@ async function loadSettings() {
444 $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);448 $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
445 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);449 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
446 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);450 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
451 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
447452
448 for (const style of extension_settings.sd.styles) {453 for (const style of extension_settings.sd.styles) {
449 const option = document.createElement('option');454 const option = document.createElement('option');
@@ -671,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) {
671 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });676 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
672677
673 if (refinedPrompt) {678 if (refinedPrompt) {
674 return refinedPrompt;679 return String(refinedPrompt);
675 } else {680 } else {
676 throw new Error('Generation aborted by user.');681 throw new Error('Generation aborted by user.');
677 }682 }
@@ -1084,6 +1089,26 @@ function onComfyWorkflowChange() {
1084 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();1089 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
1085 saveSettingsDebounced();1090 saveSettingsDebounced();
1086}1091}
1092
1093async 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
1107function onStabilityStylePresetChange() {
1108 extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val());
1109 saveSettingsDebounced();
1110}
1111
1087async function changeComfyWorkflow(_, name) {1112async function changeComfyWorkflow(_, name) {
1088 name = name.replace(/(\.json)?$/i, '.json');1113 name = name.replace(/(\.json)?$/i, '.json');
1089 if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {1114 if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {
@@ -1193,7 +1218,7 @@ async function onModelChange() {
1193 extension_settings.sd.model = $('#sd_model').find(':selected').val();1218 extension_settings.sd.model = $('#sd_model').find(':selected').val();
1194 saveSettingsDebounced();1219 saveSettingsDebounced();
11951220
1196 const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations];1221 const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability];
11971222
1198 if (cloudSources.includes(extension_settings.sd.source)) {1223 if (cloudSources.includes(extension_settings.sd.source)) {
1199 return;1224 return;
@@ -1402,6 +1427,9 @@ async function loadSamplers() {
1402 case sources.pollinations:1427 case sources.pollinations:
1403 samplers = ['N/A'];1428 samplers = ['N/A'];
1404 break;1429 break;
1430 case sources.stability:
1431 samplers = ['N/A'];
1432 break;
1405 }1433 }
14061434
1407 for (const sampler of samplers) {1435 for (const sampler of samplers) {
@@ -1585,6 +1613,9 @@ async function loadModels() {
1585 case sources.pollinations:1613 case sources.pollinations:
1586 models = await loadPollinationsModels();1614 models = await loadPollinationsModels();
1587 break;1615 break;
1616 case sources.stability:
1617 models = await loadStabilityModels();
1618 break;
1588 }1619 }
15891620
1590 for (const model of models) {1621 for (const model of models) {
@@ -1601,6 +1632,16 @@ async function loadModels() {
1601 }1632 }
1602}1633}
16031634
1635async 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
1604async function loadPollinationsModels() {1645async function loadPollinationsModels() {
1605 return [1646 return [
1606 {1647 {
@@ -1932,6 +1973,9 @@ async function loadSchedulers() {
1932 case sources.comfy:1973 case sources.comfy:
1933 schedulers = await loadComfySchedulers();1974 schedulers = await loadComfySchedulers();
1934 break;1975 break;
1976 case sources.stability:
1977 schedulers = ['N/A'];
1978 break;
1935 }1979 }
19361980
1937 for (const scheduler of schedulers) {1981 for (const scheduler of schedulers) {
@@ -2005,6 +2049,9 @@ async function loadVaes() {
2005 case sources.comfy:2049 case sources.comfy:
2006 vaes = await loadComfyVaes();2050 vaes = await loadComfyVaes();
2007 break;2051 break;
2052 case sources.stability:
2053 vaes = ['N/A'];
2054 break;
2008 }2055 }
20092056
2010 for (const vae of vaes) {2057 for (const vae of vaes) {
@@ -2485,6 +2532,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2485 case sources.pollinations:2532 case sources.pollinations:
2486 result = await generatePollinationsImage(prefixedPrompt, negativePrompt);2533 result = await generatePollinationsImage(prefixedPrompt, negativePrompt);
2487 break;2534 break;
2535 case sources.stability:
2536 result = await generateStabilityImage(prefixedPrompt, negativePrompt);
2537 break;
2488 }2538 }
24892539
2490 if (!result.data) {2540 if (!result.data) {
@@ -2508,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2508 return base64Image;2558 return base64Image;
2509}2559}
25102560
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 */
2511async function generateTogetherAIImage(prompt, negativePrompt) {2567async function generateTogetherAIImage(prompt, negativePrompt) {
2512 const result = await fetch('/api/sd/together/generate', {2568 const result = await fetch('/api/sd/together/generate', {
2513 method: 'POST',2569 method: 'POST',
@@ -2532,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) {
2532 }2588 }
2533}2589}
25342590
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 */
2535async function generatePollinationsImage(prompt, negativePrompt) {2597async function generatePollinationsImage(prompt, negativePrompt) {
2536 const result = await fetch('/api/sd/pollinations/generate', {2598 const result = await fetch('/api/sd/pollinations/generate', {
2537 method: 'POST',2599 method: 'POST',
@@ -2601,6 +2663,84 @@ async function generateExtrasImage(prompt, negativePrompt) {
2601}2663}
26022664
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 */
2671function 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 */
2706async 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 * Generates a "horde" image using the provided prompt and configuration settings.2744 * Generates a "horde" image using the provided prompt and configuration settings.
2605 *2745 *
2606 * @param {string} prompt - The main instruction used to guide the image generation.2746 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3228,6 +3368,8 @@ function isValidState() {
3228 return secret_state[SECRET_KEYS.TOGETHERAI];3368 return secret_state[SECRET_KEYS.TOGETHERAI];
3229 case sources.pollinations:3369 case sources.pollinations:
3230 return true;3370 return true;
3371 case sources.stability:
3372 return secret_state[SECRET_KEYS.STABILITY];
3231 }3373 }
3232}3374}
32333375
@@ -3455,6 +3597,8 @@ jQuery(async () => {
3455 $('#sd_command_visible').on('input', onCommandVisibleInput);3597 $('#sd_command_visible').on('input', onCommandVisibleInput);
3456 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);3598 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
3457 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);3599 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
3600 $('#sd_stability_key').on('click', onStabilityKeyClick);
3601 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
34583602
3459 $('.sd_settings .inline-drawer-toggle').on('click', function () {3603 $('.sd_settings .inline-drawer-toggle').on('click', function () {
3460 initScrollHeight($('#sd_prompt_prefix'));3604 initScrollHeight($('#sd_prompt_prefix'));
public/scripts/extensions/stable-diffusion/settings.html+40 -1
@@ -44,6 +44,7 @@
44 <option value="openai">OpenAI (DALL-E)</option>44 <option value="openai">OpenAI (DALL-E)</option>
45 <option value="pollinations">Pollinations</option>45 <option value="pollinations">Pollinations</option>
46 <option value="vlad">SD.Next (vladmandic)</option>46 <option value="vlad">SD.Next (vladmandic)</option>
47 <option value="stability">Stability AI</option>
47 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>48 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
48 <option value="horde">Stable Horde</option>49 <option value="horde">Stable Horde</option>
49 <option value="togetherai">TogetherAI</option>50 <option value="togetherai">TogetherAI</option>
@@ -189,9 +190,47 @@
189 </label>190 </label>
190 </div>191 </div>
191 </div>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>
192206
193 <div class="flex-container">207 <div class="flex-container">
194 <div class="flex1">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>
232 <div class="flex-container">
233 <div class="flex1">
195 <label for="sd_model" data-i18n="Model">Model</label>234 <label for="sd_model" data-i18n="Model">Model</label>
196 <select id="sd_model"></select>235 <select id="sd_model"></select>
197 </div>236 </div>
@@ -339,7 +378,7 @@
339 </label>378 </label>
340 </div>379 </div>
341380
342 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5">381 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5">
343 <label for="sd_seed">382 <label for="sd_seed">
344 <span data-i18n="Seed">Seed</span>383 <span data-i18n="Seed">Seed</span>
345 <small data-i18n="(-1 for random)">(-1 for random)</small>384 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/secrets.js+1 -0
@@ -31,6 +31,7 @@ export const SECRET_KEYS = {
31 FEATHERLESS: 'api_key_featherless',31 FEATHERLESS: 'api_key_featherless',
32 ZEROONEAI: 'api_key_01ai',32 ZEROONEAI: 'api_key_01ai',
33 HUGGINGFACE: 'api_key_huggingface',33 HUGGINGFACE: 'api_key_huggingface',
34 STABILITY: 'api_key_stability',
34};35};
3536
36const INPUT_MAP = {37const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -43,6 +43,7 @@ const SECRET_KEYS = {
43 FEATHERLESS: 'api_key_featherless',43 FEATHERLESS: 'api_key_featherless',
44 ZEROONEAI: 'api_key_01ai',44 ZEROONEAI: 'api_key_01ai',
45 HUGGINGFACE: 'api_key_huggingface',45 HUGGINGFACE: 'api_key_huggingface',
46 STABILITY: 'api_key_stability',
46};47};
4748
48// These are the keys that are safe to expose, even if allowKeysExposure is false49// These are the keys that are safe to expose, even if allowKeysExposure is false
src/endpoints/stable-diffusion.js+61 -0
@@ -7,6 +7,7 @@ const path = require('path');
7const writeFileAtomicSync = require('write-file-atomic').sync;7const writeFileAtomicSync = require('write-file-atomic').sync;
8const { jsonParser } = require('../express-common');8const { jsonParser } = require('../express-common');
9const { readSecret, SECRET_KEYS } = require('./secrets.js');9const { readSecret, SECRET_KEYS } = require('./secrets.js');
10const FormData = require('form-data');
1011
11/**12/**
12 * Sanitizes a string.13 * Sanitizes a string.
@@ -793,9 +794,69 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
793 }794 }
794});795});
795796
797const stability = express.Router();
798
799stability.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
796router.use('/comfy', comfy);856router.use('/comfy', comfy);
797router.use('/together', together);857router.use('/together', together);
798router.use('/drawthings', drawthings);858router.use('/drawthings', drawthings);
799router.use('/pollinations', pollinations);859router.use('/pollinations', pollinations);
860router.use('/stability', stability);
800861
801module.exports = { router };862module.exports = { router };