From 9fa10f178b3d88c3a1e94d6ca3c6ac988ede8337 Mon Sep 17 00:00:00 2001 From: Changjian Wang Date: Tue, 10 Mar 2026 18:14:50 +0800 Subject: [PATCH 1/3] Add tests for ContentRange functionality in Content Understanding SDK - Enhance ContentRangeTest with additional unit tests for various scenarios including: - Handling same start and end times. - Rejecting negative start times. - Validating time ranges and content range combinations. - Update Sample01_AnalyzeBinaryAsyncTest and Sample01_AnalyzeBinaryTest to include tests for analyzing binary documents with page content ranges. - Introduce new tests in Sample02_AnalyzeUrlAsyncTest and Sample02_AnalyzeUrlTest for analyzing URLs with page and time content ranges, ensuring proper functionality for both document and audio/video analyses. --- .../samples/Sample01_AnalyzeBinary.java | 76 ++++ .../samples/Sample01_AnalyzeBinaryAsync.java | 144 +++++++ .../samples/Sample02_AnalyzeUrl.java | 160 ++++++++ .../samples/Sample02_AnalyzeUrlAsync.java | 368 ++++++++++++++++++ .../tests/ContentRangeTest.java | 109 ++++++ .../Sample01_AnalyzeBinaryAsyncTest.java | 125 ++++++ .../samples/Sample01_AnalyzeBinaryTest.java | 104 +++++ .../samples/Sample02_AnalyzeUrlAsyncTest.java | 282 ++++++++++++++ .../samples/Sample02_AnalyzeUrlTest.java | 217 +++++++++++ 9 files changed, 1585 insertions(+) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinary.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinary.java index efcdebca52f0..eac7dec5a1d8 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinary.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinary.java @@ -8,6 +8,7 @@ import com.azure.ai.contentunderstanding.ContentUnderstandingClientBuilder; import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -29,6 +30,7 @@ * 2. Analyzing the document * 3. Extracting markdown content * 4. Accessing document properties (pages, tables, etc.) + * 5. Using ContentRange to analyze specific pages */ public class Sample01_AnalyzeBinary { @@ -126,5 +128,79 @@ public static void main(String[] args) throws IOException { // END:ContentUnderstandingAccessDocumentProperties System.out.println("\nBinary document analysis completed successfully"); + + // Demonstrate ContentRange usage with a multi-page document + System.out.println("\n--- ContentRange Examples ---"); + analyzeBinaryWithContentRange(client); + } + + /** + * Sample demonstrating how to use ContentRange to analyze specific pages of a binary document. + * ContentRange allows you to specify which pages to analyze instead of the entire document. + */ + public static void analyzeBinaryWithContentRange(ContentUnderstandingClient client) { + try { + // Load a multi-page document (4 pages) + String multiPageFilePath = "src/samples/resources/mixed_financial_docs.pdf"; + Path multiPagePath = Paths.get(multiPageFilePath); + byte[] multiPageBytes = Files.readAllBytes(multiPagePath); + BinaryData multiPageData = BinaryData.fromBytes(multiPageBytes); + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithContentRange + // Analyze only pages 3 to end using ContentRange.pagesFrom() + SyncPoller rangeOperation + = client.beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.pagesFrom(3), "application/octet-stream", null); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + + DocumentContent rangeDoc = (DocumentContent) rangeResult.getContents().get(0); + System.out.println("PagesFrom(3): returned " + rangeDoc.getPages().size() + " pages" + + " (pages " + rangeDoc.getStartPageNumber() + "-" + rangeDoc.getEndPageNumber() + ")"); + + // Analyze a single page using ContentRange.page() + SyncPoller pageOperation + = client.beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.page(2), "application/octet-stream", null); + DocumentContent pageDoc = (DocumentContent) pageOperation.getFinalResult().getContents().get(0); + System.out.println("Page(2): returned " + pageDoc.getPages().size() + " page" + + " (page " + pageDoc.getStartPageNumber() + ")"); + + // Analyze a page range using ContentRange.pages() + SyncPoller pagesOperation + = client.beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.pages(1, 3), "application/octet-stream", null); + DocumentContent pagesDoc = (DocumentContent) pagesOperation.getFinalResult().getContents().get(0); + System.out.println("Pages(1,3): returned " + pagesDoc.getPages().size() + " pages" + + " (pages " + pagesDoc.getStartPageNumber() + "-" + pagesDoc.getEndPageNumber() + ")"); + + // Combine multiple ranges using ContentRange.combine() + SyncPoller combineOperation + = client.beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.combine( + ContentRange.page(1), + ContentRange.pages(3, 4)), + "application/octet-stream", null); + DocumentContent combineDoc = (DocumentContent) combineOperation.getFinalResult().getContents().get(0); + System.out.println("Combine(Page(1), Pages(3,4)): returned " + combineDoc.getPages().size() + " pages" + + " (pages " + combineDoc.getStartPageNumber() + "-" + combineDoc.getEndPageNumber() + ")"); + + // Combine with out-of-range pages (clamped by the service) + SyncPoller bigCombineOperation + = client.beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.combine( + ContentRange.pages(1, 3), + ContentRange.page(5), + ContentRange.pagesFrom(9)), + "application/octet-stream", null); + DocumentContent bigCombineDoc + = (DocumentContent) bigCombineOperation.getFinalResult().getContents().get(0); + System.out.println( + "Combine(Pages(1,3), Page(5), PagesFrom(9)): returned " + bigCombineDoc.getPages().size() + " pages"); + // END:ContentUnderstandingAnalyzeBinaryWithContentRange + + System.out.println("ContentRange binary analysis completed successfully"); + } catch (IOException e) { + System.err.println("Error reading multi-page file: " + e.getMessage()); + } } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinaryAsync.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinaryAsync.java index e9ca0edb8e55..abfd2cf3782a 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinaryAsync.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample01_AnalyzeBinaryAsync.java @@ -8,6 +8,7 @@ import com.azure.ai.contentunderstanding.ContentUnderstandingClientBuilder; import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -32,6 +33,7 @@ * 2. Analyzing the document * 3. Extracting markdown content * 4. Accessing document properties (pages, tables, etc.) + * 5. Using ContentRange to analyze specific pages */ public class Sample01_AnalyzeBinaryAsync { @@ -162,5 +164,147 @@ public static void main(String[] args) throws IOException, InterruptedException if (!latch.await(2, TimeUnit.MINUTES)) { System.err.println("Timed out waiting for async operations to complete."); } + + // Demonstrate ContentRange usage with a multi-page document + System.out.println("\n--- ContentRange Examples ---"); + analyzeBinaryWithContentRange(client); + } + + /** + * Sample demonstrating how to use ContentRange to analyze specific pages of a binary document asynchronously. + * ContentRange allows you to specify which pages to analyze instead of the entire document. + */ + public static void analyzeBinaryWithContentRange(ContentUnderstandingAsyncClient client) + throws IOException, InterruptedException { + // Load a multi-page document (4 pages) + String multiPageFilePath = "src/samples/resources/mixed_financial_docs.pdf"; + Path multiPagePath = Paths.get(multiPageFilePath); + byte[] multiPageBytes = Files.readAllBytes(multiPagePath); + BinaryData multiPageData = BinaryData.fromBytes(multiPageBytes); + + CountDownLatch rangeLatch = new CountDownLatch(1); + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithContentRangeAsync + // Analyze only pages 3 to end using ContentRange.pagesFrom() + Mono pagesFromMono = client + .beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.pagesFrom(3), "application/octet-stream", null) + .last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent rangeDoc = (DocumentContent) result.getContents().get(0); + System.out.println("PagesFrom(3): returned " + rangeDoc.getPages().size() + " pages" + + " (pages " + rangeDoc.getStartPageNumber() + "-" + rangeDoc.getEndPageNumber() + ")"); + }); + + // Analyze a single page using ContentRange.page() + Mono pageMono = client + .beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.page(2), "application/octet-stream", null) + .last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent pageDoc = (DocumentContent) result.getContents().get(0); + System.out.println("Page(2): returned " + pageDoc.getPages().size() + " page" + + " (page " + pageDoc.getStartPageNumber() + ")"); + }); + + // Analyze a page range using ContentRange.pages() + Mono pagesMono = client + .beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.pages(1, 3), "application/octet-stream", null) + .last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent pagesDoc = (DocumentContent) result.getContents().get(0); + System.out.println("Pages(1,3): returned " + pagesDoc.getPages().size() + " pages" + + " (pages " + pagesDoc.getStartPageNumber() + "-" + pagesDoc.getEndPageNumber() + ")"); + }); + + // Combine multiple ranges using ContentRange.combine() + Mono combineMono = client + .beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.combine( + ContentRange.page(1), + ContentRange.pages(3, 4)), + "application/octet-stream", null) + .last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent combineDoc = (DocumentContent) result.getContents().get(0); + System.out.println("Combine(Page(1), Pages(3,4)): returned " + combineDoc.getPages().size() + " pages" + + " (pages " + combineDoc.getStartPageNumber() + "-" + combineDoc.getEndPageNumber() + ")"); + }); + + // Combine with out-of-range pages (clamped by the service) + Mono bigCombineMono = client + .beginAnalyzeBinary("prebuilt-documentSearch", multiPageData, + ContentRange.combine( + ContentRange.pages(1, 3), + ContentRange.page(5), + ContentRange.pagesFrom(9)), + "application/octet-stream", null) + .last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent bigCombineDoc = (DocumentContent) result.getContents().get(0); + System.out.println("Combine(Pages(1,3), Page(5), PagesFrom(9)): returned " + + bigCombineDoc.getPages().size() + " pages"); + }); + + // Chain all operations sequentially using then() + pagesFromMono + .then(pageMono) + .then(pagesMono) + .then(combineMono) + .then(bigCombineMono) + .doOnError(error -> System.err.println("Error: " + error.getMessage())) + .subscribe( + result -> { + System.out.println("ContentRange async analysis completed successfully"); + rangeLatch.countDown(); + }, + error -> rangeLatch.countDown() + ); + // END:ContentUnderstandingAnalyzeBinaryWithContentRangeAsync + + if (!rangeLatch.await(5, TimeUnit.MINUTES)) { + System.err.println("Timed out waiting for ContentRange async operations."); + } } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java index 3006c2abd851..cdb3c9c3c3cd 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java @@ -10,6 +10,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.AudioVisualContent; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -19,6 +20,7 @@ import com.azure.core.util.polling.SyncPoller; import com.azure.identity.DefaultAzureCredentialBuilder; +import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -67,6 +69,15 @@ public static void main(String[] args) { System.out.println("\n--- Image Analysis Example ---"); analyzeImageUrl(client); + + System.out.println("\n--- Document ContentRange Example ---"); + analyzeDocumentUrlWithContentRange(client); + + System.out.println("\n--- Video ContentRange Example ---"); + analyzeVideoUrlWithContentRange(client); + + System.out.println("\n--- Audio ContentRange Example ---"); + analyzeAudioUrlWithContentRange(client); } /** @@ -281,4 +292,153 @@ public static void analyzeImageUrl(ContentUnderstandingClient client) { System.out.println("Summary: " + summary); // END:ContentUnderstandingAnalyzeImageUrl } + + /** + * Sample demonstrating how to analyze a document URL with ContentRange to extract specific pages. + */ + public static void analyzeDocumentUrlWithContentRange(ContentUnderstandingClient client) { + // BEGIN:ContentUnderstandingAnalyzeUrlWithContentRange + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + + // Extract only page 1 using ContentRange + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.page(1)); + + SyncPoller rangeOperation + = client.beginAnalyze("prebuilt-documentSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + + DocumentContent rangeDoc = (DocumentContent) rangeResult.getContents().get(0); + System.out.println("Page(1): returned " + rangeDoc.getPages().size() + " page" + + " (page " + rangeDoc.getStartPageNumber() + ")"); + System.out.println("Markdown length: " + rangeDoc.getMarkdown().length() + " characters"); + // END:ContentUnderstandingAnalyzeUrlWithContentRange + } + + /** + * Sample demonstrating how to analyze a video URL with ContentRange to extract specific time ranges. + */ + public static void analyzeVideoUrlWithContentRange(ContentUnderstandingClient client) { + // BEGIN:ContentUnderstandingAnalyzeVideoUrlWithContentRange + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4"; + + // Analyze only the first 5 seconds using ContentRange.timeRange() + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(5))); + + SyncPoller rangeOperation + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + + for (AnalysisContent media : rangeResult.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRange(0, 5s): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + + // Analyze from 10 seconds onward using ContentRange.timeRangeFrom() + AnalysisInput rangeFromInput = new AnalysisInput(); + rangeFromInput.setUrl(uriSource); + rangeFromInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(10))); + + SyncPoller rangeFromOperation + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeFromInput)); + AnalysisResult rangeFromResult = rangeFromOperation.getFinalResult(); + + for (AnalysisContent media : rangeFromResult.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRangeFrom(10s): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + + // Analyze with sub-second precision using milliseconds + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange( + ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + SyncPoller subSecondOperation + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.getFinalResult(); + + for (AnalysisContent media : subSecondResult.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRange(1200ms, 3651ms): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + + // Combine multiple time ranges + AnalysisInput combineInput = new AnalysisInput(); + combineInput.setUrl(uriSource); + combineInput.setContentRange(ContentRange.combine( + ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(3)), + ContentRange.timeRangeFrom(Duration.ofSeconds(30)))); + + SyncPoller combineOperation + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(combineInput)); + AnalysisResult combineResult = combineOperation.getFinalResult(); + + int segmentIndex = 1; + for (AnalysisContent media : combineResult.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("Combined segment " + segmentIndex + ": Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + segmentIndex++; + } + // END:ContentUnderstandingAnalyzeVideoUrlWithContentRange + } + + /** + * Sample demonstrating how to analyze an audio URL with ContentRange to extract specific time ranges. + */ + public static void analyzeAudioUrlWithContentRange(ContentUnderstandingClient client) { + // BEGIN:ContentUnderstandingAnalyzeAudioUrlWithContentRange + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3"; + + // Analyze from 5 seconds to end using ContentRange.timeRangeFrom() + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(5))); + + SyncPoller rangeOperation + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + + AudioVisualContent audioContent = (AudioVisualContent) rangeResult.getContents().get(0); + System.out.println("TimeRangeFrom(5s): Start=" + audioContent.getStartTime().toMillis() + + " ms, End=" + audioContent.getEndTime().toMillis() + " ms"); + + // Analyze a specific time window + AnalysisInput windowInput = new AnalysisInput(); + windowInput.setUrl(uriSource); + windowInput.setContentRange(ContentRange.timeRange(Duration.ofSeconds(2), Duration.ofSeconds(8))); + + SyncPoller windowOperation + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(windowInput)); + AnalysisResult windowResult = windowOperation.getFinalResult(); + + AudioVisualContent windowContent = (AudioVisualContent) windowResult.getContents().get(0); + System.out.println("TimeRange(2s, 8s): Start=" + windowContent.getStartTime().toMillis() + + " ms, End=" + windowContent.getEndTime().toMillis() + " ms"); + + // Analyze with sub-second precision using milliseconds + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange( + ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + SyncPoller subSecondOperation + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.getFinalResult(); + + AudioVisualContent subSecondContent = (AudioVisualContent) subSecondResult.getContents().get(0); + System.out.println("TimeRange(1200ms, 3651ms): Start=" + subSecondContent.getStartTime().toMillis() + + " ms, End=" + subSecondContent.getEndTime().toMillis() + " ms"); + // END:ContentUnderstandingAnalyzeAudioUrlWithContentRange + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java index 758ecfb223ba..69f94376487d 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java @@ -10,6 +10,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.AudioVisualContent; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -20,6 +21,7 @@ import com.azure.identity.DefaultAzureCredentialBuilder; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -63,6 +65,15 @@ public static void main(String[] args) { System.out.println("\n--- Image Analysis Example ---"); analyzeImageUrl(client); + + System.out.println("\n--- Document URL with ContentRange Example ---"); + analyzeDocumentUrlWithContentRange(client); + + System.out.println("\n--- Video URL with ContentRange Example ---"); + analyzeVideoUrlWithContentRange(client); + + System.out.println("\n--- Audio URL with ContentRange Example ---"); + analyzeAudioUrlWithContentRange(client); } /** @@ -416,4 +427,361 @@ public static void analyzeImageUrl(ContentUnderstandingAsyncClient client) { } // END:ContentUnderstandingAnalyzeImageUrlAsyncAsync } + + /** + * Sample demonstrating how to analyze a document URL with page-based ContentRange. + */ + public static void analyzeDocumentUrlWithContentRange(ContentUnderstandingAsyncClient client) { + // BEGIN:ContentUnderstandingAnalyzeDocumentUrlWithContentRangeAsync + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + + // Analyze only page 1 using ContentRange + AnalysisInput input = new AnalysisInput(); + input.setUrl(uriSource); + input.setContentRange(ContentRange.page(1)); + + PollerFlux operation + = client.beginAnalyze("prebuilt-documentSearch", Arrays.asList(input)); + + CountDownLatch latch = new CountDownLatch(1); + + operation.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + DocumentContent document = (DocumentContent) result.getContents().get(0); + System.out.println("Page(1): returned " + document.getPages().size() + " page" + + " (page " + document.getStartPageNumber() + ")"); + System.out.println("Markdown length: " + document.getMarkdown().length() + " characters"); + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch.countDown(), + error -> latch.countDown() + ); + + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for document analysis", e); + } + // END:ContentUnderstandingAnalyzeDocumentUrlWithContentRangeAsync + } + + /** + * Sample demonstrating how to analyze a video URL with time-based ContentRange. + */ + public static void analyzeVideoUrlWithContentRange(ContentUnderstandingAsyncClient client) { + // BEGIN:ContentUnderstandingAnalyzeVideoUrlWithContentRangeAsync + String videoUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4"; + + // Analyze the first 5 seconds of the video + AnalysisInput input1 = new AnalysisInput(); + input1.setUrl(videoUrl); + input1.setContentRange(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(5))); + + PollerFlux operation1 + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(input1)); + + CountDownLatch latch1 = new CountDownLatch(1); + + operation1.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + for (AnalysisContent media : result.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRange(0, 5s): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch1.countDown(), + error -> latch1.countDown() + ); + + try { + latch1.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for video analysis", e); + } + + // Analyze from 10 seconds onward using ContentRange.timeRangeFrom() + AnalysisInput input2 = new AnalysisInput(); + input2.setUrl(videoUrl); + input2.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(10))); + + PollerFlux operation2 + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(input2)); + + CountDownLatch latch2 = new CountDownLatch(1); + + operation2.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + for (AnalysisContent media : result.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRangeFrom(10s): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch2.countDown(), + error -> latch2.countDown() + ); + + try { + latch2.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for video analysis", e); + } + + // Analyze with sub-second precision using milliseconds + AnalysisInput input3 = new AnalysisInput(); + input3.setUrl(videoUrl); + input3.setContentRange( + ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + PollerFlux operation3 + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(input3)); + + CountDownLatch latch3 = new CountDownLatch(1); + + operation3.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + for (AnalysisContent media : result.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("TimeRange(1200ms, 3651ms): Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + } + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch3.countDown(), + error -> latch3.countDown() + ); + + try { + latch3.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for video analysis", e); + } + + // Analyze with combined time ranges + AnalysisInput input4 = new AnalysisInput(); + input4.setUrl(videoUrl); + input4.setContentRange(ContentRange.combine( + ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(3)), + ContentRange.timeRangeFrom(Duration.ofSeconds(30)))); + + PollerFlux operation4 + = client.beginAnalyze("prebuilt-videoSearch", Arrays.asList(input4)); + + CountDownLatch latch4 = new CountDownLatch(1); + + operation4.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + int segmentIndex = 1; + for (AnalysisContent media : result.getContents()) { + AudioVisualContent videoContent = (AudioVisualContent) media; + System.out.println("Combined segment " + segmentIndex + ": Start=" + videoContent.getStartTime().toMillis() + + " ms, End=" + videoContent.getEndTime().toMillis() + " ms"); + segmentIndex++; + } + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch4.countDown(), + error -> latch4.countDown() + ); + + try { + latch4.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for video analysis", e); + } + // END:ContentUnderstandingAnalyzeVideoUrlWithContentRangeAsync + } + + /** + * Sample demonstrating how to analyze an audio URL with time-based ContentRange. + */ + public static void analyzeAudioUrlWithContentRange(ContentUnderstandingAsyncClient client) { + // BEGIN:ContentUnderstandingAnalyzeAudioUrlWithContentRangeAsync + String audioUrl + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3"; + + // Analyze from 5 seconds onward + AnalysisInput input1 = new AnalysisInput(); + input1.setUrl(audioUrl); + input1.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(5))); + + PollerFlux operation1 + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(input1)); + + CountDownLatch latch1 = new CountDownLatch(1); + + operation1.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + AudioVisualContent audioContent = (AudioVisualContent) result.getContents().get(0); + System.out.println("TimeRangeFrom(5s): Start=" + audioContent.getStartTime().toMillis() + + " ms, End=" + audioContent.getEndTime().toMillis() + " ms"); + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch1.countDown(), + error -> latch1.countDown() + ); + + try { + latch1.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for audio analysis", e); + } + + // Analyze a specific time range (2s to 8s) + AnalysisInput input2 = new AnalysisInput(); + input2.setUrl(audioUrl); + input2.setContentRange(ContentRange.timeRange(Duration.ofSeconds(2), Duration.ofSeconds(8))); + + PollerFlux operation2 + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(input2)); + + CountDownLatch latch2 = new CountDownLatch(1); + + operation2.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + AudioVisualContent windowContent = (AudioVisualContent) result.getContents().get(0); + System.out.println("TimeRange(2s, 8s): Start=" + windowContent.getStartTime().toMillis() + + " ms, End=" + windowContent.getEndTime().toMillis() + " ms"); + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch2.countDown(), + error -> latch2.countDown() + ); + + try { + latch2.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for audio analysis", e); + } + + // Analyze with sub-second precision using milliseconds + AnalysisInput input3 = new AnalysisInput(); + input3.setUrl(audioUrl); + input3.setContentRange( + ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + PollerFlux operation3 + = client.beginAnalyze("prebuilt-audioSearch", Arrays.asList(input3)); + + CountDownLatch latch3 = new CountDownLatch(1); + + operation3.last() + .flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error(new RuntimeException( + "Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }) + .doOnNext(result -> { + AudioVisualContent subSecondContent = (AudioVisualContent) result.getContents().get(0); + System.out.println("TimeRange(1200ms, 3651ms): Start=" + subSecondContent.getStartTime().toMillis() + + " ms, End=" + subSecondContent.getEndTime().toMillis() + " ms"); + }) + .doOnError(error -> { + System.err.println("Error occurred: " + error.getMessage()); + }) + .subscribe( + result -> latch3.countDown(), + error -> latch3.countDown() + ); + + try { + latch3.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for audio analysis", e); + } + // END:ContentUnderstandingAnalyzeAudioUrlWithContentRangeAsync + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/ContentRangeTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/ContentRangeTest.java index bbaa94b264fa..29572f98a155 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/ContentRangeTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/ContentRangeTest.java @@ -3,6 +3,7 @@ package com.azure.ai.contentunderstanding.tests; +import com.azure.ai.contentunderstanding.models.AnalysisInput; import com.azure.ai.contentunderstanding.models.ContentRange; import org.junit.jupiter.api.Test; @@ -10,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; /** @@ -77,6 +79,11 @@ public void timeRangeMilliseconds() { assertEquals("1000-2000", ContentRange.timeRange(Duration.ofMillis(1000), Duration.ofMillis(2000)).toString()); } + @Test + public void timeRangeSameStartAndEnd() { + assertEquals("1000-1000", ContentRange.timeRange(Duration.ofMillis(1000), Duration.ofMillis(1000)).toString()); + } + @Test public void timeRangeRejectsNegativeStart() { assertThrows(IllegalArgumentException.class, @@ -95,6 +102,12 @@ public void timeRangeFromOpenEnded() { assertEquals("0-", ContentRange.timeRangeFrom(Duration.ZERO).toString()); } + @Test + public void timeRangeFromZeroStartTime() { + ContentRange range = ContentRange.timeRangeFrom(Duration.ZERO); + assertEquals("0-", range.toString()); + } + @Test public void timeRangeFromRejectsNegative() { assertThrows(IllegalArgumentException.class, () -> ContentRange.timeRangeFrom(Duration.ofMillis(-1))); @@ -184,4 +197,100 @@ public void timeRangeDurationRejectsNullEnd() { public void timeRangeFromDurationRejectsNull() { assertThrows(NullPointerException.class, () -> ContentRange.timeRangeFrom((Duration) null)); } + + @Test + public void timeRangeDurationRejectsNegativeStart() { + assertThrows(IllegalArgumentException.class, + () -> ContentRange.timeRange(Duration.ofMillis(-1), Duration.ofSeconds(5))); + } + + @Test + public void timeRangeDurationEndBeforeStart() { + assertThrows(IllegalArgumentException.class, + () -> ContentRange.timeRange(Duration.ofSeconds(5), Duration.ofSeconds(1))); + } + + @Test + public void timeRangeFromDurationRejectsNegative() { + assertThrows(IllegalArgumentException.class, () -> ContentRange.timeRangeFrom(Duration.ofMillis(-1))); + } + + @Test + public void constructorRawStringCustomFormats() { + ContentRange range = new ContentRange("custom-format"); + assertEquals("custom-format", range.toString()); + } + + // =================== AnalysisInput.setContentRange integration =================== + + @Test + public void analysisInputSetContentRangeAcceptsContentRange() { + AnalysisInput input = new AnalysisInput(); + input.setContentRange(ContentRange.pages(1, 3)); + // Verify via JSON round-trip that the range was stored + assertEquals("1-3", extractContentRangeFromJson(input)); + } + + @Test + public void analysisInputContentRangeNullByDefault() { + AnalysisInput input = new AnalysisInput(); + assertNull(extractContentRangeFromJson(input)); + } + + @Test + public void analysisInputContentRangeRoundTrips() { + AnalysisInput input = new AnalysisInput(); + ContentRange range = ContentRange.combine(ContentRange.pages(1, 3), ContentRange.page(5)); + input.setContentRange(range); + assertEquals("1-3,5", extractContentRangeFromJson(input)); + } + + @Test + public void analysisInputSetContentRangeNull() { + AnalysisInput input = new AnalysisInput(); + input.setContentRange(ContentRange.page(1)); + input.setContentRange((ContentRange) null); + assertNull(extractContentRangeFromJson(input)); + } + + /** + * Helper: serialize AnalysisInput to JSON and extract the "range" field value. + * This avoids depending on package-private getContentRange(). + */ + private static String extractContentRangeFromJson(AnalysisInput input) { + try { + java.io.StringWriter sw = new java.io.StringWriter(); + com.azure.json.JsonWriter writer = com.azure.json.JsonProviders.createWriter(sw); + input.toJson(writer); + writer.flush(); + com.azure.json.JsonReader reader = com.azure.json.JsonProviders.createReader(sw.toString()); + AnalysisInput deserialized = AnalysisInput.fromJson(reader); + // Use toJson again to inspect; or parse the JSON directly + java.io.StringWriter sw2 = new java.io.StringWriter(); + com.azure.json.JsonWriter writer2 = com.azure.json.JsonProviders.createWriter(sw2); + deserialized.toJson(writer2); + writer2.flush(); + String json = sw2.toString(); + // Parse "range" field from JSON + int idx = json.indexOf("\"range\""); + if (idx < 0) { + return null; + } + int colon = json.indexOf(':', idx); + int valueStart = colon + 1; + // skip whitespace + while (valueStart < json.length() && json.charAt(valueStart) == ' ') { + valueStart++; + } + if (valueStart >= json.length() || json.charAt(valueStart) == 'n') { + return null; // null value + } + // It's a quoted string + int quoteStart = json.indexOf('"', valueStart); + int quoteEnd = json.indexOf('"', quoteStart + 1); + return json.substring(quoteStart + 1, quoteEnd); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryAsyncTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryAsyncTest.java index 957b84325594..0a6710712fa7 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryAsyncTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryAsyncTest.java @@ -6,6 +6,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -268,4 +269,128 @@ public void testAnalyzeBinaryAsync() throws IOException { } // END:Assertion_ContentUnderstandingAccessDocumentProperties } + + @Test + public void testAnalyzeBinaryWithPageContentRangesAsync() throws IOException { + + // Load the multi-page sample file (4 pages) + String filePath = "src/samples/resources/mixed_financial_docs.pdf"; + Path path = Paths.get(filePath); + byte[] fileBytes = Files.readAllBytes(path); + BinaryData binaryData = BinaryData.fromBytes(fileBytes); + + // Full analysis for comparison + PollerFlux fullOperation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData); + AnalysisResult fullResult = fullOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent fullDoc = (DocumentContent) fullResult.getContents().get(0); + assertEquals(4, fullDoc.getPages().size(), "Full document should return all 4 pages"); + + // ---- PagesFrom(3) — extract pages 3 to end ---- + PollerFlux rangeOperation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.pagesFrom(3), "application/octet-stream", null); + AnalysisResult rangeResult = rangeOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + DocumentContent rangeDoc = (DocumentContent) rangeResult.getContents().get(0); + assertEquals(2, rangeDoc.getPages().size(), "With ContentRange.pagesFrom(3), should return only 2 pages"); + assertEquals(3, rangeDoc.getStartPageNumber(), "pagesFrom(3) should start at page 3"); + assertEquals(4, rangeDoc.getEndPageNumber(), "pagesFrom(3) should end at page 4"); + assertTrue(fullDoc.getPages().size() > rangeDoc.getPages().size()); + assertTrue(fullDoc.getMarkdown().length() > rangeDoc.getMarkdown().length()); + + // ---- Page(2) — single page ---- + PollerFlux page2Operation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.page(2), "application/octet-stream", null); + AnalysisResult page2Result = page2Operation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent page2Doc = (DocumentContent) page2Result.getContents().get(0); + + assertEquals(1, page2Doc.getPages().size(), "Page(2) should return exactly 1 page"); + assertEquals(2, page2Doc.getStartPageNumber(), "Page(2) should start at page 2"); + assertEquals(2, page2Doc.getEndPageNumber(), "Page(2) should end at page 2"); + assertTrue(fullDoc.getMarkdown().length() > page2Doc.getMarkdown().length()); + + // ---- Pages(1, 3) — page range ---- + PollerFlux pages13Operation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.pages(1, 3), "application/octet-stream", null); + AnalysisResult pages13Result = pages13Operation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent pages13Doc = (DocumentContent) pages13Result.getContents().get(0); + + assertEquals(3, pages13Doc.getPages().size(), "Pages(1,3) should return exactly 3 pages"); + assertEquals(1, pages13Doc.getStartPageNumber()); + assertEquals(3, pages13Doc.getEndPageNumber()); + assertTrue(fullDoc.getMarkdown().length() > pages13Doc.getMarkdown().length()); + + // ---- Combine(Page(1), Pages(3, 4)) — combined single page and page range ---- + PollerFlux combineOperation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.combine(ContentRange.page(1), ContentRange.pages(3, 4)), "application/octet-stream", null); + AnalysisResult combineResult = combineOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent combineDoc = (DocumentContent) combineResult.getContents().get(0); + + assertTrue(combineDoc.getPages().size() >= 2); + assertEquals(1, combineDoc.getStartPageNumber()); + assertTrue(combineDoc.getEndPageNumber() >= 4); + assertTrue(fullDoc.getMarkdown().length() >= combineDoc.getMarkdown().length()); + + // ---- Combine(Pages(1,3), Page(5), PagesFrom(9)) — combined page ranges with out-of-range pages ---- + // Note: The document has only 4 pages, so Page(5) and PagesFrom(9) will be clamped + PollerFlux bigCombineOperation + = contentUnderstandingAsyncClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.combine(ContentRange.pages(1, 3), ContentRange.page(5), ContentRange.pagesFrom(9)), + "application/octet-stream", null); + AnalysisResult bigCombineResult = bigCombineOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent bigCombineDoc = (DocumentContent) bigCombineResult.getContents().get(0); + + assertNotNull(bigCombineResult); + assertNotNull(bigCombineResult.getContents()); + assertTrue(bigCombineDoc.getPages().size() > 0); + assertTrue(fullDoc.getMarkdown().length() >= bigCombineDoc.getMarkdown().length()); + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryTest.java index cfcff1e2ccc1..5265578dc4e0 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample01_AnalyzeBinaryTest.java @@ -6,6 +6,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -259,4 +260,107 @@ public void testAnalyzeBinary() throws IOException { } // END:Assertion_ContentUnderstandingAccessDocumentProperties } + + @Test + public void testAnalyzeBinaryWithPageContentRanges() throws IOException { + + // Load the multi-page sample file (4 pages) + String filePath = "src/samples/resources/mixed_financial_docs.pdf"; + Path path = Paths.get(filePath); + byte[] fileBytes = Files.readAllBytes(path); + BinaryData binaryData = BinaryData.fromBytes(fileBytes); + + // Full analysis for comparison + SyncPoller fullOperation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData); + AnalysisResult fullResult = fullOperation.getFinalResult(); + DocumentContent fullDoc = (DocumentContent) fullResult.getContents().get(0); + assertEquals(4, fullDoc.getPages().size(), "Full document should return all 4 pages"); + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithPagesFrom + // ---- PagesFrom(3) — extract pages 3 to end ---- + SyncPoller rangeOperation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.pagesFrom(3), "application/octet-stream", null); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + // END:ContentUnderstandingAnalyzeBinaryWithPagesFrom + + // BEGIN:Assertion_ContentUnderstandingAnalyzeBinaryWithPagesFrom + assertNotNull(rangeOperation); + assertTrue(rangeOperation.waitForCompletion().getStatus().isComplete()); + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + DocumentContent rangeDoc = (DocumentContent) rangeResult.getContents().get(0); + assertEquals(2, rangeDoc.getPages().size(), "With ContentRange.pagesFrom(3), should return only 2 pages"); + assertEquals(3, rangeDoc.getStartPageNumber(), "pagesFrom(3) should start at page 3"); + assertEquals(4, rangeDoc.getEndPageNumber(), "pagesFrom(3) should end at page 4"); + assertTrue(fullDoc.getPages().size() > rangeDoc.getPages().size()); + assertTrue(fullDoc.getMarkdown().length() > rangeDoc.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeBinaryWithPagesFrom + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithCombinedPages + // ---- Combine(Pages(1,3), Page(5), PagesFrom(9)) — combined page ranges ---- + // Note: The document has only 4 pages, so Page(5) and PagesFrom(9) will be clamped + SyncPoller combineRangeOperation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.combine(ContentRange.pages(1, 3), ContentRange.page(5), ContentRange.pagesFrom(9)), + "application/octet-stream", null); + AnalysisResult combineRangeResult = combineRangeOperation.getFinalResult(); + // END:ContentUnderstandingAnalyzeBinaryWithCombinedPages + + // BEGIN:Assertion_ContentUnderstandingAnalyzeBinaryWithCombinedPages + assertNotNull(combineRangeOperation); + assertTrue(combineRangeOperation.waitForCompletion().getStatus().isComplete()); + assertNotNull(combineRangeResult); + assertNotNull(combineRangeResult.getContents()); + DocumentContent combineRangeDoc = (DocumentContent) combineRangeResult.getContents().get(0); + assertTrue(combineRangeDoc.getPages().size() > 0); + assertTrue(fullDoc.getMarkdown().length() >= combineRangeDoc.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeBinaryWithCombinedPages + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithSinglePage + // ---- Page(2) — single page ---- + SyncPoller page2Operation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, ContentRange.page(2), + "application/octet-stream", null); + DocumentContent page2Doc = (DocumentContent) page2Operation.getFinalResult().getContents().get(0); + // END:ContentUnderstandingAnalyzeBinaryWithSinglePage + + // BEGIN:Assertion_ContentUnderstandingAnalyzeBinaryWithSinglePage + assertEquals(1, page2Doc.getPages().size(), "Page(2) should return exactly 1 page"); + assertEquals(2, page2Doc.getStartPageNumber(), "Page(2) should start at page 2"); + assertEquals(2, page2Doc.getEndPageNumber(), "Page(2) should end at page 2"); + assertTrue(fullDoc.getMarkdown().length() > page2Doc.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeBinaryWithSinglePage + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithPageRange + // ---- Pages(1, 3) — page range ---- + SyncPoller pages13Operation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.pages(1, 3), "application/octet-stream", null); + DocumentContent pages13Doc = (DocumentContent) pages13Operation.getFinalResult().getContents().get(0); + // END:ContentUnderstandingAnalyzeBinaryWithPageRange + + // BEGIN:Assertion_ContentUnderstandingAnalyzeBinaryWithPageRange + assertEquals(3, pages13Doc.getPages().size(), "Pages(1,3) should return exactly 3 pages"); + assertEquals(1, pages13Doc.getStartPageNumber()); + assertEquals(3, pages13Doc.getEndPageNumber()); + assertTrue(fullDoc.getMarkdown().length() > pages13Doc.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeBinaryWithPageRange + + // BEGIN:ContentUnderstandingAnalyzeBinaryWithCombinedPageAndRange + // ---- Combine(Page(1), Pages(3, 4)) — combined single page and page range ---- + SyncPoller combineOperation + = contentUnderstandingClient.beginAnalyzeBinary("prebuilt-documentSearch", binaryData, + ContentRange.combine(ContentRange.page(1), ContentRange.pages(3, 4)), "application/octet-stream", null); + DocumentContent combineDoc = (DocumentContent) combineOperation.getFinalResult().getContents().get(0); + // END:ContentUnderstandingAnalyzeBinaryWithCombinedPageAndRange + + // BEGIN:Assertion_ContentUnderstandingAnalyzeBinaryWithCombinedPageAndRange + assertTrue(combineDoc.getPages().size() >= 2); + assertEquals(1, combineDoc.getStartPageNumber()); + assertTrue(combineDoc.getEndPageNumber() >= 4); + assertTrue(fullDoc.getMarkdown().length() >= combineDoc.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeBinaryWithCombinedPageAndRange + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java index 5b146e03d78d..3d84c3a4cce0 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java @@ -8,6 +8,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.AudioVisualContent; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -23,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -439,4 +441,284 @@ public void testAnalyzeImageUrlAsync() { System.out.println("Image analysis validation completed successfully"); // END:Assertion_ContentUnderstandingAnalyzeImageUrlAsyncAsync } + + @Test + public void testAnalyzeUrlWithPageContentRangesAsync() { + + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + + // Full 4-page analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + PollerFlux fullOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-documentSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + DocumentContent fullDoc = (DocumentContent) fullResult.getContents().get(0); + + // Extract only page 1 via AnalysisInput.setContentRange() + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.page(1)); + + PollerFlux rangeOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-documentSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertEquals(4, fullDoc.getPages().size(), "Full document should return all 4 pages"); + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + DocumentContent rangeDocContent = (DocumentContent) rangeResult.getContents().get(0); + assertEquals(1, rangeDocContent.getPages().size(), "With ContentRange.page(1), should return only 1 page"); + assertEquals(1, rangeDocContent.getStartPageNumber()); + assertEquals(1, rangeDocContent.getEndPageNumber()); + assertTrue(fullDoc.getPages().size() > rangeDocContent.getPages().size()); + assertTrue(fullDoc.getMarkdown().length() > rangeDocContent.getMarkdown().length()); + } + + @Test + public void testAnalyzeVideoUrlWithTimeContentRangesAsync() { + + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4"; + + // Full analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + PollerFlux fullOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(fullResult); + assertNotNull(fullResult.getContents()); + assertTrue(fullResult.getContents().size() > 0); + + // ---- TimeRange(0, 5s) — first 5 seconds ---- + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(5))); + + PollerFlux rangeOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + assertTrue(rangeResult.getContents().size() > 0); + for (AnalysisContent content : rangeResult.getContents()) { + assertTrue(content instanceof AudioVisualContent, "Video analysis should return AudioVisualContent"); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + } + + // ---- TimeRangeFrom(10s) — from 10 seconds to end ---- + AnalysisInput rangeFromInput = new AnalysisInput(); + rangeFromInput.setUrl(uriSource); + rangeFromInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(10))); + + PollerFlux rangeFromOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeFromInput)); + AnalysisResult rangeFromResult = rangeFromOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(rangeFromResult); + assertNotNull(rangeFromResult.getContents()); + assertTrue(rangeFromResult.getContents().size() > 0); + for (AnalysisContent content : rangeFromResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + assertNotNull(avContent.getMarkdown()); + assertFalse(avContent.getMarkdown().isEmpty()); + } + + // ---- TimeRange sub-second precision (1200ms-3651ms) ---- + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange(ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + PollerFlux subSecondOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(subSecondResult); + assertNotNull(subSecondResult.getContents()); + assertTrue(subSecondResult.getContents().size() > 0); + for (AnalysisContent content : subSecondResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + } + + // ---- Combine multiple time ranges (0-3s, 30s-) ---- + AnalysisInput combineInput = new AnalysisInput(); + combineInput.setUrl(uriSource); + combineInput.setContentRange(ContentRange.combine(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(3)), + ContentRange.timeRangeFrom(Duration.ofSeconds(30)))); + + PollerFlux combineOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(combineInput)); + AnalysisResult combineResult = combineOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(combineResult); + assertNotNull(combineResult.getContents()); + assertTrue(combineResult.getContents().size() > 0); + for (AnalysisContent content : combineResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + assertNotNull(avContent.getMarkdown()); + assertFalse(avContent.getMarkdown().isEmpty()); + } + } + + @Test + public void testAnalyzeAudioUrlWithTimeContentRangesAsync() { + + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3"; + + // Full analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + PollerFlux fullOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + AudioVisualContent fullAudioContent = (AudioVisualContent) fullResult.getContents().get(0); + long fullDurationMs = fullAudioContent.getEndTime().toMillis() - fullAudioContent.getStartTime().toMillis(); + + // ---- TimeRangeFrom(5s) — from 5 seconds to end ---- + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(5))); + + PollerFlux rangeOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + assertTrue(rangeResult.getContents().size() > 0); + AudioVisualContent rangeAudioContent = (AudioVisualContent) rangeResult.getContents().get(0); + assertTrue(rangeAudioContent instanceof AudioVisualContent); + assertTrue(fullAudioContent.getMarkdown().length() >= rangeAudioContent.getMarkdown().length()); + int fullPhraseCount + = fullAudioContent.getTranscriptPhrases() != null ? fullAudioContent.getTranscriptPhrases().size() : 0; + int rangePhraseCount + = rangeAudioContent.getTranscriptPhrases() != null ? rangeAudioContent.getTranscriptPhrases().size() : 0; + assertTrue(fullPhraseCount >= rangePhraseCount); + long rangeDurationMs = rangeAudioContent.getEndTime().toMillis() - rangeAudioContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= rangeDurationMs); + + // ---- TimeRange(2s, 8s) — specific time window ---- + AnalysisInput windowInput = new AnalysisInput(); + windowInput.setUrl(uriSource); + windowInput.setContentRange(ContentRange.timeRange(Duration.ofSeconds(2), Duration.ofSeconds(8))); + + PollerFlux windowOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(windowInput)); + AnalysisResult windowResult = windowOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + AudioVisualContent audioWindowContent = (AudioVisualContent) windowResult.getContents().get(0); + + assertTrue(audioWindowContent.getEndTime().toMillis() > audioWindowContent.getStartTime().toMillis()); + assertTrue(audioWindowContent.getMarkdown().length() > 0); + long windowDurationMs + = audioWindowContent.getEndTime().toMillis() - audioWindowContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= windowDurationMs); + + // ---- TimeRange sub-second precision (1200ms-3651ms) ---- + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange(ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + PollerFlux subSecondOperation + = contentUnderstandingAsyncClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.last().flatMap(pollResponse -> { + if (pollResponse.getStatus().isComplete()) { + return pollResponse.getFinalResult(); + } else { + return Mono.error( + new RuntimeException("Polling completed unsuccessfully with status: " + pollResponse.getStatus())); + } + }).block(); + AudioVisualContent audioSubSecondContent = (AudioVisualContent) subSecondResult.getContents().get(0); + + assertTrue(audioSubSecondContent.getEndTime().toMillis() > audioSubSecondContent.getStartTime().toMillis()); + assertTrue(audioSubSecondContent.getMarkdown().length() > 0); + long subSecondDurationMs + = audioSubSecondContent.getEndTime().toMillis() - audioSubSecondContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= subSecondDurationMs); + } } diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java index f0d0df4f9426..f283cd958df8 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java @@ -8,6 +8,7 @@ import com.azure.ai.contentunderstanding.models.AnalysisResult; import com.azure.ai.contentunderstanding.models.AudioVisualContent; import com.azure.ai.contentunderstanding.models.ContentAnalyzerAnalyzeOperationStatus; +import com.azure.ai.contentunderstanding.models.ContentRange; import com.azure.ai.contentunderstanding.models.DocumentContent; import com.azure.ai.contentunderstanding.models.DocumentPage; import com.azure.ai.contentunderstanding.models.DocumentTable; @@ -26,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.time.Duration; /** * Sample demonstrating how to analyze documents from URL using Content Understanding service. @@ -363,6 +365,221 @@ public void testAnalyzeAudioUrl() { // END:Assertion_ContentUnderstandingAnalyzeAudioUrl } + @Test + public void testAnalyzeUrlWithPageContentRanges() { + + // BEGIN:ContentUnderstandingAnalyzeUrlWithPageContentRanges + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + + // Full 4-page analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + SyncPoller fullOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-documentSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.getFinalResult(); + DocumentContent fullDoc = (DocumentContent) fullResult.getContents().get(0); + + // Extract only page 1 via AnalysisInput.setContentRange() + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.page(1)); + + SyncPoller rangeOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-documentSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + // END:ContentUnderstandingAnalyzeUrlWithPageContentRanges + + // BEGIN:Assertion_ContentUnderstandingAnalyzeUrlWithPageContentRanges + assertEquals(4, fullDoc.getPages().size(), "Full document should return all 4 pages"); + assertNotNull(rangeOperation); + assertTrue(rangeOperation.waitForCompletion().getStatus().isComplete()); + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + DocumentContent rangeDocContent = (DocumentContent) rangeResult.getContents().get(0); + assertEquals(1, rangeDocContent.getPages().size(), "With ContentRange.page(1), should return only 1 page"); + assertEquals(1, rangeDocContent.getStartPageNumber()); + assertEquals(1, rangeDocContent.getEndPageNumber()); + assertTrue(fullDoc.getPages().size() > rangeDocContent.getPages().size()); + assertTrue(fullDoc.getMarkdown().length() > rangeDocContent.getMarkdown().length()); + // END:Assertion_ContentUnderstandingAnalyzeUrlWithPageContentRanges + } + + @Test + public void testAnalyzeVideoUrlWithTimeContentRanges() { + // BEGIN:ContentUnderstandingAnalyzeVideoUrlWithTimeContentRanges + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/videos/sdk_samples/FlightSimulator.mp4"; + + // Full analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + SyncPoller fullOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.getFinalResult(); + + // ---- TimeRange(0, 5s) — first 5 seconds ---- + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(5))); + + SyncPoller rangeOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + // END:ContentUnderstandingAnalyzeVideoUrlWithTimeContentRanges + + // BEGIN:Assertion_ContentUnderstandingAnalyzeVideoUrlWithTimeContentRanges + assertNotNull(fullResult); + assertNotNull(fullResult.getContents()); + assertTrue(fullResult.getContents().size() > 0); + + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + assertTrue(rangeResult.getContents().size() > 0); + for (AnalysisContent content : rangeResult.getContents()) { + assertTrue(content instanceof AudioVisualContent, "Video analysis should return AudioVisualContent"); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + } + // END:Assertion_ContentUnderstandingAnalyzeVideoUrlWithTimeContentRanges + + // ---- TimeRangeFrom(10s) — from 10 seconds to end ---- + AnalysisInput rangeFromInput = new AnalysisInput(); + rangeFromInput.setUrl(uriSource); + rangeFromInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(10))); + + SyncPoller rangeFromOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(rangeFromInput)); + AnalysisResult rangeFromResult = rangeFromOperation.getFinalResult(); + + assertNotNull(rangeFromResult); + assertNotNull(rangeFromResult.getContents()); + assertTrue(rangeFromResult.getContents().size() > 0); + for (AnalysisContent content : rangeFromResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + assertNotNull(avContent.getMarkdown()); + assertFalse(avContent.getMarkdown().isEmpty()); + } + + // ---- TimeRange sub-second precision (1200ms-3651ms) ---- + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange(ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + SyncPoller subSecondOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.getFinalResult(); + + assertNotNull(subSecondResult); + assertNotNull(subSecondResult.getContents()); + assertTrue(subSecondResult.getContents().size() > 0); + for (AnalysisContent content : subSecondResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + } + + // ---- Combine multiple time ranges (0-3s, 30s-) ---- + AnalysisInput combineInput = new AnalysisInput(); + combineInput.setUrl(uriSource); + combineInput.setContentRange(ContentRange.combine(ContentRange.timeRange(Duration.ZERO, Duration.ofSeconds(3)), + ContentRange.timeRangeFrom(Duration.ofSeconds(30)))); + + SyncPoller combineOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-videoSearch", Arrays.asList(combineInput)); + AnalysisResult combineResult = combineOperation.getFinalResult(); + + assertNotNull(combineResult); + assertNotNull(combineResult.getContents()); + assertTrue(combineResult.getContents().size() > 0); + for (AnalysisContent content : combineResult.getContents()) { + assertTrue(content instanceof AudioVisualContent); + AudioVisualContent avContent = (AudioVisualContent) content; + assertTrue(avContent.getEndTime().toMillis() > avContent.getStartTime().toMillis()); + assertNotNull(avContent.getMarkdown()); + assertFalse(avContent.getMarkdown().isEmpty()); + } + } + + @Test + public void testAnalyzeAudioUrlWithTimeContentRanges() { + // BEGIN:ContentUnderstandingAnalyzeAudioUrlWithTimeContentRanges + String uriSource + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/audio/callCenterRecording.mp3"; + + // Full analysis for comparison + AnalysisInput fullInput = new AnalysisInput(); + fullInput.setUrl(uriSource); + + SyncPoller fullOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(fullInput)); + AnalysisResult fullResult = fullOperation.getFinalResult(); + AudioVisualContent fullAudioContent = (AudioVisualContent) fullResult.getContents().get(0); + long fullDurationMs = fullAudioContent.getEndTime().toMillis() - fullAudioContent.getStartTime().toMillis(); + + // ---- TimeRangeFrom(5s) — from 5 seconds to end ---- + AnalysisInput rangeInput = new AnalysisInput(); + rangeInput.setUrl(uriSource); + rangeInput.setContentRange(ContentRange.timeRangeFrom(Duration.ofSeconds(5))); + + SyncPoller rangeOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(rangeInput)); + AnalysisResult rangeResult = rangeOperation.getFinalResult(); + // END:ContentUnderstandingAnalyzeAudioUrlWithTimeContentRanges + + // BEGIN:Assertion_ContentUnderstandingAnalyzeAudioUrlWithTimeContentRanges + assertNotNull(rangeResult); + assertNotNull(rangeResult.getContents()); + assertTrue(rangeResult.getContents().size() > 0); + AudioVisualContent rangeAudioContent = (AudioVisualContent) rangeResult.getContents().get(0); + assertTrue(rangeAudioContent instanceof AudioVisualContent); + assertTrue(fullAudioContent.getMarkdown().length() >= rangeAudioContent.getMarkdown().length()); + int fullPhraseCount + = fullAudioContent.getTranscriptPhrases() != null ? fullAudioContent.getTranscriptPhrases().size() : 0; + int rangePhraseCount + = rangeAudioContent.getTranscriptPhrases() != null ? rangeAudioContent.getTranscriptPhrases().size() : 0; + assertTrue(fullPhraseCount >= rangePhraseCount); + long rangeDurationMs = rangeAudioContent.getEndTime().toMillis() - rangeAudioContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= rangeDurationMs); + // END:Assertion_ContentUnderstandingAnalyzeAudioUrlWithTimeContentRanges + + // ---- TimeRange(2s, 8s) — specific time window ---- + AnalysisInput windowInput = new AnalysisInput(); + windowInput.setUrl(uriSource); + windowInput.setContentRange(ContentRange.timeRange(Duration.ofSeconds(2), Duration.ofSeconds(8))); + + SyncPoller windowOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(windowInput)); + AnalysisResult windowResult = windowOperation.getFinalResult(); + AudioVisualContent audioWindowContent = (AudioVisualContent) windowResult.getContents().get(0); + + assertTrue(audioWindowContent.getEndTime().toMillis() > audioWindowContent.getStartTime().toMillis()); + assertTrue(audioWindowContent.getMarkdown().length() > 0); + long windowDurationMs + = audioWindowContent.getEndTime().toMillis() - audioWindowContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= windowDurationMs); + + // ---- TimeRange sub-second precision (1200ms-3651ms) ---- + AnalysisInput subSecondInput = new AnalysisInput(); + subSecondInput.setUrl(uriSource); + subSecondInput.setContentRange(ContentRange.timeRange(Duration.ofMillis(1200), Duration.ofMillis(3651))); + + SyncPoller subSecondOperation + = contentUnderstandingClient.beginAnalyze("prebuilt-audioSearch", Arrays.asList(subSecondInput)); + AnalysisResult subSecondResult = subSecondOperation.getFinalResult(); + AudioVisualContent audioSubSecondContent = (AudioVisualContent) subSecondResult.getContents().get(0); + + assertTrue(audioSubSecondContent.getEndTime().toMillis() > audioSubSecondContent.getStartTime().toMillis()); + assertTrue(audioSubSecondContent.getMarkdown().length() > 0); + long subSecondDurationMs + = audioSubSecondContent.getEndTime().toMillis() - audioSubSecondContent.getStartTime().toMillis(); + assertTrue(fullDurationMs >= subSecondDurationMs); + } + @Test public void testAnalyzeImageUrl() { // BEGIN:ContentUnderstandingAnalyzeImageUrl From b70d35b6da5303d50e7d0aac4d946b8da25601a0 Mon Sep 17 00:00:00 2001 From: Changjian Wang Date: Wed, 11 Mar 2026 13:05:52 +0800 Subject: [PATCH 2/3] Fix document URL in ContentRange sample and tests --- .../ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java | 2 +- .../contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java | 2 +- .../tests/samples/Sample02_AnalyzeUrlAsyncTest.java | 2 +- .../tests/samples/Sample02_AnalyzeUrlTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java index cdb3c9c3c3cd..2790937a7637 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrl.java @@ -299,7 +299,7 @@ public static void analyzeImageUrl(ContentUnderstandingClient client) { public static void analyzeDocumentUrlWithContentRange(ContentUnderstandingClient client) { // BEGIN:ContentUnderstandingAnalyzeUrlWithContentRange String uriSource - = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf"; // Extract only page 1 using ContentRange AnalysisInput rangeInput = new AnalysisInput(); diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java index 69f94376487d..41787da968d8 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/samples/java/com/azure/ai/contentunderstanding/samples/Sample02_AnalyzeUrlAsync.java @@ -434,7 +434,7 @@ public static void analyzeImageUrl(ContentUnderstandingAsyncClient client) { public static void analyzeDocumentUrlWithContentRange(ContentUnderstandingAsyncClient client) { // BEGIN:ContentUnderstandingAnalyzeDocumentUrlWithContentRangeAsync String uriSource - = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf"; // Analyze only page 1 using ContentRange AnalysisInput input = new AnalysisInput(); diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java index 3d84c3a4cce0..05ab9bef190c 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlAsyncTest.java @@ -446,7 +446,7 @@ public void testAnalyzeImageUrlAsync() { public void testAnalyzeUrlWithPageContentRangesAsync() { String uriSource - = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf"; // Full 4-page analysis for comparison AnalysisInput fullInput = new AnalysisInput(); diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java index f283cd958df8..9cc382e14741 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/src/test/java/com/azure/ai/contentunderstanding/tests/samples/Sample02_AnalyzeUrlTest.java @@ -370,7 +370,7 @@ public void testAnalyzeUrlWithPageContentRanges() { // BEGIN:ContentUnderstandingAnalyzeUrlWithPageContentRanges String uriSource - = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/documents/mixed_financial_docs.pdf"; + = "https://raw.githubusercontent.com/Azure-Samples/azure-ai-content-understanding-assets/main/document/mixed_financial_docs.pdf"; // Full 4-page analysis for comparison AnalysisInput fullInput = new AnalysisInput(); From 422eeaadc04a394f743c6a456cf9010d816da2ee Mon Sep 17 00:00:00 2001 From: Changjian Wang Date: Wed, 11 Mar 2026 13:21:32 +0800 Subject: [PATCH 3/3] Update test recordings for ContentRange samples --- .../azure-ai-contentunderstanding/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json b/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json index 83ce14e6ee66..3b72fa4403b4 100644 --- a/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json +++ b/sdk/contentunderstanding/azure-ai-contentunderstanding/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/contentunderstanding/azure-ai-contentunderstanding", - "Tag": "java/contentunderstanding/azure-ai-contentunderstanding_6d6c9cbfc1" + "Tag": "java/contentunderstanding/azure-ai-contentunderstanding_a546deb443" }