You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:03:00