Timeout diversion

This commit is contained in:
Ruben van de Ven 2019-04-24 16:09:41 +02:00
parent 331f5cf1d2
commit eeed9e3161
3 changed files with 199 additions and 16 deletions

View file

@ -262,6 +262,7 @@ class CommandHandler(object):
return return
logger.info("Received {}".format(cmd)) logger.info("Received {}".format(cmd))
if cmd['action'] == 'show_yourself': if cmd['action'] == 'show_yourself':
self.showMyself() self.showMyself()
if cmd['action'] == 'prepare': if cmd['action'] == 'prepare':
@ -269,7 +270,7 @@ class CommandHandler(object):
if cmd['action'] == 'play': if cmd['action'] == 'play':
self.cmdPlay(cmd) self.cmdPlay(cmd)
if cmd['action'] == 'stop': if cmd['action'] == 'stop':
self.cmdPlay(cmd, cmd['id']) self.cmdStop(cmd['id'])
def cmdPlay(self, cmd): def cmdPlay(self, cmd):
self.muteMic = True self.muteMic = True
@ -317,7 +318,7 @@ class CommandHandler(object):
'msgId': msgId 'msgId': msgId
}) })
out, err = self.playPopen.communicate() out, err = self.playPopen.communicate()
returnCode = self.playPopen.returncode returnCode = self.playPopen.returncode if self.playPopen else 0
logger.debug('finished') logger.debug('finished')
self.playPopen = None self.playPopen = None
@ -353,7 +354,7 @@ class CommandHandler(object):
return return
# prevent a lock of the story, no repeat or anything for now # prevent a lock of the story, no repeat or anything for now
logger.warning("Interrupting playback after timeout") logger.critical("Interrupting playback after timeout")
self.playPopen.terminate() self.playPopen.terminate()
def cmdStop(self, msgId): def cmdStop(self, msgId):

View file

@ -46,6 +46,7 @@ class Message(object):
self.audioFile= None self.audioFile= None
self.filenameFetchLock = asyncio.Lock() self.filenameFetchLock = asyncio.Lock()
self.interruptCount = 0 self.interruptCount = 0
self.timeoutDiversionCount = 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.params = {} self.params = {}
@ -445,9 +446,9 @@ class Diversion(object):
else: else:
self.regex = None self.regex = None
# if type == 'timeout': if type == 'timeout':
# self.method = self._divergeIfNoResponse self.method = self._divergeIfTimeout
# self.finaliseMethod = self._returnAfterNoResponse self.finaliseMethod = self._returnAfterTimeout
if type == 'repeat': if type == 'repeat':
self.method = self._divergeIfRepeatRequest self.method = self._divergeIfRepeatRequest
self.regex = re.compile(self.params['regex']) self.regex = re.compile(self.params['regex'])
@ -492,7 +493,7 @@ class Diversion(object):
Participant doesn't speak for x consecutive replies (has had timeout) Participant doesn't speak for x consecutive replies (has had timeout)
""" """
':type story: Story' ':type story: Story'
if story.currentDiversion: if story.currentDiversion or not msgFrom or not msgTo:
return False return False
if story.stats['diversions']['no_response'] + 1 == self.params['timesOccured'] and story.stats['consecutiveSilentTimeouts'] >= int(self.params['consecutiveSilences']): if story.stats['diversions']['no_response'] + 1 == self.params['timesOccured'] and story.stats['consecutiveSilentTimeouts'] >= int(self.params['consecutiveSilences']):
@ -521,7 +522,7 @@ class Diversion(object):
Participant doesn't speak for x consecutive replies (has had timeout) Participant doesn't speak for x consecutive replies (has had timeout)
""" """
':type story: Story' ':type story: Story'
if story.currentDiversion: if story.currentDiversion or not msgFrom or not msgTo:
# don't do nested diversions # don't do nested diversions
# if we remove this, don't forget to double check 'returnMessage' # if we remove this, don't forget to double check 'returnMessage'
return False return False
@ -559,7 +560,8 @@ class Diversion(object):
""" """
Participant asks if message can be repeated. 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. # TODO: how to handle this now we sometimes use different timings.
# Perhaps set isFinished when matching condition. # Perhaps set isFinished when matching condition.
@ -574,6 +576,67 @@ class Diversion(object):
story.stats['diversions']['repeat'] += 1 story.stats['diversions']['repeat'] += 1
await story.setCurrentMessage(msgFrom) await story.setCurrentMessage(msgFrom)
return True return True
async def _divergeIfTimeout(self, story, msgFrom, msgTo):
"""
(1) last spoken at all
(2) or duration for this last reply only
"""
if msgFrom or msgTo:
# not applicable a direction has been chosen
return
interval = float(self.params['interval'])
if not self.params['fromLastMessage']:
# (1) last spoken at all
if story.stats['diversions']['timeout_total'] + 1 != self.params['timesOccured']:
return
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.stats['diversions']['timeout_last'] + 1 != self.params['timesOccured']:
return
if story.lastMsgFinishTime is None or story.currentReply is not None:
# still playing back
# or somebody has spoken already (timeout only works on silences)
return
if time.time() - 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}")
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
self.returnMessage = story.currentMessage
await story.setCurrentMessage(msg)
story.currentDiversion = self
story.timer.setMark('last_diversion_timeout')
return True
async def _returnAfterTimeout(self, story):
story.logger.info(f"Finalise diversion: {self.id}")
if self.params['returnAfterStrand']:
await story.setCurrentMessage(self.returnMessage)
storyClasses = { storyClasses = {
@ -623,7 +686,9 @@ class Stopwatch(object):
def setMark(self, name): def setMark(self, name):
self.marks[name] = time.time() self.marks[name] = time.time()
def hasMark(self, name):
return name in self.marks
def clearMark(self, name): def clearMark(self, name):
if name in self.marks: if name in self.marks:
@ -764,7 +829,9 @@ class Story(object):
'no_response': 0, 'no_response': 0,
'repeat': 0, 'repeat': 0,
'reply_contains': 0, 'reply_contains': 0,
'timeout': 0 'timeout': 0,
'timeout_total': 0,
'timeout_last': 0
} }
} }
@ -860,6 +927,7 @@ class Story(object):
# messages that come in, in the case google is faster than our playbackFinish event. # messages that come in, in the case google is faster than our playbackFinish event.
# (if this setup doesn't work, try to test on self.lastMsgFinish time anyway) # (if this setup doesn't work, try to test on self.lastMsgFinish time anyway)
# it keeps tricky with all these run conditions # it keeps tricky with all these run conditions
self.logger.info("ignore speech while playing message")
continue continue
# message is still playing: # message is still playing:
@ -882,6 +950,7 @@ class Story(object):
utterance = self.currentReply.getActiveUtterance(now) utterance = self.currentReply.getActiveUtterance(now)
utterance.setText(e['transcript'], now) utterance.setText(e['transcript'], now)
self.hugvey.eventLogger.info("speaking: content {} \"{}\"".format(id(utterance), e['transcript'])) self.hugvey.eventLogger.info("speaking: content {} \"{}\"".format(id(utterance), e['transcript']))
self.timer.setMark('last_speech')
if e['is_final']: if e['is_final']:
utterance.setFinished(self.timer.getElapsed()) utterance.setFinished(self.timer.getElapsed())
@ -893,6 +962,7 @@ class Story(object):
async def _processDirections(self, directions): async def _processDirections(self, directions):
':type directions: list(Direction)' ':type directions: list(Direction)'
chosenDirection = None
for direction in directions: for direction in directions:
for condition in direction.conditions: for condition in direction.conditions:
if condition.isMet(self): if condition.isMet(self):
@ -904,14 +974,21 @@ class Story(object):
self.addToLog(condition) self.addToLog(condition)
self.addToLog(direction) self.addToLog(direction)
self.currentMessage.setFinished(self.timer.getElapsed()) self.currentMessage.setFinished(self.timer.getElapsed())
isDiverging = await self._processDiversions(direction.msgFrom, direction.msgTo) chosenDirection = direction
if not isDiverging:
await self.setCurrentMessage(direction.msgTo) isDiverging = await self._processDiversions(
return direction chosenDirection.msgFrom if chosenDirection else None,
chosenDirection.msgTo if chosenDirection else None)
if not isDiverging and chosenDirection:
await self.setCurrentMessage(chosenDirection.msgTo)
return chosenDirection
async def _processDiversions(self, msgFrom, msgTo) -> bool: async def _processDiversions(self, msgFrom, msgTo) -> bool:
""" """
Process the diversions on stack. If diverging, return True, else False Process the diversions on stack. If diverging, return True, else False
msgFrom and msgTo contain the source and target of a headed direction if given
Else, they are None
""" """
diverge = False diverge = False
for diversion in self.diversions: for diversion in self.diversions:

View file

@ -445,6 +445,13 @@ class Graph {
div['params']['returnAfterStrand'] = true; div['params']['returnAfterStrand'] = true;
div['params']['msgId'] = ""; div['params']['msgId'] = "";
} }
else if(type == 'timeout') {
div['params']['interval'] = 20;
div['params']['timesOccured'] = 0;
div['params']['fromLastMessage'] = false;
div['params']['returnAfterStrand'] = true;
div['params']['msgId'] = "";
}
else if(type == 'repeat') { else if(type == 'repeat') {
div['params']['regex'] = "can you repeat that\\?"; div['params']['regex'] = "can you repeat that\\?";
} else { } else {
@ -603,6 +610,91 @@ class Graph {
) )
)); ));
} }
if(div['type'] == 'timeout') {
let returnAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['returnAfterStrand'] = e.target.checked
}
}
if(div['params']['returnAfterStrand']) {
returnAttrs['checked'] = 'checked';
}
let totalOrLocalAttrs = {
'type': 'checkbox',
'on': {
'change': (e) => div['params']['fromLastMessage'] = e.target.checked
}
}
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) {
let optionParams = {};
if(div['params']['msgId'] == startMsg['@id']) {
optionParams['selected'] = 'selected';
}
msgOptions.push(crel('option', optionParams , startMsg['@id']));
}
divsTimeouts.push(crel(
'div', {
'class': 'diversion',
'on': {
'mouseover': function(e) {
if(div['params']['msgId'])
document.getElementById(div['params']['msgId']).classList.add('selectedMsg');
},
'mouseout': function(e) {
if(div['params']['msgId'])
document.getElementById(div['params']['msgId']).classList.remove('selectedMsg');
}
}
},
crel('h3', div['@id']),
crel(
'div', {
'class':'btn btn--delete',
'on': {
'click': (e) => this.deleteDiversion(div)
}
}, 'Delete diversion'),
crel('label', 'For last message only',
crel('input', totalOrLocalAttrs)
),
crel('label', 'Seconds of silence',
crel('input', {
'type': 'number',
'value': div['params']['interval'],
'precision': .1,
'on': {
'change': (e) => div['params']['interval'] = parseFloat(e.target.value)
}
})
),
crel('label', 'On n-th instance',
crel('input', {
'type': 'number',
'value': div['params']['timesOccured'],
'on': {
'change': (e) => div['params']['timesOccured'] = parseInt(e.target.value)
}
})
),
crel('label', 'Return to point of departure afterwards',
crel('input', returnAttrs)
),
crel('label', 'Go to (start message)',
crel('select', {'on': {
'change': (e) => div['params']['msgId'] = e.target.value
}}, ...msgOptions)
)
));
}
if(div['type'] == 'repeat'){ if(div['type'] == 'repeat'){
divsRepeat.push(crel( divsRepeat.push(crel(
'div', {'class': 'diversion'}, 'div', {'class': 'diversion'},
@ -627,7 +719,7 @@ class Graph {
} }
} }
console.log(divsReplyContains, divsNoResponse, divsRepeat); console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts);
let divEl = crel( let divEl = crel(
'div', 'div',
@ -673,6 +765,19 @@ class Graph {
}, },
'New case for repeat' 'New case for repeat'
) )
),
crel('div',
crel('h2', 'Timeouts'),
...divsTimeouts,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('timeout')
}
},
'New case for timeout'
)
) )
); );