随着语音界面变得越来越普遍,探索我们能够使用语音交互做些什么很有价值。例如,如果我们可以说些什么,然后将其转录并输出为可下载的 PDF 文件,会怎么样?
好吧,剧透警告:我们绝对可以做到!我们可以将一些库和框架拼凑在一起以实现它,这就是我们将在本文中一起做的事情。
我们正在使用的工具
首先,这两个是主要参与者:Next.js 和 Express.js。
Next.js 为 React 添加了额外的功能,包括构建静态网站的关键特性。由于它开箱即用提供的功能,例如动态路由、图像优化、内置域名和子域名路由、快速刷新、文件系统路由和 API 路由……以及许多其他功能,因此它是许多开发人员的首选。
在我们的案例中,我们绝对需要 Next.js 的API 路由 在我们的客户端服务器上。我们希望有一个路由可以获取文本文件,将其转换为 PDF,将其写入我们的文件系统,然后向客户端发送响应。
Express.js 允许我们使用路由、HTTP 助手和模板来启动一个小的 Node.js 应用程序。它是我们自己的 API 的服务器,当我们在事物之间传递和解析数据时,我们需要它。
我们还有一些其他依赖项将被使用
- react-speech-recognition:一个用于将语音转换为文本的库,使其可用于 React 组件。
- regenerator-runtime:一个用于解决在使用 react-speech-recognition 时 Next.js 中出现的“
regeneratorRuntime
未定义”错误的库 - html-pdf-node:一个用于将 HTML 页面或公共 URL 转换为 PDF 的库
- axios:一个用于在浏览器和 Node.js 中发出 HTTP 请求的库
- cors:一个允许跨源资源共享的库
设置
我们要做的第一件事是创建两个项目文件夹,一个用于客户端,一个用于服务器。您可以随意命名它们。我分别将我的命名为audio-to-pdf-client
和 audio-to-pdf-server
。
在客户端使用 Next.js 入门最快的方法是使用create-next-app 引导它。因此,打开您的终端,并从您的客户端项目文件夹中运行以下命令
npx create-next-app client
现在我们需要我们的 Express 服务器。我们可以通过 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
文件夹。 - 在
components
子文件夹中创建一个SpeechToText.jsx
文件。
在继续之前,我们需要进行一些清理工作。具体来说,我们需要用以下内容替换 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="/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
构建 UI
好吧,如果无法与我们的语音到 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;
此组件返回一个React 片段,其中包含一个 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>;
}
接下来,让我们从 useSpeechRecognition()
hook 中提取 transcript
和 resetTranscript
const { transcript, resetTranscript } = useSpeechRecognition();
这是处理 listening
状态所需的
const [listening, setListening] = useState(false);
我们还需要一个 ref
用于具有 contentEditable
属性的 div
,然后我们需要将 ref
属性添加到其中并将 transcript
作为 children
传递
const textBodyRef = useRef(null);
…以及
<div
className="words"
contentEditable
ref={textBodyRef}
suppressContentEditableWarning={true}
>
{transcript}
</div>
这里我们需要做的最后一件事是创建一个触发语音识别的函数,并将该函数绑定到我们按钮的 onClick
事件监听器上。该按钮将 listening 设置为 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 请求。“停止”按钮具有禁用属性,该属性将在监听为 false 时触发。
如果我们重新启动服务器并刷新浏览器,我们现在可以在浏览器中启动、停止和重置语音转录。
现在我们需要应用程序通过将其转换为 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` 对象接受一个值来设置纸张大小和样式。纸张大小遵循与我们在 Web 上通常使用的尺寸单位大不相同的系统。例如,A4 是典型的信纸尺寸。
`file` 对象接受公共网站的 URL 或 HTML 标记。为了生成我们的 HTML 页面,我们将使用 `html`、`body`、`pre` HTML 标签和来自 `req.body` 的文本。
您可以应用任何您选择的样式。
接下来,我们将添加一个 `trycatch` 来处理可能出现的任何错误。
try {
} catch(error){
console.log(error);
res.status(500).send(error);
}
接下来,我们将使用 `html-pdf-node` 库中的 `generatePdf` 从我们的文件生成一个 `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 端点,则可以将其替换为您的 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` 函数中,我们将检查网页加载完成后再运行我们的代码,并确保具有 `editable` 属性的 `div` 不为空。
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` 设置配置,并向服务器发出 POST 请求。
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 上,因此您可以查看服务器和客户端的完整源代码。
是否有实时演示网站?
太棒了!
不错的教程,哥们。
继续分享。
为什么不也使用 Next.js 作为后端呢?在我看来,整个 Express 部分似乎是多余的。
太棒了!!!