Speaker Enrollment Service Phase 1 Plan
이 문서는 화자 인식 PoC를 제품 서비스 1단계로 전환할 때, 런처앱의 음성 등록 화면과 온디바이스 AI 모듈을 어떻게 나누어야 하는지 정리한다.
핵심 결론은 런처앱은 등록 UX와 녹음 세션을 담당하고, 실제 화자 등록 판단과 embedding 저장은 온디바이스 AI 모듈이 담당하는 하이브리드 구조다.
0. 현재 코드 발견과 단계 조정
2026-08-06 MR6 SKMLauncher2와 ondevice 현재 코드를 다시 대조한 결과,
ondevice ForegroundService에는 이미 overlay_speaker_manage,
overlay_speaker_register, STT 녹음 정지/복구, WAV 저장, embedding 등록
경로가 구현되어 있다. 반면 PUI/Launcher에서 이 내부 화면을 여는 보호된
entrypoint는 없다.
따라서 이 문서의 Launcher-native UX는 장기 제품형 목표로 유지하되, 첫 통합은 다음처럼 단계화한다.
Phase 1A
PUI / setLauncherScreen
-> signature permission으로 보호된 ondevice entrypoint
-> 기존 ondevice overlay 재사용
Phase 1B
등록 controller/API와 UI 상태 분리
Phase 1C
Launcher-native 제품 UX
-> bound service/AIDL로 ondevice 등록 엔진 사용
현재 코드 근거, VitalSign이 별도 앱으로 열리는 실제 경로,
speakerRegistration screen name, Receiver/Activity 선택과 완료 상태 계약은
PUI Launcher 화자 등록 화면 연동 분석을
최신 기준으로 본다.
1. 1단계 목표
1단계는 “실험용 WAV 파일을 특정 폴더에 넣어두면 등록되는 PoC”가 아니라, 서비스 환경에서 사용자가 런처앱을 통해 직접 화자를 등록/삭제/조회할 수 있는 제품 경로를 만드는 단계다.
목표:
- 런처앱에 음성 등록 화면을 분리한다.
- 사용자가 등록 버튼을 누르면 녹음, 품질 확인, 등록 결과 확인까지 하나의 UX로 제공한다.
- 등록 엔진, embedding 생성, speaker_id 관리, accepted source sample 보관/삭제 정책은 온디바이스 AI 모듈이 책임진다.
- 1단계부터
speaker_context와 후속 개인화/권한/바이탈 연동으로 확장 가능한 ID 구조를 만든다.
비목표:
- 1단계에서 Google 계정, 바이탈 Face ID, 앱 계정과 자동 연결하지 않는다.
- 1단계에서 음성만으로 고위험 권한을 자동 부여하지 않는다.
- 1단계에서 raw voice를 Cloud로 전송하지 않는다.
- 1단계에서 화자 감정/상태 추론까지 제품 기능으로 열지 않는다.
2. 권장안: 런처앱 UX + AI 모듈 등록 엔진
| 영역 | 런처앱 책임 | 온디바이스 AI 모듈 책임 |
|---|---|---|
| 화면 | 등록 안내, 동의, 녹음 진행률, 재녹음 안내, 등록 결과 표시 | 화면 직접 소유하지 않음 |
| 녹음 | 마이크 권한과 녹음 세션 시작/중지 UX | 입력 파일/FD/PCM 품질 검사 |
| 품질 판단 | 사용자에게 재시도 사유 표시 | 무음, 짧은 발화, clipping, 다화자/겹침, 음량/거리 기준 판단 |
| 등록 | 등록 요청 호출 | embedding 추출, speaker_id 생성, 등록/저장 |
| 저장 | 임시 raw audio를 넘긴 뒤 삭제 요청 또는 전달 완료 | embedding/profile metadata 영속화, accepted source sample 보관/삭제 |
| 조회/삭제 | 등록 화자 목록 UI, 삭제 버튼 | 등록 목록 제공, embedding/profile/history 삭제 |
| 확장 | 사용자 이름/앱 계정 연결 UI 후보 | speaker_context, Person Profile link 후보 제공 |
이 구조가 좋은 이유:
- 품질 실패를 등록 시점에 바로 사용자에게 돌려줄 수 있다.
- 런처앱이 voiceprint/embedding 내부 구조를 몰라도 된다.
- raw WAV가 런처앱 로컬 저장소에 오래 남는 리스크를 줄이고, accepted sample은 AI 모듈의 관리 저장소에서만 보관할 수 있다.
- 우리 모듈이
speaker_context와 동일한 기준으로 등록/식별 정책을 관리할 수 있다. - 이후 바이탈 Face ID, 계정, Google 연동은
speaker_id를 직접 쓰지 않고Person Profile에 link하는 방식으로 확장 가능하다.
3. 대안 비교
| 안 | 설명 | 장점 | 문제 | 판단 |
|---|---|---|---|---|
| A. 런처앱이 WAV만 저장 | 런처앱이 정해진 로컬 폴더에 파일만 저장하고 AI 모듈이 나중에 스캔 | 구현 시작은 빠름 | 실패가 늦게 발견됨, raw audio 잔류, 저장소/권한 결합 강함 | 비추천, debug/fallback 전용 |
| B. 런처앱이 녹음 후 AI 모듈 API 호출 | 런처앱이 녹음 파일/FD/PCM을 넘기고 AI 모듈이 등록 처리 | UX와 엔진 경계 명확, 실패 즉시 반환, 보안 관리 쉬움 | API 계약 필요 | 1단계 추천 |
| C. AI 모듈이 등록 UI까지 제공 | 우리 앱 안에 등록 화면을 직접 넣음 | 모듈 단독 테스트 쉬움 | 서비스 런처 UX와 분리, 제품 화면 일관성 낮음 | PoC/개발자 화면용 |
| D. Cloud 등록 | raw voice 또는 embedding을 Cloud로 전송해 등록 | 기기 간 동기화 가능성 | 개인정보/네트워크/법무 리스크 큼 | 1단계 제외 |
4. 컴포넌트 구조
4.1 Launcher Enrollment UI
런처앱은 사용자에게 보이는 등록 경험을 담당한다.
주요 기능:
- 등록 시작 화면
- 음성정보 이용 목적 및 삭제 가능 안내
- 3~5회 샘플 녹음 UX
- “조금 더 가까이 말해 주세요”, “주변 소음이 큽니다”, “한 명씩 말해 주세요” 같은 재시도 안내
- 등록 성공/실패 표시
- 등록 화자 이름 수정
- 등록 화자 목록/삭제 화면
런처앱이 직접 결정하지 말아야 하는 것:
- speaker embedding 계산
- speaker_id 생성 규칙
- threshold/pass/fail 기준
- raw audio 보존 기간
- 등록된 화자의 실제 식별 가능 여부
4.2 Speaker Enrollment Service
온디바이스 AI 모듈은 런처앱에 명시적 등록 API를 제공한다.
권장 형태:
- 우선순위 1: Bound Service 또는 AIDL
- 우선순위 2: ContentProvider를 통한 파일 전달 + Service API
- 비추천: Broadcast 단독 등록
이유:
- 등록은 요청/응답/진행률/취소/에러코드가 필요한 stateful workflow다.
- Broadcast는 단방향 이벤트에는 맞지만 등록 상태 추적에는 약하다.
- AIDL/Bound Service는 signature permission과 호출자 검증을 붙이기 쉽다.
4.3 Speaker Registration Engine
현재 PoC 근거:
SpeakerIdentificationEngine.kt
- registerSpeakerFromWavPaths()
- registerSpeakerFromWavPath()
- identifyFromSamples()
- searchEmbeddingWithScoreRaw()
DiarizationEngine.kt
- diarize(samples)
- DiarizationSegment(startSec, endSec, speakerId)
서비스 1단계에서 추가로 필요한 것:
- 등록 요청 DTO
- 등록 결과 DTO
- 품질 실패 사유 enum
- speaker_id 생성기
- embedding/profile 저장소
- temporary raw 삭제와 accepted source sample 보관/삭제 정책
- 등록 목록 조회/삭제 API
5. 기획/개발 추가 고려사항
화자 등록 1단계는 “녹음 버튼 하나 추가”가 아니라 런처앱 UX, 앱 간 API, AI 모듈 저장소, 개인정보 정책, QA evidence가 동시에 맞아야 한다. 아래 항목은 기획과 개발에서 빠뜨리면 후속 단계에서 재작업이 커지는 부분이다.
5.1 런처앱 UI 컴포넌트
런처앱은 서비스 사용자가 직접 보는 화면을 담당하므로 최소한 아래 컴포넌트가 필요하다.
| UI 컴포넌트 | 필요 이유 | 1단계 처리 |
|---|---|---|
| 음성 등록 진입점 | 사용자가 기능을 찾을 수 있어야 함 | 설정/프로필/개인화 영역 중 하나에 배치 |
| 기능 설명 화면 | 음성정보가 왜 필요한지 설명 | “목소리를 구분해 개인화 서비스에 사용” 수준으로 명시 |
| 동의 화면 | source sample 로컬 보관과 삭제 가능성 안내 | consent version을 API에 전달 |
| 녹음 준비 화면 | 주변 소음/한 명 발화/가까운 거리 안내 | 샘플 품질 실패율 감소 |
| 녹음 진행 UI | 3~5회 샘플 수집 상태 표시 | sample index, accepted count 표시 |
| 품질 실패 안내 | 재녹음 사유를 사용자가 이해해야 함 | qualityCode -> messageCode 매핑 |
| 등록 완료 화면 | 등록 성공과 표시 이름 확인 | display name 수정 가능 |
| 등록 화자 목록 | 등록된 voice profile 관리 | speaker list, updatedAt, 상태 표시 |
| 재등록 버튼 | 모델 변경/품질 개선/사용자 요청 대응 | 기존 speaker_id 유지 또는 신규 생성 정책 필요 |
| 삭제 버튼 | 개인정보 관리와 서비스 신뢰성 | source sample, embedding, metadata 삭제 호출 |
UI 상태는 아래처럼 최소 상태 머신으로 잡는 것이 좋다.
idle
-> consent_required
-> recording(sampleIndex)
-> sample_submitting
-> retry_required(qualityCode)
-> ready_to_commit
-> committed
-> failed(errorCode)
-> canceled
기획에서 확정해야 하는 문구:
- source sample은 Cloud로 보내지 않고 기기 내부에 저장된다는 안내
- 삭제 시 목소리 등록 정보와 재등록용 보관 샘플이 함께 삭제된다는 안내
- 재등록은 다시 녹음하거나, 보관 샘플로 embedding을 재생성하는 방식이 가능하다는 안내
- 앱 계정/바이탈 Face ID/Google 계정과의 자동 연결은 1단계 범위가 아니라는 안내
5.2 앱 간 연동 방식
런처앱과 AI 모듈은 같은 제품 안의 앱이지만, raw audio와 speaker identity를 다루므로 단순 broadcast만으로는 부족하다.
권장 구현:
| 항목 | 권장 | 이유 |
|---|---|---|
| 등록 API | Bound Service 또는 AIDL | 요청/응답, 진행률, 취소, 권한 검증 필요 |
| 파일 전달 | content:// URI 또는 ParcelFileDescriptor |
앱 private storage 직접 공유보다 안전 |
| 권한 | signature permission | 외부 앱의 등록 API 호출 차단 |
| 호출자 검증 | caller package/signature 확인 | 런처앱만 허용 |
| 진행률 | callback 또는 polling | 녹음/품질검사/commit 상태 표시 |
| 취소 | cancelEnrollment(sessionId) |
사용자가 화면을 닫거나 실패했을 때 세션 정리 |
| 음성명령 씬 잠금 | VoiceCommandSceneLock |
등록 녹음 중 WUW/F2 기반 일반 음성명령 진입 차단 |
앱 간 통신 방식 판단:
| 방식 | 적합한 영역 | 한계 | 판단 |
|---|---|---|---|
| Broadcast | “상태가 바뀌었다” 같은 단방향 알림 | 요청/응답, 진행률, 취소, 파일 전달, 권한 검증에 약함 | 단독 사용 비추천 |
| Bound Service | 같은 기기 내 앱 간 상태ful API | 타입 안정성과 버전 계약을 별도로 관리해야 함 | 1단계 단순 구현 후보 |
| AIDL | 명시적 인터페이스, 타입 계약, callback, 버전 관리 | 초기 작성량이 Bound Service보다 큼 | 제품화 기준 1순위 |
| ContentProvider | 오디오 파일/FD 전달 | workflow 제어에는 부적합 | 파일 전달 보조 수단 |
| Shared directory watch | 구현은 빠름 | race condition, 권한, cleanup, 보안 문제가 큼 | debug/fallback 외 비추천 |
결론은 제어는 AIDL 또는 Bound Service, 오디오 전달은 ContentProvider/ParcelFileDescriptor, 상태 알림은 callback/polling, broadcast는 보조 알림으로 분리하는 것이다. 화자 등록은 단순 이벤트가 아니라 세션 기반 workflow이므로 broadcast 중심 설계로 가면 나중에 lock, 취소, timeout, 재등록, 삭제, QA evidence가 모두 흔들린다.
비추천:
- 두 앱이 동일한 일반 디렉터리를 계속 watch하는 구조
- broadcast 하나로 “등록 시작/완료”만 주고받는 구조
- 런처앱이 speaker embedding이나 source sample 경로를 직접 관리하는 구조
5.3 등록 중 WUW/음성명령 씬 차단 계약
화자 등록 녹음은 일반 음성명령과 다른 경로다. 일반 명령은 WUW/F2 이후 acceptRecording() -> STT -> LLM -> TTS 흐름으로 들어가지만, 화자 등록은 런처앱 화면에서 사용자가 녹음 버튼을 눌러 샘플을 제출하는 흐름이다.
따라서 등록 중에는 아래 계약이 필요하다.
| 상태 | 처리 |
|---|---|
| 등록 준비/녹음/샘플 제출 중 | 일반 WUW/F2 이벤트가 들어와도 음성명령 씬으로 진입하지 않는다. |
| 등록 중 F2/WUW 발생 | start ring, acceptRecording(), STT 시작, LLM 호출, TTS 응답을 수행하지 않는다. |
| 등록 중 F3/VAD 이벤트 발생 | 등록 세션의 녹음 품질 판단에만 사용하거나 무시한다. 일반 명령 종료 신호로 처리하지 않는다. |
| 등록 완료 | commitEnrollment() 이후 lock을 해제하고 일반 음성명령을 다시 허용한다. |
| 등록 취소/실패 | 세션 temp 파일을 정리하고 lock을 해제한다. |
| 앱/기기 재시작 | stale lock과 timeout session을 cleanup worker가 정리한다. |
권장 계약:
data class VoiceCommandSceneLock(
val ownerSessionId: String,
val ownerPackage: String,
val reason: String = "SPEAKER_ENROLLMENT",
val startedAt: Long,
val expiresAt: Long
)
interface SpeakerEnrollmentApi {
fun acquireVoiceCommandSceneLock(lock: VoiceCommandSceneLock): Boolean
fun releaseVoiceCommandSceneLock(ownerSessionId: String): Boolean
fun getVoiceCommandSceneLockState(): VoiceCommandSceneLock?
}
실제 구현에서는 startEnrollment() 성공 시 AI 모듈이 내부적으로 lock을 잡고, commitEnrollment(), cancelEnrollment(), timeout, process restart recovery에서 반드시 release하는 방식이 단순하다. 별도 API는 런처앱과 상태를 명시적으로 맞추기 위한 계약으로 둔다.
AI 모듈 내부에서 필요한 처리:
ForegroundService또는 WUW/F2 이벤트 수신부에서VoiceCommandSceneLock상태를 먼저 확인한다.- lock이 active면 일반
ACCEPT_RECORDING경로로 보내지 않는다. - lock active 상태에서는
setLlmStatus, start/end ring, Cloud fallback, local LLM inference가 시작되지 않아야 한다. - 등록 UI가 떠 있는 동안 사용자가 “하이 나무”를 말해도 일반 명령 응답을 하지 않는다.
- lock release는 idempotent해야 한다. 중복 release가 들어와도 오류로 사용자에게 노출하지 않는다.
- timeout은 반드시 둔다. 런처앱 crash나 화면 이탈 후 영구적으로 음성명령이 막히면 안 된다.
기획/UX에서 필요한 결정:
- 등록 중 WUW를 말했을 때 완전 무시할지, 화면에 “음성 등록 중입니다” 같은 시각 안내만 줄지 결정한다.
- TTS로 “음성 등록 중입니다”를 말하는 방식은 피하는 것이 좋다. 등록 샘플에 시스템 TTS가 섞일 수 있기 때문이다.
- 등록 화면이 background로 내려가면 자동 취소할지, 일정 시간 유지할지 결정한다.
- 일반 음성명령보다 등록 세션을 우선할지, 긴급 정지 같은 예외 명령만 허용할지 결정한다.
5.4 AI 모듈 개발 항목
AI 모듈 쪽 개발 항목은 아래처럼 나누는 것이 적절하다.
| 개발 항목 | 설명 | 우선순위 |
|---|---|---|
SpeakerEnrollmentApi |
start/submit/commit/cancel/list/delete 계약 | Must |
VoiceCommandSceneLock |
등록 중 WUW/F2 기반 일반 음성명령 진입 차단 | Must |
SpeakerSampleImporter |
URI/FD/PCM 입력을 내부 temp 파일로 normalize | Must |
SpeakerSampleQualityGate |
silent/noise/clipping/multi/overlap 판정 | Must |
SpeakerEmbeddingStore |
embedding, metadata, source sample 저장/로드/삭제 | Must |
SourceSampleArchive |
accepted sample 암호화 보관, manifest 관리 | Must |
SpeakerRebuildWorker |
재부팅/모델 변경 후 embedding rebuild | Should |
SpeakerContextBuilder |
식별 결과를 speaker_context summary로 변환 |
Should |
SpeakerAuditLogger |
민감정보 없는 등록/삭제/실패 이벤트 기록 | Should |
| debug import tool | 개발용 WAV import와 품질검사 | Could |
5.5 저장소와 재등록 정책
재부팅 이후 사용자가 다시 녹음할 수 없는 상황을 고려하면 accepted source sample을 보관해야 한다. 단, 이 보관은 “일반 raw audio 방치”가 아니라 AI 모듈이 소유하는 관리 자산이어야 한다.
필수 정책:
- accepted source sample은
speakers/{speakerId}/source_samples/아래 암호화된 파일로만 둔다. - 런처앱 cache와 세션 temp 파일은 commit/cancel/timeout 후 삭제한다.
- 재등록은 두 경로를 둔다.
Re-record: 사용자가 새 샘플을 녹음해 품질을 개선한다.Rebuild: 기존 accepted source sample로 embedding만 다시 만든다.- 모델 버전이 바뀌면
metadata.model_version을 비교해 rebuild 필요 여부를 판단한다. - 사용자가 speaker를 삭제하면 accepted source sample, embedding, metadata를 모두 삭제한다.
재등록 정책 결정 포인트:
| 정책 | 선택지 | 권장 |
|---|---|---|
| speaker_id 유지 | 재등록 후 기존 ID 유지 vs 새 ID 발급 | 같은 사용자 재등록이면 유지 |
| source sample 교체 | 기존 샘플 유지 vs 새 샘플로 교체 | 새 샘플 품질이 통과하면 교체 또는 병합 |
| rebuild 트리거 | 앱 시작 시 즉시 vs idle worker | idle worker, 긴급 시 lazy rebuild |
| 삭제 실패 | 부분 성공 허용 vs pending deletion | pending deletion 상태로 재시도 |
5.6 기획 범위 분류
| 범위 | 항목 |
|---|---|
| Must | 등록 시작, 동의, 3개 이상 샘플 수집, 품질 실패 재시도, commit, 목록, 삭제, 로컬 source sample 보관, Cloud 미전송, 등록 중 음성명령 씬 잠금 |
| Should | 재등록 버튼, source sample 기반 rebuild, 모델 버전 변경 대응, duplicate likely 안내, 등록 상태 복구 |
| Could | 사용자별 별칭 추천, 바이탈 Face ID 연결 후보 표시, 앱 계정 연결 후보, Google 계정 개인화 연결 |
| Out of scope | Cloud voiceprint 등록, 기기 간 source sample 동기화, 음성만으로 민감 권한 부여, 감정 기반 제품 기능 |
5.7 QA와 릴리스 체크리스트
| 검증 항목 | 확인 방법 |
|---|---|
| 성공 등록 | 3~5개 샘플 제출 후 speaker_id와 embedding 생성 |
| 품질 실패 | silent/noise/multi/overlap 샘플에서 재녹음 UI 표시 |
| 재부팅 복구 | 재부팅 후 등록 화자 목록과 source sample manifest 로드 |
| rebuild | embedding 삭제 또는 모델 버전 변경 후 source sample로 재생성 |
| 삭제 | speaker 삭제 후 source sample/embedding/metadata 미존재 확인 |
| Cloud payload | raw WAV/PCM, source sample, embedding, local path 미포함 확인 |
| 로그 | raw path/waveform/vector가 logcat/AILog에 남지 않는지 확인 |
| 권한 | 허용되지 않은 앱에서 API 호출 실패 |
| 등록 중 WUW/F2 | 등록 화면 녹음 중 일반 음성명령 씬이 시작되지 않는지 확인 |
| lock release | commit/cancel/error/timeout/reboot 후 일반 음성명령이 다시 가능한지 확인 |
| 취소/timeout | 중간 취소와 앱 종료 후 temp session cleanup |
| UX 문구 | 보관/삭제/Cloud 미전송 안내가 사용자에게 노출되는지 확인 |
6. API 계약 초안
6.1 등록 시작
data class SpeakerEnrollmentStartRequest(
val requestedDisplayName: String,
val locale: String,
val sampleCount: Int = 5,
val consentVersion: String,
val callerPackage: String
)
data class SpeakerEnrollmentStartResult(
val enrollmentSessionId: String,
val requiredSampleCount: Int,
val samplePolicy: SpeakerSamplePolicy
)
6.2 샘플 제출
data class SpeakerSampleSubmitRequest(
val enrollmentSessionId: String,
val sampleIndex: Int,
val audioUri: String,
val audioFormat: AudioFormatSpec,
val deleteLauncherRawAfterImport: Boolean = true
)
data class SpeakerSampleSubmitResult(
val accepted: Boolean,
val qualityCode: SpeakerSampleQualityCode,
val messageCode: String,
val acceptedSampleCount: Int,
val requiredSampleCount: Int
)
6.3 등록 완료
data class SpeakerEnrollmentCommitRequest(
val enrollmentSessionId: String,
val displayName: String
)
data class SpeakerEnrollmentCommitResult(
val result: SpeakerEnrollmentResultCode,
val speakerId: String,
val displayName: String,
val embeddingCount: Int,
val profileState: String = "voice_only",
val sourceSampleRetained: Boolean = true
)
6.4 조회/삭제
data class RegisteredSpeakerSummary(
val speakerId: String,
val displayName: String,
val createdAt: Long,
val updatedAt: Long,
val embeddingCount: Int,
val profileState: String
)
interface SpeakerEnrollmentApi {
fun startEnrollment(request: SpeakerEnrollmentStartRequest): SpeakerEnrollmentStartResult
fun submitSample(request: SpeakerSampleSubmitRequest): SpeakerSampleSubmitResult
fun commitEnrollment(request: SpeakerEnrollmentCommitRequest): SpeakerEnrollmentCommitResult
fun cancelEnrollment(enrollmentSessionId: String)
fun getRegisteredSpeakers(): List<RegisteredSpeakerSummary>
fun deleteSpeaker(speakerId: String): Boolean
fun acquireVoiceCommandSceneLock(lock: VoiceCommandSceneLock): Boolean
fun releaseVoiceCommandSceneLock(ownerSessionId: String): Boolean
fun getVoiceCommandSceneLockState(): VoiceCommandSceneLock?
}
7. 에러/품질 코드
| 코드 | 의미 | 런처앱 안내 |
|---|---|---|
OK |
샘플 수락 | 다음 샘플 녹음 |
TOO_SHORT |
발화 길이 부족 | 조금 더 길게 말해 주세요 |
TOO_SILENT |
음량 부족/거리 멂 | 조금 더 가까이에서 말해 주세요 |
CLIPPING |
소리가 찢어짐 | 조금 더 작게 말해 주세요 |
NOISE_HIGH |
주변 소음 큼 | 조용한 곳에서 다시 말해 주세요 |
MULTI_SPEAKER |
다화자 감지 | 한 명씩 말해 주세요 |
OVERLAP_SPEECH |
겹침 발화 가능성 | 다른 사람이 말하지 않을 때 다시 말해 주세요 |
DUPLICATE_LIKELY |
기존 화자와 유사 | 이미 등록된 목소리일 수 있습니다 |
ENGINE_NOT_READY |
모델/엔진 초기화 전 | 잠시 후 다시 시도해 주세요 |
STORAGE_ERROR |
저장 실패 | 등록에 실패했습니다. 다시 시도해 주세요 |
1단계에서는 DUPLICATE_LIKELY를 강제 차단하지 않고 확인 안내로 낮춰도 된다. 다만 같은 사람이 중복 등록되는 문제는 운영 품질에 영향을 주므로 score 기반 warning은 남겨야 한다.
8. 저장소와 개인정보 정책
1단계의 핵심은 녹음파일을 방치하지 않는 것이다. 다만 재부팅, 모델 버전 변경, embedding 재생성, 재등록 UX를 고려하면 “등록에 성공한 음성 샘플”은 별도 관리 자산으로 남길 필요가 있다.
따라서 녹음파일은 두 종류로 나눈다.
| 구분 | 의미 | 정책 |
|---|---|---|
| temporary raw file | 품질검사/embedding 추출 전 임시 파일 | rejected/canceled/timeout 또는 import 완료 후 삭제 |
| accepted source sample | 등록에 성공한 샘플의 보관본 | AI 모듈 private storage에 암호화 보관, 재생성/재등록/삭제 정책 적용 |
즉 “raw audio를 무조건 삭제한다”가 아니라, 런처앱/세션 임시 파일은 삭제하고, accepted source sample은 재등록과 rebuild를 위해 통제된 로컬 저장소에 보관한다가 1단계 정책이다.
1단계 기본 정책:
- Cloud 전송 금지 대상은 raw WAV/PCM, accepted source sample, embedding vector, 로컬 파일 경로다.
- temporary raw WAV/PCM은 등록 처리 후 기본 삭제한다.
- accepted source sample은 재부팅 후 재등록, embedding store 손상 복구, 모델 버전 변경 시 재추출을 위해 로컬 암호화 저장소에 보관한다.
- embedding은 로컬 암호화 저장소에 저장한다.
- 등록 metadata에는
speaker_id,display_name,created_at,embedding_count,source_sample_count,consent_version,profile_state,model_version을 둔다. - raw audio path, 원문 발화, Cloud token, 앱 계정 token은 speaker metadata와 Cloud payload에 저장하지 않는다.
- 삭제는 UI 표시 이름만 지우는 것이 아니라 embedding/profile/history 후보/source sample까지 함께 지우는 API로 설계한다.
8.1 녹음파일 lifecycle 관리
서비스에서 관리해야 하는 대상은 아래와 같다.
| 관리 대상 | 설명 | 보존 여부 |
|---|---|---|
enrollmentSessionId |
등록 세션 추적 ID | 등록 이벤트 기록에 보존 가능 |
| sample index/count | 몇 번째 샘플인지, accepted sample 수 | metadata 또는 audit에 보존 가능 |
| quality result | 무음/짧음/소음/다화자/겹침 등 실패 사유 | 민감정보 제거 후 보존 가능 |
| temporary raw file | 품질검사/embedding 추출 전 임시 파일 | commit/cancel/timeout 후 삭제 |
| accepted source sample | 등록 성공 후 재생성 가능한 보관 샘플 | 사용자 삭제 전까지 로컬 암호화 보관 |
| embedding | 등록 결과물 | 사용자 삭제 전까지 보존 |
| speaker metadata | speaker_id, 표시 이름, 동의 버전, 생성일 |
사용자 삭제 전까지 보존 |
| deletion proof | temporary raw/source sample/embedding 삭제 처리 결과 | audit event로 보존 가능 |
raw 파일 상태는 아래처럼 명시적으로 둔다. 핵심은 embedded 이후 accepted sample이 source_archived로 승격되고, 세션 임시 파일만 정리된다는 점이다.
recording -> imported -> quality_checked -> embedded -> source_archived -> session_cleaned
recording -> imported -> rejected -> raw_deleted
recording -> imported -> canceled -> raw_deleted
recording -> imported -> timeout -> raw_deleted
source_archived -> rebuild_embedding -> embedding_refreshed
deleteSpeaker -> source_deleted + embedding_deleted + metadata_deleted
권장 로컬 구조:
/data/.../speaker_enrollment/
sessions/
{sessionId}/
manifest.json
sample_001.wav.tmp
sample_001.wav.ready
sample_002.wav.tmp
sample_002.wav.ready
speakers/
{speakerId}/
embedding.bin
metadata.json
source_samples/
manifest.json
sample_001.wav.enc
sample_002.wav.enc
sample_003.wav.enc
audit/
enrollment_events.log
운영 규칙:
.tmp는 쓰는 중인 파일,.ready는 import 완료 파일로 구분한다.- AI 모듈은
.ready상태만 읽는다. commitEnrollment()성공 시 accepted sample만speakers/{speakerId}/source_samples/로 암호화 이동하고,sessions/{sessionId}전체를 삭제한다.cancelEnrollment()호출 시sessions/{sessionId}전체를 삭제한다.- 앱/기기 재시작 후
timeout이 지난 session은 cleanup worker가 삭제한다. - temporary raw 삭제 실패는 audit에 남기고 재시도 대상으로 둔다.
- accepted source sample 삭제 실패는 speaker 삭제 실패로 처리하거나 pending deletion 상태로 남긴다.
- 재부팅 후에는
speakers/{speakerId}/source_samples/manifest.json을 기준으로 embedding 재생성 가능 여부를 판단한다. - 모델 버전 변경 시 source sample에서
rebuild_embedding을 수행하고, 성공 후metadata.model_version과embedding_count를 갱신한다. - debug build에서 세션 raw 파일 보존이 필요하면 feature flag와 TTL을 강제한다.
8.2 Cloud 보관 정책
1단계에서 raw 녹음파일을 Cloud로 보내는 것은 제외한다. 단, accepted source sample은 재등록과 embedding rebuild를 위해 온디바이스 AI 모듈 private storage에만 보관한다.
| 데이터 | Cloud 전송 | 사유 |
|---|---|---|
| raw WAV/PCM | 금지 | 개인 식별 가능성이 높은 원본 음성 |
| accepted source sample | 금지 | 등록 재생성용 원본이므로 Cloud 노출 금지 |
| embedding vector | 금지 | voiceprint 자체로 민감정보 |
| local file path | 금지 | 내부 저장소 구조 노출 |
| speaker_id | 가능 | 개인화/권한 판단용 summary key |
| speaker_state/confidence | 가능 | speaker_context 판단 근거 |
| profile_state | 가능 | voice_only, linked, verified 같은 상태값 |
Cloud에는 아래 수준만 전달한다.
{
"speaker_context": {
"speaker_id": "spk_001",
"speaker_state": "identified",
"speaker_confidence": 0.72,
"profile_state": "voice_only"
}
}
권장 저장 구분:
| 데이터 | 저장 위치 | 보존 정책 |
|---|---|---|
| 임시 raw audio | 런처앱 cache 또는 AI 모듈 import temp | commit/cancel 후 삭제 |
| accepted source sample | AI 모듈 private encrypted storage | 사용자 삭제 전까지, rebuild/re-registration 용도 |
| embedding | AI 모듈 private storage | 사용자 삭제 전까지 |
| speaker metadata | AI 모듈 private DB/file | 사용자 삭제 전까지 |
| registration logs | 민감정보 제거 후 event log | 운영 정책에 따름 |
| Cloud payload | raw audio/embedding 제외 | speaker_context summary만 |
9. 1단계 사용자 흐름
- 사용자가 런처앱에서 “음성 등록”을 누른다.
- 런처앱이 음성정보 이용 목적과 삭제 가능성을 안내한다.
- 사용자가 동의한다.
- 런처앱이 AI 모듈에
startEnrollment()를 호출한다. - 사용자가 안내 문장을 3~5회 말한다.
- 런처앱은 각 샘플을 AI 모듈에
submitSample()로 전달한다. - AI 모듈은 품질을 검사하고 accepted/retry를 반환한다.
- 충분한 샘플이 모이면
commitEnrollment()를 호출한다. - AI 모듈은 embedding을 저장하고
speaker_id를 반환한다. - 런처앱은 등록 완료 화면과 삭제/이름 변경 진입점을 제공한다.
10. 서비스 1단계 완료 기준
| 항목 | 완료 기준 |
|---|---|
| 런처앱 연동 | 런처앱에서 등록 시작/샘플 제출/완료/취소/삭제 호출 가능 |
| 품질 검사 | 무음/짧음/소음/다화자/겹침/저음량 케이스가 코드로 반환 |
| 등록 엔진 | 3개 이상 accepted sample로 speaker_id 생성 및 embedding 저장 |
| 식별 연계 | 등록 후 일반 발화에서 speaker_context.speaker_id 후보 생성 |
| 삭제 | speaker_id 삭제 시 embedding/profile metadata 삭제 |
| raw audio 정책 | 임시 raw audio 삭제, accepted source sample 로컬 암호화 보관/삭제 |
| 권한 | 런처앱 signature permission 또는 동등한 호출자 검증 |
| 로그 | raw audio path/embedding 미노출, 결과 코드 중심 로그 |
| 테스트 | 등록 성공/품질 실패/삭제/재시작 후 로드/식별 smoke test |
11. 제품화 전 추가 보강 체크리스트
현재 1단계 구조는 “런처앱 UX + 온디바이스 AI 모듈 등록 엔진 + 로컬 보관 + Cloud summary 전송 금지”까지 정리되어 있다. 다만 제품화 직전에는 아래 항목을 별도 게이트로 닫아야 한다. 이 항목들은 기능 화면을 만드는 작업보다 덜 눈에 띄지만, 나중에 모델 변경, 재부팅, 삭제, SQE 검증, 개인정보 검토에서 문제가 되는 영역이다.
| 영역 | 반드시 정리할 내용 | 판단 |
|---|---|---|
| Embedding 파일 계약 | embedding.bin의 포맷, dim, count, endian, 모델 fingerprint, checksum, 암호화 방식 |
Must |
| Threshold/품질 보정 | 등록 pass/fail 기준, duplicate likely 기준, 소음/거리/짧은 발화별 threshold | Must |
| 복구 상태 | pending deletion, rebuild required, source missing, stale voice lock, migration failed 상태 | Must |
| 개인정보 운영 | backup 제외, factory reset, consent revoke, 사용자 삭제 시 source/embedding/history 동시 삭제 | Must |
| 관측성 | 등록 결과 코드, 품질 실패 분포, lock timeout, rebuild 결과 로그 | Must |
| 책임 경계 | 런처앱 UI owner, AI 모듈 API owner, SoC/Cloud boundary owner | Must |
| 확장성 | Voice ID와 Face ID/앱 계정/Google 계정 link 후보 관리 | Should |
11.1 Embedding 파일 계약
현재 PoC는 임베딩을 메모리에 올려 쓰는 성격이 강하다. 제품화에서는 재부팅 후에도 등록 상태를 유지해야 하므로 embedding 저장 포맷을 명시해야 한다.
권장 metadata:
{
"speaker_id": "spk_001",
"display_name": "Yongjae",
"model_id": "embedding.onnx",
"model_version": "2026-07",
"embedding_dim": 256,
"embedding_count": 5,
"sample_rate": 16000,
"format": "float32_le",
"checksum": "sha256:...",
"encrypted": true,
"source_sample_count": 5,
"created_at": 1783420000000,
"updated_at": 1783420000000
}
정책:
- metadata만으로 화자 등록을 복구할 수는 없다.
- embedding vector를 저장하면 재부팅 후 raw audio 없이도 검색 인덱스를 복원할 수 있다.
- accepted source sample을 함께 보관하면 모델 변경이나 embedding 손상 시 재생성이 가능하다.
- 따라서 1단계 권장은
embedding 저장 + accepted source sample 암호화 보관이다. - embedding 파일 포맷이 바뀌면 migration 또는 rebuild 정책이 필요하다.
11.2 Threshold/품질 보정
화자 등록은 “등록 성공”보다 “오등록을 막는 것”이 중요하다. threshold는 단일 숫자로 끝내지 말고 품질 코드와 함께 운영해야 한다.
| 기준 | 목적 | 예시 |
|---|---|---|
| enrollment quality threshold | 등록 샘플 자체 품질 판단 | 너무 짧음, 무음, clipping, 소음 |
| speaker match threshold | 등록 화자 식별 pass 기준 | score가 기준 이상일 때 identified |
| ambiguity margin | 1등/2등 화자 점수 차이 | 점수 차이가 작으면 ambiguous |
| duplicate likely threshold | 기존 화자와 유사한 신규 등록 방지 | “이미 등록된 목소리일 수 있음” |
| environment policy | 거리/소음/팬소음 조건 | 조용한 환경에서 재시도 안내 |
운영 관점에서는 threshold를 코드 상수로만 두지 말고, 모델 버전과 함께 추적해야 한다. 모델이 바뀌면 같은 threshold라도 false accept/false reject 성격이 바뀔 수 있다.
11.3 복구 상태
제품에서는 정상 flow보다 비정상 종료 처리가 중요하다.
| 상태 | 발생 조건 | 처리 |
|---|---|---|
pending_deletion |
source sample 또는 embedding 삭제 일부 실패 | 다음 부팅/idle worker에서 재시도 |
rebuild_required |
embedding 파일 없음, 모델 버전 변경 | accepted source sample로 재생성 |
source_missing |
embedding은 있으나 source sample 없음 | 식별은 가능하되 모델 migration 불가 상태로 표시 |
lock_stale |
등록 중 앱 crash로 VoiceCommandSceneLock 유지 | timeout 후 lock 해제 |
migration_failed |
포맷 변경 또는 파일 손상 | 재녹음 안내 또는 기존 profile 비활성화 |
특히 VoiceCommandSceneLock은 등록 중 일반 음성명령 진입을 막기 때문에 stale 상태가 길게 남으면 제품 사용성이 크게 깨진다. lock에는 반드시 TTL과 ownerSessionId 검증이 필요하다.
11.4 개인정보/운영 정책
화자인식 데이터는 일반 설정값이 아니라 식별 가능한 생체성 데이터에 가깝다. 제품 문구와 삭제 정책이 기술 구현과 맞아야 한다.
필수 운영 정책:
- accepted source sample과 embedding은 Android backup 대상에서 제외한다.
- factory reset 또는 앱 데이터 초기화 시 source sample, embedding, metadata, history 후보를 모두 삭제한다.
- 사용자가 동의를 철회하면 신규 식별만 중지하는 것이 아니라 보관 데이터 삭제까지 수행한다.
- 로그에는 raw audio path, waveform, embedding vector, speaker source sample path를 남기지 않는다.
- Cloud에는
speaker_contextsummary만 전송하고, voiceprint/embedding/source sample은 전송하지 않는다.
11.5 관측성
로그가 없으면 SQE/필드에서 “등록이 잘 안 된다”를 분석할 수 없고, 로그가 과하면 개인정보 리스크가 생긴다. 따라서 결과 코드 중심으로 남긴다.
권장 event:
speaker_enrollment_started
speaker_sample_accepted
speaker_sample_rejected(qualityCode)
speaker_enrollment_committed(resultCode, embeddingCount)
speaker_enrollment_canceled(reason)
speaker_deleted(resultCode)
speaker_embedding_rebuilt(resultCode)
speaker_voice_command_lock_timeout
금지:
- raw text 전체 저장
- WAV/PCM path 저장
- embedding vector 저장
- source sample 파일명 또는 실제 사용자 이름 노출
11.6 책임 경계
제품화 전에는 담당 경계를 아래처럼 고정해야 한다.
| 책임 | Owner | 산출물 |
|---|---|---|
| 등록 화면/문구/동의 UX | 런처앱 | 화면, 문구, UX state |
| 등록 API/품질 코드 | 온디바이스 AI 모듈 | AIDL/Bound Service 계약 |
| embedding 추출/저장/삭제 | 온디바이스 AI 모듈 | SpeakerEmbeddingStore, manifest |
| 일반 음성명령 lock | 온디바이스 AI 모듈 + 런처앱 | VoiceCommandSceneLock 계약 |
| 개인정보 정책 | 제품/법무/개발 공동 | consent, 삭제, backup 제외 |
| Cloud 연동 | Cloud LLM/온디바이스 공동 | speaker_context summary 계약 |
| 실기기 검증 | QA/SQE | 등록/재부팅/삭제/lock/rebuild evidence |
정리하면, 추가로 봐야 할 핵심은 “화자를 등록할 수 있느냐”가 아니라 등록 데이터가 재부팅/삭제/모델 변경/개인정보 검토/필드 분석 상황에서도 일관되게 관리되느냐다.
12. 후속 단계와 연결
1단계 산출물은 이후 아래 단계의 기반이 된다.
| 후속 단계 | 1단계에서 준비해야 하는 것 |
|---|---|
| speaker_context 제품 적용 | speaker_id, confidence, state를 안정적으로 생성 |
| 개인화 memory | speaker_id를 person profile 후보로 연결 |
| 바이탈 Face ID link | Voice ID를 직접 face id에 묶지 않고 Person Profile link로 확장 |
| 앱 계정 link | 계정 이름과 voice_id를 1:1로 강제하지 않고 사용자 승인 기반 연결 |
| Google account link | OAuth account는 외부 identity scope로 분리 |
| 권한/가족 모델 | speaker_id별 authority level 후보 관리 |
즉 1단계의 핵심은 “목소리를 등록한다”가 아니라, 이후 서비스 확장을 깨지 않도록 Voice ID 등록 경로, 저장/삭제 정책, 런처앱 연동 계약을 제품 구조로 고정하는 것이다.