利用 Speech-AI-Forge 优化语音编辑器的实现

229 阅读3分钟

image.png

在上一篇中,我们基于浏览器原生的 SpeechSynthesis API 构建了一个基础语音编辑器。本篇将通过引入开源项目 Speech-AI-Forge,实现对 TTS 生成的全面优化,增强语音编辑器的功能和用户体验。


Speech-AI-Forge 简介

Speech-AI-Forge 是一个开源的 TTS 生成工具,支持自定义语音角色、语气风格、以及基于 SSML 的文本格式化。通过其强大的 API 接口,我们可以轻松替代传统的 Web Speech API,生成更高质量的音频资源。

安装与运行

    brew install ffmpeg
    brew install rubberband
    pip install -r requirements.txt
    python launch.py


运行后可通过 http://localhost:7870/docs 查看 API 文档。

mac运行会报cpu错误,建议使用 Docker 部署

    docker-compose -f ./docker-compose.api.yml up -d

部署后,通过以下命令测试生成的音频:

    curl http://localhost:7870/v1/audio/speech \
      -H "Content-Type: application/json" \
      -d '{
        "model": "chattts",
        "input": "Today is a wonderful day to build something people love! [lbreak]",
        "voice": "female2",
        "style": "chat"
      }' \
      --output speech.mp3

语音编辑器的功能优化

1. SSML 支持扩展

Speech-AI-Forge 提供的 SSML 支持包括:

  • voice:定义角色及其语气风格。
  • prosody:控制语速、音高和音量。
  • break:插入暂停。

我们可以扩展编辑器的 SSML 生成逻辑,使其与 Speech-AI-Forge 完美对接。

优化后的 SSML 生成逻辑
getssml(data) {
  const ssml = [];
  const regex = /(<span[^>]*>.*?</span>)/g;
  const parts = data.content.split(regex).filter(part => part.trim());

  parts.forEach(item => {
    if (item.startsWith('<span')) {
      const parser = new DOMParser();
      const spanElement = parser.parseFromString(item, 'text/html').querySelector('span');
      const dataType = spanElement.getAttribute('data-type');
      const dataNum = spanElement.getAttribute('data-num');
      
      if (dataType === 'speed') {
        ssml.push(`<prosody rate="${dataNum}"></prosody>`);
      } else if (dataType === 'break') {
        ssml.push(`<break time="${dataNum}" />`);
      }
    } else {
      ssml.push(item);
    }
  });

  const roleId = data.roleId || '';
  const toneId = data.toneId || '';
  return `<voice spk="${roleId}" style="${toneId}">${ssml.join('')}</voice>`;
}

2. TTS 接口对接

通过调用 Speech-AI-Forge 的 /v1/audio/speech 接口,生成音频文件。

音频生成函数
async function generateAudio(ssml, model = 'chattts', voice = 'female2', style = 'chat') {
  const response = await fetch('http://localhost:7870/v1/audio/speech', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model,
      input: ssml,
      voice,
      style
    }),
  });

  const audioBlob = await response.blob();
  const audioUrl = URL.createObjectURL(audioBlob);
  return audioUrl;
}

3. 背景音与段落同步播放

利用自定义的 MultiAudioPlayer 插件,实现背景音与段落内容的同步播放。

改进后的播放逻辑
this.$multiAudioPlayer.play([contentUrl, bgInfo.url]);

通过传入生成的内容音频 contentUrl 和背景音轨 bgInfo.url,用户可以实时试听内容。


前端功能优化

1. 动态音频管理

将背景音选择与内容音频整合,通过新增一个工具方法管理生成与播放:

音频预览工具
async previewAudio(paragraph) {
  const ssml = this.getssml(paragraph);
  const contentUrl = await generateAudio(ssml);
  const backgroundUrl = paragraph.backgroundUrl || '';
  
  this.$multiAudioPlayer.play([contentUrl, backgroundUrl]);
}

2. 段落编辑改进

在插入 breakspeed 时,精确调整插入位置:

// 停顿可按照用户鼠标位置插入
addBreak(breakNum) {
  const range = this.rangeInfo.range;
  const styledElement = this.commonAddStyle(`停顿${breakNum}s`, 'break', breakNum * 1000);
  range.deleteContents();
  range.insertNode(styledElement);
}
// 倍速只能插在一段文本中最后位置
addSpeed(speed) {
  let refID = `editor_${this.rangeInfo.domInfo.id}`
  let curDom = this.$refs[refID][0]
  const spans = curDom.getElementsByTagName('span')
  let found = false
  for (const span of spans) {
    // 检查 data-type 属性是否为 'speed',存在的话更改显示值和num
    if (span.getAttribute('data-type') === 'speed') {
      found = true
      span.setAttribute('data-num', speed)
      const textSpan = span.querySelector('.text-content')
      textSpan.textContent = `倍速${speed}`
      break
    }
  }
  if (found) return
  const styledElement = this.commonAddStyle(`倍速${speed}`, 'speed', speed)
  curDom.appendChild(styledElement)
},

通过 range 获取光标位置,插入停顿标签,并确保文档结构不被破坏。

3. 语音角色与语气选择

通过调用 Speech-AI-Forge 提供的角色与语气接口,动态加载用户可选的选项。

接口.png

音色.png

语气.png

4. MultiAudioPlayer 插件代码

class MultiAudioPlayer {
  constructor() {
    this.audios = [];          // 存储多个 Audio 对象
    this.currentTime = 0;      // 当前播放时间
    this.duration = 0;         // 总时长(取最短音频)
    this.isPlaying = false;    // 播放状态
    this.updateProgress = null; // 播放进度的回调函数
    this.minDurationAudio = null; // 保存最短音频的引用
  }

  // 加载并播放多个音频
  play(urls) {
    this.audios = urls.map(url => {
      const audio = new Audio(url); // 创建 Audio 实例
      return audio;
    });

    // 确定最短的音频总时长
    this.audios.forEach(audio => {
      audio.addEventListener('loadedmetadata', () => {
        if (!this.duration || audio.duration < this.duration) {
          this.duration = audio.duration;
          this.minDurationAudio = audio; // 设置最短的音频
        }
      });
    });

    // 播放所有音频
    this.audios.forEach(audio => {
      audio.play();
    });
    this.isPlaying = true;

    // 更新播放进度并同步
    this.audios.forEach(audio => {
      audio.addEventListener('timeupdate', () => {
        this.currentTime = audio.currentTime;
        if (this.updateProgress) {
          this.updateProgress(this.currentTime, this.duration);
        }
      });
    });

    // 当最短的音频结束时,停止所有音频
    this.minDurationAudio.addEventListener('ended', () => {
      this.stopAll();
    });
  }

  // 停止所有音频的播放
  stopAll() {
    this.audios.forEach(audio => {
      audio.pause();
      audio.currentTime = 0;
    });
    this.isPlaying = false;
    this.currentTime = 0;
  }

  // 暂停所有音频
  pause() {
    this.audios.forEach(audio => {
      audio.pause();
    });
    this.isPlaying = false;
  }

  // 设置播放进度
  setCurrentTime(time) {
    this.audios.forEach(audio => {
      audio.currentTime = time;
    });
  }

  // 设置音量(0 到 1)
  setVolume(volume) {
    this.audios.forEach(audio => {
      audio.volume = volume;
    });
  }

  // 静音或取消静音
  toggleMute() {
    this.audios.forEach(audio => {
      audio.muted = !audio.muted;
    });
  }

  // 获取当前播放时间和总时长
  getProgress() {
    return {
      currentTime: this.currentTime,
      duration: this.duration,
    };
  }

  // 设置进度回调
  onProgress(callback) {
    this.updateProgress = callback;
  }

  // 在组件销毁时移除事件监听
  destroy() {
    this.audios.forEach(audio => {
      audio.removeEventListener('timeupdate', this.updateProgress);
    });
  }
}

// 安装插件
const MultiAudioPlayerPlugin = {
  install(Vue) {
    Vue.config.globalProperties.$multiAudioPlayer = new MultiAudioPlayer();
  }
};

export default MultiAudioPlayerPlugin;

总结

通过引入 Speech-AI-Forge 和优化现有架构,我们成功构建了一个功能强大、体验优良的语音编辑器。无论是段落编辑、语音生成,还是背景音管理,这一工具都具备了高效性与易用性,能够很好地满足各种文本到语音的应用需求。

参考文献

Speech-AI-Forge:github.com/lenML/Speec…

原文链接:juejin.cn/post/744185…