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:
@ -95,13 +95,14 @@ EventEmitter.prototype.removeListener = function (event, listener) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
EventEmitter.prototype.emit = async function (event) {
|
EventEmitter.prototype.emit = async function (event) {
|
||||||
|
let args = [].slice.call(arguments, 1);
|
||||||
if (localStorage.getItem('eventTracing') === 'true') {
|
if (localStorage.getItem('eventTracing') === 'true') {
|
||||||
console.trace('Event emitted: ' + event, args);
|
console.trace('Event emitted: ' + event, args);
|
||||||
} else {
|
} else {
|
||||||
console.debug('Event emitted: ' + event);
|
console.debug('Event emitted: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
var i, listeners, length, args = [].slice.call(arguments, 1);
|
let i, listeners, length;
|
||||||
|
|
||||||
if (typeof this.events[event] === 'object') {
|
if (typeof this.events[event] === 'object') {
|
||||||
listeners = this.events[event].slice();
|
listeners = this.events[event].slice();
|
||||||
@ -120,13 +121,14 @@ EventEmitter.prototype.emit = async function (event) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
EventEmitter.prototype.emitAndWait = function (event) {
|
EventEmitter.prototype.emitAndWait = function (event) {
|
||||||
|
let args = [].slice.call(arguments, 1);
|
||||||
if (localStorage.getItem('eventTracing') === 'true') {
|
if (localStorage.getItem('eventTracing') === 'true') {
|
||||||
console.trace('Event emitted: ' + event, args);
|
console.trace('Event emitted: ' + event, args);
|
||||||
} else {
|
} else {
|
||||||
console.debug('Event emitted: ' + event);
|
console.debug('Event emitted: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
var i, listeners, length, args = [].slice.call(arguments, 1);
|
let i, listeners, length;
|
||||||
|
|
||||||
if (typeof this.events[event] === 'object') {
|
if (typeof this.events[event] === 'object') {
|
||||||
listeners = this.events[event].slice();
|
listeners = this.events[event].slice();
|
||||||
|
@ -14,7 +14,6 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
|||||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
|
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
|
||||||
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
|
|
||||||
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
|
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
@ -986,6 +985,71 @@ async function setSpriteSlashCommand(_, spriteId) {
|
|||||||
return label;
|
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.
|
* 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.
|
* 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
|
* @returns {Promise<string>} Rendered list item template
|
||||||
*/
|
*/
|
||||||
async function getListItem(item, imageSrc, textClass, isCustom) {
|
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 });
|
return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2217,4 +2279,43 @@ function migrateSettings() {
|
|||||||
</div>
|
</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>',
|
||||||
|
}));
|
||||||
})();
|
})();
|
||||||
|
@ -124,9 +124,10 @@ router.get('/get', jsonParser, function (request, response) {
|
|||||||
})
|
})
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
const pathToSprite = path.join(spritesPath, file);
|
const pathToSprite = path.join(spritesPath, file);
|
||||||
|
const mtime = fs.statSync(pathToSprite).mtime?.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
|
||||||
return {
|
return {
|
||||||
label: path.parse(pathToSprite).name.toLowerCase(),
|
label: path.parse(pathToSprite).name.toLowerCase(),
|
||||||
path: `/characters/${name}/${file}`,
|
path: `/characters/${name}/${file}` + (mtime ? `?t=${mtime}` : ''),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user