Encontrando la sílaba tónica

La sílaba tónica es aquella que se pronuncia con más énfasis en la frase. Encontrarla es útil para saber el significado de las palabras ya que muchas pueden cambiar con la pronunciación. Por ejemplo: médico, medico y medicó.

El algoritmo para encontrarla en español es muy simple. Vamos a suponer que ya tenemos la palabra dividida en sílabas. Si es monosílaba, no  y hay mucho donde elegir, está claro que esa sílaba es la tónica. Si tiene más sílabas hay que buscar cual tiene la tilde. Si no la tiene ninguna solo puede ser llana o aguda. Si termina en «n»,»s» o vocal y no tiene tilde es llana. Si termina en cualquier otra letra es aguda.

Y ya está, simple y rápido.

Este algoritmo está incluido en la librería jsESsyllable .


var syllable = new jsESsyllable();

var word = "...";
var syllables = syllable.divide(word);
var strong = syllable.stress(syllables);

Puedes probarlo online.

Separar palabras en sílabas

Este algoritmo esta inspirado en el paper «A Syllabification Algorithm for Spanish» de Heriberto Cuayáhuitl. Pero realizando algunos cambios.

Para separar las palabras en sílabas usamos un algoritmo de dos pasos.

Paso 1

La clave del primer paso está en analizar las consonantes entre vocales. Podemos tener los siguientes casos: VCV, VCCV, VCCCV, VCCCCV. Siempre dividimos por delante de la última consonante: V|CV, VC|CV, VCC|CV, VCCC|CV . Sin embargo si la última consonante es una ‘r’, ‘l’ o ‘h’. Estamos ante una estructura de dos consonantes como cr, fr, rr, ll, ch,… Por lo que desplazaremos el corte a la consonante anterior. Hay que tener en cuenta algún detalle más. Cuando hay varias vocales juntas actúan como si fueran una sola. Así que realmente las V pueden representar a varias vocales.

El algoritmo necesita una variable temporal donde se almacena lo queda a la derecha del punto de corte. Pero antes de actualizarla esa variable hay que tomar el valor almacenado en ella, añadirle lo que queda a la izquierda del punto de corte y almacenarlo como una de las silabas. Puede que algunas de la silabas no sean correctas, de ellas se ocupará el paso 2.

El algoritmo elimina la parte analizada de la palabra y es como si siempre estuviera al principio de la misma. Para ello el algoritmo al cortar un bloque de vocales y consonantes toma el lado de la izquierda, lo junta con lo que haya almacenado en la variable temporal (el lado de la derecha del anterior corte), eso forma una silaba y la guarda. Luego almacena en la variable la parte de la derecha del corte hasta poder unirlo con la parte izquierda del siguiente corte (si no hubiera más cortes lo guarda y finaliza).

Al final las reglas que quedan son:

La palabra comienza por….GuardarVariable Temporal (T)
V -> | VV
CV -> | CVTCV
C[rlh]V -> | C[rlh]VTC[rlh]V
CCV -> C | CVT+CCV
CC[rlh]V -> C | C[rlh]VT+CC[rlh]V
CCCV -> CC | CVT+CCCV
CCC[rlh]V -> CC | C[rlh]VT+CCC[rlh]V
CCCCV -> CCC | CVT+CCCCV
C* -> C* |T+C*

Siendo: V = vocal/es, C = Consonante, [rlh] = consonante que sea r, l, h, T = variable temporal, | = punto de corte

La última regla hace referencia a que solo quedan caracteres y ninguna vocal, en ese caso se unen todo a lo que haya en la memoria temporal.

Ejemplos:

PalabraReglaBloqueGuardarTemporalSílabas
instrumentoV -> |V| i i
nstrumentoCCC[rlh]V -> CC | C[rlh]Vns | trui+nstruins,
mentoCV -> | CV| metrumeins, tru
ntoCCV -> C | CVn | tome+ntoins, tru, men
  to ins, tru, men, to
PalabraReglaBloqueGuardarTemporalSílabas
trompetaC[rlh]V -> | C[rlh]V| tro tro
mpetaCCV -> C | CVm | petro+mpetrom,
taCV -> | CV| tapetatrom, pe
tatrom, pe, ta
PalabraReglaBloqueGuardarTemporalSílabas
aviónV -> |V| a a
viónCV -> | CV| vióavióa,
nC* -> C* |n | vió + na, vión

 Paso 2

En el segundo paso repasamos cada uno de los grupos creados en el paso anterior y vamos a dividir los grupos de vocales cuando sea necesario. Separaremos las vocales en dos grupos: débiles {i,u} y fuerte {a,e,o}. Denotaremos el grupo de débiles con d y el de fuertes con f. A su vez pueden estar acentuadas o no, lo indicaremos como D {í,ú} y F {á,é,ó}.

Para dos vocales juntas las reglas son:

  • Permanecen juntas: dd, df, dF, Fd
  • Se separan:  ff, Ff, fF, Df, fD

En el caso de tres vocales solo se separan si hay dos fuertes juntas.

  • {iuüíú}{aeoáéó}{aeoáéó} -> {iuüíú}{aeoáéó} | {aeoáéó}
  • {aeoáéó}{aeoáéó}{iuüíú} -> {aeoáéó} | {aeoáéó}{iuüíú}

Ejemplo:

«avión» tras el paso uno queda dividida en «a»,»vión». Al ser «vión» un caso dF las vocales no se dividen, el paso dos devolverá «a»,»vión»

«rocíen» se separará en el paso uno en «ro»,»cíen». Como íe es un caso de Df se separara «ro»,»cí»,»en»

En cambio «ca»,»ca»,»hue»,»te» en el caso de «hue» al ser df seguirán juntas.


Todo esto está recopilado en la librería jsESsyllable.

var syllable = new jsESsyllable();

var word = "...";
var syllables = syllable.divide(word);

Puedes usar la demo online.

Tienes un vídeo explicando este mismo algoritmo en mi canal de youtube:

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

Capturar vídeo de la cámara del dispositivo en HTML5

Para dotar a nuestro agente de ojos debemos poder acceder a las cámaras del dispositivo. Para ellos nuestra web va a necesitar un elemento vídeo y otro canvas. Enlazamos el vídeo con la webcam del dispositivo y cada cierto tiempo tomamos una captura que copiaremos sobre el canvas para poder acceder a los pixels y aplicar nuestros algoritmos de visión por computador.
Empecemos por incluir una etiqueta vídeo y una canvas en nuestra web:



Las funciones que nos da HTML5 para acceder al vídeo se basan en la API navigator.mediaDevices. En versiones antiguas de los navegadores pierdes encontrarte el problema de que los nombres no están unificados y cada uno lo llamaba de una manera. Actualmente ya lo están. Algo parecido nos pasa con la API window.URL que vamos a usar para poder conectar la webcam a nuestro elemento vídeo. Para que no haya problemas entre navegadores usaremos:

window.URL = window.URL
|| window.webkitURL
|| window.mozURL
|| window.msURL;

Nos hace falta acceder a cada uno de los elementos. El vídeo, el canvas y el contexto del canvas del cual leeremos la imagen capturada del vídeo. Para ello usaremos el id del tag.

video = document.getElementById(videoId);
canvas = document.getElementById(canvasId);
context = canvas.getContext('2d');

Para enlazar la webcam con el elemento vídeo antes hay que saber que webcam es ya que un dispositivo puede tener varias, por ejemplo un móvil tiene frontal y trasera. Para saber que dispositivos hay puedes usar la API navigator.mediaDevices.enumerateDevices . Una vez seleccionado el dispositivo que quieras usar se emplea el metodo getUserMedia() pasandole una estructura del tipo MediaStreamConstraints donde describes los requisitos que necesitas que tenga el dispositivo.  En nuestro caso va ser mas simple vamos a contemplar solo la cámara frontal, la trasera y la por defecto.

//Cámara frontal
device.video = {facingMode: "user", deviceId: ""};
//Cámara trasera
device.video = {facingMode: "environment", deviceId: ""};
//Cámara por defecto (frontal en los móviles)
device.video = {deviceId: "default"};

También podemos desear ajustar la resolución:

device.video.width = 320;
device.video.height = 240;

y el framerate.

this.configuration.framerate = 25;

Hay que decir que realmente estas condiciones no obligan que devolver un dispositivo que las cumpla, solo aconsejan que lo sea, así que no puedes confiar que la resolución sea la deseada y debes de verificarlos.

navigator.mediaDevices.getUserMedia(device)
.then(
  function(stream) { ...}
).catch(...)

Una vez conectada a la webcam hemos de conectar esta con el elemento vídeo de la web para ello usamos window.URL.createObjectURL :

navigator.mediaDevices.getUserMedia(device)
.then(
  function(stream) {
    var src = window.URL.createObjectURL(stream);
    video.src = src;
  }
).catch(
  function(e) { console.log("Video error: "+e); }
);

Y por ultimo cada cierto tiempo hemos de capturar el frame que se esta mostrando en el video y volcarlo al canvas, desde donde podremos acceder a todos sus pixeles. El proceso es muy sencillo, tan sencillo como copiarla al canvas usando el método drawimage

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.context.drawImage(video, 0, 0, this.video.videoWidth, this.video.videoHeight);

Lo mismo con videoToCanvas

Para hacer todo esto más sencillo se ha creado la librería videoToCanvas

//id del tag canvas y video
var v2c = new VideoToCanvas("canvas", "video");
//por defecto es 320x240
v2c.configuration.width=640;
v2c.configuration.height=480;
v2c.webcam();

Para realizar la captura al canvas es tan simple como:

v2c.snap();

Ademas cuenta con gran cantidad de funciones para controlar la reproducción del vídeo de la cámara o para cargar en su lugar un vídeo (útil para pruebas).

Acceder a los pixels de un canvas

Con esto tenemos ya el primer paso dado. Capturar la imagen. Antes de empezar a manipular los datos de un canvas tenemos que ver cómo trabajar con ellos.
Para recuperar los datos del canvas usamos la función getImageData del contexto

var imageData = canvas.context.getImageData(0, 0, width, height);
var data = imageData.data;

getImageData nos permite recuperar solo parte de la imagen indicando las coordenadas X e Y de la esquina superior izquierda del rectángulo de la imagen a recuperar y el ancho y el alto. En caso de que con las medidas especificadas recuperemos parte de de fuera de la imagen se devolverán pixels negros transparentes

Usando videoToCanvas tienes dos métodos getImageData(x, y, width, height) y getBoxes(rows, cols). el primero actúa igual que el getimageData del canvas.context y devuelve un ImageData. El segundo divide la imagen en rows*cols partes y devuelve un array de ImageData. Este método es útil cuando se va a trabajar en paralelo sobre distintas partes de la imagen.

En el ImageData devuelto encontramos las siguientes propiedades:

  • ImageData.height Alto de la imagen
  • ImageData.width Ancho de la imagen
  • ImageData.data Un array de bytes que contiene los pixels de la imagen en formato RGBA

Los píxels de la imagen se recuperan en formato RGBA. Que significa que cada píxel ocupa 4 bytes. Los 3 primeros se dedican a los componentes de color: rojo, verde y azul. El cuarto al nivel de transparencia, también llamado alpha. De esa forma data[0] corresponde al valor de la componente roja del primer píxel, data[1] a la verde, data[2] a la azul y data[3] a la alpha. Después repetimos esa misma distribución con data[4], data[5], data[6] y data[7]. Y así una y otra vez durante toda el array.

Los valores de cada pixel varían entre 0 y 255 e indican la intensidad de ese color en el pixel. Una ventaja del tipo de datos Uint8ClampedArray es que no hace falta comprobar los límites al asignarle un valor, todo valor menor que 0 se convierte a 0 y todo valor mayor de 255 se convierte a 255. Su mayor problema es que es un tipo bastante lento para operar con él así que vamos a tratar de reducir el número de operaciones sobre el mismo.