Read Aloud config: Show already downloaded voices in bold

This commit is contained in:
Kovid Goyal 2024-09-28 11:51:00 +05:30
parent 065bafd0c5
commit 4ae5fe59c0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 30 additions and 3 deletions

View File

@ -3,6 +3,7 @@
from qt.core import ( from qt.core import (
QCheckBox, QCheckBox,
QFont,
QFormLayout, QFormLayout,
QHBoxLayout, QHBoxLayout,
QIcon, QIcon,
@ -136,20 +137,30 @@ class Voices(QTreeWidget):
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.system_default_voice = Voice() self.system_default_voice = Voice()
self.currentItemChanged.connect(self.voice_changed) self.currentItemChanged.connect(self.voice_changed)
self.normal_font = f = self.font()
self.highlight_font = f = QFont(f)
f.setBold(True), f.setItalic(True)
def sizeHint(self) -> QSize: def sizeHint(self) -> QSize:
return QSize(400, 500) return QSize(400, 500)
def set_item_downloaded_state(self, ans: QTreeWidgetItem) -> None:
voice = ans.data(0, Qt.ItemDataRole.UserRole)
is_downloaded = bool(voice.engine_data and voice.engine_data.get('is_downloaded'))
ans.setFont(0, self.highlight_font if is_downloaded else self.normal_font)
def set_voices(self, all_voices: tuple[Voice, ...], current_voice: str, engine_metadata: EngineMetadata) -> None: def set_voices(self, all_voices: tuple[Voice, ...], current_voice: str, engine_metadata: EngineMetadata) -> None:
self.clear() self.clear()
current_item = None current_item = None
def qv(parent, voice): def qv(parent, voice):
nonlocal current_item nonlocal current_item
ans = QTreeWidgetItem(parent, [voice.short_text(engine_metadata)]) text = voice.short_text(engine_metadata)
ans = QTreeWidgetItem(parent, [text])
ans.setData(0, Qt.ItemDataRole.UserRole, voice) ans.setData(0, Qt.ItemDataRole.UserRole, voice)
ans.setToolTip(0, voice.tooltip(engine_metadata)) ans.setToolTip(0, voice.tooltip(engine_metadata))
if current_voice == voice.name: if current_voice == voice.name:
current_item = ans current_item = ans
self.set_item_downloaded_state(ans)
return ans return ans
qv(self.invisibleRootItem(), self.system_default_voice) qv(self.invisibleRootItem(), self.system_default_voice)
vmap = {} vmap = {}
@ -182,6 +193,11 @@ class Voices(QTreeWidget):
if ci is not None: if ci is not None:
return ci.data(0, Qt.ItemDataRole.UserRole) return ci.data(0, Qt.ItemDataRole.UserRole)
def refresh_current_item(self) -> None:
ci = self.currentItem()
if ci is not None:
self.set_item_downloaded_state(ci)
class EngineSpecificConfig(QWidget): class EngineSpecificConfig(QWidget):
@ -357,6 +373,7 @@ class ConfigDialog(Dialog):
else: else:
b.setIcon(QIcon.ic('download-metadata.png')) b.setIcon(QIcon.ic('download-metadata.png'))
b.setText(_('Download voice')) b.setText(_('Download voice'))
self.engine_specific_config.voices.refresh_current_item()
def voice_action(self): def voice_action(self):
self.engine_specific_config.voice_action() self.engine_specific_config.voice_action()

View File

@ -406,6 +406,9 @@ class Piper(TTSBackend):
ans = [] ans = []
lang_voices_map = {} lang_voices_map = {}
self._voice_name_map = {} self._voice_name_map = {}
downloaded = set()
with suppress(OSError):
downloaded = set(os.listdir(self.cache_dir))
for bcp_code, voice_map in d['lang_map'].items(): for bcp_code, voice_map in d['lang_map'].items():
lang, sep, country = bcp_code.partition('_') lang, sep, country = bcp_code.partition('_')
lang = canonicalize_lang(lang) or lang lang = canonicalize_lang(lang) or lang
@ -416,9 +419,10 @@ class Piper(TTSBackend):
q = Quality.from_piper_quality(qual) q = Quality.from_piper_quality(qual)
if best_qual is None or q.value < best_qual.value: if best_qual is None or q.value < best_qual.value:
best_qual = q best_qual = q
mf = f'{bcp_code}-{voice_name}-{qual}.onnx'
voice = Voice(bcp_code + ':' + voice_name, lang, country, human_name=voice_name, quality=q, engine_data={ voice = Voice(bcp_code + ':' + voice_name, lang, country, human_name=voice_name, quality=q, engine_data={
'model_url': e['model'], 'config_url': e['config'], 'model_url': e['model'], 'config_url': e['config'],
'model_filename': f'{bcp_code}-{voice_name}-{qual}.onnx', 'model_filename': mf, 'is_downloaded': mf in downloaded,
}) })
if voice: if voice:
ans.append(voice) ans.append(voice)
@ -442,9 +446,13 @@ class Piper(TTSBackend):
lang = canonicalize_lang(lang) or lang lang = canonicalize_lang(lang) or lang
return self._voice_for_lang.get(lang) or self._voice_for_lang['eng'] return self._voice_for_lang.get(lang) or self._voice_for_lang['eng']
@property
def cache_dir(self) -> str:
return os.path.join(cache_dir(), 'piper-voices')
def _paths_for_voice(self, voice: Voice) -> tuple[str, str]: def _paths_for_voice(self, voice: Voice) -> tuple[str, str]:
fname = voice.engine_data['model_filename'] fname = voice.engine_data['model_filename']
model_path = os.path.join(cache_dir(), 'piper-voices', fname) model_path = os.path.join(self.cache_dir, fname)
config_path = os.path.join(os.path.dirname(model_path), fname + '.json') config_path = os.path.join(os.path.dirname(model_path), fname + '.json')
return model_path, config_path return model_path, config_path
@ -462,6 +470,7 @@ class Piper(TTSBackend):
for path in self._paths_for_voice(v): for path in self._paths_for_voice(v):
with suppress(FileNotFoundError): with suppress(FileNotFoundError):
os.remove(path) os.remove(path)
v.engine_data['is_downloaded'] = False
def _download_voice(self, voice: Voice, download_even_if_exists: bool = False) -> tuple[str, str]: def _download_voice(self, voice: Voice, download_even_if_exists: bool = False) -> tuple[str, str]:
model_path, config_path = self._paths_for_voice(voice) model_path, config_path = self._paths_for_voice(voice)
@ -475,6 +484,7 @@ class Piper(TTSBackend):
voice.engine_data['config_url']: (config_path, _('Neural network metadata')), voice.engine_data['config_url']: (config_path, _('Neural network metadata')),
}, parent=widget_parent(self), headless=getattr(QApplication.instance(), 'headless', False) }, parent=widget_parent(self), headless=getattr(QApplication.instance(), 'headless', False)
) )
voice.engine_data['is_downloaded'] = bool(ok)
return (model_path, config_path) if ok else ('', '') return (model_path, config_path) if ok else ('', '')
def download_voice(self, v: Voice) -> None: def download_voice(self, v: Voice) -> None: