Skip to content

Commit ccc55ff

Browse files
authored
Merge pull request #92 from opensorceror/feat/python-server-tests
Add tests for Python websocket server
2 parents 34098b6 + f02747a commit ccc55ff

9 files changed

Lines changed: 679 additions & 12 deletions

File tree

python_websocket_server/.pylintrc

Lines changed: 589 additions & 0 deletions
Large diffs are not rendered by default.

python_websocket_server/README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Server configuration is specified in the [`application.conf`](application.conf)
2121

2222
Make sure your model and scorer files are present in the same directory as the `application.conf` file. Then execute:
2323

24-
```
24+
```sh
2525
python -m deepspeech_server.app
2626
```
2727

@@ -70,3 +70,24 @@ DeepSpeech service. The websocket timeout on the ingress is set to 1 hour.
7070
## Contributing
7171

7272
Bug reports and merge requests are welcome.
73+
74+
### Running pylint analysis
75+
76+
```sh
77+
pylint deepspeech_server
78+
```
79+
80+
### Running tests
81+
82+
To run tests without coverage, execute:
83+
84+
```sh
85+
python -m pytest tests/test_app.py
86+
```
87+
88+
To run tests with coverage, and to print coverage to the terminal and write a coverage report, execute:
89+
90+
```sh
91+
python -m pytest -p pytest_cov --cov=deepspeech_server --cov-report=xml --cov-report=term \
92+
--junitxml=pytest-report.xml tests/test_app.py
93+
```

python_websocket_server/application.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ server {
1111
threadpool {
1212
count = 5
1313
}
14+
15+
stt_endpoint = "/api/v1/stt"
1416
}

python_websocket_server/deepspeech_server/app.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module containing server endpoints and startup functionality"""
2+
13
import json
24
from concurrent.futures import ThreadPoolExecutor
35
from pathlib import Path
@@ -8,7 +10,7 @@
810
from sanic.log import logger
911

1012
from deepspeech_server.engine import SpeechToTextEngine
11-
from deepspeech_server.models import Response, Error
13+
from deepspeech_server.models import Response, ErrorResponse
1214

1315
# Load app configs and initialize DeepSpeech model
1416
conf = ConfigFactory.parse_file("application.conf")
@@ -24,26 +26,28 @@
2426

2527
@app.route("/", methods=["GET"])
2628
async def healthcheck(_):
29+
"""Route for simple healthcheck that simply returns greeting message"""
2730
return response.text("Welcome to DeepSpeech Server!")
2831

2932

30-
@app.websocket("/api/v1/stt")
31-
async def stt(request, ws):
33+
@app.websocket(conf['server.stt_endpoint'])
34+
async def stt(request, websocket):
35+
"""Route for requesting speech-to-text transcription"""
3236
logger.debug(f"Received {request.method} request at {request.path}")
3337
try:
34-
audio = await ws.recv()
38+
audio = await websocket.recv()
3539

3640
inference_start = perf_counter()
3741
text = await app.loop.run_in_executor(executor, lambda: engine.run(audio))
3842
inference_end = perf_counter() - inference_start
3943

40-
await ws.send(json.dumps(Response(text, inference_end).__dict__))
44+
await websocket.send(json.dumps(Response(text, inference_end).__dict__))
4145
logger.debug(f"Completed {request.method} request at {request.path} in {inference_end} seconds")
42-
except Exception as e: # pylint: disable=broad-except
43-
logger.debug(f"Failed to process {request.method} request at {request.path}. The exception is: {str(e)}.")
44-
await ws.send(json.dumps(Error("Something went wrong").__dict__))
46+
except Exception as ex: # pylint: disable=broad-except
47+
logger.debug(f"Failed to process {request.method} request at {request.path}. The exception is: {str(ex)}.")
48+
await websocket.send(json.dumps(ErrorResponse("Something went wrong").__dict__))
4549

46-
await ws.close()
50+
await websocket.close()
4751

4852

4953
if __name__ == "__main__":

python_websocket_server/deepspeech_server/engine.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module containing speech-to-text transcription functionality"""
2+
13
import wave
24
from io import BytesIO
35

@@ -7,6 +9,7 @@
79

810

911
def normalize_audio(audio):
12+
"""Normalize the audio into the format required by DeepSpeech"""
1013
out, err = (
1114
ffmpeg.input("pipe:0")
1215
.output(
@@ -26,11 +29,13 @@ def normalize_audio(audio):
2629

2730

2831
class SpeechToTextEngine:
32+
"""Class to perform speech-to-text transcription and related functionality"""
2933
def __init__(self, model_path, scorer_path):
3034
self.model = Model(model_path=model_path)
3135
self.model.enableExternalScorer(scorer_path=scorer_path)
3236

3337
def run(self, audio):
38+
"""Perform speech-to-text transcription"""
3439
audio = normalize_audio(audio)
3540
audio = BytesIO(audio)
3641
with wave.Wave_read(audio) as wav:
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
"""Module containing models used for server responses"""
2+
13
class Response:
4+
"""Response indicating successful operation"""
25
def __init__(self, text, time):
36
self.text = text
47
self.time = time
58

69

7-
class Error:
10+
class ErrorResponse:
11+
"""Response indicating failed operation"""
812
def __init__(self, message):
913
self.message = message

python_websocket_server/requirements.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ black>=20.8b1
44
ffmpeg-python==0.2.0
55
sanic==20.3.0
66
pyhocon==0.3.54
7-
numpy~=1.17.0
7+
numpy~=1.19.3
8+
pytest-sanic==1.6.2
9+
click~=7.1.2
10+
pytest~=6.1.2
11+
pylint>=2.6.0
12+
pytest-cov~=2.10.1
61.8 KB
Binary file not shown.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import ast
2+
3+
import pytest
4+
from pyhocon import ConfigFactory
5+
from sanic.websocket import WebSocketProtocol
6+
7+
from deepspeech_server.app import app
8+
9+
# Load app configs and initialize DeepSpeech model
10+
conf = ConfigFactory.parse_file("application.conf")
11+
12+
13+
@pytest.fixture
14+
def sanic_server(loop, sanic_client):
15+
return loop.run_until_complete(sanic_client(app, protocol=WebSocketProtocol))
16+
17+
18+
def test_index_returns_200():
19+
request, response = app.test_client.get("/")
20+
assert response.status == 200
21+
22+
23+
async def test_valid_audio(sanic_server):
24+
ws_conn = await sanic_server.ws_connect(conf['server.stt_endpoint'])
25+
with open("tests/experience_proves_this.wav", mode="rb") as file:
26+
audio = file.read()
27+
await ws_conn.send_bytes(audio)
28+
result = await ws_conn.receive()
29+
assert ast.literal_eval(result.data)["text"] == "experience proves this"
30+
31+
32+
async def test_invalid_audio(sanic_server):
33+
ws_conn = await sanic_server.ws_connect(conf['server.stt_endpoint'])
34+
audio = b"this ain't audio"
35+
await ws_conn.send_bytes(audio)
36+
result = await ws_conn.receive()
37+
assert ast.literal_eval(result.data)["message"] == "Something went wrong"

0 commit comments

Comments
 (0)