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:
- reagere-tale-genkendelse: Et bibliotek til at konvertere tale til tekst, hvilket gør det tilgængeligt for React-komponenter.
- 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 - html-pdf-node: Et bibliotek til at konvertere en HTML-side eller offentlig URL til en PDF
- Axios: Et bibliotek til at lave HTTP-anmodninger i både browseren og Node.js
- 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 icomponents
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
harcontentEditable
,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
:
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.