Generar frases de forma automática a partir de plantillas

Ya hemos visto otras formas de generar frases. Esta forma de generar frases consiste en tener unas plantillas de las que sólo algunas partes son variables.

Por ejemplo:

Al abrir el cofre encuentras un ${adjetivo} ${objeto} para ${acción}

Al usar una plantilla las frases generadas tienen sentido y es fácil integrarlas con otras generadas de la misma forma. Los textos generados no tienen tanta variedad como con otros métodos pero te aseguras el sentido de los mismos.

Hay un curioso proyecto en inglés que partiendo de una base de datos de palabras en inglés te permite generar un frase a partir de un aplantilla en la cual solo indicas {{an_ adjetive}}, {{noum}} y el lo rellena con palabras al azar de la base de datos (de términos terroríficos al parecer)

Por ejemplo:

This is {{ an_adjective }} {{ noun }}. => This is a wicked cadaver.

En castellano el principal problema que tiene este método son  distintas flexiones que tiene las palabras y la necesidad de que las distintas palabras de la frase tengan concordancia entre ellas. «Caperucita rojo» es claramente incorrecta. Así que nosotros tenemos que aportar género y número para nombres, complementos, adjetivos. Y en el caso de los verbos persona, tiempo y modo. Para solucionar este problema hay tres opciones:

  • Limitar las opciones, generamos frases solo con un género, número, tiempo y modo.
  • Crear diccionarios diferentes para cada opción. Por ejemplo para los nombres se crearían cuatro diccionarios: femenino-singular, femenino-plural, masculino-singular, masculino-plural.
  • Usar un algoritmo que modifique las flexiones de cada palabra Algo de eso ya hemos visto en este blog. Es un algoritmo al que le pasas una palabra y la forma de la misma que quieres obtener y te la transforma. Ya hemos visto algo así.

En JS generar este tipo de frases es muy sencillo, preparamos una plantilla.

‘Al abrir el cofre encuentras un ${objeto} ${adjetivo} para ${acción}`

Crearemos una lista de opciones:


var adjetivos = ["épico","dorado","brillante"];
var objetos = ["mazo", "mandoble", "arco"];
var acciones = ["matar dragones", "rescatar princesas", "luchar batallas"];

Ahora usaremos una función para elegir uno de ellos al azar:

function selectOne(items){
return items[Math.floor(Math.random() * items.length)];
}

Y finamente otra que rellena la plantilla:

function template(objeto, adjetivo, accion){
return `Al abrir el cofre encuentras un ${objeto} ${adjetivo} para ${accion}`
}

Vamos con el ejemplo de funcionamiento:

template(selectOne(adjetivos), selectOne(objetos), selectOne(acciones));

Ejemplo de resultados:

  • «Al abrir el cofre encuentras un brillante arco para luchar batallas»
  • «Al abrir el cofre encuentras un épico arco para matar dragones»
  • «Al abrir el cofre encuentras un dorado mazo para matar dragones»

Este sistema para generar frases tiene la ventaja de que estas tienen sentido pero tiene la desventaja de que resultan repetitivas y si se usa varias veces en seguida se percibe el patrón.

Un mejora necesaria seria usar un algoritmo de flexiones para encontrar el género y número del objeto y adecuar el del adjetivo. Imaginaros que uno de los objetos es «espada», el adjetivos debería ser femenino.

 

 

El problema del contexto en la Inteligencia Artificial

Definir el problema del contexto es difícil ya que sus causas son muchas. De forma intuitiva el problema del contexto es que cuando hablas con otra persona tiene claro dónde y cuándo estás, que os rodea y un montón de información cultural y social aprendida. Además tenemos una capacidad más o menos buena para captar las emociones e intenciones de la otra persona y de aprender cómo es su personalidad con el paso del tiempo. Suma a todo esto nuestra habilidad para comprender el lenguaje, los gestos, los símbolos visuales y el entorno junto con nuestra capacidad de abstracción. Sin ser perfectos, muchas veces ocurren malentendidos, ninguna inteligencia artificial se nos acerca si quiera. Vamos a ver algunas de las causas de forma individual.

Los conocimientos y comportamientos aprendidos se deben a nuestra cultura y sociedad. Son muchos más de los que nos pensamos. Son cosas como las normas de comportamiento, frase hechas, «lo que está de moda» o aquel conjunto de diversos conocimientos que llamamos «culturilla general». La unica forma de solucionar este problema es aportar ese conocimiento al agente. Por ejemplo, el refrán: «El que a buen árbol se arrima, buena sombra le cobija» no va a ser correctamente interpretado por ninguna I.A. que no conozca su significado previamente. No es un problema de interpretación de lenguaje natural, es un conocimiento que debe ser aprendido. Lo mismo pasa con las señales de tráfico, por ejemplo.

Otro problema del contexto es que el ser humano es perfectamente consciente del entorno que le rodea. Si por ejemplo tu pareja te dice: «¿Puedes mirar en Internet a que hora pasa el bus?». Y es un martes a las ocho de la mañana seguramente se refiera al bus al trabajo. Si es un sábado y se va al pueblo a ver a sus padres será el bus del pueblo y si es un martes de agosto tiene vacaciones y vais a coger un avión se referirá al bus al aeropuerto. Como humanos no tenemos ningún problema en entender esto. Eso se debe a que integramos la frase en un contexto espacial, temporal y personal. Para que una I.A. haga lo mismo tiene que ser capaz de fusionar varias fuentes de datos: localización, fecha, hora, agenda, costumbres de la persona. Estamos tan acostumbrados a hacerlo de forma automática que nos parece engañosamente facil hacerlo. Sin embargo para un agente saber que fuentes de información son importantes y como fusionarlas es difícil.

Las inteligencias artificiales tampoco son buenas entendiendo figuras como la ironía, el sarcasmo o interpretando las cosas desde el punto de vista de otras personas. No son capaces de las abstracciones necesarias para entender cosas como el arte o incluso distinguir un objeto de su reflejo en un espejo. La incapacidad de ponerse en el lugar de otros causa problemas como que muchos vehículos autónomos causan mareos ya que sus maniobras no están pensadas para la comodidad de los viajeros. En algunos casos sencillos se pueden resolver estos problemas con aprendizaje y realimentación de los humanos.

woman in black shirt facing mirror

Una I.A. no sabria distinguir entre la chica real y la reflejada, diria que en la foto hay dos personas. Photo by Ivan Obolensky on Pexels.com

Saber que señales ignorar y cuáles no. Hace un tiempo iba conduciendo por un calle de dos carriles uno para cada sentido con aparcamientos en batería a los lados. Llegado a un punto uno de los carriles estaba en obras, la solución por la que había optado era que uno de los carriles provisionales pasaba por encima del aparcamiento en batería y el otro usaba el carril que habitualmente circulaba en sentido opuesto. La única señal que había era unas vayas amarillas que más o menos sugerían lo que había que hacer y un cartel de cuidado obras. Tras unos segundo de duda resulto sencillo saber que durante unos metros tendría que hacer caso omiso a las indicaciones viales y circular por encima de las lineas de aparcamiento ya que mi carril estaría ocupado por coches circulando en sentido contrario. El entorno puede estar lleno de señales contradictorias y hay que entender cuáles ignorar en cada momento.

Algunas técnicas que tenemos para tratar de minimizar este problema y que los agentes den la sensación de tener cierta «consciencia» del entorno:

Limitar el ámbito del agente, si haces un agente para consultar sobre mecánica del automóvil nadie se sorprenderá de que por «gato» solo entienda los gatos hidráulicos.

Crear estados o tópicos, es una forma de ampliar el caso anterior. El programador define estados o tópicos que ayudan a crear el contexto. Si por ejemplo el estado es «mecánica del automóvil» es muy probable que gato se refiera al gato hidráulico, pero si el tópico es «mascotas» es más probable que se refiera a un animal. El problema de los tópicos es que al final el número de los mismos está limitado.

Detección de tópicos, en combinación con el punto anterior, es detectar los tópicos de un texto o conversación. Si hablo de un gato saber si me refiero al animal o a la herramienta. Generalmente se extrae relacionándolo con el resto de palabras, si digo «coche», «rueda», «maulla».

Aprendizaje, para adaptarse a cada persona es necesario aprender sus hábitos. Los asistentes actuales ya lo hacen tratando de aprender los hábitos, gustosy costumbres a base de recopilar datos automáticamente,

Interacción con los humanos, a veces la forma más fácil de saber algo es que te lo digan. Es importante la capacidad de preguntar y recibir esa información ya sea de forma natural o a través de una pantalla de configuración.

Detección de emociones, técnicas que van desde el reconocimiento de expresiones faciales a la detección de sentimientos en frases o de ironías.

Extracción de datos del entorno, muchas veces el contexto viene determinado por lo que te rodea. El que agente necesita saber dónde está y que ocurre a su alrededor. Si suena música que música es, si se es viendo una película, cuál es. Si conoce al resto de las personas que pueda haber.

Fusión de datos, no basta con captar todos los datos hay que entenderlos en su conjunto. Para ello hace falta cruzarlos pero también ser capaz de extraer nueva información al cruzar estos datos. Ha de «deducir datos nuevos». Tiene que trabajar con incertidumbre y ser capaz de «rellenar huecos» con lo más probable. También tiene que ser trabajar de descartar los datos que son correctos pero no aplican ene ese momento, que quizás sea lo más difícil.

¿Con todo esto basta? No, o al menos no con el nivel actual de la tecnología. Aún estamos lejos de lograr agentes que entiendan el contexto completamente. Sin embargo con estas técnicas, en algunos casos, los resultados pueden ser sorprendentes, o al menos parecerlo, por eso a veces los asistentes como Siri o Alexa nos sorprenden con su «comprensión» y otras con su estupidez. Aun queda mucho para lograr que la I.A. entienda el contexto correctamente.

 

Generar frases de forma automática a partir de textos

Ya hemos visto, sin entrar en detalles, un sistema para generar frases en la entrada sobre los bots y las respuestas en lenguaje natural.

La solución más común para generar texto de forma automática es usar cadenas de Markov. Explicándolo pronto y mal consiste en tener un grafo para cada palabra, esa palabra está conectada con el resto de las posibles palabras que pueden seguirla. Cada enlace tiene asociada la probabilidad de que esa sea la siguiente palabra. Además de palabras hay que tener en cuenta dos nodos especiales: «inicio de frase» y «fin de frase». El primer nodo es por el que se empieza para formar una frase y al llegar al segundo se termina la frase.

La forma de usar este grafo es sencilla. Se parte del nodo «inicio de frase». El siguiente nodo se elige al azar, cada nodo tiene una probabilidad distinta de ser elegido. Una vez un nodo es elegido se repite la operación en ese nodo hasta llegar al nodo de «final de la frase».

Generar estos grafos a mano seria demasiado trabajo, por eso estos grafos se crean fácilmente a partir de textos. Tan sencillo como recorrer los textos e ir contando para cada palabra cual es la siguiente. Por ejemplo para la palabra «perro» tomando como base del aprendizaje las siguientes frases:

  • El perro ladró a la bicicleta.
  • El perro lamió la mano de su dueño
  • El perro olisqueó la comida antes de comerla
  • El perro olisqueó la prenda antes de seguir el rastro
  • Como el perro y el gato

El resultado serían:

GrafoPerro

Olisqueó tiene el doble de probabilidades de ser elegida como siguiente palabra que las demás.

El problema que vemos aquí es con las palabras muy habituales como «y». Detrás de «y» puede ir cualquier cosa que no tenga ninguna relación con las palabras anteriores de la frase. ¿Como evitamos eso?. En lugar de generar el grafo de una sola palabra lo generamos de dos o tres. En nuestro caso si lo generamos de dos palabras quedaría así:

El problema de usar más de una palabra es que el tamaño de los textos de aprendizaje ha de ser grande o se repetirán siempre las mismas frases. Vamos a añadir una frase más para que no todas empiecen por «el perro»

  • Al perro le gusta jugar con la pelota

El resultado

 

GrafoPerron

Con más palabras de profundidad las frases tienen más sentido, pero su variedad se resiente. Al final hay que buscar un equilibrio.

Este sistema sirve para generar frases que a veces tienen sentido o que por lo menos dan la sensación de que quieren decir algo. Es importante buscar textos similares para generar grafos que mantengan el sentido. Si por ejemplo mezclamos textos de gatos, animales, con gatos, herramienta, puede ser divertido el resultado pero difícilmente tendrá sentido. Al final el resultado de estas herramientas es más artístico que practico y el texto generado difícilmente tendrá sentido más allá de una frase, aunque si los textos están bien elegidos tendrás la sensación de que quiere decir «algo».

Un ejemplo muy sencillo de usar es esta aplicación cuyo uso es muy sencillo, basta con preparar los textos de ejemplo y pasarlos al generador:

python markov.py gen <name> <count>

  • Name es el nombre del fichero que contiene esos textos
  • Count es el nivel de profundidad de los nodos (el numero de palabras que tiene en cuenta)

Una de la ventajas de este sistema es que solo depende del idioma de los textos usado como fuente, el algoritmo funciona igual en todos los idiomas.

Reemplazar caracteres

Una de la funciones más habituales de analizar texto como lenguaje natural es el reemplazar caracteres: quitar tildes, cambiar mayúsculas por minúsculas, reemplazar signos de puntuación, etc. Su utilidad es simplificar el texto para facilitar el análisis, la búsqueda en diccionarios o la separación en tokens.

Hay muchas funciones para reemplazar caracteres, desde el remplace más simple hasta cosas más complejas como expresiones regulares. ¿Para qué inventar uno nuevo?. Por sencillez y velocidad. El método que utilizamos aquí es muy simple, no permite reemplazos complicados como las expresiones regulares, solo remplazar un carácter por otro, pero a cambio es capaz de realizar todos los reemplazos en una sola iteración al texto.

Su funcionamiento es sencillo, se crea un hashmap donde el carácter a remplazar es la clave y el carácter que lo reemplaza el valor. Luego se recorre el texto caracter a caracter y se consulta en el hashmap si existe un valor para el carácter actual, si es así se remplaza, si no se deja el carácter tal cual.

Para usarlo basta con añadir los caracteres, se pueden añadir de uno en uno o varios a la vez.

replace.add(«A»,»a»);

replace.add(«Aåáäâ», «a»);

También se puede reemplazar un carácter por varios.

replace.add(«/»,» dividido por «);

No así al revés, no se pueden reemplazar varios caracteres.

Si se reemplaza por una cadena vacía será como borrar el carácter del texto

replace.add(«:», «»);

Una vez configurado el diccionario basta con llamar al método replace con el texto a tratar, como resultado devolverá el texto con los reemplazos realizados.

text = replace.replace(text);

En el repositorio de la libreria se puede ver un ejemplo de funcionamiento. En él se crea un conversor a «ninini» (léase con ritmo de «chincha pincha»). Para ello se transforman todas las vocales a letras «i».

En el ejemplo se puede ver que no bastaria solo con reemplazar los caracteres hay casos como «qii» y «gii» que necesitan un tratamiento posterior. Por eso es importante elegir bien que métodos de reemplazo a usar y el coste en tiempo de los mismos.

Finalmente hay una funcionalidad más que aporta esta librería. Si se fija un valor para la propiedad default cuando no se encuentre un carácter en el hashmap, en lugar de dejarlo igual, lo reemplazará por el valor de default. Por ejemplo en este caso eliminará todos los caracteres no

replace.setDefault(«»);

Arquitectura de un bot que interactúa por voz

Vamos a ver como crear nuestro propio bot que interactue usando la voz. Algo, salvando las distancias, al estilo Siri, Alexa o Cortana. En esta entrada voy a hablar solo de la arquitectura y no del procesamiento del lenguaje natural o de la síntesis del mismo.

Lo primero que necesita todo bot es estar escuchando a la espera de oír su nombre o alguna frase que pueda reconocer como su orden. Aunque hay implementaciones por hardware que esperan oír el nombre del bot para activar la captura de audio, en nuestro caso deberemos estar escuchando constantemente. Así que nuestro primer componente va ser un proceso que escucha constantemente y transforma el audio en texto.

Posteriormente ese texto ha de ser analizado por un sistema de reglas que determine si el audio contiene algún comando valido, en nuestro caso el sistema de reglas estará basado en expresiones regulares.

Una vez tengamos una coincidencia valida se tienen que extraer los datos del comando y ejecutar el código asociado.

El resultado de la ejecución de ese código ha de ser transformado a una frase de texto que sea entendible por el usuario.

Finalmente ese texto será leído en voz alta por nuestro bot. Por lo que necesitaremos un sistema que sintetice la voz a partir del texto.

Los navegadores modernos incluyen una API tanto de reconocimiento del habla como de síntesis del habla . Aunque la verdad que el único que les da un soporte decente es Chrome, de hecho es el único en que logré que funcionara en español.  Con eso tenemos parte de lo que necesita un bot, falta una forma de definir los comando validos y otra de componer las frases que el bot necesite decir. Para ello, y alguna cosa más, se ha creado la librería jsBotVoice

En resumen:

  1. Captura del audio
  2. Conversión a texto
  3. Análisis del texto en busca de coincidencias con los comandos de texto definidos
  4. Extracción de datos y ejecución del código asociado al comando
  5. Creación del texto en respuesta
  6. Síntesis de voz a partir del texto
botVoice

Diagrama de funcionamiento de un bot que se comunica por voz

 

Los puntos del 1 al 4 están explicados en esta entrada del blog y del 5 al 6 en esta otra.

Síntesis de voz y lenguaje natural en un bot

En una entrada anterior veíamos como interpretar los comandos de voz. Ahora vamos a ver hacer que el bot de respuesta lo más naturales posibles en lenguaje hablado.

La forma más sencilla y rápida es tener respuestas predefinidas. Lo malo de este sistema es que la respuesta es siempre igual y queda muy poco natural. Usando la librería jsBotVoice  es tan sencillo como usar el modificador * delante de la cadena y esa cadena será enunciada literalmente.

voice.talk("*Este texto se lee literal");

Para aportar algo de variedad a la respuesta vamos a utilizar un modelo muy simple basado en diccionarios. La idea es que cada palabra de la frase pueda reemplazarse por varias expresiones similares, eligiéndose una al azar lo cual dota al bot de variedad en las respuestas. Lo primero será definir el diccionario.

var spanish_dictionary = {
  "hi": ["hola", "saludos"],
  "person": ["humano","ser"],
  "yes": ["si","afirmativo"],
  "no": ["no","negativo"],
  "ok": ["vale", "de acuerdo", "hecho"]
};

Una vez definido para usarlo solo hemos de crear una frase con esos tokens

voice.talk("hi person");

Esta linea pueda dar varios posibles resultados:

hola humano
saludos humano
hola ser
saludos ser

Si queremos aladir alguna palabra que se lea de forma literal en lugar de interpretarla como un token podemos ponerle delante el modificador #

voice.talk("#Buenas person");

Posibles resultados:
buenas humano
buenas ser

Por otro lado podemos personalizar aun más las respuestas usando variables, para ello usamos el nombre de la variable precedido de un $ . Las variables han de ir almacenadas en el mapa voice.data

voice.data["name"] = "Cubiwan";
voice.talk("hi $name");

Posibles resultados:
hola cubiwan
saludos cubiwan

Más documentación y ejemplos en jsBotVoice .

Alarma con nodeMCU y un radar HW-MS03

La razón oficial de este montaje era crear un sistema de alarma con sensor de movimiento que cuando detectase algo me enviara un notificación al movil. La razón real es que me habia comprado un nodeMCU y un HW-MS03 y algo tenia que hacer con ellos.

En principio el proyecto es sencillo. Conectar el radar al nodeMCU y cuadno detecte movmiento enviar una notificación através de un webhook de IFTTT para que apararezca una notificacion en el movil. Pero resulta que nada es tan sencillo como parece, pero asi es más divertido.

Lo primero es concetar el HW-MS03 al nodeMCU lo cual es facil porque solo tiene tres pines.

HW-MS03 nodeMCU Funcion
GND GND Tierra
Vin 3V Alimentacion +3V (ojo no soporta +5V)
Out A0 Señal del HW-MS03 al nodeMCU

Cuando el HW-MS03 detecta algo que se mueve delante suyo (en teoria hasta 4 metros de distancia) manda la señal por el pin out. Basta con detectarla y realizar un http get contra la direccion que facilita IFTTT, facil. He usado al entrada analogica (A0) del nodeMCU porque no estaba segura si el voltaje de la patilla Out bastaria para activar las entradas digitales. Tras varias pruebas he fiajado el valor de umbral en 800.

if(analogRead(A0) > 800){
    ...
}

Pero volvamos un paso atras. resulta que concetar juntos ambas placas fue una mala idea, ambas trabajan en la misma frecuencia. Asi que cuando el nodeMCU esta usando el WiFi el HW-MS03 se vuelve loco. Tras realizar varias pruebas puede ver que esto solo pasa cuando la distancia entre ambos era de unaos 50 cm, pero con cables de esa longituda tambien tenia problemas para leer el estado del pin. La unica opción que me quedaba era encender el WiFi solo para enviar la notificación cuando detectase movimiento. Para ellos hice dos funciones una aque apaga el WiFi y otra que lo conecta. (En realida si que tenia otra opción usar un PIR en lugar de un radar y ya no tendria problemas con el WiFi pero seria menos divertido)

void onWiFi(){  
  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, password);

  // Wait for connection
  while((WiFiMulti.run() != WL_CONNECTED)) {
    delay(500);
  }
}

void offWiFi(){
  WiFi.disconnect(); 
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
}

Por lo que caundo se detecte movimiento habra que conectar el WiFi, enviarle mensaje y deconectarlo para volver a tener la alarma lista.

if(analogRead(A0) > 800){
  Serial.println("Alarm!!!!!!");      
  onWiFi();
  sendMsg();        
  offWiFi();
  firstStart = true;
}

firstStart es una variable booleana cuya funcion es dar tiempo «a correr» una vez conectada la alarma ya que si no saltaria al detectarte a ti mismo nada más activar la alarma. Tambien permite un tiempo de «descanso» tras el disparo de la alarma para evitar estar dando alarmas sin para y saturar el movil de notificaciones.

if(firstStart){
  Serial.println("Time to hide");    
  delay(60000);
  Serial.println("Watching...");
  firstStart = false;    
}  

Vamos aver como es el codigo que envia la notificacion. He optado por usar el servicio de IFTTT para ello hay que crearse una cuenta alli y activar un webhook, luego se crea una regla que lance una notificacion en el movil. O si se prefiere cualquiera de la opciones que disponibles.

void sendMsg(){
   http.begin("http://maker.ifttt.com/[tu codigo]/alarm/with/key/[pon aqui tu key]"); //HTTP

  int httpCode = http.GET();

  if(httpCode > 0){
    Serial.println("Alarm send");      
  } else {
    Serial.println("Error send alarm");     
  }

  delay(1000);
}

Mientras el nodeMCU este transmitiendo el radar queda inutilizado.

El codigo completo es:

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

#include <ESP8266HTTPClient.h>

const char* ssid = "[tu ssid]";
const char* password = "[password]";

bool firstStart = true;
unsigned long timeNewMeasure = 0;

HTTPClient http;
ESP8266WiFiMulti WiFiMulti;

void setup(void){  
  Serial.begin(115200);
  offWiFi();
  //timeNewMeasure = millis()+1000;
}

void offWiFi(){
  WiFi.disconnect(); 
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
}

void sendMsg(){
   http.begin("http://maker.ifttt.com/[tu codigo]/alarm/with/key/[pon aqui tu key]"); //HTTP

  int httpCode = http.GET();

  if(httpCode > 0){
    Serial.println("Alarm send");      
  } else {
    Serial.println("Error send alarm");     
  }

  delay(1000);
}

void onWiFi(){  
  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, password);

  // Wait for connection
  while((WiFiMulti.run() != WL_CONNECTED)) {
    delay(500);
  }
}

void loop(void){
  if(firstStart){
    Serial.println("Time to hide");    
    delay(60000);
    Serial.println("Watching...");
    firstStart = false;    
  }  
  if(millis() > timeNewMeasure){
    timeNewMeasure = millis()+100;
    if(analogRead(A0) > 800){
      Serial.println("Alarm!!!!!!");      
      onWiFi();
      sendMsg();        
      offWiFi();
      firstStart = true;
    }
  }
}

Test de código en Arduino

Aprovechando lo que hemos creado en la entrada sobre el debug en arduino vamos a usarlo para para realizar test en el código de nuestros programas en arduino.

Al igual que el código debug, todo el código de test ha de desaparecer del código compilado cuando se quite el #define TEST. Por ejemplo en este caso cuando hacemos tests en lugar de devolver el valor de la entrada analógica fija uno por defecto, sin embargo cuando no hagamos test funcionara de forma correcta.

#ifdef TEST
  //código que se ejecutara para los test
  sensor = 128;
#else
  //Código que se ejecutara cuando no haya test
  sensor = analogRead(sensorPin);
#endif

También necesitamos una función que nos envíe por el puerto en serie si un test ha sido correcto o incorrecto. Como verificar le null a veces tiene su complejidad se ha creado otra función propia para ello.

void test(bool t){
  if(!t){
    Serial.println("False");
  } else {
    Serial.println("True");
  }
}

void testNull(void* o){
  if(o == ((void *)0)){
    Serial.println("is null");
   } else {
    Serial.println("is no null");
  }
}

En lugar de llamarlas directamente se hace usando las macros TEST(X) y TESTNULL(X). Para que desaparezcan cuando se desactiven los test.

¿Pero que pasa si cuando un test falla queremos para la ejecución? Por ejemplo porque puede ser peligroso para el circuito o porque algún elemento no se ha inicializado correctamente. Para eso se han definido dos funciones idénticas pero dentro de la etiqueta ERROR ya que pueden ser utiles en la ejecución habitual del programa cuando ya no se hacen test. ASSERT(X) y ASSERTNONULL(X)*. La diferencia con TEST es que si son ciertas paran la ejecución del programa llamando a la función stop() definida en la librería.

void stop(){
  Serial.print("STOP!!!");
  Serial.flush();
  noInterrupts();
  while(1){
    delay(1000);
  };
}

Esta función bloquea la ejecución del código creando un bucle infinito y bloqueando las interrupciones. Es una salida segura cuando no sabes como continuar y hacerlo podría dañar algo. También se le puede llamar directamente usando la macro STOP

Un ejemplo de uso:

#define DEBUG
#define TRACE
#define INFO
#define ERROR
#define TEST

#include <Debug.h>

int c = 0;

void(* reset) (void) = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  TRACEMSG("Start loop");
  inc();
  TEST(c 40)
      STOP
  #endif

  delay(500);
}

void inc(){
  ENTER
    c++;
  EXIT
}

La librería puede encontrarse en mi github

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

Debug en Arduino

La forma habitual de hacer debug en arduino usar la instrucción Serial.print() para mostrar en la consola de monitorización del IDE de arduino los datos.

Este sistema tiene varias pegas:

  • Aumenta el tamaño del codigo y el espacio que ocupa el mismo
  • Consume tiempo de ejecución
  • Interfiere con el uso del puerto USB o de la comunicacion serie
  • No aporta ninguna informacion para saber en que parte del codigo ocurre.
  • No distingue entre mensajes de log, traza, debug, error…..

Una forma de resolver parte de estos problemas es usando el preprocesador de C.La idea es definir una serie de macros cuyo valor dependa de que se haya definido una variable del preprocesador. De tal forma que si la variable no esta definida la macro se reemplazada por nada asi evitamos que cuando no consuma espacio en memoria, que interfiera con el uso del puerto serie cuando no estamos depurando.

#ifdef DEBUG
#define DEBUGPRINT(X) Serial.print(X);
#else
#define DEBUGPRINT(X) // nothing
#endif

De tal forma que cuadno queremos depurar definimos la siguiente variable

#define DEBUG

Y todas los sitios dodne aparezca DEBUGPRINT(X) seran sustituidos por Serial.print(X); Pero cuando la quitamos todas las lineas son reemplazadas por nada y desaparecen del programa no ocupando ni esapcio ni tiempo de ejecución.

Hay que definir otro más, DEBUGPRINTLN(X) para el caso de que en lugar de quere un Serial.print(X) queramos usar un Serial.println(X)

Ademas vamos a permitir incluir alguna informacion de en que funcion, archivo y linea del codigo estmos.

#define DEBUGMSG(X) \
  Serial.print("DEBUG: ");  \
  Serial.print(__PRETTY_FUNCTION__); \
  Serial.print(' '); \
  Serial.print(__FILE__); \
  Serial.print(':'); \
  Serial.print(__LINE__); \
  Serial.print(' '); \
  Serial.println(X);

Podemos definir varias macros según el los «niveles» de log mostrando solo los que nos interesen, en nuestro caso vamos a definir cinco:
DEBUG
TRACE
INFO
ERROR
TEST

Al final nos quedan quince macros, a las que se han añadido dos más ENTER y EXIT para indicar la entrada y salida de una función que resultan muy útiles para la traza del código.

DEBUG:

DEBUGPRINT(X) Serial.print(X);
DEBUGPRINTLN(X) Serial.println(X);
DEBUGMSG(X) Serial.println(«DEBUG function file.ino:line X»);

TRACE:

TRACEPRINTLN(X) Serial.println(X);
TRACEPRINT(X) Serial.print(X);
TRACEMSG(X) Serial.println(«TRACE function file.ino:line X»);
ENTER Serial.print(«TRACE: ENTER -> function»);
EXIT Serial.print(«TRACE: EXIT -> function»);

INFO:

INFOPRINTLN(X) Serial.println(X);
INFOPRINT(X) Serial.print(X);
INFOMSG(X) Serial.println(«INFO function file.ino:line X»);

ERROR:

ERRORPRINTLN(X) Serial.println(X);
ERRORPRINT(X) Serial.print(X);
ERRORMSG(X) Serial.println(«ERROR function file.ino:line X»);

TEST:

TESTPRINTLN(X) Serial.println(X);
TESTPRINT(X) Serial.print(X);
TESTMSG(X) Serial.println(«TEST function file.ino:line X»);

Todo estas macros y alguna funcionalidad más se puden encontrar en la libreria debugino.

En un ejemplo de su uso podemos ver como funciona. Hay que señalar la necesidad de inicializar la comunicación serie con Serial.begin(9600);. Para configurar que mensajes se muestran y cuales no basta con quitar el #define correspondiente y esos mensajes desaparecerán.

#define DEBUG
#define TRACE
#define INFO
#define ERROR
#define TEST

#include <Debug.h>

int c = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  TRACEMSG("Start loop");
  inc();
  TEST(c < 30);
  DEBUGPRINT("Value of C ")
  DEBUGPRINTLN(c);
  #ifdef ERROR
    if(c > 40)
      STOP
  #endif

  delay(500);
}

void inc(){
  ENTER
    c++;
  EXIT
}

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

También puedes ver el vídeo sobre este post en mi canal:

Haz click para ver le vídeo en mi canal de youtube

Interpretar lenguaje natural en un bot usando expresiones regulares

En una entrada anterior hemos visto como capturar voz en el navegador y convertirla en mensajes de texto. Ahora nos toca tratar esos mensajes para poder lanzar el comando adecuado. Cómo realmente lo único que necesitamos entender son comandos muy simples y predefinidos el sistema va a ser relativamente sencillo. Para procesar textos más complejos exiten sistemas más avanzados que requiren procesamiento del lenguaje natural. Estos sistemas requieren un conocimiento previo del lenguaje que van a procesar tanto por parte de la libreria como por parte del programador. Las expresiones regulares son sencillas de definir por el programador y para un sistema tan simple funcionan perfectamente.

En esta entrada usaremos la librería jsBotVoice que ayuda a usar expresiones regulares para interpretar los comandos.

Por lo general los bots habitualmente funcionan definiendo varias combinaciones de frases por cada comando precedidas del nombre del bot, «chispas» en nuestro caso. Por ejemplo las frases para preguntarnos la hora pueden ser:

«¿Qué hora es?»
«Dime la hora»
«Di la hora»
«¿Me dices la hora?»
«Hora»

Sin contar que puede ir precedida de cosas como. «Por favor».

Para facilitar el trabajo la mayoría de las librerías llevan herramientas para reducir el número de reglas necesarias permitiendo agrupar varias expresiones bajo una misma regla. En este caso vamos a usar expresiones regulares. Para ello dividiremos cada expresión en tokens que van asociados a expresiones regulares que describen su parte de la frase.

En nuestro caso el modelo sería:

bot please# tell time any

Cada token se traduciría en las siguientes expresiones regulares:

bot: chispa|chispas
please:por favor|porfa
tell: que|di[a-z]|me dices
time: la hora|hora
any: .

El problema es que al usar expresiones regulares tan poco concretas reconoceremos como válidas expresiones incorrectas como:

«chispas divaga hora y media»

En este caso podriamos añadir di|dim|dinos en lugar de di[a-z]*.

Hay que buscar un equilibrio entre flexibilidad de las expresiones y concretar los resultados.

Hemos logrado traducir todas las maneras de decirlo en una sola expresión pero si necesitáramos más no habría problema. Por ejemplo supongamos que queremos añadir:

«¿En que punto de la corriente temporal me encuentro?»

Podemos añadirla directamente por ejemplo:

bot: chispa|chispas|chispita
please:por favor|porfa
tell: en que|que|di[a-z]|me dices
time: la hora|hora
any: .

temporalPoint: punto de la corriente temporal

Que se traduciría en un nuevo comando de voz:

bot please# tell temporalPoint any

Vamos a ponernos con el código, primero definir los tokens:

var tokens = {
  'bot': 'chispa|chispas|chispita',
  'please': 'por favor|porfa',
  'tell': 'que|di[a-z]*|me dices',
  'time': 'la hora|hora',
  'any': '.*',
  'temporalPoint': 'punto de la corriente temporal'
}

Ahora con esos tokens podemos definir las expresiones que el bot va a entender


var vc1 = new VoiceCommand();
vc1.name="tell me time";
vc1.expressions[0] = "bot please# tell time any";
vc1.expressions[1] = "bot please# tell TemporalPoint any";
vc1.execute = function(exp, m, voice) {
 sayTime();
};

Cuando nuestro bot oiga un comando de voz que encaje con las expresiones definidas ejecutara el comando definido en execute al que se le pasa como parámetros:

  • exp: el indice de la expresión que ha coincidido
  • m: array con la coincidencia de texto de cada token de la expresión. m[0] incluye todo el texto
  • voice: referencia al al objeto Voice

Para iniciar el bot a escuchar basta con ejecutar init.

voice.init();

Pero si se quiere probar como reconoce las frases sin iniciar la parte de reconocer el audio puede usarse la función analyze pasando el texto a analizar.

voice.analyze(text);

Hay que tener en cuenta que hay que realizar pruebas reales para detectar errores habituales en el reconocimiento del habla. Por ejemplo en el caso del nombre del bot (bot: chispa|chispas) en se ha incluido chispa porque una gran cantidad de veces se reconocia la palabra «chispas» como si fuera «chispa».

En la web de la librería se puede encontrar más información.