Stable Diffusion: fix image-prompt profile being ignored (exclude from presets; generate under the chosen profile with full context)
| @@ -385,6 +385,9 @@ const PRESET_EXCLUDE_KEYS = [ | ||
| 385 | 385 | 'settings_preset_primary', |
| 386 | 386 | 'settings_preset_secondary', |
| 387 | 387 | 'settings_fallback_enabled', |
| 388 | + // The image-prompt LLM profile is independent of the image backend, so it must | |
| 389 | + // never be captured/swapped by image-generation presets or the fallback retry. | |
| 390 | + 'prompt_generation_profile', | |
| 388 | 391 | 'prompts', |
| 389 | 392 | 'character_prompts', |
| 390 | 393 | 'character_negative_prompts', |
| @@ -421,6 +424,11 @@ function applySdSettingsSnapshot(snapshot) { | ||
| 421 | 424 | return; |
| 422 | 425 | } |
| 423 | 426 | for (const key of Object.keys(snapshot)) { |
| 427 | + // Skip excluded keys so older presets that were saved before a key was | |
| 428 | + // excluded (e.g. prompt_generation_profile) can't clobber it on load. | |
| 429 | + if (PRESET_EXCLUDE_KEYS.includes(key)) { | |
| 430 | + continue; | |
| 431 | + } | |
| 424 | 432 | extension_settings.sd[key] = structuredClone(snapshot[key]); |
| 425 | 433 | } |
| 426 | 434 | } |
| @@ -543,6 +551,9 @@ function toggleSourceControls() { | ||
| 543 | 551 | } |
| 544 | 552 | |
| 545 | 553 | let promptGenerationProfileDropdownInitialized = false; |
| 554 | +// Warn at most once per session if a prompt-gen profile is selected but cannot be | |
| 555 | +// applied because there is no active connection profile to restore afterward. | |
| 556 | +let promptProfileWarnedNoBaseProfile = false; | |
| 546 | 557 | |
| 547 | 558 | /** |
| 548 | 559 | * Populates the dedicated prompt-generation connection profile dropdown. |
| @@ -3652,26 +3663,18 @@ function getUserAvatarUrl() { | ||
| 3652 | 3663 | */ |
| 3653 | 3664 | async function generatePrompt(quietPrompt) { |
| 3654 | 3665 | const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation'); |
| 3655 | - let reply; | |
| 3656 | 3666 | const profileId = extension_settings.sd.prompt_generation_profile; |
| 3667 | + let reply; | |
| 3657 | 3668 | |
| 3658 | - if (profileId) { | |
| 3659 | 3669 | try { |
| 3660 | - // Use the chosen Connection Manager profile (non-streaming) regardless of the active model. | |
| 3670 | + reply = profileId | |
| 3661 | - // 1024 tokens is a sensible default cap for an image-prompt description. | |
| 3671 | + ? await withConnectionProfile(profileId, () => generateQuietPrompt({ quietPrompt })) | |
| 3662 | 3672 | const data =: await ConnectionManagerRequestService.sendRequestgenerateQuietPrompt(profileId,{ quietPrompt, 1024}); |
| 3663 | - // Non-streaming requests resolve to ExtractedData ({ content, reasoning }); extract the plain text. | |
| 3673 | + } finally { | |
| 3664 | - reply = typeof data === 'object' && data !== null ? String(data.content ?? '') : ''; | |
| 3674 | + toastr.clear(toast); | |
| 3665 | - } catch (err) { | |
| 3666 | - console.warn('SD: prompt-generation profile failed, falling back to active model', err); | |
| 3667 | - reply = await generateQuietPrompt({ quietPrompt }); | |
| 3668 | - } | |
| 3669 | - } else { | |
| 3670 | - reply = await generateQuietPrompt({ quietPrompt }); | |
| 3671 | 3675 | } |
| 3672 | 3676 | |
| 3673 | 3677 | const processedReply = processReply(reply); |
| 3674 | - toastr.clear(toast); | |
| 3675 | 3678 | |
| 3676 | 3679 | if (!processedReply) { |
| 3677 | 3680 | toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation'); |
| @@ -3682,6 +3685,65 @@ async function generatePrompt(quietPrompt) { | ||
| 3682 | 3685 | } |
| 3683 | 3686 | |
| 3684 | 3687 | /** |
| 3688 | + * Runs a callback with a specific Connection Manager profile temporarily active, | |
| 3689 | + * then restores the previously active profile. This lets the image prompt be | |
| 3690 | + * generated by the chosen LLM *with the full chat context* (via generateQuietPrompt), | |
| 3691 | + * instead of a context-free one-off request. | |
| 3692 | + * | |
| 3693 | + * The switch is only performed when a real connection profile is currently active | |
| 3694 | + * (so it can be reliably restored). When no profile is active — i.e. the user drives | |
| 3695 | + * the API panel manually — switching to a profile could not be undone without | |
| 3696 | + * clobbering those manual settings, so we leave the active model in place and warn once. | |
| 3697 | + * | |
| 3698 | + * @param {string} targetProfileId Profile to activate for the duration of the callback. | |
| 3699 | + * @param {() => Promise<any>} callback Work to run while the target profile is active. | |
| 3700 | + * @returns {Promise<any>} The callback's result. | |
| 3701 | + */ | |
| 3702 | +async function withConnectionProfile(targetProfileId, callback) { | |
| 3703 | + const select = /** @type {HTMLSelectElement} */ (document.getElementById('connection_profiles')); | |
| 3704 | + const connectionManager = extension_settings.connectionManager; | |
| 3705 | + const currentProfileId = connectionManager?.selectedProfile; | |
| 3706 | + | |
| 3707 | + const canSwitch = !!select | |
| 3708 | + && !!connectionManager | |
| 3709 | + && Array.isArray(connectionManager.profiles) | |
| 3710 | + && connectionManager.profiles.some(p => p.id === targetProfileId) | |
| 3711 | + && Array.from(select.options).some(o => o.value === targetProfileId) | |
| 3712 | + && !!currentProfileId // a real profile is active, so it can be restored afterwards | |
| 3713 | + && currentProfileId !== targetProfileId; | |
| 3714 | + | |
| 3715 | + if (!canSwitch) { | |
| 3716 | + // Selected but no base profile to restore from -> use the active model and warn once. | |
| 3717 | + if (targetProfileId && !currentProfileId && !promptProfileWarnedNoBaseProfile) { | |
| 3718 | + promptProfileWarnedNoBaseProfile = true; | |
| 3719 | + 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'); | |
| 3720 | + } | |
| 3721 | + return await callback(); | |
| 3722 | + } | |
| 3723 | + | |
| 3724 | + const switchToProfile = async (profileId) => { | |
| 3725 | + const loaded = new Promise(resolve => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve)); | |
| 3726 | + const timeout = new Promise(resolve => setTimeout(resolve, 10000)); | |
| 3727 | + const index = Array.from(select.options).findIndex(o => o.value === profileId); | |
| 3728 | + select.selectedIndex = index >= 0 ? index : 0; | |
| 3729 | + select.dispatchEvent(new Event('change')); | |
| 3730 | + // Don't hang the image generation if the event never fires. | |
| 3731 | + await Promise.race([loaded, timeout]); | |
| 3732 | + }; | |
| 3733 | + | |
| 3734 | + await switchToProfile(targetProfileId); | |
| 3735 | + try { | |
| 3736 | + return await callback(); | |
| 3737 | + } finally { | |
| 3738 | + try { | |
| 3739 | + await switchToProfile(currentProfileId); | |
| 3740 | + } catch (err) { | |
| 3741 | + console.error('SD: failed to restore the previous connection profile after image-prompt generation', err); | |
| 3742 | + } | |
| 3743 | + } | |
| 3744 | +} | |
| 3745 | + | |
| 3746 | +/** | |
| 3685 | 3747 | * Sends a request to image generation endpoint and processes the result. |
| 3686 | 3748 | * @param {number} generationType Type of image generation |
| 3687 | 3749 | * @param {string} prompt Prompt to be used for image generation |