文章介绍了如何通过引入开源项目Speech-AI-Forge来优化TTS生成,增强语音编辑器的功能和用户体验。包括Speech-AI-Forge简介、安装与运行、语音编辑器的功能优化、前端功能优化及MultiAudioPlayer插件代码等。
Speech-AI-Forge是一个开源的TTS生成工具,支持自定义语音角色、语气风格、以及基于SSML的文本格式化。通过其强大的API接口,可以替代传统的Web Speech API,生成更高质量的音频资源。
扩展编辑器的SSML生成逻辑,使其与Speech-AI-Forge完美对接。通过调用Speech-AI-Forge的/v1/audio/speech接口,生成音频文件。
利用自定义的MultiAudioPlayer插件,实现背景音与段落内容的同步播放。通过传入生成的内容音频contentUrl和背景音轨bgInfo.url,用户可以实时试听内容。
包括动态音频管理、段落编辑改进、语音角色与语气选择、MultiAudioPlayer插件代码等。通过这些优化,提高了语音编辑器的效率和用户体验。
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 支持包括:
我们可以扩展编辑器的 SSML 生成逻辑,使其与 Speech-AI-Forge 完美对接。
优化后的 SSML 生成逻辑
getssml(data) { const ssml = []; const regex = /(
]
>.
?)/g; const parts = data.content.split(regex).filter(part => part.trim());
parts.forEach(item => { if (item.startsWith('
if (dataType === 'speed') {
ssml.push(``);
} else if (dataType === 'break') {
ssml.push(``);
}
} else {
ssml.push(item);
}
});
const roleId = data.roleId || ''; const toneId = data.toneId || ''; return
${ssml.join('')}
; }
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. 段落编辑改进
在插入
break
和
speed
时,精确调整插入位置:
// 停顿可按照用户鼠标位置插入 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; }