|
Questo capitolo è scritto per VB2008!
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.
Ora, il computer non può prevedere tutte le possibili combinazioni di parole esistenti, quindi dobbiamo essere noi a fornirgli un
"dizionario" su cui basarsi per il riconoscimento. La costruzione di una struttura di questo tipo richiede l'uso di un oggetto particolare:
GrammarBuilder. Questa semplice classe aiuta a formare delle frasi che potranno venire catturate e riconosciute dall'Engine attraverso il
microfono. Nelle prove che ho fatto, l'engine è riuscito a catturare una sola parola alla volta, ma forse mi sono dimenticato di
impostare i tempi giusti di intervallo tra una parola e l'altra. Sta di fatto che il modo più semplice per far riconoscere una qualsiasi
parola all'engine consiste nell'aggiungere a GrammarBuilder la lista di tutte le parole contemplate (ossia, solo quelle che vogliamo noi).
Ecco un semplice esempio:
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 much
Per 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). |
|