Skip to content

Commit

Permalink
feat(WebSpeechSynthesizer): add WebSpeechSynthesizer service (#4135)
Browse files Browse the repository at this point in the history
* refactor: 使用主构造函数

* feat: 增加 WebSpeech 服务

* feat: 增加 WebSpeechService 服务

* doc: 增加示例

* feat: 增加回调方法

* doc: 更新示例

* doc: 更新示例

* chore: bump version 8.8.4-beta07
  • Loading branch information
ArgoZhang authored Aug 23, 2024
1 parent 511a88e commit ad6ac66
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Argo Zhang ([email protected]). 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;
}
}
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -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'">
Expand Down
15 changes: 2 additions & 13 deletions src/BootstrapBlazor/Components/Speech/RecognizerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
16 changes: 3 additions & 13 deletions src/BootstrapBlazor/Components/Speech/SynthesizerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
30 changes: 30 additions & 0 deletions src/BootstrapBlazor/Components/Speech/WebSpeechService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Argo Zhang ([email protected]). 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);
}
}
84 changes: 84 additions & 0 deletions src/BootstrapBlazor/Components/Speech/WebSpeechSynthesizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Argo Zhang ([email protected]). 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;

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
services.AddScoped<ResizeNotificationService>();
services.AddScoped<NotificationService>();
services.AddScoped<EyeDropperService>();
services.AddScoped<WebSpeechService>();

services.ConfigureBootstrapBlazorOption(configureOptions);

Expand Down
35 changes: 35 additions & 0 deletions src/BootstrapBlazor/wwwroot/modules/speech.js
Original file line number Diff line number Diff line change
@@ -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) {

}

0 comments on commit ad6ac66

Please sign in to comment.