[251016] feat: VoiceInputOverlay advanced fesatures

🕐 커밋 시간: 2025. 10. 16. 20:42:59

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +149줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
This commit is contained in:
2025-10-16 20:43:01 +09:00
parent 4e85a4c781
commit 659b760c8c

View File

@@ -51,6 +51,14 @@ const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
const MIC_WEBSPEECH_SPOTLIGHT_ID = 'voice-overlay-mic-webspeech-button';
// 🔧 실험적 기능: Wake Word Detection ("Hey Shoptime")
// false로 설정하면 이 기능은 완전히 비활성화됩니다
const ENABLE_WAKE_WORD = false;
// 🔧 실험적 기능: Beep Sound on Listening Start
// false로 설정하면 Beep 소리가 재생되지 않습니다
const ENABLE_BEEP_SOUND = true;
const VoiceInputOverlay = ({
isVisible,
onClose,
@@ -63,6 +71,7 @@ const VoiceInputOverlay = ({
const dispatch = useDispatch();
const lastFocusedElement = useRef(null);
const listeningTimerRef = useRef(null);
const audioContextRef = useRef(null);
const [inputFocus, setInputFocus] = useState(false);
const [micFocused, setMicFocused] = useState(false);
const [micWebSpeechFocused, setMicWebSpeechFocused] = useState(false);
@@ -73,6 +82,69 @@ const VoiceInputOverlay = ({
// STT 응답 텍스트 저장
const [sttResponseText, setSttResponseText] = useState('');
// 🔊 Beep 소리 재생 함수
const playBeep = useCallback(() => {
if (!ENABLE_BEEP_SOUND) return;
try {
// AudioContext 지원 여부 확인 (TV 환경에서는 지원되지 않을 수 있음)
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) {
console.warn(
'[VoiceInputOverlay] AudioContext not supported in this environment, skipping beep'
);
return;
}
// AudioContext 생성 (재사용)
if (!audioContextRef.current) {
try {
audioContextRef.current = new AudioContextClass();
} catch (contextErr) {
console.warn('[VoiceInputOverlay] Failed to create AudioContext:', contextErr);
return;
}
}
const audioContext = audioContextRef.current;
// null 또는 undefined 체크
if (!audioContext) {
console.warn('[VoiceInputOverlay] AudioContext is null or undefined, skipping beep');
return;
}
// AudioContext 메서드 존재 여부 확인
if (
typeof audioContext.createOscillator !== 'function' ||
typeof audioContext.createGain !== 'function'
) {
console.warn('[VoiceInputOverlay] AudioContext methods not available, skipping beep');
return;
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800; // 800Hz (높은 피치)
oscillator.type = 'sine'; // 부드러운 소리
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); // 볼륨 30%
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); // 페이드아웃
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1); // 0.1초 재생
console.log('🔊 [VoiceInputOverlay] Beep sound played successfully');
} catch (err) {
// 어떤 오류가 발생하더라도 앱이 멈추지 않도록 조용히 처리
console.warn('[VoiceInputOverlay] Failed to play beep sound (non-critical):', err);
}
}, []);
// Web Speech API Hook (WebSpeech 모드일 때만 활성화)
const handleWebSpeechSTT = useCallback((sttText) => {
console.log('🎤 [VoiceInputOverlay] WebSpeech STT text received:', sttText);
@@ -144,6 +216,24 @@ const VoiceInputOverlay = ({
// }
// }, [lastSTTText, sttTimestamp, isVisible, onSearchChange, onClose]);
// 🎉 Wake Word Detection: PROMPT 모드에서 "Hey Shoptime" 감지
useEffect(() => {
if (!ENABLE_WAKE_WORD) return;
if (currentMode !== VOICE_MODES.PROMPT) return;
if (!interimText) return;
const text = interimText.toLowerCase().trim();
const wakeWords = ['hey shoptime', 'hey shop time', 'heyshoptime'];
// Wake Word 감지
const detected = wakeWords.some((word) => text.includes(word));
if (detected) {
console.log('🎉 [VoiceInputOverlay] Wake word detected in PROMPT mode:', text);
handleWakeWordDetected();
}
}, [interimText, currentMode, handleWakeWordDetected]);
// WebSpeech Interim 텍스트 로그 출력
useEffect(() => {
if (interimText && voiceInputMode === VOICE_INPUT_MODE.WEBSPEECH) {
@@ -193,6 +283,25 @@ const VoiceInputOverlay = ({
// 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요
// 🎉 Wake Word Detection: PROMPT 모드에서 백그라운드 리스닝 시작
useEffect(() => {
if (!ENABLE_WAKE_WORD) return;
if (!isVisible) return;
if (currentMode !== VOICE_MODES.PROMPT) return;
console.log('🎙️ [VoiceInputOverlay] Starting background listening for wake word detection');
// PROMPT 모드에서 백그라운드 리스닝 시작
startListening();
// Cleanup
return () => {
if (ENABLE_WAKE_WORD && currentMode === VOICE_MODES.PROMPT) {
stopListening();
}
};
}, [ENABLE_WAKE_WORD, isVisible, currentMode, startListening, stopListening]);
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
useEffect(() => {
if (isVisible) {
@@ -281,10 +390,45 @@ const VoiceInputOverlay = ({
}
}, []);
// 🎉 Wake Word Detection: "Hey Shoptime" 감지 시 자동으로 LISTENING 모드로 전환
const handleWakeWordDetected = useCallback(() => {
if (!ENABLE_WAKE_WORD) return;
console.log('🎉 [VoiceInputOverlay] Wake word detected! Switching to LISTENING mode');
// 기존 타이머 정리
if (listeningTimerRef.current) {
clearTimeout(listeningTimerRef.current);
listeningTimerRef.current = null;
}
// LISTENING 모드로 전환
setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH);
setCurrentMode(VOICE_MODES.LISTENING);
// WebSpeech 재시작 (continuous: false로 일반 입력 모드)
stopListening();
setTimeout(() => {
startListening();
}, 300);
// 15초 타이머 설정
listeningTimerRef.current = setTimeout(() => {
console.log('⏰ [VoiceInputOverlay] 15초 타임아웃 - PROMPT 모드로 복귀');
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
listeningTimerRef.current = null;
stopListening();
}, 15000);
}, [startListening, stopListening]);
// TALK AGAIN 버튼 핸들러
const handleTalkAgain = useCallback(() => {
console.log('🎤 [VoiceInputOverlay] TALK AGAIN - Restarting LISTENING mode');
// 🔊 Beep 소리 재생
playBeep();
// 기존 타이머 정리
if (listeningTimerRef.current) {
clearTimeout(listeningTimerRef.current);
@@ -309,7 +453,7 @@ const VoiceInputOverlay = ({
listeningTimerRef.current = null;
stopListening();
}, 15000);
}, [startListening, stopListening]);
}, [playBeep, startListening, stopListening]);
// 모드에 따른 컨텐츠 렌더링
const renderModeContent = () => {
@@ -446,6 +590,9 @@ const VoiceInputOverlay = ({
// 2. WebSpeech API 시작 (독립 동작)
console.log('🎤 [VoiceInputOverlay] Starting LISTENING mode (15s) + WebSpeech API');
// 🔊 Beep 소리 재생
playBeep();
// 기존 타이머 정리
if (listeningTimerRef.current) {
clearTimeout(listeningTimerRef.current);
@@ -478,7 +625,7 @@ const VoiceInputOverlay = ({
handleClose();
}
},
[currentMode, handleClose, startListening, stopListening]
[currentMode, handleClose, playBeep, startListening, stopListening]
);
return (