← Docs hub

등록 음성 검증과 일반 음성 차단

상위 기준: 보이스 프로필 등록 제품 구현 기준서 5장

화자 등록 음성은 “녹음 파일이 생겼는가”만 확인하면 안 된다. 음성이 너무 작거나 잡음이 대부분인 세트를 등록하면 이후 화자 식별 정확도가 낮아진다. 현재 구현은 녹음 신호 검사와 5개 세트의 임베딩 일관성 검사를 추가해 이런 오등록을 줄인다.

먼저 보는 결론

  1. 녹음과 품질 판정은 OnDevice Agent Service가 담당한다.
  2. Registration App은 원본 PCM이나 임베딩을 받지 않고 진행 상태와 실패 사유만 표시한다.
  3. 등록 중에는 일반 녹음을 멈추고 speaker_enrollment이 마이크를 단독 사용한다.
  4. 각 세트는 길이·음량뿐 아니라 실제 발화량, 연속 발화, 잡음, 클리핑, DC 바이어스를 검사한다.
  5. 5개 세트의 임베딩이 같은 화자 범위인지 확인한 뒤에만 Voice ID를 만든다.
  6. 문장 일치 STT, 한 세트 내부 다화자 판정, 재생 공격 방지는 아직 별도 과제다.

등록 음성 검증과 일반 음성 차단 구조

검증 구조 SVG 크게 보기

왜 기존 검사로 부족했나

기존 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을 만든다.

  1. 임베딩 차원이 모두 같은지 확인한다.
  2. NaN, Infinity, 0 norm을 거부한다.
  3. 모든 세트 쌍의 cosine similarity를 계산한다.
  4. 최솟값이 0.45 미만이면 INCONSISTENT_SPEAKER로 거부한다.
  5. 거부 시 일부 세트만 남기지 않고 전체 임시 세트를 비운 뒤 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 빌드 통과 증거이며, 실환경 음질 승인 결과는 아니다.

아직 필요한 고도화

  1. 한 세트 내부 다화자: enrollment 전용 diarization으로 두 명 이상 또는 겹침 발화를 판정한다.
  2. 문장 준수: 제품 기획이 세 문장 낭독을 필수화할 때만 등록 전용 로컬 STT를 추가한다.
  3. Anti-spoof/liveness: 스피커 재생, 녹음 재생, 합성 음성을 별도 모델과 challenge로 차단한다.
  4. 현장 threshold: 거리, 팬, TV, 사용자 특성별 수집 데이터로 FAR/FRR을 측정한다.
  5. 장기 안정성: 취소·재시도 100회에서 AudioRecord, buffer, coroutine, native model 누수를 본다.

STT 문장 일치는 음성 품질 검사와 같지 않고, 일반 VAD/SNR은 liveness를 보장하지 않는다. 세 기능을 하나의 통과 조건으로 뭉치지 않고 독립 지표와 실패 코드로 운영해야 한다.

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date