Notification sounds

merge-requests/21/head
Bob Mottram 2021-03-09 19:52:10 +00:00
parent 88d4c8338d
commit e492190721
16 changed files with 197 additions and 80 deletions

View File

@ -2719,7 +2719,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
if hashtagStr:
msg = hashtagStr.encode('utf-8')
msglen = len(msg)
@ -2772,7 +2773,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
if historyStr:
msg = historyStr.encode('utf-8')
msglen = len(msg)
@ -2862,7 +2864,8 @@ class PubServer(BaseHTTPRequestHandler):
showPublishedDateOnly,
self.server.defaultTimeline,
self.server.peertubeInstances,
allowLocalNetworkAccess)
allowLocalNetworkAccess,
self.server.themeName)
if profileStr:
msg = profileStr.encode('utf-8')
msglen = len(msg)
@ -5987,7 +5990,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
if hashtagStr:
msg = hashtagStr.encode('utf-8')
msglen = len(msg)
@ -6950,7 +6954,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
if deleteStr:
deleteStrLen = len(deleteStr)
self._set_headers('text/html', deleteStrLen,
@ -7155,7 +7160,8 @@ class PubServer(BaseHTTPRequestHandler):
ytDomain,
self.server.showPublishedDateOnly,
peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
msg = msg.encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
@ -7242,7 +7248,8 @@ class PubServer(BaseHTTPRequestHandler):
ytDomain,
self.server.showPublishedDateOnly,
peertubeInstances,
self.server.allowLocalNetworkAccess)
self.server.allowLocalNetworkAccess,
self.server.themeName)
msg = msg.encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
@ -7532,6 +7539,8 @@ class PubServer(BaseHTTPRequestHandler):
cssCache = self.server.cssCache
allowLocalNetworkAccess = \
self.server.allowLocalNetworkAccess
themeName = \
self.server.themeName
msg = \
htmlIndividualPost(cssCache,
recentPostsCache,
@ -7552,7 +7561,8 @@ class PubServer(BaseHTTPRequestHandler):
ytDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess)
allowLocalNetworkAccess,
themeName)
msg = msg.encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
@ -7656,6 +7666,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.peertubeInstances
allowLocalNetworkAccess = \
self.server.allowLocalNetworkAccess
themeName = \
self.server.themeName
msg = \
htmlIndividualPost(self.server.cssCache,
recentPostsCache,
@ -7676,7 +7688,8 @@ class PubServer(BaseHTTPRequestHandler):
ytDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess)
allowLocalNetworkAccess,
themeName)
msg = msg.encode('utf-8')
msglen = len(msg)
self._set_headers('text/html', msglen,
@ -14667,7 +14680,8 @@ def runDaemon(brochMode: bool,
httpd.maxFollowers,
httpd.allowLocalNetworkAccess,
httpd.peertubeInstances,
verifyAllSignatures), daemon=True)
verifyAllSignatures,
httpd.themeName), daemon=True)
print('Creating scheduled post thread')
httpd.thrPostSchedule = \

View File

@ -158,7 +158,8 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
allowDeletion: bool, boxname: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> None:
allowLocalNetworkAccess: bool,
themeName: str) -> None:
"""Converts the json post into html and stores it in a cache
This enables the post to be quickly displayed later
"""
@ -176,6 +177,7 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
httpPrefix, __version__, boxname, None,
showPublishedDateOnly,
peertubeInstances, allowLocalNetworkAccess,
themeName,
not isDM(postJsonObject),
True, True, False, True)
@ -1290,7 +1292,8 @@ def _receiveAnnounce(recentPostsCache: {},
personCache: {}, messageJson: {}, federationList: [],
debug: bool, translate: {},
YTReplacementDomain: str,
allowLocalNetworkAccess: bool) -> bool:
allowLocalNetworkAccess: bool,
themeName: str) -> bool:
"""Receives an announce activity within the POST section of HTTPServer
"""
if messageJson['type'] != 'Announce':
@ -1412,7 +1415,8 @@ def _receiveAnnounce(recentPostsCache: {},
updateSpeaker(baseDir, httpPrefix,
nickname, domain, domainFull,
postJsonObject, personCache,
translate, lookupActor)
translate, lookupActor,
themeName)
ttsFile = open(postFilename + '.tts', "w+")
if ttsFile:
ttsFile.write('\n')
@ -2166,7 +2170,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
showPublishedDateOnly: bool,
allowLocalNetworkAccess: bool,
peertubeInstances: [],
lastBounceMessage: []) -> bool:
lastBounceMessage: [],
themeName: str) -> bool:
""" Anything which needs to be done after initial checks have passed
"""
actor = keyId
@ -2245,7 +2250,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
federationList,
debug, translate,
YTReplacementDomain,
allowLocalNetworkAccess):
allowLocalNetworkAccess,
themeName):
if debug:
print('DEBUG: Announce accepted from ' + actor)
@ -2497,7 +2503,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
updateSpeaker(baseDir, httpPrefix,
nickname, domain, domainFull,
postJsonObject, personCache,
translate, None)
translate, None, themeName)
if not unitTest:
if debug:
print('Saving inbox post as html to cache')
@ -2517,7 +2523,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
boxname,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess)
allowLocalNetworkAccess,
themeName)
if debug:
timeDiff = \
str(int((time.time() - htmlCacheStartTime) *
@ -2618,7 +2625,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
showPublishedDateOnly: bool,
maxFollowers: int, allowLocalNetworkAccess: bool,
peertubeInstances: [],
verifyAllSignatures: bool) -> None:
verifyAllSignatures: bool,
themeName: str) -> None:
"""Processes received items and moves them to the appropriate
directories
"""
@ -3111,7 +3119,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
showPublishedDateOnly,
allowLocalNetworkAccess,
peertubeInstances,
lastBounceMessage)
lastBounceMessage,
themeName)
if debug:
pprint(queueJson['post'])

View File

@ -66,6 +66,17 @@ def _speakerPicospeaker(pitch: int, rate: int, systemLanguage: str,
os.system(speakerCmd)
def _playNotificationSound(soundFilename: str, player='ffplay') -> None:
"""Plays a sound
"""
if not os.path.isfile(soundFilename):
return
if player == 'ffplay':
os.system('ffplay ' + soundFilename +
' -autoexit -hide_banner -nodisp')
def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str,
nickname: str, domain: str, port: int,
password: str, screenreader: str,
@ -73,64 +84,121 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str,
"""Runs the notifications and screen reader client,
which announces new inbox items
"""
if screenreader == 'espeak':
print('Setting up espeak')
from espeak import espeak
elif screenreader != 'picospeaker':
print(screenreader + ' is not a supported TTS system')
return
if screenreader:
if screenreader == 'espeak':
print('Setting up espeak')
from espeak import espeak
elif screenreader != 'picospeaker':
print(screenreader + ' is not a supported TTS system')
return
print('Running ' + screenreader + ' for ' + nickname + '@' + domain)
print('Running ' + screenreader + ' for ' + nickname + '@' + domain)
prevSay = ''
prevDM = False
prevReply = False
prevCalendar = False
prevFollow = False
prevLike = ''
prevShare = False
dmSoundFilename = 'dm.ogg'
replySoundFilename = 'reply.ogg'
calendarSoundFilename = 'calendar.ogg'
followSoundFilename = 'follow.ogg'
likeSoundFilename = 'like.ogg'
shareSoundFilename = 'share.ogg'
player = 'ffplay'
while (1):
session = createSession(proxyType)
speakerJson = \
getSpeakerFromServer(baseDir, session, nickname, password,
domain, port, httpPrefix, True, __version__)
if speakerJson:
if speakerJson['say'] != prevSay:
if speakerJson.get('name'):
nameStr = speakerJson['name']
gender = 'They/Them'
if speakerJson.get('gender'):
gender = speakerJson['gender']
if speakerJson.get('notify'):
soundsDir = 'theme/default/sounds'
if speakerJson['notify'].get('theme'):
soundsDir = \
'theme/' + speakerJson['notify']['theme'] + '/sounds'
if not os.path.isdir(soundsDir):
soundsDir = 'theme/default/sounds'
if dmSoundFilename:
if speakerJson['notify']['dm'] != prevDM:
_playNotificationSound(soundsDir + '/' +
dmSoundFilename, player)
elif replySoundFilename:
if speakerJson['notify']['reply'] != prevReply:
_playNotificationSound(soundsDir + '/' +
replySoundFilename, player)
elif calendarSoundFilename:
if speakerJson['notify']['calendar'] != prevCalendar:
_playNotificationSound(soundsDir + '/' +
calendarSoundFilename, player)
elif followSoundFilename:
if speakerJson['notify']['followRequests'] != prevFollow:
_playNotificationSound(soundsDir + '/' +
followSoundFilename, player)
elif likeSoundFilename:
if speakerJson['notify']['like'] != prevLike:
_playNotificationSound(soundsDir + '/' +
likeSoundFilename, player)
elif shareSoundFilename:
if speakerJson['notify']['share'] != prevShare:
_playNotificationSound(soundsDir + '/' +
shareSoundFilename, player)
# get the speech parameters
pitch = getSpeakerPitch(nameStr, screenreader, gender)
rate = getSpeakerRate(nameStr, screenreader)
srange = getSpeakerRange(nameStr)
prevDM = speakerJson['notify']['dm']
prevReply = speakerJson['notify']['reply']
prevCalendar = speakerJson['notify']['calendar']
prevFollow = speakerJson['notify']['followRequests']
prevLike = speakerJson['notify']['like']
prevShare = speakerJson['notify']['share']
# say the speaker's name
if screenreader == 'espeak':
_speakerEspeak(espeak, pitch, rate, srange, nameStr)
elif screenreader == 'picospeaker':
_speakerPicospeaker(pitch, rate,
systemLanguage, nameStr)
time.sleep(2)
if speakerJson.get('say'):
if speakerJson['say'] != prevSay:
if speakerJson.get('name'):
nameStr = speakerJson['name']
gender = 'They/Them'
if speakerJson.get('gender'):
gender = speakerJson['gender']
# append image description if needed
if not speakerJson.get('imageDescription'):
sayStr = speakerJson['say']
# echo spoken text to the screen
print(html.unescape(nameStr) + ': ' +
html.unescape(speakerJson['say']) + '\n')
else:
sayStr = speakerJson['say'] + '. ' + \
speakerJson['imageDescription']
# echo spoken text to the screen
print(html.unescape(nameStr) + ': ' +
html.unescape(speakerJson['say']) + '\n' +
html.unescape(speakerJson['imageDescription']))
# get the speech parameters
pitch = getSpeakerPitch(nameStr, screenreader, gender)
rate = getSpeakerRate(nameStr, screenreader)
srange = getSpeakerRange(nameStr)
# speak the post content
if screenreader == 'espeak':
_speakerEspeak(espeak, pitch, rate, srange, sayStr)
elif screenreader == 'picospeaker':
_speakerPicospeaker(pitch, rate,
systemLanguage, sayStr)
# say the speaker's name
if screenreader == 'espeak':
_speakerEspeak(espeak, pitch, rate, srange,
nameStr)
elif screenreader == 'picospeaker':
_speakerPicospeaker(pitch, rate,
systemLanguage, nameStr)
time.sleep(2)
prevSay = speakerJson['say']
# append image description if needed
if not speakerJson.get('imageDescription'):
sayStr = speakerJson['say']
# echo spoken text to the screen
print(html.unescape(nameStr) + ': ' +
html.unescape(speakerJson['say']) + '\n')
else:
sayStr = speakerJson['say'] + '. ' + \
speakerJson['imageDescription']
# echo spoken text to the screen
imageDescription = \
html.unescape(speakerJson['imageDescription'])
print(html.unescape(nameStr) + ': ' +
html.unescape(speakerJson['say']) + '\n' +
imageDescription)
# speak the post content
if screenreader == 'espeak':
_speakerEspeak(espeak, pitch, rate, srange, sayStr)
elif screenreader == 'picospeaker':
_speakerPicospeaker(pitch, rate,
systemLanguage, sayStr)
prevSay = speakerJson['say']
# wait for a while, or until a key is pressed
keyPress = _waitForKeypress(30, debug)

View File

@ -269,7 +269,7 @@ def _speakerEndpointJson(displayName: str, summary: str,
postDM: bool, postReply: bool,
followRequestsExist: bool,
likedBy: str, postCal: bool,
postShare: bool) -> {}:
postShare: bool, themeName: str) -> {}:
"""Returns a json endpoint for the TTS speaker
"""
speakerJson = {
@ -280,6 +280,7 @@ def _speakerEndpointJson(displayName: str, summary: str,
"detectedLinks": links,
"id": postId,
"notify": {
"theme": themeName,
"dm": postDM,
"reply": postReply,
"followRequests": followRequestsExist,
@ -377,7 +378,8 @@ def getSSMLbox(baseDir: str, path: str,
def _postToSpeakerJson(baseDir: str, httpPrefix: str,
nickname: str, domain: str, domainFull: str,
postJsonObject: {}, personCache: {},
translate: {}, announcingActor: str) -> {}:
translate: {}, announcingActor: str,
themeName: str) -> {}:
"""Converts an ActivityPub post into some Json containing
speech synthesis parameters.
NOTE: There currently appears to be no standardized json
@ -475,13 +477,14 @@ def _postToSpeakerJson(baseDir: str, httpPrefix: str,
postDM, postReply,
followRequestsExist,
likedBy,
postCal, postShare)
postCal, postShare, themeName)
def updateSpeaker(baseDir: str, httpPrefix: str,
nickname: str, domain: str, domainFull: str,
postJsonObject: {}, personCache: {},
translate: {}, announcingActor: str) -> None:
translate: {}, announcingActor: str,
themeName: str) -> None:
""" Generates a json file which can be used for TTS announcement
of incoming inbox posts
"""
@ -489,7 +492,8 @@ def updateSpeaker(baseDir: str, httpPrefix: str,
_postToSpeakerJson(baseDir, httpPrefix,
nickname, domain, domainFull,
postJsonObject, personCache,
translate, announcingActor)
translate, announcingActor,
themeName)
speakerFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json'
saveJson(speakerJson, speakerFilename)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -31,7 +31,8 @@ def htmlConfirmDelete(cssCache: {},
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Shows a screen asking to confirm the deletion of a post
"""
if '/statuses/' not in messageId:
@ -72,6 +73,7 @@ def htmlConfirmDelete(cssCache: {},
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances, allowLocalNetworkAccess,
themeName,
False, False, False, False, False)
deletePostStr += '<center>'
deletePostStr += \

View File

@ -30,7 +30,8 @@ def _htmlFrontScreenPosts(recentPostsCache: {}, maxRecentPosts: int,
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Shows posts on the front screen of a news instance
These should only be public blog posts from the features timeline
which is the blog timeline of the news actor
@ -71,6 +72,7 @@ def _htmlFrontScreenPosts(recentPostsCache: {}, maxRecentPosts: int,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
False, False, False, True, False)
if postStr:
profileStr += postStr + separatorStr
@ -159,7 +161,8 @@ def htmlFrontScreen(rssIconAtTop: bool,
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess) + licenseStr
allowLocalNetworkAccess,
theme) + licenseStr
# Footer which is only used for system accounts
profileFooterStr = ' </td>\n'

View File

@ -1139,6 +1139,7 @@ def individualPostAsHtml(allowDownloads: bool,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool,
themeName: str,
showRepeats=True,
showIcons=False,
manuallyApprovesFollowers=False,
@ -1307,7 +1308,8 @@ def individualPostAsHtml(allowDownloads: bool,
updateSpeaker(baseDir, httpPrefix,
nickname, domain, domainFull,
postJsonObject, personCache,
translate, postJsonObject['actor'])
translate, postJsonObject['actor'],
themeName)
ttsFile = open(announceFilename + '.tts', "w+")
if ttsFile:
ttsFile.write('\n')
@ -1684,7 +1686,8 @@ def htmlIndividualPost(cssCache: {},
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Show an individual post as html
"""
postStr = ''
@ -1725,7 +1728,7 @@ def htmlIndividualPost(cssCache: {},
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
allowLocalNetworkAccess, themeName,
False, authorized, False, False, False)
messageId = removeIdEnding(postJsonObject['id'])
@ -1753,6 +1756,7 @@ def htmlIndividualPost(cssCache: {},
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
False, authorized,
False, False, False) + postStr
@ -1783,6 +1787,7 @@ def htmlIndividualPost(cssCache: {},
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
False, authorized,
False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
@ -1804,7 +1809,8 @@ def htmlPostReplies(cssCache: {},
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Show the replies to an individual post as html
"""
repliesStr = ''
@ -1823,6 +1829,7 @@ def htmlPostReplies(cssCache: {},
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
False, False, False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'

View File

@ -66,7 +66,8 @@ def htmlProfileAfterSearch(cssCache: {},
showPublishedDateOnly: bool,
defaultTimeline: str,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Show a profile page after a search for a fediverse address
"""
if hasUsersPath(profileHandle) or '/@' in profileHandle:
@ -300,6 +301,7 @@ def htmlProfileAfterSearch(cssCache: {},
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances, allowLocalNetworkAccess,
themeName,
False, False, False, False, False)
i += 1
if i >= 20:
@ -804,7 +806,8 @@ def htmlProfile(rssIconAtTop: bool,
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess) + licenseStr
allowLocalNetworkAccess,
theme) + licenseStr
elif selected == 'following':
profileStr += \
_htmlProfileFollowing(translate, baseDir, httpPrefix,
@ -856,7 +859,8 @@ def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Shows posts on the profile screen
These should only be public posts
"""
@ -896,6 +900,7 @@ def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
False, False, False, True, False)
if postStr:
profileStr += postStr + separatorStr

View File

@ -527,7 +527,8 @@ def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str,
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Show a page containing search results for your post history
"""
if historysearch.startswith('!'):
@ -604,6 +605,7 @@ def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName,
showIndividualPostIcons,
showIndividualPostIcons,
False, False, False)
@ -626,7 +628,8 @@ def htmlHashtagSearch(cssCache: {},
YTReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool) -> str:
allowLocalNetworkAccess: bool,
themeName: str) -> str:
"""Show a page containing search results for a hashtag
"""
if hashtag.startswith('#'):
@ -778,6 +781,7 @@ def htmlHashtagSearch(cssCache: {},
allowLocalNetworkAccess,
showRepeats, showIcons,
manuallyApprovesFollowers,
themeName,
showPublicOnly,
storeToCache)
if postStr:

View File

@ -723,6 +723,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
boxName != 'dm',
showIndividualPostIcons,
manuallyApproveFollowers,
theme,
False, True)
_logTimelineTiming(enableTimingLog,
timelineStartTime, boxName, '12')