Fetch voice from Lyrebird
This commit is contained in:
parent
46941e45a5
commit
79424ccfbf
7 changed files with 186 additions and 34 deletions
|
@ -20,6 +20,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
from hugvey.voice import VoiceStorage
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("command")
|
logger = logging.getLogger("command")
|
||||||
|
@ -73,6 +74,7 @@ class CentralCommand(object):
|
||||||
|
|
||||||
self.loadLanguages()
|
self.loadLanguages()
|
||||||
|
|
||||||
|
|
||||||
self.panopticon = Panopticon(self, self.config)
|
self.panopticon = Panopticon(self, self.config)
|
||||||
|
|
||||||
def loadLanguages(self):
|
def loadLanguages(self):
|
||||||
|
@ -122,6 +124,7 @@ class CentralCommand(object):
|
||||||
"""
|
"""
|
||||||
prepare command to be picked up by the sender
|
prepare command to be picked up by the sender
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"COmmand {hv_id}: {msg}")
|
||||||
if threading.current_thread().getName() != 'MainThread':
|
if threading.current_thread().getName() != 'MainThread':
|
||||||
# Threading nightmares! Adding to queue from other thread/loop (not sure which is the isse)
|
# Threading nightmares! Adding to queue from other thread/loop (not sure which is the isse)
|
||||||
# won't trigger asyncios queue.get() so we have to do this thread
|
# won't trigger asyncios queue.get() so we have to do this thread
|
||||||
|
@ -226,9 +229,9 @@ class CentralCommand(object):
|
||||||
|
|
||||||
self.tasks = {} # collect tasks so we can cancel in case of error
|
self.tasks = {} # collect tasks so we can cancel in case of error
|
||||||
self.tasks['eventListener'] = self.loop.create_task(
|
self.tasks['eventListener'] = self.loop.create_task(
|
||||||
self.eventListener())
|
self.catchException(self.eventListener()))
|
||||||
self.tasks['commandSender'] = self.loop.create_task(
|
self.tasks['commandSender'] = self.loop.create_task(
|
||||||
self.commandSender())
|
self.catchException(self.commandSender()))
|
||||||
|
|
||||||
# we want the web interface in a separate thread
|
# we want the web interface in a separate thread
|
||||||
self.panopticon_thread = threading.Thread(
|
self.panopticon_thread = threading.Thread(
|
||||||
|
@ -240,6 +243,14 @@ class CentralCommand(object):
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.isRunning.clear()
|
self.isRunning.clear()
|
||||||
|
|
||||||
|
async def catchException(self, awaitable):
|
||||||
|
try:
|
||||||
|
print(awaitable)
|
||||||
|
await awaitable
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
logger.critical(f"Hugvey restart might be required but not implemented yet")
|
||||||
|
|
||||||
|
|
||||||
class HugveyState(object):
|
class HugveyState(object):
|
||||||
"""Represents the state of a Hugvey client on the server.
|
"""Represents the state of a Hugvey client on the server.
|
||||||
|
@ -309,6 +320,7 @@ class HugveyState(object):
|
||||||
|
|
||||||
async def catchException(self, awaitable):
|
async def catchException(self, awaitable):
|
||||||
try:
|
try:
|
||||||
|
print(awaitable)
|
||||||
await awaitable
|
await awaitable
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
@ -436,7 +448,8 @@ class HugveyState(object):
|
||||||
await self.isRunning.wait()
|
await self.isRunning.wait()
|
||||||
|
|
||||||
# new story instance on each run
|
# new story instance on each run
|
||||||
self.story = Story(self)
|
port = self.command.config['web']['port']
|
||||||
|
self.story = Story(self, port)
|
||||||
startMsgId = self.startMsgId
|
startMsgId = self.startMsgId
|
||||||
self.startMsgId = None # use only once, reset before 'run'
|
self.startMsgId = None # use only once, reset before 'run'
|
||||||
self.logger.warn(f"Starting from {startMsgId}")
|
self.logger.warn(f"Starting from {startMsgId}")
|
||||||
|
|
|
@ -11,6 +11,7 @@ def getTopic(hugvey_id):
|
||||||
|
|
||||||
|
|
||||||
def zmqSend(socket, hugvey_id, msg):
|
def zmqSend(socket, hugvey_id, msg):
|
||||||
|
logger.info("SEND: {}".format(msg))
|
||||||
msgData = json.dumps(msg)
|
msgData = json.dumps(msg)
|
||||||
topic = getTopic(hugvey_id)
|
topic = getTopic(hugvey_id)
|
||||||
logger.info("Send 0mq to {} containing {}".format(topic, msg))
|
logger.info("Send 0mq to {} containing {}".format(topic, msg))
|
||||||
|
|
|
@ -16,6 +16,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from hugvey import central_command
|
from hugvey import central_command
|
||||||
|
from hugvey.voice import VoiceStorage
|
||||||
|
|
||||||
logger = logging.getLogger("panopticon")
|
logger = logging.getLogger("panopticon")
|
||||||
|
|
||||||
|
@ -165,15 +166,41 @@ def getUploadHandler(central_command):
|
||||||
|
|
||||||
return UploadHandler
|
return UploadHandler
|
||||||
|
|
||||||
|
def getVoiceHandler(voiceStorage):
|
||||||
|
class VoiceHandler(tornado.web.RequestHandler):
|
||||||
|
async def get(self):
|
||||||
|
# TODO: we should be using ZMQ here...
|
||||||
|
text = self.get_argument('text')
|
||||||
|
isVariable = True if int(self.get_argument('variable')) >0 else False
|
||||||
|
fn = await voiceStorage.requestFile(text, isVariable)
|
||||||
|
if not fn:
|
||||||
|
raise Exception(f"No Filename for text: {text}")
|
||||||
|
|
||||||
|
if int(self.get_argument('filename')) == 1:
|
||||||
|
self.set_header("Content-Type","text/plain")
|
||||||
|
self.write(fn)
|
||||||
|
else:
|
||||||
|
self.set_header("Content-Type","audio/wave")
|
||||||
|
with open(fn, 'rb') as fp:
|
||||||
|
self.write(fp.read())
|
||||||
|
self.finish()
|
||||||
|
return VoiceHandler
|
||||||
|
|
||||||
|
|
||||||
class Panopticon(object):
|
class Panopticon(object):
|
||||||
def __init__(self, central_command, config):
|
def __init__(self, central_command, config):
|
||||||
self.command = central_command
|
self.command = central_command
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
voice_dir = os.path.join(self.config['web']['files_dir'], 'voices')
|
||||||
|
self.voiceStorage = VoiceStorage(voice_dir, self.config['voice']['token'])
|
||||||
|
|
||||||
self.application = tornado.web.Application([
|
self.application = tornado.web.Application([
|
||||||
(r"/ws", getWebSocketHandler(self.command)),
|
(r"/ws", getWebSocketHandler(self.command)),
|
||||||
(r"/local/(.*)", NonCachingStaticFileHandler,
|
(r"/local/(.*)", NonCachingStaticFileHandler,
|
||||||
{"path": config['web']['files_dir']}),
|
{"path": config['web']['files_dir']}),
|
||||||
(r"/upload", getUploadHandler(self.command)),
|
(r"/upload", getUploadHandler(self.command)),
|
||||||
|
(r"/voice", getVoiceHandler(self.voiceStorage)),
|
||||||
(r"/(.*)", tornado.web.StaticFileHandler,
|
(r"/(.*)", tornado.web.StaticFileHandler,
|
||||||
{"path": web_dir, "default_filename": 'index.html'}),
|
{"path": web_dir, "default_filename": 'index.html'}),
|
||||||
], debug=True)
|
], debug=True)
|
||||||
|
|
|
@ -3,7 +3,9 @@ import time
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import urllib.parse
|
||||||
from .communication import LOG_BS
|
from .communication import LOG_BS
|
||||||
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("narrative")
|
logger = logging.getLogger("narrative")
|
||||||
|
@ -54,6 +56,9 @@ class Message(object):
|
||||||
"""
|
"""
|
||||||
self.variables = re.findall('\$(\w+)', self.text)
|
self.variables = re.findall('\$(\w+)', self.text)
|
||||||
|
|
||||||
|
def hasVariables(self) -> bool:
|
||||||
|
return len(self.variables) > 0
|
||||||
|
|
||||||
def setReply(self, reply):
|
def setReply(self, reply):
|
||||||
self.reply = reply
|
self.reply = reply
|
||||||
|
|
||||||
|
@ -83,6 +88,25 @@ class Message(object):
|
||||||
'replyText': None if self.reply is None else [u.text for u in self.reply.utterances]
|
'replyText': None if self.reply is None else [u.text for u in self.reply.utterances]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def getAudioFilePath(self,story):
|
||||||
|
client = AsyncHTTPClient()
|
||||||
|
queryString = urllib.parse.urlencode({
|
||||||
|
'text': self.text,
|
||||||
|
'filename': 1,
|
||||||
|
'variable': 1 if self.hasVariables() else 0
|
||||||
|
})
|
||||||
|
request = HTTPRequest(
|
||||||
|
url = f"http://localhost:{story.panopticon_port}/voice?{queryString}",
|
||||||
|
method="GET"
|
||||||
|
)
|
||||||
|
logger.log(LOG_BS, request.url)
|
||||||
|
response = await client.fetch(request)
|
||||||
|
if response.code != 200:
|
||||||
|
logger.critical(f"Error when fetching filename: {response.code} for {queryString}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response.body.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
class Reply(object):
|
class Reply(object):
|
||||||
def __init__(self, message: Message):
|
def __init__(self, message: Message):
|
||||||
|
@ -363,9 +387,10 @@ class Story(object):
|
||||||
# TODO should we separate 'narrative' (the graph) from the story (the
|
# TODO should we separate 'narrative' (the graph) from the story (the
|
||||||
# current user flow)
|
# current user flow)
|
||||||
|
|
||||||
def __init__(self, hugvey_state):
|
def __init__(self, hugvey_state, panopticon_port):
|
||||||
super(Story, self).__init__()
|
super(Story, self).__init__()
|
||||||
self.hugvey = hugvey_state
|
self.hugvey = hugvey_state
|
||||||
|
self.panopticon_port = panopticon_port
|
||||||
|
|
||||||
self.events = [] # queue of received events
|
self.events = [] # queue of received events
|
||||||
self.commands = [] # queue of commands to send
|
self.commands = [] # queue of commands to send
|
||||||
|
@ -485,7 +510,7 @@ class Story(object):
|
||||||
if self.isRunning:
|
if self.isRunning:
|
||||||
self.isRunning = False
|
self.isRunning = False
|
||||||
|
|
||||||
def _processPendingEvents(self):
|
async def _processPendingEvents(self):
|
||||||
# Gather events:
|
# Gather events:
|
||||||
nr = len(self.events)
|
nr = len(self.events)
|
||||||
for i in range(nr):
|
for i in range(nr):
|
||||||
|
@ -497,7 +522,7 @@ class Story(object):
|
||||||
# a client connected. Should only happen in the beginning or in case of error
|
# a client connected. Should only happen in the beginning or in case of error
|
||||||
# that is, until we have a 'reset' or 'start' event.
|
# that is, until we have a 'reset' or 'start' event.
|
||||||
# reinitiate current message
|
# reinitiate current message
|
||||||
self.setCurrentMessage(self.currentMessage)
|
await self.setCurrentMessage(self.currentMessage)
|
||||||
|
|
||||||
if e['event'] == "playbackFinish":
|
if e['event'] == "playbackFinish":
|
||||||
if e['msgId'] == self.currentMessage.id:
|
if e['msgId'] == self.currentMessage.id:
|
||||||
|
@ -518,7 +543,7 @@ class Story(object):
|
||||||
logger.warn("Interrupt message, replay {}".format(self.previousReply.forMessage.id))
|
logger.warn("Interrupt message, replay {}".format(self.previousReply.forMessage.id))
|
||||||
self.currentReply = self.previousReply
|
self.currentReply = self.previousReply
|
||||||
self.previousReply.forMessage.interruptCount += 1
|
self.previousReply.forMessage.interruptCount += 1
|
||||||
self.currentMessage = self.setCurrentMessage(self.previousReply.forMessage)
|
self.currentMessage = await self.setCurrentMessage(self.previousReply.forMessage)
|
||||||
|
|
||||||
# log if somebody starts speaking
|
# log if somebody starts speaking
|
||||||
# TODO: implement interrupt
|
# TODO: implement interrupt
|
||||||
|
@ -532,7 +557,7 @@ class Story(object):
|
||||||
utterance.setFinished(self.timer.getElapsed())
|
utterance.setFinished(self.timer.getElapsed())
|
||||||
|
|
||||||
|
|
||||||
def _processDirections(self, directions):
|
async def _processDirections(self, directions):
|
||||||
for direction in directions:
|
for direction in directions:
|
||||||
for condition in direction.conditions:
|
for condition in direction.conditions:
|
||||||
if condition.isMet(self):
|
if condition.isMet(self):
|
||||||
|
@ -542,7 +567,7 @@ class Story(object):
|
||||||
self.addToLog(condition)
|
self.addToLog(condition)
|
||||||
self.addToLog(direction)
|
self.addToLog(direction)
|
||||||
self.currentMessage.setFinished(self.timer.getElapsed())
|
self.currentMessage.setFinished(self.timer.getElapsed())
|
||||||
self.setCurrentMessage(direction.msgTo)
|
await self.setCurrentMessage(direction.msgTo)
|
||||||
return direction
|
return direction
|
||||||
|
|
||||||
def addToLog(self, node):
|
def addToLog(self, node):
|
||||||
|
@ -563,13 +588,13 @@ class Story(object):
|
||||||
await self.timer.isRunning.wait() # wait for un-pause
|
await self.timer.isRunning.wait() # wait for un-pause
|
||||||
|
|
||||||
for i in range(len(self.events)):
|
for i in range(len(self.events)):
|
||||||
self._processPendingEvents()
|
await self._processPendingEvents()
|
||||||
|
|
||||||
if self.currentMessage.id not in self.directionsPerMsg:
|
if self.currentMessage.id not in self.directionsPerMsg:
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
directions = self.getCurrentDirections()
|
directions = self.getCurrentDirections()
|
||||||
self._processDirections(directions)
|
await self._processDirections(directions)
|
||||||
|
|
||||||
# TODO create timer event
|
# TODO create timer event
|
||||||
# self.commands.append({'msg':'TEST!'})
|
# self.commands.append({'msg':'TEST!'})
|
||||||
|
@ -581,7 +606,7 @@ class Story(object):
|
||||||
|
|
||||||
logger.info("Stop renderer")
|
logger.info("Stop renderer")
|
||||||
|
|
||||||
def setCurrentMessage(self, message):
|
async def setCurrentMessage(self, message):
|
||||||
if self.currentMessage and not self.lastMsgFinishTime:
|
if self.currentMessage and not self.lastMsgFinishTime:
|
||||||
logger.info("Interrupt playback {}".format(self.currentMessage.id))
|
logger.info("Interrupt playback {}".format(self.currentMessage.id))
|
||||||
# message is playing
|
# message is playing
|
||||||
|
@ -606,18 +631,12 @@ class Story(object):
|
||||||
message.id, message.text))
|
message.id, message.text))
|
||||||
self.addToLog(message)
|
self.addToLog(message)
|
||||||
# TODO: prep events & timer etc.
|
# TODO: prep events & timer etc.
|
||||||
if message.audioFile:
|
# TODO: preload file paths if no variables are set, or once these are loaded
|
||||||
self.hugvey.sendCommand({
|
self.hugvey.sendCommand({
|
||||||
'action': 'play',
|
'action': 'play',
|
||||||
'file': message.audioFile,
|
'file': await message.getAudioFilePath(self),
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
})
|
})
|
||||||
else:
|
|
||||||
self.hugvey.sendCommand({
|
|
||||||
'action': 'play',
|
|
||||||
'msg': message.text,
|
|
||||||
'id': message.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.debug("Pending directions: ")
|
logger.debug("Pending directions: ")
|
||||||
|
|
||||||
|
@ -640,7 +659,7 @@ class Story(object):
|
||||||
startMsg = self.get(customStartMsgId)
|
startMsg = self.get(customStartMsgId)
|
||||||
else:
|
else:
|
||||||
startMsg = self.startMessage
|
startMsg = self.startMessage
|
||||||
self.setCurrentMessage(startMsg)
|
await self.setCurrentMessage(startMsg)
|
||||||
await self._renderer()
|
await self._renderer()
|
||||||
|
|
||||||
def isFinished(self):
|
def isFinished(self):
|
||||||
|
|
80
hugvey/voice.py
Normal file
80
hugvey/voice.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from requests_threads import AsyncSession
|
||||||
|
from hashlib import sha1
|
||||||
|
import asyncio
|
||||||
|
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
|
||||||
|
logger = logging.getLogger("voice")
|
||||||
|
|
||||||
|
class VoiceStorage(object):
|
||||||
|
"""
|
||||||
|
Store & keep voices that are not part of the story json
|
||||||
|
"""
|
||||||
|
def __init__(self, cache_dir, token):
|
||||||
|
self.cache_dir = cache_dir
|
||||||
|
if not os.path.exists(self.cache_dir):
|
||||||
|
raise Exception(f"Cache dir does not exists: {self.cache_dir}")
|
||||||
|
# self.request_session = AsyncSession(n=5)
|
||||||
|
self.pendingRequests = {}
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def getId(self, text):
|
||||||
|
return sha1(text.encode()).hexdigest()
|
||||||
|
|
||||||
|
def getFilename(self, text, isVariable=False):
|
||||||
|
subdir = 'static' if not isVariable else 'variable'
|
||||||
|
id = self.getId(text)
|
||||||
|
prefix = id[:2]
|
||||||
|
storageDir = os.path.join(self.cache_dir, subdir, prefix)
|
||||||
|
fn = os.path.join(storageDir, f"{id}.wav")
|
||||||
|
return fn
|
||||||
|
|
||||||
|
async def requestFile(self, text, isVariable=False) -> str:
|
||||||
|
id = self.getId(text)
|
||||||
|
fn = self.getFilename(text)
|
||||||
|
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return fn
|
||||||
|
|
||||||
|
if id in self.pendingRequests:
|
||||||
|
await self.pendingRequests[id].wait()
|
||||||
|
if os.path.exists(fn):
|
||||||
|
return fn
|
||||||
|
return None
|
||||||
|
|
||||||
|
dirname = os.path.dirname(fn)
|
||||||
|
if not os.path.exists(dirname):
|
||||||
|
logger.debug(f"create directory for file: {dirname}")
|
||||||
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
|
||||||
|
self.pendingRequests[id] = asyncio.Event()
|
||||||
|
|
||||||
|
http_client = AsyncHTTPClient()
|
||||||
|
request = HTTPRequest(
|
||||||
|
method="POST",
|
||||||
|
url="https://avatar.lyrebird.ai/api/v0/generate",
|
||||||
|
body=json.dumps({"text": text}),
|
||||||
|
headers={"authorization": f"Bearer {self.token}"}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = await http_client.fetch(request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
self.pendingRequests[id].set()
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if response.code != 200:
|
||||||
|
logger.critical(f"No proper response! {response.code}")
|
||||||
|
self.pendingRequests[id].set()
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug(f"Wrote body: {response.code}")
|
||||||
|
with open(fn, "wb") as f:
|
||||||
|
f.write(response.body)
|
||||||
|
self.pendingRequests[id].set()
|
||||||
|
print(type(fn), fn)
|
||||||
|
return fn
|
|
@ -7,6 +7,7 @@ voice:
|
||||||
port: 4444
|
port: 4444
|
||||||
chunk: 2972
|
chunk: 2972
|
||||||
google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json"
|
google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json"
|
||||||
|
token: "oauth_SOMETHING"
|
||||||
hugveys: 25
|
hugveys: 25
|
||||||
languages:
|
languages:
|
||||||
- code: en-GB
|
- code: en-GB
|
||||||
|
|
|
@ -265,6 +265,11 @@ class Graph {
|
||||||
this.showMsg( this.selectedMsg );
|
this.showMsg( this.selectedMsg );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioUrlForMsg(msg) {
|
||||||
|
let isVariable = msg['text'].includes('$') ? '1' : '0';
|
||||||
|
return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&filename=0`;
|
||||||
|
}
|
||||||
|
|
||||||
showMsg( msg ) {
|
showMsg( msg ) {
|
||||||
let msgEl = document.getElementById( 'msg' );
|
let msgEl = document.getElementById( 'msg' );
|
||||||
msgEl.innerHTML = "";
|
msgEl.innerHTML = "";
|
||||||
|
@ -286,20 +291,19 @@ class Graph {
|
||||||
if ( msg['start'] == true ) {
|
if ( msg['start'] == true ) {
|
||||||
startAttributes['checked'] = 'checked';
|
startAttributes['checked'] = 'checked';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let audioSrcEl = crel('source', {'src': this.getAudioUrlForMsg(msg)});
|
||||||
let audioSpan = crel(
|
let audioSpan = crel(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
'title': msg['audio'] ? msg['audio']['file'] : "",
|
'title': msg['audio'] ? msg['audio']['file'] : "",
|
||||||
'class': "label-value",
|
'class': "label-value",
|
||||||
},
|
},
|
||||||
msg['audio'] ? msg['audio']['original_name'] : ""
|
crel(
|
||||||
|
'audio', {'controls': 'controls'},
|
||||||
|
audioSrcEl
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if(msg['audio']) {
|
|
||||||
audioSpan.appendChild(crel(
|
|
||||||
'audio', {'controls': 'controls'},
|
|
||||||
crel('source', {'src':msg['audio']['file']})
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
|
let msgInfoEl = crel( 'div', { 'class': 'msg__info' },
|
||||||
crel('div', {
|
crel('div', {
|
||||||
'class':'btn btn--delete',
|
'class':'btn btn--delete',
|
||||||
|
@ -319,7 +323,10 @@ class Graph {
|
||||||
'name': msg['@id'] + '-text',
|
'name': msg['@id'] + '-text',
|
||||||
'value': msg['text'],
|
'value': msg['text'],
|
||||||
'on': {
|
'on': {
|
||||||
'change': this.getEditEventListener()
|
'change': this.getEditEventListener(function(){
|
||||||
|
audioSrcEl.src = panopticon.graph.getAudioUrlForMsg(msg);
|
||||||
|
audioSrcEl.parentElement.load();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
),
|
),
|
||||||
|
@ -855,7 +862,7 @@ class Graph {
|
||||||
* Use wrapper method, because for event handlers 'this' will refer to
|
* Use wrapper method, because for event handlers 'this' will refer to
|
||||||
* the input object
|
* the input object
|
||||||
*/
|
*/
|
||||||
getEditEventListener() {
|
getEditEventListener(callback) {
|
||||||
let graph = this;
|
let graph = this;
|
||||||
let el = function( e ) {
|
let el = function( e ) {
|
||||||
console.info("Changed", e);
|
console.info("Changed", e);
|
||||||
|
@ -880,6 +887,10 @@ class Graph {
|
||||||
// node[field] = e.srcElement.value;
|
// node[field] = e.srcElement.value;
|
||||||
|
|
||||||
graph.build();
|
graph.build();
|
||||||
|
|
||||||
|
if(typeof callback !== 'undefined'){
|
||||||
|
callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue