ee4636e087
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m48s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m56s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Successful in 6m18s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m17s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 34s
变更: - RHYTHM_TEMPLATES 从 6 种扩展到 8 种(新增 [2,2,1,1,2] 和 [3,2,1,2,1]) - 修复 MIN_CLIP_DURATION 被重复定义为 1.0 的 bug(恢复为 2.0) - plan_clip_durations 新增 asset_durations 参数,钳制最大片段时长 <= 素材可用时长 × 90% - 新增时长总和误差校验(成片净时长与配音时长误差 <= 0.5s),超限时末段补偿修正 - edit_plan_service.apply_voice_duration_to_plan 传入素材时长参与钳制 - 48 个新增单元测试全部通过 向后兼容:asset_durations 默认 None,不影响现有调用方
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""节奏模板单元测试(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}"
|