"""BGM 池差异化分配单元测试 (Issue #1767). 覆盖: - BGM 池定义(10 首,覆盖 4 种风格) - 风格匹配:get_bgm_pool_candidates 按 mood 筛选 - 变体分配:select_bgm_from_pool 基于 seed 可复现选择 - 批量分配:allocate_bgm_pool_for_variants 确保不同变体不同 BGM - 段落差异化:generate_bgm_segment_offset 生成不同偏移 - 音量微调:generate_bgm_volume_adjust ±3dB - 边界条件:空配置/未启用 BGM/未知 mood """ from __future__ import annotations import pytest from packages.domain.bgm_pool import ( BGM_POOL, STYLE_TO_MOOD, BGMPoolEntry, allocate_bgm_pool_for_variants, generate_bgm_segment_offset, generate_bgm_volume_adjust, get_bgm_pool_candidates, select_bgm_from_pool, ) class TestBGMPoolDefinition: """BGM 池定义测试。""" def test_pool_has_at_least_10_entries(self): """BGM 池至少 10 首(满足 5-10 首需求)。""" assert len(BGM_POOL) >= 10 def test_all_entries_have_required_fields(self): """每条 BGM 池条目都有 id/preset_id/mood/duration。""" for entry in BGM_POOL: assert entry.id, "Missing id" assert entry.preset_id, f"Missing preset_id in {entry.id}" assert entry.mood, f"Missing mood in {entry.id}" assert entry.duration > 0, f"Duration must be > 0 in {entry.id}" def test_pool_covers_multiple_moods(self): """池覆盖至少 3 种不同 mood。""" moods = {e.mood for e in BGM_POOL} assert len(moods) >= 3, f"Expected >= 3 moods, got {moods}" def test_each_mood_has_at_least_2_entries(self): """每种 mood 至少有 2 首(保证差异化有意义)。""" mood_counts: dict[str, int] = {} for entry in BGM_POOL: mood_counts[entry.mood] = mood_counts.get(entry.mood, 0) + 1 for mood, count in mood_counts.items(): assert count >= 2, f"Mood '{mood}' only has {count} entries (need >= 2)" class TestGetBGMPoolCandidates: """风格匹配测试。""" def test_no_mood_returns_full_pool(self): """不指定 mood 时返回全池。""" candidates = get_bgm_pool_candidates(None) assert len(candidates) == len(BGM_POOL) def test_matching_mood_filters(self): """指定已知 mood 时返回同 mood 条目。""" candidates = get_bgm_pool_candidates("upbeat") assert all(c.mood == "upbeat" for c in candidates) assert len(candidates) >= 2 def test_unknown_mood_returns_full_pool(self): """未知 mood 降级返回全池。""" candidates = get_bgm_pool_candidates("nonexistent_style") # 如果没有匹配到同 mood 的(>=2),降级全池 assert len(candidates) >= 2 def test_style_to_mood_mapping(self): """STYLE_TO_MOOD 覆盖所有 preset_bgm 的 style。""" assert "upbeat" in STYLE_TO_MOOD assert "relax" in STYLE_TO_MOOD assert "tech" in STYLE_TO_MOOD assert "commerce" in STYLE_TO_MOOD class TestSelectBGMPool: """变体 BGM 选择测试。""" def test_same_seed_same_result(self): """相同 seed 返回相同 BGM(可复现)。""" result1 = select_bgm_from_pool(42) result2 = select_bgm_from_pool(42) assert result1.id == result2.id def test_different_seeds_may_differ(self): """不同 seed 可能返回不同 BGM。""" results = set() for seed in range(50): entry = select_bgm_from_pool(seed) results.add(entry.id) assert len(results) >= 3, "Expected >= 3 different BGMs from 50 seeds" def test_respects_candidates_filter(self): """传入候选池时只从中选择。""" candidates = [e for e in BGM_POOL if e.mood == "tech"] for seed in range(20): entry = select_bgm_from_pool(seed, candidates) assert entry.mood == "tech" class TestBGMSegmentOffset: """段落差异化测试(策略二)。""" def test_offset_non_negative(self): """偏移 >= 0。""" for seed in range(50): offset = generate_bgm_segment_offset(seed, 120.0) assert offset >= 0.0 def test_offset_bounded(self): """偏移 <= min(30s, duration * 0.3)。""" for seed in range(50): duration = 100.0 max_expected = min(30.0, duration * 0.3) offset = generate_bgm_segment_offset(seed, duration) assert offset <= max_expected + 0.1 # 容差 def test_different_seeds_different_offsets(self): """不同 seed 产生不同偏移(统计验证)。""" offsets = set() for seed in range(30): offsets.add(generate_bgm_segment_offset(seed, 120.0)) assert len(offsets) >= 3, "Expected >= 3 distinct offsets" def test_offset_is_quantized(self): """偏移是 5s 的整数倍。""" for seed in range(20): offset = generate_bgm_segment_offset(seed, 120.0) assert offset % 5.0 == 0.0 def test_short_bgm_zero_offset(self): """极短 BGM 偏移为 0。""" offset = generate_bgm_segment_offset(42, 5.0) # max_offset = min(30, 5*0.3) = 1.5, steps = int(1.5//5) = 0 → return 0 assert offset == 0.0 def test_offset_different_from_bgm_selection(self): """偏移的 seed 序列与 BGM 选择的 seed 序列不同(加素数偏移)。""" # 同一 seed,偏移和 BGM 选择应该独立 bgm = select_bgm_from_pool(42) offset = generate_bgm_segment_offset(42, 120.0) # 只是验证能正常运行,不直接断言独立性(统计测试需要大样本) assert isinstance(offset, float) class TestBGMVolumeAdjust: """音量微调测试(策略三)。""" def test_volume_adjust_in_range(self): """音量调整在 -3 ~ +3 dB 范围内。""" for seed in range(50): adj = generate_bgm_volume_adjust(seed) assert -3.0 <= adj <= 3.0 assert adj == int(adj) # 整数 dB 步进 def test_different_seeds_different_volumes(self): """不同 seed 产生不同音量调整值。""" values = set() for seed in range(50): values.add(generate_bgm_volume_adjust(seed)) assert len(values) >= 3, "Expected >= 3 distinct volume values" def test_includes_zero(self): """音量调整值集合包含 0(不变)。""" values = {generate_bgm_volume_adjust(seed) for seed in range(100)} assert 0.0 in values class TestAllocateBGMPoolForVariants: """批量分配入口测试。""" def test_disabled_bgm_returns_empty(self): """源 BGM 未启用时返回空列表。""" result = allocate_bgm_pool_for_variants({"enabled": False}, [1, 2, 3]) assert result == [] def test_empty_config_returns_empty(self): """空配置返回空列表。""" result = allocate_bgm_pool_for_variants({}, [1, 2, 3]) assert result == [] def test_empty_seeds_returns_empty(self): """无变体时返回空列表。""" result = allocate_bgm_pool_for_variants({"enabled": True, "preset_id": "bgm_upbeat_001"}, []) assert result == [] def test_returns_correct_count(self): """返回与 variant_seeds 等长的列表。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [100, 200, 300, 400] result = allocate_bgm_pool_for_variants(config, seeds) assert len(result) == 4 def test_each_entry_has_required_keys(self): """每项都包含必要字段。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [100, 200, 300] result = allocate_bgm_pool_for_variants(config, seeds) for entry in result: assert "preset_id" in entry assert "audio_offset" in entry assert "volume_adjust_db" in entry assert "bgm_pool_entry_id" in entry assert "bgm_pool_mood" in entry def test_batch_3_variants_at_least_2_different_bgm(self): """批量 3 个变体,至少 2 个不同 BGM。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [100, 200, 300] result = allocate_bgm_pool_for_variants(config, seeds) bgm_ids = {r["bgm_pool_entry_id"] for r in result} assert len(bgm_ids) >= 2, f"Expected >= 2 different BGMs, got {bgm_ids}" def test_style_matching_with_preset_id(self): """源 BGM 有 preset_id 时按风格筛选。""" config = {"enabled": True, "preset_id": "bgm_tech_001"} # tech 风格 seeds = [100, 200, 300] result = allocate_bgm_pool_for_variants(config, seeds) # tech mood 至少有 2 首,所以应该筛选到 tech for entry in result: assert entry["bgm_pool_mood"] == "tech" def test_reproducible_with_same_seeds(self): """相同 seeds 产生相同分配(可复现)。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [42, 100, 200] result1 = allocate_bgm_pool_for_variants(config, seeds) result2 = allocate_bgm_pool_for_variants(config, seeds) for r1, r2 in zip(result1, result2, strict=False): assert r1["bgm_pool_entry_id"] == r2["bgm_pool_entry_id"] assert r1["audio_offset"] == r2["audio_offset"] assert r1["volume_adjust_db"] == r2["volume_adjust_db"] class TestIssue1767Acceptance: """Issue #1767 验收测试。""" def test_batch_3_videos_bgm_different_or_segment_different(self): """批量生成 3 个视频,BGM 不同或起始段落不同。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [100, 200, 300] result = allocate_bgm_pool_for_variants(config, seeds) # 检查:BGM 不同 或 段落偏移不同 unique_combos = set() for r in result: combo = (r["bgm_pool_entry_id"], r["audio_offset"]) unique_combos.add(combo) assert len(unique_combos) >= 2, f"Expected >= 2 unique (bgm, offset) combos, got {unique_combos}" def test_bgm_volume_micro_adjust_doesnt_affect_voice_clarity(self): """BGM 音量微调在 ±3dB 内,不影响配音清晰度。""" config = {"enabled": True, "preset_id": "bgm_upbeat_001"} seeds = [100, 200, 300] result = allocate_bgm_pool_for_variants(config, seeds) for r in result: # ±3dB 是安全的微调范围,不会让 BGM 盖过配音 assert abs(r["volume_adjust_db"]) <= 3.0 def test_single_video_mode_unaffected(self): """单视频模式不受影响(不走池分配)。""" # 单视频不调用 allocate_bgm_pool_for_variants # 只要不主动调用,就不会改变行为 # 这个测试验证函数签名和行为不会意外影响单视频 config = {"enabled": True, "preset_id": "bgm_upbeat_001"} result = allocate_bgm_pool_for_variants(config, [42]) # 单变体 assert len(result) == 1 # 单项分配仍然有完整配置(不影响功能,只是差异化) assert "preset_id" in result[0]