ae4fad67c3
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 24s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m14s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m13s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m15s
AI Code Review / AI Code Review (pull_request) Successful in 7m1s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 18s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m23s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m3s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1243h44m21s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1243h46m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1243h47m0s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1243h47m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1243h55m39s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1243h55m41s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1243h55m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1243h55m44s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1244h31m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1244h31m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1244h31m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1244h19m40s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1244h28m21s
从 worker_app/tasks/generation.py 抽离模板效果映射纯逻辑到 packages/domain/template_effect_mapper.py: - VirtualClip / VirtualPlan 数据类 - extract_intro_outro_from_clip_configs: intro/outro 配置提取 - apply_template_clip_effects: 效果层映射(转场/滤镜/调速/色度键) - _clip_type_value: Enum/字符串兼容工具 generation.py 保留向后兼容别名(_VirtualClip/_VirtualPlan + 两个函数) 测试:+45 个单测 - 数据类默认值/可变性/config独立性 - extract_intro_outro: 空/无intro/基本字段/text_template/默认时长/配置透传/ outro/同时有intro+outro/多个intro用第一个/Enum类型/无关字段忽略 - apply_template_clip_effects: 空clips/空configs/无main类型/ 转场效果/转场时长/无效时长/零时长/cut跳过/color_grade/ playback_speed/speed key/优先级/无效speed/零speed/ chroma_key+filter/reverse/已有config保留/corner_voice排除/ showcase+b_roll作为模板/循环匹配/Enum转场/config=None/ 就地修改/三对三匹配 - 端到端集成测试 generation.py: 1538→1414行(-124行,-8%) 新模块: 179行
701 lines
26 KiB
Python
Executable File
701 lines
26 KiB
Python
Executable File
"""template_effect_mapper 单元测试 — 模板效果映射纯逻辑层。
|
|
|
|
覆盖:
|
|
- VirtualClip / VirtualPlan 数据类
|
|
- extract_intro_outro_from_clip_configs: intro/outro 配置提取
|
|
- apply_template_clip_effects: 效果层映射
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from packages.domain.template_effect_mapper import (
|
|
VirtualClip,
|
|
VirtualPlan,
|
|
apply_template_clip_effects,
|
|
extract_intro_outro_from_clip_configs,
|
|
)
|
|
|
|
# ── Mock ClipConfig ──────────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class _MockClipConfig:
|
|
"""模拟 TemplateClipConfig 对象。"""
|
|
|
|
clip_type: Any = "main"
|
|
config: dict[str, Any] | None = None
|
|
default_duration: float = 3.0
|
|
text_template: str | None = None
|
|
transition_effect: Any = "cut"
|
|
|
|
|
|
# ── VirtualClip / VirtualPlan 数据类测试 ────────────────────────────────────
|
|
|
|
|
|
class TestVirtualClip(unittest.TestCase):
|
|
"""VirtualClip 数据类测试。"""
|
|
|
|
def test_default_values(self):
|
|
"""默认值正确。"""
|
|
clip = VirtualClip(id="c1")
|
|
self.assertEqual(clip.id, "c1")
|
|
self.assertEqual(clip.plan_id, "")
|
|
self.assertEqual(clip.clip_type, "main")
|
|
self.assertEqual(clip.order, 0)
|
|
self.assertEqual(clip.asset_id, "")
|
|
self.assertEqual(clip.text_content, "")
|
|
self.assertEqual(clip.start_time, 0.0)
|
|
self.assertEqual(clip.duration, 0.0)
|
|
self.assertEqual(clip.transition_effect, "cut")
|
|
self.assertEqual(clip.transition_duration, 0.0)
|
|
self.assertEqual(clip.playback_speed, 1.0)
|
|
self.assertEqual(clip.status, "ready")
|
|
self.assertEqual(clip.config, {})
|
|
|
|
def test_mutable(self):
|
|
"""VirtualClip 是可变 dataclass(非 frozen)。"""
|
|
clip = VirtualClip(id="c1", duration=5.0)
|
|
clip.duration = 10.0
|
|
self.assertEqual(clip.duration, 10.0)
|
|
|
|
def test_config_independent(self):
|
|
"""每个 clip 的 config 是独立的 dict。"""
|
|
clip1 = VirtualClip(id="c1")
|
|
clip2 = VirtualClip(id="c2")
|
|
clip1.config["key"] = "value"
|
|
self.assertNotIn("key", clip2.config)
|
|
|
|
|
|
class TestVirtualPlan(unittest.TestCase):
|
|
"""VirtualPlan 数据类测试。"""
|
|
|
|
def test_default_values(self):
|
|
"""默认值正确。"""
|
|
plan = VirtualPlan(id="p1")
|
|
self.assertEqual(plan.id, "p1")
|
|
self.assertEqual(plan.name, "")
|
|
self.assertEqual(plan.config, {})
|
|
|
|
def test_config_independent(self):
|
|
"""每个 plan 的 config 是独立的 dict。"""
|
|
plan1 = VirtualPlan(id="p1")
|
|
plan2 = VirtualPlan(id="p2")
|
|
plan1.config["key"] = "value"
|
|
self.assertNotIn("key", plan2.config)
|
|
|
|
|
|
# ── extract_intro_outro_from_clip_configs 测试 ──────────────────────────────
|
|
|
|
|
|
class TestExtractIntroOutro(unittest.TestCase):
|
|
"""extract_intro_outro_from_clip_configs 测试。"""
|
|
|
|
def test_empty_list(self):
|
|
"""空列表返回空 dict。"""
|
|
result = extract_intro_outro_from_clip_configs([])
|
|
self.assertEqual(result, {})
|
|
|
|
def test_no_intro_no_outro(self):
|
|
"""只有 main 类型,没有 intro/outro。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="main"),
|
|
_MockClipConfig(clip_type="showcase"),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result, {})
|
|
|
|
def test_intro_only_basic(self):
|
|
"""只有 intro:基本字段。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="intro", default_duration=2.5),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(result["has_intro"])
|
|
self.assertEqual(result["intro_type"], "text") # 默认
|
|
self.assertEqual(result["intro_duration"], 2.5)
|
|
self.assertNotIn("intro_text", result)
|
|
self.assertNotIn("has_outro", result)
|
|
|
|
def test_intro_with_text_template(self):
|
|
"""intro 带 text_template。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="intro",
|
|
text_template="欢迎观看",
|
|
),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["intro_text"], "欢迎观看")
|
|
|
|
def test_intro_default_duration_zero(self):
|
|
"""intro default_duration 为 0 时使用默认 3.0。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="intro", default_duration=0),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["intro_duration"], 3.0)
|
|
|
|
def test_intro_with_config_extra_fields(self):
|
|
"""intro config 中的额外字段被透传。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="intro",
|
|
config={
|
|
"intro_type": "video",
|
|
"intro_text_color": "#ffffff",
|
|
"intro_bg_color": "#000000",
|
|
"intro_font_size": 48,
|
|
"intro_video_url": "https://example.com/intro.mp4",
|
|
"intro_video_path": "/tmp/intro.mp4",
|
|
},
|
|
),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["intro_type"], "video")
|
|
self.assertEqual(result["intro_text_color"], "#ffffff")
|
|
self.assertEqual(result["intro_bg_color"], "#000000")
|
|
self.assertEqual(result["intro_font_size"], 48)
|
|
self.assertEqual(result["intro_video_url"], "https://example.com/intro.mp4")
|
|
self.assertEqual(result["intro_video_path"], "/tmp/intro.mp4")
|
|
|
|
def test_outro_only_basic(self):
|
|
"""只有 outro:基本字段。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="outro", default_duration=4.0),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(result["has_outro"])
|
|
self.assertEqual(result["outro_type"], "text")
|
|
self.assertEqual(result["outro_duration"], 4.0)
|
|
self.assertNotIn("has_intro", result)
|
|
|
|
def test_outro_with_text_template(self):
|
|
"""outro 带 text_template。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="outro",
|
|
text_template="感谢观看",
|
|
),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["outro_text"], "感谢观看")
|
|
|
|
def test_outro_with_config_extra_fields(self):
|
|
"""outro config 中的额外字段被透传。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="outro",
|
|
config={
|
|
"outro_type": "subscribe",
|
|
"outro_text_color": "#ffffff",
|
|
"outro_bg_color": "#333333",
|
|
"outro_font_size": 36,
|
|
"outro_follow_text": "关注我们",
|
|
},
|
|
),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["outro_type"], "subscribe")
|
|
self.assertEqual(result["outro_text_color"], "#ffffff")
|
|
self.assertEqual(result["outro_bg_color"], "#333333")
|
|
self.assertEqual(result["outro_font_size"], 36)
|
|
self.assertEqual(result["outro_follow_text"], "关注我们")
|
|
|
|
def test_both_intro_and_outro(self):
|
|
"""同时有 intro 和 outro。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="intro", default_duration=2.0),
|
|
_MockClipConfig(clip_type="main"),
|
|
_MockClipConfig(clip_type="outro", default_duration=3.0),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(result["has_intro"])
|
|
self.assertEqual(result["intro_duration"], 2.0)
|
|
self.assertTrue(result["has_outro"])
|
|
self.assertEqual(result["outro_duration"], 3.0)
|
|
|
|
def test_multiple_intros_uses_first(self):
|
|
"""多个 intro 只用第一个。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="intro", default_duration=2.0, text_template="第一"),
|
|
_MockClipConfig(clip_type="intro", default_duration=5.0, text_template="第二"),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["intro_duration"], 2.0)
|
|
self.assertEqual(result["intro_text"], "第一")
|
|
|
|
def test_multiple_outros_uses_first(self):
|
|
"""多个 outro 只用第一个。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="outro", default_duration=3.0, text_template="end1"),
|
|
_MockClipConfig(clip_type="outro", default_duration=6.0, text_template="end2"),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertEqual(result["outro_duration"], 3.0)
|
|
self.assertEqual(result["outro_text"], "end1")
|
|
|
|
def test_intro_config_none(self):
|
|
"""intro.config 为 None 时正常工作。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="intro", config=None),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(result["has_intro"])
|
|
self.assertEqual(result["intro_type"], "text") # 默认
|
|
|
|
def test_clip_type_enum_value(self):
|
|
"""clip_type 是 Enum 时通过 .value 获取。"""
|
|
|
|
class _EnumType:
|
|
value = "intro"
|
|
|
|
configs = [
|
|
_MockClipConfig(clip_type=_EnumType(), default_duration=2.0),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(result["has_intro"])
|
|
self.assertEqual(result["intro_duration"], 2.0)
|
|
|
|
def test_irrelevant_config_fields_ignored(self):
|
|
"""intro/outro config 中非预期字段不被透传。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="intro",
|
|
config={"random_key": "should_not_appear", "intro_type": "text"},
|
|
),
|
|
]
|
|
result = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertNotIn("random_key", result)
|
|
|
|
|
|
# ── apply_template_clip_effects 测试 ────────────────────────────────────────
|
|
|
|
|
|
class TestApplyTemplateClipEffects(unittest.TestCase):
|
|
"""apply_template_clip_effects 效果层映射测试。"""
|
|
|
|
def test_empty_clips(self):
|
|
"""空 clips 列表:不报错、不修改。"""
|
|
configs = [_MockClipConfig(clip_type="main")]
|
|
clips: list[VirtualClip] = []
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips, [])
|
|
|
|
def test_empty_configs(self):
|
|
"""空 configs 列表:不修改 clips。"""
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, [], "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "cut")
|
|
self.assertEqual(clips[0].playback_speed, 1.0)
|
|
self.assertEqual(clips[0].config, {})
|
|
|
|
def test_no_main_configs(self):
|
|
"""没有 main/showcase/b_roll 类型的 config:不修改 clips。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="intro",
|
|
config={"color_grade": "warm"},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "cut")
|
|
self.assertEqual(clips[0].config, {})
|
|
|
|
def test_transition_effect_applied(self):
|
|
"""转场效果正确映射。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="main", transition_effect="fade"),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
|
|
def test_transition_duration_applied(self):
|
|
"""转场时长从 config 中读取并映射。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="dissolve",
|
|
config={"transition_duration": 0.8},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "dissolve")
|
|
self.assertEqual(clips[0].transition_duration, 0.8)
|
|
|
|
def test_transition_duration_invalid_string(self):
|
|
"""转场时长是无效字符串时忽略。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="fade",
|
|
config={"transition_duration": "abc"},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_duration, 0.0)
|
|
|
|
def test_transition_duration_zero_ignored(self):
|
|
"""转场时长 <= 0 时忽略。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="fade",
|
|
config={"transition_duration": 0},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_duration, 0.0)
|
|
|
|
def test_cut_transition_skipped(self):
|
|
"""cut 转场不覆盖默认值。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="main", transition_effect="cut"),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0, transition_effect="fade")]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "fade") # 保持原值
|
|
|
|
def test_color_grade_applied(self):
|
|
"""color_grade 效果层映射。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "vivid"},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["color_grade"], "vivid")
|
|
|
|
def test_playback_speed_applied_to_config_and_field(self):
|
|
"""playback_speed 同时映射到 config 和顶级字段。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"playback_speed": 1.5},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["playback_speed"], 1.5)
|
|
self.assertEqual(clips[0].playback_speed, 1.5)
|
|
|
|
def test_speed_key_also_applies(self):
|
|
"""speed key 也能设置 playback_speed 顶级字段。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"speed": 2.0},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].playback_speed, 2.0)
|
|
self.assertEqual(clips[0].config["speed"], 2.0)
|
|
|
|
def test_playback_speed_takes_priority_over_speed(self):
|
|
"""playback_speed 比 speed 优先级高。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"playback_speed": 1.2, "speed": 2.0},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].playback_speed, 1.2)
|
|
|
|
def test_speed_invalid_ignored(self):
|
|
"""无效 speed 值忽略。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"speed": "fast"},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].playback_speed, 1.0)
|
|
|
|
def test_speed_zero_ignored(self):
|
|
"""speed <= 0 时忽略。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"speed": 0},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].playback_speed, 1.0)
|
|
|
|
def test_chroma_key_and_filter_applied(self):
|
|
"""chroma_key 和 filter 效果层映射。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={
|
|
"chroma_key": {"color": "green", "tolerance": 0.1},
|
|
"filter": "vintage",
|
|
},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["chroma_key"]["color"], "green")
|
|
self.assertEqual(clips[0].config["filter"], "vintage")
|
|
|
|
def test_reverse_effect_applied(self):
|
|
"""reverse 效果层映射。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"reverse": True},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertTrue(clips[0].config["reverse"])
|
|
|
|
def test_existing_config_preserved(self):
|
|
"""已有 config 字段被保留,效果层合并。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "warm", "speed": 1.5},
|
|
),
|
|
]
|
|
clips = [
|
|
VirtualClip(
|
|
id="c1",
|
|
duration=5.0,
|
|
config={"role": "b_roll", "existing_key": "value"},
|
|
)
|
|
]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["role"], "b_roll")
|
|
self.assertEqual(clips[0].config["existing_key"], "value")
|
|
self.assertEqual(clips[0].config["color_grade"], "warm")
|
|
self.assertEqual(clips[0].config["speed"], 1.5)
|
|
|
|
def test_corner_voice_excluded(self):
|
|
"""corner_voice 类型的 clip 不应用效果映射。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="fade",
|
|
config={"color_grade": "warm"},
|
|
),
|
|
]
|
|
clips = [
|
|
VirtualClip(id="c1", clip_type="main", duration=5.0),
|
|
VirtualClip(id="c2", clip_type="corner_voice", duration=3.0),
|
|
]
|
|
apply_template_clip_effects(clips, configs, "VOICE_PIP")
|
|
# main 类型被修改
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
self.assertEqual(clips[0].config.get("color_grade"), "warm")
|
|
# corner_voice 不被修改
|
|
self.assertEqual(clips[1].transition_effect, "cut")
|
|
self.assertEqual(clips[1].config, {})
|
|
|
|
def test_showcase_and_b_roll_clip_types_used(self):
|
|
"""showcase 和 b_roll 类型的 clip_config 也作为模板池。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="showcase",
|
|
transition_effect="slideleft",
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="b_roll",
|
|
transition_effect="dissolve",
|
|
),
|
|
]
|
|
clips = [
|
|
VirtualClip(id="c1", duration=5.0),
|
|
VirtualClip(id="c2", duration=5.0),
|
|
]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "slideleft")
|
|
self.assertEqual(clips[1].transition_effect, "dissolve")
|
|
|
|
def test_cyclic_matching_more_clips_than_configs(self):
|
|
"""素材比模板多时,最后一个模板复用。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "warm"},
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "cool"},
|
|
),
|
|
]
|
|
clips = [
|
|
VirtualClip(id="c1", duration=5.0),
|
|
VirtualClip(id="c2", duration=5.0),
|
|
VirtualClip(id="c3", duration=5.0),
|
|
VirtualClip(id="c4", duration=5.0),
|
|
]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["color_grade"], "warm")
|
|
self.assertEqual(clips[1].config["color_grade"], "cool")
|
|
# 素材3和4都复用最后一个模板(cool)
|
|
self.assertEqual(clips[2].config["color_grade"], "cool")
|
|
self.assertEqual(clips[3].config["color_grade"], "cool")
|
|
|
|
def test_enum_transition_effect(self):
|
|
"""transition_effect 是 Enum 时正确处理。"""
|
|
|
|
class _EnumEffect:
|
|
value = "fade"
|
|
|
|
configs = [
|
|
_MockClipConfig(clip_type="main", transition_effect=_EnumEffect()),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
|
|
def test_config_none_handled(self):
|
|
"""clip_config.config 为 None 时正常处理。"""
|
|
configs = [
|
|
_MockClipConfig(clip_type="main", transition_effect="fade", config=None),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
# config 为 None 时不应有 config 写入
|
|
self.assertEqual(clips[0].config, {})
|
|
|
|
def test_clips_modified_in_place(self):
|
|
"""clips 是就地修改的。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="dissolve",
|
|
config={"color_grade": "warm"},
|
|
),
|
|
]
|
|
clips = [VirtualClip(id="c1", duration=5.0)]
|
|
result = apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertIsNone(result)
|
|
self.assertEqual(clips[0].transition_effect, "dissolve")
|
|
|
|
def test_three_clips_three_configs_matching(self):
|
|
"""三素材三模板一一对应。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "warm"},
|
|
transition_effect="fade",
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "cool"},
|
|
transition_effect="dissolve",
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
config={"color_grade": "vintage"},
|
|
transition_effect="wipeleft",
|
|
),
|
|
]
|
|
clips = [
|
|
VirtualClip(id="c1", duration=5.0),
|
|
VirtualClip(id="c2", duration=5.0),
|
|
VirtualClip(id="c3", duration=5.0),
|
|
]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
self.assertEqual(clips[0].config["color_grade"], "warm")
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
self.assertEqual(clips[1].config["color_grade"], "cool")
|
|
self.assertEqual(clips[1].transition_effect, "dissolve")
|
|
self.assertEqual(clips[2].config["color_grade"], "vintage")
|
|
self.assertEqual(clips[2].transition_effect, "wipeleft")
|
|
|
|
|
|
# ── 集成测试 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEndToEndMapping(unittest.TestCase):
|
|
"""端到端集成测试:intro_outro 提取 + 效果映射完整流程。"""
|
|
|
|
def test_full_template_pipeline(self):
|
|
"""完整模板流水线:intro + main + outro,效果映射正确。"""
|
|
configs = [
|
|
_MockClipConfig(
|
|
clip_type="intro",
|
|
default_duration=2.0,
|
|
text_template="精彩开始",
|
|
config={"intro_type": "text", "intro_text_color": "#ffffff"},
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="main",
|
|
transition_effect="fade",
|
|
config={
|
|
"color_grade": "warm",
|
|
"playback_speed": 1.2,
|
|
"transition_duration": 0.6,
|
|
},
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="b_roll",
|
|
transition_effect="dissolve",
|
|
config={"color_grade": "vivid", "speed": 1.5},
|
|
),
|
|
_MockClipConfig(
|
|
clip_type="outro",
|
|
default_duration=3.0,
|
|
text_template="谢谢观看",
|
|
config={"outro_type": "subscribe", "outro_follow_text": "关注+点赞"},
|
|
),
|
|
]
|
|
|
|
# 效果映射
|
|
clips = [
|
|
VirtualClip(id="c1", clip_type="main", duration=5.0),
|
|
VirtualClip(id="c2", clip_type="main", duration=4.0),
|
|
VirtualClip(id="c3", clip_type="main", duration=3.0),
|
|
]
|
|
apply_template_clip_effects(clips, configs, "ONE_TAKE")
|
|
|
|
# 第一个片段用第一个 main 模板
|
|
self.assertEqual(clips[0].transition_effect, "fade")
|
|
self.assertEqual(clips[0].transition_duration, 0.6)
|
|
self.assertEqual(clips[0].playback_speed, 1.2)
|
|
self.assertEqual(clips[0].config["color_grade"], "warm")
|
|
|
|
# 第二个片段用第二个 b_roll 模板
|
|
self.assertEqual(clips[1].transition_effect, "dissolve")
|
|
self.assertEqual(clips[1].playback_speed, 1.5)
|
|
self.assertEqual(clips[1].config["color_grade"], "vivid")
|
|
|
|
# 第三个片段复用最后一个模板
|
|
self.assertEqual(clips[2].transition_effect, "dissolve")
|
|
self.assertEqual(clips[2].playback_speed, 1.5)
|
|
|
|
# intro/outro 提取
|
|
intro_outro = extract_intro_outro_from_clip_configs(configs)
|
|
self.assertTrue(intro_outro["has_intro"])
|
|
self.assertEqual(intro_outro["intro_duration"], 2.0)
|
|
self.assertEqual(intro_outro["intro_text"], "精彩开始")
|
|
self.assertEqual(intro_outro["intro_text_color"], "#ffffff")
|
|
self.assertTrue(intro_outro["has_outro"])
|
|
self.assertEqual(intro_outro["outro_duration"], 3.0)
|
|
self.assertEqual(intro_outro["outro_text"], "谢谢观看")
|
|
self.assertEqual(intro_outro["outro_follow_text"], "关注+点赞")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|