a59a6a588a
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) 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 / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m25s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m43s
CI/CD Pipeline / Integration Tests (push) Successful in 3m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Validate - Style (push) Successful in 6m0s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m46s
CI/CD Pipeline / Validate - Security (push) Successful in 8m2s
CI/CD Pipeline / Unit Tests (push) Successful in 9m27s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
"""#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
|