Add files via upload

e2b1e14ee11b2a2eadc9de3e058867b2dd0d80b1

Boof2015 <75185879+Boof2015@users.noreply.github.com>

Signed
1 files changed, +646 -0Ignore whitespace
public/scripts/extensions/tts/chatterbox.js+646 -0
@@ -0,0 +1,646 @@
1import { saveTtsProviderSettings } from './index.js';
2
3export { ChatterboxTtsProvider };
4
5class ChatterboxTtsProvider {
6 //########//
7 // Config //
8 //########//
9
10 settings = {};
11 constructor() {
12 // Initialize with default settings
13 this.settings = {
14 provider_endpoint: this.settings.provider_endpoint || 'http://localhost:8004',
15 voice_mode: this.settings.voice_mode || 'predefined',
16 predefined_voice: this.settings.predefined_voice || 'S1',
17 reference_voice: this.settings.reference_voice || '',
18 temperature: this.settings.temperature || 0.8,
19 exaggeration: this.settings.exaggeration || 0.5,
20 cfg_weight: this.settings.cfg_weight || 0.5,
21 seed: this.settings.seed || -1,
22 speed_factor: this.settings.speed_factor || 1.0,
23 language: this.settings.language || 'en',
24 split_text: this.settings.split_text || true,
25 chunk_size: this.settings.chunk_size || 120,
26 output_format: this.settings.output_format || 'wav',
27 voiceMap: this.settings.voiceMap || {},
28 };
29 }
30
31 ready = false;
32 voices = [];
33 separator = '. ';
34 audioElement = document.createElement('audio');
35
36 languageLabels = {
37 'English': 'en',
38 'Spanish': 'es',
39 'French': 'fr',
40 'German': 'de',
41 'Italian': 'it',
42 'Portuguese': 'pt',
43 'Polish': 'pl',
44 'Turkish': 'tr',
45 'Russian': 'ru',
46 'Dutch': 'nl',
47 'Czech': 'cs',
48 'Arabic': 'ar',
49 'Chinese': 'zh-cn',
50 'Japanese': 'ja',
51 'Korean': 'ko',
52 'Hindi': 'hi',
53 };
54
55 get settingsHtml() {
56 let html = `<div class="chatterbox-settings-container">
57 <div class="chatterbox-settings-header">
58 <h3>Chatterbox TTS Settings</h3>
59 <div class="status-indicator">
60 Status: <span id="chatterbox-status" class="offline">Offline</span>
61 </div>
62 </div>`;
63
64 // Server endpoint
65 html += `<div class="chatterbox-setting-row">
66 <label for="chatterbox-endpoint">Server Endpoint:</label>
67 <input id="chatterbox-endpoint" type="text" class="text_pole" value="${this.settings.provider_endpoint}" />
68 </div>`;
69
70 // Language selection
71 html += `<div class="chatterbox-setting-row">
72 <label for="chatterbox-language">Language:</label>
73 <select id="chatterbox-language">`;
74 for (let language in this.languageLabels) {
75 html += `<option value="${this.languageLabels[language]}" ${this.languageLabels[language] === this.settings.language ? 'selected' : ''}>${language}</option>`;
76 }
77 html += `</select>
78 </div>`;
79
80 // Generation parameters
81 html += `<div class="chatterbox-params-section">
82 <h4>Generation Parameters</h4>`;
83
84 // Temperature
85 html += `<div class="chatterbox-setting-row">
86 <label for="chatterbox-temperature">Temperature: <span id="chatterbox-temperature-value">${this.settings.temperature}</span></label>
87 <input id="chatterbox-temperature" type="range" min="0" max="1" step="0.1" value="${this.settings.temperature}" />
88 </div>`;
89
90 // Exaggeration
91 html += `<div class="chatterbox-setting-row">
92 <label for="chatterbox-exaggeration">Exaggeration: <span id="chatterbox-exaggeration-value">${this.settings.exaggeration}</span></label>
93 <input id="chatterbox-exaggeration" type="range" min="0" max="2" step="0.1" value="${this.settings.exaggeration}" />
94 </div>`;
95
96 // CFG Weight
97 html += `<div class="chatterbox-setting-row">
98 <label for="chatterbox-cfg-weight">CFG Weight: <span id="chatterbox-cfg-weight-value">${this.settings.cfg_weight}</span></label>
99 <input id="chatterbox-cfg-weight" type="range" min="0" max="1" step="0.1" value="${this.settings.cfg_weight}" />
100 </div>`;
101
102 // Speed Factor
103 html += `<div class="chatterbox-setting-row">
104 <label for="chatterbox-speed">Speed Factor: <span id="chatterbox-speed-value">${this.settings.speed_factor}</span></label>
105 <input id="chatterbox-speed" type="range" min="0.5" max="2" step="0.1" value="${this.settings.speed_factor}" />
106 </div>`;
107
108 // Seed
109 html += `<div class="chatterbox-setting-row">
110 <label for="chatterbox-seed">Seed (-1 for random):</label>
111 <input id="chatterbox-seed" type="number" min="-1" value="${this.settings.seed}" />
112 </div>`;
113
114 // Text chunking
115 html += `<div class="chatterbox-setting-row">
116 <label>
117 <input type="checkbox" id="chatterbox-split-text" ${this.settings.split_text ? 'checked' : ''} />
118 Split long texts into chunks
119 </label>
120 </div>`;
121
122 // Chunk size
123 html += `<div class="chatterbox-setting-row" id="chunk-size-row" ${!this.settings.split_text ? 'style="display: none;"' : ''}>
124 <label for="chatterbox-chunk-size">Chunk Size:</label>
125 <input id="chatterbox-chunk-size" type="number" min="50" max="500" value="${this.settings.chunk_size}" />
126 </div>`;
127
128 // Output format
129 html += `<div class="chatterbox-setting-row">
130 <label for="chatterbox-format">Output Format:</label>
131 <select id="chatterbox-format">
132 <option value="wav" ${this.settings.output_format === 'wav' ? 'selected' : ''}>WAV</option>
133 <option value="opus" ${this.settings.output_format === 'opus' ? 'selected' : ''}>Opus</option>
134 </select>
135 </div>`;
136
137 html += `</div>`; // End params section
138
139 // Footer with links
140 html += `<div class="chatterbox-footer">
141 <a href="${this.settings.provider_endpoint}" target="_blank">Chatterbox Web UI</a> |
142 <a href="https://github.com/devnen/Chatterbox-TTS-Server" target="_blank">Documentation</a>
143 </div>`;
144
145 html += `</div>`; // End container
146
147 // Add CSS styles
148 html += `<style>
149 .chatterbox-settings-container {
150 padding: 10px;
151 }
152 .chatterbox-settings-header {
153 display: flex;
154 justify-content: space-between;
155 align-items: center;
156 margin-bottom: 15px;
157 }
158 .chatterbox-settings-header h3 {
159 margin: 0;
160 }
161 .status-indicator {
162 font-weight: bold;
163 }
164 #chatterbox-status.ready { color: #4CAF50; }
165 #chatterbox-status.offline { color: #f44336; }
166 #chatterbox-status.processing { color: #2196F3; }
167 .chatterbox-setting-row {
168 margin-bottom: 10px;
169 display: flex;
170 align-items: center;
171 gap: 10px;
172 }
173 .chatterbox-setting-row label {
174 flex: 0 0 150px;
175 }
176 .chatterbox-setting-row input[type="text"],
177 .chatterbox-setting-row input[type="number"],
178 .chatterbox-setting-row select {
179 flex: 1;
180 }
181 .chatterbox-setting-row input[type="range"] {
182 flex: 1;
183 }
184 .chatterbox-params-section {
185 margin-top: 15px;
186 padding-top: 15px;
187 border-top: 1px solid #ccc;
188 }
189 .chatterbox-params-section h4 {
190 margin-top: 0;
191 margin-bottom: 10px;
192 }
193 .chatterbox-footer {
194 margin-top: 15px;
195 padding-top: 15px;
196 border-top: 1px solid #ccc;
197 text-align: center;
198 font-size: 0.9em;
199 }
200 </style>`;
201
202 return html;
203 }
204
205 //######################//
206 // Startup & Initialize //
207 //######################//
208
209 async loadSettings(settings) {
210 this.updateStatus('Offline');
211
212 if (Object.keys(settings).length === 0) {
213 console.info('Using default Chatterbox TTS Provider settings');
214 } else {
215 // Populate settings with provided values
216 for (const key in settings) {
217 if (key in this.settings) {
218 this.settings[key] = settings[key];
219 }
220 }
221 }
222
223 // Update UI elements
224 this.updateUIFromSettings();
225
226 console.debug('ChatterboxTTS: Settings loaded');
227
228 try {
229 // Check if TTS provider is ready
230 await this.checkReady();
231
232 if (this.ready) {
233 // Fetch all voice types for the voice map
234 await this.fetchTtsVoiceObjects();
235 this.updateStatus('Ready');
236 }
237
238 this.setupEventListeners();
239
240 } catch (error) {
241 console.error('Error loading Chatterbox settings:', error);
242 this.updateStatus('Offline');
243 }
244 }
245
246 updateUIFromSettings() {
247 $('#chatterbox-endpoint').val(this.settings.provider_endpoint);
248 $('#chatterbox-language').val(this.settings.language);
249 $('#chatterbox-temperature').val(this.settings.temperature);
250 $('#chatterbox-temperature-value').text(this.settings.temperature);
251 $('#chatterbox-exaggeration').val(this.settings.exaggeration);
252 $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
253 $('#chatterbox-cfg-weight').val(this.settings.cfg_weight);
254 $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
255 $('#chatterbox-speed').val(this.settings.speed_factor);
256 $('#chatterbox-speed-value').text(this.settings.speed_factor);
257 $('#chatterbox-seed').val(this.settings.seed);
258 $('#chatterbox-split-text').prop('checked', this.settings.split_text);
259 $('#chatterbox-chunk-size').val(this.settings.chunk_size);
260 $('#chatterbox-format').val(this.settings.output_format);
261
262 // Show/hide chunk size based on split text
263 if (this.settings.split_text) {
264 $('#chunk-size-row').show();
265 } else {
266 $('#chunk-size-row').hide();
267 }
268 }
269
270 //##############################//
271 // Check Server is Available //
272 //##############################//
273
274 async checkReady() {
275 try {
276 const response = await fetch(`${this.settings.provider_endpoint}/api/ui/initial-data`);
277
278 if (!response.ok) {
279 throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
280 }
281
282 const data = await response.json();
283
284 // Check if we got valid data
285 if (data) {
286 this.ready = true;
287 console.log('Chatterbox TTS service is ready.');
288 } else {
289 this.ready = false;
290 console.log('Chatterbox TTS service returned invalid data.');
291 }
292 } catch (error) {
293 console.error('Error checking Chatterbox TTS service readiness:', error);
294 this.ready = false;
295 }
296 }
297
298 //######################//
299 // Get Available Voices //
300 //######################//
301
302 async fetchTtsVoiceObjects() {
303 try {
304 // Always fetch predefined voices
305 const predefinedResponse = await fetch(`${this.settings.provider_endpoint}/get_predefined_voices`);
306 if (!predefinedResponse.ok) {
307 throw new Error(`HTTP ${predefinedResponse.status}: ${predefinedResponse.statusText}`);
308 }
309
310 const predefinedData = await predefinedResponse.json();
311
312 // Transform predefined voices
313 const predefinedVoices = predefinedData.map(voice => ({
314 name: voice.display_name,
315 voice_id: voice.voice_id,
316 preview_url: null,
317 lang: voice.language || 'en'
318 }));
319
320 // Always try to fetch reference voices
321 let referenceVoices = [];
322 try {
323 const refResponse = await fetch(`${this.settings.provider_endpoint}/get_reference_files`);
324 if (refResponse.ok) {
325 const refData = await refResponse.json();
326 referenceVoices = refData.map(filename => ({
327 name: `[Clone] ${filename}`,
328 voice_id: `ref_${filename}`,
329 preview_url: null,
330 lang: 'en'
331 }));
332 }
333 } catch (error) {
334 console.warn('Failed to fetch reference voices:', error);
335 }
336
337 // Combine all voices
338 this.voices = [...predefinedVoices, ...referenceVoices];
339
340 console.log(`Loaded ${this.voices.length} voices (${predefinedVoices.length} predefined, ${referenceVoices.length} reference)`);
341 return this.voices;
342 } catch (error) {
343 console.error('Error fetching Chatterbox voices:', error);
344 this.voices = [];
345 return [];
346 }
347 }
348
349 // Alias for internal use
350 async fetchVoices() {
351 return this.fetchTtsVoiceObjects();
352 }
353
354 //###########################//
355 // Setup Event Listeners //
356 //###########################//
357
358 setupEventListeners() {
359 // Server endpoint change
360 $('#chatterbox-endpoint').on('input', () => {
361 this.settings.provider_endpoint = $('#chatterbox-endpoint').val();
362 this.onSettingsChange();
363 });
364
365 // Language
366 $('#chatterbox-language').on('change', (e) => {
367 this.settings.language = e.target.value;
368 this.onSettingsChange();
369 });
370
371 // Parameter sliders
372 $('#chatterbox-temperature').on('input', (e) => {
373 this.settings.temperature = parseFloat(e.target.value);
374 $('#chatterbox-temperature-value').text(this.settings.temperature);
375 this.onSettingsChange();
376 });
377
378 $('#chatterbox-exaggeration').on('input', (e) => {
379 this.settings.exaggeration = parseFloat(e.target.value);
380 $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
381 this.onSettingsChange();
382 });
383
384 $('#chatterbox-cfg-weight').on('input', (e) => {
385 this.settings.cfg_weight = parseFloat(e.target.value);
386 $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
387 this.onSettingsChange();
388 });
389
390 $('#chatterbox-speed').on('input', (e) => {
391 this.settings.speed_factor = parseFloat(e.target.value);
392 $('#chatterbox-speed-value').text(this.settings.speed_factor);
393 this.onSettingsChange();
394 });
395
396 // Seed
397 $('#chatterbox-seed').on('change', (e) => {
398 this.settings.seed = parseInt(e.target.value);
399 this.onSettingsChange();
400 });
401
402 // Text splitting
403 $('#chatterbox-split-text').on('change', (e) => {
404 this.settings.split_text = e.target.checked;
405 if (e.target.checked) {
406 $('#chunk-size-row').show();
407 } else {
408 $('#chunk-size-row').hide();
409 }
410 this.onSettingsChange();
411 });
412
413 $('#chatterbox-chunk-size').on('change', (e) => {
414 this.settings.chunk_size = parseInt(e.target.value);
415 this.onSettingsChange();
416 });
417
418 // Output format
419 $('#chatterbox-format').on('change', (e) => {
420 this.settings.output_format = e.target.value;
421 this.onSettingsChange();
422 });
423 }
424
425 //#############################//
426 // Store ST interface settings //
427 //#############################//
428
429 onSettingsChange() {
430 // Save the updated settings
431 saveTtsProviderSettings();
432 }
433
434 //#########################//
435 // Handle Reload button //
436 //#########################//
437
438 async onRefreshClick() {
439 try {
440 this.updateStatus('Processing');
441 await this.checkReady();
442
443 if (this.ready) {
444 await this.fetchTtsVoiceObjects();
445 this.updateStatus('Ready');
446 } else {
447 this.updateStatus('Offline');
448 }
449 } catch (error) {
450 console.error('Error during refresh:', error);
451 this.updateStatus('Offline');
452 }
453 }
454
455 //##################//
456 // Preview Voice //
457 //##################//
458
459 async previewTtsVoice(voiceId) {
460 try {
461 this.updateStatus('Processing');
462
463 const previewText = "Hello! This is a preview of the selected voice.";
464
465 // Determine if this is a reference voice
466 let isReferenceVoice = false;
467 let actualVoiceId = voiceId;
468
469 if (voiceId && voiceId.startsWith('ref_')) {
470 isReferenceVoice = true;
471 actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
472 }
473
474 // Generate preview using the main TTS endpoint
475 const requestBody = {
476 text: previewText,
477 voice_mode: isReferenceVoice ? 'clone' : 'predefined',
478 temperature: this.settings.temperature,
479 exaggeration: this.settings.exaggeration,
480 cfg_weight: this.settings.cfg_weight,
481 seed: this.settings.seed,
482 speed_factor: this.settings.speed_factor,
483 language: this.settings.language,
484 split_text: false, // Don't split for preview
485 output_format: this.settings.output_format
486 };
487
488 // Add voice-specific parameters
489 if (isReferenceVoice) {
490 requestBody.reference_audio_filename = actualVoiceId;
491 } else {
492 requestBody.predefined_voice_id = actualVoiceId;
493 }
494
495 const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
496 method: 'POST',
497 headers: {
498 'Content-Type': 'application/json'
499 },
500 body: JSON.stringify(requestBody)
501 });
502
503 if (!response.ok) {
504 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
505 }
506
507 // Get the audio blob and play it
508 const audioBlob = await response.blob();
509 const audioUrl = URL.createObjectURL(audioBlob);
510
511 const audio = new Audio(audioUrl);
512 audio.addEventListener('ended', () => {
513 URL.revokeObjectURL(audioUrl);
514 this.updateStatus('Ready');
515 });
516
517 await audio.play();
518
519 } catch (error) {
520 console.error('Error previewing voice:', error);
521 this.updateStatus('Ready');
522 throw error;
523 }
524 }
525
526 //#####################//
527 // Get Voice Object //
528 //#####################//
529
530 async getVoice(voiceName) {
531 // Ensure voices are loaded
532 if (this.voices.length === 0) {
533 await this.fetchTtsVoiceObjects();
534 }
535
536 // Find the voice object by name or voice_id
537 let match = this.voices.find(voice =>
538 voice.name === voiceName ||
539 voice.voice_id === voiceName ||
540 voice.display_name === voiceName
541 );
542
543 if (!match) {
544 console.warn(`Voice not found: ${voiceName}`);
545 // Check if it's a reference voice that wasn't in the list
546 if (voiceName && voiceName.startsWith('ref_')) {
547 const filename = voiceName.substring(4);
548 return {
549 name: `[Clone] ${filename}`,
550 voice_id: voiceName,
551 preview_url: null,
552 lang: 'en'
553 };
554 }
555 // Return a default voice object
556 return {
557 name: voiceName || 'Default',
558 voice_id: voiceName || this.settings.predefined_voice || 'S1',
559 preview_url: null,
560 lang: 'en'
561 };
562 }
563
564 return match;
565 }
566
567 //##################//
568 // Generate TTS //
569 //##################//
570
571 async generateTts(inputText, voiceId) {
572 try {
573 this.updateStatus('Processing');
574
575 // Determine if this is a reference voice
576 let isReferenceVoice = false;
577 let actualVoiceId = voiceId;
578
579 if (voiceId && voiceId.startsWith('ref_')) {
580 isReferenceVoice = true;
581 actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
582 }
583
584 // Prepare the request body
585 const requestBody = {
586 text: inputText,
587 voice_mode: isReferenceVoice ? 'clone' : 'predefined',
588 temperature: this.settings.temperature,
589 exaggeration: this.settings.exaggeration,
590 cfg_weight: this.settings.cfg_weight,
591 seed: this.settings.seed,
592 speed_factor: this.settings.speed_factor,
593 language: this.settings.language,
594 split_text: this.settings.split_text,
595 chunk_size: this.settings.chunk_size,
596 output_format: this.settings.output_format
597 };
598
599 // Add voice-specific parameters
600 if (isReferenceVoice) {
601 requestBody.reference_audio_filename = actualVoiceId;
602 } else {
603 requestBody.predefined_voice_id = actualVoiceId || this.settings.predefined_voice;
604 }
605
606 console.log('Generating TTS with params:', requestBody);
607
608 const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
609 method: 'POST',
610 headers: {
611 'Content-Type': 'application/json',
612 'Cache-Control': 'no-cache'
613 },
614 body: JSON.stringify(requestBody)
615 });
616
617 if (!response.ok) {
618 const errorText = await response.text();
619 console.error('TTS generation error:', errorText);
620 throw new Error(`HTTP ${response.status}: ${errorText}`);
621 }
622
623 this.updateStatus('Ready');
624
625 // Return the response directly - SillyTavern expects a Response object
626 return response;
627
628 } catch (error) {
629 console.error('Error in generateTts:', error);
630 this.updateStatus('Ready');
631 throw error;
632 }
633 }
634
635 //######################//
636 // Update Status //
637 //######################//
638
639 updateStatus(status) {
640 const statusElement = document.getElementById('chatterbox-status');
641 if (statusElement) {
642 statusElement.textContent = status;
643 statusElement.className = status.toLowerCase();
644 }
645 }
646}
\ No newline at end of file646 \ No newline at end of file