Add prefetch of variables, and JSON saves are indented

This commit is contained in:
Ruben van de Ven 2019-02-26 21:27:38 +01:00
parent 3fff821e28
commit a798b3b638
4 changed files with 89 additions and 24 deletions

View file

@ -492,6 +492,6 @@ class HugveyState(object):
self.logger.info("Start audio stream") self.logger.info("Start audio stream")
await self.getStreamer().run() await self.getStreamer().run()
self.logger.warn("stream has left the building") self.logger.warn(f"stream has left the building from {self.ip}")
# if we end up here, the streamer finished, probably meaning hte hugvey shutdown # if we end up here, the streamer finished, probably meaning hte hugvey shutdown
self.gone() self.gone()

View file

@ -9,6 +9,7 @@ import time
import yaml import yaml
import zmq import zmq
from zmq.asyncio import Context from zmq.asyncio import Context
import sys
try: try:
import alsaaudio import alsaaudio

View file

@ -158,7 +158,7 @@ def getUploadHandler(central_command):
print(os.path.abspath(langFile)) print(os.path.abspath(langFile))
with open(langFile, 'w') as json_fp: with open(langFile, 'w') as json_fp:
logger.info(f'Save story to {langFile} {json_fp}') logger.info(f'Save story to {langFile} {json_fp}')
json.dump(storyData, json_fp) json.dump(storyData, json_fp, indent=2)
# Reload language files for new instances # Reload language files for new instances
central_command.loadLanguages() central_command.loadLanguages()

View file

@ -7,7 +7,6 @@ import urllib.parse
from .communication import LOG_BS from .communication import LOG_BS
from tornado.httpclient import AsyncHTTPClient, HTTPRequest from tornado.httpclient import AsyncHTTPClient, HTTPRequest
logger = logging.getLogger("narrative") logger = logging.getLogger("narrative")
class Utterance(object): class Utterance(object):
@ -27,7 +26,6 @@ class Utterance(object):
return self.endTime is not None return self.endTime is not None
class Message(object): class Message(object):
def __init__(self, id, text): def __init__(self, id, text):
self.id = id self.id = id
@ -36,11 +34,16 @@ class Message(object):
self.reply = None self.reply = None
# self.replyTime = None # self.replyTime = None
self.audioFile= None self.audioFile= None
self.filenameFetchLock = asyncio.Lock()
self.interruptCount = 0 self.interruptCount = 0
self.afterrunTime = 0. # the time after this message to allow for interrupts self.afterrunTime = 0. # the time after this message to allow for interrupts
self.finishTime = None # message can be finished without finished utterance (with instant replycontains) self.finishTime = None # message can be finished without finished utterance (with instant replycontains)
self.variableValues = {}
self.parseForVariables() self.parseForVariables()
def setStory(self, story):
self.story = story
@classmethod @classmethod
def initFromJson(message, data, story): def initFromJson(message, data, story):
msg = message(data['@id'], data['text']) msg = message(data['@id'], data['text'])
@ -48,6 +51,7 @@ class Message(object):
msg.afterrunTime = data['afterrun'] if 'afterrun' in data else 0. msg.afterrunTime = data['afterrun'] if 'afterrun' in data else 0.
if 'audio' in data: if 'audio' in data:
msg.audioFile = data['audio']['file'] msg.audioFile = data['audio']['file']
msg.setStory(story)
return msg return msg
def parseForVariables(self): def parseForVariables(self):
@ -55,10 +59,40 @@ class Message(object):
Find variables in text Find variables in text
""" """
self.variables = re.findall('\$(\w+)', self.text) self.variables = re.findall('\$(\w+)', self.text)
for var in self.variables:
self.variableValues[var] = None
def hasVariables(self) -> bool: def hasVariables(self) -> bool:
return len(self.variables) > 0 return len(self.variables) > 0
def setVariable(self, name, value):
if name not in self.variables:
logger.critical("Set nonexisting variable")
return
if self.variableValues[name] == value:
return
self.variableValues[name] = value
logger.warn(f"Set variable, now fetch {name}")
if not None in self.variableValues.values():
logger.warn(f"now fetch indeed {name}")
asyncio.get_event_loop().create_task(self.getAudioFilePath())
# asyncio.get_event_loop().call_soon_threadsafe(self.getAudioFilePath)
logger.warn(f"started {name}")
def getText(self):
# sort reverse to avoid replacing the wrong variable
self.variables.sort(key=len, reverse=True)
text = self.text
logger.info(f"Getting text for {self.id}")
logger.debug(self.variables)
for var in self.variables:
logger.debug(f"try replacing ${var} with {self.variableValues[var]} in {text}")
text = text.replace('$'+var, self.variableValues[var])
return text
def setReply(self, reply): def setReply(self, reply):
self.reply = reply self.reply = reply
@ -85,29 +119,34 @@ class Message(object):
return { return {
'id': self.id, 'id': self.id,
'time': None if self.reply is None else [u.startTime for u in self.reply.utterances], 'time': None if self.reply is None else [u.startTime for u in self.reply.utterances],
'text': self.getText(),
'replyText': None if self.reply is None else [u.text for u in self.reply.utterances] 'replyText': None if self.reply is None else [u.text for u in self.reply.utterances]
} }
async def getAudioFilePath(self,story): async def getAudioFilePath(self):
if self.audioFile is not None: if self.audioFile is not None:
return self.audioFile return self.audioFile
logger.warn(f"Fetching audio for {self.getText()}")
async with self.filenameFetchLock:
client = AsyncHTTPClient() client = AsyncHTTPClient()
queryString = urllib.parse.urlencode({ queryString = urllib.parse.urlencode({
'text': self.text, 'text': self.getText(),
'filename': 1, 'filename': 1,
'variable': 1 if self.hasVariables() else 0 'variable': 1 if self.hasVariables() else 0
}) })
request = HTTPRequest( request = HTTPRequest(
url = f"http://localhost:{story.panopticon_port}/voice?{queryString}", url = f"http://localhost:{self.story.panopticon_port}/voice?{queryString}",
method="GET" method="GET"
) )
logger.log(LOG_BS, request.url) logger.log(LOG_BS, request.url)
response = await client.fetch(request) response = await client.fetch(request)
if response.code != 200: if response.code != 200:
logger.critical(f"Error when fetching filename: {response.code} for {queryString}") logger.critical(f"Error when fetching filename: {response.code} for {queryString}")
return None return None
logger.warn(f"Fetched audio for {self.getText()}")
return response.body.decode().strip() return response.body.decode().strip()
@ -221,16 +260,19 @@ class Condition(object):
# Compile once, as we probably run it more than once # Compile once, as we probably run it more than once
self.vars['regexCompiled'] = re.compile(self.vars['regex']) self.vars['regexCompiled'] = re.compile(self.vars['regex'])
t = story.currentReply.getText().lower() t = r.getText().lower()
logger.log(LOG_BS, 'attempt regex: {} on {}'.format(self.vars['regex'], t)) logger.log(LOG_BS, 'attempt regex: {} on {}'.format(self.vars['regex'], t))
result = self.vars['regexCompiled'].search(t) result = self.vars['regexCompiled'].search(t)
if result is None: if result is None:
#if there is something to match, but not found, it's never ok #if there is something to match, but not found, it's never ok
return False return False
logger.debug('Got match on {}'.format(self.vars['regex'])) logger.debug('Got match on {}'.format(self.vars['regex']))
if 'instantMatch' in self.vars and self.vars['instantMatch'] or not r.isSpeaking():
# try to avoid setting variables for intermediate strings
results = result.groupdict() results = result.groupdict()
for captureGroup in results: for captureGroup in results:
story.variableValues[captureGroup] = results[captureGroup] story.setVariableValue(captureGroup, results[captureGroup])
if 'instantMatch' in self.vars and self.vars['instantMatch']: if 'instantMatch' in self.vars and self.vars['instantMatch']:
logger.info(f"Instant match on {self.vars['regex']}, {self.vars}") logger.info(f"Instant match on {self.vars['regex']}, {self.vars}")
@ -238,13 +280,14 @@ class Condition(object):
# TODO: implement 'instant match' -> don't wait for isFinished() # TODO: implement 'instant match' -> don't wait for isFinished()
if r.isSpeaking(): if r.isSpeaking():
logger.log(LOG_BS, "is speaking") logger.log(LOG_BS, f"is speaking: {r.getLastUtterance().text} - {r.getLastUtterance().startTime}")
return False return False
# print(self.vars) # print(self.vars)
# either there's a match, or nothing to match at all # either there's a match, or nothing to match at all
if 'delays' in self.vars: if 'delays' in self.vars:
if story.lastMsgFinishTime is None: if story.lastMsgFinishTime is None:
logger.debug("not finished playback yet")
return False return False
# time between finishing playback and ending of speaking: # time between finishing playback and ending of speaking:
replyDuration = r.getLastUtterance().endTime - story.lastMsgFinishTime replyDuration = r.getLastUtterance().endTime - story.lastMsgFinishTime
@ -257,6 +300,7 @@ class Condition(object):
return True return True
break # don't check other delays break # don't check other delays
# wait for delay to match # wait for delay to match
logger.debug("Wait for it...")
return False return False
# There is a match and no delay say, person finished speaking. Go ahead sir! # There is a match and no delay say, person finished speaking. Go ahead sir!
@ -428,6 +472,14 @@ class Story(object):
else: else:
self.variables[variableName].append(message) self.variables[variableName].append(message)
def setVariableValue(self, name, value):
if name not in self.variables:
logger.warn(f"Set variable that is not needed in the story: {name}")
self.variableValues[name] = value
for message in self.variables[name]:
message.setVariable(name, value)
def setStoryData(self, story_data): def setStoryData(self, story_data):
""" """
Parse self.data into a working story engine Parse self.data into a working story engine
@ -461,6 +513,17 @@ class Story(object):
logger.warn( logger.warn(
"Could not reinstatiate current message. Starting over") "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)
logger.info(f'has variables: {self.variables}')
def reset(self): def reset(self):
self.timer.reset() self.timer.reset()
# self.startTime = time.time() # self.startTime = time.time()
@ -505,8 +568,9 @@ class Story(object):
if id in self.elements: if id in self.elements:
return self.elements[id] return self.elements[id]
return None return None
def getMessages(self): def getMessages(self):
return [el for el in self.elements if type(el) == Message] return [el for el in self.elements.values() if type(el) == Message]
def stop(self): def stop(self):
logger.info("Stop Story") logger.info("Stop Story")
@ -642,7 +706,7 @@ class Story(object):
# TODO: preload file paths if no variables are set, or once these are loaded # TODO: preload file paths if no variables are set, or once these are loaded
self.hugvey.sendCommand({ self.hugvey.sendCommand({
'action': 'play', 'action': 'play',
'file': await message.getAudioFilePath(self), 'file': await message.getAudioFilePath(),
'id': message.id, 'id': message.id,
}) })