gnodet-bot commented on code in PR #26608:
URL: https://github.com/apache/camel/pull/26608#discussion_r4051821120
##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioTranscriptionProducer.java:
##########
@@ -94,26 +130,93 @@ public void process(Exchange exchange) throws Exception {
paramsBuilder.timestampGranularities(granularities);
}
}
+ applyChunkingStrategy(paramsBuilder, chunkingStrategy);
+ applyCommaSeparatedList(knownSpeakerNames,
paramsBuilder::addKnownSpeakerName);
+ applyCommaSeparatedList(knownSpeakerReferences,
paramsBuilder::addKnownSpeakerReference);
+ applyCommaSeparatedList(keywords, paramsBuilder::addKeyword);
+ applyCommaSeparatedList(languages, paramsBuilder::addLanguage);
+ applyIncludeList(include, paramsBuilder);
+
+ return paramsBuilder.build();
+ }
- TranscriptionCreateParams params = paramsBuilder.build();
- TranscriptionCreateResponse response = getEndpoint().getClient()
- .audio().transcriptions().create(params);
+ private static void
applyChunkingStrategy(TranscriptionCreateParams.Builder paramsBuilder, String
chunkingStrategy) {
+ if (ObjectHelper.isEmpty(chunkingStrategy)) {
+ return;
+ }
+ if ("auto".equalsIgnoreCase(chunkingStrategy)) {
+ paramsBuilder.chunkingStrategyAuto();
+ return;
+ }
+ if ("vad".equalsIgnoreCase(chunkingStrategy)) {
+
paramsBuilder.chunkingStrategy(TranscriptionCreateParams.ChunkingStrategy.VadConfig.builder()
+
.type(TranscriptionCreateParams.ChunkingStrategy.VadConfig.Type.SERVER_VAD)
+ .build());
+ return;
+ }
+ throw new IllegalArgumentException(
+ "Unsupported audio chunking strategy: " + chunkingStrategy +
". Supported values are auto and vad.");
+ }
+ private static void applyCommaSeparatedList(String value, Consumer<String>
consumer) {
+ for (String item :
OpenAIAudioSupport.parseCommaSeparatedValues(value)) {
+ consumer.accept(item);
+ }
+ }
+
+ private static void applyIncludeList(String include,
TranscriptionCreateParams.Builder paramsBuilder) {
+ for (String item :
OpenAIAudioSupport.parseCommaSeparatedValues(include)) {
+ paramsBuilder.addInclude(TranscriptionInclude.of(item));
+ }
+ }
+
+ private static void populateOutput(Exchange exchange, OpenAIConfiguration
config, TranscriptionCreateResponse response) {
Message out = exchange.getMessage();
+ TranscriptionCreateResponse storedResponse = response;
if (response.isVerbose()) {
TranscriptionVerbose verbose = response.asVerbose();
out.setBody(verbose.text());
out.setHeader(OpenAIConstants.AUDIO_DURATION, verbose.duration());
out.setHeader(OpenAIConstants.AUDIO_DETECTED_LANGUAGE,
verbose.language());
+ } else if (response.isDiarized()) {
+ applyDiarizedOutput(out, response.asDiarized());
} else if (response.isTranscription()) {
- out.setBody(response.asTranscription().text());
+ String text = response.asTranscription().text();
+ TranscriptionCreateResponse reparsed =
tryParseStructuredTranscription(text);
+ if (reparsed != null && reparsed.isDiarized()) {
+ storedResponse = reparsed;
+ applyDiarizedOutput(out, reparsed.asDiarized());
+ } else {
+ out.setBody(text);
+ }
} else {
out.setBody(response.toString());
}
if (config.isStoreFullResponse()) {
- exchange.setProperty(OpenAIConstants.AUDIO_TRANSCRIPTION_RESPONSE,
response);
+ exchange.setProperty(OpenAIConstants.AUDIO_TRANSCRIPTION_RESPONSE,
storedResponse);
+ }
+ }
+
+ private static void applyDiarizedOutput(Message out, TranscriptionDiarized
diarized) {
+ out.setBody(diarized.text());
+ out.setHeader(OpenAIConstants.AUDIO_DURATION, diarized.duration());
+ out.setHeader(OpenAIConstants.AUDIO_DIARIZED_SEGMENTS,
diarized.segments());
+ }
+
+ /**
+ * The OpenAI Java SDK delivers {@code diarized_json} responses through
the plain-text handler, so the JSON payload
+ * arrives as {@link
com.openai.models.audio.transcriptions.Transcription#text()}. Re-parse it when
possible.
+ */
+ private static TranscriptionCreateResponse
tryParseStructuredTranscription(String text) {
+ if (ObjectHelper.isEmpty(text) || !text.startsWith("{")) {
+ return null;
+ }
+ try {
+ return ObjectMappers.jsonMapper().readValue(text,
TranscriptionCreateResponse.class);
+ } catch (Exception ignored) {
+ return null;
}
Review Comment:
⚠️ **Silent exception swallow causes silent data corruption for
`diarized_json` responses**
This re-parse is the workaround for the SDK routing `diarized_json` through
the plain-text handler. When the SDK delivers the JSON fine but
`ObjectMappers.jsonMapper().readValue(…, TranscriptionCreateResponse.class)`
fails — say, because the SDK model class doesn't map all fields of the diarized
response — `tryParseStructuredTranscription` returns `null`, and the `else`
branch sets the raw JSON string as the message body.
The caller then gives the user a raw JSON blob with no indication that
something went wrong. This is a silent correctness failure: the user asked for
diarized output, gets a JSON string, and must figure out why.
At minimum, log a warning so the problem is visible:
```suggestion
try {
return ObjectMappers.jsonMapper().readValue(text,
TranscriptionCreateResponse.class);
} catch (Exception e) {
LOG.warn("Failed to re-parse diarized_json transcription
response; returning raw text. Error: {}", e.getMessage());
return null;
}
```
Also consider whether the fallback branch should set the body to the parsed
JSON node (`ObjectMappers.jsonMapper().readTree(text)`) instead of the raw
string, so callers can at least navigate the structure.
##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAudioSupport.java:
##########
@@ -68,23 +71,53 @@ static void applyFileInput(
+ (body != null ?
body.getClass().getName() : "null")
+ ". Supported: File, Path,
InputStream, byte[]");
}
- multipartConsumer.accept(multipartWithFilename(converted,
resolveFilename(in)));
+ multipartConsumer.accept(multipartWithFilename(in, converted));
}
}
private static String resolveFilename(Message in) {
String filename = in.getHeader(Exchange.FILE_NAME_ONLY, String.class);
+ if (ObjectHelper.isEmpty(filename)) {
+ filename = in.getHeader(Exchange.FILE_NAME, String.class);
+ }
if (ObjectHelper.isNotEmpty(filename)) {
- return filename;
+ filename = FileUtil.stripPath(filename);
+ if (ObjectHelper.isNotEmpty(FileUtil.onlyExt(filename))) {
+ return filename;
+ }
+ }
+ String mime = MimeTypeHelper.resolveForBinary(in);
+ String extension = MimeTypeHelper.audioExtension(mime);
+ if (ObjectHelper.isNotEmpty(extension)) {
+ return "audio." + extension;
}
- return "audio";
+ return "audio.mp3";
Review Comment:
⚠️ **Hardcoded `audio.mp3` fallback will corrupt WAV payloads**
The OpenAI multipart API uses the filename extension to identify the audio
format on its end. When a caller sends raw WAV bytes (or any non-MP3 audio)
without setting a MIME header or a `CamelFileName`, this fallback names the
upload `audio.mp3`, causing the API to treat a WAV stream as MP3 — which will
produce garbage transcription or a 400 error.
The previous fallback was `"audio"` (no extension), which at least let the
server use content-type sniffing. This new fallback actively lies about the
format.
Fix: fall back to `"audio"` (no extension) unless the MIME type can actually
be resolved, or throw `IllegalArgumentException` telling the caller to set
`CamelOpenAIMediaType`:
```suggestion
return "audio";
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]