Move Stability generation to backend

e32b0cc223e3dc3d1895f51300aca4ff1660651e

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

5 files changed, +198 -161Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+121 -109
@@ -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';
@@ -285,11 +285,7 @@ const defaultSettings = {
285 interactive_visible: false,285 interactive_visible: false,
286286
287 // Stability AI settings287 // Stability AI settings
288 stability_api_key: '',288 stability_style_preset: 'anime',
289 stability_engine: 'V2beta Image Generation',
290 stability_style_preset: "anime",
291 stability_aspect_ratio: '1:1',
292 stability_output_format: 'png',
293};289};
294290
295const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);291const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -452,11 +448,7 @@ async function loadSettings() {
452 $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);448 $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
453 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);449 $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
454 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);450 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
455 $('#sd_stability_key').val(extension_settings.sd.stability_key);
456 $('#sd_stability_engine').val(extension_settings.sd.stability_engine);
457 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);451 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
458 $('#sd_stability_aspect_ratio').val(extension_settings.sd.stability_aspect_ratio);
459 $('#sd_stability_output_format').val(extension_settings.sd.stability_output_format);
460452
461 for (const style of extension_settings.sd.styles) {453 for (const style of extension_settings.sd.styles) {
462 const option = document.createElement('option');454 const option = document.createElement('option');
@@ -684,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) {
684 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' });
685677
686 if (refinedPrompt) {678 if (refinedPrompt) {
687 return refinedPrompt;679 return String(refinedPrompt);
688 } else {680 } else {
689 throw new Error('Generation aborted by user.');681 throw new Error('Generation aborted by user.');
690 }682 }
@@ -1097,32 +1089,26 @@ function onComfyWorkflowChange() {
1097 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();1089 extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val();
1098 saveSettingsDebounced();1090 saveSettingsDebounced();
1099}1091}
1100function onStabilityKeyInput() {
1101 extension_settings.sd.stability_key = $('#sd_stability_key').val();
1102 saveSettingsDebounced();
1103}
11041092
1105function onStabilityEngineChange() {1093async function onStabilityKeyClick() {
1106 extension_settings.sd.stability_engine = $('#sd_stability_engine').val();1094 const popupText = 'Stability AI API Key:';
1107 saveSettingsDebounced();1095 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT);
1108}
11091096
1110function onStabilityStylePresetChange() {1097 if (!key) {
1111 extension_settings.sd.stability_style_preset = $('#sd_stability_style_preset').val();1098 return;
1112 saveSettingsDebounced();1099 }
1113}
11141100
1115function onStabilityAspectRatioChange() {1101 await writeSecret(SECRET_KEYS.STABILITY, String(key));
1116 extension_settings.sd.stability_aspect_ratio = $('#sd_stability_aspect_ratio').val();1102
1117 saveSettingsDebounced();1103 toastr.success('API Key saved');
1104 await loadSettingOptions();
1118}1105}
11191106
1120function onStabilityOutputFormatChange() {1107function onStabilityStylePresetChange() {
1121 extension_settings.sd.stability_output_format = $('#sd_stability_output_format').val();1108 extension_settings.sd.stability_style_preset = $('#sd_stability_style_preset').val();
1122 saveSettingsDebounced();1109 saveSettingsDebounced();
1123}1110}
11241111
1125
1126async function changeComfyWorkflow(_, name) {1112async function changeComfyWorkflow(_, name) {
1127 name = name.replace(/(\.json)?$/i, '.json');1113 name = name.replace(/(\.json)?$/i, '.json');
1128 if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {1114 if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) {
@@ -1441,6 +1427,9 @@ async function loadSamplers() {
1441 case sources.pollinations:1427 case sources.pollinations:
1442 samplers = ['N/A'];1428 samplers = ['N/A'];
1443 break;1429 break;
1430 case sources.stability:
1431 samplers = ['N/A'];
1432 break;
1444 }1433 }
14451434
1446 for (const sampler of samplers) {1435 for (const sampler of samplers) {
@@ -1643,80 +1632,9 @@ async function loadModels() {
1643 }1632 }
1644}1633}
16451634
1646async function generateStabilityImage(prompt, negativePrompt) {
1647 const payload = {
1648 prompt: prompt,
1649 negative_prompt: negativePrompt,
1650 width: extension_settings.sd.width,
1651 height: extension_settings.sd.height,
1652 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
1653 style_preset: extension_settings.sd.stability_style_preset,
1654 output_format: extension_settings.sd.stability_output_format,
1655 };
1656
1657 const formData = new FormData();
1658 for (const [key, value] of Object.entries(payload)) {
1659 if (value !== undefined) {
1660 formData.append(key, String(value));
1661 }
1662 }
1663
1664 let apiUrl;
1665 switch (extension_settings.sd.model) {
1666 case 'stable-image-ultra':
1667 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/ultra';
1668 break;
1669 case 'stable-image-core':
1670 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/core';
1671 break;
1672 case 'stable-diffusion-3':
1673 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/sd3';
1674 break;
1675 default:
1676 throw new Error('Invalid Stability AI model selected');
1677 }
1678
1679 try {
1680 const response = await fetch(apiUrl, {
1681 method: 'POST',
1682 headers: {
1683 'Authorization': `Bearer ${extension_settings.sd.stability_key}`,
1684 'Accept': 'image/*',
1685 },
1686 body: formData,
1687 });
1688
1689 if (!response.ok) {
1690 const errorText = await response.text();
1691 throw new Error(`HTTP ${response.status}: ${errorText}`);
1692 }
1693
1694 const arrayBuffer = await response.arrayBuffer();
1695 const base64Image = arrayBufferToBase64(arrayBuffer);
1696
1697 return {
1698 format: extension_settings.sd.stability_output_format,
1699 data: base64Image,
1700 };
1701 } catch (error) {
1702 console.error('Error generating image with Stability AI:', error);
1703 throw error;
1704 }
1705}
1706
1707function arrayBufferToBase64(buffer) {
1708 let binary = '';
1709 const bytes = new Uint8Array(buffer);
1710 const len = bytes.byteLength;
1711 for (let i = 0; i < len; i++) {
1712 binary += String.fromCharCode(bytes[i]);
1713 }
1714 return btoa(binary);
1715}
1716
1717
1718
1719async function loadStabilityModels() {1635async function loadStabilityModels() {
1636 $('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]);
1637
1720 return [1638 return [
1721 { value: 'stable-image-ultra', text: 'Stable Image Ultra' },1639 { value: 'stable-image-ultra', text: 'Stable Image Ultra' },
1722 { value: 'stable-image-core', text: 'Stable Image Core' },1640 { value: 'stable-image-core', text: 'Stable Image Core' },
@@ -2055,6 +1973,9 @@ async function loadSchedulers() {
2055 case sources.comfy:1973 case sources.comfy:
2056 schedulers = await loadComfySchedulers();1974 schedulers = await loadComfySchedulers();
2057 break;1975 break;
1976 case sources.stability:
1977 schedulers = ['N/A'];
1978 break;
2058 }1979 }
20591980
2060 for (const scheduler of schedulers) {1981 for (const scheduler of schedulers) {
@@ -2128,6 +2049,9 @@ async function loadVaes() {
2128 case sources.comfy:2049 case sources.comfy:
2129 vaes = await loadComfyVaes();2050 vaes = await loadComfyVaes();
2130 break;2051 break;
2052 case sources.stability:
2053 vaes = ['N/A'];
2054 break;
2131 }2055 }
21322056
2133 for (const vae of vaes) {2057 for (const vae of vaes) {
@@ -2611,7 +2535,6 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2611 case sources.stability:2535 case sources.stability:
2612 result = await generateStabilityImage(prefixedPrompt, negativePrompt);2536 result = await generateStabilityImage(prefixedPrompt, negativePrompt);
2613 break;2537 break;
2614
2615 }2538 }
26162539
2617 if (!result.data) {2540 if (!result.data) {
@@ -2635,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2635 return base64Image;2558 return base64Image;
2636}2559}
26372560
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 */
2638async function generateTogetherAIImage(prompt, negativePrompt) {2567async function generateTogetherAIImage(prompt, negativePrompt) {
2639 const result = await fetch('/api/sd/together/generate', {2568 const result = await fetch('/api/sd/together/generate', {
2640 method: 'POST',2569 method: 'POST',
@@ -2659,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) {
2659 }2588 }
2660}2589}
26612590
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 */
2662async function generatePollinationsImage(prompt, negativePrompt) {2597async function generatePollinationsImage(prompt, negativePrompt) {
2663 const result = await fetch('/api/sd/pollinations/generate', {2598 const result = await fetch('/api/sd/pollinations/generate', {
2664 method: 'POST',2599 method: 'POST',
@@ -2728,6 +2663,86 @@ async function generateExtrasImage(prompt, negativePrompt) {
2728}2663}
27292664
2730/**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 const errorText = await response.text();
2729 throw new Error(`HTTP ${response.status}: ${errorText}`);
2730 }
2731
2732 const blob = await response.blob();
2733 const base64Image = await getBase64Async(blob);
2734
2735 return {
2736 format: IMAGE_FORMAT,
2737 data: base64Image,
2738 };
2739 } catch (error) {
2740 console.error('Error generating image with Stability AI:', error);
2741 throw error;
2742 }
2743}
2744
2745/**
2731 * Generates a "horde" image using the provided prompt and configuration settings.2746 * Generates a "horde" image using the provided prompt and configuration settings.
2732 *2747 *
2733 * @param {string} prompt - The main instruction used to guide the image generation.2748 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3356,7 +3371,7 @@ function isValidState() {
3356 case sources.pollinations:3371 case sources.pollinations:
3357 return true;3372 return true;
3358 case sources.stability:3373 case sources.stability:
3359 return !!extension_settings.sd.stability_key;3374 return secret_state[SECRET_KEYS.STABILITY];
3360 }3375 }
3361}3376}
33623377
@@ -3584,12 +3599,9 @@ jQuery(async () => {
3584 $('#sd_command_visible').on('input', onCommandVisibleInput);3599 $('#sd_command_visible').on('input', onCommandVisibleInput);
3585 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);3600 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
3586 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);3601 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
3587 $('#sd_stability_key').on('input', onStabilityKeyInput);3602 $('#sd_stability_key').on('click', onStabilityKeyClick);
3588 $('#sd_stability_engine').on('change', onStabilityEngineChange);
3589 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);3603 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
3590 $('#sd_stability_aspect_ratio').on('change', onStabilityAspectRatioChange);3604
3591 $('#sd_stability_output_format').on('change', onStabilityOutputFormatChange);
3592
3593 $('.sd_settings .inline-drawer-toggle').on('click', function () {3605 $('.sd_settings .inline-drawer-toggle').on('click', function () {
3594 initScrollHeight($('#sd_prompt_prefix'));3606 initScrollHeight($('#sd_prompt_prefix'));
3595 initScrollHeight($('#sd_negative_prompt'));3607 initScrollHeight($('#sd_negative_prompt'));
public/scripts/extensions/stable-diffusion/settings.html+15 -52
@@ -44,10 +44,10 @@
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>
50 <option value="stability">Stability AI</option>
51 </select>51 </select>
52 <div data-sd-source="auto">52 <div data-sd-source="auto">
53 <label for="sd_auto_url">SD Web UI URL</label>53 <label for="sd_auto_url">SD Web UI URL</label>
@@ -191,27 +191,22 @@
191 </div>191 </div>
192 </div>192 </div>
193 <div data-sd-source="stability">193 <div data-sd-source="stability">
194 <label for="sd_stability_key">API Key</label>194 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
195 <div class="flex-container flexnowrap">195 <strong class="flex1" data-i18n="API Key">API Key</strong>
196 <input id="sd_stability_key" type="password" class="text_pole flex1" placeholder="Enter your Stability AI API key" />196 <div id="sd_stability_key" class="menu_button menu_button_icon">
197 <div id="sd_stability_validate" class="menu_button menu_button_icon">197 <i class="fa-fw fa-solid fa-key"></i>
198 <i class="fa-solid fa-check"></i>198 <span data-i18n="Click to set">Click to set</span>
199 <span data-i18n="Connect">
200 Connect
201 </span>
202 </div>199 </div>
203 </div>200 </div>
204 <i>You can find your API key in the Stability AI dashboard.</i>201 <div class="marginBot5">
205 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>
206
206 <div class="flex-container">207 <div class="flex-container">
207 <div class="flex1">208 <div class="flex1">
208 <label for="sd_stability_engine">Engine</label>209 <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label>
209 <select id="sd_stability_engine">
210 <option value="v2beta">V2beta Image Generation</option>
211 </select>
212 </div>
213 <div class="flex1">
214 <label for="sd_stability_style_preset">Style Preset</label>
215 <select id="sd_stability_style_preset">210 <select id="sd_stability_style_preset">
216 <option value="anime">Anime</option>211 <option value="anime">Anime</option>
217 <option value="3d-model">3D Model</option>212 <option value="3d-model">3D Model</option>
@@ -233,39 +228,7 @@
233 </select>228 </select>
234 </div>229 </div>
235 </div>230 </div>
236 231 </div>
237 <div class="flex-container">
238 <div class="flex1">
239 <label for="sd_stability_aspect_ratio">Aspect Ratio</label>
240 <select id="sd_stability_aspect_ratio">
241 <option value="16:9">16:9</option>
242 <option value="1:1">1:1</option>
243 <option value="21:9">21:9</option>
244 <option value="2:3">2:3</option>
245 <option value="3:2">3:2</option>
246 <option value="4:5">4:5</option>
247 <option value="5:4">5:4</option>
248 <option value="9:16">9:16</option>
249 <option value="9:21">9:21</option>
250 </select>
251 </div>
252 <div class="flex1">
253 <label for="sd_stability_seed">Seed</label>
254 <input id="sd_stability_seed" type="number" class="text_pole" value="0" min="0" max="4294967295" />
255 </div>
256 </div>
257
258 <div class="flex-container">
259 <div class="flex1">
260 <label for="sd_stability_output_format">Output Format</label>
261 <select id="sd_stability_output_format">
262 <option value="png">PNG</option>
263 <option value="webp">WebP</option>
264 <option value="jpeg">JPEG</option>
265 </select>
266 </div>
267 </div>
268 </div>
269 <div class="flex-container">232 <div class="flex-container">
270 <div class="flex1">233 <div class="flex1">
271 <label for="sd_model" data-i18n="Model">Model</label>234 <label for="sd_model" data-i18n="Model">Model</label>
@@ -415,7 +378,7 @@
415 </label>378 </label>
416 </div>379 </div>
417380
418 <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">
419 <label for="sd_seed">382 <label for="sd_seed">
420 <span data-i18n="Seed">Seed</span>383 <span data-i18n="Seed">Seed</span>
421 <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+60 -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,68 @@ 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 });
840
841 if (!result.ok) {
842 const text = await result.text();
843 console.log('Stability AI returned an error.', result.status, result.statusText, text);
844 return response.sendStatus(500);
845 }
846
847 const buffer = await result.buffer();
848 return response.send(buffer);
849 } catch (error) {
850 console.log(error);
851 return response.sendStatus(500);
852 }
853});
854
796router.use('/comfy', comfy);855router.use('/comfy', comfy);
797router.use('/together', together);856router.use('/together', together);
798router.use('/drawthings', drawthings);857router.use('/drawthings', drawthings);
799router.use('/pollinations', pollinations);858router.use('/pollinations', pollinations);
859router.use('/stability', stability);
800860
801module.exports = { router };861module.exports = { router };