├── .gitignore
├── consts.js
├── README.md
├── new.deepeval
├── app
├── src
│ ├── client
│ │ ├── socket.js
│ │ ├── chart.js
│ │ ├── main.js
│ │ └── video.js
│ └── utils.js
└── views
│ └── index.html
├── package.json
├── gulpfile.js
└── app.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.*
3 | .env
4 | .DS_Store
5 | public/main.js
6 | .env.json
7 |
--------------------------------------------------------------------------------
/consts.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | endpoints: {
3 | faceAPI: 'https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect'
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DeepEval ⚡️
2 |
3 | # Development
4 |
5 | Structure:
6 | - `master` - production staging branch (with tagged releases)
7 | - `dev` - main development branch
8 | - `{YOUR_NAME}/{ISSUE}-{FEATURE_NAME}` - feature branch
9 |
10 | You should work on your feature branch until you have implemented it at which point you land it onto `dev` via a merge or, preferably, a rebase. Once `dev` is release-ready it is landed onto `master`.
--------------------------------------------------------------------------------
/new.deepeval:
--------------------------------------------------------------------------------
1 | ____ ____ ___
2 | /\ _`\ /\ _`\ /\_ \
3 | \ \ \/\ \ __ __ _____\ \ \_\_\ __ __ __ \//\ \
4 | \ \ \ \ \ /'__`\ /'__`\/\ __`\ \ _\ /\ \/\ \ /'__`\ \ \ \
5 | \ \ \_\ \/\ __//\ __/\ \ \_\ \ \ \_\ \ \ \_/ |/\ \_\ \_ \_\ \_
6 | \ \____/\ \____\ \____\\ \ __/\ \____/\ \___/ \ \__/ \_\/\____\
7 | \/___/ \/____/\/____/ \ \ \/ \/___/ \/__/ \/__/\/_/\/____/
8 | \ \_\
9 | \/_/
10 |
11 | Connecting to Skynet...
12 |
--------------------------------------------------------------------------------
/app/src/client/socket.js:
--------------------------------------------------------------------------------
1 | import openSocket from 'socket.io-client';
2 | const socket = openSocket('http://localhost:8000'),
3 | lectureIdKey = 'lIdKey',
4 | range = 9999
5 |
6 | function imageBus(imgData, cb) {
7 | // var lectureId = localStorage.getItem(lectureIdKey);
8 | // if (!lectureId) {
9 | // // create new lecture id and save
10 | // lectureId = Math.floor(Math.random() * range);
11 | // localStorage.setItem(lectureIdKey, data.key);
12 | // }
13 | // imgData['id'] = lectureId;
14 |
15 | socket.on('results', results => cb(null, results));
16 | socket.emit('imagePost', imgData);
17 | }
18 |
19 | export default imageBus;
20 |
--------------------------------------------------------------------------------
/app/src/client/chart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {AreaChart} from 'react-easy-chart';
3 |
4 | function AChart(props) {
5 | let dataObject = props.eData.map((el,i) => {
6 | // let t = new Date(el.timestamp).toUTC()
7 | return {x: i, y: el.emotion}
8 | })
9 | console.log(dataObject)
10 | return(
11 |
25 | )
26 | }
27 |
28 | export default AChart
29 |
--------------------------------------------------------------------------------
/app/src/utils.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Normalize a port into a number, string, or false.
4 | */
5 | exports.normalizePort = function (val) {
6 | var port = parseInt(val, 10)
7 |
8 | if (isNaN(port)) {
9 | // named pipe
10 | return val
11 | }
12 |
13 | if (port >= 0) {
14 | // port number
15 | return port
16 | }
17 |
18 | return false
19 | }
20 |
21 | /**
22 | * Event listener for HTTP server "error" event.
23 | */
24 | exports.onError = function (error) {
25 | if (error.syscall !== 'listen') {
26 | throw error
27 | }
28 |
29 | var bind = typeof port === 'string' ?
30 | 'Pipe ' + port :
31 | 'Port ' + port
32 |
33 | // handle specific listen errors with friendly messages
34 | switch (error.code) {
35 | case 'EACCES':
36 | console.error(bind + ' requires elevated privileges')
37 | process.exit(1)
38 | break
39 | case 'EADDRINUSE':
40 | console.error(bind + ' is already in use')
41 | process.exit(1)
42 | break
43 | default:
44 | throw error
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/client/main.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import VideoExample from './video';
4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
5 | import AreaChart from './chart'
6 |
7 | class Chart extends Component {
8 | render() {
9 | return (
10 |
11 | );
12 | }
13 | };
14 |
15 | class App extends Component {
16 | constructor(props) {
17 | super(props)
18 | this.state = {
19 | emotionData: []
20 | }
21 | this.handleEmotionData = this.handleEmotionData.bind(this)
22 | }
23 | handleEmotionData(data) {
24 | // console.log(data)
25 | this.setState({emotionData: [...this.state.emotionData, data]})
26 | }
27 | render() {
28 | const style = {
29 | margin: 12,
30 | };
31 | return (
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | };
39 |
40 | ReactDOM.render(
41 | ,
42 | document.getElementById('entry')
43 | );
44 |
45 | // ReactDOM.render(
46 | // ,
47 | // document.getElementById('well')
48 | // );
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deepeval",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "cat ./new.deepeval && node app.js",
8 | "dev": "cat ./new.deepeval && gulp",
9 | "test": "echo \"TODO\""
10 | },
11 | "dependencies": {
12 | "axios": "^0.17.1",
13 | "azure-arm-cognitiveservices": "^2.0.0",
14 | "body-parser": "^1.18.2",
15 | "cookie-parser": "^1.4.3",
16 | "d3": "3.5.17",
17 | "d3-array": "0.8.1",
18 | "d3-scale": "0.9.3",
19 | "d3-shape": "0.7.1",
20 | "d3-time-format": "2.0.3",
21 | "ejs": "^2.5.7",
22 | "errorhandler": "^1.5.0",
23 | "express": "^4.16.2",
24 | "material-ui": "^0.19.4",
25 | "mongodb": "^2.2.33",
26 | "morgan": "^1.9.0",
27 | "react": "15.5.0",
28 | "react-dom": "15.5.0",
29 | "react-easy-chart": "^0.3.0",
30 | "react-faux-dom": "2.5.0",
31 | "react-icons": "^2.2.7",
32 | "react-multimedia-capture": "^1.1.0",
33 | "serve-favicon": "^2.4.5",
34 | "serve-static": "^1.13.1",
35 | "socket.io": "^2.0.4"
36 | },
37 | "devDependencies": {
38 | "gulp": "^3.9.1",
39 | "gulp-env": "^0.4.0",
40 | "gulp-nodemon": "^2.2.1",
41 | "babel-preset-es2015": "^6.18.0",
42 | "babel-preset-react": "^6.16.0",
43 | "babelify": "^7.3.0",
44 | "browserify": "^13.1.1",
45 | "vinyl-buffer": "^1.0.0",
46 | "vinyl-source-stream": "^1.1.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp'),
2 | nodemon = require('gulp-nodemon'),
3 | env = require('gulp-env'),
4 | browserify = require('browserify'),
5 | babelify = require('babelify'),
6 | source = require('vinyl-source-stream'),
7 | buffer = require('vinyl-buffer'),
8 | JSX_FILES = ['app/src/client/**/*.js']
9 |
10 | function runCommand (command) {
11 | return function (cb) {
12 | exec(command, function (err, stdout, stderr) {
13 | console.log(stdout)
14 | console.log(stderr)
15 | cb(err)
16 | })
17 | }
18 | }
19 |
20 | /*
21 | * GULP tasks
22 | */
23 |
24 | gulp.task('default', ['jsxbuild', 'nodemon'], ()=>{
25 | gulp.watch(JSX_FILES, ['jsxbuild'])
26 | });
27 |
28 | gulp.task('jsxbuild', () => {
29 | return browserify({
30 | entries: ['./app/src/client/main.js'],
31 | debug: true
32 | })
33 | .transform(babelify, {
34 | presets: ["es2015", "react"],
35 | plugins: []
36 | })
37 | .bundle()
38 | .on('error', swallowError)
39 | .pipe(source('main.js'))
40 | .pipe(buffer())
41 | .pipe(gulp.dest('./app/public'));
42 | })
43 |
44 | /* Development */
45 | gulp.task('nodemon', function (cb) {
46 | var called = false
47 | env({
48 | file: '.env.json'
49 | })
50 | return nodemon({
51 | script: './app.js',
52 | watch: ['./app.js'],
53 | ignore: [
54 | 'test/',
55 | 'node_modules/'
56 | ],
57 | })
58 | .on('start', function onStart () {
59 | // ensure start only got called once
60 | if (!called) { cb() }
61 | called = true
62 | })
63 | .on('restart', function onRestart () {
64 | console.log('NODEMON: restarted server!')
65 |
66 | })
67 | })
68 |
69 | function swallowError (error) {
70 | console.log(error.toString());
71 | this.emit('end');
72 | }
73 |
--------------------------------------------------------------------------------
/app/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DeepEval
6 |
7 |
8 |
9 |
10 |
11 |
33 |
34 |
35 |
36 |
37 |
Sentimental Analysis
38 |
39 |
40 |
41 |
Overall Reaction
42 |
43 |

44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/src/client/video.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import MediaCapturer from 'react-multimedia-capture';
3 | import imageBus from './socket';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import FaPlay from 'react-icons/lib/fa/play';
6 | import FaPause from 'react-icons/lib/fa/pause';
7 |
8 |
9 | class VideoExample extends React.Component {
10 | constructor() {
11 | super();
12 | this.state = {
13 | granted: false,
14 | rejectedReason: '',
15 | recording: false,
16 | paused: false,
17 | emotionData: {}
18 | };
19 |
20 | this.handleGranted = this.handleGranted.bind(this);
21 | this.handleDenied = this.handleDenied.bind(this);
22 | this.handleStart = this.handleStart.bind(this);
23 | this.handleStop = this.handleStop.bind(this);
24 | this.handlePause = this.handlePause.bind(this);
25 | this.handleResume = this.handleResume.bind(this);
26 | this.setStreamToVideo = this.setStreamToVideo.bind(this);
27 | this.releaseStreamFromVideo = this.releaseStreamFromVideo.bind(this);
28 | this.downloadVideo = this.downloadVideo.bind(this);
29 | }
30 | handleGranted() {
31 | this.setState({ granted: true });
32 | console.log('Permission Granted!');
33 | }
34 | handleDenied(err) {
35 | this.setState({ rejectedReason: err.name });
36 | console.log('Permission Denied!', err);
37 | }
38 | handleStart(stream) {
39 | this.setState({
40 | recording: true
41 | });
42 |
43 | this.setStreamToVideo(stream);
44 | console.log('Recording Started.');
45 | }
46 | handleStop(blob) {
47 | this.setState({
48 | recording: false
49 | });
50 |
51 | this.releaseStreamFromVideo();
52 |
53 | console.log('Recording Stopped.');
54 | this.downloadVideo(blob);
55 | }
56 | handlePause() {
57 | this.releaseStreamFromVideo();
58 |
59 | this.setState({
60 | paused: true
61 | });
62 | }
63 | handleResume(stream) {
64 | this.setStreamToVideo(stream);
65 |
66 | this.setState({
67 | paused: false
68 | });
69 | }
70 | handleError(err) {
71 | console.error(err);
72 | }
73 | setStreamToVideo(stream) {
74 | let video = this.refs.app.querySelector('video');
75 |
76 | if(window.URL) {
77 | video.src = window.URL.createObjectURL(stream);
78 | const track = stream.getVideoTracks()[0]
79 | this.captureFrame(track)
80 | setInterval(function(){ this.captureFrame(track) }.bind(this), 2000);
81 | }
82 | else {
83 | video.src = stream;
84 | }
85 | }
86 | postImageUpdateEmotionData(img64) {
87 | let data = {
88 | uri: img64,
89 | timestamp: new Date().getTime(),
90 | }
91 | imageBus(data, (err, results) => {
92 | if (err) {
93 | console.error(err);
94 | return;
95 | }
96 | // this.setState({emotionData: results})
97 | this.props.onDataPush(results)
98 | // console.log(results)
99 | })
100 | }
101 | captureFrame(track) {
102 | let imageCapture = new ImageCapture(track)
103 | imageCapture.grabFrame()
104 | .then(imageBitmap => {
105 | const canvas = document.getElementById('myCanvas');
106 | this.drawCanvas(canvas, imageBitmap);
107 | canvas.toBlob((blob)=>{
108 | this.postImageUpdateEmotionData(blob)
109 | }, 'image/jpeg');
110 | })
111 | .catch(error => console.error(error));
112 | }
113 | drawCanvas(canvas, img) {
114 | canvas.width = getComputedStyle(canvas).width.split('px')[0];
115 | canvas.height = getComputedStyle(canvas).height.split('px')[0];
116 | let ratio = Math.min(canvas.width / img.width, canvas.height / img.height);
117 | let x = (canvas.width - img.width * ratio) / 2;
118 | let y = (canvas.height - img.height * ratio) / 2;
119 | canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
120 | canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height,
121 | x, y, img.width * ratio, img.height * ratio);
122 | }
123 | releaseStreamFromVideo() {
124 | this.refs.app.querySelector('video').src = '';
125 | }
126 | downloadVideo(blob) {
127 | let url = URL.createObjectURL(blob);
128 | let a = document.createElement('a');
129 | a.style.display = 'none';
130 | a.href = url;
131 | a.target = '_blank';
132 | document.body.appendChild(a);
133 |
134 | a.click();
135 | }
136 | render() {
137 | const granted = this.state.granted;
138 | const rejectedReason = this.state.rejectedReason;
139 | const recording = this.state.recording;
140 | const paused = this.state.paused;
141 |
142 | return (
143 |
144 |
Video Recorder
145 |
156 |
157 | {/*
Granted: {granted.toString()}
158 |
Rejected Reason: {rejectedReason}
159 |
Recording: {recording.toString()}
160 |
Paused: {paused.toString()}
*/}
161 |
162 | {/*
Live Stream: {'this.state.emotionData'}
*/}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | {/*
171 |
*/}
172 |
173 | } />
174 |
175 | );
176 | }
177 | }
178 |
179 | export default VideoExample;
180 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | app = express(),
3 | logger = require('morgan'),
4 | errorHandler = require('errorhandler'),
5 | debug = require('debug')('server'),
6 | socketio = require('socket.io'),
7 | http = require('http'),
8 | cookieParser = require('cookie-parser'),
9 | bodyParser = require('body-parser'),
10 | utils = require('./app/src/utils'),
11 | consts = require('./consts'),
12 | axios = require('axios'),
13 | BASE_PATH = `${__dirname}/app`,
14 | ENV = process.env.NODE_ENV || 'development',
15 | DEFAULT_PORT = 3001,
16 | SOCKET_PORT = 8000,
17 | EMOTION_FPS = 5; // return the analysed data after n frames
18 |
19 | /* Configuration */
20 | app.set('views', `${BASE_PATH}/views`)
21 | app.engine('html', require('ejs').renderFile);
22 | app.set('view engine', 'html');
23 | app.use(bodyParser.json())
24 | app.use(bodyParser.urlencoded({ extended: false }))
25 | app.use(cookieParser())
26 | app.use('/assets', express.static(`${BASE_PATH}/public`))
27 |
28 | if (ENV === 'development') {
29 | console.log('DEVELOPMENT env')
30 | app.use(errorHandler({dumpExceptions: true, showStack: true}))
31 | app.use(logger('dev'))
32 | } else {
33 | console.log('PRODUCTION env')
34 | app.use(errorHandler())
35 | app.use(logger())
36 | }
37 |
38 | // app.locals = {
39 | // accEmotions: {
40 | // anger: 0,
41 | // contempt: 0,
42 | // disgust: 0,
43 | // fear: 0,
44 | // happiness: 0,
45 | // neutral: 0,
46 | // sadness: 0,
47 | // surprise: 0
48 | // },
49 | // lecturer: {
50 | // overall: {
51 | // emotion: '',
52 | // emotionData: []
53 | // },
54 | // lectures: [
55 | // {
56 | // date: '',
57 | // emotion: '',
58 | // emotionData: [],
59 | // series: [
60 | // {
61 | // time: '',
62 | // emotion: '',
63 | // emotionData: []
64 | // }
65 | // ]
66 | // }
67 | // ]
68 | // }
69 | // }
70 |
71 | app.locals = {
72 | accEmotions: {
73 | anger: 0,
74 | contempt: 0,
75 | disgust: 0,
76 | fear: 0,
77 | happiness: 0,
78 | neutral: 0,
79 | sadness: 0,
80 | surprise: 0
81 | },
82 | lecturer: {
83 | overall: {
84 | emotion: ''
85 | },
86 | lecture: {
87 | accFrames: EMOTION_FPS,
88 | date: '',
89 | emotion: '',
90 | timeline: [],
91 | count: 0,
92 | avgcount: 0
93 | }
94 | }
95 | }
96 |
97 | /**
98 | * Get port from environment and use it for Express.
99 | */
100 | const PORT = utils.normalizePort(process.env.PORT || DEFAULT_PORT)
101 | app.set('port', PORT)
102 |
103 | /**
104 | * Create HTTP server.
105 | */
106 | const server = http.createServer(app)
107 |
108 | /**
109 | * Listen on provided port, on all network interfaces.
110 | */
111 |
112 | server.listen(PORT)
113 |
114 | /**
115 | * Server event handling
116 | */
117 | server.on('error', (err) => {
118 | throw err
119 | })
120 | server.on('listening', (err) => {
121 | let addr = server.address()
122 | let bind = typeof addr === 'string' ?
123 | 'pipe ' + addr :
124 | 'port ' + addr.port
125 | debug('DeepEval is alive on ' + bind)
126 | })
127 |
128 | /**
129 | * Init websockets
130 | */
131 | const io = socketio(server);
132 | io.listen(SOCKET_PORT);
133 | console.log('Listening on SOCKET_PORT ', SOCKET_PORT);
134 |
135 | io.on('connection', (client) => {
136 | client.on('imagePost', (imgData) => {
137 | // console.log('timestamp:', imgData.timestamp)
138 |
139 | axios({
140 | method: 'post',
141 | url: consts.endpoints.faceAPI,
142 | data: imgData.uri,
143 | headers: {
144 | 'Content-Type': 'application/octet-stream',
145 | 'Ocp-Apim-Subscription-Key': process.env.SUB_KEY
146 | },
147 | params: {
148 | 'returnFaceId': 'true',
149 | 'returnFaceLandmarks': 'false',
150 | 'returnFaceAttributes': 'headPose,emotion,blur,exposure,noise',
151 | },
152 | })
153 | .then(function (response) {
154 | let faceData = response.data
155 | if (faceData.length <= 0) {
156 | // console.log(faceData.length)
157 | return;
158 | }
159 | // Only include attentive faces
160 | // let attentiveFaces = faceData.filter((item) => {
161 | // return parseInt(item['headPose']['pitch']) == 0;
162 | // });
163 | let attentiveFaces = faceData
164 | app.locals.lecturer.lecture.count++;
165 |
166 | // console.log(attentiveFaces)
167 | // console.log(app.locals.lecturer.lecture.count)
168 | let maxEmotionMapping = {
169 | anger: 1,
170 | sadness: 2,
171 | disgust: 3,
172 | fear: 4,
173 | neutral: 5,
174 | contempt: 6,
175 | surprise: 7,
176 | happiness: 8,
177 | }
178 | let maximumEmotion = 0
179 | let sum = {
180 | anger: 0,
181 | contempt: 0,
182 | disgust: 0,
183 | fear: 0,
184 | happiness: 0,
185 | neutral: 0,
186 | sadness: 0,
187 | surprise: 0,
188 | }
189 | let max = 0
190 |
191 | function avg(count, x1, x2) {
192 | return ((count-1)*x1+x2)/2
193 | }
194 |
195 | attentiveFaces.forEach(elem => {
196 | for (let prop in elem['faceAttributes']['emotion']) {
197 | app.locals.accEmotions[prop] = avg(app.locals.lecturer.lecture.count, app.locals.accEmotions[prop], elem['faceAttributes']['emotion'][prop])
198 | // console.log(prop)
199 | sum[prop] = sum[prop] + elem['faceAttributes']['emotion'][prop]
200 | if(elem['faceAttributes']['emotion'][prop] > max) {
201 | max = elem['faceAttributes']['emotion'][prop]
202 | maximumEmotion = maxEmotionMapping[prop]
203 | }
204 | }
205 | });
206 | console.log(require('util').inspect(plotData, { depth: null }));
207 |
208 |
209 | if (--app.locals.lecturer.lecture.accFrames < 0) {
210 | let plotData = {
211 | emotion: maximumEmotion,
212 | timestamp: imgData.timestamp
213 | }
214 |
215 | app.locals.lecturer.lecture.timeline.append(plotData)
216 | app.locals.lecturer.lecture.avgcount++;
217 | app.locals.lecturer.lecture.emotion = app.locals.lecturer.overall.emotion = avg(app.locals.lecturer.lecture.avgcount, app.locals.lecturer.lecture.emotion, maximumEmotion)
218 |
219 |
220 |
221 | // Reset the accumlated frames
222 | app.locals.lecturer.lecture.accFrames = EMOTION_FPS
223 |
224 | // send back off to client
225 | client.emit('results', plotData)
226 | }
227 |
228 | })
229 | .catch(function (error) {
230 | // console.error(error);
231 | });
232 | });
233 | });
234 |
235 |
236 | // const websocket = socketio(server)
237 |
238 | app.get('/', function (req, res) {
239 | res.render('index')
240 | })
241 |
242 | module.exports = app
243 |
--------------------------------------------------------------------------------