Skip to content

Commit 2634d72

Browse files
committed
Bug fix copytree and user notify voice update
- Fixing a bug with copytree to make it compatible on Python < 3.8 - Checking for updates in installed voice and notifying user
1 parent 4ea3989 commit 2634d72

File tree

4 files changed

+199
-71
lines changed

4 files changed

+199
-71
lines changed

globalPlugins/hear2readng_global_plugin/__init__.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
# this is a slightly modified version of the corresponding file in the sonata
1111
# project (https://github.com/mush42/sonata-nvda)
1212

13+
from threading import Thread
14+
from urllib import request
15+
1316
import core
1417
import globalPluginHandler
1518
import gui
@@ -22,6 +25,10 @@
2225
from globalPlugins.hear2readng_global_plugin.english_settings import (
2326
EnglishSpeechSettingsDialog,
2427
)
28+
from globalPlugins.hear2readng_global_plugin.utils import (
29+
H2RNG_VOICE_LIST_URL,
30+
parse_server_voices,
31+
)
2532
from globalPlugins.hear2readng_global_plugin.voice_manager import (
2633
Hear2ReadNGVoiceManagerDialog,
2734
)
@@ -35,7 +42,7 @@ def __init__(self, *args, **kwargs):
3542
self.__voice_manager_shown = False
3643
curr_synth_name = getSynth().name
3744
self._voice_checker = lambda: wx.CallLater(2000,
38-
self._perform_voice_check)
45+
self._perform_checks)
3946
core.postNvdaStartup.register(self._voice_checker)
4047

4148
# if "Hear2Read NG" not in curr_synth_name:
@@ -47,7 +54,7 @@ def __init__(self, *args, **kwargs):
4754
4,
4855
wx.ID_ANY,
4956
# Translators: label of a menu item
50-
_("Hear2Read Voice Downloader..."),
57+
_("Hear2Read Voice Manager..."),
5158
# Translators: Hear2Read Indic's voice manager menu item help
5259
_("Open the voice manager to download Hear2Read Indic voices"),
5360
)
@@ -138,16 +145,75 @@ def on_no_voices(self, curr_synth_name):
138145
wx.YES_NO | wx.ICON_WARNING,):
139146
wx.CallAfter(self.on_manager)
140147

148+
def on_voice_update(self, lang):
149+
150+
if self.__voice_manager_shown:# or gui.isModalMessageBoxActive():
151+
return
152+
153+
if wx.YES == gui.messageBox(
154+
# Translators: message telling the user that no voice is installed
155+
_(
156+
f"Update found for installed {lang} Hear2Read voice.\n"
157+
"Updated voice is available in the Hear2Read voice manager.\n"
158+
"Do you want to open the voice manager now?"
159+
),
160+
# Translators: title of a message telling the user that no Hear2Read Indic voice was found
161+
_("Hear2Read Voice Update"),
162+
wx.YES_NO | wx.ICON_INFORMATION,):
163+
wx.CallAfter(self.on_manager)
164+
165+
def _perform_checks(self):
166+
167+
self._perform_voice_check()
168+
169+
if self.__voice_manager_shown:
170+
return
171+
172+
self._perform_voice_update_check()
173+
141174
# @gui.blockAction.when(gui.blockAction.Context.MODAL_DIALOG_OPEN)
142175
def _perform_voice_check(self):
143-
if self.__voice_manager_shown:# or gui.isModalMessageBoxActive():
176+
if self.__voice_manager_shown:
144177
return
145178

146179
if not any(Hear2ReadNGVoiceManagerDialog.get_installed_voices()):
147180
curr_synth_name = getSynth().name
148181
# queueHandler.queueFunction(queueHandler.eventQueue, self.on_no_voices, curr_synth_name)
149182
self.on_no_voices(curr_synth_name)
150183

184+
185+
def _perform_voice_update_check(self):
186+
"""Populated the list of voices available on the server. Modifies the
187+
class attribute server_voices, a list of Voice objects. The operation
188+
is done in a background thread while a BusyInfo is displayed.
189+
"""
190+
# fetch_complete_event = Event()
191+
if self.__voice_manager_shown:# or gui.isModalMessageBoxActive():
192+
return
193+
194+
def fetch():
195+
"""Main function to fetch the voice list from the server. Sets the
196+
server_error_event/network_error_event attribute in case of failure
197+
"""
198+
try:
199+
with request.urlopen(H2RNG_VOICE_LIST_URL) as response:
200+
resp_str = response.read().decode('utf-8')
201+
server_voices = parse_server_voices(resp_str)
202+
if server_voices:
203+
installed_voices = Hear2ReadNGVoiceManagerDialog.get_installed_voices()
204+
for iso, installed_voice in installed_voices.items():
205+
server_voice = server_voices.get(iso, None)
206+
if server_voice and server_voice.id != installed_voice.id:
207+
log.info(f"checking update on {installed_voice.id}, found: {server_voice.id}")
208+
self.on_voice_update(server_voice.display_name)
209+
return
210+
except Exception as e:
211+
log.warn(f"Hear2Read unable to access internet to check voice updates: {e}")
212+
# finally:
213+
# fetch_complete_event.set()
214+
215+
Thread(target=fetch).start()
216+
151217
def terminate(self):
152218
try:
153219
gui.mainFrame.sysTrayIcon.menu.DestroyItem(self.itemHandle)

globalPlugins/hear2readng_global_plugin/utils.py

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import shutil
7+
import sys
78
import urllib.request
89
from dataclasses import dataclass
910
from threading import Thread
@@ -43,6 +44,11 @@
4344
"te":"Telugu",
4445
"en":"English"}
4546

47+
# URL suffix for voice files
48+
H2RNG_VOICES_DOWNLOAD_HTTP = "https://hear2read.org/Hear2Read/voices-piper/"
49+
# voice list URL
50+
H2RNG_VOICE_LIST_URL = "https://hear2read.org/nvda-addon/getH2RNGVoiceNames.php"
51+
4652
try:
4753
_dir=os.path.dirname(__file__.decode("mbcs"))
4854
except AttributeError:
@@ -53,6 +59,42 @@
5359
OLD_H2RNG_DATA_DIR = os.path.join(os.environ['ALLUSERSPROFILE'],
5460
"Hear2Read-ng")
5561

62+
def copytree_compat(src, dst):
63+
"""Copytree version with overwrite compatible for Python < 3.8. This is
64+
copied from the answer https://stackoverflow.com/a/13814557, and has a
65+
fairly basic functionality not accounting for symlinks, which is sufficient
66+
for our purposes
67+
68+
@param src: path to the source, to be copied from
69+
@type src: string
70+
@param dst: path to the destination, to be copied to
71+
@type dst: string
72+
"""
73+
if not os.path.exists(dst):
74+
os.makedirs(dst)
75+
for item in os.listdir(src):
76+
s = os.path.join(src, item)
77+
d = os.path.join(dst, item)
78+
if os.path.isdir(s):
79+
copytree_compat(s, d)
80+
else:
81+
if not os.path.exists(d) or os.stat(s).st_mtime - os.stat(d).st_mtime > 1:
82+
shutil.copy2(s, d)
83+
84+
def copytree_overwrite(src, dst):
85+
"""Wrapper to enable consistent behaviour in Python version < 3.8
86+
87+
@param src: path to the source, to be copied from
88+
@type src: string
89+
@param dst: path to the destination, to be copied to
90+
@type dst: string
91+
"""
92+
if sys.version_info >= (3, 8):
93+
shutil.copytree(src=src, dst=dst, dirs_exist_ok=True)
94+
else:
95+
copytree_compat(src=src, dst=dst)
96+
97+
5698
def check_files():
5799
"""
58100
Checks whether the files and directories vital to Hear2Read Indic are
@@ -74,28 +116,6 @@ def check_files():
74116
if not os.listdir(H2RNG_PHONEME_DIR):
75117
return False
76118
# phonedir_is_present = True
77-
78-
# if os.path.isdir(H2RNG_VOICES_DIR):
79-
# file_list = os.listdir(H2RNG_VOICES_DIR)
80-
81-
# for file_name in file_list:
82-
# parts = file_name.split(".")
83-
# if parts[-1] == "onnx" and f"{file_name}.json" in file_list:
84-
# return True
85-
86-
# return wx.YES == gui.messageBox(
87-
# # Translators: message telling the user that no voice is installed
88-
# _(
89-
# "Hear2Read needs to have an Indic voice to function.\n"
90-
# "Please select a Hear2Read voice to download from the manager.\n"
91-
# "Do you want to open the voice manager now?"
92-
# ),
93-
# # Translators: title of a message telling the user that no Hear2Read Indic voice was found
94-
# _("Hear2Read Indic Voices"),
95-
# wx.YES_NO | wx.ICON_WARNING,)
96-
97-
# voice_is_present = True
98-
# break
99119

100120
except Exception as e:
101121
log.warn(f"Hear2Read Indic check failed with exception: {e}")
@@ -105,6 +125,40 @@ def check_files():
105125
# return dll_is_present and phonedir_is_present and voice_is_present
106126

107127

128+
def parse_server_voices(resp_str):
129+
"""Parses the pipe separated file list response from the server
130+
131+
@param resp_str: string of pipe separated voice related files
132+
@type resp_str: string
133+
"""
134+
server_voices = {}
135+
server_files = resp_str.split('|')
136+
for file in server_files:
137+
if file.startswith("en"):
138+
continue
139+
parts = file.split(".")
140+
if parts[-1] == "onnx":
141+
if f"{file}.json" in server_files:
142+
iso_lang = parts[0].split("-")[0].split("_")[0]
143+
extra = False
144+
if f"{file}.zip" in server_files:
145+
extra = True
146+
if iso_lang in lang_names.keys():
147+
server_voices[iso_lang] = Voice(id=parts[0],
148+
lang_iso=iso_lang,
149+
display_name=lang_names[iso_lang],
150+
state="Download",
151+
extra=extra)
152+
else:
153+
server_voices[iso_lang] = Voice(id=parts[0],
154+
lang_iso=iso_lang,
155+
display_name=f"Unknown Lang ({iso_lang})",
156+
state="Download",
157+
extra=extra)
158+
159+
return server_voices
160+
161+
108162
def populateVoices():
109163
pathName = os.path.join(H2RNG_VOICES_DIR)
110164
voices = dict()
@@ -164,8 +218,7 @@ def move_old_voices():
164218

165219
if os.path.isdir(old_wavs_dir):
166220
try:
167-
shutil.copytree(src=old_wavs_dir, dst=H2RNG_WAVS_DIR,
168-
dirs_exist_ok=True)
221+
copytree_overwrite(src=old_wavs_dir, dst=H2RNG_WAVS_DIR)
169222
except Exception as e:
170223
log.warn("Hear2Read Indic unable to copy old wav folders")
171224

@@ -194,7 +247,8 @@ def move_old_voices():
194247

195248
def onInstall():
196249
"""A fallback that tries moving the required data files in case it wasn't
197-
done by installTasks.py
250+
done by installTasks.py It is duplicated as importing is not available with
251+
installTasks.py
198252
199253
@raises e: raises any exceptions that can occur while transferring the data
200254
@return: returns True if any voices from an older version (<v1.5) have been
@@ -206,7 +260,7 @@ def onInstall():
206260
src_dir = os.path.join(_dir, "res")
207261

208262
try:
209-
shutil.copytree(src=src_dir, dst=H2RNG_DATA_DIR, dirs_exist_ok=True)
263+
copytree_overwrite(src=src_dir, dst=H2RNG_DATA_DIR)
210264
shutil.rmtree(src_dir)
211265
except Exception as e:
212266
log.warn(f"Error installing Hear2Read Indic data files: {e}")

globalPlugins/hear2readng_global_plugin/voice_manager.py

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@
1818
import wx
1919
from logHandler import log
2020

21+
from synthDrivers._H2R_NG_Speak import H2RNG_DATA_DIR, H2RNG_VOICES_DIR
22+
2123
from .utils import (
22-
H2RNG_DATA_DIR,
23-
H2RNG_VOICES_DIR,
24+
# H2RNG_DATA_DIR,
25+
H2RNG_VOICE_LIST_URL,
26+
# H2RNG_VOICES_DIR,
27+
H2RNG_VOICES_DOWNLOAD_HTTP,
2428
DownloadThread,
2529
Voice,
2630
check_files,
27-
lang_names,
31+
# lang_names,
2832
onInstall,
33+
parse_server_voices,
2934
populateVoices,
3035
)
3136

@@ -34,10 +39,6 @@
3439
DLL_FILE_NAME_PREFIX = "Hear2ReadNG_addon_engine"
3540
DOWNLOAD_SUFFIX = ".download"
3641

37-
# URL suffix for voice files
38-
H2RNG_VOICES_DOWNLOAD_HTTP = "https://hear2read.org/Hear2Read/voices-piper/"
39-
# voice list URL
40-
H2RNG_VOICE_LIST_URL = "https://hear2read.org/nvda-addon/getH2RNGVoiceNames.php"
4142

4243
# TODO remove copyright, add copyright
4344
# TODO rename the synth file to have no spaces(?)
@@ -529,6 +530,7 @@ def on_download_cancel(self, voice, error_message=None):
529530
wx.OK | wx.ICON_WARNING
530531
)
531532

533+
# TODO remove duplicate of utils.populateVoices
532534
@classmethod
533535
def get_installed_voices(self):
534536
"""Classmethod to get installed voices. Returns a dictionary of voices
@@ -561,7 +563,6 @@ def get_installed_voices(self):
561563

562564
return installed_voices
563565

564-
565566
def get_server_voices(self):
566567
"""Populated the list of voices available on the server. Modifies the
567568
class attribute server_voices, a list of Voice objects. The operation
@@ -574,35 +575,6 @@ class attribute server_voices, a list of Voice objects. The operation
574575
parent=self)
575576
wx.Yield()
576577

577-
def parse_server_voices(resp_str):
578-
"""Parses the pipe separated file list response from the server
579-
580-
@param resp_str: string of pipe separated voice related files
581-
@type resp_str: string
582-
"""
583-
server_files = resp_str.split('|')
584-
for file in server_files:
585-
if file.startswith("en"):
586-
continue
587-
parts = file.split(".")
588-
if parts[-1] == "onnx":
589-
if f"{file}.json" in server_files:
590-
iso_lang = parts[0].split("-")[0].split("_")[0]
591-
extra = False
592-
if f"{file}.zip" in server_files:
593-
extra = True
594-
if iso_lang in lang_names.keys():
595-
self.server_voices[iso_lang] = Voice(id=parts[0],
596-
lang_iso=iso_lang,
597-
display_name=lang_names[iso_lang],
598-
state="Download",
599-
extra=extra)
600-
else:
601-
self.server_voices[iso_lang] = Voice(id=parts[0],
602-
lang_iso=iso_lang,
603-
display_name=f"Unknown Lang ({iso_lang})",
604-
state="Download",
605-
extra=extra)
606578

607579
def dismiss_loading_dialog():
608580
nonlocal loading_dialog
@@ -615,7 +587,8 @@ def fetch():
615587
try:
616588
with request.urlopen(H2RNG_VOICE_LIST_URL) as response:
617589
resp_str = response.read().decode('utf-8')
618-
parse_server_voices(resp_str)
590+
self.server_voices = parse_server_voices(resp_str)
591+
# parse_server_voices(resp_str)
619592
except HTTPError as http_e:
620593
self.server_error_event.set()
621594
log.warn(f"Hear2Read http error: {http_e}")
@@ -663,4 +636,4 @@ def get_display_voices(self):
663636
else:
664637
self.display_voices.append(local_voice)
665638

666-
self.display_voices.sort(key=operator.attrgetter("display_name"))
639+
self.display_voices.sort(key=operator.attrgetter("display_name"))

0 commit comments

Comments
 (0)