Поскольку речевые интерфейсы становятся все более популярными, стоит изучить некоторые вещи, которые мы можем делать с помощью речевых взаимодействий. Например, что, если бы мы могли сказать что-то, расшифровать это и выгрузить в виде загружаемого PDF-файла?
Ну и спойлер: мы абсолютно может сделай это! Есть библиотеки и фреймворки, которые мы можем собрать вместе, чтобы это произошло, и это то, что мы собираемся сделать вместе в этой статье.
Это инструменты, которые мы используем
Во-первых, это два крупных игрока: Next.js и Express.js.
Next.js добавляет дополнительные функции к React, включая ключевые функции для создания статических сайтов. Многие разработчики выбирают его из-за того, что он предлагает прямо из коробки, например, динамическую маршрутизацию, оптимизацию изображений, встроенную маршрутизацию домена и поддомена, быстрое обновление, маршрутизацию файловой системы и маршруты API… много, много других вещей.
В нашем случае нам обязательно нужен Next.js для его API-маршруты на нашем клиентском сервере. Нам нужен маршрут, который берет текстовый файл, преобразует его в PDF, записывает в нашу файловую систему, а затем отправляет ответ клиенту.
Express.js позволяет нам запустить небольшое приложение Node.js с маршрутизацией, помощниками HTTP и шаблонами. Это сервер для нашего собственного API, который нам понадобится при передаче и анализе данных между вещами.
У нас есть некоторые другие зависимости, которые мы будем использовать:
- реагировать-распознавание речи: библиотека для преобразования речи в текст, что делает ее доступной для компонентов React.
- регенератор-среда выполнения: библиотека для устранения неполадок «
regeneratorRuntime
не определено», которая появляется в Next.js при использовании распознавания речи - html-pdf-узел: библиотека для преобразования HTML-страницы или общедоступного URL-адреса в PDF-файл.
- Вардар: библиотека для выполнения HTTP-запросов как в браузере, так и в Node.js.
- рожки: библиотека, позволяющая совместно использовать ресурсы из разных источников.
Настройка
Первое, что мы хотим сделать, это создать две папки проекта, одну для клиента и одну для сервера. Назовите их как хотите. я называю свою audio-to-pdf-client
и audio-to-pdf-server
, Соответственно.
Самый быстрый способ начать работу с Next.js на стороне клиента — загрузить его с помощью создать следующее приложение. Итак, откройте терминал и выполните следующую команду из папки вашего клиентского проекта:
npx create-next-app client
Теперь нам нужен наш экспресс-сервер. Мы можем получить его cd
-в папку проекта сервера и запустив npm init
команда. А package.json
файл будет создан в папке проекта сервера, как только это будет сделано.
Нам все еще нужно установить Express, поэтому давайте сделаем это сейчас с npm install express
. Теперь мы можем создать новый index.js
файл в папке проекта сервера и поместите туда этот код:
const express = require("express")
const app = express()
app.listen(4000, () => console.log("Server is running on port 4000"))
Готовы запустить сервер?
node index.js
Нам понадобится еще пара папок и еще один файл, чтобы двигаться вперед:
- Создайте
components
папка в папке клиентского проекта. - Создайте
SpeechToText.jsx
подать вcomponents
вложенная папка.
Прежде чем мы пойдем дальше, нам нужно сделать небольшую уборку. В частности, нам нужно заменить код по умолчанию в pages/index.js
файл с этим:
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="/ru/favicon.ico" />
</Head>
<h1>Convert your speech to pdf</h1>
<main>
<SpeechToText />
</main>
</div>
);
}
Импортный SpeechToText
компонент в конечном итоге будет экспортирован из components/SpeechToText.jsx
.
Давайте установим другие зависимости
Хорошо, у нас есть первоначальная настройка для нашего приложения. Теперь мы можем установить библиотеки, которые обрабатывают передаваемые данные.
Мы можем установить наши клиентские зависимости с помощью:
npm install react-speech-recognition regenerator-runtime axios
Далее идут зависимости нашего сервера Express, так что давайте cd
в папку проекта сервера и установите их:
npm install html-pdf-node cors
Вероятно, самое время сделать паузу и убедиться, что файлы в папках нашего проекта не повреждены. Вот что у вас должно быть в папке клиентского проекта на данный момент:
/audio-to-pdf-web-client
├─ /components
| └── SpeechToText.jsx
├─ /pages
| ├─ _app.js
| └── index.js
└── /styles
├─globals.css
└── Home.module.css
А вот что у вас должно быть в папке проекта сервера:
/audio-to-pdf-server
└── index.js
Создание пользовательского интерфейса
Что ж, наше преобразование речи в PDF не было бы таким уж замечательным, если бы не было возможности взаимодействовать с ним, так что давайте создадим для него компонент React, который мы можем вызвать <SpeechToText>
.
Вы можете полностью использовать свою собственную разметку. Вот что у меня есть, чтобы дать вам представление о том, что мы собираем вместе:
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;
Этот компонент возвращает Фрагмент реакции который содержит HTML <``section``>
элемент, содержащий три div:
.button-container
содержит две кнопки, которые будут использоваться для запуска и остановки распознавания речи..words
иcontentEditable
иsuppressContentEditableWarning
атрибуты, чтобы сделать этот элемент доступным для редактирования и подавить любые предупреждения от React.- Другой
.button-container
содержит еще две кнопки, которые будут использоваться для сброса и преобразования речи в PDF соответственно.
Стилистика — это совсем другое. Я не буду вдаваться в подробности здесь, но вы можете использовать некоторые написанные мной стили в качестве отправной точки для своих собственных. styles/global.css
.
Посмотреть полный 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 используются для управления цветом фона кнопок.
Давайте посмотрим последние изменения! Бежать npm run dev
в терминале и проверьте их.
Вы должны увидеть это в браузере при посещении http://localhost:3000
:
Наше первое преобразование речи в текст!
Первое действие, которое необходимо предпринять, — это импортировать необходимые зависимости в наш <SpeechToText>
компонент:
import React, { useRef, useState } from "react";
import SpeechRecognition, {
useSpeechRecognition,
} from "react-speech-recognition";
import axios from "axios";
Затем мы проверяем, поддерживается ли распознавание речи браузером, и отображаем уведомление, если оно не поддерживается:
const speechRecognitionSupported =
SpeechRecognition.browserSupportsSpeechRecognition();
if (!speechRecognitionSupported) {
return <div>Your browser does not support speech recognition.</div>;
}
Далее извлекаем transcript
и resetTranscript
из useSpeechRecognition()
крюк:
const { transcript, resetTranscript } = useSpeechRecognition();
Это то, что нам нужно для состояния, которое обрабатывает listening
:
const [listening, setListening] = useState(false);
Нам также нужен ref
для div
с contentEditable
атрибут, то нам нужно добавить ref
отнести к нему и передать transcript
as children
:
const textBodyRef = useRef(null);
…а также:
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
Последнее, что нам нужно здесь, это функция, которая запускает распознавание речи, и привязать эту функцию к onClick
прослушиватель событий нашей кнопки. Кнопка устанавливает прослушивание true
и заставляет его работать непрерывно. Мы отключим кнопку, пока она находится в этом состоянии, чтобы предотвратить запуск дополнительных событий.
const startListening = () => {
setListening(true);
SpeechRecognition.startListening({
continuous: true,
});
};
…а также:
<button
type="button"
onClick={startListening}
style={{ "--bgColor": "blue" }}
disabled={listening}
>
Start
</button>
Нажатие на кнопку теперь должно запустить транскрипцию.
Дополнительные функции
Итак, у нас есть компонент, который может Начало слушаю. Но теперь нам нужно, чтобы он делал еще несколько вещей, например stopListening
, resetText
и handleConversion
. Давайте сделаем эти функции.
const stopListening = () => {
setListening(false);
SpeechRecognition.stopListening();
};
const resetText = () => {
stopListening();
resetTranscript();
textBodyRef.current.innerText = "";
};
const handleConversion = async () => {}
Каждая из функций будет добавлена в onClick
прослушиватель событий на соответствующих кнопках:
<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>
Ассоциация handleConversion
функция асинхронна, потому что в конечном итоге мы будем делать запрос API. Кнопка «Стоп» имеет отключенный атрибут, который срабатывает, когда прослушивание ложно.
Если мы перезапустим сервер и обновим браузер, теперь мы можем запускать, останавливать и сбрасывать транскрипцию речи в браузере.
Теперь нам нужно, чтобы приложение транскрибировать который распознал речь, преобразовав ее в файл PDF. Для этого нам нужен серверный путь из Express.js.
Настройка маршрута API
Цель этого маршрута — взять текстовый файл, преобразовать его в PDF, записать этот PDF в нашу файловую систему, а затем отправить ответ клиенту.
Для настройки мы открываем server/index.js
файл и импортировать html-pdf-node
и fs
зависимости, которые будут использоваться для записи и открытия нашей файловой системы.
const HTMLToPDF = require("html-pdf-node");
const fs = require("fs");
const cors = require("cors)
Далее мы настроим наш маршрут:
app.use(cors())
app.use(express.json())
app.post("/", (req, res) => {
// etc.
})
Затем мы приступаем к определению наших опций, необходимых для использования html-pdf-node
внутри маршрута:
let options = { format: "A4" };
let file = {
content: `<html><body><pre style='font-size: 1.2rem'>${req.body.text}</pre></body></html>`,
};
Ассоциация options
Объект принимает значение для установки размера и стиля бумаги. Размеры бумаги следуют совершенно другой системе, чем единицы измерения размера, которые мы обычно используем в Интернете. Например, А4 — стандартный размер письма..
Ассоциация file
объект принимает либо URL-адрес общедоступного веб-сайта, либо HTML-разметку. Чтобы сгенерировать нашу HTML-страницу, мы будем использовать html
, body
, pre
HTML-теги и текст из req.body
.
Вы можете применить любой стиль по вашему выбору.
Далее мы добавим trycatch
для обработки любых ошибок, которые могут появиться по пути:
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
Далее мы будем использовать generatePdf
из html-pdf-node
библиотека для создания pdfBuffer
(необработанный файл PDF) из нашего файла и создать уникальный pdfName
:
HTMLToPDF.generatePdf(file, options).then((pdfBuffer) => {
// console.log("PDF Buffer:-", pdfBuffer);
const pdfName = "./data/speech" + Date.now() + ".pdf";
// Next code here
}
Оттуда мы используем модуль файловой системы для записи, чтения и (да, наконец!) отправки ответа клиентскому приложению:
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." });
});
});
Давайте немного разберем это:
- Ассоциация
writeFile
Модуль файловой системы принимает имя файла, данные и функцию обратного вызова, которая может вернуть сообщение об ошибке, если есть проблема с записью в файл. Если вы работаете с CDN, которая предоставляет конечные точки ошибок, вы можете использовать их вместо этого. - Ассоциация
readFile
Модуль файловой системы принимает имя файла и функцию обратного вызова, которая способна или возвращает ошибку чтения, а также прочитанные данные. Как только у нас не будет ошибки чтения и прочитанные данные появятся, мы создадим и отправим ответ клиенту. Опять же, это можно заменить конечными точками CDN, если они у вас есть. - Ассоциация
res.setHeader("Content-Type", "application/pdf");
сообщает браузеру, что мы отправляем файл PDF. - Ассоциация
res.setHeader("Content-Disposition", "attachment");
говорит браузеру сделать полученные данные загружаемыми.
Поскольку маршрут API готов, мы можем использовать его в нашем приложении по адресу http://localhost:4000
. Мы можем перейти к клиентской части нашего приложения, чтобы завершить handleConversion
функции.
Обработка преобразования
Прежде чем мы сможем начать работу над handleConversion
нам нужно создать состояние, которое обрабатывает запросы API на загрузку, ошибку, успех и другие сообщения. Мы собираемся использовать React useState
крючок, чтобы настроить это:
const [response, setResponse] = useState({
loading: false,
message: "",
error: false,
success: false,
});
В handleConversion
функция, мы проверим, когда веб-страница была загружена, прежде чем запускать наш код, и убедитесь, что div
с editable
атрибут не пустой:
if (typeof window !== "undefined") {
const userText = textBodyRef.current.innerText;
// console.log(textBodyRef.current.innerText);
if (!userText) {
alert("Please speak or write some text.");
return;
}
}
Мы продолжаем оборачивать наш возможный запрос API в trycatch
, обработав любую ошибку, которая может возникнуть, и обновив состояние ответа:
try {
} catch(error){
setResponse({
...response,
loading: false,
error: true,
message:
"An unexpected error occurred. Text not converted. Please try again",
success: false,
});
}
Затем мы устанавливаем некоторые значения для состояния ответа, а также устанавливаем конфигурацию для axios
и сделать почтовый запрос на сервер:
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
);
Как только мы получили успешный ответ, мы устанавливаем состояние ответа с соответствующими значениями и указываем браузеру загрузить полученный 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();
И мы можем использовать следующее ниже contentEditable div
для отображения сообщений:
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
Окончательный код
Я упаковал все на GitHub, чтобы вы могли проверить полный исходный код как для сервера, так и для клиента.