Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ private constructor(
/**
* Duration of the input audio in seconds.
*
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the
* server responded with an unexpected value).
*/
fun duration(): Double = duration.getRequired("duration")
fun duration(): Optional<Double> = duration.getOptional("duration")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the duration getter's JVM descriptor

When an application or library compiled against the previous SDK invokes this getter after the SDK jar is upgraded without recompiling that caller, its bytecode still resolves duration()D, while this class now only provides duration()Ljava/util/Optional;; the invocation therefore fails at runtime with NoSuchMethodError, even when the response contains a duration. The primitive builder overload preserves the setter ABI, but the public getter needs equivalent compatibility handling (for example, retaining it and exposing the optional value under a new method name) or this change must be treated as a breaking release.

Useful? React with 👍 / 👎.


/**
* Segments of the transcript annotated with timestamps and speaker labels.
Expand Down Expand Up @@ -149,7 +149,6 @@ private constructor(
*
* The following fields are required:
* ```java
* .duration()
* .segments()
* .text()
* ```
Expand All @@ -160,7 +159,7 @@ private constructor(
/** A builder for [TranscriptionDiarized]. */
class Builder internal constructor() {

private var duration: JsonField<Double>? = null
private var duration: JsonField<Double> = JsonMissing.of()
private var segments: JsonField<MutableList<TranscriptionDiarizedSegment>>? = null
private var task: JsonValue = JsonValue.from("transcribe")
private var text: JsonField<String>? = null
Expand All @@ -178,7 +177,17 @@ private constructor(
}

/** Duration of the input audio in seconds. */
fun duration(duration: Double) = duration(JsonField.of(duration))
fun duration(duration: Double?) = duration(JsonField.ofNullable(duration))

/**
* Alias for [Builder.duration].
*
* This unboxed primitive overload exists for backwards compatibility.
*/
fun duration(duration: Double) = duration(duration as Double?)

/** Alias for calling [Builder.duration] with `duration.orElse(null)`. */
fun duration(duration: Optional<Double>) = duration(duration.getOrNull())

/**
* Sets [Builder.duration] to an arbitrary JSON value.
Expand Down Expand Up @@ -294,7 +303,6 @@ private constructor(
*
* The following fields are required:
* ```java
* .duration()
* .segments()
* .text()
* ```
Expand All @@ -303,7 +311,7 @@ private constructor(
*/
fun build(): TranscriptionDiarized =
TranscriptionDiarized(
checkRequired("duration", duration),
duration,
checkRequired("segments", segments).map { it.toImmutable() },
task,
checkRequired("text", text),
Expand All @@ -330,7 +338,7 @@ private constructor(
duration()
segments().forEach { it.validate() }
Comment thread
wskr00 marked this conversation as resolved.
_task().let {
if (it != JsonValue.from("transcribe")) {
if (it != JsonMissing.of() && it != JsonValue.from("transcribe")) {
Comment thread
wskr00 marked this conversation as resolved.
throw OpenAIInvalidDataException("'task' is invalid, received $it")
}
}
Expand All @@ -355,6 +363,7 @@ private constructor(
@JvmSynthetic
internal fun validity(): Int =
(if (duration.asKnown().isPresent) 1 else 0) +
(if (segments.asKnown().isPresent) 1 else 0) +
Comment thread
wskr00 marked this conversation as resolved.
(segments.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
task.let { if (it == JsonValue.from("transcribe")) 1 else 0 } +
(if (text.asKnown().isPresent) 1 else 0) +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ private constructor(
(if (duration.asKnown().isPresent) 1 else 0) +
(if (language.asKnown().isPresent) 1 else 0) +
(if (text.asKnown().isPresent) 1 else 0) +
(if (segments.asKnown().isPresent) 1 else 0) +
(segments.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
(usage.asKnown().getOrNull()?.validity() ?: 0) +
(words.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,44 @@ internal class TranscriptionCreateResponseTest {
assertThat(roundtrippedTranscriptionCreateResponse).isEqualTo(transcriptionCreateResponse)
}

@Test
fun deserializeDiarizedWithEmptySegments() {
val transcriptionCreateResponse =
jsonMapper()
.readValue(
"""
{
"text": "",
"segments": []
}
"""
.trimIndent(),
jacksonTypeRef<TranscriptionCreateResponse>(),
)

assertThat(transcriptionCreateResponse.isDiarized()).isTrue()
}

@Test
fun deserializeVerboseWithEmptySegments() {
val transcriptionCreateResponse =
jsonMapper()
.readValue(
"""
{
"duration": 0.0,
"language": "en",
"text": "",
"segments": []
}
"""
.trimIndent(),
jacksonTypeRef<TranscriptionCreateResponse>(),
)

assertThat(transcriptionCreateResponse.isVerbose()).isTrue()
}

@Test
fun ofVerbose() {
val verbose =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.openai.models.audio.transcriptions

import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import com.openai.core.JsonValue
import com.openai.core.jsonMapper
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
Expand All @@ -13,7 +14,6 @@ internal class TranscriptionDiarizedTest {
fun create() {
val transcriptionDiarized =
TranscriptionDiarized.builder()
.duration(0.0)
.addSegment(
TranscriptionDiarizedSegment.builder()
.id("id")
Expand All @@ -39,7 +39,7 @@ internal class TranscriptionDiarizedTest {
)
.build()

assertThat(transcriptionDiarized.duration()).isEqualTo(0.0)
assertThat(transcriptionDiarized.duration()).isEmpty
assertThat(transcriptionDiarized.segments())
.containsExactly(
TranscriptionDiarizedSegment.builder()
Expand Down Expand Up @@ -69,6 +69,82 @@ internal class TranscriptionDiarizedTest {
)
}

@Test
fun deserializeWithoutOptionalFields() {
val transcriptionDiarized =
jsonMapper()
.readValue(
"""
{
"text": "text",
"segments": [
{
"type": "transcript.text.segment",
"id": "id",
"end": 1.0,
"speaker": "speaker",
"start": 0.0,
"text": "text"
}
]
}
"""
.trimIndent(),
jacksonTypeRef<TranscriptionDiarized>(),
)

assertThat(transcriptionDiarized.duration()).isEmpty
assertThat(transcriptionDiarized.isValid()).isTrue()
}

@Test
fun rejectsInvalidDuration() {
val transcriptionDiarized =
jsonMapper()
.readValue(
"""
{
"duration": "invalid",
"text": "text",
"segments": [
{
"type": "transcript.text.segment",
"id": "id",
"end": 1.0,
"speaker": "speaker",
"start": 0.0,
"text": "text"
}
]
}
"""
.trimIndent(),
jacksonTypeRef<TranscriptionDiarized>(),
)

assertThat(transcriptionDiarized.isValid()).isFalse()
}

@Test
fun rejectsUnexpectedTask() {
val transcriptionDiarized =
TranscriptionDiarized.builder()
.addSegment(
TranscriptionDiarizedSegment.builder()
.id("id")
.end(1.0)
.speaker("speaker")
.start(0.0)
.text("text")
.build()
)
.task(JsonValue.from("translate"))
.text("text")
.build()

assertThat(transcriptionDiarized.isValid()).isFalse()
}

@Test
fun roundtrip() {
val jsonMapper = jsonMapper()
Expand Down Expand Up @@ -107,5 +183,6 @@ internal class TranscriptionDiarizedTest {
)

assertThat(roundtrippedTranscriptionDiarized).isEqualTo(transcriptionDiarized)
assertThat(roundtrippedTranscriptionDiarized.duration()).contains(0.0)
}
}