Da Sprachschnittstellen immer wichtiger werden, lohnt es sich, einige der Dinge zu erkunden, die wir mit Sprachinteraktionen tun können. Was wäre zum Beispiel, wenn wir etwas sagen könnten und das dann transkribiert und als herunterladbares PDF ausgegeben würde?
Nun, Spoiler-Alarm: Das können wir absolut! Es gibt Bibliotheken und Frameworks, die wir zusammenfügen können, um es zu realisieren, und genau das werden wir in diesem Artikel gemeinsam tun.
Das sind die Tools, die wir verwenden
Zuerst einmal sind dies die beiden Hauptakteure: Next.js und Express.js.
Next.js fügt React zusätzliche Funktionalitäten hinzu, einschließlich wichtiger Funktionen für die Erstellung statischer Websites. Es ist für viele Entwickler aufgrund dessen, was es sofort bietet, eine erste Wahl, wie dynamisches Routing, Bildoptimierung, integriertes Domain- und Subdomain-Routing, schnelle Aktualisierungen, Dateisystem-Routing und API-Routen... unter vielen, vielen anderen Dingen.
In unserem Fall benötigen wir unbedingt Next.js für seine API-Routen auf unserem Client-Server. Wir möchten eine Route, die eine Textdatei nimmt, sie in ein PDF konvertiert, sie in unser Dateisystem schreibt und dann eine Antwort an den Client sendet.
Express.js ermöglicht es uns, eine kleine Node.js-App mit Routing, HTTP-Helfern und Templating zum Laufen zu bringen. Es ist ein Server für unsere eigene API, was wir brauchen werden, wenn wir Daten zwischen verschiedenen Dingen übergeben und parsen.
Wir werden noch einige andere Abhängigkeiten nutzen
- react-speech-recognition: Eine Bibliothek zur Umwandlung von Sprache in Text, die sie für React-Komponenten verfügbar macht.
- regenerator-runtime: Eine Bibliothek zur Fehlerbehebung des Fehlers „
regeneratorRuntimeis not defined“, der in Next.js beim Verwenden von react-speech-recognition auftritt. - html-pdf-node: Eine Bibliothek zur Konvertierung einer HTML-Seite oder einer öffentlichen URL in ein PDF.
- axios: Eine Bibliothek zur Durchführung von HTTP-Anfragen sowohl im Browser als auch in Node.js.
- cors: Eine Bibliothek, die Cross-Origin Resource Sharing (ressourcenübergreifende Freigabe) ermöglicht.
Einrichtung
Als Erstes möchten wir zwei Projektordner erstellen, einen für den Client und einen für den Server. Nennen Sie sie, wie Sie möchten. Ich nenne meine audio-to-pdf-client und audio-to-pdf-server.
Der schnellste Weg, mit Next.js auf der Client-Seite zu beginnen, ist die Verwendung von create-next-app. Öffnen Sie also Ihr Terminal und führen Sie den folgenden Befehl von Ihrem Client-Projektordner aus aus.
npx create-next-app client
Jetzt brauchen wir unseren Express-Server. Diesen können wir erhalten, indem wir in den Server-Projektordner wechseln (cd) und den Befehl npm init ausführen. Eine package.json-Datei wird im Server-Projektordner erstellt, sobald dies abgeschlossen ist.
Wir müssen Express noch tatsächlich installieren, also tun wir das jetzt mit npm install express. Jetzt können wir eine neue Datei namens index.js im Server-Projektordner erstellen und diesen Code dort einfügen.
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Bereit, den Server zu starten?
node index.js
Wir benötigen noch ein paar weitere Ordner und eine weitere Datei, um fortzufahren.
- Erstellen Sie einen
components-Ordner im Client-Projektordner. - Erstellen Sie eine Datei namens
SpeechToText.jsxim Unterordnercomponents.
Bevor wir weitergehen, müssen wir ein wenig aufräumen. Konkret müssen wir den Standardcode in der Datei pages/index.js durch diesen ersetzen.
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="/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
Die importierte SpeechToText-Komponente wird später aus components/SpeechToText.jsx exportiert.
Lassen Sie uns die anderen Abhängigkeiten installieren
Okay, die Ersteinrichtung für unsere App ist erledigt. Jetzt können wir die Bibliotheken installieren, die die übertragenen Daten verarbeiten.
Wir können unsere Client-Abhängigkeiten mit folgendem Befehl installieren:
npm install react-speech-recognition regenerator-runtime axios
Unsere Express-Server-Abhängigkeiten sind als Nächstes dran. Wechseln wir also in den Server-Projektordner (cd) und installieren wir diese.
npm install html-pdf-node cors
Es ist wahrscheinlich ein guter Zeitpunkt, um kurz innezuhalten und sicherzustellen, dass die Dateien in unseren Projektordnern intakt sind. Hier ist, was Sie zu diesem Zeitpunkt im Client-Projektordner haben sollten:
/audio-to-pdf-web-client
├─ /components
| └── SpeechToText.jsx
├─ /pages
| ├─ _app.js
| └── index.js
└── /styles
├─globals.css
└── Home.module.css
Und hier ist, was Sie im Server-Projektordner haben sollten:
/audio-to-pdf-server
└── index.js
Aufbau der Benutzeroberfläche
Nun, unsere Sprach-zu-PDF-Funktion wäre nicht besonders toll, wenn es keine Möglichkeit gäbe, mit ihr zu interagieren. Machen wir also eine React-Komponente dafür, die wir <SpeechToText> nennen können.
Sie können Ihre eigene Markup-Struktur verwenden. Hier ist, was ich habe, um Ihnen eine Vorstellung von den Teilen zu geben, die wir zusammenfügen:
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;
Diese Komponente gibt ein React Fragment zurück, das ein HTML-<``section``>-Element enthält, das wiederum drei divs enthält.
.button-containerenthält zwei Buttons, die zum Starten und Stoppen der Spracherkennung verwendet werden..wordshat die AttributecontentEditableundsuppressContentEditableWarning, um dieses Element bearbeitbar zu machen und Warnungen von React zu unterdrücken.- Ein weiterer
.button-containerenthält zwei weitere Buttons, die zum Zurücksetzen bzw. zur Konvertierung von Sprache in PDF verwendet werden.
Styling ist eine andere Sache. Ich werde hier nicht darauf eingehen, aber Sie können gerne einige von mir geschriebene Styles verwenden, entweder als Ausgangspunkt für Ihre eigene styles/global.css-Datei.
Vollständiges CSS anzeigen
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;
}
Die darin enthaltenen CSS-Variablen werden verwendet, um die Hintergrundfarbe der Buttons zu steuern.
Sehen wir uns die neuesten Änderungen an! Führen Sie npm run dev im Terminal aus und schauen Sie sie sich an.
Sie sollten dies im Browser sehen, wenn Sie https://:3000 besuchen.

Unsere erste Sprach-zu-Text-Konvertierung!
Die erste Aktion besteht darin, die notwendigen Abhängigkeiten in unsere <SpeechToText>-Komponente zu importieren.
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Dann prüfen wir, ob Spracherkennung vom Browser unterstützt wird, und zeigen eine Benachrichtigung an, falls sie nicht unterstützt wird.
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Als Nächstes extrahieren wir transcript und resetTranscript aus dem useSpeechRecognition()-Hook.
const { transcript, resetTranscript } = useSpeechRecognition();
Das benötigen wir für den Zustand, der listening verwaltet.
const [listening, setListening] = useState(false);
Wir benötigen auch einen ref für den div mit dem contentEditable-Attribut, dann müssen wir das ref-Attribut hinzufügen und transcript als children übergeben.
const textBodyRef = useRef(null);
...und
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
Das Letzte, was wir hier brauchen, ist eine Funktion, die die Spracherkennung auslöst, und um diese Funktion an den onClick-Event-Listener unseres Buttons zu binden. Der Button setzt listening auf true und lässt ihn kontinuierlich laufen. Wir deaktivieren den Button, solange er sich in diesem Zustand befindet, um zu verhindern, dass zusätzliche Ereignisse ausgelöst werden.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
...und
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Ein Klick auf den Button sollte nun die Transkription starten.
Weitere Funktionen
Okay, wir haben also eine Komponente, die *starten* kann. Aber jetzt muss sie auch ein paar andere Dinge tun, wie stopListening, resetText und handleConversion. Machen wir diese Funktionen.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Jede der Funktionen wird an einen onClick-Event-Listener auf den entsprechenden Buttons angehängt.
<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>
Die Funktion handleConversion ist asynchron, da wir letztendlich eine API-Anfrage stellen werden. Der "Stop"-Button hat das deaktivierte Attribut, das ausgelöst würde, wenn listening false ist.
Wenn wir den Server neu starten und den Browser aktualisieren, können wir nun unsere Sprachtranskription im Browser starten, stoppen und zurücksetzen.
Was wir jetzt brauchen, ist, dass die App die erkannte Sprache *transkribiert*, indem sie sie in eine PDF-Datei umwandelt. Dazu benötigen wir den serverseitigen Pfad von Express.js.
Einrichtung der API-Route
Der Zweck dieser Route ist es, eine Textdatei zu nehmen, sie in ein PDF zu konvertieren, dieses PDF in unser Dateisystem zu schreiben und dann eine Antwort an den Client zu senden.
Zur Einrichtung öffnen wir die Datei server/index.js und importieren die Abhängigkeiten html-pdf-node und fs, die zum Schreiben und Öffnen unseres Dateisystems verwendet werden.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Als Nächstes richten wir unsere Route ein.
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
Anschließend definieren wir unsere Optionen, die erforderlich sind, um html-pdf-node innerhalb der Route zu verwenden.
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
Das options-Objekt akzeptiert einen Wert zur Festlegung der Papiergröße und des Stils. Papiergrößen folgen einem ganz anderen System als die Größenangaben, die wir normalerweise im Web verwenden. Zum Beispiel ist A4 das typische Briefpapierformat.
Das file-Objekt akzeptiert entweder die URL einer öffentlichen Website oder HTML-Markup. Um unsere HTML-Seite zu generieren, verwenden wir die HTML-Tags html, body, pre und den Text aus req.body.
Sie können hier beliebige Styles anwenden.
Als Nächstes fügen wir ein trycatch hinzu, um Fehler zu behandeln, die unterwegs auftreten könnten.
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Dann verwenden wir generatePdf aus der html-pdf-node-Bibliothek, um einen pdfBuffer (die rohe PDF-Datei) aus unserer Datei zu generieren und einen eindeutigen pdfName zu erstellen.
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Von dort aus verwenden wir das Filesystem-Modul zum Schreiben, Lesen und (ja, endlich!) Senden einer Antwort an die Client-App.
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." });
});
});
Lassen Sie uns das etwas aufschlüsseln.
- Das
writeFile-Filesystem-Modul akzeptiert einen Dateinamen, Daten und eine Callback-Funktion, die eine Fehlermeldung zurückgeben kann, wenn beim Schreiben der Datei ein Problem auftritt. Wenn Sie mit einem CDN arbeiten, das Fehler-Endpunkte bereitstellt, könnten Sie diese stattdessen verwenden. - Das
readFile-Filesystem-Modul akzeptiert einen Dateinamen und eine Callback-Funktion, die in der Lage ist, einen Lesefehler sowie die gelesenen Daten zurückzugeben. Sobald wir keinen Lesefehler haben und die gelesenen Daten vorhanden sind, erstellen und senden wir eine Antwort an den Client. Auch hier kann dies durch die Endpunkte Ihres CDNs ersetzt werden, falls Sie diese haben. res.setHeader("Content-Type", "application/pdf");teilt dem Browser mit, dass wir eine PDF-Datei senden.res.setHeader("Content-Disposition", "attachment");weist den Browser an, die empfangenen Daten herunterladbar zu machen.
Da die API-Route bereit ist, können wir sie in unserer App unter https://:4000 verwenden. Wir können dann mit dem Client-Teil unserer Anwendung fortfahren, um die Funktion handleConversion abzuschließen.
Verarbeitung der Konvertierung
Bevor wir mit der Arbeit an einer Funktion handleConversion beginnen können, müssen wir einen Zustand erstellen, der unsere API-Anfragen für Laden, Fehler, Erfolg und andere Meldungen verwaltet. Wir werden den useState-Hook von React verwenden, um dies einzurichten.
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
In der Funktion handleConversion prüfen wir, wann die Webseite geladen wurde, bevor wir unseren Code ausführen, und stellen sicher, dass das div mit dem editable-Attribut nicht leer ist.
if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
// console.log(textBodyRef.current.innerText);
if (!userText) {
alert("Please speak or write some text.");
return;
}
}
Wir fahren fort, indem wir unsere zukünftige API-Anfrage in einen trycatch einpacken, alle Fehler behandeln, die auftreten könnten, und den Antwortstatus aktualisieren.
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Als Nächstes legen wir einige Werte für den Antwortstatus fest und konfigurieren axios und führen eine POST-Anfrage an den Server aus.
setResponse({
...response,
loading: true,
message: "",
error: false,
success: false,
});
const config = {
headers: {
"Content-Type": "application/json",
},
responseType: "blob",
};
const res = await axios.post(
"https://:4000",
{
text: textBodyRef.current.innerText,
},
config
);
Sobald wir eine erfolgreiche Antwort erhalten haben, setzen wir den Antwortstatus mit den entsprechenden Werten und weisen den Browser an, das empfangene PDF herunterzuladen.
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();
Und wir können das Folgende unterhalb des contentEditable-div zur Anzeige von Nachrichten verwenden.
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Endgültiger Code
Ich habe alles auf GitHub verpackt, damit Sie den vollständigen Quellcode sowohl für den Server als auch für den Client einsehen können.
Gibt es eine Live-Demo-Website?
Das ist großartig
Schönes Tutorial, Kumpel.
Teilen Sie weiter
Warum nicht auch Nextjs als Backend verwenden? Der gesamte Express-Teil erscheint mir redundant.
Großartig!!!