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 -4Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+147 -3
@@ -22,7 +22,7 @@ import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, rend
2222import { selected_group } from '../../group-chats.js';
2323import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
2424import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
2525import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
2626import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
2727import { getMultimodalCaption } from '../shared.js';
2828import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -49,6 +49,7 @@ const sources = {
4949 togetherai: 'togetherai',
5050 drawthings: 'drawthings',
5151 pollinations: 'pollinations',
52+ stability: 'stability',
5253};
5354
5455const initiators = {
@@ -282,6 +283,9 @@ const defaultSettings = {
282283 wand_visible: false,
283284 command_visible: false,
284285 interactive_visible: false,
286+
287+ // Stability AI settings
288+ stability_style_preset: 'anime',
285289};
286290
287291const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -444,6 +448,7 @@ async function loadSettings() {
444448 $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
445449 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
446450 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
451+ $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
447452
448453 for (const style of extension_settings.sd.styles) {
449454 const option = document.createElement('option');
@@ -671,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) {
671676 const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
672677
673678 if (refinedPrompt) {
674679 return String(refinedPrompt);
675680 } else {
676681 throw new Error('Generation aborted by user.');
677682 }
@@ -1084,6 +1089,26 @@ function onComfyWorkflowChange() {
10841089 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
10851090 saveSettingsDebounced();
10861091}
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+
10871112async function changeComfyWorkflow(_, name) {
10881113 name = name.replace(/(\.json)?$/i, '.json');
10891114 if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {
@@ -1193,7 +1218,7 @@ async function onModelChange() {
11931218 extension_settings.sd.model = $('#sd_model').find(':selected').val();
11941219 saveSettingsDebounced();
11951220
11961221 const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability];
11971222
11981223 if (cloudSources.includes(extension_settings.sd.source)) {
11991224 return;
@@ -1402,6 +1427,9 @@ async function loadSamplers() {
14021427 case sources.pollinations:
14031428 samplers = ['N/A'];
14041429 break;
1430+ case sources.stability:
1431+ samplers = ['N/A'];
1432+ break;
14051433 }
14061434
14071435 for (const sampler of samplers) {
@@ -1585,6 +1613,9 @@ async function loadModels() {
15851613 case sources.pollinations:
15861614 models = await loadPollinationsModels();
15871615 break;
1616+ case sources.stability:
1617+ models = await loadStabilityModels();
1618+ break;
15881619 }
15891620
15901621 for (const model of models) {
@@ -1601,6 +1632,16 @@ async function loadModels() {
16011632 }
16021633}
16031634
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+
16041645async function loadPollinationsModels() {
16051646 return [
16061647 {
@@ -1932,6 +1973,9 @@ async function loadSchedulers() {
19321973 case sources.comfy:
19331974 schedulers = await loadComfySchedulers();
19341975 break;
1976+ case sources.stability:
1977+ schedulers = ['N/A'];
1978+ break;
19351979 }
19361980
19371981 for (const scheduler of schedulers) {
@@ -2005,6 +2049,9 @@ async function loadVaes() {
20052049 case sources.comfy:
20062050 vaes = await loadComfyVaes();
20072051 break;
2052+ case sources.stability:
2053+ vaes = ['N/A'];
2054+ break;
20082055 }
20092056
20102057 for (const vae of vaes) {
@@ -2485,6 +2532,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
24852532 case sources.pollinations:
24862533 result = await generatePollinationsImage(prefixedPrompt, negativePrompt);
24872534 break;
2535+ case sources.stability:
2536+ result = await generateStabilityImage(prefixedPrompt, negativePrompt);
2537+ break;
24882538 }
24892539
24902540 if (!result.data) {
@@ -2508,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
25082558 return base64Image;
25092559}
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+ */
25112567async function generateTogetherAIImage(prompt, negativePrompt) {
25122568 const result = await fetch('/api/sd/together/generate', {
25132569 method: 'POST',
@@ -2532,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) {
25322588 }
25332589}
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+ */
25352597async function generatePollinationsImage(prompt, negativePrompt) {
25362598 const result = await fetch('/api/sd/pollinations/generate', {
25372599 method: 'POST',
@@ -2601,6 +2663,84 @@ async function generateExtrasImage(prompt, negativePrompt) {
26012663}
26022664
26032665/**
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+/**
26042744 * Generates a "horde" image using the provided prompt and configuration settings.
26052745 *
26062746 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3228,6 +3368,8 @@ function isValidState() {
32283368 return secret_state[SECRET_KEYS.TOGETHERAI];
32293369 case sources.pollinations:
32303370 return true;
3371+ case sources.stability:
3372+ return secret_state[SECRET_KEYS.STABILITY];
32313373 }
32323374}
32333375
@@ -3455,6 +3597,8 @@ jQuery(async () => {
34553597 $('#sd_command_visible').on('input', onCommandVisibleInput);
34563598 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
34573599 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
3600+ $('#sd_stability_key').on('click', onStabilityKeyClick);
3601+ $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
34583602
34593603 $('.sd_settings .inline-drawer-toggle').on('click', function () {
34603604 initScrollHeight($('#sd_prompt_prefix'));
public/scripts/extensions/stable-diffusion/settings.html+40 -1
@@ -44,6 +44,7 @@
4444 <option value="openai">OpenAI (DALL-E)</option>
4545 <option value="pollinations">Pollinations</option>
4646 <option value="vlad">SD.Next (vladmandic)</option>
47+ <option value="stability">Stability AI</option>
4748 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
4849 <option value="horde">Stable Horde</option>
4950 <option value="togetherai">TogetherAI</option>
@@ -189,7 +190,45 @@
189190 </label>
190191 </div>
191192 </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
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>
193232 <div class="flex-container">
194233 <div class="flex1">
195234 <label for="sd_model" data-i18n="Model">Model</label>
@@ -339,7 +378,7 @@
339378 </label>
340379 </div>
341380
342381 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5">
343382 <label for="sd_seed">
344383 <span data-i18n="Seed">Seed</span>
345384 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/secrets.js+1 -0
@@ -31,6 +31,7 @@ export const SECRET_KEYS = {
3131 FEATHERLESS: 'api_key_featherless',
3232 ZEROONEAI: 'api_key_01ai',
3333 HUGGINGFACE: 'api_key_huggingface',
34+ STABILITY: 'api_key_stability',
3435};
3536
3637const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -43,6 +43,7 @@ const SECRET_KEYS = {
4343 FEATHERLESS: 'api_key_featherless',
4444 ZEROONEAI: 'api_key_01ai',
4545 HUGGINGFACE: 'api_key_huggingface',
46+ STABILITY: 'api_key_stability',
4647};
4748
4849// 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');
77const writeFileAtomicSync = require('write-file-atomic').sync;
88const { jsonParser } = require('../express-common');
99const { readSecret, SECRET_KEYS } = require('./secrets.js');
10+const FormData = require('form-data');
1011
1112/**
1213 * Sanitizes a string.
@@ -793,9 +794,69 @@ pollinations.post('/generate', jsonParser, async (request, response) => {
793794 }
794795});
795796
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+
796856router.use('/comfy', comfy);
797857router.use('/together', together);
798858router.use('/drawthings', drawthings);
799859router.use('/pollinations', pollinations);
860+router.use('/stability', stability);
800861
801862module.exports = { router };