Files
xiaoxia-saas/packages/domain/intro_outro_config.py

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, ""