React 前端导航

小程序实现音频字幕方案分享

实现思路:生成带有时间的字幕文件,然后监听小程序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]
            })
        })

声明:本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。邮箱:farmerlzj@163.com。 本站原创内容未经允许不得转载,或转载时需注明出处: 内容转载自: React前端网:https://qianduan.shop/blogs/detail/191
想做或者在做副业的朋友欢迎加微信交流:farmerlzj,公众号:生财空间站。

#小程序#移动端#java

相关推荐

跨端开发框架 Rax 中的路由跳转

简单减少 Rax 中提供的各种路由跳转方式,以及推荐路由的跳转方式

rax 框架开发钉钉小程序实践问题

记录使用 rax 开发钉钉小程序遇到的问题及解决方案