Files
xiaoxia-saas/tests/unit/test_multi_track_mixer.py
T
CI Bot 2df7bc9dc8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
style: auto-format with black + isort + prettier
2026-07-24 13:35:05 +00:00

404 lines
12 KiB
Python
Executable File

"""
多轨道混音引擎配置与纯逻辑测试.
覆盖 AudioTrack.from_dict / MultiTrackMixConfig.from_config_dict / has_effect 等纯逻辑.
引擎核心混音方法依赖 FFmpeg,由集成测试覆盖.
"""
from __future__ import annotations
import pytest
from video_processing.multi_track_mixer import (
DEFAULT_VOLUMES,
MAX_AUDIO_TRACKS,
TRACK_TYPE_AMBIENT,
TRACK_TYPE_BGM,
TRACK_TYPE_MAIN,
TRACK_TYPE_SFX,
TRACK_TYPE_VOICEOVER,
AudioTrack,
MultiTrackMixConfig,
)
class TestTrackConstants:
"""轨道类型常量与默认值."""
def test_track_types_exist(self):
assert TRACK_TYPE_MAIN == "main"
assert TRACK_TYPE_BGM == "bgm"
assert TRACK_TYPE_VOICEOVER == "voiceover"
assert TRACK_TYPE_SFX == "sfx"
assert TRACK_TYPE_AMBIENT == "ambient"
def test_max_tracks(self):
assert MAX_AUDIO_TRACKS == 8
def test_default_volumes(self):
assert DEFAULT_VOLUMES[TRACK_TYPE_MAIN] == 1.0
assert DEFAULT_VOLUMES[TRACK_TYPE_BGM] == 0.3
assert DEFAULT_VOLUMES[TRACK_TYPE_VOICEOVER] == 1.0
assert DEFAULT_VOLUMES[TRACK_TYPE_SFX] == 0.7
assert DEFAULT_VOLUMES[TRACK_TYPE_AMBIENT] == 0.2
class TestAudioTrackFromDict:
"""AudioTrack.from_dict 构造逻辑."""
def test_basic(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
}
)
assert track.track_id == "t1"
assert track.track_type == "bgm"
assert track.audio_path == "/tmp/bgm.mp3"
assert track.volume == 0.3 # bgm 默认音量
def test_custom_volume(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "main",
"audio_path": "/tmp/main.wav",
"volume": 0.8,
}
)
assert track.volume == 0.8
def test_volume_clamped_to_zero(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": -1.0,
}
)
assert track.volume == 0.0
def test_volume_clamped_to_max(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": 3.0,
}
)
assert track.volume == 2.0
def test_invalid_volume_falls_back_to_default(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": "not_a_number",
}
)
assert track.volume == 0.3 # bgm 默认
def test_none_volume_falls_back(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "voiceover",
"audio_path": "/tmp/vo.wav",
"volume": None,
}
)
assert track.volume == 1.0 # voiceover 默认
def test_unknown_track_type_default_volume(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "unknown_type",
"audio_path": "/tmp/a.wav",
}
)
assert track.volume == 1.0 # 未知类型默认 1.0
def test_fade_in_fade_out(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": 1.5,
"fade_out": 2.0,
}
)
assert track.fade_in == 1.5
assert track.fade_out == 2.0
def test_negative_fade_clamped(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": -0.5,
"fade_out": -1.0,
}
)
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_invalid_fade_falls_back(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": "abc",
"fade_out": None,
}
)
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_start_time_and_duration(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"start_time": 5.0,
"duration": 3.0,
}
)
assert track.start_time == 5.0
assert track.duration == 3.0
def test_negative_start_time_clamped(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": -10.0,
"duration": -2.0,
}
)
assert track.start_time == 0.0
assert track.duration == 0.0
def test_invalid_time_values_fall_back(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": "invalid",
"duration": "bad",
}
)
assert track.start_time == 0.0
assert track.duration == 0.0
def test_enabled_default_true(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
}
)
assert track.enabled is True
def test_enabled_can_be_false(self):
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"enabled": False,
}
)
assert track.enabled is False
class TestMultiTrackMixConfigFromDict:
"""MultiTrackMixConfig.from_config_dict 构造逻辑."""
def test_none_returns_default(self):
cfg = MultiTrackMixConfig.from_config_dict(None)
assert cfg.tracks == []
assert cfg.master_volume == 1.0
assert cfg.normalize is True
def test_empty_dict_returns_default(self):
cfg = MultiTrackMixConfig.from_config_dict({})
assert cfg.tracks == []
def test_non_dict_returns_default(self):
cfg = MultiTrackMixConfig.from_config_dict([])
assert cfg.tracks == []
def test_single_track(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{
"track_id": "bgm1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": 0.5,
},
],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].track_id == "bgm1"
assert cfg.tracks[0].volume == 0.5
def test_multiple_tracks(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
],
}
)
assert len(cfg.tracks) == 3
assert cfg.tracks[0].track_type == "main"
assert cfg.tracks[1].track_type == "bgm"
assert cfg.tracks[2].track_type == "voiceover"
def test_disabled_tracks_filtered(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
],
}
)
assert len(cfg.tracks) == 2
assert all(t.track_id != "b" for t in cfg.tracks)
def test_empty_audio_path_filtered(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].track_id == "valid"
def test_invalid_tracks_skipped(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
"not_a_dict",
None,
{"no_audio_path": "xxx"},
],
}
)
assert len(cfg.tracks) == 1
def test_tracks_not_a_list(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": "not_a_list",
}
)
assert cfg.tracks == []
def test_master_volume(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": 0.8,
}
)
assert cfg.master_volume == 0.8
def test_master_volume_clamped(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": 3.0,
}
)
assert cfg.master_volume == 2.0
cfg2 = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": -1.0,
}
)
assert cfg2.master_volume == 0.0
def test_invalid_master_volume_falls_back(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": "abc",
}
)
assert cfg.master_volume == 1.0
def test_normalize_and_max_output(self):
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"normalize": False,
"max_output_volume": 2.0,
}
)
assert cfg.normalize is False
assert cfg.max_output_volume == 2.0
def test_default_values(self):
cfg = MultiTrackMixConfig.from_config_dict({"tracks": []})
assert cfg.master_volume == 1.0
assert cfg.normalize is True
assert cfg.max_output_volume == 1.5
class TestMultiTrackMixConfigProperties:
"""has_effect 属性."""
def test_has_effect_with_tracks(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
]
)
assert cfg.has_effect is True
def test_no_effect_empty(self):
cfg = MultiTrackMixConfig(tracks=[])
assert cfg.has_effect is False
def test_no_effect_all_disabled(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
]
)
assert cfg.has_effect is False
def test_no_effect_empty_paths(self):
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
]
)
assert cfg.has_effect is False