Blame Raw
· · · 344 lines (14.2 KB)
0 contributors
1import { getPreviewString, saveTtsProviderSettings } from './index.js';
2
3export { SBVits2TtsProvider };
4
5class SBVits2TtsProvider {
6 //########//
7 // Config //
8 //########//
9
10 settings;
11 ready = false;
12 voices = [];
13 separator = '. ';
14 audioElement = document.createElement('audio');
15
16 /**
17 * Perform any text processing before passing to TTS engine.
18 * @param {string} text Input text
19 * @returns {string} Processed text
20 */
21 processText(text) {
22 // backup for auto_split
23 text = text.replace(/\n+/g, '<br>');
24 return text;
25 }
26
27 languageLabels = {
28 'Chinese': 'ZH',
29 'English': 'EN',
30 'Japanese': 'JP',
31 };
32
33 langKey2LangCode = {
34 'ZH': 'zh-CN',
35 'EN': 'en-US',
36 'JP': 'ja-JP',
37 };
38
39 defaultSettings = {
40 provider_endpoint: 'http://localhost:5000',
41 sdp_ratio: 0.2,
42 noise: 0.6,
43 noisew: 0.8,
44 length: 1,
45 language: 'JP',
46 auto_split: true,
47 split_interval: 0.5,
48 assist_text: '',
49 assist_text_weight: 1,
50 style: 'Neutral',
51 style_weight: 1,
52 reference_audio_path: '',
53 };
54
55 get settingsHtml() {
56 let html = `
57 <label for="sbvits_api_language">Language</label>
58 <select id="sbvits_api_language">`;
59
60 for (let language in this.languageLabels) {
61 if (this.languageLabels[language] == this.settings?.language) {
62 html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
63 continue;
64 }
65
66 html += `<option value="${this.languageLabels[language]}">${language}</option>`;
67 }
68
69 html += `
70 </select>
71 <label">SBVits2 Settings:</label><br/>
72 <label for="sbvits_tts_endpoint">Provider Endpoint:</label>
73 <input id="sbvits_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
74 <span>Use <a target="_blank" href="https://github.com/litagin02/Style-Bert-VITS2">Style-Bert-VITS2 API Server</a>.</span><br/>
75
76 <label for="sbvits_sdp_ratio">sdp_ratio: <span id="sbvits_sdp_ratio_output">${this.defaultSettings.sdp_ratio}</span></label>
77 <input id="sbvits_sdp_ratio" type="range" value="${this.defaultSettings.sdp_ratio}" min="0.0" max="1" step="0.01" />
78
79 <label for="sbvits_noise">noise: <span id="sbvits_noise_output">${this.defaultSettings.noise}</span></label>
80 <input id="sbvits_noise" type="range" value="${this.defaultSettings.noise}" min="0.1" max="2" step="0.01" />
81
82 <label for="sbvits_noisew">noisew: <span id="sbvits_noisew_output">${this.defaultSettings.noisew}</span></label>
83 <input id="sbvits_noisew" type="range" value="${this.defaultSettings.noisew}" min="0.1" max="2" step="0.01" />
84
85 <label for="sbvits_length">length: <span id="sbvits_length_output">${this.defaultSettings.length}</span></label>
86 <input id="sbvits_length" type="range" value="${this.defaultSettings.length}" min="0.0" max="5" step="0.01" />
87
88 <label for="sbvits_auto_split" class="checkbox_label">
89 <input id="sbvits_auto_split" type="checkbox" ${this.defaultSettings.auto_split ? 'checked' : ''} />
90 Enable Text Splitting
91 </label>
92
93 <label for="sbvits_split_interval">split_interval: <span id="sbvits_split_interval_output">${this.defaultSettings.split_interval}</span></label>
94 <input id="sbvits_split_interval" type="range" value="${this.defaultSettings.split_interval}" min="0.0" max="5" step="0.01" />
95
96 <label for="sbvits_assist_text">assist_text:</label>
97 <input id="sbvits_assist_text" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.assist_text}"/>
98
99 <label for="sbvits_assist_text_weight">assist_text_weight: <span id="sbvits_assist_text_weight_output">${this.defaultSettings.assist_text_weight}</span></label>
100 <input id="sbvits_assist_text_weight" type="range" value="${this.defaultSettings.assist_text_weight}" min="0.0" max="1" step="0.01" />
101
102 <label for="sbvits_style_weight">style_weight: <span id="sbvits_style_weight_output">${this.defaultSettings.style_weight}</span></label>
103 <input id="sbvits_style_weight" type="range" value="${this.defaultSettings.style_weight}" min="0.0" max="20" step="0.01" />
104
105 <label for="sbvits_reference_audio_path">reference_audio_path:</label>
106 <input id="sbvits_reference_audio_path" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.reference_audio_path}"/>
107 `;
108
109 return html;
110 }
111
112 onSettingsChange() {
113 // Used when provider settings are updated from UI
114 this.settings.provider_endpoint = $('#sbvits_tts_endpoint').val();
115 this.settings.language = $('#sbvits_api_language').val();
116 this.settings.assist_text = $('#sbvits_assist_text').val();
117 this.settings.reference_audio_path = $('#sbvits_reference_audio_path').val();
118
119 // Update the default TTS settings based on input fields
120 this.settings.sdp_ratio = $('#sbvits_sdp_ratio').val();
121 this.settings.noise = $('#sbvits_noise').val();
122 this.settings.noisew = $('#sbvits_noisew').val();
123 this.settings.length = $('#sbvits_length').val();
124 this.settings.auto_split = $('#sbvits_auto_split').is(':checked');
125 this.settings.split_interval = $('#sbvits_split_interval').val();
126 this.settings.assist_text_weight = $('#sbvits_assist_text_weight').val();
127 this.settings.style_weight = $('#sbvits_style_weight').val();
128
129 // Update the UI to reflect changes
130 $('#sbvits_sdp_ratio_output').text(this.settings.sdp_ratio);
131 $('#sbvits_noise_output').text(this.settings.noise);
132 $('#sbvits_noisew_output').text(this.settings.noisew);
133 $('#sbvits_length_output').text(this.settings.length);
134 $('#sbvits_split_interval_output').text(this.settings.split_interval);
135 $('#sbvits_assist_text_weight_output').text(this.settings.assist_text_weight);
136 $('#sbvits_style_weight_output').text(this.settings.style_weight);
137
138 saveTtsProviderSettings();
139 this.changeTTSSettings();
140 }
141
142 async loadSettings(settings) {
143 // Pupulate Provider UI given input settings
144 if (Object.keys(settings).length == 0) {
145 console.info('Using default TTS Provider settings');
146 }
147
148 // Only accept keys defined in defaultSettings
149 this.settings = this.defaultSettings;
150
151 for (const key in settings) {
152 if (key in this.settings) {
153 this.settings[key] = settings[key];
154 } else {
155 console.debug(`Ignoring non-user-configurable setting: ${key}`);
156 }
157 }
158
159 // Set initial values from the settings
160 $('#sbvits_tts_endpoint').val(this.settings.provider_endpoint);
161 $('#sbvits_api_language').val(this.settings.language);
162 $('#sbvits_assist_text').val(this.settings.assist_text);
163 $('#sbvits_reference_audio_path').val(this.settings.reference_audio_path);
164 $('#sbvits_sdp_ratio').val(this.settings.sdp_ratio);
165 $('#sbvits_noise').val(this.settings.noise);
166 $('#sbvits_noisew').val(this.settings.noisew);
167 $('#sbvits_length').val(this.settings.length);
168 $('#sbvits_auto_split').prop('checked', this.settings.auto_split);
169 $('#sbvits_split_interval').val(this.settings.split_interval);
170 $('#sbvits_assist_text_weight').val(this.settings.assist_text_weight);
171 $('#sbvits_style_weight').val(this.settings.style_weight);
172
173 // Update the UI to reflect changes
174 $('#sbvits_sdp_ratio_output').text(this.settings.sdp_ratio);
175 $('#sbvits_noise_output').text(this.settings.noise);
176 $('#sbvits_noisew_output').text(this.settings.noisew);
177 $('#sbvits_length_output').text(this.settings.length);
178 $('#sbvits_split_interval_output').text(this.settings.split_interval);
179 $('#sbvits_assist_text_weight_output').text(this.settings.assist_text_weight);
180 $('#sbvits_style_weight_output').text(this.settings.style_weight);
181
182 // Register input/change event listeners to update settings on user interaction
183 $('#sbvits_tts_endpoint').on('input', () => { this.onSettingsChange(); });
184 $('#sbvits_api_language').on('change', () => { this.onSettingsChange(); });
185 $('#sbvits_assist_text').on('input', () => { this.onSettingsChange(); });
186 $('#sbvits_reference_audio_path').on('input', () => { this.onSettingsChange(); });
187 $('#sbvits_sdp_ratio').on('change', () => { this.onSettingsChange(); });
188 $('#sbvits_noise').on('change', () => { this.onSettingsChange(); });
189 $('#sbvits_noisew').on('change', () => { this.onSettingsChange(); });
190 $('#sbvits_length').on('change', () => { this.onSettingsChange(); });
191 $('#sbvits_auto_split').on('change', () => { this.onSettingsChange(); });
192 $('#sbvits_split_interval').on('change', () => { this.onSettingsChange(); });
193 $('#sbvits_assist_text_weight').on('change', () => { this.onSettingsChange(); });
194 $('#sbvits_style_weight').on('change', () => { this.onSettingsChange(); });
195
196 await this.checkReady();
197
198 console.info('SBVits2: Settings loaded');
199 }
200
201 // Perform a simple readiness check by trying to fetch voiceIds
202 async checkReady() {
203 await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
204 }
205
206 async onRefreshClick() {
207 return;
208 }
209
210 //#################//
211 // TTS Interfaces //
212 //#################//
213
214 /**
215 * Get a voice from the TTS provider.
216 * @param {string} voiceName Voice name to get
217 * @returns {Promise<Object>} Voice object
218 */
219 async getVoice(voiceName) {
220 if (this.voices.length == 0) {
221 this.voices = await this.fetchTtsVoiceObjects();
222 }
223 const match = this.voices.filter(
224 v => v.name == voiceName,
225 )[0];
226 if (!match) {
227 throw `TTS Voice name ${voiceName} not found`;
228 }
229 return match;
230 }
231
232 async generateTts(text, voiceId) {
233 const response = await this.fetchTtsGeneration(text, voiceId);
234 return response;
235 }
236
237 //###########//
238 // API CALLS //
239 //###########//
240 async fetchTtsVoiceObjects() {
241 const response = await fetch(`${this.settings.provider_endpoint}/models/info`);
242 if (!response.ok) {
243 throw new Error(`HTTP ${response.status}: ${await response.json()}`);
244 }
245 const data = await response.json();
246 const voices = Object.keys(data).flatMap(key => {
247 const config = data[key];
248 const spk2id = config.spk2id;
249 const style2id = config.style2id;
250
251 return Object.entries(spk2id).flatMap(([speaker, speaker_id]) => {
252 return Object.entries(style2id).map(([style, styleId]) => {
253 return {
254 name: `${speaker} (${style})`,
255 voice_id: `${key}-${speaker_id}-${style}`,
256 preview_url: false,
257 };
258 });
259 });
260 });
261
262 this.voices = voices; // Assign to the class property
263 return voices; // Also return this list
264 }
265
266 // Each time a parameter is changed, we change the configuration
267 async changeTTSSettings() {
268 }
269
270 /**
271 * Fetch TTS generation from the API.
272 * @param {string} inputText Text to generate TTS for
273 * @param {string} voiceId Voice ID to use (model_id-speaker_id-style)
274 * @returns {Promise<Response>} Fetch response
275 */
276 async fetchTtsGeneration(inputText, voiceId) {
277 console.info(`Generating new TTS for voice_id ${voiceId}`);
278
279 const [model_id, speaker_id, ...rest] = voiceId.split('-');
280 const style = rest.join('-');
281 const params = new URLSearchParams();
282 // restore for auto_split
283 inputText = inputText.replaceAll('<br>', '\n');
284 params.append('text', inputText);
285 params.append('model_id', model_id);
286 params.append('speaker_id', speaker_id);
287 params.append('sdp_ratio', this.settings.sdp_ratio);
288 params.append('noise', this.settings.noise);
289 params.append('noisew', this.settings.noisew);
290 params.append('length', this.settings.length);
291 params.append('language', this.settings.language);
292 params.append('auto_split', this.settings.auto_split);
293 params.append('split_interval', this.settings.split_interval);
294 if (this.settings.assist_text) {
295 params.append('assist_text', this.settings.assist_text);
296 params.append('assist_text_weight', this.settings.assist_text_weight);
297 }
298 params.append('style', style);
299 params.append('style_weight', this.settings.style_weight);
300 if (this.settings.reference_audio_path) {
301 params.append('reference_audio_path', this.settings.reference_audio_path);
302 }
303 const url = `${this.settings.provider_endpoint}/voice?${params.toString()}`;
304
305 const response = await fetch(
306 url,
307 {
308 method: 'POST',
309 headers: {
310 },
311 },
312 );
313 if (!response.ok) {
314 toastr.error(response.statusText, 'TTS Generation Failed');
315 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
316 }
317 return response;
318 }
319
320 /**
321 * Preview TTS for a given voice ID.
322 * @param {string} id Voice ID
323 */
324 async previewTtsVoice(id) {
325 this.audioElement.pause();
326 this.audioElement.currentTime = 0;
327 const lang_code = this.langKey2LangCode[this.settings.lang] ?? 'ja-JP';
328 const text = getPreviewString(lang_code);
329 const response = await this.fetchTtsGeneration(text, id);
330 if (!response.ok) {
331 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
332 }
333
334 const audio = await response.blob();
335 const url = URL.createObjectURL(audio);
336 this.audioElement.src = url;
337 this.audioElement.play();
338 }
339
340 // Interface not used
341 async fetchTtsFromHistory(history_item_id) {
342 return Promise.resolve(history_item_id);
343 }
344}