Merge branch 'master' of gitlab.com:hugvey/hugvey

This commit is contained in:
Ruben van de Ven 2019-05-13 14:46:46 +02:00
commit 42b4185a69
8 changed files with 407 additions and 222 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;
}
}
//
//
//