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
logger.info("Received {}".format(cmd))
if cmd['action'] == 'show_yourself':
self.showMyself()
if cmd['action'] == 'prepare':
@ -269,7 +270,7 @@ class CommandHandler(object):
if cmd['action'] == 'play':
self.cmdPlay(cmd)
if cmd['action'] == 'stop':
self.cmdPlay(cmd, cmd['id'])
self.cmdStop(cmd['id'])
def cmdPlay(self, cmd):
self.muteMic = True
@ -317,7 +318,7 @@ class CommandHandler(object):
'msgId': msgId
})
out, err = self.playPopen.communicate()
returnCode = self.playPopen.returncode
returnCode = self.playPopen.returncode if self.playPopen else 0
logger.debug('finished')
self.playPopen = None
@ -353,7 +354,7 @@ class CommandHandler(object):
return
# 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()
def cmdStop(self, msgId):

View file

@ -46,6 +46,7 @@ class Message(object):
self.audioFile= None
self.filenameFetchLock = asyncio.Lock()
self.interruptCount = 0
self.timeoutDiversionCount = 0
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.params = {}
@ -445,9 +446,9 @@ class Diversion(object):
else:
self.regex = None
# if type == 'timeout':
# self.method = self._divergeIfNoResponse
# self.finaliseMethod = self._returnAfterNoResponse
if type == 'timeout':
self.method = self._divergeIfTimeout
self.finaliseMethod = self._returnAfterTimeout
if type == 'repeat':
self.method = self._divergeIfRepeatRequest
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)
"""
':type story: Story'
if story.currentDiversion:
if story.currentDiversion or not msgFrom or not msgTo:
return False
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)
"""
':type story: Story'
if story.currentDiversion:
if story.currentDiversion or not msgFrom or not msgTo:
# don't do nested diversions
# if we remove this, don't forget to double check 'returnMessage'
return False
@ -559,7 +560,8 @@ class Diversion(object):
"""
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.
@ -575,6 +577,67 @@ class Diversion(object):
await story.setCurrentMessage(msgFrom)
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 = {
'Msg': Message,
@ -624,6 +687,8 @@ class Stopwatch(object):
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:
@ -764,7 +829,9 @@ class Story(object):
'no_response': 0,
'repeat': 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.
# (if this setup doesn't work, try to test on self.lastMsgFinish time anyway)
# it keeps tricky with all these run conditions
self.logger.info("ignore speech while playing message")
continue
# message is still playing:
@ -882,6 +950,7 @@ class Story(object):
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())
@ -893,6 +962,7 @@ class Story(object):
async def _processDirections(self, directions):
':type directions: list(Direction)'
chosenDirection = None
for direction in directions:
for condition in direction.conditions:
if condition.isMet(self):
@ -904,14 +974,21 @@ class Story(object):
self.addToLog(condition)
self.addToLog(direction)
self.currentMessage.setFinished(self.timer.getElapsed())
isDiverging = await self._processDiversions(direction.msgFrom, direction.msgTo)
if not isDiverging:
await self.setCurrentMessage(direction.msgTo)
return direction
chosenDirection = direction
isDiverging = await self._processDiversions(
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:
"""
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
for diversion in self.diversions:

View file

@ -445,6 +445,13 @@ class Graph {
div['params']['returnAfterStrand'] = true;
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') {
div['params']['regex'] = "can you repeat that\\?";
} 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'){
divsRepeat.push(crel(
'div', {'class': 'diversion'},
@ -627,7 +719,7 @@ class Graph {
}
}
console.log(divsReplyContains, divsNoResponse, divsRepeat);
console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts);
let divEl = crel(
'div',
@ -673,6 +765,19 @@ class Graph {
},
'New case for repeat'
)
),
crel('div',
crel('h2', 'Timeouts'),
...divsTimeouts,
crel('div',
{
'class': 'btn',
'on': {
'click': (e) => this.createDiversion('timeout')
}
},
'New case for timeout'
)
)
);