Skip to content

Commit

Permalink
[merge]: STT ๊ตฌํ˜„ (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
kanguk01 authored Oct 24, 2024
2 parents 61f612d + b0d810e commit 492f980
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 2 deletions.
42 changes: 41 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
id 'com.google.protobuf' version '0.9.4'

}

group = 'com.splanet'
Expand Down Expand Up @@ -38,7 +40,19 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.hibernate.validator:hibernate-validator:8.0.0.Final'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// gRPC ๋ฐ Protocol Buffers ์˜์กด์„ฑ
implementation 'io.grpc:grpc-netty-shaded:1.56.1'
implementation 'io.grpc:grpc-protobuf:1.56.1'
implementation 'io.grpc:grpc-stub:1.56.1'
implementation 'com.google.protobuf:protobuf-java:3.23.4'

// gRPC ๊ด€๋ จ ํ•„์š”ํ•œ ์˜์กด์„ฑ
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'com.google.code.gson:gson:2.8.9'


compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
Expand All @@ -52,3 +66,29 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
}

protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.23.4'
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.66.0'
}
}
generateProtoTasks {
all().forEach { task ->
task.plugins {
grpc {}
}
}
}
}

sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/java', 'build/generated/source/proto/main/grpc'
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/splanet/splanet/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.splanet.splanet.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) { //์ธํ„ฐํŽ˜์ด์Šค WebMvcConfigurer ์ƒ์†
registry.addMapping("/**") //๋ชจ๋“  ๊ฒฝ๋กœ๋ฅผ ํ—ˆ์šฉํ•ด์ค„๊ฒƒ์ด๋ฏ€๋กœ
.allowedOrigins("*") //๋ฆฌ์†Œ์Šค ๊ณต์œ  ํ—ˆ๋ฝํ•  origin ์ง€์ •
.allowedMethods("*"); //๋ชจ๋“  ๋ฉ”์†Œ๋“œ๋ฅผ ํ—ˆ์šฉ
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/splanet/splanet/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.splanet.splanet.config;

import com.splanet.splanet.core.handler.SpeechWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

private final SpeechWebSocketHandler speechWebSocketHandler;

public WebSocketConfig(SpeechWebSocketHandler speechWebSocketHandler) {
this.speechWebSocketHandler = speechWebSocketHandler;
}

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(speechWebSocketHandler, "/ws/stt")
.setAllowedOrigins("*");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.splanet.splanet.core.handler;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.ByteString;
import com.nbp.cdncp.nest.grpc.proto.v1.NestResponse;
import com.splanet.splanet.stt.service.ClovaSpeechGrpcService;
import io.grpc.stub.StreamObserver;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.BinaryWebSocketHandler;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

@Component
public class SpeechWebSocketHandler extends BinaryWebSocketHandler {

private final ClovaSpeechGrpcService clovaSpeechGrpcService;
private final Map<String, StreamObserver<ByteString>> clientObservers = new ConcurrentHashMap<>();

public SpeechWebSocketHandler(ClovaSpeechGrpcService clovaSpeechGrpcService) {
this.clovaSpeechGrpcService = clovaSpeechGrpcService;
}

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// ์„ธ์…˜์ด ์—ด๋ฆด ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด gRPC ์ŠคํŠธ๋ฆผ์„ ์ƒ์„ฑ
StreamObserver<NestResponse> responseObserver = new StreamObserver<NestResponse>() {
@Override
public void onNext(NestResponse value) {
// ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์‘๋‹ต ์ฒ˜๋ฆฌ
try {
String contents = value.getContents(); // JSON ๋ฌธ์ž์—ด

// JSON ํŒŒ์‹ฑ
JsonParser parser = new JsonParser();
JsonObject jsonObject = parser.parse(contents).getAsJsonObject();

if (jsonObject.has("transcription")) {
JsonObject transcription = jsonObject.getAsJsonObject("transcription");
String text = transcription.get("text").getAsString();
// ํด๋ผ์ด์–ธํŠธ๋กœ text ํ•„๋“œ๋งŒ ์ „์†ก
session.sendMessage(new TextMessage(text));
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void onError(Throwable t) {
t.printStackTrace();
try {
session.sendMessage(new TextMessage("์˜ค๋ฅ˜ ๋ฐœ์ƒ: " + t.getMessage()));
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void onCompleted() {
// ์ŠคํŠธ๋ฆผ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};

// ์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•  StreamObserver ์ƒ์„ฑ
StreamObserver<ByteString> requestObserver = clovaSpeechGrpcService.recognize(responseObserver);
clientObservers.put(session.getId(), requestObserver);
}

@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
// ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ๋ฅผ gRPC ์„œ๋น„์Šค๋กœ ์ „๋‹ฌ
StreamObserver<ByteString> requestObserver = clientObservers.get(session.getId());
if (requestObserver != null) {
byte[] audioData = message.getPayload().array();
ByteString audioChunk = ByteString.copyFrom(audioData);
requestObserver.onNext(audioChunk);
}
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// ์„ธ์…˜์ด ์ข…๋ฃŒ๋˜๋ฉด gRPC ์ŠคํŠธ๋ฆผ๋„ ์ข…๋ฃŒ
StreamObserver<ByteString> requestObserver = clientObservers.remove(session.getId());
if (requestObserver != null) {
requestObserver.onCompleted();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.splanet.splanet.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "clova.speech")
public class ClovaProperties {
private String clientSecret;
private String language;
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private boolean isApiPath(String requestURI) {
}

private boolean isExemptedPath(String requestURI) {
return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token");
return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token") || requestURI.startsWith("/api/stt");
}

private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.splanet.splanet.stt.service;

import com.google.protobuf.ByteString;
import com.nbp.cdncp.nest.grpc.proto.v1.*;
import com.splanet.splanet.core.properties.ClovaProperties;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.MetadataUtils;
import io.grpc.stub.StreamObserver;
import org.springframework.stereotype.Service;

@Service
public class ClovaSpeechGrpcService implements ClovaSpeechService {

private final NestServiceGrpc.NestServiceStub nestServiceStub;
private final ClovaProperties clovaProperties;

public ClovaSpeechGrpcService(ClovaProperties clovaProperties) {
this.clovaProperties = clovaProperties;

// gRPC ์ฑ„๋„ ์ƒ์„ฑ
ManagedChannel channel = NettyChannelBuilder
.forAddress("clovaspeech-gw.ncloud.com", 50051)
.useTransportSecurity()
.build();

// Stub ์ƒ์„ฑ ๋ฐ ์ธ์ฆ ์ •๋ณด ์„ค์ •
NestServiceGrpc.NestServiceStub stub = NestServiceGrpc.newStub(channel);
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + clovaProperties.getClientSecret());
this.nestServiceStub = MetadataUtils.attachHeaders(stub, metadata);
}

@Override
public StreamObserver<ByteString> recognize(StreamObserver<NestResponse> responseObserver) {
StreamObserver<NestRequest> requestObserver = nestServiceStub.recognize(responseObserver);

// Config ๋ฉ”์‹œ์ง€ ์ „์†ก
requestObserver.onNext(createConfigRequest(clovaProperties.getLanguage()));

return new StreamObserver<ByteString>() {
private int sequenceId = 0;

@Override
public void onNext(ByteString audioChunk) {
NestRequest dataRequest = createDataRequest(audioChunk, sequenceId, false);
requestObserver.onNext(dataRequest);
sequenceId++;
}

@Override
public void onError(Throwable t) {
t.printStackTrace();
requestObserver.onError(t);
}

@Override
public void onCompleted() {
requestObserver.onCompleted();
}
};
}

// Config ์„ค์ •
private NestRequest createConfigRequest(String language) {
NestConfig config = NestConfig.newBuilder()
.setConfig("{\"transcription\":{\"language\":\"" + language + "\"}}")
.build();

return NestRequest.newBuilder()
.setType(RequestType.CONFIG)
.setConfig(config)
.build();
}

// ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
private NestRequest createDataRequest(ByteString audioChunk, int sequenceId, boolean epFlag) {
NestData data = NestData.newBuilder()
.setChunk(audioChunk)
.setExtraContents("{\"seqId\":" + sequenceId + ",\"epFlag\":" + epFlag + "}")
.build();

return NestRequest.newBuilder()
.setType(RequestType.DATA)
.setData(data)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.splanet.splanet.stt.service;

import com.google.protobuf.ByteString;
import com.nbp.cdncp.nest.grpc.proto.v1.NestResponse;
import io.grpc.stub.StreamObserver;

public interface ClovaSpeechService {
StreamObserver<ByteString> recognize(StreamObserver<NestResponse> responseObserver);
}
34 changes: 34 additions & 0 deletions src/main/proto/nest.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
syntax = "proto3";
option java_multiple_files = true;

package com.nbp.cdncp.nest.grpc.proto.v1;

enum RequestType {
CONFIG = 0;
DATA = 1;
}

message NestConfig {
string config = 1;
}

message NestData {
bytes chunk = 1;
string extra_contents = 2;
}

message NestRequest {
RequestType type = 1;
oneof part {
NestConfig config = 2;
NestData data = 3;
}
}

message NestResponse {
string contents = 1;
}

service NestService {
rpc recognize(stream NestRequest) returns (stream NestResponse) {}
}

0 comments on commit 492f980

Please sign in to comment.