feat(Recognition): support continuous recognition (#4146)

* refactor: 移除测试代码

* feat: 实现 stop/abort 逻辑

* refactor: 适配 safari 浏览器

* feat: 增加识别参数

* refactor: 代码格式化

* refactor: 增加识别参数

* doc: 移除 WebSpeech 菜单

* feat: 移除可信度增加 IsFinal 参数

* feat: 支持持续识别功能

* doc: 更新示例

* refactor: 增加持续识别支持

* test: 更新单元测试

* doc: 增加持续识别本地化文字说明

* doc: 更新文档

* test: 更新单元测试
This commit is contained in:
Argo Zhang 2024-08-25 17:12:34 +08:00 committed by GitHub
parent 14161aa9de
commit 1039f1d146
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 234 additions and 407 deletions

View File

@ -4,6 +4,16 @@
<h3>@Localizer["WebSpeechTitle"]</h3>
<h4>@Localizer["WebSpeechSubTitle"]</h4>
<p class="code-label">1. </p>
<Pre>[Inject, NotNull]
private WebSpeechService? WebSpeechService { get; set; }</Pre>
<p class="code-label">2. </p>
<Pre>var synthesizer = await WebSpeechService.CreateSynthesizerAsync();
await synthesizer.SpeakAsync("Hello Blazor", "en_US"));</Pre>
<Pre>var recognition = await WebSpeechService.CreateRecognitionAsync();
await recognition.StartAsync("en-US")</Pre>
<DemoBlock Title="@Localizer["WebSpeechNormalTitle"]"
Introduction="@Localizer["WebSpeechNormalIntro"]"
Name="Normal">
@ -25,11 +35,35 @@
Name="Recognition">
<div class="row">
<div class="col-12 col-sm-6">
<Button Text="@_buttonRecognitionText" OnClickWithoutRender="OnStartRecognition" IsAsync="true" Icon="fa-fw fa-solid fa-microphone"></Button>
<div class="mt-3">@_result</div>
<Button Text="@_buttonRecognitionText" OnClickWithoutRender="OnStartRecognition" IsDisabled="_starRecognition" Icon="fa-fw fa-solid fa-microphone"></Button>
</div>
<div class="col-12 col-sm-6">
<SpeechWave Show="_starRecognition" ShowUsedTime="false" class="my-3"></SpeechWave>
</div>
<div class="col-12">
<div class="bb-result">
<span class="final">@_result</span>
</div>
</div>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["WebSpeechRecognitionContinuousTitle"]"
Introduction="@Localizer["WebSpeechRecognitionContinuousIntro"]"
Name="Continuous">
<div class="row">
<div class="col-12 col-sm-6">
<Button Text="@_buttonRecognitionContinuousText" OnClickWithoutRender="OnStartContinuousRecognition" IsDisabled="_starRecognitionContinuous" Icon="fa-fw fa-solid fa-microphone"></Button>
<Button Text="@_buttonStopText" OnClickWithoutRender="OnStopContinuousRecognition" Icon="fa-fw fa-solid fa-stop"></Button>
</div>
<div class="col-12 col-sm-6">
<SpeechWave Show="_starRecognitionContinuous" ShowUsedTime="false" class="my-3"></SpeechWave>
</div>
<div class="col-12">
<div class="bb-result">
<span class="final">@_finalResult</span>
<span class="temp">@_tempResult</span>
</div>
</div>
</div>
</DemoBlock>

View File

@ -35,6 +35,12 @@ public partial class WebSpeeches
private string? _buttonRecognitionText;
private string? _result;
private bool _starRecognitionContinuous;
private string? _buttonRecognitionContinuousText;
private string? _finalResult;
private string? _tempResult;
private WebSpeechRecognition _recognitionContinuous = default!;
/// <summary>
/// <inheritdoc/>
/// </summary>
@ -52,6 +58,7 @@ public partial class WebSpeeches
{
_speechVoices.AddRange(voices);
}
_voices.AddRange(_speechVoices.Select(i => new SelectedItem(i.Name!, $"{i.Name}({i.Lang})")));
_voiceName = _speechVoices.Find(i => i.Lang == CultureInfo.CurrentUICulture.Name)?.Name;
@ -85,6 +92,41 @@ public partial class WebSpeeches
StateHasChanged();
return Task.CompletedTask;
};
// create recognition continuous
_buttonRecognitionContinuousText = Localizer["WebSpeechRecognitionContinuousButtonText"];
_recognitionContinuous = await WebSpeechService.CreateRecognitionAsync();
_recognitionContinuous.OnSpeechStartAsync = () =>
{
_starRecognitionContinuous = true;
StateHasChanged();
return Task.CompletedTask;
};
_recognitionContinuous.OnSpeechEndAsync = () =>
{
_starRecognitionContinuous = false;
StateHasChanged();
return Task.CompletedTask;
};
_recognitionContinuous.OnErrorAsync = async e =>
{
e.ParseErrorMessage(Localizer);
await ToastService.Error("Recognition", e.Message);
};
_recognitionContinuous.OnResultAsync = e =>
{
if (e.IsFinal)
{
_finalResult += e.Transcript;
_tempResult = string.Empty;
}
else
{
_tempResult = e.Transcript;
}
StateHasChanged();
return Task.CompletedTask;
};
}
private async Task OnStart()
@ -118,7 +160,26 @@ public partial class WebSpeeches
private async Task OnStartRecognition()
{
_result = "";
await _recognition.StartAsync();
await _recognition.StartAsync(CultureInfo.CurrentUICulture.Name);
StateHasChanged();
}
private async Task OnStartContinuousRecognition()
{
_tempResult = "";
_finalResult = "";
await _recognitionContinuous.StartAsync(new WebSpeechRecognitionOption()
{
Lang = CultureInfo.CurrentUICulture.Name,
Continuous = true,
InterimResults = true
});
StateHasChanged();
}
private async Task OnStopContinuousRecognition()
{
await _recognitionContinuous.StopAsync();
StateHasChanged();
}
}

View File

@ -0,0 +1,16 @@
.bb-result {
margin-top: 1rem;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
min-height: 160px;
padding: .5rem;
}
.final {
color: #000;
}
.temp {
color: gray;
margin-left: 1rem;
}

View File

@ -1,127 +0,0 @@
@page "/web-speech"
@inject IStringLocalizer<WebSpeeches> Localizer
<h3>@Localizer["WebSpeechTitle"]</h3>
<h5>@((MarkupString)Localizer["WebSpeechDescription"].Value)</h5>
<PackageTips Name="BootstrapBlazor.WebAPI" />
<DemoBlock Title="@Localizer["WebSpeechNormalTitle"]" Introduction="" Name="WebSpeechNormal">
<div class="row g-3 mb-3">
<WebSpeech @ref="WebSpeech" OnResult="@OnResult" OnIsBusy="@OnIsBusy" OnStatus="@OnStatus" OnError="@OnError" />
<div class="col-12">
<BootstrapInputGroup>
<Button Text="普通话" OnClick="SpeechRecognition" IsAsync />
<Button Text="粤语" OnClick="SpeechRecognitionHK" IsAsync />
<Button Text="英文" OnClick="SpeechRecognitionEN" IsAsync />
<Button Text="西文" OnClick="SpeechRecognitionES" IsAsync />
<Button Text="停止" OnClick="SpeechRecognitionStop" Icon="fa-fw fa-solid fa-stop" IsAsync />
</BootstrapInputGroup>
</div>
<div>
<Checkbox TValue="bool" ShowLabel="true" @bind-Value="@Options.Continuous" />
</div>
<div>
<Checkbox TValue="bool" ShowLabel="true" @bind-Value="@Options.InterimResults" />
</div>
<div style="width: 160px;">
<BootstrapInputNumber ShowLabel="true" ShowButton="true" @bind-Value="@Options.MaxAlternatives" Max="10" Min="1" />
</div>
<div class="col-12">
<Textarea Value="@Result" />
</div>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["WebSpeechGameTitle"]" Introduction="" Name="WebSpeechGame">
<div class="row g-3 mb-3">
<WebSpeech @ref="WebSpeechGame" OnResult="@OnResult" OnIsBusy="@OnIsBusy" OnStatus="@OnStatus" OnError="@OnError" />
<div class="col-12">
<BootstrapInputGroup>
<Button Text="普通话" OnClick="SpeechRecognitionDemo" IsAsync />
<Button Text="粤语" OnClick="SpeechRecognitionHKDemo" IsAsync />
<Button Text="英文" OnClick="SpeechRecognitionENDemo" IsAsync />
<Button Text="西文" OnClick="SpeechRecognitionESDemo" IsAsync />
<Button Text="停止" OnClick="SpeechRecognitionDemoStop" Icon="fa-fw fa-solid fa-stop" IsAsync />
</BootstrapInputGroup>
</div>
@if (WebSpeechGame != null && WebSpeechGame.IsBusy)
{
<Spinner />
}
<p class="hints"></p>
<div>
<p class="output"><em>...</em></p>
</div>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["WebSpeechSynthesisTitle"]" Introduction="" Name="WebSpeechSynthesis">
<div class="row g-3 mb-3">
<div class="col-12">
<BootstrapInputGroup>
<Button Text="普通话" OnClick="SpeechSynthesis" IsAsync />
<Button Text="粤语" OnClick="SpeechSynthesisHK" IsAsync />
<Button Text="英文" OnClick="SpeechSynthesisEN" IsAsync />
<Button Text="西文" OnClick="SpeechSynthesisES" IsAsync />
<Button Text="停止" OnClick="SpeechStop" Icon="fa-fw fa-solid fa-stop" IsAsync />
</BootstrapInputGroup>
</div>
<div class="col-12">
<WebSpeech @ref="WebSpeechSynthesis" OnResult="@OnResult" OnIsBusy="@OnIsBusy" OnStatus="@OnStatus" OnError="@OnError" />
</div>
<p>
@Message
</p>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["WebSpeechSynthesisCustomTitle"]" Introduction="" Name="WebSpeechSynthesisCustom">
<div class="row g-3">
<div class="col-12">
@if (WebVoiceList != null && WebVoiceList.Any())
{
<select id="voiceSelect" class="form-select" @onchange="((e)=>OnChange(e))" style="width:70vw; max-width: 400px;">
@foreach (var voice in WebVoiceList)
{
<option value="@voice.VoiceURI">@($"{(voice.LocalService ? "" : "*")}{voice.Name} ({voice.Lang})")</option>
}
</select>
}
</div>
<div class="col-12">
<input type="range" min="0.1" max="10" step="0.1" style="width:70vw; max-width: 400px;" @bind-value="Options2.Rate" />
</div>
<div class="col-12">
<input type="range" min="0" max="2" step="0.1" style="width: 70vw; max-width: 400px; " @bind-value="Options2.Picth">
</div>
<div class="col-12">
<input type="range" min="0" max="1" step="0.01" style="width: 70vw; max-width: 400px; " @bind-value="Options2.Volume" />
</div>
<div class="col-12">
<Textarea @bind-Value="SpeakText" />
</div>
<div class="col-12">
<BootstrapInputGroup>
<Button Text="测试" OnClick="SpeechSynthesisDIY" Icon="fa-fw fa-solid fa-play" IsAsync />
<Button Text="停止" OnClick="SpeechDIYStop" Icon="fa-fw fa-solid fa-stop" IsAsync />
<Button Text="语音列表" OnClick="GetVoiceList" IsAsync />
</BootstrapInputGroup>
</div>
<div class="col-12">
<WebSpeech @ref="WebSpeechDIY" OnResult="@OnResult" OnIsBusy="@OnIsBusy" OnStatus="@OnStatus" OnError="@OnError" />
</div>
</div>
</DemoBlock>
<AttributeTable Items="@GetAttributes()" />

View File

@ -1,237 +0,0 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System.ComponentModel;
namespace BootstrapBlazor.Server.Components.Samples;
/// <summary>
/// WebSpeeches
/// </summary>
public partial class WebSpeeches
{
[NotNull]
WebSpeech? WebSpeech { get; set; }
[NotNull]
WebSpeech? WebSpeechGame { get; set; }
[NotNull]
WebSpeech? WebSpeechSynthesis { get; set; }
[NotNull]
WebSpeech? WebSpeechDIY { get; set; }
[DisplayName("识别结果")]
string? Result { get; set; } = "";
string? Result2 { get; set; } = "";
[DisplayName("内容")]
private string SpeakText { get; set; } = "我们一直与 Blazor 同行";
private string? SelectLang { get; set; }
SpeechRecognitionOption Options { get; set; } = new SpeechRecognitionOption();
SpeechSynthesisOption Options2 { get; set; } = new SpeechSynthesisOption();
[NotNull]
private Message? Message { get; set; }
[Inject]
[NotNull]
private MessageService? MessageService { get; set; }
[Inject]
[NotNull]
private ToastService? Toast { get; set; }
/// <summary>
///
/// </summary>
/// <param name="message"></param>
/// <param name="error"></param>
/// <returns></returns>
protected async Task ShowBottomMessage(string message, bool error = false)
{
await MessageService.Show(new MessageOption()
{
Content = message,
Icon = "fa-solid fa-circle-info",
Color = error ? Color.Warning : Color.Primary
}, Message);
}
#region SpeechRecognition
async Task SpeechRecognition() => Result2 = await WebSpeech.SpeechRecognition(option: Options);
async Task SpeechRecognitionHK() => Result2 = await WebSpeech.SpeechRecognition("zh-HK", Options);
async Task SpeechRecognitionEN() => Result2 = await WebSpeech.SpeechRecognition("en-US", Options);
async Task SpeechRecognitionES() => Result2 = await WebSpeech.SpeechRecognition("es-ES", Options);
async Task SpeechRecognitionStop() => await WebSpeech.SpeechRecognitionStop();
#endregion
#region SpeechRecognitionDemo
async Task SpeechRecognitionDemo() => Result2 = await WebSpeechGame.SpeechRecognitionDemo();
async Task SpeechRecognitionHKDemo() => Result2 = await WebSpeechGame.SpeechRecognitionDemo("zh-HK");
async Task SpeechRecognitionENDemo() => Result2 = await WebSpeechGame.SpeechRecognitionDemo("en-US");
async Task SpeechRecognitionESDemo() => Result2 = await WebSpeechGame.SpeechRecognitionDemo("es-ES");
async Task SpeechRecognitionDemoStop() => await WebSpeechGame.SpeechRecognitionStop();
#endregion
#region SpeechSynthesis
async Task SpeechSynthesis() => await WebSpeechSynthesis.SpeechSynthesis("你好 blazor, 现在是" + NowString());
async Task SpeechSynthesisHK() => await WebSpeechSynthesis.SpeechSynthesis("早晨 blazor, 依家系 " + NowString(), "zh-HK");
async Task SpeechSynthesisEN() => await WebSpeechSynthesis.SpeechSynthesis("Hello blazor, now is " + NowString(), "en-US");
async Task SpeechSynthesisES() => await WebSpeechSynthesis.SpeechSynthesis("Hola blazor, ahora es " + NowString(), "es-ES");
async Task SpeechStop() => await WebSpeech.SpeechStop();
#endregion
#region SpeechSynthesisDIY
async Task SpeechSynthesisDIY() => await WebSpeechDIY.SpeechSynthesis(SpeakText, Options2, "", SelectLang ?? WebVoiceList?.FirstOrDefault()?.VoiceURI);
async Task SpeechDIYStop() => await WebSpeechDIY.SpeechStop();
#endregion
static string NowString() => DateTime.Now.ToShortTimeString();
List<WebVoice>? WebVoiceList { get; set; }
async Task GetVoiceList()
{
WebVoiceList = await WebSpeechDIY.GetVoiceList();
if (WebVoiceList != null && WebVoiceList.Any()) StateHasChanged();
}
private Task OnIsBusy(bool flag)
{
StateHasChanged();
return Task.CompletedTask;
}
private void OnChange(ChangeEventArgs val)
{
if (val?.Value != null) SelectLang = val.Value.ToString();
}
private Task OnResult(string message)
{
Result = message;
StateHasChanged();
return Task.CompletedTask;
}
private async Task OnStatus(string message)
{
Result2 = message;
if (Options.InterimResults || Options.Continuous)
{
await Toast.Information("Web Speech", message);
}
else
{
await ShowBottomMessage(message);
}
}
private async Task OnError(string message)
{
Result2 = message;
await ShowBottomMessage(message, true);
}
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Delay(500);
await Task.Delay(1500);
while (WebVoiceList == null || !WebVoiceList.Any())
{
await Task.Delay(100);
await GetVoiceList();
if (WebSpeech.SpeechUndefined)
{
return;
}
}
}
}
/// <summary>
/// 获得属性方法
/// </summary>
/// <returns></returns>
protected AttributeItem[] GetAttributes() =>
[
new() {
Name = "SpeechRecognition",
Description = "语音识别",
Type = "Task<string>",
ValueList = "",
DefaultValue = "-"
},
new() {
Name = "SpeechRecognitionStop",
Description = "停止语音识别",
Type = "Task",
ValueList = "",
DefaultValue = "-"
},
new() {
Name = "SpeechSynthesis",
Description = "语音合成",
Type = "Task",
ValueList = "",
DefaultValue = "-"
},
new() {
Name = "SpeechStop",
Description = "停止语音合成",
Type = "Task",
ValueList = "",
DefaultValue = "-"
},
new() {
Name = "OnResult",
Description = "识别完成回调方法",
Type = "Func<string, Task>?",
ValueList = "-",
DefaultValue = "-"
},
new() {
Name = "OnIsBusy",
Description = "工作状态回调方法",
Type = "Func<bool, Task>?",
ValueList = "-",
DefaultValue = "-"
},
new() {
Name = "OnStatus",
Description = "状态信息回调方法",
Type = "Func<string, Task>?",
ValueList = "-",
DefaultValue = "-"
},
new() {
Name = "OnError",
Description = "错误回调方法",
Type = "Func<string, Task>?",
ValueList = "-",
DefaultValue = "-"
},
new() {
Name = "Element",
Description = "UI界面元素的引用对象,为空则使用整个页面",
Type = "ElementReference",
ValueList = "-",
DefaultValue = "-"
},
];
}

View File

@ -1,3 +0,0 @@
.hints span {
text-shadow: 0px 0px 6px rgba(255,255,255,0.7);
}

View File

@ -754,11 +754,6 @@ internal static class MenusLocalizerExtensions
{
Text = Localizer["WebSerial"],
Url = "web-serial"
},
new()
{
Text = Localizer["WebSpeech"],
Url = "web-speech"
}
};
AddBadge(item);

View File

@ -6310,6 +6310,9 @@
"WebSpeechRecognitionTitle": "Speech Recognition",
"WebSpeechRecognitionIntro": "Input voice through microphone for voice recognition",
"WebSpeechRecognitionButtonText": "Recognition",
"WebSpeechRecognitionContinuousTitle": "Real-time speech recognition",
"WebSpeechRecognitionContinuousIntro": "Real-time speech recognition by setting the <code>WebSpeechRecognitionOption</code> parameter <code>Continuous=\"true\" InterimResults=\"true\"</code>",
"WebSpeechRecognitionContinuousButtonText": "Recognition",
"RecognitionErrorNotAllowed": "The user agent disallowed any speech input from occurring for reasons of security, privacy or user preference",
"RecognitionErrorNoSpeech": "No speech was detected",
"RecognitionErrorAborted": "Speech input was aborted in some manner, perhaps by some user-agent-specific behavior like a button the user can press to cancel speech input",

View File

@ -6310,6 +6310,9 @@
"WebSpeechRecognitionTitle": "语音识别",
"WebSpeechRecognitionIntro": "通过麦克风输入语音,进行语音识别",
"WebSpeechRecognitionButtonText": "语音识别",
"WebSpeechRecognitionContinuousTitle": "实时语音识别",
"WebSpeechRecognitionContinuousIntro": "通过设置 <code>WebSpeechRecognitionOption</code> 参数 <code>Continuous=\"true\" InterimResults=\"true\"</code> 进行实时语音识别",
"WebSpeechRecognitionContinuousButtonText": "持续识别",
"RecognitionErrorNotAllowed": "用户已拒绝访问硬件设备",
"RecognitionErrorNoSpeech": "未检测到语音",
"RecognitionErrorAborted": "用户已取消",

View File

@ -51,7 +51,12 @@ public class WebSpeechRecognition(JSModule module, IComponentIdGenerator compone
/// <summary>
/// 开始识别方法
/// </summary>
public async Task StartAsync()
public Task StartAsync(string lang) => StartAsync(new WebSpeechRecognitionOption() { Lang = lang });
/// <summary>
/// 开始识别方法
/// </summary>
public async Task StartAsync(WebSpeechRecognitionOption option)
{
_id = componentIdGenerator.Generate(this);
_interop = DotNetObjectReference.Create(this);
@ -59,7 +64,7 @@ public class WebSpeechRecognition(JSModule module, IComponentIdGenerator compone
{
TriggerStart = OnStartAsync != null,
TriggerSpeechStart = OnSpeechStartAsync != null
});
}, option);
}
/// <summary>

View File

@ -9,13 +9,13 @@ namespace BootstrapBlazor.Components;
/// </summary>
public class WebSpeechRecognitionEvent
{
/// <summary>
/// 获得/设置 可信度
/// </summary>
public float Confidence { get; set; }
/// <summary>
/// 获得/设置 识别文本内容
/// </summary>
public string? Transcript { get; set; }
/// <summary>
/// 获得/设置 是否已经结束
/// </summary>
public bool IsFinal { get; set; }
}

View File

@ -0,0 +1,37 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System.Text.Json.Serialization;
namespace BootstrapBlazor.Components;
/// <summary>
/// WebSpeechRecognitionOption 配置类
/// </summary>
public class WebSpeechRecognitionOption
{
/// <summary>
/// sets the maximum number of SpeechRecognitionAlternatives provided per SpeechRecognitionResult.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public float? MaxAlternatives { get; set; }
/// <summary>
/// whether continuous results are returned for each recognition, or only a single result.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Continuous { get; set; }
/// <summary>
/// whether interim results should be returned (true) or not (false). Interim results are results that are not yet final (e.g. the SpeechRecognitionResult.isFinal property is false).
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? InterimResults { get; set; }
/// <summary>
/// sets the language of the current SpeechRecognition. If not specified, this defaults to the HTML lang attribute value, or the user agent's language setting if that isn't set either.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Lang { get; set; }
}

View File

@ -1,6 +1,6 @@
import Data from "./data.js"
export async function start(id, invoke, option) {
export async function start(id, invoke, trigger, option) {
const speechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition;
if (speechRecognition === null) {
invoke.invokeMethodAsync("TriggerErrorCallback", {
@ -9,12 +9,12 @@ export async function start(id, invoke, option) {
});
}
const recognition = new speechRecognition();
if (option.triggerStart || true) {
if (trigger.triggerStart) {
recognition.onstart = () => {
invoke.invokeMethodAsync("TriggerStartCallback");
}
}
if (option.triggerSpeechStart || true) {
if (trigger.triggerSpeechStart) {
recognition.onspeechstart = () => {
invoke.invokeMethodAsync("TriggerSpeechStartCallback");
}
@ -39,25 +39,53 @@ export async function start(id, invoke, option) {
});
}
recognition.onresult = e => {
const transcript = e.results[0][0];
let final_transcript = '';
let interim_transcript = '';
let isFinal = false;
for (let i = e.resultIndex; i < e.results.length; i++) {
if (e.results[i].isFinal) {
final_transcript += e.results[i][0].transcript;
isFinal = true;
}
else {
interim_transcript += e.results[i][0].transcript;
}
}
invoke.invokeMethodAsync("TriggerResultCallback", {
transcript: transcript.transcript,
confidence: transcript.confidence
transcript: interim_transcript || final_transcript,
isFinal: isFinal
});
}
recognition.lang = 'zh-CN';
recognition.maxAlternatives = 1;
recognition.interimResults = false;
recognition.continuous = false;
const { lang, maxAlternatives, continuous, interimResults } = option;
if (lang !== void 0) {
recognition.lang = lang;
}
if (maxAlternatives !== void 0) {
recognition.maxAlternatives = maxAlternatives;
}
if (interimResults !== void 0) {
recognition.interimResults = interimResults;
}
if (continuous !== void 0) {
recognition.continuous = continuous;
}
recognition.start();
Data.set(id, recognition);
}
export function stop(id) {
const synth = window.speechSynthesis;
synth.pause();
const recognition = Data.get(id);
Data.remove(id);
if (recognition) {
recognition.stop();
}
}
export function abort(id) {
const synth = window.speechSynthesis;
synth.resume();
const recognition = Data.get(id);
Data.remove(id);
if (recognition) {
recognition.abort();
}
}

View File

@ -59,18 +59,14 @@ export function cancel(id) {
const getUtteranceVoices = () => {
const synth = window.speechSynthesis;
let done = false;
let voices = [];
if (synth.onvoiceschanged === null) {
let voices = synth.getVoices();
let done = voices.length > 0;
if (done === false && synth.onvoiceschanged === null) {
synth.onvoiceschanged = () => {
voices = synth.getVoices();
done = true;
};
}
else {
voices = synth.getVoices();
done = true;
}
return new Promise((resolve, reject) => {
const handler = setInterval(() => {

View File

@ -11,7 +11,7 @@ public class WebSpeechRecognitionTest : BootstrapBlazorTestBase
{
var service = Context.Services.GetRequiredService<WebSpeechService>();
var recognition = await service.CreateRecognitionAsync();
await recognition.StartAsync();
await recognition.StartAsync("zh-CN");
WebSpeechRecognitionEvent? result = null;
recognition.OnResultAsync = @event =>
{
@ -20,11 +20,11 @@ public class WebSpeechRecognitionTest : BootstrapBlazorTestBase
};
await recognition.TriggerResultCallback(new WebSpeechRecognitionEvent()
{
Confidence = 0.9f,
IsFinal = true,
Transcript = "test"
});
Assert.NotNull(result);
Assert.Equal(0.9f, result.Confidence);
Assert.True(result.IsFinal);
Assert.Equal("test", result.Transcript);
}
@ -33,7 +33,7 @@ public class WebSpeechRecognitionTest : BootstrapBlazorTestBase
{
var service = Context.Services.GetRequiredService<WebSpeechService>();
var recognition = await service.CreateRecognitionAsync();
await recognition.StartAsync();
await recognition.StartAsync("zh-CN");
WebSpeechRecognitionError? error = null;
recognition.OnErrorAsync = err =>
{
@ -147,4 +147,20 @@ public class WebSpeechRecognitionTest : BootstrapBlazorTestBase
await recognition.TriggerSpeechEndCallback();
Assert.True(end);
}
[Fact]
public void WebSpeechRecognitionOption_Ok()
{
var option = new WebSpeechRecognitionOption()
{
Lang = "zh-CN",
Continuous = true,
InterimResults = true,
MaxAlternatives = 1
};
Assert.True(option.Continuous);
Assert.True(option.InterimResults);
Assert.Equal("zh-CN", option.Lang);
Assert.Equal(1, option.MaxAlternatives);
}
}