logo

DowanKim

7. 바람 소리로 데시벨은 체크되었는데, 말은 인식이 안되는 상황에선 고장이 나버린 문제

2025년 10월 10일

꽃다발 방명록

No-speech 오류 후 멈춤 — 자동 재시작 메커니즘 구현

문제 : 바람 소리 후 마이크가 반응하지 않음

전시 환경에서 바람 소리 등으로 음성 인식이 시작됐지만 실제 음성이 없어 no-speech 오류가 발생한 뒤, 이후 음성 인식이 재시작되지 않는 문제가 발생했습니다.

증상

  1. 바람 소리 등으로 볼륨 임계값 초과 → 음성 인식 시작
  2. 실제 음성이 없음 → no-speech 오류 발생
  3. 오류 후 세션 종료
  4. 이후 어떤 소리에도 반응하지 않음
  5. 마이크가 "죽은" 상태로 유지됨

예상 동작

바람 소리 → 인식 시작 → no-speech 오류 → 자동 재시작 준비 → 다음 음성 대기

실제 동작

바람 소리 → 인식 시작 → no-speech 오류 → [멈춤] → 이후 반응 없음 

원인 분석

1단계: 초기 오류 처리

초기에는 no-speech 오류를 특별히 처리하지 않았습니다.

recognition.onerror = (event) => { if (event.error === "aborted") { recognitionActiveRef.current = false; return; } recognitionActiveRef.current = false; // no-speech 오류를 특별히 처리하지 않음 finalizeRecognition(); // 재시작 로직이 없음 };

문제점:

  • no-speech 오류 시 재시작 플래그를 설정하지 않음
  • onend에서 재시작 로직이 실행되지 않음
  • recognitionActiveRef가 false로 설정되어도 재시작 요청이 큐에 없음

2단계: 실제 동작 흐름 확인

로그를 확인한 결과:

"Volume threshold exceeded 0.1523"
"Starting new recognition session"
"SpeechRecognition.start() invoked"
[바람 소리만 있고 실제 음성 없음]
"Speech recognition error no-speech"  // 오류 발생
"Recognition ended"  // 세션 종료
[이후 아무 일도 일어나지 않음]

3단계: 상태 확인

오류 발생 후 상태:

recognitionActiveRef.current = false; // ✅ false로 설정됨 pendingRecognitionStartRef.current = false; // ❌ true로 설정되지 않음 status = "idle"; // ✅ idle 상태

문제:

  • pendingRecognitionStartReffalse로 남아 재시작 요청이 없음
  • onend에서 재시작 로직이 실행되지 않음

4단계: onend 핸들러 확인

초기 onend 핸들러:

recognition.onend = () => { recognitionActiveRef.current = false; if (collectedTranscriptsRef.current.length > 0) { finalizeRecognition(); return; } setStatus((current) => { return current === "listening" ? "idle" : current; }); // ❌ pendingRecognitionStartRef를 확인하지 않음 // ❌ 재시작 로직이 없음 };

문제:

  • pendingRecognitionStartRef를 확인하지 않음
  • 재시작 로직이 없음

해결 과정

시도 1: no-speech 오류 시 플래그 설정

no-speech 오류 발생 시 재시작 플래그를 설정했습니다.

if (event.error === "no-speech") { warn("No speech detected during recognition session"); pendingRecognitionStartRef.current = true; } else { errorLog("Speech recognition error", event.error); }

핵심:

  • no-speech 오류 시 pendingRecognitionStartRef.current = true로 설정
  • 재시작 요청을 큐에 추가

시도 2: onend에서 재시작 로직 추가

onend 핸들러에서 플래그를 확인하고 재시작하도록 수정했습니다.

recognition.onend = () => { recognitionActiveRef.current = false; if (collectedTranscriptsRef.current.length > 0) { log("Recognition ended with pending transcripts. Finalizing now."); finalizeRecognition(); return; } setStatus((current) => { log("Recognition ended - previous status", current); return current === "listening" ? "idle" : current; }); if (pendingRecognitionStartRef.current && statusRef.current === "idle") { log("Processing deferred recognition start"); pendingRecognitionStartRef.current = false; setTimeout(() => { startListening(); }, 0); } };

핵심:

  • recognitionActiveRef.current = false로 상태 초기화
  • pendingRecognitionStartRef.current 확인
  • status === "idle"일 때 startListening() 재호출

시도 3: 상태 초기화 보강

오류 발생 시 상태를 확실히 초기화하도록 수정했습니다.

recognitionActiveRef.current = false;

모든 오류에서 recognitionActiveReffalse로 설정해 상태를 복원합니다.

해결 후 동작

정상적인 로그 흐름

"Volume threshold exceeded 0.1523"
"Starting new recognition session"
"SpeechRecognition.start() invoked"
[바람 소리만 있고 실제 음성 없음]
"Speech recognition error no-speech"
"No speech detected during recognition session"
"Recognition ended - previous status listening"
"Processing deferred recognition start"  // ✅ 재시작 로직 실행
"Starting new recognition session"  // ✅ 자동 재시작!
"SpeechRecognition.start() invoked"
[정상적인 음성 인식 재개]

동작 원리

타임라인: 0초 ──────────────────────────────────→ 3초

0.0초: 바람 소리 감지
       → Volume threshold exceeded
       → startListening() 호출
       → recognitionActiveRef = true

0.5초: 실제 음성 없음
       → no-speech 오류 발생
       → onerror 핸들러 실행
       → recognitionActiveRef = false ✅
       → pendingRecognitionStartRef = true ✅
       → finalizeRecognition() 호출

1.0초: onend 이벤트 발생
       → recognitionActiveRef = false (확실히)
       → status = "idle"
       → pendingRecognitionStartRef 확인 ✅
       → startListening() 재호출 ✅

1.5초: 새로운 인식 세션 시작
       → 정상적으로 음성 대기 상태로 복귀 ✅

구현 세부사항

1. no-speech 오류 감지 및 플래그 설정

recognition.onerror = (event) => { if (event.error === "aborted") { log("Recognition aborted by stop()"); recognitionActiveRef.current = false; setStatus((current) => (current === "listening" ? "idle" : current)); return; } recognitionActiveRef.current = false; const fatalErrors = new Set([ "network", "not-allowed", "service-not-allowed", "audio-capture", ]); if (event.error === "no-speech") { warn("No speech detected during recognition session"); pendingRecognitionStartRef.current = true; } else { errorLog("Speech recognition error", event.error); } if (fatalErrors.has(event.error)) { warn("Fatal recognition error detected. Clearing instance."); recognitionRef.current = null; } finalizeRecognition(); requestRecognitionRestart(event.error); };

핵심:

  • recognitionActiveRef.current = false로 상태 초기화
  • no-speech 오류 시 pendingRecognitionStartRef.current = true 설정
  • 다른 오류는 requestRecognitionRestart()로 처리

2. onend에서 재시작 처리

recognition.onend = () => { recognitionActiveRef.current = false; if (collectedTranscriptsRef.current.length > 0) { log("Recognition ended with pending transcripts. Finalizing now."); finalizeRecognition(); return; } setStatus((current) => { log("Recognition ended - previous status", current); return current === "listening" ? "idle" : current; }); if (pendingRecognitionStartRef.current && statusRef.current === "idle") { log("Processing deferred recognition start"); pendingRecognitionStartRef.current = false; setTimeout(() => { startListening(); }, 0); } };

핵심:

  • recognitionActiveRef.current = false로 상태 확실히 초기화
  • pendingRecognitionStartRef.current 확인
  • status === "idle"일 때 startListening() 재호출
  • setTimeout(..., 0)으로 다음 이벤트 루프에서 실행

3. 추가 안전장치: useEffect로 재시작 확인

useEffect(() => { if ( status === "idle" && pendingRecognitionStartRef.current && !recognitionActiveRef.current ) { pendingRecognitionStartRef.current = false; startListening(); } }, [status, startListening]);

onend에서 처리되지 않은 경우를 대비한 추가 안전장치입니다.

문제 해결 전후 비교

해결 전

타임라인:
0.0초: 바람 소리 감지
       → startListening() 호출
       → recognitionActiveRef = true

0.5초: no-speech 오류
       → onerror 실행
       → recognitionActiveRef = false
       → ❌ pendingRecognitionStartRef = false (그대로)
       → finalizeRecognition()

1.0초: onend 이벤트
       → recognitionActiveRef = false
       → status = "idle"
       → ❌ pendingRecognitionStartRef 확인 안 함
       → ❌ 재시작 안 함

결과: 마이크가 "죽은" 상태로 유지 ❌

해결 후

타임라인:
0.0초: 바람 소리 감지
       → startListening() 호출
       → recognitionActiveRef = true

0.5초: no-speech 오류
       → onerror 실행
       → recognitionActiveRef = false ✅
       → pendingRecognitionStartRef = true ✅
       → finalizeRecognition()

1.0초: onend 이벤트
       → recognitionActiveRef = false (확실히)
       → status = "idle"
       → pendingRecognitionStartRef 확인 ✅
       → startListening() 재호출 ✅

1.5초: 새로운 인식 세션 시작
       → 정상적으로 음성 대기 상태로 복귀 ✅

결과: 자동으로 재시작되어 정상 작동 ✅

추가 개선: 다른 오류 처리

no-speech 외 다른 오류는 requestRecognitionRestart()로 처리합니다.

finalizeRecognition(); requestRecognitionRestart(event.error);

일반 오류는 백오프 재시작 메커니즘을 사용합니다.

교훈

1. 오류별 처리 전략

오류 유형에 따라 다른 처리가 필요합니다.

// ✅ 좋은 패턴: 오류 유형별 처리 if (event.error === "no-speech") { // 즉시 재시작 가능한 오류 pendingRecognitionStartRef.current = true; } else if (fatalErrors.has(event.error)) { // 치명적인 오류 - 인스턴스 폐기 recognitionRef.current = null; } else { // 일반 오류 - 백오프 재시작 requestRecognitionRestart(event.error); }

2. 상태 초기화의 중요성

오류 발생 시 상태를 확실히 초기화해야 합니다.

// ✅ 여러 곳에서 상태 초기화 recognition.onerror = () => { recognitionActiveRef.current = false; // 여기서도 }; recognition.onend = () => { recognitionActiveRef.current = false; // 여기서도 확실히 };

3. 재시작 플래그 패턴

비동기 이벤트 간 통신에 플래그를 사용합니다.

// ✅ 좋은 패턴: 플래그로 재시작 요청 큐잉 onerror: () => { pendingRecognitionStartRef.current = true; // 플래그 설정 }; onend: () => { if (pendingRecognitionStartRef.current) { startListening(); // 플래그 확인 후 재시작 } };

4. setTimeout(..., 0) 사용

다음 이벤트 루프에서 실행해 상태 업데이트를 확실히 합니다.

// ✅ 좋은 패턴: 다음 이벤트 루프에서 실행 setTimeout(() => { startListening(); }, 0);

결론

no-speech 오류 후 자동 재시작을 위해 다음을 적용했습니다:

  1. onerror에서 no-speechpendingRecognitionStartRef = true 설정
  2. onend에서 플래그 확인 후 startListening() 재호출
  3. recognitionActiveRef를 확실히 초기화

이 개선으로:

  • no-speech 오류 후 자동 재시작
  • 마이크가 "죽은" 상태로 남지 않음
  • 전시 환경에서 안정적인 동작

오류 유형별 처리와 상태 초기화, 그리고 재시작 플래그를 통해 자동 복구가 되도록 하였습니다.