使用 JavaScript 原生 API 开发文本转语音(tts)开发

Qiang 发布在技术 1

文本转语音(tts

可以使用 Web Speech API 来实现文本转语音(tts)的功能。以下是一个简单的示例代码:

function textToSpeech(text) {
  const synth = window.speechSynthesis;
  const utterance = new SpeechSynthesisUtterance(text);
  synth.speak(utterance);
}

这个函数接受一个字符串参数 text,然后使用 SpeechSynthesisUtterance 创建一个新的语音合成对象,最后使用 speechSynthesis 对象的 speak 方法将文本转换为语音输出。

需要注意的是,Web Speech API 并不是所有浏览器都支持,所以在使用之前需要检查浏览器是否支持该 API。可以使用以下代码进行检查:

if ('speechSynthesis' in window) {
  // Web Speech API 可用
} else {
  // Web Speech API 不可用
}

获取说话人列表

可以使用 speechSynthesis.getVoices() 方法来获取可用的语音列表。这个方法返回一个数组,包含所有可用的语音对象。

以下是一个示例代码,演示如何获取可用的语音列表:

function getVoices() {
  const synth = window.speechSynthesis;
  const voices = synth.getVoices();
  return voices;
}

这个函数使用 speechSynthesis.getVoices() 方法获取所有可用的语音列表,然后返回这个列表,列表的数据结构如下:

SpeechSynthesisVoice 
{
  voiceURI: 'Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)', 
  name: 'Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)', 
  lang: 'zh-CN', 
  localService: false, 
  default: false
}

需要注意的是,speechSynthesis.getVoices() 方法是异步的,因为语音合成系统需要一些时间来加载可用的语音。所以在使用这个方法之前,需要先等待语音合成系统加载完毕。可以监听 speechSynthesis.onvoiceschanged 事件,在这个事件触发时再调用 speechSynthesis.getVoices() 方法。

以下是一个示例代码,演示如何监听 speechSynthesis.onvoiceschanged 事件:

function getVoices() {
  return new Promise(resolve => {
    const synth = window.speechSynthesis;
    synth.onvoiceschanged = () => {
      const voices = synth.getVoices();
      resolve(voices);
    };
  });
}

这个函数返回一个 Promise 对象,当语音合成系统加载完毕时,Promise 对象的 resolve 方法会被调用,并传递可用的语音列表作为参数。在使用这个函数时,可以使用 async/await 或者 Promise.then() 方法来获取可用的语音列表。

指定文本语言和输出的语音语言

可以使用 SpeechSynthesisUtterance 对象的 lang 属性来指定文本语言,使用 SpeechSynthesisUtterance 对象的 voice 属性来指定输出的语音语言。

以下是一个示例代码,演示如何指定文本语言和输出的语音说话人:

function textToSpeech(text, lang, voiceURI) {
  const synth = window.speechSynthesis;
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.lang = lang;
  const voices = synth.getVoices();
  const voice = voices.find(v => v.voiceURI === voiceURI);
  utterance.voice = voice;
  synth.speak(utterance);
}

这个函数接受三个参数:text 表示要转换为语音的文本,lang 表示文本语言,voiceURI 表示输出的语音说话人。

在函数内部,我们首先使用 SpeechSynthesisUtterance 创建一个新的语音合成对象,然后使用 utterance.lang 属性指定文本语言。接着,我们使用 synth.getVoices() 方法获取所有可用的语音列表,然后使用 Array.prototype.find() 方法找到指定 voiceURI 的语音对象。最后,我们使用 utterance.voice 属性将语音对象指定为输出的语音说话人。

需要注意的是,不是所有语音都支持所有语言,所以在指定语音说话人时需要检查该语音是否支持指定的文本语言。可以使用以下代码来检查语音是否支持指定的文本语言:

if (voice && voice.lang.includes(lang)) {
  // 语音支持指定的文本语言
} else {
  // 语音不支持指定的文本语言
}

完整示例

const textToSpeech = async (text, options = {}) => {
  const synth = window.speechSynthesis;

  // Check if Web Speech API is available
  if (!('speechSynthesis' in window)) {
    alert("Your web Speech API is not available");
    return;
  }

  // Detect language using franc library
  const { franc } = await import("https://cdn.jsdelivr.net/npm/franc@6.1.0/+esm");
  const lang = franc(text);

  // Get available voices and find the one that matches the detected language
  const voices = await new Promise(resolve => {
    const voices = synth.getVoices();
    resolve(voices);
  });
  const voice = voices.find(v => langEq(v.lang, lang) && !v.localService);

  // Create a new SpeechSynthesisUtterance object and set its parameters
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = voice;
  utterance.rate = options.rate || 1.0;
  utterance.pitch = options.pitch || 1.0;
  utterance.volume = options.volume || 1.0;

  // Speak the text
  synth.speak(utterance);
};

const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' });
const langEq = (lang1, lang2) => {
  let langStr1 = regionNamesInEnglish.of(lang1)
  let langStr2 = regionNamesInEnglish.of(lang2)
  if (langStr1.indexOf(langStr2) !== -1) return true
  if (langStr2.indexOf(langStr1) !== -1) return true
  return langStr1 === langStr2
}

该函数接收一个文本和一些可选参数,如语速、音调和音量。函数首先检查 Web Speech API 是否可用,如果不可用,则会弹出一个警告并返回。

然后,它使用 franc 库检测文本的语言,并查找与该语言匹配的说话人。接下来,它创建一个新的 SpeechSynthesisUtterance 对象并设置其参数,然后使用 synth.speak() 方法发出语音。

后记

Web 的文本转语音很简单,只开头的三行代码就可以用了,几秒种的事,但还是踩了几个坑。

设置说话人

说话人的属性里有一个名为 localService 的属性,这个属性表示这个语音是本地生成的,而因为我是自动获取第一个说话人,于是就返回这个本地的给我了,让我以为说话人列表是摆设,最后手动设置说话人察觉到,所以对不了解的东西一定要亲自动手,不能嫌麻烦。

获取文本语言

查了一遍,franc 这个库有的比较多,推荐的也多,但这个库使用 ECMAScript 模块化语法了,没办法通过 cdn 导入。而我对 ECMAScript 模块化语法不了解,绕了很多弯路。

这里简单的记下 ECMAScript 模块化语法

先直接说如何在浏览器的 js 文件里引入 ECMAScript 模块 —— 使用 const { obj1, func1, ... } = await import("esm.js") 语法即可得到一个 ESM 模板,其中的 obj, func1,是这个模块里公开的变量和函数 —— 这种方式称为动态 importimport 是关键字,注意:不是函数名,语法是:import(moduleSpecifier);moduleSpecifier 为模块说明符,其实就是模块地址 —— 由于import()返回一个promise,所以,我们可以使用 async/await 来代替 then` 这种回调形式。

这个坑在哪,因为在知道解决方案前,我不知道有 动态 import,一直在看 静态 import,而 静态 import 是无法跨域的,且只在 html 文件里可用,比如:

<script type="module">
  import {franc} from 'https://esm.sh/franc@6?bundle'
</script>
<script>
  const text = "Hello world!";
  const langCode = franc(text);
  console.log(langCode); // 'eng'
</script>

这里的 franc 的作用域只在他所在的 module 域里有效,在下面的 js 片段里,franc 则是未定义函数,而要想在 js 文件里引入怎么做呢?可以把 js 文件 也作为一个 module 导入,比如:

app.js

import {franc} from 'https://esm.sh/franc@6?bundle'
const text = "Hello world!";
const langCode = franc(text);
console.log(langCode); // 'eng'

index.html

<script type="module" src="./app.js">
</script>

这样是可以的。

但!

但这样 app.js 里的函数、变量等就无法被 index.html 里的元素引用了,比如:

index.html

<button onclick="showLang("我是一个中文汉字。")`>Setting</button>
<script type="module">
  import {franc} from 'https://esm.sh/franc@6?bundle'
  function showSetting(text) {
    alert(franc(text))
  }
</script>

这时点击 Setting 按钮,则会提示 showLang 函数未定义,因为这时的 showLang 在一个 ES6 模块内,所以他只在他的作用域内有效,无法被外部引用。

后来在 万岁,浏览器原生支持ES6 export和import模块啦! 这篇文章里,大概的了解了 ES6 的模块是怎么回事后,才算是成功解决“如何导入 ES6 模块“这个问题。

算是学艺不精。

把 ISO 639-3 转换为 ISO 639-1

这个坑是 franc 函数返回 ISO 639-3 代码(三字母代码),不是 ISO 639-1ISO 639-2,即不是我们常见的 zh-CNen-US 这种格式,即 ISO 639-1 两字母加地区。但还记得吗,我们语音转文本里的说话人,使用的是 ISO 639-1,所以我们需要做一个转换。

这个也找了很久没找到现成的 js 库,不知道是不是搜索姿势不对,后来通过 MDN 发现,Intl.DisplayNames 可以显示代码(即上面的几种 ISO 语言标准代码)的语言。

所以我们可以通过遍历所有的说话人语言,与文本语言进行匹配,结果一致(含子集,如 EnglishBritish English)则返回该说话人。

于是就有了上面的 langEq 函数。这个函数可以直接拿去用,还有 textToSpeech 函数。

不知道你们有没有遇到这样的问题,都是怎么解决的呢?

TOP
前往 GitHub Discussion 评论