När talgränssnitt blir mer av en sak är det värt att utforska några av de saker vi kan göra med talinteraktioner. Tänk om vi kunde säga något och få det transkriberat och pumpat ut som en nedladdningsbar PDF?
Tja, spoiler alert: vi absolut Kan gör det! Det finns bibliotek och ramverk som vi kan lägga ihop för att få det att hända, och det är vad vi ska göra tillsammans i den här artikeln.
Det här är verktygen vi använder
Först och främst är det här de två stora spelarna: Next.js och Express.js.
Next.js använder ytterligare funktioner till React, inklusive nyckelfunktioner för att bygga statiska platser. Det är ett val för många utvecklare på grund av vad det erbjuder direkt från lådan, som dynamisk routing, bildoptimering, inbyggd domän och underdomän routing, snabbuppdateringar, filsystem routing och API-rutter ... bland många, många andra saker.
I vårt fall behöver vi definitivt Next.js för dess API-rutter på vår klientserver. Vi vill ha en rutt som tar en textfil, konverterar den till PDF, skriver den till vårt filsystem och sedan skickar ett svar till klienten.
Express.js låter oss få igång en liten Node.js-app med routing, HTTP-hjälpare och mallar. Det är en server för vårt eget API, vilket är vad vi behöver när vi skickar och analyserar data mellan saker.
Vi har några andra beroenden som vi kommer att använda:
- reagera-taligenkänning: Ett bibliotek för att konvertera tal till text, vilket gör det tillgängligt för React-komponenter.
- regenerator-körtid: Ett bibliotek för felsökning av "
regeneratorRuntime
är inte definierat”-fel som dyker upp i Next.js när du använder react-speech recognition - html-pdf-nod: Ett bibliotek för att konvertera en HTML-sida eller offentlig URL till en PDF
- Axios: Ett bibliotek för att göra HTTP-förfrågningar i både webbläsaren och Node.js
- cors: Ett bibliotek som tillåter resursdelning mellan olika ursprung
Inställning
Det första vi vill göra är att skapa två projektmappar, en för klienten och en för servern. Namnge dem vad du vill. Jag namnger min audio-to-pdf-client
och audio-to-pdf-server
, Respektive.
Det snabbaste sättet att komma igång med Next.js på klientsidan är att bootstrap det med skapa-nästa-app. Så öppna din terminal och kör följande kommando från din klientprojektmapp:
npx create-next-app client
Nu behöver vi vår Express-server. Vi kan klara det cd
in i serverns projektmapp och kör npm init
kommando. A package.json
filen kommer att skapas i serverns projektmapp när den är klar.
Vi behöver fortfarande faktiskt installera Express, så låt oss göra det nu med npm install express
. Nu kan vi skapa en ny index.js
fil i serverprojektmappen och släpp den här koden där:
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Är du redo att köra servern?
node index.js
Vi kommer att behöva ytterligare ett par mappar och ytterligare en fil för att gå vidare:
- Skapa en
components
mapp i klientprojektmappen. - Skapa en
SpeechToText.jsx
fil icomponents
mapp.
Innan vi går vidare har vi en liten städning att göra. Specifikt måste vi ersätta standardkoden i pages/index.js
fil med detta:
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="/sv/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
Den importerade SpeechToText
komponent kommer så småningom att exporteras från components/SpeechToText.jsx
.
Låt oss installera de andra beroenden
Okej, vi har den första installationen för vår app ur vägen. Nu kan vi installera biblioteken som hanterar data som skickas runt.
Vi kan installera våra klientberoenden med:
npm install react-speech-recognition regenerator-runtime axios
Våra Express-serverberoenden är nästa, så låt oss cd
i serverprojektmappen och installera dessa:
npm install html-pdf-node cors
Förmodligen en bra tid att pausa och se till att filerna i våra projektmappar är i takt. Här är vad du bör ha i klientprojektmappen vid det här laget:
/audio-to-pdf-web-client
├─ /components
| └── SpeechToText.jsx
├─ /pages
| ├─ _app.js
| └── index.js
└── /styles
├─globals.css
└── Home.module.css
Och här är vad du bör ha i serverprojektmappen:
/audio-to-pdf-server
└── index.js
Bygga UI
Tja, vårt tal-till-PDF skulle inte vara så bra om det inte finns något sätt att interagera med det, så låt oss skapa en React-komponent för den som vi kan kalla <SpeechToText>
.
Du kan helt använda din egen uppmärkning. Här är vad jag har för att ge dig en uppfattning om de delar vi sätter ihop:
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;
Denna komponent returnerar en Reaktionsfragment som innehåller en HTML <``section``>
element som innehåller tre div:er:
.button-container
innehåller två knappar som kommer att användas för att starta och stoppa taligenkänning..words
harcontentEditable
ochsuppressContentEditableWarning
attribut för att göra detta element redigerbart och undertrycka alla varningar från React.- Annan
.button-container
innehåller ytterligare två knappar som kommer att användas för att återställa respektive konvertera tal till PDF.
Styling är en helt annan sak. Jag går inte in på det här, men du får gärna använda några stilar jag skrev antingen som utgångspunkt för din egen styles/global.css
fil.
Visa hela 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-variablerna där används för att styra bakgrundsfärgen på knapparna.
Låt oss se de senaste förändringarna! Springa npm run dev
i terminalen och kolla in dem.
Du bör se detta i webbläsaren när du besöker http://localhost:3000
:
Vårt första tal till textkonvertering!
Den första åtgärden att vidta är att importera nödvändiga beroenden till vårt <SpeechToText>
komponent:
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Sedan kontrollerar vi om taligenkänning stöds av webbläsaren och ger ett meddelande om det inte stöds:
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Nästa upp, låt oss extrahera transcript
och resetTranscript
från useSpeechRecognition()
krok:
const { transcript, resetTranscript } = useSpeechRecognition();
Detta är vad vi behöver för staten som hanterar listening
:
const [listening, setListening] = useState(false);
Vi behöver också en ref
för div
med contentEditable
attribut, då måste vi lägga till ref
tillskriva det och passera transcript
as children
:
const textBodyRef = useRef(null);
…och:
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
Det sista vi behöver här är en funktion som triggar taligenkänning och för att knyta den funktionen till onClick
händelseavlyssnare av vår knapp. Knappen ställer in lyssna på true
och gör att den körs kontinuerligt. Vi kommer att inaktivera knappen medan den är i det tillståndet för att förhindra oss från att avfyra ytterligare händelser.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
…och:
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Genom att klicka på knappen bör nu transkriptionen startas.
Fler funktioner
OK, så vi har en komponent som kan starta lyssnande. Men nu behöver vi det för att göra lite andra saker också, som stopListening
, resetText
och handleConversion
. Låt oss göra dessa funktioner.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Var och en av funktionerna kommer att läggas till i en onClick
händelseavlyssnare på lämpliga knappar:
<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>
Smakämnen handleConversion
funktionen är asynkron eftersom vi så småningom kommer att göra en API-förfrågan. "Stopp"-knappen har det inaktiverade attributet som skulle utlösas när lyssnandet är falskt.
Om vi startar om servern och uppdaterar webbläsaren kan vi nu starta, stoppa och återställa vår taltranskription i webbläsaren.
Nu behöver vi appen transkribera det igenkända talet genom att konvertera det till en PDF-fil. För det behöver vi sökvägen på serversidan från Express.js.
Konfigurera API-rutten
Syftet med denna väg är att ta en textfil, konvertera den till en PDF, skriva den PDF-filen till vårt filsystem och sedan skicka ett svar till klienten.
För att ställa in skulle vi öppna server/index.js
fil och importera html-pdf-node
och fs
beroenden som kommer att användas för att skriva och öppna vårt filsystem.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Därefter kommer vi att ställa in vår rutt:
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
Vi fortsätter sedan med att definiera vilka alternativ som krävs för att kunna använda html-pdf-node
inne på rutten:
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
Smakämnen options
objekt accepterar ett värde för att ställa in pappersstorlek och stil. Pappersstorlekar följer ett mycket annat system än de storleksenheter vi vanligtvis använder på webben. Till exempel, A4 är den typiska bokstavsstorleken.
Smakämnen file
objekt accepterar antingen webbadressen till en offentlig webbplats eller HTML-kod. För att skapa vår HTML-sida kommer vi att använda html
, body
, pre
HTML-taggar och texten från req.body
.
Du kan applicera valfri styling.
Därefter lägger vi till en trycatch
för att hantera eventuella fel som kan dyka upp längs vägen:
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Därefter använder vi generatePdf
från html-pdf-node
bibliotek för att generera en pdfBuffer
(den råa PDF-filen) från vår fil och skapa en unik pdfName
:
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Därifrån använder vi filsystemmodulen för att skriva, läsa och (ja, äntligen!) skicka ett svar till 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." });
});
});
Låt oss bryta ner det lite:
- Smakämnen
writeFile
filsystemmodulen accepterar ett filnamn, data och en återuppringningsfunktion som kan returnera ett felmeddelande om det finns problem med att skriva till filen. Om du arbetar med ett CDN som tillhandahåller felslutpunkter kan du använda dem istället. - Smakämnen
readFile
filsystemmodulen accepterar ett filnamn och en återuppringningsfunktion som kan eller returnerar ett läsfel samt läsdata. När vi inte har något läsfel och läsdata finns, kommer vi att konstruera och skicka ett svar till klienten. Återigen, detta kan ersättas med ditt CDN:s slutpunkter om du har dem. - Smakämnen
res.setHeader("Content-Type", "application/pdf");
talar om för webbläsaren att vi skickar en PDF-fil. - Smakämnen
res.setHeader("Content-Disposition", "attachment");
talar om för webbläsaren att göra mottagna data nedladdningsbara.
Eftersom API-rutten är klar kan vi använda den i vår app på http://localhost:4000
. Vi kan gå vidare till klientdelen av vår ansökan för att slutföra handleConversion
funktion.
Hantera konverteringen
Innan vi kan börja arbeta på en handleConversion
funktion måste vi skapa ett tillstånd som hanterar våra API-förfrågningar för laddning, fel, framgång och andra meddelanden. Vi kommer att använda React's useState
krok för att ställa in det:
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
I handleConversion
funktion kommer vi att kontrollera när webbsidan har laddats innan vi kör vår kod och se till att div
med editable
attributet är inte tomt:
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 genom att slå in vår eventuella API-förfrågan i en trycatch
, hantera eventuella fel som kan uppstå och uppdatera svarstillståndet:
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Därefter ställer vi in några värden för svarstillståndet och ställer även in config för axios
och gör en postbegäran till servern:
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ått ett framgångsrikt svar ställer vi in svarstillståndet med lämpliga värden och instruerar webbläsaren att ladda ner den mottagna PDF-filen:
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();
Och vi kan använda följande under contentEditable div
för att visa meddelanden:
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Slutlig kod
Jag har paketerat allt på GitHub så att du kan kolla in hela källkoden för både servern och klienten.