使用Google Text-to-Speech处理超长SSML报错求助
解决方案
因为Google Text-to-Speech的标准synthesizeSpeech接口对SSML有5000字节的限制,且长音频合成客户端不支持SSML输入,唯一可行的方案是拆分SSML为符合长度要求的有效片段,分别生成音频后合并。
1. 正确拆分SSML
拆分时必须保证每个片段都是完整有效的SSML,不能破坏标签结构。最稳妥的方式是按SSML中的段落标签<p>拆分,或者按句子拆分后包裹完整的<speak>根标签。
示例拆分函数:
const fs = require('fs'); function splitSSML(ssmlContent, maxByteLimit = 4800) { // 留200字节冗余避免触发限制 // 移除原有的根<speak>标签 const innerContent = ssmlContent.replace(/^<speak>/, '').replace(/<\/speak>$/, ''); // 按<p>标签拆分段落 const paragraphs = innerContent.split(/(<p>.*?<\/p>)/s).filter(part => part.trim()); const chunks = []; let currentChunk = []; let currentByteLength = 0; for (const para of paragraphs) { const paraByteLength = Buffer.byteLength(para, 'utf8'); // 如果当前段落加入后超过限制,就把当前chunk存入,重置 if (currentByteLength + paraByteLength > maxByteLimit) { chunks.push(`<speak>${currentChunk.join('')}</speak>`); currentChunk = []; currentByteLength = 0; } currentChunk.push(para); currentByteLength += paraByteLength; } // 加入最后一个chunk if (currentChunk.length > 0) { chunks.push(`<speak>${currentChunk.join('')}</speak>`); } return chunks; }
2. 批量生成音频片段
遍历拆分后的SSML片段,逐个调用synthesizeSpeech接口,保存每个音频文件到本地:
const textToSpeech = require('@google-cloud/text-to-speech'); const path = require('path'); async function generateAudioChunks(ssmlChunks, outputDir, languageCode, ssmlGender) { const client = new textToSpeech.TextToSpeechClient(); const audioPaths = []; for (let i = 0; i < ssmlChunks.length; i++) { const request = { input: { ssml: ssmlChunks[i] }, voice: { languageCode, ssmlGender }, audioConfig: { audioEncoding: 'MP3' }, }; const [response] = await client.synthesizeSpeech(request); const chunkPath = path.join(outputDir, `chunk_${i}.mp3`); fs.writeFileSync(chunkPath, response.audioContent, 'binary'); audioPaths.push(chunkPath); console.log(`生成音频片段: ${chunkPath}`); } return audioPaths; }
3. 合并音频片段
使用ffmpeg工具合并所有MP3片段(需提前安装ffmpeg并确保其在系统PATH中):
const { execSync } = require('child_process'); function mergeAudioChunks(audioPaths, outputPath) { // 生成ffmpeg命令所需的音频列表文件 const listPath = path.join(path.dirname(outputPath), 'audio_list.txt'); const listContent = audioPaths.map(path => `file '${path}'`).join('\n'); fs.writeFileSync(listPath, listContent); // 执行ffmpeg合并命令 execSync(`ffmpeg -f concat -safe 0 -i ${listPath} -c copy ${outputPath}`); // 清理临时文件 fs.unlinkSync(listPath); audioPaths.forEach(path => fs.unlinkSync(path)); console.log(`合并完成: ${outputPath}`); }
完整调用示例
async function main() { const ssmlFilePath = './input.ssml'; const outputDir = './audio_chunks'; const finalOutputPath = './final_audio.mp3'; const languageCode = 'en-US'; const ssmlGender = 'NEUTRAL'; // 创建输出目录 if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } // 读取并拆分SSML const ssmlContent = fs.readFileSync(ssmlFilePath).toString(); const ssmlChunks = splitSSML(ssmlContent); // 生成音频片段 const audioPaths = await generateAudioChunks(ssmlChunks, outputDir, languageCode, ssmlGender); // 合并音频 mergeAudioChunks(audioPaths, finalOutputPath); } main().catch(err => console.error('出错:', err));
注意事项
- 拆分时建议留一定字节冗余(比如示例中的4800字节),避免因标签转义或编码问题触发限制。
- 如果SSML中包含
<audio>、<break>等特殊标签,拆分时要确保这些标签不会被拆分到不同片段中。 - 合并音频时,ffmpeg会保持原音频的编码和格式,确保合并后的音频无间隙。
内容的提问来源于stack exchange,提问作者Dharmist Mahato
相关产品推荐
相关产品推荐

