Skip to content

Commit 277b9f6

Browse files
committed
feat: improve read aloud (#105, #341)
1 parent 6bb5a81 commit 277b9f6

File tree

2 files changed

+24
-20
lines changed

2 files changed

+24
-20
lines changed

src/components/ConversationItem/index.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function ConversationItem({ type, content, session, done, port, onRetry }
2121
<p>{t('You')}:</p>
2222
<div className="gpt-util-group">
2323
<CopyButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
24+
<ReadButton contentFn={() => content} size={14} />
2425
{!collapsed ? (
2526
<span
2627
title={t('Collapse')}
@@ -74,9 +75,7 @@ export function ConversationItem({ type, content, session, done, port, onRetry }
7475
{session && (
7576
<CopyButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
7677
)}
77-
{session && (
78-
<ReadButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
79-
)}
78+
{session && <ReadButton contentFn={() => content} size={14} />}
8079
{!collapsed ? (
8180
<span
8281
title={t('Collapse')}

src/components/ReadButton/index.jsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,57 @@
11
import { useState } from 'react'
2-
import { UnmuteIcon, MuteIcon } from '@primer/octicons-react'
2+
import { MuteIcon, UnmuteIcon } from '@primer/octicons-react'
33
import PropTypes from 'prop-types'
44
import { useTranslation } from 'react-i18next'
5+
import { useConfig } from '../../hooks/use-config.mjs'
56

67
ReadButton.propTypes = {
78
contentFn: PropTypes.func.isRequired,
89
size: PropTypes.number.isRequired,
910
className: PropTypes.string,
1011
}
1112

13+
const synth = window.speechSynthesis
14+
1215
function ReadButton({ className, contentFn, size }) {
1316
const { t } = useTranslation()
1417
const [speaking, setSpeaking] = useState(false)
18+
const config = useConfig()
1519

1620
const startSpeak = () => {
17-
speechSynthesis.cancel()
18-
19-
let text = contentFn()
21+
synth.cancel()
2022

23+
const text = contentFn()
2124
const utterance = new SpeechSynthesisUtterance(text)
25+
const voices = synth.getVoices()
26+
27+
let voice
28+
if (config.preferredLanguage.includes('en') && navigator.language.includes('en'))
29+
voice = voices.find((v) => v.name.toLowerCase().includes('microsoft aria'))
30+
else if (config.preferredLanguage.includes('zh') || navigator.language.includes('zh'))
31+
voice = voices.find((v) => v.name.toLowerCase().includes('xiaoyi'))
32+
if (!voice) voice = voices.find((v) => v.lang.substring(0, 2) === config.preferredLanguage)
33+
if (!voice) voice = voices.find((v) => v.lang === navigator.language)
34+
2235
Object.assign(utterance, {
23-
// lang: 'zh-CN',
24-
rate: 0.9,
36+
rate: 1,
2537
volume: 1,
2638
onend: () => setSpeaking(false),
2739
onerror: () => setSpeaking(false),
40+
voice: voice,
2841
})
2942

30-
let supportedVoices = speechSynthesis.getVoices()
31-
for (let i = 0; i < supportedVoices.length; i++) {
32-
if (supportedVoices[i].lang.indexOf(text[0]) >= 0) {
33-
utterance.voice = supportedVoices[i]
34-
break
35-
}
36-
}
37-
38-
speechSynthesis.speak(utterance)
43+
synth.speak(utterance)
3944
setSpeaking(true)
4045
}
4146

4247
const stopSpeak = () => {
43-
speechSynthesis.cancel()
48+
synth.cancel()
4449
setSpeaking(false)
4550
}
4651

4752
return (
4853
<span
49-
title={t('Read')}
54+
title={t('Read Aloud')}
5055
className={`gpt-util-icon ${className ? className : ''}`}
5156
onClick={speaking ? stopSpeak : startSpeak}
5257
>

0 commit comments

Comments
 (0)