feat(WebSpeechSynthesizer): add WebSpeechSynthesizer service (#4135)

* refactor: 使用主构造函数

* feat: 增加 WebSpeech 服务

* feat: 增加 WebSpeechService 服务

* doc: 增加示例

* feat: 增加回调方法

* doc: 更新示例

* doc: 更新示例

* chore: bump version 8.8.4-beta07
This commit is contained in:
Argo Zhang 2024-08-23 23:02:16 +08:00 committed by GitHub
parent 511a88ee72
commit ad6ac66d62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 227 additions and 27 deletions

View File

@ -0,0 +1,19 @@
@page "/speech/web-synthesizer"
@inherits BootstrapComponentBase
<h3>@Localizer["WebSpeechTitle"]</h3>
<h4>@Localizer["WebSpeechSubTitle"]</h4>
<DemoBlock Title="@Localizer["WebSpeechNormalTitle"]"
Introduction="@Localizer["WebSpeechNormalIntro"]"
Name="Normal">
<div class="row">
<div class="col-12 col-sm-6">
<textarea @bind="_text" rows="6"></textarea>
</div>
<div class="col-12 col-sm-6 text-center">
<SpeechWave Show="_star" ShowUsedTime="false" class="my-3"></SpeechWave>
<Button Text="@_buttonText" OnClickWithoutRender="OnStart" IsAsync="true" Icon="fa-fw fa-solid fa-microphone"></Button>
</div>
</div>
</DemoBlock>

View File

@ -0,0 +1,52 @@
// 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 Microsoft.AspNetCore.Components.Forms;
namespace BootstrapBlazor.Server.Components.Samples.Speeches;
/// <summary>
/// WebSpeech 组件示例代码
/// </summary>
public partial class WebSpeeches
{
[Inject, NotNull]
private WebSpeechService? WebSpeechService { get; set; }
[Inject, NotNull]
private IStringLocalizer<WebSpeeches>? Localizer { get; set; }
private bool _star;
private string? _text;
private string? _buttonText = "开始合成";
private WebSpeechSynthesizer _entry = default!;
private TaskCompletionSource? _tcs;
private async Task OnStart()
{
if (!string.IsNullOrEmpty(_text))
{
if (_entry == null)
{
_entry = await WebSpeechService.CreateSynthesizerAsync();
_entry.OnEndAsync = SpeakAsync;
}
_tcs ??= new();
_star = true;
StateHasChanged();
await _entry.SpeakAsync(_text, "zh-CN");
await _tcs.Task;
_star = false;
_tcs = null;
StateHasChanged();
}
}
private Task SpeakAsync()
{
_tcs?.TrySetResult();
return Task.CompletedTask;
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>8.8.4-beta06</Version>
<Version>8.8.4-beta07</Version>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

View File

@ -7,23 +7,12 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// 语音识别服务
/// </summary>
public class RecognizerService
public class RecognizerService(IRecognizerProvider provider)
{
private IRecognizerProvider Provider { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="provider"></param>
public RecognizerService(IRecognizerProvider provider)
{
Provider = provider;
}
/// <summary>
/// 语音识别回调方法
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
public Task InvokeAsync(RecognizerOption option) => Provider.InvokeAsync(option);
public Task InvokeAsync(RecognizerOption option) => provider.InvokeAsync(option);
}

View File

@ -7,23 +7,13 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// 语音合成服务
/// </summary>
public class SynthesizerService
/// <param name="provider"></param>
public class SynthesizerService(ISynthesizerProvider provider)
{
private ISynthesizerProvider Provider { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="provider"></param>
public SynthesizerService(ISynthesizerProvider provider)
{
Provider = provider;
}
/// <summary>
/// 语音合成回调方法
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
public Task InvokeAsync(SynthesizerOption option) => Provider.InvokeAsync(option);
public Task InvokeAsync(SynthesizerOption option) => provider.InvokeAsync(option);
}

View File

@ -0,0 +1,30 @@
// 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 Microsoft.Extensions.Logging;
namespace BootstrapBlazor.Components;
/// <summary>
/// Web Speech 服务
/// </summary>
public class WebSpeechService(IJSRuntime runtime, IComponentIdGenerator ComponentIdGenerator, ILogger<WebSpeechService> logger)
{
private JSModule? Module { get; set; }
/// <summary>
/// 语音合成方法
/// </summary>
/// <returns></returns>
public async Task<WebSpeechSynthesizer> CreateSynthesizerAsync()
{
if (Module == null)
{
var moduleName = "./_content/BootstrapBlazor/modules/speech.js";
logger.LogInformation("load module {moduleName}", moduleName);
Module = await runtime.LoadModule(moduleName);
}
return new WebSpeechSynthesizer(Module, ComponentIdGenerator);
}
}

View File

@ -0,0 +1,84 @@
// 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/
namespace BootstrapBlazor.Components;
/// <summary>
/// WebSpeechSynthesizer 类
/// </summary>
public class WebSpeechSynthesizer(JSModule module, IComponentIdGenerator componentIdGenerator)
{
private DotNetObjectReference<WebSpeechSynthesizer>? _interop;
private string? _id;
/// <summary>
/// 获得/设置 朗读结束回调方法 默认 null
/// </summary>
public Func<Task>? OnEndAsync { get; set; }
/// <summary>
/// 开始朗读方法
/// </summary>
/// <param name="text"></param>
/// <param name="lang"></param>
public async Task SpeakAsync(string? text, string? lang = null)
{
_id = componentIdGenerator.Generate(this);
_interop = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("speak", _id, _interop, new { text, lang });
}
/// <summary>
/// 暂停朗读方法
/// </summary>
/// <returns></returns>
public async Task Pause()
{
await module.InvokeVoidAsync("pause", _id);
}
/// <summary>
/// 恢复朗读方法
/// </summary>
/// <returns></returns>
public async Task Resume()
{
await module.InvokeVoidAsync("resume", _id);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task OnError()
{
await Task.CompletedTask;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task OnEnd()
{
if (OnEndAsync != null)
{
await OnEndAsync();
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task OnSpeaking()
{
await Task.CompletedTask;
}
}

View File

@ -88,6 +88,7 @@ public static class BootstrapBlazorServiceCollectionExtensions
services.AddScoped<ResizeNotificationService>();
services.AddScoped<NotificationService>();
services.AddScoped<EyeDropperService>();
services.AddScoped<WebSpeechService>();
services.ConfigureBootstrapBlazorOption(configureOptions);

View File

@ -0,0 +1,35 @@
import Data from "./data.js"
export function speak(id, invoke, option) {
const synth = window.speechSynthesis;
if (synth.speaking) {
console.error("speechSynthesis.speaking");
invoke.invokeMethodAsync("OnSpeaking");
return;
}
const { text, lang } = option;
if (text !== "") {
const utter = new SpeechSynthesisUtterance(text);
if (lang) {
utter.lang = lang;
}
utter.onend = () => {
invoke.invokeMethodAsync("OnEnd");
};
utter.onerror = e => {
console.error("SpeechSynthesisUtterance.onerror", e);
invoke.invokeMethodAsync("OnError");
};
synth.speak(utter);
}
}
export function pause(id) {
}
export function resume(id) {
}