diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9ccf88d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +--- +name: test + +# yamllint disable-line rule:truthy +on: + push: + branches: [master] + + pull_request: + +permissions: + contents: read + +jobs: + test_linux: + name: "test on linux" + runs-on: ubuntu-latest + strategy: + matrix: + python_version: ["3.8", "3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v4.1.1 + - uses: actions/setup-python@v5 + with: + python-version: "${{ matrix.python_version }}" + cache: "pip" + cache-dependency-path: requirements_dev.txt + - run: script/setup --dev + - run: | + test $(script/run --version) = $(cat wyoming_piper/VERSION) + - run: script/lint + - run: script/test diff --git a/.gitignore b/.gitignore index a49a04e..8e4ccc8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ htmlcov /.venv/ .mypy_cache/ __pycache__/ +/local/ +.tox/ /dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b5f5a73..d4f1a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.5.0 + +- Send speakers in `info` message +- Update voices.json with new voices +- Add tests to CI + ## 1.4.0 - Fix use of UTF-8 characters in URLs diff --git a/README.md b/README.md index c4e3c50..eeb68c8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,27 @@ [Source](https://github.com/home-assistant/addons/tree/master/piper) +## Local Install + +Clone the repository and set up Python virtual environment: + +``` sh +git clone https://github.com/rhasspy/wyoming-piper.git +cd wyoming-piper +script/setup +``` + +Install Piper +```sh +curl -L -s "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz" | tar -zxvf - -C /usr/share +``` + +Run a server that anyone can connect to: + +``` sh +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 +``` + ## Docker Image ``` sh diff --git a/requirements.txt b/requirements.txt index 179b422..1ff43a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -wyoming==1.1.0 +wyoming==1.5.3 diff --git a/requirements_dev.txt b/requirements_dev.txt index 77190e6..91c15ae 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,3 +3,9 @@ flake8==6.0.0 isort==5.11.3 mypy==0.991 pylint==2.15.9 +pytest==7.4.4 +pytest-asyncio==0.23.3 +tox>=4,<5 +scipy>=1.10,<2 +numpy>=1.20,<2 +python-speech-features==0.6 diff --git a/script/format b/script/format index d72d1b4..187a799 100755 --- a/script/format +++ b/script/format @@ -7,7 +7,10 @@ _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" _MODULE_DIR = _PROGRAM_DIR / "wyoming_piper" +_TESTS_DIR = _PROGRAM_DIR / "tests" + +_FORMAT_DIRS = [_MODULE_DIR, _TESTS_DIR] context = venv.EnvBuilder().ensure_directories(_VENV_DIR) -subprocess.check_call([context.env_exe, "-m", "black", str(_MODULE_DIR)]) -subprocess.check_call([context.env_exe, "-m", "isort", str(_MODULE_DIR)]) +subprocess.check_call([context.env_exe, "-m", "black"] + _FORMAT_DIRS) +subprocess.check_call([context.env_exe, "-m", "isort"] + _FORMAT_DIRS) diff --git a/script/lint b/script/lint index 6071488..875a265 100755 --- a/script/lint +++ b/script/lint @@ -7,10 +7,13 @@ _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" _MODULE_DIR = _PROGRAM_DIR / "wyoming_piper" +_TESTS_DIR = _PROGRAM_DIR / "tests" + +_LINT_DIRS = [_MODULE_DIR, _TESTS_DIR] context = venv.EnvBuilder().ensure_directories(_VENV_DIR) -subprocess.check_call([context.env_exe, "-m", "black", str(_MODULE_DIR), "--check"]) -subprocess.check_call([context.env_exe, "-m", "isort", str(_MODULE_DIR), "--check"]) -subprocess.check_call([context.env_exe, "-m", "flake8", str(_MODULE_DIR)]) -subprocess.check_call([context.env_exe, "-m", "pylint", str(_MODULE_DIR)]) -subprocess.check_call([context.env_exe, "-m", "mypy", str(_MODULE_DIR)]) +subprocess.check_call([context.env_exe, "-m", "black"] + _LINT_DIRS + ["--check"]) +subprocess.check_call([context.env_exe, "-m", "isort"] + _LINT_DIRS + ["--check"]) +subprocess.check_call([context.env_exe, "-m", "flake8"] + _LINT_DIRS) +subprocess.check_call([context.env_exe, "-m", "pylint"] + _LINT_DIRS) +subprocess.check_call([context.env_exe, "-m", "mypy"] + _LINT_DIRS) diff --git a/script/setup b/script/setup index 92ec185..9b35199 100755 --- a/script/setup +++ b/script/setup @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import argparse import subprocess import venv from pathlib import Path @@ -7,6 +8,9 @@ _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" +parser = argparse.ArgumentParser() +parser.add_argument("--dev", action="store_true", help="Install dev requirements") +args = parser.parse_args() # Create virtual environment builder = venv.EnvBuilder(with_pip=True) @@ -20,3 +24,9 @@ subprocess.check_call(pip + ["install", "--upgrade", "setuptools", "wheel"]) # Install requirements subprocess.check_call(pip + ["install", "-r", str(_PROGRAM_DIR / "requirements.txt")]) + +if args.dev: + # Install dev requirements + subprocess.check_call( + pip + ["install", "-r", str(_PROGRAM_DIR / "requirements_dev.txt")] + ) diff --git a/script/test b/script/test new file mode 100755 index 0000000..5ad78a8 --- /dev/null +++ b/script/test @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import subprocess +import sys +import venv +from pathlib import Path + +_DIR = Path(__file__).parent +_PROGRAM_DIR = _DIR.parent +_VENV_DIR = _PROGRAM_DIR / ".venv" +_TEST_DIR = _PROGRAM_DIR / "tests" + +context = venv.EnvBuilder().ensure_directories(_VENV_DIR) +subprocess.check_call([context.env_exe, "-m", "pytest", _TEST_DIR] + sys.argv[1:]) diff --git a/setup.py b/setup.py index 06d2257..455dd5a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ from setuptools import setup this_dir = Path(__file__).parent -module_dir = this_dir / "wyoming_piper" requirements = [] requirements_path = this_dir / "requirements.txt" @@ -13,22 +12,25 @@ with open(requirements_path, "r", encoding="utf-8") as requirements_file: requirements = requirements_file.read().splitlines() -data_files = [module_dir / "voices.json"] +module_name = "wyoming_piper" +module_dir = this_dir / module_name +version_path = module_dir / "VERSION" +version = version_path.read_text(encoding="utf-8").strip() + +data_files = [module_dir / "voices.json", version_path] # ----------------------------------------------------------------------------- setup( - name="wyoming_piper", - version="1.4.0", + name=module_name, + version=version, description="Wyoming Server for Piper", url="https://github.com/rhasspy/wyoming-piper", author="Michael Hansen", author_email="mike@rhasspy.org", license="MIT", packages=setuptools.find_packages(), - package_data={ - "wyoming_piper": [str(p.relative_to(module_dir)) for p in data_files] - }, + package_data={module_name: [str(p.relative_to(module_dir)) for p in data_files]}, install_requires=requirements, classifiers=[ "Development Status :: 3 - Alpha", @@ -42,4 +44,9 @@ "Programming Language :: Python :: 3.11", ], keywords="rhasspy wyoming piper tts", + entry_points={ + 'console_scripts': [ + 'wyoming-piper = wyoming_piper:__main__.run' + ] + }, ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dtw.py b/tests/dtw.py new file mode 100644 index 0000000..613450e --- /dev/null +++ b/tests/dtw.py @@ -0,0 +1,43 @@ +import math + +import numpy as np +import scipy + + +def compute_optimal_path(x: np.ndarray, y: np.ndarray) -> float: + """Computes optimal path between x and y.""" + m = len(x) + n = len(y) + + # Need 2-D arrays for distance calculation + if len(x.shape) == 1: + x = x.reshape(-1, 1) + + if len(y.shape) == 1: + y = y.reshape(-1, 1) + + distance_matrix = scipy.spatial.distance.cdist(x, y, metric="cosine") + + cost_matrix = np.full(shape=(m, n), fill_value=math.inf, dtype=float) + cost_matrix[0][0] = distance_matrix[0][0] + + for row in range(1, m): + cost = distance_matrix[row, 0] + cost_matrix[row][0] = cost + cost_matrix[row - 1][0] + + for col in range(1, n): + cost = distance_matrix[0, col] + cost_matrix[0][col] = cost + cost_matrix[0][col - 1] + + for row in range(1, m): + for col in range(1, n): + cost = distance_matrix[row, col] + cost_matrix[row][col] = cost + min( + cost_matrix[row - 1][col], # insertion + cost_matrix[row][col - 1], # deletion + cost_matrix[row - 1][col - 1], # match + ) + + distance = cost_matrix[m - 1][n - 1] + + return distance diff --git a/tests/test_piper.py b/tests/test_piper.py new file mode 100644 index 0000000..e75f9a7 --- /dev/null +++ b/tests/test_piper.py @@ -0,0 +1,123 @@ +"""Tests for wyoming-piper""" +import asyncio +import sys +import tarfile +import wave +from asyncio.subprocess import PIPE +from pathlib import Path +from urllib.request import urlopen + +import numpy as np +import pytest +import python_speech_features +from wyoming.audio import AudioChunk, AudioStart, AudioStop +from wyoming.event import async_read_event, async_write_event +from wyoming.info import Describe, Info +from wyoming.tts import Synthesize, SynthesizeVoice + +from .dtw import compute_optimal_path + +_DIR = Path(__file__).parent +_LOCAL_DIR = _DIR.parent / "local" +_PIPER_URL = ( + "https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz" +) +_TIMEOUT = 60 + + +def download_piper() -> None: + """Downloads a binary version of Piper.""" + piper_path = _LOCAL_DIR / "piper" + if piper_path.exists(): + return + + _LOCAL_DIR.mkdir(parents=True, exist_ok=True) + with urlopen(_PIPER_URL) as response: + with tarfile.open(fileobj=response, mode="r|*") as piper_file: + piper_file.extractall(_LOCAL_DIR) + + +@pytest.mark.asyncio +async def test_piper() -> None: + download_piper() + + proc = await asyncio.create_subprocess_exec( + sys.executable, + "-m", + "wyoming_piper", + "--uri", + "stdio://", + "--piper", + str(_LOCAL_DIR / "piper" / "piper"), + "--voice", + "en_US-ryan-low", + "--data-dir", + str(_LOCAL_DIR), + stdin=PIPE, + stdout=PIPE, + ) + assert proc.stdin is not None + assert proc.stdout is not None + + # Check info + await async_write_event(Describe().event(), proc.stdin) + while True: + event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT) + assert event is not None + + if not Info.is_type(event.type): + continue + + info = Info.from_event(event) + assert len(info.tts) == 1, "Expected one tts service" + tts = info.tts[0] + assert len(tts.voices) > 0, "Expected at least one voice" + voice_model = next((v for v in tts.voices if v.name == "en_US-ryan-low"), None) + assert voice_model is not None, "Expected ryan voice" + break + + # Synthesize text + await async_write_event( + Synthesize("This is a test.", voice=SynthesizeVoice("en_US-ryan-low")).event(), + proc.stdin, + ) + + event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT) + assert event is not None + assert AudioStart.is_type(event.type) + audio_start = AudioStart.from_event(event) + + with wave.open(str(_DIR / "this_is_a_test.wav"), "rb") as wav_file: + assert audio_start.rate == wav_file.getframerate() + assert audio_start.width == wav_file.getsampwidth() + assert audio_start.channels == wav_file.getnchannels() + expected_audio = wav_file.readframes(wav_file.getnframes()) + expected_array = np.frombuffer(expected_audio, dtype=np.int16) + + actual_audio = bytes() + while True: + event = await asyncio.wait_for(async_read_event(proc.stdout), timeout=_TIMEOUT) + assert event is not None + if AudioStop.is_type(event.type): + break + + if AudioChunk.is_type(event.type): + chunk = AudioChunk.from_event(event) + assert chunk.rate == audio_start.rate + assert chunk.width == audio_start.width + assert chunk.channels == audio_start.channels + actual_audio += chunk.audio + + actual_array = np.frombuffer(actual_audio, dtype=np.int16) + + # Less than 20% difference in length + assert ( + abs(len(actual_array) - len(expected_array)) + / max(len(actual_array), len(expected_array)) + < 0.2 + ) + + # Compute dynamic time warping (DTW) distance of MFCC features + expected_mfcc = python_speech_features.mfcc(expected_array, winstep=0.02) + actual_mfcc = python_speech_features.mfcc(actual_array, winstep=0.02) + assert compute_optimal_path(actual_mfcc, expected_mfcc) < 10 diff --git a/tests/this_is_a_test.wav b/tests/this_is_a_test.wav new file mode 100644 index 0000000..11dc7cd Binary files /dev/null and b/tests/this_is_a_test.wav differ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..f8aaad3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +env_list = + py{38,39,310,311} +minversion = 4.12.1 + +[testenv] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=7,<8 + pytest-asyncio<1 + scipy>=1.10.0,<2 + numpy>=1.20,<2 + python-speech-features==0.6 +commands = + pytest {tty:--color=yes} {posargs} diff --git a/wyoming_piper/VERSION b/wyoming_piper/VERSION new file mode 100644 index 0000000..bc80560 --- /dev/null +++ b/wyoming_piper/VERSION @@ -0,0 +1 @@ +1.5.0 diff --git a/wyoming_piper/__init__.py b/wyoming_piper/__init__.py index 7c73c2b..95fa18c 100644 --- a/wyoming_piper/__init__.py +++ b/wyoming_piper/__init__.py @@ -1 +1,9 @@ """Wyoming server for piper.""" +from pathlib import Path + +_DIR = Path(__file__).parent +_VERSION_PATH = _DIR / "VERSION" + +__version__ = _VERSION_PATH.read_text(encoding="utf-8").strip() + +__all__ = ["__version__"] diff --git a/wyoming_piper/__main__.py b/wyoming_piper/__main__.py index e256f89..50de962 100755 --- a/wyoming_piper/__main__.py +++ b/wyoming_piper/__main__.py @@ -7,9 +7,10 @@ from pathlib import Path from typing import Any, Dict, Set -from wyoming.info import Attribution, Info, TtsProgram, TtsVoice +from wyoming.info import Attribution, Info, TtsProgram, TtsVoice, TtsVoiceSpeaker from wyoming.server import AsyncServer +from . import __version__ from .download import find_voice, get_voices from .handler import PiperEventHandler from .process import PiperProcessManager @@ -65,15 +66,32 @@ async def main() -> None: action="store_true", help="Download latest voices.json during startup", ) + parser.add_argument( + "--use-cuda", + action="store_true", + help="Use GPU" + ) # parser.add_argument("--debug", action="store_true", help="Log DEBUG messages") + parser.add_argument( + "--log-format", default=logging.BASIC_FORMAT, help="Format for log messages" + ) + parser.add_argument( + "--version", + action="version", + version=__version__, + help="Print version and exit", + ) args = parser.parse_args() if not args.download_dir: # Default to first data directory args.download_dir = args.data_dir[0] - logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + logging.basicConfig( + level=logging.DEBUG if args.debug else logging.INFO, format=args.log_format + ) + _LOGGER.debug(args) # Load voice info voices_info = get_voices(args.download_dir, update_voices=args.update_voices) @@ -93,20 +111,19 @@ async def main() -> None: name="rhasspy", url="https://github.com/rhasspy/piper" ), installed=True, + version=None, languages=[ voice_info.get("language", {}).get( "code", voice_info.get("espeak", {}).get("voice", voice_name.split("_")[0]), ) ], - # - # Don't send speakers for now because it overflows StreamReader buffers - # speakers=[ - # TtsVoiceSpeaker(name=speaker_name) - # for speaker_name in voice_info["speaker_id_map"] - # ] - # if voice_info.get("speaker_id_map") - # else None, + speakers=[ + TtsVoiceSpeaker(name=speaker_name) + for speaker_name in voice_info["speaker_id_map"] + ] + if voice_info.get("speaker_id_map") + else None, ) for voice_name, voice_info in voices_info.items() if not voice_info.get("_is_alias", False) @@ -150,6 +167,7 @@ async def main() -> None: TtsVoice( name=custom_name, description=description, + version=None, attribution=Attribution(name="", url=""), installed=True, languages=[lang_code], @@ -166,6 +184,7 @@ async def main() -> None: ), installed=True, voices=sorted(voices, key=lambda v: v.name), + version=__version__, ) ], ) @@ -204,8 +223,13 @@ def get_description(voice_info: Dict[str, Any]): # ----------------------------------------------------------------------------- + +def run(): + asyncio.run(main()) + + if __name__ == "__main__": try: - asyncio.run(main()) + run() except KeyboardInterrupt: pass diff --git a/wyoming_piper/handler.py b/wyoming_piper/handler.py index 708812b..0373a8e 100644 --- a/wyoming_piper/handler.py +++ b/wyoming_piper/handler.py @@ -8,6 +8,7 @@ from typing import Any, Dict, Optional from wyoming.audio import AudioChunk, AudioStart, AudioStop +from wyoming.error import Error from wyoming.event import Event from wyoming.info import Describe, Info from wyoming.server import AsyncEventHandler @@ -43,6 +44,15 @@ async def handle_event(self, event: Event) -> bool: _LOGGER.warning("Unexpected event: %s", event) return True + try: + return await self._handle_event(event) + except Exception as err: + await self.write_event( + Error(text=str(err), code=err.__class__.__name__).event() + ) + raise err + + async def _handle_event(self, event: Event) -> bool: synthesize = Synthesize.from_event(event) _LOGGER.debug(synthesize) diff --git a/wyoming_piper/process.py b/wyoming_piper/process.py index a7e90a7..5744700 100644 --- a/wyoming_piper/process.py +++ b/wyoming_piper/process.py @@ -148,6 +148,9 @@ async def get_process(self, voice_name: Optional[str] = None) -> PiperProcess: if self.args.noise_w: piper_args.extend(["--noise-w", str(self.args.noise_w)]) + if self.args.use_cuda: + piper_args.extend(["--use-cuda"]) + _LOGGER.debug( "Starting piper process: %s args=%s", self.args.piper, piper_args ) diff --git a/wyoming_piper/voices.json b/wyoming_piper/voices.json index fa4b652..caf1afa 100644 --- a/wyoming_piper/voices.json +++ b/wyoming_piper/voices.json @@ -1,4 +1,34 @@ { + "ar_JO-kareem-low": { + "key": "ar_JO-kareem-low", + "name": "kareem", + "language": { + "code": "ar_JO", + "family": "ar", + "region": "JO", + "name_native": "العربية", + "name_english": "Arabic", + "country_english": "Jordan" + }, + "quality": "low", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "ar/ar_JO/kareem/low/ar_JO-kareem-low.onnx": { + "size_bytes": 63201294, + "md5_digest": "d335cd06fe4045a7ee9d8fb0712afaa9" + }, + "ar/ar_JO/kareem/low/ar_JO-kareem-low.onnx.json": { + "size_bytes": 5022, + "md5_digest": "465724f7d2d5f2ff061b53acb8e7f7cc" + }, + "ar/ar_JO/kareem/low/MODEL_CARD": { + "size_bytes": 274, + "md5_digest": "b6f0eaf5a7fd094be22a1bcb162173fb" + } + }, + "aliases": [] + }, "ar_JO-kareem-medium": { "key": "ar_JO-kareem-medium", "name": "kareem", @@ -311,6 +341,273 @@ "de-kerstin-low" ] }, + "de_DE-mls-medium": { + "key": "de_DE-mls-medium", + "name": "mls", + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "quality": "medium", + "num_speakers": 236, + "speaker_id_map": { + "2422": 0, + "4536": 1, + "2037": 2, + "9565": 3, + "10148": 4, + "6507": 5, + "5055": 6, + "3503": 7, + "252": 8, + "9132": 9, + "3990": 10, + "5753": 11, + "5424": 12, + "2602": 13, + "4174": 14, + "3885": 15, + "12415": 16, + "8470": 17, + "11927": 18, + "9639": 19, + "3494": 20, + "2946": 21, + "5283": 22, + "4533": 23, + "2497": 24, + "12275": 25, + "1649": 26, + "146": 27, + "8337": 28, + "4542": 29, + "589": 30, + "1998": 31, + "3797": 32, + "5244": 33, + "7328": 34, + "7998": 35, + "10179": 36, + "9610": 37, + "20": 38, + "253": 39, + "12899": 40, + "7194": 41, + "3759": 42, + "2677": 43, + "6719": 44, + "1897": 45, + "11990": 46, + "6880": 47, + "19": 48, + "9515": 49, + "327": 50, + "3244": 51, + "5324": 52, + "2234": 53, + "3124": 54, + "2043": 55, + "143": 56, + "8139": 57, + "9646": 58, + "8659": 59, + "9538": 60, + "989": 61, + "5405": 62, + "10087": 63, + "8294": 64, + "4396": 65, + "1474": 66, + "139": 67, + "136": 68, + "10791": 69, + "7242": 70, + "3631": 71, + "9908": 72, + "7906": 73, + "1171": 74, + "7479": 75, + "5632": 76, + "3731": 77, + "4650": 78, + "135": 79, + "145": 80, + "137": 81, + "1757": 82, + "91": 83, + "9514": 84, + "13494": 85, + "1946": 86, + "3277": 87, + "5595": 88, + "278": 89, + "7120": 90, + "7406": 91, + "11695": 92, + "1593": 93, + "3862": 94, + "138": 95, + "141": 96, + "9948": 97, + "1163": 98, + "1054": 99, + "1844": 100, + "4911": 101, + "7261": 102, + "8223": 103, + "7624": 104, + "144": 105, + "13871": 106, + "2974": 107, + "5934": 108, + "7002": 109, + "8769": 110, + "3363": 111, + "3040": 112, + "6067": 113, + "9494": 114, + "8743": 115, + "13255": 116, + "1660": 117, + "3588": 118, + "4748": 119, + "8450": 120, + "5295": 121, + "4705": 122, + "8125": 123, + "7272": 124, + "7320": 125, + "1874": 126, + "1262": 127, + "10870": 128, + "12379": 129, + "4463": 130, + "10349": 131, + "2252": 132, + "8325": 133, + "3052": 134, + "4001": 135, + "7456": 136, + "140": 137, + "4739": 138, + "11299": 139, + "4730": 140, + "6659": 141, + "2034": 142, + "2732": 143, + "2158": 144, + "3698": 145, + "5675": 146, + "6315": 147, + "10904": 148, + "7202": 149, + "11480": 150, + "10625": 151, + "11546": 152, + "4576": 153, + "4512": 154, + "8634": 155, + "13626": 156, + "1613": 157, + "287": 158, + "9207": 159, + "6982": 160, + "1724": 161, + "10191": 162, + "1091": 163, + "2909": 164, + "1965": 165, + "2506": 166, + "4414": 167, + "5764": 168, + "12776": 169, + "1033": 170, + "13726": 171, + "2314": 172, + "6826": 173, + "9706": 174, + "8427": 175, + "9168": 176, + "9287": 177, + "6905": 178, + "4153": 179, + "3330": 180, + "2859": 181, + "5406": 182, + "2840": 183, + "1920": 184, + "9241": 185, + "10163": 186, + "8305": 187, + "12461": 188, + "3276": 189, + "11413": 190, + "10536": 191, + "10614": 192, + "7579": 193, + "8675": 194, + "7483": 195, + "7270": 196, + "8704": 197, + "4468": 198, + "6611": 199, + "11497": 200, + "11772": 201, + "2792": 202, + "11481": 203, + "10162": 204, + "10819": 205, + "8732": 206, + "11328": 207, + "11920": 208, + "6646": 209, + "7486": 210, + "11870": 211, + "12417": 212, + "10364": 213, + "6117": 214, + "6448": 215, + "10433": 216, + "7515": 217, + "5823": 218, + "8567": 219, + "10947": 220, + "11869": 221, + "12335": 222, + "12500": 223, + "13755": 224, + "7006": 225, + "3685": 226, + "5819": 227, + "9353": 228, + "11355": 229, + "12174": 230, + "7150": 231, + "6952": 232, + "11987": 233, + "3995": 234, + "7449": 235 + }, + "files": { + "de/de_DE/mls/medium/de_DE-mls-medium.onnx": { + "size_bytes": 76961079, + "md5_digest": "bb543a8e82b95993cdd2199a0049623b" + }, + "de/de_DE/mls/medium/de_DE-mls-medium.onnx.json": { + "size_bytes": 8948, + "md5_digest": "3fb96c627820ac38cff6e8c3ac0e4aa0" + }, + "de/de_DE/mls/medium/MODEL_CARD": { + "size_bytes": 223, + "md5_digest": "e32e1746dddda1336c1b6725afa6251d" + } + }, + "aliases": [] + }, "de_DE-pavoque-low": { "key": "de_DE-pavoque-low", "name": "pavoque", @@ -1079,6 +1376,36 @@ }, "aliases": [] }, + "en_US-hfc_female-medium": { + "key": "en_US-hfc_female-medium", + "name": "hfc_female", + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx": { + "size_bytes": 63201294, + "md5_digest": "7abec91f1d6e19e913fbc4a333f62787" + }, + "en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx.json": { + "size_bytes": 5033, + "md5_digest": "c3d00f54dac3b4068f2576c15c5da3bc" + }, + "en/en_US/hfc_female/medium/MODEL_CARD": { + "size_bytes": 354, + "md5_digest": "a4a7b5da65e03e6972e44e9555a59aef" + } + }, + "aliases": [] + }, "en_US-hfc_male-medium": { "key": "en_US-hfc_male-medium", "name": "hfc_male", @@ -3507,6 +3834,66 @@ }, "aliases": [] }, + "fa_IR-amir-medium": { + "key": "fa_IR-amir-medium", + "name": "amir", + "language": { + "code": "fa_IR", + "family": "fa", + "region": "IR", + "name_native": "فارسی", + "name_english": "Farsi", + "country_english": "Iran" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "fa/fa_IR/amir/medium/fa_IR-amir-medium.onnx": { + "size_bytes": 63531379, + "md5_digest": "7c0598c9726427869e1e86447b333539" + }, + "fa/fa_IR/amir/medium/fa_IR-amir-medium.onnx.json": { + "size_bytes": 4958, + "md5_digest": "48c5e81f5aa4e1c5eba3dda0be403b58" + }, + "fa/fa_IR/amir/medium/MODEL_CARD": { + "size_bytes": 264, + "md5_digest": "0728165259eb968913a680077607cd5c" + } + }, + "aliases": [] + }, + "fa_IR-gyro-medium": { + "key": "fa_IR-gyro-medium", + "name": "gyro", + "language": { + "code": "fa_IR", + "family": "fa", + "region": "IR", + "name_native": "فارسی", + "name_english": "Farsi", + "country_english": "Iran" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "fa/fa_IR/gyro/medium/fa_IR-gyro-medium.onnx": { + "size_bytes": 63122309, + "md5_digest": "479695db58b82141fc48f0499a38a7c9" + }, + "fa/fa_IR/gyro/medium/fa_IR-gyro-medium.onnx.json": { + "size_bytes": 4958, + "md5_digest": "6289e9785140547040a1e616388dd587" + }, + "fa/fa_IR/gyro/medium/MODEL_CARD": { + "size_bytes": 262, + "md5_digest": "b30a1713c6e67946e3f1ed33eac06039" + } + }, + "aliases": [] + }, "fi_FI-harri-low": { "key": "fi_FI-harri-low", "name": "harri", @@ -3601,6 +3988,162 @@ "fr-gilles-low" ] }, + "fr_FR-mls-medium": { + "key": "fr_FR-mls-medium", + "name": "mls", + "language": { + "code": "fr_FR", + "family": "fr", + "region": "FR", + "name_native": "Français", + "name_english": "French", + "country_english": "France" + }, + "quality": "medium", + "num_speakers": 125, + "speaker_id_map": { + "1840": 0, + "3698": 1, + "123": 2, + "1474": 3, + "12709": 4, + "7423": 5, + "9242": 6, + "8778": 7, + "3060": 8, + "4512": 9, + "6249": 10, + "12541": 11, + "13634": 12, + "10065": 13, + "6128": 14, + "5232": 15, + "5764": 16, + "12713": 17, + "12823": 18, + "6070": 19, + "12501": 20, + "9121": 21, + "1649": 22, + "2776": 23, + "11772": 24, + "5612": 25, + "11822": 26, + "1590": 27, + "5525": 28, + "10827": 29, + "1243": 30, + "13142": 31, + "62": 32, + "13177": 33, + "10620": 34, + "8102": 35, + "8582": 36, + "11875": 37, + "7239": 38, + "9854": 39, + "7377": 40, + "10082": 41, + "12512": 42, + "1329": 43, + "2506": 44, + "6856": 45, + "10058": 46, + "103": 47, + "14": 48, + "6381": 49, + "1664": 50, + "11954": 51, + "66": 52, + "1127": 53, + "3270": 54, + "13611": 55, + "13658": 56, + "12968": 57, + "1989": 58, + "12981": 59, + "7193": 60, + "6348": 61, + "7679": 62, + "2284": 63, + "3182": 64, + "3503": 65, + "2033": 66, + "2771": 67, + "7614": 68, + "125": 69, + "3204": 70, + "5595": 71, + "5553": 72, + "694": 73, + "1624": 74, + "1887": 75, + "2926": 76, + "7150": 77, + "3190": 78, + "3344": 79, + "4699": 80, + "1798": 81, + "1745": 82, + "5077": 83, + "753": 84, + "52": 85, + "4174": 86, + "4018": 87, + "12899": 88, + "1844": 89, + "4396": 90, + "1817": 91, + "2155": 92, + "2946": 93, + "4336": 94, + "4609": 95, + "1977": 96, + "10957": 97, + "204": 98, + "4650": 99, + "5295": 100, + "5968": 101, + "4744": 102, + "2825": 103, + "9804": 104, + "707": 105, + "30": 106, + "115": 107, + "5840": 108, + "2587": 109, + "2607": 110, + "2544": 111, + "28": 112, + "27": 113, + "177": 114, + "112": 115, + "94": 116, + "2596": 117, + "3595": 118, + "7032": 119, + "7848": 120, + "11247": 121, + "7439": 122, + "2904": 123, + "6362": 124 + }, + "files": { + "fr/fr_FR/mls/medium/fr_FR-mls-medium.onnx": { + "size_bytes": 76733750, + "md5_digest": "87831389d3ae92347d91e38b0c57add9" + }, + "fr/fr_FR/mls/medium/fr_FR-mls-medium.onnx.json": { + "size_bytes": 7036, + "md5_digest": "be41a30aab03788f5e5c4fe51620ccbe" + }, + "fr/fr_FR/mls/medium/MODEL_CARD": { + "size_bytes": 222, + "md5_digest": "88d84c1c548aa27c1d119d2964f8fcf0" + } + }, + "aliases": [] + }, "fr_FR-mls_1840-low": { "key": "fr_FR-mls_1840-low", "name": "mls_1840", @@ -3697,6 +4240,36 @@ "fr-siwis-medium" ] }, + "fr_FR-tom-medium": { + "key": "fr_FR-tom-medium", + "name": "tom", + "language": { + "code": "fr_FR", + "family": "fr", + "region": "FR", + "name_native": "Français", + "name_english": "French", + "country_english": "France" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "fr/fr_FR/tom/medium/fr_FR-tom-medium.onnx": { + "size_bytes": 63511038, + "md5_digest": "5b460c2394a871e675f5c798af149412" + }, + "fr/fr_FR/tom/medium/fr_FR-tom-medium.onnx.json": { + "size_bytes": 4959, + "md5_digest": "964d58602df7adf76c2401b070f68ea2" + }, + "fr/fr_FR/tom/medium/MODEL_CARD": { + "size_bytes": 233, + "md5_digest": "d82342c0c27cfbe9342814c7da46cb83" + } + }, + "aliases": [] + }, "fr_FR-upmc-medium": { "key": "fr_FR-upmc-medium", "name": "upmc", @@ -4371,6 +4944,89 @@ "nl-rdh-x-low" ] }, + "nl_NL-mls-medium": { + "key": "nl_NL-mls-medium", + "name": "mls", + "language": { + "code": "nl_NL", + "family": "nl", + "region": "NL", + "name_native": "Nederlands", + "name_english": "Dutch", + "country_english": "Netherlands" + }, + "quality": "medium", + "num_speakers": 52, + "speaker_id_map": { + "2450": 0, + "1724": 1, + "1666": 2, + "5809": 3, + "496": 4, + "2506": 5, + "7432": 6, + "3619": 7, + "4429": 8, + "3798": 9, + "12500": 10, + "10587": 11, + "2951": 12, + "1775": 13, + "9861": 14, + "880": 15, + "3034": 16, + "2825": 17, + "5438": 18, + "3245": 19, + "4396": 20, + "11290": 21, + "11936": 22, + "6916": 23, + "10294": 24, + "10079": 25, + "7588": 26, + "7579": 27, + "123": 28, + "3024": 29, + "960": 30, + "10984": 31, + "2792": 32, + "7723": 33, + "4174": 34, + "2981": 35, + "5764": 36, + "6513": 37, + "7884": 38, + "6697": 39, + "12749": 40, + "11157": 41, + "2239": 42, + "10879": 43, + "1085": 44, + "8480": 45, + "8331": 46, + "6282": 47, + "10632": 48, + "2602": 49, + "5367": 50, + "11472": 51 + }, + "files": { + "nl/nl_NL/mls/medium/nl_NL-mls-medium.onnx": { + "size_bytes": 76584246, + "md5_digest": "f1d4b1452ccfdac24be72085b2b6b55c" + }, + "nl/nl_NL/mls/medium/nl_NL-mls-medium.onnx.json": { + "size_bytes": 5856, + "md5_digest": "1915807d7cb85274ba957846370fff39" + }, + "nl/nl_NL/mls/medium/MODEL_CARD": { + "size_bytes": 225, + "md5_digest": "6e5d961780907a4d7746eada893b8eca" + } + }, + "aliases": [] + }, "nl_NL-mls_5809-low": { "key": "nl_NL-mls_5809-low", "name": "mls_5809", @@ -4863,6 +5519,36 @@ }, "aliases": [] }, + "sl_SI-artur-medium": { + "key": "sl_SI-artur-medium", + "name": "artur", + "language": { + "code": "sl_SI", + "family": "sl", + "region": "SI", + "name_native": "Slovenščina", + "name_english": "Slovenian", + "country_english": "Slovenia" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "sl/sl_SI/artur/medium/sl_SI-artur-medium.onnx": { + "size_bytes": 63200492, + "md5_digest": "ca0aac61139e446bebf98561e8cf9407" + }, + "sl/sl_SI/artur/medium/sl_SI-artur-medium.onnx.json": { + "size_bytes": 4970, + "md5_digest": "8683796803bce4e9131eec00a93251ad" + }, + "sl/sl_SI/artur/medium/MODEL_CARD": { + "size_bytes": 329, + "md5_digest": "c7547b0d2c97f38dcbb231c5e77c75c9" + } + }, + "aliases": [] + }, "sr_RS-serbski_institut-medium": { "key": "sr_RS-serbski_institut-medium", "name": "serbski_institut", @@ -5016,6 +5702,36 @@ }, "aliases": [] }, + "tr_TR-fettah-medium": { + "key": "tr_TR-fettah-medium", + "name": "fettah", + "language": { + "code": "tr_TR", + "family": "tr", + "region": "TR", + "name_native": "Türkçe", + "name_english": "Turkish", + "country_english": "Turkey" + }, + "quality": "medium", + "num_speakers": 1, + "speaker_id_map": {}, + "files": { + "tr/tr_TR/fettah/medium/tr_TR-fettah-medium.onnx": { + "size_bytes": 63201294, + "md5_digest": "596984449bd075fc18e6412c66ed99c2" + }, + "tr/tr_TR/fettah/medium/tr_TR-fettah-medium.onnx.json": { + "size_bytes": 4877, + "md5_digest": "583aa5f4bfac5237afb1cdbdf5bfc992" + }, + "tr/tr_TR/fettah/medium/MODEL_CARD": { + "size_bytes": 276, + "md5_digest": "9c51c87dc191bd556c0634793c233d5c" + } + }, + "aliases": [] + }, "uk_UA-lada-x_low": { "key": "uk_UA-lada-x_low", "name": "lada",