Skip to content

Commit

Permalink
feat: 음성으로 읽기 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
cjy3458 committed Aug 25, 2024
1 parent ac2815e commit 730ea4e
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 10 deletions.
64 changes: 64 additions & 0 deletions src/components/common/speak/SpeechTale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { franc } from "franc";
import { iso6393 } from "iso-639-3";
import styled from "styled-components";

interface SpeakPracticeProps {
text1: string;
}

const getLangCode = (isoCode: string): string => {
const lang = iso6393.find((lang) => lang.iso6393 === isoCode);
if (lang && lang.iso6393 === "cmn") {
return "zh";
}
return lang ? lang.iso6391 || "en" : "en";
};

const SpeechTale = ({ text1 }: SpeakPracticeProps) => {
const handleBtn = () => {
const detectedLang = franc(text1);
const langCode = getLangCode(detectedLang);

const utterance = new SpeechSynthesisUtterance(text1);
utterance.lang = langCode;
utterance.rate = 0.6;

window.speechSynthesis.speak(utterance);
};

return (
<Wrapper>
<SpeakBox>
<SpeakBtn src="/speaker.png" onClick={handleBtn} />
<WordBox>{text1}</WordBox>
</SpeakBox>
</Wrapper>
);
};

export default SpeechTale;

const Wrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
gap: 2rem;
width: 100%;
display: none;
`;

const SpeakBox = styled.div`
display: flex;
align-items: center;
gap: 1.3rem;
height: fit-content;
`;

const SpeakBtn = styled.img`
cursor: pointer;
width: 2.2rem;
`;

const WordBox = styled.div`
display: none;
`;
39 changes: 29 additions & 10 deletions src/components/tales/readTale/ReadTale.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
import { useEffect, useState } from "react";
import Header from "@components/common/header/Header";
import * as S from "./ReadTale.styled";
import Dropdown from "@components/common/dropDown/Dropdown";
import { useEffect, useState } from "react";
import NextBtn from "@components/common/NextBtn";
import { getTale } from "@apis/createTales";
import { useLocation } from "react-router-dom";
import { nationElements } from "@utils/defaultData";
import { ResponseTaleData } from "@type/createTale";
import { toggleSpeech } from "@utils/speechUtil";

const ReadTale = () => {
const location = useLocation();
// const navigate = useNavigate();
const { response } = location.state || {};

const [language, setLanguage] = useState<string | number | null>(2);
const [data, setData] = useState<ResponseTaleData>();
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [isSpeaking, setIsSpeaking] = useState<boolean>(false);

const onClick = () => {
console.log(data);
const handleDivClick = (index: number) => {
setSelectedIndex((prevIndex) => (prevIndex === index ? null : index));
};

useEffect(() => {
const fetchData = async () => {
const tale = await getTale(Number(language) || 2, response);
setData(tale);
};

fetchData();
}, [language, response]);

const handleSpeechButtonClick = () => {
if (data) {
const textToSpeak = data.story.replace(/\n/g, " ");
toggleSpeech(textToSpeak, isSpeaking, setIsSpeaking);
}
};

return (
<>
<Header text="동화 읽기" />
Expand All @@ -47,22 +55,33 @@ const ReadTale = () => {
/>
</S.ReadTaleHead>
<S.TaleWrapper>
{data.story.split("\n").map((line, index) => (
<div key={index}>{line}</div>
{data.story.split("\n").map((line, idx) => (
<div
key={idx}
onClick={() => handleDivClick(idx)}
style={{
backgroundColor:
selectedIndex === idx ? "#FFF4B3" : "transparent",
padding: "5px",
cursor: "pointer",
}}
>
{line}
</div>
))}
</S.TaleWrapper>
<S.BtnWrapper>
<NextBtn
width="48%"
isActive={true}
text="음성으로 듣기"
handleBtn={onClick}
text={isSpeaking ? "🟩중지" : "🔊음성으로 듣기"}
handleBtn={handleSpeechButtonClick}
/>
<NextBtn
width="48%"
isActive={true}
text="학습하기"
handleBtn={onClick}
handleBtn={() => {}}
/>
</S.BtnWrapper>
</>
Expand Down
53 changes: 53 additions & 0 deletions src/utils/speechUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { franc } from "franc";
import { iso6393 } from "iso-639-3";

export const getLangCode = (isoCode: string): string => {
const lang = iso6393.find((lang) => lang.iso6393 === isoCode);
if (lang && lang.iso6393 === "cmn") {
return "zh";
}
return lang ? lang.iso6391 || "en" : "en";
};

export const speakText = (
text: string,
onEnd?: () => void,
onPause?: () => void
) => {
const detectedLang = franc(text);
const langCode = getLangCode(detectedLang);

const utterance = new SpeechSynthesisUtterance(text.replace(/\n/g, " "));
utterance.lang = langCode;
utterance.rate = 0.6;

if (onEnd) {
utterance.onend = () => onEnd();
}
if (onPause) {
utterance.onpause = () => onPause();
}

window.speechSynthesis.speak(utterance);
};

export const toggleSpeech = (
text: string,
isSpeaking: boolean,
setIsSpeaking: (isSpeaking: boolean) => void
) => {
if (isSpeaking) {
window.speechSynthesis.pause();
setIsSpeaking(false);
} else {
new SpeechSynthesisUtterance(text);
window.speechSynthesis.cancel();

speakText(
text,
() => setIsSpeaking(false),
() => setIsSpeaking(false)
);
setIsSpeaking(true);
}
};

0 comments on commit 730ea4e

Please sign in to comment.