大家好,我是本期的实验室研究员——俞坤。明天我将通过试验和残缺的操作过程,向大家介绍如何基于 Blazor 打造一个能针对语音和视频中的声音,主动生成实时字幕的字幕零碎。接下来就让咱们一起到实验室中一探到底吧!
微软MVP实验室研究员
思路浅析
很多童鞋可能接触过相似的技术,例如咱们在录制视频时,能够应用 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,而后一直的接管回调展现在页面上即可。
二、将内容展现在页面上
有了根本的我的项目构造和接口,便能够尝试将内容绑定到页面上。要将实时转换的内容展现到界面上须要进行肯定的算法转换。
在此之前,咱们须要确定一下页面展现的预期:
- 在页面上展现至多两行文本
- 当一句话超过一行文本的宽度时主动进行换行
- 当一句话完结时,下一句话主动换行
例如,下面这句话进行间断浏览时,可能会呈现如下成果:
次要须要留神的是,在判断是要更新以后行还是进行换行,这部分逻辑须要留神进行解决。
三、填充事实
- 通过 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 页面下载和操作系统对应的版本。
- 将这个软件包解压到事后创立好的文件夹。
- 在 Azure 治理门户中创立一个 Cognitive Services。
提醒:语音转文字每个月有 5 小时的收费额度,能够参见此处。此外,大家能够通过这里创立一个收费的 Azure 账号,新账号蕴含有 12 个月的收费大礼包。
- 将生成好的 Region 和 Key 填入到 appsettings.Production.json 中。
- 批改 Language 选项,例如美式英语为 en-us,简体中文为 zh-cn。大家能够点击这里查看所有反对的语言。
- 启动 Newbe.LiveCaptioning.exe,如果看到如下所示信息,就阐明所有曾经失常。
- 最初,应用浏览器关上 http://localhost:5000,并对着话筒谈话,这样便能够实时产生字幕了。
在 OBS 中退出字幕
- 关上 OBS,并增加一个 Browser 组件。
- 组件的 URL 中填入 http://localhost:5000,并设置一个适合的宽度和高度。
- 对着话筒话说,字幕就进去了。
总结
这是一个非常简单的我的项目利用,开发者能够通过该我的项目初步理解 Blazor 的应用办法。如需获取本我的项目的源代码,请点击这里。
此外对于上述试验中波及到的各类技术和服务,大家能够通过下列资源链接进一步理解。
AzureSpeech to Text
1:初步体验Azure Speech的语音辨认成果
2:C#SDK的对接计划
BlazorServer
1:如何通过服务端来推送UI变动到前端
:2:如何在UI线程之外来触发UI变动(其实也是Winform再现)
.Netcore publish
1:如何将dotnet core程序公布为一个单文件利用
2:不同操作系统下公布应用的RID
GitHub
1:如何通过GitHub Action打包公布内容到Release中
对于本次实验所波及的内容,大家有任何问题或想法,或者对于咱们这个实验室的后续摸索方向有什么倡议,都欢送通过评论来留言。
当然,如果你有任何乏味的想法并且曾经顺利实现了,也欢送给咱们投稿,将你的作品分享给更多开发者,让咱们一起玩转软件开发,共同提高和提高!
微软MVP我的项目介绍
微软最有价值专家是微软公司授予第三方技术专业人士的一个寰球奖项。28年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和教训而取得此奖项。
MVP是通过严格筛选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的激情并乐于助人的专家。MVP致力于通过演讲、论坛问答、创立网站、撰写博客、分享视频、开源我的项目、组织会议等形式来帮忙别人,并最大水平地帮忙微软技术社区用户应用Microsoft技术。
更多详情请登录官方网站:
https://mvp.microsoft.com/zh-cn
文内链接请参考:
OBS-auto-subtitle:
https://github.com/summershri...
Release页面:
https://github.com/newbe36524...
此处:
https://azure.microsoft.com/z...
这里:
https://docs.microsoft.com/en...
点击这里:
https://docs.microsoft.com/zh...
请点击这里:
https://github.com/newbe36524...
初步体验AzureSpeech的语音辨认成果:
https://azure.microsoft.com/e...
C#SDK的对接计划:
https://docs.microsoft.com/zh...
如何通过服务端来推送UI变动到前端:
https://swimburger.net/blog/d...
如何在UI线程之外来触发UI变动:
https://docs.microsoft.com/zh...
如何将dotnet core程序公布为一个单文件利用:
https://docs.microsoft.com/zh...
不同操作系统下公布应用的RID:
https://docs.microsoft.com/zh...
如何通过GitHub Action打包公布内容到Release中:
https://github.com/gittools/g...