diff --git a/public/css/popup-safari-fix.css b/public/css/popup-safari-fix.css index 4f916747e..6838e3352 100644 --- a/public/css/popup-safari-fix.css +++ b/public/css/popup-safari-fix.css @@ -1,5 +1,6 @@ /* iPhone copium land */ -body.safari .popup .popup-body:has(.maximized_textarea) { +body.safari .popup .popup-body:has(.maximized_textarea), +body.safari .popup.large_dialogue_popup .popup-body { height: 100%; } diff --git a/public/css/promptmanager.css b/public/css/promptmanager.css index 178682998..5370a98e8 100644 --- a/public/css/promptmanager.css +++ b/public/css/promptmanager.css @@ -233,7 +233,6 @@ } #completion_prompt_manager .completion_prompt_manager_footer a { - padding: 0.75em; font-size: 12px; } diff --git a/public/css/rm-groups.css b/public/css/rm-groups.css index e7cbb2c95..499f59873 100644 --- a/public/css/rm-groups.css +++ b/public/css/rm-groups.css @@ -68,9 +68,9 @@ margin-top: 0.25rem; margin-bottom: 0.5rem; border: 1px solid var(--SmartThemeBorderColor); - ; border-radius: 10px; background-color: var(--black30a); + padding: 2px; } #rm_group_buttons_expander { diff --git a/public/index.html b/public/index.html index 851b54228..6109568bb 100644 --- a/public/index.html +++ b/public/index.html @@ -4141,6 +4141,10 @@ Request token probabilities +
Auto-swipe @@ -5596,10 +5600,10 @@ -
Injection position. Next to other prompts (relative) or in-chat (absolute).
+
Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.
@@ -5796,6 +5800,7 @@
+
diff --git a/public/locales/ar-sa.json b/public/locales/ar-sa.json index d722b70c0..a227372b2 100644 --- a/public/locales/ar-sa.json +++ b/public/locales/ar-sa.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "موضع", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "موضع الحقن. بجوار المطالبات الأخرى (نسبية) أو داخل الدردشة (مطلقة).", "prompt_manager_relative": "نسبي", - "prompt_manager_absolute": "مطلق", "prompt_manager_depth": "عمق", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "عمق الحقن. 0 = بعد الرسالة الأخيرة، 1 = قبل الرسالة الأخيرة، الخ.", "Prompt": "موضوع", diff --git a/public/locales/de-de.json b/public/locales/de-de.json index 4551322da..fcadcbfbc 100644 --- a/public/locales/de-de.json +++ b/public/locales/de-de.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Position", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Injektionsposition. Neben anderen Eingabeaufforderungen (relativ) oder im Chat (absolut).", "prompt_manager_relative": "Relativ", - "prompt_manager_absolute": "Absolut", "prompt_manager_depth": "Tiefe", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Injektionstiefe. 0 = nach der letzten Nachricht, 1 = vor der letzten Nachricht usw.", "Prompt": "Aufforderung", diff --git a/public/locales/es-es.json b/public/locales/es-es.json index d4b54fa7a..36cc002bc 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Posición", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Posición de inyección. Junto a otras indicaciones (relativa) o en el chat (absoluta).", "prompt_manager_relative": "Relativo", - "prompt_manager_absolute": "Absoluto", "prompt_manager_depth": "Profundidad", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Profundidad de inyección. 0 = después del último mensaje, 1 = antes del último mensaje, etc.", "Prompt": "Indicar", diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 84f502e8c..1b30fda0e 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Position", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Position d'injection. À côté d’autres invites (relatives) ou dans le chat (absolues).", "prompt_manager_relative": "Relatif", - "prompt_manager_absolute": "Absolu", "prompt_manager_depth": "Profondeur", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Profondeur d'injection. 0 = après le dernier message, 1 = avant le dernier message, etc.", "Prompt": "Inciter", diff --git a/public/locales/is-is.json b/public/locales/is-is.json index 1d5587e0c..45e5a3095 100644 --- a/public/locales/is-is.json +++ b/public/locales/is-is.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Staða", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Inndælingarstaða. Við hliðina á öðrum leiðbeiningum (afstætt) eða í spjalli (algert).", "prompt_manager_relative": "Aðstandandi", - "prompt_manager_absolute": "Algjört", "prompt_manager_depth": "Dýpt", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Inndælingardýpt. 0 = eftir síðustu skilaboð, 1 = fyrir síðustu skilaboð o.s.frv.", "Prompt": "Ábending", diff --git a/public/locales/it-it.json b/public/locales/it-it.json index 18f0a3fa9..ce0f9f03a 100644 --- a/public/locales/it-it.json +++ b/public/locales/it-it.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Posizione", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Posizione di iniezione. Accanto ad altri suggerimenti (relativo) o in chat (assoluto).", "prompt_manager_relative": "Parente", - "prompt_manager_absolute": "Assoluto", "prompt_manager_depth": "Profondità", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Profondità di iniezione. 0 = dopo l'ultimo messaggio, 1 = prima dell'ultimo messaggio, ecc.", "Prompt": "Prompt", diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index cfb6f6c79..2e18796f4 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "位置", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "挿入位置。他のプロンプトの隣 (相対) またはチャット内 (絶対)。", "prompt_manager_relative": "相対的", - "prompt_manager_absolute": "絶対", "prompt_manager_depth": "深さ", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入の深さ。0 = 最後のメッセージの後、1 = 最後のメッセージの前など。", "Prompt": "プロンプト", diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 1c56ff3cc..cf04e38ae 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "위치", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "주입 위치. 다른 프롬프트 옆(상대적) 또는 채팅 내(절대적).", "prompt_manager_relative": "상대적인", - "prompt_manager_absolute": "순수한", "prompt_manager_depth": "깊이", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "주입 깊이. 0 = 마지막 메시지 뒤, 1 = 마지막 메시지 앞 등", "Prompt": "프롬프트", diff --git a/public/locales/nl-nl.json b/public/locales/nl-nl.json index f102ee93e..256eda091 100644 --- a/public/locales/nl-nl.json +++ b/public/locales/nl-nl.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Positie", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Injectiepositie. Naast andere prompts (relatief) of in-chat (absoluut).", "prompt_manager_relative": "Familielid", - "prompt_manager_absolute": "Absoluut", "prompt_manager_depth": "Diepte", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Injectiediepte. 0 = na het laatste bericht, 1 = voor het laatste bericht, etc.", "Prompt": "Prompt", diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index caf918890..77bbdbafd 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Posição", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Posição de injeção. Ao lado de outras solicitações (relativas) ou no chat (absolutas).", "prompt_manager_relative": "Relativo", - "prompt_manager_absolute": "Absoluto", "prompt_manager_depth": "Profundidade", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Profundidade de injeção. 0 = após a última mensagem, 1 = antes da última mensagem, etc.", "Prompt": "Prompt", diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index 2e8217605..02d19207a 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -1025,7 +1025,6 @@ "prompt_manager_position": "Точка инжекта", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Как рассчитывать позицию для инжекта. Она может располагаться по отношению к другим промптам (относительная) либо по отношению к чату (абсолютная).", "prompt_manager_relative": "Относительная", - "prompt_manager_absolute": "Абсолютная", "prompt_manager_depth": "Глубина", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.", "The prompt to be sent.": "Отправляемый ИИ промпт.", diff --git a/public/locales/uk-ua.json b/public/locales/uk-ua.json index ec9c933fa..bd2ae43f1 100644 --- a/public/locales/uk-ua.json +++ b/public/locales/uk-ua.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Позиція", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Позиція ін'єкції. Поруч з іншими підказками (відносні) або в чаті (абсолютні).", "prompt_manager_relative": "Відносна", - "prompt_manager_absolute": "Абсолютний", "prompt_manager_depth": "Глибина", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глибина ін'єкції. 0 = після останнього повідомлення, 1 = перед останнім повідомленням тощо.", "Prompt": "Запит", diff --git a/public/locales/vi-vn.json b/public/locales/vi-vn.json index bfcfb4dd3..d7399ea45 100644 --- a/public/locales/vi-vn.json +++ b/public/locales/vi-vn.json @@ -1023,7 +1023,6 @@ "prompt_manager_position": "Chức vụ", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "Vị trí tiêm. Bên cạnh các lời nhắc khác (tương đối) hoặc trong trò chuyện (tuyệt đối).", "prompt_manager_relative": "Liên quan đến", - "prompt_manager_absolute": "tuyệt đối", "prompt_manager_depth": "Chiều sâu", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Độ sâu phun. 0 = sau tin nhắn cuối cùng, 1 = trước tin nhắn cuối cùng, v.v.", "Prompt": "Đề xuất", diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 759af7fc6..1e236dcf6 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -1056,7 +1056,6 @@ "prompt_manager_position": "位置", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。其他提示词旁边(相对)或在聊天中(绝对)。", "prompt_manager_relative": "相对", - "prompt_manager_absolute": "绝对", "prompt_manager_depth": "深度", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最后一条消息之后,1 = 在最后一条消息之前,等等。", "Prompt": "提示词", diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index cb5cb0965..e8418ef56 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -1025,7 +1025,6 @@ "prompt_manager_position": "位置", "Injection position. Next to other prompts (relative) or in-chat (absolute).": "注入位置。與其他提示詞相鄰(相對位置)或在聊天中(絕對位置)。", "prompt_manager_relative": "相對位置", - "prompt_manager_absolute": "絕對位置", "prompt_manager_depth": "深度", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "注入深度。0 = 在最後一條訊息之後,1 = 在最後一條訊息之前,以此類推。", "Prompt": "提示詞", diff --git a/public/script.js b/public/script.js index c657423d9..6e25dab59 100644 --- a/public/script.js +++ b/public/script.js @@ -6175,7 +6175,7 @@ async function doOnboarding(avatarId) { template.find('input[name="enable_simple_mode"]').on('input', function () { simpleUiMode = $(this).is(':checked'); }); - let userName = await callGenericPopup(template, POPUP_TYPE.INPUT, currentUser?.name || name1, { rows: 2 }); + let userName = await callGenericPopup(template, POPUP_TYPE.INPUT, currentUser?.name || name1, { rows: 2, wide: true, large: true }); if (userName) { userName = userName.replace('\n', ' '); diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index b7f0a6496..8fcb55194 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -22,7 +22,7 @@ import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, rend import { selected_group } from '../../group-chats.js'; import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js'; import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; -import { SECRET_KEYS, secret_state } from '../../secrets.js'; +import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js'; import { getMultimodalCaption } from '../shared.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; @@ -49,6 +49,7 @@ const sources = { togetherai: 'togetherai', drawthings: 'drawthings', pollinations: 'pollinations', + stability: 'stability', }; const initiators = { @@ -282,6 +283,9 @@ const defaultSettings = { wand_visible: false, command_visible: false, interactive_visible: false, + + // Stability AI settings + stability_style_preset: 'anime', }; const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); @@ -444,6 +448,7 @@ async function loadSettings() { $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible); $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible); $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible); + $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset); for (const style of extension_settings.sd.styles) { const option = document.createElement('option'); @@ -671,7 +676,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) { const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' }); if (refinedPrompt) { - return refinedPrompt; + return String(refinedPrompt); } else { throw new Error('Generation aborted by user.'); } @@ -1084,6 +1089,26 @@ function onComfyWorkflowChange() { extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val(); saveSettingsDebounced(); } + +async function onStabilityKeyClick() { + const popupText = 'Stability AI API Key:'; + const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT); + + if (!key) { + return; + } + + await writeSecret(SECRET_KEYS.STABILITY, String(key)); + + toastr.success('API Key saved'); + await loadSettingOptions(); +} + +function onStabilityStylePresetChange() { + extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val()); + saveSettingsDebounced(); +} + async function changeComfyWorkflow(_, name) { name = name.replace(/(\.json)?$/i, '.json'); if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) { @@ -1193,7 +1218,7 @@ async function onModelChange() { extension_settings.sd.model = $('#sd_model').find(':selected').val(); saveSettingsDebounced(); - const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations]; + const cloudSources = [sources.horde, sources.novel, sources.openai, sources.togetherai, sources.pollinations, sources.stability]; if (cloudSources.includes(extension_settings.sd.source)) { return; @@ -1402,6 +1427,9 @@ async function loadSamplers() { case sources.pollinations: samplers = ['N/A']; break; + case sources.stability: + samplers = ['N/A']; + break; } for (const sampler of samplers) { @@ -1585,6 +1613,9 @@ async function loadModels() { case sources.pollinations: models = await loadPollinationsModels(); break; + case sources.stability: + models = await loadStabilityModels(); + break; } for (const model of models) { @@ -1601,6 +1632,16 @@ async function loadModels() { } } +async function loadStabilityModels() { + $('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]); + + return [ + { value: 'stable-image-ultra', text: 'Stable Image Ultra' }, + { value: 'stable-image-core', text: 'Stable Image Core' }, + { value: 'stable-diffusion-3', text: 'Stable Diffusion 3' }, + ]; +} + async function loadPollinationsModels() { return [ { @@ -1932,6 +1973,9 @@ async function loadSchedulers() { case sources.comfy: schedulers = await loadComfySchedulers(); break; + case sources.stability: + schedulers = ['N/A']; + break; } for (const scheduler of schedulers) { @@ -2005,6 +2049,9 @@ async function loadVaes() { case sources.comfy: vaes = await loadComfyVaes(); break; + case sources.stability: + vaes = ['N/A']; + break; } for (const vae of vaes) { @@ -2485,6 +2532,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP case sources.pollinations: result = await generatePollinationsImage(prefixedPrompt, negativePrompt); break; + case sources.stability: + result = await generateStabilityImage(prefixedPrompt, negativePrompt); + break; } if (!result.data) { @@ -2508,6 +2558,12 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP return base64Image; } +/** + * Generates an image using the TogetherAI API. + * @param {string} prompt - The main instruction used to guide the image generation. + * @param {string} negativePrompt - The instruction used to restrict the image generation. + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. + */ async function generateTogetherAIImage(prompt, negativePrompt) { const result = await fetch('/api/sd/together/generate', { method: 'POST', @@ -2532,6 +2588,12 @@ async function generateTogetherAIImage(prompt, negativePrompt) { } } +/** + * Generates an image using the Pollinations API. + * @param {string} prompt - The main instruction used to guide the image generation. + * @param {string} negativePrompt - The instruction used to restrict the image generation. + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. + */ async function generatePollinationsImage(prompt, negativePrompt) { const result = await fetch('/api/sd/pollinations/generate', { method: 'POST', @@ -2600,6 +2662,84 @@ async function generateExtrasImage(prompt, negativePrompt) { } } +/** + * Gets an aspect ratio for Stability that is the closest to the given width and height. + * @param {number} width Target width + * @param {number} height Target height + * @returns {string} Closest aspect ratio as a string + */ +function getClosestAspectRatio(width, height) { + const aspectRatios = { + '16:9': 16 / 9, + '1:1': 1, + '21:9': 21 / 9, + '2:3': 2 / 3, + '3:2': 3 / 2, + '4:5': 4 / 5, + '5:4': 5 / 4, + '9:16': 9 / 16, + '9:21': 9 / 21, + }; + + const aspectRatio = width / height; + + let closestAspectRatio = Object.keys(aspectRatios)[0]; + let minDiff = Math.abs(aspectRatio - aspectRatios[closestAspectRatio]); + + for (const key in aspectRatios) { + const diff = Math.abs(aspectRatio - aspectRatios[key]); + if (diff < minDiff) { + minDiff = diff; + closestAspectRatio = key; + } + } + + return closestAspectRatio; +} + +/** + * Generates an image using Stability AI. + * @param {string} prompt - The main instruction used to guide the image generation. + * @param {string} negativePrompt - The instruction used to restrict the image generation. + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. + */ +async function generateStabilityImage(prompt, negativePrompt) { + const IMAGE_FORMAT = 'png'; + const PROMPT_LIMIT = 10000; + + try { + const response = await fetch('/api/sd/stability/generate', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + model: extension_settings.sd.model, + payload: { + prompt: prompt.slice(0, PROMPT_LIMIT), + negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT), + aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height), + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, + style_preset: extension_settings.sd.stability_style_preset, + output_format: IMAGE_FORMAT, + }, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const base64Image = await response.text(); + + return { + format: IMAGE_FORMAT, + data: base64Image, + }; + } catch (error) { + console.error('Error generating image with Stability AI:', error); + throw error; + } +} + /** * Generates a "horde" image using the provided prompt and configuration settings. * @@ -3228,6 +3368,8 @@ function isValidState() { return secret_state[SECRET_KEYS.TOGETHERAI]; case sources.pollinations: return true; + case sources.stability: + return secret_state[SECRET_KEYS.STABILITY]; } } @@ -3455,6 +3597,8 @@ jQuery(async () => { $('#sd_command_visible').on('input', onCommandVisibleInput); $('#sd_interactive_visible').on('input', onInteractiveVisibleInput); $('#sd_swap_dimensions').on('click', onSwapDimensionsClick); + $('#sd_stability_key').on('click', onStabilityKeyClick); + $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange); $('.sd_settings .inline-drawer-toggle').on('click', function () { initScrollHeight($('#sd_prompt_prefix')); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 7546cc030..25869a966 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -44,6 +44,7 @@ + @@ -189,7 +190,45 @@
+
+
+ API Key + +
+
+ + You can find your API key in the Stability AI dashboard. + +
+
+
+ + +
+
+
@@ -203,7 +242,7 @@
-
+
@@ -339,7 +378,7 @@
-
+
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 2150fa4ab..b38ae1fa4 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -50,6 +50,28 @@ const world_info_logic = { AND_ALL: 3, }; +/** + * @enum {number} Possible states of the WI evaluation + */ +const scan_state = { + /** + * The scan will be stopped. + */ + NONE: 0, + /** + * Initial state. + */ + INITIAL: 1, + /** + * The scan is triggered by a recursion step. + */ + RECURSION: 2, + /** + * The scan is triggered by a min activations depth skew. + */ + MIN_ACTIVATIONS: 2, +}; + const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry'); let world_info = {}; @@ -135,6 +157,11 @@ class WorldInfoBuffer { */ #recurseBuffer = []; + /** + * @type {string[]} Array of strings added by prompt injections that are valid for the current scan + */ + #injectBuffer = []; + /** * @type {number} The skew of the global scan depth. Used in "min activations" */ @@ -184,9 +211,10 @@ class WorldInfoBuffer { /** * Gets all messages up to the given depth + recursion buffer. * @param {WIScanEntry} entry The entry that triggered the scan + * @param {number} scanState The state of the scan * @returns {string} A slice of buffer until the given depth (inclusive) */ - get(entry) { + get(entry, scanState) { let depth = entry.scanDepth ?? this.getDepth(); if (depth <= this.#startDepth) { return ''; @@ -204,7 +232,12 @@ class WorldInfoBuffer { let result = this.#depthBuffer.slice(this.#startDepth, depth).join('\n'); - if (this.#recurseBuffer.length > 0) { + if (this.#injectBuffer.length > 0) { + result += '\n' + this.#injectBuffer.join('\n'); + } + + // Min activations should not include the recursion buffer + if (this.#recurseBuffer.length > 0 && scanState !== scan_state.MIN_ACTIVATIONS) { result += '\n' + this.#recurseBuffer.join('\n'); } @@ -258,6 +291,14 @@ class WorldInfoBuffer { this.#recurseBuffer.push(message); } + /** + * Adds an injection to the buffer. + * @param {string} message The injection to add + */ + addInject(message) { + this.#injectBuffer.push(message); + } + /** * Increments skew and sets startDepth to previous depth. */ @@ -293,10 +334,11 @@ class WorldInfoBuffer { /** * Gets the match score for the given entry. * @param {WIScanEntry} entry Entry to check + * @param {number} scanState The state of the scan * @returns {number} The number of key activations for the given entry */ - getScore(entry) { - const bufferState = this.get(entry); + getScore(entry, scanState) { + const bufferState = this.get(entry, scanState); let numberOfPrimaryKeys = 0; let numberOfSecondaryKeys = 0; let primaryScore = 0; @@ -3503,12 +3545,12 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { if (context.extensionPrompts[key]?.scan) { const prompt = getExtensionPromptByName(key); if (prompt) { - buffer.addRecurse(prompt); + buffer.addInject(prompt); } } } - let needsToScan = true; + let scanState = scan_state.INITIAL; let token_budget_overflowed = false; let count = 0; let allActivatedEntries = new Set(); @@ -3532,8 +3574,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; } - while (needsToScan) { - // Track how many times the loop has run + while (scanState) { + // Track how many times the loop has run. May be useful for debugging. + // eslint-disable-next-line no-unused-vars count++; let activatedNow = new Set(); @@ -3587,7 +3630,18 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { continue; } - if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) { + if (allActivatedEntries.has(entry) || entry.disable == true) { + continue; + } + + // Only use checks for recursion flags if the scan step was activated by recursion + if (scanState !== scan_state.RECURSION && entry.delayUntilRecursion) { + console.debug(`WI entry ${entry.uid} suppressed by delay until recursion`, entry); + continue; + } + + if (scanState === scan_state.RECURSION && world_info_recursive && entry.excludeRecursion) { + console.debug(`WI entry ${entry.uid} suppressed by exclude recursion`, entry); continue; } @@ -3602,7 +3656,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { primary: for (let key of entry.key) { const substituted = substituteParams(key); - const textToScan = buffer.get(entry); + const textToScan = buffer.get(entry, scanState); if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) { console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`); @@ -3665,14 +3719,14 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { } } - needsToScan = world_info_recursive && activatedNow.size > 0; + scanState = world_info_recursive && activatedNow.size > 0 ? scan_state.RECURSION : scan_state.NONE; const newEntries = [...activatedNow] .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b)); let newContent = ''; const textToScanTokens = await getTokenCountAsync(allActivatedText); const probabilityChecksBefore = failedProbabilityChecks.size; - filterByInclusionGroups(newEntries, allActivatedEntries, buffer); + filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState); console.debug('-- PROBABILITY CHECKS BEGIN --'); for (const entry of newEntries) { @@ -3697,7 +3751,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { console.log('Alerting'); toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info'); } - needsToScan = false; + scanState = scan_state.NONE; token_budget_overflowed = true; break; } @@ -3710,15 +3764,15 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) { console.debug('WI probability checks failed for all activated entries, stopping'); - needsToScan = false; + scanState = scan_state.NONE; } if (newEntries.length === 0) { console.debug('No new entries activated, stopping'); - needsToScan = false; + scanState = scan_state.NONE; } - if (needsToScan) { + if (scanState) { const text = newEntries .filter(x => !failedProbabilityChecks.has(x)) .filter(x => !x.preventRecursion) @@ -3728,7 +3782,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { } // world_info_min_activations - if (!needsToScan && !token_budget_overflowed) { + if (!scanState && !token_budget_overflowed) { if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) { let over_max = ( world_info_min_activations_depth_max > 0 && @@ -3736,7 +3790,7 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { ) || (buffer.getDepth() > chat.length); if (!over_max) { - needsToScan = true; // loop + scanState = scan_state.MIN_ACTIVATIONS; // loop buffer.advanceScanPosition(); } } @@ -3824,8 +3878,9 @@ async function checkWorldInfo(chat, maxContext, isDryRun) { * @param {Record} groups The groups to filter * @param {WorldInfoBuffer} buffer The buffer to use for scoring * @param {(entry: WIScanEntry) => void} removeEntry The function to remove an entry + * @param {number} scanState The current scan state */ -function filterGroupsByScoring(groups, buffer, removeEntry) { +function filterGroupsByScoring(groups, buffer, removeEntry, scanState) { for (const [key, group] of Object.entries(groups)) { // Group scoring is disabled both globally and for the group entries if (!world_info_use_group_scoring && !group.some(x => x.useGroupScoring)) { @@ -3833,7 +3888,7 @@ function filterGroupsByScoring(groups, buffer, removeEntry) { continue; } - const scores = group.map(entry => buffer.getScore(entry)); + const scores = group.map(entry => buffer.getScore(entry, scanState)); const maxScore = Math.max(...scores); console.debug(`Group '${key}' max score: ${maxScore}`); //console.table(group.map((entry, i) => ({ uid: entry.uid, key: JSON.stringify(entry.key), score: scores[i] }))); @@ -3861,8 +3916,9 @@ function filterGroupsByScoring(groups, buffer, removeEntry) { * @param {object[]} newEntries Entries activated on current recursion level * @param {Set} allActivatedEntries Set of all activated entries * @param {WorldInfoBuffer} buffer The buffer to use for scanning + * @param {number} scanState The current scan state */ -function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) { +function filterByInclusionGroups(newEntries, allActivatedEntries, buffer, scanState) { console.debug('-- INCLUSION GROUP CHECKS BEGIN --'); const grouped = newEntries.filter(x => x.group).reduce((acc, item) => { item.group.split(/,\s*/).filter(x => x).forEach(group => { @@ -3891,7 +3947,7 @@ function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) { } } - filterGroupsByScoring(grouped, buffer, removeEntry); + filterGroupsByScoring(grouped, buffer, removeEntry, scanState); for (const [key, group] of Object.entries(grouped)) { console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group); diff --git a/public/style.css b/public/style.css index 8858117bb..4ccee63ad 100644 --- a/public/style.css +++ b/public/style.css @@ -2956,6 +2956,23 @@ input[type=search]:focus::-webkit-search-cancel-button { position: relative; } +.group_member .queue_position:not(:empty)::before { + content: "#"; +} + +.group_member .queue_position { + margin-right: 0.75rem; + font-size: calc(var(--mainFontSize) * 0.9); +} + +.group_member.is_queued { + outline: 2px solid var(--golden); +} + +.group_member.is_active { + outline: 2px solid var(--active); +} + .character_select.is_fav .avatar, .group_select.is_fav .avatar, .group_member.is_fav .avatar, diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index dedb23096..beba158ef 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -43,6 +43,7 @@ const SECRET_KEYS = { FEATHERLESS: 'api_key_featherless', ZEROONEAI: 'api_key_01ai', HUGGINGFACE: 'api_key_huggingface', + STABILITY: 'api_key_stability', }; // These are the keys that are safe to expose, even if allowKeysExposure is false diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index a93582803..0ea8bfc27 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -7,6 +7,7 @@ const path = require('path'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser } = require('../express-common'); const { readSecret, SECRET_KEYS } = require('./secrets.js'); +const FormData = require('form-data'); /** * Sanitizes a string. @@ -793,9 +794,71 @@ pollinations.post('/generate', jsonParser, async (request, response) => { } }); +const stability = express.Router(); + +stability.post('/generate', jsonParser, async (request, response) => { + try { + const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY); + + if (!key) { + console.log('Stability AI key not found.'); + return response.sendStatus(400); + } + + const { payload, model } = request.body; + + console.log('Stability AI request:', model, payload); + + const formData = new FormData(); + for (const [key, value] of Object.entries(payload)) { + if (value !== undefined) { + formData.append(key, String(value)); + } + } + + let apiUrl; + switch (model) { + case 'stable-image-ultra': + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/ultra'; + break; + case 'stable-image-core': + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/core'; + break; + case 'stable-diffusion-3': + apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/sd3'; + break; + default: + throw new Error('Invalid Stability AI model selected'); + } + + const result = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${key}`, + 'Accept': 'image/*', + }, + body: formData, + timeout: 0, + }); + + if (!result.ok) { + const text = await result.text(); + console.log('Stability AI returned an error.', result.status, result.statusText, text); + return response.sendStatus(500); + } + + const buffer = await result.buffer(); + return response.send(buffer.toString('base64')); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + router.use('/comfy', comfy); router.use('/together', together); router.use('/drawthings', drawthings); router.use('/pollinations', pollinations); +router.use('/stability', stability); module.exports = { router };