diff --git a/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml b/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml index 3e3e8bd33a1..ab3d20c23e8 100644 --- a/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml +++ b/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml @@ -22,7 +22,7 @@ &CMP; determines the sentiment of the extracted text and its language. - The extracted text is translated to French using &TSL;. + The extracted text is translated to English using &TSL;. &POL; synthesizes an audio file from the extracted text. diff --git a/.doc_gen/cross-content/cross_FSA_Java_block.xml b/.doc_gen/cross-content/cross_FSA_Java_block.xml index be14e657e61..5cc9641b3ef 100644 --- a/.doc_gen/cross-content/cross_FSA_Java_block.xml +++ b/.doc_gen/cross-content/cross_FSA_Java_block.xml @@ -20,7 +20,7 @@ &CMP; determines the sentiment of the extracted text and its language. - The extracted text is translated to French using &TSL;. + The extracted text is translated to English using &TSL;. &POL; synthesizes an audio file from the extracted text. diff --git a/.doc_gen/cross-content/cross_FSA_NetV3_block.xml b/.doc_gen/cross-content/cross_FSA_NetV3_block.xml new file mode 100644 index 00000000000..e778df46c48 --- /dev/null +++ b/.doc_gen/cross-content/cross_FSA_NetV3_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 English 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/cross-content/cross_FSA_Ruby_block.xml b/.doc_gen/cross-content/cross_FSA_Ruby_block.xml index 9d734724753..540e39a122f 100644 --- a/.doc_gen/cross-content/cross_FSA_Ruby_block.xml +++ b/.doc_gen/cross-content/cross_FSA_Ruby_block.xml @@ -20,7 +20,7 @@ &CMP; determines the sentiment of the extracted text and its language. - The extracted text is translated to French using &TSL;. + The extracted text is translated to English using &TSL;. &POL; synthesizes an audio file from the extracted text. diff --git a/.doc_gen/metadata/cross_metadata.yaml b/.doc_gen/metadata/cross_metadata.yaml index db23246abda..91c4b67f8fb 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: + .NET: + versions: + - sdk_version: 3 + block_content: cross_FSA_NetV3_block.xml Java: versions: - sdk_version: 2 diff --git a/.github/pre_validate/pre_validate.py b/.github/pre_validate/pre_validate.py index 07153a109f5..77eb6d7af82 100644 --- a/.github/pre_validate/pre_validate.py +++ b/.github/pre_validate/pre_validate.py @@ -202,6 +202,7 @@ 'role/AmazonSageMakerGeospatialFullAccess', 'VectorEnrichmentJobDataSourceConfigInput', 'com/workdocs/latest/APIReference/Welcome', + 'service/FeedbackSentimentAnalyzer/README' } def check_files(root, quiet): diff --git a/applications/feedback_sentiment_analyzer/README.md b/applications/feedback_sentiment_analyzer/README.md index 78e0c3ae171..53ad0180580 100644 --- a/applications/feedback_sentiment_analyzer/README.md +++ b/applications/feedback_sentiment_analyzer/README.md @@ -17,6 +17,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) +- [.NET](../../dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md) To deploy one of these implementations, follow the [Deployment instructions](#deployment-instructions). diff --git a/applications/feedback_sentiment_analyzer/SPECIFICATION.md b/applications/feedback_sentiment_analyzer/SPECIFICATION.md index e1e2b787cee..eed56cba22d 100644 --- a/applications/feedback_sentiment_analyzer/SPECIFICATION.md +++ b/applications/feedback_sentiment_analyzer/SPECIFICATION.md @@ -285,7 +285,7 @@ For example: ### SynthesizeAudio -Uses the Amazon Polly [SynthesizeSpeech](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) method to convert input text into life-like speech. Store the synthesized audio in the provided Amazon S3 bucket with a content type of "audio/mp3". +Uses the Amazon Polly [SynthesizeSpeech](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) method to convert input text into life-like speech. Store the synthesized audio in the provided Amazon S3 bucket with a content type of "audio/mpeg". #### **Input** diff --git a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts index 77bd6a54cf4..a326fa69c02 100644 --- a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts +++ b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts @@ -214,11 +214,119 @@ const JAVA_FUNCTIONS: AppFunctionConfig[] = [ }, ]; +const DOTNET_FUNCTIONS = [ + { + ...BASE_APP_FUNCTION, + name: "ExtractText", + handler: "FsaExtractText::FsaExtractText.ExtractTextFunction::FunctionHandler", + runtime: Runtime.DOTNET_6, + codeAsset() { + const source = resolve( + "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer" + ); + return Code.fromAsset(source, { + bundling: { + command: [ + "/bin/sh", + "-c", + " dotnet tool install -g Amazon.Lambda.Tools" + + " && dotnet build" + + " && cd FsaExtractText" + + " && dotnet lambda package --output-package /asset-output/function.zip", + ], + image: Runtime.DOTNET_6.bundlingImage, + user: "root", + outputType: BundlingOutput.ARCHIVED, + }, + }); + }, + }, + { + ...BASE_APP_FUNCTION, + name: "AnalyzeSentiment", + handler: "FsaAnalyzeSentiment::FsaAnalyzeSentiment.AnalyzeSentimentFunction::FunctionHandler", + runtime: Runtime.DOTNET_6, + codeAsset() { + const source = resolve( + "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer" + ); + return Code.fromAsset(source, { + bundling: { + command: [ + "/bin/sh", + "-c", + " dotnet tool install -g Amazon.Lambda.Tools" + + " && dotnet build" + + " && cd FsaAnalyzeSentiment" + + " && dotnet lambda package --output-package /asset-output/function.zip", + ], + image: Runtime.DOTNET_6.bundlingImage, + user: "root", + outputType: BundlingOutput.ARCHIVED, + }, + }); + }, + }, + { + ...BASE_APP_FUNCTION, + name: "TranslateText", + handler: "FsaTranslateText::FsaTranslateText.TranslateTextFunction::FunctionHandler", + runtime: Runtime.DOTNET_6, + codeAsset() { + const source = resolve( + "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer" + ); + return Code.fromAsset(source, { + bundling: { + command: [ + "/bin/sh", + "-c", + " dotnet tool install -g Amazon.Lambda.Tools" + + " && dotnet build" + + " && cd FsaTranslateText" + + " && dotnet lambda package --output-package /asset-output/function.zip", + ], + image: Runtime.DOTNET_6.bundlingImage, + user: "root", + outputType: BundlingOutput.ARCHIVED, + }, + }); + }, + }, + { + ...BASE_APP_FUNCTION, + name: "SynthesizeAudio", + handler: "FsaSynthesizeAudio::FsaSynthesizeAudio.SynthesizeAudioFunction::FunctionHandler", + runtime: Runtime.DOTNET_6, + codeAsset() { + const source = resolve( + "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer" + ); + return Code.fromAsset(source, { + bundling: { + command: [ + "/bin/sh", + "-c", + " dotnet tool install -g Amazon.Lambda.Tools" + + " && dotnet build" + + " && cd FsaSynthesizeAudio" + + " && dotnet lambda package --output-package /asset-output/function.zip", + ], + image: Runtime.DOTNET_6.bundlingImage, + user: "root", + outputType: BundlingOutput.ARCHIVED, + }, + }); + }, + }, +]; + const FUNCTIONS: Record = { examplelang: EXAMPLE_LANG_FUNCTIONS, ruby: RUBY_FUNCTIONS, java: JAVA_FUNCTIONS, javascript: JAVASCRIPT_FUNCTIONS, + dotnet: DOTNET_FUNCTIONS, }; export function getFunctions(language: string = ""): AppFunctionConfig[] { return FUNCTIONS[language] ?? FUNCTIONS.examplelang; diff --git a/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts b/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts index eca72660e61..8d6aeba2175 100644 --- a/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts +++ b/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts @@ -103,6 +103,7 @@ export class AppStack extends Stack { "image/png", "binary/octet-stream", "audio/mp3", + "audio/mpeg", ], cloudWatchRole: true, deployOptions: { diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln new file mode 100644 index 00000000000..64b38fcadfc --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln @@ -0,0 +1,55 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33927.249 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaExtractText", "FsaExtractText\FsaExtractText.csproj", "{C0BB9701-7EE8-4A91-B1EB-B3888ED43079}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaAnalyzeSentiment", "FsaAnalyzeSentiment\FsaAnalyzeSentiment.csproj", "{9B31C723-D4B0-4953-B5B1-9002E7181DEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaTranslateText", "FsaTranslateText\FsaTranslateText.csproj", "{9886D38A-541E-4BF5-83CE-6CA85088028B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaSynthesizeAudio", "FsaSynthesizeAudio\FsaSynthesizeAudio.csproj", "{4613C012-D517-4C57-98A3-F59B727889EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsaServices", "FsaServices\FsaServices.csproj", "{DA3393CC-DBBF-444E-947C-FBDC4BA66C55}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsaServicesTest", "FsaServicesTest\FsaServicesTest.csproj", "{3A3503C4-9077-4BB3-90C9-08A2B8ED798D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Release|Any CPU.Build.0 = Release|Any CPU + {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Release|Any CPU.Build.0 = Release|Any CPU + {9886D38A-541E-4BF5-83CE-6CA85088028B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9886D38A-541E-4BF5-83CE-6CA85088028B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9886D38A-541E-4BF5-83CE-6CA85088028B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9886D38A-541E-4BF5-83CE-6CA85088028B}.Release|Any CPU.Build.0 = Release|Any CPU + {4613C012-D517-4C57-98A3-F59B727889EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4613C012-D517-4C57-98A3-F59B727889EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4613C012-D517-4C57-98A3-F59B727889EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4613C012-D517-4C57-98A3-F59B727889EE}.Release|Any CPU.Build.0 = Release|Any CPU + {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Release|Any CPU.Build.0 = Release|Any CPU + {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C7E557C4-6C58-4B38-8C84-D71A72BD3F3C} + EndGlobalSection +EndGlobal diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs new file mode 100644 index 00000000000..e80b999ae5c --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Comprehend; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Logging; +using FsaServices.Models; +using FsaServices.Services; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace FsaAnalyzeSentiment; + +/// +/// Function to handle analysis of sentiment. +/// +public class AnalyzeSentimentFunction +{ + private readonly SentimentService _sentimentService; + + /// + /// Default constructor. This constructor is used by AWS Lambda to construct the instance. When invoked in a Lambda environment, + /// the AWS credentials will come from the AWS Identity and Access Management (IAM) role associated with the function. The AWS Region will be set to the + /// Region the Lambda function is executed in. + /// + public AnalyzeSentimentFunction() + { + _sentimentService = new SentimentService(new AmazonComprehendClient()); + } + + /// + /// Constructs an instance with a preconfigured Comprehend client. This can be used for testing outside of the Lambda environment. + /// + /// + public AnalyzeSentimentFunction(IAmazonComprehend comprehendClient) + { + this._sentimentService = new SentimentService(comprehendClient); + } + + /// + /// A function that takes in the source text and returns the sentiment details. + /// + /// The extracted text output to analyze. + /// The Lambda context + /// Sentiment details. + public async Task FunctionHandler(SourceTextDetails extractTextOutput, ILambdaContext context) + { + // Log the object with Lambda PowerTools logger. + Logger.LogInformation(extractTextOutput); + var result = await _sentimentService.AnalyzeTextSentiment(extractTextOutput.source_text); + Logger.LogInformation(result); + return result; + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj new file mode 100644 index 00000000000..49d10eefa6e --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj @@ -0,0 +1,21 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json new file mode 100644 index 00000000000..a82d4042b00 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + } + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json new file mode 100644 index 00000000000..73bd2a14f0e --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-east-1", + "configuration": "Release", + "function-runtime": "dotnet6", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "FsaAnalyzeSentiment::FsaAnalyzeSentiment.AnalyzeSentimentFunction::FunctionHandler" +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs new file mode 100644 index 00000000000..611d38cc001 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json; +using Amazon.Lambda.Core; +using Amazon.Textract; +using AWS.Lambda.Powertools.Logging; +using FsaServices.Models; +using FsaServices.Services; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace FsaExtractText; + +/// +/// Function to handle extracting the text from an image. +/// +public class ExtractTextFunction +{ + private readonly ExtractionService _extractionService; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS Region will be set to the + /// Region the Lambda function is executed in. + /// + public ExtractTextFunction() + { + var textractClient = new AmazonTextractClient(); + _extractionService = new ExtractionService(textractClient); + } + + /// + /// Constructs an instance with an Amazon Textract client. This can be used for testing outside of the Lambda environment. + /// + /// Preconfigured Textract client. + public ExtractTextFunction(IAmazonTextract textractClient) + { + _extractionService = new ExtractionService(textractClient); + } + + /// + /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used + /// to respond to S3 notifications. + /// + /// The CloudWatch S3 Event. + /// The Lambda context. + /// The extracted words as a single string. + public async Task FunctionHandler(CardObjectCreated evnt, ILambdaContext context) + { + // Log the event with Lambda PowerTools logger. + var s3Event = evnt; + Logger.LogInformation(evnt); + + var extractResponse = await _extractionService.ExtractWordsFromBucketObject(s3Event.Bucket, s3Event.Object); + Logger.LogInformation($"Extracted text: {extractResponse}."); + return extractResponse; + + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj new file mode 100644 index 00000000000..0d71581a032 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj @@ -0,0 +1,25 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json new file mode 100644 index 00000000000..a82d4042b00 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + } + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json new file mode 100644 index 00000000000..719aa730e98 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-east-1", + "configuration": "Release", + "function-runtime": "dotnet6", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "FsaExtractText::FsaExtractText.ExtractTextFunction::FunctionHandler" +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj new file mode 100644 index 00000000000..01aa8746a36 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs new file mode 100644 index 00000000000..221ea6aad71 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace FsaServices.Models; + +/// +/// Model class for audio source and destination details. +/// +public class AudioSourceDestinationDetails +{ + /// + /// The bucket to use for output. + /// + public string bucket { get; set; } = null!; + + /// + /// The translated text to synthesize. + /// + public string translated_text { get; set; } = null!; + + /// + /// The output object key. + /// + public string Object { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs new file mode 100644 index 00000000000..f427ed6ec98 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace FsaServices.Models; + +/// +/// Model for card object created event. +/// +public class CardObjectCreated +{ + /// + /// The region for the card object. + /// + public string Region { get; set; } = null!; + + /// + /// The bucket for the card object. + /// + public string Bucket { get; set; } = null!; + + /// + /// The object key for the card. + /// + public string Object { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs new file mode 100644 index 00000000000..988475cac9d --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Comprehend; + +namespace FsaServices.Models; + +/// +/// The details of the analyzed sentiment. +/// +public class SentimentDetails +{ + /// + /// The sentiment type of the analyzed text. + /// + public string sentiment { get; set; } = null!; + + /// + /// The language code of the analyzed text. + /// + public string language_code { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs new file mode 100644 index 00000000000..59f62507c57 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace FsaServices.Models; + +/// +/// Model containing source text details. +/// +public class SourceTextDetails +{ + /// + /// The source text. + /// + public string source_text { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs new file mode 100644 index 00000000000..471dfa2fb19 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace FsaServices.Models; + +/// +/// The text and a source language code. +/// +public class TextWithSourceLanguage +{ + /// + /// The extracted text. + /// + public string extracted_text { get; set; } = null!; + + /// + /// The source language code. + /// + public string source_language_code { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs new file mode 100644 index 00000000000..0c4c0edde5b --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace FsaServices.Models; + +/// +/// Model containing translated text details. +/// +public class TranslatedTextDetails +{ + /// + /// The translated text. + /// + public string translated_text { get; set; } = null!; +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs new file mode 100644 index 00000000000..089d63cfd7a --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Textract; +using Amazon.Textract.Model; + +namespace FsaServices.Services; + +/// +/// Service to handle extracting text from images. +/// +public class ExtractionService +{ + private readonly IAmazonTextract _amazonTextract; + + /// + /// Constructor that uses the injected Amazon Textract client. + /// + /// Amazon Textract client. + public ExtractionService(IAmazonTextract amazonTextract) + { + _amazonTextract = amazonTextract; + } + + /// + /// Extract the words from a given bucket object and return them in a single string. + /// + /// The source bucket. + /// The key of the bucket object. + /// Words as a single string. + public async Task ExtractWordsFromBucketObject(string bucket, string name) + { + var detectTextResponse = await _amazonTextract.DetectDocumentTextAsync( + new DetectDocumentTextRequest() + { + Document = new Document() + { + S3Object = new S3Object() { Bucket = bucket, Name = name } + } + }); + + var words = detectTextResponse.Blocks + .Where(b => b.BlockType == BlockType.WORD) + .Select(w => w.Text); + + return string.Join(' ', words); + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs new file mode 100644 index 00000000000..9d2737bd74f --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Comprehend; +using Amazon.Comprehend.Model; +using FsaServices.Models; + +namespace FsaServices.Services; + +/// +/// Service to handle analyzing sentiment from text. +/// +public class SentimentService +{ + private readonly IAmazonComprehend _amazonComprehend; + + /// + /// Constructor that uses the injected Amazon Comprehend client. + /// + /// Amazon Comprehend client. + public SentimentService(IAmazonComprehend amazonComprehend) + { + _amazonComprehend = amazonComprehend; + } + + /// + /// Analyze and return the sentiment for the source text. + /// + /// The text to analyze. + /// The sentiment details. + public async Task AnalyzeTextSentiment(string sourceText) + { + if (string.IsNullOrEmpty(sourceText)) + { + throw new InvalidOperationException( + "Cannot detect sentiment for an empty string."); + } + + var languages = await _amazonComprehend.DetectDominantLanguageAsync( + new DetectDominantLanguageRequest() + { + Text = sourceText + }); + var firstLanguage = languages.Languages[0].LanguageCode; + + var sentiment = await _amazonComprehend.DetectSentimentAsync( + new DetectSentimentRequest() + { + LanguageCode = firstLanguage, + Text = sourceText + }); + + return new SentimentDetails() + { + language_code = firstLanguage, + sentiment = sentiment.Sentiment.Value + }; + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs new file mode 100644 index 00000000000..3dfd01fdd05 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Polly; +using Amazon.Polly.Model; +using Amazon.Runtime.Internal; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; +using FsaServices.Models; + +namespace FsaServices.Services; + +/// +/// Service to handle synthesizing audio from text. +/// +public class SynthesizeService +{ + private readonly IAmazonPolly _amazonPolly; + private readonly IAmazonS3 _amazonS3; + + /// + /// Constructor that uses the injected Amazon Polly and S3 clients. + /// + /// Amazon Polly client. + /// Amazon S3 client. + public SynthesizeService(IAmazonPolly amazonPolly, IAmazonS3 amazonS3) + { + _amazonPolly = amazonPolly; + _amazonS3 = amazonS3; + } + + /// + /// Extract the words from a given bucket object and return them in a single string. + /// + /// The source destination bucket, text, and object ke.y + /// The name of the result object in the bucket. + public async Task SynthesizeSpeechFromText(AudioSourceDestinationDetails sourceDestinationDetails) + { + if (string.IsNullOrEmpty(sourceDestinationDetails.translated_text)) + { + throw new InvalidOperationException( + "Cannot synthesize audio for an empty string."); + } + + var synthesizedResponse = await _amazonPolly.SynthesizeSpeechAsync( + new SynthesizeSpeechRequest() + { + Engine = Engine.Neural, + Text = sourceDestinationDetails.translated_text, + VoiceId = VoiceId.Ruth, + OutputFormat = OutputFormat.Mp3 + }); + + var audioKey = $"{sourceDestinationDetails.Object}.mp3"; + + var transfer = new TransferUtility(_amazonS3); + var uploadStreamRequest = new TransferUtilityUploadRequest + { + BucketName = sourceDestinationDetails.bucket, + Key = audioKey, + InputStream = synthesizedResponse.AudioStream, + ContentType = "audio/mpeg", + }; + + await transfer.UploadAsync(uploadStreamRequest); + + return audioKey; + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs new file mode 100644 index 00000000000..e5b1338a305 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Translate; +using Amazon.Translate.Model; + +namespace FsaServices.Services; + +/// +/// Service to handle translating text to English. +/// +public class TranslationService +{ + private readonly IAmazonTranslate _amazonTranslate; + + /// + /// Constructor that uses the injected Amazon Translate client. + /// + /// Amazon Translate client. + public TranslationService(IAmazonTranslate amazonTranslate) + { + _amazonTranslate = amazonTranslate; + } + + /// + /// Translate a string to the specified target language. + /// + /// The source text to translate. + /// The language code of the source text. + /// The translated string. + public async Task TranslateToEnglish(string sourceText, string languageCode) + { + if (string.IsNullOrEmpty(sourceText)) + { + throw new InvalidOperationException( + "Cannot translate an empty string."); + } + + var translateResponse = await _amazonTranslate.TranslateTextAsync( + new TranslateTextRequest() + { + Text = sourceText, + SourceLanguageCode = languageCode, + TargetLanguageCode = "en" + }); + + return translateResponse.TranslatedText; + } + +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs new file mode 100644 index 00000000000..d01b3c10f11 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Textract; +using Amazon.Textract.Model; +using FsaServices.Services; +using Moq; + +namespace FsaServicesTest; + +/// +/// Tests for the FSA ExtractionService. +/// +public class ExtractionServiceTests +{ + /// + /// Verify that extracting words from an object returns a string. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task SynthesizeSpeech_ShouldReturnObjectKey() + { + // Arrange. + var mockService = new Mock(); + var detectDocumentTextResponse = new DetectDocumentTextResponse() + { + Blocks = new List() + { + new(){BlockType = BlockType.WORD, Text = "This"}, + new(){BlockType = BlockType.WORD, Text = "is"}, + new(){BlockType = BlockType.WORD, Text = "the"}, + new(){BlockType = BlockType.WORD, Text = "text"}, + new(){BlockType = BlockType.LINE} + } + }; + + _ = mockService.Setup(ms => + ms.DetectDocumentTextAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(detectDocumentTextResponse); + + var service = new ExtractionService(mockService.Object); + + // Act. + var words = await service.ExtractWordsFromBucketObject("testBucket", "testName"); + + // Assert. + Assert.Equal("This is the text", words); + } + + /// + /// Verify that no words in the extraction returns an empty string. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task ExtractNoWords_ShouldReturnEmptyString() + { + // Arrange. + var mockService = new Mock(); + var detectDocumentTextResponse = new DetectDocumentTextResponse() + { + Blocks = new List() + { + new(){BlockType = BlockType.LINE} + } + }; + + _ = mockService.Setup(ms => + ms.DetectDocumentTextAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(detectDocumentTextResponse); + + var service = new ExtractionService(mockService.Object); + + // Act. + var words = await service.ExtractWordsFromBucketObject("testBucket", "testName"); + + // Assert. + Assert.Equal("", words); + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj new file mode 100644 index 00000000000..55024b58cfa --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs new file mode 100644 index 00000000000..faabcb241b6 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs @@ -0,0 +1,88 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Comprehend; +using Amazon.Comprehend.Model; +using FsaServices.Services; +using Moq; + +namespace FsaServicesTest; + +/// +/// Tests for the FSA SentimentService. +/// +public class SentimentServiceTests +{ + /// + /// Verify that translating to English returns a string. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task DetectSentiment_ShouldReturnSentimentDetails() + { + // Arrange. + var mockService = new Mock(); + var dominantLanguageResponse = new DetectDominantLanguageResponse() + { + Languages = new List() { new() { LanguageCode = "en" } } + }; + + _ = mockService.Setup(ms => + ms.DetectDominantLanguageAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(dominantLanguageResponse); + + var detectSentimentResponse = new DetectSentimentResponse() + { + Sentiment = SentimentType.POSITIVE + }; + + _ = mockService.Setup(ms => + ms.DetectSentimentAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(detectSentimentResponse); + + var service = new SentimentService(mockService.Object); + + // Act. + var sentimentDetails = await service.AnalyzeTextSentiment("Text to analyze."); + + // Assert. + Assert.Equal("en", sentimentDetails.language_code); + Assert.Equal(SentimentType.POSITIVE.Value, sentimentDetails.sentiment); + } + + /// + /// Verify that an empty string throws an Invalid Operation Exception. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task AnalyzeEmptyString_ShouldThrowException() + { + // Arrange. + var mockService = new Mock(); + var dominantLanguageResponse = new DetectDominantLanguageResponse() + { + Languages = new List() { new() { LanguageCode = "en" } } + }; + + _ = mockService.Setup(ms => + ms.DetectDominantLanguageAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(dominantLanguageResponse); + + var detectSentimentResponse = new DetectSentimentResponse() + { + Sentiment = SentimentType.POSITIVE + }; + + _ = mockService.Setup(ms => + ms.DetectSentimentAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(detectSentimentResponse); + + var service = new SentimentService(mockService.Object); + + // Act and Assert. + await Assert.ThrowsAsync(async () => + { + await service.AnalyzeTextSentiment(""); + }); + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs new file mode 100644 index 00000000000..d96f3ae9867 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs @@ -0,0 +1,85 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Polly; +using Amazon.Polly.Model; +using Amazon.S3; +using FsaServices.Models; +using FsaServices.Services; +using Moq; + +namespace FsaServicesTest; + +/// +/// Tests for the FSA SynthesizeService. +/// +public class SynthesizeServiceTests +{ + /// + /// Verify that translating to English returns a string. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task SynthesizeSpeech_ShouldReturnObjectKey() + { + // Arrange. + var mockService = new Mock(); + var synthesizeSpeechResponse = new SynthesizeSpeechResponse() + { + AudioStream = new MemoryStream(), + }; + + _ = mockService.Setup(ms => + ms.SynthesizeSpeechAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(synthesizeSpeechResponse); + + var mockS3Service = new Mock(); + + var service = new SynthesizeService(mockService.Object, mockS3Service.Object); + + // Act. + var audioKey = await service.SynthesizeSpeechFromText(new AudioSourceDestinationDetails() + { + bucket = "BucketName", + Object = "ObjectKey.jpg", + translated_text = "Text to synthesize." + }); + + // Assert. + Assert.Equal("ObjectKey.jpg.mp3", audioKey); + } + + /// + /// Verify that an empty string throws an Invalid Operation Exception. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task AnalyzeEmptyString_ShouldThrowException() + { + // Arrange. + var mockService = new Mock(); + var synthesizeSpeechResponse = new SynthesizeSpeechResponse() + { + AudioStream = new MemoryStream(), + }; + + _ = mockService.Setup(ms => + ms.SynthesizeSpeechAsync(It.IsAny(), + CancellationToken.None)).ReturnsAsync(synthesizeSpeechResponse); + + var mockS3Service = new Mock(); + + var service = new SynthesizeService(mockService.Object, mockS3Service.Object); + + // Act and Assert. + await Assert.ThrowsAsync(async () => + { + await service.SynthesizeSpeechFromText(new AudioSourceDestinationDetails() + { + bucket = "BucketName", + Object = "ObjectKey.jpg", + translated_text = "" + }); + }); + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs new file mode 100644 index 00000000000..cc3378899a9 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Translate; +using Amazon.Translate.Model; +using FsaServices.Services; +using Moq; + +namespace FsaServicesTest; + +/// +/// Tests for the FSA TranslationService. +/// +public class TranslationServiceTests +{ + /// + /// Verify that translating to English returns a string. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task TranslateToEnglish_ShouldReturnString() + { + // Arrange. + var mockService = new Mock(); + var responseText = "Text in English."; + var response = new TranslateTextResponse { TranslatedText = responseText }; + + _ = mockService.Setup(ms => + ms.TranslateTextAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(response); + + var service = new TranslationService(mockService.Object); + + // Act. + var translation = await service.TranslateToEnglish("Text in French.", "fr"); + + // Assert. + Assert.Equal(responseText, translation); + } + + /// + /// Verify that an empty string throws an Invalid Operation Exception. + /// + [Fact] + [Trait("Category", "Unit")] + public async Task TranslateEmptyString_ShouldThrowException() + { + // Arrange. + var mockService = new Mock(); + var responseText = String.Empty; + var response = new TranslateTextResponse { TranslatedText = responseText }; + + _ = mockService.Setup(ms => + ms.TranslateTextAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(response); + + var service = new TranslationService(mockService.Object); + + // Act and Assert. + await Assert.ThrowsAsync(async () => + { + await service.TranslateToEnglish("", "fr"); + }); + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs new file mode 100644 index 00000000000..92fe41799a7 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using Xunit; \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj new file mode 100644 index 00000000000..3d577fdd16b --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj @@ -0,0 +1,21 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json new file mode 100644 index 00000000000..a82d4042b00 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + } + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs new file mode 100644 index 00000000000..0256f173458 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Core; +using Amazon.Polly; +using Amazon.S3; +using AWS.Lambda.Powertools.Logging; +using FsaServices.Models; +using FsaServices.Services; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace FsaSynthesizeAudio; + +/// +/// Function to handle synthesizing audio. +/// +public class SynthesizeAudioFunction +{ + private readonly SynthesizeService _synthesizeService; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public SynthesizeAudioFunction() + { + var pollyClient = new AmazonPollyClient(); + var s3Client = new AmazonS3Client(); + _synthesizeService = new SynthesizeService(pollyClient, s3Client); + } + + /// + /// Constructs an instance with an Amazon Textract client. This can be used for testing outside of the Lambda environment. + /// + /// + public SynthesizeAudioFunction(IAmazonPolly pollyClient, IAmazonS3 s3Client) + { + _synthesizeService = new SynthesizeService(pollyClient, s3Client); + } + + + /// + /// Takes in the audio source and destination details, and returns the key of the new media object after synthesis. + /// + /// The source and destination details input. + /// The Lambda context. + /// The key of the new media object. + public async Task FunctionHandler(AudioSourceDestinationDetails input, ILambdaContext context) + { + // Log the input with Lambda PowerTools logger. + Logger.LogInformation(input); + var synthesizeResponse = await _synthesizeService.SynthesizeSpeechFromText(input); + Logger.LogInformation(synthesizeResponse); + return synthesizeResponse; + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json new file mode 100644 index 00000000000..bc0309cab5b --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-east-1", + "configuration": "Release", + "function-runtime": "dotnet6", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "FsaSynthesizeAudio::FsaSynthesizeAudio.SynthesizeAudioFunction::FunctionHandler" +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj new file mode 100644 index 00000000000..3d577fdd16b --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj @@ -0,0 +1,21 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json new file mode 100644 index 00000000000..a82d4042b00 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + } + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs new file mode 100644 index 00000000000..0221e7cf4d9 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Core; +using Amazon.Translate; +using AWS.Lambda.Powertools.Logging; +using FsaServices.Models; +using FsaServices.Services; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace FsaTranslateText; + +/// +/// Function to handle translating the extracted text to English. +/// +public class TranslateTextFunction +{ + private readonly TranslationService _translationService; + + /// + /// Default constructor. This constructor is used by AWS Lambda to construct the instance. When invoked in a Lambda environment, + /// the AWS credentials will come from the AWS Identity and Access Management (IAM) role associated with the function. The AWS Region will be set to the + /// Region the Lambda function is executed in. + /// + public TranslateTextFunction() + { + _translationService = new TranslationService(new AmazonTranslateClient()); + } + + /// + /// Constructs an instance with a preconfigured Translate client. This can be used for testing outside of the Lambda environment. + /// + /// + public TranslateTextFunction(IAmazonTranslate translateClient) + { + this._translationService = new TranslationService(translateClient); + } + + /// + /// A function that takes in the source text and returns the sentiment details. + /// + /// The text to analyze. + /// The Lambda context + /// Sentiment details. + public async Task FunctionHandler(TextWithSourceLanguage source_text, ILambdaContext context) + { + // Log the object with Lambda PowerTools logger. + Logger.LogInformation(source_text); + var translatedText = await _translationService.TranslateToEnglish(source_text.extracted_text, + source_text.source_language_code); + Logger.LogInformation(translatedText); + return new TranslatedTextDetails() { translated_text = translatedText }; + } +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json new file mode 100644 index 00000000000..690dec7aef0 --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-east-1", + "configuration": "Release", + "function-runtime": "dotnet6", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "FsaTranslateText::FsaTranslateText.TranslateTextFunction::FunctionHandler" +} \ No newline at end of file diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md new file mode 100644 index 00000000000..b83dc81f71b --- /dev/null +++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md @@ -0,0 +1,22 @@ +# Create Feedback Sentiment Analyzer (FSA) using the SDK for .NET (v3) + +## Overview + +This document discusses the language-specific nuances of deploying the Feedback Sentiment Analyzer (FSA) application. For more details, see the application [README.md](../../../applications/feedback_sentiment_analyzer/README.md). + +## Prerequisites + +- Follow the main [README](../../README.md#Prerequisites) in the `dotnetv3` folder +- Install the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) + +### 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 the resources you create while going through this tutorial so that you won't be charged. + +### .NET Implementation Details + - This example includes AWS Lambda functions for the various operations of the Feedback Sentiment Analyzer. + - Each function was created using the AWS Lambda Template from the [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). + - Each function also uses [Powertools for AWS Lamba (.NET)](https://github.com/aws-powertools/powertools-lambda-dotnet) for enhanced logging. \ No newline at end of file diff --git a/dotnetv3/cross-service/README.md b/dotnetv3/cross-service/README.md index 665cb21d6d9..30e8d8e6de4 100644 --- a/dotnetv3/cross-service/README.md +++ b/dotnetv3/cross-service/README.md @@ -15,7 +15,25 @@ using the AWS SDK for .NET. ## Cross-service examples -### [Serverless photo asset management application](PhotoAssetManager/Readme.md) +### [Feedback Sentiment Analyzer application](FeedbackSentimentAnalyzer/README.md) + +Create a Feedback Sentiment Analyzer (FSA) example app that 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. + +The application uses the following services: + +- Amazon Textract +- Amazon Translate +- Amazon Comprehend +- Amazon Simple Storage Service (Amazon S3) +- Amazon Polly +- AWS Lambda +- Amazon Cognito +- Amazon API Gateway + +### [Serverless photo asset management application](PhotoAssetManager/README.md) Create a Photo Asset Management (PAM) example app that uses Amazon Rekognition to categorize images, which are stored with Amazon S3 Intelligent-Tiering for cost savings. Users can upload new images. Those images are analyzed with label detection and the labels are stored in an Amazon DynamoDB table. Users can later request a bundle of images matching those labels. When images are requested, they are retrieved from Amazon S3, zipped, and the user is sent a link to the zip.