← Docs hub

Speaker Identity A2A Implementation Roadmap

이 문서는 화자 분리/인식 PoC를 제품 경로와 Cloud A2A Task Manager에 연결하기 위한 구현 로드맵이다.

앞선 문서들이 “현재 구조 분석”과 “확장 설계”라면, 이 문서는 후속 개발자가 실제 작업을 티켓으로 나눌 때 사용할 실행 순서다.

Implementation Roadmap

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

주의:

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": "거실 청정하고 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()

적용 위치:

정책 예시:

기능 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

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