Konvertering af tale til PDF med NextJS og ExpressJS PlatoBlockchain Data Intelligence. Lodret søgning. Ai.

Konvertering af tale til PDF med NextJS og ExpressJS

Når talegrænseflader bliver mere af en ting, er det værd at udforske nogle af de ting, vi kan gøre med taleinteraktioner. Hvad nu hvis vi kunne sige noget og få det transskriberet og pumpet ud som en downloadbar PDF?

Nå, spoiler alert: vi absolut kan gøre det! Der er biblioteker og rammer, vi kan flette sammen for at få det til at ske, og det er det, vi skal gøre sammen i denne artikel.

Det er de værktøjer, vi bruger

For det første er disse to store spillere: Next.js og Express.js.

Next.js tager fat på yderligere funktionaliteter til React, herunder nøglefunktioner til at bygge statiske steder. Det er en go-to for mange udviklere på grund af det, det tilbyder lige ud af boksen, såsom dynamisk routing, billedoptimering, indbygget domæne- og underdomæne-routing, hurtige opdateringer, filsystem-routing og API-ruter ... blandt mange, mange andre ting.

I vores tilfælde har vi helt sikkert brug for Next.js til sin API-ruter på vores klientserver. Vi vil have en rute, der tager en tekstfil, konverterer den til PDF, skriver den til vores filsystem og sender et svar til klienten.

Express.js giver os mulighed for at få en lille Node.js-app i gang med routing, HTTP-hjælpere og skabeloner. Det er en server til vores egen API, som er det, vi skal bruge, når vi videregiver og analyserer data mellem ting.

Vi har nogle andre afhængigheder, vi vil tage i brug:

  1. reagere-tale-genkendelse: Et bibliotek til at konvertere tale til tekst, hvilket gør det tilgængeligt for React-komponenter.
  2. regenerator køretid: Et bibliotek til fejlfinding af "regeneratorRuntime er ikke defineret” fejl, der dukker op i Next.js, når du bruger react-speech recognition
  3. html-pdf-node: Et bibliotek til at konvertere en HTML-side eller offentlig URL til en PDF
  4. Axios: Et bibliotek til at lave HTTP-anmodninger i både browseren og Node.js
  5. cors: Et bibliotek, der tillader deling af ressourcer på tværs af oprindelse

Opsætning

Det første, vi vil gøre, er at oprette to projektmapper, en til klienten og en til serveren. Navngiv dem, hvad du vil. Jeg navngiver min audio-to-pdf-client , audio-to-pdf-server, henholdsvis.

Den hurtigste måde at komme i gang med Next.js på klientsiden er at bootstrap det med opret-næste-app. Så åbn din terminal og kør følgende kommando fra din klientprojektmappe:

npx create-next-app client

Nu har vi brug for vores Express-server. Vi kan klare det cd-ing ind i serverprojektmappen og kører npm init kommando. EN package.json filen oprettes i serverprojektmappen, når den er færdig.

Vi mangler stadig at installere Express, så lad os gøre det nu med npm install express. Nu kan vi oprette en ny index.js fil i serverprojektmappen og slip denne kode derinde:

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

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

Klar til at køre serveren?

node index.js

Vi skal bruge et par flere mapper og en anden fil for at komme videre:

  • Opret en components mappe i klientprojektmappen.
  • Opret en SpeechToText.jsx fil i components undermappe.

Inden vi går videre, skal vi rydde lidt op. Specifikt skal vi erstatte standardkoden i pages/index.js fil med denne:

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

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

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

Den importerede SpeechToText komponent vil i sidste ende blive eksporteret fra components/SpeechToText.jsx.

Lad os installere de andre afhængigheder

Okay, vi har den indledende opsætning af vores app af vejen. Nu kan vi installere de biblioteker, der håndterer de data, der sendes rundt.

Vi kan installere vores klientafhængigheder med:

npm install react-speech-recognition regenerator-runtime axios

Vores Express-serverafhængigheder er næste gang, så lad os cd ind i serverprojektmappen og installer disse:

npm install html-pdf-node cors

Sandsynligvis et godt tidspunkt at holde pause og sikre sig, at filerne i vores projektmapper er i takt. Her er, hvad du skal have i klientprojektmappen på dette tidspunkt:

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

Og her er, hvad du skal have i serverprojektmappen:

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

Opbygning af UI

Nå, vores tale-til-PDF ville ikke være så fantastisk, hvis der ikke er nogen måde at interagere med den på, så lad os lave en React-komponent til den, som vi kan kalde <SpeechToText>.

Du kan helt bruge din egen markup. Her er, hvad jeg skal give dig en idé om de stykker, vi sammensætter:

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;

Denne komponent returnerer en Reaktionsfragment der indeholder en HTML <``section``> element, der indeholder tre divs:

  • .button-container indeholder to knapper, der vil blive brugt til at starte og stoppe talegenkendelse.
  • .words har contentEditable , suppressContentEditableWarning attributter for at gøre dette element redigerbart og undertrykke eventuelle advarsler fra React.
  • En anden .button-container indeholder yderligere to knapper, der vil blive brugt til henholdsvis at nulstille og konvertere tale til PDF.

Styling er en helt anden ting. Jeg vil ikke komme ind på det her, men du er velkommen til at bruge nogle stile jeg skrev enten som udgangspunkt for din egen styles/global.css fil.

Se hele CSS
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;
}

CSS-variablerne derinde bliver brugt til at styre baggrundsfarven på knapperne.

Lad os se de seneste ændringer! Løb npm run dev i terminalen og tjek dem ud.

Du bør se dette i browseren, når du besøger http://localhost:3000:

Konvertering af tale til PDF med NextJS og ExpressJS

Vores første tale til tekst konvertering!

Den første handling at tage er at importere de nødvendige afhængigheder til vores <SpeechToText> komponent:

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

Derefter tjekker vi, om talegenkendelse understøttes af browseren, og afgiver en meddelelse, hvis den ikke understøttes:

const speechRecognitionSupported =
  SpeechRecognition.browserSupportsSpeechRecognition();

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

Næste op, lad os udtrække transcript , resetTranscript fra useSpeechRecognition() krog:

const { transcript, resetTranscript } = useSpeechRecognition();

Det er det, vi har brug for til den stat, der håndterer listening:

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

Vi har også brug for en ref for div med contentEditable attribut, så skal vi tilføje ref tilskrive det og bestå transcript as children:

const textBodyRef = useRef(null);

…og:

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

Den sidste ting, vi har brug for her, er en funktion, der udløser talegenkendelse og at binde den funktion til onClick begivenhedslytter af vores knap. Knappen indstiller lytning til true og får den til at køre kontinuerligt. Vi deaktiverer knappen, mens den er i den tilstand, for at forhindre os i at affyre yderligere begivenheder.

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

…og:

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

Ved at klikke på knappen skal transskriptionen nu startes.

Flere funktioner

OK, så vi har en komponent, der kan starte hører efter. Men nu har vi brug for den til også at gøre et par andre ting, f.eks stopListening, resetText , handleConversion. Lad os lave disse funktioner.

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

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

const handleConversion = async () => {}

Hver af funktionerne vil blive tilføjet til en onClick begivenhedslytter på de relevante knapper:

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

handleConversion funktion er asynkron, fordi vi i sidste ende vil lave en API-anmodning. "Stop"-knappen har den deaktiverede egenskab, der vil blive udløst, når lytning er falsk.

Hvis vi genstarter serveren og opdaterer browseren, kan vi nu starte, stoppe og nulstille vores taletransskription i browseren.

Nu skal vi bruge appen transskribere den genkendte tale ved at konvertere den til en PDF-fil. Til det har vi brug for stien på serversiden fra Express.js.

Opsætning af API-ruten

Formålet med denne rute er at tage en tekstfil, konvertere den til en PDF, skrive den PDF til vores filsystem og derefter sende et svar til klienten.

For at konfigurere, ville vi åbne server/index.js fil og importer html-pdf-node , fs afhængigheder, der vil blive brugt til at skrive og åbne vores filsystem.

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

Dernæst sætter vi vores rute op:

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

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

Vi fortsætter derefter med at definere vores nødvendige muligheder for at bruge html-pdf-node inde på ruten:

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

options objekt accepterer en værdi for at indstille papirstørrelsen og stilen. Papirstørrelser følger et meget andet system end de størrelsesenheder, vi typisk bruger på nettet. For eksempel, A4 er den typiske bogstavstørrelse.

file objekt accepterer enten URL'en på et offentligt websted eller HTML-markering. For at generere vores HTML-side, vil vi bruge html, body, pre HTML-tags og teksten fra req.body.

Du kan anvende enhver styling efter eget valg.

Dernæst vil vi tilføje en trycatch for at håndtere eventuelle fejl, der måtte dukke op undervejs:

try {

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

Dernæst vil vi bruge generatePdf fra html-pdf-node bibliotek til at generere en pdfBuffer (den rå PDF-fil) fra vores fil og opret en unik pdfName:

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

  // Next code here
}

Derfra bruger vi filsystemmodulet til at skrive, læse og (ja, endelig!) sende et svar til klientappen:

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." });
  });
});

Lad os bryde det lidt ned:

  • writeFile filsystemmodulet accepterer et filnavn, data og en tilbagekaldsfunktion, der kan returnere en fejlmeddelelse, hvis der er et problem med at skrive til filen. Hvis du arbejder med et CDN, der giver fejlendepunkter, kan du bruge dem i stedet for.
  • readFile filsystemmodulet accepterer et filnavn og en tilbagekaldsfunktion, der er i stand til eller returnerer en læsefejl samt de læste data. Når vi ikke har nogen læsefejl, og de læste data er til stede, konstruerer vi og sender et svar til klienten. Igen, dette kan erstattes med din CDN's endepunkter, hvis du har dem.
  • res.setHeader("Content-Type", "application/pdf"); fortæller browseren, at vi sender en PDF-fil.
  • res.setHeader("Content-Disposition", "attachment"); beder browseren om at gøre de modtagne data downloades.

Da API-ruten er klar, kan vi bruge den i vores app kl http://localhost:4000. Vi kan gå videre til klientdelen af ​​vores ansøgning for at fuldføre handleConversion funktion.

Håndtering af konverteringen

Inden vi kan begynde at arbejde på en handleConversion funktion, skal vi oprette en tilstand, der håndterer vores API-anmodninger om indlæsning, fejl, succes og andre meddelelser. Vi vil bruge React's useState krog for at sætte det op:

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

I handleConversion funktion, vil vi tjekke, hvornår websiden er blevet indlæst, før vi kører vores kode, og sørge for, at div med editable attribut er ikke tom:

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

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

Vi fortsætter ved at pakke vores eventuelle API-anmodning ind i en trycatch, håndtering af enhver fejl, der måtte opstå, og opdatering af svartilstanden:

try {

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

Dernæst indstiller vi nogle værdier for svartilstanden og indstiller også konfiguration for axios og lav en postanmodning til serveren:

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

Når vi har fået et vellykket svar, indstiller vi svartilstanden med de relevante værdier og instruerer browseren om at downloade den modtagne PDF:

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

Og vi kan bruge følgende under contentEditable div for at vise beskeder:

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

Endelig kode

Jeg har pakket alt sammen på GitHub, så du kan tjekke den fulde kildekode for både serveren og klienten.

Tidsstempel:

Mere fra CSS-tricks