2023-12-02 20:11:06 +01:00
import { callPopup , cancelTtsPlay , eventSource , event _types , name2 , saveSettingsDebounced } from '../../../script.js' ;
import { ModuleWorkerWrapper , doExtrasFetch , extension _settings , getApiUrl , getContext , modules } from '../../extensions.js' ;
2024-01-02 06:04:32 +01:00
import { delay , escapeRegex , getBase64Async , getStringHash , onlyUnique } from '../../utils.js' ;
2023-12-02 20:11:06 +01:00
import { EdgeTtsProvider } from './edge.js' ;
import { ElevenLabsTtsProvider } from './elevenlabs.js' ;
import { SileroTtsProvider } from './silerotts.js' ;
import { CoquiTtsProvider } from './coqui.js' ;
import { SystemTtsProvider } from './system.js' ;
import { NovelTtsProvider } from './novel.js' ;
import { power _user } from '../../power-user.js' ;
import { registerSlashCommand } from '../../slash-commands.js' ;
import { OpenAITtsProvider } from './openai.js' ;
2023-12-04 18:32:41 +01:00
import { XTTSTtsProvider } from './xtts.js' ;
2023-08-11 07:43:53 +02:00
export { talkingAnimation } ;
2023-07-20 19:32:15 +02:00
2023-12-02 20:11:06 +01:00
const UPDATE _INTERVAL = 1000 ;
2023-07-20 19:32:15 +02:00
2023-12-02 20:11:06 +01:00
let voiceMapEntries = [ ] ;
let voiceMap = { } ; // {charName:voiceid, charName2:voiceid2}
2023-07-31 11:21:32 +02:00
let storedvalue = false ;
2023-12-02 20:11:06 +01:00
let lastChatId = null ;
let lastMessageHash = null ;
2023-07-20 19:32:15 +02:00
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' ,
2023-12-02 19:04:51 +01:00
'it-IT' : 'Pranzo d\'acqua fa volti sghembi' ,
2023-07-20 19:32:15 +02:00
'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.' ,
2023-12-02 19:04:51 +01:00
'uk-UA' : 'Фабрикуймо гідність, лящім їжею, ґав хапаймо, з\'єднавці чаш!' ,
2023-07-20 19:32:15 +02:00
'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' : 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा' ,
2023-12-02 20:11:06 +01:00
} ;
const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet' ;
2023-07-20 19:32:15 +02:00
return previewStrings [ lang ] ? ? fallbackPreview ;
}
let ttsProviders = {
ElevenLabs : ElevenLabsTtsProvider ,
Silero : SileroTtsProvider ,
2023-11-21 11:16:56 +01:00
XTTSv2 : XTTSTtsProvider ,
2023-07-20 19:32:15 +02:00
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-12-02 20:11:06 +01:00
} ;
let ttsProvider ;
let ttsProviderName ;
2023-07-20 19:32:15 +02:00
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 ;
}
2023-12-02 20:11:06 +01:00
resetTtsPlayback ( ) ;
2023-07-20 19:32:15 +02:00
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 ;
}
2023-12-02 20:11:06 +01:00
resetTtsPlayback ( ) ;
2023-11-09 01:57:40 +01:00
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
2023-12-02 20:11:06 +01:00
const enabled = $ ( '#tts_enabled' ) . is ( ':checked' ) ;
2023-07-20 19:32:15 +02:00
$ ( 'body' ) . toggleClass ( 'tts' , enabled ) ;
if ( ! enabled ) {
2023-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
const context = getContext ( ) ;
const chat = context . chat ;
2023-07-20 19:32:15 +02:00
2023-12-02 20:11:06 +01:00
processTtsQueue ( ) ;
processAudioJobQueue ( ) ;
updateUiAudioPlayState ( ) ;
2023-07-20 19:32:15 +02:00
// Auto generation is disabled
if ( extension _settings . tts . auto _generation == false ) {
2023-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
// no characters or group selected
if ( ! context . groupId && context . characterId === undefined ) {
2023-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
// Chat changed
if (
context . chatId !== lastChatId
) {
2023-12-02 20:11:06 +01:00
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-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
// take the count of messages
2023-11-19 15:56:12 +01:00
let lastMessageNumber = context . chat . length ? context . chat . length : 0 ;
2023-07-20 19:32:15 +02:00
// There's no new messages
2023-11-19 15:56:12 +01:00
let diff = lastMessageNumber - currentMessageNumber ;
let hashNew = getStringHash ( ( chat . length && chat [ chat . length - 1 ] . mes ) ? ? '' ) ;
2023-07-20 19:32:15 +02:00
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
2023-11-19 15:56:12 +01:00
return ;
2023-11-08 16:40:47 +01:00
}
2023-11-19 15:56:12 +01:00
// if no new messages, or same message, or same message hash, do nothing
2023-07-20 19:32:15 +02:00
if ( diff == 0 && hashNew === lastMessageHash ) {
2023-11-19 15:56:12 +01:00
return ;
}
// If streaming, wait for streaming to finish before processing new messages
if ( context . streamingProcessor && ! context . streamingProcessor . isFinished ) {
return ;
2023-07-20 19:32:15 +02:00
}
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-19 15:56:12 +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 ) {
2023-11-19 15:56:12 +01:00
let tmp = message . mes ;
message . mes = message . mes . replace ( ttsLastMessage , '' ) ;
ttsLastMessage = tmp ;
2023-11-08 16:40:47 +01:00
} else {
2023-11-19 15:56:12 +01:00
ttsLastMessage = message . mes ;
2023-11-08 16:40:47 +01:00
}
2023-07-20 19:32:15 +02:00
2023-11-19 15:56:12 +01:00
// We're currently swiping. Don't generate voice
if ( ! message || message . mes === '...' || message . mes === '' ) {
return ;
2023-07-20 19:32:15 +02:00
}
// 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
2023-12-02 20:11:06 +01:00
lastMessageHash = hashNew ;
currentMessageNumber = lastMessageNumber ;
2023-07-20 19:32:15 +02:00
console . debug (
2023-12-02 21:06:57 +01:00
` Adding message from ${ message . name } for TTS processing: " ${ message . mes } " ` ,
2023-12-02 20:11:06 +01:00
) ;
ttsJobQueue . push ( message ) ;
2023-07-20 19:32:15 +02:00
}
2023-07-31 11:21:32 +02:00
function talkingAnimation ( switchValue ) {
2023-08-13 17:43:17 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
2023-12-02 19:04:51 +01:00
console . debug ( 'Talking Animation module not loaded' ) ;
2023-08-13 17:43:17 +02:00
return ;
}
2023-07-31 19:56:05 +02:00
const apiUrl = getApiUrl ( ) ;
2023-12-02 19:04:51 +01:00
const animationType = switchValue ? 'start' : 'stop' ;
2023-07-31 19:56:05 +02:00
if ( switchValue !== storedvalue ) {
try {
2023-12-02 19:04:51 +01:00
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-12-02 20:11:06 +01: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 ( ) {
2023-12-02 20:11:06 +01:00
let processing = false ;
2023-07-20 19:32:15 +02:00
// Check job queues
2023-08-29 14:27:22 +02:00
if ( ttsJobQueue . length > 0 || audioJobQueue . length > 0 ) {
2023-12-02 20:11:06 +01:00
processing = true ;
2023-07-20 19:32:15 +02:00
}
// Check current jobs
if ( currentTtsJob != null || currentAudioJob != null ) {
2023-12-02 20:11:06 +01:00
processing = true ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
return processing ;
2023-07-20 19:32:15 +02:00
}
function debugTtsPlayback ( ) {
console . log ( JSON . stringify (
{
2023-12-02 19:04:51 +01:00
'ttsProviderName' : ttsProviderName ,
'voiceMap' : voiceMap ,
'currentMessageNumber' : currentMessageNumber ,
'audioPaused' : audioPaused ,
'audioJobQueue' : audioJobQueue ,
'currentAudioJob' : currentAudioJob ,
'audioQueueProcessorReady' : audioQueueProcessorReady ,
'ttsJobQueue' : ttsJobQueue ,
'currentTtsJob' : currentTtsJob ,
2023-12-02 21:06:57 +01:00
'ttsConfig' : extension _settings . tts ,
} ,
2023-12-02 20:11:06 +01:00
) ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
window . debugTtsPlayback = debugTtsPlayback ;
2023-07-20 19:32:15 +02:00
//##################//
// Audio Control //
//##################//
2023-12-02 20:11:06 +01:00
let audioElement = new Audio ( ) ;
audioElement . id = 'tts_audio' ;
audioElement . autoplay = true ;
2023-07-20 19:32:15 +02:00
2023-12-02 20:11:06 +01:00
let audioJobQueue = [ ] ;
let currentAudioJob ;
let audioPaused = false ;
let audioQueueProcessorReady = true ;
2023-07-20 19:32:15 +02:00
async function playAudioData ( audioBlob ) {
// Since current audio job can be cancelled, don't playback if it is null
if ( currentAudioJob == null ) {
2023-12-02 20:11:06 +01:00
console . log ( 'Cancelled TTS playback because currentAudioJob was null' ) ;
2023-07-20 19:32:15 +02:00
}
2024-01-02 06:04:32 +01:00
if ( audioBlob instanceof Blob ) {
const srcUrl = await getBase64Async ( audioBlob ) ;
2023-12-02 20:11:06 +01:00
audioElement . src = srcUrl ;
2024-01-02 06:04:32 +01:00
} else if ( typeof audioBlob === 'string' ) {
audioElement . src = audioBlob ;
} else {
throw ` TTS received invalid audio data type ${ typeof audioBlob } ` ;
}
2023-12-02 20:11:06 +01:00
audioElement . addEventListener ( 'ended' , completeCurrentAudioJob ) ;
2023-07-20 19:32:15 +02:00
audioElement . addEventListener ( 'canplay' , ( ) => {
2023-12-02 20:11:06 +01:00
console . debug ( 'Starting TTS playback' ) ;
audioElement . play ( ) ;
} ) ;
2023-07-20 19:32:15 +02:00
}
window [ 'tts_preview' ] = function ( id ) {
2023-12-02 20:11:06 +01:00
const audio = document . getElementById ( id ) ;
2023-07-20 19:32:15 +02:00
if ( audio && ! $ ( audio ) . data ( 'disabled' ) ) {
2023-12-02 20:11:06 +01:00
audio . play ( ) ;
2023-07-20 19:32:15 +02:00
}
else {
2023-12-02 20:11:06 +01:00
ttsProvider . previewTtsVoice ( id ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
} ;
2023-07-20 19:32:15 +02:00
async function onTtsVoicesClick ( ) {
2023-12-02 20:11:06 +01:00
let popupText = '' ;
2023-07-20 19:32:15 +02:00
try {
2023-12-02 20:11:06 +01: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 >
2023-12-02 20:11:06 +01:00
< / d i v > ` ;
2023-07-20 19:32:15 +02:00
if ( voice . preview _url ) {
2023-12-02 20:11:06 +01:00
popupText += ` <audio id=" ${ voice . voice _id } " src=" ${ voice . preview _url } " data-disabled=" ${ voice . preview _url == false } "></audio> ` ;
2023-07-20 19:32:15 +02:00
}
}
} catch {
2023-12-02 20:11:06 +01:00
popupText = 'Could not load voices list. Check your API key.' ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
callPopup ( popupText , 'text' ) ;
2023-07-20 19:32:15 +02:00
}
function updateUiAudioPlayState ( ) {
if ( extension _settings . tts . enabled == true ) {
$ ( '#ttsExtensionMenuItem' ) . show ( ) ;
2023-12-02 20:11:06 +01:00
let img ;
2023-07-20 19:32:15 +02:00
// Give user feedback that TTS is active by setting the stop icon if processing or playing
if ( ! audioElement . paused || isTtsProcessing ( ) ) {
2023-12-02 20:11:06 +01:00
img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton' ;
2023-07-20 19:32:15 +02:00
} else {
2023-12-02 20:11:06 +01:00
img = 'fa-solid fa-circle-play extensionsMenuExtensionButton' ;
2023-07-20 19:32:15 +02:00
}
$ ( '#tts_media_control' ) . attr ( 'class' , img ) ;
} else {
$ ( '#ttsExtensionMenuItem' ) . hide ( ) ;
}
}
function onAudioControlClicked ( ) {
audioElement . src = '/sounds/silence.mp3' ;
2023-12-02 20:11:06 +01:00
let context = getContext ( ) ;
2023-07-20 19:32:15 +02:00
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
if ( ! audioElement . paused || isTtsProcessing ( ) ) {
2023-12-02 20:11:06 +01:00
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.
2023-12-02 20:11:06 +01:00
ttsJobQueue . push ( context . chat [ context . chat . length - 1 ] ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
updateUiAudioPlayState ( ) ;
2023-07-20 19:32:15 +02:00
}
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
2023-12-02 20:11:06 +01:00
< / d i v > ` ) ;
$ ( '#ttsExtensionMenuItem' ) . attr ( 'title' , 'TTS play/pause' ) . on ( 'click' , onAudioControlClicked ) ;
updateUiAudioPlayState ( ) ;
2023-07-20 19:32:15 +02:00
}
function completeCurrentAudioJob ( ) {
2023-12-02 20:11:06 +01:00
audioQueueProcessorReady = true ;
currentAudioJob = null ;
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
2023-12-02 17:41:54 +01:00
* @ param { Response } response
2023-07-20 19:32:15 +02:00
* /
async function addAudioJob ( response ) {
2024-01-02 06:04:32 +01:00
if ( typeof response === 'string' ) {
audioJobQueue . push ( response ) ;
} else {
const audioData = await response . blob ( ) ;
if ( ! audioData . type . startsWith ( 'audio/' ) ) {
throw ` TTS received HTTP response with invalid data format. Expecting audio/*, got ${ audioData . type } ` ;
}
audioJobQueue . push ( audioData ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
console . debug ( 'Pushed audio job to queue.' ) ;
2023-07-20 19:32:15 +02:00
}
async function processAudioJobQueue ( ) {
// Nothing to do, audio not completed, or audio paused - stop processing.
if ( audioJobQueue . length == 0 || ! audioQueueProcessorReady || audioPaused ) {
2023-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
try {
2023-12-02 20:11:06 +01:00
audioQueueProcessorReady = false ;
2024-01-02 06:04:32 +01:00
currentAudioJob = audioJobQueue . shift ( ) ;
2023-12-02 20:11:06 +01:00
playAudioData ( currentAudioJob ) ;
talkingAnimation ( true ) ;
2023-07-20 19:32:15 +02:00
} catch ( error ) {
2023-12-02 20:11:06 +01:00
console . error ( error ) ;
audioQueueProcessorReady = true ;
2023-07-20 19:32:15 +02:00
}
}
//################//
// TTS Control //
//################//
2023-12-02 20:11:06 +01:00
let ttsJobQueue = [ ] ;
let currentTtsJob ; // Null if nothing is currently being processed
let currentMessageNumber = 0 ;
2023-07-20 19:32:15 +02:00
function completeTtsJob ( ) {
2023-12-02 20:11:06 +01:00
console . info ( ` Current TTS job for ${ currentTtsJob ? . name } completed. ` ) ;
currentTtsJob = null ;
2023-07-20 19:32:15 +02:00
}
function saveLastValues ( ) {
2023-12-02 20:11:06 +01:00
const context = getContext ( ) ;
lastChatId = context . chatId ;
2023-07-20 19:32:15 +02:00
lastMessageHash = getStringHash (
2023-12-02 21:06:57 +01:00
( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' ,
2023-12-02 20:11:06 +01:00
) ;
2023-07-20 19:32:15 +02:00
}
2023-08-09 03:30:26 +02:00
async function tts ( text , voiceId , char ) {
2024-01-01 20:31:08 +01:00
async function processResponse ( response ) {
// RVC injection
if ( extension _settings . rvc . enabled && typeof window [ 'rvcVoiceConversion' ] === 'function' )
response = await window [ 'rvcVoiceConversion' ] ( response , char , text ) ;
2024-01-14 04:26:06 +01:00
// VRM injection
if ( extension _settings . vrm . enabled && typeof window [ 'vrmLipSync' ] === 'function' )
await window [ 'vrmLipSync' ] ( response , char ) ;
2024-01-01 20:31:08 +01:00
await addAudioJob ( response ) ;
}
2023-12-02 20:11:06 +01:00
let response = await ttsProvider . generateTts ( text , voiceId ) ;
2023-08-09 03:30:26 +02:00
2024-01-01 20:31:08 +01:00
// If async generator, process every chunk as it comes in
if ( typeof response [ Symbol . asyncIterator ] === 'function' ) {
for await ( const chunk of response ) {
await processResponse ( chunk ) ;
}
} else {
await processResponse ( response ) ;
}
2023-08-09 03:30:26 +02:00
2023-12-02 20:11:06 +01:00
completeTtsJob ( ) ;
2023-07-20 19:32:15 +02:00
}
async function processTtsQueue ( ) {
// Called each moduleWorker iteration to pull chat messages from queue
if ( currentTtsJob || ttsJobQueue . length <= 0 || audioPaused ) {
2023-12-02 20:11:06 +01:00
return ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
console . debug ( 'New message found, running TTS' ) ;
currentTtsJob = ttsJobQueue . shift ( ) ;
let text = extension _settings . tts . narrate _translated _only ? ( currentTtsJob ? . extra ? . display _text || currentTtsJob . mes ) : currentTtsJob . mes ;
2023-12-10 17:32:10 +01:00
if ( extension _settings . tts . skip _codeblocks ) {
text = text . replace ( /^\s{4}.*$/gm , '' ) . trim ( ) ;
text = text . replace ( / ` ` ` . * ? ` ` ` / g s , ' ' ) . t r i m ( ) ;
}
2023-07-20 19:32:15 +02:00
text = extension _settings . tts . narrate _dialogues _only
2023-12-02 16:17:31 +01:00
? text . replace ( /\*[^*]*?(\*|$)/g , '' ) . trim ( ) // remove asterisks content
2023-12-02 20:11:06 +01:00
: text . replaceAll ( '*' , '' ) . trim ( ) ; // remove just the asterisks
2023-07-20 19:32:15 +02:00
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 ;
}
2023-11-22 16:47:58 +01:00
2023-11-27 12:25:49 +01:00
if ( typeof ttsProvider ? . processText === 'function' ) {
text = await ttsProvider . processText ( text ) ;
}
2023-11-22 16:47:58 +01:00
// Collapse newlines and spaces into single space
2023-11-28 15:56:50 +01:00
text = text . replace ( /\s+/g , ' ' ) . trim ( ) ;
2023-11-22 16:47:58 +01:00
2023-12-02 20:11:06 +01:00
console . log ( ` TTS: ${ text } ` ) ;
const char = currentTtsJob . name ;
2023-07-20 19:32:15 +02:00
// 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.' ) ;
2023-12-02 20:11:06 +01:00
completeTtsJob ( ) ;
2023-07-20 19:32:15 +02:00
return ;
}
2023-12-02 20:11:06 +01: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-12-02 20:11:06 +01:00
throw ` ${ char } not in voicemap. Configure character in extension settings voice map ` ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
const voice = await ttsProvider . getVoice ( voiceMapEntry ) ;
const voiceId = voice . voice _id ;
2023-07-20 19:32:15 +02:00
if ( voiceId == null ) {
2023-12-02 20:11:06 +01:00
toastr . error ( ` Specified voice for ${ char } was not found. Check the TTS extension settings. ` ) ;
throw ` Unable to attain voiceId for ${ char } ` ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
tts ( text , voiceId , char ) ;
2023-07-20 19:32:15 +02:00
} catch ( error ) {
2023-12-02 20:11:06 +01:00
console . error ( error ) ;
currentTtsJob = null ;
2023-07-20 19:32:15 +02:00
}
}
// Secret function for now
async function playFullConversation ( ) {
2023-12-02 20:11:06 +01:00
const context = getContext ( ) ;
const chat = context . chat ;
ttsJobQueue = chat ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
window . playFullConversation = playFullConversation ;
2023-07-20 19:32:15 +02:00
//#############################//
// Extension UI and Settings //
//#############################//
function loadSettings ( ) {
if ( Object . keys ( extension _settings . tts ) . length === 0 ) {
2023-12-02 20:11:06 +01:00
Object . assign ( extension _settings . tts , defaultSettings ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-28 20:46:41 +02:00
for ( const key in defaultSettings ) {
if ( ! ( key in extension _settings . tts ) ) {
2023-12-02 20:11:06 +01:00
extension _settings . tts [ key ] = defaultSettings [ key ] ;
2023-08-28 20:46:41 +02:00
}
}
2023-12-02 20:11:06 +01:00
$ ( '#tts_provider' ) . val ( extension _settings . tts . currentProvider ) ;
2023-07-20 19:32:15 +02:00
$ ( '#tts_enabled' ) . prop (
'checked' ,
2023-12-02 21:06:57 +01:00
extension _settings . tts . enabled ,
2023-12-02 20:11:06 +01:00
) ;
$ ( '#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 ) ;
2023-07-20 19:32:15 +02:00
$ ( '#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 ,
2023-12-02 19:04:51 +01:00
currentProvider : 'ElevenLabs' ,
2023-08-29 14:27:22 +02:00
auto _generation : true ,
narrate _user : false ,
2023-12-02 20:11:06 +01:00
} ;
2023-07-20 19:32:15 +02:00
function setTtsStatus ( status , success ) {
2023-12-02 20:11:06 +01:00
$ ( '#tts_status' ) . text ( status ) ;
2023-07-20 19:32:15 +02:00
if ( success ) {
2023-12-02 20:11:06 +01:00
$ ( '#tts_status' ) . removeAttr ( 'style' ) ;
2023-07-20 19:32:15 +02:00
} else {
2023-12-02 20:11:06 +01:00
$ ( '#tts_status' ) . css ( 'color' , 'red' ) ;
2023-07-20 19:32:15 +02:00
}
}
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 ( ( ) => {
2023-12-02 20:11:06 +01:00
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings ;
saveSettingsDebounced ( ) ;
setTtsStatus ( 'Successfully applied settings' , true ) ;
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` ) ;
initVoiceMap ( ) ;
updateVoiceMap ( ) ;
2023-07-20 19:32:15 +02:00
} ) . catch ( error => {
2023-12-02 20:11:06 +01:00
console . error ( error ) ;
setTtsStatus ( error , false ) ;
} ) ;
2023-07-20 19:32:15 +02:00
}
function onEnableClick ( ) {
extension _settings . tts . enabled = $ ( '#tts_enabled' ) . is (
2023-12-02 21:06:57 +01:00
':checked' ,
2023-12-02 20:11:06 +01:00
) ;
updateUiAudioPlayState ( ) ;
saveSettingsDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
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-12-02 20:11:06 +01:00
saveSettingsDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
function onNarrateDialoguesClick ( ) {
2023-08-29 14:27:22 +02:00
extension _settings . tts . narrate _dialogues _only = ! ! $ ( '#tts_narrate_dialogues' ) . prop ( 'checked' ) ;
2023-12-02 20:11:06 +01:00
saveSettingsDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
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-12-02 20:11:06 +01:00
saveSettingsDebounced ( ) ;
2023-07-20 19:32:15 +02:00
}
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 ( ) ;
}
2023-12-10 17:32:10 +01:00
function onSkipCodeblocksClick ( ) {
extension _settings . tts . skip _codeblocks = ! ! $ ( '#tts_skip_codeblocks' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2023-07-20 19:32:15 +02:00
//##############//
// 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
2023-12-02 20:11:06 +01:00
$ ( '#tts_provider_settings' ) . html ( '' ) ;
2023-07-20 19:32:15 +02:00
if ( ! provider ) {
2023-12-02 20:11:06 +01: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
2023-12-02 20:11:06 +01:00
extension _settings . tts . currentProvider = provider ;
ttsProviderName = provider ;
ttsProvider = new ttsProviders [ provider ] ;
2023-07-20 19:32:15 +02:00
// Init provider settings
2023-12-02 20:11:06 +01:00
$ ( '#tts_provider_settings' ) . append ( ttsProvider . settingsHtml ) ;
2023-07-20 19:32:15 +02:00
if ( ! ( ttsProviderName in extension _settings . tts ) ) {
2023-12-02 20:11:06 +01:00
console . warn ( ` Provider ${ ttsProviderName } not in Extension Settings, initiatilizing provider in settings ` ) ;
extension _settings . tts [ ttsProviderName ] = { } ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
await ttsProvider . loadSettings ( extension _settings . tts [ ttsProviderName ] ) ;
await initVoiceMap ( ) ;
2023-07-20 19:32:15 +02:00
}
function onTtsProviderChange ( ) {
2023-12-02 20:11:06 +01:00
const ttsProviderSelection = $ ( '#tts_provider' ) . val ( ) ;
extension _settings . tts . currentProvider = ttsProviderSelection ;
loadTtsProvider ( ttsProviderSelection ) ;
2023-07-20 19:32:15 +02:00
}
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-12-02 20:11:06 +01:00
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings ;
updateVoiceMap ( ) ;
saveSettingsDebounced ( ) ;
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-23 15:27:53 +02:00
//###################//
// voiceMap Handling //
//###################//
async function onChatChanged ( ) {
2023-12-02 20:11:06 +01:00
await resetTtsPlayback ( ) ;
2023-12-10 20:50:52 +01:00
const voiceMapInit = initVoiceMap ( ) ;
2023-12-10 20:51:16 +01:00
await Promise . race ( [ voiceMapInit , delay ( 1000 ) ] ) ;
2023-12-02 20:11:06 +01:00
ttsLastMessage = null ;
2023-08-23 15:27:53 +02:00
}
2023-11-08 16:40:47 +01:00
async function onChatDeleted ( ) {
2023-12-02 20:11:06 +01:00
const context = getContext ( ) ;
2023-11-08 16:40:47 +01:00
// update internal references to new last message
2023-12-02 20:11:06 +01:00
lastChatId = context . chatId ;
currentMessageNumber = context . chat . length ? context . chat . length : 0 ;
2023-11-08 16:40:47 +01:00
// compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
2023-12-02 20:11:06 +01:00
let messageHash = getStringHash ( ( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' ) ;
2023-11-08 16:40:47 +01:00
if ( messageHash === lastMessageHash ) {
2023-12-02 20:11:06 +01:00
return ;
2023-11-08 16:40:47 +01:00
}
2023-12-02 20:11:06 +01:00
lastMessageHash = messageHash ;
2023-11-08 16:40:47 +01:00
ttsLastMessage = ( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' ;
// stop any tts playback since message might not exist anymore
2023-12-02 20:11:06 +01:00
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-12-02 20:11:06 +01: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 ) ;
2024-01-01 19:22:48 +01:00
return names . filter ( onlyUnique ) ;
2023-11-09 01:57:40 +01:00
}
2023-12-02 20:11:06 +01: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-12-02 20:11:06 +01:00
characters . push ( DEFAULT _VOICE _MARKER ) ;
characters . push ( context . name1 ) ;
characters . push ( context . name2 ) ;
2023-08-23 15:27:53 +02:00
} else {
// Group chat
2023-12-02 20:11:06 +01:00
characters . push ( DEFAULT _VOICE _MARKER ) ;
characters . push ( context . name1 ) ;
const group = context . groups . find ( group => context . groupId == group . id ) ;
2023-08-23 15:27:53 +02:00
for ( let member of group . members ) {
2024-01-01 19:22:48 +01:00
const character = context . characters . find ( char => char . avatar == member ) ;
if ( character ) {
characters . push ( character . name ) ;
2023-08-23 15:27:53 +02:00
}
}
}
2024-01-01 19:22:48 +01:00
return characters . filter ( onlyUnique ) ;
2023-08-23 15:27:53 +02:00
}
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 ) {
2023-12-02 20:11:06 +01:00
let parsedVoiceMap = { } ;
2023-08-23 15:27:53 +02:00
for ( const [ charName , voiceId ] of voiceMapString
. split ( ',' )
. map ( s => s . split ( ':' ) ) ) {
if ( charName && voiceId ) {
2023-12-02 20:11:06 +01:00
parsedVoiceMap [ charName . trim ( ) ] = voiceId . trim ( ) ;
2023-08-23 15:27:53 +02:00
}
}
2023-12-02 20:11:06 +01:00
return parsedVoiceMap ;
2023-08-23 15:27:53 +02:00
}
/ * *
* Apply voiceMap based on current voiceMapEntries
* /
function updateVoiceMap ( ) {
2023-12-02 20:11:06 +01:00
const tempVoiceMap = { } ;
2023-11-09 01:57:40 +01:00
for ( const voice of voiceMapEntries ) {
if ( voice . voiceId === null ) {
2023-12-02 20:11:06 +01:00
continue ;
2023-08-23 15:27:53 +02:00
}
2023-12-02 20:11:06 +01:00
tempVoiceMap [ voice . name ] = voice . voiceId ;
2023-08-23 15:27:53 +02:00
}
2023-11-09 01:57:40 +01:00
if ( Object . keys ( tempVoiceMap ) . length !== 0 ) {
2023-12-02 20:11:06 +01:00
voiceMap = tempVoiceMap ;
console . log ( ` Voicemap updated to ${ JSON . stringify ( voiceMap ) } ` ) ;
2023-08-23 15:27:53 +02:00
}
2023-10-22 13:46:54 +02:00
if ( ! extension _settings . tts [ ttsProviderName ] . voiceMap ) {
2023-12-02 20:11:06 +01:00
extension _settings . tts [ ttsProviderName ] . voiceMap = { } ;
2023-10-22 13:46:54 +02:00
}
2023-12-02 20:11:06 +01:00
Object . assign ( extension _settings . tts [ ttsProviderName ] . voiceMap , voiceMap ) ;
saveSettingsDebounced ( ) ;
2023-08-23 15:27:53 +02:00
}
class VoiceMapEntry {
2023-12-02 20:11:06 +01:00
name ;
voiceId ;
selectElement ;
2023-11-09 01:57:40 +01:00
constructor ( name , voiceId = DEFAULT _VOICE _MARKER ) {
2023-12-02 20:11:06 +01:00
this . name = name ;
this . voiceId = voiceId ;
this . selectElement = null ;
2023-08-23 15:27:53 +02:00
}
2023-11-09 01:57:40 +01:00
addUI ( voiceIds ) {
2023-12-02 20:11:06 +01: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> ` :
2023-12-02 20:11:06 +01:00
` <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 >
2023-12-02 20:11:06 +01:00
` ;
$ ( '#tts_voicemap_block' ) . append ( template ) ;
2023-08-23 15:27:53 +02:00
// 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 ;
2023-12-02 20:11:06 +01:00
$ ( ` #tts_voicemap_char_ ${ sanitizedName } _voice ` ) . append ( option ) ;
2023-08-23 15:27:53 +02:00
}
2023-12-02 20:11:06 +01:00
this . selectElement = $ ( ` #tts_voicemap_char_ ${ sanitizedName } _voice ` ) ;
this . selectElement . on ( 'change' , args => this . onSelectChange ( args ) ) ;
this . selectElement . val ( this . voiceId ) ;
2023-08-23 15:27:53 +02:00
}
onSelectChange ( args ) {
2023-12-02 20:11:06 +01:00
this . voiceId = this . selectElement . find ( ':selected' ) . val ( ) ;
updateVoiceMap ( ) ;
2023-08-23 15:27:53 +02:00
}
}
/ * *
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.
2023-12-02 20:11:06 +01:00
const enabled = $ ( '#tts_enabled' ) . is ( ':checked' ) ;
2023-11-09 01:57:40 +01:00
if ( ! enabled ) {
2023-12-02 20:11:06 +01:00
return ;
2023-08-23 15:27:53 +02:00
}
// Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying.
try {
2023-12-02 20:11:06 +01:00
await ttsProvider . checkReady ( ) ;
2023-08-23 15:27:53 +02:00
} catch ( error ) {
2023-12-02 20:11:06 +01:00
const message = ` TTS Provider not ready. ${ error } ` ;
setTtsStatus ( message , false ) ;
return ;
2023-08-23 15:27:53 +02:00
}
2023-12-02 20:11:06 +01:00
setTtsStatus ( 'TTS Provider Loaded' , true ) ;
2023-08-23 15:27:53 +02:00
2023-10-22 17:54:25 +02:00
// Clear existing voiceMap state
2023-12-02 20:11:06 +01:00
$ ( '#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
2023-12-02 20:11:06 +01:00
let voiceMapFromSettings = { } ;
2023-12-02 19:04:51 +01:00
if ( 'voiceMap' in extension _settings . tts [ ttsProviderName ] ) {
2023-08-23 15:27:53 +02:00
// Handle previous representation
2023-12-02 19:04:51 +01:00
if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === 'string' ) {
2023-12-02 20:11:06 +01:00
voiceMapFromSettings = parseVoiceMap ( extension _settings . tts [ ttsProviderName ] . voiceMap ) ;
2023-11-09 01:57:40 +01:00
// Handle new representation
2023-12-02 19:04:51 +01:00
} else if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === 'object' ) {
2023-12-02 20:11:06 +01:00
voiceMapFromSettings = extension _settings . tts [ ttsProviderName ] . voiceMap ;
2023-08-23 15:27:53 +02:00
}
}
// Get voiceIds from provider
2023-12-02 20:11:06 +01:00
let voiceIdsFromProvider ;
2023-08-23 15:27:53 +02:00
try {
2023-12-02 20:11:06 +01:00
voiceIdsFromProvider = await ttsProvider . fetchTtsVoiceObjects ( ) ;
2023-08-23 15:27:53 +02:00
}
catch {
2023-12-02 20:11:06 +01:00
toastr . error ( 'TTS Provider failed to return voice ids.' ) ;
2023-08-23 15:27:53 +02:00
}
// Build UI using VoiceMapEntry objects
2023-11-09 01:57:40 +01:00
for ( const character of characters ) {
2023-12-02 19:04:51 +01:00
if ( character === 'SillyTavern System' ) {
2023-12-02 20:11:06 +01:00
continue ;
2023-08-23 15:27:53 +02:00
}
// Check provider settings for voiceIds
2023-12-02 20:11:06 +01:00
let voiceId ;
2023-11-09 01:57:40 +01:00
if ( character in voiceMapFromSettings ) {
2023-12-02 20:11:06 +01:00
voiceId = voiceMapFromSettings [ character ] ;
2023-09-02 09:23:18 +02:00
} else if ( character === DEFAULT _VOICE _MARKER ) {
2023-12-02 20:11:06 +01:00
voiceId = DISABLED _VOICE _MARKER ;
2023-09-02 09:23:18 +02:00
} else {
2023-12-02 20:11:06 +01:00
voiceId = DEFAULT _VOICE _MARKER ;
2023-08-23 15:27:53 +02:00
}
2023-12-02 20:11:06 +01:00
const voiceMapEntry = new VoiceMapEntry ( character , voiceId ) ;
voiceMapEntry . addUI ( voiceIdsFromProvider ) ;
voiceMapEntries . push ( voiceMapEntry ) ;
2023-08-23 15:27:53 +02:00
}
2023-12-02 20:11:06 +01:00
updateVoiceMap ( ) ;
2023-08-23 15:27:53 +02:00
}
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 >
2023-12-10 17:32:10 +01:00
< label class = "checkbox_label" for = "tts_skip_codeblocks" >
< input type = "checkbox" id = "tts_skip_codeblocks" >
< small > Skip codeblocks < / s m a l l >
< / l a b e l >
2023-07-20 19:32:15 +02:00
< / 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 >
2023-12-02 20:11:06 +01:00
` ;
$ ( '#extensions_settings' ) . append ( settingsHtml ) ;
$ ( '#tts_refresh' ) . on ( 'click' , onRefreshClick ) ;
$ ( '#tts_enabled' ) . on ( 'click' , onEnableClick ) ;
2023-07-20 19:32:15 +02:00
$ ( '#tts_narrate_dialogues' ) . on ( 'click' , onNarrateDialoguesClick ) ;
$ ( '#tts_narrate_quoted' ) . on ( 'click' , onNarrateQuotedClick ) ;
$ ( '#tts_narrate_translated_only' ) . on ( 'click' , onNarrateTranslatedOnlyClick ) ;
2023-12-10 17:32:10 +01:00
$ ( '#tts_skip_codeblocks' ) . on ( 'click' , onSkipCodeblocksClick ) ;
2023-07-20 19:32:15 +02:00
$ ( '#tts_auto_generation' ) . on ( 'click' , onAutoGenerationClick ) ;
2023-08-29 14:27:22 +02:00
$ ( '#tts_narrate_user' ) . on ( 'click' , onNarrateUserClick ) ;
2023-12-02 20:11:06 +01:00
$ ( '#tts_voices' ) . on ( 'click' , onTtsVoicesClick ) ;
2023-07-20 19:32:15 +02:00
for ( const provider in ttsProviders ) {
2023-12-02 20:11:06 +01:00
$ ( '#tts_provider' ) . append ( $ ( '<option />' ) . val ( provider ) . text ( provider ) ) ;
2023-07-20 19:32:15 +02:00
}
2023-12-02 20:11:06 +01:00
$ ( '#tts_provider' ) . on ( 'change' , onTtsProviderChange ) ;
2023-07-20 19:32:15 +02:00
$ ( document ) . on ( 'click' , '.mes_narrate' , onNarrateOneMessage ) ;
}
2023-12-02 20:11:06 +01:00
addExtensionControls ( ) ; // No init dependencies
loadSettings ( ) ; // Depends on Extension Controls and loadTtsProvider
loadTtsProvider ( extension _settings . tts . currentProvider ) ; // No dependencies
addAudioControl ( ) ; // Depends on Extension Controls
2023-07-20 19:32:15 +02:00
const wrapper = new ModuleWorkerWrapper ( moduleWorker ) ;
2023-12-02 20:11:06 +01:00
setInterval ( wrapper . update . bind ( wrapper ) , UPDATE _INTERVAL ) ; // Init depends on all the things
2023-07-20 19:32:15 +02:00
eventSource . on ( event _types . MESSAGE _SWIPED , resetTtsPlayback ) ;
2023-12-02 20:11:06 +01:00
eventSource . on ( event _types . CHAT _CHANGED , onChatChanged ) ;
2023-11-08 16:40:47 +01:00
eventSource . on ( event _types . MESSAGE _DELETED , onChatDeleted ) ;
2023-12-02 20:11:06 +01:00
eventSource . on ( event _types . GROUP _UPDATED , onChatChanged ) ;
2023-12-02 19:04:51 +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-12-02 20:11:06 +01:00
} ) ;