Implement InstantMatch and OnlyIfNoReply

This commit is contained in:
Ruben van de Ven 2019-02-15 12:26:56 +01:00
parent 138ede6ad6
commit 46941e45a5
2 changed files with 71 additions and 27 deletions

View file

@ -162,11 +162,19 @@ class Condition(object):
return self.method(story) return self.method(story)
def _hasMetTimeout(self, story): def _hasMetTimeout(self, story):
now = time.time() now = story.timer.getElapsed()
# check if the message already finished playing # check if the message already finished playing
if not story.lastMsgFinishTime: if not story.lastMsgFinishTime:
return False return False
if 'onlyIfNoReply' in self.vars and self.vars['onlyIfNoReply']:
if story.currentReply and story.currentReply.hasUtterances():
logger.log(LOG_BS, f'Only if no reply has text! {story.currentReply.getText()}')
# 'onlyIfNoReply': only use this timeout if participants doesn't speak.
return False
# else:
# logger.debug('Only if no reply has no text yet!')
return now - story.lastMsgFinishTime >= float(self.vars['seconds']) return now - story.lastMsgFinishTime >= float(self.vars['seconds'])
def _hasMetReplyContains(self, story) -> bool: def _hasMetReplyContains(self, story) -> bool:
@ -197,6 +205,9 @@ class Condition(object):
for captureGroup in results: for captureGroup in results:
story.variableValues[captureGroup] = results[captureGroup] story.variableValues[captureGroup] = results[captureGroup]
if 'instantMatch' in self.vars and self.vars['instantMatch']:
logger.info(f"Instant match on {self.vars['regex']}, {self.vars}")
return True
# TODO: implement 'instant match' -> don't wait for isFinished() # TODO: implement 'instant match' -> don't wait for isFinished()
if r.isSpeaking(): if r.isSpeaking():
@ -206,11 +217,14 @@ class Condition(object):
# print(self.vars) # print(self.vars)
# either there's a match, or nothing to match at all # either there's a match, or nothing to match at all
if 'delays' in self.vars: if 'delays' in self.vars:
replyDuration = story.timer.getElapsed() - r.getFirstUtterance().endTime if story.lastMsgFinishTime is None:
timeSinceReply = story.timer.getElapsed() - r.getLastUtterance().endTime return False
# time between finishing playback and ending of speaking:
replyDuration = r.getLastUtterance().endTime - story.lastMsgFinishTime
delays = sorted(self.vars['delays'], key=lambda k: float(k['minReplyDuration']), reverse=True) delays = sorted(self.vars['delays'], key=lambda k: float(k['minReplyDuration']), reverse=True)
for delay in delays: for delay in delays:
if replyDuration > float(delay['minReplyDuration']): if replyDuration > float(delay['minReplyDuration']):
timeSinceReply = story.timer.getElapsed() - r.getLastUtterance().endTime
logger.log(LOG_BS, f"check delay duration is now {replyDuration}, already waiting for {timeSinceReply}, have to wait {delay['waitTime']}") logger.log(LOG_BS, f"check delay duration is now {replyDuration}, already waiting for {timeSinceReply}, have to wait {delay['waitTime']}")
if timeSinceReply > float(delay['waitTime']): if timeSinceReply > float(delay['waitTime']):
return True return True
@ -435,6 +449,9 @@ class Story(object):
self.log = [] # all nodes/elements that are triggered self.log = [] # all nodes/elements that are triggered
self.currentReply = None self.currentReply = None
for msg in self.getMessages():
pass
def add(self, obj): def add(self, obj):
if obj.id in self.elements: if obj.id in self.elements:
# print(obj) # print(obj)
@ -460,6 +477,8 @@ class Story(object):
if id in self.elements: if id in self.elements:
return self.elements[id] return self.elements[id]
return None return None
def getMessages(self):
return [el for el in self.elements if type(el) == Message]
def stop(self): def stop(self):
logger.info("Stop Story") logger.info("Stop Story")
@ -482,7 +501,8 @@ class Story(object):
if e['event'] == "playbackFinish": if e['event'] == "playbackFinish":
if e['msgId'] == self.currentMessage.id: if e['msgId'] == self.currentMessage.id:
self.lastMsgFinishTime = time.time() #TODO: migrate value to Messagage instead of Story
self.lastMsgFinishTime = self.timer.getElapsed()
if self.currentMessage.id not in self.directionsPerMsg: if self.currentMessage.id not in self.directionsPerMsg:
logger.info("THE END!") logger.info("THE END!")
@ -573,8 +593,14 @@ class Story(object):
self.currentMessage = message self.currentMessage = message
self.lastMsgTime = time.time() self.lastMsgTime = time.time()
self.lastMsgFinishTime = None # to be filled in by the event self.lastMsgFinishTime = None # to be filled in by the event
# if not reset:
self.previousReply = self.currentReply # we can use this for interrptions self.previousReply = self.currentReply # we can use this for interrptions
self.currentReply = self.currentMessage.reply self.currentReply = self.currentMessage.reply
# 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
logger.info("Current message: ({0}) \"{1}\"".format( logger.info("Current message: ({0}) \"{1}\"".format(
message.id, message.text)) message.id, message.text))

View file

@ -15,7 +15,6 @@ class Panopticon {
}, },
methods: { methods: {
time_passed: function( hugvey, property ) { time_passed: function( hugvey, property ) {
console.log( "property!", Date( hugvey[property] * 1000 ) );
return moment( Date( hugvey[property] * 1000 ) ).fromNow(); return moment( Date( hugvey[property] * 1000 ) ).fromNow();
}, },
timer: function(hugvey, property) { timer: function(hugvey, property) {
@ -202,14 +201,12 @@ class Graph {
let graph = this; let graph = this;
this.controlDown = false; this.controlDown = false;
document.addEventListener( 'keydown', function( e ) { document.addEventListener( 'keydown', function( e ) {
console.log( e );
if ( e.which == "16" ) { // shift if ( e.which == "16" ) { // shift
graph.controlDown = true; graph.controlDown = true;
document.body.classList.add( 'controlDown' ); document.body.classList.add( 'controlDown' );
} }
} ); } );
document.addEventListener( 'keyup', function( e ) { document.addEventListener( 'keyup', function( e ) {
console.log( e );
if ( e.which == "16" ) { // shift if ( e.which == "16" ) { // shift
graph.controlDown = false; graph.controlDown = false;
document.body.classList.remove( 'controlDown' ); document.body.classList.remove( 'controlDown' );
@ -236,8 +233,6 @@ class Graph {
clickMsg( msg ) { clickMsg( msg ) {
// event when a message is clicked. // event when a message is clicked.
console.log( msg );
if ( this.controlDown ) { if ( this.controlDown ) {
this.secondarySelectMsg( msg ); this.secondarySelectMsg( msg );
} else { } else {
@ -282,7 +277,7 @@ class Graph {
let startAttributes = { let startAttributes = {
'name': msg['@id'] + '-start', 'name': msg['@id'] + '-start',
'readonly': 'readonly', // 'readonly': 'readonly',
'type': 'checkbox', 'type': 'checkbox',
'on': { 'on': {
'change': this.getEditEventListener() 'change': this.getEditEventListener()
@ -344,10 +339,10 @@ class Graph {
'change': function(e) { 'change': function(e) {
audioSpan.innerHTML = "..."; audioSpan.innerHTML = "...";
panopticon.graph.saveJson(msg['@id'], e.target, function(e2){ panopticon.graph.saveJson(msg['@id'], e.target, function(e2){
console.log(e); // console.log(e, e2);
audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>"; audioSpan.innerHTML = e.target.files[0].name + "<sup>*</sup>";
// reload graph: // reload graph:
console.log('reload', panopticon.graph.language_code); // console.log('reload', panopticon.graph.language_code);
panopticon.loadNarrative(panopticon.graph.language_code); panopticon.loadNarrative(panopticon.graph.language_code);
}); });
// console.log(this,e); // console.log(this,e);
@ -461,7 +456,6 @@ class Graph {
for ( let conditionId of direction['conditions'] ) { for ( let conditionId of direction['conditions'] ) {
let condition = this.getNodeById( conditionId ); let condition = this.getNodeById( conditionId );
console.log(conditionId, condition);
directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) ); directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) );
} }
@ -518,7 +512,8 @@ class Graph {
getConditionTypes() { getConditionTypes() {
return { return {
'timeout': { 'timeout': {
'seconds': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1, 'unit': "s" } 'seconds': { 'type': 'number', 'value': 10, 'min': 0, 'step': 0.1, 'unit': "s" },
'onlyIfNoReply': { 'type': 'checkbox', label: "Only if no reply", "title": "This timeout is only used if the participant doesn't say a word. If the participant starts speaking within the time of this timeout condition, only other conditions are applicable." }
}, },
'replyContains': { 'replyContains': {
'delays.0.minReplyDuration': { 'type': 'number', 'value': 0, 'min': 0, 'step': 0.1, 'label': 'Delay 1 - reply duration', 'unit': "s", 'readonly': 'readonly' }, 'delays.0.minReplyDuration': { 'type': 'number', 'value': 0, 'min': 0, 'step': 0.1, 'label': 'Delay 1 - reply duration', 'unit': "s", 'readonly': 'readonly' },
@ -541,12 +536,16 @@ class Graph {
attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`; attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`;
if(typeof values != 'undefined') { if(typeof values != 'undefined') {
let value = this._getValueForPath(v, values); let value = this._getValueForPath(v, values);
if(attr['type'] == 'checkbox' ) {
if(value)
attr['checked'] = 'checked';
}
attr['value'] = typeof value == 'undefined' ? "": value; attr['value'] = typeof value == 'undefined' ? "": value;
attr['on'] = { attr['on'] = {
'change': this.getEditEventListener() 'change': this.getEditEventListener()
} ; } ;
} else { } else {
console.log(attr); // console.log(attr);
} }
inputs.push( inputs.push(
@ -573,17 +572,21 @@ class Graph {
_getValueForPath(path, vars) { _getValueForPath(path, vars) {
path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value
let v = vars; let v = vars;
let result = null;
for ( let i = 0; i < path.length; i++ ) { for ( let i = 0; i < path.length; i++ ) {
if(!isNaN(parseInt(path[i])) && isFinite(path[i])) { if(!isNaN(parseInt(path[i])) && isFinite(path[i])) {
// is int, use array, instead of obj // is int, use array, instead of obj
path[i] = parseInt(path[i]); path[i] = parseInt(path[i]);
} }
v = v[path[i]]; v = v[path[i]];
if(i == path.length - 1) {
result = v;
}
if(typeof v == 'undefined') { if(typeof v == 'undefined') {
break; break;
} }
} }
return v; return result;
} }
/** /**
@ -595,11 +598,9 @@ class Graph {
*/ */
_formPathToVars(path, value, vars) { _formPathToVars(path, value, vars) {
path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value
console.log(path);
let res = vars; let res = vars;
for ( let i = 0; i < path.length; i++ ) { for ( let i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) { if ( i == ( path.length - 1 ) ) {
console.log( 'last', path[i] );
res[path[i]] = value; res[path[i]] = value;
} else { } else {
if(!isNaN(parseInt(path[i+1])) && isFinite(path[i+1])) { if(!isNaN(parseInt(path[i+1])) && isFinite(path[i+1])) {
@ -636,8 +637,24 @@ class Graph {
form.delete( 'type' ); form.delete( 'type' );
let label = form.get( 'label' ); let label = form.get( 'label' );
form.delete( 'label' ); form.delete( 'label' );
// checkboxes to true/false
let defs = g.getConditionTypes()[type];
console.log(defs);
for(let field in defs) {
console.log(field);
if(defs[field]['type'] == 'checkbox') {
console.info('configure checkbox', field);
form.set(field, form.has(field));
}
}
let vars = {}; let vars = {};
for ( var pair of form.entries() ) { for ( var pair of form.entries() ) {
// FormData only has strings & blobs, we want booleans:
if(pair[1] === 'true') pair[1] = true;
if(pair[1] === 'false') pair[1] = false;
vars = g._formPathToVars(pair[0], pair[1], vars); vars = g._formPathToVars(pair[0], pair[1], vars);
} }
// TODO: checkboxes // TODO: checkboxes
@ -841,18 +858,20 @@ class Graph {
getEditEventListener() { getEditEventListener() {
let graph = this; let graph = this;
let el = function( e ) { let el = function( e ) {
console.info("Changed", e);
let parts = e.srcElement.name.split( '-' ); let parts = e.srcElement.name.split( '-' );
let field = parts.pop(); let field = parts.pop();
let id = parts.join('-'); let id = parts.join('-');
console.log( this, graph );
let node = graph.getNodeById( id ); let node = graph.getNodeById( id );
let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value
console.log(id, node);
var res = node; var res = node;
let value = e.srcElement.value
if(e.srcElement.type == 'checkbox') {
value = e.srcElement.checked;
}
for ( var i = 0; i < path.length; i++ ) { for ( var i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) { if ( i == ( path.length - 1 ) ) {
console.log( 'last', path[i] ); res[path[i]] = value;
res[path[i]] = e.srcElement.value;
} else { } else {
res = res[path[i]]; res = res[path[i]];
} }
@ -885,6 +904,7 @@ class Graph {
} }
d.push( n ); d.push( n );
} }
console.info("Jsonified graph:",d);
return JSON.stringify( d ); return JSON.stringify( d );
} }
@ -923,7 +943,7 @@ class Graph {
let blob = new Blob( [this.getJsonString()], { type: "application/json" } ); let blob = new Blob( [this.getJsonString()], { type: "application/json" } );
formData.append( "json", blob ); formData.append( "json", blob );
console.log( formData ); console.info("Save json", formData );
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open( "POST", "http://localhost:8888/upload" ); request.open( "POST", "http://localhost:8888/upload" );
@ -1010,7 +1030,6 @@ class Graph {
this.clickMsg( d ); this.clickMsg( d );
}.bind( this ) ) }.bind( this ) )
; ;
console.log( 'a' );
let circle = newNodeG.append( "circle" ) let circle = newNodeG.append( "circle" )
.attr( 'r', this.nodeSize ) .attr( 'r', this.nodeSize )
// .text(d => d.id) // .text(d => d.id)
@ -1058,7 +1077,6 @@ class Graph {
link.attr( 'class', l => { return `link ` + ( l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions" ); } ); link.attr( 'class', l => { return `link ` + ( l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions" ); } );
link.attr('id', (l) => l['@id']); link.attr('id', (l) => l['@id']);
// console.log('c');
let formatText = ( t ) => { let formatText = ( t ) => {
if ( t.length > this.maxChars ) { if ( t.length > this.maxChars ) {
return t.substr( 0, this.maxChars - 3 ) + '...'; return t.substr( 0, this.maxChars - 3 ) + '...';