Stable Diffusion: primary/secondary settings presets with auto-fallback Reviewed-by: auto-review
| @@ -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 | }; |
| 364 | 369 | ||
| 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 | |||
| 365 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); | 440 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| 366 | const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase()); | 441 | const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase()); |
| 367 | 442 | ||
| @@ -495,6 +570,19 @@ async function loadSettings() { | |||
| 495 | extension_settings.sd.styles = defaultStyles; | 570 | extension_settings.sd.styles = defaultStyles; |
| 496 | } | 571 | } |
| 497 | 572 | ||
| 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 exists | 586 | // 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); | ||
| 563 | 652 | ||
| 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 | } |
| 805 | 894 | ||
| 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 | |||
| 806 | /** | 936 | /** |
| 807 | * Modifies prompt based on user inputs. | 937 | * Modifies prompt based on user inputs. |
| 808 | * @param {string} prompt Prompt to refine | 938 | * @param {string} prompt Prompt to refine |
| @@ -3321,108 +3451,121 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 3321 | 3451 | ||
| 3322 | const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType); | 3452 | const skipCharPrefix = !ignoreNoCharForSwipe && noCharPrefix.includes(generationType); |
| 3323 | 3453 | ||
| 3324 | const prefix = skipCharPrefix | 3454 | /** |
| 3325 | ? extension_settings.sd.prompt_prefix | 3455 | * 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. |
| 3327 | 3457 | * @param {AbortSignal} attemptSignal Abort signal to cancel the request. | |
| 3328 | const negativePrefix = skipCharPrefix | 3458 | * @returns {Promise<{result: {format: string, data: string}, prefixedPrompt: string}>} |
| 3329 | ? extension_settings.sd.negative_prompt | 3459 | * @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()); | ||
| 3331 | 3465 | ||
| 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()); | ||
| 3334 | 3469 | ||
| 3335 | let result = { format: '', data: '' }; | 3470 | const prefixedPrompt = substituteParams(combinePrefixes(prefix, prompt, '{prompt}')); |
| 3336 | const currentChatId = getCurrentChatId(); | 3471 | const negativePrompt = substituteParams(combinePrefixes(additionalNegativePrefix, negativePrefix)); |
| 3337 | 3472 | ||
| 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 | } |
| 3422 | 3557 | ||
| 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 user | 3570 | // 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 | } |
| 3433 | 3576 | ||
| 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 | } |
| 3438 | 3603 | ||
| 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); |
| @@ -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 & 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> |