You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by fm...@apache.org on 2026/05/04 11:45:55 UTC
(camel) 02/02: CAMEL-23390: Use DefaultProducer for audio transcription
This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit ea92d45791db0ab0747de68525558170aac40255
Author: Croway <fe...@gmail.com>
AuthorDate: Thu Apr 30 16:01:53 2026 +0200
CAMEL-23390: Use DefaultProducer for audio transcription
The audio transcription producer performs synchronous blocking work,
so DefaultProducer is a better fit than DefaultAsyncProducer.
---
.../apache/camel/catalog/components/openai.json | 7 +-
.../org/apache/camel/component/openai/openai.json | 7 +-
.../openai/OpenAIAudioTranscriptionProducer.java | 59 +++---
.../camel/component/openai/OpenAIConstants.java | 8 +
.../openai/OpenAIAudioTranscriptionMockTest.java | 19 ++
.../OpenAIAudioTranscriptionVerboseMockTest.java | 66 ++++++
.../component/ComponentsBuilderFactory.java | 2 +-
.../dsl/OpenaiComponentBuilderFactory.java | 20 +-
.../builder/endpoint/StaticEndpointBuilders.java | 20 +-
.../endpoint/dsl/OpenAIEndpointBuilderFactory.java | 230 +++++++++++++++++++--
.../openai/mock/AudioTranscriptionExpectation.java | 4 +
.../mock/AudioTranscriptionResponseBuilder.java | 23 ++-
12 files changed, 408 insertions(+), 57 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
index 4b9f9e775b86..e261cc641659 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
@@ -67,8 +67,11 @@
"CamelOpenAIAudioModel": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" },
"CamelOpenAIAudioLanguage": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" },
"CamelOpenAIAudioResponseFormat": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" },
- "CamelOpenAIAudioDuration": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" },
- "CamelOpenAIAudioDetectedLanguage": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }
+ "CamelOpenAIAudioTemperature": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" },
+ "CamelOpenAIAudioPrompt": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" },
+ "CamelOpenAIAudioTimestampGranularities": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" },
+ "CamelOpenAIAudioDuration": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" },
+ "CamelOpenAIAudioDetectedLanguage": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }
},
"properties": {
"operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "embeddings", "tool-execution", "audio-transcription" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'embeddings', 'tool-execution', or 'audio-transcription'" },
diff --git a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
index 4b9f9e775b86..e261cc641659 100644
--- a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
+++ b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
@@ -67,8 +67,11 @@
"CamelOpenAIAudioModel": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" },
"CamelOpenAIAudioLanguage": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" },
"CamelOpenAIAudioResponseFormat": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" },
- "CamelOpenAIAudioDuration": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" },
- "CamelOpenAIAudioDetectedLanguage": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }
+ "CamelOpenAIAudioTemperature": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" },
+ "CamelOpenAIAudioPrompt": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" },
+ "CamelOpenAIAudioTimestampGranularities": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" },
+ "CamelOpenAIAudioDuration": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" },
+ "CamelOpenAIAudioDetectedLanguage": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }
},
"properties": {
"operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "embeddings", "tool-execution", "audio-transcription" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'embeddings', 'tool-execution', or 'audio-transcription'" },
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionProducer.java
index 33e781354b8e..9ede8199c267 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionProducer.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionProducer.java
@@ -28,16 +28,16 @@ import com.openai.models.audio.AudioResponseFormat;
import com.openai.models.audio.transcriptions.TranscriptionCreateParams;
import com.openai.models.audio.transcriptions.TranscriptionCreateResponse;
import com.openai.models.audio.transcriptions.TranscriptionVerbose;
-import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
-import org.apache.camel.support.DefaultAsyncProducer;
+import org.apache.camel.WrappedFile;
+import org.apache.camel.support.DefaultProducer;
import org.apache.camel.util.ObjectHelper;
/**
* OpenAI producer for audio transcription.
*/
-public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
+public class OpenAIAudioTranscriptionProducer extends DefaultProducer {
public OpenAIAudioTranscriptionProducer(OpenAIEndpoint endpoint) {
super(endpoint);
@@ -49,19 +49,7 @@ public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
}
@Override
- public boolean process(Exchange exchange, AsyncCallback callback) {
- try {
- processInternal(exchange);
- callback.done(true);
- return true;
- } catch (Exception e) {
- exchange.setException(e);
- callback.done(true);
- return true;
- }
- }
-
- private void processInternal(Exchange exchange) throws Exception {
+ public void process(Exchange exchange) throws Exception {
OpenAIConfiguration config = getEndpoint().getConfiguration();
Message in = exchange.getIn();
@@ -74,9 +62,11 @@ public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
String language = resolveParameter(in, OpenAIConstants.AUDIO_LANGUAGE, config.getAudioLanguage(), String.class);
String responseFormat = resolveParameter(in, OpenAIConstants.AUDIO_RESPONSE_FORMAT,
config.getAudioResponseFormat(), String.class);
- Double temperature = resolveParameter(in, null, config.getAudioTemperature(), Double.class);
- String prompt = resolveParameter(in, null, config.getAudioPrompt(), String.class);
- String timestampGranularities = resolveParameter(in, null, config.getAudioTimestampGranularities(), String.class);
+ Double temperature = resolveParameter(in, OpenAIConstants.AUDIO_TEMPERATURE, config.getAudioTemperature(),
+ Double.class);
+ String prompt = resolveParameter(in, OpenAIConstants.AUDIO_PROMPT, config.getAudioPrompt(), String.class);
+ String timestampGranularities = resolveParameter(in, OpenAIConstants.AUDIO_TIMESTAMP_GRANULARITIES,
+ config.getAudioTimestampGranularities(), String.class);
TranscriptionCreateParams.Builder paramsBuilder = TranscriptionCreateParams.builder()
.model(model);
@@ -98,9 +88,14 @@ public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
if (ObjectHelper.isNotEmpty(timestampGranularities)) {
List<TranscriptionCreateParams.TimestampGranularity> granularities = new ArrayList<>();
for (String g : timestampGranularities.split(",")) {
- granularities.add(TranscriptionCreateParams.TimestampGranularity.of(g.trim()));
+ String trimmed = g.trim();
+ if (!trimmed.isEmpty()) {
+ granularities.add(TranscriptionCreateParams.TimestampGranularity.of(trimmed));
+ }
+ }
+ if (!granularities.isEmpty()) {
+ paramsBuilder.timestampGranularities(granularities);
}
- paramsBuilder.timestampGranularities(granularities);
}
TranscriptionCreateParams params = paramsBuilder.build();
@@ -128,14 +123,18 @@ public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
private void setFileInput(TranscriptionCreateParams.Builder paramsBuilder, Message in) {
Object body = in.getBody();
+ if (body instanceof WrappedFile<?> wrappedFile) {
+ body = wrappedFile.getFile();
+ }
+
if (body instanceof File file) {
paramsBuilder.file(file.toPath());
} else if (body instanceof Path path) {
paramsBuilder.file(path);
} else if (body instanceof byte[] bytes) {
- paramsBuilder.file(multipartWithFilename(new ByteArrayInputStream(bytes)));
+ paramsBuilder.file(multipartWithFilename(new ByteArrayInputStream(bytes), resolveFilename(in)));
} else if (body instanceof InputStream inputStream) {
- paramsBuilder.file(multipartWithFilename(inputStream));
+ paramsBuilder.file(multipartWithFilename(inputStream, resolveFilename(in)));
} else {
InputStream converted = in.getBody(InputStream.class);
if (converted == null) {
@@ -144,14 +143,22 @@ public class OpenAIAudioTranscriptionProducer extends DefaultAsyncProducer {
+ (body != null ? body.getClass().getName() : "null")
+ ". Supported: File, Path, InputStream, byte[]");
}
- paramsBuilder.file(multipartWithFilename(converted));
+ paramsBuilder.file(multipartWithFilename(converted, resolveFilename(in)));
+ }
+ }
+
+ private String resolveFilename(Message in) {
+ String filename = in.getHeader(Exchange.FILE_NAME_ONLY, String.class);
+ if (ObjectHelper.isNotEmpty(filename)) {
+ return filename;
}
+ return "audio";
}
- private MultipartField<InputStream> multipartWithFilename(InputStream stream) {
+ private MultipartField<InputStream> multipartWithFilename(InputStream stream, String filename) {
return MultipartField.<InputStream> builder()
.value(stream)
- .filename("audio")
+ .filename(filename)
.build();
}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
index 473eaeada6bf..6464d792d601 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
@@ -110,6 +110,14 @@ public final class OpenAIConstants {
@Metadata(description = "The response format for audio transcription (json, text, srt, verbose_json, vtt)",
javaType = "String")
public static final String AUDIO_RESPONSE_FORMAT = "CamelOpenAIAudioResponseFormat";
+ @Metadata(description = "Sampling temperature for audio transcription (0.0 to 1.0)", javaType = "Double")
+ public static final String AUDIO_TEMPERATURE = "CamelOpenAIAudioTemperature";
+ @Metadata(description = "Optional text to guide the model's style or continue a previous audio segment",
+ javaType = "String")
+ public static final String AUDIO_PROMPT = "CamelOpenAIAudioPrompt";
+ @Metadata(description = "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)",
+ javaType = "String")
+ public static final String AUDIO_TIMESTAMP_GRANULARITIES = "CamelOpenAIAudioTimestampGranularities";
// Audio Transcription Output Headers
@Metadata(description = "Duration of the audio in seconds (verbose_json only)", javaType = "Double")
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionMockTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionMockTest.java
index eeca71767c58..427791d4325c 100644
--- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionMockTest.java
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionMockTest.java
@@ -21,9 +21,11 @@ import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.infra.openai.mock.OpenAIMock;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;
@@ -61,6 +63,11 @@ public class OpenAIAudioTranscriptionMockTest extends CamelTestSupport {
from("direct:transcribe-no-model")
.to("openai:audio-transcription?apiKey=dummy&baseUrl="
+ openAIMock.getBaseUrl() + "/v1");
+
+ from("file:" + tempDir.toString() + "?noop=true&initialDelay=0&delay=100")
+ .to("openai:audio-transcription?audioModel=whisper-1&apiKey=dummy&baseUrl="
+ + openAIMock.getBaseUrl() + "/v1")
+ .to("mock:transcribed");
}
};
}
@@ -126,6 +133,18 @@ public class OpenAIAudioTranscriptionMockTest extends CamelTestSupport {
assertEquals(TRANSCRIPTION_TEXT, result.getMessage().getBody(String.class));
}
+ @Test
+ void testTranscriptionFromFileComponent() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:transcribed");
+ mock.expectedMessageCount(1);
+ mock.expectedBodiesReceived(TRANSCRIPTION_TEXT);
+
+ Files.write(tempDir.resolve("recording.wav"), new byte[] { 0x00, 0x01, 0x02 });
+
+ mock.await(10, TimeUnit.SECONDS);
+ mock.assertIsSatisfied();
+ }
+
@Test
void testTranscriptionMissingModel() {
byte[] audioBytes = new byte[] { 0x00, 0x01, 0x02 };
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionVerboseMockTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionVerboseMockTest.java
new file mode 100644
index 000000000000..da75fdf5797c
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionVerboseMockTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class OpenAIAudioTranscriptionVerboseMockTest extends CamelTestSupport {
+
+ private static final String TRANSCRIPTION_TEXT = "The stale smell of old beer lingers.";
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .whenTranscription()
+ .replyWithTranscription(TRANSCRIPTION_TEXT)
+ .withDuration(5.2)
+ .withLanguage("en")
+ .end()
+ .build();
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:transcribe-verbose")
+ .to("openai:audio-transcription?audioModel=whisper-1&audioResponseFormat=verbose_json"
+ + "&apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1");
+ }
+ };
+ }
+
+ @Test
+ void testVerboseResponseSetsHeaders() {
+ Exchange result = template.request("direct:transcribe-verbose",
+ e -> e.getIn().setBody(new byte[] { 0x00, 0x01, 0x02 }));
+
+ assertNotNull(result);
+ assertNull(result.getException());
+ assertEquals(TRANSCRIPTION_TEXT, result.getMessage().getBody(String.class));
+ assertEquals(5.2, result.getMessage().getHeader(OpenAIConstants.AUDIO_DURATION, Double.class), 0.001);
+ assertEquals("en", result.getMessage().getHeader(OpenAIConstants.AUDIO_DETECTED_LANGUAGE, String.class));
+ }
+}
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
index bfcc887afa17..244293d099f0 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/ComponentsBuilderFactory.java
@@ -3773,7 +3773,7 @@ public interface ComponentsBuilderFactory {
}
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Category: ai
* Since: 4.17
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpenaiComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpenaiComponentBuilderFactory.java
index 27e93f41ffda..8d9bf939bf2f 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpenaiComponentBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/OpenaiComponentBuilderFactory.java
@@ -24,7 +24,7 @@ import org.apache.camel.builder.component.ComponentBuilder;
import org.apache.camel.component.openai.OpenAIComponent;
/**
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Generated by camel build tools - do NOT edit this file!
*/
@@ -33,7 +33,7 @@ public interface OpenaiComponentBuilderFactory {
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Category: ai
* Since: 4.17
@@ -65,6 +65,21 @@ public interface OpenaiComponentBuilderFactory {
return this;
}
+ /**
+ * Default model for audio transcription endpoints.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioModel the value to set
+ * @return the dsl builder
+ */
+ default OpenaiComponentBuilder audioModel(java.lang.String audioModel) {
+ doSetProperty("audioModel", audioModel);
+ return this;
+ }
+
/**
* Default base URL for all endpoints.
@@ -191,6 +206,7 @@ public interface OpenaiComponentBuilderFactory {
Object value) {
switch (name) {
case "apiKey": ((OpenAIComponent) component).setApiKey((java.lang.String) value); return true;
+ case "audioModel": ((OpenAIComponent) component).setAudioModel((java.lang.String) value); return true;
case "baseUrl": ((OpenAIComponent) component).setBaseUrl((java.lang.String) value); return true;
case "embeddingModel": ((OpenAIComponent) component).setEmbeddingModel((java.lang.String) value); return true;
case "lazyStartProducer": ((OpenAIComponent) component).setLazyStartProducer((boolean) value); return true;
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
index 988bffc6c715..3a5a55431e67 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
@@ -12546,7 +12546,7 @@ public class StaticEndpointBuilders {
}
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Category: ai
* Since: 4.17
@@ -12555,10 +12555,10 @@ public class StaticEndpointBuilders {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'embeddings', or
- * 'tool-execution'
- * There are 3 enums and the value can be one of: chat-completion,
- * embeddings, tool-execution
+ * The operation to perform: 'chat-completion', 'embeddings',
+ * 'tool-execution', or 'audio-transcription'
+ * There are 4 enums and the value can be one of: chat-completion,
+ * embeddings, tool-execution, audio-transcription
*
* @param path operation
* @return the dsl builder
@@ -12568,7 +12568,7 @@ public class StaticEndpointBuilders {
}
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Category: ai
* Since: 4.17
@@ -12577,10 +12577,10 @@ public class StaticEndpointBuilders {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'embeddings', or
- * 'tool-execution'
- * There are 3 enums and the value can be one of: chat-completion,
- * embeddings, tool-execution
+ * The operation to perform: 'chat-completion', 'embeddings',
+ * 'tool-execution', or 'audio-transcription'
+ * There are 4 enums and the value can be one of: chat-completion,
+ * embeddings, tool-execution, audio-transcription
*
* @param componentName to use a custom component name for the endpoint
* instead of the default name
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
index 3231c4da3a30..779c8c3c9633 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
@@ -27,7 +27,7 @@ import org.apache.camel.builder.EndpointProducerBuilder;
import org.apache.camel.builder.endpoint.AbstractEndpointBuilder;
/**
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio transcription.
*
* Generated by camel build tools - do NOT edit this file!
*/
@@ -100,6 +100,109 @@ public interface OpenAIEndpointBuilderFactory {
doSetProperty("apiKey", apiKey);
return this;
}
+ /**
+ * The language of the input audio in ISO-639-1 format (e.g., 'en').
+ * Improves accuracy and latency.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioLanguage the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioLanguage(String audioLanguage) {
+ doSetProperty("audioLanguage", audioLanguage);
+ return this;
+ }
+ /**
+ * The model to use for audio transcription (e.g., whisper-1,
+ * gpt-4o-transcribe).
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioModel the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioModel(String audioModel) {
+ doSetProperty("audioModel", audioModel);
+ return this;
+ }
+ /**
+ * Optional text to guide the model's style or continue a previous audio
+ * segment.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioPrompt the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioPrompt(String audioPrompt) {
+ doSetProperty("audioPrompt", audioPrompt);
+ return this;
+ }
+ /**
+ * The format of the transcription output.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Default: json
+ * Group: producer
+ *
+ * @param audioResponseFormat the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioResponseFormat(String audioResponseFormat) {
+ doSetProperty("audioResponseFormat", audioResponseFormat);
+ return this;
+ }
+ /**
+ * Sampling temperature for transcription (0.0 to 1.0).
+ *
+ * The option is a: <code>java.lang.Double</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioTemperature the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioTemperature(Double audioTemperature) {
+ doSetProperty("audioTemperature", audioTemperature);
+ return this;
+ }
+ /**
+ * Sampling temperature for transcription (0.0 to 1.0).
+ *
+ * The option will be converted to a <code>java.lang.Double</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioTemperature the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioTemperature(String audioTemperature) {
+ doSetProperty("audioTemperature", audioTemperature);
+ return this;
+ }
+ /**
+ * Comma-separated timestamp granularities: 'word', 'segment', or
+ * 'word,segment'. Only applicable with verbose_json response format.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: producer
+ *
+ * @param audioTimestampGranularities the value to set
+ * @return the dsl builder
+ */
+ default OpenAIEndpointBuilder audioTimestampGranularities(String audioTimestampGranularities) {
+ doSetProperty("audioTimestampGranularities", audioTimestampGranularities);
+ return this;
+ }
/**
* When true and MCP servers are configured, automatically execute tool
* calls and loop back to the model. When false, tool calls are returned
@@ -972,7 +1075,8 @@ public interface OpenAIEndpointBuilderFactory {
public interface OpenAIBuilders {
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio
+ * transcription.
*
* Category: ai
* Since: 4.17
@@ -985,7 +1089,8 @@ public interface OpenAIEndpointBuilderFactory {
}
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio
+ * transcription.
*
* Category: ai
* Since: 4.17
@@ -994,10 +1099,10 @@ public interface OpenAIEndpointBuilderFactory {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'embeddings', or
- * 'tool-execution'
- * There are 3 enums and the value can be one of: chat-completion,
- * embeddings, tool-execution
+ * The operation to perform: 'chat-completion', 'embeddings',
+ * 'tool-execution', or 'audio-transcription'
+ * There are 4 enums and the value can be one of: chat-completion,
+ * embeddings, tool-execution, audio-transcription
*
* @param path operation
* @return the dsl builder
@@ -1007,7 +1112,8 @@ public interface OpenAIEndpointBuilderFactory {
}
/**
* OpenAI (camel-openai)
- * OpenAI endpoint for chat completion and embeddings.
+ * OpenAI endpoint for chat completion, embeddings, and audio
+ * transcription.
*
* Category: ai
* Since: 4.17
@@ -1016,10 +1122,10 @@ public interface OpenAIEndpointBuilderFactory {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'embeddings', or
- * 'tool-execution'
- * There are 3 enums and the value can be one of: chat-completion,
- * embeddings, tool-execution
+ * The operation to perform: 'chat-completion', 'embeddings',
+ * 'tool-execution', or 'audio-transcription'
+ * There are 4 enums and the value can be one of: chat-completion,
+ * embeddings, tool-execution, audio-transcription
*
* @param componentName to use a custom component name for the endpoint
* instead of the default name
@@ -1408,6 +1514,106 @@ public interface OpenAIEndpointBuilderFactory {
public String openAIOriginalText() {
return "CamelOpenAIOriginalText";
}
+ /**
+ * The model to use for audio transcription.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioModel}.
+ */
+ public String openAIAudioModel() {
+ return "CamelOpenAIAudioModel";
+ }
+ /**
+ * The language of the input audio (ISO-639-1).
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioLanguage}.
+ */
+ public String openAIAudioLanguage() {
+ return "CamelOpenAIAudioLanguage";
+ }
+ /**
+ * The response format for audio transcription (json, text, srt,
+ * verbose_json, vtt).
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioResponseFormat}.
+ */
+ public String openAIAudioResponseFormat() {
+ return "CamelOpenAIAudioResponseFormat";
+ }
+ /**
+ * Sampling temperature for audio transcription (0.0 to 1.0).
+ *
+ * The option is a: {@code Double} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioTemperature}.
+ */
+ public String openAIAudioTemperature() {
+ return "CamelOpenAIAudioTemperature";
+ }
+ /**
+ * Optional text to guide the model's style or continue a previous audio
+ * segment.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioPrompt}.
+ */
+ public String openAIAudioPrompt() {
+ return "CamelOpenAIAudioPrompt";
+ }
+ /**
+ * Comma-separated timestamp granularities: word, segment, or
+ * word,segment (verbose_json only).
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * OpenAIAudioTimestampGranularities}.
+ */
+ public String openAIAudioTimestampGranularities() {
+ return "CamelOpenAIAudioTimestampGranularities";
+ }
+ /**
+ * Duration of the audio in seconds (verbose_json only).
+ *
+ * The option is a: {@code Double} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioDuration}.
+ */
+ public String openAIAudioDuration() {
+ return "CamelOpenAIAudioDuration";
+ }
+ /**
+ * Language detected in the audio (verbose_json only).
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIAudioDetectedLanguage}.
+ */
+ public String openAIAudioDetectedLanguage() {
+ return "CamelOpenAIAudioDetectedLanguage";
+ }
}
static OpenAIEndpointBuilder endpointBuilder(String componentName, String path) {
class OpenAIEndpointBuilderImpl extends AbstractEndpointBuilder implements OpenAIEndpointBuilder, AdvancedOpenAIEndpointBuilder {
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionExpectation.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionExpectation.java
index 51fd82708434..824b1b026618 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionExpectation.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionExpectation.java
@@ -49,6 +49,10 @@ public class AudioTranscriptionExpectation {
this.duration = duration;
}
+ public boolean isVerbose() {
+ return duration > 0;
+ }
+
@Override
public String toString() {
return String.format("AudioTranscriptionExpectation{text='%s', language='%s', duration=%.1f}",
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionResponseBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionResponseBuilder.java
index 63861bec042a..ae675f20b673 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionResponseBuilder.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/AudioTranscriptionResponseBuilder.java
@@ -29,10 +29,29 @@ public class AudioTranscriptionResponseBuilder {
}
public String createTranscriptionResponse(AudioTranscriptionExpectation expectation) throws Exception {
+ if (expectation.isVerbose()) {
+ return createVerboseResponse(expectation);
+ }
+ return createSimpleResponse(expectation);
+ }
+
+ private String createSimpleResponse(AudioTranscriptionExpectation expectation) throws Exception {
record TranscriptionResponse(String text) {
}
- TranscriptionResponse response = new TranscriptionResponse(expectation.getTranscriptionText());
- return objectMapper.writeValueAsString(response);
+ return objectMapper.writeValueAsString(new TranscriptionResponse(expectation.getTranscriptionText()));
+ }
+
+ private String createVerboseResponse(AudioTranscriptionExpectation expectation) throws Exception {
+ record VerboseTranscriptionResponse(String text, String language, double duration, java.util.List<?> segments,
+ java.util.List<?> words) {
+ }
+
+ return objectMapper.writeValueAsString(new VerboseTranscriptionResponse(
+ expectation.getTranscriptionText(),
+ expectation.getLanguage(),
+ expectation.getDuration(),
+ java.util.List.of(),
+ java.util.List.of()));
}
}