用NextJS和ExpressJS将语音转换为PDF的教程

217 阅读11分钟

随着语音界面逐渐成为一种事物,我们值得探索一些可以用语音互动的事情。比如,如果我们可以说些什么,并将其转录为可下载的PDF文件,会怎么样?

好吧,搅局者警告:我们绝对可以做到这一点我们可以用一些库和框架来实现它,这就是我们在这篇文章中要做的。

这些是我们正在使用的工具

首先,这些是两个大玩家:Next.js和Express.js。

Next.js在React的基础上增加了一些功能,包括建立静态网站的关键功能。它是许多开发者的首选,因为它提供了开箱即用的功能,如动态路由、图像优化、内置域名和子域名路由、快速刷新、文件系统路由和API路由......还有许多其他功能

在我们的案例中,我们肯定需要Next.js的客户端服务器上的API路由。我们想要一个路由,它可以接收一个文本文件,将其转换为PDF,将其写入我们的文件系统,然后向客户端发送一个响应。

Express.js允许我们用路由、HTTP助手和模板来实现一个小的Node.js应用。它是我们自己的API的服务器,这也是我们在事情之间传递和解析数据时所需要的。

我们还有一些其他的依赖,我们将投入使用:

  1. react-speech-recognition。一个用于将语音转换为文本的库,使其对React组件可用。
  2. regenerator-runtime。一个用于排除使用 react-speech-recognition 时 Next.js 中出现的 "regeneratorRuntime is not defined" 错误的库。
  3. html-pdf-node。一个用于将HTML页面或公共URL转换为PDF的库。
  4. axios。一个用于在浏览器和 Node.js 中进行 HTTP 请求的库
  5. cors。一个允许跨源资源共享的库

设置

我们要做的第一件事是创建两个项目文件夹,一个用于客户端,一个用于服务器。你想给它们取什么名字就取什么名字。我将我的文件夹命名为audio-to-pdf-clientaudio-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包含两个按钮,将用于启动和停止语音识别。
  • .wordscontentEditablesuppressContentEditableWarning 属性,使这个元素可编辑,并抑制来自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() 钩子中提取transcriptresetTranscript :

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,resetTexthandleConversion 。让我们来制作这些功能。

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-nodefs 依赖关系,这些依赖关系将用于写入和打开我们的文件系统。

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页面,我们将使用htmlbodypre 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 函数中,我们将在运行我们的代码之前检查网页是否已经被加载,并确保diveditable 属性不是空的:

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上,所以你可以查看服务器和客户端的完整源代码。

服务器 repo

客户端 repo