Utilizar o NodeMcu para criar um web server com as funcionalidades de reconhecimento de voz que existem no Google Chrome é uma estratégia interessante. Entretanto, esta solução deixou de funcionar com uma atualização do navegador. De qualquer forma, neste post mostrarei como fiz para implementá-la, apenas a título de curiosidade.
Caso você não tenha lido o post sobre as diferentes formas de fazer reconhecimento de voz, passe lá para entender tudo que vou falar.
Características
Esta solução utiliza um web server criado pelo NodeMcu e o Web Speech API do Google.
O Web Speech API possibilita o uso do reconhecimento de voz nos navegadores por meio do código em html/javascript. Como o web server é feito em html, podemos facilmente utilizar esta API e criar uma função de reconhecimento de voz.
Considerando que esse método ainda funcione:
Como foi dito no outro post, essa é uma solução mesclada. Portanto, a principal desvantagem é a falta de praticidade em se usar o sistema. E, a vantagem é que esta é uma solução simples e é tão boa quanto outras que mostrei aqui (links no fim do tópico abaixo).
Para alcançar o resultado, não é necessário instalar nenhum programa específico (desconsiderando o navegador e a IDE do NodeMcu). É necessário apenas comprar a plaquinha e o restante é feito sem custo.
Abaixo falarei o porquê deste método ter parado de funcionar.
Problema
Em uma atualização um pouco recente do Google Chrome, em relação à presente data do post, foi proibido o acesso ao microfone pelas páginas que não utilizam o protocolo HTTPS. Já comentei sobre o HTTP em outros posts, e basicamente o HTTPS é um protocolo mais seguro e confiável de transmissão de dados.
Mas o que isso altera? Acontece que o NodeMcu é capaz de criar apenas web server com o protocolo http (pelos meios convencionais). Então, fica impossível a página acessar o microfone para fazer o reconhecimento de voz.
Pesquisei se havia alguma maneira de desabilitar esse recurso. Entretanto, não obtive resultados satisfatórios. Busquei também uma forma de criar um web server com o protocolo https, mas a única alternativa que encontrei é um tanto quanto complicada. Deixo aqui o link para quem tiver interesse.
Considerei também a possibilidade de utilizar outros navegadores. Inclusive, o Mozilla Firefox também tem uma API para reconhecimento de voz. Novamente, o desfecho foi infeliz. A API do Mozilla estava apresentando alguns erros e a API do Google no Mozilla não teve efeito (considerando minhas versões).
E, além disso, o Internet Explorer ou o Edge, não possuem compatibilidade para nenhuma das API’s. Ou seja, é praticamente um beco sem saída.
De qualquer forma, não deixa de ser interessante observar como criei a página (html) e o processamento dos comandos. Inclusive, esse foi um projeto que fiz pra faculdade (na época funcionava).
Vou deixar abaixo os links para os outros posts de reconhecimento de voz (até a data do post), que funcionam de verdade e são bem úteis (na ordem do melhor pro “pior” na minha opinião): extensão do Chrome, usando o Processing e o módulo de reconhecimento de voz.
Como fazer
O reconhecimento de voz é feito no web server e os dados são enviados para o NodeMcu, onde são processados. Recomendo o curso NodeMcu Básico para entender como fazer boa parte dos procedimentos que descreverei. O procedimento que faremos pode ser ilustrado pela seguinte imagem:
Sem mais enrolação, vamos ao passo a passo:
Página HTML
O primeiro passo é criar a página com as funcionalidades do reconhecimento de voz. Para isso, farei a página da seguinte forma:
Terá um campo de texto onde irá aparecer o que está sendo reconhecido; um botão para ativar/desativar o reconhecimento de voz; um form para enviar os dados para o NodeMcu; elementos adicionais para enfeitar a página (ex: título, subtítulo, cores); por fim, o script.
Então, vamos analisar a criação dos elementos separadamente, um em cada tópico:
Campo de texto
O campo de texto irá apenas receber o texto do script contendo o reconhecimento de voz. O que fiz foi criar um <div>:
1 | <div id="result" value=""></div> |
Repare que dei a ele o nome de “result”. Além disso, no ínicio do código, editei o estilo do div:
1 2 3 4 5 6 7 8 9 10 11 12 | <style> #result{ height: 30px; border: 2px inset white; border-radius:10px; padding: 3px 10px 10px 10px; margin-bottom: 30px; font-size: 14px; line-height: 20px; background-color:white; } </style> |
Botão ativar/desativar reconhecimento
A criação do botão é bem direta:
1 | <button style="font-size:35px;position:absolute;left:48.8%;top:200px;border-radius:25px;padding: 3px 10px;border-color:rgb(89, 171, 227);background-color:rgb(228, 241, 254);" id="fala"><i class="fa fa-microphone"></i></button> |
Pode parecer muito, mas a maioria é adicionando detalhes visuais ao botão. O importante é o ‘id’, que configurei como “fala”. É por meio dele que faremos as funções do botão.
Form
Para saber pra que serve o form e como ele funciona, veja este post do curso NodeMcu Básico.
Neste caso, o form não precisa ter um botão de submit, pois a submissão será feita pelo script. Será necessário apenas ter um input que fica escondido recebendo o texto da fala reconhecida. Portanto:
1 2 3 | <form action="" method="POST" id="dados" target="refresh"> <input type="hidden" name="Reconhecido" id="mandar"> </form> |
O parâmetro importante aqui é também o ‘id’, que defini como “dados”. Utilizaremos ele para fazer a submissão dos dados.
Para que a página não dê refresh toda vez que uma informação for submetida, utilizei um truque interessante: defini o ‘target’ (onde a resposta da submissão é exibida) como sendo o elemento “refresh”; e o elemento “refresh” é o seguinte <iframe>:
<iframe name=”refresh” style=”display:none;”></iframe>
Fazer isso faz com que a página permaneça intacta sem dar refresh. Assim, poderemos criar uma lista dos comandos já ditos:
<ol id=”lista”></ol>
Para a lista, definimos o ‘id’ como sendo “lista”. Bem intuitivo.
Script do reconhecimento
Abordarei os pontos mais importantes do script. O código completo dele pode ser visto abaixo do tópico dos resultados. Fique atento ao uso dos ‘ids’ citados nos tópicos anteriores.
Primeiro é necessário criar a instância do reconhecimento de voz com o seguinte comando:
1 | var speechRecognizer = new webkitSpeechRecognition(); |
Essa instância possui duas principais funções, que são:
1 2 | speechRecognizer.onresult = function(event){} speechRecognizer.onerror = function (event) {} |
A primeira é executada toda vez que o reconhecimento de voz retorna uma transcrição (mesmo que de uma letra/palavra). Já a segunda, é executada quando ocorre algum erro no processo de reconhecimento (falta de microfone, erro de conexão etc). Dentro da primeira função, ficamos gravando constantemente o resultado traduzido em tempo real e executamos alguns comandos importantes quando a transcrição termina (quando a pessoa para de falar).
Para detectar o fim da transcrição usamos: event.results[indiceDoEvento].isFinal. Quando isso ocorre, nós definimos o valor do ‘form’ como sendo o texto reconhecido (finalTranscripts) e submetemos este valor:
1 2 | document.getElementById('mandar').value = finalTranscripts; document.getElementById('dados').submit(); |
É claro que tem muitos outros aspectos que estão no código completo que não estou abordando. Porém, estou tentando dar uma visão simplificada do procedimento. O restante dos comandos dá para interpretar sem muita dificuldade.
Script do botão
Por fim, explicarei o script do botão. É possível detectar se ele foi clicado com a seguinte função:
1 | document.getElementById('fala').onclick = function() { |
Como o botão serve para ativar/desativar o reconhecimento de voz, preciso de uma variável para informar qual é o estado atual do botão (ativo ou desativo):
var pres = false;
Considerando que ela começa em false (botão desativado), ao apertar o botão, o reconhecimento de voz deve ser iniciado e o valor da variável deve ser invertido. Se o botão já está ativo e eu apertá-lo novamente, o reconhecimento deve parar e a variável deve ser invertida:
1 2 3 4 5 6 7 | if (pres == true){ speechRecognizer.stop(); pres = !pres; }else{ pres = !pres; speechRecognizer.start(); } |
Novamente, no código completo existem mais comandos, mas foquei no principal. Por exemplo, se você clicar no botão para desativar o reconhecimento antes da transcrição terminar, o programa força a submissão do texto que foi reconhecido.
Resultado
Adicionando algumas configurações de cor e estilo, o resultado ficou o seguinte:
Quando colocamos está página no NodeMcu e clicamos no botão, o seguinte problema aparece:
Este é o problema que comentei no início do post. O Google Chrome não deixa habilitar de forma alguma o microfone (mesmo clicando em ‘perguntar se deseja acessar’). Na primeira imagem deu certo, pois abri a página como um arquivo local e não por um web server.
Código HTML completo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <style> #result{ height: 30px; border: 2px inset white; border-radius:10px; padding: 3px 10px 10px 10px; margin-bottom: 30px; font-size: 14px; line-height: 20px; background-color:white; } </style> </head> <body style="background-color:rgb(103, 128,159)"> <p style="color:white;text-align:center;font-family: Segoe UI Black;font-size:32px;font-weight: bold;">Sistema Controlado por Voz</p> <div style="text-align:center;">Clique no botão e comece a falar</div> <iframe name="refresh" style="display:none;"></iframe> <form action="" method="POST" id="dados" target="refresh"> <input type="hidden" name="Reconhecido" id="mandar"> </form> <div id="result" value=""></div> <button style="font-size:35px;position:absolute;left:48.8%;top:200px;border-radius:25px;padding: 3px 10px;border-color:rgb(89, 171, 227);background-color:rgb(228, 241, 254);" id="fala"><i class="fa fa-microphone"></i></button> <ol id="lista"></ol> <script> var speechRecognizer = new webkitSpeechRecognition(); var pres = false; speechRecognizer.continuous = true; speechRecognizer.interimResults = true; speechRecognizer.lang = 'pt-BR'; speechRecognizer.onresult = function(event){ var interimTranscripts = ''; var finalTranscripts = ''; for(var i = event.resultIndex; i < event.results.length; i++){ var transcript = event.results[i][0].transcript; transcript.replace("\n", "<br>"); if(event.results[i].isFinal){ finalTranscripts = transcript; document.getElementById('mandar').value = finalTranscripts; document.getElementById('dados').submit(); speechRecognizer.stop(); document.getElementById('fala').style.backgroundColor = 'rgb(228, 241, 254)'; pres = !pres; document.getElementById('lista').innerHTML += "<li>" + finalTranscripts + "</li>"; document.getElementById('mandar').value = "nada"; }else{ interimTranscripts += transcript; } } document.getElementById('result').innerHTML = finalTranscripts + '<span style="color:#999">' + interimTranscripts + '</span>'; }; speechRecognizer.onerror = function (event) { console.log(event.error); }; document.getElementById('fala').onclick = function() { if (pres == true){ speechRecognizer.stop(); document.getElementById('fala').style.backgroundColor = 'rgb(228, 241, 254)'; pres = !pres; document.getElementById('dados').submit(); document.getElementById('mandar').value = "nada"; }else{ pres = !pres; speechRecognizer.start(); document.getElementById('result').innerHTML = " "; document.getElementById('fala').style.backgroundColor = 'rgb(37, 116, 169)'; } } </script> </body> </html> |
NodeMcu
Disponibilizarei o código que criei que pega a fala reconhecida e faz o processamento dela para acionar pinos do NodeMcu. Os comandos existentes são: ligar, desligar, horas, criar, apagar. Os dois primeiros servem para ligar/desligar um pino; o comando “horas” faz a página falar as horas (síntese de voz); e os últimos dois criam/apagam comandos.
Antes disso, vou explicar algumas partes importantes do código.
Criação do web server
Não vou explicar como a criação do web server é feita, pois já fiz isso neste post. Quero apenas explicar um ponto novo aqui.
Toda vez que uma submissão for feita no web server, ela chega da seguinte forma:
… Reconhecido=”texto reconhecido” …
Saiba mais sobre, lendo este post.
Portanto, toda vez que recebermos a submissão, precisamos recortar o texto reconhecido para processá-lo. A forma que escolhi de fazer isso foi criar uma variável chamada postparse que recebe o valor do indice da palavra “Reconhecido” com o comando ‘string.find’. Se o índice realmente existir, então extraímos o texto usando o comando string.sub. Por fim, enviados o texto, juntamente com a conexão, para uma função que criamos (chamada processamento). Enviamos o conn, pois essa função precisará enviar alguns dados para o cliente.
1 2 3 4 5 | postparse={string.find(payload,"Reconhecido")} --Pega o indice da palavra 'reconhecido' no payload if postparse[2]~=nil then reco=string.sub(payload,postparse[2]+2,#payload) --extraimos a frase reconhecida processamento(conn, reco) --Envia os dados para o processamento dos comandos end |
Processamento dos dados
Explicar passo a passo da função é bem complicado, visto que é possível utilizar uma outra lógica melhor (creio eu). Sugiro ler os comentários para entender. Portanto, vou explicar a lógica de funcionamento do processamento dos dados.
Considere o texto recebido pela função:
“Quero ligar a luz da sala e desligar a luz do quarto e quero saber as horas”
A função do processamento, basicamente, divide o texto por comando. Ela verifica se no texto existe algum comando (dos que foram definidos), e pega aquele que tem o menor índice (o primeiro que aparece). Dado este comando, a função pega todo o texto que está depois dele até o próximo comando. Ou seja, considerando nosso exemplo, a função pegaria o comando “ligar”, com o texto “a luz da sala e “. A palavra “quero” foi ignorada, pois o comando (“ligar”) está depois dela.
Dessa forma, nós obtemos o comando e as características associadas a ele. No nosso caso, a ação é “ligar” e a característica associada a esta ação é “a luz da sala e”. Esta letra “e” no final acaba não tendo importância na segunda parte do processamento. Enfim, quando o programa faz esse reconhecimento do primeiro comando, ele executa a ação do comando (tópico abaixo) e depois corta o texto deixando do segundo comando pra frente.
No exemplo citado, o programa cortaria o texto e deixaria apenas:
“desligar a luz do quarto e quero saber as horas”
Dessa forma, ele pode fazer a verificação toda novamente e identificar qual é o primeiro comando que aparece. E assim se repete até não existir mais comandos.
Observações
Aqui valem ser feitas duas observações:
- Adicionei um recurso que, se a função detecta a palavra “cancelar”, ela corta o texto todo que está atrás da palavra.
- Ex: “quero ligar a luz da sala cancelar quero saber as horas” – tudo que está atrás de cancelar é cortado e sobra apenas: “quero saber as horas”.
- O comando horas pode acabar tendo alguma “característica” associada a ele (texto depois do comando), mas isso não faz diferença no acionamento do comando (tópico seguinte).
- Ex: “me fale as horas e quero ligar a luz do quarto” – o programa identifica o comando “horas” e pega o texto associado que está antes do próximo comando, que é: “e quero”. Mesmo com esse texto associado, o resultado não é atrapalhado.
Acionamento dos comandos
O acionamento dos comandos é feito logo depois que o processamento detecta qual é o comando e o texto associado da frase dita. Se o comando dito for:
Horas
- O programa envia um código de síntese de voz com o elemento <script>
- cliente:send(‘<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance(“‘ .. “19:45″ .. ‘”));</script>’)
- Coloquei uma hora fixa (19:45), pois quis apenas mostrar a possibilidade da síntese de voz. Para pegar a hora atual é muito simples, veja aqui.
Ligar/Desligar
- Criei uma tabela com um nome e um número associado a ele. Esta lista serve de referencia para saber quais nomes existem para acionar os dispositivos e quais são os pinos (GPIO) associados a eles.
- Ex: a tabela que já existe: acionamento = {“luz da sala:1″,”luz do quarto:2″,”luz da cozinha:3”} – se eu falar “ligar luz da sala”, o programa aciona o pino 1.
- A lógica do programa é passar em todos os elementos da tabela “acionamento” e verificar se existe cada acionamento no texto associado ao comando. Se existe, ele liga/desliga o pino atrelado ao acionamento.
- É possível fazer dois acionamentos ao mesmo tempo: ‘quero ligar a luz da sala e a luz do quarto’.
Criar
- Para criar um comando, é preciso dizer “criar ‘nome do comando’ pino ‘pino associado’ “. É possível, ter um mesmo nome acionando mais de um pino. E é possível também, ter nomes diferentes acionando o mesmo pino.
- A função separa o nome do comando que você quer do pino que você falou e adiciona ele à tabela ‘acionamento’. Além disso, o programa informa, por voz no web server, se deu certo ou não.
- Ex: “criar luz do escritório pino 6” – adiciona “escritorio:6” à tabela ‘acionamento’. Ou seja, você pode falar “ligar luz do escritório” para ligar o pino 6.
Apagar
- Para apagar um comando, basta dizer “apagar ‘nome do comando’ “.
- A função passa por todos os elementos da tabela “acionamento” e verifica se existe o acionamento que a pessoa falou. Se sim, ele é removido da tabela. Além disso, o programa informa, por voz no web server, se deu certo.
- Ex: “apagar luz da sala” – o acionamento padrão “luz da sala” não existirá mais.
Código completo
Lembrando, ler os outros posts que referenciei ajuda bastante a entender o código completo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | wifi.setmode(wifi.STATION) wifi.sta.config {ssid="NOME_DO_SEU_WI-FI", pwd="SENHA_DO_WI-FI"} comandos = {"ligar","desligar","horas","criar","apagar"} --Define os comandos existentes acionamento = {"luz da sala:1","luz do quarto:2","luz da cozinha:3"} gpio.mode(1,gpio.OUTPUT) --Define 6 pinos como saida (pode ser aumentado até 13) gpio.mode(2,gpio.OUTPUT) gpio.mode(3,gpio.OUTPUT) gpio.mode(4,gpio.OUTPUT) gpio.mode(5,gpio.OUTPUT) gpio.mode(6,gpio.OUTPUT) tmr.alarm(0, 1000, 1, function() --Aguarda conectar na rede if wifi.sta.getip() == nil then print("Conectando à rede...\n") else ip=wifi.sta.getip() print("IP: ",ip) tmr.stop(0) end end) srv = net.createServer(net.TCP) srv:listen(80, function(conn) conn:on("receive", function(conn, payload) tls.cert.verify(enable) print(payload) -- Imprime os dados recebidos do servidor postparse = 0 postparse={string.find(payload,"Reconhecido")} --Pega o indice da palavra 'reconhecido' no payload if postparse[2]~=nil then reco=string.sub(payload,postparse[2]+2,#payload) --extraimos a frase reconhecida processamento(conn, reco) --Envia os dados para o processamento dos comandos end local _line --Envia a pagina em html para o usuario if file.open("index.html","r") then repeat _line = file.readline() if (_line~=nil) then conn:send(string.sub(_line,1,-2)) end until _line==nil file.close() end conn:on("sent", function(conn) conn:close() end) --fecha a conexão end) end) ----PROCESSA OS COMANDOS---- function processamento(cliente, base) base = string.lower(base) repeat if string.find(base,"cancelar") ~= nil then --Ignora tudo que foi falado antes da palavra cancelar base = string.sub(base,string.find(base,"cancelar")+string.len("cancelar")) end comando = '****'; --Qual o comando reconhecido ref = string.len(base) --A referencia inicial cont = false --A variável que indica se ha outros comandos na string for i=#comandos, 1, -1 do if string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]") ~= nil then if string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]")<ref then --Testa o comando com menor indice ref = string.find(base,comandos[i])+string.len(comandos[i]) --Pega o comando com menor indice e define ele como referencia comando = comandos[i] --Pega o valor do comando de menor indice end end end x = pegarelementos(base,comando) --Funçao que retorna os atributos do comando(o que o comando deve fazer) x = string.gsub(x, "+", " ") if comando == "criar" then --Funcao que adiciona um comando nos acionamentos if string.find(x, "pino") ~= nil and string.match(x, '%d') then y=string.sub(x,2,string.find(x,"pino")-2) pino = string.match(x, '%d')--pino=string.sub(x,string.find(x,"pino")+5,-1) table.insert(acionamento,1,y .. ":" .. pino) cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Comando adicionado" .. '"));</script>') else cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Faltou informar o pino" .. '"));</script>') end elseif comando == "apagar" then --Funcao que apaga um comandos dos acionamentos for i=#acionamento,1,-1 do -- Funcao que aciona os pinos de acordo com o comando y=string.sub(acionamento[i],1,string.find(acionamento[i],":")-1) -- Qual e o acionamento if string.find(x,y) ~= nil then table.remove(acionamento,i) cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Comando removido" .. '"));</script>') end end elseif comando == "horas" then cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "19:45" .. '"));</script>') else for i=#acionamento,1,-1 do -- Funcao que aciona os pinos de acordo com o comando y=string.sub(acionamento[i],1,string.find(acionamento[i],":")-1) -- Qual e o acionamento if string.find(x,"%f[%a]" .. y .. "%f[%A]") ~= nil then pino=string.sub(acionamento[i],string.find(acionamento[i],":")+1,-1) --Qual e o pino que deve ser acionado if comando == "ligar" or comando == "Ligar" then gpio.write(pino,gpio.HIGH) cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Ligando".. y .. '"));</script>') end if comando == "desligar" or comando == "desligar" then gpio.write(pino,gpio.LOW) cliente:send('<script>window.speechSynthesis.speak(new SpeechSynthesisUtterance("' .. "Desligando".. y .. '"));</script>') end end end end base = string.sub(base,ref) --Redefine a base, excluindo o comando analisado for i=#comandos, 1, -1 do --Testa se ainda ha outros comandos para serem analisados if string.find(base,comandos[i]) ~= nil then cont = true end end until not cont end ----Funçao auxiliar da de processamento---- function pegarelementos(base,valor) ref = string.find(base,valor)+string.len(valor) --Valor referência=A analise começa a partir dele lim=string.len(base) --valor maximo da busca for i=#comandos, 1, -1 do if string.find(base,comandos[i],ref) ~= nil then if string.find(base,comandos[i],ref)<lim then --Acha qual o comando que aparece apos o comando de entrada(valor) lim = string.find(base,"%f[%a]" .. comandos[i] .. "%f[%A]",ref) --Se achar, pega o valor do indice do comando end end end return string.sub(base,ref,lim); --retorna a parte da string que fica despois do comando de entrada ate o valor limite end |
Espero que, mesmo sendo uma solução que não funciona mais, o post tenha trago algum aprendizado e conhecimento para você. Com toda certeza, posso falar que, para chegar no código final, foi muito conhecimento absorvido (programação web, programação em lua, noções básicas de rede, módulos do NodeMcu etc). E isso tudo tinha que ser partilhado.
Reconhecimento de voz – Extensão Google Chrome