Blame Raw
· · · 215 lines (7.6 KB)
0 contributors
1import { getRequestHeaders } from '../../../script.js';
2import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
3import { splitRecursive } from '../../utils.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';
5import { initVoiceMap } from './index.js';
6
7export { NovelTtsProvider };
8
9class NovelTtsProvider {
10 //########//
11 // Config //
12 //########//
13
14 settings;
15 voices = [];
16 separator = ' . ';
17 audioElement = document.createElement('audio');
18
19 defaultSettings = {
20 voiceMap: {},
21 customVoices: [],
22 };
23
24 /**
25 * Perform any text processing before passing to TTS engine.
26 * @param {string} text Input text
27 * @returns {string} Processed text
28 */
29 processText(text) {
30 // Novel reads tilde as a word. Replace with full stop
31 text = text.replace(/~/g, '.');
32 // Novel reads asterisk as a word. Remove it
33 text = text.replace(/\*/g, '');
34 return text;
35 }
36
37 get settingsHtml() {
38 let html = `
39 <div class="novel_tts_hints">
40 <div>Use NovelAI's TTS engine.</div>
41 <div>
42 The default Voice IDs are only examples. Add custom voices and Novel will create a new random voice for it.
43 Feel free to try different options!
44 </div>
45 <i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
46 </div>
47 <label for="tts-novel-custom-voices-add">Custom Voices</label>
48 <div class="tts_custom_voices">
49 <select id="tts-novel-custom-voices-select"><select>
50 <i id="tts-novel-custom-voices-add" class="tts-button fa-solid fa-plus fa-xl success" title="Add"></i>
51 <i id="tts-novel-custom-voices-delete" class="tts-button fa-solid fa-xmark fa-xl failure" title="Delete"></i>
52 </div>
53 `;
54 return html;
55 }
56
57
58 // Add a new Novel custom voice to provider
59 async addCustomVoice() {
60 const voiceName = await callGenericPopup('Custom Voice name:', POPUP_TYPE.INPUT);
61 this.settings.customVoices.push(voiceName);
62 this.populateCustomVoices();
63 initVoiceMap(); // Update TTS extension voiceMap
64 saveTtsProviderSettings();
65 }
66
67 // Delete selected custom voice from provider
68 deleteCustomVoice() {
69 const selected = $('#tts-novel-custom-voices-select').find(':selected').val();
70 const voiceIndex = this.settings.customVoices.indexOf(selected);
71
72 if (voiceIndex !== -1) {
73 this.settings.customVoices.splice(voiceIndex, 1);
74 }
75 this.populateCustomVoices();
76 initVoiceMap(); // Update TTS extension voiceMap
77 saveTtsProviderSettings();
78 }
79
80 // Create the UI dropdown list of voices in provider
81 populateCustomVoices() {
82 let voiceSelect = $('#tts-novel-custom-voices-select');
83 voiceSelect.empty();
84 this.settings.customVoices.forEach(voice => {
85 voiceSelect.append(`<option>${voice}</option>`);
86 });
87 }
88
89 async loadSettings(settings) {
90 // Populate Provider UI given input settings
91 if (Object.keys(settings).length == 0) {
92 console.info('Using default TTS Provider settings');
93 }
94 $('#tts-novel-custom-voices-add').on('click', () => (this.addCustomVoice()));
95 $('#tts-novel-custom-voices-delete').on('click', () => (this.deleteCustomVoice()));
96
97 // Only accept keys defined in defaultSettings
98 this.settings = this.defaultSettings;
99
100 for (const key in settings) {
101 if (key in this.settings) {
102 this.settings[key] = settings[key];
103 } else {
104 throw `Invalid setting passed to TTS Provider: ${key}`;
105 }
106 }
107
108 this.populateCustomVoices();
109 await this.checkReady();
110 console.debug('NovelTTS: Settings loaded');
111 }
112
113 // Perform a simple readiness check by trying to fetch voiceIds
114 // Doesnt really do much for Novel, not seeing a good way to test this at the moment.
115 async checkReady() {
116 await this.fetchTtsVoiceObjects();
117 }
118
119 async onRefreshClick() {
120 return;
121 }
122
123 //#################//
124 // TTS Interfaces //
125 //#################//
126
127 async getVoice(voiceName) {
128 if (!voiceName) {
129 throw 'TTS Voice name not provided';
130 }
131
132 return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false };
133 }
134
135 /**
136 * Generate TTS audio for the given text using the specified voice.
137 * @param {string} text Text to generate
138 * @param {string} voiceId Voice ID
139 * @returns {AsyncGenerator<Response>} Audio response generator
140 */
141 generateTts(text, voiceId) {
142 return this.fetchTtsGeneration(text, voiceId);
143 }
144
145 //###########//
146 // API CALLS //
147 //###########//
148 async fetchTtsVoiceObjects() {
149 let voices = [
150 { name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false },
151 { name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false },
152 { name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false },
153 { name: 'Claea', voice_id: 'Claea', lang: 'en-US', preview_url: false },
154 { name: 'Lim', voice_id: 'Lim', lang: 'en-US', preview_url: false },
155 { name: 'Aurae', voice_id: 'Aurae', lang: 'en-US', preview_url: false },
156 { name: 'Naia', voice_id: 'Naia', lang: 'en-US', preview_url: false },
157 { name: 'Aulon', voice_id: 'Aulon', lang: 'en-US', preview_url: false },
158 { name: 'Elei', voice_id: 'Elei', lang: 'en-US', preview_url: false },
159 { name: 'Ogma', voice_id: 'Ogma', lang: 'en-US', preview_url: false },
160 { name: 'Raid', voice_id: 'Raid', lang: 'en-US', preview_url: false },
161 { name: 'Pega', voice_id: 'Pega', lang: 'en-US', preview_url: false },
162 { name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false },
163 ];
164
165 // Add in custom voices to the map
166 let addVoices = this.settings.customVoices.map(voice =>
167 ({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false }),
168 );
169 voices = voices.concat(addVoices);
170
171 return voices;
172 }
173
174
175 async previewTtsVoice(id) {
176 this.audioElement.pause();
177 this.audioElement.currentTime = 0;
178
179 const text = getPreviewString('en-US');
180 for await (const response of this.generateTts(text, id)) {
181 const audio = await response.blob();
182 const url = URL.createObjectURL(audio);
183 await new Promise(resolve => {
184 const audioElement = new Audio();
185 audioElement.src = url;
186 audioElement.play();
187 audioElement.onended = () => resolve();
188 });
189 URL.revokeObjectURL(url);
190 }
191 }
192
193 async* fetchTtsGeneration(inputText, voiceId) {
194 const MAX_LENGTH = 1000;
195 console.info(`Generating new TTS for voice_id ${voiceId}`);
196 const chunks = splitRecursive(inputText, MAX_LENGTH);
197 for (const chunk of chunks) {
198 const response = await fetch('/api/novelai/generate-voice',
199 {
200 method: 'POST',
201 headers: getRequestHeaders(),
202 body: JSON.stringify({
203 'text': chunk,
204 'voice': voiceId,
205 }),
206 },
207 );
208 if (!response.ok) {
209 toastr.error(response.statusText, 'TTS Generation Failed');
210 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
211 }
212 yield response;
213 }
214 }
215}