├── .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 |
50 |
51 |
52 |
53 |
54 |
55 |
57 |
59 |
60 |
61 |
135 |
136 | '''
137 |
--------------------------------------------------------------------------------