Skip to content

Commit 1166e56

Browse files
committed
Add wake HTTP server
1 parent f54e98e commit 1166e56

File tree

6 files changed

+183
-72
lines changed

6 files changed

+183
-72
lines changed

wyoming/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.3
1+
1.5.4

wyoming/http/asr_server.py

+8-34
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
11
"""HTTP server for automated speech recognition (ASR)."""
2-
3-
import argparse
42
import io
3+
import logging
54
import wave
65
from pathlib import Path
76

8-
from flask import Flask, Response, jsonify, redirect, request
9-
from swagger_ui import flask_api_doc # pylint: disable=no-name-in-module
7+
from flask import Response, jsonify, request
108

119
from wyoming.asr import Transcribe, Transcript
1210
from wyoming.audio import wav_to_chunks
1311
from wyoming.client import AsyncClient
1412
from wyoming.error import Error
15-
from wyoming.info import Describe, Info
13+
14+
from .shared import get_app, get_argument_parser
1615

1716
_DIR = Path(__file__).parent
1817
CONF_PATH = _DIR / "conf" / "asr.yaml"
1918

2019

2120
def main():
22-
parser = argparse.ArgumentParser()
23-
parser.add_argument("--host", default="0.0.0.0")
24-
parser.add_argument("--port", type=int, default=5000)
25-
parser.add_argument("--uri", help="URI of Wyoming ASR service")
21+
parser = get_argument_parser()
2622
parser.add_argument("--model", help="Default model name for transcription")
2723
parser.add_argument("--language", help="Default language for transcription")
2824
parser.add_argument("--samples-per-chunk", type=int, default=1024)
2925
args = parser.parse_args()
26+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
3027

31-
app = Flask("asr")
32-
33-
@app.route("/")
34-
def redirect_to_api():
35-
return redirect("/api")
28+
app = get_app("asr", CONF_PATH, args)
3629

3730
@app.route("/api/speech-to-text", methods=["POST"])
3831
async def api_stt() -> Response:
@@ -52,7 +45,7 @@ async def api_stt() -> Response:
5245
with wave.open(wav_io, "rb") as wav_file:
5346
chunks = wav_to_chunks(
5447
wav_file,
55-
samples_per_chunk=1024,
48+
samples_per_chunk=args.samples_per_chunk,
5649
start_event=True,
5750
stop_event=True,
5851
)
@@ -74,25 +67,6 @@ async def api_stt() -> Response:
7467
f"Unexpected error from client: code={error.code}, text={error.text}"
7568
)
7669

77-
@app.route("/api/info", methods=["GET"])
78-
async def api_info():
79-
uri = request.args.get("uri", args.uri)
80-
if not uri:
81-
raise ValueError("URI is required")
82-
83-
async with AsyncClient.from_uri(uri) as client:
84-
await client.write_event(Describe().event())
85-
86-
while True:
87-
event = await client.read_event()
88-
if event is None:
89-
raise RuntimeError("Client disconnected")
90-
91-
if Info.is_type(event.type):
92-
info = Info.from_event(event)
93-
return jsonify(info.to_dict())
94-
95-
flask_api_doc(app, config_path=str(CONF_PATH), url_prefix="/api", title="API doc")
9670
app.run(args.host, args.port)
9771

9872

wyoming/http/conf/wake.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
openapi: "3.0.0"
3+
info:
4+
title: 'Wyoming Wake'
5+
version: '1.0.0'
6+
description: 'API for Wake Word Detection'
7+
paths:
8+
/api/info:
9+
get:
10+
summary: 'Get service information'
11+
responses:
12+
'200':
13+
description: OK
14+
content:
15+
application/json:
16+
schema:
17+
/api/detect-wake-word:
18+
post:
19+
summary: 'Transcribe WAV data to text'
20+
requestBody:
21+
description: 'WAV data (16-bit 16Khz mono preferred)'
22+
required: true
23+
content:
24+
audio/wav:
25+
schema:
26+
type: string
27+
format: binary
28+
parameters:
29+
- in: query
30+
name: uri
31+
description: 'URI of Wyoming ASR service'
32+
schema:
33+
type: string
34+
responses:
35+
'200':
36+
description: OK
37+
content:
38+
application/json:
39+
schema:
40+
type: object

wyoming/http/shared.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Shared code for HTTP servers."""
2+
import argparse
3+
from pathlib import Path
4+
from typing import Union
5+
6+
from flask import Flask, jsonify, redirect, request
7+
from swagger_ui import flask_api_doc # pylint: disable=no-name-in-module
8+
9+
from wyoming.client import AsyncClient
10+
from wyoming.info import Describe, Info
11+
12+
13+
def get_argument_parser() -> argparse.ArgumentParser:
14+
"""Create argument parser with shared arguments."""
15+
parser = argparse.ArgumentParser()
16+
parser.add_argument("--host", default="0.0.0.0")
17+
parser.add_argument("--port", type=int, default=5000)
18+
parser.add_argument("--uri", help="URI of Wyoming service")
19+
parser.add_argument(
20+
"--debug", action="store_true", help="Print DEBUG logs to console"
21+
)
22+
return parser
23+
24+
25+
def get_app(
26+
name: str, openapi_config_path: Union[str, Path], args: argparse.Namespace
27+
) -> Flask:
28+
"""Create Flask app with default endpoints."""
29+
30+
app = Flask(name)
31+
32+
@app.route("/")
33+
def redirect_to_api():
34+
return redirect("/api")
35+
36+
@app.route("/api/info", methods=["GET"])
37+
async def api_info():
38+
uri = request.args.get("uri", args.uri)
39+
if not uri:
40+
raise ValueError("URI is required")
41+
42+
async with AsyncClient.from_uri(uri) as client:
43+
await client.write_event(Describe().event())
44+
45+
while True:
46+
event = await client.read_event()
47+
if event is None:
48+
raise RuntimeError("Client disconnected")
49+
50+
if Info.is_type(event.type):
51+
info = Info.from_event(event)
52+
return jsonify(info.to_dict())
53+
54+
@app.errorhandler(Exception)
55+
async def handle_error(err):
56+
"""Return error as text."""
57+
return (f"{err.__class__.__name__}: {err}", 500)
58+
59+
flask_api_doc(
60+
app, config_path=str(openapi_config_path), url_prefix="/api", title="API doc"
61+
)
62+
63+
return app

wyoming/http/tts_server.py

+7-37
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,31 @@
11
"""HTTP server for text to speech (TTS)."""
2-
import argparse
32
import io
3+
import logging
44
import wave
55
from pathlib import Path
66
from typing import Optional
77

8-
from flask import Flask, Response, jsonify, redirect, request
9-
from swagger_ui import flask_api_doc # pylint: disable=no-name-in-module
8+
from flask import Response, request
109

1110
from wyoming.audio import AudioChunk, AudioStart, AudioStop
1211
from wyoming.client import AsyncClient
1312
from wyoming.error import Error
14-
from wyoming.info import Describe, Info
1513
from wyoming.tts import Synthesize, SynthesizeVoice
1614

15+
from .shared import get_app, get_argument_parser
16+
1717
_DIR = Path(__file__).parent
1818
CONF_PATH = _DIR / "conf" / "tts.yaml"
1919

2020

2121
def main():
22-
parser = argparse.ArgumentParser()
23-
parser.add_argument("--host", default="0.0.0.0")
24-
parser.add_argument("--port", type=int, default=5000)
25-
parser.add_argument("--uri", help="URI of Wyoming ASR service")
22+
parser = get_argument_parser()
2623
parser.add_argument("--voice", help="Default voice for synthesis")
2724
parser.add_argument("--speaker", help="Default voice speaker for synthesis")
2825
args = parser.parse_args()
26+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
2927

30-
app = Flask("tts")
31-
32-
@app.route("/")
33-
def redirect_to_api():
34-
return redirect("/api")
28+
app = get_app("tts", CONF_PATH, args)
3529

3630
@app.route("/api/text-to-speech", methods=["POST", "GET"])
3731
async def api_stt() -> Response:
@@ -84,30 +78,6 @@ async def api_stt() -> Response:
8478
f"Unexpected error from client: code={error.code}, text={error.text}"
8579
)
8680

87-
@app.route("/api/info", methods=["GET"])
88-
async def api_info():
89-
uri = request.args.get("uri", args.uri)
90-
if not uri:
91-
raise ValueError("URI is required")
92-
93-
async with AsyncClient.from_uri(uri) as client:
94-
await client.write_event(Describe().event())
95-
96-
while True:
97-
event = await client.read_event()
98-
if event is None:
99-
raise RuntimeError("Client disconnected")
100-
101-
if Info.is_type(event.type):
102-
info = Info.from_event(event)
103-
return jsonify(info.to_dict())
104-
105-
@app.errorhandler(Exception)
106-
async def handle_error(err):
107-
"""Return error as text."""
108-
return (f"{err.__class__.__name__}: {err}", 500)
109-
110-
flask_api_doc(app, config_path=str(CONF_PATH), url_prefix="/api", title="API doc")
11181
app.run(args.host, args.port)
11282

11383

wyoming/http/wake_server.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""HTTP server for wake word detection."""
2+
import io
3+
import logging
4+
import wave
5+
from pathlib import Path
6+
7+
from flask import Response, jsonify, request
8+
9+
from wyoming.audio import wav_to_chunks
10+
from wyoming.client import AsyncClient
11+
from wyoming.error import Error
12+
from wyoming.wake import Detection, NotDetected
13+
14+
from .shared import get_app, get_argument_parser
15+
16+
_DIR = Path(__file__).parent
17+
CONF_PATH = _DIR / "conf" / "wake.yaml"
18+
19+
20+
def main():
21+
parser = get_argument_parser()
22+
parser.add_argument("--samples-per-chunk", type=int, default=1024)
23+
args = parser.parse_args()
24+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
25+
26+
app = get_app("wake", CONF_PATH, args)
27+
28+
@app.route("/api/detect-wake-word", methods=["POST", "GET"])
29+
async def api_wake() -> Response:
30+
uri = request.args.get("uri", args.uri)
31+
if not uri:
32+
raise ValueError("URI is required")
33+
34+
async with AsyncClient.from_uri(uri) as client:
35+
with io.BytesIO(request.data) as wav_io:
36+
with wave.open(wav_io, "rb") as wav_file:
37+
chunks = wav_to_chunks(
38+
wav_file,
39+
samples_per_chunk=args.samples_per_chunk,
40+
start_event=True,
41+
stop_event=True,
42+
)
43+
for chunk in chunks:
44+
await client.write_event(chunk.event())
45+
46+
while True:
47+
event = await client.read_event()
48+
if event is None:
49+
raise RuntimeError("Client disconnected")
50+
51+
if Detection.is_type(event.type) or NotDetected.is_type(event.type):
52+
return jsonify(event.to_dict())
53+
54+
if Error.is_type(event.type):
55+
error = Error.from_event(event)
56+
raise RuntimeError(
57+
f"Unexpected error from client: code={error.code}, text={error.text}"
58+
)
59+
60+
app.run(args.host, args.port)
61+
62+
63+
if __name__ == "__main__":
64+
main()

0 commit comments

Comments
 (0)