220 lines
7.9 KiB
Python
Executable File
220 lines
7.9 KiB
Python
Executable File
"""片头片尾配置领域模型 — 纯逻辑,无外部依赖.
|
|
|
|
抽离自 intro_outro_engine.py 的数据类和纯逻辑函数,
|
|
方便单测覆盖,同时保持向后兼容。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
|
|
INTRO_OUTRO_TYPE_NONE = "none"
|
|
INTRO_OUTRO_TYPE_VIDEO = "video"
|
|
INTRO_OUTRO_TYPE_TEXT = "text"
|
|
INTRO_OUTRO_TYPE_FOLLOW = "follow"
|
|
|
|
TRANSITION_FADE = "fade"
|
|
TRANSITION_SLIDE = "slide"
|
|
TRANSITION_WIPE = "wipe"
|
|
|
|
_VALID_INTRO_TYPES = {INTRO_OUTRO_TYPE_NONE, INTRO_OUTRO_TYPE_VIDEO, INTRO_OUTRO_TYPE_TEXT}
|
|
_VALID_OUTRO_TYPES = {
|
|
INTRO_OUTRO_TYPE_NONE,
|
|
INTRO_OUTRO_TYPE_VIDEO,
|
|
INTRO_OUTRO_TYPE_TEXT,
|
|
INTRO_OUTRO_TYPE_FOLLOW,
|
|
}
|
|
|
|
|
|
# ── 数据模型 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class IntroOutroConfig:
|
|
"""片头片尾配置.
|
|
|
|
type: "video" 视频片段 | "text" 纯文字 | "none" 不启用
|
|
"""
|
|
|
|
enabled: bool = False
|
|
|
|
# 片头
|
|
intro_type: str = INTRO_OUTRO_TYPE_NONE # none | video | text
|
|
intro_video_path: str = "" # 视频片段路径
|
|
intro_duration: float = 3.0 # 片头时长(秒)
|
|
|
|
# 文字片头配置
|
|
intro_background: str = "#000000" # 背景色
|
|
intro_title: str = ""
|
|
intro_subtitle: str = ""
|
|
intro_title_color: str = "white"
|
|
intro_title_size: int = 48
|
|
intro_subtitle_color: str = "gray"
|
|
intro_subtitle_size: int = 24
|
|
|
|
# 片尾
|
|
outro_type: str = INTRO_OUTRO_TYPE_NONE # none | video | text | follow
|
|
outro_video_path: str = "" # 视频片段路径
|
|
outro_duration: float = 3.0 # 片尾时长(秒)
|
|
|
|
# 文字片尾配置
|
|
outro_background: str = "#000000"
|
|
outro_title: str = "感谢观看"
|
|
outro_subtitle: str = "点赞关注不迷路"
|
|
outro_title_color: str = "white"
|
|
outro_title_size: int = 48
|
|
outro_subtitle_color: str = "gray"
|
|
outro_subtitle_size: int = 24
|
|
|
|
# 转场
|
|
transition_effect: str = TRANSITION_FADE
|
|
transition_duration: float = 0.5
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any] | None) -> "IntroOutroConfig":
|
|
"""从字典构造."""
|
|
if not data:
|
|
return cls()
|
|
|
|
enabled = data.get("enabled", False)
|
|
if not enabled:
|
|
return cls()
|
|
|
|
intro = data.get("intro", {}) or {}
|
|
outro = data.get("outro", {}) or {}
|
|
|
|
# 安全解析数值,失败时回退到默认值
|
|
try:
|
|
intro_duration = float(intro.get("duration", 3.0))
|
|
except (TypeError, ValueError):
|
|
intro_duration = 3.0
|
|
|
|
try:
|
|
intro_title_size = int(intro.get("title_size", 48))
|
|
except (TypeError, ValueError):
|
|
intro_title_size = 48
|
|
|
|
try:
|
|
intro_subtitle_size = int(intro.get("subtitle_size", 24))
|
|
except (TypeError, ValueError):
|
|
intro_subtitle_size = 24
|
|
|
|
try:
|
|
outro_duration = float(outro.get("duration", 3.0))
|
|
except (TypeError, ValueError):
|
|
outro_duration = 3.0
|
|
|
|
try:
|
|
outro_title_size = int(outro.get("title_size", 48))
|
|
except (TypeError, ValueError):
|
|
outro_title_size = 48
|
|
|
|
try:
|
|
outro_subtitle_size = int(outro.get("subtitle_size", 24))
|
|
except (TypeError, ValueError):
|
|
outro_subtitle_size = 24
|
|
|
|
try:
|
|
transition_duration = float(data.get("transition_duration", 0.5))
|
|
except (TypeError, ValueError):
|
|
transition_duration = 0.5
|
|
|
|
return cls(
|
|
enabled=True,
|
|
# 片头
|
|
intro_type=str(intro.get("type", INTRO_OUTRO_TYPE_NONE)),
|
|
intro_video_path=str(intro.get("video_path", intro.get("video", "")) or ""),
|
|
intro_duration=intro_duration,
|
|
intro_background=str(intro.get("background", "#000000")),
|
|
intro_title=str(intro.get("title", "") or ""),
|
|
intro_subtitle=str(intro.get("subtitle", "") or ""),
|
|
intro_title_color=str(intro.get("title_color", "white")),
|
|
intro_title_size=intro_title_size,
|
|
intro_subtitle_color=str(intro.get("subtitle_color", "gray")),
|
|
intro_subtitle_size=intro_subtitle_size,
|
|
# 片尾
|
|
outro_type=str(outro.get("type", INTRO_OUTRO_TYPE_NONE)),
|
|
outro_video_path=str(outro.get("video_path", outro.get("video", "")) or ""),
|
|
outro_duration=outro_duration,
|
|
outro_background=str(outro.get("background", "#000000")),
|
|
outro_title=str(outro.get("title", "感谢观看") or "感谢观看"),
|
|
outro_subtitle=str(outro.get("subtitle", "点赞关注不迷路") or "点赞关注不迷路"),
|
|
outro_title_color=str(outro.get("title_color", "white")),
|
|
outro_title_size=outro_title_size,
|
|
outro_subtitle_color=str(outro.get("subtitle_color", "gray")),
|
|
outro_subtitle_size=outro_subtitle_size,
|
|
# 转场
|
|
transition_effect=str(data.get("transition", TRANSITION_FADE)),
|
|
transition_duration=transition_duration,
|
|
)
|
|
|
|
@property
|
|
def has_intro(self) -> bool:
|
|
"""是否有片头(视频或文字类型)."""
|
|
return self.enabled and self.intro_type in (
|
|
INTRO_OUTRO_TYPE_VIDEO,
|
|
INTRO_OUTRO_TYPE_TEXT,
|
|
)
|
|
|
|
@property
|
|
def has_outro(self) -> bool:
|
|
"""是否有片尾(视频/文字/follow类型)."""
|
|
return self.enabled and self.outro_type in (
|
|
INTRO_OUTRO_TYPE_VIDEO,
|
|
INTRO_OUTRO_TYPE_TEXT,
|
|
INTRO_OUTRO_TYPE_FOLLOW,
|
|
)
|
|
|
|
@property
|
|
def total_extra_duration(self) -> float:
|
|
"""片头片尾总共增加的时长(秒)."""
|
|
total = 0.0
|
|
if self.has_intro and self.intro_duration > 0:
|
|
total += self.intro_duration
|
|
if self.has_outro and self.outro_duration > 0:
|
|
total += self.outro_duration
|
|
return total
|
|
|
|
def validate(self) -> tuple[bool, str]:
|
|
"""校验配置合法性,返回 (是否合法, 错误信息)."""
|
|
if not self.enabled:
|
|
return True, ""
|
|
|
|
if self.intro_type not in _VALID_INTRO_TYPES:
|
|
return False, f"无效的片头类型: {self.intro_type}"
|
|
|
|
if self.outro_type not in _VALID_OUTRO_TYPES:
|
|
return False, f"无效的片尾类型: {self.outro_type}"
|
|
|
|
if self.intro_type == INTRO_OUTRO_TYPE_VIDEO and not self.intro_video_path:
|
|
return False, "视频片头缺少 video_path"
|
|
if self.intro_type == INTRO_OUTRO_TYPE_TEXT and not self.intro_title:
|
|
return False, "文字片头缺少 title"
|
|
|
|
if self.outro_type == INTRO_OUTRO_TYPE_VIDEO and not self.outro_video_path:
|
|
return False, "视频片尾缺少 video_path"
|
|
if self.outro_type in (INTRO_OUTRO_TYPE_TEXT, INTRO_OUTRO_TYPE_FOLLOW) and not self.outro_title:
|
|
return False, "文字片尾缺少 title"
|
|
|
|
if self.intro_duration <= 0:
|
|
return False, "片头时长必须大于 0"
|
|
if self.outro_duration <= 0:
|
|
return False, "片尾时长必须大于 0"
|
|
|
|
if self.transition_duration < 0:
|
|
return False, "转场时长不能为负数"
|
|
|
|
if self.intro_title_size <= 0:
|
|
return False, "片头标题字号必须大于 0"
|
|
if self.intro_subtitle_size <= 0:
|
|
return False, "片头副标题字号必须大于 0"
|
|
if self.outro_title_size <= 0:
|
|
return False, "片尾标题字号必须大于 0"
|
|
if self.outro_subtitle_size <= 0:
|
|
return False, "片尾副标题字号必须大于 0"
|
|
|
|
return True, ""
|