Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2021-03-03 13:40:09 +00:00
commit 680e047af2
21 changed files with 297 additions and 40 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

@ -14,6 +14,7 @@ import html
import urllib.parse import urllib.parse
from linked_data_sig import verifyJsonSignature from linked_data_sig import verifyJsonSignature
from utils import getDisplayName from utils import getDisplayName
from utils import getGenderFromBio
from utils import removeHtml from utils import removeHtml
from utils import getConfigParam from utils import getConfigParam
from utils import hasUsersPath from utils import hasUsersPath
@ -84,6 +85,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:
@ -2193,6 +2195,8 @@ def _updateSpeaker(baseDir: str, nickname: str, domain: str,
speakerName = \ speakerName = \
getDisplayName(baseDir, postJsonObject['actor'], personCache) getDisplayName(baseDir, postJsonObject['actor'], personCache)
gender = getGenderFromBio(baseDir, postJsonObject['actor'],
personCache, translate)
if not speakerName: if not speakerName:
return return
if announcingActor: if announcingActor:
@ -2201,13 +2205,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, gender)
"say": content,
"imageDescription": imageDescription,
"detectedLinks": detectedLinks
}
saveJson(speakerJson, speakerFilename) saveJson(speakerJson, speakerFilename)

View File

@ -276,12 +276,13 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
'devices': personId + '/collections/devices', 'devices': personId + '/collections/devices',
'endpoints': { 'endpoints': {
'id': personId + '/endpoints', 'id': personId + '/endpoints',
'sharedInbox': httpPrefix+'://' + domain + '/inbox', 'sharedInbox': httpPrefix + '://' + domain + '/inbox',
}, },
'featured': personId + '/collections/featured', 'featured': personId + '/collections/featured',
'featuredTags': personId + '/collections/tags', 'featuredTags': personId + '/collections/tags',
'followers': personId + '/followers', 'followers': personId + '/followers',
'following': personId + '/following', 'following': personId + '/following',
'tts': personId + '/speaker',
'shares': personId + '/shares', 'shares': personId + '/shares',
'orgSchema': None, 'orgSchema': None,
'skills': {}, 'skills': {},
@ -556,6 +557,10 @@ def personUpgradeActor(baseDir: str, personJson: {},
personJson = loadJson(filename) personJson = loadJson(filename)
if updateActor: if updateActor:
# add a speaker endpoint
if not personJson.get('tts'):
personJson['tts'] = personJson['id'] + '/speaker'
saveJson(personJson, filename) saveJson(personJson, filename)
# also update the actor within the cache # also update the actor within the cache

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: [], gender: str) -> {}:
"""Returns a json endpoint for the TTS speaker
"""
speakerJson = {
"name": displayName,
"summary": summary,
"say": content,
"imageDescription": imageDescription,
"detectedLinks": links
}
if gender:
speakerJson['gender'] = gender
return speakerJson
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 'he/him' in gender:
gender = 'male'
elif 'she/her' in gender:
gender = 'female'
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)

View File

@ -68,8 +68,9 @@ def _copyThemeHelpFiles(baseDir: str, themeName: str,
if destHelpMarkdownFile == 'profile.md' or \ if destHelpMarkdownFile == 'profile.md' or \
destHelpMarkdownFile == 'final.md': destHelpMarkdownFile == 'final.md':
destHelpMarkdownFile = 'welcome_' + destHelpMarkdownFile destHelpMarkdownFile = 'welcome_' + destHelpMarkdownFile
copyfile(themeDir + '/' + helpMarkdownFile, if os.path.isdir(baseDir + '/accounts'):
baseDir + '/accounts/' + destHelpMarkdownFile) copyfile(themeDir + '/' + helpMarkdownFile,
baseDir + '/accounts/' + destHelpMarkdownFile)
break break
@ -659,6 +660,8 @@ def _setClearCacheFlag(baseDir: str) -> None:
"""Sets a flag which can be used by an external system """Sets a flag which can be used by an external system
(eg. a script in a cron job) to clear the browser cache (eg. a script in a cron job) to clear the browser cache
""" """
if not os.path.isdir(baseDir + '/accounts'):
return
flagFilename = baseDir + '/accounts/.clear_cache' flagFilename = baseDir + '/accounts/.clear_cache'
with open(flagFilename, 'w+') as flagFile: with open(flagFilename, 'w+') as flagFile:
flagFile.write('\n') flagFile.write('\n')

View File

@ -381,5 +381,8 @@
"mentioning": "ذكر", "mentioning": "ذكر",
"sad face": "وجه حزين", "sad face": "وجه حزين",
"thinking emoji": "التفكير الرموز التعبيرية", "thinking emoji": "التفكير الرموز التعبيرية",
"laughing": "يضحك" "laughing": "يضحك",
"gender": "جنس تذكير أو تأنيث",
"He/Him": "هو",
"She/Her": "هي"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "esmentant", "mentioning": "esmentant",
"sad face": "cara trista", "sad face": "cara trista",
"thinking emoji": "emoji pensant", "thinking emoji": "emoji pensant",
"laughing": "rient" "laughing": "rient",
"gender": "gènere",
"He/Him": "Ell",
"She/Her": "Ella"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "sôn", "mentioning": "sôn",
"sad face": "wyneb trist", "sad face": "wyneb trist",
"thinking emoji": "meddwl emoji", "thinking emoji": "meddwl emoji",
"laughing": "chwerthin" "laughing": "chwerthin",
"gender": "rhyw",
"He/Him": "Ef",
"She/Her": "Hi/Ei"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "Erwähnen", "mentioning": "Erwähnen",
"sad face": "trauriges Gesicht", "sad face": "trauriges Gesicht",
"thinking emoji": "Emowji denken", "thinking emoji": "Emowji denken",
"laughing": "Lachen" "laughing": "Lachen",
"gender": "geschlecht",
"He/Him": "Er/ihm",
"She/Her": "Sie"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "mentioning", "mentioning": "mentioning",
"sad face": "sad face", "sad face": "sad face",
"thinking emoji": "thinking emowji", "thinking emoji": "thinking emowji",
"laughing": "laughing" "laughing": "laughing",
"gender": "gender",
"He/Him": "He/Him",
"She/Her": "She/Her"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "mencionar", "mentioning": "mencionar",
"sad face": "cara triste", "sad face": "cara triste",
"thinking emoji": "pensando emowji", "thinking emoji": "pensando emowji",
"laughing": "risa" "laughing": "risa",
"gender": "género",
"He/Him": "El",
"She/Her": "Ella"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "mentionnant", "mentioning": "mentionnant",
"sad face": "visage triste", "sad face": "visage triste",
"thinking emoji": "penser emowji", "thinking emoji": "penser emowji",
"laughing": "en riant" "laughing": "en riant",
"gender": "le genre",
"He/Him": "Il/Lui",
"She/Her": "Elle"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "ag lua", "mentioning": "ag lua",
"sad face": "aghaidh brónach", "sad face": "aghaidh brónach",
"thinking emoji": "ag smaoineamh emowji", "thinking emoji": "ag smaoineamh emowji",
"laughing": "ag gáire" "laughing": "ag gáire",
"gender": "inscne",
"He/Him": "Sé/Eisean",
"She/Her": "Sí"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "उल्लेख", "mentioning": "उल्लेख",
"sad face": "उदास चेहरा", "sad face": "उदास चेहरा",
"thinking emoji": "सोच रहे हैं इमोजी", "thinking emoji": "सोच रहे हैं इमोजी",
"laughing": "हस रहा" "laughing": "हस रहा",
"gender": "लिंग",
"He/Him": "वह/उसे",
"She/Her": "वह/उसकी"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "menzionando", "mentioning": "menzionando",
"sad face": "faccia triste", "sad face": "faccia triste",
"thinking emoji": "pensiero emoji", "thinking emoji": "pensiero emoji",
"laughing": "ridendo" "laughing": "ridendo",
"gender": "genere",
"He/Him": "Lui",
"She/Her": "Lei"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "言及する", "mentioning": "言及する",
"sad face": "悲しい顔", "sad face": "悲しい顔",
"thinking emoji": "絵文字を考える", "thinking emoji": "絵文字を考える",
"laughing": "笑い" "laughing": "笑い",
"gender": "性別",
"He/Him": "彼",
"She/Her": "彼女"
} }

View File

@ -377,5 +377,8 @@
"mentioning": "mentioning", "mentioning": "mentioning",
"sad face": "sad face", "sad face": "sad face",
"thinking emoji": "thinking emowji", "thinking emoji": "thinking emowji",
"laughing": "laughing" "laughing": "laughing",
"gender": "gender",
"He/Him": "He/Him",
"She/Her": "She/Her"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "mencionando", "mentioning": "mencionando",
"sad face": "rosto triste", "sad face": "rosto triste",
"thinking emoji": "pensando emowji", "thinking emoji": "pensando emowji",
"laughing": "rindo" "laughing": "rindo",
"gender": "gênero",
"He/Him": "Ele",
"She/Her": "Ela"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "упоминание", "mentioning": "упоминание",
"sad face": "грустное лицо", "sad face": "грустное лицо",
"thinking emoji": "думающий смайлик", "thinking emoji": "думающий смайлик",
"laughing": "смеющийся" "laughing": "смеющийся",
"gender": "Пол",
"He/Him": "Он/Его",
"She/Her": "Она/Ее"
} }

View File

@ -381,5 +381,8 @@
"mentioning": "提及", "mentioning": "提及",
"sad face": "悲伤的脸", "sad face": "悲伤的脸",
"thinking emoji": "思维表情符号", "thinking emoji": "思维表情符号",
"laughing": "笑" "laughing": "笑",
"gender": "",
"He/Him": "",
"She/Her": ""
} }

View File

@ -669,6 +669,74 @@ def getDisplayName(baseDir: str, actor: str, personCache: {}) -> str:
return nameFound return nameFound
def getGenderFromBio(baseDir: str, actor: str, personCache: {},
translate: {}) -> str:
"""Tries to ascertain gender from bio description
"""
if '/statuses/' in actor:
actor = actor.split('/statuses/')[0]
if not personCache.get(actor):
return None
bioFound = None
if personCache[actor].get('actor'):
# is gender defined as a profile tag?
if personCache[actor]['actor'].get('attachment'):
tagsList = personCache[actor]['actor']['attachment']
if isinstance(tagsList, list):
for tag in tagsList:
if not isinstance(tag, dict):
continue
if not tag.get('name') or not tag.get('value'):
continue
if tag['name'].lower() == \
translate['gender'].lower():
bioFound = tag['value']
break
# if not then use the bio
if not bioFound and personCache[actor]['actor'].get('summary'):
bioFound = personCache[actor]['actor']['summary']
else:
# Try to obtain from the cached actors
cachedActorFilename = \
baseDir + '/cache/actors/' + (actor.replace('/', '#')) + '.json'
if os.path.isfile(cachedActorFilename):
actorJson = loadJson(cachedActorFilename, 1)
if actorJson:
# is gender defined as a profile tag?
if actorJson.get('attachment'):
tagsList = actorJson['attachment']
if isinstance(tagsList, list):
for tag in tagsList:
if not isinstance(tag, dict):
continue
if not tag.get('name') or not tag.get('value'):
continue
if tag['name'].lower() == \
translate['gender'].lower():
bioFound = tag['value']
break
# if not then use the bio
if not bioFound and actorJson.get('summary'):
bioFound = actorJson['summary']
if not bioFound:
return None
gender = 'They/Them'
bioFoundOrig = bioFound
bioFound = bioFound.lower()
if translate['He/Him'] in bioFound:
gender = 'He/Him'
elif translate['She/Her'] in bioFound:
gender = 'She/Her'
elif 'him' in bioFound or 'male' in bioFound:
gender = 'He/Him'
elif 'her' in bioFound or 'she' in bioFound or \
'fem' in bioFound or 'woman' in bioFound:
gender = 'She/Her'
elif 'man' in bioFound or 'He' in bioFoundOrig:
gender = 'He/Him'
return gender
def getNicknameFromActor(actor: str) -> str: def getNicknameFromActor(actor: str) -> str:
"""Returns the nickname from an actor url """Returns the nickname from an actor url
""" """