視訊通訊近實時生成字幕專案實踐

2023-03-19 21:00:34

1、概述

這兩天做了一個視訊通訊近實時字幕生成工具,前端通過瀏覽器開啟攝像頭,生成使用者畫面,根據使用者的語音近實時自動生成字幕展示在畫面下方。對於沒有接觸過音視訊處理的我來說,剛開始還是有點懵的,雖然藉助了 chatgpt,但是還是走了一段時間的彎路。不過花了大概一天時間還是比較完美的實現了,還是非常有成就感的。謹以此記錄最終成功的版本的實現思路和實現過程,文末附帶原始碼和原始碼啟動過程。

2、環境準備

第四節「詳細過程」中會有這些工具安裝或者申請教學

ffmpeg,一個強大的視訊處理工具,此次主要用它來實現視訊轉成音訊。

阿里雲 OSS bucket

阿里雲 語音識別專案

本地 golang 執行環境

3、實現思路

  • 前端使用 WebRTC 調起攝像頭,與後端建立 websocket 連線,每隔三秒傳送一段視訊二進位制流到後端;

  • 後端將視訊流儲存到本地,使用 ffmpeg 將本地視訊轉換成音訊;

  • 把音訊上傳到阿里雲 OSS 物件儲存伺服器中;

  • 獲取到音訊的存取地址;呼叫阿里雲的語音識別功能的 sdk 解析出音訊對應的文字內容;

  • 後端通過 websocket 把文字內容回傳給前端,前端進行字幕展示。

3.1 提示

謹以此提示來降低心理壓力,看起來此專案設計到前後端專案的開發和部署,但是其實不對此工具不用產生太大的壓力,因為很多操作都有現成工具可以借用。

雖然此次專案需要同時開發前後端,但是對於此次工具的開發,不需要把前端部署到伺服器,只需編寫一個簡單的 html,用瀏覽器渲染開啟即可。

chatpgt 可以一定程度上加快我們的問題解決過程,但是也不要全信它的內容,親身經歷被它坑了好多次。

github 上已有一些優秀的開源專案,比如此次所借用的開源專案wxbool/video-srt ,大大加快了專案的開發速度。

前後端 websocket 互動的實現也比較簡單,幾行程式碼就可以搞定。

4、詳細過程

4.1 工具準備和安裝

4.1.1 安裝 ffmpeg

在 Mac 上安裝方式是 brew install ffmpeg(其他作業系統可以自行尋找安裝教學),安裝過程可能比較久,我安裝了大概 40 分鐘。

安裝完畢執行ffmpeg -version ,輸出如下資訊說明安裝成功。

4.1.2 建立阿里雲的 RPM 使用者

登入阿里雲賬號後,存取 https://ram.console.aliyun.com/users,建立使用者

隨後在進入使用者首頁,點選「建立 AcessKey」,身份驗證通過後,會建立一個 RAM使用者的 AcessKeyAccessKey Secret,立刻把兩個引數記錄下來,因為這個 AccessKey Secret 只在建立時顯示,後續不支援檢視。

4.1.3 建立阿里雲 OSS bucket

存取 OSS物件儲存,點選立即開通,然後建立 bucket ,由於後續語音識別會存取 bucket 中的檔案,而語音識別只能存取到公開的資源,所以還需要設定 bucket 的開放範圍為「公開」

給 RPM 使用者新增完全控制許可權,否則後面執行程式碼時 oss 會報錯 StatusCode=403, ErrorCode=AccessDenied, ErrorMessage="You have no right to access this object because of bucket acl.",

4.1.4 建立阿里雲智慧語音互動專案

存取 錄音檔案識別,點選立即開通,然後建立專案,獲取到專案AppKey,記錄下來。

4.1.5 golang 環境安裝

wget "https://studygolang.com/dl/golang/go1.18.3.darwin-amd64.tar.gz" -O go.tar.gz
tar -C /usr/local -xfz go.tar.gz
sudo echo 'export GOROOT=/usr/local/go' >> ~/.zshrc
sudo echo 'export GOPATH=~/go' >> ~/.zshrc
sudo echo 'export PATH=$GOPATH/bin:$GOROOT/bin:$PATH' >> ~/.zshrc
source ~/.zshrc

執行 go version 輸出版本資訊說明安裝成功

4.2 前端實現

只有一個 html 頁面,通過 websocket 跟後端建立連線,進行資料互動,包含一些必要的 dom 節點,以及三個按鈕。

javascript 指令碼包含四部分,

  • 第一部分是使用 navigator.mediaDevices.getUserMedia 開啟使用者的媒體裝置,這個工具函數底層是通過 WebRTC 來實現的,隨後跟後端建立 websocket 連線,使用 ws.onmessage 將獲取到的後端訊息新增上 dom 節點裡
  • 後三部分分別是三個按鈕所繫結的函數,
    • startGenerageSubtitle() ,繫結「啟動字幕生成」按鈕,功能是啟動字幕的生成,函數內部會啟動定時器,以三秒為週期,記錄使用者媒體的視訊流,通過 websocket 物件傳送到後端
    • stopGenerageSubtitle(),繫結「停止生成字幕」按鈕,功能是停止生成字幕,刪除定時器,終止視訊流的記錄和傳送。
    • clearGenerageSubtitle(),繫結「清空字幕」按鈕,清空 html 頁面已有的字幕,清除 dom 元素的節點內容。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>字幕生成</title>
</head>
<body>
<h1>椿輝近實時字幕生成工具</h1>
<div>
  <div style="width: 700px; float: left; display: block">
    <video id="video" autoplay></video>
    <button id="startButton" onclick="startGenerageSubtitle()">啟動字幕生成</button>
    <button id="stopButton"  onclick="stopGenerageSubtitle()">停止生成字幕</button>
    <button id="clearButton"  onclick="clearGenerageSubtitle()">清空字幕</button>
    <p id="subtitle" style="text-align: center"></p>
  </div>
  <div  style="width: 500px; float: left; display: block">
    <h3>所有字幕</h3>
    <p id="result"></p>
  </div>
</div>

<script>
  const video = document.getElementById('video');
  const result = document.getElementById('result');
  const subtitle = document.getElementById('subtitle');
  let ws = null;
  let mediaRecorder = null;
  let isRecording = false;
  let intervalId = null;
  // 獲取使用者媒體裝置
  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
          .then((stream) => {
            console.log("ws ===>", ws);
            ws = new WebSocket('ws://localhost:8080');
            video.srcObject = stream;
            // 建立WebSocket連線
            ws.onopen = function (){
              console.log('===> WebSocket連線已經建立');
            };
            ws.onmessage = function(map) {
              let newP = document.createElement("p");//建立一個p標籤
              newP.innerText = map.data;
              result.appendChild(newP);
              subtitle.textContent = map.data;
              console.log(map.data);
            }
          })
          .catch((err) => {
            console.log(err);
          });
  // 啟動字幕生成
  function startGenerageSubtitle() {
    if (isRecording) {
      console.log('===> 已經在生成字幕');
      return;
    }
    console.log('===> 開始生成字幕');
    isRecording = true;
    // 獲取使用者媒體裝置
    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
            .then((stream) => {
              console.log("每3秒傳送一次視訊流資料")
              // 每3秒傳送一次視訊流資料
              intervalId = setInterval(() => {
                const mediaRecorder = new MediaRecorder(stream, {
                  mimeType: 'video/webm;codecs=h264'
                });
                mediaRecorder.addEventListener('dataavailable', (event) => {
                  if (event.data.size > 0) {
                    // 傳送資料到後端
                    ws.send(event.data);
                  }
                });
                mediaRecorder.start();
                // console.log("mediaRecorder.start===", mediaRecorder)
                setTimeout(() => {
                  // console.log("mediaRecorder.stop===", mediaRecorder)
                  mediaRecorder.stop();
                }, 3000);
              }, 3000);
            })
            .catch((err) => {
              console.log(err);
            });
  }
  // 停止生成字幕
  function stopGenerageSubtitle() {
    if (!isRecording) {
      console.log('===> 沒有在生成字幕');
      return;
    }
    console.log('===> 停止生成字幕');
    isRecording = false;
    clearInterval(intervalId);
    // mediaRecorder.stop();
  }

  // 清空字幕
  function clearGenerageSubtitle() {
    subtitle.textContent = "";
    result.innerHTML = "<p></p>";
  }
</script>
</body>
</html>

4.3 後端實現

藉助了一個開源專案wxbool/video-srt ,這個開源專案可以把本地視訊檔轉成音訊(通過 ffmpeg 實現),傳到 OSS,並呼叫阿里的語音識別服務獲取到字幕資訊,我對他進行了一些改造,加入了服務的監聽啟動,隨後使用 websocket 接收前端視訊流,把視訊流轉存成本地視訊檔,最後呼叫了 video-srt 的原有邏輯程式碼,完成了視訊流字幕的提取生成。下面是一些關鍵程式碼。

專案根路徑的 main.go 以 http 服務監聽 8080 埠的形式啟動服務,介面的回撥處理常式是 RecognizeHandler2

RecognizeHandler2() 函數的程式碼邏輯在根路徑下的 handler.go 中,用 websocket 來處理這個 http 介面,迴圈讀取前端的視訊流,把視訊流儲存成一個本地視訊檔,呼叫 getSubtitle() 函數提取視訊檔中的字幕, getSubtitle() 封裝了原開源專案 wxbool/video-srt 的既有能力。

5、效果演示

關閉所有代理,否則呼叫阿里雲的 SDK 可能超時,以及存取阿里雲的 OSS 也可能超時。

5.1 啟動前的引數設定

如果你想執行本專案,請先拉取 luoChunhui-1024/video-subtitle 專案到本地,把專案根目錄的 config.ini 中的各種引數替換成剛才讓你記錄下來的那些阿里雲設定。

#字幕相關設定
[srt]
#智慧分段處理:true(開啟) false(關閉)
intelligent_block=true

#阿里雲Oss物件服務設定
#檔案:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.6.582.4e7858a85Dr5pA
[aliyunOss]
# OSS 對外服務的存取域名
endpoint=oss-cn-beijing.aliyuncs.com
# 儲存空間(Bucket)名稱
bucketName=my-test-bucket-lch
# 儲存空間(Bucket 域名)地址
bucketDomain=my-test-bucket-lch.oss-cn-beijing.aliyuncs.com
accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon
accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu

#阿里雲語音識別設定
#檔案:
[aliyunClound]
# 在管控臺中建立的專案Appkey,專案的唯一標識
appKey=5Xcb7kOlcSFAF248
accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon
accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu

5.2 啟動執行

先在後端專案的根路徑對專案進行編譯,編譯完成後在專案根路徑會生成一個 output 可執行檔案

go build -tags="recorder" -mod=mod -o output

直接執行這個可執行檔案,即可啟動後端服務

./output

隨後通過瀏覽器開啟專案中的 html/index.html 檔案,過程中可能會詢問獲取麥克風和攝像頭許可權,允許即可,這樣前端也啟動完成了。

提示:Mac 可以直接在瀏覽器的位址列輸入 html 頁面的絕對路徑來開啟 html 頁面

5.3 效果展示

整體介面如下,由於本人樣貌醜陋,為了不影響大家學習的心情,所以打了馬賽克。

點選「啟動字幕生成」按鈕,則會開始每三秒給後端傳送一次視訊流,後端經過大概 6~8 秒的處理,把視訊字幕返回給前端進行展示。所以字幕相較於畫面中的語音,是有 8~9 秒的延遲的。

畫面右側會展示已有的字幕,畫面最下方則僅展示最新的字幕。

點選「停止字幕生成」按鈕,終止給後端傳送視訊流的定時器。但是點選啟動字幕生成按鈕可以再次啟動定時器,進行字幕生成。

點選「清空字幕」按鈕,會同時清空畫面右側的「所有字幕」和畫面下方的最新字幕。

6、專案地址

github:https://github.com/luoChunhui-1024/video-subtitle

7、參考和致謝

特別感謝 wxbool/video-srt 專案,本專案後端的大部分都是直接借用了該專案,也特別感謝 chatgpt,雖然它提供的程式碼和方式坑了我很多次,但是仍舊給我提供了很大的幫助。

其他參考

阿里雲智慧語音互動幫助檔案錯誤碼查詢

golang伺服器端與web前端使用websocket通訊

Golang使用WebSocket通訊

通過使用WebSocket使前後端資料互動

webRTC結合webSocket實時通訊

WebRTC 從實戰到未來!前端如何實現一個最簡單的音視訊通話?

WebRTC API:MediaDevices.getUserMedia()

實時websocket視訊流儲存

建立和插入DOM節點

實時語音識別-websocket API(百度的產品,這次其實沒有用上)

實時語音轉寫 API 檔案(訊飛的產品,這次也沒用上)