利用Web Speech API實(shí)現(xiàn)文本轉(zhuǎn)語音與高亮播放
Terence Eden的數(shù)字電臺(tái)是一個(gè)使用Web Speech API進(jìn)行文本轉(zhuǎn)語音(TTS)的有趣實(shí)驗(yàn)。現(xiàn)代網(wǎng)絡(luò)瀏覽器內(nèi)置了TTS功能。了解這個(gè)強(qiáng)大的API給了我一個(gè)想法。
我是個(gè)重度播客和有聲書聽眾。我很欣賞博客提供替代音頻版本的做法。《需要引用》和《轉(zhuǎn)向AI》是兩個(gè)典范。我一直想為自己的文章配音,但不像Molly和David那樣,我的聲音更適合默片。我還在開發(fā)一個(gè)RSS閱讀器,TTS將是一個(gè)完美的功能。
語音合成
Web Speech API讓我擺脫了自己聲音的困擾。朗讀整篇文章可以簡(jiǎn)單到三行代碼。
const $post = document.querySelector(".Main > .Prose");
const utterance = new SpeechSynthesisUtterance($post.innerText);
globalThis.speechSynthesis.speak(utterance);全局的speechSynthesis對(duì)象有speak、pause和resume方法。utterance實(shí)例將觸發(fā)pause和end等事件。這些基本元素足以構(gòu)建基本的播放控制。
高亮語音
我想要播放狀態(tài)的視覺追蹤。是否可以在朗讀時(shí)高亮特定單詞?是的!但這需要付出更多的努力。
utterance實(shí)例還會(huì)觸發(fā)boundary事件。
當(dāng)朗讀的語句達(dá)到單詞或句子邊界時(shí)觸發(fā)。 — Web Speech API草案規(guī)范
邊界事件包含兩個(gè)屬性:
charIndex— 下一個(gè)字符的起始索引charLength— 下一個(gè)要朗讀的單詞長(zhǎng)度
這非常有前景!CSS高亮API接受起始和結(jié)束范圍。下一個(gè)問題是,語音合成器只有一個(gè)文本塊。無法準(zhǔn)確地將這些數(shù)字映射回DOM節(jié)點(diǎn)。
我想到的解決方案是收集所有文本節(jié)點(diǎn)的數(shù)組。
const nodeList = [];
constcollectNodes = ($parent) => {
for (const $child of $parent.childNodes) {
if ($child.nodeType === Node.TEXT_NODE) {
if ($child.textContent.trim() !== "") {
nodeList.push($child);
}
} elseif ($child.nodeType === Node.ELEMENT_NODE) {
collectNodes($child);
}
}
};
const $post = document.querySelector(".Main > .Prose");
collectNodes($post);我使用遞歸函數(shù)創(chuàng)建一個(gè)扁平數(shù)組,包含博客文章中的所有文本節(jié)點(diǎn)。接下來,我可以遍歷數(shù)組,逐個(gè)朗讀每個(gè)節(jié)點(diǎn)。
const nextWord = () => {
if (nodeList.length === 0) {
return;
}
const $text = nodeList.shift();
const utterance = newSpeechSynthesisUtterance($text.textContent);
utterance.addEventListener("end", nextWord());
globalThis.speechSynthesis.speak(utterance);
};
nextWord();這個(gè)函數(shù)通過從列表頂部移除第一個(gè)單詞并朗讀它來工作。使用end事件,它會(huì)重復(fù)這個(gè)過程,直到所有單詞都被朗讀。
現(xiàn)在,當(dāng)我添加boundary事件監(jiān)聽器時(shí),我有了對(duì)父文本節(jié)點(diǎn)的引用。我可以將其用于CSS高亮范圍。
const highlight = newHighlight();
CSS.highlights.set("speech-synth", highlight);
constnextWord = () => {
if (nodeList.length === 0) {
return;
}
const $text = nodeList.shift();
const utterance = newSpeechSynthesisUtterance($text.textContent);
utterance.addEventListener("end", nextWord());
utterance.addEventListener("boundary", (ev) => {
highlight.clear();
const range = newRange();
range.setStart($text, ev.charIndex);
range.setEnd($text, ev.charIndex + ev.charLength);
highlight.add(range);
});
globalThis.speechSynthesis.speak(utterance);
};
nextWord();CSS有一個(gè)特殊的高亮選擇器。
::highlight(speech-synth) {
background: green;
}這樣,我就能在朗讀時(shí)高亮每個(gè)單詞。真酷!為了跟蹤高亮的單詞,我將父元素滾動(dòng)到視圖內(nèi)。
$text.parentNode.scrollIntoView({
behavior: "auto",
block: "nearest",
});改進(jìn)
使用此技術(shù)的一些元素(如圖片和視頻)沒有文本內(nèi)容。為此,我添加了額外的條件。首先,我創(chuàng)建一個(gè)映射(稍后解釋)。
const nodeParent = new WeakMap();然后在collectNodes中,我添加了特殊情況。對(duì)于圖片,我生成一個(gè)帶有“image:”前綴的文本節(jié)點(diǎn),用于朗讀時(shí)的上下文。
const tagName = $child.nodeName.toLowerCase();
if (tagName === "img") {
const $text = document.createTextNode(`image: ${$child.alt}`);
nodeParent.set($text, $child);
nodeList.push($text);
continue next;
}在boundary事件監(jiān)聽器中,在我應(yīng)用高亮范圍之前,我首先檢查弱映射。如果映射了父元素,我將應(yīng)用不同的樣式。
if (nodeParent.has($text)) {
const $parent = nodeParent.get($text);
$parent.dataset.speechSynthHighlight = "true";
return;
}這些節(jié)點(diǎn)不能被高亮,所以我應(yīng)用了一個(gè)輪廓。
[data-speech-synth-highlight] {
outline: 10px solid green;
}稍后,我會(huì)移除數(shù)據(jù)屬性并清除任何高亮(代碼未顯示)。
我對(duì)視頻和代碼示例做了同樣的事情。我應(yīng)該深入代碼塊并逐字閱讀語法嗎?我選擇不這樣做,因?yàn)槲艺J(rèn)為那會(huì)是糟糕的體驗(yàn)。這并不是要替代專業(yè)的屏幕閱讀器。
瀏覽器支持
Web Speech API得到了很好的支持。CSS高亮API的支持較少。我使用的最新版Chromium和WebKit瀏覽器工作良好。我使用的Firefox版本(Mullvad;ESR 128)不起作用。(當(dāng)Mozilla做的時(shí)候,我才會(huì)再次關(guān)心Firefox。)
macOS上的合成語音足夠好。它聽起來很機(jī)械,會(huì)犯一些語法錯(cuò)誤。但它可用!想必Windows和Linux有類似的語音。
源代碼
您可以查看我的JavaScript源文件以獲取完整代碼。目前它有點(diǎn)混亂!我將其實(shí)現(xiàn)為一個(gè)<speech-synth>自定義元素。我添加了一個(gè)額外的<dialog>的播放控制,用于暫停、恢復(fù)和結(jié)束語音。
在我的博客文章和單獨(dú)的筆記頁面的頂部有一個(gè)“播放合成音頻”按鈕。我希望有人覺得它有用!我將在下周改進(jìn)它。
原文鏈接:https://dbushell.com/2025/07/26/text-to-speech-synthesis/
作者:David Bushell































