[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:
@@ -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, // 전체 최종 텍스트
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; // 긴 단어 줄바꿈
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user