Files
xiaoxia-saas/packages/domain/config_schemas.py
T
xiaoxia e8312482d5
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging API Image (push) Failing after 2s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m23s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Failing after 2s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been skipped
pr_title
pr_body
2026-07-17 09:18:23 +08:00

348 lines
14 KiB
Python
Executable File
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 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="入场动画")
# ASR 自动字幕
auto_generated: bool = Field(default=False, description="是否启用ASR自动生成字幕")
language: str = Field(default="", description="字幕语言,空字符串表示自动检测(如 zh/en/ja")
max_chars_per_line: int = Field(default=20, ge=8, le=40, description="每行最多字符数")
min_chars_per_segment: int = Field(default=8, ge=2, le=20, description="每段最少字符数(低于则合并)")
class BGMConfig(BaseModel):
"""BGM 配置"""
enabled: bool = Field(default=False, description="是否启用 BGM")
source: BGMSource = Field(default=BGMSource.LIBRARY, description="BGM 来源")
asset_id: str = Field(default="", description="BGM 素材 ID(来源为 library/upload 时使用)")
preset_id: str = Field(default="", description="预设 BGM ID(来源为 ai_recommend 或使用内置库时使用)")
audio_url: str = Field(default="", description="BGM 音频 URL(外部直链,优先级最高)")
volume: float = Field(default=0.3, ge=0.0, le=1.0, description="BGM 音量 (0.0 ~ 1.0)")
fade_in: float = Field(default=0.0, ge=0.0, le=30.0, description="淡入时长(秒)")
fade_out: float = Field(default=0.0, ge=0.0, le=30.0, description="淡出时长(秒)")
loop_enabled: bool = Field(default=True, description="BGM 是否循环播放以铺满整个视频时长")
sidechain_enabled: bool = Field(default=False, description="是否启用人声闪避(有人声时 BGM 自动降低音量)")
sidechain_ratio: float = Field(
default=0.3, ge=0.0, le=1.0, description="人声闪避时 BGM 音量降低比例(0.3 = 降低30%"
)
sidechain_attack: float = Field(default=0.02, ge=0.001, le=1.0, description="人声闪避攻击时间(秒)")
sidechain_release: float = Field(default=0.5, ge=0.01, le=5.0, description="人声闪避释放时间(秒)")
sidechain_threshold: float = Field(default=-25.0, ge=-60.0, le=0.0, description="人声闪避触发阈值(dB")
class ExportConfig(BaseModel):
"""导出配置
视频输出参数设置。
"""
resolution: str = Field(default="1080x1920", description="输出分辨率,如 1080x1920 / 720x1280 / 2160x3840")
fps: int = Field(default=30, ge=15, le=60, description="输出帧率 15~60")
video_bitrate: int = Field(default=8000, ge=1000, le=20000, description="视频码率(kbps")
audio_bitrate: int = Field(default=128, ge=64, le=320, description="音频码率(kbps")
format: str = Field(default="mp4", description="输出格式:mp4 / mov")
quality_preset: str = Field(
default="balanced",
description="质量预设:ultra_fast / fast / balanced / high / best",
)
watermark_enabled: bool = Field(default=False, description="是否启用水印")
watermark_text: str = Field(default="", description="水印文字")
class FilterConfig(BaseModel):
"""滤镜调色配置
支持全局滤镜和按片段覆盖。
强度 0-100,0 表示不应用,100 表示全量应用预设。
"""
enabled: bool = Field(default=False, description="是否启用滤镜")
preset_id: str = Field(default="filter_none", description="滤镜预设 ID")
intensity: int = Field(default=100, ge=0, le=100, description="滤镜强度 0-100")
# 自定义微调参数(在预设基础上叠加调整)
brightness: float = Field(default=0.0, ge=-1.0, le=1.0, description="亮度微调")
contrast: float = Field(default=1.0, ge=0.0, le=2.0, description="对比度微调(倍率)")
saturation: float = Field(default=1.0, ge=0.0, le=3.0, description="饱和度微调(倍率)")
warmth: float = Field(default=0.0, ge=-1.0, le=1.0, description="色温微调(正=暖,负=冷)")
# ── 完整 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 配置")
export: ExportConfig = Field(default_factory=ExportConfig, description="导出配置")
filter: FilterConfig = Field(default_factory=FilterConfig, description="滤镜调色配置")
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 默认配置")
export: ExportConfig = Field(default_factory=ExportConfig, description="导出默认配置")
filter: FilterConfig = Field(default_factory=FilterConfig, description="滤镜默认配置")
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": {
"enabled": False,
"source": "library",
"asset_id": "",
"preset_id": "",
"audio_url": "",
"volume": 0.3,
"fade_in": 0.0,
"fade_out": 0.0,
"loop_enabled": True,
"sidechain_enabled": False,
"sidechain_ratio": 0.3,
"sidechain_attack": 0.02,
"sidechain_release": 0.5,
"sidechain_threshold": -25.0,
},
"export": {
"resolution": "1080x1920",
"fps": 30,
"video_bitrate": 8000,
"audio_bitrate": 128,
"format": "mp4",
"quality_preset": "balanced",
"watermark_enabled": False,
"watermark_text": "",
},
"filter": {
"enabled": False,
"preset_id": "filter_none",
"intensity": 100,
"brightness": 0.0,
"contrast": 1.0,
"saturation": 1.0,
"warmth": 0.0,
},
"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