Blame Raw
· · · 136 lines (5.3 KB)
0 contributors
1import fetch from 'node-fetch';
2import { Router } from 'express';
3
4import { readSecret, SECRET_KEYS } from './secrets.js';
5
6export const router = Router();
7
8
9router.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});