스트리밍 STT - WebSocket
본 문서는 스트리밍 STT 중에서 WebSocket로 구현하는 방식에 대한 가이드를 제공합니다.
연동예제
본 문서의 예제는 로컬 오디오 파일로부터 스트리밍 음성인식을 수행하는 방법을 설명해줍니다. 마이크와 같은 스트리밍 입력 장치로 API를 이용하고 싶은 경우, 파일로 읽어오는 코드 부분을 장치 인식을 수행하는 코드로 변경함으로써 사용하실 수 있습니다.
모든 스트리밍 요청에는 사용 한도가 있습니다. 한도를 초과하면 오류가 발생합니다.
인증 토큰 발급
스트리밍 STT API를 사용하기 위해서는 인증 토큰 발급 가이드를 통해 토큰을 발급받아야 합니다.
Parameter
Name | Type | Description | Required |
---|---|---|---|
sample_rate | integer | 범위: 8000 ~ 48000, 단위: Hz | O |
encoding | string | 인코딩 타입 (참고. 지원 인코딩) | O |
use_itn | boolean | 영어/숫자/단위 변환 사용 여부 (default: true, 참고: 영어/숫자/단위 변환) | X |
use_disfluency_filter | boolean | 간투어 필터기능 사용 여부 (default: false, 참고: 간투어 필터) | X |
use_profanity_filter | boolean | 비속어 필터기능 사용 여부 (default: false, 참고: 비속어 필터) | X |
Response
{
// 문장의 발화 id
seq: integer
// 스트리밍 시작 기준 문장의 발화 시점 (단위: msec)
start_at: integer
// final이 true 일 경우 문자의 발화 시간, final 이 false일 경우 0 (단위: msec)
duration: integer
// 문장의 종료 여부
final: boolean
// 대체 텍스트, 첫번째 값이 정확도가 가장 높은 결과
alternatives: [
{
// 문장의 텍스트
text: string
// 단어(토큰)의 정보, final 이 true 일 경우만 제공
words?: [
{
// 단어(토큰)의 텍스트, `|` 로 띄어쓰기 구분
text: string
// 문장의 시작 기준 단어(토큰)의 발화 시점 (단위: msec)
start_at: integer
// 단어(토큰)의 발화 시간 (단위: msec)
duration: integer
// 단어(토큰)의 정확도 (미지원)
confidence: float
}
]
}
]
}
샘플 코드
- Golang
- Python
- Java
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
)
const ServerHost = "openapi.vito.ai"
const ClientId = "{YOUR_CLIENT_ID}"
const ClientSecret = "{YOUR_CLIENT_SECRET}"
func main() {
flag.Parse()
log.SetFlags(0)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
u := url.URL{Scheme: "wss", Host: ServerHost, Path: "/v1/transcribe:streaming"}
query := u.Query()
query.Set("sample_rate", "8000")
query.Set("encoding", "LINEAR16")
query.Set("use_itn", "true")
query.Set("use_disfluency_filter", "true")
query.Set("use_profanity_filter", "false")
u.RawQuery = query.Encode()
log.Printf("connecting to %s", u.String())
audioFile := flag.Arg(0)
f, err := os.Open(audioFile)
data := map[string][]string{
"client_id": []string{ClientId},
"client_secret": []string{ClientSecret},
}
resp, _ := http.PostForm("https://openapi.vito.ai/v1/authenticate", data)
if resp.StatusCode != 200 {
panic("Failed to authenticate")
}
bytes, _ := io.ReadAll(resp.Body)
var result struct {
Token string `json:"access_token"`
}
json.Unmarshal(bytes, &result)
requestHeader := http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", result.Token)},
}
c, res, err := websocket.DefaultDialer.Dial(u.String(), requestHeader)
if err != nil {
log.Printf("status: %s, body: %v", res.Status, res.Body)
log.Fatal("dial:", err)
}
defer c.Close()
start := time.Now()
go func() {
buf := make([]byte, 1024)
for {
n, err := f.Read(buf)
if err == io.EOF {
// Nothing else to pipe, close the stream.
log.Println("send EOS")
if err := c.WriteMessage(websocket.TextMessage, []byte("EOS")); err != nil {
log.Fatalf("Could not close stream: %v", err)
}
return
}
if err != nil {
log.Printf("Could not read from %s: %v", audioFile, err)
continue
}
err = c.WriteMessage(websocket.BinaryMessage, buf[:n])
if err != nil {
log.Println("write:", err)
}
//time.Sleep((time.Duration(n) * 1000 / 2 / 8000) * time.Millisecond)
select {
case <-interrupt:
log.Println("interrupt")
if err := c.WriteMessage(websocket.TextMessage, []byte("EOS")); err != nil {
log.Fatalf("Could not close: %v", err)
}
return
default:
}
}
}()
start2 := time.Now()
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Printf("elapsed[%v]\n", time.Since(start))
log.Println("read:", err)
return
}
log.Printf("[%v]recv: %s", time.Since(start2), message)
start2 = time.Now()
}
}
import asyncio
import json
import logging
import time
from io import DEFAULT_BUFFER_SIZE
import websockets
from requests import Session
API_BASE = "https://openapi.vito.ai"
class VITOOpenAPIClient:
def __init__(self, client_id, client_secret):
super().__init__()
self._logger = logging.getLogger(__name__)
self.client_id = client_id
self.client_secret = client_secret
self._sess = Session()
self._token = None
@property
def token(self):
if self._token is None or self._token["expire_at"] < time.time():
resp = self._sess.post(
API_BASE + "/v1/authenticate",
data={"client_id": self.client_id, "client_secret": self.client_secret},
)
resp.raise_for_status()
self._token = resp.json()
return self._token["access_token"]
async def streaming_transcribe(self, filename, config=None):
if config is None:
config = dict(
sample_rate="8000",
encoding="LINEAR16",
use_itn="true",
use_disfluency_filter="false",
use_profanity_filter="false",
)
STREAMING_ENDPOINT = "wss://{}/v1/transcribe:streaming?{}".format(
API_BASE.split("://")[1], "&".join(map("=".join, config.items()))
)
conn_kwargs = dict(extra_headers={"Authorization": "bearer " + self.token})
async def streamer(websocket):
with open(filename, "rb") as f:
while True:
buff = f.read(DEFAULT_BUFFER_SIZE)
if buff is None or len(buff) == 0:
break
await websocket.send(buff)
await websocket.send("EOS")
async def transcriber(websocket):
async for msg in websocket:
msg = json.loads(msg)
print(msg)
if msg["final"]:
print("final ended with " + msg["alternatives"][0]["text"])
async with websockets.connect(STREAMING_ENDPOINT, **conn_kwargs) as websocket:
await asyncio.gather(
streamer(websocket),
transcriber(websocket),
)
if __name__ == "__main__":
CLIENT_ID = "{YOUR_CLIENT_ID}"
CLIENT_SECRET = "{YOUR_CLIENT_SECRET}"
client = VITOOpenAPIClient(CLIENT_ID, CLIENT_SECRET)
fname = "sample.wav"
asyncio.run(client.streaming_transcribe(fname))
// full version
// https://github.com/vito-ai/java-sample
package ai.vito.openapi.stream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class VitoSttWebSocketClient {
public static String getAccessToken() throws IOException {
OkHttpClient client = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add("client_id", "{YOUR_CLIENT_ID}")
.add("client_secret", "{YOUR_CLIENT_SECRET}")
.build();
Request request = new Request.Builder()
.url("https://openapi.vito.ai/v1/authenticate")
.post(formBody)
.build();
Response response = client.newCall(request).execute();
ObjectMapper objectMapper = new ObjectMapper();
HashMap<String,String> map = objectMapper.readValue(response.body().string(), HashMap.class);
return map.get("access_token");
}
public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient();
String token = getAccessToken();
HttpUrl.Builder httpBuilder = HttpUrl.get("https://openapi.vito.ai/v1/transcribe:streaming").newBuilder();
httpBuilder.addQueryParameter("sample_rate","8000");
httpBuilder.addQueryParameter("encoding","LINEAR16");
httpBuilder.addQueryParameter("use_itn","true");
httpBuilder.addQueryParameter("use_disfluency_filter","true");
httpBuilder.addQueryParameter("use_profanity_filter","true");
String url = httpBuilder.toString().replace("https://", "wss://");
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization","Bearer "+token)
.build();
VitoWebSocketListener webSocketListener = new VitoWebSocketListener();
WebSocket vitoWebSocket = client.newWebSocket(request, webSocketListener);
File file = new File("sample.wav");
AudioInputStream in = AudioSystem.getAudioInputStream(file);
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
vitoWebSocket.send(ByteString.of(buffer));
}
client.dispatcher().executorService().shutdown();
}
}
class VitoWebSocketListener extends WebSocketListener {
private static final Logger logger = Logger.getLogger(VitoSttGrpcClient.class.getName());
private static final int NORMAL_CLOSURE_STATUS = 1000;
private static void log(Level level, String msg, Object... args) {
logger.log(level, msg, args);
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
log(Level.INFO, "Open " + response.message());
}
@Override
public void onMessage(WebSocket webSocket, String text) {
System.out.println(text);
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
System.out.println(bytes.hex());
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
webSocket.close(NORMAL_CLOSURE_STATUS, null);
log(Level.INFO, "Closing", code, reason);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
t.printStackTrace();
}
}
오류 코드
스트리밍 STT - WebSocket 의 오류 처리는 Dial시 응답값의 StatusCode로 오류를 처리 합니다.
HttpStatus | Code | Notes |
---|---|---|
400 | H0001 | 잘못된 파라미터 요청 |
401 | H0002 | 인증실패 |
429 | A0001 | 사용량 초과 |
500 | E500 | 서버 오류 |
참고사항
오디오 파일을 텍스트로 변환할 경우, 스트리밍 STT API를 이용하여 처리할 수도 있지만 일반 STT 가이드 문서에서 기술된 것처럼 일반 STT API로 변환 작업을 수행하는 것이 더 편리합니다.