Files
xiaoxia-saas/tests/unit/test_bgm_mixer_pure.py
xiaoxia de201436ea
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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
test(wave204): text_splitter + pagination 单测补全 +37测 (#1171)
2026-07-30 00:26:00 +08:00

485 lines
17 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""bgm_mixer_pure 单元测试."""
from apps.worker.video_processing.bgm_mixer_pure import (
BGMPureConfig,
build_bgm_filter_chain,
build_sidechain_mix_filter,
build_simple_mix_filter,
calculate_fade_out_start,
calculate_loop_count,
calculate_sidechain_ratio,
estimate_bgm_processing_duration,
normalize_bgm_config,
should_loop_bgm,
validate_bgm_config,
)
# ── BGMPureConfig ────────────────────────────────────────────────────────────
class TestBGMPureConfig:
def test_default_values(self):
cfg = BGMPureConfig()
assert cfg.volume == 0.3
assert cfg.fade_in == 0.0
assert cfg.fade_out == 0.0
assert cfg.loop_enabled is True
assert cfg.sidechain_enabled is False
assert cfg.sidechain_ratio == 0.3
assert cfg.sidechain_attack == 0.02
assert cfg.sidechain_release == 0.5
assert cfg.sidechain_threshold == -25.0
def test_custom_values(self):
cfg = BGMPureConfig(
volume=0.5,
fade_in=1.0,
fade_out=2.0,
loop_enabled=False,
sidechain_enabled=True,
sidechain_ratio=0.5,
)
assert cfg.volume == 0.5
assert cfg.loop_enabled is False
assert cfg.sidechain_enabled is True
assert cfg.sidechain_ratio == 0.5
# ── should_loop_bgm ─────────────────────────────────────────────────────────
class TestShouldLoopBgm:
def test_loop_enabled_much_shorter(self):
# BGM 10秒,目标60秒 → 需要循环
assert should_loop_bgm(10, 60) is True
def test_loop_disabled(self):
assert should_loop_bgm(10, 60, loop_enabled=False) is False
def test_bgm_longer_than_target(self):
# BGM 100秒,目标60秒 → 不需要循环
assert should_loop_bgm(100, 60) is False
def test_bgm_slightly_shorter_no_loop(self):
# BGM 58秒,目标60秒 → 58 > 60*0.9=54,不需要循环
assert should_loop_bgm(58, 60) is False
def test_bgm_significantly_shorter_loops(self):
# BGM 50秒,目标60秒 → 50 < 54,需要循环
assert should_loop_bgm(50, 60) is True
def test_zero_bgm_duration(self):
assert should_loop_bgm(0, 60) is False
def test_negative_bgm_duration(self):
assert should_loop_bgm(-1, 60) is False
def test_zero_target_duration(self):
assert should_loop_bgm(10, 0) is False
def test_negative_target_duration(self):
assert should_loop_bgm(10, -1) is False
def test_exact_90_percent_no_loop(self):
# 边界:bgm == target * 0.9 → 不小于,不循环
assert should_loop_bgm(54, 60) is False
# ── calculate_loop_count ────────────────────────────────────────────────────
class TestCalculateLoopCount:
def test_exact_fit_returns_1(self):
assert calculate_loop_count(60, 60) == 1
def test_bgm_longer_returns_1(self):
assert calculate_loop_count(100, 60) == 1
def test_needs_3_loops_plus_2_margin(self):
# 60/20 = 3 + 2 = 5
assert calculate_loop_count(20, 60) == 5
def test_needs_2_loops_plus_2_margin(self):
# 60/30 = 2 + 2 = 4
assert calculate_loop_count(30, 60) == 4
def test_zero_bgm_duration(self):
assert calculate_loop_count(0, 60) == 1
def test_negative_bgm_duration(self):
assert calculate_loop_count(-1, 60) == 1
def test_zero_target_duration(self):
assert calculate_loop_count(10, 0) == 1
def test_negative_target_duration(self):
assert calculate_loop_count(10, -1) == 1
def test_minimum_is_1(self):
assert calculate_loop_count(10, 5) == 1
# ── build_bgm_filter_chain ──────────────────────────────────────────────────
class TestBuildBgmFilterChain:
def test_basic_structure(self):
result = build_bgm_filter_chain(100, 60)
parts = result.split(",")
# 至少有 atrim + asetpts
assert any("atrim=" in p for p in parts)
assert "asetpts=N/SR/TB" in parts
def test_volume_filter_applied(self):
result = build_bgm_filter_chain(100, 60, volume=0.5)
assert "volume=0.500" in result
def test_volume_one_omitted(self):
result = build_bgm_filter_chain(100, 60, volume=1.0)
assert "volume=" not in result
def test_volume_clamped(self):
# volume=2.0钳制到1.01.0等于默认值所以被跳过
result = build_bgm_filter_chain(100, 60, volume=2.0)
assert "volume=" not in result # 钳制到1.0后与默认相同,跳过
# 用0.5验证音量过滤器本身存在
result2 = build_bgm_filter_chain(100, 60, volume=0.5)
assert "volume=0.500" in result2
def test_volume_zero(self):
result = build_bgm_filter_chain(100, 60, volume=0.0)
assert "volume=0.000" in result
def test_fade_in_applied(self):
result = build_bgm_filter_chain(100, 60, fade_in=1.5)
assert "afade=t=in:st=0:d=1.500" in result
def test_fade_in_zero_skipped(self):
result = build_bgm_filter_chain(100, 60, fade_in=0)
assert "afade=t=in" not in result
def test_fade_out_applied(self):
result = build_bgm_filter_chain(100, 60, fade_out=2.0)
assert "afade=t=out:st=58.000:d=2.000" in result
def test_fade_out_longer_than_target_skipped(self):
result = build_bgm_filter_chain(100, 10, fade_out=20)
# fade_out >= safe_target,不做淡出
assert "afade=t=out" not in result
def test_loop_applied_when_needed(self):
result = build_bgm_filter_chain(10, 60)
assert "aloop=loop=" in result
def test_no_loop_when_bgm_long(self):
result = build_bgm_filter_chain(100, 60)
assert "aloop=" not in result
def test_loop_disabled(self):
result = build_bgm_filter_chain(10, 60, loop_enabled=False)
assert "aloop=" not in result
def test_trim_to_target_duration(self):
result = build_bgm_filter_chain(100, 60)
assert "atrim=0:60.000" in result
def test_zero_target_uses_fallback(self):
result = build_bgm_filter_chain(100, 0)
# 兜底5秒
assert "atrim=0:5.000" in result
def test_negative_target_uses_fallback(self):
result = build_bgm_filter_chain(100, -5)
assert "atrim=0:5.000" in result
def test_all_features_combined(self):
result = build_bgm_filter_chain(
bgm_duration=15,
target_duration=60,
volume=0.3,
fade_in=1.0,
fade_out=2.0,
loop_enabled=True,
)
assert "aloop=loop=" in result
assert "volume=0.300" in result
assert "afade=t=in" in result
assert "afade=t=out" in result
assert "atrim=0:60.000" in result
assert "asetpts=N/SR/TB" in result
# ── calculate_sidechain_ratio ───────────────────────────────────────────────
class TestCalculateSidechainRatio:
def test_zero_ratio_minimum(self):
assert calculate_sidechain_ratio(0) == 2.0
def test_negative_clamped(self):
assert calculate_sidechain_ratio(-0.5) == 2.0
def test_one_ratio_maximum(self):
assert calculate_sidechain_ratio(1.0) == 10.0
def test_above_one_clamped(self):
assert calculate_sidechain_ratio(1.5) == 10.0
def test_mid_value(self):
# ratio = 1/(1-0.5) = 2.0
result = calculate_sidechain_ratio(0.5)
assert abs(result - 2.0) < 0.01
def test_high_value(self):
# 1/(1-0.9) = 10 → 钳制到10
assert calculate_sidechain_ratio(0.9) == 10.0
def test_03_default(self):
# 1/(1-0.3) = 1.428... → 钳制到2.0
result = calculate_sidechain_ratio(0.3)
assert result >= 2.0
# ── build_simple_mix_filter ─────────────────────────────────────────────────
class TestBuildSimpleMixFilter:
def test_contains_inputs_and_output(self):
result = build_simple_mix_filter()
assert "[0:a][1:a]" in result
assert "amix=inputs=2" in result
assert "duration=first" in result
assert "[final]" in result
assert "volume=2" in result
# ── build_sidechain_mix_filter ──────────────────────────────────────────────
class TestBuildSidechainMixFilter:
def test_contains_sidechaincompress(self):
result = build_sidechain_mix_filter()
assert "sidechaincompress=" in result
assert "[1:a][0:a]sidechaincompress" in result
def test_threshold_in_db(self):
result = build_sidechain_mix_filter(threshold=-30.0)
assert "threshold=-30.0dB" in result
def test_attack_and_release(self):
result = build_sidechain_mix_filter(attack=0.01, release=0.3)
assert "attack=0.010" in result
assert "release=0.300" in result
def test_contains_amix(self):
result = build_sidechain_mix_filter()
assert "amix=inputs=2" in result
assert "duration=first" in result
def test_contains_volume_compensation(self):
result = build_sidechain_mix_filter()
assert "volume=1.5" in result
def test_output_label(self):
result = build_sidechain_mix_filter()
assert "[final]" in result
def test_bgm_comp_label(self):
result = build_sidechain_mix_filter()
assert "[bgm_comp]" in result
# ── normalize_bgm_config ────────────────────────────────────────────────────
class TestNormalizeBgmConfig:
def test_default_values(self):
result = normalize_bgm_config({})
assert result["volume"] == 0.3
assert result["fade_in"] == 0.0
assert result["fade_out"] == 0.0
assert result["loop_enabled"] is True
assert result["sidechain_enabled"] is False
assert result["sidechain_ratio"] == 0.3
assert result["sidechain_attack"] == 0.02
assert result["sidechain_release"] == 0.5
assert result["sidechain_threshold"] == -25.0
def test_volume_clamped(self):
result = normalize_bgm_config({"volume": 2.0})
assert result["volume"] == 1.0
result = normalize_bgm_config({"volume": -1.0})
assert result["volume"] == 0.0
def test_fade_in_clamped_to_zero(self):
result = normalize_bgm_config({"fade_in": -5})
assert result["fade_in"] == 0.0
def test_fade_out_clamped_to_zero(self):
result = normalize_bgm_config({"fade_out": -5})
assert result["fade_out"] == 0.0
def test_loop_enabled_bool_conversion(self):
assert normalize_bgm_config({"loop_enabled": True})["loop_enabled"] is True
assert normalize_bgm_config({"loop_enabled": False})["loop_enabled"] is False
assert normalize_bgm_config({"loop_enabled": 1})["loop_enabled"] is True
assert normalize_bgm_config({"loop_enabled": 0})["loop_enabled"] is False
def test_sidechain_ratio_clamped(self):
result = normalize_bgm_config({"sidechain_ratio": 2.0})
assert result["sidechain_ratio"] == 1.0
result = normalize_bgm_config({"sidechain_ratio": -1.0})
assert result["sidechain_ratio"] == 0.0
def test_sidechain_attack_minimum(self):
result = normalize_bgm_config({"sidechain_attack": 0})
assert result["sidechain_attack"] == 0.001
def test_sidechain_release_minimum(self):
result = normalize_bgm_config({"sidechain_release": 0})
assert result["sidechain_release"] == 0.01
def test_sidechain_threshold_pass_through(self):
result = normalize_bgm_config({"sidechain_threshold": -40.0})
assert result["sidechain_threshold"] == -40.0
def test_string_values_converted(self):
result = normalize_bgm_config(
{
"volume": "0.5",
"fade_in": "1.0",
"sidechain_ratio": "0.7",
}
)
assert result["volume"] == 0.5
assert result["fade_in"] == 1.0
assert result["sidechain_ratio"] == 0.7
# ── validate_bgm_config ─────────────────────────────────────────────────────
class TestValidateBgmConfig:
def test_valid_config(self):
valid, errors = validate_bgm_config({"volume": 0.3})
assert valid is True
assert errors == []
def test_invalid_volume_type(self):
valid, errors = validate_bgm_config({"volume": "abc"})
assert valid is False
assert any("volume" in e for e in errors)
def test_volume_out_of_range(self):
valid, errors = validate_bgm_config({"volume": -0.1})
assert valid is False
assert any("volume" in e for e in errors)
valid, errors = validate_bgm_config({"volume": 1.1})
assert valid is False
assert any("volume" in e for e in errors)
def test_volume_at_boundaries(self):
assert validate_bgm_config({"volume": 0})[0] is True
assert validate_bgm_config({"volume": 1})[0] is True
def test_invalid_fade_in_type(self):
valid, errors = validate_bgm_config({"fade_in": "abc"})
assert valid is False
assert any("fade_in" in e for e in errors)
def test_negative_fade_in(self):
valid, errors = validate_bgm_config({"fade_in": -1})
assert valid is False
assert any("fade_in" in e for e in errors)
def test_invalid_fade_out_type(self):
valid, errors = validate_bgm_config({"fade_out": "abc"})
assert valid is False
assert any("fade_out" in e for e in errors)
def test_negative_fade_out(self):
valid, errors = validate_bgm_config({"fade_out": -1})
assert valid is False
assert any("fade_out" in e for e in errors)
def test_invalid_sidechain_ratio_type(self):
valid, errors = validate_bgm_config({"sidechain_ratio": "abc"})
assert valid is False
assert any("sidechain_ratio" in e for e in errors)
def test_sidechain_ratio_out_of_range(self):
valid, errors = validate_bgm_config({"sidechain_ratio": -0.1})
assert valid is False
assert any("sidechain_ratio" in e for e in errors)
valid, errors = validate_bgm_config({"sidechain_ratio": 1.1})
assert valid is False
assert any("sidechain_ratio" in e for e in errors)
def test_multiple_errors(self):
valid, errors = validate_bgm_config(
{
"volume": "bad",
"fade_in": -1,
"sidechain_ratio": 2.0,
}
)
assert valid is False
assert len(errors) >= 3
# ── calculate_fade_out_start ────────────────────────────────────────────────
class TestCalculateFadeOutStart:
def test_normal_case(self):
assert calculate_fade_out_start(60, 2) == 58.0
def test_zero_fade_out(self):
assert calculate_fade_out_start(60, 0) is None
def test_negative_fade_out(self):
assert calculate_fade_out_start(60, -1) is None
def test_zero_target(self):
assert calculate_fade_out_start(0, 2) is None
def test_negative_target(self):
assert calculate_fade_out_start(-5, 2) is None
def test_fade_longer_than_target(self):
assert calculate_fade_out_start(10, 20) is None
def test_fade_equal_to_target(self):
assert calculate_fade_out_start(10, 10) is None
def test_float_values(self):
assert calculate_fade_out_start(60.5, 2.5) == 58.0
# ── estimate_bgm_processing_duration ────────────────────────────────────────
class TestEstimateBgmProcessingDuration:
def test_bgm_longer_no_loop(self):
assert estimate_bgm_processing_duration(100, 60, loop_enabled=False) == 60.0
def test_bgm_longer_with_loop(self):
# 够长但允许循环,仍然截断到target
assert estimate_bgm_processing_duration(100, 60, loop_enabled=True) == 60.0
def test_bgm_shorter_with_loop(self):
assert estimate_bgm_processing_duration(10, 60, loop_enabled=True) == 60.0
def test_bgm_shorter_no_loop(self):
# 需要循环但不允许 → 截断到target
assert estimate_bgm_processing_duration(10, 60, loop_enabled=False) == 60.0
def test_zero_target_fallback(self):
assert estimate_bgm_processing_duration(100, 0) == 5.0
def test_negative_target_fallback(self):
assert estimate_bgm_processing_duration(100, -5) == 5.0
def test_equal_duration(self):
assert estimate_bgm_processing_duration(60, 60) == 60.0