Guida al Visual Basic .NET
Capitolo 105° - Riconoscimento vocale
Costruire la grammatica
Per il riconoscimento vocale, la faccenda si fa un po' più complicata. L'oggetto principale su cui si regge tutto il capitolo è
System.Speech.Recognition.SpeechRecognitionEngine. Non analizzerò in dettaglio i suoi membri, poiché la gran parte di essi
verrà spiegata nel codice che scriverò dopo: basterà dire che per l'inizializzazione, anch'esso dispone di metodi
SetInpuTo... identici a quelli di SpeechSynthesizer. Imports System.Speech Imports System.Speech.Recognition Imports System.Speech.Synthesis '... Dim GrammarBuilder As New GrammarBuilder GrammarBuilder.Append(New Choices("one", "two", "three", "four")) In questo modo si è aggiunta al GrammarBuilder una gamma di parole possibili: one, two, three e four. Usando un oggetto Choices comunichiamo all'engine che ogni parola può essere presa singolarmente. Ho usato dei numeri perchè l'esempio di questo capitolo sarà un programma in grado di riconoscere un numero pronunciato in inglese. A questo proposito, bisogna dire che l'oggetto GrammarBuilder può essere creato per differenti lingue (anche se non ci sono ancora pacchetti per molte lingue diverse), ma esso da solo non è in grado di riconoscere a quale lingua appartengano le parole che il programmatore inserisce: per questo prende come nazionalità di default quella del computer su cui sta correndo. Per il 99% di coloro che leggono questa guida, quindi, GrammarBuilder considererà la cultura corrente come Italiana, poiché il computer ha installata quella. Tuttavia l'engine che useremo è impostato solo per la lingua inglese e questo genererà sicuramente un errore. Perciò, prima di inserire la grammatica di GrammarBuilder nell'engine di riconoscimento vocale, dobbiamo specificare esplicitamente che si tratta di inglese: GrammarBuilder.Culture = Globalization.CultureInfo.GetCultureInfo("en-US") Una volta fatto questo, per formare una nuova grammatica usabile (un insieme di parole in questo caso) bisogna creare un nuovo oggetto Grammar e passare al costruttore GrammarBuilder come parametro: Dim Grammar As Grammar '... Grammar = New Grammar(GrammarBuilder) Ora manca la cosa più importante: l'engine di riconoscimento vocale. Useremo la classe SpeechRecognitionEngine, che descriverò direttamente nei commenti del prossimo esempio. Esempio: Tell me how muchPer questo esempio basta un semplicissimo form, con una label (lblNumber) e due pulsanti (btnStart e btnStop). Questo è il codice: Imports System.Speech Imports System.Speech.Recognition Imports System.Speech.Synthesis Public Class Form2 'Nuovo Engine di riconoscimento vocale Private Engine As New SpeechRecognitionEngine 'GrammarBuilder per costruire la grammatica Private GrammarBuilder As New GrammarBuilder 'Oggetto Grammar che rappresenta la grammatica Private Grammar As Grammar 'Questo dizionario associa ad ogni parola il 'corrispondente valore numerico (one=1) Private TextNumber As Dictionary(Of String, Int32) 'Questo array già inizializzato contiene l'elenco di 'tutte le parole che l'engine può rilevare Private Numbers As String() = _ New String() {"one", "two", "three", "four", _ "five", "six", "seven", "eight", "nine", "ten", _ "eleven", "twelve", "thirteen", "fourteen", _ "fiftheen", "sixteen", "seventeen", "eighteen", _ "nineteen", "twenty", "thirty", "fourty", "fifty", _ "sixty", "seventy", "eighty", "ninty", "hundred", _ "thousand", "reset"} 'L'ultima parola, reset, serve per porre a 0 il 'conteggio, nel caso si volesse ripetere 'Prev ricorda l'ultimo numero immesso Private Prev As Int32 'Result contiene il numero finale Private Result As Int32 Private Sub Form2_Load(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Load 'All'avvio del form, si imposta l'input dell'engine sul 'normale microfono (che deve essere collegato al computer). 'Anche in questo caso si usa un thread, per lo stesso 'motivo citato nel capitolo precedente Dim T As New Threading.Thread( _ AddressOf Engine.SetInputToDefaultAudioDevice) T.Start() T.Join() 'Poi si genera il dizionario che associa le parole ai 'valori numerici veri e propri. Dato che l'array 'Numbers contiene i numeri in ordine, sfruttermo 'qualche for per riempire il dizionario in poche 'righe di codice TextNumber = New Dictionary(Of String, Int32) With TextNumber 'I primi 20 numeri sono in ordine crescente, 'da 1 a 20. Perciò basta aggiungere 1 'all'indice I per ottenere il numero che la 'parola indica For I As Int16 = 0 To 19 .Add(Numbers(I), I + 1) Next 'I successivi sette numeri sono tutti i multipli 'di 10, da 30 a 90. Con la formula: '(I-19)*10 + 20 'è come se I andasse da 1 a 7 e quindi 'otteniamo tutte le decine da 20+10 a 20+70 For I As Int16 = 20 To 26 .Add(Numbers(I), (I - 19) * 10 + 20) Next 'Infine si aggiungono centinaia e migliaia a parte .Add("hundred", 100) .Add("thousand", 1000) End With 'Aggiunge tutte le parole-numero al GrammarBuilder GrammarBuilder.Append(New Choices(Numbers)) 'Imposta la lingua a inglese GrammarBuilder.Culture = _ Globalization.CultureInfo.GetCultureInfo("en-US") 'Costruisce la nuova "grammatica" con il GrammarBuilder Grammar = New Grammar(GrammarBuilder) 'Questo metodo serve per eliminare tutte le grammatiche 'già presenti. Anche se quasi sicuramente non ci 'sarà nessun grammatica precaricata, è sempre 'meglio farlo prima di aggiungerne di nuove Engine.UnloadAllGrammars() 'Quindi carica la grammatica Grammar. Ora Engine è in 'grado di riconoscere le parole dell'array Numbers Engine.LoadGrammar(Grammar) 'Parte importantissima: aggiunge l'handler di evento per 'l'evento SpeechRecognized, che viene lanciato quando 'l'engine ha ascoltato la voce, l'ha analizzata e ha 'trovato una corrispondenza valida nella sua grammatica AddHandler Engine.SpeechRecognized, AddressOf Speech_Recognized End Sub Private Sub btnStart_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles btnStart.Click 'Fa partire il riconoscimento vocale. Il metodo è asincrono, 'quindi viene eseguito su un altro thread e non blocca il form 'chiamante. L'argomento Multiple indica che si effetteranno più 'riconoscimenti e non uno solo Engine.RecognizeAsync(RecognizeMode.Multiple) 'Disabilita Start e abilita Stop btnStart.Enabled = False btnStop.Enabled = True End Sub Private Sub btnStop_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles btnStop.Click 'Termina il riconoscimento asincrono Engine.RecognizeAsyncCancel() 'Abilita Start e disabilita Stop btnStart.Enabled = True btnStop.Enabled = False End Sub Private Sub Speech_Recognized(ByVal sender As Object, _ ByVal e As SpeechRecognizedEventArgs) Dim N As Int32 'Ottiene il testo, ossia la parola pronunciata Dim Text As String = e.Result.Text 'Se il testo è "reset", annulla tutto If Text = "reset" Then Result = 0 End If 'Se il testo è contenuto nel dizionario, allora 'è un numero valido If TextNumber.ContainsKey(Text) Then 'Ottiene il numero N = TextNumber(Text) 'Se è 100, significa che si è pronunciato '"hundred". Hundred indica le centinaia e perciò 'sicuramente non si può dire "twenty hundred", né '"one thousand hundred": l'unico caso in cui si può 'usare hundred è dopo una singola cifra, ad esempio '"one hundred" o "nine hundred". Quindi controlla che il 'numero precedente sia compreso tra 1 e 9 If (N = 100) And (Prev > 0 And Prev < 10) Then 'Toglie l'unità Result -= Prev 'E la trasforma in centinaia Result += Prev * 100 End If 'Parimenti, si può usare "thousand" solo dopo un 'numero minore di mille. Anche se lecito, nessuno direbbe '"a thousand thousand", ma piuttosto "a million" If (N = 1000) And (Result < 1000) Then Result *= 1000 End If 'Se il numero è minore di 100, semplicemente lo 'aggiunge. Se quindi si pronunciano "twenty" e "thirty" 'di seguito, si otterà 50. Non chiedetemi perchè 'l'ho fatto così... If (N < 100) Then Result += N End If Else N = 0 End If Prev = N 'Imposta il testo della label lblNumber.Text = String.Format("{0:N0}", Result) End Sub End Class Eseguendo questo codice e parlando bene nel microfono, si dovrebbe ottenere un risultato discreto (alcune parole, comunque, si confondono). Nonostante il codice sia esatto, tuttavia, System.Speech rimane un namespace strano, poiché le sue classi generano spesso errori incomprensibili. Se siete stati così fortunati da aver riecevuto, come me, una TargetInvocationException nell'evento SpeechRecognized, mi sarete grati per il codice che propongo qui, un'alternativa più di quella sopra, ma almeno più sicura: Imports System.Speech Imports System.Speech.Recognition Imports System.Speech.Synthesis Public Class Form2 'Nuovo Engine di riconoscimento vocale Private Engine As New SpeechRecognitionEngine 'GrammarBuilder per costruire la grammatica Private GrammarBuilder As New GrammarBuilder 'Oggetto Grammar che rappresenta la grammatica Private Grammar As Grammar 'Questo dizionario associa ad ogni parola il 'corrispondente valore numerico (one=1) Private TextNumber As Dictionary(Of String, Int32) 'Questo array già inizializzato contiene l'elenco di 'tutte le parole che l'engine può rilevare Private Numbers As String() = _ New String() {"one", "two", "three", "four", _ "five", "six", "seven", "eight", "nine", "ten", _ "eleven", "twelve", "thirteen", "fourteen", _ "fiftheen", "sixteen", "seventeen", "eighteen", _ "nineteen", "twenty", "thirty", "fourty", "fifty", _ "sixty", "seventy", "eighty", "ninty", "hundred", _ "thousand", "reset"} 'L'ultima parola, reset, serve per porre a 0 il 'conteggio, nel caso si volesse ripetere 'Delegato che servirà dopo Private Delegate Sub SetLabel(ByVal Res As RecognitionResult) 'Prev ricorda l'ultimo numero immesso Private Prev As Int32 'Result contiene il numero finale Private Result As Int32 Private Sub Form2_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Load 'All'avvio del form, si imposta l'input dell'engine sul 'normale microfono (che deve essere collegato al computer). 'Anche in questo caso si usa un thread, per lo stesso 'motivo citato nel capitolo precedente Dim T As New Threading.Thread( _ AddressOf Engine.SetInputToDefaultAudioDevice) T.Start() T.Join() 'Poi si genera il dizionario che associa le parole ai 'valori numerici veri e propri. Dato che l'array 'Numbers contiene i numeri in ordine, sfruttermo 'qualche for per riempire il dizionario in poche 'righe di codice TextNumber = New Dictionary(Of String, Int32) With TextNumber 'I primi 20 numeri sono in ordine crescente, 'da 1 a 20. Perciò basta aggiungere 1 'all'indice I per ottenere il numero che la 'parola indica For I As Int16 = 0 To 19 .Add(Numbers(I), I + 1) Next 'I successivi sette numeri sono tutti i multipli 'di 10, da 30 a 90. Con la formula: '(I-19)*10 + 20 'è come se I andasse da 1 a 7 e quindi 'otteniamo tutte le decine da 20+10 a 20+70 For I As Int16 = 20 To 26 .Add(Numbers(I), (I - 19) * 10 + 20) Next 'Infine si aggiungono centinaia e migliaia a parte .Add("hundred", 100) .Add("thousand", 1000) End With 'Aggiunge tutte le parole-numero al GrammarBuilder GrammarBuilder.Append(New Choices(Numbers)) 'Imposta la lingua a inglese GrammarBuilder.Culture = Globalization.CultureInfo.GetCultureInfo("en-US") 'Costruisce la nuova "grammatica" con il GrammarBuilder Grammar = New Grammar(GrammarBuilder) 'Questo metodo serve per eliminare tutte le grammatiche 'già presenti. Anche se quasi sicuramente non ci 'sarà nessun grammatica precaricata, è sempre 'meglio farlo prima di aggiungerne di nuove Engine.UnloadAllGrammars() 'Quindi carica la grammatica Grammar. Ora Engine è in 'grado di riconoscere le parole dell'array Numbers Engine.LoadGrammar(Grammar) 'Parte importantissima: aggiunge l'handler di evento per 'l'evento SpeechRecognized, che viene lanciato quando 'l'engine ha ascoltato la voce, l'ha analizzata e ha 'trovato una corrispondenza valida nella sua grammatica AddHandler Engine.SpeechRecognized, AddressOf Speech_Recognized End Sub Private Sub btnStart_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles btnStart.Click 'Fa partire il riconoscimento vocale. Il metodo è asincrono, 'quindi viene eseguito su un altro thread e non blocca il form 'chiamante. L'argomento Multiple indica che si effetteranno più 'riconoscimenti e non uno solo Engine.RecognizeAsync(RecognizeMode.Multiple) 'Disabilita Start e abilita Stop btnStart.Enabled = False btnStop.Enabled = True End Sub Private Sub btnStop_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles btnStop.Click 'Termina il riconoscimento asincrono Engine.RecognizeAsyncCancel() 'Abilita Start e disabilita Stop btnStart.Enabled = True btnStop.Enabled = False End Sub Private Sub Speech_Recognized(ByVal sender As Object, _ ByVal e As SpeechRecognizedEventArgs) 'Può capitare che dopo l'esecuzione di questo evento, 'sia generata un'eccezione TargetInvocationException, causata 'dall'engine, il quale lancia un evento uguale prima che 'questo sia terminato. Usando un thread risolviamo tutto Dim T As New Threading.Thread(AddressOf InvokeSetLabel) T.Start(e.Result) End Sub Private Sub InvokeSetLabel(ByVal Res As RecognitionResult) 'Ovviamente questi stupido tipo di errori ci fa usare 'una via alternativa sprecando molto codice in più. 'Dato che, come sapete, non si può accedere ai 'controlli di un form da un thread differente da quello 'in cui sono stati creati, dobbiamo usare Invoke 'per far eseguire lo stesso compito al thread principale 'partendo da questo thread secondario. 'Per chi non si ricorda i delegate, Invoke permette di 'far correre un metodo nel thread dell'oggetto da cui è 'richiamato (Me, ossia il form). In questo caso usiamo 'il delegato di tipo SetLabel che punta ad AnalyuzeText 'e gli passiamo dierettamente Res come parametro Me.Invoke(New SetLabel(AddressOf AnalyzeText), _ New Object() {Res}) End Sub Private Sub AnalyzeText(ByVal Res As RecognitionResult) Dim N As Int32 'Ottiene il testo, ossia la parola pronunciata Dim Text As String = Res.Text 'Se il testo è "reset", annulla tutto If Text = "reset" Then Result = 0 End If 'Se il testo è contenuto nel dizionario, allora 'è un numero valido If TextNumber.ContainsKey(Text) Then 'Ottiene il numero N = TextNumber(Text) 'Se è 100, significa che si è pronunciato '"hundred". Hundred indica le centinaia e perciò 'sicuramente non si può dire "twenty hundred", né '"one thousand hundred": l'unico caso in cui si può 'usare hundred è dopo una singola cifra, ad esempio '"one hundred" o "nine hundred". Quindi controlla che il 'numero precedente sia compreso tra 1 e 9 If (N = 100) And (Prev > 0 And Prev < 10) Then 'Toglie l'unità Result -= Prev 'E la trasforma in centinaia Result += Prev * 100 End If 'Parimenti, si può usare "thousand" solo dopo un 'numero minore di mille. Anche se lecito, nessuno direbbe '"a thousand thousand", ma piuttosto "a million" If (N = 1000) And (Result < 1000) Then Result *= 1000 End If 'Se il numero è minore di 100, semplicemente lo 'aggiunge. Se quindi si pronunciano "twenty" e "thirty" 'di seguito, si otterà 50. Non chiedetemi perchè 'l'ho fatto così... If (N < 100) Then Result += N End If Else N = 0 End If Prev = N 'Imposta il testo della label lblNumber.Text = String.Format("{0:N0}", Result) End Sub End Class Potrebbe anche verificarsi un altro errore, qui: Me.Invoke(New SetLabel(AddressOf AnalyzeText), New Object() {Res}) di tipo FormatException, che non c'entra assolutamente niente con quello che si sta facendo. Se anche voi siete così fortunati, chiudete visual studio e riprovateci domani (con me ha funzionato XD).
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|