| | 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 | } |
|
\ No newline at end of file | 646 |
\ No newline at end of file |