wyoming-piper 1.3.2__tar.gz → 1.6.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.
- wyoming_piper-1.6.3/LICENSE.md +21 -0
- wyoming_piper-1.6.3/PKG-INFO +75 -0
- wyoming_piper-1.6.3/README.md +39 -0
- wyoming_piper-1.6.3/pyproject.toml +71 -0
- wyoming_piper-1.6.3/setup.cfg +21 -0
- wyoming_piper-1.6.3/tests/test_piper.py +124 -0
- wyoming_piper-1.6.3/tests/test_sentence_boundary.py +61 -0
- wyoming_piper-1.6.3/wyoming_piper/__init__.py +7 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/__main__.py +60 -17
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/download.py +45 -25
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/handler.py +73 -5
- wyoming_piper-1.6.3/wyoming_piper/sentence_boundary.py +58 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/voices.json +2878 -199
- wyoming_piper-1.6.3/wyoming_piper.egg-info/PKG-INFO +75 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper.egg-info/SOURCES.txt +8 -3
- wyoming_piper-1.6.3/wyoming_piper.egg-info/entry_points.txt +2 -0
- wyoming_piper-1.6.3/wyoming_piper.egg-info/requires.txt +15 -0
- wyoming_piper-1.3.2/MANIFEST.in +0 -2
- wyoming_piper-1.3.2/PKG-INFO +0 -17
- wyoming_piper-1.3.2/requirements.txt +0 -1
- wyoming_piper-1.3.2/setup.cfg +0 -4
- wyoming_piper-1.3.2/setup.py +0 -44
- wyoming_piper-1.3.2/wyoming_piper/__init__.py +0 -1
- wyoming_piper-1.3.2/wyoming_piper.egg-info/PKG-INFO +0 -17
- wyoming_piper-1.3.2/wyoming_piper.egg-info/requires.txt +0 -1
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/const.py +0 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/file_hash.py +0 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/process.py +0 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper.egg-info/dependency_links.txt +0 -0
- {wyoming_piper-1.3.2 → wyoming_piper-1.6.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,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wyoming-piper
|
|
3
|
+
Version: 1.6.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
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.md
|
|
21
|
+
Requires-Dist: wyoming<1.8,>=1.7.2
|
|
22
|
+
Requires-Dist: regex==2024.11.6
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: black==22.12.0; extra == "dev"
|
|
25
|
+
Requires-Dist: flake8==6.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: isort==5.11.3; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy==0.991; extra == "dev"
|
|
28
|
+
Requires-Dist: pylint==2.15.9; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest==7.4.4; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio==0.23.3; extra == "dev"
|
|
31
|
+
Requires-Dist: build==1.2.2.post1; extra == "dev"
|
|
32
|
+
Requires-Dist: scipy<2,>=1.10; extra == "dev"
|
|
33
|
+
Requires-Dist: numpy<2,>=1.20; extra == "dev"
|
|
34
|
+
Requires-Dist: python-speech-features==0.6; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Wyoming Piper
|
|
38
|
+
|
|
39
|
+
[Wyoming protocol](https://github.com/rhasspy/wyoming) server for the [Piper](https://github.com/rhasspy/piper/) text to speech system.
|
|
40
|
+
|
|
41
|
+
## Home Assistant Add-on
|
|
42
|
+
|
|
43
|
+
[](https://my.home-assistant.io/redirect/supervisor_addon/?addon=core_piper)
|
|
44
|
+
|
|
45
|
+
[Source](https://github.com/home-assistant/addons/tree/master/piper)
|
|
46
|
+
|
|
47
|
+
## Local Install
|
|
48
|
+
|
|
49
|
+
Clone the repository and set up Python virtual environment:
|
|
50
|
+
|
|
51
|
+
``` sh
|
|
52
|
+
git clone https://github.com/rhasspy/wyoming-piper.git
|
|
53
|
+
cd wyoming-piper
|
|
54
|
+
script/setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Install Piper
|
|
58
|
+
```sh
|
|
59
|
+
curl -L -s "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz" | tar -zxvf - -C /usr/share
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run a server that anyone can connect to:
|
|
63
|
+
|
|
64
|
+
``` sh
|
|
65
|
+
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
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Docker Image
|
|
69
|
+
|
|
70
|
+
``` sh
|
|
71
|
+
docker run -it -p 10200:10200 -v /path/to/local/data:/data rhasspy/wyoming-piper \
|
|
72
|
+
--voice en_US-lessac-medium
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
[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
|
+
[](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,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wyoming-piper"
|
|
3
|
+
version = "1.6.3"
|
|
4
|
+
description = "Wyoming Server for Piper"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
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
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"wyoming>=1.7.2,<1.8",
|
|
25
|
+
"regex==2024.11.6",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "http://github.com/rhasspy/wyoming-piper"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
wyoming-piper = "wyoming_piper.__main__:run"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["wyoming_piper"]
|
|
36
|
+
exclude = ["tests", "tests.*"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
wyoming_piper = ["voices.json"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["setuptools>=42", "wheel"]
|
|
43
|
+
build-backend = "setuptools.build_meta"
|
|
44
|
+
|
|
45
|
+
[tool.black]
|
|
46
|
+
line-length = 88
|
|
47
|
+
|
|
48
|
+
[tool.isort]
|
|
49
|
+
profile = "black"
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
check_untyped_defs = true
|
|
56
|
+
disallow_untyped_defs = true
|
|
57
|
+
|
|
58
|
+
[project.optional-dependencies]
|
|
59
|
+
dev = [
|
|
60
|
+
"black==22.12.0",
|
|
61
|
+
"flake8==6.0.0",
|
|
62
|
+
"isort==5.11.3",
|
|
63
|
+
"mypy==0.991",
|
|
64
|
+
"pylint==2.15.9",
|
|
65
|
+
"pytest==7.4.4",
|
|
66
|
+
"pytest-asyncio==0.23.3",
|
|
67
|
+
"build==1.2.2.post1",
|
|
68
|
+
"scipy>=1.10,<2",
|
|
69
|
+
"numpy>=1.20,<2",
|
|
70
|
+
"python-speech-features==0.6",
|
|
71
|
+
]
|
|
@@ -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,124 @@
|
|
|
1
|
+
"""Tests for wyoming-piper"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import tarfile
|
|
6
|
+
import wave
|
|
7
|
+
from asyncio.subprocess import PIPE
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.request import urlopen
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pytest
|
|
13
|
+
import python_speech_features
|
|
14
|
+
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
|
15
|
+
from wyoming.event import async_read_event, async_write_event
|
|
16
|
+
from wyoming.info import Describe, Info
|
|
17
|
+
from wyoming.tts import Synthesize, SynthesizeVoice
|
|
18
|
+
|
|
19
|
+
from .dtw import compute_optimal_path
|
|
20
|
+
|
|
21
|
+
_DIR = Path(__file__).parent
|
|
22
|
+
_LOCAL_DIR = _DIR.parent / "local"
|
|
23
|
+
_PIPER_URL = (
|
|
24
|
+
"https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz"
|
|
25
|
+
)
|
|
26
|
+
_TIMEOUT = 60
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def download_piper() -> None:
|
|
30
|
+
"""Downloads a binary version of Piper."""
|
|
31
|
+
piper_path = _LOCAL_DIR / "piper"
|
|
32
|
+
if piper_path.exists():
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
_LOCAL_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with urlopen(_PIPER_URL) as response:
|
|
37
|
+
with tarfile.open(fileobj=response, mode="r|*") as piper_file:
|
|
38
|
+
piper_file.extractall(_LOCAL_DIR)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_piper() -> None:
|
|
43
|
+
download_piper()
|
|
44
|
+
|
|
45
|
+
proc = await asyncio.create_subprocess_exec(
|
|
46
|
+
sys.executable,
|
|
47
|
+
"-m",
|
|
48
|
+
"wyoming_piper",
|
|
49
|
+
"--uri",
|
|
50
|
+
"stdio://",
|
|
51
|
+
"--piper",
|
|
52
|
+
str(_LOCAL_DIR / "piper" / "piper"),
|
|
53
|
+
"--voice",
|
|
54
|
+
"en_US-ryan-low",
|
|
55
|
+
"--data-dir",
|
|
56
|
+
str(_LOCAL_DIR),
|
|
57
|
+
stdin=PIPE,
|
|
58
|
+
stdout=PIPE,
|
|
59
|
+
)
|
|
60
|
+
assert proc.stdin is not None
|
|
61
|
+
assert proc.stdout is not None
|
|
62
|
+
|
|
63
|
+
# Check info
|
|
64
|
+
await async_write_event(Describe().event(), proc.stdin)
|
|
65
|
+
while True:
|
|
66
|
+
event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
|
|
67
|
+
assert event is not None
|
|
68
|
+
|
|
69
|
+
if not Info.is_type(event.type):
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
info = Info.from_event(event)
|
|
73
|
+
assert len(info.tts) == 1, "Expected one tts service"
|
|
74
|
+
tts = info.tts[0]
|
|
75
|
+
assert len(tts.voices) > 0, "Expected at least one voice"
|
|
76
|
+
voice_model = next((v for v in tts.voices if v.name == "en_US-ryan-low"), None)
|
|
77
|
+
assert voice_model is not None, "Expected ryan voice"
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
# Synthesize text
|
|
81
|
+
await async_write_event(
|
|
82
|
+
Synthesize("This is a test.", voice=SynthesizeVoice("en_US-ryan-low")).event(),
|
|
83
|
+
proc.stdin,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
|
|
87
|
+
assert event is not None
|
|
88
|
+
assert AudioStart.is_type(event.type)
|
|
89
|
+
audio_start = AudioStart.from_event(event)
|
|
90
|
+
|
|
91
|
+
with wave.open(str(_DIR / "this_is_a_test.wav"), "rb") as wav_file:
|
|
92
|
+
assert audio_start.rate == wav_file.getframerate()
|
|
93
|
+
assert audio_start.width == wav_file.getsampwidth()
|
|
94
|
+
assert audio_start.channels == wav_file.getnchannels()
|
|
95
|
+
expected_audio = wav_file.readframes(wav_file.getnframes())
|
|
96
|
+
expected_array = np.frombuffer(expected_audio, dtype=np.int16)
|
|
97
|
+
|
|
98
|
+
actual_audio = bytes()
|
|
99
|
+
while True:
|
|
100
|
+
event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT)
|
|
101
|
+
assert event is not None
|
|
102
|
+
if AudioStop.is_type(event.type):
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
if AudioChunk.is_type(event.type):
|
|
106
|
+
chunk = AudioChunk.from_event(event)
|
|
107
|
+
assert chunk.rate == audio_start.rate
|
|
108
|
+
assert chunk.width == audio_start.width
|
|
109
|
+
assert chunk.channels == audio_start.channels
|
|
110
|
+
actual_audio += chunk.audio
|
|
111
|
+
|
|
112
|
+
actual_array = np.frombuffer(actual_audio, dtype=np.int16)
|
|
113
|
+
|
|
114
|
+
# Less than 20% difference in length
|
|
115
|
+
assert (
|
|
116
|
+
abs(len(actual_array) - len(expected_array))
|
|
117
|
+
/ max(len(actual_array), len(expected_array))
|
|
118
|
+
< 0.2
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Compute dynamic time warping (DTW) distance of MFCC features
|
|
122
|
+
expected_mfcc = python_speech_features.mfcc(expected_array, winstep=0.02)
|
|
123
|
+
actual_mfcc = python_speech_features.mfcc(actual_array, winstep=0.02)
|
|
124
|
+
assert compute_optimal_path(actual_mfcc, expected_mfcc) < 10
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Tests for sentence boundary detection."""
|
|
2
|
+
|
|
3
|
+
from wyoming_piper.sentence_boundary import SentenceBoundaryDetector, remove_asterisks
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_one_chunk() -> None:
|
|
7
|
+
sbd = SentenceBoundaryDetector()
|
|
8
|
+
assert not list(sbd.add_chunk("Test chunk"))
|
|
9
|
+
assert sbd.finish() == "Test chunk"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_one_chunk_with_punctuation() -> None:
|
|
13
|
+
sbd = SentenceBoundaryDetector()
|
|
14
|
+
assert list(sbd.add_chunk("Test chunk 1. Test chunk 2")) == ["Test chunk 1."]
|
|
15
|
+
assert sbd.finish() == "Test chunk 2"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_multiple_chunks() -> None:
|
|
19
|
+
sbd = SentenceBoundaryDetector()
|
|
20
|
+
assert not list(sbd.add_chunk("Test chunk"))
|
|
21
|
+
assert list(sbd.add_chunk(" 1. Test chunk")) == ["Test chunk 1."]
|
|
22
|
+
assert not list(sbd.add_chunk(" 2."))
|
|
23
|
+
assert sbd.finish() == "Test chunk 2."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_numbered_lists() -> None:
|
|
27
|
+
sbd = SentenceBoundaryDetector()
|
|
28
|
+
sentences = list(
|
|
29
|
+
sbd.add_chunk(
|
|
30
|
+
"Final Fantasy VII features several key characters who drive the narrative: "
|
|
31
|
+
"1. **Cloud Strife** - The protagonist, an ex-SOLDIER mercenary and a skilled fighter. "
|
|
32
|
+
"2. **Aerith Gainsborough (Aeris)** - A kindhearted flower seller with spiritual powers and deep connections to the planet's ecosystem. "
|
|
33
|
+
"3. **Barret Wallace** - A leader of eco-terrorists called AVALANCHE, fighting against Shinra Corporation's exploitation of the planet. "
|
|
34
|
+
"4. **Tifa Lockhart** - Cloud's childhood friend who runs a bar in Sector 7 and helps him recover from past trauma. "
|
|
35
|
+
"5. **Sephiroth** - The main antagonist, an ex-SOLDIER with god-like abilities, seeking to control or destroy the planet. "
|
|
36
|
+
"6. **Red XIII (aka Red 13)** - A member of a catlike race called Cetra, searching for answers about his heritage and destiny. "
|
|
37
|
+
"7. **Vincent Valentine** - A brooding former Turk who lives in isolation from guilt over past failures but aids Cloud's party with his powerful abilities. "
|
|
38
|
+
"8. **Cid Highwind** - The pilot of the rocket plane Highwind and a skilled engineer working on various airship projects. 9. "
|
|
39
|
+
"**Shinra Employees (JENOVA Project)** - Characters like Professor Hojo, President Shinra, and Reno who play crucial roles in the plot's development. "
|
|
40
|
+
"Each character brings unique skills and perspectives to the story, contributing to its rich narrative and gameplay dynamics."
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
assert len(sentences) == 9
|
|
44
|
+
assert sbd.finish().startswith("Each character")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_remove_word_asterisks() -> None:
|
|
48
|
+
sbd = SentenceBoundaryDetector()
|
|
49
|
+
assert list(
|
|
50
|
+
sbd.add_chunk(
|
|
51
|
+
"**Test** sentence with *emphasized* words! Another *** sentence."
|
|
52
|
+
)
|
|
53
|
+
) == ["Test sentence with emphasized words!"]
|
|
54
|
+
assert sbd.finish() == "Another *** sentence."
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_remove_line_asterisks() -> None:
|
|
58
|
+
assert (
|
|
59
|
+
remove_asterisks("* Test item 1.\n\n** Test item 2\n * Test item 3.")
|
|
60
|
+
== " Test item 1.\n\n Test item 2\n Test item 3."
|
|
61
|
+
)
|
|
@@ -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
|
|
@@ -59,6 +60,11 @@ async def main() -> None:
|
|
|
59
60
|
default=1,
|
|
60
61
|
help="Maximum number of piper process to run simultaneously (default: 1)",
|
|
61
62
|
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--streaming",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Enable audio streaming on sentence boundaries",
|
|
67
|
+
)
|
|
62
68
|
#
|
|
63
69
|
parser.add_argument(
|
|
64
70
|
"--update-voices",
|
|
@@ -67,13 +73,25 @@ async def main() -> None:
|
|
|
67
73
|
)
|
|
68
74
|
#
|
|
69
75
|
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--log-format", default=logging.BASIC_FORMAT, help="Format for log messages"
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--version",
|
|
81
|
+
action="version",
|
|
82
|
+
version=__version__,
|
|
83
|
+
help="Print version and exit",
|
|
84
|
+
)
|
|
70
85
|
args = parser.parse_args()
|
|
71
86
|
|
|
72
87
|
if not args.download_dir:
|
|
73
88
|
# Default to first data directory
|
|
74
89
|
args.download_dir = args.data_dir[0]
|
|
75
90
|
|
|
76
|
-
logging.basicConfig(
|
|
91
|
+
logging.basicConfig(
|
|
92
|
+
level=logging.DEBUG if args.debug else logging.INFO, format=args.log_format
|
|
93
|
+
)
|
|
94
|
+
_LOGGER.debug(args)
|
|
77
95
|
|
|
78
96
|
# Load voice info
|
|
79
97
|
voices_info = get_voices(args.download_dir, update_voices=args.update_voices)
|
|
@@ -93,15 +111,21 @@ async def main() -> None:
|
|
|
93
111
|
name="rhasspy", url="https://github.com/rhasspy/piper"
|
|
94
112
|
),
|
|
95
113
|
installed=True,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
version=None,
|
|
115
|
+
languages=[
|
|
116
|
+
voice_info.get("language", {}).get(
|
|
117
|
+
"code",
|
|
118
|
+
voice_info.get("espeak", {}).get("voice", voice_name.split("_")[0]),
|
|
119
|
+
)
|
|
120
|
+
],
|
|
121
|
+
speakers=(
|
|
122
|
+
[
|
|
123
|
+
TtsVoiceSpeaker(name=speaker_name)
|
|
124
|
+
for speaker_name in voice_info["speaker_id_map"]
|
|
125
|
+
]
|
|
126
|
+
if voice_info.get("speaker_id_map")
|
|
127
|
+
else None
|
|
128
|
+
),
|
|
105
129
|
)
|
|
106
130
|
for voice_name, voice_info in voices_info.items()
|
|
107
131
|
if not voice_info.get("_is_alias", False)
|
|
@@ -123,20 +147,32 @@ async def main() -> None:
|
|
|
123
147
|
|
|
124
148
|
for custom_voice_name in custom_voice_names:
|
|
125
149
|
# Add custom voice info
|
|
126
|
-
|
|
150
|
+
custom_voice_path, custom_config_path = find_voice(
|
|
127
151
|
custom_voice_name, args.data_dir
|
|
128
152
|
)
|
|
129
153
|
with open(custom_config_path, "r", encoding="utf-8") as custom_config_file:
|
|
130
154
|
custom_config = json.load(custom_config_file)
|
|
131
|
-
custom_name = custom_config
|
|
132
|
-
custom_quality = custom_config
|
|
155
|
+
custom_name = custom_config.get("dataset", custom_voice_path.stem)
|
|
156
|
+
custom_quality = custom_config.get("audio", {}).get("quality")
|
|
157
|
+
if custom_quality:
|
|
158
|
+
description = f"{custom_name} ({custom_quality})"
|
|
159
|
+
else:
|
|
160
|
+
description = custom_name
|
|
161
|
+
|
|
162
|
+
lang_code = custom_config.get("language", {}).get("code")
|
|
163
|
+
if not lang_code:
|
|
164
|
+
lang_code = custom_config.get("espeak", {}).get("voice")
|
|
165
|
+
if not lang_code:
|
|
166
|
+
lang_code = custom_voice_path.stem.split("_")[0]
|
|
167
|
+
|
|
133
168
|
voices.append(
|
|
134
169
|
TtsVoice(
|
|
135
170
|
name=custom_name,
|
|
136
|
-
description=
|
|
171
|
+
description=description,
|
|
172
|
+
version=None,
|
|
137
173
|
attribution=Attribution(name="", url=""),
|
|
138
174
|
installed=True,
|
|
139
|
-
languages=[
|
|
175
|
+
languages=[lang_code],
|
|
140
176
|
)
|
|
141
177
|
)
|
|
142
178
|
|
|
@@ -150,6 +186,8 @@ async def main() -> None:
|
|
|
150
186
|
),
|
|
151
187
|
installed=True,
|
|
152
188
|
voices=sorted(voices, key=lambda v: v.name),
|
|
189
|
+
version=__version__,
|
|
190
|
+
supports_synthesize_streaming=args.streaming,
|
|
153
191
|
)
|
|
154
192
|
],
|
|
155
193
|
)
|
|
@@ -188,8 +226,13 @@ def get_description(voice_info: Dict[str, Any]):
|
|
|
188
226
|
|
|
189
227
|
# -----------------------------------------------------------------------------
|
|
190
228
|
|
|
229
|
+
|
|
230
|
+
def run():
|
|
231
|
+
asyncio.run(main())
|
|
232
|
+
|
|
233
|
+
|
|
191
234
|
if __name__ == "__main__":
|
|
192
235
|
try:
|
|
193
|
-
|
|
236
|
+
run()
|
|
194
237
|
except KeyboardInterrupt:
|
|
195
238
|
pass
|