2019-01-18 12:42:50 +01:00
import json
import time
import logging
2019-01-28 17:26:48 +01:00
import re
2019-01-18 12:42:50 +01:00
import asyncio
2019-02-18 20:38:54 +01:00
import urllib . parse
2019-02-11 21:28:48 +01:00
from . communication import LOG_BS
2019-02-18 20:38:54 +01:00
from tornado . httpclient import AsyncHTTPClient , HTTPRequest
2019-03-27 13:36:09 +01:00
import uuid
import shortuuid
2019-04-01 16:36:34 +02:00
import threading
import faulthandler
from zmq . asyncio import Context
import zmq
2019-04-24 11:31:20 +02:00
import wave
2019-05-11 17:31:00 +02:00
import sox
2019-04-25 11:12:27 +02:00
from pythonosc import udp_client
2019-05-01 18:27:10 +02:00
import random
2019-05-11 23:34:06 +02:00
import pickle
import os
import traceback
2019-01-18 12:42:50 +01:00
2019-03-23 18:18:52 +01:00
mainLogger = logging . getLogger ( " hugvey " )
logger = mainLogger . getChild ( " narrative " )
2019-01-18 12:42:50 +01:00
2019-02-11 21:28:48 +01:00
class Utterance ( object ) :
""" Part of a reply """
def __init__ ( self , startTime ) :
self . startTime = startTime
self . endTime = None
self . text = " "
2019-04-10 18:46:15 +02:00
self . lastUpdateTime = startTime
2019-05-11 17:31:00 +02:00
2019-04-02 17:32:01 +02:00
def setText ( self , text , now ) :
2019-02-11 21:28:48 +01:00
self . text = text
2019-04-10 18:46:15 +02:00
self . lastUpdateTime = now
2019-05-14 18:18:42 +02:00
def hasText ( self ) :
return len ( self . text ) > 0
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def setFinished ( self , endTime ) :
self . endTime = endTime
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def isFinished ( self ) :
return self . endTime is not None
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get utterance {self}')
state = self . __dict__ . copy ( )
return state
2019-01-22 08:59:45 +01:00
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
class Message ( object ) :
def __init__ ( self , id , text ) :
self . id = id
self . text = text
self . isStart = False
2019-05-01 12:37:35 +02:00
self . isStrandStart = False
2019-04-26 11:14:49 +02:00
self . chapterStart = False
2019-01-18 12:42:50 +01:00
self . reply = None
2019-02-11 21:28:48 +01:00
# self.replyTime = None
2019-01-25 10:43:55 +01:00
self . audioFile = None
2019-02-26 21:27:38 +01:00
self . filenameFetchLock = asyncio . Lock ( )
2019-02-11 21:28:48 +01:00
self . interruptCount = 0
2019-04-24 16:09:41 +02:00
self . timeoutDiversionCount = 0
2019-02-11 21:28:48 +01:00
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)
2019-02-28 18:58:03 +01:00
self . params = { }
2019-02-26 21:27:38 +01:00
self . variableValues = { }
2019-02-14 08:39:31 +01:00
self . parseForVariables ( )
2019-03-27 13:36:09 +01:00
self . uuid = None # Have a unique id each time the message is played back.
2019-04-25 11:12:27 +02:00
self . color = None
2019-06-08 16:10:46 +02:00
self . lightChange = None
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
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.
# print(f'get msg {self.id}')
state = self . __dict__ . copy ( )
# Remove the unpicklable entries.
del state [ ' filenameFetchLock ' ]
return state
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __setstate__ ( self , state ) :
self . __dict__ . update ( state )
self . filenameFetchLock = asyncio . Lock ( )
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
def setStory ( self , story ) :
self . story = story
2019-03-23 18:18:52 +01:00
self . logger = story . logger . getChild ( " message " )
2019-01-18 12:42:50 +01:00
@classmethod
def initFromJson ( message , data , story ) :
2019-01-22 08:59:45 +01:00
msg = message ( data [ ' @id ' ] , data [ ' text ' ] )
2019-03-07 20:19:43 +01:00
msg . isStart = data [ ' beginning ' ] if ' beginning ' in data else False
2019-05-01 12:37:35 +02:00
msg . isStrandStart = data [ ' start ' ] if ' start ' in data else False
2019-04-26 11:14:49 +02:00
msg . chapterStart = bool ( data [ ' chapterStart ' ] ) if ' chapterStart ' in data else False
2019-02-11 21:28:48 +01:00
msg . afterrunTime = data [ ' afterrun ' ] if ' afterrun ' in data else 0.
2019-04-25 11:12:27 +02:00
msg . color = data [ ' color ' ] if ' color ' in data else None
2019-04-15 20:57:32 +02:00
if ' audio ' in data and data [ ' audio ' ] is not None :
2019-01-25 10:43:55 +01:00
msg . audioFile = data [ ' audio ' ] [ ' file ' ]
2019-02-26 21:27:38 +01:00
msg . setStory ( story )
2019-02-28 18:58:03 +01:00
if ' params ' in data :
msg . params = data [ ' params ' ]
2019-04-08 17:35:10 +02:00
if not ' vol ' in msg . params :
# prevent clipping on some Lyrebird tracks
msg . params [ ' vol ' ] = .8
2019-06-08 16:10:46 +02:00
msg . lightChange = data [ ' light ' ] if ' light ' in data else None
2019-05-12 19:51:54 +02:00
2019-05-12 14:54:37 +02:00
msg . params [ ' vol ' ] = float ( msg . params [ ' vol ' ] )
2019-05-12 19:51:54 +02:00
2019-01-22 08:59:45 +01:00
return msg
2019-05-11 17:31:00 +02:00
2019-02-14 08:39:31 +01:00
def parseForVariables ( self ) :
"""
Find variables in text
"""
self . variables = re . findall ( ' \ $( \ w+) ' , self . text )
2019-02-26 21:27:38 +01:00
for var in self . variables :
self . variableValues [ var ] = None
2019-05-11 17:31:00 +02:00
2019-02-18 20:38:54 +01:00
def hasVariables ( self ) - > bool :
return len ( self . variables ) > 0
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
def setVariable ( self , name , value ) :
if name not in self . variables :
2019-03-23 18:18:52 +01:00
self . logger . critical ( " Set nonexisting variable " )
2019-02-26 21:27:38 +01:00
return
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
if self . variableValues [ name ] == value :
return
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
self . variableValues [ name ] = value
2019-05-11 17:31:00 +02:00
2019-04-17 11:58:40 +02:00
self . logger . warn ( f " Set variable, fetch { name } " )
2019-02-26 21:27:38 +01:00
if not None in self . variableValues . values ( ) :
2019-04-17 11:58:40 +02:00
self . logger . warn ( f " now fetch { name } " )
2019-02-26 21:27:38 +01:00
asyncio . get_event_loop ( ) . create_task ( self . getAudioFilePath ( ) )
# asyncio.get_event_loop().call_soon_threadsafe(self.getAudioFilePath)
2019-03-27 13:36:09 +01:00
self . logger . warn ( f " started { name } " )
2019-02-26 21:27:38 +01:00
def getText ( self ) :
# sort reverse to avoid replacing the wrong variable
self . variables . sort ( key = len , reverse = True )
text = self . text
2019-04-17 11:58:40 +02:00
# self.logger.debug(f"Getting text for {self.id}")
2019-02-26 21:27:38 +01:00
for var in self . variables :
2019-03-23 18:18:52 +01:00
self . logger . debug ( f " try replacing $ { var } with { self . variableValues [ var ] } in { text } " )
2019-05-13 14:45:52 +02:00
replacement = self . variableValues [ var ] if ( self . variableValues [ var ] is not None ) else self . story . configuration . nothing_text #TODO: translate nothing to each language
2019-02-28 18:58:03 +01:00
text = text . replace ( ' $ ' + var , replacement )
2019-02-26 21:27:38 +01:00
return text
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def setReply ( self , reply ) :
self . reply = reply
2019-01-18 12:42:50 +01:00
def hasReply ( self ) :
return self . reply is not None
def getReply ( self ) :
2019-02-11 21:28:48 +01:00
if not self . hasReply ( ) :
2019-01-22 08:59:45 +01:00
raise Exception (
" Getting reply while there is none! {0} " . format ( self . id ) )
2019-01-18 12:42:50 +01:00
return self . reply
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def isFinished ( self ) :
return self . finishTime is not None
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def setFinished ( self , currentTime ) :
self . finishTime = currentTime
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def getFinishedTime ( self ) :
return self . finishTime
2019-05-11 17:31:00 +02:00
2019-02-28 18:58:03 +01:00
def getParams ( self ) :
return self . params
2019-01-18 12:42:50 +01:00
2019-01-25 14:10:19 +01:00
def getLogSummary ( self ) :
return {
' id ' : self . id ,
2019-02-11 21:28:48 +01:00
' time ' : None if self . reply is None else [ u . startTime for u in self . reply . utterances ] ,
2019-02-26 21:27:38 +01:00
' text ' : self . getText ( ) ,
2019-02-11 21:28:48 +01:00
' replyText ' : None if self . reply is None else [ u . text for u in self . reply . utterances ]
2019-01-25 14:10:19 +01:00
}
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
async def getAudioFilePath ( self ) :
2019-02-24 21:38:08 +01:00
if self . audioFile is not None :
return self . audioFile
2019-05-11 17:31:00 +02:00
2019-04-01 16:36:34 +02:00
text = self . getText ( )
self . logger . debug ( f " Fetching audio for { text } " )
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
# return "test";
2019-02-26 21:27:38 +01:00
async with self . filenameFetchLock :
2019-04-01 16:36:51 +02:00
# print(threading.enumerate())
2019-05-11 17:31:00 +02:00
2019-04-01 16:36:34 +02:00
info = {
' text ' : text ,
' variable ' : True if self . hasVariables ( ) else False
}
s = Context . instance ( ) . socket ( zmq . REQ ) #: :type s: zmq.sugar.Socket
voiceAddr = f " ipc://voice { self . story . hugvey . id } "
s . connect ( voiceAddr )
await s . send_json ( info )
filename = await s . recv_string ( )
s . close ( )
2019-05-11 17:31:00 +02:00
2019-04-01 16:36:51 +02:00
# print(threading.enumerate())
2019-05-11 17:31:00 +02:00
2019-04-01 16:36:51 +02:00
self . logger . debug ( f " Fetched audio for { text } : { filename } " )
2019-04-01 16:36:34 +02:00
return filename
2019-01-25 14:10:19 +01:00
2019-01-18 12:42:50 +01:00
2019-05-11 23:34:06 +02:00
2019-02-11 21:28:48 +01:00
class Reply ( object ) :
def __init__ ( self , message : Message ) :
self . forMessage = None
self . utterances = [ ]
self . setForMessage ( message )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get reply {self}')
state = self . __dict__ . copy ( )
return state
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def setForMessage ( self , message : Message ) :
self . forMessage = message
message . setReply ( self )
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def getLastUtterance ( self ) - > Utterance :
if not self . hasUtterances ( ) :
return None
2019-04-02 17:32:01 +02:00
u = self . utterances [ - 1 ] #: :type u: Utterance
2019-05-11 17:31:00 +02:00
2019-04-02 17:32:01 +02:00
# attempt to fix a glitch that google does not always send is_finished
if u . isFinished ( ) :
return u
2019-05-11 17:31:00 +02:00
2019-04-02 17:32:01 +02:00
now = self . forMessage . story . timer . getElapsed ( )
2019-04-10 18:46:15 +02:00
diff = now - u . lastUpdateTime
2019-04-08 17:35:10 +02:00
if diff > 5 : # time in seconds to force silence in utterance
# useful for eg. 'hello', or 'no'
2019-04-02 17:32:01 +02:00
self . forMessage . story . logger . warn (
f " Set finish time for utterance after { diff } s { u . text } "
)
u . setFinished ( now )
2019-05-11 17:31:00 +02:00
2019-04-02 17:32:01 +02:00
return u
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def getFirstUtterance ( self ) - > Utterance :
if not self . hasUtterances ( ) :
return None
return self . utterances [ 0 ]
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def hasUtterances ( self ) - > bool :
return len ( self . utterances ) > 0
def addUtterance ( self , utterance : Utterance ) :
self . utterances . append ( utterance )
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def getText ( self ) - > str :
return " . " . join ( [ u . text for u in self . utterances ] )
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
def getActiveUtterance ( self , currentTime ) - > Utterance :
"""
If no utterance is active , create a new one . Otherwise return non - finished utterance for update
"""
if len ( self . utterances ) < 1 or self . getLastUtterance ( ) . isFinished ( ) :
u = Utterance ( currentTime )
self . addUtterance ( u )
else :
u = self . getLastUtterance ( )
return u
def isSpeaking ( self ) :
u = self . getLastUtterance ( )
if u is not None and not u . isFinished ( ) :
return True
return False
2019-05-11 17:31:00 +02:00
2019-05-07 14:30:57 +02:00
def getTimeSinceLastUtterance ( self ) :
if not self . hasUtterances ( ) :
return None
2019-05-11 17:31:00 +02:00
2019-05-07 14:30:57 +02:00
return self . forMessage . story . timer . getElapsed ( ) - self . getLastUtterance ( ) . lastUpdateTime
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
class Condition ( object ) :
"""
A condition , basic conditions are built in , custom condition can be given by
providing a custom method .
"""
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
def __init__ ( self , id ) :
self . id = id
self . method = None
2019-04-25 11:12:27 +02:00
self . type = None
2019-01-18 12:42:50 +01:00
self . vars = { }
2019-04-27 15:33:51 +02:00
self . logInfo = None
2019-05-01 18:27:10 +02:00
self . originalJsonString = None
self . usedContainsDuration = None
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get condition {self.id}')
state = self . __dict__ . copy ( )
return state
2019-01-18 12:42:50 +01:00
@classmethod
def initFromJson ( conditionClass , data , story ) :
2019-01-22 08:59:45 +01:00
condition = conditionClass ( data [ ' @id ' ] )
2019-04-25 11:12:27 +02:00
condition . type = data [ ' type ' ]
2019-05-01 18:27:10 +02:00
condition . originalJsonString = json . dumps ( data )
2019-06-16 17:44:13 +02:00
#: :type condition: Condition
2019-01-18 12:42:50 +01:00
# TODO: should Condition be subclassed?
if data [ ' type ' ] == " replyContains " :
condition . method = condition . _hasMetReplyContains
if data [ ' type ' ] == " timeout " :
condition . method = condition . _hasMetTimeout
2019-03-29 14:11:48 +01:00
if data [ ' type ' ] == " variable " :
condition . method = condition . _hasVariable
2019-04-26 11:51:07 +02:00
if data [ ' type ' ] == " diversion " :
condition . method = condition . _hasDiverged
2019-06-16 17:44:13 +02:00
if data [ ' type ' ] == " messagePlayed " :
condition . method = condition . _hasPlayed
2019-06-13 22:40:32 +02:00
if data [ ' type ' ] == " variableEquals " :
condition . method = condition . _variableEquals
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
if ' vars ' in data :
condition . vars = data [ ' vars ' ]
2019-01-22 08:59:45 +01:00
return condition
2019-01-18 12:42:50 +01:00
def isMet ( self , story ) :
"""
Validate if condition is met for the current story state
"""
return self . method ( story )
def _hasMetTimeout ( self , story ) :
2019-02-15 12:26:56 +01:00
now = story . timer . getElapsed ( )
2019-01-18 12:42:50 +01:00
# check if the message already finished playing
if not story . lastMsgFinishTime :
return False
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
if ' onlyIfNoReply ' in self . vars and self . vars [ ' onlyIfNoReply ' ] :
2019-03-07 20:19:43 +01:00
if story . currentReply and story . currentReply is not None and story . currentReply . hasUtterances ( ) :
2019-03-23 18:18:52 +01:00
story . logger . log ( LOG_BS , f ' Only if no reply has text! { story . currentReply . getText ( ) } ' )
2019-02-15 12:26:56 +01:00
# 'onlyIfNoReply': only use this timeout if participants doesn't speak.
return False
# else:
2019-03-23 18:18:52 +01:00
# story.logger.debug('Only if no reply has no text yet!')
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
hasMetTimeout = now - story . lastMsgFinishTime > = float ( self . vars [ ' seconds ' ] )
if not hasMetTimeout :
return False
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
# update stats:
story . stats [ ' timeouts ' ] + = 1
2019-04-02 08:54:26 +02:00
if ' needsReply ' in self . vars and self . vars [ ' needsReply ' ] is True :
2019-03-27 15:43:02 +01:00
if story . currentReply is None or not story . currentReply . hasUtterances ( ) :
story . stats [ ' silentTimeouts ' ] + = 1
story . stats [ ' consecutiveSilentTimeouts ' ] + = 1
2019-05-11 17:31:00 +02:00
2019-04-27 15:33:51 +02:00
self . logInfo = " {} s " . format ( self . vars [ ' seconds ' ] )
2019-03-07 20:19:43 +01:00
return True
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
def _hasVariable ( self , story ) - > bool :
if not story . lastMsgFinishTime :
return False
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
r = story . hasVariableSet ( self . vars [ ' variable ' ] )
if r :
story . logger . debug ( f " Variable { self . vars [ ' variable ' ] } is set. " )
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
if ' notSet ' in self . vars and self . vars [ ' notSet ' ] :
# inverse:
r = not r
2019-04-27 15:33:51 +02:00
self . logInfo = " Does {} have variable {} " . format (
' not ' if ' notSet ' in self . vars and self . vars [ ' notSet ' ] else ' ' ,
self . vars [ ' variable ' ]
)
2019-03-29 14:11:48 +01:00
return r
2019-06-13 22:40:32 +02:00
def _variableEquals ( self , story ) - > bool :
v1 = story . variableValues [ self . vars [ ' variable1 ' ] ] if story . hasVariableSet ( self . vars [ ' variable1 ' ] ) else None
v2 = story . variableValues [ self . vars [ ' variable2 ' ] ] if story . hasVariableSet ( self . vars [ ' variable2 ' ] ) else None
if v1 :
story . logger . debug ( f " Variable { self . vars [ ' variable1 ' ] } : { v1 } " )
if v2 :
story . logger . debug ( f " Variable { self . vars [ ' variable2 ' ] } : { v2 } " )
if ' notEq ' in self . vars and self . vars [ ' notEq ' ] :
# inverse:
r = ( v1 != v2 )
else :
r = ( v1 == v2 )
story . logger . info ( " ' {} ' {} ' {} ' ( {} ) " . format ( v1 , ' == ' if v1 == v2 else ' != ' , v2 , r ) )
return r
2019-04-26 11:51:07 +02:00
def _hasDiverged ( self , story ) - > bool :
if not story . lastMsgFinishTime :
return False
2019-05-11 17:31:00 +02:00
2019-04-26 11:51:07 +02:00
d = story . get ( self . vars [ ' diversionId ' ] )
if not d :
story . logger . critical ( f " Condition on non-existing diversion: { self . vars [ ' diversionId ' ] } " )
2019-05-11 17:31:00 +02:00
2019-04-26 11:51:07 +02:00
r = d . hasHit
if r :
story . logger . debug ( f " Diversion { self . vars [ ' diversionId ' ] } has been hit. " )
2019-05-11 17:31:00 +02:00
2019-04-26 11:51:07 +02:00
if ' inverseMatch ' in self . vars and self . vars [ ' inverseMatch ' ] :
# inverse:
r = not r
2019-05-11 17:31:00 +02:00
2019-04-27 15:33:51 +02:00
self . logInfo = " Has {} diverged to {} " . format (
' not ' if ' inverseMatch ' in self . vars and self . vars [ ' inverseMatch ' ] else ' ' ,
self . vars [ ' diversionId ' ]
)
2019-05-11 17:31:00 +02:00
2019-04-26 11:51:07 +02:00
return r
2019-06-16 17:44:13 +02:00
def _hasPlayed ( self , story ) - > bool :
if not story . lastMsgFinishTime :
return False
msg = story . get ( self . vars [ ' msgId ' ] )
if not msg :
story . logger . critical ( f " Condition on non-existing message: { self . vars [ ' msgId ' ] } " )
#: :type msg: Message
r = msg . isFinished ( )
if r :
story . logger . debug ( f " Msg { self . vars [ ' msgId ' ] } has been played. " )
if ' inverseMatch ' in self . vars and self . vars [ ' inverseMatch ' ] :
# inverse:
r = not r
self . logInfo = " Has {} played msg {} " . format (
' not ' if ' inverseMatch ' in self . vars and self . vars [ ' inverseMatch ' ] else ' ' ,
self . vars [ ' msgId ' ]
)
return r
2019-01-18 12:42:50 +01:00
2019-02-11 21:28:48 +01:00
def _hasMetReplyContains ( self , story ) - > bool :
"""
Check the reply for specific characteristics :
- regex : regular expression . If empy , just way for isFinished ( )
- delays : an array of [ { ' minReplyDuration ' , ' waitTime ' } , . . . ]
- minReplyDuration : the nr of seconds the reply should take . Preferably have one with 0
- waitTime : the time to wait after isFinished ( ) before continuing
"""
r = story . currentReply # make sure we keep working with the same object
if not r or not r . hasUtterances ( ) :
2019-01-18 12:42:50 +01:00
return False
2019-04-17 11:58:40 +02:00
capturedVariables = None
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
if ' regex ' in self . vars and len ( self . vars [ ' regex ' ] ) :
2019-01-18 12:42:50 +01:00
if ' regexCompiled ' not in self . vars :
# Compile once, as we probably run it more than once
self . vars [ ' regexCompiled ' ] = re . compile ( self . vars [ ' regex ' ] )
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
t = r . getText ( ) . lower ( )
2019-03-23 18:18:52 +01:00
story . logger . log ( LOG_BS , ' attempt regex: {} on {} ' . format ( self . vars [ ' regex ' ] , t ) )
2019-02-11 21:28:48 +01:00
result = self . vars [ ' regexCompiled ' ] . search ( t )
2019-01-18 12:42:50 +01:00
if result is None :
2019-02-11 21:28:48 +01:00
#if there is something to match, but not found, it's never ok
2019-01-18 12:42:50 +01:00
return False
2019-03-23 18:18:52 +01:00
story . logger . debug ( ' Got match on {} ' . format ( self . vars [ ' regex ' ] ) )
2019-05-11 17:31:00 +02:00
2019-04-17 11:58:40 +02:00
capturedVariables = result . groupdict ( )
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
if ' instantMatch ' in self . vars and self . vars [ ' instantMatch ' ] :
2019-03-23 18:18:52 +01:00
story . logger . info ( f " Instant match on { self . vars [ ' regex ' ] } , { self . vars } " )
2019-04-27 15:33:51 +02:00
self . logInfo = " Instant match of {} , captured {} " . format (
self . vars [ ' regex ' ] ,
capturedVariables
)
2019-06-15 20:10:53 +02:00
# Set variables only when direction returns true
if capturedVariables is not None :
for captureGroup in capturedVariables :
story . setVariableValue ( captureGroup , capturedVariables [ captureGroup ] )
2019-02-15 12:26:56 +01:00
return True
2019-02-11 21:28:48 +01:00
# TODO: implement 'instant match' -> don't wait for isFinished()
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
# print(self.vars)
# either there's a match, or nothing to match at all
if ' delays ' in self . vars :
2019-02-15 12:26:56 +01:00
if story . lastMsgFinishTime is None :
2019-03-23 18:18:52 +01:00
story . logger . debug ( " not finished playback yet " )
2019-02-15 12:26:56 +01:00
return False
# time between finishing playback and ending of speaking:
2019-04-10 18:46:15 +02:00
replyDuration = r . getLastUtterance ( ) . lastUpdateTime - story . lastMsgFinishTime # using lastUpdateTime instead of endTime
2019-02-11 21:28:48 +01:00
delays = sorted ( self . vars [ ' delays ' ] , key = lambda k : float ( k [ ' minReplyDuration ' ] ) , reverse = True )
for delay in delays :
if replyDuration > float ( delay [ ' minReplyDuration ' ] ) :
2019-05-07 14:30:57 +02:00
timeSinceReply = r . getTimeSinceLastUtterance ( )
2019-03-23 18:18:52 +01:00
story . logger . log ( LOG_BS , f " check delay duration is now { replyDuration } , already waiting for { timeSinceReply } , have to wait { delay [ ' waitTime ' ] } " )
2019-02-11 21:28:48 +01:00
if timeSinceReply > float ( delay [ ' waitTime ' ] ) :
2019-04-17 11:58:40 +02:00
# if variables are captured, only set them the moment the condition matches
if capturedVariables is not None :
for captureGroup in capturedVariables :
story . setVariableValue ( captureGroup , capturedVariables [ captureGroup ] )
2019-04-27 15:33:51 +02:00
self . logInfo = " Match of {} , captured {} after, {} " . format (
self . vars [ ' regex ' ] ,
capturedVariables ,
2019-05-11 17:31:00 +02:00
timeSinceReply
2019-04-27 15:33:51 +02:00
)
2019-05-01 18:27:10 +02:00
self . usedContainsDuration = float ( delay [ ' waitTime ' ] )
2019-02-11 21:28:48 +01:00
return True
break # don't check other delays
# wait for delay to match
2019-04-10 18:46:15 +02:00
story . logger . log ( LOG_BS , " Wait for it... " )
return False
2019-05-11 17:31:00 +02:00
2019-04-10 18:46:15 +02:00
# 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 } " )
2019-01-18 12:42:50 +01:00
return False
2019-05-11 17:31:00 +02:00
2019-02-11 21:28:48 +01:00
# There is a match and no delay say, person finished speaking. Go ahead sir!
2019-04-27 15:33:51 +02:00
self . logInfo = " Match "
2019-02-11 21:28:48 +01:00
return True
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def getLogSummary ( self ) :
return {
' id ' : self . id
}
2019-01-18 12:42:50 +01:00
class Direction ( object ) :
"""
A condition based edge in the story graph
"""
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
def __init__ ( self , id , msgFrom : Message , msgTo : Message ) :
self . id = id
self . msgFrom = msgFrom
self . msgTo = msgTo
2019-03-07 20:19:43 +01:00
#: :type self.conditions: list(Condition)
2019-01-18 12:42:50 +01:00
self . conditions = [ ]
2019-01-25 14:10:19 +01:00
self . conditionMet = None
2019-05-01 12:37:35 +02:00
self . isDiversionReturn = False
2019-06-16 19:42:59 +02:00
self . diversionHasReturned = False # for isDiversionReturn.
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get direction {self.id}')
state = self . __dict__ . copy ( )
return state
2019-01-18 12:42:50 +01:00
def addCondition ( self , condition : Condition ) :
self . conditions . append ( condition )
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def setMetCondition ( self , condition : Condition ) :
self . conditionMet = condition
2019-01-18 12:42:50 +01:00
@classmethod
def initFromJson ( direction , data , story ) :
msgFrom = story . get ( data [ ' source ' ] )
msgTo = story . get ( data [ ' target ' ] )
2019-01-22 08:59:45 +01:00
direction = direction ( data [ ' @id ' ] , msgFrom , msgTo )
2019-01-18 12:42:50 +01:00
if ' conditions ' in data :
for conditionId in data [ ' conditions ' ] :
c = story . get ( conditionId )
direction . addCondition ( c )
2019-01-22 08:59:45 +01:00
return direction
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def getLogSummary ( self ) :
return {
' id ' : self . id ,
2019-05-11 17:31:00 +02:00
' condition ' : self . conditionMet . id if self . conditionMet else None
2019-01-25 14:10:19 +01:00
}
2019-01-18 12:42:50 +01:00
2019-01-25 10:43:55 +01:00
class Diversion ( object ) :
2019-01-18 12:42:50 +01:00
"""
2019-01-25 10:43:55 +01:00
An Diversion . Used to catch events outside of story flow .
2019-04-26 11:14:49 +02:00
Not sure why I ' m not using subclasses here o:)
2019-01-18 12:42:50 +01:00
"""
2019-01-22 08:59:45 +01:00
2019-03-07 20:19:43 +01:00
def __init__ ( self , id , type : str , params : dict ) :
2019-01-18 12:42:50 +01:00
self . id = id
2019-03-07 20:19:43 +01:00
self . params = params
self . finaliseMethod = None
2019-04-24 13:38:41 +02:00
self . hasHit = False
2019-05-01 13:08:41 +02:00
self . disabled = False
self . type = type
2019-05-01 18:27:10 +02:00
self . counter = 0
2019-03-07 20:19:43 +01:00
if type == ' no_response ' :
self . method = self . _divergeIfNoResponse
self . finaliseMethod = self . _returnAfterNoResponse
self . counter = 0
2019-04-24 13:38:41 +02:00
if type == ' reply_contains ' :
self . method = self . _divergeIfReplyContains
self . finaliseMethod = self . _returnAfterReplyContains
if len ( self . params [ ' regex ' ] ) > 0 :
self . regex = re . compile ( self . params [ ' regex ' ] )
else :
self . regex = None
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
if type == ' timeout ' :
self . method = self . _divergeIfTimeout
self . finaliseMethod = self . _returnAfterTimeout
2019-07-03 17:54:14 +02:00
if type == ' collective_moment ' :
self . method = self . _divergeIfCollectiveMoment
2019-03-07 20:19:43 +01:00
if type == ' repeat ' :
self . method = self . _divergeIfRepeatRequest
self . regex = re . compile ( self . params [ ' regex ' ] )
2019-05-01 18:27:10 +02:00
if type == ' interrupt ' :
self . method = self . _divergeIfInterrupted
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
if not self . method :
raise Exception ( " No valid type given for diversion " )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get diversion {self.id}')
state = self . __dict__ . copy ( )
return state
2019-01-18 12:42:50 +01:00
@classmethod
2019-01-25 10:43:55 +01:00
def initFromJson ( diversionClass , data , story ) :
2019-03-07 20:19:43 +01:00
diversion = diversionClass ( data [ ' @id ' ] , data [ ' type ' ] , data [ ' params ' ] )
2019-05-11 17:31:00 +02:00
2019-01-25 10:43:55 +01:00
return diversion
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def getLogSummary ( self ) :
return {
' id ' : self . id ,
}
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
async def divergeIfNeeded ( self , story , direction = None ) :
2019-03-07 20:19:43 +01:00
"""
Validate if condition is met for the current story state
Returns True when diverging
"""
2019-05-11 17:31:00 +02:00
2019-04-28 11:34:30 +02:00
# 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 ' ] )
if msg is None :
story . logger . warn ( f " Invalid message selected for diversion: { self . params [ ' notAfterMsgId ' ] } for { self . id } " )
elif story . logHasMsg ( msg ) :
# story.logger.warn(f"Block diversion {self.id} because of hit message {self.params['notAfterMsgId']}")
2019-05-01 13:08:41 +02:00
self . disabled = True # never run it and allow following timeouts/no_responses to run
2019-04-28 11:34:30 +02:00
return False
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
r = await self . method ( story ,
direction . msgFrom if direction else None ,
direction . msgTo if direction else None ,
direction if direction else None )
2019-04-24 13:38:41 +02:00
if r :
2019-05-01 18:27:10 +02:00
if self . type != ' repeat ' and self . type != ' interrupt ' :
2019-05-01 13:08:41 +02:00
# repeat diversion should be usable infinte times
self . hasHit = True
2019-06-18 10:46:48 +02:00
2019-04-25 13:24:08 +02:00
story . addToLog ( self )
2019-06-18 10:46:48 +02:00
story . hugvey . eventLogger . info ( f " diverge { self . id } " )
2019-04-24 13:38:41 +02:00
return r
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
def createReturnDirectionsTo ( self , story , startMsg , returnMsg , originalDirection = None , inheritTiming = True , timeoutDuration = .5 , replyContainsDurations = None ) :
2019-05-01 12:37:35 +02:00
"""
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 .
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
replyContainsDurations : list formatted as in JSON
[ {
" minReplyDuration " : " 0 " ,
" waitTime " : " 3 "
} ]
2019-05-01 12:37:35 +02:00
"""
2019-05-01 18:27:10 +02:00
self . counter + = 1
2019-06-11 15:10:46 +02:00
story . logger . info ( f " Creating return directions for { startMsg . id } " )
2019-05-01 12:37:35 +02:00
finishMessageIds = story . getFinishesForMsg ( startMsg )
2019-05-01 18:27:10 +02:00
finalTimeoutDuration = timeoutDuration
finalContainsDurations = replyContainsDurations
2019-05-01 12:37:35 +02:00
#: :type story: Story
#: :type originalDirection: Direction
# story.directionsPerMsg[story.currentMessage.id]
# take the timeouts that are on the current message, and apply it to our return
# as to have somewhat equal pace as to where we originate from
if inheritTiming :
for originalDirection in story . getCurrentDirections ( ) :
# if originalDirection:
for condition in originalDirection . conditions :
if condition . type == ' timeout ' :
finalTimeoutDuration = float ( condition . vars [ ' seconds ' ] )
2019-05-01 18:27:10 +02:00
if condition . type == ' replyContains ' :
finalContainsDurations = json . loads ( condition . originalJsonString ) [ ' vars ' ] [ ' delays ' ]
2019-05-11 17:31:00 +02:00
2019-06-11 15:10:46 +02:00
story . logger . debug ( f " Finishes for { startMsg . id } : { finishMessageIds } " )
2019-05-01 12:37:35 +02:00
i = 0
2019-05-12 19:51:54 +02:00
# story.logger.warn(f"FINISHES: {finishMessageIds}")
2019-05-01 12:37:35 +02:00
for msgId in finishMessageIds :
# Some very ugly hack to add a direction & condition
i + = 1
msg = story . get ( msgId )
if not msg :
continue
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
direction = Direction ( f " { self . id } - { i } - { self . counter } " , msg , returnMsg )
2019-05-01 12:37:35 +02:00
data = json . loads ( f """
{ {
2019-05-01 18:27:10 +02:00
" @id " : " {self.id} -ct {i} - {self.counter} " ,
2019-05-01 12:37:35 +02:00
" @type " : " Condition " ,
" type " : " timeout " ,
2019-05-01 18:27:10 +02:00
" label " : " Autogenerated Timeout " ,
2019-05-01 12:37:35 +02:00
" vars " : { {
2019-05-01 18:27:10 +02:00
" seconds " : " {finalTimeoutDuration} "
2019-05-01 12:37:35 +02:00
} }
} }
""" )
2019-05-01 18:27:10 +02:00
data [ ' vars ' ] [ ' onlyIfNoReply ' ] = finalContainsDurations is not None
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
# TODO: also at replycontains if it exists, with the same timings
2019-05-01 12:37:35 +02:00
condition = Condition . initFromJson ( data , story )
direction . addCondition ( condition )
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
if finalContainsDurations is not None :
data2 = json . loads ( f """
{ {
" @id " : " {self.id} -cr {i} - {self.counter} " ,
" @type " : " Condition " ,
" type " : " replyContains " ,
" label " : " Autogenerated Reply Contains " ,
" vars " : { {
" regex " : " " ,
" instantMatch " : false
} } } }
""" )
data2 [ ' vars ' ] [ ' delays ' ] = finalContainsDurations
condition2 = Condition . initFromJson ( data2 , story )
direction . addCondition ( condition2 )
story . add ( condition2 )
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
direction . isDiversionReturn = True # will clear the currentDiversion on story
2019-06-11 15:10:46 +02:00
story . logger . info ( f " Created direction: { direction . id } ( { msg . id } -> { returnMsg . id } ) { condition . id } with timeout { finalTimeoutDuration } s " )
2019-05-01 12:37:35 +02:00
story . add ( condition )
story . add ( direction )
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
async def finalise ( self , story ) :
""" "
Only used if the Diversion sets the story . currentDiversion
"""
2019-05-01 12:37:35 +02:00
story . logger . info ( " end of diversion " )
2019-06-18 10:46:48 +02:00
story . hugvey . eventLogger . info ( f " return from { self . id } " )
2019-03-07 20:19:43 +01:00
if not self . finaliseMethod :
2019-04-24 13:38:41 +02:00
story . logger . info ( f " No finalisation for diversion { self . id } " )
2019-05-01 12:37:35 +02:00
story . currentDiversion = None
2019-03-07 20:19:43 +01:00
return False
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
await self . finaliseMethod ( story )
2019-05-01 12:37:35 +02:00
story . currentDiversion = None
2019-03-07 20:19:43 +01:00
return True
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
async def _divergeIfNoResponse ( self , story , msgFrom , msgTo , direction ) :
2019-03-07 20:19:43 +01:00
"""
Participant doesn ' t speak for x consecutive replies (has had timeout)
"""
' :type story: Story '
2019-04-24 16:09:41 +02:00
if story . currentDiversion or not msgFrom or not msgTo :
2019-03-07 20:19:43 +01:00
return False
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
if story . stats [ ' consecutiveSilentTimeouts ' ] > = int ( self . params [ ' consecutiveSilences ' ] ) :
2019-03-07 20:19:43 +01:00
story . stats [ ' diversions ' ] [ ' no_response ' ] + = 1
msg = story . get ( self . params [ ' msgId ' ] )
if msg is None :
2019-03-23 18:18:52 +01:00
story . logger . critical ( f " Not a valid message id for diversion: { self . params [ ' msgId ' ] } " )
2019-03-07 20:19:43 +01:00
return
2019-05-11 17:31:00 +02:00
2019-03-23 18:18:52 +01:00
story . logger . info ( f " Diverge: No response { self . id } { story . stats } " )
2019-05-11 17:31:00 +02:00
2019-04-26 13:34:17 +02:00
self . returnMessage = msgTo
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
if self . params [ ' returnAfterStrand ' ] :
self . createReturnDirectionsTo ( story , msg , msgTo , direction )
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
await story . setCurrentMessage ( msg )
story . currentDiversion = self
return True
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
return
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
async def _returnAfterNoResponse ( self , story ) :
2019-03-23 18:18:52 +01:00
story . logger . info ( f " Finalise diversion: { self . id } " )
2019-03-07 20:19:43 +01:00
story . stats [ ' consecutiveSilentTimeouts ' ] = 0 # reset counter after diverging
2019-05-01 12:37:35 +02:00
# if self.params['returnAfterStrand']:
# await story.setCurrentMessage(self.returnMessage)
2019-05-11 17:31:00 +02:00
2019-05-10 15:14:13 +02:00
async def _divergeIfReplyContains ( self , story , msgFrom , msgTo , _ ) :
2019-04-24 13:38:41 +02:00
"""
Participant doesn ' t speak for x consecutive replies (has had timeout)
"""
' :type story: Story '
2019-05-07 14:35:52 +02:00
# use story.currentReply.getTimeSinceLastUtterance() > 2
2019-05-10 15:14:13 +02:00
if story . currentDiversion : # or not msgFrom or not msgTo:
2019-04-24 13:38:41 +02:00
# don't do nested diversions
return False
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
if self . hasHit :
# don't match twice
return
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
if story . currentReply is None or not self . regex :
return
2019-05-11 17:31:00 +02:00
2019-05-10 15:14:13 +02:00
direction = story . getDefaultDirectionForMsg ( story . currentMessage )
if not direction :
# ignore the direction argument, and only check if the current message has a valid default
2019-05-11 17:31:00 +02:00
return
2019-06-09 11:34:41 +02:00
2019-05-10 15:14:13 +02:00
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
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
r = self . regex . search ( story . currentReply . getText ( ) )
if r is None :
return
2019-05-11 17:31:00 +02:00
2019-04-25 18:13:44 +02:00
if ' notForColor ' in self . params and self . params [ ' notForColor ' ] and story . currentMessage . color :
2019-04-25 11:12:27 +02:00
if story . currentMessage . color . lower ( ) == self . params [ ' notForColor ' ] . lower ( ) :
story . logger . debug ( f " Skip diversion { self . id } because of section color " )
return
2019-05-11 17:31:00 +02:00
2019-04-25 11:12:27 +02:00
story . logger . info ( f " Diverge: reply contains { self . id } " )
2019-04-24 13:38:41 +02:00
story . stats [ ' diversions ' ] [ ' reply_contains ' ] + = 1
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
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
2019-05-11 17:31:00 +02:00
2019-06-09 11:34:41 +02:00
if ' nextChapterOnReturn ' in self . params and self . params [ ' nextChapterOnReturn ' ] :
msgTo = story . getNextChapterForMsg ( story . currentMessage , False ) or direction . msgTo
returnInheritTiming = False
else :
msgTo = direction . msgTo
returnInheritTiming = True
2019-04-26 13:34:17 +02:00
self . returnMessage = msgTo
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
if self . params [ ' returnAfterStrand ' ] :
2019-06-09 11:34:41 +02:00
self . createReturnDirectionsTo ( story , msg , msgTo , direction , inheritTiming = returnInheritTiming )
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
await story . setCurrentMessage ( msg )
story . currentDiversion = self
return True
2019-05-11 17:31:00 +02:00
2019-04-24 13:38:41 +02:00
async def _returnAfterReplyContains ( self , story ) :
story . logger . info ( f " Finalise diversion: { self . id } " )
2019-05-01 12:37:35 +02:00
# if self.params['returnAfterStrand']:
# await story.setCurrentMessage(self.returnMessage)
2019-07-03 17:54:14 +02:00
async def _divergeIfCollectiveMoment ( self , story , msgFrom , msgTo , direction ) :
"""
Central command timer times to a collective moment
"""
#: :var story: Story
if story . currentDiversion or not msgFrom or not msgTo :
return False
if not msgTo . chapterStart :
# only when changing chapter
return
window_open_second = float ( self . params [ ' start_minute ' ] ) * 60
window_close_second = window_open_second + float ( self . params [ ' window ' ] )
now = story . hugvey . command . timer . getElapsed ( )
if now < window_open_second or now > window_close_second :
return
#open!
msg = story . get ( self . params [ ' msgId ' ] )
if msg is None :
story . logger . critical ( f " Not a valid message id for diversion: { self . id } { self . params [ ' msgId ' ] } " )
return
self . returnMessage = msgTo
self . createReturnDirectionsTo ( story , msg , msgTo , direction , inheritTiming = True )
2019-05-11 17:31:00 +02:00
2019-07-03 17:54:14 +02:00
await story . setCurrentMessage ( msg )
story . currentDiversion = self
return True
2019-05-01 12:37:35 +02:00
async def _divergeIfRepeatRequest ( self , story , msgFrom , msgTo , direction ) :
2019-03-07 20:19:43 +01:00
"""
Participant asks if message can be repeated .
"""
2019-05-07 14:30:57 +02:00
# if not msgFrom or not msgTo:
# return
2019-05-11 17:31:00 +02:00
2019-04-24 11:31:20 +02:00
# TODO: how to handle this now we sometimes use different timings.
# Perhaps set isFinished when matching condition.
2019-05-07 14:30:57 +02:00
if story . currentReply is None or story . currentReply . getTimeSinceLastUtterance ( ) > 1 :
2019-03-07 20:19:43 +01:00
return
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
r = self . regex . search ( story . currentReply . getText ( ) )
if r is None :
return
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
logger . info ( f " Diverge: request repeat { self . id } " )
story . stats [ ' diversions ' ] [ ' repeat ' ] + = 1
2019-05-07 14:30:57 +02:00
await story . setCurrentMessage ( story . currentMessage )
2019-03-07 20:19:43 +01:00
return True
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
async def _divergeIfTimeout ( self , story , msgFrom , msgTo , direction ) :
2019-04-24 16:09:41 +02:00
"""
( 1 ) last spoken at all
( 2 ) or duration for this last reply only
"""
2019-05-07 14:30:57 +02:00
if story . currentDiversion :
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
if msgFrom or msgTo :
# not applicable a direction has been chosen
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:50:34 +02:00
if not story . lastMsgFinishTime :
# not during play back
return
2019-05-11 17:31:00 +02:00
2019-04-25 11:12:27 +02:00
# not applicable when timeout is set
directions = story . getCurrentDirections ( )
for direction in directions :
for condition in direction . conditions :
if condition . type == ' timeout ' :
return
2019-04-24 16:50:34 +02:00
now = story . timer . getElapsed ( )
if now - story . lastMsgFinishTime < float ( self . params [ ' minTimeAfterMessage ' ] ) :
# not less than x sec after it
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
interval = float ( self . params [ ' interval ' ] )
if not self . params [ ' fromLastMessage ' ] :
# (1) last spoken at all
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
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
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
story . stats [ ' diversions ' ] [ ' timeout_total ' ] + = 1
else :
if story . currentMessage is None :
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
# if story.currentMessage.timeoutDiversionCount + 1
2019-05-11 17:31:00 +02:00
2019-04-24 16:50:34 +02:00
if story . currentReply is not None :
2019-04-24 16:09:41 +02:00
# still playing back
# or somebody has spoken already (timeout only works on silences)
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:50:34 +02:00
if now - story . lastMsgFinishTime < interval :
2019-04-24 16:09:41 +02:00
return
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
story . currentMessage . timeoutDiversionCount + = 1
story . stats [ ' diversions ' ] [ ' timeout_last ' ] + = 1
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
# if we're still here, there's a match!
2019-04-24 16:50:34 +02:00
story . logger . info ( f " Diverge: Timeout { self . id } of { self . params [ ' interval ' ] } " )
2019-04-24 16:09:41 +02:00
story . stats [ ' diversions ' ] [ ' timeout ' ] + = 1
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
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
2019-05-11 17:31:00 +02:00
2019-04-26 11:14:49 +02:00
# 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
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
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 )
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
await story . setCurrentMessage ( msg , allowReplyInterrupt = True )
2019-04-24 16:09:41 +02:00
story . currentDiversion = self
story . timer . setMark ( ' last_diversion_timeout ' )
return True
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
async def _returnAfterTimeout ( self , story ) :
story . logger . info ( f " Finalise diversion: { self . id } " )
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
async def _divergeIfInterrupted ( self , story , msgFrom , msgTo , direction ) :
"""
This is here as a placeholder for the interruption diversion .
These will however be triggered differently
"""
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
if story . currentDiversion or story . allowReplyInterrupt :
return False
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
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
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
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 = [ {
" minReplyDuration " : " 0 " ,
" waitTime " : " 2 "
} ] )
await story . setCurrentMessage ( msg )
story . currentDiversion = self
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
return True
2019-01-22 08:59:45 +01:00
2019-05-12 14:54:37 +02:00
class Configuration ( object ) :
id = ' configuration '
2019-05-13 14:45:52 +02:00
volume = 1 # Volume multiplier for 'play' command
nothing_text = " nothing " # When variable is not set, but used in sentence, replace it with this word.
2019-05-12 19:51:54 +02:00
2019-05-12 14:54:37 +02:00
@classmethod
def initFromJson ( configClass , data , story ) :
config = Configuration ( )
config . __dict__ . update ( data )
return config
2019-01-18 12:42:50 +01:00
storyClasses = {
' Msg ' : Message ,
' Direction ' : Direction ,
' Condition ' : Condition ,
2019-01-25 10:43:55 +01:00
' Diversion ' : Diversion ,
2019-05-12 14:54:37 +02:00
' Configuration ' : Configuration ,
2019-01-18 12:42:50 +01:00
}
2019-01-22 08:59:45 +01:00
class Stopwatch ( object ) :
"""
Keep track of elapsed time . Use multiple markers , but a single pause / resume button
"""
def __init__ ( self ) :
self . isRunning = asyncio . Event ( )
self . reset ( )
def getElapsed ( self , since_mark = ' start ' ) :
t = time . time ( )
if self . paused_at != 0 :
pause_duration = t - self . paused_at
2019-05-11 17:31:00 +02:00
else :
2019-01-22 08:59:45 +01:00
pause_duration = 0
return t - self . marks [ since_mark ] - pause_duration
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def pause ( self ) :
self . paused_at = time . time ( )
self . isRunning . clear ( )
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def resume ( self ) :
if self . paused_at == 0 :
return
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
pause_duration = time . time ( ) - self . paused_at
for m in self . marks :
self . marks [ m ] + = pause_duration
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
self . paused_at = 0
self . isRunning . set ( )
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def reset ( self ) :
self . marks = { }
self . setMark ( ' start ' )
self . paused_at = 0
self . isRunning . set ( )
2019-05-11 17:31:00 +02:00
2019-07-03 17:54:14 +02:00
def setMark ( self , name , overrideValue = None ) :
"""
Set a marker to current time . Or , if given , to any float one desires
"""
self . marks [ name ] = overrideValue if overrideValue else time . time ( )
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
def hasMark ( self , name ) :
return name in self . marks
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def clearMark ( self , name ) :
if name in self . marks :
self . marks . pop ( name )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __getstate__ ( self ) :
# print(f'get stopwatch')
state = self . __dict__ . copy ( )
state [ ' isRunning ' ] = self . isRunning . is_set ( )
return state
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __setstate__ ( self , state ) :
self . __dict__ . update ( state )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
self . isRunning = asyncio . Event ( )
if ' isRunning ' in state and state [ ' isRunning ' ] :
self . isRunning . set ( )
else :
self . isRunning . clear ( )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
class StoryState ( object ) :
"""
Because Story not only contains state , but also logic / control variables , we need
a separate class to keep track of the state of things . This way , we can recreate
the exact state in which a story was before .
"""
msgLog = [ ]
currentMessage = None
currentDiversion = None
currentReply = None
allowReplyInterrupt = False
timer = Stopwatch ( )
isRunning = False
lastMsgTime = None
lastSpeechStartTime = None
lastSpeechEndTime = None
variableValues = { } # captured variables from replies
finish_time = False
events = [ ] # queue of received events
commands = [ ] # queue of commands to send
log = [ ] # all nodes/elements that are triggered
msgLog = [ ]
stats = {
' timeouts ' : 0 ,
' silentTimeouts ' : 0 ,
' consecutiveSilentTimeouts ' : 0 ,
' diversions ' : {
' no_response ' : 0 ,
' repeat ' : 0 ,
' reply_contains ' : 0 ,
' timeout ' : 0 ,
' timeout_total ' : 0 ,
' timeout_last ' : 0
}
}
def __init__ ( self ) :
pass
2019-05-12 19:51:54 +02:00
#
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
class Story ( object ) :
""" Story represents and manages a story/narrative flow """
2019-01-22 08:59:45 +01:00
# TODO should we separate 'narrative' (the graph) from the story (the
# current user flow)
2019-02-18 20:38:54 +01:00
def __init__ ( self , hugvey_state , panopticon_port ) :
2019-01-18 12:42:50 +01:00
super ( Story , self ) . __init__ ( )
self . hugvey = hugvey_state
2019-02-18 20:38:54 +01:00
self . panopticon_port = panopticon_port
2019-01-18 12:42:50 +01:00
2019-01-22 08:59:45 +01:00
self . events = [ ] # queue of received events
self . commands = [ ] # queue of commands to send
self . log = [ ] # all nodes/elements that are triggered
2019-04-28 11:34:30 +02:00
self . msgLog = [ ] # hit messages
2019-03-23 18:18:52 +01:00
self . logger = mainLogger . getChild ( f " { self . hugvey . id } " ) . getChild ( " story " )
2019-01-22 08:59:45 +01:00
self . currentMessage = None
2019-03-07 20:19:43 +01:00
self . currentDiversion = None
2019-02-11 21:28:48 +01:00
self . currentReply = None
2019-05-01 18:27:10 +02:00
self . allowReplyInterrupt = False
2019-01-22 08:59:45 +01:00
self . timer = Stopwatch ( )
2019-01-25 11:17:10 +01:00
self . isRunning = False
2019-03-07 20:19:43 +01:00
self . diversions = [ ]
2019-05-01 18:27:10 +02:00
self . interruptionDiversions = [ ]
2019-02-14 08:39:31 +01:00
self . variables = { }
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def pause ( self ) :
2019-03-23 18:18:52 +01:00
self . logger . debug ( ' pause hugvey ' )
2019-01-22 08:59:45 +01:00
self . timer . pause ( )
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def resume ( self ) :
2019-03-23 18:18:52 +01:00
self . logger . debug ( ' resume hugvey ' )
2019-01-22 08:59:45 +01:00
self . timer . resume ( )
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def getLogSummary ( self ) :
summary = {
# e[0]: the entity, e[1]: the logged time
' messages ' : [ ( e [ 0 ] . getLogSummary ( ) , e [ 1 ] ) for e in self . log if isinstance ( e [ 0 ] , Message ) ] ,
' directions ' : [ ( e [ 0 ] . getLogSummary ( ) , e [ 1 ] ) for e in self . log if isinstance ( e [ 0 ] , Direction ) ] ,
' diversions ' : [ ( e [ 0 ] . getLogSummary ( ) , e [ 1 ] ) for e in self . log if isinstance ( e [ 0 ] , Diversion ) ] ,
2019-01-22 08:59:45 +01:00
}
2019-01-25 14:10:19 +01:00
# print(self.log)
return summary
2019-05-11 17:31:00 +02:00
2019-04-25 13:24:08 +02:00
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 ) ] ) ,
}
2019-05-11 17:31:00 +02:00
2019-02-14 08:39:31 +01:00
def registerVariable ( self , variableName , message ) :
if variableName not in self . variables :
self . variables [ variableName ] = [ message ]
else :
self . variables [ variableName ] . append ( message )
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
def setVariableValue ( self , name , value ) :
if name not in self . variables :
2019-03-23 18:18:52 +01:00
self . logger . warn ( f " Set variable that is not needed in the story: { name } " )
2019-04-17 11:58:40 +02:00
else :
self . logger . debug ( f " Set variable { name } to { value } " )
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
self . variableValues [ name ] = value
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
if name not in self . variables :
return
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
for message in self . variables [ name ] :
message . setVariable ( name , value )
2019-05-11 17:31:00 +02:00
2019-03-29 14:11:48 +01:00
def hasVariableSet ( self , name ) - > bool :
return name in self . variableValues and self . variableValues is not None
2019-01-18 12:42:50 +01:00
2019-05-11 23:34:06 +02:00
def setStoryData ( self , story_data , language_code ) :
2019-01-18 12:42:50 +01:00
"""
Parse self . data into a working story engine
"""
self . data = story_data
2019-05-11 23:34:06 +02:00
self . language_code = language_code
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
# keep to be able to reset it in the end
currentId = self . currentMessage . id if self . currentMessage else None
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
self . elements = { }
2019-05-01 12:37:35 +02:00
self . strands = { }
2019-01-25 10:43:55 +01:00
self . diversions = [ ]
2019-05-01 18:27:10 +02:00
self . interruptionDiversions = [ ]
2019-01-18 12:42:50 +01:00
self . directionsPerMsg = { }
2019-01-22 08:59:45 +01:00
self . startMessage = None # The entrypoint to the graph
2019-02-14 08:39:31 +01:00
self . variables = { }
2019-01-18 12:42:50 +01:00
self . reset ( )
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
for el in self . data :
2019-05-10 11:38:57 +02:00
try :
className = storyClasses [ el [ ' @type ' ] ]
obj = className . initFromJson ( el , self )
self . add ( obj )
except Exception as e :
self . logger . critical ( f " Error loading story element: { el } " )
self . logger . exception ( e )
raise e
2019-01-18 12:42:50 +01:00
2019-03-28 12:15:15 +01:00
# self.logger.debug(self.elements)
# self.logger.debug(self.directionsPerMsg)
2019-05-11 17:31:00 +02:00
2019-03-07 20:19:43 +01:00
self . diversions = [ el for el in self . elements . values ( ) if type ( el ) == Diversion ]
2019-05-01 18:27:10 +02:00
self . interruptionDiversions = [ el for el in self . elements . values ( ) if type ( el ) == Diversion and el . type == ' interrupt ' ]
2019-05-12 14:54:37 +02:00
configurations = [ el for el in self . elements . values ( ) if type ( el ) == Configuration ]
self . configuration = configurations [ 0 ] if len ( configurations ) else Configuration ( )
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
if currentId :
self . currentMessage = self . get ( currentId )
if self . currentMessage :
2019-03-23 18:18:52 +01:00
self . logger . info (
2019-01-22 08:59:45 +01:00
f " Reinstantiated current message: { self . currentMessage . id } " )
2019-01-18 12:42:50 +01:00
else :
2019-03-23 18:18:52 +01:00
self . logger . warn (
2019-01-22 08:59:45 +01:00
" Could not reinstatiate current message. Starting over " )
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
# Register variables
for msg in self . getMessages ( ) :
2019-04-10 10:13:35 +02:00
# print(msg.id, msg.hasVariables())
2019-02-26 21:27:38 +01:00
if not msg . hasVariables ( ) :
continue
2019-05-11 17:31:00 +02:00
2019-02-26 21:27:38 +01:00
for var in msg . variables :
self . registerVariable ( var , msg )
2019-05-11 17:31:00 +02:00
2019-03-23 18:18:52 +01:00
self . logger . info ( f ' has variables: { self . variables } ' )
2019-05-01 12:37:35 +02:00
self . logger . info ( f ' has { len ( self . strands ) } strands: { self . strands } ' )
2019-06-11 15:10:46 +02:00
# self.logger.info(f"Directions: {self.directionsPerMsg}")
2019-05-01 12:37:35 +02:00
self . calculateFinishesForStrands ( )
2019-01-18 12:42:50 +01:00
def reset ( self ) :
2019-01-22 08:59:45 +01:00
self . timer . reset ( )
# self.startTime = time.time()
# currently active message, determines active listeners etc.
self . currentMessage = None
2019-03-07 20:19:43 +01:00
self . currentDiversion = None
2019-01-18 12:42:50 +01:00
self . lastMsgTime = None
self . lastSpeechStartTime = None
self . lastSpeechEndTime = None
2019-02-14 08:39:31 +01:00
self . variableValues = { } # captured variables from replies
2019-01-22 08:59:45 +01:00
self . finish_time = False
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
self . events = [ ] # queue of received events
self . commands = [ ] # queue of commands to send
self . log = [ ] # all nodes/elements that are triggered
2019-04-28 11:34:30 +02:00
self . msgLog = [ ]
2019-02-11 21:28:48 +01:00
self . currentReply = None
2019-05-11 17:31:00 +02:00
2019-02-28 18:58:03 +01:00
self . stats = {
' timeouts ' : 0 ,
2019-03-07 20:19:43 +01:00
' silentTimeouts ' : 0 ,
' consecutiveSilentTimeouts ' : 0 ,
' diversions ' : {
' no_response ' : 0 ,
' repeat ' : 0 ,
2019-04-24 13:38:41 +02:00
' reply_contains ' : 0 ,
2019-04-24 16:09:41 +02:00
' timeout ' : 0 ,
' timeout_total ' : 0 ,
' timeout_last ' : 0
2019-03-07 20:19:43 +01:00
}
2019-02-28 18:58:03 +01:00
}
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
for msg in self . getMessages ( ) :
pass
2019-01-22 08:59:45 +01:00
2019-01-18 12:42:50 +01:00
def add ( self , obj ) :
if obj . id in self . elements :
# print(obj)
raise Exception ( " Duplicate id for ' ' " . format ( obj . id ) )
self . elements [ obj . id ] = obj
2019-01-25 10:43:55 +01:00
if type ( obj ) == Diversion :
self . diversions . append ( obj )
2019-01-18 12:42:50 +01:00
2019-05-01 12:37:35 +02:00
if type ( obj ) == Message :
if obj . isStart :
#confusingly, isStart is 'beginning' in the story json file
self . startMessage = obj
if obj . isStrandStart :
self . strands [ obj . id ] = [ ]
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
if type ( obj ) == Direction :
if obj . msgFrom . id not in self . directionsPerMsg :
self . directionsPerMsg [ obj . msgFrom . id ] = [ ]
self . directionsPerMsg [ obj . msgFrom . id ] . append ( obj )
def get ( self , id ) :
"""
Get a story element by its id
"""
if id in self . elements :
return self . elements [ id ]
return None
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
def getMessages ( self ) :
2019-02-26 21:27:38 +01:00
return [ el for el in self . elements . values ( ) if type ( el ) == Message ]
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
def stop ( self ) :
2019-03-23 18:18:52 +01:00
self . logger . info ( " Stop Story " )
2019-01-18 12:42:50 +01:00
if self . isRunning :
self . isRunning = False
2019-05-11 17:31:00 +02:00
2019-04-25 17:39:44 +02:00
def shutdown ( self ) :
self . stop ( )
self . hugvey = None
2019-01-18 12:42:50 +01:00
2019-02-18 20:38:54 +01:00
async def _processPendingEvents ( self ) :
2019-01-18 12:42:50 +01:00
# Gather events:
nr = len ( self . events )
for i in range ( nr ) :
e = self . events . pop ( 0 )
2019-03-23 18:18:52 +01:00
self . logger . debug ( " handle ' {} ' " . format ( e ) )
2019-01-18 12:42:50 +01:00
if e [ ' event ' ] == " exit " :
self . stop ( )
if e [ ' event ' ] == ' connect ' :
2019-01-26 22:35:26 +01:00
# a client connected. Should only happen in the beginning or in case of error
2019-01-18 12:42:50 +01:00
# that is, until we have a 'reset' or 'start' event.
2019-01-22 08:59:45 +01:00
# reinitiate current message
2019-02-18 20:38:54 +01:00
await self . setCurrentMessage ( self . currentMessage )
2019-05-11 17:31:00 +02:00
2019-04-12 12:38:00 +02:00
if e [ ' event ' ] == " playbackStart " :
if e [ ' msgId ' ] != self . currentMessage . id :
continue
self . lastMsgStartTime = self . timer . getElapsed ( )
self . logger . debug ( " Start playback " )
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
if e [ ' event ' ] == " playbackFinish " :
if e [ ' msgId ' ] == self . currentMessage . id :
2019-02-15 12:26:56 +01:00
#TODO: migrate value to Messagage instead of Story
self . lastMsgFinishTime = self . timer . getElapsed ( )
2019-03-27 13:36:09 +01:00
self . hugvey . eventLogger . info ( f " message: { self . currentMessage . id } { self . currentMessage . uuid } done " )
2019-05-11 17:31:00 +02:00
2019-02-22 14:45:36 +01:00
# 2019-02-22 temporary disable listening while playing audio:
2019-02-25 12:52:23 +01:00
# if self.hugvey.google is not None:
2019-03-23 18:18:52 +01:00
# self.logger.warn("Temporary 'fix' -> resume recording?")
2019-02-25 12:52:23 +01:00
# self.hugvey.google.resume()
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
if self . currentMessage . id not in self . directionsPerMsg :
2019-05-01 12:37:35 +02:00
# print(self.currentDiversion)
# if self.currentDiversion is not None:
# await self.currentDiversion.finalise(self)
# else:
self . logger . info ( " THE END! " )
self . _finish ( )
return
2019-01-18 12:42:50 +01:00
if e [ ' event ' ] == ' speech ' :
2019-03-07 20:19:43 +01:00
# participants speaks, reset counter
self . stats [ ' consecutiveSilentTimeouts ' ] = 0
2019-05-11 17:31:00 +02:00
2019-04-12 12:38:00 +02:00
# if self.currentMessage and not self.lastMsgStartTime:
if self . currentMessage and not self . lastMsgFinishTime :
# Ignore incoming speech events until we receive a 'playbackStart' event.
# After that moment the mic will be muted, so nothing should come in _anyway_
# unless google is really slow on us. But by taking the start time we don't ignore
# 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
2019-05-07 14:01:37 +02:00
# if len(self.interruptionDiversions) and not self.currentDiversion and not self.allowReplyInterrupt:
# self.logger.warn("diverge when speech during playing message")
# diversion = random.choice(self.interruptionDiversions)
# #: :type diversion: Diversion
# r = await diversion.divergeIfNeeded(self, None)
# print(r) # is always needed :-)
# else:
2019-05-01 18:27:10 +02:00
self . logger . info ( " ignore speech during playing message " )
continue
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
# log if somebody starts speaking
2019-02-11 21:28:48 +01:00
if self . currentReply is None :
2019-03-23 18:18:52 +01:00
self . logger . info ( " Start speaking " )
2019-02-11 21:28:48 +01:00
self . currentReply = Reply ( self . currentMessage )
2019-05-11 17:31:00 +02:00
2019-04-02 17:32:01 +02:00
now = self . timer . getElapsed ( )
utterance = self . currentReply . getActiveUtterance ( now )
2019-05-14 18:18:42 +02:00
# The 'is_final' from google sometimes comes 2 sec after finishing speaking
# therefore, we ignore the timing of this transcription if something has been said already
if e [ ' is_final ' ] and utterance . hasText ( ) :
2019-05-14 18:39:22 +02:00
self . logger . debug ( f ' ignore timing: { now } use { utterance . lastUpdateTime } ' )
2019-05-14 18:18:42 +02:00
utterance . setText ( e [ ' transcript ' ] , utterance . lastUpdateTime )
else :
utterance . setText ( e [ ' transcript ' ] , now )
2019-03-27 13:36:09 +01:00
self . hugvey . eventLogger . info ( " speaking: content {} \" {} \" " . format ( id ( utterance ) , e [ ' transcript ' ] ) )
2019-04-24 16:09:41 +02:00
self . timer . setMark ( ' last_speech ' )
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
if e [ ' is_final ' ] :
2019-02-11 21:28:48 +01:00
utterance . setFinished ( self . timer . getElapsed ( ) )
2019-03-27 13:36:09 +01:00
self . hugvey . eventLogger . info ( " speaking: stop {} " . format ( id ( utterance ) ) )
2019-05-11 17:31:00 +02:00
2019-04-10 10:13:35 +02:00
if self . hugvey . recorder :
self . hugvey . recorder . updateTranscription ( self . currentReply . getText ( ) )
2019-05-11 17:31:00 +02:00
2019-06-15 19:21:22 +02:00
def _processDirection ( self , direction ) :
"""
return matching condition
"""
for condition in direction . conditions :
if condition . isMet ( self ) :
self . logger . info ( " Condition is met: {0} ( {2} ), going to {1} " . format (
condition . id , direction . msgTo . id , condition . type ) )
self . hugvey . eventLogger . info ( " condition: {0} " . format ( condition . id ) )
self . hugvey . eventLogger . info ( " direction: {0} " . format ( direction . id ) )
direction . setMetCondition ( condition )
return condition
return None
2019-05-11 17:31:00 +02:00
2019-02-18 20:38:54 +01:00
async def _processDirections ( self , directions ) :
2019-03-07 20:19:43 +01:00
' :type directions: list(Direction) '
2019-04-24 16:09:41 +02:00
chosenDirection = None
2019-05-01 18:27:10 +02:00
metCondition = None
2019-01-18 12:42:50 +01:00
for direction in directions :
2019-06-16 19:42:59 +02:00
if direction . isDiversionReturn and direction . diversionHasReturned :
# Prevent that returns created from the same message send you
# back to a previous point in time.
# self.logger.warn("Skipping double direction for diversion")
continue
2019-06-15 19:21:22 +02:00
condition = self . _processDirection ( direction )
if not condition :
continue
self . addToLog ( condition )
self . addToLog ( direction )
self . currentMessage . setFinished ( self . timer . getElapsed ( ) )
chosenDirection = direction
metCondition = condition
break
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
isDiverging = await self . _processDiversions ( chosenDirection )
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
allowReplyInterrupt = False
# in some cases, conditions should be allowed to interrupt the reply
if metCondition :
if metCondition . type == ' timeout ' and not ( ' onlyIfNoReply ' in metCondition . vars and metCondition . vars [ ' onlyIfNoReply ' ] ) :
allowReplyInterrupt = True
if metCondition . usedContainsDuration is not None and metCondition . usedContainsDuration < 0.1 :
allowReplyInterrupt = True
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
if not isDiverging and chosenDirection :
2019-05-01 12:37:35 +02:00
if chosenDirection . isDiversionReturn and self . currentDiversion :
2019-06-16 19:42:59 +02:00
chosenDirection . diversionHasReturned = True
2019-05-01 12:37:35 +02:00
await self . currentDiversion . finalise ( self )
2019-05-11 17:31:00 +02:00
2019-05-01 18:27:10 +02:00
await self . setCurrentMessage ( chosenDirection . msgTo , allowReplyInterrupt = allowReplyInterrupt )
2019-05-11 17:31:00 +02:00
2019-04-24 16:09:41 +02:00
return chosenDirection
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
async def _processDiversions ( self , direction : None ) - > bool :
2019-03-07 20:19:43 +01:00
"""
Process the diversions on stack . If diverging , return True , else False
2019-04-24 16:09:41 +02:00
msgFrom and msgTo contain the source and target of a headed direction if given
Else , they are None
2019-03-07 20:19:43 +01:00
"""
diverge = False
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
activeDiversions = [ ]
activeTimeoutDiv = None
activeTimeoutLastDiv = None
activeNoResponseDiv = None
2019-03-07 20:19:43 +01:00
for diversion in self . diversions :
2019-05-01 13:08:41 +02:00
#: :type diversion: Diversion
2019-05-01 18:27:10 +02:00
if diversion . disabled or diversion . hasHit or diversion . type == ' interrupt ' :
# interruptions are triggered somewhere else.
2019-05-01 13:08:41 +02:00
continue
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
if diversion . type == ' timeout ' :
if diversion . params [ ' timesOccured ' ] > 0 :
if not diversion . params [ ' fromLastMessage ' ] :
# perhaps neater if we collect them in a list, and then sort by key, but this works just as well
if not activeTimeoutDiv or activeTimeoutDiv . params [ ' timesOccured ' ] > diversion . params [ ' timesOccured ' ] :
activeTimeoutDiv = diversion
else :
if not activeTimeoutLastDiv or activeTimeoutLastDiv . params [ ' timesOccured ' ] > diversion . params [ ' timesOccured ' ] :
activeTimeoutLastDiv = diversion
continue
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
if diversion . type == ' no_response ' :
if diversion . params [ ' timesOccured ' ] > 0 :
if not activeNoResponseDiv or activeNoResponseDiv . params [ ' timesOccured ' ] > diversion . params [ ' timesOccured ' ] :
activeNoResponseDiv = diversion
continue
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
activeDiversions . append ( diversion )
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
if activeTimeoutDiv :
activeDiversions . append ( activeTimeoutDiv )
if activeTimeoutLastDiv :
activeDiversions . append ( activeTimeoutLastDiv )
if activeNoResponseDiv :
activeDiversions . append ( activeNoResponseDiv )
2019-05-11 17:31:00 +02:00
2019-05-01 13:08:41 +02:00
for diversion in activeDiversions :
# TODO: collect diversions and order by times + timesOccured (for timeout & no_response)
2019-05-01 12:37:35 +02:00
d = await diversion . divergeIfNeeded ( self , direction )
2019-03-07 20:19:43 +01:00
if d :
diverge = True
return diverge
2019-05-11 17:31:00 +02:00
2019-01-25 14:10:19 +01:00
def addToLog ( self , node ) :
self . log . append ( ( node , self . timer . getElapsed ( ) ) )
2019-05-11 17:31:00 +02:00
2019-04-28 11:34:30 +02:00
if isinstance ( node , Message ) :
self . msgLog . append ( node )
2019-05-11 17:31:00 +02:00
2019-04-27 15:33:51 +02:00
if self . hugvey . recorder :
if isinstance ( node , Message ) :
self . hugvey . recorder . log ( ' hugvey ' , node . text , node . id )
if isinstance ( node , Diversion ) :
self . hugvey . recorder . log ( ' diversion ' , node . id )
if isinstance ( node , Condition ) :
self . hugvey . recorder . log ( ' condition ' , node . logInfo , node . id )
2019-05-11 17:31:00 +02:00
2019-04-28 11:34:30 +02:00
def logHasMsg ( self , node ) :
return node in self . msgLog
2019-01-18 12:42:50 +01:00
async def _renderer ( self ) :
"""
every 1 / 10 sec . determine what needs to be done based on the current story state
"""
2019-01-22 08:59:45 +01:00
loopDuration = 0.1 # Configure fps
2019-01-18 12:42:50 +01:00
lastTime = time . time ( )
2019-03-23 18:18:52 +01:00
self . logger . debug ( " Start renderer " )
2019-01-18 12:42:50 +01:00
while self . isRunning :
if self . isRunning is False :
break
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
# pause on timer paused
await self . timer . isRunning . wait ( ) # wait for un-pause
2019-01-18 12:42:50 +01:00
for i in range ( len ( self . events ) ) :
2019-02-18 20:38:54 +01:00
await self . _processPendingEvents ( )
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
directions = self . getCurrentDirections ( )
2019-02-18 20:38:54 +01:00
await self . _processDirections ( directions )
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
# TODO create timer event
# self.commands.append({'msg':'TEST!'})
2019-05-12 19:51:54 +02:00
2019-05-12 12:17:03 +02:00
# Test stability of Central Command with deliberate crash
# if self.timer.getElapsed() > 10:
# raise Exception("Test exception")
if not self . timer . hasMark ( ' state_save ' ) or self . timer . getElapsed ( ' state_save ' ) > 5 :
self . storeState ( )
self . timer . setMark ( ' state_save ' )
2019-01-18 12:42:50 +01:00
# wait for next iteration to avoid too high CPU
t = time . time ( )
await asyncio . sleep ( max ( 0 , loopDuration - ( t - lastTime ) ) )
lastTime = t
2019-03-23 18:18:52 +01:00
self . logger . debug ( " Stop renderer " )
2019-01-18 12:42:50 +01:00
2019-05-01 18:27:10 +02:00
async def setCurrentMessage ( self , message , useReply = None , allowReplyInterrupt = False ) :
2019-03-07 20:19:43 +01:00
"""
Use Reply allows to pre - initiate a reply to use with the message . This is used eg . when doing an interruption .
"""
2019-02-11 21:28:48 +01:00
if self . currentMessage and not self . lastMsgFinishTime :
2019-03-23 18:18:52 +01:00
self . logger . info ( " Interrupt playback {} " . format ( self . currentMessage . id ) )
2019-03-27 13:36:09 +01:00
self . hugvey . eventLogger . info ( " interrupt " )
2019-02-11 21:28:48 +01:00
# message is playing
self . hugvey . sendCommand ( {
' action ' : ' stop ' ,
' id ' : self . currentMessage . id ,
} )
2019-05-11 17:31:00 +02:00
2019-03-27 13:36:09 +01:00
message . uuid = shortuuid . uuid ( )
2019-01-18 12:42:50 +01:00
self . currentMessage = message
self . lastMsgTime = time . time ( )
2019-01-22 08:59:45 +01:00
self . lastMsgFinishTime = None # to be filled in by the event
2019-04-12 12:38:00 +02:00
self . lastMsgStartTime = None # to be filled in by the event
2019-05-01 18:27:10 +02:00
self . allowReplyInterrupt = allowReplyInterrupt
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
# if not reset:
2019-02-11 21:28:48 +01:00
self . previousReply = self . currentReply # we can use this for interrptions
2019-03-07 20:19:43 +01:00
self . currentReply = useReply #self.currentMessage.reply
2019-05-11 17:31:00 +02:00
2019-04-12 12:38:00 +02:00
# send command to already mute mic
self . hugvey . sendCommand ( {
' action ' : ' prepare ' ,
' id ' : message . id
} )
2019-05-11 17:31:00 +02:00
2019-02-15 12:26:56 +01:00
# 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
# self.currentReply = self.currentMessage.reply
2019-01-18 12:42:50 +01:00
2019-03-23 18:18:52 +01:00
self . logger . info ( " Current message: ( {0} ) \" {1} \" " . format (
2019-04-28 17:31:02 +02:00
message . id , message . getText ( ) ) )
2019-01-25 14:10:19 +01:00
self . addToLog ( message )
2019-04-28 17:31:02 +02:00
self . hugvey . eventLogger . info ( f " message: { message . id } { message . uuid } start \" { message . getText ( ) } \" " )
2019-05-11 17:31:00 +02:00
2019-01-18 12:42:50 +01:00
# TODO: prep events & timer etc.
2019-04-08 12:16:41 +02:00
fn = await message . getAudioFilePath ( )
2019-05-11 17:31:00 +02:00
2019-04-24 11:31:20 +02:00
# get duration of audio file, so the client can detect a hang of 'play'
2019-05-11 18:12:25 +02:00
try :
2019-05-11 18:14:20 +02:00
duration = sox . file_info . duration ( fn )
2019-05-11 18:12:25 +02:00
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 } " )
2019-05-11 18:15:13 +02:00
self . logger . exception ( e )
2019-05-11 18:12:25 +02:00
duration = 10 # some default duration to have something to fall back to
2019-05-12 19:51:54 +02:00
2019-05-12 14:54:37 +02:00
params = message . getParams ( ) . copy ( )
params [ ' vol ' ] = params [ ' vol ' ] * self . configuration . volume if ' vol ' in params else self . configuration . volume
2019-05-12 19:51:54 +02:00
2019-04-12 12:38:00 +02:00
# self.hugvey.google.pause() # pause STT to avoid text events while decision is made
2019-02-18 20:38:54 +01:00
self . hugvey . sendCommand ( {
' action ' : ' play ' ,
2019-04-08 12:16:41 +02:00
' file ' : fn ,
2019-02-18 20:38:54 +01:00
' id ' : message . id ,
2019-05-12 14:54:37 +02:00
' params ' : params ,
2019-04-24 11:31:20 +02:00
' duration ' : duration
2019-02-18 20:38:54 +01:00
} )
2019-06-08 16:10:46 +02:00
if message . lightChange is not None :
self . hugvey . setLightStatus ( message . lightChange )
2019-05-11 17:31:00 +02:00
2019-02-22 14:45:36 +01:00
# 2019-02-22 temporary disable listening while playing audio:
2019-02-25 12:52:23 +01:00
# if self.hugvey.google is not None:
2019-03-23 18:18:52 +01:00
# self.logger.warn("Temporary 'fix' -> stop recording")
2019-02-25 12:52:23 +01:00
# self.hugvey.google.pause()
2019-01-18 12:42:50 +01:00
2019-04-11 12:00:11 +02:00
logmsg = " Pending directions: "
2019-01-18 12:42:50 +01:00
for direction in self . getCurrentDirections ( ) :
conditions = [ c . id for c in direction . conditions ]
2019-04-11 12:00:11 +02:00
logmsg + = " \n - {0} -> {1} (when: {2} ) " . format ( direction . msgFrom . id , direction . msgTo . id , conditions )
2019-05-11 17:31:00 +02:00
2019-04-11 12:00:11 +02:00
self . logger . log ( LOG_BS , logmsg )
2019-05-11 23:34:06 +02:00
self . storeState ( )
2019-01-18 12:42:50 +01:00
def getCurrentDirections ( self ) :
if self . currentMessage . id not in self . directionsPerMsg :
return [ ]
else :
return self . directionsPerMsg [ self . currentMessage . id ]
2019-05-11 17:31:00 +02:00
2019-04-26 11:14:49 +02:00
def getNextChapterForMsg ( self , msg , canIncludeSelf = True , depth = 0 ) :
if canIncludeSelf and msg . chapterStart :
self . logger . info ( f " Next chapter: { msg . id } " )
return msg
2019-05-11 17:31:00 +02:00
2019-04-26 11:14:49 +02:00
if depth > = 70 :
# protection against infinite loop?
return None
2019-05-11 17:31:00 +02:00
2019-04-26 11:14:49 +02:00
if msg . id not in self . directionsPerMsg :
return None
2019-05-11 17:31:00 +02:00
2019-04-26 11:14:49 +02:00
for direction in self . directionsPerMsg [ msg . id ] :
r = self . getNextChapterForMsg ( direction . msgTo , True , depth + 1 )
if r :
return r
# none found
return None
2019-01-18 12:42:50 +01:00
2019-05-12 12:17:03 +02:00
async def run ( self , customStartMsgId = None , resuming = False ) :
2019-03-23 18:18:52 +01:00
self . logger . info ( " Starting story " )
2019-05-12 12:17:03 +02:00
if not resuming :
self . hugvey . eventLogger . info ( " story: start " )
self . timer . reset ( )
self . isRunning = True
if customStartMsgId is not None :
startMsg = self . get ( customStartMsgId )
else :
startMsg = self . startMessage
await self . setCurrentMessage ( startMsg )
2019-02-14 11:15:09 +01:00
else :
2019-05-12 12:17:03 +02:00
self . hugvey . eventLogger . info ( f " story: resume from { self . currentMessage } " )
self . isRunning = True
if not self . lastMsgFinishTime and self . currentMessage :
await self . setCurrentMessage ( self . currentMessage )
2019-05-12 19:51:54 +02:00
2019-01-18 12:42:50 +01:00
await self . _renderer ( )
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def isFinished ( self ) :
2019-01-25 10:43:55 +01:00
if hasattr ( self , ' finish_time ' ) and self . finish_time :
return time . time ( ) - self . finish_time
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
return False
2019-05-11 17:31:00 +02:00
2019-04-25 17:39:44 +02:00
def _finish ( self ) :
"""
Finish story and set hugvey to the right state
"""
self . finish ( )
#stop google etc:
2019-04-27 12:13:34 +02:00
self . hugvey . available ( )
2019-05-11 17:31:00 +02:00
2019-01-22 08:59:45 +01:00
def finish ( self ) :
2019-04-25 17:39:44 +02:00
"""
Finish only the story
"""
2019-03-23 18:18:52 +01:00
self . logger . info ( f " Finished story for { self . hugvey . id } " )
2019-03-27 13:36:09 +01:00
self . hugvey . eventLogger . info ( " story: finished " )
2019-04-24 13:38:41 +02:00
self . stop ( )
2019-01-22 08:59:45 +01:00
self . finish_time = time . time ( )
self . timer . pause ( )
2019-05-17 20:47:33 +02:00
if self . hugvey . google :
self . hugvey . google . stop ( )
2019-05-11 17:31:00 +02:00
2019-05-12 15:06:00 +02:00
def calculateFinishesForMsg ( self , msgId , depth = 0 , checked = [ ] ) :
2019-06-11 15:10:46 +02:00
"""
BEWARE : checked = [ ] is evaluated at creation time of the method . Meaning that each call to this method
which doesn ' t explicitly specify the checked list, relies upon the list created at parse time. This means
subsequent call to the method make the list larger ! ! So the default should actually never be used . ( found
out the hard way ; - ) )
"""
# print(checked)
2019-06-08 16:10:46 +02:00
if msgId in checked :
2019-06-11 15:10:46 +02:00
# self.logger.log(LOG_BS, f"Finish for {msgId} already checked")
2019-06-08 16:10:46 +02:00
return [ ]
checked . append ( msgId )
2019-05-12 19:51:54 +02:00
2019-05-01 12:37:35 +02:00
if not msgId in self . directionsPerMsg or len ( self . directionsPerMsg [ msgId ] ) < 1 :
# is finish
return [ msgId ]
2019-05-11 17:31:00 +02:00
2019-06-11 15:10:46 +02:00
if depth == 400 :
2019-06-08 16:10:46 +02:00
self . logger . warn ( f " Very deep hidden message to calculate finish for: msgId { msgId } " )
# return []
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
finishes = [ ]
for d in self . directionsPerMsg [ msgId ] :
if d . msgTo . id == msgId :
continue
2019-05-12 15:06:00 +02:00
finishes . extend ( self . calculateFinishesForMsg ( d . msgTo . id , depth + 1 , checked ) )
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
# de-duplicate before returning
return list ( set ( finishes ) )
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
def calculateFinishesForStrands ( self ) :
for startMsgId in self . strands :
msg = self . get ( startMsgId ) #: :type msg: Message
if msg . isStart :
# ignore for the beginning
continue
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
self . logger . log ( LOG_BS , f " Get finishes for { startMsgId } " )
2019-06-11 15:10:46 +02:00
self . strands [ startMsgId ] = self . calculateFinishesForMsg ( startMsgId , checked = [ ] )
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
self . logger . log ( LOG_BS , f " Finishes: { self . strands } " )
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
def getFinishesForMsg ( self , msg ) :
"""
Find the end of strands
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
Most often they will be ' start ' s so to speed up these are pre - calculated
Others can be calculated on the spot
2019-05-11 17:31:00 +02:00
2019-05-01 12:37:35 +02:00
returns message ids
"""
2019-06-11 15:10:46 +02:00
self . logger . debug ( f " Get finishes for { msg . id } from { self . strands } " )
2019-05-01 12:37:35 +02:00
if msg . id in self . strands :
return self . strands [ msg . id ]
2019-05-11 17:31:00 +02:00
2019-06-11 15:10:46 +02:00
return self . calculateFinishesForMsg ( msg . id , checked = [ ] )
2019-05-11 17:31:00 +02:00
2019-05-10 15:14:13 +02:00
def getDefaultDirectionForMsg ( self , msg ) :
"""
There is only a default direction ( for reply contains diversion ) if it has
one , and only one , direction to go . If there ' s more, it should do nothing.
"""
if not msg . id in self . directionsPerMsg :
# is finish
return None
2019-05-11 17:31:00 +02:00
2019-05-10 15:14:13 +02:00
if len ( self . directionsPerMsg [ msg . id ] ) > 1 :
return None
2019-05-11 17:31:00 +02:00
2019-05-10 15:14:13 +02:00
# TODO: should the direction have at least a timeout condition set, or not perse?
2019-05-11 17:31:00 +02:00
return self . directionsPerMsg [ msg . id ] [ 0 ]
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
@classmethod
def getStateDir ( self ) :
return " /tmp "
# day = time.strftime("%Y%m%d")
# t = time.strftime("%H:%M:%S")
2019-05-12 19:51:54 +02:00
#
2019-05-11 23:34:06 +02:00
# 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)
2019-05-12 19:51:54 +02:00
@classmethod
2019-05-11 23:34:06 +02:00
def getStateFilename ( cls , hv_id ) :
return os . path . join ( cls . getStateDir ( ) , f " hugvey { hv_id } " )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def storeState ( self ) :
# TODO: stop stopwatch
2019-06-08 16:10:46 +02:00
fn = self . getStateFilename ( self . hugvey . lightId )
2019-05-11 23:34:06 +02:00
tmpfn = fn + ' .tmp '
self . stateSave = time . time ( )
2019-06-14 20:47:59 +02:00
self . lightStateSave = self . hugvey . lightStatus
2019-05-11 23:34:06 +02:00
with open ( tmpfn , ' wb ' ) as fp :
pickle . dump ( self , fp )
# write atomic to disk: flush, close, rename
fp . flush ( )
2019-05-12 19:51:54 +02:00
os . fsync ( fp . fileno ( ) )
2019-05-11 23:34:06 +02:00
os . rename ( tmpfn , fn )
self . logger . debug ( f " saved state to { fn } " )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def hasSavedState ( self ) :
2019-06-08 16:10:46 +02:00
return self . hugveyHasSavedState ( self . hugvey . lightId )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
@classmethod
def hugveyHasSavedState ( cls , hv_id ) :
return os . path . exists ( cls . getStateFilename ( hv_id ) )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
@classmethod
def loadStoryFromState ( cls , hugvey_state ) :
# restart stopwatch
2019-06-08 16:10:46 +02:00
with open ( cls . getStateFilename ( hugvey_state . lightId ) , ' rb ' ) as fp :
2019-05-11 23:34:06 +02:00
story = pickle . load ( fp )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
story . hugvey = hugvey_state
2019-06-14 20:47:59 +02:00
#: :type story: Story
2019-05-11 23:34:06 +02:00
story . logger = mainLogger . getChild ( f " { story . hugvey . id } " ) . getChild ( " story " )
2019-06-14 20:47:59 +02:00
# TODO: this is not really working because it is overridden by the set-status later.
story . hugvey . setLightStatus ( story . lightStateSave )
2019-05-11 23:34:06 +02:00
return story
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
@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 } " )
2019-05-12 19:51:54 +02:00
#
2019-05-11 23:34:06 +02:00
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 ( )
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
# Remove the unpicklable entries.
del state [ ' hugvey ' ]
del state [ ' logger ' ]
# del state['isRunning']
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
return state
2019-05-12 19:51:54 +02:00
2019-05-11 23:34:06 +02:00
def __setstate__ ( self , state ) :
self . __dict__ . update ( state )