- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
这两天做了一个视频通信近实时字幕生成工具,前端通过浏览器打开摄像头,生成用户画面,根据用户的语音近实时自动生成字幕展示在画面下方。对于没有接触过音视频处理的我来说,刚开始还是有点懵的,虽然借助了 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 ,虽然它提供的代码和方式坑了我很多次,但是仍旧给我提供了很大的帮助.
其他参考 。
阿里云智能语音交互帮助文档 、 错误码查询 。
golang服务端与web前端使用websocket通信 。
Golang使用WebSocket通信 。
通过使用WebSocket使前后端数据交互 。
webRTC结合webSocket实时通信 。
WebRTC 从实战到未来!前端如何实现一个最简单的音视频通话?
WebRTC API:MediaDevices.getUserMedia() 。
实时websocket视频流存储 。
创建和插入DOM节点 。
实时语音识别-websocket API (百度的产品,这次其实没有用上) 。
实时语音转写 API 文档 (讯飞的产品,这次也没用上) 。
最后此篇关于视频通信近实时生成字幕项目实践的文章就讲到这里了,如果你想了解更多关于视频通信近实时生成字幕项目实践的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
本文分享自华为云社区《大模型LLM之分布式训练》,作者: 码上开花_Lancer。 随着语言模型参数量和所需训练数据量的急速增长,单个机器上有限的资源已无法满足大语言模型训练的要求。需要设计分布式训
本文分享自华为云社区《五大基础算法--动态规划法》,作者: 大金(内蒙的)。 一、基本概念 动态规划法,和分治法极其相似。区别就是,在求解子问题时,会保存该子问题的解,后面的子问题求解时,可以直接拿来
pip install scp pip install pexpect 测试代码: import os import stat import paramiko # 用于调用scp命令 def s
我目前正在实现“ token ”REST 服务。 token 只是一个字符串,由一些参数构建而成,然后经过哈希处理并在一定时间后过期。 我想在我的 REST 服务中有一个可以验证 token 的端点,
打开软删除后,我在客户端上添加一条记录,推送,删除添加的记录推送,然后尝试使用与初始记录相同的主键添加新记录(然后推送),我得到一个异常(exception)。 EntityDomainManager
打开软删除后,我在客户端上添加一条记录,推送,删除添加的记录推送,然后尝试使用与初始记录相同的主键添加新记录(然后推送),我得到一个异常(exception)。 EntityDomainManager
我有一个应用程序,每 x 秒接收一次天气信息。我想将此数据保存到 XML 文件中。 我应该为每个天气通知创建一个新的 XML 文件,还是将每个通知附加到同一个 XML 文件中?我不确定 XML 标准的
我猜我们大多数人都必须在某个时候处理这个问题,所以我想我会问这个问题。 当您的 BLL 中有很多集合并且您发现自己一遍又一遍地编写相同的旧内联(匿名)谓词时,显然有必要进行封装,但实现封装的最佳方
我有一些 c# 代码已经运行了一段时间了..我不得不说,虽然我了解 OO 原则的基础知识,但显然有不止一种方法可以给猫剥皮(尽管我讨厌那个短语!)。 因此,我有一个基本抽象类作为基本数据服务类,如下所
我设计了一个 SQL 数据库系统(使用 Postgre),我有一个问题,即创建一个关系/引用的常见做法是什么,这种关系/引用即使在引用的对象被删除时也能持续存在。 比如有一个UserORM,还有Act
我们的目标是搜索用户输入的字符串并计算在其中找到多少元音。不幸的是我被困在这里,有什么帮助吗? def numVowels(s): vowels= "AEIOUaeiou" if s
我有一个适用于我的“items”int 数组的旋转函数。下面的代码完成了它,除了我不必要地传输值。我正在努力实现“就地”轮换。我的意思是 ptrs 会递增或递减,而不是从数组中获取值。我需要通过这种方
我有一个 json 存储在我的应用程序文档文件夹中,我需要在我的所有 View 中使用它。我正在加载 json 并将其添加到每个 View 中的 NSMutableArray。但现在我了解到,我可以将
我用 C++ 开始了一个项目。这种语言的内存管理对我来说是新的。 我过去常常使用 new () 创建对象,然后传递指针,虽然它可以工作,但调试起来很痛苦,人们看到代码时会用有趣的眼神看着我。我为它没有
已结束。 这个问题是 off-topic .它目前不接受答案。 想要改进这个问题? Update the question所以它是on-topic堆栈溢出。 关闭 10 年前。 Improve thi
保持类松散耦合是编写易于理解、修改和调试的代码的一个重要方面——我明白这一点。然而,作为一个新手,几乎任何时候我都会超越我所苦苦挣扎的最简单的例子。 我或多或少地了解如何将字符串、整数和简单数据类型封
我发现我需要编写大量重复代码,因为我无法从其他 Controller 调用函数。例如,这里新闻提要内容在我的代码中重复,我对一个 Controller 做一些特定的事情,然后需要像这样加载我的新闻提要
假设需要一种数字数据类型,其允许值在指定范围内。更具体地说,假设要定义一个整数类型,其最小值为0,最大值为5000。这种情况在很多情况下都会出现,例如在对数据库数据类型,XSD数据类型进行建模时。 在
假设我想循环整个数组来访问每个元素。使用 for 循环、for...in 循环或 for...of 循环是 JavaScript 开发人员的标准做法吗? 例如: var myArray = ["app
我有一个旧的 SL4/ria 应用程序,我希望用 Breeze 取代它。我有一个关于内存使用和缓存的问题。我的应用程序加载工作列表(一个典型的用户可以访问大约 1,000 个这些工作)。此外,还有很多
我是一名优秀的程序员,十分优秀!