Skip to content

Commit 17cf7c9

Browse files
committed
11.25 17시
STT 컨트롤러가 텍스트 변환 결과를 프런트가 아닌 gemini에게 전달한 후 레시피 결과를 반환하도록 수정하고 stt와 gemini 도메인 통합함.
1 parent 0000d63 commit 17cf7c9

5 files changed

Lines changed: 134 additions & 4 deletions

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
3434
compileOnly 'org.projectlombok:lombok'
3535
developmentOnly 'org.springframework.boot:spring-boot-devtools'
36+
implementation 'com.google.cloud:google-cloud-speech:4.40.0' // google stt
3637
runtimeOnly 'com.mysql:mysql-connector-j'
3738
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
3839
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.webservice.algorithmchef.controller;
2+
3+
import com.webservice.algorithmchef.dto.gemini.GeminiRecipeResponse;
4+
import com.webservice.algorithmchef.service.GeminiRecipeService;
5+
import com.webservice.algorithmchef.service.SpeechService;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.*;
11+
import org.springframework.web.multipart.MultipartFile;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
@Slf4j
17+
@RestController
18+
@RequestMapping("/api")
19+
@CrossOrigin(origins = "http://localhost:3000")
20+
@RequiredArgsConstructor
21+
public class SttController {
22+
23+
private final SpeechService speechService;
24+
private final GeminiRecipeService geminiRecipeService; // 레시피 서비스 객체
25+
26+
@PostMapping("/stt")
27+
public ResponseEntity<?> stt(
28+
@RequestParam("audio") MultipartFile audioFile,
29+
@RequestParam("userId") String userIdStr
30+
) {
31+
log.info("음성 기반 레시피 추천 요청 - UserId: {}, 파일크기: {} bytes", userIdStr, audioFile.getSize());
32+
33+
try {
34+
// 1. 음성 -> 텍스트 변환 (STT)
35+
String convertedText = speechService.stt(audioFile);
36+
37+
if (convertedText == null || convertedText.isEmpty()) {
38+
log.warn("STT 변환 실패: 음성 인식 결과 없음");
39+
return ResponseEntity.badRequest().body("음성을 인식하지 못했습니다. 다시 말씀해 주세요.");
40+
}
41+
42+
log.info("STT 변환 결과: \"{}\"", convertedText);
43+
44+
// userId String -> Long 변환
45+
Long userId = Long.parseLong(userIdStr);
46+
47+
// 음성 검색은 보통 '새로운 검색'이므로 excludedTitles는 빈 리스트로 전달
48+
List<String> excludedTitles = Collections.emptyList();
49+
50+
// recipeservice 호출
51+
List<GeminiRecipeResponse> recipes = geminiRecipeService.recommendCondition(
52+
userId,
53+
convertedText,
54+
excludedTitles
55+
);
56+
57+
log.info("음성 추천 완료 - {}개의 레시피 반환", recipes.size());
58+
59+
// 3. 레시피 리스트 반환
60+
return ResponseEntity.ok(recipes);
61+
62+
} catch (NumberFormatException e) {
63+
log.error("UserId 형식 오류: {}", userIdStr, e);
64+
return ResponseEntity.badRequest().body("유효하지 않은 사용자 ID입니다.");
65+
} catch (Exception e) {
66+
log.error("음성 레시피 추천 중 오류 발생", e);
67+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
68+
.body("처리 중 서버 오류가 발생했습니다: " + e.getMessage());
69+
}
70+
}
71+
}

src/main/java/com/webservice/algorithmchef/dto/gemini/GeminiRecipeResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public record GeminiRecipeResponse (
99
String portions,
1010
String time,
1111
String imageUrl,
12-
List<String> ingredients,
13-
List<String> instructions,
12+
String ingredients,
13+
String instructions,
1414
String tip,
1515
String type
1616
) {}

src/main/java/com/webservice/algorithmchef/service/GeminiRecipeService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ private List<GeminiRecipeResponse> generateAndCallGemini(
163163
"portions": "몇 인분"
164164
"time": "예상 조리 시간",
165165
"imageUrl" : "완성된 요리 사진 url 주소"
166-
"ingredients": ["재료1", "재료2"],
167-
"instructions": ["1. ...", "2. ..."],
166+
"ingredients": "재료1", "재료2",
167+
"instructions": "1. ..., 2. ...",
168168
"tip": "레시피에 관한 기타 참고사항 및 추천사항",
169169
"type": "gemini"
170170
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.webservice.algorithmchef.service;
2+
3+
import com.google.cloud.speech.v1.*;
4+
import com.google.protobuf.ByteString;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.web.multipart.MultipartFile;
8+
9+
import java.io.IOException;
10+
import java.util.List;
11+
12+
@Slf4j
13+
@Service
14+
public class SpeechService {
15+
16+
private final SpeechClient speechClient;
17+
18+
public SpeechService() throws IOException {
19+
this.speechClient = SpeechClient.create();
20+
}
21+
22+
public String stt(MultipartFile audioFile) throws IOException {
23+
byte[] data = audioFile.getBytes();
24+
ByteString audioBytes = ByteString.copyFrom(data);
25+
26+
RecognitionConfig config = RecognitionConfig.newBuilder()
27+
.setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
28+
.setSampleRateHertz(16000)
29+
.setLanguageCode("ko-KR")
30+
.build();
31+
32+
RecognitionAudio audio = RecognitionAudio.newBuilder()
33+
.setContent(audioBytes)
34+
.build();
35+
36+
try {
37+
RecognizeResponse response = speechClient.recognize(config, audio);
38+
List<SpeechRecognitionResult> results = response.getResultsList();
39+
40+
if (results.isEmpty()) {
41+
log.warn("STT 결과 없음 (Google API 응답이 비어있음)");
42+
return "";
43+
}
44+
45+
StringBuilder sb = new StringBuilder();
46+
for (SpeechRecognitionResult result : results) {
47+
SpeechRecognitionAlternative alternative = result.getAlternativesList().get(0);
48+
sb.append(alternative.getTranscript());
49+
}
50+
51+
return sb.toString();
52+
53+
} catch (Exception e) {
54+
log.error("Google Cloud Speech API 호출 실패", e);
55+
throw new IOException("Speech-to-Text fail", e);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)