2024-06-30 13:06:31 +02:00
import { cancelTtsPlay , eventSource , event _types , getCurrentChatId , isStreamingEnabled , name2 , saveSettingsDebounced , substituteParams } from '../../../script.js' ;
2024-06-24 21:19:21 +02:00
import { ModuleWorkerWrapper , doExtrasFetch , extension _settings , getApiUrl , getContext , modules , renderExtensionTemplateAsync } from '../../extensions.js' ;
2024-01-18 01:36:18 +01:00
import { delay , escapeRegex , getBase64Async , getStringHash , onlyUnique } from '../../utils.js' ;
import { EdgeTtsProvider } from './edge.js' ;
import { ElevenLabsTtsProvider } from './elevenlabs.js' ;
import { SileroTtsProvider } from './silerotts.js' ;
2024-09-08 17:00:25 +02:00
import { GptSovitsV2Provider } from './gpt-sovits-v2.js' ;
2024-01-18 01:36:18 +01:00
import { CoquiTtsProvider } from './coqui.js' ;
import { SystemTtsProvider } from './system.js' ;
import { NovelTtsProvider } from './novel.js' ;
import { power _user } from '../../power-user.js' ;
import { OpenAITtsProvider } from './openai.js' ;
2024-08-13 11:07:46 +02:00
import { OpenAICompatibleTtsProvider } from './openai-compatible.js' ;
2024-01-18 01:36:18 +01:00
import { XTTSTtsProvider } from './xtts.js' ;
2024-06-29 13:15:37 +02:00
import { VITSTtsProvider } from './vits.js' ;
2024-03-13 17:36:56 +01:00
import { GSVITtsProvider } from './gsvi.js' ;
2024-06-09 05:03:09 +02:00
import { SBVits2TtsProvider } from './sbvits2.js' ;
2024-01-18 01:36:18 +01:00
import { AllTalkTtsProvider } from './alltalk.js' ;
2024-09-08 09:23:48 +02:00
import { CosyVoiceProvider } from './cosyvoice.js' ;
2024-02-04 19:31:20 +01:00
import { SpeechT5TtsProvider } from './speecht5.js' ;
2024-05-22 00:37:51 +02:00
import { AzureTtsProvider } from './azure.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js' ;
import { SlashCommand } from '../../slash-commands/SlashCommand.js' ;
import { ARGUMENT _TYPE , SlashCommandArgument , SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js' ;
2024-06-17 20:21:41 +02:00
import { debounce _timeout } from '../../constants.js' ;
2024-06-21 20:04:55 +02:00
import { SlashCommandEnumValue , enumTypes } from '../../slash-commands/SlashCommandEnumValue.js' ;
2024-06-23 14:43:57 +02:00
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js' ;
2024-06-25 23:41:37 +02:00
import { POPUP _TYPE , callGenericPopup } from '../../popup.js' ;
2024-10-12 12:35:11 +02:00
import { GoogleTranslateTtsProvider } from './google-translate.js' ;
2024-01-18 01:36:18 +01:00
export { talkingAnimation } ;
const UPDATE _INTERVAL = 1000 ;
let voiceMapEntries = [ ] ;
let voiceMap = { } ; // {charName:voiceid, charName2:voiceid2}
2024-04-09 16:50:27 +02:00
let talkingHeadState = false ;
2024-01-18 01:36:18 +01:00
let lastChatId = null ;
2024-04-09 16:50:27 +02:00
let lastMessage = null ;
2024-01-18 01:36:18 +01:00
let lastMessageHash = null ;
2024-06-17 20:21:41 +02:00
let periodicMessageGenerationTimer = null ;
let lastPositionOfParagraphEnd = - 1 ;
2024-06-30 06:58:51 +02:00
let currentInitVoiceMapPromise = null ;
2024-01-18 01:36:18 +01:00
const DEFAULT _VOICE _MARKER = '[Default Voice]' ;
const DISABLED _VOICE _MARKER = 'disabled' ;
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 ;
}
2024-04-09 16:50:27 +02:00
const ttsProviders = {
2024-08-13 19:29:33 +02:00
AllTalk : AllTalkTtsProvider ,
Azure : AzureTtsProvider ,
2024-01-18 01:36:18 +01:00
Coqui : CoquiTtsProvider ,
2024-09-27 19:27:51 +02:00
'CosyVoice (Unofficial)' : CosyVoiceProvider ,
2024-01-18 01:36:18 +01:00
Edge : EdgeTtsProvider ,
2024-08-13 19:29:33 +02:00
ElevenLabs : ElevenLabsTtsProvider ,
2024-10-12 12:35:11 +02:00
'Google Translate' : GoogleTranslateTtsProvider ,
2024-08-13 19:29:33 +02:00
GSVI : GSVITtsProvider ,
2024-09-27 19:34:21 +02:00
'GPT-SoVITS-V2 (Unofficial)' : GptSovitsV2Provider ,
2024-01-18 01:36:18 +01:00
Novel : NovelTtsProvider ,
OpenAI : OpenAITtsProvider ,
2024-08-13 19:29:33 +02:00
'OpenAI Compatible' : OpenAICompatibleTtsProvider ,
SBVits2 : SBVits2TtsProvider ,
Silero : SileroTtsProvider ,
2024-02-04 19:31:20 +01:00
SpeechT5 : SpeechT5TtsProvider ,
2024-08-13 19:29:33 +02:00
System : SystemTtsProvider ,
VITS : VITSTtsProvider ,
XTTSv2 : XTTSTtsProvider ,
2024-01-18 01:36:18 +01:00
} ;
let ttsProvider ;
let ttsProviderName ;
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 ( ) ;
}
async function onNarrateText ( args , text ) {
if ( ! text ) {
2024-06-17 20:21:41 +02:00
return '' ;
2024-01-18 01:36:18 +01:00
}
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 ) ;
2024-06-17 20:21:41 +02:00
return '' ;
2024-01-18 01:36:18 +01:00
}
async function moduleWorker ( ) {
2024-04-09 16:50:27 +02:00
if ( ! extension _settings . tts . enabled ) {
2024-01-18 01:36:18 +01:00
return ;
}
processTtsQueue ( ) ;
processAudioJobQueue ( ) ;
updateUiAudioPlayState ( ) ;
}
function talkingAnimation ( switchValue ) {
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( 'Talking Animation module not loaded' ) ;
return ;
}
const apiUrl = getApiUrl ( ) ;
const animationType = switchValue ? 'start' : 'stop' ;
2024-04-09 16:50:27 +02:00
if ( switchValue !== talkingHeadState ) {
2024-01-18 01:36:18 +01:00
try {
console . log ( animationType + ' Talking Animation' ) ;
doExtrasFetch ( ` ${ apiUrl } /api/talkinghead/ ${ animationType } _talking ` ) ;
2024-04-09 16:50:27 +02:00
talkingHeadState = switchValue ;
2024-01-18 01:36:18 +01:00
} catch ( error ) {
// Handle the error here or simply ignore it to prevent logging
}
}
updateUiAudioPlayState ( ) ;
}
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
if ( ttsJobQueue . length > 0 || audioJobQueue . length > 0 ) {
processing = true ;
}
// Check current jobs
if ( currentTtsJob != null || currentAudioJob != null ) {
processing = true ;
}
return processing ;
}
function debugTtsPlayback ( ) {
console . log ( JSON . stringify (
{
'ttsProviderName' : ttsProviderName ,
'voiceMap' : voiceMap ,
'audioPaused' : audioPaused ,
'audioJobQueue' : audioJobQueue ,
'currentAudioJob' : currentAudioJob ,
'audioQueueProcessorReady' : audioQueueProcessorReady ,
'ttsJobQueue' : ttsJobQueue ,
'currentTtsJob' : currentTtsJob ,
'ttsConfig' : extension _settings . tts ,
} ,
) ) ;
}
2024-01-21 14:19:13 +01:00
window [ 'debugTtsPlayback' ] = debugTtsPlayback ;
2024-01-18 01:36:18 +01:00
//##################//
// Audio Control //
//##################//
let audioElement = new Audio ( ) ;
audioElement . id = 'tts_audio' ;
audioElement . autoplay = true ;
2024-01-21 14:19:13 +01:00
/ * *
* @ type AudioJob [ ] Audio job queue
* @ typedef { { audioBlob : Blob | string , char : string } } AudioJob Audio job object
* /
2024-01-18 01:36:18 +01:00
let audioJobQueue = [ ] ;
2024-01-21 14:19:13 +01:00
/ * *
* @ type AudioJob Current audio job
* /
2024-01-18 01:36:18 +01:00
let currentAudioJob ;
let audioPaused = false ;
let audioQueueProcessorReady = true ;
2024-01-21 14:19:13 +01:00
/ * *
* Play audio data from audio job object .
* @ param { AudioJob } audioJob Audio job object
* @ returns { Promise < void > } Promise that resolves when audio playback is started
* /
2024-01-16 04:54:14 +01:00
async function playAudioData ( audioJob ) {
2024-01-21 14:19:13 +01:00
const { audioBlob , char } = audioJob ;
2024-01-18 01:36:18 +01:00
// 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' ) ;
}
if ( audioBlob instanceof Blob ) {
const srcUrl = await getBase64Async ( audioBlob ) ;
2024-01-19 09:34:32 +01:00
// VRM lip sync
2024-01-21 14:11:47 +01:00
if ( extension _settings . vrm ? . enabled && typeof window [ 'vrmLipSync' ] === 'function' ) {
2024-01-21 14:19:13 +01:00
await window [ 'vrmLipSync' ] ( audioBlob , char ) ;
2024-01-16 04:54:14 +01:00
}
2024-01-19 09:34:32 +01:00
2024-01-18 01:36:18 +01:00
audioElement . src = srcUrl ;
} else if ( typeof audioBlob === 'string' ) {
audioElement . src = audioBlob ;
} else {
throw ` TTS received invalid audio data type ${ typeof audioBlob } ` ;
}
audioElement . addEventListener ( 'ended' , completeCurrentAudioJob ) ;
audioElement . addEventListener ( 'canplay' , ( ) => {
console . debug ( 'Starting TTS playback' ) ;
2024-05-16 08:16:25 +02:00
audioElement . playbackRate = extension _settings . tts . playback _rate ;
2024-01-18 01:36:18 +01:00
audioElement . play ( ) ;
} ) ;
}
window [ 'tts_preview' ] = function ( id ) {
const audio = document . getElementById ( id ) ;
2024-01-21 14:19:13 +01:00
if ( audio instanceof HTMLAudioElement && ! $ ( audio ) . data ( 'disabled' ) ) {
2024-01-18 01:36:18 +01:00
audio . play ( ) ;
}
else {
ttsProvider . previewTtsVoice ( id ) ;
}
} ;
async function onTtsVoicesClick ( ) {
let popupText = '' ;
try {
const voiceIds = await ttsProvider . fetchTtsVoiceObjects ( ) ;
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.' ;
}
2024-06-25 23:41:37 +02:00
callGenericPopup ( popupText , POPUP _TYPE . TEXT , '' , { allowVerticalScrolling : true } ) ;
2024-01-18 01:36:18 +01:00
}
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 ( ) ;
talkingAnimation ( false ) ;
} 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 ( ) {
2024-06-24 22:17:58 +02:00
$ ( '#tts_wand_container' ) . append ( `
2024-01-18 01:36:18 +01:00
< 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 > ` ) ;
2024-07-10 22:32:00 +02:00
$ ( '#tts_wand_container' ) . append ( `
< div id = "ttsExtensionNarrateAll" class = "list-group-item flex-container flexGap5" >
< div class = "extensionsMenuExtensionButton fa-solid fa-radio" > < / d i v >
Narrate All Chat
< / d i v > ` ) ;
2024-01-18 01:36:18 +01:00
$ ( '#ttsExtensionMenuItem' ) . attr ( 'title' , 'TTS play/pause' ) . on ( 'click' , onAudioControlClicked ) ;
2024-07-10 22:32:00 +02:00
$ ( '#ttsExtensionNarrateAll' ) . attr ( 'title' , 'Narrate all messages in the current chat. Includes user messages, excludes hidden comments.' ) . on ( 'click' , playFullConversation ) ;
2024-01-18 01:36:18 +01:00
updateUiAudioPlayState ( ) ;
}
function completeCurrentAudioJob ( ) {
audioQueueProcessorReady = true ;
currentAudioJob = null ;
talkingAnimation ( false ) ; //stop lip animation
// updateUiPlayState();
}
/ * *
* Accepts an HTTP response containing audio / mpeg data , and puts the data as a Blob ( ) on the queue for playback
* @ param { Response } response
* /
2024-01-16 04:54:14 +01:00
async function addAudioJob ( response , char ) {
2024-01-18 01:36:18 +01:00
if ( typeof response === 'string' ) {
2024-01-21 14:19:13 +01:00
audioJobQueue . push ( { audioBlob : response , char : char } ) ;
2024-01-18 01:36:18 +01:00
} 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 } ` ;
}
2024-01-21 14:19:13 +01:00
audioJobQueue . push ( { audioBlob : audioData , char : char } ) ;
2024-01-18 01:36:18 +01:00
}
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 . shift ( ) ;
playAudioData ( currentAudioJob ) ;
talkingAnimation ( true ) ;
} catch ( error ) {
2024-04-06 00:41:36 +02:00
toastr . error ( error . toString ( ) ) ;
2024-01-18 01:36:18 +01:00
console . error ( error ) ;
audioQueueProcessorReady = true ;
}
}
//################//
// TTS Control //
//################//
let ttsJobQueue = [ ] ;
let currentTtsJob ; // Null if nothing is currently being processed
function completeTtsJob ( ) {
console . info ( ` Current TTS job for ${ currentTtsJob ? . name } completed. ` ) ;
currentTtsJob = null ;
}
async function tts ( text , voiceId , char ) {
async function processResponse ( response ) {
// RVC injection
2024-08-26 09:30:17 +02:00
if ( typeof window [ 'rvcVoiceConversion' ] === 'function' && extension _settings . rvc . enabled )
2024-01-18 01:36:18 +01:00
response = await window [ 'rvcVoiceConversion' ] ( response , char , text ) ;
2024-01-16 04:54:14 +01:00
await addAudioJob ( response , char ) ;
2024-01-18 01:36:18 +01:00
}
let response = await ttsProvider . generateTts ( text , voiceId ) ;
// 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 ) ;
}
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 ( ) ;
let text = extension _settings . tts . narrate _translated _only ? ( currentTtsJob ? . extra ? . display _text || currentTtsJob . mes ) : currentTtsJob . mes ;
2024-04-21 00:02:49 +02:00
// Substitute macros
text = substituteParams ( text ) ;
2024-01-18 01:36:18 +01:00
if ( extension _settings . tts . skip _codeblocks ) {
text = text . replace ( /^\s{4}.*$/gm , '' ) . trim ( ) ;
text = text . replace ( / ` ` ` . * ? ` ` ` / g s , ' ' ) . t r i m ( ) ;
}
2024-03-17 23:31:16 +01:00
if ( extension _settings . tts . skip _tags ) {
text = text . replace ( /<.*?>.*?<\/.*?>/g , '' ) . trim ( ) ;
}
2024-01-18 01:36:18 +01:00
if ( ! extension _settings . tts . pass _asterisks ) {
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 ) {
2024-10-05 19:14:07 +02:00
const special _quotes = /[“”«»「」『』""]/g ; // Extend this regex to include other special quotes
2024-01-18 01:36:18 +01:00
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 ;
}
if ( typeof ttsProvider ? . processText === 'function' ) {
text = await ttsProvider . processText ( text ) ;
}
// Collapse newlines and spaces into single space
text = text . replace ( /\s+/g , ' ' ) . trim ( ) ;
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 ;
}
const voiceMapEntry = voiceMap [ char ] === DEFAULT _VOICE _MARKER ? voiceMap [ DEFAULT _VOICE _MARKER ] : voiceMap [ char ] ;
if ( ! voiceMapEntry || voiceMapEntry === DISABLED _VOICE _MARKER ) {
throw ` ${ char } not in voicemap. Configure character in extension settings voice map ` ;
}
const voice = await ttsProvider . getVoice ( voiceMapEntry ) ;
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 } ` ;
}
2024-04-06 00:57:51 +02:00
await tts ( text , voiceId , char ) ;
2024-01-18 01:36:18 +01:00
} catch ( error ) {
2024-04-06 00:41:36 +02:00
toastr . error ( error . toString ( ) ) ;
2024-01-18 01:36:18 +01:00
console . error ( error ) ;
currentTtsJob = null ;
}
}
async function playFullConversation ( ) {
2024-07-10 22:32:00 +02:00
resetTtsPlayback ( ) ;
if ( ! extension _settings . tts . enabled ) {
return toastr . warning ( 'TTS is disabled. Please enable it in the extension settings.' ) ;
}
2024-01-18 01:36:18 +01:00
const context = getContext ( ) ;
2024-07-10 22:32:00 +02:00
const chat = context . chat . filter ( x => ! x . is _system && x . mes !== '...' && x . mes !== '' ) ;
if ( chat . length === 0 ) {
return toastr . info ( 'No messages to narrate.' ) ;
}
2024-01-18 01:36:18 +01:00
ttsJobQueue = chat ;
}
2024-07-10 22:32:00 +02:00
2024-01-21 14:19:13 +01:00
window [ 'playFullConversation' ] = playFullConversation ;
2024-01-18 01:36:18 +01:00
//#############################//
// Extension UI and Settings //
//#############################//
function loadSettings ( ) {
if ( Object . keys ( extension _settings . tts ) . length === 0 ) {
Object . assign ( extension _settings . tts , defaultSettings ) ;
}
for ( const key in defaultSettings ) {
if ( ! ( key in extension _settings . tts ) ) {
extension _settings . tts [ key ] = defaultSettings [ key ] ;
}
}
$ ( '#tts_provider' ) . val ( extension _settings . tts . currentProvider ) ;
$ ( '#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 ) ;
2024-06-17 20:17:31 +02:00
$ ( '#tts_periodic_auto_generation' ) . prop ( 'checked' , extension _settings . tts . periodic _auto _generation ) ;
2024-01-18 01:36:18 +01:00
$ ( '#tts_narrate_translated_only' ) . prop ( 'checked' , extension _settings . tts . narrate _translated _only ) ;
$ ( '#tts_narrate_user' ) . prop ( 'checked' , extension _settings . tts . narrate _user ) ;
$ ( '#tts_pass_asterisks' ) . prop ( 'checked' , extension _settings . tts . pass _asterisks ) ;
2024-03-17 23:17:17 +01:00
$ ( '#tts_skip_codeblocks' ) . prop ( 'checked' , extension _settings . tts . skip _codeblocks ) ;
2024-03-17 23:31:16 +01:00
$ ( '#tts_skip_tags' ) . prop ( 'checked' , extension _settings . tts . skip _tags ) ;
2024-05-16 08:35:48 +02:00
$ ( '#playback_rate' ) . val ( extension _settings . tts . playback _rate ) ;
$ ( '#playback_rate_counter' ) . val ( Number ( extension _settings . tts . playback _rate ) . toFixed ( 2 ) ) ;
$ ( '#playback_rate_block' ) . toggle ( extension _settings . tts . currentProvider !== 'System' ) ;
2024-01-18 01:36:18 +01:00
$ ( 'body' ) . toggleClass ( 'tts' , extension _settings . tts . enabled ) ;
}
const defaultSettings = {
voiceMap : '' ,
ttsEnabled : false ,
currentProvider : 'ElevenLabs' ,
auto _generation : true ,
narrate _user : false ,
2024-05-16 08:35:48 +02:00
playback _rate : 1 ,
2024-01-18 01:36:18 +01:00
} ;
function setTtsStatus ( status , success ) {
$ ( '#tts_status' ) . text ( status ) ;
if ( success ) {
$ ( '#tts_status' ) . removeAttr ( 'style' ) ;
} else {
$ ( '#tts_status' ) . css ( 'color' , 'red' ) ;
}
}
function onRefreshClick ( ) {
Promise . all ( [
ttsProvider . onRefreshClick ( ) ,
// updateVoiceMap()
] ) . then ( ( ) => {
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings ;
saveSettingsDebounced ( ) ;
setTtsStatus ( 'Successfully applied settings' , true ) ;
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` ) ;
initVoiceMap ( ) ;
updateVoiceMap ( ) ;
} ) . catch ( error => {
2024-04-06 00:41:36 +02:00
toastr . error ( error . toString ( ) ) ;
2024-01-18 01:36:18 +01:00
console . error ( error ) ;
setTtsStatus ( error , false ) ;
} ) ;
}
function onEnableClick ( ) {
extension _settings . tts . enabled = $ ( '#tts_enabled' ) . is (
':checked' ,
) ;
updateUiAudioPlayState ( ) ;
saveSettingsDebounced ( ) ;
2024-06-24 21:19:21 +02:00
$ ( 'body' ) . toggleClass ( 'tts' , extension _settings . tts . enabled ) ;
2024-01-18 01:36:18 +01:00
}
function onAutoGenerationClick ( ) {
extension _settings . tts . auto _generation = ! ! $ ( '#tts_auto_generation' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2024-06-17 20:17:31 +02:00
function onPeriodicAutoGenerationClick ( ) {
extension _settings . tts . periodic _auto _generation = ! ! $ ( '#tts_periodic_auto_generation' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2024-01-18 01:36:18 +01:00
function onNarrateDialoguesClick ( ) {
extension _settings . tts . narrate _dialogues _only = ! ! $ ( '#tts_narrate_dialogues' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onNarrateUserClick ( ) {
extension _settings . tts . narrate _user = ! ! $ ( '#tts_narrate_user' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onNarrateQuotedClick ( ) {
extension _settings . tts . narrate _quoted _only = ! ! $ ( '#tts_narrate_quoted' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onNarrateTranslatedOnlyClick ( ) {
extension _settings . tts . narrate _translated _only = ! ! $ ( '#tts_narrate_translated_only' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onSkipCodeblocksClick ( ) {
extension _settings . tts . skip _codeblocks = ! ! $ ( '#tts_skip_codeblocks' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2024-03-17 23:31:16 +01:00
function onSkipTagsClick ( ) {
extension _settings . tts . skip _tags = ! ! $ ( '#tts_skip_tags' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
2024-01-18 01:36:18 +01:00
function onPassAsterisksClick ( ) {
extension _settings . tts . pass _asterisks = ! ! $ ( '#tts_pass_asterisks' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
2024-01-20 19:40:40 +01:00
console . log ( 'setting pass asterisks' , extension _settings . tts . pass _asterisks ) ;
2024-01-18 01:36:18 +01:00
}
//##############//
// TTS Provider //
//##############//
async function loadTtsProvider ( provider ) {
//Clear the current config and add new config
$ ( '#tts_provider_settings' ) . html ( '' ) ;
if ( ! provider ) {
return ;
}
// 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 ] = { } ;
}
await ttsProvider . loadSettings ( extension _settings . tts [ ttsProviderName ] ) ;
await initVoiceMap ( ) ;
}
function onTtsProviderChange ( ) {
const ttsProviderSelection = $ ( '#tts_provider' ) . val ( ) ;
extension _settings . tts . currentProvider = ttsProviderSelection ;
2024-05-16 08:35:48 +02:00
$ ( '#playback_rate_block' ) . toggle ( extension _settings . tts . currentProvider !== 'System' ) ;
2024-01-18 01:36:18 +01:00
loadTtsProvider ( ttsProviderSelection ) ;
}
// Ensure that TTS provider settings are saved to extension settings.
export function saveTtsProviderSettings ( ) {
extension _settings . tts [ ttsProviderName ] = ttsProvider . settings ;
updateVoiceMap ( ) ;
saveSettingsDebounced ( ) ;
console . info ( ` Saved settings ${ ttsProviderName } ${ JSON . stringify ( ttsProvider . settings ) } ` ) ;
}
//###################//
// voiceMap Handling //
//###################//
async function onChatChanged ( ) {
2024-06-17 20:21:41 +02:00
await onGenerationEnded ( ) ;
resetTtsPlayback ( ) ;
2024-01-18 01:36:18 +01:00
const voiceMapInit = initVoiceMap ( ) ;
2024-06-17 20:21:41 +02:00
await Promise . race ( [ voiceMapInit , delay ( debounce _timeout . relaxed ) ] ) ;
2024-04-09 16:50:27 +02:00
lastMessage = null ;
}
2024-06-17 20:17:31 +02:00
async function onMessageEvent ( messageId , lastCharIndex ) {
2024-04-09 16:50:27 +02:00
// If TTS is disabled, do nothing
if ( ! extension _settings . tts . enabled ) {
return ;
}
// Auto generation is disabled
if ( ! extension _settings . tts . auto _generation ) {
return ;
}
const context = getContext ( ) ;
// no characters or group selected
if ( ! context . groupId && context . characterId === undefined ) {
return ;
}
// Chat changed
if ( context . chatId !== lastChatId ) {
lastChatId = context . chatId ;
lastMessageHash = getStringHash ( context . chat [ messageId ] ? . mes ? ? '' ) ;
// Force to speak on the first message in the new chat
if ( context . chat . length === 1 ) {
lastMessageHash = - 1 ;
}
}
// clone message object, as things go haywire if message object is altered below (it's passed by reference)
const message = structuredClone ( context . chat [ messageId ] ) ;
const hashNew = getStringHash ( message ? . mes ? ? '' ) ;
2024-08-22 21:13:57 +02:00
// Ignore prompt-hidden messages
if ( message . is _system ) {
return ;
}
2024-04-09 16:50:27 +02:00
// if no new messages, or same message, or same message hash, do nothing
if ( hashNew === lastMessageHash ) {
return ;
}
2024-06-17 20:17:31 +02:00
// if we only want to process part of the message
if ( lastCharIndex ) {
2024-06-17 20:21:41 +02:00
message . mes = message . mes . substring ( 0 , lastCharIndex ) ;
2024-06-17 20:17:31 +02:00
}
2024-04-09 16:50:27 +02:00
const isLastMessageInCurrent = ( ) =>
lastMessage &&
typeof lastMessage === 'object' &&
message . swipe _id === lastMessage . swipe _id &&
2024-06-17 20:21:41 +02:00
message . name === lastMessage . name &&
message . is _user === lastMessage . is _user &&
2024-04-09 16:50:27 +02:00
message . mes . indexOf ( lastMessage . mes ) !== - 1 ;
// if last message within current message, message got extended. only send diff to TTS.
if ( isLastMessageInCurrent ( ) ) {
const tmp = structuredClone ( message ) ;
message . mes = message . mes . replace ( lastMessage . mes , '' ) ;
lastMessage = tmp ;
} else {
lastMessage = structuredClone ( message ) ;
}
// We're currently swiping. Don't generate voice
if ( ! message || message . mes === '...' || message . mes === '' ) {
return ;
}
// Don't generate if message doesn't have a display text
if ( extension _settings . tts . narrate _translated _only && ! ( message ? . extra ? . display _text ) ) {
return ;
}
// 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 ;
}
// New messages, add new chat to history
lastMessageHash = hashNew ;
lastChatId = context . chatId ;
console . debug ( ` Adding message from ${ message . name } for TTS processing: " ${ message . mes } " ` ) ;
ttsJobQueue . push ( message ) ;
2024-01-18 01:36:18 +01:00
}
2024-04-09 16:50:27 +02:00
async function onMessageDeleted ( ) {
2024-01-18 01:36:18 +01:00
const context = getContext ( ) ;
// update internal references to new last message
lastChatId = context . chatId ;
// compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
2024-04-09 16:50:27 +02:00
const messageHash = getStringHash ( ( context . chat . length && context . chat [ context . chat . length - 1 ] . mes ) ? ? '' ) ;
2024-01-18 01:36:18 +01:00
if ( messageHash === lastMessageHash ) {
return ;
}
lastMessageHash = messageHash ;
2024-04-09 16:50:27 +02:00
lastMessage = context . chat . length ? structuredClone ( context . chat [ context . chat . length - 1 ] ) : null ;
2024-01-18 01:36:18 +01:00
// stop any tts playback since message might not exist anymore
2024-04-09 16:50:27 +02:00
resetTtsPlayback ( ) ;
2024-01-18 01:36:18 +01:00
}
2024-06-17 20:21:41 +02:00
async function onGenerationStarted ( generationType , _args , isDryRun ) {
// If dry running or quiet mode, do nothing
if ( isDryRun || [ 'quiet' , 'impersonate' ] . includes ( generationType ) ) {
return ;
}
2024-06-17 20:17:31 +02:00
// If TTS is disabled, do nothing
if ( ! extension _settings . tts . enabled ) {
return ;
}
// Auto generation is disabled
if ( ! extension _settings . tts . auto _generation ) {
return ;
}
// Periodic auto generation is disabled
if ( ! extension _settings . tts . periodic _auto _generation ) {
return ;
}
2024-06-17 20:21:41 +02:00
// If the reply is not being streamed
if ( ! isStreamingEnabled ( ) ) {
return ;
}
2024-06-17 20:17:31 +02:00
// start the timer
2024-06-17 20:21:41 +02:00
if ( ! periodicMessageGenerationTimer ) {
periodicMessageGenerationTimer = setInterval ( onPeriodicMessageGenerationTick , UPDATE _INTERVAL ) ;
2024-06-17 20:17:31 +02:00
}
}
async function onGenerationEnded ( ) {
2024-06-17 20:21:41 +02:00
if ( periodicMessageGenerationTimer ) {
clearInterval ( periodicMessageGenerationTimer ) ;
periodicMessageGenerationTimer = null ;
2024-06-17 20:17:31 +02:00
}
lastPositionOfParagraphEnd = - 1 ;
}
async function onPeriodicMessageGenerationTick ( ) {
const context = getContext ( ) ;
// no characters or group selected
if ( ! context . groupId && context . characterId === undefined ) {
return ;
}
const lastMessageId = context . chat . length - 1 ;
// the last message was from the user
if ( context . chat [ lastMessageId ] . is _user ) {
2024-06-17 20:21:41 +02:00
return ;
2024-06-17 20:17:31 +02:00
}
const lastMessage = structuredClone ( context . chat [ lastMessageId ] ) ;
const lastMessageText = lastMessage ? . mes ? ? '' ;
// look for double ending lines which should indicate the end of a paragraph
2024-06-17 20:21:41 +02:00
let newLastPositionOfParagraphEnd = lastMessageText
2024-06-17 20:17:31 +02:00
. indexOf ( '\n\n' , lastPositionOfParagraphEnd + 1 ) ;
// if not found, look for a single ending line which should indicate the end of a paragraph
if ( newLastPositionOfParagraphEnd === - 1 ) {
newLastPositionOfParagraphEnd = lastMessageText
. indexOf ( '\n' , lastPositionOfParagraphEnd + 1 ) ;
}
// send the message to the tts module if we found the new end of a paragraph
if ( newLastPositionOfParagraphEnd > - 1 ) {
onMessageEvent ( lastMessageId , newLastPositionOfParagraphEnd ) ;
2024-06-17 20:21:41 +02:00
if ( periodicMessageGenerationTimer ) {
2024-06-17 20:17:31 +02:00
lastPositionOfParagraphEnd = newLastPositionOfParagraphEnd ;
}
}
}
2024-01-18 01:36:18 +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 ) {
const context = getContext ( ) ;
if ( unrestricted ) {
const names = context . characters . map ( char => char . name ) ;
names . unshift ( DEFAULT _VOICE _MARKER ) ;
return names . filter ( onlyUnique ) ;
}
let characters = [ ] ;
if ( context . groupId === null ) {
// Single char chat
characters . push ( DEFAULT _VOICE _MARKER ) ;
characters . push ( context . name1 ) ;
characters . push ( context . name2 ) ;
} else {
// Group chat
characters . push ( DEFAULT _VOICE _MARKER ) ;
characters . push ( context . name1 ) ;
const group = context . groups . find ( group => context . groupId == group . id ) ;
for ( let member of group . members ) {
const character = context . characters . find ( char => char . avatar == member ) ;
if ( character ) {
characters . push ( character . name ) ;
}
}
}
return characters . filter ( onlyUnique ) ;
}
function sanitizeId ( input ) {
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
2024-06-25 15:23:55 +02:00
let sanitized = encodeURIComponent ( input ) . replace ( /[^a-zA-Z0-9-_]/g , '' ) ;
2024-01-18 01:36:18 +01:00
// Ensure first character is always a letter
if ( ! /^[a-zA-Z]/ . test ( sanitized ) ) {
sanitized = 'element_' + sanitized ;
}
return sanitized ;
}
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 = { } ;
for ( const voice of voiceMapEntries ) {
if ( voice . voiceId === null ) {
continue ;
}
tempVoiceMap [ voice . name ] = voice . voiceId ;
}
if ( Object . keys ( tempVoiceMap ) . length !== 0 ) {
voiceMap = tempVoiceMap ;
console . log ( ` Voicemap updated to ${ JSON . stringify ( voiceMap ) } ` ) ;
}
if ( ! extension _settings . tts [ ttsProviderName ] . voiceMap ) {
extension _settings . tts [ ttsProviderName ] . voiceMap = { } ;
}
Object . assign ( extension _settings . tts [ ttsProviderName ] . voiceMap , voiceMap ) ;
saveSettingsDebounced ( ) ;
}
class VoiceMapEntry {
name ;
voiceId ;
selectElement ;
constructor ( name , voiceId = DEFAULT _VOICE _MARKER ) {
this . name = name ;
this . voiceId = voiceId ;
this . selectElement = null ;
}
addUI ( voiceIds ) {
let sanitizedName = sanitizeId ( this . name ) ;
let defaultOption = this . name === DEFAULT _VOICE _MARKER ?
` <option> ${ DISABLED _VOICE _MARKER } </option> ` :
` <option> ${ DEFAULT _VOICE _MARKER } </option><option> ${ DISABLED _VOICE _MARKER } </option> ` ;
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' >
$ { defaultOption }
< / s e l e c t >
< / d i v >
` ;
$ ( '#tts_voicemap_block' ) . append ( template ) ;
// Populate voice ID select list
for ( const voiceId of voiceIds ) {
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 ( ) ;
}
}
/ * *
* Init voiceMapEntries for character select list .
2024-06-30 06:58:51 +02:00
* If an initialization is already in progress , it returns the existing Promise instead of starting a new one .
2024-01-18 01:36:18 +01:00
* @ param { boolean } unrestricted - If true , will include all characters in voiceMapEntries , even if they are not in the current chat .
2024-06-30 06:58:51 +02:00
* @ returns { Promise } A promise that resolves when the initialization is complete .
2024-01-18 01:36:18 +01:00
* /
export async function initVoiceMap ( unrestricted = false ) {
2024-06-30 06:58:51 +02:00
// Preventing parallel execution
if ( currentInitVoiceMapPromise ) {
return currentInitVoiceMapPromise ;
}
currentInitVoiceMapPromise = ( async ( ) => {
2024-06-30 13:06:31 +02:00
const initialChatId = getCurrentChatId ( ) ;
2024-06-30 06:58:51 +02:00
try {
await initVoiceMapInternal ( unrestricted ) ;
} finally {
currentInitVoiceMapPromise = null ;
}
2024-06-30 13:06:31 +02:00
const currentChatId = getCurrentChatId ( ) ;
if ( initialChatId !== currentChatId ) {
// Chat changed during initialization, reinitialize
await initVoiceMap ( unrestricted ) ;
}
2024-06-30 06:58:51 +02:00
} ) ( ) ;
return currentInitVoiceMapPromise ;
}
/ * *
* Init voiceMapEntries for character select list .
* @ param { boolean } unrestricted - If true , will include all characters in voiceMapEntries , even if they are not in the current chat .
* /
async function initVoiceMapInternal ( unrestricted ) {
2024-01-18 01:36:18 +01:00
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
const enabled = $ ( '#tts_enabled' ) . is ( ':checked' ) ;
if ( ! enabled ) {
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 ) ;
// Clear existing voiceMap state
$ ( '#tts_voicemap_block' ) . empty ( ) ;
voiceMapEntries = [ ] ;
// Get characters in current chat
const characters = getCharacters ( unrestricted ) ;
// Get saved voicemap from provider settings, handling new and old representations
let voiceMapFromSettings = { } ;
if ( 'voiceMap' in extension _settings . tts [ ttsProviderName ] ) {
// Handle previous representation
if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === 'string' ) {
voiceMapFromSettings = parseVoiceMap ( extension _settings . tts [ ttsProviderName ] . voiceMap ) ;
// Handle new representation
} else if ( typeof extension _settings . tts [ ttsProviderName ] . voiceMap === 'object' ) {
voiceMapFromSettings = extension _settings . tts [ ttsProviderName ] . voiceMap ;
}
}
// Get voiceIds from provider
let voiceIdsFromProvider ;
try {
voiceIdsFromProvider = await ttsProvider . fetchTtsVoiceObjects ( ) ;
}
catch {
toastr . error ( 'TTS Provider failed to return voice ids.' ) ;
}
// Build UI using VoiceMapEntry objects
for ( const character of characters ) {
if ( character === 'SillyTavern System' ) {
continue ;
}
// Check provider settings for voiceIds
let voiceId ;
if ( character in voiceMapFromSettings ) {
voiceId = voiceMapFromSettings [ character ] ;
} else if ( character === DEFAULT _VOICE _MARKER ) {
voiceId = DISABLED _VOICE _MARKER ;
} else {
voiceId = DEFAULT _VOICE _MARKER ;
}
const voiceMapEntry = new VoiceMapEntry ( character , voiceId ) ;
voiceMapEntry . addUI ( voiceIdsFromProvider ) ;
voiceMapEntries . push ( voiceMapEntry ) ;
}
updateVoiceMap ( ) ;
}
2024-06-24 21:19:21 +02:00
jQuery ( async function ( ) {
async function addExtensionControls ( ) {
const settingsHtml = $ ( await renderExtensionTemplateAsync ( 'tts' , 'settings' ) ) ;
2024-06-24 21:15:08 +02:00
$ ( '#tts_container' ) . append ( settingsHtml ) ;
2024-01-18 01:36:18 +01:00
$ ( '#tts_refresh' ) . on ( 'click' , onRefreshClick ) ;
$ ( '#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_skip_codeblocks' ) . on ( 'click' , onSkipCodeblocksClick ) ;
2024-03-17 23:31:16 +01:00
$ ( '#tts_skip_tags' ) . on ( 'click' , onSkipTagsClick ) ;
2024-01-18 01:36:18 +01:00
$ ( '#tts_pass_asterisks' ) . on ( 'click' , onPassAsterisksClick ) ;
$ ( '#tts_auto_generation' ) . on ( 'click' , onAutoGenerationClick ) ;
2024-06-17 20:17:31 +02:00
$ ( '#tts_periodic_auto_generation' ) . on ( 'click' , onPeriodicAutoGenerationClick ) ;
2024-01-18 01:36:18 +01:00
$ ( '#tts_narrate_user' ) . on ( 'click' , onNarrateUserClick ) ;
2024-05-16 08:16:25 +02:00
$ ( '#playback_rate' ) . on ( 'input' , function ( ) {
const value = $ ( this ) . val ( ) ;
const formattedValue = Number ( value ) . toFixed ( 2 ) ;
extension _settings . tts . playback _rate = value ;
$ ( '#playback_rate_counter' ) . val ( formattedValue ) ;
saveSettingsDebounced ( ) ;
} ) ;
2024-01-18 01:36:18 +01: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 ) ;
}
2024-06-24 21:19:21 +02:00
await addExtensionControls ( ) ; // No init dependencies
2024-01-18 01:36:18 +01:00
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 ) ;
eventSource . on ( event _types . CHAT _CHANGED , onChatChanged ) ;
2024-04-09 16:50:27 +02:00
eventSource . on ( event _types . MESSAGE _DELETED , onMessageDeleted ) ;
2024-01-18 01:36:18 +01:00
eventSource . on ( event _types . GROUP _UPDATED , onChatChanged ) ;
2024-06-17 20:17:31 +02:00
eventSource . on ( event _types . GENERATION _STARTED , onGenerationStarted ) ;
eventSource . on ( event _types . GENERATION _ENDED , onGenerationEnded ) ;
2024-05-13 17:53:54 +02:00
eventSource . makeLast ( event _types . CHARACTER _MESSAGE _RENDERED , onMessageEvent ) ;
eventSource . makeLast ( event _types . USER _MESSAGE _RENDERED , onMessageEvent ) ;
2024-06-17 20:21:41 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'speak' ,
2024-06-23 14:44:53 +02:00
callback : async ( args , value ) => {
await onNarrateText ( args , value ) ;
2024-06-17 07:04:10 +02:00
return '' ;
} ,
2024-05-12 21:15:05 +02:00
aliases : [ 'narrate' , 'tts' ] ,
namedArgumentList : [
2024-06-21 20:04:55 +02:00
SlashCommandNamedArgument . fromProps ( {
name : 'voice' ,
description : 'character voice name' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : false ,
2024-06-23 14:43:57 +02:00
enumProvider : ( ) => Object . keys ( voiceMap ) . map ( voiceName => new SlashCommandEnumValue ( voiceName , null , enumTypes . enum , enumIcons . voice ) ) ,
2024-06-21 20:04:55 +02:00
} ) ,
2024-05-12 21:15:05 +02:00
] ,
unnamedArgumentList : [
new SlashCommandArgument (
'text' , [ ARGUMENT _TYPE . STRING ] , true ,
) ,
] ,
helpString : `
< div >
Narrate any text using currently selected character ' s voice .
< / d i v >
< div >
Use < code > voice = "Character Name" < / c o d e > a r g u m e n t t o s e t o t h e r v o i c e f r o m t h e v o i c e m a p .
< / d i v >
< div >
< strong > Example : < / s t r o n g >
< ul >
< li >
< pre > < code > / s p e a k v o i c e = " D o n a l d D u c k " Q u a c k ! < / c o d e > < / p r e >
< / l i >
< / u l >
< / d i v >
` ,
} ) ) ;
2024-01-18 01:36:18 +01:00
document . body . appendChild ( audioElement ) ;
} ) ;