등록 음성 검증과 일반 음성 차단
상위 기준: 보이스 프로필 등록 제품 구현 기준서 5장
화자 등록 음성은 “녹음 파일이 생겼는가”만 확인하면 안 된다. 음성이 너무 작거나 잡음이 대부분인 세트를 등록하면 이후 화자 식별 정확도가 낮아진다. 현재 구현은 녹음 신호 검사와 5개 세트의 임베딩 일관성 검사를 추가해 이런 오등록을 줄인다.
먼저 보는 결론
- 녹음과 품질 판정은 OnDevice Agent Service가 담당한다.
- Registration App은 원본 PCM이나 임베딩을 받지 않고 진행 상태와 실패 사유만 표시한다.
- 등록 중에는 일반 녹음을 멈추고
speaker_enrollment이 마이크를 단독 사용한다. - 각 세트는 길이·음량뿐 아니라 실제 발화량, 연속 발화, 잡음, 클리핑, DC 바이어스를 검사한다.
- 5개 세트의 임베딩이 같은 화자 범위인지 확인한 뒤에만 Voice ID를 만든다.
- 문장 일치 STT, 한 세트 내부 다화자 판정, 재생 공격 방지는 아직 별도 과제다.
왜 기존 검사로 부족했나
기존 validateVoiceProfileSet()은 아래 세 조건만 확인했다.
| 기존 검사 | 기준 |
|---|---|
| 녹음 길이 | 3초 이상 |
| 전체 RMS | 0.005 이상 |
| 클리핑 | ±0.98 이상 샘플이 15% 이하 |
이 조건에서는 5초 동안 일정하게 들어오는 팬 소음이나 TV 음성, 실제 발화가 매우 짧은 녹음도 통과할 수 있다. 또한 다섯 세트를 서로 다른 사람이 읽어도 최종 평균 임베딩을 만들었다.
현재 처리 흐름
Registration App 시작
-> OnDevice Agent Service가 일반 AudioRecord 정지
-> speaker_enrollment이 5.2초 수집
-> VoiceProfileAudioQualityGate
실패: 현재 세트 재녹음
통과: 세트 보관
-> 5세트 임베딩 생성
-> VoiceProfileEmbeddingQualityGate
실패: 모든 임시 세트 폐기, 1세트부터 재등록
통과: 기준 임베딩 평균·정규화
-> Voice ID와 reference_embedding.bin 저장
-> DeviceAgent에 Voice ID + 기준 임베딩 전달
-> 일반 음성 경로 복구
일반 STT/LLM과 등록 녹음이 같은 AudioRecord를 동시에 열지 않는다. 진입 시
VoiceRecognitionStatusStore.STOP, stopContinuousRecording()을 적용하고 등록 종료·취소에서
restoreAudioAfterVoiceProfileSession()으로 복구한다.
세트별 음향 검사
검사는 16 kHz float PCM을 20ms 프레임으로 나눠 수행한다.
| 검사 | 현재 시작값 | 실패 코드 |
|---|---|---|
| PCM 값 | NaN/Infinity 없음 | INVALID_AUDIO |
| 전체 길이 | 3초 이상 | TOO_SHORT |
| 전체 음량 | RMS 0.005 이상 |
TOO_QUIET |
| 클리핑 | ±0.98 샘플 비율 3% 이하 |
TOO_LOUD |
| DC 바이어스 | 샘플 평균 절댓값 0.05 이하 |
AUDIO_BIAS |
| 누적 발화 | 1.5초 이상 | INSUFFICIENT_SPEECH |
| 최장 연속 발화 | 300ms 이상 | INSUFFICIENT_SPEECH |
| 추정 SNR | 5dB 이상 | TOO_NOISY |
발화 임계값은 고정 RMS만 사용하지 않는다. 녹음 내 하위 20% 프레임을 noise floor 후보로 보고 전체 RMS와 함께 adaptive threshold를 만든다. 일정한 기계 소리를 발화로 오인하는 위험을 줄이면서, 기기에서 먼 사용자의 작은 정상 발화도 전부 차단하지 않기 위한 시작점이다.
로그에는 원본 PCM 대신 다음 숫자만 남긴다.
durationMs, rms, peak, clippedRatio, dcOffset,
speechMs, longestSpeechMs, estimatedSnrDb
세트 간 같은 화자 검사
5개 세트가 음향 검사를 통과하면 각 세트의 speaker embedding을 만든다.
- 임베딩 차원이 모두 같은지 확인한다.
- NaN, Infinity, 0 norm을 거부한다.
- 모든 세트 쌍의 cosine similarity를 계산한다.
- 최솟값이
0.45미만이면INCONSISTENT_SPEAKER로 거부한다. - 거부 시 일부 세트만 남기지 않고 전체 임시 세트를 비운 뒤 1세트부터 다시 받는다.
현재 matcher의 기본 식별 시작값도 0.45다. 이 값은 제품 확정 기준이 아니며 같은 화자의
거리·문장 변화와 다른 화자의 오수락 분포를 수집해 ROC 기준으로 조정해야 한다.
화면에 전달하는 실패 사유
| 사유 | 화면 안내 |
|---|---|
NO_SPEECH, TOO_SHORT, INSUFFICIENT_SPEECH |
세 문장을 다시 읽도록 안내 |
TOO_QUIET |
기기에 가까이에서 더 분명히 말하도록 안내 |
INCONSISTENT_SPEAKER |
같은 사용자 혼자 다시 녹음하도록 안내 |
| 나머지 신호 오류 | 조용한 곳에서 다시 녹음하도록 안내 |
Registration App은 실패 코드를 사용자 문구로 바꾸지만, 음성·세부 측정값·임베딩은 받지 않는다.
구현 코드
| 코드 | 역할 |
|---|---|
VoiceProfileAudioQualityGate.kt |
세트별 신호와 발화 품질 판정 |
VoiceProfileEmbeddingQualityGate.kt |
5개 세트 임베딩 유효성·동일 화자 범위 판정 |
ForegroundService.completeVoiceProfileSetCapture() |
세트 수집 종료와 품질 결과 상태 전이 |
ForegroundService.finalizeVoiceProfile() |
임베딩 검사 후 Voice ID·기준 임베딩 저장 |
VoiceProfileRegistrationActivity.errorDetail() |
reason code를 사용자 안내로 변환 |
검증 결과와 범위
집중 JVM 단위 테스트로 정상 음성형 신호, 무음, 일정 소음, 클리핑, DC 바이어스,
NaN 입력, 동일·상이 임베딩을 확인했다. assembleDeviceRelease와 Registration App의
unit/lint/release 빌드는 통과했다.
전체 OnDevice 단위 테스트에는 이번 변경 파일 범위 밖의 dictionary, night mode, DeviceErrorCode 테스트 실패 7건이 남아 있다. 따라서 이번 결과는 새 품질 게이트의 집중 테스트와 APK 빌드 통과 증거이며, 실환경 음질 승인 결과는 아니다.
아직 필요한 고도화
- 한 세트 내부 다화자: enrollment 전용 diarization으로 두 명 이상 또는 겹침 발화를 판정한다.
- 문장 준수: 제품 기획이 세 문장 낭독을 필수화할 때만 등록 전용 로컬 STT를 추가한다.
- Anti-spoof/liveness: 스피커 재생, 녹음 재생, 합성 음성을 별도 모델과 challenge로 차단한다.
- 현장 threshold: 거리, 팬, TV, 사용자 특성별 수집 데이터로 FAR/FRR을 측정한다.
- 장기 안정성: 취소·재시도 100회에서 AudioRecord, buffer, coroutine, native model 누수를 본다.
STT 문장 일치는 음성 품질 검사와 같지 않고, 일반 VAD/SNR은 liveness를 보장하지 않는다. 세 기능을 하나의 통과 조건으로 뭉치지 않고 독립 지표와 실패 코드로 운영해야 한다.