Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate the ExtractorDecider #101

Merged
merged 6 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docker/docker-compose.base.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.7'

services:
base:
image: vvd-env
Expand Down
2 changes: 0 additions & 2 deletions docker/docker-compose.debug-test-all.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# this compose file should be run together with .base.yml file
version: '3.7'

services:
base:
container_name: vvd-debug-test-all
Expand Down
2 changes: 0 additions & 2 deletions docker/docker-compose.test-all.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# this compose file should be run together with .base.yml file
version: '3.7'

services:
base:
container_name: vvd-test-all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mikufan.cx.vvd.extractor.component

import mikufan.cx.inlinelogging.KInlineLogging
import mikufan.cx.vvd.common.exception.RuntimeVocaloidException
import mikufan.cx.vvd.extractor.component.extractor.base.BaseAudioExtractor
import mikufan.cx.vvd.extractor.component.extractor.impl.AacToM4aAudioExtractor
import mikufan.cx.vvd.extractor.component.extractor.impl.OpusToOggAudioExtractor
import mikufan.cx.vvd.extractor.config.IOConfig
Expand All @@ -19,53 +20,68 @@ import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.notExists

@Component
@Order(OrderConstants.EXTRACTOR_DECIDER_ORDER)
class ExtractorDecider(
private val extractorDeciderCore: ExtractorDeciderCore
) : RecordProcessor<VSongTask, VSongTask> {

override fun processRecord(record: Record<VSongTask>): Record<VSongTask> {
val task = record.payload
val extractor = extractorDeciderCore.decideExtractor(
audioFileName = task.label.audioFileName,
videoFileName = task.label.pvFileName,
baseFileName = task.parameters.songProperFileName.toString()
)
task.parameters.chosenAudioExtractor = Optional.ofNullable(extractor)
return record
}
}


/**
* @date 2022-07-01
* @author CX无敌
*/
@Component
@Order(OrderConstants.EXTRACTOR_DECIDER_ORDER)
class ExtractorDecider(
class ExtractorDeciderCore(
ioConfig: IOConfig,
private val audioMediaFormatChecker: MediaFormatChecker,
private val ctx: ApplicationContext,
) : RecordProcessor<VSongTask, VSongTask> {
) {

private val inputDirectory = ioConfig.inputDirectory

override fun processRecord(record: Record<VSongTask>): Record<VSongTask> {
val baseFileName = record.payload.parameters.songProperFileName
fun decideExtractor(audioFileName: String? = null, videoFileName: String, baseFileName: String): BaseAudioExtractor? {
log.info { "Start deciding the best audio extractor for $baseFileName" }
// if the label contains a valid audio file, then skip extraction
val audioFileName: String? = record.payload.label.audioFileName

if (!audioFileName.isNullOrBlank()) {
val audioFile = inputDirectory / audioFileName
if (audioFile.exists()) {
log.info { "Skip choosing audio extractor for $baseFileName as it contains an audio file $audioFile" }
record.payload.parameters.chosenAudioExtractor = Optional.empty()
return record
return null
} else {
log.warn { "Audio file $audioFileName is declared in label json file but doesn't exist in inout directory, trading as no audio file" }
log.warn { "Audio file $audioFileName is declared but doesn't exist in input directory, treating as no audio file" }
}
}

val pvFile = inputDirectory / record.payload.label.pvFileName
val pvFile = inputDirectory / videoFileName
if (pvFile.notExists()) {
throw RuntimeVocaloidException(
"pv file not found: ${pvFile.absolute()} for song $baseFileName. " +
"Nor does it has a valid audio file."
)
}

val chosenAudioExtractor = when (val audioFormat = audioMediaFormatChecker.checkAudioFormat(pvFile)) {
return when (val audioFormat = audioMediaFormatChecker.checkAudioFormat(pvFile)) {
"aac" -> ctx.getBean<AacToM4aAudioExtractor>()
"opus" -> ctx.getBean<OpusToOggAudioExtractor>()
else -> throw RuntimeVocaloidException("Unsupported audio format $audioFormat for song $baseFileName")
}.also {
log.info { "Decided to use ${it.name} to extract audio from $baseFileName" }
}
log.info { "Decided to use ${chosenAudioExtractor.name} to extract audio from $baseFileName" }
record.payload.parameters.chosenAudioExtractor = Optional.of(chosenAudioExtractor)
return record
}
}


private val log = KInlineLogging.logger()
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import kotlin.io.path.deleteIfExists
import kotlin.io.path.exists

/**
* The lossless audio extractor for any video with AAC LC audio track.
* The lossless audio extractor for video with AAC LC audio track.
* Extracted audio will be in m4a format.
*
* It execute two commands:
* 1. ffmpeg -i input.mp4 -vn -acodec copy -y temp.aac
* 2. ffmpeg -i temp.aac -vn --acodec copy -y -movflags +faststart output.m4a
* It executes two commands:
* 1. `ffmpeg -i input.mp4 -vn -acodec copy -y temp.aac` to extract the raw audio stream
* 2. `ffmpeg -i temp.aac -vn --acodec copy -y -movflags +faststart output.m4a`
* to package the audio stream into m4a container with iTunes style faststart flag
*
* @date 2022-07-16
* @author CX无敌
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import java.util.concurrent.ThreadPoolExecutor
import kotlin.io.path.exists

/**
* The lossless audio extractor for any video with opus audio track (or any ogg/opus related audio codec).
* The lossless audio extractor for video with opus audio track (or any ogg/opus related audio codec).
* Extracted audio will be in ogg format.
* Although it is preferred to extracted as opus, but since NetEase Cloud Music does not support opus, it will be extracted as ogg.
* Although it is preferred to be extracted as opus,
* but since NetEase Cloud Music does not support opus, it will be extracted as ogg.
*
* It will run this command:
* ffmpeg -i input.mkv -vn -acodec copy output.ogg
* `ffmpeg -i input.mkv -vn -acodec copy output.ogg`
*
* @date 2022-07-16
* @author CX无敌
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package mikufan.cx.vvd.extractor.component

import io.kotest.assertions.fail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.every
import io.mockk.mockk
import mikufan.cx.vvd.common.exception.RuntimeVocaloidException
import mikufan.cx.vvd.extractor.component.extractor.base.BaseAudioExtractor
import mikufan.cx.vvd.extractor.component.extractor.impl.AacToM4aAudioExtractor
import mikufan.cx.vvd.extractor.component.extractor.impl.OpusToOggAudioExtractor
import mikufan.cx.vvd.extractor.config.IOConfig
import org.springframework.beans.factory.getBean
import org.springframework.context.ApplicationContext
import java.nio.file.Files
import kotlin.io.path.createFile
import kotlin.io.path.deleteExisting
import kotlin.io.path.div

class ExtractorDeciderCoreTest : ShouldSpec({

context("extractor decider") {
val tempInputDir = Files.createTempDirectory("extractor-core-test-")
val ioConfig = mockk<IOConfig> {
every { inputDirectory } returns tempInputDir
}
val baseInputFileName = "fake input file"

val mockCtx = mockk<ApplicationContext> {
every { getBean<AacToM4aAudioExtractor>() } returns mockk {
every { name } returns "Mock AAC to M4A Audio Extractor"
}
every { getBean<OpusToOggAudioExtractor>() } returns mockk {
every { name } returns "Mock Opus to Ogg Audio Extractor"
}
}

should("not set audio extractor if using audio file") {
val audioFileName = "$baseInputFileName.aac"
val audioFile = tempInputDir / audioFileName
audioFile.createFile()

val extractorDeciderCore = ExtractorDeciderCore(ioConfig, mockk(), mockk())

val decideExtractor: BaseAudioExtractor? = extractorDeciderCore.decideExtractor(audioFileName, "", baseInputFileName)

decideExtractor.shouldBeNull()
audioFile.deleteExisting()
}

context("on pv files") {
listOf("aac", "opus").forEach { format ->
val mockChecker = mockk<MediaFormatChecker> {
every { checkAudioFormat(any()) } returns format
}

val pvFileName = "$baseInputFileName.mp4"
val pvFile = tempInputDir / pvFileName
pvFile.createFile()

should("set the correct extractor for $format format") {
val extractorDeciderCore = ExtractorDeciderCore(ioConfig, mockChecker, mockCtx)
val decideExtractor: BaseAudioExtractor? = extractorDeciderCore.decideExtractor("", pvFileName, baseInputFileName)
decideExtractor.shouldNotBeNull()
when (format) {
"aac" -> decideExtractor.shouldBeInstanceOf<AacToM4aAudioExtractor>()
"opus" -> decideExtractor.shouldBeInstanceOf<OpusToOggAudioExtractor>()
else -> fail("Unknown format $format")
}
}
pvFile.deleteExisting()
}
}

should("fails if encounter an unknown pv file format") {
val mockChecker = mockk<MediaFormatChecker> {
every { checkAudioFormat(any()) } returns "wired format"
}

val pvFileName = "$baseInputFileName.mp4"
val pvFile = tempInputDir / pvFileName
pvFile.createFile()

val extractorDeciderCore = ExtractorDeciderCore(ioConfig, mockChecker, mockCtx)
val exception = shouldThrow<RuntimeVocaloidException> {
extractorDeciderCore.decideExtractor("", pvFileName, baseInputFileName)
}
exception.message shouldContain "Unsupported audio format"

pvFile.deleteExisting()
}

should("fails if neither audio file nor pv file exists") {
val extractorDeciderCore = ExtractorDeciderCore(ioConfig, mockk(), mockk())
val exception = shouldThrow<RuntimeVocaloidException> {
extractorDeciderCore.decideExtractor("fake.mp3", "fake.mp4", baseInputFileName)
}

exception.message shouldContain "pv file not found"
}
}
})
Loading