Spaces:
Build error
Build error
| import http.client | |
| import json | |
| import os | |
| import tempfile | |
| import urllib.request | |
| from typing import Tuple | |
| import numpy as np | |
| import requests | |
| from scipy.io import wavfile | |
| from TTS.utils.audio.numpy_transforms import save_wav | |
| class Speaker(object): | |
| """Convert dict to object.""" | |
| def __init__(self, d, is_voice=False): | |
| self.is_voice = is_voice | |
| for k, v in d.items(): | |
| if isinstance(k, (list, tuple)): | |
| setattr(self, k, [Speaker(x) if isinstance(x, dict) else x for x in v]) | |
| else: | |
| setattr(self, k, Speaker(v) if isinstance(v, dict) else v) | |
| def __repr__(self): | |
| return str(self.__dict__) | |
| class CS_API: | |
| """🐸Coqui Studio API Wrapper. | |
| 🐸Coqui Studio is the most advanced voice generation platform. You can generate new voices by voice cloning, voice | |
| interpolation, or our unique prompt to voice technology. It also provides a set of built-in voices with different | |
| characteristics. You can use these voices to generate new audio files or use them in your applications. | |
| You can use all the built-in and your own 🐸Coqui Studio speakers with this API with an API token. | |
| You can signup to 🐸Coqui Studio from https://app.coqui.ai/auth/signup and get an API token from | |
| https://app.coqui.ai/account. We can either enter the token as an environment variable as | |
| `export COQUI_STUDIO_TOKEN=<token>` or pass it as `CS_API(api_token=<toke>)`. | |
| Visit https://app.coqui.ai/api for more information. | |
| Args: | |
| api_token (str): 🐸Coqui Studio API token. If not provided, it will be read from the environment variable | |
| `COQUI_STUDIO_TOKEN`. | |
| model (str): 🐸Coqui Studio model. It can be either `V1`, `XTTS`. Default is `XTTS`. | |
| Example listing all available speakers: | |
| >>> from TTS.api import CS_API | |
| >>> tts = CS_API() | |
| >>> tts.speakers | |
| Example listing all emotions: | |
| >>> # emotions are only available for `V1` model | |
| >>> from TTS.api import CS_API | |
| >>> tts = CS_API(model="V1") | |
| >>> tts.emotions | |
| Example with a built-in 🐸 speaker: | |
| >>> from TTS.api import CS_API | |
| >>> tts = CS_API() | |
| >>> wav, sr = api.tts("Hello world", speaker_name=tts.speakers[0].name) | |
| >>> filepath = tts.tts_to_file(text="Hello world!", speaker_name=tts.speakers[0].name, file_path="output.wav") | |
| Example with multi-language model: | |
| >>> from TTS.api import CS_API | |
| >>> tts = CS_API(model="XTTS") | |
| >>> wav, sr = api.tts("Hello world", speaker_name=tts.speakers[0].name, language="en") | |
| """ | |
| MODEL_ENDPOINTS = { | |
| "V1": { | |
| "list_speakers": "https://app.coqui.ai/api/v2/speakers", | |
| "synthesize": "https://app.coqui.ai/api/v2/samples", | |
| "list_voices": "https://app.coqui.ai/api/v2/voices", | |
| }, | |
| "XTTS": { | |
| "list_speakers": "https://app.coqui.ai/api/v2/speakers", | |
| "synthesize": "https://app.coqui.ai/api/v2/samples/xtts/render/", | |
| "list_voices": "https://app.coqui.ai/api/v2/voices/xtts", | |
| }, | |
| } | |
| SUPPORTED_LANGUAGES = ["en", "es", "de", "fr", "it", "pt", "pl", "tr", "ru", "nl", "cs", "ar", "zh-cn", "ja"] | |
| def __init__(self, api_token=None, model="XTTS"): | |
| self.api_token = api_token | |
| self.model = model | |
| self.headers = None | |
| self._speakers = None | |
| self._check_token() | |
| def ping_api(): | |
| URL = "https://coqui.gateway.scarf.sh/tts/api" | |
| _ = requests.get(URL) | |
| def speakers(self): | |
| if self._speakers is None: | |
| self._speakers = self.list_all_speakers() | |
| return self._speakers | |
| def emotions(self): | |
| """Return a list of available emotions. | |
| TODO: Get this from the API endpoint. | |
| """ | |
| if self.model == "V1": | |
| return ["Neutral", "Happy", "Sad", "Angry", "Dull"] | |
| else: | |
| raise ValueError(f"❗ Emotions are not available for {self.model}.") | |
| def _check_token(self): | |
| if self.api_token is None: | |
| self.api_token = os.environ.get("COQUI_STUDIO_TOKEN") | |
| self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_token}"} | |
| if not self.api_token: | |
| raise ValueError( | |
| "No API token found for 🐸Coqui Studio voices - https://coqui.ai \n" | |
| "Visit 🔗https://app.coqui.ai/account to get one.\n" | |
| "Set it as an environment variable `export COQUI_STUDIO_TOKEN=<token>`\n" | |
| "" | |
| ) | |
| def list_all_speakers(self): | |
| """Return both built-in Coqui Studio speakers and custom voices created by the user.""" | |
| return self.list_speakers() + self.list_voices() | |
| def list_speakers(self): | |
| """List built-in Coqui Studio speakers.""" | |
| self._check_token() | |
| conn = http.client.HTTPSConnection("app.coqui.ai") | |
| url = self.MODEL_ENDPOINTS[self.model]["list_speakers"] | |
| conn.request("GET", f"{url}?page=1&per_page=100", headers=self.headers) | |
| res = conn.getresponse() | |
| data = res.read() | |
| return [Speaker(s) for s in json.loads(data)["result"]] | |
| def list_voices(self): | |
| """List custom voices created by the user.""" | |
| conn = http.client.HTTPSConnection("app.coqui.ai") | |
| url = self.MODEL_ENDPOINTS[self.model]["list_voices"] | |
| conn.request("GET", f"{url}?page=1&per_page=100", headers=self.headers) | |
| res = conn.getresponse() | |
| data = res.read() | |
| return [Speaker(s, True) for s in json.loads(data)["result"]] | |
| def list_speakers_as_tts_models(self): | |
| """List speakers in ModelManager format.""" | |
| models = [] | |
| for speaker in self.speakers: | |
| model = f"coqui_studio/multilingual/{speaker.name}/{self.model}" | |
| models.append(model) | |
| return models | |
| def name_to_speaker(self, name): | |
| for speaker in self.speakers: | |
| if speaker.name == name: | |
| return speaker | |
| raise ValueError(f"Speaker {name} not found in {self.speakers}") | |
| def id_to_speaker(self, speaker_id): | |
| for speaker in self.speakers: | |
| if speaker.id == speaker_id: | |
| return speaker | |
| raise ValueError(f"Speaker {speaker_id} not found.") | |
| def url_to_np(url): | |
| tmp_file, _ = urllib.request.urlretrieve(url) | |
| rate, data = wavfile.read(tmp_file) | |
| return data, rate | |
| def _create_payload(model, text, speaker, speed, emotion, language): | |
| payload = {} | |
| # if speaker.is_voice: | |
| payload["voice_id"] = speaker.id | |
| # else: | |
| payload["speaker_id"] = speaker.id | |
| if model == "V1": | |
| payload.update( | |
| { | |
| "emotion": emotion, | |
| "name": speaker.name, | |
| "text": text, | |
| "speed": speed, | |
| } | |
| ) | |
| elif model == "XTTS": | |
| payload.update( | |
| { | |
| "name": speaker.name, | |
| "text": text, | |
| "speed": speed, | |
| "language": language, | |
| } | |
| ) | |
| else: | |
| raise ValueError(f"❗ Unknown model {model}") | |
| return payload | |
| def _check_tts_args(self, text, speaker_name, speaker_id, emotion, speed, language): | |
| assert text is not None, "❗ text is required for V1 model." | |
| assert speaker_name is not None, "❗ speaker_name is required for V1 model." | |
| if self.model == "V1": | |
| if emotion is None: | |
| emotion = "Neutral" | |
| assert language is None, "❗ language is not supported for V1 model." | |
| elif self.model == "XTTS": | |
| assert emotion is None, f"❗ Emotions are not supported for XTTS model. Use V1 model." | |
| assert language is not None, "❗ Language is required for XTTS model." | |
| assert ( | |
| language in self.SUPPORTED_LANGUAGES | |
| ), f"❗ Language {language} is not yet supported. Check https://docs.coqui.ai/reference/samples_xtts_create." | |
| return text, speaker_name, speaker_id, emotion, speed, language | |
| def tts( | |
| self, | |
| text: str, | |
| speaker_name: str = None, | |
| speaker_id=None, | |
| emotion=None, | |
| speed=1.0, | |
| language=None, # pylint: disable=unused-argument | |
| ) -> Tuple[np.ndarray, int]: | |
| """Synthesize speech from text. | |
| Args: | |
| text (str): Text to synthesize. | |
| speaker_name (str): Name of the speaker. You can get the list of speakers with `list_speakers()` and | |
| voices (user generated speakers) with `list_voices()`. | |
| speaker_id (str): Speaker ID. If None, the speaker name is used. | |
| emotion (str): Emotion of the speaker. One of "Neutral", "Happy", "Sad", "Angry", "Dull". Emotions are only | |
| supported by `V1` model. Defaults to None. | |
| speed (float): Speed of the speech. 1.0 is normal speed. | |
| language (str): Language of the text. If None, the default language of the speaker is used. Language is only | |
| supported by `XTTS` model. See https://docs.coqui.ai/reference/samples_xtts_create for supported languages. | |
| """ | |
| self._check_token() | |
| self.ping_api() | |
| if speaker_name is None and speaker_id is None: | |
| raise ValueError(" [!] Please provide either a `speaker_name` or a `speaker_id`.") | |
| if speaker_id is None: | |
| speaker = self.name_to_speaker(speaker_name) | |
| else: | |
| speaker = self.id_to_speaker(speaker_id) | |
| text, speaker_name, speaker_id, emotion, speed, language = self._check_tts_args( | |
| text, speaker_name, speaker_id, emotion, speed, language | |
| ) | |
| conn = http.client.HTTPSConnection("app.coqui.ai") | |
| payload = self._create_payload(self.model, text, speaker, speed, emotion, language) | |
| url = self.MODEL_ENDPOINTS[self.model]["synthesize"] | |
| conn.request("POST", url, json.dumps(payload), self.headers) | |
| res = conn.getresponse() | |
| data = res.read() | |
| try: | |
| wav, sr = self.url_to_np(json.loads(data)["audio_url"]) | |
| except KeyError as e: | |
| raise ValueError(f" [!] 🐸 API returned error: {data}") from e | |
| return wav, sr | |
| def tts_to_file( | |
| self, | |
| text: str, | |
| speaker_name: str, | |
| speaker_id=None, | |
| emotion=None, | |
| speed=1.0, | |
| pipe_out=None, | |
| language=None, | |
| file_path: str = None, | |
| ) -> str: | |
| """Synthesize speech from text and save it to a file. | |
| Args: | |
| text (str): Text to synthesize. | |
| speaker_name (str): Name of the speaker. You can get the list of speakers with `list_speakers()` and | |
| voices (user generated speakers) with `list_voices()`. | |
| speaker_id (str): Speaker ID. If None, the speaker name is used. | |
| emotion (str): Emotion of the speaker. One of "Neutral", "Happy", "Sad", "Angry", "Dull". | |
| speed (float): Speed of the speech. 1.0 is normal speed. | |
| pipe_out (BytesIO, optional): Flag to stdout the generated TTS wav file for shell pipe. | |
| language (str): Language of the text. If None, the default language of the speaker is used. Language is only | |
| supported by `XTTS` model. Currently supports en, de, es, fr, it, pt, pl. Defaults to "en". | |
| file_path (str): Path to save the file. If None, a temporary file is created. | |
| """ | |
| if file_path is None: | |
| file_path = tempfile.mktemp(".wav") | |
| wav, sr = self.tts(text, speaker_name, speaker_id, emotion, speed, language) | |
| save_wav(wav=wav, path=file_path, sample_rate=sr, pipe_out=pipe_out) | |
| return file_path | |
| if __name__ == "__main__": | |
| import time | |
| api = CS_API() | |
| print(api.speakers) | |
| print(api.list_speakers_as_tts_models()) | |
| ts = time.time() | |
| wav, sr = api.tts( | |
| "It took me quite a long time to develop a voice.", language="en", speaker_name=api.speakers[0].name | |
| ) | |
| print(f" [i] XTTS took {time.time() - ts:.2f}s") | |
| filepath = api.tts_to_file( | |
| text="Hello world!", speaker_name=api.speakers[0].name, language="en", file_path="output.wav" | |
| ) | |