-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] OpenAI Realtime API 웹소켓으로 AI 전화 회화 기능 구현 #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2121ab0
7e27ec9
c5224ba
62828fa
c2e6f6b
82f1e23
538290e
266fbea
b7756eb
0871895
8af8278
9845e31
3a49ef0
fb16d31
ed04ebf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.saegil.data.model | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class RealtimeResponse( | ||
| val id: String, | ||
| val `object`: String, | ||
| val model: String, | ||
| val modalities: List<String>, | ||
| val instructions: String, | ||
| val voice: String, | ||
| val input_audio_format: String, | ||
| val output_audio_format: String, | ||
| val input_audio_transcription: InputAudioTranscription, | ||
| val turn_detection: String, // null 허용 | ||
| val tools: List<String> = emptyList(), | ||
| val tool_choice: String, | ||
| val temperature: Double, | ||
| val speed: Double, | ||
| val tracing: String, // "auto" | ||
| val max_response_output_tokens: Int, | ||
| val client_secret: ClientSecret | ||
| ){ | ||
| fun toDomain(): String { | ||
| return client_secret.value | ||
| } | ||
| } | ||
|
|
||
| @Serializable | ||
| data class InputAudioTranscription( | ||
| val model: String | ||
| ) | ||
|
|
||
| //@Serializable | ||
| //data class TurnDetection( | ||
| // // 현재 예시에선 null이므로 생략 가능. 나중에 구조 생기면 필드 추가 | ||
| //) | ||
| // | ||
| //@Serializable | ||
| //data class Tool( | ||
| // // 현재 예시에선 빈 객체 리스트, 추후 구조 생기면 필드 추가 | ||
| //) | ||
|
Comment on lines
+35
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| @Serializable | ||
| data class ClientSecret( | ||
| val value: String, | ||
| val expires_at: Long | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,15 @@ | ||
| package com.saegil.data.remote | ||
|
|
||
| import com.saegil.data.model.RealtimeResponse | ||
| import com.saegil.data.model.UploadAudioDto | ||
| import java.io.File | ||
|
|
||
| interface AssistantService { | ||
|
|
||
| suspend fun getAssistant(file: File, threadId: String?, scenarioId: Int): UploadAudioDto | ||
|
|
||
| suspend fun realtimeAssistant() | ||
|
|
||
| suspend fun getRealtimeToken(): String? | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,38 @@ | ||||||||||||||||||||||||||||||||||||||||
| package com.saegil.data.remote | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import android.util.Log | ||||||||||||||||||||||||||||||||||||||||
| import com.saegil.data.model.OrganizationDto | ||||||||||||||||||||||||||||||||||||||||
| import com.saegil.data.model.RealtimeResponse | ||||||||||||||||||||||||||||||||||||||||
| import com.saegil.data.model.UploadAudioDto | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.HttpClient | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.call.body | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.accept | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.forms.formData | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.forms.submitFormWithBinaryData | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.get | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.headers | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.request.parameter | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.client.statement.bodyAsText | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.ContentType | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.Headers | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.HttpHeaders | ||||||||||||||||||||||||||||||||||||||||
| import io.ktor.http.URLBuilder | ||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.Json | ||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.contentOrNull | ||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.jsonObject | ||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.jsonPrimitive | ||||||||||||||||||||||||||||||||||||||||
| import java.io.File | ||||||||||||||||||||||||||||||||||||||||
| import javax.inject.Inject | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class AssistantServiceImpl @Inject constructor( | ||||||||||||||||||||||||||||||||||||||||
| private val client: HttpClient | ||||||||||||||||||||||||||||||||||||||||
| ) : AssistantService { | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| override suspend fun getAssistant(file: File, threadId: String?, scenarioId: Int): UploadAudioDto { | ||||||||||||||||||||||||||||||||||||||||
| override suspend fun getAssistant( | ||||||||||||||||||||||||||||||||||||||||
| file: File, | ||||||||||||||||||||||||||||||||||||||||
| threadId: String?, | ||||||||||||||||||||||||||||||||||||||||
| scenarioId: Int | ||||||||||||||||||||||||||||||||||||||||
| ): UploadAudioDto { | ||||||||||||||||||||||||||||||||||||||||
| val response = client.submitFormWithBinaryData( | ||||||||||||||||||||||||||||||||||||||||
| url = HttpRoutes.ASSISTANT, | ||||||||||||||||||||||||||||||||||||||||
| formData = formData { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,4 +48,32 @@ class AssistantServiceImpl @Inject constructor( | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return response.body() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| override suspend fun getRealtimeToken(): String? { | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val urlBuilder = URLBuilder(HttpRoutes.GET_REALTIME_TOKEN) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| val response = client.get(urlBuilder.build()) { | ||||||||||||||||||||||||||||||||||||||||
| headers { | ||||||||||||||||||||||||||||||||||||||||
| accept(ContentType.Application.Json) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| val responseBody = response.bodyAsText() | ||||||||||||||||||||||||||||||||||||||||
| val json = Json.parseToJsonElement(responseBody) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // JSON 구조에서 client_secret.value 접근 | ||||||||||||||||||||||||||||||||||||||||
| val value = json | ||||||||||||||||||||||||||||||||||||||||
| .jsonObject["client_secret"] | ||||||||||||||||||||||||||||||||||||||||
| ?.jsonObject | ||||||||||||||||||||||||||||||||||||||||
| ?.get("value") | ||||||||||||||||||||||||||||||||||||||||
| ?.jsonPrimitive | ||||||||||||||||||||||||||||||||||||||||
| ?.contentOrNull | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Log.d("value", value.toString()) | ||||||||||||||||||||||||||||||||||||||||
| return value.toString() | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Manual JSON parsing is brittle and can lead to runtime errors if the API response structure changes. It's better to use Ktor's content negotiation feature to deserialize the response into the Additionally, the current implementation
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| override suspend fun realtimeAssistant() { | ||||||||||||||||||||||||||||||||||||||||
| TODO("Not yet implemented") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment indicates that
turn_detectioncan be null, but its type is a non-nullableString. This will cause aJsonDecodingExceptionif the API returnsnullfor this field. To prevent a crash, the type should be made nullable.