Blame Raw
· · · 270 lines (8.6 KB)
0 contributors
1import { getRequestHeaders } from '../../../script.js';
2import { getApiUrl } from '../../extensions.js';
3import { doExtrasFetch, modules } from '../../extensions.js';
4import { getPreviewString } from './index.js';
5import { saveTtsProviderSettings } from './index.js';
6
7export { EdgeTtsProvider };
8
9const EDGE_TTS_PROVIDER = {
10 extras: 'extras',
11 plugin: 'plugin',
12};
13
14class EdgeTtsProvider {
15 //########//
16 // Config //
17 //########//
18
19 settings;
20 voices = [];
21 separator = ' . ';
22 audioElement = document.createElement('audio');
23
24 defaultSettings = {
25 voiceMap: {},
26 rate: 0,
27 provider: EDGE_TTS_PROVIDER.extras,
28 };
29
30 get settingsHtml() {
31 let html = `Microsoft Edge TTS<br>
32 <label for="edge_tts_provider">Provider</label>
33 <select id="edge_tts_provider">
34 <option value="${EDGE_TTS_PROVIDER.extras}">Extras</option>
35 <option value="${EDGE_TTS_PROVIDER.plugin}">Plugin</option>
36 </select>
37 <label for="edge_tts_rate">Rate: <span id="edge_tts_rate_output"></span></label>
38 <input id="edge_tts_rate" type="range" value="${this.defaultSettings.rate}" min="-100" max="100" step="1" />
39 `;
40 return html;
41 }
42
43 onSettingsChange() {
44 this.settings.rate = Number($('#edge_tts_rate').val());
45 $('#edge_tts_rate_output').text(this.settings.rate);
46 this.settings.provider = String($('#edge_tts_provider').val());
47 saveTtsProviderSettings();
48 }
49
50 async loadSettings(settings) {
51 // Pupulate Provider UI given input settings
52 if (Object.keys(settings).length == 0) {
53 console.info('Using default TTS Provider settings');
54 }
55
56 // Only accept keys defined in defaultSettings
57 this.settings = this.defaultSettings;
58
59 for (const key in settings) {
60 if (key in this.settings) {
61 this.settings[key] = settings[key];
62 } else {
63 throw `Invalid setting passed to TTS Provider: ${key}`;
64 }
65 }
66
67 $('#edge_tts_rate').val(this.settings.rate || 0);
68 $('#edge_tts_rate_output').text(this.settings.rate || 0);
69 $('#edge_tts_rate').on('input', () => { this.onSettingsChange(); });
70 $('#edge_tts_provider').val(this.settings.provider || EDGE_TTS_PROVIDER.extras);
71 $('#edge_tts_provider').on('change', () => { this.onSettingsChange(); });
72 await this.checkReady();
73
74 console.debug('EdgeTTS: Settings loaded');
75 }
76
77 /**
78 * Perform a simple readiness check by trying to fetch voiceIds
79 */
80 async checkReady() {
81 await this.throwIfModuleMissing();
82 await this.fetchTtsVoiceObjects();
83 }
84
85 async onRefreshClick() {
86 return;
87 }
88
89 //#################//
90 // TTS Interfaces //
91 //#################//
92
93 /**
94 * Get a voice from the TTS provider.
95 * @param {string} voiceName Voice name to get
96 * @returns {Promise<Object>} Voice object
97 */
98 async getVoice(voiceName) {
99 if (this.voices.length == 0) {
100 this.voices = await this.fetchTtsVoiceObjects();
101 }
102 const match = this.voices.filter(
103 voice => voice.name == voiceName,
104 )[0];
105 if (!match) {
106 throw `TTS Voice name ${voiceName} not found`;
107 }
108 return match;
109 }
110
111 /**
112 * Generate TTS for a given text.
113 * @param {string} text Text to generate TTS for
114 * @param {string} voiceId Voice ID to use
115 * @returns {Promise<Response>} Fetch response
116 */
117 async generateTts(text, voiceId) {
118 const response = await this.fetchTtsGeneration(text, voiceId);
119 return response;
120 }
121
122 //###########//
123 // API CALLS //
124 //###########//
125 async fetchTtsVoiceObjects() {
126 await this.throwIfModuleMissing();
127
128 const url = this.getVoicesUrl();
129 const response = await this.doFetch(url);
130 if (!response.ok) {
131 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
132 }
133 let responseJson = await response.json();
134 responseJson = responseJson
135 .sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
136 .map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
137 return responseJson;
138 }
139
140 /**
141 * Preview TTS for a given voice ID.
142 * @param {string} id Voice ID
143 */
144 async previewTtsVoice(id) {
145 this.audioElement.pause();
146 this.audioElement.currentTime = 0;
147 const voice = await this.getVoice(id);
148 const text = getPreviewString(voice.lang);
149 const response = await this.fetchTtsGeneration(text, id);
150 if (!response.ok) {
151 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
152 }
153
154 const audio = await response.blob();
155 const url = URL.createObjectURL(audio);
156 this.audioElement.src = url;
157 this.audioElement.play();
158 this.audioElement.onended = () => URL.revokeObjectURL(url);
159 }
160
161 /**
162 * Fetch TTS generation from the API.
163 * @param {string} inputText Text to generate TTS for
164 * @param {string} voiceId Voice ID to use
165 * @returns {Promise<Response>} Fetch response
166 */
167 async fetchTtsGeneration(inputText, voiceId) {
168 await this.throwIfModuleMissing();
169
170 console.info(`Generating new TTS for voice_id ${voiceId}`);
171 const url = this.getGenerateUrl();
172 const response = await this.doFetch(url,
173 {
174 method: 'POST',
175 headers: getRequestHeaders(),
176 body: JSON.stringify({
177 'text': inputText,
178 'voice': voiceId,
179 'rate': Number(this.settings.rate),
180 }),
181 },
182 );
183 if (!response.ok) {
184 toastr.error(response.statusText, 'TTS Generation Failed');
185 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
186 }
187 return response;
188 }
189
190 /**
191 * Perform a fetch request using the configured provider.
192 * @param {string} url URL string
193 * @param {any} options Request options
194 * @returns {Promise<Response>} Fetch response
195 */
196 doFetch(url, options) {
197 if (this.settings.provider === EDGE_TTS_PROVIDER.extras) {
198 return doExtrasFetch(url, options);
199 }
200
201 if (this.settings.provider === EDGE_TTS_PROVIDER.plugin) {
202 return fetch(url, options);
203 }
204
205 throw new Error('Invalid TTS Provider');
206 }
207
208 /**
209 * Get the URL for the TTS generation endpoint.
210 * @returns {string} URL string
211 */
212 getGenerateUrl() {
213 if (this.settings.provider === EDGE_TTS_PROVIDER.extras) {
214 const url = new URL(getApiUrl());
215 url.pathname = '/api/edge-tts/generate';
216 return url.toString();
217 }
218
219 if (this.settings.provider === EDGE_TTS_PROVIDER.plugin) {
220 return '/api/plugins/edge-tts/generate';
221 }
222
223 throw new Error('Invalid TTS Provider');
224 }
225
226 /**
227 * Get the URL for the TTS voices endpoint.
228 * @returns {string} URL object or string
229 */
230 getVoicesUrl() {
231 if (this.settings.provider === EDGE_TTS_PROVIDER.extras) {
232 const url = new URL(getApiUrl());
233 url.pathname = '/api/edge-tts/list';
234 return url.toString();
235 }
236
237 if (this.settings.provider === EDGE_TTS_PROVIDER.plugin) {
238 return '/api/plugins/edge-tts/list';
239 }
240
241 throw new Error('Invalid TTS Provider');
242 }
243
244 async throwIfModuleMissing() {
245 if (this.settings.provider === EDGE_TTS_PROVIDER.extras && !modules.includes('edge-tts')) {
246 const message = 'Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.';
247 // toastr.error(message)
248 throw new Error(message);
249 }
250
251 if (this.settings.provider === EDGE_TTS_PROVIDER.plugin && !this.isPluginAvailable()) {
252 const message = 'Edge TTS Server plugin not loaded. Install it from https://github.com/SillyTavern/SillyTavern-EdgeTTS-Plugin and restart the SillyTavern server.';
253 // toastr.error(message)
254 throw new Error(message);
255 }
256 }
257
258 async isPluginAvailable() {
259 try {
260 const result = await fetch('/api/plugins/edge-tts/probe', {
261 method: 'POST',
262 headers: getRequestHeaders({ omitContentType: true }),
263 });
264 return result.ok;
265 } catch (e) {
266 return false;
267 }
268 }
269}
270