Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Emojivision App #13

Merged
merged 8 commits into from
Aug 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ jobs:
name: run linters
command: |
source venv/bin/activate
pylint manage.py
pylint pymoji
pylint manage.py pymoji

# Run tests and save results
- run:
Expand All @@ -77,6 +76,7 @@ jobs:

# Deploy successful builds on master to Google App Engine
# https://circleci.com/docs/2.0/deployment_integrations/
# https://cloud.google.com/sdk/gcloud/reference/app/deploy
- deploy:
name: maybe deploy
command: |
Expand All @@ -85,7 +85,7 @@ jobs:
gcloud --quiet components update
gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json
gcloud --quiet config set project $GCLOUD_PROJECT_ID
gcloud --quiet app deploy --promote
gcloud --quiet app deploy --promote --stop-previous-version
else
echo 'skipping deployment for non-master branch'
fi
Expand Down
80 changes: 61 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
### 🍺 OS Dependencies

- Install and update [Homebrew](https://brew.sh/) if necessary
```bash
```
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew update
brew doctor
```
- Install dependencies
```bash
```
brew install coreutils pyenv pyenv-virtualenv
```
- Add required shims to your bash profile:
Expand All @@ -24,28 +24,28 @@ eval "$(pyenv virtualenv-init -)"

#### Google Cloud Platform Deployment Dependencies
- If deploying via Google Cloud
```bash
```
brew cask install google-cloud-sdk
cd pymoji
./gcloud init
cd <project-dir>
./gc init
# enter 'pymoji-176318' for project
```

### 🐍 Python Environment

- Snake menagerie tamed via [pyenv](https://github.com/pyenv/) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)
- Install versions of Python if necessary
```bash
```
pyenv install 2.7.13 # necessary for google cloud sdk
pyenv install 3.5.3 # necessary for pymoji
```
- Create new virtual environment
```bash
```
pyenv virtualenv 3.5.3 pymoji
```
- Install dependencies
```bash
cd pymoji
```
cd <project-dir>
echo 'pymoji' > .python-version
pip install --upgrade pip setuptools
pip install -r requirements.txt
Expand All @@ -54,8 +54,8 @@ pip install -r requirements.txt
### Git Hooks

- Auomatically run linters before pushing:
```bash
cd pymoji
```
cd <project-dir>
./git-pre-push.sh
```

Expand All @@ -65,28 +65,70 @@ cd pymoji
- [pylint extension](https://packagecontrol.io/packages/SublimeLinter-pylint)
- [eslint extension](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint)
- IMPORTANT: because reasons, you must also do some global setup:
```bash
```
pyenv global 3.5.3
cd ~ # go somewhere that falls through to global environment
pip install pylint pylint-flask
```


## Run Local
## Run Local Webserver

- Open a terminal and run the dev server:
```bash
cd pymoji
python manage.py runserver
- Open a terminal and run the dev webserver:
```
cd <project-dir>
./cli runserver
```

- Open a browser and navigate to http://localhost:5000


## Run Local Scripts

- HALP.
```
cd <project-dir>
./cli -?
```

- Wat do `runserver`?
```
cd <project-dir>
./cli runserver -?
```

- Emojivision a file:
```
cd <project-dir>
./cli runface -i face-input.jpg
```
```
Found 1 face
Writing to file pymoji/static/gen/face-input-output.jpg
```

- Emojivision a directory:
```
cd <project-dir>
./cli runfolder -d pymoji/static/
```
```
processing angry
Found 1 face
Writing to file pymoji/static/gen/angry-output.jpg
processing composite
Found 5 faces
Writing to file pymoji/static/gen/composite-output.jpg
processing cry
...
```


## Deploy to Cloud (Manual)

```bash
./gcloud app deploy --project pymoji-176318
```
cd <project-dir>
./gc app deploy --project pymoji-176318
```


2 changes: 2 additions & 0 deletions cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
python manage.py "$@"
File renamed without changes.
7 changes: 4 additions & 3 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

from flask_script import Manager

from pymoji.app import app, RESOURCES
from pymoji import faces, process_folder
from pymoji.app import app
from pymoji.constants import OUTPUT_DIR, STATIC_DIR


MANAGER = Manager(app)
Expand All @@ -20,10 +21,10 @@ def runface(input_image=None, output_image=None):
input_image: name of image resource file to process faces in.
output_image: name of output file to write modified image to. (optional)
"""
input_path = os.path.join(RESOURCES, input_image)
input_path = os.path.join(STATIC_DIR, input_image)

if output_image:
output_path = os.path.join(RESOURCES, output_image)
output_path = os.path.join(OUTPUT_DIR, output_image)
else:
output_path = process_folder.generate_output_path(input_image)

Expand Down
84 changes: 76 additions & 8 deletions pymoji/app.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,92 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import logging
import os

from flask import Flask
from flask import Flask, flash, redirect, render_template, request, url_for
from google.cloud import error_reporting
import google.cloud.logging

from pymoji.constants import OUTPUT_PATH, PROJECT_ID, TEMP_FILENAME, TEMP_PATH
from pymoji.faces import main

RESOURCES = os.path.join('pymoji', 'static')
OUTPUT_DIR = os.path.join(RESOURCES, 'gen')

app = Flask(__name__)
app.config['SECRET_KEY'] = 'so-so-secret' # change this
# IMPORTANT: be extremely careful with config['TRAP_HTTP_EXCEPTIONS']
# Setting to True will break Google App Engine load balancers!!!
# (this probably has to do with GAE expecting a 404 at /_ah/healthcheck)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ this comment represents almost 48 hours of debugging
😞


# Configure logging
if not app.testing:
client = google.cloud.logging.Client(project=PROJECT_ID)
# Attaches a Google Stackdriver logging handler to the root logger
client.setup_logging(logging.INFO)

@app.route('/')
def hello():
"""Return a friendly HTTP greeting."""
return '👋 🌎!'

@app.after_request
def after_request(response):
# Quick and dirty hack to wipe out caching for now
response.headers['Last-Modified'] = datetime.now()
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'

# Security-related best practice headers
response.headers.add('X-Frame-Options', 'DENY')
response.headers.add('X-Content-Type-Options', 'nosniff')
response.headers.add('X-XSS-Protection', '1')
return response


@app.route('/emojivision')
def emojivision():
input_image_url = url_for('static', filename=TEMP_FILENAME)
output_image_url = None

if os.path.isfile(OUTPUT_PATH):
output_image_url = url_for('static', filename='gen/face-input-output.jpg')

return render_template('result.html',
input_image_url=input_image_url,
output_image_url=output_image_url)


@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
# check if the post request has an image
if 'image' not in request.files:
flash('No image')
return redirect(request.url)
image = request.files['image']
# if user does not select file, browser submits an empty part sans filename
if image.filename == '':
flash('No selected file')
return redirect(request.url)
if image and image.filename:
image.save(TEMP_PATH)
image.close()
main(TEMP_PATH, OUTPUT_PATH)

return redirect(url_for('emojivision'))

return render_template("form.html")


@app.route("/robots.txt")
def robots_txt():
return "User-agent: *\nDisallow: /\n"


@app.errorhandler(500)
def server_error(e):
"""Error handler that reports exceptions to Stackdriver Error Reporting.

Note that this is only used iff DEBUG=False
"""
http_context = error_reporting.build_flask_context(request)
error_client = error_reporting.Client(project=PROJECT_ID)
error_client.report_exception(http_context=http_context)
logging.exception('An error occurred during a request.')
return """
An internal error occurred: <pre>{}</pre>
Expand Down
14 changes: 14 additions & 0 deletions pymoji/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os


# Project directory paths
PACKAGE_DIR = 'pymoji'
STATIC_DIR = os.path.join(PACKAGE_DIR, 'static')
EMOJI_DIR = os.path.join(STATIC_DIR, 'emoji')
OUTPUT_DIR = os.path.join(STATIC_DIR, 'gen')
TEMP_FILENAME = 'face-input.jpg'
TEMP_PATH = os.path.join(STATIC_DIR, TEMP_FILENAME)
OUTPUT_PATH = os.path.join(OUTPUT_DIR, 'face-input-output.jpg')

# Google App Engine
PROJECT_ID = 'pymoji-176318'
8 changes: 4 additions & 4 deletions pymoji/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
from google.cloud.vision import types
from PIL import Image

from pymoji.app import RESOURCES


EMOJI_DIR = os.path.join(RESOURCES, 'emoji')
from pymoji.constants import EMOJI_DIR, OUTPUT_DIR


def detect_face(face_file):
Expand Down Expand Up @@ -41,6 +38,9 @@ def replace_faces(image, faces, output_filename):
for face in faces:
render_emoji(im, face)

if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)

im.save(output_filename)


Expand Down
7 changes: 2 additions & 5 deletions pymoji/process_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
import PIL

from pymoji import faces
from pymoji.app import OUTPUT_DIR
from pymoji.constants import OUTPUT_DIR


#TODO put this into a helper/utilities file if it's used by both process_folder and faces.py
def generate_output_path(input_image):
"""Makes a path to save the result image into. Makes the dir if it ain't there.
"""Makes a path to save the result image into.

Args:
input_image: A string, eg "face-input.jpg"

Returns:
"pymoji/static/gen/face-input-output.jpg"
"""
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)

filename = input_image.split('.')[-2]
extension = input_image.split('.')[-1]
output_image = filename + "-output." + extension
Expand Down
Binary file added pymoji/static/favicon.ico
Binary file not shown.
29 changes: 29 additions & 0 deletions pymoji/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>✨📸🕶✨</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="TensorBros: Emojivision">
<link rel="shortcut icon" href="/static/favicon.ico" />
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
</head>
<body>
<div class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a href="/">
<div class="navbar-brand">📸 Upload 📸</div>
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="/emojivision">✨🕶 Emojivision 🕶✨</a></li>
</ul>
</div>
</div>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
Loading