update checkReady. add voiceMap ui select

This commit is contained in:
ouoertheo 2023-08-23 08:27:53 -05:00
parent 56fcf1cbb8
commit 5b43fe25e8
8 changed files with 234 additions and 131 deletions

View File

@ -42,8 +42,9 @@ const languageLabels = {
function throwIfModuleMissing() {
if (!modules.includes('coqui-tts')) {
toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(DEBUG_PREFIX, `Coqui TTS module not loaded.`);
const message = `Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.`
// toastr.error(message, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(DEBUG_PREFIX, message);
}
}
@ -84,7 +85,6 @@ class CoquiTtsProvider {
//#############################//
settings
ready
defaultSettings = {
voiceMap: "",
@ -148,7 +148,6 @@ class CoquiTtsProvider {
loadSettings(settings) {
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
this.ready = false
for (const key in settings) {
if (key in this.settings) {
@ -231,17 +230,8 @@ class CoquiTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
try {
if (!modules.includes('coqui-tts')){
this.ready = false
return
}
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
throwIfModuleMissing()
await this.fetchTtsVoiceIds()
}
updateVoiceMap() {

View File

@ -11,7 +11,6 @@ class EdgeTtsProvider {
//########//
settings
ready = false
voices = []
separator = ' . '
audioElement = document.createElement('audio')
@ -61,17 +60,8 @@ class EdgeTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
try {
if (!modules.includes('edge-tts')){
this.ready = false
return
}
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
throwIfModuleMissing()
await this.fetchTtsVoiceIds()
}
async onApplyClick() {
@ -162,8 +152,9 @@ class EdgeTtsProvider {
}
function throwIfModuleMissing() {
if (!modules.includes('edge-tts')) {
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
throw new Error(`Edge TTS module not loaded.`)
const message = `Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`
// toastr.error(message)
throw new Error(message)
}
}

View File

@ -6,7 +6,6 @@ class ElevenLabsTtsProvider {
//########//
settings
ready = false
voices = []
separator = ' ... ... ... '
@ -74,13 +73,7 @@ class ElevenLabsTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
try {
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
await this.fetchTtsVoiceIds()
}
async onApplyClick() {

View File

@ -13,6 +13,7 @@ export { talkingAnimation };
const UPDATE_INTERVAL = 1000
let voiceMapEntries = []
let voiceMap = {} // {charName:voiceid, charName2:voiceid2}
let audioControl
let storedvalue = false;
@ -224,8 +225,8 @@ function debugTtsPlayback() {
console.log(JSON.stringify(
{
"ttsProviderName": ttsProviderName,
"voiceMap": voiceMap,
"currentMessageNumber": currentMessageNumber,
"isWorkerBusy": isWorkerBusy,
"audioPaused": audioPaused,
"audioJobQueue": audioJobQueue,
"currentAudioJob": currentAudioJob,
@ -486,6 +487,7 @@ function loadSettings() {
if (Object.keys(extension_settings.tts).length === 0) {
Object.assign(extension_settings.tts, defaultSettings)
}
$('#tts_provider').val(extension_settings.tts.currentProvider)
$('#tts_enabled').prop(
'checked',
extension_settings.tts.enabled
@ -513,59 +515,17 @@ function setTtsStatus(status, success) {
}
}
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
}
async function voicemapIsValid(parsedVoiceMap) {
let valid = true
for (const characterName in parsedVoiceMap) {
const parsedVoiceName = parsedVoiceMap[characterName]
try {
await ttsProvider.getVoice(parsedVoiceName)
} catch (error) {
console.error(error)
valid = false
}
}
return valid
}
async function updateVoiceMap() {
let isValidResult = false
const value = $('#tts_voice_map').val()
const parsedVoiceMap = parseVoiceMap(value)
isValidResult = await voicemapIsValid(parsedVoiceMap)
if (isValidResult) {
ttsProvider.settings.voiceMap = String(value)
// console.debug(`ttsProvider.voiceMap: ${ttsProvider.settings.voiceMap}`)
voiceMap = parsedVoiceMap
console.debug(`Saved new voiceMap: ${value}`)
saveSettingsDebounced()
} else {
throw 'Voice map is invalid, check console for errors'
}
}
function onApplyClick() {
Promise.all([
ttsProvider.onApplyClick(),
updateVoiceMap()
// 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 => {
console.error(error)
setTtsStatus(error, false)
@ -608,13 +568,14 @@ function onNarrateTranslatedOnlyClick() {
// TTS Provider //
//##############//
function loadTtsProvider(provider) {
async function loadTtsProvider(provider) {
//Clear the current config and add new config
$("#tts_provider_settings").html("")
if (!provider) {
provider
return
}
// Init provider references
extension_settings.tts.currentProvider = provider
ttsProviderName = provider
@ -626,27 +587,17 @@ function loadTtsProvider(provider) {
console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`)
extension_settings.tts[ttsProviderName] = {}
}
// Load voicemap settings
let voiceMapFromSettings
if ("voiceMap" in extension_settings.tts[ttsProviderName]) {
voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap
voiceMap = parseVoiceMap(voiceMapFromSettings)
} else {
voiceMapFromSettings = ""
voiceMap = {}
}
$('#tts_voice_map').val(voiceMapFromSettings)
$('#tts_provider').val(ttsProviderName)
ttsProvider.loadSettings(extension_settings.tts[ttsProviderName])
initVoiceMap()
}
function onTtsProviderChange() {
const ttsProviderSelection = $('#tts_provider').val()
extension_settings.tts.currentProvider = ttsProviderSelection
loadTtsProvider(ttsProviderSelection)
}
// Ensure that TTS provider settings are saved to extension settings.
function onTtsProviderSettingsInput() {
ttsProvider.onSettingsChange()
@ -658,6 +609,191 @@ function onTtsProviderSettingsInput() {
}
//###################//
// voiceMap Handling //
//###################//
async function onChatChanged() {
await resetTtsPlayback()
await initVoiceMap()
}
function getCharacters(){
const context = getContext()
let characters = []
if (context.groupId === null){
// Single char chat
characters.push(context.name1)
characters.push(context.name2)
} else {
// Group chat
characters.push(context.name1)
const group = context.groups.find(group => context.groupId == group.id)
for (let member of group.members) {
// Remove suffix
if (member.endsWith('.png')){
member = member.slice(0, -4)
}
characters.push(member)
}
}
return characters
}
function sanitizeId(input) {
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
// 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)}`)
}
extension_settings.tts[ttsProviderName].voiceMap = voiceMap
saveSettingsDebounced()
}
class VoiceMapEntry {
name
voiceId
selectElement
constructor (name, voiceId='disabled') {
this.name = name
this.voiceId = voiceId
this.selectElement = null
}
addUI(voiceIds){
let sanitizedName = sanitizeId(this.name)
let template = `
<div class='tts_voicemap_block_char flex-container flexGap5'>
<span id='tts_voicemap_char_${sanitizedName}'>${this.name}</span>
<select id='tts_voicemap_char_${sanitizedName}_voice'>
<option>disabled</option>
</select>
</div>
`
$('#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. Should only be called when character/chat is changed.
*
*/
async function initVoiceMap(){
// Clear existing voiceMap state
$('#tts_voicemap_block').empty()
voiceMapEntries = []
// 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)
// Get characters in current chat
const characters = getCharacters()
// 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.fetchTtsVoiceIds()
}
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 {
voiceId = 'disabled'
}
const voiceMapEntry = new VoiceMapEntry(character, voiceId)
voiceMapEntry.addUI(voiceIdsFromProvider)
voiceMapEntries.push(voiceMapEntry)
}
updateVoiceMap()
}
$(document).ready(function () {
function addExtensionControls() {
@ -669,6 +805,8 @@ $(document).ready(function () {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div id="tts_status">
</div>
<div>
<span>Select TTS Provider</span> </br>
<select id="tts_provider">
@ -696,16 +834,13 @@ $(document).ready(function () {
<small>Narrate only the translated text</small>
</label>
</div>
<label>Voice Map</label>
<textarea id="tts_voice_map" type="text" class="text_pole textarea_compact" rows="4"
placeholder="Enter comma separated map of charName:ttsName. Example: \nAqua:Bella,\nYou:Josh,"></textarea>
<div id="tts_status">
<div id="tts_voicemap_block">
</div>
<hr>
<form id="tts_provider_settings" class="inline-drawer-content">
</form>
<div class="tts_buttons">
<input id="tts_apply" class="menu_button" type="submit" value="Apply" />
<input id="tts_apply" class="menu_button" type="submit" value="Reload / Apply" />
<input id="tts_voices" class="menu_button" type="submit" value="Available voices" />
</div>
</div>
@ -735,4 +870,6 @@ $(document).ready(function () {
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)
eventSource.on(event_types.GROUP_UPDATED, onChatChanged)
})

View File

@ -9,7 +9,6 @@ class NovelTtsProvider {
//########//
settings
ready = false
voices = []
separator = ' . '
audioElement = document.createElement('audio')
@ -95,13 +94,7 @@ class NovelTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
// Doesnt really do much for Novel, not seeing a good way to test this at the moment.
async checkReady(){
try {
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
await this.fetchTtsVoiceIds()
}
async onApplyClick() {

View File

@ -69,17 +69,7 @@ class SileroTtsProvider {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
try {
if (!modules.includes('silero-tts')){
this.ready = false
return
}
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
await this.fetchTtsVoiceIds()
}
async onApplyClick() {

View File

@ -50,4 +50,20 @@
.voice_preview .fa-play {
cursor: pointer;
}
.tts-button {
margin: 0;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
align-items: center;
justify-content: center;
}
.tts-button:hover {
opacity: 1;
}

View File

@ -146,19 +146,12 @@ class SystemTtsProvider {
$('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch);
$('#system_tts_pitch_output').text(this.settings.pitch);
$('#system_tts_rate_output').text(this.settings.rate);
this.checkReady()
console.info("Settings loaded");
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
try {
await this.fetchTtsVoiceIds()
this.ready = true
} catch {
this.ready = false
}
await this.fetchTtsVoiceIds()
}
async onApplyClick() {