Skip to content

Service cleanup #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions app/SharedWebComponents/Components/VoiceDialog.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public sealed partial class VoiceDialog : IDisposable

[Inject] public required ISpeechSynthesisService SpeechSynthesis { get; set; }

[Inject] public required ITextToSpeechPreferencesListener VoiceChangesListener { get; set; }

[Inject] public required ILocalStorageService LocalStorage { get; set; }

[CascadingParameter] public required MudDialogInstance Dialog { get; set; }
Expand All @@ -22,19 +24,7 @@ protected override async Task OnInitializedAsync()

await GetVoicesAsync();

try
{
SpeechSynthesis.OnVoicesChanged(() => GetVoicesAsync(true));
}
catch
{
// TODO: Find a better way to do this
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
// however the underlying code does not do this using a DI-friendly way.
// The code assumes the concrete implementation for the ISpeechSynthesisService
// service is the concrete Web Assembly type which is not valid.
// There is no alternative API that MAUI apps can use.
}
VoiceChangesListener.OnAvailableVoicesChanged(() => GetVoicesAsync(true));

_voicePreferences = new VoicePreferences(LocalStorage);

Expand Down Expand Up @@ -70,19 +60,7 @@ private void OnValueChanged(string selectedVoice) => _voicePreferences = _voiceP

public void Dispose()
{
try
{
SpeechSynthesis.UnsubscribeFromVoicesChanged();
}
catch
{
// TODO: Find a better way to do this
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
// however the underlying code does not do this using a DI-friendly way.
// The code assumes the concrete implementation for the ISpeechSynthesisService
// service is the concrete Web Assembly type which is not valid.
// There is no alternative API that MAUI apps can use.
}
VoiceChangesListener.UnsubscribeFromAvailableVoicesChanged();
}
}

Expand Down
3 changes: 1 addition & 2 deletions app/SharedWebComponents/Pages/VoiceChat.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ private void OnSendPrompt()
Name = voice
};
}

try
{
SpeechSynthesis.Speak(utterance, duration =>
Expand All @@ -95,9 +96,7 @@ private void OnSendPrompt()
// The code assumes the concrete implementation for the ISpeechSynthesisService
// service is the concrete Web Assembly type which is not valid. However, the
// alternate API is something MAUI can implement.

SpeechSynthesis.Speak(utterance);

_isReadingResponse = false;
StateHasChanged();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.

namespace SharedWebComponents.Services;

public interface ITextToSpeechPreferencesListener
{
void OnAvailableVoicesChanged(Func<Task> onVoicesChanged);

void UnsubscribeFromAvailableVoicesChanged();
}
1 change: 1 addition & 0 deletions app/frontend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
builder.Services.AddSessionStorageServices();
builder.Services.AddSpeechSynthesisServices();
builder.Services.AddSpeechRecognitionServices();
builder.Services.AddSingleton<ITextToSpeechPreferencesListener, TextToSpeechPreferencesListenerService>();
builder.Services.AddMudServices();
builder.Services.AddTransient<IPdfViewer, WebPdfViewer>();

Expand Down
15 changes: 15 additions & 0 deletions app/frontend/Services/TextToSpeechPreferencesListenerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.JSInterop;

namespace ClientApp.Services;

public sealed class TextToSpeechPreferencesListenerService(
ISpeechSynthesisService speechSynthesisService) : ITextToSpeechPreferencesListener
{
public void OnAvailableVoicesChanged(Func<Task> onVoicesChanged) =>
speechSynthesisService.OnVoicesChanged(onVoicesChanged);

public void UnsubscribeFromAvailableVoicesChanged() =>
speechSynthesisService.UnsubscribeFromVoicesChanged();
}
1 change: 1 addition & 0 deletions app/maui-blazor/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static MauiApp CreateMauiApp()
builder.Services.AddSingleton<ISessionStorageService, MauiSessionStorageService>();
builder.Services.AddSingleton<ISpeechRecognitionService, MauiSpeechRecognitionService>();
builder.Services.AddSingleton<ISpeechSynthesisService, MauiSpeechSynthesisService>();
builder.Services.AddSingleton<ITextToSpeechPreferencesListener, MauiSpeechSynthesisService>();
builder.Services.AddTransient<IPdfViewer, MauiPdfViewer>();

builder.Services.AddMudServices();
Expand Down
4 changes: 3 additions & 1 deletion app/maui-blazor/Services/MauiSpeechRecognitionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

namespace MauiBlazor.Services;

public class MauiSpeechRecognitionService(ISpeechToText speechToText) : ISpeechRecognitionService
public sealed class MauiSpeechRecognitionService(ISpeechToText speechToText) : ISpeechRecognitionService
{
private SpeechRecognitionOperation? _current;

public void CancelSpeechRecognition(bool isAborted)
{
_ = isAborted;
_current?.Dispose();
}

Expand All @@ -19,6 +20,7 @@ public ValueTask DisposeAsync()

public Task InitializeModuleAsync(bool logModuleDetails = true)
{
_ = logModuleDetails;
return Task.CompletedTask;
}

Expand Down
62 changes: 39 additions & 23 deletions app/maui-blazor/Services/MauiSpeechSynthesisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace MauiBlazor.Services;

public class MauiSpeechSynthesisService : ISpeechSynthesisService
public class MauiSpeechSynthesisService(ITextToSpeech textToSpeech)
: ISpeechSynthesisService, ITextToSpeechPreferencesListener
{
private CancellationTokenSource? _cts;
private Task? _speakTask;

public bool Paused => throw new NotImplementedException();

Expand All @@ -28,6 +30,11 @@ public ValueTask<SpeechSynthesisVoice[]> GetVoicesAsync()
return ValueTask.FromResult<SpeechSynthesisVoice[]>([voice]);
}

public void OnAvailableVoicesChanged(Func<Task> onVoicesChanged)
{
_ = onVoicesChanged;
}

public void Pause()
{
Cancel();
Expand All @@ -40,35 +47,44 @@ public void Resume()
// TODO: support pause & resume
}

public async void Speak(SpeechSynthesisUtterance utterance)
public void Speak(SpeechSynthesisUtterance utterance)
{
_cts?.Cancel();
_cts = new();

var current = CultureInfo.CurrentUICulture.Name;

var locales = await TextToSpeech.Default.GetLocalesAsync();
var localeArray = locales.ToArray();
var locale = localeArray.FirstOrDefault(l => current == $"{l.Language}-{l.Country}");
if (locale is null)
_speakTask = Task.Run(async () =>
{
// an exact match was not found, try just the lang
var split = current.Split('-');
if (split.Length == 1 || split.Length == 2)
var current = CultureInfo.CurrentUICulture.Name;

var locales = await textToSpeech.GetLocalesAsync();
var localeArray = locales.ToArray();
var locale = localeArray.FirstOrDefault(l => current == $"{l.Language}-{l.Country}");
if (locale is null)
{
// try the first part (or the whole thing if it is just lang)
locale = localeArray.FirstOrDefault(l => split[0] == $"{l.Language}");
// an exact match was not found, try just the lang
var split = current.Split('-');
if (split.Length is 1 or 2)
{
// try the first part (or the whole thing if it is just lang)
locale = localeArray.FirstOrDefault(l => split[0] == $"{l.Language}");
}
else
{
// just go with the first one
locale = localeArray.FirstOrDefault();
}
}
else

var options = new SpeechOptions
{
// just go with the first one
locale = localeArray.FirstOrDefault();
}
}
Locale = locale
};

var options = new SpeechOptions
{
Locale = locale
};
await TextToSpeech.Default.SpeakAsync(utterance.Text, options, _cts.Token);
await textToSpeech.SpeakAsync(utterance.Text, options, _cts.Token);
}, _cts.Token);
}

public void UnsubscribeFromAvailableVoicesChanged()
{
}
}