diff --git a/src/calibre/gui2/tts/config.py b/src/calibre/gui2/tts/config.py index c1d6ea28b2..1822a65802 100644 --- a/src/calibre/gui2/tts/config.py +++ b/src/calibre/gui2/tts/config.py @@ -3,6 +3,7 @@ from qt.core import ( QCheckBox, + QFont, QFormLayout, QHBoxLayout, QIcon, @@ -136,20 +137,30 @@ class Voices(QTreeWidget): self.setHeaderHidden(True) self.system_default_voice = Voice() 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: 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: self.clear() current_item = None def qv(parent, voice): 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.setToolTip(0, voice.tooltip(engine_metadata)) if current_voice == voice.name: current_item = ans + self.set_item_downloaded_state(ans) return ans qv(self.invisibleRootItem(), self.system_default_voice) vmap = {} @@ -182,6 +193,11 @@ class Voices(QTreeWidget): if ci is not None: 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): @@ -357,6 +373,7 @@ class ConfigDialog(Dialog): else: b.setIcon(QIcon.ic('download-metadata.png')) b.setText(_('Download voice')) + self.engine_specific_config.voices.refresh_current_item() def voice_action(self): self.engine_specific_config.voice_action() diff --git a/src/calibre/gui2/tts/piper.py b/src/calibre/gui2/tts/piper.py index 1b30308c6a..7386ef4e1a 100644 --- a/src/calibre/gui2/tts/piper.py +++ b/src/calibre/gui2/tts/piper.py @@ -406,6 +406,9 @@ class Piper(TTSBackend): ans = [] lang_voices_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(): lang, sep, country = bcp_code.partition('_') lang = canonicalize_lang(lang) or lang @@ -416,9 +419,10 @@ class Piper(TTSBackend): q = Quality.from_piper_quality(qual) if best_qual is None or q.value < best_qual.value: 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={ '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: ans.append(voice) @@ -442,9 +446,13 @@ class Piper(TTSBackend): lang = canonicalize_lang(lang) or lang 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]: 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') return model_path, config_path @@ -462,6 +470,7 @@ class Piper(TTSBackend): for path in self._paths_for_voice(v): with suppress(FileNotFoundError): os.remove(path) + v.engine_data['is_downloaded'] = False def _download_voice(self, voice: Voice, download_even_if_exists: bool = False) -> tuple[str, str]: 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')), }, 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 ('', '') def download_voice(self, v: Voice) -> None: