From 90169020d1d21c6b196f781dc0f28aaf61a3fa1a Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:13:12 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Docs:=20build.gradle=EC=97=90=20m4a=20to?= =?UTF-8?q?=20wav=20=EB=B3=80=ED=99=98=EC=9D=84=20=EC=9C=84=ED=95=9C=20FFm?= =?UTF-8?q?peg=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) 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') { From 7885c1fe09fbfc566b3549e6ce5d082ac30adbc6 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:14:58 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Docker:=20=EB=8F=84=EC=BB=A4=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=EC=97=90=20ffmpeg=EB=A5=BC=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=ED=95=98=EB=8A=94=20RUN=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8f81a1d..5bc1b20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,6 @@ COPY build/libs/*.jar /app/app.jar EXPOSE 8080 +RUN apt-get update && apt-get install -y ffmpeg + ENTRYPOINT ["java", "-jar", "/app/app.jar", "--spring.config.additional-location=file:/app/config/"] From cb5445f6aec829505636fec295e1aaa0ecc11bd4 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:15:22 +0900 Subject: [PATCH 03/10] =?UTF-8?q?Feat:=20=EC=8B=B1=EA=B8=80=ED=86=A4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20FFmpeg=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20Bea?= =?UTF-8?q?n=EC=9C=BC=EB=A1=9C=20=EB=93=B1=EB=A1=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kkumteul/global/config/FFmpegDI.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java 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..aecd6a1 --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java @@ -0,0 +1,20 @@ +package com.capstone.kkumteul.global.config; + +import net.bramp.ffmpeg.FFmpeg; +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); + } + } +} From 3e36beb723ba0f89e0d0a6edb21567de1e757d19 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:53:20 +0900 Subject: [PATCH 04/10] =?UTF-8?q?Docker:=20COPY=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=EC=97=90=20RUN=EC=9D=B4=20=EC=8B=A4=ED=96=89=EB=90=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5bc1b20..7fa52fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +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 -RUN apt-get update && apt-get install -y ffmpeg ENTRYPOINT ["java", "-jar", "/app/app.jar", "--spring.config.additional-location=file:/app/config/"] From dd11240805c978b3780db27f541da430207591da Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:54:01 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Fix:=20MultipartFile=EC=9D=84=20=EB=B0=9B?= =?UTF-8?q?=EC=95=84=20.getInputstream()=EC=9D=84=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EA=B2=83=EC=9D=84=20ByteArrayInputStream?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95,=20content-type?= =?UTF-8?q?=EC=9D=80=20audio/wav=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kkumteul/domain/voice/service/S3Uploader.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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" ); } From f1d733fbb9bbfdd7775db5fbee899bae2d46bd42 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:54:34 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Fix:=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B7=B8=EB=8B=88=EC=B2=98=EB=A5=BC=20MultipartFile?= =?UTF-8?q?=EC=9D=84=20=EB=B0=9B=EB=8A=94=20=EA=B2=83=EC=9D=84=20byte[]?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD,=20originalFilename=EC=9D=84=20?= =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capstone/kkumteul/domain/voice/service/VoiceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From ee8b0bb60da16f15de84ca23e267b971ab01200d Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:54:58 +0900 Subject: [PATCH 07/10] =?UTF-8?q?Fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20Voic?= =?UTF-8?q?eService=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=98=A4=EB=B2=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kkumteul/domain/voice/service/VoiceServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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(); } From eeeb341518f2ed756ad814fd18a074d12ccdf1a8 Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:55:29 +0900 Subject: [PATCH 08/10] =?UTF-8?q?Fix:=20m4a=20=ED=8C=8C=EC=9D=BC=EC=9D=B4?= =?UTF-8?q?=20=EB=93=A4=EC=96=B4=EC=98=A4=EB=A9=B4=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=B4=20.wav=EC=9D=B8=20byte[]=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EC=97=AC=20service=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voice/web/controller/VoiceController.java | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) 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..96f5354 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(); + String originName = m4aFile.getOriginalFilename(); + + // not null validation if(originName == null) { throw new InvalidFileException(); } - if(wavFile.isEmpty() + // is invalid or not m4a file validtaion + if(m4aFile.isEmpty() || originName.isBlank() - || !originName.toLowerCase().endsWith(".wav")) + || !originName.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 = m4aFile.getOriginalFilename().replaceAll(".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, From 1b34ed5fd9a9ec67e20748176871489e4d720ece Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:55:44 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Feat:=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EB=B9=88=EC=9D=B8=20FFprobe=EB=A5=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/capstone/kkumteul/global/config/FFmpegDI.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java b/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java index aecd6a1..5ab71bb 100644 --- a/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java +++ b/src/main/java/com/capstone/kkumteul/global/config/FFmpegDI.java @@ -1,6 +1,7 @@ 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; @@ -17,4 +18,13 @@ public FFmpeg ffmpeg() { throw new RuntimeException(e); } } + + @Bean + public FFprobe ffprobe() { + try { + return new FFprobe("/usr/bin/ffprobe"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } From 2f463346579ec3f9a4a0b2cd499bd1d4c15112ac Mon Sep 17 00:00:00 2001 From: Joonseok-Lee Date: Mon, 18 May 2026 22:58:34 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Fix:=20.m4a=EB=A5=BC=20=EC=A0=95=EA=B7=9C?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD=ED=95=98=EB=8D=98=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=ED=99=95=EC=9E=A5=EC=9E=90=EB=A7=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voice/web/controller/VoiceController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 96f5354..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 @@ -40,17 +40,17 @@ public ResponseEntity> sendTtsRequestMessage( ) { // File validation - String originName = m4aFile.getOriginalFilename(); + String m4aFilename = m4aFile.getOriginalFilename(); // not null validation - if(originName == null) { + if(m4aFilename == null) { throw new InvalidFileException(); } // is invalid or not m4a file validtaion if(m4aFile.isEmpty() - || originName.isBlank() - || !originName.toLowerCase().endsWith(".m4a")) + || m4aFilename.isBlank() + || !m4aFilename.toLowerCase().endsWith(".m4a")) throw new InvalidFileException(); byte[] wavFile; @@ -60,7 +60,7 @@ public ResponseEntity> sendTtsRequestMessage( } catch (IOException e) { throw new RuntimeException(e); } - String originalFilename = m4aFile.getOriginalFilename().replaceAll(".m4a", ".wav"); + String originalFilename = m4aFilename.replace(".m4a", ".wav"); voiceService.saveWav(wavFile, originalFilename, user);