├── 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 | Watch the video 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 | --------------------------------------------------------------------------------