Convertendo fala em PDF com NextJS e ExpressJS PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.

Convertendo fala para PDF com NextJS e ExpressJS

Com as interfaces de fala se tornando cada vez mais comuns, vale a pena explorar algumas das coisas que podemos fazer com as interações de fala. Tipo, e se pudéssemos dizer alguma coisa e ter isso transcrito e bombeado como um PDF para download?

Bem, alerta de spoiler: nós absolutamente pode faça isso! Existem bibliotecas e frameworks que podemos juntar para que isso aconteça, e é isso que vamos fazer juntos neste artigo.

Estas são as ferramentas que estamos usando

Em primeiro lugar, estes são os dois grandes players: Next.js e Express.js.

Próximo.js adiciona funcionalidades adicionais ao React, incluindo recursos-chave para a construção de sites estáticos. É um item obrigatório para muitos desenvolvedores por causa do que oferece imediatamente, como roteamento dinâmico, otimização de imagem, roteamento integrado de domínio e subdomínio, atualizações rápidas, roteamento de sistema de arquivos e rotas de API... entre muitas, muitas outras coisas.

No nosso caso, definitivamente precisamos do Next.js para sua Rotas de API em nosso servidor cliente. Queremos uma rota que pegue um arquivo de texto, converta-o em PDF, grave-o em nosso sistema de arquivos e envie uma resposta ao cliente.

Express.js nos permite obter um pequeno aplicativo Node.js com roteamento, auxiliares HTTP e modelagem. É um servidor para nossa própria API, que é o que precisaremos ao passar e analisar dados entre as coisas.

Temos algumas outras dependências que usaremos:

  1. reagir-reconhecimento de fala: Uma biblioteca para converter fala em texto, tornando-a disponível para componentes React.
  2. tempo de execução do regenerador: Uma biblioteca para solucionar problemas do “regeneratorRuntime não está definido” que aparece no Next.js ao usar o reconhecimento de fala de reação
  3. nó html-pdf: Uma biblioteca para converter uma página HTML ou URL pública em um PDF
  4. axios: uma biblioteca para fazer solicitações HTTP no navegador e no Node.js
  5. duri: Uma biblioteca que permite o compartilhamento de recursos entre origens

Configurando

A primeira coisa que queremos fazer é criar duas pastas de projeto, uma para o cliente e outra para o servidor. Nomeie-os como quiser. estou nomeando o meu audio-to-pdf-client e audio-to-pdf-server, Respectivamente.

A maneira mais rápida de começar a usar o Next.js no lado do cliente é inicializá-lo com criar-próximo-aplicativo. Portanto, abra seu terminal e execute o seguinte comando na pasta do projeto do cliente:

npx create-next-app client

Agora precisamos do nosso servidor Express. Podemos obtê-lo por cd-ing na pasta do projeto do servidor e executando o npm init comando. UMA package.json arquivo será criado na pasta do projeto do servidor assim que estiver pronto.

Ainda precisamos instalar o Express, então vamos fazer isso agora com npm install express. Agora podemos criar um novo index.js arquivo na pasta do projeto do servidor e solte este código lá:

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

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

Pronto para executar o servidor?

node index.js

Vamos precisar de mais algumas pastas e outro arquivo para avançar:

  • Crie uma components pasta na pasta do projeto do cliente.
  • Crie uma SpeechToText.jsx arquivo no components subpasta.

Antes de prosseguirmos, temos uma pequena limpeza a fazer. Especificamente, precisamos substituir o código padrão no pages/index.js arquivo com este:

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="/pt/favicon.ico" />
      </Head>

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

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

O importado SpeechToText componente acabará por ser exportado de components/SpeechToText.jsx.

Vamos instalar as outras dependências

Tudo bem, temos a configuração inicial do nosso aplicativo fora do caminho. Agora podemos instalar as bibliotecas que lidam com os dados que são transmitidos.

Podemos instalar nossas dependências de cliente com:

npm install react-speech-recognition regenerator-runtime axios

Nossas dependências do servidor Express são as próximas, então vamos cd na pasta do projeto do servidor e instale-os:

npm install html-pdf-node cors

Provavelmente um bom momento para fazer uma pausa e certificar-se de que os arquivos em nossas pastas de projeto estão intactos. Aqui está o que você deve ter na pasta do projeto do cliente neste momento:

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

E aqui está o que você deve ter na pasta do projeto do servidor:

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

Construindo a IU

Bem, nosso speech-to-PDF não seria tão bom se não houvesse como interagir com ele, então vamos fazer um componente React para ele que podemos chamar <SpeechToText>.

Você pode usar totalmente sua própria marcação. Aqui está o que eu tenho para dar uma ideia das peças que estamos montando:

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 retorna um Reagir fragmento que contém um HTML <``section``> elemento que contém três divs:

  • .button-container contém dois botões que serão usados ​​para iniciar e parar o reconhecimento de fala.
  • .words tem contentEditable e suppressContentEditableWarning atributos para tornar este elemento editável e suprimir quaisquer avisos do React.
  • Outro .button-container contém mais dois botões que serão usados ​​para redefinir e converter fala em PDF, respectivamente.

Estilo é outra coisa. Não vou entrar nisso aqui, mas você pode usar alguns estilos que escrevi como ponto de partida para o seu próprio styles/global.css arquivo.

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;
}

As variáveis ​​CSS estão sendo usadas para controlar a cor de fundo dos botões.

Vamos ver as últimas mudanças! Corre npm run dev no terminal e confira.

Você deve ver isso no navegador quando visitar http://localhost:3000:

Convertendo fala para PDF com NextJS e ExpressJS

Nosso primeiro discurso para conversão de texto!

A primeira ação a ser tomada é importar as dependências necessárias para o nosso <SpeechToText> componente:

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

Em seguida, verificamos se o reconhecimento de fala é suportado pelo navegador e renderizamos um aviso se não for suportado:

const speechRecognitionSupported =
  SpeechRecognition.browserSupportsSpeechRecognition();

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

Em seguida, vamos extrair transcript e resetTranscript do useSpeechRecognition() gancho:

const { transcript, resetTranscript } = useSpeechRecognition();

Isso é o que precisamos para o estado que lida com listening:

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

Também precisamos de um ref para o div com o contentEditable atributo, então precisamos adicionar o ref atribua a ele e passe transcript as children:

const textBodyRef = useRef(null);

…e:

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

A última coisa que precisamos aqui é uma função que acione o reconhecimento de fala e vincule essa função ao onClick ouvinte de eventos do nosso botão. O botão define a escuta true e faz com que funcione continuamente. Desabilitaremos o botão enquanto estiver nesse estado para nos impedir de disparar eventos adicionais.

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

…e:

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

Clicar no botão agora deve iniciar a transcrição.

Mais funções

OK, então temos um componente que pode começo ouvindo. Mas agora precisamos dele para fazer algumas outras coisas também, como stopListening, resetText e handleConversion. Vamos fazer essas funções.

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

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

const handleConversion = async () => {}

Cada uma das funções será adicionada a um onClick ouvinte de eventos nos botões apropriados:

<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>

A handleConversion A função é assíncrona porque eventualmente faremos uma solicitação de API. O botão “Parar” possui o atributo desabilitado que seria acionado quando a escuta for falsa.

Se reiniciarmos o servidor e atualizarmos o navegador, agora podemos iniciar, parar e redefinir nossa transcrição de fala no navegador.

Agora o que precisamos é que o aplicativo transcrever que reconheceu a fala convertendo-a em um arquivo PDF. Para isso, precisamos do caminho do lado do servidor do Express.js.

Configurando a rota da API

O objetivo dessa rota é pegar um arquivo de texto, convertê-lo em PDF, gravar esse PDF em nosso sistema de arquivos e enviar uma resposta ao cliente.

Para configurar, abrimos o server/index.js arquivo e importe o html-pdf-node e fs dependências que serão usadas para escrever e abrir nosso sistema de arquivos.

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

Em seguida, vamos configurar nossa rota:

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

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

Em seguida, passamos a definir nossas opções necessárias para usar html-pdf-node dentro da rota:

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

A options O objeto aceita um valor para definir o tamanho e o estilo do papel. Os tamanhos de papel seguem um sistema muito diferente das unidades de tamanho que normalmente usamos na web. Por exemplo, A4 é o tamanho típico de carta.

A file O objeto aceita a URL de um site público ou marcação HTML. Para gerar nossa página HTML, usaremos o html, body, pre Tags HTML e o texto da req.body.

Você pode aplicar qualquer estilo de sua escolha.

A seguir, adicionaremos um trycatch para lidar com quaisquer erros que possam aparecer ao longo do caminho:

try {

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

A seguir, usaremos o generatePdf do html-pdf-node biblioteca para gerar um pdfBuffer (o arquivo PDF bruto) do nosso arquivo e criar um pdfName:

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

  // Next code here
}

A partir daí, usamos o módulo filesystem para escrever, ler e (sim, finalmente!) enviar uma resposta para o aplicativo 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 detalhar um pouco:

  • A writeFile O módulo do sistema de arquivos aceita um nome de arquivo, dados e uma função de retorno de chamada que pode retornar uma mensagem de erro se houver um problema ao gravar no arquivo. Se você estiver trabalhando com uma CDN que fornece pontos de extremidade de erro, poderá usá-los.
  • A readFile O módulo do sistema de arquivos aceita um nome de arquivo e uma função de retorno de chamada capaz de retornar um erro de leitura, bem como os dados lidos. Uma vez que não tenhamos nenhum erro de leitura e os dados de leitura estejam presentes, construiremos e enviaremos uma resposta ao cliente. Novamente, isso pode ser substituído pelos endpoints do seu CDN, se você os tiver.
  • A res.setHeader("Content-Type", "application/pdf"); informa ao navegador que estamos enviando um arquivo PDF.
  • A res.setHeader("Content-Disposition", "attachment"); diz ao navegador para fazer download dos dados recebidos.

Como a rota da API está pronta, podemos usá-la em nosso aplicativo em http://localhost:4000. Podemos prosseguir para a parte do cliente do nosso aplicativo para concluir o handleConversion função.

Manipulando a conversão

Antes de começarmos a trabalhar em um handleConversion função, precisamos criar um estado que lide com nossas solicitações de API para carregamento, erro, sucesso e outras mensagens. Vamos usar o React useState gancho para configurar isso:

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

No handleConversion função, verificaremos quando a página da Web foi carregada antes de executar nosso código e garantiremos que o div com o editable atributo não está vazio:

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

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

Prosseguimos envolvendo nossa eventual solicitação de API em um trycatch, tratando de qualquer erro que possa surgir e atualizando o estado de resposta:

try {

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

Em seguida, definimos alguns valores para o estado de resposta e também definimos a configuração para axios e faça uma requisição post para o 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
);

Assim que obtivermos uma resposta bem-sucedida, definimos o estado da resposta com os valores apropriados e instruímos o navegador a baixar o PDF recebido:

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();

E podemos usar o seguinte abaixo do contentEditable div para exibir mensagens:

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

Código final

Empacotei tudo no GitHub para que você possa conferir o código-fonte completo do servidor e do cliente.

Carimbo de hora:

Mais de Truques CSS