Files
xiaoxia-saas/packages/domain/config_schemas.py
T
xiaoxia ed972a230c
CI/CD Pipeline / Production Browser E2E (push) Failing after 1650h6m27s
CI/CD Pipeline / Deploy Production (push) Failing after 1650h6m30s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1650h6m32s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
feat: 扩展剪辑模板体系 + 实现剪辑计划生成器
2026-07-09 21:21:23 +08:00

260 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""剪辑计划 / 模板 config JSON 结构定义.
定义 EditPlan.config 和 EditTemplate.config 中 cover / title / subtitle / bgm
四个子结构的 Pydantic 模型,供 API 层做入参校验和默认值填充。
所有字段均有合理默认值,前端可只传需要修改的字段。
"""
from __future__ import annotations
import copy
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field
# ── 枚举类型 ──────────────────────────────────────────────────────────────────
class CoverType(str, Enum):
"""封面来源类型"""
AI_FRAME = "ai_frame" # AI 智能选帧
MANUAL = "manual" # 手动选择帧
UPLOAD = "upload" # 用户上传
AI_REGENERATE = "ai_regenerate" # AI 重新生成
class TextPosition(str, Enum):
"""文字位置"""
TOP = "top"
CENTER = "center"
BOTTOM = "bottom"
class TextAnimation(str, Enum):
"""文字动画效果"""
NONE = "none"
FADE_IN = "fade_in"
SLIDE_UP = "slide_up"
SLIDE_DOWN = "slide_down"
SCALE = "scale"
class BGMSource(str, Enum):
"""BGM 来源"""
LIBRARY = "library" # 素材库
UPLOAD = "upload" # 用户上传
AI_RECOMMEND = "ai_recommend" # AI 推荐
# ── 子结构模型 ────────────────────────────────────────────────────────────────
class StrokeConfig(BaseModel):
"""文字描边配置"""
enabled: bool = Field(default=False, description="是否启用描边")
color: str = Field(default="#000000", description="描边颜色 (HEX)")
width: int = Field(default=1, ge=1, le=10, description="描边宽度")
class ShadowConfig(BaseModel):
"""文字阴影配置"""
enabled: bool = Field(default=False, description="是否启用阴影")
blur: int = Field(default=4, ge=0, le=20, description="模糊半径")
offset_x: int = Field(default=2, description="X 偏移")
offset_y: int = Field(default=2, description="Y 偏移")
class CoverConfig(BaseModel):
"""封面配置
type 说明:
- ai_frame: AI 从视频中智能选取最佳帧
- manual: 用户手动指定 frame_time 处的帧
- upload: 用户上传图片,image_url 为上传后的地址
- ai_regenerate: AI 重新生成封面图
"""
type: CoverType = Field(default=CoverType.AI_FRAME, description="封面来源类型")
image_url: str = Field(default="", description="封面图片 URL")
frame_time: Optional[float] = Field(default=None, ge=0.0, description="截取帧时间点 (秒)")
class TitleConfig(BaseModel):
"""标题配置"""
enabled: bool = Field(default=True, description="是否显示标题")
ai_auto: bool = Field(default=True, description="是否由 AI 自动生成标题文案")
text: str = Field(default="", description="标题文案 (ai_auto=false 时由用户填写)")
position: TextPosition = Field(default=TextPosition.TOP, description="标题位置")
font: str = Field(default="思源黑体", description="字体名称")
color: str = Field(default="#ffffff", description="文字颜色 (HEX)")
size: int = Field(default=48, ge=12, le=120, description="字号")
bold: bool = Field(default=True, description="是否加粗")
italic: bool = Field(default=False, description="是否斜体")
stroke: StrokeConfig = Field(default_factory=StrokeConfig, description="描边配置")
shadow: ShadowConfig = Field(default_factory=ShadowConfig, description="阴影配置")
class SubtitleConfig(BaseModel):
"""字幕配置"""
enabled: bool = Field(default=True, description="是否显示字幕")
position: TextPosition = Field(default=TextPosition.BOTTOM, description="字幕位置")
font: str = Field(default="思源黑体", description="字体名称")
color: str = Field(default="#ffffff", description="文字颜色 (HEX)")
size: int = Field(default=24, ge=12, le=60, description="字号")
animation: TextAnimation = Field(default=TextAnimation.FADE_IN, description="入场动画")
class BGMConfig(BaseModel):
"""BGM 配置"""
source: BGMSource = Field(default=BGMSource.LIBRARY, description="BGM 来源")
asset_id: str = Field(default="", description="BGM 素材 ID")
volume: float = Field(default=0.3, ge=0.0, le=1.0, description="音量 (0.0 ~ 1.0)")
# ── 完整 config 模型 ─────────────────────────────────────────────────────────
class EditPlanConfigSchema(BaseModel):
"""EditPlan.config 完整结构
用于 API 层校验和默认值填充。所有子结构均可选,
未传入时使用各自默认值。
editing_mode 记录计划使用的剪辑模式。
"""
cover: CoverConfig = Field(default_factory=CoverConfig, description="封面配置")
title: TitleConfig = Field(default_factory=TitleConfig, description="标题配置")
subtitle: SubtitleConfig = Field(default_factory=SubtitleConfig, description="字幕配置")
bgm: BGMConfig = Field(default_factory=BGMConfig, description="BGM 配置")
editing_mode: str = Field(default="one_take", description="剪辑模式")
class EditTemplateConfigSchema(BaseModel):
"""EditTemplate.config 完整结构
模板级别的默认配置,创建计划时可作为初始值继承。
editing_mode 指定模板对应的剪辑模式,transition_enabled 控制是否启用转场。
"""
cover: CoverConfig = Field(default_factory=CoverConfig, description="封面默认配置")
title: TitleConfig = Field(default_factory=TitleConfig, description="标题默认配置")
subtitle: SubtitleConfig = Field(default_factory=SubtitleConfig, description="字幕默认配置")
bgm: BGMConfig = Field(default_factory=BGMConfig, description="BGM 默认配置")
editing_mode: str = Field(default="one_take", description="剪辑模式")
transition_enabled: bool = Field(default=True, description="是否启用转场")
# ── 默认值常量 ────────────────────────────────────────────────────────────────
DEFAULT_EDIT_PLAN_CONFIG: dict = {
"cover": {
"type": "ai_frame",
"image_url": "",
"frame_time": None,
},
"title": {
"enabled": True,
"ai_auto": True,
"text": "",
"position": "top",
"font": "思源黑体",
"color": "#ffffff",
"size": 48,
"bold": True,
"italic": False,
"stroke": {"enabled": False, "color": "#000000", "width": 1},
"shadow": {"enabled": False, "blur": 4, "offset_x": 2, "offset_y": 2},
},
"subtitle": {
"enabled": True,
"position": "bottom",
"font": "思源黑体",
"color": "#ffffff",
"size": 24,
"animation": "fade_in",
},
"bgm": {
"source": "library",
"asset_id": "",
"volume": 0.3,
},
"editing_mode": "one_take",
}
DEFAULT_EDIT_TEMPLATE_CONFIG: dict = {
**DEFAULT_EDIT_PLAN_CONFIG,
"transition_enabled": True,
}
# ── 工具函数 ──────────────────────────────────────────────────────────────────
def normalize_plan_config(raw: dict | None) -> dict:
"""将原始 config dict 标准化,填充缺失字段为默认值。
用于创建/更新计划时确保 config 结构完整。
"""
if raw is None:
return copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
base = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
for section_key in ("cover", "title", "subtitle", "bgm"):
if section_key in raw and isinstance(raw[section_key], dict):
if section_key not in base:
base[section_key] = {}
base[section_key].update(raw[section_key])
# editing_mode 顶层字段
if "editing_mode" in raw and isinstance(raw["editing_mode"], str):
base["editing_mode"] = raw["editing_mode"]
# 保留非标准字段(如 generation_task_id)
for key, value in raw.items():
if key not in ("cover", "title", "subtitle", "bgm", "editing_mode"):
base[key] = value
return base
def normalize_template_config(raw: dict | None) -> dict:
"""将模板原始 config dict 标准化。
在 plan config 基础上额外支持 transition_enabled 字段。
"""
if raw is None:
return copy.deepcopy(DEFAULT_EDIT_TEMPLATE_CONFIG)
base = copy.deepcopy(DEFAULT_EDIT_TEMPLATE_CONFIG)
for section_key in ("cover", "title", "subtitle", "bgm"):
if section_key in raw and isinstance(raw[section_key], dict):
if section_key not in base:
base[section_key] = {}
base[section_key].update(raw[section_key])
# 顶层字段
if "editing_mode" in raw and isinstance(raw["editing_mode"], str):
base["editing_mode"] = raw["editing_mode"]
if "transition_enabled" in raw and isinstance(raw["transition_enabled"], bool):
base["transition_enabled"] = raw["transition_enabled"]
# 保留非标准字段
for key, value in raw.items():
if key not in ("cover", "title", "subtitle", "bgm", "editing_mode", "transition_enabled"):
base[key] = value
return base