Pitch modifier for story configuration

This commit is contained in:
Ruben van de Ven 2020-01-13 08:23:30 +01:00
parent 7c7fcf071a
commit ac11645dd0
2 changed files with 112 additions and 92 deletions

View File

@ -25,8 +25,10 @@ from .communication import LOG_BS
mainLogger = logging.getLogger("hugvey")
logger = mainLogger.getChild("narrative")
class Utterance(object):
"""Part of a reply"""
def __init__(self, startTime):
self.startTime = startTime
self.endTime = None
@ -34,9 +36,9 @@ class Utterance(object):
self.lastUpdateTime = startTime
def setText(self, text, now):
self.text = text.lower() # always lowercase
self.text = text.lower() # always lowercase
self.lastUpdateTime = now
def hasText(self):
return len(self.text) > 0
@ -77,7 +79,7 @@ class Message(object):
self.lightChange = None
self.didRepeat = False
self.fileError = False
# Used by diversions, autogenerated directions should link to next chapter mark instead of the given msgTo
self.generatedDirectionsJumpToChapter = False
@ -151,7 +153,7 @@ class Message(object):
# if not None in self.variableValues.values():
# self.logger.warn(f"now fetch {name} for {self.id}")
# asyncio.get_event_loop().create_task(self.getAudioFilePath())
def getVariableValue(self, var):
return self.variableValues[var] if (self.variableValues[var] is not None) else self.story.configuration.nothing_text #TODO: translate nothing to each language
@ -173,7 +175,7 @@ class Message(object):
if self.label and len(self.label):
return self.label
return self.getText()
def getTextLabel(self):
"""
A combination of getText and getLabel for maximum verbosity
@ -236,8 +238,8 @@ class Message(object):
await s.send_json(info)
filename = await s.recv_string()
s.close()
# TODO: should this go trough the event Queue? risking a too long delay though
# TODO: should this go trough the event Queue? risking a too long delay though
if filename == 'local/crash.wav' or len(filename) < 1:
self.logger.warning("Noting crash")
self.fileError = True
@ -246,7 +248,7 @@ class Message(object):
self.logger.debug(f"Fetched audio for {textlabel}: {filename}")
return filename
class Reply(object):
def __init__(self, message: Message):
@ -373,7 +375,7 @@ class Condition(object):
if 'vars' in data:
condition.vars = data['vars']
if 'regex' in condition.vars:
condition.vars['regex'] = condition.vars['regex'].rstrip()
@ -435,7 +437,7 @@ class Condition(object):
self.vars['variable']
)
return r
def _hasTimer(self, story) -> bool:
if not story.lastMsgFinishTime:
return False
@ -443,7 +445,7 @@ class Condition(object):
loopTime = story.hugvey.command.timer.getElapsed() % 3600
ltTime = int(self.vars['less_than'])
gtTime = int(self.vars['more_than'])
if not ltTime and not gtTime:
# ignore invalid times
return
@ -455,21 +457,21 @@ class Condition(object):
r = True
else:
r = False
if 'inverseMatch' in self.vars and self.vars['inverseMatch']:
r = not r
self.logInfo = "Looptime is {} {} < {} < {}".format(
'' if r else 'not',
'' if r else 'not',
f'{gtTime}' if gtTime else '-',
loopTime,
f'{ltTime}' if ltTime else '-',
)
return r
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
@ -483,10 +485,10 @@ class Condition(object):
r = (v1 != v2)
else:
r = (v1 == v2)
story.logger.info("'{}' {} '{}' ({})".format(v1, '==' if v1 == v2 else '!=', v2, r))
return r
def _hasDiverged(self, story) -> bool:
if not story.lastMsgFinishTime:
return False
@ -509,15 +511,15 @@ class Condition(object):
)
return r
def _hasAudioError(self, story) -> bool:
if not story.currentMessage or not story.currentMessage.fileError:
return False
self.logInfo = f"Has error loading audio file for {story.currentMessage.id}"
return True
def _hasPlayed(self, story) -> bool:
if not story.lastMsgFinishTime:
return False
@ -547,12 +549,12 @@ class Condition(object):
)
return r
def _hasVariableStorage(self, story) -> bool:
if not story.lastMsgFinishTime:
return False
if self.hasRan:
# Prevent multiple runs of the same query within eg. waiting for a timeout.
return False
@ -561,11 +563,11 @@ class Condition(object):
unique = bool(self.vars['unique']) if 'unique' in self.vars else False
varValues = story.hugvey.command.variableStore.getLastOfName(self.vars['var_name'], story.language_code, number, unique)
self.hasRan = True
if len(varValues) < number:
story.logger.warn(f"{self.id}: Too few instances of {self.vars['var_name']}, only {len(varValues)} in store")
return False
for i in range(number):
story.setVariableValue(
f"stored_{self.vars['var_name']}_{i+1}",
@ -574,7 +576,7 @@ class Condition(object):
)
return True
def _hasMetReplyContains(self, story) -> bool:
"""
Check the reply for specific characteristics:
@ -789,15 +791,15 @@ class Diversion(object):
if self.type != 'repeat' and self.type !='interrupt':
# repeat diversion should be usable infinte times
self.hasHit = True
story.addToLog(self)
story.hugvey.eventLogger.info(f"diverge {self.id}")
except Exception as e:
story.logger.critical("Exception when attempting diversion")
story.logger.exception(e)
return False
return r
def createReturnDirectionsTo(self, story, startMsg, returnMsg, originalDirection = None, inheritTiming = True, timeoutDuration = .5, replyContainsDurations = None):
@ -840,11 +842,11 @@ class Diversion(object):
msg = story.get(msgId)
if not msg:
continue
usedReturnMessage = returnMsg
if msg.generatedDirectionsJumpToChapter:
usedReturnMessage = story.getNextChapterForMsg(returnMsg, canIncludeSelf=True)
if not usedReturnMessage:
# in case of a diversion in the last bit of the story, it can be there there is no return message.
raise Exception(f"No return message found for {msg.id}")
@ -885,7 +887,7 @@ class Diversion(object):
story.add(condition2)
direction.isDiversionReturn = True # will clear the currentDiversion on story
story.diversionDirections.append(direction)
story.diversionDirections.append(direction)
story.logger.info(f"Created direction: {direction.id} ({msg.id} -> {usedReturnMessage.id}) {condition.id} with timeout {finalTimeoutDuration}s")
story.add(condition)
story.add(direction)
@ -962,7 +964,7 @@ class Diversion(object):
if not direction:
# ignore the direction argument, and only check if the current message has a valid default
return
waitTime = story.applyTimeFactor(1.8 if 'waitTime' not in self.params else float(self.params['waitTime']))
timeSince = story.currentReply.getTimeSinceLastUtterance()
if timeSince < waitTime:
@ -986,7 +988,7 @@ class Diversion(object):
story.logger.critical(f"Not a valid message id for diversion: {self.params['msgId']}")
return
if 'nextChapterOnReturn' in self.params and self.params['nextChapterOnReturn']:
msgTo = story.getNextChapterForMsg(story.currentMessage, False) or direction.msgTo
returnInheritTiming = False
@ -1014,33 +1016,33 @@ class Diversion(object):
#: :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_second'])
window_close_second = window_open_second + float(self.params['window'])
# Only keep a 1h loop
now = story.hugvey.command.timer.getElapsed() % 3600
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)
await story.setCurrentMessage(msg)
story.currentDiversion = self
return True
async def _divergeIfRepeatRequest(self, story, msgFrom, msgTo, direction):
"""
Participant asks if message can be repeated.
@ -1052,7 +1054,7 @@ class Diversion(object):
# Perhaps set isFinished when matching condition.
if story.currentReply is None or story.currentReply.getTimeSinceLastUtterance() < story.applyTimeFactor(1.8):
return
if story.currentMessage.didRepeat:
# repeat only once
return
@ -1151,7 +1153,6 @@ class Diversion(object):
async def _returnAfterTimeout(self, story):
story.logger.info(f"Finalise diversion: {self.id}")
async def _divergeIfInterrupted(self, story, msgFrom, msgTo, direction):
"""
This is here as a placeholder for the interruption diversion.
@ -1177,14 +1178,16 @@ class Diversion(object):
return True
class Configuration(object):
id = 'configuration'
volume = 1 # Volume multiplier for 'play' command
nothing_text = "nothing" # When variable is not set, but used in sentence, replace it with this word.
time_factor = 1
tempo_factor = 1
volume = 1 # Volume multiplier for 'play' command
nothing_text = "nothing" # When variable is not set, but used in sentence, replace it with this word.
time_factor = 1 # time is multiplied to timeouts etc. (not playback)
tempo_factor = 1 # tempo is multiplied (playback)
pitch_modifier = 1 # pitch is added (playback)
light0_intensity = 0
light0_fade = 30. # fade duration in seconds
light0_fade = 30. # fade duration in seconds
light0_isSophie = False
light1_intensity = 150
light1_fade = 10.
@ -1210,7 +1213,7 @@ class Configuration(object):
l = []
for i in range(5):
l.append({
'intensity': int(c[f"light{i}_intensity"]),
'intensity': int(c[f"light{i}_intensity"]),
'fade': float(c[f"light{i}_fade"]),
'isSophie': float(c[f"light{i}_isSophie"])
})
@ -1396,13 +1399,13 @@ class Story(object):
def setVariableValue(self, name, value, store=True):
if name not in self.variables:
self.logger.warn(f"Set variable that is not needed in the story: {name}")
if name in self.variableValues and self.variableValues[name] == value:
self.logger.debug(f"Skip double setting of variable {name} to {value}")
return
self.logger.debug(f"Set variable {name} to {value}")
self.variableValues[name] = value
if store:
self.hugvey.command.variableStore.addVariable(self.runId, name, value, self.hugvey.id, self.language_code)
@ -1604,7 +1607,7 @@ class Story(object):
if len(e['transcript'].strip()) < 1:
self.logger.warning(f'ignore empty transcription {e}')
continue
# participants speaks, reset counter
self.stats['consecutiveSilentTimeouts'] = 0
@ -1641,7 +1644,7 @@ class Story(object):
utterance.setText(e['transcript'], utterance.lastUpdateTime)
else:
utterance.setText(e['transcript'], now)
self.hugvey.eventLogger.debug("speaking: content {} \"{}\"".format(id(utterance), e['transcript']))
if not self.timer.hasMark('first_speech'):
self.timer.setMark('first_speech')
@ -1678,7 +1681,7 @@ class Story(object):
# back to a previous point in time.
# self.logger.warn("Skipping double direction for diversion")
continue
condition = self._processDirection(direction)
if not condition:
continue
@ -1688,7 +1691,7 @@ class Story(object):
chosenDirection = direction
metCondition = condition
break
isDiverging = await self._processDiversions(chosenDirection)
@ -1706,12 +1709,12 @@ class Story(object):
if direction.isDiversionReturn and not direction.diversionHasReturned:
self.logger.info(f"Mark diversion as returned for return direction {direction.id}")
direction.diversionHasReturned = True
# chosenDirection.diversionHasReturned = True
await self.currentDiversion.finalise(self)
await self.setCurrentMessage(chosenDirection.msgTo, allowReplyInterrupt=allowReplyInterrupt)
return chosenDirection
@ -1782,22 +1785,22 @@ class Story(object):
def logHasMsg(self, node):
return node in self.msgLog
def checkIfGone(self):
'''
Make a guestimation if the audience has left... just really a simple timer check.
If we do think so, give an error and stop the conversation
'''
if not self.lastMsgFinishTime:
# don't do it when hugvey is speaking
return
if self.timer.hasMark('last_speech') and self.timer.getElapsed('last_speech') > 30*60:
self.hugvey.eventLogger.warning("Audience is quiet for too long...stopping")
self.logger.warning("Audience is quiet, force END!")
self._finish()
def checkIfHanging(self):
'''
Make a guestimation if the story is hanging at a message. Raise exception once.
@ -1807,9 +1810,9 @@ class Story(object):
# or when it already gave the error for this message
return
diff = self.timer.getElapsed() - self.lastMsgFinishTime
safeDiff = self.hugvey.command.config['story']['hugvey_critical_silence'] if 'hugvey_critical_silence' in self.hugvey.command.config['story'] else 90
if diff > safeDiff:
self.hugvey.eventLogger.warning("Hugvey is quiet for very long!")
self.logger.critical("Hugvey is quiet for very long!") # critical messages are forwarded to telegram
@ -1896,7 +1899,7 @@ class Story(object):
message.id, message.getTextLabel()))
if message.id != self.startMessage.id:
self.addToLog(message)
self.hugvey.eventLogger.info(f"message: {message.id} {message.uuid} start \"{message.getLabel()}\"")
# TODO: prep events & timer etc.
@ -1916,9 +1919,12 @@ class Story(object):
params['vol'] = "{:.4f}".format(params['vol'])
params['tempo'] = (float(params['tempo']) if 'tempo' in params else 1) * (float(self.configuration.tempo_factor) if hasattr(self.configuration, 'tempo_factor') else 1)
duration = float(duration) / params['tempo']
params['tempo'] = "{:.4f}".format(params['tempo'])
params['pitch'] = (float(params['pitch']) if 'pitch' in params else 0)\
+ (float(self.configuration.pitch_modifier) if hasattr(self.configuration, 'pitch_modifier') else 0)
params['pitch'] = "{:.4f}".format(params['pitch'])
# self.hugvey.google.pause() # pause STT to avoid text events while decision is made
self.hugvey.sendCommand({
'action': 'play',
@ -1927,7 +1933,7 @@ class Story(object):
'params': params,
'duration': duration
})
if message.lightChange is not None:
self.fadeLightPreset(message.lightChange)
# self.hugvey.setLightStatus(message.lightChange)
@ -1944,18 +1950,18 @@ class Story(object):
logmsg += "\n- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions)
self.logger.log(LOG_BS,logmsg)
# if message.id != self.startMessage.id:
# self.storeState()
def fadeLightPreset(self, presetNr: int):
if presetNr < 0 or presetNr > 4:
self.logger.critical(f"Error parsing light fade preset code '{presetNr}'")
return
preset = self.configuration.getLightPresets()[presetNr]
self.currentLightPresetNr = presetNr
self.hugvey.transitionLight(preset['intensity'], preset['fade'], preset['isSophie'])
def getCurrentDirections(self):
@ -2039,7 +2045,7 @@ class Story(object):
if msgId in checked:
# self.logger.log(LOG_BS, f"Finish for {msgId} already checked")
return []
checked.append(msgId)
if not msgId in self.directionsPerMsg or len(self.directionsPerMsg[msgId]) < 1:
@ -2085,7 +2091,7 @@ class Story(object):
return self.strands[msg.id]
return self.calculateFinishesForMsg(msg.id, checked=[])
def applyTimeFactor(self, time) -> float:
"""
Apply the particularities of the configuration.time_factor

View File

@ -190,18 +190,18 @@ class Panopticon {
if(this.hugveys.selectedId) {
this.updateSelectedHugvey();
}
let avail = 0;
let blocked = 0;
for(let hv of this.hugveys.hugveys) {
if(hv.status =='available') avail ++;
if(hv.status =='blocked') blocked ++;
}
this.hugveys.blockedHugveys = blocked;
this.hugveys.availableHugveys = avail;
break;
case 'log':
@ -209,12 +209,12 @@ class Panopticon {
}
} );
}
selectHugvey(hv_id) {
this.hugveys.selectedId = hv_id;
this.send({ action: 'selection', selected_id: hv_id });
}
change_loop_time(newTime) {
console.log('update', newTime);
this.send({ action: 'loop_time', time: newTime });
@ -288,7 +288,7 @@ class Panopticon {
}
}
this.hugveys.selectedLang = code;
let req = new XMLHttpRequest();
let graph = this.graph;
req.addEventListener( "load", function( e ) {
@ -392,7 +392,7 @@ class Graph {
graph.saveJson();
el.classList.remove('loading');
}, 100);
} );
document.getElementById( 'btn-addMsg' ).addEventListener( 'click', function( e ) { graph.createMsg(); } );
document.getElementById( 'btn-diversions' ).addEventListener( 'click', function( e ) { graph.showDiversions(); } );
@ -497,7 +497,7 @@ class Graph {
}
else if(type == 'repeat') {
div['params']['regex'] = "can you repeat that\\?";
}
}
else if(type == 'collective_moment') {
div['params']['start_second'] = 20 * 60; // second to start
div['params']['window'] = 60; // how long to wait, in seconds
@ -1122,6 +1122,20 @@ class Graph {
'step': 0.01
})
),
crel(
'label',
"Playback pitch modifier: (< 0 is lower, >0 is higher)",
crel('input', {
'type': 'number',
'on': {
'change': function(e){
panopticon.graph.configuration['pitch_modifier'] = parseFloat(e.target.value)
}
},
'value': this.configuration.hasOwnProperty('pitch_modifier') ? this.configuration.pitch_modifier : 0,
'step': 1
})
),
crel('hr'),
crel('h2', 'Light fade setting #0'),
crel(
@ -1444,7 +1458,7 @@ class Graph {
"uploaded"
) : 'Auto-generated')
);
let lightOptions = [
crel("option", {'value': null}, "Do nothing")
];
@ -1463,18 +1477,18 @@ class Graph {
}
lightOptions.push(crel("option", l, `Fade preset #${i} (${intensity} in ${duration}s)`));
}
// let lightOptionNone = {'value': null}
//
//
// let lightOptionOn = {'value': 1}
// let lightOptionOff = {'value': 0}
//
//
// if(msg.hasOwnProperty('light')) {
// if(msg['light'] === 1) lightOptionOn['selected'] = 'selected';
// if(msg['light'] === 0) lightOptionOff['selected'] = 'selected';
// if(msg['light'] === null) lightOptionNone['selected'] = 'selected';
// }
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
crel('div', {
'class':'btn btn--delete btn--delete-msg',
@ -1893,7 +1907,7 @@ class Graph {
if(attr.hasOwnProperty('description')) {
inputs.push(crel('div', {'class':'description'}, attr['description']));
}
}
return inputs;
}