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 CHANGED
@@ -1 +1,6 @@
1
1
  """Wyoming server for piper."""
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("wyoming_piper")
5
+
6
+ __all__ = ["__version__"]
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(level=logging.DEBUG if args.debug else logging.INFO)
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
- languages=[voice_info["language"]["code"]],
97
- #
98
- # Don't send speakers for now because it overflows StreamReader buffers
99
- # speakers=[
100
- # TtsVoiceSpeaker(name=speaker_name)
101
- # for speaker_name in voice_info["speaker_id_map"]
102
- # ]
103
- # if voice_info.get("speaker_id_map")
104
- # else None,
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
- _custom_voice_path, custom_config_path = find_voice(
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["dataset"]
132
- custom_quality = custom_config["audio"]["quality"]
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=f"{custom_name} ({custom_quality})",
164
+ description=description,
165
+ version=None,
137
166
  attribution=Attribution(name="", url=""),
138
167
  installed=True,
139
- languages=[custom_config["language"]["code"]],
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
- asyncio.run(main())
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 files_to_download:
83
- # Already planning to download
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
- # Download missing files
125
- download_dir = Path(download_dir)
138
+ try:
139
+ # Download missing or update to date files
140
+ download_dir = Path(download_dir)
126
141
 
127
- for file_path in files_to_download:
128
- file_name = Path(file_path).name
129
- if file_name in _SKIP_FILES:
130
- continue
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
- file_url = URL_FORMAT.format(file=file_path)
133
- download_file_path = download_dir / file_name
134
- download_file_path.parent.mkdir(parents=True, exist_ok=True)
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
- _LOGGER.debug("Downloading %s to %s", file_url, download_file_path)
137
- with urlopen(file_url) as response, open(
138
- download_file_path, "wb"
139
- ) as download_file:
140
- shutil.copyfileobj(response, download_file)
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
- _LOGGER.info("Downloaded %s (%s)", download_file_path, file_url)
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