header-ia-sentiment

Clasificación de texto con JavaScript y TensorFlow.js: Crea una aplicación web de análisis de sentimiento

Clasificación de texto con JavaScript y TensorFlow.js: Crea una aplicación web de análisis de sentimiento

La clasificación de texto es una tarea común en inteligencia artificial y aprendizaje automático. Con JavaScript y TensorFlow.js, podemos realizar este tipo de tareas directamente en una aplicación web, sin depender de servidores externos. En esta guía, aprenderás cómo crear una aplicación interactiva que analiza el sentimiento de un texto usando TensorFlow.js y el modelo pre-entrenado Universal Sentence Encoder.

Paso 1: Configuración del entorno

  1. Crea un nuevo proyecto de Node.js ejecutando el siguiente comando:
npm init -y

Esto generará el archivo package.json.

2. Instala las dependencias necesarias para este proyecto:

npm install @tensorflow/tfjs @tensorflow-models/universal-sentence-encoder --force

Usaremos la librería @tensorflow/tfjs para trabajar con TensorFlow en JavaScript y @tensorflow-models/universal-sentence-encoder como modelo pre-entrenado para procesar texto y generar embeddings.

Paso 2: Cargar el modelo Universal Sentence Encoder

El modelo Universal Sentence Encoder genera representaciones numéricas (embeddings) para oraciones. Vamos a cargar este modelo en nuestro proyecto.

Crea un archivo index.js y agrega el siguiente código:

import * as use from '@tensorflow-models/universal-sentence-encoder';

(async () => {
  const sentences = [
    'Me encanta el clima hoy.',
    'No me gusta nada este lugar.',
    'El café está delicioso.',
    'La película fue aburrida.',
  ];

  // Cargar el modelo pre-entrenado
  const model = await use.load();
  console.log('Modelo Universal Sentence Encoder cargado.');

  // Generar embeddings para las oraciones
  const embeddings = await model.embed(sentences);
  console.log('Embeddings generados:', embeddings.arraySync());
})();
  • Este código carga el modelo y genera embeddings (vectores numéricos) para una lista de oraciones.
  • Estos embeddings serán la base para analizar el sentimiento o clasificar el texto.

Paso 3: Clasificación de texto (simulada)

Como el modelo Universal Sentence Encoder no clasifica directamente el texto, crearemos una regla sencilla para simular el análisis de sentimiento utilizando los embeddings.

Actualiza tu index.js con el siguiente código:

async function classifyText(model, text) {
  const embeddings = await model.embed();
  const score = embeddings.arraySync()[0].reduce((sum, value) => sum + value, 0);

  // Simulación: Sentimiento positivo si el puntaje es mayor que 0
  return score > 0 ? 'positivo' : 'negativo';
}

(async () => {
  const sentences = [
    'Me encanta el clima hoy.',
    'No me gusta nada este lugar.',
    'El café está delicioso.',
    'La película fue aburrida.',
  ];

  const model = await use.load();
  console.log('Modelo cargado exitosamente.');

  for (const sentence of sentences) {
    const sentiment = await classifyText(model, sentence);
    console.log(`"${sentence}" es ${sentiment}.`);
  }
})();
"Me encanta el clima hoy." es positivo.
"No me gusta nada este lugar." es negativo.
"El café está delicioso." es positivo.
"La película fue aburrida." es negativo.

Paso 4: Integración en una aplicación web

Ahora integraremos el modelo en una aplicación web para que los usuarios puedan ingresar su propio texto y obtener una clasificación.

Creamos un archivo index.html con la siguiente estructura:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Clasificación de Sentimiento con TensorFlow.js</title>
  </head>
  <body>
    <h1>Análisis de Sentimiento</h1>
    <div>
      <textarea id="input-text" rows="4" cols="50" placeholder="Escribe aquí..."></textarea>
      <br />
      <button id="classify-button">Clasificar</button>
      <p>Resultado: <span id="result"></span></p>
    </div>

    <!-- Importar TensorFlow.js y Universal Sentence Encoder -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]/dist/universal-sentence-encoder.min.js"></script>
    <script src="index.js"></script>
  </body>
</html>

Archivo JS: index.js

Este script conecta el HTML con el modelo de TensorFlow.js:

let model;

async function loadModel() {
  model = await use.load();
  console.log('Modelo cargado exitosamente.');
}

async function classifyText(text) {
  const embeddings = await model.embed();
  const score = embeddings.arraySync()[0].reduce((sum, value) => sum + value, 0);
  return score > 0 ? 'positivo' : 'negativo';
}

async function handleClassifyClick() {
  const inputText = document.getElementById('input-text').value;
  const resultElement = document.getElementById('result');

  if (!inputText) {
    resultElement.textContent = 'Por favor, ingresa un texto.';
    return;
  }

  const sentiment = await classifyText(inputText);
  resultElement.textContent = `Sentimiento: ${sentiment}`;
}

// Inicializar
loadModel();
document.getElementById('classify-button').addEventListener('click', handleClassifyClick);

Conclusión

En este artículo, aprendimos cómo usar TensorFlow.js y el modelo Universal Sentence Encoder para analizar y clasificar texto en una aplicación web.

Hemos visto:

  1. Cómo configurar el entorno y cargar el modelo.
  2. Cómo generar embeddings para el texto.
  3. Cómo integrar la funcionalidad en una aplicación interactiva que puede analizar el sentimiento de cualquier texto ingresado por el usuario.

Con esta base, puedes expandir el proyecto utilizando modelos más complejos o entrenando tus propios clasificadores. ¡El límite es tu imaginación!

shorturl

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

En el vertiginoso mundo digital, compartir enlaces largos y complejos puede resultar engorroso. Es en este escenario donde Tecnops URL Shortener, una herramienta desarrollada con tecnologías tan conocidas y usadas como Node.js, Express, JavaScript, CSS y HTML, se presenta como la solución ideal para simplificar y optimizar tus enlaces, ofreciéndote una experiencia de navegación más eficiente.

Creando una URL Corta Personalizada

  1. Accede a la plataforma Tecnops URL Shortener visitando //tecnops.es:10000 en tu navegador favorito.
  2. En el campo URL, introduce la URL que deseas acortar.
  3. Asigna un alias único y fácil de recordar en el campo Alias. Por ejemplo, podrías utilizar mi-alias.
  4. Haz clic en el botón Acortar para generar tu URL corta personalizada con el alias.

crear url

Accediendo a tu URL Acortada

Para acceder a la URL acortada que has creado, sigue estos simples pasos:

  1. Utiliza el siguiente formato de URL: //tecnops.es:10000/code/mi-alias.
  2. Reemplaza mi-alias con el alias que asignaste al acortar la URL.

Al abrir esta URL en tu navegador, experimentarás una redirección automática hacia la URL original asociada con el alias.

Este proceso, respaldado por las tecnologías líderes de Node.js, Express, JavaScript, CSS y HTML, hace que compartir enlaces sea más sencillo y elegante, permitiéndote mantener el control sobre tus enlaces y mejorar la experiencia de tus usuarios.

Beneficios de Tecnops URL Shortener:

  • Personalización: Asigna alias significativos para tus enlaces y crea una identidad única.
  • Gestión Sencilla: Accede a una interfaz intuitiva para crear tus enlaces acortados.
  • Eficiencia: Comparte enlaces de manera efectiva, ya sea en redes sociales, correos electrónicos o mensajes.

Conclusión: Tecnops URL Shortener, construido con tecnologías avanzadas, redefine la forma en que interactuamos con los enlaces en línea. ¡Simplifica tu experiencia en la web hoy mismo y descubre el poder de tus enlaces acortados con Tecnops!

totp-2fa

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

En esta nueva entrada vamos a realizar un sistema de autenticación de doble factor  (2FA) usando Node.js en el Back End y Javascript en el Front End.

Bien, antes de empezar a codificar vamos a definir dos conceptos que son la base de este tutorial: 2FA y TOTP.

Las siglas 2FA provienen del ingles Two Factor Authentication (autenticación de dos factores) y TOTP de las siglas Time-based One-Time Password (Contraseña de un solo uso basada en el tiempo).

Como funciona

Cuando accedemos a nuestra cuenta de usuario lo hacemos mediante nuestro usuario y una contraseña, pero ahora vamos añadir un sistema de autenticación con el cual realizaremos una segunda validación.

Con este sistema ya tendríamos la autenticación de dos factores (2FA), pero necesitamos dotar a este sistema de mayor seguridad y por eso en los últimos años se ha añadido una nueva característica, un algoritmo conocido como contraseñas de un solo uso basadas en tiempo (TOTP).

Esta técnica nos ofrece mayor seguridad ya que nos permite validar nuestra identidad mediante un código generado en paralelo en un servidor y en una aplicación (normalmente en el teléfono), que se genera y actualiza cada 30 segundos.

Todo este conjunto nos ofrece un sistema muy seguro, ya que necesitamos conocer el usuario, la contraseña y además tener registrado en nuestro teléfono un sistema de validación que esta constantemente generando códigos de un solo uso y con un tiempo limitado para su uso.

Tecnologías y funcionamiento

La aplicación la he creado usando Javascript en el Front End, Node.js en el Back End y para el estilo visual he usado la librería de componentes Metro UI.

qr-app-2fa-totpComo podemos ver en la imagen la aplicación es extremadamente sencilla, simplemente pulsamos en el botón para generar un nuevo código QR.

Con este código registraremos el token de autorización y validación en Google Authenticator, el cual nos generará automáticamente cada 30 segundos un nuevo código de autorización.

google-authenticatorSolo queda añadir el código numérico que ha generador Google Authenticator y comprobarlo en la aplicación.

Como hemos podido ver, es un sistema muy sencillo de integrar en cualquier aplicación y nos permite asegurar el acceso a nuestras aplicaciones.

Para finalizar, desde aquí podéis acceder al repositorio para obtener el código fuente completo.

Patrones de diseño y ejemplos de uso en Javascript

Patrones de diseño y ejemplos de uso en Javascript

En esta entrada veremos que son los patrones de diseño, ya que nos aportan una solución que se aplica al diseño de software y no a un lenguaje de programación en concreto.

Por lo tanto podríamos definir un patrón de diseño como:

Una solución estandarizada y reutilizable que nos permite solucionar problemas comunes cuando desarrollamos cualquier tipo de software.

Debemos entender que un patrón de diseño nos permite resolver problemas con soluciones estandarizadas, y por lo tanto acelerar el desarrollo ya que existe una solución establecida.

Otra cosa que debemos tener en cuenta es no excedernos en el uso de patrones de diseño ya que esto podría volverse en nuestra contra aumentando la complejidad de nuestro software.

Agrupando los patrones de diseño

Existen múltiples patrones de diseño pero todos ellos se pueden agrupan en 3 categorías:

  • Patrones creacionales
    • Solucionan problemas en la creación de instancias, de tal forma que su creación nos permite desacoplarlo de nuestra aplicación.
  • Patrones estructurales
    • Definen como usar estructuras complejas a partir de elementos simples.
  • Patrones de comportamiento
    • Nos permiten desacoplar nuestro código.

Patrones creacionales

En este grupo se engloban los patrones que permiten la creación de objetos, ofreciendo flexibilidad y reutilización.

  • Patrón Constructor:  Este patrón nos permite crear una variedad de objetos complejos desde un objeto raíz, ya que el mismo esta compuesto por diferentes partes que contribuyen a la creación de cada objeto complejo.
    • class Home {
          constructor() {
              this.bathroom = 0;
              this.bedroom = 0;
              this.diningRoom = 0;
              this.kitchen = 0;
              this.livingRoom = 0;
          }
          setBathroom(value) {
              this.bathroom = value;
              return this;
          }
          setBedroom(value) {
              this.bedroom = value;
              return this;
          }
          setDiningRoom(value) {
              this.diningRoom = value;
              return this;
          }
          setKitchen(value) {
              this.kitchen = value;
              return this;
          }
          setLivingRoom(value) {
              this.livingRoom = value;
              return this;
          }
          getHome() {
              return {
                  bathroom: this.bathroom,
                  bedrrom: this.bedroom,
                  diningRoom: this.diningRoom,
                  kitchen: this.kitchen,
                  livingRoom: this.livingRoom,
              };
          }
      }
      const home1 = new Home()
                      .setBedroom(4)
                      .setBathroom(2)
                      .setDiningRoom(1)
                      .setKitchen(1)
                      .setLivingRoom(2);
      
      const home2 = new Home()
                      .setBedroom(1)
                      .setBathroom(1)
                      .setKitchen(1)
                      .setLivingRoom(1);
      
      console.log(home1.getHome()); // {bathroom: 2, bedrrom: 4, diningRoom: 1, kitchen: 1, livingRoom: 2}
      console.log(home2.getHome()); // {bathroom: 1, bedrrom: 1, diningRoom: 0, kitchen: 1, livingRoom: 1}
  • Patrón Módulo: Con este patrón podemos encapsular toda la lógica de nuestra aplicación, ya que dentro del módulo estarán declaradas todas las variables y métodos.
    • const LIBRARY = { 
          API: async (url = '' ,params = {}) => { 
              const urlAPI = new URL(url); 
              urlAPI.search = new URLSearchParams(params).toString(); 
              return await (await fetch(urlAPI)).json(); 
          } 
      };
      
      (async () => { 
          const result = await LIBRARY.API('https://jsonplaceholder.typicode.com/todos'); 
          console.log(result); 
          console.log(LIBRARY);
      })();
  • Patrón Módulo revelador: Misma comportamiento que el anterior patrón pero en este caso tendremos una parte pública y otra privada.
    • const LIBRARY = (() => {
         // Ámbito privado 
         const API = async (url = '' ,params = {}) => {
            const urlAPI = new URL(url);
            urlAPI.search = new URLSearchParams(params).toString();
            return await (await fetch(urlAPI)).json(); 
         } 
         return {
            // Ámbito público
            apiCall: async (url = '', params = {}) => await API(url, params)
         } 
      })(); 
      (async () => {
         const result = await LIBRARY.apiCall('https://jsonplaceholder.typicode.com/todos');
         console.log(result); 
         console.log(LIBRARY); // {apiCall: ƒ} 
      })();
  • Patrón Prototipo: Con este patrón podemos crear un objeto que sirva de modelo para otros objetos mediante la herencia prototípica. Debemos tener en cuenta que nos devuelve un objeto vacío donde las propiedades y los métodos están en el prototipo.
    • const vivienda = {
          direccion: '',
          cp: '',
          poblacion: '',
          provincia: '',
          set: {},
          get: {},
      };    
      Object.keys(vivienda.valueOf()).forEach(value => {
          if (!['set', 'get'].includes(value)) {
              vivienda.set[value] = function(data) {
                  this[value] = data;
              }
              vivienda.get[value] = function() {
                  this[value];
              }
          }
      });
      console.log('OBJETO VIVIENDA', vivienda);
      const v1 = Object.create(vivienda);
      const v2 = Object.create(vivienda);
      console.log(v1, v2, vivienda);
    • Resultado prototype
  • Patrón Singleton: Este patrón nos ofrece una instancia única y proporciona un acceso global a dicha instancia.
    • class SingletonClass {
          static instance;
          constructor() {
              if (SingletonClass.instance) {
                  return SingletonClass.instance;
              }
              SingletonClass.instance = this;
          }
          getInstance() {
              return this.instance;
          }
      }
      const a = new SingletonClass(); // Primera y única instancia
      const b = new SingletonClass(); // Misma instancia que la anterior
      console.log(a.getInstance() === b.getInstance()) // true
    • function SingletonFunction() {
         if (!SingletonFunction.instance) {
             SingletonFunction.instance = this;
         }
         return SingletonFunction.instance;
      }
      const a = new SingletonFunction();
      const b = new SingletonFunction();
      console.log(a === b); // true

Patrones de estructura

Aquí agruparemos los patrones que hacen referencia a la composición de objetos, y que nos simplifica la relación entre ellos.

  • Patrón Mixin: Este patrón nos permite añadir mas funcionalidades a una clase existente sin tener que modificar la clase.
    • const mixin = {
          getNombre() {
              return this.nombre;
          },
          getEdad() {
              return this.edad;
          }
      };
      
      class Persona {
          constructor(nombre = '', edad = 0) {
              this.nombre = nombre;
              this.edad = edad;
          }
      }
      
      Object.assign(Persona.prototype, mixin);
      
      const p1 = new Persona('Iván', 43);
      console.log(p1, p1.getNombre(), p1.getEdad()); // {nombre: "Iván", edad: 43}, edad: 43, nombre: "Iván"
  • Patrón Decorador: Este patrón es parecido al patrón mixin, salvo que en lugar de añadir funcionalidades al prototipo se hace en las instancias de la clase.
    • class Videoclub {
          constructor(title = '', rented = false) {
              this.title = title;
              this.rented = rented;
              this.time = 0;
          }
      }
      
      const person1 = new Videoclub('The mummy', true);
      
      person1.rentedTime = function(value) {
          this.time = value;
      };
      
      person1.rentedTime(2);
      
      console.log(person1); // rented: true, rentedTime: ƒ (value), time: 2, title: "The mummy"
  • Patrón Facade: Este patrón proporciona una interfaz que nos abstrae de una funcionalidad compleja. Al aplicar este patrón solo exponemos lo necesario haciendo el código más simplificado y fácil de utilizar.
    • (() => {
          const Maths = function() {
              // Lógica compleja que aislamos
              const multiply = (number, cont = 0, arr = []) => {
                  arr.push(number * cont);
                  return cont < 10 ? multiply(number, ++cont, [...arr]) : [...arr];
              }
              const saveTable = (number, cb) => {
                  if (tableExist(number)) {
                      window.localStorage.removeItem(`Table ${number}`);
                  }
                  window.localStorage.setItem(`Table ${number}`, cb(number));
                  return window.localStorage.getItem(`Table ${number}`)
              }
              const tableExist = (number) => {
                  return window.localStorage.getItem(`Table ${number}`) !== null;
              }
              
              // Lógica simple que exponemos (fachada - facade)
              this.numberTable = (number) => saveTable(number, multiply);
          }
      
          const t1 = new Maths();
          for (let cont = 1; cont <= 10; cont++) {
              t1.numberTable(cont);    
          }
      })();
  • Patrón adaptador: Este patrón nos permite convertir la interfaz de una clase en otra que se adapte a nuestras necesidades, consiguiendo de esta forma que ambas clases puedan convivir sin problema.
    • (async () => {
          /* ------------------------------ */
          class oldClass {
              constructor() {
                  this.url = '';
              }
              setURL(url) {
                  this.url = url;
              }
              call(cb) {
                  const xhttp = new XMLHttpRequest();
                  xhttp.onreadystatechange = function() {
                      const readyState = this.readyState;
                      const status = this.status;
                      if (readyState === 4 && status === 200) {
                          cb(JSON.parse(this.responseText));
                      }
                  };
                  xhttp.open("GET", this.url, true);
                  xhttp.send();        
              }
          }
      
          oldC.setURL('https://swapi.dev/api/');
          oldC.call((data) => console.log('Old class', data));
          /* ------------------------------ */
          /* ------------------------------ */
          class newClass {
              constructor() {
                  this.url = '';
              }
              setURL(url) {
                  debugger;
                  this.url = url;
              }
              async call() {
                  debugger;
                  return await (await fetch(this.url)).json();
              }
          }
          const newC = new newClass();
          newC.setURL('https://swapi.dev/api/');
          const data = await newC.call();
          console.log('New class', data);
          /* ------------------------------ */
          /* ------------------------------ */
          class adapterClass {
              constructor() {
                  debugger;
                  this.url = '';
                  this.newClass = new newClass();
              }
              setURL(url) {
                  debugger;
                  this.newClass.setURL(url);
              }
              async call(cb) {
                  debugger;
                  const result = await this.newClass.call();
                  cb(result);
              }
          }
          const adapterC = new adapterClass();
          // Tenemos la sintaxis de la clase antigua, pero con la lógica adaptada de la nueva clase
          adapterC.setURL('https://swapi.dev/api/');
          adapterC.call((data) => console.log('Adapter', data));
          /* ------------------------------ */
      })();
  • Patrón proxy: Este patrón nos permite usar un objeto que haga de intermediario de otro, permitiéndonos controlar el acceso a el objeto.
    • (() => {
          const proxyPatter = {};
          (function() {
              // this === proxyPatter
      
              const privateNumber = 100;
              const privateMethod = function() {
                  return privateNumber;
              };
              this.publicMethod = function() {
                  return privateMethod();
              }
              this.getTHIS = function() {
                  return this;
              }
      
          }).apply(proxyPatter);
      
          console.log(proxyPatter);
          // { publicMethod: ƒ, getTHIS: ƒ }
      
          console.log(proxyPatter.publicMethod());
          // 100
      })();

Patrones de comportamiento

En este último grupo añadiremos los patrones que se encargan de la comunicación entre objetos.

  • Patrón observador: Este patrón nos permite tener el control y la gestión de eventos mediante dos objetos, ya que uno de los objetos se encarga de publicar los eventos y el otro suscribirse. Un ejemplo muy usado en Javascrtipt lo podemos ver cuando usamos addEventListener.
    • (() => {
          class ObserverPattern {
              constructor() {
                  this.observers = [];
              }
              on(observer) {
                  return this.observers.push(observer);
              }
              off(observer) {
                  const index = this.observers.indexOf(observer);
                  if (index > -1) {
                      this.observers.splice(index, 1);
                  }
              }
              notify(observer, message) {
                  const [ob] = this.observers.filter(ob => ob === observer);
                  ob?.notify?.(message);
              }
          }
          
          class Person {
              constructor(name) {
                  debugger;
                  this.name = name;
              }
              notify(message) {
                  debugger;
                  console.log('MENSAJE', message);
              }
          }
      
          const p1 = new Person('Person 1');
          const p2 = new Person('Person 2');
          const p3 = new Person('Person 3');
      
          const notify = new ObserverPattern();
          notify.on(p1);
          notify.on(p2);
          notify.on(p3);
          notify.notify(p1, 'Hola P1'); // MENSAJE Hola P1
          notify.notify(p2, 'Hola P2'); // MENSAJE Hola P2
          notify.notify(p3, 'Hola P3'); // MENSAJE Hola P3
          notify.off(p1);               // Eliminamos el observer a P1
          notify.off(p2);               // Eliminamos el observer a P2
          notify.notify(p1, 'Hola P1'); //
          notify.notify(p2, 'Hola P2'); //
          notify.notify(p3, 'Hola P3'); // MENSAJE Hola P3
      
      })();
      
  • Patrón mediador:  Este patrón define un objeto que será el encargado de gestionar la comunicación entre el resto de los objetos.
    • (() => {
          // Mediator 
          class ControlTower {
              constructor() {
                  this.airplane = {};    
              }
              add(airplane) {
                  this.airplane[airplane.name] = airplane;
                  airplane.mediator = this;
              }
              send(message, from, to = null) {
                  if (to) {
                      to.receive(message, from);
                  } else {
                      Object.values(this.airplane).forEach((item, index, arr) => {
                          if (item.id !== from.id) {
                              item.receive(message, from);
                          }
                      });    
                  }
              }
          }
          class Airplane {
              constructor(name) {
                  this.name = name;
                  this.mediator = null;
                  const [id] = window.crypto.getRandomValues(new Uint32Array(1));
                  this.id = id;
              }
              send(message, to) {
                  this.mediator.send(message, this, to);
              }
              receive(message, from) {
                  console.log(`[${this.name}]: You have a message from....${from.name} ==> ${message}`);
              }
          }
      
          const plane1 = new Airplane('P1');
          const plane2 = new Airplane('P2');
          const plane3 = new Airplane('P3');
          const plane4 = new Airplane('P4');
      
          const control = new ControlTower();
          control.add(plane1);
          control.add(plane2);
          control.add(plane3);
          control.add(plane4);
      
          plane1.send('Prueba1');
          plane2.send('Prueba2');
          plane3.send('Prueba3');
          plane4.send('Prueba4');
      
          plane1.send('1 to 2', plane2);
          plane4.send('4 to 3', plane3);
      
      })();
  • Patrón comando: Con este patrón ejecutamos una operación sin saber el contenido de la misma, encapsulando la petición y facilitando su parametrización.
    • (() => {
          const Command = function() {
              this.item = [];
              this.add = function(...args) {
                  debugger;
                  this.item.push(...args);
              }
              this.getItems = function() {
                  debugger;
                  return this.item;
              }
              return {
                  cmd: (command, ...args) => {
                      return this?.[command]?.(args);
                  }
              }
          };
              
          const exec = new Command();
          debugger;
          exec.cmd('add', 1, 2, 3, 4, 5, 'a', 'b', 'c', 'd', true, false, () => 'function');
          console.log(exec.cmd('getItems'));
      })();
  • Patrón cadena de responsabilidad: Este patrón permite pasar solicitudes entre manejadores, ya que cada manejador decide si procesar o pasar al siguiente.
    • (() => {
          class ManipulationText {
              constructor(text, seed) {
                  this.text = text;
                  this.seed = seed;
              }
              cut() {
                  this.text = Array.from(this.text)
                  return this;
              }
              letterToNumber() {
                  this.text = this.text.map(i => {
                      if (i.toLowerCase() === 'a') return 4;
                      else if (i.toLowerCase() === 'b') return 6;
                      else if (i.toLowerCase() === 'e') return 3;
                      else if (i.toLowerCase() === 'l') return 1;
                      else if (i.toLowerCase() === 'o') return 0;
                      else if (i.toLowerCase() === 't') return 7;
                      else return i;
                  });
                  return this;
              }
              convertToBinary() {
                  this.text = this.text.map(i => {
                      const isString = typeof i === 'string';
                      return parseInt(isString ? i.charCodeAt().toString(2) : i.toString(2));
                  });
                  return this;
              }
              compact() {
                  this.text = this.text.map(i => i & this.seed);
                  return this;
              }
              join() {
                  this.text = this.text.join('.');
                  return this;
              }
      
          }
          const seed = Math.floor(Math.random() * 100000) + 1;
          const text1 = new ManipulationText('Hello World', parseInt(seed.toString(2)));
          const crypto = text1.cut().letterToNumber().convertToBinary().compact().join();
      })();
  • Patrón iterador: Con este patrón podemos acceder a los elementos de un objeto secuencialmente sin necesidad de conocer su estructura, y además la responsabilidad de recorrer los elementos recae sobre el iterador.
    • (async () => {
          async function* Iterator(data) {
              let index = 0;
              while (index < data.length) {
                  const response = await (await fetch(data[index++])).json();
                  yield response;    
              }
          }
          const baseURL = 'https://jsonplaceholder.typicode.com/';
          const urls = {
              todos: 'todos/',
              posts: 'posts/',
              comments: 'comments/',
              albums: 'albums/',
              photos: 'photos/',
              users: 'users/',
          };
      
          const list = Object.values(urls).map(i => `${baseURL}${i}`);
          const urlIterator = Iterator(list);
          debugger;
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
      })();
  • Patrón state: Con este patrón podemos modificar el comportamiento de un objeto en base a lo que suceda dentro del mismo.
    • (() => {
          const QueueManagement = function() {
              var count = 0;
              var currentState = new x1(this);
              this.change = function(state) {
                  console.log(count);
                  currentState = state;
                  count++;
                  if (count > 10) count = 0;
                  currentState.next();
              };
              this.count = function() {
                  return count;
              }
              this.run = function() {
                  currentState.next();
              };
          };
          const x1 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('X1', data)
                  const send = data.count() < 10 ? new x1(data) : new x10(data);
                  data.change(send)
              }
          };
          const x10 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('X10', data)
                  const send = data.count() < 10 ? new x10(data) : new x100(data);
                  data.change(send)
              }
          };
          const x100 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('FIN - X100', data)
              }
          };
          const statePattern = new QueueManagement();
          statePattern.run();
      })();

Aunque existen mas patrones de diseño, he añadido los que he usado y he podido aplicar tanto a proyectores reales como personales.

chat realtime cover

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Con esta nueva entrada quiero mostrar como he creado un chat usando NodeJS Socket.io. Para realizar el chat he usado la guía que ofrece la librería socket.io y su extensa documentación.

El código del proyecto

Para realizar este pequeño proyecto no he usado ningún framework basado en Javascript, ya que en principio iba a ser una prueba de concepto con pocas líneas para entender como funciona la librería socket.io.

Revisando la documentación oficial de socket.io se puede desarrollar un chat básico sin problema, pero como la idea era investigar, he probado algunas cosas añadiendo las siguientes funcionalidades:

  • Subida de ficheros
  • Chat privados
  • Información visual sobre los nuevos mensajes.
  • Cerrar y tener salas privadas

A continuación veremos el código correspondiente a la vista que he realizado usando bulma, el cual aconsejo echar un ojo ya que además de ligero, es bastante completo tanto en la documentación como en la cantidad de ejemplos de código.

index.html

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CHAT</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" />
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="notification hidden">
    <p></p>
  </div>
  <div class="container">
    <div class="columns">
      <div class="column is-one-quarter mt-6">
        <div class="card  has-background-black">
          <div class="card-content">
            <div class="media mb-0">
              <div class="media-content">
                <p class="title is-6 has-text-white">Usuarios conectados</p>
                <hr>
              </div>
            </div>
            <div class="content has-text-white" id="userConnected">
            </div>
          </div>
        </div>
      </div>
      <div class="column">
        <div class="tabs is-boxed pt-6 mb-0">
          <ul>
            <li id="tabGeneral" data-content="chatGeneral">
              <a class="active">
                <span class="has-text-white">General</span>
                <i class="fas fa-comment newMessage hide"></i>
              </a>
            </li>
          </ul>
        </div>
        <div class="columns">
          <div class="column containerChats pb-0">
            <div id="chatGeneral" class="chat textarea contentTab"></div>
            <div class="infoInput"></div>
          </div>
        </div>
        <div class="columns is-mobile">
          <div class="column is-one-quarter pr-0 pt-1">
            <input class="input has-text-white has-background-black" type="text" id="user" placeholder="Usuario">
          </div>
          <div class="column ml-1 mt-1 mr-3">
            <div class="columns is-mobile">
              <input class="input has-text-white has-background-black" type="text" id="message" placeholder="Mensaje" title="Subir fichero">
              <form>
                <button class="button is-info" id="btnUploadFile">
                  <i class="fas fa-file-upload"></i>
                  <input class="file-input" type="file" name="uploadFile" accept="image/x-png,image/gif,image/jpeg">
                </button>
              </form>
            </div>
          </div>
        </div>
        <div class="columns is-mobile hidden" id="containerProgress">
          <div class="column">
            <progress id="upload-progress-bar" max="100" value="0">0</progress>
            <div class="info-progress" />
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="/socket.io/socket.io.js"></script>
  <script src="./index.js"></script>
</body>
</html>

style.css

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300&display=swap');
html {
  overflow: hidden;
}
body {
  font-family: 'Montserrat', sans-serif;
  height: 100vh;
  width: 100vw;
  margin: 0;
  padding: 0;
  background: #354b52;
  color: #ffffff;
  overflow: hidden;
}
input {
  padding: 10px;
  font-size: 14px;
}
.chat {
  width: 100%;
  height: 250px;
  padding: 10px;
  font-size: 14px;
  overflow-y: auto;
  border: 1px solid #dddddd;
  background: #000000;
  color: #ffffff;
  resize: none !important;
}
.infoInput {
  font-size: 11px;
  position: relative;
  left: 10px;
  top: -5px;
}
.disabled {
  pointer-events: none;
  color: #aaaaaa !important;
}
.chatUser {
  font-weight: bold;
  margin-right: 5px;
}
::placeholder {
  color: #ffffff !important;
  opacity: .3 !important;
}
.chat::-webkit-scrollbar,
#userConnected::-webkit-scrollbar,
.tabs ul::-webkit-scrollbar {
  width: 10px;
}
.chat::-webkit-scrollbar-track,
#userConnected::-webkit-scrollbar-track,
.tabs ul::-webkit-scrollbar-track {
  border-radius: 10px;
}
.chat::-webkit-scrollbar-thumb,
#userConnected::-webkit-scrollbar-thumb,
.tabs ul::-webkit-scrollbar-thumb {
  background: #777777;       
  border-radius: 3px;
}
.chat::-webkit-scrollbar-thumb:hover,
#userConnected::-webkit-scrollbar-thumb:hover,
.tabs ul::-webkit-scrollbar-thumb:hover {
  background: #ffffff; 
  cursor: pointer;
}
.tabs ul {
  border-bottom: 0 !important;
  font-size: 14px;
  overflow: auto;
}
.tabs.is-boxed a:hover, .active {
  background-color: #000000;
  border: 1px solid #ffffff;
}
.containerChats .infoInput {
  text-align: right;
  left: 0;
  top: 0px;
  height: 15px;
}
.hide {
  display: none;
}
.card {
  border: 1px solid #dddddd;
  max-height: 343px;
  height: 343px;
}
.notification {
  transition: 300ms ease-in-out;
  position: fixed;
  bottom: -24px;
  width: 100%;
  opacity: 1;
  z-index: 10;
}
.notification.hidden {
  transition: 300ms ease-in-out;
  bottom: -100px;
  opacity: 0;
  z-index: -1;
}
#userConnected {
  overflow-y: auto;
  max-height: 225px;
  height: 225px;
}
#userConnected .chatUser {
  cursor: pointer !important;
}
@media (max-width: 768px) {
  .tabs  {
    padding-top: 0px !important;
  }
}
.newMessage {
  font-size: 11px;
  left: 5px;
  position: relative;
  color: #2196F3;
}
.closeChat {
  position: relative;
  left: 12px;
  top: -11px;
  color: #d46a10;
  font-size: 10px;
}
@media all and (max-width: 1023px) {
  .container {
    max-width: 960px;
    margin-left: 32px !important;
    margin-right: 32px !important;
  }
}
progress {
  width: 100%;
  position: relative;
  bottom: 25px;
  transition: 200ms ease-in-out;
  opacity: 1;
}
.info-progress {
  font-size: 11px;
  text-align: right;
  width: 100%;
  bottom: 30px;
  position: relative;
}
.hidden {
  opacity: 0;
  transition: 200ms ease-in-out;
}

Sobre el código anterior no hay mucho que añadir, una página simple pero suficiente para la prueba de concepto.

El último trozo de código que veremos, es el encargado de que la aplicación funcione en nuestro navegador.

index.js

// Definición de constantes y variables para la gestión del chat
let socket = null;
let user = null;
let message = null;
let usersPanel = null;
let notification = null;
let uploadFile = null;
let webrtc = null;
const CHAT_GENERAL = 'chatGeneral';
const LENGTH_MIN_USERNAME = 3;
const EMPTY = 0;
const LITERAL = {
  sameUser: 'No puedes enviarte un mensaje a ti mismo',
  minSizeUser: `El nombre del usuario debe tener <b>mínimo ${LENGTH_MIN_USERNAME} caracteres</b>`,
  uploadFile: 'Ha compartido un fichero',
  uploadSuccess: 'El fichero se ha subido y se esta compartiendo correctamente',
};

/** 
 * @description Cierra una sala de chat privada
 * @param {string} selectorID ID de la sala a cerrar
 */
const closeChat = selectorID => {
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).removeEventListener('click', selectedChat);
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).remove();
  document.querySelector(`.containerChats #${selectorID}`).remove();
  document.querySelector(`li[data-content='chatGeneral']`).click();
};

/** 
 * @description Creamos una sala privada
 * @param {object} data Información para crear la sala
 */
const chatTo = data => {
  const selectorID = data.idHTML || data.idOrigenHTML;
  const selectorIdUser = data.idUser ||data.idOrigen;
  const selectorUser = data.user || data.userOrigen;
  const existChat = document.querySelectorAll(`#${selectorID}`).length;
  if (existChat === EMPTY) {
    if (user.dataset.idhtml !== selectorID || data.idDestinoHTML !== data.idOrigenHTML) {
      document.querySelectorAll('.tabs ul li').forEach(item => {
        item.children[0].classList.remove('active');
      });
      document.querySelectorAll('.containerChats .chat').forEach(item => {
        item.classList.add('hide');
      });
      const chat = document.createElement('div');
      const li = document.createElement('li');
      const a = document.createElement('a');
      const span = document.createElement('span');
      const iNewChat = document.createElement('i');
      const iCloseChat = document.createElement('i');
      chat.setAttribute('id', selectorID);
      chat.classList.add('chat','textarea', 'contentTab');
      document.querySelector('.containerChats').prepend(chat);
      li.setAttribute('data-content', selectorID.toString());
      li.setAttribute('data-idUser', selectorIdUser.toString());
      a.classList.add('active');
      span.style.color = data.color;
      span.classList.add('has-text-white', 'chatUser');
      span.innerHTML = selectorUser;
      a.appendChild(span);
      iNewChat.classList.add('fas', 'fa-comment', 'newMessage', 'hide');
      a.appendChild(iNewChat);
      iCloseChat.classList.add('fas', 'fa-times-circle', 'closeChat');
      iCloseChat.onclick = () => { closeChat(selectorID.toString()); };
      a.appendChild(iCloseChat);
      li.appendChild(a);
      document.querySelector('.tabs ul').appendChild(li);
      document.querySelector(`.tabs ul li[data-content='${selectorID}']`).addEventListener('click', selectedChat);
    } else {
      notify(LITERAL.sameUser, 'danger');
    }
  }
};

/** 
 * @description Seleccionar una sala para charlar
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const selectedChat = evt => {
  document.querySelectorAll('.containerChats .chat').forEach(item => {
    item.classList.add('hide');
  });
  const showChat = evt.currentTarget.dataset['content'];
  const chatElement = document.querySelector(`#${showChat}`);
  if (chatElement) {
    chatElement.classList.remove('hide');
  }
  document.querySelectorAll('.tabs a').forEach(item => item.classList.remove('active'));
  evt.currentTarget.children[0].classList.add('active');
  if (!Array.from(evt.currentTarget.children[0].querySelector('i').classList).includes('hide')) {
    evt.currentTarget.children[0].querySelector('i').classList.add('hide')
  }
};

/** 
 * @description Envia mensajes al chat general y a un usuario privado
 * @param {object} evt Evento implicito en la acción ejecutada
 * @param {boolean} status Indicamos el estado para saber que usuario esta escribiendo
 */
const sendMessage = (evt, status) => {
  if (evt.key === 'Enter') {
    if (user.value.length >= LENGTH_MIN_USERNAME && message.value.trim().length > EMPTY) {
      const parent = document.querySelectorAll('.tabs a.active')[0].parentElement;
      const content = parent.dataset.content;
      const currentlyChat = document.querySelector(`#${content}`);
      const isChatGeneral = content === CHAT_GENERAL;
      const infoMensaje = {
        idOrigen: user.dataset.iduser,
        idOrigenHTML: user.dataset.idhtml,
        idDestino: isChatGeneral ? CHAT_GENERAL : parent.dataset.iduser,
        idDestinoHTML: content,
        userOrigen: user.value,
        userDestino: isChatGeneral ? CHAT_GENERAL : parent.innerText,
        message: message.value
      };
      if (content !== CHAT_GENERAL) {
        const color = document.querySelector(`#userConnected .chatUser[data-idhtml='${user.dataset.idhtml}']`).style.color;
        const messageSend = document.querySelector(`.containerChats #${content}`);
        const div = document.createElement('div');
        const span = document.createElement('span');
        span.style.color = color;
        span.classList.add('chatUser');
        span.appendChild(document.createTextNode(`${infoMensaje.userOrigen}:`));
        div.appendChild(span);
        div.innerHTML += infoMensaje.message;
        messageSend.appendChild(div);
      }
      socket.emit(`message-chat-${isChatGeneral ? 'general' : 'private'}`, infoMensaje);
      message.value = '';
      currentlyChat.scrollTo(0, currentlyChat.scrollHeight);
    } 
    user.classList[user.value.length >= LENGTH_MIN_USERNAME ? 'remove' : 'add']('has-background-danger');
  } else {
    if (user.value.length >= LENGTH_MIN_USERNAME) {
      if (evt.key !== 'Tab') {
        socket.emit('write-client', { data: status ? user.value : ''});
      }
    }
  }
};

/** 
 * @description Muestra texto indicando quien esta escribiendo
 * @param {object} payload Información que pinta cuando pulsamos un tecla
 */
const clientBeenWriting = payload => {
  document.querySelector('.containerChats .infoInput').innerHTML = payload;
};

/** 
 * @description Muestra texto en el chat general
 * @param {object} data Información relativa al usuario
 */
const reciveMessage = data => {
  const {
    color,
    idDestinoHTML,
    idOrigenHTML,
    userOrigen,
    message 
  } = data;
  let activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  if (!activeChatID) {
    chatTo(data);
    activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  }
  if (idDestinoHTML !== idOrigenHTML) {
    const div = document.createElement('div');
    const span = document.createElement('span');
    span.style.color = color;
    span.classList.add('chatUser');
    span.appendChild(document.createTextNode(`${userOrigen}:`));
    div.appendChild(span);
    div.innerHTML += message;
    activeChatID.appendChild(div);
    activeChatID.scrollTo(0, activeChatID.scrollHeight);
  }
  const newMessage = document.querySelector('.tabs a.active').parentElement.dataset.content
  if (newMessage !== activeChatID.id && newMessage !== idDestinoHTML) {
    const destino = document.querySelector(`.tabs li[data-content='${idDestinoHTML}'] i`);
    const origen = document.querySelector(`.tabs li[data-content='${idOrigenHTML}'] i`);
    (destino || origen).classList.remove('hide');
  }
};

/** 
 * @description Añade y actualiza el panel lateral con el listado de los usuarios
 * @param {object} payload Información de cada uno de los usuarios
 */
const registerUser = payload => {
  Array.from(usersPanel.children).forEach(item => item.remove())
  payload.forEach(item => {
    const div = document.createElement('div');
    div.style.color = item.color;
    div.classList.add('chatUser');
    div.onclick = () => { chatTo(item); };
    div.setAttribute('data-idUser', item.idUser);
    div.setAttribute('data-idHTML', item.idHTML);
    div.appendChild(document.createTextNode(item.user));
    usersPanel.appendChild(div);
    if (user.value === item.user) {
      user.setAttribute('data-idHTML', item.idHTML);
      user.setAttribute('data-idUser', item.idUser);
    }
  });
};

/** 
 * @description Muestra mensaje de error al registrar el usuario
 * @param {object} payload Información de cada uno de los usuarios
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const errorRegisteredUser = (payload, evt) => {
  const { error } = payload;
  notify(error, 'danger');
  evt.target.classList.remove('disabled');
  user.classList.add('has-background-danger');
};

/** 
 * @description Realiza la conexion al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const connectedToServer = evt => {
  const target = evt.target;
  if (target.value.length >= LENGTH_MIN_USERNAME) {
    target.classList.add('disabled');
    user.classList.remove('has-background-danger');
    socket = io.connect();
    socket.on('recive-message', reciveMessage);
    socket.on('registered-user', registerUser);
    socket.on('error-registered-user', payload => errorRegisteredUser(payload, evt));
    socket.on('client-been-writing', clientBeenWriting);
    socket.on('upload-progress', data => uploadProgress(data));
    const idHTML = generateIDHtml();
    socket.emit('connected-to-server', { user: target.value, idHTML });
  } else {
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Mostramos el progreso de la subida
 * @param {object} data Información sobre la subida del archivo
 */
const uploadProgress = data => {
  const { recived, total, who } = data;
  const porcent = Math.floor((recived * 100) / total);
  const currentlySize = (recived / 1024) / 1024; // MB
  const totalSize = (total / 1024) / 1024; // MB
  const progress = document.querySelector('#upload-progress-bar');
  const infoProgress = document.querySelector('.info-progress');
  progress.value = Math.floor(porcent);
  progress.innerHTML = porcent.toFixed(2);
  infoProgress.innerHTML = `${currentlySize.toFixed(2)} MB ${totalSize.toFixed(2)} MB ${porcent} %`;
};

/** 
 * @description Cargamos un fichero y se sube al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const upload = async evt => {
  if (user.value.length >= LENGTH_MIN_USERNAME) {
    const uploadProgress = document.querySelector('#containerProgress');
    uploadProgress.classList.remove('hidden');
    const btnUploadFile = document.querySelector('#btnUploadFile');
    btnUploadFile.classList.add('is-loading');
    btnUploadFile.setAttribute('disabled', true);
    const files = evt.target.files;
    const data = new FormData();
    data.append('archivo', files[0]);
    socket.emit('upload-file', { idUser: user.dataset.iduser });
    const result = await (await fetch('/upload-file', {
      method: 'POST',
      body: data
    })).json();
    btnUploadFile.classList.remove('is-loading');
    btnUploadFile.removeAttribute('disabled');
    const { statusCode, path, statusMessage } = result;
    if (statusCode === 200) {
      message.value = `${LITERAL.uploadFile} <a href='${statusMessage}' target='_blank'>[${files[0].name}]</a>`;
      sendMessage(evt = {key: 'Enter'}, true);
      notify(LITERAL.uploadSuccess, 'success', 4000);
    } else {
      notify(statusMessage, 'danger', 4000);
    }
    setTimeout(() => {
      uploadProgress.classList.add('hidden');
    }, 2000);
  } else {
    document.querySelector('form').reset();
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Inicialización del chat
 */
const load = () => {
  const tabGeneral = document.querySelector('#tabGeneral');
  user = document.querySelector('#user');
  message = document.querySelector('#message');
  usersPanel = document.querySelector('#userConnected');
  notification = document.querySelector('.notification');
  message.addEventListener('keydown', evt => sendMessage(evt, true));
  message.addEventListener('keyup',  evt => sendMessage(evt, false));
  user.addEventListener('blur', connectedToServer);
  tabGeneral.addEventListener('click', selectedChat);
  uploadFile = document.querySelector('input[type=file]');
  uploadFile.addEventListener('change', upload);
};

/** 
 * @description Muestra notificaciones en el chat
 * @param {string} msn Mensaje que se muestra en la notificación
 * @param {string} type Tipo de mensaje (danger, info, success, warning)
 * @param {number} timeout Duración de la notificación
 */
const notify = (msn = '', type = 'info', timeout = 2000) => {
  notification.children[0].innerHTML = msn;
  notification.classList.add(`is-${type}`);
  notification.classList.remove('hidden');
  setTimeout(() => {
    notification.classList.add('hidden');
    notification.classList.remove(`is-${type}`);
  }, timeout);
};

/** 
 * @description Generamos un ID para cada usuario
 */
const generateIDHtml = () => new Date().getTime().toString().split('').map(i => String.fromCharCode(parseInt(i) + 65)).join('');

/** 
 * @description Espera a que este la página completamente cargada
 */
document.addEventListener("DOMContentLoaded", load);

En un futuro me gustaría realizar esta misma aplicación usando React Vue, entre otras cosas para aprovechar la magia que ofrecen y poder añadir mas características como, llamadas de audio y video, compartir pantalla, guardar conversaciones, stickers, gifs, etc…

Para finalizar esta entrada, si estás interesado en usar el código, lo puedes obtener desde Github y ademas puedes probar el chat en la siguiente url: https://chat.tecnops.es/

Tensorflow.js y otros servicios API de inteligencia artificial

Tensorflow y los servicios API de Inteligencia Artificial

En esta nueva entrada vamos a crear algún ejemplo usando Tensorflow.js y algunos de los servicios de Inteligencia Artificial que nos ofrecen algunas de las empresas mas punteras y potentes actualmente.

Para realizar las pruebas usaremos los siguientes servicios y librerías:

  • Tensorflow.js: Librería para Javascript que se ejecuta en nuestro navegador.
  • IBM Watson: Usando una parte de sus servicios enfocados a la Inteligencia Artificial.

En principio solo estoy añadiendo las librerías y/o servicios que he usado para realizar alguna POC.

El resto de los servicios que me gustaría probar como son Microsoft Azure, Google Cloud y Amazon, de momento no los vamos a usar, ya que o bien no tienen versiones de prueba o requieren pasar por caja previamente, cosa que de momento no vamos hacer.

Bien, como existen mas servicios y librerías vamos a continuar nuestras pruebas con otros servicios y/o librerías.

Tensorflow.js

Vamos a comenzar por Tensorflow.js, ya que no requiere de ningún registro previo para utilizar la librería permitiendo realizar todas las pruebas que queramos sin ningún límite.

Como en el tutorial anterior sentamos las bases para usar Tensorflow.js, ahora iremos al grano mostrando el código con el resultado para realizar las comparativas.

En cada sección mostrare solo el código correspondiente usado para cada librería y/o servicio, y al final del articulo mostrare el código completo de la aplicación y su enlace al repositorio en Github.

const tensorflowObjectDetection = msn => {
  ...
  const img = document.getElementById('imgPreview');
  mobilenet.load().then(model => {
    ...
    model.classify(img, totalClasses).then(predictions => {
      ...
      predictions.forEach((data, index) => {
        ...
      });
      container.appendChild(ul);
    });
  });
};

Como había comentado, en este trozo de código solo quiero mostrar lo simple que resulta usar los modelos pre-entrenados de Tensorflow, ya que al final esta el enlace a todo el código.

IBM Watson

Con este servicio podemos hacer entre otras muchas cosas, la clasificación de una imagen como hicimos en el ejemplo anterior.

El servicio ofrecido por IBM nos permite usar todas las características en su plan LITE pero con ciertas limitaciones.

Para realizar nuestra prueba, esas limitaciones no nos influyen.

El siguiente ejemplo lo voy a realizar en NodeJS, para lo cual tendremos que instalar la siguiente librería:

npm install ibm-watson@^5.2.1

Una vez instalada la dependencia, procederemos a usarla en nuestro código.

A continuación el código necesario en la parte de NodeJS para que funcione nuestro servicio:

...
const visualRecognition = new VisualRecognitionV3({
  version: '2018-03-19',
  authenticator: new IamAuthenticator({
    apikey: '{apiKey}',
  }),
  url: '{url}',
});
...
const classifyParams = {
  imagesFile: 'filename.jpg',
  owners: ['IBM', 'me'],
  threshold: 0.1,
};
...
visualRecognition.classify(classifyParams)
.then(response => {
  const classifiedImages = response.result;
  res.status(200).json(classifiedImages);
})
.catch(err => {
  res.status(400).json({ message: err });
  console.log('error:', err);
});
...

Y ahora el código para la parte de Javascript:

const IBMWatsonObjectDetection = () => {
  ...
  // Donde hemos levantado el servidor de NodeJS
  const url = new URL('https://localhost:3000/ibm-watson');
  ...
  fetch(url).then(response => response.json()).then(responseJson => {
    ...
    const { images } = responseJson;
    ...
  });
};

Es algo más de código pero realmente estamos dividiendo el trabajo, ya que en Javascript únicamente estamos pintado el resultado que nos devuelve el script de NodeJS.

Ahora el encargado de todo el trabajo es NodeJS mediante la librería de IBM Watson y su servicio.

En principio estas son las dos opciones que conozco y que puedo probar sin ningún coste directo asociado.

El resto de los servicios los he intentado probar, pero no he podido ver el resultado ya que me solicitan amablemente que realice el abono.

No obstante la cantidad de código necesario para ejecutar cualquiera de los otros servicios no llega a poco mas de 20 lineas.

Como había comentado al principio del artículo, voy a mostrar un pequeño vídeo ya que el servicio de IBM Watson tiene límites y una vez superados deja de funcionar.

Para finalizar el código lo puedes descargar desde su repositorio en Github.

Tensorflow para Javascript usando Tensorflow.js, ml5js y Processing (P5.js)

Empezando con Tensorflow.js, ml5js y Processing (P5.js)

En esta nueva entrada vamos a usar diferentes tecnologías para desarrollar algunos ejemplos muy curiosos y potentes.

Para unir todas estas tecnologías usaremos como punto de unión nuestro querido Javascript.

Antes de empezar con la magia es importante explicar que son y como se usan las diferentes herramientas que explicaremos durante este tutorial.

  • Tensorflow.jsEs una librería de Machine Learning (ML) para Javascript creada por Google.
  • Ml5jsEs una librería que nos proporciona acceso a los algoritmos y modelos de aprendizaje automático usando Javascript y como única dependencia tensorflow.js.
  • P5.jsEs otra librería de Javascript  extremadamente potente que nos permite realizar programación gráfica sin mucha complicación y esta basada en Processing.
  • ProcessingEs un software enfocado en la programación gráfica.

Tensorflow.js

Con esta potente librería para computación numérica podemos construir y entrenar redes neuronales que permiten detectar y descifrar patrones y correlaciones, análogos al aprendizaje y razonamiento usados por los humanos.

Para usarlo en Javascript podemos hacerlo de diferentes formas:

  • Usando el tag <script> en nuestra página.
    • <script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script>
  • Instalándolo desde NPM / YARN.
    • YARN
      yarn add @tensorflow/tfjs
      
      NPM
      npm install @tensorflow/tfjs
  • Usando NPM/ YARN pero esta vez para Node.js
    • Instalando TensorFlow.js con enlaces nativos de C ++
    • YARN
      yarn add @tensorflow/tfjs-node
      
      NPM
      npm install @tensorflow/tfjs-node
    • Solo para linux y si tienes una tarjeta gráfica NVIDIA GPU con soporte CUDA, puedes usar estas dependencias para un mayor rendimiento.
    • YARN
      yarn add @tensorflow/tfjs-node-gpu
      
      NPM
      npm install @tensorflow/tfjs-node-gpu
    • Instalando la versión para Javascript, que es la opción más lenta en cuanto a rendimiento.
    • YARN
      yarn add @tensorflow/tfjs
      
      NPM
      npm install @tensorflow/tfjs

Si necesitas saber más de Tensorflow.js, en esta guía oficial de Tensorflow.js tienes toda la información necesaria para practicar y profundizar sobre esta impresionante librería.

Para nuestro tutorial usare los modelos pre-entrenados que nos ofrecen desde Tensorflow.

En el siguiente ejemplo usaremos Mobilenet, pero…., ¿que es Mobilenet?

Mobilenet son modelos pequeños, de baja latencia y baja potencia parametrizados para cumplir con las limitaciones de recursos de una variedad de casos de uso, pudiendo realizar la clasificación, detección, incrustaciones y segmentación de forma similar a cómo se utilizan otros modelos populares a gran escala.

La ventaja de usar este modelo de TensorFlow.js es que no requiere que sepas sobre el aprendizaje automático, pudiendo tomar como entrada cualquier tag de imagen basado en el navegador (img, video, canvas) y esta devuelve una serie de predicciones más probables y sus confidencias.

El código fuente esta disponible en Github, no obstante también lo añado en la entrada para un copy/paste rápido.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Clasificación de imágenes</title>
  <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
  <link href="./style.css" rel="stylesheet">
</head>
<body>
  <h1>Clasificación de imágenes usando <b>Tensorflow.js</b> y <b>Mobilenet</b></h1>
  <div>
    <label for="image">Seleccionar una imagen</label>
    <input type="file" id="image" accept=".gif,.jpg,.jpeg,.png">
    <label for="classify" onclick="classify()" class="lock">Clasificar la imagen</label>
  </div>
  <img id="imgPreview" class="hide" />
  <div id="result"></div>
  <div class="loading hide">
    <div class="lds-ripple">
      <div></div>
      <div></div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]"></script>
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]"></script>
  <script src="./index.js"></script>
</body>
</html>
/* style.css */
body {
  font-family: 'Montserrat', sans-serif;
}
#imgPreview {
  box-shadow: 0px 5px 10px rgba(0,0,0,0.4);
  padding: 20px;
  margin-top: 20px;
}
input[type="file"]#image {
  width: 0.1px;
  height: 0.1px;
  opacity: 0;
  overflow: hidden;
  position: absolute;
  z-index: -1;
}
label {
  font-size: 14px;
  font-weight: 600;
  color: #ffffff;
  background-color: #106BA0;
  box-shadow: 0px 3px 6px rgba(0,0,0,0.3);
  display: inline-block;
  transition: all .5s;
  cursor: pointer;
  padding: 15px 40px;
  text-transform: uppercase;
  width: fit-content;
  text-align: center;
}
.lock {
  pointer-events: none;
  opacity: 0.3;
}
.hide {
  display: none;
}
.lds-ripple {
  display: inline-block;
  position: absolute;
  width: 80px;
  height: 80px;
  left: calc(50% - 40px);
  top: calc(50% - 40px);
}
.lds-ripple div {
  position: absolute;
  border: 4px solid #e90202;
  opacity: 1;
  border-radius: 50%;
  animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
  animation-delay: -0.5s;
}
@keyframes lds-ripple {
  0% {
    top: 36px;
    left: 36px;
    width: 0;
    height: 0;
    opacity: 1;
  }
  100% {
    top: 0px;
    left: 0px;
    width: 72px;
    height: 72px;
    opacity: 0;
  }
}
.loading {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  opacity: 0.7;
  background: #cccccc;
}
// index.js
const init = () => {
  document.querySelector("#image").onchange = evt => {
    let reader = new FileReader();
    reader.readAsDataURL(evt.target.files[0]);
    reader.onload = () => {
      const img = document.querySelector('#imgPreview');
      const container = document.querySelector('#result');
      img.setAttribute('src', reader.result);
      img.classList.remove('hide');
      document.querySelector('label[for="classify"]').classList.remove('lock');
      container.innerHTML = '';
    };
  }
};

const classify = () => {
  const loading = document.querySelector('.loading');
  loading.classList.remove('hide');
  const img = document.querySelector('#imgPreview');
  mobilenet.load().then(model => {
    model.classify(img).then(predictions => {
      const body = document.body;
      const container = document.querySelector('#result');
      container.innerHTML = '';
      predictions.forEach(data => {
        const porcent = parseFloat(data.probability * 100).toFixed(2);
        const progress = document.createElement('progress');
        const label = document.createElement('div');
        const info = document.createElement('span');
        label.innerHTML = `<b>Elemento reconocidos:</b> <i>${data.className}</i>`;
        progress.setAttribute('max', 100);
        progress.setAttribute('value', porcent);
        info.innerHTML = ` <b>${porcent}%</b>`;
        container.classList.add('result');
        container.appendChild(label);
        container.appendChild(progress);
        container.appendChild(info);
      });
      body.appendChild(container);
      loading.classList.add('hide');
    });
  });
};

window.onload = init();

Me gustaría seguir profundizando un poco más en el uso de Tensorflow.js, no solo para clasificar imágenes, ya que podríamos hacerlo también sobre videos, detectar objetos, detectar la segmentación de personas, estimación de posiciones, etc…, pero debemos continuar con el resto de las herramientas.

Ml5.js

Como habíamos comentado al principio, ml5.js es una interfaz de alto nivel, muy simple, de código abierto para TensorFlow.js, que nos permite manejar operaciones matemáticas aceleradas por GPU y gestión de memoria para algoritmos de aprendizaje automático.

Viendo el código anterior y lo que se puede conseguir, podríamos pensar que no se puede hacer mas fácil, pero si se puede hacer todavía mas sencillo y sobre todo más potente.

Para empezar usaremos el tag script para acceder al CDN de la librería y poder hacer uso de ella.

<script src="https://unpkg.com/[email protected]/dist/ml5.min.js"></script>

Igual que en el ejemplo de Tensorflow.js, podéis acceder también a su repositorio en Github.

No obstante añadiré el código en el tutorial para un acceso mas rápido.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Ml5.js y P5.js</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.sound.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/ml5.min.js"></script>
  <link rel="stylesheet" href="./style.css" />
</head>
<body>
  <div class="loading hide">
    Analizando imagen
  </div>
  <script src="./index.js"></script>
</body>
</html>
/* style.css */
.loading {
  position: absolute;
  left: calc(50% - 100px);
  top: calc(50% - 10px);
  width: 200px;
  text-align: center;
  background: #6677aa;
  padding: 20px 0px;
  border-radius: 100px;
  font-size: 20px;
  color: #ffffff;
}
.hide {
  display: none;
}
.result {
  padding: 10px;
  box-shadow: 0px 6px 12px rgba(0,0,0,0.3);
  margin-top: 20px;
  width: 500px;
}
canvas {
  box-shadow: 0px 6px 12px rgba(0,0,0,0.3);
  margin-bottom: 20px;
}
// index.js
let mobilenet;
let img;
window.onerror = (errorMsg, url, lineNumber) => {
  console.log('----- ERROR ------');
  console.log(errorMsg, url, lineNumber);
  console.log('------------------');
  return false;
}

const show = (el, status) => {
  document.querySelector(el).classList[status ? 'remove' : 'add']('hide');
};

function modelReady() {
  console.log('model is ready!!');
}

function gotResult(error, results) {
  show('.loading', false);
  if (error) {
    console.error(error);
  } else {
    console.log(results);
    const body = document.body;
    const container = document.createElement('div');
    results.forEach(data => {
      const porcent = nf(data.confidence, 0, 4) * 100;
      const progress = document.createElement('progress');
      const label = document.createElement('div');
      const info = document.createElement('span');
      label.innerHTML = `Elemento reconocidos: ${data.label}`;
      progress.setAttribute('max', 100);
      progress.setAttribute('value', porcent);
      info.innerHTML = ` ${porcent}%`;
      container.classList.add('result');
      container.appendChild(label);
      container.appendChild(progress);
      container.appendChild(info);
    });
    body.appendChild(container);
  }
}
window.onload = () => {
  show('.loading', true);
}
function imageReady() {
  image(img,0,0 , width, height);
  mobilenet.predict(img, gotResult);
}
function preload() {
  classifier = ml5.imageClassifier('MobileNet');
  img = loadImage('img1.jpg');
}

function setup() {
  createCanvas(640, 480);
  img = createImg('img1.jpg', imageReady);
  img.hide();
  background(0);
  mobilenet = ml5.imageClassifier('MobileNet',modelReady);
}

Si comparáis el código y el resultado, ambos realizan la misma acción, clasificar una imagen.

La diferencia esta en que usando la librería ml5.js podemos hacer uso de la GPU, y usando p5.js podemos crear canvas y realizar una programación gráfica mucho mas rápida ya que no tenemos que complicarnos con el canvas y como pintar en el, que aunque para este ejemplo es muy simple para otros puede ser algo más doloroso.

Como he dicho en el punto anterior, me gustaría continuar profundizando pero debemos continuar con la última herramienta.

Processing (p5.js)

Processing es un software que nos permite programar de una forma muy simple pero obteniendo unos resultado bastante buenos.

Esto suena maravilloso pero como siempre digo, «el movimiento se demuestra andando».

 

Puedes encontrar el código en Github y para un acceso rápido añado también el código en la página.

<!-- index.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Processing P5.JS</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
  <script src="./index.js"></script>
</body>
</html>
// index.js
function setup() {
  createCanvas(710, 400, WEBGL);
}

function draw() {
  background(0);

  translate(-240, -100, 0);
  normalMaterial();
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  plane(70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  box(70, 70, 70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  cylinder(70, 70);
  pop();

  translate(-240 * 2, 200, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  cone(70, 70);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  torus(70, 20);
  pop();

  translate(240, 0, 0);
  push();
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  sphere(70);
  pop();
}

Como has podido comprobar el resultado es muy llamativo, pero lo mas llamativo son las pocas lineas de código que han sido necesarias para generar este ejemplo.

Con Processing (P5.js) damos por finalizado este pequeño tutorial de Tensorflow.js, no obstante en breve intentare publicar otra entrada con mas detalle sobre sobre Tensorflow.js y el ecosistema que ofrece.

Creando nuestro CLI (Command-Line Interface) usando NodeJS

Empezando con nuestro CLI

Hoy en día es muy común encontrar decenas de herramientas para acelerar nuestro desarrollo en la parte de Front-end, ya sea para crear proyectos desde cero e inclusive repetir ciertas tareas repetitivas y monótonas.

En ocasiones nos encontramos situaciones en las que esas herramientas no cubren nuestras necesidades:

  • Ya sea por la naturaleza del proyecto que tiene cierta lógica especial y requiere de ciertas personalizaciones.
  • El proyecto no tiene necesidades especiales pero tenemos que realizar ciertos procesos repetitivos que se acaban convirtiendo en un copy/paste, con lo que eso supone.
  • También nos podemos encontrar en la situación que el proyecto sea normal, no requiera ninguna acción especial pero nos gustaría tener unos procesos definidos de como se deben hacer las cosas.

Bien, para cualquiera de las situaciones anteriores y seguramente alguna que otra mas, NodeJS nos ofrece un entorno y ciertas herramientas para crear nuestro propio CLI (Command-Line Interface).

¿Por donde comienzo?

Lo primero de todo sera crear la carpeta de nuestro CLI y dentro crearemos el fichero package.json.

Si no queremos complicarnos usaremos el comando npm init -y, el cual nos creara un package.json por defecto.

Lo siguiente sera instalar las dependencias necesarias para poder crear nuestro CLI:

chalk: Con esta dependencia podemos dar colores y ciertos estilos al texto de nuestro terminal (npm i chalk)

 

inquirer: Con esta dependencia podemos hacer que nuestra linea de comandos sea interactiva (npm i inquirer)

 

figlet: Con esta dependencia podemos crear banners con caracteres y simbolos (npm i figlet)

Con las dependencias instaladas, procederemos a crear el fichero que tendrá la lógica encargada de realizar la «magia».

Empezando a crear nuestro CLI

Antes de comenzar a realizar nuestras pruebas, desde aquí podéis obtener el repositorio de Github.

Ahora ya podemos empezar, para este ejemplo lo llamaremos index.js

#!/usr/bin/env node
/* 
  La linea anterior es una instancia de una línea shebang: 
  la primera línea en un archivo ejecutable de texto sin formato en plataformas tipo Unix 
  que le dice al sistema a qué intérprete pasar ese archivo para su ejecución, 
  a través del comando línea siguiendo el prefijo máfico #! (llamado shebang).
  En Windows no admite líneas shebang, por lo que se ignoran allí; 
  en Windows, es únicamente la extensión del nombre de archivo de un archivo determinado 
  lo que determina qué ejecutable lo interpretará. 
  Sin embargo, aún los necesita en el contexto de npm.
*/
const chalk = require('chalk');
const figlet = require('figlet');
const inquirer = require('inquirer');
const fs = require('fs');
const pathBase = process.cwd();

// Template que usaremos para la creación del contenido del fichero
let templateVUE = require('./templates/templateVUE');

// Mostrar un banner con un mensaje formado por caracteres.
const msn = msn => {
  console.log(chalk.bold.cyan(figlet.textSync(msn, { 
    font:  'ANSI Shadow',
    horizontalLayout: 'default',
    verticalLayout: 'default'
  })));
}

// Preguntas que se van a realizar y que más tarde usaremos
const queryParams = () => {
  const qs = [{
      name: 'componentName',
      type: 'input',
      message: 'Escribe el nombre del componente'
    },{
      name: 'fileName',
      type: 'input',
      message: 'Escribe el nombre del fichero: '
    }, {
      name: 'type',
      type: 'list',
      message: 'Selecciona el tipo de elemento a crear: ',
      choices: [
        'Components',
        'Views',
        'Layouts',
        'Models',
        'Javascript',
      ],
    },
  ];

  return inquirer.prompt(qs);
};

// Método que se encarga de crear el fichero en base a las preguntas realizadas
const createFile = (data) => {
  const extension = data.type === 'Javascript' ? 'js' : 'vue'
  const path = `${pathBase}\\src\\${data.type}`;
  const file = `${path}\\${data.fileName}.${extension}`;
  if (!fs.existsSync(path)) {
    fs.mkdirSync(path, 0777);
  }
  try {
    templateVUE = templateVUE.replace('$name', data.componentName);
    fs.writeFileSync(file, templateVUE, { mode: 0o777 });
  } catch(err) {
    console.error(err);
  } finally {
    console.log(`
      ------ CREADO CORRECTAMENTE ------\n
      Se ha creado el siguiente elemento\n
      - Tipo: ${chalk.blue.bold(data.type)}\n
      - Ruta: ${chalk.blue.bold(file)}\n
      ----------------------------------\n
    `);
  }
}

// IIFE (Immediately Invoked Function Expression)
(async() => {
  msn('MTM-CLI');
  createFile(await queryParams());
})();

El fichero anterior es un ejemplo de lo que se puede hacer, realmente en el fichero index.js puedes desarrollar lo que necesites, desde crear archivos, tareas de backups, proyectos desde cero, etc…

En este punto nosotros podríamos ejecutar el fichero usando el siguiente comando: node index.js, y comenzaría a ejecutar el script:

Como podemos comprobar, hemos ejecutado nuestro archivo desde NodeJS y el código ha funcionado correctamente, dando como resultado la creación de un fichero dentro de una carpeta usando unos parámetros.

Pero claro, lo interesante es ejecutar el fichero sin ayuda del comando node xxxx, pues bien, para eso vamos a realizar los siguientes pasos.

Lo primero añadiremos, dentro del fichero package.json un nuevo valor llamado «bin»: «nombre de fichero».

{
  "name": "mtm-cli",
  "version": "1.0.0",
  "description": "\"# create-cli\"",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kimagure44/create-cli.git"
  },
  "keywords": [
    "cli",
    "javascript"
  ],
  "author": "Iván Lara Gómez, <[email protected]>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/kimagure44/create-cli/issues"
  },
  "homepage": "https://github.com/kimagure44/create-cli#readme",
  "dependencies": {
    "chalk": "^2.4.2",
    "figlet": "^1.2.4",
    "inquirer": "^7.0.0",
  },
  "bin": "./index.js"
}

Añadiendo esa linea, estaríamos definiendo el fichero que se va a ejecutar como si fuera un binario de forma global.

El nombre de nuestro CLI estará definido por la key name dentro del package.json, que para este ejemplo es «name»:»mtm-cli».

El proceso estaría casi finalizado, solo necesitaríamos realizar un enlace simbólico a nuestra carpeta.

Para esa tarea usaremos el siguiente comando: npm link, que nos permite crear un enlace simbólico a nuestro paquete / carpeta.

Con el comando anterior estamos creando un enlace simbólico en la carpeta global ({prefix}/lib/node_modules/<package>) que enlaza con el paquete donde se ejecuto.

Recordar nuevamente, que el nombre que usaremos para ejecutar el CLI, viene definido por el key name dentro del package.json

Ahora ya podemos ejecutar nuestro CLI usando el nombre dentro del package.json, tanto el nuestra carpeta actual como en cualquier otra parte ya que el comando npm link nos creo un enlace simbólico de forma global.

Con este último paso ya tendríamos preparado nuestro CLI, para poder usarlo donde lo necesitemos.

streaming torrent cover

Streaming de torrents usando NodeJS y VLC

Streaming de torrents usando NodeJS y VLC

Antes de comenzar, me gustaría aclarar algunos puntos importantes debido a la aplicación y al código que voy a publicar:

  • Bajo ningún concepto apoyo la piratería de ningún medio, formato o producto digital y/o físico, ya que toda persona tiene derecho a cobrar por su trabajo.
  • De la misma forma que he aprovechado el conocimiento de otras personas, yo ofrezco mis conocimientos para quien pueda o quiera aprovecharlos mediante este blog.
  • Tanto el código fuente que he desarrollado, como las dependencias de terceros y las adaptaciones de las mismas están a disposición de todo el mundo.
  • La aplicación y el código fuente aquí expuestos, se ofrecen y publican solo con fines educativos, descargando toda la responsabilidad sobre quien realice un mal uso de la misma.

Bien, una vez aclarados estos puntos comencemos con la aplicación.

La idea surge cuando intento realizar streaming de un vídeo usando WebRTC y NodeJS.

Como no me convence la solución, me pongo a buscar información por internet y me encuentro con esta página.

Para sorpresa mía, resulta que es una librería que te permite hacer streaming de ficheros torrent desde el navegador y con unas pocas lineas de código.

Pasadas unas horas y viendo que no conseguía que funcionase con ficheros torrent, encuentro en la documentación que esta librería usa WebRTC para hacer el per-to-per pero no es compatible con los fichero .torrent.

Como soy un poco cabezón, continuo buscando y por fin encuentro unas dependencias para usar en NodeJS que te permiten hacer streaming de torrents.

A partir de ese momento, solo queda pelearse un poco con el código y las dependencias.

Al cabo de unos días creo una pequeña aplicación y su repositorio correspondiente.

Poniendo la aplicación en marcha

Una vez bajado / clonado el repositorio ejecutaremos npm i para instalar todas las dependencias necesarias.

A continuación ejecutaremos el comando node server.js, el cual nos levantara un servidor en local que se encargara de todo el proceso de streaming.

Levantando el servidor

Levantando el servidor

Ahora que nuestro servidor esta funcionando, abriremos la página index.html

Abriendo la página

Abriendo la página

Como había indicado al principio y para evitar problemas con derechos de autor, vamos a usar un fichero torrent sin copyright.

Seleccionado el torrent

Seleccionado el torrent

 

Cuando tengamos el fichero torrent en nuestro equipo, procederemos a subirlo a nuestra aplicación

Subiendo el torrent

Subiendo el torrent

En el momento hagamos clic sobre el botón, esta acción subirá en nuestro servidor de NodeJS a una carpeta llamada torrents el fichero del cual realizara el streaming.

Streaming torrent

Streaming torrent

Una cosa a tener en cuenta, es que desde el navegador que usemos para abrir el index.html deberemos instalar una extensión para evitar problemas de CORS.

El siguiente paso, sera abrir VLC y cargar un vídeo desde una URL que nos proporcionara NodeJS.

Carga url VLC

Carga url VLC

Carga url VLC

Carga url VLC

Comenzara el proceso de carga del vídeo desde la URL de NodeJS.

Carga url VLC

Carga url VLC

Cuando se haya obtenido una pequeña parte de la información, VLC comenzara a reproducir el vídeo mientras NodeJS continuar obteniendo el resto de la información, hasta completar todo el fichero.

Reproducir URL VLC

Reproducir URL VLC

Y ya solo nos queda comprobar que nuestro vídeo se muestra correctamente.

youtube mp3 cover

Descargar MP3 de Youtube

En esta ocasión y a falta de terminar la segunda entrega del tutorial / guía de referencia sobre Javascript y sus mejoras, he decido crear una pequeña aplicación que hacia tiempo tenia ganas de crear.

La aplicación es para convertir y descargar el audio MP3 de cualquier vídeo de Youtube.

Para llevar a cabo esta aplicación he usado las siguiente herramientas:

  • Para el diseño de la aplicación he usado Materialize CSS que es bastante simple y tiene un buen acabado.
  • La lógica esta realizada en Javascript nativo ya que tampoco tiene mucha complicación.
  • Para la descarga / conversión he usado NodeJS y algunas dependencias extras como ffmpeg, express y ytdl-core

A continuación un pequeño vídeo de la aplicación en funcionamiento y pulsando aquí puedes usarla.