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_body
348 lines
14 KiB
Python
Executable File
348 lines
14 KiB
Python
Executable File
"""剪辑计划 / 模板 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
|