diff --git a/.gitignore b/.gitignore index 1b2ac94..710e1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ venv .project .pydevproject venv3 +node_modules +.sass-cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3dc234 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Hugvey / Pillow Talk + +## Server + +Run the server: `python hugvey_server.py --config server_config.yml` + +### Panopticon + +The server also integrates the _panopticon_, the monitoring & administration interface to Hugvey. + + +## Client + +To run it: `python hugvey_client.py -c client_config.yml` + +## Development + +The Panopticon uses gulp to compile SASS into CSS, and to set up browser-sync for css & js. For now, no js user facing dependencies are managed trough node/npm. \ No newline at end of file diff --git a/hugvey/central_command.py b/hugvey/central_command.py index 90555ae..11b607b 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -18,6 +18,7 @@ from hugvey.voice.google import GoogleVoiceClient from hugvey.voice.player import Player from hugvey.voice.streamer import AudioStreamer import queue +import os logger = logging.getLogger("command") @@ -67,7 +68,8 @@ class CentralCommand(object): self.languages = {} for lang in self.config['languages']: - with open(lang['file'], 'r') as fp: + lang_filename = os.path.join(self.config['web']['files_dir'], lang['file']) + with open(lang_filename, 'r') as fp: self.languages[lang['code']] = yaml.load(fp) self.panopticon = Panopticon(self, self.config) diff --git a/hugvey/panopticon.py b/hugvey/panopticon.py index adf8b23..31ed946 100644 --- a/hugvey/panopticon.py +++ b/hugvey/panopticon.py @@ -12,6 +12,7 @@ import os from pytz.reference import Central import asyncio import json +from urllib.parse import urlparse logger = logging.getLogger("panopticon") @@ -21,7 +22,14 @@ print(web_dir) def getWebSocketHandler(central_command): class WebSocketHandler(tornado.websocket.WebSocketHandler): + CORS_ORIGINS = ['localhost'] connections = set() + + def check_origin(self, origin): + parsed_origin = urlparse(origin) + # parsed_origin.netloc.lower() gives localhost:3333 + valid = parsed_origin.hostname in self.CORS_ORIGINS + return valid # the client connected def open(self): @@ -30,6 +38,7 @@ def getWebSocketHandler(central_command): # the client sent the message def on_message(self, message): + logger.debug(f"recieve: {message}") try: msg = json.loads(message) if msg['action'] == 'init': @@ -48,6 +57,7 @@ def getWebSocketHandler(central_command): def send(self, message): j = json.dumps(message) + print(self.connections) [con.write_message(j) for con in self.connections] # client disconnected @@ -86,7 +96,7 @@ class Panopticon(object): self.config = config self.application = tornado.web.Application([ (r"/ws", getWebSocketHandler(self.command)), - (r"/uploads/(.*)", tornado.web.StaticFileHandler, + (r"/local/(.*)", tornado.web.StaticFileHandler, {"path": config['web']['files_dir']}), (r"/(.*)", tornado.web.StaticFileHandler, {"path": web_dir, "default_filename": 'index.html'}), @@ -100,7 +110,7 @@ class Panopticon(object): asyncio.set_event_loop(evt_loop) self.loop = tornado.ioloop.IOLoop.current() - logger.info(f"Start Panopticon on port {self.config['web']['port']}") + logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}") self.loop.start() def stop(self): diff --git a/hugvey/story.py b/hugvey/story.py index a5dee2f..6f07d75 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -421,7 +421,6 @@ class Story(object): async def start(self): logger.info("Starting story") self.timer.reset() -# self.startTime = time.time() self.isRunning = True self.setCurrentMessage(self.startMessage) await self._renderer() diff --git a/story_de.json b/local/story_de.json similarity index 100% rename from story_de.json rename to local/story_de.json diff --git a/story_en.json b/local/story_en.json similarity index 100% rename from story_en.json rename to local/story_en.json diff --git a/story_fr.json b/local/story_fr.json similarity index 100% rename from story_fr.json rename to local/story_fr.json diff --git a/server_config.yml b/server_config.yml index b8cc1d0..ff68099 100644 --- a/server_config.yml +++ b/server_config.yml @@ -6,7 +6,7 @@ voice: out_rate: 44100 port: 4444 chunk: 2972 - google_credentials: "/home/ruben/Documents/Projecten/2018/Hugvey/test_googlespeech/My First Project-0c7833e0d5fa.json" + google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json" hugveys: 25 languages: - code: en-GB @@ -17,4 +17,4 @@ languages: file: story_fr.json web: port: 8888 - files_dir: "/home/ruben/Documents/Projecten/2018/Hugvey/hugvey/local/" \ No newline at end of file + files_dir: "local/" \ No newline at end of file diff --git a/www/css/styles.css b/www/css/styles.css new file mode 100644 index 0000000..97caf6c --- /dev/null +++ b/www/css/styles.css @@ -0,0 +1,58 @@ +body { + font-family: sans-serif; + margin: 0; } + +.btn { + display: inline-block; + cursor: pointer; + background: #333; + padding: 5px; + color: white; + border-radius: 5px; } + +#interface { + display: flex; + flex-direction: row; + height: 100vh; + width: 100vw; } + +#status { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 430px; + height: 100%; + overflow-y: scroll; } + #status > div { + width: 33.3333333%; + height: 150px; + border: solid 1px; + box-sizing: border-box; + position: relative; } + #status .hugvey { + background-image: linear-gradient(to top, #587457, #35a589); + color: white; + display: flex; + flex-direction: column; + justify-content: center; } + #status .hugvey h1 { + text-align: center; + margin: 0; } + #status .hugvey.hugvey--on h1 { + position: absolute; + left: 5px; + top: 5px; } + #status .hugvey.hugvey--off { + background-image: linear-gradient(to top, #575d74, #3572a5); } + #status .hugvey.hugvey--off h1::after { + content: '[off]'; } + +#story { + position: relative; } + #story #controls { + position: absolute; + top: 5px; + left: 5px; } + #story svg#graph { + width: 100%; + height: 100%; } diff --git a/www/css/styles.css.map b/www/css/styles.css.map new file mode 100644 index 0000000..09951c6 --- /dev/null +++ b/www/css/styles.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;AAED;EACC;EACG;EACA;EACA;EACA;;;AAGJ;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;;;AAKF;EACC;EACA;;;AAED;EACC;;;AAED;EACI;EACA;EACA","file":"styles.css"} \ No newline at end of file diff --git a/www/gulpfile.js b/www/gulpfile.js new file mode 100644 index 0000000..0019f1c --- /dev/null +++ b/www/gulpfile.js @@ -0,0 +1,82 @@ +var gulp = require('gulp'); +var sass = require('gulp-sass'); +//var babel = require('gulp-babel'); +//var concat = require('gulp-concat'); +//var rename = require('gulp-rename'); +//var uglify = require('gulp-uglify'); +//var rollup = require('rollup-stream'); +//var sourcemaps = require('gulp-sourcemaps'); +var browserSync = require('browser-sync'); + + +var through = require('through2') +// todo: rollup for d3 & possibly jsonld + +var paths = { + "styles": { + "src": "./scss/*.scss", + "dest": "./css/" + } +}; + +gulp.task('styles', function() { + gulp.src(paths.styles.src, { sourcemaps: true }) + .pipe(sass().on('error', sass.logError)) + .pipe(gulp.dest(paths.styles.dest)) + .pipe(browserSync.reload({ stream: true })); +}); + + +/*gulp.task('scripts', function() { + return gulp.src(paths.scripts.src) + .pipe(sourcemaps.init()) + .pipe(babel({ + ignore: [ + './src/d3.v5.js', + './src/vue.js', + ] + })) + .pipe(concat('portfolio.js')) + .pipe(gulp.dest(paths.scripts.dest)) // save .js + .pipe(uglify()) + .pipe(rename({ extname: '.min.js' })) + // .pipe(sourcemaps.write('maps')) + .pipe(gulp.dest(paths.scripts.d3destDir)) // save .min.js +}); + +gulp.task('d3', function() { + return rollup( 'rollup.config.js' ) + .pipe(source('d3.bundle.js')) + .pipe(gulp.dest(paths.scripts.d3destDir)) // save .js + .pipe(buffer()) + .pipe(uglify()) + .pipe(rename({ extname: '.min.js' })) + .pipe(sourcemaps.write('maps')) + .pipe(gulp.dest(paths.scripts.d3destDir)) // save .min.js +}); +*/ + +var watchStylesAndScripts = function() { + gulp.watch(paths.styles.src,['styles']); +// gulp.watch(paths.scripts.src,['scripts', browserSync.reload]); +// gulp.watch(paths.scripts.d3src,['d3', browserSync.reload]); +} + +gulp.task('watch', watchStylesAndScripts); + +// watch files for changes and reload +gulp.task('serve', function() { + browserSync.init({ + proxy: { + target: "localhost:8888", + ws: true + }, + port: 3000 + }); + + gulp.watch(['index.html', 'js/hugvey_console.js'], browserSync.reload); + watchStylesAndScripts(); +}); + + +gulp.task('default', ['serve']); diff --git a/www/hugvey_console.js b/www/hugvey_console.js deleted file mode 100644 index 436e11c..0000000 --- a/www/hugvey_console.js +++ /dev/null @@ -1,100 +0,0 @@ -var panopticon; - -class Panopticon { - constructor() { - console.log( "Init panopticon" ); - this.hugveys = new Vue( { - el: "#status", - data: { - uptime: 0, - languages: [], - hugveys: [] - }, - methods: { - time_passed: function (hugvey, property) { - console.log("property!", Date(hugvey[property] * 1000)); - return moment(Date(hugvey[property] * 1000)).fromNow(); - } - } - } ); - - - this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } ); - - - this.socket.addEventListener( 'open', ( e ) => { - this.send( { action: 'init' } ); - } ); - - this.socket.addEventListener( 'close', function( e ) { - console.log( 'Closed connection' ); - } ); - this.socket.addEventListener( 'message', ( e ) => { - let msg = JSON.parse( e.data ); - if ( typeof msg['alert'] !== 'undefined' ) { - alert(msg['alert']); - } - - if ( typeof msg['action'] === 'undefined' ) { - console.error( "not a valid message: " + e.data ); - return; - } - - switch ( msg['action'] ) { - - case 'status': - this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']); - this.hugveys.languages = msg['languages']; - this.hugveys.hugveys = msg['hugveys']; - break; - } - } ); - } - - send( msg ) { - this.socket.send( JSON.stringify( msg ) ); - } - - getStatus() { -// console.log('get status', this, panopticon); - panopticon.send( { action: 'get_status' } ); - } - - init() { - setInterval( this.getStatus, 3000 ); - } - - stringToHHMMSS (string) { - var sec_num = parseInt(string, 10); // don't forget the second param - var hours = Math.floor(sec_num / 3600); - var minutes = Math.floor((sec_num - (hours * 3600)) / 60); - var seconds = sec_num - (hours * 3600) - (minutes * 60); - - if (hours < 10) {hours = "0"+hours;} - if (minutes < 10) {minutes = "0"+minutes;} - if (seconds < 10) {seconds = "0"+seconds;} - return hours+':'+minutes+':'+seconds; - } - - - loadNarrative(code, file) { - - } - - resume(hv_id) { - this.send({ action: 'resume', hugvey: hv_id }) - } - pause(hv_id) { - this.send({ action: 'play', hugvey: hv_id }) - } - restart(hv_id) { - this.send({ action: 'restart', hugvey: hv_id }) - } -} - - - -window.addEventListener( 'load', function() { - panopticon = new Panopticon(); - panopticon.init(); -}) \ No newline at end of file diff --git a/www/index.html b/www/index.html index 0734a71..c0b6c33 100644 --- a/www/index.html +++ b/www/index.html @@ -2,42 +2,63 @@