Skip to content

Commit 3838e19

Browse files
authored
Merge pull request #12 from brionmario/develop
stable version 2019-05-03
2 parents ec01806 + 331624b commit 3838e19

23 files changed

+558
-92
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Ignoring shape predictor
2+
app/data/classifiers/shape_predictor_68_face_landmarks.dat
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

app/__init__.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import os
22
import logging.config
33

4+
from celery import Celery
45
from flask import Flask
56
from flask_cors import CORS
67
from flask_sqlalchemy import SQLAlchemy
78
from flask_marshmallow import Marshmallow
9+
from flask_socketio import SocketIO
10+
from cssi.core import CSSI
11+
812
from config import CONFIG
913

1014
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
11-
LOG_FILES_PATH = os.path.split(BASE_DIR)[0] + '/logs'
15+
LOG_FILES_PATH = os.path.join(os.path.split(BASE_DIR)[0], "logs")
16+
LOGGER_CONFIG_PATH = os.path.join(os.path.split(BASE_DIR)[0], "config", "logging.conf")
1217

1318
# Try to create a log folder
1419
try:
@@ -17,31 +22,63 @@
1722
except OSError:
1823
pass
1924

20-
# load logging config file
21-
logging.config.fileConfig('config/logging.conf', disable_existing_loggers=False)
22-
# init file logger
23-
logger = logging.getLogger('CSSI_REST_API')
25+
# Load logging config file
26+
logging.config.fileConfig(LOGGER_CONFIG_PATH, disable_existing_loggers=False)
27+
# Init file logger
28+
logger = logging.getLogger('cssi.api')
29+
30+
# set `socketio` and `engineio` log level to `ERROR`
31+
logging.getLogger('socketio').setLevel(logging.ERROR)
32+
logging.getLogger('engineio').setLevel(logging.ERROR)
2433

34+
cssi = CSSI(shape_predictor="app/data/classifiers/shape_predictor_68_face_landmarks.dat", debug=False, config_file="config.cssi")
2535
db = SQLAlchemy()
2636
ma = Marshmallow()
37+
socketio = SocketIO()
38+
celery = Celery(__name__,
39+
broker=os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0'),
40+
backend=os.environ.get('CELERY_BACKEND', 'redis://localhost:6379/0'))
41+
celery.config_from_object('celeryconfig')
42+
43+
# Import models to register them with SQLAlchemy
44+
from app.models import * # noqa
45+
46+
# Import celery task to register them with Celery workers
47+
from . import tasks # noqa
48+
49+
# Import Socket.IO events to register them with Flask-SocketIO
50+
from . import events # noqa
2751

2852

29-
def create_app(config_name):
53+
def create_app(config_name=None, main=True):
54+
if config_name is None:
55+
config_name = os.environ.get('CSSI_CONFIG', 'default')
3056
app = Flask(__name__)
31-
CORS(app, support_credentials=True)
3257
app.config.from_object(CONFIG[config_name])
58+
3359
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # disabling sqlalchemy event system
3460

61+
CORS(app, support_credentials=True) # Add CORS support
62+
3563
CONFIG[config_name].init_app(app)
3664

3765
root = CONFIG[config_name].APPLICATION_ROOT
3866

39-
# flask migrate doesn't recognize the tables without this import
40-
from app.models import Application, Genre, ApplicationType, Session, Questionnaire
41-
4267
# Set up extensions
4368
db.init_app(app)
4469

70+
if main:
71+
# Initialize socketio server and attach it to the message queue.
72+
socketio.init_app(app,
73+
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'])
74+
else:
75+
# Initialize socketio to emit events through through the message queue.
76+
socketio.init_app(None,
77+
message_queue=app.config['SOCKETIO_MESSAGE_QUEUE'],
78+
async_mode='threading')
79+
80+
celery.conf.update(CONFIG[config_name].CELERY_CONFIG)
81+
4582
# Create app blueprints
4683
from app.routes.v1 import main as main_blueprint
4784
app.register_blueprint(main_blueprint, url_prefix=root + '/')

app/data/classifiers/readme.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
use the bellow link to download the shape predictor file.
2+
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

app/events.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
3+
from app.models import Session
4+
from . import socketio, celery, db
5+
from .tasks import calculate_latency, persist_frames, record_sentiment
6+
from .utils import decode_base64
7+
8+
logger = logging.getLogger('cssi.api')
9+
10+
11+
@socketio.on("test/init")
12+
def on_test_init(session_id):
13+
from .wsgi_aux import app
14+
with app.app_context():
15+
session = Session.query.filter_by(id=session_id).first()
16+
if session is not None:
17+
if session.status == 'initialized':
18+
session.status = 'started'
19+
db.session.commit()
20+
socketio.send({"status": "success", "message": "The test session started successfully."}, json=True)
21+
logger.info("Successfully initialized the test session. ID: {0}".format(session_id))
22+
23+
24+
@socketio.on("test/start")
25+
def on_test_start(head_frame, scene_frame, session_id, latency_interval=2):
26+
_head_frame = head_frame["head_frame"]
27+
_scene_frame = scene_frame["scene_frame"]
28+
latency_frame_count = 10
29+
30+
# decoding head-frame image(base64) string to OpenCV compatible format
31+
_head_frame_decoded = decode_base64(_head_frame)
32+
33+
# decoding scene-frame image(base64) string to OpenCV compatible format
34+
_scene_frame_decoded = decode_base64(_scene_frame)
35+
36+
# chain the two tasks which persist the frames and pass them to
37+
# the latency worker after the specified time interval.
38+
result = persist_frames.delay(head_frame=_head_frame, scene_frame=_scene_frame, limit=latency_frame_count)
39+
40+
if result:
41+
calculate_latency.delay(session_id["session_id"], limit=latency_frame_count)
42+
43+
record_sentiment.apply_async(args=[_head_frame_decoded, session_id["session_id"]], expires=10)
44+
45+
46+
@socketio.on("test/stop")
47+
def on_test_stop(session_id):
48+
from .wsgi_aux import app
49+
with app.app_context():
50+
session = Session.query.filter_by(id=session_id).first()
51+
if session is not None:
52+
if session.status == 'started':
53+
session.status = 'completed'
54+
db.session.commit()
55+
socketio.send({"status": "success", "message": "The test session completed successfully."}, json=True)
56+
celery.control.purge() # stop all celery workers
57+
logger.info("The test session was terminated successfully. ID: {0}".format(session_id))
58+
59+
60+
@socketio.on("disconnect")
61+
def on_disconnect():
62+
"""A Socket.IO client has disconnected."""
63+
celery.control.purge() # stop all celery workers
64+
logger.info("The Socket.IO client disconnected.")

app/models/application.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Application(db.Model):
1313
developer = db.Column(db.String(100), nullable=False)
1414
type_id = db.Column(db.Integer, db.ForeignKey('application_type.id', use_alter=True, name='fk_type_id'), nullable=False)
1515
description = db.Column(db.String(250), nullable=False)
16+
public_sharing = db.Column(db.Boolean, nullable=False, default=False)
1617
creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
1718
genre_id = db.Column(db.Integer, db.ForeignKey('genre.id', use_alter=True, name='fk_genre_id'), nullable=False)
1819
sessions = db.relationship('Session', backref='app', lazy='dynamic')
@@ -36,5 +37,6 @@ class ApplicationSchema(ma.Schema):
3637
developer = fields.String(required=True, validate=validate.Length(1, 100))
3738
type = fields.Nested(ApplicationTypeSchema, dump_only=True)
3839
description = fields.String(required=True, validate=validate.Length(1, 250))
39-
creation_date = fields.DateTime()
40+
creation_date = fields.DateTime(dump_only=True)
4041
genre = fields.Nested(GenreSchema, dump_only=True)
42+
public_sharing = fields.Boolean(required=True)

app/models/application_type.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __repr__(self):
5050

5151

5252
class ApplicationTypeSchema(ma.Schema):
53-
id = fields.Integer()
53+
id = fields.Integer(dump_only=True)
5454
name = fields.String(required=True)
5555
display_name = fields.String(required=True)
5656
display_name_full = fields.String(required=True)

app/models/genre.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ def __repr__(self):
4848

4949

5050
class GenreSchema(ma.Schema):
51-
id = fields.Integer()
51+
id = fields.Integer(dump_only=True)
5252
name = fields.String(required=True)
5353
display_name = fields.String(required=True)

app/models/questionnaire.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from marshmallow import fields, validate
1+
from marshmallow import fields
22
from .. import db, ma
33

44

@@ -19,14 +19,8 @@ def __repr__(self):
1919
return '<Questionnaire %r>' % self.id
2020

2121

22-
class SymptomSchema(ma.Schema):
23-
name = fields.String(required=False)
24-
display_name = fields.String(required=False)
25-
score = fields.String(required=False)
26-
27-
2822
class QuestionnaireSchema(ma.Schema):
2923
id = fields.Integer(dump_only=True)
30-
pre = fields.List(fields.Nested(SymptomSchema), dump_only=True)
31-
post = fields.List(fields.Nested(SymptomSchema), dump_only=True)
32-
creation_date = fields.DateTime()
24+
pre = fields.Dict(required=True)
25+
post = fields.Dict(required=False)
26+
creation_date = fields.DateTime(dump_only=True)

app/models/session.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from marshmallow import fields, validate
2+
from sqlalchemy.ext.mutable import MutableList
23
from .. import db, ma
34
from .application import ApplicationSchema
45
from .questionnaire import QuestionnaireSchema
@@ -8,14 +9,15 @@ class Session(db.Model):
89
__tablename__ = 'session'
910

1011
id = db.Column(db.Integer, primary_key=True)
12+
status = db.Column(db.String(25), nullable=False, default='initialised')
1113
app_id = db.Column(db.Integer, db.ForeignKey('application.id', use_alter=True, name='fk_app_id'), nullable=False)
1214
creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
1315
expected_emotions = db.Column(db.JSON, nullable=False)
1416
questionnaire_id = db.Column(db.Integer, db.ForeignKey('questionnaire.id', use_alter=True, name='fk_questionnaire_id'), nullable=False)
1517
cssi_score = db.Column(db.Float, nullable=False, default=0)
16-
latency_scores = db.Column(db.JSON, nullable=False, default={})
18+
latency_scores = db.Column(MutableList.as_mutable(db.JSON), nullable=False, default=[])
1719
total_latency_score = db.Column(db.Float, nullable=False, default=0)
18-
sentiment_scores = db.Column(db.JSON, nullable=False, default={})
20+
sentiment_scores = db.Column(MutableList.as_mutable(db.JSON), nullable=False, default=[])
1921
total_sentiment_score = db.Column(db.Float, nullable=False, default=0)
2022
questionnaire_scores = db.Column(db.JSON, nullable=True, default={})
2123
total_questionnaire_score = db.Column(db.Float, nullable=False, default=0)
@@ -31,7 +33,16 @@ def __repr__(self):
3133

3234
class SessionSchema(ma.Schema):
3335
id = fields.Integer(dump_only=True)
34-
creation_date = fields.DateTime()
35-
expected_emotions = fields.List(fields.String(), dump_only=True)
36+
status = fields.String()
37+
creation_date = fields.DateTime(dump_only=True)
38+
expected_emotions = fields.List(fields.String(), required=True)
3639
app = fields.Nested(ApplicationSchema, dump_only=True)
3740
questionnaire = fields.Nested(QuestionnaireSchema, dump_only=True)
41+
cssi_score = fields.Float()
42+
latency_scores = fields.List(fields.Dict())
43+
total_latency_score = fields.Float()
44+
sentiment_scores = fields.List(fields.Dict())
45+
total_sentiment_score = fields.Float()
46+
questionnaire_scores = fields.Dict()
47+
total_questionnaire_score = fields.Float()
48+

app/routes/v1/application.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
import traceback
2121
from flask_cors import cross_origin
2222
from flask import Blueprint, jsonify, request
23-
from app.models import Application, ApplicationType,ApplicationTypeSchema, ApplicationSchema, Genre, GenreSchema
23+
from app.models import Application, ApplicationType, ApplicationTypeSchema, ApplicationSchema, Genre, GenreSchema
2424
from app import db
2525

26-
logger = logging.getLogger('CSSI_REST_API')
26+
logger = logging.getLogger('cssi.api')
2727

2828
application = Blueprint('application', __name__)
2929

@@ -73,17 +73,6 @@ def get_application_genres():
7373
@cross_origin(supports_credentials=True)
7474
def create_application():
7575
"""Create a new Application"""
76-
77-
json_data = request.get_json(force=True)
78-
79-
if not json_data:
80-
return jsonify({'status': 'error', 'message': 'No input was provided.'}), 400
81-
82-
# Validate and deserialize input
83-
data, errors = application_schema.load(json_data)
84-
if errors:
85-
return jsonify({'status': 'error', 'message': 'Incorrect format of data provided.', 'data': errors}), 422
86-
8776
name = request.json['name']
8877
identifier = str(uuid.uuid4().hex)
8978
developer = request.json['developer']
@@ -99,7 +88,8 @@ def create_application():
9988
if not genre:
10089
return {'status': 'error', 'message': 'Invalid Genre Type'}, 400
10190

102-
new_application = Application(name=name, identifier=identifier, developer=developer, type=type, description=description, genre=genre)
91+
new_application = Application(name=name, identifier=identifier,
92+
developer=developer, type=type, description=description, genre=genre)
10393

10494
db.session.add(new_application)
10595
db.session.commit()
@@ -112,13 +102,15 @@ def create_application():
112102
@application.after_request
113103
def after_request(response):
114104
"""Logs a debug message on every successful request."""
115-
logger.debug('%s %s %s %s %s', request.remote_addr, request.method, request.scheme, request.full_path, response.status)
105+
logger.debug('%s %s %s %s %s', request.remote_addr, request.method,
106+
request.scheme, request.full_path, response.status)
116107
return response
117108

118109

119110
@application.errorhandler(Exception)
120111
def exceptions(e):
121112
"""Logs an error message and stacktrace if a request ends in error."""
122113
tb = traceback.format_exc()
123-
logger.error('%s %s %s %s 5xx INTERNAL SERVER ERROR\n%s', request.remote_addr, request.method, request.scheme, request.full_path, tb)
114+
logger.error('%s %s %s %s 5xx INTERNAL SERVER ERROR\n%s', request.remote_addr,
115+
request.method, request.scheme, request.full_path, tb)
124116
return e.status_code

0 commit comments

Comments
 (0)