wyoming-piper 1.3.1__tar.gz → 1.5.3__tar.gz

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.
Files changed (28) hide show
  1. wyoming_piper-1.5.3/LICENSE.md +21 -0
  2. wyoming_piper-1.5.3/PKG-INFO +73 -0
  3. wyoming_piper-1.5.3/README.md +39 -0
  4. wyoming_piper-1.5.3/pyproject.toml +69 -0
  5. wyoming_piper-1.5.3/setup.cfg +21 -0
  6. wyoming_piper-1.5.3/tests/test_piper.py +123 -0
  7. wyoming_piper-1.5.3/wyoming_piper/__init__.py +6 -0
  8. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/__main__.py +101 -29
  9. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/download.py +61 -39
  10. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/handler.py +10 -0
  11. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/voices.json +2687 -188
  12. wyoming_piper-1.5.3/wyoming_piper.egg-info/PKG-INFO +73 -0
  13. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper.egg-info/SOURCES.txt +6 -3
  14. wyoming_piper-1.5.3/wyoming_piper.egg-info/entry_points.txt +2 -0
  15. wyoming_piper-1.5.3/wyoming_piper.egg-info/requires.txt +14 -0
  16. wyoming_piper-1.3.1/MANIFEST.in +0 -2
  17. wyoming_piper-1.3.1/PKG-INFO +0 -19
  18. wyoming_piper-1.3.1/requirements.txt +0 -1
  19. wyoming_piper-1.3.1/setup.cfg +0 -4
  20. wyoming_piper-1.3.1/setup.py +0 -44
  21. wyoming_piper-1.3.1/wyoming_piper/__init__.py +0 -1
  22. wyoming_piper-1.3.1/wyoming_piper.egg-info/PKG-INFO +0 -19
  23. wyoming_piper-1.3.1/wyoming_piper.egg-info/requires.txt +0 -1
  24. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/const.py +0 -0
  25. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/file_hash.py +0 -0
  26. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/process.py +0 -0
  27. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper.egg-info/dependency_links.txt +0 -0
  28. {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Michael Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: wyoming-piper
3
+ Version: 1.5.3
4
+ Summary: Wyoming Server for Piper
5
+ Author-email: Michael Hansen <mike@rhasspy.org>
6
+ License: MIT
7
+ Project-URL: Homepage, http://github.com/rhasspy/wyoming-piper
8
+ Keywords: rhasspy,wyoming,piper,tts
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Text Processing :: Linguistic
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: <3.13,>=3.8.1
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE.md
20
+ Requires-Dist: wyoming>=1.5.3
21
+ Provides-Extra: dev
22
+ Requires-Dist: black==22.12.0; extra == "dev"
23
+ Requires-Dist: flake8==6.0.0; extra == "dev"
24
+ Requires-Dist: isort==5.11.3; extra == "dev"
25
+ Requires-Dist: mypy==0.991; extra == "dev"
26
+ Requires-Dist: pylint==2.15.9; extra == "dev"
27
+ Requires-Dist: pytest==7.4.4; extra == "dev"
28
+ Requires-Dist: pytest-asyncio==0.23.3; extra == "dev"
29
+ Requires-Dist: build==1.2.2.post1; extra == "dev"
30
+ Requires-Dist: scipy<2,>=1.10; extra == "dev"
31
+ Requires-Dist: numpy<2,>=1.20; extra == "dev"
32
+ Requires-Dist: python-speech-features==0.6; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Wyoming Piper
36
+
37
+ [Wyoming protocol](https://github.com/rhasspy/wyoming) server for the [Piper](https://github.com/rhasspy/piper/) text to speech system.
38
+
39
+ ## Home Assistant Add-on
40
+
41
+ [![Show add-on](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=core_piper)
42
+
43
+ [Source](https://github.com/home-assistant/addons/tree/master/piper)
44
+
45
+ ## Local Install
46
+
47
+ Clone the repository and set up Python virtual environment:
48
+
49
+ ``` sh
50
+ git clone https://github.com/rhasspy/wyoming-piper.git
51
+ cd wyoming-piper
52
+ script/setup
53
+ ```
54
+
55
+ Install Piper
56
+ ```sh
57
+ curl -L -s "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz" | tar -zxvf - -C /usr/share
58
+ ```
59
+
60
+ Run a server that anyone can connect to:
61
+
62
+ ``` sh
63
+ script/run --piper '/usr/share/piper/piper' --voice en_US-lessac-medium --uri 'tcp://0.0.0.0:10200' --data-dir /data --download-dir /data
64
+ ```
65
+
66
+ ## Docker Image
67
+
68
+ ``` sh
69
+ docker run -it -p 10200:10200 -v /path/to/local/data:/data rhasspy/wyoming-piper \
70
+ --voice en_US-lessac-medium
71
+ ```
72
+
73
+ [Source](https://github.com/rhasspy/wyoming-addons/tree/master/piper)
@@ -0,0 +1,39 @@
1
+ # Wyoming Piper
2
+
3
+ [Wyoming protocol](https://github.com/rhasspy/wyoming) server for the [Piper](https://github.com/rhasspy/piper/) text to speech system.
4
+
5
+ ## Home Assistant Add-on
6
+
7
+ [![Show add-on](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=core_piper)
8
+
9
+ [Source](https://github.com/home-assistant/addons/tree/master/piper)
10
+
11
+ ## Local Install
12
+
13
+ Clone the repository and set up Python virtual environment:
14
+
15
+ ``` sh
16
+ git clone https://github.com/rhasspy/wyoming-piper.git
17
+ cd wyoming-piper
18
+ script/setup
19
+ ```
20
+
21
+ Install Piper
22
+ ```sh
23
+ curl -L -s "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz" | tar -zxvf - -C /usr/share
24
+ ```
25
+
26
+ Run a server that anyone can connect to:
27
+
28
+ ``` sh
29
+ script/run --piper '/usr/share/piper/piper' --voice en_US-lessac-medium --uri 'tcp://0.0.0.0:10200' --data-dir /data --download-dir /data
30
+ ```
31
+
32
+ ## Docker Image
33
+
34
+ ``` sh
35
+ docker run -it -p 10200:10200 -v /path/to/local/data:/data rhasspy/wyoming-piper \
36
+ --voice en_US-lessac-medium
37
+ ```
38
+
39
+ [Source](https://github.com/rhasspy/wyoming-addons/tree/master/piper)
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "wyoming-piper"
3
+ version = "1.5.3"
4
+ description = "Wyoming Server for Piper"
5
+ readme = "README.md"
6
+ requires-python = ">=3.8.1,<3.13"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Michael Hansen", email = "mike@rhasspy.org"}
10
+ ]
11
+ keywords = ["rhasspy", "wyoming", "piper", "tts"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Topic :: Text Processing :: Linguistic",
16
+ "Programming Language :: Python :: 3.8",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ ]
22
+ dependencies = [
23
+ "wyoming>=1.5.3",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "http://github.com/rhasspy/wyoming-piper"
28
+
29
+ [project.scripts]
30
+ wyoming-piper = "wyoming_piper.__main__:run"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["wyoming_piper"]
34
+ exclude = ["tests", "tests.*"]
35
+
36
+ [tool.setuptools.package-data]
37
+ wyoming_piper = ["voices.json"]
38
+
39
+ [build-system]
40
+ requires = ["setuptools>=42", "wheel"]
41
+ build-backend = "setuptools.build_meta"
42
+
43
+ [tool.black]
44
+ line-length = 88
45
+
46
+ [tool.isort]
47
+ profile = "black"
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
51
+
52
+ [tool.mypy]
53
+ check_untyped_defs = true
54
+ disallow_untyped_defs = true
55
+
56
+ [project.optional-dependencies]
57
+ dev = [
58
+ "black==22.12.0",
59
+ "flake8==6.0.0",
60
+ "isort==5.11.3",
61
+ "mypy==0.991",
62
+ "pylint==2.15.9",
63
+ "pytest==7.4.4",
64
+ "pytest-asyncio==0.23.3",
65
+ "build==1.2.2.post1",
66
+ "scipy>=1.10,<2",
67
+ "numpy>=1.20,<2",
68
+ "python-speech-features==0.6",
69
+ ]
@@ -0,0 +1,21 @@
1
+ [flake8]
2
+ max-line-length = 88
3
+ ignore =
4
+ E501,
5
+ W503,
6
+ E203,
7
+ D202,
8
+ W504
9
+
10
+ [isort]
11
+ multi_line_output = 3
12
+ include_trailing_comma = True
13
+ force_grid_wrap = 0
14
+ use_parentheses = True
15
+ line_length = 88
16
+ indent = " "
17
+
18
+ [egg_info]
19
+ tag_build =
20
+ tag_date = 0
21
+
@@ -0,0 +1,123 @@
1
+ """Tests for wyoming-piper"""
2
+ import asyncio
3
+ import sys
4
+ import tarfile
5
+ import wave
6
+ from asyncio.subprocess import PIPE
7
+ from pathlib import Path
8
+ from urllib.request import urlopen
9
+
10
+ import numpy as np
11
+ import pytest
12
+ import python_speech_features
13
+ from wyoming.audio import AudioChunk, AudioStart, AudioStop
14
+ from wyoming.event import async_read_event, async_write_event
15
+ from wyoming.info import Describe, Info
16
+ from wyoming.tts import Synthesize, SynthesizeVoice
17
+
18
+ from .dtw import compute_optimal_path
19
+
20
+ _DIR = Path(__file__).parent
21
+ _LOCAL_DIR = _DIR.parent / "local"
22
+ _PIPER_URL = (
23
+ "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz"
24
+ )
25
+ _TIMEOUT = 60
26
+
27
+
28
+ def download_piper() -> None:
29
+ """Downloads a binary version of Piper."""
30
+ piper_path = _LOCAL_DIR / "piper"
31
+ if piper_path.exists():
32
+ return
33
+
34
+ _LOCAL_DIR.mkdir(parents=True, exist_ok=True)
35
+ with urlopen(_PIPER_URL) as response:
36
+ with tarfile.open(fileobj=response, mode="r|*") as piper_file:
37
+ piper_file.extractall(_LOCAL_DIR)
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_piper() -> None:
42
+ download_piper()
43
+
44
+ proc = await asyncio.create_subprocess_exec(
45
+ sys.executable,
46
+ "-m",
47
+ "wyoming_piper",
48
+ "--uri",
49
+ "stdio://",
50
+ "--piper",
51
+ str(_LOCAL_DIR / "piper" / "piper"),
52
+ "--voice",
53
+ "en_US-ryan-low",
54
+ "--data-dir",
55
+ str(_LOCAL_DIR),
56
+ stdin=PIPE,
57
+ stdout=PIPE,
58
+ )
59
+ assert proc.stdin is not None
60
+ assert proc.stdout is not None
61
+
62
+ # Check info
63
+ await async_write_event(Describe().event(), proc.stdin)
64
+ while True:
65
+ event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
66
+ assert event is not None
67
+
68
+ if not Info.is_type(event.type):
69
+ continue
70
+
71
+ info = Info.from_event(event)
72
+ assert len(info.tts) == 1, "Expected one tts service"
73
+ tts = info.tts[0]
74
+ assert len(tts.voices) > 0, "Expected at least one voice"
75
+ voice_model = next((v for v in tts.voices if v.name == "en_US-ryan-low"), None)
76
+ assert voice_model is not None, "Expected ryan voice"
77
+ break
78
+
79
+ # Synthesize text
80
+ await async_write_event(
81
+ Synthesize("This is a test.", voice=SynthesizeVoice("en_US-ryan-low")).event(),
82
+ proc.stdin,
83
+ )
84
+
85
+ event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
86
+ assert event is not None
87
+ assert AudioStart.is_type(event.type)
88
+ audio_start = AudioStart.from_event(event)
89
+
90
+ with wave.open(str(_DIR / "this_is_a_test.wav"), "rb") as wav_file:
91
+ assert audio_start.rate == wav_file.getframerate()
92
+ assert audio_start.width == wav_file.getsampwidth()
93
+ assert audio_start.channels == wav_file.getnchannels()
94
+ expected_audio = wav_file.readframes(wav_file.getnframes())
95
+ expected_array = np.frombuffer(expected_audio, dtype=np.int16)
96
+
97
+ actual_audio = bytes()
98
+ while True:
99
+ event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
100
+ assert event is not None
101
+ if AudioStop.is_type(event.type):
102
+ break
103
+
104
+ if AudioChunk.is_type(event.type):
105
+ chunk = AudioChunk.from_event(event)
106
+ assert chunk.rate == audio_start.rate
107
+ assert chunk.width == audio_start.width
108
+ assert chunk.channels == audio_start.channels
109
+ actual_audio += chunk.audio
110
+
111
+ actual_array = np.frombuffer(actual_audio, dtype=np.int16)
112
+
113
+ # Less than 20% difference in length
114
+ assert (
115
+ abs(len(actual_array) - len(expected_array))
116
+ / max(len(actual_array), len(expected_array))
117
+ < 0.2
118
+ )
119
+
120
+ # Compute dynamic time warping (DTW) distance of MFCC features
121
+ expected_mfcc = python_speech_features.mfcc(expected_array, winstep=0.02)
122
+ actual_mfcc = python_speech_features.mfcc(actual_array, winstep=0.02)
123
+ assert compute_optimal_path(actual_mfcc, expected_mfcc) < 10
@@ -0,0 +1,6 @@
1
+ """Wyoming server for piper."""
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("wyoming_piper")
5
+
6
+ __all__ = ["__version__"]
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env python3
2
2
  import argparse
3
3
  import asyncio
4
+ import json
4
5
  import logging
5
6
  from functools import partial
6
- from typing import Any, Dict
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Set
7
9
 
8
10
  from wyoming.info import Attribution, Info, TtsProgram, TtsVoice, TtsVoiceSpeaker
9
11
  from wyoming.server import AsyncServer
10
12
 
11
- from .download import get_voices
13
+ from . import __version__
14
+ from .download import find_voice, get_voices
12
15
  from .handler import PiperEventHandler
13
16
  from .process import PiperProcessManager
14
17
 
@@ -37,8 +40,7 @@ async def main() -> None:
37
40
  )
38
41
  parser.add_argument(
39
42
  "--download-dir",
40
- required=True,
41
- help="Directory to download voices into",
43
+ help="Directory to download voices into (default: first data dir)",
42
44
  )
43
45
  #
44
46
  parser.add_argument(
@@ -66,9 +68,25 @@ async def main() -> None:
66
68
  )
67
69
  #
68
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
+ )
69
80
  args = parser.parse_args()
70
81
 
71
- logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
82
+ if not args.download_dir:
83
+ # Default to first data directory
84
+ args.download_dir = args.data_dir[0]
85
+
86
+ logging.basicConfig(
87
+ level=logging.DEBUG if args.debug else logging.INFO, format=args.log_format
88
+ )
89
+ _LOGGER.debug(args)
72
90
 
73
91
  # Load voice info
74
92
  voices_info = get_voices(args.download_dir, update_voices=args.update_voices)
@@ -80,6 +98,76 @@ async def main() -> None:
80
98
  aliases_info[voice_alias] = {"_is_alias": True, **voice_info}
81
99
 
82
100
  voices_info.update(aliases_info)
101
+ voices = [
102
+ TtsVoice(
103
+ name=voice_name,
104
+ description=get_description(voice_info),
105
+ attribution=Attribution(
106
+ name="rhasspy", url="https://github.com/rhasspy/piper"
107
+ ),
108
+ installed=True,
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,
122
+ )
123
+ for voice_name, voice_info in voices_info.items()
124
+ if not voice_info.get("_is_alias", False)
125
+ ]
126
+
127
+ custom_voice_names: Set[str] = set()
128
+ if args.voice not in voices_info:
129
+ custom_voice_names.add(args.voice)
130
+
131
+ for data_dir in args.data_dir:
132
+ data_dir = Path(data_dir)
133
+ if not data_dir.is_dir():
134
+ continue
135
+
136
+ for onnx_path in data_dir.glob("*.onnx"):
137
+ custom_voice_name = onnx_path.stem
138
+ if custom_voice_name not in voices_info:
139
+ custom_voice_names.add(custom_voice_name)
140
+
141
+ for custom_voice_name in custom_voice_names:
142
+ # Add custom voice info
143
+ custom_voice_path, custom_config_path = find_voice(
144
+ custom_voice_name, args.data_dir
145
+ )
146
+ with open(custom_config_path, "r", encoding="utf-8") as custom_config_file:
147
+ custom_config = json.load(custom_config_file)
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
+
161
+ voices.append(
162
+ TtsVoice(
163
+ name=custom_name,
164
+ description=description,
165
+ version=None,
166
+ attribution=Attribution(name="", url=""),
167
+ installed=True,
168
+ languages=[lang_code],
169
+ )
170
+ )
83
171
 
84
172
  wyoming_info = Info(
85
173
  tts=[
@@ -90,29 +178,8 @@ async def main() -> None:
90
178
  name="rhasspy", url="https://github.com/rhasspy/piper"
91
179
  ),
92
180
  installed=True,
93
- voices=[
94
- TtsVoice(
95
- name=voice_name,
96
- description=get_description(voice_info),
97
- attribution=Attribution(
98
- name="rhasspy", url="https://github.com/rhasspy/piper"
99
- ),
100
- installed=True,
101
- languages=[voice_info["language"]["code"]],
102
- #
103
- # Don't send speakers for now because it overflows StreamReader buffers
104
- # speakers=[
105
- # TtsVoiceSpeaker(name=speaker_name)
106
- # for speaker_name in voice_info["speaker_id_map"]
107
- # ]
108
- # if voice_info.get("speaker_id_map")
109
- # else None,
110
- )
111
- for voice_name, voice_info in sorted(
112
- voices_info.items(), key=lambda kv: kv[0]
113
- )
114
- if not voice_info.get("_is_alias", False)
115
- ],
181
+ voices=sorted(voices, key=lambda v: v.name),
182
+ version=__version__,
116
183
  )
117
184
  ],
118
185
  )
@@ -151,8 +218,13 @@ def get_description(voice_info: Dict[str, Any]):
151
218
 
152
219
  # -----------------------------------------------------------------------------
153
220
 
221
+
222
+ def run():
223
+ asyncio.run(main())
224
+
225
+
154
226
  if __name__ == "__main__":
155
227
  try:
156
- asyncio.run(main())
228
+ run()
157
229
  except KeyboardInterrupt:
158
230
  pass