Move Stability generation to backend
| @@ -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'; |
| @@ -285,11 +285,7 @@ const defaultSettings = { | |||
| 285 | interactive_visible: false, | 285 | interactive_visible: false, |
| 286 | 286 | ||
| 287 | // Stability AI settings | 287 | // 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 | }; |
| 294 | 290 | ||
| 295 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); | 291 | const 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); | ||
| 460 | 452 | ||
| 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' }); |
| 685 | 677 | ||
| 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 | } |
| 1100 | function onStabilityKeyInput() { | ||
| 1101 | extension_settings.sd.stability_key = $('#sd_stability_key').val(); | ||
| 1102 | saveSettingsDebounced(); | ||
| 1103 | } | ||
| 1104 | 1092 | ||
| 1105 | function onStabilityEngineChange() { | 1093 | async 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 | } | ||
| 1109 | 1096 | ||
| 1110 | function onStabilityStylePresetChange() { | 1097 | if (!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 | function onStabilityOutputFormatChange() { | 1107 | function 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 | } |
| 1124 | 1111 | ||
| 1125 | |||
| 1126 | async function changeComfyWorkflow(_, name) { | 1112 | async 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 | } |
| 1445 | 1434 | ||
| 1446 | for (const sampler of samplers) { | 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 | async function loadStabilityModels() { | 1635 | async 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 | } |
| 2059 | 1980 | ||
| 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 | } |
| 2132 | 2056 | ||
| 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 | } |
| 2616 | 2539 | ||
| 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 | } |
| 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 | async function generateTogetherAIImage(prompt, negativePrompt) { | 2567 | async 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 | } |
| 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 | async function generatePollinationsImage(prompt, negativePrompt) { | 2597 | async 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 | } |
| 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 | * 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 | } |
| 3362 | 3377 | ||
| @@ -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')); |
| @@ -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> |
| 417 | 380 | ||
| 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> |
| @@ -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,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 | router.use('/comfy', comfy); | 855 | router.use('/comfy', comfy); |
| 797 | router.use('/together', together); | 856 | router.use('/together', together); |
| 798 | router.use('/drawthings', drawthings); | 857 | router.use('/drawthings', drawthings); |
| 799 | router.use('/pollinations', pollinations); | 858 | router.use('/pollinations', pollinations); |
| 859 | router.use('/stability', stability); | ||
| 800 | 860 | ||
| 801 | module.exports = { router }; | 861 | module.exports = { router }; |