diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 39f296545..2f7963e0a 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -58,6 +58,17 @@ function isTalkingHeadEnabled() { return extension_settings.expressions.talkinghead && !extension_settings.expressions.local; } +/** + * Toggles Talkinghead mode on/off. + * + * Implements the `/th` slash command, which is meant to be bound to a Quick Reply button + * as a quick way to switch Talkinghead on or off (e.g. to conserve GPU resources when AFK + * for a long time). + */ +function toggleTalkingHeadCommand(_) { + setTalkingHeadState(!extension_settings.expressions.talkinghead); +} + function isVisualNovelMode() { return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId); } @@ -389,13 +400,14 @@ function onExpressionsShowDefaultInput() { } /** - * Stops animating a talkinghead. + * Stops animating Talkinghead. */ async function unloadTalkingHead() { if (!modules.includes('talkinghead')) { console.debug('talkinghead module is disabled'); return; } + console.debug('expressions: Stopping Talkinghead'); try { const url = new URL(getApiUrl()); @@ -418,6 +430,7 @@ async function loadTalkingHead() { console.debug('talkinghead module is disabled'); return; } + console.debug('expressions: Starting Talkinghead'); const spriteFolderName = getSpriteFolderName(); @@ -528,8 +541,7 @@ function handleImageChange() { return; } - if (isTalkingHeadEnabled()) { - // Method get IP of endpoint + if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`; $('#expression-holder').css({ display: '' }); if (imgElement.src !== talkingheadResultFeedSrc) { @@ -545,20 +557,26 @@ function handleImageChange() { } }) .catch(error => { - console.error(error); // Log the error if necessary + console.error(error); }); } } } else { - imgElement.src = ''; //remove incase char doesnt have expressions - setExpression(getContext().name2, FALLBACK_EXPRESSION, true); + imgElement.src = ''; // remove in case char doesn't have expressions + + // When switching Talkinghead off, force-set the character to the last known expression, if any. + // This preserves the same expression Talkinghead had at the moment it was switched off. + const charName = getContext().name2; + const last = lastExpression[charName]; + const targetExpression = last ? last : FALLBACK_EXPRESSION; + setExpression(charName, targetExpression, true); } } async function moduleWorker() { const context = getContext(); - // Hide and disable talkinghead while in local mode + // Hide and disable Talkinghead while in local mode $('#image_type_block').toggle(!extension_settings.expressions.local); if (extension_settings.expressions.local && extension_settings.expressions.talkinghead) { @@ -691,7 +709,7 @@ async function moduleWorker() { } /** - * Starts/stops talkinghead talking animation. + * Starts/stops Talkinghead talking animation. * * Talking starts only when all the following conditions are met: * - The LLM is currently streaming its output. @@ -700,10 +718,13 @@ async function moduleWorker() { * * In all other cases, talking stops. * - * A talkinghead API call is made only when the talking state changes. + * A Talkinghead API call is made only when the talking state changes. + * + * Note that also the TTS system, if enabled, starts/stops the Talkinghead talking animation. + * See `talkingAnimation` in `SillyTavern/public/scripts/extensions/tts/index.js`. */ async function updateTalkingState() { - // Don't bother if talkinghead is disabled or not loaded. + // Don't bother if Talkinghead is disabled or not loaded. if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { return; } @@ -727,7 +748,7 @@ async function updateTalkingState() { newTalkingState = false; } try { - // Call the talkinghead API only if the talking state changed. + // Call the Talkinghead API only if the talking state changed. if (newTalkingState !== lastTalkingState) { console.debug(`updateTalkingState: calling ${url.pathname}`); await doExtrasFetch(url); @@ -787,6 +808,7 @@ function getSpriteFolderName(characterMessage = null, characterName = null) { } function setTalkingHeadState(newState) { + console.debug(`expressions: New talkinghead state: ${newState}`); extension_settings.expressions.talkinghead = newState; // Store setting saveSettingsDebounced(); @@ -871,12 +893,12 @@ async function setSpriteSlashCommand(_, spriteId) { spriteId = spriteId.trim().toLowerCase(); - // In talkinghead mode, don't check for the existence of the sprite + // In Talkinghead mode, don't check for the existence of the sprite // (emotion names are the same as for sprites, but it only needs "talkinghead.png"). const currentLastMessage = getLastCharacterMessage(); const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name); let label = spriteId; - if (!isTalkingHeadEnabled()) { + if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { await validateImages(spriteFolderName); // Fuzzy search for sprite @@ -1144,7 +1166,7 @@ async function getExpressionsList() { } async function setExpression(character, expression, force) { - if (!isTalkingHeadEnabled()) { + if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) { console.debug('entered setExpressions'); await validateImages(character); const img = $('img.expression'); @@ -1255,8 +1277,8 @@ async function setExpression(character, expression, force) { document.getElementById('expression-holder').style.display = ''; } else { - // Set the talkinghead emotion to the specified expression - // TODO: For now, talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode. + // Set the Talkinghead emotion to the specified expression + // TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode. try { let result = await isTalkingHeadAvailable(); if (result) { @@ -1409,8 +1431,8 @@ async function onClickExpressionUpload(event) { // Reset the input e.target.form.reset(); - // In talkinghead mode, when a new talkinghead image is uploaded, refresh the live char. - if (isTalkingHeadEnabled() && id === 'talkinghead') { + // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char. + if (id === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) { await loadTalkingHead(); } }; @@ -1520,6 +1542,11 @@ async function onClickExpressionUploadPackButton() { // Reset the input e.target.form.reset(); + + // In Talkinghead mode, refresh the live char. + if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { + await loadTalkingHead(); + } }; $('#expression_upload_pack') @@ -1657,6 +1684,34 @@ async function fetchImagesNoCache() { $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); } + // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized. + // We currently do this via loading/unloading. Could be improved by adding new pause/unpause endpoints to Extras. + document.addEventListener("visibilitychange", function (event) { + let pageIsVisible; + if (document.hidden) { + console.debug('expressions: SillyTavern is now hidden'); + pageIsVisible = false; + } else { + console.debug('expressions: SillyTavern is now visible'); + pageIsVisible = true; + } + + if (isTalkingHeadEnabled() && modules.includes('talkinghead')) { + isTalkingHeadAvailable().then(result => { + if (result) { + if (pageIsVisible) { + loadTalkingHead(); + } else { + unloadTalkingHead(); + } + handleImageChange(); // Change image as needed + } else { + //console.log("talkinghead does not exist."); + } + }); + } + }); + addExpressionImage(); addVisualNovelMode(); addSettings(); @@ -1664,7 +1719,7 @@ async function fetchImagesNoCache() { const updateFunction = wrapper.update.bind(wrapper); setInterval(updateFunction, UPDATE_INTERVAL); moduleWorker(); - // For setting the talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule. + // For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule. const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState); const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState); setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL); @@ -1701,4 +1756,5 @@ async function fetchImagesNoCache() { registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '(spriteId) – force sets the sprite for the current character', true, true); registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '(optional folder) – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true); registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '(charName) – Returns the last set sprite / expression for the named character.', true, true); + registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], '– Character Expressions: toggles Image Type - talkinghead (extras) on/off.'); })();