| 1 | import fetch from 'node-fetch'; |
| 2 | import { Router } from 'express'; |
| 3 | |
| 4 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 5 | |
| 6 | export const router = Router(); |
| 7 | |
| 8 | |
| 9 | router.post('/generate-voice', async (req, res) => { |
| 10 | try { |
| 11 | let provider_endpoint = req.body.provider_endpoint; |
| 12 | if (!provider_endpoint) { |
| 13 | console.warn('Volcengine endpoint not set, use default endpoint instead'); |
| 14 | provider_endpoint = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional'; |
| 15 | } |
| 16 | |
| 17 | const appId = readSecret(req.user.directories, SECRET_KEYS.VOLCENGINE_APP_ID); |
| 18 | const accessKey = readSecret(req.user.directories, SECRET_KEYS.VOLCENGINE_ACCESS_KEY); |
| 19 | |
| 20 | if (!appId || !accessKey) { |
| 21 | console.warn('Volcengine generate-voice request missing required parameters appId or accessKey'); |
| 22 | return res.sendStatus(403); |
| 23 | } |
| 24 | |
| 25 | const resourceId = req.body.resource_id; |
| 26 | const text = req.body.text; |
| 27 | const voice_speaker = req.body.voice_speaker; |
| 28 | |
| 29 | if (!resourceId || !text || !voice_speaker) { |
| 30 | console.warn('Volcengine generate-voice request missing required parameters resourceId or text or voice_speaker'); |
| 31 | return res.sendStatus(400); |
| 32 | } |
| 33 | |
| 34 | const response = await fetch(provider_endpoint, { |
| 35 | method: 'POST', |
| 36 | headers: { |
| 37 | 'X-Api-App-Id': appId || '', |
| 38 | 'X-Api-Access-Key': accessKey || '', |
| 39 | 'X-Api-Resource-Id': resourceId || '', |
| 40 | 'Content-Type': 'application/json', |
| 41 | }, |
| 42 | body: JSON.stringify({ |
| 43 | 'req_params': { |
| 44 | 'text': text, |
| 45 | 'speaker': voice_speaker, |
| 46 | 'audio_params': { |
| 47 | 'format': 'mp3', |
| 48 | 'speech_rate': Number.parseInt(req.body.speed || '0'), |
| 49 | }, |
| 50 | 'additions': JSON.stringify({ |
| 51 | 'mute_cut_threshold': '400', |
| 52 | 'mute_cut_remain_ms': '1', |
| 53 | 'explicit_language': 'crosslingual', |
| 54 | 'enable_language_detector': true, |
| 55 | 'disable_markdown_filter': true, |
| 56 | 'cache_config': { |
| 57 | 'use_cache': true, |
| 58 | 'text_type': 1, |
| 59 | }, |
| 60 | }), |
| 61 | }, |
| 62 | }), |
| 63 | }); |
| 64 | |
| 65 | if (!response.ok) { |
| 66 | const logid = response.headers.get('X-Tt-Logid') || ''; |
| 67 | console.warn('Volcengine Request failed', response.status, response.statusText, logid); |
| 68 | return res.header('X-Tt-Logid', logid).status(500).send(`TTS Generation Failed: ${response.statusText}`); |
| 69 | } |
| 70 | const decoder = new TextDecoder(); |
| 71 | |
| 72 | const result = await new Promise((resolve, reject) => { |
| 73 | let audioChunks_ = []; |
| 74 | let buffer = ''; |
| 75 | if (!response.body) { |
| 76 | reject(new Error('Response body is null')); |
| 77 | return; |
| 78 | } |
| 79 | response.body.on('data', (chunk) => { |
| 80 | buffer += decoder.decode(chunk, { stream: true }); |
| 81 | |
| 82 | const lines = buffer.split('\n'); |
| 83 | buffer = lines.pop() || ''; |
| 84 | |
| 85 | for (const line of lines) { |
| 86 | if (!line.trim()) continue; |
| 87 | |
| 88 | try { |
| 89 | const { data, code, message } = JSON.parse(line); |
| 90 | if (code !== 0 && code !== 20000000) { |
| 91 | reject(`Volcengine TTS stream line code ${code}, ${message}`); |
| 92 | return; |
| 93 | } |
| 94 | if (data) { |
| 95 | const audioData = Buffer.from(data, 'base64'); |
| 96 | audioChunks_.push(audioData); |
| 97 | } |
| 98 | } catch (e) { |
| 99 | console.error('Error parsing Volcengine TTS stream line:', e); |
| 100 | } |
| 101 | } |
| 102 | }); |
| 103 | |
| 104 | response.body.on('end', () => { |
| 105 | if (buffer.trim()) { |
| 106 | try { |
| 107 | const { code, data, message } = JSON.parse(buffer); |
| 108 | if (code !== 0 && code !== 20000000) { |
| 109 | reject(`Volcengine TTS stream line code ${code}, ${message}`); |
| 110 | return; |
| 111 | } |
| 112 | if (data) { |
| 113 | const audioData = Buffer.from(data, 'base64'); |
| 114 | audioChunks_.push(audioData); |
| 115 | } |
| 116 | } catch (e) { |
| 117 | reject(`Error parsing final Volcengine TTS stream line: ${e}`); |
| 118 | } |
| 119 | } |
| 120 | resolve(audioChunks_); |
| 121 | }); |
| 122 | |
| 123 | response.body.on('error', (error) => { |
| 124 | reject(`Error reading Volcengine TTS stream: ${error}`); |
| 125 | }); |
| 126 | }); |
| 127 | |
| 128 | const finalAudioData = Buffer.concat(result); |
| 129 | |
| 130 | res.set('Content-Type', 'audio/mpeg'); |
| 131 | res.status(200).send(finalAudioData); |
| 132 | } catch (error) { |
| 133 | console.error('Volcengine generate-voice fetch failed', error); |
| 134 | res.status(500).send(`TTS Generation Failed: ${error}`); |
| 135 | } |
| 136 | }); |