diff --git a/README.md b/README.md index 9c5154c..ed16384 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ chown=pi:pi ## Deploy / usefull commands ```bash -for i in {1..6}; do rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git; done +for i in {1..26}; do echo $i; rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git --exclude=recordings --exclude=/voice* --exclude=/pd; done ``` ```bash @@ -204,4 +204,4 @@ times occured/only on n-th instance: determines the order of diversions of the s ## 4G Modem Visit 192.168.5.1 -The password is at the bottom of the device. \ No newline at end of file +The password is at the bottom of the device. diff --git a/client_config.yml b/client_config.yml index a49d591..5ec33d3 100644 --- a/client_config.yml +++ b/client_config.yml @@ -5,7 +5,7 @@ voice: input_rate: 44100 target_rate: 16000 port: 4444 - input_name: 'AK5371' + input_name: 'USB Audio Device' output_name: 'USB Audio Device' input_mixer: 'Mic' output_mixer: 'PCM' @@ -13,6 +13,3 @@ voice: output_volume: 30 file_address: "http://hugveycmd.local:8888" output_driver: pulseaudio - - - diff --git a/hugvey/central_command.py b/hugvey/central_command.py index 5859630..7e4ec83 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -24,6 +24,7 @@ from hugvey.voice import VoiceStorage import multiprocessing from hugvey.speech.recorder import Recorder from pythonosc import udp_client +import copy mainLogger = logging.getLogger("hugvey") @@ -67,7 +68,7 @@ class CentralCommand(object): self.languageFiles = {} self.languageConfig = {} self.args = args # cli args - + eventLogger.addHandler(logging.handlers.QueueHandler(self.logQueue)) def loadConfig(self, filename): @@ -85,14 +86,14 @@ class CentralCommand(object): self.hugvey_ids = [i + 1 for i in range(self.config['hugveys'])] self.loadLanguages() - - + + voice_dir = os.path.join(self.config['web']['files_dir'], 'voices') self.voiceStorage = VoiceStorage(voice_dir, self.languageConfig) - + self.panopticon = Panopticon(self, self.config, self.voiceStorage) - - + + def loadLanguages(self): logger.debug('load language files') self.languages = {} @@ -116,7 +117,7 @@ class CentralCommand(object): # if not hv.story: # status['status'] = 'off' # return status - + status['status'] = hv.getStatus() status['language'] = hv.language_code status['light_id'] = hv.lightId @@ -126,7 +127,7 @@ class CentralCommand(object): # status['history'] = hv.story.getLogSummary() # disabled as it is a bit slow. We now have eventLog # status['counts'] = {t: len(a) for t, a in status['history'].items() if t != 'directions' } status['counts'] = {} if not hv.story else hv.story.getLogCounts() - status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed() + status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed() return status @@ -139,10 +140,10 @@ class CentralCommand(object): 'logbookId': None, 'logbook': [], } - + #use this to test if any threads stay open # eg. after killing/dying of a hugvey -# print(threading.enumerate()) +# print(threading.enumerate()) for hv_id in self.hugvey_ids: status['hugveys'].append(self.getHugveyStatus(hv_id, selected_id == hv_id)) @@ -151,7 +152,7 @@ class CentralCommand(object): if self.hugveys[selected_id].recorder: status['logbook'] = self.hugveys[selected_id].recorder.currentLog status['logbookId'] = selected_id - + return status def commandHugvey(self, hv_id, msg): @@ -169,8 +170,8 @@ class CentralCommand(object): def _queueCommand(self, hv_id, msg): self.commandQueue.put_nowait((hv_id, msg)) - - + + def commandLight(self, route, data): """ Buffer light commands @@ -215,13 +216,13 @@ class CentralCommand(object): logger.warn('Stopping command sender') s.close() - - + + async def lightSender(self): lightConn = udp_client.SimpleUDPClient( self.config['light']['ip'], self.config['light']['port']) - + logger.info(f"Ready to send light commands to: {self.config['light']['ip']}:{self.config['light']['port']}") while self.isRunning.is_set(): @@ -232,7 +233,7 @@ class CentralCommand(object): logger.warn('Stopping light sender') lightConn._sock.close() - + async def redLightController(self): """ Every second, check if no hugveys are available. If so, the red light should be @@ -264,7 +265,7 @@ class CentralCommand(object): thread = threading.Thread( target=self.hugveyStateRunner, args=(hugvey_id,), name=f"hugvey#{hugvey_id}") thread.start() - + def hugveyStateRunner(self, hugvey_id): while self.isRunning.is_set(): logger.info(f'Instantiate hugvey #{hugvey_id}') @@ -278,7 +279,7 @@ class CentralCommand(object): return logger.critical(f'Hugvey stopped (crashed?). Reinstantiate after 5 sec') time.sleep(5) - + async def timerEmitter(self): """ This is fixed: a one hour loop with a collective moment 10-15 minutes, @@ -288,25 +289,25 @@ class CentralCommand(object): intervals = [ { 'start_time': 10*60, - 'duration': 5 * 60, + 'duration': 5 * 60, }, { 'start_time': 30*60, - 'duration': 5 * 60, + 'duration': 5 * 60, }, { 'start_time': 50*60, - 'duration': 5 * 60, + 'duration': 5 * 60, } ] self.start_time = time.time() - + # TODO: emit start event - + while self.isRunning.is_set(): - + pass - + async def eventListener(self): s = self.ctx.socket(zmq.SUB) s.bind(self.config['events']['listen_address']) @@ -319,7 +320,7 @@ class CentralCommand(object): while self.isRunning.is_set(): try: hugvey_id, msg = await zmqReceive(s) - + if hugvey_id not in self.hugvey_ids: logger.critical( "Message from alien Hugvey: {}".format(hugvey_id)) @@ -355,7 +356,7 @@ class CentralCommand(object): fn = await self.voiceStorage.requestFile(hv.language_code, text, isVariable) if fn is None: eventLogger.getChild(f"{hugvey_id}").critical("error: No voice file fetched, check logs.") - fn = 'local/crash.wav' + fn = 'local/crash.wav' # TODO: trigger a repeat/crash event. await s.send_string(fn) except Exception as e: @@ -376,7 +377,7 @@ class CentralCommand(object): self.catchException(self.lightSender())) self.tasks['redLightController'] = self.loop.create_task( self.catchException(self.redLightController())) - + for hid in self.hugvey_ids: self.tasks['voiceListener'] = self.loop.create_task( self.catchException(self.voiceListener(hid))) @@ -386,12 +387,12 @@ class CentralCommand(object): self.panopticon_thread = threading.Thread( target=self.panopticon.start, name="Panopticon") self.panopticon_thread.start() - + self.loop.run_forever() def stop(self): self.isRunning.clear() - + async def catchException(self, awaitable): try: # print(awaitable) @@ -405,7 +406,7 @@ class HugveyState(object): """Represents the state of a Hugvey client on the server. Manages server connections & voice parsing etc. """ - + # all statusses can only go up or down, except for gone, which is an error state: # off <-> blocked <-> available <-> running <-> paused STATE_OFF = "off" @@ -424,7 +425,7 @@ class HugveyState(object): self.isConfigured = None self.isRunning = asyncio.Event(loop=self.loop) self.isRunning.clear() - + self.eventQueue = None self.language_code = 'en-GB' self.story = None @@ -435,24 +436,24 @@ class HugveyState(object): self.notShuttingDown = True # TODO: allow shutdown of object self.startMsgId = None self.eventLogger = eventLogger.getChild(f"{self.id}") - + self.setStatus(self.STATE_GONE) - + self.requireRestartAfterStop = None def __del__(self): self.logger.warn("Destroying hugvey object") - + def getStatus(self): return self.status - + def setStatus(self, status): self.status = status lightOn = status in [self.STATE_AVAILABLE, self.STATE_PAUSE] self.setLightStatus(lightOn) self.eventLogger.info(f"status: {self.status}") - - + + def config(self, hostname, ip): self.ip = ip self.hostname = hostname @@ -463,7 +464,7 @@ class HugveyState(object): else: self.logger.info( f"Hugvey {self.id} at {self.ip}, host: {self.hostname}") - + if self.status == self.STATE_GONE: # turn on :-) self.setStatus(self.STATE_BLOCKED) @@ -496,7 +497,7 @@ class HugveyState(object): self.logger.exception(e) self.logger.critical(f"Hugvey crash") self.eventLogger.critical(f"error: {e}") - + # restart # TODO: test proper functioning self.shutdown() @@ -510,18 +511,18 @@ class HugveyState(object): else: # Allow for both the Hugvey Command, or the Story handle the event. self.loop.call_soon_threadsafe(self._queueEvent, msg) - + def _queueEvent(self, msg): """ Put event in both the event loop for the story as well as the Hugvey State handler """ self.logger.debug(f"Queue event in hugvey loop: {msg}") self.eventQueue.put_nowait(msg) - + # connection events don't need to go to the story if msg['event'] == 'connection': return - + if self.story: self.story.events.append(msg) else: @@ -539,7 +540,7 @@ class HugveyState(object): # self.gone() self.shutdown() continue - + self.logger.debug("Received: {}".format(event)) if event['event'] == 'connection': # 'event': 'connection', @@ -547,11 +548,11 @@ class HugveyState(object): # 'host': socket.gethostname(), # 'ip': self.getIp(), self.config(event['host'], event['ip']) - - + + if event['event'] == 'language': self.setLanguage(event['code']) - + if event['event'] == 'pause': self.pause() if event['event'] == 'block': @@ -564,7 +565,7 @@ class HugveyState(object): self.story._finish() # finish story AND hugvey state if event['event'] == 'resume': self.resume() - + if event['event'] == 'change_language': self.setLanguage(event['lang_code']) if event['event'] == 'change_light': @@ -578,7 +579,7 @@ class HugveyState(object): # self.restart() if self.story is None: return - + self.startMsgId = event['msg_id'] self.logger.debug(f"Restart from {self.startMsgId}") self.restart() @@ -588,18 +589,18 @@ class HugveyState(object): def setLanguage(self, language_code): if language_code not in self.command.languages: raise Exception("Invalid language {}".format(language_code)) - + self.logger.info(f"set language: {language_code}") self.language_code = language_code - + if self.google: self.google.setLanguage(language_code) - + if self.isRunning.is_set(): self.restart() # self.story.reset() # self.story.setStoryData(self.command.languages[language_code]) - + def pause(self): self.logger.info('Pause') if self.google: @@ -608,7 +609,7 @@ class HugveyState(object): self.story.pause() self.isRunning.clear() self.setStatus(self.STATE_PAUSE) - + def resume(self): """ Start playing without reset""" self.logger.info('Resume') @@ -618,14 +619,14 @@ class HugveyState(object): self.story.resume() self.isRunning.set() self.setStatus(self.STATE_RUNNING) - + def restart(self): """Start playing with reset""" self.logger.info('Restart') if self.story: self.story.stop() self.resume() - + def block(self): """Block a hugvey""" self.logger.info('block') @@ -635,37 +636,37 @@ class HugveyState(object): self.story.finish() self.isRunning.clear() self.setStatus(self.STATE_BLOCKED) - + def available(self): """Put in available mode""" self.logger.info('Finish/Await') self.pause() self.setStatus(self.STATE_AVAILABLE) - + def setLightStatus(self, on): status = 1 if on else 0 self.logger.log(LOG_BS, f"Send /hugvey {status}") - + self.command.commandLight('/hugvey', [self.lightId, status]) - + def setLightId(self, id): """ Connect hugvey to another light """ self.lightId = id - + def gone(self): '''Status to 'gone' as in, shutdown/crashed/whatever ''' self.pause() if self.story: self.story.stop() - + self.logger.warn('Gone') self.eventLogger.warn("Gone") self.isConfigured = None self.setStatus(self.STATE_GONE) - + def shutdown(self, definitive = False): self.logger.info(f"Start shutdown sequence {definitive}") self.eventLogger.critical(f"error: shutting down") @@ -674,7 +675,7 @@ class HugveyState(object): if self.story: self.story.shutdown() self.story = None - + # shutdown for stream consumers already ran. Only clear references if self.google: self.google = None @@ -682,14 +683,14 @@ class HugveyState(object): self.player = None if self.recorder: self.recorder = None - + if self.requireRestartAfterStop is None: # prevent double setting of the same variable # first call sometimes triggers second self.requireRestartAfterStop = not definitive - + self.notShuttingDown = False - + async def playStory(self): while self.notShuttingDown: @@ -707,13 +708,13 @@ class HugveyState(object): self.logger.warn(f"Starting from {startMsgId}") if not self.streamer: await asyncio.sleep(1) - + self.streamer.triggerStart() - self.story.setStoryData(self.command.languages[self.language_code]) + self.story.setStoryData(copy.deepcopy(self.command.languages[self.language_code])) self.setLightStatus(False) await self.story.run(startMsgId) # self.story = None - + def getStreamer(self): if not self.streamer: self.streamer = AudioStreamer( @@ -721,19 +722,19 @@ class HugveyState(object): self.ip, int(self.command.config['voice']['port']) + self.id, self.id) - + if self.command.config['voyeur']: self.logger.warn("Debug on: Connecting Audio player") self.player = Player( self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate']) self.streamer.addConsumer(self.player) - + if self.command.config['voice']['record_dir']: self.logger.warn("Record Audio of conversation") self.recorder = Recorder( self.id, self.command.config['voice']['src_rate'], self.command.config['voice']['record_dir']) self.streamer.addConsumer(self.recorder) - + self.logger.debug("Start Speech") self.google = GoogleVoiceClient( hugvey=self, @@ -748,7 +749,7 @@ class HugveyState(object): ''' Start the audio streamer service ''' - + self.logger.debug("Start audio loop") while self.notShuttingDown: diff --git a/hugvey/story.py b/hugvey/story.py index 5cf53d4..d5b7920 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -13,6 +13,7 @@ import faulthandler from zmq.asyncio import Context import zmq import wave +import sox from pythonosc import udp_client import random @@ -26,18 +27,18 @@ class Utterance(object): self.endTime = None self.text = "" self.lastUpdateTime = startTime - + def setText(self, text, now): self.text = text self.lastUpdateTime = now - + def setFinished(self, endTime): self.endTime = endTime - + def isFinished(self): return self.endTime is not None - + class Message(object): def __init__(self, id, text): self.id = id @@ -58,7 +59,7 @@ class Message(object): self.parseForVariables() self.uuid = None # Have a unique id each time the message is played back. self.color = None - + def setStory(self, story): self.story = story self.logger = story.logger.getChild("message") @@ -80,7 +81,7 @@ class Message(object): # prevent clipping on some Lyrebird tracks msg.params['vol'] = .8 return msg - + def parseForVariables(self): """ Find variables in text @@ -88,20 +89,20 @@ class Message(object): self.variables = re.findall('\$(\w+)', self.text) for var in self.variables: self.variableValues[var] = None - + def hasVariables(self) -> bool: return len(self.variables) > 0 - + def setVariable(self, name, value): if name not in self.variables: self.logger.critical("Set nonexisting variable") return - + if self.variableValues[name] == value: return - + self.variableValues[name] = value - + self.logger.warn(f"Set variable, fetch {name}") if not None in self.variableValues.values(): self.logger.warn(f"now fetch {name}") @@ -116,10 +117,10 @@ class Message(object): # self.logger.debug(f"Getting text for {self.id}") for var in self.variables: self.logger.debug(f"try replacing ${var} with {self.variableValues[var]} in {text}") - replacement = self.variableValues[var] if (self.variableValues[var] is not None) else "nothing" #TODO: translate nothing to each language + replacement = self.variableValues[var] if (self.variableValues[var] is not None) else "nothing" #TODO: translate nothing to each language text = text.replace('$'+var, replacement) return text - + def setReply(self, reply): self.reply = reply @@ -132,16 +133,16 @@ class Message(object): "Getting reply while there is none! {0}".format(self.id)) return self.reply - + def isFinished(self): return self.finishTime is not None - + def setFinished(self, currentTime): self.finishTime = currentTime - + def getFinishedTime(self): return self.finishTime - + def getParams(self): return self.params @@ -152,18 +153,18 @@ class Message(object): 'text': self.getText(), 'replyText': None if self.reply is None else [u.text for u in self.reply.utterances] } - + async def getAudioFilePath(self): if self.audioFile is not None: return self.audioFile - + text = self.getText() self.logger.debug(f"Fetching audio for {text}") - + # return "test"; async with self.filenameFetchLock: # print(threading.enumerate()) - + info = { 'text': text, 'variable': True if self.hasVariables() else False @@ -174,10 +175,10 @@ class Message(object): await s.send_json(info) filename = await s.recv_string() s.close() - - + + # print(threading.enumerate()) - + self.logger.debug(f"Fetched audio for {text}: {filename}") return filename @@ -187,20 +188,20 @@ class Reply(object): self.forMessage = None self.utterances = [] self.setForMessage(message) - + def setForMessage(self, message: Message): self.forMessage = message message.setReply(self) - + def getLastUtterance(self) -> Utterance: if not self.hasUtterances(): return None u = self.utterances[-1] #: :type u: Utterance - + # attempt to fix a glitch that google does not always send is_finished if u.isFinished(): return u - + now = self.forMessage.story.timer.getElapsed() diff = now - u.lastUpdateTime if diff > 5: # time in seconds to force silence in utterance @@ -209,23 +210,23 @@ class Reply(object): f"Set finish time for utterance after {diff}s {u.text}" ) u.setFinished(now) - + return u - + def getFirstUtterance(self) -> Utterance: if not self.hasUtterances(): return None return self.utterances[0] - + def hasUtterances(self) -> bool: return len(self.utterances) > 0 def addUtterance(self, utterance: Utterance): self.utterances.append(utterance) - + def getText(self) -> str: return ". ".join([u.text for u in self.utterances]) - + def getActiveUtterance(self, currentTime) -> Utterance: """ If no utterance is active, create a new one. Otherwise return non-finished utterance for update @@ -242,13 +243,13 @@ class Reply(object): if u is not None and not u.isFinished(): return True return False - + def getTimeSinceLastUtterance(self): if not self.hasUtterances(): return None - + return self.forMessage.story.timer.getElapsed() - self.getLastUtterance().lastUpdateTime - + class Condition(object): """ A condition, basic conditions are built in, custom condition can be given by @@ -269,7 +270,7 @@ class Condition(object): condition = conditionClass(data['@id']) condition.type = data['type'] condition.originalJsonString = json.dumps(data) - + # TODO: should Condition be subclassed? if data['type'] == "replyContains": condition.method = condition._hasMetReplyContains @@ -279,7 +280,7 @@ class Condition(object): condition.method = condition._hasVariable if data['type'] == "diversion": condition.method = condition._hasDiverged - + if 'vars' in data: condition.vars = data['vars'] @@ -296,7 +297,7 @@ class Condition(object): # check if the message already finished playing if not story.lastMsgFinishTime: return False - + if 'onlyIfNoReply' in self.vars and self.vars['onlyIfNoReply']: if story.currentReply and story.currentReply is not None and story.currentReply.hasUtterances(): story.logger.log(LOG_BS, f'Only if no reply has text! {story.currentReply.getText()}') @@ -304,29 +305,29 @@ class Condition(object): return False # else: # story.logger.debug('Only if no reply has no text yet!') - + hasMetTimeout = now - story.lastMsgFinishTime >= float(self.vars['seconds']) if not hasMetTimeout: return False - + # update stats: story.stats['timeouts'] +=1 if 'needsReply' in self.vars and self.vars['needsReply'] is True: if story.currentReply is None or not story.currentReply.hasUtterances(): story.stats['silentTimeouts'] +=1 story.stats['consecutiveSilentTimeouts'] += 1 - + self.logInfo = "{}s".format(self.vars['seconds']) return True - + def _hasVariable(self, story) -> bool: if not story.lastMsgFinishTime: return False - + r = story.hasVariableSet(self.vars['variable']) if r: story.logger.debug(f"Variable {self.vars['variable']} is set.") - + if 'notSet' in self.vars and self.vars['notSet']: # inverse: r = not r @@ -335,28 +336,28 @@ class Condition(object): self.vars['variable'] ) return r - + def _hasDiverged(self, story) -> bool: if not story.lastMsgFinishTime: return False - + d = story.get(self.vars['diversionId']) if not d: story.logger.critical(f"Condition on non-existing diversion: {self.vars['diversionId']}") - + r = d.hasHit if r: story.logger.debug(f"Diversion {self.vars['diversionId']} has been hit.") - + if 'inverseMatch' in self.vars and self.vars['inverseMatch']: # inverse: r = not r - + self.logInfo = "Has {} diverged to {}".format( 'not' if 'inverseMatch' in self.vars and self.vars['inverseMatch'] else '', self.vars['diversionId'] ) - + return r def _hasMetReplyContains(self, story) -> bool: @@ -372,12 +373,12 @@ class Condition(object): return False capturedVariables = None - + if 'regex' in self.vars and len(self.vars['regex']): if 'regexCompiled' not in self.vars: # Compile once, as we probably run it more than once self.vars['regexCompiled'] = re.compile(self.vars['regex']) - + t = r.getText().lower() story.logger.log(LOG_BS, 'attempt regex: {} on {}'.format(self.vars['regex'], t)) result = self.vars['regexCompiled'].search(t) @@ -385,14 +386,14 @@ class Condition(object): #if there is something to match, but not found, it's never ok return False story.logger.debug('Got match on {}'.format(self.vars['regex'])) - + capturedVariables = result.groupdict() - + if ('instantMatch' in self.vars and self.vars['instantMatch']) or not r.isSpeaking(): # try to avoid setting variables for intermediate strings for captureGroup in capturedVariables: story.setVariableValue(captureGroup, capturedVariables[captureGroup]) - + if 'instantMatch' in self.vars and self.vars['instantMatch']: story.logger.info(f"Instant match on {self.vars['regex']}, {self.vars}") self.logInfo = "Instant match of {}, captured {}".format( @@ -401,8 +402,8 @@ class Condition(object): ) return True # TODO: implement 'instant match' -> don't wait for isFinished() - - + + # print(self.vars) # either there's a match, or nothing to match at all if 'delays' in self.vars: @@ -424,7 +425,7 @@ class Condition(object): self.logInfo = "Match of {}, captured {} after, {}".format( self.vars['regex'], capturedVariables, - timeSinceReply + timeSinceReply ) self.usedContainsDuration = float(delay['waitTime']) return True @@ -432,16 +433,16 @@ class Condition(object): # wait for delay to match story.logger.log(LOG_BS, "Wait for it...") return False - + # If there is a delay, it takes precedence of isSpeaking, since google does not always give an is_finished on short utterances (eg. "hello" or "no") if r.isSpeaking(): story.logger.log(LOG_BS, f"is speaking: {r.getLastUtterance().text} - {r.getLastUtterance().startTime}") return False - + # There is a match and no delay say, person finished speaking. Go ahead sir! self.logInfo = "Match" return True - + def getLogSummary(self): return { 'id': self.id @@ -463,7 +464,7 @@ class Direction(object): def addCondition(self, condition: Condition): self.conditions.append(condition) - + def setMetCondition(self, condition: Condition): self.conditionMet = condition @@ -477,11 +478,11 @@ class Direction(object): c = story.get(conditionId) direction.addCondition(c) return direction - + def getLogSummary(self): return { 'id': self.id, - 'condition': self.conditionMet.id if self.conditionMet else None + 'condition': self.conditionMet.id if self.conditionMet else None } @@ -510,7 +511,7 @@ class Diversion(object): self.regex = re.compile(self.params['regex']) else: self.regex = None - + if type == 'timeout': self.method = self._divergeIfTimeout self.finaliseMethod = self._returnAfterTimeout @@ -519,28 +520,28 @@ class Diversion(object): self.regex = re.compile(self.params['regex']) if type == 'interrupt': self.method = self._divergeIfInterrupted - + if not self.method: raise Exception("No valid type given for diversion") @classmethod def initFromJson(diversionClass, data, story): diversion = diversionClass(data['@id'], data['type'], data['params']) - + return diversion - - + + def getLogSummary(self): return { 'id': self.id, } - + async def divergeIfNeeded(self, story, direction = None): """ Validate if condition is met for the current story state Returns True when diverging """ - + # For all diversion except repeat (which simply doesn't have the variable) if 'notAfterMsgId' in self.params and self.params['notAfterMsgId']: msg = story.get(self.params['notAfterMsgId']) @@ -550,7 +551,7 @@ class Diversion(object): # story.logger.warn(f"Block diversion {self.id} because of hit message {self.params['notAfterMsgId']}") self.disabled = True # never run it and allow following timeouts/no_responses to run return False - + r = await self.method(story, direction.msgFrom if direction else None, direction.msgTo if direction else None, @@ -559,16 +560,16 @@ class Diversion(object): if self.type != 'repeat' and self.type !='interrupt': # repeat diversion should be usable infinte times self.hasHit = True - + story.addToLog(self) return r - + def createReturnDirectionsTo(self, story, startMsg, returnMsg, originalDirection = None, inheritTiming = True, timeoutDuration = .5, replyContainsDurations = None): """ The finishes of this diversion's strand should point to the return message with the right timeout/timing. If hit, this direction should also notify this diversion. - + replyContainsDurations: list formatted as in JSON [{ "minReplyDuration": "0", @@ -576,7 +577,7 @@ class Diversion(object): }] """ self.counter +=1 - + finishMessageIds = story.getFinishesForMsg(startMsg) finalTimeoutDuration = timeoutDuration finalContainsDurations = replyContainsDurations @@ -593,7 +594,7 @@ class Diversion(object): finalTimeoutDuration = float(condition.vars['seconds']) if condition.type == 'replyContains': finalContainsDurations = json.loads(condition.originalJsonString)['vars']['delays'] - + i = 0 for msgId in finishMessageIds: # Some very ugly hack to add a direction & condition @@ -601,7 +602,7 @@ class Diversion(object): msg = story.get(msgId) if not msg: continue - + direction = Direction(f"{self.id}-{i}-{self.counter}", msg, returnMsg) data = json.loads(f""" {{ @@ -615,11 +616,11 @@ class Diversion(object): }} """) data['vars']['onlyIfNoReply'] = finalContainsDurations is not None - + # TODO: also at replycontains if it exists, with the same timings condition = Condition.initFromJson(data, story) direction.addCondition(condition) - + if finalContainsDurations is not None: data2 = json.loads(f""" {{ @@ -636,14 +637,14 @@ class Diversion(object): condition2 = Condition.initFromJson(data2, story) direction.addCondition(condition2) story.add(condition2) - + direction.isDiversionReturn = True # will clear the currentDiversion on story story.logger.info(f"Created direction: {direction.id} {condition.id} with timeout {finalTimeoutDuration}s") story.add(condition) story.add(direction) - - - + + + async def finalise(self, story): """" Only used if the Diversion sets the story.currentDiversion @@ -653,11 +654,11 @@ class Diversion(object): story.logger.info(f"No finalisation for diversion {self.id}") story.currentDiversion = None return False - + await self.finaliseMethod(story) story.currentDiversion = None return True - + async def _divergeIfNoResponse(self, story, msgFrom, msgTo, direction): """ Participant doesn't speak for x consecutive replies (has had timeout) @@ -665,33 +666,33 @@ class Diversion(object): ':type story: Story' if story.currentDiversion or not msgFrom or not msgTo: return False - + if story.stats['consecutiveSilentTimeouts'] >= int(self.params['consecutiveSilences']): story.stats['diversions']['no_response'] += 1 msg = story.get(self.params['msgId']) if msg is None: story.logger.critical(f"Not a valid message id for diversion: {self.params['msgId']}") return - + story.logger.info(f"Diverge: No response {self.id} {story.stats}") - + self.returnMessage = msgTo - + if self.params['returnAfterStrand']: self.createReturnDirectionsTo(story, msg, msgTo, direction) - + await story.setCurrentMessage(msg) story.currentDiversion = self return True - + return - + async def _returnAfterNoResponse(self, story): story.logger.info(f"Finalise diversion: {self.id}") story.stats['consecutiveSilentTimeouts'] = 0 # reset counter after diverging # if self.params['returnAfterStrand']: # await story.setCurrentMessage(self.returnMessage) - + async def _divergeIfReplyContains(self, story, msgFrom, msgTo, _): """ Participant doesn't speak for x consecutive replies (has had timeout) @@ -701,83 +702,83 @@ class Diversion(object): if story.currentDiversion: # or not msgFrom or not msgTo: # don't do nested diversions return False - + if self.hasHit: # don't match twice return - + if story.currentReply is None or not self.regex: return - + direction = story.getDefaultDirectionForMsg(story.currentMessage) if not direction: # ignore the direction argument, and only check if the current message has a valid default - return - + return + msgTo = direction.msgTo - + if not direction: return - + waitTime = 1.8 if 'waitTime' not in self.params else float(self.params['waitTime']) timeSince = story.currentReply.getTimeSinceLastUtterance() if timeSince < waitTime: story.logger.log(LOG_BS, f"Waiting for replyContains: {timeSince} (needs {waitTime})") return - + r = self.regex.search(story.currentReply.getText()) if r is None: return - + if 'notForColor' in self.params and self.params['notForColor'] and story.currentMessage.color: if story.currentMessage.color.lower() == self.params['notForColor'].lower(): story.logger.debug(f"Skip diversion {self.id} because of section color") return - + story.logger.info(f"Diverge: reply contains {self.id}") story.stats['diversions']['reply_contains'] += 1 - + msg = story.get(self.params['msgId']) if msg is None: story.logger.critical(f"Not a valid message id for diversion: {self.params['msgId']}") return - + # TODO: pick the direction with timeout as next Message. self.returnMessage = msgTo - + if self.params['returnAfterStrand']: self.createReturnDirectionsTo(story, msg, msgTo, direction) - + await story.setCurrentMessage(msg) story.currentDiversion = self return True - + async def _returnAfterReplyContains(self, story): story.logger.info(f"Finalise diversion: {self.id}") # if self.params['returnAfterStrand']: # await story.setCurrentMessage(self.returnMessage) - + async def _divergeIfRepeatRequest(self, story, msgFrom, msgTo, direction): """ Participant asks if message can be repeated. """ # if not msgFrom or not msgTo: # return - + # TODO: how to handle this now we sometimes use different timings. # Perhaps set isFinished when matching condition. if story.currentReply is None or story.currentReply.getTimeSinceLastUtterance() > 1: return - + r = self.regex.search(story.currentReply.getText()) if r is None: return - + logger.info(f"Diverge: request repeat {self.id}") story.stats['diversions']['repeat'] += 1 await story.setCurrentMessage(story.currentMessage) return True - + async def _divergeIfTimeout(self, story, msgFrom, msgTo, direction): """ (1) last spoken at all @@ -785,15 +786,15 @@ class Diversion(object): """ if story.currentDiversion: return - + if msgFrom or msgTo: # not applicable a direction has been chosen return - + if not story.lastMsgFinishTime: # not during play back return - + # not applicable when timeout is set directions = story.getCurrentDirections() for direction in directions: @@ -805,78 +806,78 @@ class Diversion(object): if now - story.lastMsgFinishTime < float(self.params['minTimeAfterMessage']): # not less than x sec after it return - - + + interval = float(self.params['interval']) if not self.params['fromLastMessage']: # (1) last spoken at all - + timeSince = story.timer.getElapsed('last_speech') if story.timer.hasMark('last_speech') else story.timer.getElapsed('start') if story.timer.hasMark('last_diversion_timeout') and story.timer.getElapsed('last_diversion_timeout') > timeSince: timeSince = story.timer.getElapsed('last_diversion_timeout') if timeSince < interval: return - + story.stats['diversions']['timeout_total'] += 1 else: if story.currentMessage is None: return - + # if story.currentMessage.timeoutDiversionCount + 1 - + if story.currentReply is not None: # still playing back # or somebody has spoken already (timeout only works on silences) return - + if now - story.lastMsgFinishTime < interval: return - + story.currentMessage.timeoutDiversionCount += 1 story.stats['diversions']['timeout_last'] += 1 - + # if we're still here, there's a match! story.logger.info(f"Diverge: Timeout {self.id} of {self.params['interval']}") story.stats['diversions']['timeout'] += 1 - + msg = story.get(self.params['msgId']) if msg is None: story.logger.critical(f"Not a valid message id for diversion: {self.params['msgId']}") return - + # fall back to the currentMessage to return on. # TODO: maybe, if not chapter is found, the diversion should be # blocked alltogether? self.returnMessage = story.getNextChapterForMsg(story.currentMessage, False) or story.currentMessage - - + + if self.params['returnAfterStrand']: # no direction is here, as this diversion triggers before a direction is taken self.createReturnDirectionsTo(story, msg, self.returnMessage, inheritTiming=False) - + await story.setCurrentMessage(msg, allowReplyInterrupt=True) story.currentDiversion = self story.timer.setMark('last_diversion_timeout') return True - + async def _returnAfterTimeout(self, story): story.logger.info(f"Finalise diversion: {self.id}") - + async def _divergeIfInterrupted(self, story, msgFrom, msgTo, direction): """ This is here as a placeholder for the interruption diversion. These will however be triggered differently """ - + if story.currentDiversion or story.allowReplyInterrupt: return False - + msg = story.get(self.params['msgId']) if msg is None: story.logger.critical(f"Not a valid message id for diversion: {self.params['msgId']}") return - + self.returnMessage = story.currentMessage # no direction is here, as this diversion triggers before a direction is taken self.createReturnDirectionsTo(story, msg, self.returnMessage, inheritTiming=False, timeoutDuration=3, replyContainsDurations = [{ @@ -885,7 +886,7 @@ class Diversion(object): }]) await story.setCurrentMessage(msg) story.currentDiversion = self - + return True @@ -909,37 +910,37 @@ class Stopwatch(object): t = time.time() if self.paused_at != 0: pause_duration = t - self.paused_at - else: + else: pause_duration = 0 return t - self.marks[since_mark] - pause_duration - + def pause(self): self.paused_at = time.time() self.isRunning.clear() - + def resume(self): if self.paused_at == 0: return - + pause_duration = time.time() - self.paused_at for m in self.marks: self.marks[m] += pause_duration - + self.paused_at = 0 self.isRunning.set() - + def reset(self): self.marks = {} self.setMark('start') self.paused_at = 0 self.isRunning.set() - + def setMark(self, name): self.marks[name] = time.time() - + def hasMark(self, name): return name in self.marks - + def clearMark(self, name): if name in self.marks: self.marks.pop(name) @@ -968,15 +969,15 @@ class Story(object): self.diversions = [] self.interruptionDiversions = [] self.variables = {} - + def pause(self): self.logger.debug('pause hugvey') self.timer.pause() - + def resume(self): self.logger.debug('resume hugvey') self.timer.resume() - + def getLogSummary(self): summary = { # e[0]: the entity, e[1]: the logged time @@ -986,33 +987,33 @@ class Story(object): } # print(self.log) return summary - + def getLogCounts(self): return { 'messages': len([1 for e in self.log if isinstance(e[0], Message)]), 'diversions': len([1 for e in self.log if isinstance(e[0], Diversion)]), } - + def registerVariable(self, variableName, message): if variableName not in self.variables: self.variables[variableName] = [message] else: self.variables[variableName].append(message) - + def setVariableValue(self, name, value): if name not in self.variables: self.logger.warn(f"Set variable that is not needed in the story: {name}") else: self.logger.debug(f"Set variable {name} to {value}") - + self.variableValues[name] = value - + if name not in self.variables: return - + for message in self.variables[name]: message.setVariable(name, value) - + def hasVariableSet(self, name) -> bool: return name in self.variableValues and self.variableValues is not None @@ -1046,7 +1047,7 @@ class Story(object): # self.logger.debug(self.elements) # self.logger.debug(self.directionsPerMsg) - + self.diversions = [el for el in self.elements.values() if type(el) == Diversion] self.interruptionDiversions = [el for el in self.elements.values() if type(el) == Diversion and el.type == 'interrupt'] @@ -1058,17 +1059,17 @@ class Story(object): else: self.logger.warn( "Could not reinstatiate current message. Starting over") - + # Register variables for msg in self.getMessages(): # print(msg.id, msg.hasVariables()) if not msg.hasVariables(): continue - + for var in msg.variables: self.registerVariable(var, msg) - - + + self.logger.info(f'has variables: {self.variables}') self.logger.info(f'has {len(self.strands)} strands: {self.strands}') self.calculateFinishesForStrands() @@ -1084,13 +1085,13 @@ class Story(object): self.lastSpeechEndTime = None self.variableValues = {} # captured variables from replies self.finish_time = False - + self.events = [] # queue of received events self.commands = [] # queue of commands to send self.log = [] # all nodes/elements that are triggered self.msgLog = [] self.currentReply = None - + self.stats = { 'timeouts': 0, 'silentTimeouts': 0, @@ -1104,7 +1105,7 @@ class Story(object): 'timeout_last': 0 } } - + for msg in self.getMessages(): pass @@ -1124,7 +1125,7 @@ class Story(object): self.startMessage = obj if obj.isStrandStart: self.strands[obj.id] = [] - + if type(obj) == Direction: if obj.msgFrom.id not in self.directionsPerMsg: self.directionsPerMsg[obj.msgFrom.id] = [] @@ -1137,15 +1138,15 @@ class Story(object): if id in self.elements: return self.elements[id] return None - + def getMessages(self): return [el for el in self.elements.values() if type(el) == Message] - + def stop(self): self.logger.info("Stop Story") if self.isRunning: self.isRunning = False - + def shutdown(self): self.stop() self.hugvey = None @@ -1163,25 +1164,25 @@ class Story(object): # that is, until we have a 'reset' or 'start' event. # reinitiate current message await self.setCurrentMessage(self.currentMessage) - + if e['event'] == "playbackStart": if e['msgId'] != self.currentMessage.id: continue self.lastMsgStartTime = self.timer.getElapsed() self.logger.debug("Start playback") - - + + if e['event'] == "playbackFinish": if e['msgId'] == self.currentMessage.id: #TODO: migrate value to Messagage instead of Story self.lastMsgFinishTime = self.timer.getElapsed() self.hugvey.eventLogger.info(f"message: {self.currentMessage.id} {self.currentMessage.uuid} done") - + # 2019-02-22 temporary disable listening while playing audio: # if self.hugvey.google is not None: # self.logger.warn("Temporary 'fix' -> resume recording?") # self.hugvey.google.resume() - + if self.currentMessage.id not in self.directionsPerMsg: # print(self.currentDiversion) # if self.currentDiversion is not None: @@ -1194,7 +1195,7 @@ class Story(object): if e['event'] == 'speech': # participants speaks, reset counter self.stats['consecutiveSilentTimeouts'] = 0 - + # if self.currentMessage and not self.lastMsgStartTime: if self.currentMessage and not self.lastMsgFinishTime: # Ignore incoming speech events until we receive a 'playbackStart' event. @@ -1212,27 +1213,27 @@ class Story(object): # else: self.logger.info("ignore speech during playing message") continue - - + + # log if somebody starts speaking if self.currentReply is None: self.logger.info("Start speaking") self.currentReply= Reply(self.currentMessage) - + now = self.timer.getElapsed() utterance = self.currentReply.getActiveUtterance(now) utterance.setText(e['transcript'], now) self.hugvey.eventLogger.info("speaking: content {} \"{}\"".format(id(utterance), e['transcript'])) self.timer.setMark('last_speech') - + if e['is_final']: utterance.setFinished(self.timer.getElapsed()) self.hugvey.eventLogger.info("speaking: stop {}".format(id(utterance))) - + if self.hugvey.recorder: self.hugvey.recorder.updateTranscription(self.currentReply.getText()) - - + + async def _processDirections(self, directions): ':type directions: list(Direction)' chosenDirection = None @@ -1250,9 +1251,9 @@ class Story(object): self.addToLog(direction) self.currentMessage.setFinished(self.timer.getElapsed()) chosenDirection = direction - + isDiverging = await self._processDiversions(chosenDirection) - + allowReplyInterrupt = False # in some cases, conditions should be allowed to interrupt the reply if metCondition: @@ -1260,15 +1261,15 @@ class Story(object): allowReplyInterrupt = True if metCondition.usedContainsDuration is not None and metCondition.usedContainsDuration < 0.1: allowReplyInterrupt = True - + if not isDiverging and chosenDirection: if chosenDirection.isDiversionReturn and self.currentDiversion: await self.currentDiversion.finalise(self) - + await self.setCurrentMessage(chosenDirection.msgTo, allowReplyInterrupt=allowReplyInterrupt) - + return chosenDirection - + async def _processDiversions(self, direction: None) -> bool: """ Process the diversions on stack. If diverging, return True, else False @@ -1276,7 +1277,7 @@ class Story(object): Else, they are None """ diverge = False - + activeDiversions = [] activeTimeoutDiv = None activeTimeoutLastDiv = None @@ -1286,7 +1287,7 @@ class Story(object): if diversion.disabled or diversion.hasHit or diversion.type == 'interrupt': # interruptions are triggered somewhere else. continue - + if diversion.type == 'timeout': if diversion.params['timesOccured'] > 0: if not diversion.params['fromLastMessage']: @@ -1297,35 +1298,35 @@ class Story(object): if not activeTimeoutLastDiv or activeTimeoutLastDiv.params['timesOccured'] > diversion.params['timesOccured']: activeTimeoutLastDiv = diversion continue - + if diversion.type == 'no_response': if diversion.params['timesOccured'] > 0: if not activeNoResponseDiv or activeNoResponseDiv.params['timesOccured'] > diversion.params['timesOccured']: activeNoResponseDiv = diversion continue - + activeDiversions.append(diversion) - + if activeTimeoutDiv: activeDiversions.append(activeTimeoutDiv) if activeTimeoutLastDiv: activeDiversions.append(activeTimeoutLastDiv) if activeNoResponseDiv: activeDiversions.append(activeNoResponseDiv) - + for diversion in activeDiversions: # TODO: collect diversions and order by times + timesOccured (for timeout & no_response) d = await diversion.divergeIfNeeded(self, direction) if d: diverge = True return diverge - + def addToLog(self, node): self.log.append((node, self.timer.getElapsed())) - + if isinstance(node, Message): self.msgLog.append(node) - + if self.hugvey.recorder: if isinstance(node, Message): self.hugvey.recorder.log('hugvey', node.text, node.id) @@ -1333,7 +1334,7 @@ class Story(object): self.hugvey.recorder.log('diversion',node.id) if isinstance(node, Condition): self.hugvey.recorder.log('condition',node.logInfo, node.id) - + def logHasMsg(self, node): return node in self.msgLog @@ -1347,13 +1348,13 @@ class Story(object): while self.isRunning: if self.isRunning is False: break - + # pause on timer paused await self.timer.isRunning.wait() # wait for un-pause for i in range(len(self.events)): await self._processPendingEvents() - + # Test stability of Central Command with deliberate crash # if self.timer.getElapsed() > 5: # raise Exception('test') @@ -1362,7 +1363,7 @@ class Story(object): directions = self.getCurrentDirections() await self._processDirections(directions) - + # TODO create timer event # self.commands.append({'msg':'TEST!'}) @@ -1385,25 +1386,25 @@ class Story(object): 'action': 'stop', 'id': self.currentMessage.id, }) - - + + message.uuid = shortuuid.uuid() self.currentMessage = message self.lastMsgTime = time.time() self.lastMsgFinishTime = None # to be filled in by the event self.lastMsgStartTime = None # to be filled in by the event self.allowReplyInterrupt = allowReplyInterrupt - + # if not reset: self.previousReply = self.currentReply # we can use this for interrptions self.currentReply = useReply #self.currentMessage.reply - + # send command to already mute mic self.hugvey.sendCommand({ 'action': 'prepare', 'id': message.id }) - + # else: # # if we press 'save & play', it should not remember it's last reply to that msg # self.previousReply = self.currentReply # we can use this for interrptions @@ -1413,16 +1414,13 @@ class Story(object): message.id, message.getText())) self.addToLog(message) self.hugvey.eventLogger.info(f"message: {message.id} {message.uuid} start \"{message.getText()}\"") - + # TODO: prep events & timer etc. fn = await message.getAudioFilePath() - + # get duration of audio file, so the client can detect a hang of 'play' try: - with wave.open(fn,'r') as fp: - frames = fp.getnframes() - rate = fp.getframerate() - duration = frames/float(rate) + duration = sox.file_info.duration(fn) except Exception as e: self.hugvey.eventLogger.critical(f"error: crash when reading wave file: {fn}") self.logger.critical(f"error: crash when reading wave file: {fn}") @@ -1436,7 +1434,7 @@ class Story(object): 'params': message.getParams(), 'duration': duration }) - + # 2019-02-22 temporary disable listening while playing audio: # if self.hugvey.google is not None: # self.logger.warn("Temporary 'fix' -> stop recording") @@ -1447,7 +1445,7 @@ class Story(object): for direction in self.getCurrentDirections(): conditions = [c.id for c in direction.conditions] logmsg += "\n- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions) - + self.logger.log(LOG_BS,logmsg) def getCurrentDirections(self): @@ -1455,19 +1453,19 @@ class Story(object): return [] else: return self.directionsPerMsg[self.currentMessage.id] - + def getNextChapterForMsg(self, msg, canIncludeSelf = True, depth = 0): if canIncludeSelf and msg.chapterStart: self.logger.info(f"Next chapter: {msg.id}") return msg - + if depth >= 70: # protection against infinite loop? return None - + if msg.id not in self.directionsPerMsg: return None - + for direction in self.directionsPerMsg[msg.id]: r = self.getNextChapterForMsg(direction.msgTo, True, depth+1) if r: @@ -1486,13 +1484,13 @@ class Story(object): startMsg = self.startMessage await self.setCurrentMessage(startMsg) await self._renderer() - + def isFinished(self): if hasattr(self, 'finish_time') and self.finish_time: return time.time() - self.finish_time - + return False - + def _finish(self): """ Finish story and set hugvey to the right state @@ -1500,7 +1498,7 @@ class Story(object): self.finish() #stop google etc: self.hugvey.available() - + def finish(self): """ Finish only the story @@ -1510,50 +1508,50 @@ class Story(object): self.stop() self.finish_time = time.time() self.timer.pause() - + def calculateFinishesForMsg(self, msgId, depth = 0): if not msgId in self.directionsPerMsg or len(self.directionsPerMsg[msgId]) < 1: # is finish return [msgId] - + if depth > 40: return [] - + finishes = [] for d in self.directionsPerMsg[msgId]: if d.msgTo.id == msgId: continue finishes.extend(self.calculateFinishesForMsg(d.msgTo.id, depth+1)) - + # de-duplicate before returning return list(set(finishes)) - + def calculateFinishesForStrands(self): for startMsgId in self.strands: msg = self.get(startMsgId) #: :type msg: Message if msg.isStart: # ignore for the beginning continue - + self.logger.log(LOG_BS, f"Get finishes for {startMsgId}") self.strands[startMsgId] = self.calculateFinishesForMsg(startMsgId) - + self.logger.log(LOG_BS, f"Finishes: {self.strands}") - + def getFinishesForMsg(self, msg): """ Find the end of strands - + Most often they will be 'start's so to speed up these are pre-calculated Others can be calculated on the spot - + returns message ids """ if msg.id in self.strands: return self.strands[msg.id] - + return self.calculateFinishesForMsg(msg.id) - + def getDefaultDirectionForMsg(self, msg): """ There is only a default direction (for reply contains diversion) if it has @@ -1562,10 +1560,10 @@ class Story(object): if not msg.id in self.directionsPerMsg: # is finish return None - + if len(self.directionsPerMsg[msg.id]) > 1: return None - + # TODO: should the direction have at least a timeout condition set, or not perse? - - return self.directionsPerMsg[msg.id][0] \ No newline at end of file + + return self.directionsPerMsg[msg.id][0] diff --git a/install_server.sh b/install_server.sh index d009e02..dc4a2bb 100755 --- a/install_server.sh +++ b/install_server.sh @@ -1,6 +1,6 @@ apt-get update -apt-get install -y munin-node bc supervisor libsox-fmt-pulse +apt-get install -y munin-node bc supervisor cp installation/rpi-internal-temp /usr/share/munin/plugins ln -sf /usr/share/munin/plugins/rpi-internal-temp /etc/munin/plugins/rpi-internal-temp rm /etc/munin/plugins/irqstats diff --git a/pd/loopaudio.pd b/pd/loopaudio.pd index bf5df1e..34732d3 100644 --- a/pd/loopaudio.pd +++ b/pd/loopaudio.pd @@ -1,34 +1,51 @@ -#N canvas 223 291 679 478 10; -#X obj 155 392 dac~; -#X obj 155 342 readsf~; -#X msg 81 297 0; -#X obj 233 324 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 +#N canvas 200 136 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 252 322 (re-)start loop; -#X obj 239 60 oscparse; -#X obj 239 85 list trim; -#X obj 239 38 netreceive -u -b 5555; -#X obj 339 163 route 1; -#X obj 425 162 route 0; -#X obj 339 119 route trigger; -#X text 69 276 stop loop; -#X obj 469 216 print "starting loop"; -#X obj 470 239 print "stopping loop"; -#X msg 155 244 open testaudio.wav \, 1; -#X text 237 17 listen to osc at port 5555; -#X text 155 219 change audiofile HERE; +#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 +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 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 +1; +#X text 309 61 !STOP!; +#X text 422 60 Playing indicator; +#X msg 261 118 1; +#X msg 330 117 0; #X connect 1 0 0 0; #X connect 1 0 0 1; -#X connect 1 1 3 0; -#X connect 2 0 1 0; -#X connect 3 0 14 0; +#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 6 0 10 0; #X connect 7 0 5 0; -#X connect 8 0 3 0; -#X connect 8 0 12 0; -#X connect 9 0 2 0; -#X connect 9 0 13 0; -#X connect 10 0 8 0; -#X connect 10 0 9 0; -#X connect 14 0 1 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; diff --git a/pd/sendOSCMsg.py b/pd/sendOSCMsg.py index 6552643..488113d 100644 --- a/pd/sendOSCMsg.py +++ b/pd/sendOSCMsg.py @@ -25,7 +25,7 @@ while(1): print("sending start to {0} on port {1}".format(args.ip, args.port)) client.send_message("/trigger", 1) time.sleep(2) - print("sending stop to {0} on port {1}".format(args.ip, args.port)) + print("sending start to {0} on port {1}".format(args.ip, args.port)) client.send_message("/trigger", 0) time.sleep(2) diff --git a/server_config.yml b/server_config.yml index e39086c..2d6f3e7 100644 --- a/server_config.yml +++ b/server_config.yml @@ -7,7 +7,7 @@ voice: port: 4444 chunk: 2972 google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json" -hugveys: 25 +hugveys: 26 languages: - code: en-GB file: story_en.json @@ -28,7 +28,7 @@ languages: ms_lang: "fr-FR" web: port: 8888 - files_dir: "local/" + files_dir: "local/" light: ip: "192.168.178.15" - port: 7400 \ No newline at end of file + port: 7400 diff --git a/www/js/hugvey_timeline.js b/www/js/hugvey_timeline.js index 560ba49..4b9c94a 100644 --- a/www/js/hugvey_timeline.js +++ b/www/js/hugvey_timeline.js @@ -18,60 +18,60 @@ class Timeline{ {content: '.', start: new Date(), type: 'point', group: 1} ]); console.log('init timeline'); - + let groups = []; for(let hid = 1; hid<=this.count; hid++) { groups.push({id: parseInt(hid), content: 'Hugvey #'+hid}); this.eventDataSet.add({content: 'initiate', start: new Date(), type: 'point', group: parseInt(hid)}) } - + let dataGroups = new vis.DataSet(groups); let options = { // 'rollingMode': {'follow': true, 'offset': .8 } }; console.log('groups', dataGroups, groups, options); - + this.timeline = new vis.Timeline(this.el, this.eventDataSet, dataGroups, options); - + let tl = this.timeline; let startDate = new Date(); startDate.setMinutes(startDate.getMinutes()-1); let endDate = new Date(); endDate.setMinutes(endDate.getMinutes()+20); setTimeout(function(){ - tl.setWindow(startDate, endDate); + tl.setWindow(startDate, endDate); }, 500); this.moveInterval = setInterval(function(){ // skip movement if not visible tl.moveTo(new Date()); }, 1000); - + ws.addEventListener( 'message', this); } - + handleEvent(e) { console.log('handle', e, this); if(e.type == 'message') { this.wsOnMessage(e) } } - + wsOnMessage(e) { let msg = JSON.parse( e.data ); - + if ( typeof msg['action'] === 'undefined' ) { console.error( "not a valid message: " + e.data ); return; } - + if(msg['action'] != 'log') { return; } - + console.debug(msg, this); - + let hv_id = parseInt(msg['id']); // {'action': 'log', 'id':hugvey_id, 'type': items[0], 'info', 'args'} let d, parts; @@ -91,7 +91,7 @@ class Timeline{ this.eventDataSet.update(d); console.log('update', d); } else { - this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'}); + this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'}); } break; case 'speaking': @@ -101,7 +101,7 @@ class Timeline{ let id = parts.shift(); let content = parts.join(' '); let scId = 'sc-'+id+'-'+hv_id; - + if(info.startsWith('start')){ this.eventDataSet.add({content: info, start: new Date(), type: 'point', group: hv_id, 'className': 'speech'}); } @@ -115,7 +115,7 @@ class Timeline{ this.eventDataSet.update(d); } else { console.log('add'); - this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'}); + this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'}); } } if(info.startsWith('end')){ @@ -125,7 +125,7 @@ class Timeline{ this.eventDataSet.update(d); } } - + break; case 'story': // 'info': 'start'/'finished' @@ -147,4 +147,4 @@ class Timeline{ } } -var tl = new Timeline(ws, document.getElementById('line'), 25); +var tl = new Timeline(ws, document.getElementById('line'), 26);