Files
video-gen/video-gen-api/app/schemas/private_portrait.py
T

401 lines
29 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,
PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME,
PrivatePortraitAssetStatus,
PrivatePortraitAssetType,
PrivatePortraitLibraryType,
PrivatePortraitProjectStatus,
PrivatePortraitRemoteDeleteStatus,
PrivatePortraitValidateSessionStatus,
)
from app.schemas.common import NaiveDatetimeOptional
PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS = 2
PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS = 15
_LIBRARY_TYPE_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitLibraryType.REAL_PERSON.value: (
"真人认证素材库",
"真人素材库。创建项目时会生成火山真人认证 H5 链接,用户扫码并认证成功后,项目状态变为 active,才允许上传真人素材。",
),
PrivatePortraitLibraryType.AIGC_VIRTUAL.value: (
"私域虚拟人像素材库",
"虚拟素材库。创建项目时直接同步火山 CreateAssetGroupGroupType=AIGC,不需要真人扫码认证。",
),
}
_PROJECT_STATUS_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitProjectStatus.VALIDATING.value: ("认证中", "真人项目已创建,等待用户扫码完成真人认证。"),
PrivatePortraitProjectStatus.ACTIVE.value: ("可用", "项目可用,允许上传素材,项目内 Active 素材可用于 AI 创作。"),
PrivatePortraitProjectStatus.VALIDATE_FAILED.value: ("认证失败", "真人认证失败或回调结果不通过。"),
PrivatePortraitProjectStatus.CREATING_REMOTE_GROUP.value: ("远端分组创建中", "正在调用火山创建 Asset Group。"),
PrivatePortraitProjectStatus.CREATE_GROUP_FAILED.value: ("远端分组创建失败", "火山 Asset Group 创建失败,需要重试或人工排查。"),
PrivatePortraitProjectStatus.DELETED.value: ("已删除", "本地已软删,不再对普通前端展示。"),
}
_VALIDATE_SESSION_STATUS_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitValidateSessionStatus.CREATED.value: ("已创建", "认证会话已创建,前端可展示 h5_link 二维码。"),
PrivatePortraitValidateSessionStatus.CALLBACK_SUCCESS.value: ("回调成功", "火山回调 resultCode=10000,认证通过,等待或已绑定远端分组。"),
PrivatePortraitValidateSessionStatus.CALLBACK_FAILED.value: ("回调失败", "火山回调 resultCode 非成功值,认证未通过。"),
PrivatePortraitValidateSessionStatus.GROUP_ACTIVE.value: ("分组可用", "认证成功并已获得可用 remote_group_id,项目可上传素材。"),
PrivatePortraitValidateSessionStatus.EXPIRED.value: ("已过期", "认证会话超过有效期,需要重新创建认证会话。"),
PrivatePortraitValidateSessionStatus.FAILED.value: ("失败", "认证会话创建、查询或回调处理失败。"),
}
_ASSET_TYPE_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitAssetType.IMAGE.value: ("图片", "当前开放。用于真人/虚拟图片参考素材。"),
PrivatePortraitAssetType.VIDEO.value: (
"视频",
f"当前开放。必须传 video_duration,支持 {PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS}~{PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS} 秒。",
),
PrivatePortraitAssetType.AUDIO.value: ("音频", "火山支持但当前业务暂不开放,接口会拒绝 Audio。"),
}
_ASSET_STATUS_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitAssetStatus.CREATING.value: ("创建中", "本地记录已创建,正在调用火山 CreateAsset。"),
PrivatePortraitAssetStatus.PROCESSING.value: ("处理中", "火山侧正在处理素材,暂不可用于 AI 创作。"),
PrivatePortraitAssetStatus.ACTIVE.value: ("可用", "素材已处理完成,可在 AI 创作中作为参考素材选择。"),
PrivatePortraitAssetStatus.FAILED.value: ("失败", "火山侧处理失败或本地同步失败。"),
PrivatePortraitAssetStatus.LOCAL_DELETED.value: ("本地已删除", "本地已软删,等待远端删除或无需远端删除。"),
PrivatePortraitAssetStatus.REMOTE_DELETED.value: ("远端已删除", "火山侧素材删除成功。"),
PrivatePortraitAssetStatus.DELETE_FAILED.value: ("删除失败", "火山侧删除失败,后续可通过补偿任务重试。"),
}
_REMOTE_DELETE_STATUS_DESCRIPTIONS: dict[str, tuple[str, str]] = {
PrivatePortraitRemoteDeleteStatus.NONE.value: ("无需删除", "未触发远端删除,通常表示资源仍正常。"),
PrivatePortraitRemoteDeleteStatus.PENDING.value: ("等待删除", "本地已软删并提交成功,远端删除任务等待执行或执行中。"),
PrivatePortraitRemoteDeleteStatus.SUCCESS.value: ("删除成功", "火山侧远端资源已删除。"),
PrivatePortraitRemoteDeleteStatus.FAILED.value: ("删除失败", "火山侧远端删除失败,已记录错误,后续可补偿重试。"),
PrivatePortraitRemoteDeleteStatus.SKIPPED.value: ("跳过删除", "资源没有远端 ID 或已不需要调用远端删除。"),
}
def _enum_items(enum_values: list[str], mapping: dict[str, tuple[str, str]]) -> list["PrivatePortraitEnumItem"]:
return [
PrivatePortraitEnumItem(
value=value,
label=mapping.get(value, (value, ""))[0],
description=mapping.get(value, (value, None))[1],
)
for value in enum_values
]
class PrivatePortraitEnumItem(BaseModel):
value: str = Field(..., description="枚举值,接口入参和出参均使用该值。")
label: str = Field(..., description="枚举中文名称,用于前端展示。")
description: str | None = Field(None, description="枚举说明,用于 OpenAPI 文档、前端提示或排查。")
class PrivatePortraitEnumMetaOut(BaseModel):
library_types: list[PrivatePortraitEnumItem] = Field(..., description="素材库类型枚举:real_person=真人认证素材库,aigc_virtual=私域虚拟人像素材库。")
asset_types: list[PrivatePortraitEnumItem] = Field(..., description="素材类型枚举:Image=图片,Video=视频,Audio=音频;当前业务仅开放 Image/Video。")
project_statuses: list[PrivatePortraitEnumItem] = Field(..., description="项目状态枚举,用于项目列表、项目详情、筛选和状态展示。")
asset_statuses: list[PrivatePortraitEnumItem] = Field(..., description="素材状态枚举。只有 Active 素材可被 selectable-assets 返回并用于 AI 创作。")
validate_session_statuses: list[PrivatePortraitEnumItem] = Field(..., description="真人认证会话状态枚举,用于 PC 端轮询扫码认证结果。")
remote_delete_statuses: list[PrivatePortraitEnumItem] = Field(..., description="远端删除状态枚举。本地删除先 commit,远端删除异步执行。")
class PrivatePortraitConfigOut(BaseModel):
enabled: bool = Field(..., description="是否启用私域人像素材库。asset_limit > 0 表示启用;asset_limit = 0 表示关闭。")
asset_limit: int = Field(..., description="私域素材总量限制。真人/虚拟共用,图片/视频共用;0 表示关闭私域素材模块。")
used_asset_count: int = Field(..., description="当前已占用额度的素材数量。统计真人+虚拟、Image+Video,状态包含 creating/Processing/Active。")
remaining_asset_count: int = Field(..., description="剩余可上传素材数量。计算方式:max(asset_limit - used_asset_count, 0)。")
supported_asset_types: list[str] = Field(
default_factory=lambda: [PrivatePortraitAssetType.IMAGE.value, PrivatePortraitAssetType.VIDEO.value],
description="当前业务开放的素材类型枚举值。固定为 Image / Video。",
)
unsupported_asset_types: list[str] = Field(
default_factory=lambda: [PrivatePortraitAssetType.AUDIO.value],
description="火山支持但当前业务暂不开放的素材类型。当前为 Audio。",
)
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="真人认证素材项目组名称。前端展示用,不直接作为火山 ProjectName。")
description: str | None = Field(None, max_length=2000, description="真人认证素材项目组描述,前端展示和管理备注用。")
callback_redirect_url: str | None = Field(
None,
description="真人认证完成后的手机端跳转地址。为空时使用后端默认回调页;PC 端仍应通过 validate_session 轮询最终状态。",
)
class PrivatePortraitVirtualProjectCreate(BaseModel):
name: str = Field(
...,
min_length=1,
max_length=128,
description="虚拟人像素材项目组名称。创建后会同步火山 CreateAssetGroupGroupType=AIGC。",
)
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="项目状态。可选值:validating、active、validate_failed、creating_remote_group、create_group_failed、deleted。普通前端不建议手动变更。",
)
class PrivatePortraitProjectOut(BaseModel):
id: str = Field(..., description="本地私域项目 ID。后续查询、更新、删除、上传素材均使用该 ID。")
user_id: str | None = Field(None, description="项目所属用户 ID。普通用户接口一般返回当前用户 ID;管理接口用于定位用户。")
library_type: str = Field(default=PrivatePortraitLibraryType.REAL_PERSON.value, description="素材库类型:real_person=真人认证素材库,aigc_virtual=私域虚拟素材库。")
name: str = Field(..., description="项目组名称,前端展示用。")
name_slug: str | None = Field(None, description="项目名称归一化值,兼容旧逻辑或排查使用。")
remote_project_name: str | None = Field(None, description=f"火山侧 ProjectName 快照。当前固定为 {PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME}。")
description: str | None = Field(None, description="项目组描述。")
status: str = Field(..., description="项目状态。active 表示可上传素材;真人项目 validating 表示等待扫码认证。")
asset_group_count: int = Field(0, description="项目下本地素材组数量。真人一般认证成功后为 1,虚拟创建项目后为 1。")
asset_count: int = Field(0, description="项目下素材总数,包含图片和视频。")
image_asset_count: int = Field(0, description="项目下图片素材数量。")
video_asset_count: int = Field(0, description="项目下视频素材数量。")
active_asset_count: int = Field(0, description="项目下 Active 可用素材总数。")
active_image_asset_count: int = Field(0, description="项目下 Active 可用图片素材数量。")
active_video_asset_count: int = Field(0, description="项目下 Active 可用视频素材数量。")
last_used_at: NaiveDatetimeOptional = Field(None, description="最近被 AI 创作引用的时间。为空表示尚未使用。")
created_at: NaiveDatetimeOptional = Field(None, description="本地项目创建时间。")
updated_at: NaiveDatetimeOptional = Field(None, description="本地项目最后更新时间。")
model_config = {"from_attributes": True}
class PrivatePortraitProjectListOut(BaseModel):
items: list[PrivatePortraitProjectOut] = Field(..., description="项目列表。真人接口只返回 real_person,虚拟接口只返回 aigc_virtual。")
total: int = Field(..., description="符合筛选条件的项目总数。")
page: int = Field(..., description="当前页码,从 1 开始。")
page_size: int = Field(..., description="每页数量。")
class PrivatePortraitValidateSessionCreate(BaseModel):
callback_redirect_url: str | None = Field(
None,
description="真人认证完成后手机端要跳转的页面。为空时使用后端默认回调页;PC 端应继续轮询 validate-session。",
)
class PrivatePortraitValidateSessionOut(BaseModel):
id: str = Field(..., description="本地认证会话 ID。PC 端轮询认证状态时使用。")
user_id: str | None = Field(None, description="认证会话所属用户 ID。")
project_id: str = Field(..., description="认证会话绑定的本地真人项目 ID。")
byted_token: str | None = Field(None, description="火山认证 token,排查用。前端一般不直接展示。")
h5_link: str | None = Field(None, description="火山真人认证 H5 链接。前端应转二维码供手机扫码。")
callback_url: str | None = Field(None, description="后端传给火山的回调地址。认证完成后火山会请求该地址。")
result_code: str | None = Field(None, description="火山回调 resultCode。10000 表示成功,其他值表示失败或未通过。")
algorithm_base_resp_code: str | None = Field(None, description="火山算法返回码,排查认证失败原因用。")
verify_type: str | None = Field(None, description="火山认证类型。当前真人认证使用 real_time。")
status: str = Field(..., description="认证会话状态:created、callback_success、callback_failed、group_active、expired、failed。")
remote_group_id: str | None = Field(None, description="认证成功后火山返回的 Asset Group ID。后续上传真人素材会绑定该分组。")
remote_project_name: str | None = Field(None, description=f"火山侧 ProjectName 快照。当前固定为 {PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME}。")
expired_at: NaiveDatetimeOptional = Field(None, description="认证会话过期时间。过期后需要重新创建认证会话。")
error_message: str | None = Field(None, description="认证失败、回调失败或查询失败的错误信息。")
created_at: NaiveDatetimeOptional = Field(None, description="认证会话创建时间。")
updated_at: NaiveDatetimeOptional = Field(None, description="认证会话最后更新时间。")
model_config = {"from_attributes": True}
class PrivatePortraitProjectCreateWithValidateOut(BaseModel):
project: PrivatePortraitProjectOut = Field(..., description="创建成功的真人项目数据。初始状态通常为 validating。")
validate_session: PrivatePortraitValidateSessionOut = Field(..., description="随项目创建一起生成的真人认证会话。前端用 h5_link 展示二维码。")
poll_interval_ms: int = Field(default=2000, description="PC 端轮询认证状态的建议间隔,单位毫秒。")
class PrivatePortraitAssetGroupOut(BaseModel):
id: str = Field(..., description="本地素材组 ID。")
user_id: str | None = Field(None, description="素材组所属用户 ID。")
project_id: str = Field(..., description="素材组所属本地项目 ID。")
library_type: str = Field(..., description="素材库类型:real_person=真人认证素材库,aigc_virtual=私域虚拟素材库。")
remote_group_id: str = Field(..., description="火山 Asset Group ID。")
remote_group_name: str | None = Field(None, description="火山 Asset Group 名称。")
remote_project_name: str = Field(..., description=f"火山侧 ProjectName。当前固定为 {PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME}。")
group_type: str = Field(..., description="火山分组类型。真人为 LivenessFace,虚拟为 AIGC。")
status: str = Field(..., description="素材组本地状态:creating、active、local_deleted、remote_deleted、delete_failed、failed。")
remote_delete_status: str = Field(..., description="远端删除状态:none、pending、success、failed、skipped。")
remote_deleted_at: NaiveDatetimeOptional = Field(None, description="远端删除成功时间。")
remote_delete_error: str | None = Field(None, description="远端删除失败错误信息。")
created_at: NaiveDatetimeOptional = Field(None, description="素材组创建时间。")
updated_at: NaiveDatetimeOptional = Field(None, description="素材组最后更新时间。")
model_config = {"from_attributes": True}
class PrivatePortraitUploadOut(BaseModel):
url: str = Field(..., description="上传后的本地资源 URL,可直接用于创建私域素材")
filename: str = Field(..., description="原始文件名或安全文件名")
type: str = Field(..., description="资源类型:image/video")
module: str = Field(..., description="上传模块:private_portrait_real/private_portrait_virtual")
resource_id: str = Field(..., description="UploadResource.id。创建素材时必须作为 upload_resource_id 传回后端绑定素材")
file_size_bytes: int = Field(0, description="文件大小,单位字节")
duration_seconds: float | None = Field(None, description="视频素材秒数,图片为空")
class PrivatePortraitAssetCreate(BaseModel):
url: str = Field(
...,
min_length=1,
description="已上传到本系统且可公网访问的素材 URL。支持图片/视频,后端会转换为公网地址后调用火山 CreateAsset。",
)
upload_resource_id: str | None = Field(
None,
max_length=32,
description="可选但新客户端必须传:真人/虚拟专用上传接口返回的 UploadResource.id。后端 CreateAsset 成功后会绑定到 PrivatePortraitAsset。",
)
asset_type: str = Field(
default=PrivatePortraitAssetType.IMAGE.value,
description="素材类型枚举:Image=图片,Video=视频,Audio=音频。当前业务仅开放 Image / VideoAudio 会被拒绝。",
)
name: str | None = Field(None, max_length=256, description="素材名称。用于前端展示、检索和管理备注;不影响远端素材处理。")
video_duration: float | None = Field(
None,
ge=0,
description=f"视频素材时长,单位秒。asset_type=Video 时必填,范围 {PRIVATE_PORTRAIT_VIDEO_MIN_DURATION_SECONDS}~{PRIVATE_PORTRAIT_VIDEO_MAX_DURATION_SECONDS} 秒;图片可为空。",
)
video_cover_url: str | None = Field(None, description="视频封面预览地址。asset_type=Video 时建议传入;图片可为空。")
file_size: int | None = Field(None, ge=0, description="文件大小,单位字节。用于前端展示和排查。")
mime_type: str | None = Field(None, max_length=128, description="素材 MIME 类型,例如 image/png、video/mp4。用于前端展示和排查。")
@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":
# 允许新客户端只传 upload_resource_id,服务层会优先从 UploadResource.duration_seconds 回填秒数。
# 如果前端已传 video_duration,则这里先做范围校验,避免无效视频进入远端入库。
if self.asset_type == PrivatePortraitAssetType.VIDEO.value and self.video_duration is not None:
duration = self.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 = Field(..., description="本地素材 ID。删除、同步、详情查询均使用该 ID。")
user_id: str | None = Field(None, description="素材所属用户 ID。")
project_id: str = Field(..., description="素材所属项目 ID。")
project_name: str | None = Field(None, description="素材所属项目名称。列表接口会尽量回填,详情接口会单独查询回填。")
group_id: str = Field(..., description="本地素材组 ID。")
library_type: str = Field(..., description="素材库类型:real_person=真人认证素材库,aigc_virtual=私域虚拟素材库。")
remote_group_id: str = Field(..., description="火山 Asset Group ID。")
remote_asset_id: str | None = Field(None, description="火山 Asset ID。创建成功后回填。")
remote_project_name: str | None = Field(None, description=f"火山侧 ProjectName 快照。当前固定为 {PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME}。")
asset_type: str = Field(..., description="素材类型:Image=图片,Video=视频。Audio 当前不会返回给普通业务。")
name: str | None = Field(None, description="素材名称。")
source_url: str = Field(..., description="本地原始素材 URL。用于回显和排查,生成时通常使用 display_url/provider_url。")
preview_url: str | None = Field(None, description="前端预览 URL。图片/视频都优先使用该字段展示缩略或预览。")
display_url: str | None = Field(None, description="前端可访问展示 URL。用于素材列表、素材云或 AI 创作选择器展示。")
provider_url: str | None = Field(None, description="提供给生成服务使用的 URL。可能是本地签名 URL 或远端可访问 URL。")
remote_url: str | None = Field(None, description="火山侧返回的素材 URL。可能有过期时间,仅排查或兜底使用。")
remote_url_expired_at: NaiveDatetimeOptional = Field(None, description="remote_url 过期时间。过期后应重新同步素材状态或使用 display_url/provider_url。")
video_duration: float | None = Field(None, description="视频素材时长,单位秒。图片为空。")
video_cover_url: str | None = Field(None, description="视频封面 URL。图片为空。")
file_size: int | None = Field(None, description="素材文件大小,单位字节。")
mime_type: str | None = Field(None, description="素材 MIME 类型。")
status: str = Field(..., description="素材状态:creating、Processing、Active、Failed、local_deleted、remote_deleted、delete_failed。只有 Active 可用于 AI 创作。")
moderation: Any = Field(None, description="火山审核或处理返回的原始补充信息。结构可能随火山返回变化,前端一般不依赖该字段。")
last_poll_at: NaiveDatetimeOptional = Field(None, description="最近一次同步/轮询火山素材状态的时间。")
next_poll_at: NaiveDatetimeOptional = Field(None, description="下一次计划同步/轮询火山素材状态的时间。")
poll_count: int = Field(0, description="素材状态轮询次数。用于排查处理超时。")
remote_delete_status: str = Field(..., description="远端删除状态:none、pending、success、failed、skipped。")
remote_deleted_at: NaiveDatetimeOptional = Field(None, description="远端素材删除成功时间。")
remote_delete_error: str | None = Field(None, description="远端素材删除失败错误信息。")
error_message: str | None = Field(None, description="创建、同步、处理或删除过程中的错误信息。")
created_at: NaiveDatetimeOptional = Field(None, description="素材创建时间。")
updated_at: NaiveDatetimeOptional = Field(None, description="素材最后更新时间。")
model_config = {"from_attributes": True}
class PrivatePortraitAssetListOut(BaseModel):
items: list[PrivatePortraitAssetOut] = Field(..., description="素材列表。包含图片/视频,是否可用以 status=Active 为准。")
total: int = Field(..., description="符合筛选条件的素材总数。")
page: int = Field(..., description="当前页码,从 1 开始。")
page_size: int = Field(..., description="每页数量。")
class PrivatePortraitSelectableAssetOut(BaseModel):
id: str = Field(..., description="本地素材 ID。AI 创作选择后可作为 private_asset_id / private_portrait_asset 引用。")
project_id: str = Field(..., description="素材所属项目 ID。")
project_name: str = Field(..., description="素材所属项目名称。")
library_type: str = Field(..., description="素材库类型:real_person=真人认证素材库,aigc_virtual=私域虚拟素材库。")
name: str | None = Field(None, description="素材名称。")
asset_type: str = Field(..., description="素材类型:Image=图片,Video=视频。")
preview_url: str | None = Field(None, description="前端选择器预览 URL。")
display_url: str | None = Field(None, description="前端展示 URL。")
provider_url: str | None = Field(None, description="传给 AI 创作生成接口的可访问 URL。")
video_duration: float | None = Field(None, description="视频素材时长,单位秒。图片为空。AI 创作会用它做视频总时长校验。")
video_cover_url: str | None = Field(None, description="视频封面 URL。图片为空。")
status: str = Field(default=PrivatePortraitAssetStatus.ACTIVE.value, description="固定返回 Active。该接口只返回可用于 AI 创作的素材。")
created_at: NaiveDatetimeOptional = Field(None, description="素材创建时间。")
class PrivatePortraitSelectableAssetListOut(BaseModel):
items: list[PrivatePortraitSelectableAssetOut] = Field(..., description="可用于 AI 创作选择器的素材列表。只包含 Active 的 Image/Video。")
total: int = Field(..., description="符合筛选条件的可选素材总数。")
page: int = Field(..., description="当前页码,从 1 开始。")
page_size: int = Field(..., description="每页数量。")
class PrivatePortraitDeleteOut(BaseModel):
success: bool = Field(True, description="本地删除是否成功。接口返回时表示本地软删已提交成功。")
remote_delete_status: str = Field(..., description="远端删除状态。通常返回 pending,表示远端删除任务已投递或等待补偿。")
class PrivatePortraitAdminStatsOut(BaseModel):
total_projects: int = Field(0, description="项目总数,包含真人和虚拟项目。")
total_assets: int = Field(0, description="素材总数,包含真人/虚拟、图片/视频。")
image_assets: int = Field(0, description="图片素材数量。")
video_assets: int = Field(0, description="视频素材数量。")
active_assets: int = Field(0, description="Active 可用素材数量。")
processing_assets: int = Field(0, description="Processing 处理中素材数量。")
failed_assets: int = Field(0, description="Failed 失败素材数量。")
real_person_assets: int = Field(0, description="真人素材数量,library_type=real_person。")
virtual_assets: int = Field(0, description="虚拟素材数量,library_type=aigc_virtual。")
def build_private_portrait_enum_meta() -> PrivatePortraitEnumMetaOut:
return PrivatePortraitEnumMetaOut(
library_types=_enum_items(
[PrivatePortraitLibraryType.REAL_PERSON.value, PrivatePortraitLibraryType.AIGC_VIRTUAL.value],
_LIBRARY_TYPE_DESCRIPTIONS,
),
asset_types=_enum_items(
[PrivatePortraitAssetType.IMAGE.value, PrivatePortraitAssetType.VIDEO.value, PrivatePortraitAssetType.AUDIO.value],
_ASSET_TYPE_DESCRIPTIONS,
),
project_statuses=_enum_items([item.value for item in PrivatePortraitProjectStatus], _PROJECT_STATUS_DESCRIPTIONS),
asset_statuses=_enum_items([item.value for item in PrivatePortraitAssetStatus], _ASSET_STATUS_DESCRIPTIONS),
validate_session_statuses=_enum_items([item.value for item in PrivatePortraitValidateSessionStatus], _VALIDATE_SESSION_STATUS_DESCRIPTIONS),
remote_delete_statuses=_enum_items([item.value for item in PrivatePortraitRemoteDeleteStatus], _REMOTE_DELETE_STATUS_DESCRIPTIONS),
)