Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/mush42/piper-nvda
Browse files Browse the repository at this point in the history
  • Loading branch information
mush42 committed Oct 22, 2023
2 parents e36f4e9 + e833caa commit af78d99
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 35 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-ast
- id: check-case-conflict
- id: check-yaml
29 changes: 0 additions & 29 deletions 2.0-beta.json

This file was deleted.

2 changes: 1 addition & 1 deletion addon/globalPlugins/piper_voices_globalPlugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs):
# Translators: label of a menu item
_("Piper voice &manager..."),
# Translators: Piper's voice manager menu item help
_("Open the voice manager to preview and download piper voices"),
_("Open the voice manager to preview, install or download piper voices"),
)
gui.mainFrame.sysTrayIcon.menu.Bind(wx.EVT_MENU, self.on_manager, self.itemHandle)

Expand Down
66 changes: 62 additions & 4 deletions addon/globalPlugins/piper_voices_globalPlugin/voice_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
import threading

import wx
from wx.adv import CommandLinkButton
import gui
import nvwave
import synthDriverHandler
from logHandler import log

from . import PiperTextToSpeechSystem
from . import PiperTextToSpeechSystem, PIPER_VOICES_DIR
from . import voice_download
from . import helpers
from .components import AsyncSnakDialog, ColumnDefn, ImmutableObjectListView, SimpleDialog, make_sized_static_box
Expand All @@ -35,7 +36,7 @@ def __init__(self, parent):
super().__init__(parent, -1)
self.__already_populated = threading.Event()
# Add controls
# Translators: lable for a list of installed voices
# Translators: label for a list of installed voices
voices_label = wx.StaticText(self, -1, _("Installed voices"))
self.voices_list = ImmutableObjectListView(
self,
Expand All @@ -53,12 +54,25 @@ def __init__(self, parent):
)
self.buttons_panel = SizedPanel(self, -1)
self.buttons_panel.SetSizerType("horizontal")
# Translators: lable for a button for showing voice model card
# Translators: label for a button for showing voice model card
self.model_card_button = wx.Button(self.buttons_panel, -1, _("&Voice model card..."))
# Translators: lable for a button for removing a voice
# Translators: label for a button for removing a voice
self.remove_voice_button = wx.Button(self.buttons_panel, -1, _("&Remove voice..."))
add_voice_button = CommandLinkButton(
self,
-1,
# Translators: the main label of the install button
_("Install from local file"),
# Translators: the note for this button
_(
"Install a voice from a local archive.\n"
"The archive contains the voice model and configuration.\n"
"The archive should have a (.tar.gz) file extension."
)
)
self.Bind(wx.EVT_BUTTON, self.on_model_card, self.model_card_button)
self.Bind(wx.EVT_BUTTON, self.on_remove_voice, self.remove_voice_button)
self.Bind(wx.EVT_BUTTON, self._on_install_voice_from_tar, add_voice_button)

def update_voices_list(self, set_focus=False, invalidate_synth_voices_cache=False):
voices = list(PiperTextToSpeechSystem.load_piper_voices_from_nvda_config_dir())
Expand Down Expand Up @@ -162,6 +176,50 @@ def on_remove_voice(self, event):
)
self.update_voices_list(set_focus=True, invalidate_synth_voices_cache=True)

def _on_install_voice_from_tar(self, event):
openFileDialog = wx.FileDialog(
parent=gui.mainFrame,
# Translators: title for a dialog for opening a file
message=_("Choose voice archive file "),
defaultDir=wx.GetUserHome(),
wildcard="Tar archives *.tar.gz | *.tar.gz",
style=wx.FD_OPEN,
)
gui.runScriptModalDialog(
openFileDialog,
functools.partial(self._get_process_tar_archive, openFileDialog),
)

def _get_process_tar_archive(self, dialog, res):
if res != wx.ID_OK:
return
filepath = dialog.GetPath().strip()
if not filepath:
return
try:
voice_key = PiperTextToSpeechSystem.install_voice(
filepath, PIPER_VOICES_DIR
)
except:
log.error("Failed to install voice from archive", exc_info=True)
gui.messageBox(
# Translators: message telling the user that installing the voice has failed
_(
"Failed to install voice from archive. See NVDA's log for more details."
),
_("Voice installation failed"),
style=wx.ICON_ERROR,
)
else:
gui.messageBox(
# Translators: message telling the user that installing the voice is successful
_(
"Voice {voice} has been installed successfully."
).format(voice=voice_key),
_("Voice installed successfully"),
style=wx.ICON_INFORMATION,
)


class OnlinePiperVoicesPanel(SizedPanel):
def __init__(self, parent):
Expand Down
40 changes: 40 additions & 0 deletions addon/synthDrivers/piper_neural_voices/tts_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import io
import operator
import os
import re
import string
import sys
import tarfile
import typing
import wave
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -223,6 +225,10 @@ async def speak_text(self, text):


class PiperTextToSpeechSystem:

VOICE_NAME_REGEX = re.compile( r"voice(-|_)(?P<language>[a-z]+[_]?([a-z]+)?)(-|_)(?P<name>[a-z]+)(-|_)(?P<quality>(high|medium|low|x-low))"
)

def __init__(
self, voices: Sequence[PiperVoice], speech_options: SpeechOptions = None
):
Expand Down Expand Up @@ -362,3 +368,37 @@ def load_voices_from_directory(
continue
rv.append(voice)
return rv

@classmethod
def install_voice(cls, voice_archive_path, dest_dir):
"""Uniform handleing of voice tar archives."""
archive_path = Path(voice_archive_path)
voice_name = archive_path.name.rstrip("".join(archive_path.suffixes))
match = cls.VOICE_NAME_REGEX.match(voice_name)
if match is None:
raise ValueError(f"Invalid voice archive: `{archive_path}`")
info = match.groupdict()
language = info["language"]
name = info["name"]
quality = info["quality"]
voice_key = f"{language}-{name}-{quality}"
with tarfile.open(os.fspath(archive_path), "r:gz") as tar:
members = tar.getmembers()
try:
m_onnx_model = next(m for m in members if m.name.endswith(".onnx"))
m_model_config = next(
m for m in members if m.name.endswith(".onnx.json")
)
except StopIteration:
raise ValueError(f"Invalid voice archive: `{archive_path}`")
dst = Path(dest_dir).joinpath(voice_key)
dst.mkdir(parents=True, exist_ok=True)
tar.extract(m_onnx_model, path=os.fspath(dst), set_attrs=False)
tar.extract(m_model_config, path=os.fspath(dst), set_attrs=False)
try:
m_model_card = next(m for m in members if m.name.endswith("MODEL_CARD"))
except StopIteration:
pass
else:
tar.extract(m_model_card, path=os.fspath(dst), set_attrs=False)
return voice_key
2 changes: 1 addition & 1 deletion buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _(arg):
# Add-on update channel (default is None, denoting stable releases,
# and for development releases, use "dev".)
# Do not change unless you know what you are doing!
"addon_updateChannel": None,
"addon_updateChannel": "beta",
# Add-on license such as GPL 2
"addon_license": "GPL 2",
# URL for the license document the ad-on is licensed under
Expand Down

0 comments on commit af78d99

Please sign in to comment.