Skip to content

Commit

Permalink
Replace fast api with litestar. Merge pull request #9
Browse files Browse the repository at this point in the history
* feat: add InvalidSampleRate exception
* feat: replace fastAPI with litestar
* chore: rename server.py to app.py
* chore: update README
* fix: run command in dockerfile
* chore: update README.md
  • Loading branch information
MrPandir authored Feb 14, 2024
2 parents 6aebf19 + e7e8512 commit 880a201
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 152 deletions.
30 changes: 26 additions & 4 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- Created in https://leviarista.github.io/github-profile-header-generator/ -->
![Header](./header.png)

## Languages supported
# Languages supported

> [!NOTE]
> All models are from the repository: [snakers4/silero-models](https://github.com/snakers4/silero-models)
Expand Down Expand Up @@ -46,10 +46,10 @@ All languages support sample rate: 8 000, 24 000, 48 000

# Run API server
```bash
python3 main.py
litestar run
```
> [!NOTE]
> The default will be [localhost:8000](http://localhost:8000/docs). All endpoints can be viewed and tested at [localhost:8000/docs](http://localhost:8000/docs)
> The default will be [localhost:8000](http://localhost:8000/)

# Run API server via docker
```bash
Expand All @@ -61,7 +61,7 @@ docker run --rm -p 8000:8000 twirapp/silero-tts-api-server

Build the API server image:
```bash
docker build --rm -f docker/Dockerfile -t silero-tts-api-server .
docker build -f docker/Dockerfile -t silero-tts-api-server .
```

Run the API server container:
Expand All @@ -76,6 +76,28 @@ docker-compose -f docker/compose.yml up

</details>

# Documentation
You can view the automatically generated documentation based on OpenAPI at:

| Provider | Url |
|--------|--------|
| [ReDoc](https://redocly.com/redoc) | https://localhost:8000/schema |
| [Swagger UI](https://swagger.io) | https://localhost:8000/schema/swagger |
| [Stoplight Elements](https://stoplight-site.webflow.io/open-source/elements) | https://localhost:8000/schema/elements |
| [RepiDoc](https://rapidocweb.com) | https://localhost:8000/schema/repidoc |
| OpenAPI schema yaml | https://localhost:8000/schema/openapi.yaml |
| OpenAPI schema json | https://localhost:8000/schema/openapi.json |

# Endpoints

- `GET` `/generate` - Generate audio in wav format from text
- `GET` `/speakers` - Get list of speakers

# Environment variables:

- `TEXT_LENGTH_LIMIT` - Maximum length of the text to be processed. Default is 930 characters.
- `MKL_NUM_THREADS` - Number of threads to use for generating audio. Default number of threads: number of CPU cores.

# Considerations for the future
This repository is dedicated to twir.app and is designed to meet its requirements.

Expand Down
70 changes: 70 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from os import environ
from typing import Annotated

from dotenv import load_dotenv
from litestar import Litestar, get, Response
from litestar.openapi import OpenAPIConfig
from litestar.config.response_cache import CACHE_FOREVER
from litestar.params import Parameter

from tts import tts
from openapi_examples import *
from http_exceptions import *
from exceptions import *


load_dotenv()

SILERO_MAX_TEXT_LENGTH = 930
text_length_limit = min(
int(environ.get("TEXT_LENGTH_LIMIT", SILERO_MAX_TEXT_LENGTH)),
SILERO_MAX_TEXT_LENGTH,
)


@get(
"/generate",
summary="Generate WAV audio from text",
media_type="audio/wav",
sync_to_thread=True,
raises=genetate_exceptions,
)
def generate(
text: Annotated[str, Parameter(examples=text_examples)],
speaker: Annotated[str, Parameter(examples=speaker_examples)],
sample_rate: Annotated[
int, Parameter(examples=sample_rate_examples, default=48_000)
],
) -> Response:
if len(text) > text_length_limit:
raise TextTooLongHTTPException(
{"text": text, "length": len(text), "max_length": text_length_limit}
)

try:
audio = tts.generate(text, speaker, sample_rate)
except NotFoundModelException:
raise NotFoundSpeakerHTTPException({"speaker": speaker})
except NotCorrectTextException:
raise NotCorrectTextHTTPException({"text": text})
except TextTooLongException:
raise TextTooLongHTTPException(
{"text": text, "length": len(text), "max_length": text_length_limit}
)
except InvalidSampleRateException:
raise InvalidSampleRateHTTPException(
{"sample_rate": sample_rate, "valid_sample_rates": tts.VALID_SAMPLE_RATES}
)
else:
return Response(audio, media_type="audio/wav")


@get("/speakers", summary="List available speakers", cache=CACHE_FOREVER)
async def speakers() -> dict[str, list[str]]:
return tts.speakers


app = Litestar(
[generate, speakers],
openapi_config=OpenAPIConfig(title="Silero TTS API", version="1.0.0"),
)
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ COPY --from=models-installer /app/models /app/models
COPY --from=pip-installer /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["litestar", "run", "--host", "0.0.0.0", "--port", "8000"]
10 changes: 9 additions & 1 deletion exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
class NotFoundModelException(Exception):
def __init__(self, speaker_name: str):
self.speaker_name = speaker_name
super().__init__(f"Model not found for speaker: {speaker_name}")

class NotCorrectTextException(Exception):
def __init__(self, text: str):
self.text = text
super().__init__(f"Text not correct: {text}")

class TextTooLongException(Exception):
def __init__(self, text: str):
super().__init__(f"Text too long. Length is {len(text)}. Max length is 930 symbols.")
self.text = text
super().__init__(f"Text too long. Length is {len(text)}. Max length is 930 symbols.")

class InvalidSampleRateException(Exception):
def __init__(self, sample_rate: int) -> None:
self.sample_rate = sample_rate
super().__init__(f"Invalid sample rate {sample_rate}. Supported sample rates are 8 000, 24 000, and 48 000.")
44 changes: 44 additions & 0 deletions http_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Any

from litestar.exceptions import HTTPException
from litestar import status_codes as status


class BaseHTTPException(HTTPException):
headers = {"Content-Type": "application/json"}

def __init__(self, extra: dict[str, Any] = None) -> None:
super().__init__(
detail=self.detail,
status_code=self.status_code,
headers=self.headers,
extra=extra,
)


class NotFoundSpeakerHTTPException(BaseHTTPException):
status_code = status.HTTP_404_NOT_FOUND
detail = "Speaker not found"


class NotCorrectTextHTTPException(BaseHTTPException):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
detail = "Text is not correct"


class TextTooLongHTTPException(BaseHTTPException):
status_code = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
detail = "Text too long"


class InvalidSampleRateHTTPException(BaseHTTPException):
status_code = status.HTTP_400_BAD_REQUEST
detail = "Invalid sample rate"


genetate_exceptions = [
NotFoundSpeakerHTTPException,
NotCorrectTextHTTPException,
TextTooLongHTTPException,
InvalidSampleRateHTTPException,
]
15 changes: 0 additions & 15 deletions main.py

This file was deleted.

45 changes: 16 additions & 29 deletions openapi_examples.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
from fastapi import Query
from litestar.openapi.spec import Example

TextExamples = Query(
openapi_examples={
"ru_1": {
"value": "Съешьте ещё этих мягких французских булочек, да выпейте чаю."
},
"ru_2": {
"value": "В недрах тундры выдры в гетрах тырят в вёдра ядра кедров."
},
"en_1": {
"value": "Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?"
},
}
)
text_examples = [
Example("ru_1", value="Съешьте ещё этих мягких французских булочек, да выпейте чаю."),
Example("ru_2", value="В недрах тундры выдры в гетрах тырят в вёдра ядра кедров."),
Example("en_1", value="Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?"),
]

SpeakerExamples = Query(
openapi_examples={
"ru_aidar": {"value": "aidar"},
"ru_baya": {"value": "baya"},
"en_0": {"value": "en_0"},
}
)
speaker_examples = [
Example("ru_aidar", value="aidar"),
Example("ru_baya", value="baya"),
Example("en_0", value="en_0"),
]

SampleRateExamples = Query(
openapi_examples={
"8 000": {"value": 8_000},
"24 000": {"value": 24_000},
"48 000": {"value": 48_000},
},
description="Sample rate in Hz",
)
sample_rate_examples = [
Example("8 000", value=8_000),
Example("24 000", value=24_000),
Example("48 000", value=48_000),
]
47 changes: 0 additions & 47 deletions openapi_responses.py

This file was deleted.

4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
fastapi==0.109.0
uvicorn==0.25.0
litestar==2.5.5
uvicorn==0.27.1

--extra-index-url https://download.pytorch.org/whl/cpu
torch==2.2.0
Expand Down
51 changes: 0 additions & 51 deletions server.py

This file was deleted.

Loading

0 comments on commit 880a201

Please sign in to comment.