[251016] fix: VoiceInputOverlay animation update

🕐 커밋 시간: 2025. 10. 16. 20:08:38

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +129줄
  • 삭제: -24줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/webSpeechActions.js
  ~ com.twin.app.shoptime/src/hooks/useWebSpeech.js
  ~ com.twin.app.shoptime/src/services/webSpeech/WebSpeechService.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.module.less
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.jsx (javascript):
     Added: VoiceListening()
     Deleted: VoiceListening()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceResponse.jsx (javascript):
     Added: SpotlightContainerDecorator()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
This commit is contained in:
2025-10-16 20:08:39 +09:00
parent d90385ec7d
commit 4e85a4c781
7 changed files with 129 additions and 24 deletions

View File

@@ -49,18 +49,18 @@ export const initializeWebSpeech =
webSpeechService.on('result', (result) => {
console.log('[WebSpeechActions] Result:', result);
// Interim 결과 (중간 결과)
// Interim 결과 (중간 결과) - 전체 연결된 텍스트 사용
if (!result.isFinal) {
dispatch({
type: types.WEB_SPEECH_INTERIM_RESULT,
payload: result.transcript,
payload: result.transcript, // 이미 전체 연결된 텍스트 (final + interim)
});
}
// Final 결과 (최종 결과)
else {
dispatch({
type: types.VOICE_STT_TEXT_RECEIVED, // 기존 VUI와 동일한 액션 사용
payload: result.transcript,
payload: result.transcript, // 전체 최종 텍스트
});
}
});

View File

@@ -28,7 +28,7 @@ export const useWebSpeech = (isActive, onSTTText, config = {}) => {
console.log('[useWebSpeech] Initializing Web Speech API');
dispatch(
initializeWebSpeech({
lang: config.lang || 'ko-KR',
lang: config.lang || 'en-US',
continuous: config.continuous || false,
interimResults: config.interimResults !== false,
})

View File

@@ -45,7 +45,7 @@ class WebSpeechService {
this.recognition = new SpeechRecognition();
// 설정 적용
this.recognition.lang = config.lang || 'ko-KR';
this.recognition.lang = config.lang || 'en-US';
this.recognition.continuous = config.continuous || false;
this.recognition.interimResults = config.interimResults !== false; // default true
this.recognition.maxAlternatives = config.maxAlternatives || 1;
@@ -78,16 +78,38 @@ class WebSpeechService {
// 음성 인식 결과
this.recognition.onresult = (event) => {
const results = event.results;
// 전체 텍스트 조합: 모든 final 결과 + 마지막 interim 결과
let finalTranscript = '';
let interimTranscript = '';
for (let i = 0; i < results.length; i++) {
const transcript = results[i][0].transcript;
if (results[i].isFinal) {
finalTranscript += transcript + ' ';
} else {
interimTranscript += transcript;
}
}
const fullTranscript = (finalTranscript + interimTranscript).trim();
const lastResult = results[results.length - 1];
const transcript = lastResult[0].transcript;
const isFinal = lastResult.isFinal;
const confidence = lastResult[0].confidence;
console.log('[WebSpeech] Result:', { transcript, isFinal, confidence });
console.log('[WebSpeech] Result:', {
fullTranscript,
finalTranscript,
interimTranscript,
isFinal,
confidence,
});
if (this.callbacks.onResult) {
this.callbacks.onResult({
transcript,
transcript: fullTranscript, // 전체 연결된 텍스트
finalTranscript: finalTranscript.trim(), // 확정된 텍스트만
interimTranscript: interimTranscript.trim(), // 중간 텍스트만
isFinal,
confidence,
alternatives: Array.from(lastResult).map((alt) => ({

View File

@@ -96,7 +96,7 @@ const VoiceInputOverlay = ({
handleWebSpeechSTT,
{
lang: 'en-US',
continuous: false,
continuous: false, // 침묵 감지 후 자동 종료
interimResults: true,
}
);
@@ -330,7 +330,7 @@ const VoiceInputOverlay = ({
);
case VOICE_MODES.LISTENING:
console.log('📺 Rendering: VoiceListening (15초 타이머 기반)');
return <VoiceListening />;
return <VoiceListening interimText={interimText} />;
case VOICE_MODES.RESPONSE:
console.log('📺 Rendering: VoiceResponse with text:', sttResponseText);
return <VoiceResponse responseText={sttResponseText} onTalkAgain={handleTalkAgain} />;

View File

@@ -1,13 +1,38 @@
// src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.jsx
import React from 'react';
import PropTypes from 'prop-types';
import css from './VoiceListening.module.less';
/**
* VoiceListening - 음성 입력 중 애니메이션 표시 컴포넌트
* 화면 중앙에 표시되는 음성 입력 시각화 막대
* 포커스를 받지 않으며 순수하게 시각적 피드백만 제공
* @param {string} interimText - 실시간 중간 음성 인식 텍스트
*/
const VoiceListening = () => {
const VoiceListening = ({ interimText }) => {
// 각 문장의 첫 글자를 대문자로 변환
const capitalizeSentences = (text) => {
if (!text || text.length === 0) return text;
// 공백 제거 후 첫 글자를 대문자로 변환
const trimmed = text.trim();
if (trimmed.length === 0) return text;
// 첫 글자를 대문자로 + 나머지 문자열
const firstChar = trimmed.charAt(0).toUpperCase();
const rest = trimmed.slice(1);
// 문장 구분자(. ! ?) 뒤의 첫 글자도 대문자로
const result = (firstChar + rest).replace(/([.!?]\s+)([a-z])/g, (match, separator, letter) => {
return separator + letter.toUpperCase();
});
return result;
};
const displayText = capitalizeSentences(interimText);
const isReceivingInput = interimText && interimText.trim().length > 0;
return (
<div className={css.container}>
<div className={css.listeningText}>
@@ -18,20 +43,35 @@ const VoiceListening = () => {
<span className={`${css.dot} ${css.dot3}`}>.</span>
</span>
</div>
<div className={css.visualizer}>
<div className={`${css.bar} ${css.bar1}`} />
<div className={`${css.bar} ${css.bar2}`} />
<div className={`${css.bar} ${css.bar3}`} />
<div className={`${css.bar} ${css.bar4}`} />
<div className={`${css.bar} ${css.bar5}`} />
<div className={`${css.bar} ${css.bar6}`} />
<div className={`${css.bar} ${css.bar7}`} />
<div className={`${css.bar} ${css.bar8}`} />
<div className={`${css.bar} ${css.bar9}`} />
<div className={`${css.bar} ${css.bar10}`} />
</div>
{isReceivingInput && (
<div className={css.visualizer}>
<div className={`${css.bar} ${css.bar1}`} />
<div className={`${css.bar} ${css.bar2}`} />
<div className={`${css.bar} ${css.bar3}`} />
<div className={`${css.bar} ${css.bar4}`} />
<div className={`${css.bar} ${css.bar5}`} />
<div className={`${css.bar} ${css.bar6}`} />
<div className={`${css.bar} ${css.bar7}`} />
<div className={`${css.bar} ${css.bar8}`} />
<div className={`${css.bar} ${css.bar9}`} />
<div className={`${css.bar} ${css.bar10}`} />
</div>
)}
{interimText && (
<div className={css.interimTextContainer}>
<p className={css.interimText}>{displayText}</p>
</div>
)}
</div>
);
};
VoiceListening.propTypes = {
interimText: PropTypes.string,
};
VoiceListening.defaultProps = {
interimText: '',
};
export default VoiceListening;

View File

@@ -180,3 +180,24 @@
opacity: 0;
}
}
// Interim Text 컨테이너
.interimTextContainer {
margin-top: 40px;
width: 80%;
max-width: 800px;
text-align: center;
pointer-events: none;
}
// Interim Text 스타일
.interimText {
font-size: 28px;
font-family: "LG Smart UI";
font-weight: 400;
color: rgba(255, 255, 255, 0.7); // 약간 투명한 흰색
margin: 0;
padding: 0 20px;
line-height: 1.4;
word-break: break-word; // 긴 단어 줄바꿈
}

View File

@@ -17,6 +17,28 @@ const ResponseContainer = SpotlightContainerDecorator(
);
const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
// 각 문장의 첫 글자를 대문자로 변환
const capitalizeSentences = (text) => {
if (!text || text.length === 0) return text;
// 공백 제거 후 첫 글자를 대문자로 변환
const trimmed = text.trim();
if (trimmed.length === 0) return text;
// 첫 글자를 대문자로 + 나머지 문자열
const firstChar = trimmed.charAt(0).toUpperCase();
const rest = trimmed.slice(1);
// 문장 구분자(. ! ?) 뒤의 첫 글자도 대문자로
const result = (firstChar + rest).replace(/([.!?]\s+)([a-z])/g, (match, separator, letter) => {
return separator + letter.toUpperCase();
});
return result;
};
const displayText = capitalizeSentences(responseText);
const handleTalkAgainClick = () => {
console.log('[VoiceResponse] TALK AGAIN clicked');
if (onTalkAgain) {
@@ -42,7 +64,7 @@ const VoiceResponse = ({ responseText = '', onTalkAgain }) => {
</SpottableButton>
<SpottableBubble className={css.bubbleMessage} spotlightId="voice-response-text">
<div className={css.bubbleText}>{responseText}</div>
<div className={css.bubbleText}>{displayText}</div>
</SpottableBubble>
</div>
</ResponseContainer>