diff --git a/hugvey/central_command.py b/hugvey/central_command.py index 203c8da..ed47588 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -26,6 +26,7 @@ from hugvey.speech.recorder import Recorder from pythonosc import udp_client, osc_server, dispatcher import copy from pythonosc.osc_server import AsyncIOOSCUDPServer +from hugvey.variablestore import VariableStore mainLogger = logging.getLogger("hugvey") @@ -93,6 +94,11 @@ class CentralCommand(object): voice_dir = os.path.join(self.config['web']['files_dir'], 'voices') self.voiceStorage = VoiceStorage(voice_dir, self.languageConfig) + varDb = os.path.join( + self.config['voice']['record_dir'], + 'hugvey_variable_store.db' + ) + self.variableStore = VariableStore(varDb) self.panopticon = Panopticon(self, self.config, self.voiceStorage) @@ -419,6 +425,8 @@ class CentralCommand(object): self.catchException(self.oscListener())) self.tasks['redLightController'] = self.loop.create_task( self.catchException(self.redLightController())) + self.tasks['variableStore'] = self.loop.create_task( + self.catchException(self.variableStore.queueProcessor())) for hid in self.hugvey_ids: self.tasks['voiceListener'] = self.loop.create_task( diff --git a/hugvey/story.py b/hugvey/story.py index 81373a8..d3e09e3 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -142,6 +142,9 @@ class Message(object): asyncio.get_event_loop().create_task(self.getAudioFilePath()) # asyncio.get_event_loop().call_soon_threadsafe(self.getAudioFilePath) self.logger.warn(f"started {name}") + + 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 def getText(self): # sort reverse to avoid replacing the wrong variable @@ -150,7 +153,7 @@ class Message(object): # self.logger.debug(f"Getting text for {self.id}") for var in self.variables: self.logger.debug(f"try replacing ${var} with {self.variableValues[var]} in {text}") - replacement = self.variableValues[var] if (self.variableValues[var] is not None) else self.story.configuration.nothing_text #TODO: translate nothing to each language + replacement = self.getVariableValue(var) text = text.replace('$'+var, replacement) return text @@ -330,6 +333,9 @@ class Condition(object): condition.method = condition._hasPlayed if data['type'] == "variableEquals": condition.method = condition._variableEquals + if data['type'] == "variable_storage": + condition.method = condition._hasVariableStorage + condition.hasRan = False if 'vars' in data: condition.vars = data['vars'] @@ -450,6 +456,32 @@ 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 + + number = int(self.vars['number']) + varValues = story.hugvey.command.variableStore.getLastOfName(self.vars['var_name'], number) + self.hasRan = True + + if len(varValues) < number: + story.logger.info(f"{self.id}: Too few instances of {self.vars['var_name']}") + return False + + for i in range(number): + story.setVariableValue( + f"stored_{self.vars['var_name']}_{i+1}", + varValues[i], + store=False + ) + + return True def _hasMetReplyContains(self, story) -> bool: """ @@ -1237,13 +1269,15 @@ class Story(object): else: self.variables[variableName].append(message) - def setVariableValue(self, name, value): + 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}") else: self.logger.debug(f"Set variable {name} to {value}") - + self.variableValues[name] = value + if store: + self.hugvey.command.variableStore.addVariable(name, value, self.hugvey.id) if name not in self.variables: return diff --git a/hugvey/variablestore.py b/hugvey/variablestore.py new file mode 100644 index 0000000..b25e804 --- /dev/null +++ b/hugvey/variablestore.py @@ -0,0 +1,61 @@ +import sqlite3 +import asyncio +import logging + +mainLogger = logging.getLogger("hugvey") +logger = mainLogger.getChild("variableStore") + + + +class Variable: + def __init__(self, name: str, value: str, hugveyId: int): + self.name = name + self.value = value + self.hugveyId = hugveyId + +class VariableStore: + def __init__(self, db_filename): + self.conn = sqlite3.connect(db_filename, check_same_thread=False) + + # make sure the table exits. + createSqls = [""" + CREATE TABLE IF NOT EXISTS `variables` ( + `name` VARCHAR(255), + `hugvey` INTEGER, + `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `val` VARCHAR(1024) + ); + """, + """ + CREATE INDEX IF NOT EXISTS `name_time` ON `variables` ( + `name`, + `createdAt` DESC + ); + """] + cur = self.conn.cursor() + for sql in createSqls: + cur.execute(sql) + self.conn.commit() + self.q = asyncio.Queue() + + def addVariable(self, name, value, hugveyId): + logger.debug(f"Queing storing of {name} for {hugveyId}") + self.q.put_nowait(Variable(name, value, hugveyId)) + + async def queueProcessor(self): + while True: + #: :var v: Variable + v = await self.q.get() + c = self.conn.cursor() + logger.info(f"Store variable {v.name} for {v.hugveyId}: '{v.value}'") + c.execute("INSERT INTO variables (name, hugvey, createdAt, val) VALUES (?,?, current_timestamp,?)", (v.name, v.hugveyId, v.value)) + self.conn.commit() + c.close() + + def getLastOfName(self, name, n = 10): + cur = self.conn.cursor() + logging.debug(f"Get last {n} stored variables of {name}") + cur.execute("SELECT val FROM variables WHERE name = ? ORDER BY createdAt DESC LIMIT ?", (name, n)) + values = [v[0] for v in cur.fetchall()] + cur.close() + return values \ No newline at end of file diff --git a/www/js/hugvey_console.js b/www/js/hugvey_console.js index 55e2703..055e6ad 100644 --- a/www/js/hugvey_console.js +++ b/www/js/hugvey_console.js @@ -1607,6 +1607,11 @@ class Graph { '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." }, 'needsReply': { 'type': 'checkbox', label: "Reply needed", "title": "If checked, the timeout is counted if met. Used by consecutive-timeouts diversions." }, }, + 'variable_storage': { + // when matched, variable will be accessible as {store_name_1} + 'var_name': { 'label': "Variable name", 'type': 'text', 'description': "When matched, variable will be accessible as $stored_VARNAME_1, $stored_VARNAME_2.. etc (use the name given here instead of VARNAME)" }, + 'number': { 'label': "Nr. of items to get", 'type': 'number', 'value': 5, 'min': 0, 'step': 1 }, + }, 'replyContains': { 'delays.0.minReplyDuration': { 'type': 'number', 'value': 0, 'min': 0, 'step': 0.1, 'label': 'Delay 1 - reply duration', 'unit': "s", 'readonly': 'readonly' }, 'delays.0.waitTime': { 'type': 'number', 'value': 3, 'min': 0, 'step': 0.1 , 'label': 'Delay 1 - wait time', 'unit': "s" }, @@ -1668,6 +1673,10 @@ class Graph { // crel('span', {'class': 'label-unit'}, attr.hasOwnProperty('unit') ? attr['unit'] : "" ) ) ); + if(attr.hasOwnProperty('description')) { + inputs.push(crel('div', {'class':'description'}, attr['description'])); + } + } return inputs; }