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.
Files changed (30) hide show
  1. wyoming_piper-1.6.3/LICENSE.md +21 -0
  2. wyoming_piper-1.6.3/PKG-INFO +75 -0
  3. wyoming_piper-1.6.3/README.md +39 -0
  4. wyoming_piper-1.6.3/pyproject.toml +71 -0
  5. wyoming_piper-1.6.3/setup.cfg +21 -0
  6. wyoming_piper-1.6.3/tests/test_piper.py +124 -0
  7. wyoming_piper-1.6.3/tests/test_sentence_boundary.py +61 -0
  8. wyoming_piper-1.6.3/wyoming_piper/__init__.py +7 -0
  9. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/__main__.py +60 -17
  10. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/download.py +45 -25
  11. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/handler.py +73 -5
  12. wyoming_piper-1.6.3/wyoming_piper/sentence_boundary.py +58 -0
  13. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/voices.json +2878 -199
  14. wyoming_piper-1.6.3/wyoming_piper.egg-info/PKG-INFO +75 -0
  15. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper.egg-info/SOURCES.txt +8 -3
  16. wyoming_piper-1.6.3/wyoming_piper.egg-info/entry_points.txt +2 -0
  17. wyoming_piper-1.6.3/wyoming_piper.egg-info/requires.txt +15 -0
  18. wyoming_piper-1.3.2/MANIFEST.in +0 -2
  19. wyoming_piper-1.3.2/PKG-INFO +0 -17
  20. wyoming_piper-1.3.2/requirements.txt +0 -1
  21. wyoming_piper-1.3.2/setup.cfg +0 -4
  22. wyoming_piper-1.3.2/setup.py +0 -44
  23. wyoming_piper-1.3.2/wyoming_piper/__init__.py +0 -1
  24. wyoming_piper-1.3.2/wyoming_piper.egg-info/PKG-INFO +0 -17
  25. wyoming_piper-1.3.2/wyoming_piper.egg-info/requires.txt +0 -1
  26. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/const.py +0 -0
  27. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/file_hash.py +0 -0
  28. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper/process.py +0 -0
  29. {wyoming_piper-1.3.2 → wyoming_piper-1.6.3}/wyoming_piper.egg-info/dependency_links.txt +0 -0
  30. {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
+ [![Show add-on](https://my.home-assistant.io/badges/supervisor_addon.svg)](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
+ [![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,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
+ )
@@ -0,0 +1,7 @@
1
+ """Wyoming server for piper."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("wyoming_piper")
6
+
7
+ __all__ = ["__version__"]
@@ -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(level=logging.DEBUG if args.debug else logging.INFO)
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
- 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,
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
- _custom_voice_path, custom_config_path = find_voice(
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["dataset"]
132
- custom_quality = custom_config["audio"]["quality"]
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=f"{custom_name} ({custom_quality})",
171
+ description=description,
172
+ version=None,
137
173
  attribution=Attribution(name="", url=""),
138
174
  installed=True,
139
- languages=[custom_config["language"]["code"]],
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
- asyncio.run(main())
236
+ run()
194
237
  except KeyboardInterrupt:
195
238
  pass