Java實現抓取在線視頻並提取視頻語音爲文本

一、 背景

最近在做大模型相關的項目,其中有個模塊需要提取在線視頻語音爲文本並輸出給用戶。作爲一個純後端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;
    }
    
}

四、 總結

經過一系列嘗試實踐,最終能夠在本地電腦實現抓取在線視頻並提取視頻語音爲文本。後續可以繼續研究相關插件在服務器上的使用以及對應功能塊的失敗重試等,保障轉換的質量。

反觀上文,代碼量以及流程並不多,但是在初次探索時也是充滿了坑點。總之呢,借鑑前人的經驗不斷積累才能打磨更好的工具。

作者:京東零售 王江波

來源:京東雲開發者社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章