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

5273c09fac552c7004b0f777c2d557a4202c6c1a

permissionBRICK <permissionBRICK@gmail.com>

2 files changed, +215 -28Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+200 -28
@@ -360,8 +360,83 @@ const defaultSettings = {
360360 google_api: 'makersuite',
361361 google_enhance: true,
362362 google_duration: 6,
363+
364+ // Settings presets & auto-fallback
365+ settings_preset_primary: null,
366+ settings_preset_secondary: null,
367+ settings_fallback_enabled: false,
363368};
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+ */
376+const 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+ */
396+function 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+ */
411+function 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+ */
425+function 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+ */
434+async function refreshSettingsUi() {
435+ // loadSettings() appends to #sd_style without clearing it, so empty it first.
436+ $('#sd_style').empty();
437+ await loadSettings();
438+}
439+
365440const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
366441const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase());
367442
@@ -495,6 +570,19 @@ async function loadSettings() {
495570 extension_settings.sd.styles = defaultStyles;
496571 }
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+
498586 // Preserve an original seed if exists
499587 if (extension_settings.sd.original_seed >= 0) {
500588 extension_settings.sd.seed = extension_settings.sd.original_seed;
@@ -560,6 +648,7 @@ async function loadSettings() {
560648 $('#sd_google_api').val(extension_settings.sd.google_api);
561649 $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance);
562650 $('#sd_google_duration').val(extension_settings.sd.google_duration);
651+ $('#sd_fallback_enabled').prop('checked', extension_settings.sd.settings_fallback_enabled);
563652
564653 for (const style of extension_settings.sd.styles) {
565654 const option = document.createElement('option');
@@ -803,6 +892,47 @@ async function onRenameStyleClick() {
803892 saveSettingsDebounced();
804893}
805894
895+function onSavePresetPrimaryClick() {
896+ extension_settings.sd.settings_preset_primary = snapshotSdSettings();
897+ saveSettingsDebounced();
898+ toastr.success(t`Primary settings preset saved.`, t`Image Generation`);
899+}
900+
901+function onSavePresetSecondaryClick() {
902+ extension_settings.sd.settings_preset_secondary = snapshotSdSettings();
903+ saveSettingsDebounced();
904+ toastr.success(t`Secondary settings preset saved.`, t`Image Generation`);
905+}
906+
907+async 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+
919+async 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+
931+function onFallbackEnabledChange() {
932+ extension_settings.sd.settings_fallback_enabled = !!$(this).prop('checked');
933+ saveSettingsDebounced();
934+}
935+
806936/**
807937 * Modifies prompt based on user inputs.
808938 * @param {string} prompt Prompt to refine
@@ -3321,6 +3451,14 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
33213451
33223452 const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType);
33233453
3454+ /**
3455+ * Performs a single image generation attempt against the live extension_settings.sd config.
3456+ * Reads settings live so it can be retried after applying a fallback preset.
3457+ * @param {AbortSignal} attemptSignal Abort signal to cancel the request.
3458+ * @returns {Promise<{result: {format: string, data: string}, prefixedPrompt: string}>}
3459+ * @throws {Error} On failure or when the endpoint returns no image data.
3460+ */
3461+ async function attemptImageGeneration(attemptSignal) {
33243462 const prefix = skipCharPrefix
33253463 ? extension_settings.sd.prompt_prefix
33263464 : combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix());
@@ -3333,96 +3471,101 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
33333471 const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix));
33343472
33353473 let result = { format: '', data: '' };
3336- const currentChatId = getCurrentChatId();
3337-
3338- try {
33393474 switch (extension_settings.sd.source) {
33403475 case sources.extras:
33413476 result = await generateExtrasImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33423477 break;
33433478 case sources.horde:
33443479 result = await generateHordeImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33453480 break;
33463481 case sources.vlad:
33473482 result = await generateAutoImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33483483 break;
33493484 case sources.drawthings:
33503485 result = await generateDrawthingsImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33513486 break;
33523487 case sources.auto:
33533488 result = await generateAutoImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33543489 break;
33553490 case sources.sdcpp:
33563491 result = await generateSdcppImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33573492 break;
33583493 case sources.novel:
33593494 result = await generateNovelImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33603495 break;
33613496 case sources.openai:
33623497 result = await generateOpenAiImage(prefixedPrompt, signalattemptSignal);
33633498 break;
33643499 case sources.aimlapi:
33653500 result = await generateAimlapiImage(prefixedPrompt, signalattemptSignal);
33663501 break;
33673502 case sources.comfy:
33683503 switch (extension_settings.sd.comfy_type) {
33693504 case comfyTypes.runpod_serverless:
33703505 result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33713506 break;
33723507 case comfyTypes.standard:
33733508 result = await generateComfyImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33743509 break;
33753510 default:
33763511 throw new Error('Unknown comfyUI server type.');
33773512 }
33783513 break;
33793514 case sources.togetherai:
33803515 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33813516 break;
33823517 case sources.pollinations:
33833518 result = await generatePollinationsImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33843519 break;
33853520 case sources.stability:
33863521 result = await generateStabilityImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33873522 break;
33883523 case sources.huggingface:
33893524 result = await generateHuggingFaceImage(prefixedPrompt, signalattemptSignal);
33903525 break;
33913526 case sources.chutes:
33923527 result = await generateChutesImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33933528 break;
33943529 case sources.electronhub:
33953530 result = await generateElectronHubImage(prefixedPrompt, signalattemptSignal);
33963531 break;
33973532 case sources.nanogpt:
33983533 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signalattemptSignal);
33993534 break;
34003535 case sources.bfl:
34013536 result = await generateBflImage(prefixedPrompt, signalattemptSignal);
34023537 break;
34033538 case sources.falai:
34043539 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signalattemptSignal);
34053540 break;
34063541 case sources.xai:
34073542 result = await generateXAIImage(prefixedPrompt, negativePrompt, signalattemptSignal);
34083543 break;
34093544 case sources.google:
34103545 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signalattemptSignal);
34113546 break;
34123547 case sources.zai:
34133548 result = await generateZaiImage(prefixedPrompt, signalattemptSignal);
34143549 break;
34153550 case sources.openrouter:
34163551 result = await generateOpenRouterImage(prefixedPrompt, signalattemptSignal);
34173552 break;
34183553 case sources.workersai:
34193554 result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, signalattemptSignal);
34203555 break;
34213556 }
34223557
34233558 if (!result.data) {
34243559 throw new Error('Endpoint did not return image data.');
34253560 }
3561+
3562+ return { result, prefixedPrompt };
3563+ }
3564+
3565+ const currentChatId = getCurrentChatId();
3566+ let genOutput;
3567+ try {
3568+ genOutput = await attemptImageGeneration(signal);
34263569 } catch (err) {
34273570 // Check if this was an intentional abort by user
34283571 if (signal?.aborted) {
@@ -3431,10 +3574,34 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
34313574 return;
34323575 }
34333576
3577+ if (extension_settings.sd.settings_fallback_enabled && isPresetConfigured(extension_settings.sd.settings_preset_secondary)) {
3578+ console.warn('SD: primary generation failed, falling back to secondary preset', err);
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 {
34343598 console.error('Image generation request error: ', err);
34353599 toastr.error('Image generation failed. Please try again.' + '\n\n' + String(err), 'Image Generation');
34363600 return;
34373601 }
3602+ }
3603+
3604+ const { result, prefixedPrompt } = genOutput;
34383605
34393606 if (currentChatId !== getCurrentChatId()) {
34403607 console.warn('Chat changed, aborting SD result saving');
@@ -5859,6 +6026,11 @@ export async function init() {
58596026 $('#sd_save_style').on('click', onSaveStyleClick);
58606027 $('#sd_rename_style').on('click', onRenameStyleClick);
58616028 $('#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);
58626034 $('#sd_character_prompt_block').hide();
58636035 $('#sd_interactive_mode').on('input', onInteractiveModeInput);
58646036 $('#sd_openai_style').on('change', onOpenAiStyleSelect);
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -39,6 +39,21 @@
3939 <input id="sd_minimal_prompt_processing" type="checkbox" />
4040 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>
4141 </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>
4257 <label for="sd_source" data-i18n="Source">Source</label>
4358 <select id="sd_source" class="text_pole">
4459 <option value="aimlapi">AI/ML API</option>