文本转语音(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
,是这个模块里公开的变量和函数 —— 这种方式称为动态 import
,import
是关键字,注意:不是函数名,语法是: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-1
或 ISO 639-2
,即不是我们常见的 zh-CN
、en-US
这种格式,即 ISO 639-1
两字母加地区。但还记得吗,我们语音转文本里的说话人,使用的是 ISO 639-1
,所以我们需要做一个转换。
这个也找了很久没找到现成的 js 库,不知道是不是搜索姿势不对,后来通过 MDN 发现,Intl.DisplayNames
可以显示代码(即上面的几种 ISO
语言标准代码)的语言。
所以我们可以通过遍历所有的说话人语言,与文本语言进行匹配,结果一致(含子集,如 English
和 British English
)则返回该说话人。
于是就有了上面的 langEq
函数。这个函数可以直接拿去用,还有 textToSpeech
函数。
不知道你们有没有遇到这样的问题,都是怎么解决的呢?