Files
video-gen/video-gen-api/app/schemas/private_portrait.py
T
2026-07-07 15:18:52 +08:00

277 lines
12 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 typing import Any
from pydantic import BaseModel, Field, field_validator, model_validator
from app.enums.private_portrait import (
PRIVATE_PORTRAIT_ENABLED_ASSET_TYPES,
PrivatePortraitAssetStatus,
PrivatePortraitAssetType,
PrivatePortraitLibraryType,
PrivatePortraitProjectStatus,
)
from app.schemas.common import NaiveDatetimeOptional
PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS = 2
PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS = 15
class PrivatePortraitEnumItem(BaseModel):
value: str
label: str
description: str | None = None
class PrivatePortraitEnumMetaOut(BaseModel):
library_types: list[PrivatePortraitEnumItem]
asset_types: list[PrivatePortraitEnumItem]
project_statuses: list[PrivatePortraitEnumItem]
asset_statuses: list[PrivatePortraitEnumItem]
class PrivatePortraitConfigOut(BaseModel):
enabled: bool = Field(..., description="是否启用私域人像素材库。asset_limit > 0 表示启用。")
asset_limit: int = Field(..., description="私域人像素材总量限制:真人/虚拟共用,图片/视频共用,0 表示关闭。")
used_asset_count: int = Field(..., description="当前占用额度的素材数量。统计 creating/Processing/Active 的 Image/Video。")
remaining_asset_count: int = Field(..., description="剩余可上传素材数量。")
supported_asset_types: list[str] = Field(default_factory=lambda: [PrivatePortraitAssetType.IMAGE.value, PrivatePortraitAssetType.VIDEO.value], description="当前业务开放的素材类型。")
unsupported_asset_types: list[str] = Field(default_factory=lambda: [PrivatePortraitAssetType.AUDIO.value], description="火山支持但当前业务暂不开放的素材类型。")
# 兼容旧前端,后续确认无引用后可移除。
image_limit: int | None = Field(None, description="兼容旧字段:请改用 asset_limit。")
used_image_count: int | None = Field(None, description="兼容旧字段:请改用 used_asset_count。")
remaining_image_count: int | None = Field(None, description="兼容旧字段:请改用 remaining_asset_count。")
class PrivatePortraitAdminConfigUpdate(BaseModel):
private_portrait_asset_limit: int = Field(..., ge=0, le=9999, description="私域人像素材总量限制。0 表示关闭;>0 表示启用并限制真人/虚拟、图片/视频素材总量。")
class PrivatePortraitProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=128, description="项目组名称。")
description: str | None = Field(None, max_length=2000, description="项目组描述。")
callback_redirect_url: str | None = Field(None, description="仅真人认证使用:认证完成后的手机端提示页地址。")
class PrivatePortraitVirtualProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=128, description="虚拟人像素材项目组名称。创建后会同步创建火山 Asset Group。")
description: str | None = Field(None, max_length=2000, description="虚拟人像素材项目组描述,会同步到火山 Asset Group。")
class PrivatePortraitProjectUpdate(BaseModel):
name: str | None = Field(None, min_length=1, max_length=128, description="项目组名称。")
description: str | None = Field(None, max_length=2000, description="项目组描述。")
status: str | None = Field(None, description="项目状态。普通前端不建议手动变更,仅管理/排查使用。")
class PrivatePortraitProjectOut(BaseModel):
id: str
user_id: str | None = None
library_type: str = Field(default=PrivatePortraitLibraryType.REAL_PERSON.value)
name: str
name_slug: str | None = None
remote_project_name: str | None = None
description: str | None = None
status: str
asset_group_count: int = 0
asset_count: int = 0
image_asset_count: int = 0
video_asset_count: int = 0
active_asset_count: int = 0
active_image_asset_count: int = 0
active_video_asset_count: int = 0
last_used_at: NaiveDatetimeOptional = None
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = {"from_attributes": True}
class PrivatePortraitProjectListOut(BaseModel):
items: list[PrivatePortraitProjectOut]
total: int
page: int
page_size: int
class PrivatePortraitValidateSessionCreate(BaseModel):
callback_redirect_url: str | None = Field(None, description="认证完成后前端要跳转的页面。为空时使用后端默认回调页。")
class PrivatePortraitValidateSessionOut(BaseModel):
id: str
user_id: str | None = None
project_id: str
byted_token: str | None = None
h5_link: str | None = None
callback_url: str | None = None
result_code: str | None = None
algorithm_base_resp_code: str | None = None
verify_type: str | None = None
status: str
remote_group_id: str | None = None
remote_project_name: str | None = None
expired_at: NaiveDatetimeOptional = None
error_message: str | None = None
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = {"from_attributes": True}
class PrivatePortraitProjectCreateWithValidateOut(BaseModel):
project: PrivatePortraitProjectOut
validate_session: PrivatePortraitValidateSessionOut
poll_interval_ms: int = Field(default=2000, description="PC 端轮询认证状态的建议间隔,单位毫秒。")
class PrivatePortraitAssetGroupOut(BaseModel):
id: str
user_id: str | None = None
project_id: str
library_type: str
remote_group_id: str
remote_group_name: str | None = None
remote_project_name: str
group_type: str
status: str
remote_delete_status: str
remote_deleted_at: NaiveDatetimeOptional = None
remote_delete_error: str | None = None
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = {"from_attributes": True}
class PrivatePortraitAssetCreate(BaseModel):
url: str = Field(..., min_length=1, description="已上传到本系统且可公网访问的素材 URL。支持图片/视频,后端会转换公网地址后调用火山 CreateAsset。")
asset_type: str = Field(default=PrivatePortraitAssetType.IMAGE.value, description="素材类型。当前业务仅开放 Image / VideoAudio 暂不开放。")
name: str | None = Field(None, max_length=256, description="素材名称,仅用于检索和管理。")
video_duration: float | None = Field(None, ge=0, description="视频素材时长,单位秒。图片可为空。")
video_cover_url: str | None = Field(None, description="视频封面预览地址。图片可为空。")
file_size: int | None = Field(None, ge=0, description="文件大小,字节。")
mime_type: str | None = Field(None, max_length=128, description="素材 MIME 类型。")
@field_validator("asset_type")
@classmethod
def validate_asset_type(cls, v: str) -> str:
value = v or PrivatePortraitAssetType.IMAGE.value
if value not in {item.value for item in PrivatePortraitAssetType}:
raise ValueError("asset_type 仅支持 Image/VideoAudio 暂未开放")
if value not in PRIVATE_PORTRAIT_ENABLED_ASSET_TYPES:
raise ValueError("Audio 暂未开放,当前仅支持 Image/Video")
return value
@model_validator(mode="after")
def validate_video_duration(self) -> "PrivatePortraitAssetCreate":
if self.asset_type == PrivatePortraitAssetType.VIDEO.value:
duration = self.video_duration
if duration is None:
raise ValueError("Video 素材必须提供 video_duration")
if duration < PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS:
raise ValueError(f"视频素材最短不能少于 {PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS} 秒")
if duration > PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS:
raise ValueError(f"视频素材最长不能超过 {PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS} 秒")
return self
class PrivatePortraitAssetOut(BaseModel):
id: str
user_id: str | None = None
project_id: str
project_name: str | None = None
group_id: str
library_type: str
remote_group_id: str
remote_asset_id: str | None = None
remote_project_name: str | None = None
asset_type: str
name: str | None = None
source_url: str
preview_url: str | None = None
display_url: str | None = None
provider_url: str | None = None
remote_url: str | None = None
remote_url_expired_at: NaiveDatetimeOptional = None
video_duration: float | None = None
video_cover_url: str | None = None
file_size: int | None = None
mime_type: str | None = None
status: str
moderation: Any = None
last_poll_at: NaiveDatetimeOptional = None
next_poll_at: NaiveDatetimeOptional = None
poll_count: int = 0
remote_delete_status: str
remote_deleted_at: NaiveDatetimeOptional = None
remote_delete_error: str | None = None
error_message: str | None = None
created_at: NaiveDatetimeOptional = None
updated_at: NaiveDatetimeOptional = None
model_config = {"from_attributes": True}
class PrivatePortraitAssetListOut(BaseModel):
items: list[PrivatePortraitAssetOut]
total: int
page: int
page_size: int
class PrivatePortraitSelectableAssetOut(BaseModel):
id: str
project_id: str
project_name: str
library_type: str
name: str | None = None
asset_type: str
preview_url: str | None = None
display_url: str | None = None
provider_url: str | None = None
video_duration: float | None = None
video_cover_url: str | None = None
status: str = PrivatePortraitAssetStatus.ACTIVE.value
created_at: NaiveDatetimeOptional = None
class PrivatePortraitSelectableAssetListOut(BaseModel):
items: list[PrivatePortraitSelectableAssetOut]
total: int
page: int
page_size: int
class PrivatePortraitDeleteOut(BaseModel):
success: bool = True
remote_delete_status: str
class PrivatePortraitAdminStatsOut(BaseModel):
total_projects: int = 0
total_assets: int = 0
image_assets: int = 0
video_assets: int = 0
active_assets: int = 0
processing_assets: int = 0
failed_assets: int = 0
real_person_assets: int = 0
virtual_assets: int = 0
def build_private_portrait_enum_meta() -> PrivatePortraitEnumMetaOut:
return PrivatePortraitEnumMetaOut(
library_types=[
PrivatePortraitEnumItem(value=PrivatePortraitLibraryType.REAL_PERSON.value, label="真人认证素材库", description="需要用户扫码完成真人授权认证后才能上传素材。"),
PrivatePortraitEnumItem(value=PrivatePortraitLibraryType.AIGC_VIRTUAL.value, label="私域虚拟人像素材库", description="通过火山 CreateAssetGroup/CreateAsset 入库的虚拟人像素材。"),
],
asset_types=[
PrivatePortraitEnumItem(value=PrivatePortraitAssetType.IMAGE.value, label="图片", description="当前开放。"),
PrivatePortraitEnumItem(value=PrivatePortraitAssetType.VIDEO.value, label="视频", description="当前开放,处理时间通常比图片更长。"),
PrivatePortraitEnumItem(value=PrivatePortraitAssetType.AUDIO.value, label="音频", description="火山支持但当前业务暂不开放。"),
],
project_statuses=[PrivatePortraitEnumItem(value=item.value, label=item.value) for item in PrivatePortraitProjectStatus],
asset_statuses=[PrivatePortraitEnumItem(value=item.value, label=item.value) for item in PrivatePortraitAssetStatus],
)