2023-11-09 01:57:40 +01:00
import { callPopup , cancelTtsPlay , eventSource , event _types , name2 , saveSettingsDebounced } from '../../../script.js'
2023-08-13 17:43:17 +02:00
import { ModuleWorkerWrapper , doExtrasFetch , extension _settings , getApiUrl , getContext , modules } from '../../extensions.js'
2023-07-20 19:32:15 +02:00
import { escapeRegex , getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
import { SileroTtsProvider } from './silerotts.js'
2023-08-14 04:03:28 +02:00
import { CoquiTtsProvider } from './coqui.js'
2023-07-20 19:32:15 +02:00
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
import { power _user } from '../../power-user.js'
2023-11-09 01:57:40 +01:00
import { registerSlashCommand } from '../../slash-commands.js'
2023-11-12 01:28:03 +01:00
import { OpenAITtsProvider } from './openai.js'
2023-08-11 07:43:53 +02:00
export { talkingAnimation } ;
2023-07-20 19:32:15 +02:00
const UPDATE _INTERVAL = 1000
2023-08-23 15:27:53 +02:00
let voiceMapEntries = [ ]
2023-07-20 19:32:15 +02:00
let voiceMap = { } // {charName:voiceid, charName2:voiceid2}
let audioControl
2023-07-31 11:21:32 +02:00
let storedvalue = false ;
2023-07-20 19:32:15 +02:00
let lastCharacterId = null
let lastGroupId = null
let lastChatId = null
let lastMessageHash = null
2023-09-01 16:58:33 +02:00
const DEFAULT _VOICE _MARKER = '[Default Voice]' ;
2023-09-04 13:21:22 +02:00
const DISABLED _VOICE _MARKER = 'disabled' ;
2023-09-01 16:58:33 +02:00
2023-07-20 19:32:15 +02:00
export function getPreviewString ( lang ) {
const previewStrings = {
'en-US' : 'The quick brown fox jumps over the lazy dog' ,
'en-GB' : 'Sphinx of black quartz, judge my vow' ,
'fr-FR' : 'Portez ce vieux whisky au juge blond qui fume' ,
'de-DE' : 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich' ,
'it-IT' : "Pranzo d'acqua fa volti sghembi" ,
'es-ES' : 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón' ,
'es-MX' : 'Fabio me exige, sin tapujos, que añada cerveza al whisky' ,
'ru-RU' : 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!' ,
'pt-BR' : 'Vejo xá gritando que fez show sem playback.' ,
'pt-PR' : 'Todo pajé vulgar faz boquinha sexy com kiwi.' ,
'uk-UA' : "Фабрикуймо гідність, лящім їжею, ґав хапаймо, з'єднавці чаш!" ,
'pl-PL' : 'Pchnąć w tę łódź jeża lub ośm skrzyń fig' ,
'cs-CZ' : 'Příliš žluťoučký kůň úpěl ďábelské ódy' ,
'sk-SK' : 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny' ,
'hu-HU' : 'Árvíztűrő tükörfúrógép' ,
'tr-TR' : 'Pijamalı hasta yağı z şoföre çabucak güvendi' ,
'nl-NL' : 'De waard heeft een kalfje en een pinkje opgegeten' ,
'sv-SE' : 'Yxskaftbud, ge vårbygd, zinkqvarn' ,
'da-DK' : 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon' ,
'ja-JP' : 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす' ,
'ko-KR' : '가나다라마바사아자차카타파하' ,
'zh-CN' : '我能吞下玻璃而不伤身体' ,
'ro-RO' : 'Muzicologă în bej vând whisky și tequila, preț fix' ,
'bg-BG' : 'Щъркелите с е разпръснаха по цялото небе' ,
'el-GR' : 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός' ,
'fi-FI' : 'Voi veljet, miksi juuri teille myin nämä vehkeet?' ,
'he-IL' : 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"' ,
'id-ID' : 'Jangkrik itu memang enak, apalagi kalau digoreng' ,
'ms-MY' : 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa' ,
'th-TH' : 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ' ,
'vi-VN' : 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh' ,
'ar-SA' : 'أَبْجَدِيَّة عَرَبِيَّة' ,
'hi-IN' : 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा' ,
}
const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
return previewStrings [ lang ] ? ? fallbackPreview ;
}
let ttsProviders = {
ElevenLabs : ElevenLabsTtsProvider ,
Silero : SileroTtsProvider ,
System : SystemTtsProvider ,
2023-07-25 02:59:08 +02:00
Coqui : CoquiTtsProvider ,
2023-07-20 19:32:15 +02:00
Edge : EdgeTtsProvider ,
Novel : NovelTtsProvider ,
2023-11-12 01:28:03 +01:00
OpenAI : OpenAITtsProvider ,
2023-07-20 19:32:15 +02:00
}
let ttsProvider
let ttsProviderName
2023-11-08 16:40:47 +01:00
let ttsLastMessage = null ;
2023-07-20 19:32:15 +02:00
async function onNarrateOneMessage ( ) {
audioElement . src = '/sounds/silence.mp3' ;
const context = getContext ( ) ;
const id = $ ( this ) . closest ( '.mes' ) . attr ( 'mesid' ) ;
const message = context . chat [ id ] ;
if ( ! message ) {
return ;
}
resetTtsPlayback ( )
ttsJobQueue . push ( message ) ;
moduleWorker ( ) ;
}
2023-11-09 01:57:40 +01:00
async function onNarrateText ( args , text ) {
if ( ! text ) {
return ;
}
audioElement . src = '/sounds/silence.mp3' ;
// To load all characters in the voice map, set unrestricted to true
await initVoiceMap ( true ) ;
const baseName = args ? . voice || name2 ;
const name = ( baseName === 'SillyTavern System' ? DEFAULT _VOICE _MARKER : baseName ) || DEFAULT _VOICE _MARKER ;
const voiceMapEntry = voiceMap [ name ] === DEFAULT _VOICE _MARKER
? voiceMap [ DEFAULT _VOICE _MARKER ]
: voiceMap [ name ] ;
if ( ! voiceMapEntry || voiceMapEntry === DISABLED _VOICE _MARKER ) {
toastr . info ( ` Specified voice for ${ name } was not found. Check the TTS extension settings. ` ) ;
return ;
}
resetTtsPlayback ( )
ttsJobQueue . push ( { mes : text , name : name } ) ;
await moduleWorker ( ) ;
// Return back to the chat voices
await initVoiceMap ( false ) ;
}
2023-07-20 19:32:15 +02:00
async function moduleWorker ( ) {
// Primarily determining when to add new chat to the TTS queue
const enabled = $ ( '#tts_enabled' ) . is ( ':checked' )
$ ( 'body' ) . toggleClass ( 'tts' , enabled ) ;
if ( ! enabled ) {
return
}
const context = getContext ( )
const chat = context . chat
processTtsQueue ( )
processAudioJobQueue ( )
updateUiAudioPlayState ( )
// Auto generation is disabled
if ( extension _settings . tts . auto _generation == false ) {
return
}
// no characters or group selected
if ( ! context . groupId && context . characterId === undefined ) {
return
}
// Chat changed
if (
context . chatId !== lastChatId
) {
currentMessageNumber = context . chat . length ? context . chat . length : 0
saveLastValues ( )
2023-11-09 00:30:54 +01:00
// Force to speak on the first message in the new chat
if ( context . chat . length === 1 ) {
lastMessageHash = - 1 ;
}
2023-07-20 19:32:15 +02:00
return
}
// take the count of messages
let lastMessageNumber = context . chat . length ? context . chat . length : 0
// There's no new messages
let diff = lastMessageNumber - currentMessageNumber
let hashNew = getStringHash ( ( chat . length && chat [ chat . length - 1 ] . mes ) ? ? '' )
2023-11-08 16:40:47 +01:00
// if messages got deleted, diff will be < 0
if ( diff < 0 ) {
// necessary actions will be taken by the onChatDeleted() handler
return
}
2023-07-20 19:32:15 +02:00
if ( diff == 0 && hashNew === lastMessageHash ) {
return
}
2023-11-08 16:40:47 +01:00
// clone message object, as things go haywire if message object is altered below (it's passed by reference)
2023-11-08 19:08:42 +01:00
const message = structuredClone ( chat [ chat . length - 1 ] )
2023-11-08 16:40:47 +01:00
// if last message within current message, message got extended. only send diff to TTS.
if ( ttsLastMessage !== null && message . mes . indexOf ( ttsLastMessage ) !== - 1 ) {
let tmp = message . mes
message . mes = message . mes . replace ( ttsLastMessage , '' )
ttsLastMessage = tmp
} else {
ttsLastMessage = message . mes
}
2023-07-20 19:32:15 +02:00
// We're currently swiping or streaming. Don't generate voice
if (
! message ||
message . mes === '...' ||
message . mes === '' ||
( context . streamingProcessor && ! context . streamingProcessor . isFinished )
) {
return
}
// Don't generate if message doesn't have a display text
if ( extension _settings . tts . narrate _translated _only && ! ( message ? . extra ? . display _text ) ) {
return ;
}
2023-08-29 14:27:22 +02:00
// Don't generate if message is a user message and user message narration is disabled
if ( message . is _user && ! extension _settings . tts . narrate _user ) {
return ;
}
2023-07-20 19:32:15 +02:00
// New messages, add new chat to history
lastMessageHash = hashNew
currentMessageNumber = lastMessageNumber
console . debug (
` Adding message from ${ message . name } for TTS processing: " ${ message . mes } " `
)
ttsJobQueue . push ( message )
}
2023-07-31 11:21:32 +02:00
function talkingAnimation ( switchValue ) {
2023-08-13 17:43:17 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( "Talking Animation module not loaded" ) ;
return ;
}
2023-07-31 19:56:05 +02:00
const apiUrl = getApiUrl ( ) ;
const animationType = switchValue ? "start" : "stop" ;
if ( switchValue !== storedvalue ) {
try {
console . log ( animationType + " Talking Animation" ) ;
2023-08-10 23:52:14 +02:00
doExtrasFetch ( ` ${ apiUrl } /api/talkinghead/ ${ animationType } _talking ` ) ;
2023-07-31 19:56:05 +02:00
storedvalue = switchValue ; // Update the storedvalue to the current switchValue
} catch ( error ) {
// Handle the error here or simply ignore it to prevent logging
}
}
2023-08-11 07:43:53 +02:00
updateUiAudioPlayState ( )
2023-07-31 11:21:32 +02:00
}
2023-07-20 19:32:15 +02:00
function resetTtsPlayback ( ) {
// Stop system TTS utterance
cancelTtsPlay ( ) ;
// Clear currently processing jobs
currentTtsJob = null ;
currentAudioJob = null ;
// Reset audio element
audioElement . currentTime = 0 ;
audioElement . src = '' ;
// Clear any queue items
ttsJobQueue . splice ( 0 , ttsJobQueue . length ) ;
audioJobQueue . splice ( 0 , audioJobQueue . length ) ;
// Set audio ready to process again
audioQueueProcessorReady = true ;
}
function isTtsProcessing ( ) {
let processing = false
// Check job queues
2023-08-29 14:27:22 +02:00
if ( ttsJobQueue . length > 0 || audioJobQueue . length > 0 ) {
2023-07-20 19:32:15 +02:00
processing = true
}
// Check current jobs
if ( currentTtsJob != null || currentAudioJob != null ) {
processing = true
}
return processing
}
function debugTtsPlayback ( ) {
console . log ( JSON . stringify (
{
"ttsProviderName" : ttsProviderName ,
2023-08-23 15:27:53 +02:00
"voiceMap" : voiceMap ,
2023-07-20 19:32:15 +02:00
"currentMessageNumber" : currentMessageNumber ,
"audioPaused" : audioPaused ,
"audioJobQueue" : audioJobQueue ,
"currentAudioJob" : currentAudioJob ,
"audioQueueProcessorReady" : audioQueueProcessorReady ,
"ttsJobQueue" : ttsJobQueue ,
"currentTtsJob" : currentTtsJob ,
"ttsConfig" : extension _settings . tts
}
) )
}
window . debugTtsPlayback = debugTtsPlayback
//##################//
// Audio Control //
//##################//
let audioElement = new Audio ( )
2023-11-17 00:30:32 +01:00
audioElement . id = 'tts_audio'
2023-07-20 19:32:15 +02:00
audioElement . autoplay = true
let audioJobQueue = [ ]
let currentAudioJob
let audioPaused = false
let audioQueueProcessorReady = true
let lastAudioPosition = 0
async function playAudioData ( audioBlob ) {
// Since current audio job can be cancelled, don't playback if it is null
if ( currentAudioJob == null ) {
console . log ( "Cancelled TTS playback because currentAudioJob was null" )
}
const reader = new FileReader ( )
reader . onload = function ( e ) {
const srcUrl = e . target . result
audioElement . src = srcUrl
}
reader . readAsDataURL ( audioBlob )
audioElement . addEventListener ( 'ended' , completeCurrentAudioJob )
audioElement . addEventListener ( 'canplay' , ( ) => {
console . debug ( ` Starting TTS playback ` )
audioElement . play ( )
} )
}
window [ 'tts_preview' ] = function ( id ) {
const audio = document . getElementById ( id )
if ( audio && ! $ ( audio ) . data ( 'disabled' ) ) {
audio . play ( )
}
else {
ttsProvider . previewTtsVoice ( id )
}
}
async function onTtsVoicesClick ( ) {
let popupText = ''
try {
2023-08-26 05:52:55 +02:00
const voiceIds = await ttsProvider . fetchTtsVoiceObjects ( )
2023-07-20 19:32:15 +02:00
for ( const voice of voiceIds ) {
popupText += `
< div class = "voice_preview" >
< span class = "voice_lang" > $ { voice . lang || '' } < / s p a n >
< b class = "voice_name" > $ { voice . name } < / b >
< i onclick = "tts_preview('${voice.voice_id}')" class = "fa-solid fa-play" > < / i >
< / d i v > `
if ( voice . preview _url ) {
popupText += ` <audio id=" ${ voice . voice _id } " src=" ${ voice . preview _url } " data-disabled=" ${ voice . preview _url == false } "></audio> `
}
}
} catch {
popupText = 'Could not load voices list. Check your API key.'
}
callPopup ( popupText , 'text' )
}
function updateUiAudioPlayState ( ) {
if ( extension _settings . tts . enabled == true ) {
$ ( '#ttsExtensionMenuItem' ) . show ( ) ;
let img
// Give user feedback that TTS is active by setting the stop icon if processing or playing
if ( ! audioElement . paused || isTtsProcessing ( ) ) {
img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton'
} else {
img = 'fa-solid fa-circle-play extensionsMenuExtensionButton'
}
$ ( '#tts_media_control' ) . attr ( 'class' , img ) ;
} else {
$ ( '#ttsExtensionMenuItem' ) . hide ( ) ;
}
}
function onAudioControlClicked ( ) {
audioElement . src = '/sounds/silence.mp3' ;
let context = getContext ( )
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
if ( ! audioElement . paused || isTtsProcessing ( ) ) {
resetTtsPlayback ( )
2023-08-15 12:25:42 +02:00
talkingAnimation ( false ) ;
2023-07-20 19:32:15 +02:00
} else {
// Default play behavior if not processing or playing is to play the last message.
ttsJobQueue . push ( context . chat [ context . chat . length - 1 ] )
}
updateUiAudioPlayState ( )
}
function addAudioControl ( ) {
$ ( '#extensionsMenu' ) . prepend ( `
< div id = "ttsExtensionMenuItem" class = "list-group-item flex-container flexGap5" >
< div id = "tts_media_control" class = "extensionsMenuExtensionButton " / > < / d i v >
TTS Playback
< / d i v > ` )
$ ( '#ttsExtensionMenuItem' ) . attr ( 'title' , 'TTS play/pause' ) . on ( 'click' , onAudioControlClicked )
audioControl = document . getElementById ( 'tts_media_control' )
updateUiAudioPlayState ( )
}
function completeCurrentAudioJob ( ) {
audioQueueProcessorReady = true
currentAudioJob = null
lastAudioPosition = 0
2023-08-11 07:43:53 +02:00
talkingAnimation ( false ) //stop lip animation
2023-07-20 19:32:15 +02:00
// updateUiPlayState();
}
/ * *
* Accepts an HTTP response containing audio / mpeg data , and puts the data as a Blob ( ) on the queue for playback
* @ param { * } response
* /
async function addAudioJob ( response ) {
const audioData = await response . blob ( )
if ( ! audioData . type in [ 'audio/mpeg' , 'audio/wav' , 'audio/x-wav' , 'audio/wave' , 'audio/webm' ] ) {
throw ` TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${ audioData . type } `
}
audioJobQueue . push ( audioData )
console . debug ( 'Pushed audio job to queue.' )
}
async function processAudioJobQueue ( ) {
// Nothing to do, audio not completed, or audio paused - stop processing.
if ( audioJobQueue . length == 0 || ! audioQueueProcessorReady || audioPaused ) {
return
}
try {
audioQueueProcessorReady = false
currentAudioJob = audioJobQueue . pop ( )
playAudioData ( currentAudioJob )
2023-07-31 11:21:32 +02:00
talkingAnimation ( true )
2023-07-20 19:32:15 +02:00
} catch ( error ) {
console . error ( error )
audioQueueProcessorReady = true
}
}
//################//
// TTS Control //
//################//
let ttsJobQueue = [ ]
let currentTtsJob // Null if nothing is currently being processed
let currentMessageNumber = 0
function completeTtsJob ( ) {
console . info ( ` Current TTS job for ${ currentTtsJob . name } completed. ` )
currentTtsJob = null
}
function saveLastValues ( ) {
const context = getContext ( )
lastGroupId = context . groupId
lastCharacterId = context . characterId
lastChatId = context . chatId
lastMessageHash = getStringHash (
( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? ''
)
}
2023-08-09 03:30:26 +02:00
async function tts ( text , voiceId , char ) {
let response = await ttsProvider . generateTts ( text , voiceId )
// RVC injection
2023-10-15 16:27:11 +02:00
if ( extension _settings . rvc . enabled && typeof window [ 'rvcVoiceConversion' ] === 'function' )
response = await window [ 'rvcVoiceConversion' ] ( response , char , text )
2023-08-09 03:30:26 +02:00
2023-07-20 19:32:15 +02:00
addAudioJob ( response )
completeTtsJob ( )
}
async function processTtsQueue ( ) {
// Called each moduleWorker iteration to pull chat messages from queue
if ( currentTtsJob || ttsJobQueue . length <= 0 || audioPaused ) {
return
}
console . debug ( 'New message found, running TTS' )
currentTtsJob = ttsJobQueue . shift ( )
2023-10-22 13:46:54 +02:00
let text = extension _settings . tts . narrate _translated _only ? ( currentTtsJob ? . extra ? . display _text || currentTtsJob . mes ) : currentTtsJob . mes
2023-07-20 19:32:15 +02:00
text = extension _settings . tts . narrate _dialogues _only
? text . replace ( /\*[^\*]*?(\*|$)/g , '' ) . trim ( ) // remove asterisks content
: text . replaceAll ( '*' , '' ) . trim ( ) // remove just the asterisks
if ( extension _settings . tts . narrate _quoted _only ) {
const special _quotes = /[“”]/g ; // Extend this regex to include other special quotes
text = text . replace ( special _quotes , '"' ) ;
const matches = text . match ( /".*?"/g ) ; // Matches text inside double quotes, non-greedily
const partJoiner = ( ttsProvider ? . separator || ' ... ' ) ;
text = matches ? matches . join ( partJoiner ) : text ;
}
console . log ( ` TTS: ${ text } ` )
const char = currentTtsJob . name
// Remove character name from start of the line if power user setting is disabled
if ( char && ! power _user . allow _name2 _display ) {
const escapedChar = escapeRegex ( char ) ;
text = text . replace ( new RegExp ( ` ^ ${ escapedChar } : ` , 'gm' ) , '' ) ;
}
try {
if ( ! text ) {
console . warn ( 'Got empty text in TTS queue job.' ) ;
completeTtsJob ( )
return ;
}
2023-09-02 09:23:18 +02:00
const voiceMapEntry = voiceMap [ char ] === DEFAULT _VOICE _MARKER ? voiceMap [ DEFAULT _VOICE _MARKER ] : voiceMap [ char ]
2023-09-01 16:58:33 +02:00
2023-09-04 13:21:22 +02:00
if ( ! voiceMapEntry || voiceMapEntry === DISABLED _VOICE _MARKER ) {
2023-07-20 19:32:15 +02:00
throw ` ${ char } not in voicemap. Configure character in extension settings voice map `
}
2023-09-01 16:58:33 +02:00
const voice = await ttsProvider . getVoice ( voiceMapEntry )
2023-07-20 19:32:15 +02:00
const voiceId = voice . voice _id
if ( voiceId == null ) {
toastr . error ( ` Specified voice for ${ char } was not found. Check the TTS extension settings. ` )
throw ` Unable to attain voiceId for ${ char } `
}
2023-08-09 03:30:26 +02:00
tts ( text , voiceId , char )
2023-07-20 19:32:15 +02:00
} catch ( error ) {
console . error ( error )
currentTtsJob = null
}
}
// Secret function for now
async function playFullConversation ( ) {
const context = getContext ( )
const chat = context . chat
ttsJobQueue = chat
}
window . playFullConversation = playFullConversation
//#############################//
// Extension UI and Settings //
//#############################//
function loadSettings ( ) {
if ( Object . keys ( extension _settings . tts ) . length === 0 ) {
Object . assign ( extension _settings . tts , defaultSettings )
}
2023-08-28 20:46:41 +02:00
for ( const key in defaultSettings ) {
if ( ! ( key in extension _settings . tts ) ) {
extension _settings . tts [ key ] = defaultSettings [ key ]
}
}
2023-08-23 15:27:53 +02:00
$ ( '#tts_provider' ) . val ( extension _settings . tts . currentProvider )
2023-07-20 19:32:15 +02:00
$ ( '#tts_enabled' ) . prop (
'checked' ,
extension _settings . tts . enabled
)
$ ( '#tts_narrate_dialogues' ) . prop ( 'checked' , extension _settings . tts . narrate _dialogues _only )
$ ( '#tts_narrate_quoted' ) . prop ( 'checked' , extension _settings . tts . narrate _quoted _only )
$ ( '#tts_auto_generation' ) . prop ( 'checked' , extension _settings . tts . auto _generation )
$ ( '#tts_narrate_translated_only' ) . prop ( 'checked' , extension _settings . tts . narrate _translated _only ) ;
2023-08-29 14:27:22 +02:00
$ ( '#tts_narrate_user' ) . prop ( 'checked' , extension _settings . tts . narrate _user ) ;
2023-07-20 19:32:15 +02:00
$ ( 'body' ) . toggleClass ( 'tts' , extension _settings . tts . enabled ) ;
}
const defaultSettings = {
voiceMap : '' ,
ttsEnabled : false ,
currentProvider : "ElevenLabs" ,
2023-08-29 14:27:22 +02:00
auto _generation : true ,
narrate _user : false ,
2023-07-20 19:32:15 +02:00
}
function setTtsStatus ( status , success ) {
$ ( '#tts_status' ) . text ( status )
if ( success ) {
$ ( '#tts_status' ) . removeAttr ( 'style' )
} else {
$ ( '#tts_status' ) . css ( 'color' , 'red' )
}
}
2023-08-25 15:27:43 +02:00
function onRefreshClick ( ) {
2023-07-20 19:32:15 +02:00
Promise . all ( [
2023-08-26 05:52:55 +02:00
ttsProvider . onRefreshClick ( ) ,
2023-08-23 15:27:53 +02:00
// updateVoiceMap()
2023-07-20 19:32:15 +02:00
] ) . then ( ( ) => {
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings
saveSettingsDebounced ( )
setTtsStatus ( 'Successfully applied settings' , true )
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` )
2023-08-23 15:27:53 +02:00
initVoiceMap ( )
updateVoiceMap ( )
2023-07-20 19:32:15 +02:00
} ) . catch ( error => {
console . error ( error )
setTtsStatus ( error , false )
} )
}
function onEnableClick ( ) {
extension _settings . tts . enabled = $ ( '#tts_enabled' ) . is (
':checked'
)
updateUiAudioPlayState ( )
saveSettingsDebounced ( )
}
2023-08-09 03:30:26 +02:00
2023-07-20 19:32:15 +02:00
function onAutoGenerationClick ( ) {
2023-08-29 14:27:22 +02:00
extension _settings . tts . auto _generation = ! ! $ ( '#tts_auto_generation' ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( )
}
function onNarrateDialoguesClick ( ) {
2023-08-29 14:27:22 +02:00
extension _settings . tts . narrate _dialogues _only = ! ! $ ( '#tts_narrate_dialogues' ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( )
}
2023-08-29 14:27:22 +02:00
function onNarrateUserClick ( ) {
extension _settings . tts . narrate _user = ! ! $ ( '#tts_narrate_user' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2023-07-20 19:32:15 +02:00
function onNarrateQuotedClick ( ) {
2023-08-29 14:27:22 +02:00
extension _settings . tts . narrate _quoted _only = ! ! $ ( '#tts_narrate_quoted' ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( )
}
function onNarrateTranslatedOnlyClick ( ) {
2023-08-29 14:27:22 +02:00
extension _settings . tts . narrate _translated _only = ! ! $ ( '#tts_narrate_translated_only' ) . prop ( 'checked' ) ;
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( ) ;
}
//##############//
// TTS Provider //
//##############//
2023-08-23 15:27:53 +02:00
async function loadTtsProvider ( provider ) {
2023-07-20 19:32:15 +02:00
//Clear the current config and add new config
$ ( "#tts_provider_settings" ) . html ( "" )
if ( ! provider ) {
2023-08-23 15:27:53 +02:00
return
2023-07-20 19:32:15 +02:00
}
2023-08-28 20:46:41 +02:00
2023-07-20 19:32:15 +02:00
// Init provider references
extension _settings . tts . currentProvider = provider
ttsProviderName = provider
ttsProvider = new ttsProviders [ provider ]
// Init provider settings
$ ( '#tts_provider_settings' ) . append ( ttsProvider . settingsHtml )
if ( ! ( ttsProviderName in extension _settings . tts ) ) {
console . warn ( ` Provider ${ ttsProviderName } not in Extension Settings, initiatilizing provider in settings ` )
extension _settings . tts [ ttsProviderName ] = { }
}
2023-08-28 20:58:46 +02:00
await ttsProvider . loadSettings ( extension _settings . tts [ ttsProviderName ] )
await initVoiceMap ( )
2023-07-20 19:32:15 +02:00
}
function onTtsProviderChange ( ) {
const ttsProviderSelection = $ ( '#tts_provider' ) . val ( )
2023-08-23 15:27:53 +02:00
extension _settings . tts . currentProvider = ttsProviderSelection
2023-07-20 19:32:15 +02:00
loadTtsProvider ( ttsProviderSelection )
}
2023-08-23 15:27:53 +02:00
// Ensure that TTS provider settings are saved to extension settings.
2023-08-26 05:52:55 +02:00
export function saveTtsProviderSettings ( ) {
2023-08-14 05:03:42 +02:00
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings
2023-10-22 13:46:54 +02:00
updateVoiceMap ( )
2023-07-20 19:32:15 +02:00
saveSettingsDebounced ( )
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` )
}
2023-08-23 15:27:53 +02:00
//###################//
// voiceMap Handling //
//###################//
async function onChatChanged ( ) {
await resetTtsPlayback ( )
await initVoiceMap ( )
2023-11-08 19:08:42 +01:00
ttsLastMessage = null
2023-08-23 15:27:53 +02:00
}
2023-11-08 16:40:47 +01:00
async function onChatDeleted ( ) {
const context = getContext ( )
// update internal references to new last message
lastChatId = context . chatId
currentMessageNumber = context . chat . length ? context . chat . length : 0
// compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
let messageHash = getStringHash ( ( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' )
if ( messageHash === lastMessageHash ) {
return
}
lastMessageHash = messageHash
ttsLastMessage = ( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' ;
// stop any tts playback since message might not exist anymore
await resetTtsPlayback ( )
2023-08-23 15:27:53 +02:00
}
2023-11-09 01:57:40 +01:00
/ * *
* Get characters in current chat
* @ param { boolean } unrestricted - If true , will include all characters in voiceMapEntries , even if they are not in the current chat .
* @ returns { string [ ] } - Array of character names
* /
function getCharacters ( unrestricted ) {
2023-08-23 15:27:53 +02:00
const context = getContext ( )
2023-11-09 01:57:40 +01:00
if ( unrestricted ) {
const names = context . characters . map ( char => char . name ) ;
names . unshift ( DEFAULT _VOICE _MARKER ) ;
return names ;
}
2023-08-23 15:27:53 +02:00
let characters = [ ]
2023-11-09 01:57:40 +01:00
if ( context . groupId === null ) {
2023-08-23 15:27:53 +02:00
// Single char chat
2023-09-01 16:58:33 +02:00
characters . push ( DEFAULT _VOICE _MARKER )
2023-08-23 15:27:53 +02:00
characters . push ( context . name1 )
characters . push ( context . name2 )
} else {
// Group chat
2023-09-01 16:58:33 +02:00
characters . push ( DEFAULT _VOICE _MARKER )
2023-08-23 15:27:53 +02:00
characters . push ( context . name1 )
const group = context . groups . find ( group => context . groupId == group . id )
for ( let member of group . members ) {
// Remove suffix
2023-11-09 01:57:40 +01:00
if ( member . endsWith ( '.png' ) ) {
2023-08-23 15:27:53 +02:00
member = member . slice ( 0 , - 4 )
}
characters . push ( member )
}
}
return characters
}
function sanitizeId ( input ) {
2023-11-09 01:57:40 +01:00
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
let sanitized = input . replace ( /[^a-zA-Z0-9-_]/g , '' ) ;
2023-08-23 15:27:53 +02:00
2023-11-09 01:57:40 +01:00
// Ensure first character is always a letter
if ( ! /^[a-zA-Z]/ . test ( sanitized ) ) {
sanitized = 'element_' + sanitized ;
}
2023-08-23 15:27:53 +02:00
2023-11-09 01:57:40 +01:00
return sanitized ;
2023-08-23 15:27:53 +02:00
}
function parseVoiceMap ( voiceMapString ) {
let parsedVoiceMap = { }
for ( const [ charName , voiceId ] of voiceMapString
. split ( ',' )
. map ( s => s . split ( ':' ) ) ) {
if ( charName && voiceId ) {
parsedVoiceMap [ charName . trim ( ) ] = voiceId . trim ( )
}
}
return parsedVoiceMap
}
/ * *
* Apply voiceMap based on current voiceMapEntries
* /
function updateVoiceMap ( ) {
const tempVoiceMap = { }
2023-11-09 01:57:40 +01:00
for ( const voice of voiceMapEntries ) {
if ( voice . voiceId === null ) {
2023-08-23 15:27:53 +02:00
continue
}
tempVoiceMap [ voice . name ] = voice . voiceId
}
2023-11-09 01:57:40 +01:00
if ( Object . keys ( tempVoiceMap ) . length !== 0 ) {
2023-08-23 15:27:53 +02:00
voiceMap = tempVoiceMap
console . log ( ` Voicemap updated to ${ JSON . stringify ( voiceMap ) } ` )
}
2023-10-22 13:46:54 +02:00
if ( ! extension _settings . tts [ ttsProviderName ] . voiceMap ) {
extension _settings . tts [ ttsProviderName ] . voiceMap = { }
}
2023-08-28 03:47:44 +02:00
Object . assign ( extension _settings . tts [ ttsProviderName ] . voiceMap , voiceMap )
2023-08-23 15:27:53 +02:00
saveSettingsDebounced ( )
}
class VoiceMapEntry {
name
voiceId
selectElement
2023-11-09 01:57:40 +01:00
constructor ( name , voiceId = DEFAULT _VOICE _MARKER ) {
2023-08-23 15:27:53 +02:00
this . name = name
this . voiceId = voiceId
this . selectElement = null
}
2023-11-09 01:57:40 +01:00
addUI ( voiceIds ) {
2023-08-23 15:27:53 +02:00
let sanitizedName = sanitizeId ( this . name )
2023-09-03 17:38:34 +02:00
let defaultOption = this . name === DEFAULT _VOICE _MARKER ?
2023-09-04 13:21:22 +02:00
` <option> ${ DISABLED _VOICE _MARKER } </option> ` :
` <option> ${ DEFAULT _VOICE _MARKER } </option><option> ${ DISABLED _VOICE _MARKER } </option> `
2023-08-23 15:27:53 +02:00
let template = `
< div class = 'tts_voicemap_block_char flex-container flexGap5' >
< span id = 'tts_voicemap_char_${sanitizedName}' > $ { this . name } < / s p a n >
< select id = 'tts_voicemap_char_${sanitizedName}_voice' >
2023-09-03 17:38:34 +02:00
$ { defaultOption }
2023-08-23 15:27:53 +02:00
< / s e l e c t >
< / d i v >
`
$ ( '#tts_voicemap_block' ) . append ( template )
// Populate voice ID select list
2023-11-09 01:57:40 +01:00
for ( const voiceId of voiceIds ) {
2023-08-23 15:27:53 +02:00
const option = document . createElement ( 'option' ) ;
option . innerText = voiceId . name ;
option . value = voiceId . name ;
$ ( ` #tts_voicemap_char_ ${ sanitizedName } _voice ` ) . append ( option )
}
this . selectElement = $ ( ` #tts_voicemap_char_ ${ sanitizedName } _voice ` )
this . selectElement . on ( 'change' , args => this . onSelectChange ( args ) )
this . selectElement . val ( this . voiceId )
}
onSelectChange ( args ) {
this . voiceId = this . selectElement . find ( ':selected' ) . val ( )
updateVoiceMap ( )
}
}
/ * *
2023-08-26 05:52:55 +02:00
* Init voiceMapEntries for character select list .
2023-11-09 01:57:40 +01:00
* @ param { boolean } unrestricted - If true , will include all characters in voiceMapEntries , even if they are not in the current chat .
2023-08-23 15:27:53 +02:00
* /
2023-11-09 01:57:40 +01:00
export async function initVoiceMap ( unrestricted = false ) {
2023-08-23 15:27:53 +02:00
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
const enabled = $ ( '#tts_enabled' ) . is ( ':checked' )
2023-11-09 01:57:40 +01:00
if ( ! enabled ) {
2023-08-23 15:27:53 +02:00
return
}
// Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying.
try {
await ttsProvider . checkReady ( )
} catch ( error ) {
const message = ` TTS Provider not ready. ${ error } `
setTtsStatus ( message , false )
return
}
setTtsStatus ( "TTS Provider Loaded" , true )
2023-10-22 17:54:25 +02:00
// Clear existing voiceMap state
$ ( '#tts_voicemap_block' ) . empty ( )
voiceMapEntries = [ ]
2023-11-09 00:30:54 +01:00
2023-08-23 15:27:53 +02:00
// Get characters in current chat
2023-11-09 01:57:40 +01:00
const characters = getCharacters ( unrestricted ) ;
2023-08-28 20:46:41 +02:00
2023-08-23 15:27:53 +02:00
// Get saved voicemap from provider settings, handling new and old representations
let voiceMapFromSettings = { }
if ( "voiceMap" in extension _settings . tts [ ttsProviderName ] ) {
// Handle previous representation
2023-11-09 01:57:40 +01:00
if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === "string" ) {
2023-08-23 15:27:53 +02:00
voiceMapFromSettings = parseVoiceMap ( extension _settings . tts [ ttsProviderName ] . voiceMap )
2023-11-09 01:57:40 +01:00
// Handle new representation
} else if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === "object" ) {
2023-08-23 15:27:53 +02:00
voiceMapFromSettings = extension _settings . tts [ ttsProviderName ] . voiceMap
}
}
// Get voiceIds from provider
let voiceIdsFromProvider
try {
2023-08-26 05:52:55 +02:00
voiceIdsFromProvider = await ttsProvider . fetchTtsVoiceObjects ( )
2023-08-23 15:27:53 +02:00
}
catch {
toastr . error ( "TTS Provider failed to return voice ids." )
}
// Build UI using VoiceMapEntry objects
2023-11-09 01:57:40 +01:00
for ( const character of characters ) {
if ( character === "SillyTavern System" ) {
2023-08-23 15:27:53 +02:00
continue
}
// Check provider settings for voiceIds
let voiceId
2023-11-09 01:57:40 +01:00
if ( character in voiceMapFromSettings ) {
2023-08-23 15:27:53 +02:00
voiceId = voiceMapFromSettings [ character ]
2023-09-02 09:23:18 +02:00
} else if ( character === DEFAULT _VOICE _MARKER ) {
2023-09-04 13:21:22 +02:00
voiceId = DISABLED _VOICE _MARKER
2023-09-02 09:23:18 +02:00
} else {
2023-09-03 17:38:34 +02:00
voiceId = DEFAULT _VOICE _MARKER
2023-08-23 15:27:53 +02:00
}
const voiceMapEntry = new VoiceMapEntry ( character , voiceId )
voiceMapEntry . addUI ( voiceIdsFromProvider )
voiceMapEntries . push ( voiceMapEntry )
}
updateVoiceMap ( )
}
2023-07-20 19:32:15 +02:00
$ ( document ) . ready ( function ( ) {
function addExtensionControls ( ) {
const settingsHtml = `
< div id = "tts_settings" >
< div class = "inline-drawer" >
< div class = "inline-drawer-toggle inline-drawer-header" >
< b > TTS < / b >
< div class = "inline-drawer-icon fa-solid fa-circle-chevron-down down" > < / d i v >
< / d i v >
< div class = "inline-drawer-content" >
2023-08-23 15:27:53 +02:00
< div id = "tts_status" >
< / d i v >
2023-08-25 15:27:43 +02:00
< span > Select TTS Provider < / s p a n > < / b r >
< div class = "tts_block" >
< select id = "tts_provider" class = "flex1" >
2023-07-20 19:32:15 +02:00
< / s e l e c t >
2023-08-25 15:27:43 +02:00
< input id = "tts_refresh" class = "menu_button" type = "submit" value = "Reload" / >
2023-07-20 19:32:15 +02:00
< / d i v >
< div >
< label class = "checkbox_label" for = "tts_enabled" >
< input type = "checkbox" id = "tts_enabled" name = "tts_enabled" >
< small > Enabled < / s m a l l >
< / l a b e l >
2023-08-29 14:27:22 +02:00
< label class = "checkbox_label" for = "tts_narrate_user" >
< input type = "checkbox" id = "tts_narrate_user" >
< small > Narrate user messages < / s m a l l >
< / l a b e l >
2023-07-20 19:32:15 +02:00
< label class = "checkbox_label" for = "tts_auto_generation" >
< input type = "checkbox" id = "tts_auto_generation" >
< small > Auto Generation < / s m a l l >
< / l a b e l >
< label class = "checkbox_label" for = "tts_narrate_quoted" >
< input type = "checkbox" id = "tts_narrate_quoted" >
< small > Only narrate "quotes" < / s m a l l >
< / l a b e l >
< label class = "checkbox_label" for = "tts_narrate_dialogues" >
< input type = "checkbox" id = "tts_narrate_dialogues" >
< small > Ignore * text , even "quotes" , inside asterisks * < / s m a l l >
< / l a b e l >
< label class = "checkbox_label" for = "tts_narrate_translated_only" >
< input type = "checkbox" id = "tts_narrate_translated_only" >
< small > Narrate only the translated text < / s m a l l >
< / l a b e l >
< / d i v >
2023-08-23 15:27:53 +02:00
< div id = "tts_voicemap_block" >
2023-07-20 19:32:15 +02:00
< / d i v >
2023-08-23 15:27:53 +02:00
< hr >
2023-07-20 19:32:15 +02:00
< form id = "tts_provider_settings" class = "inline-drawer-content" >
< / f o r m >
< div class = "tts_buttons" >
< input id = "tts_voices" class = "menu_button" type = "submit" value = "Available voices" / >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
`
$ ( '#extensions_settings' ) . append ( settingsHtml )
2023-08-25 15:27:43 +02:00
$ ( '#tts_refresh' ) . on ( 'click' , onRefreshClick )
2023-07-20 19:32:15 +02:00
$ ( '#tts_enabled' ) . on ( 'click' , onEnableClick )
$ ( '#tts_narrate_dialogues' ) . on ( 'click' , onNarrateDialoguesClick ) ;
$ ( '#tts_narrate_quoted' ) . on ( 'click' , onNarrateQuotedClick ) ;
$ ( '#tts_narrate_translated_only' ) . on ( 'click' , onNarrateTranslatedOnlyClick ) ;
$ ( '#tts_auto_generation' ) . on ( 'click' , onAutoGenerationClick ) ;
2023-08-29 14:27:22 +02:00
$ ( '#tts_narrate_user' ) . on ( 'click' , onNarrateUserClick ) ;
2023-07-20 19:32:15 +02:00
$ ( '#tts_voices' ) . on ( 'click' , onTtsVoicesClick )
for ( const provider in ttsProviders ) {
$ ( '#tts_provider' ) . append ( $ ( "<option />" ) . val ( provider ) . text ( provider ) )
}
$ ( '#tts_provider' ) . on ( 'change' , onTtsProviderChange )
$ ( document ) . on ( 'click' , '.mes_narrate' , onNarrateOneMessage ) ;
}
addExtensionControls ( ) // No init dependencies
loadSettings ( ) // Depends on Extension Controls and loadTtsProvider
loadTtsProvider ( extension _settings . tts . currentProvider ) // No dependencies
addAudioControl ( ) // Depends on Extension Controls
const wrapper = new ModuleWorkerWrapper ( moduleWorker ) ;
setInterval ( wrapper . update . bind ( wrapper ) , UPDATE _INTERVAL ) // Init depends on all the things
eventSource . on ( event _types . MESSAGE _SWIPED , resetTtsPlayback ) ;
2023-08-23 15:27:53 +02:00
eventSource . on ( event _types . CHAT _CHANGED , onChatChanged )
2023-11-08 16:40:47 +01:00
eventSource . on ( event _types . MESSAGE _DELETED , onChatDeleted ) ;
2023-08-23 15:27:53 +02:00
eventSource . on ( event _types . GROUP _UPDATED , onChatChanged )
2023-11-09 01:57:40 +01:00
registerSlashCommand ( 'speak' , onNarrateText , [ 'narrate' , 'tts' ] , ` <span class="monospace">(text)</span> – narrate any text using currently selected character's voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt> ` , true , true ) ;
2023-11-17 00:30:32 +01:00
document . body . appendChild ( audioElement ) ;
2023-07-20 19:32:15 +02:00
} )