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
404 lines
12 KiB
Python
Executable File
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
|