mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-12-02 03:59:14 +08:00
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:
parent
14161aa9de
commit
1039f1d146
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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()" />
|
@ -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 = "-"
|
||||
},
|
||||
];
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.hints span {
|
||||
text-shadow: 0px 0px 6px rgba(255,255,255,0.7);
|
||||
}
|
@ -754,11 +754,6 @@ internal static class MenusLocalizerExtensions
|
||||
{
|
||||
Text = Localizer["WebSerial"],
|
||||
Url = "web-serial"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Text = Localizer["WebSpeech"],
|
||||
Url = "web-speech"
|
||||
}
|
||||
};
|
||||
AddBadge(item);
|
||||
|
@ -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",
|
||||
|
@ -6310,6 +6310,9 @@
|
||||
"WebSpeechRecognitionTitle": "语音识别",
|
||||
"WebSpeechRecognitionIntro": "通过麦克风输入语音,进行语音识别",
|
||||
"WebSpeechRecognitionButtonText": "语音识别",
|
||||
"WebSpeechRecognitionContinuousTitle": "实时语音识别",
|
||||
"WebSpeechRecognitionContinuousIntro": "通过设置 <code>WebSpeechRecognitionOption</code> 参数 <code>Continuous=\"true\" InterimResults=\"true\"</code> 进行实时语音识别",
|
||||
"WebSpeechRecognitionContinuousButtonText": "持续识别",
|
||||
"RecognitionErrorNotAllowed": "用户已拒绝访问硬件设备",
|
||||
"RecognitionErrorNoSpeech": "未检测到语音",
|
||||
"RecognitionErrorAborted": "用户已取消",
|
||||
|
@ -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>
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user