feat: 음성파일 병합 및 병합 시 파일 간 간격 지정에 사용되는 Ffmpeg 사용 기능 구현

This commit is contained in:
이상진 2025-08-20 21:15:20 +09:00
parent fc9331d411
commit 67c724ddbd

View File

@ -0,0 +1,94 @@
package com.sangdol.text_to_speech
import net.bramp.ffmpeg.FFmpeg
import net.bramp.ffmpeg.FFmpegExecutor
import net.bramp.ffmpeg.builder.FFmpegBuilder
import org.springframework.stereotype.Component
import java.io.File
private const val FFMPEG_PATH: String = "/opt/homebrew/bin/ffmpeg"
private const val SILENCE_AUDIO_PATH: String = "library/silences"
private fun convertSilenceAudioFilePath(durationMs: Long) = "$SILENCE_AUDIO_PATH/silence-${durationMs}ms.mp3"
@Component
class FfmpegUtils(
private val ffmpegExecutor: FFmpegExecutor = FFmpegExecutor(FFmpeg(FFMPEG_PATH))
) {
fun createSilenceAudio(durationMs: Long) {
val durationSeconds = durationMs / 1000.0
val outputFile = convertSilenceAudioFilePath(durationMs)
val command = listOf(
FFMPEG_PATH,
"-y",
"-f", "lavfi",
"-i", "anullsrc=r=44100:cl=stereo",
"-t", durationSeconds.toString(),
"-acodec", "mp3",
outputFile
)
val process = ProcessBuilder(command)
.redirectErrorStream(true)
.start()
process.inputStream.bufferedReader().forEachLine { println(it) }
process.waitFor()
.takeUnless { exitCode -> exitCode == 0 }
?: throw RuntimeException("FFmpeg failed with exit code ${process.exitValue()}")
}
fun mergeAudioWithInterval(intervalMs: Long, targetDirectory: String, saveDirectory: String, fileName: String) {
val targetDir = File(targetDirectory).also { validateIsValidFileDirectory(it) }
val fileNameRegex = """(\d+)(.*)""".toRegex()
if (intervalMs > 0 && !File(convertSilenceAudioFilePath(intervalMs)).exists()) {
createSilenceAudio(intervalMs)
}
val files = targetDir.listFiles().sortedBy { it ->
val matchResult: MatchResult = fileNameRegex.find(it.name)
?: throw IllegalStateException("Invalid filename-convention")
matchResult.groups[1]!!.value.toInt()
}.map { "$it" }
val inputs = mutableListOf<String>()
files.forEachIndexed { idx, file ->
inputs.add(file)
if (intervalMs > 0L && (idx != files.lastIndex)) {
inputs.add("library/silences/silence-${intervalMs}ms.mp3")
}
}
val builder = FFmpegBuilder()
inputs.forEach { filePath -> builder.addInput(filePath) }
val filterInputs = inputs.indices.joinToString("") { idx -> "[$idx:a]" }
val filterComplex = "$filterInputs concat=n=${inputs.size}:v=0:a=1[out]"
builder.setComplexFilter(filterComplex)
.addOutput("$saveDirectory/$fileName")
.setAudioCodec("mp3")
.addExtraArgs("-map", "[out]")
.done()
ffmpegExecutor.createJob(builder).run()
}
private fun validateIsValidFileDirectory(dir: File) {
if (!dir.exists()) {
throw IllegalArgumentException("Directory not found: $dir")
}
if (!dir.isDirectory) {
throw IllegalArgumentException("Path is not a directory: $dir")
}
val fileList = dir.listFiles()
if (fileList == null || fileList.isEmpty()) {
throw IllegalArgumentException("Directory is null or empty: $dir")
}
}
}