Conversión de voz a PDF con NextJS y ExpressJS PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Conversión de voz a PDF con NextJS y ExpressJS

Con las interfaces de voz cada vez más importantes, vale la pena explorar algunas de las cosas que podemos hacer con las interacciones de voz. Por ejemplo, ¿qué pasaría si pudiéramos decir algo y transcribirlo y sacarlo como un PDF descargable?

Bueno, alerta de spoiler: absolutamente podemos ¡Haz eso! Hay bibliotecas y marcos que podemos improvisar para que esto suceda, y eso es lo que haremos juntos en este artículo.

Estas son las herramientas que estamos usando

En primer lugar, estos son los dos grandes jugadores: Next.js y Express.js.

Next.js agrega funcionalidades adicionales a React, incluidas funciones clave para crear sitios estáticos. Es una opción para muchos desarrolladores debido a lo que ofrece desde el primer momento, como enrutamiento dinámico, optimización de imágenes, enrutamiento integrado de dominios y subdominios, actualizaciones rápidas, enrutamiento de sistemas de archivos y rutas API... entre muchas, muchas otras cosas.

En nuestro caso, definitivamente necesitamos Next.js para su rutas API en nuestro servidor cliente. Queremos una ruta que tome un archivo de texto, lo convierta a PDF, lo escriba en nuestro sistema de archivos y luego envíe una respuesta al cliente.

Express.js nos permite poner en marcha una pequeña aplicación Node.js con enrutamiento, asistentes HTTP y plantillas. Es un servidor para nuestra propia API, que es lo que necesitaremos cuando pasemos y analicemos datos entre cosas.

Tenemos algunas otras dependencias que usaremos:

  1. reaccionar-reconocimiento de voz: una biblioteca para convertir voz en texto, poniéndola a disposición de los componentes de React.
  2. tiempo de ejecución del regenerador: una biblioteca para solucionar problemas de “regeneratorRuntime no está definido” error que aparece en Next.js cuando se usa el reconocimiento de voz de reacción
  3. html-pdf-nodo: una biblioteca para convertir una página HTML o una URL pública en un PDF
  4. axios: una biblioteca para realizar solicitudes HTTP tanto en el navegador como en Node.js
  5. cuernos: una biblioteca que permite compartir recursos de origen cruzado

Configuración

Lo primero que queremos hacer es crear dos carpetas de proyecto, una para el cliente y otra para el servidor. Nómbralos como quieras. estoy nombrando el mio audio-to-pdf-client y audio-to-pdf-server, respectivamente.

La forma más rápida de comenzar con Next.js en el lado del cliente es arrancarlo con crear-siguiente-aplicación. Entonces, abra su terminal y ejecute el siguiente comando desde la carpeta del proyecto de su cliente:

npx create-next-app client

Ahora necesitamos nuestro servidor Express. Podemos conseguirlo por cd-ing en la carpeta del proyecto del servidor y ejecutando el npm init mando. A package.json El archivo se creará en la carpeta del proyecto del servidor una vez que haya terminado.

Todavía necesitamos instalar Express, así que hagámoslo ahora con npm install express. Ahora podemos crear un nuevo index.js archivo en la carpeta del proyecto del servidor y suelte este código allí:

const express = require("express")
const app = express()

app.listen(4000, () => console.log("Server is running on port 4000"))

¿Listo para ejecutar el servidor?

node index.js

Vamos a necesitar un par de carpetas más y otro archivo para avanzar:

  • Créar un components carpeta en la carpeta del proyecto del cliente.
  • Créar un SpeechToText.jsx presentar en el components subcarpeta

Antes de continuar, tenemos que hacer una pequeña limpieza. Específicamente, necesitamos reemplazar el código predeterminado en el pages/index.js archivo con esto:

import Head from "next/head";
import SpeechToText from "../components/SpeechToText";

export default function Home() {
  return (
    <div className="home">
      <Head>
        <title>Audio To PDF</title>
        <meta
          name="description"
          content="An app that converts audio to pdf in the browser"
        />
        <link rel="icon" href="/es/favicon.ico" />
      </Head>

      <h1>Convert your speech to pdf</h1>

      <main>
        <SpeechToText />
      </main>
    </div>
  );
}

El importado SpeechToText el componente finalmente se exportará desde components/SpeechToText.jsx.

Instalamos las otras dependencias

Muy bien, ya tenemos la configuración inicial de nuestra aplicación. Ahora podemos instalar las bibliotecas que manejan los datos que se transmiten.

Podemos instalar las dependencias de nuestro cliente con:

npm install react-speech-recognition regenerator-runtime axios

Las dependencias de nuestro servidor Express son las siguientes, así que vamos a cd en la carpeta del proyecto del servidor e instálelos:

npm install html-pdf-node cors

Probablemente sea un buen momento para hacer una pausa y asegurarse de que los archivos en nuestras carpetas de proyectos estén intactos. Esto es lo que debe tener en la carpeta del proyecto del cliente en este momento:

/audio-to-pdf-web-client
├─ /components
|  └── SpeechToText.jsx
├─ /pages
|  ├─ _app.js
|  └── index.js
└── /styles
    ├─globals.css
    └── Home.module.css

Y esto es lo que debe tener en la carpeta del proyecto del servidor:

/audio-to-pdf-server
└── index.js

Construyendo la interfaz de usuario

Bueno, nuestra conversión de voz a PDF no sería tan buena si no hubiera forma de interactuar con ella, así que hagamos un componente React para ella que podamos llamar <SpeechToText>.

Puedes usar totalmente tu propio marcado. Esto es lo que tengo para darle una idea de las piezas que estamos armando:

import React from "react";

const SpeechToText = () => {
  return (
    <>
      <section>
        <div className="button-container">
          <button type="button" style={{ "--bgColor": "blue" }}>
            Start
          </button>
          <button type="button" style={{ "--bgColor": "orange" }}>
            Stop
          </button>
        </div>
        <div
          className="words"
          contentEditable
          suppressContentEditableWarning={true}
        ></div>
        <div className="button-container">
          <button type="button" style={{ "--bgColor": "red" }}>
            Reset
          </button>
          <button type="button" style={{ "--bgColor": "green" }}>
            Convert to pdf
          </button>
        </div>
      </section>
    </>
  );
};

export default SpeechToText;

Este componente devuelve un Fragmento de reacción que contiene un HTML <``section``> elemento que contiene tres divs:

  • .button-container contiene dos botones que se utilizarán para iniciar y detener el reconocimiento de voz.
  • .words tiene contentEditable y suppressContentEditableWarning atributos para hacer que este elemento sea editable y suprimir cualquier advertencia de React.
  • Otra .button-container contiene dos botones más que se utilizarán para restablecer y convertir voz a PDF, respectivamente.

El estilo es otra cosa por completo. No entraré en eso aquí, pero puede usar algunos estilos que escribí como punto de partida para el suyo propio. styles/global.css archivo.

Ver CSS completo
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

.home {
  background-color: #333;
  min-height: 100%;
  padding: 0 1rem;
  padding-bottom: 3rem;
}

h1 {
  width: 100%;
  max-width: 400px;
  margin: auto;
  padding: 2rem 0;
  text-align: center;
  text-transform: capitalize;
  color: white;
  font-size: 1rem;
}

.button-container {
  text-align: center;
  display: flex;
  justify-content: center;
  gap: 3rem;
}

button {
  color: white;
  background-color: var(--bgColor);
  font-size: 1.2rem;
  padding: 0.5rem 1.5rem;
  border: none;
  border-radius: 20px;
  cursor: pointer;
}

button:hover {
  opacity: 0.9;
}

button:active {
  transform: scale(0.99);
}

.words {
  max-width: 700px;
  margin: 50px auto;
  height: 50vh;
  border-radius: 5px;
  padding: 1rem 2rem 1rem 5rem;
  background-image: -webkit-gradient(
    linear,
    0 0,
    0 100%,
    from(#d9eaf3),
    color-stop(4%, #fff)
  ) 0 4px;
  background-size: 100% 3rem;
  background-attachment: scroll;
  position: relative;
  line-height: 3rem;
  overflow-y: auto;
}

.success,
.error {
  background-color: #fff;
  margin: 1rem auto;
  padding: 0.5rem 1rem;
  border-radius: 5px;
  width: max-content;
  text-align: center;
  display: block;
}

.success {
  color: green;
}

.error {
  color: red;
}

Las variables CSS allí se utilizan para controlar el color de fondo de los botones.

¡Veamos los últimos cambios! Correr npm run dev en la terminal y échales un vistazo.

Deberías ver esto en el navegador cuando visites http://localhost:3000:

Conversión de voz a PDF con NextJS y ExpressJS

¡Nuestra primera conversión de voz a texto!

La primera acción a realizar es importar las dependencias necesarias a nuestro <SpeechToText> componente:

import React, { useRef, useState } from "react";
import SpeechRecognition, {
  useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";

Luego, verificamos si el navegador admite el reconocimiento de voz y muestra un aviso si no es compatible:

const speechRecognitionSupported =
  SpeechRecognition.browserSupportsSpeechRecognition();

if (!speechRecognitionSupported) {
  return <div>Your browser does not support speech recognition.</div>;
}

A continuación, extraigamos transcript y resetTranscript del desplegable useSpeechRecognition() gancho:

const { transcript, resetTranscript } = useSpeechRecognition();

Esto es lo que necesitamos para el estado que maneja listening:

const [listening, setListening] = useState(false);

También necesitamos un ref para div con el contentEditable atributo, entonces necesitamos agregar el ref atribuirle y pasar transcript as children:

const textBodyRef = useRef(null);

…y:

<div
  className="words"
  contentEditable
  ref={textBodyRef}
  suppressContentEditableWarning={true}
  >
  {transcript}
</div>

Lo último que necesitamos aquí es una función que active el reconocimiento de voz y vincular esa función al onClick detector de eventos de nuestro botón. El botón establece la escucha true y lo hace funcionar continuamente. Deshabilitaremos el botón mientras esté en ese estado para evitar que activemos eventos adicionales.

const startListening = () => {
  setListening(true);
  SpeechRecognition.startListening({
    continuous: true,
  });
};

…y:

<button
  type="button"
  onClick={startListening}
  style={{ "--bgColor": "blue" }}
  disabled={listening}
>
  Start
</button>

Al hacer clic en el botón ahora debería iniciarse la transcripción.

Más funciones

Bien, entonces tenemos un componente que puede comienzo escuchando. Pero ahora lo necesitamos para hacer algunas otras cosas también, como stopListening, resetText y handleConversion. Hagamos esas funciones.

const stopListening = () => {
  setListening(false);
  SpeechRecognition.stopListening();
};

const resetText = () => {
  stopListening();
  resetTranscript();
  textBodyRef.current.innerText = "";
};

const handleConversion = async () => {}

Cada una de las funciones se añadirá a un onClick detector de eventos en los botones apropiados:

<button
  type="button"
  onClick={stopListening}
  style={{ "--bgColor": "orange" }}
  disabled={listening === false}
>
  Stop
</button>

<div className="button-container">
  <button
    type="button"
    onClick={resetText}
    style={{ "--bgColor": "red" }}
  >
    Reset
  </button>
  <button
    type="button"
    style={{ "--bgColor": "green" }}
    onClick={handleConversion}
  >
    Convert to pdf
  </button>
</div>

La handleConversion la función es asíncrona porque eventualmente realizaremos una solicitud de API. El botón "Detener" tiene el atributo deshabilitado que se activaría cuando la escucha es falsa.

Si reiniciamos el servidor y actualizamos el navegador, ahora podemos iniciar, detener y restablecer nuestra transcripción de voz en el navegador.

Ahora lo que necesitamos es que la aplicación transcribir ese discurso reconocido convirtiéndolo en un archivo PDF. Para eso, necesitamos la ruta del lado del servidor de Express.js.

Configuración de la ruta API

El propósito de esta ruta es tomar un archivo de texto, convertirlo a PDF, escribir ese PDF en nuestro sistema de archivos y luego enviar una respuesta al cliente.

Para configurar, abriríamos el server/index.js archivar e importar el html-pdf-node y fs dependencias que se utilizarán para escribir y abrir nuestro sistema de archivos.

const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)

A continuación, configuraremos nuestra ruta:

app.use(cors())
app.use(express.json())

app.post("/", (req, res) => {
  // etc.
})

Luego procedemos a definir nuestras opciones requeridas para usar html-pdf-node dentro de la ruta:

let options = { format: "A4" };
let file = {
  content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};

La options El objeto acepta un valor para establecer el tamaño y el estilo del papel. Los tamaños de papel siguen un sistema muy diferente al de las unidades de tamaño que normalmente usamos en la web. Por ejemplo, A4 es el tamaño de carta típico.

La file El objeto acepta la URL de un sitio web público o el marcado HTML. Para generar nuestra página HTML, utilizaremos el html, body, pre etiquetas HTML y el texto de la req.body.

Puede aplicar cualquier estilo de su elección.

A continuación, añadiremos un trycatch para manejar cualquier error que pueda aparecer en el camino:

try {

} catch(error){
  console.log(error);
  res.status(500).send(error);
}

A continuación, usaremos el generatePdf del desplegable html-pdf-node biblioteca para generar pdfBuffer (el archivo PDF sin formato) de nuestro archivo y crear un único pdfName:

HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
  // console.log("PDF Buffer:-", pdfBuffer);
  const pdfName = "./data/speech" + Date.now() + ".pdf";

  // Next code here
}

A partir de ahí, usamos el módulo del sistema de archivos para escribir, leer y (¡sí, finalmente!) enviar una respuesta a la aplicación cliente:

fs.writeFile(pdfName, pdfBuffer, function (writeError) {
  if (writeError) {
    return res
      .status(500)
      .json({ message: "Unable to write file. Try again." });
  }

  fs.readFile(pdfName, function (readError, readData) {
    if (!readError && readData) {
      // console.log({ readData });
      res.setHeader("Content-Type", "application/pdf");
      res.setHeader("Content-Disposition", "attachment");
      res.send(readData);
      return;
    }

    return res
      .status(500)
      .json({ message: "Unable to write file. Try again." });
  });
});

Vamos a desglosarlo un poco:

  • La writeFile El módulo del sistema de archivos acepta un nombre de archivo, datos y una función de devolución de llamada que puede devolver un mensaje de error si hay un problema al escribir en el archivo. Si está trabajando con una CDN que proporciona puntos de conexión de error, puede utilizarlos en su lugar.
  • La readFile El módulo del sistema de archivos acepta un nombre de archivo y una función de devolución de llamada que es capaz de devolver un error de lectura, así como los datos leídos. Una vez que no tengamos ningún error de lectura y los datos de lectura estén presentes, construiremos y enviaremos una respuesta al cliente. Nuevamente, esto se puede reemplazar con los puntos finales de su CDN si los tiene.
  • La res.setHeader("Content-Type", "application/pdf"); le dice al navegador que estamos enviando un archivo PDF.
  • La res.setHeader("Content-Disposition", "attachment"); le dice al navegador que haga descargables los datos recibidos.

Dado que la ruta API está lista, podemos usarla en nuestra aplicación en http://localhost:4000. Podemos proceder a la parte del cliente de nuestra aplicación para completar el handleConversion función.

Manejo de la conversión

Antes de que podamos empezar a trabajar en un handleConversion función, necesitamos crear un estado que maneje nuestras solicitudes de API para carga, error, éxito y otros mensajes. Vamos a usar React's useState gancho para configurar eso:

const [response, setResponse] = useState({
  loading: false,
  message: "",
  error: false,
  success: false,
});

En handleConversion función, comprobaremos cuándo se ha cargado la página web antes de ejecutar nuestro código y nos aseguraremos de que la div con el editable el atributo no está vacío:

if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
  // console.log(textBodyRef.current.innerText);

  if (!userText) {
    alert("Please speak or write some text.");
    return;
  }
}

Procedemos envolviendo nuestra eventual solicitud de API en un trycatch, gestionando cualquier error que pueda surgir, y actualizando el estado de respuesta:

try {

} catch(error){
  setResponse({
    ...response,
    loading: false,
    error: true,
    message:
      "An unexpected error occurred. Text not converted. Please try again",
    success: false,
  });
}

A continuación, establecemos algunos valores para el estado de respuesta y también establecemos la configuración para axios y haga una solicitud de publicación al servidor:

setResponse({
  ...response,
  loading: true,
  message: "",
  error: false,
  success: false,
});
const config = {
  headers: {
    "Content-Type": "application/json",
  },
  responseType: "blob",
};

const res = await axios.post(
  "http://localhost:4000",
  {
    text: textBodyRef.current.innerText,
  },
  config
);

Una vez que hayamos obtenido una respuesta exitosa, establecemos el estado de respuesta con los valores apropiados e indicamos al navegador que descargue el PDF recibido:

setResponse({
  ...response,
  loading: false,
  error: false,
  message:
    "Conversion was successful. Your download will start soon...",
  success: true,
});

// convert the received data to a file
const url = window.URL.createObjectURL(new Blob([res.data]));
// create an anchor element
const link = document.createElement("a");
// set the href of the created anchor element
link.href = url;
// add the download attribute, give the downloaded file a name
link.setAttribute("download", "yourfile.pdf");
// add the created anchor tag to the DOM
document.body.appendChild(link);
// force a click on the link to start a simulated download
link.click();

Y podemos usar lo siguiente debajo del contenido Editable div para mostrar mensajes:

<div>
  {response.success && <i className="success">{response.message}</i>}
  {response.error && <i className="error">{response.message}</i>}
</div>

Código final

He empaquetado todo en GitHub para que pueda consultar el código fuente completo tanto para el servidor como para el cliente.

Sello de tiempo:

Mas de Trucos CSS