wyoming-piper 1.3.2__py3-none-any.whl → 1.5.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wyoming_piper/__init__.py +5 -0
- wyoming_piper/__main__.py +52 -17
- wyoming_piper/download.py +36 -18
- wyoming_piper/handler.py +10 -0
- wyoming_piper/voices.json +2687 -188
- wyoming_piper-1.5.3.dist-info/METADATA +73 -0
- wyoming_piper-1.5.3.dist-info/RECORD +14 -0
- {wyoming_piper-1.3.2.dist-info → wyoming_piper-1.5.3.dist-info}/WHEEL +1 -1
- wyoming_piper-1.5.3.dist-info/entry_points.txt +2 -0
- wyoming_piper-1.5.3.dist-info/licenses/LICENSE.md +21 -0
- wyoming_piper-1.3.2.dist-info/METADATA +0 -19
- wyoming_piper-1.3.2.dist-info/RECORD +0 -12
- {wyoming_piper-1.3.2.dist-info → wyoming_piper-1.5.3.dist-info}/top_level.txt +0 -0
wyoming_piper/__init__.py
CHANGED
wyoming_piper/__main__.py
CHANGED
|
@@ -7,9 +7,10 @@ from functools import partial
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Dict, Set
|
|
9
9
|
|
|
10
|
-
from wyoming.info import Attribution, Info, TtsProgram, TtsVoice
|
|
10
|
+
from wyoming.info import Attribution, Info, TtsProgram, TtsVoice, TtsVoiceSpeaker
|
|
11
11
|
from wyoming.server import AsyncServer
|
|
12
12
|
|
|
13
|
+
from . import __version__
|
|
13
14
|
from .download import find_voice, get_voices
|
|
14
15
|
from .handler import PiperEventHandler
|
|
15
16
|
from .process import PiperProcessManager
|
|
@@ -67,13 +68,25 @@ async def main() -> None:
|
|
|
67
68
|
)
|
|
68
69
|
#
|
|
69
70
|
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--log-format", default=logging.BASIC_FORMAT, help="Format for log messages"
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--version",
|
|
76
|
+
action="version",
|
|
77
|
+
version=__version__,
|
|
78
|
+
help="Print version and exit",
|
|
79
|
+
)
|
|
70
80
|
args = parser.parse_args()
|
|
71
81
|
|
|
72
82
|
if not args.download_dir:
|
|
73
83
|
# Default to first data directory
|
|
74
84
|
args.download_dir = args.data_dir[0]
|
|
75
85
|
|
|
76
|
-
logging.basicConfig(
|
|
86
|
+
logging.basicConfig(
|
|
87
|
+
level=logging.DEBUG if args.debug else logging.INFO, format=args.log_format
|
|
88
|
+
)
|
|
89
|
+
_LOGGER.debug(args)
|
|
77
90
|
|
|
78
91
|
# Load voice info
|
|
79
92
|
voices_info = get_voices(args.download_dir, update_voices=args.update_voices)
|
|
@@ -93,15 +106,19 @@ async def main() -> None:
|
|
|
93
106
|
name="rhasspy", url="https://github.com/rhasspy/piper"
|
|
94
107
|
),
|
|
95
108
|
installed=True,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
version=None,
|
|
110
|
+
languages=[
|
|
111
|
+
voice_info.get("language", {}).get(
|
|
112
|
+
"code",
|
|
113
|
+
voice_info.get("espeak", {}).get("voice", voice_name.split("_")[0]),
|
|
114
|
+
)
|
|
115
|
+
],
|
|
116
|
+
speakers=[
|
|
117
|
+
TtsVoiceSpeaker(name=speaker_name)
|
|
118
|
+
for speaker_name in voice_info["speaker_id_map"]
|
|
119
|
+
]
|
|
120
|
+
if voice_info.get("speaker_id_map")
|
|
121
|
+
else None,
|
|
105
122
|
)
|
|
106
123
|
for voice_name, voice_info in voices_info.items()
|
|
107
124
|
if not voice_info.get("_is_alias", False)
|
|
@@ -123,20 +140,32 @@ async def main() -> None:
|
|
|
123
140
|
|
|
124
141
|
for custom_voice_name in custom_voice_names:
|
|
125
142
|
# Add custom voice info
|
|
126
|
-
|
|
143
|
+
custom_voice_path, custom_config_path = find_voice(
|
|
127
144
|
custom_voice_name, args.data_dir
|
|
128
145
|
)
|
|
129
146
|
with open(custom_config_path, "r", encoding="utf-8") as custom_config_file:
|
|
130
147
|
custom_config = json.load(custom_config_file)
|
|
131
|
-
custom_name = custom_config
|
|
132
|
-
custom_quality = custom_config
|
|
148
|
+
custom_name = custom_config.get("dataset", custom_voice_path.stem)
|
|
149
|
+
custom_quality = custom_config.get("audio", {}).get("quality")
|
|
150
|
+
if custom_quality:
|
|
151
|
+
description = f"{custom_name} ({custom_quality})"
|
|
152
|
+
else:
|
|
153
|
+
description = custom_name
|
|
154
|
+
|
|
155
|
+
lang_code = custom_config.get("language", {}).get("code")
|
|
156
|
+
if not lang_code:
|
|
157
|
+
lang_code = custom_config.get("espeak", {}).get("voice")
|
|
158
|
+
if not lang_code:
|
|
159
|
+
lang_code = custom_voice_path.stem.split("_")[0]
|
|
160
|
+
|
|
133
161
|
voices.append(
|
|
134
162
|
TtsVoice(
|
|
135
163
|
name=custom_name,
|
|
136
|
-
description=
|
|
164
|
+
description=description,
|
|
165
|
+
version=None,
|
|
137
166
|
attribution=Attribution(name="", url=""),
|
|
138
167
|
installed=True,
|
|
139
|
-
languages=[
|
|
168
|
+
languages=[lang_code],
|
|
140
169
|
)
|
|
141
170
|
)
|
|
142
171
|
|
|
@@ -150,6 +179,7 @@ async def main() -> None:
|
|
|
150
179
|
),
|
|
151
180
|
installed=True,
|
|
152
181
|
voices=sorted(voices, key=lambda v: v.name),
|
|
182
|
+
version=__version__,
|
|
153
183
|
)
|
|
154
184
|
],
|
|
155
185
|
)
|
|
@@ -188,8 +218,13 @@ def get_description(voice_info: Dict[str, Any]):
|
|
|
188
218
|
|
|
189
219
|
# -----------------------------------------------------------------------------
|
|
190
220
|
|
|
221
|
+
|
|
222
|
+
def run():
|
|
223
|
+
asyncio.run(main())
|
|
224
|
+
|
|
225
|
+
|
|
191
226
|
if __name__ == "__main__":
|
|
192
227
|
try:
|
|
193
|
-
|
|
228
|
+
run()
|
|
194
229
|
except KeyboardInterrupt:
|
|
195
230
|
pass
|
wyoming_piper/download.py
CHANGED
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
import shutil
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Dict, Iterable, Set, Tuple, Union
|
|
7
|
+
from urllib.error import URLError
|
|
8
|
+
from urllib.parse import quote, urlsplit, urlunsplit
|
|
7
9
|
from urllib.request import urlopen
|
|
8
10
|
|
|
9
11
|
from .file_hash import get_file_hash
|
|
@@ -20,6 +22,13 @@ class VoiceNotFoundError(Exception):
|
|
|
20
22
|
pass
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
def _quote_url(url: str) -> str:
|
|
26
|
+
"""Quote file part of URL in case it contains UTF-8 characters."""
|
|
27
|
+
parts = list(urlsplit(url))
|
|
28
|
+
parts[2] = quote(parts[2])
|
|
29
|
+
return urlunsplit(parts)
|
|
30
|
+
|
|
31
|
+
|
|
23
32
|
def get_voices(
|
|
24
33
|
download_dir: Union[str, Path], update_voices: bool = False
|
|
25
34
|
) -> Dict[str, Any]:
|
|
@@ -32,7 +41,7 @@ def get_voices(
|
|
|
32
41
|
try:
|
|
33
42
|
voices_url = URL_FORMAT.format(file="voices.json")
|
|
34
43
|
_LOGGER.debug("Downloading %s to %s", voices_url, voices_download)
|
|
35
|
-
with urlopen(voices_url) as response:
|
|
44
|
+
with urlopen(_quote_url(voices_url)) as response:
|
|
36
45
|
with open(voices_download, "wb") as download_file:
|
|
37
46
|
shutil.copyfileobj(response, download_file)
|
|
38
47
|
except Exception:
|
|
@@ -72,6 +81,7 @@ def ensure_voice_exists(
|
|
|
72
81
|
|
|
73
82
|
voice_info = voices_info[name]
|
|
74
83
|
voice_files = voice_info["files"]
|
|
84
|
+
verified_files: Set[str] = set()
|
|
75
85
|
files_to_download: Set[str] = set()
|
|
76
86
|
|
|
77
87
|
for data_dir in data_dirs:
|
|
@@ -79,8 +89,8 @@ def ensure_voice_exists(
|
|
|
79
89
|
|
|
80
90
|
# Check sizes/hashes
|
|
81
91
|
for file_path, file_info in voice_files.items():
|
|
82
|
-
if file_path in
|
|
83
|
-
# Already
|
|
92
|
+
if file_path in verified_files:
|
|
93
|
+
# Already verified this file in a different data directory
|
|
84
94
|
continue
|
|
85
95
|
|
|
86
96
|
file_name = Path(file_path).name
|
|
@@ -118,28 +128,36 @@ def ensure_voice_exists(
|
|
|
118
128
|
files_to_download.add(file_path)
|
|
119
129
|
continue
|
|
120
130
|
|
|
131
|
+
# File exists and has been verified
|
|
132
|
+
verified_files.add(file_path)
|
|
133
|
+
files_to_download.discard(file_path)
|
|
134
|
+
|
|
121
135
|
if (not voice_files) and (not files_to_download):
|
|
122
136
|
raise ValueError(f"Unable to find or download voice: {name}")
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
try:
|
|
139
|
+
# Download missing or update to date files
|
|
140
|
+
download_dir = Path(download_dir)
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
for file_path in files_to_download:
|
|
143
|
+
file_name = Path(file_path).name
|
|
144
|
+
if file_name in _SKIP_FILES:
|
|
145
|
+
continue
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
file_url = URL_FORMAT.format(file=file_path)
|
|
148
|
+
download_file_path = download_dir / file_name
|
|
149
|
+
download_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
_LOGGER.debug("Downloading %s to %s", file_url, download_file_path)
|
|
152
|
+
with urlopen(_quote_url(file_url)) as response, open(
|
|
153
|
+
download_file_path, "wb"
|
|
154
|
+
) as download_file:
|
|
155
|
+
shutil.copyfileobj(response, download_file)
|
|
141
156
|
|
|
142
|
-
|
|
157
|
+
_LOGGER.info("Downloaded %s (%s)", download_file_path, file_url)
|
|
158
|
+
except URLError:
|
|
159
|
+
# find_voice will fail down the line
|
|
160
|
+
_LOGGER.exception("Unexpected error while downloading files for %s", name)
|
|
143
161
|
|
|
144
162
|
|
|
145
163
|
def find_voice(name: str, data_dirs: Iterable[Union[str, Path]]) -> Tuple[Path, Path]:
|
wyoming_piper/handler.py
CHANGED
|
@@ -8,6 +8,7 @@ import wave
|
|
|
8
8
|
from typing import Any, Dict, Optional
|
|
9
9
|
|
|
10
10
|
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
|
11
|
+
from wyoming.error import Error
|
|
11
12
|
from wyoming.event import Event
|
|
12
13
|
from wyoming.info import Describe, Info
|
|
13
14
|
from wyoming.server import AsyncEventHandler
|
|
@@ -43,6 +44,15 @@ class PiperEventHandler(AsyncEventHandler):
|
|
|
43
44
|
_LOGGER.warning("Unexpected event: %s", event)
|
|
44
45
|
return True
|
|
45
46
|
|
|
47
|
+
try:
|
|
48
|
+
return await self._handle_event(event)
|
|
49
|
+
except Exception as err:
|
|
50
|
+
await self.write_event(
|
|
51
|
+
Error(text=str(err), code=err.__class__.__name__).event()
|
|
52
|
+
)
|
|
53
|
+
raise err
|
|
54
|
+
|
|
55
|
+
async def _handle_event(self, event: Event) -> bool:
|
|
46
56
|
synthesize = Synthesize.from_event(event)
|
|
47
57
|
_LOGGER.debug(synthesize)
|
|
48
58
|
|