042cae7a61
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
454 lines
14 KiB
Python
Executable File
454 lines
14 KiB
Python
Executable File
"""speed_config 调速配置领域模型单测."""
|
|
|
|
import pytest
|
|
from domain.speed_config import (
|
|
DEFAULT_SPEED,
|
|
MAX_SPEED,
|
|
MIN_SPEED,
|
|
SpeedConfig,
|
|
adjust_duration,
|
|
build_audio_filter,
|
|
build_clip_speed_filter,
|
|
build_video_filter,
|
|
resolve_clip_speed,
|
|
)
|
|
|
|
# ── 常量测试 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestConstants:
|
|
"""模块常量"""
|
|
|
|
def test_speed_limits(self):
|
|
assert MIN_SPEED == 0.25
|
|
assert MAX_SPEED == 4.0
|
|
assert DEFAULT_SPEED == 1.0
|
|
|
|
|
|
# ── SpeedConfig 默认值与基础 ────────────────────────────────────────────────
|
|
|
|
|
|
class TestSpeedConfigDefaults:
|
|
"""SpeedConfig 默认值"""
|
|
|
|
def test_default_values(self):
|
|
c = SpeedConfig()
|
|
assert c.speed == 1.0
|
|
assert c.pitch_correct is True
|
|
|
|
def test_custom_values(self):
|
|
c = SpeedConfig(speed=2.0, pitch_correct=False)
|
|
assert c.speed == 2.0
|
|
assert c.pitch_correct is False
|
|
|
|
|
|
# ── SpeedConfig.parse ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSpeedConfigParse:
|
|
"""SpeedConfig.parse 工厂方法"""
|
|
|
|
def test_none_returns_default(self):
|
|
c = SpeedConfig.parse(None)
|
|
assert c.speed == 1.0
|
|
assert c.pitch_correct is True
|
|
|
|
def test_empty_dict_returns_default(self):
|
|
c = SpeedConfig.parse({})
|
|
assert c.speed == 1.0
|
|
|
|
def test_not_dict_returns_default(self):
|
|
c = SpeedConfig.parse("not a dict")
|
|
assert c.speed == 1.0
|
|
|
|
def test_valid_speed(self):
|
|
c = SpeedConfig.parse({"speed": 2.0})
|
|
assert c.speed == 2.0
|
|
|
|
def test_valid_speed_int(self):
|
|
c = SpeedConfig.parse({"speed": 2})
|
|
assert c.speed == 2.0
|
|
assert isinstance(c.speed, float)
|
|
|
|
def test_pitch_correct_false(self):
|
|
c = SpeedConfig.parse({"pitch_correct": False})
|
|
assert c.pitch_correct is False
|
|
|
|
def test_pitch_correct_non_bool_falls_back(self):
|
|
c = SpeedConfig.parse({"pitch_correct": "true"})
|
|
assert c.pitch_correct is True
|
|
|
|
def test_invalid_speed_string_falls_back(self):
|
|
c = SpeedConfig.parse({"speed": "fast"})
|
|
assert c.speed == 1.0
|
|
|
|
def test_speed_below_min_clamped(self):
|
|
c = SpeedConfig.parse({"speed": 0.1})
|
|
assert c.speed == MIN_SPEED
|
|
|
|
def test_speed_above_max_clamped(self):
|
|
c = SpeedConfig.parse({"speed": 10.0})
|
|
assert c.speed == MAX_SPEED
|
|
|
|
def test_zero_speed_falls_back_to_default(self):
|
|
c = SpeedConfig.parse({"speed": 0})
|
|
assert c.speed == DEFAULT_SPEED
|
|
|
|
def test_negative_speed_falls_back(self):
|
|
c = SpeedConfig.parse({"speed": -1.0})
|
|
assert c.speed == DEFAULT_SPEED
|
|
|
|
def test_min_speed_boundary(self):
|
|
c = SpeedConfig.parse({"speed": 0.25})
|
|
assert c.speed == 0.25
|
|
|
|
def test_max_speed_boundary(self):
|
|
c = SpeedConfig.parse({"speed": 4.0})
|
|
assert c.speed == 4.0
|
|
|
|
|
|
# ── SpeedConfig.clamp ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSpeedConfigClamp:
|
|
"""SpeedConfig.clamp 方法"""
|
|
|
|
def test_normal_speed_no_change(self):
|
|
c = SpeedConfig(speed=1.5)
|
|
c.clamp()
|
|
assert c.speed == 1.5
|
|
|
|
def test_zero_speed_reset_default(self):
|
|
c = SpeedConfig(speed=0.0)
|
|
c.clamp()
|
|
assert c.speed == DEFAULT_SPEED
|
|
|
|
def test_negative_speed_reset_default(self):
|
|
c = SpeedConfig(speed=-0.5)
|
|
c.clamp()
|
|
assert c.speed == DEFAULT_SPEED
|
|
|
|
def test_below_min_clamped(self):
|
|
c = SpeedConfig(speed=0.1)
|
|
c.clamp()
|
|
assert c.speed == MIN_SPEED
|
|
|
|
def test_above_max_clamped(self):
|
|
c = SpeedConfig(speed=5.0)
|
|
c.clamp()
|
|
assert c.speed == MAX_SPEED
|
|
|
|
def test_exact_min_unchanged(self):
|
|
c = SpeedConfig(speed=MIN_SPEED)
|
|
c.clamp()
|
|
assert c.speed == MIN_SPEED
|
|
|
|
def test_exact_max_unchanged(self):
|
|
c = SpeedConfig(speed=MAX_SPEED)
|
|
c.clamp()
|
|
assert c.speed == MAX_SPEED
|
|
|
|
|
|
# ── SpeedConfig 属性方法 ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestSpeedConfigProperties:
|
|
"""SpeedConfig 属性方法"""
|
|
|
|
def test_is_original_true(self):
|
|
c = SpeedConfig(speed=1.0)
|
|
assert c.is_original is True
|
|
|
|
def test_is_original_very_close(self):
|
|
c = SpeedConfig(speed=1.0 + 1e-7)
|
|
assert c.is_original is True
|
|
|
|
def test_is_original_false_fast(self):
|
|
c = SpeedConfig(speed=1.5)
|
|
assert c.is_original is False
|
|
|
|
def test_is_original_false_slow(self):
|
|
c = SpeedConfig(speed=0.8)
|
|
assert c.is_original is False
|
|
|
|
def test_is_fast_true(self):
|
|
c = SpeedConfig(speed=2.0)
|
|
assert c.is_fast is True
|
|
|
|
def test_is_fast_false(self):
|
|
c = SpeedConfig(speed=0.5)
|
|
assert c.is_fast is False
|
|
|
|
def test_is_fast_at_one(self):
|
|
c = SpeedConfig(speed=1.0)
|
|
assert c.is_fast is False
|
|
|
|
def test_is_slow_true(self):
|
|
c = SpeedConfig(speed=0.5)
|
|
assert c.is_slow is True
|
|
|
|
def test_is_slow_false(self):
|
|
c = SpeedConfig(speed=2.0)
|
|
assert c.is_slow is False
|
|
|
|
def test_is_slow_at_one(self):
|
|
c = SpeedConfig(speed=1.0)
|
|
assert c.is_slow is False
|
|
|
|
|
|
# ── build_video_filter ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestBuildVideoFilter:
|
|
"""build_video_filter 视频滤镜构建"""
|
|
|
|
def test_original_speed_empty(self):
|
|
c = SpeedConfig(speed=1.0)
|
|
assert build_video_filter(c) == ""
|
|
|
|
def test_double_speed(self):
|
|
c = SpeedConfig(speed=2.0)
|
|
result = build_video_filter(c)
|
|
assert "setpts=PTS/2.0" in result
|
|
|
|
def test_half_speed(self):
|
|
c = SpeedConfig(speed=0.5)
|
|
result = build_video_filter(c)
|
|
assert "setpts=PTS/0.5" in result
|
|
|
|
def test_format_precision(self):
|
|
c = SpeedConfig(speed=1.5)
|
|
result = build_video_filter(c)
|
|
# 应该是 4 位小数
|
|
assert "1.5000" in result
|
|
|
|
def test_min_speed(self):
|
|
c = SpeedConfig(speed=0.25)
|
|
result = build_video_filter(c)
|
|
assert result.startswith("setpts=PTS/")
|
|
|
|
def test_max_speed(self):
|
|
c = SpeedConfig(speed=4.0)
|
|
result = build_video_filter(c)
|
|
assert "4.0000" in result
|
|
|
|
|
|
# ── build_audio_filter / atempo 拆分 ────────────────────────────────────────
|
|
|
|
|
|
class TestBuildAudioFilter:
|
|
"""build_audio_filter 音频滤镜构建"""
|
|
|
|
def test_original_speed_empty(self):
|
|
c = SpeedConfig(speed=1.0)
|
|
assert build_audio_filter(c) == ""
|
|
|
|
def test_within_range_single_stage(self):
|
|
c = SpeedConfig(speed=1.5)
|
|
result = build_audio_filter(c)
|
|
assert result == "atempo=1.5000"
|
|
|
|
def test_05_speed_single_stage(self):
|
|
c = SpeedConfig(speed=0.5)
|
|
result = build_audio_filter(c)
|
|
assert result == "atempo=0.5000"
|
|
|
|
def test_20_speed_single_stage(self):
|
|
c = SpeedConfig(speed=2.0)
|
|
result = build_audio_filter(c)
|
|
assert result == "atempo=2.0000"
|
|
|
|
def test_4x_speed_two_stages(self):
|
|
c = SpeedConfig(speed=4.0)
|
|
result = build_audio_filter(c)
|
|
# 2.0 * 2.0 = 4.0
|
|
assert result == "atempo=2.0000,atempo=2.0000"
|
|
|
|
def test_025_speed_two_stages(self):
|
|
c = SpeedConfig(speed=0.25)
|
|
result = build_audio_filter(c)
|
|
# 0.5 * 0.5 = 0.25
|
|
assert result == "atempo=0.5000,atempo=0.5000"
|
|
|
|
def test_3x_speed_two_stages(self):
|
|
c = SpeedConfig(speed=3.0)
|
|
result = build_audio_filter(c)
|
|
# 2.0 * 1.5 = 3.0
|
|
stages = result.split(",")
|
|
assert len(stages) == 2
|
|
assert "atempo=2.0000" in stages[0]
|
|
assert "atempo=1.5000" in stages[1]
|
|
|
|
def test_03_speed_two_stages(self):
|
|
c = SpeedConfig(speed=0.3)
|
|
result = build_audio_filter(c)
|
|
stages = result.split(",")
|
|
assert len(stages) == 2
|
|
# 0.5 * 0.6 = 0.3
|
|
assert "atempo=0.5000" in stages[0]
|
|
|
|
def test_format_each_stage(self):
|
|
c = SpeedConfig(speed=1.2345)
|
|
result = build_audio_filter(c)
|
|
assert "atempo=1.2345" in result
|
|
|
|
|
|
class TestAtempoStages:
|
|
"""atempo 多级拆分逻辑验证"""
|
|
|
|
def _extract_speeds(self, filter_str: str) -> list[float]:
|
|
"""从 atempo 滤镜字符串中提取速度值."""
|
|
import re
|
|
|
|
return [float(m) for m in re.findall(r"atempo=([\d.]+)", filter_str)]
|
|
|
|
def test_product_equals_speed_fast_3x(self):
|
|
c = SpeedConfig(speed=3.0)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
product = 1.0
|
|
for s in speeds:
|
|
product *= s
|
|
assert abs(product - 3.0) < 1e-4
|
|
|
|
def test_product_equals_speed_4x(self):
|
|
c = SpeedConfig(speed=4.0)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
product = 1.0
|
|
for s in speeds:
|
|
product *= s
|
|
assert abs(product - 4.0) < 1e-4
|
|
|
|
def test_product_equals_speed_slow_025(self):
|
|
c = SpeedConfig(speed=0.25)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
product = 1.0
|
|
for s in speeds:
|
|
product *= s
|
|
assert abs(product - 0.25) < 1e-4
|
|
|
|
def test_product_equals_speed_slow_03(self):
|
|
c = SpeedConfig(speed=0.3)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
product = 1.0
|
|
for s in speeds:
|
|
product *= s
|
|
assert abs(product - 0.3) < 1e-4
|
|
|
|
def test_each_stage_in_range_fast(self):
|
|
c = SpeedConfig(speed=3.5)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
for s in speeds:
|
|
assert 0.5 <= s <= 2.0
|
|
|
|
def test_each_stage_in_range_slow(self):
|
|
c = SpeedConfig(speed=0.35)
|
|
speeds = self._extract_speeds(build_audio_filter(c))
|
|
for s in speeds:
|
|
assert 0.5 <= s <= 2.0
|
|
|
|
|
|
# ── adjust_duration ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAdjustDuration:
|
|
"""adjust_duration 时长计算"""
|
|
|
|
def test_original_speed_no_change(self):
|
|
assert adjust_duration(10.0, SpeedConfig(speed=1.0)) == 10.0
|
|
|
|
def test_double_speed_half_duration(self):
|
|
assert adjust_duration(10.0, SpeedConfig(speed=2.0)) == 5.0
|
|
|
|
def test_half_speed_double_duration(self):
|
|
assert adjust_duration(10.0, SpeedConfig(speed=0.5)) == 20.0
|
|
|
|
def test_zero_duration_unchanged(self):
|
|
assert adjust_duration(0.0, SpeedConfig(speed=2.0)) == 0.0
|
|
|
|
def test_negative_duration_unchanged(self):
|
|
assert adjust_duration(-1.0, SpeedConfig(speed=2.0)) == -1.0
|
|
|
|
def test_original_with_zero_duration(self):
|
|
assert adjust_duration(0.0, SpeedConfig(speed=1.0)) == 0.0
|
|
|
|
def test_triple_speed(self):
|
|
assert adjust_duration(30.0, SpeedConfig(speed=3.0)) == 10.0
|
|
|
|
def test_quarter_speed(self):
|
|
assert adjust_duration(10.0, SpeedConfig(speed=0.25)) == 40.0
|
|
|
|
|
|
# ── build_clip_speed_filter ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestBuildClipSpeedFilter:
|
|
"""build_clip_speed_filter 便捷方法"""
|
|
|
|
def test_returns_tuple_of_three(self):
|
|
result = build_clip_speed_filter(1.5)
|
|
assert len(result) == 3
|
|
video_filter, audio_filter, config = result
|
|
assert isinstance(video_filter, str)
|
|
assert isinstance(audio_filter, str)
|
|
assert isinstance(config, SpeedConfig)
|
|
|
|
def test_normal_speed(self):
|
|
video_filter, audio_filter, config = build_clip_speed_filter(1.0)
|
|
assert video_filter == ""
|
|
assert audio_filter == ""
|
|
assert config.speed == 1.0
|
|
|
|
def test_double_speed(self):
|
|
video_filter, audio_filter, config = build_clip_speed_filter(2.0)
|
|
assert "setpts" in video_filter
|
|
assert "atempo" in audio_filter
|
|
assert config.speed == 2.0
|
|
|
|
def test_clamps_speed(self):
|
|
_, _, config = build_clip_speed_filter(10.0)
|
|
assert config.speed == MAX_SPEED
|
|
|
|
def test_pitch_correct_false(self):
|
|
# pitch_correct=False 时仍然生成滤镜(实际使用中可能换其他算法,但接口返回不变)
|
|
video_filter, audio_filter, config = build_clip_speed_filter(2.0, pitch_correct=False)
|
|
assert config.pitch_correct is False
|
|
assert "setpts" in video_filter
|
|
|
|
|
|
# ── resolve_clip_speed ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestResolveClipSpeed:
|
|
"""resolve_clip_speed 片段速度解析"""
|
|
|
|
def test_none_config_uses_global(self):
|
|
assert resolve_clip_speed(None, 1.5) == 1.5
|
|
|
|
def test_zero_speed_uses_global(self):
|
|
assert resolve_clip_speed({"playback_speed": 0}, 1.5) == 1.5
|
|
|
|
def test_missing_key_uses_global(self):
|
|
assert resolve_clip_speed({}, 2.0) == 2.0
|
|
|
|
def test_valid_speed(self):
|
|
assert resolve_clip_speed({"playback_speed": 1.5}, 1.0) == 1.5
|
|
|
|
def test_negative_speed_uses_global(self):
|
|
assert resolve_clip_speed({"playback_speed": -1.0}, 1.0) == 1.0
|
|
|
|
def test_invalid_type_uses_global(self):
|
|
assert resolve_clip_speed({"playback_speed": "fast"}, 1.0) == 1.0
|
|
|
|
def test_default_global_is_one(self):
|
|
assert resolve_clip_speed({"playback_speed": 0}) == 1.0
|
|
|
|
def test_int_speed(self):
|
|
result = resolve_clip_speed({"playback_speed": 2})
|
|
assert result == 2.0
|
|
assert isinstance(result, float)
|
|
|
|
def test_very_small_positive_uses_it(self):
|
|
# 只要 > 0 就用
|
|
result = resolve_clip_speed({"playback_speed": 0.1})
|
|
assert result == 0.1
|