feat: 음성파일 병합 및 병합 시 파일 간 간격 지정에 사용되는 Ffmpeg 사용 기능 구현
This commit is contained in:
parent
fc9331d411
commit
67c724ddbd
94
src/main/kotlin/com/sangdol/text_to_speech/FfmpegUtils.kt
Normal file
94
src/main/kotlin/com/sangdol/text_to_speech/FfmpegUtils.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user