diff --git a/speaker.py b/speaker.py index eb71b6086..5bb896f90 100644 --- a/speaker.py +++ b/speaker.py @@ -28,55 +28,55 @@ from utils import acct_dir from utils import local_actor_url from content import html_replace_quote_marks -speakerRemoveChars = ('.\n', '. ', ',', ';', '?', '!') +SPEAKER_REMOVE_CHARS = ('.\n', '. ', ',', ';', '?', '!') -def get_speaker_pitch(displayName: str, screenreader: str, gender) -> int: +def get_speaker_pitch(display_name: str, screenreader: str, gender) -> int: """Returns the speech synthesis pitch for the given name """ - random.seed(displayName) - rangeMin = 1 - rangeMax = 100 + random.seed(display_name) + range_min = 1 + range_max = 100 if 'She' in gender: - rangeMin = 50 + range_min = 50 elif 'Him' in gender: - rangeMax = 50 + range_max = 50 if screenreader == 'picospeaker': - rangeMin = -6 - rangeMax = 3 + range_min = -6 + range_max = 3 if 'She' in gender: - rangeMin = -1 + range_min = -1 elif 'Him' in gender: - rangeMax = -1 - return random.randint(rangeMin, rangeMax) + range_max = -1 + return random.randint(range_min, range_max) -def get_speaker_rate(displayName: str, screenreader: str) -> int: +def get_speaker_rate(display_name: str, screenreader: str) -> int: """Returns the speech synthesis rate for the given name """ - random.seed(displayName) + random.seed(display_name) if screenreader == 'picospeaker': return random.randint(-40, -20) return random.randint(50, 120) -def get_speaker_range(displayName: str) -> int: +def get_speaker_range(display_name: str) -> int: """Returns the speech synthesis range for the given name """ - random.seed(displayName) + random.seed(display_name) return random.randint(300, 800) -def _speaker_pronounce(base_dir: str, sayText: str, translate: {}) -> str: +def _speaker_pronounce(base_dir: str, say_text: str, translate: {}) -> str: """Screen readers may not always pronounce correctly, so you can have a file which specifies conversions. File should contain line items such as: Epicyon -> Epi-cyon """ - pronounceFilename = base_dir + '/accounts/speaker_pronounce.txt' - convertDict = {} + pronounce_filename = base_dir + '/accounts/speaker_pronounce.txt' + convert_dict = {} if translate: - convertDict = { + convert_dict = { "Epicyon": "Epi-cyon", "espeak": "e-speak", "emoji": "emowji", @@ -139,10 +139,10 @@ def _speaker_pronounce(base_dir: str, sayText: str, translate: {}) -> str: "(": ",", ")": "," } - if os.path.isfile(pronounceFilename): - with open(pronounceFilename, 'r') as fp: - pronounceList = fp.readlines() - for conversion in pronounceList: + if os.path.isfile(pronounce_filename): + with open(pronounce_filename, 'r') as fp_pro: + pronounce_list = fp_pro.readlines() + for conversion in pronounce_list: separator = None if '->' in conversion: separator = '->' @@ -157,43 +157,43 @@ def _speaker_pronounce(base_dir: str, sayText: str, translate: {}) -> str: text = conversion.split(separator)[0].strip() converted = conversion.split(separator)[1].strip() - convertDict[text] = converted - for text, converted in convertDict.items(): - if text in sayText: - sayText = sayText.replace(text, converted) - return sayText + convert_dict[text] = converted + for text, converted in convert_dict.items(): + if text in say_text: + say_text = say_text.replace(text, converted) + return say_text -def speaker_replace_links(sayText: str, translate: {}, - detectedLinks: []) -> str: +def speaker_replace_links(say_text: str, translate: {}, + detected_links: []) -> str: """Replaces any links in the given text with "link to [domain]". Instead of reading out potentially very long and meaningless links """ - text = sayText + text = say_text text = text.replace('?v=', '__v=') - for ch in speakerRemoveChars: - text = text.replace(ch, ' ') + for char in SPEAKER_REMOVE_CHARS: + text = text.replace(char, ' ') text = text.replace('__v=', '?v=') replacements = {} - wordsList = text.split(' ') + words_list = text.split(' ') if translate.get('Linked'): - linkedStr = translate['Linked'] + linked_str = translate['Linked'] else: - linkedStr = 'Linked' - prevWord = '' - for word in wordsList: + linked_str = 'Linked' + prev_word = '' + for word in words_list: if word.startswith('v='): replacements[word] = '' if word.startswith(':'): if word.endswith(':'): replacements[word] = ', emowji ' + word.replace(':', '') + ',' continue - if word.startswith('@') and not prevWord.endswith('RT'): + if word.startswith('@') and not prev_word.endswith('RT'): # replace mentions, but not re-tweets if translate.get('mentioning'): replacements[word] = \ translate['mentioning'] + ' ' + word[1:] + ', ' - prevWord = word + prev_word = word domain = None domain_full = None @@ -209,94 +209,94 @@ def speaker_replace_links(sayText: str, translate: {}, domain = domain.split('/')[0] if domain.startswith('www.'): domain = domain.replace('www.', '') - replacements[domain_full] = '. ' + linkedStr + ' ' + domain + '.' - detectedLinks.append(domain_full) - for replaceStr, newStr in replacements.items(): - sayText = sayText.replace(replaceStr, newStr) - return sayText.replace('..', '.') + replacements[domain_full] = '. ' + linked_str + ' ' + domain + '.' + detected_links.append(domain_full) + for replace_str, new_str in replacements.items(): + say_text = say_text.replace(replace_str, new_str) + return say_text.replace('..', '.') -def _add_ssm_lemphasis(sayText: str) -> str: +def _add_ssm_lemphasis(say_text: 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(' ') + if '*' not in say_text: + return say_text + text = say_text + for char in SPEAKER_REMOVE_CHARS: + text = text.replace(char, ' ') + words_list = text.split(' ') replacements = {} - for word in wordsList: + for word in words_list: if word.startswith('*'): if word.endswith('*'): replacements[word] = \ '' + \ word.replace('*', '') + \ '' - for replaceStr, newStr in replacements.items(): - sayText = sayText.replace(replaceStr, newStr) - return sayText + for replace_str, new_str in replacements.items(): + say_text = say_text.replace(replace_str, new_str) + return say_text -def _remove_emoji_from_text(sayText: str) -> str: +def _remove_emoji_from_text(say_text: str) -> str: """Removes :emoji: from the given text """ - if ':' not in sayText: - return sayText - text = sayText - for ch in speakerRemoveChars: - text = text.replace(ch, ' ') - wordsList = text.split(' ') + if ':' not in say_text: + return say_text + text = say_text + for char in SPEAKER_REMOVE_CHARS: + text = text.replace(char, ' ') + words_list = text.split(' ') replacements = {} - for word in wordsList: + for word in words_list: if word.startswith(':'): if word.endswith(':'): replacements[word] = '' - for replaceStr, newStr in replacements.items(): - sayText = sayText.replace(replaceStr, newStr) - return sayText.replace(' ', ' ').strip() + for replace_str, new_str in replacements.items(): + say_text = say_text.replace(replace_str, new_str) + return say_text.replace(' ', ' ').strip() -def _speaker_endpoint_json(displayName: str, summary: str, - content: str, sayContent: str, - imageDescription: str, +def _speaker_endpoint_json(display_name: str, summary: str, + content: str, say_content: str, + image_description: str, links: [], gender: str, post_id: str, - postDM: bool, postReply: bool, - followRequestsExist: bool, - followRequestsList: [], - likedBy: str, published: str, postCal: bool, - postShare: bool, theme_name: str, - isDirect: bool, replyToYou: bool) -> {}: + post_dm: bool, post_reply: bool, + follow_requests_exist: bool, + follow_requests_list: [], + liked_by: str, published: str, post_cal: bool, + post_share: bool, theme_name: str, + is_direct: bool, reply_to_you: bool) -> {}: """Returns a json endpoint for the TTS speaker """ - speakerJson = { - "name": displayName, + speaker_json = { + "name": display_name, "summary": summary, "content": content, - "say": sayContent, + "say": say_content, "published": published, - "imageDescription": imageDescription, + "imageDescription": image_description, "detectedLinks": links, "id": post_id, - "direct": isDirect, - "replyToYou": replyToYou, + "direct": is_direct, + "replyToYou": reply_to_you, "notify": { "theme": theme_name, - "dm": postDM, - "reply": postReply, - "followRequests": followRequestsExist, - "followRequestsList": followRequestsList, - "likedBy": likedBy, - "calendar": postCal, - "share": postShare + "dm": post_dm, + "reply": post_reply, + "followRequests": follow_requests_exist, + "followRequestsList": follow_requests_list, + "likedBy": liked_by, + "calendar": post_cal, + "share": post_share } } if gender: - speakerJson['gender'] = gender - return speakerJson + speaker_json['gender'] = gender + return speaker_json -def _ssm_lheader(system_language: str, instanceTitle: str) -> str: +def _ssm_lheader(system_language: str, instance_title: str) -> str: """Returns a header for an SSML document """ return '\n' + \ @@ -307,26 +307,26 @@ def _ssm_lheader(system_language: str, instanceTitle: str) -> str: ' version="1.1">\n' + \ ' \n' + \ ' ' + \ - instanceTitle + ' inbox\n' + \ + instance_title + ' inbox\n' + \ ' \n' -def _speaker_endpoint_ssml(displayName: str, summary: str, - content: str, imageDescription: str, +def _speaker_endpoint_ssml(display_name: str, summary: str, + content: str, image_description: str, links: [], language: str, - instanceTitle: str, + instance_title: 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' + lang_short = 'en' if language: - langShort = language[:2] + lang_short = language[:2] if not gender: gender = 'neutral' else: - if langShort == 'en': + if lang_short == 'en': gender = gender.lower() if 'he/him' in gender: gender = 'male' @@ -336,11 +336,11 @@ def _speaker_endpoint_ssml(displayName: str, summary: str, gender = 'neutral' content = _add_ssm_lemphasis(content) - voiceParams = 'name="' + displayName + '" gender="' + gender + '"' - return _ssm_lheader(langShort, instanceTitle) + \ + voice_params = 'name="' + display_name + '" gender="' + gender + '"' + return _ssm_lheader(lang_short, instance_title) + \ '

\n' + \ ' \n' + \ - ' \n' + \ + ' \n' + \ ' ' + content + '\n' + \ ' \n' + \ ' \n' + \ @@ -351,30 +351,30 @@ def _speaker_endpoint_ssml(displayName: str, summary: str, def get_ssm_lbox(base_dir: str, path: str, domain: str, system_language: str, - instanceTitle: str, - boxName: str) -> str: + instance_title: str, + box_name: str) -> str: """Returns SSML for the given timeline """ nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] - speakerFilename = \ + speaker_filename = \ acct_dir(base_dir, nickname, domain) + '/speaker.json' - if not os.path.isfile(speakerFilename): + if not os.path.isfile(speaker_filename): return None - speakerJson = load_json(speakerFilename) - if not speakerJson: + speaker_json = load_json(speaker_filename) + if not speaker_json: return None gender = None - if speakerJson.get('gender'): - gender = speakerJson['gender'] - return _speaker_endpoint_ssml(speakerJson['name'], - speakerJson['summary'], - speakerJson['say'], - speakerJson['imageDescription'], - speakerJson['detectedLinks'], + if speaker_json.get('gender'): + gender = speaker_json['gender'] + return _speaker_endpoint_ssml(speaker_json['name'], + speaker_json['summary'], + speaker_json['say'], + speaker_json['imageDescription'], + speaker_json['detectedLinks'], system_language, - instanceTitle, gender) + instance_title, gender) def speakable_text(base_dir: str, content: str, translate: {}) -> (str, []): @@ -389,23 +389,23 @@ def speakable_text(base_dir: str, content: str, translate: {}) -> (str, []): if ' <3' in content: content = content.replace(' <3', ' ' + translate['heart']) content = remove_html(html_replace_quote_marks(content)) - detectedLinks = [] - content = speaker_replace_links(content, translate, detectedLinks) + detected_links = [] + content = speaker_replace_links(content, translate, detected_links) # replace all double spaces while ' ' in content: content = content.replace(' ', ' ') content = content.replace(' . ', '. ').strip() - sayContent = _speaker_pronounce(base_dir, content, translate) + say_content = _speaker_pronounce(base_dir, content, translate) # replace all double spaces - while ' ' in sayContent: - sayContent = sayContent.replace(' ', ' ') - return sayContent.replace(' . ', '. ').strip(), detectedLinks + while ' ' in say_content: + say_content = say_content.replace(' ', ' ') + return say_content.replace(' . ', '. ').strip(), detected_links def _post_to_speaker_json(base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, post_json_object: {}, person_cache: {}, - translate: {}, announcingActor: str, + translate: {}, announcing_actor: str, theme_name: str) -> {}: """Converts an ActivityPub post into some Json containing speech synthesis parameters. @@ -418,7 +418,7 @@ def _post_to_speaker_json(base_dir: str, http_prefix: str, return if not isinstance(post_json_object['object']['content'], str): return - detectedLinks = [] + detected_links = [] content = urllib.parse.unquote_plus(post_json_object['object']['content']) content = html.unescape(content) content = content.replace('

', '').replace('

', ' ') @@ -427,35 +427,35 @@ def _post_to_speaker_json(base_dir: str, http_prefix: str, if ' <3' in content: content = content.replace(' <3', ' ' + translate['heart']) content = remove_html(html_replace_quote_marks(content)) - content = speaker_replace_links(content, translate, detectedLinks) + content = speaker_replace_links(content, translate, detected_links) # replace all double spaces while ' ' in content: content = content.replace(' ', ' ') content = content.replace(' . ', '. ').strip() - sayContent = content - sayContent = _speaker_pronounce(base_dir, content, translate) + say_content = content + say_content = _speaker_pronounce(base_dir, content, translate) # replace all double spaces - while ' ' in sayContent: - sayContent = sayContent.replace(' ', ' ') - sayContent = sayContent.replace(' . ', '. ').strip() + while ' ' in say_content: + say_content = say_content.replace(' ', ' ') + say_content = say_content.replace(' . ', '. ').strip() else: - sayContent = content + say_content = content - imageDescription = '' + image_description = '' if post_json_object['object'].get('attachment'): - attachList = post_json_object['object']['attachment'] - if isinstance(attachList, list): - for img in attachList: + attach_list = post_json_object['object']['attachment'] + if isinstance(attach_list, list): + for img in attach_list: if not isinstance(img, dict): continue if img.get('name'): if isinstance(img['name'], str): - imageDescription += \ + image_description += \ img['name'] + '. ' - isDirect = is_dm(post_json_object) + is_direct = is_dm(post_json_object) actor = local_actor_url(http_prefix, nickname, domain_full) - replyToYou = is_reply(post_json_object, actor) + reply_to_you = is_reply(post_json_object, actor) published = '' if post_json_object['object'].get('published'): @@ -469,84 +469,85 @@ def _post_to_speaker_json(base_dir: str, http_prefix: str, urllib.parse.unquote_plus(post_json_object_summary) summary = html.unescape(summary) - speakerName = \ + speaker_name = \ get_display_name(base_dir, post_json_object['actor'], person_cache) - if not speakerName: + if not speaker_name: return - speakerName = _remove_emoji_from_text(speakerName) - speakerName = speakerName.replace('_', ' ') - speakerName = camel_case_split(speakerName) + speaker_name = _remove_emoji_from_text(speaker_name) + speaker_name = speaker_name.replace('_', ' ') + speaker_name = camel_case_split(speaker_name) gender = get_gender_from_bio(base_dir, post_json_object['actor'], person_cache, translate) - if announcingActor: - announcedNickname = get_nickname_from_actor(announcingActor) - announcedDomain, announcedport = get_domain_from_actor(announcingActor) - if announcedNickname and announcedDomain: - announcedHandle = announcedNickname + '@' + announcedDomain - sayContent = \ + if announcing_actor: + announced_nickname = get_nickname_from_actor(announcing_actor) + announced_domain, _ = \ + get_domain_from_actor(announcing_actor) + if announced_nickname and announced_domain: + announced_handle = announced_nickname + '@' + announced_domain + say_content = \ translate['announces'] + ' ' + \ - announcedHandle + '. ' + sayContent + announced_handle + '. ' + say_content content = \ translate['announces'] + ' ' + \ - announcedHandle + '. ' + content + announced_handle + '. ' + content post_id = None if post_json_object['object'].get('id'): post_id = remove_id_ending(post_json_object['object']['id']) - followRequestsExist = False - followRequestsList = [] - accountsDir = acct_dir(base_dir, nickname, domain_full) - approveFollowsFilename = accountsDir + '/followrequests.txt' - if os.path.isfile(approveFollowsFilename): - with open(approveFollowsFilename, 'r') as fp: - follows = fp.readlines() + follow_requests_exist = False + follow_requests_list = [] + accounts_dir = acct_dir(base_dir, nickname, domain_full) + approve_follows_filename = accounts_dir + '/followrequests.txt' + if os.path.isfile(approve_follows_filename): + with open(approve_follows_filename, 'r') as fp_foll: + follows = fp_foll.readlines() if len(follows) > 0: - followRequestsExist = True + follow_requests_exist = True for i in range(len(follows)): follows[i] = follows[i].strip() - followRequestsList = follows - postDM = False - dmFilename = accountsDir + '/.newDM' - if os.path.isfile(dmFilename): - postDM = True - postReply = False - replyFilename = accountsDir + '/.newReply' - if os.path.isfile(replyFilename): - postReply = True - likedBy = '' - likeFilename = accountsDir + '/.newLike' - if os.path.isfile(likeFilename): - with open(likeFilename, 'r') as fp: - likedBy = fp.read() - calendarFilename = accountsDir + '/.newCalendar' - postCal = os.path.isfile(calendarFilename) - shareFilename = accountsDir + '/.newShare' - postShare = os.path.isfile(shareFilename) + follow_requests_list = follows + post_dm = False + dm_filename = accounts_dir + '/.newDM' + if os.path.isfile(dm_filename): + post_dm = True + post_reply = False + reply_filename = accounts_dir + '/.newReply' + if os.path.isfile(reply_filename): + post_reply = True + liked_by = '' + like_filename = accounts_dir + '/.newLike' + if os.path.isfile(like_filename): + with open(like_filename, 'r') as fp_like: + liked_by = fp_like.read() + calendar_filename = accounts_dir + '/.newCalendar' + post_cal = os.path.isfile(calendar_filename) + share_filename = accounts_dir + '/.newShare' + post_share = os.path.isfile(share_filename) - return _speaker_endpoint_json(speakerName, summary, - content, sayContent, imageDescription, - detectedLinks, gender, post_id, - postDM, postReply, - followRequestsExist, - followRequestsList, - likedBy, published, - postCal, postShare, theme_name, - isDirect, replyToYou) + return _speaker_endpoint_json(speaker_name, summary, + content, say_content, image_description, + detected_links, gender, post_id, + post_dm, post_reply, + follow_requests_exist, + follow_requests_list, + liked_by, published, + post_cal, post_share, theme_name, + is_direct, reply_to_you) def update_speaker(base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, post_json_object: {}, person_cache: {}, - translate: {}, announcingActor: str, + translate: {}, announcing_actor: str, theme_name: str) -> None: """ Generates a json file which can be used for TTS announcement of incoming inbox posts """ - speakerJson = \ + speaker_json = \ _post_to_speaker_json(base_dir, http_prefix, nickname, domain, domain_full, post_json_object, person_cache, - translate, announcingActor, + translate, announcing_actor, theme_name) - speakerFilename = acct_dir(base_dir, nickname, domain) + '/speaker.json' - save_json(speakerJson, speakerFilename) + speaker_filename = acct_dir(base_dir, nickname, domain) + '/speaker.json' + save_json(speaker_json, speaker_filename)