"""#1749 问题 C:跨视频素材级去重 + target_durations 测试。""" import random import pytest from packages.domain.variant_plan_selector import ( BATCH_CLIP_OVERLAP_LIMIT, _clip_overlap_ratio, reselect_clips_for_variant, ) def _src_clips(n=3, dur=5.0): return [ { "order": i, "asset_id": f"src{i}", "start_time": 0.0, "duration": dur, "clip_type": "main", "transition_effect": "cut", "transition_duration": 0.0, } for i in range(n) ] def test_three_variants_three_clips_eleven_assets_no_cross_reuse(): """3 变体 × 3 片段 / 11 素材:fresh 优先 → 跨变体零重复。""" pool = [f"a{i}" for i in range(11)] durs = {a: 30.0 for a in pool} batch: dict = {} used_per_variant = [] for v in range(3): clips = reselect_clips_for_variant( _src_clips(3), pool, asset_durations=durs, batch_segments=batch, rng=random.Random(100 + v) ) assert len(clips) == 3 used_per_variant.append({c["asset_id"] for c in clips}) # 两两交集为空(9 个素材位置,11 素材足够 fresh 分配) assert used_per_variant[0] & used_per_variant[1] == set() assert used_per_variant[0] & used_per_variant[2] == set() assert used_per_variant[1] & used_per_variant[2] == set() def test_reuse_overlap_under_limit_and_no_full_overlap(): """素材池不足被迫复用时:重叠 ≤20% 且不得完全重叠。""" # 2 个长素材、3 变体 × 3 片段 → 必然复用 pool = ["x", "y"] durs = {"x": 60.0, "y": 60.0} batch: dict = {} for v in range(3): reselect_clips_for_variant( _src_clips(3, 5.0), pool, asset_durations=durs, batch_segments=batch, rng=random.Random(7 + v) ) # 校验 batch_segments 中同素材任意两区间重叠占比 for asset, segs in batch.items(): for i in range(len(segs)): for j in range(i + 1, len(segs)): s1, e1 = segs[i] s2, e2 = segs[j] ov = max(0.0, min(e1, e2) - max(s1, s2)) seg_dur = min(e1 - s1, e2 - s2) ratio = ov / seg_dur if seg_dur > 0 else 0.0 assert ratio <= BATCH_CLIP_OVERLAP_LIMIT + 0.01, f"{asset} overlap {ratio}" # 不得完全重叠 assert not (abs(s1 - s2) < 0.01 and abs(e1 - e2) < 0.01), f"{asset} 完全重叠" def test_short_asset_cannot_be_reused_across_variants(): """短素材(时长 < 段长 80%)数学上无法错开 → 禁跨变体复用。""" pool = ["short", "long1", "long2"] durs = {"short": 6.2, "long1": 40.0, "long2": 40.0} batch = {"short": [(0.0, 6.2)]} # 短素材已被变体0使用 clips = reselect_clips_for_variant( _src_clips(1, 10.0), pool, asset_durations=durs, batch_segments=batch, rng=random.Random(1) ) assert clips[0]["asset_id"] != "short" def test_target_durations_applied_to_clips(): """target_durations 落库到片段 duration。""" pool = ["a", "b", "c"] durs = {"a": 30.0, "b": 30.0, "c": 30.0} clips = reselect_clips_for_variant( _src_clips(3, 5.0), pool, asset_durations=durs, batch_segments={}, target_durations=[7.333, 7.333, 7.334], rng=random.Random(3), ) durs_out = sorted(c["duration"] for c in clips) assert durs_out == pytest.approx([7.333, 7.333, 7.334], abs=0.01) def test_short_asset_freeze_start_zero(): """素材短于目标段长:起点为 0(末帧冻结由渲染侧铺满),不报错。""" pool = ["short6s"] durs = {"short6s": 6.0} clips = reselect_clips_for_variant( _src_clips(1, 10.0), pool, asset_durations=durs, batch_segments={}, target_durations=[10.0], rng=random.Random(5), ) assert clips[0]["asset_id"] == "short6s" assert clips[0]["start_time"] == 0.0 assert clips[0]["duration"] == pytest.approx(10.0) def test_empty_pool_raises(): with pytest.raises(ValueError): reselect_clips_for_variant(_src_clips(2), [], asset_durations={}, rng=random.Random(1)) def test_empty_source_raises(): with pytest.raises(ValueError): reselect_clips_for_variant([], ["a"], asset_durations={"a": 10.0}, rng=random.Random(1)) def test_zero_duration_assets_raises(): with pytest.raises(ValueError): reselect_clips_for_variant( _src_clips(2), ["a", "b"], asset_durations={"a": 0.0, "b": 0.0}, rng=random.Random(1) ) def test_overlap_ratio_helper(): batch = {"a": [(0.0, 5.0)]} assert _clip_overlap_ratio("a", 0.0, 5.0, batch) == pytest.approx(1.0) assert _clip_overlap_ratio("a", 5.0, 5.0, batch) == pytest.approx(0.0) assert _clip_overlap_ratio("a", 4.0, 5.0, batch) == pytest.approx(0.2) assert _clip_overlap_ratio("b", 0.0, 5.0, batch) == 0.0