Move Stability generation to backend
| @@ -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'; |
| @@ -285,11 +285,7 @@ const defaultSettings = { | ||
| 285 | 285 | interactive_visible: false, |
| 286 | 286 | |
| 287 | 287 | // Stability AI settings |
| 288 | 288 | stability_api_keystability_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 | }; |
| 294 | 290 | |
| 295 | 291 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| @@ -452,11 +448,7 @@ async function loadSettings() { | ||
| 452 | 448 | $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible); |
| 453 | 449 | $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible); |
| 454 | 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 | 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); | |
| 460 | 452 | |
| 461 | 453 | for (const style of extension_settings.sd.styles) { |
| 462 | 454 | const option = document.createElement('option'); |
| @@ -684,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) { | ||
| 684 | 676 | const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' }); |
| 685 | 677 | |
| 686 | 678 | if (refinedPrompt) { |
| 687 | 679 | return String(refinedPrompt); |
| 688 | 680 | } else { |
| 689 | 681 | throw new Error('Generation aborted by user.'); |
| 690 | 682 | } |
| @@ -1097,32 +1089,26 @@ function onComfyWorkflowChange() { | ||
| 1097 | 1089 | extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val(); |
| 1098 | 1090 | saveSettingsDebounced(); |
| 1099 | 1091 | } |
| 1100 | -function onStabilityKeyInput() { | |
| 1101 | - extension_settings.sd.stability_key = $('#sd_stability_key').val(); | |
| 1102 | - saveSettingsDebounced(); | |
| 1103 | -} | |
| 1104 | 1092 | |
| 1105 | 1093 | async function onStabilityEngineChangeonStabilityKeyClick() { |
| 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 | -} | |
| 1109 | 1096 | |
| 1110 | 1097 | function if onStabilityStylePresetChange(!key) { |
| 1111 | - extension_settings.sd.stability_style_preset = $('#sd_stability_style_preset').val(); | |
| 1098 | + return; | |
| 1112 | - saveSettingsDebounced(); | |
| 1099 | + } | |
| 1113 | -} | |
| 1114 | 1100 | |
| 1115 | -function 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 | } |
| 1119 | 1106 | |
| 1120 | 1107 | function onStabilityOutputFormatChangeonStabilityStylePresetChange() { |
| 1121 | 1108 | extension_settings.sd.stability_output_formatstability_style_preset = $('#sd_stability_output_formatsd_stability_style_preset').val(); |
| 1122 | 1109 | saveSettingsDebounced(); |
| 1123 | 1110 | } |
| 1124 | 1111 | |
| 1125 | - | |
| 1126 | 1112 | async function changeComfyWorkflow(_, name) { |
| 1127 | 1113 | name = name.replace(/(\.json)?$/i, '.json'); |
| 1128 | 1114 | if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) { |
| @@ -1441,6 +1427,9 @@ async function loadSamplers() { | ||
| 1441 | 1427 | case sources.pollinations: |
| 1442 | 1428 | samplers = ['N/A']; |
| 1443 | 1429 | break; |
| 1430 | + case sources.stability: | |
| 1431 | + samplers = ['N/A']; | |
| 1432 | + break; | |
| 1444 | 1433 | } |
| 1445 | 1434 | |
| 1446 | 1435 | for (const sampler of samplers) { |
| @@ -1643,80 +1632,9 @@ async function loadModels() { | ||
| 1643 | 1632 | } |
| 1644 | 1633 | } |
| 1645 | 1634 | |
| 1646 | -async 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 | - | |
| 1707 | -function 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 | - | |
| 1719 | 1635 | async function loadStabilityModels() { |
| 1636 | + $('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]); | |
| 1637 | + | |
| 1720 | 1638 | return [ |
| 1721 | 1639 | { value: 'stable-image-ultra', text: 'Stable Image Ultra' }, |
| 1722 | 1640 | { value: 'stable-image-core', text: 'Stable Image Core' }, |
| @@ -2055,6 +1973,9 @@ async function loadSchedulers() { | ||
| 2055 | 1973 | case sources.comfy: |
| 2056 | 1974 | schedulers = await loadComfySchedulers(); |
| 2057 | 1975 | break; |
| 1976 | + case sources.stability: | |
| 1977 | + schedulers = ['N/A']; | |
| 1978 | + break; | |
| 2058 | 1979 | } |
| 2059 | 1980 | |
| 2060 | 1981 | for (const scheduler of schedulers) { |
| @@ -2128,6 +2049,9 @@ async function loadVaes() { | ||
| 2128 | 2049 | case sources.comfy: |
| 2129 | 2050 | vaes = await loadComfyVaes(); |
| 2130 | 2051 | break; |
| 2052 | + case sources.stability: | |
| 2053 | + vaes = ['N/A']; | |
| 2054 | + break; | |
| 2131 | 2055 | } |
| 2132 | 2056 | |
| 2133 | 2057 | for (const vae of vaes) { |
| @@ -2611,7 +2535,6 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2611 | 2535 | case sources.stability: |
| 2612 | 2536 | result = await generateStabilityImage(prefixedPrompt, negativePrompt); |
| 2613 | 2537 | break; |
| 2614 | - | |
| 2615 | 2538 | } |
| 2616 | 2539 | |
| 2617 | 2540 | if (!result.data) { |
| @@ -2635,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2635 | 2558 | return base64Image; |
| 2636 | 2559 | } |
| 2637 | 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 | + */ | |
| 2638 | 2567 | async function generateTogetherAIImage(prompt, negativePrompt) { |
| 2639 | 2568 | const result = await fetch('/api/sd/together/generate', { |
| 2640 | 2569 | method: 'POST', |
| @@ -2659,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) { | ||
| 2659 | 2588 | } |
| 2660 | 2589 | } |
| 2661 | 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 | + */ | |
| 2662 | 2597 | async function generatePollinationsImage(prompt, negativePrompt) { |
| 2663 | 2598 | const result = await fetch('/api/sd/pollinations/generate', { |
| 2664 | 2599 | method: 'POST', |
| @@ -2728,6 +2663,86 @@ async function generateExtrasImage(prompt, negativePrompt) { | ||
| 2728 | 2663 | } |
| 2729 | 2664 | |
| 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 | + */ | |
| 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 | + 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 | 2746 | * Generates a "horde" image using the provided prompt and configuration settings. |
| 2732 | 2747 | * |
| 2733 | 2748 | * @param {string} prompt - The main instruction used to guide the image generation. |
| @@ -3356,7 +3371,7 @@ function isValidState() { | ||
| 3356 | 3371 | case sources.pollinations: |
| 3357 | 3372 | return true; |
| 3358 | 3373 | case sources.stability: |
| 3359 | - return !!extension_settings.sd.stability_key; | |
| 3374 | + return secret_state[SECRET_KEYS.STABILITY]; | |
| 3360 | 3375 | } |
| 3361 | 3376 | } |
| 3362 | 3377 | |
| @@ -3584,12 +3599,9 @@ jQuery(async () => { | ||
| 3584 | 3599 | $('#sd_command_visible').on('input', onCommandVisibleInput); |
| 3585 | 3600 | $('#sd_interactive_visible').on('input', onInteractiveVisibleInput); |
| 3586 | 3601 | $('#sd_swap_dimensions').on('click', onSwapDimensionsClick); |
| 3587 | 3602 | $('#sd_stability_key').on('inputclick', onStabilityKeyInputonStabilityKeyClick); |
| 3588 | - $('#sd_stability_engine').on('change', onStabilityEngineChange); | |
| 3589 | 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 | 3605 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| 3594 | 3606 | initScrollHeight($('#sd_prompt_prefix')); |
| 3595 | 3607 | initScrollHeight($('#sd_negative_prompt')); |
| @@ -44,10 +44,10 @@ | ||
| 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> |
| 50 | - <option value="stability">Stability AI</option> | |
| 51 | 51 | </select> |
| 52 | 52 | <div data-sd-source="auto"> |
| 53 | 53 | <label for="sd_auto_url">SD Web UI URL</label> |
| @@ -191,27 +191,22 @@ | ||
| 191 | 191 | </div> |
| 192 | 192 | </div> |
| 193 | 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 | 196 | <inputdiv id="sd_stability_key" type="password" class="text_pole flex1" placeholder="Enter your Stability AI APImenu_button keymenu_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 | 199 | </div> |
| 203 | 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 | 207 | <div class="flex-container"> |
| 207 | 208 | <div class="flex1"> |
| 208 | 209 | <label for="sd_stability_enginesd_stability_style_preset" data-i18n="Style Preset">EngineStyle 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 | 210 | <select id="sd_stability_style_preset"> |
| 216 | 211 | <option value="anime">Anime</option> |
| 217 | 212 | <option value="3d-model">3D Model</option> |
| @@ -233,39 +228,7 @@ | ||
| 233 | 228 | </select> |
| 234 | 229 | </div> |
| 235 | 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 | 232 | <div class="flex-container"> |
| 270 | 233 | <div class="flex1"> |
| 271 | 234 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -415,7 +378,7 @@ | ||
| 415 | 378 | </label> |
| 416 | 379 | </div> |
| 417 | 380 | |
| 418 | 381 | <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability" class="marginTop5"> |
| 419 | 382 | <label for="sd_seed"> |
| 420 | 383 | <span data-i18n="Seed">Seed</span> |
| 421 | 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,68 @@ 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 | + }); | |
| 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 | + | |
| 796 | 855 | router.use('/comfy', comfy); |
| 797 | 856 | router.use('/together', together); |
| 798 | 857 | router.use('/drawthings', drawthings); |
| 799 | 858 | router.use('/pollinations', pollinations); |
| 859 | +router.use('/stability', stability); | |
| 800 | 860 | |
| 801 | 861 | module.exports = { router }; |