"""节奏模板单元测试(Issue #1764)。 覆盖: - RHYTHM_TEMPLATES 池定义(8 种模板,#1764 原始 6 种 + #1768 新增 2 种) - get_rhythm_template:根据 seed 选择模板 - adapt_template_length:适配不同片段数 - plan_clip_durations:按权重分配时长 - 时长约束:总时长 ≈ 配音时长,每段 >= 2s """ from __future__ import annotations import pytest from packages.domain.voice_duration_planner import ( MIN_CLIP_DURATION, RHYTHM_TEMPLATES, adapt_template_length, get_rhythm_template, plan_clip_durations, total_output_duration, ) class TestRhythmTemplates: """节奏模板池测试。""" def test_six_templates_defined(self): """预设 8 种节奏模板(#1764 原始 6 种 + #1768 新增 2 种)。""" assert len(RHYTHM_TEMPLATES) == 8 def test_average_template_is_all_ones(self): """第一种模板是平均(全 1)。""" assert RHYTHM_TEMPLATES[0] == [1, 1, 1, 1, 1] def test_all_templates_have_5_elements(self): """所有模板长度为 5(会被 adapt 适配)。""" for tpl in RHYTHM_TEMPLATES: assert len(tpl) == 5 class TestGetRhythmTemplate: """get_rhythm_template 测试。""" def test_none_seed_returns_average(self): """None seed 返回平均模板。""" assert get_rhythm_template(None) == [1, 1, 1, 1, 1] def test_same_seed_same_template(self): """相同 seed 返回相同模板。""" tpl1 = get_rhythm_template(42) tpl2 = get_rhythm_template(42) assert tpl1 == tpl2 def test_different_seeds_may_differ(self): """不同 seed 可能返回不同模板。""" templates_seen = set() for seed in range(100): tpl = tuple(get_rhythm_template(seed)) templates_seen.add(tpl) # 100 个 seed 应该至少看到 3 种不同模板 assert len(templates_seen) >= 3 class TestAdaptTemplateLength: """adapt_template_length 测试。""" def test_same_length(self): """片段数 == 模板长度时直接返回。""" tpl = [2, 1, 3, 1, 2] assert adapt_template_length(tpl, 5) == [2, 1, 3, 1, 2] def test_shorter_clip_count(self): """片段数 < 模板长度时截断。""" tpl = [2, 1, 3, 1, 2] assert adapt_template_length(tpl, 3) == [2, 1, 3] def test_longer_clip_count(self): """片段数 > 模板长度时循环填充。""" tpl = [2, 1, 3] result = adapt_template_length(tpl, 7) assert result == [2, 1, 3, 2, 1, 3, 2] def test_zero_clip_count(self): """片段数 0 返回空列表。""" assert adapt_template_length([1, 2, 3], 0) == [] class TestPlanClipDurationsWithRhythm: """plan_clip_durations 节奏模板测试。""" def test_average_template_equals_old_behavior(self): """全 1 模板 = 原来的平均分配。""" voice = 20.0 clips = 4 result = plan_clip_durations(clips, voice, rhythm_template=[1, 1, 1, 1]) # 每段应该 ≈ 5s assert all(abs(d - 5.0) < 0.1 for d in result) assert abs(sum(result) - voice) < 0.1 def test_weighted_template_different_durations(self): """权重模板产生不同时长的片段。""" voice = 18.0 clips = 5 # 权重 [2, 1, 3, 1, 2]:第 3 段最长,第 2/4 段最短 template = [2, 1, 3, 1, 2] result = plan_clip_durations(clips, voice, rhythm_template=template) # 总时长 ≈ 配音时长 assert abs(sum(result) - voice) < 0.5 # 第 3 段应该最长 assert result[2] > result[1] assert result[2] > result[3] def test_min_clip_duration_enforced(self): """每段 >= MIN_CLIP_DURATION (2s)。""" voice = 15.0 clips = 5 # 极端权重:某段权重极低 template = [10, 1, 1, 1, 1] result = plan_clip_durations(clips, voice, rhythm_template=template) for d in result: assert d >= MIN_CLIP_DURATION def test_total_duration_with_transitions(self): """含转场时总时长仍然正确。""" voice = 20.0 clips = 4 effects = [None, "xfade", "fade", "cut"] durations = [0.0, 0.5, 0.3, 0.0] template = [2, 1, 1, 2] result = plan_clip_durations( clips, voice, transition_effects=effects, transition_durations=durations, rhythm_template=template, ) # 成片净时长 = Σ段长 - Σ转场重叠 ≈ 配音时长 output = total_output_duration(result, effects, durations) assert abs(output - voice) < 0.5 def test_no_template_backward_compatible(self): """不传模板时行为与旧版一致(平均分配)。""" voice = 16.0 clips = 4 result = plan_clip_durations(clips, voice) assert all(abs(d - 4.0) < 0.1 for d in result) def test_six_templates_produce_different_structures(self): """6 种模板产生不同的时长结构。""" voice = 25.0 clips = 5 structures = set() for tpl in RHYTHM_TEMPLATES: result = plan_clip_durations(clips, voice, rhythm_template=tpl) # 用 round 后的元组作为结构指纹 structure = tuple(round(d, 1) for d in result) structures.add(structure) # 至少 4 种不同结构 assert len(structures) >= 4 class TestIssue1764Acceptance: """Issue #1764 验收测试。""" def test_batch_3_variants_at_least_2_different(self): """批量 3 个变体,至少 2 组不同片段时长序列。""" voice = 20.0 clips = 5 # 模拟 3 个变体用不同 seed seeds = [100, 200, 300] structures = [] for seed in seeds: template = get_rhythm_template(seed) adapted = adapt_template_length(template, clips) durations = plan_clip_durations(clips, voice, rhythm_template=adapted) structures.append(tuple(round(d, 1) for d in durations)) # 至少 2 种不同结构 unique = len(set(structures)) assert unique >= 2, f"Expected >= 2 unique structures, got {unique}: {structures}"