随着语音界面变得越来越重要,探索我们可以通过语音交互做的一些事情是值得的。 比如说,如果我们可以说些什么,然后将其转录并输出为可下载的 PDF 会怎样?
好吧,剧透警报:我们绝对 能够 去做! 我们可以拼凑一些库和框架来实现它,这就是我们将在本文中一起做的事情。
这些是我们正在使用的工具
首先,这是两个大玩家:Next.js 和 Express.js。
Next.js 增加了 React 的附加功能,包括构建静态站点的关键特性。 它是许多开发人员的首选,因为它提供了开箱即用的功能,例如动态路由、图像优化、内置域和子域路由、快速刷新、文件系统路由和 API 路由…… 很多很多其他的东西.
在我们的例子中,我们肯定需要 Next.js API 路由 在我们的客户端服务器上。 我们想要一个获取文本文件的路由,将其转换为 PDF,将其写入我们的文件系统,然后向客户端发送响应。
Express.js 允许我们获得一个带有路由、HTTP 助手和模板的小 Node.js 应用程序。 它是我们自己的 API 的服务器,这是我们在事物之间传递和解析数据时所需要的。
我们将使用其他一些依赖项:
- 反应语音识别:用于将语音转换为文本的库,使其可用于 React 组件。
- 再生器运行时:用于解决“
regeneratorRuntime
未定义”使用 react-speech-recognition 时出现在 Next.js 中的错误 - html-pdf-节点:用于将 HTML 页面或公共 URL 转换为 PDF 的库
- 爱可信:用于在浏览器和 Node.js 中发出 HTTP 请求的库
- CORS: 一个允许跨域资源共享的库
配置
我们要做的第一件事是创建两个项目文件夹,一个用于客户端,一个用于服务器。 随意命名它们。 我正在命名我的 audio-to-pdf-client
和 audio-to-pdf-server
。
在客户端开始使用 Next.js 的最快方法是引导它 创建下一个应用. 因此,打开您的终端并从您的客户端项目文件夹中运行以下命令:
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
客户端项目文件夹中的文件夹。 - 创建一个
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="/zh-CN/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
object 接受一个值来设置纸张大小和样式。 纸张尺寸遵循与我们通常在网络上使用的尺寸单位大不相同的系统。 例如, A4 是典型的字母大小.
file
object 接受公共网站的 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
并向服务器发出 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 上打包了所有内容,因此您可以查看服务器和客户端的完整源代码。