Files
video-gen/video-gen-api/app/schemas/home_material.py
T
2026-06-30 17:10:51 +08:00

302 lines
18 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.
from __future__ import annotations
from decimal import Decimal
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.enums.home_material import (
HomeMaterialAssetStatus,
HomeMaterialMediaType,
HomeMaterialPublicResponseMode,
HomeMaterialWatermarkPosition,
HomeMaterialWatermarkSizeMode,
HomeMaterialWatermarkType,
)
from app.schemas.common import NaiveDatetimeOptional
class HomeMaterialConfigOut(BaseModel):
enabled: bool = Field(False, description="首页是否展示素材装修模块。false 时前台行业和素材接口返回 enabled=false 且列表为空。")
title: str = Field("行业素材案例", max_length=64, description="首页素材模块标题。")
subtitle: str = Field("精选图片与视频素材展示", max_length=128, description="首页素材模块副标题。")
show_original_in_admin: bool = Field(True, description="后台是否展示原始素材链接。前台接口永远不返回原始素材链接。")
class HomeMaterialConfigUpdate(BaseModel):
enabled: bool = Field(False, description="首页是否展示素材装修模块。")
title: str = Field("行业素材案例", min_length=1, max_length=64, description="首页素材模块标题。")
subtitle: str = Field("精选图片与视频素材展示", max_length=128, description="首页素材模块副标题。")
show_original_in_admin: bool = Field(True, description="后台是否展示原始素材链接。")
class HomeMaterialCategoryCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=64, description="行业名称,例如:美妆护肤、食品生鲜。")
key: str = Field(..., min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$", description="行业唯一标识,只允许字母、数字、下划线、中划线;前台可通过 category_key 查询。")
description: str | None = Field(None, max_length=255, description="行业描述。")
icon: str | None = Field(None, max_length=64, description="前端图标名称,例如 PictureOutlined、VideoCameraOutlined。")
is_active: bool = Field(True, description="是否启用。禁用后前台不展示。")
sort_order: int = Field(0, ge=0, le=999999, description="排序值,越小越靠前。")
class HomeMaterialCategoryUpdate(HomeMaterialCategoryCreate):
pass
class HomeMaterialCategoryOut(BaseModel):
id: str = Field(..., description="行业ID。")
name: str = Field(..., description="行业名称。")
key: str = Field(..., description="行业唯一标识。")
description: str | None = Field(None, description="行业描述。")
icon: str | None = Field(None, description="前端图标名称。")
is_active: bool = Field(True, description="是否启用。")
sort_order: int = Field(0, description="排序值。")
asset_count: int = Field(0, description="可展示素材总数。后台列表按未删除素材统计,前台按 success + is_active 统计。")
image_count: int = Field(0, description="图片素材数量。")
video_count: int = Field(0, description="视频素材数量。")
created_at: NaiveDatetimeOptional = Field(None, description="创建时间。")
updated_at: NaiveDatetimeOptional = Field(None, description="更新时间。")
model_config = ConfigDict(from_attributes=True)
class HomeMaterialCategoryListOut(BaseModel):
items: list[HomeMaterialCategoryOut] = Field(default_factory=list, description="行业列表。")
total: int = Field(0, description="符合条件的行业总数。")
class HomeMaterialWatermarkOut(BaseModel):
id: str = Field(..., description="水印ID。")
name: str = Field(..., description="水印名称。")
file_url: str = Field(..., description="水印图片URL。")
file_name: str | None = Field(None, description="上传文件名。")
file_size_bytes: int = Field(0, description="文件大小,单位字节。")
width: int | None = Field(None, description="水印图片宽度。")
height: int | None = Field(None, description="水印图片高度。")
is_default: bool = Field(False, description="是否默认水印。")
is_active: bool = Field(True, description="是否启用。")
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = ConfigDict(from_attributes=True)
class HomeMaterialWatermarkUpdate(BaseModel):
name: str = Field(..., min_length=1, max_length=128, description="水印名称。")
is_default: bool = Field(False, description="是否设为默认水印。设为 true 时会自动取消其他默认水印。")
is_active: bool = Field(True, description="是否启用。")
class HomeMaterialWatermarkListOut(BaseModel):
items: list[HomeMaterialWatermarkOut] = Field(default_factory=list, description="水印列表。")
total: int = Field(0, description="符合条件的水印总数。")
class HomeMaterialTextWatermarkConfig(BaseModel):
"""重复文字水印配置。字体固定由后端 HOME_MATERIAL_TEXT_WATERMARK_FONT 指定,不允许前端传字体,避免版权和一致性问题。"""
text: str = Field(..., min_length=1, max_length=64, description="重复水印文字。")
opacity_level: int = Field(2, ge=1, le=10, description="文字透明度档位,1-101=0.110=1.0。")
opacity: float = Field(0.2, ge=0.1, le=1.0, description="实际透明度,后端根据 opacity_level 计算。")
font_size_px: int = Field(28, ge=8, le=160, description="字号,单位 px。")
color: str = Field("#ffffff", pattern=r"^#[0-9A-Fa-f]{6}$", description="文字颜色,#RRGGBB。")
rotate_deg: int = Field(-30, ge=-90, le=90, description="文字旋转角度,单位度。")
gap_x: int = Field(220, ge=20, le=2000, description="横向平铺间距。")
gap_y: int = Field(140, ge=20, le=2000, description="纵向平铺间距。")
staggered: bool = Field(True, description="是否交错排列。")
@field_validator("text")
@classmethod
def validate_text(cls, value: str) -> str:
value = (value or "").strip()
if not value:
raise ValueError("重复文字水印内容不能为空")
return value
@model_validator(mode="after")
def fill_opacity(self) -> "HomeMaterialTextWatermarkConfig":
self.opacity = round(self.opacity_level / 10, 2)
return self
class HomeMaterialWatermarkConfig(BaseModel):
watermark_type: HomeMaterialWatermarkType = Field(HomeMaterialWatermarkType.IMAGE, description="水印类型:image=图片水印;repeated_text=重复文字水印。旧数据无该字段时默认 image。")
watermark_id: str | None = Field(None, description="图片水印ID。watermark_type=image 时使用。上传素材时可由 watermark_id 指定,也可通过临时 watermark_file 生成。")
opacity_level: int = Field(6, ge=1, le=10, description="图片水印透明度档位,1-101=0.110=1.0。")
opacity: float = Field(0.6, ge=0.1, le=1.0, description="图片水印实际透明度,后端根据 opacity_level 计算。")
position: HomeMaterialWatermarkPosition = Field(HomeMaterialWatermarkPosition.BOTTOM_RIGHT, description="图片水印位置枚举。")
custom_x_ratio: float | None = Field(None, ge=0, le=1, description="图片水印自定义拖拽位置 X 比例,position=custom 时使用,0-1。")
custom_y_ratio: float | None = Field(None, ge=0, le=1, description="图片水印自定义拖拽位置 Y 比例,position=custom 时使用,0-1。")
size_mode: HomeMaterialWatermarkSizeMode = Field(HomeMaterialWatermarkSizeMode.RATIO, description="图片水印尺寸模式:ratio=按素材宽度比例;px=固定像素宽度。")
width_ratio: float | None = Field(0.18, ge=0.01, le=1, description="图片水印 size_mode=ratio 时使用,水印宽度占素材宽度比例,例如 0.18。")
width_px: int | None = Field(None, ge=1, le=10000, description="图片水印 size_mode=px 时使用,固定水印宽度。")
margin_x: int = Field(24, ge=0, le=2000, description="图片水印横向边距,九宫格位置使用。")
margin_y: int = Field(24, ge=0, le=2000, description="图片水印纵向边距,九宫格位置使用。")
text_watermark: HomeMaterialTextWatermarkConfig | None = Field(None, description="重复文字水印配置。watermark_type=repeated_text 时必填。")
@model_validator(mode="after")
def validate_by_type(self) -> "HomeMaterialWatermarkConfig":
self.opacity = round(self.opacity_level / 10, 2)
if self.watermark_type == HomeMaterialWatermarkType.REPEATED_TEXT:
if self.text_watermark is None:
raise ValueError("watermark_type=repeated_text 时 text_watermark 必填")
self.watermark_id = None
return self
if self.position == HomeMaterialWatermarkPosition.CUSTOM:
if self.custom_x_ratio is None or self.custom_y_ratio is None:
raise ValueError("position=custom 时 custom_x_ratio 和 custom_y_ratio 必填")
if self.size_mode == HomeMaterialWatermarkSizeMode.RATIO and self.width_ratio is None:
raise ValueError("size_mode=ratio 时 width_ratio 必填")
if self.size_mode == HomeMaterialWatermarkSizeMode.PX and self.width_px is None:
raise ValueError("size_mode=px 时 width_px 必填")
self.text_watermark = None
return self
class HomeMaterialTextWatermarkPreviewRequest(BaseModel):
width: int = Field(..., ge=1, le=8192, description="预览层宽度,建议传素材真实宽度。")
height: int = Field(..., ge=1, le=8192, description="预览层高度,建议传素材真实高度。")
text_watermark: HomeMaterialTextWatermarkConfig = Field(..., description="重复文字水印配置。")
class HomeMaterialTextWatermarkPreviewResponse(BaseModel):
width: int = Field(..., description="预览层宽度。")
height: int = Field(..., description="预览层高度。")
preview_layer_data_url: str = Field(..., description="透明 PNG 预览层 data URL。")
class HomeMaterialAssetOut(BaseModel):
id: str = Field(..., description="素材ID。")
category_id: str = Field(..., description="行业ID。")
category_name: str | None = Field(None, description="行业名称,列表接口批量查询回填。")
category_key: str | None = Field(None, description="行业key,列表接口批量查询回填。")
title: str | None = Field(None, description="素材标题。")
media_type: HomeMaterialMediaType = Field(..., description="素材类型:image=图片,video=视频。")
status: HomeMaterialAssetStatus = Field(..., description="处理状态:draft草稿,processing处理中,success成功,failed失败。")
original_url: str | None = Field(None, description="原始素材URL,仅后台返回;前台不返回。")
watermarked_url: str | None = Field(None, description="水印素材URL。")
cover_url: str | None = Field(None, description="视频封面URL。")
watermark_id: str | None = Field(None, description="图片水印ID。重复文字水印素材为空。")
watermark_name: str | None = Field(None, description="图片水印名称,列表接口批量查询回填。重复文字水印显示为空。")
watermark_config: HomeMaterialWatermarkConfig | dict[str, Any] | None = Field(None, description="水印配置快照。")
width: int | None = Field(None, description="素材宽度。")
height: int | None = Field(None, description="素材高度。")
duration_seconds: Decimal | float | None = Field(None, description="视频时长,图片为空。")
file_size_bytes: int = Field(0, description="原始文件大小。")
watermarked_file_size_bytes: int | None = Field(None, description="水印后文件大小。")
is_active: bool = Field(True, description="是否前台展示。")
sort_order: int = Field(0, description="排序值。")
error_message: str | None = Field(None, description="失败原因。")
processed_at: NaiveDatetimeOptional = Field(None, description="处理完成时间。")
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = ConfigDict(from_attributes=True)
class HomeMaterialAssetListOut(BaseModel):
items: list[HomeMaterialAssetOut] = Field(default_factory=list, description="素材列表。")
total: int = Field(0, description="符合条件的素材总数。")
class HomeMaterialAssetUpdate(BaseModel):
category_id: str = Field(..., description="行业ID。")
title: str | None = Field(None, max_length=128, description="素材标题。")
is_active: bool = Field(True, description="是否前台展示。")
sort_order: int = Field(0, ge=0, le=999999, description="排序值。")
@field_validator("title")
@classmethod
def clean_title(cls, value: str | None) -> str | None:
value = (value or "").strip()
return value or None
class HomeMaterialRegenerateWatermarkRequest(HomeMaterialWatermarkConfig):
wait: bool = Field(False, description="是否等待水印处理完成。默认 false,前端轮询状态。")
wait_timeout_seconds: int = Field(30, ge=1, le=60, description="wait=true 时最长等待秒数,最大 60 秒。")
class HomeMaterialAssetStatusOut(BaseModel):
id: str = Field(..., description="素材ID。")
status: HomeMaterialAssetStatus = Field(..., description="处理状态。")
error_message: str | None = Field(None, description="失败原因。")
original_url: str | None = Field(None, description="原始素材URL,仅后台返回。")
watermarked_url: str | None = Field(None, description="水印素材URL。")
cover_url: str | None = Field(None, description="视频封面URL。")
processed_at: NaiveDatetimeOptional = Field(None, description="处理完成时间。")
class HomeMaterialPublicCategoryOut(BaseModel):
id: str = Field(..., description="行业ID。")
name: str = Field(..., description="行业名称。")
key: str = Field(..., description="行业key。")
icon: str | None = Field(None, description="前端图标名。")
description: str | None = Field(None, description="行业描述。")
sort_order: int = Field(0, description="排序。")
asset_count: int = Field(0, description="可展示素材总数。")
image_count: int = Field(0, description="图片素材数量。")
video_count: int = Field(0, description="视频素材数量。")
class HomeMaterialPublicCategoryListOut(BaseModel):
enabled: bool = Field(False, description="首页素材展示开关。false 时 items 为空。")
title: str = Field("行业素材案例", description="首页素材模块标题。")
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
items: list[HomeMaterialPublicCategoryOut] = Field(default_factory=list, description="行业列表。")
class HomeMaterialPublicAssetOut(BaseModel):
id: str = Field(..., description="素材ID。")
title: str | None = Field(None, description="素材标题。")
media_type: HomeMaterialMediaType = Field(..., description="素材类型。")
url: str = Field(..., description="前台展示URL,永远返回 watermarked_url。")
cover_url: str | None = Field(None, description="视频封面URL。")
width: int | None = Field(None, description="宽度。")
height: int | None = Field(None, description="高度。")
duration_seconds: Decimal | float | None = Field(None, description="视频时长。")
sort_order: int = Field(0, description="排序。")
class HomeMaterialPublicCategoryGroupOut(BaseModel):
category: HomeMaterialPublicCategoryOut
assets: list[HomeMaterialPublicAssetOut] = Field(default_factory=list)
class HomeMaterialPublicGroupedOut(BaseModel):
enabled: bool = Field(True, description="首页素材展示开关。")
title: str = Field("行业素材案例", description="首页素材模块标题。")
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
response_mode: HomeMaterialPublicResponseMode = Field(HomeMaterialPublicResponseMode.GROUPED, description="返回模式。")
categories: list[HomeMaterialPublicCategoryGroupOut] = Field(default_factory=list, description="行业分组素材。")
class HomeMaterialPublicFlatItemOut(HomeMaterialPublicAssetOut):
category_id: str = Field(..., description="行业ID。")
category_name: str = Field(..., description="行业名称。")
category_key: str = Field(..., description="行业key。")
category_icon: str | None = Field(None, description="行业图标。")
class HomeMaterialPublicFlatOut(BaseModel):
enabled: bool = Field(True, description="首页素材展示开关。")
title: str = Field("行业素材案例", description="首页素材模块标题。")
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
response_mode: HomeMaterialPublicResponseMode = Field(HomeMaterialPublicResponseMode.FLAT, description="返回模式。")
items: list[HomeMaterialPublicFlatItemOut] = Field(default_factory=list, description="平铺素材列表。")
total: int = Field(0, description="总数。")
class HomeMaterialUploadResultOut(BaseModel):
id: str = Field(..., description="素材ID。")
category_id: str = Field(..., description="行业ID。")
title: str | None = Field(None, description="素材标题。")
media_type: HomeMaterialMediaType = Field(..., description="素材类型。")
status: HomeMaterialAssetStatus = Field(..., description="处理状态。")
original_url: str | None = Field(None, description="原始素材URL。")
watermarked_url: str | None = Field(None, description="水印素材URL。")
cover_url: str | None = Field(None, description="视频封面URL。")
watermark_config: HomeMaterialWatermarkConfig | dict[str, Any] | None = Field(None, description="水印配置快照。")
message: str = Field("", description="提示消息。")