등록 App·AI-Agent ↔ DeviceAgent 인터페이스 정의
실제 구현 요청은 경계별로 분리했다: DeviceAgent ↔ AI-Agent · DeviceAgent ↔ 등록 App · 담당별 범위. 이 페이지는 기존 전체 필드·Sample 참고 자료이며, 중복되는 현재 동작과 책임 범위는 신규 요청서를 먼저 본다.
연동 시 함께 확인: 단계별 완료 조건, 단절·타임아웃·재부팅 복구, 삭제 범위와 부분 실패, 공통 시험표. 제품 제안 규칙은 현재 Binder의 완료 보장이나 신규 enum이 아니다.
관심사 관리: 저장 주체는 서버다. getVoicePersonalization / voicePersonalizationResult 전체 명세·구조도. AI-Agent가 조회를 시작하고 DeviceAgent는 IoTAgent로 전달한다. 등록 App은 관심사 저장·조회 당사자가 아니다. 아래 구형 set/getVoiceProfileInterests는 로컬 DB 처리 없이 실패를 반환한다.
전체 명세: 26개 메서드별 입력·결과·예제 · 시간·취소·재연결 한계. 앞부분은 공통 규칙이며, 12절부터 각 메서드를 독립적으로 확인할 수 있다.
DeviceAgent·IotAgent 개발자가 기존 Registration App·AI-Agent와 연결할 때 구현할 약속이다. 역할 소개가 아니라 호출 진입점, Bundle 직렬화, 요청별 입력, 결과와 callback 경로를 정의한다. 현재 구현과 맞지 않는 제품 확장은 마지막 절에 별도로 표시한다.
- 모바일·관제 담당: HTTPS·MQTT 연동 명세
- 전체 상태 enum·녹음 오류: 기존 프로토콜 규격
- 구조도: 개발 분담 구조도
기기 내부 요청은 DeviceAgent를 거친다. 등록 App은 녹음을 요청하고 화면을 표시하며, AI-Agent Service가 실제 녹음과 검증을 수행한다. 함수의 반환값은 요청 수락 여부이고, 진행 상태와 결과는 별도 callback으로 돌아온다. 아래 2절에서 callback 수신 대상을 확인한다.
필요한 인터페이스 찾기
| 구현할 작업 | 요청 명세 | 결과·다음 처리 |
|---|---|---|
| 처음 Binder 연결 | 1. 공통 함수, 2. callback 대상 | 연결 후 getVoiceProfileEnrollmentState |
| 등록 화면 열기 | 5. 원격 Activity 실행 | 화면에서 시작한 요청은 AI-Agent로 중계 |
| 녹음 시작·재시도·취소 | 4. 등록 제어 입력 | 6. 상태 Bundle |
| 생성 프로필 받기 | 7. commit 입력과 응답 | 저장 후 voiceProfileCommitResult |
| 목록·삭제 | 8. 관리 요청과 결과 | 결과 method에 맞춰 IotAgent로 전달 |
| 서버 관심사 조회 | getVoicePersonalization | AI-Agent 요청을 IoTAgent로, 서버 결과는 AI-Agent로 전달 |
| 호출 코드 작성 | 10. Kotlin 예제 | 11. 구현 확인 |
표 읽는 법: 요청 표는 상대가 보내는 값, 결과 표는 내가 반환하거나 중계할 값이다. 동일한 method가 왕복하는 것이 아니라 요청 method와 결과 method가 서로 다를 수 있다.
구현 상대별 명세
DeviceAgent는 요청을 검사하고 method에 따라 수신 모듈을 선택한다. 녹음과 프로필 생성은 AI-Agent가 수행하며, DeviceAgent는 상태를 등록 App·IoTAgent에 분배한다. 그림의 번호는 대표 흐름이지 모든 요청을 직렬 실행한다는 뜻은 아니다. 상태 이벤트는 반복되며 취소는 진행 중에도 들어올 수 있다.
| 구현 경계 | 직접 주고받는 값 | 상세 페이지 |
|---|---|---|
| 등록 App ↔ DeviceAgent | 실행 Intent, 시작·재녹음·취소 Bundle, 상태 callback | 등록 App 인터페이스 |
| AI-Agent ↔ DeviceAgent | 실행 요청, 상태, 임베딩 commit, 저장 ACK, 관리 결과 | AI-Agent 인터페이스 |
먼저 해당 경계의 요청·응답 쌍을 구현하고, 아래 전체 메서드 명세는 필드 검증·예외 확인용으로 사용한다.
1. 연결할 함수
새로운 기능별 AIDL을 만드는 방식이 아니다. 기존 DeviceControlProxy와 공통 Binder 함수를 사용한다.
| 구분 | 실제 호출/수신 | 의미 |
|---|---|---|
| 연결 | DeviceControlProxy.connect(context, callback, true, callbackClassName) |
기존 Proxy에 연결하고 callback 등록 |
| 요청 | DeviceControlProxy.getService().sendModuleCommand(input, output) |
input은 Bundle, 반환값은 Int |
| 수신 | IDeviceControlProxyCallback.onModuleCallback(outdata: Bundle?) |
요청·상태·최종 결과를 비동기로 받음 |
| 중계 | mMainApi.onModuleCallbackLoop(caller, new Bundle(input)) |
DeviceAgent가 대상 모듈에 전달 |
| 연결 해제 | DeviceControlProxy.disconnect() |
등록 App 클라이언트 종료 처리 |
화자인식 요청의 output Bundle을 최종 결과로 읽으면 안 된다. 결과는 별도 callback이다.
현재 분기의 반환값은 0 전달/수락, -1 요청·결과 검증 또는 저장 실패,
-2 일부 분기의 호출자 거절, -3 원격 등록 Activity 실행 실패다.
관리 요청의 호출자 거절도 -1일 수 있어 전역 오류 enum으로 해석하지 않는다.
반환값 0은 등록·삭제·Cloud 저장 완료가 아니다.
2. 호출자와 callback 배선
| 요청/이벤트 발신자 | DeviceAgent 처리 | callback 대상 |
|---|---|---|
| Registration App: 등록 제어 | 동일 Bundle 중계 | CALLER_LLM=1: AI-Agent |
| IotAgent: 원격 start/cancel | 등록 Activity 실행/새 Intent 전달 | Activity가 다시 등록 제어 요청 |
| IotAgent: 목록·삭제 | 동일 Bundle 중계 | CALLER_LLM=1 |
| AI-Agent: getVoicePersonalization | 요청 Bundle 중계 | CALLER_IOT=2 |
| IotAgent: voicePersonalizationResult | 결과 Bundle 중계 | CALLER_LLM=1 |
AI-Agent: voiceProfileStateChanged |
두 대상에 복사 전달 | CALLER_VOICE_PROFILE_REGISTRATION=8, CALLER_IOT=2 |
AI-Agent: commitVoiceProfile |
전달 대기 저장 후 결과 생성 | CALLER_LLM=1로 voiceProfileCommitResult |
호출자 검증 패키지는 Registration App com.sk.airbot.voiceprofileregistration,
IotAgent com.sk.airbot.iotagent, AI-Agent com.skmagic.ondeviceai.agent다.
현재 코드는 Binder UID에 연결된 패키지를 검사한다. 제품 서명·권한 설정 검증도 별도로 수행한다.
ModuleCmdCallbackPolicy에 요청·응답 상수를 등록해야 하며 라우터만 추가해서는 callback이 도착하지 않는다.
3. 공통 Bundle
아래 '필수'는 호출자가 보내야 할 항목이다. 수신 코드의 일부 기본값은 누락 허용 계약과 구분한다.
| 키 | Android 타입/쓰기 | 필수 | 값 |
|---|---|---|---|
method |
String / putString | 필수 | 아래 메서드 이름 그대로 |
contractVersion |
String / putString | 필수 | 등록·프로필: voice-profile-v2, 서버 관심사 조회: voice-personalization-v1 |
requestId |
String / putString | 필수 | 비어 있지 않은 작업 ID. 결과에서 그대로 반환 |
issuedAtMs |
Long / putLong | 등록 App이 전송 | Unix ms. 현재 Service의 만료 판정 필드는 아님 |
expiresAtMs |
Long / putLong | 원격 요청에 포함 | DeviceAgent 원격 start/cancel 만료 검사. 생략 시 현재 구현은 무기한 |
Long 필드를 Int/String으로 넣지 않는다. Bundle.getLong()과 JSON 정수 변환을 맞춘다.
알 수 없는 method/버전/잘못된 타입은 실행하지 않는다. 현재 일부 경로는 음수 반환 또는 로그 후 종료하므로
제품에서는 IotAgent가 호출 실패를 Backend에 알려야 한다.
4. 등록 제어 요청
모두 voice-profile-v2. 공통 Bundle 외 추가 필드는 아래와 같다.
결과는 별도 표시가 없으면 voiceProfileStateChanged다.
| method | 추가 입력 | 처리 |
|---|---|---|
getVoiceProfileEnrollmentState |
없음 | requestId 상태 조회. 없는 세션은 WAITING_START |
startVoiceProfileEnrollment |
nickname:String 필수, locale:String 필수, :String 선택 |
등록 App에서 보낼 때 AI-Agent 등록 시작 |
retryVoiceProfileSet |
setIndex:Int 화면이 전달 |
Service가 현재 세트 기준 재시도. 임의 세트 선택 API 아님 |
pauseVoiceProfileEnrollmentForExit |
setIndex:Int 화면이 전달 |
나가기 확인용 일시 정지 |
cancelVoiceProfileEnrollment |
reason:String 선택 |
Service는 requestId 기준 취소. reason별 동작 분기 없음 |
setVoiceProfilePersonalization |
enabled:Boolean 필수 |
개인화 설정. 누락 시 현재 Service 기본 true |
retryVoiceProfileCreation |
없음 | 프로필 생성 재시도 |
retryVoiceProfileDelivery |
없음 | DeviceAgent 전달 재시도 |
closeVoiceProfileRegistration |
없음 | 현재 세션의 화면 종료 처리. 취소와 같은 메서드가 아님 |
nickname: trim 후 1~40자, 경로 구분자와 제어 문자 금지.
발음 보조 필드는 외부 Bundle에 포함하지 않는다.
locale: 한·영 연동 송신값은 한국어 ko-KR, 영어 en-US로 정의한다. 현재 원격 시작 검사는 비어 있지 않은 최대 32자만 확인하며 언어 목록을 강제하지 않는다. 기본값·누락·잘못된 값 처리.
userId는 등록 App이 보내는 기존 호환 필드이며 AI-Agent 등록 시작 인자로 사용하지 않는다.
발음 변환은 AI-Agent 내부 책임이며 기본 비활성이다.
setVoiceProfilePersonalization은 발음을 수정하는 API가 아니다.
5. 원격 요청의 등록 Activity 실행
IotAgent에서 온 start는 Service로 즉시 넘기지 않고 다음 Component를 연다.
- 패키지:
com.sk.airbot.voiceprofileregistration - Action:
com.sk.airbot.voiceprofile.action.REGISTER - 클래스:
com.sk.airbot.voiceprofileregistration.VoiceProfileRegistrationActivity - 플래그:
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP NEW_TASK: 서비스에서 등록 화면을 열 수 있도록 기존 Task를 활용하거나 새로 만든다.CLEAR_TOP: 기존 등록 화면 위에 쌓인 다른 Activity를 종료한다. 등록 화면의 재사용 여부는launchMode에도 영향을 받는다.- 화면 실행 옵션이며 녹음 취소나 등록 상태 초기화 명령이 아니다. Flag 의미와 사용 예시.
- 시작 Intent extras:
requestId:String,nickname:String,locale:String,personalizationEnabled:Boolean - 취소 Intent extras: 같은
requestId:String,command:String="cancel"
등록 App은 DeviceAgent에 연결한 뒤 상태를 조회하고 사용자 시작 시 start Bundle을 보낸다. 원격 취소의 현재 구현도 Activity를 경유한다. 화면 미설치/실행 실패를 취소 성공으로 처리하면 안 된다.
6. 상태 callback
발신: AI-Agent → 중계: DeviceAgent → 수신: Registration App와 IotAgent. DeviceAgent는 같은 상태 Bundle을 두 대상으로 전달한다. 현재 IotAgent의 결과 발행은 로컬 시험 Bridge 경로다. 제품 AWS Backend 응답 토픽 발행은 추가 구현 대상이다. 상태 순번의 현재 한계는 13절을 참고한다.
method=voiceProfileStateChanged, contractVersion=voice-profile-v2.
| 필드 | 타입 | 포함 조건/의미 |
|---|---|---|
requestId |
String | 항상. 등록 요청과 일치 |
stateSequence |
Long | 항상. 같은 작업 내 상태 순번 |
state |
String | 항상. Service enum 이름 |
setIndex / setCount |
Int / Int | 항상. 세트 번호/전체 수 |
progress |
Int | 항상. 단계 진행률 0~100 |
reasonCode |
String | 항상. 정상은 빈 문자열 가능 |
terminal |
Boolean | 항상. 상태의 종료 여부 |
nickname |
String | 항상. 아직 요청 정보가 없으면 빈 문자열 가능 |
voiceId |
String | 생성된 값이 있을 때만. COMPLETED에는 필수 |
화면은 다른 requestId를 무시하고 같은 작업의 오래된 순번을 덮어쓰지 않는다.
SET_REJECTED는 재녹음 가능한 상태다. 현재 PROFILE_FAILED는 시작 요청 거절 시 terminal=true,
임베딩 생성 실패 시 false다. DELIVERY_FAILED는 현재 false로 전송하며 재시도할 수 있다.
실패 이름만으로 종료를 추정하지 말고 callback의 terminal과 재시도 가능 단계를 함께 읽는다.
7. 프로필 전달: AI-Agent → DeviceAgent
이 절은 AI-Agent와 DeviceAgent 사이의 임베딩 전달이다. 등록 App 요청과 구분해서 구현한다. 전체 왕복 필드는 AI-Agent ↔ DeviceAgent 명세, 화면 제어는 등록 App ↔ DeviceAgent 명세를 참고한다.
DeviceAgent가 구현할 수신 처리: 요청 검증 → 전달 대기 저장 → 결과 Bundle 생성 → AI-Agent callback. 단순 전달 함수와 달리 이 메서드는 DeviceAgent가 결과를 직접 만든다.
method=commitVoiceProfile, 버전 voice-profile-v2.
| 추가 필드 | 타입 | 송신 계약 |
|---|---|---|
voiceId |
String | 필수. AI-Agent가 생성한 ID |
nickname |
String | 필수. 표시 이름 |
locale |
String | 송신 |
personalizationEnabled |
Boolean | 송신 |
referenceEmbedding |
FloatArray / putFloatArray | 필수. 대표 임베딩 |
현재 DeviceAgent는 voiceId 1~128자, 임베딩 1~4096개와 각 값이 유한한지 검사한다.
이 범위는 방어 상한이지 실제 모델 차원이 아니다. 원본 audio/wav/pcm은 금지한다.
소유한 저장소에 안전하게 저장한 뒤 callback을 만든다.
DeviceAgent → AI-Agent 결과
method=voiceProfileCommitResult, 동일 버전·requestId.
| 추가 필드 | 타입 | 의미 |
|---|---|---|
accepted |
Boolean | DeviceAgent 전달 대기 저장 성공 |
cloudSynced |
Boolean | Cloud 저장 확인 여부 |
reasonCode |
String | 결과 사유 |
현재 사유는 INVALID_PROFILE_PAYLOAD, DEVICE_OUTBOX_FAILED,
WAITING_CLOUD_SYNC, 시험용 LOCAL_TEST_ACK다.
현재 제품 Cloud ACK 연결은 없다. accepted=true, cloudSynced=false이면 Service는 DELIVERY_FAILED로 처리한다.
제품 ACK를 늦게 받았을 때 이미 실패 상태인 Service에 완료를 적용하는 정책도 함께 구현해야 한다.
임의로 cloudSynced=true를 반환해서 제품 등록 성공을 만들면 안 된다.
getVoicePersonalization
방향: AI-Agent → DeviceAgent → IoTAgent → Backend. AI-Agent가 sendModuleCommand로 요청하고 DeviceAgent는 CALLER_IOT=2 callback에 전달한다. 등록 App은 이 요청을 보내지 않는다.
| 필드 | Bundle 타입 | 필수·값 |
|---|---|---|
method |
String | getVoicePersonalization |
contractVersion |
String | voice-personalization-v1 |
requestId |
String | 새 요청 UUID. 검증: [A-Za-z0-9_-]{1,128} |
voiceId |
String | 인식된 등록 화자 ID. 검증: [A-Za-z0-9_-]{1,128} |
issuedAtMs |
Long | 생성 시각, Unix ms. 미래 시각 거절 |
expiresAtMs |
Long | AI-Agent 생성 시각 + 5,000ms. DeviceAgent는 만료 또는 요청 기간 10,000ms 초과 거절 |
DeviceAgent는 AI-Agent 호출자와 버전·타입·기한을 검사한다. 유효 요청은 0, 거절은 -1이다. 0은 콜백 중계 수락이며 서버 조회 완료가 아니다. MQTT 연결이 없으면 별도 실패 콜백 없이 기한이 끝날 수 있다.
호출 조건·처리: 등록 화자 확정과 관심사 기능 활성 후 새 요청 한 건을 추적한다. 새 요청은 이전 pending과 임시 결과를 비운다.
오류·중복·검증: 잘못된 caller·타입·만료는 거절하며 별도 callback이 없을 수 있다. 다른 requestId/voiceId, 취소 후 응답, 중복 응답을 사용하지 않는지 확인한다.
{
"method": "getVoicePersonalization",
"contractVersion": "voice-personalization-v1",
"requestId": "query-001",
"voiceId": "voice-001",
"issuedAtMs": 1789689600000,
"expiresAtMs": 1789689605000
}
시각은 형식 예시다. 실행 시 현재 시각과 5초 후 시각으로 대체한다.
voicePersonalizationResult
방향: Backend → IoTAgent → DeviceAgent → AI-Agent. IoTAgent가 sendModuleCommand로 보내고 DeviceAgent는 CALLER_LLM=1 callback으로 전달한다. 등록 App에는 전달하지 않는다.
| 필드 | Bundle 타입 | 필수·값 |
|---|---|---|
method |
String | voicePersonalizationResult |
contractVersion |
String | voice-personalization-v1 |
requestId |
String | 원 요청과 동일 |
voiceId |
String | 원 요청과 동일 |
success |
Boolean | 항상 포함 |
reasonCode |
String | 항상 포함. 성공은 빈 문자열 |
expiresAtMs |
Long | 원 요청 기한 유지; 서버에서 연장하지 않음 |
personalizationEnabled |
Boolean | 성공 시 필수 |
interestCodes |
ArrayList<String> | 성공 시 필수. 개인화 비허용이면 [] |
revision |
Long | 성공 시 필수, 1 이상 |
허용 코드는 WEATHER, AIR_QUALITY, HEALTH, SCHEDULE, TRAVEL, FOOD, CULTURE, SPORTS, FINANCE, TECHNOLOGY다. 최대 10개, 중복 금지이며 빈 목록은 관심사 미설정이다. putStringArrayList를 사용한다.
서버 실패는 PROFILE_NOT_FOUND 또는 BACKEND_UNAVAILABLE이며 interestCodes를 포함하지 않는다. DeviceAgent는 IoTAgent 호출자·타입·기한을 검사하고, AI-Agent는 현재 requestId·voiceId·기한을 대조한다. 종료된 요청·다른 화자·중복 응답을 사용하지 않는다. 조회 결과는 원 요청의 5초 기한까지만 임시 보관하고 로컬 DB에 저장하지 않는다.
현재 서버 통신은 VoiceProfileMqttTestBridge와 샘플 Backend 경로다. 제품 AWS 인증·권한·통신 검증과 LLM 프롬프트 자동 주입은 완료 범위가 아니다.
호출 조건·처리: 서버가 원 요청에 대응해 반환한다. AI-Agent는 유효 성공만 snapshot에 보관한다.
오류·중복·검증: 실패에는 interestCodes를 넣지 않는다. 동일 결과 재수신·기한 연장·다른 화자 응답이 무시되는지 확인한다.
{
"method": "voicePersonalizationResult",
"contractVersion": "voice-personalization-v1",
"requestId": "query-001",
"voiceId": "voice-001",
"success": true,
"reasonCode": "",
"expiresAtMs": 1789689605000,
"personalizationEnabled": true,
"interestCodes": [
"WEATHER",
"HEALTH"
],
"revision": 2
}
8. 프로필 조회·삭제 요청
요청: IotAgent → DeviceAgent → AI-Agent. 결과: AI-Agent → DeviceAgent → IotAgent. DeviceAgent가 자체 데이터로 목록을 만들어 반환하는 API가 아니다. 관심사 신규 조회는 위 getVoicePersonalization의 반대 방향 요청을 사용한다.
발신은 IotAgent, DeviceAgent가 AI-Agent callback으로 전달한다.
| method | 버전 | 추가 입력 |
|---|---|---|
listVoiceProfiles |
voice-profile-v2 | 없음 |
deleteVoiceProfile |
voice-profile-v2 | voiceId:String 필수 |
voiceId는 1~128자의 [A-Za-z0-9_-]+. nickname을 대신 보내지 않는다.
관심사 변경은 모바일 → Backend HTTPS PUT에서 처리한다. 구형 로컬 set/get은 실패 통지만 반환하며 아래 성공 결과 표에 포함하지 않는다.
관리 결과
모두 공통 method:String, contractVersion:String, requestId:String,
success:Boolean, reasonCode:String을 포함한다.
아래는 성공 시 추가 필드이며 실패 시 일부가 생략될 수 있다.
| method | 성공 시 추가 필드 |
|---|---|
voiceProfileListResult |
profiles:ArrayList<Bundle> |
voiceProfileDeleteResult |
voiceId:String |
profiles 각 Bundle: voiceId, nickname 모두 String; personalizationEnabled:Boolean은 기록된 동의이며 예전 필드 누락은 false.
실패 사유: PROFILE_LIST_FAILED, ENROLLMENT_ACTIVE, PROFILE_NOT_FOUND, PROFILE_DELETE_FAILED,
FEATURE_DISABLED, HISTORY_READ_FAILED, HISTORY_CLEAR_FAILED, STORAGE_STATS_FAILED. 구형 관심사 요청은 SERVER_OWNED_PERSONALIZATION.
없는 프로필 삭제는 현재 PROFILE_NOT_FOUND다. 성공 결과 수신 시 DeviceAgent가 대기 사본 제거를 시도하지만, 제거 예외가 발생해도 원래 성공 결과가 전달될 수 있다. 제품 Cloud 사본 삭제는 미연결이다. 샘플 Backend는 기기 삭제 성공 또는 PROFILE_NOT_FOUND 응답 후 별도로 관심사 행을 삭제하며, 이를 원자적 삭제로 보장하지 않는다.
10. 호출 예제
아래 문자열은 wire 값을 보여주기 위한 예제다. 구현에서는 각 모듈 Contract 상수를 사용한다.
val input = Bundle().apply {
putString("method", "getVoicePersonalization")
putString("contractVersion", "voice-personalization-v1")
putString("requestId", UUID.randomUUID().toString())
putString("voiceId", "vp-example-001")
val now = System.currentTimeMillis()
putLong("issuedAtMs", now)
putLong("expiresAtMs", now + 5_000L)
}
// AI-Agent 측: 0은 중계 수락. 조회 결과는 callback으로 받는다.
val result = DeviceControlProxy.getService().sendModuleCommand(input, Bundle())
override fun onModuleCallback(outdata: Bundle?) {
val data = outdata ?: return
if (data.getString("method") == "voicePersonalizationResult") {
val requestId = data.getString("requestId")
val success = data.getBoolean("success", false)
val reason = data.getString("reasonCode").orEmpty()
// 현재 requestId·voiceId·기한을 확인한 뒤 성공 결과만 사용한다.
}
}
11. 구현 확인 항목
- Proxy 연결 후 GET_STATE callback이 등록 App에 도착한다.
- 등록 상태는 등록 App과 IotAgent 두 곳에 도착한다.
- 관리 요청은 UI를 열지 않고 Service로 전달되고 결과는 IotAgent로 돌아온다.
- Long·FloatArray·Parcelable 목록이 타입 손실 없이 왕복한다.
- commit 저장 실패는 accepted=false이며 등록 성공 UI가 뜨지 않는다.
- MQTT publish 성공이나 Binder 반환 0을 등록 완료로 처리하지 않는다.
- 잘못된 호출자·만료·미지원 버전은 실행되지 않는다. 거절 경로는 callback이 없을 수 있으므로 모바일 실패 통지가 보장된다고 판단하지 않는다.
코드 기준과 미완료 경계
기준은 SoC speaker_identification 작업본과 AI-Agent feature/cpu-speaker-recognition 작업본이다. DeviceAgent.java의 sendModuleCommand/launchVoiceProfileRegistration, ModuleCmdCallbackPolicy.java, VoiceProfileContract.java, Registration App의 DeviceAgentVoiceProfileClient.kt, AI-Agent의 VoiceProfileIpcContract.kt/ForegroundService.kt를 대조했다.
제품 MQTT 결과 발행, Cloud ACK 수신·Service 완료 연결, 재전송, 영어 발음 전달, 원격 취소 실패 응답은 추가 작업이다. 이 문서가 해당 구현 완료를 의미하지 않는다.
12. 메서드별 전체 명세
아래 26개 항목은 요청 16개, commit 1개, 결과·이벤트 9개다. 문자열 키 목록을 메서드 수에 포함하지 않는다.
공통 적용: 모든 항목에 method/contractVersion/requestId가 필수다. 표의 선택 필드만 생략 가능하다. 결과 항목의 '성공 필수' 필드는 실패 시 생략할 수 있다. JSON은 Bundle 값의 보기용 표현이며 Binder로 JSON 문자열을 보내는 뜻이 아니다.
| 메서드 | 목적 |
|---|---|
| getVoiceProfileEnrollmentState | 등록 상태 조회 |
| startVoiceProfileEnrollment | 등록 녹음 시작 |
| retryVoiceProfileSet | 현재 세트 다시 녹음 |
| pauseVoiceProfileEnrollmentForExit | 나가기 확인 중 녹음 정지 |
| cancelVoiceProfileEnrollment | 등록 작업 취소 |
| setVoiceProfilePersonalization | 개인화 선택 후 생성 시작 |
| retryVoiceProfileCreation | 로컬 프로필 생성 재시도 |
| retryVoiceProfileDelivery | 프로필 전달 다시 시도 |
| closeVoiceProfileRegistration | 등록 UI 세션 정리 |
| commitVoiceProfile | 생성 프로필 전달 대기 저장 |
| listVoiceProfiles | 기기 프로필 목록 |
| deleteVoiceProfile | 프로필과 관련 로컬 데이터 삭제 |
| getVoicePersonalization | AI-Agent 시작 서버 관심사 조회 |
| voicePersonalizationResult | IoTAgent → AI-Agent 서버 조회 결과 |
| setVoiceProfileInterests | 중단된 로컬 API: 실패 통지 |
| getVoiceProfileInterests | 중단된 로컬 API: 실패 통지 |
| voiceProfileStateChanged | 등록 상태 이벤트 |
| voiceProfileCommitResult | DeviceAgent 저장 응답 |
| voiceProfileListResult | 프로필 목록 결과 |
| voiceProfileDeleteResult | 프로필 삭제 결과 |
| voiceProfileInterestsResult | 구형 요청 거절 결과 |
getVoiceProfileEnrollmentState
목적: 등록 상태 조회. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: Proxy 연결 후 또는 화면 재진입.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
처리 순서·상태: 동일 requestId의 세션이 있으면 현재 상태를 전송한다. 없으면 WAITING_START, 빈 nickname, sequence=0을 전송한다.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: 별도 not-found 오류 없음. WAITING_START는 녹음 시작 성공이 아니다.
중복·취소·대기: 반복 조회 가능. 요청 ID가 다르면 활성 세션 정보를 대신 반환하지 않는다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "getVoiceProfileEnrollmentState",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001"
}
연동 검증: 없는 ID와 활성 ID 조회 결과를 각각 확인.
startVoiceProfileEnrollment
목적: 등록 녹음 시작. 방향: Registration App → DeviceAgent → AI-Agent / 원격 IotAgent 요청은 Activity 실행.
호출 조건: 화면 시작 버튼. 다른 비종료 세션이 없어야 함.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
nickname |
String | 필수 · 1~40자, 안전한 표시 이름 |
locale |
String | 원격 필수 ≤32자; Service 빈 값 기본 ko-KR |
처리 순서·상태: Service는 기존 세트·pending을 비우고 녹음을 시작한다. RECORDING_SET → SET_ACCEPTED/SET_REJECTED. 5세트 후 PERSONALIZATION_SETTINGS.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: ANOTHER_SESSION_ACTIVE; 진입 검증에서 무응답 거절 가능. 내부 시작 함수에는 INVALID_NICKNAME/INVALID_SPOKEN_NAME_KO. 장치 확보 실패 AUDIO_BUSY.
중복·취소·대기: 동일 requestId의 start도 세트를 초기화한다. 자동 재전송 금지; GET_STATE로 먼저 확인. 녹음 5,200ms, 진행 갱신 500ms. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "startVoiceProfileEnrollment",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"nickname": "Niky",
"locale": "ko-KR"
}
연동 검증: 중복 start가 무해하다고 가정하지 않고, 화면 시작 1회당 한 요청인지 확인.
retryVoiceProfileSet
목적: 현재 세트 다시 녹음. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 일치하는 세션, terminal=false. UI는 SET_REJECTED 또는 나가기 확인 후 계속에서 사용.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
setIndex |
Int | 화면 전송; Service는 값 대신 현재 세트 사용 |
처리 순서·상태: 현재 capture 중단 후 같은 세션의 현재 세트 녹음을 다시 시작.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: 없는 세션/다른 ID/terminal이면 callback 없이 반환.
중복·취소·대기: 반복 요청은 녹음을 다시 시작하므로 중복 실행 방지 필요. 다른 세트로 점프하는 API가 아니다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "retryVoiceProfileSet",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"setIndex": 1
}
연동 검증: 다른 setIndex를 보내도 임의 세트 선택으로 취급하지 않는지 확인.
pauseVoiceProfileEnrollmentForExit
목적: 나가기 확인 중 녹음 정지. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 동일 requestId이고 RECORDING_SET.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
setIndex |
Int | 화면 전송, Service에서 제어 인자로 사용하지 않음 |
처리 순서·상태: capture 타이머를 제거하고 현재 녹음을 중단한다. 별도 PAUSED 상태나 callback을 발행하지 않는다.
결과: 전용 결과 method 없음. 위 처리에 명시된 상태만 수신한다.
오류·무응답: 상태/ID 불일치는 무응답.
중복·취소·대기: 계속하기는 retryVoiceProfileSet, 나가기는 cancel. pause ACK를 기다리는 타이머를 UI에 만들지 않는다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "pauseVoiceProfileEnrollmentForExit",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"setIndex": 1
}
연동 검증: 일시정지 후 UI 확인창, 계속 선택 시 재녹음 확인.
cancelVoiceProfileEnrollment
목적: 등록 작업 취소. 방향: Registration App → DeviceAgent → AI-Agent / 원격은 IotAgent → DeviceAgent → Activity.
호출 조건: 동일 requestId의 세션이 존재.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
reason |
String | 선택 · Service는 사유별 분기 없이 USER_CANCELLED 반환 |
처리 순서·상태: 타이머/capture/수집 세트/pending을 정리하고 CANCELLED, terminal=true 전송 후 음성 경로 복원.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: ID 불일치/세션 없음은 무응답. 생성 중 취소는 비동기 생성 완료와 경쟁할 수 있다.
중복·취소·대기: 동일 취소 반복 시 callback 재발행 가능. 이미 생성된 로컬/Cloud 사본 삭제는 이 함수의 보장이 아니다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "cancelVoiceProfileEnrollment",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"reason": "MOBILE_CANCELLED"
}
연동 검증: 녹음 중 취소와 생성 중 취소를 분리 시험; 늦은 생성 완료가 되살리는지 확인.
setVoiceProfilePersonalization
목적: 개인화 선택 후 생성 시작. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 동일 requestId이고 PERSONALIZATION_SETTINGS.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
enabled |
Boolean | 송신 필수; 현재 수신 기본 true |
처리 순서·상태: 선택값 저장 후 finalizeVoiceProfile. 5개 세트가 있어야 CREATING_LOCAL_PROFILE로 진행.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: 조건 불일치는 무응답. 생성 실패는 EMBEDDING_CREATION_FAILED 또는 품질 사유.
중복·취소·대기: 해당 화면에서 1회만 전송. 발음 수정 함수가 아니며 personalizationEnabled 키와 혼동 금지. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "setVoiceProfilePersonalization",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"enabled": true
}
연동 검증: enabled=false가 commit의 personalizationEnabled=false로 전달되는지 확인.
retryVoiceProfileCreation
목적: 로컬 프로필 생성 재시도. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 동일 requestId이고 수집 세트 수가 setCount와 같아야 함.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
처리 순서·상태: finalizeVoiceProfile 호출. 현재 함수는 stage 제한 없이 세트 수를 검사한다.
결과: voiceProfileStateChanged의 전체 필드를 따른다.
오류·무응답: 5개 세트가 이미 비워진 생성 실패 뒤에는 재시도 요청이 아무 결과 없이 반환될 수 있다.
중복·취소·대기: 새 녹음 없이 생성 재시도가 항상 가능하다고 안내하지 않는다. 재진입/중복 생성 방어는 보완 필요. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "retryVoiceProfileCreation",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001"
}
연동 검증: 생성 실패 후 실제로 재시도가 가능한지 별도 시험; 무응답이면 완료로 처리 금지.
retryVoiceProfileDelivery
목적: 프로필 전달 다시 시도. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 일치하는 pendingVoiceProfileDelivery 존재.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
처리 순서·상태: PENDING_DEVICE_DELIVERY 상태 전송 후 같은 프로필로 commitVoiceProfile 재호출.
결과: 등록 App은 voiceProfileStateChanged를 받는다. 내부 DeviceAgent 응답인 voiceProfileCommitResult는 AI-Agent가 수신한다.
오류·무응답: pending 없음/ID 불일치 무응답. 전달 예외 DEVICE_AGENT_UNAVAILABLE.
중복·취소·대기: 프로필 새 생성 아님. pending은 메모리이므로 프로세스 재시작 복구를 보장하지 않는다. ACK 대기 타이머 없음. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "retryVoiceProfileDelivery",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001"
}
연동 검증: 동일 voiceId commit 재전송과 DeviceAgent 저장 중복 처리를 확인.
closeVoiceProfileRegistration
목적: 등록 UI 세션 정리. 방향: Registration App → DeviceAgent → AI-Agent.
호출 조건: 일치하는 세션 존재.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
처리 순서·상태: terminal=false면 cancel을 호출해 CANCELLED 전송. 이후 세션·pending 제거. 이미 terminal이면 별도 상태 없이 정리.
결과: 전용 결과 method 없음. 위 처리에 명시된 상태만 수신한다.
오류·무응답: 세션 없음/ID 불일치 무응답.
중복·취소·대기: 반복 close는 no-op. 프로필 삭제 API가 아니다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "closeVoiceProfileRegistration",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001"
}
연동 검증: 완료 후 close가 등록 프로필을 삭제하지 않는지, 미완료 close는 취소되는지 확인.
commitVoiceProfile
목적: 생성 프로필 전달 대기 저장. 방향: AI-Agent → DeviceAgent.
호출 조건: AI-Agent 호출자 및 등록 envelope 검증.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
voiceId |
String | 필수 · 1~128자, [A-Za-z0-9_-]+ |
nickname |
String | AI-Agent 항상 전송 |
locale |
String | 항상 전송 |
personalizationEnabled |
Boolean | 항상 전송 |
referenceEmbedding |
FloatArray | 필수 · 1~4096개 유한 값. 실제 모델 차원 아님 |
처리 순서·상태: DeviceAgent 전용 pending 저장 후 accepted/cloudSynced callback 생성. 원본 audio/wav/pcm 금지.
결과: voiceProfileCommitResult의 전체 필드를 따른다.
오류·무응답: INVALID_PROFILE_PAYLOAD, DEVICE_OUTBOX_FAILED; 호출자 거절 -2. 저장 실패 -1.
중복·취소·대기: 같은 요청 재전송 가능성에 대비해 requestId/voiceId로 저장 중복 관리 필요. Cloud ACK 경로 미구현. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "commitVoiceProfile",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"voiceId": "vp-example-001",
"nickname": "Niky",
"locale": "ko-KR",
"personalizationEnabled": true,
"referenceEmbedding": [
0.5,
0.5,
-0.5,
0.5
]
}
연동 검증: 저장 성공 accepted=true/Cloud 미확인 false, 파일 저장 실패 accepted=false 확인.
listVoiceProfiles
목적: 기기 프로필 목록. 방향: IotAgent → DeviceAgent → AI-Agent.
호출 조건: 관리 명령 호출자 IotAgent.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
처리 순서·상태: AI-Agent 실제 등록 저장소에서 목록 조회. Backend 캐시를 대신 반환하지 않음.
결과: voiceProfileListResult의 전체 필드를 따른다.
오류·무응답: PROFILE_LIST_FAILED.
중복·취소·대기: 읽기 재요청 가능. 같은 응답을 중복 화면 추가하지 않는다. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "listVoiceProfiles",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001"
}
연동 검증: 등록 전 빈 목록, 등록 후 voiceId/발음 포함, 삭제 후 제거 확인.
deleteVoiceProfile
목적: 프로필과 관련 로컬 데이터 삭제. 방향: IotAgent → DeviceAgent → AI-Agent.
호출 조건: voiceId 유효; 진행 중 등록 세션이 없어야 함.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
voiceId |
String | 필수 · 1~128자, [A-Za-z0-9_-]+ |
처리 순서·상태: 프로필 파일 삭제 → 식별기 재초기화. 서버 관심사 삭제는 샘플 Backend의 후속 처리이며 이 Binder 호출만으로 완료되지 않는다. DeviceAgent는 성공 결과를 받고 pending 사본 제거.
결과: voiceProfileDeleteResult의 전체 필드를 따른다.
오류·무응답: ENROLLMENT_ACTIVE, PROFILE_NOT_FOUND, PROFILE_DELETE_FAILED.
중복·취소·대기: 반복 삭제는 PROFILE_NOT_FOUND. 원자적 전체 rollback 보장 없음; 일부 삭제 후 실패 가능. Cloud 삭제 별도. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "deleteVoiceProfile",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"voiceId": "vp-example-001"
}
연동 검증: 등록 진행 중 거절, 없는 ID, 삭제 후 목록/식별 상태 및 서버 관심사 후속 처리 확인.
setVoiceProfileInterests
중단된 로컬 API. 아래 입력 형식은 이전 호출 식별용이다. 신규 모바일은 서버 HTTPS PUT을 사용한다.
목적: 이전 저장 요청의 거절. 방향: IotAgent → DeviceAgent → AI-Agent.
호출 조건: voiceId 유효, 허용 코드 목록, 기능 활성.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
voiceId |
String | 필수 · 1~128자, [A-Za-z0-9_-]+ |
interestCodes |
ArrayList<String> |
필수 · 허용 코드, 중복 금지, 빈 목록은 전체 해제 |
처리 순서·상태: DB 변경 없이 success=false, reasonCode=SERVER_OWNED_PERSONALIZATION 반환.
결과: voiceProfileInterestsResult의 전체 필드를 따른다.
오류·무응답: SERVER_OWNED_PERSONALIZATION; 잘못된 입력은 앞단 무응답 거절 가능.
중복·취소·대기: 재전송해도 저장하지 않고 거절한다. 서버 HTTPS PUT으로 전환한다.
값 예제
{
"method": "setVoiceProfileInterests",
"contractVersion": "voice-memory-v1",
"requestId": "vp-request-001",
"voiceId": "vp-example-001",
"interestCodes": [
"WEATHER",
"HEALTH"
]
}
연동 검증: DB 무변경과 SERVER_OWNED_PERSONALIZATION 실패 확인.
getVoiceProfileInterests
중단된 로컬 API. 새 AI-Agent 조회는 getVoicePersonalization이며 방향은 AI-Agent → DeviceAgent다.
목적: 이전 조회 요청의 거절. 방향: IotAgent → DeviceAgent → AI-Agent.
호출 조건: voiceId 유효, 기능 활성.
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
voiceId |
String | 필수 · 1~128자, [A-Za-z0-9_-]+ |
처리 순서·상태: DB 조회 없이 success=false, reasonCode=SERVER_OWNED_PERSONALIZATION 반환.
결과: voiceProfileInterestsResult의 전체 필드를 따른다.
오류·무응답: SERVER_OWNED_PERSONALIZATION.
중복·취소·대기: 읽기 재시도로 복구되지 않는다. 신규 getVoicePersonalization으로 전환한다.
값 예제
{
"method": "getVoiceProfileInterests",
"contractVersion": "voice-memory-v1",
"requestId": "vp-request-001",
"voiceId": "vp-example-001"
}
연동 검증: 동일 nickname을 화자 키로 사용하지 않는지 확인.
voiceProfileStateChanged
목적: 등록 상태 이벤트. 방향: AI-Agent → DeviceAgent → Registration App + IotAgent.
호출 조건: 등록 제어에 따른 상태 변경 또는 상태 조회
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
stateSequence |
Long | 필수 · 상태 순번 |
state |
String | 필수 · 등록 enum |
setIndex |
Int | 필수 · 현재 세트 |
setCount |
Int | 필수 · 현재 구현5 |
progress |
Int | 필수 · 0~100 |
reasonCode |
String | 필수 · 정상 빈 문자열 |
terminal |
Boolean | 필수 · 단계별 종료 여부 |
nickname |
String | 항상 포함, 상태 조회 초기값은 빈 문자열 가능 |
voiceId |
String | 생성 후 선택, COMPLETED 필수 |
처리 순서·상태: UI는 requestId별로 처리. SET_REJECTED는 재녹음, COMPLETED는 완료. PROFILE_FAILED의 terminal은 발생 경로별로 다름.
결과: 이 항목 자체가 결과/이벤트다. 추가 응답 메서드는 없다.
오류·무응답: 알 수 없는 state를 성공으로 표시하지 않는다. 결과 수신이 단절되면 GET_STATE.
중복·취소·대기: 순번이 낮은 상태는 덮어쓰지 않는 것이 수신 계약. 현재 생성 과정에서 이전 session.copy를 복원해 순번이 후퇴할 가능성은 보완 필요. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "voiceProfileStateChanged",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"stateSequence": 8,
"state": "SET_ACCEPTED",
"setIndex": 2,
"setCount": 5,
"progress": 100,
"reasonCode": "",
"terminal": false,
"nickname": "Niky"
}
연동 검증: UI와 IoT 모두 수신, 초기/재시도 순번과 완료 voiceId 확인.
voiceProfileCommitResult
목적: DeviceAgent 저장 응답. 방향: DeviceAgent → AI-Agent.
호출 조건: 동일 requestId의 세션과 pending이 존재
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
accepted |
Boolean | 필수 · DeviceAgent 저장 수락 |
cloudSynced |
Boolean | 필수 · Cloud 저장 확인 |
reasonCode |
String | 필수 · 결과 사유 |
처리 순서·상태: accepted && cloudSynced이면 COMPLETED, 그 외 DELIVERY_FAILED. 음성 경로 복원.
결과: 이 항목 자체가 결과/이벤트다. 추가 응답 메서드는 없다.
오류·무응답: INVALID_PROFILE_PAYLOAD, DEVICE_OUTBOX_FAILED, WAITING_CLOUD_SYNC. 빈 사유는 Service가 DEVICE_DELIVERY_REJECTED 등으로 대체.
중복·취소·대기: ID 불일치/pending 없음 무시. 완료 후 pending 제거하므로 같은 ACK 재수신은 무시. 프로세스 재시작 후 pending 복구 없음. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "voiceProfileCommitResult",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"accepted": true,
"cloudSynced": false,
"reasonCode": "WAITING_CLOUD_SYNC"
}
연동 검증: false/false, true/false, true/true, 다른 requestId를 각각 시험.
voiceProfileListResult
목적: 프로필 목록 결과. 방향: AI-Agent → DeviceAgent → IotAgent.
호출 조건: listVoiceProfiles 완료
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
success |
Boolean | 필수 · 업무 성공 여부 |
reasonCode |
String | 필수 · 정상 빈 문자열 / 실패 사유 |
profiles |
ArrayList<Bundle> |
성공 필수 · 최대64, 실패 시 빈 목록 가능 |
처리 순서·상태: profiles를 JSON 객체 배열로 변환. 빈 목록+success=true는 등록된 화자 없음.
결과: 이 항목 자체가 결과/이벤트다. 추가 응답 메서드는 없다.
오류·무응답: PROFILE_LIST_FAILED. 실패를 빈 목록 성공으로 바꾸지 않는다.
중복·취소·대기: 같은 requestId 결과는 한 번만 반영. raw/embedding 필드 추가 금지. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "voiceProfileListResult",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"success": true,
"reasonCode": "",
"profiles": [
{
"voiceId": "vp-example-001",
"nickname": "Niky"
}
]
}
연동 검증: 64개 상한, 필드 타입과 빈 목록 성공/실패 구별.
voiceProfileDeleteResult
목적: 프로필 삭제 결과. 방향: AI-Agent → DeviceAgent → IotAgent.
호출 조건: deleteVoiceProfile 완료
필드 명세
| 키 | Android 타입 | 필수·기본값·범위 |
|---|---|---|
method |
String | 필수 · 고정값 |
contractVersion |
String | 필수 · 고정값 |
requestId |
String | 필수 · 작업 식별자 |
success |
Boolean | 필수 · 업무 성공 여부 |
reasonCode |
String | 필수 · 정상 빈 문자열 / 실패 사유 |
voiceId |
String | 필수 · 1~128자, [A-Za-z0-9_-]+ |
처리 순서·상태: success=true이면 DeviceAgent pending 사본도 제거한 후 IoT 전달.
결과: 이 항목 자체가 결과/이벤트다. 추가 응답 메서드는 없다.
오류·무응답: ENROLLMENT_ACTIVE, PROFILE_NOT_FOUND, PROFILE_DELETE_FAILED.
중복·취소·대기: 중복 삭제 실패를 이전 성공 취소로 해석하지 않는다. DeviceAgent 사본 제거 실패는 로그만 남길 수 있음. 공통 경합/타임아웃 규칙은 다음 절을 함께 적용한다.
값 예제
{
"method": "voiceProfileDeleteResult",
"contractVersion": "voice-profile-v2",
"requestId": "vp-request-001",
"success": true,
"reasonCode": "",
"voiceId": "vp-example-001"
}
연동 검증: 로컬 결과와 Cloud 사본 삭제를 별도로 확인.
voiceProfileInterestsResult
중단된 로컬 API의 거절 결과. 방향은 AI-Agent → DeviceAgent → IoTAgent다. 정상 서버 조회는 voicePersonalizationResult를 사용한다.
호출 조건·처리: 구형 set/getVoiceProfileInterests가 AI-Agent에 도달하면 DB 접근 없이 아래 실패를 송신한다.
| 필드 | Bundle 타입 | 현재 송신값 |
|---|---|---|
method |
String | voiceProfileInterestsResult |
contractVersion |
String | voice-memory-v1 |
requestId |
String | 도달한 구형 요청 ID |
success |
Boolean | false |
reasonCode |
String | SERVER_OWNED_PERSONALIZATION |
현재 rejectLocalInterests는 voiceId·interestCodes를 넣지 않는다. requestId로 원 요청을 찾는다. 앞단 caller·입력 검증에서 거절되면 이 콜백도 없을 수 있다.
오류·중복: SERVER_OWNED_PERSONALIZATION은 재시도로 해결되지 않는다. 같은 요청의 중복 결과는 requestId로 구분한다.
{
"method": "voiceProfileInterestsResult",
"contractVersion": "voice-memory-v1",
"requestId": "vp-request-001",
"success": false,
"reasonCode": "SERVER_OWNED_PERSONALIZATION"
}
연동 검증: 구형 요청으로 DB가 변경·조회되지 않고 실패가 반환되는지 확인한다. 로컬 API 재시도로 서버 관심사를 저장할 수 없다.
13. 시간·취소·재연결의 정확한 보장 범위
호출자 검사는 getPackagesForUid(uid)의 패키지 중 하나가 허용되는지 확인한다. AI-Agent·DeviceAgent·IotAgent가 공유하는 android.uid.system 안에서 개별 앱을 구분하는 인증 보장은 아니다. 등록 App의 별도 UID 검사와 구분해야 한다.
| 항목 | 현재 코드 | 상대 구현 시 주의 |
|---|---|---|
| 녹음 세트 | 5,200ms, 진행 갱신500ms, 통과 표시700ms | 전송 지연과 녹음 시간을 합쳐 고정 완료 시각으로 판단하지 않음 |
| Binder 수락 | 전달/수락 반환값과 비동기 결과 분리 | 0으로 업무 성공 표시 금지 |
| ACK 대기 타이머 | Service에 별도 ACK 대기 타이머 없음 | timeout을 임의 성공으로 처리하지 말고 제품 대기/복구 정책 정의 |
| 동일 start | 동일 requestId의 start도 세트 초기화 | 상태 조회 후 사용자 명시 동작으로만 재시작 |
| 생성 중 취소 | 생성 coroutine 완료 시 최신 세션 ID/취소 여부 재검사가 없음 | 늦은 생성 완료가 상태/pending을 복원할 위험. 제품 통합 전 보완 |
| 세트 간 지연 작업 | 700ms 후속 Runnable의 세션 경계 검증이 없음 | 통과 직후 취소/새 등록 경합 시험 필요 |
| 상태 순번 | 생성 완료/품질 실패에서 이전 session.copy 사용 | stateSequence가 엄격히 증가한다는 현재 구현 보장 없음. 송신측 보완 필요 |
| 생성 재시도 | 실패 후 acceptedSets를 비움 | 5개 세트가 이미 비워진 상태의 retryCreation은 동작하지 않을 수 있음 |
| 프로세스 종료 | 등록 session/pending은 메모리 | 재연결 GET_STATE는 디스크 기반 등록 복구가 아님 |
| 삭제 실패 | 프로필·메모리·식별기 갱신 전체 트랜잭션 아님 | 일부 삭제 후 실패 가능; 조회로 확인 |
| 대기 사본 삭제 | DeviceAgent 제거 예외에도 기존 삭제 성공 결과 전달 가능 | 성공 응답을 모든 사본 삭제 완료로 취급하지 않음 |
| 원격 요청 충돌 | 등록 App이 다른 활성 요청을 무시할 수 있음 | Service의 ANOTHER_SESSION_ACTIVE 응답이 항상 도착한다고 가정하지 않음 |
| 원격 만료 | expiresAtMs 검사, 누락 시 Long.MAX_VALUE | issuedAtMs만 보내면 만료 방어가 되지 않음; Service 자체 만료 검사 없음 |
담당별 보완 범위는 프로토콜 작업표로 모았다.
이 절은 확인된 코드 한계를 적은 것이다. 해당 방어 로직을 이번 문서 작업에서 수정하거나 실기기 검증한 것은 아니다.
Binder 반환값과 검증 책임
2026-09-18 DeviceAgent.sendModuleCommand의 화자인식 분기 기준이다. 음수 값의 의미가 모든 method에서 동일하지 않다. 호출자는 아래 분기와 비동기 결과 method를 함께 확인해야 한다.
| 요청 분기 | 허용 호출자 → 전달 대상 | 즉시 반환 | 실제 결과 채널 |
|---|---|---|---|
| 개인화 조회·결과 | AI-Agent → IoTAgent / IoTAgent → AI-Agent | 호출자 또는 payload 오류 -1, 전달 분기 0 |
voicePersonalizationResult |
| 등록 제어 | 등록 App → AI-Agent; IoTAgent 원격 요청은 등록 UI 경로 | envelope 오류 -1, 호출자 오류 -2, 원격 UI 입력 오류 -1, UI 실행 실패 -3, 수락 0 |
voiceProfileStateChanged |
| 등록 상태 이벤트 | AI-Agent → DeviceAgent → 등록 App·IoTAgent | 입력 오류 -1, 호출자 오류 -2, 전달 분기 0 |
이벤트 자체가 결과; 별도 완료 응답 없음 |
| 관리 요청 | IoTAgent → AI-Agent | 호출자·입력 오류 -1, 전달 분기 0 |
list/delete/history/stats에 대응하는 Result method |
| 관리 결과 | AI-Agent → IoTAgent | 호출자·입력 오류 -1, 전달 분기 0 |
결과 Bundle의 success·reasonCode |
| 프로필 commit | AI-Agent → DeviceAgent 저장소 | 호출자 오류 -2; accepted에 따라 0 또는 -1 |
voiceProfileCommitResult callback |
commit의 잘못된 호출자는 ACK 없이 -2로 거절된다. 허용 호출자의 잘못된 프로필에는 INVALID_PROFILE_PAYLOAD, 저장 실패에는 DEVICE_OUTBOX_FAILED ACK를 만든다. 다른 분기까지 동일한 오류 callback이 있다고 가정하지 않는다.
송신 규약과 실제 수신 검증의 차이
| 항목 | 송신 측이 지킬 규약 | 현재 DeviceAgent·수신 측 검증 |
|---|---|---|
| 등록·상태 버전 | voice-profile-v2 |
DeviceAgent에서 envelope 확인; 등록 App callback 클라이언트는 버전을 다시 검증하지 않음 |
| 개인화 버전 | voice-personalization-v1 |
별도 검증 경로 사용. 예전 관심사 set/get과 혼용 금지 |
| 상태 순번·진행률 | stateSequence Long, progress Int, terminal Boolean 등 명세 타입 준수 | DeviceAgent 상태 envelope는 이들 모든 필드의 타입·범위를 검증하지 않음. UI는 기본값·범위 보정 사용 |
| COMPLETED 상태 | 유효한 nickname·voiceId 포함 | DeviceAgent는 완료 상태에서 이름·ID 검사; 그 외 상태는 같은 검사를 강제하지 않음 |
| 개인화 응답 | 같은 요청·화자, Boolean success, Long revision, interestCodes는 ArrayList<String> | 타입·필드 검사 후 AI-Agent가 요청 상관관계·만료·중복 검사 |
| 삭제 성공 결과 | 삭제 대상 voiceId와 success 유지 | DeviceAgent의 전달 대기 사본 제거 오류는 로그에 남기지만 이미 받은 성공값을 실패로 바꾸지는 않음 |
따라서 명세서의 필수 필드를 일부 수신 코드가 기본값으로 처리한다고 해서 생략 가능한 계약은 아니다. 반대로 엄격한 수신 검사가 아직 없는 항목을 구현 완료로 표시하지 않는다.
구성요소별 연동 확인 순서
- 등록 App과 AI-Agent의 Proxy 연결 및 callback 등록을 먼저 확인한다. 잘못된 caller 배선은 payload가 정상이어도 거절된다.
- 등록 App은 등록 제어, AI-Agent는 상태·commit, IoTAgent는 원격 관리 요청을 각각 보내 반환값과 callback을 함께 기록한다.
- 서버 관심사는 AI-Agent의 get 요청을 IoTAgent까지 전달하고 같은 requestId·voiceId로 역방향 결과를 돌려준다. 등록 App에 관심사 쓰기 책임을 추가하지 않는다.
- 서버 미연결과 로컬 시험 ACK를 나누어 시험한다. local_ack로 완료된 등록을 실제 서버 저장 완료 시험에 포함하지 않는다.
- 파일·DB·모델·DeviceAgent 전달 대기 사본과 서버 사본을 구분해 조회·삭제 결과를 확인한다. 하나의 success로 모든 사본 삭제를 단정하지 않는다.
상호 연동 시험표
아래는 실제 Bundle을 사용한 실기기 검증 조건이다. 문서의 예시 JSON 파싱 시험만으로 충족되지 않는다.
| 번호 | 보낼 입력·상황 | 확인 결과 |
|---|---|---|
| BC-01 | 등록 App caller로 commit 전송 | 잘못된 호출자 거절; 저장·성공 ACK 없음 |
| BC-02 | 등록 상태의 requestId·state 누락 | DeviceAgent에서 입력 오류 처리 |
| BC-03 | 개인화 revision을 Long 대신 String으로 전송 | 잘못된 타입 거절, AI-Agent 스냅샷 변경 없음 |
| BC-04 | 상태의 progress·stateSequence 누락 | 현재 기본값 처리/순번 거절을 확인; 엄격한 계약 검증과의 차이 기록 |
| BC-05 | UI 실행 불가능한 원격 등록 요청 | -3과 상태 callback 미도착을 구분해 실패 보고 |
| BC-06 | 삭제 성공 후 DeviceAgent 사본 제거 실패 주입 | 성공 결과와 잔여 사본 불일치를 검출; 서버까지 삭제 완료로 표시 금지 |
14. 구현 출처와 적용 범위
코드 대조 기준: AI-Agent HEAD 091e52bc, SoC HEAD e54419a와 현재 작업본의 미커밋 변경을 포함한다.
커밋만 checkout한 형상과 이 명세의 작업본이 같다고 가정하지 않는다.
- 등록 입력·상태·commit 데이터: AI-Agent
service/VoiceProfileIpcContract.kt. - 녹음/취소/재시도/관리 처리: AI-Agent
service/ForegroundService.kt의 handleVoiceProfileCommand부터 sendVoiceProfileState. - Binder 진입점/결과 배분: DeviceAgent
DeviceAgent.java의 sendModuleCommand. - 타입·호출자 검사: DeviceAgent
voiceprofile/VoiceProfileContract.java. - Proxy 송신: Registration App
DeviceAgentVoiceProfileClient.kt. - 각 필드 표의 클라이언트 요구사항과 현재 수신 코드 기본값은 구분했다. 미구현 Cloud ACK는 함수 이름을 임의로 만들어 호출하지 않는다.