├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── deployment ├── build-s3-dist.sh ├── content-localization-on-aws-auth.yaml ├── content-localization-on-aws-opensearch.yaml ├── content-localization-on-aws-use-existing-mie-stack.yaml ├── content-localization-on-aws-video-workflow.yaml ├── content-localization-on-aws-web.yaml ├── content-localization-on-aws.yaml ├── run-unit-tests.sh └── sync-s3-dist.sh ├── doc ├── diagrams │ ├── ContentLocalizationDiagrams.xml │ ├── MIE-architecture.xml │ └── TranslatorDiagram.xml ├── images │ ├── ContentLocalizationArchitectureOverview.png │ ├── MIE-execute-workflow-architecture.png │ ├── MIEDemo.gif │ ├── collection-search.png │ ├── create_user01.png │ ├── create_user02.png │ ├── create_user03.png │ ├── ig-architecture.png │ ├── ig-collection-page.png │ ├── ig-cost-custom-table1.png │ ├── ig-cost-customization-table2.png │ ├── ig-cost-customization.png │ ├── ig-create-user.png │ ├── ig-edit-vocabulary.png │ ├── ig-find-the-url.png │ ├── ig-full-text-search-speech.png │ ├── ig-homepage.png │ ├── ig-opensearch-consumer.png │ ├── ig-operator-categories.png │ ├── ig-playhead-search.png │ ├── ig-search-box.png │ ├── ig-terminology.png │ ├── ig-upload-configure.png │ ├── ig-upload-video.png │ ├── ig-user-to-group.png │ ├── ig-view-subtitles.png │ ├── ig-view-translation.png │ ├── ig-webapp.png │ ├── ig-webappauth.png │ ├── ig-workflow.png │ ├── kibana-create-index.png │ ├── launch-stack.png │ ├── screenshot-analytics.png │ ├── screenshot-uploads.png │ ├── search.png │ ├── upload-workflow-status.png │ ├── workflow-analysis-results.png │ └── workflow-error-step-fn.png └── sigv4_post_sample.py ├── source ├── anonymized-data-logger │ ├── anonymized-data-logger.py │ ├── lib │ │ ├── cfnresponse.py │ │ └── metrics.py │ └── requirements.txt ├── consumer │ ├── lambda_handler.py │ └── requirements.txt ├── helper │ ├── requirements.txt │ └── website_helper.py └── website │ ├── eslint.config.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── img │ │ └── icons │ │ │ └── favicon-16x16.png │ ├── manifest.json │ ├── robots.txt │ └── runtimeConfig.json │ ├── src │ ├── App.vue │ ├── components │ │ ├── Celebrities.vue │ │ ├── ComponentLoadingError.vue │ │ ├── ComprehendEntities.vue │ │ ├── ComprehendKeyPhrases.vue │ │ ├── ContentModeration.vue │ │ ├── FaceDetection.vue │ │ ├── HeaderView.vue │ │ ├── ImageFeature.vue │ │ ├── LabelObjects.vue │ │ ├── LineChart.vue │ │ ├── Loading.vue │ │ ├── MediaSummaryBox.vue │ │ ├── ShotDetection.vue │ │ ├── Subtitles.vue │ │ ├── TechnicalCues.vue │ │ ├── TextDetection.vue │ │ ├── Transcript.vue │ │ ├── Translation.vue │ │ ├── VideoPlayer.vue │ │ ├── VideoThumbnail.vue │ │ ├── VoerroTagsInput.css │ │ ├── VoerroTagsInput.vue │ │ ├── Waveform.vue │ │ └── vue-dropzone.vue │ ├── main.js │ ├── router.js │ ├── services │ │ └── urlsigner.js │ ├── static │ │ └── favicon.ico │ ├── store │ │ ├── actions.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ └── views │ │ ├── Analysis.vue │ │ ├── Collection.vue │ │ ├── Login.vue │ │ └── UploadToAWSS3.vue │ └── vite.config.js └── test ├── README.md ├── e2e ├── conftest.py ├── requirements.txt ├── run_e2e.sh ├── test_app.py └── test_workflow_reprocess.py ├── test-media ├── sample-audio.m4a ├── sample-data.json ├── sample-face.jpg ├── sample-image.jpg ├── sample-text.txt ├── sample-video-tiny.mp4 ├── sample-video.mp4 └── uitestvocabulary └── unit ├── anonymous-data-logger ├── conftest.py ├── lib │ ├── test_cfnresponse.py │ └── test_metrics.py └── test_anonymous-data-logger.py ├── consumer ├── conftest.py ├── operators │ ├── ContentModeration.json │ ├── GenericDataLookup.json │ ├── Mediainfo.json │ ├── TranscribeAudio.json │ ├── TranscribeVideo.json │ ├── Translate.json │ ├── WebCaptions_en.json │ ├── WebCaptions_es.json │ ├── celebrityRecognition.json │ ├── entities.json │ ├── faceDetection.json │ ├── face_search.json │ ├── key_phrases.json │ ├── labelDetection.json.gz │ ├── shotDetection.json │ ├── technicalCueDetection.json │ └── textDetection.json └── test_lambda_handler.py ├── helper ├── conftest.py └── test_website_helper.py ├── requirements.txt └── run_unit.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | **Describe the bug** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Please complete the following information about the solution:** 21 | 22 | - [ ] Version: [e.g. v1.0.0] 23 | 24 | To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0021) - Video On Demand workflow with AWS Step Functions, MediaConvert, MediaPackage, S3, CloudFront and DynamoDB. Version **v5.0.0**_". If the description does not contain the version information, you can look at the mappings section of the template: 25 | 26 | ```yaml 27 | Mappings: 28 | SourceCode: 29 | General: 30 | S3Bucket: "solutions" 31 | KeyPrefix: "video-on-demand-on-aws/v5.0.0" 32 | ``` 33 | 34 | - [ ] Region: [e.g. us-east-1] 35 | 36 | - [ ] Was the solution modified from the version published on this repository? 37 | 38 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 39 | 40 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? 41 | 42 | - [ ] Were there any errors in the CloudWatch Logs? 43 | 44 | **Screenshots** 45 | 46 | If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). 47 | 48 | **Additional context** 49 | 50 | Add any other context about the problem here. 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | **Is your feature request related to a problem? Please describe.** 9 | 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the feature you'd like** 13 | 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .idea/* 3 | deployment/regional-s3-assets 4 | deployment/global-s3-assets 5 | deployment/template_url_that_deploys_mie_as_nested_stack.txt 6 | deployment/template_url_that_uses_an_existing_mie_stack.txt 7 | *node_modules 8 | *package 9 | *dist 10 | webapp-manifest.json 11 | source/website/public/runtimeConfig.json 12 | *.iml 13 | .idea/ 14 | *.pyc 15 | test/coverage-reports 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | TODO: Update release date when time is known 9 | 10 | ## [2.1.14] - 2025-06-04 11 | 12 | ### Security 13 | 14 | - Updated `Media Insights on AWS` version to v5.1.11 15 | 16 | ## [2.1.13] - 2025-04-04 17 | 18 | ### Security 19 | 20 | - Upgraded vulnerable packages 21 | 22 | ## [2.1.12] - 2025-03-17 23 | 24 | ### Security 25 | 26 | - Upgraded vulnerable packages 27 | - Updated `Media Insights on AWS` version to v5.1.10 28 | 29 | ## [2.1.11] - 2024-12-23 30 | 31 | ### Changed 32 | 33 | - Changed build tool from vue-loader to vite-js 34 | 35 | ### Fixed 36 | 37 | - Fixed an issue in Analysis page graph where it failed to update upon analysis context change 38 | 39 | ### Security 40 | 41 | - Upgraded vulnerable packages 42 | 43 | ### Removed 44 | 45 | - removed unused libraries after migration to vite-js 46 | 47 | ## [2.1.10] - 2024-10-31 48 | 49 | ### Security 50 | 51 | - Upgraded vulnerable packages 52 | 53 | ## [2.1.9] - 2024-10-01 54 | 55 | ### Security 56 | 57 | - Upgraded vulnerable packages 58 | 59 | ## [2.1.8] - 2024-09-17 60 | 61 | ### Security 62 | 63 | - Upgraded vulnerable packages 64 | 65 | ## [2.1.7] - 2024-08-26 66 | 67 | ### Security 68 | 69 | - Upgraded vulnerable packages 70 | 71 | ## [2.1.6] - 2024-07-02 72 | 73 | ### Security 74 | 75 | - Upgraded vulnerable packages 76 | 77 | ## [2.1.5] - 2024-06-11 78 | 79 | ### Changed 80 | 81 | - Updated `Media Insights on AWS` version to v5.1.7 82 | 83 | ### Security 84 | 85 | - Upgraded vulnerable packages 86 | 87 | ## [2.1.4] - 2024-03-29 88 | 89 | ### Security 90 | 91 | - Upgraded vulnerable packages 92 | 93 | ## [2.1.3] - 2024-03-20 94 | 95 | ### Changed 96 | 97 | - Added resource dependency on OpensearchServiceDomain 98 | - Upgraded to Media Insights on AWS v5.1.6 99 | - Enabled versioning on website bucket 100 | 101 | ### Security 102 | 103 | - Upgraded vulnerable packages 104 | - Replaced some bootstrap-vue code that was incompatible with Vue 2 to 3 migration build 105 | 106 | ## [2.1.2] - 2023-10-19 107 | 108 | ### Changed 109 | 110 | - Upgraded to Media Insights on AWS v5.1.4 111 | - Consolidated policy statements into one S3 bucket policy for the website bucket 112 | - Updated instructions in README to opt out of metric reporting 113 | - Updated all Lambda functions Python Runtime version to 3.11 114 | 115 | ## [2.1.1] - 2023-07-13 116 | 117 | ### Changed 118 | 119 | - Upgraded vulnerable packages 120 | 121 | ## [2.1.0] - 2023-04-19 122 | 123 | ### Added 124 | 125 | - Added ServiceCatalog AppRegistry resources 126 | - Added unit tests for lambda code written in Python 127 | - Added KMS Key parameter to Auth stack and KMS permissions to Cognito Admin role 128 | 129 | ### Fixed 130 | 131 | - The "Download Data" button on the **Celebrities** tab works now. 132 | - Fixed upload of WebVTT files as "Existing Subtitles". 133 | - Incorrect Cloudformation template in public bucket (#365) 134 | - Updated object ownership configuration on ContentAnalysisWebsiteBucket 135 | 136 | ### Changed 137 | 138 | - Removed Hawkeye scan from github actions (deprecated) 139 | - Upgrade Media Insights on AWS dependency to v5.1.1 140 | - Refactored some code for maintainability 141 | - Updated e2e tests for bug fixes and compatibility with new Media Insights on AWS version 142 | - Updated references to "Media Insights Engine" to "Media Insights on AWS" 143 | - Separated s3 sync commands in build script to a separate script (sync-s3-dist.sh) 144 | 145 | ## [2.0.2] - 2023-01-11 146 | 147 | ### Fixed 148 | 149 | - Updated python packages 150 | - Replaced deprecated python command with python3 151 | 152 | ## [2.0.1] - 2022-08-18 153 | 154 | ### Fixed 155 | 156 | - Version bumped python runtime for web helper lambda function which was preventing successful deployment 157 | 158 | ## [2.0.0] - 2022-03-01 159 | 160 | ### Added 161 | 162 | - Upgrade MIE dependency to v4.0.1 163 | - Add support for using custom language models with Transcribe (#297) 164 | - Document instructions for starting workflows from the command line and from an S3 trigger. (#266) 165 | - Record the state machine ARN for the VideoWorkflow in cloud formation outputs. This makes it easier to find the video workflow in AWS Step Functions. (#268) 166 | - Support the new languages and variants recently added to Transcribe and Translate (#263) 167 | - Add option to see API requests for computer vision results in the front-end (#303) 168 | - Save filenames to Opensearch so assets can be found by searching for their filename (#249) 169 | - Add an option to auto-detect source language (#209) 170 | 171 | ### Fixed 172 | 173 | - Fix missing data in line chart for computer vision results (#303) 174 | - Fix opensearch throttling (#303) 175 | - Remove unused subtitles checkbox from Upload view (#300) 176 | - Avoid showing empty operator configurations in media summary view (#299) 177 | - Fix miscellaneous bugs in the workflow configuration used to save subtitle edits (#286, #289) 178 | - Fix invalid table format that's used when saving custom vocabularies (#260) 179 | - Fix video load error that occurs with large caption data (#239) 180 | - Support filenames with multiple periods (#237) 181 | - Make language selection for Translate behave more intuitively (#228) 182 | - Fix forever spinner that occurs when there is no data (#225) 183 | - Fix the missing red video player position marker (#224) 184 | - Add missing option to download SRT formatted subtitles (#272) 185 | - Fix broken video player for S3 triggered workflows (#271) 186 | - Fix invalid table format that's used when saving custom vocabularies (#260) 187 | - Use the correct source language when saving a new or updated custom vocabulary (#258) 188 | - Fix bug in WebCaptions that occurs when using source language autodetection in Transcribe (#306) 189 | - Removed profanity checker due to insufficient support for non-english languages (#256) 190 | 191 | ## [1.0.0] - 2021-11-03 192 | 193 | ### Added 194 | 195 | - CHANGELOG version 1.0.0 release 196 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-solutions/content-localization-on-aws/issues), or [recently closed](https://github.com/aws-solutions/content-localization-on-aws/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *development* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-solutions/content-localization-on-aws/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](https://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Content Localization on AWS 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except 6 | in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ 7 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the 9 | specific language governing permissions and limitations under the License. 10 | 11 | --- 12 | 13 | THIRD PARTY COMPONENTS 14 | 15 | --- 16 | 17 | This software includes third party software subject to the following copyrights: 18 | 19 | attrs under the Massachusetts Institute of Technology license 20 | aws-amplify under the Apache License Version 2.0 21 | aws-amplify-vue under the Apache License Version 2.0 22 | aws_xray_sdk under the Apache License Version 2.0 23 | axios under the Massachusetts Institute of Technology license 24 | bad-words under the Massachusetts Institute of Technology license 25 | bootstrap under the Massachusetts Institute of Technology license 26 | bootstrap-vue under the Massachusetts Institute of Technology license 27 | boto3 under the Apache License Version 2.0 28 | botocore under the Apache License Version 2.0 29 | cfn-lint under the Massachusetts Institute of Technology license 30 | cfnresponse under the Apache License Version 2.0 31 | chalice under the Apache License Version 2.0 32 | chart.js under the Massachusetts Institute of Technology license 33 | colorama under the BSD license 34 | configparser under the Massachusetts Institute of Technology license 35 | coverage under the Apache License Version 2.0 36 | crayons under the Massachusetts Institute of Technology license 37 | diff under the BSD with attribution (BSD-3) license 38 | dropzone under the Massachusetts Institute of Technology license 39 | elasticsearch-py under the Apache License Version 2.0 40 | exceptiongroup under the Massachusetts Institute of Technology license 41 | h11 under the Massachusetts Institute of Technology license 42 | iniconfig under the Massachusetts Institute of Technology license 43 | jmespath under the Massachusetts Institute of Technology license 44 | jquery under the Massachusetts Institute of Technology license 45 | jwt-decode under the Massachusetts Institute of Technology license 46 | latest-version under the Massachusetts Institute of Technology license 47 | lodash under the Massachusetts Institute of Technology license 48 | number-to-words under the Massachusetts Institute of Technology license 49 | outcome under the Apache License Version 2.0 50 | pluggy under the Massachusetts Institute of Technology license 51 | pytest under the Massachusetts Institute of Technology license 52 | pytest-cov under the Massachusetts Institute of Technology license 53 | python-dateutil under the Apache License Version 2.0 54 | register-service-worker under the Massachusetts Institute of Technology license 55 | requests under the Apache License Version 2.0 56 | requests-aws4auth under the Massachusetts Institute of Technology license 57 | routerjs under the Massachusetts Institute of Technology license 58 | s3transfer under the Apache License Version 2.0 59 | selenium under the Apache License Version 2.0 60 | sniffio under the Apache License Version 2.0 61 | tomli under the Massachusetts Institute of Technology license 62 | trio under the Apache License Version 2.0 63 | trio-websocket under the Massachusetts Institute of Technology license 64 | urllib3 under the Massachusetts Institute of Technology license 65 | video.js under the Apache License Version 2.0 66 | videojs-flash under the Apache License Version 2.0 67 | videojs-hotkeys under the Apache License Version 2.0 68 | videojs-markers under the Massachusetts Institute of Technology license 69 | voerro-vue-tagsinput under the Massachusetts Institute of Technology license 70 | vue under the Massachusetts Institute of Technology license 71 | vue-highlightjs under the Massachusetts Institute of Technology license 72 | vue-router under the Massachusetts Institute of Technology license 73 | vuex under the Massachusetts Institute of Technology license 74 | vuex-persistedstate under the Massachusetts Institute of Technology license 75 | @vue/compat under the Massachusetts Institute of Technology license 76 | @vue/compiler-sfc under the Massachusetts Institute of Technology license 77 | wavesurfer.js under the BSD with attribution (BSD-3) license 78 | webdriver-manager under the Apache License Version 2.0 79 | wsproto under the Massachusetts Institute of Technology license 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, 5 | we will investigate and subsequently address 6 | any potential vulnerabilities as quickly as possible. 7 | If you discover a potential security issue in this project, 8 | please notify AWS/Amazon Security via our 9 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 10 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 11 | Please do *not* create a public GitHub issue in this project. 12 | -------------------------------------------------------------------------------- /deployment/content-localization-on-aws-opensearch.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Content Localization on AWS Opensearch consumer stack %%VERSION%%" 3 | 4 | Parameters: 5 | NodeType: 6 | Description: "The node type to be provisioned for the Opensearch cluster" 7 | Type: String 8 | Default: "t3.small.search" 9 | AllowedValues: 10 | - "t3.small.search" 11 | - "m4.large.search" 12 | - "m4.xlarge.search" 13 | - "c4.large.search" 14 | - "c4.xlarge.search" 15 | - "r4.large.search" 16 | - "r4.xlarge.search" 17 | ConstraintDescription: "must be a valid Opensearch node type." 18 | NodeCount: 19 | Description: "The number of nodes in the Opensearch cluster." 20 | Type: Number 21 | Default: 2 22 | MieDataplaneBucket: 23 | Type: String 24 | Description: "Name of the dataplane bucket" 25 | AnalyticsStreamArn: 26 | Description: "Arn of the Media Insights on AWS kinesis data stream" 27 | Type: String 28 | MieKMSArn: 29 | Description: ARN of the Media Insights KMS Key 30 | Type: String 31 | 32 | Mappings: 33 | SourceCode: 34 | General: 35 | RegionalS3Bucket: '%%REGIONAL_BUCKET_NAME%%' 36 | KeyPrefix: "content-localization-on-aws/%%VERSION%%" 37 | 38 | Resources: 39 | # Opensearch cluster 40 | 41 | # TODO: Best Practice - Resource found with an explicit name, this disallows updates that require replacement of this resource 42 | 43 | OpensearchServiceDomain: 44 | Type: "AWS::OpenSearchService::Domain" 45 | Metadata: 46 | cfn_nag: 47 | rules_to_suppress: 48 | - id: W90 49 | reason: "This resource does not need to access any other resource provisioned within a VPC." 50 | Properties: 51 | EBSOptions: 52 | EBSEnabled: true 53 | Iops: 0 54 | VolumeSize: 10 55 | VolumeType: gp2 56 | EncryptionAtRestOptions: 57 | Enabled: true 58 | NodeToNodeEncryptionOptions: 59 | Enabled: true 60 | ClusterConfig: 61 | DedicatedMasterEnabled: false 62 | InstanceCount: 63 | !Ref NodeCount 64 | ZoneAwarenessEnabled: false 65 | InstanceType: 66 | !Ref NodeType 67 | EngineVersion: Elasticsearch_7.10 68 | SnapshotOptions: 69 | AutomatedSnapshotStartHour: 0 70 | 71 | # open search consumer lambda 72 | 73 | OpensearchConsumerLambda: 74 | Type: "AWS::Lambda::Function" 75 | Metadata: 76 | cfn_nag: 77 | rules_to_suppress: 78 | - id: W89 79 | reason: "This resource does not need to access any other resource provisioned within a VPC." 80 | - id: W92 81 | reason: "This function does not performance optimization, so the default concurrency limits suffice." 82 | Properties: 83 | Handler: "lambda_handler.lambda_handler" 84 | Role: !GetAtt StreamConsumerRole.Arn 85 | Code: 86 | S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "RegionalS3Bucket"], Ref: "AWS::Region"]] 87 | S3Key: 88 | !Join [ 89 | "/", 90 | [ 91 | !FindInMap ["SourceCode", "General", "KeyPrefix"], 92 | "esconsumer.zip", 93 | ], 94 | ] 95 | Runtime: "python3.11" 96 | Timeout: 900 97 | MemorySize: 2048 98 | Environment: 99 | Variables: 100 | EsEndpoint: !GetAtt OpensearchServiceDomain.DomainEndpoint 101 | DataplaneBucket: !Ref MieDataplaneBucket 102 | botoConfig: '{"user_agent_extra": "AwsSolution/SO0164/%%VERSION%%"}' 103 | DependsOn: OpensearchServiceDomain 104 | 105 | # stream event mapping for lambda 106 | 107 | StreamingFunctionEventMapping: 108 | Type: "AWS::Lambda::EventSourceMapping" 109 | Properties: 110 | Enabled: true 111 | EventSourceArn: !Ref AnalyticsStreamArn 112 | FunctionName: !GetAtt OpensearchConsumerLambda.Arn 113 | StartingPosition: "LATEST" 114 | 115 | # IAM 116 | 117 | # TODO: Need to clean up this policy with regards to opensearch access 118 | StreamConsumerRole: 119 | Type: "AWS::IAM::Role" 120 | Metadata: 121 | cfn_nag: 122 | rules_to_suppress: 123 | - id: W11 124 | reason: "Lambda requires ability to write to cloudwatch *, as configured in the default AWS lambda execution role." 125 | Properties: 126 | AssumeRolePolicyDocument: 127 | Version: 2012-10-17 128 | Statement: 129 | - Effect: Allow 130 | Principal: 131 | Service: 132 | - lambda.amazonaws.com 133 | Action: 134 | - sts:AssumeRole 135 | Policies: 136 | - PolicyName: !Sub "${AWS::StackName}-ElasticKinesisAccessPolicy" 137 | PolicyDocument: 138 | Statement: 139 | - Effect: Allow 140 | Action: 141 | - "kinesis:DescribeStream" 142 | - "kinesis:GetShardIterator" 143 | - "kinesis:GetRecords" 144 | Resource: !Ref AnalyticsStreamArn 145 | - Effect: Allow 146 | Action: 147 | - "logs:CreateLogGroup" 148 | - "logs:CreateLogStream" 149 | - "logs:PutLogEvents" 150 | Resource: !Join ["",["arn:aws:logs:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":log-group:*"]] 151 | - Effect: Allow 152 | Action: 153 | - "es:ESHttpPost" 154 | - "es:ESHttpPut" 155 | - "es:ESHttpDelete" 156 | - "es:ESHttpGet" 157 | Resource: !Join ["", [!GetAtt OpensearchServiceDomain.Arn, "/*"]] 158 | - Effect: Allow 159 | Action: 160 | - "es:DescribeOpensearchDomain" 161 | - "es:GetCompatibleOpensearchVersions" 162 | - "es:DescribeOpensearchDomains" 163 | Resource: !GetAtt OpensearchServiceDomain.Arn 164 | - Effect: Allow 165 | Action: 166 | - "s3:GetObject" 167 | - "s3:PutObject" 168 | Resource: !Sub "arn:aws:s3:::${MieDataplaneBucket}/*" 169 | - Effect: Allow 170 | Action: 171 | - "kms:GenerateDataKey*" 172 | - "kms:DescribeKey" 173 | - "kms:Encrypt" 174 | - "kms:Decrypt" 175 | - "kms:ReEncrypt*" 176 | Resource: !Ref MieKMSArn 177 | Outputs: 178 | DomainEndpoint: 179 | Value: !GetAtt OpensearchServiceDomain.DomainEndpoint 180 | DomainArn: 181 | Value: !GetAtt OpensearchServiceDomain.Arn 182 | -------------------------------------------------------------------------------- /deployment/content-localization-on-aws-video-workflow.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Content Localization on AWS video processing workflow %%VERSION%%" 3 | 4 | Parameters: 5 | WorkflowCustomResourceArn: 6 | Type: String 7 | Description: "ARN of the Media Insights custom resource that handles creating operations, stages and workflows" 8 | # FIXME - this doesn't work well with nesting - just pass in the layer resource 9 | # MediaInsightsWorkflowStack: 10 | # Description: "Name of the base media insights workflow stack" 11 | # Type: String 12 | OperatorLibraryStack: 13 | Description: "Name of the Media Insights on AWS operator library stack" 14 | Type: String 15 | 16 | Resources: 17 | WebCaptionsStage: 18 | Type: Custom::CustomResource 19 | Properties: 20 | ServiceToken: !Ref WorkflowCustomResourceArn 21 | ResourceType: "Stage" 22 | Name: "WebCaptions" 23 | Operations: 24 | - Fn::ImportValue: 25 | Fn::Sub: "${OperatorLibraryStack}:WebCaptions" 26 | TranslateStage: 27 | Type: Custom::CustomResource 28 | Properties: 29 | ServiceToken: !Ref WorkflowCustomResourceArn 30 | ResourceType: "Stage" 31 | Name: "Translate" 32 | Operations: 33 | - Fn::ImportValue: 34 | Fn::Sub: "${OperatorLibraryStack}:Translate" 35 | - Fn::ImportValue: 36 | Fn::Sub: "${OperatorLibraryStack}:TranslateWebCaptions" 37 | TransformTextStage: 38 | Type: Custom::CustomResource 39 | Properties: 40 | ServiceToken: !Ref WorkflowCustomResourceArn 41 | ResourceType: "Stage" 42 | Name: "TransformText" 43 | Operations: 44 | - Fn::ImportValue: 45 | Fn::Sub: "${OperatorLibraryStack}:WebToSRTCaptions" 46 | - Fn::ImportValue: 47 | Fn::Sub: "${OperatorLibraryStack}:WebToVTTCaptions" 48 | - Fn::ImportValue: 49 | Fn::Sub: "${OperatorLibraryStack}:PollyWebCaptions" 50 | PreprocessVideoStage: 51 | Type: Custom::CustomResource 52 | Properties: 53 | ServiceToken: !Ref WorkflowCustomResourceArn 54 | ResourceType: "Stage" 55 | Name: "PreprocessVideo" 56 | Operations: 57 | - Fn::ImportValue: 58 | Fn::Sub: "${OperatorLibraryStack}:Thumbnail" 59 | - Fn::ImportValue: 60 | Fn::Sub: "${OperatorLibraryStack}:Mediainfo" 61 | AnalyzeVideoStage: 62 | Type: Custom::CustomResource 63 | Properties: 64 | ServiceToken: !Ref WorkflowCustomResourceArn 65 | ResourceType: "Stage" 66 | Name: "AnalyzeVideo" 67 | Operations: 68 | - Fn::ImportValue: 69 | Fn::Sub: "${OperatorLibraryStack}:CelebRecognition" 70 | - Fn::ImportValue: 71 | Fn::Sub: "${OperatorLibraryStack}:FaceDetection" 72 | - Fn::ImportValue: 73 | Fn::Sub: "${OperatorLibraryStack}:FaceSearch" 74 | - Fn::ImportValue: 75 | Fn::Sub: "${OperatorLibraryStack}:LabelDetection" 76 | - Fn::ImportValue: 77 | Fn::Sub: "${OperatorLibraryStack}:PersonTracking" 78 | - Fn::ImportValue: 79 | Fn::Sub: "${OperatorLibraryStack}:TextDetection" 80 | - Fn::ImportValue: 81 | Fn::Sub: "${OperatorLibraryStack}:Mediaconvert" 82 | - Fn::ImportValue: 83 | Fn::Sub: "${OperatorLibraryStack}:TechnicalCueDetection" 84 | - Fn::ImportValue: 85 | Fn::Sub: "${OperatorLibraryStack}:ShotDetection" 86 | - Fn::ImportValue: 87 | Fn::Sub: "${OperatorLibraryStack}:TranscribeVideo" 88 | 89 | AnalyzeTextStage: 90 | Type: Custom::CustomResource 91 | Properties: 92 | ServiceToken: !Ref WorkflowCustomResourceArn 93 | ResourceType: "Stage" 94 | Name: "AnalyzeText" 95 | Operations: 96 | - Fn::ImportValue: 97 | Fn::Sub: "${OperatorLibraryStack}:ComprehendPhrases" 98 | - Fn::ImportValue: 99 | Fn::Sub: "${OperatorLibraryStack}:ComprehendEntities" 100 | 101 | ContentLocalizationWorkflow: 102 | DependsOn: 103 | - PreprocessVideoStage 104 | - AnalyzeVideoStage 105 | - AnalyzeTextStage 106 | - WebCaptionsStage 107 | - TranslateStage 108 | - TransformTextStage 109 | Type: Custom::CustomResource 110 | Properties: 111 | ServiceToken: !Ref WorkflowCustomResourceArn 112 | ResourceType: "Workflow" 113 | Name: "ContentLocalizationWorkflow" 114 | StartAt: !GetAtt PreprocessVideoStage.Name 115 | Stages: !Sub 116 | - |- 117 | { 118 | "${PreprocessVideoStage}":{ 119 | "Next": "${AnalyzeVideoStage}" 120 | }, 121 | "${AnalyzeVideoStage}":{ 122 | "Next": "${AnalyzeTextStage}" 123 | }, 124 | "${AnalyzeTextStage}":{ 125 | "Next": "${WebCaptionsStage}" 126 | }, 127 | "${WebCaptionsStage}":{ 128 | "Next": "${TranslateStage}" 129 | }, 130 | "${TranslateStage}":{ 131 | "Next": "${TransformTextStage}" 132 | }, 133 | "${TransformTextStage}":{ 134 | "End": true 135 | } 136 | } 137 | - { 138 | PreprocessVideoStage: !GetAtt PreprocessVideoStage.Name, 139 | AnalyzeVideoStage: !GetAtt AnalyzeVideoStage.Name, 140 | AnalyzeTextStage: !GetAtt AnalyzeTextStage.Name, 141 | WebCaptionsStage: !GetAtt WebCaptionsStage.Name, 142 | TranslateStage: !GetAtt TranslateStage.Name, 143 | TransformTextStage: !GetAtt TransformTextStage.Name 144 | } 145 | 146 | Outputs: 147 | StateMachineArn: 148 | Value: !GetAtt ContentLocalizationWorkflow.StateMachineArn 149 | StateMachineName: 150 | Value: !GetAtt ContentLocalizationWorkflow.Name 151 | -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script should be run from the repo's deployment directory 4 | # cd deployment 5 | # ./run-unit-tests.sh 6 | 7 | # Run unit tests 8 | echo "Running unit tests" 9 | 10 | echo "------------------------------------------------------------------------------" 11 | echo "Installing Dependencies And Testing Modules" 12 | echo "------------------------------------------------------------------------------" 13 | 14 | ../test/unit/run_unit.sh 15 | -------------------------------------------------------------------------------- /deployment/sync-s3-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################################################### 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | # PURPOSE: 7 | # Copy templates and assets generated by the build script into S3 bucket 8 | # 9 | # USAGE: 10 | # ./sync-s3-dist.sh [-h] [-v] --template-bucket {TEMPLATE_BUCKET} --code-bucket {CODE_BUCKET} --version {VERSION} --region {REGION} --profile {PROFILE} 11 | # TEMPLATE_BUCKET should be the name for the S3 bucket location where Media Insights on AWS 12 | # cloud formation templates should be saved. 13 | # CODE_BUCKET should be the name for the S3 bucket location where cloud 14 | # formation templates should find Lambda source code packages. 15 | # VERSION can be anything but should be in a format like v1.0.0 just to be consistent 16 | # with the official solution release labels. 17 | # REGION needs to be in a format like us-east-1 18 | # PROFILE is optional. It's the profile that you have setup in ~/.aws/credentials 19 | # that you want to use for AWS CLI commands. 20 | # 21 | # The following options are available: 22 | # 23 | # -h | --help Print usage 24 | # -v | --verbose Print script debug info 25 | # 26 | ############################################################################### 27 | 28 | usage() { 29 | msg "$msg" 30 | cat <&2 -e "${1-}" 48 | } 49 | 50 | parse_params() { 51 | # default values of variables set from params 52 | flag=0 53 | param='' 54 | 55 | while :; do 56 | case "${1-}" in 57 | -h | --help) usage ;; 58 | -v | --verbose) set -x ;; 59 | --template-bucket) 60 | global_bucket="${2}" 61 | shift 62 | ;; 63 | --code-bucket) 64 | regional_bucket="${2}" 65 | shift 66 | ;; 67 | --version) 68 | version="${2}" 69 | shift 70 | ;; 71 | --region) 72 | region="${2}" 73 | shift 74 | ;; 75 | --profile) 76 | profile="${2}" 77 | shift 78 | ;; 79 | -?*) die "Unknown option: $1" ;; 80 | *) break ;; 81 | esac 82 | shift 83 | done 84 | 85 | args=("$@") 86 | 87 | # check required params and arguments 88 | [[ -z "${global_bucket}" ]] && usage "Missing required parameter: template-bucket" 89 | [[ -z "${regional_bucket}" ]] && usage "Missing required parameter: code-bucket" 90 | [[ -z "${version}" ]] && usage "Missing required parameter: version" 91 | [[ -z "${region}" ]] && usage "Missing required parameter: region" 92 | 93 | return 0 94 | } 95 | 96 | parse_params "$@" 97 | msg "Build parameters:" 98 | msg "- Template bucket: ${global_bucket}" 99 | msg "- Code bucket: ${regional_bucket}-${region}" 100 | msg "- Version: ${version}" 101 | msg "- Region: ${region}" 102 | msg "- Profile: ${profile}" 103 | 104 | # Make sure aws cli is installed 105 | if [[ ! -x "$(command -v aws)" ]]; then 106 | echo "ERROR: This script requires the AWS CLI to be installed. Please install it then run again." 107 | exit 1 108 | fi 109 | 110 | # Get reference for all important folders 111 | build_dir="$PWD" 112 | source_dir="$build_dir/../source" 113 | consumer_dir="$build_dir/../source/consumer" 114 | global_dist_dir="$build_dir/global-s3-assets" 115 | regional_dist_dir="$build_dir/regional-s3-assets" 116 | 117 | echo "------------------------------------------------------------------------------" 118 | echo "Copy dist to S3" 119 | echo "------------------------------------------------------------------------------" 120 | echo "Validating ownership of distribution buckets before copying deployment assets to them..." 121 | # Get account id 122 | account_id=$(aws sts get-caller-identity --query Account --output text $(if [ ! -z $profile ]; then echo "--profile $profile"; fi)) 123 | if [ $? -ne 0 ]; then 124 | die "ERROR: Failed to get AWS account ID" 125 | fi 126 | # Validate ownership of $global_dist_dir 127 | aws s3api head-bucket --bucket $global_bucket --expected-bucket-owner $account_id $(if [ ! -z $profile ]; then echo "--profile $profile"; fi) 128 | if [ $? -ne 0 ]; then 129 | die "ERROR: Your AWS account does not own s3://$global_bucket/" 130 | fi 131 | # Validate ownership of ${regional_bucket}-${region} 132 | aws s3api head-bucket --bucket ${regional_bucket}-${region} --expected-bucket-owner $account_id $(if [ ! -z $profile ]; then echo "--profile $profile"; fi) 133 | if [ $? -ne 0 ]; then 134 | die "ERROR: Your AWS account does not own s3://${regional_bucket}-${region} " 135 | fi 136 | # Copy deployment assets to distribution buckets 137 | cd "$build_dir"/ || exit 1 138 | echo "Copying the prepared distribution to:" 139 | echo "s3://$global_bucket/content-localization-on-aws/$version/" 140 | echo "s3://${regional_bucket}-${region}/content-localization-on-aws/$version/" 141 | 142 | s3domain="s3.$region.amazonaws.com" 143 | set -x 144 | aws s3 sync $global_dist_dir s3://$global_bucket/content-localization-on-aws/$version/ $(if [ ! -z $profile ]; then echo "--profile $profile"; fi) 145 | aws s3 sync $regional_dist_dir s3://${regional_bucket}-${region}/content-localization-on-aws/$version/ $(if [ ! -z $profile ]; then echo "--profile $profile"; fi) 146 | set +x 147 | 148 | echo "------------------------------------------------------------------------------" 149 | echo "S3 packaging complete" 150 | echo "------------------------------------------------------------------------------" 151 | 152 | echo "" 153 | echo "Template to deploy:" 154 | echo "" 155 | echo "With existing Media Insights on AWS deployment:" 156 | echo "TEMPLATE='"https://"$global_bucket"."$s3domain"/content-localization-on-aws/"$version"/content-localization-on-aws-use-existing-mie-stack.template"'" 157 | echo "Without existing Media Insights on AWS deployment:" 158 | echo "TEMPLATE='"https://"$global_bucket"."$s3domain"/content-localization-on-aws/"$version"/content-localization-on-aws.template"'" 159 | 160 | echo "https://"$global_bucket"."$s3domain"/content-localization-on-aws/"$version"/content-localization-on-aws.template" > template_url_that_deploys_mie_as_nested_stack.txt 161 | echo "https://"$global_bucket"."$s3domain"/content-localization-on-aws/"$version"/content-localization-on-aws-use-existing-mie-stack.template" > template_url_that_uses_an_existing_mie_stack.txt 162 | 163 | echo "Done" 164 | exit 0 165 | -------------------------------------------------------------------------------- /doc/diagrams/MIE-architecture.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 -------------------------------------------------------------------------------- /doc/diagrams/TranslatorDiagram.xml: -------------------------------------------------------------------------------- 1 | 2 | 7R3bcuK48muos/uAy/INeExCMpOqmdnUJmfmzHlJCVsBT4zlteUQ5utXki1fkBwgXOwk8JDgtq59U3erJXrmxfz5Uwyj2VfsoaBn6N5zzxz3DAM4jkP/Mcgyh4xGwwwyjX0vh5WAW/83yoF6Dk19DyW1ggTjgPhRHejiMEQuqcFgHONFvdgDDuq9RnCKJMCtCwMZ+sP3yCyDDm29hH9G/nQmegZ6/mYOReEckMyghxcVkHnZMy9ijEn2bf58gQKGPYGXrN5Vw9tiYDEKySYV7r///e36r/+n39yvg2/zqRc4y99928iaeYJBms+4ZzgBbfB8RntwpuzbbTohPgn8cMqQGnrsb0BQHELiPyH6FMBwmk7ZNxJD95GRDYUopq9xSB8WOH58COjc6VefYsXQY/TkowUnG449PxQlYRQFviue/JCgadFKhOmzGBydZjk+AYwEAGi0/BnHTpqwYeGIjQbHrNGE/qFQj3MSowvBEfs+Q/Wh5hNdQJ8UI6btOnAe0S7CSRLxrvVr9tqFbIiIzg7FfKwc9ZBxPiRwArNRxJUG2Ts3wKm3gMRlOEFPjJAMhf50imJFV9KzQEXUiAmDYeLvDbBtnjW1n0RsbllzY58+0eGipMBYQUqOTFhAtptBtZONJqYanVlOllMhGwgbKpMKimHPJ+XAKauGiRv7EYdl1KYt/ZMygTxPowBDLykBlQknaUDEhL9eXxa15zB+lPHi4nkUIIL2PVtLTdqMyWpylIZcDsqpV7j8j5I/Lxo4ksE1Tftz3xOw2QR+lEOJ0zARaA3RM+GyydTzDv1eJ0nKlo+zjUp/TudQKB6qyIQaSrhEE4oMHwbBkqlLFLN/AeYqkQ485MrxQoVhXp0uTwldyGKudory+mLmB4h3+KKKuZvxRhY+1ypurtSiGE8CNE9KtYrTOOvLTWO6LLjL7InEOOA8XnJAskwImheKK0aQU9yFEXR9wur9ShNGAaatsrE+4FKmUJww4l3wyotsBrQXL3URJ96ijgJKRsb/+h83bOh/MggjMteDfGYh5koUpxTxnhgpIwBdXswrBUZ+zCCrMaOMjjjXLGYorCh83mOSSSuBMVG38hny5Ytz3BSxBido5od1GvFp/5OilI9MzCpRtvhKSRCwSaxg7r3I2o49cPuFLIVRRJh0UguBzAMKAKwvSv1HdIEDii1zHOKQljx/oPy6AkoYf1G0mmO7fLqjC7A57lP1a55TiSDolsJZVwtqVFIYpuLGOckcz3zPo4Q2z2PKKx5ipg2r9UCZPLcdgZE/i557hqnzTzGTqs2Um1G0BzqpCii3oT4hPEeEC7t4a9hZldyi7QNdWHiL0kAcODlsVjEOjWEOhLlROi1aL+02+iU33dRm3Dn8a3T9C1m3z5NH4/Pwd/T9Z78/kEiEPGrG5o84JjM8xSEMLkvoCgbLMl8wJwej6i9EyDLHK0ypmNRojp598r/K95+sKc2w88fxc940f1iKB6onlqxWX9d00xSQvC4nHgeUtflTrfoNin2KNxTnwGzybMaNpnEOSqiGdNELiMydE6oymD5oLmdZaiaJUZAZxTU/REFuXvUsjuGyUoCbuEml5ZvM5hW8N9QtbZX77OEK92SNlrxUjO717DWUnIQ7ZjwFmVa/zRbpFQYs2Qs0iHWFlQI4QcENTvzMFh27KOTkPWdySQ2Z4MtKAWazm+cw8KfK4mf5iwkmBM8LFpGkXMEkjYI/kJFP+VWAKrJfuIA12beLonuX/lF3pH9D4dfrcm+vkfo9CriIK6yVcGDsKOE7kVQMsyJy33A8pwz/m4ncdRil5OOIngFk0dM3Fr3hoeQOAIlIX6mPCS9wyKYrS2UQ+FGC1tMEJlEWy3rwnxkd94JDQ4FD2XAZqhB4KLMFyNGnu9wrn3yglcUcdJO9zVbWlZcMRAuYa5aKQxuIwNp0/XDasBCtoWwhOlaVFV5R5TBGpcBkg/B3SXeaMora151269JprXpvg5aF0xI7ROuEc9iqbedIjH9DUQL5EL6wrZRK0LMWsKlFmz7I4mibuyyOo8N5XWAgkfEHmlzAiOEq6ZYCs60OKrAXowpdwt5Adj+AOVCw4HHxN3rL+HNax58hu9hvCH+j9vEHZCwd0QCh9gdY8Q4oUlo2QAxjQwMk20Q4tncAwEghiStpI9vXOYx/IHBZkc98caXAuzzP5JY02Erv3SwCusInAUAzNo5Hl2X3rxpMiXTf7+66pVSBLvO1oQ+1Qdtq1WpXrbYWmRfcuFZ3mrt6b6/TnYooHXCMl3Xn+joH0p22bNtkGTW3y5DMUMKzLj6IBwkMRyHpKvtJrSmtg4m6HAjoqAWqUpaOcrE5rq6UXfA3hMBRBxAo++BvYqEGRgdwJ/vfhSEq7NAsO3QMCYwCGFYzGSctmaqFYl2rgjNNvQcCyru3jhx6AqapIB44GPVM2fuHqedjbR6Zsj/x5HuIvbLkV2SWzkPoswTL1Vdzthnshw94Awfl9Sl1awk59z0vaBLeuuW4D2pLoUYAZHJbqoX2YLSW9+lJsdOk/UqYR/leyWFJWUvtk0N27Bdo4uaB8/skdvvsQElOmFXJ0TTt/VLLllSlilrAPiq5ZGdepgol2j0n2hMhzbnGJYSVBerC7JWhfvWuaQ/0wWiV+IrM4oIhjkN8eZO+KqsZGdWCulrO+IgCPRxKNFUJ9HFpqggOdNRrMhTxE0u1+XtUy9+U/fY3gLdB+3iT3fUbzA9UdR156oyD4yJPdtXHKPDzk2gfdx/EUuWHtJ06acqxge5mBgNLlR3SOsNbsofO3XDQ4J4rrFIOV7jz2ru2OYC1mRdxVKPDbuWQCj/7ecaugSipxWFX7Cit2NDyRAk3gEniuxkwLwJ6e93l2jhF0W4lf9hxVhXB6OX0gKENXqxwmP0tSw4h7FGnVkW8Z5hXV6bJE9H3IZuKVGKg2HI2jimbVruJ/itbzkbrOf7Wpjn+GdcdfSdaV+SD1Y+BvqbOgSS1IcufOZsehddSZj+uGTtUJKNslc1zuDxn682EC0aK49Gm0/pGofVmtvlVCHQ6gMA3lGmvQuGoAyiUowdvxGDS5XOrDTx5XJtJ9vBJLtSUKV8MjK+W+5CBcUfe7ACKxe6oO122HHHgOQE5NZWZAWUBQ13gXVMRGHb39jeER7rJ/vJ/GsSTpwbcF7uaedH3SkZ7JFFx1Pa2s+JmSvXOo4KE6q3Hd0zAkSyG7ROw5UT2Mnf9Z68aRVDHFGqRwjIOWA0WgnqwMOeEMlK4XRRi/RVTraS5FzfbCmPLXL1Wdl2FdRFDoJsvxxjrgYjNR1jMMQvw5NVWuHgPIQ273ZP3uzD2+hD4wbm6lZDZuii14uTGaEcmBS0zqRx0EFetXj4jN2WL4y2BJE0u2S2vdzii+m6VrXHKbn2my6G4W5sxgweTWcHIL4fQGiNm0vJZXYJnMGL9z5+n7E5xDS4SU0uony/5mePR2cBiVWhBz0fllY85EzcG/vh1kfk0my6A3Cp6J67KEdS2FIfrB45YoqvLbwnd/wLczq2QrzxJ1r2tOnvTyyAz72YHnbYbmTcI7UgRG64wUcxlPykFL7ugXpLydQby608hqA8dSOphRfB1HQyHY4WumCcuRFp2+Xyg8e3/PYh34b4WAaihHKoQAaGqcNuHsq0d2Tmq3/pbIb64wJwpvX7CxZZdjQ+M6Ll6u3nmOX26vNvylvbt+susCZ2/e4BzP1hmb4tOKXVNq8fOToQBlqC85IWyPFVS7jYVXCr+Pr8mnl9X3VSPtosT7r1JuGKUQwSyC9QZNU9oakDT4JyqakTuKUcPxic0NXPTCTcNuKns9hQxOTnqc/PfO97YSSpPUnmSys5JZfl7EFdIeKAMvYNz8ea+gNe5coMfLTi+n7N9wPAILovzsiHb17WBpUtpQHnr24Vg5NTBlZBHEYEWTWR+lBTx2DpFsaGfNYEZ0Tx+eEjQQUIujrzvJDGmcFj8Of/dsSpDqV2htR4U973Oofs45Sxd8ZIe+IcW4Z2dCReQ8bvkD+bjGc8IibKfy6F678r1QkPzXRw++FRYYs2lPRpX+dLK4Akr5MdugPr8sU9RcMUuj72asJ9eQ3EfGEMtYnph902qobOaCGAPFJsc4k6D2jlyATzARe0q9/vkip1csS6i6WT0nYy+kyvWMZSepPIklSdX7L25YkAfbe2LAX0/rhjQxXG7A/tijR217ozl6D95YwfyxkbygZUOeGPWBi54J9PcHbuDOe6gA78AZPVWDge2fcW3+PWO9ZlODRTfNcwm33cAbEcDo/IzNOqNNqQaSU0PdF1ueqAN9IGt5x9xnG/NOrJei9PHGDMbpizOdvC/Yg+xEv8C -------------------------------------------------------------------------------- /doc/images/ContentLocalizationArchitectureOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ContentLocalizationArchitectureOverview.png -------------------------------------------------------------------------------- /doc/images/MIE-execute-workflow-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/MIE-execute-workflow-architecture.png -------------------------------------------------------------------------------- /doc/images/MIEDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/MIEDemo.gif -------------------------------------------------------------------------------- /doc/images/collection-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/collection-search.png -------------------------------------------------------------------------------- /doc/images/create_user01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/create_user01.png -------------------------------------------------------------------------------- /doc/images/create_user02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/create_user02.png -------------------------------------------------------------------------------- /doc/images/create_user03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/create_user03.png -------------------------------------------------------------------------------- /doc/images/ig-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-architecture.png -------------------------------------------------------------------------------- /doc/images/ig-collection-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-collection-page.png -------------------------------------------------------------------------------- /doc/images/ig-cost-custom-table1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-cost-custom-table1.png -------------------------------------------------------------------------------- /doc/images/ig-cost-customization-table2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-cost-customization-table2.png -------------------------------------------------------------------------------- /doc/images/ig-cost-customization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-cost-customization.png -------------------------------------------------------------------------------- /doc/images/ig-create-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-create-user.png -------------------------------------------------------------------------------- /doc/images/ig-edit-vocabulary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-edit-vocabulary.png -------------------------------------------------------------------------------- /doc/images/ig-find-the-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-find-the-url.png -------------------------------------------------------------------------------- /doc/images/ig-full-text-search-speech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-full-text-search-speech.png -------------------------------------------------------------------------------- /doc/images/ig-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-homepage.png -------------------------------------------------------------------------------- /doc/images/ig-opensearch-consumer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-opensearch-consumer.png -------------------------------------------------------------------------------- /doc/images/ig-operator-categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-operator-categories.png -------------------------------------------------------------------------------- /doc/images/ig-playhead-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-playhead-search.png -------------------------------------------------------------------------------- /doc/images/ig-search-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-search-box.png -------------------------------------------------------------------------------- /doc/images/ig-terminology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-terminology.png -------------------------------------------------------------------------------- /doc/images/ig-upload-configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-upload-configure.png -------------------------------------------------------------------------------- /doc/images/ig-upload-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-upload-video.png -------------------------------------------------------------------------------- /doc/images/ig-user-to-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-user-to-group.png -------------------------------------------------------------------------------- /doc/images/ig-view-subtitles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-view-subtitles.png -------------------------------------------------------------------------------- /doc/images/ig-view-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-view-translation.png -------------------------------------------------------------------------------- /doc/images/ig-webapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-webapp.png -------------------------------------------------------------------------------- /doc/images/ig-webappauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-webappauth.png -------------------------------------------------------------------------------- /doc/images/ig-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/ig-workflow.png -------------------------------------------------------------------------------- /doc/images/kibana-create-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/kibana-create-index.png -------------------------------------------------------------------------------- /doc/images/launch-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/launch-stack.png -------------------------------------------------------------------------------- /doc/images/screenshot-analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/screenshot-analytics.png -------------------------------------------------------------------------------- /doc/images/screenshot-uploads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/screenshot-uploads.png -------------------------------------------------------------------------------- /doc/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/search.png -------------------------------------------------------------------------------- /doc/images/upload-workflow-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/upload-workflow-status.png -------------------------------------------------------------------------------- /doc/images/workflow-analysis-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/workflow-analysis-results.png -------------------------------------------------------------------------------- /doc/images/workflow-error-step-fn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/doc/images/workflow-error-step-fn.png -------------------------------------------------------------------------------- /source/anonymized-data-logger/anonymized-data-logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | ############################################################################## 4 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # 5 | # # 6 | # Licensed under the Apache License Version 2.0 (the "License"). # 7 | # You may not use this file except in compliance with the License. # 8 | # A copy of the License is located at # 9 | # # 10 | # http://www.apache.org/licenses/ # 11 | # # 12 | # or in the "license" file accompanying this file. This file is distributed # 13 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # 14 | # express or implied. See the License for the specific language governing # 15 | # permissions and limitations under the License. # 16 | ############################################################################## 17 | # 18 | # PURPOSE: 19 | # This function sends anonymized performance data to the AWS 20 | # Solutions metrics API. This information is anonymized and helps improve the 21 | # quality of the solution. 22 | # 23 | ############################################################################## 24 | 25 | import uuid 26 | import lib.cfnresponse as cfn 27 | import lib.metrics as Metrics 28 | 29 | 30 | def handler(event, context): 31 | print("We got this event:\n", event) 32 | # Each resource returns a promise with a json object to return cloudformation. 33 | try: 34 | request_type = event['RequestType'] 35 | resource = event['ResourceProperties']['Resource'] 36 | config = event['ResourceProperties'] 37 | # Remove ServiceToken (lambda arn) to avoid sending AccountId 38 | config.pop("ServiceToken", None) 39 | config.pop("Resource", None) 40 | # Add some useful fields related to stack change 41 | config["CFTemplate"] = ( 42 | request_type + "d" 43 | ) # Created, Updated, or Deleted 44 | response_data = {} 45 | print('Request::{} Resource::{}'.format(request_type, resource)) 46 | if request_type == 'Create' or request_type == 'Update': 47 | if resource == 'UUID': 48 | response_data = {'UUID': str(uuid.uuid4())} 49 | unique_id = response_data['UUID'] 50 | cfn.send(event, context, 'SUCCESS', response_data, unique_id) 51 | elif resource == 'AnonymizedMetric': 52 | Metrics.send_metrics(config) 53 | unique_id = 'Metrics Sent' 54 | cfn.send(event, context, 'SUCCESS', response_data, unique_id) 55 | else: 56 | print('Create failed, {} not defined in the Custom Resource'.format(resource)) 57 | cfn.send(event, context, 'FAILED', {}, context.log_stream_name) 58 | elif request_type == 'Delete': 59 | print('RESPONSE:: {}: Not required to report data for delete request.'.format(resource)) 60 | cfn.send(event, context, 'SUCCESS', {}) 61 | else: 62 | print('RESPONSE:: {} Not supported'.format(request_type)) 63 | except Exception as e: 64 | print('Exception: {}'.format(e)) 65 | cfn.send(event, context, 'FAILED', {}, context.log_stream_name) 66 | print(e) 67 | -------------------------------------------------------------------------------- /source/anonymized-data-logger/lib/cfnresponse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | ############################################################################## 4 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # 5 | # # 6 | # Licensed under the Apache License Version 2.0 (the "License"). # 7 | # You may not use this file except in compliance with the License. # 8 | # A copy of the License is located at # 9 | # # 10 | # http://www.apache.org/licenses/ # 11 | # # 12 | # or in the "license" file accompanying this file. This file is distributed # 13 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # 14 | # express or implied. See the License for the specific language governing # 15 | # permissions and limitations under the License. # 16 | ############################################################################## 17 | 18 | import requests 19 | import json 20 | 21 | 22 | def send(event, context, response_status, response_data, physical_resource_id=None, no_echo=False): 23 | response_url = event['ResponseURL'] 24 | 25 | response_body = {} 26 | response_body['Status'] = response_status 27 | response_body['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name 28 | response_body['PhysicalResourceId'] = physical_resource_id or context.log_stream_name 29 | response_body['StackId'] = event['StackId'] 30 | response_body['RequestId'] = event['RequestId'] 31 | response_body['LogicalResourceId'] = event['LogicalResourceId'] 32 | response_body['NoEcho'] = no_echo 33 | response_body['Data'] = response_data 34 | 35 | json_response_body = json.dumps(response_body) 36 | 37 | headers = { 38 | 'content-type': '', 39 | 'content-length': str(len(json_response_body)) 40 | } 41 | 42 | try: 43 | response = requests.put(response_url, 44 | timeout=20, 45 | data=json_response_body, 46 | headers=headers) 47 | print("Status code: " + response.reason) 48 | except Exception as e: 49 | print("send(..) failed executing requests.put(..): " + str(e)) 50 | -------------------------------------------------------------------------------- /source/anonymized-data-logger/lib/metrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | ############################################################################## 4 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # 5 | # # 6 | # Licensed under the Apache License Version 2.0 (the "License"). # 7 | # You may not use this file except in compliance with the License. # 8 | # A copy of the License is located at # 9 | # # 10 | # http://www.apache.org/licenses/ # 11 | # # 12 | # or in the "license" file accompanying this file. This file is distributed # 13 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # 14 | # express or implied. See the License for the specific language governing # 15 | # permissions and limitations under the License. # 16 | ############################################################################## 17 | 18 | import datetime 19 | import json 20 | import urllib.request 21 | 22 | 23 | def send_metrics(config): 24 | metrics = {} 25 | # move Solution ID and UUID to the root JSON level 26 | metrics['Solution'] = config.pop("SolutionId", None) 27 | metrics['UUID'] = config.pop("UUID", None) 28 | metrics['TimeStamp'] = str(datetime.datetime.utcnow().isoformat()) 29 | metrics['Data'] = config 30 | url = 'https://metrics.awssolutionsbuilder.com/generic' 31 | data = json.dumps(metrics).encode('utf8') 32 | headers = {'content-type': 'application/json'} 33 | req = urllib.request.Request(url, data, headers) 34 | response = urllib.request.urlopen(req) # nosec - verified urlopen is using a Request object as expected 35 | print('RESPONSE CODE:: {}'.format(response.getcode())) 36 | print('METRICS SENT:: {}'.format(data)) 37 | -------------------------------------------------------------------------------- /source/anonymized-data-logger/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | urllib3==2.2.2 3 | -------------------------------------------------------------------------------- /source/consumer/requirements.txt: -------------------------------------------------------------------------------- 1 | elasticsearch==7.13.4 2 | requests-aws4auth==1.2.3 3 | -------------------------------------------------------------------------------- /source/helper/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/source/helper/requirements.txt -------------------------------------------------------------------------------- /source/helper/website_helper.py: -------------------------------------------------------------------------------- 1 | ###################################################################################################################### 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # 5 | # with the License. A copy of the License is located at # 6 | # # 7 | # http://www.apache.org/licenses/LICENSE-2.0 # 8 | # # 9 | # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # 10 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # 11 | # and limitations under the License. # 12 | ###################################################################################################################### 13 | 14 | import boto3 15 | from botocore import config 16 | import json 17 | import logging 18 | import os 19 | from urllib.request import build_opener, HTTPHandler, Request 20 | 21 | 22 | LOGGER = logging.getLogger() 23 | LOGGER.setLevel(logging.INFO) 24 | 25 | helper_config = json.loads(os.environ['botoConfig']) 26 | config = config.Config(**helper_config) 27 | 28 | s3 = boto3.resource('s3', config=config) 29 | s3_client = boto3.client('s3', config=config) 30 | 31 | replace_env_variables = False 32 | 33 | 34 | def send_response(event, context, response_status, response_data): 35 | """ 36 | Send a resource manipulation status response to CloudFormation 37 | """ 38 | response_body = json.dumps({ 39 | "Status": response_status, 40 | "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, 41 | "PhysicalResourceId": context.log_stream_name, 42 | "StackId": event['StackId'], 43 | "RequestId": event['RequestId'], 44 | "LogicalResourceId": event['LogicalResourceId'], 45 | "Data": response_data 46 | }) 47 | 48 | LOGGER.info('ResponseURL: {s}'.format(s=event['ResponseURL'])) 49 | LOGGER.info('ResponseBody: {s}'.format(s=response_body)) 50 | 51 | opener = build_opener(HTTPHandler) 52 | request = Request(event['ResponseURL'], data=response_body.encode('utf-8')) 53 | request.add_header('Content-Type', '') 54 | request.add_header('Content-Length', str(len(response_body))) 55 | request.get_method = lambda: 'PUT' 56 | response = opener.open(request) 57 | LOGGER.info("Status code: {s}".format(s=response.getcode)) 58 | LOGGER.info("Status message: {s}".format(s=response.msg)) 59 | 60 | 61 | def write_to_s3(event, context, bucket, key, body): 62 | try: 63 | s3_client.put_object(Bucket=bucket, Key=key, Body=body) 64 | except Exception as e: 65 | LOGGER.info('Unable to write file to s3: {e}'.format(e=e)) 66 | send_response(event, context, "FAILED", 67 | {"Message": "Failed to write file to s3 after variable replacement"}) 68 | else: 69 | LOGGER.info('Wrote file back to s3 after variable replacement') 70 | 71 | 72 | def copy_source(event, context): 73 | try: 74 | source_bucket = event["ResourceProperties"]["WebsiteCodeBucket"] 75 | source_key = event["ResourceProperties"]["WebsiteCodePrefix"] 76 | website_bucket = event["ResourceProperties"]["DeploymentBucket"].split('.')[0] 77 | except KeyError as e: 78 | LOGGER.info("Failed to retrieve required values from the CloudFormation event: {e}".format(e=e)) 79 | send_response(event, context, "FAILED", {"Message": "Failed to retrieve required values from the CloudFormation event"}) 80 | else: 81 | try: 82 | LOGGER.info("Checking if custom environment variables are present") 83 | 84 | try: 85 | search = 'https://' + os.environ['SearchEndpoint'] 86 | dataplane = os.environ['DataplaneEndpoint'] 87 | workflow = os.environ['WorkflowEndpoint'] 88 | dataplane_bucket = os.environ['DataplaneBucket'] 89 | user_pool_id = os.environ['UserPoolId'] 90 | region = os.environ['AwsRegion'] 91 | client_id = os.environ['PoolClientId'] 92 | identity_id = os.environ['IdentityPoolId'] 93 | except KeyError: 94 | replace_env_variables = False 95 | else: 96 | new_variables = {"SEARCH_ENDPOINT": search, "WORKFLOW_API_ENDPOINT": workflow, 97 | "DATAPLANE_API_ENDPOINT": dataplane, "DATAPLANE_BUCKET": dataplane_bucket, "AWS_REGION": region, 98 | "USER_POOL_ID": user_pool_id, "USER_POOL_CLIENT_ID": client_id, "IDENTITY_POOL_ID": identity_id} 99 | replace_env_variables = True 100 | LOGGER.info( 101 | "New variables: {v}".format(v=new_variables)) 102 | 103 | with open('./webapp-manifest.json') as file: 104 | manifest = json.load(file) 105 | print('UPLOADING FILES::') 106 | for key in manifest: 107 | print('s3://' + source_bucket + '/' + source_key + '/' + key) 108 | copy_source = { 109 | 'Bucket': source_bucket, 110 | 'Key': source_key + '/' + key 111 | } 112 | s3.meta.client.copy(copy_source, website_bucket, key) 113 | if replace_env_variables is True and key == "runtimeConfig.json": 114 | LOGGER.info("updating runtimeConfig.json") 115 | write_to_s3(event, context, website_bucket, key, json.dumps(new_variables)) 116 | 117 | except Exception as e: 118 | LOGGER.info("Unable to copy website source code into the website bucket: {e}".format(e=e)) 119 | send_response(event, context, "FAILED", {"Message": "Unexpected event received from CloudFormation"}) 120 | else: 121 | send_response(event, context, "SUCCESS", 122 | {"Message": "Resource creation successful!"}) 123 | 124 | 125 | def lambda_handler(event, context): 126 | """ 127 | Handle Lambda event from AWS 128 | """ 129 | try: 130 | LOGGER.info('REQUEST RECEIVED:\n {s}'.format(s=event)) 131 | LOGGER.info('REQUEST RECEIVED:\n {s}'.format(s=context)) 132 | if event['RequestType'] == 'Create': 133 | LOGGER.info('CREATE!') 134 | copy_source(event, context) 135 | elif event['RequestType'] == 'Update': 136 | LOGGER.info('UPDATE!') 137 | copy_source(event, context) 138 | elif event['RequestType'] == 'Delete': 139 | LOGGER.info('DELETE!') 140 | send_response(event, context, "SUCCESS", 141 | {"Message": "Resource deletion successful!"}) 142 | else: 143 | LOGGER.info('FAILED!') 144 | send_response(event, context, "FAILED", {"Message": "Unexpected event received from CloudFormation"}) 145 | except Exception as e: 146 | LOGGER.info('FAILED!') 147 | send_response(event, context, "FAILED", {"Message": "Exception during processing: {e}".format(e=e)}) 148 | -------------------------------------------------------------------------------- /source/website/eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue' 2 | 3 | export default [ 4 | ...pluginVue.configs['flat/recommended'], 5 | { 6 | files: ["src/**/*.js", "src/**/*.vue"], 7 | ignores: [ 8 | "source/website/src/dist/*.js", 9 | "source/website/src/dist/min/*.js" 10 | ], 11 | rules: { 12 | "no-console": "off", 13 | "no-undef": "off", 14 | "vue/require-prop-types": "off", 15 | "vue/attribute-hyphenation": "off", 16 | "vue/valid-v-for": "off", 17 | "vue/max-attributes-per-line": "off", 18 | "vue/html-self-closing": "off", 19 | "vue/require-explicit-emits": "off", 20 | "vue/multi-word-component-names": ["error", { 21 | ignores: [ 22 | "Celebrities", 23 | "Entities", 24 | "Loading", 25 | "Subtitles", 26 | "Transcript", 27 | "Translation", 28 | "Waveform", 29 | "Home", 30 | "Login", 31 | ] 32 | }], 33 | "vue/require-valid-default-prop": "off", 34 | "vue/no-deprecated-dollar-listeners-api": "off" 35 | } 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /source/website/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Content Localization on AWS 14 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /source/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "content-localization-on-aws", 3 | "version": "2.1.14", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview", 10 | "lint": "eslint -c eslint.config.js --fix src", 11 | "deploy": "aws s3 sync --acl public-read --profile mie --delete dist/ s3://[your_website_bucket]" 12 | }, 13 | "dependencies": { 14 | "aws-amplify": "5.3.27", 15 | "aws-amplify-vue": "2.1.9", 16 | "axios": "^1.8.2", 17 | "bootstrap": "4.6.1", 18 | "bootstrap-vue": "^2.23.1", 19 | "chart.js": "^4.4.6", 20 | "diff": "5.0.0", 21 | "dropzone": "5.9.3", 22 | "jquery": "3.6.0", 23 | "jwt-decode": "3.1.2", 24 | "latest-version": "^7.0.0", 25 | "lodash": "4.17.21", 26 | "number-to-words": "1.2.4", 27 | "video.js": "^8.19.1", 28 | "videojs-flash": "2.2.1", 29 | "videojs-hotkeys": "0.2.30", 30 | "videojs-markers": "1.0.1", 31 | "vue": "^3.4.21", 32 | "@vue/compat": "^3.4.21", 33 | "@vue/compiler-sfc": "^3.4.21", 34 | "vue-highlightjs": "1.3.3", 35 | "vue-router": "3.5.2", 36 | "vuex": "^4.1.0", 37 | "vuex-persistedstate": "4.1.0", 38 | "wavesurfer.js": "^7.8.9" 39 | }, 40 | "devDependencies": { 41 | "@eslint/eslintrc": "^3.2.0", 42 | "@vitejs/plugin-vue": "^5.2.1", 43 | "eslint": "^9.16.0", 44 | "eslint-plugin-vue": "^9.32.0", 45 | "unplugin-vue-components": "^0.27.4", 46 | "vite": "^6.0.3", 47 | "webpack-subresource-integrity": "5.1.0" 48 | }, 49 | "overrides": { 50 | "vue": "^3.4.21" 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions" 55 | ], 56 | "description": "This application allows users to generate subtitles for videos using human-in-the-loop workflows for AI enhanced transcription and translation services on AWS.", 57 | "directories": { 58 | "doc": "../../doc", 59 | "test": "../../test" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "git+https://github.com/aws-samples/aws-media-content-localization.git" 64 | }, 65 | "keywords": [], 66 | "author": { 67 | "name": "Amazon Web Services", 68 | "url": "https://aws.amazon.com/solutions" 69 | }, 70 | "license": "Apache-2.0", 71 | "bugs": { 72 | "url": "https://github.com/aws-samples/aws-media-content-localization/issues" 73 | }, 74 | "homepage": "https://github.com/aws-samples/aws-media-content-localization#readme" 75 | } 76 | -------------------------------------------------------------------------------- /source/website/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/source/website/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /source/website/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "media_indexer", 3 | "short_name": "media_indexer", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "background_color": "#000000", 7 | "theme_color": "#4DBA87" 8 | } 9 | -------------------------------------------------------------------------------- /source/website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /source/website/public/runtimeConfig.json: -------------------------------------------------------------------------------- 1 | {"SEARCH_ENDPOINT": "", "WORKFLOW_API_ENDPOINT": "", "DATAPLANE_API_ENDPOINT": "", "DATAPLANE_BUCKET": "", "USER_POOL_ID": "", "USER_POOL_CLIENT_ID": "", "IDENTITY_POOL_ID": "", "AWS_REGION": ""} 2 | -------------------------------------------------------------------------------- /source/website/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /source/website/src/components/ComponentLoadingError.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /source/website/src/components/ComprehendEntities.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 41 | 42 | 116 | -------------------------------------------------------------------------------- /source/website/src/components/ComprehendKeyPhrases.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 44 | 45 | 118 | -------------------------------------------------------------------------------- /source/website/src/components/HeaderView.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 55 | 56 | 96 | 97 | 104 | -------------------------------------------------------------------------------- /source/website/src/components/ImageFeature.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /source/website/src/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 225 | 226 | 235 | -------------------------------------------------------------------------------- /source/website/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /source/website/src/components/MediaSummaryBox.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 89 | 90 | 184 | -------------------------------------------------------------------------------- /source/website/src/components/ShotDetection.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 73 | 74 | 215 | -------------------------------------------------------------------------------- /source/website/src/components/TechnicalCues.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 72 | 73 | 214 | -------------------------------------------------------------------------------- /source/website/src/components/Transcript.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | 30 | 98 | -------------------------------------------------------------------------------- /source/website/src/components/VideoPlayer.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 30 | 31 | 134 | 135 | 140 | 149 | -------------------------------------------------------------------------------- /source/website/src/components/VideoThumbnail.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /source/website/src/components/VoerroTagsInput.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /* The input */ 7 | .tags-input { 8 | display: flex; 9 | flex-wrap: wrap; 10 | align-items: center; 11 | } 12 | 13 | .tags-input input { 14 | flex: 1; 15 | background: transparent; 16 | border: none; 17 | } 18 | 19 | .tags-input input:focus { 20 | outline: none; 21 | } 22 | 23 | .tags-input input[type="text"] { 24 | padding: 0 .1em; 25 | color: #495057; 26 | margin-bottom: 0px; 27 | font-size: small; 28 | } 29 | 30 | .tags-input-wrapper-default { 31 | padding: .1em .1em; 32 | background: #fff; 33 | border: 1px solid transparent; 34 | border-radius: .5em; 35 | border-color: #dbdbdb; 36 | margin-bottom: 5px; 37 | } 38 | 39 | .tags-input-wrapper-default.active { 40 | border: 2px solid #8bbafe; 41 | box-shadow: 0 0 0 0.2em rgba(13, 110, 253, 0.25); 42 | outline: 0 none; 43 | } 44 | 45 | /* The tag badges & the remove icon */ 46 | .tags-input span { 47 | margin-right: 0.3em; 48 | } 49 | 50 | .tags-input-remove { 51 | cursor: pointer; 52 | position: absolute; 53 | display: inline-block; 54 | right: 0.3em; 55 | top: 0.3em; 56 | padding: 0.5em; 57 | overflow: hidden; 58 | } 59 | 60 | .tags-input-remove:focus { 61 | outline: none; 62 | } 63 | 64 | .tags-input-remove:before, .tags-input-remove:after { 65 | content: ''; 66 | position: absolute; 67 | width: 75%; 68 | left: 0.15em; 69 | background: #5dc282; 70 | 71 | height: 2px; 72 | margin-top: -1px; 73 | } 74 | 75 | .tags-input-remove:before { 76 | transform: rotate(45deg); 77 | } 78 | .tags-input-remove:after { 79 | transform: rotate(-45deg); 80 | } 81 | 82 | /* Tag badge styles */ 83 | .tags-input-badge { 84 | position: relative; 85 | display: inline-block; 86 | padding: 0.25em 0.4em; 87 | font-size: 75%; 88 | font-weight: 700; 89 | line-height: 1; 90 | text-align: center; 91 | white-space: nowrap; 92 | vertical-align: baseline; 93 | border-radius: 0.25em; 94 | overflow: hidden; 95 | text-overflow: ellipsis; 96 | } 97 | 98 | .tags-input-badge-pill { 99 | padding-right: 1.25em; 100 | padding-left: 0.6em; 101 | border-radius: 10em; 102 | } 103 | 104 | .tags-input-badge-selected-default { 105 | margin-bottom: .25em; 106 | color: #212529; 107 | background-color: #f0f1f2; 108 | } 109 | 110 | /* Typeahead */ 111 | .typeahead-hide-btn { 112 | color: #999 !important; 113 | font-style: italic; 114 | } 115 | 116 | /* Typeahead - badges */ 117 | .typeahead-badges > span { 118 | cursor: pointer; 119 | margin-right: 0.3em; 120 | } 121 | 122 | /* Typeahead - dropdown */ 123 | .typeahead-dropdown { 124 | list-style-type: none; 125 | padding: 0; 126 | margin: 0; 127 | position: absolute; 128 | width: 100%; 129 | z-index: 1000; 130 | } 131 | 132 | .typeahead-dropdown li { 133 | padding: .25em 1em; 134 | cursor: pointer; 135 | } 136 | 137 | /* Typeahead elements style/theme */ 138 | .tags-input-typeahead-item-default { 139 | color: #fff; 140 | background-color: #343a40; 141 | } 142 | 143 | .tags-input-typeahead-item-highlighted-default { 144 | color: #fff; 145 | background-color: #007bff; 146 | } 147 | -------------------------------------------------------------------------------- /source/website/src/components/Waveform.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 142 | -------------------------------------------------------------------------------- /source/website/src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { createApp } from 'vue' 5 | import VueHighlightJS from 'vue-highlightjs' 6 | import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue' 7 | 8 | import 'bootstrap/dist/css/bootstrap.css' 9 | import 'bootstrap-vue/dist/bootstrap-vue.css' 10 | import 'dropzone/dist/dropzone.css' 11 | import "highlight.js/styles/github.css" 12 | 13 | import App from './App.vue' 14 | import store from './store' 15 | import router from './router.js' 16 | import { Amplify } from "aws-amplify"; 17 | import * as AmplifyModules from "aws-amplify"; 18 | import { AmplifyPlugin } from "aws-amplify-vue"; 19 | 20 | const app = createApp({ 21 | router, 22 | ...App 23 | }) 24 | 25 | const getRuntimeConfig = async () => { 26 | const runtimeConfig = await fetch('/runtimeConfig.json'); 27 | return await runtimeConfig.json() 28 | }; 29 | 30 | getRuntimeConfig().then(function(json) { 31 | const awsconfig = { 32 | Auth: { 33 | region: json.AWS_REGION, 34 | userPoolId: json.USER_POOL_ID, 35 | userPoolWebClientId: json.USER_POOL_CLIENT_ID, 36 | identityPoolId: json.IDENTITY_POOL_ID 37 | }, 38 | Storage: { 39 | AWSS3: { 40 | bucket: json.DATAPLANE_BUCKET, 41 | region: json.AWS_REGION 42 | } 43 | }, 44 | API: { 45 | endpoints: [ 46 | { 47 | name: "search", 48 | endpoint: json.SEARCH_ENDPOINT, 49 | service: "es", 50 | region: json.AWS_REGION 51 | }, 52 | { 53 | name: "mieWorkflowApi", 54 | endpoint: json.WORKFLOW_API_ENDPOINT, 55 | service: "execute-api", 56 | region: json.AWS_REGION 57 | }, 58 | { 59 | name: "mieDataplaneApi", 60 | endpoint: json.DATAPLANE_API_ENDPOINT, 61 | service: "execute-api", 62 | region: json.AWS_REGION 63 | } 64 | ] 65 | } 66 | }; 67 | console.log("Runtime config: " + JSON.stringify(json)); 68 | Amplify.configure(awsconfig); 69 | app.mixin({ 70 | data() { 71 | return { 72 | // Distribute runtime configs into every Vue component 73 | SEARCH_ENDPOINT: json.SEARCH_ENDPOINT, 74 | DATAPLANE_API_ENDPOINT: json.DATAPLANE_API_ENDPOINT, 75 | DATAPLANE_BUCKET: json.DATAPLANE_BUCKET, 76 | WORKFLOW_API_ENDPOINT: json.WORKFLOW_API_ENDPOINT, 77 | AWS_REGION: json.AWS_REGION 78 | } 79 | }, 80 | }); 81 | 82 | app.use(AmplifyPlugin, AmplifyModules); 83 | app.use(BootstrapVue); 84 | app.use(store); 85 | app.use(BootstrapVueIcons); 86 | app.use(VueHighlightJS) 87 | 88 | router.beforeResolve(async (to, from, next) => { 89 | if (to.matched.some(record => record.meta.requiresAuth)) { 90 | try { 91 | await app.prototype.$Amplify.Auth.currentAuthenticatedUser(); 92 | next(); 93 | } catch (e) { 94 | console.log(e); 95 | next({ 96 | path: "/", 97 | query: { 98 | redirect: to.fullPath 99 | } 100 | }); 101 | } 102 | } 103 | console.log(next); 104 | next(); 105 | }); 106 | 107 | app.mount('#app') 108 | }); 109 | -------------------------------------------------------------------------------- /source/website/src/router.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import Vue from 'vue' 5 | import VueRouter from 'vue-router' 6 | import Analysis from '@/views/Analysis.vue' 7 | import Upload from '@/views/UploadToAWSS3.vue' 8 | import Collection from '@/views/Collection.vue' 9 | import Login from '@/views/Login.vue' 10 | 11 | Vue.use(VueRouter); 12 | 13 | const routes = [ 14 | { 15 | path: '/collection', 16 | name: 'collection', 17 | component: Collection, 18 | meta: { requiresAuth: true } 19 | }, 20 | { 21 | path: '/upload', 22 | name: 'upload', 23 | component: Upload, 24 | meta: { requiresAuth: true } 25 | }, 26 | { 27 | path: '/analysis/:asset_id', 28 | name: 'analysis', 29 | component: Analysis, 30 | meta: { requiresAuth: true } 31 | }, 32 | { 33 | path: "/", 34 | name: "Login", 35 | component: Login, 36 | meta: { requiresAuth: false }, 37 | } 38 | ] 39 | 40 | const router = new VueRouter({ 41 | mode: 'history', 42 | routes, 43 | }); 44 | 45 | export default router; 46 | -------------------------------------------------------------------------------- /source/website/src/services/urlsigner.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import axios from "axios"; 5 | 6 | export default { 7 | getSignedURL(file, config) { 8 | return new Promise((resolve, reject) => { 9 | const token = config.token; 10 | let request = new XMLHttpRequest(), 11 | signingURL = (typeof config.signingURL === "function") ? config.signingURL(file) : config.signingURL; 12 | // console.log('signing URL: ', signingURL) 13 | request.open("POST", signingURL); 14 | request.setRequestHeader("Content-Type", "application/json"); 15 | request.setRequestHeader("Authorization", token); 16 | // console.log(token) 17 | request.onload = function () { 18 | if (request.status === 200) { 19 | resolve(JSON.parse(request.response)); 20 | } else { 21 | reject((request.statusText)); 22 | } 23 | }; 24 | request.onerror = function (err) { 25 | console.error("Network Error : Could not send request to AWS (Maybe CORS errors)"); 26 | reject(err) 27 | }; 28 | if (config.withCredentials === true) { 29 | request.withCredentials = true; 30 | } 31 | axios.get('/runtimeConfig.json').then(response => { 32 | request.send("{\"S3Bucket\":\""+response.data.DATAPLANE_BUCKET+"\",\"S3Key\":\""+file.name+"\"}"); 33 | }) 34 | }); 35 | }, 36 | sendFile(file, config) { 37 | return this.getSignedURL(file, config) 38 | .then((response) => {return ({'success': true, 'message': response})}); 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /source/website/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/source/website/src/static/favicon.ico -------------------------------------------------------------------------------- /source/website/src/store/actions.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export default { 5 | } 6 | -------------------------------------------------------------------------------- /source/website/src/store/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { createStore as _createStore } from 'vuex' 5 | import state from './state' 6 | import mutations from './mutations' 7 | import actions from './actions' 8 | import createPersistedState from "vuex-persistedstate"; 9 | 10 | 11 | export default new _createStore({ 12 | state, 13 | mutations, 14 | actions, 15 | plugins: [createPersistedState({ 16 | paths: ['execution_history'] 17 | })] 18 | }) 19 | -------------------------------------------------------------------------------- /source/website/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export default { 5 | updateAssetId (state, value) { 6 | state.display_asset_id = value 7 | }, 8 | updatePlayer (state, player) { 9 | state.player = player 10 | }, 11 | updateTimeseries (state, value){ 12 | state.chart_tuples = value 13 | }, 14 | updateCurrentTime (state, value) { 15 | state.current_time = value 16 | }, 17 | updateSelectedLabel (state, value){ 18 | state.selected_label = value 19 | }, 20 | updateExecutedAssets (state, value){ 21 | state.execution_history = value 22 | }, 23 | updateWaveformSeekPosition (state, value){ 24 | state.waveform_seek_position = value 25 | }, 26 | updateOperatorInfo (state, value){ 27 | state.operator_info = value 28 | }, 29 | updateUnsavedCustomVocabularies (state, value){ 30 | state.unsaved_custom_vocabularies = value 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /source/website/src/store/state.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export default { 5 | player: null, 6 | markers: [], 7 | display_asset_id: '', 8 | chart_tuples: [], 9 | selected_label: '', 10 | execution_history: [], 11 | current_time: 0, 12 | waveform_seek_position: null, 13 | operator_info: [], 14 | unsaved_custom_vocabularies: [], 15 | } 16 | -------------------------------------------------------------------------------- /source/website/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 46 | -------------------------------------------------------------------------------- /source/website/vite.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import {defineConfig} from 'vite' 5 | import vue from '@vitejs/plugin-vue' 6 | import { fileURLToPath, URL } from 'url'; 7 | 8 | export default defineConfig({ 9 | define: { 10 | global: {}, 11 | }, 12 | plugins: [ 13 | vue({ 14 | template: { 15 | compilerOptions: { 16 | compatConfig: { 17 | MODE: 2 18 | } 19 | } 20 | } 21 | }) 22 | ], 23 | server: { 24 | port: 3000, 25 | watch: { 26 | }, 27 | }, 28 | optimizeDeps: { 29 | }, 30 | resolve: { 31 | alias: { 32 | '@': fileURLToPath(new URL('./src', import.meta.url)), 33 | 'node_modules': fileURLToPath(new URL('./node_modules', import.meta.url)), 34 | vue: '@vue/compat' 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Media Insights Content Localization Testing - How To 2 | 3 | ### Overview 4 | 5 | Content Localization has the following types of tests: 6 | 7 | 8 | *Unit:* Tests for each of the Python components found in the `source` 9 | directory of the project. Scope is the code itself, checks for 10 | introduction of bugs by syntax errors, white-space issues, correct flow 11 | control and order of execution, etc. 12 | 13 | Unit tests can be run locally without building nor deploying the stack. 14 | 15 | *End to End:* Tests of each functional component of the framework with each other and all dependencies. Scope is the ensure all components work successfully to perform the expected function, e.g. ensure the workflowapi can successfully communicate with the dataplaneapi and successfully complete a workflow 16 | 17 | *These tests require Content Localization on AWS to be deployed.* 18 | 19 | 20 | You can find each of these within the `test` directory of the project 21 | 22 | 23 | ### Unit Tests 24 | 25 | These tests are invoked by running the `run_unit.sh` script in the 26 | `test/unit` directory. The script optionally takes positional arguments 27 | for the components to run the tests against. If one or more positional 28 | arguments are supplied, test for each component specified will run. If 29 | no positional argument is supplied, all tests will run: 30 | 31 | Examples: 32 | 33 | * `./run_unit.sh` 34 | * `./run_unit.sh consumer` 35 | * `./run_unit.sh helper anonymized-data-logger` 36 | 37 | Otherwise it runs all available unit tests when no arguments are passed 38 | 39 | 40 | ### End to End tests 41 | 42 | Before these tests are run, you must have a healthy Content Localization 43 | on AWS and Media Insights on AWS deployment in your AWS account. 44 | 45 | You also need to set the following environment variables: 46 | 47 | * `MIE_REGION` - The AWS region your deployment is in 48 | * `MIE_STACK_NAME` - The name of your Media Insights on AWS CloudFormation stack 49 | * `APP_ENDPOINT` - The URL for the Content Localization web application 50 | * `AWS_ACCESS_KEY_ID` - A valid AWS Access Key 51 | * `AWS_SECRET_ACCESS_KEY` - A valid AWS Secret Access Key 52 | * `APP_USERNAME` - A valid username to use to log in to the web application 53 | * `APP_PASSWORD` - web application password for the username 54 | 55 | If you are using temporary STS credentials, you also need to set the session token: 56 | 57 | * `AWS_SESSION_TOKEN` - For use with STS temporary credentials 58 | 59 | *Note, the IAM credentials you specify must belong to an IAM principal that 60 | has administrator permissions on the Media Insights on AWS API's.* 61 | 62 | These tests are invoked by running the `run_e2e.sh` script in the `test/e2e` directory. 63 | 64 | 65 | ### Debugging the selenium e2e tests 66 | 67 | Here are some debug tips to diagnose failing tests and help with extending them to new features. The tests are implemented using the [Selenium Chrome webdriver for python](https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.chrome.webdriver). Refer to these docs for different capabilities for this utility. 68 | 69 | #### Skip workflow setup for tests 70 | 71 | There are a number of pytest fixtures that are used to setup the AWS resources and workflow that are required for the e2e tests. The workflows that are created take 10-15 minutes to setup. When debugging it can be useful to reuse the workflow from a previous test run. You need to be careful that the test you are working with won't modify the workflow in non-deterministic ways. For the existing tests, the workflow is not modified. 72 | 73 | To skip workflow creating for individual test, you can use the environment variable before running the test: 74 | 75 | * `USE_EXISTING_WORKFLOW` - when this environment variable is set, the pytest fixtures to create a content localization workflow is skipped 76 | 77 | #### View the test execution in the browser 78 | 79 | The selenium driver is setup to run in headless mode. You can disable headless mode by commenting out the chrome driver setting in the test case you are running: 80 | 81 | ``` 82 | #chrome_options.add_argument("--headless") 83 | ``` 84 | 85 | ### Coverage 86 | 87 | #### test\_app.py 88 | 89 | This test checks the web app pages created for a workflow. It clicks through the pages, clicks on form elements and fills in inputs. It doesn't modify the workflow result in any way. 90 | 91 | #### test\_reprocess.py 92 | 93 | This test checks the reprocessing functions of the web app. It modifies the subtitles output by the workflow and saves the edits so the workflow will reprocess the downstream assets. 94 | -------------------------------------------------------------------------------- /test/e2e/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==7.2.0 2 | requests==2.32.3 3 | requests-aws4auth==1.2.2 4 | webdriver-manager==3.4.2 5 | selenium==4.14.0 6 | boto3==1.26.17 7 | -------------------------------------------------------------------------------- /test/e2e/test_workflow_reprocess.py: -------------------------------------------------------------------------------- 1 | ###################################################################################################################### 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # 5 | # with the License. A copy of the License is located at # 6 | # # 7 | # http://www.apache.org/licenses/LICENSE-2.0 # 8 | # # 9 | # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # 10 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # 11 | # and limitations under the License. # 12 | ###################################################################################################################### 13 | import pytest 14 | import time 15 | from webdriver_manager.chrome import ChromeDriverManager 16 | from selenium import webdriver 17 | from selenium.webdriver.common.keys import Keys 18 | from selenium.webdriver.chrome.options import Options 19 | from selenium.webdriver.support.ui import Select 20 | from selenium.webdriver.support import expected_conditions as EC 21 | from selenium.webdriver.support.ui import WebDriverWait 22 | from selenium.webdriver.common.by import By 23 | 24 | 25 | @pytest.fixture 26 | def browser(): 27 | chrome_options = Options() 28 | 29 | ####### TESTING - remove headless to see browser actions 30 | chrome_options.add_argument("--headless") 31 | chrome_options.add_argument("--window-size=1920,1080") 32 | chrome_options.add_argument("--start-maximized") 33 | ####### TESTING - remove headless to see browser actions 34 | 35 | browser = webdriver.Chrome(ChromeDriverManager().install(), chrome_options=chrome_options) 36 | yield browser 37 | browser.quit() 38 | 39 | 40 | # Test the happy path through the app by loading and verifying data after a successful workflow run. No 41 | # CRUD interactions such as creating vocabularies are included here 42 | def test_workflow_reprocess(browser, workflow_to_modify, testing_env_variables): 43 | #### TESTING - workflow is already created 44 | # def test_complete_app(browser, testing_env_variables): 45 | #### TESTING - workflow is already created 46 | 47 | print(workflow_to_modify) 48 | 49 | browser.implicitly_wait(5) 50 | browser.get(testing_env_variables['APP_ENDPOINT']) 51 | 52 | ####### Login 53 | 54 | username_field = browser.find_element_by_xpath("/html/body/div[1]/div/div/div/div[2]/div[1]/div/input") 55 | username_field.send_keys(testing_env_variables['APP_USERNAME']) 56 | password_field = browser.find_element_by_xpath("/html/body/div[1]/div/div/div/div[2]/div[2]/input") 57 | password_field.send_keys(testing_env_variables['APP_PASSWORD']) 58 | browser.find_element_by_xpath("/html/body/div[1]/div/div/div/div[3]/span[1]/button").click() 59 | 60 | time.sleep(20) 61 | 62 | # Analyze view 63 | browser.find_element_by_xpath( 64 | "/html/body/div/div/div[2]/div/div/div/div/div[1]/div/div/table/tbody/tr[1]/td[6]/a[1]").click() 65 | # Speech recognition is the default tab 66 | time.sleep(5) 67 | 68 | ####### SUBTITLES COMPONENT 69 | # Navigate to subtitles and wait for them to load 70 | # browser.find_elements_by_link_text("Subtitles")[0].click() 71 | browser.find_element_by_xpath( 72 | "/html/body/div/div/div[2]/div/div[1]/div[1]/div/div/div[2]/div[1]/div/div[1]/ul/li[2]/a").click() 73 | wait = WebDriverWait(browser, 120) 74 | wait.until(EC.presence_of_element_located((By.ID, "caption0"))) 75 | 76 | # Check a subtitle 77 | subtitle1 = browser.find_element_by_xpath( 78 | "/html/body/div[1]/div/div[2]/div/div[1]/div[2]/div/div/div/div/table/tbody/tr[1]/td[2]/div/div/div[1]/textarea") 79 | subtitle1_text = subtitle1.get_attribute("value") 80 | assert "Boulder" in subtitle1_text 81 | 82 | # Edit a subtitle 83 | time.sleep(5) 84 | subtitle1.send_keys("\ue003\ue003\ue003\ue003\ue003\ue003\ue003\ue003 00BOULDEREPLACEDBYSOURCELANGUAGEEDITS00") 85 | 86 | # Save edits 87 | time.sleep(5) 88 | browser.find_element_by_xpath("/html/body/div/div/div[2]/div/div[1]/div[2]/div/div/button[2]").click() 89 | time.sleep(5) 90 | # Confirm Save Edits 91 | # browser.find_elements_by_link_text("Confirm")[0].click() 92 | 93 | wait = WebDriverWait(browser, 120) 94 | wait.until(EC.element_to_be_clickable((By.XPATH, "/html/body/div[2]/div[1]/div/div/footer/button[2]"))) 95 | browser.find_element_by_xpath("/html/body/div[2]/div[1]/div/div/footer/button[2]").click() 96 | time.sleep(10) 97 | 98 | # Check that the workflow has started 99 | # Collection 100 | browser.find_element_by_xpath("/html/body/div/div/div[1]/nav/div/ul/li[2]/a").click() 101 | # Status should be "Started" 102 | time.sleep(5) 103 | workflow_status = browser.find_element_by_xpath( 104 | "/html/body/div/div/div[2]/div/div/div/div/div[1]/div/div/table/tbody/tr[1]/td[3]/a").get_attribute( 105 | "textContent") 106 | assert workflow_status == "Started" 107 | 108 | iterations = 0 109 | 110 | while workflow_status != "Complete" and iterations < 60: 111 | print('Sleeping for 60 seconds before retrying') 112 | iterations = iterations + 1 113 | time.sleep(55) 114 | browser.refresh() 115 | time.sleep(5) 116 | workflow_status = browser.find_element_by_xpath( 117 | "/html/body/div/div/div[2]/div/div/div/div/div[1]/div/div/table/tbody/tr[1]/td[3]/a").get_attribute( 118 | "textContent") 119 | assert workflow_status in ["Started", "Queued", "Complete"] 120 | print(workflow_status) 121 | print(iterations) 122 | 123 | # Check that editing is disabled 124 | # FIXME - ISSUE #131 UI editing and save edits of source language subtitles should be disabled when there is an active workflow on an asset 125 | 126 | # Reload the page and check for the edits 127 | # Analyze view 128 | browser.find_element_by_xpath( 129 | "/html/body/div/div/div[2]/div/div/div/div/div[1]/div/div/table/tbody/tr[1]/td[6]/a[1]").click() 130 | # Speech recognition is the default tab 131 | time.sleep(10) 132 | # Navigate to subtitles 133 | # browser.find_elements_by_link_text("Subtitles")[0].click() 134 | browser.find_element_by_xpath( 135 | "/html/body/div/div/div[2]/div/div[1]/div[1]/div/div/div[2]/div[1]/div/div[1]/ul/li[2]/a").click() 136 | wait = WebDriverWait(browser, 120) 137 | wait.until(EC.presence_of_element_located((By.ID, "caption0"))) 138 | 139 | # FIXME - ISSUE #132 Edits are not applied after workflow reprocess 140 | # subtitle1 = browser.find_element_by_xpath("/html/body/div[1]/div/div[2]/div/div[1]/div[2]/div/div/div/div/table/tbody/tr[1]/td[2]/div/div/div[1]/textarea") 141 | # subtitle1_text = subtitle1.get_attribute("value") 142 | # assert "00BOULDEREPLACEDBYSOURCELANGUAGEEDITS00" in subtitle1_text 143 | 144 | # Check for the edits in the Translation 145 | 146 | # Cancel 147 | # browser.find_element_by_xpath('/html/body/div[2]/div[1]/div/div/footer/button[1]').click() 148 | 149 | time.sleep(5) 150 | 151 | ####### TRANSLATE COMPONENT 152 | 153 | # Sign out 154 | browser.find_element_by_xpath("/html/body/div/div/div[1]/nav/div/ul/li[4]/a/p").click() 155 | -------------------------------------------------------------------------------- /test/test-media/sample-audio.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/test-media/sample-audio.m4a -------------------------------------------------------------------------------- /test/test-media/sample-data.json: -------------------------------------------------------------------------------- 1 | {"GenericItems": [{"GenericItem": {"Confidence": 0.5743707418441772, "Instances": [{"BoundingBox": {"Height": 152.0, "Left": 878.0, "Top": 567.0, "Width": 213.0}, "Confidence": 0.5717031359672546}, {"BoundingBox": {"Height": 159.0, "Left": 23.0, "Top": 560.0, "Width": 242.0}, "Confidence": 0.5743707418441772}, {"BoundingBox": {"Height": 81.0, "Left": 669.0, "Top": 514.0, "Width": 91.0}, "Confidence": 0.5537793040275574}, {"BoundingBox": {"Height": 18.0, "Left": 606.0, "Top": 440.0, "Width": 44.0}, "Confidence": 0.5329275727272034}], "Name": "brand one"}, "Timestamp": 2433000}, {"GenericItem": {"Confidence": 0.4665040671825409, "Instances": [{"BoundingBox": {"Height": 263.0, "Left": 341.0, "Top": 40.0, "Width": 519.0}, "Confidence": 0.4606759250164032}, {"BoundingBox": {"Height": 86.0, "Left": 332.0, "Top": 453.0, "Width": 125.0}, "Confidence": 0.4665040671825409}], "Name": "brand three"}, "Timestamp": 2433000}, {"GenericItem": {"Confidence": 0.5303182005882263, "Instances": [{"BoundingBox": {"Height": 44.0, "Left": 358.0, "Top": 469.0, "Width": 37.0}, "Confidence": 0.5303182005882263}], "Name": "brand two"}, "Timestamp": 2433000}, {"GenericItem": {"Confidence": 0.5772619843482971, "Instances": [{"BoundingBox": {"Height": 157.0, "Left": 891.0, "Top": 562.0, "Width": 180.0}, "Confidence": 0.5489823222160339}, {"BoundingBox": {"Height": 156.0, "Left": 20.0, "Top": 562.0, "Width": 227.0}, "Confidence": 0.5772619843482971}, {"BoundingBox": {"Height": 19.0, "Left": 592.0, "Top": 439.0, "Width": 46.0}, "Confidence": 0.5321167707443237}, {"BoundingBox": {"Height": 83.0, "Left": 666.0, "Top": 500.0, "Width": 96.0}, "Confidence": 0.5650003552436829}], "Name": "brand one"}, "Timestamp": 2063000}, {"GenericItem": {"Confidence": 0.5786570310592651, "Instances": [{"BoundingBox": {"Height": 159.0, "Left": 891.0, "Top": 561.0, "Width": 184.0}, "Confidence": 0.5599979758262634}, {"BoundingBox": {"Height": 158.0, "Left": 14.0, "Top": 561.0, "Width": 233.0}, "Confidence": 0.5763710737228394}, {"BoundingBox": {"Height": 84.0, "Left": 665.0, "Top": 499.0, "Width": 98.0}, "Confidence": 0.5786570310592651}, {"BoundingBox": {"Height": 18.0, "Left": 594.0, "Top": 439.0, "Width": 43.0}, "Confidence": 0.5239437818527222}], "Name": "brand one"}, "Timestamp": 2126000}]} 2 | -------------------------------------------------------------------------------- /test/test-media/sample-face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/test-media/sample-face.jpg -------------------------------------------------------------------------------- /test/test-media/sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/test-media/sample-image.jpg -------------------------------------------------------------------------------- /test/test-media/sample-text.txt: -------------------------------------------------------------------------------- 1 | The quick brown fox jumped over the lazy dog. 2 | -------------------------------------------------------------------------------- /test/test-media/sample-video-tiny.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/test-media/sample-video-tiny.mp4 -------------------------------------------------------------------------------- /test/test-media/sample-video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/test-media/sample-video.mp4 -------------------------------------------------------------------------------- /test/test-media/uitestvocabulary: -------------------------------------------------------------------------------- 1 | Phrase SoundsLike IPA DisplayAs 2 | JEFF-STEEN CUSTOM VOCABULARY REPLACED JEFF STEEN -------------------------------------------------------------------------------- /test/unit/anonymous-data-logger/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def mock_env_variables(monkeypatch): 9 | monkeypatch.syspath_prepend('../../source/anonymized-data-logger/') 10 | -------------------------------------------------------------------------------- /test/unit/anonymous-data-logger/lib/test_cfnresponse.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from unittest.mock import MagicMock 5 | import unittest 6 | import requests 7 | import json 8 | 9 | 10 | class Context(): 11 | log_stream_name = '' 12 | 13 | def __init__(self, log_stream_name='testLogStreamName'): 14 | self.log_stream_name = log_stream_name 15 | 16 | 17 | class TestCfnResponse(unittest.TestCase): 18 | def setUp(self): 19 | # mocks 20 | self.requests = requests.put 21 | requests.put = MagicMock(return_value={'reason': 'test_reason'}) 22 | 23 | def tearDown(self): 24 | requests.put = self.requests 25 | 26 | def test_send_success(self): 27 | # imports 28 | from lib import cfnresponse 29 | 30 | # test parameters 31 | event_param = { 32 | 'ResponseURL': 'testResponseURL', 33 | 'StackId': 'testStackId', 34 | 'RequestId': 'testRequestId', 35 | 'LogicalResourceId': 'testLogicalResourceId' 36 | } 37 | context_param = Context() 38 | response_status_param = 'testResponseStatus' 39 | response_data_param = 'testResponseData' 40 | physical_resource_id_param = 'testPhysicalResourceId' 41 | no_echo_param = 'testNoEchoParam' 42 | 43 | # expected responses 44 | expected_response_body = json.dumps({ 45 | 'Status': response_status_param, 46 | 'Reason': 'See the details in CloudWatch Log Stream: ' + context_param.log_stream_name, 47 | 'PhysicalResourceId': physical_resource_id_param, 48 | 'StackId': event_param['StackId'], 49 | 'RequestId': event_param['RequestId'], 50 | 'LogicalResourceId': event_param['LogicalResourceId'], 51 | 'NoEcho': no_echo_param, 52 | 'Data': response_data_param 53 | }) 54 | 55 | cfnresponse.send( 56 | event_param, 57 | context_param, 58 | response_status_param, 59 | response_data_param, 60 | physical_resource_id_param, 61 | no_echo_param 62 | ) 63 | 64 | # assertions 65 | assert requests.put.call_count == 1 66 | assert requests.put.call_args[0][0] == event_param['ResponseURL'] 67 | assert requests.put.call_args[1]['data'] == expected_response_body 68 | assert requests.put.call_args[1]['headers'] == { 69 | 'content-type': '', 70 | 'content-length': str(len(expected_response_body)) 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/anonymous-data-logger/lib/test_metrics.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import unittest 5 | import urllib.request as request 6 | from unittest.mock import MagicMock 7 | import json 8 | 9 | 10 | class MockUrlResponse(): 11 | def getcode(self): 12 | return 'testCode' 13 | 14 | 15 | class TestMetrics(unittest.TestCase): 16 | def setUp(self): 17 | self.request = request.Request 18 | self.urlopen = request.urlopen 19 | 20 | # mocks 21 | request.Request = MagicMock(return_value='testValue') 22 | request.urlopen = MagicMock(return_value=MockUrlResponse()) 23 | 24 | def tearDown(self) -> None: 25 | request.Request = self.request 26 | request.urlopen = self.urlopen 27 | return super().tearDownClass() 28 | 29 | def test_send_metrics(self): 30 | from lib import metrics 31 | 32 | # test parameters 33 | config_param = { 34 | 'SolutionId': 'testSolutionId', 35 | 'UUID': 'testUUID' 36 | } 37 | 38 | # expected values 39 | expected_data = { 40 | 'Solution': 'testSolutionId', 41 | 'UUID': 'testUUID', 42 | 'Data': {} 43 | } 44 | 45 | metrics.send_metrics(config_param) 46 | 47 | # assertions 48 | assert request.Request.call_count == 1 49 | assert request.Request.call_args[0][0] == 'https://metrics.awssolutionsbuilder.com/generic' 50 | parsed_args = json.loads(request.Request.call_args[0][1].decode()) 51 | del parsed_args['TimeStamp'] 52 | assert parsed_args == expected_data 53 | assert request.Request.call_args[0][2] == {'content-type': 'application/json'} 54 | 55 | assert request.urlopen.call_count == 1 56 | assert request.urlopen.call_args[0][0] == 'testValue' 57 | -------------------------------------------------------------------------------- /test/unit/consumer/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import pytest 5 | import glob 6 | import os 7 | from unittest.mock import create_autospec, patch 8 | from botocore.stub import Stubber 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def mock_env_variables(monkeypatch): 13 | """Mock up environment variables that the testing target depends on""" 14 | monkeypatch.syspath_prepend('../../source/') 15 | monkeypatch.setenv("DataplaneBucket", 'testDataplaneBucket') 16 | monkeypatch.setenv("EsEndpoint", 'testSearchEndpoint') 17 | monkeypatch.setenv("botoConfig", '{"user_agent_extra": "AwsSolution/SO0164/2.0.4"}') 18 | monkeypatch.setenv('AWS_XRAY_CONTEXT_MISSING', 'LOG_ERROR') 19 | monkeypatch.setenv('AWS_REGION', 'us-west-2') 20 | monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") 21 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test") 22 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "test") 23 | monkeypatch.setenv("AWS_SESSION_TOKEN", "test") 24 | 25 | 26 | @pytest.fixture 27 | def s3_client_stub(mock_env_variables): 28 | """Activate a Stubber for the s3 client used by the target module. 29 | 30 | https://botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html 31 | 32 | Yields the Stubber instance. 33 | """ 34 | import consumer.lambda_handler as app 35 | with Stubber(app.s3) as stubber: 36 | yield stubber 37 | stubber.assert_no_pending_responses() 38 | 39 | 40 | @pytest.fixture 41 | def elasticsearch_stub(mock_env_variables): 42 | """Create auto-spec Mock for `Elasticsearch` and yield the Mock. 43 | 44 | Also, reduce MAC_BULK_INDEX_PAYLOAD_SIZE to 2000000 for testing. 45 | """ 46 | import consumer.lambda_handler as app 47 | es = app.Elasticsearch 48 | bulk_size = app.MAX_BULK_INDEX_PAYLOAD_SIZE 49 | app.MAX_BULK_INDEX_PAYLOAD_SIZE = 2000000 50 | wrapper = create_autospec(es) 51 | app.Elasticsearch = wrapper 52 | try: 53 | yield wrapper 54 | finally: 55 | app.Elasticsearch = es 56 | app.MAX_BULK_INDEX_PAYLOAD_SIZE = bulk_size 57 | 58 | 59 | @pytest.fixture 60 | def index_document_stub(): 61 | """Patch the `index_document` function by replacing it with an auto-spec Mock.""" 62 | with patch('consumer.lambda_handler.index_document', autospec=True, spec_set=True) as stub: 63 | yield stub 64 | 65 | 66 | # Find all JSON files under at ./operators/*.json 67 | # The files represent the S3 objects pointed to by the modify operator. 68 | # Any JSON files found in that directory will be assumed to represent a test case. 69 | @pytest.fixture(params=[ 70 | # Just take the base name. 71 | os.path.basename(p) 72 | # Look for .json and .json.gz in case we have a large test case that requires compression. 73 | for ext in ['*.json', '*.json.gz'] 74 | # Find each of the files by joining this source files's directory to the sub-directory. 75 | for p in glob.glob(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'operators', ext)) 76 | ]) 77 | def modify_operator_data(request): 78 | """Finds all data files representing operators and yields a tuple[str, str]. 79 | 80 | * The first element of the tuple is the operator name. 81 | * The second element of the tuple is the full file path of the JSON data file. 82 | 83 | This fixture causes the test case to run once for each operator found. 84 | """ 85 | # Reconstruct the full path of the file 86 | full_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'operators', request.param) 87 | # Trim off the extension (i.e., .json or .json.gz) 88 | op_name = request.param 89 | ext = 'ext' 90 | while ext not in ['.json', '']: 91 | op_name, ext = os.path.splitext(op_name) 92 | # return the name of the operator and the full path off the file 93 | return op_name, full_path 94 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/ContentModeration.json: -------------------------------------------------------------------------------- 1 | { 2 | "ModerationLabels": [ 3 | { 4 | "ModerationLabel": { "Error": "Intentionally missing expected properties" } 5 | }, 6 | { 7 | "ModerationLabel": { 8 | "Name": "testName", 9 | "ParentName": "parentName", 10 | "Confidence": 50 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/GenericDataLookup.json: -------------------------------------------------------------------------------- 1 | { 2 | "Labels": [ 3 | { 4 | "Label": { "Error": "Intentionally missing expected properties" } 5 | }, 6 | { 7 | "Label": { 8 | "Confidence": 50, 9 | "Name": "testName", 10 | "Instances": [{ 11 | "BoundingBox": { 12 | "Width": "0.36959773302078247", 13 | "Height": "0.6934193968772888", 14 | "Left": "0.3121304512023926", 15 | "Top": "0.17156612873077393" 16 | }, 17 | "Confidence": "79.54973602294922" 18 | }], 19 | "Parents": [] 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/TranscribeAudio.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobName": "transcribe-55555555-aaaa-bbbb-cccc-666666666666", 3 | "results": { 4 | "transcripts": [ 5 | { 6 | "transcript": "Welcome back to Boulder. It's a show about everything local from the farm to the Table." 7 | } 8 | ], 9 | "items": [ 10 | { "start_time": "0.54", "end_time": "1.19", "alternatives": [ { "confidence": "1.0", "content": "Welcome" } ], "type": "pronunciation" }, 11 | { "start_time": "1.19", "end_time": "1.38", "alternatives": [ { "confidence": "1.0", "content": "back" } ], "type": "pronunciation" }, 12 | { "start_time": "1.38", "end_time": "1.48", "alternatives": [ { "confidence": "1.0", "content": "to" } ], "type": "pronunciation" }, 13 | { "start_time": "1.48", "end_time": "1.82", "alternatives": [ { "confidence": "0.9982", "content": "Boulder" } ], "type": "pronunciation" }, 14 | { "alternatives": [ { "confidence": "0.0", "content": "." } ], "type": "punctuation" }, 15 | { "start_time": "1.82", "end_time": "2.07", "alternatives": [ { "confidence": "0.9797", "content": "It's" } ], "type": "pronunciation" }, 16 | { "start_time": "2.08", "end_time": "2.37", "alternatives": [ { "confidence": "1.0", "content": "a" } ], "type": "pronunciation" }, 17 | { "start_time": "2.37", "end_time": "2.59", "alternatives": [ { "confidence": "1.0", "content": "show" } ], "type": "pronunciation" }, 18 | { "start_time": "2.59", "end_time": "2.94", "alternatives": [ { "confidence": "1.0", "content": "about" } ], "type": "pronunciation" }, 19 | { "start_time": "2.95", "end_time": "3.45", "alternatives": [ { "confidence": "0.9989", "content": "everything" } ], "type": "pronunciation" }, 20 | { "start_time": "3.45", "end_time": "3.91", "alternatives": [ { "confidence": "0.9987", "content": "local" } ], "type": "pronunciation" }, 21 | { "start_time": "3.91", "end_time": "4.15", "alternatives": [ { "confidence": "1.0", "content": "from" } ], "type": "pronunciation" }, 22 | { "start_time": "4.15", "end_time": "4.24", "alternatives": [ { "confidence": "1.0", "content": "the" } ], "type": "pronunciation" }, 23 | { "start_time": "4.25", "end_time": "4.68", "alternatives": [ { "confidence": "1.0", "content": "farm" } ], "type": "pronunciation" }, 24 | { "start_time": "4.68", "end_time": "4.79", "alternatives": [ { "confidence": "1.0", "content": "to" } ], "type": "pronunciation" }, 25 | { "start_time": "4.79", "end_time": "4.9", "alternatives": [ { "confidence": "1.0", "content": "the" } ], "type": "pronunciation" }, 26 | { "start_time": "4.9", "end_time": "5.35", "alternatives": [ { "confidence": "1.0", "content": "Table" } ], "type": "pronunciation" }, 27 | { "alternatives": [ { "confidence": "0.0", "content": "." } ], "type": "punctuation" } 28 | ] 29 | }, 30 | "status": "COMPLETED", 31 | "TextTranscriptUri": { 32 | "S3Bucket": "testDataplaneBucket", 33 | "S3Key": "private/assets/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/workflows/11111111-2222-3333-4444-555555555555/transcript.txt" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/TranscribeVideo.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobName": "transcribe-55555555-aaaa-bbbb-cccc-666666666666", 3 | "results": { 4 | "transcripts": [ 5 | { 6 | "transcript": "Welcome back to Boulder. It's a show about everything local from the farm to the Table." 7 | } 8 | ], 9 | "items": [ 10 | { "start_time": "0.54", "end_time": "1.19", "alternatives": [ { "confidence": "1.0", "content": "Welcome" } ], "type": "pronunciation" }, 11 | { "start_time": "1.19", "end_time": "1.38", "alternatives": [ { "confidence": "1.0", "content": "back" } ], "type": "pronunciation" }, 12 | { "start_time": "1.38", "end_time": "1.48", "alternatives": [ { "confidence": "1.0", "content": "to" } ], "type": "pronunciation" }, 13 | { "start_time": "1.48", "end_time": "1.82", "alternatives": [ { "confidence": "0.9982", "content": "Boulder" } ], "type": "pronunciation" }, 14 | { "alternatives": [ { "confidence": "0.0", "content": "." } ], "type": "punctuation" }, 15 | { "start_time": "1.82", "end_time": "2.07", "alternatives": [ { "confidence": "0.9797", "content": "It's" } ], "type": "pronunciation" }, 16 | { "start_time": "2.08", "end_time": "2.37", "alternatives": [ { "confidence": "1.0", "content": "a" } ], "type": "pronunciation" }, 17 | { "start_time": "2.37", "end_time": "2.59", "alternatives": [ { "confidence": "1.0", "content": "show" } ], "type": "pronunciation" }, 18 | { "start_time": "2.59", "end_time": "2.94", "alternatives": [ { "confidence": "1.0", "content": "about" } ], "type": "pronunciation" }, 19 | { "start_time": "2.95", "end_time": "3.45", "alternatives": [ { "confidence": "0.9989", "content": "everything" } ], "type": "pronunciation" }, 20 | { "start_time": "3.45", "end_time": "3.91", "alternatives": [ { "confidence": "0.9987", "content": "local" } ], "type": "pronunciation" }, 21 | { "start_time": "3.91", "end_time": "4.15", "alternatives": [ { "confidence": "1.0", "content": "from" } ], "type": "pronunciation" }, 22 | { "start_time": "4.15", "end_time": "4.24", "alternatives": [ { "confidence": "1.0", "content": "the" } ], "type": "pronunciation" }, 23 | { "start_time": "4.25", "end_time": "4.68", "alternatives": [ { "confidence": "1.0", "content": "farm" } ], "type": "pronunciation" }, 24 | { "start_time": "4.68", "end_time": "4.79", "alternatives": [ { "confidence": "1.0", "content": "to" } ], "type": "pronunciation" }, 25 | { "start_time": "4.79", "end_time": "4.9", "alternatives": [ { "confidence": "1.0", "content": "the" } ], "type": "pronunciation" }, 26 | { "start_time": "4.9", "end_time": "5.35", "alternatives": [ { "confidence": "1.0", "content": "Table" } ], "type": "pronunciation" }, 27 | { "alternatives": [ { "confidence": "0.0", "content": "." } ], "type": "punctuation" } 28 | ] 29 | }, 30 | "status": "COMPLETED", 31 | "TextTranscriptUri": { 32 | "S3Bucket": "testDataplaneBucket", 33 | "S3Key": "private/assets/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/workflows/11111111-2222-3333-4444-555555555555/transcript.txt" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/Translate.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/WebCaptions_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "WebCaptions": [ 3 | { 4 | "start": "0.54", 5 | "caption": "Welcome back to 00BOULDEREPLACEDBYSOURCELANGUAGEEDITS00", 6 | "wordConfidence": [ 7 | { "w": "welcome", "c": "1.0" }, 8 | { "w": "back", "c": "1.0" }, 9 | { "w": "to", "c": "1.0" }, 10 | { "w": "boulder", "c": "0.9982" } 11 | ], 12 | "end": "1.82" 13 | }, 14 | { 15 | "start": "1.82", 16 | "caption": "It's a show about everything local from the farm to the Table.", 17 | "wordConfidence": [ 18 | { "w": "it's", "c": "0.9797" }, 19 | { "w": "a", "c": "1.0" }, 20 | { "w": "show", "c": "1.0" }, 21 | { "w": "about", "c": "1.0" }, 22 | { "w": "everything", "c": "0.9989" }, 23 | { "w": "local", "c": "0.9987" }, 24 | { "w": "from", "c": "1.0" }, 25 | { "w": "the", "c": "1.0" }, 26 | { "w": "farm", "c": "1.0" }, 27 | { "w": "to", "c": "1.0" }, 28 | { "w": "the", "c": "1.0" }, 29 | { "w": "table", "c": "1.0" } 30 | ], 31 | "end": "5.35" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/WebCaptions_es.json: -------------------------------------------------------------------------------- 1 | { 2 | "WebCaptions": [ 3 | { 4 | "start": "0.54", 5 | "end": "1.82", 6 | "caption": "Bienvenido de nuevo a 00BOULDEREPLACEDBYSOURCELANGUAGEEDITS00", 7 | "sourceCaption": "Welcome back to 00BOULDEREPLACEDBYSOURCELANGUAGEEDITS00" 8 | }, 9 | { 10 | "start": "1.82", 11 | "end": "5.35", 12 | "caption": " Es un programa sobre todo lo local, desde la granja hasta la mesa. ", 13 | "sourceCaption": "It's a show about everything local from the farm to the Table." 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/celebrityRecognition.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobStatus": "SUCCEEDED", 3 | "VideoMetadata": { 4 | "Codec": "h264", 5 | "DurationMillis": 180066, 6 | "Format": "QuickTime / MOV", 7 | "FrameRate": "30.0", 8 | "FrameHeight": 540, 9 | "FrameWidth": 960, 10 | "ColorRange": "LIMITED" 11 | }, 12 | "Celebrities": [ 13 | { 14 | "Timestamp": 59500, 15 | "Celebrity": { 16 | "Urls": [ 17 | "www.wikidata.org/wiki/Q22004103" 18 | ], 19 | "Name": "Laura Dotson", 20 | "Id": "Cy0co9x", 21 | "Confidence": "77.20094299316406", 22 | "Face": { 23 | "BoundingBox": { 24 | "Width": "0.061153411865234375", 25 | "Height": "0.1457747220993042", 26 | "Left": "0.651340901851654", 27 | "Top": "0.34650906920433044" 28 | }, 29 | "Landmarks": [ 30 | { "Type": "eyeLeft", "X": "0.6635822057723999", "Y": "0.4095280170440674" }, 31 | { "Type": "eyeRight", "X": "0.6910726428031921", "Y": "0.40344157814979553" }, 32 | { "Type": "mouthLeft", "X": "0.6698732972145081", "Y": "0.4566854238510132" }, 33 | { "Type": "mouthRight", "X": "0.6928272247314453", "Y": "0.45171236991882324" }, 34 | { "Type": "nose", "X": "0.6762213110923767", "Y": "0.43156084418296814" } 35 | ], 36 | "Pose": { 37 | "Roll": "-8.534097671508789", 38 | "Yaw": "-6.502280235290527", 39 | "Pitch": "8.88282585144043" 40 | }, 41 | "Quality": { 42 | "Brightness": "66.57852172851562", 43 | "Sharpness": "38.89601135253906" 44 | }, 45 | "Confidence": "99.99699401855469" 46 | }, 47 | "KnownGender": { 48 | "Type": "Female" 49 | } 50 | } 51 | }, 52 | { 53 | "Timestamp": 60000, 54 | "Celebrity": { 55 | "Urls": [ 56 | "www.wikidata.org/wiki/Q22004103" 57 | ], 58 | "Name": "Laura Dotson", 59 | "Id": "Cy0co9x", 60 | "Confidence": "79.72498321533203", 61 | "Face": { 62 | "BoundingBox": { 63 | "Width": "0.06206149980425835", 64 | "Height": "0.14381197094917297", 65 | "Left": "0.6475560069084167", 66 | "Top": "0.3477523624897003" 67 | }, 68 | "Landmarks": [ 69 | { "Type": "eyeLeft", "X": "0.6605901122093201", "Y": "0.4091332256793976" }, 70 | { "Type": "eyeRight", "X": "0.688330352306366", "Y": "0.40328481793403625" }, 71 | { "Type": "mouthLeft", "X": "0.6670447587966919", "Y": "0.4561467468738556" }, 72 | { "Type": "mouthRight", "X": "0.690250813961029", "Y": "0.4514188766479492" }, 73 | { "Type": "nose", "X": "0.6730112433433533", "Y": "0.43235015869140625" } 74 | ], 75 | "Pose": { 76 | "Roll": "-9.182001113891602", 77 | "Yaw": "-7.564344882965088", 78 | "Pitch": "8.211339950561523" 79 | }, 80 | "Quality": { 81 | "Brightness": "66.87061309814453", 82 | "Sharpness": "46.02980041503906" 83 | }, 84 | "Confidence": "99.99813842773438" 85 | }, 86 | "KnownGender": { 87 | "Type": "Female" 88 | } 89 | } 90 | } 91 | ], 92 | "ResponseMetadata": { 93 | "RequestId": "0c25a889-4e86-4e5a-bb04-135aafba4e77", 94 | "HTTPStatusCode": 200, 95 | "HTTPHeaders": { 96 | "x-amzn-requestid": "0c25a889-4e86-4e5a-bb04-135aafba4e77", 97 | "content-type": "application/x-amz-json-1.1", 98 | "content-length": "1886", 99 | "date": "Fri, 03 Mar 2023 20:35:21 GMT" 100 | }, 101 | "RetryAttempts": 0 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/entities.json: -------------------------------------------------------------------------------- 1 | {"LanguageCode": "en", "Results": ["{\"Entities\": [{\"BeginOffset\": 16, \"EndOffset\": 23, \"Score\": 0.9904271548248808, \"Text\": \"Boulder\", \"Type\": \"LOCATION\"}, {\"BeginOffset\": 92, \"EndOffset\": 102, \"Score\": 0.9996887864918023, \"Text\": \"Jeff Steen\", \"Type\": \"PERSON\"}, {\"BeginOffset\": 113, \"EndOffset\": 119, \"Score\": 0.7177615758197331, \"Text\": \"couple\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 149, \"EndOffset\": 156, \"Score\": 0.9666753939451227, \"Text\": \"Boulder\", \"Type\": \"LOCATION\"}, {\"BeginOffset\": 212, \"EndOffset\": 216, \"Score\": 0.9684646064150594, \"Text\": \"both\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 240, \"EndOffset\": 247, \"Score\": 0.9869439413856198, \"Text\": \"Boulder\", \"Type\": \"LOCATION\"}, {\"BeginOffset\": 336, \"EndOffset\": 347, \"Score\": 0.914761396815676, \"Text\": \"Fresh Times\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 356, \"EndOffset\": 361, \"Score\": 0.8078227144216096, \"Text\": \"Fresh\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 362, \"EndOffset\": 367, \"Score\": 0.6925835016226661, \"Text\": \"Times\", \"Type\": \"TITLE\"}, {\"BeginOffset\": 391, \"EndOffset\": 400, \"Score\": 0.9982002296436752, \"Text\": \"two years\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 407, \"EndOffset\": 411, \"Score\": 0.9122645227723072, \"Text\": \"2013\", \"Type\": \"DATE\"}, {\"BeginOffset\": 450, \"EndOffset\": 461, \"Score\": 0.963791046914947, \"Text\": \"Table Ethos\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 465, \"EndOffset\": 474, \"Score\": 0.9927348977325562, \"Text\": \"Black Cat\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 479, \"EndOffset\": 495, \"Score\": 0.9325151269082469, \"Text\": \"Taco Dining Hall\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 650, \"EndOffset\": 659, \"Score\": 0.9991590901395899, \"Text\": \"Christine\", \"Type\": \"PERSON\"}, {\"BeginOffset\": 784, \"EndOffset\": 791, \"Score\": 0.84630985314638, \"Text\": \"Alfredo\", \"Type\": \"PERSON\"}, {\"BeginOffset\": 799, \"EndOffset\": 808, \"Score\": 0.99964154697793, \"Text\": \"Christine\", \"Type\": \"PERSON\"}, {\"BeginOffset\": 831, \"EndOffset\": 836, \"Score\": 0.9706104985536407, \"Text\": \"today\", \"Type\": \"DATE\"}, {\"BeginOffset\": 1019, \"EndOffset\": 1033, \"Score\": 0.9935422860138847, \"Text\": \"Bowman College\", \"Type\": \"ORGANIZATION\"}, {\"BeginOffset\": 1485, \"EndOffset\": 1500, \"Score\": 0.9851135301169553, \"Text\": \"about six years\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 1877, \"EndOffset\": 1881, \"Score\": 0.9994427688200107, \"Text\": \"2006\", \"Type\": \"DATE\"}, {\"BeginOffset\": 1904, \"EndOffset\": 1922, \"Score\": 0.6827198470161789, \"Text\": \"multiple sclerosis\", \"Type\": \"OTHER\"}, {\"BeginOffset\": 1990, \"EndOffset\": 1999, \"Score\": 0.995493983114137, \"Text\": \"two parts\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 2015, \"EndOffset\": 2018, \"Score\": 0.9673185781208488, \"Text\": \"One\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 2175, \"EndOffset\": 2188, \"Score\": 0.9804854299709578, \"Text\": \"two diagnoses\", \"Type\": \"QUANTITY\"}, {\"BeginOffset\": 2557, \"EndOffset\": 2567, \"Score\": 0.8280379971116671, \"Text\": \"Bob Munson\", \"Type\": \"PERSON\"}, {\"BeginOffset\": 2781, \"EndOffset\": 2798, \"Score\": 0.9804181968535195, \"Text\": \"a year and a half\", \"Type\": \"QUANTITY\"}], \"File\": \"transcript.txt\"}\n"]} -------------------------------------------------------------------------------- /test/unit/consumer/operators/face_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "Persons": [ 3 | { "Person": { "Index": 0 } }, 4 | { 5 | "Person": { 6 | "Index": 1, 7 | "BoundingBox": { "Width": "0.36959773302078247", "Height": "0.6934193968772888", "Left": "0.3121304512023926", "Top": "0.17156612873077393" }, 8 | "Face": { 9 | "BoundingBox": { "Width": "0.36959773302078247", "Height": "0.6934193968772888", "Left": "0.3121304512023926", "Top": "0.17156612873077393" }, 10 | "Landmarks": "testLandmarks", 11 | "Pose": "testPose", 12 | "Quality": "testQuality", 13 | "Confidence": "79.54973602294922" 14 | } 15 | }, 16 | "FaceMatches": [{ 17 | "Similarity": "testSimilarity", 18 | "Face": { "FaceId": 2, "BoundingBox": {}, "ImageId": "imageId" } 19 | }] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/labelDetection.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/content-localization-on-aws/3ccf6931aba27e9c33f5929e055b488e5c067e42/test/unit/consumer/operators/labelDetection.json.gz -------------------------------------------------------------------------------- /test/unit/consumer/operators/shotDetection.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobStatus": "SUCCEEDED", 3 | "VideoMetadata": [ 4 | { 5 | "Codec": "h264", 6 | "DurationMillis": 180066, 7 | "Format": "QuickTime / MOV", 8 | "FrameRate": "30.0", 9 | "FrameHeight": 540, 10 | "FrameWidth": 960, 11 | "ColorRange": "LIMITED" 12 | } 13 | ], 14 | "AudioMetadata": [ 15 | { 16 | "Codec": "aac", 17 | "DurationMillis": 180020, 18 | "SampleRate": 48000, 19 | "NumberOfChannels": 2 20 | } 21 | ], 22 | "Segments": [ 23 | { 24 | "Type": "SHOT", 25 | "StartTimestampMillis": 0, 26 | "EndTimestampMillis": 1000, 27 | "DurationMillis": 1000, 28 | "StartTimecodeSMPTE": "00:00:00:00", 29 | "EndTimecodeSMPTE": "00:00:01:00", 30 | "DurationSMPTE": "00:00:01:00", 31 | "ShotSegment": { 32 | "Index": 0, 33 | "Confidence": "99.99995422363281" 34 | }, 35 | "StartFrameNumber": 0, 36 | "EndFrameNumber": 30, 37 | "DurationFrames": 30 38 | }, 39 | { 40 | "Type": "SHOT", 41 | "StartTimestampMillis": 1033, 42 | "EndTimestampMillis": 11766, 43 | "DurationMillis": 10733, 44 | "StartTimecodeSMPTE": "00:00:01:01", 45 | "EndTimecodeSMPTE": "00:00:11:23", 46 | "DurationSMPTE": "00:00:10:22", 47 | "ShotSegment": { 48 | "Index": 1, 49 | "Confidence": "76.58549499511719" 50 | }, 51 | "StartFrameNumber": 31, 52 | "EndFrameNumber": 353, 53 | "DurationFrames": 322 54 | }, 55 | { 56 | "Type": "SHOT", 57 | "StartTimestampMillis": 11800, 58 | "EndTimestampMillis": 12000, 59 | "DurationMillis": 200, 60 | "StartTimecodeSMPTE": "00:00:11:24", 61 | "EndTimecodeSMPTE": "00:00:12:00", 62 | "DurationSMPTE": "00:00:00:06", 63 | "ShotSegment": { 64 | "Index": 2, 65 | "Confidence": "76.58549499511719" 66 | }, 67 | "StartFrameNumber": 354, 68 | "EndFrameNumber": 360, 69 | "DurationFrames": 6 70 | }, 71 | { 72 | "Type": "SHOT", 73 | "StartTimestampMillis": 12033, 74 | "EndTimestampMillis": 13800, 75 | "DurationMillis": 1767, 76 | "StartTimecodeSMPTE": "00:00:12:01", 77 | "EndTimecodeSMPTE": "00:00:13:24", 78 | "DurationSMPTE": "00:00:01:23", 79 | "ShotSegment": { 80 | "Index": 3, 81 | "Confidence": "89.5938491821289" 82 | }, 83 | "StartFrameNumber": 361, 84 | "EndFrameNumber": 414, 85 | "DurationFrames": 53 86 | }, 87 | { 88 | "Type": "SHOT", 89 | "StartTimestampMillis": 178033, 90 | "EndTimestampMillis": 180033, 91 | "DurationMillis": 2000, 92 | "StartTimecodeSMPTE": "00:02:58:01", 93 | "EndTimecodeSMPTE": "00:03:00:01", 94 | "DurationSMPTE": "00:00:02:00", 95 | "ShotSegment": { 96 | "Index": 25, 97 | "Confidence": "99.97348022460938" 98 | }, 99 | "StartFrameNumber": 5341, 100 | "EndFrameNumber": 5401, 101 | "DurationFrames": 60 102 | } 103 | ], 104 | "SelectedSegmentTypes": [ 105 | { 106 | "Type": "SHOT", 107 | "ModelVersion": "2.0" 108 | } 109 | ], 110 | "ResponseMetadata": { 111 | "RequestId": "d55cfcfb-f4f6-4b4b-9e00-dd6f973e1c9c", 112 | "HTTPStatusCode": 200, 113 | "HTTPHeaders": { 114 | "x-amzn-requestid": "d55cfcfb-f4f6-4b4b-9e00-dd6f973e1c9c", 115 | "content-type": "application/x-amz-json-1.1", 116 | "content-length": "8537", 117 | "date": "Fri, 03 Mar 2023 20:32:22 GMT" 118 | }, 119 | "RetryAttempts": 0 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/technicalCueDetection.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobStatus": "SUCCEEDED", 3 | "VideoMetadata": [ 4 | { 5 | "Codec": "h264", 6 | "DurationMillis": 180066, 7 | "Format": "QuickTime / MOV", 8 | "FrameRate": "30.0", 9 | "FrameHeight": 540, 10 | "FrameWidth": 960, 11 | "ColorRange": "LIMITED" 12 | } 13 | ], 14 | "AudioMetadata": [ 15 | { 16 | "Codec": "aac", 17 | "DurationMillis": 180020, 18 | "SampleRate": 48000, 19 | "NumberOfChannels": 2 20 | } 21 | ], 22 | "Segments": [ 23 | { 24 | "Type": "TECHNICAL_CUE", 25 | "StartTimestampMillis": 0, 26 | "EndTimestampMillis": 180033, 27 | "DurationMillis": 180033, 28 | "StartTimecodeSMPTE": "00:00:00:00", 29 | "EndTimecodeSMPTE": "00:03:00:01", 30 | "DurationSMPTE": "00:03:00:01", 31 | "TechnicalCueSegment": { 32 | "Type": "Content", 33 | "Confidence": "100.0" 34 | }, 35 | "StartFrameNumber": 0, 36 | "EndFrameNumber": 5401, 37 | "DurationFrames": 5401 38 | } 39 | ], 40 | "SelectedSegmentTypes": [ 41 | { 42 | "Type": "TECHNICAL_CUE", 43 | "ModelVersion": "2.0" 44 | } 45 | ], 46 | "ResponseMetadata": { 47 | "RequestId": "43ccc76d-7a0e-41d0-82c5-0ef6be152157", 48 | "HTTPStatusCode": 200, 49 | "HTTPHeaders": { 50 | "x-amzn-requestid": "43ccc76d-7a0e-41d0-82c5-0ef6be152157", 51 | "content-type": "application/x-amz-json-1.1", 52 | "content-length": "693", 53 | "date": "Fri, 03 Mar 2023 20:32:32 GMT" 54 | }, 55 | "RetryAttempts": 0 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/unit/consumer/operators/textDetection.json: -------------------------------------------------------------------------------- 1 | { 2 | "JobStatus": "SUCCEEDED", 3 | "VideoMetadata": { 4 | "Codec": "h264", 5 | "DurationMillis": 180066, 6 | "Format": "QuickTime / MOV", 7 | "FrameRate": "30.0", 8 | "FrameHeight": 540, 9 | "FrameWidth": 960, 10 | "ColorRange": "LIMITED" 11 | }, 12 | "TextDetections": [ 13 | { 14 | "Timestamp": 2000, 15 | "TextDetection": { 16 | "DetectedText": "eate", 17 | "Type": "LINE", 18 | "Id": 0, 19 | "Confidence": "93.85590362548828", 20 | "Geometry": { 21 | "BoundingBox": { "Width": "0.09284265339374542", "Height": "0.05842217430472374", "Left": "0.738525927066803", "Top": "0.5583003163337708" }, 22 | "Polygon": [ 23 | { "X": "0.7424951195716858", "Y": "0.5583003163337708" }, 24 | { "X": "0.831368625164032", "Y": "0.5651387572288513" }, 25 | { "X": "0.8273994326591492", "Y": "0.6167225241661072" }, 26 | { "X": "0.738525927066803", "Y": "0.6098840832710266" } 27 | ] 28 | } 29 | } 30 | }, 31 | { 32 | "Timestamp": 2000, 33 | "TextDetection": { 34 | "DetectedText": "eate", 35 | "Type": "WORD", 36 | "Id": 1, 37 | "ParentId": 0, 38 | "Confidence": "93.85590362548828", 39 | "Geometry": { 40 | "BoundingBox": { "Width": "0.08893153071403503", "Height": "0.05842217430472374", "Left": "0.7404814958572388", "Top": "0.5583003163337708" }, 41 | "Polygon": [ 42 | { "X": "0.7424951195716858", "Y": "0.5583003163337708" }, 43 | { "X": "0.8294130563735962", "Y": "0.5701865553855896" }, 44 | { "X": "0.8273994326591492", "Y": "0.6167225241661072" }, 45 | { "X": "0.7404814958572388", "Y": "0.6048362851142883" } 46 | ] 47 | } 48 | } 49 | }, 50 | { 51 | "Timestamp": 3000, 52 | "TextDetection": { 53 | "DetectedText": "fr", 54 | "Type": "LINE", 55 | "Id": 0, 56 | "Confidence": "96.13220977783203", 57 | "Geometry": { 58 | "BoundingBox": { "Width": "0.0634494423866272", "Height": "0.11505743116140366", "Left": "0.9403117895126343", "Top": "0.3286333382129669" }, 59 | "Polygon": [ 60 | { "X": "0.9442570805549622", "Y": "0.3286333382129669" }, 61 | { "X": "1.0037611722946167", "Y": "0.3307112753391266" }, 62 | { "X": "0.9998158812522888", "Y": "0.44369077682495117" }, 63 | { "X": "0.9403117895126343", "Y": "0.4416128396987915" } 64 | ] 65 | } 66 | } 67 | }, 68 | { 69 | "Timestamp": 3000, 70 | "TextDetection": { 71 | "DetectedText": "e", 72 | "Type": "LINE", 73 | "Id": 1, 74 | "Confidence": "92.60140991210938", 75 | "Geometry": { 76 | "BoundingBox": { "Width": "0.01153564453125", "Height": "0.021484375", "Left": "0.98712158203125", "Top": "0.537109375" }, 77 | "Polygon": [ 78 | { "X": "0.98712158203125", "Y": "0.537109375" }, 79 | { "X": "0.9986572265625", "Y": "0.537109375" }, 80 | { "X": "0.9986572265625", "Y": "0.55859375" }, 81 | { "X": "0.98712158203125", "Y": "0.55859375" } 82 | ] 83 | } 84 | } 85 | }, 86 | { 87 | "Timestamp": 3000, 88 | "TextDetection": { 89 | "DetectedText": "fr", 90 | "Type": "WORD", 91 | "Id": 2, 92 | "ParentId": 0, 93 | "Confidence": "96.13220977783203", 94 | "Geometry": { 95 | "BoundingBox": { "Width": "0.057980600744485855", "Height": "0.11505743116140366", "Left": "0.9420194029808044", "Top": "0.3286333382129669" }, 96 | "Polygon": [ 97 | { "X": "0.9442570805549622", "Y": "0.3286333382129669" }, 98 | { "X": "1.0", "Y": "0.3323029577732086" }, 99 | { "X": "0.9998158812522888", "Y": "0.44369077682495117" }, 100 | { "X": "0.9420194029808044", "Y": "0.4400211572647095" } 101 | ] 102 | } 103 | } 104 | }, 105 | { 106 | "Timestamp": 3000, 107 | "TextDetection": { 108 | "DetectedText": "e", 109 | "Type": "WORD", 110 | "Id": 3, 111 | "ParentId": 1, 112 | "Confidence": "92.60140991210938", 113 | "Geometry": { 114 | "BoundingBox": { "Width": "0.01153564453125", "Height": "0.021484375", "Left": "0.98712158203125", "Top": "0.537109375" }, 115 | "Polygon": [ 116 | { "X": "0.98712158203125", "Y": "0.537109375" }, 117 | { "X": "0.9986572265625", "Y": "0.537109375" }, 118 | { "X": "0.9986572265625", "Y": "0.55859375" }, 119 | { "X": "0.98712158203125", "Y": "0.55859375" } 120 | ] 121 | } 122 | } 123 | }, 124 | { 125 | "Timestamp": 4000, 126 | "TextDetection": { 127 | "DetectedText": "Host", 128 | "Type": "LINE", 129 | "Id": 0, 130 | "Confidence": "100.0", 131 | "Geometry": { 132 | "BoundingBox": { "Width": "0.0780029296875", "Height": "0.0546875", "Left": "0.2757568359375", "Top": "0.7900390625" }, 133 | "Polygon": [ 134 | { "X": "0.2757568359375", "Y": "0.7900390625" }, 135 | { "X": "0.353759765625", "Y": "0.7900390625" }, 136 | { "X": "0.353759765625", "Y": "0.8447265625" }, 137 | { "X": "0.2757568359375", "Y": "0.8447265625" } 138 | ] 139 | } 140 | } 141 | }, 142 | { 143 | "Timestamp": 158000, 144 | "TextDetection": { 145 | "DetectedText": "-", 146 | "Type": "WORD", 147 | "Id": 2, 148 | "ParentId": 0, 149 | "Confidence": "84.69396209716797", 150 | "Geometry": { 151 | "BoundingBox": { "Width": "0.00933837890625", "Height": "0.0068359375", "Left": "0.8843994140625", "Top": "0.1455078125" }, 152 | "Polygon": [ 153 | { "X": "0.8843994140625", "Y": "0.1455078125" }, 154 | { "X": "0.89373779296875", "Y": "0.1455078125" }, 155 | { "X": "0.89373779296875", "Y": "0.15234375" }, 156 | { "X": "0.8843994140625", "Y": "0.15234375" } 157 | ] 158 | } 159 | } 160 | }, 161 | { 162 | "Timestamp": 158000, 163 | "TextDetection": { 164 | "DetectedText": "PIZZA", 165 | "Type": "WORD", 166 | "Id": 3, 167 | "ParentId": 1, 168 | "Confidence": "95.39642333984375", 169 | "Geometry": { 170 | "BoundingBox": { "Width": "0.021879959851503372", "Height": "0.01627572998404503", "Left": "0.8918451070785522", "Top": "0.15577061474323273" }, 171 | "Polygon": [ 172 | { "X": "0.8918451070785522", "Y": "0.15750867128372192" }, 173 | { "X": "0.9133533835411072", "Y": "0.15577061474323273" }, 174 | { "X": "0.9137250781059265", "Y": "0.17030830681324005" }, 175 | { "X": "0.8922168016433716", "Y": "0.17204634845256805" } 176 | ] 177 | } 178 | } 179 | } 180 | ], 181 | "TextModelVersion": "3.1", 182 | "ResponseMetadata": { 183 | "RequestId": "2b317efd-6016-400f-8d04-5c04367717aa", 184 | "HTTPStatusCode": 200, 185 | "HTTPHeaders": { 186 | "x-amzn-requestid": "2b317efd-6016-400f-8d04-5c04367717aa", 187 | "content-type": "application/x-amz-json-1.1", 188 | "content-length": "191240", 189 | "date": "Fri, 03 Mar 2023 20:32:32 GMT" 190 | }, 191 | "RetryAttempts": 0 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /test/unit/helper/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import pytest 5 | import os 6 | import tempfile 7 | import json 8 | from unittest.mock import patch 9 | from botocore.stub import Stubber 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def mock_env_variables(monkeypatch): 14 | """Mock up environment variables that the testing target depends on""" 15 | monkeypatch.syspath_prepend('../../source/') 16 | monkeypatch.setenv("DataplaneBucket", 'testDataplaneBucket') 17 | monkeypatch.setenv("botoConfig", '{"user_agent_extra": "AwsSolution/SO0164/2.0.4"}') 18 | monkeypatch.setenv('AWS_XRAY_CONTEXT_MISSING', 'LOG_ERROR') 19 | monkeypatch.setenv('AWS_REGION', 'us-west-2') 20 | monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") 21 | monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test") 22 | monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "test") 23 | monkeypatch.setenv("AWS_SESSION_TOKEN", "test") 24 | monkeypatch.setenv("SearchEndpoint", "localhost/search") 25 | monkeypatch.setenv("DataplaneEndpoint", "https://localhost/dataplane/api") 26 | monkeypatch.setenv("WorkflowEndpoint", "https://localhost/workflow/api") 27 | monkeypatch.setenv("UserPoolId", "us-west-2_userPoolId") 28 | monkeypatch.setenv("AwsRegion", "us-west-2") 29 | monkeypatch.setenv("PoolClientId", "poolClientId") 30 | monkeypatch.setenv("IdentityPoolId", "us-west2:identityPoolId") 31 | 32 | 33 | class Context: 34 | """Fake context that represents the lambda context passed to the lambda handler.""" 35 | log_stream_name = 'testLogStreamName' 36 | 37 | def __str__(self) -> str: 38 | return "LambdaContext(log_stream_name={})".format(self.log_stream_name) 39 | 40 | 41 | class FakeResponse: 42 | """Fake response to return when opening a HTTP request.""" 43 | getcode = 200 44 | msg = "SUCCESS" 45 | 46 | 47 | @pytest.fixture 48 | def context(request): 49 | """Yields an instance of `Context` representing the lambda context. 50 | 51 | If the test case is a class method, a `context` attribute will also 52 | be set on the object instance so the method can access it via self.context. 53 | """ 54 | c = Context() 55 | if request.cls is not None: 56 | request.cls.context = c 57 | yield c 58 | 59 | 60 | @pytest.fixture 61 | def s3_client_stub(request, mock_env_variables): 62 | """Activate a Stubber for the s3 client used by the target module. 63 | 64 | https://botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html 65 | 66 | Yields the Stubber instance and also creates an attribute on the test class instance. 67 | """ 68 | import helper.website_helper as app 69 | with Stubber(app.s3_client) as stubber: 70 | if request.cls is not None: 71 | request.cls.s3_client_stub = stubber 72 | yield stubber 73 | # Always assert that no responses are pending in the queue. 74 | stubber.assert_no_pending_responses() 75 | 76 | 77 | @pytest.fixture 78 | def s3_resource_stub(request, mock_env_variables): 79 | """Activate a Stubber for the s3 client associated with the s3 resource in the target. 80 | 81 | Yields the Stubber instance and also creates an attribute on the test class instance. 82 | """ 83 | import helper.website_helper as app 84 | with Stubber(app.s3.meta.client) as stubber: 85 | if request.cls is not None: 86 | request.cls.s3_resource_stub = stubber 87 | yield stubber 88 | # Always assert that no responses are pending in the queue. 89 | stubber.assert_no_pending_responses() 90 | 91 | 92 | @pytest.fixture 93 | def build_opener(request, mock_env_variables): 94 | """Patch the `build_opener` by replacing it with an auto-spec Mock. 95 | 96 | Yields the Mock instance and also creates an attribute on the test class instance. 97 | """ 98 | with patch('helper.website_helper.build_opener', autospec=True, spec_set=True) as wrapper: 99 | # Set our fake response as the return value of the `open` method on the instance. 100 | instance = wrapper.return_value 101 | instance.open.return_value = FakeResponse() 102 | 103 | if request.cls is not None: 104 | request.cls.build_opener = wrapper 105 | yield wrapper 106 | 107 | # Each test case would call `build_opener` and then `open` exactly one time. 108 | # Make that assertion here so the individual test case doesn't need to do it. 109 | wrapper.assert_called_once() 110 | instance.open.assert_called_once() 111 | 112 | 113 | @pytest.fixture 114 | def send_response(request): 115 | """Patch the `send_response` function by replacing it with an auto-spec Mock. 116 | 117 | Yields the Mock instance and also creates an attribute on the test class instance. 118 | """ 119 | with patch('helper.website_helper.send_response', autospec=True, spec_set=True) as wrapper: 120 | # Make a property on the test class instance that can be accessed via self.send_response 121 | if request.cls is not None: 122 | request.cls.send_response = wrapper 123 | yield wrapper 124 | wrapper.assert_called_once() 125 | 126 | 127 | @pytest.fixture 128 | def cwd_temp(): 129 | """Creates a temp directory and makes it the current working directory for a test case. 130 | 131 | Any temporary files written to the directory will get deleted when the test case ends. 132 | """ 133 | # Create the temporary directory. It will get deleted when the `with` block ends. 134 | with tempfile.TemporaryDirectory() as tempdir: 135 | # Save the current working directory so we can reset it at the end 136 | cwd = os.getcwd() 137 | # Change to the temp directory 138 | os.chdir(tempdir) 139 | # Yield to the test case so it can run 140 | yield 141 | # Reset the working directory 142 | os.chdir(cwd) 143 | 144 | 145 | @pytest.fixture 146 | def webapp_manifest(cwd_temp): 147 | """Make a webapp-manifest.json file to be read by the `copy_source` function. 148 | 149 | This fixture depends on the `cwd_temp` fixture, so the file can be created 150 | in the current working directory. It writes a file at './webapp-manifest.json' 151 | containing a JSON list of fake files making up the web application. 152 | 153 | After writing the JSON file to the file system, it yields the list of files 154 | to the test case so the test case can make assertions based on the list. 155 | """ 156 | 157 | # Make up a fake manifest list. 158 | manifest = [ 159 | "index.html", 160 | "css/chunk.css", 161 | "css/app.css", 162 | "js/chunk.js", 163 | "js/app.js", 164 | "img/icons/favicon-16x16.png", 165 | "runtimeConfig.json", 166 | "manifest.json", 167 | "service-worker.js", 168 | "robots.txt" 169 | ] 170 | 171 | # Dump the list to the known file name as JSON. 172 | with open('./webapp-manifest.json', mode='w') as file: 173 | json.dump(manifest, file) 174 | 175 | # Yield the list to the test case. 176 | yield manifest 177 | 178 | 179 | @pytest.fixture(params=["Create", "Update"]) 180 | def request_type_copy(request): 181 | """Yields each request type that causes files to be copied. 182 | 183 | Test cases depending on this fixture will be called once for 184 | each parameter in the fixture's `params` list. 185 | i.e., It will result in two test cases for each test case; 186 | one for "Create" and one for "Update". 187 | """ 188 | yield request.param 189 | -------------------------------------------------------------------------------- /test/unit/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.26.46 2 | botocore==1.29.46 3 | pytest==7.2.0 4 | pytest-cov==4.1.0 5 | requests==2.32.3 6 | -------------------------------------------------------------------------------- /test/unit/run_unit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | ############################################################################### 7 | # PURPOSE: This script runs our pytest unit test suite. 8 | # 9 | # PRELIMINARY: 10 | # Set the required environment variables; see the testing README.md for more 11 | # details. 12 | # 13 | # USAGE: 14 | # ./run_unit.sh [COMPONENT...] 15 | # 16 | # where COMPONENT is an optional sub-set of components to run tests for. 17 | # The sub-directories contained within this directory match the components 18 | # so, COMPONENT is one or more sub-directories. If COMPONENT is not specified, 19 | # all unit tests will run for all components. 20 | # 21 | ############################################################################### 22 | 23 | #################### Nothing for users to change below here #################### 24 | 25 | # Make sure working directory is the directory containing this script 26 | cd "$(dirname "${BASH_SOURCE[0]}")" 27 | 28 | # Returns shell SUCCESS value if ${1} is a SUCCESS value; otherwise FAIL. 29 | is_set() { 30 | return ${1:-1} 31 | } 32 | 33 | log_step() { 34 | echo "------------------------------------------------------------------------------" 35 | echo "$*" 36 | echo "------------------------------------------------------------------------------" 37 | } 38 | 39 | log_step "Setup test environment variables" 40 | 41 | TEMP_VENV='' 42 | VENV='' 43 | [ -n "${CACHED_TEST_VENV:-}" ]; declare -ri cache_venv=$? 44 | is_set $cache_venv && [ -f "${CACHED_TEST_VENV:-}/bin/activate" ]; declare -ri cache_hit=$? 45 | 46 | source_dir=`cd ../../source; pwd` 47 | coverage_reports_dir="$(cd ..; pwd)/coverage-reports" 48 | 49 | # Make sure we clean up 50 | cleanup_before_exit() { 51 | cleanup $? 52 | } 53 | 54 | # Clean up temporary files 55 | cleanup() { 56 | # Reset the signals to default behavior 57 | trap - SIGINT SIGTERM EXIT 58 | log_step "Cleaning up" 59 | 60 | # Deactivate and remove the temporary python virtualenv used to run this script 61 | [ -n "${VIRTUAL_ENV:-}" ] && deactivate 62 | [ -n "${TEMP_VENV:-}" ] && [ -d "$TEMP_VENV" ] && rm -rf "$TEMP_VENV" 63 | rm -rf __pycache__ .coverage 64 | exit ${1:-0} 65 | } 66 | 67 | # Install packages defined in ${1:-.}/requirements.txt if it exists 68 | pip_install_requirements() { 69 | # Skip install if we are using the cached venv 70 | is_set $cache_hit && return 0 71 | 72 | local -r src_dir="${1:-.}" 73 | if [ "$src_dir" = . ] 74 | then 75 | local -r req_file=requirements.txt 76 | else 77 | local -r req_file="${src_dir}/requirements.txt" 78 | fi 79 | 80 | if [ -f "${req_file}" -a -r "${req_file}" ] 81 | then 82 | [ "$src_dir" = . ] || pushd "${src_dir}" &>/dev/null || exit 1 83 | pip install -r requirements.txt || { 84 | echo "ERROR: Failed to install required Python libraries at ${req_file}" 85 | exit 1 86 | } 87 | [ "$src_dir" = . ] || popd &>/dev/null || exit 1 88 | fi 89 | } 90 | 91 | # Install packages defined in source/${1}/requirements.txt if it exists 92 | pip_install_source_requirements() { 93 | log_step "Installing required Python packages for ${tc}" 94 | pip_install_requirements "${source_dir}/${1}" 95 | } 96 | 97 | # Create and activate a temporary Python environment for this script. 98 | 99 | log_step "Creating a temporary Python virtualenv for this script" 100 | if [ -n "${VIRTUAL_ENV:-}" ]; then 101 | echo "ERROR: Do not run this script inside Virtualenv. Type \`deactivate\` and run again."; 102 | exit 1; 103 | fi 104 | if ! command -v python3 &>/dev/null; then 105 | echo "ERROR: install Python3 before running this script" 106 | exit 1 107 | fi 108 | 109 | # Make temporary directory for the virtual environment 110 | if is_set $cache_venv 111 | then 112 | VENV="$CACHED_TEST_VENV" 113 | else 114 | VENV="$(mktemp -d)" 115 | TEMP_VENV="${VENV}" 116 | fi 117 | 118 | # Trap exits so we are sure to clean up the virtual environment 119 | trap cleanup_before_exit SIGINT SIGTERM EXIT 120 | 121 | # Create and activate the virtual environment 122 | [ -f "$VENV/bin/activate" ] || python3 -m venv "$VENV" || exit $? 123 | source "$VENV/bin/activate" || exit $? 124 | 125 | # Install packages into the virtual environment 126 | pip_install_requirements 127 | 128 | 129 | # Run all test cases specified 130 | run_tests() { 131 | while [ $# -ne 0 ]; do 132 | local tc="${1%/}" 133 | shift 134 | 135 | # Make sure required packages are installed 136 | pip_install_source_requirements ${tc} 137 | 138 | log_step "Running ${tc} unit tests" 139 | coverage_report_path="$coverage_reports_dir/coverage-source-${tc}.xml" 140 | pytest "${tc}" -s -W ignore::DeprecationWarning \ 141 | -W 'ignore:IAMAuthorizer is not a supported in local mode:UserWarning' -p no:cacheprovider \ 142 | --cov="$source_dir/${tc}" --cov-report=term --cov-report=xml:$coverage_report_path \ 143 | || exit 1 144 | 145 | sed -i.orig -e "s,$source_dir,source,g" $coverage_report_path 146 | rm -f $coverage_reports_dir/*.orig 147 | done 148 | } 149 | 150 | if [ $# -gt 0 ]; then 151 | # Run all tests spcified as command line arguments 152 | run_tests "$@" 153 | else 154 | # No tests specified so run all of them found in sub-directories 155 | log_step "Running all unit tests" 156 | run_tests */ 157 | fi 158 | 159 | cleanup $? 160 | --------------------------------------------------------------------------------