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 | import { selected_group } from '../../group-chats.js'; | 22 | import { selected_group } from '../../group-chats.js'; |
| 23 | import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js'; | 23 | import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js'; |
| 24 | import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; | 24 | import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; |
| 25 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; | 25 | import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; |
| 26 | import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js'; | 26 | import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js'; |
| 27 | import { getMultimodalCaption } from '../shared.js'; | 27 | import { getMultimodalCaption } from '../shared.js'; |
| 28 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 28 | import { 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 | }; |
| 53 | 54 | ||
| 54 | const initiators = { | 55 | const 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 | }; |
| 286 | 290 | ||
| 287 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); | 291 | const 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); | ||
| 447 | 452 | ||
| 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' }); |
| 672 | 677 | ||
| 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 | |||
| 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 | async function changeComfyWorkflow(_, name) { | 1112 | async 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(); |
| 1195 | 1220 | ||
| 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]; |
| 1197 | 1222 | ||
| 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 | } |
| 1406 | 1434 | ||
| 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 | } |
| 1589 | 1620 | ||
| 1590 | for (const model of models) { | 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 | async function loadPollinationsModels() { | 1645 | async 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 | } |
| 1936 | 1980 | ||
| 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 | } |
| 2009 | 2056 | ||
| 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 | } |
| 2489 | 2539 | ||
| 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 | } |
| 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 | async function generateTogetherAIImage(prompt, negativePrompt) { | 2567 | async 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 | } |
| 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 | async function generatePollinationsImage(prompt, negativePrompt) { | 2597 | async 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 | } |
| 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 | * 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 | } |
| 3233 | 3375 | ||
| @@ -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); | ||
| 3458 | 3602 | ||
| 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')); |
| @@ -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,7 +190,45 @@ | |||
| 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> | ||
| 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 | <div class="flex-container"> | 232 | <div class="flex-container"> |
| 194 | <div class="flex1"> | 233 | <div class="flex1"> |
| 195 | <label for="sd_model" data-i18n="Model">Model</label> | 234 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -339,7 +378,7 @@ | |||
| 339 | </label> | 378 | </label> |
| 340 | </div> | 379 | </div> |
| 341 | 380 | ||
| 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> |
| @@ -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 | }; |
| 35 | 36 | ||
| 36 | const INPUT_MAP = { | 37 | const INPUT_MAP = { |
| @@ -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 | }; |
| 47 | 48 | ||
| 48 | // These are the keys that are safe to expose, even if allowKeysExposure is false | 49 | // These are the keys that are safe to expose, even if allowKeysExposure is false |
| @@ -7,6 +7,7 @@ const path = require('path'); | |||
| 7 | const writeFileAtomicSync = require('write-file-atomic').sync; | 7 | const writeFileAtomicSync = require('write-file-atomic').sync; |
| 8 | const { jsonParser } = require('../express-common'); | 8 | const { jsonParser } = require('../express-common'); |
| 9 | const { readSecret, SECRET_KEYS } = require('./secrets.js'); | 9 | const { readSecret, SECRET_KEYS } = require('./secrets.js'); |
| 10 | const FormData = require('form-data'); | ||
| 10 | 11 | ||
| 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 | }); |
| 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 | router.use('/comfy', comfy); | 856 | router.use('/comfy', comfy); |
| 797 | router.use('/together', together); | 857 | router.use('/together', together); |
| 798 | router.use('/drawthings', drawthings); | 858 | router.use('/drawthings', drawthings); |
| 799 | router.use('/pollinations', pollinations); | 859 | router.use('/pollinations', pollinations); |
| 860 | router.use('/stability', stability); | ||
| 800 | 861 | ||
| 801 | module.exports = { router }; | 862 | module.exports = { router }; |