diff --git a/.doc_gen/cross-content/cross_FSA_Java_block.xml b/.doc_gen/cross-content/cross_FSA_Java_block.xml new file mode 100644 index 00000000000..be14e657e61 --- /dev/null +++ b/.doc_gen/cross-content/cross_FSA_Java_block.xml @@ -0,0 +1,33 @@ + + + %phrases-shared; +]> + + + This example application analyzes and stores customer feedback cards. Specifically, + it fulfills the need of a fictitious hotel in New York City. The hotel receives feedback + from guests in various languages in the form of physical comment cards. That feedback + is uploaded into the app through a web client. + + After an image of a comment card is uploaded, the following steps occur: + + + + Text is extracted from the image using &TEXTRACT;. + + + &CMP; determines the sentiment of the extracted text and its language. + + + The extracted text is translated to French using &TSL;. + + + &POL; synthesizes an audio file from the extracted text. + + + The full app can be deployed with the &CDK;. For source code and deployment + instructions, see the project in + GitHub. + \ No newline at end of file diff --git a/.doc_gen/metadata/cross_metadata.yaml b/.doc_gen/metadata/cross_metadata.yaml index da78e464480..87d447c5160 100644 --- a/.doc_gen/metadata/cross_metadata.yaml +++ b/.doc_gen/metadata/cross_metadata.yaml @@ -6,6 +6,10 @@ cross_FSA: their original language, determines their sentiment, and generates an audio file from the translated text. languages: + Java: + versions: + - sdk_version: 2 + block_content: cross_FSA_Java_block.xml Ruby: versions: - sdk_version: 3 diff --git a/.github/pre_validate/pre_validate.py b/.github/pre_validate/pre_validate.py index 3357d7bacec..f7d79ff6e2b 100644 --- a/.github/pre_validate/pre_validate.py +++ b/.github/pre_validate/pre_validate.py @@ -103,6 +103,7 @@ 'AKIAIOSFODNN7EXAMPLE', 'APKAEIBAERJR2EXAMPLE', 'AppStreamUsageReportsCFNGlueAthenaAccess', + 'examples/blob/main/applications/feedback', 'aws/acm/model/DescribeCertificateRequest', 'aws/cloudtrail/model/LookupEventsRequest', 'aws/codebuild/model/BatchGetBuildsResult', diff --git a/applications/feedback_sentiment_analyzer/README.md b/applications/feedback_sentiment_analyzer/README.md index 04c30745163..78e0c3ae171 100644 --- a/applications/feedback_sentiment_analyzer/README.md +++ b/applications/feedback_sentiment_analyzer/README.md @@ -16,6 +16,7 @@ This application has been implemented with the following AWS SDKs. - [Ruby](../../ruby/cross_service_examples/feedback_sentiment_analyzer/README.md) - [JavaScript](../../javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/README.md) +- [Java](../../javav2/usecases/creating_fsa_app/README.md) To deploy one of these implementations, follow the [Deployment instructions](#deployment-instructions). diff --git a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts index 21c9cd18b6d..77bd6a54cf4 100644 --- a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts +++ b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts @@ -6,7 +6,6 @@ import {resolve} from "path"; import {BundlingOutput, Duration} from "aws-cdk-lib"; import {Code, Runtime} from "aws-cdk-lib/aws-lambda"; - import {AppFunctionConfig} from "./constructs/app-lambdas"; const BASE_APP_FUNCTION: AppFunctionConfig = { @@ -55,7 +54,7 @@ const EXAMPLE_LANG_FUNCTIONS: AppFunctionConfig[] = [ return Code.fromInline(` exports.handler = async (event) => { console.log("AnalyzeSentiment", event); - return { translated_text: "Bonjour", source_language: "en" } + return { translated_text: "Bonjour", source_language: "en"} } `); }, @@ -164,14 +163,63 @@ const JAVASCRIPT_FUNCTIONS = [ }, ]; +const JAVA_BUNDLING_CONFIG = { + command: [ + "bash", + "-c", + "mvn install -DskipTests && cp target/creating_fsa_app-1.0-SNAPSHOT.jar /asset-output/", + ], + output: BundlingOutput.ARCHIVED, + user: "root", + image: Runtime.JAVA_11.bundlingImage, + volumes: [ + { + hostPath: `${process.env.HOME}/.m2/`, + containerPath: "/root/.m2", + }, + ], +}; + +const COMMON_JAVA_FUNCTION_CONFIG = { + ...BASE_APP_FUNCTION, + runtime: Runtime.JAVA_11, + codeAsset: () => { + const source = resolve("../../../javav2/usecases/creating_fsa_app"); + return Code.fromAsset(source, { + bundling: JAVA_BUNDLING_CONFIG, + }); + }, +}; + +const JAVA_FUNCTIONS: AppFunctionConfig[] = [ + { + ...COMMON_JAVA_FUNCTION_CONFIG, + name: "ExtractText", + handler: "com.example.fsa.handlers.ExtractTextHandler::handleRequest", + }, + { + ...COMMON_JAVA_FUNCTION_CONFIG, + name: "AnalyzeSentiment", + handler: "com.example.fsa.handlers.AnalyzeSentimentHandler::handleRequest", + }, + { + ...COMMON_JAVA_FUNCTION_CONFIG, + name: "TranslateText", + handler: "com.example.fsa.handlers.TranslateTextHandler::handleRequest", + }, + { + ...COMMON_JAVA_FUNCTION_CONFIG, + name: "SynthesizeAudio", + handler: "com.example.fsa.handlers.SynthesizeAudioHandler::handleRequest", + }, +]; + const FUNCTIONS: Record = { examplelang: EXAMPLE_LANG_FUNCTIONS, - // Add more languages here. For example - // javascript: JAVASCRIPT_FUNCTIONS, ruby: RUBY_FUNCTIONS, + java: JAVA_FUNCTIONS, javascript: JAVASCRIPT_FUNCTIONS, }; - export function getFunctions(language: string = ""): AppFunctionConfig[] { return FUNCTIONS[language] ?? FUNCTIONS.examplelang; } diff --git a/javav2/README.md b/javav2/README.md index ee1cccab5ab..42efadcf472 100644 --- a/javav2/README.md +++ b/javav2/README.md @@ -25,7 +25,9 @@ The **javav2** folder in this repository contains examples of complete use cases In the **use_cases** folder, find step-by-step development tutorials that use multiple AWS services. By following these tutorials, you will gain a deeper understanding of how to create Java-based applications that use the AWS SDK for Java. Most of these AWS SDK for Java tutorials use the Synchronous Java client. -If you are interested in using the Asynchronous Java client, see one of these tutorials: +If you are interested in using Asynchronous Java service clients, see one of these tutorials: + ++ [Creating a Feedback Sentiment Analyzer application using the SDK for Java](https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/usecases/creating_fsa_app) - Discusses how to develop a Feedback Sentiment Analyzer application using Machine Learning AWS services. The application solves a fictitious use case of a hotel that receives guest feedback on comment cards in a variety of languages. The application is developed by using the AWS SDK for Java (v2) and asynchronous Java clients. + [Creating a dynamic web application that asynchronously analyzes photos using the AWS SDK for Java](https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/usecases/creating_photo_analyzer_async) - Discusses using the AWS SDK for Java (asynchronous client) and various AWS services, such as the Amazon Rekognition service, to analyze images. This web MVC application can analyze many images and generate a report that breaks down each image into a series of labels. diff --git a/javav2/usecases/creating_fsa_app/README.md b/javav2/usecases/creating_fsa_app/README.md new file mode 100644 index 00000000000..1911f49a5d2 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/README.md @@ -0,0 +1,846 @@ +# Creating a Feedback Sentiment Analyzer application using the SDK for Java + +## Overview + +| Heading | Description | +| ----------- | ----------- | +| Description | Discusses how to develop a Feedback Sentiment Analyzer application using [Machine Learning AWS services](https://aws.amazon.com/machine-learning/). The application solves a fictitious use case of a hotel that receives guest feedback on comment cards in a variety of languages. The application is developed by using the AWS SDK for Java (v2) and asynchronous service clients. For information, see [Asynchronous programming](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/asynchronous.html). | +| Audience | Developer (intermediate / advanced) | +| Updated | 9/12/2023 | +| Required skills | Java, Maven | + +## Purpose + +You can develop a sample application for a Feedback Sentiment Analyzer (FSA) that accomplishes the following tasks: + +1. Hosts static website assets in an Amazon Simple Storage Service (Amazon S3) bucket, and uses an Amazon CloudFront distribution to serve the website. +2. Implements authenticated access to Amazon API Gateway through Amazon Cognito. +3. Configures Amazon API Gateway to deposit items into an S3 bucket, subsequently triggering an Amazon EventBridge rule that initiates an AWS Step Functions workflow. +4. The Step Functions workflow leverages AWS Lambda, Amazon Textract, Amazon Comprehend, Amazon Translate, and Amazon Polly to execute the core business logic. +5. Metadata gets stored within Amazon DynamoDB, while audio files are stored in the same S3 bucket referenced in step 3. +6. Amazon API Gateway retrieves the metadata from Amazon DynamoDB. + + +![AWS Photo Analyzer](images/overview.png) + +As displayed in the preceding illustration, the FSA application uses the following AWS services: + +* Amazon Textract - Extracts text +* Amazon Comprehend - Detects sentiment +* Amazon Translate - Translates to English +* Amazon Polly - Synthesizes to human-like speech + +#### Topics + ++ Prerequisites ++ Understand the Feedback Sentiment Analyzer application ++ Create an IntelliJ project ++ Add the POM dependencies to your project ++ Create the Java classes ++ Deploy the AWS resources + +## Prerequisites + +To complete the tutorial, you need the following: + ++ An AWS account ++ A Java IDE (this tutorial uses the IntelliJ IDE) ++ Java JDK 11 ++ Maven 3.6 or later + +### Important + ++ The AWS services included in this document are included in the [AWS Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc). ++ This code has not been tested in all AWS Regions. Some AWS services are available only in specific Regions. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). ++ Running this code might result in charges to your AWS account. ++ Be sure to delete all of the resources you create while going through this tutorial so that you won't be charged. ++ Also make sure to properly set up your development environment. For information, see [Setting up the AWS SDK for Java 2.x](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/setup.html). + +### Resource creation + +The required AWS resources are created by using an AWS Cloud Development Kit (AWS CDK) script. This is discussed later in the document. There is no need to create any resources by using the AWS Management Console. + +## Understand the Feedback Sentiment Analyzer application + +The front end of the FSA application is a React application that uses the [Cloudscape Design System](https://cloudscape.design/). The application supports uploading images that contain text to an S3 bucket. The text represents comments made by a customer in various languages, such as French. + +After a user authenticates by using Amazon Cognito, the application displays all uploaded images, the translated text, and a button that lets the user hear the audio (which was created by using Amazon Polly). + +![AWS Photo Analyzer](images/app.png) + +### What happens after an image is uploaded to an S3 bucket + +After an image is uploaded into the storage bucket, an EventBridge rule is triggered that starts a Step Functions workflow. + +![AWS Photo Analyzer](images/workflow.png) + +The following descibes each step in the workflow. + +- **ExtractText** - Extracts text from the image. The text can be in another language such as French. +- **AnalyzeSentiment** - Retrieves the sentiment of the text. For example, it can determine if the text is positive or negative. +- **ContinueIfPositive** - Routes the workflow based on the sentiment value. If the value is positive, the the next step is TranslateText. +- **TranslateText** - Translates the text into English. +- **SynthesizeAudio** - Converts the English text into an MP3 audio file and places the audio file into an S3 bucket. +- **PutPositiveComment** - Places the data into an Amazon DynamoDB table. + +**Note**: The client React application does not display any data until the workflow successfully completes and data is stored in the S3 bucket and DynamoDB table. + +The following illustration shows the Amazon DynamoDB table storing the values. + +![AWS Photo Analyzer](images/dbtable.png) + +**Note**: This DynamoDB table is created when you run the AWS CDK script to set up the resources. This is discussed later in this document. + +### Understand the AWS resources used by the FSA application + +This section describes the AWS resources that the FSA application uses. You do not have to manually deploy any of these AWS resources, such as the AWS Lambda functions, by using the AWS Management Console. Instead, you can deploy all of them by running a provided AWS CDK script. Instructions on how to deploy these AWS resources are provided later in this document. + +#### AWS Lambda functions + +The backend of the FSA application is implemented by using these AWS Lambda functions created by using the AWS SDK for Java (v2): + +- **ExtractText** +- **AnalyzeSentiment** +- **TranslateText** +- **SynthesizeAudio** + +**Note**: These AWS Lambda names are short names. The full names that appear in the AWS Management Console depend on how you configure the provided AWS CDK script. Full names appear as {NAME}{Function Name}. For example, **fsa-user-java-SynthesizeAudio134971D4-x8Q5178Y4ZBH**. + +## Create an IntelliJ project + +1. In the IntelliJ IDE, choose **File**, **New**, **Project**. +2. In the **New Project** dialog box, choose **Maven**, and then choose **Next**. +3. For **GroupId**, enter **aws-fsa**. +4. For **ArtifactId**, enter **fsa_app**. +6. Choose **Next**. +7. Choose **Finish**. + +## Add the POM dependencies to your project + +At this point, you have a new project named **fsa_app**. + +**Note:** Be sure to use Java 11 (as shown in the following **pom.xml** file). + +Make sure that the **pom.xml** file looks like the following. + +```xml + + + 4.0.0 + org.example + creating_fsa_app + 1.0-SNAPSHOT + + 11 + 11 + UTF-8 + + + + + software.amazon.awssdk + bom + 2.20.45 + pom + import + + + org.apache.logging.log4j + log4j-bom + 2.19.0 + pom + import + + + + + + software.amazon.awssdk + dynamodb + + + commons-logging + commons-logging + 1.2 + + + software.amazon.awssdk + apache-client + + + commons-logging + commons-logging + + + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk.crt + aws-crt + 0.25.1 + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-1.2-api + + + com.amazonaws + aws-lambda-java-events + 3.11.1 + + + software.amazon.awssdk + textract + + + software.amazon.awssdk + s3control + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + software.amazon.awssdk + translate + + + software.amazon.awssdk + comprehend + + + org.json + json + 20230227 + + + software.amazon.awssdk + polly + + + org.apache.maven.surefire + surefire-booter + 3.0.0-M3 + + + software.amazon.awssdk + lambda + + + commons-io + commons-io + 2.7 + + + software.amazon.awssdk + s3-transfer-manager + 2.20.26 + + + org.junit.jupiter + junit-jupiter-engine + 5.9.0 + test + + + com.googlecode.json-simple + json-simple + 1.1 + + + org.junit.jupiter + junit-jupiter-api + 5.9.0 + test + + + + + + maven-surefire-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + false + + + + package + + shade + + + + + + + +``` + +## Create the Java classes + +Create a Java package in the **main/java** folder named **com.example.fsa**. The Java files go into sub-packages. + +### Handlers package + +Create these Java classes in the **com.example.fsa.handlers** package. These Java classes use the AWS Lambda Java runtime API to build the AWS Lambda functions described earlier in this document. Each class represents a handler for a separate AWS Lambda function. For more information about the AWS Lambda Java runtime API, see [AWS Lambda function handler in Java](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html). + ++ **AnalyzeSentimentHandler** - The handler for the **AnalyzeSentiment** Lambda function. ++ **ExtractTextHandler** - The handler for the **ExtractText** Lambda function. ++ **SynthesizeAudioHandler** - The handler for the **fnSynthesizeAudio** Lambda function. ++ **TranslateTextHandler** - The handler for the **TranslateText** Lambda function. + +#### AnalyzeSentimentHandler class + +The following is the **AnalyzeSentimentHandler** class. + +```java +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.DetectSentimentService; +import org.json.simple.JSONObject; +import java.util.Map; + +public class AnalyzeSentimentHandler implements RequestHandler, JSONObject> { + + @Override + public JSONObject handleRequest(Map requestObject, Context context) { + String sourceText = (String) requestObject.get("source_text"); + context.getLogger().log("Extracted text: " +sourceText); + DetectSentimentService detectSentimentService = new DetectSentimentService(); + JSONObject jsonOb = detectSentimentService.detectSentiments(sourceText); + context.getLogger().log("JSON: " + jsonOb.toJSONString()); + return jsonOb; + } +} + +``` + +#### ExtractTextHandler class + +The following Java code represents the **S3Handler** class. + +```java +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.ExtractTextService; +import java.util.Map; + +public class ExtractTextHandler implements RequestHandler, String>{ + + @Override + public String handleRequest(Map requestObject, Context context) { + // Get the Amazon Simple Storage Service (Amazon S3) bucket and object key from the Amazon EventBridge event. + ExtractTextService textService = new ExtractTextService(); + String bucket = (String) requestObject.get("bucket"); + String fileName = (String) requestObject.get("object"); + context.getLogger().log("*** Bucket: " + bucket + ", fileName: " + fileName); + String extractedText = textService.getCardText(bucket, fileName); + context.getLogger().log("*** Text: " + extractedText); + return extractedText; + } +} + + +``` + +#### SynthesizeAudioHandler class + +The following Java code represents the **SynthesizeAudioHandler** class. + +```java +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.PollyService; +import com.example.fsa.services.S3Service; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class SynthesizeAudioHandler implements RequestHandler, String> { + @Override + public String handleRequest(Map requestObject, Context context) { + S3Service s3Service = new S3Service(); + PollyService pollyService = new PollyService(); + String translatedText = (String) requestObject.get("translated_text"); + String bucket = (String) requestObject.get("bucket"); + String key = (String) requestObject.get("object"); + key = key + ".mp3"; // Appends ".mp3" to the existing value of key + context.getLogger().log("*** Translated Text: " +translatedText +" and new key is "+key); + try { + InputStream is = pollyService.synthesize(translatedText); + String audioFile = s3Service.putAudio(is, bucket, key); + context.getLogger().log("You have successfully added the " +audioFile +" in "+bucket); + return audioFile ; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} + +``` + +#### TranslateTextHandler class + +The following Java code represents the **TranslateTextHandler** class. + +```java +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.TranslateService; +import org.json.simple.JSONObject; +import java.util.Map; + +public class TranslateTextHandler implements RequestHandler, JSONObject> { + + @Override + public JSONObject handleRequest(Map requestObject, Context context) { + TranslateService translateService = new TranslateService(); + String sourceText = (String) requestObject.get("extracted_text"); + String lanCode = (String) requestObject.get("source_language_code"); + context.getLogger().log("sourceText: " + sourceText + "lang code: "+lanCode); + String translatedText = translateService.translateText(lanCode, sourceText); + context.getLogger().log("Translated text : " + translatedText); + JSONObject jsonResponse = new JSONObject(); + jsonResponse.put("translated_text", translatedText); + return jsonResponse; + } +} +``` + +### Services package + +Create these Java classes in the **com.example.fsa.services** package. These Java classes use the AWS SDK for Java (v2) asynchronous service clients to perform various AWS operations. For example, the **TextractAsyncClient** class translates the given text into English. + ++ **DetectSentimentService** - Uses the **ComprehendAsyncClient** to detect the sentimant of text. ++ **ExtractTextService** - Uses the **TextractAsyncClient** to extract text from an image located in an S3 bucket. ++ **PollyService** - Uses the **PollyAsyncClient** to convert text into an MP3 audio file. ++ **S3Service** - Uses the **S3TransferManager** to place an audio file into an S3 bucket. ++ **TranslateService** - Uses the **TranslateAsyncClient** to translate text into English. + + #### DetectSentimentService class + + The following Java code represents the **DetectSentimentService** class. + +```java +package com.example.fsa.services; + +import org.json.simple.JSONObject; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.comprehend.ComprehendAsyncClient; +import software.amazon.awssdk.services.comprehend.model.ComprehendException; +import software.amazon.awssdk.services.comprehend.model.DetectDominantLanguageRequest; +import software.amazon.awssdk.services.comprehend.model.DetectDominantLanguageResponse; +import software.amazon.awssdk.services.comprehend.model.DetectSentimentRequest; +import software.amazon.awssdk.services.comprehend.model.DetectSentimentResponse; +import software.amazon.awssdk.services.comprehend.model.DominantLanguage; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class DetectSentimentService { + + private static ComprehendAsyncClient comprehendAsyncClient; + private static synchronized ComprehendAsyncClient getComprehendAsyncClient() { + if (comprehendAsyncClient == null) { + comprehendAsyncClient = ComprehendAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return comprehendAsyncClient; + } + + public JSONObject detectSentiments(String text){ + try { + String languageCode = detectTheDominantLanguage(text); + DetectSentimentRequest detectSentimentRequest = DetectSentimentRequest.builder() + .text(text) + .languageCode(languageCode) + .build(); + + CompletableFuture future = getComprehendAsyncClient().detectSentiment(detectSentimentRequest); + future.join(); + + // Wait for the operation to complete and get the result + DetectSentimentResponse detectSentimentResult = (DetectSentimentResponse) future.join(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("sentiment", detectSentimentResult.sentimentAsString()); + jsonObject.put("language_code", languageCode); + return jsonObject; + + } catch (ComprehendException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } + + public String detectTheDominantLanguage(String text){ + try { + DetectDominantLanguageRequest request = DetectDominantLanguageRequest.builder() + .text(text) + .build(); + + CompletableFuture future = getComprehendAsyncClient().detectDominantLanguage(request); + future.join(); + + DetectDominantLanguageResponse resp = (DetectDominantLanguageResponse) future.join(); + List allLanList = resp.languages(); + if (!allLanList.isEmpty()) { + DominantLanguage firstLanguage = allLanList.get(0); + return firstLanguage.languageCode(); + } else { + return "No languages found"; + } + + } catch (ComprehendException e) { + System.out.println(e.getMessage()); + throw e; + } + } +} + +``` + +#### ExtractTextService class + +The following Java code represents the **ExtractTextService** class. + +```java + package com.example.fsa.services; + +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.textract.model.BlockType; +import software.amazon.awssdk.services.textract.model.Document; +import software.amazon.awssdk.services.textract.model.DetectDocumentTextRequest; +import software.amazon.awssdk.services.textract.model.DetectDocumentTextResponse; +import software.amazon.awssdk.services.textract.model.Block; +import software.amazon.awssdk.services.textract.TextractAsyncClient; +import software.amazon.awssdk.services.textract.model.S3Object; +import software.amazon.awssdk.services.textract.model.TextractException; + +import java.util.concurrent.CompletableFuture; + +public class ExtractTextService { + + private static TextractAsyncClient textractAsyncClient; + + private static synchronized TextractAsyncClient getTextractAsyncClient() { + if (textractAsyncClient == null) { + textractAsyncClient = TextractAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return textractAsyncClient; + } + + public String getCardText(String bucketName, String obName) { + try { + S3Object s3Object = S3Object.builder() + .bucket(bucketName) + .name(obName) + .build(); + + Document myDoc = Document.builder() + .s3Object(s3Object) + .build(); + + DetectDocumentTextRequest detectDocumentTextRequest = DetectDocumentTextRequest.builder() + .document(myDoc) + .build(); + + StringBuilder completeText = new StringBuilder(); + CompletableFuture future = getTextractAsyncClient().detectDocumentText(detectDocumentTextRequest); + future.join(); + + DetectDocumentTextResponse textResponse = (DetectDocumentTextResponse) future.join(); + for (Block block : textResponse.blocks()) { + if (block.blockType() == BlockType.WORD) { + if (completeText.length() == 0) { + completeText.append(block.text()); + } else { + completeText.append(" ").append(block.text()); + } + } + } + return completeText.toString(); + + } catch (TextractException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; // Re-throw the exception. + } catch (SdkClientException e) { + System.err.println(e.getMessage()); + throw e; // Re-throw the exception. + } + } +} + +``` + + #### PollyService class + + The following Java code represents the **PollyService** class. This code performs the following tasks: + ++ It starts by creating a **DescribeVoicesRequest** object, which is used to request information about available voices for speech synthesis. + ++ It then initiates an asynchronous request to the Polly service to describe available voices using the **PollyAsyncClient** object's **describeVoices** method. This returns a **CompletableFuture** called future. + ++ The code uses **future.join()** to block and wait for the completion of the asynchronous request. This ensures that the voice description information is available before proceeding. + ++ After the request is complete, it casts the result to a **DescribeVoicesResponse** object called describeVoicesResult. + ++ The code then filters the list of available voices to find a voice named **Joanna** using Java streams. + ++ Once it has found the desired voice (in this case, "Joanna"), it constructs a **SynthesizeSpeechRequest** object. This request is used to synthesize speech from the input text. It specifies the input text, output format (MP3), and the voice to be used (based on the previously found "Joanna"). + ++ Another asynchronous request is initiated using **PollyAsyncClient** object's **synthesizeSpeech** method. This request is used to synthesize the speech, and it returns a CompletableFuture> called **audioFuture**. + ++ The code then blocks and waits for the completion of the audio synthesis request using audioFuture.join(). + ++ Finally, it returns the resulting **InputStream** containing the synthesized audio. + +```java +package com.example.fsa.services; + +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.polly.PollyAsyncClient; +import software.amazon.awssdk.services.polly.model.DescribeVoicesRequest; +import software.amazon.awssdk.services.polly.model.PollyException; +import software.amazon.awssdk.services.polly.model.SynthesizeSpeechRequest; +import software.amazon.awssdk.services.polly.model.SynthesizeSpeechResponse; +import software.amazon.awssdk.services.polly.model.Voice; +import software.amazon.awssdk.services.polly.model.DescribeVoicesResponse; +import software.amazon.awssdk.services.polly.model.OutputFormat; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; + +public class PollyService { + private static PollyAsyncClient pollyAsyncClient; + + private static synchronized PollyAsyncClient getPollyAsyncClient() { + if (pollyAsyncClient == null) { + Region region = Region.US_EAST_1; + pollyAsyncClient = PollyAsyncClient.builder() + .region(region) + .build(); + } + return pollyAsyncClient; + } + + public InputStream synthesize(String text) throws IOException { + try { + DescribeVoicesRequest describeVoicesRequest = DescribeVoicesRequest.builder() + .engine("neural") + .build(); + + CompletableFuture future = getPollyAsyncClient().describeVoices(describeVoicesRequest); + DescribeVoicesResponse describeVoicesResult = (DescribeVoicesResponse) future.join(); + Voice voice = describeVoicesResult.voices().stream() + .filter(v -> v.name().equals("Joanna")) + .findFirst() + .orElseThrow(() -> new RuntimeException("Voice not found")); + + SynthesizeSpeechRequest request = SynthesizeSpeechRequest.builder() + .text(text) + .outputFormat(OutputFormat.MP3) + .voiceId(voice.id()) + .build(); + + CompletableFuture> audioFuture = getPollyAsyncClient().synthesizeSpeech(request, AsyncResponseTransformer.toBlockingInputStream()); + InputStream audioInputStream = audioFuture.join(); + return audioInputStream; + + } catch (PollyException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } +} + +``` + +#### S3Service class + +The following Java code represents the **S3Service** class. This example uses the [Amazon S3 Transfer Manager API](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/transfer/s3/S3TransferManager.html#upload(software.amazon.awssdk.transfer.s3.model.UploadRequest)) to upload a .mp3 file to an S3 bucket. This code performs the following tasks: + ++ It configures the **S3TransferManager** instance by specifying the S3 client to use, which is obtained from the **getS3AsyncClient()** method. + ++ It creates a **BlockingInputStreamAsyncRequestBody** named body, which is used to provide the data to be uploaded to S3. It is initialized with a null input stream, indicating that the stream will be provided later. + ++ It creates an **Upload** instance by using the **transferManager.upload()** method. The **Upload** object is responsible for managing the upload operation. + ++ Finally, it waits for the completion of the upload operation using **upload.completionFuture().join()**, ensuring that the upload is finished before returning. + +```java +package com.example.fsa.services; + +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.Upload; +import java.io.IOException; +import java.io.InputStream; + +public class S3Service { + + private static S3AsyncClient s3AsyncClient; + + private static synchronized S3AsyncClient getS3AsyncClient() { + if (s3AsyncClient == null) { + s3AsyncClient = S3AsyncClient.crtBuilder() + .region(Region.US_EAST_1) + .build(); + } + return s3AsyncClient; + } + + // Put the audio file into the Amazon S3 bucket. + public String putAudio(InputStream is, String bucketName, String key) throws S3Exception, IOException { + try { + S3TransferManager transferManager = S3TransferManager.builder() + .s3Client(getS3AsyncClient()) + .build(); + + // 'null' indicates a stream will be provided later. + BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(null); + Upload upload = transferManager.upload(builder -> builder + .requestBody(body) + .putObjectRequest(req -> req.bucket(bucketName).key(key)) + .build()); + + body.writeInputStream(is); + upload.completionFuture().join(); + return key; + + } catch (S3Exception e) { + System.err.println(e.getMessage()); + throw e; + } + } +} + + +``` + +#### TranslateService class + +The following Java code represents the **TranslateService** class. + +```java + package com.example.fsa.services; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.translate.TranslateAsyncClient; +import software.amazon.awssdk.services.translate.model.TranslateException; +import software.amazon.awssdk.services.translate.model.TranslateTextRequest; +import software.amazon.awssdk.services.translate.model.TranslateTextResponse; + +import java.util.concurrent.CompletableFuture; + +public class TranslateService { + + private static TranslateAsyncClient translateAsyncClient; + + private static synchronized TranslateAsyncClient getTranslateAsyncClient() { + if (translateAsyncClient == null) { + translateAsyncClient = TranslateAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return translateAsyncClient; + } + + public String translateText(String lanCode, String text) { + try { + TranslateTextRequest textRequest = TranslateTextRequest.builder() + .sourceLanguageCode(lanCode) + .targetLanguageCode("en") + .text(text) + .build(); + + CompletableFuture future = getTranslateAsyncClient().translateText(textRequest); + TranslateTextResponse textResponse = (TranslateTextResponse) future.join(); + return textResponse.translatedText(); + + } catch (TranslateException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } +} + +``` + +## Deploy the AWS resources + +At this point, you have completed all of the application Java business logic required for the FSA application to work. Now you need to deploy the AWS resources, including the AWS Lambda functions and API Gateway endpoints. + +Instead of deploying all of the resources manually by using the AWS Management Console, you can use a provided AWS CDK script. The script makes it more efficient to deploy the resources. + +**Note**: For information about the AWS CDK, see [What is the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/home.html). + +For complete instuctions on how to run the supplied AWS CDK script, see [Feedback Sentiment Analyzer (FSA)](https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/applications/feedback_sentiment_analyzer/README.md). + +### Check permissions for the Lambda roles + +Each Lambda function has a role used to invoke the Lambda function. For example, the **AnalyzeSentiment** function has the following role. + +**java-AnalyzeSentimentServiceR-14R5C2CE32WCM** + +**Note**: The exact name of the role depends on how you configured the AWS CDK script. + +Make sure each role has the correct service permission to invoke the corresponding AWS service. Otherwise, you may encounter an error message such as: + + user: arn:aws:sts::xxxx:assumed-role/fsa-user-java-fnTranslateTextServiceRole41F86E1F-7K31GKO5HOUF is not authorized to perform: comprehend:DetectDominantLanguage because no identity-based policy allows the comprehend:DetectDominantLanguage action + +**Note**: For information about how to modify a role's permission, see [Using service-linked roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html). + +### Check the Lambda configuration options + +Check the Lambda configuration options and if desired you can modify values such as the timeout value and adjust the memory allocated to each function. For more information, see [Configuring Lambda function options](https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-common.html). + +### Run the application + +After you run the AWS CDK script, you can run the client application by using the Amazon Cloudfront distribution URL as specified in the supplied AWS CDK instructions. After you upload an image using the React client, a workflow is invoked and you can view its progress using AWS Step Functions. When it successfully completes, each step is green, you will see a .mp3 file in the S3 bucket, and you will see a new record in the Amazon DynamoDB table. + +### Next steps +Congratulations! You have created and deployed the FSA application. As stated at the beginning of this tutorial, delete all of the resources by following the AWS CDK instructions so that you won't continue to be charged for them. + +For more AWS multiservice examples, see +[usecases](https://github.com/awsdocs/aws-doc-sdk-examples/tree/master/javav2/usecases). + diff --git a/javav2/usecases/creating_fsa_app/images/app.png b/javav2/usecases/creating_fsa_app/images/app.png new file mode 100644 index 00000000000..205a0d957da Binary files /dev/null and b/javav2/usecases/creating_fsa_app/images/app.png differ diff --git a/javav2/usecases/creating_fsa_app/images/dbtable.png b/javav2/usecases/creating_fsa_app/images/dbtable.png new file mode 100644 index 00000000000..2fdde05729f Binary files /dev/null and b/javav2/usecases/creating_fsa_app/images/dbtable.png differ diff --git a/javav2/usecases/creating_fsa_app/images/overview.png b/javav2/usecases/creating_fsa_app/images/overview.png new file mode 100644 index 00000000000..16c48840924 Binary files /dev/null and b/javav2/usecases/creating_fsa_app/images/overview.png differ diff --git a/javav2/usecases/creating_fsa_app/images/role.png b/javav2/usecases/creating_fsa_app/images/role.png new file mode 100644 index 00000000000..4682849e4cd Binary files /dev/null and b/javav2/usecases/creating_fsa_app/images/role.png differ diff --git a/javav2/usecases/creating_fsa_app/images/workflow.png b/javav2/usecases/creating_fsa_app/images/workflow.png new file mode 100644 index 00000000000..1510288c556 Binary files /dev/null and b/javav2/usecases/creating_fsa_app/images/workflow.png differ diff --git a/javav2/usecases/creating_fsa_app/pom.xml b/javav2/usecases/creating_fsa_app/pom.xml new file mode 100644 index 00000000000..a71bc6f7bf4 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + org.example + creating_fsa_app + 1.0-SNAPSHOT + + 11 + 11 + UTF-8 + + + + + software.amazon.awssdk + bom + 2.20.45 + pom + import + + + org.apache.logging.log4j + log4j-bom + 2.19.0 + pom + import + + + + + + software.amazon.awssdk + dynamodb + + + commons-logging + commons-logging + 1.2 + + + software.amazon.awssdk + apache-client + + + commons-logging + commons-logging + + + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk.crt + aws-crt + 0.25.1 + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-1.2-api + + + com.amazonaws + aws-lambda-java-events + 3.11.1 + + + software.amazon.awssdk + textract + + + software.amazon.awssdk + s3control + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + software.amazon.awssdk + translate + + + software.amazon.awssdk + comprehend + + + org.json + json + 20230227 + + + software.amazon.awssdk + polly + + + org.apache.maven.surefire + surefire-booter + 3.0.0-M3 + + + software.amazon.awssdk + lambda + + + commons-io + commons-io + 2.7 + + + software.amazon.awssdk + s3-transfer-manager + 2.20.26 + + + org.junit.jupiter + junit-jupiter-engine + 5.9.0 + test + + + com.googlecode.json-simple + json-simple + 1.1 + + + org.junit.jupiter + junit-jupiter-api + 5.9.0 + test + + + + + + maven-surefire-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + false + + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/AnalyzeSentimentHandler.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/AnalyzeSentimentHandler.java new file mode 100644 index 00000000000..e5e922529b0 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/AnalyzeSentimentHandler.java @@ -0,0 +1,26 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.DetectSentimentService; +import org.json.simple.JSONObject; +import java.util.Map; + +public class AnalyzeSentimentHandler implements RequestHandler, JSONObject> { + + @Override + public JSONObject handleRequest(Map requestObject, Context context) { + String sourceText = (String) requestObject.get("source_text"); + context.getLogger().log("Extracted text: " +sourceText); + DetectSentimentService detectSentimentService = new DetectSentimentService(); + JSONObject jsonOb = detectSentimentService.detectSentiments(sourceText); + context.getLogger().log("JSON: " + jsonOb.toJSONString()); + return jsonOb; + } +} + diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/ExtractTextHandler.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/ExtractTextHandler.java new file mode 100644 index 00000000000..8119e55c359 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/ExtractTextHandler.java @@ -0,0 +1,26 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.ExtractTextService; +import java.util.Map; + +public class ExtractTextHandler implements RequestHandler, String>{ + + @Override + public String handleRequest(Map requestObject, Context context) { + // Get the Amazon Simple Storage Service (Amazon S3) bucket and object key from the Amazon EventBridge event. + ExtractTextService textService = new ExtractTextService(); + String bucket = (String) requestObject.get("bucket"); + String fileName = (String) requestObject.get("object"); + context.getLogger().log("*** Bucket: " + bucket + ", fileName: " + fileName); + String extractedText = textService.getCardText(bucket, fileName); + context.getLogger().log("*** Text: " + extractedText); + return extractedText; + } +} diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/SynthesizeAudioHandler.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/SynthesizeAudioHandler.java new file mode 100644 index 00000000000..bf0df7ee177 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/SynthesizeAudioHandler.java @@ -0,0 +1,35 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.PollyService; +import com.example.fsa.services.S3Service; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class SynthesizeAudioHandler implements RequestHandler, String> { + @Override + public String handleRequest(Map requestObject, Context context) { + S3Service s3Service = new S3Service(); + PollyService pollyService = new PollyService(); + String translatedText = (String) requestObject.get("translated_text"); + String bucket = (String) requestObject.get("bucket"); + String key = (String) requestObject.get("object"); + key = key + ".mp3"; // Appends ".mp3" to the existing value of key + context.getLogger().log("*** Translated Text: " +translatedText +" and new key is "+key); + try { + InputStream is = pollyService.synthesize(translatedText); + String audioFile = s3Service.putAudio(is, bucket, key); + context.getLogger().log("You have successfully added the " +audioFile +" in "+bucket); + return audioFile ; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/TranslateTextHandler.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/TranslateTextHandler.java new file mode 100644 index 00000000000..db77a077ad7 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/handlers/TranslateTextHandler.java @@ -0,0 +1,28 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.example.fsa.services.TranslateService; +import org.json.simple.JSONObject; +import java.util.Map; + +public class TranslateTextHandler implements RequestHandler, JSONObject> { + + @Override + public JSONObject handleRequest(Map requestObject, Context context) { + TranslateService translateService = new TranslateService(); + String sourceText = (String) requestObject.get("extracted_text"); + String lanCode = (String) requestObject.get("source_language_code"); + context.getLogger().log("sourceText: " + sourceText + "lang code: "+lanCode); + String translatedText = translateService.translateText(lanCode, sourceText); + context.getLogger().log("Translated text : " + translatedText); + JSONObject jsonResponse = new JSONObject(); + jsonResponse.put("translated_text", translatedText); + return jsonResponse; + } +} \ No newline at end of file diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/DetectSentimentService.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/DetectSentimentService.java new file mode 100644 index 00000000000..036b4b87f40 --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/DetectSentimentService.java @@ -0,0 +1,79 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.services; + +import org.json.simple.JSONObject; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.comprehend.ComprehendAsyncClient; +import software.amazon.awssdk.services.comprehend.model.ComprehendException; +import software.amazon.awssdk.services.comprehend.model.DetectDominantLanguageRequest; +import software.amazon.awssdk.services.comprehend.model.DetectDominantLanguageResponse; +import software.amazon.awssdk.services.comprehend.model.DetectSentimentRequest; +import software.amazon.awssdk.services.comprehend.model.DetectSentimentResponse; +import software.amazon.awssdk.services.comprehend.model.DominantLanguage; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class DetectSentimentService { + + private static ComprehendAsyncClient comprehendAsyncClient; + private static synchronized ComprehendAsyncClient getComprehendAsyncClient() { + if (comprehendAsyncClient == null) { + comprehendAsyncClient = ComprehendAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return comprehendAsyncClient; + } + + public JSONObject detectSentiments(String text){ + try { + String languageCode = detectTheDominantLanguage(text); + DetectSentimentRequest detectSentimentRequest = DetectSentimentRequest.builder() + .text(text) + .languageCode(languageCode) + .build(); + + CompletableFuture future = getComprehendAsyncClient().detectSentiment(detectSentimentRequest); + future.join(); + + // Wait for the operation to complete and get the result + DetectSentimentResponse detectSentimentResult = (DetectSentimentResponse) future.join(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("sentiment", detectSentimentResult.sentimentAsString()); + jsonObject.put("language_code", languageCode); + return jsonObject; + + } catch (ComprehendException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } + + public String detectTheDominantLanguage(String text){ + try { + DetectDominantLanguageRequest request = DetectDominantLanguageRequest.builder() + .text(text) + .build(); + + CompletableFuture future = getComprehendAsyncClient().detectDominantLanguage(request); + future.join(); + + DetectDominantLanguageResponse resp = (DetectDominantLanguageResponse) future.join(); + List allLanList = resp.languages(); + if (!allLanList.isEmpty()) { + DominantLanguage firstLanguage = allLanList.get(0); + return firstLanguage.languageCode(); + } else { + return "No languages found"; + } + + } catch (ComprehendException e) { + System.out.println(e.getMessage()); + throw e; + } + } +} diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/ExtractTextService.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/ExtractTextService.java new file mode 100644 index 00000000000..58960444cac --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/ExtractTextService.java @@ -0,0 +1,73 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.services; + +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.textract.model.BlockType; +import software.amazon.awssdk.services.textract.model.Document; +import software.amazon.awssdk.services.textract.model.DetectDocumentTextRequest; +import software.amazon.awssdk.services.textract.model.DetectDocumentTextResponse; +import software.amazon.awssdk.services.textract.model.Block; +import software.amazon.awssdk.services.textract.TextractAsyncClient; +import software.amazon.awssdk.services.textract.model.S3Object; +import software.amazon.awssdk.services.textract.model.TextractException; + +import java.util.concurrent.CompletableFuture; + +public class ExtractTextService { + + private static TextractAsyncClient textractAsyncClient; + + private static synchronized TextractAsyncClient getTextractAsyncClient() { + if (textractAsyncClient == null) { + textractAsyncClient = TextractAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return textractAsyncClient; + } + + public String getCardText(String bucketName, String obName) { + try { + S3Object s3Object = S3Object.builder() + .bucket(bucketName) + .name(obName) + .build(); + + Document myDoc = Document.builder() + .s3Object(s3Object) + .build(); + + DetectDocumentTextRequest detectDocumentTextRequest = DetectDocumentTextRequest.builder() + .document(myDoc) + .build(); + + StringBuilder completeText = new StringBuilder(); + CompletableFuture future = getTextractAsyncClient().detectDocumentText(detectDocumentTextRequest); + future.join(); + + DetectDocumentTextResponse textResponse = (DetectDocumentTextResponse) future.join(); + for (Block block : textResponse.blocks()) { + if (block.blockType() == BlockType.WORD) { + if (completeText.length() == 0) { + completeText.append(block.text()); + } else { + completeText.append(" ").append(block.text()); + } + } + } + return completeText.toString(); + + } catch (TextractException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; // Re-throw the exception. + } catch (SdkClientException e) { + System.err.println(e.getMessage()); + throw e; // Re-throw the exception. + } + } +} \ No newline at end of file diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/PollyService.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/PollyService.java new file mode 100644 index 00000000000..e2a06e5bb5d --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/PollyService.java @@ -0,0 +1,64 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.services; + +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.polly.PollyAsyncClient; +import software.amazon.awssdk.services.polly.model.DescribeVoicesRequest; +import software.amazon.awssdk.services.polly.model.PollyException; +import software.amazon.awssdk.services.polly.model.SynthesizeSpeechRequest; +import software.amazon.awssdk.services.polly.model.SynthesizeSpeechResponse; +import software.amazon.awssdk.services.polly.model.Voice; +import software.amazon.awssdk.services.polly.model.DescribeVoicesResponse; +import software.amazon.awssdk.services.polly.model.OutputFormat; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; + +public class PollyService { + private static PollyAsyncClient pollyAsyncClient; + + private static synchronized PollyAsyncClient getPollyAsyncClient() { + if (pollyAsyncClient == null) { + Region region = Region.US_EAST_1; + pollyAsyncClient = PollyAsyncClient.builder() + .region(region) + .build(); + } + return pollyAsyncClient; + } + + public InputStream synthesize(String text) throws IOException { + try { + DescribeVoicesRequest describeVoicesRequest = DescribeVoicesRequest.builder() + .engine("neural") + .build(); + + CompletableFuture future = getPollyAsyncClient().describeVoices(describeVoicesRequest); + DescribeVoicesResponse describeVoicesResult = (DescribeVoicesResponse) future.join(); + Voice voice = describeVoicesResult.voices().stream() + .filter(v -> v.name().equals("Joanna")) + .findFirst() + .orElseThrow(() -> new RuntimeException("Voice not found")); + + SynthesizeSpeechRequest request = SynthesizeSpeechRequest.builder() + .text(text) + .outputFormat(OutputFormat.MP3) + .voiceId(voice.id()) + .build(); + + CompletableFuture> audioFuture = getPollyAsyncClient().synthesizeSpeech(request, AsyncResponseTransformer.toBlockingInputStream()); + InputStream audioInputStream = audioFuture.join(); + return audioInputStream; + + } catch (PollyException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } +} diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/S3Service.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/S3Service.java new file mode 100644 index 00000000000..ca1a51ba89f --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/S3Service.java @@ -0,0 +1,53 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.services; + +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.Upload; +import java.io.IOException; +import java.io.InputStream; + +public class S3Service { + + private static S3AsyncClient s3AsyncClient; + + private static synchronized S3AsyncClient getS3AsyncClient() { + if (s3AsyncClient == null) { + s3AsyncClient = S3AsyncClient.crtBuilder() + .region(Region.US_EAST_1) + .build(); + } + return s3AsyncClient; + } + + // Put the audio file into the Amazon S3 bucket. + public String putAudio(InputStream is, String bucketName, String key) throws S3Exception, IOException { + try { + S3TransferManager transferManager = S3TransferManager.builder() + .s3Client(getS3AsyncClient()) + .build(); + + BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(null); // 'null' indicates a stream will be provided later. + Upload upload = transferManager.upload(builder -> builder + .requestBody(body) + .putObjectRequest(req -> req.bucket(bucketName).key(key)) + .build()); + + body.writeInputStream(is); + upload.completionFuture().join(); + return key; + + } catch (S3Exception e) { + System.err.println(e.getMessage()); + throw e; + } + } +} diff --git a/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/TranslateService.java b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/TranslateService.java new file mode 100644 index 00000000000..b79e7bb4c1d --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/main/java/com/example/fsa/services/TranslateService.java @@ -0,0 +1,46 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.fsa.services; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.translate.TranslateAsyncClient; +import software.amazon.awssdk.services.translate.model.TranslateException; +import software.amazon.awssdk.services.translate.model.TranslateTextRequest; +import software.amazon.awssdk.services.translate.model.TranslateTextResponse; + +import java.util.concurrent.CompletableFuture; + +public class TranslateService { + + private static TranslateAsyncClient translateAsyncClient; + + private static synchronized TranslateAsyncClient getTranslateAsyncClient() { + if (translateAsyncClient == null) { + translateAsyncClient = TranslateAsyncClient.builder() + .region(Region.US_EAST_1) + .build(); + } + return translateAsyncClient; + } + + public String translateText(String lanCode, String text) { + try { + TranslateTextRequest textRequest = TranslateTextRequest.builder() + .sourceLanguageCode(lanCode) + .targetLanguageCode("en") + .text(text) + .build(); + + CompletableFuture future = getTranslateAsyncClient().translateText(textRequest); + TranslateTextResponse textResponse = (TranslateTextResponse) future.join(); + return textResponse.translatedText(); + + } catch (TranslateException e) { + System.err.println(e.awsErrorDetails().errorMessage()); + throw e; + } + } +} diff --git a/javav2/usecases/creating_fsa_app/src/test/java/FSATests.java b/javav2/usecases/creating_fsa_app/src/test/java/FSATests.java new file mode 100644 index 00000000000..972265fc95b --- /dev/null +++ b/javav2/usecases/creating_fsa_app/src/test/java/FSATests.java @@ -0,0 +1,97 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +import com.example.fsa.services.DetectSentimentService; +import com.example.fsa.services.ExtractTextService; +import com.example.fsa.services.PollyService; +import com.example.fsa.services.S3Service; +import com.example.fsa.services.TranslateService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import java.io.IOException; +import java.io.InputStream; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class FSATests { + + // Specify the key to use. + private static final String objectName = "Enter the object name here"; + + // Specify the bucket name. + private static final String bucketName = "Enter the bucket name here"; + + private ExtractTextService textService; + private TranslateService translateService; + private PollyService pollyService; + private S3Service s3Service; + private DetectSentimentService sentimentService; + + @BeforeEach + public void setUp() { + textService = new ExtractTextService(); + translateService = new TranslateService(); + pollyService = new PollyService(); + s3Service = new S3Service(); + sentimentService = new DetectSentimentService(); + } + + @Test + @Tag("IntegrationTest") + @Order(1) + public void testTextExtraction() { + String text = textService.getCardText(bucketName, objectName); + Assertions.assertNotNull(text); + } + + @Test + @Tag("IntegrationTest") + @Order(2) + public void testSentimentAnalysis() { + String text = textService.getCardText(bucketName, objectName); + String sentiment = String.valueOf(sentimentService.detectSentiments(text)); + Assertions.assertNotNull(sentiment); + } + + @Test + @Tag("IntegrationTest") + @Order(3) + public void testTranslation() { + String text = textService.getCardText(bucketName, objectName); + String lanCode = sentimentService.detectTheDominantLanguage(text); + String translatedText = translateService.translateText(lanCode, text); + Assertions.assertNotNull(lanCode); + Assertions.assertNotNull(translatedText); + } + + @Test + @Tag("IntegrationTest") + @Order(4) + public void testAudioSynthesis() throws IOException { + String text = textService.getCardText(bucketName, objectName); + String lanCode = sentimentService.detectTheDominantLanguage(text); + String translatedText = translateService.translateText(lanCode, text); + InputStream is = pollyService.synthesize(translatedText); + Assertions.assertNotNull(is); + } + + @Test + @Tag("IntegrationTest") + @Order(5) + public void testS3Upload() throws IOException { + String text = textService.getCardText(bucketName, objectName); + String lanCode = sentimentService.detectTheDominantLanguage(text); + String translatedText = translateService.translateText(lanCode, text); + InputStream is = pollyService.synthesize(translatedText); + String mp3File = objectName + ".mp3"; + String uploadResult = s3Service.putAudio(is, bucketName, mp3File); + Assertions.assertNotNull(uploadResult); + } +} \ No newline at end of file