Blame Raw
Cohee · e3f41666 · · 228 lines (9.4 KB)
1 contributor
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 } catch (conversionError) {
193 console.error('MiniMax TTS: Audio conversion error:', conversionError);
194 return response.status(500).json({ error: `Audio data conversion failed: ${conversionError.message}` });
195 }
196 } else if (responseData.data && responseData.data.url) {
197 // Handle URL-based audio response
198 console.debug('MiniMax TTS: Received audio URL:', responseData.data.url);
199
200 try {
201 const audioResponse = await fetch(responseData.data.url);
202 if (!audioResponse.ok) {
203 console.error('MiniMax TTS: Failed to fetch audio from URL:', audioResponse.status);
204 return response.status(500).json({ error: `Failed to fetch audio from URL: ${audioResponse.status}` });
205 }
206
207 const audioBuffer = await audioResponse.arrayBuffer();
208 const mimeType = getAudioMimeType(format);
209
210 response.setHeader('Content-Type', mimeType);
211 response.setHeader('Content-Length', audioBuffer.byteLength);
212
213 return response.send(Buffer.from(audioBuffer));
214 } catch (urlError) {
215 console.error('MiniMax TTS: Error fetching audio from URL:', urlError);
216 return response.status(500).json({ error: `Failed to fetch audio: ${urlError.message}` });
217 }
218 } else {
219 // Handle error response
220 const errorMessage = responseData.base_resp?.status_msg || responseData.error?.message || 'Unknown error';
221 console.error('MiniMax TTS: No valid audio data in response:', responseData);
222 return response.status(500).json({ error: `API Error: ${errorMessage}` });
223 }
224 } catch (error) {
225 console.error('MiniMax TTS generation failed:', error);
226 return response.status(500).json({ error: 'Internal server error' });
227 }
228});