Con le interfacce vocali che stanno diventando più importanti, vale la pena esplorare alcune delle cose che possiamo fare con le interazioni vocali. Ad esempio, e se potessimo dire qualcosa e averlo trascritto e pompato come PDF scaricabile?
Bene, avviso spoiler: noi assolutamente può Fai quello! Ci sono librerie e framework che possiamo mettere insieme per realizzarlo, ed è quello che faremo insieme in questo articolo.
Questi sono gli strumenti che stiamo usando
Prima di tutto, questi sono i due grandi giocatori: Next.js ed Express.js.
Next.js punta su funzionalità aggiuntive per React, comprese le funzionalità chiave per la costruzione di siti statici. È un punto di riferimento per molti sviluppatori grazie a ciò che offre immediatamente, come routing dinamico, ottimizzazione delle immagini, routing integrato di dominio e sottodomini, aggiornamenti rapidi, routing del file system e percorsi API... tra tante, tante altre cose.
Nel nostro caso, abbiamo sicuramente bisogno di Next.js per il suo Percorsi API sul nostro server client. Vogliamo un percorso che prenda un file di testo, lo converta in PDF, lo scriva nel nostro filesystem, quindi invii una risposta al cliente.
Express.js ci consente di utilizzare una piccola app Node.js con routing, helper HTTP e modelli. È un server per la nostra API, che è ciò di cui avremo bisogno mentre passiamo e analizzeremo i dati tra le cose.
Abbiamo alcune altre dipendenze che useremo:
- reagire-riconoscimento vocale: Una libreria per convertire il parlato in testo, rendendolo disponibile ai componenti di React.
- runtime del rigeneratore: Una libreria per la risoluzione dei problemi di "
regeneratorRuntime
non è definito" errore visualizzato in Next.js quando si utilizza il riconoscimento vocale di reazione - nodo html-pdf: una libreria per convertire una pagina HTML o un URL pubblico in un PDF
- Axios: una libreria per effettuare richieste HTTP sia nel browser che in Node.js
- corna: una libreria che consente la condivisione di risorse tra le origini
Impostare
La prima cosa che vogliamo fare è creare due cartelle di progetto, una per il client e una per il server. Dai loro un nome come preferisci. Sto nominando il mio audio-to-pdf-client
ed audio-to-pdf-server
, Rispettivamente.
Il modo più veloce per iniziare con Next.js sul lato client è avviarlo con crea-successiva-app. Quindi, apri il tuo terminale ed esegui il seguente comando dalla cartella del progetto client:
npx create-next-app client
Ora abbiamo bisogno del nostro server Express. Possiamo farcela cd
-ing nella cartella del progetto del server ed eseguendo il file npm init
comando. UN package.json
il file verrà creato nella cartella del progetto del server una volta terminato.
Dobbiamo ancora installare effettivamente Express, quindi facciamolo ora con npm install express
. Ora possiamo crearne uno nuovo index.js
file nella cartella del progetto del server e rilascia questo codice lì:
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Pronto per eseguire il server?
node index.js
Avremo bisogno di un altro paio di cartelle e di un altro file per andare avanti:
- Creare un
components
cartella nella cartella del progetto client. - Creare un
SpeechToText.jsx
file nellacomponents
sottocartella.
Prima di andare oltre, abbiamo un po' di pulizia da fare. In particolare, dobbiamo sostituire il codice predefinito nel file pages/index.js
file con questo:
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="/it/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
L'importato SpeechToText
il componente verrà eventualmente esportato da components/SpeechToText.jsx
.
Installiamo le altre dipendenze
Bene, abbiamo la configurazione iniziale per la nostra app fuori mano. Ora possiamo installare le librerie che gestiscono i dati che vengono passati.
Possiamo installare le nostre dipendenze client con:
npm install react-speech-recognition regenerator-runtime axios
Le nostre dipendenze del server Express sono le prossime, quindi andiamo cd
nella cartella del progetto del server e installa quelli:
npm install html-pdf-node cors
Probabilmente un buon momento per fare una pausa e assicurarsi che i file nelle nostre cartelle di progetto siano intatti. Ecco cosa dovresti avere nella cartella del progetto client a questo punto:
/audio-to-pdf-web-client
├─ /components
| └── SpeechToText.jsx
├─ /pages
| ├─ _app.js
| └── index.js
└── /styles
├─globals.css
└── Home.module.css
Ed ecco cosa dovresti avere nella cartella del progetto del server:
/audio-to-pdf-server
└── index.js
Costruire l'interfaccia utente
Bene, il nostro discorso in PDF non sarebbe eccezionale se non ci fosse modo di interagire con esso, quindi creiamo un componente React per esso che possiamo chiamare <SpeechToText>
.
Puoi usare totalmente il tuo markup. Ecco cosa ho per darti un'idea dei pezzi che stiamo mettendo insieme:
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;
Questo componente restituisce a Frammento di reazione che contiene un HTML <``section``>
elemento che contiene tre div:
.button-container
contiene due pulsanti che verranno utilizzati per avviare e interrompere il riconoscimento vocale..words
hacontentEditable
edsuppressContentEditableWarning
attributi per rendere questo elemento modificabile e sopprimere eventuali avvisi da React.- Un altro
.button-container
contiene altri due pulsanti che verranno utilizzati rispettivamente per ripristinare e convertire la voce in PDF.
Lo stile è un'altra cosa. Non ne parlerò qui, ma puoi usare alcuni stili che ho scritto come punto di partenza per il tuo styles/global.css
file.
Visualizza CSS completo
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;
}
Le variabili CSS in esso contenute vengono utilizzate per controllare il colore di sfondo dei pulsanti.
Vediamo le ultime modifiche! Correre npm run dev
nel terminale e controllali.
Dovresti vederlo nel browser quando visiti http://localhost:3000
:
La nostra prima conversione da discorso a testo!
La prima azione da intraprendere è importare le dipendenze necessarie nel ns <SpeechToText>
componente:
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Quindi controlliamo se il riconoscimento vocale è supportato dal browser e visualizziamo un avviso se non supportato:
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Successivamente, estraiamo transcript
ed resetTranscript
dal useSpeechRecognition()
gancio:
const { transcript, resetTranscript } = useSpeechRecognition();
Questo è ciò di cui abbiamo bisogno per lo stato che gestisce listening
:
const [listening, setListening] = useState(false);
Abbiamo anche bisogno di un ref
per l' div
con la contentEditable
attributo, quindi dobbiamo aggiungere l' ref
attribuire ad esso e passare transcript
as children
:
const textBodyRef = useRef(null);
…e:
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
L'ultima cosa di cui abbiamo bisogno qui è una funzione che attivi il riconoscimento vocale e colleghi quella funzione al onClick
ascoltatore di eventi del nostro pulsante. Il pulsante imposta l'ascolto true
e lo fa funzionare continuamente. Disattiveremo il pulsante mentre è in quello stato per impedirci di attivare ulteriori eventi.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
…e:
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Cliccando sul pulsante dovrebbe ora avviare la trascrizione.
Altre funzioni
OK, quindi abbiamo un componente che può inizia a ascoltando. Ma ora abbiamo bisogno che faccia anche alcune altre cose, come stopListening
, resetText
ed handleConversion
. Facciamo quelle funzioni.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Ciascuna delle funzioni verrà aggiunta a un onClick
listener di eventi sui pulsanti appropriati:
<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>
Il handleConversion
la funzione è asincrona perché alla fine faremo una richiesta API. Il pulsante "Stop" ha l'attributo disabilitato che verrebbe attivato quando l'ascolto è falso.
Se riavviamo il server e aggiorniamo il browser, ora possiamo avviare, interrompere e ripristinare la nostra trascrizione vocale nel browser.
Ora ciò di cui abbiamo bisogno è che l'app lo faccia trascrivere quel parlato riconosciuto convertendolo in un file PDF. Per questo, abbiamo bisogno del percorso lato server da Express.js.
Configurazione del percorso API
Lo scopo di questo percorso è prendere un file di testo, convertirlo in PDF, scrivere quel PDF nel nostro filesystem, quindi inviare una risposta al cliente.
Per configurare, vorremmo aprire il server/index.js
archiviare e importare il html-pdf-node
ed fs
dipendenze che verranno utilizzate per scrivere e aprire il nostro filesystem.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Successivamente, imposteremo il nostro percorso:
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
Procediamo quindi a definire le nostre opzioni necessarie per l'utilizzo html-pdf-node
all'interno del percorso:
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
Il options
oggetto accetta un valore per impostare il formato e lo stile della carta. I formati carta seguono un sistema molto diverso rispetto alle unità di dimensionamento che utilizziamo tipicamente sul web. Per esempio, A4 è il tipico formato lettera.
Il file
oggetto accetta l'URL di un sito Web pubblico o il markup HTML. Per generare la nostra pagina HTML, utilizzeremo il file html
, body
, pre
Tag HTML e il testo da req.body
.
Puoi applicare qualsiasi stile di tua scelta.
Successivamente, aggiungeremo a trycatch
per gestire eventuali errori che potrebbero apparire lungo il percorso:
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Successivamente, useremo il generatePdf
dal html-pdf-node
libreria per generare a pdfBuffer
(il file PDF grezzo) dal nostro file e creare un file unico pdfName
:
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Da lì, utilizziamo il modulo del filesystem per scrivere, leggere e (sì, finalmente!) inviare una risposta all'app client:
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." });
});
});
Analizziamolo un po':
- Il
writeFile
il modulo filesystem accetta un nome file, dati e una funzione di callback che può restituire un messaggio di errore se si verifica un problema durante la scrittura del file. Se stai lavorando con una CDN che fornisce endpoint di errore, puoi invece usarli. - Il
readFile
il modulo filesystem accetta un nome file e una funzione di callback che è in grado o restituisce un errore di lettura così come i dati letti. Una volta che non abbiamo errori di lettura e i dati letti sono presenti, costruiremo e invieremo una risposta al client. Ancora una volta, questo può essere sostituito con gli endpoint della tua CDN se li hai. - Il
res.setHeader("Content-Type", "application/pdf");
indica al browser che stiamo inviando un file PDF. - Il
res.setHeader("Content-Disposition", "attachment");
indica al browser di rendere scaricabili i dati ricevuti.
Poiché il percorso API è pronto, possiamo usarlo nella nostra app all'indirizzo http://localhost:4000
. Possiamo procedere alla parte client della nostra applicazione per completare il handleConversion
funzione.
Gestire la conversione
Prima di poter iniziare a lavorare su a handleConversion
funzione, dobbiamo creare uno stato che gestisca le nostre richieste API per il caricamento, l'errore, il successo e altri messaggi. Useremo React's useState
hook per configurarlo:
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
Nel handleConversion
funzione, verificheremo quando la pagina web è stata caricata prima di eseguire il nostro codice e ci assicureremo che il div
con la editable
l'attributo non è vuoto:
if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
// console.log(textBodyRef.current.innerText);
if (!userText) {
alert("Please speak or write some text.");
return;
}
}
Procediamo avvolgendo la nostra eventuale richiesta API in a trycatch
, gestendo eventuali errori che potrebbero verificarsi e aggiornando lo stato di risposta:
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Successivamente, impostiamo alcuni valori per lo stato di risposta e impostiamo anche config per axios
e fai una richiesta di posta al 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
);
Una volta ottenuta una risposta positiva, impostiamo lo stato della risposta con i valori appropriati e istruiamo il browser a scaricare il PDF ricevuto:
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();
E possiamo usare quanto segue sotto il contentEditable div
per la visualizzazione dei messaggi:
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Codice finale
Ho impacchettato tutto su GitHub in modo da poter controllare il codice sorgente completo sia per il server che per il client.