"""#1970 PR2 微变换纯逻辑单元测试。 覆盖: - 种子可复现(同 task_id+video_index 跨调用一致;不同 video_index 不同) - 6 维参数取值范围(speed 0.97~1.03、色彩 ±0.02、hflip 概率与字幕门控) - BGM 偏移 2~8s 与 atrim 片段边界 - filter 片段格式 """ from __future__ import annotations import random import pytest from video_processing.micro_transform_pure import ( BGM_OFFSET_MAX, BGM_OFFSET_MIN, COLOR_DELTA, HFLIP_PROBABILITY, SPEED_MAX, SPEED_MIN, build_bgm_offset_trim, build_micro_transform_plan, make_video_seed, ) class TestSeed: def test_seed_in_range(self): for i in range(50): s = make_video_seed("task-xyz", i) assert 0 <= s < 10000 def test_seed_deterministic_across_calls(self): a = make_video_seed("task-1", 2) b = make_video_seed("task-1", 2) assert a == b def test_seed_differs_by_task_or_index(self): base = make_video_seed("task-1", 0) assert make_video_seed("task-2", 0) != base or make_video_seed("task-1", 1) != base # 至少 video_index 不同时种子不同(概率上必然,用多组确认) seeds = {make_video_seed("task-fixed", i) for i in range(8)} assert len(seeds) > 1 def test_empty_task_id_does_not_raise(self): assert 0 <= make_video_seed("", 0) < 10000 class TestBuildPlan: def test_zero_clips_plan_has_bgm_offset(self): plan = build_micro_transform_plan("t1", 0, 0) assert plan.clips == [] assert BGM_OFFSET_MIN <= plan.bgm_start_offset <= BGM_OFFSET_MAX def test_clip_param_ranges(self): plan = build_micro_transform_plan("t-range", 0, 30) assert len(plan.clips) == 30 for c in plan.clips: assert SPEED_MIN <= c.speed <= SPEED_MAX assert -COLOR_DELTA - 1e-9 <= c.brightness <= COLOR_DELTA + 1e-9 assert 1.0 - COLOR_DELTA - 1e-9 <= c.contrast <= 1.0 + COLOR_DELTA + 1e-9 assert 1.0 - COLOR_DELTA - 1e-9 <= c.saturation <= 1.0 + COLOR_DELTA + 1e-9 def test_plan_reproducible(self): p1 = build_micro_transform_plan("repro", 1, 10) p2 = build_micro_transform_plan("repro", 1, 10) assert [c.speed for c in p1.clips] == [c.speed for c in p2.clips] assert [c.brightness for c in p1.clips] == [c.brightness for c in p2.clips] assert p1.bgm_start_offset == p2.bgm_start_offset def test_hflip_disabled_when_no_text_info(self): # clip_has_text=None(P1 保守):全部按有文字处理,一律不翻转 plan = build_micro_transform_plan("t1", 0, 40, clip_has_text=None) assert all(not c.hflip for c in plan.clips) assert all(c.has_text for c in plan.clips) def test_hflip_never_on_text_clips(self): # 全部标记有文字:无论如何都不翻转 plan = build_micro_transform_plan("t-text", 0, 40, clip_has_text=[True] * 40) assert all(not c.hflip for c in plan.clips) def test_hflip_roughly_half_on_clean_clips(self): # 全部无文字:翻转比例应接近 50%(给宽松区间防 flaky) plan = build_micro_transform_plan("t-clean", 0, 2000, clip_has_text=[False] * 2000) flipped = sum(1 for c in plan.clips if c.hflip) ratio = flipped / 2000 assert HFLIP_PROBABILITY == 0.5 assert 0.40 < ratio < 0.60 def test_hflip_mixed_text_mask(self): mask = [i % 2 == 0 for i in range(100)] # 偶数位有文字 plan = build_micro_transform_plan("t-mask", 0, 100, clip_has_text=mask) for c in plan.clips: if mask[c.clip_index]: assert not c.hflip def test_bgm_offset_disabled(self): plan = build_micro_transform_plan("t1", 0, 5, enable_bgm_offset=False) assert plan.bgm_start_offset == 0.0 def test_clip_lookup(self): plan = build_micro_transform_plan("t1", 0, 3) assert plan.clip(0) is plan.clips[0] assert plan.clip(2) is plan.clips[2] assert plan.clip(99) is None class TestFilterSuffix: def test_identity_transform_empty_suffix(self): plan = build_micro_transform_plan("t", 0, 1, clip_has_text=[True]) c = plan.clips[0] # 强制为恒等参数验证格式 object.__setattr__(c, "speed", 1.0) object.__setattr__(c, "brightness", 0.0) object.__setattr__(c, "contrast", 1.0) object.__setattr__(c, "saturation", 1.0) object.__setattr__(c, "hflip", False) assert c.video_filter_suffix() == "" assert c.audio_filter_suffix() == "" def test_video_filter_order_speed_hflip_eq(self): plan = build_micro_transform_plan("t", 0, 1, clip_has_text=[False]) c = plan.clips[0] object.__setattr__(c, "speed", 1.02) object.__setattr__(c, "hflip", True) object.__setattr__(c, "has_text", False) object.__setattr__(c, "brightness", 0.01) suffix = c.video_filter_suffix() steps = suffix.split(",") assert steps[0].startswith("setpts=") assert steps[1] == "hflip" assert steps[2].startswith("eq=brightness=") def test_hflip_blocked_by_text_in_suffix(self): plan = build_micro_transform_plan("t", 0, 1) c = plan.clips[0] object.__setattr__(c, "hflip", True) object.__setattr__(c, "has_text", True) assert "hflip" not in c.video_filter_suffix() def test_audio_suffix_only_for_speed(self): plan = build_micro_transform_plan("t", 0, 1) c = plan.clips[0] object.__setattr__(c, "speed", 0.98) assert c.audio_filter_suffix() == "atempo=0.98000" object.__setattr__(c, "speed", 1.0) assert c.audio_filter_suffix() == "" class TestBgmTrim: def test_normal_offset(self): assert build_bgm_offset_trim(3.0, 30.0) == "atrim=start=3.000," def test_zero_or_negative(self): assert build_bgm_offset_trim(0.0, 30.0) == "" assert build_bgm_offset_trim(-1.0, 30.0) == "" def test_offset_near_end_falls_back(self): # 距尾部不足 0.5s → 空串 assert build_bgm_offset_trim(29.7, 30.0) == "" def test_invalid_duration(self): assert build_bgm_offset_trim(3.0, 0.0) == "" class TestDistributionSanity: def test_speed_distribution_spans_range(self): # 多片段采样确认速度在全区间有分布(非常量) plan = build_micro_transform_plan("t-dist", 0, 500) speeds = [c.speed for c in plan.clips] assert min(speeds) < 0.99 assert max(speeds) > 1.01 def test_bgm_offset_range_many_seeds(self): for i in range(100): plan = build_micro_transform_plan("t", i, 1) assert BGM_OFFSET_MIN <= plan.bgm_start_offset <= BGM_OFFSET_MAX