Skip to content

Commit 08c4e9c

Browse files
committed
feature: Add audio transcriptions entrypoint
1 parent 79faba6 commit 08c4e9c

File tree

18 files changed

+337
-28
lines changed

18 files changed

+337
-28
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package co.yml.ychat.domain.model
2+
3+
actual typealias FileBytes = ByteArray
4+
5+
actual fun FileBytes.toByteArray(): ByteArray {
6+
return this
7+
}

ychat/src/commonMain/kotlin/co/yml/ychat/YChat.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package co.yml.ychat
22

3+
import co.yml.ychat.entrypoint.features.AudioTranscriptions
34
import co.yml.ychat.entrypoint.features.ChatCompletions
45
import co.yml.ychat.entrypoint.features.Completion
56
import co.yml.ychat.entrypoint.features.Edits
@@ -137,6 +138,8 @@ interface YChat {
137138
*/
138139
fun edits(): Edits
139140

141+
fun audioTranscriptions(): AudioTranscriptions
142+
140143
/**
141144
* Callback is an interface used for handling the results of an operation.
142145
* It provides two methods, `onSuccess` and `onError`, for handling the success

ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package co.yml.ychat.data.api
22

3+
import co.yml.ychat.data.dto.AudioParamsDto
4+
import co.yml.ychat.data.dto.AudioResultDto
35
import co.yml.ychat.data.dto.ChatCompletionParamsDto
46
import co.yml.ychat.data.dto.ChatCompletionsDto
57
import co.yml.ychat.data.dto.CompletionDto
@@ -25,4 +27,6 @@ internal interface ChatGptApi {
2527
suspend fun models(): ApiResult<ModelListDto>
2628

2729
suspend fun model(id: String): ApiResult<ModelDto>
30+
31+
suspend fun audioTranscriptions(audioParamsDto: AudioParamsDto): ApiResult<AudioResultDto>
2832
}

ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package co.yml.ychat.data.api.impl
22

33
import co.yml.ychat.data.api.ChatGptApi
4+
import co.yml.ychat.data.dto.AudioParamsDto
5+
import co.yml.ychat.data.dto.AudioResultDto
46
import co.yml.ychat.data.dto.ChatCompletionParamsDto
57
import co.yml.ychat.data.dto.ChatCompletionsDto
68
import co.yml.ychat.data.dto.CompletionDto
@@ -13,6 +15,7 @@ import co.yml.ychat.data.dto.ModelDto
1315
import co.yml.ychat.data.dto.ModelListDto
1416
import co.yml.ychat.data.infrastructure.ApiExecutor
1517
import co.yml.ychat.data.infrastructure.ApiResult
18+
import co.yml.ychat.domain.model.toByteArray
1619
import io.ktor.http.HttpMethod
1720

1821
internal class ChatGptApiImpl(private val apiExecutor: ApiExecutor) : ChatGptApi {
@@ -62,4 +65,14 @@ internal class ChatGptApiImpl(private val apiExecutor: ApiExecutor) : ChatGptApi
6265
.setHttpMethod(HttpMethod.Get)
6366
.execute()
6467
}
68+
69+
override suspend fun audioTranscriptions(audioParamsDto: AudioParamsDto): ApiResult<AudioResultDto> {
70+
val byteArray = audioParamsDto.byteArray.toByteArray()
71+
val apiBuilder = apiExecutor
72+
.setEndpoint("v1/audio/transcriptions")
73+
.setHttpMethod(HttpMethod.Post)
74+
.addFormPart("file", audioParamsDto.filename, byteArray)
75+
audioParamsDto.getMap().forEach { apiBuilder.addFormPart(it.key, it.value) }
76+
return apiBuilder.execute()
77+
}
6578
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package co.yml.ychat.data.dto
2+
3+
import co.yml.ychat.domain.model.FileBytes
4+
5+
internal data class AudioParamsDto(
6+
val filename: String,
7+
val byteArray: FileBytes,
8+
val model: String,
9+
val prompt: String,
10+
val responseFormat: String,
11+
val temperature: Double,
12+
val language: String,
13+
) {
14+
15+
fun getMap(): Map<String, Any> {
16+
return mapOf(
17+
"model" to model,
18+
"prompt" to prompt,
19+
"response_format" to responseFormat,
20+
"temperature" to temperature,
21+
"language" to language,
22+
)
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package co.yml.ychat.data.dto
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
internal data class AudioResultDto(
8+
@SerialName("text")
9+
val text: String,
10+
)
Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package co.yml.ychat.data.infrastructure
22

3-
import co.yml.ychat.data.exception.ChatGptException
43
import io.ktor.client.HttpClient
5-
import io.ktor.client.call.body
64
import io.ktor.client.plugins.ResponseException
5+
import io.ktor.client.request.forms.FormPart
6+
import io.ktor.client.request.forms.formData
7+
import io.ktor.client.request.forms.submitFormWithBinaryData
78
import io.ktor.client.request.request
89
import io.ktor.client.request.setBody
910
import io.ktor.client.statement.HttpResponse
1011
import io.ktor.client.utils.EmptyContent
12+
import io.ktor.http.ContentType
13+
import io.ktor.http.Headers
14+
import io.ktor.http.HttpHeaders
1115
import io.ktor.http.HttpMethod
12-
import io.ktor.http.isSuccess
13-
import io.ktor.util.toMap
1416
import io.ktor.utils.io.errors.IOException
1517
import kotlin.collections.set
1618

@@ -24,6 +26,8 @@ internal class ApiExecutor(private val httpClient: HttpClient) {
2426

2527
private var query: HashMap<String, String> = HashMap()
2628

29+
private val formParts = mutableListOf<FormPart<*>>()
30+
2731
fun setEndpoint(endpoint: String): ApiExecutor {
2832
this.endpoint = endpoint
2933
return this
@@ -49,13 +53,23 @@ internal class ApiExecutor(private val httpClient: HttpClient) {
4953
return this
5054
}
5155

56+
fun <T : Any> addFormPart(key: String, value: T): ApiExecutor {
57+
formParts += FormPart(key, value)
58+
return this
59+
}
60+
61+
fun addFormPart(key: String, fileName: String, value: ByteArray): ApiExecutor {
62+
val headers = Headers.build {
63+
append(HttpHeaders.ContentType, ContentType.Application.OctetStream.contentType)
64+
append(HttpHeaders.ContentDisposition, "filename=$fileName")
65+
}
66+
formParts += FormPart(key, value, headers = headers)
67+
return this
68+
}
69+
5270
suspend inline fun <reified T> execute(): ApiResult<T> {
5371
return try {
54-
val response = httpClient.request(endpoint) {
55-
url { query.forEach { parameters.append(it.key, it.value) } }
56-
method = httpMethod
57-
setBody(this@ApiExecutor.body)
58-
}
72+
val response = if (formParts.isEmpty()) executeRequest() else executeRequestAsForm()
5973
return response.toApiResult()
6074
} catch (responseException: ResponseException) {
6175
responseException.toApiResult()
@@ -64,28 +78,18 @@ internal class ApiExecutor(private val httpClient: HttpClient) {
6478
}
6579
}
6680

67-
private suspend inline fun <reified T> HttpResponse.toApiResult(): ApiResult<T> {
68-
val headers = this.headers.toMap()
69-
val statusCode = this.status.value
70-
return if (!this.status.isSuccess()) {
71-
val exception = ChatGptException(null, statusCode)
72-
ApiResult(null, headers, statusCode, exception)
73-
} else {
74-
ApiResult(this.body<T>(), headers, statusCode, null)
81+
private suspend fun executeRequest(): HttpResponse {
82+
return httpClient.request(endpoint) {
83+
url { query.forEach { parameters.append(it.key, it.value) } }
84+
method = httpMethod
85+
setBody(this@ApiExecutor.body)
7586
}
7687
}
7788

78-
private fun <T> ResponseException.toApiResult(): ApiResult<T> {
79-
return ApiResult(
80-
statusCode = this.response.status.value,
81-
exception = ChatGptException(this.cause, this.response.status.value)
82-
)
83-
}
84-
85-
private fun <T> Throwable.toApiResult(): ApiResult<T> {
86-
return ApiResult(
87-
statusCode = null,
88-
exception = ChatGptException(this.cause)
89+
private suspend fun executeRequestAsForm(): HttpResponse {
90+
return httpClient.submitFormWithBinaryData(
91+
url = endpoint,
92+
formData = formData { formParts.forEach { append(it) } }
8993
)
9094
}
9195
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package co.yml.ychat.data.infrastructure
2+
3+
import co.yml.ychat.data.exception.ChatGptException
4+
import io.ktor.client.call.body
5+
import io.ktor.client.plugins.ResponseException
6+
import io.ktor.client.statement.HttpResponse
7+
import io.ktor.http.isSuccess
8+
import io.ktor.util.toMap
9+
10+
internal suspend inline fun <reified T> HttpResponse.toApiResult(): ApiResult<T> {
11+
val headers = this.headers.toMap()
12+
val statusCode = this.status.value
13+
return if (!this.status.isSuccess()) {
14+
val exception = ChatGptException(null, statusCode)
15+
ApiResult(null, headers, statusCode, exception)
16+
} else {
17+
ApiResult(this.body<T>(), headers, statusCode, null)
18+
}
19+
}
20+
21+
internal fun <T> ResponseException.toApiResult(): ApiResult<T> {
22+
return ApiResult(
23+
statusCode = this.response.status.value,
24+
exception = ChatGptException(this.cause, this.response.status.value)
25+
)
26+
}
27+
28+
internal fun <T> Throwable.toApiResult(): ApiResult<T> {
29+
return ApiResult(
30+
statusCode = null,
31+
exception = ChatGptException(this.cause)
32+
)
33+
}

ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ import co.yml.ychat.data.api.impl.ChatGptApiImpl
55
import co.yml.ychat.data.infrastructure.ApiExecutor
66
import co.yml.ychat.data.storage.ChatLogStorage
77
import co.yml.ychat.di.provider.NetworkProvider
8+
import co.yml.ychat.domain.usecases.AudioUseCase
89
import co.yml.ychat.domain.usecases.ChatCompletionsUseCase
910
import co.yml.ychat.domain.usecases.CompletionUseCase
1011
import co.yml.ychat.domain.usecases.EditsUseCase
1112
import co.yml.ychat.domain.usecases.ImageGenerationsUseCase
1213
import co.yml.ychat.domain.usecases.ListModelsUseCase
1314
import co.yml.ychat.domain.usecases.RetrieveModelUseCase
15+
import co.yml.ychat.entrypoint.features.AudioTranscriptions
1416
import co.yml.ychat.entrypoint.features.ChatCompletions
1517
import co.yml.ychat.entrypoint.features.Completion
1618
import co.yml.ychat.entrypoint.features.Edits
1719
import co.yml.ychat.entrypoint.features.ImageGenerations
1820
import co.yml.ychat.entrypoint.features.ListModels
1921
import co.yml.ychat.entrypoint.features.RetrieveModel
22+
import co.yml.ychat.entrypoint.impl.AudioTranscriptionsImpl
2023
import co.yml.ychat.entrypoint.impl.ChatCompletionsImpl
2124
import co.yml.ychat.entrypoint.impl.CompletionImpl
2225
import co.yml.ychat.entrypoint.impl.EditsImpl
@@ -39,6 +42,7 @@ internal class LibraryModule(private val apiKey: String) {
3942
factory<ChatCompletions> { ChatCompletionsImpl(Dispatchers.Default, get()) }
4043
factory<ImageGenerations> { ImageGenerationsImpl(Dispatchers.Default, get()) }
4144
factory<Edits> { EditsImpl(Dispatchers.Default, get()) }
45+
factory<AudioTranscriptions> { AudioTranscriptionsImpl(Dispatchers.Default, get()) }
4246
}
4347

4448
private val domainModule = module {
@@ -48,6 +52,7 @@ internal class LibraryModule(private val apiKey: String) {
4852
factory { ChatCompletionsUseCase(get()) }
4953
factory { ImageGenerationsUseCase(get()) }
5054
factory { EditsUseCase(get()) }
55+
factory { AudioUseCase(get()) }
5156
}
5257

5358
private val dataModule = module {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package co.yml.ychat.domain.mapper
2+
3+
import co.yml.ychat.data.dto.AudioParamsDto
4+
import co.yml.ychat.domain.model.AudioParams
5+
import co.yml.ychat.domain.model.FileBytes
6+
7+
internal fun AudioParams.toAudioParamsDto(filename: String, fileBytes: FileBytes): AudioParamsDto {
8+
return AudioParamsDto(
9+
filename = filename,
10+
byteArray = fileBytes,
11+
model = this.model,
12+
prompt = this.prompt,
13+
responseFormat = this.responseFormat,
14+
temperature = this.temperature,
15+
language = this.language,
16+
)
17+
}

0 commit comments

Comments
 (0)