diff --git a/Dockerfile b/Dockerfile index 8f81a1d..7fa52fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,11 @@ FROM eclipse-temurin:21-jre WORKDIR /app +RUN apt-get update && apt-get install -y ffmpeg + COPY build/libs/*.jar /app/app.jar EXPOSE 8080 + ENTRYPOINT ["java", "-jar", "/app/app.jar", "--spring.config.additional-location=file:/app/config/"] diff --git a/build.gradle b/build.gradle index 0557c30..84eee04 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,10 @@ dependencies { // S3 (latest dependency) implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2' + + // m4a to wav + implementation 'net.bramp.ffmpeg:ffmpeg:0.9.1' + } tasks.named('test') { diff --git a/src/main/java/com/capstone/kkumteul/domain/voice/service/S3Uploader.java b/src/main/java/com/capstone/kkumteul/domain/voice/service/S3Uploader.java index 4547d7a..cfb39f6 100644 --- a/src/main/java/com/capstone/kkumteul/domain/voice/service/S3Uploader.java +++ b/src/main/java/com/capstone/kkumteul/domain/voice/service/S3Uploader.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.UUID; @@ -30,14 +31,14 @@ public S3Uploader( } // upload S3 bucket - public String upload(MultipartFile wavFile, User user) throws IOException { + public String upload(byte[] wavFile, String originalFilename, User user) throws IOException { return putS3( - convert(wavFile), + new ByteArrayInputStream(wavFile), createFilename( - wavFile.getOriginalFilename(), + originalFilename, user.getId() ), - wavFile.getContentType() + "audio/wav" ); } diff --git a/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceService.java b/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceService.java index e126ec9..e827b7c 100644 --- a/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceService.java +++ b/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceService.java @@ -5,7 +5,7 @@ import org.springframework.web.multipart.MultipartFile; public interface VoiceService { - Void saveWav(MultipartFile wavFile, User user); + Void saveWav(byte[] wavFile, String originalFilename, User user); Boolean hasTtsHistory(Long userId, Long paragraphId); TtsFileResponse getTtsFile(Long userId, Long paragraphId); Void createTtsFile(Long userId, Long paragraphId); diff --git a/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceServiceImpl.java b/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceServiceImpl.java index 05428b5..6a1144f 100644 --- a/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceServiceImpl.java +++ b/src/main/java/com/capstone/kkumteul/domain/voice/service/VoiceServiceImpl.java @@ -35,14 +35,14 @@ public class VoiceServiceImpl implements VoiceService { @Override @Transactional - public Void saveWav(MultipartFile wavFile, User user) { + public Void saveWav(byte[] wavFile, String originalFilename, User user) { String uploadedUrl; try { - uploadedUrl = s3Uploader.upload(wavFile, user); + uploadedUrl = s3Uploader.upload(wavFile, originalFilename, user); } catch (IOException e) { - log.error("VoiceService error occurred. userId = {}, filename = {}", user.getId(), wavFile.getOriginalFilename()); + log.error("VoiceService error occurred. userId = {}, filename = {}", user.getId(), originalFilename); throw new FileUploadFailException(); } diff --git a/src/main/java/com/capstone/kkumteul/domain/voice/web/controller/VoiceController.java b/src/main/java/com/capstone/kkumteul/domain/voice/web/controller/VoiceController.java index ccabd15..1c4d2f0 100644 --- a/src/main/java/com/capstone/kkumteul/domain/voice/web/controller/VoiceController.java +++ b/src/main/java/com/capstone/kkumteul/domain/voice/web/controller/VoiceController.java @@ -8,11 +8,20 @@ import com.capstone.kkumteul.global.security.AuthUser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.bramp.ffmpeg.FFmpeg; +import net.bramp.ffmpeg.FFmpegExecutor; +import net.bramp.ffmpeg.FFprobe; +import net.bramp.ffmpeg.builder.FFmpegBuilder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + @Slf4j @RestController // not hard-fix configuration convention @@ -21,30 +30,73 @@ public class VoiceController { private final VoiceService voiceService; + private final FFmpeg ffmpeg; + private final FFprobe ffprobe; @PostMapping public ResponseEntity> sendTtsRequestMessage( @AuthUser User user, - @RequestPart MultipartFile wavFile + @RequestPart MultipartFile m4aFile ) { // File validation - String originName = wavFile.getOriginalFilename(); - if(originName == null) { + String m4aFilename = m4aFile.getOriginalFilename(); + + // not null validation + if(m4aFilename == null) { throw new InvalidFileException(); } - if(wavFile.isEmpty() - || originName.isBlank() - || !originName.toLowerCase().endsWith(".wav")) + // is invalid or not m4a file validtaion + if(m4aFile.isEmpty() + || m4aFilename.isBlank() + || !m4aFilename.toLowerCase().endsWith(".m4a")) throw new InvalidFileException(); - voiceService.saveWav(wavFile, user); + byte[] wavFile; + + try { + wavFile = convertM4aToWav(m4aFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + String originalFilename = m4aFilename.replace(".m4a", ".wav"); + + voiceService.saveWav(wavFile, originalFilename, user); return ResponseEntity.status(HttpStatus.CREATED) .body(SuccessResponse.created(user.getId())); } + private byte[] convertM4aToWav(MultipartFile m4aFile) throws IOException { + Path in = Files.createTempFile("audio-", "m4a"); + Path out = Files.createTempFile("audio-", "wav"); + + try { + Files.copy(m4aFile.getInputStream(), in, StandardCopyOption.REPLACE_EXISTING); + + FFmpegBuilder ffmpegBuilder = new FFmpegBuilder() + .setInput(in.toString()) + .done() + .overrideOutputFiles(true) + .addOutput(out.toString()) + .setFormat("wav") + .setAudioCodec("pcm_s16le") + .setAudioChannels(1) + .setAudioSampleRate(16_000) + .done(); + + new FFmpegExecutor(ffmpeg, ffprobe) + .createJob(ffmpegBuilder) + .run(); + + return Files.readAllBytes(out); + } finally { + Files.deleteIfExists(in); + Files.deleteIfExists(out); + } + } + @GetMapping("/{paragraphId}") public ResponseEntity> getTtsFile( @AuthUser User user, diff --git a/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java b/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java new file mode 100644 index 0000000..5ab71bb --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java @@ -0,0 +1,30 @@ +package com.capstone.kkumteul.global.config; + +import net.bramp.ffmpeg.FFmpeg; +import net.bramp.ffmpeg.FFprobe; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class FFmpegDI { + + @Bean + public FFmpeg ffmpeg() { + try { + return new FFmpeg("/usr/bin/ffmpeg"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Bean + public FFprobe ffprobe() { + try { + return new FFprobe("/usr/bin/ffprobe"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}