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.
- wyoming_piper-1.5.3/LICENSE.md +21 -0
- wyoming_piper-1.5.3/PKG-INFO +73 -0
- wyoming_piper-1.5.3/README.md +39 -0
- wyoming_piper-1.5.3/pyproject.toml +69 -0
- wyoming_piper-1.5.3/setup.cfg +21 -0
- wyoming_piper-1.5.3/tests/test_piper.py +123 -0
- wyoming_piper-1.5.3/wyoming_piper/__init__.py +6 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/__main__.py +101 -29
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/download.py +61 -39
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/handler.py +10 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/voices.json +2687 -188
- wyoming_piper-1.5.3/wyoming_piper.egg-info/PKG-INFO +73 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper.egg-info/SOURCES.txt +6 -3
- wyoming_piper-1.5.3/wyoming_piper.egg-info/entry_points.txt +2 -0
- wyoming_piper-1.5.3/wyoming_piper.egg-info/requires.txt +14 -0
- wyoming_piper-1.3.1/MANIFEST.in +0 -2
- wyoming_piper-1.3.1/PKG-INFO +0 -19
- wyoming_piper-1.3.1/requirements.txt +0 -1
- wyoming_piper-1.3.1/setup.cfg +0 -4
- wyoming_piper-1.3.1/setup.py +0 -44
- wyoming_piper-1.3.1/wyoming_piper/__init__.py +0 -1
- wyoming_piper-1.3.1/wyoming_piper.egg-info/PKG-INFO +0 -19
- wyoming_piper-1.3.1/wyoming_piper.egg-info/requires.txt +0 -1
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/const.py +0 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/file_hash.py +0 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper/process.py +0 -0
- {wyoming_piper-1.3.1 → wyoming_piper-1.5.3}/wyoming_piper.egg-info/dependency_links.txt +0 -0
- {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
|
+
[](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
|
+
[](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
|
|
@@ -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
|
|
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 .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
run()
|
|
157
229
|
except KeyboardInterrupt:
|
|
158
230
|
pass
|