AI-Agent 화자인식 내부 구조·설계
09-17 변경: 관심사 원본은 서버로 이동했다. 서버 중심 구조·신규 Binder·MQTT. AI-Agent는 요청 결과만 임시 보관하며, 기존 로컬 관심사 테이블은 자동 이전하거나 사용하지 않는다. 이름·발음의 서버 최신값 동기화와 LLM 프롬프트 주입은 별도 작업이다.
기기 안에서 음성을 등록하고, 발화자를 식별하고, 화자별 기록을 관리하는 AI-Agent의 내부 설계다. 외부 Bundle 명세를 반복하기보다 어떤 클래스가 어떤 데이터를 받아 처리·저장하는지 설명한다.
검토 기준은 skmagic_ondeviceai_agent_speaker_recognition의 HEAD 091e52bc와 현재 미커밋 작업본이다. 아래 수치는 코드 기본값이며 정확도·지연·메모리의 실기기 측정 결과가 아니다. Android 코드 수정이나 APK 배포는 이번 문서 작업에 포함하지 않았다.
1. 전체 구조와 책임
| 주체·클래스 | 목적 | 입력 → 처리 → 출력 |
|---|---|---|
| 등록 App | 사용자 등록 화면 | 버튼 → DeviceAgent에 요청; 상태 callback → 화면 갱신 |
| DeviceAgent | 기기 앱 사이 중계 | Bundle 검사·대상 선택; 프로필 대기 저장·ACK |
ForegroundService |
AI-Agent 처리 조정 | Binder 명령·녹음 이벤트 → 세션·모델·저장 호출 → 상태/결과 Bundle |
AudioRecordManager |
음성 캡처 관리 | 등록 전용 clientId와 callback → PCM 샘플 |
VoiceProfileAudioQualityGate |
등록 음성의 신호 품질 확인 | FloatArray → 길이·음량·잡음 검사 → 수락/사유 |
VoiceProfileEmbeddingQualityGate |
세트 간 화자 일관성 확인 | 여러 임베딩 → 유효성·최소 쌍별 유사도 → 수락/사유 |
SpeakerIdentificationEngine |
임베딩 추출·등록 화자 검색 | 음성/기준 임베딩 → CPU 추론·코사인 비교 → voiceId 후보·점수 |
DiarizationEngine |
한 턴의 화자 구간 분리 | 전체 음성 → 시작/끝 시각·임시 speakerId |
SpeakerTurnAnalyzer |
한 턴 식별 파이프라인 | 구간 분리 → 화자별 임베딩 평균·검색 → 대표 화자 결과 |
DominantSpeakerResolver |
대표 화자 결정 | 구간 길이·라벨·점수 → voiceId 또는 미식별 |
ConversationSpeakerBinding |
후속 턴 화자 유지 | 이번 식별 결과·대화 지속 여부 → 기록 귀속 voiceId |
SpeakerVoiceProfileStore |
등록 프로필 저장 | UUID·닉네임·발음·기준 임베딩 → 파일 |
LegacyVoiceMemoryCleanup |
구형 DB 정리 | 서비스 시작 시 DB와 보조 파일 제거; 프로필·임베딩 보존 |
등록 App과 DeviceAgent는 화자인식 모델을 실행하지 않는다. AWS 연결도 AI-Agent가 직접 소유하지 않으며 외부 전달은 DeviceAgent를 경유한다.
2. 식별자와 데이터 구분
| 값 | 생성·범위 | 쓰임 |
|---|---|---|
requestId |
외부 등록/관리 요청이 제공 | 같은 작업의 요청·상태·ACK 연결 |
voiceId |
새 등록의 createNew()가 UUID 발급 |
프로필·임베딩·관심사·대화의 화자 키 |
nickname / displayName |
사용자 입력 | 화면 표시 이름. 고유 식별자가 아님 |
speakerId:Int |
구간 분리 모델이 한 음성 안에서 생성 | 임시 군집 번호. 영구 voiceId나 계정 ID가 아님 |
sessionId |
현재 저장 함수에서 매번 UUID 생성 | 현재는 여러 턴을 묶는 안정적인 대화 세션 키가 아님 |
모델이 UUID를 추출하는 것이 아니다. 앱이 등록 시 UUID를 만들고 기준 임베딩에 연결한다. 인식 모델의 비교 결과로 그 UUID를 찾아오는 구조다. 예전 수동 WAV 등록에는 이름 기반 키 경로도 남아 있으므로 새 등록과 구분한다.
3. 등록 요청에서 녹음까지
- 등록 App이 DeviceAgent를 통해
startVoiceProfileEnrollment를 보낸다. handleVoiceProfileCommand()가 계약·requestId·입력을 검사하고 mainHandler에서 분기한다.startVoiceProfileSession()이 닉네임·발음·다른 활성 세션을 확인한다. 받은 locale이 비어 있으면ko-KR을 사용한다.- 이전 수락 세트와 pending 값을 비우고 메모리에
VoiceProfileEnrollmentSession을 만든다. beginVoiceProfileSetCapture()가 기존 연속 녹음을 정지하고 등록 전용 callback을 요청한다.- 녹음 확보 성공이면
RECORDING_SET; 실패하면SET_REJECTED와AUDIO_BUSY를 보낸다. - 타이머로 캡처를 끝내고 샘플을 품질 검사에 전달한다.
| 시간·형식 | 코드값 | 의미 |
|---|---|---|
| 샘플레이트 | 16,000 Hz | 등록·식별 FloatArray 처리 기준 |
| 녹음 종료 타이머 | 5,200 ms | VAD 발화 종료가 아니라 고정 시간 종료 |
| 진행률 목표 | 80,000 samples, 5초 | 녹음 중 0~99; 수락 시 100 |
| 진행률 갱신 | 500 ms | 상태 변화 시 callback |
| 녹음 세트 수 | 5 | 세트별 음질 확인 후 모두 모아 임베딩 검사 |
| 수락 화면 유지 | 700 ms | 다음 세트 또는 개인화 선택으로 이동 |
음성 입력은 Service에서 받는다. 등록 App에서 WAV를 만들어 보내는 방식이 아니다. 이 경로는 STT로 문장을 정확히 읽었는지 검증하지 않는다.
4. 음질 검증 알고리즘
VoiceProfileAudioQualityGate.evaluate()는 아래 순서로 검사한다. 먼저 실패한 조건의 reasonCode를 반환한다.
| 검사 | 기본 조건 | 실패 코드 |
|---|---|---|
| 음성 존재 | samples가 null이면 거절 | NO_SPEECH |
| 전체 길이 | 최소 3,000 ms | TOO_SHORT |
| 샘플 숫자 | 모든 값이 유한값 | INVALID_AUDIO |
| 전체 RMS | 0.005 이상 | TOO_QUIET |
| 클리핑 | 절댓값 0.98 이상인 샘플 비율 ≤ 0.03 | TOO_LOUD |
| DC 편향 | 샘플 평균 절댓값 ≤ 0.05 | AUDIO_BIAS |
| 추정 발화량 | 합계 ≥ 1,500 ms, 연속 ≥ 300 ms | INSUFFICIENT_SPEECH |
| 추정 신호/잡음 비 | 계산 가능한 경우 ≥ 5 dB | TOO_NOISY |
프레임은 20 ms다. 프레임 RMS의 하위 약 20% 지점을 잡음 바닥으로 사용하고, 전체 RMS와 고정 하한 0.007을 이용해 적응형 발화 임계값을 만든다. 임계값 이상인 프레임의 총 길이와 최장 연속 길이를 센다.
추정 SNR은 발화/비발화로 분류된 프레임 에너지의 비율이다. 독립적으로 측정한 실제 환경 SNR과 같지 않다. 계산 불가능하면 null이며 이때 SNR 항목은 거절하지 않는다. 이 검사는 신호 품질 휴리스틱이지 사람 음성·정확한 문장·생체 진위의 증명은 아니다.
5. 임베딩 생성과 프로필 발급
각 세트가 통과하면 PCM을 voiceProfileAcceptedSets에 보관한다. 다섯 세트 완료 후 PERSONALIZATION_SETTINGS에서 사용자 선택을 받고 finalizeVoiceProfile()로 넘어간다.
- 수락 PCM을 복사해
speakerInferenceDispatcher에서 처리한다. SpeakerIdentificationEngine으로 세트별 임베딩을 만든다. 임시 엔진은 finally에서 release한다.- 모든 임베딩의 차원 일치·유한값·0이 아닌 norm을 검사한다.
- 모든 세트 쌍의 코사인 유사도를 비교한다. 최소값이 0.45 미만이면
INCONSISTENT_SPEAKER다. - 통과하면
createNew()로 UUID·프로필을 만들고 임베딩을 평균한 뒤 정규화한다. - 기준 임베딩 파일과 DB의 voiceId/닉네임 연결을 저장한다.
- 식별 모델을 재초기화하여 새 화자를 검색 목록에 반영한다.
- 전달할 프로필을 pending에 보관하고 DeviceAgent로
commitVoiceProfile을 보낸다.
차원·유한값·norm 실패는 INVALID_EMBEDDING이다. 품질 실패는 녹음 세트를 비우고 1번 세트부터 SET_REJECTED로 돌아간다. 일반 생성 오류는 PROFILE_FAILED/EMBEDDING_CREATION_FAILED이며 이때도 세트를 비우므로 생성 재시도가 항상 가능한 것은 아니다.
6. 저장 위치와 원본 음성 수명
| 데이터 | 저장 위치·소유자 | 수명·주의 |
|---|---|---|
| 현재 등록 PCM | Service 메모리 FloatArray | 수락 세트 누적; 생성 완료·실패·취소 시 참조 정리 |
| 이름·발음·UUID | filesDir/speaker_id/{voiceId}/profile.json |
등록 프로필 |
| 기준 임베딩 | 같은 폴더의 reference_embedding.bin |
차원과 float 값; 최대 4096차원 검증은 상한이지 모델 차원 고정값 아님 |
| 대화내역 DB | voice_memory.db 제거 |
신규 생성·저장·조회 없음; 다음 차수 기능 |
| 전송 대기 프로필 | AI-Agent 메모리 pending | 프로세스 재시작 후 복원되지 않음 |
| DeviceAgent 전달 사본 | DeviceAgent 암호화 대기 저장 | AI-Agent 로컬 파일·DB와 별도 수명 |
새 등록 v2 경로는 원본 WAV를 영구 저장하지 않는다. 그러나 이전 수동 등록은 WAV를 저장·재로딩하는 경로가 남아 있다. 일반 대화의 음성 로깅/업로드 또한 등록 PCM 수명 정책과 별개다. 메모리 목록을 clear하는 것은 참조 해제이며 즉시 메모리 0 덮어쓰기나 포렌식 삭제를 보장하지 않는다.
파일은 .pending에 기록 후 교체하는 방식이지만 JSON·임베딩·DeviceAgent 사본 전체를 하나의 트랜잭션으로 묶지는 않는다. 일부 실패·삭제 시 정합성 검증이 필요하다.
7. DeviceAgent 전달과 완료 조건
AI-Agent가 전달하는 주요 값은 requestId, voiceId, nickname, 발음, locale, personalizationEnabled, referenceEmbedding이다. 임베딩은 Binder의 FloatArray이며 PCM/WAV가 아니다.
| ACK | AI-Agent 상태 |
|---|---|
| accepted=true, cloudSynced=true | COMPLETED, progress=100, terminal=true |
| accepted=true, cloudSynced=false | DELIVERY_FAILED, 사유 없으면 WAITING_CLOUD_SYNC |
| accepted=false | DELIVERY_FAILED, 사유 없으면 DEVICE_DELIVERY_REJECTED |
ACK는 voiceProfileCommitResult이며 requestId로 현재 세션·pending과 대응한다. 실제 ACK에는 voiceId가 없다. DeviceAgent의 로컬 시험 ACK는 실제 Cloud 저장 완료를 증명하지 않는다. 제품 임베딩 업로드와 Cloud ACK는 미연결이다.
로컬 프로필 저장과 모델 재로딩이 ACK보다 먼저 일어나므로 전달 실패 상태라도 로컬 식별은 가능한 상황이 생길 수 있다. UI 완료와 로컬 등록 존재를 같은 조건으로 가정하면 안 된다.
8. 일반 대화에서 화자를 찾는 순서
- 정상 녹음 종료에서 post-WUW PCM을 확보한다. 취소 종료 경로와 구분한다.
- STT 해독과 화자 분석을 병행하고 STT 결과 처리 전에 화자 분석 결과를 기다린다.
SpeakerTurnAnalyzer.analyze()가 등록 모델·화자 수를 확인한다. 미설정이면 configured=false다.- 입력이 8,000 samples 미만이면
NO_SPEECH를 반환한다. 16 kHz에서 0.5초에 해당한다. DiarizationEngine이{startSec,endSec,speakerId}구간을 만든다.- 구간별 임베딩을 추출하고 같은 임시 speakerId의 임베딩을 평균한다.
- 등록 기준 임베딩과 코사인 유사도를 계산해 가장 높은 voiceId와 점수를 얻는다. 기본 0.45 미만이면 unknown이다.
DominantSpeakerResolver가 발화 시간 비중까지 검토해 한 턴의 최종 화자를 고른다.
이것은 녹음 중 즉시 이름이 바뀌는 스트리밍 식별이 아니라 완료된 한 턴 분석이다. STT와 병행해도 화자 결과 await가 있으므로 느린 식별은 다음 처리의 지연에 영향을 줄 수 있다.
9. 대표 화자 선택 규칙
| 기준 | 기본값·계산 |
|---|---|
| 주 화자 비율 | 해당 voiceId 발화 시간 / 전체 유효 구간 시간 ≥ 0.60 |
| 2위와의 차이 | (1위 시간 − 2위 시간) / 전체 시간 ≥ 0.20 |
| 신뢰 점수 | 해당 화자의 구간 길이 가중 점수 ≥ 0.45 |
unknown 구간도 전체 시간 분모에 포함한다. 따라서 일부 구간만 높은 점수로 맞았다고 바로 전체 발화를 그 화자에게 귀속하지 않는다. 시간은 구간 길이 합계이며 겹치는 구간의 합집합 길이를 계산하는 구현은 아니다.
결과 reason은 ACCEPTED, NO_SPEECH, NO_KNOWN_SPEAKER, INSUFFICIENT_SPEECH_SHARE, INSUFFICIENT_LEAD, LOW_CONFIDENCE다. 점수는 인증 확률이 아니다. 화자식별 결과만으로 결제·보안 권한을 부여하는 구조로 사용해서는 안 된다.
10. 후속 대화 화자 유지와 호칭
| 이전·이번 결과 | 대화 조건 | 귀속 |
|---|---|---|
| A → unknown | 후속 대화 | A 유지 |
| A → 등록 화자 B | 후속 대화 | B로 변경 |
| unknown | 새 대화 | 미귀속 |
| 종료·삭제 | 유지 조건 종료 또는 같은 화자 삭제 | binding 해제 |
processVoiceCmd()는 cloud continuation, 일반 multiTurnBusy, 스케줄·나이트모드 활성 세션을 후속 대화로 판단한다. resetState()는 이 조건이 모두 꺼졌을 때 binding을 비운다.
화면 표시 이름과 TTS 발음은 SpeakerVoiceProfile.addressNames()에서 나뉜다. 닉네임을 voiceId로 다시 검색하거나 모델에서 한국어 이름을 생성하는 것은 아니다. 알파벳 닉네임의 발음은 사용자 확인 발음 데이터의 품질에 영향을 받는다.
11. 대화·관심사 저장과 이벤트
제품 소비 설계: 프롬프트 작성 직전 적용·일반 응답 전환, App 수정 반영 시점, 철회·삭제 후 무효화, 검증 조건을 함께 본다. 현재는 조회 결과 getter까지 구현되어 있으며 제품 제안의 자동 활용·원격 철회 처리와 구분한다.
대화내역 저장 함수와 외부 조회·삭제·용량 조회·저장 이벤트는 이번 차수에서 제거했다.
일반 LLM 멀티턴 문맥은 유지하지만 화자별 대화내역 DB에는 저장하지 않는다.
| 설정 | 활성 조건 |
|---|---|
구형 voiceHistoryEnabled |
지원하지 않음. 설정해도 대화내역을 활성화하지 않음 |
persist.sys.debug.deviceagent.LlmManager.voiceInterestEnabled |
문자열 true일 때 관심사 기능 활성 |
관심사는 서버에 voiceId별 전체 코드 목록으로 저장한다. AI-Agent의 기존 interestsSet/Get 경로는 SERVER_OWNED_PERSONALIZATION을 반환한다. 이전 대화 DB 용량 정책은 현재 서비스에서 사용하지 않는다.
서버 관심사 조회의 내부 순서
| 단계 | 실제 코드 | 동작 |
|---|---|---|
| 화자 확정 | processVoiceCmd / ConversationSpeakerBinding | 현재 대화에 귀속할 voiceId 결정 |
| 새 조회 | requestVoicePersonalization | 이전 pending·snapshot 비움. ID와 기능 플래그가 유효할 때 IO coroutine에서 getVoicePersonalization 전송 |
| 요청 추적 | VoicePersonalizationRequest | requestId·voiceId·5초 deadline 한 건만 보관 |
| 결과 수신 | handleVoicePersonalizationResult | 현재 요청·화자 일치, 만료 전, 기한 연장 없음 검사. 같은 응답은 한 번만 수락 |
| 임시 보관 | PersonalizationSnapshot | 성공·개인화 허용·revision>0·유효 코드일 때 목록 한 건 보관 |
| 소비 | currentServerInterests(voiceId) | 같은 화자이며 만료 전인 목록 사본 반환. 실패·unknown·만료 시 빈 목록 |
| 정리 | resetState / onDestroy / 프로필 삭제 | 요청과 snapshot 무효화; 이미 보낸 네트워크 요청 자체를 취소한 것은 아님 |
5초는 요청 생성부터 계산한 전체 유효기간이며 응답 수신 후 추가 5초가 아니다. 취소 뒤 늦게 도착한 결과는 pending이 없어 사용하지 않는다. 타이머로 객체를 즉시 삭제하는 구조가 아니라 읽을 때 만료를 확인하며, 보관 건수는 각각 한 건으로 제한한다.
새 턴에서 비동기 조회하므로 STT·LLM 응답을 이 결과 때문에 기다리게 하지 않는다. 현재 getter를 통한 전달까지만 구현되어 있고 LLM 프롬프트 자동 주입은 미구현이다. 오프라인에서는 일반 대화와 로컬 화자 인식을 유지하며 예전 로컬 관심사를 대체값으로 읽지 않는다.
12. 실행 스레드와 모델 자원
| 자원 | 관리 |
|---|---|
| 세션 분기·녹음 타이머 | mainHandler |
| 모델 초기화·분석·생성 및 관련 저장 | 단일 speaker-cpu-inference executor의 coroutine dispatcher |
| 엔진 내부 추론 | CPU provider, 기본 스레드 2 |
| 모델 assets | embedding.onnx, sherpa-onnx-speaker-diarization/segmentation.onnx |
| 모델 스트림 | 임베딩 추출의 finally에서 release |
| 장기 모델 | Analyzer가 소유; 재초기화 전 release, Service 종료 경로에서 정리 |
단일 dispatcher는 같은 경로의 추론 작업을 줄 세우지만 모든 Binder·타이머·네이티브 경합을 자동으로 해결하지 않는다. 등록 5세트의 5초 Float PCM만 약 1.6 MB이며, 생성 시 복사본·모델·STT·일반 녹음 버퍼는 별도다. 이 계산은 전체 메모리 사용량이나 누수 없음의 증명이 아니다.
구간 분리 clustering threshold는 0.75, minDurationOn은 0.4초, minDurationOff는 0.5초다. 등록 일관성 0.45 및 등록 화자 매칭 0.45와 서로 다른 파라미터다.
13. 취소·실패·wakeword 연계의 한계
| 상황 | 현재 처리 | 남은 확인·보완 |
|---|---|---|
| 녹음 중 취소 | 타이머·캡처 정지, 수락 세트·pending 정리, CANCELLED | 생성 coroutine이 이미 시작된 경우 늦은 완료 방어 |
| 나가기 확인창 | RECORDING_SET일 때 캡처 정지 | 별도 pause ACK 없음; 취소/재시도는 별도 요청 |
| 품질 불일치 | 1세트부터 재녹음 | 사용자 음성/환경에 대한 임계값 실기기 검증 |
| 생성 실패 | 세트 제거·PROFILE_FAILED | 기존 녹음 없는 생성 재시도 무동작 가능 |
| 프로필 전달 실패 | pending 유지·DELIVERY_FAILED | 재부팅 pending 복원·Cloud ACK 구현 |
| 동일 start 재수신 | 세트 초기화 가능 | 중복 제거 계약 필요 |
| 700 ms 지연 전환 | mainHandler Runnable | 취소/다른 세션 이후 실행 방어 필요 |
| 등록과 일반 음성 | 등록 캡처 시작 시 STOP 상태·연속 녹음 중지 | 모든 wakeword/F키/외부 명령 진입점의 등록 세션 차단을 별도 검증해야 함 |
| 삭제 | 로컬 파일·DB·모델 갱신 | 부분 실패·DeviceAgent/Cloud 사본 삭제 정합성 |
STT 문장 일치, 재생 음성 공격 방지, 생체 진위 확인은 현재 품질 게이트가 제공하지 않는다. 위 한계는 이번 문서에서 해결된 것으로 표시하지 않는다.
14. 검증 항목과 구현 근거
| 검증 | 확인할 결과 |
|---|---|
| 음질 경계 | 짧음·무음·클리핑·잡음·NaN 각각 reasonCode |
| 등록 세트 | 같은 사람 5세트, 다른 사람 섞임, 전체 재녹음 |
| ID·저장 | 같은 닉네임 새 등록에도 UUID 분리; 파일·DB·검색 목록 일치 |
| 식별 | A/B/미등록, 짧은 발화, 여러 화자, 겹침, 저신뢰 |
| 후속 턴 | A→unknown 유지, A→B 변경, 새 대화 unknown 미귀속 |
| 취소 | 녹음·700 ms 지연·임베딩 생성·ACK 대기 각각 취소 |
| 저장·복구 | DB 실패, 전송 실패, 재부팅, 삭제 후 재인식 |
| 자원 | 반복 등록·삭제·일반 대화 후 native/Java 메모리와 마이크 점유 |
이 표는 시험 계획이며 전 항목 통과 선언이 아니다. 문서 검증과 실제 Android/JNI/Binder/음향 검증은 구분한다.
소스 경로는 모두 app/src/main/java/com/skmagic/ondeviceai/agent/ 기준이다.
| 파일 | 확인한 핵심 함수·값 |
|---|---|
service/ForegroundService.kt |
handleVoiceProfileCommand, beginVoiceProfileSetCapture, finalizeVoiceProfile, handleVoiceProfileCommitResult, processVoiceCmd, persistSpeakerHistoryTurn |
service/VoiceProfileAudioQualityGate.kt |
evaluate, VoiceProfileAudioQualityConfig |
service/VoiceProfileEmbeddingQualityGate.kt |
evaluate |
service/SpeakerTurnAnalyzer.kt |
initialize, analyze, release |
service/module/DiarizationEngine.kt |
CPU 설정, diarize |
service/module/SpeakerIdentificationEngine.kt |
추출·코사인 검색·스트림 해제 |
service/DominantSpeakerResolver.kt |
resolve |
service/ConversationSpeakerBinding.kt |
resolve, clearIfMatches, clear |
service/SpeakerVoiceProfile.kt |
createNew, persistReferenceEmbedding, listRegisteredProfiles |
service/LegacyVoiceMemoryCleanup.kt |
구형 DB 및 WAL/SHM/journal 정리; 등록·삭제는 프로필 파일만 사용 |
service/VoiceMemoryFeatureFlags.kt |
기능 활성 조건 |
15. 비동기 작업과 데이터 수명
아래는 ForegroundService.finalizeVoiceProfile() 및 취소·오디오 복구 경로를 2026-09-18에 확인한 내용이다. 단일 추론 스레드가 있어도 화면 종료와 추론 완료의 순서는 달라질 수 있다.
| 단계 | 실제 데이터·실행 주체 | 정리 시점 | 주의점 |
|---|---|---|---|
| 녹음 | AudioRecordManager callback과 등록 샘플 버퍼 | 캡처 정지 시 callback 해제, 버퍼 회수 | 세트 캡처는 고정 5.2초, 진행 이벤트 500ms 간격. VAD가 발화 끝을 판단해 즉시 종료하는 방식이 아님 |
| 수락 세트 | voiceProfileAcceptedSets의 Float PCM |
취소 또는 생성 결과 처리에서 clear | 목록을 비워도 이미 만든 복사본까지 사라지지는 않음 |
| 임베딩 생성 | capturedSets = ...map { it.copyOf() }, speakerInferenceDispatcher |
생성 coroutine 종료 후 참조 해제 대상 | 취소가 생성 Job을 직접 중단하는 구조는 확인되지 않음 |
| 임시 추론 엔진 | 생성 coroutine 안의 엔진 | finally에서 release |
네이티브 메모리 반환량은 실측 필요 |
| 프로필 저장 | UUID 프로필 → 임베딩 파일 → DB 이름 연결 → 인식 모델 재초기화 | 각 저장 단계별 처리 | 하나의 원자적 트랜잭션이 아님 |
| 전달 대기 | pendingVoiceProfileDelivery |
완료·취소·화면 닫기 처리 | AI-Agent 메모리 pending과 DeviceAgent 영속 전달 대기 저장소는 별개 |
취소와 생성 완료가 겹치는 경우
- 생성 함수가 현재 세션과 PCM을 복사하고 추론을 시작한다.
- 사용자가 나가면 취소 함수는 타이머·캡처·원래 세트·pending을 정리하고
CANCELLED를 발행한다. - 이미 시작한 생성 coroutine이 성공하면 현재 코드의 Main 복귀 블록은 캡처했던
session.copy(...)를 다시 대입하고 전달을 시작할 수 있다.
현재 남은 보완: Main 복귀 직전에 현재 세션의 요청 ID·취소/종료 여부를 재검사하고, 취소된 생성 작업이 만든 파일·DB 연결도 일관되게 정리해야 한다. 이 문서 수정에서 런타임 방어를 추가한 것은 아니다.
부분 실패와 복구 경계
| 실패 위치 | 코드에서 확인되는 처리 | 추가로 확인해야 할 잔여 상태 |
|---|---|---|
| UUID 발급 후 저장·재초기화 실패 | createdVoiceId가 있으면 프로필 삭제 시도 |
이미 생성한 DB 연결과 인식 모델 상태까지 함께 원복됐는지 |
| 임베딩 품질 불일치 | 세트 clear, 1세트부터 SET_REJECTED |
녹음 화면에서 다시 녹음할 수 있는지 |
| 일반 생성 오류 | 세트 clear, PROFILE_FAILED, 오디오 복구 |
retryVoiceProfileCreation의 입력 세트가 비어 재시도가 무동작하는 경우 |
| 오디오 재획득 실패 | requestAudioRecord 결과가 false이면 연속 녹음 재시작 안 함 |
이후 START 상태 설정과 실제 마이크 사용 가능 상태가 일치하는지 |
| 전달 ACK 없음 | pending 상태 유지 | 대기 제한·재부팅 후 복구는 별도 구현 필요 |
알고리즘·자원 시험표
아래는 실기기·음성 샘플 시험 계획이다. 문서 검사 통과를 음질 성능이나 메모리 누수 없음으로 해석하지 않는다.
| 번호 | 시험 입력·상황 | 관찰할 결과 |
|---|---|---|
| AR-01 | 무음·짧은 음성·클리핑·저음량 샘플 각각 입력 | 품질 게이트의 accepted와 reasonCode, UI 오류 문구가 일치 |
| AR-02 | 동일인 5세트와 다른 화자 혼합 5세트 | 세트 간 코사인 기준 0.45의 수락·거절, 오수락/오거절을 각각 기록 |
| AR-03 | 휴대폰으로 녹음된 타인 음성 재생 | 현 품질 게이트는 생체 진위 판정이 아님을 확인; 보안 인증으로 사용 금지 |
| AR-04 | 생성 중 취소 후 새 등록 시작 | 이전 요청의 파일 생성·상태 복귀·commit 발생 여부. 현재 경합 보완의 회귀 조건 |
| AR-05 | 등록·취소·삭제 반복 후 일반 대화 | Java heap, native PSS/RSS, 스레드·callback 수, 마이크 재사용을 반복 횟수별 비교 |
| AR-06 | 파일 저장·DB 갱신·모델 초기화 단계별 실패 주입 | 파일 목록·DB 연결·인식 가능한 voiceId의 불일치 검출 |