| 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" class="text_pole" type="number" min="-1" value="${this.settings.seed}" /> |
| 112 | + </div>`; |
| 113 | + |
| 114 | + // Text chunking |
| 115 | + html += `<div class="chatterbox-setting-row"> |
| 116 | + <label class="checkbox_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" class="text_pole" 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 | + .chatterbox-settings-container .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 label.checkbox_label { |
| 177 | + flex-basis: auto; |
| 178 | + } |
| 179 | + .chatterbox-setting-row input[type="text"], |
| 180 | + .chatterbox-setting-row input[type="number"], |
| 181 | + .chatterbox-setting-row select { |
| 182 | + flex: 1; |
| 183 | + } |
| 184 | + .chatterbox-setting-row input[type="range"] { |
| 185 | + flex: 1; |
| 186 | + } |
| 187 | + .chatterbox-params-section { |
| 188 | + margin-top: 15px; |
| 189 | + padding-top: 15px; |
| 190 | + border-top: 1px solid #ccc; |
| 191 | + } |
| 192 | + .chatterbox-params-section h4 { |
| 193 | + margin-top: 0; |
| 194 | + margin-bottom: 10px; |
| 195 | + } |
| 196 | + .chatterbox-footer { |
| 197 | + margin-top: 15px; |
| 198 | + padding-top: 15px; |
| 199 | + border-top: 1px solid #ccc; |
| 200 | + text-align: center; |
| 201 | + font-size: 0.9em; |
| 202 | + } |
| 203 | + </style>`; |
| 204 | + |
| 205 | + return html; |
| 206 | + } |
| 207 | + |
| 208 | + //######################// |
| 209 | + // Startup & Initialize // |
| 210 | + //######################// |
| 211 | + |
| 212 | + async loadSettings(settings) { |
| 213 | + this.updateStatus('Offline'); |
| 214 | + |
| 215 | + if (Object.keys(settings).length === 0) { |
| 216 | + console.info('Using default Chatterbox TTS Provider settings'); |
| 217 | + } else { |
| 218 | + // Populate settings with provided values |
| 219 | + for (const key in settings) { |
| 220 | + if (key in this.settings) { |
| 221 | + this.settings[key] = settings[key]; |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + // Update UI elements |
| 227 | + this.updateUIFromSettings(); |
| 228 | + |
| 229 | + console.debug('ChatterboxTTS: Settings loaded'); |
| 230 | + |
| 231 | + try { |
| 232 | + // Check if TTS provider is ready |
| 233 | + await this.checkReady(); |
| 234 | + |
| 235 | + if (this.ready) { |
| 236 | + // Fetch all voice types for the voice map |
| 237 | + await this.fetchTtsVoiceObjects(); |
| 238 | + this.updateStatus('Ready'); |
| 239 | + } |
| 240 | + |
| 241 | + this.setupEventListeners(); |
| 242 | + |
| 243 | + } catch (error) { |
| 244 | + console.error('Error loading Chatterbox settings:', error); |
| 245 | + this.updateStatus('Offline'); |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + updateUIFromSettings() { |
| 250 | + $('#chatterbox-endpoint').val(this.settings.provider_endpoint); |
| 251 | + $('#chatterbox-language').val(this.settings.language); |
| 252 | + $('#chatterbox-temperature').val(this.settings.temperature); |
| 253 | + $('#chatterbox-temperature-value').text(this.settings.temperature); |
| 254 | + $('#chatterbox-exaggeration').val(this.settings.exaggeration); |
| 255 | + $('#chatterbox-exaggeration-value').text(this.settings.exaggeration); |
| 256 | + $('#chatterbox-cfg-weight').val(this.settings.cfg_weight); |
| 257 | + $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight); |
| 258 | + $('#chatterbox-speed').val(this.settings.speed_factor); |
| 259 | + $('#chatterbox-speed-value').text(this.settings.speed_factor); |
| 260 | + $('#chatterbox-seed').val(this.settings.seed); |
| 261 | + $('#chatterbox-split-text').prop('checked', this.settings.split_text); |
| 262 | + $('#chatterbox-chunk-size').val(this.settings.chunk_size); |
| 263 | + $('#chatterbox-format').val(this.settings.output_format); |
| 264 | + |
| 265 | + // Show/hide chunk size based on split text |
| 266 | + if (this.settings.split_text) { |
| 267 | + $('#chunk-size-row').show(); |
| 268 | + } else { |
| 269 | + $('#chunk-size-row').hide(); |
| 270 | + } |
| 271 | + } |
| 272 | + |
| 273 | + //##############################// |
| 274 | + // Check Server is Available // |
| 275 | + //##############################// |
| 276 | + |
| 277 | + async checkReady() { |
| 278 | + try { |
| 279 | + const response = await fetch(`${this.settings.provider_endpoint}/api/ui/initial-data`); |
| 280 | + |
| 281 | + if (!response.ok) { |
| 282 | + throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`); |
| 283 | + } |
| 284 | + |
| 285 | + const data = await response.json(); |
| 286 | + |
| 287 | + // Check if we got valid data |
| 288 | + if (data) { |
| 289 | + this.ready = true; |
| 290 | + console.log('Chatterbox TTS service is ready.'); |
| 291 | + } else { |
| 292 | + this.ready = false; |
| 293 | + console.log('Chatterbox TTS service returned invalid data.'); |
| 294 | + } |
| 295 | + } catch (error) { |
| 296 | + console.error('Error checking Chatterbox TTS service readiness:', error); |
| 297 | + this.ready = false; |
| 298 | + } |
| 299 | + } |
| 300 | + |
| 301 | + //######################// |
| 302 | + // Get Available Voices // |
| 303 | + //######################// |
| 304 | + |
| 305 | + async fetchTtsVoiceObjects() { |
| 306 | + try { |
| 307 | + // Always fetch predefined voices |
| 308 | + const predefinedResponse = await fetch(`${this.settings.provider_endpoint}/get_predefined_voices`); |
| 309 | + if (!predefinedResponse.ok) { |
| 310 | + throw new Error(`HTTP ${predefinedResponse.status}: ${predefinedResponse.statusText}`); |
| 311 | + } |
| 312 | + |
| 313 | + const predefinedData = await predefinedResponse.json(); |
| 314 | + |
| 315 | + // Transform predefined voices |
| 316 | + const predefinedVoices = predefinedData.map(voice => ({ |
| 317 | + name: voice.display_name, |
| 318 | + voice_id: voice.voice_id || voice.filename, |
| 319 | + preview_url: null, |
| 320 | + lang: voice.language || 'en', |
| 321 | + })); |
| 322 | + |
| 323 | + // Always try to fetch reference voices |
| 324 | + let referenceVoices = []; |
| 325 | + try { |
| 326 | + const refResponse = await fetch(`${this.settings.provider_endpoint}/get_reference_files`); |
| 327 | + if (refResponse.ok) { |
| 328 | + const refData = await refResponse.json(); |
| 329 | + referenceVoices = refData.map(filename => ({ |
| 330 | + name: `[Clone] ${filename}`, |
| 331 | + voice_id: `ref_${filename}`, |
| 332 | + preview_url: null, |
| 333 | + lang: 'en', |
| 334 | + })); |
| 335 | + } |
| 336 | + } catch (error) { |
| 337 | + console.warn('Failed to fetch reference voices:', error); |
| 338 | + } |
| 339 | + |
| 340 | + // Combine all voices |
| 341 | + this.voices = [...predefinedVoices, ...referenceVoices]; |
| 342 | + |
| 343 | + console.log(`Loaded ${this.voices.length} voices (${predefinedVoices.length} predefined, ${referenceVoices.length} reference)`); |
| 344 | + return this.voices; |
| 345 | + } catch (error) { |
| 346 | + console.error('Error fetching Chatterbox voices:', error); |
| 347 | + this.voices = []; |
| 348 | + return []; |
| 349 | + } |
| 350 | + } |
| 351 | + |
| 352 | + // Alias for internal use |
| 353 | + async fetchVoices() { |
| 354 | + return this.fetchTtsVoiceObjects(); |
| 355 | + } |
| 356 | + |
| 357 | + //###########################// |
| 358 | + // Setup Event Listeners // |
| 359 | + //###########################// |
| 360 | + |
| 361 | + setupEventListeners() { |
| 362 | + // Server endpoint change |
| 363 | + $('#chatterbox-endpoint').on('input', () => { |
| 364 | + this.settings.provider_endpoint = $('#chatterbox-endpoint').val(); |
| 365 | + this.onSettingsChange(); |
| 366 | + }); |
| 367 | + |
| 368 | + // Language |
| 369 | + $('#chatterbox-language').on('change', (e) => { |
| 370 | + this.settings.language = e.target.value; |
| 371 | + this.onSettingsChange(); |
| 372 | + }); |
| 373 | + |
| 374 | + // Parameter sliders |
| 375 | + $('#chatterbox-temperature').on('input', (e) => { |
| 376 | + this.settings.temperature = parseFloat(e.target.value); |
| 377 | + $('#chatterbox-temperature-value').text(this.settings.temperature); |
| 378 | + this.onSettingsChange(); |
| 379 | + }); |
| 380 | + |
| 381 | + $('#chatterbox-exaggeration').on('input', (e) => { |
| 382 | + this.settings.exaggeration = parseFloat(e.target.value); |
| 383 | + $('#chatterbox-exaggeration-value').text(this.settings.exaggeration); |
| 384 | + this.onSettingsChange(); |
| 385 | + }); |
| 386 | + |
| 387 | + $('#chatterbox-cfg-weight').on('input', (e) => { |
| 388 | + this.settings.cfg_weight = parseFloat(e.target.value); |
| 389 | + $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight); |
| 390 | + this.onSettingsChange(); |
| 391 | + }); |
| 392 | + |
| 393 | + $('#chatterbox-speed').on('input', (e) => { |
| 394 | + this.settings.speed_factor = parseFloat(e.target.value); |
| 395 | + $('#chatterbox-speed-value').text(this.settings.speed_factor); |
| 396 | + this.onSettingsChange(); |
| 397 | + }); |
| 398 | + |
| 399 | + // Seed |
| 400 | + $('#chatterbox-seed').on('change', (e) => { |
| 401 | + this.settings.seed = parseInt(e.target.value); |
| 402 | + this.onSettingsChange(); |
| 403 | + }); |
| 404 | + |
| 405 | + // Text splitting |
| 406 | + $('#chatterbox-split-text').on('change', (e) => { |
| 407 | + this.settings.split_text = e.target.checked; |
| 408 | + if (e.target.checked) { |
| 409 | + $('#chunk-size-row').show(); |
| 410 | + } else { |
| 411 | + $('#chunk-size-row').hide(); |
| 412 | + } |
| 413 | + this.onSettingsChange(); |
| 414 | + }); |
| 415 | + |
| 416 | + $('#chatterbox-chunk-size').on('change', (e) => { |
| 417 | + this.settings.chunk_size = parseInt(e.target.value); |
| 418 | + this.onSettingsChange(); |
| 419 | + }); |
| 420 | + |
| 421 | + // Output format |
| 422 | + $('#chatterbox-format').on('change', (e) => { |
| 423 | + this.settings.output_format = e.target.value; |
| 424 | + this.onSettingsChange(); |
| 425 | + }); |
| 426 | + } |
| 427 | + |
| 428 | + //#############################// |
| 429 | + // Store ST interface settings // |
| 430 | + //#############################// |
| 431 | + |
| 432 | + onSettingsChange() { |
| 433 | + // Save the updated settings |
| 434 | + saveTtsProviderSettings(); |
| 435 | + } |
| 436 | + |
| 437 | + //#########################// |
| 438 | + // Handle Reload button // |
| 439 | + //#########################// |
| 440 | + |
| 441 | + async onRefreshClick() { |
| 442 | + try { |
| 443 | + this.updateStatus('Processing'); |
| 444 | + await this.checkReady(); |
| 445 | + |
| 446 | + if (this.ready) { |
| 447 | + await this.fetchTtsVoiceObjects(); |
| 448 | + this.updateStatus('Ready'); |
| 449 | + } else { |
| 450 | + this.updateStatus('Offline'); |
| 451 | + } |
| 452 | + } catch (error) { |
| 453 | + console.error('Error during refresh:', error); |
| 454 | + this.updateStatus('Offline'); |
| 455 | + } |
| 456 | + } |
| 457 | + |
| 458 | + //##################// |
| 459 | + // Preview Voice // |
| 460 | + //##################// |
| 461 | + |
| 462 | + async previewTtsVoice(voiceId) { |
| 463 | + try { |
| 464 | + this.updateStatus('Processing'); |
| 465 | + |
| 466 | + const previewText = 'Hello! This is a preview of the selected voice.'; |
| 467 | + |
| 468 | + // Determine if this is a reference voice |
| 469 | + let isReferenceVoice = false; |
| 470 | + let actualVoiceId = voiceId; |
| 471 | + |
| 472 | + if (voiceId && voiceId.startsWith('ref_')) { |
| 473 | + isReferenceVoice = true; |
| 474 | + actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix |
| 475 | + } |
| 476 | + |
| 477 | + // Generate preview using the main TTS endpoint |
| 478 | + const requestBody = { |
| 479 | + text: previewText, |
| 480 | + voice_mode: isReferenceVoice ? 'clone' : 'predefined', |
| 481 | + temperature: this.settings.temperature, |
| 482 | + exaggeration: this.settings.exaggeration, |
| 483 | + cfg_weight: this.settings.cfg_weight, |
| 484 | + seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1 |
| 485 | + speed_factor: this.settings.speed_factor, |
| 486 | + language: this.settings.language, |
| 487 | + split_text: false, // Don't split for preview |
| 488 | + output_format: this.settings.output_format, |
| 489 | + }; |
| 490 | + |
| 491 | + // Add voice-specific parameters |
| 492 | + if (isReferenceVoice) { |
| 493 | + requestBody.reference_audio_filename = actualVoiceId; |
| 494 | + } else { |
| 495 | + requestBody.predefined_voice_id = actualVoiceId; |
| 496 | + } |
| 497 | + |
| 498 | + const response = await fetch(`${this.settings.provider_endpoint}/tts`, { |
| 499 | + method: 'POST', |
| 500 | + headers: { |
| 501 | + 'Content-Type': 'application/json', |
| 502 | + }, |
| 503 | + body: JSON.stringify(requestBody), |
| 504 | + }); |
| 505 | + |
| 506 | + if (!response.ok) { |
| 507 | + throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| 508 | + } |
| 509 | + |
| 510 | + // Get the audio blob and play it |
| 511 | + const audioBlob = await response.blob(); |
| 512 | + const audioUrl = URL.createObjectURL(audioBlob); |
| 513 | + |
| 514 | + const audio = new Audio(audioUrl); |
| 515 | + audio.addEventListener('ended', () => { |
| 516 | + URL.revokeObjectURL(audioUrl); |
| 517 | + this.updateStatus('Ready'); |
| 518 | + }); |
| 519 | + |
| 520 | + await audio.play(); |
| 521 | + |
| 522 | + } catch (error) { |
| 523 | + console.error('Error previewing voice:', error); |
| 524 | + this.updateStatus('Ready'); |
| 525 | + throw error; |
| 526 | + } |
| 527 | + } |
| 528 | + |
| 529 | + //#####################// |
| 530 | + // Get Voice Object // |
| 531 | + //#####################// |
| 532 | + |
| 533 | + async getVoice(voiceName) { |
| 534 | + // Ensure voices are loaded |
| 535 | + if (this.voices.length === 0) { |
| 536 | + await this.fetchTtsVoiceObjects(); |
| 537 | + } |
| 538 | + |
| 539 | + // Find the voice object by name or voice_id |
| 540 | + let match = this.voices.find(voice => |
| 541 | + voice.name === voiceName || |
| 542 | + voice.voice_id === voiceName || |
| 543 | + voice.display_name === voiceName, |
| 544 | + ); |
| 545 | + |
| 546 | + if (!match) { |
| 547 | + console.warn(`Voice not found: ${voiceName}`); |
| 548 | + // Check if it's a reference voice that wasn't in the list |
| 549 | + if (voiceName && voiceName.startsWith('ref_')) { |
| 550 | + const filename = voiceName.substring(4); |
| 551 | + return { |
| 552 | + name: `[Clone] ${filename}`, |
| 553 | + voice_id: voiceName, |
| 554 | + preview_url: null, |
| 555 | + lang: 'en', |
| 556 | + }; |
| 557 | + } |
| 558 | + // Return a default voice object |
| 559 | + return { |
| 560 | + name: voiceName || 'Default', |
| 561 | + voice_id: voiceName || this.settings.predefined_voice || 'S1', |
| 562 | + preview_url: null, |
| 563 | + lang: 'en', |
| 564 | + }; |
| 565 | + } |
| 566 | + |
| 567 | + return match; |
| 568 | + } |
| 569 | + |
| 570 | + //##################// |
| 571 | + // Generate TTS // |
| 572 | + //##################// |
| 573 | + |
| 574 | + async generateTts(inputText, voiceId) { |
| 575 | + try { |
| 576 | + this.updateStatus('Processing'); |
| 577 | + |
| 578 | + // Determine if this is a reference voice |
| 579 | + let isReferenceVoice = false; |
| 580 | + let actualVoiceId = voiceId; |
| 581 | + |
| 582 | + if (voiceId && voiceId.startsWith('ref_')) { |
| 583 | + isReferenceVoice = true; |
| 584 | + actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix |
| 585 | + } |
| 586 | + |
| 587 | + // Prepare the request body |
| 588 | + const requestBody = { |
| 589 | + text: inputText, |
| 590 | + voice_mode: isReferenceVoice ? 'clone' : 'predefined', |
| 591 | + temperature: this.settings.temperature, |
| 592 | + exaggeration: this.settings.exaggeration, |
| 593 | + cfg_weight: this.settings.cfg_weight, |
| 594 | + seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1 |
| 595 | + speed_factor: this.settings.speed_factor, |
| 596 | + language: this.settings.language, |
| 597 | + split_text: this.settings.split_text, |
| 598 | + chunk_size: this.settings.chunk_size, |
| 599 | + output_format: this.settings.output_format, |
| 600 | + }; |
| 601 | + |
| 602 | + // Add voice-specific parameters |
| 603 | + if (isReferenceVoice) { |
| 604 | + requestBody.reference_audio_filename = actualVoiceId; |
| 605 | + } else { |
| 606 | + requestBody.predefined_voice_id = actualVoiceId || this.settings.predefined_voice; |
| 607 | + } |
| 608 | + |
| 609 | + console.log('Generating TTS with params:', requestBody); |
| 610 | + |
| 611 | + const response = await fetch(`${this.settings.provider_endpoint}/tts`, { |
| 612 | + method: 'POST', |
| 613 | + headers: { |
| 614 | + 'Content-Type': 'application/json', |
| 615 | + 'Cache-Control': 'no-cache', |
| 616 | + }, |
| 617 | + body: JSON.stringify(requestBody), |
| 618 | + }); |
| 619 | + |
| 620 | + if (!response.ok) { |
| 621 | + const errorText = await response.text(); |
| 622 | + console.error('TTS generation error:', errorText); |
| 623 | + throw new Error(`HTTP ${response.status}: ${errorText}`); |
| 624 | + } |
| 625 | + |
| 626 | + this.updateStatus('Ready'); |
| 627 | + |
| 628 | + // Return the response directly - SillyTavern expects a Response object |
| 629 | + return response; |
| 630 | + |
| 631 | + } catch (error) { |
| 632 | + console.error('Error in generateTts:', error); |
| 633 | + this.updateStatus('Ready'); |
| 634 | + throw error; |
| 635 | + } |
| 636 | + } |
| 637 | + |
| 638 | + //######################// |
| 639 | + // Update Status // |
| 640 | + //######################// |
| 641 | + |
| 642 | + updateStatus(status) { |
| 643 | + const statusElement = document.getElementById('chatterbox-status'); |
| 644 | + if (statusElement) { |
| 645 | + statusElement.textContent = status; |
| 646 | + statusElement.className = status.toLowerCase(); |
| 647 | + } |
| 648 | + } |
| 649 | +} |