实现思路:生成带有时间的字幕文件,然后监听小程序audio组件的播放进度,根据当前播放时间取对应的字幕内容并展示。
需求
关于语音部分主要有两块功能:
1、录入文字,讲文字转换为想要的语音音频文件.mp3
2、小程序audio组建实现音频播放
现在想要在音频播放的同时,可以实现字幕的同步播放显示,其实很好理解,类似音乐播放器的字幕同步效果:
之所以想要分享下实现方式,是因为如果在web内核浏览器下基于js来做非常简单,但是想要在微信小程序内实现这个效果,有点麻烦,至少我调研了一番一周发现是这样子的,所以这片文章主要想分享下我的实现方案,肯定不是最优的,大家参考即可。
实现思路:生成带有时间的字幕文件,然后监听小程序audio组件的播放进度,根据当前播放时间取对应的字幕内容并展示。
一、利用EdgeTTS在生成语音文件同时生成VTT字幕文件
关于edgetts的使用,以及生成音频文件,可以看下我的上篇文章:
edge-tts微软文本转语音库,快速搭建自己的TTS文本转语音服务
在上篇文章中,我给的python脚本示例,只是将文字转换为mp3音频文件,这里我们同时需要生成vtt文件,所以tts.py修改如下:
import uvicorn
import edge_tts
import asyncio
from fastapi import FastAPI, Request, Body
app = FastAPI()
@app.post("/api/tts")
async def post_data(text: str = Body('', title = '文本', embed = True),
voice: str = Body('', title = '语言参数', embed = True),
name: str = Body('', title = '文件名', embed = True),
rate: str = Body('', title = '文件名', embed = True),
volume: str = Body('', title = '文件名', embed = True)):
mp3File = "/home/work/tts/result/" + name + ".mp3"
vttFile = "/home/work/tts/result/" + name + ".vtt"
communicate = edge_tts.Communicate(text = text, voice = voice, rate = rate, volume=volume)
submaker = edge_tts.SubMaker()
with open(mp3File, "wb") as file:
async for chunk in communicate.stream():
if chunk["type"] == "audio":
file.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
submaker.create_sub((chunk["offset"], chunk["duration"]), chunk["text"])
with open(vttFile, "w", encoding="utf-8") as file:
file.write(submaker.generate_subs())
return {"message": mp3File, "mp3": mp3File, "vtt": vttFile}
if __name__ == '__main__':
uvicorn.run('tts:app', host = '0.0.0.0', post = 8080)
通过这个脚本文件我们可以拿到mp3File和vttFile两个路径下的mp3音频文件和vtt字幕文件。
二、在java中对音频文件进行切割处理
字幕文件原始内容长这个样子:
WEBVTT
00:00:00.102 --> 00:00:03.670
因为 我 这边 是 一个 自定义 的 弹窗 组件 需要
00:00:03.696 --> 00:00:07.329
弹窗 加载 出来 后 再 播放 所以 在 Component 中
00:00:07.329 --> 00:00:10.050
的 methods 中 定义 播放 方法
这个原始文件我们在js中是可以直接使用的,但是在微信小程序没发直接使用,因此需要先加文件内容加载到内存,读文字幕文件的文本内容进行处理,这里我是把它处理成了一个Hash结构,hash key为当前这行字幕开始时间所在的秒,value为字幕内容,代码如下:
public Map<Integer, String> ttv2Map(String vttBosUrl) {
Map<Integer, String> vttMap = new LinkedHashMap<>();
URL url;
HttpURLConnection conn = null;
InputStream is = null;
try {
url = new URL(vttBosUrl);
conn = (HttpURLConnection) url.openConnection();
// 设置Http请求头为GET
conn.setRequestMethod("GET");
// 设置超时响应时间为5s
conn.setConnectTimeout(10 * 1000);
// 通过输入流获取图片资源
is = conn.getInputStream();
byte data[] = new byte[1024]; // 开辟1K的空间进行读取
int len = 0 ;// 保存数据读取的个数
StringBuffer buffer = new StringBuffer();
do {
len = is.read(data); // 读取数据到字节数组并返回读取个数
if (len != -1) {
buffer.append(new String(data, 0, len)); // 每次读取到的内容保存在缓冲流中
}
} while (len != -1) ; // 没有读取到底
String ttvText = buffer.toString().replaceAll("WEBVTT", "").replaceAll(" ", "").replaceAll("\\r\\n", "MIAOMIAO");
String[] arr = ttvText.split("MIAOMIAO");
// 这里将vtt内容转换为小程序待会可以用到的hash结构。
for (int i = 2; i < arr.length; i+=3) {
int hh = Integer.valueOf(arr[i].substring(0, 2));
int mm = Integer.valueOf(arr[i].substring(3, 5));
int ss = Integer.valueOf(arr[i].substring(6, 8));
vttMap.put(hh * 3600 + mm * 60 + ss, arr[i+1]);
}
return vttMap;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return vttMap;
}
参数我这里是因为将生成的原始文件上传到了OSS存储,所以先通过网络请求的方式下载并将内容加载到内容,如果你的vtt文件在本地,可以通过流的方式直接读取本地文件,通过这个方法的转换,可以拿到一个map, 转为json后如下:
{
0: "因为 我 这边 是 一个 自定义 的 弹窗 组件 需要",
3: "弹窗 加载 出来 后 再 播放 所以 在 Component 中",
7: "的 methods 中 定义 播放 方法"
}
三、小程序中监听audio组件的播放进度,并读取对应时间的字幕
data定义:
其中currVTT为当前播放字幕文字内容:
data: {
currVTT: "", // 当前播放字幕,
h5Data: {
photo: '',
text: '',
audio: '',
title: '',
audioVTT: {} // java程序转换vtt后的hash
}
监听audio组件播放进度,并按当前播放的秒,也就是hash的key读取hash的value值,这样就实现了字幕的同步播放(基础功能哈):
this.innerAudioContext.onTimeUpdate(() => {
var vttIndex = parseInt(this.innerAudioContext.currentTime);
console.log('进度更新了总进度为:' + this.innerAudioContext.duration + '当前进度为:' + this.innerAudioContext.currentTime+'字幕key:' + vttIndex);
this.setData({
currVTT: this.data.h5Data.audioVTT[vttIndex]
})
})