Stable Diffusion: primary/secondary settings presets with auto-fallback Reviewed-by: auto-review

5273c09fac552c7004b0f777c2d557a4202c6c1a

permissionBRICK <permissionBRICK@gmail.com>

2 files changed, +227 -40Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+212 -40
@@ -360,8 +360,83 @@ const defaultSettings = {
360 google_api: 'makersuite',360 google_api: 'makersuite',
361 google_enhance: true,361 google_enhance: true,
362 google_duration: 6,362 google_duration: 6,
363
364 // Settings presets & auto-fallback
365 settings_preset_primary: null,
366 settings_preset_secondary: null,
367 settings_fallback_enabled: false,
363};368};
364369
370/**
371 * Keys that are NOT part of a settings preset snapshot.
372 * A preset captures backend/connection params (source, model, sampler, dimensions, etc.)
373 * but not the prompt library, styles, custom entries, or UI prefs.
374 * @type {string[]}
375 */
376const PRESET_EXCLUDE_KEYS = [
377 'settings_preset_primary',
378 'settings_preset_secondary',
379 'settings_fallback_enabled',
380 'prompts',
381 'character_prompts',
382 'character_negative_prompts',
383 'styles',
384 'custom_entries',
385 'interactive_visible',
386 'wand_visible',
387 'command_visible',
388 'tool_visible',
389 'expand',
390];
391
392/**
393 * Creates a deep clone snapshot of the settings keys that belong to a preset.
394 * @returns {object} Snapshot of the included settings keys.
395 */
396function snapshotSdSettings() {
397 const snapshot = {};
398 for (const key of Object.keys(extension_settings.sd)) {
399 if (PRESET_EXCLUDE_KEYS.includes(key)) {
400 continue;
401 }
402 snapshot[key] = structuredClone(extension_settings.sd[key]);
403 }
404 return snapshot;
405}
406
407/**
408 * Applies a settings snapshot to the live settings object (in-memory only; does NOT touch the DOM).
409 * @param {object} snapshot Snapshot previously created by snapshotSdSettings().
410 */
411function applySdSettingsSnapshot(snapshot) {
412 if (!snapshot || typeof snapshot !== 'object') {
413 return;
414 }
415 for (const key of Object.keys(snapshot)) {
416 extension_settings.sd[key] = structuredClone(snapshot[key]);
417 }
418}
419
420/**
421 * Checks whether a preset is configured (a non-null object with at least one key).
422 * @param {object} preset Preset to check.
423 * @returns {boolean} True if the preset is configured.
424 */
425function isPresetConfigured(preset) {
426 return !!preset && typeof preset === 'object' && Object.keys(preset).length > 0;
427}
428
429/**
430 * Refreshes all settings UI controls to reflect the current extension_settings.sd values.
431 * Used after loading a settings preset.
432 * @returns {Promise<void>}
433 */
434async function refreshSettingsUi() {
435 // loadSettings() appends to #sd_style without clearing it, so empty it first.
436 $('#sd_style').empty();
437 await loadSettings();
438}
439
365const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);440const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
366const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase());441const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase());
367442
@@ -495,6 +570,19 @@ async function loadSettings() {
495 extension_settings.sd.styles = defaultStyles;570 extension_settings.sd.styles = defaultStyles;
496 }571 }
497572
573 // Settings presets & auto-fallback
574 if (extension_settings.sd.settings_preset_primary === undefined) {
575 extension_settings.sd.settings_preset_primary = null;
576 }
577
578 if (extension_settings.sd.settings_preset_secondary === undefined) {
579 extension_settings.sd.settings_preset_secondary = null;
580 }
581
582 if (extension_settings.sd.settings_fallback_enabled === undefined) {
583 extension_settings.sd.settings_fallback_enabled = false;
584 }
585
498 // Preserve an original seed if exists586 // Preserve an original seed if exists
499 if (extension_settings.sd.original_seed >= 0) {587 if (extension_settings.sd.original_seed >= 0) {
500 extension_settings.sd.seed = extension_settings.sd.original_seed;588 extension_settings.sd.seed = extension_settings.sd.original_seed;
@@ -560,6 +648,7 @@ async function loadSettings() {
560 $('#sd_google_api').val(extension_settings.sd.google_api);648 $('#sd_google_api').val(extension_settings.sd.google_api);
561 $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance);649 $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance);
562 $('#sd_google_duration').val(extension_settings.sd.google_duration);650 $('#sd_google_duration').val(extension_settings.sd.google_duration);
651 $('#sd_fallback_enabled').prop('checked', extension_settings.sd.settings_fallback_enabled);
563652
564 for (const style of extension_settings.sd.styles) {653 for (const style of extension_settings.sd.styles) {
565 const option = document.createElement('option');654 const option = document.createElement('option');
@@ -803,6 +892,47 @@ async function onRenameStyleClick() {
803 saveSettingsDebounced();892 saveSettingsDebounced();
804}893}
805894
895function onSavePresetPrimaryClick() {
896 extension_settings.sd.settings_preset_primary = snapshotSdSettings();
897 saveSettingsDebounced();
898 toastr.success(t`Primary settings preset saved.`, t`Image Generation`);
899}
900
901function onSavePresetSecondaryClick() {
902 extension_settings.sd.settings_preset_secondary = snapshotSdSettings();
903 saveSettingsDebounced();
904 toastr.success(t`Secondary settings preset saved.`, t`Image Generation`);
905}
906
907async function onLoadPresetPrimaryClick() {
908 if (!isPresetConfigured(extension_settings.sd.settings_preset_primary)) {
909 toastr.info(t`No primary settings preset has been saved yet.`, t`Image Generation`);
910 return;
911 }
912
913 applySdSettingsSnapshot(extension_settings.sd.settings_preset_primary);
914 saveSettingsDebounced();
915 await refreshSettingsUi();
916 toastr.success(t`Primary settings preset loaded.`, t`Image Generation`);
917}
918
919async function onLoadPresetSecondaryClick() {
920 if (!isPresetConfigured(extension_settings.sd.settings_preset_secondary)) {
921 toastr.info(t`No secondary settings preset has been saved yet.`, t`Image Generation`);
922 return;
923 }
924
925 applySdSettingsSnapshot(extension_settings.sd.settings_preset_secondary);
926 saveSettingsDebounced();
927 await refreshSettingsUi();
928 toastr.success(t`Secondary settings preset loaded.`, t`Image Generation`);
929}
930
931function onFallbackEnabledChange() {
932 extension_settings.sd.settings_fallback_enabled = !!$(this).prop('checked');
933 saveSettingsDebounced();
934}
935
806/**936/**
807 * Modifies prompt based on user inputs.937 * Modifies prompt based on user inputs.
808 * @param {string} prompt Prompt to refine938 * @param {string} prompt Prompt to refine
@@ -3321,108 +3451,121 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
33213451
3322 const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType);3452 const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType);
33233453
3324 const prefix = skipCharPrefix3454 /**
3325 ? extension_settings.sd.prompt_prefix3455 * Performs a single image generation attempt against the live extension_settings.sd config.
3326 : combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix());3456 * Reads settings live so it can be retried after applying a fallback preset.
33273457 * @param {AbortSignal} attemptSignal Abort signal to cancel the request.
3328 const negativePrefix = skipCharPrefix3458 * @returns {Promise<{result: {format: string, data: string}, prefixedPrompt: string}>}
3329 ? extension_settings.sd.negative_prompt3459 * @throws {Error} On failure or when the endpoint returns no image data.
3330 : combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix());3460 */
3461 async function attemptImageGeneration(attemptSignal) {
3462 const prefix = skipCharPrefix
3463 ? extension_settings.sd.prompt_prefix
3464 : combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix());
33313465
3332 const prefixedPrompt = substituteParams(combinePrefixes(prefix, prompt, '{prompt}'));3466 const negativePrefix = skipCharPrefix
3333 const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix));3467 ? extension_settings.sd.negative_prompt
3468 : combinePrefixes(extension_settings.sd.negative_prompt, getCharacterNegativePrefix());
33343469
3335 let result = { format: '', data: '' };3470 const prefixedPrompt = substituteParams(combinePrefixes(prefix, prompt, '{prompt}'));
3336 const currentChatId = getCurrentChatId();3471 const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix));
33373472
3338 try {3473 let result = { format: '', data: '' };
3339 switch (extension_settings.sd.source) {3474 switch (extension_settings.sd.source) {
3340 case sources.extras:3475 case sources.extras:
3341 result = await generateExtrasImage(prefixedPrompt, negativePrompt, signal);3476 result = await generateExtrasImage(prefixedPrompt, negativePrompt, attemptSignal);
3342 break;3477 break;
3343 case sources.horde:3478 case sources.horde:
3344 result = await generateHordeImage(prefixedPrompt, negativePrompt, signal);3479 result = await generateHordeImage(prefixedPrompt, negativePrompt, attemptSignal);
3345 break;3480 break;
3346 case sources.vlad:3481 case sources.vlad:
3347 result = await generateAutoImage(prefixedPrompt, negativePrompt, signal);3482 result = await generateAutoImage(prefixedPrompt, negativePrompt, attemptSignal);
3348 break;3483 break;
3349 case sources.drawthings:3484 case sources.drawthings:
3350 result = await generateDrawthingsImage(prefixedPrompt, negativePrompt, signal);3485 result = await generateDrawthingsImage(prefixedPrompt, negativePrompt, attemptSignal);
3351 break;3486 break;
3352 case sources.auto:3487 case sources.auto:
3353 result = await generateAutoImage(prefixedPrompt, negativePrompt, signal);3488 result = await generateAutoImage(prefixedPrompt, negativePrompt, attemptSignal);
3354 break;3489 break;
3355 case sources.sdcpp:3490 case sources.sdcpp:
3356 result = await generateSdcppImage(prefixedPrompt, negativePrompt, signal);3491 result = await generateSdcppImage(prefixedPrompt, negativePrompt, attemptSignal);
3357 break;3492 break;
3358 case sources.novel:3493 case sources.novel:
3359 result = await generateNovelImage(prefixedPrompt, negativePrompt, signal);3494 result = await generateNovelImage(prefixedPrompt, negativePrompt, attemptSignal);
3360 break;3495 break;
3361 case sources.openai:3496 case sources.openai:
3362 result = await generateOpenAiImage(prefixedPrompt, signal);3497 result = await generateOpenAiImage(prefixedPrompt, attemptSignal);
3363 break;3498 break;
3364 case sources.aimlapi:3499 case sources.aimlapi:
3365 result = await generateAimlapiImage(prefixedPrompt, signal);3500 result = await generateAimlapiImage(prefixedPrompt, attemptSignal);
3366 break;3501 break;
3367 case sources.comfy:3502 case sources.comfy:
3368 switch (extension_settings.sd.comfy_type) {3503 switch (extension_settings.sd.comfy_type) {
3369 case comfyTypes.runpod_serverless:3504 case comfyTypes.runpod_serverless:
3370 result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, signal);3505 result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, attemptSignal);
3371 break;3506 break;
3372 case comfyTypes.standard:3507 case comfyTypes.standard:
3373 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);3508 result = await generateComfyImage(prefixedPrompt, negativePrompt, attemptSignal);
3374 break;3509 break;
3375 default:3510 default:
3376 throw new Error('Unknown comfyUI server type.');3511 throw new Error('Unknown comfyUI server type.');
3377 }3512 }
3378 break;3513 break;
3379 case sources.togetherai:3514 case sources.togetherai:
3380 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);3515 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, attemptSignal);
3381 break;3516 break;
3382 case sources.pollinations:3517 case sources.pollinations:
3383 result = await generatePollinationsImage(prefixedPrompt, negativePrompt, signal);3518 result = await generatePollinationsImage(prefixedPrompt, negativePrompt, attemptSignal);
3384 break;3519 break;
3385 case sources.stability:3520 case sources.stability:
3386 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signal);3521 result = await generateStabilityImage(prefixedPrompt, negativePrompt, attemptSignal);
3387 break;3522 break;
3388 case sources.huggingface:3523 case sources.huggingface:
3389 result = await generateHuggingFaceImage(prefixedPrompt, signal);3524 result = await generateHuggingFaceImage(prefixedPrompt, attemptSignal);
3390 break;3525 break;
3391 case sources.chutes:3526 case sources.chutes:
3392 result = await generateChutesImage(prefixedPrompt, negativePrompt, signal);3527 result = await generateChutesImage(prefixedPrompt, negativePrompt, attemptSignal);
3393 break;3528 break;
3394 case sources.electronhub:3529 case sources.electronhub:
3395 result = await generateElectronHubImage(prefixedPrompt, signal);3530 result = await generateElectronHubImage(prefixedPrompt, attemptSignal);
3396 break;3531 break;
3397 case sources.nanogpt:3532 case sources.nanogpt:
3398 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal);3533 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, attemptSignal);
3399 break;3534 break;
3400 case sources.bfl:3535 case sources.bfl:
3401 result = await generateBflImage(prefixedPrompt, signal);3536 result = await generateBflImage(prefixedPrompt, attemptSignal);
3402 break;3537 break;
3403 case sources.falai:3538 case sources.falai:
3404 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);3539 result = await generateFalaiImage(prefixedPrompt, negativePrompt, attemptSignal);
3405 break;3540 break;
3406 case sources.xai:3541 case sources.xai:
3407 result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);3542 result = await generateXAIImage(prefixedPrompt, negativePrompt, attemptSignal);
3408 break;3543 break;
3409 case sources.google:3544 case sources.google:
3410 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);3545 result = await generateGoogleImage(prefixedPrompt, negativePrompt, attemptSignal);
3411 break;3546 break;
3412 case sources.zai:3547 case sources.zai:
3413 result = await generateZaiImage(prefixedPrompt, signal);3548 result = await generateZaiImage(prefixedPrompt, attemptSignal);
3414 break;3549 break;
3415 case sources.openrouter:3550 case sources.openrouter:
3416 result = await generateOpenRouterImage(prefixedPrompt, signal);3551 result = await generateOpenRouterImage(prefixedPrompt, attemptSignal);
3417 break;3552 break;
3418 case sources.workersai:3553 case sources.workersai:
3419 result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, signal);3554 result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, attemptSignal);
3420 break;3555 break;
3421 }3556 }
34223557
3423 if (!result.data) {3558 if (!result.data) {
3424 throw new Error('Endpoint did not return image data.');3559 throw new Error('Endpoint did not return image data.');
3425 }3560 }
3561
3562 return { result, prefixedPrompt };
3563 }
3564
3565 const currentChatId = getCurrentChatId();
3566 let genOutput;
3567 try {
3568 genOutput = await attemptImageGeneration(signal);
3426 } catch (err) {3569 } catch (err) {
3427 // Check if this was an intentional abort by user3570 // Check if this was an intentional abort by user
3428 if (signal?.aborted) {3571 if (signal?.aborted) {
@@ -3431,11 +3574,35 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
3431 return;3574 return;
3432 }3575 }
34333576
3434 console.error('Image generation request error: ', err);3577 if (extension_settings.sd.settings_fallback_enabled && isPresetConfigured(extension_settings.sd.settings_preset_secondary)) {
3435 toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');3578 console.warn('SD: primary generation failed, falling back to secondary preset', err);
3436 return;3579 toastr.warning('Primary image generation failed. Retrying with secondary settings…', 'Image Generation');
3580 const restore = snapshotSdSettings();
3581 try {
3582 applySdSettingsSnapshot(extension_settings.sd.settings_preset_secondary);
3583 genOutput = await attemptImageGeneration(signal);
3584 } catch (err2) {
3585 applySdSettingsSnapshot(restore);
3586 if (signal?.aborted) {
3587 console.log('SD: Image generation aborted by user');
3588 toastr.info('Image generation stopped.', 'Image Generation');
3589 return;
3590 }
3591 console.error('SD: secondary generation also failed', err2);
3592 toastr.error('Image generation failed (primary and secondary).' + '\n\n' + String(err2), 'Image Generation');
3593 return;
3594 }
3595 // Restore live (primary) settings after a successful fallback attempt.
3596 applySdSettingsSnapshot(restore);
3597 } else {
3598 console.error('Image generation request error: ', err);
3599 toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');
3600 return;
3601 }
3437 }3602 }
34383603
3604 const { result, prefixedPrompt } = genOutput;
3605
3439 if (currentChatId !== getCurrentChatId()) {3606 if (currentChatId !== getCurrentChatId()) {
3440 console.warn('Chat changed, aborting SD result saving');3607 console.warn('Chat changed, aborting SD result saving');
3441 toastr.warning('Chat changed, generated image discarded.', 'Image Generation');3608 toastr.warning('Chat changed, generated image discarded.', 'Image Generation');
@@ -5859,6 +6026,11 @@ export async function init() {
5859 $('#sd_save_style').on('click', onSaveStyleClick);6026 $('#sd_save_style').on('click', onSaveStyleClick);
5860 $('#sd_rename_style').on('click', onRenameStyleClick);6027 $('#sd_rename_style').on('click', onRenameStyleClick);
5861 $('#sd_delete_style').on('click', onDeleteStyleClick);6028 $('#sd_delete_style').on('click', onDeleteStyleClick);
6029 $('#sd_save_preset_primary').on('click', onSavePresetPrimaryClick);
6030 $('#sd_save_preset_secondary').on('click', onSavePresetSecondaryClick);
6031 $('#sd_load_preset_primary').on('click', onLoadPresetPrimaryClick);
6032 $('#sd_load_preset_secondary').on('click', onLoadPresetSecondaryClick);
6033 $('#sd_fallback_enabled').on('change', onFallbackEnabledChange);
5862 $('#sd_character_prompt_block').hide();6034 $('#sd_character_prompt_block').hide();
5863 $('#sd_interactive_mode').on('input', onInteractiveModeInput);6035 $('#sd_interactive_mode').on('input', onInteractiveModeInput);
5864 $('#sd_openai_style').on('change', onOpenAiStyleSelect);6036 $('#sd_openai_style').on('change', onOpenAiStyleSelect);
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -39,6 +39,21 @@
39 <input id="sd_minimal_prompt_processing" type="checkbox" />39 <input id="sd_minimal_prompt_processing" type="checkbox" />
40 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>40 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>
41 </label>41 </label>
42 <hr>
43 <h4 data-i18n="Settings Presets & Fallback">Settings Presets &amp; Fallback</h4>
44 <small data-i18n="sd_settings_presets_small">Save the current backend/connection settings (source, model, sampler, dimensions, etc.) as Primary or Secondary presets and load them back later. Prompt templates, styles, and custom entries are not included.</small>
45 <div class="flex-container marginTopBot5 flexWrap">
46 <div id="sd_save_preset_primary" class="menu_button" data-i18n="Save as Primary">Save as Primary</div>
47 <div id="sd_load_preset_primary" class="menu_button" data-i18n="Load Primary">Load Primary</div>
48 <div id="sd_save_preset_secondary" class="menu_button" data-i18n="Save as Secondary">Save as Secondary</div>
49 <div id="sd_load_preset_secondary" class="menu_button" data-i18n="Load Secondary">Load Secondary</div>
50 </div>
51 <label for="sd_fallback_enabled" class="checkbox_label" data-i18n="[title]sd_fallback_enabled" title="If an image generation fails, automatically retry the same prompt using the Secondary preset's settings.">
52 <input id="sd_fallback_enabled" type="checkbox" />
53 <span data-i18n="sd_fallback_enabled_txt">Auto-fallback to Secondary on failure</span>
54 </label>
55 <small data-i18n="sd_fallback_enabled_small">When enabled, a failed generation is retried once with the Secondary preset before reporting an error.</small>
56 <hr>
42 <label for="sd_source" data-i18n="Source">Source</label>57 <label for="sd_source" data-i18n="Source">Source</label>
43 <select id="sd_source" class="text_pole">58 <select id="sd_source" class="text_pole">
44 <option value="aimlapi">AI/ML API</option>59 <option value="aimlapi">AI/ML API</option>