From 7802479670529363f336b87e196685e2e5d6ca34 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Thu, 25 Dec 2025 19:59:25 -0800 Subject: [PATCH 1/6] auto-image-generation --- .claude/settings.local.json | 9 ++ index.js | 19 ++++ src/core/state.js | 6 +- src/i18n/en.json | 10 ++ src/systems/features/avatarGenerator.js | 136 ++++++++++++++++++++++++ src/systems/rendering/thoughts.js | 30 +++++- style.css | 20 ++++ template.html | 29 +++++ 8 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 src/systems/features/avatarGenerator.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9a7952d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "mcp__plugin_context7_context7__resolve-library-id", + "mcp__plugin_context7_context7__get-library-docs", + "Bash(find:*)" + ] + } +} diff --git a/index.js b/index.js index ce7e8b2..cfe298f 100644 --- a/index.js +++ b/index.js @@ -402,6 +402,22 @@ async function initUI() { toggleAnimations(); }); + // Auto avatar generation settings + $('#rpg-toggle-auto-avatars').on('change', function() { + extensionSettings.autoGenerateAvatars = $(this).prop('checked'); + saveSettings(); + }); + + $('#rpg-avatar-style-select').on('change', function() { + extensionSettings.avatarGenerationStyle = String($(this).val()); + saveSettings(); + }); + + $('#rpg-avatar-custom-prompt').on('input', function() { + extensionSettings.avatarGenerationPrompt = $(this).val().trim(); + saveSettings(); + }); + $('#rpg-manual-update').on('click', async function() { if (!extensionSettings.enabled) { // console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.'); @@ -493,6 +509,9 @@ async function initUI() { $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations); + $('#rpg-toggle-auto-avatars').prop('checked', extensionSettings.autoGenerateAvatars || false); + $('#rpg-avatar-style-select').val(extensionSettings.avatarGenerationStyle || 'auto'); + $('#rpg-avatar-custom-prompt').val(extensionSettings.avatarGenerationPrompt || ''); $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); $('#rpg-theme-select').val(extensionSettings.theme); diff --git a/src/core/state.js b/src/core/state.js index bc7e59f..f30ee4c 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -166,7 +166,11 @@ export let extensionSettings = { }, debugMode: false, // Enable debug logging visible in UI (for mobile debugging) memoryMessagesToProcess: 16, // Number of messages to process per batch in memory recollection - npcAvatars: {} // Store custom avatar images for NPCs (key: character name, value: base64 data URI) + npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI) + // Auto avatar generation settings + autoGenerateAvatars: false, // Master toggle for auto-generating avatars + avatarGenerationPrompt: 'portrait, fantasy character, RPG style', // Default prompt template + avatarGenerationStyle: 'auto', // Style preset: auto, fantasy, sci-fi, anime, realistic }; /** diff --git a/src/i18n/en.json b/src/i18n/en.json index 47a4f22..3967028 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -41,6 +41,16 @@ "template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts", "template.settingsModal.display.enableDebugMode": "Enable Debug Mode", "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", + "template.settingsModal.display.autoGenerateAvatars": "Auto-generate Missing Avatars", + "template.settingsModal.display.autoGenerateAvatarsNote": "Automatically generate avatars for characters without custom images using Stable Diffusion", + "template.settingsModal.display.avatarPromptStyle": "Avatar Generation Style:", + "template.settingsModal.display.avatarPromptStyleOptions.auto": "Auto (Fantasy)", + "template.settingsModal.display.avatarPromptStyleOptions.fantasy": "Fantasy", + "template.settingsModal.display.avatarPromptStyleOptions.scifi": "Sci-Fi", + "template.settingsModal.display.avatarPromptStyleOptions.anime": "Anime", + "template.settingsModal.display.avatarPromptStyleOptions.realistic": "Realistic", + "template.settingsModal.display.avatarCustomPrompt": "Custom Avatar Prompt:", + "template.settingsModal.display.avatarCustomPromptNote": "Additional prompt modifiers for avatar generation", "template.settingsModal.advancedTitle": "Advanced", "template.settingsModal.advanced.generationMode": "Generation Mode:", "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js new file mode 100644 index 0000000..2673bf6 --- /dev/null +++ b/src/systems/features/avatarGenerator.js @@ -0,0 +1,136 @@ +/** + * Avatar Generator Module + * Handles automatic avatar generation for characters without images + */ + +import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; +import { extensionSettings } from '../../core/state.js'; +import { saveSettings } from '../../core/persistence.js'; + +// Track pending avatar generations to avoid duplicate requests +const pendingGenerations = new Set(); + +/** + * Style presets for avatar generation prompts + */ +const STYLE_PRESETS = { + 'auto': 'portrait, fantasy character, RPG style', + 'fantasy': 'portrait, fantasy character, medieval RPG style, detailed face', + 'scifi': 'portrait, sci-fi character, futuristic, cyberpunk style, detailed face', + 'anime': 'portrait, anime character, manga style, detailed face', + 'realistic': 'portrait, realistic character, detailed face, photorealistic' +}; + +/** + * Builds the generation prompt for a character + * @param {string} characterName - Name of the character + * @returns {string} Full prompt for /sd command + */ +function buildGenerationPrompt(characterName) { + const style = STYLE_PRESETS[extensionSettings.avatarGenerationStyle] || STYLE_PRESETS.auto; + const custom = extensionSettings.avatarGenerationPrompt || ''; + return `${style}, ${characterName}, ${custom}`.trim(); +} + +/** + * Generates an avatar for a character using /sd command + * @param {string} characterName - Name of the character to generate avatar for + * @returns {Promise} Avatar URL or null if failed + */ +export async function generateAvatar(characterName) { + // Skip if already generating + if (pendingGenerations.has(characterName)) { + console.log(`[RPG Avatar] Already generating avatar for: ${characterName}`); + return null; + } + + // Skip if disabled + if (!extensionSettings.autoGenerateAvatars) { + return null; + } + + // Skip if custom avatar already exists + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + return null; + } + + pendingGenerations.add(characterName); + console.log(`[RPG Avatar] Starting generation for: ${characterName}`); + + try { + const prompt = buildGenerationPrompt(characterName); + + // Execute /sd command with quiet=true + // This saves to gallery without posting to chat + const result = await executeSlashCommandsOnChatInput( + `/sd ${prompt} quiet=true`, + { clearChatInput: false } + ); + + // The result might be an object with various properties + // We need to extract the actual image URL if available + let imageUrl = null; + + if (result) { + // Handle different result formats + if (typeof result === 'string') { + imageUrl = result; + } else if (result.pipe) { + imageUrl = result.pipe; + } else if (result.output || result.image || result.url) { + imageUrl = result.output || result.image || result.url; + } + + // Only store if we got a valid string URL + if (imageUrl && typeof imageUrl === 'string') { + if (!extensionSettings.npcAvatars) { + extensionSettings.npcAvatars = {}; + } + extensionSettings.npcAvatars[characterName] = imageUrl; + saveSettings(); + + console.log(`[RPG Avatar] Generation complete for: ${characterName}`); + return imageUrl; + } else { + console.warn(`[RPG Avatar] Generation result for ${characterName} was not a valid URL:`, result); + } + } + + return null; + } catch (error) { + console.error(`[RPG Avatar] Generation failed for ${characterName}:`, error); + return null; + } finally { + pendingGenerations.delete(characterName); + } +} + +/** + * Checks if a character needs an avatar and triggers generation + * @param {string} characterName - Name of the character to check + * @param {boolean} hasAvatar - Whether the character already has an avatar + * @returns {Promise} + */ +export function checkAndGenerateAvatar(characterName, hasAvatar) { + // Only generate if no avatar exists and feature is enabled + if (hasAvatar || !extensionSettings.autoGenerateAvatars) { + return; + } + + // Check if we already have a custom NPC avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + return; + } + + // Trigger generation (non-blocking) + generateAvatar(characterName); +} + +/** + * Checks if an avatar is currently being generated for a character + * @param {string} characterName - Name of the character to check + * @returns {boolean} True if generation is in progress + */ +export function isGenerating(characterName) { + return pendingGenerations.has(characterName); +} diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 84c5498..5162c68 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -17,6 +17,7 @@ import { import { saveChatData } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; import { saveSettings } from '../../core/persistence.js'; +import { checkAndGenerateAvatar, isGenerating } from '../features/avatarGenerator.js'; /** * Helper to log to both console and debug logs array @@ -110,12 +111,21 @@ function namesMatch(cardName, aiName) { function getCharacterAvatar(characterName) { // First, check if there's a custom NPC avatar if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - debugLog(`[RPG Thoughts] Found custom NPC avatar for: ${characterName}`); - return extensionSettings.npcAvatars[characterName]; + const avatar = extensionSettings.npcAvatars[characterName]; + // Skip if not a valid string (e.g., if it's an object from a previous bug) + if (typeof avatar === 'string' && avatar) { + debugLog(`[RPG Thoughts] Found custom NPC avatar for: ${characterName}`); + return avatar; + } else { + // Clear invalid avatar data + console.warn(`[RPG Thoughts] Invalid avatar data for ${characterName}, clearing...`); + delete extensionSettings.npcAvatars[characterName]; + } } // Use the existing avatar lookup logic let characterPortrait = FALLBACK_AVATAR_DATA_URI; + let hasAvatar = false; // For group chats, search through group members first if (selected_group) { @@ -129,6 +139,7 @@ function getCharacterAvatar(characterName) { if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar); if (thumbnailUrl) { + hasAvatar = true; return thumbnailUrl; } } @@ -147,6 +158,7 @@ function getCharacterAvatar(characterName) { if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar); if (thumbnailUrl) { + hasAvatar = true; return thumbnailUrl; } } @@ -157,10 +169,16 @@ function getCharacterAvatar(characterName) { characters[this_chid].name && namesMatch(characters[this_chid].name, characterName)) { const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); if (thumbnailUrl) { + hasAvatar = true; return thumbnailUrl; } } + // Trigger auto-generation if no avatar was found + if (!hasAvatar) { + checkAndGenerateAvatar(characterName, false); + } + return characterPortrait; } @@ -500,7 +518,7 @@ export function renderThoughts() { // Find character portrait using the new helper function const characterPortrait = getCharacterAvatar(char.name); - debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...'); + debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, typeof characterPortrait === 'string' ? characterPortrait.substring(0, 50) + '...' : characterPortrait); // Get relationship badge - only if relationships are enabled in config let relationshipBadge = '⚖️'; // Default @@ -519,10 +537,14 @@ export function renderThoughts() { // Escape character name for use in HTML attributes const escapedName = escapeHtmlAttr(char.name); + // Check if avatar is being generated + const isCurrentlyGenerating = isGenerating(char.name); + html += `
-
+
${escapedName} + ${isCurrentlyGenerating ? '
' : ''} ${hasRelationshipEnabled ? `
${relationshipBadge}
` : ''}
diff --git a/style.css b/style.css index a534144..bc10106 100644 --- a/style.css +++ b/style.css @@ -6767,3 +6767,23 @@ body:has(.rpg-panel.rpg-position-left) #sheld { background: linear-gradient(180deg, #4a9eff 0%, transparent 100%); opacity: 0.5; } + +/* Avatar generation loading overlay */ +.rpg-avatar-generating { + position: relative; +} + +.rpg-generating-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + border-radius: 8px; +} diff --git a/template.html b/template.html index 9fb899d..e554b86 100644 --- a/template.html +++ b/template.html @@ -229,6 +229,35 @@ Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button. + + + + Automatically generate avatars for characters without custom images using Stable Diffusion + + +
+ + +
+ +
+ + + + Additional prompt modifiers for avatar generation + +
From b7e52046bc7809fd96608eba06d9d9500a16ac88 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Thu, 25 Dec 2025 20:39:01 -0800 Subject: [PATCH 2/6] fixed avatar images appearing in rpg --- src/systems/features/avatarGenerator.js | 60 +++++++++++++++++++++++-- src/systems/generation/apiClient.js | 12 +++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js index 2673bf6..fc357de 100644 --- a/src/systems/features/avatarGenerator.js +++ b/src/systems/features/avatarGenerator.js @@ -10,6 +10,12 @@ import { saveSettings } from '../../core/persistence.js'; // Track pending avatar generations to avoid duplicate requests const pendingGenerations = new Set(); +/** + * Callback for when all avatar generations complete + * Used to trigger UI updates + */ +let onGenerationCompleteCallback = null; + /** * Style presets for avatar generation prompts */ @@ -32,6 +38,24 @@ function buildGenerationPrompt(characterName) { return `${style}, ${characterName}, ${custom}`.trim(); } +/** + * Sets a callback to be called when all avatar generations complete + * @param {Function} callback - Function to call when all generations are done + */ +export function setOnGenerationComplete(callback) { + onGenerationCompleteCallback = callback; +} + +/** + * Triggers the completion callback if all generations are done + */ +function checkAndTriggerCompletionCallback() { + if (pendingGenerations.size === 0 && onGenerationCompleteCallback) { + onGenerationCompleteCallback(); + onGenerationCompleteCallback = null; + } +} + /** * Generates an avatar for a character using /sd command * @param {string} characterName - Name of the character to generate avatar for @@ -61,10 +85,11 @@ export async function generateAvatar(characterName) { const prompt = buildGenerationPrompt(characterName); // Execute /sd command with quiet=true - // This saves to gallery without posting to chat + // IMPORTANT: quiet=true must come BEFORE the prompt + // This suppresses chat output and returns the image URL via pipe const result = await executeSlashCommandsOnChatInput( - `/sd ${prompt} quiet=true`, - { clearChatInput: false } + `/sd quiet=true ${prompt}`, + { clearChatInput: true } ); // The result might be an object with various properties @@ -102,6 +127,8 @@ export async function generateAvatar(characterName) { return null; } finally { pendingGenerations.delete(characterName); + // Check if all generations are complete and trigger callback + checkAndTriggerCompletionCallback(); } } @@ -134,3 +161,30 @@ export function checkAndGenerateAvatar(characterName, hasAvatar) { export function isGenerating(characterName) { return pendingGenerations.has(characterName); } + +/** + * Checks if ANY avatars are currently being generated + * @returns {boolean} True if any generation is in progress + */ +export function isAnyGenerating() { + return pendingGenerations.size > 0; +} + +/** + * Waits for all pending avatar generations to complete + * @returns {Promise} + */ +export function waitForAllGenerations() { + if (pendingGenerations.size === 0) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (pendingGenerations.size === 0) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); +} diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index c3e2c89..e1792f7 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -23,6 +23,7 @@ import { renderThoughts } from '../rendering/thoughts.js'; import { renderInventory } from '../rendering/inventory.js'; import { renderQuests } from '../rendering/quests.js'; import { i18n } from '../../core/i18n.js'; +import { setOnGenerationComplete, waitForAllGenerations } from '../features/avatarGenerator.js'; // Store the original preset name to restore after tracker generation let originalPresetName = null; @@ -214,6 +215,12 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough renderInventory(); renderQuests(); + // Set up callback to re-render thoughts when avatars finish generating + setOnGenerationComplete(() => { + console.log('[RPG Companion] Avatar generation complete, re-rendering thoughts...'); + renderThoughts(); + }); + // Save to chat metadata saveChatData(); } @@ -221,6 +228,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough } catch (error) { console.error('[RPG Companion] Error updating RPG data:', error); } finally { + // Wait for all avatar generations to complete before finishing + console.log('[RPG Companion] Waiting for avatar generations to complete...'); + await waitForAllGenerations(); + console.log('[RPG Companion] All avatar generations complete.'); + // Restore original preset if we switched to a separate one if (originalPresetName && extensionSettings.useSeparatePreset) { console.log(`[RPG Companion] Restoring original preset: "${originalPresetName}"`); From de11f6f7e26255c5ab301e2969fd3d1ede015c5b Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Thu, 25 Dec 2025 21:13:19 -0800 Subject: [PATCH 3/6] llm generated image gen prompts --- index.js | 35 +++++++--- src/core/state.js | 22 +++++- src/i18n/en.json | 10 +-- src/systems/features/avatarGenerator.js | 31 ++++----- src/systems/generation/apiClient.js | 89 ++++++++++++++++++++++++- src/systems/generation/promptBuilder.js | 82 +++++++++++++++++++++++ style.css | 14 ++++ template.html | 30 +++------ 8 files changed, 256 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index cfe298f..0cd0cb0 100644 --- a/index.js +++ b/index.js @@ -36,7 +36,8 @@ import { setInfoBoxContainer, setThoughtsContainer, setInventoryContainer, - setQuestsContainer + setQuestsContainer, + clearSessionAvatarPrompts } from './src/core/state.js'; import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js'; import { registerAllEvents } from './src/core/events.js'; @@ -406,15 +407,18 @@ async function initUI() { $('#rpg-toggle-auto-avatars').on('change', function() { extensionSettings.autoGenerateAvatars = $(this).prop('checked'); saveSettings(); + + // Show/hide avatar options based on toggle + const $options = $('#rpg-avatar-options'); + if (extensionSettings.autoGenerateAvatars) { + $options.slideDown(200); + } else { + $options.slideUp(200); + } }); - $('#rpg-avatar-style-select').on('change', function() { - extensionSettings.avatarGenerationStyle = String($(this).val()); - saveSettings(); - }); - - $('#rpg-avatar-custom-prompt').on('input', function() { - extensionSettings.avatarGenerationPrompt = $(this).val().trim(); + $('#rpg-avatar-llm-instruction').on('input', function() { + extensionSettings.avatarLLMCustomInstruction = $(this).val().trim(); saveSettings(); }); @@ -509,9 +513,18 @@ async function initUI() { $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations); + + // Initialize avatar options $('#rpg-toggle-auto-avatars').prop('checked', extensionSettings.autoGenerateAvatars || false); - $('#rpg-avatar-style-select').val(extensionSettings.avatarGenerationStyle || 'auto'); - $('#rpg-avatar-custom-prompt').val(extensionSettings.avatarGenerationPrompt || ''); + $('#rpg-avatar-llm-instruction').val(extensionSettings.avatarLLMCustomInstruction || ''); + + // Initialize avatar options visibility + if (extensionSettings.autoGenerateAvatars) { + $('#rpg-avatar-options').show(); + } else { + $('#rpg-avatar-options').hide(); + } + $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); $('#rpg-theme-select').val(extensionSettings.theme); @@ -733,7 +746,7 @@ jQuery(async () => { [event_types.MESSAGE_RECEIVED]: onMessageReceived, [event_types.GENERATION_STOPPED]: onGenerationEnded, [event_types.GENERATION_ENDED]: onGenerationEnded, - [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad], + [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts], [event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.SETTINGS_UPDATED]: updatePersonaAvatar diff --git a/src/core/state.js b/src/core/state.js index f30ee4c..fdeef1b 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -169,8 +169,7 @@ export let extensionSettings = { npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI) // Auto avatar generation settings autoGenerateAvatars: false, // Master toggle for auto-generating avatars - avatarGenerationPrompt: 'portrait, fantasy character, RPG style', // Default prompt template - avatarGenerationStyle: 'auto', // Style preset: auto, fantasy, sci-fi, anime, realistic + avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation }; /** @@ -193,6 +192,25 @@ export let committedTrackerData = { characterThoughts: null }; +/** + * Session-only storage for LLM-generated avatar prompts + * Maps character names to their generated prompts + * Resets on new chat (not persisted to extensionSettings) + */ +export let sessionAvatarPrompts = {}; + +export function setSessionAvatarPrompt(characterName, prompt) { + sessionAvatarPrompts[characterName] = prompt; +} + +export function getSessionAvatarPrompt(characterName) { + return sessionAvatarPrompts[characterName] || null; +} + +export function clearSessionAvatarPrompts() { + sessionAvatarPrompts = {}; +} + /** * Tracks whether the last action was a swipe (for separate mode) * Used to determine whether to commit lastGeneratedData to committedTrackerData diff --git a/src/i18n/en.json b/src/i18n/en.json index 3967028..3c2f113 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -43,14 +43,8 @@ "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", "template.settingsModal.display.autoGenerateAvatars": "Auto-generate Missing Avatars", "template.settingsModal.display.autoGenerateAvatarsNote": "Automatically generate avatars for characters without custom images using Stable Diffusion", - "template.settingsModal.display.avatarPromptStyle": "Avatar Generation Style:", - "template.settingsModal.display.avatarPromptStyleOptions.auto": "Auto (Fantasy)", - "template.settingsModal.display.avatarPromptStyleOptions.fantasy": "Fantasy", - "template.settingsModal.display.avatarPromptStyleOptions.scifi": "Sci-Fi", - "template.settingsModal.display.avatarPromptStyleOptions.anime": "Anime", - "template.settingsModal.display.avatarPromptStyleOptions.realistic": "Realistic", - "template.settingsModal.display.avatarCustomPrompt": "Custom Avatar Prompt:", - "template.settingsModal.display.avatarCustomPromptNote": "Additional prompt modifiers for avatar generation", + "template.settingsModal.display.avatarLLMInstruction": "LLM Instruction:", + "template.settingsModal.display.avatarLLMInstructionNote": "The LLM will use character cards, tracker data, and chat context to generate detailed prompts", "template.settingsModal.advancedTitle": "Advanced", "template.settingsModal.advanced.generationMode": "Generation Mode:", "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js index fc357de..50eb912 100644 --- a/src/systems/features/avatarGenerator.js +++ b/src/systems/features/avatarGenerator.js @@ -4,7 +4,7 @@ */ import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; -import { extensionSettings } from '../../core/state.js'; +import { extensionSettings, sessionAvatarPrompts } from '../../core/state.js'; import { saveSettings } from '../../core/persistence.js'; // Track pending avatar generations to avoid duplicate requests @@ -16,26 +16,21 @@ const pendingGenerations = new Set(); */ let onGenerationCompleteCallback = null; -/** - * Style presets for avatar generation prompts - */ -const STYLE_PRESETS = { - 'auto': 'portrait, fantasy character, RPG style', - 'fantasy': 'portrait, fantasy character, medieval RPG style, detailed face', - 'scifi': 'portrait, sci-fi character, futuristic, cyberpunk style, detailed face', - 'anime': 'portrait, anime character, manga style, detailed face', - 'realistic': 'portrait, realistic character, detailed face, photorealistic' -}; - /** * Builds the generation prompt for a character + * Uses LLM-generated prompt from session storage * @param {string} characterName - Name of the character * @returns {string} Full prompt for /sd command */ function buildGenerationPrompt(characterName) { - const style = STYLE_PRESETS[extensionSettings.avatarGenerationStyle] || STYLE_PRESETS.auto; - const custom = extensionSettings.avatarGenerationPrompt || ''; - return `${style}, ${characterName}, ${custom}`.trim(); + const llmPrompt = sessionAvatarPrompts[characterName]; + if (llmPrompt) { + console.log(`[RPG Avatar] Using LLM prompt for ${characterName}`); + return llmPrompt; + } + + console.warn(`[RPG Avatar] No LLM prompt generated for ${characterName}, skipping generation`); + return null; } /** @@ -84,6 +79,12 @@ export async function generateAvatar(characterName) { try { const prompt = buildGenerationPrompt(characterName); + // Skip if no prompt was generated (LLM hasn't generated one yet) + if (!prompt) { + console.log(`[RPG Avatar] No prompt available for ${characterName}, skipping`); + return null; + } + // Execute /sd command with quiet=true // IMPORTANT: quiet=true must come BEFORE the prompt // This suppresses chat output and returns the image URL via pipe diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index e1792f7..9d787fb 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -12,10 +12,15 @@ import { isGenerating, lastActionWasSwipe, setIsGenerating, - setLastActionWasSwipe + setLastActionWasSwipe, + sessionAvatarPrompts } from '../../core/state.js'; import { saveChatData } from '../../core/persistence.js'; -import { generateSeparateUpdatePrompt } from './promptBuilder.js'; +import { + generateSeparateUpdatePrompt, + generateAvatarPromptGenerationPrompt, + parseAvatarPromptsResponse +} from './promptBuilder.js'; import { parseResponse, parseUserStats } from './parser.js'; import { renderUserStats } from '../rendering/userStats.js'; import { renderInfoBox } from '../rendering/infoBox.js'; @@ -158,6 +163,15 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough lastGeneratedData.characterThoughts = parsedData.characterThoughts; } + // Generate avatar prompts if auto-generate is enabled and characters need avatars + if (extensionSettings.autoGenerateAvatars) { + const charactersNeedingPrompts = parseCharactersWithoutAvatars(parsedData.characterThoughts); + if (charactersNeedingPrompts.length > 0) { + console.log('[RPG Companion] Generating LLM avatar prompts for:', charactersNeedingPrompts); + await generateAvatarPrompts(charactersNeedingPrompts); + } + } + // When saveTrackerHistory is enabled, store tracker data on the user's message too // This allows scrolling through history and seeing trackers at each point if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) { @@ -253,3 +267,74 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough setLastActionWasSwipe(false); } } + +/** + * Parses character thoughts to find characters that need avatar prompts + * @param {string} characterThoughtsData - Raw character thoughts data + * @returns {Array} Array of character names needing prompts + */ +function parseCharactersWithoutAvatars(characterThoughtsData) { + if (!characterThoughtsData) return []; + + const lines = characterThoughtsData.split('\n'); + const characters = []; + + for (const line of lines) { + if (line.trim().startsWith('- ')) { + const name = line.trim().substring(2).trim(); + if (name && name.toLowerCase() !== 'unavailable') { + // Skip if already has custom avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) { + continue; + } + // Skip if already has session prompt + if (sessionAvatarPrompts[name]) { + continue; + } + characters.push(name); + } + } + } + return characters; +} + +/** + * Generates LLM-based avatar prompts for specified characters + * Called during batch RPG data refresh when avatar generation is enabled + * + * @param {Array} characterNames - Array of character names needing prompts + * @returns {Promise} Map of character name to generated prompt + */ +export async function generateAvatarPrompts(characterNames) { + if (!characterNames || characterNames.length === 0) { + return {}; + } + + try { + console.log('[RPG Avatar] Generating LLM prompts for characters:', characterNames); + + const prompt = await generateAvatarPromptGenerationPrompt(characterNames); + + // Generate using raw prompt + const response = await generateRaw({ + prompt: prompt, + quietToLoud: false + }); + + if (response) { + const prompts = parseAvatarPromptsResponse(response); + console.log('[RPG Avatar] Generated prompts:', prompts); + + // Store in session-only storage + for (const [name, prompt] of Object.entries(prompts)) { + sessionAvatarPrompts[name] = prompt; + } + + return prompts; + } + } catch (error) { + console.error('[RPG Avatar] LLM prompt generation failed:', error); + } + + return {}; +} diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 829293c..658af1f 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -597,3 +597,85 @@ export async function generateSeparateUpdatePrompt() { return messages; } + +/** + * Default custom instruction for avatar prompt generation + */ +const DEFAULT_AVATAR_CUSTOM_INSTRUCTION = `Create a detailed portrait prompt focusing on the character's appearance, clothing, and mood. Include appropriate artistic style keywords.`; + +/** + * Generates the prompt for LLM-based avatar prompt generation + * Uses the same context as RPG generation (character cards, tracker data, chat history) + * + * @param {Array} characterNames - Array of character names to generate prompts for + * @returns {Promise>} Message array for generateRaw API + */ +export async function generateAvatarPromptGenerationPrompt(characterNames) { + const depth = extensionSettings.updateDepth; + const messages = []; + + // Build system message with character context + let systemMessage = `You are an AI assistant specializing in creating detailed image generation prompts for character avatars.\n\n`; + + // Add character card information (reusing existing function) + const characterInfo = await getCharacterCardsInfo(); + if (characterInfo) { + systemMessage += `Character Information:\n${characterInfo}\n\n`; + } + + // Add tracker context if available + if (committedTrackerData.characterThoughts) { + systemMessage += `Current Scene Context:\n${committedTrackerData.characterThoughts}\n\n`; + } + + systemMessage += `Recent conversation context:\n`; + messages.push({ role: 'system', content: systemMessage }); + + // Add chat history + const recentMessages = chat.slice(-depth); + for (const message of recentMessages) { + messages.push({ + role: message.is_user ? 'user' : 'assistant', + content: message.mes + }); + } + + // Build instruction message + let instructionMessage = `\n\n`; + const customInstruction = extensionSettings.avatarLLMCustomInstruction || DEFAULT_AVATAR_CUSTOM_INSTRUCTION; + + instructionMessage += `Task: Generate detailed image prompts for the following characters.\n\n`; + instructionMessage += `Instructions: ${customInstruction}\n\n`; + instructionMessage += `Characters:\n`; + characterNames.forEach((name, index) => { + instructionMessage += `${index + 1}. ${name}\n`; + }); + + instructionMessage += `\nOutput Format (one per line):\n`; + instructionMessage += `CHARACTER_NAME: [detailed prompt]\n\n`; + instructionMessage += `Example:\n`; + instructionMessage += `Gandalf: portrait, elderly wizard with long white beard, wearing grey robes, holding wooden staff, intense blue eyes, wise expression, fantasy art style\n\n`; + instructionMessage += `Provide ONLY the formatted prompts, no other text.`; + + messages.push({ role: 'user', content: instructionMessage }); + return messages; +} + +/** + * Parses LLM response to extract character prompts + * @param {string} response - Raw LLM response + * @returns {Object} Map of character name to prompt + */ +export function parseAvatarPromptsResponse(response) { + const prompts = {}; + const lines = response.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + const match = trimmed.match(/^([^:]+):\s*(.+)$/); + if (match) { + prompts[match[1].trim()] = match[2].trim(); + } + } + return prompts; +} diff --git a/style.css b/style.css index bc10106..981a00a 100644 --- a/style.css +++ b/style.css @@ -6787,3 +6787,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: 24px; border-radius: 8px; } + +/* Textarea style for LLM instruction input */ +.rpg-textarea { + width: 100%; + min-height: 80px; + padding: 10px; + border-radius: 4px; + border: 1px solid var(--rpg-border); + background: var(--SmartThemeBlurTintColor); + color: var(--SmartThemeBodyColor); + font-family: inherit; + resize: vertical; + box-sizing: border-box; +} diff --git a/template.html b/template.html index e554b86..94c851f 100644 --- a/template.html +++ b/template.html @@ -238,25 +238,17 @@ Automatically generate avatars for characters without custom images using Stable Diffusion -
- - -
- -
- - - - Additional prompt modifiers for avatar generation - + +
From 2df173e6af2e516aff60783f8d5cc8897fbc2973 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Fri, 26 Dec 2025 00:54:44 -0600 Subject: [PATCH 4/6] fixed up re-rendering images when right clicking --- src/systems/features/avatarGenerator.js | 538 +++++++++++++++++------- src/systems/generation/apiClient.js | 114 ++--- src/systems/rendering/thoughts.js | 58 ++- 3 files changed, 446 insertions(+), 264 deletions(-) diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js index 50eb912..b7f28df 100644 --- a/src/systems/features/avatarGenerator.js +++ b/src/systems/features/avatarGenerator.js @@ -1,170 +1,36 @@ /** * Avatar Generator Module - * Handles automatic avatar generation for characters without images + * Handles automatic and manual avatar generation for NPC characters + * + * Features: + * - Batch generation with awaitable completion + * - Batch prompt generation via LLM + * - Individual image generation via /sd command + * - Manual regeneration support */ +import { generateRaw, characters, this_chid } from '../../../../../../../script.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; -import { extensionSettings, sessionAvatarPrompts } from '../../core/state.js'; +import { selected_group, getGroupMembers } from '../../../../../../group-chats.js'; +import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js'; import { saveSettings } from '../../core/persistence.js'; +import { generateAvatarPromptGenerationPrompt, parseAvatarPromptsResponse } from '../generation/promptBuilder.js'; +import { getCurrentPresetName, switchToPreset } from '../generation/apiClient.js'; -// Track pending avatar generations to avoid duplicate requests +// Generation state - tracks characters currently being generated const pendingGenerations = new Set(); /** - * Callback for when all avatar generations complete - * Used to trigger UI updates - */ -let onGenerationCompleteCallback = null; - -/** - * Builds the generation prompt for a character - * Uses LLM-generated prompt from session storage - * @param {string} characterName - Name of the character - * @returns {string} Full prompt for /sd command - */ -function buildGenerationPrompt(characterName) { - const llmPrompt = sessionAvatarPrompts[characterName]; - if (llmPrompt) { - console.log(`[RPG Avatar] Using LLM prompt for ${characterName}`); - return llmPrompt; - } - - console.warn(`[RPG Avatar] No LLM prompt generated for ${characterName}, skipping generation`); - return null; -} - -/** - * Sets a callback to be called when all avatar generations complete - * @param {Function} callback - Function to call when all generations are done - */ -export function setOnGenerationComplete(callback) { - onGenerationCompleteCallback = callback; -} - -/** - * Triggers the completion callback if all generations are done - */ -function checkAndTriggerCompletionCallback() { - if (pendingGenerations.size === 0 && onGenerationCompleteCallback) { - onGenerationCompleteCallback(); - onGenerationCompleteCallback = null; - } -} - -/** - * Generates an avatar for a character using /sd command - * @param {string} characterName - Name of the character to generate avatar for - * @returns {Promise} Avatar URL or null if failed - */ -export async function generateAvatar(characterName) { - // Skip if already generating - if (pendingGenerations.has(characterName)) { - console.log(`[RPG Avatar] Already generating avatar for: ${characterName}`); - return null; - } - - // Skip if disabled - if (!extensionSettings.autoGenerateAvatars) { - return null; - } - - // Skip if custom avatar already exists - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - return null; - } - - pendingGenerations.add(characterName); - console.log(`[RPG Avatar] Starting generation for: ${characterName}`); - - try { - const prompt = buildGenerationPrompt(characterName); - - // Skip if no prompt was generated (LLM hasn't generated one yet) - if (!prompt) { - console.log(`[RPG Avatar] No prompt available for ${characterName}, skipping`); - return null; - } - - // Execute /sd command with quiet=true - // IMPORTANT: quiet=true must come BEFORE the prompt - // This suppresses chat output and returns the image URL via pipe - const result = await executeSlashCommandsOnChatInput( - `/sd quiet=true ${prompt}`, - { clearChatInput: true } - ); - - // The result might be an object with various properties - // We need to extract the actual image URL if available - let imageUrl = null; - - if (result) { - // Handle different result formats - if (typeof result === 'string') { - imageUrl = result; - } else if (result.pipe) { - imageUrl = result.pipe; - } else if (result.output || result.image || result.url) { - imageUrl = result.output || result.image || result.url; - } - - // Only store if we got a valid string URL - if (imageUrl && typeof imageUrl === 'string') { - if (!extensionSettings.npcAvatars) { - extensionSettings.npcAvatars = {}; - } - extensionSettings.npcAvatars[characterName] = imageUrl; - saveSettings(); - - console.log(`[RPG Avatar] Generation complete for: ${characterName}`); - return imageUrl; - } else { - console.warn(`[RPG Avatar] Generation result for ${characterName} was not a valid URL:`, result); - } - } - - return null; - } catch (error) { - console.error(`[RPG Avatar] Generation failed for ${characterName}:`, error); - return null; - } finally { - pendingGenerations.delete(characterName); - // Check if all generations are complete and trigger callback - checkAndTriggerCompletionCallback(); - } -} - -/** - * Checks if a character needs an avatar and triggers generation - * @param {string} characterName - Name of the character to check - * @param {boolean} hasAvatar - Whether the character already has an avatar - * @returns {Promise} - */ -export function checkAndGenerateAvatar(characterName, hasAvatar) { - // Only generate if no avatar exists and feature is enabled - if (hasAvatar || !extensionSettings.autoGenerateAvatars) { - return; - } - - // Check if we already have a custom NPC avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - return; - } - - // Trigger generation (non-blocking) - generateAvatar(characterName); -} - -/** - * Checks if an avatar is currently being generated for a character - * @param {string} characterName - Name of the character to check - * @returns {boolean} True if generation is in progress + * Checks if a character is pending generation (waiting or actively generating) + * @param {string} characterName - Name of character to check + * @returns {boolean} True if generation is pending */ export function isGenerating(characterName) { return pendingGenerations.has(characterName); } /** - * Checks if ANY avatars are currently being generated + * Checks if any avatars are currently being generated * @returns {boolean} True if any generation is in progress */ export function isAnyGenerating() { @@ -172,20 +38,366 @@ export function isAnyGenerating() { } /** - * Waits for all pending avatar generations to complete - * @returns {Promise} + * Gets all characters currently pending generation + * @returns {string[]} Array of character names */ -export function waitForAllGenerations() { - if (pendingGenerations.size === 0) { - return Promise.resolve(); +export function getPendingGenerations() { + return [...pendingGenerations]; +} + +/** + * Helper to check if two character names match (case-insensitive, handles partial matches) + * @param {string} cardName - Name from character card + * @param {string} aiName - Name from AI response + * @returns {boolean} True if names match + */ +function namesMatch(cardName, aiName) { + if (!cardName || !aiName) return false; + const cardLower = cardName.toLowerCase().trim(); + const aiLower = aiName.toLowerCase().trim(); + if (cardLower === aiLower) return true; + const cardCore = cardLower.split(/[\s,'"]+/)[0]; + const aiCore = aiLower.split(/[\s,'"]+/)[0]; + if (cardCore === aiCore) return true; + const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`); + return wordBoundary.test(aiCore); +} + +/** + * Checks if a character already has an avatar (custom NPC avatar or from character card) + * @param {string} characterName - Name of character to check + * @returns {boolean} True if character has an avatar + */ +export function hasExistingAvatar(characterName) { + // Check for custom NPC avatar first + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + const avatar = extensionSettings.npcAvatars[characterName]; + if (typeof avatar === 'string' && avatar) { + return true; + } } - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (pendingGenerations.size === 0) { - clearInterval(checkInterval); - resolve(); + // Check group members for avatar + if (selected_group) { + try { + const groupMembers = getGroupMembers(selected_group); + if (groupMembers && groupMembers.length > 0) { + const matchingMember = groupMembers.find(member => + member && member.name && namesMatch(member.name, characterName) + ); + if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { + return true; + } } - }, 100); - }); + } catch (e) { + // Ignore errors + } + } + + // Check all characters for avatar + if (characters && characters.length > 0) { + const matchingCharacter = characters.find(c => + c && c.name && namesMatch(c.name, characterName) + ); + if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { + return true; + } + } + + // Check current character in 1-on-1 chat + if (this_chid !== undefined && characters[this_chid] && + characters[this_chid].name && namesMatch(characters[this_chid].name, characterName)) { + if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') { + return true; + } + } + + return false; +} + +/** + * Generates avatars for multiple characters and waits for all to complete. + * This is the main entry point for auto-generation within a workflow. + * + * @param {string[]} characterNames - Array of character names to generate avatars for + * @param {Function} onStarted - Optional callback when generation starts (to update UI) + * @returns {Promise} Resolves when all generations complete + */ +export async function generateAvatarsForCharacters(characterNames, onStarted = null) { + if (!extensionSettings.autoGenerateAvatars) { + return; + } + + // Filter to characters that need avatars + const needsGeneration = characterNames.filter(name => { + // Skip if already pending + if (pendingGenerations.has(name)) { + return false; + } + // Skip if has avatar + if (hasExistingAvatar(name)) { + return false; + } + return true; + }); + + if (needsGeneration.length === 0) { + return; + } + + console.log('[RPG Avatar] Starting batch generation for:', needsGeneration); + + // Mark all as pending IMMEDIATELY (before any async work) + for (const name of needsGeneration) { + pendingGenerations.add(name); + } + + // Trigger UI update to show loading spinners + if (onStarted) { + try { + onStarted([...needsGeneration]); + } catch (e) { + console.error('[RPG Avatar] Error in onStarted callback:', e); + } + } + + try { + // Generate LLM prompts for all characters that don't have them + const needsPrompts = needsGeneration.filter(name => !sessionAvatarPrompts[name]); + if (needsPrompts.length > 0) { + await generateLLMPrompts(needsPrompts); + } + + // Generate images one at a time + for (const characterName of needsGeneration) { + // Skip if somehow already has avatar now + if (hasExistingAvatar(characterName)) { + pendingGenerations.delete(characterName); + continue; + } + + await generateSingleAvatar(characterName); + pendingGenerations.delete(characterName); + + // Small delay between generations to avoid overwhelming the API + if (needsGeneration.indexOf(characterName) < needsGeneration.length - 1) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + } finally { + // Ensure all are removed from pending even if there's an error + for (const name of needsGeneration) { + pendingGenerations.delete(name); + } + } + + console.log('[RPG Avatar] Batch generation complete'); +} + +/** + * Regenerates avatar for a specific character + * Clears existing avatar and prompt, then generates new ones + * Handles preset switching if useSeparatePreset is enabled + * + * @param {string} characterName - Name of character to regenerate + * @returns {Promise} New avatar URL or null if failed + */ +export async function regenerateAvatar(characterName) { + console.log('[RPG Avatar] Regenerating avatar for:', characterName); + + // Mark as pending immediately + pendingGenerations.add(characterName); + + // Clear existing avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + delete extensionSettings.npcAvatars[characterName]; + saveSettings(); + } + + // Clear existing prompt to force new LLM generation + if (sessionAvatarPrompts[characterName]) { + delete sessionAvatarPrompts[characterName]; + } + + // Save current preset and switch to RPG Companion Trackers if enabled + let originalPresetName = null; + if (extensionSettings.useSeparatePreset) { + originalPresetName = await getCurrentPresetName(); + if (originalPresetName) { + console.log(`[RPG Avatar] Switching from "${originalPresetName}" to RPG Companion Trackers preset`); + await switchToPreset('RPG Companion Trackers'); + } + } + + try { + // Generate new LLM prompt + await generateLLMPrompts([characterName]); + + // Generate the avatar + return await generateSingleAvatar(characterName); + } finally { + // Restore original preset if we switched + if (originalPresetName && extensionSettings.useSeparatePreset) { + console.log(`[RPG Avatar] Restoring original preset: "${originalPresetName}"`); + await switchToPreset(originalPresetName); + } + + // Remove from pending when done + pendingGenerations.delete(characterName); + } +} + +/** + * Generates LLM prompts for multiple characters in a single API call + * + * @param {string[]} characterNames - Names of characters needing prompts + */ +async function generateLLMPrompts(characterNames) { + if (characterNames.length === 0) return; + + try { + console.log('[RPG Avatar] Generating LLM prompts for:', characterNames); + + const promptMessages = await generateAvatarPromptGenerationPrompt(characterNames); + const response = await generateRaw({ + prompt: promptMessages, + quietToLoud: false + }); + + if (response) { + const prompts = parseAvatarPromptsResponse(response); + console.log('[RPG Avatar] Generated prompts:', prompts); + + // Store prompts in session storage + for (const [name, prompt] of Object.entries(prompts)) { + setSessionAvatarPrompt(name, prompt); + } + } + } catch (error) { + console.error('[RPG Avatar] Failed to generate LLM prompts:', error); + } +} + +/** + * Builds a fallback prompt when LLM prompt generation fails or isn't available + * Uses information embedded in the character name if present (e.g., from malformed tracker output) + * + * @param {string} characterName - Character name (may contain additional details) + * @returns {string} A basic prompt for image generation + */ +function buildFallbackPrompt(characterName) { + // Check if the name contains embedded details (malformed format from weaker models) + // e.g., "Eris Details: 🌟 | beautiful girl with white hair | kind expression" + if (characterName.includes('Details:') || characterName.includes('|')) { + // Extract useful description parts + const parts = characterName.split(/Details:|[|]/).map(p => p.trim()).filter(p => p && !p.match(/^[\p{Emoji}]+$/u)); + if (parts.length > 1) { + // First part is likely the name, rest are descriptions + const name = parts[0]; + const descriptions = parts.slice(1).join(', '); + return `portrait of ${name}, ${descriptions}, fantasy art style, detailed`; + } + } + + // Simple fallback - just use the name + return `portrait of ${characterName}, character portrait, fantasy art style, detailed face, high quality`; +} + +/** + * Generates a single avatar using the /sd command + * + * @param {string} characterName - Name of character to generate avatar for + * @returns {Promise} Avatar URL or null if failed + */ +async function generateSingleAvatar(characterName) { + // Get the prompt from session storage, or build a fallback + let prompt = sessionAvatarPrompts[characterName]; + if (!prompt) { + console.log(`[RPG Avatar] No LLM prompt for ${characterName}, using fallback prompt`); + prompt = buildFallbackPrompt(characterName); + } + + console.log(`[RPG Avatar] Starting image generation for: ${characterName}`); + + try { + // Execute /sd command with quiet=true to suppress chat output + const result = await executeSlashCommandsOnChatInput( + `/sd quiet=true ${prompt}`, + { clearChatInput: true } + ); + + // Extract image URL from result + const imageUrl = extractImageUrl(result); + + if (imageUrl) { + // Store the avatar + if (!extensionSettings.npcAvatars) { + extensionSettings.npcAvatars = {}; + } + extensionSettings.npcAvatars[characterName] = imageUrl; + saveSettings(); + + console.log(`[RPG Avatar] Successfully generated avatar for: ${characterName}`); + return imageUrl; + } else { + console.warn(`[RPG Avatar] Failed to extract image URL for ${characterName}:`, result); + return null; + } + } catch (error) { + console.error(`[RPG Avatar] Generation failed for ${characterName}:`, error); + return null; + } +} + +/** + * Extracts image URL from /sd command result + * Handles various result formats + * + * @param {any} result - Result from executeSlashCommandsOnChatInput + * @returns {string|null} Image URL or null + */ +function extractImageUrl(result) { + if (!result) return null; + + // Handle string result + if (typeof result === 'string') { + // Validate it looks like a URL or data URI + if (result.startsWith('http') || result.startsWith('data:') || result.startsWith('/')) { + return result; + } + return null; + } + + // Handle object result with various possible properties + if (typeof result === 'object') { + // Try common properties + const url = result.pipe || result.output || result.image || result.url || result.result; + + if (url && typeof url === 'string') { + if (url.startsWith('http') || url.startsWith('data:') || url.startsWith('/')) { + return url; + } + } + } + + return null; +} + +/** + * Clears all pending generations and resets state + */ +export function clearPendingGenerations() { + pendingGenerations.clear(); +} + +/** + * Gets the current generation status for display + * @returns {{pending: number, names: string[]}} + */ +export function getGenerationStatus() { + return { + pending: pendingGenerations.size, + names: [...pendingGenerations] + }; } diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 9d787fb..2c74834 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -12,14 +12,11 @@ import { isGenerating, lastActionWasSwipe, setIsGenerating, - setLastActionWasSwipe, - sessionAvatarPrompts + setLastActionWasSwipe } from '../../core/state.js'; import { saveChatData } from '../../core/persistence.js'; import { - generateSeparateUpdatePrompt, - generateAvatarPromptGenerationPrompt, - parseAvatarPromptsResponse + generateSeparateUpdatePrompt } from './promptBuilder.js'; import { parseResponse, parseUserStats } from './parser.js'; import { renderUserStats } from '../rendering/userStats.js'; @@ -28,7 +25,7 @@ import { renderThoughts } from '../rendering/thoughts.js'; import { renderInventory } from '../rendering/inventory.js'; import { renderQuests } from '../rendering/quests.js'; import { i18n } from '../../core/i18n.js'; -import { setOnGenerationComplete, waitForAllGenerations } from '../features/avatarGenerator.js'; +import { generateAvatarsForCharacters } from '../features/avatarGenerator.js'; // Store the original preset name to restore after tracker generation let originalPresetName = null; @@ -37,7 +34,7 @@ let originalPresetName = null; * Gets the current preset name using the /preset command * @returns {Promise} Current preset name or null if unavailable */ -async function getCurrentPresetName() { +export async function getCurrentPresetName() { try { // Use /preset without arguments to get the current preset name const result = await executeSlashCommandsOnChatInput('/preset', { quiet: true }); @@ -61,12 +58,14 @@ async function getCurrentPresetName() { console.error('[RPG Companion] Error getting current preset:', error); return null; } -}/** +} + +/** * Switches to a specific preset by name using the /preset slash command * @param {string} presetName - Name of the preset to switch to * @returns {Promise} True if switching succeeded, false otherwise */ -async function switchToPreset(presetName) { +export async function switchToPreset(presetName) { try { // Use the /preset slash command to switch presets // This is the proper way to change presets in SillyTavern @@ -163,15 +162,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough lastGeneratedData.characterThoughts = parsedData.characterThoughts; } - // Generate avatar prompts if auto-generate is enabled and characters need avatars - if (extensionSettings.autoGenerateAvatars) { - const charactersNeedingPrompts = parseCharactersWithoutAvatars(parsedData.characterThoughts); - if (charactersNeedingPrompts.length > 0) { - console.log('[RPG Companion] Generating LLM avatar prompts for:', charactersNeedingPrompts); - await generateAvatarPrompts(charactersNeedingPrompts); - } - } - // When saveTrackerHistory is enabled, store tracker data on the user's message too // This allows scrolling through history and seeing trackers at each point if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) { @@ -222,31 +212,40 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); } - // Render the updated data (outside the message check, always render) + // Render the updated data renderUserStats(); renderInfoBox(); renderThoughts(); renderInventory(); renderQuests(); - // Set up callback to re-render thoughts when avatars finish generating - setOnGenerationComplete(() => { - console.log('[RPG Companion] Avatar generation complete, re-rendering thoughts...'); - renderThoughts(); - }); - // Save to chat metadata saveChatData(); + + // Generate avatars if auto-generate is enabled (runs within this workflow) + // This uses the RPG Companion Trackers preset and keeps the button spinning + if (extensionSettings.autoGenerateAvatars) { + const charactersNeedingAvatars = parseCharactersFromThoughts(parsedData.characterThoughts); + if (charactersNeedingAvatars.length > 0) { + console.log('[RPG Companion] Generating avatars for:', charactersNeedingAvatars); + + // Generate avatars - this awaits completion + await generateAvatarsForCharacters(charactersNeedingAvatars, (names) => { + // Callback when generation starts - re-render to show loading spinners + console.log('[RPG Companion] Avatar generation started, showing spinners...'); + renderThoughts(); + }); + + // Re-render once all avatars are generated + console.log('[RPG Companion] All avatars generated, re-rendering...'); + renderThoughts(); + } + } } } catch (error) { console.error('[RPG Companion] Error updating RPG data:', error); } finally { - // Wait for all avatar generations to complete before finishing - console.log('[RPG Companion] Waiting for avatar generations to complete...'); - await waitForAllGenerations(); - console.log('[RPG Companion] All avatar generations complete.'); - // Restore original preset if we switched to a separate one if (originalPresetName && extensionSettings.useSeparatePreset) { console.log(`[RPG Companion] Restoring original preset: "${originalPresetName}"`); @@ -269,11 +268,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough } /** - * Parses character thoughts to find characters that need avatar prompts + * Parses character names from Present Characters thoughts data * @param {string} characterThoughtsData - Raw character thoughts data - * @returns {Array} Array of character names needing prompts + * @returns {Array} Array of character names found */ -function parseCharactersWithoutAvatars(characterThoughtsData) { +function parseCharactersFromThoughts(characterThoughtsData) { if (!characterThoughtsData) return []; const lines = characterThoughtsData.split('\n'); @@ -283,58 +282,9 @@ function parseCharactersWithoutAvatars(characterThoughtsData) { if (line.trim().startsWith('- ')) { const name = line.trim().substring(2).trim(); if (name && name.toLowerCase() !== 'unavailable') { - // Skip if already has custom avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) { - continue; - } - // Skip if already has session prompt - if (sessionAvatarPrompts[name]) { - continue; - } characters.push(name); } } } return characters; } - -/** - * Generates LLM-based avatar prompts for specified characters - * Called during batch RPG data refresh when avatar generation is enabled - * - * @param {Array} characterNames - Array of character names needing prompts - * @returns {Promise} Map of character name to generated prompt - */ -export async function generateAvatarPrompts(characterNames) { - if (!characterNames || characterNames.length === 0) { - return {}; - } - - try { - console.log('[RPG Avatar] Generating LLM prompts for characters:', characterNames); - - const prompt = await generateAvatarPromptGenerationPrompt(characterNames); - - // Generate using raw prompt - const response = await generateRaw({ - prompt: prompt, - quietToLoud: false - }); - - if (response) { - const prompts = parseAvatarPromptsResponse(response); - console.log('[RPG Avatar] Generated prompts:', prompts); - - // Store in session-only storage - for (const [name, prompt] of Object.entries(prompts)) { - sessionAvatarPrompts[name] = prompt; - } - - return prompts; - } - } catch (error) { - console.error('[RPG Avatar] LLM prompt generation failed:', error); - } - - return {}; -} diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 5162c68..b75f5d8 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -17,7 +17,7 @@ import { import { saveChatData } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; import { saveSettings } from '../../core/persistence.js'; -import { checkAndGenerateAvatar, isGenerating } from '../features/avatarGenerator.js'; +import { isGenerating, regenerateAvatar } from '../features/avatarGenerator.js'; /** * Helper to log to both console and debug logs array @@ -174,11 +174,6 @@ function getCharacterAvatar(characterName) { } } - // Trigger auto-generation if no avatar was found - if (!hasAvatar) { - checkAndGenerateAvatar(characterName, false); - } - return characterPortrait; } @@ -481,7 +476,7 @@ export function renderThoughts() { html += '
'; html += `
-
+
${escapedDefaultName}
⚖️
@@ -542,7 +537,7 @@ export function renderThoughts() { html += `
-
+
${escapedName} ${isCurrentlyGenerating ? '
' : ''} ${hasRelationshipEnabled ? `
${relationshipBadge}
` : ''} @@ -628,8 +623,8 @@ export function renderThoughts() { uploadNpcAvatar(characterName); }); - // Add event handler for removing custom avatars (right-click) - $thoughtsContainer.find('.rpg-avatar-upload').on('contextmenu', function(e) { + // Add event handler for regenerating avatars (right-click) + $thoughtsContainer.find('.rpg-avatar-upload').on('contextmenu', async function(e) { // Prevent triggering if clicking on the relationship badge if ($(e.target).hasClass('rpg-relationship-badge') || $(e.target).closest('.rpg-relationship-badge').length > 0) { return; @@ -637,17 +632,42 @@ export function renderThoughts() { e.preventDefault(); // Prevent default context menu const characterName = $(this).data('character'); + const $avatarEl = $(this); - // Check if this character has a custom avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - // Remove the custom avatar - delete extensionSettings.npcAvatars[characterName]; - saveSettings(); - console.log(`[RPG Companion] Removed custom avatar for: ${characterName}`); - - // Re-render to show the default avatar - renderThoughts(); + // Check if auto-generation is enabled + if (!extensionSettings.autoGenerateAvatars) { + // If auto-generation is disabled, just remove the custom avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + delete extensionSettings.npcAvatars[characterName]; + saveSettings(); + console.log(`[RPG Companion] Removed custom avatar for: ${characterName}`); + renderThoughts(); + } + return; } + + // Show generating state with spinner overlay + $avatarEl.addClass('rpg-avatar-generating'); + if (!$avatarEl.find('.rpg-generating-overlay').length) { + $avatarEl.append('
'); + } + console.log(`[RPG Companion] Regenerating avatar for: ${characterName}`); + + try { + // Regenerate the avatar + const newUrl = await regenerateAvatar(characterName); + + if (newUrl) { + console.log(`[RPG Companion] Successfully regenerated avatar for: ${characterName}`); + } else { + console.warn(`[RPG Companion] Failed to regenerate avatar for: ${characterName}`); + } + } catch (error) { + console.error(`[RPG Companion] Error regenerating avatar for ${characterName}:`, error); + } + + // Re-render to show the new avatar (or fallback) + renderThoughts(); }); // Add event handler for character removal From fdaca39d393e982d975e2d1789d5f68f1fd8d0c9 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Fri, 26 Dec 2025 01:01:04 -0600 Subject: [PATCH 5/6] renamed stable diffusion to image generation --- src/i18n/en.json | 2 +- template.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index 3c2f113..3cc46e4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -42,7 +42,7 @@ "template.settingsModal.display.enableDebugMode": "Enable Debug Mode", "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", "template.settingsModal.display.autoGenerateAvatars": "Auto-generate Missing Avatars", - "template.settingsModal.display.autoGenerateAvatarsNote": "Automatically generate avatars for characters without custom images using Stable Diffusion", + "template.settingsModal.display.autoGenerateAvatarsNote": "Automatically generate avatars for characters without custom images using the Image Generation Plugin", "template.settingsModal.display.avatarLLMInstruction": "LLM Instruction:", "template.settingsModal.display.avatarLLMInstructionNote": "The LLM will use character cards, tracker data, and chat context to generate detailed prompts", "template.settingsModal.advancedTitle": "Advanced", diff --git a/template.html b/template.html index 94c851f..b115459 100644 --- a/template.html +++ b/template.html @@ -235,7 +235,7 @@ Auto-generate Missing Avatars - Automatically generate avatars for characters without custom images using Stable Diffusion + Automatically generate avatars for characters without custom images using the Image Generation Plugin From 87e86bfbb4e076aaddb641b1fc9324635d73dede Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Thu, 25 Dec 2025 23:43:39 -0800 Subject: [PATCH 6/6] Removing my .claude settings file oopsies --- .claude/settings.local.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 9a7952d..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__plugin_context7_context7__resolve-library-id", - "mcp__plugin_context7_context7__get-library-docs", - "Bash(find:*)" - ] - } -}