SSML inbox endpoint

merge-requests/30/head
Bob Mottram 2021-03-03 12:34:46 +00:00
parent 5af36bf817
commit 7c20406d3f
3 changed files with 155 additions and 22 deletions

View File

@ -269,6 +269,7 @@ from filters import isFiltered
from filters import addGlobalFilter from filters import addGlobalFilter
from filters import removeGlobalFilter from filters import removeGlobalFilter
from context import hasValidContext from context import hasValidContext
from speaker import getSSMLbox
import os import os
@ -487,24 +488,28 @@ class PubServer(BaseHTTPRequestHandler):
""" """
if not self.headers.get('Accept'): if not self.headers.get('Accept'):
return False return False
acceptStr = self.headers['Accept']
if self.server.debug: if self.server.debug:
print('ACCEPT: ' + self.headers['Accept']) print('ACCEPT: ' + acceptStr)
if 'image/' in self.headers['Accept']: if 'application/ssml' in acceptStr:
if 'text/html' not in self.headers['Accept']: if 'text/html' not in acceptStr:
return False return False
if 'video/' in self.headers['Accept']: if 'image/' in acceptStr:
if 'text/html' not in self.headers['Accept']: if 'text/html' not in acceptStr:
return False return False
if 'audio/' in self.headers['Accept']: if 'video/' in acceptStr:
if 'text/html' not in self.headers['Accept']: if 'text/html' not in acceptStr:
return False return False
if self.headers['Accept'].startswith('*'): if 'audio/' in acceptStr:
if 'text/html' not in acceptStr:
return False
if acceptStr.startswith('*'):
if self.headers.get('User-Agent'): if self.headers.get('User-Agent'):
if 'ELinks' in self.headers['User-Agent'] or \ if 'ELinks' in self.headers['User-Agent'] or \
'Lynx' in self.headers['User-Agent']: 'Lynx' in self.headers['User-Agent']:
return True return True
return False return False
if 'json' in self.headers['Accept']: if 'json' in acceptStr:
return False return False
return True return True
@ -10480,10 +10485,25 @@ class PubServer(BaseHTTPRequestHandler):
# arriving in your inbox # arriving in your inbox
if authorized and usersInPath and \ if authorized and usersInPath and \
self.path.endswith('/speaker'): self.path.endswith('/speaker'):
self._getSpeaker(callingDomain, self.path, if 'application/ssml' not in self.headers['Accept']:
self.server.baseDir, # json endpoint
self.server.domain, self._getSpeaker(callingDomain, self.path,
self.server.debug) self.server.baseDir,
self.server.domain,
self.server.debug)
else:
xmlStr = \
getSSMLbox(self.server.baseDir,
self.path, self.server.domain,
self.server.systemLanguage,
self.server.instanceTitle,
'inbox')
if xmlStr:
msg = xmlStr.encode('utf-8')
msglen = len(msg)
self._set_headers('application/xrd+xml', msglen,
None, callingDomain)
self._write(msg)
return return
# redirect to the welcome screen # redirect to the welcome screen

View File

@ -84,6 +84,7 @@ from context import hasValidContext
from content import htmlReplaceQuoteMarks from content import htmlReplaceQuoteMarks
from speaker import speakerReplaceLinks from speaker import speakerReplaceLinks
from speaker import speakerPronounce from speaker import speakerPronounce
from speaker import speakerEndpointJson
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
@ -2201,13 +2202,9 @@ def _updateSpeaker(baseDir: str, nickname: str, domain: str,
announcedHandle = announcedNickname + '@' + announcedDomain announcedHandle = announcedNickname + '@' + announcedDomain
content = \ content = \
translate['announces'] + ' ' + announcedHandle + '. ' + content translate['announces'] + ' ' + announcedHandle + '. ' + content
speakerJson = { speakerJson = speakerEndpointJson(speakerName, summary,
"name": speakerName, content, imageDescription,
"summary": summary, detectedLinks)
"say": content,
"imageDescription": imageDescription,
"detectedLinks": detectedLinks
}
saveJson(speakerJson, speakerFilename) saveJson(speakerJson, speakerFilename)

View File

@ -10,8 +10,11 @@ import os
import random import random
from auth import createBasicAuthHeader from auth import createBasicAuthHeader
from session import getJson from session import getJson
from utils import loadJson
from utils import getFullDomain from utils import getFullDomain
speakerRemoveChars = ('.\n', '. ', ',', ';', '?', '!')
def getSpeakerPitch(displayName: str, screenreader: str) -> int: def getSpeakerPitch(displayName: str, screenreader: str) -> int:
"""Returns the speech synthesis pitch for the given name """Returns the speech synthesis pitch for the given name
@ -96,9 +99,8 @@ def speakerReplaceLinks(sayText: str, translate: {},
"""Replaces any links in the given text with "link to [domain]". """Replaces any links in the given text with "link to [domain]".
Instead of reading out potentially very long and meaningless links Instead of reading out potentially very long and meaningless links
""" """
removeChars = ('.\n', '. ', ',', ';', '?', '!')
text = sayText text = sayText
for ch in removeChars: for ch in speakerRemoveChars:
text = text.replace(ch, ' ') text = text.replace(ch, ' ')
replacements = {} replacements = {}
wordsList = text.split(' ') wordsList = text.split(' ')
@ -136,6 +138,28 @@ def speakerReplaceLinks(sayText: str, translate: {},
return sayText.replace('..', '.') return sayText.replace('..', '.')
def _addSSMLemphasis(sayText: str) -> str:
"""Adds emphasis to *emphasised* text
"""
if '*' not in sayText:
return sayText
text = sayText
for ch in speakerRemoveChars:
text = text.replace(ch, ' ')
wordsList = text.split(' ')
replacements = {}
for word in wordsList:
if word.startswith('*'):
if word.endswith('*'):
replacements[word] = \
'<emphasis level="strong">' + \
word.replace('*', '') + \
'</emphasis>'
for replaceStr, newStr in replacements.items():
sayText = sayText.replace(replaceStr, newStr)
return sayText
def getSpeakerFromServer(baseDir: str, session, def getSpeakerFromServer(baseDir: str, session,
nickname: str, password: str, nickname: str, password: str,
domain: str, port: int, domain: str, port: int,
@ -166,3 +190,95 @@ def getSpeakerFromServer(baseDir: str, session,
getJson(session, url, headers, None, getJson(session, url, headers, None,
__version__, httpPrefix, domain) __version__, httpPrefix, domain)
return speakerJson return speakerJson
def speakerEndpointJson(displayName: str, summary: str,
content: str, imageDescription: str,
links: []) -> {}:
"""Returns a json endpoint for the TTS speaker
"""
return {
"name": displayName,
"summary": summary,
"say": content,
"imageDescription": imageDescription,
"detectedLinks": links
}
def _speakerEndpointSSML(displayName: str, summary: str,
content: str, imageDescription: str,
links: [], language: str,
instanceTitle: str,
gender: str) -> str:
"""Returns an SSML endpoint for the TTS speaker
https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language
https://www.w3.org/TR/speech-synthesis/
"""
langShort = 'en'
if language:
langShort = language[:2]
if not gender:
gender = 'neutral'
else:
if langShort == 'en':
gender = gender.lower()
if 'him' in gender or 'male' in gender:
gender = 'male'
elif 'her' in gender or 'she' in gender or \
'fem' in gender or 'woman' in gender:
gender = 'female'
elif 'man' in gender:
gender = 'male'
else:
gender = 'neutral'
content = _addSSMLemphasis(content)
voiceParams = 'name="' + displayName + '" gender="' + gender + '"'
return '<?xml version="1.0"?>\n' + \
'<speak xmlns="http://www.w3.org/2001/10/synthesis"\n' + \
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' + \
' xsi:schemaLocation="http://www.w3.org/2001/10/synthesis\n' + \
' http://www.w3.org/TR/speech-synthesis11/synthesis.xsd"\n' + \
' version="1.1">\n' + \
' <metadata>\n' + \
' <dc:title xml:lang="' + langShort + '">' + \
instanceTitle + ' inbox</dc:title>\n' + \
' </metadata>\n' + \
' <p>\n' + \
' <s xml:lang="' + language + '">\n' + \
' <voice ' + voiceParams + '>\n' + \
' ' + content + '\n' + \
' </voice>\n' + \
' </s>\n' + \
' </p>\n' + \
'</speak>\n'
def getSSMLbox(baseDir: str, path: str,
domain: str,
systemLanguage: str,
instanceTitle: str,
boxName: str) -> str:
"""Returns SSML for the given timeline
"""
nickname = path.split('/users/')[1]
if '/' in nickname:
nickname = nickname.split('/')[0]
speakerFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json'
if not os.path.isfile(speakerFilename):
return None
speakerJson = loadJson(speakerFilename)
if not speakerJson:
return None
gender = None
if speakerJson.get('gender'):
gender = speakerJson['gender']
return _speakerEndpointSSML(speakerJson['name'],
speakerJson['summary'],
speakerJson['say'],
speakerJson['imageDescription'],
speakerJson['detectedLinks'],
systemLanguage,
instanceTitle, gender)