feat: add tts provider MiniMax (#4309) * feat: add tts provider MiniMax * fix: address code review feedback from @Cohee1207 - Moved inline styles to CSS classes in minimax.js - Removed unused MINIMAX API key from secrets.js - Removed static headers from debug logs - Changed default API host to international endpoint - Sorted TTS providers alphabetically in index.js * feat: improve error handling and add language-specific test sentences based on selected voice * fix: if no language mapping to MiniMax Format should use "auto" * fix: move this minimax-tts.css under /public/scripts/extensions/tts/ * refactor: move minimax api communication to Node backend * chore: unify import style for minimax router * Move css into subfolder * More secure secret key handling * Better UI layout * Finish secrets update * Replace with a normal import * This block shouldn't be here --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

c283025b157d9668b47b4ad3780cb772dbb712b6

An5w1r <167383411+MindsMaster@users.noreply.github.com>

Signed
8 files changed, +1210 -0Ignore whitespace
public/scripts/extensions/tts/css/minimax-tts.css+33 -0
@@ -0,0 +1,33 @@
1.minimax-custom-item {
2 display: flex;
3 justify-content: space-between;
4 align-items: center;
5 padding: 8px;
6 background: #333;
7 margin: 5px 0;
8 border-radius: 4px;
9}
10
11.minimax-custom-item-info {
12 display: flex;
13 flex-direction: column;
14}
15
16.minimax-custom-item-name {
17 font-weight: bold;
18}
19
20.minimax-custom-item-details {
21 color: #aaa;
22 font-size: 0.9em;
23}
24
25.minimax-custom-item-remove {
26 padding: 4px 8px;
27 font-size: 12px;
28}
29
30.minimax-empty-list {
31 color: #888;
32 font-style: italic;
33}
public/scripts/extensions/tts/index.js+2 -0
@@ -32,6 +32,7 @@ import { ChatterboxTtsProvider } from './chatterbox.js';
32import { KokoroTtsProvider } from './kokoro.js';32import { KokoroTtsProvider } from './kokoro.js';
33import { TtsWebuiProvider } from './tts-webui.js';33import { TtsWebuiProvider } from './tts-webui.js';
34import { PollinationsTtsProvider } from './pollinations.js';34import { PollinationsTtsProvider } from './pollinations.js';
35import { MiniMaxTtsProvider } from './minimax.js';
3536
36const UPDATE_INTERVAL = 1000;37const UPDATE_INTERVAL = 1000;
37const wrapper = new ModuleWorkerWrapper(moduleWorker);38const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -127,6 +128,7 @@ const ttsProviders = {
127 GSVI: GSVITtsProvider,128 GSVI: GSVITtsProvider,
128 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,129 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
129 Kokoro: KokoroTtsProvider,130 Kokoro: KokoroTtsProvider,
131 MiniMax: MiniMaxTtsProvider,
130 Novel: NovelTtsProvider,132 Novel: NovelTtsProvider,
131 OpenAI: OpenAITtsProvider,133 OpenAI: OpenAITtsProvider,
132 'OpenAI Compatible': OpenAICompatibleTtsProvider,134 'OpenAI Compatible': OpenAICompatibleTtsProvider,
public/scripts/extensions/tts/minimax.js+935 -0
@@ -0,0 +1,935 @@
1import { getPreviewString, initVoiceMap, saveTtsProviderSettings } from './index.js';
2import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
3import { SECRET_KEYS, secret_state } from '../../secrets.js';
4import { getBase64Async } from '../../utils.js';
5
6export { MiniMaxTtsProvider };
7
8class MiniMaxTtsProvider {
9 //########//
10 // Config //
11 //########//
12
13 settings;
14 voices = [];
15 separator = ' . ';
16 audioElement = document.createElement('audio');
17
18 defaultSettings = {
19 apiHost: 'https://api.minimax.io',
20 model: 'speech-02-hd',
21 voiceMap: {},
22 speed: 1.0,
23 volume: 1.0,
24 pitch: 1.0,
25 audioSampleRate: 32000,
26 bitrate: 128000,
27 format: 'mp3',
28 customModels: [],
29 customVoices: [],
30 customVoiceId: '',
31 };
32
33 // MiniMax API doesn't provide a method to list user's cloned voices
34 // so users need to manually input their custom cloned voice IDs
35 static defaultVoices = [
36 { name: 'Unrestrained Young Man', voice_id: 'Chinese (Mandarin)_Unrestrained_Young_Man', lang: 'zh-CN', preview_url: null },
37 ];
38
39 // default models (by MiniMax doc)
40 static defaultModels = [
41 { id: 'speech-02-hd', name: 'Speech-02-HD (High Quality)' },
42 { id: 'speech-02-turbo', name: 'Speech-02-Turbo (Fast)' },
43 { id: 'speech-01', name: 'Speech-01 (Legacy)' },
44 { id: 'speech-01-240228', name: 'Speech-01-240228 (Legacy)' },
45 ];
46
47 availableModels = [];
48 availableVoices = [];
49
50 get settingsHtml() {
51 return `
52 <div class="minimax_tts_settings">
53 <div class="tts_block justifyCenter">
54 <div id="api_key_minimax" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_minimax">
55 <i class="fa-solid fa-key"></i>
56 <span>Click to set API Key</span>
57 </div>
58 <div id="minimax_group_id" class="menu_button menu_button_icon manage-api-keys" data-key="minimax_group_id">
59 <i class="fa-solid fa-key"></i>
60 <span>Click to set Group ID</span>
61 </div>
62 </div>
63 <div class="tts_block">
64 <label for="minimax_tts_api_host">API Host</label>
65 <select id="minimax_tts_api_host" class="text_pole">
66 <option value="https://api.minimax.io">Official (api.minimax.io)</option>
67 <option value="https://api.minimaxi.chat">Global (api.minimaxi.chat)</option>
68 <option value="https://api.minimax.chat">Mainland China (api.minimax.chat)</option>
69 </select>
70 </div>
71 <div class="tts_block">
72 <label for="minimax_tts_model">Model</label>
73 <select id="minimax_tts_model" class="text_pole">
74 <option value="speech-02-hd">Speech-02-HD (High Quality)</option>
75 <option value="speech-02-turbo">Speech-02-Turbo (Fast)</option>
76 <option value="speech-01">Speech-01 (Legacy)</option>
77 <option value="speech-01-240228">Speech-01-240228 (Legacy)</option>
78 </select>
79 </div>
80 <div class="tts_block">
81 <input id="minimax_connect" class="menu_button" type="button" value="Connect" />
82 <input id="minimax_refresh" class="menu_button" type="button" value="Refresh" />
83 </div>
84
85 <div class="tts_block">
86 <label for="minimax_tts_speed">Speed: <span id="minimax_tts_speed_output"></span></label>
87 <input id="minimax_tts_speed" type="range" value="${this.defaultSettings.speed}" min="0.5" max="2.0" step="0.1" />
88 </div>
89 <div class="tts_block">
90 <label for="minimax_tts_volume">Volume: <span id="minimax_tts_volume_output"></span></label>
91 <input id="minimax_tts_volume" type="range" value="${this.defaultSettings.volume}" min="0.1" max="2.0" step="0.1" />
92 </div>
93 <div class="tts_block">
94 <label for="minimax_tts_pitch">Pitch: <span id="minimax_tts_pitch_output"></span></label>
95 <input id="minimax_tts_pitch" type="range" value="${this.defaultSettings.pitch}" min="0.5" max="2.0" step="0.1" />
96 </div>
97 <div class="tts_block">
98 <label for="minimax_tts_format">Audio Format</label>
99 <select id="minimax_tts_format" class="text_pole">
100 <option value="mp3">MP3</option>
101 <option value="wav">WAV</option>
102 <option value="flac">FLAC</option>
103 </select>
104 </div>
105
106 <hr>
107 <div class="tts_block">
108 <label for="minimax_tts_custom_voice_id">Custom Voice ID (for 'customVoice' option)</label>
109 <input id="minimax_tts_custom_voice_id" type="text" class="text_pole" placeholder="Enter custom voice ID from MiniMax platform"/>
110 </div>
111
112 <hr>
113 <div id="minimax_custom_voice_cloning" class="tts_block flexFlowColumn">
114 <h4>Custom Voice Management</h4>
115 <div class="tts_block wide100p">
116 <input id="minimax_custom_voice_name" type="text" class="text_pole" placeholder="Voice Name"/>
117 </div>
118 <div class="tts_block wide100p">
119 <input id="minimax_custom_voice_id" type="text" class="text_pole" placeholder="Voice ID (from MiniMax platform)"/>
120 </div>
121 <div class="tts_block wide100p">
122 <select id="minimax_custom_voice_lang" class="text_pole">
123 <option value="auto">Auto Detect</option>
124 <option value="Chinese">Chinese (中文)</option>
125 <option value="Chinese,Yue">Chinese, Yue (粤语)</option>
126 <option value="English">English</option>
127 <option value="Arabic">Arabic (العربية)</option>
128 <option value="Russian">Russian (Русский)</option>
129 <option value="Spanish">Spanish (Español)</option>
130 <option value="French">French (Français)</option>
131 <option value="Portuguese">Portuguese (Português)</option>
132 <option value="German">German (Deutsch)</option>
133 <option value="Turkish">Turkish (Türkçe)</option>
134 <option value="Dutch">Dutch (Nederlands)</option>
135 <option value="Ukrainian">Ukrainian (Українська)</option>
136 <option value="Vietnamese">Vietnamese (Tiếng Việt)</option>
137 <option value="Indonesian">Indonesian (Bahasa Indonesia)</option>
138 <option value="Japanese">Japanese (日本語)</option>
139 <option value="Italian">Italian (Italiano)</option>
140 <option value="Korean">Korean (한국어)</option>
141 <option value="Thai">Thai (ไทย)</option>
142 <option value="Polish">Polish (Polski)</option>
143 <option value="Romanian">Romanian (Română)</option>
144 <option value="Greek">Greek (Ελληνικά)</option>
145 <option value="Czech">Czech (Čeština)</option>
146 <option value="Finnish">Finnish (Suomi)</option>
147 <option value="Hindi">Hindi (हिन्दी)</option>
148 </select>
149 </div>
150 <div class="tts_block">
151 <input id="minimax_add_custom_voice" class="menu_button" type="button" value="Add Custom Voice">
152 </div>
153 <div id="minimax_custom_voices_list" style="margin-top: 10px;"></div>
154 </div>
155
156 <hr>
157 <div id="minimax_custom_model_management" class="tts_block flexFlowColumn">
158 <h4>Custom Model Management</h4>
159 <div class="tts_block wide100p">
160 <input id="minimax_custom_model_id" type="text" class="text_pole" placeholder="Model ID"/>
161 </div>
162 <div class="tts_block wide100p">
163 <input id="minimax_custom_model_name" type="text" class="text_pole" placeholder="Model Name"/>
164 </div>
165 <div class="tts_block">
166 <input id="minimax_add_custom_model" class="menu_button" type="button" value="Add Custom Model">
167 </div>
168 <div id="minimax_custom_models_list" style="margin-top: 10px;"></div>
169 </div>
170 </div>
171 `;
172 }
173
174 constructor() {
175 this.handler = async function (/** @type {string} */ key) {
176 if (![SECRET_KEYS.MINIMAX, SECRET_KEYS.MINIMAX_GROUP_ID].includes(key)) return;
177 $('#api_key_minimax').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX]);
178 $('#minimax_group_id').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]);
179 await this.onRefreshClick();
180 }.bind(this);
181 }
182
183 dispose() {
184 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
185 eventSource.removeListener(event, this.handler);
186 });
187 }
188
189 onSettingsChange() {
190 this.settings.apiHost = $('#minimax_tts_api_host').val();
191 this.settings.speed = parseFloat($('#minimax_tts_speed').val().toString());
192 this.settings.volume = parseFloat($('#minimax_tts_volume').val().toString());
193 this.settings.pitch = parseFloat($('#minimax_tts_pitch').val().toString());
194 this.settings.model = $('#minimax_tts_model').find(':selected').val();
195 this.settings.format = $('#minimax_tts_format').find(':selected').val();
196 this.settings.customVoiceId = $('#minimax_tts_custom_voice_id').val();
197
198 $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1));
199 $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1));
200 $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1));
201
202 saveTtsProviderSettings();
203 }
204
205 addCustomModel() {
206 const modelId = $('#minimax_custom_model_id').val().toString().trim();
207 const modelName = $('#minimax_custom_model_name').val().toString().trim();
208
209 if (!modelId || !modelName) {
210 toastr.error('Please enter model ID and name');
211 return;
212 }
213
214 // Check if already exists in custom models
215 if (this.settings.customModels.find(m => m.id === modelId)) {
216 toastr.error('Model ID already exists in custom models');
217 return;
218 }
219
220 // Check if conflicts with default models
221 if (MiniMaxTtsProvider.defaultModels.find(m => m.id === modelId)) {
222 toastr.error('Model ID conflicts with default model. Please use a different model ID.');
223 return;
224 }
225
226 // Check if conflicts with default model names
227 if (MiniMaxTtsProvider.defaultModels.find(m => m.name === modelName)) {
228 toastr.error('Model name conflicts with default model. Please use a different model name.');
229 return;
230 }
231
232 this.settings.customModels.push({ id: modelId, name: modelName });
233 $('#minimax_custom_model_id').val('');
234 $('#minimax_custom_model_name').val('');
235
236 this.updateCustomModelsDisplay();
237 this.updateModelSelect(this.getAllModels());
238 saveTtsProviderSettings();
239 toastr.success('Model added successfully');
240 }
241
242 removeCustomModel(modelId) {
243 this.settings.customModels = this.settings.customModels.filter(m => m.id !== modelId);
244 this.updateCustomModelsDisplay();
245 this.updateModelSelect(this.getAllModels());
246 saveTtsProviderSettings();
247
248 toastr.success('Model removed successfully');
249 }
250
251 addCustomVoice() {
252 const voiceName = $('#minimax_custom_voice_name').val().toString().trim();
253 const voiceId = $('#minimax_custom_voice_id').val().toString().trim();
254 const voiceLang = $('#minimax_custom_voice_lang').val().toString().trim();
255
256 if (!voiceName || !voiceId) {
257 toastr.error('Please enter voice name and ID');
258 return;
259 }
260
261 // Check if already exists in custom voices
262 if (this.settings.customVoices.find(v => v.voice_id === voiceId)) {
263 toastr.error('Voice ID already exists in custom voices');
264 return;
265 }
266
267 // Check if conflicts with default voices
268 if (MiniMaxTtsProvider.defaultVoices.find(v => v.voice_id === voiceId)) {
269 toastr.error('Voice ID conflicts with default voice. Please use a different voice ID.');
270 return;
271 }
272
273 // Check if conflicts with default voice names
274 if (MiniMaxTtsProvider.defaultVoices.find(v => v.name === voiceName)) {
275 toastr.error('Voice name conflicts with default voice. Please use a different voice name.');
276 return;
277 }
278
279 // Convert display name to standard language code before saving
280 const standardLangCode = this.convertDisplayNameToLanguageCode(voiceLang);
281
282 this.settings.customVoices.push({
283 name: voiceName,
284 voice_id: voiceId,
285 lang: standardLangCode,
286 preview_url: null,
287 });
288
289 $('#minimax_custom_voice_name').val('');
290 $('#minimax_custom_voice_id').val('');
291 $('#minimax_custom_voice_lang').val('auto');
292
293 this.updateCustomVoicesDisplay();
294 initVoiceMap(); // Update TTS extension voiceMap
295 saveTtsProviderSettings();
296 toastr.success('Voice added successfully');
297 }
298
299 // Remove custom voice
300 removeCustomVoice(voiceId) {
301 this.settings.customVoices = this.settings.customVoices.filter(v => v.voice_id !== voiceId);
302 this.updateCustomVoicesDisplay();
303 initVoiceMap(); // Update TTS extension voiceMap
304 saveTtsProviderSettings();
305 toastr.success('Voice removed successfully');
306 }
307
308 // Helper function to escape HTML
309 escapeHtml(text) {
310 const div = document.createElement('div');
311 div.textContent = text;
312 return div.innerHTML;
313 }
314
315 // Update custom models display
316 updateCustomModelsDisplay() {
317 const container = $('#minimax_custom_models_list');
318 container.empty();
319
320 if (this.settings.customModels.length === 0) {
321 container.append('<div class="minimax-empty-list">No custom models added</div>');
322 return;
323 }
324
325 this.settings.customModels.forEach(model => {
326 const modelDiv = $('<div></div>').addClass('minimax-custom-item');
327
328 const modelInfo = $('<div></div>').addClass('minimax-custom-item-info');
329 const modelName = $('<div></div>').addClass('minimax-custom-item-name').text(model.name);
330 const modelId = $('<div></div>').addClass('minimax-custom-item-details').text(`(${model.id})`);
331 modelInfo.append(modelName).append(modelId);
332
333 const removeBtn = $('<button></button>')
334 .addClass('menu_button minimax-custom-item-remove')
335 .text('Remove')
336 .on('click', () => {
337 try {
338 this.removeCustomModel(model.id);
339 } catch (error) {
340 console.error('MiniMax TTS: Error removing custom model:', error);
341 toastr.error(`Failed to remove custom model: ${error.message}`);
342 }
343 });
344
345 modelDiv.append(modelInfo).append(removeBtn);
346 container.append(modelDiv);
347 });
348 }
349
350 // Update custom voices display
351 updateCustomVoicesDisplay() {
352 const container = $('#minimax_custom_voices_list');
353 container.empty();
354
355 if (this.settings.customVoices.length === 0) {
356 container.append('<div class="minimax-empty-list">No custom voices added</div>');
357 return;
358 }
359
360 this.settings.customVoices.forEach(voice => {
361 const voiceDiv = $('<div></div>').addClass('minimax-custom-item');
362
363 const voiceInfo = $('<div></div>').addClass('minimax-custom-item-info');
364 const voiceName = $('<div></div>').addClass('minimax-custom-item-name').text(voice.name);
365 const voiceDetails = $('<div></div>').addClass('minimax-custom-item-details').text(`(${voice.voice_id}) - ${voice.lang}`);
366 voiceInfo.append(voiceName).append(voiceDetails);
367
368 const removeBtn = $('<button></button>')
369 .addClass('menu_button minimax-custom-item-remove')
370 .text('Remove')
371 .on('click', () => {
372 try {
373 this.removeCustomVoice(voice.voice_id);
374 } catch (error) {
375 console.error('MiniMax TTS: Error removing custom voice:', error);
376 toastr.error(`Failed to remove custom voice: ${error.message}`);
377 }
378 });
379
380 voiceDiv.append(voiceInfo).append(removeBtn);
381 container.append(voiceDiv);
382 });
383 }
384
385 // Get all models (default + custom)
386 getAllModels() {
387 return [...MiniMaxTtsProvider.defaultModels, ...this.settings.customModels];
388 }
389
390 // Get all voices (default + custom)
391 getAllVoices() {
392 return [...MiniMaxTtsProvider.defaultVoices, ...this.settings.customVoices];
393 }
394
395 /**
396 * Convert display names to standard language codes
397 * @param {string} displayName Language display name
398 * @returns {string} Standard language code
399 */
400 convertDisplayNameToLanguageCode(displayName) {
401 const displayNameToCode = {
402 'Chinese': 'zh-CN',
403 'Chinese,Yue': 'zh-TW',
404 'English': 'en-US',
405 'Japanese': 'ja-JP',
406 'Korean': 'ko-KR',
407 'French': 'fr-FR',
408 'German': 'de-DE',
409 'Spanish': 'es-ES',
410 'Portuguese': 'pt-BR',
411 'Italian': 'it-IT',
412 'Arabic': 'ar-SA',
413 'Russian': 'ru-RU',
414 'Turkish': 'tr-TR',
415 'Dutch': 'nl-NL',
416 'Ukrainian': 'uk-UA',
417 'Vietnamese': 'vi-VN',
418 'Indonesian': 'id-ID',
419 'Thai': 'th-TH',
420 'Polish': 'pl-PL',
421 'Romanian': 'ro-RO',
422 'Greek': 'el-GR',
423 'Czech': 'cs-CZ',
424 'Finnish': 'fi-FI',
425 'Hindi': 'hi-IN',
426 };
427
428 return displayNameToCode[displayName] || displayName;
429 }
430
431 updateModelSelect(models) {
432 const modelSelect = $('#minimax_tts_model');
433 const currentValue = modelSelect.val();
434
435 // Clear existing options
436 modelSelect.empty();
437
438 // Add all models
439 models.forEach(model => {
440 const option = $('<option></option>');
441 option.val(model.id);
442 option.text(model.name);
443 modelSelect.append(option);
444 });
445
446 // Restore previous selection if it still exists
447 if (currentValue && models.find(m => m.id === currentValue)) {
448 modelSelect.val(currentValue);
449 }
450 }
451
452 async loadSettings(settings) {
453 // Populate Provider UI given input settings
454 if (Object.keys(settings).length === 0) {
455 console.info('Using default MiniMax TTS Provider settings');
456 }
457
458 // Only accept keys defined in defaultSettings
459 this.settings = { ...this.defaultSettings };
460
461 for (const key in settings) {
462 if (key in this.settings) {
463 this.settings[key] = settings[key];
464 } else {
465 console.warn(`Invalid setting passed to MiniMax TTS Provider: ${key}`);
466 }
467 }
468
469 // Ensure custom configuration arrays exist
470 if (!this.settings.customModels) this.settings.customModels = [];
471 if (!this.settings.customVoices) this.settings.customVoices = [];
472
473 $('#minimax_tts_api_host').val(this.settings.apiHost || 'https://api.minimax.io');
474 $('#minimax_tts_model').val(this.settings.model);
475 $('#minimax_tts_speed').val(this.settings.speed);
476 $('#minimax_tts_volume').val(this.settings.volume);
477 $('#minimax_tts_pitch').val(this.settings.pitch);
478 $('#minimax_tts_format').val(this.settings.format);
479 $('#minimax_tts_custom_voice_id').val(this.settings.customVoiceId);
480
481 $('#minimax_connect').on('click', () => {
482 try {
483 this.onConnectClick();
484 } catch (error) {
485 console.error('MiniMax TTS: Error in connect click handler:', error);
486 toastr.error(`Connection failed: ${error.message}`);
487 }
488 });
489 $('#minimax_refresh').on('click', () => {
490 try {
491 this.onRefreshClick();
492 } catch (error) {
493 console.error('MiniMax TTS: Error in refresh click handler:', error);
494 toastr.error(`Refresh failed: ${error.message}`);
495 }
496 });
497 $('#minimax_tts_api_host').on('change', this.onSettingsChange.bind(this));
498 $('#minimax_tts_speed').on('input', this.onSettingsChange.bind(this));
499 $('#minimax_tts_volume').on('input', this.onSettingsChange.bind(this));
500 $('#minimax_tts_pitch').on('input', this.onSettingsChange.bind(this));
501 $('#minimax_tts_model').on('change', this.onSettingsChange.bind(this));
502 $('#minimax_tts_format').on('change', this.onSettingsChange.bind(this));
503 $('#minimax_tts_custom_voice_id').on('input', this.onSettingsChange.bind(this));
504
505 // Custom model and voice event listeners
506 $('#minimax_add_custom_model').on('click', () => {
507 try {
508 this.addCustomModel();
509 } catch (error) {
510 console.error('MiniMax TTS: Error adding custom model:', error);
511 toastr.error(`Failed to add custom model: ${error.message}`);
512 }
513 });
514 $('#minimax_add_custom_voice').on('click', () => {
515 try {
516 this.addCustomVoice();
517 } catch (error) {
518 console.error('MiniMax TTS: Error adding custom voice:', error);
519 toastr.error(`Failed to add custom voice: ${error.message}`);
520 }
521 });
522
523 // Keyboard event listeners
524 const ENTER_KEY = 13;
525 $('#minimax_custom_model_id, #minimax_custom_model_name').on('keypress', (e) => {
526 if (e.which === ENTER_KEY) {
527 try {
528 this.addCustomModel();
529 } catch (error) {
530 console.error('MiniMax TTS: Error adding custom model via keyboard:', error);
531 toastr.error(`Failed to add custom model: ${error.message}`);
532 }
533 }
534 });
535
536 $('#minimax_custom_voice_name, #minimax_custom_voice_id').on('keypress', (e) => {
537 if (e.which === ENTER_KEY) {
538 try {
539 this.addCustomVoice();
540 } catch (error) {
541 console.error('MiniMax TTS: Error adding custom voice via keyboard:', error);
542 toastr.error(`Failed to add custom voice: ${error.message}`);
543 }
544 }
545 });
546
547 $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1));
548 $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1));
549 $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1));
550
551 // Initialize custom configuration display
552 this.updateCustomModelsDisplay();
553 this.updateCustomVoicesDisplay();
554
555 // Update model selector to include custom models
556 this.updateModelSelect(this.getAllModels());
557
558 // Initialize voice map for character voice assignment
559 try {
560 await initVoiceMap();
561 } catch (error) {
562 console.debug('MiniMax: Voice map initialization failed, but continuing');
563 }
564
565 $('#api_key_minimax').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX]);
566 $('#minimax_group_id').toggleClass('success', !!secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]);
567 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
568 eventSource.on(event, this.handler);
569 });
570
571 // Only check ready status when API credentials are available
572 if (secret_state[SECRET_KEYS.MINIMAX] && secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
573 try {
574 await this.checkReady();
575 console.debug('MiniMax TTS: Settings loaded and ready');
576 } catch (error) {
577 console.debug('MiniMax TTS: Settings loaded, but not ready:', error);
578 }
579 } else {
580 console.debug('MiniMax TTS: Settings loaded, waiting for API credentials');
581 }
582 }
583
584 // Perform a simple readiness check
585 async checkReady() {
586 if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
587 const error = new Error('API Key and Group ID are required');
588 console.error('MiniMax TTS checkReady error:', error.message);
589 throw error;
590 }
591 // Try to fetch available models and voices, but don't block connection on failure
592 try {
593 await this.updateModelsAndVoices();
594 } catch (error) {
595 console.warn('MiniMax TTS: Failed to fetch models/voices during ready check, will use all available:', error);
596 // Even if API call fails, set all available values to ensure basic functionality
597 this.availableModels = this.getAllModels();
598 this.availableVoices = this.getAllVoices();
599 }
600
601 // Ensure at least voices are available
602 if (!this.availableVoices || this.availableVoices.length === 0) {
603 this.availableVoices = this.getAllVoices();
604 }
605 }
606
607 async onRefreshClick() {
608 try {
609 await this.updateModelsAndVoices();
610 await initVoiceMap(); // Update voice map after refresh
611 toastr.success('MiniMax TTS: Models and voices refreshed successfully');
612 } catch (error) {
613 toastr.error(`MiniMax TTS: Failed to refresh - ${error.message}`);
614 }
615 }
616
617 async onConnectClick() {
618 try {
619 await this.checkReady();
620 await initVoiceMap(); // Update voice map after connection
621 toastr.success('MiniMax TTS: Connected successfully');
622 saveTtsProviderSettings();
623 } catch (error) {
624 toastr.error(`MiniMax TTS: ${error.message}`);
625 }
626 }
627
628 async getVoice(voiceName) {
629 if (!voiceName) {
630 const error = new Error('TTS Voice name not provided');
631 console.error('MiniMax TTS getVoice error:', error.message);
632 throw error;
633 }
634
635 // If no available voices, try to fetch them
636 if (!this.availableVoices || this.availableVoices.length === 0) {
637 this.availableVoices = await this.fetchTtsVoiceObjects();
638 }
639
640 // Ensure at least voices are available
641 if (!this.availableVoices || this.availableVoices.length === 0) {
642 this.availableVoices = this.getAllVoices();
643 }
644
645 const voice = this.availableVoices.find(voice =>
646 voice.voice_id === voiceName || voice.name === voiceName,
647 );
648
649 if (!voice) {
650 const error = new Error(`TTS Voice not found: ${voiceName}`);
651 console.error('MiniMax TTS getVoice error:', error.message);
652 throw error;
653 }
654
655 return voice;
656 }
657
658 async generateTts(text, voiceId) {
659 // If voiceId is 'customVoice', use the custom voice ID from settings
660 if (voiceId === 'customVoice') {
661 const customVoiceId = this.settings.customVoiceId;
662 if (!customVoiceId || customVoiceId.trim() === '') {
663 const error = new Error('Please enter custom voice ID in settings first');
664 console.error('MiniMax TTS generateTts error:', error.message);
665 throw error;
666 }
667 voiceId = customVoiceId.trim();
668 }
669
670 // Get the voice object to determine language
671 let language = null;
672 try {
673 const voice = await this.getVoice(voiceId);
674 if (voice && voice.lang) {
675 language = this.mapLanguageToMiniMaxFormat(voice.lang);
676 console.debug(`MiniMax TTS: Using voice language ${voice.lang}, API language: ${language}`);
677 }
678 } catch (error) {
679 console.debug('MiniMax TTS: Could not determine voice language, using default');
680 }
681
682 return await this.fetchTtsGeneration(text, voiceId, language);
683 }
684
685 async fetchTtsVoiceObjects() {
686 try {
687 if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
688 console.warn('MiniMax TTS: API Key and Group ID required for fetching voices');
689 console.warn('Using all available voices (default + custom). Please check your API credentials');
690 return this.getAllVoices();
691 }
692
693 // MiniMax API doesn't provide a voices listing endpoint
694 // Using all available voices (default + custom)
695 console.info('MiniMax TTS: Using all available voices (default + custom)');
696 return this.getAllVoices();
697 } catch (error) {
698 console.error('Error fetching MiniMax voices:', error);
699 console.warn('Using all available voices (default + custom). Please check your API credentials');
700 return this.getAllVoices();
701 }
702 }
703
704 async fetchTtsModels() {
705 // MiniMax API doesn't provide a models listing endpoint
706 // Using all available models (default + custom)
707 console.info('MiniMax TTS: Using all available models (default + custom)');
708 this.availableModels = this.getAllModels();
709 return this.getAllModels();
710 }
711
712 async updateModelsAndVoices() {
713 try {
714 // Get models list
715 this.availableModels = await this.fetchTtsModels();
716 console.info(`MiniMax TTS: Loaded ${this.availableModels.length} models`);
717
718 // Get voices list (now fetched from API)
719 this.availableVoices = await this.fetchTtsVoiceObjects();
720 console.info(`MiniMax TTS: Loaded ${this.availableVoices.length} voices`);
721
722 // Update model dropdown
723 this.updateModelSelect(this.availableModels);
724
725 return {
726 models: this.availableModels,
727 voices: this.availableVoices,
728 };
729 } catch (error) {
730 console.error('MiniMax TTS: Failed to update models and voices:', error);
731 // Set all available values to ensure basic functionality
732 this.availableModels = this.getAllModels();
733 this.availableVoices = this.getAllVoices();
734 throw error;
735 }
736 }
737
738 // Get correct MIME type
739 getAudioMimeType(format) {
740 const mimeTypes = {
741 'mp3': 'audio/mpeg',
742 'wav': 'audio/wav',
743 'pcm': 'audio/pcm',
744 'flac': 'audio/flac',
745 'aac': 'audio/aac',
746 };
747 return mimeTypes[format] || 'audio/mpeg';
748 }
749
750 async fetchTtsGeneration(inputText, voiceId, language = null) {
751 console.info(`Generating new MiniMax TTS for voice_id ${voiceId}`);
752
753 if (!secret_state[SECRET_KEYS.MINIMAX] || !secret_state[SECRET_KEYS.MINIMAX_GROUP_ID]) {
754 const error = new Error('API Key and Group ID are required');
755 console.error('MiniMax TTS fetchTtsGeneration error:', error.message);
756 throw error;
757 }
758
759 const requestBody = {
760 text: inputText,
761 voiceId: voiceId,
762 apiHost: this.settings.apiHost,
763 model: this.settings.model || 'speech-02-hd',
764 speed: Number(this.settings.speed) || 1.0,
765 volume: Number(this.settings.volume) || 1.0,
766 pitch: Number(this.settings.pitch) || 1.0,
767 audioSampleRate: Number(this.settings.audioSampleRate) || 32000,
768 bitrate: Number(this.settings.bitrate) || 128000,
769 format: this.settings.format || 'mp3',
770 language: language,
771 };
772
773 console.debug('MiniMax TTS Request:', {
774 body: { ...requestBody, voiceId: '[REDACTED]' },
775 });
776
777 try {
778 const response = await fetch('/api/minimax/generate-voice', {
779 method: 'POST',
780 headers: getRequestHeaders(),
781 body: JSON.stringify(requestBody),
782 });
783
784 if (!response.ok) {
785 let errorMessage = `HTTP ${response.status}`;
786
787 try {
788 // Try to parse JSON error response from backend
789 const errorData = await response.json();
790 console.error('MiniMax TTS backend error:', errorData);
791 errorMessage = errorData.error || errorMessage;
792 } catch (jsonError) {
793 // If not JSON, try to read text
794 try {
795 const errorText = await response.text();
796 console.error('MiniMax TTS backend error (Text):', errorText);
797 errorMessage = errorText || errorMessage;
798 } catch (textError) {
799 console.error('MiniMax TTS: Failed to read error response:', textError);
800 }
801 }
802
803 toastr.error(`${errorMessage}`, 'MiniMax TTS Generation Failed');
804 const error = new Error(errorMessage);
805 console.error('MiniMax TTS fetchTtsGeneration error:', error.message);
806 throw error;
807 }
808
809 // Backend handles all the complex processing and returns audio data directly
810 console.debug('MiniMax TTS: Audio response received from backend');
811 return response;
812
813 } catch (error) {
814 console.error('Error in MiniMax TTS generation:', error);
815 throw error;
816 }
817 }
818
819 /**
820 * Map language codes to MiniMax API supported language format
821 * @param {string} lang Language code or display name
822 * @returns {string} MiniMax API language format
823 */
824 mapLanguageToMiniMaxFormat(lang) {
825 // Convert display name to language code if needed
826 const languageCode = this.convertDisplayNameToLanguageCode(lang);
827
828 // Then map language codes to MiniMax API format
829 const languageMap = {
830 'zh-CN': 'zh_CN',
831 'zh-TW': 'zh_TW',
832 'en-US': 'en_US',
833 'en-GB': 'en_GB',
834 'en-AU': 'en_AU',
835 'en-IN': 'en_IN',
836 'ja-JP': 'ja_JP',
837 'ko-KR': 'ko_KR',
838 'fr-FR': 'fr_FR',
839 'de-DE': 'de_DE',
840 'es-ES': 'es_ES',
841 'pt-BR': 'pt_BR',
842 'it-IT': 'it_IT',
843 'ar-SA': 'ar_SA',
844 'ru-RU': 'ru_RU',
845 'tr-TR': 'tr_TR',
846 'nl-NL': 'nl_NL',
847 'uk-UA': 'uk_UA',
848 'vi-VN': 'vi_VN',
849 'id-ID': 'id_ID',
850 'th-TH': 'th_TH',
851 'pl-PL': 'pl_PL',
852 'ro-RO': 'ro_RO',
853 'el-GR': 'el_GR',
854 'cs-CZ': 'cs_CZ',
855 'fi-FI': 'fi_FI',
856 'hi-IN': 'hi_IN',
857 };
858
859 // Return mapped language or default to auto
860 return languageMap[languageCode] || 'auto';
861 }
862
863 /**
864 * Preview TTS for a given voice ID.
865 * @param {string} voiceId Voice ID
866 */
867 async previewTtsVoice(voiceId) {
868 this.audioElement.pause();
869 this.audioElement.currentTime = 0;
870
871 try {
872 const voice = await this.getVoice(voiceId);
873 // Get preview text based on voice language, defaulting to en-US
874 const previewLang = voice.lang || 'en-US';
875 const text = getPreviewString(previewLang);
876
877 // Map the language to MiniMax API format for the request
878 const apiLang = this.mapLanguageToMiniMaxFormat(previewLang);
879 console.debug(`MiniMax TTS: Using preview language ${previewLang}, API language: ${apiLang}`);
880
881 const response = await this.fetchTtsGeneration(text, voiceId, apiLang);
882
883 if (!response.ok) {
884 const errorText = await response.text();
885 const error = new Error(`HTTP ${response.status}: ${errorText}`);
886 console.error('MiniMax TTS previewTtsVoice error:', error.message);
887 throw error;
888 }
889
890 const audio = await response.blob();
891 console.debug(`MiniMax TTS: Audio blob size: ${audio.size}, type: ${audio.type}`);
892
893 // Use the same method as other TTS providers - convert to base64 data URL
894 const srcUrl = await getBase64Async(audio);
895 console.debug('MiniMax TTS: Base64 data URL created');
896
897 // Clean up previous event listener to prevent memory leaks
898 this.audioElement.onended = null;
899 this.audioElement.onerror = null;
900
901 this.audioElement.src = srcUrl;
902 this.audioElement.volume = Math.min(this.settings.volume || 1.0, 1.0); // HTML audio element max is 1.0
903
904 // Add error handler for audio element
905 this.audioElement.onerror = (e) => {
906 console.error('MiniMax TTS: Audio element error:', e);
907 console.error('MiniMax TTS: Audio element error details:', {
908 error: this.audioElement.error,
909 networkState: this.audioElement.networkState,
910 readyState: this.audioElement.readyState,
911 src: this.audioElement.src,
912 });
913
914 toastr.error('Audio playback failed. The audio format may not be supported by your browser.');
915 };
916
917 try {
918 await this.audioElement.play();
919 console.debug('MiniMax TTS: Audio playback started successfully');
920 } catch (playError) {
921 console.error('MiniMax TTS: Play error:', playError);
922 throw new Error(`Audio playback failed: ${playError.message}`);
923 }
924
925 this.audioElement.onended = () => {
926 this.audioElement.onended = null;
927 this.audioElement.onerror = null;
928 };
929
930 } catch (error) {
931 console.error('MiniMax TTS Preview Error:', error);
932 toastr.error(`Could not generate preview: ${error.message}`);
933 }
934 }
935}
public/scripts/extensions/tts/style.css+2 -0
@@ -125,3 +125,5 @@
125#at-status_info {125#at-status_info {
126 color: lightgreen;126 color: lightgreen;
127}127}
128
129@import './css/minimax-tts.css';
public/scripts/secrets.js+4 -0
@@ -62,6 +62,8 @@ export const SECRET_KEYS = {
62 FALAI: 'api_key_falai',62 FALAI: 'api_key_falai',
63 XAI: 'api_key_xai',63 XAI: 'api_key_xai',
64 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',64 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
65 MINIMAX: 'api_key_minimax',
66 MINIMAX_GROUP_ID: 'minimax_group_id',
65};67};
6668
67const FRIENDLY_NAMES = {69const FRIENDLY_NAMES = {
@@ -112,6 +114,8 @@ const FRIENDLY_NAMES = {
112 [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)',114 [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)',
113 [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)',115 [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)',
114 [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)',116 [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)',
117 [SECRET_KEYS.MINIMAX]: 'MiniMax TTS',
118 [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',
115};119};
116120
117const INPUT_MAP = {121const INPUT_MAP = {
src/endpoints/minimax.js+230 -0
@@ -0,0 +1,230 @@
1import express from 'express';
2import fetch from 'node-fetch';
3import { readSecret, SECRET_KEYS } from './secrets.js';
4
5export const router = express.Router();
6
7// Audio format MIME type mapping
8const getAudioMimeType = (format) => {
9 const mimeTypes = {
10 'mp3': 'audio/mpeg',
11 'wav': 'audio/wav',
12 'pcm': 'audio/pcm',
13 'flac': 'audio/flac',
14 'aac': 'audio/aac',
15 };
16 return mimeTypes[format] || 'audio/mpeg';
17};
18
19router.post('/generate-voice', async (request, response) => {
20 try {
21 const {
22 text,
23 voiceId,
24 apiHost = 'https://api.minimax.io',
25 model = 'speech-02-hd',
26 speed = 1.0,
27 volume = 1.0,
28 pitch = 1.0,
29 audioSampleRate = 32000,
30 bitrate = 128000,
31 format = 'mp3',
32 language,
33 } = request.body;
34
35 const apiKey = readSecret(request.user.directories, SECRET_KEYS.MINIMAX);
36 const groupId = readSecret(request.user.directories, SECRET_KEYS.MINIMAX_GROUP_ID);
37
38 // Validate required parameters
39 if (!text || !voiceId || !apiKey || !groupId) {
40 console.warn('MiniMax TTS: Missing required parameters');
41 return response.status(400).json({ error: 'Missing required parameters: text, voiceId, apiKey, and groupId are required' });
42 }
43
44 const requestBody = {
45 model: model,
46 text: text,
47 stream: false,
48 voice_setting: {
49 voice_id: voiceId,
50 speed: Number(speed),
51 vol: Number(volume),
52 pitch: Number(pitch),
53 },
54 audio_setting: {
55 sample_rate: Number(audioSampleRate),
56 bitrate: Number(bitrate),
57 format: format,
58 channel: 1,
59 },
60 };
61
62 // Add language parameter if provided
63 if (language) {
64 requestBody.lang = language;
65 }
66
67 const apiUrl = `${apiHost}/v1/t2a_v2?GroupId=${groupId}`;
68
69 console.debug('MiniMax TTS Request:', {
70 url: apiUrl,
71 body: { ...requestBody, voice_setting: { ...requestBody.voice_setting, voice_id: '[REDACTED]' } },
72 });
73
74 const apiResponse = await fetch(apiUrl, {
75 method: 'POST',
76 headers: {
77 'Authorization': `Bearer ${apiKey}`,
78 'Content-Type': 'application/json',
79 'MM-API-Source': 'SillyTavern-TTS',
80 },
81 body: JSON.stringify(requestBody),
82 });
83
84 if (!apiResponse.ok) {
85 let errorMessage = `HTTP ${apiResponse.status}`;
86
87 try {
88 // Try to parse JSON error response
89 /** @type {any} */
90 const errorData = await apiResponse.json();
91 console.error('MiniMax TTS API error (JSON):', errorData);
92
93 // Check for MiniMax specific error format
94 const baseResp = errorData?.base_resp;
95 if (baseResp && baseResp.status_code !== 0) {
96 if (baseResp.status_code === 1004) {
97 errorMessage = 'Authentication failed - Please check your API key and API host';
98 } else {
99 errorMessage = `API Error: ${baseResp.status_msg}`;
100 }
101 } else {
102 errorMessage = errorData.error?.message || errorData.message || errorData.detail || `HTTP ${apiResponse.status}`;
103 }
104 } catch (jsonError) {
105 // If not JSON, try to read text
106 try {
107 const errorText = await apiResponse.text();
108 console.error('MiniMax TTS API error (Text):', errorText);
109 if (errorText && errorText.length > 500) {
110 errorMessage = `HTTP ${apiResponse.status}: Response too large (${errorText.length} characters)`;
111 } else {
112 errorMessage = errorText || `HTTP ${apiResponse.status}`;
113 }
114 } catch (textError) {
115 console.error('MiniMax TTS: Failed to read error response:', textError);
116 errorMessage = `HTTP ${apiResponse.status}: Unable to read error details`;
117 }
118 }
119
120 console.error('MiniMax TTS API request failed:', errorMessage);
121 return response.status(500).json({ error: errorMessage });
122 }
123
124 // Parse the response
125 /** @type {any} */
126 let responseData;
127 try {
128 responseData = await apiResponse.json();
129 console.debug('MiniMax TTS Response received');
130 } catch (jsonError) {
131 console.error('MiniMax TTS: Failed to parse response as JSON:', jsonError);
132 return response.status(500).json({ error: 'Invalid response format from MiniMax API' });
133 }
134
135 // Check for API error codes in response data
136 const baseResp = responseData?.base_resp;
137 if (baseResp && baseResp.status_code !== 0) {
138 let errorMessage;
139 if (baseResp.status_code === 1004) {
140 errorMessage = 'Authentication failed - Please check your API key and API host';
141 } else {
142 errorMessage = `API Error: ${baseResp.status_msg}`;
143 }
144 console.error('MiniMax TTS API error:', baseResp);
145 return response.status(500).json({ error: errorMessage });
146 }
147
148 // Process the audio data
149 if (responseData.data && responseData.data.audio) {
150 // Process hex-encoded audio data
151 const hexAudio = responseData.data.audio;
152
153 if (!hexAudio || typeof hexAudio !== 'string') {
154 console.error('MiniMax TTS: Invalid audio data format');
155 return response.status(500).json({ error: 'Invalid audio data format' });
156 }
157
158 // Remove possible prefix and spaces
159 const cleanHex = hexAudio.replace(/^0x/, '').replace(/\s/g, '');
160
161 // Validate hex string format
162 if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {
163 console.error('MiniMax TTS: Invalid hex string format');
164 return response.status(500).json({ error: 'Invalid audio data format' });
165 }
166
167 // Ensure hex string length is even
168 const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex;
169
170 try {
171 // Convert hex string to byte array
172 const hexMatches = paddedHex.match(/.{1,2}/g);
173 if (!hexMatches) {
174 console.error('MiniMax TTS: Failed to parse hex string');
175 return response.status(500).json({ error: 'Invalid hex string format' });
176 }
177 const audioBytes = new Uint8Array(hexMatches.map(byte => parseInt(byte, 16)));
178
179 if (audioBytes.length === 0) {
180 console.error('MiniMax TTS: Audio conversion resulted in empty array');
181 return response.status(500).json({ error: 'Audio data conversion failed' });
182 }
183
184 console.debug(`MiniMax TTS: Converted ${paddedHex.length} hex characters to ${audioBytes.length} bytes`);
185
186 // Set appropriate headers and send audio data
187 const mimeType = getAudioMimeType(format);
188 response.setHeader('Content-Type', mimeType);
189 response.setHeader('Content-Length', audioBytes.length);
190
191 return response.send(Buffer.from(audioBytes));
192
193 } catch (conversionError) {
194 console.error('MiniMax TTS: Audio conversion error:', conversionError);
195 return response.status(500).json({ error: `Audio data conversion failed: ${conversionError.message}` });
196 }
197 } else if (responseData.data && responseData.data.url) {
198 // Handle URL-based audio response
199 console.debug('MiniMax TTS: Received audio URL:', responseData.data.url);
200
201 try {
202 const audioResponse = await fetch(responseData.data.url);
203 if (!audioResponse.ok) {
204 console.error('MiniMax TTS: Failed to fetch audio from URL:', audioResponse.status);
205 return response.status(500).json({ error: `Failed to fetch audio from URL: ${audioResponse.status}` });
206 }
207
208 const audioBuffer = await audioResponse.arrayBuffer();
209 const mimeType = getAudioMimeType(format);
210
211 response.setHeader('Content-Type', mimeType);
212 response.setHeader('Content-Length', audioBuffer.byteLength);
213
214 return response.send(Buffer.from(audioBuffer));
215 } catch (urlError) {
216 console.error('MiniMax TTS: Error fetching audio from URL:', urlError);
217 return response.status(500).json({ error: `Failed to fetch audio: ${urlError.message}` });
218 }
219 } else {
220 // Handle error response
221 const errorMessage = responseData.base_resp?.status_msg || responseData.error?.message || 'Unknown error';
222 console.error('MiniMax TTS: No valid audio data in response:', responseData);
223 return response.status(500).json({ error: `API Error: ${errorMessage}` });
224 }
225
226 } catch (error) {
227 console.error('MiniMax TTS generation failed:', error);
228 return response.status(500).json({ error: 'Internal server error' });
229 }
230});
src/endpoints/secrets.js+2 -0
@@ -55,6 +55,8 @@ export const SECRET_KEYS = {
55 AIMLAPI: 'api_key_aimlapi',55 AIMLAPI: 'api_key_aimlapi',
56 XAI: 'api_key_xai',56 XAI: 'api_key_xai',
57 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',57 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
58 MINIMAX: 'api_key_minimax',
59 MINIMAX_GROUP_ID: 'minimax_group_id',
58};60};
5961
60/**62/**
src/server-startup.js+2 -0
@@ -45,6 +45,7 @@ import { router as koboldRouter } from './endpoints/backends/kobold.js';
45import { router as textCompletionsRouter } from './endpoints/backends/text-completions.js';45import { router as textCompletionsRouter } from './endpoints/backends/text-completions.js';
46import { router as speechRouter } from './endpoints/speech.js';46import { router as speechRouter } from './endpoints/speech.js';
47import { router as azureRouter } from './endpoints/azure.js';47import { router as azureRouter } from './endpoints/azure.js';
48import { router as minimaxRouter } from './endpoints/minimax.js';
48import { router as dataMaidRouter } from './endpoints/data-maid.js';49import { router as dataMaidRouter } from './endpoints/data-maid.js';
4950
50/**51/**
@@ -172,6 +173,7 @@ export function setupPrivateEndpoints(app) {
172 app.use('/api/backends/chat-completions', chatCompletionsRouter);173 app.use('/api/backends/chat-completions', chatCompletionsRouter);
173 app.use('/api/speech', speechRouter);174 app.use('/api/speech', speechRouter);
174 app.use('/api/azure', azureRouter);175 app.use('/api/azure', azureRouter);
176 app.use('/api/minimax', minimaxRouter);
175 app.use('/api/data-maid', dataMaidRouter);177 app.use('/api/data-maid', dataMaidRouter);
176}178}
177179