Ponieważ interfejsy głosowe stają się coraz bardziej popularne, warto zbadać niektóre rzeczy, które możemy zrobić z interakcjami mowy. Na przykład, co by było, gdybyśmy mogli coś powiedzieć i przepisać to jako plik PDF do pobrania?
Cóż, uwaga spoilera: absolutnie mogą Zrób to! Istnieją biblioteki i frameworki, które możemy skleić, aby tak się stało, i to właśnie zamierzamy zrobić razem w tym artykule.
To są narzędzia, których używamy
Po pierwsze, są to dwaj wielcy gracze: Next.js i Express.js.
Next.js dodaje dodatkowe funkcje do Reacta, w tym kluczowe funkcje do budowania statycznych witryn. Jest to cel dla wielu programistów ze względu na to, co oferuje zaraz po wyjęciu z pudełka, takie jak routing dynamiczny, optymalizacja obrazu, wbudowany routing domen i subdomen, szybkie odświeżanie, routing systemu plików i trasy API… między innymi wiele, wiele innych rzeczy.
W naszym przypadku zdecydowanie potrzebujemy do tego Next.js Trasy API na naszym serwerze klienta. Chcemy trasy, która pobiera plik tekstowy, konwertuje go do formatu PDF, zapisuje go w naszym systemie plików, a następnie wysyła odpowiedź do klienta.
Express.js. pozwala nam uzyskać małą aplikację Node.js obsługującą routing, pomocniki HTTP i tworzenie szablonów. Jest to serwer dla naszego własnego API, którego będziemy potrzebować, gdy będziemy przekazywać i analizować dane między rzeczami.
Mamy kilka innych zależności, które wykorzystamy:
- Reaguj-rozpoznawanie-mowy: Biblioteka do konwersji mowy na tekst, udostępniająca ją komponentom React.
- czas pracy regeneratora: Biblioteka do rozwiązywania problemów „
regeneratorRuntime
nie jest zdefiniowany” błąd, który pojawia się w Next.js podczas używania rozpoznawania mowy - węzeł html-pdf: Biblioteka do konwersji strony HTML lub publicznego adresu URL na plik PDF
- aksjos: Biblioteka do tworzenia żądań HTTP zarówno w przeglądarce, jak i Node.js
- cors: Biblioteka, która umożliwia współdzielenie zasobów między źródłami
Konfigurowanie
Pierwszą rzeczą, którą chcemy zrobić, to utworzyć dwa foldery projektu, jeden dla klienta i jeden dla serwera. Nazwij je jak chcesz. Nazywam moje audio-to-pdf-client
i audio-to-pdf-server
, Odpowiednio.
Najszybszym sposobem na rozpoczęcie pracy z Next.js po stronie klienta jest uruchomienie go za pomocą utwórz-następną-aplikację. Otwórz więc terminal i uruchom następujące polecenie z folderu projektu klienta:
npx create-next-app client
Teraz potrzebujemy naszego serwera Express. Możemy to zdobyć przez cd
- wpisanie do folderu projektu na serwerze i uruchomienie npm init
Komenda. A package.json
plik zostanie utworzony w folderze projektu serwera po zakończeniu.
Nadal musimy zainstalować Express, więc zróbmy to teraz za pomocą npm install express
. Teraz możemy stworzyć nowy index.js
plik w folderze projektu serwera i upuść tam ten kod:
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Gotowy do uruchomienia serwera?
node index.js
Będziemy potrzebować jeszcze kilku folderów i kolejnego pliku, aby przejść dalej:
- Stwórz
components
folder w folderze projektu klienta. - Stwórz
SpeechToText.jsx
Plik wcomponents
podfolder.
Zanim przejdziemy dalej, musimy trochę posprzątać. W szczególności musimy zastąpić domyślny kod w pages/index.js
plik z tym:
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="/pl/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
Importowane SpeechToText
komponent zostanie ostatecznie wyeksportowany z components/SpeechToText.jsx
.
Zainstalujmy pozostałe zależności
W porządku, mamy już początkową konfigurację naszej aplikacji. Teraz możemy zainstalować biblioteki, które obsługują przekazywane dane.
Zależności naszych klientów możemy zainstalować za pomocą:
npm install react-speech-recognition regenerator-runtime axios
Nasze zależności serwera Express są następne, więc zacznijmy cd
do folderu projektu serwera i zainstaluj te:
npm install html-pdf-node cors
To chyba dobry moment na przerwę i upewnienie się, że pliki w naszych folderach projektów są nienaruszone. Oto, co powinieneś mieć w tym momencie w folderze projektu klienta:
/audio-to-pdf-web-client
├─ /components
| └── SpeechToText.jsx
├─ /pages
| ├─ _app.js
| └── index.js
└── /styles
├─globals.css
└── Home.module.css
A oto, co powinieneś mieć w folderze projektu serwera:
/audio-to-pdf-server
└── index.js
Budowanie interfejsu użytkownika
Cóż, nasza zamiana mowy na plik PDF nie byłaby tak świetna, gdyby nie było możliwości interakcji z nią, więc stwórzmy dla niej komponent React, który możemy wywołać <SpeechToText>
.
Możesz całkowicie użyć własnego znacznika. Oto, co muszę dać ci wyobrażenie o elementach, które składamy razem:
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;
Ten składnik zwraca a Fragment reakcji który zawiera HTML <``section``>
element zawierający trzy divy:
.button-container
zawiera dwa przyciski, które będą używane do uruchamiania i zatrzymywania rozpoznawania mowy..words
macontentEditable
isuppressContentEditableWarning
atrybuty, aby uczynić ten element edytowalnym i wyłączyć wszelkie ostrzeżenia z Reacta.- Inne
.button-container
posiada dwa dodatkowe przyciski, które będą używane odpowiednio do resetowania i konwertowania mowy do formatu PDF.
Stylizacja to zupełnie inna sprawa. Nie będę się w to tutaj zagłębiał, ale możesz użyć niektórych stylów, które napisałem, jako punkt wyjścia dla własnych styles/global.css
plik.
Zobacz pełną wersję 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;
}
Znajdujące się tam zmienne CSS są używane do kontrolowania koloru tła przycisków.
Zobaczmy najnowsze zmiany! Biegać npm run dev
w terminalu i sprawdź je.
Powinieneś to zobaczyć w przeglądarce podczas wizyty http://localhost:3000
:
Nasza pierwsza konwersja mowy na tekst!
Pierwszą czynnością, jaką należy podjąć, jest zaimportowanie niezbędnych zależności do naszego <SpeechToText>
składnik:
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Następnie sprawdzamy, czy rozpoznawanie mowy jest obsługiwane przez przeglądarkę i wyświetlamy powiadomienie, jeśli nie jest obsługiwane:
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Następnie wydobądźmy transcript
i resetTranscript
z useSpeechRecognition()
hak:
const { transcript, resetTranscript } = useSpeechRecognition();
To jest to, czego potrzebujemy dla państwa, które obsługuje listening
:
const [listening, setListening] = useState(false);
Potrzebujemy również ref
dla div
z contentEditable
atrybut, to musimy dodać ref
przypisz do niego i przekaż transcript
as children
:
const textBodyRef = useRef(null);
…oraz:
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
Ostatnią rzeczą, jakiej potrzebujemy, jest funkcja, która uruchamia rozpoznawanie mowy i wiąże tę funkcję z onClick
Odbiornik zdarzeń naszego przycisku. Przycisk ustawia słuchanie true
i sprawia, że działa w sposób ciągły. Wyłączymy przycisk, gdy jest w tym stanie, aby uniemożliwić nam uruchamianie dodatkowych zdarzeń.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
…oraz:
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Kliknięcie przycisku powinno teraz uruchomić transkrypcję.
Więcej funkcji
OK, więc mamy komponent, który może początek słuchający. Ale teraz potrzebujemy go również do zrobienia kilku innych rzeczy, takich jak stopListening
, resetText
i handleConversion
. Zróbmy te funkcje.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Każda z funkcji zostanie dodana do jednego onClick
detektor zdarzeń na odpowiednich przyciskach:
<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>
Połączenia handleConversion
funkcja jest asynchroniczna, ponieważ w końcu będziemy wysyłać żądanie API. Przycisk „Stop” ma wyłączony atrybut, który zostanie uruchomiony, gdy nasłuch jest fałszywy.
Jeśli ponownie uruchomimy serwer i odświeżymy przeglądarkę, możemy teraz uruchomić, zatrzymać i zresetować transkrypcję mowy w przeglądarce.
Teraz potrzebujemy, aby aplikacja rozpisać rozpoznał mowę, konwertując ją na plik PDF. W tym celu potrzebujemy ścieżki po stronie serwera z Express.js.
Konfiguracja trasy API
Celem tej trasy jest pobranie pliku tekstowego, przekonwertowanie go na plik PDF, zapisanie tego pliku PDF w naszym systemie plików, a następnie wysłanie odpowiedzi do klienta.
Aby skonfigurować, otworzymy server/index.js
plik i zaimportuj html-pdf-node
i fs
zależności, które będą używane do pisania i otwierania naszego systemu plików.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Następnie ustawimy naszą trasę:
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
Następnie przystępujemy do zdefiniowania naszych opcji wymaganych do użycia html-pdf-node
wewnątrz trasy:
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
Połączenia options
obiekt przyjmuje wartość określającą rozmiar i styl papieru. Rozmiary papieru mają znacznie inny system niż jednostki rozmiaru, których zwykle używamy w Internecie. Na przykład, A4 to typowy rozmiar liter.
Połączenia file
obiekt akceptuje adres URL publicznej witryny internetowej lub znacznik HTML. W celu wygenerowania naszej strony HTML użyjemy html
, body
, pre
Tagi HTML i tekst z req.body
.
Możesz zastosować dowolną stylizację.
Następnie dodamy trycatch
do obsługi wszelkich błędów, które mogą pojawić się po drodze:
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Następnie użyjemy generatePdf
z html-pdf-node
biblioteka do generowania pdfBuffer
(surowy plik PDF) z naszego pliku i stwórz unikalny pdfName
:
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Stamtąd używamy modułu systemu plików do pisania, czytania i (tak, w końcu!) wysyłania odpowiedzi do aplikacji klienckiej:
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." });
});
});
Rozłóżmy to trochę:
- Połączenia
writeFile
Moduł systemu plików akceptuje nazwę pliku, dane i funkcję zwrotną, która może zwrócić komunikat o błędzie, jeśli wystąpi problem z zapisem do pliku. Jeśli pracujesz z siecią CDN, która udostępnia punkty końcowe błędów, możesz ich użyć. - Połączenia
readFile
Moduł systemu plików akceptuje nazwę pliku i funkcję zwrotną, która jest zdolna lub zwraca błąd odczytu, jak również odczytane dane. Gdy nie wystąpi błąd odczytu, a odczytane dane są obecne, skonstruujemy i wyślemy odpowiedź do klienta. Ponownie, można to zastąpić punktami końcowymi CDN, jeśli je masz. - Połączenia
res.setHeader("Content-Type", "application/pdf");
informuje przeglądarkę, że wysyłamy plik PDF. - Połączenia
res.setHeader("Content-Disposition", "attachment");
nakazuje przeglądarce, aby odebrane dane można było pobrać.
Ponieważ trasa API jest gotowa, możemy jej użyć w naszej aplikacji pod adresem http://localhost:4000
. Możemy przejść do części klienckiej naszej aplikacji, aby zakończyć handleConversion
funkcja.
Obsługa konwersji
Zanim zaczniemy pracować nad handleConversion
funkcji, musimy utworzyć stan, który obsługuje nasze żądania API dotyczące ładowania, błędu, sukcesu i innych komunikatów. Będziemy korzystać z React useState
zaczep, aby to ustawić:
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
W handleConversion
funkcja, sprawdzimy, kiedy strona internetowa została załadowana przed uruchomieniem naszego kodu i upewnimy się, że div
z editable
atrybut nie jest pusty:
if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
// console.log(textBodyRef.current.innerText);
if (!userText) {
alert("Please speak or write some text.");
return;
}
}
Kontynuujemy, pakując nasze ewentualne żądanie API w a trycatch
, obsługa wszelkich błędów, które mogą wystąpić, i aktualizowanie stanu odpowiedzi:
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Następnie ustawiamy kilka wartości dla stanu odpowiedzi, a także ustawiamy konfigurację dla axios
i wyślij zapytanie do serwera:
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
);
Gdy otrzymamy pomyślną odpowiedź, ustawiamy stan odpowiedzi z odpowiednimi wartościami i instruujemy przeglądarkę, aby pobrała otrzymany plik 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();
I możemy użyć następującego poniżej contentEditable div
do wyświetlania wiadomości:
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Kod końcowy
Wszystko spakowałem na GitHub, więc możesz sprawdzić pełny kod źródłowy zarówno dla serwera, jak i klienta.