| 1 | | -import { Popper } from '../../../lib.js'; |
| 2 | | -import { |
| 3 | | - animation_duration, |
| 4 | | - appendMediaToMessage, |
| 5 | | - event_types, |
| 6 | | - eventSource, |
| 7 | | - formatCharacterAvatar, |
| 8 | | - generateQuietPrompt, |
| 9 | | - getCharacterAvatar, |
| 10 | | - getCurrentChatId, |
| 11 | | - getRequestHeaders, |
| 12 | | - getUserAvatar, |
| 13 | | - online_status, |
| 14 | | - saveSettingsDebounced, |
| 15 | | - substituteParams, |
| 16 | | - substituteParamsExtended, |
| 17 | | - systemUserName, |
| 18 | | - this_chid, |
| 19 | | - user_avatar, |
| 20 | | -} from '../../../script.js'; |
| 21 | | -import { |
| 22 | | - doExtrasFetch, |
| 23 | | - extension_settings, |
| 24 | | - getApiUrl, |
| 25 | | - getContext, |
| 26 | | - modules, |
| 27 | | - renderExtensionTemplateAsync, |
| 28 | | - writeExtensionField, |
| 29 | | -} from '../../extensions.js'; |
| 30 | | -import { selected_group } from '../../group-chats.js'; |
| 31 | | -import { |
| 32 | | - clamp, |
| 33 | | - debounce, |
| 34 | | - deepMerge, |
| 35 | | - delay, |
| 36 | | - getBase64Async, |
| 37 | | - getCharaFilename, |
| 38 | | - initScrollHeight, |
| 39 | | - isFalseBoolean, |
| 40 | | - isTrueBoolean, |
| 41 | | - resetScrollHeight, |
| 42 | | - saveBase64AsFile, |
| 43 | | - stringFormat, |
| 44 | | - waitUntilCondition, |
| 45 | | -} from '../../utils.js'; |
| 46 | | -import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js'; |
| 47 | | -import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 48 | | -import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js'; |
| 49 | | -import { ConnectionManagerRequestService, getMultimodalCaption } from '../shared.js'; |
| 50 | | -import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 51 | | -import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 52 | | -import { |
| 53 | | - ARGUMENT_TYPE, |
| 54 | | - SlashCommandArgument, |
| 55 | | - SlashCommandNamedArgument, |
| 56 | | -} from '../../slash-commands/SlashCommandArgument.js'; |
| 57 | | -import { debounce_timeout, IMAGE_OVERSWIPE, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION, VIDEO_EXTENSIONS } from '../../constants.js'; |
| 58 | | -import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 59 | | -import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 60 | | -import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 61 | | -import { ToolManager } from '../../tool-calling.js'; |
| 62 | | -import { macros, MacroCategory } from '../../macros/macro-system.js'; |
| 63 | | -import { t, translate } from '../../i18n.js'; |
| 64 | | -import { oai_settings } from '../../openai.js'; |
| 65 | | -import { power_user } from '/scripts/power-user.js'; |
| 66 | | -import { MacrosParser } from '/scripts/macros.js'; |
| 67 | | -import { ActionLoaderHandle, loader } from '/scripts/action-loader.js'; |
| 68 | | - |
| 69 | | -export { MODULE_NAME }; |
| 70 | | - |
| 71 | | -const MODULE_NAME = 'sd'; |
| 72 | | -// This is a 1x1 transparent PNG |
| 73 | | -const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; |
| 74 | | - |
| 75 | | -const sources = { |
| 76 | | - extras: 'extras', |
| 77 | | - horde: 'horde', |
| 78 | | - auto: 'auto', |
| 79 | | - sdcpp: 'sdcpp', |
| 80 | | - novel: 'novel', |
| 81 | | - vlad: 'vlad', |
| 82 | | - openai: 'openai', |
| 83 | | - aimlapi: 'aimlapi', |
| 84 | | - comfy: 'comfy', |
| 85 | | - togetherai: 'togetherai', |
| 86 | | - drawthings: 'drawthings', |
| 87 | | - pollinations: 'pollinations', |
| 88 | | - stability: 'stability', |
| 89 | | - huggingface: 'huggingface', |
| 90 | | - chutes: 'chutes', |
| 91 | | - electronhub: 'electronhub', |
| 92 | | - nanogpt: 'nanogpt', |
| 93 | | - bfl: 'bfl', |
| 94 | | - falai: 'falai', |
| 95 | | - xai: 'xai', |
| 96 | | - google: 'google', |
| 97 | | - zai: 'zai', |
| 98 | | - openrouter: 'openrouter', |
| 99 | | - workersai: 'workersai', |
| 100 | | -}; |
| 101 | | -const comfyTypes = { |
| 102 | | - standard: 'standard', |
| 103 | | - runpod_serverless: 'runpod_serverless', |
| 104 | | -}; |
| 105 | | - |
| 106 | | -const initiators = { |
| 107 | | - command: 'command', |
| 108 | | - action: 'action', |
| 109 | | - interactive: 'interactive', |
| 110 | | - wand: 'wand', |
| 111 | | - swipe: 'swipe', |
| 112 | | - tool: 'tool', |
| 113 | | -}; |
| 114 | | - |
| 115 | | -const generationMode = { |
| 116 | | - TOOL: -2, |
| 117 | | - MESSAGE: -1, |
| 118 | | - CHARACTER: 0, |
| 119 | | - USER: 1, |
| 120 | | - SCENARIO: 2, |
| 121 | | - RAW_LAST: 3, |
| 122 | | - NOW: 4, |
| 123 | | - FACE: 5, |
| 124 | | - FREE: 6, |
| 125 | | - BACKGROUND: 7, |
| 126 | | - CHARACTER_MULTIMODAL: 8, |
| 127 | | - USER_MULTIMODAL: 9, |
| 128 | | - FACE_MULTIMODAL: 10, |
| 129 | | - FREE_EXTENDED: 11, |
| 130 | | - CUSTOM: 12, |
| 131 | | -}; |
| 132 | | - |
| 133 | | -const multimodalMap = { |
| 134 | | - [generationMode.CHARACTER]: generationMode.CHARACTER_MULTIMODAL, |
| 135 | | - [generationMode.USER]: generationMode.USER_MULTIMODAL, |
| 136 | | - [generationMode.FACE]: generationMode.FACE_MULTIMODAL, |
| 137 | | -}; |
| 138 | | - |
| 139 | | -const modeLabels = { |
| 140 | | - [generationMode.TOOL]: 'Function Tool Prompt Description', |
| 141 | | - [generationMode.MESSAGE]: 'Chat Message Template', |
| 142 | | - [generationMode.CHARACTER]: 'Character ("Yourself")', |
| 143 | | - [generationMode.FACE]: 'Portrait ("Your Face")', |
| 144 | | - [generationMode.USER]: 'User ("Me")', |
| 145 | | - [generationMode.SCENARIO]: 'Scenario ("The Whole Story")', |
| 146 | | - [generationMode.NOW]: 'Last Message', |
| 147 | | - [generationMode.RAW_LAST]: 'Raw Last Message', |
| 148 | | - [generationMode.BACKGROUND]: 'Background', |
| 149 | | - [generationMode.CHARACTER_MULTIMODAL]: 'Character (Multimodal Mode)', |
| 150 | | - [generationMode.FACE_MULTIMODAL]: 'Portrait (Multimodal Mode)', |
| 151 | | - [generationMode.USER_MULTIMODAL]: 'User (Multimodal Mode)', |
| 152 | | - [generationMode.FREE_EXTENDED]: 'Free Mode (LLM-Extended)', |
| 153 | | - [generationMode.CUSTOM]: 'Custom', |
| 154 | | -}; |
| 155 | | - |
| 156 | | -const triggerWords = { |
| 157 | | - [generationMode.CHARACTER]: ['you'], |
| 158 | | - [generationMode.USER]: ['me'], |
| 159 | | - [generationMode.SCENARIO]: ['scene'], |
| 160 | | - [generationMode.RAW_LAST]: ['raw_last'], |
| 161 | | - [generationMode.NOW]: ['last'], |
| 162 | | - [generationMode.FACE]: ['face'], |
| 163 | | - [generationMode.BACKGROUND]: ['background'], |
| 164 | | -}; |
| 165 | | - |
| 166 | | -const messageTrigger = { |
| 167 | | - activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render|show)\b.{0,10}\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those|your)?\s+)?(.+)/i, |
| 168 | | - specialCases: { |
| 169 | | - [generationMode.CHARACTER]: ['you', 'yourself'], |
| 170 | | - [generationMode.USER]: ['me', 'myself'], |
| 171 | | - [generationMode.SCENARIO]: ['story', 'scenario', 'whole story'], |
| 172 | | - [generationMode.NOW]: ['last message'], |
| 173 | | - [generationMode.FACE]: ['face', 'portrait', 'selfie'], |
| 174 | | - [generationMode.BACKGROUND]: ['background', 'scene background', 'scene', 'scenery', 'surroundings', 'environment'], |
| 175 | | - }, |
| 176 | | -}; |
| 177 | | - |
| 178 | | -const promptTemplates = { |
| 179 | | - // Not really a prompt template, rather an outcome message template and function tool prompt |
| 180 | | - [generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].', |
| 181 | | - [generationMode.TOOL]: [ |
| 182 | | - 'The text prompt used to generate the image.', |
| 183 | | - 'Must represent an exhaustive description of the desired image that will allow an artist or a photographer to perfectly recreate it.', |
| 184 | | - ].join(' '), |
| 185 | | - [generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'', |
| 186 | | - //face-specific prompt |
| 187 | | - [generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'', |
| 188 | | - //prompt for only the last message |
| 189 | | - [generationMode.USER]: 'Ignore previous instructions and provide a detailed description of {{user}}\'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'. Ignore the rest of the story when crafting this description. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.', |
| 190 | | - [generationMode.SCENARIO]: 'Ignore previous instructions and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}\'s appearance, and {{char}}\'s surroundings. Do not reply as {{char}} while writing this description.', |
| 191 | | - |
| 192 | | - [generationMode.NOW]: `Ignore previous instructions. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message. |
| 193 | | - |
| 194 | | - Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman'). |
| 195 | | - |
| 196 | | - Ignore non-visible things such as feelings, personality traits, thoughts, and spoken dialog. |
| 197 | | - |
| 198 | | - Add keywords in this precise order: |
| 199 | | - a keyword to describe the location of the scene, |
| 200 | | - a keyword to mention how many characters of each gender or type are present in the scene (minimum of two characters: |
| 201 | | - {{user}} and {{char}}, example: '2 men ' or '1 man 1 woman ', '1 man 3 robots'), |
| 202 | | - |
| 203 | | - keywords to describe the relative physical positioning of the characters to each other (if a commonly known term for the positioning is known use it instead of describing the positioning in detail) + 'POV', |
| 204 | | - |
| 205 | | - a single keyword or phrase to describe the primary act taking place in the last chat message, |
| 206 | | - |
| 207 | | - keywords to describe {{char}}'s physical appearance and facial expression, |
| 208 | | - keywords to describe {{char}}'s actions, |
| 209 | | - keywords to describe {{user}}'s physical appearance and actions. |
| 210 | | - |
| 211 | | - If character actions involve direct physical interaction with another character, mention specifically which body parts interacting and how. |
| 212 | | - |
| 213 | | - A correctly formatted example response would be: |
| 214 | | - '(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)'`, |
| 215 | | - |
| 216 | | - [generationMode.RAW_LAST]: 'Ignore previous instructions and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.', |
| 217 | | - [generationMode.BACKGROUND]: 'Ignore previous instructions and provide a detailed description of {{char}}\'s surroundings in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: location, time of day, weather, lighting, and any other relevant details. Do not include descriptions of characters and non-visual qualities such as names, personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'background,\'. Ignore the rest of the story when crafting this description. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.', |
| 218 | | - [generationMode.FACE_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "close-up portrait".', |
| 219 | | - [generationMode.CHARACTER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".', |
| 220 | | - [generationMode.USER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".', |
| 221 | | - [generationMode.FREE_EXTENDED]: 'Ignore previous instructions and provide an exhaustive comma-separated list of tags describing the appearance of "{0}" in great detail. Start with {{charPrefix}} (sic) if the subject is associated with {{char}}.', |
| 222 | | -}; |
| 223 | | - |
| 224 | | -const defaultPrefix = 'best quality, absurdres, aesthetic,'; |
| 225 | | -const defaultNegative = 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry'; |
| 226 | | - |
| 227 | | -const defaultStyles = [ |
| 228 | | - { |
| 229 | | - name: 'Default', |
| 230 | | - negative: defaultNegative, |
| 231 | | - prefix: defaultPrefix, |
| 232 | | - }, |
| 233 | | -]; |
| 234 | | - |
| 235 | | -const placeholderVae = 'Automatic'; |
| 236 | | - |
| 237 | | -const defaultSettings = { |
| 238 | | - source: sources.extras, |
| 239 | | - |
| 240 | | - // CFG Scale |
| 241 | | - scale_min: 1, |
| 242 | | - scale_max: 30, |
| 243 | | - scale_step: 0.1, |
| 244 | | - scale: 7, |
| 245 | | - |
| 246 | | - // Sampler steps |
| 247 | | - steps_min: 1, |
| 248 | | - steps_max: 150, |
| 249 | | - steps_step: 1, |
| 250 | | - steps: 20, |
| 251 | | - |
| 252 | | - // Scheduler |
| 253 | | - scheduler: 'normal', |
| 254 | | - |
| 255 | | - // Image dimensions (Width & Height) |
| 256 | | - dimension_min: 64, |
| 257 | | - dimension_max: 2048, |
| 258 | | - dimension_step: 64, |
| 259 | | - width: 512, |
| 260 | | - height: 512, |
| 261 | | - |
| 262 | | - prompt_prefix: defaultPrefix, |
| 263 | | - negative_prompt: defaultNegative, |
| 264 | | - sampler: 'DDIM', |
| 265 | | - model: '', |
| 266 | | - vae: '', |
| 267 | | - seed: -1, |
| 268 | | - |
| 269 | | - // Automatic1111/Horde exclusives |
| 270 | | - restore_faces: false, |
| 271 | | - enable_hr: false, |
| 272 | | - adetailer_face: false, |
| 273 | | - |
| 274 | | - // Horde settings |
| 275 | | - horde: false, |
| 276 | | - horde_nsfw: false, |
| 277 | | - horde_karras: true, |
| 278 | | - horde_sanitize: true, |
| 279 | | - |
| 280 | | - // Refine mode |
| 281 | | - refine_mode: false, |
| 282 | | - interactive_mode: false, |
| 283 | | - multimodal_captioning: false, |
| 284 | | - snap: false, |
| 285 | | - free_extend: false, |
| 286 | | - function_tool: false, |
| 287 | | - minimal_prompt_processing: false, |
| 288 | | - |
| 289 | | - prompts: promptTemplates, |
| 290 | | - |
| 291 | | - // AUTOMATIC1111 settings |
| 292 | | - auto_url: 'http://localhost:7860', |
| 293 | | - auto_auth: '', |
| 294 | | - |
| 295 | | - // stable-diffusion.cpp settings |
| 296 | | - sdcpp_url: 'http://127.0.0.1:1234', |
| 297 | | - |
| 298 | | - vlad_url: 'http://localhost:7860', |
| 299 | | - vlad_auth: '', |
| 300 | | - |
| 301 | | - drawthings_url: 'http://localhost:7860', |
| 302 | | - drawthings_auth: '', |
| 303 | | - |
| 304 | | - hr_upscaler: 'Latent', |
| 305 | | - hr_scale: 1.0, |
| 306 | | - hr_scale_min: 1.0, |
| 307 | | - hr_scale_max: 4.0, |
| 308 | | - hr_scale_step: 0.1, |
| 309 | | - denoising_strength: 0.7, |
| 310 | | - denoising_strength_min: 0.0, |
| 311 | | - denoising_strength_max: 1.0, |
| 312 | | - denoising_strength_step: 0.01, |
| 313 | | - hr_second_pass_steps: 0, |
| 314 | | - hr_second_pass_steps_min: 0, |
| 315 | | - hr_second_pass_steps_max: 150, |
| 316 | | - hr_second_pass_steps_step: 1, |
| 317 | | - |
| 318 | | - // CLIP skip |
| 319 | | - clip_skip_min: 1, |
| 320 | | - clip_skip_max: 12, |
| 321 | | - clip_skip_step: 1, |
| 322 | | - clip_skip: 1, |
| 323 | | - |
| 324 | | - // NovelAI settings |
| 325 | | - novel_anlas_guard: false, |
| 326 | | - novel_sm: false, |
| 327 | | - novel_sm_dyn: false, |
| 328 | | - novel_decrisper: false, |
| 329 | | - novel_variety_boost: false, |
| 330 | | - |
| 331 | | - // OpenAI settings |
| 332 | | - openai_style: 'vivid', |
| 333 | | - openai_quality: 'standard', |
| 334 | | - openai_quality_gpt: 'auto', |
| 335 | | - openai_duration: '8', |
| 336 | | - |
| 337 | | - style: 'Default', |
| 338 | | - styles: defaultStyles, |
| 339 | | - |
| 340 | | - // ComyUI settings |
| 341 | | - comfy_type: 'standard', |
| 342 | | - |
| 343 | | - comfy_url: 'http://127.0.0.1:8188', |
| 344 | | - comfy_workflow: 'Default_Comfy_Workflow.json', |
| 345 | | - |
| 346 | | - comfy_runpod_url: '', |
| 347 | | - |
| 348 | | - // Pollinations settings |
| 349 | | - pollinations_enhance: false, |
| 350 | | - |
| 351 | | - // Visibility toggles |
| 352 | | - wand_visible: false, |
| 353 | | - command_visible: false, |
| 354 | | - interactive_visible: false, |
| 355 | | - tool_visible: false, |
| 356 | | - |
| 357 | | - // Stability AI settings |
| 358 | | - stability_style_preset: 'anime', |
| 359 | | - |
| 360 | | - // BFL API settings |
| 361 | | - bfl_upsampling: false, |
| 362 | | - |
| 363 | | - // Google settings |
| 364 | | - google_api: 'makersuite', |
| 365 | | - google_enhance: true, |
| 366 | | - google_duration: 6, |
| 367 | | - |
| 368 | | - // Settings presets & auto-fallback ({ name, preset } entries, tried in order) |
| 369 | | - settings_preset_chain: [], |
| 370 | | - settings_fallback_enabled: false, |
| 371 | | - |
| 372 | | - // Reference image library ({ tag, description, path } entries) |
| 373 | | - ref_images_enabled: false, |
| 374 | | - ref_images: [], |
| 375 | | - |
| 376 | | - // RunPod lazy-pod proxy base URL ('' = feature off) |
| 377 | | - runpod_lazy_url: '', |
| 378 | | - |
| 379 | | - // RunPod model catalog ({ name, value, kind: 'model'|'lora', downloads } entries) |
| 380 | | - runpod_models: [], |
| 381 | | - |
| 382 | | - // Selected LoRA for comfy workflows using the %lora% placeholder |
| 383 | | - lora: '', |
| 384 | | - lora_strength: 1.0, |
| 385 | | - lora_strength_min: 0.0, |
| 386 | | - lora_strength_max: 2.0, |
| 387 | | - lora_strength_step: 0.01, |
| 388 | | - |
| 389 | | - // Per-workflow remembered selections ({ [workflow]: { model, lora } }); |
| 390 | | - // part of preset snapshots, so each chain provider keeps its own map. |
| 391 | | - comfy_workflow_prefs: {}, |
| 392 | | - |
| 393 | | - // Dedicated LLM connection profile for image-prompt generation ('' = use active model) |
| 394 | | - prompt_generation_profile: '', |
| 395 | | - |
| 396 | | - // User-defined custom wand dropdown entries ({ id, title, prompt }) |
| 397 | | - custom_entries: [], |
| 398 | | -}; |
| 399 | | - |
| 400 | | -/** |
| 401 | | - * Keys that are NOT part of a settings preset snapshot. |
| 402 | | - * A preset captures backend/connection params (source, model, sampler, dimensions, etc.) |
| 403 | | - * but not the prompt library, styles, custom entries, or UI prefs. |
| 404 | | - * @type {string[]} |
| 405 | | - */ |
| 406 | | -const PRESET_EXCLUDE_KEYS = [ |
| 407 | | - 'settings_preset_chain', |
| 408 | | - 'settings_fallback_enabled', |
| 409 | | - // The reference image library is global, not a per-backend setting. |
| 410 | | - 'ref_images_enabled', |
| 411 | | - 'ref_images', |
| 412 | | - 'runpod_lazy_url', |
| 413 | | - 'runpod_models', |
| 414 | | - // The image-prompt LLM profile is independent of the image backend, so it must |
| 415 | | - // never be captured/swapped by image-generation presets or the fallback retry. |
| 416 | | - 'prompt_generation_profile', |
| 417 | | - 'prompts', |
| 418 | | - 'character_prompts', |
| 419 | | - 'character_negative_prompts', |
| 420 | | - 'styles', |
| 421 | | - 'custom_entries', |
| 422 | | - 'interactive_visible', |
| 423 | | - 'wand_visible', |
| 424 | | - 'command_visible', |
| 425 | | - 'tool_visible', |
| 426 | | - 'expand', |
| 427 | | -]; |
| 428 | | - |
| 429 | | -/** |
| 430 | | - * Creates a deep clone snapshot of the settings keys that belong to a preset. |
| 431 | | - * @returns {object} Snapshot of the included settings keys. |
| 432 | | - */ |
| 433 | | -function snapshotSdSettings() { |
| 434 | | - const snapshot = {}; |
| 435 | | - for (const key of Object.keys(extension_settings.sd)) { |
| 436 | | - if (PRESET_EXCLUDE_KEYS.includes(key)) { |
| 437 | | - continue; |
| 438 | | - } |
| 439 | | - snapshot[key] = structuredClone(extension_settings.sd[key]); |
| 440 | | - } |
| 441 | | - return snapshot; |
| 442 | | -} |
| 443 | | - |
| 444 | | -/** |
| 445 | | - * Applies a settings snapshot to the live settings object (in-memory only; does NOT touch the DOM). |
| 446 | | - * @param {object} snapshot Snapshot previously created by snapshotSdSettings(). |
| 447 | | - */ |
| 448 | | -function applySdSettingsSnapshot(snapshot) { |
| 449 | | - if (!snapshot || typeof snapshot !== 'object') { |
| 450 | | - return; |
| 451 | | - } |
| 452 | | - for (const key of Object.keys(snapshot)) { |
| 453 | | - // Skip excluded keys so older presets that were saved before a key was |
| 454 | | - // excluded (e.g. prompt_generation_profile) can't clobber it on load. |
| 455 | | - if (PRESET_EXCLUDE_KEYS.includes(key)) { |
| 456 | | - continue; |
| 457 | | - } |
| 458 | | - extension_settings.sd[key] = structuredClone(snapshot[key]); |
| 459 | | - } |
| 460 | | -} |
| 461 | | - |
| 462 | | -/** |
| 463 | | - * Checks whether a preset is configured (a non-null object with at least one key). |
| 464 | | - * @param {object} preset Preset to check. |
| 465 | | - * @returns {boolean} True if the preset is configured. |
| 466 | | - */ |
| 467 | | -function isPresetConfigured(preset) { |
| 468 | | - return !!preset && typeof preset === 'object' && Object.keys(preset).length > 0; |
| 469 | | -} |
| 470 | | - |
| 471 | | -/** |
| 472 | | - * Returns the fallback chain entries that hold a usable preset snapshot, in order. |
| 473 | | - * @returns {{name: string, preset: object}[]} Ordered list of configured chain entries. |
| 474 | | - */ |
| 475 | | -function getConfiguredPresetChain() { |
| 476 | | - const chain = Array.isArray(extension_settings.sd.settings_preset_chain) ? extension_settings.sd.settings_preset_chain : []; |
| 477 | | - return chain.filter(entry => entry && isPresetConfigured(entry.preset)); |
| 478 | | -} |
| 479 | | - |
| 480 | | -/** |
| 481 | | - * How long to wait for a locally-hosted backend's status endpoint before treating |
| 482 | | - * the server as down and moving on to the next entry in the fallback chain. |
| 483 | | - */ |
| 484 | | -const SOURCE_PROBE_TIMEOUT_MS = 1500; |
| 485 | | - |
| 486 | | -/** |
| 487 | | - * Quickly checks whether the currently configured source is up. Only locally-hosted |
| 488 | | - * backends with a status endpoint are probed (ComfyUI, A1111, SD.Next, DrawThings, |
| 489 | | - * stable-diffusion.cpp); sources without a probe are assumed reachable. |
| 490 | | - * @returns {Promise<boolean>} False when the backend has a probe and it failed. |
| 491 | | - */ |
| 492 | | -async function isCurrentSourceReachable() { |
| 493 | | - /** |
| 494 | | - * @param {string} endpoint ST server ping route for the backend. |
| 495 | | - * @param {object} body Request body identifying the backend server. |
| 496 | | - * @returns {Promise<boolean>} Whether the ping succeeded within the timeout. |
| 497 | | - */ |
| 498 | | - const probe = async (endpoint, body) => { |
| 499 | | - try { |
| 500 | | - const result = await fetch(endpoint, { |
| 501 | | - method: 'POST', |
| 502 | | - headers: getRequestHeaders(), |
| 503 | | - signal: AbortSignal.timeout(SOURCE_PROBE_TIMEOUT_MS), |
| 504 | | - body: JSON.stringify(body), |
| 505 | | - }); |
| 506 | | - return result.ok; |
| 507 | | - } catch { |
| 508 | | - return false; |
| 509 | | - } |
| 510 | | - }; |
| 511 | | - |
| 512 | | - switch (extension_settings.sd.source) { |
| 513 | | - case sources.comfy: |
| 514 | | - if (extension_settings.sd.comfy_type !== comfyTypes.standard) { |
| 515 | | - return true; |
| 516 | | - } |
| 517 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 518 | | - return isRunpodReady(SOURCE_PROBE_TIMEOUT_MS); |
| 519 | | - } |
| 520 | | - return probe('/api/sd/comfy/ping', { url: extension_settings.sd.comfy_url }); |
| 521 | | - case sources.auto: |
| 522 | | - case sources.vlad: |
| 523 | | - case sources.drawthings: |
| 524 | | - return probe(extension_settings.sd.source === sources.drawthings ? '/api/sd/drawthings/ping' : '/api/sd/ping', getSdRequestBody()); |
| 525 | | - case sources.sdcpp: |
| 526 | | - return probe('/api/sd/sdcpp/ping', { url: extension_settings.sd.sdcpp_url }); |
| 527 | | - default: |
| 528 | | - return true; |
| 529 | | - } |
| 530 | | -} |
| 531 | | - |
| 532 | | -/** |
| 533 | | - * Refreshes all settings UI controls to reflect the current extension_settings.sd values. |
| 534 | | - * Used after loading a settings preset. |
| 535 | | - * @returns {Promise<void>} |
| 536 | | - */ |
| 537 | | -async function refreshSettingsUi() { |
| 538 | | - // loadSettings() appends to #sd_style without clearing it, so empty it first. |
| 539 | | - $('#sd_style').empty(); |
| 540 | | - await loadSettings(); |
| 541 | | -} |
| 542 | | - |
| 543 | | -const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| 544 | | -const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase()); |
| 545 | | - |
| 546 | | -/** |
| 547 | | - * Generate interceptor for interactive mode triggers. |
| 548 | | - * @param {any[]} chat Chat messages |
| 549 | | - * @param {number} _ Context size (unused) |
| 550 | | - * @param {function(boolean): void} abort Abort generation function |
| 551 | | - * @param {string} type Type of the generation |
| 552 | | - */ |
| 553 | | -function processTriggers(chat, _, abort, type) { |
| 554 | | - if (type === 'quiet') { |
| 555 | | - return; |
| 556 | | - } |
| 557 | | - |
| 558 | | - if (extension_settings.sd.function_tool && ToolManager.isToolCallingSupported()) { |
| 559 | | - return; |
| 560 | | - } |
| 561 | | - |
| 562 | | - if (!extension_settings.sd.interactive_mode) { |
| 563 | | - return; |
| 564 | | - } |
| 565 | | - |
| 566 | | - const lastMessage = chat[chat.length - 1]; |
| 567 | | - |
| 568 | | - if (!lastMessage) { |
| 569 | | - return; |
| 570 | | - } |
| 571 | | - |
| 572 | | - const message = lastMessage.mes; |
| 573 | | - const isUser = lastMessage.is_user; |
| 574 | | - |
| 575 | | - if (!message || !isUser) { |
| 576 | | - return; |
| 577 | | - } |
| 578 | | - |
| 579 | | - const messageLower = message.toLowerCase(); |
| 580 | | - |
| 581 | | - try { |
| 582 | | - const activationRegex = new RegExp(messageTrigger.activationRegex, 'i'); |
| 583 | | - const activationMatch = messageLower.match(activationRegex); |
| 584 | | - |
| 585 | | - if (!activationMatch) { |
| 586 | | - return; |
| 587 | | - } |
| 588 | | - |
| 589 | | - let subject = activationMatch[3].trim(); |
| 590 | | - |
| 591 | | - if (!subject) { |
| 592 | | - return; |
| 593 | | - } |
| 594 | | - |
| 595 | | - console.log(`SD: Triggered by "${message}", detected subject: "${subject}"`); |
| 596 | | - |
| 597 | | - outer: for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) { |
| 598 | | - for (const trigger of triggers) { |
| 599 | | - if (subject === trigger) { |
| 600 | | - subject = triggerWords[specialMode][0]; |
| 601 | | - console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`); |
| 602 | | - break outer; |
| 603 | | - } |
| 604 | | - } |
| 605 | | - } |
| 606 | | - |
| 607 | | - abort(true); |
| 608 | | - setTimeout(() => generatePicture(initiators.interactive, {}, subject, message), 1); |
| 609 | | - } catch { |
| 610 | | - console.log('SD: Failed to process triggers.'); |
| 611 | | - } |
| 612 | | -} |
| 613 | | - |
| 614 | | -globalThis.SD_ProcessTriggers = processTriggers; |
| 615 | | - |
| 616 | | -function getSdRequestBody() { |
| 617 | | - switch (extension_settings.sd.source) { |
| 618 | | - case sources.vlad: |
| 619 | | - return { url: extension_settings.sd.vlad_url, auth: extension_settings.sd.vlad_auth }; |
| 620 | | - case sources.auto: |
| 621 | | - return { url: extension_settings.sd.auto_url, auth: extension_settings.sd.auto_auth }; |
| 622 | | - case sources.drawthings: |
| 623 | | - return { url: extension_settings.sd.drawthings_url, auth: extension_settings.sd.drawthings_auth }; |
| 624 | | - default: |
| 625 | | - throw new Error('Invalid SD source.'); |
| 626 | | - } |
| 627 | | -} |
| 628 | | - |
| 629 | | -function toggleSourceControls() { |
| 630 | | - $('.sd_settings [data-sd-source]').each(function () { |
| 631 | | - const source = $(this).data('sd-source').split(','); |
| 632 | | - $(this).toggle(source.includes(extension_settings.sd.source)); |
| 633 | | - }); |
| 634 | | - $('.sd_settings [data-sd-comfy-type]').each(function () { |
| 635 | | - const source = $(this).data('sd-comfy-type').split(','); |
| 636 | | - $(this).toggle(source.includes(extension_settings.sd.comfy_type)); |
| 637 | | - }); |
| 638 | | -} |
| 639 | | - |
| 640 | | -let promptGenerationProfileDropdownInitialized = false; |
| 641 | | -// Warn at most once per session if a prompt-gen profile is selected but cannot be |
| 642 | | -// applied because there is no active connection profile to restore afterward. |
| 643 | | -let promptProfileWarnedNoBaseProfile = false; |
| 644 | | - |
| 645 | | -/** |
| 646 | | - * Populates the dedicated prompt-generation connection profile dropdown. |
| 647 | | - * Only initializes once to avoid attaching duplicate Connection Manager event listeners |
| 648 | | - * when loadSettings() is called again (e.g. after loading a settings preset). |
| 649 | | - */ |
| 650 | | -function initPromptGenerationProfileDropdown() { |
| 651 | | - if (promptGenerationProfileDropdownInitialized) { |
| 652 | | - return; |
| 653 | | - } |
| 654 | | - |
| 655 | | - try { |
| 656 | | - ConnectionManagerRequestService.handleDropdown( |
| 657 | | - '#sd_prompt_generation_profile', |
| 658 | | - extension_settings.sd.prompt_generation_profile, |
| 659 | | - (profile) => { |
| 660 | | - extension_settings.sd.prompt_generation_profile = profile?.id ?? ''; |
| 661 | | - saveSettingsDebounced(); |
| 662 | | - }, |
| 663 | | - ); |
| 664 | | - promptGenerationProfileDropdownInitialized = true; |
| 665 | | - } catch (error) { |
| 666 | | - // Connection Manager may be unavailable/disabled; leave the dropdown empty in that case. |
| 667 | | - console.warn('SD: could not populate prompt-generation profile dropdown', error); |
| 668 | | - } |
| 669 | | -} |
| 670 | | - |
| 671 | | -async function loadSettings() { |
| 672 | | - // Initialize settings |
| 673 | | - if (Object.keys(extension_settings.sd).length === 0) { |
| 674 | | - Object.assign(extension_settings.sd, defaultSettings); |
| 675 | | - } |
| 676 | | - |
| 677 | | - // Insert missing settings |
| 678 | | - for (const [key, value] of Object.entries(defaultSettings)) { |
| 679 | | - if (extension_settings.sd[key] === undefined) { |
| 680 | | - extension_settings.sd[key] = value; |
| 681 | | - } |
| 682 | | - } |
| 683 | | - |
| 684 | | - if (extension_settings.sd.prompts === undefined) { |
| 685 | | - extension_settings.sd.prompts = promptTemplates; |
| 686 | | - } |
| 687 | | - |
| 688 | | - // Insert missing templates |
| 689 | | - for (const [key, value] of Object.entries(promptTemplates)) { |
| 690 | | - if (extension_settings.sd.prompts[key] === undefined) { |
| 691 | | - extension_settings.sd.prompts[key] = value; |
| 692 | | - } |
| 693 | | - } |
| 694 | | - |
| 695 | | - if (extension_settings.sd.character_prompts === undefined) { |
| 696 | | - extension_settings.sd.character_prompts = {}; |
| 697 | | - } |
| 698 | | - |
| 699 | | - if (extension_settings.sd.character_negative_prompts === undefined) { |
| 700 | | - extension_settings.sd.character_negative_prompts = {}; |
| 701 | | - } |
| 702 | | - |
| 703 | | - if (!Array.isArray(extension_settings.sd.styles)) { |
| 704 | | - extension_settings.sd.styles = defaultStyles; |
| 705 | | - } |
| 706 | | - |
| 707 | | - // Settings presets & auto-fallback |
| 708 | | - if (!Array.isArray(extension_settings.sd.settings_preset_chain)) { |
| 709 | | - // Migrate the old two-slot primary/secondary presets into a chain. |
| 710 | | - const chain = []; |
| 711 | | - if (isPresetConfigured(extension_settings.sd.settings_preset_primary)) { |
| 712 | | - chain.push({ name: 'Primary', preset: extension_settings.sd.settings_preset_primary }); |
| 713 | | - } |
| 714 | | - if (isPresetConfigured(extension_settings.sd.settings_preset_secondary)) { |
| 715 | | - chain.push({ name: 'Secondary', preset: extension_settings.sd.settings_preset_secondary }); |
| 716 | | - } |
| 717 | | - extension_settings.sd.settings_preset_chain = chain; |
| 718 | | - delete extension_settings.sd.settings_preset_primary; |
| 719 | | - delete extension_settings.sd.settings_preset_secondary; |
| 720 | | - } |
| 721 | | - |
| 722 | | - if (extension_settings.sd.settings_fallback_enabled === undefined) { |
| 723 | | - extension_settings.sd.settings_fallback_enabled = false; |
| 724 | | - } |
| 725 | | - |
| 726 | | - // Reference image library |
| 727 | | - if (extension_settings.sd.ref_images_enabled === undefined) { |
| 728 | | - extension_settings.sd.ref_images_enabled = false; |
| 729 | | - } |
| 730 | | - |
| 731 | | - if (!Array.isArray(extension_settings.sd.ref_images)) { |
| 732 | | - extension_settings.sd.ref_images = []; |
| 733 | | - } |
| 734 | | - |
| 735 | | - if (!Array.isArray(extension_settings.sd.runpod_models)) { |
| 736 | | - extension_settings.sd.runpod_models = []; |
| 737 | | - } |
| 738 | | - |
| 739 | | - if (extension_settings.sd.lora === undefined) { |
| 740 | | - extension_settings.sd.lora = ''; |
| 741 | | - } |
| 742 | | - |
| 743 | | - if (typeof extension_settings.sd.lora_strength !== 'number') { |
| 744 | | - extension_settings.sd.lora_strength = 1.0; |
| 745 | | - } |
| 746 | | - |
| 747 | | - // The flux workflows moved from a hardcoded flux2 VAE to %vae%; a leftover |
| 748 | | - // flux1 selection ('ae.safetensors') would fail pod-side validation. |
| 749 | | - const migrateVae = (config) => { |
| 750 | | - if (config && config.vae === 'ae.safetensors') { |
| 751 | | - config.vae = 'flux2-vae.safetensors'; |
| 752 | | - } |
| 753 | | - }; |
| 754 | | - migrateVae(extension_settings.sd); |
| 755 | | - (extension_settings.sd.settings_preset_chain ?? []).forEach(entry => migrateVae(entry?.preset)); |
| 756 | | - |
| 757 | | - if (typeof extension_settings.sd.comfy_workflow_prefs !== 'object' || !extension_settings.sd.comfy_workflow_prefs) { |
| 758 | | - extension_settings.sd.comfy_workflow_prefs = {}; |
| 759 | | - } |
| 760 | | - |
| 761 | | - if (!Array.isArray(extension_settings.sd.custom_entries)) { |
| 762 | | - extension_settings.sd.custom_entries = []; |
| 763 | | - } |
| 764 | | - |
| 765 | | - // Preserve an original seed if exists |
| 766 | | - if (extension_settings.sd.original_seed >= 0) { |
| 767 | | - extension_settings.sd.seed = extension_settings.sd.original_seed; |
| 768 | | - delete extension_settings.sd.original_seed; |
| 769 | | - } |
| 770 | | - |
| 771 | | - $('#sd_source').val(extension_settings.sd.source); |
| 772 | | - $('#sd_scale').val(extension_settings.sd.scale).trigger('input'); |
| 773 | | - $('#sd_steps').val(extension_settings.sd.steps).trigger('input'); |
| 774 | | - $('#sd_prompt_prefix').val(extension_settings.sd.prompt_prefix).trigger('input'); |
| 775 | | - $('#sd_negative_prompt').val(extension_settings.sd.negative_prompt).trigger('input'); |
| 776 | | - $('#sd_width').val(extension_settings.sd.width).trigger('input'); |
| 777 | | - $('#sd_height').val(extension_settings.sd.height).trigger('input'); |
| 778 | | - $('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input'); |
| 779 | | - $('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input'); |
| 780 | | - $('#sd_lora_strength').val(extension_settings.sd.lora_strength ?? 1.0).trigger('input'); |
| 781 | | - $('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input'); |
| 782 | | - $('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard); |
| 783 | | - $('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm); |
| 784 | | - $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn); |
| 785 | | - $('#sd_novel_sm_dyn').prop('disabled', !extension_settings.sd.novel_sm); |
| 786 | | - $('#sd_novel_decrisper').prop('checked', extension_settings.sd.novel_decrisper); |
| 787 | | - $('#sd_novel_variety_boost').prop('checked', extension_settings.sd.novel_variety_boost); |
| 788 | | - $('#sd_pollinations_enhance').prop('checked', extension_settings.sd.pollinations_enhance); |
| 789 | | - $('#sd_horde').prop('checked', extension_settings.sd.horde); |
| 790 | | - $('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw); |
| 791 | | - $('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras); |
| 792 | | - $('#sd_horde_sanitize').prop('checked', extension_settings.sd.horde_sanitize); |
| 793 | | - $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces); |
| 794 | | - $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr); |
| 795 | | - $('#sd_adetailer_face').prop('checked', extension_settings.sd.adetailer_face); |
| 796 | | - $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode); |
| 797 | | - $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning); |
| 798 | | - $('#sd_auto_url').val(extension_settings.sd.auto_url); |
| 799 | | - $('#sd_auto_auth').val(extension_settings.sd.auto_auth); |
| 800 | | - $('#sd_sdcpp_url').val(extension_settings.sd.sdcpp_url); |
| 801 | | - $('#sd_vlad_url').val(extension_settings.sd.vlad_url); |
| 802 | | - $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth); |
| 803 | | - $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url); |
| 804 | | - $('#sd_drawthings_auth').val(extension_settings.sd.drawthings_auth); |
| 805 | | - $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode); |
| 806 | | - $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 807 | | - $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 808 | | - $('#sd_openai_quality_gpt').val(extension_settings.sd.openai_quality_gpt); |
| 809 | | - $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 810 | | - $('#sd_comfy_type').val(extension_settings.sd.comfy_type); |
| 811 | | - $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| 812 | | - $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); |
| 813 | | - $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url); |
| 814 | | - $('#sd_snap').prop('checked', extension_settings.sd.snap); |
| 815 | | - $('#sd_minimal_prompt_processing').prop('checked', extension_settings.sd.minimal_prompt_processing); |
| 816 | | - $('#sd_clip_skip').val(extension_settings.sd.clip_skip); |
| 817 | | - $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| 818 | | - $('#sd_seed').val(extension_settings.sd.seed); |
| 819 | | - $('#sd_free_extend').prop('checked', extension_settings.sd.free_extend); |
| 820 | | - $('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible); |
| 821 | | - $('#sd_command_visible').prop('checked', extension_settings.sd.command_visible); |
| 822 | | - $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible); |
| 823 | | - $('#sd_tool_visible').prop('checked', extension_settings.sd.tool_visible); |
| 824 | | - $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset); |
| 825 | | - $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id); |
| 826 | | - $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool); |
| 827 | | - $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling); |
| 828 | | - $('#sd_google_api').val(extension_settings.sd.google_api); |
| 829 | | - $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance); |
| 830 | | - $('#sd_google_duration').val(extension_settings.sd.google_duration); |
| 831 | | - $('#sd_fallback_enabled').prop('checked', extension_settings.sd.settings_fallback_enabled); |
| 832 | | - $('#sd_ref_images_enabled').prop('checked', extension_settings.sd.ref_images_enabled); |
| 833 | | - $('#sd_runpod_lazy_url').val(extension_settings.sd.runpod_lazy_url ?? ''); |
| 834 | | - renderPresetChain(); |
| 835 | | - renderRefImages(); |
| 836 | | - renderRunpodModels(); |
| 837 | | - setupRunpodLoops(); |
| 838 | | - |
| 839 | | - for (const style of extension_settings.sd.styles) { |
| 840 | | - const option = document.createElement('option'); |
| 841 | | - option.value = style.name; |
| 842 | | - option.text = style.name; |
| 843 | | - option.selected = style.name === extension_settings.sd.style; |
| 844 | | - $('#sd_style').append(option); |
| 845 | | - } |
| 846 | | - |
| 847 | | - const resolutionId = getClosestKnownResolution(); |
| 848 | | - $('#sd_resolution').val(resolutionId); |
| 849 | | - |
| 850 | | - initPromptGenerationProfileDropdown(); |
| 851 | | - |
| 852 | | - toggleSourceControls(); |
| 853 | | - addPromptTemplates(); |
| 854 | | - renderCustomEntriesList(); |
| 855 | | - renderCustomDropdownEntries(); |
| 856 | | - registerFunctionTool(); |
| 857 | | - |
| 858 | | - await loadSettingOptions(); |
| 859 | | -} |
| 860 | | - |
| 861 | | -/** |
| 862 | | - * Find a closest resolution option match for the current width and height. |
| 863 | | - */ |
| 864 | | -function getClosestKnownResolution() { |
| 865 | | - let resolutionId = null; |
| 866 | | - let minTotalDiff = Infinity; |
| 867 | | - |
| 868 | | - const targetAspect = extension_settings.sd.width / extension_settings.sd.height; |
| 869 | | - const targetResolution = extension_settings.sd.width * extension_settings.sd.height; |
| 870 | | - |
| 871 | | - const diffs = Object.entries(resolutionOptions).map(([id, resolution]) => { |
| 872 | | - const aspectDiff = Math.abs((resolution.width / resolution.height) - targetAspect) / targetAspect; |
| 873 | | - const resolutionDiff = Math.abs(resolution.width * resolution.height - targetResolution) / targetResolution; |
| 874 | | - return { id, totalDiff: aspectDiff + resolutionDiff }; |
| 875 | | - }); |
| 876 | | - |
| 877 | | - for (const { id, totalDiff } of diffs) { |
| 878 | | - if (totalDiff < minTotalDiff) { |
| 879 | | - minTotalDiff = totalDiff; |
| 880 | | - resolutionId = id; |
| 881 | | - } |
| 882 | | - } |
| 883 | | - |
| 884 | | - return resolutionId; |
| 885 | | -} |
| 886 | | - |
| 887 | | -async function loadSettingOptions() { |
| 888 | | - return Promise.all([ |
| 889 | | - loadSamplers(), |
| 890 | | - loadModels(), |
| 891 | | - loadSchedulers(), |
| 892 | | - loadVaes(), |
| 893 | | - loadLoras(), |
| 894 | | - loadComfyWorkflows(), |
| 895 | | - ]); |
| 896 | | -} |
| 897 | | - |
| 898 | | -async function loadLoras() { |
| 899 | | - $('#sd_lora').empty(); |
| 900 | | - let loras = ['N/A']; |
| 901 | | - if (extension_settings.sd.source === sources.comfy && extension_settings.sd.comfy_type === comfyTypes.standard) { |
| 902 | | - loras = await loadComfyLoras(); |
| 903 | | - } |
| 904 | | - for (const lora of loras) { |
| 905 | | - const option = document.createElement('option'); |
| 906 | | - option.innerText = lora?.text ?? lora; |
| 907 | | - option.value = lora?.value ?? lora; |
| 908 | | - option.selected = option.value === extension_settings.sd.lora; |
| 909 | | - $('#sd_lora').append(option); |
| 910 | | - } |
| 911 | | -} |
| 912 | | - |
| 913 | | -async function loadComfyLoras() { |
| 914 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 915 | | - return getRunpodCatalog().filter(m => m.kind === 'lora').map(m => ({ value: m.value, text: m.name || m.value })); |
| 916 | | - } |
| 917 | | - if (!extension_settings.sd.comfy_url) { |
| 918 | | - return []; |
| 919 | | - } |
| 920 | | - try { |
| 921 | | - const result = await fetch('/api/sd/comfy/loras', { |
| 922 | | - method: 'POST', |
| 923 | | - headers: getRequestHeaders(), |
| 924 | | - body: JSON.stringify({ url: extension_settings.sd.comfy_url }), |
| 925 | | - }); |
| 926 | | - if (!result.ok) { |
| 927 | | - throw new Error('ComfyUI returned an error.'); |
| 928 | | - } |
| 929 | | - return await result.json(); |
| 930 | | - } catch (error) { |
| 931 | | - return []; |
| 932 | | - } |
| 933 | | -} |
| 934 | | - |
| 935 | | -function addPromptTemplates() { |
| 936 | | - $('#sd_prompt_templates').empty(); |
| 937 | | - |
| 938 | | - for (const [name, prompt] of Object.entries(extension_settings.sd.prompts).sort((a, b) => Number(a[0]) - Number(b[0]))) { |
| 939 | | - const label = $('<label></label>') |
| 940 | | - .text(modeLabels[name]) |
| 941 | | - .attr('for', `sd_prompt_${name}`) |
| 942 | | - .attr('data-i18n', `sd_prompt_${name}`); |
| 943 | | - const textarea = $('<textarea></textarea>') |
| 944 | | - .addClass('textarea_compact text_pole') |
| 945 | | - .attr('id', `sd_prompt_${name}`) |
| 946 | | - .attr('rows', 3) |
| 947 | | - .val(prompt).on('input', () => { |
| 948 | | - extension_settings.sd.prompts[name] = textarea.val(); |
| 949 | | - saveSettingsDebounced(); |
| 950 | | - }); |
| 951 | | - const button = $('<button></button>') |
| 952 | | - .addClass('menu_button fa-solid fa-undo') |
| 953 | | - .attr('title', 'Restore default') |
| 954 | | - .attr('data-i18n', 'Restore default') |
| 955 | | - .on('click', () => { |
| 956 | | - textarea.val(promptTemplates[name]); |
| 957 | | - extension_settings.sd.prompts[name] = promptTemplates[name]; |
| 958 | | - if (String(name) === String(generationMode.TOOL)) { |
| 959 | | - registerFunctionTool(); |
| 960 | | - } |
| 961 | | - saveSettingsDebounced(); |
| 962 | | - }); |
| 963 | | - const container = $('<div></div>') |
| 964 | | - .addClass('title_restorable') |
| 965 | | - .append(label) |
| 966 | | - .append(button); |
| 967 | | - $('#sd_prompt_templates').append(container); |
| 968 | | - $('#sd_prompt_templates').append(textarea); |
| 969 | | - } |
| 970 | | -} |
| 971 | | - |
| 972 | | -function onInteractiveModeInput() { |
| 973 | | - extension_settings.sd.interactive_mode = !!$(this).prop('checked'); |
| 974 | | - saveSettingsDebounced(); |
| 975 | | -} |
| 976 | | - |
| 977 | | -function onMultimodalCaptioningInput() { |
| 978 | | - extension_settings.sd.multimodal_captioning = !!$(this).prop('checked'); |
| 979 | | - saveSettingsDebounced(); |
| 980 | | -} |
| 981 | | - |
| 982 | | -function onSnapInput() { |
| 983 | | - extension_settings.sd.snap = !!$(this).prop('checked'); |
| 984 | | - saveSettingsDebounced(); |
| 985 | | -} |
| 986 | | - |
| 987 | | -function onMinimalPromptProcessing() { |
| 988 | | - extension_settings.sd.minimal_prompt_processing = !!$(this).prop('checked'); |
| 989 | | - saveSettingsDebounced(); |
| 990 | | -} |
| 991 | | - |
| 992 | | -function onStyleSelect() { |
| 993 | | - const selectedStyle = String($('#sd_style').find(':selected').val()); |
| 994 | | - const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle); |
| 995 | | - |
| 996 | | - if (!styleObject) { |
| 997 | | - console.warn(`Could not find style object for ${selectedStyle}`); |
| 998 | | - return; |
| 999 | | - } |
| 1000 | | - |
| 1001 | | - $('#sd_prompt_prefix').val(styleObject.prefix).trigger('input'); |
| 1002 | | - $('#sd_negative_prompt').val(styleObject.negative).trigger('input'); |
| 1003 | | - extension_settings.sd.style = selectedStyle; |
| 1004 | | - saveSettingsDebounced(); |
| 1005 | | -} |
| 1006 | | - |
| 1007 | | -async function onDeleteStyleClick() { |
| 1008 | | - const selectedStyle = String($('#sd_style').find(':selected').val()); |
| 1009 | | - const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle); |
| 1010 | | - |
| 1011 | | - if (!styleObject) { |
| 1012 | | - return; |
| 1013 | | - } |
| 1014 | | - |
| 1015 | | - const confirmed = await callGenericPopup(t`Are you sure you want to delete the style "${selectedStyle}"?`, POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel' }); |
| 1016 | | - |
| 1017 | | - if (!confirmed) { |
| 1018 | | - return; |
| 1019 | | - } |
| 1020 | | - |
| 1021 | | - const index = extension_settings.sd.styles.indexOf(styleObject); |
| 1022 | | - |
| 1023 | | - if (index === -1) { |
| 1024 | | - return; |
| 1025 | | - } |
| 1026 | | - |
| 1027 | | - extension_settings.sd.styles.splice(index, 1); |
| 1028 | | - $('#sd_style').find(`option[value="${selectedStyle}"]`).remove(); |
| 1029 | | - |
| 1030 | | - if (extension_settings.sd.styles.length > 0) { |
| 1031 | | - extension_settings.sd.style = extension_settings.sd.styles[0].name; |
| 1032 | | - $('#sd_style').val(extension_settings.sd.style).trigger('change'); |
| 1033 | | - } else { |
| 1034 | | - extension_settings.sd.style = ''; |
| 1035 | | - $('#sd_prompt_prefix').val('').trigger('input'); |
| 1036 | | - $('#sd_negative_prompt').val('').trigger('input'); |
| 1037 | | - $('#sd_style').val(''); |
| 1038 | | - } |
| 1039 | | - |
| 1040 | | - saveSettingsDebounced(); |
| 1041 | | -} |
| 1042 | | - |
| 1043 | | -async function onSaveStyleClick() { |
| 1044 | | - const selectedStyle = extension_settings.sd.style || ''; |
| 1045 | | - const userInput = await callGenericPopup(t`Enter style name:`, POPUP_TYPE.INPUT, selectedStyle); |
| 1046 | | - |
| 1047 | | - if (!userInput) { |
| 1048 | | - return; |
| 1049 | | - } |
| 1050 | | - |
| 1051 | | - const name = String(userInput).trim(); |
| 1052 | | - const prefix = String($('#sd_prompt_prefix').val()); |
| 1053 | | - const negative = String($('#sd_negative_prompt').val()); |
| 1054 | | - |
| 1055 | | - const existingStyle = extension_settings.sd.styles.find(x => x.name === name); |
| 1056 | | - |
| 1057 | | - if (existingStyle) { |
| 1058 | | - existingStyle.prefix = prefix; |
| 1059 | | - existingStyle.negative = negative; |
| 1060 | | - $('#sd_style').val(name); |
| 1061 | | - saveSettingsDebounced(); |
| 1062 | | - return; |
| 1063 | | - } |
| 1064 | | - |
| 1065 | | - const styleObject = { |
| 1066 | | - name: name, |
| 1067 | | - prefix: prefix, |
| 1068 | | - negative: negative, |
| 1069 | | - }; |
| 1070 | | - |
| 1071 | | - extension_settings.sd.styles.push(styleObject); |
| 1072 | | - const option = document.createElement('option'); |
| 1073 | | - option.value = styleObject.name; |
| 1074 | | - option.text = styleObject.name; |
| 1075 | | - option.selected = true; |
| 1076 | | - $('#sd_style').append(option); |
| 1077 | | - $('#sd_style').val(styleObject.name); |
| 1078 | | - saveSettingsDebounced(); |
| 1079 | | -} |
| 1080 | | - |
| 1081 | | -async function onRenameStyleClick() { |
| 1082 | | - const selectedStyle = extension_settings.sd.style; |
| 1083 | | - const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle); |
| 1084 | | - |
| 1085 | | - if (!styleObject) { |
| 1086 | | - return; |
| 1087 | | - } |
| 1088 | | - |
| 1089 | | - const newName = await callGenericPopup(t`Enter new style name:`, POPUP_TYPE.INPUT, selectedStyle); |
| 1090 | | - |
| 1091 | | - if (!newName) { |
| 1092 | | - return; |
| 1093 | | - } |
| 1094 | | - |
| 1095 | | - const name = String(newName).trim(); |
| 1096 | | - |
| 1097 | | - if (name === selectedStyle) { |
| 1098 | | - return; |
| 1099 | | - } |
| 1100 | | - |
| 1101 | | - const existingStyle = extension_settings.sd.styles.find(x => x.name === name); |
| 1102 | | - |
| 1103 | | - if (existingStyle) { |
| 1104 | | - toastr.error(t`A style with that name already exists`); |
| 1105 | | - return; |
| 1106 | | - } |
| 1107 | | - |
| 1108 | | - styleObject.name = name; |
| 1109 | | - extension_settings.sd.style = name; |
| 1110 | | - |
| 1111 | | - $('#sd_style').empty(); |
| 1112 | | - for (const style of extension_settings.sd.styles) { |
| 1113 | | - const option = document.createElement('option'); |
| 1114 | | - option.value = style.name; |
| 1115 | | - option.text = style.name; |
| 1116 | | - option.selected = style.name === extension_settings.sd.style; |
| 1117 | | - $('#sd_style').append(option); |
| 1118 | | - } |
| 1119 | | - |
| 1120 | | - saveSettingsDebounced(); |
| 1121 | | -} |
| 1122 | | - |
| 1123 | | -/** |
| 1124 | | - * Rebuilds the provider fallback chain list in the settings UI. |
| 1125 | | - */ |
| 1126 | | -function renderPresetChain() { |
| 1127 | | - const container = $('#sd_preset_chain_list'); |
| 1128 | | - if (!container.length) { |
| 1129 | | - return; |
| 1130 | | - } |
| 1131 | | - |
| 1132 | | - container.empty(); |
| 1133 | | - |
| 1134 | | - const chain = Array.isArray(extension_settings.sd.settings_preset_chain) ? extension_settings.sd.settings_preset_chain : []; |
| 1135 | | - |
| 1136 | | - if (chain.length === 0) { |
| 1137 | | - const empty = $('<small></small>') |
| 1138 | | - .attr('data-i18n', 'No presets in the chain yet.') |
| 1139 | | - .text('No presets in the chain yet.'); |
| 1140 | | - container.append(empty); |
| 1141 | | - return; |
| 1142 | | - } |
| 1143 | | - |
| 1144 | | - chain.forEach((entry, index) => { |
| 1145 | | - const orderEl = $('<div></div>').addClass('sd_preset_chain_order').text(`${index + 1}.`); |
| 1146 | | - const sourceHint = String(entry.preset?.source ?? ''); |
| 1147 | | - const nameInput = $('<input>') |
| 1148 | | - .addClass('text_pole flex1') |
| 1149 | | - .attr('type', 'text') |
| 1150 | | - .attr('title', sourceHint ? `Source: ${sourceHint}` : '') |
| 1151 | | - .val(entry.name || '') |
| 1152 | | - .on('change', function () { |
| 1153 | | - entry.name = String($(this).val() ?? '').trim() || `Preset ${index + 1}`; |
| 1154 | | - saveSettingsDebounced(); |
| 1155 | | - }); |
| 1156 | | - |
| 1157 | | - const makeButton = (icon, title, handler) => $('<div></div>') |
| 1158 | | - .addClass(`menu_button menu_button_icon fa-solid ${icon}`) |
| 1159 | | - .attr('title', title) |
| 1160 | | - .attr('data-i18n', `[title]${title}`) |
| 1161 | | - .on('click', handler); |
| 1162 | | - |
| 1163 | | - const upButton = makeButton('fa-chevron-up', 'Move up', () => { |
| 1164 | | - if (index === 0) return; |
| 1165 | | - [chain[index - 1], chain[index]] = [chain[index], chain[index - 1]]; |
| 1166 | | - saveSettingsDebounced(); |
| 1167 | | - renderPresetChain(); |
| 1168 | | - }); |
| 1169 | | - const downButton = makeButton('fa-chevron-down', 'Move down', () => { |
| 1170 | | - if (index === chain.length - 1) return; |
| 1171 | | - [chain[index + 1], chain[index]] = [chain[index], chain[index + 1]]; |
| 1172 | | - saveSettingsDebounced(); |
| 1173 | | - renderPresetChain(); |
| 1174 | | - }); |
| 1175 | | - const loadButton = makeButton('fa-file-import', 'Load this preset into the current settings', async () => { |
| 1176 | | - applySdSettingsSnapshot(entry.preset); |
| 1177 | | - saveSettingsDebounced(); |
| 1178 | | - await refreshSettingsUi(); |
| 1179 | | - toastr.success(t`Settings preset loaded.`, t`Image Generation`); |
| 1180 | | - }); |
| 1181 | | - const saveButton = makeButton('fa-floppy-disk', 'Overwrite this preset with the current settings', () => { |
| 1182 | | - entry.preset = snapshotSdSettings(); |
| 1183 | | - saveSettingsDebounced(); |
| 1184 | | - renderPresetChain(); |
| 1185 | | - toastr.success(t`Settings preset updated.`, t`Image Generation`); |
| 1186 | | - }); |
| 1187 | | - const deleteButton = makeButton('fa-trash-can', 'Remove from the chain', () => { |
| 1188 | | - chain.splice(index, 1); |
| 1189 | | - saveSettingsDebounced(); |
| 1190 | | - renderPresetChain(); |
| 1191 | | - }); |
| 1192 | | - |
| 1193 | | - const row = $('<div></div>') |
| 1194 | | - .addClass('flex-container alignItemsCenter marginTopBot5') |
| 1195 | | - .append(orderEl) |
| 1196 | | - .append(nameInput) |
| 1197 | | - .append(upButton) |
| 1198 | | - .append(downButton) |
| 1199 | | - .append(loadButton) |
| 1200 | | - .append(saveButton) |
| 1201 | | - .append(deleteButton); |
| 1202 | | - |
| 1203 | | - container.append(row); |
| 1204 | | - }); |
| 1205 | | -} |
| 1206 | | - |
| 1207 | | -function onPresetChainAddClick() { |
| 1208 | | - const nameInput = $('#sd_preset_chain_name'); |
| 1209 | | - const name = String(nameInput.val() ?? '').trim(); |
| 1210 | | - const chain = extension_settings.sd.settings_preset_chain; |
| 1211 | | - chain.push({ name: name || `Preset ${chain.length + 1}`, preset: snapshotSdSettings() }); |
| 1212 | | - nameInput.val(''); |
| 1213 | | - saveSettingsDebounced(); |
| 1214 | | - renderPresetChain(); |
| 1215 | | - toastr.success(t`Current settings added to the fallback chain.`, t`Image Generation`); |
| 1216 | | -} |
| 1217 | | - |
| 1218 | | -function onFallbackEnabledChange() { |
| 1219 | | - extension_settings.sd.settings_fallback_enabled = !!$(this).prop('checked'); |
| 1220 | | - saveSettingsDebounced(); |
| 1221 | | -} |
| 1222 | | - |
| 1223 | | -// #region Reference image library |
| 1224 | | - |
| 1225 | | -/** |
| 1226 | | - * Matches the reference image placeholder in a raw ComfyUI workflow ("%reference_image%" or "%reference-image%"). |
| 1227 | | - */ |
| 1228 | | -const REFERENCE_IMAGE_PLACEHOLDER = /"%reference[-_]image%"/i; |
| 1229 | | - |
| 1230 | | -/** |
| 1231 | | - * Reference image chosen for the in-flight generation. Set at prompt-generation time |
| 1232 | | - * (or lazily by the workflow builder) and read when the ComfyUI workflow is assembled. |
| 1233 | | - * Deliberately kept across swipe regenerations so a swipe reuses the same reference. |
| 1234 | | - * @type {{tag: string, description: string, path: string} | null} |
| 1235 | | - */ |
| 1236 | | -let pendingReferenceImage = null; |
| 1237 | | - |
| 1238 | | -/** |
| 1239 | | - * Returns library entries that have an uploaded image. |
| 1240 | | - * @returns {{tag: string, description: string, path: string}[]} Valid reference images. |
| 1241 | | - */ |
| 1242 | | -function getValidRefImages() { |
| 1243 | | - const images = Array.isArray(extension_settings.sd.ref_images) ? extension_settings.sd.ref_images : []; |
| 1244 | | - return images.filter(image => image && typeof image.path === 'string' && image.path.length > 0); |
| 1245 | | -} |
| 1246 | | - |
| 1247 | | -/** |
| 1248 | | - * Rebuilds the reference image library list in the settings UI. |
| 1249 | | - */ |
| 1250 | | -function renderRefImages() { |
| 1251 | | - const container = $('#sd_ref_images_list'); |
| 1252 | | - if (!container.length) { |
| 1253 | | - return; |
| 1254 | | - } |
| 1255 | | - |
| 1256 | | - container.empty(); |
| 1257 | | - |
| 1258 | | - const images = Array.isArray(extension_settings.sd.ref_images) ? extension_settings.sd.ref_images : []; |
| 1259 | | - |
| 1260 | | - if (images.length === 0) { |
| 1261 | | - const empty = $('<small></small>') |
| 1262 | | - .attr('data-i18n', 'No reference images yet.') |
| 1263 | | - .text('No reference images yet.'); |
| 1264 | | - container.append(empty); |
| 1265 | | - return; |
| 1266 | | - } |
| 1267 | | - |
| 1268 | | - images.forEach((image, index) => { |
| 1269 | | - const thumb = $('<img>') |
| 1270 | | - .addClass('sd_ref_image_thumb') |
| 1271 | | - .attr('src', image.path) |
| 1272 | | - .attr('alt', image.tag || ''); |
| 1273 | | - const tagInput = $('<input>') |
| 1274 | | - .addClass('text_pole') |
| 1275 | | - .attr('type', 'text') |
| 1276 | | - .attr('placeholder', 'Tag') |
| 1277 | | - .attr('data-i18n', '[placeholder]Tag') |
| 1278 | | - .val(image.tag || '') |
| 1279 | | - .on('change', function () { |
| 1280 | | - image.tag = String($(this).val() ?? '').trim(); |
| 1281 | | - saveSettingsDebounced(); |
| 1282 | | - }); |
| 1283 | | - const descriptionInput = $('<input>') |
| 1284 | | - .addClass('text_pole flex1') |
| 1285 | | - .attr('type', 'text') |
| 1286 | | - .attr('placeholder', 'Description (used to pick the best fit)') |
| 1287 | | - .attr('data-i18n', '[placeholder]Description (used to pick the best fit)') |
| 1288 | | - .val(image.description || '') |
| 1289 | | - .on('change', function () { |
| 1290 | | - image.description = String($(this).val() ?? '').trim(); |
| 1291 | | - saveSettingsDebounced(); |
| 1292 | | - }); |
| 1293 | | - const deleteButton = $('<div></div>') |
| 1294 | | - .addClass('menu_button menu_button_icon fa-solid fa-trash-can') |
| 1295 | | - .attr('title', 'Remove reference image') |
| 1296 | | - .attr('data-i18n', '[title]Remove reference image') |
| 1297 | | - .on('click', () => { |
| 1298 | | - images.splice(index, 1); |
| 1299 | | - saveSettingsDebounced(); |
| 1300 | | - renderRefImages(); |
| 1301 | | - }); |
| 1302 | | - |
| 1303 | | - const row = $('<div></div>') |
| 1304 | | - .addClass('flex-container alignItemsCenter marginTopBot5') |
| 1305 | | - .append(thumb) |
| 1306 | | - .append(tagInput) |
| 1307 | | - .append(descriptionInput) |
| 1308 | | - .append(deleteButton); |
| 1309 | | - |
| 1310 | | - container.append(row); |
| 1311 | | - }); |
| 1312 | | -} |
| 1313 | | - |
| 1314 | | -function onRefImagesEnabledChange() { |
| 1315 | | - extension_settings.sd.ref_images_enabled = !!$(this).prop('checked'); |
| 1316 | | - saveSettingsDebounced(); |
| 1317 | | -} |
| 1318 | | - |
| 1319 | | -async function onRefImagesFileChange() { |
| 1320 | | - const files = Array.from(this.files ?? []); |
| 1321 | | - this.value = ''; |
| 1322 | | - |
| 1323 | | - for (const file of files) { |
| 1324 | | - try { |
| 1325 | | - const dataUrl = await getBase64Async(file); |
| 1326 | | - const base64 = String(dataUrl).split(',')[1]; |
| 1327 | | - const extension = (file.type.split('/')[1] || 'png').replace('jpeg', 'jpg'); |
| 1328 | | - const baseName = file.name.replace(/\.[^/.]+$/, ''); |
| 1329 | | - const path = await saveBase64AsFile(base64, 'reference-images', baseName, extension); |
| 1330 | | - extension_settings.sd.ref_images.push({ tag: baseName, description: '', path }); |
| 1331 | | - } catch (error) { |
| 1332 | | - console.error('SD: failed to add reference image', error); |
| 1333 | | - toastr.error(String(error), t`Image Generation`); |
| 1334 | | - } |
| 1335 | | - } |
| 1336 | | - |
| 1337 | | - saveSettingsDebounced(); |
| 1338 | | - renderRefImages(); |
| 1339 | | -} |
| 1340 | | - |
| 1341 | | -/** |
| 1342 | | - * Collects the ComfyUI workflow file names that could be used by this generation: |
| 1343 | | - * the live settings' workflow plus, when the fallback chain is enabled, the workflow |
| 1344 | | - * of every comfy preset in the chain. |
| 1345 | | - * @returns {string[]} Unique workflow file names. |
| 1346 | | - */ |
| 1347 | | -function collectCandidateComfyWorkflows() { |
| 1348 | | - const names = new Set(); |
| 1349 | | - /** @param {object} config A settings-shaped object (live settings or a preset snapshot). */ |
| 1350 | | - const consider = (config) => { |
| 1351 | | - if (config && config.source === sources.comfy && config.comfy_type === comfyTypes.standard && config.comfy_workflow) { |
| 1352 | | - names.add(config.comfy_workflow); |
| 1353 | | - } |
| 1354 | | - }; |
| 1355 | | - consider(extension_settings.sd); |
| 1356 | | - if (extension_settings.sd.settings_fallback_enabled) { |
| 1357 | | - for (const entry of getConfiguredPresetChain()) { |
| 1358 | | - consider(entry.preset); |
| 1359 | | - } |
| 1360 | | - } |
| 1361 | | - return [...names]; |
| 1362 | | -} |
| 1363 | | - |
| 1364 | | -/** |
| 1365 | | - * Checks whether any workflow this generation could run contains the reference image placeholder. |
| 1366 | | - * @returns {Promise<boolean>} True when a candidate workflow uses the placeholder. |
| 1367 | | - */ |
| 1368 | | -async function anyCandidateWorkflowUsesReferenceImage() { |
| 1369 | | - for (const fileName of collectCandidateComfyWorkflows()) { |
| 1370 | | - try { |
| 1371 | | - const result = await fetch('/api/sd/comfy/workflow', { |
| 1372 | | - method: 'POST', |
| 1373 | | - headers: getRequestHeaders(), |
| 1374 | | - body: JSON.stringify({ file_name: fileName }), |
| 1375 | | - }); |
| 1376 | | - if (!result.ok) { |
| 1377 | | - continue; |
| 1378 | | - } |
| 1379 | | - const workflow = await result.json(); |
| 1380 | | - if (REFERENCE_IMAGE_PLACEHOLDER.test(String(workflow))) { |
| 1381 | | - return true; |
| 1382 | | - } |
| 1383 | | - } catch (error) { |
| 1384 | | - console.warn('SD: could not inspect workflow for reference image placeholder', fileName, error); |
| 1385 | | - } |
| 1386 | | - } |
| 1387 | | - return false; |
| 1388 | | -} |
| 1389 | | - |
| 1390 | | -/** |
| 1391 | | - * Returns the reference images to choose from for this generation, or an empty array |
| 1392 | | - * when the feature is disabled, the library is empty, or no candidate workflow uses |
| 1393 | | - * the placeholder. |
| 1394 | | - * @returns {Promise<{tag: string, description: string, path: string}[]>} Selectable reference images. |
| 1395 | | - */ |
| 1396 | | -async function getEligibleReferenceImages() { |
| 1397 | | - if (!extension_settings.sd.ref_images_enabled) { |
| 1398 | | - return []; |
| 1399 | | - } |
| 1400 | | - const images = getValidRefImages(); |
| 1401 | | - if (images.length === 0) { |
| 1402 | | - return []; |
| 1403 | | - } |
| 1404 | | - if (!(await anyCandidateWorkflowUsesReferenceImage())) { |
| 1405 | | - return []; |
| 1406 | | - } |
| 1407 | | - return images; |
| 1408 | | -} |
| 1409 | | - |
| 1410 | | -/** |
| 1411 | | - * Builds the instruction appended to the image-prompt request that makes the LLM |
| 1412 | | - * also pick a reference image, as a machine-readable JSON line. |
| 1413 | | - * @param {{tag: string, description: string}[]} candidates Reference images to choose from. |
| 1414 | | - * @returns {string} Instruction text. |
| 1415 | | - */ |
| 1416 | | -function buildReferenceSelectionAddendum(candidates) { |
| 1417 | | - const list = candidates.map(x => `- "${x.tag}": ${x.description || 'no description'}`).join('\n'); |
| 1418 | | - return [ |
| 1419 | | - '', |
| 1420 | | - 'After the image prompt, append one final line containing exactly this JSON and nothing else:', |
| 1421 | | - '{"reference_image": "<tag>"}', |
| 1422 | | - 'where <tag> is the tag of the reference image whose description best fits the requested scene. Available reference images:', |
| 1423 | | - list, |
| 1424 | | - ].join('\n'); |
| 1425 | | -} |
| 1426 | | - |
| 1427 | | -/** |
| 1428 | | - * Finds the library entry whose tag matches the given text. |
| 1429 | | - * @param {string} text Tag text returned by the LLM. |
| 1430 | | - * @param {{tag: string}[]} candidates Reference images to match against. |
| 1431 | | - * @returns {object|null} The matching entry, or null. |
| 1432 | | - */ |
| 1433 | | -function matchReferenceTag(text, candidates) { |
| 1434 | | - const needle = String(text ?? '').trim().toLowerCase(); |
| 1435 | | - if (!needle) { |
| 1436 | | - return null; |
| 1437 | | - } |
| 1438 | | - const tagged = candidates.filter(x => String(x.tag ?? '').trim().length > 0); |
| 1439 | | - return tagged.find(x => x.tag.trim().toLowerCase() === needle) |
| 1440 | | - ?? tagged.find(x => needle.includes(x.tag.trim().toLowerCase())) |
| 1441 | | - ?? null; |
| 1442 | | -} |
| 1443 | | - |
| 1444 | | -/** |
| 1445 | | - * Extracts the {"reference_image": "..."} selection from a combined prompt+selection reply. |
| 1446 | | - * @param {string} reply Raw LLM reply. |
| 1447 | | - * @param {{tag: string}[]} candidates Reference images to match against. |
| 1448 | | - * @returns {{cleaned: string, selected: object|null}} Reply without the JSON line, and the matched entry. |
| 1449 | | - */ |
| 1450 | | -function extractReferenceSelection(reply, candidates) { |
| 1451 | | - const pattern = /\{\s*"?reference_image"?\s*:\s*"([^"]*)"\s*\}/gi; |
| 1452 | | - let match; |
| 1453 | | - let lastTag = null; |
| 1454 | | - while ((match = pattern.exec(reply)) !== null) { |
| 1455 | | - lastTag = match[1]; |
| 1456 | | - } |
| 1457 | | - const cleaned = reply.replace(/\{\s*"?reference_image"?\s*:\s*"[^"]*"\s*\}/gi, ' '); |
| 1458 | | - return { cleaned, selected: lastTag ? matchReferenceTag(lastTag, candidates) : null }; |
| 1459 | | -} |
| 1460 | | - |
| 1461 | | -/** |
| 1462 | | - * Asks the image-prompt LLM to pick the best-fitting reference image for a scene |
| 1463 | | - * in a dedicated (second) request. |
| 1464 | | - * @param {string} prompt The final image prompt describing the scene. |
| 1465 | | - * @param {{tag: string, description: string}[]} candidates Reference images to choose from. |
| 1466 | | - * @returns {Promise<object|null>} The matched entry, or null. |
| 1467 | | - */ |
| 1468 | | -async function selectReferenceImageWithLlm(prompt, candidates) { |
| 1469 | | - const list = candidates.map(x => `- "${x.tag}": ${x.description || 'no description'}`).join('\n'); |
| 1470 | | - const quietPrompt = [ |
| 1471 | | - 'Pause your roleplay. An image is being generated for the current scene from this prompt:', |
| 1472 | | - prompt, |
| 1473 | | - '', |
| 1474 | | - 'Pick the reference image whose description best fits that scene:', |
| 1475 | | - list, |
| 1476 | | - '', |
| 1477 | | - 'Reply with ONLY the tag of the chosen reference image and nothing else.', |
| 1478 | | - ].join('\n'); |
| 1479 | | - const profileId = extension_settings.sd.prompt_generation_profile; |
| 1480 | | - const reply = profileId |
| 1481 | | - ? await withConnectionProfile(profileId, () => generateQuietPrompt({ quietPrompt })) |
| 1482 | | - : await generateQuietPrompt({ quietPrompt }); |
| 1483 | | - return matchReferenceTag(String(reply ?? '').trim(), candidates); |
| 1484 | | -} |
| 1485 | | - |
| 1486 | | -/** |
| 1487 | | - * Resolves which reference image the current ComfyUI generation should use. |
| 1488 | | - * Prefers the selection made together with the image prompt; falls back to a |
| 1489 | | - * dedicated LLM call, and finally to the first library image. |
| 1490 | | - * @param {string} prompt The image prompt describing the scene. |
| 1491 | | - * @returns {Promise<object|null>} The reference image to use, or null when the feature is off/empty. |
| 1492 | | - */ |
| 1493 | | -async function resolveReferenceImageForGeneration(prompt) { |
| 1494 | | - if (!extension_settings.sd.ref_images_enabled) { |
| 1495 | | - return null; |
| 1496 | | - } |
| 1497 | | - const images = getValidRefImages(); |
| 1498 | | - if (images.length === 0) { |
| 1499 | | - return null; |
| 1500 | | - } |
| 1501 | | - if (pendingReferenceImage && images.some(x => x.path === pendingReferenceImage.path)) { |
| 1502 | | - return pendingReferenceImage; |
| 1503 | | - } |
| 1504 | | - if (images.length === 1) { |
| 1505 | | - pendingReferenceImage = images[0]; |
| 1506 | | - return pendingReferenceImage; |
| 1507 | | - } |
| 1508 | | - try { |
| 1509 | | - pendingReferenceImage = await selectReferenceImageWithLlm(prompt, images) ?? images[0]; |
| 1510 | | - } catch (error) { |
| 1511 | | - console.error('SD: reference image selection failed, using the first library image', error); |
| 1512 | | - pendingReferenceImage = images[0]; |
| 1513 | | - } |
| 1514 | | - return pendingReferenceImage; |
| 1515 | | -} |
| 1516 | | - |
| 1517 | | -/** |
| 1518 | | - * Loads a reference image and returns it as a raw base64 string (no data URL header). |
| 1519 | | - * @param {{path: string}} refImage Reference image entry. |
| 1520 | | - * @returns {Promise<string|null>} Base64 image data, or null on failure. |
| 1521 | | - */ |
| 1522 | | -async function fetchReferenceImageBase64(refImage) { |
| 1523 | | - try { |
| 1524 | | - const response = await fetch(refImage.path); |
| 1525 | | - if (!response.ok) { |
| 1526 | | - throw new Error(`HTTP ${response.status}`); |
| 1527 | | - } |
| 1528 | | - const blob = await response.blob(); |
| 1529 | | - const dataUrl = await getBase64Async(blob); |
| 1530 | | - return String(dataUrl).split(',')[1] ?? null; |
| 1531 | | - } catch (error) { |
| 1532 | | - console.error('SD: could not load reference image', refImage.path, error); |
| 1533 | | - return null; |
| 1534 | | - } |
| 1535 | | -} |
| 1536 | | - |
| 1537 | | -// #endregion |
| 1538 | | - |
| 1539 | | -// #region RunPod lazy pod |
| 1540 | | - |
| 1541 | | -/** Poll cadence for the pod status indicator (faster while it is starting). */ |
| 1542 | | -const RUNPOD_POLL_IDLE_MS = 30000; |
| 1543 | | -const RUNPOD_POLL_BUSY_MS = 5000; |
| 1544 | | -/** Frontend heartbeat cadence. The proxy tolerates browser timer pauses for five minutes. */ |
| 1545 | | -const RUNPOD_HEARTBEAT_MS = 20000; |
| 1546 | | - |
| 1547 | | -let runpodStatusTimer = null; |
| 1548 | | -let runpodHeartbeatTimer = null; |
| 1549 | | -let runpodLastPhase = 'red'; |
| 1550 | | - |
| 1551 | | -function getRunpodLazyUrl() { |
| 1552 | | - return String(extension_settings.sd.runpod_lazy_url ?? '').trim().replace(/\/$/, ''); |
| 1553 | | -} |
| 1554 | | - |
| 1555 | | -/** |
| 1556 | | - * Reads the lazy proxy status without starting or warming a pod. |
| 1557 | | - * @param {number} timeout Request timeout in milliseconds. |
| 1558 | | - * @returns {Promise<object>} Parsed /lazy/status response. |
| 1559 | | - */ |
| 1560 | | -async function getRunpodStatus(timeout = 8000) { |
| 1561 | | - const url = getRunpodLazyUrl(); |
| 1562 | | - if (!url) { |
| 1563 | | - throw new Error('RunPod lazy proxy URL is not configured.'); |
| 1564 | | - } |
| 1565 | | - const result = await fetch(`${url}/lazy/status`, { signal: AbortSignal.timeout(timeout) }); |
| 1566 | | - if (!result.ok) { |
| 1567 | | - throw new Error(`RunPod status returned ${result.status}.`); |
| 1568 | | - } |
| 1569 | | - return result.json(); |
| 1570 | | -} |
| 1571 | | - |
| 1572 | | -/** |
| 1573 | | - * Checks whether the lazy proxy already has a ready pod. |
| 1574 | | - * @param {number} timeout Request timeout in milliseconds. |
| 1575 | | - * @returns {Promise<boolean>} True only when /lazy/status reports green. |
| 1576 | | - */ |
| 1577 | | -async function isRunpodReady(timeout = 8000) { |
| 1578 | | - try { |
| 1579 | | - return (await getRunpodStatus(timeout))?.state === 'green'; |
| 1580 | | - } catch { |
| 1581 | | - return false; |
| 1582 | | - } |
| 1583 | | -} |
| 1584 | | - |
| 1585 | | -/** |
| 1586 | | - * Updates the status dot + text from a /lazy/status response (or an error). |
| 1587 | | - * @param {object|null} status Parsed status JSON, or null when unreachable. |
| 1588 | | - */ |
| 1589 | | -function renderRunpodStatus(status) { |
| 1590 | | - const dot = document.getElementById('sd_runpod_dot'); |
| 1591 | | - const text = document.getElementById('sd_runpod_status_text'); |
| 1592 | | - if (!dot || !text) { |
| 1593 | | - return; |
| 1594 | | - } |
| 1595 | | - const phase = status?.state ?? 'red'; |
| 1596 | | - runpodLastPhase = phase; |
| 1597 | | - const phaseClass = `sd_runpod_${['red', 'orange', 'green'].includes(phase) ? phase : 'red'}`; |
| 1598 | | - for (const el of [dot, document.getElementById('sd_runpod_bar_dot')].filter(Boolean)) { |
| 1599 | | - el.classList.remove('sd_runpod_red', 'sd_runpod_orange', 'sd_runpod_green'); |
| 1600 | | - el.classList.add(phaseClass); |
| 1601 | | - } |
| 1602 | | - const openLink = document.getElementById('sd_runpod_open'); |
| 1603 | | - if (openLink) { |
| 1604 | | - const showLink = phase === 'green' && status?.url; |
| 1605 | | - openLink.style.display = showLink ? '' : 'none'; |
| 1606 | | - if (showLink) { |
| 1607 | | - openLink.dataset.url = status.url; |
| 1608 | | - } |
| 1609 | | - } |
| 1610 | | - if (!status) { |
| 1611 | | - text.textContent = 'proxy unreachable'; |
| 1612 | | - } else if (phase === 'green') { |
| 1613 | | - const left = status.idle_seconds_left ? ` (idle stop in ${Math.round(status.idle_seconds_left / 60)}m)` : ''; |
| 1614 | | - text.textContent = `ready on ${status.gpu ?? 'GPU'}${left}`; |
| 1615 | | - } else if (phase === 'orange') { |
| 1616 | | - text.textContent = `starting (models: ${status.model || (status.active ?? []).join(' + ') || '?'})…`; |
| 1617 | | - } else { |
| 1618 | | - text.textContent = 'off'; |
| 1619 | | - } |
| 1620 | | - const barDot = document.getElementById('sd_runpod_bar_dot'); |
| 1621 | | - if (barDot) { |
| 1622 | | - barDot.title = `RunPod pod: ${text.textContent} — click to ${phase === 'red' ? 'warm up' : 'shut down'}`; |
| 1623 | | - } |
| 1624 | | - return phase; |
| 1625 | | -} |
| 1626 | | - |
| 1627 | | -/** Adds the clickable pod-status dot to the chat bar (next to other extension icons). */ |
| 1628 | | -function ensureRunpodBarDot() { |
| 1629 | | - if (document.getElementById('sd_runpod_bar_dot')) { |
| 1630 | | - return; |
| 1631 | | - } |
| 1632 | | - const anchor = document.getElementById('leftSendForm'); |
| 1633 | | - if (!anchor) { |
| 1634 | | - return; |
| 1635 | | - } |
| 1636 | | - const dot = document.createElement('div'); |
| 1637 | | - dot.id = 'sd_runpod_bar_dot'; |
| 1638 | | - dot.classList.add('sd_runpod_dot', 'sd_runpod_bar_dot', 'sd_runpod_red', 'interactable'); |
| 1639 | | - dot.title = 'RunPod pod'; |
| 1640 | | - dot.tabIndex = 0; |
| 1641 | | - dot.addEventListener('click', () => runpodControl(runpodLastPhase === 'red' ? 'warmup' : 'shutdown')); |
| 1642 | | - anchor.appendChild(dot); |
| 1643 | | - dot.style.display = getRunpodLazyUrl() ? '' : 'none'; |
| 1644 | | -} |
| 1645 | | - |
| 1646 | | -let runpodPollFailures = 0; |
| 1647 | | - |
| 1648 | | -async function pollRunpodStatus() { |
| 1649 | | - const url = getRunpodLazyUrl(); |
| 1650 | | - if (!url) { |
| 1651 | | - return; |
| 1652 | | - } |
| 1653 | | - let phase = runpodLastPhase; |
| 1654 | | - try { |
| 1655 | | - runpodPollFailures = 0; |
| 1656 | | - phase = renderRunpodStatus(await getRunpodStatus()); |
| 1657 | | - } catch { |
| 1658 | | - // A single slow/failed poll (e.g. while the proxy is provisioning a pod) |
| 1659 | | - // must not flip the dot to red; only sustained unreachability does. |
| 1660 | | - runpodPollFailures++; |
| 1661 | | - if (runpodPollFailures >= 3) { |
| 1662 | | - phase = renderRunpodStatus(null); |
| 1663 | | - } |
| 1664 | | - } |
| 1665 | | - clearTimeout(runpodStatusTimer); |
| 1666 | | - runpodStatusTimer = setTimeout(pollRunpodStatus, phase === 'orange' || runpodPollFailures ? RUNPOD_POLL_BUSY_MS : RUNPOD_POLL_IDLE_MS); |
| 1667 | | -} |
| 1668 | | - |
| 1669 | | -async function runpodControl(action) { |
| 1670 | | - const url = getRunpodLazyUrl(); |
| 1671 | | - if (!url) { |
| 1672 | | - return; |
| 1673 | | - } |
| 1674 | | - try { |
| 1675 | | - if (action === 'warmup' && !(await pushRunpodCatalog())) { |
| 1676 | | - throw new Error('Could not sync the RunPod model catalog. The pod was not started.'); |
| 1677 | | - } |
| 1678 | | - const result = await fetch(`${url}/lazy/${action}`, { method: 'POST', signal: AbortSignal.timeout(10000) }); |
| 1679 | | - if (!result.ok) { |
| 1680 | | - throw new Error(`RunPod ${action} returned ${result.status}.`); |
| 1681 | | - } |
| 1682 | | - renderRunpodStatus(await result.json()); |
| 1683 | | - if (action === 'warmup') { |
| 1684 | | - toastr.info(t`Pod warmup requested. Models will pre-download; the dot turns green when ready.`, t`Image Generation`); |
| 1685 | | - } |
| 1686 | | - } catch (error) { |
| 1687 | | - toastr.error(String(error), t`Image Generation`); |
| 1688 | | - } |
| 1689 | | - clearTimeout(runpodStatusTimer); |
| 1690 | | - runpodStatusTimer = setTimeout(pollRunpodStatus, RUNPOD_POLL_BUSY_MS); |
| 1691 | | -} |
| 1692 | | - |
| 1693 | | -function runpodHeartbeat() { |
| 1694 | | - const url = getRunpodLazyUrl(); |
| 1695 | | - if (!url) { |
| 1696 | | - return; |
| 1697 | | - } |
| 1698 | | - // This only renews the proxy's frontend lease and never starts a pod. The |
| 1699 | | - // proxy owns the stable upstream keepalive cadence so browser timer |
| 1700 | | - // throttling cannot directly skew it. |
| 1701 | | - fetch(`${url}/lazy/ping`, { method: 'POST', signal: AbortSignal.timeout(5000) }).catch(() => { }); |
| 1702 | | -} |
| 1703 | | - |
| 1704 | | -/** (Re)starts the status polling and frontend-heartbeat loops. */ |
| 1705 | | -function setupRunpodLoops() { |
| 1706 | | - clearTimeout(runpodStatusTimer); |
| 1707 | | - clearInterval(runpodHeartbeatTimer); |
| 1708 | | - ensureRunpodBarDot(); |
| 1709 | | - const barDot = document.getElementById('sd_runpod_bar_dot'); |
| 1710 | | - if (barDot) { |
| 1711 | | - barDot.style.display = getRunpodLazyUrl() ? '' : 'none'; |
| 1712 | | - } |
| 1713 | | - if (!getRunpodLazyUrl()) { |
| 1714 | | - renderRunpodStatus(null); |
| 1715 | | - return; |
| 1716 | | - } |
| 1717 | | - pollRunpodStatus(); |
| 1718 | | - runpodHeartbeat(); |
| 1719 | | - runpodHeartbeatTimer = setInterval(runpodHeartbeat, RUNPOD_HEARTBEAT_MS); |
| 1720 | | -} |
| 1721 | | - |
| 1722 | | -function onRunpodLazyUrlInput() { |
| 1723 | | - extension_settings.sd.runpod_lazy_url = String($(this).val() ?? '').trim(); |
| 1724 | | - saveSettingsDebounced(); |
| 1725 | | - setupRunpodLoops(); |
| 1726 | | -} |
| 1727 | | - |
| 1728 | | -/** |
| 1729 | | - * Whether a comfy URL points at the runpod-lazy proxy. |
| 1730 | | - * @param {string} url ComfyUI URL to test. |
| 1731 | | - * @returns {boolean} True when it equals the configured proxy URL. |
| 1732 | | - */ |
| 1733 | | -function isRunpodProxyUrl(url) { |
| 1734 | | - const lazy = getRunpodLazyUrl(); |
| 1735 | | - return !!lazy && String(url ?? '').trim().replace(/\/$/, '') === lazy; |
| 1736 | | -} |
| 1737 | | - |
| 1738 | | -/** |
| 1739 | | - * Returns the configured RunPod model catalog entries that have a filename. |
| 1740 | | - * @returns {{name: string, value: string, downloads: string}[]} Catalog entries. |
| 1741 | | - */ |
| 1742 | | -function getRunpodCatalog() { |
| 1743 | | - const models = Array.isArray(extension_settings.sd.runpod_models) ? extension_settings.sd.runpod_models : []; |
| 1744 | | - return models.filter(m => m && String(m.value ?? '').trim()); |
| 1745 | | -} |
| 1746 | | - |
| 1747 | | -/** Catalog entries that go in the Model dropdown (kind !== 'lora'). */ |
| 1748 | | -function getRunpodModelEntries() { |
| 1749 | | - return getRunpodCatalog().filter(m => m.kind !== 'lora'); |
| 1750 | | -} |
| 1751 | | - |
| 1752 | | -/** |
| 1753 | | - * Parses a catalog entry's download lines ("dest-subpath url" per line). |
| 1754 | | - * @param {string} text Multiline downloads definition. |
| 1755 | | - * @returns {{dest: string, url: string}[]} Parsed file list. |
| 1756 | | - */ |
| 1757 | | -function parseRunpodFiles(text) { |
| 1758 | | - return String(text ?? '').split('\n') |
| 1759 | | - .map(line => line.trim()) |
| 1760 | | - .filter(Boolean) |
| 1761 | | - .map(line => { |
| 1762 | | - const space = line.indexOf(' '); |
| 1763 | | - return space > 0 ? { dest: line.slice(0, space).trim(), url: line.slice(space + 1).trim() } : null; |
| 1764 | | - }) |
| 1765 | | - .filter(x => x && x.dest && x.url); |
| 1766 | | -} |
| 1767 | | - |
| 1768 | | -/** |
| 1769 | | - * The model+lora filenames saved for whichever config uses the proxy: the live |
| 1770 | | - * settings if they point at it, else the chain entry that does. |
| 1771 | | - * @returns {string[]} Active catalog values (model and/or lora). |
| 1772 | | - */ |
| 1773 | | -function getRunpodActiveModels() { |
| 1774 | | - let config = null; |
| 1775 | | - // Only use live settings when the current source is actually ComfyUI AND |
| 1776 | | - // its model is in the catalog. If the user is on Grok/OpenRouter but |
| 1777 | | - // comfy_url points at the proxy, the fallback chain's ComfyUI presets are |
| 1778 | | - // what matter, not the live source which may not even be image-gen capable. |
| 1779 | | - if (extension_settings.sd.source === 'comfy' && isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 1780 | | - const known = new Set(getRunpodCatalog().map(m => m.value)); |
| 1781 | | - if (known.has(extension_settings.sd.model)) { |
| 1782 | | - config = extension_settings.sd; |
| 1783 | | - } |
| 1784 | | - } |
| 1785 | | - if (!config) { |
| 1786 | | - const chain = Array.isArray(extension_settings.sd.settings_preset_chain) ? extension_settings.sd.settings_preset_chain : []; |
| 1787 | | - // Find the first ComfyUI preset in the chain, not just any preset with a matching URL |
| 1788 | | - config = chain.find(e => e?.preset?.source === 'comfy' && isRunpodProxyUrl(e?.preset?.comfy_url))?.preset ?? null; |
| 1789 | | - } |
| 1790 | | - if (!config) { |
| 1791 | | - return []; |
| 1792 | | - } |
| 1793 | | - const known = new Set(getRunpodCatalog().map(m => m.value)); |
| 1794 | | - return [config.model, config.lora].filter(v => v && known.has(v)); |
| 1795 | | -} |
| 1796 | | - |
| 1797 | | -/** |
| 1798 | | - * Pushes the model catalog + active selection to the proxy. |
| 1799 | | - * Catalog updates only store configuration; only /lazy/warmup may provision. |
| 1800 | | - * @returns {Promise<boolean>} Whether the proxy accepted the catalog. |
| 1801 | | - */ |
| 1802 | | -async function pushRunpodCatalog() { |
| 1803 | | - const url = getRunpodLazyUrl(); |
| 1804 | | - if (!url) { |
| 1805 | | - return false; |
| 1806 | | - } |
| 1807 | | - const models = getRunpodCatalog().map(m => ({ |
| 1808 | | - name: m.name || m.value, |
| 1809 | | - value: m.value, |
| 1810 | | - kind: m.kind === 'lora' ? 'lora' : 'model', |
| 1811 | | - files: parseRunpodFiles(m.downloads), |
| 1812 | | - })); |
| 1813 | | - try { |
| 1814 | | - const result = await fetch(`${url}/lazy/catalog`, { |
| 1815 | | - method: 'POST', |
| 1816 | | - headers: { 'Content-Type': 'application/json' }, |
| 1817 | | - signal: AbortSignal.timeout(8000), |
| 1818 | | - body: JSON.stringify({ models, active: getRunpodActiveModels() }), |
| 1819 | | - }); |
| 1820 | | - if (!result.ok) { |
| 1821 | | - throw new Error(`catalog returned ${result.status}`); |
| 1822 | | - } |
| 1823 | | - return true; |
| 1824 | | - } catch (error) { |
| 1825 | | - console.warn('SD: runpod catalog push failed', error); |
| 1826 | | - return false; |
| 1827 | | - } |
| 1828 | | -} |
| 1829 | | - |
| 1830 | | -/** |
| 1831 | | - * Rebuilds the RunPod model catalog editor in the settings UI. |
| 1832 | | - */ |
| 1833 | | -function renderRunpodModels() { |
| 1834 | | - const container = $('#sd_runpod_models_list'); |
| 1835 | | - if (!container.length) { |
| 1836 | | - return; |
| 1837 | | - } |
| 1838 | | - container.empty(); |
| 1839 | | - const models = Array.isArray(extension_settings.sd.runpod_models) ? extension_settings.sd.runpod_models : []; |
| 1840 | | - if (models.length === 0) { |
| 1841 | | - container.append($('<small></small>').text('No models configured.')); |
| 1842 | | - return; |
| 1843 | | - } |
| 1844 | | - models.forEach((model, index) => { |
| 1845 | | - const nameInput = $('<input>').addClass('text_pole flex1').attr({ type: 'text', placeholder: 'Display name' }) |
| 1846 | | - .val(model.name || '') |
| 1847 | | - .on('change', function () { model.name = String($(this).val() ?? '').trim(); saveSettingsDebounced(); pushRunpodCatalog(); }); |
| 1848 | | - const valueInput = $('<input>').addClass('text_pole flex1').attr({ type: 'text', placeholder: 'Filename (used by %model% / %lora%)' }) |
| 1849 | | - .val(model.value || '') |
| 1850 | | - .on('change', function () { model.value = String($(this).val() ?? '').trim(); saveSettingsDebounced(); pushRunpodCatalog(); }); |
| 1851 | | - const kindSelect = $('<select></select>').addClass('text_pole') |
| 1852 | | - .append($('<option></option>').val('model').text('model')) |
| 1853 | | - .append($('<option></option>').val('lora').text('lora')) |
| 1854 | | - .val(model.kind === 'lora' ? 'lora' : 'model') |
| 1855 | | - .on('change', function () { model.kind = String($(this).val()); saveSettingsDebounced(); pushRunpodCatalog(); }); |
| 1856 | | - const deleteButton = $('<div></div>').addClass('menu_button menu_button_icon fa-solid fa-trash-can') |
| 1857 | | - .attr('title', 'Remove model') |
| 1858 | | - .on('click', () => { models.splice(index, 1); saveSettingsDebounced(); renderRunpodModels(); pushRunpodCatalog(); }); |
| 1859 | | - const downloadsInput = $('<textarea></textarea>').addClass('text_pole textarea_compact') |
| 1860 | | - .attr({ rows: 2, placeholder: 'One file per line: <models-subpath> <url>\ne.g. unet/model.gguf https://huggingface.co/...' }) |
| 1861 | | - .val(model.downloads || '') |
| 1862 | | - .on('change', function () { model.downloads = String($(this).val() ?? ''); saveSettingsDebounced(); pushRunpodCatalog(); }); |
| 1863 | | - container.append( |
| 1864 | | - $('<div></div>').addClass('flexFlowColumn marginTopBot5') |
| 1865 | | - .append($('<div></div>').addClass('flex-container alignItemsCenter').append(nameInput).append(valueInput).append(kindSelect).append(deleteButton)) |
| 1866 | | - .append(downloadsInput)); |
| 1867 | | - }); |
| 1868 | | -} |
| 1869 | | - |
| 1870 | | -function onRunpodModelsAddClick() { |
| 1871 | | - if (!Array.isArray(extension_settings.sd.runpod_models)) { |
| 1872 | | - extension_settings.sd.runpod_models = []; |
| 1873 | | - } |
| 1874 | | - extension_settings.sd.runpod_models.push({ name: '', value: '', downloads: '' }); |
| 1875 | | - saveSettingsDebounced(); |
| 1876 | | - renderRunpodModels(); |
| 1877 | | -} |
| 1878 | | - |
| 1879 | | -// #endregion |
| 1880 | | - |
| 1881 | | -/** |
| 1882 | | - * Rebuilds the custom wand entries list in the settings UI. |
| 1883 | | - */ |
| 1884 | | -function renderCustomEntriesList() { |
| 1885 | | - const container = $('#sd_custom_entries_list'); |
| 1886 | | - if (!container.length) { |
| 1887 | | - return; |
| 1888 | | - } |
| 1889 | | - |
| 1890 | | - container.empty(); |
| 1891 | | - |
| 1892 | | - const entries = Array.isArray(extension_settings.sd.custom_entries) ? extension_settings.sd.custom_entries : []; |
| 1893 | | - |
| 1894 | | - if (entries.length === 0) { |
| 1895 | | - const empty = $('<small></small>') |
| 1896 | | - .attr('data-i18n', 'No custom entries yet.') |
| 1897 | | - .text('No custom entries yet.'); |
| 1898 | | - container.append(empty); |
| 1899 | | - return; |
| 1900 | | - } |
| 1901 | | - |
| 1902 | | - for (const entry of entries) { |
| 1903 | | - const preview = String(entry.prompt || '').replace(/\s+/g, ' ').trim(); |
| 1904 | | - const truncated = preview.length > 80 ? preview.slice(0, 80) + '…' : preview; |
| 1905 | | - |
| 1906 | | - const titleEl = $('<div></div>').addClass('sd_custom_entry_title').text(entry.title); |
| 1907 | | - const previewEl = $('<small></small>').addClass('sd_custom_entry_preview').text(truncated); |
| 1908 | | - const textBlock = $('<div></div>').addClass('flex1 flexFlowColumn').append(titleEl).append(previewEl); |
| 1909 | | - |
| 1910 | | - const editButton = $('<div></div>') |
| 1911 | | - .addClass('menu_button menu_button_icon fa-solid fa-pencil') |
| 1912 | | - .attr('data-entry-id', entry.id) |
| 1913 | | - .attr('data-action', 'edit') |
| 1914 | | - .attr('title', 'Edit') |
| 1915 | | - .attr('data-i18n', '[title]Edit'); |
| 1916 | | - const deleteButton = $('<div></div>') |
| 1917 | | - .addClass('menu_button menu_button_icon fa-solid fa-trash-can') |
| 1918 | | - .attr('data-entry-id', entry.id) |
| 1919 | | - .attr('data-action', 'delete') |
| 1920 | | - .attr('title', 'Delete') |
| 1921 | | - .attr('data-i18n', '[title]Delete'); |
| 1922 | | - |
| 1923 | | - const row = $('<div></div>') |
| 1924 | | - .addClass('flex-container alignItemsCenter marginTopBot5 sd_custom_entry_row') |
| 1925 | | - .append(textBlock) |
| 1926 | | - .append(editButton) |
| 1927 | | - .append(deleteButton); |
| 1928 | | - |
| 1929 | | - container.append(row); |
| 1930 | | - } |
| 1931 | | -} |
| 1932 | | - |
| 1933 | | -/** |
| 1934 | | - * Generates a unique id for a custom entry. |
| 1935 | | - * @returns {string} A unique identifier. |
| 1936 | | - */ |
| 1937 | | -function getUniqueCustomEntryId() { |
| 1938 | | - return crypto.randomUUID?.() ?? ('ce_' + Date.now() + '_' + Math.floor(Math.random() * 1e6)); |
| 1939 | | -} |
| 1940 | | - |
| 1941 | | -/** |
| 1942 | | - * Opens a popup to create/edit a custom entry and returns the entered values. |
| 1943 | | - * @param {string} title Initial title value. |
| 1944 | | - * @param {string} prompt Initial prompt value. |
| 1945 | | - * @returns {Promise<{title: string, prompt: string} | null>} Entered values, or null if cancelled. |
| 1946 | | - */ |
| 1947 | | -async function showCustomEntryPopup(title, prompt) { |
| 1948 | | - const form = $('<div></div>').addClass('flex-container flexFlowColumn'); |
| 1949 | | - const titleLabel = $('<label></label>').attr('data-i18n', 'Title').text('Title'); |
| 1950 | | - const titleInput = $('<input>') |
| 1951 | | - .addClass('text_pole') |
| 1952 | | - .attr('type', 'text') |
| 1953 | | - .attr('id', 'sd_custom_entry_title_input') |
| 1954 | | - .val(title || ''); |
| 1955 | | - const promptLabel = $('<label></label>').attr('data-i18n', 'Prompt').text('Prompt'); |
| 1956 | | - const promptInput = $('<textarea></textarea>') |
| 1957 | | - .addClass('text_pole textarea_compact') |
| 1958 | | - .attr('id', 'sd_custom_entry_prompt_input') |
| 1959 | | - .attr('rows', 5) |
| 1960 | | - .val(prompt || ''); |
| 1961 | | - |
| 1962 | | - form.append(titleLabel).append(titleInput).append(promptLabel).append(promptInput); |
| 1963 | | - |
| 1964 | | - const popup = new Popup(form, POPUP_TYPE.CONFIRM, '', { okButton: t`Save`, cancelButton: t`Cancel` }); |
| 1965 | | - const result = await popup.show(); |
| 1966 | | - |
| 1967 | | - if (!result) { |
| 1968 | | - return null; |
| 1969 | | - } |
| 1970 | | - |
| 1971 | | - return { |
| 1972 | | - title: String(titleInput.val() ?? '').trim(), |
| 1973 | | - prompt: String(promptInput.val() ?? '').trim(), |
| 1974 | | - }; |
| 1975 | | -} |
| 1976 | | - |
| 1977 | | -async function onAddCustomEntryClick() { |
| 1978 | | - const values = await showCustomEntryPopup('', ''); |
| 1979 | | - |
| 1980 | | - if (!values) { |
| 1981 | | - return; |
| 1982 | | - } |
| 1983 | | - |
| 1984 | | - if (!values.title || !values.prompt) { |
| 1985 | | - toastr.warning(t`Both a title and a prompt are required.`, t`Image Generation`); |
| 1986 | | - return; |
| 1987 | | - } |
| 1988 | | - |
| 1989 | | - if (!Array.isArray(extension_settings.sd.custom_entries)) { |
| 1990 | | - extension_settings.sd.custom_entries = []; |
| 1991 | | - } |
| 1992 | | - |
| 1993 | | - extension_settings.sd.custom_entries.push({ |
| 1994 | | - id: getUniqueCustomEntryId(), |
| 1995 | | - title: values.title, |
| 1996 | | - prompt: values.prompt, |
| 1997 | | - }); |
| 1998 | | - |
| 1999 | | - saveSettingsDebounced(); |
| 2000 | | - renderCustomEntriesList(); |
| 2001 | | - renderCustomDropdownEntries(); |
| 2002 | | -} |
| 2003 | | - |
| 2004 | | -async function onEditCustomEntryClick(id) { |
| 2005 | | - const entries = Array.isArray(extension_settings.sd.custom_entries) ? extension_settings.sd.custom_entries : []; |
| 2006 | | - const entry = entries.find(e => e.id === id); |
| 2007 | | - |
| 2008 | | - if (!entry) { |
| 2009 | | - return; |
| 2010 | | - } |
| 2011 | | - |
| 2012 | | - const values = await showCustomEntryPopup(entry.title, entry.prompt); |
| 2013 | | - |
| 2014 | | - if (!values) { |
| 2015 | | - return; |
| 2016 | | - } |
| 2017 | | - |
| 2018 | | - if (!values.title || !values.prompt) { |
| 2019 | | - toastr.warning(t`Both a title and a prompt are required.`, t`Image Generation`); |
| 2020 | | - return; |
| 2021 | | - } |
| 2022 | | - |
| 2023 | | - entry.title = values.title; |
| 2024 | | - entry.prompt = values.prompt; |
| 2025 | | - |
| 2026 | | - saveSettingsDebounced(); |
| 2027 | | - renderCustomEntriesList(); |
| 2028 | | - renderCustomDropdownEntries(); |
| 2029 | | -} |
| 2030 | | - |
| 2031 | | -async function onDeleteCustomEntryClick(id) { |
| 2032 | | - const entries = Array.isArray(extension_settings.sd.custom_entries) ? extension_settings.sd.custom_entries : []; |
| 2033 | | - const index = entries.findIndex(e => e.id === id); |
| 2034 | | - |
| 2035 | | - if (index === -1) { |
| 2036 | | - return; |
| 2037 | | - } |
| 2038 | | - |
| 2039 | | - const confirmed = await callGenericPopup(t`Are you sure you want to delete the entry "${entries[index].title}"?`, POPUP_TYPE.CONFIRM, '', { okButton: t`Delete`, cancelButton: t`Cancel` }); |
| 2040 | | - |
| 2041 | | - if (!confirmed) { |
| 2042 | | - return; |
| 2043 | | - } |
| 2044 | | - |
| 2045 | | - entries.splice(index, 1); |
| 2046 | | - saveSettingsDebounced(); |
| 2047 | | - renderCustomEntriesList(); |
| 2048 | | - renderCustomDropdownEntries(); |
| 2049 | | -} |
| 2050 | | - |
| 2051 | | -/** |
| 2052 | | - * Modifies prompt based on user inputs. |
| 2053 | | - * @param {string} prompt Prompt to refine |
| 2054 | | - * @param {object} [args] Additional arguments for refinement |
| 2055 | | - * @param {string} [args.negative] Negative prompt to prefill |
| 2056 | | - * @param {string} [args.resolution] Saved resolution to offer as a checkbox option |
| 2057 | | - * @returns {Promise<string>} Refined prompt |
| 2058 | | - */ |
| 2059 | | -async function refinePrompt(prompt, args = null) { |
| 2060 | | - if (extension_settings.sd.refine_mode) { |
| 2061 | | - /** @type {import('../../popup.js').CustomPopupInput[]} */ |
| 2062 | | - const customInputs = []; |
| 2063 | | - |
| 2064 | | - if (args?.negative) { |
| 2065 | | - customInputs.push({ |
| 2066 | | - id: 'sd_refine_negative', |
| 2067 | | - label: t`Negative prompt (optional)`, |
| 2068 | | - type: 'textarea', |
| 2069 | | - rows: 4, |
| 2070 | | - defaultState: String(args.negative || ''), |
| 2071 | | - }); |
| 2072 | | - } |
| 2073 | | - |
| 2074 | | - if (args?.resolution) { |
| 2075 | | - customInputs.push({ |
| 2076 | | - id: 'sd_use_saved_resolution', |
| 2077 | | - label: t`Use saved resolution (${args.resolution})`, |
| 2078 | | - type: 'checkbox', |
| 2079 | | - defaultState: true, |
| 2080 | | - }); |
| 2081 | | - } |
| 2082 | | - |
| 2083 | | - const refinedPrompt = await Popup.show.input( |
| 2084 | | - t`Review and edit the prompt:`, |
| 2085 | | - t`Press "Cancel" to abort the image generation.`, |
| 2086 | | - prompt.trim(), |
| 2087 | | - { |
| 2088 | | - rows: 8, |
| 2089 | | - okButton: t`Continue`, |
| 2090 | | - cancelButton: t`Cancel`, |
| 2091 | | - customInputs, |
| 2092 | | - onClose: (popup) => { |
| 2093 | | - if (!popup.result || !(popup.inputResults instanceof Map) || !args) { |
| 2094 | | - return; |
| 2095 | | - } |
| 2096 | | - |
| 2097 | | - const negativeInput = popup.inputResults.get('sd_refine_negative'); |
| 2098 | | - const useSavedResolution = popup.inputResults.get('sd_use_saved_resolution'); |
| 2099 | | - |
| 2100 | | - if (negativeInput) { |
| 2101 | | - args.negative = negativeInput.toString().trim(); |
| 2102 | | - } |
| 2103 | | - if (!useSavedResolution) { |
| 2104 | | - args.resolution = null; |
| 2105 | | - } |
| 2106 | | - }, |
| 2107 | | - }); |
| 2108 | | - |
| 2109 | | - if (refinedPrompt) { |
| 2110 | | - return String(refinedPrompt); |
| 2111 | | - } else { |
| 2112 | | - throw new Error('Generation aborted by user.'); |
| 2113 | | - } |
| 2114 | | - } |
| 2115 | | - |
| 2116 | | - return prompt; |
| 2117 | | -} |
| 2118 | | - |
| 2119 | | -async function onChatChanged() { |
| 2120 | | - if (this_chid === undefined || selected_group) { |
| 2121 | | - $('#sd_character_prompt_block').hide(); |
| 2122 | | - return; |
| 2123 | | - } |
| 2124 | | - |
| 2125 | | - $('#sd_character_prompt_block').show(); |
| 2126 | | - |
| 2127 | | - const key = getCharaFilename(this_chid); |
| 2128 | | - let characterPrompt = key ? (extension_settings.sd.character_prompts[key] || '') : ''; |
| 2129 | | - let negativePrompt = key ? (extension_settings.sd.character_negative_prompts[key] || '') : ''; |
| 2130 | | - |
| 2131 | | - const context = getContext(); |
| 2132 | | - const sharedPromptData = context?.characters[this_chid]?.data?.extensions?.sd_character_prompt; |
| 2133 | | - const hasSharedData = sharedPromptData && typeof sharedPromptData === 'object'; |
| 2134 | | - |
| 2135 | | - if (typeof sharedPromptData?.positive === 'string' && !characterPrompt && sharedPromptData.positive) { |
| 2136 | | - characterPrompt = sharedPromptData.positive; |
| 2137 | | - extension_settings.sd.character_prompts[key] = characterPrompt; |
| 2138 | | - } |
| 2139 | | - if (typeof sharedPromptData?.negative === 'string' && !negativePrompt && sharedPromptData.negative) { |
| 2140 | | - negativePrompt = sharedPromptData.negative; |
| 2141 | | - extension_settings.sd.character_negative_prompts[key] = negativePrompt; |
| 2142 | | - } |
| 2143 | | - |
| 2144 | | - $('#sd_character_prompt').val(characterPrompt); |
| 2145 | | - $('#sd_character_negative_prompt').val(negativePrompt); |
| 2146 | | - $('#sd_character_prompt_share').prop('checked', hasSharedData); |
| 2147 | | - await adjustElementScrollHeight(); |
| 2148 | | -} |
| 2149 | | - |
| 2150 | | -async function adjustElementScrollHeight() { |
| 2151 | | - if (CSS.supports('field-sizing', 'content') || !$('.sd_settings').is(':visible')) { |
| 2152 | | - return; |
| 2153 | | - } |
| 2154 | | - |
| 2155 | | - await resetScrollHeight($('#sd_prompt_prefix')); |
| 2156 | | - await resetScrollHeight($('#sd_negative_prompt')); |
| 2157 | | - await resetScrollHeight($('#sd_character_prompt')); |
| 2158 | | - await resetScrollHeight($('#sd_character_negative_prompt')); |
| 2159 | | -} |
| 2160 | | - |
| 2161 | | -async function onCharacterPromptInput() { |
| 2162 | | - const key = getCharaFilename(this_chid); |
| 2163 | | - extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val(); |
| 2164 | | - saveSettingsDebounced(); |
| 2165 | | - writePromptFieldsDebounced(this_chid); |
| 2166 | | - if (CSS.supports('field-sizing', 'content')) return; |
| 2167 | | - await resetScrollHeight($(this)); |
| 2168 | | -} |
| 2169 | | - |
| 2170 | | -async function onCharacterNegativePromptInput() { |
| 2171 | | - const key = getCharaFilename(this_chid); |
| 2172 | | - extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val(); |
| 2173 | | - saveSettingsDebounced(); |
| 2174 | | - writePromptFieldsDebounced(this_chid); |
| 2175 | | - if (CSS.supports('field-sizing', 'content')) return; |
| 2176 | | - await resetScrollHeight($(this)); |
| 2177 | | -} |
| 2178 | | - |
| 2179 | | -function getCharacterPrefix() { |
| 2180 | | - if (this_chid === undefined || selected_group) { |
| 2181 | | - return ''; |
| 2182 | | - } |
| 2183 | | - |
| 2184 | | - const key = getCharaFilename(this_chid); |
| 2185 | | - |
| 2186 | | - if (key) { |
| 2187 | | - return extension_settings.sd.character_prompts[key] || ''; |
| 2188 | | - } |
| 2189 | | - |
| 2190 | | - return ''; |
| 2191 | | -} |
| 2192 | | - |
| 2193 | | -function getCharacterNegativePrefix() { |
| 2194 | | - if (this_chid === undefined || selected_group) { |
| 2195 | | - return ''; |
| 2196 | | - } |
| 2197 | | - |
| 2198 | | - const key = getCharaFilename(this_chid); |
| 2199 | | - |
| 2200 | | - if (key) { |
| 2201 | | - return extension_settings.sd.character_negative_prompts[key] || ''; |
| 2202 | | - } |
| 2203 | | - |
| 2204 | | - return ''; |
| 2205 | | -} |
| 2206 | | - |
| 2207 | | -/** |
| 2208 | | - * Combines two prompt prefixes into one. |
| 2209 | | - * @param {string} str1 Base string |
| 2210 | | - * @param {string} str2 Secondary string |
| 2211 | | - * @param {string} macro Macro to replace with the secondary string |
| 2212 | | - * @returns {string} Combined string with a comma between them |
| 2213 | | - */ |
| 2214 | | -function combinePrefixes(str1, str2, macro = '') { |
| 2215 | | - // Remove leading/trailing white spaces and commas from the strings |
| 2216 | | - const process = (s) => s.trim().replace(/^,|,$/g, '').trim(); |
| 2217 | | - |
| 2218 | | - if (!str2) { |
| 2219 | | - return str1; |
| 2220 | | - } |
| 2221 | | - |
| 2222 | | - str1 = process(str1); |
| 2223 | | - str2 = process(str2); |
| 2224 | | - |
| 2225 | | - // Combine the strings with a comma between them) |
| 2226 | | - const result = macro && str1.includes(macro) ? str1.replace(macro, str2) : `${str1}, ${str2},`; |
| 2227 | | - return process(result); |
| 2228 | | -} |
| 2229 | | - |
| 2230 | | -function onRefineModeInput() { |
| 2231 | | - extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked'); |
| 2232 | | - saveSettingsDebounced(); |
| 2233 | | -} |
| 2234 | | - |
| 2235 | | -function onFreeExtendInput() { |
| 2236 | | - extension_settings.sd.free_extend = !!$('#sd_free_extend').prop('checked'); |
| 2237 | | - saveSettingsDebounced(); |
| 2238 | | -} |
| 2239 | | - |
| 2240 | | -function onWandVisibleInput() { |
| 2241 | | - extension_settings.sd.wand_visible = !!$('#sd_wand_visible').prop('checked'); |
| 2242 | | - saveSettingsDebounced(); |
| 2243 | | -} |
| 2244 | | - |
| 2245 | | -function onCommandVisibleInput() { |
| 2246 | | - extension_settings.sd.command_visible = !!$('#sd_command_visible').prop('checked'); |
| 2247 | | - saveSettingsDebounced(); |
| 2248 | | -} |
| 2249 | | - |
| 2250 | | -function onInteractiveVisibleInput() { |
| 2251 | | - extension_settings.sd.interactive_visible = !!$('#sd_interactive_visible').prop('checked'); |
| 2252 | | - saveSettingsDebounced(); |
| 2253 | | -} |
| 2254 | | - |
| 2255 | | -function onToolVisibleInput() { |
| 2256 | | - extension_settings.sd.tool_visible = !!$('#sd_tool_visible').prop('checked'); |
| 2257 | | - saveSettingsDebounced(); |
| 2258 | | -} |
| 2259 | | - |
| 2260 | | -function onClipSkipInput() { |
| 2261 | | - extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val()); |
| 2262 | | - $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| 2263 | | - saveSettingsDebounced(); |
| 2264 | | -} |
| 2265 | | - |
| 2266 | | -function onSeedInput() { |
| 2267 | | - extension_settings.sd.seed = Number($('#sd_seed').val()); |
| 2268 | | - saveSettingsDebounced(); |
| 2269 | | -} |
| 2270 | | - |
| 2271 | | -function onScaleInput() { |
| 2272 | | - extension_settings.sd.scale = Number($('#sd_scale').val()); |
| 2273 | | - $('#sd_scale_value').val(extension_settings.sd.scale.toFixed(1)); |
| 2274 | | - saveSettingsDebounced(); |
| 2275 | | -} |
| 2276 | | - |
| 2277 | | -function onStepsInput() { |
| 2278 | | - extension_settings.sd.steps = Number($('#sd_steps').val()); |
| 2279 | | - $('#sd_steps_value').val(extension_settings.sd.steps); |
| 2280 | | - saveSettingsDebounced(); |
| 2281 | | -} |
| 2282 | | - |
| 2283 | | -async function onPromptPrefixInput() { |
| 2284 | | - extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val(); |
| 2285 | | - saveSettingsDebounced(); |
| 2286 | | - if (CSS.supports('field-sizing', 'content')) return; |
| 2287 | | - await resetScrollHeight($(this)); |
| 2288 | | -} |
| 2289 | | - |
| 2290 | | -async function onNegativePromptInput() { |
| 2291 | | - extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val(); |
| 2292 | | - saveSettingsDebounced(); |
| 2293 | | - if (CSS.supports('field-sizing', 'content')) return; |
| 2294 | | - await resetScrollHeight($(this)); |
| 2295 | | -} |
| 2296 | | - |
| 2297 | | -function onSamplerChange() { |
| 2298 | | - extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val(); |
| 2299 | | - saveSettingsDebounced(); |
| 2300 | | -} |
| 2301 | | - |
| 2302 | | -function onADetailerFaceChange() { |
| 2303 | | - extension_settings.sd.adetailer_face = !!$('#sd_adetailer_face').prop('checked'); |
| 2304 | | - saveSettingsDebounced(); |
| 2305 | | -} |
| 2306 | | - |
| 2307 | | -const resolutionOptions = { |
| 2308 | | - sd_res_512x512: { width: 512, height: 512, name: translate('512x512 (1:1, icons, profile pictures)', 'sd_res_512x512') }, |
| 2309 | | - sd_res_600x600: { width: 600, height: 600, name: translate('600x600 (1:1, icons, profile pictures)', 'sd_res_600x600') }, |
| 2310 | | - sd_res_512x768: { width: 512, height: 768, name: translate('512x768 (2:3, vertical character card)', 'sd_res_512x768') }, |
| 2311 | | - sd_res_768x512: { width: 768, height: 512, name: translate('768x512 (3:2, horizontal 35-mm movie film)', 'sd_res_768x512') }, |
| 2312 | | - sd_res_960x540: { width: 960, height: 540, name: translate('960x540 (16:9, horizontal wallpaper)', 'sd_res_960x540') }, |
| 2313 | | - sd_res_540x960: { width: 540, height: 960, name: translate('540x960 (9:16, vertical wallpaper)', 'sd_res_540x960') }, |
| 2314 | | - sd_res_1920x1088: { width: 1920, height: 1088, name: translate('1920x1088 (16:9, 1080p, horizontal wallpaper)', 'sd_res_1920x1088') }, |
| 2315 | | - sd_res_1088x1920: { width: 1088, height: 1920, name: translate('1088x1920 (9:16, 1080p, vertical wallpaper)', 'sd_res_1088x1920') }, |
| 2316 | | - sd_res_1280x720: { width: 1280, height: 720, name: translate('1280x720 (16:9, 720p, horizontal wallpaper)', 'sd_res_1280x720') }, |
| 2317 | | - sd_res_720x1280: { width: 720, height: 1280, name: translate('720x1280 (9:16, 720p, vertical wallpaper)', 'sd_res_720x1280') }, |
| 2318 | | - sd_res_1024x1024: { width: 1024, height: 1024, name: '1024x1024 (1:1, SDXL)' }, |
| 2319 | | - sd_res_1152x896: { width: 1152, height: 896, name: '1152x896 (9:7, SDXL)' }, |
| 2320 | | - sd_res_896x1152: { width: 896, height: 1152, name: '896x1152 (7:9, SDXL)' }, |
| 2321 | | - sd_res_1216x832: { width: 1216, height: 832, name: '1216x832 (19:13, SDXL)' }, |
| 2322 | | - sd_res_832x1216: { width: 832, height: 1216, name: '832x1216 (13:19, SDXL)' }, |
| 2323 | | - sd_res_1344x768: { width: 1344, height: 768, name: '1344x768 (4:3, SDXL)' }, |
| 2324 | | - sd_res_768x1344: { width: 768, height: 1344, name: '768x1344 (3:4, SDXL)' }, |
| 2325 | | - sd_res_1536x640: { width: 1536, height: 640, name: '1536x640 (24:10, SDXL)' }, |
| 2326 | | - sd_res_640x1536: { width: 640, height: 1536, name: '640x1536 (10:24, SDXL)' }, |
| 2327 | | - sd_res_1536x1024: { width: 1536, height: 1024, name: '1536x1024 (3:2, ChatGPT)' }, |
| 2328 | | - sd_res_1024x1536: { width: 1024, height: 1536, name: '1024x1536 (2:3, ChatGPT)' }, |
| 2329 | | - sd_res_1024x1792: { width: 1024, height: 1792, name: '1024x1792 (4:7, DALL-E)' }, |
| 2330 | | - sd_res_1792x1024: { width: 1792, height: 1024, name: '1792x1024 (7:4, DALL-E)' }, |
| 2331 | | - sd_res_1280x1280: { width: 1280, height: 1280, name: '1280x1280 (1:1, Z.AI)' }, |
| 2332 | | - sd_res_1568x1056: { width: 1568, height: 1056, name: '1568x1056 (3:2, Z.AI)' }, |
| 2333 | | - sd_res_1056x1568: { width: 1056, height: 1568, name: '1056x1568 (2:3, Z.AI)' }, |
| 2334 | | - sd_res_1472x1088: { width: 1472, height: 1088, name: '1472x1088 (4:3, Z.AI)' }, |
| 2335 | | - sd_res_1088x1472: { width: 1088, height: 1472, name: '1088x1472 (3:4, Z.AI)' }, |
| 2336 | | - sd_res_1728x960: { width: 1728, height: 960, name: '1728x960 (16:9, Z.AI)' }, |
| 2337 | | - sd_res_960x1728: { width: 960, height: 1728, name: '960x1728 (9:16, Z.AI)' }, |
| 2338 | | -}; |
| 2339 | | - |
| 2340 | | -function onResolutionChange() { |
| 2341 | | - const selectedOption = $('#sd_resolution').val(); |
| 2342 | | - const selectedResolution = resolutionOptions[selectedOption]; |
| 2343 | | - |
| 2344 | | - if (!selectedResolution) { |
| 2345 | | - console.warn(`Could not find resolution option for ${selectedOption}`); |
| 2346 | | - return; |
| 2347 | | - } |
| 2348 | | - |
| 2349 | | - $('#sd_height').val(selectedResolution.height).trigger('input'); |
| 2350 | | - $('#sd_width').val(selectedResolution.width).trigger('input'); |
| 2351 | | -} |
| 2352 | | - |
| 2353 | | -function onSchedulerChange() { |
| 2354 | | - extension_settings.sd.scheduler = $('#sd_scheduler').find(':selected').val(); |
| 2355 | | - saveSettingsDebounced(); |
| 2356 | | -} |
| 2357 | | - |
| 2358 | | -function onWidthInput() { |
| 2359 | | - extension_settings.sd.width = Number($('#sd_width').val()); |
| 2360 | | - $('#sd_width_value').val(extension_settings.sd.width); |
| 2361 | | - saveSettingsDebounced(); |
| 2362 | | -} |
| 2363 | | - |
| 2364 | | -function onHeightInput() { |
| 2365 | | - extension_settings.sd.height = Number($('#sd_height').val()); |
| 2366 | | - $('#sd_height_value').val(extension_settings.sd.height); |
| 2367 | | - saveSettingsDebounced(); |
| 2368 | | -} |
| 2369 | | - |
| 2370 | | -function onSwapDimensionsClick() { |
| 2371 | | - const w = extension_settings.sd.height; |
| 2372 | | - const h = extension_settings.sd.width; |
| 2373 | | - extension_settings.sd.width = w; |
| 2374 | | - extension_settings.sd.height = h; |
| 2375 | | - $('#sd_width').val(w).trigger('input'); |
| 2376 | | - $('#sd_height').val(h).trigger('input'); |
| 2377 | | - saveSettingsDebounced(); |
| 2378 | | -} |
| 2379 | | - |
| 2380 | | -async function onSourceChange() { |
| 2381 | | - extension_settings.sd.source = $('#sd_source').find(':selected').val(); |
| 2382 | | - extension_settings.sd.model = null; |
| 2383 | | - extension_settings.sd.sampler = null; |
| 2384 | | - extension_settings.sd.scheduler = null; |
| 2385 | | - extension_settings.sd.vae = null; |
| 2386 | | - toggleSourceControls(); |
| 2387 | | - saveSettingsDebounced(); |
| 2388 | | - await loadSettingOptions(); |
| 2389 | | -} |
| 2390 | | - |
| 2391 | | -async function onComfyTypeChange() { |
| 2392 | | - extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val(); |
| 2393 | | - await onSourceChange(); |
| 2394 | | -} |
| 2395 | | - |
| 2396 | | -function onFunctionToolInput() { |
| 2397 | | - extension_settings.sd.function_tool = !!$(this).prop('checked'); |
| 2398 | | - saveSettingsDebounced(); |
| 2399 | | - registerFunctionTool(); |
| 2400 | | -} |
| 2401 | | - |
| 2402 | | -async function onOpenAiStyleSelect() { |
| 2403 | | - extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val()); |
| 2404 | | - saveSettingsDebounced(); |
| 2405 | | -} |
| 2406 | | - |
| 2407 | | -async function onOpenAiQualitySelect() { |
| 2408 | | - extension_settings.sd.openai_quality = String($('#sd_openai_quality').find(':selected').val()); |
| 2409 | | - saveSettingsDebounced(); |
| 2410 | | -} |
| 2411 | | - |
| 2412 | | -async function onOpenAiDurationSelect() { |
| 2413 | | - extension_settings.sd.openai_duration = String($('#sd_openai_duration').find(':selected').val()); |
| 2414 | | - saveSettingsDebounced(); |
| 2415 | | -} |
| 2416 | | - |
| 2417 | | -async function onViewAnlasClick() { |
| 2418 | | - const result = await loadNovelSubscriptionData(); |
| 2419 | | - |
| 2420 | | - if (!result) { |
| 2421 | | - toastr.warning('Are you subscribed?', 'Could not load NovelAI subscription data'); |
| 2422 | | - return; |
| 2423 | | - } |
| 2424 | | - |
| 2425 | | - const anlas = getNovelAnlas(); |
| 2426 | | - const unlimitedGeneration = getNovelUnlimitedImageGeneration(); |
| 2427 | | - |
| 2428 | | - toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`); |
| 2429 | | -} |
| 2430 | | - |
| 2431 | | -function onNovelAnlasGuardInput() { |
| 2432 | | - extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked'); |
| 2433 | | - saveSettingsDebounced(); |
| 2434 | | -} |
| 2435 | | - |
| 2436 | | -function onNovelSmInput() { |
| 2437 | | - extension_settings.sd.novel_sm = !!$('#sd_novel_sm').prop('checked'); |
| 2438 | | - saveSettingsDebounced(); |
| 2439 | | - |
| 2440 | | - if (!extension_settings.sd.novel_sm) { |
| 2441 | | - $('#sd_novel_sm_dyn').prop('checked', false).prop('disabled', true).trigger('input'); |
| 2442 | | - } else { |
| 2443 | | - $('#sd_novel_sm_dyn').prop('disabled', false); |
| 2444 | | - } |
| 2445 | | -} |
| 2446 | | - |
| 2447 | | -function onNovelSmDynInput() { |
| 2448 | | - extension_settings.sd.novel_sm_dyn = !!$('#sd_novel_sm_dyn').prop('checked'); |
| 2449 | | - saveSettingsDebounced(); |
| 2450 | | -} |
| 2451 | | - |
| 2452 | | -function onNovelDecrisperInput() { |
| 2453 | | - extension_settings.sd.novel_decrisper = !!$('#sd_novel_decrisper').prop('checked'); |
| 2454 | | - saveSettingsDebounced(); |
| 2455 | | -} |
| 2456 | | - |
| 2457 | | -function onNovelVarietyBoostInput() { |
| 2458 | | - extension_settings.sd.novel_variety_boost = !!$('#sd_novel_variety_boost').prop('checked'); |
| 2459 | | - saveSettingsDebounced(); |
| 2460 | | -} |
| 2461 | | - |
| 2462 | | -function onPollinationsEnhanceInput() { |
| 2463 | | - extension_settings.sd.pollinations_enhance = !!$('#sd_pollinations_enhance').prop('checked'); |
| 2464 | | - saveSettingsDebounced(); |
| 2465 | | -} |
| 2466 | | - |
| 2467 | | -function onHordeNsfwInput() { |
| 2468 | | - extension_settings.sd.horde_nsfw = !!$(this).prop('checked'); |
| 2469 | | - saveSettingsDebounced(); |
| 2470 | | -} |
| 2471 | | - |
| 2472 | | -function onHordeKarrasInput() { |
| 2473 | | - extension_settings.sd.horde_karras = !!$(this).prop('checked'); |
| 2474 | | - saveSettingsDebounced(); |
| 2475 | | -} |
| 2476 | | - |
| 2477 | | -function onHordeSanitizeInput() { |
| 2478 | | - extension_settings.sd.horde_sanitize = !!$(this).prop('checked'); |
| 2479 | | - saveSettingsDebounced(); |
| 2480 | | -} |
| 2481 | | - |
| 2482 | | -function onRestoreFacesInput() { |
| 2483 | | - extension_settings.sd.restore_faces = !!$(this).prop('checked'); |
| 2484 | | - saveSettingsDebounced(); |
| 2485 | | -} |
| 2486 | | - |
| 2487 | | -function onHighResFixInput() { |
| 2488 | | - extension_settings.sd.enable_hr = !!$(this).prop('checked'); |
| 2489 | | - saveSettingsDebounced(); |
| 2490 | | -} |
| 2491 | | - |
| 2492 | | -function onAutoUrlInput() { |
| 2493 | | - extension_settings.sd.auto_url = $('#sd_auto_url').val(); |
| 2494 | | - saveSettingsDebounced(); |
| 2495 | | -} |
| 2496 | | - |
| 2497 | | -function onAutoAuthInput() { |
| 2498 | | - extension_settings.sd.auto_auth = $('#sd_auto_auth').val(); |
| 2499 | | - saveSettingsDebounced(); |
| 2500 | | -} |
| 2501 | | - |
| 2502 | | -function onSdcppUrlInput() { |
| 2503 | | - extension_settings.sd.sdcpp_url = $('#sd_sdcpp_url').val(); |
| 2504 | | - saveSettingsDebounced(); |
| 2505 | | -} |
| 2506 | | - |
| 2507 | | -function onVladUrlInput() { |
| 2508 | | - extension_settings.sd.vlad_url = $('#sd_vlad_url').val(); |
| 2509 | | - saveSettingsDebounced(); |
| 2510 | | -} |
| 2511 | | - |
| 2512 | | -function onVladAuthInput() { |
| 2513 | | - extension_settings.sd.vlad_auth = $('#sd_vlad_auth').val(); |
| 2514 | | - saveSettingsDebounced(); |
| 2515 | | -} |
| 2516 | | - |
| 2517 | | -function onDrawthingsUrlInput() { |
| 2518 | | - extension_settings.sd.drawthings_url = $('#sd_drawthings_url').val(); |
| 2519 | | - saveSettingsDebounced(); |
| 2520 | | -} |
| 2521 | | - |
| 2522 | | -function onDrawthingsAuthInput() { |
| 2523 | | - extension_settings.sd.drawthings_auth = $('#sd_drawthings_auth').val(); |
| 2524 | | - saveSettingsDebounced(); |
| 2525 | | -} |
| 2526 | | - |
| 2527 | | -function onHrUpscalerChange() { |
| 2528 | | - extension_settings.sd.hr_upscaler = $('#sd_hr_upscaler').find(':selected').val(); |
| 2529 | | - saveSettingsDebounced(); |
| 2530 | | -} |
| 2531 | | - |
| 2532 | | -function onHrScaleInput() { |
| 2533 | | - extension_settings.sd.hr_scale = Number($('#sd_hr_scale').val()); |
| 2534 | | - $('#sd_hr_scale_value').val(extension_settings.sd.hr_scale.toFixed(1)); |
| 2535 | | - saveSettingsDebounced(); |
| 2536 | | -} |
| 2537 | | - |
| 2538 | | -function onDenoisingStrengthInput() { |
| 2539 | | - extension_settings.sd.denoising_strength = Number($('#sd_denoising_strength').val()); |
| 2540 | | - $('#sd_denoising_strength_value').val(extension_settings.sd.denoising_strength.toFixed(2)); |
| 2541 | | - saveSettingsDebounced(); |
| 2542 | | -} |
| 2543 | | - |
| 2544 | | -function onLoraStrengthInput() { |
| 2545 | | - extension_settings.sd.lora_strength = Number($('#sd_lora_strength').val()); |
| 2546 | | - $('#sd_lora_strength_value').val(extension_settings.sd.lora_strength.toFixed(2)); |
| 2547 | | - saveSettingsDebounced(); |
| 2548 | | -} |
| 2549 | | - |
| 2550 | | -function onHrSecondPassStepsInput() { |
| 2551 | | - extension_settings.sd.hr_second_pass_steps = Number($('#sd_hr_second_pass_steps').val()); |
| 2552 | | - $('#sd_hr_second_pass_steps_value').val(extension_settings.sd.hr_second_pass_steps); |
| 2553 | | - saveSettingsDebounced(); |
| 2554 | | -} |
| 2555 | | - |
| 2556 | | -function onComfyUrlInput() { |
| 2557 | | - extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); |
| 2558 | | - saveSettingsDebounced(); |
| 2559 | | -} |
| 2560 | | - |
| 2561 | | -function onComfyRunPodUrlInput() { |
| 2562 | | - extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val()); |
| 2563 | | - saveSettingsDebounced(); |
| 2564 | | -} |
| 2565 | | - |
| 2566 | | -function onHFModelInput() { |
| 2567 | | - extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val(); |
| 2568 | | - saveSettingsDebounced(); |
| 2569 | | -} |
| 2570 | | - |
| 2571 | | -/** |
| 2572 | | - * Stores the current model/lora choice for the active comfy workflow, so |
| 2573 | | - * switching back to this workflow restores it (per provider via presets). |
| 2574 | | - * @param {'model'|'lora'} key Which selection to remember. |
| 2575 | | - * @param {string} value Selected value. |
| 2576 | | - */ |
| 2577 | | -function rememberWorkflowPref(key, value) { |
| 2578 | | - if (extension_settings.sd.source !== sources.comfy || !extension_settings.sd.comfy_workflow) { |
| 2579 | | - return; |
| 2580 | | - } |
| 2581 | | - const prefs = extension_settings.sd.comfy_workflow_prefs; |
| 2582 | | - prefs[extension_settings.sd.comfy_workflow] = { ...(prefs[extension_settings.sd.comfy_workflow] ?? {}), [key]: value }; |
| 2583 | | -} |
| 2584 | | - |
| 2585 | | -async function onLoraChange() { |
| 2586 | | - extension_settings.sd.lora = $('#sd_lora').find(':selected').val(); |
| 2587 | | - rememberWorkflowPref('lora', extension_settings.sd.lora); |
| 2588 | | - saveSettingsDebounced(); |
| 2589 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 2590 | | - pushRunpodCatalog(); |
| 2591 | | - } |
| 2592 | | -} |
| 2593 | | - |
| 2594 | | -function onComfyWorkflowChange() { |
| 2595 | | - extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val(); |
| 2596 | | - // Restore the model/lora remembered for this workflow (auto-configuration |
| 2597 | | - // when switching between e.g. the flux and qwen workflow families). |
| 2598 | | - const pref = extension_settings.sd.comfy_workflow_prefs?.[extension_settings.sd.comfy_workflow]; |
| 2599 | | - if (pref) { |
| 2600 | | - if (pref.model) { |
| 2601 | | - extension_settings.sd.model = pref.model; |
| 2602 | | - $('#sd_model').val(pref.model); |
| 2603 | | - } |
| 2604 | | - if (pref.lora !== undefined) { |
| 2605 | | - extension_settings.sd.lora = pref.lora; |
| 2606 | | - $('#sd_lora').val(pref.lora); |
| 2607 | | - } |
| 2608 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 2609 | | - pushRunpodCatalog(); |
| 2610 | | - } |
| 2611 | | - } |
| 2612 | | - saveSettingsDebounced(); |
| 2613 | | -} |
| 2614 | | - |
| 2615 | | -function onBflUpsamplingInput() { |
| 2616 | | - extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 2617 | | - saveSettingsDebounced(); |
| 2618 | | -} |
| 2619 | | - |
| 2620 | | -function onStabilityStylePresetChange() { |
| 2621 | | - extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val()); |
| 2622 | | - saveSettingsDebounced(); |
| 2623 | | -} |
| 2624 | | - |
| 2625 | | -async function changeComfyWorkflow(_, name) { |
| 2626 | | - name = name.replace(/(\.json)?$/i, '.json'); |
| 2627 | | - if ($(`#sd_comfy_workflow > [value="${name}"]`).length > 0) { |
| 2628 | | - extension_settings.sd.comfy_workflow = name; |
| 2629 | | - $('#sd_comfy_workflow').val(extension_settings.sd.comfy_workflow); |
| 2630 | | - saveSettingsDebounced(); |
| 2631 | | - } else { |
| 2632 | | - toastr.error(`ComfyUI Workflow "${name}" does not exist.`); |
| 2633 | | - } |
| 2634 | | - return ''; |
| 2635 | | -} |
| 2636 | | - |
| 2637 | | -async function validateAutoUrl() { |
| 2638 | | - try { |
| 2639 | | - if (!extension_settings.sd.auto_url) { |
| 2640 | | - throw new Error('URL is not set.'); |
| 2641 | | - } |
| 2642 | | - |
| 2643 | | - const result = await fetch('/api/sd/ping', { |
| 2644 | | - method: 'POST', |
| 2645 | | - headers: getRequestHeaders(), |
| 2646 | | - body: JSON.stringify(getSdRequestBody()), |
| 2647 | | - }); |
| 2648 | | - |
| 2649 | | - if (!result.ok) { |
| 2650 | | - throw new Error('SD WebUI returned an error.'); |
| 2651 | | - } |
| 2652 | | - |
| 2653 | | - await loadSettingOptions(); |
| 2654 | | - toastr.success('SD WebUI API connected.'); |
| 2655 | | - } catch (error) { |
| 2656 | | - toastr.error(`Could not validate SD WebUI API: ${error.message}`); |
| 2657 | | - } |
| 2658 | | -} |
| 2659 | | - |
| 2660 | | -async function validateSdcppUrl() { |
| 2661 | | - try { |
| 2662 | | - if (!extension_settings.sd.sdcpp_url) { |
| 2663 | | - throw new Error('URL is not set.'); |
| 2664 | | - } |
| 2665 | | - |
| 2666 | | - const result = await fetch('/api/sd/sdcpp/ping', { |
| 2667 | | - method: 'POST', |
| 2668 | | - headers: getRequestHeaders(), |
| 2669 | | - body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }), |
| 2670 | | - }); |
| 2671 | | - |
| 2672 | | - if (!result.ok) { |
| 2673 | | - throw new Error('stable-diffusion.cpp server returned an error.'); |
| 2674 | | - } |
| 2675 | | - |
| 2676 | | - await loadSettingOptions(); |
| 2677 | | - toastr.success('stable-diffusion.cpp server connected.'); |
| 2678 | | - } catch (error) { |
| 2679 | | - toastr.error(`Could not validate stable-diffusion.cpp server: ${error.message}`); |
| 2680 | | - } |
| 2681 | | -} |
| 2682 | | - |
| 2683 | | -async function validateDrawthingsUrl() { |
| 2684 | | - try { |
| 2685 | | - if (!extension_settings.sd.drawthings_url) { |
| 2686 | | - throw new Error('URL is not set.'); |
| 2687 | | - } |
| 2688 | | - |
| 2689 | | - const result = await fetch('/api/sd/drawthings/ping', { |
| 2690 | | - method: 'POST', |
| 2691 | | - headers: getRequestHeaders(), |
| 2692 | | - body: JSON.stringify(getSdRequestBody()), |
| 2693 | | - }); |
| 2694 | | - |
| 2695 | | - if (!result.ok) { |
| 2696 | | - throw new Error('SD Drawthings returned an error.'); |
| 2697 | | - } |
| 2698 | | - |
| 2699 | | - await loadSettingOptions(); |
| 2700 | | - toastr.success('SD Drawthings API connected.'); |
| 2701 | | - } catch (error) { |
| 2702 | | - toastr.error(`Could not validate SD Drawthings API: ${error.message}`); |
| 2703 | | - } |
| 2704 | | -} |
| 2705 | | - |
| 2706 | | -async function validateVladUrl() { |
| 2707 | | - try { |
| 2708 | | - if (!extension_settings.sd.vlad_url) { |
| 2709 | | - throw new Error('URL is not set.'); |
| 2710 | | - } |
| 2711 | | - |
| 2712 | | - const result = await fetch('/api/sd/ping', { |
| 2713 | | - method: 'POST', |
| 2714 | | - headers: getRequestHeaders(), |
| 2715 | | - body: JSON.stringify(getSdRequestBody()), |
| 2716 | | - }); |
| 2717 | | - |
| 2718 | | - if (!result.ok) { |
| 2719 | | - throw new Error('SD.Next returned an error.'); |
| 2720 | | - } |
| 2721 | | - |
| 2722 | | - await loadSettingOptions(); |
| 2723 | | - toastr.success('SD.Next API connected.'); |
| 2724 | | - } catch (error) { |
| 2725 | | - toastr.error(`Could not validate SD.Next API: ${error.message}`); |
| 2726 | | - } |
| 2727 | | -} |
| 2728 | | - |
| 2729 | | -async function validateComfyUrl() { |
| 2730 | | - try { |
| 2731 | | - if (!extension_settings.sd.comfy_url) { |
| 2732 | | - throw new Error('URL is not set.'); |
| 2733 | | - } |
| 2734 | | - |
| 2735 | | - const result = await fetch('/api/sd/comfy/ping', { |
| 2736 | | - method: 'POST', |
| 2737 | | - headers: getRequestHeaders(), |
| 2738 | | - body: JSON.stringify({ |
| 2739 | | - url: extension_settings.sd.comfy_url, |
| 2740 | | - }), |
| 2741 | | - }); |
| 2742 | | - if (!result.ok) { |
| 2743 | | - throw new Error('ComfyUI returned an error.'); |
| 2744 | | - } |
| 2745 | | - |
| 2746 | | - await loadSettingOptions(); |
| 2747 | | - toastr.success('ComfyUI API connected.'); |
| 2748 | | - } catch (error) { |
| 2749 | | - toastr.error(`Could not validate ComfyUI API: ${error.message}`); |
| 2750 | | - } |
| 2751 | | -} |
| 2752 | | - |
| 2753 | | -async function validateComfyRunPodUrl() { |
| 2754 | | - try { |
| 2755 | | - if (!extension_settings.sd.comfy_runpod_url) { |
| 2756 | | - throw new Error('URL is not set.'); |
| 2757 | | - } |
| 2758 | | - |
| 2759 | | - const result = await fetch('/api/sd/comfyrunpod/ping', { |
| 2760 | | - method: 'POST', |
| 2761 | | - headers: getRequestHeaders(), |
| 2762 | | - body: JSON.stringify({ |
| 2763 | | - url: extension_settings.sd.comfy_runpod_url, |
| 2764 | | - }), |
| 2765 | | - }); |
| 2766 | | - if (!result.ok) { |
| 2767 | | - throw new Error('ComfyUI RunPod returned an error.'); |
| 2768 | | - } |
| 2769 | | - |
| 2770 | | - await loadSettingOptions(); |
| 2771 | | - toastr.success('ComfyUI RunPod API connected.'); |
| 2772 | | - } catch (error) { |
| 2773 | | - toastr.error(`Could not validate ComfyUI RunPod API: ${error.message}`); |
| 2774 | | - } |
| 2775 | | -} |
| 2776 | | - |
| 2777 | | -async function onModelChange() { |
| 2778 | | - const selectedModel = $('#sd_model').find(':selected'); |
| 2779 | | - extension_settings.sd.model = selectedModel.val(); |
| 2780 | | - rememberWorkflowPref('model', extension_settings.sd.model); |
| 2781 | | - saveSettingsDebounced(); |
| 2782 | | - |
| 2783 | | - // Keep the proxy's stored selection current, but catalog updates never |
| 2784 | | - // download, provision, or start a pod. Warm up applies the selection. |
| 2785 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 2786 | | - pushRunpodCatalog(); |
| 2787 | | - } |
| 2788 | | - |
| 2789 | | - if (extension_settings.sd.model && extension_settings.sd.source === sources.electronhub) { |
| 2790 | | - const cachedModel = selectedModel.data('model'); |
| 2791 | | - const models = cachedModel ? [cachedModel] : await loadElectronHubModels(); |
| 2792 | | - ensureElectronHubQualitySelect(models); |
| 2793 | | - } |
| 2794 | | - |
| 2795 | | - switchModelSpecificControls(extension_settings.sd.model); |
| 2796 | | - |
| 2797 | | - const updateRemoteModelSources = [ |
| 2798 | | - sources.auto, |
| 2799 | | - sources.vlad, |
| 2800 | | - sources.extras, |
| 2801 | | - ]; |
| 2802 | | - |
| 2803 | | - if (!updateRemoteModelSources.includes(extension_settings.sd.source)) { |
| 2804 | | - return; |
| 2805 | | - } |
| 2806 | | - |
| 2807 | | - toastr.info('Updating remote model...', 'Please wait'); |
| 2808 | | - if (extension_settings.sd.source === sources.extras) { |
| 2809 | | - await updateExtrasRemoteModel(); |
| 2810 | | - } |
| 2811 | | - if (extension_settings.sd.source === sources.auto || extension_settings.sd.source === sources.vlad) { |
| 2812 | | - await updateAutoRemoteModel(); |
| 2813 | | - } |
| 2814 | | - toastr.success('Model successfully loaded!', 'Image Generation'); |
| 2815 | | -} |
| 2816 | | - |
| 2817 | | -async function getAutoRemoteModel() { |
| 2818 | | - try { |
| 2819 | | - const result = await fetch('/api/sd/get-model', { |
| 2820 | | - method: 'POST', |
| 2821 | | - headers: getRequestHeaders(), |
| 2822 | | - body: JSON.stringify(getSdRequestBody()), |
| 2823 | | - }); |
| 2824 | | - |
| 2825 | | - if (!result.ok) { |
| 2826 | | - throw new Error('SD WebUI returned an error.'); |
| 2827 | | - } |
| 2828 | | - |
| 2829 | | - return await result.text(); |
| 2830 | | - } catch (error) { |
| 2831 | | - console.error(error); |
| 2832 | | - return null; |
| 2833 | | - } |
| 2834 | | -} |
| 2835 | | - |
| 2836 | | -async function getDrawthingsRemoteModel() { |
| 2837 | | - try { |
| 2838 | | - const result = await fetch('/api/sd/drawthings/get-model', { |
| 2839 | | - method: 'POST', |
| 2840 | | - headers: getRequestHeaders(), |
| 2841 | | - body: JSON.stringify(getSdRequestBody()), |
| 2842 | | - }); |
| 2843 | | - |
| 2844 | | - if (!result.ok) { |
| 2845 | | - throw new Error('SD DrawThings API returned an error.'); |
| 2846 | | - } |
| 2847 | | - |
| 2848 | | - return await result.text(); |
| 2849 | | - } catch (error) { |
| 2850 | | - console.error(error); |
| 2851 | | - return null; |
| 2852 | | - } |
| 2853 | | -} |
| 2854 | | - |
| 2855 | | -async function onVaeChange() { |
| 2856 | | - extension_settings.sd.vae = $('#sd_vae').find(':selected').val(); |
| 2857 | | -} |
| 2858 | | - |
| 2859 | | -async function getAutoRemoteUpscalers() { |
| 2860 | | - try { |
| 2861 | | - const result = await fetch('/api/sd/upscalers', { |
| 2862 | | - method: 'POST', |
| 2863 | | - headers: getRequestHeaders(), |
| 2864 | | - body: JSON.stringify(getSdRequestBody()), |
| 2865 | | - }); |
| 2866 | | - |
| 2867 | | - if (!result.ok) { |
| 2868 | | - throw new Error('SD WebUI returned an error.'); |
| 2869 | | - } |
| 2870 | | - |
| 2871 | | - return await result.json(); |
| 2872 | | - } catch (error) { |
| 2873 | | - console.error(error); |
| 2874 | | - return [extension_settings.sd.hr_upscaler]; |
| 2875 | | - } |
| 2876 | | -} |
| 2877 | | - |
| 2878 | | -async function getAutoRemoteSchedulers() { |
| 2879 | | - try { |
| 2880 | | - const result = await fetch('/api/sd/schedulers', { |
| 2881 | | - method: 'POST', |
| 2882 | | - headers: getRequestHeaders(), |
| 2883 | | - body: JSON.stringify(getSdRequestBody()), |
| 2884 | | - }); |
| 2885 | | - |
| 2886 | | - if (!result.ok) { |
| 2887 | | - throw new Error('SD WebUI returned an error.'); |
| 2888 | | - } |
| 2889 | | - |
| 2890 | | - return await result.json(); |
| 2891 | | - } catch (error) { |
| 2892 | | - console.error(error); |
| 2893 | | - return ['N/A']; |
| 2894 | | - } |
| 2895 | | -} |
| 2896 | | - |
| 2897 | | -async function getVladRemoteUpscalers() { |
| 2898 | | - try { |
| 2899 | | - const result = await fetch('/api/sd/sd-next/upscalers', { |
| 2900 | | - method: 'POST', |
| 2901 | | - headers: getRequestHeaders(), |
| 2902 | | - body: JSON.stringify(getSdRequestBody()), |
| 2903 | | - }); |
| 2904 | | - |
| 2905 | | - if (!result.ok) { |
| 2906 | | - throw new Error('SD.Next returned an error.'); |
| 2907 | | - } |
| 2908 | | - |
| 2909 | | - return await result.json(); |
| 2910 | | - } catch (error) { |
| 2911 | | - console.error(error); |
| 2912 | | - return [extension_settings.sd.hr_upscaler]; |
| 2913 | | - } |
| 2914 | | -} |
| 2915 | | - |
| 2916 | | -async function getDrawthingsRemoteUpscalers() { |
| 2917 | | - try { |
| 2918 | | - const result = await fetch('/api/sd/drawthings/get-upscaler', { |
| 2919 | | - method: 'POST', |
| 2920 | | - headers: getRequestHeaders(), |
| 2921 | | - body: JSON.stringify(getSdRequestBody()), |
| 2922 | | - }); |
| 2923 | | - |
| 2924 | | - if (!result.ok) { |
| 2925 | | - throw new Error('SD DrawThings API returned an error.'); |
| 2926 | | - } |
| 2927 | | - |
| 2928 | | - const data = await result.text(); |
| 2929 | | - |
| 2930 | | - return data ? [data] : ['N/A']; |
| 2931 | | - } catch (error) { |
| 2932 | | - console.error(error); |
| 2933 | | - return ['N/A']; |
| 2934 | | - } |
| 2935 | | -} |
| 2936 | | - |
| 2937 | | -async function updateAutoRemoteModel() { |
| 2938 | | - try { |
| 2939 | | - const result = await fetch('/api/sd/set-model', { |
| 2940 | | - method: 'POST', |
| 2941 | | - headers: getRequestHeaders(), |
| 2942 | | - body: JSON.stringify({ ...getSdRequestBody(), model: extension_settings.sd.model }), |
| 2943 | | - }); |
| 2944 | | - |
| 2945 | | - if (!result.ok) { |
| 2946 | | - throw new Error('SD WebUI returned an error.'); |
| 2947 | | - } |
| 2948 | | - |
| 2949 | | - console.log('Model successfully updated on SD WebUI remote.'); |
| 2950 | | - } catch (error) { |
| 2951 | | - console.error(error); |
| 2952 | | - toastr.error(`Could not update SD WebUI model: ${error.message}`); |
| 2953 | | - } |
| 2954 | | -} |
| 2955 | | - |
| 2956 | | -async function updateExtrasRemoteModel() { |
| 2957 | | - const url = new URL(getApiUrl()); |
| 2958 | | - url.pathname = '/api/image/model'; |
| 2959 | | - const getCurrentModelResult = await doExtrasFetch(url, { |
| 2960 | | - method: 'POST', |
| 2961 | | - body: JSON.stringify({ model: extension_settings.sd.model }), |
| 2962 | | - }); |
| 2963 | | - |
| 2964 | | - if (getCurrentModelResult.ok) { |
| 2965 | | - console.log('Model successfully updated on SD remote.'); |
| 2966 | | - } |
| 2967 | | -} |
| 2968 | | - |
| 2969 | | -async function loadSamplers() { |
| 2970 | | - $('#sd_sampler').empty(); |
| 2971 | | - let samplers = []; |
| 2972 | | - |
| 2973 | | - switch (extension_settings.sd.source) { |
| 2974 | | - case sources.extras: |
| 2975 | | - samplers = await loadExtrasSamplers(); |
| 2976 | | - break; |
| 2977 | | - case sources.horde: |
| 2978 | | - samplers = await loadHordeSamplers(); |
| 2979 | | - break; |
| 2980 | | - case sources.auto: |
| 2981 | | - samplers = await loadAutoSamplers(); |
| 2982 | | - break; |
| 2983 | | - case sources.sdcpp: |
| 2984 | | - samplers = await loadSdcppSamplers(); |
| 2985 | | - break; |
| 2986 | | - case sources.drawthings: |
| 2987 | | - samplers = await loadDrawthingsSamplers(); |
| 2988 | | - break; |
| 2989 | | - case sources.novel: |
| 2990 | | - samplers = await loadNovelSamplers(); |
| 2991 | | - break; |
| 2992 | | - case sources.vlad: |
| 2993 | | - samplers = await loadVladSamplers(); |
| 2994 | | - break; |
| 2995 | | - case sources.openai: |
| 2996 | | - samplers = ['N/A']; |
| 2997 | | - break; |
| 2998 | | - case sources.aimlapi: |
| 2999 | | - samplers = ['N/A']; |
| 3000 | | - break; |
| 3001 | | - case sources.comfy: |
| 3002 | | - samplers = await loadComfySamplers(); |
| 3003 | | - break; |
| 3004 | | - case sources.togetherai: |
| 3005 | | - samplers = ['N/A']; |
| 3006 | | - break; |
| 3007 | | - case sources.pollinations: |
| 3008 | | - samplers = ['N/A']; |
| 3009 | | - break; |
| 3010 | | - case sources.stability: |
| 3011 | | - samplers = ['N/A']; |
| 3012 | | - break; |
| 3013 | | - case sources.huggingface: |
| 3014 | | - samplers = ['N/A']; |
| 3015 | | - break; |
| 3016 | | - case sources.chutes: |
| 3017 | | - samplers = ['N/A']; |
| 3018 | | - break; |
| 3019 | | - case sources.electronhub: |
| 3020 | | - samplers = ['N/A']; |
| 3021 | | - break; |
| 3022 | | - case sources.nanogpt: |
| 3023 | | - samplers = ['N/A']; |
| 3024 | | - break; |
| 3025 | | - case sources.bfl: |
| 3026 | | - samplers = ['N/A']; |
| 3027 | | - break; |
| 3028 | | - case sources.falai: |
| 3029 | | - samplers = ['N/A']; |
| 3030 | | - break; |
| 3031 | | - case sources.xai: |
| 3032 | | - samplers = ['N/A']; |
| 3033 | | - break; |
| 3034 | | - case sources.google: |
| 3035 | | - samplers = ['N/A']; |
| 3036 | | - break; |
| 3037 | | - case sources.zai: |
| 3038 | | - samplers = ['N/A']; |
| 3039 | | - break; |
| 3040 | | - case sources.openrouter: |
| 3041 | | - samplers = ['N/A']; |
| 3042 | | - break; |
| 3043 | | - case sources.workersai: |
| 3044 | | - samplers = ['N/A']; |
| 3045 | | - break; |
| 3046 | | - } |
| 3047 | | - |
| 3048 | | - for (const sampler of samplers) { |
| 3049 | | - const option = document.createElement('option'); |
| 3050 | | - option.innerText = sampler; |
| 3051 | | - option.value = sampler; |
| 3052 | | - option.selected = sampler === extension_settings.sd.sampler; |
| 3053 | | - $('#sd_sampler').append(option); |
| 3054 | | - } |
| 3055 | | - |
| 3056 | | - if (!extension_settings.sd.sampler && samplers.length > 0) { |
| 3057 | | - extension_settings.sd.sampler = samplers[0]; |
| 3058 | | - $('#sd_sampler').val(extension_settings.sd.sampler).trigger('change'); |
| 3059 | | - } |
| 3060 | | -} |
| 3061 | | - |
| 3062 | | -async function loadHordeSamplers() { |
| 3063 | | - const result = await fetch('/api/horde/sd-samplers', { |
| 3064 | | - method: 'POST', |
| 3065 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3066 | | - }); |
| 3067 | | - |
| 3068 | | - if (result.ok) { |
| 3069 | | - return await result.json(); |
| 3070 | | - } |
| 3071 | | - |
| 3072 | | - return []; |
| 3073 | | -} |
| 3074 | | - |
| 3075 | | -async function loadExtrasSamplers() { |
| 3076 | | - if (!modules.includes('sd')) { |
| 3077 | | - return []; |
| 3078 | | - } |
| 3079 | | - |
| 3080 | | - const url = new URL(getApiUrl()); |
| 3081 | | - url.pathname = '/api/image/samplers'; |
| 3082 | | - const result = await doExtrasFetch(url); |
| 3083 | | - |
| 3084 | | - if (result.ok) { |
| 3085 | | - const data = await result.json(); |
| 3086 | | - return data.samplers; |
| 3087 | | - } |
| 3088 | | - |
| 3089 | | - return []; |
| 3090 | | -} |
| 3091 | | - |
| 3092 | | -async function loadAutoSamplers() { |
| 3093 | | - if (!extension_settings.sd.auto_url) { |
| 3094 | | - return []; |
| 3095 | | - } |
| 3096 | | - |
| 3097 | | - try { |
| 3098 | | - const result = await fetch('/api/sd/samplers', { |
| 3099 | | - method: 'POST', |
| 3100 | | - headers: getRequestHeaders(), |
| 3101 | | - body: JSON.stringify(getSdRequestBody()), |
| 3102 | | - }); |
| 3103 | | - |
| 3104 | | - if (!result.ok) { |
| 3105 | | - throw new Error('SD WebUI returned an error.'); |
| 3106 | | - } |
| 3107 | | - |
| 3108 | | - return await result.json(); |
| 3109 | | - } catch (error) { |
| 3110 | | - return []; |
| 3111 | | - } |
| 3112 | | -} |
| 3113 | | - |
| 3114 | | -async function loadSdcppModels() { |
| 3115 | | - if (!extension_settings.sd.sdcpp_url) { |
| 3116 | | - return [{ value: '', text: 'N/A' }]; |
| 3117 | | - } |
| 3118 | | - |
| 3119 | | - try { |
| 3120 | | - const result = await fetch('/api/sd/sdcpp/models', { |
| 3121 | | - method: 'POST', |
| 3122 | | - headers: getRequestHeaders(), |
| 3123 | | - body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }), |
| 3124 | | - }); |
| 3125 | | - |
| 3126 | | - if (!result.ok) { |
| 3127 | | - return [{ value: '', text: 'N/A' }]; |
| 3128 | | - } |
| 3129 | | - |
| 3130 | | - const data = await result.json(); |
| 3131 | | - |
| 3132 | | - if (data?.data?.length > 0) { |
| 3133 | | - return data.data.map(model => ({ value: model.id, text: model.name || model.id })); |
| 3134 | | - } |
| 3135 | | - } catch (error) { |
| 3136 | | - console.error('Failed to load sd.cpp models:', error); |
| 3137 | | - } |
| 3138 | | - |
| 3139 | | - return [{ value: '', text: 'N/A' }]; |
| 3140 | | -} |
| 3141 | | - |
| 3142 | | -async function loadSdcppSamplers() { |
| 3143 | | - // The sdcpp server does not provide an API for samplers, so we return the known list. |
| 3144 | | - return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd']; |
| 3145 | | -} |
| 3146 | | - |
| 3147 | | -async function loadDrawthingsSamplers() { |
| 3148 | | - // The app developer doesn't provide an API to get these yet |
| 3149 | | - return [ |
| 3150 | | - 'UniPC', |
| 3151 | | - 'DPM++ 2M Karras', |
| 3152 | | - 'Euler a', |
| 3153 | | - 'DPM++ SDE Karras', |
| 3154 | | - 'PLMS', |
| 3155 | | - 'DDIM', |
| 3156 | | - 'LCM', |
| 3157 | | - 'Euler A Substep', |
| 3158 | | - 'DPM++ SDE Substep', |
| 3159 | | - 'TCD', |
| 3160 | | - ]; |
| 3161 | | -} |
| 3162 | | - |
| 3163 | | -async function loadVladSamplers() { |
| 3164 | | - if (!extension_settings.sd.vlad_url) { |
| 3165 | | - return []; |
| 3166 | | - } |
| 3167 | | - |
| 3168 | | - try { |
| 3169 | | - const result = await fetch('/api/sd/samplers', { |
| 3170 | | - method: 'POST', |
| 3171 | | - headers: getRequestHeaders(), |
| 3172 | | - body: JSON.stringify(getSdRequestBody()), |
| 3173 | | - }); |
| 3174 | | - |
| 3175 | | - if (!result.ok) { |
| 3176 | | - throw new Error('SD.Next returned an error.'); |
| 3177 | | - } |
| 3178 | | - |
| 3179 | | - return await result.json(); |
| 3180 | | - } catch (error) { |
| 3181 | | - return []; |
| 3182 | | - } |
| 3183 | | -} |
| 3184 | | - |
| 3185 | | -async function loadNovelSamplers() { |
| 3186 | | - return [ |
| 3187 | | - 'k_euler_ancestral', |
| 3188 | | - 'k_euler', |
| 3189 | | - 'k_dpmpp_2m', |
| 3190 | | - 'k_dpmpp_sde', |
| 3191 | | - 'k_dpmpp_2s_ancestral', |
| 3192 | | - 'k_dpm_fast', |
| 3193 | | - 'ddim', |
| 3194 | | - ]; |
| 3195 | | -} |
| 3196 | | - |
| 3197 | | -async function loadComfySamplers() { |
| 3198 | | - if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { |
| 3199 | | - return ['N/A']; |
| 3200 | | - } |
| 3201 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 3202 | | - // Do not query /object_info through a cold lazy proxy on page load. |
| 3203 | | - return extension_settings.sd.sampler ? [extension_settings.sd.sampler] : []; |
| 3204 | | - } |
| 3205 | | - if (!extension_settings.sd.comfy_url) { |
| 3206 | | - return []; |
| 3207 | | - } |
| 3208 | | - |
| 3209 | | - try { |
| 3210 | | - const result = await fetch('/api/sd/comfy/samplers', { |
| 3211 | | - method: 'POST', |
| 3212 | | - headers: getRequestHeaders(), |
| 3213 | | - body: JSON.stringify({ |
| 3214 | | - url: extension_settings.sd.comfy_url, |
| 3215 | | - }), |
| 3216 | | - }); |
| 3217 | | - if (!result.ok) { |
| 3218 | | - throw new Error('ComfyUI returned an error.'); |
| 3219 | | - } |
| 3220 | | - return await result.json(); |
| 3221 | | - } catch (error) { |
| 3222 | | - return []; |
| 3223 | | - } |
| 3224 | | -} |
| 3225 | | - |
| 3226 | | -async function loadModels() { |
| 3227 | | - $('#sd_model').empty(); |
| 3228 | | - let models = []; |
| 3229 | | - |
| 3230 | | - switch (extension_settings.sd.source) { |
| 3231 | | - case sources.extras: |
| 3232 | | - models = await loadExtrasModels(); |
| 3233 | | - break; |
| 3234 | | - case sources.horde: |
| 3235 | | - models = await loadHordeModels(); |
| 3236 | | - break; |
| 3237 | | - case sources.auto: |
| 3238 | | - models = await loadAutoModels(); |
| 3239 | | - break; |
| 3240 | | - case sources.sdcpp: |
| 3241 | | - models = await loadSdcppModels(); |
| 3242 | | - break; |
| 3243 | | - case sources.drawthings: |
| 3244 | | - models = await loadDrawthingsModels(); |
| 3245 | | - break; |
| 3246 | | - case sources.novel: |
| 3247 | | - models = await loadNovelModels(); |
| 3248 | | - break; |
| 3249 | | - case sources.vlad: |
| 3250 | | - models = await loadVladModels(); |
| 3251 | | - break; |
| 3252 | | - case sources.openai: |
| 3253 | | - models = await loadOpenAiModels(); |
| 3254 | | - break; |
| 3255 | | - case sources.aimlapi: |
| 3256 | | - models = await loadAimlapiModels(); |
| 3257 | | - break; |
| 3258 | | - case sources.comfy: |
| 3259 | | - models = await loadComfyModels(); |
| 3260 | | - break; |
| 3261 | | - case sources.togetherai: |
| 3262 | | - models = await loadTogetherAIModels(); |
| 3263 | | - break; |
| 3264 | | - case sources.pollinations: |
| 3265 | | - models = await loadPollinationsModels(); |
| 3266 | | - break; |
| 3267 | | - case sources.stability: |
| 3268 | | - models = await loadStabilityModels(); |
| 3269 | | - break; |
| 3270 | | - case sources.huggingface: |
| 3271 | | - models = [{ value: '', text: t`<Enter Model ID above>` }]; |
| 3272 | | - break; |
| 3273 | | - case sources.chutes: |
| 3274 | | - models = await loadChutesModels(); |
| 3275 | | - break; |
| 3276 | | - case sources.electronhub: |
| 3277 | | - models = await loadElectronHubModels(); |
| 3278 | | - break; |
| 3279 | | - case sources.nanogpt: |
| 3280 | | - models = await loadNanoGPTModels(); |
| 3281 | | - break; |
| 3282 | | - case sources.bfl: |
| 3283 | | - models = await loadBflModels(); |
| 3284 | | - break; |
| 3285 | | - case sources.falai: |
| 3286 | | - models = await loadFalaiModels(); |
| 3287 | | - break; |
| 3288 | | - case sources.xai: |
| 3289 | | - models = await loadXAIModels(); |
| 3290 | | - break; |
| 3291 | | - case sources.google: |
| 3292 | | - models = await loadGoogleModels(); |
| 3293 | | - break; |
| 3294 | | - case sources.zai: |
| 3295 | | - models = await loadZaiModels(); |
| 3296 | | - break; |
| 3297 | | - case sources.openrouter: |
| 3298 | | - models = await loadOpenRouterModels(); |
| 3299 | | - break; |
| 3300 | | - case sources.workersai: |
| 3301 | | - models = await loadWorkersAIImageModels(); |
| 3302 | | - break; |
| 3303 | | - } |
| 3304 | | - |
| 3305 | | - if (extension_settings.sd.source === sources.electronhub) { |
| 3306 | | - ensureElectronHubQualitySelect(models); |
| 3307 | | - } |
| 3308 | | - |
| 3309 | | - switchModelSpecificControls(extension_settings.sd.model); |
| 3310 | | - |
| 3311 | | - for (const model of models) { |
| 3312 | | - const option = document.createElement('option'); |
| 3313 | | - option.innerText = model.text; |
| 3314 | | - option.value = model.value; |
| 3315 | | - option.selected = model.value === extension_settings.sd.model; |
| 3316 | | - $(option).data('model', model); |
| 3317 | | - $('#sd_model').append(option); |
| 3318 | | - } |
| 3319 | | - |
| 3320 | | - if (!extension_settings.sd.model && models.length > 0) { |
| 3321 | | - extension_settings.sd.model = models[0].value; |
| 3322 | | - $('#sd_model').val(extension_settings.sd.model).trigger('change'); |
| 3323 | | - } |
| 3324 | | -} |
| 3325 | | - |
| 3326 | | -/** |
| 3327 | | - * Show or hide model-specific controls based on the selected model. |
| 3328 | | - * @param {string} modelId Model ID |
| 3329 | | - */ |
| 3330 | | -function switchModelSpecificControls(modelId) { |
| 3331 | | - const modelControls = $('.sd_settings [data-sd-model]'); |
| 3332 | | - modelControls.hide(); |
| 3333 | | - |
| 3334 | | - if (!modelId) { |
| 3335 | | - return; |
| 3336 | | - } |
| 3337 | | - |
| 3338 | | - modelControls.each(function () { |
| 3339 | | - const models = String($(this).attr('data-sd-model') || '').split(',').map(m => m.trim()); |
| 3340 | | - $(this).toggle(models.some(m => modelId.includes(m))); |
| 3341 | | - }); |
| 3342 | | -} |
| 3343 | | - |
| 3344 | | -/** |
| 3345 | | - * Ensure the Electron Hub quality select is populated based on the selected model. |
| 3346 | | - * @param {any[]} models Array of models |
| 3347 | | - */ |
| 3348 | | -function ensureElectronHubQualitySelect(models) { |
| 3349 | | - try { |
| 3350 | | - const modelId = String(extension_settings.sd.model || ''); |
| 3351 | | - if (!modelId) return; |
| 3352 | | - |
| 3353 | | - const model = Array.isArray(models) ? models.find(m => String(m?.id) === modelId) : undefined; |
| 3354 | | - const qualities = Array.isArray(model?.qualities) ? model.qualities : undefined; |
| 3355 | | - |
| 3356 | | - const $qualityRow = $('#sd_electronhub_quality_row'); |
| 3357 | | - const $select = $('#sd_electronhub_quality'); |
| 3358 | | - |
| 3359 | | - $qualityRow.toggle(!!qualities && qualities.length > 0); |
| 3360 | | - $select.empty(); |
| 3361 | | - |
| 3362 | | - if (!qualities || qualities.length === 0) { |
| 3363 | | - extension_settings.sd.electronhub_quality = undefined; |
| 3364 | | - saveSettingsDebounced(); |
| 3365 | | - return; |
| 3366 | | - } |
| 3367 | | - |
| 3368 | | - for (const q of qualities) { |
| 3369 | | - const opt = document.createElement('option'); |
| 3370 | | - opt.value = String(q); |
| 3371 | | - opt.textContent = String(q); |
| 3372 | | - opt.selected = String(q) === String(extension_settings.sd.electronhub_quality || ''); |
| 3373 | | - $select.append(opt); |
| 3374 | | - } |
| 3375 | | - |
| 3376 | | - if (!$select.val()) { |
| 3377 | | - const first = String(qualities[0]); |
| 3378 | | - extension_settings.sd.electronhub_quality = first; |
| 3379 | | - $select.val(first); |
| 3380 | | - saveSettingsDebounced(); |
| 3381 | | - } |
| 3382 | | - } catch (e) { |
| 3383 | | - console.error(e); |
| 3384 | | - } |
| 3385 | | -} |
| 3386 | | - |
| 3387 | | -async function loadStabilityModels() { |
| 3388 | | - $('#sd_stability_key').toggleClass('success', !!secret_state[SECRET_KEYS.STABILITY]); |
| 3389 | | - |
| 3390 | | - return [ |
| 3391 | | - { value: 'stable-image-ultra', text: 'Stable Image Ultra' }, |
| 3392 | | - { value: 'stable-image-core', text: 'Stable Image Core' }, |
| 3393 | | - { value: 'stable-diffusion-3', text: 'Stable Diffusion 3' }, |
| 3394 | | - ]; |
| 3395 | | -} |
| 3396 | | - |
| 3397 | | -async function loadBflModels() { |
| 3398 | | - $('#sd_bfl_key').toggleClass('success', !!secret_state[SECRET_KEYS.BFL]); |
| 3399 | | - |
| 3400 | | - return [ |
| 3401 | | - { value: 'flux-pro-1.1-ultra', text: 'flux-pro-1.1-ultra' }, |
| 3402 | | - { value: 'flux-pro-1.1', text: 'flux-pro-1.1' }, |
| 3403 | | - { value: 'flux-pro', text: 'flux-pro' }, |
| 3404 | | - { value: 'flux-dev', text: 'flux-dev' }, |
| 3405 | | - ]; |
| 3406 | | -} |
| 3407 | | - |
| 3408 | | -async function loadFalaiModels() { |
| 3409 | | - $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); |
| 3410 | | - |
| 3411 | | - const result = await fetch('/api/sd/falai/models', { |
| 3412 | | - method: 'POST', |
| 3413 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3414 | | - }); |
| 3415 | | - |
| 3416 | | - if (result.ok) { |
| 3417 | | - return await result.json(); |
| 3418 | | - } |
| 3419 | | - |
| 3420 | | - return []; |
| 3421 | | -} |
| 3422 | | - |
| 3423 | | -async function loadXAIModels() { |
| 3424 | | - return [ |
| 3425 | | - { value: 'grok-imagine-image', text: 'grok-imagine-image' }, |
| 3426 | | - { value: 'grok-imagine-image-pro', text: 'grok-imagine-image-pro' }, |
| 3427 | | - ]; |
| 3428 | | -} |
| 3429 | | - |
| 3430 | | -async function loadWorkersAIImageModels() { |
| 3431 | | - $('#sd_cf_workers_key').toggleClass('success', !!secret_state[SECRET_KEYS.WORKERS_AI]); |
| 3432 | | - |
| 3433 | | - if (!secret_state[SECRET_KEYS.WORKERS_AI]) { |
| 3434 | | - return []; |
| 3435 | | - } |
| 3436 | | - |
| 3437 | | - if (!oai_settings.workers_ai_account_id) { |
| 3438 | | - toastr.warning('Workers AI account ID is required. Save it in the "API Connections" panel.', 'Image Generation'); |
| 3439 | | - return []; |
| 3440 | | - } |
| 3441 | | - |
| 3442 | | - const result = await fetch('/api/sd/workersai/models', { |
| 3443 | | - method: 'POST', |
| 3444 | | - headers: getRequestHeaders(), |
| 3445 | | - body: JSON.stringify({ |
| 3446 | | - account_id: oai_settings.workers_ai_account_id, |
| 3447 | | - }), |
| 3448 | | - }); |
| 3449 | | - |
| 3450 | | - if (result.ok) { |
| 3451 | | - return await result.json(); |
| 3452 | | - } |
| 3453 | | - |
| 3454 | | - return []; |
| 3455 | | -} |
| 3456 | | - |
| 3457 | | -async function loadPollinationsModels() { |
| 3458 | | - $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]); |
| 3459 | | - |
| 3460 | | - const result = await fetch('/api/sd/pollinations/models', { |
| 3461 | | - method: 'POST', |
| 3462 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3463 | | - }); |
| 3464 | | - |
| 3465 | | - if (result.ok) { |
| 3466 | | - return await result.json(); |
| 3467 | | - } |
| 3468 | | - |
| 3469 | | - return []; |
| 3470 | | -} |
| 3471 | | - |
| 3472 | | -async function loadTogetherAIModels() { |
| 3473 | | - if (!secret_state[SECRET_KEYS.TOGETHERAI]) { |
| 3474 | | - console.debug('TogetherAI API key is not set.'); |
| 3475 | | - return []; |
| 3476 | | - } |
| 3477 | | - |
| 3478 | | - const result = await fetch('/api/sd/together/models', { |
| 3479 | | - method: 'POST', |
| 3480 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3481 | | - }); |
| 3482 | | - |
| 3483 | | - if (result.ok) { |
| 3484 | | - return await result.json(); |
| 3485 | | - } |
| 3486 | | - |
| 3487 | | - return []; |
| 3488 | | -} |
| 3489 | | - |
| 3490 | | -async function loadChutesModels() { |
| 3491 | | - if (!secret_state[SECRET_KEYS.CHUTES]) { |
| 3492 | | - console.debug('Chutes API key is not set.'); |
| 3493 | | - return []; |
| 3494 | | - } |
| 3495 | | - |
| 3496 | | - const result = await fetch('/api/sd/chutes/models', { |
| 3497 | | - method: 'POST', |
| 3498 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3499 | | - }); |
| 3500 | | - |
| 3501 | | - if (result.ok) { |
| 3502 | | - const models = await result.json(); |
| 3503 | | - console.debug('Loaded Chutes image models:', models); |
| 3504 | | - return models; |
| 3505 | | - } |
| 3506 | | - |
| 3507 | | - console.warn('Failed to load Chutes models:', result.status); |
| 3508 | | - return []; |
| 3509 | | -} |
| 3510 | | - |
| 3511 | | -async function loadElectronHubModels() { |
| 3512 | | - if (!secret_state[SECRET_KEYS.ELECTRONHUB]) { |
| 3513 | | - console.debug('Electron Hub API key is not set.'); |
| 3514 | | - return []; |
| 3515 | | - } |
| 3516 | | - |
| 3517 | | - const result = await fetch('/api/sd/electronhub/models', { |
| 3518 | | - method: 'POST', |
| 3519 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3520 | | - }); |
| 3521 | | - |
| 3522 | | - function getModelName(model) { |
| 3523 | | - const name = String(model?.name || model?.id || ''); |
| 3524 | | - const premium = model?.premium_model ? ' | Premium' : ''; |
| 3525 | | - let price = 'Unknown'; |
| 3526 | | - if (model?.pricing?.type === 'per_image') { |
| 3527 | | - const coeff = Number(model.pricing.coefficient); |
| 3528 | | - if (!isNaN(coeff)) { |
| 3529 | | - price = `$${coeff}/image`; |
| 3530 | | - } |
| 3531 | | - } |
| 3532 | | - return `${name} | ${price}${premium}`; |
| 3533 | | - } |
| 3534 | | - |
| 3535 | | - if (result.ok) { |
| 3536 | | - /** @type {any[]} */ |
| 3537 | | - const data = await result.json(); |
| 3538 | | - return Array.isArray(data) ? data.map(m => ({ ...m, text: getModelName(m) })) : []; |
| 3539 | | - } |
| 3540 | | - |
| 3541 | | - return []; |
| 3542 | | -} |
| 3543 | | - |
| 3544 | | -async function loadNanoGPTModels() { |
| 3545 | | - if (!secret_state[SECRET_KEYS.NANOGPT]) { |
| 3546 | | - console.debug('NanoGPT API key is not set.'); |
| 3547 | | - return []; |
| 3548 | | - } |
| 3549 | | - |
| 3550 | | - const result = await fetch('/api/sd/nanogpt/models', { |
| 3551 | | - method: 'POST', |
| 3552 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3553 | | - }); |
| 3554 | | - |
| 3555 | | - if (result.ok) { |
| 3556 | | - return await result.json(); |
| 3557 | | - } |
| 3558 | | - |
| 3559 | | - return []; |
| 3560 | | -} |
| 3561 | | - |
| 3562 | | -async function loadHordeModels() { |
| 3563 | | - const result = await fetch('/api/horde/sd-models', { |
| 3564 | | - method: 'POST', |
| 3565 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3566 | | - }); |
| 3567 | | - |
| 3568 | | - |
| 3569 | | - if (result.ok) { |
| 3570 | | - const data = await result.json(); |
| 3571 | | - data.sort((a, b) => b.count - a.count); |
| 3572 | | - return data.map(x => ({ |
| 3573 | | - value: x.name, |
| 3574 | | - text: `${x.name} (ETA: ${x.eta}s, Queue: ${x.queued}, Workers: ${x.count})`, |
| 3575 | | - })); |
| 3576 | | - } |
| 3577 | | - |
| 3578 | | - return []; |
| 3579 | | -} |
| 3580 | | - |
| 3581 | | -async function loadExtrasModels() { |
| 3582 | | - if (!modules.includes('sd')) { |
| 3583 | | - return []; |
| 3584 | | - } |
| 3585 | | - |
| 3586 | | - const url = new URL(getApiUrl()); |
| 3587 | | - url.pathname = '/api/image/model'; |
| 3588 | | - const getCurrentModelResult = await doExtrasFetch(url); |
| 3589 | | - |
| 3590 | | - if (getCurrentModelResult.ok) { |
| 3591 | | - const data = await getCurrentModelResult.json(); |
| 3592 | | - extension_settings.sd.model = data.model; |
| 3593 | | - } |
| 3594 | | - |
| 3595 | | - url.pathname = '/api/image/models'; |
| 3596 | | - const getModelsResult = await doExtrasFetch(url); |
| 3597 | | - |
| 3598 | | - if (getModelsResult.ok) { |
| 3599 | | - const data = await getModelsResult.json(); |
| 3600 | | - return data.models.map(x => ({ value: x, text: x })); |
| 3601 | | - } |
| 3602 | | - |
| 3603 | | - return []; |
| 3604 | | -} |
| 3605 | | - |
| 3606 | | -async function loadAutoModels() { |
| 3607 | | - if (!extension_settings.sd.auto_url) { |
| 3608 | | - return []; |
| 3609 | | - } |
| 3610 | | - |
| 3611 | | - try { |
| 3612 | | - const currentModel = await getAutoRemoteModel(); |
| 3613 | | - |
| 3614 | | - if (currentModel) { |
| 3615 | | - extension_settings.sd.model = currentModel; |
| 3616 | | - } |
| 3617 | | - |
| 3618 | | - const result = await fetch('/api/sd/models', { |
| 3619 | | - method: 'POST', |
| 3620 | | - headers: getRequestHeaders(), |
| 3621 | | - body: JSON.stringify(getSdRequestBody()), |
| 3622 | | - }); |
| 3623 | | - |
| 3624 | | - if (!result.ok) { |
| 3625 | | - throw new Error('SD WebUI returned an error.'); |
| 3626 | | - } |
| 3627 | | - |
| 3628 | | - const upscalers = await getAutoRemoteUpscalers(); |
| 3629 | | - |
| 3630 | | - if (Array.isArray(upscalers) && upscalers.length > 0) { |
| 3631 | | - $('#sd_hr_upscaler').empty(); |
| 3632 | | - |
| 3633 | | - for (const upscaler of upscalers) { |
| 3634 | | - const option = document.createElement('option'); |
| 3635 | | - option.innerText = upscaler; |
| 3636 | | - option.value = upscaler; |
| 3637 | | - option.selected = upscaler === extension_settings.sd.hr_upscaler; |
| 3638 | | - $('#sd_hr_upscaler').append(option); |
| 3639 | | - } |
| 3640 | | - } |
| 3641 | | - |
| 3642 | | - return await result.json(); |
| 3643 | | - } catch (error) { |
| 3644 | | - return []; |
| 3645 | | - } |
| 3646 | | -} |
| 3647 | | - |
| 3648 | | -async function loadDrawthingsModels() { |
| 3649 | | - if (!extension_settings.sd.drawthings_url) { |
| 3650 | | - return []; |
| 3651 | | - } |
| 3652 | | - |
| 3653 | | - try { |
| 3654 | | - const currentModel = await getDrawthingsRemoteModel(); |
| 3655 | | - |
| 3656 | | - if (currentModel) { |
| 3657 | | - extension_settings.sd.model = currentModel; |
| 3658 | | - } |
| 3659 | | - |
| 3660 | | - const data = [{ value: currentModel, text: currentModel }]; |
| 3661 | | - |
| 3662 | | - |
| 3663 | | - const upscalers = await getDrawthingsRemoteUpscalers(); |
| 3664 | | - |
| 3665 | | - if (Array.isArray(upscalers) && upscalers.length > 0) { |
| 3666 | | - $('#sd_hr_upscaler').empty(); |
| 3667 | | - |
| 3668 | | - for (const upscaler of upscalers) { |
| 3669 | | - const option = document.createElement('option'); |
| 3670 | | - option.innerText = upscaler; |
| 3671 | | - option.value = upscaler; |
| 3672 | | - option.selected = upscaler === extension_settings.sd.hr_upscaler; |
| 3673 | | - $('#sd_hr_upscaler').append(option); |
| 3674 | | - } |
| 3675 | | - } |
| 3676 | | - |
| 3677 | | - return data; |
| 3678 | | - } catch (error) { |
| 3679 | | - console.log('Error loading DrawThings API models:', error); |
| 3680 | | - return []; |
| 3681 | | - } |
| 3682 | | -} |
| 3683 | | - |
| 3684 | | -async function loadOpenAiModels() { |
| 3685 | | - return [ |
| 3686 | | - { value: 'gpt-image-2', text: 'gpt-image-2' }, |
| 3687 | | - { value: 'gpt-image-2-2026-04-21', text: 'gpt-image-2-2026-04-21' }, |
| 3688 | | - { value: 'gpt-image-1.5', text: 'gpt-image-1.5' }, |
| 3689 | | - { value: 'gpt-image-1-mini', text: 'gpt-image-1-mini' }, |
| 3690 | | - { value: 'gpt-image-1', text: 'gpt-image-1' }, |
| 3691 | | - { value: 'chatgpt-image-latest', text: 'chatgpt-image-latest' }, |
| 3692 | | - { value: 'dall-e-3', text: 'dall-e-3' }, |
| 3693 | | - { value: 'dall-e-2', text: 'dall-e-2' }, |
| 3694 | | - { value: 'sora-2', text: 'sora-2' }, |
| 3695 | | - { value: 'sora-2-pro', text: 'sora-2-pro' }, |
| 3696 | | - ]; |
| 3697 | | -} |
| 3698 | | - |
| 3699 | | -async function loadAimlapiModels() { |
| 3700 | | - $('#sd_aimlapi_key').toggleClass('success', !!secret_state[SECRET_KEYS.AIMLAPI]); |
| 3701 | | - |
| 3702 | | - const result = await fetch('/api/sd/aimlapi/models', { |
| 3703 | | - method: 'POST', |
| 3704 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3705 | | - }); |
| 3706 | | - |
| 3707 | | - if (!result.ok) { |
| 3708 | | - return []; |
| 3709 | | - } |
| 3710 | | - |
| 3711 | | - const json = await result.json(); |
| 3712 | | - |
| 3713 | | - return (json.data || []); |
| 3714 | | -} |
| 3715 | | - |
| 3716 | | -async function loadVladModels() { |
| 3717 | | - if (!extension_settings.sd.vlad_url) { |
| 3718 | | - return []; |
| 3719 | | - } |
| 3720 | | - |
| 3721 | | - try { |
| 3722 | | - const currentModel = await getAutoRemoteModel(); |
| 3723 | | - |
| 3724 | | - if (currentModel) { |
| 3725 | | - extension_settings.sd.model = currentModel; |
| 3726 | | - } |
| 3727 | | - |
| 3728 | | - const result = await fetch('/api/sd/models', { |
| 3729 | | - method: 'POST', |
| 3730 | | - headers: getRequestHeaders(), |
| 3731 | | - body: JSON.stringify(getSdRequestBody()), |
| 3732 | | - }); |
| 3733 | | - |
| 3734 | | - if (!result.ok) { |
| 3735 | | - throw new Error('SD WebUI returned an error.'); |
| 3736 | | - } |
| 3737 | | - |
| 3738 | | - const upscalers = await getVladRemoteUpscalers(); |
| 3739 | | - |
| 3740 | | - if (Array.isArray(upscalers) && upscalers.length > 0) { |
| 3741 | | - $('#sd_hr_upscaler').empty(); |
| 3742 | | - |
| 3743 | | - for (const upscaler of upscalers) { |
| 3744 | | - const option = document.createElement('option'); |
| 3745 | | - option.innerText = upscaler; |
| 3746 | | - option.value = upscaler; |
| 3747 | | - option.selected = upscaler === extension_settings.sd.hr_upscaler; |
| 3748 | | - $('#sd_hr_upscaler').append(option); |
| 3749 | | - } |
| 3750 | | - } |
| 3751 | | - |
| 3752 | | - return await result.json(); |
| 3753 | | - } catch (error) { |
| 3754 | | - return []; |
| 3755 | | - } |
| 3756 | | -} |
| 3757 | | - |
| 3758 | | -async function loadNovelModels() { |
| 3759 | | - return [ |
| 3760 | | - { |
| 3761 | | - value: 'nai-diffusion-4-5-full', |
| 3762 | | - text: 'NAI Diffusion Anime V4.5 (Full)', |
| 3763 | | - }, |
| 3764 | | - { |
| 3765 | | - value: 'nai-diffusion-4-5-curated', |
| 3766 | | - text: 'NAI Diffusion Anime V4.5 (Curated)', |
| 3767 | | - }, |
| 3768 | | - { |
| 3769 | | - value: 'nai-diffusion-4-full', |
| 3770 | | - text: 'NAI Diffusion Anime V4 (Full)', |
| 3771 | | - }, |
| 3772 | | - { |
| 3773 | | - value: 'nai-diffusion-4-curated-preview', |
| 3774 | | - text: 'NAI Diffusion Anime V4 (Curated)', |
| 3775 | | - }, |
| 3776 | | - { |
| 3777 | | - value: 'nai-diffusion-3', |
| 3778 | | - text: 'NAI Diffusion Anime V3', |
| 3779 | | - }, |
| 3780 | | - { |
| 3781 | | - value: 'nai-diffusion-2', |
| 3782 | | - text: 'NAI Diffusion Anime V2', |
| 3783 | | - }, |
| 3784 | | - { |
| 3785 | | - value: 'nai-diffusion-furry-3', |
| 3786 | | - text: 'NAI Diffusion Furry V3', |
| 3787 | | - }, |
| 3788 | | - ]; |
| 3789 | | -} |
| 3790 | | - |
| 3791 | | -async function loadGoogleModels() { |
| 3792 | | - return [ |
| 3793 | | - 'imagen-4.0-generate-001', |
| 3794 | | - 'imagen-4.0-ultra-generate-001', |
| 3795 | | - 'imagen-4.0-fast-generate-001', |
| 3796 | | - 'imagen-4.0-generate-preview-06-06', |
| 3797 | | - 'imagen-4.0-fast-generate-preview-06-06', |
| 3798 | | - 'imagen-4.0-ultra-generate-preview-06-06', |
| 3799 | | - 'imagen-3.0-generate-002', |
| 3800 | | - 'imagen-3.0-generate-001', |
| 3801 | | - 'imagen-3.0-fast-generate-001', |
| 3802 | | - 'imagen-3.0-capability-001', |
| 3803 | | - 'imagegeneration@006', |
| 3804 | | - 'imagegeneration@005', |
| 3805 | | - 'imagegeneration@002', |
| 3806 | | - 'veo-3.1-generate-preview', |
| 3807 | | - 'veo-3.1-fast-generate-preview', |
| 3808 | | - 'veo-3.0-generate-001', |
| 3809 | | - 'veo-3.0-fast-generate-001', |
| 3810 | | - 'veo-2.0-generate-001', |
| 3811 | | - 'veo-2.0-generate-exp', |
| 3812 | | - 'veo-2.0-generate-preview', |
| 3813 | | - ].map(name => ({ value: name, text: name })); |
| 3814 | | -} |
| 3815 | | - |
| 3816 | | -async function loadZaiModels() { |
| 3817 | | - return [ |
| 3818 | | - { value: 'glm-image', text: 'GLM-Image' }, |
| 3819 | | - { value: 'cogview-4-250304', text: 'CogView-4' }, |
| 3820 | | - { value: 'cogvideox-3', text: 'CogVideoX-3' }, |
| 3821 | | - { value: 'viduq1-text', text: 'Viduq1-Text' }, |
| 3822 | | - ]; |
| 3823 | | -} |
| 3824 | | - |
| 3825 | | -async function loadOpenRouterModels() { |
| 3826 | | - const result = await fetch('/api/openrouter/models/image', { |
| 3827 | | - method: 'POST', |
| 3828 | | - headers: getRequestHeaders({ omitContentType: true }), |
| 3829 | | - }); |
| 3830 | | - |
| 3831 | | - if (result.ok) { |
| 3832 | | - return await result.json(); |
| 3833 | | - } |
| 3834 | | - |
| 3835 | | - return []; |
| 3836 | | -} |
| 3837 | | - |
| 3838 | | -function loadNovelSchedulers() { |
| 3839 | | - return ['karras', 'native', 'exponential', 'polyexponential']; |
| 3840 | | -} |
| 3841 | | - |
| 3842 | | -async function loadComfyModels() { |
| 3843 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 3844 | | - // The RunPod pod downloads models on demand: list the configured catalog |
| 3845 | | - // instead of asking the (possibly cold) backend what it has on disk. |
| 3846 | | - return getRunpodModelEntries().map(m => ({ value: m.value, text: m.name || m.value })); |
| 3847 | | - } |
| 3848 | | - if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { |
| 3849 | | - $('#sd_runpod_key').toggleClass('success', !!secret_state[SECRET_KEYS.COMFY_RUNPOD]); |
| 3850 | | - return [ |
| 3851 | | - { value: '', text: 'N/A' }, |
| 3852 | | - ]; |
| 3853 | | - } |
| 3854 | | - if (!extension_settings.sd.comfy_url) { |
| 3855 | | - return []; |
| 3856 | | - } |
| 3857 | | - |
| 3858 | | - try { |
| 3859 | | - const result = await fetch('/api/sd/comfy/models', { |
| 3860 | | - method: 'POST', |
| 3861 | | - headers: getRequestHeaders(), |
| 3862 | | - body: JSON.stringify({ |
| 3863 | | - url: extension_settings.sd.comfy_url, |
| 3864 | | - }), |
| 3865 | | - }); |
| 3866 | | - if (!result.ok) { |
| 3867 | | - throw new Error('ComfyUI returned an error.'); |
| 3868 | | - } |
| 3869 | | - return await result.json(); |
| 3870 | | - } catch (error) { |
| 3871 | | - return []; |
| 3872 | | - } |
| 3873 | | -} |
| 3874 | | - |
| 3875 | | -async function loadSchedulers() { |
| 3876 | | - $('#sd_scheduler').empty(); |
| 3877 | | - let schedulers = []; |
| 3878 | | - |
| 3879 | | - switch (extension_settings.sd.source) { |
| 3880 | | - case sources.extras: |
| 3881 | | - schedulers = ['N/A']; |
| 3882 | | - break; |
| 3883 | | - case sources.horde: |
| 3884 | | - schedulers = ['N/A']; |
| 3885 | | - break; |
| 3886 | | - case sources.auto: |
| 3887 | | - schedulers = await getAutoRemoteSchedulers(); |
| 3888 | | - break; |
| 3889 | | - case sources.sdcpp: |
| 3890 | | - schedulers = await loadSdcppSchedulers(); |
| 3891 | | - break; |
| 3892 | | - case sources.novel: |
| 3893 | | - schedulers = loadNovelSchedulers(); |
| 3894 | | - break; |
| 3895 | | - case sources.vlad: |
| 3896 | | - schedulers = ['N/A']; |
| 3897 | | - break; |
| 3898 | | - case sources.drawthings: |
| 3899 | | - schedulers = ['N/A']; |
| 3900 | | - break; |
| 3901 | | - case sources.openai: |
| 3902 | | - schedulers = ['N/A']; |
| 3903 | | - break; |
| 3904 | | - case sources.aimlapi: |
| 3905 | | - schedulers = ['N/A']; |
| 3906 | | - break; |
| 3907 | | - case sources.togetherai: |
| 3908 | | - schedulers = ['N/A']; |
| 3909 | | - break; |
| 3910 | | - case sources.pollinations: |
| 3911 | | - schedulers = ['N/A']; |
| 3912 | | - break; |
| 3913 | | - case sources.comfy: |
| 3914 | | - schedulers = await loadComfySchedulers(); |
| 3915 | | - break; |
| 3916 | | - case sources.stability: |
| 3917 | | - schedulers = ['N/A']; |
| 3918 | | - break; |
| 3919 | | - case sources.huggingface: |
| 3920 | | - schedulers = ['N/A']; |
| 3921 | | - break; |
| 3922 | | - case sources.chutes: |
| 3923 | | - schedulers = ['N/A']; |
| 3924 | | - break; |
| 3925 | | - case sources.electronhub: |
| 3926 | | - schedulers = ['N/A']; |
| 3927 | | - break; |
| 3928 | | - case sources.nanogpt: |
| 3929 | | - schedulers = ['N/A']; |
| 3930 | | - break; |
| 3931 | | - case sources.bfl: |
| 3932 | | - schedulers = ['N/A']; |
| 3933 | | - break; |
| 3934 | | - case sources.falai: |
| 3935 | | - schedulers = ['N/A']; |
| 3936 | | - break; |
| 3937 | | - case sources.xai: |
| 3938 | | - schedulers = ['N/A']; |
| 3939 | | - break; |
| 3940 | | - case sources.google: |
| 3941 | | - schedulers = ['N/A']; |
| 3942 | | - break; |
| 3943 | | - case sources.zai: |
| 3944 | | - schedulers = ['N/A']; |
| 3945 | | - break; |
| 3946 | | - case sources.openrouter: |
| 3947 | | - schedulers = ['N/A']; |
| 3948 | | - break; |
| 3949 | | - case sources.workersai: |
| 3950 | | - schedulers = ['N/A']; |
| 3951 | | - break; |
| 3952 | | - } |
| 3953 | | - |
| 3954 | | - for (const scheduler of schedulers) { |
| 3955 | | - const option = document.createElement('option'); |
| 3956 | | - option.innerText = scheduler; |
| 3957 | | - option.value = scheduler; |
| 3958 | | - option.selected = scheduler === extension_settings.sd.scheduler; |
| 3959 | | - $('#sd_scheduler').append(option); |
| 3960 | | - } |
| 3961 | | - |
| 3962 | | - if (!extension_settings.sd.scheduler && schedulers.length > 0 && schedulers[0] !== 'N/A') { |
| 3963 | | - extension_settings.sd.scheduler = schedulers[0]; |
| 3964 | | - $('#sd_scheduler').val(extension_settings.sd.scheduler).trigger('change'); |
| 3965 | | - } |
| 3966 | | -} |
| 3967 | | - |
| 3968 | | -async function loadComfySchedulers() { |
| 3969 | | - if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { |
| 3970 | | - return ['N/A']; |
| 3971 | | - } |
| 3972 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 3973 | | - // Do not query /object_info through a cold lazy proxy on page load. |
| 3974 | | - return extension_settings.sd.scheduler ? [extension_settings.sd.scheduler] : []; |
| 3975 | | - } |
| 3976 | | - if (!extension_settings.sd.comfy_url) { |
| 3977 | | - return []; |
| 3978 | | - } |
| 3979 | | - |
| 3980 | | - try { |
| 3981 | | - const result = await fetch('/api/sd/comfy/schedulers', { |
| 3982 | | - method: 'POST', |
| 3983 | | - headers: getRequestHeaders(), |
| 3984 | | - body: JSON.stringify({ |
| 3985 | | - url: extension_settings.sd.comfy_url, |
| 3986 | | - }), |
| 3987 | | - }); |
| 3988 | | - if (!result.ok) { |
| 3989 | | - throw new Error('ComfyUI returned an error.'); |
| 3990 | | - } |
| 3991 | | - return await result.json(); |
| 3992 | | - } catch (error) { |
| 3993 | | - return []; |
| 3994 | | - } |
| 3995 | | -} |
| 3996 | | - |
| 3997 | | -async function loadSdcppSchedulers() { |
| 3998 | | - // The sdcpp server does not provide an API for schedulers, so we return the known list. |
| 3999 | | - return ['discrete', 'karras', 'exponential', 'ays', 'gits', 'smoothstep', 'sgm_uniform', 'simple', 'kl_optimal', 'lcm']; |
| 4000 | | -} |
| 4001 | | - |
| 4002 | | -async function loadVaes() { |
| 4003 | | - $('#sd_vae').empty(); |
| 4004 | | - let vaes = []; |
| 4005 | | - |
| 4006 | | - switch (extension_settings.sd.source) { |
| 4007 | | - case sources.extras: |
| 4008 | | - vaes = ['N/A']; |
| 4009 | | - break; |
| 4010 | | - case sources.horde: |
| 4011 | | - vaes = ['N/A']; |
| 4012 | | - break; |
| 4013 | | - case sources.auto: |
| 4014 | | - vaes = await loadAutoVaes(); |
| 4015 | | - break; |
| 4016 | | - case sources.sdcpp: |
| 4017 | | - vaes = ['N/A']; |
| 4018 | | - break; |
| 4019 | | - case sources.novel: |
| 4020 | | - vaes = ['N/A']; |
| 4021 | | - break; |
| 4022 | | - case sources.vlad: |
| 4023 | | - vaes = ['N/A']; |
| 4024 | | - break; |
| 4025 | | - case sources.drawthings: |
| 4026 | | - vaes = ['N/A']; |
| 4027 | | - break; |
| 4028 | | - case sources.openai: |
| 4029 | | - vaes = ['N/A']; |
| 4030 | | - break; |
| 4031 | | - case sources.aimlapi: |
| 4032 | | - vaes = ['N/A']; |
| 4033 | | - break; |
| 4034 | | - case sources.togetherai: |
| 4035 | | - vaes = ['N/A']; |
| 4036 | | - break; |
| 4037 | | - case sources.pollinations: |
| 4038 | | - vaes = ['N/A']; |
| 4039 | | - break; |
| 4040 | | - case sources.comfy: |
| 4041 | | - vaes = await loadComfyVaes(); |
| 4042 | | - break; |
| 4043 | | - case sources.stability: |
| 4044 | | - vaes = ['N/A']; |
| 4045 | | - break; |
| 4046 | | - case sources.huggingface: |
| 4047 | | - vaes = ['N/A']; |
| 4048 | | - break; |
| 4049 | | - case sources.chutes: |
| 4050 | | - vaes = ['N/A']; |
| 4051 | | - break; |
| 4052 | | - case sources.electronhub: |
| 4053 | | - vaes = ['N/A']; |
| 4054 | | - break; |
| 4055 | | - case sources.nanogpt: |
| 4056 | | - vaes = ['N/A']; |
| 4057 | | - break; |
| 4058 | | - case sources.bfl: |
| 4059 | | - vaes = ['N/A']; |
| 4060 | | - break; |
| 4061 | | - case sources.falai: |
| 4062 | | - vaes = ['N/A']; |
| 4063 | | - break; |
| 4064 | | - case sources.xai: |
| 4065 | | - vaes = ['N/A']; |
| 4066 | | - break; |
| 4067 | | - case sources.google: |
| 4068 | | - vaes = ['N/A']; |
| 4069 | | - break; |
| 4070 | | - case sources.zai: |
| 4071 | | - vaes = ['N/A']; |
| 4072 | | - break; |
| 4073 | | - case sources.openrouter: |
| 4074 | | - vaes = ['N/A']; |
| 4075 | | - break; |
| 4076 | | - case sources.workersai: |
| 4077 | | - vaes = ['N/A']; |
| 4078 | | - break; |
| 4079 | | - } |
| 4080 | | - |
| 4081 | | - for (const vae of vaes) { |
| 4082 | | - const option = document.createElement('option'); |
| 4083 | | - option.innerText = vae; |
| 4084 | | - option.value = vae; |
| 4085 | | - option.selected = vae === extension_settings.sd.vae; |
| 4086 | | - $('#sd_vae').append(option); |
| 4087 | | - } |
| 4088 | | - |
| 4089 | | - // Snap to the first real entry when nothing is selected OR the saved value |
| 4090 | | - // is not offered (otherwise the dropdown displays the first option while |
| 4091 | | - // generations keep sending the stale saved value). |
| 4092 | | - if (vaes.length > 0 && vaes[0] !== 'N/A' && !vaes.includes(extension_settings.sd.vae)) { |
| 4093 | | - extension_settings.sd.vae = vaes[0]; |
| 4094 | | - $('#sd_vae').val(extension_settings.sd.vae).trigger('change'); |
| 4095 | | - } |
| 4096 | | -} |
| 4097 | | - |
| 4098 | | -async function loadAutoVaes() { |
| 4099 | | - if (!extension_settings.sd.auto_url) { |
| 4100 | | - return ['N/A']; |
| 4101 | | - } |
| 4102 | | - |
| 4103 | | - try { |
| 4104 | | - const result = await fetch('/api/sd/vaes', { |
| 4105 | | - method: 'POST', |
| 4106 | | - headers: getRequestHeaders(), |
| 4107 | | - body: JSON.stringify(getSdRequestBody()), |
| 4108 | | - }); |
| 4109 | | - |
| 4110 | | - if (!result.ok) { |
| 4111 | | - throw new Error('SD WebUI returned an error.'); |
| 4112 | | - } |
| 4113 | | - |
| 4114 | | - const data = await result.json(); |
| 4115 | | - Array.isArray(data) && data.unshift(placeholderVae); |
| 4116 | | - return data; |
| 4117 | | - } catch (error) { |
| 4118 | | - return ['N/A']; |
| 4119 | | - } |
| 4120 | | -} |
| 4121 | | - |
| 4122 | | -async function loadComfyVaes() { |
| 4123 | | - if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { |
| 4124 | | - return ['N/A']; |
| 4125 | | - } |
| 4126 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url)) { |
| 4127 | | - // Derive the VAE list from the catalog's download manifests (dest under |
| 4128 | | - // vae/) instead of querying the pod, which may be cold. |
| 4129 | | - const vaes = getRunpodCatalog() |
| 4130 | | - .flatMap(m => parseRunpodFiles(m.downloads)) |
| 4131 | | - .filter(f => /^vae\//i.test(f.dest)) |
| 4132 | | - .map(f => f.dest.split('/').pop()); |
| 4133 | | - return [...new Set(vaes)]; |
| 4134 | | - } |
| 4135 | | - if (!extension_settings.sd.comfy_url) { |
| 4136 | | - return []; |
| 4137 | | - } |
| 4138 | | - |
| 4139 | | - try { |
| 4140 | | - const result = await fetch('/api/sd/comfy/vaes', { |
| 4141 | | - method: 'POST', |
| 4142 | | - headers: getRequestHeaders(), |
| 4143 | | - body: JSON.stringify({ |
| 4144 | | - url: extension_settings.sd.comfy_url, |
| 4145 | | - }), |
| 4146 | | - }); |
| 4147 | | - if (!result.ok) { |
| 4148 | | - throw new Error('ComfyUI returned an error.'); |
| 4149 | | - } |
| 4150 | | - return await result.json(); |
| 4151 | | - } catch (error) { |
| 4152 | | - return []; |
| 4153 | | - } |
| 4154 | | -} |
| 4155 | | - |
| 4156 | | -async function loadComfyWorkflows() { |
| 4157 | | - try { |
| 4158 | | - $('#sd_comfy_workflow').empty(); |
| 4159 | | - const result = await fetch('/api/sd/comfy/workflows', { |
| 4160 | | - method: 'POST', |
| 4161 | | - headers: getRequestHeaders(), |
| 4162 | | - body: JSON.stringify({ |
| 4163 | | - url: extension_settings.sd.comfy_url, |
| 4164 | | - }), |
| 4165 | | - }); |
| 4166 | | - if (!result.ok) { |
| 4167 | | - throw new Error('ComfyUI returned an error.'); |
| 4168 | | - } |
| 4169 | | - const workflows = await result.json(); |
| 4170 | | - for (const workflow of workflows) { |
| 4171 | | - const option = document.createElement('option'); |
| 4172 | | - option.innerText = workflow; |
| 4173 | | - option.value = workflow; |
| 4174 | | - option.selected = workflow === extension_settings.sd.comfy_workflow; |
| 4175 | | - $('#sd_comfy_workflow').append(option); |
| 4176 | | - } |
| 4177 | | - } catch (error) { |
| 4178 | | - console.error(`Could not load ComfyUI workflows: ${error.message}`); |
| 4179 | | - } |
| 4180 | | -} |
| 4181 | | - |
| 4182 | | -function getGenerationType(prompt) { |
| 4183 | | - // Custom wand entries use the trigger convention 'custom_<id>' and bypass |
| 4184 | | - // the multimodal/free_extend transforms applied to the built-in triggers. |
| 4185 | | - const trimmedPrompt = String(prompt).trim(); |
| 4186 | | - if (Array.isArray(extension_settings.sd.custom_entries)) { |
| 4187 | | - const customEntry = extension_settings.sd.custom_entries.find(e => ('custom_' + e.id) === trimmedPrompt); |
| 4188 | | - if (customEntry) { |
| 4189 | | - return generationMode.CUSTOM; |
| 4190 | | - } |
| 4191 | | - } |
| 4192 | | - |
| 4193 | | - let mode = generationMode.FREE; |
| 4194 | | - |
| 4195 | | - for (const [key, values] of Object.entries(triggerWords)) { |
| 4196 | | - for (const value of values) { |
| 4197 | | - if (value.toLowerCase() === prompt.toLowerCase().trim()) { |
| 4198 | | - mode = Number(key); |
| 4199 | | - break; |
| 4200 | | - } |
| 4201 | | - } |
| 4202 | | - } |
| 4203 | | - |
| 4204 | | - if (extension_settings.sd.multimodal_captioning && multimodalMap[mode] !== undefined) { |
| 4205 | | - mode = multimodalMap[mode]; |
| 4206 | | - } |
| 4207 | | - |
| 4208 | | - if (mode === generationMode.FREE && extension_settings.sd.free_extend) { |
| 4209 | | - mode = generationMode.FREE_EXTENDED; |
| 4210 | | - } |
| 4211 | | - |
| 4212 | | - return mode; |
| 4213 | | -} |
| 4214 | | - |
| 4215 | | -function getQuietPrompt(mode, trigger) { |
| 4216 | | - if (mode === generationMode.CUSTOM) { |
| 4217 | | - const entry = Array.isArray(extension_settings.sd.custom_entries) |
| 4218 | | - ? extension_settings.sd.custom_entries.find(e => ('custom_' + e.id) === String(trigger).trim()) |
| 4219 | | - : undefined; |
| 4220 | | - return entry ? entry.prompt : trigger; |
| 4221 | | - } |
| 4222 | | - |
| 4223 | | - if (mode === generationMode.FREE) { |
| 4224 | | - return trigger; |
| 4225 | | - } |
| 4226 | | - |
| 4227 | | - return stringFormat(extension_settings.sd.prompts[mode], trigger); |
| 4228 | | -} |
| 4229 | | - |
| 4230 | | -/** |
| 4231 | | - * Sanitizes generated prompt for image generation. |
| 4232 | | - * @param {string} str String to process |
| 4233 | | - * @returns {string} Processed reply |
| 4234 | | - */ |
| 4235 | | -function processReply(str) { |
| 4236 | | - if (!str) { |
| 4237 | | - return ''; |
| 4238 | | - } |
| 4239 | | - |
| 4240 | | - if (extension_settings.sd.minimal_prompt_processing) { |
| 4241 | | - // Minimal prompt processing |
| 4242 | | - // JSON and similar should be preserved |
| 4243 | | - str = str.normalize('NFD'); |
| 4244 | | - str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one |
| 4245 | | - str = str.trim(); |
| 4246 | | - return str; |
| 4247 | | - } |
| 4248 | | - |
| 4249 | | - str = str.replaceAll('"', ''); |
| 4250 | | - str = str.replaceAll('“', ''); |
| 4251 | | - str = str.replaceAll('\n', ', '); |
| 4252 | | - str = str.normalize('NFD'); |
| 4253 | | - |
| 4254 | | - // Strip out non-alphanumeric characters barring model syntax exceptions |
| 4255 | | - str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' '); |
| 4256 | | - |
| 4257 | | - str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one |
| 4258 | | - str = str.trim(); |
| 4259 | | - |
| 4260 | | - str = str |
| 4261 | | - .split(',') // list split by commas |
| 4262 | | - .map(x => x.trim()) // trim each entry |
| 4263 | | - .filter(x => x) // remove empty entries |
| 4264 | | - .join(', '); // join it back with proper spacing |
| 4265 | | - |
| 4266 | | - return str; |
| 4267 | | -} |
| 4268 | | - |
| 4269 | | -function getRawLastMessage() { |
| 4270 | | - const getLastUsableMessage = () => { |
| 4271 | | - for (const message of context.chat.slice().reverse()) { |
| 4272 | | - if (message.is_system) { |
| 4273 | | - continue; |
| 4274 | | - } |
| 4275 | | - |
| 4276 | | - return { |
| 4277 | | - mes: message.mes, |
| 4278 | | - original_avatar: message.original_avatar, |
| 4279 | | - }; |
| 4280 | | - } |
| 4281 | | - |
| 4282 | | - toastr.warning('No usable messages found.', 'Image Generation'); |
| 4283 | | - throw new Error('No usable messages found.'); |
| 4284 | | - }; |
| 4285 | | - |
| 4286 | | - const context = getContext(); |
| 4287 | | - const lastMessage = getLastUsableMessage(); |
| 4288 | | - const character = context.groupId |
| 4289 | | - ? context.characters.find(c => c.avatar === lastMessage.original_avatar) |
| 4290 | | - : context.characters[context.characterId]; |
| 4291 | | - |
| 4292 | | - if (!character) { |
| 4293 | | - console.debug('Character not found, using raw message.'); |
| 4294 | | - return processReply(lastMessage.mes); |
| 4295 | | - } |
| 4296 | | - |
| 4297 | | - return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`; |
| 4298 | | -} |
| 4299 | | - |
| 4300 | | -/** |
| 4301 | | - * Ensure that the selected option exists in the dropdown. |
| 4302 | | - * @param {string} setting Setting key |
| 4303 | | - * @param {string} selector Dropdown selector |
| 4304 | | - * @returns {void} |
| 4305 | | - */ |
| 4306 | | -function ensureSelectionExists(setting, selector) { |
| 4307 | | - /** @type {HTMLSelectElement} */ |
| 4308 | | - const selectElement = document.querySelector(selector); |
| 4309 | | - if (!selectElement) { |
| 4310 | | - return; |
| 4311 | | - } |
| 4312 | | - const options = Array.from(selectElement.options); |
| 4313 | | - const value = extension_settings.sd[setting]; |
| 4314 | | - if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) { |
| 4315 | | - extension_settings.sd[setting] = selectElement.selectedOptions[0].value; |
| 4316 | | - } |
| 4317 | | -} |
| 4318 | | - |
| 4319 | | -/** |
| 4320 | | - * Generates an image based on the given trigger word. |
| 4321 | | - * @param {string} initiator The initiator of the image generation |
| 4322 | | - * @param {Record<string, object>} args Command arguments |
| 4323 | | - * @param {string} trigger Subject trigger word |
| 4324 | | - * @param {string} [message] Chat message |
| 4325 | | - * @param {function} [callback] Callback function |
| 4326 | | - * @returns {Promise<string|undefined>} Image path |
| 4327 | | - * @throws {Error} If the prompt or image generation fails |
| 4328 | | - */ |
| 4329 | | -async function generatePicture(initiator, args, trigger, message, callback) { |
| 4330 | | - if (!trigger || trigger.trim().length === 0) { |
| 4331 | | - console.log('Trigger word empty, aborting'); |
| 4332 | | - return; |
| 4333 | | - } |
| 4334 | | - |
| 4335 | | - if (!isValidState()) { |
| 4336 | | - toastr.warning('Image generation is not available. Check your settings and try again.'); |
| 4337 | | - return; |
| 4338 | | - } |
| 4339 | | - |
| 4340 | | - ensureSelectionExists('sampler', '#sd_sampler'); |
| 4341 | | - ensureSelectionExists('model', '#sd_model'); |
| 4342 | | - |
| 4343 | | - trigger = trigger.trim(); |
| 4344 | | - const generationType = getGenerationType(trigger); |
| 4345 | | - const generationTypeKey = Object.keys(generationMode).find(key => generationMode[key] === generationType); |
| 4346 | | - console.log(`Image generation mode ${generationTypeKey} triggered with "${trigger}"`); |
| 4347 | | - |
| 4348 | | - const quietPrompt = getQuietPrompt(generationType, trigger); |
| 4349 | | - const context = getContext(); |
| 4350 | | - |
| 4351 | | - let characterName = context.groupId |
| 4352 | | - ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString() |
| 4353 | | - : context.characters[context.characterId]?.name; |
| 4354 | | - |
| 4355 | | - if (generationType === generationMode.BACKGROUND) { |
| 4356 | | - const callbackOriginal = callback; |
| 4357 | | - callback = async function (prompt, imagePath, generationType, _negativePromptPrefix, _initiator, prefixedPrompt, format) { |
| 4358 | | - const imgUrl = `url("${encodeURI(imagePath)}")`; |
| 4359 | | - await eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath }); |
| 4360 | | - |
| 4361 | | - if (typeof callbackOriginal === 'function') { |
| 4362 | | - await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format); |
| 4363 | | - } else { |
| 4364 | | - await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format); |
| 4365 | | - } |
| 4366 | | - }; |
| 4367 | | - } |
| 4368 | | - |
| 4369 | | - if (isTrueBoolean(args?.quiet)) { |
| 4370 | | - callback = () => { }; |
| 4371 | | - } |
| 4372 | | - |
| 4373 | | - if (isFalseBoolean(args?.gallery)) { |
| 4374 | | - characterName = ''; |
| 4375 | | - } |
| 4376 | | - |
| 4377 | | - const dimensions = setTypeSpecificDimensions(generationType); |
| 4378 | | - const abortController = new AbortController(); |
| 4379 | | - let negativePromptPrefix = args?.negative || ''; |
| 4380 | | - let imagePath = ''; |
| 4381 | | - |
| 4382 | | - const stopListener = () => abortController.abort('Aborted by user'); |
| 4383 | | - |
| 4384 | | - let loaderHandle = ActionLoaderHandle.EMPTY; |
| 4385 | | - |
| 4386 | | - try { |
| 4387 | | - const combineNegatives = (prefix) => { negativePromptPrefix = combinePrefixes(negativePromptPrefix, prefix); }; |
| 4388 | | - |
| 4389 | | - // Each new generation picks its own reference image (swipes reuse the last one). |
| 4390 | | - pendingReferenceImage = null; |
| 4391 | | - |
| 4392 | | - // generate the text prompt for the image |
| 4393 | | - let prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives); |
| 4394 | | - console.log('Processed image prompt:', prompt); |
| 4395 | | - |
| 4396 | | - // Extension hook for prompt processing |
| 4397 | | - const eventData = { prompt, generationType, message, trigger }; |
| 4398 | | - await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData); |
| 4399 | | - prompt = eventData.prompt; // Allow extensions to modify the prompt |
| 4400 | | - |
| 4401 | | - if (typeof args?._abortController?.addEventListener === 'function') { |
| 4402 | | - args._abortController.addEventListener('abort', stopListener); |
| 4403 | | - } |
| 4404 | | - |
| 4405 | | - // Show non-blocking stoppable toast for this generation |
| 4406 | | - loaderHandle = loader.show({ |
| 4407 | | - blocking: false, |
| 4408 | | - slug: `${MODULE_NAME}-image-generation`, |
| 4409 | | - title: t`Image Generation`, |
| 4410 | | - message: t`Generating an image...`, |
| 4411 | | - onStop: stopListener, |
| 4412 | | - }); |
| 4413 | | - |
| 4414 | | - // generate the image |
| 4415 | | - imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal); |
| 4416 | | - } catch (err) { |
| 4417 | | - // Check if this was an intentional abort by user |
| 4418 | | - if (abortController.signal.aborted) { |
| 4419 | | - console.log('SD: Image generation aborted by user'); |
| 4420 | | - toastr.info('Image generation stopped.', 'Image Generation'); |
| 4421 | | - return; |
| 4422 | | - } |
| 4423 | | - |
| 4424 | | - console.trace(err); |
| 4425 | | - // errors here are most likely due to text generation failure |
| 4426 | | - // sendGenerationRequest mostly deals with its own errors |
| 4427 | | - const reason = err.error?.message || err.message || 'Unknown error'; |
| 4428 | | - const errorText = 'SD prompt text generation failed. ' + reason; |
| 4429 | | - toastr.error(errorText, 'Image Generation'); |
| 4430 | | - throw new Error(errorText); |
| 4431 | | - } finally { |
| 4432 | | - restoreOriginalDimensions(dimensions); |
| 4433 | | - await loaderHandle.hide(); |
| 4434 | | - } |
| 4435 | | - |
| 4436 | | - return imagePath; |
| 4437 | | -} |
| 4438 | | - |
| 4439 | | -/** |
| 4440 | | - * Adjusts image generation dimensions based on the generation type and/or previous media attachment. |
| 4441 | | - * @param {number} generationType The type of image generation to perform, used to determine dimension adjustments |
| 4442 | | - * @param {MediaAttachment} [mediaAttachment] Media attachment to base dimension adjustments on |
| 4443 | | - * @returns {{height: number, width: number}} Previous dimensions before modification |
| 4444 | | - */ |
| 4445 | | -function setTypeSpecificDimensions(generationType, mediaAttachment = null) { |
| 4446 | | - const prevSDHeight = extension_settings.sd.height; |
| 4447 | | - const prevSDWidth = extension_settings.sd.width; |
| 4448 | | - const aspectRatio = extension_settings.sd.width / extension_settings.sd.height; |
| 4449 | | - |
| 4450 | | - // 1. If there's a media attachment, match its previous dimensions |
| 4451 | | - // 2. Face images are always portrait (pun intended) - increase height if needed |
| 4452 | | - // 3. Background images are always landscape - increase width if needed |
| 4453 | | - if (Number.isInteger(mediaAttachment?.width) && Number.isInteger(mediaAttachment?.height)) { |
| 4454 | | - extension_settings.sd.width = mediaAttachment.width; |
| 4455 | | - extension_settings.sd.height = mediaAttachment.height; |
| 4456 | | - } else if ((generationType === generationMode.FACE || generationType === generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) { |
| 4457 | | - // Round to nearest multiple of 64 |
| 4458 | | - extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64; |
| 4459 | | - } else if (generationType === generationMode.BACKGROUND && aspectRatio <= 1) { |
| 4460 | | - // Round to nearest multiple of 64 |
| 4461 | | - extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64; |
| 4462 | | - } |
| 4463 | | - |
| 4464 | | - if (extension_settings.sd.snap) { |
| 4465 | | - // Force to use roughly the same pixel count as before rescaling |
| 4466 | | - const prevPixelCount = prevSDHeight * prevSDWidth; |
| 4467 | | - const newPixelCount = extension_settings.sd.height * extension_settings.sd.width; |
| 4468 | | - |
| 4469 | | - if (prevPixelCount !== newPixelCount) { |
| 4470 | | - const ratio = Math.sqrt(prevPixelCount / newPixelCount); |
| 4471 | | - extension_settings.sd.height = Math.round(extension_settings.sd.height * ratio / 64) * 64; |
| 4472 | | - extension_settings.sd.width = Math.round(extension_settings.sd.width * ratio / 64) * 64; |
| 4473 | | - console.log(`Pixel counts after rescaling: ${prevPixelCount} -> ${newPixelCount} (ratio: ${ratio})`); |
| 4474 | | - |
| 4475 | | - const resolution = resolutionOptions[getClosestKnownResolution()]; |
| 4476 | | - if (resolution) { |
| 4477 | | - extension_settings.sd.height = resolution.height; |
| 4478 | | - extension_settings.sd.width = resolution.width; |
| 4479 | | - console.log('Snap to resolution', JSON.stringify(resolution)); |
| 4480 | | - } else { |
| 4481 | | - console.warn('Snap to resolution failed, using custom dimensions'); |
| 4482 | | - } |
| 4483 | | - } |
| 4484 | | - } |
| 4485 | | - |
| 4486 | | - return { height: prevSDHeight, width: prevSDWidth }; |
| 4487 | | -} |
| 4488 | | - |
| 4489 | | -/** |
| 4490 | | - * Restores the original image generation dimensions after generation is complete. |
| 4491 | | - * @param {{height: number, width: number}} savedParams The original dimensions to restore |
| 4492 | | - */ |
| 4493 | | -function restoreOriginalDimensions(savedParams) { |
| 4494 | | - extension_settings.sd.height = savedParams.height; |
| 4495 | | - extension_settings.sd.width = savedParams.width; |
| 4496 | | -} |
| 4497 | | - |
| 4498 | | -/** |
| 4499 | | - * Generates a prompt for image generation. |
| 4500 | | - * @param {number} generationType The type of image generation to perform. |
| 4501 | | - * @param {string} message A message text to use for the image generation. |
| 4502 | | - * @param {string} trigger A trigger string to use for the image generation. |
| 4503 | | - * @param {string} quietPrompt A quiet prompt to use for the image generation. |
| 4504 | | - * @param {function} combineNegatives A function that combines the negative prompt with other prompts. |
| 4505 | | - * @returns {Promise<string>} - A promise that resolves when the prompt generation completes. |
| 4506 | | - */ |
| 4507 | | -async function getPrompt(generationType, message, trigger, quietPrompt, combineNegatives) { |
| 4508 | | - let prompt; |
| 4509 | | - console.log('getPrompt: Generation mode', generationType, 'triggered with', trigger); |
| 4510 | | - switch (generationType) { |
| 4511 | | - case generationMode.RAW_LAST: |
| 4512 | | - prompt = message || getRawLastMessage(); |
| 4513 | | - break; |
| 4514 | | - case generationMode.FREE: |
| 4515 | | - prompt = generateFreeModePrompt(trigger.trim(), combineNegatives); |
| 4516 | | - break; |
| 4517 | | - case generationMode.FACE_MULTIMODAL: |
| 4518 | | - case generationMode.CHARACTER_MULTIMODAL: |
| 4519 | | - case generationMode.USER_MULTIMODAL: |
| 4520 | | - prompt = await generateMultimodalPrompt(generationType, quietPrompt); |
| 4521 | | - break; |
| 4522 | | - default: |
| 4523 | | - prompt = await generatePrompt(quietPrompt); |
| 4524 | | - break; |
| 4525 | | - } |
| 4526 | | - |
| 4527 | | - if (generationType === generationMode.FREE_EXTENDED) { |
| 4528 | | - prompt = generateFreeModePrompt(prompt.trim(), combineNegatives); |
| 4529 | | - } |
| 4530 | | - |
| 4531 | | - if (generationType !== generationMode.FREE) { |
| 4532 | | - prompt = await refinePrompt(prompt); |
| 4533 | | - } |
| 4534 | | - |
| 4535 | | - return prompt; |
| 4536 | | -} |
| 4537 | | - |
| 4538 | | -/** |
| 4539 | | - * Generates a free prompt with a character-specific prompt prefix support. |
| 4540 | | - * @param {string} trigger - The prompt to use for the image generation. |
| 4541 | | - * @param {function} combineNegatives - A function that combines the negative prompt with other prompts. |
| 4542 | | - * @returns {string} |
| 4543 | | - */ |
| 4544 | | -function generateFreeModePrompt(trigger, combineNegatives) { |
| 4545 | | - return trigger |
| 4546 | | - .replace(/^char(\s|,)|{{charPrefix}}/gi, (_, suffix) => { |
| 4547 | | - const getLastCharacterKey = () => { |
| 4548 | | - if (typeof this_chid !== 'undefined') { |
| 4549 | | - return getCharaFilename(this_chid); |
| 4550 | | - } |
| 4551 | | - const context = getContext(); |
| 4552 | | - for (let i = context.chat.length - 1; i >= 0; i--) { |
| 4553 | | - const message = context.chat[i]; |
| 4554 | | - if (!message.is_user && !message.is_system && typeof message.original_avatar === 'string') { |
| 4555 | | - return message.original_avatar.replace(/\.[^/.]+$/, ''); |
| 4556 | | - } |
| 4557 | | - } |
| 4558 | | - return ''; |
| 4559 | | - }; |
| 4560 | | - |
| 4561 | | - const key = getLastCharacterKey(); |
| 4562 | | - const value = (extension_settings.sd.character_prompts[key] || '').trim(); |
| 4563 | | - const negativeValue = (extension_settings.sd.character_negative_prompts[key] || '').trim(); |
| 4564 | | - typeof combineNegatives === 'function' && negativeValue ? combineNegatives(negativeValue) : void 0; |
| 4565 | | - return value ? combinePrefixes(value, (suffix || '')) : ''; |
| 4566 | | - }); |
| 4567 | | -} |
| 4568 | | - |
| 4569 | | -/** |
| 4570 | | - * Generates a prompt using multimodal captioning. |
| 4571 | | - * @param {number} generationType - The type of image generation to perform. |
| 4572 | | - * @param {string} quietPrompt - The prompt to use for the image generation. |
| 4573 | | - */ |
| 4574 | | -async function generateMultimodalPrompt(generationType, quietPrompt) { |
| 4575 | | - let avatarUrl; |
| 4576 | | - |
| 4577 | | - if (generationType === generationMode.USER_MULTIMODAL) { |
| 4578 | | - avatarUrl = getUserAvatarUrl(); |
| 4579 | | - } |
| 4580 | | - |
| 4581 | | - if (generationType === generationMode.CHARACTER_MULTIMODAL || generationType === generationMode.FACE_MULTIMODAL) { |
| 4582 | | - avatarUrl = getCharacterAvatarUrl(); |
| 4583 | | - } |
| 4584 | | - |
| 4585 | | - try { |
| 4586 | | - const toast = toastr.info('Generating multimodal caption...', 'Image Generation'); |
| 4587 | | - const response = await fetch(avatarUrl); |
| 4588 | | - |
| 4589 | | - if (!response.ok) { |
| 4590 | | - throw new Error('Could not fetch avatar image.'); |
| 4591 | | - } |
| 4592 | | - |
| 4593 | | - const avatarBlob = await response.blob(); |
| 4594 | | - const avatarBase64 = await getBase64Async(avatarBlob); |
| 4595 | | - |
| 4596 | | - const caption = await getMultimodalCaption(avatarBase64, quietPrompt); |
| 4597 | | - toastr.clear(toast); |
| 4598 | | - |
| 4599 | | - if (!caption) { |
| 4600 | | - throw new Error('No caption returned from the API.'); |
| 4601 | | - } |
| 4602 | | - |
| 4603 | | - return caption; |
| 4604 | | - } catch (error) { |
| 4605 | | - console.error(error); |
| 4606 | | - toastr.error('Multimodal captioning failed. Please try again.', 'Image Generation'); |
| 4607 | | - throw new Error('Multimodal captioning failed.'); |
| 4608 | | - } |
| 4609 | | -} |
| 4610 | | - |
| 4611 | | -function getCharacterAvatarUrl() { |
| 4612 | | - const context = getContext(); |
| 4613 | | - |
| 4614 | | - if (context.groupId) { |
| 4615 | | - const groupMembers = context.groups.find(x => x.id === context.groupId)?.members; |
| 4616 | | - const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar; |
| 4617 | | - const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)] : null; |
| 4618 | | - const avatarToUse = lastMessageAvatar || randomMemberAvatar; |
| 4619 | | - return formatCharacterAvatar(avatarToUse); |
| 4620 | | - } else { |
| 4621 | | - return getCharacterAvatar(context.characterId); |
| 4622 | | - } |
| 4623 | | -} |
| 4624 | | - |
| 4625 | | -function getUserAvatarUrl() { |
| 4626 | | - return getUserAvatar(user_avatar); |
| 4627 | | -} |
| 4628 | | - |
| 4629 | | -/** |
| 4630 | | - * Generates a prompt using the main LLM API. |
| 4631 | | - * @param {string} quietPrompt - The prompt to use for the image generation. |
| 4632 | | - * @returns {Promise<string>} - A promise that resolves when the prompt generation completes. |
| 4633 | | - */ |
| 4634 | | -async function generatePrompt(quietPrompt) { |
| 4635 | | - const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation'); |
| 4636 | | - const profileId = extension_settings.sd.prompt_generation_profile; |
| 4637 | | - let reply; |
| 4638 | | - |
| 4639 | | - // When the workflow uses a reference image and there is more than one to choose |
| 4640 | | - // from, have the same LLM request return the selection along with the prompt. |
| 4641 | | - const refCandidates = await getEligibleReferenceImages(); |
| 4642 | | - if (refCandidates.length === 1) { |
| 4643 | | - pendingReferenceImage = refCandidates[0]; |
| 4644 | | - } |
| 4645 | | - const combineReferenceSelection = refCandidates.length > 1; |
| 4646 | | - const effectiveQuietPrompt = combineReferenceSelection |
| 4647 | | - ? quietPrompt + '\n' + buildReferenceSelectionAddendum(refCandidates) |
| 4648 | | - : quietPrompt; |
| 4649 | | - |
| 4650 | | - try { |
| 4651 | | - reply = profileId |
| 4652 | | - ? await withConnectionProfile(profileId, () => generateQuietPrompt({ quietPrompt: effectiveQuietPrompt })) |
| 4653 | | - : await generateQuietPrompt({ quietPrompt: effectiveQuietPrompt }); |
| 4654 | | - } finally { |
| 4655 | | - toastr.clear(toast); |
| 4656 | | - } |
| 4657 | | - |
| 4658 | | - if (combineReferenceSelection) { |
| 4659 | | - const { cleaned, selected } = extractReferenceSelection(String(reply ?? ''), refCandidates); |
| 4660 | | - reply = cleaned; |
| 4661 | | - // No/invalid selection -> leave unset; the workflow builder retries with a dedicated call. |
| 4662 | | - pendingReferenceImage = selected; |
| 4663 | | - console.log('SD: reference image selected with the image prompt:', selected?.tag ?? '(none)'); |
| 4664 | | - } |
| 4665 | | - |
| 4666 | | - const processedReply = processReply(reply); |
| 4667 | | - |
| 4668 | | - if (!processedReply) { |
| 4669 | | - toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation'); |
| 4670 | | - throw new Error('Prompt generation failed.'); |
| 4671 | | - } |
| 4672 | | - |
| 4673 | | - return processedReply; |
| 4674 | | -} |
| 4675 | | - |
| 4676 | | -/** |
| 4677 | | - * Runs a callback with a specific Connection Manager profile temporarily active, |
| 4678 | | - * then restores the previously active profile. This lets the image prompt be |
| 4679 | | - * generated by the chosen LLM *with the full chat context* (via generateQuietPrompt), |
| 4680 | | - * instead of a context-free one-off request. |
| 4681 | | - * |
| 4682 | | - * The switch is only performed when a real connection profile is currently active |
| 4683 | | - * (so it can be reliably restored). When no profile is active — i.e. the user drives |
| 4684 | | - * the API panel manually — switching to a profile could not be undone without |
| 4685 | | - * clobbering those manual settings, so we leave the active model in place and warn once. |
| 4686 | | - * |
| 4687 | | - * @param {string} targetProfileId Profile to activate for the duration of the callback. |
| 4688 | | - * @param {() => Promise<any>} callback Work to run while the target profile is active. |
| 4689 | | - * @returns {Promise<any>} The callback's result. |
| 4690 | | - */ |
| 4691 | | -async function withConnectionProfile(targetProfileId, callback) { |
| 4692 | | - const select = /** @type {HTMLSelectElement} */ (document.getElementById('connection_profiles')); |
| 4693 | | - const connectionManager = extension_settings.connectionManager; |
| 4694 | | - const currentProfileId = connectionManager?.selectedProfile; |
| 4695 | | - |
| 4696 | | - const canSwitch = !!select |
| 4697 | | - && !!connectionManager |
| 4698 | | - && Array.isArray(connectionManager.profiles) |
| 4699 | | - && connectionManager.profiles.some(p => p.id === targetProfileId) |
| 4700 | | - && Array.from(select.options).some(o => o.value === targetProfileId) |
| 4701 | | - && !!currentProfileId // a real profile is active, so it can be restored afterwards |
| 4702 | | - && currentProfileId !== targetProfileId; |
| 4703 | | - |
| 4704 | | - if (!canSwitch) { |
| 4705 | | - // Selected but no base profile to restore from -> use the active model and warn once. |
| 4706 | | - if (targetProfileId && !currentProfileId && !promptProfileWarnedNoBaseProfile) { |
| 4707 | | - promptProfileWarnedNoBaseProfile = true; |
| 4708 | | - toastr.info('The image-prompt LLM profile is only applied while a connection profile is active (so the original can be restored). Using the current model.', 'Image Generation'); |
| 4709 | | - } |
| 4710 | | - return await callback(); |
| 4711 | | - } |
| 4712 | | - |
| 4713 | | - const switchToProfile = async (profileId) => { |
| 4714 | | - const loaded = new Promise(resolve => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve)); |
| 4715 | | - const index = Array.from(select.options).findIndex(o => o.value === profileId); |
| 4716 | | - select.selectedIndex = index >= 0 ? index : 0; |
| 4717 | | - select.dispatchEvent(new Event('change')); |
| 4718 | | - // Wait for the profile's commands to finish applying (don't hang forever if the event never fires). |
| 4719 | | - await Promise.race([loaded, delay(10000)]); |
| 4720 | | - // Applying a profile reconnects the API, which is asynchronous. Generating before the |
| 4721 | | - // connection is re-established fails instantly, so wait for it to come back up |
| 4722 | | - // (mirrors the built-in /profile command). rejectOnTimeout:false -> proceed anyway after the timeout. |
| 4723 | | - await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100, { rejectOnTimeout: false }); |
| 4724 | | - }; |
| 4725 | | - |
| 4726 | | - // Background image-prompt work must not disturb foreground features that follow the user's |
| 4727 | | - // active connection profile (notably text prompt-cache keepalives). |
| 4728 | | - await eventSource.emit(event_types.CONNECTION_PROFILE_TEMPORARY_STARTED); |
| 4729 | | - try { |
| 4730 | | - await switchToProfile(targetProfileId); |
| 4731 | | - return await callback(); |
| 4732 | | - } finally { |
| 4733 | | - try { |
| 4734 | | - await switchToProfile(currentProfileId); |
| 4735 | | - } catch (err) { |
| 4736 | | - console.error('SD: failed to restore the previous connection profile after image-prompt generation', err); |
| 4737 | | - } finally { |
| 4738 | | - await eventSource.emit(event_types.CONNECTION_PROFILE_TEMPORARY_ENDED); |
| 4739 | | - } |
| 4740 | | - } |
| 4741 | | -} |
| 4742 | | - |
| 4743 | | -/** |
| 4744 | | - * Sends a request to image generation endpoint and processes the result. |
| 4745 | | - * @param {number} generationType Type of image generation |
| 4746 | | - * @param {string} prompt Prompt to be used for image generation |
| 4747 | | - * @param {string} additionalNegativePrefix Additional negative prompt to be used for image generation |
| 4748 | | - * @param {string} characterName Name of the character |
| 4749 | | - * @param {function} callback Callback function to be called after image generation |
| 4750 | | - * @param {string} initiator The initiator of the image generation |
| 4751 | | - * @param {AbortSignal} signal Abort signal to cancel the request |
| 4752 | | - * @returns |
| 4753 | | - */ |
| 4754 | | -async function sendGenerationRequest(generationType, prompt, additionalNegativePrefix, characterName, callback, initiator, signal) { |
| 4755 | | - const noCharPrefix = [generationMode.FREE, generationMode.BACKGROUND, generationMode.USER, generationMode.USER_MULTIMODAL, generationMode.FREE_EXTENDED]; |
| 4756 | | - const isCharChat = this_chid !== undefined && !selected_group; |
| 4757 | | - const ignoreNoCharForSwipe = initiator === initiators.swipe && isCharChat; |
| 4758 | | - |
| 4759 | | - const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType); |
| 4760 | | - |
| 4761 | | - /** |
| 4762 | | - * Performs a single image generation attempt against the live extension_settings.sd config. |
| 4763 | | - * Reads settings live so it can be retried after applying a fallback preset. |
| 4764 | | - * @param {AbortSignal} attemptSignal Abort signal to cancel the request. |
| 4765 | | - * @returns {Promise<{result: {format: string, data: string}, prefixedPrompt: string}>} |
| 4766 | | - * @throws {Error} On failure or when the endpoint returns no image data. |
| 4767 | | - */ |
| 4768 | | - async function attemptImageGeneration(attemptSignal) { |
| 4769 | | - const prefix = skipCharPrefix |
| 4770 | | - ? extension_settings.sd.prompt_prefix |
| 4771 | | - : combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()); |
| 4772 | | - |
| 4773 | | - const negativePrefix = skipCharPrefix |
| 4774 | | - ? extension_settings.sd.negative_prompt |
| 4775 | | - : combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix()); |
| 4776 | | - |
| 4777 | | - const prefixedPrompt = substituteParams(combinePrefixes(prefix, prompt, '{prompt}')); |
| 4778 | | - const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix)); |
| 4779 | | - |
| 4780 | | - let result = { format: '', data: '' }; |
| 4781 | | - switch (extension_settings.sd.source) { |
| 4782 | | - case sources.extras: |
| 4783 | | - result = await generateExtrasImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4784 | | - break; |
| 4785 | | - case sources.horde: |
| 4786 | | - result = await generateHordeImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4787 | | - break; |
| 4788 | | - case sources.vlad: |
| 4789 | | - result = await generateAutoImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4790 | | - break; |
| 4791 | | - case sources.drawthings: |
| 4792 | | - result = await generateDrawthingsImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4793 | | - break; |
| 4794 | | - case sources.auto: |
| 4795 | | - result = await generateAutoImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4796 | | - break; |
| 4797 | | - case sources.sdcpp: |
| 4798 | | - result = await generateSdcppImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4799 | | - break; |
| 4800 | | - case sources.novel: |
| 4801 | | - result = await generateNovelImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4802 | | - break; |
| 4803 | | - case sources.openai: |
| 4804 | | - result = await generateOpenAiImage(prefixedPrompt, attemptSignal); |
| 4805 | | - break; |
| 4806 | | - case sources.aimlapi: |
| 4807 | | - result = await generateAimlapiImage(prefixedPrompt, attemptSignal); |
| 4808 | | - break; |
| 4809 | | - case sources.comfy: |
| 4810 | | - switch (extension_settings.sd.comfy_type) { |
| 4811 | | - case comfyTypes.runpod_serverless: |
| 4812 | | - result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4813 | | - break; |
| 4814 | | - case comfyTypes.standard: |
| 4815 | | - result = await generateComfyImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4816 | | - break; |
| 4817 | | - default: |
| 4818 | | - throw new Error('Unknown comfyUI server type.'); |
| 4819 | | - } |
| 4820 | | - break; |
| 4821 | | - case sources.togetherai: |
| 4822 | | - result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4823 | | - break; |
| 4824 | | - case sources.pollinations: |
| 4825 | | - result = await generatePollinationsImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4826 | | - break; |
| 4827 | | - case sources.stability: |
| 4828 | | - result = await generateStabilityImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4829 | | - break; |
| 4830 | | - case sources.huggingface: |
| 4831 | | - result = await generateHuggingFaceImage(prefixedPrompt, attemptSignal); |
| 4832 | | - break; |
| 4833 | | - case sources.chutes: |
| 4834 | | - result = await generateChutesImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4835 | | - break; |
| 4836 | | - case sources.electronhub: |
| 4837 | | - result = await generateElectronHubImage(prefixedPrompt, attemptSignal); |
| 4838 | | - break; |
| 4839 | | - case sources.nanogpt: |
| 4840 | | - result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4841 | | - break; |
| 4842 | | - case sources.bfl: |
| 4843 | | - result = await generateBflImage(prefixedPrompt, attemptSignal); |
| 4844 | | - break; |
| 4845 | | - case sources.falai: |
| 4846 | | - result = await generateFalaiImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4847 | | - break; |
| 4848 | | - case sources.xai: |
| 4849 | | - result = await generateXAIImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4850 | | - break; |
| 4851 | | - case sources.google: |
| 4852 | | - result = await generateGoogleImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4853 | | - break; |
| 4854 | | - case sources.zai: |
| 4855 | | - result = await generateZaiImage(prefixedPrompt, attemptSignal); |
| 4856 | | - break; |
| 4857 | | - case sources.openrouter: |
| 4858 | | - result = await generateOpenRouterImage(prefixedPrompt, attemptSignal); |
| 4859 | | - break; |
| 4860 | | - case sources.workersai: |
| 4861 | | - result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, attemptSignal); |
| 4862 | | - break; |
| 4863 | | - } |
| 4864 | | - |
| 4865 | | - if (!result.data) { |
| 4866 | | - throw new Error('Endpoint did not return image data.'); |
| 4867 | | - } |
| 4868 | | - |
| 4869 | | - return { result, prefixedPrompt }; |
| 4870 | | - } |
| 4871 | | - |
| 4872 | | - const currentChatId = getCurrentChatId(); |
| 4873 | | - const fallbackChain = extension_settings.sd.settings_fallback_enabled ? getConfiguredPresetChain() : []; |
| 4874 | | - let genOutput; |
| 4875 | | - |
| 4876 | | - if (fallbackChain.length > 0) { |
| 4877 | | - // Chain mode: try every preset in order. Locally-hosted backends are probed |
| 4878 | | - // first so a powered-off server is skipped after ~1.5s instead of stalling |
| 4879 | | - // the attempt. The live settings are restored afterward either way. |
| 4880 | | - const restore = snapshotSdSettings(); |
| 4881 | | - let lastError = new Error('No provider in the fallback chain was reachable.'); |
| 4882 | | - try { |
| 4883 | | - for (const entry of fallbackChain) { |
| 4884 | | - applySdSettingsSnapshot(entry.preset); |
| 4885 | | - |
| 4886 | | - if (!(await isCurrentSourceReachable())) { |
| 4887 | | - console.warn(`SD: chain entry "${entry.name}" is not reachable, skipping`); |
| 4888 | | - toastr.warning(`Provider "${entry.name}" is not reachable, trying the next one…`, 'Image Generation'); |
| 4889 | | - continue; |
| 4890 | | - } |
| 4891 | | - |
| 4892 | | - try { |
| 4893 | | - genOutput = await attemptImageGeneration(signal); |
| 4894 | | - break; |
| 4895 | | - } catch (err) { |
| 4896 | | - if (signal?.aborted) { |
| 4897 | | - console.log('SD: Image generation aborted by user'); |
| 4898 | | - toastr.info('Image generation stopped.', 'Image Generation'); |
| 4899 | | - return; |
| 4900 | | - } |
| 4901 | | - lastError = err; |
| 4902 | | - console.error(`SD: generation with chain entry "${entry.name}" failed`, err); |
| 4903 | | - toastr.warning(`Provider "${entry.name}" failed, trying the next one…`, 'Image Generation'); |
| 4904 | | - } |
| 4905 | | - } |
| 4906 | | - } finally { |
| 4907 | | - applySdSettingsSnapshot(restore); |
| 4908 | | - } |
| 4909 | | - |
| 4910 | | - if (!genOutput) { |
| 4911 | | - toastr.error('Image generation failed for every provider in the fallback chain.' + '\n\n' + String(lastError), 'Image Generation'); |
| 4912 | | - return; |
| 4913 | | - } |
| 4914 | | - } else { |
| 4915 | | - try { |
| 4916 | | - genOutput = await attemptImageGeneration(signal); |
| 4917 | | - } catch (err) { |
| 4918 | | - // Check if this was an intentional abort by user |
| 4919 | | - if (signal?.aborted) { |
| 4920 | | - console.log('SD: Image generation aborted by user'); |
| 4921 | | - toastr.info('Image generation stopped.', 'Image Generation'); |
| 4922 | | - return; |
| 4923 | | - } |
| 4924 | | - |
| 4925 | | - console.error('Image generation request error: ', err); |
| 4926 | | - toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation'); |
| 4927 | | - return; |
| 4928 | | - } |
| 4929 | | - } |
| 4930 | | - |
| 4931 | | - const { result, prefixedPrompt } = genOutput; |
| 4932 | | - |
| 4933 | | - if (currentChatId !== getCurrentChatId()) { |
| 4934 | | - console.warn('Chat changed, aborting SD result saving'); |
| 4935 | | - toastr.warning('Chat changed, generated image discarded.', 'Image Generation'); |
| 4936 | | - return; |
| 4937 | | - } |
| 4938 | | - |
| 4939 | | - const filename = characterName ? `${characterName}_${humanizedDateTime()}` : humanizedDateTime(); |
| 4940 | | - const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format); |
| 4941 | | - callback |
| 4942 | | - ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format) |
| 4943 | | - : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format); |
| 4944 | | - return base64Image; |
| 4945 | | -} |
| 4946 | | - |
| 4947 | | -/** |
| 4948 | | - * Generates an image using the TogetherAI API. |
| 4949 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 4950 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 4951 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 4952 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 4953 | | - */ |
| 4954 | | -async function generateTogetherAIImage(prompt, negativePrompt, signal) { |
| 4955 | | - const result = await fetch('/api/sd/together/generate', { |
| 4956 | | - method: 'POST', |
| 4957 | | - headers: getRequestHeaders(), |
| 4958 | | - signal: signal, |
| 4959 | | - body: JSON.stringify({ |
| 4960 | | - prompt: prompt, |
| 4961 | | - negative_prompt: negativePrompt, |
| 4962 | | - model: extension_settings.sd.model, |
| 4963 | | - steps: extension_settings.sd.steps, |
| 4964 | | - width: extension_settings.sd.width, |
| 4965 | | - height: extension_settings.sd.height, |
| 4966 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 4967 | | - }), |
| 4968 | | - }); |
| 4969 | | - |
| 4970 | | - if (result.ok) { |
| 4971 | | - return await result.json(); |
| 4972 | | - } else { |
| 4973 | | - const text = await result.text(); |
| 4974 | | - throw new Error(text); |
| 4975 | | - } |
| 4976 | | -} |
| 4977 | | - |
| 4978 | | -/** |
| 4979 | | - * Generates an image using the Pollinations API. |
| 4980 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 4981 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 4982 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 4983 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 4984 | | - */ |
| 4985 | | -async function generatePollinationsImage(prompt, negativePrompt, signal) { |
| 4986 | | - const result = await fetch('/api/sd/pollinations/generate', { |
| 4987 | | - method: 'POST', |
| 4988 | | - headers: getRequestHeaders(), |
| 4989 | | - signal: signal, |
| 4990 | | - body: JSON.stringify({ |
| 4991 | | - prompt: prompt, |
| 4992 | | - negative_prompt: negativePrompt, |
| 4993 | | - model: extension_settings.sd.model, |
| 4994 | | - width: extension_settings.sd.width, |
| 4995 | | - height: extension_settings.sd.height, |
| 4996 | | - enhance: extension_settings.sd.pollinations_enhance, |
| 4997 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 4998 | | - }), |
| 4999 | | - }); |
| 5000 | | - |
| 5001 | | - if (result.ok) { |
| 5002 | | - const data = await result.json(); |
| 5003 | | - return { format: data?.format, data: data?.image }; |
| 5004 | | - } else { |
| 5005 | | - const text = await result.text(); |
| 5006 | | - throw new Error(text); |
| 5007 | | - } |
| 5008 | | -} |
| 5009 | | - |
| 5010 | | -/** |
| 5011 | | - * Generates an "extras" image using a provided prompt and other settings. |
| 5012 | | - * |
| 5013 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5014 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5015 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5016 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5017 | | - */ |
| 5018 | | -async function generateExtrasImage(prompt, negativePrompt, signal) { |
| 5019 | | - const url = new URL(getApiUrl()); |
| 5020 | | - url.pathname = '/api/image'; |
| 5021 | | - const result = await doExtrasFetch(url, { |
| 5022 | | - method: 'POST', |
| 5023 | | - headers: { |
| 5024 | | - 'Content-Type': 'application/json', |
| 5025 | | - }, |
| 5026 | | - signal: signal, |
| 5027 | | - body: JSON.stringify({ |
| 5028 | | - prompt: prompt, |
| 5029 | | - sampler: extension_settings.sd.sampler, |
| 5030 | | - steps: extension_settings.sd.steps, |
| 5031 | | - scale: extension_settings.sd.scale, |
| 5032 | | - width: extension_settings.sd.width, |
| 5033 | | - height: extension_settings.sd.height, |
| 5034 | | - negative_prompt: negativePrompt, |
| 5035 | | - restore_faces: !!extension_settings.sd.restore_faces, |
| 5036 | | - enable_hr: !!extension_settings.sd.enable_hr, |
| 5037 | | - karras: !!extension_settings.sd.horde_karras, |
| 5038 | | - hr_upscaler: extension_settings.sd.hr_upscaler, |
| 5039 | | - hr_scale: extension_settings.sd.hr_scale, |
| 5040 | | - denoising_strength: extension_settings.sd.denoising_strength, |
| 5041 | | - hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps, |
| 5042 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5043 | | - }), |
| 5044 | | - }); |
| 5045 | | - |
| 5046 | | - if (result.ok) { |
| 5047 | | - const data = await result.json(); |
| 5048 | | - return { format: 'jpg', data: data.image }; |
| 5049 | | - } else { |
| 5050 | | - const text = await result.text(); |
| 5051 | | - throw new Error(text); |
| 5052 | | - } |
| 5053 | | -} |
| 5054 | | - |
| 5055 | | -/** |
| 5056 | | - * Gets an aspect ratio for Stability that is the closest to the given width and height. |
| 5057 | | - * @param {number} width Target width |
| 5058 | | - * @param {number} height Target height |
| 5059 | | - * @param {'google'|'stability'|'zai'|'xai'} source Source of the request, used to determine aspect ratio |
| 5060 | | - * @returns {string} Closest aspect ratio as a string |
| 5061 | | - */ |
| 5062 | | -function getClosestAspectRatio(width, height, source) { |
| 5063 | | - function getAspectRatios() { |
| 5064 | | - switch (source) { |
| 5065 | | - case 'stability': |
| 5066 | | - return { |
| 5067 | | - '16:9': 16 / 9, |
| 5068 | | - '1:1': 1, |
| 5069 | | - '21:9': 21 / 9, |
| 5070 | | - '2:3': 2 / 3, |
| 5071 | | - '3:2': 3 / 2, |
| 5072 | | - '4:5': 4 / 5, |
| 5073 | | - '5:4': 5 / 4, |
| 5074 | | - '9:16': 9 / 16, |
| 5075 | | - '9:21': 9 / 21, |
| 5076 | | - }; |
| 5077 | | - case 'google': |
| 5078 | | - return { |
| 5079 | | - '1:1': 1, |
| 5080 | | - '16:9': 16 / 9, |
| 5081 | | - '9:16': 9 / 16, |
| 5082 | | - '4:3': 4 / 3, |
| 5083 | | - '3:4': 3 / 4, |
| 5084 | | - }; |
| 5085 | | - case 'zai': |
| 5086 | | - return { |
| 5087 | | - '1:1': 1, |
| 5088 | | - '16:9': 16 / 9, |
| 5089 | | - '9:16': 9 / 16, |
| 5090 | | - }; |
| 5091 | | - case 'xai': |
| 5092 | | - return { |
| 5093 | | - '1:1': 1, |
| 5094 | | - '3:4': 3 / 4, |
| 5095 | | - '4:3': 4 / 3, |
| 5096 | | - '9:16': 9 / 16, |
| 5097 | | - '16:9': 16 / 9, |
| 5098 | | - '2:3': 2 / 3, |
| 5099 | | - '3:2': 3 / 2, |
| 5100 | | - '9:19.5': 9 / 19.5, |
| 5101 | | - '19.5:9': 19.5 / 9, |
| 5102 | | - '9:20': 9 / 20, |
| 5103 | | - '20:9': 20 / 9, |
| 5104 | | - '1:2': 1 / 2, |
| 5105 | | - '2:1': 2 / 1, |
| 5106 | | - }; |
| 5107 | | - default: |
| 5108 | | - console.warn(`Unknown source "${source}" for aspect ratio calculation.`); |
| 5109 | | - return null; |
| 5110 | | - } |
| 5111 | | - } |
| 5112 | | - |
| 5113 | | - const aspectRatios = getAspectRatios() || { '1:1': 1 }; |
| 5114 | | - |
| 5115 | | - const aspectRatio = width / height; |
| 5116 | | - |
| 5117 | | - let closestAspectRatio = Object.keys(aspectRatios)[0]; |
| 5118 | | - let minDiff = Math.abs(aspectRatio - aspectRatios[closestAspectRatio]); |
| 5119 | | - |
| 5120 | | - for (const key in aspectRatios) { |
| 5121 | | - const diff = Math.abs(aspectRatio - aspectRatios[key]); |
| 5122 | | - if (diff < minDiff) { |
| 5123 | | - minDiff = diff; |
| 5124 | | - closestAspectRatio = key; |
| 5125 | | - } |
| 5126 | | - } |
| 5127 | | - |
| 5128 | | - return closestAspectRatio; |
| 5129 | | -} |
| 5130 | | - |
| 5131 | | -/** |
| 5132 | | - * Get closest size for Electron Hub |
| 5133 | | - * @param {number} width - The width of the image |
| 5134 | | - * @param {number} height - The height of the image |
| 5135 | | - * @param {string[]} sizes - Available sizes |
| 5136 | | - * @returns {Promise<string>} - The closest size |
| 5137 | | - */ |
| 5138 | | -async function getClosestSize(width, height, sizes = []) { |
| 5139 | | - const sizesData = []; |
| 5140 | | - |
| 5141 | | - if (Array.isArray(sizes) && sizes.length > 0) { |
| 5142 | | - sizesData.push(...sizes); |
| 5143 | | - } else if (extension_settings.sd.source === sources.electronhub) { |
| 5144 | | - const response = await fetch('/api/sd/electronhub/sizes', { |
| 5145 | | - method: 'POST', |
| 5146 | | - headers: getRequestHeaders(), |
| 5147 | | - body: JSON.stringify({ |
| 5148 | | - model: extension_settings.sd.model, |
| 5149 | | - }), |
| 5150 | | - }); |
| 5151 | | - if (!response.ok) { |
| 5152 | | - const text = await response.text(); |
| 5153 | | - throw new Error(text); |
| 5154 | | - } |
| 5155 | | - const result = await response.json(); |
| 5156 | | - sizesData.push(...result.sizes); |
| 5157 | | - } else { |
| 5158 | | - return null; |
| 5159 | | - } |
| 5160 | | - |
| 5161 | | - const targetWidth = Number(width); |
| 5162 | | - const targetHeight = Number(height); |
| 5163 | | - |
| 5164 | | - if (isNaN(targetWidth) || isNaN(targetHeight)) { |
| 5165 | | - return null; |
| 5166 | | - } |
| 5167 | | - |
| 5168 | | - const targetAspect = targetWidth / targetHeight; |
| 5169 | | - const targetResolution = targetWidth * targetHeight; |
| 5170 | | - |
| 5171 | | - const closestSize = sizesData.reduce((closest, size) => { |
| 5172 | | - if (!size || typeof size !== 'string') { |
| 5173 | | - return closest; |
| 5174 | | - } |
| 5175 | | - const sizeParts = size.split('x'); |
| 5176 | | - if (sizeParts.length !== 2) { |
| 5177 | | - return closest; |
| 5178 | | - } |
| 5179 | | - |
| 5180 | | - const sizeWidth = Number(sizeParts[0]); |
| 5181 | | - const sizeHeight = Number(sizeParts[1]); |
| 5182 | | - |
| 5183 | | - if (isNaN(sizeWidth) || isNaN(sizeHeight)) { |
| 5184 | | - return closest; |
| 5185 | | - } |
| 5186 | | - |
| 5187 | | - const aspectDiff = Math.abs((sizeWidth / sizeHeight) - targetAspect) / targetAspect; |
| 5188 | | - const resolutionDiff = Math.abs(sizeWidth * sizeHeight - targetResolution) / targetResolution; |
| 5189 | | - const diff = aspectDiff + resolutionDiff; |
| 5190 | | - |
| 5191 | | - return diff < closest.diff ? { size, diff } : closest; |
| 5192 | | - }, { size: null, diff: Infinity }); |
| 5193 | | - |
| 5194 | | - const size = closestSize.size; |
| 5195 | | - return size; |
| 5196 | | -} |
| 5197 | | - |
| 5198 | | -/** |
| 5199 | | - * Generates an image using Stability AI. |
| 5200 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5201 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5202 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5203 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5204 | | - */ |
| 5205 | | -async function generateStabilityImage(prompt, negativePrompt, signal) { |
| 5206 | | - const IMAGE_FORMAT = 'png'; |
| 5207 | | - const PROMPT_LIMIT = 10000; |
| 5208 | | - |
| 5209 | | - try { |
| 5210 | | - const response = await fetch('/api/sd/stability/generate', { |
| 5211 | | - method: 'POST', |
| 5212 | | - headers: getRequestHeaders(), |
| 5213 | | - signal: signal, |
| 5214 | | - body: JSON.stringify({ |
| 5215 | | - model: extension_settings.sd.model, |
| 5216 | | - payload: { |
| 5217 | | - prompt: prompt.slice(0, PROMPT_LIMIT), |
| 5218 | | - negative_prompt: negativePrompt.slice(0, PROMPT_LIMIT), |
| 5219 | | - aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'), |
| 5220 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5221 | | - style_preset: extension_settings.sd.stability_style_preset, |
| 5222 | | - output_format: IMAGE_FORMAT, |
| 5223 | | - }, |
| 5224 | | - }), |
| 5225 | | - }); |
| 5226 | | - |
| 5227 | | - if (!response.ok) { |
| 5228 | | - throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| 5229 | | - } |
| 5230 | | - |
| 5231 | | - const base64Image = await response.text(); |
| 5232 | | - |
| 5233 | | - return { |
| 5234 | | - format: IMAGE_FORMAT, |
| 5235 | | - data: base64Image, |
| 5236 | | - }; |
| 5237 | | - } catch (error) { |
| 5238 | | - console.error('Error generating image with Stability AI:', error); |
| 5239 | | - throw error; |
| 5240 | | - } |
| 5241 | | -} |
| 5242 | | - |
| 5243 | | -/** |
| 5244 | | - * Generates a "horde" image using the provided prompt and configuration settings. |
| 5245 | | - * |
| 5246 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5247 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5248 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5249 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5250 | | - */ |
| 5251 | | -async function generateHordeImage(prompt, negativePrompt, signal) { |
| 5252 | | - const result = await fetch('/api/horde/generate-image', { |
| 5253 | | - method: 'POST', |
| 5254 | | - headers: getRequestHeaders(), |
| 5255 | | - signal: signal, |
| 5256 | | - body: JSON.stringify({ |
| 5257 | | - prompt: prompt, |
| 5258 | | - sampler: extension_settings.sd.sampler, |
| 5259 | | - steps: extension_settings.sd.steps, |
| 5260 | | - scale: extension_settings.sd.scale, |
| 5261 | | - width: extension_settings.sd.width, |
| 5262 | | - height: extension_settings.sd.height, |
| 5263 | | - negative_prompt: negativePrompt, |
| 5264 | | - model: extension_settings.sd.model, |
| 5265 | | - nsfw: extension_settings.sd.horde_nsfw, |
| 5266 | | - restore_faces: !!extension_settings.sd.restore_faces, |
| 5267 | | - enable_hr: !!extension_settings.sd.enable_hr, |
| 5268 | | - sanitize: !!extension_settings.sd.horde_sanitize, |
| 5269 | | - clip_skip: extension_settings.sd.clip_skip, |
| 5270 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5271 | | - }), |
| 5272 | | - }); |
| 5273 | | - |
| 5274 | | - if (result.ok) { |
| 5275 | | - const data = await result.text(); |
| 5276 | | - return { format: 'webp', data: data }; |
| 5277 | | - } else { |
| 5278 | | - const text = await result.text(); |
| 5279 | | - throw new Error(text); |
| 5280 | | - } |
| 5281 | | -} |
| 5282 | | - |
| 5283 | | -/** |
| 5284 | | - * Generates an image in SD WebUI API using the provided prompt and configuration settings. |
| 5285 | | - * |
| 5286 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5287 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5288 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5289 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5290 | | - */ |
| 5291 | | -async function generateAutoImage(prompt, negativePrompt, signal) { |
| 5292 | | - const isValidVae = extension_settings.sd.vae && !['N/A', placeholderVae].includes(extension_settings.sd.vae); |
| 5293 | | - let payload = { |
| 5294 | | - ...getSdRequestBody(), |
| 5295 | | - prompt: prompt, |
| 5296 | | - negative_prompt: negativePrompt, |
| 5297 | | - sampler_name: extension_settings.sd.sampler, |
| 5298 | | - scheduler: extension_settings.sd.scheduler, |
| 5299 | | - steps: extension_settings.sd.steps, |
| 5300 | | - cfg_scale: extension_settings.sd.scale, |
| 5301 | | - width: extension_settings.sd.width, |
| 5302 | | - height: extension_settings.sd.height, |
| 5303 | | - restore_faces: !!extension_settings.sd.restore_faces, |
| 5304 | | - enable_hr: !!extension_settings.sd.enable_hr, |
| 5305 | | - hr_upscaler: extension_settings.sd.hr_upscaler, |
| 5306 | | - hr_scale: extension_settings.sd.hr_scale, |
| 5307 | | - hr_additional_modules: [], |
| 5308 | | - denoising_strength: extension_settings.sd.denoising_strength, |
| 5309 | | - hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps, |
| 5310 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5311 | | - override_settings: { |
| 5312 | | - CLIP_stop_at_last_layers: extension_settings.sd.clip_skip, |
| 5313 | | - sd_vae: isValidVae ? extension_settings.sd.vae : undefined, |
| 5314 | | - forge_additional_modules: isValidVae ? [extension_settings.sd.vae] : undefined, // For SD Forge |
| 5315 | | - }, |
| 5316 | | - override_settings_restore_afterwards: true, |
| 5317 | | - clip_skip: extension_settings.sd.clip_skip, // For SD.Next |
| 5318 | | - save_images: true, |
| 5319 | | - send_images: true, |
| 5320 | | - do_not_save_grid: false, |
| 5321 | | - do_not_save_samples: false, |
| 5322 | | - }; |
| 5323 | | - |
| 5324 | | - // Conditionally add the ADetailer if adetailer_face is enabled |
| 5325 | | - if (extension_settings.sd.adetailer_face) { |
| 5326 | | - payload = deepMerge(payload, { |
| 5327 | | - alwayson_scripts: { |
| 5328 | | - ADetailer: { |
| 5329 | | - args: [ |
| 5330 | | - true, // ad_enable |
| 5331 | | - true, // skip_img2img |
| 5332 | | - { |
| 5333 | | - 'ad_model': 'face_yolov8n.pt', |
| 5334 | | - }, |
| 5335 | | - ], |
| 5336 | | - }, |
| 5337 | | - }, |
| 5338 | | - }); |
| 5339 | | - } |
| 5340 | | - |
| 5341 | | - // Make the fetch call with the payload |
| 5342 | | - const result = await fetch('/api/sd/generate', { |
| 5343 | | - method: 'POST', |
| 5344 | | - headers: getRequestHeaders(), |
| 5345 | | - signal: signal, |
| 5346 | | - body: JSON.stringify(payload), |
| 5347 | | - }); |
| 5348 | | - |
| 5349 | | - if (result.ok) { |
| 5350 | | - const data = await result.json(); |
| 5351 | | - return { format: 'png', data: data.images[0] }; |
| 5352 | | - } else { |
| 5353 | | - const text = await result.text(); |
| 5354 | | - throw new Error(text); |
| 5355 | | - } |
| 5356 | | -} |
| 5357 | | - |
| 5358 | | -/** |
| 5359 | | - * Generates an image using stable-diffusion.cpp server API. |
| 5360 | | - * |
| 5361 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5362 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5363 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5364 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5365 | | - */ |
| 5366 | | -async function generateSdcppImage(prompt, negativePrompt, signal) { |
| 5367 | | - const payload = { |
| 5368 | | - url: extension_settings.sd.sdcpp_url, |
| 5369 | | - model: extension_settings.sd.model || undefined, |
| 5370 | | - prompt: prompt, |
| 5371 | | - negative_prompt: negativePrompt, |
| 5372 | | - steps: extension_settings.sd.steps, |
| 5373 | | - cfg_scale: extension_settings.sd.scale, |
| 5374 | | - width: extension_settings.sd.width, |
| 5375 | | - height: extension_settings.sd.height, |
| 5376 | | - batch_size: 1, |
| 5377 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5378 | | - }; |
| 5379 | | - |
| 5380 | | - if (extension_settings.sd.sampler && extension_settings.sd.sampler !== 'N/A') { |
| 5381 | | - payload.sampler_name = extension_settings.sd.sampler; |
| 5382 | | - } |
| 5383 | | - |
| 5384 | | - if (extension_settings.sd.scheduler && extension_settings.sd.scheduler !== 'N/A') { |
| 5385 | | - payload.scheduler = extension_settings.sd.scheduler; |
| 5386 | | - } |
| 5387 | | - |
| 5388 | | - if (Number.isFinite(extension_settings.sd.clip_skip)) { |
| 5389 | | - payload.clip_skip = extension_settings.sd.clip_skip; |
| 5390 | | - } |
| 5391 | | - |
| 5392 | | - const result = await fetch('/api/sd/sdcpp/generate', { |
| 5393 | | - method: 'POST', |
| 5394 | | - headers: getRequestHeaders(), |
| 5395 | | - signal: signal, |
| 5396 | | - body: JSON.stringify(payload), |
| 5397 | | - }); |
| 5398 | | - |
| 5399 | | - if (result.ok) { |
| 5400 | | - const data = await result.json(); |
| 5401 | | - return { format: 'png', data: data.images?.[0] }; |
| 5402 | | - } else { |
| 5403 | | - const text = await result.text(); |
| 5404 | | - throw new Error(text); |
| 5405 | | - } |
| 5406 | | -} |
| 5407 | | - |
| 5408 | | -/** |
| 5409 | | - * Generates an image in Drawthings API using the provided prompt and configuration settings. |
| 5410 | | - * |
| 5411 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5412 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5413 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5414 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5415 | | - */ |
| 5416 | | -async function generateDrawthingsImage(prompt, negativePrompt, signal) { |
| 5417 | | - const result = await fetch('/api/sd/drawthings/generate', { |
| 5418 | | - method: 'POST', |
| 5419 | | - headers: getRequestHeaders(), |
| 5420 | | - signal: signal, |
| 5421 | | - body: JSON.stringify({ |
| 5422 | | - ...getSdRequestBody(), |
| 5423 | | - prompt: prompt, |
| 5424 | | - negative_prompt: negativePrompt, |
| 5425 | | - sampler_name: extension_settings.sd.sampler, |
| 5426 | | - steps: extension_settings.sd.steps, |
| 5427 | | - cfg_scale: extension_settings.sd.scale, |
| 5428 | | - width: extension_settings.sd.width, |
| 5429 | | - height: extension_settings.sd.height, |
| 5430 | | - restore_faces: !!extension_settings.sd.restore_faces, |
| 5431 | | - enable_hr: !!extension_settings.sd.enable_hr, |
| 5432 | | - denoising_strength: extension_settings.sd.denoising_strength, |
| 5433 | | - clip_skip: extension_settings.sd.clip_skip, |
| 5434 | | - upscaler_scale: extension_settings.sd.hr_scale, |
| 5435 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5436 | | - // TODO: advanced API parameters: hr, upscaler |
| 5437 | | - }), |
| 5438 | | - }); |
| 5439 | | - |
| 5440 | | - if (result.ok) { |
| 5441 | | - const data = await result.json(); |
| 5442 | | - return { format: 'png', data: data.images[0] }; |
| 5443 | | - } else { |
| 5444 | | - const text = await result.text(); |
| 5445 | | - throw new Error(text); |
| 5446 | | - } |
| 5447 | | -} |
| 5448 | | - |
| 5449 | | -/** |
| 5450 | | - * Generates an image in NovelAI API using the provided prompt and configuration settings. |
| 5451 | | - * |
| 5452 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5453 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5454 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5455 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5456 | | - */ |
| 5457 | | -async function generateNovelImage(prompt, negativePrompt, signal) { |
| 5458 | | - const { steps, width, height, sm, sm_dyn } = getNovelParams(); |
| 5459 | | - |
| 5460 | | - const result = await fetch('/api/novelai/generate-image', { |
| 5461 | | - method: 'POST', |
| 5462 | | - headers: getRequestHeaders(), |
| 5463 | | - signal: signal, |
| 5464 | | - body: JSON.stringify({ |
| 5465 | | - prompt: prompt, |
| 5466 | | - model: extension_settings.sd.model, |
| 5467 | | - sampler: extension_settings.sd.sampler, |
| 5468 | | - scheduler: extension_settings.sd.scheduler, |
| 5469 | | - steps: steps, |
| 5470 | | - scale: extension_settings.sd.scale, |
| 5471 | | - width: width, |
| 5472 | | - height: height, |
| 5473 | | - negative_prompt: negativePrompt, |
| 5474 | | - upscale_ratio: extension_settings.sd.hr_scale, |
| 5475 | | - decrisper: extension_settings.sd.novel_decrisper, |
| 5476 | | - variety_boost: extension_settings.sd.novel_variety_boost, |
| 5477 | | - sm: sm, |
| 5478 | | - sm_dyn: sm_dyn, |
| 5479 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5480 | | - }), |
| 5481 | | - }); |
| 5482 | | - |
| 5483 | | - if (result.ok) { |
| 5484 | | - const data = await result.text(); |
| 5485 | | - return { format: 'png', data: data }; |
| 5486 | | - } else { |
| 5487 | | - const text = await result.text(); |
| 5488 | | - throw new Error(text); |
| 5489 | | - } |
| 5490 | | -} |
| 5491 | | - |
| 5492 | | -/** |
| 5493 | | - * Adjusts extension parameters for NovelAI. Applies Anlas guard if needed. |
| 5494 | | - * @returns {{steps: number, width: number, height: number, sm: boolean, sm_dyn: boolean}} - A tuple of parameters for NovelAI API. |
| 5495 | | - */ |
| 5496 | | -function getNovelParams() { |
| 5497 | | - let steps = Math.min(extension_settings.sd.steps, 50); |
| 5498 | | - let width = extension_settings.sd.width; |
| 5499 | | - let height = extension_settings.sd.height; |
| 5500 | | - let sm = extension_settings.sd.novel_sm; |
| 5501 | | - let sm_dyn = extension_settings.sd.novel_sm_dyn; |
| 5502 | | - |
| 5503 | | - // If a source was never changed after the scheduler setting was added, we need to set it to 'karras' for compatibility. |
| 5504 | | - const schedulers = loadNovelSchedulers(); |
| 5505 | | - if (!schedulers.includes(extension_settings.sd.scheduler)) { |
| 5506 | | - extension_settings.sd.scheduler = 'karras'; |
| 5507 | | - } |
| 5508 | | - |
| 5509 | | - if (extension_settings.sd.sampler === 'ddim' || |
| 5510 | | - ['nai-diffusion-4-curated-preview', 'nai-diffusion-4-full'].includes(extension_settings.sd.model)) { |
| 5511 | | - sm = false; |
| 5512 | | - sm_dyn = false; |
| 5513 | | - } |
| 5514 | | - |
| 5515 | | - // Don't apply Anlas guard if it's disabled. |
| 5516 | | - if (!extension_settings.sd.novel_anlas_guard) { |
| 5517 | | - return { steps, width, height, sm, sm_dyn }; |
| 5518 | | - } |
| 5519 | | - |
| 5520 | | - const MAX_STEPS = 28; |
| 5521 | | - const MAX_PIXELS = 1024 * 1024; |
| 5522 | | - |
| 5523 | | - if (width * height > MAX_PIXELS) { |
| 5524 | | - const ratio = Math.sqrt(MAX_PIXELS / (width * height)); |
| 5525 | | - |
| 5526 | | - // Calculate new width and height while maintaining aspect ratio. |
| 5527 | | - let newWidth = Math.round(width * ratio); |
| 5528 | | - let newHeight = Math.round(height * ratio); |
| 5529 | | - |
| 5530 | | - // Ensure new dimensions are multiples of 64. If not, reduce accordingly. |
| 5531 | | - if (newWidth % 64 !== 0) { |
| 5532 | | - newWidth = newWidth - newWidth % 64; |
| 5533 | | - } |
| 5534 | | - |
| 5535 | | - if (newHeight % 64 !== 0) { |
| 5536 | | - newHeight = newHeight - newHeight % 64; |
| 5537 | | - } |
| 5538 | | - |
| 5539 | | - // If total pixel count after rounding still exceeds MAX_PIXELS, decrease dimension size by 64 accordingly. |
| 5540 | | - while (newWidth * newHeight > MAX_PIXELS) { |
| 5541 | | - if (newWidth > newHeight) { |
| 5542 | | - newWidth -= 64; |
| 5543 | | - } else { |
| 5544 | | - newHeight -= 64; |
| 5545 | | - } |
| 5546 | | - } |
| 5547 | | - |
| 5548 | | - console.log(`Anlas Guard: Image size (${width}x${height}) > ${MAX_PIXELS}, reducing size to ${newWidth}x${newHeight}`); |
| 5549 | | - width = newWidth; |
| 5550 | | - height = newHeight; |
| 5551 | | - } |
| 5552 | | - |
| 5553 | | - if (steps > MAX_STEPS) { |
| 5554 | | - console.log(`Anlas Guard: Steps (${steps}) > ${MAX_STEPS}, reducing steps to ${MAX_STEPS}`); |
| 5555 | | - steps = MAX_STEPS; |
| 5556 | | - } |
| 5557 | | - |
| 5558 | | - return { steps, width, height, sm, sm_dyn }; |
| 5559 | | -} |
| 5560 | | - |
| 5561 | | -/** |
| 5562 | | - * Generates an image in OpenAI API using the provided prompt and configuration settings. |
| 5563 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5564 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5565 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5566 | | - */ |
| 5567 | | -async function generateOpenAiImage(prompt, signal) { |
| 5568 | | - const dalle2PromptLimit = 1000; |
| 5569 | | - const dalle3PromptLimit = 4000; |
| 5570 | | - const gptImgPromptLimit = 32000; |
| 5571 | | - |
| 5572 | | - const isDalle2 = /dall-e-2/.test(extension_settings.sd.model); |
| 5573 | | - const isDalle3 = /dall-e-3/.test(extension_settings.sd.model); |
| 5574 | | - const isGptImg = /gpt-image-(1|2|latest)/.test(extension_settings.sd.model); |
| 5575 | | - const isSora2 = /sora-2/.test(extension_settings.sd.model); |
| 5576 | | - |
| 5577 | | - if (isDalle2 && prompt.length > dalle2PromptLimit) { |
| 5578 | | - prompt = prompt.substring(0, dalle2PromptLimit); |
| 5579 | | - } |
| 5580 | | - |
| 5581 | | - if (isDalle3 && prompt.length > dalle3PromptLimit) { |
| 5582 | | - prompt = prompt.substring(0, dalle3PromptLimit); |
| 5583 | | - } |
| 5584 | | - |
| 5585 | | - if (isGptImg && prompt.length > gptImgPromptLimit) { |
| 5586 | | - prompt = prompt.substring(0, gptImgPromptLimit); |
| 5587 | | - } |
| 5588 | | - |
| 5589 | | - let width = 1024; |
| 5590 | | - let height = 1024; |
| 5591 | | - let aspectRatio = extension_settings.sd.width / extension_settings.sd.height; |
| 5592 | | - |
| 5593 | | - if (isDalle3 && aspectRatio < 1) { |
| 5594 | | - height = 1792; |
| 5595 | | - } |
| 5596 | | - |
| 5597 | | - if (isDalle3 && aspectRatio > 1) { |
| 5598 | | - width = 1792; |
| 5599 | | - } |
| 5600 | | - |
| 5601 | | - if (isGptImg && aspectRatio < 1) { |
| 5602 | | - height = 1536; |
| 5603 | | - } |
| 5604 | | - |
| 5605 | | - if (isGptImg && aspectRatio > 1) { |
| 5606 | | - width = 1536; |
| 5607 | | - } |
| 5608 | | - |
| 5609 | | - if (isDalle2 && (extension_settings.sd.width <= 512 && extension_settings.sd.height <= 512)) { |
| 5610 | | - width = 512; |
| 5611 | | - height = 512; |
| 5612 | | - } |
| 5613 | | - |
| 5614 | | - if (isSora2) { |
| 5615 | | - width = aspectRatio >= 1 ? 1280 : 720; |
| 5616 | | - height = aspectRatio >= 1 ? 720 : 1280; |
| 5617 | | - |
| 5618 | | - const videoResult = await fetch('/api/openai/generate-video', { |
| 5619 | | - method: 'POST', |
| 5620 | | - headers: getRequestHeaders(), |
| 5621 | | - signal: signal, |
| 5622 | | - body: JSON.stringify({ |
| 5623 | | - prompt: prompt, |
| 5624 | | - model: extension_settings.sd.model, |
| 5625 | | - size: `${width}x${height}`, |
| 5626 | | - seconds: extension_settings.sd.openai_duration, |
| 5627 | | - }), |
| 5628 | | - }); |
| 5629 | | - |
| 5630 | | - if (!videoResult.ok) { |
| 5631 | | - throw new Error(await videoResult.text()); |
| 5632 | | - } |
| 5633 | | - |
| 5634 | | - const { format, data } = await videoResult.json(); |
| 5635 | | - return { format, data }; |
| 5636 | | - } |
| 5637 | | - |
| 5638 | | - const result = await fetch('/api/openai/generate-image', { |
| 5639 | | - method: 'POST', |
| 5640 | | - headers: getRequestHeaders(), |
| 5641 | | - signal: signal, |
| 5642 | | - body: JSON.stringify({ |
| 5643 | | - prompt: prompt, |
| 5644 | | - model: extension_settings.sd.model, |
| 5645 | | - size: `${width}x${height}`, |
| 5646 | | - n: 1, |
| 5647 | | - quality: isDalle3 ? extension_settings.sd.openai_quality : (isGptImg ? extension_settings.sd.openai_quality_gpt : undefined), |
| 5648 | | - style: isDalle3 ? extension_settings.sd.openai_style : undefined, |
| 5649 | | - response_format: isDalle2 || isDalle3 ? 'b64_json' : undefined, |
| 5650 | | - moderation: isGptImg ? 'low' : undefined, |
| 5651 | | - }), |
| 5652 | | - }); |
| 5653 | | - |
| 5654 | | - if (result.ok) { |
| 5655 | | - const data = await result.json(); |
| 5656 | | - return { format: 'png', data: data?.data[0]?.b64_json }; |
| 5657 | | - } else { |
| 5658 | | - const text = await result.text(); |
| 5659 | | - throw new Error(text); |
| 5660 | | - } |
| 5661 | | -} |
| 5662 | | - |
| 5663 | | -/** |
| 5664 | | - * Universal image generation via AIMLAPI: |
| 5665 | | - * - Builds the right request body for any model (OpenAI vs SD/Flux/Recraft). |
| 5666 | | - * - Extracts the URL or base64 response. |
| 5667 | | - * - If it’s a URL, fetches the image and converts to base64. |
| 5668 | | - * - Returns { format: 'png', data: '<base64 string>' }, ready for saveBase64AsFile(). |
| 5669 | | - */ |
| 5670 | | -async function generateAimlapiImage(prompt, signal) { |
| 5671 | | - const model = extension_settings.sd.model.toLowerCase(); |
| 5672 | | - const isSdLike = |
| 5673 | | - model.startsWith('flux/') || |
| 5674 | | - model.startsWith('stable') || |
| 5675 | | - model === 'recraft-v3' || |
| 5676 | | - model === 'triposr'; |
| 5677 | | - |
| 5678 | | - const body = { prompt, model }; |
| 5679 | | - if (isSdLike) { |
| 5680 | | - body.steps = clamp(extension_settings.sd.steps, 1, 50); |
| 5681 | | - body.guidance = clamp(extension_settings.sd.scale, 1.5, 5); |
| 5682 | | - body.width = clamp(extension_settings.sd.width, 256, 1440); |
| 5683 | | - body.height = clamp(extension_settings.sd.height, 256, 1440); |
| 5684 | | - if (extension_settings.sd.seed >= 0) body.seed = extension_settings.sd.seed; |
| 5685 | | - } else { |
| 5686 | | - body.n = 1; |
| 5687 | | - body.size = `${extension_settings.sd.width}x${extension_settings.sd.height}`; |
| 5688 | | - body.quality = extension_settings.sd.openai_quality; |
| 5689 | | - body.style = extension_settings.sd.openai_style; |
| 5690 | | - } |
| 5691 | | - |
| 5692 | | - const res = await fetch('/api/sd/aimlapi/generate-image', { |
| 5693 | | - method: 'POST', |
| 5694 | | - headers: getRequestHeaders(), |
| 5695 | | - signal, |
| 5696 | | - body: JSON.stringify(body), |
| 5697 | | - }); |
| 5698 | | - if (!res.ok) throw new Error(await res.text()); |
| 5699 | | - |
| 5700 | | - const { format, data } = await res.json(); |
| 5701 | | - return { format, data }; |
| 5702 | | -} |
| 5703 | | - |
| 5704 | | -/** |
| 5705 | | - * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings. |
| 5706 | | - * |
| 5707 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5708 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5709 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5710 | | - * @param {string} basePath - ST server endpoint for the service. '/api/sd/comfy' for local, '/api/sd/comfyrunpod' for serverless. |
| 5711 | | - * @param {string[]} placeholders - Array of substitutions to apply to the workflow. |
| 5712 | | - * @param {string} url - The url of the service to call. Passed to ST server. |
| 5713 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5714 | | - */ |
| 5715 | | -async function generateComfyImageCommon(prompt, negativePrompt, signal, basePath, placeholders, url) { |
| 5716 | | - const workflowResponse = await fetch('/api/sd/comfy/workflow', { |
| 5717 | | - method: 'POST', |
| 5718 | | - headers: getRequestHeaders(), |
| 5719 | | - body: JSON.stringify({ |
| 5720 | | - file_name: extension_settings.sd.comfy_workflow, |
| 5721 | | - }), |
| 5722 | | - }); |
| 5723 | | - if (!workflowResponse.ok) { |
| 5724 | | - const text = await workflowResponse.text(); |
| 5725 | | - toastr.error(`Failed to load workflow.\n\n${text}`); |
| 5726 | | - } |
| 5727 | | - let workflow = (await workflowResponse.json()).replaceAll('"%prompt%"', JSON.stringify(prompt)); |
| 5728 | | - workflow = workflow.replaceAll('"%negative_prompt%"', JSON.stringify(negativePrompt)); |
| 5729 | | - |
| 5730 | | - const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER); |
| 5731 | | - workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed)); |
| 5732 | | - |
| 5733 | | - const denoising_strength = extension_settings.sd.denoising_strength === undefined ? 1.0 : extension_settings.sd.denoising_strength; |
| 5734 | | - workflow = workflow.replaceAll('"%denoise%"', JSON.stringify(denoising_strength)); |
| 5735 | | - |
| 5736 | | - const clip_skip = isNaN(extension_settings.sd.clip_skip) ? -1 : -extension_settings.sd.clip_skip; |
| 5737 | | - workflow = workflow.replaceAll('"%clip_skip%"', JSON.stringify(clip_skip)); |
| 5738 | | - |
| 5739 | | - placeholders.forEach(ph => { |
| 5740 | | - workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); |
| 5741 | | - }); |
| 5742 | | - (extension_settings.sd.comfy_placeholders ?? []).forEach(ph => { |
| 5743 | | - workflow = workflow.replaceAll(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace))); |
| 5744 | | - }); |
| 5745 | | - // Log the workflow before image payloads are substituted in: keeps private |
| 5746 | | - // image data (avatars, reference images) out of the console and avoids |
| 5747 | | - // scanning multi-megabyte strings (a redaction regex here previously blew |
| 5748 | | - // the stack on large reference images). |
| 5749 | | - console.log(`{ |
| 5750 | | - "prompt": ${workflow} |
| 5751 | | - }`); |
| 5752 | | - if (/%user_avatar%/gi.test(workflow)) { |
| 5753 | | - const response = await fetch(getUserAvatarUrl()); |
| 5754 | | - if (response.ok) { |
| 5755 | | - const avatarBlob = await response.blob(); |
| 5756 | | - const avatarBase64DataUrl = await getBase64Async(avatarBlob); |
| 5757 | | - const avatarBase64 = avatarBase64DataUrl.split(',')[1]; |
| 5758 | | - workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64)); |
| 5759 | | - } else { |
| 5760 | | - workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL)); |
| 5761 | | - } |
| 5762 | | - } |
| 5763 | | - if (/%char_avatar%/gi.test(workflow)) { |
| 5764 | | - const response = await fetch(getCharacterAvatarUrl()); |
| 5765 | | - if (response.ok) { |
| 5766 | | - const avatarBlob = await response.blob(); |
| 5767 | | - const avatarBase64DataUrl = await getBase64Async(avatarBlob); |
| 5768 | | - const avatarBase64 = avatarBase64DataUrl.split(',')[1]; |
| 5769 | | - workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64)); |
| 5770 | | - } else { |
| 5771 | | - workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL)); |
| 5772 | | - } |
| 5773 | | - } |
| 5774 | | - if (REFERENCE_IMAGE_PLACEHOLDER.test(workflow)) { |
| 5775 | | - const refImage = await resolveReferenceImageForGeneration(prompt); |
| 5776 | | - const refBase64 = (refImage && await fetchReferenceImageBase64(refImage)) || PNG_PIXEL; |
| 5777 | | - if (refBase64 !== PNG_PIXEL) { |
| 5778 | | - console.log(`SD: workflow reference image: "${refImage.tag}" (${refImage.path})`); |
| 5779 | | - toastr.info(`Reference image: ${refImage.description || refImage.tag || refImage.path}`, 'Image Generation'); |
| 5780 | | - } else { |
| 5781 | | - console.warn('SD: workflow uses %reference_image% but no library image was available; sending a transparent pixel'); |
| 5782 | | - } |
| 5783 | | - workflow = workflow.replaceAll('"%reference_image%"', JSON.stringify(refBase64)); |
| 5784 | | - workflow = workflow.replaceAll('"%reference-image%"', JSON.stringify(refBase64)); |
| 5785 | | - } |
| 5786 | | - const promptResult = await fetch(`${basePath}/generate`, { |
| 5787 | | - method: 'POST', |
| 5788 | | - headers: getRequestHeaders(), |
| 5789 | | - signal: signal, |
| 5790 | | - body: JSON.stringify({ |
| 5791 | | - url, |
| 5792 | | - prompt: `{ |
| 5793 | | - "prompt": ${workflow} |
| 5794 | | - }`, |
| 5795 | | - }), |
| 5796 | | - }); |
| 5797 | | - if (!promptResult.ok) { |
| 5798 | | - const text = await promptResult.text(); |
| 5799 | | - throw new Error(text); |
| 5800 | | - } |
| 5801 | | - const { format, data } = await promptResult.json(); |
| 5802 | | - return { format, data }; |
| 5803 | | -} |
| 5804 | | - |
| 5805 | | -/** |
| 5806 | | - * Generates an image in ComfyUI using the provided prompt and configuration settings. |
| 5807 | | - * |
| 5808 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5809 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5810 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5811 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5812 | | - */ |
| 5813 | | -async function generateComfyImage(prompt, negativePrompt, signal) { |
| 5814 | | - if (isRunpodProxyUrl(extension_settings.sd.comfy_url) && !(await isRunpodReady(SOURCE_PROBE_TIMEOUT_MS))) { |
| 5815 | | - throw new Error('RunPod on-demand pod is not ready. Start it with the Warm up control.'); |
| 5816 | | - } |
| 5817 | | - const placeholders = [ |
| 5818 | | - 'model', |
| 5819 | | - 'vae', |
| 5820 | | - 'lora', |
| 5821 | | - 'lora_strength', |
| 5822 | | - 'sampler', |
| 5823 | | - 'scheduler', |
| 5824 | | - 'steps', |
| 5825 | | - 'scale', |
| 5826 | | - 'width', |
| 5827 | | - 'height', |
| 5828 | | - ]; |
| 5829 | | - return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfy', placeholders, extension_settings.sd.comfy_url); |
| 5830 | | -} |
| 5831 | | - |
| 5832 | | -/** |
| 5833 | | - * Generates an image using ComfyUI through serverless runpod endpoint using the provided prompt and configuration settings. |
| 5834 | | - * |
| 5835 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5836 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5837 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5838 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5839 | | - */ |
| 5840 | | -async function generateComfyRunPodImage(prompt, negativePrompt, signal) { |
| 5841 | | - const placeholders = [ |
| 5842 | | - 'steps', |
| 5843 | | - 'scale', |
| 5844 | | - 'width', |
| 5845 | | - 'height', |
| 5846 | | - ]; |
| 5847 | | - |
| 5848 | | - return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfyrunpod', placeholders, extension_settings.sd.comfy_runpod_url); |
| 5849 | | -} |
| 5850 | | - |
| 5851 | | -/** |
| 5852 | | - * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected). |
| 5853 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5854 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5855 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5856 | | - */ |
| 5857 | | -async function generateHuggingFaceImage(prompt, signal) { |
| 5858 | | - const result = await fetch('/api/sd/huggingface/generate', { |
| 5859 | | - method: 'POST', |
| 5860 | | - headers: getRequestHeaders(), |
| 5861 | | - signal: signal, |
| 5862 | | - body: JSON.stringify({ |
| 5863 | | - model: extension_settings.sd.huggingface_model_id, |
| 5864 | | - prompt: prompt, |
| 5865 | | - }), |
| 5866 | | - }); |
| 5867 | | - |
| 5868 | | - if (result.ok) { |
| 5869 | | - const data = await result.json(); |
| 5870 | | - return { format: 'jpg', data: data.image }; |
| 5871 | | - } else { |
| 5872 | | - const text = await result.text(); |
| 5873 | | - throw new Error(text); |
| 5874 | | - } |
| 5875 | | -} |
| 5876 | | - |
| 5877 | | -/** |
| 5878 | | - * Generates an image using the Chutes API. |
| 5879 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5880 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5881 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5882 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5883 | | - */ |
| 5884 | | -async function generateChutesImage(prompt, negativePrompt, signal) { |
| 5885 | | - const result = await fetch('/api/sd/chutes/generate', { |
| 5886 | | - method: 'POST', |
| 5887 | | - headers: getRequestHeaders(), |
| 5888 | | - signal: signal, |
| 5889 | | - body: JSON.stringify({ |
| 5890 | | - model: extension_settings.sd.model, |
| 5891 | | - prompt: prompt, |
| 5892 | | - negative_prompt: negativePrompt, |
| 5893 | | - width: extension_settings.sd.width, |
| 5894 | | - height: extension_settings.sd.height, |
| 5895 | | - steps: extension_settings.sd.steps, |
| 5896 | | - guidance_scale: extension_settings.sd.scale, |
| 5897 | | - }), |
| 5898 | | - }); |
| 5899 | | - |
| 5900 | | - if (result.ok) { |
| 5901 | | - const data = await result.json(); |
| 5902 | | - return { format: 'jpg', data: data.image }; |
| 5903 | | - } else { |
| 5904 | | - const text = await result.text(); |
| 5905 | | - throw new Error(text); |
| 5906 | | - } |
| 5907 | | -} |
| 5908 | | - |
| 5909 | | -/** |
| 5910 | | - * Generates an image using the Electron Hub API. |
| 5911 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5912 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5913 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5914 | | - */ |
| 5915 | | -async function generateElectronHubImage(prompt, signal) { |
| 5916 | | - const size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height); |
| 5917 | | - |
| 5918 | | - const result = await fetch('/api/sd/electronhub/generate', { |
| 5919 | | - method: 'POST', |
| 5920 | | - headers: getRequestHeaders(), |
| 5921 | | - signal: signal, |
| 5922 | | - body: JSON.stringify({ |
| 5923 | | - model: extension_settings.sd.model, |
| 5924 | | - prompt: prompt, |
| 5925 | | - size: size, |
| 5926 | | - quality: String(extension_settings.sd.electronhub_quality || '').trim() || undefined, |
| 5927 | | - }), |
| 5928 | | - }); |
| 5929 | | - |
| 5930 | | - if (result.ok) { |
| 5931 | | - const data = await result.json(); |
| 5932 | | - return { format: 'jpg', data: data.image }; |
| 5933 | | - } else { |
| 5934 | | - const text = await result.text(); |
| 5935 | | - throw new Error(text); |
| 5936 | | - } |
| 5937 | | -} |
| 5938 | | - |
| 5939 | | -/** |
| 5940 | | - * Generates an image using the NanoGPT API. |
| 5941 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5942 | | - * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 5943 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5944 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5945 | | - */ |
| 5946 | | -async function generateNanoGPTImage(prompt, negativePrompt, signal) { |
| 5947 | | - const result = await fetch('/api/sd/nanogpt/generate', { |
| 5948 | | - method: 'POST', |
| 5949 | | - headers: getRequestHeaders(), |
| 5950 | | - signal: signal, |
| 5951 | | - body: JSON.stringify({ |
| 5952 | | - model: extension_settings.sd.model, |
| 5953 | | - prompt: prompt, |
| 5954 | | - negative_prompt: negativePrompt, |
| 5955 | | - num_steps: parseInt(extension_settings.sd.steps), |
| 5956 | | - scale: parseFloat(extension_settings.sd.scale), |
| 5957 | | - width: parseInt(extension_settings.sd.width), |
| 5958 | | - height: parseInt(extension_settings.sd.height), |
| 5959 | | - resolution: `${extension_settings.sd.width}x${extension_settings.sd.height}`, |
| 5960 | | - showExplicitContent: true, |
| 5961 | | - nImages: 1, |
| 5962 | | - }), |
| 5963 | | - }); |
| 5964 | | - |
| 5965 | | - if (result.ok) { |
| 5966 | | - const data = await result.json(); |
| 5967 | | - return { format: 'jpg', data: data.image }; |
| 5968 | | - } else { |
| 5969 | | - const text = await result.text(); |
| 5970 | | - throw new Error(text); |
| 5971 | | - } |
| 5972 | | -} |
| 5973 | | - |
| 5974 | | -/** |
| 5975 | | - * Generates an image using the BFL API. |
| 5976 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 5977 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 5978 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 5979 | | - */ |
| 5980 | | -async function generateBflImage(prompt, signal) { |
| 5981 | | - const result = await fetch('/api/sd/bfl/generate', { |
| 5982 | | - method: 'POST', |
| 5983 | | - headers: getRequestHeaders(), |
| 5984 | | - signal: signal, |
| 5985 | | - body: JSON.stringify({ |
| 5986 | | - prompt: prompt, |
| 5987 | | - model: extension_settings.sd.model, |
| 5988 | | - steps: clamp(extension_settings.sd.steps, 1, 50), |
| 5989 | | - guidance: clamp(extension_settings.sd.scale, 1.5, 5), |
| 5990 | | - width: clamp(extension_settings.sd.width, 256, 1440), |
| 5991 | | - height: clamp(extension_settings.sd.height, 256, 1440), |
| 5992 | | - prompt_upsampling: !!extension_settings.sd.bfl_upsampling, |
| 5993 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 5994 | | - }), |
| 5995 | | - }); |
| 5996 | | - |
| 5997 | | - if (result.ok) { |
| 5998 | | - const data = await result.json(); |
| 5999 | | - return { format: 'jpg', data: data.image }; |
| 6000 | | - } else { |
| 6001 | | - const text = await result.text(); |
| 6002 | | - throw new Error(text); |
| 6003 | | - } |
| 6004 | | -} |
| 6005 | | - |
| 6006 | | -/** |
| 6007 | | - * Generates an image using the xAI API. |
| 6008 | | - * @param {string} prompt The main instruction used to guide the image generation. |
| 6009 | | - * @param {string} _negativePrompt Negative prompt is not used in this API |
| 6010 | | - * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request. |
| 6011 | | - * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. |
| 6012 | | - */ |
| 6013 | | -async function generateXAIImage(prompt, _negativePrompt, signal) { |
| 6014 | | - let aspectRatio; |
| 6015 | | - let resolution; |
| 6016 | | - |
| 6017 | | - if (/grok-imagine/.test(extension_settings.sd.model)) { |
| 6018 | | - const resolutionThreshold = 1296 * 864; |
| 6019 | | - const use2kResolution = (extension_settings.sd.width * extension_settings.sd.height) > resolutionThreshold; |
| 6020 | | - aspectRatio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'xai'); |
| 6021 | | - resolution = use2kResolution ? '2k' : '1k'; |
| 6022 | | - } |
| 6023 | | - |
| 6024 | | - const result = await fetch('/api/sd/xai/generate', { |
| 6025 | | - method: 'POST', |
| 6026 | | - headers: getRequestHeaders(), |
| 6027 | | - signal: signal, |
| 6028 | | - body: JSON.stringify({ |
| 6029 | | - prompt: prompt, |
| 6030 | | - model: extension_settings.sd.model, |
| 6031 | | - aspect_ratio: aspectRatio, |
| 6032 | | - resolution: resolution, |
| 6033 | | - }), |
| 6034 | | - }); |
| 6035 | | - |
| 6036 | | - if (result.ok) { |
| 6037 | | - const data = await result.json(); |
| 6038 | | - return { format: data.format, data: data.image }; |
| 6039 | | - } else { |
| 6040 | | - const text = await result.text(); |
| 6041 | | - throw new Error(text); |
| 6042 | | - } |
| 6043 | | -} |
| 6044 | | - |
| 6045 | | -/** |
| 6046 | | - * Generates an image using the FAL.AI API. |
| 6047 | | - * @param {string} prompt - The main instruction used to guide the image generation. |
| 6048 | | - * @param {string} negativePrompt - The negative prompt used to guide the image generation. |
| 6049 | | - * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 6050 | | - * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 6051 | | - */ |
| 6052 | | -async function generateFalaiImage(prompt, negativePrompt, signal) { |
| 6053 | | - const result = await fetch('/api/sd/falai/generate', { |
| 6054 | | - method: 'POST', |
| 6055 | | - headers: getRequestHeaders(), |
| 6056 | | - signal: signal, |
| 6057 | | - body: JSON.stringify({ |
| 6058 | | - prompt: prompt, |
| 6059 | | - negative_prompt: negativePrompt, |
| 6060 | | - model: extension_settings.sd.model, |
| 6061 | | - steps: clamp(extension_settings.sd.steps, 1, 50), |
| 6062 | | - guidance: clamp(extension_settings.sd.scale, 1.5, 5), |
| 6063 | | - width: clamp(extension_settings.sd.width, 256, 1440), |
| 6064 | | - height: clamp(extension_settings.sd.height, 256, 1440), |
| 6065 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 6066 | | - }), |
| 6067 | | - }); |
| 6068 | | - |
| 6069 | | - if (result.ok) { |
| 6070 | | - const data = await result.json(); |
| 6071 | | - return { format: 'jpg', data: data.image }; |
| 6072 | | - } else { |
| 6073 | | - const text = await result.text(); |
| 6074 | | - throw new Error(text); |
| 6075 | | - } |
| 6076 | | -} |
| 6077 | | - |
| 6078 | | -/** |
| 6079 | | - * Generates an image using the Google Vertex AI API. |
| 6080 | | - * @param {string} prompt The main instruction used to guide the image generation. |
| 6081 | | - * @param {string} negativePrompt The instruction used to restrict the image generation. |
| 6082 | | - * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request. |
| 6083 | | - * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. |
| 6084 | | - */ |
| 6085 | | -async function generateGoogleImage(prompt, negativePrompt, signal) { |
| 6086 | | - const isVeo = /^veo-/.test(extension_settings.sd.model); |
| 6087 | | - |
| 6088 | | - if (isVeo) { |
| 6089 | | - const aspectRatio = extension_settings.sd.width / extension_settings.sd.height; |
| 6090 | | - const maxPromptLength = 3000; // 1024 tokens approx. |
| 6091 | | - const videoResult = await fetch('/api/google/generate-video', { |
| 6092 | | - method: 'POST', |
| 6093 | | - headers: getRequestHeaders(), |
| 6094 | | - signal: signal, |
| 6095 | | - body: JSON.stringify({ |
| 6096 | | - prompt: prompt.slice(0, maxPromptLength), |
| 6097 | | - aspect_ratio: aspectRatio >= 1 ? '16:9' : '9:16', |
| 6098 | | - seconds: extension_settings.sd.google_duration, |
| 6099 | | - negative_prompt: negativePrompt, |
| 6100 | | - model: extension_settings.sd.model, |
| 6101 | | - api: extension_settings.sd.google_api || 'makersuite', |
| 6102 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 6103 | | - vertexai_auth_mode: oai_settings.vertexai_auth_mode, |
| 6104 | | - vertexai_region: oai_settings.vertexai_region, |
| 6105 | | - vertexai_express_project_id: oai_settings.vertexai_express_project_id, |
| 6106 | | - }), |
| 6107 | | - }); |
| 6108 | | - |
| 6109 | | - if (!videoResult.ok) { |
| 6110 | | - const text = await videoResult.text(); |
| 6111 | | - throw new Error(text); |
| 6112 | | - } |
| 6113 | | - |
| 6114 | | - const data = await videoResult.json(); |
| 6115 | | - return { format: 'mp4', data: data.video }; |
| 6116 | | - } |
| 6117 | | - |
| 6118 | | - const result = await fetch('/api/google/generate-image', { |
| 6119 | | - method: 'POST', |
| 6120 | | - headers: getRequestHeaders(), |
| 6121 | | - signal: signal, |
| 6122 | | - body: JSON.stringify({ |
| 6123 | | - prompt: prompt, |
| 6124 | | - aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'google'), |
| 6125 | | - negative_prompt: negativePrompt, |
| 6126 | | - model: extension_settings.sd.model, |
| 6127 | | - enhance: extension_settings.sd.google_enhance, |
| 6128 | | - api: extension_settings.sd.google_api || 'makersuite', |
| 6129 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 6130 | | - vertexai_auth_mode: oai_settings.vertexai_auth_mode, |
| 6131 | | - vertexai_region: oai_settings.vertexai_region, |
| 6132 | | - vertexai_express_project_id: oai_settings.vertexai_express_project_id, |
| 6133 | | - }), |
| 6134 | | - }); |
| 6135 | | - |
| 6136 | | - if (result.ok) { |
| 6137 | | - const data = await result.json(); |
| 6138 | | - return { format: 'jpg', data: data.image }; |
| 6139 | | - } else { |
| 6140 | | - const text = await result.text(); |
| 6141 | | - throw new Error(text); |
| 6142 | | - } |
| 6143 | | -} |
| 6144 | | - |
| 6145 | | -/** |
| 6146 | | - * Generates an image using the Z.AI API. |
| 6147 | | - * @param {string} prompt The main instruction used to guide the image generation. |
| 6148 | | - * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request. |
| 6149 | | - * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. |
| 6150 | | - */ |
| 6151 | | -async function generateZaiImage(prompt, signal) { |
| 6152 | | - // Video generation models (CogVideoX, Viduq1) |
| 6153 | | - if (/(cogvideox|vidu)/.test(extension_settings.sd.model)) { |
| 6154 | | - const videoParams = {}; |
| 6155 | | - if (/cogvideox/.test(extension_settings.sd.model)) { |
| 6156 | | - const cogVideoSizes = ['1280x720', '720x1280', '1024x1024', '1080x1920', '2048x1080', '3840x2160']; |
| 6157 | | - videoParams.quality = extension_settings.sd.openai_quality === 'hd' ? 'quality' : 'speed'; |
| 6158 | | - videoParams.size = await getClosestSize(extension_settings.sd.width, extension_settings.sd.height, cogVideoSizes); |
| 6159 | | - } |
| 6160 | | - if (/vidu/.test(extension_settings.sd.model)) { |
| 6161 | | - videoParams.aspect_ratio = getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'zai'); |
| 6162 | | - } |
| 6163 | | - |
| 6164 | | - const videoResult = await fetch('/api/sd/zai/generate-video', { |
| 6165 | | - method: 'POST', |
| 6166 | | - headers: getRequestHeaders(), |
| 6167 | | - signal: signal, |
| 6168 | | - body: JSON.stringify({ |
| 6169 | | - prompt: prompt, |
| 6170 | | - model: extension_settings.sd.model, |
| 6171 | | - ...videoParams, |
| 6172 | | - }), |
| 6173 | | - }); |
| 6174 | | - |
| 6175 | | - if (videoResult.ok) { |
| 6176 | | - const data = await videoResult.json(); |
| 6177 | | - return { format: data.format, data: data.video }; |
| 6178 | | - } |
| 6179 | | - |
| 6180 | | - const text = await videoResult.text(); |
| 6181 | | - throw new Error(text); |
| 6182 | | - } else { |
| 6183 | | - // Image generation models (GLM-Image, CogView) |
| 6184 | | - // GLM-Image requires multiples of 32, CogView requires multiples of 16 |
| 6185 | | - const isGlmImage = /glm-image/.test(extension_settings.sd.model); |
| 6186 | | - const multiple = isGlmImage ? 32 : 16; |
| 6187 | | - |
| 6188 | | - // Round width and height to nearest multiple and clamp to 512-2048 range |
| 6189 | | - let width = clamp(Math.round(extension_settings.sd.width / multiple) * multiple, 512, 2048); |
| 6190 | | - let height = clamp(Math.round(extension_settings.sd.height / multiple) * multiple, 512, 2048); |
| 6191 | | - |
| 6192 | | - // CogView has a 2^21px pixel count limit, GLM-Image does not |
| 6193 | | - if (!isGlmImage) { |
| 6194 | | - while ((width * height) > Math.pow(2, 21)) { |
| 6195 | | - if (width >= height) { |
| 6196 | | - width -= multiple; |
| 6197 | | - } else { |
| 6198 | | - height -= multiple; |
| 6199 | | - } |
| 6200 | | - } |
| 6201 | | - } |
| 6202 | | - |
| 6203 | | - const result = await fetch('/api/sd/zai/generate', { |
| 6204 | | - method: 'POST', |
| 6205 | | - headers: getRequestHeaders(), |
| 6206 | | - signal: signal, |
| 6207 | | - body: JSON.stringify({ |
| 6208 | | - prompt: prompt, |
| 6209 | | - model: extension_settings.sd.model, |
| 6210 | | - quality: extension_settings.sd.openai_quality, |
| 6211 | | - size: `${width}x${height}`, |
| 6212 | | - }), |
| 6213 | | - }); |
| 6214 | | - |
| 6215 | | - if (result.ok) { |
| 6216 | | - const data = await result.json(); |
| 6217 | | - return { format: data.format, data: data.image }; |
| 6218 | | - } |
| 6219 | | - |
| 6220 | | - const text = await result.text(); |
| 6221 | | - throw new Error(text); |
| 6222 | | - } |
| 6223 | | -} |
| 6224 | | - |
| 6225 | | -/** |
| 6226 | | - * Generates an image using the OpenRouter API. |
| 6227 | | - * @param {string} prompt The main instruction used to guide the image generation. |
| 6228 | | - * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request. |
| 6229 | | - * @returns {Promise<{format: string, data: string}>} |
| 6230 | | - */ |
| 6231 | | -async function generateOpenRouterImage(prompt, signal) { |
| 6232 | | - const result = await fetch('/api/openrouter/image/generate', { |
| 6233 | | - method: 'POST', |
| 6234 | | - headers: getRequestHeaders(), |
| 6235 | | - signal: signal, |
| 6236 | | - body: JSON.stringify({ |
| 6237 | | - model: extension_settings.sd.model, |
| 6238 | | - prompt: prompt, |
| 6239 | | - aspect_ratio: getClosestAspectRatio(extension_settings.sd.width, extension_settings.sd.height, 'stability'), |
| 6240 | | - }), |
| 6241 | | - }); |
| 6242 | | - |
| 6243 | | - if (result.ok) { |
| 6244 | | - const data = await result.json(); |
| 6245 | | - return { format: 'jpg', data: data.image }; |
| 6246 | | - } |
| 6247 | | - |
| 6248 | | - const text = await result.text(); |
| 6249 | | - throw new Error(text); |
| 6250 | | -} |
| 6251 | | - |
| 6252 | | -async function generateWorkersAIImage(prompt, negativePrompt, signal) { |
| 6253 | | - const result = await fetch('/api/sd/workersai/generate', { |
| 6254 | | - method: 'POST', |
| 6255 | | - headers: getRequestHeaders(), |
| 6256 | | - signal: signal, |
| 6257 | | - body: JSON.stringify({ |
| 6258 | | - prompt: prompt, |
| 6259 | | - negative_prompt: negativePrompt, |
| 6260 | | - model: extension_settings.sd.model, |
| 6261 | | - width: extension_settings.sd.width, |
| 6262 | | - height: extension_settings.sd.height, |
| 6263 | | - steps: extension_settings.sd.steps, |
| 6264 | | - scale: extension_settings.sd.scale, |
| 6265 | | - seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, |
| 6266 | | - account_id: oai_settings.workers_ai_account_id, |
| 6267 | | - }), |
| 6268 | | - }); |
| 6269 | | - |
| 6270 | | - if (result.ok) { |
| 6271 | | - const data = await result.json(); |
| 6272 | | - return { format: data?.format, data: data?.image }; |
| 6273 | | - } else { |
| 6274 | | - const text = await result.text(); |
| 6275 | | - throw new Error(text); |
| 6276 | | - } |
| 6277 | | -} |
| 6278 | | - |
| 6279 | | -async function onComfyOpenWorkflowEditorClick() { |
| 6280 | | - let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 6281 | | - method: 'POST', |
| 6282 | | - headers: getRequestHeaders(), |
| 6283 | | - body: JSON.stringify({ |
| 6284 | | - file_name: extension_settings.sd.comfy_workflow, |
| 6285 | | - }), |
| 6286 | | - })).json(); |
| 6287 | | - const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html')); |
| 6288 | | - const saveValue = (/** @type {Popup} */ _popup) => { |
| 6289 | | - workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); |
| 6290 | | - return true; |
| 6291 | | - }; |
| 6292 | | - const popup = new Popup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Save', cancelButton: 'Cancel', wide: true, large: true, onClosing: saveValue }); |
| 6293 | | - const popupResult = popup.show(); |
| 6294 | | - const checkPlaceholders = () => { |
| 6295 | | - workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); |
| 6296 | | - $('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function () { |
| 6297 | | - const key = this.getAttribute('data-placeholder'); |
| 6298 | | - const found = workflow.search(`"%${key}%"`) !== -1; |
| 6299 | | - this.classList[found ? 'remove' : 'add']('sd_comfy_workflow_editor_not_found'); |
| 6300 | | - }); |
| 6301 | | - }; |
| 6302 | | - $('#sd_comfy_workflow_editor_name').text(extension_settings.sd.comfy_workflow); |
| 6303 | | - $('#sd_comfy_workflow_editor_workflow').val(workflow); |
| 6304 | | - const addPlaceholderDom = (placeholder) => { |
| 6305 | | - const el = $(` |
| 6306 | | - <li class="sd_comfy_workflow_editor_not_found" data-placeholder="${placeholder.find}"> |
| 6307 | | - <span class="sd_comfy_workflow_editor_custom_remove" title="Remove custom placeholder">⊘</span> |
| 6308 | | - <span class="sd_comfy_workflow_editor_custom_final">"%${placeholder.find}%"</span><br> |
| 6309 | | - <input placeholder="find" title="find" type="text" class="text_pole sd_comfy_workflow_editor_custom_find" value=""><br> |
| 6310 | | - <input placeholder="replace" title="replace" type="text" class="text_pole sd_comfy_workflow_editor_custom_replace"> |
| 6311 | | - </li> |
| 6312 | | - `); |
| 6313 | | - $('#sd_comfy_workflow_editor_placeholder_list_custom').append(el); |
| 6314 | | - el.find('.sd_comfy_workflow_editor_custom_find').val(placeholder.find); |
| 6315 | | - el.find('.sd_comfy_workflow_editor_custom_find').on('input', function () { |
| 6316 | | - if (!(this instanceof HTMLInputElement)) { |
| 6317 | | - return; |
| 6318 | | - } |
| 6319 | | - placeholder.find = this.value; |
| 6320 | | - el.find('.sd_comfy_workflow_editor_custom_final').text(`"%${this.value}%"`); |
| 6321 | | - el.attr('data-placeholder', `${this.value}`); |
| 6322 | | - checkPlaceholders(); |
| 6323 | | - saveSettingsDebounced(); |
| 6324 | | - }); |
| 6325 | | - el.find('.sd_comfy_workflow_editor_custom_replace').val(placeholder.replace); |
| 6326 | | - el.find('.sd_comfy_workflow_editor_custom_replace').on('input', function () { |
| 6327 | | - if (!(this instanceof HTMLInputElement)) { |
| 6328 | | - return; |
| 6329 | | - } |
| 6330 | | - placeholder.replace = this.value; |
| 6331 | | - saveSettingsDebounced(); |
| 6332 | | - }); |
| 6333 | | - el.find('.sd_comfy_workflow_editor_custom_remove').on('click', () => { |
| 6334 | | - el.remove(); |
| 6335 | | - extension_settings.sd.comfy_placeholders.splice(extension_settings.sd.comfy_placeholders.indexOf(placeholder)); |
| 6336 | | - saveSettingsDebounced(); |
| 6337 | | - }); |
| 6338 | | - }; |
| 6339 | | - $('#sd_comfy_workflow_editor_placeholder_add').on('click', () => { |
| 6340 | | - if (!extension_settings.sd.comfy_placeholders) { |
| 6341 | | - extension_settings.sd.comfy_placeholders = []; |
| 6342 | | - } |
| 6343 | | - const placeholder = { |
| 6344 | | - find: '', |
| 6345 | | - replace: '', |
| 6346 | | - }; |
| 6347 | | - extension_settings.sd.comfy_placeholders.push(placeholder); |
| 6348 | | - addPlaceholderDom(placeholder); |
| 6349 | | - saveSettingsDebounced(); |
| 6350 | | - }); |
| 6351 | | - (extension_settings.sd.comfy_placeholders ?? []).forEach(placeholder => { |
| 6352 | | - addPlaceholderDom(placeholder); |
| 6353 | | - }); |
| 6354 | | - checkPlaceholders(); |
| 6355 | | - $('#sd_comfy_workflow_editor_workflow').on('input', checkPlaceholders); |
| 6356 | | - if (await popupResult) { |
| 6357 | | - const response = await fetch('/api/sd/comfy/save-workflow', { |
| 6358 | | - method: 'POST', |
| 6359 | | - headers: getRequestHeaders(), |
| 6360 | | - body: JSON.stringify({ |
| 6361 | | - file_name: extension_settings.sd.comfy_workflow, |
| 6362 | | - workflow: workflow, |
| 6363 | | - }), |
| 6364 | | - }); |
| 6365 | | - if (!response.ok) { |
| 6366 | | - const text = await response.text(); |
| 6367 | | - toastr.error(`Failed to save workflow.\n\n${text}`); |
| 6368 | | - } |
| 6369 | | - } |
| 6370 | | -} |
| 6371 | | - |
| 6372 | | -async function onComfyNewWorkflowClick() { |
| 6373 | | - let name = await callGenericPopup('Workflow name:', POPUP_TYPE.INPUT); |
| 6374 | | - if (!name) { |
| 6375 | | - return; |
| 6376 | | - } |
| 6377 | | - if (!String(name).toLowerCase().endsWith('.json')) { |
| 6378 | | - name += '.json'; |
| 6379 | | - } |
| 6380 | | - extension_settings.sd.comfy_workflow = name; |
| 6381 | | - const response = await fetch('/api/sd/comfy/save-workflow', { |
| 6382 | | - method: 'POST', |
| 6383 | | - headers: getRequestHeaders(), |
| 6384 | | - body: JSON.stringify({ |
| 6385 | | - file_name: extension_settings.sd.comfy_workflow, |
| 6386 | | - workflow: '', |
| 6387 | | - }), |
| 6388 | | - }); |
| 6389 | | - if (!response.ok) { |
| 6390 | | - const text = await response.text(); |
| 6391 | | - toastr.error(`Failed to save workflow.\n\n${text}`); |
| 6392 | | - } |
| 6393 | | - saveSettingsDebounced(); |
| 6394 | | - await loadComfyWorkflows(); |
| 6395 | | - await delay(200); |
| 6396 | | - await onComfyOpenWorkflowEditorClick(); |
| 6397 | | -} |
| 6398 | | - |
| 6399 | | -async function onComfyDeleteWorkflowClick() { |
| 6400 | | - const confirm = await callGenericPopup(t`Delete the workflow? This action is irreversible.`, POPUP_TYPE.CONFIRM, '', { okButton: t`Delete`, cancelButton: t`Cancel` }); |
| 6401 | | - if (!confirm) { |
| 6402 | | - return; |
| 6403 | | - } |
| 6404 | | - const response = await fetch('/api/sd/comfy/delete-workflow', { |
| 6405 | | - method: 'POST', |
| 6406 | | - headers: getRequestHeaders(), |
| 6407 | | - body: JSON.stringify({ |
| 6408 | | - file_name: extension_settings.sd.comfy_workflow, |
| 6409 | | - }), |
| 6410 | | - }); |
| 6411 | | - if (!response.ok) { |
| 6412 | | - const text = await response.text(); |
| 6413 | | - toastr.error(`Failed to save workflow.\n\n${text}`); |
| 6414 | | - } |
| 6415 | | - await loadComfyWorkflows(); |
| 6416 | | - onComfyWorkflowChange(); |
| 6417 | | -} |
| 6418 | | - |
| 6419 | | -async function onComfyRenameWorkflowClick() { |
| 6420 | | - const oldName = extension_settings.sd.comfy_workflow; |
| 6421 | | - |
| 6422 | | - if (!oldName) { |
| 6423 | | - return; |
| 6424 | | - } |
| 6425 | | - |
| 6426 | | - let newName = await callGenericPopup(t`Enter new workflow name:`, POPUP_TYPE.INPUT, oldName); |
| 6427 | | - |
| 6428 | | - if (!newName) { |
| 6429 | | - return; |
| 6430 | | - } |
| 6431 | | - |
| 6432 | | - newName = String(newName).trim(); |
| 6433 | | - |
| 6434 | | - if (!newName.toLowerCase().endsWith('.json')) { |
| 6435 | | - newName += '.json'; |
| 6436 | | - } |
| 6437 | | - |
| 6438 | | - if (newName === oldName) { |
| 6439 | | - return; |
| 6440 | | - } |
| 6441 | | - |
| 6442 | | - const existingWorkflow = Array |
| 6443 | | - .from(document.querySelectorAll('#sd_comfy_workflow option')) |
| 6444 | | - .find(opt => opt instanceof HTMLOptionElement && opt.value === newName); |
| 6445 | | - |
| 6446 | | - if (existingWorkflow) { |
| 6447 | | - toastr.warning(t`A workflow with that name already exists`); |
| 6448 | | - return; |
| 6449 | | - } |
| 6450 | | - |
| 6451 | | - const response = await fetch('/api/sd/comfy/rename-workflow', { |
| 6452 | | - method: 'POST', |
| 6453 | | - headers: getRequestHeaders(), |
| 6454 | | - body: JSON.stringify({ |
| 6455 | | - old_name: oldName, |
| 6456 | | - new_name: newName, |
| 6457 | | - }), |
| 6458 | | - }); |
| 6459 | | - |
| 6460 | | - if (!response.ok) { |
| 6461 | | - const text = await response.text(); |
| 6462 | | - toastr.error(t`Failed to rename workflow.\n\n${text}`); |
| 6463 | | - return; |
| 6464 | | - } |
| 6465 | | - |
| 6466 | | - extension_settings.sd.comfy_workflow = newName; |
| 6467 | | - saveSettingsDebounced(); |
| 6468 | | - await loadComfyWorkflows(); |
| 6469 | | -} |
| 6470 | | - |
| 6471 | | -/** |
| 6472 | | - * Sends a chat message with the generated image. |
| 6473 | | - * @param {string} prompt Prompt used for the image generation |
| 6474 | | - * @param {string} image Base64 encoded image |
| 6475 | | - * @param {number} generationType Generation type of the image |
| 6476 | | - * @param {string} additionalNegativePrefix Additional negative prompt used for the image generation |
| 6477 | | - * @param {string} initiator The initiator of the image generation |
| 6478 | | - * @param {string} prefixedPrompt Prompt with an attached specific prefix |
| 6479 | | - * @param {string} format Format of the image (e.g., 'png', 'jpg') |
| 6480 | | - */ |
| 6481 | | -async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, format) { |
| 6482 | | - const context = getContext(); |
| 6483 | | - const name = context.groupId ? systemUserName : context.name2; |
| 6484 | | - const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}'; |
| 6485 | | - const messageText = substituteParamsExtended(template, { char: name, prompt: prompt, prefixedPrompt: prefixedPrompt }); |
| 6486 | | - const mediaType = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; |
| 6487 | | - /** @type {MediaAttachment} */ |
| 6488 | | - const mediaAttachment = { |
| 6489 | | - url: image, |
| 6490 | | - type: mediaType, |
| 6491 | | - title: prompt, |
| 6492 | | - generation_type: generationType, |
| 6493 | | - negative: additionalNegativePrefix, |
| 6494 | | - source: MEDIA_SOURCE.GENERATED, |
| 6495 | | - }; |
| 6496 | | - /** @type {ChatMessage} */ |
| 6497 | | - const message = { |
| 6498 | | - name: name, |
| 6499 | | - is_user: false, |
| 6500 | | - is_system: !getVisibilityByInitiator(initiator), |
| 6501 | | - send_date: getMessageTimeStamp(), |
| 6502 | | - mes: messageText, |
| 6503 | | - extra: { |
| 6504 | | - media: [mediaAttachment], |
| 6505 | | - media_display: MEDIA_DISPLAY.GALLERY, |
| 6506 | | - media_index: 0, |
| 6507 | | - inline_image: false, |
| 6508 | | - sd_prompt: prompt, |
| 6509 | | - sd_prompt_message: messageText, |
| 6510 | | - }, |
| 6511 | | - }; |
| 6512 | | - context.chat.push(message); |
| 6513 | | - const messageId = context.chat.length - 1; |
| 6514 | | - await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension'); |
| 6515 | | - context.addOneMessage(message); |
| 6516 | | - await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension'); |
| 6517 | | - await context.saveChat(); |
| 6518 | | - setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short); |
| 6519 | | -} |
| 6520 | | - |
| 6521 | | -/** |
| 6522 | | - * Gets the visibility of the resulting message based on the initiator. |
| 6523 | | - * @param {string} initiator Generation initiator |
| 6524 | | - * @returns {boolean} Is resulting message visible |
| 6525 | | - */ |
| 6526 | | -function getVisibilityByInitiator(initiator) { |
| 6527 | | - switch (initiator) { |
| 6528 | | - case initiators.interactive: |
| 6529 | | - return !!extension_settings.sd.interactive_visible; |
| 6530 | | - case initiators.wand: |
| 6531 | | - return !!extension_settings.sd.wand_visible; |
| 6532 | | - case initiators.command: |
| 6533 | | - return !!extension_settings.sd.command_visible; |
| 6534 | | - case initiators.tool: |
| 6535 | | - return !!extension_settings.sd.tool_visible; |
| 6536 | | - default: |
| 6537 | | - return false; |
| 6538 | | - } |
| 6539 | | -} |
| 6540 | | - |
| 6541 | | -async function addSDGenButtons() { |
| 6542 | | - const buttonHtml = await renderExtensionTemplateAsync('stable-diffusion', 'button'); |
| 6543 | | - const dropdownHtml = await renderExtensionTemplateAsync('stable-diffusion', 'dropdown'); |
| 6544 | | - |
| 6545 | | - $('#sd_wand_container').append(buttonHtml); |
| 6546 | | - $(document.body).append(dropdownHtml); |
| 6547 | | - |
| 6548 | | - const button = $('#sd_gen'); |
| 6549 | | - const dropdown = $('#sd_dropdown'); |
| 6550 | | - dropdown.hide(); |
| 6551 | | - |
| 6552 | | - let popper = Popper.createPopper(button.get(0), dropdown.get(0), { |
| 6553 | | - placement: 'top', |
| 6554 | | - }); |
| 6555 | | - |
| 6556 | | - $(document).on('click', '.sd_message_gen', (e) => sdMessageButton($(e.currentTarget), { animate: false })); |
| 6557 | | - $(document).on('click', '.mes_edit', rememberImagePromptBeforeEdit); |
| 6558 | | - |
| 6559 | | - $(document).on('click touchend', function (e) { |
| 6560 | | - const target = $(e.target); |
| 6561 | | - if (target.is(dropdown) || target.closest(dropdown).length) return; |
| 6562 | | - if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible')) { |
| 6563 | | - e.preventDefault(); |
| 6564 | | - |
| 6565 | | - dropdown.fadeIn(animation_duration); |
| 6566 | | - popper.update(); |
| 6567 | | - } else { |
| 6568 | | - dropdown.fadeOut(animation_duration); |
| 6569 | | - } |
| 6570 | | - }); |
| 6571 | | - |
| 6572 | | - renderCustomDropdownEntries(); |
| 6573 | | - |
| 6574 | | - // Use event delegation so dynamically-added custom entries also respond to clicks. |
| 6575 | | - $('#sd_dropdown').on('click', 'li[id]', function () { |
| 6576 | | - dropdown.fadeOut(animation_duration); |
| 6577 | | - const id = $(this).attr('id'); |
| 6578 | | - const idParamMap = { |
| 6579 | | - 'sd_you': 'you', |
| 6580 | | - 'sd_face': 'face', |
| 6581 | | - 'sd_me': 'me', |
| 6582 | | - 'sd_world': 'scene', |
| 6583 | | - 'sd_last': 'last', |
| 6584 | | - 'sd_raw_last': 'raw_last', |
| 6585 | | - 'sd_background': 'background', |
| 6586 | | - }; |
| 6587 | | - |
| 6588 | | - const param = idParamMap[id]; |
| 6589 | | - |
| 6590 | | - if (param) { |
| 6591 | | - console.log('doing /sd ' + param); |
| 6592 | | - generatePicture(initiators.wand, {}, param); |
| 6593 | | - return; |
| 6594 | | - } |
| 6595 | | - |
| 6596 | | - if (id && id.startsWith('sd_custom_')) { |
| 6597 | | - const entryId = id.slice('sd_custom_'.length); |
| 6598 | | - console.log('doing /sd custom_' + entryId); |
| 6599 | | - generatePicture(initiators.wand, {}, 'custom_' + entryId); |
| 6600 | | - } |
| 6601 | | - }); |
| 6602 | | -} |
| 6603 | | - |
| 6604 | | -/** |
| 6605 | | - * Renders the user-defined custom entries into the wand dropdown. |
| 6606 | | - * Removes any previously rendered custom entries first. |
| 6607 | | - */ |
| 6608 | | -function renderCustomDropdownEntries() { |
| 6609 | | - const list = $('#sd_dropdown ul.list-group'); |
| 6610 | | - if (!list.length) { |
| 6611 | | - return; |
| 6612 | | - } |
| 6613 | | - |
| 6614 | | - list.find('li.sd_custom_entry').remove(); |
| 6615 | | - |
| 6616 | | - const entries = Array.isArray(extension_settings.sd.custom_entries) ? extension_settings.sd.custom_entries : []; |
| 6617 | | - for (const entry of entries) { |
| 6618 | | - const li = $('<li></li>') |
| 6619 | | - .addClass('list-group-item sd_custom_entry') |
| 6620 | | - .attr('id', 'sd_custom_' + entry.id) |
| 6621 | | - .text(entry.title); |
| 6622 | | - list.append(li); |
| 6623 | | - } |
| 6624 | | -} |
| 6625 | | - |
| 6626 | | -function isValidState() { |
| 6627 | | - switch (extension_settings.sd.source) { |
| 6628 | | - case sources.extras: |
| 6629 | | - return modules.includes('sd'); |
| 6630 | | - case sources.horde: |
| 6631 | | - return true; |
| 6632 | | - case sources.auto: |
| 6633 | | - return !!extension_settings.sd.auto_url; |
| 6634 | | - case sources.sdcpp: |
| 6635 | | - return !!extension_settings.sd.sdcpp_url; |
| 6636 | | - case sources.drawthings: |
| 6637 | | - return !!extension_settings.sd.drawthings_url; |
| 6638 | | - case sources.vlad: |
| 6639 | | - return !!extension_settings.sd.vlad_url; |
| 6640 | | - case sources.novel: |
| 6641 | | - return secret_state[SECRET_KEYS.NOVEL]; |
| 6642 | | - case sources.openai: |