JS朗读功能踩坑记录
API SpeechSynthesisUtterance
- `lang` : 语言 Gets and sets the language of the utterance.
- `pitch` : 音高 Gets and sets the pitch at which the utterance will be spoken at.
- `rate` : 语速 Gets and sets the speed at which the utterance will be spoken at.
- `text` : 文本 Gets and sets the text that will be synthesised when the utterance is spoken.
- `voice` : 声音 Gets and sets the voice that will be used to speak the utterance.
- `volume` : 音量 Gets and sets the volume that the utterance will be spoken at.
- `onboundary` : 单词或句子边界触发,即分隔处触发 Fired when the spoken utterance reaches a word or sentence boundary.
- `onend` : 结束时触发 Fired when the utterance has finished being spoken.
- `onerror` : 错误时触发 Fired when an error occurs that prevents the utterance from being succesfully spoken.
- `onmark` : Fired when the spoken utterance reaches a named SSML "mark" tag.
- `onpause` : 暂停时触发 Fired when the utterance is paused part way through.
- `onresume` : 重新播放时触发 Fired when a paused utterance is resumed.
- `onstart` : 开始时触发 Fired when the utterance has begun to be spoken.
API SpeechSynthesis
- `paused` : **Read only** 是否暂停 A Boolean that returns true if the SpeechSynthesis object is in a paused state.
- `pending` : **Read only** 是否处理中 A Boolean that returns true if the utterance queue contains as-yet-unspoken utterances.
- `speaking` : **Read only** 是否朗读中 A Boolean that returns true if an utterance is currently in the process of being spoken — even if SpeechSynthesis is in a paused state.
- `onvoiceschanged` : 声音变化时触发
- `cancel()` : 情况待朗读队列 Removes all utterances from the utterance queue.
- `getVoices()` : 获取浏览器支持的语音包列表 Returns a list of SpeechSynthesisVoice objects representing all the available voices on the current device.
- `pause()` : 暂停 Puts the SpeechSynthesis object into a paused state.
- `resume()` : 重新开始 Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused.
- `speak()` : 读合成的语音,参数必须为`SpeechSynthesisUtterance`的实例 Adds an utterance to the utterance queue; it will be spoken when any other utterances queued before it have been spoken.
简单实现
const speaker = new window.SpeechSynthesisUtterance();
speaker.text="请朗读这段文字的内容!"
window.speechSynthesis.speak('speaker')
踩坑记录
const speech = new SpeechSynthesisUtterance()
let language =lang ||(config?.language === Language.Auto ? window.navigator.language : userConfig?.language)
if (language === 'ja') {
language = 'ja-JP'
} else if (language === 'zh-Hans') {
language = 'zh-CN'
} else if (language === 'zh-Hant') {
language = 'zh-TW'
}
const voiceName = isEdge ? 'Microsoft' : isChrome ? 'Google' : 'Google'
const voices = window.speechSynthesis.getVoices()
const hasVoices = voices.find((item) => {
const name = item.name
return name.includes(voiceName) &&item.lang.toUpperCase().includes(language.toUpperCase())
})
if (hasVoices) {
speech.voice = hasVoices
} else {
speech.voice = voices[0]
}
- 如何处理朗读外语中断(韩语,日语,俄语,未解决)
- 将朗读文字根据标点符号分割 (朗读依旧会中断)
- 将朗读文字分割为100个字符的文字 (英语不好配置)
- 怀疑是特殊字符导致(去除特殊字符后依旧朗读失败)
完整代码
import { Language, UserConfig, getUserConfig } from '@/config'
import { memo, useEffect, useRef, useState } from 'react'
import { VscDebugContinueSmall, VscDebugStart, VscDebugPause, VscClose } from 'react-icons/vsc'
import { isMac, isChrome, isEdge } from '@/utils/utils'
import { debounce } from 'lodash-es'
interface ReadAloudType {
text: string
lang?: string
}
const ReadAloud = ({ text, lang }: ReadAloudType) => {
const [isReading, setIsReading] = useState(false)
const [isPause, setIsPause] = useState(false)
const [config, setConfing] = useState<UserConfig>()
const speechRef = useRef<any>([])
const play = debounce(async () => {
window.speechSynthesis.cancel()
setIsReading(true)
speechRef.current.forEach((res) => {
window.speechSynthesis.speak(res)
})
}, 1000)
const pause = () => {
setIsPause(true)
window.speechSynthesis.pause()
}
const resume = () => {
setIsPause(false)
window.speechSynthesis.resume()
}
const cancel = () => {
setIsReading(false)
window.speechSynthesis.cancel()
}
const splitSentences = (text: string) => {
let sentences = text.split(/([.!?。!?])/g)
sentences = sentences.filter(function (sentence) {
return sentence.trim().length > 0
})
let result:string[] = []
let currentSentence = ''
for (let i = 0; i < sentences.length; i++) {
if ((currentSentence + sentences[i]).length <= 120) {
currentSentence += sentences[i]
} else {
let temp: string = currentSentence.trim()
while (temp.length > 120) {
result.push(temp.substring(0, 120))
temp = temp.substring(120)
}
result.push(temp)
currentSentence = sentences[i]
}
}
if (currentSentence !== '') {
result.push(currentSentence)
}
return result
}
const init = async () => {
const userConfig = await getUserConfig()
let language =
lang ||
(config?.language === Language.Auto ? window.navigator.language : userConfig?.language)
setConfing(userConfig)
const unescapedText = text.replace(/\\(.)/g, (match, escapedChar) => {
const unescapedCharMap = {
n: '\n',
r: '\r',
t: '\t',
f: '\f',
b: '\b',
"'": "'",
'"': '"',
'\\': '\\',
}
return unescapedCharMap[escapedChar] || escapedChar
})
const textList = splitSentences(unescapedText)
for (let i = 0; i < textList?.length; i++) {
const textItem = textList[i]
const speechList = speechRef.current
const speech = new SpeechSynthesisUtterance()
speech.text = textItem
.replace(/(\*{1,2}|#{1,6}|[`~]{1,2})(.*?)(\1)/g, '$2')
.replace(/\[.*?\]\(.*?\)/g, '')
if (language === 'ja') {
language = 'ja-JP'
} else if (language === 'zh-Hans') {
language = 'zh-CN'
} else if (language === 'zh-Hant') {
language = 'zh-TW'
}
const voiceName = isEdge ? 'Microsoft' : isChrome ? 'Google' : 'Google'
const voices = window.speechSynthesis.getVoices()
const hasVoices = voices.find((item) => {
const name = item.name
return name.includes(voiceName) && item.lang.toUpperCase().includes(language.toUpperCase())
})
if (hasVoices) {
speech.voice = hasVoices
} else {
speech.voice = voices[0]
}
if (i === textList.length - 1) {
speech.onend = (e) => {
console.log('🚀 ~ file: ReadAloud.tsx:85 ~ init ~ onend:')
setIsReading(false)
}
}
speech.onerror = (error) => {
console.log('🚀 ~ file: ReadAloud.tsx:39 ~ play ~ error:', error)
setIsPause(false)
setIsReading(false)
}
speech.onstart = (e) => {
console.log('🚀 ~ file: ReadAloud.tsx:39 ~ play ~ start:', e)
}
speech.onpause = () => {
setIsPause(true)
}
speechList.push(speech)
console.log('🚀 ~ file: ReadAloud.tsx:106 ~ init ~ speech:', speech, speechList)
speechRef.current = speechList
}
}
useEffect(() => {
if (text.length > 0) {
init()
}
return () => {
window.speechSynthesis.cancel()
}
}, [text])
return (
<div className="glarity---flex glarity---items-center">
{!isReading && (
<div
onClick={() => {
play()
}}
className="glarity---cursor-pointer glarity---flex glarity---items-center"
title="Read"
>
<VscDebugStart size={16} />
</div>
)}
{isReading && !isPause && (
<div
onClick={pause}
className="glarity---cursor-pointer glarity---flex glarity---items-center"
title="Pause"
>
<VscDebugPause size={16} />
</div>
)}
{isReading && isPause && (
<div
onClick={resume}
className="glarity---cursor-pointer glarity---flex glarity---items-center"
title="Resume"
>
<VscDebugContinueSmall size={16} />
</div>
)}
{isReading && (
<div
onClick={cancel}
className="glarity---cursor-pointer glarity---flex glarity---items-center"
title="Cancel"
>
<VscClose size={16} />
</div>
)}
</div>
)
}
export default memo(ReadAloud)