在上一篇中,我们基于浏览器原生的 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. 段落编辑改进
在插入 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 提供的角色与语气接口,动态加载用户可选的选项。
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…