diff --git a/public/img/aimlapi.svg b/public/img/aimlapi.svg new file mode 100644 index 000000000..9520c60d4 --- /dev/null +++ b/public/img/aimlapi.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/index.html b/public/index.html index 7933d91dc..792c938da 100644 --- a/public/index.html +++ b/public/index.html @@ -652,7 +652,7 @@ -
+
Multiple swipes per generation
@@ -691,7 +691,7 @@
-
+
Temperature
@@ -704,7 +704,7 @@
-
+
Frequency Penalty
@@ -717,7 +717,7 @@
-
+
Presence Penalty
@@ -730,7 +730,7 @@
-
+
Top K
@@ -743,7 +743,7 @@
-
+
Top P
@@ -980,7 +980,7 @@
-
+
Seed
@@ -1969,7 +1969,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
Logit Bias
@@ -2376,7 +2376,7 @@
-
+
For privacy reasons, your API key will be hidden after you click 'Connect'.
@@ -2789,6 +2789,7 @@ + @@ -3632,6 +3633,21 @@
+
+

+ AI/ML API Key +

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you click 'Connect'. +
+

AI/ML Model

+ +

Pollinations Model

+ @@ -127,6 +128,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index c68d6c575..3301c1077 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -233,6 +233,10 @@ function throwIfInvalidModel(useReverseProxy) { if (multimodalApi === 'custom' && !oai_settings.custom_url) { throw new Error('Custom API URL is not set.'); } + + if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) { + throw new Error('AI/ML API key is not set.'); + } } /** diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 2d3190449..7edb6cfc5 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -41,7 +41,7 @@ import { stringFormat, } from '../../utils.js'; import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; -import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; +import { SECRET_KEYS, secret_state } from '../../secrets.js'; import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js'; import { getMultimodalCaption } from '../shared.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; @@ -53,7 +53,7 @@ import { } from '../../slash-commands/SlashCommandArgument.js'; import { debounce_timeout } from '../../constants.js'; import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; -import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; +import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { ToolManager } from '../../tool-calling.js'; import { MacrosParser } from '../../macros.js'; @@ -74,6 +74,7 @@ const sources = { novel: 'novel', vlad: 'vlad', openai: 'openai', + aimlapi: 'aimlapi', comfy: 'comfy', togetherai: 'togetherai', drawthings: 'drawthings', @@ -1144,42 +1145,6 @@ function onComfyWorkflowChange() { saveSettingsDebounced(); } -async function onApiKeyClick(popupText, secretKey) { - const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', { - customButtons: [{ - text: 'Remove Key', - appendAtEnd: true, - result: POPUP_RESULT.NEGATIVE, - action: async () => { - await writeSecret(secretKey, ''); - toastr.success('API Key removed'); - await loadSettingOptions(); - }, - }], - }); - - if (!key) { - return; - } - - await writeSecret(secretKey, String(key)); - - toastr.success('API Key saved'); - await loadSettingOptions(); -} - -async function onStabilityKeyClick() { - return onApiKeyClick('Stability AI API Key:', SECRET_KEYS.STABILITY); -} - -async function onBflKeyClick() { - return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); -} - -async function onFalaiKeyClick() { - return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI); -} - function onBflUpsamplingInput() { extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); saveSettingsDebounced(); @@ -1303,6 +1268,7 @@ async function onModelChange() { sources.horde, sources.novel, sources.openai, + sources.aimlapi, sources.togetherai, sources.pollinations, sources.stability, @@ -1505,6 +1471,9 @@ async function loadSamplers() { case sources.openai: samplers = ['N/A']; break; + case sources.aimlapi: + samplers = ['N/A']; + break; case sources.comfy: samplers = await loadComfySamplers(); break; @@ -1695,6 +1664,9 @@ async function loadModels() { case sources.openai: models = await loadOpenAiModels(); break; + case sources.aimlapi: + models = await loadAimlapiModels(); + break; case sources.comfy: models = await loadComfyModels(); break; @@ -1959,6 +1931,23 @@ async function loadOpenAiModels() { ]; } +async function loadAimlapiModels() { + $('#sd_aimlapi_key').toggleClass('success', !!secret_state[SECRET_KEYS.AIMLAPI]); + + const result = await fetch('/api/sd/aimlapi/models', { + method: 'POST', + headers: getRequestHeaders(), + }); + + if (!result.ok) { + return []; + } + + const json = await result.json(); + + return (json.data || []); +} + async function loadVladModels() { if (!extension_settings.sd.vlad_url) { return []; @@ -2086,6 +2075,9 @@ async function loadSchedulers() { case sources.openai: schedulers = ['N/A']; break; + case sources.aimlapi: + schedulers = ['N/A']; + break; case sources.togetherai: schedulers = ['N/A']; break; @@ -2177,6 +2169,9 @@ async function loadVaes() { case sources.openai: vaes = ['N/A']; break; + case sources.aimlapi: + vaes = ['N/A']; + break; case sources.togetherai: vaes = ['N/A']; break; @@ -2749,6 +2744,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP case sources.openai: result = await generateOpenAiImage(prefixedPrompt, signal); break; + case sources.aimlapi: + result = await generateAimlapiImage(prefixedPrompt, signal); + break; case sources.comfy: result = await generateComfyImage(prefixedPrompt, negativePrompt, signal); break; @@ -3333,6 +3331,47 @@ async function generateOpenAiImage(prompt, signal) { } } +/** + * Universal image generation via AIMLAPI: + * - Builds the right request body for any model (OpenAI vs SD/Flux/Recraft). + * - Extracts the URL or base64 response. + * - If it’s a URL, fetches the image and converts to base64. + * - Returns { format: 'png', data: '' }, ready for saveBase64AsFile(). + */ +async function generateAimlapiImage(prompt, signal) { + const model = extension_settings.sd.model.toLowerCase(); + const isSdLike = + model.startsWith('flux/') || + model.startsWith('stable') || + model === 'recraft-v3' || + model === 'triposr'; + + const body = { prompt, model }; + if (isSdLike) { + body.steps = clamp(extension_settings.sd.steps, 1, 50); + body.guidance = clamp(extension_settings.sd.scale, 1.5, 5); + body.width = clamp(extension_settings.sd.width, 256, 1440); + body.height = clamp(extension_settings.sd.height, 256, 1440); + if (extension_settings.sd.seed >= 0) body.seed = extension_settings.sd.seed; + } else { + body.n = 1; + body.size = `${extension_settings.sd.width}x${extension_settings.sd.height}`; + body.quality = extension_settings.sd.openai_quality; + body.style = extension_settings.sd.openai_style; + } + + const res = await fetch('/api/sd/aimlapi/generate-image', { + method: 'POST', + headers: getRequestHeaders(), + signal, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(await res.text()); + + const { format, data } = await res.json(); + return { format, data }; +} + /** * Generates an image in ComfyUI using the provided prompt and configuration settings. * @@ -3849,6 +3888,8 @@ function isValidState() { return secret_state[SECRET_KEYS.NOVEL]; case sources.openai: return secret_state[SECRET_KEYS.OPENAI]; + case sources.aimlapi: + return secret_state[SECRET_KEYS.AIMLAPI]; case sources.comfy: return true; case sources.togetherai: @@ -4522,13 +4563,10 @@ jQuery(async () => { $('#sd_interactive_visible').on('input', onInteractiveVisibleInput); $('#sd_tool_visible').on('input', onToolVisibleInput); $('#sd_swap_dimensions').on('click', onSwapDimensionsClick); - $('#sd_stability_key').on('click', onStabilityKeyClick); $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange); $('#sd_huggingface_model_id').on('input', onHFModelInput); $('#sd_function_tool').on('input', onFunctionToolInput); - $('#sd_bfl_key').on('click', onBflKeyClick); $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); - $('#sd_falai_key').on('click', onFalaiKeyClick); if (!CSS.supports('field-sizing', 'content')) { $('.sd_settings .inline-drawer-toggle').on('click', function () { @@ -4556,6 +4594,19 @@ jQuery(async () => { eventSource.on(event_types.CHAT_CHANGED, onChatChanged); + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { + eventSource.on(event, async (/** @type {string} */ key) => { + switch (key) { + case SECRET_KEYS.BFL: + case SECRET_KEYS.FALAI: + case SECRET_KEYS.STABILITY: + case SECRET_KEYS.AIMLAPI: + await loadSettingOptions(); + break; + } + }); + }); + await loadSettings(); $('body').addClass('sd'); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 5f7174c63..d99e072eb 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -37,6 +37,7 @@