Spaces:
Running
Running
Test background music side-chain ducking
Browse files- tests/test_mix_audio.py +33 -0
tests/test_mix_audio.py
CHANGED
|
@@ -95,3 +95,36 @@ async def test_add_audio_rejects_invalid_bitrate(tmp_path: Path) -> None:
|
|
| 95 |
sfx = _media(tmp_path / "one.wav", "one.wav", "audio/wav")
|
| 96 |
with pytest.raises(InputError, match="supported AAC bitrate"):
|
| 97 |
await add_audio(FakeFFmpeg(), [video, sfx], {"audio_bitrate": "999k"}, tmp_path / "out")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
sfx = _media(tmp_path / "one.wav", "one.wav", "audio/wav")
|
| 96 |
with pytest.raises(InputError, match="supported AAC bitrate"):
|
| 97 |
await add_audio(FakeFFmpeg(), [video, sfx], {"audio_bitrate": "999k"}, tmp_path / "out")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@pytest.mark.asyncio
|
| 101 |
+
async def test_duck_background_music_uses_voice_sidechain_and_preserves_video(tmp_path: Path) -> None:
|
| 102 |
+
from app.operations.mix_audio import duck_background_music
|
| 103 |
+
|
| 104 |
+
video = _media(
|
| 105 |
+
tmp_path / "voice.mp4", "voice.mp4", "video/mp4", duration=18.0,
|
| 106 |
+
metadata={"audio_streams": [{"codec": "aac"}]},
|
| 107 |
+
)
|
| 108 |
+
music = _media(tmp_path / "music.mp3", "music.mp3", "audio/mpeg")
|
| 109 |
+
ffmpeg = FakeFFmpeg()
|
| 110 |
+
result = await duck_background_music(
|
| 111 |
+
ffmpeg, [video, music], {"music_volume": 0.2, "ratio": 8, "attack": 20, "release": 300},
|
| 112 |
+
tmp_path / "out",
|
| 113 |
+
)
|
| 114 |
+
command = [str(value) for value in ffmpeg.args]
|
| 115 |
+
filt = command[command.index("-filter_complex") + 1]
|
| 116 |
+
assert "[music][voice]sidechaincompress=threshold=0.03:ratio=8:attack=20:release=300[ducked]" in filt
|
| 117 |
+
assert "[voice][ducked]amix=inputs=2:duration=first:normalize=0[mixed]" in filt
|
| 118 |
+
assert command[command.index("-c:v") + 1] == "copy"
|
| 119 |
+
assert result.metadata["mix_mode"] == "sidechain_ducking"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@pytest.mark.asyncio
|
| 123 |
+
async def test_duck_background_music_rejects_silent_video(tmp_path: Path) -> None:
|
| 124 |
+
from app.operations.mix_audio import duck_background_music
|
| 125 |
+
|
| 126 |
+
video = _media(tmp_path / "silent.mp4", "silent.mp4", "video/mp4", duration=8.0,
|
| 127 |
+
metadata={"audio_streams": []})
|
| 128 |
+
music = _media(tmp_path / "music.mp3", "music.mp3", "audio/mpeg")
|
| 129 |
+
with pytest.raises(InputError, match="requires narration audio"):
|
| 130 |
+
await duck_background_music(FakeFFmpeg(), [video, music], {}, tmp_path / "out")
|