Nu spraakinterfaces steeds meer een ding worden, is het de moeite waard om enkele dingen te onderzoeken die we met spraakinteracties kunnen doen. Wat als we iets zouden kunnen zeggen en dat laten transcriberen en uitpompen als een downloadbare pdf?
Nou, spoiler alert: wij absoluut wel doe dat! Er zijn bibliotheken en frameworks die we kunnen samenvoegen om het mogelijk te maken, en dat is wat we samen gaan doen in dit artikel.
Dit zijn de tools die we gebruiken
Ten eerste zijn dit de twee grote spelers: Next.js en Express.js.
Next.js pakt extra functionaliteiten voor React aan, inclusief belangrijke functies voor het bouwen van statische sites. Het is een go-to voor veel ontwikkelaars vanwege wat het direct uit de doos biedt, zoals dynamische routering, beeldoptimalisatie, ingebouwde domein- en subdomeinroutering, snelle vernieuwingen, bestandssysteemroutering en API-routes ... onder andere vele, vele andere dingen.
In ons geval hebben we Next.js zeker nodig voor zijn API-routes op onze clientserver. We willen een route die een tekstbestand neemt, het naar PDF converteert, het naar ons bestandssysteem schrijft en vervolgens een reactie naar de klant stuurt.
Express.js stelt ons in staat om een โโkleine Node.js-app aan de gang te krijgen met routering, HTTP-helpers en sjablonen. Het is een server voor onze eigen API, wat we nodig hebben als we gegevens tussen dingen doorgeven en parseren.
We hebben nog enkele andere afhankelijkheden die we gaan gebruiken:
- reageren-spraakherkenning: Een bibliotheek voor het converteren van spraak naar tekst, zodat deze beschikbaar wordt voor React-componenten.
- regenerator-looptijd: een bibliotheek voor het oplossen van problemen met de "
regeneratorRuntime
is niet gedefinieerd "fout die verschijnt in Next.js bij gebruik van react-speech-recognition - html-pdf-knooppunt: Een bibliotheek voor het converteren van een HTML-pagina of openbare URL naar een PDF
- Axios: Een bibliotheek voor het maken van HTTP-verzoeken in zowel de browser als Node.js
- hoorns: een bibliotheek die het delen van bronnen tussen verschillende bronnen mogelijk maakt
Opzetten
Het eerste dat we willen doen, is twee projectmappen maken, รฉรฉn voor de client en รฉรฉn voor de server. Noem ze zoals je wilt. ik noem de mijne audio-to-pdf-client
en audio-to-pdf-server
, Respectievelijk.
De snelste manier om met Next.js aan de clientzijde aan de slag te gaan, is door het op te starten met maak-volgende-app. Open dus uw terminal en voer de volgende opdracht uit vanuit uw clientprojectmap:
npx create-next-app client
Nu hebben we onze Express-server nodig. We kunnen het krijgen door cd
-ing naar de serverprojectmap en het uitvoeren van de npm init
opdracht. EEN package.json
bestand wordt gemaakt in de serverprojectmap zodra het klaar is.
We moeten Express nog echt installeren, dus laten we dat nu doen met npm install express
. Nu kunnen we een nieuwe . maken index.js
bestand in de serverprojectmap en zet deze code daar neer:
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Klaar om de server te draaien?
node index.js
We hebben nog een paar mappen nodig en nog een bestand om verder te gaan:
- Maak een
components
map in de projectmap van de klant. - Maak een
SpeechToText.jsx
bestand in decomponents
submap.
Voordat we verder gaan, moeten we nog wat opruimen. In het bijzonder moeten we de standaardcode in de pages/index.js
bestand met dit:
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="/nl/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
de geรฏmporteerde SpeechToText
component zal uiteindelijk worden geรซxporteerd van components/SpeechToText.jsx
.
Laten we de andere afhankelijkheden installeren
Okรฉ, we hebben de eerste installatie voor onze app uit de weg. Nu kunnen we de bibliotheken installeren die de gegevens verwerken die worden doorgegeven.
We kunnen onze klantafhankelijkheden installeren met:
npm install react-speech-recognition regenerator-runtime axios
Onze Express-serverafhankelijkheden zijn de volgende, dus laten we cd
in de serverprojectmap en installeer deze:
npm install html-pdf-node cors
Waarschijnlijk een goed moment om even te pauzeren en ervoor te zorgen dat de bestanden in onze projectmappen intact zijn. Dit is wat u op dit moment in de clientprojectmap zou moeten hebben:
/audio-to-pdf-web-client
โโ /components
| โโโ SpeechToText.jsx
โโ /pages
| โโ _app.js
| โโโ index.js
โโโ /styles
โโglobals.css
โโโ Home.module.css
En dit is wat u in de serverprojectmap zou moeten hebben:
/audio-to-pdf-server
โโโ index.js
De gebruikersinterface bouwen
Welnu, onze spraak-naar-PDF zou niet zo geweldig zijn als er geen manier is om ermee te communiceren, dus laten we er een React-component voor maken die we kunnen noemen <SpeechToText>
.
U kunt volledig uw eigen opmaak gebruiken. Dit is wat ik heb om je een idee te geven van de stukken die we aan het samenstellen zijn:
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;
Deze component retourneert a Reageer fragment die een HTML bevat <``section``>
element dat drie divs bevat:
.button-container
bevat twee knoppen die worden gebruikt om spraakherkenning te starten en te stoppen..words
heeftcontentEditable
ensuppressContentEditableWarning
attributen om dit element bewerkbaar te maken en eventuele waarschuwingen van React te onderdrukken.- Nog een
.button-container
bevat nog twee knoppen die zullen worden gebruikt om respectievelijk spraak opnieuw in te stellen en naar PDF te converteren.
Styling is iets heel anders. Ik zal er hier niet op ingaan, maar u bent van harte welkom om enkele stijlen die ik heb geschreven te gebruiken als uitgangspunt voor uw eigen stijlen styles/global.css
bestand.
Volledige CSS bekijken
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;
}
De CSS-variabelen daarin worden gebruikt om de achtergrondkleur van de knoppen te regelen.
Laten we eens kijken naar de laatste wijzigingen! Rennen npm run dev
in de terminal en bekijk ze.
U zou dit in de browser moeten zien wanneer u bezoekt http://localhost:3000
:
Onze eerste spraak naar tekst conversie!
De eerste actie die moet worden ondernomen, is het importeren van de noodzakelijke afhankelijkheden in onze <SpeechToText>
component:
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Vervolgens controleren we of spraakherkenning door de browser wordt ondersteund en geven we een melding als dit niet wordt ondersteund:
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Laten we vervolgens extraheren transcript
en resetTranscript
van het useSpeechRecognition()
haak:
const { transcript, resetTranscript } = useSpeechRecognition();
Dit is wat we nodig hebben voor de staat die handelt listening
:
const [listening, setListening] = useState(false);
We hebben ook een ref
voor de div
met de contentEditable
attribuut, dan moeten we de . toevoegen ref
toeschrijven en doorgeven transcript
as children
:
const textBodyRef = useRef(null);
โฆen:
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
Het laatste dat we hier nodig hebben, is een functie die spraakherkenning activeert en die functie koppelt aan de onClick
gebeurtenis luisteraar van onze knop. Met de knop wordt luisteren naar true
en laat het continu draaien. We schakelen de knop uit terwijl deze zich in die staat bevindt om te voorkomen dat we extra gebeurtenissen afvuren.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
โฆen:
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Als u op de knop klikt, moet de transcriptie nu worden gestart.
Meer functies
OK, dus we hebben een component die kan begin luisteren. Maar nu hebben we het ook nodig om een โโpaar andere dingen te doen, zoals: stopListening
, resetText
en handleConversion
. Laten we die functies maken.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Elk van de functies wordt toegevoegd aan een onClick
gebeurtenislistener op de juiste knoppen:
<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>
De handleConversion
functie is asynchroon omdat we uiteindelijk een API-verzoek zullen doen. De knop "Stoppen" heeft het kenmerk uitgeschakeld dat zou worden geactiveerd als luisteren niet waar is.
Als we de server opnieuw opstarten en de browser vernieuwen, kunnen we nu onze spraaktranscriptie in de browser starten, stoppen en resetten.
Wat we nu nodig hebben, is dat de app overschrijven die herkende spraak door deze naar een PDF-bestand te converteren. Daarvoor hebben we het pad aan de serverzijde van Express.js nodig.
De API-route instellen
Het doel van deze route is om een โโtekstbestand te nemen, het naar een PDF te converteren, die PDF naar ons bestandssysteem te schrijven en vervolgens een reactie naar de klant te sturen.
Om in te stellen, zouden we de . openen server/index.js
bestand en importeer de html-pdf-node
en fs
afhankelijkheden die zullen worden gebruikt om ons bestandssysteem te schrijven en te openen.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Vervolgens zullen we onze route opzetten:
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
We gaan dan verder met het definiรซren van onze opties die nodig zijn om te gebruiken html-pdf-node
binnen de route:
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
De options
object accepteert een waarde om het papierformaat en de stijl in te stellen. Papierformaten volgen een heel ander systeem dan de maateenheden die we doorgaans op internet gebruiken. Bijvoorbeeld, A4 is het typische letterformaat.
De file
object accepteert de URL van een openbare website of HTML-opmaak. Om onze HTML-pagina te genereren, gebruiken we de html
, body
, pre
HTML-tags en de tekst van de req.body
.
U kunt elke gewenste styling toepassen.
Vervolgens zullen we een toevoegen trycatch
om eventuele fouten op te lossen die onderweg kunnen optreden:
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Vervolgens gebruiken we de generatePdf
van het html-pdf-node
bibliotheek om een โโte genereren pdfBuffer
(het onbewerkte PDF-bestand) uit ons bestand en maak een unieke pdfName
:
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Van daaruit gebruiken we de bestandssysteemmodule om te schrijven, lezen en (ja, eindelijk!) een reactie naar de client-app te sturen:
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." });
});
});
Laten we dat een beetje opsplitsen:
- De
writeFile
bestandssysteemmodule accepteert een bestandsnaam, gegevens en een callback-functie die een foutmelding kan retourneren als er een probleem is met het schrijven naar het bestand. Als u werkt met een CDN die fouteindpunten biedt, kunt u die in plaats daarvan gebruiken. - De
readFile
bestandssysteemmodule accepteert een bestandsnaam en een callback-functie die in staat is of een leesfout retourneert, evenals de leesgegevens. Zodra we geen leesfout hebben en de leesgegevens aanwezig zijn, zullen we een reactie opstellen en naar de klant sturen. Nogmaals, dit kan worden vervangen door de eindpunten van uw CDN als u die heeft. - De
res.setHeader("Content-Type", "application/pdf");
vertelt de browser dat we een PDF-bestand verzenden. - De
res.setHeader("Content-Disposition", "attachment");
vertelt de browser om de ontvangen gegevens downloadbaar te maken.
Aangezien de API-route gereed is, kunnen we deze gebruiken in onze app op http://localhost:4000
. We kunnen doorgaan naar het klantgedeelte van onze applicatie om de handleConversion
functie.
De conversie afhandelen
Voordat we kunnen gaan werken aan een handleConversion
functie, moeten we een status maken die onze API-verzoeken voor laden, fouten, succes en andere berichten afhandelt. We gaan React's gebruiken useState
haak om dat in te stellen:
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
In het handleConversion
functie, zullen we controleren wanneer de webpagina is geladen voordat onze code wordt uitgevoerd en ervoor zorgen dat de div
met de editable
attribuut is niet leeg:
if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
// console.log(textBodyRef.current.innerText);
if (!userText) {
alert("Please speak or write some text.");
return;
}
}
We gaan verder door ons uiteindelijke API-verzoek in een trycatch
, het afhandelen van eventuele fouten en het bijwerken van de antwoordstatus:
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Vervolgens stellen we enkele waarden in voor de responsstatus en stellen we ook config in voor axios
en doe een postverzoek aan de server:
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
);
Zodra we een succesvol antwoord hebben gekregen, stellen we de antwoordstatus in met de juiste waarden en instrueren we de browser om de ontvangen PDF te downloaden:
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();
En we kunnen het volgende gebruiken onder de inhoudBewerkbaar div
voor het weergeven van berichten:
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Laatste code
Ik heb alles op GitHub verpakt, zodat je de volledige broncode voor zowel de server als de client kunt bekijken.