yoink talkinghead - goodbye extras dependency

73393a5d5eadc92814d9689091d13c24bc0e3d95

Wolfsblvt <wolfsblvt@gmail.com>

8 files changed, +118 -586Ignore whitespace
public/locales/fr-fr.json+0 -1
@@ -1602,7 +1602,6 @@
16021602 "Character Expressions": "Expressions de personnages",
16031603 "Translate text to English before classification": "Traduire le texte en anglais avant de le classer",
16041604 "Show default images (emojis) if sprite missing": "Afficher les images par défaut (emojis) si le sprite est manquant",
1605- "Image Type - talkinghead (extras)": "Type d'image - talkinghead (extras)",
16061605 "Classifier API": "API de classification",
16071606 "Select the API for classifying expressions.": "Sélectionnez l'API pour classer les expressions.",
16081607 "Main API": "API principale",
public/locales/ko-kr.json+0 -1
@@ -1467,7 +1467,6 @@
14671467 "menu within": "내의 메뉴",
14681468 "Translate text to English before classification": "분류 전에 텍스트를 영어로 번역합니다.",
14691469 "Show default images (emojis) if sprite missing": "해당하는 스프라이트가 없으면 기본 이미지 (이모지들)을 표시합니다.",
1470- "Image Type - talkinghead (extras)": "이미지 유형 - 토킹 헤드 (부가 사항)",
14711470 "Classifier API": "분류를 위한 API",
14721471 "Select the API for classifying expressions.": "감정 이미지들을 분류할 API를 선택하세요.",
14731472 "Local": "로컬",
public/locales/zh-cn.json+0 -1
@@ -1349,7 +1349,6 @@
13491349 "Character Expressions": "角色表情",
13501350 "Translate text to English before classification": "分类之前将文本翻译成英文",
13511351 "Show default images (emojis) if sprite missing": "如果表情包缺失,则显示默认图像(表情符号)",
1352- "Image Type - talkinghead (extras)": "图像类型 - 说话头像(附加内容)",
13531352 "Classifier API": "分类器 API",
13541353 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
13551354 "Main API": "主要 API",
public/locales/zh-tw.json+0 -1
@@ -1653,7 +1653,6 @@
16531653 "HuggingFace Token": "HuggingFace 符元",
16541654 "Image Captioning": "圖片註解",
16551655 "Generate Caption": "產生圖片註解",
1656- "Image Type - talkinghead (extras)": "圖片類型 - talkinghead(額外選項)",
16571656 "Injection Position": "插入位置",
16581657 "Injection position. Relative (to other prompts in prompt manager) or In-chat @ Depth.": "插入位置(與提示詞管理器中的其他提示相比)或聊天中的深度位置。",
16591658 "Injection Template": "插入範本",
public/scripts/extensions/expressions/index.js+118 -549
@@ -38,11 +38,9 @@ export { MODULE_NAME };
3838const MODULE_NAME = 'expressions';
3939const UPDATE_INTERVAL = 2000;
4040const STREAMING_UPDATE_INTERVAL = 10000;
41-const TALKINGCHECK_UPDATE_INTERVAL = 500;
4241const DEFAULT_FALLBACK_EXPRESSION = 'joy';
4342const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
4443const DEFAULT_EXPRESSIONS = [
45- 'talkinghead',
4644 'admiration',
4745 'amusement',
4846 'anger',
@@ -86,8 +84,6 @@ const NO_IMAGE_PLACEHOLDER = { title: 'No Image', type: 'failure', fileName: 'No
8684let expressionsList = null;
8785let lastCharacter = undefined;
8886let lastMessage = null;
89-let lastTalkingState = false;
90-let lastTalkingStateMessage = null; // last message as seen by `updateTalkingState` (tracked separately, different timer)
9187/** @type {{[characterKey: string]: Expression[]}} */
9288let spriteCache = {};
9389let inApiCall = false;
@@ -96,10 +92,6 @@ let lastServerResponseTime = 0;
9692/** @type {{[characterName: string]: string}} */
9793export let lastExpression = {};
9894
99-function isTalkingHeadEnabled() {
100- return extension_settings.expressions.talkinghead && extension_settings.expressions.api == EXPRESSION_API.extras;
101-}
102-
10395/**
10496 * Returns the fallback expression if explicitly chosen, otherwise the default one
10597 * @returns {string} expression name
@@ -108,18 +100,6 @@ function getFallbackExpression() {
108100 return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION;
109101}
110102
111-/**
112- * Toggles Talkinghead mode on/off.
113- *
114- * Implements the `/th` slash command, which is meant to be bound to a Quick Reply button
115- * as a quick way to switch Talkinghead on or off (e.g. to conserve GPU resources when AFK
116- * for a long time).
117- */
118-function toggleTalkingHeadCommand(_) {
119- setTalkingHeadState(!extension_settings.expressions.talkinghead);
120- return String(extension_settings.expressions.talkinghead);
121-}
122-
123103function isVisualNovelMode() {
124104 return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
125105}
@@ -451,199 +431,9 @@ function onExpressionsShowDefaultInput() {
451431 }
452432}
453433
454-/**
455- * Stops animating Talkinghead.
456- */
457-async function unloadTalkingHead() {
458- if (!modules.includes('talkinghead')) {
459- console.debug('talkinghead module is disabled');
460- return;
461- }
462- console.debug('expressions: Stopping Talkinghead');
463-
464- try {
465- const url = new URL(getApiUrl());
466- url.pathname = '/api/talkinghead/unload';
467- const loadResponse = await doExtrasFetch(url);
468- if (!loadResponse.ok) {
469- throw new Error(loadResponse.statusText);
470- }
471- //console.log(`Response: ${loadResponseText}`);
472- } catch (error) {
473- //console.error(`Error unloading - ${error}`);
474- }
475-}
476-
477-/**
478- * Posts `talkinghead.png` of the current character to the talkinghead module in SillyTavern-extras, to start animating it.
479- */
480-async function loadTalkingHead() {
481- if (!modules.includes('talkinghead')) {
482- console.debug('talkinghead module is disabled');
483- return;
484- }
485- console.debug('expressions: Starting Talkinghead');
486-
487- const spriteFolderName = getSpriteFolderName();
488-
489- const talkingheadPath = `/characters/${encodeURIComponent(spriteFolderName)}/talkinghead.png`;
490- const emotionsSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_emotions.json`;
491- const animatorSettingsPath = `/characters/${encodeURIComponent(spriteFolderName)}/_animator.json`;
492-
493- try {
494- const spriteResponse = await fetch(talkingheadPath);
495-
496- if (!spriteResponse.ok) {
497- throw new Error(spriteResponse.statusText);
498- }
499-
500- const spriteBlob = await spriteResponse.blob();
501- const spriteFile = new File([spriteBlob], 'talkinghead.png', { type: 'image/png' });
502- const formData = new FormData();
503- formData.append('file', spriteFile);
504-
505- const url = new URL(getApiUrl());
506- url.pathname = '/api/talkinghead/load';
507-
508- const loadResponse = await doExtrasFetch(url, {
509- method: 'POST',
510- body: formData,
511- });
512-
513- if (!loadResponse.ok) {
514- throw new Error(loadResponse.statusText);
515- }
516-
517- const loadResponseText = await loadResponse.text();
518- console.log(`Load talkinghead response: ${loadResponseText}`);
519-
520- // Optional: per-character emotion templates
521- let emotionsSettings;
522- try {
523- const emotionsResponse = await fetch(emotionsSettingsPath);
524- if (emotionsResponse.ok) {
525- emotionsSettings = await emotionsResponse.json();
526- console.log(`Loaded ${emotionsSettingsPath}`);
527- } else {
528- throw new Error();
529- }
530- }
531- catch (error) {
532- emotionsSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
533- console.log(`No valid config at ${emotionsSettingsPath}, using server defaults`);
534- }
535- try {
536- const url = new URL(getApiUrl());
537- url.pathname = '/api/talkinghead/load_emotion_templates';
538- const apiResult = await doExtrasFetch(url, {
539- method: 'POST',
540- headers: {
541- 'Content-Type': 'application/json',
542- 'Bypass-Tunnel-Reminder': 'bypass',
543- },
544- body: JSON.stringify(emotionsSettings),
545- });
546-
547- if (!apiResult.ok) {
548- throw new Error(apiResult.statusText);
549- }
550- }
551- catch (error) {
552- // it's ok if not supported
553- console.log('Failed to send _emotions.json (backend too old?), ignoring');
554- }
555-
556- // Optional: per-character animator and postprocessor config
557- let animatorSettings;
558- try {
559- const animatorResponse = await fetch(animatorSettingsPath);
560- if (animatorResponse.ok) {
561- animatorSettings = await animatorResponse.json();
562- console.log(`Loaded ${animatorSettingsPath}`);
563- } else {
564- throw new Error();
565- }
566- }
567- catch (error) {
568- animatorSettings = {}; // blank -> use server defaults (to unload the previous character's customizations)
569- console.log(`No valid config at ${animatorSettingsPath}, using server defaults`);
570- }
571- try {
572- const url = new URL(getApiUrl());
573- url.pathname = '/api/talkinghead/load_animator_settings';
574- const apiResult = await doExtrasFetch(url, {
575- method: 'POST',
576- headers: {
577- 'Content-Type': 'application/json',
578- 'Bypass-Tunnel-Reminder': 'bypass',
579- },
580- body: JSON.stringify(animatorSettings),
581- });
582-
583- if (!apiResult.ok) {
584- throw new Error(apiResult.statusText);
585- }
586- }
587- catch (error) {
588- // it's ok if not supported
589- console.log('Failed to send _animator.json (backend too old?), ignoring');
590- }
591- } catch (error) {
592- console.error(`Error loading talkinghead image: ${talkingheadPath} - ${error}`);
593- }
594-}
595-
596-function handleImageChange() {
597- const imgElement = document.querySelector('img#expression-image.expression');
598-
599- if (!imgElement || !(imgElement instanceof HTMLImageElement)) {
600- console.log('Cannot find addExpressionImage()');
601- return;
602- }
603-
604- if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
605- const talkingheadResultFeedSrc = `${getApiUrl()}/api/talkinghead/result_feed`;
606- $('#expression-holder').css({ display: '' });
607- if (imgElement.src !== talkingheadResultFeedSrc) {
608- const expressionImageElement = document.querySelector('.expression_list_image');
609-
610- if (expressionImageElement && expressionImageElement instanceof HTMLImageElement) {
611- doExtrasFetch(expressionImageElement.src, {
612- method: 'HEAD',
613- })
614- .then(response => {
615- if (response.ok) {
616- imgElement.src = talkingheadResultFeedSrc;
617- }
618- })
619- .catch(error => {
620- console.error(error);
621- });
622- }
623- }
624- } else {
625- imgElement.src = ''; // remove in case char doesn't have expressions
626-
627- // When switching Talkinghead off, force-set the character to the last known expression, if any.
628- // This preserves the same expression Talkinghead had at the moment it was switched off.
629- const charName = getContext().name2;
630- const last = lastExpression[charName];
631- const targetExpression = last ? last : getFallbackExpression();
632- setExpression(charName, targetExpression, true);
633- }
634-}
635-
636434async function moduleWorker() {
637435 const context = getContext();
638436
639- // Hide and disable Talkinghead while not in extras
640- $('#image_type_block').toggle(extension_settings.expressions.api == EXPRESSION_API.extras);
641-
642- if (extension_settings.expressions.api != EXPRESSION_API.extras && extension_settings.expressions.talkinghead) {
643- $('#image_type_toggle').prop('checked', false);
644- setTalkingHeadState(false);
645- }
646-
647437 // non-characters not supported
648438 if (!context.groupId && context.characterId === undefined) {
649439 removeExpression();
@@ -773,91 +563,6 @@ async function moduleWorker() {
773563 }
774564}
775565
776-/**
777- * Starts/stops Talkinghead talking animation.
778- *
779- * Talking starts only when all the following conditions are met:
780- * - The LLM is currently streaming its output.
781- * - The AI's current last message is non-empty, and also not just '...' (as produced by a swipe).
782- * - The AI's current last message has changed from what we saw during the previous call.
783- *
784- * In all other cases, talking stops.
785- *
786- * A Talkinghead API call is made only when the talking state changes.
787- *
788- * Note that also the TTS system, if enabled, starts/stops the Talkinghead talking animation.
789- * See `talkingAnimation` in `SillyTavern/public/scripts/extensions/tts/index.js`.
790- */
791-async function updateTalkingState() {
792- // Don't bother if Talkinghead is disabled or not loaded.
793- if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
794- return;
795- }
796-
797- const context = getContext();
798- const currentLastMessage = getLastCharacterMessage();
799-
800- try {
801- // TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker`
802- // (that similarly checks the streaming processor state) does that for some reason.
803- // Talkinghead isn't currently designed to work with groups.
804- const lastMessageChanged = !((lastCharacter === context.characterId || lastCharacter === context.groupId) && lastTalkingStateMessage === currentLastMessage.mes);
805- const url = new URL(getApiUrl());
806- let newTalkingState;
807- if (context.streamingProcessor && !context.streamingProcessor.isFinished &&
808- currentLastMessage.mes.length !== 0 && currentLastMessage.mes !== '...' && lastMessageChanged) {
809- url.pathname = '/api/talkinghead/start_talking';
810- newTalkingState = true;
811- } else {
812- url.pathname = '/api/talkinghead/stop_talking';
813- newTalkingState = false;
814- }
815- try {
816- // Call the Talkinghead API only if the talking state changed.
817- if (newTalkingState !== lastTalkingState) {
818- console.debug(`updateTalkingState: calling ${url.pathname}`);
819- await doExtrasFetch(url);
820- }
821- }
822- catch (error) {
823- // it's ok if not supported
824- }
825- finally {
826- lastTalkingState = newTalkingState;
827- }
828- }
829- catch (error) {
830- // console.log(error);
831- }
832- finally {
833- lastTalkingStateMessage = currentLastMessage.mes;
834- }
835-}
836-
837-/**
838- * Checks whether the current character has a talkinghead image available.
839- * @returns {Promise<boolean>} True if the character has a talkinghead image available, false otherwise.
840- */
841-async function isTalkingHeadAvailable() {
842- let spriteFolderName = getSpriteFolderName();
843-
844- try {
845- await validateImages(spriteFolderName);
846-
847- let talkingheadObj = spriteCache[spriteFolderName].find(obj => obj.label === 'talkinghead');
848- let talkingheadPath = talkingheadObj ? talkingheadObj.path : null;
849-
850- if (talkingheadPath != null) {
851- return true;
852- } else {
853- await unloadTalkingHead();
854- return false;
855- }
856- } catch (err) {
857- return err;
858- }
859-}
860-
861566function getSpriteFolderName(characterMessage = null, characterName = null) {
862567 const context = getContext();
863568 let spriteFolderName = characterName ?? context.name2;
@@ -872,33 +577,6 @@ function getSpriteFolderName(characterMessage = null, characterName = null) {
872577 return spriteFolderName;
873578}
874579
875-function setTalkingHeadState(newState) {
876- console.debug(`expressions: New talkinghead state: ${newState}`);
877- extension_settings.expressions.talkinghead = newState; // Store setting
878- saveSettingsDebounced();
879-
880- if ([EXPRESSION_API.local, EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) {
881- return;
882- }
883-
884- isTalkingHeadAvailable().then(result => {
885- if (result) {
886- //console.log("talkinghead exists!");
887-
888- if (extension_settings.expressions.talkinghead) {
889- loadTalkingHead();
890- } else {
891- unloadTalkingHead();
892- }
893- handleImageChange(); // Change image as needed
894-
895-
896- } else {
897- //console.log("talkinghead does not exist.");
898- }
899- });
900-}
901-
902580function getFolderNameByMessage(message) {
903581 const context = getContext();
904582 let avatarPath = '';
@@ -976,34 +654,28 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ {
976654}
977655
978656async function setSpriteSlashCommand(_, spriteId) {
657+ spriteId = spriteId.trim().toLowerCase();
979658 if (!spriteId) {
980659 console.log('No sprite id provided');
981660 return '';
982661 }
983662
984663 spriteIdconst spriteFolderName = spriteId.trim().toLowerCasegetSpriteFolderName();
985-
986- // In Talkinghead mode, don't check for the existence of the sprite
987- // (emotion names are the same as for sprites, but it only needs "talkinghead.png").
988- const currentLastMessage = getLastCharacterMessage();
989- const spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
990- let label = spriteId;
991- if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
992- await validateImages(spriteFolderName);
993664
994- // Fuzzy search for sprite
665+ await validateImages(spriteFolderName);
995- const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
996- const results = fuse.search(spriteId);
997- const spriteItem = results[0]?.item;
998666
999- if (!spriteItem) {
667+ // Fuzzy search for sprite
1000- console.log('No sprite found for search term ' + spriteId);
668+ const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
1001- return '';
669+ const results = fuse.search(spriteId);
1002- }
670+ const spriteItem = results[0]?.item;
1003671
1004- label = spriteItem.label;
672+ if (!spriteItem) {
673+ console.log('No sprite found for search term ' + spriteId);
674+ return '';
1005675 }
1006676
677+ const label = spriteItem.label;
678+
1007679 const vnMode = isVisualNovelMode();
1008680 await sendExpressionCall(spriteFolderName, label, true, vnMode);
1009681 return label;
@@ -1190,7 +862,7 @@ function getJsonSchema(emotions) {
1190862function onTextGenSettingsReady(args) {
1191863 // Only call if inside an API call
1192864 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
1193- const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
865+ const emotions = DEFAULT_EXPRESSIONS;
1194866 Object.assign(args, {
1195867 top_k: 1,
1196868 stop: [],
@@ -1581,7 +1253,7 @@ export async function getExpressionsList() {
15811253 }
15821254
15831255 // If there was no specific list, or an error, just return the default expressions
15841256 expressionsList = DEFAULT_EXPRESSIONS.filter(e => e !== 'talkinghead').slice();
15851257 return expressionsList;
15861258 }
15871259
@@ -1597,162 +1269,126 @@ export async function getExpressionsList() {
15971269 * @returns {Promise<void>} A promise that resolves when the expression has been set.
15981270 */
15991271async function setExpression(character, expression, force = false) {
1600- if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
1272+ console.debug('entered setExpressions');
1601- console.debug('entered setExpressions');
1273+ await validateImages(character);
1602- await validateImages(character);
1274+ const img = $('img.expression');
16031275 const imgprevExpressionSrc = $('img.expressionattr('src');
16041276 const prevExpressionSrcexpressionClone = img.attrclone('src');
1605- const expressionClone = img.clone();
1277+
1606-
1278+ /** @type {Expression} */
1607- /** @type {Expression} */
1279+ const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
1608- const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
1280+ console.debug('checking for expression images to show..');
1609- console.debug('checking for expression images to show..');
1281+ if (sprite && sprite.files.length > 0) {
1610- if (sprite && sprite.files.length > 0) {
1282+ console.debug('setting expression from character images folder');
1611- console.debug('setting expression from character images folder');
1283+
1612-
1284+ let spriteFile = sprite.files[0];
1613- let spriteFile = sprite.files[0];
1285+
1614-
1286+ // Calculate next expression, if multiple are allowed
1615- // Calculate next expression, if multiple are allowed
1287+ if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1616- if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1288+ let possibleFiles = sprite.files;
1617- let possibleFiles = sprite.files;
1289+ if (extension_settings.expressions.rerollIfSame) {
1618- if (extension_settings.expressions.rerollIfSame) {
1290+ possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
1619- possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
1620- }
1621- spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
16221291 }
1292+ spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
1293+ }
16231294
16241295 if (force && isVisualNovelMode()) {
16251296 const context = getContext();
16261297 const group = context.groups.find(x => x.id === context.groupId);
16271298
16281299 for (const member of group.members) {
16291300 const groupMember = context.characters.find(x => x.avatar === member);
16301301
16311302 if (!groupMember) {
16321303 continue;
16331304 }
16341305
16351306 if (groupMember.name == character) {
16361307 await setImage($(`.expression-holder[data-avatar="${member}"] img`), spriteFile.imageSrc);
16371308 return;
1638- }
16391309 }
16401310 }
1641- //only swap expressions when necessary
1311+ }
1642- if (prevExpressionSrc !== spriteFile.imageSrc
1312+ //only swap expressions when necessary
1643- && !img.hasClass('expression-animating')) {
1313+ if (prevExpressionSrc !== spriteFile.imageSrc
1644- //clone expression
1314+ && !img.hasClass('expression-animating')) {
1645- expressionClone.addClass('expression-clone');
1315+ //clone expression
1646- //make invisible and remove id to prevent double ids
1316+ expressionClone.addClass('expression-clone');
1647- //must be made invisible to start because they share the same Z-index
1317+ //make invisible and remove id to prevent double ids
1648- expressionClone.attr('id', '').css({ opacity: 0 });
1318+ //must be made invisible to start because they share the same Z-index
1649- //add new sprite path to clone src
1319+ expressionClone.attr('id', '').css({ opacity: 0 });
1650- expressionClone.attr('src', spriteFile.imageSrc);
1320+ //add new sprite path to clone src
1651- //add invisible clone to html
1321+ expressionClone.attr('src', spriteFile.imageSrc);
1652- expressionClone.appendTo($('#expression-holder'));
1322+ //add invisible clone to html
1653-
1323+ expressionClone.appendTo($('#expression-holder'));
1654- const duration = 200;
1655-
1656- //add animation flags to both images
1657- //to prevent multiple expression changes happening simultaneously
1658- img.addClass('expression-animating');
1659-
1660- // Set the parent container's min width and height before running the transition
1661- const imgWidth = img.width();
1662- const imgHeight = img.height();
1663- const expressionHolder = img.parent();
1664- expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
1665- expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
1666-
1667- //position absolute prevent the original from jumping around during transition
1668- img.css('position', 'absolute').width(imgWidth).height(imgHeight);
1669- expressionClone.addClass('expression-animating');
1670- //fade the clone in
1671- expressionClone.css({
1672- opacity: 0,
1673- }).animate({
1674- opacity: 1,
1675- }, duration)
1676- //when finshed fading in clone, fade out the original
1677- .promise().done(function () {
1678- img.animate({
1679- opacity: 0,
1680- }, duration);
1681- //remove old expression
1682- img.remove();
1683- //replace ID so it becomes the new 'original' expression for next change
1684- expressionClone.attr('id', 'expression-image');
1685- expressionClone.removeClass('expression-animating');
1686-
1687- // Reset the expression holder min height and width
1688- expressionHolder.css('min-width', 100);
1689- expressionHolder.css('min-height', 100);
1690- });
16911324
1325+ const duration = 200;
16921326
1693- expressionClone.removeClass('expression-clone');
1327+ //add animation flags to both images
1328+ //to prevent multiple expression changes happening simultaneously
1329+ img.addClass('expression-animating');
16941330
1695- expressionClone.removeClass('default');
1331+ // Set the parent container's min width and height before running the transition
1696- expressionClone.off('error');
1332+ const imgWidth = img.width();
1697- expressionClone.on('error', function () {
1333+ const imgHeight = img.height();
1698- console.debug('Expression image error', spriteFile.imageSrc);
1334+ const expressionHolder = img.parent();
1699- $(this).attr('src', '');
1335+ expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
1700- $(this).off('error');
1336+ expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
1701- if (force && extension_settings.expressions.showDefault) {
1702- setDefault();
1703- }
1704- });
1705- }
1706- }
1707- else {
1708- if (extension_settings.expressions.showDefault) {
1709- setDefault();
1710- }
1711- }
17121337
1713- function setDefault() {
1338+ //position absolute prevent the original from jumping around during transition
1714- console.debug('setting default');
1339+ img.css('position', 'absolute').width(imgWidth).height(imgHeight);
1715- const defImgUrl = `/img/default-expressions/${expression}.png`;
1340+ expressionClone.addClass('expression-animating');
1716- //console.log(defImgUrl);
1341+ //fade the clone in
1717- img.attr('src', defImgUrl);
1342+ expressionClone.css({
1718- img.addClass('default');
1343+ opacity: 0,
1719- }
1344+ }).animate({
1720- document.getElementById('expression-holder').style.display = '';
1345+ opacity: 1,
1346+ }, duration)
1347+ //when finshed fading in clone, fade out the original
1348+ .promise().done(function () {
1349+ img.animate({
1350+ opacity: 0,
1351+ }, duration);
1352+ //remove old expression
1353+ img.remove();
1354+ //replace ID so it becomes the new 'original' expression for next change
1355+ expressionClone.attr('id', 'expression-image');
1356+ expressionClone.removeClass('expression-animating');
17211357
1722- } else {
1358+ // Reset the expression holder min height and width
1723- // Set the Talkinghead emotion to the specified expression
1359+ expressionHolder.css('min-width', 100);
1724- // TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
1360+ expressionHolder.css('min-height', 100);
1725- try {
1726- let result = await isTalkingHeadAvailable();
1727- if (result) {
1728- const url = new URL(getApiUrl());
1729- url.pathname = '/api/talkinghead/set_emotion';
1730- await doExtrasFetch(url, {
1731- method: 'POST',
1732- headers: {
1733- 'Content-Type': 'application/json',
1734- },
1735- body: JSON.stringify({ emotion_name: expression }),
17361361 });
1737- }
1738- }
1739- catch (error) {
1740- // `set_emotion` is not present in old versions, so let it 404.
1741- }
17421362
1743- try {
1363+
1744- // Find the <img> element with id="expression-image" and class="expression"
1364+ expressionClone.removeClass('expression-clone');
1745- const imgElement = document.querySelector('img#expression-image.expression');
1365+
1746- //console.log("searching");
1366+ expressionClone.removeClass('default');
1747- if (imgElement && imgElement instanceof HTMLImageElement) {
1367+ expressionClone.off('error');
1748- //console.log("setting value");
1368+ expressionClone.on('error', function () {
1749- imgElement.src = getApiUrl() + '/api/talkinghead/result_feed';
1369+ console.debug('Expression image error', spriteFile.imageSrc);
1750- }
1370+ $(this).attr('src', '');
1371+ $(this).off('error');
1372+ if (force && extension_settings.expressions.showDefault) {
1373+ setDefault();
1374+ }
1375+ });
17511376 }
1752- catch (error) {
1377+ }
1753- //console.log("The fetch failed!");
1378+ else {
1379+ if (extension_settings.expressions.showDefault) {
1380+ setDefault();
17541381 }
17551382 }
1383+
1384+ function setDefault() {
1385+ console.debug('setting default');
1386+ const defImgUrl = `/img/default-expressions/${expression}.png`;
1387+ //console.log(defImgUrl);
1388+ img.attr('src', defImgUrl);
1389+ img.addClass('default');
1390+ }
1391+ document.getElementById('expression-holder').style.display = '';
17561392}
17571393
17581394function onClickExpressionImage() {
@@ -1976,11 +1612,6 @@ async function onClickExpressionUpload(event) {
19761612
19771613 // Reset the input
19781614 e.target.form.reset();
1979-
1980- // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
1981- if (expression === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1982- await loadTalkingHead();
1983- }
19841615 };
19851616
19861617 $('#expression_upload')
@@ -2091,11 +1722,6 @@ async function onClickExpressionUploadPackButton() {
20911722
20921723 // Reset the input
20931724 e.target.form.reset();
2094-
2095- // In Talkinghead mode, refresh the live char.
2096- if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
2097- await loadTalkingHead();
2098- }
20991725 };
21001726
21011727 $('#expression_upload_pack')
@@ -2254,18 +1880,12 @@ function migrateSettings() {
22541880 $(window).on('resize', updateVisualNovelModeDebounced);
22551881 $('#open_chat_expressions').hide();
22561882
2257- $('#image_type_toggle').on('click', function () {
2258- if (this instanceof HTMLInputElement) {
2259- setTalkingHeadState(this.checked);
2260- }
2261- });
2262-
22631883 await renderAdditionalExpressionSettings();
22641884 $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras);
22651885 $('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api));
22661886 $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? '');
22671887 $('#expression_llm_prompt').on('input', function () {
22681888 extension_settings.expressions.llmPrompt = String($(this).val());
22691889 saveSettingsDebounced();
22701890 });
22711891 $('#expression_llm_prompt_restore').on('click', function () {
@@ -2280,34 +1900,6 @@ function migrateSettings() {
22801900 $('#expression_api').on('change', onExpressionApiChanged);
22811901 }
22821902
2283- // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized.
2284- // We currently do this via loading/unloading. Could be improved by adding new pause/unpause endpoints to Extras.
2285- document.addEventListener('visibilitychange', function (event) {
2286- let pageIsVisible;
2287- if (document.hidden) {
2288- console.debug('expressions: SillyTavern is now hidden');
2289- pageIsVisible = false;
2290- } else {
2291- console.debug('expressions: SillyTavern is now visible');
2292- pageIsVisible = true;
2293- }
2294-
2295- if (isTalkingHeadEnabled() && modules.includes('talkinghead')) {
2296- isTalkingHeadAvailable().then(result => {
2297- if (result) {
2298- if (pageIsVisible) {
2299- loadTalkingHead();
2300- } else {
2301- unloadTalkingHead();
2302- }
2303- handleImageChange(); // Change image as needed
2304- } else {
2305- //console.log("talkinghead does not exist.");
2306- }
2307- });
2308- }
2309- });
2310-
23111903 addExpressionImage();
23121904 addVisualNovelMode();
23131905 migrateSettings();
@@ -2316,11 +1908,6 @@ function migrateSettings() {
23161908 const updateFunction = wrapper.update.bind(wrapper);
23171909 setInterval(updateFunction, UPDATE_INTERVAL);
23181910 moduleWorker();
2319- // For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
2320- const wrapperTalkingState = new ModuleWorkerWrapper(updateTalkingState);
2321- const updateTalkingStateFunction = wrapperTalkingState.update.bind(wrapperTalkingState);
2322- setInterval(updateTalkingStateFunction, TALKINGCHECK_UPDATE_INTERVAL);
2323- updateTalkingState();
23241911 dragElement($('#expression-holder'));
23251912 eventSource.on(event_types.CHAT_CHANGED, () => {
23261913 // character changed
@@ -2334,12 +1921,6 @@ function migrateSettings() {
23341921 imgElement.src = '';
23351922 }
23361923
2337- //set checkbox to global var
2338- $('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead);
2339- if (extension_settings.expressions.talkinghead) {
2340- setTalkingHeadState(extension_settings.expressions.talkinghead);
2341- }
2342-
23431924 setExpressionOverrideHtml();
23441925
23451926 if (isVisualNovelMode()) {
@@ -2350,11 +1931,6 @@ function migrateSettings() {
23501931 });
23511932 eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
23521933 eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
2353- eventSource.on(event_types.EXTRAS_CONNECTED, () => {
2354- if (extension_settings.expressions.talkinghead) {
2355- setTalkingHeadState(extension_settings.expressions.talkinghead);
2356- }
2357- });
23581934
23591935 const localEnumProviders = {
23601936 expressions: () => getCachedExpressions().map(expression => {
@@ -2421,13 +1997,6 @@ function migrateSettings() {
24211997 helpString: 'Returns the last set expression for the named character.',
24221998 }));
24231999 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2424- name: 'expression-talkinghead',
2425- callback: toggleTalkingHeadCommand,
2426- aliases: ['th', 'talkinghead'],
2427- helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
2428- returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.',
2429- }));
2430- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
24312000 name: 'expression-classify',
24322001 aliases: ['classify-expressions', 'expressions'],
24332002 /** @type {(args: {return: string}) => Promise<string>} */
public/scripts/extensions/expressions/settings.html+0 -4
@@ -22,10 +22,6 @@
2222 <input id="expressions_reroll_if_same" type="checkbox">
2323 <span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
2424 </label>
25- <label id="image_type_block" class="checkbox_label" for="image_type_toggle">
26- <input id="image_type_toggle" type="checkbox">
27- <span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
28- </label>
2925 <div class="expression_api_block m-b-1 m-t-1">
3026 <label for="expression_api" data-i18n="Classifier API">Classifier API</label>
3127 <small data-i18n="Select the API for classifying expressions.">Select the API for classifying expressions.</small>
public/scripts/extensions/tts/index.js+0 -26
@@ -27,13 +27,11 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
2727import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
2828import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
2929import { GoogleTranslateTtsProvider } from './google-translate.js';
30-export { talkingAnimation };
3130
3231const UPDATE_INTERVAL = 1000;
3332
3433let voiceMapEntries = [];
3534let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
36-let talkingHeadState = false;
3735let lastChatId = null;
3836let lastMessage = null;
3937let lastMessageHash = null;
@@ -165,27 +163,6 @@ async function moduleWorker() {
165163 updateUiAudioPlayState();
166164}
167165
168-function talkingAnimation(switchValue) {
169- if (!modules.includes('talkinghead')) {
170- console.debug('Talking Animation module not loaded');
171- return;
172- }
173-
174- const apiUrl = getApiUrl();
175- const animationType = switchValue ? 'start' : 'stop';
176-
177- if (switchValue !== talkingHeadState) {
178- try {
179- console.log(animationType + ' Talking Animation');
180- doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
181- talkingHeadState = switchValue;
182- } catch (error) {
183- // Handle the error here or simply ignore it to prevent logging
184- }
185- }
186- updateUiAudioPlayState();
187-}
188-
189166function resetTtsPlayback() {
190167 // Stop system TTS utterance
191168 cancelTtsPlay();
@@ -347,7 +324,6 @@ function onAudioControlClicked() {
347324 // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
348325 if (!audioElement.paused || isTtsProcessing()) {
349326 resetTtsPlayback();
350- talkingAnimation(false);
351327 } else {
352328 // Default play behavior if not processing or playing is to play the last message.
353329 ttsJobQueue.push(context.chat[context.chat.length - 1]);
@@ -374,7 +350,6 @@ function addAudioControl() {
374350function completeCurrentAudioJob() {
375351 audioQueueProcessorReady = true;
376352 currentAudioJob = null;
377- talkingAnimation(false); //stop lip animation
378353 // updateUiPlayState();
379354}
380355
@@ -404,7 +379,6 @@ async function processAudioJobQueue() {
404379 audioQueueProcessorReady = false;
405380 currentAudioJob = audioJobQueue.shift();
406381 playAudioData(currentAudioJob);
407- talkingAnimation(true);
408382 } catch (error) {
409383 toastr.error(error.toString());
410384 console.error(error);
public/scripts/extensions/tts/system.js+0 -3
@@ -1,6 +1,5 @@
11import { isMobile } from '../../RossAscends-mods.js';
22import { getPreviewString } from './index.js';
3-import { talkingAnimation } from './index.js';
43import { saveTtsProviderSettings } from './index.js';
54export { SystemTtsProvider };
65
@@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
7069 //placing the speak invocation inside a callback fixes ordering and onend issues.
7170 setTimeout(function () {
7271 speechSynthesis.speak(newUtt);
73- talkingAnimation(true);
7472 }, 0);
7573};
7674
@@ -240,7 +238,6 @@ class SystemTtsProvider {
240238 //some code to execute when done
241239 resolve(silence);
242240 console.log('System TTS done');
243- talkingAnimation(false);
244241 });
245242 });
246243 }