diff --git a/app/api/tts/speak/route.ts b/app/api/tts/speak/route.ts new file mode 100644 index 0000000..0178061 --- /dev/null +++ b/app/api/tts/speak/route.ts @@ -0,0 +1,35 @@ +// app/api/tts/speak/route.ts +import { NextResponse } from 'next/server'; +import textToSpeech from '@google-cloud/text-to-speech'; + +export async function POST(req: Request) { + try { + const { text, voiceName } = await req.json(); + + // 서버에서만 JSON 키 읽기 + const client = new textToSpeech.TextToSpeechClient({ + credentials: JSON.parse(process.env.GOOGLE_TTS_KEY!), + }); + + const [response] = await client.synthesizeSpeech({ + input: { text }, + voice: { + name: voiceName || 'ko-KR-Chirp3-HD-Fenrir', + languageCode: 'ko-KR', + }, + audioConfig: { audioEncoding: 'MP3' }, + }); + + const audioContent = response.audioContent; + if (!audioContent) + return NextResponse.json({ error: 'No audio content' }, { status: 500 }); + + // ArrayBuffer → Base64 + const base64Audio = Buffer.from(audioContent).toString('base64'); + + return NextResponse.json({ audio: base64Audio }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: 'TTS failed' }, { status: 500 }); + } +} diff --git a/app/main/conversation/calling/page.tsx b/app/main/conversation/calling/page.tsx index fc3dc31..98f0969 100644 --- a/app/main/conversation/calling/page.tsx +++ b/app/main/conversation/calling/page.tsx @@ -6,35 +6,55 @@ import { motion } from 'framer-motion'; import { useRouter } from 'next/navigation'; import { ROUTES } from '@/constants/routes'; import { useCallStore } from '@/stores/callStore'; +import { useVoiceStore } from '@/stores/voiceStore'; +import { useTTS } from '@/hooks/useTTS'; export default function CallingPage() { const router = useRouter(); const decrementCall = useCallStore(state => state.decrementCall); + const selectedVoice = useVoiceStore(state => state.selectedVoice); + const { playTTS } = useTTS(); + const [showConnecting, setShowConnecting] = useState(true); const [callTime, setCallTime] = useState(0); + // textStep: 0 = 첫번째, 1 = 세번째 텍스트 + const [textStep, setTextStep] = useState(0); + // ttsStep: 0 = 첫번째 TTS, 1 = 세번째 TTS + const [ttsStep, setTtsStep] = useState(0); + + const CONNECTING_DURATION = 5000; // 연결 화면 유지 시간 (ms) + + // 텍스트 & TTS 정의 + const TEXTS = [ + { + text: '안녕! 오늘은 어떤 얘기할래?', + translation: 'What do you want to talk about today?', + }, // 첫번째 + { + text: '그런 일이 있었구나. 속상했겠다.. 그럼 나랑 오늘 한국어로 대화 많이 해보면서 공부해보자', + translation: + 'I see… that must have been upsetting. Let’s practice a lot of Korean conversation today!', + }, // 세번째 + ]; + + // 연결 화면 타이머 useEffect(() => { const timer = setTimeout(() => { setShowConnecting(false); setCallTime(0); - }, 5000); + }, CONNECTING_DURATION); return () => clearTimeout(timer); }, []); + // 통화 타이머 + // @ts-ignore useEffect(() => { let interval: number | undefined; - if (!showConnecting) { - interval = window.setInterval(() => { - setCallTime(prev => prev + 1); - }, 1000); + interval = window.setInterval(() => setCallTime(prev => prev + 1), 1000); } - - return () => { - if (interval !== undefined) { - window.clearInterval(interval); - } - }; + return () => interval && clearInterval(interval); }, [showConnecting]); const formatTime = (seconds: number) => { @@ -45,15 +65,36 @@ export default function CallingPage() { return `${m}:${s}`; }; + // TODO: PROTO 화면 클릭 시 TTS 재생 & 텍스트 단계 변경 + const handleUserInteraction = async () => { + if (!showConnecting && selectedVoice) { + if (ttsStep === 0) { + // 첫번째 클릭: 첫번째 TTS + await playTTS(TEXTS[0].text, selectedVoice.voiceName); + setTtsStep(1); // 다음 TTS는 세번째 + } else if (ttsStep === 1) { + setTtsStep(-1); + setTextStep(1); + // 두번째 클릭: 두번째 TTS (세번째 텍스트) + await playTTS(TEXTS[1].text, selectedVoice.voiceName); + console.log(ttsStep); + } else { + return; + } + } + }; + const handleEndCall = () => { decrementCall(); router.replace(ROUTES.MAIN.CONVERSATION.ROOT); }; return ( -
+
{showConnecting ? ( - // 연결 중 모션 ) : ( <> - {/* 통화 시작 텍스트 모션 */} + {/* 통화 타이머 */} Kody - {/* 캐릭터 번역 텍스트 모션 */} + {/* 캐릭터 대사 */}

- 안녕! 오늘은 어떤 얘기할래? + {textStep === 0 ? TEXTS[0].text : TEXTS[1].text}

- What do you want to talk about today? + {textStep === 0 ? TEXTS[0].translation : TEXTS[1].translation}

)} - {/* 이미지 영역 */} + {/* 캐릭터 + 종료 버튼 */} { + if (!selectedVoice || playing) return; + await playTTS(title, selectedVoice.voiceName); + }; return (
@@ -30,13 +41,18 @@ export default function CharacterFrontText({

{title} {audio && ( - audio icon + > + audio icon + )}

{/* 구분선 */} diff --git a/app/main/my-learning/_components/step1/PhrasePracticeText.tsx b/app/main/my-learning/_components/step1/PhrasePracticeText.tsx index 209a392..b353319 100644 --- a/app/main/my-learning/_components/step1/PhrasePracticeText.tsx +++ b/app/main/my-learning/_components/step1/PhrasePracticeText.tsx @@ -1,5 +1,9 @@ +'use client'; + import Image from 'next/image'; import { useLanguageStore } from '@/stores/languageStore'; +import { useVoiceStore } from '@/stores/voiceStore'; +import { useTTS } from '@/hooks/useTTS'; import { useState } from 'react'; import { RATING_THRESHOLDS, @@ -32,6 +36,8 @@ export default function PhrasePracticeText({ evaluationType, }: PhrasePracticeTextProps) { const { currentLanguage } = useLanguageStore(); + const { selectedVoice } = useVoiceStore(); + const { playTTS, playing } = useTTS(); const [isBookmarked, setIsBookmarked] = useState(false); const typedWords = inputText.trim() === '' ? [] : inputText.trim().split(' '); @@ -86,6 +92,11 @@ export default function PhrasePracticeText({ ); }); + const handlePlayAudio = async () => { + if (!selectedVoice || playing) return; + await playTTS(phrase, selectedVoice.voiceName); + }; + return (
@@ -96,15 +107,20 @@ export default function PhrasePracticeText({

{renderHighlightedPhrase()}

- {'listen'} 0 && showEvaluation ? 'pb-5.5' : 'pb-1.5' - } - /> + )} {showTranslate && translation && (