2024-08-04 12:24:24 +02:00
import { callPopup , eventSource , event _types , generateRaw , getRequestHeaders , main _api , online _status , saveSettingsDebounced , substituteParams , substituteParamsExtended , system _message _types } from '../../../script.js' ;
2023-12-02 19:04:51 +01:00
import { dragElement , isMobile } from '../../RossAscends-mods.js' ;
2024-04-11 22:38:44 +02:00
import { getContext , getApiUrl , modules , extension _settings , ModuleWorkerWrapper , doExtrasFetch , renderExtensionTemplateAsync } from '../../extensions.js' ;
2023-12-02 19:04:51 +01:00
import { loadMovingUIState , power _user } from '../../power-user.js' ;
2024-05-25 14:38:32 +02:00
import { onlyUnique , debounce , getCharaFilename , trimToEndSentence , trimToStartSentence , waitUntilCondition } from '../../utils.js' ;
2023-12-02 19:04:51 +01:00
import { hideMutedSprites } from '../../group-chats.js' ;
2024-04-14 16:13:54 +02:00
import { isJsonSchemaSupported } from '../../textgen-settings.js' ;
2024-04-28 18:47:53 +02:00
import { debounce _timeout } from '../../constants.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js' ;
import { SlashCommand } from '../../slash-commands/SlashCommand.js' ;
2024-09-05 00:06:14 +02:00
import { ARGUMENT _TYPE , SlashCommandArgument , SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js' ;
2024-05-25 14:38:32 +02:00
import { isFunctionCallingSupported } from '../../openai.js' ;
2024-06-21 20:04:55 +02:00
import { SlashCommandEnumValue , enumTypes } from '../../slash-commands/SlashCommandEnumValue.js' ;
2024-06-17 07:04:10 +02:00
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js' ;
2023-07-20 19:32:15 +02:00
export { MODULE _NAME } ;
const MODULE _NAME = 'expressions' ;
const UPDATE _INTERVAL = 2000 ;
2024-04-14 16:13:54 +02:00
const STREAMING _UPDATE _INTERVAL = 10000 ;
2024-01-09 19:17:17 +01:00
const TALKINGCHECK _UPDATE _INTERVAL = 500 ;
2024-04-07 06:12:52 +02:00
const DEFAULT _FALLBACK _EXPRESSION = 'joy' ;
2024-05-25 14:38:32 +02:00
const FUNCTION _NAME = 'set_emotion' ;
2024-09-22 11:58:46 +02:00
const 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}}' ;
2023-07-20 19:32:15 +02:00
const DEFAULT _EXPRESSIONS = [
2023-12-02 19:04:51 +01:00
'talkinghead' ,
'admiration' ,
'amusement' ,
'anger' ,
'annoyance' ,
'approval' ,
'caring' ,
'confusion' ,
'curiosity' ,
'desire' ,
'disappointment' ,
'disapproval' ,
'disgust' ,
'embarrassment' ,
'excitement' ,
'fear' ,
'gratitude' ,
'grief' ,
'joy' ,
'love' ,
'nervousness' ,
'optimism' ,
'pride' ,
'realization' ,
'relief' ,
'remorse' ,
'sadness' ,
'surprise' ,
2023-12-02 21:06:57 +01:00
'neutral' ,
2023-07-20 19:32:15 +02:00
] ;
2024-09-05 00:06:14 +02:00
/** @enum {number} */
2024-04-12 07:49:09 +02:00
const EXPRESSION _API = {
local : 0 ,
extras : 1 ,
llm : 2 ,
2024-04-14 14:40:10 +02:00
} ;
2023-07-20 19:32:15 +02:00
let expressionsList = null ;
let lastCharacter = undefined ;
let lastMessage = null ;
2024-01-07 23:47:59 +01:00
let lastTalkingState = false ;
let lastTalkingStateMessage = null ; // last message as seen by `updateTalkingState` (tracked separately, different timer)
2023-07-20 19:32:15 +02:00
let spriteCache = { } ;
let inApiCall = false ;
2023-09-10 19:14:57 +02:00
let lastServerResponseTime = 0 ;
2023-12-30 12:35:10 +01:00
export let lastExpression = { } ;
2023-07-20 19:32:15 +02:00
2024-01-07 20:14:29 +01:00
function isTalkingHeadEnabled ( ) {
2024-04-12 07:49:09 +02:00
return extension _settings . expressions . talkinghead && extension _settings . expressions . api == EXPRESSION _API . extras ;
2024-01-07 20:14:29 +01:00
}
2024-04-07 06:12:52 +02:00
/ * *
* Returns the fallback expression if explicitly chosen , otherwise the default one
* @ returns { string } expression name
* /
function getFallbackExpression ( ) {
return extension _settings . expressions . fallback _expression ? ? DEFAULT _FALLBACK _EXPRESSION ;
}
2024-02-04 23:46:23 +01:00
/ * *
* 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 ) ;
2024-06-15 17:21:44 +02:00
return String ( extension _settings . expressions . talkinghead ) ;
2024-02-04 23:46:23 +01:00
}
2023-07-20 19:32:15 +02:00
function isVisualNovelMode ( ) {
return Boolean ( ! isMobile ( ) && power _user . waifuMode && getContext ( ) . groupId ) ;
}
async function forceUpdateVisualNovelMode ( ) {
if ( isVisualNovelMode ( ) ) {
await updateVisualNovelMode ( ) ;
}
}
2024-04-28 06:21:47 +02:00
const updateVisualNovelModeDebounced = debounce ( forceUpdateVisualNovelMode , debounce _timeout . quick ) ;
2023-07-20 19:32:15 +02:00
async function updateVisualNovelMode ( name , expression ) {
const container = $ ( '#visual-novel-wrapper' ) ;
await visualNovelRemoveInactive ( container ) ;
const setSpritePromises = await visualNovelSetCharacterSprites ( container , name , expression ) ;
// calculate layer indices based on recent messages
await visualNovelUpdateLayers ( container ) ;
await Promise . allSettled ( setSpritePromises ) ;
// update again based on new sprites
if ( setSpritePromises . length > 0 ) {
await visualNovelUpdateLayers ( container ) ;
}
}
async function visualNovelRemoveInactive ( container ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const removeInactiveCharactersPromises = [ ] ;
// remove inactive characters after 1 second
container . find ( '.expression-holder' ) . each ( ( _ , current ) => {
const promise = new Promise ( resolve => {
const element = $ ( current ) ;
const avatar = element . data ( 'avatar' ) ;
if ( ! group . members . includes ( avatar ) || group . disabled _members . includes ( avatar ) ) {
element . fadeOut ( 250 , ( ) => {
element . remove ( ) ;
resolve ( ) ;
} ) ;
} else {
resolve ( ) ;
}
} ) ;
removeInactiveCharactersPromises . push ( promise ) ;
} ) ;
await Promise . allSettled ( removeInactiveCharactersPromises ) ;
}
async function visualNovelSetCharacterSprites ( container , name , expression ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const labels = await getExpressionsList ( ) ;
const createCharacterPromises = [ ] ;
const setSpritePromises = [ ] ;
for ( const avatar of group . members ) {
const isDisabled = group . disabled _members . includes ( avatar ) ;
// skip disabled characters
2023-11-21 23:48:35 +01:00
if ( isDisabled && hideMutedSprites ) {
2023-07-20 19:32:15 +02:00
continue ;
}
const character = context . characters . find ( x => x . avatar == avatar ) ;
if ( ! character ) {
continue ;
}
2023-09-07 16:48:12 +02:00
const spriteFolderName = getSpriteFolderName ( { original _avatar : character . avatar } , character . name ) ;
2023-07-20 19:32:15 +02:00
// download images if not downloaded yet
if ( spriteCache [ spriteFolderName ] === undefined ) {
spriteCache [ spriteFolderName ] = await getSpritesList ( spriteFolderName ) ;
}
const sprites = spriteCache [ spriteFolderName ] ;
const expressionImage = container . find ( ` .expression-holder[data-avatar=" ${ avatar } "] ` ) ;
2024-04-07 06:12:52 +02:00
const defaultExpression = getFallbackExpression ( ) ;
const defaultSpritePath = sprites . find ( x => x . label === defaultExpression ) ? . path ;
2023-07-20 19:32:15 +02:00
const noSprites = sprites . length === 0 ;
if ( expressionImage . length > 0 ) {
if ( name == spriteFolderName ) {
await validateImages ( spriteFolderName , true ) ;
setExpressionOverrideHtml ( true ) ; // <= force clear expression override input
const currentSpritePath = labels . includes ( expression ) ? sprites . find ( x => x . label === expression ) ? . path : '' ;
const path = currentSpritePath || defaultSpritePath || '' ;
const img = expressionImage . find ( 'img' ) ;
await setImage ( img , path ) ;
}
expressionImage . toggleClass ( 'hidden' , noSprites ) ;
} else {
const template = $ ( '#expression-holder' ) . clone ( ) ;
template . attr ( 'id' , ` expression- ${ avatar } ` ) ;
template . attr ( 'data-avatar' , avatar ) ;
template . find ( '.drag-grabber' ) . attr ( 'id' , ` expression- ${ avatar } header ` ) ;
$ ( '#visual-novel-wrapper' ) . append ( template ) ;
dragElement ( $ ( template [ 0 ] ) ) ;
template . toggleClass ( 'hidden' , noSprites ) ;
await setImage ( template . find ( 'img' ) , defaultSpritePath || '' ) ;
const fadeInPromise = new Promise ( resolve => {
template . fadeIn ( 250 , ( ) => resolve ( ) ) ;
} ) ;
createCharacterPromises . push ( fadeInPromise ) ;
const setSpritePromise = setLastMessageSprite ( template . find ( 'img' ) , avatar , labels ) ;
setSpritePromises . push ( setSpritePromise ) ;
}
}
await Promise . allSettled ( createCharacterPromises ) ;
return setSpritePromises ;
}
async function visualNovelUpdateLayers ( container ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const recentMessages = context . chat . map ( x => x . original _avatar ) . filter ( x => x ) . reverse ( ) . filter ( onlyUnique ) ;
const filteredMembers = group . members . filter ( x => ! group . disabled _members . includes ( x ) ) ;
const layerIndices = filteredMembers . slice ( ) . sort ( ( a , b ) => {
const aRecentIndex = recentMessages . indexOf ( a ) ;
const bRecentIndex = recentMessages . indexOf ( b ) ;
const aFilteredIndex = filteredMembers . indexOf ( a ) ;
const bFilteredIndex = filteredMembers . indexOf ( b ) ;
if ( aRecentIndex !== - 1 && bRecentIndex !== - 1 ) {
return bRecentIndex - aRecentIndex ;
} else if ( aRecentIndex !== - 1 ) {
return 1 ;
} else if ( bRecentIndex !== - 1 ) {
return - 1 ;
} else {
return aFilteredIndex - bFilteredIndex ;
}
} ) ;
const setLayerIndicesPromises = [ ] ;
const sortFunction = ( a , b ) => {
const avatarA = $ ( a ) . data ( 'avatar' ) ;
const avatarB = $ ( b ) . data ( 'avatar' ) ;
const indexA = filteredMembers . indexOf ( avatarA ) ;
const indexB = filteredMembers . indexOf ( avatarB ) ;
return indexA - indexB ;
} ;
const containerWidth = container . width ( ) ;
const pivotalPoint = containerWidth * 0.5 ;
2023-11-28 10:40:39 +01:00
let images = $ ( '#visual-novel-wrapper .expression-holder' ) ;
2023-07-20 19:32:15 +02:00
let imagesWidth = [ ] ;
images . sort ( sortFunction ) . each ( function ( ) {
imagesWidth . push ( $ ( this ) . width ( ) ) ;
} ) ;
let totalWidth = imagesWidth . reduce ( ( a , b ) => a + b , 0 ) ;
let currentPosition = pivotalPoint - ( totalWidth / 2 ) ;
if ( totalWidth > containerWidth ) {
let totalOverlap = totalWidth - containerWidth ;
let totalWidthWithoutWidest = imagesWidth . reduce ( ( a , b ) => a + b , 0 ) - Math . max ( ... imagesWidth ) ;
let overlaps = imagesWidth . map ( width => ( width / totalWidthWithoutWidest ) * totalOverlap ) ;
imagesWidth = imagesWidth . map ( ( width , index ) => width - overlaps [ index ] ) ;
currentPosition = 0 ; // Reset the initial position to 0
}
images . sort ( sortFunction ) . each ( ( index , current ) => {
const element = $ ( current ) ;
2023-12-02 20:11:06 +01:00
const elementID = element . attr ( 'id' ) ;
2023-07-20 19:32:15 +02:00
// skip repositioning of dragged elements
if ( element . data ( 'dragged' )
|| ( power _user . movingUIState [ elementID ]
&& ( typeof power _user . movingUIState [ elementID ] === 'object' )
&& Object . keys ( power _user . movingUIState [ elementID ] ) . length > 0 ) ) {
2023-12-02 20:11:06 +01:00
loadMovingUIState ( ) ;
2023-07-20 19:32:15 +02:00
//currentPosition += imagesWidth[index];
return ;
}
const avatar = element . data ( 'avatar' ) ;
const layerIndex = layerIndices . indexOf ( avatar ) ;
element . css ( 'z-index' , layerIndex ) ;
element . show ( ) ;
const promise = new Promise ( resolve => {
element . animate ( { left : currentPosition + 'px' } , 500 , ( ) => {
resolve ( ) ;
} ) ;
} ) ;
currentPosition += imagesWidth [ index ] ;
setLayerIndicesPromises . push ( promise ) ;
} ) ;
await Promise . allSettled ( setLayerIndicesPromises ) ;
}
async function setLastMessageSprite ( img , avatar , labels ) {
const context = getContext ( ) ;
const lastMessage = context . chat . slice ( ) . reverse ( ) . find ( x => x . original _avatar == avatar || ( x . force _avatar && x . force _avatar . includes ( encodeURIComponent ( avatar ) ) ) ) ;
if ( lastMessage ) {
const text = lastMessage . mes || '' ;
2023-09-07 16:52:37 +02:00
const spriteFolderName = getSpriteFolderName ( lastMessage , lastMessage . name ) ;
2023-07-20 19:32:15 +02:00
const sprites = spriteCache [ spriteFolderName ] || [ ] ;
const label = await getExpressionLabel ( text ) ;
const path = labels . includes ( label ) ? sprites . find ( x => x . label === label ) ? . path : '' ;
if ( path ) {
setImage ( img , path ) ;
}
}
}
async function setImage ( img , path ) {
// Cohee: If something goes wrong, uncomment this to return to the old behavior
/ *
img . attr ( 'src' , path ) ;
img . removeClass ( 'default' ) ;
img . off ( 'error' ) ;
img . on ( 'error' , function ( ) {
console . debug ( 'Error loading image' , path ) ;
$ ( this ) . off ( 'error' ) ;
$ ( this ) . attr ( 'src' , '' ) ;
} ) ;
* /
return new Promise ( resolve => {
const prevExpressionSrc = img . attr ( 'src' ) ;
const expressionClone = img . clone ( ) ;
const originalId = img . attr ( 'id' ) ;
//only swap expressions when necessary
if ( prevExpressionSrc !== path && ! img . hasClass ( 'expression-animating' ) ) {
//clone expression
2023-12-02 20:11:06 +01:00
expressionClone . addClass ( 'expression-clone' ) ;
2023-07-20 19:32:15 +02:00
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone . attr ( 'id' , '' ) . css ( { opacity : 0 } ) ;
//add new sprite path to clone src
expressionClone . attr ( 'src' , path ) ;
//add invisible clone to html
expressionClone . appendTo ( img . parent ( ) ) ;
const duration = 200 ;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img . addClass ( 'expression-animating' ) ;
// Set the parent container's min width and height before running the transition
const imgWidth = img . width ( ) ;
const imgHeight = img . height ( ) ;
const expressionHolder = img . parent ( ) ;
expressionHolder . css ( 'min-width' , imgWidth > 100 ? imgWidth : 100 ) ;
expressionHolder . css ( 'min-height' , imgHeight > 100 ? imgHeight : 100 ) ;
//position absolute prevent the original from jumping around during transition
2023-08-25 01:07:59 +02:00
img . css ( 'position' , 'absolute' ) . width ( imgWidth ) . height ( imgHeight ) ;
2023-07-20 19:32:15 +02:00
expressionClone . addClass ( 'expression-animating' ) ;
//fade the clone in
expressionClone . css ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-20 19:32:15 +02:00
} ) . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 1 ,
2023-07-20 19:32:15 +02:00
} , duration )
//when finshed fading in clone, fade out the original
. promise ( ) . done ( function ( ) {
img . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-20 19:32:15 +02:00
} , duration ) ;
//remove old expression
img . remove ( ) ;
//replace ID so it becomes the new 'original' expression for next change
expressionClone . attr ( 'id' , originalId ) ;
expressionClone . removeClass ( 'expression-animating' ) ;
// Reset the expression holder min height and width
expressionHolder . css ( 'min-width' , 100 ) ;
expressionHolder . css ( 'min-height' , 100 ) ;
resolve ( ) ;
} ) ;
expressionClone . removeClass ( 'expression-clone' ) ;
expressionClone . removeClass ( 'default' ) ;
expressionClone . off ( 'error' ) ;
expressionClone . on ( 'error' , function ( ) {
2023-09-07 16:48:12 +02:00
console . debug ( 'Expression image error' , path ) ;
2023-07-20 19:32:15 +02:00
$ ( this ) . attr ( 'src' , '' ) ;
$ ( this ) . off ( 'error' ) ;
resolve ( ) ;
} ) ;
} else {
resolve ( ) ;
}
} ) ;
}
function onExpressionsShowDefaultInput ( ) {
const value = $ ( this ) . prop ( 'checked' ) ;
extension _settings . expressions . showDefault = value ;
saveSettingsDebounced ( ) ;
const existingImageSrc = $ ( 'img.expression' ) . prop ( 'src' ) ;
if ( existingImageSrc !== undefined ) { //if we have an image in src
if ( ! value && existingImageSrc . includes ( '/img/default-expressions/' ) ) { //and that image is from /img/ (default)
$ ( 'img.expression' ) . prop ( 'src' , '' ) ; //remove it
lastMessage = null ;
}
if ( value ) {
lastMessage = null ;
}
}
}
2024-01-07 23:46:54 +01:00
/ * *
2024-02-04 23:44:26 +01:00
* Stops animating Talkinghead .
2024-01-07 23:46:54 +01:00
* /
2024-01-07 20:14:29 +01:00
async function unloadTalkingHead ( ) {
2023-08-29 20:50:26 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( 'talkinghead module is disabled' ) ;
return ;
}
2024-02-04 23:40:03 +01:00
console . debug ( 'expressions: Stopping Talkinghead' ) ;
2023-08-29 20:50:26 +02:00
2023-08-04 01:29:39 +02:00
try {
const url = new URL ( getApiUrl ( ) ) ;
2023-08-10 23:52:14 +02:00
url . pathname = '/api/talkinghead/unload' ;
2023-08-04 02:27:01 +02:00
const loadResponse = await doExtrasFetch ( url ) ;
2023-08-04 01:29:39 +02:00
if ( ! loadResponse . ok ) {
throw new Error ( loadResponse . statusText ) ;
}
2023-08-04 07:54:01 +02:00
//console.log(`Response: ${loadResponseText}`);
2023-08-04 01:29:39 +02:00
} catch ( error ) {
2023-08-04 07:54:01 +02:00
//console.error(`Error unloading - ${error}`);
2023-08-04 01:29:39 +02:00
}
}
2024-01-07 23:46:54 +01:00
/ * *
* Posts ` talkinghead.png ` of the current character to the talkinghead module in SillyTavern - extras , to start animating it .
* /
2024-01-07 20:14:29 +01:00
async function loadTalkingHead ( ) {
2023-08-10 23:52:14 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( 'talkinghead module is disabled' ) ;
2023-08-01 15:33:30 +02:00
return ;
}
2024-02-04 23:40:03 +01:00
console . debug ( 'expressions: Starting Talkinghead' ) ;
2023-08-01 15:33:30 +02:00
2023-09-07 16:48:12 +02:00
const spriteFolderName = getSpriteFolderName ( ) ;
2023-08-01 22:57:04 +02:00
2023-08-10 23:52:14 +02:00
const talkingheadPath = ` /characters/ ${ encodeURIComponent ( spriteFolderName ) } /talkinghead.png ` ;
2024-01-12 17:26:14 +01:00
const emotionsSettingsPath = ` /characters/ ${ encodeURIComponent ( spriteFolderName ) } /_emotions.json ` ;
const animatorSettingsPath = ` /characters/ ${ encodeURIComponent ( spriteFolderName ) } /_animator.json ` ;
2023-08-01 22:57:04 +02:00
try {
2023-08-10 23:52:14 +02:00
const spriteResponse = await fetch ( talkingheadPath ) ;
2023-08-01 22:57:04 +02:00
if ( ! spriteResponse . ok ) {
throw new Error ( spriteResponse . statusText ) ;
}
const spriteBlob = await spriteResponse . blob ( ) ;
2023-08-10 23:52:14 +02:00
const spriteFile = new File ( [ spriteBlob ] , 'talkinghead.png' , { type : 'image/png' } ) ;
2023-08-01 22:57:04 +02:00
const formData = new FormData ( ) ;
formData . append ( 'file' , spriteFile ) ;
const url = new URL ( getApiUrl ( ) ) ;
2023-08-10 23:52:14 +02:00
url . pathname = '/api/talkinghead/load' ;
2023-08-01 22:57:04 +02:00
const loadResponse = await doExtrasFetch ( url , {
method : 'POST' ,
body : formData ,
} ) ;
if ( ! loadResponse . ok ) {
throw new Error ( loadResponse . statusText ) ;
}
const loadResponseText = await loadResponse . text ( ) ;
2023-08-10 23:52:14 +02:00
console . log ( ` Load talkinghead response: ${ loadResponseText } ` ) ;
2023-08-01 22:57:04 +02:00
2024-01-12 17:26:14 +01:00
// Optional: per-character emotion templates
let emotionsSettings ;
try {
const emotionsResponse = await fetch ( emotionsSettingsPath ) ;
if ( emotionsResponse . ok ) {
emotionsSettings = await emotionsResponse . json ( ) ;
console . log ( ` Loaded ${ emotionsSettingsPath } ` ) ;
} else {
throw new Error ( ) ;
}
}
catch ( error ) {
emotionsSettings = { } ; // blank -> use server defaults (to unload the previous character's customizations)
console . log ( ` No valid config at ${ emotionsSettingsPath } , using server defaults ` ) ;
}
try {
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/talkinghead/load_emotion_templates' ;
const apiResult = await doExtrasFetch ( url , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Bypass-Tunnel-Reminder' : 'bypass' ,
} ,
body : JSON . stringify ( emotionsSettings ) ,
} ) ;
2024-04-14 23:39:15 +02:00
if ( ! apiResult . ok ) {
throw new Error ( apiResult . statusText ) ;
}
2024-01-12 17:26:14 +01:00
}
catch ( error ) {
// it's ok if not supported
console . log ( 'Failed to send _emotions.json (backend too old?), ignoring' ) ;
}
// Optional: per-character animator and postprocessor config
let animatorSettings ;
try {
const animatorResponse = await fetch ( animatorSettingsPath ) ;
if ( animatorResponse . ok ) {
animatorSettings = await animatorResponse . json ( ) ;
console . log ( ` Loaded ${ animatorSettingsPath } ` ) ;
} else {
throw new Error ( ) ;
}
}
catch ( error ) {
animatorSettings = { } ; // blank -> use server defaults (to unload the previous character's customizations)
console . log ( ` No valid config at ${ animatorSettingsPath } , using server defaults ` ) ;
}
try {
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/talkinghead/load_animator_settings' ;
const apiResult = await doExtrasFetch ( url , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Bypass-Tunnel-Reminder' : 'bypass' ,
} ,
body : JSON . stringify ( animatorSettings ) ,
} ) ;
2024-04-14 23:39:15 +02:00
if ( ! apiResult . ok ) {
throw new Error ( apiResult . statusText ) ;
}
2024-01-12 17:26:14 +01:00
}
catch ( error ) {
// it's ok if not supported
console . log ( 'Failed to send _animator.json (backend too old?), ignoring' ) ;
}
2023-08-01 22:57:04 +02:00
} catch ( error ) {
2023-08-10 23:52:14 +02:00
console . error ( ` Error loading talkinghead image: ${ talkingheadPath } - ${ error } ` ) ;
2023-08-01 22:57:04 +02:00
}
2023-07-31 07:52:30 +02:00
}
2023-08-03 12:05:21 +02:00
function handleImageChange ( ) {
2023-07-31 07:52:30 +02:00
const imgElement = document . querySelector ( 'img#expression-image.expression' ) ;
2023-08-03 13:07:50 +02:00
2023-09-07 16:48:12 +02:00
if ( ! imgElement || ! ( imgElement instanceof HTMLImageElement ) ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Cannot find addExpressionImage()' ) ;
2023-07-31 07:52:30 +02:00
return ;
}
2024-02-04 23:45:20 +01:00
if ( isTalkingHeadEnabled ( ) && modules . includes ( 'talkinghead' ) ) {
2023-08-10 23:52:14 +02:00
const talkingheadResultFeedSrc = ` ${ getApiUrl ( ) } /api/talkinghead/result_feed ` ;
2023-08-03 13:07:50 +02:00
$ ( '#expression-holder' ) . css ( { display : '' } ) ;
2023-08-10 23:52:14 +02:00
if ( imgElement . src !== talkingheadResultFeedSrc ) {
2023-08-03 12:05:21 +02:00
const expressionImageElement = document . querySelector ( '.expression_list_image' ) ;
2023-09-07 16:48:12 +02:00
if ( expressionImageElement && expressionImageElement instanceof HTMLImageElement ) {
2023-08-03 12:05:21 +02:00
doExtrasFetch ( expressionImageElement . src , {
method : 'HEAD' ,
2023-07-31 19:56:05 +02:00
} )
2023-08-03 12:05:21 +02:00
. then ( response => {
if ( response . ok ) {
2023-08-10 23:52:14 +02:00
imgElement . src = talkingheadResultFeedSrc ;
2023-08-03 12:05:21 +02:00
}
} )
. catch ( error => {
2024-02-04 23:45:37 +01:00
console . error ( error ) ;
2023-08-03 12:05:21 +02:00
} ) ;
}
2023-07-31 07:52:30 +02:00
}
2023-08-03 12:05:21 +02:00
} else {
2024-02-04 23:47:14 +01:00
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 ] ;
2024-04-07 06:12:52 +02:00
const targetExpression = last ? last : getFallbackExpression ( ) ;
2024-02-04 23:47:14 +01:00
setExpression ( charName , targetExpression , true ) ;
2023-07-31 07:52:30 +02:00
}
}
2023-07-20 19:32:15 +02:00
async function moduleWorker ( ) {
const context = getContext ( ) ;
2024-04-12 07:49:09 +02:00
// Hide and disable Talkinghead while not in extras
$ ( '#image_type_block' ) . toggle ( extension _settings . expressions . api == EXPRESSION _API . extras ) ;
2023-09-11 12:01:45 +02:00
2024-04-12 07:49:09 +02:00
if ( extension _settings . expressions . api != EXPRESSION _API . extras && extension _settings . expressions . talkinghead ) {
2023-09-11 12:01:45 +02:00
$ ( '#image_type_toggle' ) . prop ( 'checked' , false ) ;
setTalkingHeadState ( false ) ;
}
2023-07-20 19:32:15 +02:00
// non-characters not supported
2024-04-11 22:50:01 +02:00
if ( ! context . groupId && context . characterId === undefined ) {
2023-07-20 19:32:15 +02:00
removeExpression ( ) ;
return ;
}
const vnMode = isVisualNovelMode ( ) ;
const vnWrapperVisible = $ ( '#visual-novel-wrapper' ) . is ( ':visible' ) ;
if ( vnMode ) {
$ ( '#expression-wrapper' ) . hide ( ) ;
$ ( '#visual-novel-wrapper' ) . show ( ) ;
} else {
$ ( '#expression-wrapper' ) . show ( ) ;
$ ( '#visual-novel-wrapper' ) . hide ( ) ;
}
const vnStateChanged = vnMode !== vnWrapperVisible ;
if ( vnStateChanged ) {
lastMessage = null ;
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
2023-12-02 19:04:51 +01:00
$ ( '#expression-holder' ) . css ( { top : '' , left : '' , right : '' , bottom : '' , height : '' , width : '' , margin : '' } ) ;
2023-07-20 19:32:15 +02:00
}
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-10-16 08:12:12 +02:00
let spriteFolderName = context . groupId ? getSpriteFolderName ( currentLastMessage , currentLastMessage . name ) : getSpriteFolderName ( ) ;
2023-07-20 19:32:15 +02:00
// character has no expressions or it is not loaded
if ( Object . keys ( spriteCache ) . length === 0 ) {
await validateImages ( spriteFolderName ) ;
lastCharacter = context . groupId || context . characterId ;
}
const offlineMode = $ ( '.expression_settings .offline_mode' ) ;
2024-04-12 07:49:09 +02:00
if ( ! modules . includes ( 'classify' ) && extension _settings . expressions . api == EXPRESSION _API . extras ) {
2023-09-13 14:19:10 +02:00
$ ( '#open_chat_expressions' ) . show ( ) ;
$ ( '#no_chat_expressions' ) . hide ( ) ;
2023-07-20 19:32:15 +02:00
offlineMode . css ( 'display' , 'block' ) ;
lastCharacter = context . groupId || context . characterId ;
if ( context . groupId ) {
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
return ;
}
else {
// force reload expressions list on connect to API
if ( offlineMode . is ( ':visible' ) ) {
expressionsList = null ;
spriteCache = { } ;
expressionsList = await getExpressionsList ( ) ;
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
2023-10-08 10:22:48 +02:00
if ( context . groupId && ! Array . isArray ( spriteCache [ spriteFolderName ] ) ) {
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
2023-07-20 19:32:15 +02:00
offlineMode . css ( 'display' , 'none' ) ;
}
2023-09-18 00:49:00 +02:00
// Don't bother classifying if current char has no sprites and no default expressions are enabled
2023-09-21 20:15:05 +02:00
if ( ( ! Array . isArray ( spriteCache [ spriteFolderName ] ) || spriteCache [ spriteFolderName ] . length === 0 ) && ! extension _settings . expressions . showDefault ) {
2023-09-18 00:49:00 +02:00
return ;
}
2024-01-09 18:52:49 +01:00
const lastMessageChanged = ! ( ( lastCharacter === context . characterId || lastCharacter === context . groupId ) && lastMessage === currentLastMessage . mes ) ;
2024-01-07 23:47:18 +01:00
2023-07-20 19:32:15 +02:00
// check if last message changed
2024-01-07 23:47:18 +01:00
if ( ! lastMessageChanged ) {
2023-07-20 19:32:15 +02:00
return ;
}
// API is busy
if ( inApiCall ) {
2023-09-18 00:49:00 +02:00
console . debug ( 'Classification API is busy' ) ;
2023-07-20 19:32:15 +02:00
return ;
}
2023-09-10 19:14:57 +02:00
// Throttle classification requests during streaming
2023-10-08 10:22:48 +02:00
if ( ! context . groupId && context . streamingProcessor && ! context . streamingProcessor . isFinished ) {
2023-09-10 19:14:57 +02:00
const now = Date . now ( ) ;
const timeSinceLastServerResponse = now - lastServerResponseTime ;
if ( timeSinceLastServerResponse < STREAMING _UPDATE _INTERVAL ) {
console . log ( 'Streaming in progress: throttling expression update. Next update at ' + new Date ( lastServerResponseTime + STREAMING _UPDATE _INTERVAL ) ) ;
return ;
}
}
2023-07-20 19:32:15 +02:00
try {
inApiCall = true ;
let expression = await getExpressionLabel ( currentLastMessage . mes ) ;
// If we're not already overriding the folder name, account for group chats.
if ( spriteFolderName === currentLastMessage . name && ! context . groupId ) {
spriteFolderName = context . name2 ;
}
const force = ! ! context . groupId ;
// Character won't be angry on you for swiping
2024-04-07 06:12:52 +02:00
if ( currentLastMessage . mes == '...' && expressionsList . includes ( getFallbackExpression ( ) ) ) {
expression = getFallbackExpression ( ) ;
2023-07-20 19:32:15 +02:00
}
await sendExpressionCall ( spriteFolderName , expression , force , vnMode ) ;
}
catch ( error ) {
console . log ( error ) ;
}
finally {
inApiCall = false ;
lastCharacter = context . groupId || context . characterId ;
lastMessage = currentLastMessage . mes ;
2023-09-10 19:14:57 +02:00
lastServerResponseTime = Date . now ( ) ;
2023-07-20 19:32:15 +02:00
}
}
2024-01-07 23:47:59 +01:00
/ * *
2024-02-04 23:44:26 +01:00
* Starts / stops Talkinghead talking animation .
2024-01-07 23:47:59 +01:00
*
* Talking starts only when all the following conditions are met :
* - The LLM is currently streaming its output .
* - The AI 's current last message is non-empty, and also not just ' ... ' ( as produced by a swipe ) .
* - The AI ' s current last message has changed from what we saw during the previous call .
*
* In all other cases , talking stops .
*
2024-02-04 23:44:26 +01:00
* A Talkinghead API call is made only when the talking state changes .
2024-02-04 23:44:39 +01:00
*
* Note that also the TTS system , if enabled , starts / stops the Talkinghead talking animation .
* See ` talkingAnimation ` in ` SillyTavern/public/scripts/extensions/tts/index.js ` .
2024-01-07 23:47:59 +01:00
* /
async function updateTalkingState ( ) {
2024-02-04 23:44:26 +01:00
// Don't bother if Talkinghead is disabled or not loaded.
2024-01-09 18:52:49 +01:00
if ( ! isTalkingHeadEnabled ( ) || ! modules . includes ( 'talkinghead' ) ) {
return ;
}
2024-01-07 23:47:59 +01:00
const context = getContext ( ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
try {
// TODO: Not sure if we need also "&& !context.groupId" here - the classify check in `moduleWorker`
// (that similarly checks the streaming processor state) does that for some reason.
// Talkinghead isn't currently designed to work with groups.
2024-01-09 18:52:49 +01:00
const lastMessageChanged = ! ( ( lastCharacter === context . characterId || lastCharacter === context . groupId ) && lastTalkingStateMessage === currentLastMessage . mes ) ;
const url = new URL ( getApiUrl ( ) ) ;
let newTalkingState ;
if ( context . streamingProcessor && ! context . streamingProcessor . isFinished &&
currentLastMessage . mes . length !== 0 && currentLastMessage . mes !== '...' && lastMessageChanged ) {
url . pathname = '/api/talkinghead/start_talking' ;
newTalkingState = true ;
} else {
url . pathname = '/api/talkinghead/stop_talking' ;
newTalkingState = false ;
}
try {
2024-02-04 23:44:26 +01:00
// Call the Talkinghead API only if the talking state changed.
2024-01-09 18:52:49 +01:00
if ( newTalkingState !== lastTalkingState ) {
console . debug ( ` updateTalkingState: calling ${ url . pathname } ` ) ;
await doExtrasFetch ( url ) ;
2024-01-07 23:47:59 +01:00
}
}
2024-01-09 18:52:49 +01:00
catch ( error ) {
// it's ok if not supported
}
finally {
lastTalkingState = newTalkingState ;
}
2024-01-07 23:47:59 +01:00
}
catch ( error ) {
// console.log(error);
}
finally {
lastTalkingStateMessage = currentLastMessage . mes ;
}
}
2024-01-06 01:09:27 +01:00
/ * *
* Checks whether the current character has a talkinghead image available .
2024-01-07 20:14:29 +01:00
* @ returns { Promise < boolean > } True if the character has a talkinghead image available , false otherwise .
2024-01-06 01:09:27 +01:00
* /
2024-01-07 20:14:29 +01:00
async function isTalkingHeadAvailable ( ) {
2023-09-07 16:48:12 +02:00
let spriteFolderName = getSpriteFolderName ( ) ;
2023-08-04 07:54:01 +02:00
try {
await validateImages ( spriteFolderName ) ;
2023-08-10 23:52:14 +02:00
let talkingheadObj = spriteCache [ spriteFolderName ] . find ( obj => obj . label === 'talkinghead' ) ;
2024-01-07 20:14:29 +01:00
let talkingheadPath = talkingheadObj ? talkingheadObj . path : null ;
2023-08-04 07:54:01 +02:00
2024-01-07 20:14:29 +01:00
if ( talkingheadPath != null ) {
2023-08-04 07:54:01 +02:00
return true ;
2023-08-10 11:42:52 +02:00
} else {
2024-01-07 20:14:29 +01:00
await unloadTalkingHead ( ) ;
2023-08-04 07:54:01 +02:00
return false ;
}
} catch ( err ) {
return err ;
}
}
2023-09-07 16:48:12 +02:00
function getSpriteFolderName ( characterMessage = null , characterName = null ) {
const context = getContext ( ) ;
let spriteFolderName = characterName ? ? context . name2 ;
const message = characterMessage ? ? getLastCharacterMessage ( ) ;
const avatarFileName = getFolderNameByMessage ( message ) ;
const expressionOverride = extension _settings . expressionOverrides . find ( e => e . name == avatarFileName ) ;
if ( expressionOverride && expressionOverride . path ) {
spriteFolderName = expressionOverride . path ;
}
return spriteFolderName ;
}
2024-01-07 20:14:29 +01:00
function setTalkingHeadState ( newState ) {
2024-02-04 23:44:46 +01:00
console . debug ( ` expressions: New talkinghead state: ${ newState } ` ) ;
2024-01-07 20:14:29 +01:00
extension _settings . expressions . talkinghead = newState ; // Store setting
2023-08-03 12:05:21 +02:00
saveSettingsDebounced ( ) ;
2024-04-14 18:46:33 +02:00
if ( extension _settings . expressions . api == EXPRESSION _API . local || extension _settings . expressions . api == EXPRESSION _API . llm ) {
2023-09-11 12:01:45 +02:00
return ;
}
2024-01-07 20:14:29 +01:00
isTalkingHeadAvailable ( ) . then ( result => {
2023-08-04 07:54:01 +02:00
if ( result ) {
2023-08-10 23:52:14 +02:00
//console.log("talkinghead exists!");
2023-08-04 07:54:01 +02:00
2023-08-10 23:55:05 +02:00
if ( extension _settings . expressions . talkinghead ) {
2024-01-07 20:14:29 +01:00
loadTalkingHead ( ) ;
2023-08-10 11:42:52 +02:00
} else {
2024-01-07 20:14:29 +01:00
unloadTalkingHead ( ) ;
2023-08-10 11:42:52 +02:00
}
2023-09-07 16:48:12 +02:00
handleImageChange ( ) ; // Change image as needed
2023-08-04 07:54:01 +02:00
} else {
2023-08-10 23:52:14 +02:00
//console.log("talkinghead does not exist.");
2023-08-04 07:54:01 +02:00
}
} ) ;
2023-08-03 12:05:21 +02:00
}
2023-09-07 16:48:12 +02:00
function getFolderNameByMessage ( message ) {
2023-07-20 19:32:15 +02:00
const context = getContext ( ) ;
let avatarPath = '' ;
if ( context . groupId ) {
avatarPath = message . original _avatar || context . characters . find ( x => message . force _avatar && message . force _avatar . includes ( encodeURIComponent ( x . avatar ) ) ) ? . avatar ;
}
else if ( context . characterId ) {
avatarPath = getCharaFilename ( ) ;
}
if ( ! avatarPath ) {
return '' ;
}
2023-12-02 19:04:51 +01:00
const folderName = avatarPath . replace ( /\.[^/.]+$/ , '' ) ;
2023-07-20 19:32:15 +02:00
return folderName ;
}
async function sendExpressionCall ( name , expression , force , vnMode ) {
2023-12-30 12:35:10 +01:00
lastExpression [ name . split ( '/' ) [ 0 ] ] = expression ;
2023-07-20 19:32:15 +02:00
if ( ! vnMode ) {
vnMode = isVisualNovelMode ( ) ;
}
if ( vnMode ) {
await updateVisualNovelMode ( name , expression ) ;
} else {
setExpression ( name , expression , force ) ;
}
}
2023-09-14 17:37:13 +02:00
async function setSpriteSetCommand ( _ , folder ) {
if ( ! folder ) {
console . log ( 'Clearing sprite set' ) ;
folder = '' ;
}
2023-09-14 18:12:54 +02:00
if ( folder . startsWith ( '/' ) || folder . startsWith ( '\\' ) ) {
folder = folder . slice ( 1 ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
folder = ` ${ currentLastMessage . name } / ${ folder } ` ;
}
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( folder . trim ( ) ) ;
2023-09-14 17:37:13 +02:00
onClickExpressionOverrideButton ( ) ;
2024-05-12 21:15:05 +02:00
// removeExpression();
// moduleWorker();
const vnMode = isVisualNovelMode ( ) ;
await sendExpressionCall ( folder , lastExpression , true , vnMode ) ;
2024-06-15 17:21:44 +02:00
return '' ;
2023-09-14 17:37:13 +02:00
}
2024-09-05 00:13:54 +02:00
async function classifyCallback ( /** @type {{api: string?, prompt: string?}} */ { api = null , prompt = null } , text ) {
2024-04-05 21:33:16 +02:00
if ( ! text ) {
2024-09-05 00:06:14 +02:00
toastr . warning ( 'No text provided' ) ;
return '' ;
}
if ( api && ! Object . keys ( EXPRESSION _API ) . includes ( api ) ) {
toastr . warning ( 'Invalid API provided' ) ;
2024-04-05 21:33:16 +02:00
return '' ;
}
2024-09-05 00:06:14 +02:00
const expressionApi = EXPRESSION _API [ api ] || extension _settings . expressions . api ;
if ( ! modules . includes ( 'classify' ) && expressionApi == EXPRESSION _API . extras ) {
2024-04-05 21:33:16 +02:00
toastr . warning ( 'Text classification is disabled or not available' ) ;
return '' ;
}
2024-09-05 00:34:52 +02:00
const label = await getExpressionLabel ( text , expressionApi , { customPrompt : prompt } ) ;
2024-04-05 21:33:16 +02:00
console . debug ( ` Classification result for " ${ text } ": ${ label } ` ) ;
return label ;
}
2023-09-07 16:48:12 +02:00
async function setSpriteSlashCommand ( _ , spriteId ) {
if ( ! spriteId ) {
console . log ( 'No sprite id provided' ) ;
2024-06-15 17:21:44 +02:00
return '' ;
2023-09-07 16:48:12 +02:00
}
spriteId = spriteId . trim ( ) . toLowerCase ( ) ;
2024-02-04 23:44:26 +01:00
// In Talkinghead mode, don't check for the existence of the sprite
2024-01-06 01:10:09 +01:00
// (emotion names are the same as for sprites, but it only needs "talkinghead.png").
2024-01-06 01:23:10 +01:00
const currentLastMessage = getLastCharacterMessage ( ) ;
const spriteFolderName = getSpriteFolderName ( currentLastMessage , currentLastMessage . name ) ;
2024-01-07 20:14:29 +01:00
let label = spriteId ;
2024-02-04 23:45:20 +01:00
if ( ! isTalkingHeadEnabled ( ) || ! modules . includes ( 'talkinghead' ) ) {
2024-01-06 01:10:09 +01:00
await validateImages ( spriteFolderName ) ;
2023-09-07 16:48:12 +02:00
2024-01-06 01:10:09 +01:00
// Fuzzy search for sprite
const fuse = new Fuse ( spriteCache [ spriteFolderName ] , { keys : [ 'label' ] } ) ;
const results = fuse . search ( spriteId ) ;
const spriteItem = results [ 0 ] ? . item ;
2023-09-07 16:48:12 +02:00
2024-01-06 01:10:09 +01:00
if ( ! spriteItem ) {
console . log ( 'No sprite found for search term ' + spriteId ) ;
2024-06-15 17:21:44 +02:00
return '' ;
2024-01-06 01:10:09 +01:00
}
2024-01-06 01:23:10 +01:00
label = spriteItem . label ;
}
2023-09-07 16:48:12 +02:00
const vnMode = isVisualNovelMode ( ) ;
2024-01-06 01:23:10 +01:00
await sendExpressionCall ( spriteFolderName , label , true , vnMode ) ;
2024-06-15 17:21:44 +02:00
return label ;
2023-09-07 16:48:12 +02:00
}
2023-09-09 16:31:27 +02:00
/ * *
* 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 .
* If the text is more than 300 characters , the first and last 150 characters are returned .
* The result is trimmed to the end of sentence .
* @ param { string } text The text to process .
* @ returns { string }
* /
function sampleClassifyText ( text ) {
if ( ! text ) {
return text ;
}
2024-05-06 21:02:13 +02:00
// Replace macros, remove asterisks and quotes
let result = substituteParams ( text ) . replace ( /[*"]/g , '' ) ;
2023-09-09 16:31:27 +02:00
2023-10-23 01:14:29 +02:00
const SAMPLE _THRESHOLD = 500 ;
2023-09-09 16:31:27 +02:00
const HALF _SAMPLE _THRESHOLD = SAMPLE _THRESHOLD / 2 ;
if ( text . length < SAMPLE _THRESHOLD ) {
result = trimToEndSentence ( result ) ;
} else {
result = trimToEndSentence ( result . slice ( 0 , HALF _SAMPLE _THRESHOLD ) ) + ' ' + trimToStartSentence ( result . slice ( - HALF _SAMPLE _THRESHOLD ) ) ;
}
return result . trim ( ) ;
}
2024-04-14 16:13:54 +02:00
/ * *
* Gets the classification prompt for the LLM API .
* @ param { string [ ] } labels A list of labels to search for .
* @ returns { Promise < string > } Prompt for the LLM API .
* /
async function getLlmPrompt ( labels ) {
if ( isJsonSchemaSupported ( ) ) {
return '' ;
}
2024-05-25 14:38:32 +02:00
if ( isFunctionCallingSupported ( ) ) {
return '' ;
}
2024-04-14 18:53:40 +02:00
const labelsString = labels . map ( x => ` " ${ x } " ` ) . join ( ', ' ) ;
2024-06-15 00:40:16 +02:00
const prompt = substituteParamsExtended ( String ( extension _settings . expressions . llmPrompt ) , { labels : labelsString } ) ;
2024-04-14 16:13:54 +02:00
return prompt ;
}
/ * *
* Parses the emotion response from the LLM API .
* @ param { string } emotionResponse The response from the LLM API .
* @ param { string [ ] } labels A list of labels to search for .
* @ returns { string } The parsed emotion or the fallback expression .
* /
function parseLlmResponse ( emotionResponse , labels ) {
try {
const parsedEmotion = JSON . parse ( emotionResponse ) ;
2024-05-25 16:11:58 +02:00
const response = parsedEmotion ? . emotion ? . trim ( ) ? . toLowerCase ( ) ;
2024-05-25 14:38:32 +02:00
if ( ! response || ! labels . includes ( response ) ) {
2024-05-25 16:32:11 +02:00
console . debug ( ` Parsed emotion response: ${ response } not in labels: ${ labels } ` ) ;
throw new Error ( 'Emotion not in labels' ) ;
2024-05-25 14:38:32 +02:00
}
return response ;
2024-04-14 16:13:54 +02:00
} catch {
2024-05-18 17:58:25 +02:00
const fuse = new Fuse ( labels , { includeScore : true } ) ;
console . debug ( 'Using fuzzy search in labels:' , labels ) ;
2024-05-18 17:05:55 +02:00
const result = fuse . search ( emotionResponse ) ;
if ( result . length > 0 ) {
2024-05-18 17:58:25 +02:00
console . debug ( ` fuzzy search found: ${ result [ 0 ] . item } as closest for the LLM response: ` , emotionResponse ) ;
2024-05-18 17:05:55 +02:00
return result [ 0 ] . item ;
2024-04-14 16:13:54 +02:00
}
}
2024-04-14 19:02:34 +02:00
throw new Error ( 'Could not parse emotion response ' + emotionResponse ) ;
2024-04-14 16:13:54 +02:00
}
2024-05-25 14:38:32 +02:00
/ * *
* Registers the function tool for the LLM API .
* @ param { FunctionToolRegister } args Function tool register arguments .
* /
function onFunctionToolRegister ( args ) {
if ( inApiCall && extension _settings . expressions . api === EXPRESSION _API . llm && isFunctionCallingSupported ( ) ) {
// Only trigger on quiet mode
if ( args . type !== 'quiet' ) {
return ;
}
const emotions = DEFAULT _EXPRESSIONS . filter ( ( e ) => e != 'talkinghead' ) ;
const jsonSchema = {
$schema : 'http://json-schema.org/draft-04/schema#' ,
type : 'object' ,
properties : {
emotion : {
type : 'string' ,
enum : emotions ,
2024-05-25 16:09:47 +02:00
description : ` One of the following: ${ JSON . stringify ( emotions ) } ` ,
2024-05-25 14:38:32 +02:00
} ,
} ,
required : [
'emotion' ,
] ,
} ;
args . registerFunctionTool (
FUNCTION _NAME ,
substituteParams ( 'Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.' ) ,
jsonSchema ,
true ,
) ;
}
}
2024-04-12 07:49:09 +02:00
function onTextGenSettingsReady ( args ) {
// Only call if inside an API call
2024-04-14 16:13:54 +02:00
if ( inApiCall && extension _settings . expressions . api === EXPRESSION _API . llm && isJsonSchemaSupported ( ) ) {
2024-04-14 14:40:10 +02:00
const emotions = DEFAULT _EXPRESSIONS . filter ( ( e ) => e != 'talkinghead' ) ;
2024-04-12 07:49:09 +02:00
Object . assign ( args , {
top _k : 1 ,
2024-04-12 15:57:09 +02:00
stop : [ ] ,
stopping _strings : [ ] ,
custom _token _bans : [ ] ,
2024-04-12 07:49:09 +02:00
json _schema : {
2024-04-14 14:40:10 +02:00
$schema : 'http://json-schema.org/draft-04/schema#' ,
type : 'object' ,
2024-04-12 07:49:09 +02:00
properties : {
emotion : {
2024-04-14 14:40:10 +02:00
type : 'string' ,
enum : emotions ,
} ,
2024-04-12 07:49:09 +02:00
} ,
required : [
2024-04-14 14:40:10 +02:00
'emotion' ,
] ,
} ,
2024-04-12 07:49:09 +02:00
} ) ;
}
}
2024-09-05 00:06:14 +02:00
/ * *
* Retrieves the label of an expression via classification based on the provided text .
* Optionally allows to override the expressions API being used .
* @ param { string } text - The text to classify and retrieve the expression label for .
* @ param { EXPRESSION _API } [ expressionsApi = extension _settings . expressions . api ] - The expressions API to use for classification .
2024-09-05 00:13:54 +02:00
* @ param { object } [ options = { } ] - Optional arguments .
* @ param { string ? } [ options . customPrompt = null ] - The custom prompt to use for classification .
2024-09-05 00:06:14 +02:00
* @ returns { Promise < string > } - The label of the expression .
* /
2024-09-05 00:13:54 +02:00
export async function getExpressionLabel ( text , expressionsApi = extension _settings . expressions . api , { customPrompt = null } = { } ) {
2023-07-20 19:32:15 +02:00
// Return if text is undefined, saving a costly fetch request
2024-09-05 00:06:14 +02:00
if ( ( ! modules . includes ( 'classify' ) && expressionsApi == EXPRESSION _API . extras ) || ! text ) {
2024-04-07 06:12:52 +02:00
return getFallbackExpression ( ) ;
2023-07-20 19:32:15 +02:00
}
2024-04-11 21:09:05 +02:00
if ( extension _settings . expressions . translate && typeof window [ 'translate' ] === 'function' ) {
text = await window [ 'translate' ] ( text , 'en' ) ;
}
2023-09-09 16:31:27 +02:00
text = sampleClassifyText ( text ) ;
2023-09-09 14:14:16 +02:00
try {
2024-09-05 00:06:14 +02:00
switch ( expressionsApi ) {
2024-04-14 16:13:54 +02:00
// Local BERT pipeline
case EXPRESSION _API . local : {
2024-04-12 07:49:09 +02:00
const localResult = await fetch ( '/api/extra/classify' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { text : text } ) ,
} ) ;
2023-07-20 19:32:15 +02:00
2024-04-12 07:49:09 +02:00
if ( localResult . ok ) {
const data = await localResult . json ( ) ;
return data . classification [ 0 ] . label ;
}
2024-04-14 16:13:54 +02:00
} break ;
// Using LLM
case EXPRESSION _API . llm : {
2024-05-25 14:38:32 +02:00
try {
await waitUntilCondition ( ( ) => online _status !== 'no_connection' , 3000 , 250 ) ;
} catch ( error ) {
console . warn ( 'No LLM connection. Using fallback expression' , error ) ;
return getFallbackExpression ( ) ;
}
2024-04-14 16:13:54 +02:00
const expressionsList = await getExpressionsList ( ) ;
2024-09-25 04:55:48 +02:00
const prompt = substituteParamsExtended ( customPrompt , { labels : expressionsList } ) || await getLlmPrompt ( expressionsList ) ;
2024-05-25 14:38:32 +02:00
let functionResult = null ;
2024-04-19 22:01:31 +02:00
eventSource . once ( event _types . TEXT _COMPLETION _SETTINGS _READY , onTextGenSettingsReady ) ;
2024-05-25 14:38:32 +02:00
eventSource . once ( event _types . LLM _FUNCTION _TOOL _REGISTER , onFunctionToolRegister ) ;
eventSource . once ( event _types . LLM _FUNCTION _TOOL _CALL , ( /** @type {FunctionToolCall} */ args ) => {
if ( args . name !== FUNCTION _NAME ) {
return ;
}
functionResult = args ? . arguments ;
} ) ;
2024-08-04 12:24:24 +02:00
const emotionResponse = await generateRaw ( text , main _api , false , false , prompt ) ;
2024-05-25 14:38:32 +02:00
return parseLlmResponse ( functionResult || emotionResponse , expressionsList ) ;
2024-04-14 16:13:54 +02:00
}
// Extras
default : {
2024-04-12 07:49:09 +02:00
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/classify' ;
2023-07-20 19:32:15 +02:00
2024-04-12 07:49:09 +02:00
const extrasResult = await doExtrasFetch ( url , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Bypass-Tunnel-Reminder' : 'bypass' ,
} ,
body : JSON . stringify ( { text : text } ) ,
} ) ;
if ( extrasResult . ok ) {
const data = await extrasResult . json ( ) ;
return data . classification [ 0 ] . label ;
}
2024-04-14 16:13:54 +02:00
} break ;
2023-09-09 14:14:16 +02:00
}
} catch ( error ) {
2024-04-14 14:40:10 +02:00
toastr . info ( 'Could not classify expression. Check the console or your backend for more information.' ) ;
2024-04-13 07:29:54 +02:00
console . error ( error ) ;
2024-04-07 06:12:52 +02:00
return getFallbackExpression ( ) ;
2023-07-20 19:32:15 +02:00
}
}
function getLastCharacterMessage ( ) {
const context = getContext ( ) ;
const reversedChat = context . chat . slice ( ) . reverse ( ) ;
for ( let mes of reversedChat ) {
2024-06-16 10:27:11 +02:00
if ( mes . is _user || mes . is _system || mes . extra ? . type === system _message _types . NARRATOR ) {
2024-06-15 17:13:59 +02:00
continue ;
}
2023-07-20 19:32:15 +02:00
return { mes : mes . mes , name : mes . name , original _avatar : mes . original _avatar , force _avatar : mes . force _avatar } ;
}
return { mes : '' , name : null , original _avatar : null , force _avatar : null } ;
}
function removeExpression ( ) {
lastMessage = null ;
$ ( 'img.expression' ) . off ( 'error' ) ;
$ ( 'img.expression' ) . prop ( 'src' , '' ) ;
$ ( 'img.expression' ) . removeClass ( 'default' ) ;
2023-09-13 14:19:10 +02:00
$ ( '#open_chat_expressions' ) . hide ( ) ;
$ ( '#no_chat_expressions' ) . show ( ) ;
2023-07-20 19:32:15 +02:00
}
async function validateImages ( character , forceRedrawCached ) {
if ( ! character ) {
return ;
}
const labels = await getExpressionsList ( ) ;
if ( spriteCache [ character ] ) {
if ( forceRedrawCached && $ ( '#image_list' ) . data ( 'name' ) !== character ) {
2023-12-02 20:11:06 +01:00
console . debug ( 'force redrawing character sprites list' ) ;
2024-04-11 22:38:44 +02:00
await drawSpritesList ( character , labels , spriteCache [ character ] ) ;
2023-07-20 19:32:15 +02:00
}
return ;
}
const sprites = await getSpritesList ( character ) ;
2024-04-11 22:38:44 +02:00
let validExpressions = await drawSpritesList ( character , labels , sprites ) ;
2023-07-20 19:32:15 +02:00
spriteCache [ character ] = validExpressions ;
}
2024-04-11 22:38:44 +02:00
async function drawSpritesList ( character , labels , sprites ) {
2023-07-20 19:32:15 +02:00
let validExpressions = [ ] ;
2023-09-13 14:19:10 +02:00
$ ( '#no_chat_expressions' ) . hide ( ) ;
$ ( '#open_chat_expressions' ) . show ( ) ;
2023-07-20 19:32:15 +02:00
$ ( '#image_list' ) . empty ( ) ;
$ ( '#image_list' ) . data ( 'name' , character ) ;
2023-09-13 14:19:10 +02:00
$ ( '#image_list_header_name' ) . text ( character ) ;
2023-07-20 19:32:15 +02:00
if ( ! Array . isArray ( labels ) ) {
return [ ] ;
}
2024-04-11 22:38:44 +02:00
for ( const item of labels . sort ( ) ) {
2023-07-20 19:32:15 +02:00
const sprite = sprites . find ( x => x . label == item ) ;
2023-09-14 20:30:02 +02:00
const isCustom = extension _settings . expressions . custom . includes ( item ) ;
2023-07-20 19:32:15 +02:00
if ( sprite ) {
validExpressions . push ( sprite ) ;
2024-04-11 22:38:44 +02:00
const listItem = await getListItem ( item , sprite . path , 'success' , isCustom ) ;
$ ( '#image_list' ) . append ( listItem ) ;
2023-07-20 19:32:15 +02:00
}
else {
2024-04-11 22:38:44 +02:00
const listItem = await getListItem ( item , '/img/No-Image-Placeholder.svg' , 'failure' , isCustom ) ;
$ ( '#image_list' ) . append ( listItem ) ;
2023-07-20 19:32:15 +02:00
}
2024-04-11 22:38:44 +02:00
}
2023-07-20 19:32:15 +02:00
return validExpressions ;
}
2023-09-14 20:30:02 +02:00
/ * *
* Renders a list item template for the expressions list .
* @ param { string } item Expression name
* @ param { string } imageSrc Path to image
* @ param { 'success' | 'failure' } textClass 'success' or 'failure'
* @ param { boolean } isCustom If expression is added by user
2024-04-11 22:38:44 +02:00
* @ returns { Promise < string > } Rendered list item template
2023-09-14 20:30:02 +02:00
* /
2024-04-11 22:38:44 +02:00
async function getListItem ( item , imageSrc , textClass , isCustom ) {
2024-02-05 01:21:20 +01:00
const isFirefox = navigator . userAgent . toLowerCase ( ) . indexOf ( 'firefox' ) > - 1 ;
imageSrc = isFirefox ? ` ${ imageSrc } ?t= ${ Date . now ( ) } ` : imageSrc ;
2024-04-11 22:38:44 +02:00
return renderExtensionTemplateAsync ( MODULE _NAME , 'list-item' , { item , imageSrc , textClass , isCustom } ) ;
2023-07-20 19:32:15 +02:00
}
async function getSpritesList ( name ) {
console . debug ( 'getting sprites list' ) ;
try {
2023-09-16 17:03:31 +02:00
const result = await fetch ( ` /api/sprites/get?name= ${ encodeURIComponent ( name ) } ` ) ;
2023-07-20 19:32:15 +02:00
let sprites = result . ok ? ( await result . json ( ) ) : [ ] ;
return sprites ;
}
catch ( err ) {
console . log ( err ) ;
return [ ] ;
}
}
2024-04-07 06:12:52 +02:00
async function renderAdditionalExpressionSettings ( ) {
renderCustomExpressions ( ) ;
await renderFallbackExpressionPicker ( ) ;
}
2023-09-14 20:30:02 +02:00
function renderCustomExpressions ( ) {
if ( ! Array . isArray ( extension _settings . expressions . custom ) ) {
extension _settings . expressions . custom = [ ] ;
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
const customExpressions = extension _settings . expressions . custom . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
$ ( '#expression_custom' ) . empty ( ) ;
for ( const expression of customExpressions ) {
const option = document . createElement ( 'option' ) ;
option . value = expression ;
option . text = expression ;
$ ( '#expression_custom' ) . append ( option ) ;
}
2023-09-14 20:41:30 +02:00
if ( customExpressions . length === 0 ) {
$ ( '#expression_custom' ) . append ( '<option value="" disabled selected>[ No custom expressions ]</option>' ) ;
}
2023-09-14 20:30:02 +02:00
}
2024-04-07 06:12:52 +02:00
async function renderFallbackExpressionPicker ( ) {
const expressions = await getExpressionsList ( ) ;
const defaultPicker = $ ( '#expression_fallback' ) ;
defaultPicker . empty ( ) ;
const fallbackExpression = getFallbackExpression ( ) ;
for ( const expression of expressions ) {
const option = document . createElement ( 'option' ) ;
option . value = expression ;
option . text = expression ;
option . selected = expression == fallbackExpression ;
defaultPicker . append ( option ) ;
}
}
2024-06-15 17:21:44 +02:00
function getCachedExpressions ( ) {
if ( ! Array . isArray ( expressionsList ) ) {
return [ ] ;
}
return [ ... expressionsList , ... extension _settings . expressions . custom ] . filter ( onlyUnique ) ;
}
2024-09-05 00:24:10 +02:00
export async function getExpressionsList ( ) {
2023-09-14 20:30:02 +02:00
// Return cached list if available
2023-07-20 19:32:15 +02:00
if ( Array . isArray ( expressionsList ) ) {
2024-06-15 17:21:44 +02:00
return getCachedExpressions ( ) ;
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
/ * *
* Returns the list of expressions from the API or fallback in offline mode .
* @ returns { Promise < string [ ] > }
* /
async function resolveExpressionsList ( ) {
2024-04-25 04:29:20 +02:00
// See if we can retrieve a specific expression list from the API
2023-09-14 20:30:02 +02:00
try {
2024-04-25 04:29:20 +02:00
// Check Extras api first, if enabled and that module active
if ( extension _settings . expressions . api == EXPRESSION _API . extras && modules . includes ( 'classify' ) ) {
2023-09-14 20:30:02 +02:00
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/classify/labels' ;
2023-09-09 14:14:16 +02:00
2023-09-14 20:30:02 +02:00
const apiResult = await doExtrasFetch ( url , {
method : 'GET' ,
headers : { 'Bypass-Tunnel-Reminder' : 'bypass' } ,
} ) ;
2023-07-20 19:32:15 +02:00
2023-09-14 20:30:02 +02:00
if ( apiResult . ok ) {
2023-07-20 19:32:15 +02:00
2024-04-12 07:49:09 +02:00
const data = await apiResult . json ( ) ;
expressionsList = data . labels ;
return expressionsList ;
2024-04-14 14:40:10 +02:00
}
2024-04-25 04:29:20 +02:00
}
// If running the local classify model (not using the LLM), we ask that one
if ( extension _settings . expressions . api == EXPRESSION _API . local ) {
2024-04-12 07:49:09 +02:00
const apiResult = await fetch ( '/api/extra/classify/labels' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
if ( apiResult . ok ) {
2023-09-14 20:30:02 +02:00
const data = await apiResult . json ( ) ;
expressionsList = data . labels ;
return expressionsList ;
}
2023-09-09 14:14:16 +02:00
}
2024-04-25 04:29:20 +02:00
} catch ( error ) {
2023-09-14 20:30:02 +02:00
console . log ( error ) ;
}
2024-04-25 04:29:20 +02:00
// If there was no specific list, or an error, just return the default expressions
2024-07-27 12:57:39 +02:00
expressionsList = DEFAULT _EXPRESSIONS . filter ( e => e !== 'talkinghead' ) . slice ( ) ;
return expressionsList ;
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
const result = await resolveExpressionsList ( ) ;
2024-04-11 22:38:44 +02:00
return [ ... result , ... extension _settings . expressions . custom ] . filter ( onlyUnique ) ;
2023-07-20 19:32:15 +02:00
}
async function setExpression ( character , expression , force ) {
2024-02-04 23:45:20 +01:00
if ( ! isTalkingHeadEnabled ( ) || ! modules . includes ( 'talkinghead' ) ) {
2023-07-31 19:56:05 +02:00
console . debug ( 'entered setExpressions' ) ;
await validateImages ( character ) ;
const img = $ ( 'img.expression' ) ;
const prevExpressionSrc = img . attr ( 'src' ) ;
2023-12-02 20:11:06 +01:00
const expressionClone = img . clone ( ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
const sprite = ( spriteCache [ character ] && spriteCache [ character ] . find ( x => x . label === expression ) ) ;
console . debug ( 'checking for expression images to show..' ) ;
if ( sprite ) {
console . debug ( 'setting expression from character images folder' ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( force && isVisualNovelMode ( ) ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id === context . groupId ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
for ( const member of group . members ) {
const groupMember = context . characters . find ( x => x . avatar === member ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( ! groupMember ) {
continue ;
}
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( groupMember . name == character ) {
await setImage ( $ ( ` .expression-holder[data-avatar=" ${ member } "] img ` ) , sprite . path ) ;
return ;
}
2023-07-20 19:32:15 +02:00
}
}
2023-07-31 19:56:05 +02:00
//only swap expressions when necessary
if ( prevExpressionSrc !== sprite . path
&& ! img . hasClass ( 'expression-animating' ) ) {
//clone expression
2023-12-02 20:11:06 +01:00
expressionClone . addClass ( 'expression-clone' ) ;
2023-07-31 19:56:05 +02:00
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone . attr ( 'id' , '' ) . css ( { opacity : 0 } ) ;
//add new sprite path to clone src
expressionClone . attr ( 'src' , sprite . path ) ;
//add invisible clone to html
2023-12-02 20:11:06 +01:00
expressionClone . appendTo ( $ ( '#expression-holder' ) ) ;
2023-07-31 19:56:05 +02:00
const duration = 200 ;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img . addClass ( 'expression-animating' ) ;
// Set the parent container's min width and height before running the transition
const imgWidth = img . width ( ) ;
const imgHeight = img . height ( ) ;
const expressionHolder = img . parent ( ) ;
expressionHolder . css ( 'min-width' , imgWidth > 100 ? imgWidth : 100 ) ;
expressionHolder . css ( 'min-height' , imgHeight > 100 ? imgHeight : 100 ) ;
//position absolute prevent the original from jumping around during transition
2023-08-25 01:07:59 +02:00
img . css ( 'position' , 'absolute' ) . width ( imgWidth ) . height ( imgHeight ) ;
2023-07-31 19:56:05 +02:00
expressionClone . addClass ( 'expression-animating' ) ;
//fade the clone in
expressionClone . css ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-31 19:56:05 +02:00
} ) . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 1 ,
2023-07-31 19:56:05 +02:00
} , duration )
//when finshed fading in clone, fade out the original
. promise ( ) . done ( function ( ) {
img . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-31 19:56:05 +02:00
} , duration ) ;
//remove old expression
img . remove ( ) ;
//replace ID so it becomes the new 'original' expression for next change
expressionClone . attr ( 'id' , 'expression-image' ) ;
expressionClone . removeClass ( 'expression-animating' ) ;
// Reset the expression holder min height and width
expressionHolder . css ( 'min-width' , 100 ) ;
expressionHolder . css ( 'min-height' , 100 ) ;
} ) ;
expressionClone . removeClass ( 'expression-clone' ) ;
expressionClone . removeClass ( 'default' ) ;
expressionClone . off ( 'error' ) ;
expressionClone . on ( 'error' , function ( ) {
console . debug ( 'Expression image error' , sprite . path ) ;
$ ( this ) . attr ( 'src' , '' ) ;
$ ( this ) . off ( 'error' ) ;
if ( force && extension _settings . expressions . showDefault ) {
setDefault ( ) ;
}
2023-07-20 19:32:15 +02:00
} ) ;
2023-08-10 11:47:56 +02:00
}
}
else {
if ( extension _settings . expressions . showDefault ) {
setDefault ( ) ;
2023-07-31 07:52:30 +02:00
}
2023-07-20 19:32:15 +02:00
}
2023-07-31 19:56:05 +02:00
function setDefault ( ) {
console . debug ( 'setting default' ) ;
const defImgUrl = ` /img/default-expressions/ ${ expression } .png ` ;
//console.log(defImgUrl);
img . attr ( 'src' , defImgUrl ) ;
img . addClass ( 'default' ) ;
}
2023-12-02 19:04:51 +01:00
document . getElementById ( 'expression-holder' ) . style . display = '' ;
2023-07-31 19:56:05 +02:00
2023-08-03 12:05:21 +02:00
} else {
2024-02-04 23:44:26 +01:00
// Set the Talkinghead emotion to the specified expression
// TODO: For now, Talkinghead emote only supported when VN mode is off; see also updateVisualNovelMode.
2024-01-05 14:45:05 +01:00
try {
2024-01-07 20:14:29 +01:00
let result = await isTalkingHeadAvailable ( ) ;
2023-08-04 07:54:01 +02:00
if ( result ) {
2024-01-02 01:18:54 +01:00
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/talkinghead/set_emotion' ;
2024-01-05 11:15:24 +01:00
await doExtrasFetch ( url , {
2024-01-02 01:18:54 +01:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON . stringify ( { emotion _name : expression } ) ,
2024-01-05 11:15:24 +01:00
} ) ;
2023-08-03 13:07:50 +02:00
}
2024-01-05 11:15:24 +01:00
}
catch ( error ) {
// `set_emotion` is not present in old versions, so let it 404.
}
2023-08-25 22:48:59 +02:00
2024-01-05 11:15:24 +01:00
try {
// Find the <img> element with id="expression-image" and class="expression"
const imgElement = document . querySelector ( 'img#expression-image.expression' ) ;
//console.log("searching");
if ( imgElement && imgElement instanceof HTMLImageElement ) {
//console.log("setting value");
imgElement . src = getApiUrl ( ) + '/api/talkinghead/result_feed' ;
}
}
catch ( error ) {
//console.log("The fetch failed!");
}
2023-07-31 07:52:30 +02:00
}
2023-07-31 19:56:05 +02:00
}
2023-07-20 19:32:15 +02:00
function onClickExpressionImage ( ) {
const expression = $ ( this ) . attr ( 'id' ) ;
2023-09-07 16:48:12 +02:00
setSpriteSlashCommand ( { } , expression ) ;
2023-07-20 19:32:15 +02:00
}
2023-09-07 16:48:12 +02:00
2023-09-14 20:30:02 +02:00
async function onClickExpressionAddCustom ( ) {
2024-04-11 22:38:44 +02:00
const template = await renderExtensionTemplateAsync ( MODULE _NAME , 'add-custom-expression' ) ;
let expressionName = await callPopup ( template , 'input' ) ;
2023-09-14 20:30:02 +02:00
if ( ! expressionName ) {
console . debug ( 'No custom expression name provided' ) ;
return ;
}
expressionName = expressionName . trim ( ) . toLowerCase ( ) ;
// a-z, 0-9, dashes and underscores only
if ( ! /^[a-z0-9-_]+$/ . test ( expressionName ) ) {
toastr . info ( 'Invalid custom expression name provided' ) ;
return ;
}
// Check if expression name already exists in default expressions
if ( DEFAULT _EXPRESSIONS . includes ( expressionName ) ) {
toastr . info ( 'Expression name already exists' ) ;
return ;
}
// Check if expression name already exists in custom expressions
if ( extension _settings . expressions . custom . includes ( expressionName ) ) {
toastr . info ( 'Custom expression already exists' ) ;
return ;
}
// Add custom expression into settings
extension _settings . expressions . custom . push ( expressionName ) ;
2024-04-07 06:12:52 +02:00
await renderAdditionalExpressionSettings ( ) ;
2023-09-14 20:30:02 +02:00
saveSettingsDebounced ( ) ;
// Force refresh sprites list
expressionsList = null ;
spriteCache = { } ;
moduleWorker ( ) ;
}
async function onClickExpressionRemoveCustom ( ) {
2024-04-11 22:38:44 +02:00
const selectedExpression = String ( $ ( '#expression_custom' ) . val ( ) ) ;
2023-09-14 20:30:02 +02:00
if ( ! selectedExpression ) {
console . debug ( 'No custom expression selected' ) ;
return ;
}
2024-04-11 22:38:44 +02:00
const template = await renderExtensionTemplateAsync ( MODULE _NAME , 'remove-custom-expression' , { expression : selectedExpression } ) ;
const confirmation = await callPopup ( template , 'confirm' ) ;
2023-09-14 20:30:02 +02:00
if ( ! confirmation ) {
console . debug ( 'Custom expression removal cancelled' ) ;
return ;
}
// Remove custom expression from settings
const index = extension _settings . expressions . custom . indexOf ( selectedExpression ) ;
extension _settings . expressions . custom . splice ( index , 1 ) ;
2024-04-07 06:26:48 +02:00
if ( selectedExpression == getFallbackExpression ( ) ) {
toastr . warning ( ` Deleted custom expression ' ${ selectedExpression } ' that was also selected as the fallback expression. \n Fallback expression has been reset to ' ${ DEFAULT _FALLBACK _EXPRESSION } '. ` ) ;
extension _settings . expressions . fallback _expression = DEFAULT _FALLBACK _EXPRESSION ;
}
2024-04-07 06:12:52 +02:00
await renderAdditionalExpressionSettings ( ) ;
2023-09-14 20:30:02 +02:00
saveSettingsDebounced ( ) ;
// Force refresh sprites list
expressionsList = null ;
spriteCache = { } ;
moduleWorker ( ) ;
}
2024-07-27 12:57:39 +02:00
function onExpressionApiChanged ( ) {
2024-04-12 07:49:09 +02:00
const tempApi = this . value ;
if ( tempApi ) {
extension _settings . expressions . api = Number ( tempApi ) ;
2024-04-14 16:13:54 +02:00
$ ( '.expression_llm_prompt_block' ) . toggle ( extension _settings . expressions . api === EXPRESSION _API . llm ) ;
2024-07-27 12:57:39 +02:00
expressionsList = null ;
spriteCache = { } ;
2024-04-12 07:49:09 +02:00
moduleWorker ( ) ;
saveSettingsDebounced ( ) ;
}
}
2024-04-07 06:12:52 +02:00
function onExpressionFallbackChanged ( ) {
const expression = this . value ;
if ( expression ) {
extension _settings . expressions . fallback _expression = expression ;
saveSettingsDebounced ( ) ;
}
}
2023-07-20 19:32:15 +02:00
async function handleFileUpload ( url , formData ) {
try {
const data = await jQuery . ajax ( {
2023-12-02 19:04:51 +01:00
type : 'POST' ,
2023-07-20 19:32:15 +02:00
url : url ,
data : formData ,
beforeSend : function ( ) { } ,
cache : false ,
contentType : false ,
processData : false ,
} ) ;
// Refresh sprites list
const name = formData . get ( 'name' ) ;
delete spriteCache [ name ] ;
2024-01-20 18:48:56 +01:00
await fetchImagesNoCache ( ) ;
2023-07-20 19:32:15 +02:00
await validateImages ( name ) ;
return data ;
} catch ( error ) {
toastr . error ( 'Failed to upload image' ) ;
}
}
async function onClickExpressionUpload ( event ) {
// Prevents the expression from being set
event . stopPropagation ( ) ;
const id = $ ( this ) . closest ( '.expression_list_item' ) . attr ( 'id' ) ;
const name = $ ( '#image_list' ) . data ( 'name' ) ;
const handleExpressionUploadChange = async ( e ) => {
const file = e . target . files [ 0 ] ;
if ( ! file ) {
return ;
}
const formData = new FormData ( ) ;
formData . append ( 'name' , name ) ;
formData . append ( 'label' , id ) ;
formData . append ( 'avatar' , file ) ;
2023-09-16 16:28:28 +02:00
await handleFileUpload ( '/api/sprites/upload' , formData ) ;
2023-07-20 19:32:15 +02:00
// Reset the input
e . target . form . reset ( ) ;
2024-01-06 01:09:34 +01:00
2024-02-04 23:44:26 +01:00
// In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
2024-02-04 23:45:20 +01:00
if ( id === 'talkinghead' && isTalkingHeadEnabled ( ) && modules . includes ( 'talkinghead' ) ) {
2024-01-07 20:14:29 +01:00
await loadTalkingHead ( ) ;
2024-01-06 01:09:34 +01:00
}
2023-07-20 19:32:15 +02:00
} ;
$ ( '#expression_upload' )
. off ( 'change' )
. on ( 'change' , handleExpressionUploadChange )
. trigger ( 'click' ) ;
}
async function onClickExpressionOverrideButton ( ) {
const context = getContext ( ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-09-07 16:48:12 +02:00
const avatarFileName = getFolderNameByMessage ( currentLastMessage ) ;
2023-07-20 19:32:15 +02:00
// If the avatar name couldn't be found, abort.
if ( ! avatarFileName ) {
console . debug ( ` Could not find filename for character with name ${ currentLastMessage . name } and ID ${ context . characterId } ` ) ;
return ;
}
2023-12-02 19:04:51 +01:00
const overridePath = String ( $ ( '#expression_override' ) . val ( ) ) ;
2023-07-20 19:32:15 +02:00
const existingOverrideIndex = extension _settings . expressionOverrides . findIndex ( ( e ) =>
2023-12-02 21:06:57 +01:00
e . name == avatarFileName ,
2023-07-20 19:32:15 +02:00
) ;
// If the path is empty, delete the entry from overrides
if ( overridePath === undefined || overridePath . length === 0 ) {
if ( existingOverrideIndex === - 1 ) {
return ;
}
extension _settings . expressionOverrides . splice ( existingOverrideIndex , 1 ) ;
console . debug ( ` Removed existing override for ${ avatarFileName } ` ) ;
} else {
// Properly override objects and clear the sprite cache of the previously set names
const existingOverride = extension _settings . expressionOverrides [ existingOverrideIndex ] ;
if ( existingOverride ) {
Object . assign ( existingOverride , { path : overridePath } ) ;
delete spriteCache [ existingOverride . name ] ;
} else {
const characterOverride = { name : avatarFileName , path : overridePath } ;
extension _settings . expressionOverrides . push ( characterOverride ) ;
delete spriteCache [ currentLastMessage . name ] ;
}
console . debug ( ` Added/edited expression override for character with filename ${ avatarFileName } to folder ${ overridePath } ` ) ;
}
saveSettingsDebounced ( ) ;
// Refresh sprites list. Assume the override path has been properly handled.
try {
2024-04-12 07:49:09 +02:00
inApiCall = true ;
2023-07-20 19:32:15 +02:00
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
await validateImages ( overridePath . length === 0 ? currentLastMessage . name : overridePath , true ) ;
const expression = await getExpressionLabel ( currentLastMessage . mes ) ;
await sendExpressionCall ( overridePath . length === 0 ? currentLastMessage . name : overridePath , expression , true ) ;
forceUpdateVisualNovelMode ( ) ;
} catch ( error ) {
console . debug ( ` Setting expression override for ${ avatarFileName } failed with error: ${ error } ` ) ;
2024-04-12 07:49:09 +02:00
} finally {
inApiCall = false ;
2023-07-20 19:32:15 +02:00
}
}
async function onClickExpressionOverrideRemoveAllButton ( ) {
// Remove all the overrided entries from sprite cache
for ( const element of extension _settings . expressionOverrides ) {
delete spriteCache [ element . name ] ;
}
extension _settings . expressionOverrides = [ ] ;
saveSettingsDebounced ( ) ;
2023-12-02 19:04:51 +01:00
console . debug ( 'All expression image overrides have been cleared.' ) ;
2023-07-20 19:32:15 +02:00
// Refresh sprites list to use the default name if applicable
try {
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
await validateImages ( currentLastMessage . name , true ) ;
const expression = await getExpressionLabel ( currentLastMessage . mes ) ;
await sendExpressionCall ( currentLastMessage . name , expression , true ) ;
forceUpdateVisualNovelMode ( ) ;
console . debug ( extension _settings . expressionOverrides ) ;
} catch ( error ) {
console . debug ( ` The current expression could not be set because of error: ${ error } ` ) ;
}
}
async function onClickExpressionUploadPackButton ( ) {
const name = $ ( '#image_list' ) . data ( 'name' ) ;
const handleFileUploadChange = async ( e ) => {
const file = e . target . files [ 0 ] ;
if ( ! file ) {
return ;
}
const formData = new FormData ( ) ;
formData . append ( 'name' , name ) ;
formData . append ( 'avatar' , file ) ;
2023-09-16 16:28:28 +02:00
const { count } = await handleFileUpload ( '/api/sprites/upload-zip' , formData ) ;
2023-07-20 19:32:15 +02:00
toastr . success ( ` Uploaded ${ count } image(s) for ${ name } ` ) ;
// Reset the input
e . target . form . reset ( ) ;
2024-02-04 23:45:50 +01:00
// In Talkinghead mode, refresh the live char.
if ( isTalkingHeadEnabled ( ) && modules . includes ( 'talkinghead' ) ) {
await loadTalkingHead ( ) ;
}
2023-07-20 19:32:15 +02:00
} ;
$ ( '#expression_upload_pack' )
. off ( 'change' )
. on ( 'change' , handleFileUploadChange )
. trigger ( 'click' ) ;
}
async function onClickExpressionDelete ( event ) {
// Prevents the expression from being set
event . stopPropagation ( ) ;
2023-12-02 19:04:51 +01:00
const confirmation = await callPopup ( '<h3>Are you sure?</h3>Once deleted, it\'s gone forever!' , 'confirm' ) ;
2023-07-20 19:32:15 +02:00
if ( ! confirmation ) {
return ;
}
const id = $ ( this ) . closest ( '.expression_list_item' ) . attr ( 'id' ) ;
const name = $ ( '#image_list' ) . data ( 'name' ) ;
try {
2023-09-16 16:28:28 +02:00
await fetch ( '/api/sprites/delete' , {
2023-07-20 19:32:15 +02:00
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { name , label : id } ) ,
} ) ;
} catch ( error ) {
toastr . error ( 'Failed to delete image. Try again later.' ) ;
}
// Refresh sprites list
delete spriteCache [ name ] ;
2024-01-20 18:48:56 +01:00
await fetchImagesNoCache ( ) ;
2023-07-20 19:32:15 +02:00
await validateImages ( name ) ;
}
function setExpressionOverrideHtml ( forceClear = false ) {
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-09-07 16:48:12 +02:00
const avatarFileName = getFolderNameByMessage ( currentLastMessage ) ;
2023-07-20 19:32:15 +02:00
if ( ! avatarFileName ) {
return ;
}
const expressionOverride = extension _settings . expressionOverrides . find ( ( e ) =>
2023-12-02 21:06:57 +01:00
e . name == avatarFileName ,
2023-07-20 19:32:15 +02:00
) ;
if ( expressionOverride && expressionOverride . path ) {
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( expressionOverride . path ) ;
2023-07-20 19:32:15 +02:00
} else if ( expressionOverride ) {
delete extension _settings . expressionOverrides [ expressionOverride . name ] ;
}
if ( forceClear && ! expressionOverride ) {
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( '' ) ;
2023-07-20 19:32:15 +02:00
}
}
2024-01-20 18:48:56 +01:00
async function fetchImagesNoCache ( ) {
const promises = [ ] ;
$ ( '#image_list img' ) . each ( function ( ) {
const src = $ ( this ) . attr ( 'src' ) ;
if ( ! src ) {
return ;
}
const promise = fetch ( src , {
method : 'GET' ,
cache : 'no-cache' ,
headers : {
'Cache-Control' : 'no-cache' ,
'Pragma' : 'no-cache' ,
'Expires' : '0' ,
} ,
} ) ;
promises . push ( promise ) ;
} ) ;
return await Promise . allSettled ( promises ) ;
}
2024-04-12 07:49:09 +02:00
function migrateSettings ( ) {
2024-04-15 03:04:30 +02:00
if ( extension _settings . expressions . api === undefined ) {
extension _settings . expressions . api = EXPRESSION _API . extras ;
saveSettingsDebounced ( ) ;
}
2024-04-12 07:49:09 +02:00
if ( Object . keys ( extension _settings . expressions ) . includes ( 'local' ) ) {
if ( extension _settings . expressions . local ) {
2024-04-14 14:40:10 +02:00
extension _settings . expressions . api = EXPRESSION _API . local ;
2024-04-12 07:49:09 +02:00
}
delete extension _settings . expressions . local ;
saveSettingsDebounced ( ) ;
}
2024-04-14 16:13:54 +02:00
if ( extension _settings . expressions . llmPrompt === undefined ) {
extension _settings . expressions . llmPrompt = DEFAULT _LLM _PROMPT ;
saveSettingsDebounced ( ) ;
}
2024-04-12 07:49:09 +02:00
}
2024-04-07 06:12:52 +02:00
( async function ( ) {
2023-07-20 19:32:15 +02:00
function addExpressionImage ( ) {
const html = `
< div id = "expression-wrapper" >
2023-08-04 02:27:01 +02:00
< div id = "expression-holder" class = "expression-holder" style = "display:none;" >
2023-07-20 19:32:15 +02:00
< div id = "expression-holderheader" class = "fa-solid fa-grip drag-grabber" > < / d i v >
< img id = "expression-image" class = "expression" >
< / d i v >
< / d i v > ` ;
$ ( 'body' ) . append ( html ) ;
loadMovingUIState ( ) ;
}
function addVisualNovelMode ( ) {
const html = `
< div id = "visual-novel-wrapper" >
2023-12-02 20:11:06 +01:00
< / d i v > ` ;
2023-07-20 19:32:15 +02:00
const element = $ ( html ) ;
element . hide ( ) ;
$ ( 'body' ) . append ( element ) ;
}
2024-04-07 06:12:52 +02:00
async function addSettings ( ) {
2024-04-11 22:38:44 +02:00
const template = await renderExtensionTemplateAsync ( MODULE _NAME , 'settings' ) ;
2024-06-24 21:15:08 +02:00
$ ( '#expressions_container' ) . append ( template ) ;
2023-07-20 19:32:15 +02:00
$ ( '#expression_override_button' ) . on ( 'click' , onClickExpressionOverrideButton ) ;
$ ( '#expressions_show_default' ) . on ( 'input' , onExpressionsShowDefaultInput ) ;
$ ( '#expression_upload_pack_button' ) . on ( 'click' , onClickExpressionUploadPackButton ) ;
$ ( '#expressions_show_default' ) . prop ( 'checked' , extension _settings . expressions . showDefault ) . trigger ( 'input' ) ;
2024-04-11 21:09:05 +02:00
$ ( '#expression_translate' ) . prop ( 'checked' , extension _settings . expressions . translate ) . on ( 'input' , function ( ) {
extension _settings . expressions . translate = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
} ) ;
2023-07-20 19:32:15 +02:00
$ ( '#expression_override_cleanup_button' ) . on ( 'click' , onClickExpressionOverrideRemoveAllButton ) ;
$ ( document ) . on ( 'dragstart' , '.expression' , ( e ) => {
2023-12-02 20:11:06 +01:00
e . preventDefault ( ) ;
return false ;
} ) ;
2023-07-20 19:32:15 +02:00
$ ( document ) . on ( 'click' , '.expression_list_item' , onClickExpressionImage ) ;
$ ( document ) . on ( 'click' , '.expression_list_upload' , onClickExpressionUpload ) ;
$ ( document ) . on ( 'click' , '.expression_list_delete' , onClickExpressionDelete ) ;
2023-12-02 19:04:51 +01:00
$ ( window ) . on ( 'resize' , updateVisualNovelModeDebounced ) ;
$ ( '#open_chat_expressions' ) . hide ( ) ;
2023-07-31 07:52:30 +02:00
2023-08-03 12:05:21 +02:00
$ ( '#image_type_toggle' ) . on ( 'click' , function ( ) {
2023-09-07 16:48:12 +02:00
if ( this instanceof HTMLInputElement ) {
setTalkingHeadState ( this . checked ) ;
}
2023-07-31 07:52:30 +02:00
} ) ;
2023-09-14 20:30:02 +02:00
2024-04-07 06:12:52 +02:00
await renderAdditionalExpressionSettings ( ) ;
2024-04-14 16:13:54 +02:00
$ ( '#expression_api' ) . val ( extension _settings . expressions . api ? ? EXPRESSION _API . extras ) ;
$ ( '.expression_llm_prompt_block' ) . toggle ( extension _settings . expressions . api === EXPRESSION _API . llm ) ;
$ ( '#expression_llm_prompt' ) . val ( extension _settings . expressions . llmPrompt ? ? '' ) ;
$ ( '#expression_llm_prompt' ) . on ( 'input' , function ( ) {
extension _settings . expressions . llmPrompt = $ ( this ) . val ( ) ;
saveSettingsDebounced ( ) ;
} ) ;
2024-04-14 18:53:40 +02:00
$ ( '#expression_llm_prompt_restore' ) . on ( 'click' , function ( ) {
$ ( '#expression_llm_prompt' ) . val ( DEFAULT _LLM _PROMPT ) ;
extension _settings . expressions . llmPrompt = DEFAULT _LLM _PROMPT ;
saveSettingsDebounced ( ) ;
} ) ;
2023-09-14 20:30:02 +02:00
$ ( '#expression_custom_add' ) . on ( 'click' , onClickExpressionAddCustom ) ;
$ ( '#expression_custom_remove' ) . on ( 'click' , onClickExpressionRemoveCustom ) ;
2024-04-11 22:38:44 +02:00
$ ( '#expression_fallback' ) . on ( 'change' , onExpressionFallbackChanged ) ;
2024-07-27 12:57:39 +02:00
$ ( '#expression_api' ) . on ( 'change' , onExpressionApiChanged ) ;
2023-07-20 19:32:15 +02:00
}
2024-02-04 23:46:44 +01:00
// 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.
2024-02-07 18:28:34 +01:00
document . addEventListener ( 'visibilitychange' , function ( event ) {
2024-02-04 23:46:44 +01:00
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.");
}
} ) ;
}
} ) ;
2023-07-20 19:32:15 +02:00
addExpressionImage ( ) ;
addVisualNovelMode ( ) ;
2024-04-12 07:49:09 +02:00
migrateSettings ( ) ;
2024-04-07 06:12:52 +02:00
await addSettings ( ) ;
2023-07-20 19:32:15 +02:00
const wrapper = new ModuleWorkerWrapper ( moduleWorker ) ;
const updateFunction = wrapper . update . bind ( wrapper ) ;
setInterval ( updateFunction , UPDATE _INTERVAL ) ;
moduleWorker ( ) ;
2024-02-04 23:44:26 +01:00
// For setting the Talkinghead talking animation on/off quickly enough for realtime use, we need another timer on a shorter schedule.
2024-01-09 18:52:49 +01:00
const wrapperTalkingState = new ModuleWorkerWrapper ( updateTalkingState ) ;
const updateTalkingStateFunction = wrapperTalkingState . update . bind ( wrapperTalkingState ) ;
2024-01-07 23:47:59 +01:00
setInterval ( updateTalkingStateFunction , TALKINGCHECK _UPDATE _INTERVAL ) ;
updateTalkingState ( ) ;
2023-12-02 20:11:06 +01:00
dragElement ( $ ( '#expression-holder' ) ) ;
2023-07-20 19:32:15 +02:00
eventSource . on ( event _types . CHAT _CHANGED , ( ) => {
2023-10-16 08:12:12 +02:00
// character changed
2023-10-29 22:15:40 +01:00
removeExpression ( ) ;
spriteCache = { } ;
2023-12-30 12:35:10 +01:00
lastExpression = { } ;
2023-10-16 08:12:12 +02:00
2023-10-29 22:15:40 +01:00
//clear expression
let imgElement = document . getElementById ( 'expression-image' ) ;
if ( imgElement && imgElement instanceof HTMLImageElement ) {
2023-12-02 19:04:51 +01:00
imgElement . src = '' ;
2023-10-29 22:15:40 +01:00
}
2023-10-16 08:12:12 +02:00
2023-10-29 22:15:40 +01:00
//set checkbox to global var
$ ( '#image_type_toggle' ) . prop ( 'checked' , extension _settings . expressions . talkinghead ) ;
if ( extension _settings . expressions . talkinghead ) {
setTalkingHeadState ( extension _settings . expressions . talkinghead ) ;
2023-10-16 08:12:12 +02:00
}
2023-07-20 19:32:15 +02:00
setExpressionOverrideHtml ( ) ;
if ( isVisualNovelMode ( ) ) {
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
}
2023-10-23 00:56:27 +02:00
updateFunction ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
eventSource . on ( event _types . MOVABLE _PANELS _RESET , updateVisualNovelModeDebounced ) ;
eventSource . on ( event _types . GROUP _UPDATED , updateVisualNovelModeDebounced ) ;
2024-09-16 11:17:23 +02:00
eventSource . on ( event _types . EXTRAS _CONNECTED , ( ) => {
if ( extension _settings . expressions . talkinghead ) {
setTalkingHeadState ( extension _settings . expressions . talkinghead ) ;
}
} ) ;
2024-06-21 20:04:55 +02:00
const localEnumProviders = {
expressions : ( ) => getCachedExpressions ( ) . map ( expression => {
const isCustom = extension _settings . expressions . custom ? . includes ( expression ) ;
return new SlashCommandEnumValue ( expression , null , isCustom ? enumTypes . name : enumTypes . enum , isCustom ? 'C' : 'D' ) ;
} ) ,
2024-06-23 14:23:03 +02:00
} ;
2024-06-21 20:04:55 +02:00
2024-06-15 17:21:44 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'sprite' ,
2024-05-12 21:15:05 +02:00
aliases : [ 'emote' ] ,
callback : setSpriteSlashCommand ,
unnamedArgumentList : [
2024-06-15 17:21:44 +02:00
SlashCommandArgument . fromProps ( {
description : 'spriteId' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-21 20:04:55 +02:00
enumProvider : localEnumProviders . expressions ,
2024-06-15 17:21:44 +02:00
} ) ,
2024-05-12 21:15:05 +02:00
] ,
helpString : 'Force sets the sprite for the current character.' ,
2024-09-05 00:24:10 +02:00
returns : 'the currently set sprite label after setting it.' ,
2024-05-12 21:15:05 +02:00
} ) ) ;
2024-06-15 17:21:44 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'spriteoverride' ,
2024-05-12 21:15:05 +02:00
aliases : [ 'costume' ] ,
callback : setSpriteSetCommand ,
unnamedArgumentList : [
new SlashCommandArgument (
'optional folder' , [ ARGUMENT _TYPE . STRING ] , false ,
) ,
] ,
helpString : '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.' ,
} ) ) ;
2024-06-15 17:21:44 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'lastsprite' ,
callback : ( _ , value ) => lastExpression [ String ( value ) . trim ( ) ] ? ? '' ,
2024-09-05 00:24:10 +02:00
returns : 'the last set sprite / expression for the named character.' ,
2024-05-12 21:15:05 +02:00
unnamedArgumentList : [
2024-06-17 07:04:10 +02:00
SlashCommandArgument . fromProps ( {
description : 'character name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
2024-06-23 14:23:03 +02:00
enumProvider : commonEnumProviders . characters ( 'character' ) ,
2024-06-17 07:04:10 +02:00
} ) ,
2024-05-12 21:15:05 +02:00
] ,
helpString : 'Returns the last set sprite / expression for the named character.' ,
} ) ) ;
2024-06-15 17:21:44 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'th' ,
2024-05-12 21:15:05 +02:00
callback : toggleTalkingHeadCommand ,
aliases : [ 'talkinghead' ] ,
helpString : 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.' ,
2024-09-05 00:24:10 +02:00
returns : 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.' ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'classify-expressions' ,
aliases : [ 'expressions' ] ,
2024-09-05 09:48:46 +02:00
callback : async ( args ) => {
const list = await getExpressionsList ( ) ;
switch ( String ( args . format ) . toLowerCase ( ) ) {
case 'json' :
return JSON . stringify ( list ) ;
default :
return list . join ( ', ' ) ;
}
} ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'format' ,
description : 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
enumList : [
new SlashCommandEnumValue ( 'plain' , null , enumTypes . enum , ', ' ) ,
new SlashCommandEnumValue ( 'json' , null , enumTypes . enum , '[]' ) ,
] ,
} ) ,
] ,
2024-09-05 00:24:10 +02:00
returns : 'The comma-separated list of available expressions, including custom expressions.' ,
helpString : 'Returns a list of available expressions, including custom expressions.' ,
2024-05-12 21:15:05 +02:00
} ) ) ;
2024-06-15 17:21:44 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'classify' ,
2024-09-05 00:06:14 +02:00
callback : classifyCallback ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'api' ,
description : 'The Classifier API to classify with. If not specified, the configured one will be used.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
enumList : Object . keys ( EXPRESSION _API ) . map ( api => new SlashCommandEnumValue ( api , null , enumTypes . enum ) ) ,
} ) ,
2024-09-05 00:13:54 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'prompt' ,
description : 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
} ) ,
2024-09-05 00:06:14 +02:00
] ,
2024-05-12 21:15:05 +02:00
unnamedArgumentList : [
new SlashCommandArgument (
'text' , [ ARGUMENT _TYPE . STRING ] , true ,
) ,
] ,
returns : 'emotion classification label for the given text' ,
helpString : `
< div >
Performs an emotion classification of the given text and returns a label .
< / d i v >
2024-09-05 00:06:14 +02:00
< div >
Allows to specify which Classifier API to perform the classification with .
< / d i v >
2024-05-12 21:15:05 +02:00
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / c l a s s i f y I a m s o h a p p y t o d a y ! < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2023-07-20 19:32:15 +02:00
} ) ( ) ;