diff --git a/hugvey/panopticon.py b/hugvey/panopticon.py index 4ef79be..c904e36 100644 --- a/hugvey/panopticon.py +++ b/hugvey/panopticon.py @@ -30,7 +30,7 @@ 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 @@ -79,7 +79,7 @@ def getWebSocketHandler(central_command): logger.exception(e) def send(self, message): - # Possible useless method: use self.write_message() + # Possible useless method: use self.write_message() j = json.dumps(message) self.write_message(j) @@ -100,40 +100,40 @@ def getWebSocketHandler(central_command): def msgInit(self): msg = self.getStatusMsg() self.send(msg) - + def msgBlock(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'block'}) - + def msgUnblock(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'unblock'}) - + def msgResume(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'resume'}) - + def msgPause(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'pause'}) - + def msgRestart(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'restart'}) - + def msgFinish(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'finish'}) - + def msgChangeLanguage(self, hv_id, lang_code): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_language', 'lang_code': lang_code}) - + def msgChangeLightId(self, hv_id, lightId): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_light', 'light_id': lightId}) - + def msgPlayMsg(self, hv_id, msg_id, reloadStory): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id, 'reloadStory':bool(reloadStory)}) - + @classmethod def write_to_clients(wsHandlerClass, msg): if msg is None: logger.critical("Tried to send 'none' to Panopticon") return - + for client in wsHandlerClass.connections: client.write_message(msg) @@ -143,7 +143,7 @@ class NonCachingStaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): # Disable cache self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - + def getUploadHandler(central_command): class UploadHandler(tornado.web.RequestHandler): def set_default_headers(self): @@ -160,12 +160,12 @@ def getUploadHandler(central_command): logger.info('upload') langCode = self.get_argument("language") langFile = os.path.join(central_command.config['web']['files_dir'] , central_command.languageFiles[langCode]) - + storyData = json.loads(self.request.files['json'][0]['body']) # print(json.dumps(storyData)) # self.finish() # return - + if 'audio' in self.request.files: msgId = self.get_argument("message_id") audioFile = self.request.files['audio'][0] @@ -179,7 +179,7 @@ def getUploadHandler(central_command): if 'audio' in storyData[i] and storyData[i]['audio'] is not None and os.path.exists(storyData[i]['audio']['file']): logger.info(f"Remove previous file {storyData[i]['audio']['file']} ({storyData[i]['audio']['original_name']})") os.unlink(storyData[i]['audio']['file']) - + storyData[i]['audio'] = { 'file': audioFilename, 'original_name': original_fname @@ -188,17 +188,17 @@ def getUploadHandler(central_command): logger.info(f'Save {original_fname} to {audioFilename}') fp.write(audioFile['body']) break - + # logger.info(os.path.abspath(langFile)) langFile = os.path.abspath(langFile) with open(langFile, 'w') as json_fp: logger.info(f'Save story to {langFile} {json_fp}') json.dump(storyData, json_fp, indent=2) - + # Reload language files for new instances central_command.loadLanguages() self.finish() - + return UploadHandler def getVoiceHandler(voiceStorage): @@ -212,7 +212,7 @@ def getVoiceHandler(voiceStorage): fn = await voiceStorage.requestFile(lang_code, text, isVariable) if not fn: raise Exception(f"No Filename for text: {text}") - + if int(self.get_argument('filename')) == 1: self.set_header("Content-Type","text/plain") self.write(fn) @@ -221,25 +221,30 @@ def getVoiceHandler(voiceStorage): with open(fn, 'rb') as fp: self.write(fp.read()) self.finish() - return VoiceHandler + return VoiceHandler +class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler): + def set_extra_headers(self, path): + """For subclass to add extra headers to the response""" + if path[-5:] == '.html': + self.set_header("Access-Control-Allow-Origin", "*") class Panopticon(object): def __init__(self, central_command, config, voiceStorage): self.command = central_command self.config = config - + self.voiceStorage = voiceStorage - + self.wsHandler = getWebSocketHandler(self.command) - + self.application = tornado.web.Application([ (r"/ws(.*)", self.wsHandler), (r"/local/(.*)", NonCachingStaticFileHandler, {"path": config['web']['files_dir']}), (r"/upload", getUploadHandler(self.command)), (r"/voice", getVoiceHandler(self.voiceStorage)), - (r"/(.*)", tornado.web.StaticFileHandler, + (r"/(.*)", StaticFileWithHeaderHandler, {"path": web_dir, "default_filename": 'index.html'}), ], debug=True) @@ -251,17 +256,17 @@ class Panopticon(object): asyncio.set_event_loop(evt_loop) self.loop = tornado.ioloop.IOLoop.current() - + thread = threading.Thread( target=self.broadcastLoggingQueueToWs, kwargs={'wsHandler': self.wsHandler, 'q': self.command.logQueue}, name=f"panopticon/logws") thread.start() - + logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}") self.loop.start() def stop(self): self.loop.stop() - + def broadcastLoggingQueueToWs(self, wsHandler, q: Queue): while True: record = q.get() @@ -278,4 +283,3 @@ class Panopticon(object): j = json.dumps(msg) logger.debug(j) self.loop.add_callback(wsHandler.write_to_clients, j) - \ No newline at end of file diff --git a/hugvey/story.py b/hugvey/story.py index d90eaa5..689a27e 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -40,8 +40,8 @@ class Utterance(object): def isFinished(self): return self.endTime is not None - - + + def __getstate__(self): # print(f'get utterance {self}') state = self.__dict__.copy() @@ -68,7 +68,7 @@ class Message(object): self.parseForVariables() self.uuid = None # Have a unique id each time the message is played back. self.color = None - + def __getstate__(self): # Copy the object's state from self.__dict__ which contains # all our instance attributes. Always use the dict.copy() @@ -78,7 +78,7 @@ class Message(object): # Remove the unpicklable entries. del state['filenameFetchLock'] return state - + def __setstate__(self, state): self.__dict__.update(state) self.filenameFetchLock = asyncio.Lock() @@ -103,9 +103,9 @@ class Message(object): if not 'vol' in msg.params: # prevent clipping on some Lyrebird tracks msg.params['vol'] = .8 - + msg.params['vol'] = float(msg.params['vol']) - + return msg def parseForVariables(self): @@ -215,8 +215,8 @@ class Reply(object): self.forMessage = None self.utterances = [] self.setForMessage(message) - - + + def __getstate__(self): # print(f'get reply {self}') state = self.__dict__.copy() @@ -297,8 +297,8 @@ class Condition(object): self.logInfo = None self.originalJsonString = None self.usedContainsDuration = None - - + + def __getstate__(self): # print(f'get condition {self.id}') state = self.__dict__.copy() @@ -500,8 +500,8 @@ class Direction(object): self.conditions = [] self.conditionMet = None self.isDiversionReturn = False - - + + def __getstate__(self): # print(f'get direction {self.id}') state = self.__dict__.copy() @@ -568,8 +568,8 @@ class Diversion(object): if not self.method: raise Exception("No valid type given for diversion") - - + + def __getstate__(self): # print(f'get diversion {self.id}') state = self.__dict__.copy() @@ -628,7 +628,7 @@ class Diversion(object): }] """ self.counter +=1 - + # story.logger.warn(f"CREATING DIRECTIONS FOR {startMsg.id}") finishMessageIds = story.getFinishesForMsg(startMsg) finalTimeoutDuration = timeoutDuration finalContainsDurations = replyContainsDurations @@ -647,6 +647,7 @@ class Diversion(object): finalContainsDurations = json.loads(condition.originalJsonString)['vars']['delays'] i = 0 + # story.logger.warn(f"FINISHES: {finishMessageIds}") for msgId in finishMessageIds: # Some very ugly hack to add a direction & condition i+=1 @@ -693,6 +694,7 @@ class Diversion(object): story.logger.info(f"Created direction: {direction.id} {condition.id} with timeout {finalTimeoutDuration}s") story.add(condition) story.add(direction) + # story.logger.warn(f"ADDED DIRECTION {direction.id}") @@ -944,7 +946,7 @@ class Configuration(object): id = 'configuration' volume = 1 # Volume multiplier for 'play' command nothing_text = "nothing" # When variable is not set, but used in sentence, replace it with this word. - + @classmethod def initFromJson(configClass, data, story): config = Configuration() @@ -1007,22 +1009,22 @@ class Stopwatch(object): def clearMark(self, name): if name in self.marks: self.marks.pop(name) - + def __getstate__(self): # print(f'get stopwatch') state = self.__dict__.copy() state['isRunning'] = self.isRunning.is_set() return state - + def __setstate__(self, state): self.__dict__.update(state) - + self.isRunning = asyncio.Event() if 'isRunning' in state and state['isRunning']: self.isRunning.set() else: self.isRunning.clear() - + class StoryState(object): """ @@ -1065,7 +1067,7 @@ class StoryState(object): def __init__(self): pass -# +# class Story(object): """Story represents and manages a story/narrative flow""" @@ -1485,7 +1487,7 @@ class Story(object): # TODO create timer event # self.commands.append({'msg':'TEST!'}) - + # Test stability of Central Command with deliberate crash # if self.timer.getElapsed() > 10: # raise Exception("Test exception") @@ -1553,10 +1555,10 @@ class Story(object): self.logger.critical(f"error: crash when reading wave file: {fn}") self.logger.exception(e) duration = 10 # some default duration to have something to fall back to - + params = message.getParams().copy() params['vol'] = params['vol'] * self.configuration.volume if 'vol' in params else self.configuration.volume - + # self.hugvey.google.pause() # pause STT to avoid text events while decision is made self.hugvey.sendCommand({ 'action': 'play', @@ -1621,7 +1623,7 @@ class Story(object): self.isRunning = True if not self.lastMsgFinishTime and self.currentMessage: await self.setCurrentMessage(self.currentMessage) - + await self._renderer() def isFinished(self): @@ -1649,16 +1651,16 @@ class Story(object): self.timer.pause() def calculateFinishesForMsg(self, msgId, depth = 0, checked = []): - if msgId in checked: - return [] - - checked.append(msgId) - + # if msgId in checked: + # return [] + # + # checked.append(msgId) + if not msgId in self.directionsPerMsg or len(self.directionsPerMsg[msgId]) < 1: # is finish return [msgId] - if depth > 200: + if depth > 100: return [] finishes = [] @@ -1691,6 +1693,7 @@ class Story(object): returns message ids """ + print(msg.id, self.strands) if msg.id in self.strands: return self.strands[msg.id] @@ -1710,22 +1713,22 @@ class Story(object): # TODO: should the direction have at least a timeout condition set, or not perse? return self.directionsPerMsg[msg.id][0] - + @classmethod def getStateDir(self): return "/tmp" # day = time.strftime("%Y%m%d") # t = time.strftime("%H:%M:%S") -# +# # self.out_folder = os.path.join(self.main_folder, day, f"{self.hv_id}", t) # if not os.path.exists(self.out_folder): # self.logger.debug(f"Create directory {self.out_folder}") # self.target_folder = os.makedirs(self.out_folder, exist_ok=True) - - @classmethod + + @classmethod def getStateFilename(cls, hv_id): return os.path.join(cls.getStateDir(), f"hugvey{hv_id}") - + def storeState(self): # TODO: stop stopwatch fn = self.getStateFilename(self.hugvey.id) @@ -1735,49 +1738,48 @@ class Story(object): pickle.dump(self, fp) # write atomic to disk: flush, close, rename fp.flush() - os.fsync(fp.fileno()) - + os.fsync(fp.fileno()) + os.rename(tmpfn, fn) self.logger.debug(f"saved state to {fn}") - + def hasSavedState(self): return self.hugveyHasSavedState(self.hugvey.id) - + @classmethod def hugveyHasSavedState(cls, hv_id): return os.path.exists(cls.getStateFilename(hv_id)) - + @classmethod def loadStoryFromState(cls, hugvey_state): # restart stopwatch with open(cls.getStateFilename(hugvey_state.id), 'rb') as fp: story = pickle.load(fp) - + story.hugvey = hugvey_state story.logger = mainLogger.getChild(f"{story.hugvey.id}").getChild("story") return story # TODO: take running state etc. - + @classmethod def clearSavedState(cls, hv_id): fn = cls.getStateFilename(hv_id) if os.path.exists(fn): os.unlink(fn) mainLogger.info(f"Removed state: {fn}") -# +# def __getstate__(self): # Copy the object's state from self.__dict__ which contains # all our instance attributes. Always use the dict.copy() # method to avoid modifying the original state. state = self.__dict__.copy() - + # Remove the unpicklable entries. del state['hugvey'] del state['logger'] # del state['isRunning'] - + return state - + def __setstate__(self, state): self.__dict__.update(state) - diff --git a/install_server.sh b/install_server.sh index 65fb11c..5843f2f 100755 --- a/install_server.sh +++ b/install_server.sh @@ -15,6 +15,16 @@ echo "blacklist snd_bcm2835" > /etc/modprobe.d/internalsnd-blacklist.conf echo "d /var/log/supervisor 0777 root root" > /etc/tmpfiles.d/supervisor.conf cp installation/fstab /etc/fstab +# setup pulseaudio as system daemon and grant access: https://rudd-o.com/linux-and-free-software/how-to-make-pulseaudio-run-once-at-boot-for-all-your-users +cp installation/pulseaudio.service /etc/systemd/system/pulseaudio.service +# disable autospawn +cp installation/pulseaudio-client.conf /etc/pulse/client.conf +# put tsched to 0: https://wiki.archlinux.org/index.php/PulseAudio/Troubleshooting#Sound_stuttering_when_streaming_over_network +cp installation/pulse-default.pa /etc/pulse/default.pa +systemctl --system enable pulseaudio.service +systemctl --system start pulseaudio.service +usermod -a -G pulse-access pi + # Added chown=pi:pi cp installation/supervisord.conf /etc/supervisor/supervisord.conf ln -s /home/pi/hugvey/supervisor.conf /etc/supervisor/conf.d/hugvey.conf diff --git a/installation/pulse-default.pa b/installation/pulse-default.pa new file mode 100644 index 0000000..9f1ba49 --- /dev/null +++ b/installation/pulse-default.pa @@ -0,0 +1,141 @@ +#!/usr/bin/pulseaudio -nF +# +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +# This startup script is used only if PulseAudio is started per-user +# (i.e. not in system mode) + +.fail + +### Automatically restore the volume of streams and devices +load-module module-device-restore +load-module module-stream-restore +load-module module-card-restore + +### Automatically augment property information from .desktop files +### stored in /usr/share/application +load-module module-augment-properties + +### Should be after module-*-restore but before module-*-detect +load-module module-switch-on-port-available + +### Load audio drivers statically +### (it's probably better to not load these drivers manually, but instead +### use module-udev-detect -- see below -- for doing this automatically) +#load-module module-alsa-sink +#load-module module-alsa-source device=hw:1,0 +#load-module module-oss device="/dev/dsp" sink_name=output source_name=input +#load-module module-oss-mmap device="/dev/dsp" sink_name=output source_name=input +#load-module module-null-sink +#load-module module-pipe-sink + +### Automatically load driver modules depending on the hardware available +.ifexists module-udev-detect.so +# HUGVEY: put tsched to 0: https://wiki.archlinux.org/index.php/PulseAudio/Troubleshooting#Sound_stuttering_when_streaming_over_network +load-module module-udev-detect tsched=0 +.else +### Use the static hardware detection module (for systems that lack udev support) +load-module module-detect +.endif + +### Automatically connect sink and source if JACK server is present +.ifexists module-jackdbus-detect.so +.nofail +load-module module-jackdbus-detect channels=2 +.fail +.endif + +### Automatically load driver modules for Bluetooth hardware +.ifexists module-bluetooth-policy.so +load-module module-bluetooth-policy +.endif + +.ifexists module-bluetooth-discover.so +load-module module-bluetooth-discover +.endif + +### Load several protocols +.ifexists module-esound-protocol-unix.so +load-module module-esound-protocol-unix +.endif +load-module module-native-protocol-unix + +### Network access (may be configured with paprefs, so leave this commented +### here if you plan to use paprefs) +#load-module module-esound-protocol-tcp +#load-module module-native-protocol-tcp +#load-module module-zeroconf-publish + +### Load the RTP receiver module (also configured via paprefs, see above) +#load-module module-rtp-recv + +### Load the RTP sender module (also configured via paprefs, see above) +#load-module module-null-sink sink_name=rtp format=s16be channels=2 rate=44100 sink_properties="device.description='RTP Multicast Sink'" +#load-module module-rtp-send source=rtp.monitor + +### Load additional modules from GConf settings. This can be configured with the paprefs tool. +### Please keep in mind that the modules configured by paprefs might conflict with manually +### loaded modules. +.ifexists module-gconf.so +.nofail +load-module module-gconf +.fail +.endif + +### Automatically restore the default sink/source when changed by the user +### during runtime +### NOTE: This should be loaded as early as possible so that subsequent modules +### that look up the default sink/source get the right value +load-module module-default-device-restore + +### Automatically move streams to the default sink if the sink they are +### connected to dies, similar for sources +load-module module-rescue-streams + +### Make sure we always have a sink around, even if it is a null sink. +load-module module-always-sink + +### Honour intended role device property +load-module module-intended-roles + +### Automatically suspend sinks/sources that become idle for too long +load-module module-suspend-on-idle + +### If autoexit on idle is enabled we want to make sure we only quit +### when no local session needs us anymore. +.ifexists module-console-kit.so +load-module module-console-kit +.endif +.ifexists module-systemd-login.so +load-module module-systemd-login +.endif + +### Enable positioned event sounds +load-module module-position-event-sounds + +### Cork music/video streams when a phone stream is active +load-module module-role-cork + +### Modules to allow autoloading of filters (such as echo cancellation) +### on demand. module-filter-heuristics tries to determine what filters +### make sense, and module-filter-apply does the heavy-lifting of +### loading modules and rerouting streams. +load-module module-filter-heuristics +load-module module-filter-apply + +### Make some devices default +#set-default-sink output +#set-default-source input diff --git a/installation/pulseaudio-client.conf b/installation/pulseaudio-client.conf new file mode 100644 index 0000000..9de638a --- /dev/null +++ b/installation/pulseaudio-client.conf @@ -0,0 +1,18 @@ +# disable autospawn because we're in system wide mode + +; default-sink = +; default-source = +; default-server = +; default-dbus-server = + +autospawn = no +; daemon-binary = /usr/bin/pulseaudio +; extra-arguments = --log-target=syslog + +; cookie-file = + +; enable-shm = yes +; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB + +; auto-connect-localhost = no +; auto-connect-display = no diff --git a/installation/pulseaudio.service b/installation/pulseaudio.service new file mode 100644 index 0000000..7542c17 --- /dev/null +++ b/installation/pulseaudio.service @@ -0,0 +1,9 @@ +[Unit] +Description=PulseAudio system server + +[Service] +Type=notify +ExecStart=/usr/bin/pulseaudio --daemonize=no --system --realtime --log-target=journal + +[Install] +WantedBy=multi-user.target diff --git a/pd/loopaudio.pd b/pd/loopaudio.pd index 34732d3..226d149 100644 --- a/pd/loopaudio.pd +++ b/pd/loopaudio.pd @@ -1,51 +1,52 @@ -#N canvas 200 136 660 592 10; +#N canvas 976 211 660 592 10; #X obj 131 277 dac~; #X obj 131 227 readsf~; #X obj 209 209 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 -1; #X text 229 206 (re-)start loop; -#X msg 132 170 open /home/a/projects/pd-play/testaudio.wav \, 1; #X obj 208 459 netsend -u -b; -#X obj 208 481 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 +#X obj 208 481 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1 1; #X msg 355 464 disconnect; #X obj 208 393 list prepend send; #X obj 208 415 list trim; -#X msg 357 434 connect localhost 5555; -#X obj 208 345 oscformat /trigger; #X msg 51 193 0; #X obj 227 83 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 -1; #X text 40 173 stop loop; -#X msg 210 292 1; +#X msg 208 320 1; #X obj 318 84 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 -1; #X text 217 60 START; -#X obj 436 84 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 +#X obj 436 84 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1 1; #X text 309 61 !STOP!; #X text 422 60 Playing indicator; #X msg 261 118 1; #X msg 330 117 0; +#X obj 208 345 oscformat /loop; +#X msg 356 434 connect 192.168.1.174 7400; +#X msg 132 170 open /mnt/stash/hugvey/score38_loop_40s_extra.wav \, +1; #X connect 1 0 0 0; #X connect 1 0 0 1; #X connect 1 1 2 0; -#X connect 2 0 4 0; -#X connect 2 0 15 0; -#X connect 4 0 1 0; -#X connect 5 0 6 0; -#X connect 7 0 5 0; -#X connect 8 0 9 0; -#X connect 9 0 5 0; -#X connect 10 0 5 0; -#X connect 11 0 8 0; -#X connect 12 0 1 0; -#X connect 13 0 10 0; -#X connect 13 0 2 0; -#X connect 13 0 21 0; -#X connect 15 0 11 0; -#X connect 16 0 12 0; -#X connect 16 0 7 0; -#X connect 16 0 22 0; -#X connect 21 0 18 0; -#X connect 22 0 18 0; +#X connect 2 0 22 0; +#X connect 2 0 12 0; +#X connect 4 0 5 0; +#X connect 6 0 4 0; +#X connect 7 0 8 0; +#X connect 8 0 4 0; +#X connect 9 0 1 0; +#X connect 10 0 21 0; +#X connect 10 0 2 0; +#X connect 10 0 18 0; +#X connect 12 0 20 0; +#X connect 13 0 9 0; +#X connect 13 0 6 0; +#X connect 13 0 19 0; +#X connect 18 0 15 0; +#X connect 19 0 15 0; +#X connect 20 0 7 0; +#X connect 21 0 4 0; +#X connect 22 0 1 0; diff --git a/www/js/hugvey_console.js b/www/js/hugvey_console.js index cbc888a..edeafa1 100644 --- a/www/js/hugvey_console.js +++ b/www/js/hugvey_console.js @@ -28,7 +28,7 @@ class Panopticon { }, loadNarrative: function( code, file ) { panopticon.hugveys.selectedId = null; - + if(panopticon.hasGraph) { return panopticon.loadNarrative( code, file ); } @@ -79,16 +79,16 @@ class Panopticon { this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } ); - + if(this.hasGraph) { this.graph = new Graph(); } - - + + this.socket.addEventListener( 'open', ( e ) => { this.send( { action: 'init' } ); } ); - + // request close before unloading window.addEventListener('beforeunload', function(){ panopticon.socket.close(); @@ -102,7 +102,7 @@ class Panopticon { console.log("Websocket connected") return; } - + let msg = JSON.parse( e.data ); if ( typeof msg['alert'] !== 'undefined' ) { alert( msg['alert'] ); @@ -112,9 +112,9 @@ class Panopticon { console.error( "not a valid message: " + e.data ); return; } - + console.debug(msg); - + switch ( msg['action'] ) { case 'status': @@ -134,28 +134,28 @@ class Panopticon { } } ); } - + updateSelectedHugvey() { let hv = null; - + if(this.hugveys.selectedId) { hv = this.getHugvey(this.hugveys.selectedId); - + if(this.hasGraph) { if(hv.language && this.graph.language_code != hv.language) { this.loadNarrative(hv.language); } } - + // let varEl = document.getElementById("variables"); // varEl.innerHTML = ""; } - + if(this.hasGraph) { this.graph.updateHugveyStatus(hv); } } - + getHugvey(id) { for(let hv of this.hugveys.hugveys) { if(hv.id == id) { @@ -206,7 +206,7 @@ class Panopticon { let graph = this.graph; req.addEventListener( "load", function( e ) { graph.loadData( JSON.parse( this.response ), code ); - // console.log(, e); + // console.log(, e); } ); req.open( "GET", "/local/" + file ); req.send(); @@ -236,7 +236,7 @@ class Panopticon { console.log("Light", hv_id, light_id); this.send( { action: 'change_light', hugvey: hv_id, light_id: light_id } ); } - + playFromSelected(msg_id, reloadStory) { if(!this.hugveys.selectedId) { alert('No hugvey selected'); @@ -337,17 +337,17 @@ class Graph { // used eg. after a condition creation. this.showMsg( this.selectedMsg ); } - + getAudioUrlForMsg(msg) { let isVariable = msg['text'].includes('$') ? '1' : '0'; let lang = panopticon.graph.language_code; return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&lang=${lang}&filename=0`; } - + getConfig() { - + } - + getNumericId(prefix) { let id, i = 0; let hasId= function(a, id) { @@ -362,10 +362,10 @@ class Graph { id = prefix + i; i++; } while(hasId(this.data, id)) - + return id; } - + createDiversion(type) { let div = { "@id": this.getNumericId(this.language_code.substring( 0, 2 ) + `-div-${type}#`), @@ -373,7 +373,7 @@ class Graph { 'type': type, 'params': {} } - + if(type == 'no_response') { div['params']['consecutiveSilences'] = 3; div['params']['timesOccured'] = 0; @@ -399,33 +399,33 @@ class Graph { div['params']['msgId'] = ""; } else if(type == 'repeat') { - div['params']['regex'] = "can you repeat that\\?"; + div['params']['regex'] = "can you repeat that\\?"; } else { console.log("invalid type", type); alert('invalid type for diversion'); } - + if(type != 'repeat' && type != 'interrupt') { div['params']['notAfterMsgId'] = ""; } - + this.data.push( div ); this.updateFromData(); this.build(); - + this.showDiversions(); return msg; } - + deleteDiversion(div) { this._rmNode( div ); this.showDiversions( ); } - + showDiversions( ) { let msgEl = document.getElementById( 'msg' ); msgEl.innerHTML = ""; - + let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = []; for(let div of this.diversions) { @@ -448,7 +448,7 @@ class Graph { }}, ...notMsgOptions) ); } - + if(div['type'] == 'no_response') { let returnAttrs = { 'type': 'checkbox', @@ -468,7 +468,7 @@ class Graph { } msgOptions.push(crel('option', optionParams , startMsg['@id'])); } - + divsNoResponse.push(crel( 'div', { 'class': 'diversion', @@ -539,7 +539,7 @@ class Graph { } msgOptions.push(crel('option', optionParams , startMsg['@id'])); } - + divsReplyContains.push(crel( 'div', { 'class': 'diversion', @@ -606,7 +606,7 @@ class Graph { ), notAfterMsgIdEl )); - } + } if(div['type'] == 'timeout') { let returnAttrs = { 'type': 'checkbox', @@ -617,7 +617,7 @@ class Graph { if(div['params']['returnAfterStrand']) { returnAttrs['checked'] = 'checked'; } - + let totalOrLocalAttrs = { 'type': 'checkbox', 'on': { @@ -627,7 +627,7 @@ class Graph { if(div['params']['fromLastMessage']) { totalOrLocalAttrs['checked'] = 'checked'; } - + let msgOptions = [crel('option',"")]; let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true); for(let startMsg of starts) { @@ -637,7 +637,7 @@ class Graph { } msgOptions.push(crel('option', optionParams , startMsg['@id'])); } - + divsTimeouts.push(crel( 'div', { 'class': 'diversion', @@ -734,7 +734,7 @@ class Graph { } msgOptions.push(crel('option', optionParams , startMsg['@id'])); } - + divsInterrupts.push(crel( 'div', {'class': 'diversion'}, crel('h3', div['@id']), @@ -753,9 +753,9 @@ class Graph { )); } } - + console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts); - + let divEl = crel( 'div', { @@ -797,7 +797,7 @@ class Graph { 'on': { 'click': (e) => this.createDiversion('repeat') } - }, + }, 'New case for repeat' ) ), @@ -810,7 +810,7 @@ class Graph { 'on': { 'click': (e) => this.createDiversion('timeout') } - }, + }, 'New case for timeout' ) ) @@ -824,12 +824,12 @@ class Graph { // 'on': { // 'click': (e) => this.createDiversion('interrupt') // } -// }, +// }, // 'New case for Interrupt' // ) // ) ); - + msgEl.appendChild(divEl); } @@ -904,19 +904,19 @@ class Graph { }) ) ); - + document.getElementById("interface").appendChild(configEl); } showMsg( msg ) { let msgEl = document.getElementById( 'msg' ); msgEl.innerHTML = ""; - + if(msg == null){ return; } - - + + let startAttributes = { 'name': msg['@id'] + '-start', // 'readonly': 'readonly', @@ -939,7 +939,7 @@ class Graph { if ( msg['beginning'] == true ) { beginningAttributes['checked'] = 'checked'; } - + // chapter marker: let chapterAttributes = { 'name': msg['@id'] + '-chapterStart', @@ -952,14 +952,14 @@ class Graph { if ( typeof msg['chapterStart'] !== 'undefined' && msg['chapterStart'] == true ) { chapterAttributes['checked'] = 'checked'; } - + let params = {}; if(msg.hasOwnProperty('params')) { params = msg['params']; } else { msg['params'] = {}; } - + let audioSrcEl = crel('source', {'src': msg['audio'] ? msg['audio']['file'] : this.getAudioUrlForMsg(msg)}); let audioSpan = crel( 'span', @@ -1105,7 +1105,7 @@ class Graph { } } ) ), - + // color for beter overview crel( 'label', @@ -1123,9 +1123,9 @@ class Graph { ) ); msgEl.appendChild( msgInfoEl ); - - + + if(panopticon.hugveys.selectedId) { msgEl.appendChild(crel( 'div', @@ -1161,9 +1161,9 @@ class Graph { "Continue on #" + panopticon.hugveys.selectedId ) )); - + } - + // let directionHEl = document.createElement('h2'); // directionHEl.innerHTML = "Directions"; @@ -1220,13 +1220,13 @@ class Graph { 'on': { 'click': ( e ) => { if(confirm("Do you want to remove this direction and its conditions?")) { - g.rmDirection( direction ); + g.rmDirection( direction ); } } } }, 'disconnect') ); - + for ( let conditionId of direction['conditions'] ) { let condition = this.getNodeById( conditionId ); directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) ); @@ -1246,7 +1246,7 @@ class Graph { 'click': ( e ) => { if(confirm("Do you want to remove this condition?")) { // console.log('remove condition for direction', condition, direction); - panopticon.graph.rmCondition( condition, direction ); + panopticon.graph.rmCondition( condition, direction ); } } } @@ -1264,7 +1264,7 @@ class Graph { } ); labelLabel.appendChild( labelInput ); conditionEl.appendChild( labelLabel ); - + // for ( let v in condition['vars'] ) { // let varLabel = document.createElement( 'label' ); @@ -1309,14 +1309,14 @@ class Graph { } }; } - + getConditionInputsForType( type, conditionId, values ) { let inputs = []; let vars = this.getConditionTypes()[type]; for ( let v in vars ) { let attr = vars[v]; let inputType = attr.hasOwnProperty('tag') ? attr['tag'] : 'input'; - + attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`; if(typeof values != 'undefined') { let value = this._getValueForPath(v, values); @@ -1326,12 +1326,12 @@ class Graph { } attr['value'] = typeof value == 'undefined' ? "": value; attr['on'] = { - 'change': this.getEditEventListener() + 'change': this.getEditEventListener() } ; } else { // console.log(attr); } - + inputs.push( crel( 'label', crel( 'span', { @@ -1352,7 +1352,7 @@ class Graph { conditionForm.appendChild(i); } } - + _getValueForPath(path, vars) { path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value let v = vars; @@ -1372,7 +1372,7 @@ class Graph { } return result; } - + /** * Save an array path (string) with a value to an object. Used to turn * strings into nested arrays @@ -1421,7 +1421,7 @@ class Graph { form.delete( 'type' ); let label = form.get( 'label' ); form.delete( 'label' ); - + // checkboxes to true/false let defs = g.getConditionTypes()[type]; // console.log(defs); @@ -1432,13 +1432,13 @@ class Graph { form.set(field, form.has(field)); } } - + let vars = {}; for ( var pair of form.entries() ) { // FormData only has strings & blobs, we want booleans: if(pair[1] === 'true') pair[1] = true; - if(pair[1] === 'false') pair[1] = false; - + if(pair[1] === 'false') pair[1] = false; + vars = g._formPathToVars(pair[0], pair[1], vars); } // TODO: checkboxes @@ -1481,7 +1481,7 @@ class Graph { return addConditionEl; } - + /** * remove condition from the graph or merely from the given direction * @param {any} condition The condition to remove @@ -1496,7 +1496,7 @@ class Graph { if(pos > -1) { direction['conditions'].splice(pos, 1); } - + for(let dir of this.directions) { // console.log('check if condition exists for dir', dir) if(dir['conditions'].indexOf(id) > -1) { @@ -1547,14 +1547,14 @@ class Graph { "afterrunTime": 0.5, } this.data.push( msg ); - + console.log("skip or not to skip?", skipRebuild); if(typeof skipRebuild == 'undefined' || !skipRebuild) { this.updateFromData(); this.build(); this.selectMsg(msg); } - + return msg; } @@ -1611,7 +1611,7 @@ class Graph { "conditions": [] } this.data.push( dir ); - + let skipDistances; // orphaned target and source has no other destinations. We can copy the vertical position: if(this.getDirectionsFrom( source ).length < 1 && this.getDirectionsFrom( target ).length < 1 && this.getDirectionsTo( target ).length < 1) { @@ -1620,11 +1620,11 @@ class Graph { let d = [distance[0] + 1, distance[1]]; // create a distance based on source's position // this saves us from running the slow calculateDistancesFromStart - this.distances[target['@id']] = d; + this.distances[target['@id']] = d; } else { skipDistances = false; } - + this.updateFromData(skipDistances); this.build(); return dir; @@ -1639,18 +1639,18 @@ class Graph { this.addMsg(); this.build(); } - + createConnectedMsg(sourceMsg) { console.time('createConnected'); console.time("Add"); let newMsg = this.addMsg(true); // skipRebuild = true, as addDirection() already rebuilds the graph this.getNodeById(newMsg['@id']).y = this.getNodeById(sourceMsg['@id']).y; - + if(this.getNodeById(sourceMsg['@id']).hasOwnProperty('color')){ - this.getNodeById(newMsg['@id']).color = this.getNodeById(sourceMsg['@id']).color + this.getNodeById(newMsg['@id']).color = this.getNodeById(sourceMsg['@id']).color } console.timeEnd("Add"); - + console.time("direction"); this.addDirection(sourceMsg, newMsg); console.timeEnd("direction"); @@ -1677,15 +1677,15 @@ class Graph { let graph = this; let el = function( e ) { console.info("Changed", e); - let parts = e.srcElement.name.split( '-' ); + let parts = e.target.name.split( '-' ); let field = parts.pop(); let id = parts.join('-'); let node = graph.getNodeById( id ); let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value var res = node; - let value = e.srcElement.value - if(e.srcElement.type == 'checkbox') { - value = e.srcElement.checked; + let value = e.target.value + if(e.target.type == 'checkbox') { + value = e.target.checked; } for ( var i = 0; i < path.length; i++ ) { if ( i == ( path.length - 1 ) ) { @@ -1698,7 +1698,7 @@ class Graph { // node[field] = e.srcElement.value; graph.build(); - + if(typeof callback !== 'undefined'){ callback(); } @@ -1769,11 +1769,11 @@ class Graph { console.info("Save json", formData ); var request = new XMLHttpRequest(); request.open( "POST", "http://localhost:8888/upload" ); - + if(callback) { request.addEventListener( "load", callback); } - + request.send( formData ); } @@ -1789,13 +1789,13 @@ class Graph { this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' ); this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' ); this.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' ); - + let configurations = this.data.filter(( node ) => node['@type'] == 'Configuration' ); this.configuration = configurations.length > 0 ? configurations[0] : { "@id": "config", "@type": "Configuration" }; - + document.getElementById('current_lang').innerHTML = ""; document.getElementById('current_lang').appendChild(crel('span', { 'class': 'flag-icon ' + this.language_code @@ -1803,7 +1803,7 @@ class Graph { let storyEl = document.getElementById('story'); storyEl.classList.remove(... panopticon.languages.map((l) => l['code'])) storyEl.classList.add(this.language_code); - + if(typeof skipDistances == 'undefined' || !skipDistances) { this.distances = this.calculateDistancesFromStart(); } @@ -1811,7 +1811,7 @@ class Graph { // save state; this.saveState(); } - + updateHugveyStatus(hv) { let els = document.getElementsByClassName('beenHit'); while(els.length > 0) { @@ -1820,11 +1820,11 @@ class Graph { if(!hv || typeof hv['history'] == 'undefined') { return; } - + if(hv['history'].hasOwnProperty('messages')){ for(let msg of hv['history']['messages']) { document.getElementById(msg[0]['id']).classList.add('beenHit'); - } + } } if(hv['history'].hasOwnProperty('directions')){ for(let msg of hv['history']['directions']) { @@ -1857,7 +1857,7 @@ class Graph { return fx; }).strength(50)) .force( "forceY", d3.forceY(function(m){ -// if(panopticon.graph.distances[m['@id']] !== null ) +// if(panopticon.graph.distances[m['@id']] !== null ) // console.log(panopticon.graph.distances[m['@id']][1]); let fy = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][1] * panopticon.graph.nodeSize * 3: 0 // console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx); @@ -1872,7 +1872,7 @@ class Graph { .selectAll( "g" ) .data( this.messages, n => n['@id'] ) ; - + // Update existing nodes let newNode = node.enter(); @@ -1887,7 +1887,7 @@ class Graph { .attr( 'r', this.nodeSize ) // .text(d => d.id) ; - + let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' ); let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' ); let statusIcon = newNodeG.append( "image" ) @@ -1901,7 +1901,7 @@ class Graph { // remove node.exit().remove(); node = node.merge( newNodeG ); - + // for all existing nodes: node.attr( 'class', msg => { @@ -1916,9 +1916,9 @@ class Graph { return classes.join( ' ' ); } ) - + .on(".drag", null) - .call( + .call( d3.drag( this.simulation ) .on("start", function(d){ if (!d3.event.active) panopticon.graph.simulation.alphaTarget(0.3).restart(); @@ -1935,9 +1935,9 @@ class Graph { d.fy = null; }) // .container(document.getElementById('container')) - + ); - + node.select('circle').attr('style', (d) => 'fill: ' + (d.hasOwnProperty('color') ? d['color'] : '#77618e')); let link = this.linkG @@ -1947,7 +1947,7 @@ class Graph { let newLink = link.enter() .append( "line" ) ; - + //remove link.exit().remove(); link = link.merge( newLink ); @@ -2032,7 +2032,7 @@ class Graph { } return this.svg.node(); } - + calculateDistancesFromStart() { console.time('calculateDistancesFromStart'); let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true); @@ -2040,26 +2040,26 @@ class Graph { console.error("No start set"); return; } - + //initiate distances let distances = {}; for(let msg of this.messages) { // distances[msg['@id']] = msg === startMsg ? 0 : null; distances[msg['@id']] = null; } - + let targetsPerMsg = {}; let sourcesPerMsg = {}; // console.log("dir", this.directions); for(let direction of this.directions) { let from = typeof direction['source'] == "string" ? direction['source'] : direction['source']['@id']; let to = typeof direction['target'] == "string" ? direction['target'] : direction['target']['@id']; - + if(!targetsPerMsg.hasOwnProperty(from)) { targetsPerMsg[from] = []; } targetsPerMsg[from].push(to); - + if(!sourcesPerMsg.hasOwnProperty(to)) { sourcesPerMsg[to] = []; @@ -2074,21 +2074,21 @@ class Graph { // end of trail return yPos; } - - + + let i = 0, y =0; for(let childMsgId of msgsPerMsg[msgId]) { if(distances[childMsgId] !== null){ continue; } if(distances[childMsgId] === null || (goingDown && distances[childMsgId][0] > depth)) { - + if(distances[childMsgId] === null) { if(i > 0){ yPos++; } i++; - + console.log('set for id', childMsgId, goingDown, depth, yPos); distances[childMsgId] = [depth, yPos]; @@ -2106,7 +2106,7 @@ class Graph { if(distances[childMsgId] === null) { distances[childMsgId] = [depth, yPos]; } - + // console.log('a', depth); yPos = traverseMsg(childMsgId, depth - 1, goingDown, yPos); } else { @@ -2114,7 +2114,7 @@ class Graph { } } - + // if( i == 0 && y == 1) { // // we reached an item that branches back into the tree // return yPos -1; @@ -2122,7 +2122,7 @@ class Graph { // console.log('yPos',msgId,yPos); return yPos; } - + let yPos = 0; console.time('step1'); for(let startMsg of starts) { @@ -2148,7 +2148,7 @@ class Graph { console.timeEnd('polish: '+ msgId); } console.timeEnd('step2'); - + // let additionalsDepth = 0; //// now the secondary strands: // for(let msgId in distances) { @@ -2158,11 +2158,11 @@ class Graph { // } // distances[msgId] = additionalsDepth; // traverseMsg(msgId, additionalsDepth+1, true); -// +// // } console.timeEnd("calculateDistancesFromStart"); return distances; } } // -// \ No newline at end of file +//