Java实现抓取在线视频并提取视频语音为文本

京东云开发者
• 阅读 613

一、 背景

最近在做大模型相关的项目,其中有个模块需要提取在线视频语音为文本并输出给用户。作为一个纯后端Jave工程师,搞这个确实是初次尝试。

二、 调研

基于上述功能模块,主要有三大任务:1、 提取网页中的视频 2、 视频转语音 3、 语音转文本

首先是第一项:尝试了jsoup,webmagic等工具,最终还得是 selenium(也是各种踩坑)才实现了想要的效果。

第二项:这个探索是相当费劲,首选开源库 FFmpeg,但是命令行安装一直失败。因此转向其他方案,尝试了 Xuggler、JAVE、JAVE2、JavaCV 等均以失败告终。最终决定还是用 FFmpeg 吧。经过不懈努力,终于是安装好了,直接官网下载本地解压即可。

第三项:团队大哥提供了一个技术方案: https://www.**funasr**.com。虽说是现成的方案但是实践起来也是费了一把力。

经过上述三步,理论上来说,整体流程总算是可以调通了。但是实际运行起来却不那么顺利,如:长视频转语音超时、语音转文本超时等等。但是经过不懈努力呢,总算是搞定了上述一系列问题,实现了想要的效果。具体实践方案如下:

三、 实践

1、 提取网页中的视频

a. 下载插件 chromedriver

建议从网页下载,需要与chrome浏览器版本适配,不然运行不起来。下载地址: https://chromedriver.storage.googleapis.com/index.html

b. 导入selenium的jar包

<dependency>

<groupId>org.seleniumhq.selenium</groupId>

<artifactId>selenium-java</artifactId>

<version>3.1.0</version>

</dependency>

c. 话不多说,直接上🐎:

    /**
     * 从指定网址获取主视频链接
     *
     * @param targetUrl 目标网址
     * @return 主视频链接,如果未找到则返回null
     */
    public static String catchMainVideo(String targetUrl) {
        // 加载驱动,后面的路径自己要选择正确,也可以放在本地
        System.setProperty("webdriver.chrome.driver", "xxx/driver/chromedriver");
        // ChromeOptions 可以注释 这里是阻止浏览器的打开
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        options.addArguments("--disable-gpu");

        // 初始化一个谷歌浏览器实例,实例名称叫driver
        WebDriver driver = new ChromeDriver(options);

        // get()打开一个站点
        driver.get(targetUrl);

        // 等待页面加载
        try {
            Thread.sleep(100);
        } catch (Exception e) {
            return null;
        }

        JavascriptExecutor js = CastUtil.convert(driver);

        List<WebElement> elements = CastUtil.convert(js.executeScript("return document.querySelectorAll('.sgVideoWrapper video source')"));

        // 处理返回的WebElement列表
        for (WebElement element : elements) {
            // 你可以获取元素的属性,例如src
            if ("video/mp4".equals(element.getAttribute("type"))) {
                return element.getAttribute("src");
            }
        }

        return null;
    }

2、 视频转语音

a. 先下载 ffmpeg,建议也是网页下载,命令行下载失败了n次,升级xcode也不好使。最后还是从网页success:https://ffmpeg.org/download.html

b. 话不多说,直接上🐎

这里初次转换的时候打视频转语音没问题,但是在后续的语音转文本流程超时失败,所以最终决定视频转语音分段。

    /**
     * 将视频分割为音频文件
     *
     * @param inputVideoPath       输入视频文件的路径
     * @param outputAudioPrefix    输出音频文件的前缀
     * @param segmentSizeInSeconds 分段大小(以秒为单位)
     */
    public static void video2audio(String inputVideoPath, String outputAudioPrefix, int segmentSizeInSeconds) {
        try {
            ProcessBuilder pb = new ProcessBuilder("xxx/ffmpeg", "-i", inputVideoPath, "-vn", "-c:a", "copy", "-f", "segment", "-segment_time", String.valueOf(segmentSizeInSeconds), outputAudioPrefix + "%03d.aac");
            pb.inheritIO();
            Process process = pb.start();
            process.waitFor();
            log.info("Audio splitting completed.");
        } catch (Exception e) {
            log.error("video2audio error", e);
        }
    }

3、 语音转文本

本部分实现参考了funasr,拿到离线代码之后解读简化,最后得到如下🐎,其中用到的wss地址需要自行部署,详见文档:

import com.google.common.collect.Maps;
import com.jd.store.common.util.JsonUtil;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.compress.utils.Lists;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class FunasrWsClient extends WebSocketClient {

    private static final Logger log = LoggerFactory.getLogger(FunasrWsClient.class);

    String fileName;

    private String fileContent;

    public String getFileContent() {
        return fileContent;
    }

    public void setFileContent(String fileContent) {
        this.fileContent = fileContent;
    }

    public FunasrWsClient(URI serverURI, String fileName) {
        super(serverURI);
        this.fileName = fileName;
    }

    public void sendJson(String mode, String strChunkSize, int chunkInterval, String wavName, boolean isSpeaking, String suffix) {
        try {
            Map<String, Object> obj = Maps.newHashMap();
            obj.put("mode", mode);

            String[] chunkList = strChunkSize.split(",");
            List<Integer> array = Lists.newArrayList();
            for (String s : chunkList) {
                array.add(Integer.parseInt(s.trim()));
            }

            obj.put("chunk_size", array);
            obj.put("chunk_interval", chunkInterval);
            obj.put("wav_name", wavName);

//            if (FunasrWsClient.hotwords.trim().length() > 0) {
//                String regex = "\d+";
//                JSONObject jsonitems = new JSONObject();
//                String[] items = FunasrWsClient.hotwords.trim().split(" ");
//                Pattern pattern = Pattern.compile(regex);
//                StringBuilder tmpWords = new StringBuilder();
//                for (String item : items) {
//                    Matcher matcher = pattern.matcher(item);
//                    if (matcher.matches()) {
//                        jsonitems.put(tmpWords.toString().trim(), item.trim());
//                        tmpWords = new StringBuilder();
//                        continue;
//                    }
//                    tmpWords.append(item).append(" ");
//                }
//                obj.put("hotwords", jsonitems.toString());
//            }

//            if (suffix.equals("wav")) {
//                suffix = "mp3";
//            }
            obj.put("wav_format", suffix);
            if (isSpeaking) {
                obj.put("is_speaking", Boolean.TRUE);
            } else {
                obj.put("is_speaking", Boolean.FALSE);
            }
            log.info("sendJson: " + JsonUtil.toJsonString(obj));
            send(JsonUtil.toJsonString(obj));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendEof() {
        try {
            Map<String, Object> obj = Maps.newHashMap();

            obj.put("is_speaking", Boolean.FALSE);

            log.info("sendEof: " + JsonUtil.toJsonString(obj));
            send(JsonUtil.toJsonString(obj));
        } catch (Exception e) {
            log.error("sendEof", e);
        }
    }

    public void recWav() {
        String suffix = fileName.split("\.")[fileName.split("\.").length - 1];
        sendJson(mode, strChunkSize, chunkInterval, fileName, true, suffix);
        File file = new File(fileName);

        int chunkSize = sendChunkSize;
        byte[] bytes = new byte[chunkSize];

        int readSize;
        try (FileInputStream fis = new FileInputStream(file)) {
            if (fileName.endsWith(".wav")) {
                fis.read(bytes, 0, 44);
            }
            readSize = fis.read(bytes, 0, chunkSize);
            while (readSize > 0) {
                // send when it is chunk size
                if (readSize == chunkSize) {
                    send(bytes);
                } else {
                    // send when at last or not is chunk size
                    byte[] tmpBytes = new byte[readSize];
                    System.arraycopy(bytes, 0, tmpBytes, 0, readSize);
                    send(tmpBytes);
                }
                if (!mode.equals("offline")) {
                    Thread.sleep(chunkSize / 32);
                }

                readSize = fis.read(bytes, 0, chunkSize);
            }

//            if (!mode.equals("offline")) {
//                // if not offline, we send eof and wait for 3 seconds to close
//                Thread.sleep(2000);
//                sendEof();
//                Thread.sleep(3000);
//                close();
//            }
//
//            else {
            // if offline, just send eof
            sendEof();
//            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onOpen(ServerHandshake handshake) {
        this.recWav();
    }

    @Override
    public void onMessage(String message) {
        log.info("received: " + message);

        Map<String, Object> jsonObject = JsonUtil.parseMap(message);
        if (MapUtils.isEmpty(jsonObject)) {
            return;
        }
        log.info("text: " + jsonObject.get("text"));

        // 回传文件内容
        fileContent = jsonObject.get("text").toString();

        close();
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {

    }

    @Override
    public void onError(Exception e) {
        log.error("onError ", e);
    }

    static String mode = "online";
    static String strChunkSize = "5,10,5";
    static int chunkInterval = 10;
    static int sendChunkSize = 1920;

    public static String execute(String fileName) {
        try {
            String wsAddress = "wss://xxx";

            FunasrWsClient c = new FunasrWsClient(new URI(wsAddress), fileName);

            c.connect();

            TimeUnit.SECONDS.sleep(5);
            return c.fileContent;
        } catch (Exception e) {
            log.error("execute error", e);
        }
        return null;
    }

}

四、 总结

经过一系列尝试实践,最终能够在本地电脑实现抓取在线视频并提取视频语音为文本。后续可以继续研究相关插件在服务器上的使用以及对应功能块的失败重试等,保障转换的质量。

反观上文,代码量以及流程并不多,但是在初次探索时也是充满了坑点。总之呢,借鉴前人的经验不断积累才能打磨更好的工具。

作者:京东零售 王江波

来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
Android VideoView播放在线视频(2)
AndroidVideoView播放在线视频(2)附录参考文章1简单介绍了如何使用AndroidVideoView播放本地视频,AndroidVideoView也可以直接播放在线视频资源,首先和文章1一样,需要在布局文件中写一个AndroidVideoView,然后在java代码中播放:packagecom.exa
四儿 四儿
1年前
语音合成数据的重要性:训练高质量语音合成模型的关键
语音合成是一种将文本转换为语音的技术,它在智能客服、智能助手、语音广告等多个领域有着广泛的应用。而要实现高质量的语音合成,就需要大量的语音合成数据。语音合成数据是指包含语音信号和文本标注的数据,它是训练语音合成模型的关键之一。通常,语音合成数据需要包含大量
四儿 四儿
1年前
语音合成数据为智能化语音应用赋能
在数字化时代,语音技术的应用越来越广泛,语音合成作为其中的重要组成部分,为智能化语音应用提供了强有力的支持。语音合成技术可以将文本转化为自然流畅的人工语音,使得机器能够通过语音与人进行交互,为用户带来更便捷和愉悦的体验。而要实现高质量的语音合成,数据定制服
四儿 四儿
1年前
语音合成技术的简介与未来发展
语音合成是一种通过计算机技术生成人类可听的语音的技术。它将文本转换为语音,从而实现语音交互。本文将介绍语音合成技术的概念、现状以及未来的发展趋势。一、语音合成技术的概念和分类语音合成是通过计算机技术生成人类可听的语音,从而实现语音交互的过程。根据合成方式的
流浪剑客 流浪剑客
1年前
Mac端文本识别工具:TextSniper for Mac激活版 支持M1
是一款易于使用的桌面OCR应用程序,它可以帮助用户快速提取和识别Mac屏幕上不可搜索和不可编辑的文本。这款工具的使用非常直观,不论是图像、扫描的纸质文档、PDF还是视频中的文本,都可以轻松地从其中提取文本。作为一项额外功能,TextSniper可以将OCR
四儿 四儿
1年前
语音识别技术:现状、挑战与未来发展
一、引言语音识别技术是一种将人类语音转化为计算机可读文本的技术,它在许多领域都有广泛的应用,如智能助手、智能家居、医疗诊断等。本文将探讨语音识别技术的现状、挑战和未来发展。二、语音识别技术的现状1.深度学习驱动的语音识别:深度学习已经在语音识别领域取得了显
四儿 四儿
1年前
语音识别技术:端到端的挑战与解决方案
一、引言随着人工智能技术的不断发展,语音识别技术得到了越来越广泛的应用。端到端语音识别技术是近年来备受关注的一种新型语音识别技术,它能够直接将语音转换成文本,省略了传统的语音特征提取步骤。本文将探讨端到端语音识别技术的挑战与解决方案。二、端到端语音识别技术
四儿 四儿
11个月前
情感语音识别:技术发展与未来趋势
一、引言情感语音识别是近年来人工智能领域的研究热点,它通过分析人类语音中的情感信息,实现更加智能化和个性化的人机交互。本文将探讨情感语音识别技术的技术发展与未来趋势。二、情感语音识别技术的技术发展特征提取技术:特征提取是情感语音识别的关键步骤之一。目前,基
四儿 四儿
11个月前
情感语音识别:技术发展与挑战
一、引言情感语音识别是人工智能领域的重要研究方向,它通过分析人类语音中的情感信息,实现人机之间的情感交互。本文将探讨情感语音识别技术的发展历程和面临的挑战。二、情感语音识别技术的发展早期研究:情感语音识别的早期研究主要集中在特征提取和情感词典的构建上。研究