Skip to content

Commit 0a11b15

Browse files
authored
Merge pull request #47 from AI-Tutor-2024/develop
[DEPLOY]
2 parents 2c33c70 + 98f0ab6 commit 0a11b15

File tree

4 files changed

+115
-1
lines changed

4 files changed

+115
-1
lines changed

src/main/java/com/example/ai_tutor/domain/auth/application/AuthService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,22 @@ public ResponseEntity<?> signIn(SignInReq signInReq, @RequestHeader("Authorizati
148148
UsernamePasswordAuthenticationToken authentication =
149149
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
150150
SecurityContextHolder.getContext().setAuthentication(authentication);
151+
151152
// 5. JWT 토큰 생성 및 refresh 저장
152153
TokenMapping tokenMapping = customTokenProviderService.createToken(authentication);
153154

155+
// 기존 토큰이 있으면 삭제
156+
tokenRepository.findByUserEmail(user.getEmail())
157+
.ifPresent(tokenRepository::delete);
158+
159+
// 새 토큰 저장
154160
Token token = Token.builder()
155161
.userEmail(user.getEmail())
156162
.refreshToken(tokenMapping.getRefreshToken())
157163
.build();
158164
tokenRepository.save(token);
159165

166+
160167
// 6. 응답 구성
161168
AuthRes authResponse = AuthRes.builder()
162169
.accessToken(tokenMapping.getAccessToken())

src/main/java/com/example/ai_tutor/domain/note/application/ProfessorNoteService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.example.ai_tutor.global.DefaultAssert;
2323
import com.example.ai_tutor.global.config.security.token.UserPrincipal;
2424
import com.example.ai_tutor.global.payload.ApiResponse;
25+
import com.example.ai_tutor.global.util.ConvertToMp3;
2526
import com.fasterxml.jackson.databind.JsonNode;
2627
import lombok.RequiredArgsConstructor;
2728
import lombok.extern.slf4j.Slf4j;
@@ -30,6 +31,7 @@
3031
import org.springframework.transaction.annotation.Transactional;
3132
import org.springframework.web.multipart.MultipartFile;
3233

34+
import java.io.File;
3335
import java.util.Random;
3436
import java.util.List;
3537

@@ -50,6 +52,7 @@ public class ProfessorNoteService {
5052
private final ClovaService clovaService;
5153

5254
private final AmazonS3 amazonS3;
55+
private final ConvertToMp3 convertToMp3;
5356

5457
// 노트 생성
5558
@Transactional
@@ -89,7 +92,8 @@ public boolean convertSpeechToText(Long noteId, MultipartFile file) {
8992
try {
9093
// 3. CLOVA STT API 호출 (동기 처리)
9194
log.info("CLOVA STT 응답");
92-
JsonNode response = clovaService.processSpeechToText(file).block();
95+
File mp3File = convertToMp3.convert(file);
96+
JsonNode response = clovaService.processSpeechToText(mp3File).block();
9397
log.info("CLOVA STT 응답: {}", response);
9498

9599
// 4. STT 결과에서 텍스트 추출 및 저장

src/main/java/com/example/ai_tutor/domain/openAPI/clova/ClovaService.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.RequiredArgsConstructor;
55
import lombok.extern.slf4j.Slf4j;
66
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.core.io.FileSystemResource;
78
import org.springframework.http.HttpHeaders;
89
import org.springframework.http.MediaType;
910
import org.springframework.stereotype.Service;
@@ -13,6 +14,8 @@
1314
import org.springframework.web.reactive.function.client.WebClient;
1415
import reactor.core.publisher.Mono;
1516

17+
import java.io.File;
18+
1619
@Service
1720
@RequiredArgsConstructor
1821
@Slf4j
@@ -47,4 +50,30 @@ public Mono<JsonNode> processSpeechToText(MultipartFile file) {
4750
return Mono.error(new RuntimeException("Clova Speech API 호출 실패", error));
4851
});
4952
}
53+
54+
public Mono<JsonNode> processSpeechToText(File mp3File) {
55+
// 1. 파일을 Resource로 감싸기
56+
FileSystemResource resource = new FileSystemResource(mp3File);
57+
58+
// 2. 멀티파트 구성
59+
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
60+
body.add("media", resource);
61+
body.add("params", "{ \"language\": \"enko\", \"completion\": \"sync\" }");
62+
63+
return clovaWebClient.post()
64+
.uri(clovaUrl + "/recognizer/upload")
65+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
66+
.header("X-CLOVASPEECH-API-KEY", secret)
67+
.contentType(MediaType.MULTIPART_FORM_DATA)
68+
.bodyValue(body)
69+
.retrieve()
70+
.bodyToMono(JsonNode.class)
71+
.doOnSuccess(response -> log.info("Clova Speech API 응답 성공: {}", response))
72+
.doOnError(error -> log.error("Clova Speech API 호출 중 오류 발생: {}", error.getMessage()))
73+
.onErrorResume(error -> {
74+
log.error("Clova Speech API 호출 중 예외 처리 발생: {}", error.getMessage());
75+
return Mono.error(new RuntimeException("Clova Speech API 호출 실패", error));
76+
});
77+
}
78+
5079
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.example.ai_tutor.global.util;
2+
3+
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.multipart.MultipartFile;
7+
8+
import java.io.File;
9+
import java.io.IOException;
10+
import java.util.List;
11+
12+
@Slf4j
13+
@Component
14+
public class ConvertToMp3 {
15+
16+
private static final List<String> SUPPORTED_EXTENSIONS = List.of("mp4", "m4a", "wav");
17+
18+
/**
19+
* 확장자 확인
20+
*/
21+
private String getExtension(MultipartFile file) {
22+
String originalName = file.getOriginalFilename();
23+
if (originalName == null || !originalName.contains(".")) {
24+
throw new IllegalArgumentException("파일 이름에 확장자가 없습니다.");
25+
}
26+
return originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
27+
}
28+
29+
/**
30+
* 사용자가 업로드한 파일을 mp3로 변환하여 반환합니다.
31+
* @param inputFile 사용자로부터 업로드된 MultipartFile (.mp4, .m4a, .wav)
32+
* @return mp3로 변환된 File 객체
33+
*/
34+
public File convert(MultipartFile inputFile) throws IOException, InterruptedException {
35+
String ext = getExtension(inputFile);
36+
if (!SUPPORTED_EXTENSIONS.contains(ext)) {
37+
throw new IllegalArgumentException("지원하지 않는 확장자입니다: " + ext);
38+
}
39+
40+
// 임시 파일로 저장
41+
File inputTemp = File.createTempFile("input-", "." + ext);
42+
inputFile.transferTo(inputTemp);
43+
44+
// 출력될 mp3 임시 파일
45+
File outputMp3 = File.createTempFile("converted-", ".mp3");
46+
47+
// ffmpeg 명령어 실행
48+
ProcessBuilder pb = new ProcessBuilder(
49+
"ffmpeg", "-y", // 덮어쓰기 허용
50+
"-i", inputTemp.getAbsolutePath(),
51+
"-vn", // 영상 제거 (오디오만)
52+
"-acodec", "libmp3lame", // mp3 코덱
53+
outputMp3.getAbsolutePath()
54+
);
55+
56+
pb.redirectErrorStream(true);
57+
Process process = pb.start();
58+
int exitCode = process.waitFor();
59+
60+
// 임시 원본 파일은 삭제
61+
if (!inputTemp.delete()) {
62+
log.warn("임시 입력 파일 삭제 실패: {}", inputTemp.getAbsolutePath());
63+
}
64+
65+
if (exitCode != 0) {
66+
if (!outputMp3.delete()) {
67+
log.warn("실패한 mp3 파일 삭제 실패: {}", outputMp3.getAbsolutePath());
68+
}
69+
throw new RuntimeException("ffmpeg 변환 실패 (exitCode=" + exitCode + ")");
70+
}
71+
72+
return outputMp3;
73+
}
74+
}

0 commit comments

Comments
 (0)