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 + +