這兩天做了一個視訊通訊近實時字幕生成工具,前端通過瀏覽器開啟攝像頭,生成使用者畫面,根據使用者的語音近實時自動生成字幕展示在畫面下方。對於沒有接觸過音視訊處理的我來說,剛開始還是有點懵的,雖然藉助了 chatgpt,但是還是走了一段時間的彎路。不過花了大概一天時間還是比較完美的實現了,還是非常有成就感的。謹以此記錄最終成功的版本的實現思路和實現過程,文末附帶原始碼和原始碼啟動過程。
第四節「詳細過程」中會有這些工具安裝或者申請教學
ffmpeg,一個強大的視訊處理工具,此次主要用它來實現視訊轉成音訊。
阿里雲 OSS bucket
阿里雲 語音識別專案
本地 golang 執行環境
前端使用 WebRTC 調起攝像頭,與後端建立 websocket 連線,每隔三秒傳送一段視訊二進位制流到後端;
後端將視訊流儲存到本地,使用 ffmpeg 將本地視訊轉換成音訊;
把音訊上傳到阿里雲 OSS 物件儲存伺服器中;
獲取到音訊的存取地址;呼叫阿里雲的語音識別功能的 sdk 解析出音訊對應的文字內容;
後端通過 websocket 把文字內容回傳給前端,前端進行字幕展示。
謹以此提示來降低心理壓力,看起來此專案設計到前後端專案的開發和部署,但是其實不對此工具不用產生太大的壓力,因為很多操作都有現成工具可以借用。
雖然此次專案需要同時開發前後端,但是對於此次工具的開發,不需要把前端部署到伺服器,只需編寫一個簡單的 html,用瀏覽器渲染開啟即可。
chatpgt 可以一定程度上加快我們的問題解決過程,但是也不要全信它的內容,親身經歷被它坑了好多次。
github 上已有一些優秀的開源專案,比如此次所借用的開源專案wxbool/video-srt ,大大加快了專案的開發速度。
前後端 websocket 互動的實現也比較簡單,幾行程式碼就可以搞定。
在 Mac 上安裝方式是 brew install ffmpeg
(其他作業系統可以自行尋找安裝教學),安裝過程可能比較久,我安裝了大概 40 分鐘。
安裝完畢執行ffmpeg -version
,輸出如下資訊說明安裝成功。
登入阿里雲賬號後,存取 https://ram.console.aliyun.com/users,建立使用者
隨後在進入使用者首頁,點選「建立 AcessKey」,身份驗證通過後,會建立一個 RAM使用者的 AcessKey
和 AccessKey Secret
,立刻把兩個引數記錄下來,因為這個 AccessKey Secret
只在建立時顯示,後續不支援檢視。
存取 OSS物件儲存,點選立即開通,然後建立 bucket ,由於後續語音識別會存取 bucket 中的檔案,而語音識別只能存取到公開的資源,所以還需要設定 bucket 的開放範圍為「公開」
給 RPM 使用者新增完全控制許可權,否則後面執行程式碼時 oss 會報錯 StatusCode=403, ErrorCode=AccessDenied, ErrorMessage="You have no right to access this object because of bucket acl.",
存取 錄音檔案識別,點選立即開通,然後建立專案,獲取到專案AppKey,記錄下來。
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
輸出版本資訊說明安裝成功
只有一個 html 頁面,通過 websocket 跟後端建立連線,進行資料互動,包含一些必要的 dom 節點,以及三個按鈕。
javascript 指令碼包含四部分,
navigator.mediaDevices.getUserMedia
開啟使用者的媒體裝置,這個工具函數底層是通過 WebRTC 來實現的,隨後跟後端建立 websocket 連線,使用 ws.onmessage 將獲取到的後端訊息新增上 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>
藉助了一個開源專案wxbool/video-srt ,這個開源專案可以把本地視訊檔轉成音訊(通過 ffmpeg 實現),傳到 OSS,並呼叫阿里的語音識別服務獲取到字幕資訊,我對他進行了一些改造,加入了服務的監聽啟動,隨後使用 websocket 接收前端視訊流,把視訊流轉存成本地視訊檔,最後呼叫了 video-srt 的原有邏輯程式碼,完成了視訊流字幕的提取生成。下面是一些關鍵程式碼。
專案根路徑的 main.go 以 http 服務監聽 8080 埠的形式啟動服務,介面的回撥處理常式是 RecognizeHandler2
RecognizeHandler2() 函數的程式碼邏輯在根路徑下的 handler.go 中,用 websocket 來處理這個 http 介面,迴圈讀取前端的視訊流,把視訊流儲存成一個本地視訊檔,呼叫 getSubtitle() 函數提取視訊檔中的字幕, getSubtitle() 封裝了原開源專案 wxbool/video-srt 的既有能力。
關閉所有代理,否則呼叫阿里雲的 SDK 可能超時,以及存取阿里雲的 OSS 也可能超時。
如果你想執行本專案,請先拉取 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
先在後端專案的根路徑對專案進行編譯,編譯完成後在專案根路徑會生成一個 output
可執行檔案
go build -tags="recorder" -mod=mod -o output
直接執行這個可執行檔案,即可啟動後端服務
./output
隨後通過瀏覽器開啟專案中的 html/index.html
檔案,過程中可能會詢問獲取麥克風和攝像頭許可權,允許即可,這樣前端也啟動完成了。
提示:Mac 可以直接在瀏覽器的位址列輸入 html 頁面的絕對路徑來開啟 html 頁面
整體介面如下,由於本人樣貌醜陋,為了不影響大家學習的心情,所以打了馬賽克。
點選「啟動字幕生成」按鈕,則會開始每三秒給後端傳送一次視訊流,後端經過大概 6~8 秒的處理,把視訊字幕返回給前端進行展示。所以字幕相較於畫面中的語音,是有 8~9 秒的延遲的。
畫面右側會展示已有的字幕,畫面最下方則僅展示最新的字幕。
點選「停止字幕生成」按鈕,終止給後端傳送視訊流的定時器。但是點選啟動字幕生成按鈕可以再次啟動定時器,進行字幕生成。
點選「清空字幕」按鈕,會同時清空畫面右側的「所有字幕」和畫面下方的最新字幕。
github:https://github.com/luoChunhui-1024/video-subtitle
特別感謝 wxbool/video-srt 專案,本專案後端的大部分都是直接借用了該專案,也特別感謝 chatgpt,雖然它提供的程式碼和方式坑了我很多次,但是仍舊給我提供了很大的幫助。
其他參考
WebRTC 從實戰到未來!前端如何實現一個最簡單的音視訊通話?
WebRTC API:MediaDevices.getUserMedia()
實時語音識別-websocket API(百度的產品,這次其實沒有用上)
實時語音轉寫 API 檔案(訊飛的產品,這次也沒用上)