├── .python-version ├── code ├── src │ ├── pmlocek │ │ ├── __init__.py │ │ └── common │ │ │ ├── __init__.py │ │ │ ├── log.py │ │ │ └── s3.py │ └── whats_your_name │ │ ├── __init__.py │ │ ├── config.py │ │ ├── sns.py │ │ ├── lambda_handler.py │ │ ├── rekognition.py │ │ └── html │ │ └── __init__.py └── test │ └── whats_your_name │ ├── __init__.py │ └── test_lambda_handler.py ├── requirements.txt ├── dev-requirements.txt ├── setup.cfg ├── scripts ├── init.sh ├── deploy.sh └── package.sh ├── whats-your-name.iml ├── setup.py ├── LICENSE ├── README.md ├── template.yaml ├── template-serverless-repo.yaml └── .gitignore /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.7 2 | -------------------------------------------------------------------------------- /code/src/pmlocek/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/src/pmlocek/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/src/whats_your_name/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/test/whats_your_name/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/test/whats_your_name/test_lambda_handler.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3>1.9.0 2 | requests>2.20.0 -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | awscli==1.11.190 2 | boto3==1.4.7 3 | virtualenv==15.1.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = code/test 3 | addopts = -v 4 | 5 | [aliases] 6 | test=pytest -------------------------------------------------------------------------------- /scripts/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install -r dev-requirements.txt 4 | 5 | virtualenv -p `pyenv which python3` venv 6 | source venv/bin/activate -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | aws cloudformation deploy \ 4 | --template-file ./packaged-template.yaml \ 5 | --stack-name WhatsYourName \ 6 | --capabilities CAPABILITY_IAM \ 7 | --parameter-overrides FaceImagesBucketName=$FACES_BUCKET_NAME RekognitionCollectionId=$REKOGNITION_COLLECTION_ID -------------------------------------------------------------------------------- /whats-your-name.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from os.path import splitext, basename 3 | 4 | from setuptools import setup, find_packages 5 | 6 | setup( 7 | name='whats-your-name', 8 | version='0.7.0', 9 | license='MIT License', 10 | packages=find_packages('code/src'), 11 | package_dir={'': 'code/src'}, 12 | py_modules=[ 13 | splitext(basename(path))[0] for path in glob('code/src/*.py') 14 | ], 15 | include_package_data=False, 16 | setup_requires=['pytest-runner'], 17 | tests_require=['pytest'], 18 | zip_safe=False, 19 | author='Piotr Mlocek', 20 | url='https://github.com/pimlock/whats-your-name' 21 | ) 22 | -------------------------------------------------------------------------------- /code/src/whats_your_name/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config(object): 5 | def __init__(self, face_collection_id, faces_bucket_name): 6 | self._face_collection_id = face_collection_id 7 | self._faces_bucket_name = faces_bucket_name 8 | 9 | @property 10 | def face_collection_id(self): 11 | return self._face_collection_id 12 | 13 | @property 14 | def face_bucket_name(self): 15 | return self._faces_bucket_name 16 | 17 | @staticmethod 18 | def create_from_env(): 19 | return Config( 20 | os.environ.get('REKOGNITION_COLLECTION_ID'), 21 | os.environ.get('FACES_BUCKET_NAME') 22 | ) 23 | -------------------------------------------------------------------------------- /code/src/pmlocek/common/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import sys 4 | 5 | 6 | def setup_lambda_logging(): 7 | root_logger = logging.getLogger() 8 | root_logger.setLevel(logging.INFO) 9 | 10 | 11 | def _setup_custom_handler(): 12 | """ 13 | Disabling this for now - if we log in different format than default format of Lambda than 14 | logs that are multiline are not parsed correctly by CloudWatch Logs. 15 | """ 16 | # remove all the default handlers that AWSLambda is adding 17 | root_logger = logging.getLogger() 18 | if root_logger.handlers: 19 | for handler in root_logger.handlers: 20 | root_logger.removeHandler(handler) 21 | 22 | formatter = logging.Formatter('%(asctime)-15s [%(levelname)s] %(name)s - %(message)s') 23 | 24 | stdout_handler = logging.StreamHandler(sys.stdout) 25 | stdout_handler.setFormatter(formatter) 26 | 27 | root_logger.addHandler(stdout_handler) 28 | -------------------------------------------------------------------------------- /code/src/whats_your_name/sns.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class AppLinkSender(object): 7 | def __init__(self, sns, app_url): 8 | self.sns = sns 9 | self.app_url = app_url 10 | 11 | def send_link_to_phone_number(self, phone_number): 12 | message_text = 'You requested a link to the What\'sYourName app: {}'.format(self.app_url) 13 | 14 | # TODO figure out exact format of this 15 | message_attributes = { 16 | 'maxPrice': { 17 | 'DataType': 'Number', 18 | 'StringValue': '0.2' 19 | } 20 | } 21 | response = self.sns.publish( 22 | PhoneNumber=phone_number, 23 | Message=message_text, 24 | MessageAttributes=message_attributes 25 | ) 26 | logger.info('Published link to a phone number [%s], messageId: [%s]', phone_number, response.get('MessageId')) 27 | -------------------------------------------------------------------------------- /code/src/pmlocek/common/s3.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class S3ObjectInfo: 7 | def __init__(self, bucket_name, object_key, object_size=None): 8 | self._bucket_name = bucket_name 9 | self._object_key = object_key 10 | 11 | self._object_size = object_size 12 | 13 | @property 14 | def bucket_name(self): 15 | return self._bucket_name 16 | 17 | @property 18 | def object_key(self): 19 | return self._object_key 20 | 21 | @property 22 | def object_size(self): 23 | return self._object_size 24 | 25 | def __str__(self): 26 | return '{}/{}'.format(self._bucket_name, self._object_key) 27 | 28 | @staticmethod 29 | def create_from_s3_notification(record): 30 | s3_info = record['s3'] 31 | object_info = s3_info['object'] 32 | 33 | return S3ObjectInfo( 34 | s3_info['bucket']['name'], 35 | object_info['key'], 36 | object_info['size'] 37 | ) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Piotr Mlocek 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. -------------------------------------------------------------------------------- /scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z ${CODE_DEPLOYMENT_BUCKET+x} ]; 4 | then 5 | echo "You have to set 'CODE_DEPLOYMENT_BUCKET' var first and point it to the bucket where the code will be uploaded." 6 | exit 1 7 | else 8 | echo "Code will be uploaded to this bucket: $CODE_DEPLOYMENT_BUCKET" 9 | fi 10 | 11 | # cleanup 12 | rm -rf dist 13 | rm -f whats-your-name-package.zip 14 | 15 | # create directory for all the code to go to 16 | mkdir dist 17 | 18 | # copy source files 19 | rsync -a \ 20 | --prune-empty-dirs \ 21 | --exclude '*.pyc' \ 22 | code/src/ dist/ 23 | 24 | # copy dependencies 25 | pip install -r requirements.txt 26 | rsync -a \ 27 | --prune-empty-dirs \ 28 | --exclude '*.pyc' \ 29 | --exclude 'pip*' \ 30 | --exclude 'setuptools*' \ 31 | --exclude 'wheel*' \ 32 | --exclude 'boto*' \ 33 | --exclude 'botocore*' \ 34 | --exclude 'docutils*' \ 35 | venv/lib/python3.6/site-packages/ dist/ 36 | 37 | # create package 38 | pushd . 39 | cd dist 40 | zip -q -r ../whats-your-name-package.zip . 41 | popd 42 | 43 | aws cloudformation package \ 44 | --template-file ./template.yaml \ 45 | --s3-bucket $CODE_DEPLOYMENT_BUCKET \ 46 | --output-template-file ./packaged-template.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's Your Name? 2 | 3 | ## Setup 4 | 5 | #### Clone the repo 6 | 7 | ```bash 8 | git clone https://github.com/pimlock/whats-your-name.git 9 | cd whats-your-name 10 | ``` 11 | 12 | #### Install dev dependencies (make sure you are using Python3) 13 | 14 | ```bash 15 | pip install -r dev-requirements.txt 16 | ``` 17 | 18 | #### Create Virtualenv: 19 | 20 | ```bash 21 | virtualenv venv 22 | source venv/bin/activate 23 | ``` 24 | 25 | #### Create CloudFormation stack 26 | 27 | This step requires your AWS credentials to be set up: 28 | * as `export AWS_ACCESS_KEY_ID=""; export AWS_SECRET_ACCESS_KEY=""` 29 | * stored in `~/.aws/credentials` 30 | 31 | Create required S3 buckets: 32 | 33 | 1. Where CloudFormation will upload Lambda code to (`CODE_DEPLOYMENT_BUCKET`) 34 | 35 | ```bash 36 | # this bucket is where the zip file with AWSLambda code will be uploaded (it's used by CloudFormation to deploy Lambda) 37 | export CODE_DEPLOYMENT_BUCKET=my-bucket 38 | 39 | # creates deployable package for CloudFormation 40 | scripts/package.sh 41 | 42 | export REKOGNITION_COLLECTION_ID=collection-id 43 | export FACES_BUCKET_NAME=bucket-name 44 | 45 | # creates/updates the CloudFormation stack 46 | scripts/deploy.sh 47 | ``` 48 | 49 | © 2018 Piotr Mlocek. This project is licensed under the terms of the MIT license. 50 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: App that tells you name of the person from the picture. 4 | 5 | Parameters: 6 | FaceImagesBucketName: 7 | Type: String 8 | Description: Bucket to which images with faces to index will be uploaded (the bucket cannot already exist!) 9 | Default: whats-your-name-bucket 10 | RekognitionCollectionId: 11 | Type: String 12 | Description: ID of collection in Amazon Rekognition that will store face index 13 | Default: whats-your-name-collection 14 | 15 | Resources: 16 | WhatsYourNameFunction: 17 | Type: AWS::Serverless::Function 18 | Properties: 19 | Description: Heart of the app, it handles indexing and detecting faces. 20 | Handler: whats_your_name.lambda_handler.main 21 | Runtime: python3.6 22 | CodeUri: ./whats-your-name-package.zip 23 | Timeout: 60 24 | MemorySize: 256 25 | Policies: 26 | - RekognitionNoDataAccessPolicy: 27 | CollectionId: !Ref RekognitionCollectionId 28 | - RekognitionReadPolicy: 29 | CollectionId: !Ref RekognitionCollectionId 30 | - RekognitionWriteOnlyAccessPolicy: 31 | CollectionId: !Ref RekognitionCollectionId 32 | - S3CrudPolicy: 33 | BucketName: !Ref FaceImagesBucketName 34 | Events: 35 | get: 36 | Type: Api 37 | Properties: 38 | Path: / 39 | Method: get 40 | post: 41 | Type: Api 42 | Properties: 43 | Path: / 44 | Method: post 45 | upload: 46 | Type: S3 47 | Properties: 48 | Bucket: 49 | Ref: FacesBucket 50 | Events: s3:ObjectCreated:* 51 | Environment: 52 | Variables: 53 | REKOGNITION_COLLECTION_ID: !Ref RekognitionCollectionId 54 | FACES_BUCKET_NAME: !Ref FaceImagesBucketName 55 | DeadLetterQueue: 56 | Type: SQS 57 | TargetArn: 58 | Fn::GetAtt: [WhatsYourNameFunctionDLQ, Arn] 59 | 60 | FacesBucket: 61 | Type: AWS::S3::Bucket 62 | Metadata: 63 | Description: Bucket to which images with faces to index will be uploaded (the bucket cannot already exist!) 64 | Properties: 65 | BucketName: !Ref FaceImagesBucketName 66 | 67 | WhatsYourNameFunctionDLQ: 68 | Type: AWS::SQS::Queue 69 | 70 | Outputs: 71 | ServerUrl: 72 | Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod 73 | Description: The url of the app 74 | -------------------------------------------------------------------------------- /template-serverless-repo.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: App that tells you name of the person from the picture. 4 | 5 | Parameters: 6 | FaceImagesBucketName: 7 | Type: String 8 | Description: Bucket to which images with faces to index will be uploaded (the bucket cannot already exist!) 9 | Default: whats-your-name-bucket 10 | RekognitionCollectionId: 11 | Type: String 12 | Description: ID of collection in Amazon Rekognition that will store face index 13 | Default: whats-your-name-collection 14 | 15 | Resources: 16 | WhatsYourNameFunction: 17 | Type: AWS::Serverless::Function 18 | Properties: 19 | Description: Heart of the app, it handles indexing and detecting faces. 20 | Handler: whats_your_name.lambda_handler.main 21 | Runtime: python3.6 22 | CodeUri: s3://pimlock-serverlessrepo-code/whats-your-name/whats-your-name-package.zip 23 | Timeout: 60 24 | MemorySize: 256 25 | Policies: 26 | - RekognitionNoDataAccessPolicy: 27 | CollectionId: !Ref RekognitionCollectionId 28 | - RekognitionReadPolicy: 29 | CollectionId: !Ref RekognitionCollectionId 30 | - RekognitionWriteOnlyAccessPolicy: 31 | CollectionId: !Ref RekognitionCollectionId 32 | - S3CrudPolicy: 33 | BucketName: !Ref FaceImagesBucketName 34 | Events: 35 | get: 36 | Type: Api 37 | Properties: 38 | Path: / 39 | Method: get 40 | post: 41 | Type: Api 42 | Properties: 43 | Path: / 44 | Method: post 45 | upload: 46 | Type: S3 47 | Properties: 48 | Bucket: 49 | Ref: FacesBucket 50 | Events: s3:ObjectCreated:* 51 | Environment: 52 | Variables: 53 | REKOGNITION_COLLECTION_ID: !Ref RekognitionCollectionId 54 | FACES_BUCKET_NAME: !Ref FaceImagesBucketName 55 | DeadLetterQueue: 56 | Type: SQS 57 | TargetArn: 58 | Fn::GetAtt: [WhatsYourNameFunctionDLQ, Arn] 59 | 60 | FacesBucket: 61 | Type: AWS::S3::Bucket 62 | Metadata: 63 | Description: Bucket to which images with faces to index will be uploaded (the bucket cannot already exist!) 64 | Properties: 65 | BucketName: !Ref FaceImagesBucketName 66 | 67 | WhatsYourNameFunctionDLQ: 68 | Type: AWS::SQS::Queue 69 | 70 | Outputs: 71 | ServerUrl: 72 | Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod 73 | Description: The url of the app 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | .static_storage/ 58 | .media/ 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | ### VirtualEnv template 105 | # Virtualenv 106 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 107 | .Python 108 | [Bb]in 109 | [Ii]nclude 110 | [Ll]ib 111 | [Ll]ib64 112 | [Ll]ocal 113 | pyvenv.cfg 114 | .venv 115 | pip-selfcheck.json 116 | ### JetBrains template 117 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 118 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 119 | 120 | # User-specific stuff: 121 | .idea/**/workspace.xml 122 | .idea/**/tasks.xml 123 | .idea/dictionaries 124 | 125 | # Sensitive or high-churn files: 126 | .idea/**/dataSources/ 127 | .idea/**/dataSources.ids 128 | .idea/**/dataSources.xml 129 | .idea/**/dataSources.local.xml 130 | .idea/**/sqlDataSources.xml 131 | .idea/**/dynamic.xml 132 | .idea/**/uiDesigner.xml 133 | 134 | # Gradle: 135 | .idea/**/gradle.xml 136 | .idea/**/libraries 137 | 138 | # CMake 139 | cmake-build-debug/ 140 | 141 | # Mongo Explorer plugin: 142 | .idea/**/mongoSettings.xml 143 | 144 | ## File-based project format: 145 | *.iws 146 | 147 | ## Plugin-specific files: 148 | 149 | # IntelliJ 150 | out/ 151 | 152 | # mpeltonen/sbt-idea plugin 153 | .idea_modules/ 154 | 155 | # JIRA plugin 156 | atlassian-ide-plugin.xml 157 | 158 | # Cursive Clojure plugin 159 | .idea/replstate.xml 160 | 161 | # Crashlytics plugin (for Android Studio and IntelliJ) 162 | com_crashlytics_export_strings.xml 163 | crashlytics.properties 164 | crashlytics-build.properties 165 | fabric.properties 166 | 167 | /packaged-template.yaml 168 | /s3-uncompressor-lambda-package.zip 169 | -------------------------------------------------------------------------------- /code/src/whats_your_name/lambda_handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | 5 | import boto3 6 | from botocore.exceptions import ClientError 7 | 8 | from pmlocek.common.log import setup_lambda_logging 9 | from pmlocek.common.s3 import S3ObjectInfo 10 | from whats_your_name.config import Config 11 | from whats_your_name.html import file_upload_page 12 | from whats_your_name.rekognition import FaceIndexer, FaceRecognizer, FaceCollection, CelebritiesDetector 13 | from whats_your_name.sns import AppLinkSender 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | _face_collection = None 18 | _celebrities_detector = None 19 | 20 | 21 | class Handler: 22 | def __init__(self, context, config): 23 | self.context = context 24 | self.config = config 25 | 26 | def handle(self, event): 27 | records = event.get('Records', []) 28 | if records and records[0].get('eventSource') == 'aws:s3': 29 | return self._process_s3_event(event) 30 | 31 | request_context = event.get('requestContext') 32 | if request_context: 33 | if event.get('httpMethod') == 'GET': 34 | return self._process_website(event) 35 | elif event.get('httpMethod') == 'POST': 36 | return self._process_whats_your_name(event) 37 | else: 38 | raise Exception('Invalid input!') 39 | 40 | def _process_s3_event(self, event): 41 | face_collection = self._create_face_collection() 42 | face_indexer = FaceIndexer(face_collection) 43 | 44 | for record in event.get('Records', []): 45 | s3_object_info = S3ObjectInfo.create_from_s3_notification(record) 46 | face_indexer.index_face_from_s3(s3_object_info) 47 | 48 | def _process_send_link(self, event): 49 | phone_number = event.get('phoneNumber') 50 | if not phone_number: 51 | raise Exception('Invalid input: missing "phoneNumber" attribute!') 52 | 53 | app_link_sender = AppLinkSender(boto3.client('sns'), self._app_url(event)) 54 | app_link_sender.send_link_to_phone_number(phone_number) 55 | 56 | def _app_url(self, event): 57 | return 'https://{}.execute-api.us-east-1.amazonaws.com/Prod'.format(event.get('requestContext').get('apiId')) 58 | 59 | def _process_whats_your_name(self, event): 60 | image_data = event.get('body', '') 61 | position = image_data.find('base64,') 62 | if not image_data or position == -1: 63 | return { 64 | 'statusCode': 502, 65 | 'body': 'Wrong input!' 66 | } 67 | 68 | image_data = base64.b64decode(image_data[position + 7:]) 69 | response = self._recognize_faces(image_data) 70 | if not response: 71 | response = self._recognize_celebrities(image_data) 72 | if not response: 73 | response = self._create_json_response({}) 74 | 75 | return response 76 | 77 | def _recognize_faces(self, image_data): 78 | face_collection = self._create_face_collection() 79 | face_recognizer = FaceRecognizer(face_collection) 80 | 81 | recognized_faces = face_recognizer.recognize_face_from_image(image_data) 82 | if recognized_faces: 83 | return self._create_json_response({ 84 | 'faces': [{ 85 | 'face_id': face.user_id 86 | } for face in recognized_faces] 87 | }) 88 | 89 | def _recognize_celebrities(self, image_data): 90 | celebrities = self._create_celebrities_detector().detect_celebrities_from_image_data(image_data) 91 | if celebrities: 92 | return self._create_json_response({ 93 | 'celebrities': [{ 94 | 'face_id': celebrity.id, 95 | 'name': celebrity.name, 96 | 'urls': celebrity.urls 97 | } for celebrity in celebrities] 98 | }) 99 | 100 | def _create_json_response(self, body_dict, status_code=200): 101 | return { 102 | 'statusCode': status_code, 103 | 'body': json.dumps(body_dict, separators=(',', ':')), 104 | 'headers': { 105 | 'Content-Type': 'application/json', 106 | } 107 | } 108 | 109 | def _create_face_collection(self): 110 | global _face_collection 111 | if _face_collection is None: 112 | _face_collection = FaceCollection(boto3.client('rekognition'), self.config.face_collection_id) 113 | try: 114 | _face_collection.create_collection() 115 | except ClientError as e: 116 | if not e.response.get('Error', {}).get('Code') == 'ResourceAlreadyExistsException': 117 | logger.exception('Exception when creating Rekognition collection') 118 | 119 | return _face_collection 120 | 121 | def _create_celebrities_detector(self): 122 | global _celebrities_detector 123 | if _celebrities_detector is None: 124 | _celebrities_detector = CelebritiesDetector(boto3.client('rekognition')) 125 | 126 | return _celebrities_detector 127 | 128 | def _process_website(self, event): 129 | return { 130 | 'body': file_upload_page, 131 | 'statusCode': 200, 132 | 'headers': { 133 | 'Content-Type': 'text/html', 134 | } 135 | } 136 | 137 | 138 | def main(event, context): 139 | setup_lambda_logging() 140 | 141 | handler = Handler(context, Config.create_from_env()) 142 | return handler.handle(event) 143 | -------------------------------------------------------------------------------- /code/src/whats_your_name/rekognition.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class Celebrity(object): 7 | def __init__(self, face, raw_json): 8 | self.face = face 9 | self.raw_json = raw_json 10 | 11 | @property 12 | def id(self): 13 | return self.raw_json.get('Id') 14 | 15 | @property 16 | def name(self): 17 | return self.raw_json.get('Name') 18 | 19 | @property 20 | def urls(self): 21 | return self.raw_json.get('Urls', []) 22 | 23 | 24 | class Face(object): 25 | def __init__(self, raw_json): 26 | self.raw_json = raw_json 27 | 28 | @property 29 | def user_id(self): 30 | return self.raw_json['Face'].get('ExternalImageId') 31 | 32 | @property 33 | def face_id(self): 34 | return self.raw_json['Face'].get('FaceId') 35 | 36 | @property 37 | def confidence(self): 38 | return self.raw_json['Face'].get('Confidence') 39 | 40 | 41 | class FaceCollection(object): 42 | def __init__(self, rekognition, collection_id): 43 | self.rekognition = rekognition 44 | self.collection_id = collection_id 45 | 46 | def index_face_from_s3(self, s3_object_info, external_id): 47 | """ 48 | :type external_id: string 49 | :type s3_object_info: pmlocek.common.s3.S3ObjectInfo 50 | """ 51 | logger.info('Indexing face from s3 path: %s with externalId: %s', s3_object_info, external_id) 52 | response = self.rekognition.index_faces( 53 | CollectionId=self.collection_id, 54 | ExternalImageId=external_id, 55 | Image={ 56 | 'S3Object': { 57 | 'Bucket': s3_object_info.bucket_name, 58 | 'Name': s3_object_info.object_key 59 | } 60 | } 61 | ) 62 | logger.info('Indexed: %s', response) 63 | 64 | def index_face_from_file(self, image_path, external_id): 65 | logger.info('Indexing face from path: %s for externalId: %s', image_path, external_id) 66 | 67 | with open(image_path, 'rb') as image_file: 68 | response = self.rekognition.index_faces( 69 | CollectionId=self.collection_id, 70 | ExternalImageId=external_id, 71 | Image={ 72 | 'Bytes': image_file.read() 73 | } 74 | ) 75 | logger.info('Indexed: %s', response) 76 | 77 | def detect_faces_from_file(self, image_path): 78 | logger.info('Detecting faces from image: %s', image_path) 79 | with open(image_path, 'rb') as image_file: 80 | return self.detect_faces_from_image_data(image_file.read()) 81 | 82 | def detect_faces_from_image_data(self, image_data): 83 | detected_faces = [] 84 | try: 85 | response = self.rekognition.search_faces_by_image( 86 | CollectionId=self.collection_id, 87 | Image={'Bytes': image_data}, 88 | MaxFaces=2 89 | ) 90 | face_matches = response.get('FaceMatches', []) 91 | if face_matches: 92 | for face_match in face_matches: 93 | detected_faces.append(Face(face_match)) 94 | logger.info('Found: %s', response) 95 | except Exception as e: 96 | logger.exception('Could not find any faces!') 97 | logger.info(type(e)) 98 | 99 | return detected_faces 100 | 101 | def create_collection(self): 102 | self.rekognition.create_collection(CollectionId=self.collection_id) 103 | 104 | 105 | class CelebritiesDetector(object): 106 | def __init__(self, rekognition): 107 | self.rekognition = rekognition 108 | 109 | def detect_celebrities_from_image_data(self, image_data): 110 | detected_celebrities = [] 111 | try: 112 | response = self.rekognition.recognize_celebrities( 113 | Image={'Bytes': image_data} 114 | ) 115 | celebrities = response.get('CelebrityFaces', []) 116 | if celebrities: 117 | for celebrity_json in celebrities: 118 | detected_celebrities.append(Celebrity(Face(celebrity_json), celebrity_json)) 119 | logger.info('Found: %s', response) 120 | except Exception as e: 121 | logger.exception('No celebrities found!') 122 | logger.info(type(e)) 123 | 124 | return detected_celebrities 125 | 126 | 127 | class FaceIndexer(object): 128 | 129 | def __init__(self, face_collection): 130 | """ 131 | :type face_collection: FaceCollection 132 | """ 133 | self.face_collection = face_collection 134 | 135 | def index_face_from_s3(self, s3_object_info): 136 | """ 137 | :type s3_object_info: pmlocek.common.s3.S3ObjectInfo 138 | """ 139 | # we assume that name of the object will be an ID 140 | external_id = s3_object_info.object_key.split('/')[-1].replace('+', '_') 141 | if '.' in external_id: 142 | external_id = external_id[:external_id.find('.')] 143 | 144 | return self.face_collection.index_face_from_s3(s3_object_info, external_id) 145 | 146 | 147 | class FaceRecognizer(object): 148 | 149 | def __init__(self, face_collection): 150 | """ 151 | :type face_collection: FaceCollection 152 | """ 153 | self.face_collection = face_collection 154 | 155 | def recognize_face_from_image(self, image_data): 156 | """ 157 | :type image_data: binary 158 | """ 159 | return self.face_collection.detect_faces_from_image_data(image_data) 160 | -------------------------------------------------------------------------------- /code/src/whats_your_name/html/__init__.py: -------------------------------------------------------------------------------- 1 | file_upload_page = ''' 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | What's Your Name? 15 | 16 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |

What's Your Name?!

25 |

Take a photo and find out what's their name!

26 |
27 |
28 |
29 | Uploaded photo 30 |
31 |
32 | Checking... 33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 | 57 | 59 | 60 | 61 | 135 | 136 | ''' 137 | --------------------------------------------------------------------------------