如何使用WebSpeechAPI构建文本转语音应用程序

来自菜鸟教程
跳转至:导航、​搜索

介绍

您很有可能与提供某种形式的语音体验的应用程序进行了交互。 它可能是一个具有文本转语音功能的应用程序,例如朗读您的短信或通知。 它也可以是具有语音识别功能的应用程序,例如 Siri 或 Google Assistant。

随着 HTML5 的出现,Web 平台上可用的 API 数量快速增长。 有几个称为 Web Speech API 的 API 已被开发用于无缝构建各种网络语音应用程序和体验。 这些 API 仍然是相当实验性的,尽管在所有现代浏览器中对它们中的大多数都提供了越来越多的支持。

在本文中,您将构建一个应用程序,该应用程序检索随机引用、显示引用并为用户提供使用文本到语音的浏览器朗读引用的能力。

先决条件

要完成本教程,您需要:

  • Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。

本教程已使用 Node v14.4.0、npm v6.14.5、axios v0.19.2、cors v2.8.5、express v4.17.1、和 jQuery v3.5.1。

使用网络语音 API

Web Speech API 有两个主要接口:

  • SpeechSynthesis - 用于文本到语音的应用程序。 这允许应用程序使用设备的语音合成器读出其文本内容。 可用的语音类型由 SpeechSynthesisVoice 对象表示,而要发出的文本由 SpeechSynthesisUtterance 对象表示。 请参阅 SpeechSynthesis 接口的 支持表 以了解有关浏览器支持的更多信息。
  • SpeechRecognition - 适用于需要异步语音识别的应用程序。 这允许应用程序从音频输入中识别语音上下文。 可以使用构造函数创建 SpeechRecognition 对象。 SpeechGrammar 接口用于表示应用程序应识别的语法集。 请参阅 SpeechRecognition 接口的 支持表 以了解有关浏览器支持的更多信息。

本教程将重点介绍 SpeechSynthesis

获得参考

只需一行代码即可获得对 SpeechSynthesis 对象的引用:

var synthesis = window.speechSynthesis;

以下代码片段显示了如何检查浏览器支持:

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;
} else {
  console.log('Text-to-speech not supported.');
}

在使用它提供的功能之前检查浏览器是否支持 [X30X] 非常有用。

获取可用的声音

在此步骤中,您将在现有代码的基础上获取可用的语音。 getVoices() 方法返回代表设备上所有可用声音的 SpeechSynthesisVoice 对象列表。

看看下面的代码片段:

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Regex to match all English language tags e.g en, en-US, en-GB
  var langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  var voices = synthesis
    .getVoices()
    .filter((voice) => langRegex.test(voice.lang));

  // Log the properties of the voices in the list
  voices.forEach(function (voice) {
    console.log({
      name: voice.name,
      lang: voice.lang,
      uri: voice.voiceURI,
      local: voice.localService,
      default: voice.default,
    });
  });
} else {
  console.log('Text-to-speech not supported.');
}

在这部分代码中,您将获取设备上可用语音的列表,并使用 langRegex 正则表达式过滤列表,以确保我们仅获取英语使用者的语音。 最后,您遍历列表中的声音并将每个声音的属性记录到控制台。

构建语音话语

在此步骤中,您将使用 SpeechSynthesisUtterance 构造函数并设置可用属性的值来构造语音。

以下代码片段创建用于阅读文本 "Hello World" 的语音:

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Get the first `en` language voice in the list
  var voice = synthesis.getVoices().filter(function (voice) {
    return voice.lang === 'en';
  })[0];

  // Create an utterance object
  var utterance = new SpeechSynthesisUtterance('Hello World');

  // Set utterance properties
  utterance.voice = voice;
  utterance.pitch = 1.5;
  utterance.rate = 1.25;
  utterance.volume = 0.8;

  // Speak the utterance
  synthesis.speak(utterance);
} else {
  console.log('Text-to-speech not supported.');
}

在这里,您从可用语音列表中获得第一个 en 语言语音。 接下来,您使用 SpeechSynthesisUtterance 构造函数创建一个新的话语。 然后设置话语对象的一些属性,例如 voicepitchratevolume。 最后,它使用 SpeechSynthesisspeak() 方法说出话语。

注意: 话语中可以说出的文本大小是有限制的。 每个话语中可以说出的文本的最大长度为 32,767 个字符。


请注意,您在构造函数中传递了要发出的文本。

您还可以通过设置 utterance 对象的 text 属性来设置要发出的文本。

这是一个简单的例子:

var synthesis = window.speechSynthesis;
var utterance = new SpeechSynthesisUtterance("Hello World");

// This overrides the text "Hello World" and is uttered instead
utterance.text = "My name is Glad.";

synthesis.speak(utterance);

这将覆盖在构造函数中传递的任何文本。

说话

在前面的代码片段中,我们通过在 SpeechSynthesis 实例上调用 speak() 方法来说出话语。 我们现在可以将 SpeechSynthesisUtterance 实例作为参数传递给 speak() 方法来说出话语。

var synthesis = window.speechSynthesis;

var utterance1 = new SpeechSynthesisUtterance("Hello World");
var utterance2 = new SpeechSynthesisUtterance("My name is Glad.");
var utterance3 = new SpeechSynthesisUtterance("I'm a web developer from Nigeria.");

synthesis.speak(utterance1);
synthesis.speak(utterance2);
synthesis.speak(utterance3);

您还可以使用 SpeechSynthesis 实例执行其他一些操作,例如暂停、恢复和取消话语。 因此 pause()resume()cancel() 方法在 SpeechSynthesis 实例上也可用。

第 1 步 — 构建文本转语音应用程序

我们已经看到了SpeechSynthesis接口的基本方面。 我们现在将开始构建我们的文本转语音应用程序。 在开始之前,请确保您的机器上安装了 Node 和 npm。

在终端上运行以下命令为应用程序设置项目并安装依赖项。

创建一个新的项目目录:

mkdir web-speech-app

进入新创建的项目目录:

cd web-speech-app

初始化项目:

npm init -y

安装项目所需的依赖项 - expresscorsaxios

npm install express cors axios

修改 package.json 文件的 "scripts" 部分,如下所示:

包.json

"scripts": {
  "start": "node server.js"
}

现在您已经为应用程序初始化了一个项目,您将继续使用 Express 为应用程序设置服务器。

新建一个server.js文件,添加如下内容:

服务器.js

const cors = require('cors');
const path = require('path');
const axios = require('axios');
const express = require('express');

const app = express();
const PORT = process.env.PORT || 5000;

app.set('port', PORT);

// Enable CORS (Cross-Origin Resource Sharing)
app.use(cors());

// Serve static files from the /public directory
app.use('/', express.static(path.join(__dirname, 'public')));

// A simple endpoint for fetching a random quote from QuotesOnDesign
app.get('/api/quote', (req, res) => {
  axios
    .get(
      'https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand'
    )
    .then((response) => {
      const [post] = response.data;
      const { title, content } = post || {};

      return title && content
        ? res.json({ status: 'success', data: { title, content } })
        : res
            .status(500)
            .json({ status: 'failed', message: 'Could not fetch quote.' });
    })
    .catch((err) =>
      res
        .status(500)
        .json({ status: 'failed', message: 'Could not fetch quote.' })
    );
});

app.listen(PORT, () => console.log(`> App server is running on port ${PORT}.`));

在这里,您使用 Express 设置节点服务器。 您使用 cors() 中间件启用了 CORS(跨域请求共享)。 您还可以使用 express.static() 中间件从项目根目录中的 /public 目录中提供静态文件。 这将使您能够为您将很快创建的索引页面提供服务。

最后,您设置了一个 GET /api/quote 路由,用于从 QuotesOnDesign API 服务获取随机报价。 您正在使用 axios(基于 Promise 的 HTTP 客户端库)发出 HTTP 请求。

以下是来自 QuotesOnDesign API 的示例响应:

Output[
  {
    "title": { "rendered": "Victor Papanek" },
    "content": {
      "rendered": "<p>Any attempt to separate design, to make it a thing-by-itself, works counter to the inherent value of design as the primary, underlying matrix of life.</p>\n",
      "protected": false
    }
  }
]

注意: 有关 QuotesOnDesign API 更改的更多信息,请参阅他们的 页面,其中记录了 4.0 和 5.0 之间的更改。


当您成功获取报价时,报价的 titlecontent 将在 JSON 响应的 data 字段中返回。 否则,将返回带有 500 HTTP 状态代码的失败 JSON 响应。

接下来,您将为应用视图创建一个索引页面。

首先,在项目的根目录下创建一个新的 public 文件夹:

mkdir public

接下来,在新建的public文件夹中新建一个index.html文件,添加如下内容:

公共/index.html

<html>

<head>
    <title>Daily Quotes</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="position-absolute h-100 w-100">
    <div id="app" class="d-flex flex-wrap align-items-center align-content-center p-5 mx-auto w-50 position-relative"></div>

    <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
    <script src="main.js"></script>
</body>

</html>

这将为应用程序创建一个基本索引页面,其中只有一个 <div id="app"> 将用作应用程序所有动态内容的挂载点。

您还添加了一个指向 Bootstrap CDN 的链接,以获取应用程序的一些默认 Bootstrap 4 样式。 您还包括用于 DOM 操作和 AJAX 请求的 jQuery,以及用于优雅 SVG 图标的 Feather 图标

第 2 步 — 构建主脚本

现在,您已经到了为应用程序提供动力的最后一部分——主脚本。 在你的app的public目录下新建一个main.js文件,添加如下内容:

公共/main.js

jQuery(function ($) {
  let app = $('#app');

  let SYNTHESIS = null;
  let VOICES = null;

  let QUOTE_TEXT = null;
  let QUOTE_PERSON = null;

  let VOICE_SPEAKING = false;
  let VOICE_PAUSED = false;
  let VOICE_COMPLETE = false;

  let iconProps = {
    'stroke-width': 1,
    'width': 48,
    'height': 48,
    'class': 'text-secondary d-none',
    'style': 'cursor: pointer'
  };

  function iconSVG(icon) {}

  function showControl(control) {}

  function hideControl(control) {}

  function getVoices() {}

  function resetVoice() {}

  function fetchNewQuote() {}

  function renderQuote(quote) {}

  function renderVoiceControls(synthesis, voice) {}

  function updateVoiceControls() {}

  function initialize() {}

  initialize();
});

此代码使用 jQuery 在加载 DOM 时执行函数。 您将获得对 #app 元素的引用并初始化一些变量。 您还声明了几个将在以下部分中实现的空函数。 最后,我们调用 initialize() 函数来初始化应用程序。

iconProps 变量包含几个属性,这些属性将用于将 Feather 图标作为 SVG 呈现给 DOM。

有了该代码,您就可以开始实现这些功能了。 修改public/main.js文件实现如下功能:

公共/main.js

// Gets the SVG markup for a Feather icon
function iconSVG(icon) {
  let props = $.extend(iconProps, { id: icon });
  return feather.icons[icon].toSvg(props);
}

// Shows an element
function showControl(control) {
  control.addClass('d-inline-block').removeClass('d-none');
}

// Hides an element
function hideControl(control) {
  control.addClass('d-none').removeClass('d-inline-block');
}

// Get the available voices, filter the list to have only English filters
function getVoices() {
  // Regex to match all English language tags e.g en, en-US, en-GB
  let langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  VOICES = SYNTHESIS.getVoices()
    .filter(function (voice) {
      return langRegex.test(voice.lang);
    })
    .map(function (voice) {
      return {
        voice: voice,
        name: voice.name,
        lang: voice.lang.toUpperCase(),
      };
    });
}

// Reset the voice variables to the defaults
function resetVoice() {
  VOICE_SPEAKING = false;
  VOICE_PAUSED = false;
  VOICE_COMPLETE = false;
}

iconSVG(icon) 函数将羽毛图标名称字符串作为参数(例如,'play-circle')并返回图标的 SVG 标记。 查看 Feather 网站 以查看可用 Feather 图标的完整列表。 另请查看 Feather 文档 以了解有关 API 的更多信息。

getVoices() 函数使用 SYNTHESIS 对象来获取设备上所有可用语音的列表。 然后,它使用正则表达式过滤列表以获取只有英语使用者的声音。

接下来,您将实现在 DOM 上获取和呈现引号的函数。 修改public/main.js文件实现如下功能:

公共/main.js

function fetchNewQuote() {
  // Clean up the #app element
  app.html('');

  // Reset the quote variables
  QUOTE_TEXT = null;
  QUOTE_PERSON = null;

  // Reset the voice variables
  resetVoice();

  // Pick a voice at random from the VOICES list
  let voice =
    VOICES && VOICES.length > 0
      ? VOICES[Math.floor(Math.random() * VOICES.length)]
      : null;

  // Fetch a quote from the API and render the quote and voice controls
  $.get('/api/quote', function (quote) {
    renderQuote(quote.data);
    SYNTHESIS && renderVoiceControls(SYNTHESIS, voice || null);
  });
}

function renderQuote(quote) {
  // Create some markup for the quote elements
  let quotePerson = $('<h1 id="quote-person" class="mb-2 w-100"></h1>');
  let quoteText = $('<div id="quote-text" class="h3 py-5 mb-4 w-100 font-weight-light text-secondary border-bottom border-gray"></div>');

  // Add the quote data to the markup
  quotePerson.html(quote.title.rendered);
  quoteText.html(quote.content.rendered);

  // Attach the quote elements to the DOM
  app.append(quotePerson);
  app.append(quoteText);

  // Update the quote variables with the new data
  QUOTE_TEXT = quoteText.text();
  QUOTE_PERSON = quotePerson.text();
}

fetchNewQuote() 方法中,您首先重置应用程序元素和变量。 然后,您使用 Math.random() 从存储在 VOICES 变量中的声音列表中随机选择一个声音。 您使用 $.get()/api/quote 端点发出 AJAX 请求,以获取随机报价,并将报价数据呈现到语音控件旁边的视图中。

renderQuote(quote) 方法接收一个引用对象作为其参数,并将内容添加到 DOM。 最后,它更新引用变量:QUOTE_TEXTQUOTE_PERSON

如果您查看 fetchNewQuote() 函数,您会注意到您调用了 renderVoiceControls() 函数。 此函数负责渲染用于播放、暂停和停止语音输出的控件。 它还呈现当前使用的语音和语言。

public/main.js文件进行如下修改,实现renderVoiceControls()功能:

公共/main.js

function renderVoiceControls(synthesis, voice) {
  let controlsPane = $('<div id="voice-controls-pane" class="d-flex flex-wrap w-100 align-items-center align-content-center justify-content-between"></div>');

  let voiceControls = $('<div id="voice-controls"></div>');

  // Create the SVG elements for the voice control buttons
  let playButton = $(iconSVG('play-circle'));
  let pauseButton = $(iconSVG('pause-circle'));
  let stopButton = $(iconSVG('stop-circle'));

  // Helper function to enable pause state for the voice output
  let paused = function () {
    VOICE_PAUSED = true;
    updateVoiceControls();
  };

  // Helper function to disable pause state for the voice output
  let resumed = function () {
    VOICE_PAUSED = false;
    updateVoiceControls();
  };

  // Click event handler for the play button
  playButton.on('click', function (evt) {});

  // Click event handler for the pause button
  pauseButton.on('click', function (evt) {});

  // Click event handler for the stop button
  stopButton.on('click', function (evt) {});

  // Add the voice controls to their parent element
  voiceControls.append(playButton);
  voiceControls.append(pauseButton);
  voiceControls.append(stopButton);

  // Add the voice controls parent to the controlsPane element
  controlsPane.append(voiceControls);

  // If voice is available, add the voice info element to the controlsPane
  if (voice) {
    let currentVoice = $('<div class="text-secondary font-weight-normal"><span class="text-dark font-weight-bold">' + voice.name + '</span> (' + voice.lang + ')</div>');

    controlsPane.append(currentVoice);
  }

  // Add the controlsPane to the DOM
  app.append(controlsPane);

  // Show the play button
  showControl(playButton);
}

在这里,您为语音控件和控件窗格创建容器元素。 您使用之前创建的 iconSVG() 函数来获取控制按钮的 SVG 标记并同时创建按钮元素。 您定义 paused()resumed() 辅助函数,它们将在设置按钮的事件处理程序时使用。

最后,将语音控制按钮和语音信息呈现给 DOM。 它也被配置为最初只显示 Play 按钮。

接下来,您将为在上一节中定义的语音控制按钮实现单击事件处理程序。

设置事件处理程序,如以下代码片段所示:

公共/main.js

// Click event handler for the play button
playButton.on('click', function (evt) {
  evt.preventDefault();

  if (VOICE_SPEAKING) {
    // If voice is paused, it is resumed when the playButton is clicked
    if (VOICE_PAUSED) synthesis.resume();
    return resumed();
  } else {
    // Create utterances for the quote and the person
    let quoteUtterance = new SpeechSynthesisUtterance(QUOTE_TEXT);
    let personUtterance = new SpeechSynthesisUtterance(QUOTE_PERSON);

    // Set the voice for the utterances if available
    if (voice) {
      quoteUtterance.voice = voice.voice;
      personUtterance.voice = voice.voice;
    }

    // Set event listeners for the quote utterance
    quoteUtterance.onpause = paused;
    quoteUtterance.onresume = resumed;
    quoteUtterance.onboundary = updateVoiceControls;

    // Set the listener to activate speaking state when the quote utterance starts
    quoteUtterance.onstart = function (evt) {
      VOICE_COMPLETE = false;
      VOICE_SPEAKING = true;
      updateVoiceControls();
    };

    // Set event listeners for the person utterance
    personUtterance.onpause = paused;
    personUtterance.onresume = resumed;
    personUtterance.onboundary = updateVoiceControls;

    // Refresh the app and fetch a new quote when the person utterance ends
    personUtterance.onend = fetchNewQuote;

    // Speak the utterances
    synthesis.speak(quoteUtterance);
    synthesis.speak(personUtterance);
  }
});

// Click event handler for the pause button
pauseButton.on('click', function (evt) {
  evt.preventDefault();

  // Pause the utterance if it is not in paused state
  if (VOICE_SPEAKING) synthesis.pause();
  return paused();
});

// Click event handler for the stop button
stopButton.on('click', function (evt) {
  evt.preventDefault();

  // Clear the utterances queue
  if (VOICE_SPEAKING) synthesis.cancel();
  resetVoice();

  // Set the complete status of the voice output
  VOICE_COMPLETE = true;
  updateVoiceControls();
});

在这里,您为语音控制按钮设置了单击事件侦听器。 单击 Play 按钮时,它会开始朗读以 quoteUtterance 开头的话语,然后是 personUtterance。 但是,如果语音输出处于暂停状态,它会恢复它。

您在 quoteUtteranceonstart 事件处理程序中将 VOICE_SPEAKING 设置为 true。 当 personUtterance 结束时,该应用程序还将刷新并获取新报价。

Pause按钮暂停语音输出,而Stop按钮结束语音输出并从队列中删除所有话语,使用的cancel()方法【X185X】接口。 代码每次调用updateVoiceControls()函数来显示相应的按钮。

在前面的代码片段中,您已经对 updateVoiceControls() 函数进行了几次调用和引用。 此函数负责更新语音控件以根据语音状态变量显示适当的控件。

public/main.js文件进行如下修改,实现updateVoiceControls()功能:

公共/main.js

function updateVoiceControls() {
  // Get a reference to each control button
  let playButton = $('#play-circle');
  let pauseButton = $('#pause-circle');
  let stopButton = $('#stop-circle');

  if (VOICE_SPEAKING) {
    // Show the stop button if speaking is in progress
    showControl(stopButton);

    // Toggle the play and pause buttons based on paused state
    if (VOICE_PAUSED) {
      showControl(playButton);
      hideControl(pauseButton);
    } else {
      hideControl(playButton);
      showControl(pauseButton);
    }
  } else {
    // Show only the play button if no speaking is in progress
    showControl(playButton);
    hideControl(pauseButton);
    hideControl(stopButton);
  }
}

在这部分代码中,您首先获得对每个语音控制按钮元素的引用。 然后,您指定在语音输出的不同状态下应显示哪些语音控制按钮。

您现在已准备好实现 initialize() 功能。 该函数负责初始化应用程序。 在public/main.js文件中添加如下代码片段,实现initialize()功能。

公共/main.js

function initialize() {
  if ('speechSynthesis' in window) {
    SYNTHESIS = window.speechSynthesis;

    let timer = setInterval(function () {
      let voices = SYNTHESIS.getVoices();

      if (voices.length > 0) {
        getVoices();
        fetchNewQuote();
        clearInterval(timer);
      }
    }, 200);
  } else {
    let message = 'Text-to-speech not supported by your browser.';

    // Create the browser notice element
    let notice = $('<div class="w-100 py-4 bg-danger font-weight-bold text-white position-absolute text-center" style="bottom:0; z-index:10">' + message + '</div>');

    fetchNewQuote();
    console.log(message);

    // Display non-support info on DOM
    $(document.body).append(notice);
  }
}

此代码首先检查 speechSynthesiswindow 全局对象上是否可用,然后将其分配给 SYNTHESIS 变量(如果可用)。 接下来,您设置获取可用语音列表的时间间隔。

您在这里使用间隔是因为 SpeechSynthesis.getVoices() 存在一个已知的异步行为,这使得它在初始调用时返回一个空数组,因为尚未加载语音。 间隔确保您在获取随机报价和清除间隔之前获得声音列表。

您现在已成功完成文本转语音应用程序。 您可以通过在终端中运行以下命令来启动应用程序:

npm start

如果可用,该应用程序将在端口 5000 上运行。

在浏览器中访问 localhost:5000 以观察应用程序。

现在,与播放按钮交互以听到所说的报价。

结论

在本教程中,您使用 Web Speech API 构建了一个用于 Web 的文本转语音应用程序。 您可以在 MDN Web Docs 中了解有关 Web Speech API 的更多信息并找到 一些有用的资源。

如果您想继续改进您的应用程序,您仍然可以实现和试验一些有趣的功能,例如音量控制、音高控制、速度/速率控制、说出的文本百分比等。

本教程的完整 源代码可在 GitHub 上获得。