Speaker Identity A2A Implementation Roadmap
이 문서는 화자 분리/인식 PoC를 제품 경로와 Cloud A2A Task Manager에 연결하기 위한 구현 로드맵이다.
앞선 문서들이 “현재 구조 분석”과 “확장 설계”라면, 이 문서는 후속 개발자가 실제 작업을 티켓으로 나눌 때 사용할 실행 순서다.
1. 구현 원칙
| 원칙 | 설명 |
|---|---|
| additive contract | 기존 voice_context를 깨지 않고 speaker_context를 추가한다. |
| on-device authority | 화자 판정은 온디바이스가 수행하고 Cloud는 결과를 evidence로만 사용한다. |
| safety before personalization | unknown, overlap, multi-speaker 상태에서는 개인화보다 재시도/차단을 우선한다. |
| no raw voice to cloud | raw audio와 embedding은 Cloud로 보내지 않는다. |
| evidence-first rollout | 각 단계는 payload/log/unit/E2E evidence가 있어야 다음 단계로 넘어간다. |
2. 구현 티켓 분해
| 티켓 | 영역 | 목표 | 주요 파일 | 완료 증거 |
|---|---|---|---|---|
SPK-01 |
On-device | speaker context DTO 정의 | 신규 DTO 파일 또는 ForegroundService 인접 모델 |
unit 또는 log에서 기본값/상태값 확인 |
SPK-02 |
On-device | PoC 결과를 speaker context로 변환 | ForegroundService.kt, SpeakerIdentificationEngine.kt, DiarizationEngine.kt |
등록/인식 결과가 speaker_state, speaker_id, confidence로 변환 |
SPK-03 |
On-device -> Cloud | Cloud request voice_context에 speaker context 삽입 |
Cloud LLM request 생성부, ForegroundService.kt |
request log에 voice_context.speaker_context 포함 |
SPK-04 |
Cloud A2A | speaker context 정규화 | gemini/a2a/runtime/session_state.py |
unit test에서 누락/invalid/default 처리 확인 |
SPK-05 |
Cloud Memory | speaker-scoped memory update | gemini/a2a/runtime/memory_context.py |
speaker별 read/write 분리, unknown no-write 확인 |
SPK-06 |
Cloud TaskManager | side-effect gate와 workflow owner 정책 | gemini/a2a/runtime/task_manager.py, workflow_state.py |
unknown/multi/overlap block, 동일 speaker continuation 확인 |
SPK-07 |
Identity Service | Voice ID, Vital Face ID, Account, Google 계정 연결 모델 | Person Profile/Identity Link 설계 | linked/verified/revoked 상태와 동의 범위 정의 |
SPK-08 |
QA/E2E | 실기기 evidence 수집 | logcat, Cloud request/response log | 등록->인식->Cloud->TaskManager 전체 trace |
SPK-07은 단순 문서 작업이 아니라 제품 architecture 결정이다. Voice ID를 계정/얼굴 ID에 직접 붙이는 방식과 Person Profile link 방식은 이후 DB, 앱 UI, 개인정보 동의, Cloud payload가 모두 달라진다.
3. 온디바이스 구현 상세
3.1 SPK-01: SpeakerContext DTO
권장 Kotlin 모델:
data class SpeakerContext(
val contractVersion: String = "speaker-context-v1",
val speakerId: String = "",
val displayName: String = "",
val speakerConfidence: Float = 0f,
val speakerState: SpeakerState = SpeakerState.UNKNOWN,
val speakerCount: Int = 0,
val overlapRatio: Float = 0f,
val unknownRatio: Float = 0f,
val maxScore: Float = 0f,
val safetyGate: SpeakerSafetyGate = SpeakerSafetyGate.REPROMPT,
val personalizationAllowed: Boolean = false,
val authorityLevel: String = "unknown",
val source: String = "ondevice_sherpa_poc"
)
권장 상태:
identified
unknown
ambiguous
multi_speaker
overlap
far_speech
disabled
주의:
displayName은 UI/TTS 용도이고, Cloud 정책의 primary key는speakerId여야 한다.- 초기 단계에서는
speakerId를 등록 폴더명 기반으로 만들 수 있지만, 제품화 단계에서는 stable id와 삭제 정책이 필요하다. contractVersion은 Cloud 정규화에서 필수 확인해야 한다.
3.2 SPK-02: PoC 결과 변환 Adapter
현재 근거:
ForegroundService.kt
- speakerEnrollmentBufferLock
- pendingSpeakerPrefix
- lastIdentifiedSpeakerName
- idAcceptThreshold = 0.50f
- extensionFeatures.enableSpeakerStability = true
SpeakerIdentificationEngine.kt
- registerSpeakerFromWavPath()
- identifyFromSamples()
- identifyFromSegment()
- searchEmbeddingWithScoreRaw()
DiarizationEngine.kt
- diarize(samples)
- DiarizationSegment(startSec, endSec, speakerId)
SpeakerStabilityFeature.kt
- sessionBoostScore = 0.04f
- recentBoostScore = 0.02f
- sessionDecayMs = 20_000L
변환 규칙:
| 입력 | 변환 |
|---|---|
| accepted identification | speaker_state=identified, safety_gate=pass, personalization_allowed=true |
| score below threshold | speaker_state=unknown, safety_gate=reprompt |
| multiple diarization speakers | speaker_state=multi_speaker, safety_gate=block 또는 reprompt |
| overlap high | speaker_state=overlap, safety_gate=reprompt |
| distance prompt | speaker_state=far_speech, safety_gate=reprompt |
3.3 SPK-03: Cloud Request 삽입
삽입 위치는 Cloud LLM request를 구성하는 경로다.
원칙:
- 기존
recognized_text,asr_confidence,audio_locale,memory_context,device_context를 유지한다. voice_context.speaker_context를 새 field로 추가한다.- log에는 raw audio, embedding, WAV path를 남기지 않는다.
요청 예시:
{
"recognized_text": "거실 청정하고 10분 뒤에 돌아와",
"voice_context": {
"recognized_text": "거실 청정하고 10분 뒤에 돌아와",
"asr_confidence": 0.94,
"audio_locale": "ko-KR",
"speaker_context": {
"contract_version": "speaker-context-v1",
"speaker_id": "spk_001",
"display_name": "아빠",
"speaker_confidence": 0.72,
"speaker_state": "identified",
"speaker_count": 1,
"overlap_ratio": 0.0,
"unknown_ratio": 0.0,
"max_score": 0.72,
"safety_gate": "pass",
"personalization_allowed": true,
"authority_level": "normal",
"source": "ondevice_sherpa_poc"
}
}
}
4. Cloud A2A 구현 상세
4.1 SPK-04: normalize_voice_context() 확장
현재 근거:
gemini/a2a/runtime/session_state.py
normalize_voice_context()
현재 기본화:
recognized_text
asr_confidence
audio_locale
slot_state
handoff_chain
replan_reason
selected_flow_id
owner_selection
추가 권장:
def _normalize_speaker_context(value):
speaker = value if isinstance(value, dict) else {}
return {
"contract_version": str(speaker.get("contract_version") or "speaker-context-v1"),
"speaker_id": str(speaker.get("speaker_id") or ""),
"display_name": str(speaker.get("display_name") or ""),
"speaker_confidence": _safe_float(speaker.get("speaker_confidence"), 0.0),
"speaker_state": _allowed_state(speaker.get("speaker_state")),
"speaker_count": _safe_int(speaker.get("speaker_count"), 0),
"overlap_ratio": _safe_float(speaker.get("overlap_ratio"), 0.0),
"unknown_ratio": _safe_float(speaker.get("unknown_ratio"), 0.0),
"max_score": _safe_float(speaker.get("max_score"), 0.0),
"safety_gate": _allowed_gate(speaker.get("safety_gate")),
"personalization_allowed": bool(speaker.get("personalization_allowed", False)),
"authority_level": str(speaker.get("authority_level") or "unknown"),
"source": str(speaker.get("source") or "unknown"),
}
Cloud는 값이 없으면 unknown으로 낮춰야 한다. 값이 없다고 제품 경로를 실패시키면 기존 기능 회귀가 생긴다.
Identity 확장 필드는 아래 기본값으로 정규화한다.
| 필드 | 기본값 | 설명 |
|---|---|---|
person_id |
"" |
없으면 장기 개인화/바이탈/외부계정 접근 금지 |
identity_link_state |
unlinked |
linked/verified만 민감 정보 접근 후보 |
consent_scope |
{} |
domain별 동의 없으면 false로 취급 |
linked_vital_face_ids |
[] |
직접 raw ID 노출 대신 scope token 검토 |
external_identity_scope |
[] |
provider token이 아니라 scope 요약만 |
latency_budget_ms |
500 |
timeout/fallback 정책 판단 |
4.2 SPK-05: Speaker-scoped Memory
현재 근거:
gemini/a2a/runtime/memory_context.py
normalize_memory_context()
build_memory_update()
추가 권장:
{
"write_candidates": {
"short_term_pair": {
"speaker_id": "spk_001",
"speaker_state": "identified",
"write_policy": "speaker_scoped",
"user_text": "...",
"assistant_text": "...",
"route_family": "ODL",
"workflow_id": "wf_001"
},
"profile_candidates": []
}
}
정책:
| speaker 상태 | memory read | memory write |
|---|---|---|
| identified + allowed | speaker scoped read | speaker scoped write |
| identified + restricted | limited read | no profile write |
| unknown | anonymous/session read only | no write |
| ambiguous | no personal read | no write |
| multi_speaker/overlap | no personal read | no write |
4.3 SPK-06: TaskManager Side-effect Gate
현재 근거:
gemini/a2a/runtime/task_manager.py
build_device_task_requests()
execute_task_plan()
적용 위치:
- device task request를 만들기 전 speaker safety를 확인한다.
- side-effect가 큰 기능은
safety_gate=pass가 아니면confirmation_required또는blocked로 낮춘다. - active workflow가 있을 때
workflow_owner_speaker_id와 현재speaker_id가 다르면 takeover 정책을 적용한다.
정책 예시:
| 기능 | identified | unknown | multi/overlap |
|---|---|---|---|
| 단순 질의 | 허용 | 허용 | 제한적 허용 |
| 화면 조회 | 허용 | 허용 | 허용 가능 |
| 이동/청정/스케줄 | 허용 | 확인 질문 | 차단 또는 재시도 |
| 보안/개인정보 | 권한 확인 후 허용 | 차단 | 차단 |
| active workflow continuation | owner 동일 시 허용 | 확인 질문 | 차단 |
5. 권장 구현 순서
Phase 1: On-device DTO와 adapter만 추가
Phase 2: Cloud request에 speaker_context 추가, Cloud는 무시해도 기존 기능 유지
Phase 3: Cloud normalize_voice_context에서 speaker_context 정규화
Phase 4: memory_update에 speaker scope 추가, unknown no-write 적용
Phase 5: TaskManager side-effect gate 적용
Phase 6: Person Profile / Identity Link 설계 반영
Phase 7: 실기기 E2E evidence 수집
Phase 8: threshold tuning과 privacy/delete 정책 확정
이 순서를 지키는 이유는 rollback 위험을 줄이기 위해서다. Cloud가 speaker_context를 아직 사용하지 않더라도 payload에 추가하는 것은 additive change다. 반대로 TaskManager gate를 먼저 넣으면 기존 기능 실행이 막히는 회귀가 생길 수 있다.
6. 테스트 매트릭스
| 케이스 | 입력 상태 | 기대 결과 |
|---|---|---|
| 등록 화자 단독 발화 | identified, confidence high |
개인화/TaskManager 실행 가능 |
| 미등록 화자 | unknown |
일반 질의 가능, side-effect는 확인 질문 |
| 동시 발화 | overlap |
재시도 안내, memory write 금지 |
| 3명 이상 | multi_speaker |
실행 차단 또는 재시도 |
| 동일 workflow 후속 발화 | same speaker_id |
workflow continuation 허용 |
| 다른 화자 takeover | different speaker_id |
takeover 확인 질문 |
| 아이/게스트 권한 | authority_level=restricted |
보안/개인정보/스케줄 제한 |
7. 릴리즈 게이트
| 게이트 | 통과 기준 |
|---|---|
| Contract gate | request/response fixture에 speaker_context가 포함되고 backward compatible |
| On-device gate | 등록/인식/unknown/multi 상태가 DTO로 변환됨 |
| Cloud gate | speaker_context 누락/invalid/default unit test 통과 |
| Memory gate | speaker_id별 memory가 섞이지 않음 |
| Task gate | side-effect 기능이 speaker safety에 따라 pass/confirm/block 분기 |
| Identity gate | Voice ID가 Account/Face ID와 직접 1:1 강제 결합되지 않고 Person Profile link로 관리됨 |
| Privacy gate | raw audio/embedding cloud 미전송, 삭제/동의 정책 문서화 |
| E2E gate | 실제 디바이스에서 trace log 확보 |
8. 후속 의사결정
아래 항목은 구현 전에 소유자 합의가 필요하다.
| 항목 | 선택지 | 권장 |
|---|---|---|
| speaker_id 생성 | 이름 기반, UUID 기반, 계정 기반 | UUID 기반 stable id |
| embedding 저장 | plain file, encrypted file, TEE/keystore 연계 | 최소 encrypted file |
| Cloud 전송 범위 | speaker_id only, display_name 포함, score 포함 | speaker_id + state + score, raw embedding 금지 |
| unknown side-effect | 허용, 확인 질문, 차단 | 이동/청정은 확인 질문, 보안/개인정보는 차단 |
| multi-speaker | 최우수 점수 선택, 재시도, 사용자 선택 | 재시도 우선 |
| memory write | 항상 write, identified만 write, user opt-in | identified + consent only |