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 @@
1+import { saveTtsProviderSettings } from './index.js';
2+
3+export { ChatterboxTtsProvider };
4+
5+class 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+}
646 \ No newline at end of file