98cf571ab3
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) 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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m20s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""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]
|