Merge pull request #3725 from SillyTavern/feat/expressions-filter-available Adds filtering to expressions to ignore labels that do not have sprites available

6bfa54e9b4535579abc22ce58a82b44eca788b00

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +58 -16Ignore whitespace
public/scripts/extensions/expressions/index.js+53 -15
@@ -4,7 +4,7 @@ import { characters, eventSource, event_types, generateRaw, getRequestHeaders, m
4import { dragElement, isMobile } from '../../RossAscends-mods.js';4import { dragElement, isMobile } from '../../RossAscends-mods.js';
5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
6import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';6import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
7import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';7import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar, isFalseBoolean } from '../../utils.js';
8import { hideMutedSprites, selected_group } from '../../group-chats.js';8import { hideMutedSprites, selected_group } from '../../group-chats.js';
9import { isJsonSchemaSupported } from '../../textgen-settings.js';9import { isJsonSchemaSupported } from '../../textgen-settings.js';
10import { debounce_timeout } from '../../constants.js';10import { debounce_timeout } from '../../constants.js';
@@ -679,7 +679,7 @@ async function setSpriteFolderCommand(_, folder) {
679 return '';679 return '';
680}680}
681681
682async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {682async function classifyCallback(/** @type {{api: string?, filter: string?, prompt: string?}} */ { api = null, filter = null, prompt = null }, text) {
683 if (!text) {683 if (!text) {
684 toastr.error('No text provided');684 toastr.error('No text provided');
685 return '';685 return '';
@@ -690,13 +690,14 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ {
690 }690 }
691691
692 const expressionApi = EXPRESSION_API[api] || extension_settings.expressions.api;692 const expressionApi = EXPRESSION_API[api] || extension_settings.expressions.api;
693 const filterAvailable = !isFalseBoolean(filter);
693694
694 if (!modules.includes('classify') && expressionApi == EXPRESSION_API.extras) {695 if (!modules.includes('classify') && expressionApi == EXPRESSION_API.extras) {
695 toastr.warning('Text classification is disabled or not available');696 toastr.warning('Text classification is disabled or not available');
696 return '';697 return '';
697 }698 }
698699
699 const label = await getExpressionLabel(text, expressionApi, { customPrompt: prompt });700 const label = await getExpressionLabel(text, expressionApi, { filterAvailable: filterAvailable, customPrompt: prompt });
700 console.debug(`Classification result for "${text}": ${label}`);701 console.debug(`Classification result for "${text}": ${label}`);
701 return label;702 return label;
702}703}
@@ -992,10 +993,11 @@ function onTextGenSettingsReady(args) {
992 * @param {string} text - The text to classify and retrieve the expression label for.993 * @param {string} text - The text to classify and retrieve the expression label for.
993 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.994 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
994 * @param {object} [options={}] - Optional arguments.995 * @param {object} [options={}] - Optional arguments.
996 * @param {boolean?} [options.filterAvailable=null] - Whether to filter available expressions. If not specified, uses the extension setting.
995 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.997 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
996 * @returns {Promise<string?>} - The label of the expression.998 * @returns {Promise<string?>} - The label of the expression.
997 */999 */
998export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {1000export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { filterAvailable = null, customPrompt = null } = {}) {
999 // Return if text is undefined, saving a costly fetch request1001 // Return if text is undefined, saving a costly fetch request
1000 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {1002 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
1001 return extension_settings.expressions.fallback_expression;1003 return extension_settings.expressions.fallback_expression;
@@ -1007,6 +1009,11 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
10071009
1008 text = sampleClassifyText(text);1010 text = sampleClassifyText(text);
10091011
1012 filterAvailable ??= extension_settings.expressions.filterAvailable;
1013 if (filterAvailable && ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(expressionsApi)) {
1014 console.debug('Filter available is only supported for LLM and WebLLM expressions');
1015 }
1016
1010 try {1017 try {
1011 switch (expressionsApi) {1018 switch (expressionsApi) {
1012 // Local BERT pipeline1019 // Local BERT pipeline
@@ -1031,7 +1038,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1031 return extension_settings.expressions.fallback_expression;1038 return extension_settings.expressions.fallback_expression;
1032 }1039 }
10331040
1034 const expressionsList = await getExpressionsList();1041 const expressionsList = await getExpressionsList({ filterAvailable: filterAvailable });
1035 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);1042 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1036 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);1043 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
1037 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);1044 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
@@ -1044,7 +1051,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1044 return extension_settings.expressions.fallback_expression;1051 return extension_settings.expressions.fallback_expression;
1045 }1052 }
10461053
1047 const expressionsList = await getExpressionsList();1054 const expressionsList = await getExpressionsList({ filterAvailable: filterAvailable });
1048 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);1055 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1049 const messages = [1056 const messages = [
1050 { role: 'user', content: text + '\n\n' + prompt },1057 { role: 'user', content: text + '\n\n' + prompt },
@@ -1324,12 +1331,28 @@ function getCachedExpressions() {
1324 return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);1331 return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
1325}1332}
13261333
1327export async function getExpressionsList() {1334export async function getExpressionsList({ filterAvailable = false } = {}) {
1328 // Return cached list if available1335 // If there is no cached list, load and cache it
1329 if (Array.isArray(expressionsList)) {1336 if (!Array.isArray(expressionsList)) {
1330 return getCachedExpressions();1337 expressionsList = await resolveExpressionsList();
1331 }1338 }
13321339
1340 const expressions = getCachedExpressions();
1341
1342 // Filtering is only available for llm and webllm APIs
1343 if (!filterAvailable || ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) {
1344 return expressions;
1345 }
1346
1347 // Get expressions with available sprites
1348 const currentLastMessage = selected_group ? getLastCharacterMessage() : null;
1349 const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage?.name);
1350
1351 return expressions.filter(label => {
1352 const expression = spriteCache[spriteFolderName]?.find(x => x.label === label);
1353 return (expression?.files.length ?? 0) > 0;
1354 });
1355
1333 /**1356 /**
1334 * Returns the list of expressions from the API or fallback in offline mode.1357 * Returns the list of expressions from the API or fallback in offline mode.
1335 * @returns {Promise<string[]>}1358 * @returns {Promise<string[]>}
@@ -1376,9 +1399,6 @@ export async function getExpressionsList() {
1376 expressionsList = DEFAULT_EXPRESSIONS.slice();1399 expressionsList = DEFAULT_EXPRESSIONS.slice();
1377 return expressionsList;1400 return expressionsList;
1378 }1401 }
1379
1380 const result = await resolveExpressionsList();
1381 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
1382}1402}
13831403
1384/**1404/**
@@ -2106,6 +2126,10 @@ function migrateSettings() {
2106 extension_settings.expressions.rerollIfSame = !!$(this).prop('checked');2126 extension_settings.expressions.rerollIfSame = !!$(this).prop('checked');
2107 saveSettingsDebounced();2127 saveSettingsDebounced();
2108 });2128 });
2129 $('#expressions_filter_available').prop('checked', extension_settings.expressions.filterAvailable).on('input', function () {
2130 extension_settings.expressions.filterAvailable = !!$(this).prop('checked');
2131 saveSettingsDebounced();
2132 });
2109 $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);2133 $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
2110 $(document).on('dragstart', '.expression', (e) => {2134 $(document).on('dragstart', '.expression', (e) => {
2111 e.preventDefault();2135 e.preventDefault();
@@ -2283,13 +2307,13 @@ function migrateSettings() {
2283 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2307 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2284 name: 'expression-list',2308 name: 'expression-list',
2285 aliases: ['expressions'],2309 aliases: ['expressions'],
2286 /** @type {(args: {return: string}) => Promise<string>} */2310 /** @type {(args: {return: string, filter: string}) => Promise<string>} */
2287 callback: async (args) => {2311 callback: async (args) => {
2288 let returnType =2312 let returnType =
2289 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */2313 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
2290 (args.return);2314 (args.return);
22912315
2292 const list = await getExpressionsList();2316 const list = await getExpressionsList({ filterAvailable: !isFalseBoolean(args.filter) });
22932317
2294 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });2318 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
2295 },2319 },
@@ -2302,6 +2326,13 @@ function migrateSettings() {
2302 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),2326 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
2303 forceEnum: true,2327 forceEnum: true,
2304 }),2328 }),
2329 SlashCommandNamedArgument.fromProps({
2330 name: 'filter',
2331 description: 'Filter the list to only include expressions that have available sprites for the current character.',
2332 typeList: [ARGUMENT_TYPE.BOOLEAN],
2333 enumList: commonEnumProviders.boolean('trueFalse')(),
2334 defaultValue: 'true',
2335 }),
2305 ],2336 ],
2306 returns: 'The comma-separated list of available expressions, including custom expressions.',2337 returns: 'The comma-separated list of available expressions, including custom expressions.',
2307 helpString: 'Returns a list of available expressions, including custom expressions.',2338 helpString: 'Returns a list of available expressions, including custom expressions.',
@@ -2318,6 +2349,13 @@ function migrateSettings() {
2318 enumList: Object.keys(EXPRESSION_API).map(api => new SlashCommandEnumValue(api, null, enumTypes.enum)),2349 enumList: Object.keys(EXPRESSION_API).map(api => new SlashCommandEnumValue(api, null, enumTypes.enum)),
2319 }),2350 }),
2320 SlashCommandNamedArgument.fromProps({2351 SlashCommandNamedArgument.fromProps({
2352 name: 'filter',
2353 description: 'Filter the list to only include expressions that have available sprites for the current character.',
2354 typeList: [ARGUMENT_TYPE.BOOLEAN],
2355 enumList: commonEnumProviders.boolean('trueFalse')(),
2356 defaultValue: 'true',
2357 }),
2358 SlashCommandNamedArgument.fromProps({
2321 name: 'prompt',2359 name: 'prompt',
2322 description: 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.',2360 description: 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.',
2323 typeList: [ARGUMENT_TYPE.STRING],2361 typeList: [ARGUMENT_TYPE.STRING],
public/scripts/extensions/expressions/settings.html+5 -1
@@ -29,7 +29,11 @@
29 </select>29 </select>
30 </div>30 </div>
31 <div class="expression_llm_prompt_block m-b-1 m-t-1">31 <div class="expression_llm_prompt_block m-b-1 m-t-1">
32 <label for="expression_llm_prompt" class="title_restorable">32 <label class="checkbox_label" for="expressions_filter_available" title="When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them." data-i18n="[title]When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.">
33 <input id="expressions_filter_available" type="checkbox">
34 <span data-i18n="Filter expressions for available sprites">Filter expressions for available sprites</span>
35 </label>
36 <label for="expression_llm_prompt" class="title_restorable m-t-1">
33 <span data-i18n="LLM Prompt">LLM Prompt</span>37 <span data-i18n="LLM Prompt">LLM Prompt</span>
34 <div id="expression_llm_prompt_restore" title="Restore default value" class="right_menu_button">38 <div id="expression_llm_prompt_restore" title="Restore default value" class="right_menu_button">
35 <i class="fa-solid fa-clock-rotate-left fa-sm"></i>39 <i class="fa-solid fa-clock-rotate-left fa-sm"></i>