API 참조
핵심 컴포넌트
KlleonOndeviceSdk
모든 기능을 제공하는 메인 SDK 클래스입니다.
// 싱글톤으로 관리 (권장)
object SdkConfig {
val sdk = KlleonOndeviceSdk()
}
val klleonOndeviceSdk = SdkConfig.sdk
KlleonSdkView
SDK 콘텐츠를 렌더링하기 위한 커스텀 Android View입니다.
@Composable
fun SdkViewComponent() {
var workerView by remember { mutableStateOf<KlleonSdkView?>(null) }
AndroidView(
factory = { context ->
KlleonSdkView(context).also { view ->
workerView = view
}
},
modifier = Modifier.fillMaxSize()
)
}
setRenderRotation()
TV 등에서 화면 회전이 필요할 때 사용합니다.
klleonSdkView.setRenderRotation(270) // 0, 90, 270도 지원
ResourceManager
리소스 초기화 및 해제를 담당하는 클래스입니다. play() 전에 반드시 init()으로 리소스를 준비해야 합니다.
val resourceManager = ResourceManager()
init()
아바타 리소스를 캐시 디렉토리로 복사하고, YUV 변환 및 네이티브 메모리 적재를 수행합니다.
resourceManager.init(
context: Context,
mp4FileName: String,
npzFileName: String,
styleFileName: String = "",
faceCropYuvFileName: String = "",
faceCropYuvMetaFileName: String = "",
avatarId: String = "",
onSuccess: () -> Unit,
onError: (Throwable) -> Unit
)
매개변수:
| 매개변수 | 타입 | 필수 | 설명 |
|---|---|---|---|
context | Context | O | 현재 컨텍스트 |
mp4FileName | String | O | 아바타 MP4 파일명 (assets 내) |
npzFileName | String | O | 아바타 NPZ 파일명 (assets 내) |
styleFileName | String | X | 스타일 파일명 (기본값: "") |
faceCropYuvFileName | String | X | 사전 추출된 face crop YUV 파일명 (기본값: ""). 제공 시 warpAffine 단계 스킵하여 추론 경로 단축 (LIPSYNC-007, v0.6.0) |
faceCropYuvMetaFileName | String | X | face crop YUV 메타 파일명 (기본값: ""). faceCropYuvFileName과 함께 사용 |
avatarId | String | X | 아바타 ID. 동일 아바타 재진입 시 버퍼를 재사용하여 즉시 onSuccess 반환 |
onSuccess | () -> Unit | O | 리소스 준비 완료 콜백 |
onError | (Throwable) -> Unit | O | 에러 콜백 |
예제:
resourceManager.init(
context = this,
mp4FileName = "loop_sample.mp4",
npzFileName = "loop_sample.npz",
styleFileName = "loop_sample.style",
avatarId = "character_001",
onSuccess = {
// 준비 완료 — play()는 suspend이므로 코루틴에서 호출
CoroutineScope(Dispatchers.IO).launch {
klleonOndeviceSdk.play(context, sdkView, sdkKey, avatarId, language)
}
},
onError = { error ->
Log.e("SDK", "리소스 준비 실패", error)
}
)
동일한 아바타(avatarId)로 init()을 재호출하면 이미 준비된 버퍼를 재사용하여 즉시 onSuccess를 반환합니다. 다른 아바타로 전환 시에는 이전 버퍼를 해제하고 새로 할당합니다.
deinit()
네이티브 YUV 버퍼 메모리를 해제하고 리소스 상태를 초기화합니다.
resourceManager.deinit()
deinit()은 여러 번 호출해도 안전합니다. 이미 해제된 상태에서 호출하면 no-op입니다.
호출 규칙
init()— 아바타 선택 완료 시 호출. 아바타 변경 시 이전 버퍼를 자동 해제합니다.play()—init()의onSuccess콜백이 왔을 때만 호출 가능deinit()— 앱 완전 종료 시 호출.finish()는 버퍼를 유지하여 동일 아바타 재진입을 최적화합니다.
SDK 메서드
play()
AI 아바타 비디오 재생을 시작합니다. suspend 함수이므로 코루틴 내에서 호출해야 합니다.
suspend fun play(
context: Context,
klleonSdkViewInstance: KlleonSdkView?,
sdkKey: String,
avatarId: String,
language: String
)
매개변수:
| 매개변수 | 타입 | 필수 | 설명 |
|---|---|---|---|
context | Context | O | 현재 컨텍스트 |
klleonSdkViewInstance | KlleonSdkView? | O | 렌더링 뷰 |
sdkKey | String | O | SDK 인증 키 |
avatarId | String | O | AI 아바타 ID |
language | String | O | 언어 코드 (ko_kr, en_us, ja_jp) |
예제:
CoroutineScope(Dispatchers.IO).launch {
workerView?.let { view ->
klleonOndeviceSdk.play(
context = this@Activity,
klleonSdkViewInstance = view,
sdkKey = "YOUR_SDK_KEY",
avatarId = "character_001",
language = "ko_kr"
)
}
}
connect()
WebSocket 재연결을 수행합니다.
klleonOndeviceSdk.connect()
finish()
SDK를 중지하고 모든 리소스를 정리합니다. suspend 함수이므로 코루틴 내에서 호출해야 합니다.
내부 버퍼는 동일 아바타 재진입 시 재사용을 위해 유지됩니다 (soft reset).
// Activity 종료 시
CoroutineScope(Dispatchers.Default).launch {
klleonOndeviceSdk.finish()
}
awaitFinish()
진행 중인 finish()가 완료될 때까지 대기합니다. 종료 중이 아니면 즉시 반환됩니다.
finish() 후 빠르게 play()를 재호출하는 경우, ResourceManager.init() 전에 호출하여 리소스 경합을 방지합니다.
// 빠른 재진입 시
CoroutineScope(Dispatchers.Default).launch {
klleonOndeviceSdk.awaitFinish() // 이전 종료 완료 대기
ResourceManager.init(context, ...)
}
메시지 API
sendMessage()
SDK의 메시징 시스템을 통해 메시지를 전송합니다.
klleonOndeviceSdk.sendMessage(text: String)
예제:
klleonOndeviceSdk.sendMessage("안녕하세요")
sendMessageEcho()
입력 텍스트를 그대로 TTS + 립싱크로 재생합니다 (LLM 바이패스). 고정 안내 멘트 등에 사용합니다.
klleonOndeviceSdk.sendMessageEcho(text: String)
sendChangeLanguage()
대화 중 언어를 동적으로 변경합니다.
klleonOndeviceSdk.sendChangeLanguage(language: String)
예제:
// 영어로 변경
klleonOndeviceSdk.sendChangeLanguage("en")
sendStopSpeech()
아바타의 현재 발화를 즉시 중단합니다.
klleonOndeviceSdk.sendStopSpeech()
Wakeword API
STT 인식 결과에서 등록된 wakeword를 감지하여 콜백을 전달합니다. Wakeword가 매칭되면 해당 STT 결과는 일반 대화 입력(STT_RESULT)에서 제외됩니다.
Wakeword 매칭은 기본적으로 비활성화 상태입니다. enterStandby() 호출 시 자동으로 활성화되고, exitStandby() 호출 시 자동으로 비활성화됩니다. registerWakewords()로 wakeword를 등록하더라도 standby 모드에 진입해야 매칭이 동작합니다.
play(language) 또는 sendChangeLanguage(language)로 설정한 언어가 STT 인식 언어로도 적용됩니다. Wakeword 매칭은 해당 언어의 STT 인식 결과에서 동작하므로, 각 언어의 STT가 인식할 수 있는 wakeword를 등록해야 합니다.
예시:
- 한국어 STT →
"영어","일본어","니혼고"등 한국어 발음 wakeword - 일본어 STT →
"韓国語","英語","コリアン"등 일본어 인식 wakeword - 영어 STT →
"Korean","Japanese"등 영어 wakeword
registerWakewords()
Wakeword를 등록합니다. 기존 wakeword를 전체 교체합니다. 등록만으로는 매칭이 동작하지 않으며, enterStandby() 진입 시 활성화됩니다.
fun registerWakewords(wakewords: Map<String, String>)
매개변수:
| 매개변수 | 타입 | 설명 |
|---|---|---|
wakewords | Map<String, String> | wakeword → 태그 맵. Wakeword가 감지되면 태그 값이 콜백으로 전달됨 |
예제:
// 언어 전환용 wakeword 등록
klleonOndeviceSdk.registerWakewords(mapOf(
"니혼고" to "ja_jp", "일본어" to "ja_jp",
"잉글리쉬" to "en_us", "영어" to "en_us",
"한국어" to "ko_kr", "코리안" to "ko_kr",
))
clearWakewords()
등록된 wakeword를 전체 초기화합니다. 초기화 후에는 wakeword 매칭이 동작하지 않으며, 모든 STT 결과가 STT_RESULT로 전달됩니다.
klleonOndeviceSdk.clearWakewords()
wakewordDetected
Wakeword 매칭 결과를 방출하는 SharedFlow입니다.
val wakewordDetected: SharedFlow<WakewordResult>
WakewordResult:
| 필드 | 타입 | 설명 |
|---|---|---|
wakeword | String | 매칭된 wakeword 원문 (예: "니혼고") |
tag | String | 등록 시 지정한 태그 (예: "ja_jp") |
fullText | String | STT가 인식한 전체 텍스트 |
예제:
// Wakeword 감지 시 언어 전환
LaunchedEffect(Unit) {
klleonOndeviceSdk.wakewordDetected.collect { result ->
// result.wakeword = "니혼고", result.tag = "ja_jp"
klleonOndeviceSdk.sendChangeLanguage(result.tag)
}
}
SDK는 wakeword가 무엇을 의미하는지 관여하지 않습니다. 태그 값은 앱이 자유롭게 정의할 수 있으며, 언어 코드 외에도 "mode_quiet", "stop" 등 어떤 문자열이든 사용 가능합니다.
대기화면(Standby) API
enterStandby()
대기화면 모드로 진입합니다. 파이프라인 전체를 정리(큐 비우기, 오디오 버퍼 폐기, 타이밍 초기화)한 후 비디오 디코딩과 오디오 재생을 일시정지합니다. 리소스는 유지됩니다.
klleonOndeviceSdk.enterStandby()
exitStandby()
대기화면 모드를 해제합니다. 비디오 디코딩과 오디오 재생을 재개합니다.
klleonOndeviceSdk.exitStandby()
isStandby()
대기화면 모드 상태인지 확인합니다.
val isStandby: Boolean = klleonOndeviceSdk.isStandby()
setIdleTimeout(timeoutMs)
idle timeout을 활성화합니다. 아바타 발화 종료(TTS_END) 후 지정 시간 동안 무활동 시 자동으로 enterStandby()를 호출합니다. STANDBY_ENTER 이벤트가 sdkState로 발행된 후 대기화면에 진입합니다.
// 기본값 60초
klleonOndeviceSdk.setIdleTimeout()
// 커스텀 timeout (30초)
klleonOndeviceSdk.setIdleTimeout(30_000L)
play() 호출 전후 어느 시점에든 호출 가능합니다. 호출하지 않으면 자동 대기화면 기능은 비활성화 상태입니다.
disableIdleTimeout()
idle timeout을 비활성화합니다. 활성화된 타이머가 있으면 취소됩니다.
klleonOndeviceSdk.disableIdleTimeout()
isResponding: StateFlow<Boolean>
아바타가 응답 준비 중이거나 발화 중이면 true. PREPARING_RESPONSE 수신 시 true, TTS_END 또는 ERROR 수신 시 false로 전환됩니다.
// 현재 값 확인
if (klleonOndeviceSdk.isResponding.value) {
// 아바타 응답 중
}
// Flow로 구독
klleonOndeviceSdk.isResponding.collect { responding ->
// UI 업데이트
}
음성 인식 API
startVoiceRecognition()
음성 인식을 시작합니다. 마이크 녹음을 시작하고 WebSocket으로 START_VOICE 신호를 전송합니다. 무음이 감지되면 자동으로 녹음을 종료하고 END_VOICE 신호를 전송합니다.
fun startVoiceRecognition(
config: SilenceDetectionConfig = SilenceDetectionConfig.default()
): Boolean
매개변수:
config: 무음 감지 설정 (선택, 기본값:SilenceDetectionConfig.default())
반환값: 성공 여부 (Boolean)
예제:
// 기본 설정으로 음성 인식 시작
val success = klleonOndeviceSdk.startVoiceRecognition()
// 커스텀 설정으로 시작
val customConfig = SilenceDetectionConfig.default().copy(
silenceTimeoutMs = 2000L // 무음 감지 시간 2초
)
val success = klleonOndeviceSdk.startVoiceRecognition(customConfig)
stopVoiceRecognition()
음성 인식을 정지합니다.
klleonOndeviceSdk.stopVoiceRecognition()
isVoiceRecognizing()
음성 인식 중인지 확인합니다.
val isRecognizing: Boolean = klleonOndeviceSdk.isVoiceRecognizing()
voiceRecognitionMode
음성 인식 모드를 설정합니다. play() 호출 전에 설정해야 합니다.
klleonOndeviceSdk.voiceRecognitionMode = VoiceRecognitionMode.ALWAYS_ON
모드:
| 모드 | 설명 |
|---|---|
DISCRETE | 단발 모드 (기본값). 수동으로 시작/종료 |
ALWAYS_ON | 상시 리스닝. play() 후 자동 시작, 세션 유지 |
startAlwaysOnListening()
ALWAYS_ON 리스닝을 수동으로 시작합니다.
val success: Boolean = klleonOndeviceSdk.startAlwaysOnListening()
isAutoListening()
ALWAYS_ON 자동 리스닝 중인지 확인합니다.
val isListening: Boolean = klleonOndeviceSdk.isAutoListening()
오디오 입력 유틸리티
hasRecordAudioPermission()
RECORD_AUDIO 권한이 있는지 확인합니다.
val hasPermission: Boolean = klleonOndeviceSdk.hasRecordAudioPermission()
hasUsbMicrophone()
USB 마이크가 연결되어 있는지 확인합니다.
val hasUsb: Boolean = klleonOndeviceSdk.hasUsbMicrophone()
상태 관리
sdkState
SDK 상태 업데이트를 JSON 문자열로 방출하는 SharedFlow입니다.
klleonOndeviceSdk.sdkState: SharedFlow<String>
사용법:
klleonOndeviceSdk.sdkState.collect { state ->
val messageToAdd = try {
val json = JSONObject(state)
json.getString("message")
} catch (t: Throwable) {
state.toString()
}
// 상태 업데이트 처리
updateUI(messageToAdd)
}
상태 형식 (JSON):
{
"message": "",
"chat_type": "ACTIVATE_VOICE",
"time": "2025-09-03T05:42:08.394975295",
"id": "99dd3ab3-53c4-4418-bee4-b1fbaea272aa"
}
주요 chat_type 값:
| chat_type | 설명 |
|---|---|
WSS_OPEN | WebSocket 연결 성공 |
WSS_FAILURE | WebSocket 연결 실패 |
WSS_CLOSING | WebSocket 연결 종료 중 |
WSS_CLOSED | WebSocket 연결 종료됨 |
WSS_RETRY_OVER | 최대 재시도 초과 |
WSS_NOT_CONNECTED | 미연결 상태 |
ACTIVATE_VOICE | 음성 입력 수신 준비 완료 |
TEXT | 서버로부터 텍스트 응답 수신 |
RESPONSE_IS_ENDED | 서버 응답 스트림 종료 |
TTS_START | TTS 음성 재생 시작 |
TTS_END | TTS 음성 재생 완료 (발화 종료 감지에 사용) |
SHOW_AVATAR | 아바타 표시 준비 완료 |
USER_SPEECH_STARTED | 사용자 발화 시작 감지 |
USER_SPEECH_STOPPED | 사용자 발화 종료 감지 |
PREPARING_RESPONSE | 서버 응답 준비 중 |
STT_RESULT | 음성 인식 최종 결과 |
STT_PARTIAL | 음성 인식 중간 결과 |
STT_ERROR | 음성 인식 에러 |
STT_STATUS | 음성 인식 상태 변화 알림 (준비 중/재연결 중/준비 완료) |
WORKER_DISCONNECTED | 소켓 연결 종료 |
SPEECH_START | 아바타 발화 시작 |
SPEECH_END | 아바타 발화 종료 |
STOP_RESPONSE_MESSAGE | 현재 아바타 발화 중단 |
STT_STATUS 상세
음성 인식 세션의 일시적 상태 변화를 알려줍니다. 토큰 기반 인증을 사용하는 STT 제공자(Azure 등)에서 발생합니다.
| 상태 | message 값 | 의미 | 발생 시점 |
|---|---|---|---|
Preparing | 음성 인식 준비 중 | 토큰 대기 중으로 인식 시작이 지연됨 | 토큰 미수신 상태에서 음성 인식 시작 시 |
Recovering | 음성 인식 재연결 중 | 인증 실패로 토큰 재발급 대기 중 | 인식 중 인증 토큰 만료 시 |
Ready | 음성 인식 준비 완료 | 복구 완료, 음성 인식 재개 | 토큰 재발급 후 인식 재시작 시 |
STT_STATUS는 일시적 상태 알림입니다. 음성 인식 준비 중 또는 음성 인식 재연결 중 수신 시 사용자에게 토스트 등으로 안내하고, 음성 인식 준비 완료 수신 시 자연스럽게 해제하면 됩니다.
recorderState
녹음기 상태를 실시간으로 구독할 수 있는 StateFlow입니다.
klleonOndeviceSdk.recorderState: StateFlow<OboeRecorderState>
상태 값:
| 상태 | 설명 |
|---|---|
Idle | 초기 상태 |
Starting | 시작 중 |
Recording(deviceId, deviceName, deviceType) | 녹음 중 |
Stopping | 정지 중 |
Error(message) | 에러 상태 |
SilenceDetectionConfig
음성 인식 시 무음 감지 동작을 설정합니다.
import io.klleon.ondevice.audio.oboe.SilenceDetectionConfig
필드:
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
silenceThreshold | Float | 0.02 | 무음 판단 레벨 임계값 |
speechThreshold | Float | 0.05 | 음성 판단 레벨 임계값 |
speechTimeoutMs | Long | 5000 | 음성 입력 대기 시간 (ms) |
silenceTimeoutMs | Long | 2000 | 음성 후 무음 감지 시간 (ms) |
maxRecordingTimeMs | Long | 60000 | 최대 녹음 시간 (ms, 0=무제한) |
preBufferMs | Int | 300 | 음성 시작 전 오디오 보존 (ms) |
preBufferDelayMs | Long | 300 | 프리버퍼 플러시 지연 (ms) |
프리셋:
| 프리셋 | 특징 |
|---|---|
SilenceDetectionConfig.default() | 기본 설정 |
SilenceDetectionConfig.fast() | 빠른 응답 (speechTimeout 3초, silenceTimeout 1.5초) |
SilenceDetectionConfig.longSpeech() | 긴 발화 (speechTimeout 7초, silenceTimeout 3초, maxRecording 120초) |
SilenceDetectionConfig.noisyEnvironment() | 소음 환경 (임계값 상향: silence 0.05, speech 0.10) |
예제:
// 프리셋 사용
klleonOndeviceSdk.startVoiceRecognition(SilenceDetectionConfig.fast())
// 커스텀 설정
val config = SilenceDetectionConfig.default().copy(
speechTimeoutMs = 3000L,
maxRecordingTimeMs = 120_000L
)
klleonOndeviceSdk.startVoiceRecognition(config)
프로퍼티
klleonSdkView
현재 렌더링 뷰 참조입니다.
var klleonSdkView: KlleonSdkView?
Companion Object
| 프로퍼티 | 타입 | 설명 |
|---|---|---|
VERSION | String | SDK 버전 |
playing | Boolean | SDK 재생 상태 |
sendVoice | Boolean | 음성 전송 상태 |
인스턴스 프로퍼티
| 프로퍼티 | 타입 | 설명 |
|---|---|---|
isStopping | Boolean | SDK가 현재 종료 중인지 여부 (finish() 진행 중이면 true) |
오류 처리
모든 SDK 메서드는 try-catch 블록으로 감싸야 합니다:
try {
klleonOndeviceSdk.sendMessage(message)
} catch (e: Exception) {
Log.e("KlleonSDK", "작업 실패", e)
// 적절하게 오류 처리
}
라이프사이클 관리
play()와 finish()는 suspend 함수입니다. Activity 라이프사이클에서는 코루틴으로 래핑하여 호출합니다.
class ChatActivity : ComponentActivity() {
private val sdk = SdkConfig.sdk
override fun onDestroy() {
super.onDestroy()
// finish()는 suspend이므로 코루틴에서 실행
CoroutineScope(Dispatchers.Default).launch {
sdk.finish() // SDK 정지 (버퍼는 재사용을 위해 유지)
}
ResourceManager.deinit() // 앱 완전 종료 시에만 호출
}
}
빠른 재진입 (finish → play)
finish() 후 곧바로 play()를 호출하면 리소스 경합이 발생할 수 있습니다.
awaitFinish()를 사용하여 이전 종료가 완료된 후 진행하세요:
// 캐릭터 선택 화면에서 새 아바타 시작 시
CoroutineScope(Dispatchers.Default).launch {
sdk.awaitFinish() // 이전 finish()가 진행 중이면 완료 대기
ResourceManager.init(context, mp4, npz, style, avatarId,
onSuccess = { /* ChatActivity 시작 → play() */ },
onError = { /* 에러 처리 */ }
)
}