基于 Blazor 打造一款实时字幕
早先在录制视频的时候一直使用的是 obs-auto-subtitle 作为实时字幕展示功能。不过这个是以 OBS 插件的形式存在,不管是语言和功能上都有一定的限制。故而使用 Blazor server 实现一个。
总体思路
- 实时字幕自然需要语音转文字的功能。考察了一些服务之后,发现同时具备有一定免费额度和有 C# SDK 两个条件的,就只有 Azure Cognitive Service 了。故而选择了它。
 - 使用 Blazor server 从服务端实时刷新页面到前端是非常简单的事情。因此,渲染一个简单的列表文本,然后通过 OBS 的 browser 组件接入画面即可。
 
快乐编码
有了基本的思路,我们就可以开始快乐的编码了。
简要设计
一般来说,语音转文字服务是一个与服务端进行持续交互的过程。因此需要一个对象来保持和服务端之间的沟通。我们可以设计一个ILiveCaptioningProvider来表示这种行为:
using System;
using System.Threading.Tasks; namespace Newbe.LiveCaptioning.Services
{
public interface ILiveCaptioningProvider : IAsyncDisposable
{
Task StartAsync(); void AddCallBack(Func<CaptionItem, Task> captionCallBack);
}
}
 
为了扩展可能适配不同提供商的可能,我们同样设计一个ILiveCaptioningProviderFactory用于表现创建ILiveCaptioningProvider的行为:
namespace Newbe.LiveCaptioning.Services
{
public interface ILiveCaptioningProviderFactory
{
ILiveCaptioningProvider Create();
}
}
 
有了这样两个接口,在页面上只要通过ILiveCaptioningProviderFactory创建ILiveCaptioningProvider,然后不断的接收回调展示在页面上即可。
将内容展示在页面上
有了基本的项目结构和接口,便可以尝试将内容绑定到页面上。要将实时转换的内容展示到界面上需要进行一定的算法转换。
在此之前,我们需要确定一下页面展示的预期:
- 在页面上展示至少两行文本
 - 当一句话超过一行文本的宽度时自动进行换行
 - 当一句话结束时,下一句话自动换行
 
例如,上面这句话进行连续阅读时,可能会出现如下效果:
live caption display
主要需要注意的是,在判断是要更新当前行还是进行换行,这部分逻辑需要注意进行处理。
填充实现
- 通过 Azure SDK 提供的
SpeechRecognizer对象来进行语音识别 - 通过 Subject 将事件转换为一个简单的可观测流,简化业务回调的处理
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; namespace Newbe.LiveCaptioning.Services
{
public class AzureLiveCaptioningProvider : ILiveCaptioningProvider
{
private readonly ILogger<AzureLiveCaptioningProvider> _logger;
private readonly IOptions<LiveCaptionOptions> _options;
private AudioConfig _audioConfig;
private SpeechRecognizer _recognizer;
private readonly List<Func<CaptionItem, Task>> _callbacks = new();
private Subject<CaptionItem> _sub; public AzureLiveCaptioningProvider(
ILogger<AzureLiveCaptioningProvider> logger,
IOptions<LiveCaptionOptions> options)
{
_logger = logger;
_options = options;
} public async Task StartAsync()
{
var azureProviderOptions = _options.Value.Azure;
var speechConfig = SpeechConfig.FromSubscription(azureProviderOptions.Key, azureProviderOptions.Region);
speechConfig.SpeechRecognitionLanguage = azureProviderOptions.Language;
_audioConfig = AudioConfig.FromDefaultMicrophoneInput();
_recognizer = new SpeechRecognizer(speechConfig, _audioConfig);
_sub = new Subject<CaptionItem>();
_sub
.Select(item => Observable.FromAsync(async () =>
{
try
{
await Task.WhenAll(_callbacks.Select(f => f.Invoke(item)));
}
catch (Exception e)
{
_logger.LogError(e, "failed to recognize");
}
}))
.Merge()
.Subscribe(); _recognizer.Recognizing += (sender, args) =>
{
_sub.OnNext(new CaptionItem
{
Text = args.Result.Text,
LineEnd = false
});
};
_recognizer.Recognized += (sender, args) =>
{
_sub.OnNext(new CaptionItem
{
Text = args.Result.Text,
LineEnd = true
});
};
await _recognizer.StartContinuousRecognitionAsync();
} public void AddCallBack(Func<CaptionItem, Task> captionCallBack)
{
_callbacks.Add(captionCallBack);
} public ValueTask DisposeAsync()
{
_recognizer?.Dispose();
_audioConfig?.Dispose();
_sub?.Dispose();
return ValueTask.CompletedTask;
}
}
}
 
- 实现工厂的方式非常多,这里采用 Autofac 来协助完成对象的创建
 
using Autofac;
using Microsoft.Extensions.Options; namespace Newbe.LiveCaptioning.Services
{
public class LiveCaptioningProviderFactory : ILiveCaptioningProviderFactory
{
private readonly ILifetimeScope _lifetimeScope;
private readonly IOptions<LiveCaptionOptions> _options; public LiveCaptioningProviderFactory(
ILifetimeScope lifetimeScope,
IOptions<LiveCaptionOptions> options)
{
_lifetimeScope = lifetimeScope;
_options = options;
} public ILiveCaptioningProvider Create()
{
var liveCaptionProviderType = _options.Value.Provider;
switch (liveCaptionProviderType)
{
case LiveCaptionProviderType.Azure:
var liveCaptioningProvider = _lifetimeScope.Resolve<AzureLiveCaptioningProvider>();
return liveCaptioningProvider;
default:
throw new ProviderNotFoundException();
}
}
}
}
 
- 对页面逻辑进行填充,完成效果
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Newbe.LiveCaptioning.Services; namespace Newbe.LiveCaptioning.Pages
{
public partial class Index : IAsyncDisposable
{
[Inject] public ILiveCaptioningProviderFactory LiveCaptioningProviderFactory { get; set; }
[Inject] public ILogger<Index> Logger { get; set; }
private ILiveCaptioningProvider _liveCaptioningProvider; private readonly List<CaptionDisplayItem> _captionList = new(); protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
_liveCaptioningProvider = LiveCaptioningProviderFactory.Create();
_liveCaptioningProvider.AddCallBack(CaptionCallBack);
await _liveCaptioningProvider.StartAsync();
}
} private int maxCount = 20; private Task CaptionCallBack(CaptionItem arg)
{
return InvokeAsync(() =>
{
Logger.LogDebug("Received: {Text}", arg.Text);
var last = _captionList.FirstOrDefault();
var newLine = false;
var text = arg.Text;
var skipPage = 0;
if (arg.Text.Length > maxCount)
{
skipPage = (int) Math.Floor(text.Length * 1.0 / maxCount);
text = arg.Text[(skipPage * maxCount)..];
} if (last == null || skipPage > last.TagCount)
{
newLine = true;
} if (newLine || _captionList.Count == 0)
{
_captionList.Insert(0, new CaptionDisplayItem
{
Text = text,
TagCount = arg.LineEnd ? -1 : skipPage
});
}
else
{
_captionList[0].Text = text;
if (arg.LineEnd)
{
_captionList[0].TagCount = -1;
}
} if (_captionList.Count > 4)
{
_captionList.RemoveRange(4, _captionList.Count - 4);
} StateHasChanged();
});
} private record CaptionDisplayItem
{
public string Text { get; set; }
public int TagCount { get; set; }
} public async ValueTask DisposeAsync()
{
if (_liveCaptioningProvider != null)
{
await _liveCaptioningProvider.DisposeAsync();
}
}
}
}
 
通过以上核心的代码,就可以完成从识别到展示相关的内容。
下载与安装
在尝试进行源码了解之前,你可以通过以下步骤来初步体验一下项目的效果。
首先,你可以从 Release 页面下载和你操作系统对应的版本:
https://github.com/newbe36524/Newbe.LiveCaptioning/releases
release
然后,将这个软件包解压到预先创建好的文件夹。
unzip
接着,在 Azure Portal 中创建一个 Cognitive Services。
提示 1:语音转文字每个月有 5 个小时的免费额度,可以参见
提示 2:你可以通过这个帮助来创建一个免费的 Azure 账号,新账号包含有 12 个月的免费大礼包,参见
create service 
region and key
随后,将生成好的 region 和 key 填入到 appsettings.Production.json 中。
记得同时修改 Language 选项,例如美式英语为 en-us,简体中文为 zh-cn。你可以通过以下链接来查看所有支持的语言:
update appsettings.Production.json
继而,启动 Newbe.LiveCaptioning.exe,你可以看到如下这样的提示信息,就说明一切已经正常。
region and key
最后,你可以使用浏览器打开http://localhost:5000,并对着你的话筒说话,这样便可以实时产生字幕了。
live caption
在 OBS 中加入字幕
首先,打开你的 OBS,并添加一个 browser 组件。
add browser
在组件的 url 中填入 http://localhost:5000,并设置一个合适的宽度和高度。
add browser
对着你的话筒话说,字幕就出来了。
test
辅助资料
Azure Speech to Text
可以通过以下链接在初步体验一下识别的效果:
可以通过以下链接找到 C# SDK 的对接方案:
Blazor server
可以通过以下链接来了解,如何通过服务端来推送 UI 变化到前端:
可以通过以下链接来了解,如何在 UI 线程之外来出发 UI 变化(这不就是 winform 再现):
.Net core publish
通过这里了解如何将 dotnet core 程序发布为一个单文件应用
https://docs.microsoft.com/dotnet/core/deploying/single-file?WT.mc_id=DX-MVP-5003606
了解不同操作系统下发布使用的 RID
https://docs.microsoft.com/dotnet/core/rid-catalog?WT.mc_id=DX-MVP-5003606
Github
了解如何通过 github action 打包发布内容到 release 中:
https://github.com/gittools/gitreleasemanager
小结
这是一个非常简单的项目应用,开发者可以通过该项目初步的了解 Blazor 的使用方法。你可以通过以下地址来获取本项目的源代码:
https://github.com/newbe36524/Newbe.LiveCaptioning
基于 Blazor 打造一款实时字幕的更多相关文章
- 【FastDFS】如何打造一款高可用的分布式文件系统?这次我明白了!!
		
写在前面 前面我们学习了如何基于两台服务器搭建FastDFS环境,而往往在生产环境中,需要FastDFS做到高可用,那如何基于FastDFS打造一款高可用的分布式文件系统呢?别急,今天,我们就一起来基 ...
 - 基于VueJS的render渲染函数结合自定义组件打造一款非常强大的IView 的Table
		
基于VueJS的render渲染函数结合自定义组件打造一款非常强大的IView 的Table https://segmentfault.com/a/1190000015970367
 - 【Microsoft Azure 的1024种玩法】八. 基于Azure云端轻松打造一款好用的私有云笔记
		
[简介] Leanote一款开源云笔记软件,它使用Go的Web框架revel和MongoDB开发完成的,其是目前为止发现的最有bigger的云笔记,它支持markdown输入,代码高亮,多人协作,笔记 ...
 - 转-基于NodeJS的14款Web框架
		
基于NodeJS的14款Web框架 2014-10-16 23:28 作者: NodeJSNet 来源: 本站 浏览: 1,399 次阅读 我要评论暂无评论 字号: 大 中 小 摘要: 在几年的时间里 ...
 - 打造一款属于自己的web服务器——开篇
		
JVM总结慢慢来吧,先插播一篇水文,来介绍下最近业余一直在写的一个小项目——easy-httpserver(github).适合新手学习,大神们路过即可^_^. 一.这是个什么玩意? easy-htt ...
 - 基于jquery打造的网页右侧自动收缩浮动在线客服代码
		
基于jquery打造的网页右侧自动收缩浮动在线QQ客服代码, 当前比较流行的一款QQ在线jquery特效代码, 代码中还带有IE6下PNG图片透明的特效,如果想研究IE6下PNG透明的同学也可以下载研 ...
 - 基于NodeJS的14款Web框架
		
摘要: 在几年的时间里,Node.js逐渐发展成一个成熟的开发平台,吸引了许多开发者.有许多大型高流量网站都采用Node.js进行开发,像PayPal, 此外,开发人员还可以使用它来开发一些快速移动W ...
 - 基于Jquery插件Uploadify实现实时显示进度条上传图片
		
网址:http://www.jb51.net/article/83811.htm 这篇文章主要介绍了基于Jquery插件Uploadify实现实时显示进度条上传图片的相关资料,感兴趣的小伙伴们 ...
 - 【ASP.NET程序员福利】打造一款人见人爱的ORM(一)
		
“很多人都不太认可以第三方ORM,因为考虑的点不够全面,没有大用户群体的ORM有保证,这点是不可否认确是事实.但是往往用户群体大的ORM又有不足之处,今天我们就来聊聊关于ORM的话题,打造 ...
 
随机推荐
- 「题解」NWRRC2017 Grand Test
			
本文将同步发布于: 洛谷博客: csdn: 博客园: 简书. 题目 题目链接:洛谷 P7025.gym101612G. 题意概述 给你一张有 \(n\) 个点 \(m\) 条边的无向图,无重边无自环, ...
 - csp-s模拟测试42「世界线·时间机器·密码」
			
$t3$不会 世界线 题解 题目让求的就是每个点能到点的数量$-$出度 设每个点能到的点为$f[x]$ 则$f[x]=x \sum\limits_{y}^{y\in son[x]} U f[y]$ 用 ...
 - Redundant Paths 分离的路径
			
Redundant Paths 分离的路径 题目描述 为了从F(1≤F≤5000)个草场中的一个走到另一个,贝茜和她的同伴们有时不得不路过一些她们讨厌的可怕的树.奶牛们已经厌倦了被迫走某一条路,所以她 ...
 - 管中窥豹-ssh链接过多的问题分析及复盘
			
缘起 某一天,产品侧同事联系过来,反馈话单传输程序报错,现象如下: 实际上,该节点仅提供了一个sftp服务,供产品侧传输话单过来进行临时存储,由计费部门取走而已. 分析 于是找运维同事上服务器看了下情 ...
 - Java程序安装失败
			
检查文件路径,应该不含中文汉字,空格以及特殊字符.应将jdk的安装目录设置为纯英文路径. 是否有多个安装程序同时运行,若多点安装程序则会安装失败,打开任务管理器,查看是否有多个安装程序运行 注册表 ...
 - zookeeper使用教程
			
Zookeeper 1. Zookeeper概述 1.1 概述 Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目. 1.2 特点 1)Zookeeper: ...
 - MySQL 中存储时间的最佳实践
			
平时开发中经常需要记录时间,比如用于记录某条记录的创建时间以及修改时间.在数据库中存储时间的方式有很多种,比如 MySQL 本身就提供了日期类型,比如 DATETIME,TIMESTAMEP 等,我们 ...
 - mysql字符集utf8和utf8mb4区别
			
1.起因 公司游戏项目上线第一天,出现单个区服异常宕机的问题,根据日志排查下来,连接数据的时候报错,后面排查是因为有玩家插入Emoji 等表情导致无法存储如数据库,数据库字符集编码为utf8,后续改成 ...
 - 关于WLS2中Ubuntu启用SSH远程登录功能,基于Xshell登录,支持Root
			
背景介绍 虽然WSL2提供了非常便利的访问Ubuntu目录的形式,但是仍然我们需要通过一个工具,比如XSHELL来实现对Ubuntu的SSH登录. 获取并安装Xshell 7 目前Xshell已经更新 ...
 - 13、windows下卸载oracle
			
13.1.停用oracle服务: 进入计算机管理,在服务中,找到oracle开头的所有服务,右击选择停止: 13.2.删除oracle: 在开始菜单中,找到oracle->Universal I ...