Stable Diffusion: fix image-prompt profile being ignored (exclude from presets; generate under the chosen profile with full context)

fec2f49e9803e457282f43757f59fac971229bea

permissionBRICK <permissionBRICK@gmail.com>

1 files changed, +77 -15Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+77 -15
@@ -385,6 +385,9 @@ const PRESET_EXCLUDE_KEYS = [
385 'settings_preset_primary',385 'settings_preset_primary',
386 'settings_preset_secondary',386 'settings_preset_secondary',
387 'settings_fallback_enabled',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 'prompts',391 'prompts',
389 'character_prompts',392 'character_prompts',
390 'character_negative_prompts',393 'character_negative_prompts',
@@ -421,6 +424,11 @@ function applySdSettingsSnapshot(snapshot) {
421 return;424 return;
422 }425 }
423 for (const key of Object.keys(snapshot)) {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 extension_settings.sd[key] = structuredClone(snapshot[key]);432 extension_settings.sd[key] = structuredClone(snapshot[key]);
425 }433 }
426}434}
@@ -543,6 +551,9 @@ function toggleSourceControls() {
543}551}
544552
545let promptGenerationProfileDropdownInitialized = false;553let 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.
556let promptProfileWarnedNoBaseProfile = false;
546557
547/**558/**
548 * Populates the dedicated prompt-generation connection profile dropdown.559 * Populates the dedicated prompt-generation connection profile dropdown.
@@ -3652,26 +3663,18 @@ function getUserAvatarUrl() {
3652 */3663 */
3653async function generatePrompt(quietPrompt) {3664async function generatePrompt(quietPrompt) {
3654 const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation');3665 const toast = toastr.info('Generating image prompt with an LLM...', 'Image Generation');
3655 let reply;
3656 const profileId = extension_settings.sd.prompt_generation_profile;3666 const profileId = extension_settings.sd.prompt_generation_profile;
3667 let reply;
36573668
3658 if (profileId) {3669 try {
3659 try {3670 reply = profileId
3660 // Use the chosen Connection Manager profile (non-streaming) regardless of the active model.3671 ? await withConnectionProfile(profileId, () => generateQuietPrompt({ quietPrompt }))
3661 // 1024 tokens is a sensible default cap for an image-prompt description.3672 : await generateQuietPrompt({ quietPrompt });
3662 const data = await ConnectionManagerRequestService.sendRequest(profileId, quietPrompt, 1024);3673 } finally {
3663 // Non-streaming requests resolve to ExtractedData ({ content, reasoning }); extract the plain text.3674 toastr.clear(toast);
3664 reply = typeof data === 'object' && data !== null ? String(data.content ?? '') : '';
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 }
36723676
3673 const processedReply = processReply(reply);3677 const processedReply = processReply(reply);
3674 toastr.clear(toast);
36753678
3676 if (!processedReply) {3679 if (!processedReply) {
3677 toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation');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}
36833686
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 */
3702async 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 * Sends a request to image generation endpoint and processes the result.3747 * Sends a request to image generation endpoint and processes the result.
3686 * @param {number} generationType Type of image generation3748 * @param {number} generationType Type of image generation
3687 * @param {string} prompt Prompt to be used for image generation3749 * @param {string} prompt Prompt to be used for image generation