├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── assets
├── architecture.drawio
├── architecture.jpg
├── vus-chat-1.gif
├── vus-chat-2.gif
├── vus-chat-3.gif
├── vus-entities.gif
├── vus-search.gif
└── vus-summary.gif
├── cdk.json
├── lib
├── amplify_deploy_lambda
│ ├── index.py
│ └── requirements.txt
├── app.py
├── cognito_user_setup
│ └── index.py
├── db_setup_lambda
│ ├── index.py
│ └── requirements.txt
├── main_analyzer
│ ├── Dockerfile
│ ├── default_visual_extraction_system_prompt.txt
│ ├── default_visual_extraction_task_prompt.txt
│ ├── index.py
│ └── requirements.txt
├── preprocessing_lambda
│ ├── index.py
│ └── requirements.txt
├── prompt_setup_lambda
│ ├── index.py
│ └── requirements.txt
├── video_understanding_solution_stack.py
└── videos_search
│ ├── lambda-handler.py
│ └── requirements.txt
├── utils
├── bootstrap.sh
├── deploy.sh
├── destroy.sh
├── install_prerequisites_for_amazon_linux_2.sh
├── install_prerequisites_for_amazon_linux_2023.sh
├── install_prerequisites_for_macos_ventura.sh
└── scan.sh
└── webui
├── package.json
├── public
├── index.html
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── VideoTable
├── VideoTable.css
└── VideoTable.js
├── VideoUpload
├── VideoUpload.css
└── VideoUpload.js
├── aws-exports.js
├── index.css
├── index.js
├── reportWebVitals.js
└── setupTests.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | package-lock.json
3 | webui/package-lock.json
4 | __pycache__
5 | .pytest_cache
6 | .venv
7 | *.egg-info
8 |
9 | # CDK asset staging directory
10 | .cdk.staging
11 | cdk.out
12 | cdk.context.json
13 |
14 | .DS_STORE
15 | deployment-output.json
16 | .DEFAULT_REGION
17 | team-provider-info.json
18 | venv/
19 |
20 | # Amplify
21 | build/
22 | webui/build/
23 | dist/
24 | webui/dist/
25 | node_modules/
26 | webui/node_modules/
27 | awsconfiguration.json
28 | amplifyconfiguration.json
29 | amplifyconfiguration.dart
30 | amplify-build-config.json
31 | amplify-gradle-config.json
32 | amplifytools.xcconfig
33 | .secret-*
34 |
35 | ui_repo*.zip
36 | banditreport.txt
37 | semgrepreport.txt
38 | semgreplog.txt
39 | npmauditreport.json
40 |
--------------------------------------------------------------------------------
/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 |
6 | In addition, the acceptable use policy from the models used by the solution also applies.
7 | See:
8 | * https://www.anthropic.com/legal/aup
9 | * The EULA files for Anthropic Claude 3 Sonnet & Haiku models and Cohere Embed Multilingual model which can be seen from this page Amazon Bedrock model access page. Follow this instruction to go to the page https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html
10 |
11 |
--------------------------------------------------------------------------------
/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, or recently closed, 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 *main* 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT No Attribution
2 |
3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | bootstrap:
2 | ./utils/bootstrap.sh
3 |
4 | prepare:
5 | OS=$$(uname -a | sed -n 's/^\([^ ]*\) .*/\1/p') ;\
6 | if [ "$$OS" = "Darwin" ]; then ./utils/install_prerequisites_for_macos_ventura.sh ;\
7 | elif [ "$$OS" = "Linux" ]; then @VER=$$(uname -a | sed -n 's/.*amzn\([0-9]*\).*/\1/p') ; if [ "$$VER" = "2" ]; then ./utils/install_prerequisites_for_amazon_linux_2.sh ; elif [ "$$VER" = "2023" ]; then ./utils/install_prerequisites_for_amazon_linux_2023.sh ; else echo "OS is not currently supported" ; fi;\
8 | else echo "OS is not currently supported" ;\
9 | fi
10 |
11 | deploy:
12 | OS=$$(uname -a | sed -n 's/^\([^ ]*\) .*/\1/p') ;\
13 | if [ "$$OS" = "Darwin" ]; then ./utils/deploy.sh ;\
14 | elif [ "$$OS" = "Linux" ]; then sg docker -c './utils/deploy.sh' ;\
15 | else echo "OS not currently supported" ;\
16 | fi
17 |
18 | destroy:
19 | ./utils/destroy.sh
20 |
21 | scan:
22 | ./utils/scan.sh
23 |
24 | bootstrap_and_deploy: bootstrap deploy
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Video Understanding Solution
2 |
3 | ## Introduction
4 |
5 | This is a deployable solution to help save your time in understanding videos without having to watch every video. You can upload videos and this solution can generate AI-powered summary and entities extraction for each video. It also supports Q&A about the video like "What is funny about the video?", "How does Jeff Bezos look like there?", and "What shirt did he wear?". You can also search for videos using semantic search e.g. "Amazon's culture and history". This solution extracts information from visual scenes, audio, visible texts, and detected celebrities or faces in the video. It leverages an LLM which can understand visual and describe the video frames.
6 |
7 | You can upload videos to your Amazon Simple Storage Service (S3) bucket bucket by using AWS console, CLI, SDK, or other means (e.g. via AWS Transfer Family). This solution will automatically trigger processes including call to Amazon Transcribe for voice transcription, call to Amazon Rekognition to extract the objects visible, and call to Amazon Bedrock with Claude 3 model to extract scenes and visually visible text. The LLM used can perform VQA (visual question answering) from images (video frames), which is used to extract the scene and text. This combined information is used to generate the summary and entities extraction as powered by generative AI with Amazon Bedrock. The UI chatbot also uses Amazon Bedrock for the Q&A chatbot. The summaries, entities, and combined extracted information are stored in S3 bucket, available to be used for further custom analytics.
8 |
9 | **Demo for summarization feature:**
10 |
11 | 
12 |
13 | **Demo for entities and sentiment extraction feature:**
14 |
15 | 
16 |
17 | **Demo for Q&A 1:**
18 |
19 | 
20 |
21 | **Demo for Q&A 2:**
22 |
23 | 
24 |
25 | **Demo for Q&A 3:**
26 |
27 | 
28 |
29 | **Demo for search feature:**
30 |
31 | 
32 |
33 |
34 | The diagram below shows the architecture. This is extendable since the summaries, entities, and extracted information are stored in Amazon S3 bucket, which you can use for further purposes.
35 |
36 | 
37 |
38 | Note that the architecture diagram may not represent all the components and relations for readability. The Amazon Aurora PostgreSQL database is deployed in Amazon VPC with isolated subnet spanning across 3 availability zones. The Fargate task for analysis, the video search Lambda function, and the preprocessing Lambda function are all deployed in the VPC with private subnet.
39 |
40 | Refer to the Deployment section below on how to deploy it to your own AWS account. Refer to the Use section on how to use it.
41 |
42 | ## Deployment
43 |
44 | ### Prerequisites
45 |
46 | Here are prerequisites for deploying this solution with AWS CDK. Note that you can automatically install item 4-11 by running `make prepare`.
47 |
48 | The `make prepare` utility is currently supported only for MacOS Ventura, Amazon Linux 2, and Amazon Linux 2023 operating systems.
49 |
50 |
51 | 1. AWS account with Amazon Bedrock model access enabled for Anthropic - Claude, Anthropic - Claude Instant and Cohere - Embed Multilingual. Follow the steps in https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html#add-model-access to enable those models and make sure the region is correct.
52 | 2. Make 3.82 or above in your environment
53 | 3. AWS IAM credentials with sufficient permission for deploying this solution configured in your environment. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html for credentials setup.
54 | 4. AWS CLI 2.15.5 or above / 1.29.80 or above. Follow https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html for installation.
55 | 5. Python 3.8 or above, pip, and virtualenv.
56 | 6. Docker 20.10.25 or above
57 | 7. NodeJS 16 with version 16.20.2 above, or NodeJS 20 with version 20.10.0 or above, along with NPM.
58 | 8. jq, zip, unzip
59 | 9. CDK Toolkit 2.122.0 or above. Follow https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install for installation.
60 | 10. Python CDK lib 2.122.0 or above with @aws-cdk/aws-amplify-alpha. Follow https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_concepts. This is for the Python CDK libraries since the CDK template is in Python.
61 | 11. CDK nag 2.28.16 or above. CDK nag is used to scan the CDK template for best practices violation.
62 |
63 | ### Deploy
64 |
65 | **IMPORTANT**
66 |
67 | At the moment,this solution can only be used in AWS regions where Amazon Bedrock is available.
68 | At the time of the writing of this document, you can deploy and use this in us-east-1 (N. Virginia) and us-west-2 (Oregon) regions.
69 |
70 | Follow the steps below for deploying this solution from your current environment
71 |
72 | 1. Make sure that all prerequisites are met. Please refer to the [Prerequisites](#prerequisites) section above.
73 |
74 | 2. Run `make bootstrap_and_deploy` and enter an email address and the region to deploy when prompted. Make sure the email address is valid to receive the login credentials.
75 |
76 | 3. [Optional] For second or later redeployment, run `make deploy` instead. Note that any change to the UI (under "webui" folder) is not redeployed with `make deploy`. For UI changes you can push them to the deployed CodeCommit repository directly or run `make destroy` followed by `make deploy`.
77 |
78 |
79 | ## Use
80 |
81 | ### First login
82 | After a successful deployment, the email address inputted during deployment should receive an email with temporary password and the web UI portal URL. It can take 15 minutes or so after you receive the email until the deployment completion, so just wait if the web UI isn't ready. Visit the web UI, use the email address as username, and input the temporary password. Then reset the password. When presented with a QR code, that is for you to set the MFA. Use authenticator software like Google Authenticator from your phone to set the MFA. Enter the MFA code back to the UI. After a successful sign-in, you should be able to see the page that will display your uploaded videos.
83 |
84 | ### Uploading videos and checking analysis outputs
85 | You can upload videos into the S3 bucket using the web UI. Or, you can upload them straight to the S3 bucket using other means, under folder "source" as also instructed in the web UI. Uploaded videos will automatically trigger asynchronous tasks. For each video wait for few minutes for the tasks to finish. The analysis will be displayed in the web UI. It will also be stored in S3 under folder "summary", "entities", "audio_transcript" (the formatted transcription result), and "video_timeline" (the combined extracted video raw information), which you can use for further custom analysis / machine learning by extending this solution.
86 |
87 | ### Using the web UI
88 | In the web UI, you can search for videos in your S3 bucket. For each video found, you can view the video, see its summary, check the extracted entities, and ask the chatbot about the video e.g. "What is funny or interesting about the video?". The chatbot is equipped with memory for the current conversation, so the user can have a context-aware conversation with the bot. You can also search videos using the name prefix, uploaded date range, and what the video is about. The latter is powered by semantic search using LLM.
89 |
90 | ## Configuration
91 | ### Enabling/disabling features
92 | You can enable/disable 2 features for now:
93 | 1. Transcription. For use cases where extracting information from the speech in the video won't be important, you can save cost by disabling this feature.
94 | 2. Face detection, and celebrity detection. For use case where these information won't be important, you can disable them. With these disabled, the solution will still extract the visual scene information and the text overlay (if any) of the videos.
95 |
96 |
97 | To enable/disable these, you can log in to your AWS Console and go to AWS SSM Parameter Store for a parameter VideoUnderstandingStack-configuration. For example you can go [here](https://us-west-2.console.aws.amazon.com/systems-manager/parameters/VideoUnderstandingStack-configuration/description?region=us-west-2&tab=Table) if you deploy the solution in Oregon (us-west-2) region. For other region, you can change any "us-west-2" in that hyperlink to your selected region. Then you can edit the parameter value. The value "1" means enabled and value "0" means disabled. The "label_detection_enabled" represents the face and celebrity detection while "transcription_enabled" represents the transcription. By default, both are enabled ("1").
98 |
99 | ## Cost
100 | The cost of using this solution is determined by the pricing and usage of the components being deployed. This includes, but not being limited to (not the exhaustive list):
101 |
102 | 1. Less variable components:
103 |
104 | 1.1. Amazon Aurora PostgreSQL - Pricing is [here](https://aws.amazon.com/rds/aurora/pricing/). This solution by default uses 2 x 0.5 ACU of Aurora Serverless PostgreSQL.
105 |
106 | 1.2. NAT Gateway - Pricing is [here](https://aws.amazon.com/vpc/pricing/). This is used for egress access of Fargate task and some Lambda function. By default this solution uses 1 NAT Gateway.
107 |
108 | 2. More variable components (driven mainly by the total number of video minutes being processed and the activities e.g. chat):
109 |
110 | 2.1. Amazon Rekognition - Pricing is [here](https://aws.amazon.com/rekognition/pricing/). You may be eligible for its FREE TIER for image and video analysis. This solution uses StartLabelDetection and GetLabelDetection video APIs in addition to DetectFaces and RecognizeCelebrities image APIs. The cost can vary based on the number of detected faces in the whole videos.
111 |
112 | 2.2. Amazon Transcribe - Pricing is [here](https://aws.amazon.com/transcribe/pricing/). Youo may be eligible for its FREE TIER. This solution uses Standard Batch transcription. The cost can vary based on the video duration and the number of videos being processed.
113 |
114 | 2.3. Amazon Bedrock - Pricing is [here](https://aws.amazon.com/bedrock/pricing/). This solution uses Claude, Claude Instant and Cohere Embed Multilingual model. The cost can vary based on the amount of information extracted from the video and the usage frequency.
115 |
116 | 2.4. Amazon S3 - Pricing is [here](https://aws.amazon.com/s3/pricing/). You may be eligible for its FREE TIER. S3 is used for storing the raw videos and the extracted information.
117 |
118 | 2.5. AWS Fargate - Pricing is [here](https://aws.amazon.com/fargate/pricing/). Fargate task is where the extracted video information is being analyzed and where the calls to LLMs for summary and entities generation happen. It is provisioned on demand when a video is uploaded.
119 |
120 | 2.6. AWS Lambda - Pricing is [here](https://aws.amazon.com/lambda/pricing/). You may be eligible for its FREE TIER. Lambda function is used in several parts of the worklflow, including to handle video search requests.
121 |
122 | 2.7. Amazon CloudWatch - Pricing is [here](https://aws.amazon.com/cloudwatch/pricing/). You may be eligible for its FREE TIER. Amazon CloudWatch metrics and logs are used.
123 |
124 | It is recommended to test with smaller number of shorter videos first and observe the cost. It is recommended to monitor the cost with [AWS Cost Explorer](https://aws.amazon.com/aws-cost-management/aws-cost-explorer/) and set budget and alerts with [AWS Budget](https://aws.amazon.com/aws-cost-management/aws-budgets/). You can filter the cost by tagging with key = "Application" and value = "VideoUnderstandingSolution" to monitor the cost generated by this solution when deployed. Please note that some compnents may not be tagged or may not be taggable.
125 |
126 |
127 | ## Limitations
128 |
129 | 1. Currently only .mp4/.MP4 video files are supported
130 | 2. The language supported is limited by the language supported by Amazon Rekognition text detection, Amazon Transcribe, and the models used in Amazon Bedrock.
131 | 3. This works best for video under 15 minutes. It may have issue or show slow latency for very long video at the moment.
132 | 4. Video file names (when uploaded) must adhere to the [S3 object key pattern](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html). When uploaded via this solution's UI, it will automatically convert non-compliant characters to _ (underscore).
133 |
134 | ## Removal
135 | To remove the solution from your AWS account, follow these steps:
136 |
137 | 1. [ Conditional ] This is if you want to delete the database. In RDS console, find the Aurora PostgreSQL cluster provisioned by this solution, whose name starts with "videounderstandingstack". Disable the RDS deletion protection as appropriate. For disabling the protection, follow [these steps](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Modifying.html#Aurora.Modifying.Cluster) and make sure you **uncheck** the "Enable deletion protection". You can choose to "Apply immediately" as appropriate.
138 | 2. [ Conditional ] This is if you want to delete the S3 bucket hosting the videos and the extracted outputs. In S3 console, find the S3 bucket provisioned by this solution, whose name starts with "videounderstandingstack". Empty the bucket. Follow this [documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/empty-bucket.html) to empty a bucket. Note that if the step (3) below failed because the S3 bucket is not empty while you have performed step (2), just redo step (2) and continue step (3). That situation might arise when some logs are still being delivered to the S3 bucket, so after it gets emptied the log comes in.
139 | 3. run `make destroy` and specify the region when asked. If you chose not to do step (1) and (2) above, then at some point the `make destroy` will fail, which is expected. You need to go to AWS CloudFormation console, find the stack named "VideoUnderstandingStack", and delete it. When asked whether to skip the deletion of the RDS and S3 bucket, then confirm.
140 |
141 | ## Security
142 |
143 | Run `make scan` before submitting any pull request to make sure that the introduced changes do not open a vulnerability. Make sure the generated banditreport.txt, semgrepreport.txt, semgreplog.txt, npmauditreport.txt, and the cdk_nag files in cdk.out folders all show **no high or critical finding**.
144 |
145 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
146 |
147 | ## Extendability and customization
148 | This solution is architected to be extendable and customizable.
149 |
150 | ### Extending the solution for other purposes
151 | 1. The audio transcript, the combined visual and audio information extracted from LLM (with VQA) and AWS services, the summaries, and the extracted entities and sentiment are stored in S3 bucket. Navigate to the S3 bucket and find the relevant folders there. The file name for each of those category is based on the actual file name from the 'source' folder. You can further use this information for other purposes
152 |
153 | 2. The summaries, the combined visual and audio information extracted from LLM (with VQA) and AWS services, and the extracted entities and sentiment are stored in the Amazon Aurora Serverless v2 PostgreSQL database in addition to S3. The summaries and the combined extracted information are stored in the form of text and embedding (using pgVector). These are under "videos" database name. The summaries are stored in "videos" table. The entities are stored in "entities" table. The extracted information of the video (arranged in timestamps basis and may stored in chunks) is stored in "content" table. You can use the database with credentials from the AWS Secrets Manager.
154 |
155 | ### Customizing the solution
156 | To customize the solution, you can edit the code. Please refer to the Development section below. The possible customizations include:
157 | 1. Modifying prompts to add more data to be extracted from the video frames
158 | 2. Modifying prompts to add more features
159 | 3. Modifying the backend code
160 | 4. Modiyfing the UI
161 |
162 | ## Development
163 |
164 | This information is meant for the contributors of the repository or builders who build new solutions from this.
165 |
166 | ### Web UI development
167 |
168 | * You can first deploy the whole solution to deploy both storage, frontend, and backend. This should deploy an AWS CodeCommit repo. You can then clone from that repo, do development, and push the changes. Remember that running `make deploy` again will update the other CDK components, but it won't update or re-initialize the CodeCommit repo.
169 |
170 | * During development, you may want to test the UI changes live without having to wait for CodeCommit code update and Amplify build & deploy. Instead you can temporarily edit the /webui/src/aws-exports.js and replace the placeholders with actual value of the deployed components (e.g. Cognito user pool ID). Then `cd webui` and run `npm start`. This should run the frontend in your localhost, supported by the backend and storage in the cloud.
171 |
172 | ### Backend development
173 |
174 | * This solution uses CDK in Python. You can go [here](https://docs.aws.amazon.com/cdk/api/v2/python/) for references of the CDK components.
175 |
176 | * This solution uses cdk_nag to prevent the solution being deployed when some components are not adhering to the best practices. If your changes resulted in cdk_nag error, fix them. Sometimes, the cdk_nag finding could be a false alarm which you can suppress.
177 |
178 |
179 | ## License
180 |
181 | This library is licensed under the MIT-0 License. See the LICENSE file.
182 |
183 | ## Notes
184 |
185 | * The facial recognition in this solution is provided by Amazon Rekognition.
186 | * Please refer to the CODE_OF_CONDUCT.md file to see the terms of use and acceptable use policy. Some AI models have prohibition on what to use the model for. Refer to the architecture diagram above to see what models are being used by this solution and for which part.
187 |
--------------------------------------------------------------------------------
/assets/architecture.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
--------------------------------------------------------------------------------
/assets/architecture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/architecture.jpg
--------------------------------------------------------------------------------
/assets/vus-chat-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-chat-1.gif
--------------------------------------------------------------------------------
/assets/vus-chat-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-chat-2.gif
--------------------------------------------------------------------------------
/assets/vus-chat-3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-chat-3.gif
--------------------------------------------------------------------------------
/assets/vus-entities.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-entities.gif
--------------------------------------------------------------------------------
/assets/vus-search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-search.gif
--------------------------------------------------------------------------------
/assets/vus-summary.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/video-understanding-solution/46fb6662c0dc547ac83746126cb61b85fe4e72bb/assets/vus-summary.gif
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "python3 ./lib/app.py",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "requirements*.txt",
11 | "source.bat",
12 | "**/__init__.py",
13 | "**/__pycache__",
14 | "tests"
15 | ]
16 | },
17 | "context": {
18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19 | "@aws-cdk/core:checkSecretUsage": true,
20 | "@aws-cdk/core:target-partitions": [
21 | "aws",
22 | "aws-cn"
23 | ],
24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27 | "@aws-cdk/aws-iam:minimizePolicies": true,
28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33 | "@aws-cdk/core:enablePartitionLiterals": true,
34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
39 | "@aws-cdk/aws-route53-patters:useCertificate": true,
40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
46 | "@aws-cdk/aws-redshift:columnId": true,
47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
50 | "@aws-cdk/aws-kms:aliasNameRef": true,
51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true,
54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
57 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
58 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
59 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/amplify_deploy_lambda/index.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import os
3 | import json
4 |
5 | amplify = boto3.client('amplify')
6 | codepipeline = boto3.client('codepipeline')
7 | app_id = os.environ['AMPLIFY_APP_ID']
8 | branch_name = os.environ['BRANCH_NAME']
9 |
10 | # Notify CodePipeline of a successful job
11 | def put_job_success(job_id, message):
12 | codepipeline.put_job_success_result(jobId=job_id)
13 |
14 | # Notify CodePipeline of a failed job
15 | def put_job_failure(job_id, message):
16 | codepipeline.put_job_failure_result(
17 | jobId=job_id,
18 | failureDetails={
19 | 'message': json.dumps(message),
20 | 'type': 'JobFailed',
21 | }
22 | )
23 |
24 | def handler(event, context):
25 | job_id = event['CodePipeline.job']['id']
26 | try:
27 | user_parameters = json.loads(event['CodePipeline.job']['data']['actionConfiguration']['configuration']['UserParameters'])
28 | bucket_name = user_parameters['bucket_name']
29 | object_key = user_parameters['prefix']
30 |
31 | # Then start the deployment with the created job
32 | start_response = amplify.start_deployment(
33 | appId=app_id,
34 | branchName=branch_name,
35 | sourceUrl=f"s3://{bucket_name}/{object_key}/",
36 | sourceUrlType="BUCKET_PREFIX"
37 | )
38 |
39 | put_job_success(job_id, "Deployment is successful")
40 |
41 | return {
42 | 'statusCode': 200,
43 | 'body': json.dumps('Successfully triggered Amplify deployment')
44 | }
45 |
46 | except Exception as e:
47 | print(f"Error: {str(e)}")
48 | put_job_failure(job_id, str(e))
49 | raise e
--------------------------------------------------------------------------------
/lib/amplify_deploy_lambda/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3>=1.35.83
2 | botocore>=1.35.83
--------------------------------------------------------------------------------
/lib/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 |
4 | from aws_cdk import (
5 | App, Environment, Tags
6 | )
7 |
8 | from video_understanding_solution_stack import VideoUnderstandingSolutionStack
9 |
10 |
11 | account=os.environ["CDK_DEPLOY_ACCOUNT"]
12 | region=os.environ["CDK_DEPLOY_REGION"]
13 | allowed_regions = ["us-east-1", "us-west-2"]
14 |
15 | print(f"Account ID: {account}")
16 | print(f"Region: {region}")
17 |
18 | try:
19 | if region not in allowed_regions:
20 | raise AssertionError
21 | except AssertionError:
22 | print(f"Selected region is {region}. Please use only one of these regions {str(allowed_regions)}")
23 |
24 |
25 | app = App()
26 | vus_main_stack = VideoUnderstandingSolutionStack(app, "VideoUnderstandingStack", env=Environment(
27 | account=account,
28 | region=region
29 | ))
30 | Tags.of(vus_main_stack).add("Application", "VideoUnderstandingSolution")
31 |
32 | app.synth()
--------------------------------------------------------------------------------
/lib/cognito_user_setup/index.py:
--------------------------------------------------------------------------------
1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | # SPDX-License-Identifier: MIT-0
3 |
4 | import time
5 | import boto3
6 |
7 | client = boto3.client("cognito-idp")
8 |
9 | def on_event(event, context):
10 | print(event)
11 | request_type = event["RequestType"].lower()
12 | if request_type == "create":
13 | return on_create(event)
14 | if request_type == "update":
15 | return on_update(event)
16 | if request_type == "delete":
17 | return on_delete(event)
18 | raise Exception(f"Invalid request type: {request_type}")
19 |
20 |
21 | def on_create(event):
22 | props = event["ResourceProperties"]
23 | print(f"create new resource with {props=}")
24 |
25 | user_pool = client.describe_user_pool(UserPoolId=props['user_pool_id'])['UserPool']
26 |
27 | user_pool['AdminCreateUserConfig'] = {}
28 |
29 | user_pool['AdminCreateUserConfig']['AllowAdminCreateUserOnly'] = True
30 |
31 | user_pool['AdminCreateUserConfig']['InviteMessageTemplate']= {}
32 | user_pool['AdminCreateUserConfig']['InviteMessageTemplate']['SMSMessage'] = f"Hello {{username}}. Temporary password is {{####}} The video understanding portal will be at {props['url']}"
33 | user_pool['AdminCreateUserConfig']['InviteMessageTemplate']['EmailMessage'] = f"Hello {{username}}, welcome to video understanding solution. Your temporary password is {{####}} The video understanding portal will be at {props['url']}"
34 | user_pool['AdminCreateUserConfig']['InviteMessageTemplate']['EmailSubject'] = 'Welcome to the video understanding solution'
35 |
36 | response = client.update_user_pool(
37 | UserPoolId=props['user_pool_id'],
38 | Policies=user_pool["Policies"],
39 | DeletionProtection=user_pool["DeletionProtection"],
40 | MfaConfiguration=user_pool["MfaConfiguration"],
41 | AdminCreateUserConfig=user_pool['AdminCreateUserConfig']
42 | )
43 |
44 | response = client.admin_create_user(
45 | UserPoolId=props['user_pool_id'],
46 | Username=props['admin_email_address'],
47 | UserAttributes=[
48 | {
49 | 'Name': 'email',
50 | 'Value': props['admin_email_address']
51 | },
52 | ],
53 | DesiredDeliveryMediums=['EMAIL'],
54 | )
55 |
56 | physical_id = "admincognitouser"
57 |
58 | return {"PhysicalResourceId": physical_id}
59 |
60 |
61 | def on_update(event):
62 | props = event["ResourceProperties"]
63 | print(f"no op")
64 |
65 | return {"PhysicalResourceId": "admincognitouser"}
66 |
67 | def on_delete(event):
68 | props = event["ResourceProperties"]
69 | print(f"delete resource with {props=}")
70 |
71 | response = client.admin_delete_user(
72 | UserPoolId=props['user_pool_id'],
73 | Username=props['admin_email_address']
74 | )
75 |
76 | return {"PhysicalResourceId": None}
77 |
--------------------------------------------------------------------------------
/lib/db_setup_lambda/index.py:
--------------------------------------------------------------------------------
1 | import json, os
2 | import boto3
3 | import psycopg2
4 |
5 | database_name = os.environ['DATABASE_NAME']
6 | video_table_name = os.environ['VIDEO_TABLE_NAME']
7 | entities_table_name = os.environ['ENTITIES_TABLE_NAME']
8 | content_table_name = os.environ['CONTENT_TABLE_NAME']
9 | secret_name = os.environ['SECRET_NAME']
10 | embedding_dimension = os.environ["EMBEDDING_DIMENSION"]
11 | writer_endpoint = os.environ['DB_WRITER_ENDPOINT']
12 |
13 | class Database():
14 | def __init__(self, writer, database_name, embedding_dimension, port=5432):
15 | self.writer_endpoint = writer
16 | self.username = None
17 | self.password = None
18 | self.port = port
19 | self.database_name = database_name
20 | self.video_table_name = video_table_name
21 | self.entities_table_name = entities_table_name
22 | self.content_table_name = content_table_name
23 | self.secret_name = secret_name
24 | self.embedding_dimension = embedding_dimension
25 | self.conn = None
26 |
27 | def fetch_credentials(self):
28 | secrets_manager = boto3.client('secretsmanager')
29 | credentials = json.loads(secrets_manager.get_secret_value(SecretId=self.secret_name)["SecretString"])
30 | self.username = credentials["username"]
31 | self.password = credentials["password"]
32 |
33 | def connect_for_writing(self):
34 | if self.username is None or self.password is None: self.fetch_credentials()
35 |
36 | conn = psycopg2.connect(host=self.writer_endpoint, port=self.port, user=self.username, password=self.password, database=self.database_name)
37 | self.conn = conn
38 | return conn
39 |
40 | def close_connection(self):
41 | self.conn.close()
42 | self.conn = None
43 |
44 | def setup_vector_db(self, embedding_dimension):
45 | if self.conn is None:
46 | self.connect_for_writing()
47 |
48 | cur = self.conn.cursor()
49 |
50 | # Create pgVector extension
51 | cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
52 |
53 | # For the statements below, semgrep rule for flagging formatted query is disabled as this Lambda is to be invoked in deployment phase by CloudFormation, not user facing.
54 |
55 | # Create videos table and set indexes
56 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
57 | cur.execute(f"CREATE TABLE {self.video_table_name} (name varchar(200) PRIMARY KEY NOT NULL, uploaded_at timestamp without time zone NOT NULL DEFAULT (current_timestamp AT TIME ZONE 'UTC'), summary text, summary_embedding vector({str(embedding_dimension)}));")
58 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
59 | cur.execute(f"CREATE INDEX name_index ON {self.video_table_name} (name);")
60 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
61 | cur.execute(f"CREATE INDEX uploaded_at_index ON {self.video_table_name} (uploaded_at);")
62 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
63 | cur.execute(f"CREATE INDEX name_and_uploaded_at_index ON {self.video_table_name} (name, uploaded_at);")
64 |
65 | # Create entities table
66 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
67 | cur.execute(f"CREATE TABLE {self.entities_table_name} (id bigserial PRIMARY KEY NOT NULL, name VARCHAR(100) NOT NULL, sentiment VARCHAR(20), reason text, video_name varchar(200) NOT NULL REFERENCES {self.video_table_name}(name));")
68 |
69 | # Create content table
70 | # nosemgrep: python.lang.security.audit.formatted-sql-query.formatted-sql-query, python.sqlalchemy.security.sqlalchemy-execute-raw-query.sqlalchemy-execute-raw-query
71 | cur.execute(f"CREATE TABLE {self.content_table_name} (id bigserial PRIMARY KEY NOT NULL, chunk text NOT NULL, chunk_embedding vector({str(embedding_dimension)}), video_name varchar(200) NOT NULL REFERENCES {self.video_table_name}(name));")
72 |
73 | self.conn.commit()
74 | cur.close()
75 | return True
76 |
77 | def on_event(event, context):
78 | request_type = event['RequestType'].lower()
79 | if request_type == 'create':
80 | return on_create(event)
81 | if request_type == 'update':
82 | return on_update(event)
83 | if request_type == 'delete':
84 | return on_delete(event)
85 | raise Exception(f'Invalid request type: {request_type}')
86 |
87 |
88 | def on_create(event):
89 | try:
90 | db = Database(writer=writer_endpoint, database_name = database_name, embedding_dimension = embedding_dimension)
91 | db.setup_vector_db(embedding_dimension)
92 | db.close_connection()
93 | except Exception as e:
94 | print(e)
95 |
96 | return {'PhysicalResourceId': "VectorDBDatabaseSetup"}
97 |
98 |
99 | def on_update(event):
100 | physical_id = event["PhysicalResourceId"]
101 | print("no op")
102 | #return {'PhysicalResourceId': physical_id}
103 | return {'PhysicalResourceId': "VectorDBDatabaseSetup"}
104 |
105 | def on_delete(event):
106 | physical_id = event["PhysicalResourceId"]
107 | print("no op")
108 | #return {'PhysicalResourceId': physical_id}
109 | return {'PhysicalResourceId': "VectorDBDatabaseSetup"}
110 |
--------------------------------------------------------------------------------
/lib/db_setup_lambda/requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary>=2.9.9
--------------------------------------------------------------------------------
/lib/main_analyzer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=linux/amd64 python:3.12
2 | RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
3 | RUN addgroup --system vusgroup && adduser --system vususer --ingroup vusgroup --home /vus
4 | USER vususer
5 | WORKDIR /lib/main_analyzer
6 | ADD . /lib/main_analyzer
7 | RUN python3.12 -m pip install -r ./requirements.txt
8 | CMD python3.12 index.py
--------------------------------------------------------------------------------
/lib/main_analyzer/default_visual_extraction_system_prompt.txt:
--------------------------------------------------------------------------------
1 | You are an expert in extracting information from video frames. Each video frame is an image. You will extract the scene, text, and caption.
--------------------------------------------------------------------------------
/lib/main_analyzer/default_visual_extraction_task_prompt.txt:
--------------------------------------------------------------------------------
1 | Extract information from this image and output a JSON with this format:
2 | {
3 | "scene":"String",
4 | "caption":"String",
5 | "text":["String", "String" , . . .]
6 | }
7 | For "scene", look carefully, think hard, and describe what you see in the image in detail, yet succinct.
8 | For "caption", look carefully, think hard, and give a SHORT caption (3-8 words) that best describes what is happening in the image. This is intended for visually impaired ones.
9 | For "text", list the text you see in that image confidently. If nothing return empty list.
--------------------------------------------------------------------------------
/lib/main_analyzer/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3>=1.35.84
2 | awscli>=1.36.25
3 | botocore>=1.35.84
4 | psycopg2-binary>=2.9.9
5 | SQLAlchemy>=2.0.31
6 | pgvector>=0.3.2
7 | opencv-python>=4.10.0.84
8 | pillow>=10.4.0
--------------------------------------------------------------------------------
/lib/preprocessing_lambda/index.py:
--------------------------------------------------------------------------------
1 | import json, os
2 | import boto3
3 | from datetime import datetime, timezone
4 | from sqlalchemy import create_engine, Column, Text, DateTime, String, func
5 | from sqlalchemy.ext.declarative import declarative_base
6 | from sqlalchemy.orm import mapped_column, sessionmaker
7 | from sqlalchemy.sql import bindparam
8 | from sqlalchemy.dialects.postgresql import insert as db_insert
9 | from pgvector.sqlalchemy import Vector
10 |
11 | database_name = os.environ['DATABASE_NAME']
12 | video_table_name = os.environ['VIDEO_TABLE_NAME']
13 | secret_name = os.environ['SECRET_NAME']
14 | configuration_parameter_name = os.environ['CONFIGURATION_PARAMETER_NAME']
15 | writer_endpoint = os.environ['DB_WRITER_ENDPOINT']
16 | embedding_dimension = os.environ['EMBEDDING_DIMENSION']
17 | CONFIG_LABEL_DETECTION_ENABLED = "label_detection_enabled"
18 | CONFIG_TRANSCRIPTION_ENABLED = "transcription_enabled"
19 |
20 | ssm = boto3.client('ssm')
21 | secrets_manager = boto3.client('secretsmanager')
22 |
23 | credentials = json.loads(secrets_manager.get_secret_value(SecretId=secret_name)["SecretString"])
24 | username = credentials["username"]
25 | password = credentials["password"]
26 |
27 | engine = create_engine(f'postgresql://{username}:{password}@{writer_endpoint}:5432/{database_name}')
28 | Base = declarative_base()
29 |
30 | class Videos(Base):
31 | __tablename__ = video_table_name
32 |
33 | name = Column(String(200), primary_key=True, nullable=False)
34 | uploaded_at = Column(DateTime(timezone=True), nullable=False)
35 | summary = Column(Text)
36 | summary_embedding = mapped_column(Vector(int(embedding_dimension)))
37 |
38 | Session = sessionmaker(bind=engine)
39 | session = Session()
40 |
41 | def handler(event, context):
42 | video_s3_path = event["videoS3Path"]
43 |
44 | # Get the name of the video along with its folder location from the raw_folder.
45 | video_name = '/'.join(video_s3_path.split('/')[1:]) #os.path.basename(video_s3_path)
46 |
47 | # Validate video file extension. Only .mp4, .MP4, .mov, and .MOV are allowed.
48 | if video_name[-4:] not in [".mp4", ".MP4", ".mov", ".MOV"]:
49 | return {
50 | 'statusCode': 400,
51 | 'body': json.dumps({"preprocessing": "Unsupported video file extension. Only .mp4, .MP4, .mov, and .MOV are allowed."})
52 | }
53 |
54 | # Parameterize
55 | video_name_param = bindparam('name')
56 | uploaded_at_param = bindparam('uploaded_at')
57 |
58 | upsert = db_insert(Videos).values(
59 | name=video_name_param,
60 | uploaded_at=uploaded_at_param
61 | )
62 |
63 | upsert = upsert.on_conflict_do_update(
64 | constraint=f"{video_table_name}_pkey",
65 | set_={
66 | Videos.uploaded_at: uploaded_at_param
67 | }
68 | )
69 |
70 |
71 | configuration_parameter_json = ssm.get_parameter(
72 | Name=configuration_parameter_name
73 | )['Parameter']['Value']
74 | configuration_parameter = json.loads(configuration_parameter_json)
75 |
76 |
77 | with session.begin():
78 | date_now = datetime.now(timezone.utc)
79 | session.execute(upsert, {
80 | "name": video_name,
81 | "uploaded_at": date_now
82 | })
83 | session.commit()
84 |
85 | return {
86 | 'statusCode': 200,
87 | 'body': {
88 | "preprocessing": "success",
89 | CONFIG_LABEL_DETECTION_ENABLED:configuration_parameter[CONFIG_LABEL_DETECTION_ENABLED],
90 | CONFIG_TRANSCRIPTION_ENABLED:configuration_parameter[CONFIG_TRANSCRIPTION_ENABLED]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/preprocessing_lambda/requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary>=2.9.9
2 | SQLAlchemy>=2.0.31
3 | pgvector>=0.3.2
--------------------------------------------------------------------------------
/lib/prompt_setup_lambda/index.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import json
3 | import base64
4 |
5 | def on_event(event, context):
6 | print(event)
7 | request_type = event['RequestType'].lower()
8 |
9 | if request_type == 'create' or request_type == 'update':
10 | return on_create(event)
11 | if request_type == 'delete':
12 | return on_delete(event)
13 | raise Exception(f"Invalid request type: {request_type}")
14 |
15 | def on_create(event):
16 | props = event["ResourceProperties"]
17 | bedrock = boto3.client('bedrock-agent')
18 | ssm = boto3.client('ssm')
19 |
20 | # Base64 encode the prompts to safely handle special characters
21 | system_prompt = base64.b64decode(props['visual_extraction_system_prompt_text']).decode('utf-8')
22 | task_prompt = base64.b64decode(props['visual_extraction_task_prompt_text']).decode('utf-8')
23 |
24 | # Create prompt
25 | prompt = bedrock.create_prompt(
26 | name=props['visual_extraction_prompt_name'],
27 | description=props['visual_extraction_prompt_description'],
28 | defaultVariant=props['visual_extraction_prompt_variant_name'],
29 | variants=[{
30 | 'name': props['visual_extraction_prompt_variant_name'],
31 | 'templateConfiguration': {
32 | 'chat': {
33 | 'messages': [
34 | {
35 | 'content': [
36 | {
37 | 'text': task_prompt
38 | },
39 | ],
40 | 'role': 'user'
41 | },
42 | ],
43 | 'system': [
44 | {
45 | 'text': system_prompt
46 | },
47 | ]
48 | }
49 | },
50 | 'templateType': 'CHAT'
51 | }]
52 | )
53 |
54 | # Create prompt version
55 | prompt_version = bedrock.create_prompt_version(
56 | promptIdentifier=prompt['id'],
57 | description=props['visual_extraction_prompt_version_description']
58 | )['version']
59 |
60 | # Get current parameter store value
61 | try:
62 | parameter = ssm.get_parameter(Name=props['configuration_parameter_name'])
63 | config = json.loads(parameter['Parameter']['Value'])
64 | except ssm.exceptions.ParameterNotFound:
65 | config = {}
66 |
67 | # Update prompt configuration
68 | config['visual_extraction_prompt'] = {
69 | "prompt_id": prompt['id'],
70 | "variant_name": props['visual_extraction_prompt_variant_name'],
71 | "version_id": prompt_version
72 | }
73 |
74 | # Update parameter store
75 | ssm.put_parameter(
76 | Name=props['configuration_parameter_name'],
77 | Value=json.dumps(config),
78 | Type='String',
79 | Overwrite=True
80 | )
81 |
82 | return {
83 | 'PhysicalResourceId': f"{prompt['id']}|{prompt_version}",
84 | 'Data': {
85 | 'PromptId': prompt['id'],
86 | 'VersionId': prompt_version
87 | }
88 | }
89 |
90 | def on_delete(event):
91 | physical_id = event['PhysicalResourceId']
92 | prompt_id, version_id = physical_id.split('|')
93 | bedrock = boto3.client('bedrock-agent')
94 |
95 | try:
96 | bedrock.delete_prompt(promptIdentifier=prompt_id)
97 | except:
98 | pass
99 |
100 | return {
101 | 'PhysicalResourceId': physical_id
102 | }
--------------------------------------------------------------------------------
/lib/prompt_setup_lambda/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3>=1.35.84
2 | botocore>=1.35.84
3 | awscli>=1.36.25
--------------------------------------------------------------------------------
/lib/videos_search/lambda-handler.py:
--------------------------------------------------------------------------------
1 | import os, json
2 | import boto3
3 | import urllib.parse
4 | from sqlalchemy import create_engine, Column, DateTime, String, Text
5 | from sqlalchemy.ext.declarative import declarative_base
6 | from sqlalchemy.orm import sessionmaker, mapped_column
7 | from sqlalchemy.sql import bindparam
8 | from pgvector.sqlalchemy import Vector
9 | from datetime import datetime
10 |
11 | bedrock = boto3.client("bedrock-runtime")
12 | secrets_manager = boto3.client('secretsmanager')
13 |
14 | reader_endpoint = os.environ['DB_READER_ENDPOINT']
15 | database_name = os.environ['DATABASE_NAME']
16 | video_table_name = os.environ['VIDEO_TABLE_NAME']
17 | secret_name = os.environ['SECRET_NAME']
18 | embedding_model_id = os.environ["EMBEDDING_MODEL_ID"]
19 | embedding_dimension = int(os.environ['EMBEDDING_DIMENSION'])
20 | acceptable_embedding_distance = float(os.environ['ACCEPTABLE_EMBEDDING_DISTANCE'])
21 | display_page_size = int(os.environ['DISPLAY_PAGE_SIZE'])
22 |
23 | credentials = json.loads(secrets_manager.get_secret_value(SecretId=secret_name)["SecretString"])
24 | username = credentials["username"]
25 | password = credentials["password"]
26 |
27 | engine = create_engine(f'postgresql://{username}:{password}@{reader_endpoint}:5432/{database_name}')
28 | Base = declarative_base()
29 |
30 | Session = sessionmaker(bind=engine)
31 | session = Session()
32 |
33 | class Videos(Base):
34 | __tablename__ = video_table_name
35 |
36 | name = Column(String(200), primary_key=True, nullable=False)
37 | uploaded_at = Column(DateTime(timezone=True), nullable=False)
38 | summary = Column(Text)
39 | summary_embedding = mapped_column(Vector(int(embedding_dimension)))
40 |
41 |
42 | def handler(event, context):
43 | params = event["queryStringParameters"]
44 |
45 | page = int(params["page"])
46 | video_name_contains = urllib.parse.unquote(params["videoNameContains"]) if "videoNameContains" in params else None
47 | uploaded_between= urllib.parse.unquote(params["uploadedBetween"]) if "uploadedBetween" in params else None
48 | about = urllib.parse.unquote(params["about"]) if "about" in params else None
49 |
50 | # Use SQLAlchemy to search videos with the 3 filters above.
51 | videos = session.query(Videos.name)
52 | if video_name_contains is not None:
53 | video_name_contains_param = bindparam("name")
54 | videos = videos.filter(Videos.name.like(video_name_contains_param))
55 | if uploaded_between is not None:
56 | # Assume uploaded_between is like 2024-02-07T16:00:00.000Z|2024-02-15T16:00:00.000Z
57 | start, stop = uploaded_between.split("|")
58 | start = datetime.strptime(start[:-5], "%Y-%m-%dT%H:%M:%S")
59 | stop = datetime.strptime(stop[:-5], "%Y-%m-%dT%H:%M:%S")
60 | start_param = bindparam("start")
61 | stop_param = bindparam("stop")
62 | videos = videos.filter(Videos.uploaded_at.between(start_param, stop_param))
63 | if about is not None:
64 | # Get the embedding for the video topic
65 | body = json.dumps({
66 | "texts":[about],
67 | "input_type": "search_query",
68 | })
69 | call_done = False
70 | while(not call_done):
71 | try:
72 | response = bedrock.invoke_model(body=body, modelId=embedding_model_id)
73 | call_done = True
74 | except ThrottlingException:
75 | print("Amazon Bedrock throttling exception")
76 | time.sleep(60)
77 | except Exception as e:
78 | raise e
79 |
80 | # Disabling semgrep rule for checking data size to be loaded to JSON as the source is from Amazon Bedrock
81 | # nosemgrep: python.aws-lambda.deserialization.tainted-json-aws-lambda.tainted-json-aws-lambda
82 | about_embedding = json.loads(response.get("body").read().decode())['embeddings'][0]
83 |
84 | videos = videos.filter(Videos.summary_embedding.cosine_distance(about_embedding) < acceptable_embedding_distance)
85 | videos = videos.order_by(Videos.summary_embedding.cosine_distance(about_embedding))
86 |
87 | videos = videos.order_by(Videos.uploaded_at.desc())
88 |
89 | if video_name_contains is not None:
90 | videos = videos.params(name=f"%{video_name_contains}%")
91 | if uploaded_between is not None:
92 | videos = videos.params(start=start, stop=stop)
93 |
94 | videos = videos.offset(page*display_page_size).limit(display_page_size+1)
95 |
96 | video_names = videos.all()
97 | video_names = [v.name for v in video_names]
98 |
99 | next_page = None
100 | if len(video_names) > display_page_size:
101 | video_names = video_names[:display_page_size]
102 | next_page = page + 1
103 |
104 | response_payload = {
105 | "videos": video_names,
106 | "pageSize": display_page_size,
107 | }
108 | if next_page is not None: response_payload['nextPage'] = next_page
109 |
110 | response = {
111 | "statusCode": 200,
112 | "headers": {
113 | "Content-Type": "application/json",
114 | "Access-Control-Allow-Origin": "*"
115 | },
116 | "body": json.dumps(response_payload)
117 | }
118 |
119 | return response
--------------------------------------------------------------------------------
/lib/videos_search/requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary>=2.9.9
2 | SQLAlchemy>=2.0.31
3 | boto3>=1.34.150
4 | awscli>=1.33.32
5 | botocore>=1.34.150
6 | pgvector>=0.3.2
--------------------------------------------------------------------------------
/utils/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -d "venv" ]; then
4 | source venv/bin/activate
5 | fi
6 |
7 | export NVM_DIR="$HOME/.nvm"
8 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
9 |
10 | nvm use 20.10.0 || nvm use 16.20.2
11 | node -e "console.log('Currently running Node.js ' + process.version)"
12 |
13 | account=$(aws sts get-caller-identity | jq -r '.Account')
14 | export CDK_DEPLOY_ACCOUNT=$account
15 |
16 | environment="dev" # This is a default value
17 | echo "Bootstrap is using default value for environment with value = dev"
18 |
19 | default_region="us-west-2" # This is a default value which is expected to be overridden by user input.
20 | echo ""
21 | read -p "Which AWS region to deploy this solution to? (default: $default_region) : " region
22 | region=${region:-$default_region}
23 | export CDK_DEPLOY_REGION=$region
24 | echo $region > .DEFAULT_REGION
25 |
26 | npm --prefix ./webui install ./webui
27 |
28 | cd webui && find . -name 'ui_repo*.zip' -exec rm {} \; && zip -r "ui_repo$(date +%s).zip" src package.json package-lock.json public && cd ..
29 |
30 | cdk bootstrap aws://${account}/${region} --context environment=$environment
31 |
32 | if [ -d "venv" ]; then
33 | deactivate
34 | fi
--------------------------------------------------------------------------------
/utils/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -d "venv" ]; then
4 | source venv/bin/activate
5 | fi
6 |
7 | export NVM_DIR="$HOME/.nvm"
8 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
9 |
10 | nvm use 20.10.0 || nvm use 16.20.2
11 | node -e "console.log('Currently running Node.js ' + process.version)"
12 |
13 | account=$(aws sts get-caller-identity | jq -r '.Account')
14 | export CDK_DEPLOY_ACCOUNT=$account
15 |
16 | default_region="us-west-2" # This is a default value which is expected to be overridden by user input.
17 | if [ -f .DEFAULT_REGION ]
18 | then
19 | default_region=$(cat .DEFAULT_REGION)
20 | fi
21 | echo ""
22 | email="" # This is a default value
23 | read -p "The admin email for the video management: " email
24 | email=${email:-$email}
25 |
26 | read -p "Which AWS region to deploy this solution to? (default: $default_region) : " region
27 | region=${region:-$default_region}
28 | export CDK_DEPLOY_REGION=$region
29 |
30 | npm --prefix ./webui install ./webui
31 |
32 | cd webui && find . -name 'ui_repo*.zip' -exec rm {} \; && zip -r "ui_repo$(date +%s).zip" src package.json package-lock.json public && cd ..
33 |
34 | cdk deploy --outputs-file ./deployment-output.json --context email=$email
35 |
36 | if [ -d "venv" ]; then
37 | deactivate
38 | fi
39 |
--------------------------------------------------------------------------------
/utils/destroy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -d "venv" ]; then
4 | source venv/bin/activate
5 | fi
6 |
7 | export NVM_DIR="$HOME/.nvm"
8 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
9 |
10 | nvm use 20.10.0 || nvm use 16.20.2
11 | node -e "console.log('Currently running Node.js ' + process.version)"
12 |
13 | account=$(aws sts get-caller-identity | jq -r '.Account')
14 | export CDK_DEPLOY_ACCOUNT=$account
15 |
16 | default_region="us-west-2" # This is a default value which is expected to be overridden by user input.
17 | if [ -f .DEFAULT_REGION ]
18 | then
19 | default_region=$(cat .DEFAULT_REGION)
20 | fi
21 | echo ""
22 |
23 | read -p "Which AWS region is the solution to be destroyed currently in? (default: $default_region) : " region
24 | region=${region:-$default_region}
25 | export CDK_DEPLOY_REGION=$region
26 |
27 | cdk destroy --context email="placeholder"
28 |
29 | if [ -d "venv" ]; then
30 | deactivate
31 | fi
32 |
--------------------------------------------------------------------------------
/utils/install_prerequisites_for_amazon_linux_2.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install Python if not installed or if the version is below the required one
4 | needPython=false
5 | version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
6 | parsedVersion=$(echo "${version//./}")
7 | if [[ -z "$version" ]]
8 | then
9 | needPython=true
10 | else
11 | if { echo $version; echo "3.8.0"; } | sort --version-sort --check
12 | then
13 | needPython=true
14 | fi
15 | fi
16 |
17 | if [[ "$needPython" = true ]]
18 | then
19 | sudo yum install -y gcc openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel
20 | mkdir packages
21 | cd packages
22 | curl https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz -o Python-3.9.18.tgz
23 | tar xzf Python-3.9.18.tgz && cd Python-3.9.18
24 | ./configure --enable-optimizations
25 | sudo make altinstall
26 | cd .. && sudo rm ./Python-3.9.18.tgz && sudo rm -rf ./Python-3.9.18
27 | cd .. && rm -rf ./packages
28 | fi
29 |
30 | # Install pip
31 | python3 -m ensurepip
32 |
33 | # Install VirtualEnv if not installed
34 | isVirtualEnvInstalled=$(python3 -m pip list | grep "virtualenv")
35 | if [[ -z "$isVirtualEnvInstalled" ]]
36 | then
37 | sudo python3 -m pip install virtualenv
38 | fi
39 |
40 | if [[ "$needPython" = true ]]
41 | then
42 | virtualenv -p $(which python3.9) venv
43 | else
44 | virtualenv -p $(which python3) venv
45 | fi
46 |
47 | # Activate virtual environment
48 | source venv/bin/activate
49 |
50 | # Install zip if not installed
51 | if ! command -v zip &> /dev/null
52 | then
53 | sudo yum update -y
54 | sudo yum install zip -y
55 | fi
56 |
57 | # Install unzip if not installed
58 | if ! command -v unzip &> /dev/null
59 | then
60 | sudo yum update -y
61 | sudo yum install unzip -y
62 | fi
63 |
64 | # Install NPM
65 | curl -o /tmp/nvm_install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh
66 | /bin/bash /tmp/nvm_install.sh
67 | . ~/.nvm/nvm.sh
68 |
69 | export NVM_DIR="$HOME/.nvm"
70 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
71 |
72 | nvm install v16.20.2
73 | nvm use 16.20.2
74 | rm /tmp/nvm_install.sh
75 |
76 | node -e "console.log('Currently running Node.js ' + process.version)"
77 | npm install -g aws-cdk@">=2.173.1"
78 | npm install -g @aws-cdk/aws-amplify-alpha@">=2.173.1-alpha.0"
79 |
80 | python3 -m pip install --upgrade pip
81 | python3 -m pip install --upgrade "aws-cdk-lib>=2.173.1"
82 | python3 -m pip install --upgrade "aws-cdk.aws-amplify-alpha"
83 | python3 -m pip install --upgrade "cdk-nag>=2.34.8"
84 |
85 | # Install Docker if not installed. It will be needed to build the code for the AWS Lambda functions during CDK deployment.
86 | if ! command -v docker &> /dev/null
87 | then
88 | sudo yum update -y
89 | sudo yum install -y docker
90 | fi
91 |
92 | # Give users permissions to access docker
93 | sudo groupadd docker
94 | sudo usermod -a -G docker ec2-user
95 | sudo usermod -a -G docker ssm-user
96 | # IMPORTANT #
97 | # If you are using OS user other than ec2-user and ssm-user, also add them to the `docker` group below. Edit appropriately and uncomment below line. You can run `whoami` to get your OS user name.
98 | # sudo usermod -a -G docker
99 | # --------- #
100 |
101 | # Start Docker service
102 | sudo service docker start
103 |
104 | # Install JQ if not installed
105 | if ! command -v jq &> /dev/null
106 | then
107 | sudo yum update -y
108 | sudo yum install jq -y
109 | fi
110 |
111 | # Install AWS CLI if not installed
112 | if ! command -v aws &> /dev/null
113 | then
114 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
115 | unzip awscliv2.zip
116 | sudo ./aws/install && sudo rm awscliv2.zip
117 | fi
118 |
119 | # Deactivate virtual environment
120 | deactivate
--------------------------------------------------------------------------------
/utils/install_prerequisites_for_amazon_linux_2023.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install Python if not installed or if the version is below the required one
4 | needPython=false
5 | version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
6 | parsedVersion=$(echo "${version//./}")
7 | if [[ -z "$version" ]]
8 | then
9 | needPython=true
10 | else
11 | if { echo $version; echo "3.8.0"; } | sort --version-sort --check
12 | then
13 | needPython=true
14 | fi
15 | fi
16 |
17 | if [[ "$needPython" = true ]]
18 | then
19 | sudo yum install -y gcc openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel
20 | mkdir packages
21 | cd packages
22 | curl https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz -o Python-3.9.18.tgz
23 | tar xzf Python-3.9.18.tgz && cd Python-3.9.18
24 | ./configure --enable-optimizations
25 | sudo make altinstall
26 | cd .. && sudo rm ./Python-3.9.18.tgz && sudo rm -rf ./Python-3.9.18 && cd ..
27 | cd .. && rm -rf ./packages
28 | fi
29 |
30 | # Install pip
31 | python3 -m ensurepip
32 |
33 | # Install VirtualEnv if not installed
34 | isVirtualEnvInstalled=$(python3 -m pip list | grep "virtualenv")
35 | if [[ -z "$isVirtualEnvInstalled" ]]
36 | then
37 | python3 -m pip install --user virtualenv
38 | fi
39 |
40 | if [[ "$needPython" = true ]]
41 | then
42 | virtualenv -p $(which python3.9) venv
43 | else
44 | virtualenv -p $(which python3) venv
45 | fi
46 |
47 | # Activate virtual environment
48 | source venv/bin/activate
49 |
50 | # Install zip if not installed
51 | if ! command -v zip &> /dev/null
52 | then
53 | sudo yum update -y
54 | sudo yum install zip -y
55 | fi
56 |
57 | # Install unzip if not installed
58 | if ! command -v unzip &> /dev/null
59 | then
60 | sudo yum update -y
61 | sudo yum install unzip -y
62 | fi
63 |
64 | # Install NPM
65 | curl -o /tmp/nvm_install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh
66 | /bin/bash /tmp/nvm_install.sh
67 | . ~/.nvm/nvm.sh
68 |
69 | export NVM_DIR="$HOME/.nvm"
70 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
71 |
72 | nvm install v20.10.0
73 | nvm use 20.10.0
74 | rm /tmp/nvm_install.sh
75 |
76 | node -e "console.log('Currently running Node.js ' + process.version)"
77 | npm install -g aws-cdk@">=2.173.1"
78 | npm install -g @aws-cdk/aws-amplify-alpha@">=2.173.1-alpha.0"
79 |
80 | python3 -m pip install --upgrade pip
81 | python3 -m pip install --upgrade "aws-cdk-lib>=2.173.1"
82 | python3 -m pip install --upgrade "aws-cdk.aws-amplify-alpha"
83 | python3 -m pip install --upgrade "cdk-nag>=2.34.8"
84 |
85 | # Install Docker if not installed. It will be needed to build the code for the AWS Lambda functions during CDK deployment.
86 | if ! command -v docker &> /dev/null
87 | then
88 | sudo yum update -y
89 | sudo yum install -y docker
90 | fi
91 |
92 | # Give users permissions to access docker
93 | sudo groupadd docker
94 | sudo usermod -a -G docker ec2-user
95 | sudo usermod -a -G docker ssm-user
96 | # IMPORTANT #
97 | # If you are using OS user other than ec2-user and ssm-user, also add them to the `docker` group below. Edit appropriately and uncomment below line. You can run `whoami` to get your OS user name.
98 | # sudo usermod -a -G docker
99 | # --------- #
100 |
101 |
102 | # Start Docker service
103 | sudo service docker start
104 |
105 | # Install JQ if not installed
106 | if ! command -v jq &> /dev/null
107 | then
108 | sudo yum update -y
109 | sudo yum install jq -y
110 | fi
111 |
112 | # Install AWS CLI if not installed
113 | if ! command -v aws &> /dev/null
114 | then
115 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
116 | unzip awscliv2.zip
117 | sudo ./aws/install && sudo rm awscliv2.zip
118 | fi
119 |
120 | # Deactivate virtual environment
121 | deactivate
--------------------------------------------------------------------------------
/utils/install_prerequisites_for_macos_ventura.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install brew if not there
4 | if ! command -v brew &> /dev/null
5 | then
6 | curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew_install.sh
7 | /bin/bash /tmp/brew_install.sh
8 | brew update
9 | rm /tmp/brew_install.sh
10 | fi
11 |
12 | # Install Python if not installed or if the version is below the required one
13 | needPython=false
14 | version=$(python3 --version | sed 's/.* \([0-9\.]*\).*/\1/')
15 | parsedVersion=$(echo "${version//./}")
16 | if [[ -z "$version" ]]
17 | then
18 | needPython=true
19 | else
20 | if { echo $version; echo "3.8.0"; } | sort --version-sort --check
21 | then
22 | needPython=true
23 | fi
24 | fi
25 |
26 | if [[ "$needPython" = true ]]
27 | then
28 | brew install python@3.9
29 | fi
30 |
31 | # Install pip
32 | python3 -m ensurepip
33 |
34 | # Install VirtualEnv if not installed
35 | isVirtualEnvInstalled=$(python3 -m pip list | grep "virtualenv")
36 | if [[ -z "$isVirtualEnvInstalled" ]]
37 | then
38 | python3 -m pip install --user virtualenv
39 | fi
40 |
41 | if [[ "$needPython" = true ]]
42 | then
43 | virtualenv -p $(which python3.9) venv
44 | else
45 | virtualenv -p $(which python3) venv
46 | fi
47 |
48 | # Activate virtual environment
49 | source venv/bin/activate
50 |
51 | # Install zip if not installed
52 | if ! command -v zip &> /dev/null
53 | then
54 | brew install zip
55 | fi
56 |
57 | # Install unzip if not installed
58 | if ! command -v unzip &> /dev/null
59 | then
60 | brew install unzip
61 | fi
62 |
63 | # Install NPM if not installed
64 | brew install nvm
65 |
66 | export NVM_DIR="$HOME/.nvm"
67 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
68 |
69 | nvm install v20.10.0
70 | nvm use 20.10.0
71 |
72 | node -e "console.log('Currently running Node.js ' + process.version)"
73 | sudo npm install -g aws-cdk@">=2.173.1"
74 | sudo npm install -g @aws-cdk/aws-amplify-alpha@">=2.173.1-alpha.0"
75 |
76 | python3 -m pip install --upgrade pip
77 | python3 -m pip install --upgrade "aws-cdk-lib>=2.173.1"
78 | python3 -m pip install --upgrade "aws-cdk.aws-amplify-alpha"
79 | python3 -m pip install --upgrade "cdk-nag>=2.34.8"
80 |
81 | # Install Docker if not installed. It will be needed to build the code for the AWS Lambda functions during CDK deployment.
82 | if ! command -v docker &> /dev/null
83 | then
84 | brew install --cask docker
85 | fi
86 |
87 | # Install JQ if not installed
88 | if ! command -v jq &> /dev/null
89 | then
90 | brew install jq
91 | fi
92 |
93 | # Install AWS CLI if not installed
94 | if ! command -v aws &> /dev/null
95 | then
96 | curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
97 | sudo installer -pkg AWSCLIV2.pkg -target /
98 | fi
99 |
100 | # Open docker
101 | open /Applications/Docker.app
102 |
103 | # Deactivate virtual environment
104 | deactivate
105 |
--------------------------------------------------------------------------------
/utils/scan.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python3 -m pip install bandit
4 | bandit -r . -o banditreport.txt -f txt --exclude "./cdk.out/,./webui/node_modules/,./venv/"
5 |
6 | python3 -m pip install semgrep
7 | semgrep login
8 | semgrep scan --exclude "./cdk.out/" -o semgrepreport.txt &> semgreplog.txt
9 |
10 | cd webui && npm i --package-lock-only && npm audit --json > ../npmauditreport.json
11 | cd ..
--------------------------------------------------------------------------------
/webui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-understanding-solution",
3 | "version": "1.2.2",
4 | "private": true,
5 | "dependencies": {
6 | "@aws-amplify/ui-react": "^6.7.2",
7 | "@aws-cdk/aws-amplify-alpha": "^2.173.2-alpha.0",
8 | "@aws-sdk/client-bedrock-runtime": "^3.714.0",
9 | "@aws-sdk/client-s3": "^3.715.0",
10 | "@aws-sdk/s3-request-presigner": "^3.715.0",
11 | "@aws-sdk/credential-providers": "^3.715.0",
12 | "@aws-sdk/client-cognito-identity": "^3.714.0",
13 | "aws-amplify": "^6.10.3",
14 | "bootstrap": "^5.3.3",
15 | "react": "^18.3.1",
16 | "react-bootstrap": "^2.10.7",
17 | "react-dom": "^18.3.1",
18 | "react-scripts": "5.0.1",
19 | "web-vitals": "^4.2.4",
20 | "@svgr/webpack": "^8.1.0",
21 | "postcss": "^8.4.49",
22 | "react-datepicker": "^7.5.0"
23 | },
24 | "overrides": {
25 | "@svgr/webpack": "$@svgr/webpack",
26 | "postcss": "$postcss"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/webui/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Video Understanding Solution
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/webui/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/webui/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/webui/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
40 | #VideoTableContainer {
41 | margin-top: 20px;
42 | }
43 |
--------------------------------------------------------------------------------
/webui/src/App.js:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import "@aws-amplify/ui-react/styles.css";
3 | import { VideoTable } from "./VideoTable/VideoTable";
4 | import { VideoUpload } from "./VideoUpload/VideoUpload";
5 | import awsExports from "./aws-exports";
6 | import { Amplify } from "aws-amplify";
7 | import { withAuthenticator } from "@aws-amplify/ui-react";
8 | import type { WithAuthenticatorProps } from "@aws-amplify/ui-react";
9 | import { fetchAuthSession } from "aws-amplify/auth";
10 | import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
11 | import {
12 | CognitoIdentityClient,
13 | GetIdCommand,
14 | } from "@aws-sdk/client-cognito-identity";
15 | import { S3Client } from "@aws-sdk/client-s3";
16 | import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
17 |
18 | import Container from "react-bootstrap/Container";
19 | import Row from "react-bootstrap/Row";
20 | import Col from "react-bootstrap/Col";
21 | import Navbar from "react-bootstrap/Navbar";
22 | import { useEffect, useState } from "react";
23 |
24 | Amplify.configure({
25 | Auth: {
26 | Cognito: {
27 | userPoolId: awsExports.aws_user_pools_id,
28 | userPoolClientId: awsExports.aws_user_pools_web_client_id,
29 | },
30 | },
31 | });
32 |
33 | const REGION = awsExports.aws_cognito_region;
34 | const COGNITO_ID = `cognito-idp.${REGION}.amazonaws.com/${awsExports.aws_user_pools_id}`;
35 |
36 | function App({ signOut, user }: WithAuthenticatorProps) {
37 | const [s3Client, setS3Client] = useState(null);
38 | const [bedrockClient, setBedrockClient] = useState(null);
39 | const [tokens, setTokens] = useState(null);
40 |
41 | const getTs = async () => {
42 | return (await fetchAuthSession()).tokens;
43 | }
44 |
45 | const getCredentials = async (ts) => {
46 | let idT = (await ts).idToken.toString();
47 |
48 | const cognitoidentity = new CognitoIdentityClient({
49 | credentials: fromCognitoIdentityPool({
50 | clientConfig: { region: awsExports.aws_cognito_region },
51 | identityPoolId: awsExports.aws_cognito_identity_pool_id,
52 | logins: {
53 | [COGNITO_ID]: idT,
54 | },
55 | }),
56 | });
57 | return await cognitoidentity.config.credentials();
58 | };
59 |
60 | useEffect(() => {
61 | const initializeClients = async () => {
62 | const ts = await getTs();
63 | const crs = await getCredentials(ts);
64 |
65 | setTokens(ts);
66 |
67 | setS3Client(new S3Client({
68 | region: REGION,
69 | credentials: crs,
70 | }));
71 |
72 | setBedrockClient(new BedrockRuntimeClient({
73 | region: REGION,
74 | credentials: crs,
75 | }));
76 | };
77 |
78 | initializeClients();
79 | const interval = setInterval(initializeClients, 3300000); // Refresh every 55 minutes
80 |
81 | return () => clearInterval(interval);
82 | }, []);
83 |
84 | if (!s3Client || !bedrockClient || !tokens) return (Loading...
)
85 |
86 | return (
87 |
88 |
89 |
90 | Video Understanding Solution
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | }
138 |
139 | export default withAuthenticator(App, { hideSignUp: true });
140 |
--------------------------------------------------------------------------------
/webui/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/webui/src/VideoTable/VideoTable.css:
--------------------------------------------------------------------------------
1 | .preserve-line-breaks {
2 | white-space: pre-wrap;
3 | }
4 | .page-spinner{
5 | margin-top: 100px;
6 | }
7 | .chat-spinner{
8 | margin-bottom: 5px;
9 | }
10 | .paragraph-with-newlines{
11 | white-space: pre-line
12 | }
13 | .left-align-no-bottom-margin{
14 | text-align: left;
15 | margin-bottom: 0px;
16 | }
17 | .search-button{
18 | margin-top: 15px;
19 | }
20 | .find-part-result{
21 | text-align:left;
22 | margin-top:5px
23 | }
--------------------------------------------------------------------------------
/webui/src/VideoTable/VideoTable.js:
--------------------------------------------------------------------------------
1 | import React, { Component, useState } from 'react';
2 | import { Buffer } from "buffer";
3 | import Accordion from 'react-bootstrap/Accordion';
4 | import Collapse from 'react-bootstrap/Collapse';
5 | import Button from 'react-bootstrap/Button';
6 | import Form from 'react-bootstrap/Form';
7 | import InputGroup from 'react-bootstrap/InputGroup';
8 | import Dropdown from 'react-bootstrap/Dropdown';
9 | import DropdownButton from 'react-bootstrap/DropdownButton';
10 | import Pagination from 'react-bootstrap/Pagination';
11 | import Spinner from 'react-bootstrap/Spinner';
12 | import DatePicker from "react-datepicker";
13 |
14 | import {ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
15 | import { InvokeModelWithResponseStreamCommand, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
16 | import {getSignedUrl} from "@aws-sdk/s3-request-presigner"
17 |
18 | import Row from 'react-bootstrap/Row';
19 | import Col from 'react-bootstrap/Col';
20 |
21 | import './VideoTable.css';
22 | import "react-datepicker/dist/react-datepicker.css";
23 |
24 | export class VideoTable extends Component {
25 | constructor(props) {
26 | super(props);
27 |
28 | this.fastModelId= props.fastModelId
29 | this.balancedModelId = props.balancedModelId
30 |
31 | this.state = {
32 | videos: [],
33 | pages: {},
34 | firstFetch: false,
35 | pageLoading: false,
36 | searchByNameText: "",
37 | searchByStartDate: null,
38 | searchByEndDate: null,
39 | searchByAboutText: "",
40 | chatModelChoice: "Fastest" // as default
41 | };
42 |
43 | this.s3Client = props.s3Client
44 | this.bedrockClient = props.bedrockClient
45 | this.bucketName = props.bucketName
46 | this.rawFolder = props.rawFolder
47 | this.summaryFolder = props.summaryFolder
48 | this.transcriptionFolder = props.transcriptionFolder
49 | this.videoScriptFolder = props.videoScriptFolder
50 | this.videoCaptionFolder = props.videoCaptionFolder
51 | this.entitySentimentFolder = props.entitySentimentFolder
52 | this.restApiUrl = props.restApiUrl
53 | this.videosApiResource = props.videosApiResource
54 | this.cognitoTs = props.cognitoTs
55 | this.maxCharactersForChat = 5000
56 | this.maxCharactersForVideoScript = 300000
57 | this.maxCharactersForSingleLLMCall = 100000
58 | this.estimatedCharactersPerToken = 4
59 |
60 | this.renderVideoTableBody = this.renderVideoTableBody.bind(this)
61 | this.render = this.render.bind(this)
62 | this.renderChat = this.renderChat.bind(this)
63 |
64 | }
65 |
66 | async componentDidMount() {
67 | var [videos, pages] = await this.fetchVideos(0)
68 | this.setState({videos: videos, pages: pages, firstFetch: true})
69 | }
70 |
71 | async fetchVideos(page){
72 | var videos = []
73 |
74 | this.setState({
75 | pageLoading: true
76 | })
77 |
78 | // Construct filter
79 | var params = `?page=${page.toString()}&`
80 | if(this.state.searchByNameText != ""){
81 | params += `videoNameContains=${encodeURI(this.state.searchByNameText)}&`
82 | }
83 | if(this.state.searchByStartDate != null && this.state.searchByStartDate != null ){
84 | const startDateString = this.state.searchByStartDate.toISOString()
85 | const endDateString = this.state.searchByEndDate.toISOString()
86 | params += `uploadedBetween=${encodeURI(startDateString + "|" + endDateString)}&`
87 | }
88 | if(this.state.searchByAboutText != ""){
89 | params += `about=${encodeURI(this.state.searchByAboutText)}`
90 | }
91 |
92 | // Call API Gateway to fetch the video names
93 | var ts = await this.cognitoTs;
94 |
95 | const response = await fetch(`${this.restApiUrl}/${this.videosApiResource}${params}`, {
96 | method: 'GET',
97 | headers: {
98 | 'Content-Type': 'application/json',
99 | 'Authorization': ts.idToken.toString(),
100 | }
101 | })
102 |
103 | const responseBody = await(response.json())
104 |
105 | this.setState({
106 | pageLoading: false
107 | })
108 |
109 | if(responseBody.videos.length == 0) return [videos, this.state.pages];
110 |
111 | // Add the video representation in the UI
112 | for (const i in responseBody.videos){
113 | const videoName = responseBody.videos[i]
114 |
115 | videos.push({
116 | index: videos.length,
117 | name: videoName,
118 | loaded: false,
119 | summary: undefined,
120 | rawEntities: undefined,
121 | entities: undefined,
122 | videoScript: undefined,
123 | videoCaption: undefined,
124 | videoShown: false,
125 | chats: [],
126 | chatSummary: "",
127 | chatLength: 0,
128 | chatWaiting: false,
129 | findPartWaiting: false,
130 | findPartResult: undefined,
131 | url: undefined
132 | })
133 | }
134 |
135 | // Activate the right page on the pagination bar
136 | var pages = this.state.pages
137 | var pageFound = false
138 | for (const pi in pages){
139 | if(pages[pi].index == page){
140 | pageFound = true
141 | pages[pi] = {
142 | displayName: (page + 1).toString(),
143 | active: true,
144 | index: page
145 | }
146 | }else{
147 | pages[pi].active = false
148 | }
149 | }
150 | // If the page is not found, add it to the pagination bar
151 | if(!pageFound){
152 | pages[page] = {
153 | displayName: (page + 1).toString(),
154 | active: true,
155 | index: page
156 | }
157 | }
158 |
159 | // If there is a next page indicated in the API call return, then either add a new page in pagination bar or just update existing page metadata
160 | if ("nextPage" in responseBody){
161 | pages[page+1] = {
162 | displayName: (page + 2).toString(),
163 | active: false,
164 | index: page + 1
165 | }
166 | }
167 |
168 | return [videos, pages]
169 | }
170 |
171 | async fetchVideoDetails(video){
172 | const name = video.name
173 |
174 | // Try to find the summary
175 | var command = new GetObjectCommand({
176 | Bucket: this.bucketName,
177 | Key: this.summaryFolder + "/" + name + ".txt",
178 | });
179 | try {
180 | const response = await this.s3Client.send(command);
181 | video.summary = await response.Body.transformToString();
182 | } catch (err) {
183 | console.log(err)
184 | }
185 |
186 | // Try to find the entities and sentiment
187 | var command = new GetObjectCommand({
188 | Bucket: this.bucketName,
189 | Key: this.entitySentimentFolder + "/" + name + ".txt",
190 | });
191 | try {
192 | const response = await this.s3Client.send(command);
193 | const rawEntities = await response.Body.transformToString();
194 | video.rawEntities = rawEntities
195 | video.entities = []
196 | rawEntities.split("\n").forEach(entity => {
197 | const data = entity.split("|")
198 | video.entities.push(data)
199 | })
200 | } catch (err) {
201 | console.log(err)
202 | }
203 |
204 |
205 | // Try to get the video script
206 | var command = new GetObjectCommand({
207 | Bucket: this.bucketName,
208 | Key: this.videoScriptFolder + "/" + name + ".txt",
209 | });
210 | try {
211 | const response = await this.s3Client.send(command);
212 | video.videoScript = await response.Body.transformToString();
213 | } catch (err) {}
214 |
215 | // Try to get the video per frame caption
216 | var command = new GetObjectCommand({
217 | Bucket: this.bucketName,
218 | Key: this.videoCaptionFolder + "/" + name + ".txt",
219 | });
220 | try {
221 | const response = await this.s3Client.send(command);
222 | video.videoCaption = await response.Body.transformToString();
223 | } catch (err) {}
224 |
225 | video.loaded = true
226 |
227 | return video
228 | }
229 |
230 | async showVideo(video){
231 | // Generate presigned URL for video
232 | const getObjectParams = {
233 | Bucket: this.bucketName,
234 | Key: this.rawFolder + "/" + video.name
235 | }
236 | const command = new GetObjectCommand(getObjectParams);
237 | video.url = await getSignedUrl(this.s3Client, command, { expiresIn: 180 });
238 | video.videoShown = true
239 | this.setState({videos: this.state.videos})
240 | }
241 |
242 | async videoClicked(video) {
243 | if(!video.loaded) {
244 | video = await this.fetchVideoDetails(video)
245 | this.setState({videos: this.state.videos})
246 | }
247 | }
248 |
249 | handleChatInputChange(video, event) {
250 | if(event.keyCode == 13 && !event.shiftKey) { // When Enter is pressed without Shift
251 | const chatText = event.target.value
252 | video.chats.push({
253 | actor: "You",
254 | text: chatText
255 | })
256 | video.chatLength += chatText.length // Increment the length of the chat
257 | this.setState({videos: this.state.videos})
258 | event.target.value = "" // Clear input field
259 | this.handleChat(video.index)
260 | }
261 | }
262 |
263 | async handleFindPartInputChange(video, event) {
264 | if(event.keyCode == 13 && !event.shiftKey) { // When Enter is pressed without Shift
265 | video.findPartWaiting = true
266 | this.setState({videos: this.state.videos})
267 |
268 | const findPartText = event.target.value
269 | const part = await this.handleFindPart(video.index, findPartText)
270 | if("error" in part){
271 | video.findPartResult = "Part not found"
272 | video.findPartWaiting = false
273 | this.setState({videos: this.state.videos})
274 | }else{
275 | video.findPartResult = `Start at second ${part.start} and stop at second ${part.stop}`
276 | video.findPartWaiting = false
277 | this.setState({videos: this.state.videos})
278 |
279 | if(!video.videoShown){
280 | await this.showVideo(video)
281 | await new Promise(r => setTimeout(r, 5000));
282 | }
283 | const videoElement = document.getElementById(`video-${video.index}-canvas`);
284 | videoElement.currentTime = parseInt(part.start);
285 | await new Promise(r => setTimeout(r, 3000));
286 | videoElement.scrollIntoView()
287 | }
288 |
289 | event.target.value = "" // Clear input field
290 | }
291 | }
292 |
293 | async handleFindPart(videoIndex, findPartText){
294 | var video = this.state.videos[videoIndex]
295 | var systemPrompt = "You can find relevant part of a video given the text presentation of the video called ."
296 |
297 | var prompt = ""
298 | prompt += "Below is the with information about the voice heard in the video, visual scenes seen, visual text visible, and any face or celebrity visible.\n"
299 | prompt += "For voice, the same speaker number ALWAYS indicates the same person throughout the video.\n"
300 | prompt += "For faces, while the estimated age may differ across timestamp, if they are very close, they can refer to the same person.\n"
301 | prompt += "The numbers on the left represents the seconds into the video where the information was extracted.\n"
302 | prompt += `\n${video.videoScript} \n\n`
303 | prompt += "Now your job is to find the part of the video based on the .\n"
304 | prompt += "The answer MUST be expressed as the start and stop second of the relevant part.\n"
305 | prompt += "The \"start\" MUST represents the earliest point of where the relevant part is. Start from the BEGINNING of the context that leads to the relevant part. It is okay to have some buffer for the \"start\" point.\n"
306 | prompt += "The \"stop\" MUST represents the latest point of where the relevant part is. Do not put the \"stop\" in a way that it will stop the clip abruptly. Add some buffer so that the clip has full context.\n"
307 | prompt += "ALWAYS answer in JSON format ONLY. For example:{\"start\":2.0,\"stop\":15.5}\n"
308 | prompt += "If there is no relevant video part, return {\"error\":404}\n\n"
309 | prompt += `${findPartText} \n\n`
310 | prompt += "Your JSON ONLY answer:"
311 |
312 | const input = {
313 | body: JSON.stringify({
314 | anthropic_version: "bedrock-2023-05-31",
315 | system: systemPrompt,
316 | messages: [
317 | { role: "user", content: prompt},
318 | { role: "assistant", content: "Here is my JSON only answer:"}
319 | ],
320 | temperature: 0.1,
321 | max_tokens: 50
322 | }),
323 | modelId: this.balancedModelId,
324 | contentType: "application/json",
325 | accept: "application/json"
326 | };
327 | const response = await this.bedrockClient.send(new InvokeModelCommand(input));
328 | const part = JSON.parse(JSON.parse(new TextDecoder().decode(response.body)).content[0].text.trim())
329 |
330 | return part
331 | }
332 |
333 | renderChat(video) {
334 | return video.chats.map((chat, i) => {
335 | return (
336 | {(chat.actor + " : ")}
{chat.text}
337 | )
338 | })
339 | }
340 |
341 | async retrieveAnswerForShortVideo(video, systemPrompt){
342 | var [videoScriptPrompt, chatPrompt] = ["",""]
343 |
344 | videoScriptPrompt += "To help you, here is the summary of the whole video that you previously wrote:\n"
345 | videoScriptPrompt += `${video.summary}\n\n`
346 | videoScriptPrompt += "Also, here are the entities and sentiment that you previously extracted. Each row is entity|sentiment|sentiment's reason:\n"
347 | videoScriptPrompt += `${video.rawEntities}\n\n`
348 |
349 | videoScriptPrompt += "Below is the video timeline with information about the voice heard in the video, visual scenes seen, visual text visible, and any face or celebrity visible.\n"
350 | videoScriptPrompt += "For voice, the same speaker number ALWAYS indicates the same person throughout the video.\n"
351 | videoScriptPrompt += "For faces, while the estimated age may differ across timestamp, if they are very close, they can refer to the same person.\n"
352 | videoScriptPrompt += "The numbers on the left represents the seconds into the video where the information was extracted.\n"
353 | videoScriptPrompt += `\n${video.videoScript} \n\n`
354 |
355 | // If the chat history is not too long, then include the whole history in the prompt
356 | // Otherwise, use the summary + last chat instead.
357 | // However, when the summary is not yet generated (First generation is when characters exceed 5000), then use the whole history first
358 | // The last scenario mentioned above may happen during the transition of <5000 characters to >5000 characters for the history
359 | if (video.chatLength <= this.maxCharactersForChat || video.chatSummary == ""){
360 | for (const i in video.chats){
361 | if (video.chats[i].actor == "You"){
362 | chatPrompt += "\nUser: " + video.chats[i].text
363 | }else{
364 | chatPrompt += "\nYou: " + video.chats[i].text
365 | }
366 | }
367 | }else{ // If the chat history is too long, so that we include the summary of the chats + last chat only.
368 | chatPrompt += "You had a conversation with a user about that video as summarized below.\n"
369 | chatPrompt += `\n${video.chatSummary}\n \n`
370 | chatPrompt += "Now the user comes back with a reply below.\n"
371 | chatPrompt += `\n${video.chats[video.chats.length - 1].text}\n \n`
372 | chatPrompt += "Answer the user's reply above directly, without any XML tag.\n"
373 | }
374 |
375 | const prompt = videoScriptPrompt + chatPrompt
376 |
377 | const input = {
378 | body: JSON.stringify({
379 | anthropic_version: "bedrock-2023-05-31",
380 | system: systemPrompt,
381 | messages: [
382 | { role: "user", content: prompt},
383 | { role: "assistant", content: "Here is my succinct answer:"}
384 | ],
385 | temperature: 0.1,
386 | max_tokens: 1000,
387 | stop_sequences: ["\nUser:"]
388 | }),
389 | modelId: this.state.chatModelChoice == "Fastest" ? this.fastModelId : this.balancedModelId,
390 | contentType: "application/json",
391 | accept: "application/json"
392 | };
393 | const response = await this.bedrockClient.send(new InvokeModelWithResponseStreamCommand(input));
394 |
395 | await this.displayNewBotAnswerWithStreaming(video, response)
396 | }
397 |
398 | async retrieveAnswerForLongVideo(video, systemPrompt){
399 | const numberOfFragments = parseInt(video.videoScript.length/this.maxCharactersForVideoScript) + 1
400 |
401 | const estimatedTime = (numberOfFragments * 25/60).toFixed(1) // Assuming one LLM call is 25 seconds.
402 | await this.displayNewBotAnswer(video, `Long video is detected. Estimated answer time is ${estimatedTime} minutes or sooner.`)
403 |
404 | var [currentFragment, rollingAnswer] = [1,""]
405 | // For every video fragment, try to find the answer to the question, if and not found just write partial answer and go to next iteration.
406 | while(currentFragment <= numberOfFragments){
407 | var [videoScriptPrompt, chatPrompt, instructionPrompt, partialAnswerPrompt] = ["","","",""]
408 |
409 | videoScriptPrompt += "To help you, here is the summary of the whole video that you previously wrote:\n"
410 | videoScriptPrompt += `${video.summary}\n\n`
411 | videoScriptPrompt += "Also, here are the entities and sentiment that you previously extracted. Each row is entity|sentiment|sentiment's reason:\n"
412 | videoScriptPrompt += `${video.rawEntities}\n\n`
413 |
414 | // Construct the prompt containing the video script (the text representation of the current part of the video)
415 | videoScriptPrompt += "The video timeline has information about the voice heard in the video, visual scenes seen, visual texts visible, and any face or celebrity visible.\n"
416 | videoScriptPrompt += "For voice, the same speaker number ALWAYS indicates the same person throughout the video.\n"
417 | videoScriptPrompt += "For faces, while the estimated age may differ across timestamp, if they are very close, they can refer to the same person.\n"
418 | videoScriptPrompt += "The numbers on the left represents the seconds into the video where the information was extracted.\n"
419 | videoScriptPrompt += `Because the video is long, the video timeline is split into ${numberOfFragments} parts. `
420 | if (currentFragment == numberOfFragments){
421 | videoScriptPrompt += "Below is the last part of the video timeline.\n"
422 | }else if (currentFragment == 1){
423 | videoScriptPrompt += "Below is the first part of the video timeline.\n"
424 | }else{
425 | videoScriptPrompt += `Below is the part ${currentFragment.toString()} out of ${numberOfFragments} of the video timeline.\n`
426 | }
427 |
428 | // The video script part
429 | const currentVideoScriptFragment = video.videoScript.substring((currentFragment - 1)*this.maxCharactersForVideoScript, currentFragment*this.maxCharactersForVideoScript)
430 | videoScriptPrompt += `\n${currentVideoScriptFragment}\n \n\n`
431 |
432 | // Add the conversation between user and bot for context
433 | if (video.chatLength <= this.maxCharactersForChat || video.chatSummary == ""){
434 | chatPrompt += "You had a conversation with a user as below. Your previous answers may be based on the knowledge of the whole video timeline, not only part of it.\n"
435 | chatPrompt += "\n"
436 | for (const i in video.chats){
437 | if (video.chats[i].actor == "You"){
438 | chatPrompt += `\nUser: ${video.chats[i].text}`
439 | }else{
440 | chatPrompt += `\nYou: ${video.chats[i].text}`
441 | }
442 | }
443 | chatPrompt += "\n \n\n"
444 | }else{ // If the chat history is too long, so that we include the summary of the chats + last chat only.
445 | chatPrompt += "You had a conversation with a user as summarized below. Your previous answers may be based on the knowledge of the whole video timeline, not only part of it.\n"
446 | chatPrompt += `\n${video.chatSummary} \n`
447 | chatPrompt += "Now the user comes back with a reply below.\n"
448 | chatPrompt += `\n${video.chats[video.chats.length - 1].text}\n \n\n`
449 | }
450 |
451 | // Add previous partial answer from previous parts of the videos to gather information so far
452 | if (rollingAnswer != ""){
453 | partialAnswerPrompt += "You also saved some partial answer from the previous video parts below. You can use this to provide the final answer or the next partial answer.\n"
454 | partialAnswerPrompt += `\n${rollingAnswer}\n \n\n`
455 | }
456 |
457 | if (currentFragment == (numberOfFragments + 1)){
458 | instructionPrompt += "Now, since there is no more video part left, provide your final answer directly without XML tag. Answer succinctly. Use well-formatted language and do not quote information from video timeline as is, unless asked."
459 | }else{
460 | instructionPrompt += "When answering the question, do not use XML tags and do not mention about video timeline, chat summary, or partial answer.\n"
461 | instructionPrompt += "If you have the final answer already, WRITE |BREAK| at the very end of your answer.\n"
462 | instructionPrompt += "If you stil need the next part to come with final answer, then write any partial answer based on the current , , if any, and .\n"
463 | instructionPrompt += "This partial answer will be included when we ask you again right after this by giving the video timeline for the next part, so that you can use the current partial answer.\n"
464 | instructionPrompt += `For question that needs you to see the whole video such as "is there any?", "does it have", "is it mentioned", DO NOT provide final answer until we give you all ${numberOfFragments} parts of the video timeline.\n`
465 | }
466 |
467 | const prompt = videoScriptPrompt + chatPrompt + partialAnswerPrompt + instructionPrompt
468 |
469 | const input = {
470 | body: JSON.stringify({
471 | anthropic_version: "bedrock-2023-05-31",
472 | system: systemPrompt,
473 | messages: [
474 | { role: "user", content: prompt},
475 | { role: "assistant", content: "Here is my succinct answer:"}
476 | ],
477 | temperature: 0.1,
478 | max_tokens: 1000,
479 | stop_sequences: ["\nUser:"]
480 | }),
481 | modelId: this.state.chatModelChoice == "Fastest" ? this.fastModelId : this.balancedModelId,
482 | contentType: "application/json",
483 | accept: "application/json"
484 | };
485 | const response = await this.bedrockClient.send(new InvokeModelCommand(input));
486 | rollingAnswer = JSON.parse(new TextDecoder().decode(response.body)).content[0].text
487 |
488 | // When the answer is found without having to go through all video fragments, then break and make it as a final answer.
489 | if (rollingAnswer.includes("|BREAK|")) {
490 | rollingAnswer = rollingAnswer.replace("|BREAK|","")
491 | break
492 | }
493 |
494 | currentFragment += 1
495 | }
496 |
497 | await this.displayNewBotAnswer(video, rollingAnswer)
498 | }
499 |
500 | async handleChat(videoIndex, target){
501 | var video = this.state.videos[videoIndex]
502 |
503 | if(video.chats.length <= 0) return // Ignore if no chats are there
504 | if(video.chats[video.chats.length - 1].actor != "You") return // Ignore if last message was from bot
505 |
506 | this.addChatWaitingSpinner(video)
507 |
508 | var systemPrompt = "You are an expert in analyzing video and you can answer questions about the video given the video timeline.\n"
509 | systemPrompt += "Answer in the same language as the question from user.\n"
510 | systemPrompt += "If you do not know, say do not know. DO NOT make up wrong answers.\n"
511 | systemPrompt += "The Human who asks can watch the video and they are not aware of the 'video timeline' which will be copied below. So, when answering, DO NOT indicate the presence of 'video timeline'. DO NOT quote raw information as is from the 'video timeline'\n"
512 | systemPrompt += "Use well-formatted language, sentences, and paragraphs. Use correct grammar and writing rule.\n\n"
513 | systemPrompt += "When answering, answer SUCCINCTLY and DO NOT provide extra information unless asked.\n"
514 | systemPrompt += "Answer SUCCINCTLY like examples below:\n"
515 | systemPrompt += "Question: Who is the first narrator and how does the person look like?\n"
516 | systemPrompt += "Answer: The first narrator is likely Tom Bush. He is around 25-30 years old man with beard and mustache. He appears to be happy throughout the video.\n"
517 | systemPrompt += "Question: Did Oba smile at the end of the video?\n"
518 | systemPrompt += "Answer: Yes he did.\n"
519 | systemPrompt += "Question: When did he smile?\n"
520 | systemPrompt += "Answer: He smiled at 5.5 and 7 second marks.\n"
521 |
522 | if(video.videoScript.length < this.maxCharactersForSingleLLMCall){
523 | await this.retrieveAnswerForShortVideo(video, systemPrompt)
524 | } else {
525 | // Long video gets split into fragments for the video script. For each fragment, it goes to LLM to find the partial answer.
526 | await this.retrieveAnswerForLongVideo(video, systemPrompt)
527 | }
528 |
529 | await this.removeChatWaitingSpinner(video)
530 | this.updateChatSummary(video)
531 |
532 | }
533 |
534 | async displayNewBotAnswer(video, text){
535 | video.chats.push({actor: "Bot", text: text})
536 | video.chatLength += text.length
537 | this.setState({videos: this.state.videos})
538 | }
539 |
540 | async displayNewBotAnswerWithStreaming(video, response){
541 | video.chats.push({actor: "Bot", text: ""})
542 | for await (const event of response.body) {
543 | if (event.chunk && event.chunk.bytes) {
544 | const chunk = JSON.parse(Buffer.from(event.chunk.bytes).toString("utf-8"));
545 | if(chunk.type == "content_block_delta"){
546 | video.chats[video.chats.length - 1].text += chunk.delta.text
547 | video.chatLength += chunk.delta.text.length // Increment the length of the chat
548 | this.setState({videos: this.state.videos})
549 | }
550 | }
551 | };
552 | }
553 |
554 | async handleChatModelPicker(event){
555 | this.setState({chatModelChoice: event})
556 | }
557 |
558 | async removeChatWaitingSpinner(video){
559 | video.chatWaiting = false
560 | this.setState({videos: this.state.videos})
561 | }
562 |
563 | async addChatWaitingSpinner(video){
564 | video.chatWaiting = true
565 | this.setState({videos: this.state.videos})
566 | }
567 |
568 | async updateChatSummary(video){
569 | // If the chats between bot and human is roughly more than 5000 characters (1250 tokens) then summarize the chats, otherwise ignore and return.
570 | if (video.chatLength < this.maxCharactersForChat) return;
571 |
572 | var systemPrompt = "You are an expert in summarizing chat. The chat is between user and a bot. The bot answers questions about a video.\n"
573 | var prompt = ""
574 |
575 | if (video.chatSummary == ""){
576 | prompt += "Find the chat below.\n\n"
577 | for (const i in video.chats){
578 | if (video.chats[i].actor == "You"){
579 | prompt += `\nUser: ${video.chats[i].text}`
580 | }else{
581 | prompt += `\nYou: ${video.chats[i].text}`
582 | }
583 | }
584 | prompt += "\n \nSummarize the chat in no longer than 1 page."
585 | }else{
586 | prompt += "Below is the summary of the chat so far.\n"
587 | prompt += `\n${video.chatSummary}\n \n`
588 | prompt += "You recently replied to the User as below.\n"
589 | prompt += `\n${video.chats[video.chats.length - 1].text}\n \n`
590 | prompt += "Summarize the whole chat including your newest reply in no longer than 1 page."
591 | }
592 |
593 | const input = {
594 | body: JSON.stringify({
595 | anthropic_version: "bedrock-2023-05-31",
596 | system: systemPrompt,
597 | messages: [
598 | { role: "user", content: prompt},
599 | { role: "assistant", content: "Here is my succinct answer:"}
600 | ],
601 | temperature: 0.1,
602 | max_tokens: Math.round(this.maxCharactersForChat/this.estimatedCharactersPerToken),
603 | stop_sequences: ["\nUser:", "\nYou:"]
604 | }),
605 | modelId: this.fastModelId,
606 | contentType: "application/json",
607 | accept: "application/json"
608 | };
609 | const response = await this.bedrockClient.send(new InvokeModelCommand(input));
610 | video.chatSummary = JSON.parse(new TextDecoder().decode(response.body)).content[0].text
611 | this.setState({videos: this.state.videos})
612 | }
613 |
614 | async handleSearchByNameTextChange(event){
615 | this.setState({searchByNameText: event.target.value })
616 | }
617 |
618 | async handleSearchByAboutTextChange(event){
619 | this.setState({searchByAboutText: event.target.value })
620 | }
621 |
622 | async handleSearchByDateChange(event){
623 | const startDate = event[0]
624 | const endDate = event[1]
625 | this.setState({
626 | searchByStartDate: startDate,
627 | searchByEndDate: endDate
628 | })
629 | }
630 |
631 | async handleSearch(){
632 | await this.setState({pages: []})
633 | var [videos, pages] = await this.fetchVideos(0)
634 | this.setState({videos: videos, pages: pages})
635 | }
636 |
637 | renderEntities(video, videoIndex){
638 | return video.entities.map((entity, i) => {
639 | return (
640 |
641 | {entity[0]}
642 | {entity[1]}
643 | {entity[2]}
644 |
645 | )
646 | })
647 | }
648 |
649 | renderVideoTableBody() {
650 | // Get which page is active to make sure Accordion keeps open for same page and will auto close for page switch.
651 | var activePage = 0
652 | Object.keys(this.state.pages).forEach((i) => {if(this.state.pages[i].active) activePage = i})
653 |
654 | // Render the video names as Accordion headers.
655 | return this.state.videos.map((video, i) => {
656 | return (
657 |
658 | {video.name}
659 |
660 |
661 |
662 | { !video.videoShown ? Show video : }
663 |
664 |
665 |
666 |
667 | Summary:
668 | {typeof video.summary === "undefined" ? "Summary is not ready yet" : video.summary }
669 |
670 |
671 |
672 |
673 | Entities:
674 | { typeof video.entities === "undefined" ? Entity list is not ready yet
: Entity Sentiment Reason
}
675 | { typeof video.entities !== "undefined" ? this.renderEntities(video, i) : "" }
676 |
677 |
678 |
679 |
680 |
681 | Ask about the video:
682 | {this.renderChat(video)}
683 | { video.chatWaiting ? Thinking ...
: "" }
684 |
685 |
686 |
693 |
694 |
700 | Fastest
701 | Balanced
702 | Smartest
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 | Find video part:
712 |
713 |
714 |
715 | Show me where
716 |
723 |
724 |
725 |
726 | {(typeof video.findPartResult !== "undefined") ? {video.findPartResult} : "" }
727 |
728 | { video.findPartWaiting ? Thinking ...
: "" }
729 |
730 |
731 |
732 |
733 | )
734 | })
735 | }
736 |
737 | async handlePageClick(index){
738 | var [videos, pages] = await this.fetchVideos(parseInt(index))
739 | this.setState({videos: videos, pages: pages})
740 | }
741 |
742 | renderPagination(index){
743 | var page = this.state.pages[index]
744 | return (
745 | {page.displayName}
746 | )
747 | }
748 |
749 | render() {
750 | return (
751 |
752 |
753 |
754 |
755 |
756 |
Search videos with filters
757 |
758 |
759 |
766 | {
771 | this.handleSearchByDateChange(update);
772 | }}
773 | isClearable={true}
774 | showIcon={true}
775 | placeholderText="Video uploaded between . . ."
776 | />
777 |
778 |
785 |
786 |
787 | Search
788 |
789 |
790 |
791 |
792 | {/* */}
793 |
794 |
795 |
796 |
797 | { Object.keys(this.state.pages).map((i) => { return this.renderPagination(i) })}
798 |
799 |
800 |
801 |
802 | { this.state.pageLoading ? Fetching videos ... : '' }
803 | { Object.keys(this.state.pages).length > 0 && this.state.videos.length > 0 ? this.renderVideoTableBody() : ""}
804 | { this.state.firstFetch && Object.keys(this.state.pages).length == 0 && this.state.videos.length == 0 ? Found no relevant video in S3 "{this.rawFolder}" folder
: ""}
805 |
806 |
807 |
808 |
809 | )
810 | }
811 | }
--------------------------------------------------------------------------------
/webui/src/VideoUpload/VideoUpload.css:
--------------------------------------------------------------------------------
1 | #video-upload-text{
2 | background-color: white;
3 | }
4 | .no-margin-bottom{
5 | margin-bottom: 0px;
6 | }
--------------------------------------------------------------------------------
/webui/src/VideoUpload/VideoUpload.js:
--------------------------------------------------------------------------------
1 | import React, { Component, useState } from "react";
2 | import { CreateMultipartUploadCommand,
3 | UploadPartCommand,
4 | CompleteMultipartUploadCommand,
5 | AbortMultipartUploadCommand,
6 | PutObjectCommand,
7 | } from "@aws-sdk/client-s3";
8 |
9 | import Form from "react-bootstrap/Form";
10 | import InputGroup from 'react-bootstrap/InputGroup';
11 | import Row from "react-bootstrap/Row";
12 | import Col from "react-bootstrap/Col";
13 | import Button from "react-bootstrap/Button";
14 | import ProgressBar from 'react-bootstrap/ProgressBar';
15 |
16 | import './VideoUpload.css';
17 |
18 | export class VideoUpload extends Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | selectedVideoFiles: null,
23 | isUploading: false,
24 | currentUploadSize: 0,
25 | totalUploadSize: 0.001,
26 | };
27 |
28 | this.s3Client = props.s3Client;
29 | this.bucketName = props.bucketName;
30 | this.rawFolder = props.rawFolder;
31 | }
32 |
33 | cleanFileName = (rawFileName) => {
34 | return rawFileName.replace(/[^a-zA-Z0-9\-\.]/g, '_');
35 | }
36 |
37 | onFileChange = (event) => {
38 | this.setState({
39 | selectedVideoFiles: event.target.files,
40 | });
41 | };
42 |
43 | onFilesUpload = async () => {
44 | this.setState({
45 | isUploading: true
46 | });
47 |
48 | var totalSizeToUpload = 0.0
49 | for (let i = 0; i < this.state.selectedVideoFiles.length; i++) {
50 | totalSizeToUpload += this.state.selectedVideoFiles[i].size
51 | }
52 | this.setState({
53 | totalUploadSize: this.state.totalUploadSize + totalSizeToUpload
54 | });
55 |
56 | for (let i = 0; i < this.state.selectedVideoFiles.length; i++) {
57 | const fileSize = this.state.selectedVideoFiles[i].size
58 | if(fileSize >=25e6){ // User Multipart upload
59 | let uploadId;
60 | try {
61 | const multipartUpload = await this.s3Client.send(
62 | new CreateMultipartUploadCommand({
63 | Bucket: this.bucketName,
64 | Key: `${this.rawFolder}/${this.cleanFileName(this.state.selectedVideoFiles[i].name)}`,
65 | }),
66 | );
67 |
68 | uploadId = multipartUpload.UploadId;
69 |
70 | const uploadPromises = [];
71 | // Multipart uploads require a minimum size of 5 MB per part.
72 | const partSize = 10e6
73 | const numberOfParts = Math.floor(fileSize / partSize)
74 | const lastPartSize = partSize + fileSize % partSize
75 |
76 | // Upload each part.
77 | for (let j = 0; j < numberOfParts; j++) {
78 | const start = j * partSize;
79 | const end = (j === numberOfParts - 1 ) ? (start + lastPartSize) : (start + partSize)
80 | uploadPromises.push(
81 | this.s3Client
82 | .send(
83 | new UploadPartCommand({
84 | Bucket: this.bucketName,
85 | Key: `${this.rawFolder}/${this.cleanFileName(this.state.selectedVideoFiles[i].name)}`,
86 | UploadId: uploadId,
87 | Body: this.state.selectedVideoFiles[i].slice(start, end),
88 | PartNumber: j + 1,
89 | }),
90 | ).then(singlePartUploadResult => {
91 | this.setState({
92 | currentUploadSize: this.state.currentUploadSize + ((j === numberOfParts - 1 ) ? lastPartSize : partSize)
93 | });
94 | return singlePartUploadResult
95 | })
96 | );
97 | }
98 |
99 | const uploadResults = await Promise.all(uploadPromises);
100 |
101 | await this.s3Client.send(
102 | new CompleteMultipartUploadCommand({
103 | Bucket: this.bucketName,
104 | Key: `${this.rawFolder}/${this.cleanFileName(this.state.selectedVideoFiles[i].name)}`,
105 | UploadId: uploadId,
106 | MultipartUpload: {
107 | Parts: uploadResults.map(({ ETag }, i) => ({
108 | ETag,
109 | PartNumber: i + 1,
110 | })),
111 | },
112 | }),
113 | );
114 | } catch (err) {
115 | console.error(err);
116 |
117 | if (uploadId) {
118 | const abortCommand = new AbortMultipartUploadCommand({
119 | Bucket: this.bucketName,
120 | Key: `${this.rawFolder}/${this.cleanFileName(this.state.selectedVideoFiles[i].name)}`,
121 | UploadId: uploadId,
122 | });
123 |
124 | await this.s3Client.send(abortCommand);
125 | }
126 | }
127 |
128 | }else{ // Use simple upload
129 | const command = new PutObjectCommand({
130 | Bucket: this.bucketName,
131 | Key: `${this.rawFolder}/${this.cleanFileName(this.state.selectedVideoFiles[i].name)}`,
132 | Body: this.state.selectedVideoFiles[i],
133 | });
134 |
135 | // Upload file
136 | try {
137 | await this.s3Client.send(command);
138 | this.setState({
139 | currentUploadSize: this.state.currentUploadSize + parseInt(this.state.selectedVideoFiles[i].size)
140 | });
141 | } catch (error) {
142 | console.log(error)
143 | }
144 | }
145 | }
146 |
147 | this.setState({
148 | isUploading: false,
149 | selectedVideoFiles: null,
150 | currentUploadSize: 0,
151 | totalUploadSize: 0.001,
152 | });
153 | };
154 |
155 | render() {
156 | return (
157 | <>
158 |
159 |
160 |
161 |
162 | Upload video files
163 |
169 |
175 | {this.state.isUploading ? 'Uploading . . .' : 'Upload'}
176 |
177 |
178 |
179 |
180 |
181 | Alternatively, upload to Amazon S3 bucket "{this.bucketName}" inside folder "{this.rawFolder}" .
182 |
183 |
184 | { this.state.isUploading ?
: "" }
185 | { this.state.isUploading && Math.floor(this.state.currentUploadSize / parseInt(this.state.totalUploadSize) * 100) > 50 ? When upload finishes, you can reload page or re-search the videos to refresh the video list.
: "" }
186 | >
187 | );
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/webui/src/aws-exports.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | export default {
4 | "aws_project_region": "PLACEHOLDER_REGION",
5 | "aws_cognito_identity_pool_id": "PLACEHOLDER_IDENTITY_POOL_ID",
6 | "aws_cognito_region": "PLACEHOLDER_COGNITO_REGION",
7 | "aws_user_pools_id": "PLACEHOLDER_USER_POOL_ID",
8 | "aws_user_pools_web_client_id": "PLACEHOLDER_USER_POOL_WEB_CLIENT_ID",
9 | "bucket_name": "PLACEHOLDER_BUCKET_NAME",
10 | "balanced_model_id": "PLACEHOLDER_BALANCED_MODEL_ID",
11 | "fast_model_id": "PLACEHOLDER_FAST_MODEL_ID",
12 | "raw_folder": "PLACEHOLDER_RAW_FOLDER",
13 | "video_script_folder": "PLACEHOLDER_VIDEO_SCRIPT_FOLDER",
14 | "video_caption_folder": "PLACEHOLDER_VIDEO_CAPTION_FOLDER",
15 | "transcription_folder": "PLACEHOLDER_TRANSCRIPTION_FOLDER",
16 | "entity_sentiment_folder": "PLACEHOLDER_ENTITY_SENTIMENT_FOLDER",
17 | "summary_folder": "PLACEHOLDER_SUMMARY_FOLDER",
18 | "rest_api_url": "PLACEHOLDER_REST_API_URL",
19 | "videos_api_resource": "PLACEHOLDER_VIDEOS_API_RESOURCE"
20 | }
--------------------------------------------------------------------------------
/webui/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/webui/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import 'bootstrap/dist/css/bootstrap.min.css';
7 |
8 |
9 | const root = ReactDOM.createRoot(document.getElementById('root'));
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/webui/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/webui/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------