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]

Reply via email to