├── cdk.json
├── config.json
├── .gitattributes
├── cdk.context.json
├── .DS_Store
├── lambda
├── .DS_Store
├── check_sqs
│ └── index.py
├── stepfunction_status
│ └── index.py
├── create_thumbnails
│ ├── index.py
│ └── mediaconvert_setting.json
├── merge_clips
│ ├── mediaconvert_setting.json
│ └── index.py
├── create_large_clips
│ ├── mediaconvert_setting.json
│ └── index.py
├── mediaconvert_check
│ └── index.py
├── send_email
│ └── index.py
├── delete_s3_sqs_ddb
│ └── index.py
├── create_short_clips
│ ├── mediaconvert_setting.json
│ └── index.py
├── receive_message
│ ├── index.py
│ └── other_prompts.txt
└── create_assets
│ └── index.py
├── sample_video
├── .DS_Store
├── cover.jpg
└── Final_2022.mp4
├── LICENSE.md
├── README.md
└── app.py
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "python3 app.py"
3 | }
4 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "user_email": "xxx@amazon.com"
3 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.mp4 filter=lfs diff=lfs merge=lfs -text
2 |
--------------------------------------------------------------------------------
/cdk.context.json:
--------------------------------------------------------------------------------
1 | {
2 | "acknowledged-issue-numbers": [
3 | 19836
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iut62elec/Soccer-Highlight-Generator-with-GenAI/HEAD/.DS_Store
--------------------------------------------------------------------------------
/lambda/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iut62elec/Soccer-Highlight-Generator-with-GenAI/HEAD/lambda/.DS_Store
--------------------------------------------------------------------------------
/sample_video/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iut62elec/Soccer-Highlight-Generator-with-GenAI/HEAD/sample_video/.DS_Store
--------------------------------------------------------------------------------
/sample_video/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iut62elec/Soccer-Highlight-Generator-with-GenAI/HEAD/sample_video/cover.jpg
--------------------------------------------------------------------------------
/sample_video/Final_2022.mp4:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:778181c1cb8b80a2e40e5617538012394a631f779a626e4ed94d6e5571ef54be
3 | size 131121459
4 |
--------------------------------------------------------------------------------
/lambda/check_sqs/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import pprint
4 | import os
5 |
6 | def lambda_handler(event, context):
7 | sqs_client = boto3.client('sqs')
8 | #input_data = json.loads(event['Input'])
9 | print(event)
10 | bucket_name=event['s3_bucket']
11 | table_name=event['DDB_table']
12 | file_name=event['vide_file_name']
13 | queue_url=event['queue_url']
14 | queue_arn=event['queue_arn']
15 |
16 | response = sqs_client.get_queue_attributes(
17 | QueueUrl=queue_url,
18 | AttributeNames=['ApproximateNumberOfMessages'])
19 | ApproximateNumberOfMessages=response['Attributes']['ApproximateNumberOfMessages']
20 | print('ApproximateNumberOfMessages',response['Attributes']['ApproximateNumberOfMessages'])
21 |
22 | Status='Default'
23 | if ApproximateNumberOfMessages=='0':
24 | Status='COMPLETE'
25 | return Status #"checked job status" #json.dumps(response)
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Pedram Jahangiri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/lambda/stepfunction_status/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 |
4 | dynamodb = boto3.resource('dynamodb')
5 |
6 |
7 | def lambda_handler(event, context):
8 |
9 | print(event)
10 | try:
11 | id=event['Executionid']
12 | except:
13 | id='unknown'
14 |
15 | event_str = event['outputs']['Payload']
16 | event_payload = json.loads(event_str) # Convert the JSON string back to a dictionary
17 |
18 | final_highlight = event_payload['final_highlight']
19 | final_original = event_payload['final_original']
20 |
21 |
22 | status='Succeeded'
23 |
24 | # # Write to DynamoDB table
25 | # table_name='SFworkflow-2yumt4yc2raibewutmhs4p3qhi-dev'
26 | # table = dynamodb.Table(table_name)
27 | # table.put_item(
28 | # Item={
29 | # 'id': id,
30 | # 'status': status,
31 | # 'final_highlight':final_highlight,
32 | # 'final_original': final_original
33 | # }
34 | # )
35 |
36 | response = {
37 | 'statusCode': 200,
38 | 'body': json.dumps({
39 | 'message': 'Data saved successfully!'
40 | })
41 | }
42 |
43 | return response
44 |
--------------------------------------------------------------------------------
/lambda/create_thumbnails/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import os
4 | import boto3
5 | ssm = boto3.client('ssm')
6 |
7 | r=os.getenv("MEDIA_CONVERT_ROLE_ARN")
8 |
9 | def lambda_handler(event, context):
10 |
11 | with open('mediaconvert_setting.json') as f:
12 | data = json.load(f)
13 |
14 | print(event)
15 | bucket_name=event['s3_bucket']
16 | table_name=event['DDB_table']
17 | file_name=event['vide_file_name']
18 | s3=bucket_name
19 | v=file_name
20 | my_config = {
21 | "bucket_name":event['s3_bucket'],
22 | "table_name":event['DDB_table'],
23 | "file_name":event['vide_file_name'],
24 | "queue_url":event['queue_url'],
25 | "queue_arn":event['queue_arn']
26 | }
27 |
28 | my_json_str = json.dumps(my_config)
29 |
30 | # Set the parameter value in Parameter Store
31 | ssm.put_parameter(
32 | Name=event['s3_bucket'],
33 | Value=my_json_str,
34 | Type='String',
35 | Overwrite=True
36 | )
37 |
38 |
39 |
40 |
41 | client=boto3.client('mediaconvert')
42 | endpoint=client.describe_endpoints()['Endpoints'][0]['Url']
43 | myclient=boto3.client('mediaconvert', endpoint_url=endpoint)
44 | data['Settings']['Inputs'][0]['FileInput']='s3://'+s3+'/'+v
45 | data['Settings']['OutputGroups'][0]['OutputGroupSettings']['FileGroupSettings']['Destination']='s3://'+s3+'/MP4/'
46 | data['Settings']['OutputGroups'][1]['OutputGroupSettings']['FileGroupSettings']['Destination']='s3://'+s3+'/Thumbnails/'
47 | #print(data)
48 | #print(type(data))
49 | response = myclient.create_job(Role=r,Settings=data['Settings'])
50 | print(response)
51 | return "done"
52 |
--------------------------------------------------------------------------------
/lambda/merge_clips/mediaconvert_setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings": {
3 | "TimecodeConfig": {
4 | "Source": "ZEROBASED"
5 | },
6 | "OutputGroups": [
7 | {
8 | "CustomName": "final",
9 | "Name": "File Group",
10 | "Outputs": [
11 | {
12 | "ContainerSettings": {
13 | "Container": "MP4",
14 | "Mp4Settings": {}
15 | },
16 | "VideoDescription": {
17 | "CodecSettings": {
18 | "Codec": "H_264",
19 | "H264Settings": {
20 | "MaxBitrate": 10000000,
21 | "RateControlMode": "QVBR",
22 | "SceneChangeDetect": "TRANSITION_DETECTION"
23 | }
24 | }
25 | },
26 | "AudioDescriptions": [
27 | {
28 | "CodecSettings": {
29 | "Codec": "AAC",
30 | "AacSettings": {
31 | "Bitrate": 96000,
32 | "CodingMode": "CODING_MODE_2_0",
33 | "SampleRate": 48000
34 | }
35 | }
36 | }
37 | ],
38 | "NameModifier": "highlight"
39 | }
40 | ],
41 | "OutputGroupSettings": {
42 | "Type": "FILE_GROUP_SETTINGS",
43 | "FileGroupSettings": {
44 | "Destination": "xxx"
45 | }
46 | }
47 | }
48 | ],
49 | "Inputs": [
50 |
51 | ]
52 | },
53 | "AccelerationSettings": {
54 | "Mode": "DISABLED"
55 | },
56 | "StatusUpdateInterval": "SECONDS_60",
57 | "Priority": 0
58 | }
--------------------------------------------------------------------------------
/lambda/create_large_clips/mediaconvert_setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings": {
3 | "TimecodeConfig": {
4 | "Source": "ZEROBASED"
5 | },
6 | "OutputGroups": [
7 | {
8 | "CustomName": "final",
9 | "Name": "File Group",
10 | "Outputs": [
11 | {
12 | "ContainerSettings": {
13 | "Container": "MP4",
14 | "Mp4Settings": {}
15 | },
16 | "VideoDescription": {
17 | "CodecSettings": {
18 | "Codec": "H_264",
19 | "H264Settings": {
20 | "MaxBitrate": 10000000,
21 | "RateControlMode": "QVBR",
22 | "SceneChangeDetect": "TRANSITION_DETECTION"
23 | }
24 | }
25 | },
26 | "AudioDescriptions": [
27 | {
28 | "CodecSettings": {
29 | "Codec": "AAC",
30 | "AacSettings": {
31 | "Bitrate": 96000,
32 | "CodingMode": "CODING_MODE_2_0",
33 | "SampleRate": 48000
34 | }
35 | }
36 | }
37 | ],
38 | "NameModifier": "final"
39 | }
40 | ],
41 | "OutputGroupSettings": {
42 | "Type": "FILE_GROUP_SETTINGS",
43 | "FileGroupSettings": {
44 | "Destination": "xxx"
45 | }
46 | }
47 | }
48 | ],
49 | "Inputs": [
50 |
51 | ]
52 | },
53 | "AccelerationSettings": {
54 | "Mode": "DISABLED"
55 | },
56 | "StatusUpdateInterval": "SECONDS_60",
57 | "Priority": 0
58 | }
--------------------------------------------------------------------------------
/lambda/mediaconvert_check/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import pprint
4 | import os
5 | import time
6 | def lambda_handler(event, context):
7 | print(event)
8 |
9 |
10 |
11 |
12 | client = boto3.client('mediaconvert')
13 | endpoint = client.describe_endpoints()['Endpoints'][0]['Url']
14 | print (endpoint)
15 | myclient = boto3.client('mediaconvert', endpoint_url=endpoint)
16 |
17 |
18 | # Check the status of MediaConvert jobs
19 | response1 = myclient.list_jobs(
20 | MaxResults=20,
21 | Order='DESCENDING',
22 | Status='PROGRESSING'
23 | )
24 | response2 = myclient.list_jobs(
25 | MaxResults=20,
26 | Order='DESCENDING',
27 | Status='SUBMITTED'
28 | )
29 |
30 | # Calculate the total number of active jobs
31 | active_jobs = len(response1['Jobs']) + len(response2['Jobs'])
32 | print(f"Number of active jobs: {active_jobs}")
33 |
34 | # Determine the status based on active jobs count
35 | if active_jobs > 0:
36 | # If there are active jobs, return NONCOMPLETE status
37 | status = 'NONCOMPLETE'
38 | else:
39 | # If there are no active jobs, return COMPLETE status
40 | status = 'COMPLETE'
41 |
42 | # Return the status
43 | print(f"Status: {status}")
44 | return status
45 |
46 |
47 | # a=3
48 | # while a>0:
49 |
50 | # response1 = myclient.list_jobs(
51 | # MaxResults=20,
52 | # Order='DESCENDING',
53 | # Status='PROGRESSING')
54 | # response2 = myclient.list_jobs(
55 | # MaxResults=20,
56 | # Order='DESCENDING',
57 | # Status='SUBMITTED')
58 | # a=len(response1['Jobs'])+len(response2['Jobs'])
59 | # print(response1)
60 | # print(response2)
61 | # print(a)
62 | # time.sleep(5)
63 | # Status='COMPLETE'
64 |
65 | # print(Status)
66 |
67 | # return Status #"checked job status" #json.dumps(response)
68 |
--------------------------------------------------------------------------------
/lambda/send_email/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import time
4 | import os
5 |
6 |
7 | MP4output='HighlightClips'
8 | client = boto3.client('mediaconvert')
9 | endpoint = client.describe_endpoints()['Endpoints'][0]['Url']
10 | myclient = boto3.client('mediaconvert', endpoint_url=endpoint)
11 |
12 | snsarn=os.getenv("TOPIC_ARN")
13 | client_sns = boto3.client('sns')
14 |
15 | CF_endpoint='https://'+os.environ.get('CLOUDFRONT_ENDPOINT_URL')
16 | Amplify_bucket=os.environ.get('VIDEO_ASSETS_BUCKET_NAME')
17 |
18 |
19 | def lambda_handler(event, context):
20 |
21 | s3=boto3.client('s3')
22 | import math
23 |
24 | print(type(event))
25 | print('-----------------')
26 | bucket_name=event['s3_bucket']
27 | table_name=event['DDB_table']
28 | file_name=event['vide_file_name']
29 | queue_url=event['queue_url']
30 | queue_arn=event['queue_arn']
31 |
32 |
33 |
34 | bucket=bucket_name
35 | prefix='High';postfix='highlight.mp4'
36 | filelist=s3.list_objects(Bucket=bucket)['Contents']
37 | mp4files = [file['Key'] for file in filelist if file['Key'][-13:]==postfix if file['Key'][:4]==prefix]
38 |
39 |
40 | source=bucket+'/'+ mp4files[0]
41 | key1='public/'+ mp4files[0].replace('finalhighlight.mp4','HL.mp4')
42 | print(key1,bucket,mp4files[0],source)
43 |
44 | response = s3.copy_object(
45 | Bucket=bucket,
46 | CopySource=source,
47 | Key=key1
48 | )
49 |
50 | response2 = s3.copy_object(
51 | Bucket=Amplify_bucket,
52 | CopySource=source,
53 | Key=key1
54 | )
55 |
56 | key2='public/HighlightClips/'+file_name
57 | source2=bucket+'/'+ file_name
58 |
59 | response2 = s3.copy_object(
60 | Bucket=Amplify_bucket,
61 | CopySource=source2,
62 | Key=key2
63 | )
64 |
65 | final_name=mp4files[0].replace('finalhighlight.mp4','HL.mp4').split('HighlightClips/')[1]
66 | final_highlight=CF_endpoint+final_name
67 | final_original=CF_endpoint+file_name
68 |
69 | final_highlight=CF_endpoint+'/public/HighlightClips/'+final_name
70 | final_original=CF_endpoint+'/public/HighlightClips/'+file_name
71 |
72 |
73 | message={"final_highlight":final_highlight,
74 | "final_original":final_original
75 |
76 | }
77 | sns_response = client_sns.publish(TargetArn=snsarn,Message=json.dumps({'default': json.dumps(message)}),MessageStructure='json')
78 |
79 | return json.dumps(message)
80 |
--------------------------------------------------------------------------------
/lambda/delete_s3_sqs_ddb/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | def lambda_handler(event, context):
4 | s3 = boto3.resource('s3')
5 | s3_cl = boto3.client('s3')
6 |
7 | # list all S3 buckets
8 |
9 | # iterate through each bucket and check if it starts with "sport"
10 | for bucket in s3.buckets.all():
11 | if bucket.name.startswith('soccer-'):
12 |
13 | # delete the bucket
14 | bucket.objects.all().delete()
15 | bucket.delete()
16 |
17 | dynamodb = boto3.client('dynamodb')
18 |
19 | # Get a list of all DynamoDB table names
20 | response = dynamodb.list_tables()
21 | table_names = response['TableNames']
22 |
23 | # Loop through all table names
24 | for table_name in table_names:
25 | # If the table name starts with "soccer-", delete the table
26 | if table_name.startswith('soccer-'):
27 | dynamodb.delete_table(TableName=table_name)
28 |
29 |
30 | sqs = boto3.client('sqs')
31 |
32 | # Get a list of all SQS queue URLs
33 | response = sqs.list_queues()
34 | queue_urls = response['QueueUrls']
35 |
36 | # Loop through all queue URLs
37 | for queue_url in queue_urls:
38 | # Get the queue name from the queue URL
39 | queue_name = queue_url.split('/')[-1]
40 | # If the queue name starts with "soccer-", delete the queue
41 | if queue_name.startswith('soccer-'):
42 | sqs.delete_queue(QueueUrl=queue_url)
43 |
44 | # queue_url = 'https://sqs.us-east-1.amazonaws.com/456667773660/sport_frames_files'
45 |
46 | # # Retrieve and delete messages in batches
47 | # while True:
48 | # response = sqs.receive_message(
49 | # QueueUrl=queue_url,
50 | # AttributeNames=['All'],
51 | # MaxNumberOfMessages=10,
52 | # VisibilityTimeout=0,
53 | # WaitTimeSeconds=0
54 | # )
55 |
56 | # messages = response.get('Messages', [])
57 | # if not messages:
58 | # break
59 |
60 | # entries = [{'Id': msg['MessageId'], 'ReceiptHandle': msg['ReceiptHandle']} for msg in messages]
61 | # response = sqs.delete_message_batch(QueueUrl=queue_url, Entries=entries)
62 |
63 | # # Wait for all deletions to finish
64 | # waiter = sqs.get_waiter('queue_empty')
65 | # waiter.wait(QueueUrl=queue_url)
66 |
67 | return {
68 | 'statusCode': 200,
69 | 'body': json.dumps('Hello from Lambda!')
70 | }
71 |
--------------------------------------------------------------------------------
/lambda/create_large_clips/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import time
4 | import os
5 |
6 |
7 | MP4output='HighlightClips'
8 | client = boto3.client('mediaconvert')
9 | endpoint = client.describe_endpoints()['Endpoints'][0]['Url']
10 | myclient = boto3.client('mediaconvert', endpoint_url=endpoint)
11 | r=os.getenv("MEDIA_CONVERT_ROLE_ARN")
12 |
13 | def lambda_handler(event, context):
14 | s3=boto3.client('s3')
15 | import math
16 |
17 | print(type(event))
18 | print('-----------------')
19 | if 1==1:
20 | #time.sleep(10) #sleep for 5 seconds to wait for some previous job to complete
21 |
22 |
23 |
24 | #bucket='artifact-sport';
25 | prefix='High';postfix='mp4'
26 | bucket_name=event['s3_bucket']
27 | table_name=event['DDB_table']
28 | file_name=event['vide_file_name']
29 | queue_url=event['queue_url']
30 | queue_arn=event['queue_arn']
31 | bucket=bucket_name
32 | filelist=s3.list_objects(Bucket=bucket)['Contents']
33 | mp4files = [file['Key'] for file in filelist if file['Key'][-3:]==postfix if file['Key'][:4]==prefix]
34 | s3 = boto3.resource('s3')
35 | snslinks=[]
36 | hyperlink_format = '{text}'
37 |
38 |
39 |
40 |
41 |
42 | with open('mediaconvert_setting.json') as f:
43 | data = json.load(f)
44 | data["Settings"]["OutputGroups"][0]["OutputGroupSettings"]["FileGroupSettings"]["Destination"]=f"s3://{bucket}/HighlightClips/"
45 |
46 |
47 |
48 | counter=0;
49 | print(mp4files)
50 | print(type(mp4files))
51 | print(mp4files[0].split('from-')[1].split('-to')[0])
52 | mp4files=sorted(mp4files, key=lambda x: int(x.split('from-')[1].split('-to')[0]))
53 | #mp4files = sorted(mp4files)
54 |
55 | print(mp4files)
56 | large_clip_counter=0
57 | if 1==1:
58 | for key in mp4files[:]:
59 | #print(key,counter)
60 | counter=counter+1;
61 | if counter<150:
62 | data['Settings']['Inputs'].append({
63 | "AudioSelectors": {
64 | "Audio Selector 1": {
65 | "DefaultSelection": "DEFAULT"
66 | }
67 | },
68 | "VideoSelector": {},
69 | "TimecodeSource": "ZEROBASED",
70 | "FileInput": 's3://'+bucket+'/'+key
71 | })
72 |
73 | else:
74 | #print(data['Settings']['Inputs'])
75 | data["Settings"]["OutputGroups"][0]["CustomName"]='final'+str(counter)
76 | print(data["Settings"])
77 | response = myclient.create_job(
78 | Role=r,
79 | Settings=data['Settings'])
80 | large_clip_counter=large_clip_counter+1;
81 | counter=0
82 | with open('mediaconvert_setting.json') as f:
83 | data = json.load(f)
84 | data["Settings"]["OutputGroups"][0]["OutputGroupSettings"]["FileGroupSettings"]["Destination"]=f"s3://{bucket}/HighlightClips/"
85 |
86 | print(data["Settings"])
87 | response = myclient.create_job(
88 | Role=r,
89 | Settings=data['Settings'])
90 | large_clip_counter=large_clip_counter+1
91 |
92 | return json.dumps({"fileschanged":mp4files,"large_clip_counter":large_clip_counter})
93 |
94 |
--------------------------------------------------------------------------------
/lambda/merge_clips/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import time
4 | import os
5 |
6 |
7 | MP4output='HighlightClips'
8 | client = boto3.client('mediaconvert')
9 | endpoint = client.describe_endpoints()['Endpoints'][0]['Url']
10 | myclient = boto3.client('mediaconvert', endpoint_url=endpoint)
11 | r=os.getenv("MEDIA_CONVERT_ROLE_ARN")
12 |
13 | def lambda_handler(event, context):
14 | #large_clip_counter=1;
15 | #if large_clip_counter >1:
16 |
17 | s3=boto3.client('s3')
18 | import math
19 |
20 | print(type(event))
21 | print('-----------------')
22 | if 1==1:
23 | #time.sleep(10) #sleep for 5 seconds to wait for some previous job to complete
24 |
25 |
26 |
27 | prefix='High';postfix='final.mp4'
28 | bucket_name=event['s3_bucket']
29 | table_name=event['DDB_table']
30 | file_name=event['vide_file_name']
31 | queue_url=event['queue_url']
32 | queue_arn=event['queue_arn']
33 | bucket=bucket_name
34 | filelist=s3.list_objects(Bucket=bucket)['Contents']
35 | mp4files = [file['Key'] for file in filelist if file['Key'][-9:]==postfix if file['Key'][:4]==prefix]
36 |
37 | if len(mp4files) >1:
38 |
39 | s3 = boto3.resource('s3')
40 | snslinks=[]
41 | hyperlink_format = '{text}'
42 |
43 |
44 |
45 |
46 | with open('mediaconvert_setting.json') as f:
47 | data = json.load(f)
48 |
49 | data["Settings"]["OutputGroups"][0]["OutputGroupSettings"]["FileGroupSettings"]["Destination"]=f"s3://{bucket}/HighlightClips/"
50 |
51 |
52 | counter=0;
53 | print(mp4files)
54 | print(type(mp4files))
55 | print(mp4files[0].split('from-')[1].split('-to')[0])
56 | mp4files=sorted(mp4files, key=lambda x: int(x.split('from-')[1].split('-to')[0]))
57 | #mp4files = sorted(mp4files)
58 |
59 | print(mp4files)
60 | large_clip_counter=0
61 | if 1==1:
62 | for key in mp4files[:]:
63 | #print(key,counter)
64 | counter=counter+1;
65 | if counter<150:
66 | data['Settings']['Inputs'].append({
67 | "AudioSelectors": {
68 | "Audio Selector 1": {
69 | "DefaultSelection": "DEFAULT"
70 | }
71 | },
72 | "VideoSelector": {},
73 | "TimecodeSource": "ZEROBASED",
74 | "FileInput": 's3://'+bucket+'/'+key
75 | })
76 |
77 | else:
78 | #print(data['Settings']['Inputs'])
79 | data["Settings"]["OutputGroups"][0]["CustomName"]='highlight'+str(counter)
80 | print(data["Settings"])
81 | response = myclient.create_job(
82 | Role=r,
83 | Settings=data['Settings'])
84 | large_clip_counter=large_clip_counter+1;
85 | counter=0
86 | with open('mediaconvert_setting.json') as f:
87 | data = json.load(f)
88 | data["Settings"]["OutputGroups"][0]["OutputGroupSettings"]["FileGroupSettings"]["Destination"]=f"s3://{bucket}/HighlightClips/"
89 |
90 | print(data["Settings"])
91 | response = myclient.create_job(
92 | Role=r,
93 | Settings=data['Settings'])
94 | large_clip_counter=large_clip_counter+1
95 | else:
96 |
97 | ###just copy the file as highlight
98 | print("only one file")
99 | source=bucket+'/'+ mp4files[0]
100 | key1=mp4files[0].replace('.mp4','highlight.mp4')
101 | print(key1,bucket,mp4files[0],source)
102 |
103 | response = s3.copy_object(
104 | Bucket=bucket,
105 | CopySource=source,
106 | Key=key1
107 | )
108 |
109 | return json.dumps({"filesmerged":mp4files})
110 |
111 |
--------------------------------------------------------------------------------
/lambda/create_short_clips/mediaconvert_setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings": {
3 | "OutputGroups": [
4 | {
5 | "CustomName": "mp4CLIPoutput",
6 | "Name": "File Group",
7 | "Outputs": [
8 | {
9 | "ContainerSettings": {
10 | "Container": "MP4",
11 | "Mp4Settings": {
12 | "CslgAtom": "INCLUDE",
13 | "FreeSpaceBox": "EXCLUDE",
14 | "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
15 | }
16 | },
17 | "VideoDescription": {
18 | "Width": 1280,
19 | "ScalingBehavior": "DEFAULT",
20 | "Height": 720,
21 | "TimecodeInsertion": "DISABLED",
22 | "AntiAlias": "ENABLED",
23 | "Sharpness": 100,
24 | "CodecSettings": {
25 | "Codec": "H_264",
26 | "H264Settings": {
27 | "InterlaceMode": "PROGRESSIVE",
28 | "NumberReferenceFrames": 3,
29 | "Syntax": "DEFAULT",
30 | "Softness": 0,
31 | "GopClosedCadence": 1,
32 | "GopSize": 90,
33 | "Slices": 1,
34 | "GopBReference": "DISABLED",
35 | "SlowPal": "DISABLED",
36 | "SpatialAdaptiveQuantization": "ENABLED",
37 | "TemporalAdaptiveQuantization": "ENABLED",
38 | "FlickerAdaptiveQuantization": "DISABLED",
39 | "EntropyEncoding": "CABAC",
40 | "Bitrate": 950000,
41 | "FramerateControl": "INITIALIZE_FROM_SOURCE",
42 | "RateControlMode": "CBR",
43 | "CodecProfile": "MAIN",
44 | "Telecine": "NONE",
45 | "MinIInterval": 0,
46 | "AdaptiveQuantization": "HIGH",
47 | "CodecLevel": "AUTO",
48 | "FieldEncoding": "PAFF",
49 | "SceneChangeDetect": "ENABLED",
50 | "QualityTuningLevel": "SINGLE_PASS_HQ",
51 | "FramerateConversionAlgorithm": "DUPLICATE_DROP",
52 | "UnregisteredSeiTimecode": "DISABLED",
53 | "GopSizeUnits": "FRAMES",
54 | "ParControl": "INITIALIZE_FROM_SOURCE",
55 | "NumberBFramesBetweenReferenceFrames": 2,
56 | "RepeatPps": "DISABLED"
57 | }
58 | },
59 | "AfdSignaling": "NONE",
60 | "DropFrameTimecode": "ENABLED",
61 | "RespondToAfd": "NONE",
62 | "ColorMetadata": "INSERT"
63 | },
64 | "AudioDescriptions": [
65 | {
66 | "CodecSettings": {
67 | "Codec": "AAC",
68 | "AacSettings": {
69 | "Bitrate": 96000,
70 | "CodingMode": "CODING_MODE_2_0",
71 | "SampleRate": 48000
72 | }
73 | }
74 | }
75 | ],
76 | "NameModifier": "-trimmed-"
77 | }
78 | ],
79 | "OutputGroupSettings": {
80 | "Type": "FILE_GROUP_SETTINGS",
81 | "FileGroupSettings": {
82 | "Destination": ""
83 | }
84 | }
85 | }
86 | ],
87 | "AdAvailOffset": 0,
88 | "Inputs": [
89 | {
90 | "InputClippings": [
91 | {
92 | "EndTimecode": "00:00:10:00",
93 | "StartTimecode": "00:00:00:00"
94 | }
95 | ],
96 | "AudioSelectors": {
97 | "Audio Selector 1": {
98 | "DefaultSelection": "DEFAULT"
99 | }
100 | },
101 | "VideoSelector": {
102 | "ColorSpace": "FOLLOW"
103 | },
104 | "FilterEnable": "AUTO",
105 | "PsiControl": "USE_PSI",
106 | "FilterStrength": 0,
107 | "DeblockFilter": "DISABLED",
108 | "DenoiseFilter": "DISABLED",
109 | "TimecodeSource": "ZEROBASED",
110 | "FileInput": ""
111 | }
112 | ]
113 | }
114 | }
--------------------------------------------------------------------------------
/lambda/create_short_clips/index.py:
--------------------------------------------------------------------------------
1 | import json
2 | import boto3
3 | import time
4 | from boto3.dynamodb.conditions import Key, Attr
5 | import math
6 | import os
7 |
8 |
9 | r=os.getenv("MEDIA_CONVERT_ROLE_ARN")
10 |
11 |
12 | MP4output='HighlightClips'
13 | client = boto3.client('mediaconvert')
14 | endpoint = client.describe_endpoints()['Endpoints'][0]['Url']
15 | myclient = boto3.client('mediaconvert', endpoint_url=endpoint)
16 |
17 | def start_mediaconvert_job(data, sec_in, sec_out,bucket_name,file_name):
18 | print('CLIPPING : '+ str(sec_in) + '--------------->>>' + str(sec_out))
19 | s3=bucket_name;v=file_name
20 | data['Settings']['Inputs'][0]['FileInput']='s3://'+s3+'/'+v
21 | print(data['Settings']['Inputs'][0]['FileInput'])
22 | data['Settings']['OutputGroups'][0]['OutputGroupSettings']['FileGroupSettings']['Destination']='s3://'+s3+'/'+MP4output+'/'
23 |
24 | starttime = time.strftime('%H:%M:%S:00', time.gmtime(sec_in))
25 | print(starttime)
26 | endtime = time.strftime('%H:%M:%S:00', time.gmtime(sec_out))
27 |
28 | data['Settings']['Inputs'][0]['InputClippings'][0] = {'EndTimecode': endtime, 'StartTimecode': starttime}
29 | #print(data['Settings']['Inputs'][0]['InputClippings'][0])
30 |
31 | data['Settings']['OutputGroups'][0]['Outputs'][0]['NameModifier'] = '-from-'+str(sec_in)+'-to-'+str(sec_out)
32 |
33 | response = myclient.create_job(
34 | Role=r,
35 | Settings=data['Settings'])
36 |
37 | def lambda_handler(event, context):
38 | #print(input_data)
39 | bucket_name=event['s3_bucket']
40 | table_name=event['DDB_table']
41 | file_name=event['vide_file_name']
42 | queue_url=event['queue_url']
43 | queue_arn=event['queue_arn']
44 |
45 | ddtable=table_name;s3=bucket_name;v=file_name
46 |
47 | # TODO DYNAMO DB
48 | dynamodb = boto3.resource('dynamodb')
49 | table = dynamodb.Table(ddtable)
50 | response = table.scan()
51 |
52 | timeins = []
53 | timeouts=[]
54 | dict1={}
55 | for i in response['Items']:
56 | if(i['pickup']=='yes'):
57 |
58 | timeins.append(i['timeins'])
59 | timeouts.append(i['timeouts'])
60 | dict1[i['filename']]=i['timeins']
61 | print(dict1)
62 |
63 | sortedDict = sorted(dict1)
64 | print(sortedDict)
65 |
66 | sortedDict1 = sorted(dict1.items(), key=lambda x:x[1])
67 | print(sortedDict1)
68 |
69 |
70 |
71 | print("timeins*******")
72 | print(timeins)
73 | print("timeouts*******")
74 | print(timeouts)
75 | timeins = sorted([int(float(x)) for x in timeins])
76 |
77 | timeouts =sorted([int(float(x)) for x in timeouts])
78 | print("timeins after sort*******")
79 | ##[5800, 5850, 5900, 5950, 6000, 6050, 6100, 6150, 6200, 6250, 6300, 6350, 6400, 6450, 6700, 6800, 6850, 6900, 6950, 7000, 7050, 7100, 7150, 7200, 7250, 7300, 7350, 7400]
80 |
81 | print(timeins)
82 | print("timeouts after sort*******")
83 | print(timeouts)
84 | print("Min timein:{}".format(min(timeins)))
85 | print("MAx timeouts:{}".format(min(timeouts)))
86 | mintime =min(timeins)
87 | maxtime =max(timeouts)
88 |
89 | print('mintime='+str(mintime))
90 | print('maxtime='+str(maxtime))
91 |
92 | print(timeins)
93 | print(timeouts)
94 | mystarttime = mintime
95 | #
96 |
97 | if 1==1:
98 | # TODO MEDIACONVERT
99 | buffer1=2 #5 #1sec
100 | start_sec=math.floor(timeins[0]/1000)
101 | with open('mediaconvert_setting.json') as f:
102 | data = json.load(f)
103 | begin=1
104 | for time1 in timeins[0:]:
105 | #print('time1:',time1)
106 | if begin==1:
107 | sec_in=math.floor(time1/1000)
108 | begin=0;
109 | else:
110 | pass
111 |
112 | if (sec_in + buffer1) > math.floor(time1/1000):
113 | pass
114 | else:
115 | #sec_out=math.floor(time1/1000)
116 | sec_out=sec_in + buffer1
117 |
118 | print('new clip',sec_in,sec_out)
119 | start_mediaconvert_job(data, sec_in, sec_out,bucket_name,file_name)
120 | #time.sleep(1)
121 | #begin=1
122 | sec_in=math.floor(time1/1000)
123 | sec_out=math.ceil(time1/1000)
124 | if sec_in!=sec_out:
125 | print('new clip',sec_in,sec_out)
126 | start_mediaconvert_job(data, sec_in, sec_out,bucket_name,file_name)
127 | #sec_in = 5
128 | #sec_out = 8
129 | #start_mediaconvert_job(data, sec_in, sec_out)
130 | #time.sleep(1)
131 |
132 |
133 | return json.dumps({'bucket':s3,'prefix':'High','postfix':'mp4'})
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Soccer Highlight Generator
3 |
4 | Automate the creation of soccer match highlights with the power of Generative AI and AWS. This solution leverages AWS Bedrock (Anthropic’s Claude 3 Sonnet model), AWS MediaConvert, Lambda, Step Functions and other AWS services to identify and compile exciting game moments without manual editing.
5 |
6 | ## Author
7 |
8 | [Pedram Jahangiri](www.linkedin.com/in/pedram-jahangiri)
9 |
10 | ## Getting Started
11 |
12 | For a detailed explanation of what this solution does and the benefits it offers, please refer to [my blog](https://medium.com/@pedram.jahangiri62/accelerating-sport-highlights-generation-with-genai-ffdfd5c51685)
13 |
14 | ### Prerequisites
15 |
16 | - AWS CLI installed and configured with the necessary permissions
17 | - Node.js and npm
18 | - Python 3.11 and pip
19 | - An AWS account with the required services enabled
20 | - Access to Amazon Bedrock foundation models (Before you can use a foundation model in Amazon Bedrock, you must request access to it. Use this Link for detail )
21 |
22 | ### Installation
23 |
24 | Clone the repository in your local machine:
25 |
26 | ```bash
27 | git clone https://github.com/iut62elec/Soccer-Highlight-Generator-with-GenAI.git
28 | ```
29 |
30 |
31 | Navigate to the Soccer-Highlight-Generator-with-GenAI directory:
32 |
33 | ```bash
34 | cd Soccer-Highlight-Generator-with-GenAI
35 | ```
36 |
37 | Set up a virtual environment and activate it:
38 |
39 | ```bash
40 | python3.11 -m venv .venv
41 | source .venv/bin/activate
42 | ```
43 |
44 | Install the AWS CDK and required Python packages:
45 |
46 | ```bash
47 | npm install -g aws-cdk@latest
48 | npm update -g aws-cdk
49 | nvm install 18
50 | nvm use 18
51 | npm install -g aws-cdk@latest
52 | pip install --upgrade pip
53 | pip install aws-cdk.aws-lambda aws-cdk.aws-stepfunctions aws-cdk.aws-stepfunctions-tasks aws-cdk.aws-cloudfront aws_cdk.aws_cloudfront_origins aws-cdk.aws-s3-deployment
54 | ```
55 |
56 | Deploy the solution using CDK:
57 |
58 | - Email subscription: Open the config.json file and add the email address where you want to receive the highlight video link
59 |
60 | - Deploy
61 |
62 | ```bash
63 | aws configure --profile xxx
64 | export AWS_PROFILE=XXX
65 | cdk bootstrap
66 | cdk deploy --profile XXX
67 | ```
68 |
69 | ### Usage
70 |
71 | After the solution is deployed:
72 |
73 | 1. Go to the S3 console and locate the bucket contain "videoassetsbucket".
74 | 2. Create a folder named "public" and upload a sample video named "Final_2022.mp4" from "sample_video" folder into the "public" folder.
75 | 3. In the AWS Step Functions console, find the "SoccerHighlightsStateMachine" state machine.
76 | 4. Start the execution with the following JSON input in str format:
77 |
78 | ```
79 | "{\"file_name\":\"Final_2022.mp4\"}"
80 | ```
81 | 5. After completion, you will receive an email at the subscribed address with a link to the highlight video.
82 |
83 |
84 | Note: Please increase the AWS Lambda concurrent execution limit for your account to 1000 through AWS Service Quotas. This is necessary to ensure the proper functioning of the highlight generation process.
85 |
86 |
87 | This example video highlight was generated using this solution. The tool processed an already extended highlighted video from the 2022 FIFA World Cup final between Argentina and France, originally 5 minutes long, provided by Fox. This game was chosen due to its high-scoring nature, including 6 goals and subsequent penalty shots. The generated highlight effectively removes all unnecessary moments, retaining only the goals and penalty kicks, and reduces the video to ~4 minutes. Feel free to test this tool with other games as well.
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | ## Cleanup
96 |
97 | ### Cleaning Up After a Run
98 |
99 | Each execution of the Soccer Highlight Generator creates certain AWS resources like a dedicated S3 bucket, DynamoDB table, and SQS queue for processing. To delete these resources for a specific video after processing:
100 |
101 | 1. Navigate to the AWS Lambda console in your AWS account.
102 | 2. Find and select the Lambda function named `"SoccerHighlightsStack-deletes3sqsddbLambda"`.
103 | 3. Run this function directly from the console without any input. This will remove the processing assets created during that specific execution.
104 |
105 | ### Completely Removing the Solution
106 |
107 | If you wish to completely remove all assets associated with the Soccer Highlight Generator from your AWS account:
108 |
109 | 1. Ensure that you have first performed the cleanup steps for individual runs as described above.
110 | 2. Run the following command in your terminal where the CDK project is initialized:
111 |
112 | ```bash
113 | cdk destroy
114 | ```
115 |
116 | ## Contributing
117 |
118 | Join the game by implementing and testing the Soccer Highlight Generator. Your feedback and contributions are welcome. Please follow the instructions in the repository and share your experiences to enhance sports entertainment with AWS and Generative AI.
119 |
120 | ## License
121 |
122 | This project is licensed under the MIT License.
123 |
124 | ## Disclaimer
125 |
126 | This repository and its contents are not endorsed by or affiliated with Amazon Web Services (AWS) or any other third-party entities. It represents my personal viewpoints and not those of my past or current employers. All third-party libraries, modules, plugins, and SDKs are the property of their respective owners.
127 |
128 |
129 |
--------------------------------------------------------------------------------
/lambda/receive_message/index.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import json
3 | import math
4 | import os
5 | import base64
6 |
7 | import boto3
8 | import io
9 | ssm = boto3.client('ssm')
10 | s3 = boto3.client('s3')
11 |
12 |
13 | region=os.environ.get('REGION')
14 |
15 | bedrock_runtime = boto3.client('bedrock-runtime', region_name=region)
16 |
17 | dynamodb = boto3.resource('dynamodb')
18 | lambda_client = boto3.client('lambda')
19 | sqs_client = boto3.client('sqs')
20 | client_rek=boto3.client('rekognition')
21 |
22 |
23 | def lambda_handler(event, context):
24 |
25 | s3_event_message = json.loads(event['Records'][0]['body'])
26 | bucket_name = s3_event_message['Records'][0]['s3']['bucket']['name']
27 | # Retrieve the parameter value from Parameter Store and parse it as JSON
28 | response = ssm.get_parameter(Name=bucket_name, WithDecryption=False)
29 | #print(bucket_name,response)
30 | my_json_str = response['Parameter']['Value']
31 | config_json = json.loads(my_json_str)
32 |
33 |
34 | bucket_name=config_json['bucket_name']
35 | table_name=config_json['table_name']
36 | table = dynamodb.Table(table_name)
37 |
38 |
39 | file=event['Records'][0]['body']
40 | #print(type(file))
41 | #print(file)
42 | file=file.split("key")[1].split(",")[0].replace('":"',"").replace('"',"")
43 | #print(file)
44 |
45 | if 1==1:
46 | #for file in master_filename_list[:]:
47 | #outevent['filename'] = file
48 | #bucket=bucket_name
49 | #photo=file
50 |
51 |
52 | # Get Object in S3
53 | response = s3.get_object(Bucket=bucket_name, Key=file)
54 | image_content = response['Body'].read()
55 |
56 | # Encoding images to base64
57 | base64_encoded_image = base64.b64encode(image_content).decode('utf-8')
58 |
59 |
60 |
61 | prompt="""
62 | Begin a meticulous inspection of the soccer game image at hand. Examine each aspect within the frame closely to identify and catalogue visible elements. Concentrate on pinpointing the location of the players, the soccer ball, and most importantly, the soccer goal — defined as the structure composed of the goalposts and the net. It is vital to distinguish the soccer goal from the field's white markings, such as midfield lines or sidelines. The classification is straightforward: an image is marked as 'Highlight' if the soccer goal is clearly present, without any consideration of the event's context or your knowledge of the game's significance. In contrast, if the soccer goal is not visible, classify the image as 'Normal'. Additionally, any frame that does not display the soccer field should be automatically labeled as 'Normal' as well. Focus purely on object presence within the image for categorization, adhering strictly to the visible inclusion of the entire soccer goal to determine a 'Highlight', independent of any other activity taking place on the field. Again, The soccer goal must be fully visible, including both goalposts and the entire net between them, to be classified as a 'Highlight'. Your final response should be a SINGLE WORD ONLY: Word 'Normal' or Word 'Highlight' and DO NOT provide any other explaination.
63 | """
64 |
65 |
66 |
67 |
68 | # Create payloads for Bedrock Invoke, and can change model parameters to get the results you want.
69 | ##"modelId": "anthropic.claude-3-opus-20240229-v1:0"
70 | ###"modelId": "anthropic.claude-3-sonnet-20240229-v1:0"
71 | payload = {
72 | "modelId": "anthropic.claude-3-sonnet-20240229-v1:0",
73 | "contentType": "application/json",
74 | "accept": "application/json",
75 | "body": {
76 | "anthropic_version": "bedrock-2023-05-31",
77 | "max_tokens": 4096,
78 | "top_k": 250,
79 | "top_p": 0.999,
80 | "temperature": 0,
81 | "messages": [
82 | {
83 | "role": "user",
84 | "content": [
85 | {
86 | "type": "image",
87 | "source": {
88 | "type": "base64",
89 | "media_type": "image/jpeg",
90 | "data": base64_encoded_image
91 | }
92 | },
93 | {
94 | "type": "text",
95 | "text": prompt
96 | }
97 | ]
98 | }
99 | ]
100 | }
101 | }
102 |
103 | # Convert the payload to bytes
104 | body_bytes = json.dumps(payload['body']).encode('utf-8')
105 |
106 | # Invoke the model
107 | response = bedrock_runtime.invoke_model(
108 | body=body_bytes,
109 | contentType=payload['contentType'],
110 | accept=payload['accept'],
111 | modelId=payload['modelId']
112 | )
113 |
114 | # Process the response
115 | response_body = json.loads(response['body'].read())
116 | result = response_body['content'][0]['text']
117 |
118 | #print(result)
119 | #print("Custom labels detected: " + str(label_count))
120 | #print(response1)
121 | #feature1=result
122 | # try:
123 | # feature1=result
124 | # except:
125 | # feature1='Normal'
126 | # #print(file)
127 | # ####
128 |
129 | pickup='no'
130 | if result=='Highlight':
131 | pickup='yes'
132 | #FramerateNumerator=10
133 | #FramerateNumerator=10
134 | FramerateNumerator=1
135 | #FramerateNumerator=2
136 | time_parm=1000/FramerateNumerator ##originally FramerateNumerator was 20 then param was 50
137 | #timeins=str(int(file.split('.')[1])*50);timeouts=str(1 + int(file.split('.')[1])*50)
138 | timeins=str(int(file.split('.')[1])*time_parm);timeouts=str(1 + int(file.split('.')[1])*time_parm)
139 | #response = table.put_item(Item={'filename':file, 'features':feature1})
140 | response = table.put_item(Item={'filename':file, 'features':result, 'pickup':pickup, 'timeins':timeins, 'timeouts':timeouts})
141 |
142 |
143 |
--------------------------------------------------------------------------------
/lambda/create_thumbnails/mediaconvert_setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "Queue": "",
3 | "UserMetadata": {},
4 | "Role": "",
5 | "Settings": {
6 | "OutputGroups": [
7 | {
8 | "CustomName": "MP4",
9 | "Name": "File Group",
10 | "Outputs": [
11 | {
12 | "ContainerSettings": {
13 | "Container": "MP4",
14 | "Mp4Settings": {
15 | "CslgAtom": "INCLUDE",
16 | "FreeSpaceBox": "EXCLUDE",
17 | "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
18 | }
19 | },
20 | "VideoDescription": {
21 | "Width": 1280,
22 | "ScalingBehavior": "DEFAULT",
23 | "Height": 720,
24 | "TimecodeInsertion": "DISABLED",
25 | "AntiAlias": "ENABLED",
26 | "Sharpness": 50,
27 | "CodecSettings": {
28 | "Codec": "H_264",
29 | "H264Settings": {
30 | "InterlaceMode": "PROGRESSIVE",
31 | "NumberReferenceFrames": 3,
32 | "Syntax": "DEFAULT",
33 | "Softness": 0,
34 | "GopClosedCadence": 1,
35 | "GopSize": 90,
36 | "Slices": 1,
37 | "GopBReference": "DISABLED",
38 | "SlowPal": "DISABLED",
39 | "SpatialAdaptiveQuantization": "ENABLED",
40 | "TemporalAdaptiveQuantization": "ENABLED",
41 | "FlickerAdaptiveQuantization": "DISABLED",
42 | "EntropyEncoding": "CABAC",
43 | "Bitrate": 7000000,
44 | "FramerateControl": "INITIALIZE_FROM_SOURCE",
45 | "RateControlMode": "CBR",
46 | "CodecProfile": "MAIN",
47 | "Telecine": "NONE",
48 | "MinIInterval": 0,
49 | "AdaptiveQuantization": "HIGH",
50 | "CodecLevel": "AUTO",
51 | "FieldEncoding": "PAFF",
52 | "SceneChangeDetect": "ENABLED",
53 | "QualityTuningLevel": "SINGLE_PASS",
54 | "FramerateConversionAlgorithm": "DUPLICATE_DROP",
55 | "UnregisteredSeiTimecode": "DISABLED",
56 | "GopSizeUnits": "FRAMES",
57 | "ParControl": "INITIALIZE_FROM_SOURCE",
58 | "NumberBFramesBetweenReferenceFrames": 2,
59 | "RepeatPps": "DISABLED"
60 | }
61 | },
62 | "AfdSignaling": "NONE",
63 | "DropFrameTimecode": "ENABLED",
64 | "RespondToAfd": "NONE",
65 | "ColorMetadata": "INSERT"
66 | },
67 | "AudioDescriptions": [
68 | {
69 | "AudioTypeControl": "FOLLOW_INPUT",
70 | "CodecSettings": {
71 | "Codec": "AAC",
72 | "AacSettings": {
73 | "AudioDescriptionBroadcasterMix": "NORMAL",
74 | "Bitrate": 96000,
75 | "RateControlMode": "CBR",
76 | "CodecProfile": "LC",
77 | "CodingMode": "CODING_MODE_2_0",
78 | "RawFormat": "NONE",
79 | "SampleRate": 48000,
80 | "Specification": "MPEG4"
81 | }
82 | },
83 | "LanguageCodeControl": "FOLLOW_INPUT"
84 | }
85 | ]
86 | }
87 | ],
88 | "OutputGroupSettings": {
89 | "Type": "FILE_GROUP_SETTINGS",
90 | "FileGroupSettings": {
91 | "Destination": ""
92 | }
93 | }
94 | },
95 | {
96 | "CustomName": "Thumbnails",
97 | "Name": "File Group",
98 | "Outputs": [
99 | {
100 | "ContainerSettings": {
101 | "Container": "RAW"
102 | },
103 | "VideoDescription": {
104 | "Width": 768,
105 | "ScalingBehavior": "DEFAULT",
106 | "Height": 576,
107 | "TimecodeInsertion": "DISABLED",
108 | "AntiAlias": "ENABLED",
109 | "Sharpness": 50,
110 | "CodecSettings": {
111 | "Codec": "FRAME_CAPTURE",
112 | "FrameCaptureSettings": {
113 | "FramerateNumerator": 1,
114 | "FramerateDenominator": 1,
115 | "MaxCaptures": 10000000,
116 | "Quality": 100
117 | }
118 | },
119 | "AfdSignaling": "NONE",
120 | "DropFrameTimecode": "ENABLED",
121 | "RespondToAfd": "NONE",
122 | "ColorMetadata": "INSERT"
123 | }
124 | }
125 | ],
126 | "OutputGroupSettings": {
127 | "Type": "FILE_GROUP_SETTINGS",
128 | "FileGroupSettings": {
129 | "Destination": ""
130 | }
131 | }
132 | }
133 | ],
134 | "AdAvailOffset": 0,
135 | "Inputs": [
136 | {
137 | "AudioSelectors": {
138 | "Audio Selector 1": {
139 | "Offset": 0,
140 | "DefaultSelection": "DEFAULT",
141 | "ProgramSelection": 1
142 | }
143 | },
144 | "VideoSelector": {
145 | "ColorSpace": "FOLLOW"
146 | },
147 | "FilterEnable": "AUTO",
148 | "PsiControl": "USE_PSI",
149 | "FilterStrength": 0,
150 | "DeblockFilter": "DISABLED",
151 | "DenoiseFilter": "DISABLED",
152 | "TimecodeSource": "EMBEDDED",
153 | "FileInput": ""
154 | }
155 | ]
156 | }
157 | }
--------------------------------------------------------------------------------
/lambda/create_assets/index.py:
--------------------------------------------------------------------------------
1 | import boto3
2 | import random
3 | import string
4 | import json
5 | import os
6 | #from botocore.vendored import requests
7 |
8 | region=os.environ.get('REGION')
9 |
10 | s3 = boto3.client('s3',region_name=region)
11 | dynamodb = boto3.client('dynamodb')
12 | sqs = boto3.client('sqs')
13 | lambda_client = boto3.client('lambda')
14 | dynamo = boto3.resource('dynamodb')
15 |
16 |
17 | source_bucket_name=os.environ.get('VIDEO_ASSETS_BUCKET_NAME')
18 |
19 |
20 | def lambda_handler(event, context):
21 | random_string=''.join(random.choice(string.ascii_lowercase) for i in range(10))
22 | entireInput = json.loads(event['entireInput'])
23 | file_name=entireInput['file_name']
24 |
25 | id=event['Executionid']
26 | final_highlight='none'
27 | final_original='none'
28 | status='Running'
29 |
30 |
31 | #######
32 |
33 | # table_name1='SFworkflow-2yumt4yc2raibewutmhs4p3qhi-dev'
34 | # table = dynamo.Table(table_name1)
35 | # print(event)
36 |
37 |
38 | # # Write to DynamoDB table
39 | # table.put_item(
40 | # Item={
41 | # 'id': id,
42 | # 'status': status,
43 | # 'final_highlight':final_highlight,
44 | # 'final_original': final_original
45 | # }
46 | # )
47 |
48 | #####
49 | # Define the SQS queue name
50 | queue_name = 'soccer-'+ random_string
51 |
52 | if 1==1:
53 | # Create the SQS queue
54 | response = sqs.create_queue(
55 | QueueName=queue_name,
56 | Attributes={
57 | "VisibilityTimeout":"900",
58 | "MessageRetentionPeriod":"3600",
59 | "KmsMasterKeyId":""
60 | },
61 |
62 | )
63 | # Wait until the queue is created
64 | queue_url = response['QueueUrl']
65 | #waiter = sqs.get_waiter('queue_exists')
66 | #waiter.wait(QueueUrl=queue_url)
67 |
68 | # Get the ARN of the newly created SQS queue
69 |
70 | queue_attributes = sqs.get_queue_attributes(
71 | QueueUrl=queue_url,
72 | AttributeNames=['QueueArn']
73 | )
74 | queue_arn = queue_attributes['Attributes']['QueueArn']
75 | print(queue_arn)
76 |
77 |
78 | policy={
79 | "Version": "2008-10-17",
80 | "Id": "__default_policy_ID",
81 | "Statement": [
82 | {
83 | "Sid": "__owner_statement",
84 | "Effect": "Allow",
85 | "Principal": {
86 | "AWS": "arn:aws:iam::456667773660:root"
87 | },
88 | "Action": "SQS:*",
89 | "Resource": queue_arn
90 | },
91 | {
92 | "Sid": "example-statement-ID",
93 | "Effect": "Allow",
94 | "Principal": {
95 | "Service": "s3.amazonaws.com"
96 | },
97 | "Action": "SQS:SendMessage",
98 | "Resource": queue_arn
99 | }
100 | ]
101 | }
102 |
103 | response = sqs.set_queue_attributes(
104 | QueueUrl=queue_url,
105 | Attributes={
106 | 'Policy': json.dumps(policy)
107 | }
108 | )
109 |
110 |
111 | lambda_function_arn=os.environ.get('RECEIVE_MESSAGE_LAMBDA_ARN')
112 | lambda_function_name=lambda_function_arn.split(':')[-1]
113 | StatementId=f'{queue_name}-lambda',
114 |
115 |
116 |
117 |
118 | if 1==1:
119 | response = lambda_client.create_event_source_mapping(
120 | EventSourceArn=queue_arn,
121 | FunctionName=lambda_function_arn,
122 | Enabled=True,
123 | BatchSize=1
124 | )
125 | print(f'SQS trigger added to Lambda function {lambda_function_name}')
126 | # except:
127 | # print('error')
128 |
129 | #####
130 | bucket_name ='soccer-'+ random_string
131 | if 1==1:
132 | # Generate a random bucket name
133 | #Create the S3 bucket
134 | bucket = s3.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={
135 | 'LocationConstraint': region
136 | })
137 | # Wait until the bucket is created
138 | waiter = s3.get_waiter('bucket_exists')
139 | waiter.wait(Bucket=bucket_name)
140 |
141 |
142 | # Define the S3 event notification configuration
143 | event_notification_configuration = {
144 | 'QueueConfigurations': [
145 | {
146 | 'Id': 's3-put-notification'+queue_name,
147 | 'QueueArn': queue_arn,
148 | 'Events': [
149 | 's3:ObjectCreated:Put'
150 | ],
151 | 'Filter': {
152 | 'Key': {
153 | 'FilterRules': [
154 | {
155 | 'Name': 'prefix',
156 | 'Value': 'Thumbnails/'
157 | },
158 | {
159 | 'Name': 'suffix',
160 | 'Value': '.jpg'
161 | }
162 | ]
163 | }
164 | }
165 | }
166 | ]
167 | }
168 |
169 |
170 | # Add the S3 event notification configuration to the bucket
171 | s3.put_bucket_notification_configuration(
172 | Bucket=bucket_name,
173 | NotificationConfiguration=event_notification_configuration
174 | )
175 |
176 |
177 | #
178 |
179 | destination_bucket_name = bucket_name
180 |
181 | # Define the name of the file you want to copy
182 |
183 | # Create an S3 client object
184 |
185 | # Copy the file from the source bucket to the destination bucket
186 | copy_source = {
187 | 'Bucket': source_bucket_name,
188 | 'Key': 'public/'+file_name
189 | }
190 | s3.copy_object(CopySource=copy_source, Bucket=destination_bucket_name, Key=file_name)
191 |
192 |
193 |
194 | table_name = 'soccer-'+random_string
195 |
196 | if 1==1:
197 | # Generate a random table name
198 | params = {
199 | 'TableName': table_name,
200 | 'KeySchema': [
201 | {'AttributeName': 'filename', 'KeyType': 'HASH'},
202 | {'AttributeName': 'features', 'KeyType': 'RANGE'}
203 | ],
204 | 'AttributeDefinitions': [
205 | {'AttributeName': 'filename', 'AttributeType': 'S'},
206 | {'AttributeName': 'features', 'AttributeType': 'S'}
207 | ],
208 | 'ProvisionedThroughput': {
209 | 'ReadCapacityUnits': 100,
210 | 'WriteCapacityUnits': 100
211 | }
212 | }
213 | table = dynamo.create_table(**params)
214 | # Wait until the table is created
215 | #waiter = dynamodb.meta.client.get_waiter('table_exists')
216 | #waiter.wait(TableName=table_name)
217 | table.wait_until_exists()
218 | print(f'Creating {table_name}')
219 |
220 | print('S3 bucket created: ', bucket_name)
221 | print('DynamoDB table created: ', table_name)
222 |
223 | #
224 | return {
225 | 'statusCode': 200,
226 | 'body': json.dumps('Hello from Lambda!'),
227 | 's3_bucket':bucket_name,
228 | 'DDB_table':table_name,
229 | 'queue_url':queue_url,
230 | 'queue_arn':queue_arn,
231 | 'vide_file_name':file_name
232 |
233 | }
234 |
--------------------------------------------------------------------------------
/lambda/receive_message/other_prompts.txt:
--------------------------------------------------------------------------------
1 | # prompt= """
2 | # You are an AI assistant tasked on checking soccer game images to label them as 'Normal' or 'Highlight'. An image earns a 'Highlight' label for capturing crucial moments: the ball on its way to the net, players near the goal in pivotal scoring or defending actions, penalty shots, or intense moments close to the 18-yard box. Specifically, a 'Highlight' scene involves the goal in view with players from both teams near it, or at least a striker approaching the goal, with or without the goalkeeper present. On the flip side, images are 'Normal' if they capture mid-field action, scenes showing fans, or anything outside the soccer field—these don't make the cut for 'Highlight'. In essence, 'Highlight' is for those edge-of-your-seat moments that could change the game, while 'Normal' is for everything else, including celebrations or off-pitch scenes. Just remember, your decision: 'Normal' for routine play or off-pitch scenes, 'Highlight' for those potential game-changer moments.Your final response for each analysis should be a single word: 'Normal' or 'Highlight'.
3 | # """
4 | # prompt= """
5 | # You are an AI assistant tasked with looking at pictures from a soccer game to decide if they are 'Normal' or 'Highlight'. A picture is a 'Highlight' if it shows a truly exciting moment, like when the ball is in the goal, hitting the net, or when the goalkeeper is diving to block a shot. If the ball is very close to a goal and both teams are actively involved in the play, that's also a 'Highlight'. However, avoid selecting frames that simply show players celebrating after a goal as those are not the key moments we want to capture. We want our highlight video to be concise, showcasing only the pivotal actions, such as goals or near-goals. Frames that show celebrations post-goal or lack a critical event, as well as those depicting teams in isolation or mid-field action, should be classified as 'Normal'. Make your choice based on the impact of the scene: classify it as 'Highlight' for high-tension plays or 'Normal' for everything else. Your final response for each analysis should be a single word: 'Normal' or 'Highlight'.
6 | # """
7 |
8 | # prompt= """
9 | # You are an AI assistant assigned to review soccer game images and categorize them as 'Normal' or 'Highlight'. An image is designated as a 'Highlight' if it captures a decisive moment directly involving a goal attempt: the ball clearly heading towards the net with a visible trajectory, a close encounter where players are either scoring or making a critical save near the goal area, penalty kicks, or any high-stakes interaction within the penalty box. To specifically qualify as a 'Highlight', the image must show the goalpost with at least one player making a direct attempt on goal, and the ball must be present in the frame, indicating a goal-scoring or saving moment. Images are considered 'Normal' if they depict midfield duels, fans, player close-ups, crowd reactions and general field play without imminent scoring opportunities, or any scene not directly linked to an active play towards the goal. Thus, 'Highlight' captures the essence of pivotal plays with potential game-altering outcomes, whereas 'Normal' encompasses general gameplay and off-field activities. Your determination for each image should be concise, classified as either 'Normal' or 'Highlight', with 'Highlight' images strictly adhering to the criteria of showing both the goal attempt and the ball within a goal-scoring context.
10 | # Your final response for each analysis should be a single word: 'Normal' or 'Highlight'
11 | # """
12 | # prompt="""
13 | # Your role as an AI assistant is to meticulously inspect soccer game images and assign them a label: 'Normal' or 'Highlight'. To merit a 'Highlight' designation, an image must capture a critical goal-oriented action: this includes the ball in motion towards the net or within the goal vicinity, or a distinct moment of either a scoring chance or a defensive save within or around the goal area. Crucially, for an image to be classified as 'Highlight', it must visually confirm the presence of the goalpost with clear evidence of a player attempting to score or prevent a goal. Conversely, images representing general match play, player celebrations, crowd shots, close-ups of players' expressions, and other such non-goal-centric scenes will be labeled as 'Normal'. 'Highlight' is reserved exclusively for those electrifying junctures that could turn the tide of the game, while 'Normal' covers the broader spectrum of the match, including all instances of play that don't directly concern goal-scoring scenarios. As such, only assign 'Highlight' to those shots that definitively showcase both the goalpost and the ball in a context that suggests a scoring or saving episode.Your final response for each analysis should be a single word: 'Normal' or 'Highlight'.
14 | # """
15 |
16 | # prompt="""
17 | # As an AI trained to discern the thrill of soccer, your task is to analyze game images and label them either 'Normal' or 'Highlight'. A 'Highlight' is a snapshot of peak action, where the goalpost is in clear view within the frame, underscoring a potential goal or a crucial save. Look for the goalpost as a central element; it's a must-have in every 'Highlight' image. While the ball's presence enhances a 'Highlight', it's the goalpost that's non-negotiable. If it's not visible, the moment is 'Normal', regardless of the players' proximity to the goal or the intensity of their expressions. Celebratory huddles, crowd reactions, or player close-ups, despite their excitement, fall into the 'Normal' category unless they coincide with an imminent goal action where the goalpost is also captured. This sharp focus on the goalpost helps ensure we spotlight those pivotal instances that could alter the course of the match, such as a ball sailing towards the net, a last-ditch save, or a heated clash right at the goal line. Remember, 'Highlight' is for those goalpost-framed, match-defining instances; everything else is 'Normal'. Your final response for each analysis should be a single word: 'Normal' or 'Highlight'
18 | # """
19 |
20 |
21 | # prompt="""
22 | # First do a close inspection of the image which is from soccer game and then in your mind explain objects that you can see and then as an AI designed for object detection in soccer images, your key task is to categorize each image as 'Highlight' or 'Normal.' To classify an image as 'Highlight,' a soccer goalpost must be clearly visible in the frame. This is a strict requirement: no goalpost, no highlight—no exceptions. If the goalpost is not discernible, then the image should be classified as 'Normal' without hesitation. Even partial obstruction of a goalpost means it should be classified as 'Normal'. The classification is binary and dependent entirely on the clear visibility of the goalpost. Conclude your analysis with a definitive single-word label: 'Highlight' if the goalpost is present, 'Normal' otherwise. Your final response for each analysis should be a single word: 'Normal' or 'Highlight'
23 | # """
24 |
25 | # prompt="""
26 | # Commence with a thorough inspection and analytical overview of the soccer game image presented. Engage in a meticulous examination to identify and mentally catalog all visible objects. Scrutinize the details: observe player positions, the ball's location, and especially the presence of the goalpost. As an AI configured for precise object detection in soccer imagery, your principal duty is to accurately categorize each frame. A 'Highlight' classification is unequivocally reserved for images where the soccer goalpost is present clearly in the image. Your decision-making process must be black and white, hinging solely on the visibility of the soccer goalpost. Your final response should be A SINGLE WORD: 'Normal' or 'Highlight.
27 | # """
28 | #
29 | # prompt= """
30 | # You are an AI assistant tasked with analyzing images from soccer matches to classify them as either 'Normal' or 'Highlight'. A 'Highlight' image must capture a critical moment that directly impacts the game's outcome, such as a visible goal attempt. This includes the ball on its trajectory towards the goal, clear actions of scoring or a pivotal save within the goal area, or penalty kicks—all within the vicinity of the goalpost. For an image to be classified as a 'Highlight', it must explicitly showcase the goalpost and the ball, with at least one player actively involved in the goal-scoring or goal-saving action. Images that lack the ball's presence, focus on player close-ups, crowd reactions, midfield actions, or any scene not directly connected to an active goal attempt are classified as 'Normal'. The essence of a 'Highlight' lies in capturing the dramatic instances that could alter the game's direction, whereas 'Normal' covers broader aspects of the game, including non-critical gameplay and scenes outside the immediate field of play. Your final response for each analysis should be a single word: 'Normal' or 'Highlight'.
31 | # """
32 |
33 | prompt="""
34 | Examine the provided image from a soccer match. Focus on identifying elements that indicate a crucial play leading to or nearly leading to a goal. Look for images showing players in active engagement with the ball near the goal area, significant defensive or offensive maneuvers, direct goal attempts, or key saves by a goalkeeper. Avoid labeling scenes of general gameplay away from goal areas, crowd reactions, or post-goal celebrations as "Highlight". Your classification should discern the intensity and potential of the play depicted. Label the image as "Highlight" if it captures potential goal-scoring actions. Otherwise, label it as "Normal". The required output is one word: "Highlight" or "Normal"
35 | """
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | from aws_cdk import core
5 | from aws_cdk import aws_lambda as lambda_
6 | from aws_cdk import aws_iam as iam
7 | from aws_cdk import aws_stepfunctions as sfn
8 | from aws_cdk import aws_stepfunctions_tasks as tasks
9 | from constructs import Construct
10 | from aws_cdk import aws_s3 as s3
11 | from aws_cdk import aws_cloudfront as cloudfront
12 | from aws_cdk import aws_cloudfront_origins as origins
13 | import os
14 | from aws_cdk import aws_cloudfront_origins as origins
15 | from aws_cdk import aws_sns as sns
16 | from aws_cdk import aws_sns_subscriptions as subscriptions
17 | import json
18 | from aws_cdk import aws_s3_deployment as s3deploy
19 | import tempfile
20 |
21 |
22 | region="us-west-2"
23 |
24 | class SoccerHighlightsStack(core.Stack):
25 | def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
26 | super().__init__(scope, id, **kwargs)
27 |
28 | # Read the config file
29 | with open("config.json") as config_file:
30 | config = json.load(config_file)
31 |
32 | # Create an SNS topic
33 |
34 | self.highlight_link_topic = sns.Topic(
35 | self, "SoccerHighlightLink",
36 | topic_name="soccer_highlight_link"
37 | )
38 |
39 | # Subscribe the user email to the topic
40 | user_email = config.get("user_email")
41 | if user_email:
42 | self.highlight_link_topic.add_subscription(subscriptions.EmailSubscription(user_email))
43 |
44 |
45 | # Create an S3 bucket
46 | self.video_assets_bucket = s3.Bucket(self, "VideoAssetsBucket") # Be careful with this in production
47 |
48 | oai = cloudfront.OriginAccessIdentity(self, "OAI")
49 |
50 | # Update S3 bucket policy to be accessible from CloudFront OAI
51 | self.video_assets_bucket.grant_read(oai)
52 |
53 | # Create a CloudFront web distribution
54 | self.video_assets_distribution = cloudfront.CloudFrontWebDistribution(self, "VideoAssetsDistribution",
55 | origin_configs=[
56 | cloudfront.SourceConfiguration(
57 | s3_origin_source=cloudfront.S3OriginConfig(
58 | s3_bucket_source=self.video_assets_bucket,
59 | origin_access_identity=oai
60 | ),
61 | behaviors=[cloudfront.Behavior(
62 | is_default_behavior=True,
63 | # Default behavior settings can be adjusted here
64 | )]
65 | ),
66 | cloudfront.SourceConfiguration(
67 | s3_origin_source=cloudfront.S3OriginConfig(
68 | s3_bucket_source=self.video_assets_bucket,
69 | origin_access_identity=oai
70 | ),
71 | behaviors=[cloudfront.Behavior(
72 | path_pattern="/public/HighlightClips/*",
73 | # Settings specific to this path can be adjusted here
74 | )]
75 | ),
76 | # Additional path as needed
77 | cloudfront.SourceConfiguration(
78 | s3_origin_source=cloudfront.S3OriginConfig(
79 | s3_bucket_source=self.video_assets_bucket,
80 | origin_access_identity=oai
81 | ),
82 | behaviors=[cloudfront.Behavior(
83 | path_pattern="/public/*",
84 | # Settings specific to this path can be adjusted here
85 | )]
86 | )
87 | ]
88 | )
89 |
90 | self.mediaconvert_role = iam.Role(self, 'MediaConvertRole',
91 | assumed_by=iam.ServicePrincipal('mediaconvert.amazonaws.com'),
92 | managed_policies=[
93 | iam.ManagedPolicy.from_aws_managed_policy_name('AmazonS3FullAccess'),
94 | iam.ManagedPolicy.from_aws_managed_policy_name('AmazonAPIGatewayInvokeFullAccess'),
95 | iam.ManagedPolicy.from_aws_managed_policy_name('AWSElementalMediaConvertFullAccess')
96 | ],
97 | role_name='mediaconvert_role')
98 |
99 | # Add trust relationship
100 | self.mediaconvert_role.assume_role_policy.add_statements(iam.PolicyStatement(
101 | actions=["sts:AssumeRole"],
102 | principals=[iam.ServicePrincipal("mediaconvert.amazonaws.com")]
103 | ))
104 | # IAM Role for Lambda Functions
105 | lambda_role = self.create_lambda_execution_role()
106 | full_access_policy = iam.ManagedPolicy(
107 | self, 'LambdaFullAccessPolicy',
108 | statements=[
109 | iam.PolicyStatement(
110 | actions=["s3:*"],
111 | resources=["*"],
112 | effect=iam.Effect.ALLOW
113 | ),
114 | iam.PolicyStatement(
115 | actions=["dynamodb:*"],
116 | resources=["*"],
117 | effect=iam.Effect.ALLOW
118 | ),
119 | iam.PolicyStatement(
120 | actions=["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath","ssm:PutParameter"], # Full access may not be advisable depending on your use case
121 | resources=["*"],
122 | effect=iam.Effect.ALLOW
123 | ),
124 | iam.PolicyStatement(
125 | actions=["sqs:*"],
126 | resources=["*"],
127 | effect=iam.Effect.ALLOW
128 | ),
129 | iam.PolicyStatement(
130 | actions=["logs:*", "cloudwatch:*"],
131 | resources=["*"],
132 | effect=iam.Effect.ALLOW
133 | ),
134 | # Assuming 'bedrock:*' is a placeholder for actual actions for a service named 'Bedrock'
135 | iam.PolicyStatement(
136 | actions=["bedrock:*"],
137 | resources=["*"],
138 | effect=iam.Effect.ALLOW
139 | ),
140 | iam.PolicyStatement(
141 | actions=["lambda:*"],
142 | resources=["*"],
143 | effect=iam.Effect.ALLOW
144 | ),
145 | # Additional permission for CreateEventSourceMapping
146 | iam.PolicyStatement(
147 | actions=["lambda:CreateEventSourceMapping"],
148 | resources=["*"],
149 | effect=iam.Effect.ALLOW
150 | ),
151 | iam.PolicyStatement(
152 | actions=[
153 | "lambda:InvokeFunction",
154 | "lambda:CreateEventSourceMapping", # Add this line to include the required permission
155 | # Add other lambda related permissions as needed
156 | ],
157 | resources=["*"] # Adjust this as necessary for your use case
158 | ),
159 | iam.PolicyStatement(
160 | actions=[
161 | "mediaconvert:*",
162 | ],
163 | resources=["*"],
164 | effect=iam.Effect.ALLOW
165 | ),
166 | iam.PolicyStatement(
167 | actions=['iam:PassRole'],
168 | resources=['*'],
169 | effect=iam.Effect.ALLOW
170 | ),
171 | iam.PolicyStatement(
172 | actions=["sns:Publish"],
173 | resources=["*"],
174 | effect=iam.Effect.ALLOW
175 | )
176 | ]
177 | )
178 |
179 | # Attach the policy to the lambda_role
180 | lambda_role.add_managed_policy(full_access_policy)
181 | #lambda_role.add_to_policy()
182 | # IAM Role for Step Functions
183 | step_functions_role = self.create_step_functions_role()
184 | step_functions_role.add_to_policy(iam.PolicyStatement(
185 | actions=[
186 | "lambda:InvokeFunction",
187 | "states:StartExecution",
188 | "logs:CreateLogGroup",
189 | "logs:CreateLogStream",
190 | "logs:PutLogEvents"
191 | ],
192 | resources=["*"]
193 | ))
194 | # Define the Lambda functions
195 | self.lambda_functions = self.create_lambda_functions(lambda_role)
196 |
197 | # Define State Machine
198 | self.state_machine = self.define_state_machine(step_functions_role)
199 |
200 | def create_lambda_execution_role(self):
201 | lambda_role = iam.Role(
202 | self, 'LambdaExecutionRole',
203 | assumed_by=iam.ServicePrincipal('lambda.amazonaws.com')
204 | )
205 | # Add policies to lambda_role as necessary...
206 | return lambda_role
207 |
208 |
209 | def create_step_functions_role(self):
210 | step_functions_role = iam.Role(
211 | self, 'StepFunctionsExecutionRole',
212 | assumed_by=iam.ServicePrincipal('states.amazonaws.com')
213 | )
214 | # Add policies to step_functions_role as necessary...
215 | return step_functions_role
216 |
217 | def create_lambda_functions(self, role):
218 | functions = {}
219 | function_names = [
220 | 'receive_message', #
221 | 'create_assets',
222 | 'create_thumbnails',
223 | 'check_sqs',
224 | 'create_short_clips',
225 | 'create_large_clips',
226 | 'merge_clips',
227 | 'mediaconvert_check',
228 | 'stepfunction_status',
229 | 'send_email',
230 | 'delete_s3_sqs_ddb',
231 | ]
232 |
233 | for name in function_names:
234 | # Check if the function is either 'receive_message' or 'create_assets'
235 | if name in ['receive_message']:
236 | timeout = core.Duration.minutes(15) # Set timeout to 15 minutes
237 | memory_size = 3000 # Set memory size to 10240 MB
238 | else:
239 | # For all other functions, use default values (or any other values you prefer)
240 | timeout = core.Duration.minutes(15) # Default timeout
241 | memory_size = 512 # Default memory size
242 |
243 | if name == 'receive_message':
244 | functions[name] = lambda_.Function(
245 | self, f'{name}Lambda',
246 | runtime=lambda_.Runtime.PYTHON_3_9,
247 | handler='index.lambda_handler',
248 | code=lambda_.Code.from_asset(f'lambda/{name}'),
249 | role=role,
250 | timeout=timeout, # Apply custom or default timeout
251 | memory_size=memory_size, # Apply custom or default memory size
252 | environment={
253 | "REGION":region }
254 | )
255 | receive_message_lambda_arn = functions[name].function_arn
256 | elif name == 'create_assets':
257 | functions[name] = lambda_.Function(
258 | self, f'{name}Lambda',
259 | runtime=lambda_.Runtime.PYTHON_3_9,
260 | handler='index.lambda_handler',
261 | code=lambda_.Code.from_asset(f'lambda/{name}'),
262 | role=role,
263 | timeout=timeout, # Apply custom or default timeout
264 | memory_size=memory_size, # Apply custom or default memory size
265 | environment={
266 | 'RECEIVE_MESSAGE_LAMBDA_ARN': receive_message_lambda_arn,
267 | 'VIDEO_ASSETS_BUCKET_NAME': self.video_assets_bucket.bucket_name,
268 | "REGION":region }
269 | )
270 | elif name == 'send_email':
271 | functions[name] = lambda_.Function(
272 | self, f'{name}Lambda',
273 | runtime=lambda_.Runtime.PYTHON_3_9,
274 | handler='index.lambda_handler',
275 | code=lambda_.Code.from_asset(f'lambda/{name}'),
276 | role=role,
277 | timeout=timeout, # Apply custom or default timeout
278 | memory_size=memory_size, # Apply custom or default memory size
279 | environment={
280 | 'CLOUDFRONT_ENDPOINT_URL': self.video_assets_distribution.distribution_domain_name,
281 | 'VIDEO_ASSETS_BUCKET_NAME': self.video_assets_bucket.bucket_name,
282 | 'TOPIC_ARN': self.highlight_link_topic.topic_arn,
283 | #'MEDIA_CONVERT_ROLE_ARN': self.mediaconvert_role.role_arn
284 | }
285 | )
286 | else:
287 | functions[name] = lambda_.Function(
288 | self, f'{name}Lambda',
289 | runtime=lambda_.Runtime.PYTHON_3_9,
290 | handler='index.lambda_handler',
291 | code=lambda_.Code.from_asset(f'lambda/{name}'),
292 | role=role,
293 | timeout=timeout, # Apply custom or default timeout
294 | memory_size=memory_size, # Apply custom or default memory size
295 | environment={
296 | 'MEDIA_CONVERT_ROLE_ARN': self.mediaconvert_role.role_arn}
297 |
298 | )
299 |
300 | return functions
301 |
302 | def define_state_machine(self, role):
303 |
304 | create_assets_task = tasks.LambdaInvoke(
305 | self, 'create assets',
306 | lambda_function=self.lambda_functions['create_assets'],
307 | payload=sfn.TaskInput.from_object({
308 | "Executionid.$": "$$.Execution.Id",
309 | "entireInput.$": "$"
310 | }),
311 | retry_on_service_exceptions=True,
312 | output_path="$.Payload"
313 | )
314 | create_thumbnails_task = tasks.LambdaInvoke(
315 | self, 'Create Thumbnails',
316 | lambda_function=self.lambda_functions['create_thumbnails'],
317 | result_path="$.guid",
318 | retry_on_service_exceptions=True,
319 | )
320 |
321 | check_sqs_empty_task = tasks.LambdaInvoke(
322 | self, 'sqs empty?',
323 | lambda_function=self.lambda_functions['check_sqs'],
324 | result_path="$.status1",
325 | retry_on_service_exceptions=True,
326 | )
327 |
328 | create_short_clips_task = tasks.LambdaInvoke(
329 | self, 'Start clipping jobs',
330 | lambda_function=self.lambda_functions['create_short_clips'],
331 | result_path="$.guid1",
332 | retry_on_service_exceptions=True,
333 | )
334 |
335 | Thumbnails_Status_task = tasks.LambdaInvoke(
336 | self, 'Thumbnails Status?',
337 | lambda_function=self.lambda_functions['mediaconvert_check'],
338 | result_path="$.status",
339 | retry_on_service_exceptions=True,
340 | )
341 | ShortClips_status_task = tasks.LambdaInvoke(
342 | self, 'ShortClips status',
343 | lambda_function=self.lambda_functions['mediaconvert_check'],
344 | result_path="$.status2",
345 | retry_on_service_exceptions=True,
346 | )
347 | final_clips_status_task = tasks.LambdaInvoke(
348 | self, 'final clips status',
349 | lambda_function=self.lambda_functions['mediaconvert_check'],
350 | result_path="$.status3",
351 | retry_on_service_exceptions=True,
352 | )
353 | merge_clips_status_task = tasks.LambdaInvoke(
354 | self, 'merge clips status',
355 | lambda_function=self.lambda_functions['mediaconvert_check'],
356 | result_path="$.status4",
357 | retry_on_service_exceptions=True,
358 | )
359 |
360 |
361 | create_large_clips_task = tasks.LambdaInvoke(
362 | self, 'Final long clips',
363 | lambda_function=self.lambda_functions['create_large_clips'],
364 | result_path="$.guid2",
365 | retry_on_service_exceptions=True,
366 | )
367 | merge_clips_task = tasks.LambdaInvoke(
368 | self, 'Merge long clips',
369 | lambda_function=self.lambda_functions['merge_clips'],
370 | result_path="$.guid3",
371 | retry_on_service_exceptions=True,
372 | )
373 | write_status_task = tasks.LambdaInvoke(
374 | self, 'write status',
375 | lambda_function=self.lambda_functions['stepfunction_status'],
376 | result_path="$.status_write",
377 | retry_on_service_exceptions=True,
378 | )
379 |
380 | send_email_task = tasks.LambdaInvoke(
381 | self, 'Get Highlight Link',
382 | lambda_function=self.lambda_functions['send_email'],
383 | result_path="$.outputs",
384 | retry_on_service_exceptions=True,
385 | )
386 | def add_custom_retry_policy(task):
387 | task.add_retry(
388 | errors=["Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"],
389 | interval=core.Duration.seconds(10),
390 | max_attempts=300,
391 | backoff_rate=2
392 | )
393 |
394 | def add_standard_retry_policy(task):
395 | task.add_retry(
396 | errors=["Lambda.TooManyRequestsException", "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"],
397 | interval=core.Duration.seconds(10),
398 | max_attempts=6,
399 | backoff_rate=2
400 | )
401 |
402 | # Apply the retry policy
403 | add_custom_retry_policy(Thumbnails_Status_task)
404 | add_custom_retry_policy(check_sqs_empty_task)
405 | add_custom_retry_policy(ShortClips_status_task)
406 | add_custom_retry_policy(final_clips_status_task)
407 | add_custom_retry_policy(merge_clips_status_task)
408 |
409 | add_standard_retry_policy(Thumbnails_Status_task)
410 | add_standard_retry_policy(check_sqs_empty_task)
411 | add_standard_retry_policy(ShortClips_status_task)
412 | add_standard_retry_policy(final_clips_status_task)
413 | add_standard_retry_policy(merge_clips_status_task)
414 |
415 |
416 | # Waits
417 | wait_for_thumbnail = sfn.Wait(
418 | self, 'Wait for Thumbnail',
419 | time=sfn.WaitTime.duration(core.Duration.seconds(25))
420 | )
421 |
422 | wait_for_classification = sfn.Wait(
423 | self, 'Wait for classification',
424 | time=sfn.WaitTime.duration(core.Duration.seconds(15))
425 | )
426 |
427 | wait_for_short_clip = sfn.Wait(
428 | self, 'Wait short clip',
429 | time=sfn.WaitTime.duration(core.Duration.seconds(10))
430 | )
431 |
432 | wait_for_long_clips = sfn.Wait(
433 | self, 'Wait for long clips',
434 | time=sfn.WaitTime.duration(core.Duration.seconds(10))
435 | )
436 |
437 | wait_for_merge_long = sfn.Wait(
438 | self, 'Wait for merge long',
439 | time=sfn.WaitTime.duration(core.Duration.seconds(10))
440 | )
441 |
442 |
443 | # Define the success state
444 |
445 | success_state = sfn.Succeed(self, "Success")
446 |
447 | # Define fail state
448 | failed_state = sfn.Fail(self, "Failed")
449 |
450 | # Define Choice states, these should direct the flow within the choice itself
451 | thumbnails_finish_choice = sfn.Choice(self, "Thumbnails Finish?")\
452 | .when(sfn.Condition.string_equals("$.status.Payload", "COMPLETE"), wait_for_classification)\
453 | .when(sfn.Condition.string_equals("$.status.Payload", "ERROR"), failed_state)\
454 | .otherwise(wait_for_thumbnail)
455 |
456 | all_classified_choice = sfn.Choice(self, "all classified?")\
457 | .when(sfn.Condition.string_equals("$.status1.Payload", "COMPLETE"), create_short_clips_task)\
458 | .when(sfn.Condition.string_equals("$.status1.Payload", "ERROR"), failed_state)\
459 | .otherwise(wait_for_classification)
460 |
461 | wait_for_classification.next(check_sqs_empty_task)
462 | check_sqs_empty_task.next(all_classified_choice)
463 |
464 |
465 |
466 | create_short_clips_task.next(wait_for_short_clip)
467 | wait_for_short_clip.next(ShortClips_status_task)
468 |
469 | short_clips_finish_choice = sfn.Choice(self, "short clips Finish?")\
470 | .when(sfn.Condition.string_equals("$.status2.Payload", "COMPLETE"),create_large_clips_task )\
471 | .when(sfn.Condition.string_equals("$.status2.Payload", "ERROR"), failed_state)\
472 | .otherwise(wait_for_short_clip)
473 |
474 | ShortClips_status_task.next(short_clips_finish_choice)
475 |
476 | create_large_clips_task.next(wait_for_long_clips)
477 | wait_for_long_clips.next(final_clips_status_task)
478 |
479 |
480 | final_clips_finish_choice = sfn.Choice(self, "final clips Finish?")\
481 | .when(sfn.Condition.string_equals("$.status3.Payload", "COMPLETE"), merge_clips_task)\
482 | .when(sfn.Condition.string_equals("$.status3.Payload", "ERROR"), failed_state)\
483 | .otherwise(wait_for_long_clips)
484 | final_clips_status_task.next(final_clips_finish_choice)
485 | merge_clips_task.next(wait_for_merge_long)
486 | wait_for_merge_long.next(merge_clips_status_task)
487 |
488 |
489 | final_merge_finish_choice = sfn.Choice(self, "final merge finish")\
490 | .when(sfn.Condition.string_equals("$.status4.Payload", "COMPLETE"), send_email_task)\
491 | .when(sfn.Condition.string_equals("$.status4.Payload", "ERROR"), failed_state)\
492 | .otherwise(wait_for_merge_long)
493 |
494 | merge_clips_status_task.next(final_merge_finish_choice)
495 | send_email_task.next(write_status_task)
496 | write_status_task.next(success_state)
497 |
498 |
499 | # Define the complete state machine definition
500 | definition = create_assets_task\
501 | .next(create_thumbnails_task)\
502 | .next(wait_for_thumbnail)\
503 | .next(Thumbnails_Status_task)\
504 | .next(thumbnails_finish_choice)\
505 |
506 | state_machine = sfn.StateMachine(
507 | self, 'SoccerHighlightsStateMachine',
508 | definition=definition,
509 | timeout=core.Duration.hours(5),
510 | role=role
511 | )
512 |
513 | return state_machine
514 |
515 |
516 |
517 | app = core.App()
518 | SoccerHighlightsStack(app, "SoccerHighlightsStack")
519 |
520 | app.synth()
521 |
--------------------------------------------------------------------------------