随着语音界面逐渐成为一种事物,我们值得探索一些可以用语音互动的事情。比如,如果我们可以说些什么,并将其转录为可下载的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
is not defined" 错误的库。 - 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
-ing 进入服务器项目文件夹并运行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
构建用户界面
好吧,如果没有办法与之互动,我们的语音转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()
钩子中提取transcript
和resetTranscript
:
const { transcript, resetTranscript } = useSpeechRecognition();
这就是我们处理listening
的状态所需要的:
const [listening, setListening] = useState(false);
div
我们还需要一个带有contentEditable
属性的ref
,然后我们需要给它添加ref
属性,并将transcript
作为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
对象接受一个值来设置纸张大小和样式。纸张尺寸遵循的系统与我们在网络上通常使用的尺寸单位大不相同。例如,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的端点,可以用它们来代替。res.setHeader("Content-Type", "application/pdf");
告诉浏览器,我们正在发送一个PDF文件。res.setHeader("Content-Disposition", "attachment");
告诉浏览器,使收到的数据可以下载。
由于API路线已经准备好了,我们可以在我们的应用程序中使用它,网址是http://localhost:4000
。我们可以进入我们应用程序的客户端部分,完成handleConversion
的功能。
处理转换
在我们开始处理handleConversion
功能之前,我们需要创建一个状态来处理我们的API请求的加载、错误、成功和其他信息。我们将使用React的 [useState](https://reactjs.org/docs/hooks-state.html)
钩子来设置它:
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
,并向服务器发出一个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();
而我们可以在contentEditablediv
下面使用以下内容来显示信息。
<div>
{response.success && <i className="success">{response.message}</i>}
{response.error && <i className="error">{response.message}</i>}
</div>
最终代码
我把所有的东西都打包到了GitHub上,所以你可以查看服务器和客户端的完整源代码。