Reconhecimento de voz personalizado

Concluído

Se quisermos algo melhor do que o reconhecimento de voz padrão do Windows, teremos de codificar um sistema de reconhecimento de voz específico para a aplicação que se destine a lidar com entradas de frases inteiras.

Isto implica bastante trabalho de codificação, pelo que, em vez de o fazer, pode valer a pena melhorar as propriedades AutomationProperties.Name da sua aplicação e voltar a testar em relação ao utilitário de reconhecimento de voz do Windows. É verdade que já temos um sistema acessível, apesar de um pouco ineficiente, para este método de entrada. Contudo, para obtermos uma entrada de voz verdadeiramente fluida num contexto especializado, precisaremos de criar este sistema personalizado.

Referências

Para obter uma lista completa de comandos, veja Comandos de reconhecimento de voz do Windows.

Obter permissão para receber entradas de um microfone e executar o reconhecimento de voz nessas entradas

Antes de podermos começar a utilizar o reconhecimento de voz personalizado, temos de definir várias permissões e funcionalidades.

  1. No Visual Studio, com o projeto da calculadora carregado, abra o ficheiro Package.appxmainifest e, em seguida, selecione Capabilities (Funcionalidades). Ative a funcionalidade do microfone.

Setting the microphone capability.

  1. A definição desta funcionalidade permite aceder ao feed de áudio do microfone. Guarde e feche o ficheiro de manifesto.

  2. Isto é tudo o que é necessário da aplicação, mas não é tudo o que é necessário para que o reconhecimento de voz funcione. O utilizador tem de ativar o microfone e o reconhecimento de voz para a aplicação, sendo que o último está desativado por predefinição. Durante o teste, o programador também é utilizador, por isso, deve escrever "definições de privacidade" na barra de pesquisa do Windows.

Setting the privacy settings.

  1. Selecione Voz e garanta que Reconhecimento de voz online está ativado. Selecione Microfone e garanta que Permitir que as aplicações acedam ao microfone está ativado. Feche ou minimize a janela das definições.

Tentaremos desativar estas definições numa fase posterior apenas para testar se lidámos corretamente com estas situações na nossa aplicação.

Adicionar código para fazer correspondência entre palavras e expressões e elementos da interface de utilizador

É necessário adicionar bastante código para suportar um utilitário de reconhecimento de voz personalizado, mas vamos começar com as instruções using e as variáveis globais.

  1. Adicione as seguintes instruções using à parte superior do seu código.
using Windows.Media.SpeechRecognition;
using Windows.Media.Capture;
  1. Adicione as seguintes variáveis globais e uma nova enumeração.
        enum eElements
        {
            Button,
            ToggleSwitch,
            Unknown
        }

        bool isRecognitionAvailable;
        SpeechRecognizer speechRecognizer;
  1. Para resolver as questões relacionadas com as permissões acima descritas, adicione a seguinte classe ao seu código. Para verificar todas as permissões necessárias, basta realizar uma chamada a RequestMicrophonePermission. Este código é genérico e pode ser utilizado em qualquer aplicação criada para o Windows 10 com vista ao suporte de permissões de reconhecimento de voz via microfone, embora não suporte a privacidade da Cortana/Ditado.
        public class AudioCapturePermissions
        {
            // If no microphone is present, an exception is thrown with the following HResult value.
            private static readonly int NoCaptureDevicesHResult = -1072845856;

            /// <summary>
            ///  Note that this method only checks the Settings->Privacy->Microphone setting, it does not handle
            /// the Cortana/Dictation privacy check.
            /// </summary>
            /// <returns>True, if the microphone is available.</returns>
            public async static Task<bool> RequestMicrophonePermission()
            {
                try
                {
                    // Request access to the audio capture device.
                    var settings = new MediaCaptureInitializationSettings
                    {
                        StreamingCaptureMode = StreamingCaptureMode.Audio,
                        MediaCategory = MediaCategory.Speech,
                    };
                    var capture = new MediaCapture();

                    await capture.InitializeAsync(settings);
                }
                catch (TypeLoadException)
                {
                    // Thrown when a media player is not available.
                    var messageDialog = new Windows.UI.Popups.MessageDialog("Media player components are unavailable.");
                    await messageDialog.ShowAsync();
                    return false;
                }
                catch (UnauthorizedAccessException)
                {
                    // Thrown when permission to use the audio capture device is denied.
                    var messageDialog = new Windows.UI.Popups.MessageDialog("Permission to use the audio capture device is denied.");
                    await messageDialog.ShowAsync();
                    return false;
                }
                catch (Exception exception)
                {
                    // Thrown when an audio capture device is not present.
                    if (exception.HResult == NoCaptureDevicesHResult)
                    {
                        var messageDialog = new Windows.UI.Popups.MessageDialog("No Audio Capture devices are present on this system.");
                        await messageDialog.ShowAsync();
                        return false;
                    }
                    else
                    {
                        throw;
                    }
                }
                return true;
            }
        }
  1. É boa prática ter uma definição que ative e desative as funcionalidades de reconhecimento de voz. Defina outro botão de ativar/desativar no ficheiro MainPage.xaml. Repare que estamos a definir a tecla aceleradora de teclado L (de "Listener" [Serviço de Escuta, em português]) para acionar o botão de ativar/desativar. Adicione isto imediatamente antes da entrada ListConstants.
        <ToggleSwitch x:Name="ToggleSpeechRecognition"
            Margin="685,409,0,0"
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Header="Speech recognition"
            IsOn="False"
            Toggled="ToggleSpeechRecognition_Toggled">
            <ToggleSwitch.KeyboardAccelerators>
                <KeyboardAccelerator Key="L" Modifiers="None" />
            </ToggleSwitch.KeyboardAccelerators>
        </ToggleSwitch>
  1. Agora defina novamente o evento ToggleSpeechRecognition_Toggled nomeado no xaml, e em alguns métodos de suporte, no ficheiro MainPage.xaml.cs.
        private async Task InitSpeechRecognition()
        {
            isRecognitionAvailable = await AudioCapturePermissions.RequestMicrophonePermission();

            if (isRecognitionAvailable)
            {
                // Create an instance of SpeechRecognizer.
                speechRecognizer = new SpeechRecognizer();

                // Compile the dictation grammar by default.
                await speechRecognizer.CompileConstraintsAsync();

                speechRecognizer.UIOptions.ShowConfirmation = true;
            }
            else
            {
                ToggleSpeechRecognition.IsOn = false;
                isRecognitionAvailable = false;
            }
        }

        private async void ToggleSpeechRecognition_Toggled(object sender, RoutedEventArgs e)
        {
            if (ToggleSpeechRecognition.IsOn)
            {
                await InitSpeechRecognition();
                await StartListening();
            }
            else
            {
                isRecognitionAvailable = false;
            }
        }

        private async Task StartListening()
        {
            if (isRecognitionAvailable)
            {
                try
                {
                    // Start recognition.
                    var speechRecognitionResult = await speechRecognizer.RecognizeWithUIAsync();
                    ParseSpokenCalculationAsync(speechRecognitionResult.Text);

                    // Turn off the Toggle each time.
                    ToggleSpeechRecognition.IsOn = false;
                }
                catch (Exception ex)
                {
                    var messageDialog = new Windows.UI.Popups.MessageDialog(ex.Message);
                    await messageDialog.ShowAsync();
                    ToggleSpeechRecognition.IsOn = false;
                    isRecognitionAvailable = false;
                }
            }
        }
  1. É dada uma cadeia reconhecida por voz como entrada ao método ParseSpokenCalculation. Para processar esta cadeia, precisamos de adicionar um grande segmento de código específico para a aplicação.

Este código utiliza uma frase falada e tenta fazer correspondências entre palavras e expressões dessa frase e os botões, botões de ativar/desativar ou constantes da nossa aplicação. As palavras que não correspondem são ignoradas. O código que se segue é uma abordagem de força bruta ao problema.

Cole o código que se segue na sua aplicação.

        private bool FindConstantFromSpeech(string spokenText, ref string value)
        {
            bool isLocated = false;
            int n = 0;
            string[] nameValue;

            // Remove the word "constant" from the start of the spoken text.
            spokenText = spokenText.Remove(0, spokenText.IndexOf(' ')).Trim();

            while (n < ListConstants.Items.Count && !isLocated)
            {
                nameValue = ListConstants.Items[n].ToString().Split('=');

                if (spokenText == nameValue[0].Trim().ToLower())
                {
                    value = nameValue[1].Trim();
                    isLocated = true;
                }
                else
                {
                    ++n;
                }
            }
            return isLocated;
        }

        private async void SayCurrentCalculationAsync()
        {
            if (TextDisplay.Text.Length == 0)
            {
                await SayAsync("The current calculation is empty.");
            }
            else
            {
                await SayAsync($"The current calculation is: {TextDisplay.Text}.");
            }
        }

        private async void ParseSpokenCalculationAsync(string spokenText)
        {
            spokenText = spokenText.ToLower().Trim();
            if (spokenText.Length == 0)
            {
                return;
            }

            // First check for specific control phrases.
            if (spokenText == "say memory")
            {
                await SayAsync($"The current memory is: {TextMemory.Text}.");
            }
            else
                if (spokenText == "say calculation")
            {
                SayCurrentCalculationAsync();
            }
            else
                 if (spokenText.StartsWith("const"))
            {
                string value = "";
                if (FindConstantFromSpeech(spokenText, ref value))
                {
                    MathEntry(value, "Number");
                    SayCurrentCalculationAsync();
                }
                else
                {
                    await SayAsync("Sorry, I did not recognize that constant.");
                }
            }
            else
            {
                // Ensure + is a word in its own right.
                // Sometimes the speech recognizer will enter "+N" and we need "+ N".
                spokenText = spokenText.Replace("+", "+ ");
                spokenText = spokenText.Replace("  ", " ");

                double d;
                string[] words = spokenText.Split(' ');
                int w = 0;
                ToggleSwitch ts;
                object obj;
                var eType = eElements.Unknown;

                while (w < words.Length)
                {
                    try
                    {
                        // Is the word a number?
                        d = double.Parse(words[w]);
                        MathEntry(d.ToString(), "Number");
                    }
                    catch
                    {
                        try
                        {
                            // Is the word a ratio?
                            string[] ratio = words[w].Split('/');
                            d = double.Parse(ratio[0]) / double.Parse(ratio[1]);
                            MathEntry(d.ToString(), "Number");
                        }
                        catch
                        {
                            // Check if a word or phrase refers to a button, test phrases up to 4 words long.
                            // There are only buttons in gridButtons, so no need to test for anything else.
                            obj = FindElementFromString(GridButtons.Children, words, w, 4, ref w, ref eType);
                            if (obj != null)
                            {
                                Button_Click(obj, null);
                            }
                            else
                            {
                                // Controls can be up to three words in our app.
                                obj = FindElementFromString(GridCalculator.Children, words, w, 3, ref w, ref eType);
                                if (obj != null)
                                {
                                    switch (eType)
                                    {
                                        case eElements.Button:
                                            Button_Click(obj, null);
                                            break;

                                        case eElements.ToggleSwitch:
                                            ts = (ToggleSwitch)obj;
                                            ts.IsOn = !ts.IsOn;
                                            break;

                                        default:
                                            break;
                                    }
                                }
                            }
                        }
                    }
                    ++w;
                }
                if (mode != Emode.CalculateDone)
                {
                    SayCurrentCalculationAsync();
                }
            }
        }

        private bool IsMatchingElementText(eElements elementType, object obj, string textToMatch)
        {
            string name = "";
            string accessibleName = "";

            switch (elementType)
            {
                case eElements.Button:
                    var b = (Button)obj;
                    name = b.Content.ToString().ToLower();
                    accessibleName = b.GetValue(AutomationProperties.NameProperty).ToString().ToLower();
                    break;

                case eElements.ToggleSwitch:
                    var ts = (ToggleSwitch)obj;
                    name = ts.Header.ToString().ToLower();
                    accessibleName = ts.GetValue(AutomationProperties.NameProperty).ToString().ToLower();
                    break;
            }

            // Return true if the name or accessibleName matches the spoken text.
            if ((textToMatch == name && name.Length > 0) || (textToMatch == accessibleName && accessibleName.Length > 0))
            {
                return true;
            }

            return false;
        }

        private object FindElementFromString(UIElementCollection elements, string[] words, int startIndex, int maxConcatenatedWords, ref int updatedIndex, ref eElements elementType)
        {
            // Return true if the spoken text matches the text for a button.
            int n;
            Button b;
            ToggleSwitch ts;

            // Longer phrazes take precendence over shorter ones, so start with the longest allowed and work down.
            for (int c = maxConcatenatedWords; c > 0; c--)
            {
                if (startIndex + c - 1 < words.Length)
                {
                    // Build the phraze from the following words.
                    string txt = words[startIndex];
                    for (n = 1; n < c; n++)
                    {
                        txt += " " + words[startIndex + n];
                    }

                    // Test the word or phrase against the content/tag/name of each button.
                    for (int i = 0; i < elements.Count; i++)
                    {
                        // Is the UI element a button?
                        try
                        {
                            b = (Button)elements[i];
                            if (IsMatchingElementText(eElements.Button, (object)b, txt))
                            {
                                updatedIndex = startIndex + c - 1;
                                elementType = eElements.Button;
                                return (object)b;
                            }
                        }
                        catch
                        {
                            // UI element is not a button, is it a ToggleSwitch?
                            try
                            {
                                ts = (ToggleSwitch)elements[i];
                                if (IsMatchingElementText(eElements.ToggleSwitch, (object)ts, txt))
                                {
                                    updatedIndex = startIndex + c - 1;
                                    elementType = eElements.ToggleSwitch;
                                    return (object)ts;
                                }
                            }
                            catch
                            {
                                // Ignore the UI element.
                            }
                        }
                    }
                }
            }
            updatedIndex = startIndex;
            return null;
        }

Nota

Para introduzir uma constante, diga "constant" (constante) e, em seguida, diga o nome completo da constante. Dizer "memory" (memória) permitirá enunciar o conteúdo da memória. Dizer "calculation" (cálculo) permitirá enunciar o conteúdo do cálculo atual.

  1. Compile e execute a aplicação e ative o reconhecimento de fala através do respetivo botão de ativar/desativar.

  2. Com o microfone pronto, diga "what is 1.23456 times 2.789" (1,23456 vezes 2,789 é igual a). O que diz deve aparecer numa caixa de diálogo chamada A ouvir, que se fechará quase imediatamente depois de parar de falar. Em seguida, o cálculo deve aparecer no ecrã.

Speaking a natural addition.

Nota

Se a caixa de diálogo "A ouvir" apresentar a mensagem Não entendi, prima a barra de espaço para aceder novamente ao Serviço de Escuta.

  1. Tente introduzir vários cálculos simples com a sua voz.

Nota

Basta premir a tecla L e dizer "clear" (apagar) para apagar um cálculo errático.

  1. Os cálculos podem ser proferidos por partes, dado que uma breve pausa é suficiente para que o Serviço de Escuta feche e a entrada seja analisada. Por exemplo, diga "what is sine 30 times" (o seno 30 vezes é igual a). Em seguida, prima a tecla L e, quando o Serviço de Escuta voltar a aparecer, diga "the cosine of 30" (o cosseno de 30). Depois, prima novamente L e diga "equals" (é igual a). Deve obter o resultado.

  2. Repare que as palavras irrelevantes como "what is" (o), "of" (de) e "the" (a) podem ser incluídas na frase, mas são corretamente ignoradas.

  3. Tente inventar equações (não há problema se não fizerem sentido no âmbito da Matemática) que testem todos os botões e botões de ativar/desativar, incluindo a tecla Ctrl, a tecla Delete, os botões de armazenamento de memória e as constantes. Isto deve ajudar a garantir que o código está a lidar devidamente com todos eles.