mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-13 02:20:14 +01:00
Merge pull request #3403 from SillyTavern/support-multiple-expressions
Support multiple expressions
This commit is contained in:
commit
fb06e7afa1
8
public/global.d.ts
vendored
8
public/global.d.ts
vendored
@ -40,4 +40,12 @@ declare global {
|
||||
searchInputCssClass?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a text to a target language using a translation provider.
|
||||
* @param text Text to translate
|
||||
* @param lang Target language
|
||||
* @param provider Translation provider
|
||||
*/
|
||||
async function translate(text: string, lang: string, provider: string = null): Promise<string>;
|
||||
}
|
||||
|
@ -1602,7 +1602,6 @@
|
||||
"Character Expressions": "Expressions de personnages",
|
||||
"Translate text to English before classification": "Traduire le texte en anglais avant de le classer",
|
||||
"Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",
|
||||
"Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)",
|
||||
"Classifier API": "API de classification",
|
||||
"Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",
|
||||
"Main API": "API principale",
|
||||
|
@ -1467,7 +1467,6 @@
|
||||
"menu within": "내의 메뉴",
|
||||
"Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",
|
||||
"Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",
|
||||
"Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)",
|
||||
"Classifier API": "분류를 위한 API",
|
||||
"Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",
|
||||
"Local": "로컬",
|
||||
|
@ -1349,7 +1349,6 @@
|
||||
"Character Expressions": "角色表情",
|
||||
"Translate text to English before classification": "分类之前将文本翻译成英文",
|
||||
"Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",
|
||||
"Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
|
||||
"Classifier API": "分类器 API",
|
||||
"Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
|
||||
"Main API": "主要 API",
|
||||
|
@ -1653,7 +1653,6 @@
|
||||
"HuggingFace Token": "HuggingFace 符元",
|
||||
"Image Captioning": "圖片註解",
|
||||
"Generate Caption": "產生圖片註解",
|
||||
"Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)",
|
||||
"Injection Position": "插入位置",
|
||||
"Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
|
||||
"Injection Template": "插入範本",
|
||||
|
@ -154,8 +154,18 @@ export const extension_settings = {
|
||||
refine_mode: false,
|
||||
},
|
||||
expressions: {
|
||||
/** @type {number} see `EXPRESSION_API` */
|
||||
api: undefined,
|
||||
/** @type {string[]} */
|
||||
custom: [],
|
||||
showDefault: false,
|
||||
translate: false,
|
||||
/** @type {string} */
|
||||
fallback_expression: undefined,
|
||||
/** @type {string} */
|
||||
llmPrompt: undefined,
|
||||
allowMultiple: true,
|
||||
rerollIfSame: false,
|
||||
},
|
||||
connectionManager: {
|
||||
selectedProfile: '',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
<div id="{{item}}" class="expression_list_item">
|
||||
{{#each images}}
|
||||
<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
|
||||
<div class="expression_list_buttons">
|
||||
<div class="menu_button expression_list_upload" title="Upload image">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
@ -7,11 +8,14 @@
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="expression_list_title {{textClass}}">
|
||||
<span>{{item}}</span>
|
||||
{{#if isCustom}}
|
||||
<div class="expression_list_title">
|
||||
<span>{{../expression}}</span>
|
||||
{{#if ../isCustom}}
|
||||
<small class="expression_list_custom">(custom)</small>
|
||||
{{/if}}
|
||||
</div>
|
||||
<img class="expression_list_image" src="{{imageSrc}}" />
|
||||
<div class="expression_list_image_container" title="{{this.title}}">
|
||||
<img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" />
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
@ -6,17 +6,17 @@
|
||||
</div>
|
||||
|
||||
<div class="inline-drawer-content">
|
||||
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
|
||||
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings.">
|
||||
<input id="expression_translate" type="checkbox">
|
||||
<span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="expressions_show_default">
|
||||
<input id="expressions_show_default" type="checkbox">
|
||||
<span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
|
||||
<label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
|
||||
<input id="expressions_allow_multiple" type="checkbox">
|
||||
<span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>
|
||||
</label>
|
||||
<label id="image_type_block" class="checkbox_label" for="image_type_toggle">
|
||||
<input id="image_type_toggle" type="checkbox">
|
||||
<span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
|
||||
<label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.">
|
||||
<input id="expressions_reroll_if_same" type="checkbox">
|
||||
<span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
|
||||
</label>
|
||||
<div class="expression_api_block m-b-1 m-t-1">
|
||||
<label for="expression_api" data-i18n="Classifier API">Classifier API</label>
|
||||
@ -75,8 +75,20 @@
|
||||
<span data-i18n="Remove all image overrides">Remove all image overrides</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
|
||||
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
|
||||
<p class="hint">
|
||||
<b data-i18n="Hint:">Hint:</b>
|
||||
<i>
|
||||
<span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
|
||||
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt>
|
||||
</i>
|
||||
</p>
|
||||
<p>
|
||||
<i>
|
||||
<span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a
|
||||
dash.
|
||||
Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>
|
||||
</i>
|
||||
</p>
|
||||
<h3 id="image_list_header">
|
||||
<strong data-i18n="Sprite set:">Sprite set:</strong> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
|
@ -111,6 +111,10 @@ img.expression.default {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expression_list_image_container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expression_list_title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -126,6 +130,9 @@ img.expression.default {
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
}
|
||||
.expression_list_custom {
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
|
||||
.expression_list_buttons {
|
||||
position: absolute;
|
||||
@ -162,11 +169,24 @@ img.expression.default {
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
#image_list .success {
|
||||
#image_list .expression_list_item[data-expression-type="success"] .expression_list_title {
|
||||
color: green;
|
||||
}
|
||||
|
||||
#image_list .failure {
|
||||
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title {
|
||||
color: darkolivegreen;
|
||||
}
|
||||
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before {
|
||||
content: '➕';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -9px;
|
||||
font-size: 14px;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 darkolivegreen;
|
||||
}
|
||||
|
||||
#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@ -189,3 +209,12 @@ img.expression.default {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"],
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] {
|
||||
opacity: 0.3;
|
||||
transition: opacity var(--animation-duration) ease;
|
||||
}
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover,
|
||||
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus {
|
||||
opacity: unset;
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
|
||||
<div class="m-b-1" data-i18n="upload_expression_naming_1">
|
||||
Sprite names must follow the naming schema for the selected expression: {{expression}}
|
||||
</div>
|
||||
<div data-i18n="upload_expression_naming_2">
|
||||
For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
|
||||
</div>
|
||||
<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>
|
||||
{{#if clickedFileName}}
|
||||
<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
|
||||
<tt>{{clickedFileName}}</tt>
|
||||
{{/if}}
|
@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
|
||||
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
|
||||
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
|
||||
|
||||
window['translate'] = translate;
|
||||
globalThis.translate = translate;
|
||||
|
||||
jQuery(async () => {
|
||||
const html = await renderExtensionTemplateAsync('translate', 'index');
|
||||
|
@ -27,14 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { GoogleTranslateTtsProvider } from './google-translate.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
|
||||
let voiceMapEntries = [];
|
||||
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
|
||||
let talkingHeadState = false;
|
||||
let lastChatId = null;
|
||||
let lastMessage = null;
|
||||
let lastMessageHash = null;
|
||||
@ -166,27 +164,6 @@ async function moduleWorker() {
|
||||
updateUiAudioPlayState();
|
||||
}
|
||||
|
||||
function talkingAnimation(switchValue) {
|
||||
if (!modules.includes('talkinghead')) {
|
||||
console.debug('Talking Animation module not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = getApiUrl();
|
||||
const animationType = switchValue ? 'start' : 'stop';
|
||||
|
||||
if (switchValue !== talkingHeadState) {
|
||||
try {
|
||||
console.log(animationType + ' Talking Animation');
|
||||
doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
|
||||
talkingHeadState = switchValue;
|
||||
} catch (error) {
|
||||
// Handle the error here or simply ignore it to prevent logging
|
||||
}
|
||||
}
|
||||
updateUiAudioPlayState();
|
||||
}
|
||||
|
||||
function resetTtsPlayback() {
|
||||
// Stop system TTS utterance
|
||||
cancelTtsPlay();
|
||||
@ -378,7 +355,6 @@ function onAudioControlClicked() {
|
||||
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
|
||||
if (!audioElement.paused || isTtsProcessing()) {
|
||||
resetTtsPlayback();
|
||||
talkingAnimation(false);
|
||||
} else {
|
||||
// Default play behavior if not processing or playing is to play the last message.
|
||||
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
|
||||
@ -405,7 +381,6 @@ function addAudioControl() {
|
||||
function completeCurrentAudioJob() {
|
||||
audioQueueProcessorReady = true;
|
||||
currentAudioJob = null;
|
||||
talkingAnimation(false); //stop lip animation
|
||||
// updateUiPlayState();
|
||||
wrapper.update();
|
||||
}
|
||||
@ -436,7 +411,6 @@ async function processAudioJobQueue() {
|
||||
audioQueueProcessorReady = false;
|
||||
currentAudioJob = audioJobQueue.shift();
|
||||
playAudioData(currentAudioJob);
|
||||
talkingAnimation(true);
|
||||
} catch (error) {
|
||||
toastr.error(error.toString());
|
||||
console.error(error);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { isMobile } from '../../RossAscends-mods.js';
|
||||
import { getPreviewString } from './index.js';
|
||||
import { talkingAnimation } from './index.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
export { SystemTtsProvider };
|
||||
|
||||
@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
|
||||
//placing the speak invocation inside a callback fixes ordering and onend issues.
|
||||
setTimeout(function () {
|
||||
speechSynthesis.speak(newUtt);
|
||||
talkingAnimation(true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@ -240,7 +238,6 @@ class SystemTtsProvider {
|
||||
//some code to execute when done
|
||||
resolve(silence);
|
||||
console.log('System TTS done');
|
||||
talkingAnimation(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) {
|
||||
*/
|
||||
async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
|
||||
try {
|
||||
if (settings.translate_files && typeof window['translate'] === 'function') {
|
||||
if (settings.translate_files && typeof globalThis.translate === 'function') {
|
||||
console.log(`Vectors: Translating file ${fileName} to English...`);
|
||||
const translatedText = await window['translate'](fileText, 'en');
|
||||
const translatedText = await globalThis.translate(fileText, 'en');
|
||||
fileText = translatedText;
|
||||
}
|
||||
|
||||
|
@ -1845,14 +1845,15 @@ async function loadContextSettings() {
|
||||
|
||||
/**
|
||||
* Common function to perform fuzzy search with optional caching
|
||||
* @template T
|
||||
* @param {string} type - Type of search from fuzzySearchCategories
|
||||
* @param {any[]} data - Data array to search in
|
||||
* @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration
|
||||
* @param {T[]} data - Data array to search in
|
||||
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
|
||||
* @param {string} searchValue - The search term
|
||||
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
|
||||
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
|
||||
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
|
||||
*/
|
||||
function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
||||
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
||||
// Check cache if provided
|
||||
if (fuzzySearchCaches) {
|
||||
const cache = fuzzySearchCaches[type];
|
||||
|
@ -125,8 +125,14 @@ router.get('/get', jsonParser, function (request, response) {
|
||||
.map((file) => {
|
||||
const pathToSprite = path.join(spritesPath, file);
|
||||
const mtime = fs.statSync(pathToSprite).mtime?.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
|
||||
|
||||
const fileName = path.parse(pathToSprite).name.toLowerCase();
|
||||
// Extract the label from the filename via regex, which can be suffixed with a sub-name, either connected with a dash or a dot.
|
||||
// Examples: joy.png, joy-1.png, joy.expressive.png
|
||||
const label = fileName.match(/^(.+?)(?:[-\\.].*?)?$/)?.[1] ?? fileName;
|
||||
|
||||
return {
|
||||
label: path.parse(pathToSprite).name.toLowerCase(),
|
||||
label: label,
|
||||
path: `/characters/${name}/${file}` + (mtime ? `?t=${mtime}` : ''),
|
||||
};
|
||||
});
|
||||
@ -141,8 +147,9 @@ router.get('/get', jsonParser, function (request, response) {
|
||||
router.post('/delete', jsonParser, async (request, response) => {
|
||||
const label = request.body.label;
|
||||
const name = request.body.name;
|
||||
const spriteName = request.body.spriteName || label;
|
||||
|
||||
if (!label || !name) {
|
||||
if (!spriteName || !name) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
@ -158,7 +165,7 @@ router.post('/delete', jsonParser, async (request, response) => {
|
||||
|
||||
// Remove existing sprite with the same label
|
||||
for (const file of files) {
|
||||
if (path.parse(file).name === label) {
|
||||
if (path.parse(file).name === spriteName) {
|
||||
fs.rmSync(path.join(spritesPath, file));
|
||||
}
|
||||
}
|
||||
@ -221,6 +228,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
const file = request.file;
|
||||
const label = request.body.label;
|
||||
const name = request.body.name;
|
||||
const spriteName = request.body.spriteName || label;
|
||||
|
||||
if (!file || !label || !name) {
|
||||
return response.sendStatus(400);
|
||||
@ -243,12 +251,12 @@ router.post('/upload', urlencodedParser, async (request, response) => {
|
||||
|
||||
// Remove existing sprite with the same label
|
||||
for (const file of files) {
|
||||
if (path.parse(file).name === label) {
|
||||
if (path.parse(file).name === spriteName) {
|
||||
fs.rmSync(path.join(spritesPath, file));
|
||||
}
|
||||
}
|
||||
|
||||
const filename = label + path.parse(file.originalname).ext;
|
||||
const filename = spriteName + path.parse(file.originalname).ext;
|
||||
const spritePath = path.join(file.destination, file.filename);
|
||||
const pathToFile = path.join(spritesPath, filename);
|
||||
// Copy uploaded file to sprites folder
|
||||
|
Loading…
x
Reference in New Issue
Block a user