mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Merge pull request #3215 from ceruleandeep/feature/uploadSprite
/uploadsprite slashcommand
This commit is contained in:
		@@ -14,7 +14,6 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
 | 
			
		||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
 | 
			
		||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
 | 
			
		||||
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
 | 
			
		||||
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
 | 
			
		||||
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
 | 
			
		||||
export { MODULE_NAME };
 | 
			
		||||
 | 
			
		||||
@@ -986,6 +985,71 @@ async function setSpriteSlashCommand(_, spriteId) {
 | 
			
		||||
    return label;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns the sprite folder name (including override) for a character.
 | 
			
		||||
 * @param {object} char Character object
 | 
			
		||||
 * @param {string} char.avatar Avatar filename with extension
 | 
			
		||||
 * @returns {string} Sprite folder name
 | 
			
		||||
 * @throws {Error} If character not found or avatar not set
 | 
			
		||||
 */
 | 
			
		||||
function spriteFolderNameFromCharacter(char) {
 | 
			
		||||
    const avatarFileName = char.avatar.replace(/\.[^/.]+$/, '');
 | 
			
		||||
    const expressionOverride = extension_settings.expressionOverrides.find(e => e.name === avatarFileName);
 | 
			
		||||
    return expressionOverride?.path ? expressionOverride.path : avatarFileName;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Slash command callback for /uploadsprite
 | 
			
		||||
 *
 | 
			
		||||
 * label= is required
 | 
			
		||||
 * if name= is provided, it will be used as a findChar lookup
 | 
			
		||||
 * if name= is not provided, the last character's name will be used
 | 
			
		||||
 * if folder= is a full path, it will be used as the folder
 | 
			
		||||
 * if folder= is a partial path, it will be appended to the character's name
 | 
			
		||||
 * if folder= is not provided, the character's override folder will be used, if set
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} args
 | 
			
		||||
 * @param {string} args.name Character name or avatar key, passed through findChar
 | 
			
		||||
 * @param {string} args.label Expression label
 | 
			
		||||
 * @param {string} args.folder Sprite folder path, processed using backslash rules
 | 
			
		||||
 * @param {string} imageUrl Image URI to fetch and upload
 | 
			
		||||
 * @returns {Promise<void>}
 | 
			
		||||
 */
 | 
			
		||||
async function uploadSpriteCommand({ name, label, folder }, imageUrl) {
 | 
			
		||||
    if (!imageUrl) throw new Error('Image URL is required');
 | 
			
		||||
    if (!label || typeof label !== 'string') throw new Error('Expression label is required');
 | 
			
		||||
 | 
			
		||||
    label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();
 | 
			
		||||
    if (!label) throw new Error('Expression label must contain at least one letter');
 | 
			
		||||
 | 
			
		||||
    name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;
 | 
			
		||||
    const char = findChar({ name });
 | 
			
		||||
 | 
			
		||||
    if (!folder) {
 | 
			
		||||
        folder = spriteFolderNameFromCharacter(char);
 | 
			
		||||
    } else if (folder.startsWith('/') || folder.startsWith('\\')) {
 | 
			
		||||
        const subfolder = folder.slice(1);
 | 
			
		||||
        folder = `${char.name}/${subfolder}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const response = await fetch(imageUrl);
 | 
			
		||||
        const blob = await response.blob();
 | 
			
		||||
        const file = new File([blob], 'image.png', { type: 'image/png' });
 | 
			
		||||
 | 
			
		||||
        const formData = new FormData();
 | 
			
		||||
        formData.append('name', folder); // this is the folder or character name
 | 
			
		||||
        formData.append('label', label); // this is the expression label
 | 
			
		||||
        formData.append('avatar', file);  // this is the image file
 | 
			
		||||
 | 
			
		||||
        await handleFileUpload('/api/sprites/upload', formData);
 | 
			
		||||
        console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error(`[${MODULE_NAME}] Error uploading file:`, error);
 | 
			
		||||
        throw error;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Processes the classification text to reduce the amount of text sent to the API.
 | 
			
		||||
 * Quotes and asterisks are to be removed. If the text is less than 300 characters, it is returned as is.
 | 
			
		||||
@@ -1283,8 +1347,6 @@ async function drawSpritesList(character, labels, sprites) {
 | 
			
		||||
 * @returns {Promise<string>} Rendered list item template
 | 
			
		||||
 */
 | 
			
		||||
async function getListItem(item, imageSrc, textClass, isCustom) {
 | 
			
		||||
    const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
 | 
			
		||||
    imageSrc = isFirefox ? `${imageSrc}?t=${Date.now()}` : imageSrc;
 | 
			
		||||
    return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -2217,4 +2279,43 @@ function migrateSettings() {
 | 
			
		||||
            </div>
 | 
			
		||||
        `,
 | 
			
		||||
    }));
 | 
			
		||||
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
			
		||||
        name: 'uploadsprite',
 | 
			
		||||
        callback: async (args, url) => {
 | 
			
		||||
            await uploadSpriteCommand(args, url);
 | 
			
		||||
            return '';
 | 
			
		||||
        },
 | 
			
		||||
        unnamedArgumentList: [
 | 
			
		||||
            SlashCommandArgument.fromProps({
 | 
			
		||||
                description: 'URL of the image to upload',
 | 
			
		||||
                typeList: [ARGUMENT_TYPE.STRING],
 | 
			
		||||
                isRequired: true,
 | 
			
		||||
            }),
 | 
			
		||||
        ],
 | 
			
		||||
        namedArgumentList: [
 | 
			
		||||
            SlashCommandNamedArgument.fromProps({
 | 
			
		||||
                name: 'name',
 | 
			
		||||
                description: 'Character name or avatar key (default is current character)',
 | 
			
		||||
                typeList: [ARGUMENT_TYPE.STRING],
 | 
			
		||||
                isRequired: false,
 | 
			
		||||
                acceptsMultiple: false,
 | 
			
		||||
            }),
 | 
			
		||||
            SlashCommandNamedArgument.fromProps({
 | 
			
		||||
                name: 'label',
 | 
			
		||||
                description: 'Sprite label/expression name',
 | 
			
		||||
                typeList: [ARGUMENT_TYPE.STRING],
 | 
			
		||||
                enumProvider: localEnumProviders.expressions,
 | 
			
		||||
                isRequired: true,
 | 
			
		||||
                acceptsMultiple: false,
 | 
			
		||||
            }),
 | 
			
		||||
            SlashCommandNamedArgument.fromProps({
 | 
			
		||||
                name: 'folder',
 | 
			
		||||
                description: 'Override folder to upload into',
 | 
			
		||||
                typeList: [ARGUMENT_TYPE.STRING],
 | 
			
		||||
                isRequired: false,
 | 
			
		||||
                acceptsMultiple: false,
 | 
			
		||||
            }),
 | 
			
		||||
        ],
 | 
			
		||||
        helpString: '<div>Upload a sprite from a URL.</div><div>Example:</div><pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>',
 | 
			
		||||
    }));
 | 
			
		||||
})();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user