Skip to content
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

Speech Recognition v2 - StartListening/StopListening #1382

Merged
merged 33 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5172070
Speech Recognition v2
VladislavAntonyuk Sep 4, 2023
7048308
Merge branch 'main' into speech-recognition-v2
VladislavAntonyuk Sep 6, 2023
58ad063
Fix tizen
VladislavAntonyuk Sep 6, 2023
912357e
Merge branch 'speech-recognition-v2' of https://github.com/CommunityT…
VladislavAntonyuk Sep 6, 2023
ce22dca
Fix tizen
VladislavAntonyuk Sep 6, 2023
c14a5e8
Fix PR comments
VladislavAntonyuk Sep 8, 2023
6552e04
Merge branch 'main' into speech-recognition-v2
VladislavAntonyuk Sep 8, 2023
82dc58d
Add tests
VladislavAntonyuk Sep 8, 2023
34ec3c1
Merge branch 'speech-recognition-v2' of https://github.com/CommunityT…
VladislavAntonyuk Sep 8, 2023
8412930
Fix tests
VladislavAntonyuk Sep 8, 2023
b0275d3
Merge branch 'main' into speech-recognition-v2
brminnick Sep 9, 2023
d0c85f5
Rename `ISpeechToText.State` -> `ISpeechToText.CurrentState`
brminnick Sep 9, 2023
f576493
Update Layout
brminnick Sep 9, 2023
4aaf087
Add `ISpeechToText.StateChanged`
brminnick Sep 9, 2023
61d1895
Add Missing CancellationToken
brminnick Sep 9, 2023
085b53c
Update Sample App
brminnick Sep 9, 2023
85352d7
Update SpeechToTextPage.xaml
brminnick Sep 9, 2023
6e00d06
`dotnet format`
brminnick Sep 9, 2023
2f50edb
Add tests, update CurrentState
VladislavAntonyuk Sep 10, 2023
4103ba5
Merge branch 'main' into speech-recognition-v2
brminnick Sep 15, 2023
efcd439
Add Missing XML
brminnick Sep 16, 2023
b0b8338
`dotnet format`
brminnick Sep 16, 2023
60c59cb
Update Formatting
brminnick Sep 16, 2023
e9d03a0
Add Missing Cancellation Usage, Update `SpeechToTextImplementation.ge…
brminnick Sep 16, 2023
5212804
Add `ResetSpeechRecognitionTaskCompletionSource()`
brminnick Sep 16, 2023
7eda8ff
Update SpeechToTextImplementation.tizen.cs
brminnick Sep 16, 2023
25906ad
`dotnet format`
brminnick Sep 16, 2023
35e23a5
Merge branch 'main' into speech-recognition-v2
brminnick Sep 18, 2023
0bcfd57
Dispose of `CancellationTokenRegistration`
brminnick Sep 19, 2023
6c6fed1
Merge branch 'speech-recognition-v2' of https://github.com/CommunityT…
brminnick Sep 19, 2023
b02fe3b
Add StateChanged impl on Tizen
JoonghyunCho Sep 22, 2023
09ca1e9
Merge branch 'main' into speech-recognition-v2
brminnick Sep 22, 2023
40a17d1
Merge branch 'main' into speech-recognition-v2
brminnick Sep 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@
CS1574: XML comment has cref attribute that could not be resolved
CS1580: Invalid type for parameter 'parameter number' in XML comment cref attribute
CS1581: Invalid return type in XML comment cref attribute
CS1584: XML comment has syntactically incorrect cref attribute
CS1584: XML comment has syntactically incorrect cref attribute
CS1587: XML comment is not placed on a valid language element
CS1589: The syntax of a tag which referenced a file was incorrect
CS1590: Invalid XML include element Missing file attribute
CS1591: Missing XML comment for publicly visible type or member
CS1592: Badly formed XML in included comments file
CS1598: XML parser could not be loaded. The XML documentation file will not be generated.
CS1658: Identifier expected; 'true' is a keyword
CS1658: Identifier expected; 'true' is a keyword
CS1710: XML comment on 'type' has a duplicate typeparam tag for 'parameter'
CS1711: XML comment has a typeparam tag, but there is no type parameter by that name
CS1712: Type parameter has no matching typeparam tag in the XML comment
CS1723: XML comment has cref attribute that refers to a type parameter
CS1734: XML comment has a paramref tag, but there is no parameter by that name -->
<WarningsAsErrors>nullable,CS0419,CS1570,CS1571,CS1572,CS1573,CS1574,CS1580,CS1581,CS1584,CS1589,CS1590,CS1592,CS1598,CS1658,CS1734</WarningsAsErrors>
<WarningsAsErrors>nullable,CS0419,CS1570,CS1571,CS1572,CS1573,CS1574,CS1580,CS1581,CS1584,CS1587,CS1589,CS1590,CS1591,CS1592,CS1598,CS1658,CS1710,CS1711,CS1712,CS1723,CS1734</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,115 @@
</ContentPage.Resources>

<ScrollView>
<Grid
RowDefinitions="64, 24, 64, 24, auto, 60, 60, 60"
<VerticalStackLayout
Spacing="20"
Padding="30,0">

<Label
Grid.Row="0"
Text="SpeechToText allows the user to convert speech to text in real time"/>
Text="SpeechToText allows the user to convert speech to text in real time"
HorizontalTextAlignment="Center"/>

<Label
Grid.Row="1"
Text="Locale"
FontAttributes="Bold"/>

<Picker
Grid.Row="2"
ItemsSource="{Binding Locales}"
SelectedItem="{Binding CurrentLocale}"
ItemDisplayBinding="{Binding ., Converter={StaticResource PickerLocaleDisplayConverter}}"
Margin="0,0,0,20">
</Picker>
ItemDisplayBinding="{Binding ., Converter={StaticResource PickerLocaleDisplayConverter}}"/>

<Label
Grid.Row="3"
Text="Language Output"
FontAttributes="Bold"/>

<Label
Grid.Row="4"
Text="{Binding RecognitionText}"
FontSize="18"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
MinimumHeightRequest="100"
Margin="0,0,0,20" />
MinimumHeightRequest="100" />

<Button
Grid.Row="5"
Text="Play"
Command="{Binding PlayCommand}"
HorizontalOptions="Center"
Margin="0,0,0,20"/>
HorizontalOptions="Center" />

<Button
Grid.Row="6"
Text="Listen"
Command="{Binding ListenCommand}"
HorizontalOptions="Center"
Margin="0,0,0,20"/>
<Border
StrokeThickness="2"
Stroke="#808080"
StrokeShape="RoundRectangle 8,8,8,8"
Padding="12">
<Border.Content>

<Button
Grid.Row="7"
Text="Stop Listening"
Command="{Binding ListenCancelCommand}"
HorizontalOptions="Center"/>
<Grid RowDefinitions="*,60"
ColumnDefinitions="*,*"
RowSpacing="12"
ColumnSpacing="12">

<Button
Grid.Row="0"
Grid.Column="0"
Text="ListenAsync"
Command="{Binding ListenCommand}"
HorizontalOptions="End" />

<Button
Grid.Row="0"
Grid.Column="1"
Text="Cancel Token"
Command="{Binding ListenCancelCommand}"
HorizontalOptions="Start" />

<Label
Grid.Row="1"
Grid.ColumnSpan="2"
Text="The `ListenAsync` API allows you to await the final speech recognition results using async/await. `ListenAsync` is cancelled via CancellationToken."
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
FontSize="12"/>

</Grid>
</Grid>
</Border.Content>
</Border>

<Border
StrokeThickness="2"
Stroke="#808080"
StrokeShape="RoundRectangle 8,8,8,8"
Padding="12">
<Border.Content>
<Grid RowDefinitions="*,60"
ColumnDefinitions="*,*"
RowSpacing="12"
ColumnSpacing="12">

<Button
Grid.Row="0"
Grid.Column="0"
Text="StartListenAsync"
Command="{Binding StartListenCommand}"
HorizontalOptions="End" />

<Button
Grid.Row="0"
Grid.Column="1"
Text="StopListenAsync"
Command="{Binding StopListenCommand}"
HorizontalOptions="Start" />

<Label
Grid.Row="1"
Grid.ColumnSpan="2"
Text="The `StartListenAsync` API starts the speech-to-text service and shares the results using `RecognitionResultUpdated` event and `RecognitionResultCompleted` event."
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
FontSize="12"/>

</Grid>
</Border.Content>
</Border>

</VerticalStackLayout>
</ScrollView>

</pages:BasePage>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Maui.Dispatching;

namespace CommunityToolkit.Maui.Sample.ViewModels.Converters;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,23 @@ public partial class SpeechToTextViewModel : BaseViewModel
[ObservableProperty]
string? recognitionText = "Welcome to .NET MAUI Community Toolkit!";

[ObservableProperty, NotifyCanExecuteChangedFor(nameof(ListenCommand))]
bool canListenExecute = true;

[ObservableProperty, NotifyCanExecuteChangedFor(nameof(StartListenCommand))]
bool canStartListenExecute = true;

[ObservableProperty, NotifyCanExecuteChangedFor(nameof(StopListenCommand))]
bool canStopListenExecute = false;

public SpeechToTextViewModel(ITextToSpeech textToSpeech, ISpeechToText speechToText)
{
this.textToSpeech = textToSpeech;
this.speechToText = speechToText;

Locales.CollectionChanged += HandleLocalesCollectionChanged;
this.speechToText.StateChanged += HandleSpeechToTextStateChanged;
this.speechToText.RecognitionResultCompleted += HandleRecognitionResultCompleted;
}

public ObservableCollection<Locale> Locales { get; } = new();
Expand Down Expand Up @@ -59,9 +70,63 @@ await textToSpeech.SpeakAsync(RecognitionText ?? "Welcome to .NET MAUI Community
}, cancellationToken);
}

[RelayCommand(IncludeCancelCommand = true)]
[RelayCommand(IncludeCancelCommand = true, CanExecute = nameof(CanListenExecute))]
async Task Listen(CancellationToken cancellationToken)
{
CanStartListenExecute = false;

try
{
var isGranted = await speechToText.RequestPermissions(cancellationToken);
if (!isGranted)
{
await Toast.Make("Permission not granted").Show(CancellationToken.None);
return;
}

const string beginSpeakingPrompt = "Begin speaking...";

RecognitionText = beginSpeakingPrompt;

var recognitionResult = await speechToText.ListenAsync(
CultureInfo.GetCultureInfo(CurrentLocale?.Language ?? defaultLanguage),
new Progress<string>(partialText =>
{
if (RecognitionText is beginSpeakingPrompt)
{
RecognitionText = string.Empty;
}

RecognitionText += partialText + " ";
}), cancellationToken);

if (recognitionResult.IsSuccessful)
{
RecognitionText = recognitionResult.Text;
}
else
{
await Toast.Make(recognitionResult.Exception?.Message ?? "Unable to recognize speech").Show(CancellationToken.None);
}

if (RecognitionText is beginSpeakingPrompt)
{
RecognitionText = string.Empty;
}
}
finally
{
CanStartListenExecute = true;
}
}

[RelayCommand(CanExecute = nameof(CanStartListenExecute))]
async Task StartListen(CancellationToken cancellationToken)
{
CanListenExecute = false;
CanStartListenExecute = false;
CanStopListenExecute = true;

var isGranted = await speechToText.RequestPermissions(cancellationToken);
if (!isGranted)
{
Expand All @@ -73,33 +138,43 @@ async Task Listen(CancellationToken cancellationToken)

RecognitionText = beginSpeakingPrompt;

var recognitionResult = await speechToText.ListenAsync(
CultureInfo.GetCultureInfo(CurrentLocale?.Language ?? defaultLanguage),
new Progress<string>(partialText =>
{
if (RecognitionText is beginSpeakingPrompt)
{
RecognitionText = string.Empty;
}

RecognitionText += partialText + " ";
}), cancellationToken);
await speechToText.StartListenAsync(CultureInfo.GetCultureInfo(CurrentLocale?.Language ?? defaultLanguage), cancellationToken);

if (recognitionResult.IsSuccessful)
{
RecognitionText = recognitionResult.Text;
}
else
{
await Toast.Make(recognitionResult.Exception?.Message ?? "Unable to recognize speech").Show(CancellationToken.None);
}
speechToText.RecognitionResultUpdated += HandleRecognitionResultUpdated;

if (RecognitionText is beginSpeakingPrompt)
{
RecognitionText = string.Empty;
}
}

[RelayCommand(CanExecute = nameof(CanStopListenExecute))]
Task StopListen(CancellationToken cancellationToken)
{
CanListenExecute = true;
CanStartListenExecute = true;
CanStopListenExecute = false;

speechToText.RecognitionResultUpdated -= HandleRecognitionResultUpdated;

return speechToText.StopListenAsync(cancellationToken);
}

void HandleRecognitionResultUpdated(object? sender, SpeechToTextRecognitionResultUpdatedEventArgs e)
{
RecognitionText += e.RecognitionResult;
}

void HandleRecognitionResultCompleted(object? sender, SpeechToTextRecognitionResultCompletedEventArgs e)
{
RecognitionText = e.RecognitionResult;
}

async void HandleSpeechToTextStateChanged(object? sender, SpeechToTextStateChangedEventArgs e)
{
await Toast.Make($"State Changed: {e.State}").Show(CancellationToken.None);
}

void HandleLocalesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(CurrentLocale));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace CommunityToolkit.Maui.Media;

/// <summary>
/// <see cref="EventArgs"/> for <see cref="ISpeechToText.RecognitionResultCompleted"/>
/// </summary>
public class SpeechToTextRecognitionResultCompletedEventArgs : EventArgs
{
/// <summary>
/// Initialize a new instance of <see cref="SpeechToTextRecognitionResultCompletedEventArgs"/>
/// </summary>
public SpeechToTextRecognitionResultCompletedEventArgs(string recognitionResult)
{
RecognitionResult = recognitionResult;
}

/// <summary>
/// Speech recognition result
/// </summary>
public string RecognitionResult { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace CommunityToolkit.Maui.Media;

/// <summary>
/// <see cref="EventArgs"/> for <see cref="ISpeechToText.RecognitionResultUpdated"/>
/// </summary>
public class SpeechToTextRecognitionResultUpdatedEventArgs : EventArgs
{
/// <summary>
/// Initialize a new instance of <see cref="SpeechToTextRecognitionResultUpdatedEventArgs"/>
/// </summary>
public SpeechToTextRecognitionResultUpdatedEventArgs(string recognitionResult)
{
RecognitionResult = recognitionResult;
}

/// <summary>
/// Speech recognition result
brminnick marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public string RecognitionResult { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace CommunityToolkit.Maui.Media;

/// <summary>
/// <see cref="EventArgs"/> for <see cref="ISpeechToText.StateChanged"/>
/// </summary>
public class SpeechToTextStateChangedEventArgs : EventArgs
{
/// <summary>
/// Initialize a new instance of <see cref="SpeechToTextStateChangedEventArgs"/>
/// </summary>
public SpeechToTextStateChangedEventArgs(SpeechToTextState state)
{
State = state;
}

/// <summary>
/// Speech To Text State
/// </summary>
public SpeechToTextState State { get; }
}