├── .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 |
12 |

Hello:

13 | 24 |
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 | --------------------------------------------------------------------------------