上传素材管控-历史素材success
This commit is contained in:
@@ -43,8 +43,11 @@ class GenerationAIReference(BaseModel):
|
||||
)
|
||||
source: str | None = Field(
|
||||
None,
|
||||
description="参考素材来源。private_portrait_asset=真人素材库;为空表示普通上传文件",
|
||||
examples=["private_portrait_asset"],
|
||||
description=(
|
||||
"参考素材来源枚举:upload_resource=历史上传素材;"
|
||||
"private_portrait_asset=私域真人/虚拟素材;为空表示本次普通上传素材。"
|
||||
),
|
||||
examples=["upload_resource"],
|
||||
)
|
||||
private_asset_id: str | None = Field(
|
||||
None,
|
||||
@@ -73,9 +76,17 @@ class GenerationAIReference(BaseModel):
|
||||
)
|
||||
role: str | None = Field(
|
||||
None,
|
||||
description="参考素材角色。image 可选 first_frame/last_frame/reference_image;video 可选 reference_video;audio 可选 reference_audio",
|
||||
description=(
|
||||
"参考素材角色枚举:first_frame=首帧图;last_frame=尾帧图;"
|
||||
"reference_image=普通参考图;reference_video=普通参考视频;reference_audio=普通参考音频。"
|
||||
),
|
||||
examples=["first_frame"],
|
||||
)
|
||||
upload_resource_id: str | None = Field(
|
||||
None,
|
||||
description="历史上传素材 ID。source=upload_resource 时由 /upload-resources/history 返回,便于前端排查和去重;生成链路主要使用 url。",
|
||||
examples=["0019e0a448a23114888"],
|
||||
)
|
||||
|
||||
|
||||
class GenerationAITaskCreate(BaseModel):
|
||||
@@ -147,7 +158,11 @@ class GenerationAITaskCreate(BaseModel):
|
||||
)
|
||||
media_references: list[GenerationAIReference] | None = Field(
|
||||
None,
|
||||
description="参考素材列表。可以传图片/视频/音频参考素材;为空表示不使用参考素材",
|
||||
description=(
|
||||
"参考素材列表。支持 image/video/audio。"
|
||||
"source=upload_resource 时表示来自历史上传素材;source=private_portrait_asset 时表示来自私域真人/虚拟素材。"
|
||||
"视频和音频素材必须携带 duration,前端和后端均按 AI 创作原规则校验数量、单段 2~15 秒、总时长不超过 15 秒。"
|
||||
),
|
||||
)
|
||||
idempotency_key: str | None = Field(
|
||||
None,
|
||||
@@ -163,12 +178,12 @@ class GenerationAITaskCreate(BaseModel):
|
||||
# image params
|
||||
image_size: str | None = Field(
|
||||
None,
|
||||
description="图片分辨率档位,例如:1K、2K。仅图片生成或视频首帧参数需要使用;为空则使用引擎默认值",
|
||||
description="图片分辨率档位枚举示例:1K、2K、4K。实际可选值以 /generation-ai/engines 返回的 supported_sizes 为准。",
|
||||
examples=["2K"],
|
||||
)
|
||||
image_proportion: str | None = Field(
|
||||
None,
|
||||
description="图片比例,例如:1:1、16:9、9:16。仅图片生成或视频首帧参数需要使用;为空则使用默认值",
|
||||
description="图片比例枚举示例:1:1、16:9、9:16、4:3、3:4、21:9。实际可选值以 /generation-ai/engines 返回为准。",
|
||||
examples=["1:1"],
|
||||
)
|
||||
image_px: str | None = Field(
|
||||
@@ -180,17 +195,17 @@ class GenerationAITaskCreate(BaseModel):
|
||||
# video params
|
||||
duration: int | None = Field(
|
||||
None,
|
||||
description="视频时长,单位秒。仅视频生成使用;为空则使用默认时长",
|
||||
description="视频生成时长,单位秒。枚举范围通常为 4~15 秒,实际可选值以视频引擎 supported_durations 为准。",
|
||||
examples=[4],
|
||||
)
|
||||
aspect_ratio: str | None = Field(
|
||||
None,
|
||||
description="视频比例,例如:16:9、9:16、1:1。仅视频生成使用;为空则使用默认比例",
|
||||
description="视频比例枚举示例:16:9、4:3、1:1、3:4、9:16、21:9。实际可选值以视频引擎 supported_ratios 为准。",
|
||||
examples=["16:9"],
|
||||
)
|
||||
resolution: str | None = Field(
|
||||
None,
|
||||
description="视频分辨率,例如:480p、720p、1080p。仅视频生成使用;为空则使用默认分辨率",
|
||||
description="视频分辨率枚举示例:480p、720p、1080p。实际可选值以视频引擎 supported_resolutions 为准。",
|
||||
examples=["480p"],
|
||||
)
|
||||
|
||||
@@ -380,8 +395,9 @@ class GenerationAITaskOut(BaseModel):
|
||||
history_source: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"素材云历史来源:chat_task=AI创作,generation_record=项目生成,"
|
||||
"hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻"
|
||||
"素材云生成历史来源:chat_task=AI创作,generation_record=项目生成,"
|
||||
"hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻。"
|
||||
"注意:上传素材历史不在本接口返回,请使用 /upload-resources/history。"
|
||||
),
|
||||
)
|
||||
history_source_label: str | None = Field(None, description="素材云历史来源中文名称")
|
||||
@@ -705,8 +721,9 @@ class GenerationAIRecordHistoryItemOut(BaseModel):
|
||||
history_source: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"素材云历史来源:chat_task=AI创作,generation_record=项目生成,"
|
||||
"hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻"
|
||||
"素材云生成历史来源:chat_task=AI创作,generation_record=项目生成,"
|
||||
"hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻。"
|
||||
"注意:上传素材历史不在本接口返回,请使用 /upload-resources/history。"
|
||||
),
|
||||
)
|
||||
history_source_label: str | None = Field(None, description="素材云历史来源中文名称")
|
||||
|
||||
@@ -43,6 +43,9 @@ HOT_OPENING_STEP_IO_EXAMPLES: dict[str, dict[str, Any]] = {
|
||||
"payload": {
|
||||
"material_video_url": "https://example.com/source.mp4",
|
||||
"material_image_url": "https://example.com/product.png",
|
||||
"material_video_resource_id": "0019...",
|
||||
"material_image_resource_id": "0019...",
|
||||
"material_video_duration_seconds": 8.2,
|
||||
"source_project_name": "参考素材项目名称",
|
||||
"target_project_name": "新项目名称",
|
||||
"core_content_point": "50字以内核心内容点",
|
||||
@@ -183,6 +186,9 @@ class HotOpeningTaskCreate(BaseModel):
|
||||
"example": {
|
||||
"material_video_url": "https://example.com/source.mp4",
|
||||
"material_image_url": "https://example.com/product.png",
|
||||
"material_video_resource_id": "0019...",
|
||||
"material_image_resource_id": "0019...",
|
||||
"material_video_duration_seconds": 8.2,
|
||||
"source_project_name": "参考素材项目名称",
|
||||
"target_project_name": "新项目名称",
|
||||
"core_content_point": "突出产品能帮助用户认识附近新朋友",
|
||||
@@ -193,6 +199,10 @@ class HotOpeningTaskCreate(BaseModel):
|
||||
|
||||
material_video_url: str = Field(..., min_length=1, description="素材视频链接,参考素材,1份。由项目已有上传接口返回,本接口不负责上传,不做后端素材校验")
|
||||
material_image_url: str = Field(..., min_length=1, description="素材图片链接,新产品图片,1份。由项目已有上传接口返回,本接口不负责上传,不做后端素材校验")
|
||||
material_video_resource_id: str | None = Field(None, max_length=32, description="可选,模块上传接口返回的视频 UploadResource.id;创建项目后用于绑定资源")
|
||||
material_image_resource_id: str | None = Field(None, max_length=32, description="可选,模块上传接口返回的图片 UploadResource.id;创建项目后用于绑定资源")
|
||||
material_video_duration_seconds: float | None = Field(None, gt=0, description="可选,前端识别的视频秒数,用于上传资源元数据兜底")
|
||||
material_image_duration_seconds: float | None = Field(None, gt=0, description="兼容字段,图片一般为空")
|
||||
source_project_name: str = Field(..., min_length=1, max_length=20, description="视频素材内容项目名称")
|
||||
target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称")
|
||||
core_content_point: str = Field(..., min_length=1, max_length=50, description="生成的项目核心内容点,最多50字")
|
||||
@@ -224,11 +234,14 @@ class HotOpeningMaterialUpdateRequest(BaseModel):
|
||||
|
||||
material_video_url: str | None = Field(None, min_length=1, description="素材视频链接,未传则沿用旧值")
|
||||
material_image_url: str | None = Field(None, min_length=1, description="素材图片链接,未传则沿用旧值")
|
||||
material_video_resource_id: str | None = Field(None, max_length=32, description="可选,模块上传接口返回的视频 UploadResource.id")
|
||||
material_image_resource_id: str | None = Field(None, max_length=32, description="可选,模块上传接口返回的图片 UploadResource.id")
|
||||
material_video_duration_seconds: float | None = Field(None, gt=0, description="可选,前端识别的视频秒数")
|
||||
source_project_name: str | None = Field(None, min_length=1, max_length=20, description="视频素材内容项目名称,未传则沿用旧值")
|
||||
target_project_name: str | None = Field(None, min_length=1, max_length=20, description="生成项目名称,未传则沿用旧值")
|
||||
core_content_point: str | None = Field(None, min_length=1, max_length=50, description="生成项目核心内容点,最多50字,未传则沿用旧值")
|
||||
|
||||
@field_validator("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point", mode="before")
|
||||
@field_validator("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point", "material_video_resource_id", "material_image_resource_id", mode="before")
|
||||
@classmethod
|
||||
def _strip_optional(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
@@ -240,7 +253,7 @@ class HotOpeningMaterialUpdateRequest(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_at_least_one(self) -> "HotOpeningMaterialUpdateRequest":
|
||||
if not any(getattr(self, field) is not None for field in ("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point")):
|
||||
if not any(getattr(self, field) is not None for field in ("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point", "material_video_resource_id", "material_image_resource_id")):
|
||||
raise ValueError("至少需要传入一个需要修改的字段")
|
||||
return self
|
||||
|
||||
@@ -486,6 +499,9 @@ class HotOpeningDeleteOut(BaseModel):
|
||||
message: str = Field(..., description="删除结果提示")
|
||||
project_id: str = Field(..., description="被软删除的总任务项目ID")
|
||||
deleted: bool = Field(..., description="是否已软删除")
|
||||
released_size_bytes: int = Field(0, description="本次释放的用户容量占用字节数;UploadResource 真实文件在事务提交后清理")
|
||||
upload_resource_released: int = Field(0, description="本次标记删除的上传资源数量")
|
||||
pending_delete_resource_ids: list[str] = Field(default_factory=list, exclude=True, description="内部字段:commit 后待删除真实文件的 UploadResource ID")
|
||||
|
||||
|
||||
class HotOpeningSpecOut(BaseModel):
|
||||
|
||||
@@ -6,10 +6,13 @@ 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
|
||||
|
||||
@@ -17,141 +20,246 @@ 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: (
|
||||
"私域虚拟人像素材库",
|
||||
"虚拟素材库。创建项目时直接同步火山 CreateAssetGroup,GroupType=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
|
||||
label: str
|
||||
description: str | None = None
|
||||
value: str = Field(..., description="枚举值,接口入参和出参均使用该值。")
|
||||
label: str = Field(..., description="枚举中文名称,用于前端展示。")
|
||||
description: str | None = Field(None, description="枚举说明,用于 OpenAPI 文档、前端提示或排查。")
|
||||
|
||||
|
||||
class PrivatePortraitEnumMetaOut(BaseModel):
|
||||
library_types: list[PrivatePortraitEnumItem]
|
||||
asset_types: list[PrivatePortraitEnumItem]
|
||||
project_statuses: list[PrivatePortraitEnumItem]
|
||||
asset_statuses: list[PrivatePortraitEnumItem]
|
||||
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: 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。")
|
||||
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 表示启用并限制真人/虚拟、图片/视频素材总量。")
|
||||
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="仅真人认证使用:认证完成后的手机端提示页地址。")
|
||||
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="虚拟人像素材项目组名称。创建后会同步创建火山 Asset Group。")
|
||||
name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
description="虚拟人像素材项目组名称。创建后会同步火山 CreateAssetGroup,GroupType=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="项目状态。普通前端不建议手动变更,仅管理/排查使用。")
|
||||
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
|
||||
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
|
||||
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]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
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="认证完成后前端要跳转的页面。为空时使用后端默认回调页。")
|
||||
callback_redirect_url: str | None = Field(
|
||||
None,
|
||||
description="真人认证完成后手机端要跳转的页面。为空时使用后端默认回调页;PC 端应继续轮询 validate-session。",
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
validate_session: PrivatePortraitValidateSessionOut
|
||||
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
|
||||
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
|
||||
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 PrivatePortraitAssetCreate(BaseModel):
|
||||
url: str = Field(..., min_length=1, description="已上传到本系统且可公网访问的素材 URL。支持图片/视频,后端会转换公网地址后调用火山 CreateAsset。")
|
||||
asset_type: str = Field(default=PrivatePortraitAssetType.IMAGE.value, description="素材类型。当前业务仅开放 Image / Video,Audio 暂不开放。")
|
||||
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 类型。")
|
||||
url: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="已上传到本系统且可公网访问的素材 URL。支持图片/视频,后端会转换为公网地址后调用火山 CreateAsset。",
|
||||
)
|
||||
asset_type: str = Field(
|
||||
default=PrivatePortraitAssetType.IMAGE.value,
|
||||
description="素材类型枚举:Image=图片,Video=视频,Audio=音频。当前业务仅开放 Image / Video,Audio 会被拒绝。",
|
||||
)
|
||||
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
|
||||
@@ -177,100 +285,101 @@ class PrivatePortraitAssetCreate(BaseModel):
|
||||
|
||||
|
||||
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
|
||||
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]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
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
|
||||
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
|
||||
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]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
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 = True
|
||||
remote_delete_status: str
|
||||
success: bool = Field(True, description="本地删除是否成功。接口返回时表示本地软删已提交成功。")
|
||||
remote_delete_status: str = Field(..., description="远端删除状态。通常返回 pending,表示远端删除任务已投递或等待补偿。")
|
||||
|
||||
|
||||
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
|
||||
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=[
|
||||
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],
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -227,6 +227,7 @@ class ShotReplicateMaterialUpdateRequest(BaseModel):
|
||||
)
|
||||
|
||||
material_image_url: str | None = Field(None, min_length=1, description="素材图片链接,未传则沿用旧值。用于新产品/目标素材图片,来自已有上传接口")
|
||||
material_image_resource_id: str | None = Field(None, max_length=32, description="可选,拆镜模块上传接口返回的图片 UploadResource.id")
|
||||
source_project_name: str | None = Field(None, min_length=1, max_length=20, description="视频素材内容项目名称,未传则沿用旧值")
|
||||
target_project_name: str | None = Field(None, min_length=1, max_length=20, description="生成项目名称,未传则沿用旧值")
|
||||
core_content_point: str | None = Field(None, min_length=1, max_length=50, description="生成项目核心内容点,最多50字,未传则沿用旧值")
|
||||
@@ -243,7 +244,7 @@ class ShotReplicateMaterialUpdateRequest(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_at_least_one(self) -> "ShotReplicateMaterialUpdateRequest":
|
||||
if not any(getattr(self, field) is not None for field in ("material_image_url", "source_project_name", "target_project_name", "core_content_point")):
|
||||
if not any(getattr(self, field) is not None for field in ("material_image_url", "source_project_name", "target_project_name", "core_content_point", "material_image_resource_id")):
|
||||
raise ValueError("至少需要传入一个需要修改的字段")
|
||||
return self
|
||||
|
||||
@@ -482,9 +483,11 @@ class ShotReplicateActionOut(BaseModel):
|
||||
|
||||
class ShotReplicateDeleteOut(BaseModel):
|
||||
message: str = Field(..., description="删除结果提示")
|
||||
project_id: str = Field(..., description="被软删除的总任务项目ID")
|
||||
project_id: str = Field(..., description="被软删除的内部拆镜复刻 ModuleGenerationProject ID")
|
||||
deleted: bool = Field(..., description="是否已软删除")
|
||||
released_size_bytes: int = Field(0, description="本次软删释放的用户容量占用字节数;不删除物理文件")
|
||||
released_size_bytes: int = Field(0, description="本次软删释放的用户容量占用字节数;UploadResource 真实文件在事务提交后清理")
|
||||
upload_resource_released: int = Field(0, description="本次标记删除的上传资源数量")
|
||||
pending_delete_resource_ids: list[str] = Field(default_factory=list, exclude=True, description="内部字段:commit 后待删除真实文件的 UploadResource ID")
|
||||
|
||||
|
||||
class ShotSegmentDeleteOut(BaseModel):
|
||||
@@ -493,7 +496,20 @@ class ShotSegmentDeleteOut(BaseModel):
|
||||
task_set_id: str = Field(..., description="所属拆镜总任务集ID")
|
||||
deleted: bool = Field(..., description="是否已软删除")
|
||||
deleted_module_project_id: str | None = Field(None, description="联动软删除的拆镜复刻项目ID;没有关联项目时为空")
|
||||
released_size_bytes: int = Field(0, description="本次释放的用户容量占用字节数;只释放数据账本,不删除物理文件")
|
||||
released_size_bytes: int = Field(0, description="本次释放的用户容量占用字节数;UploadResource 真实文件在事务提交后清理")
|
||||
upload_resource_released: int = Field(0, description="本次标记删除的上传资源数量")
|
||||
pending_delete_resource_ids: list[str] = Field(default_factory=list, exclude=True, description="内部字段:commit 后待删除真实文件的 UploadResource ID")
|
||||
|
||||
|
||||
class ShotTaskSetDeleteOut(BaseModel):
|
||||
message: str = Field(..., description="删除结果提示")
|
||||
task_set_id: str = Field(..., description="被软删除的拆镜任务集ID")
|
||||
deleted: bool = Field(..., description="是否已软删除")
|
||||
deleted_segment_count: int = Field(0, description="本次软删除的片段数量")
|
||||
deleted_module_project_count: int = Field(0, description="本次联动软删除的内部复刻项目数量")
|
||||
released_size_bytes: int = Field(0, description="本次释放的用户容量占用字节数;UploadResource 真实文件在事务提交后清理")
|
||||
upload_resource_released: int = Field(0, description="本次标记删除的上传资源数量")
|
||||
pending_delete_resource_ids: list[str] = Field(default_factory=list, exclude=True, description="内部字段:commit 后待删除真实文件的 UploadResource ID")
|
||||
|
||||
|
||||
|
||||
@@ -568,6 +584,7 @@ class ShotTaskSetCreate(BaseModel):
|
||||
"example": {
|
||||
"video_url": "/uploads/2026/06/11/demo.mp4",
|
||||
"video_duration_seconds": 31.42,
|
||||
"video_resource_id": "0019...",
|
||||
"title": "游戏视频拆镜",
|
||||
"idempotency_key": "frontend-shot-task-001",
|
||||
}
|
||||
@@ -576,6 +593,7 @@ class ShotTaskSetCreate(BaseModel):
|
||||
|
||||
video_url: str = Field(..., min_length=1, description="已有上传接口返回的视频链接。必须能反解到 storage/uploads 下文件")
|
||||
video_duration_seconds: float = Field(..., gt=0, description="前端获取的视频时长秒数,允许浮点;后端会用 ffprobe 校验并以后端真实时长为准")
|
||||
video_resource_id: str | None = Field(None, max_length=32, description="可选,拆镜模块上传接口返回的视频 UploadResource.id;创建任务集后用于绑定资源")
|
||||
title: str | None = Field(None, max_length=160, description="拆镜总任务标题")
|
||||
idempotency_key: str | None = Field(None, max_length=64, description="创建总任务幂等键")
|
||||
|
||||
@@ -742,6 +760,7 @@ class ShotSegmentReplicationCreateRequest(BaseModel):
|
||||
"target_project_name": "新产品推广视频",
|
||||
"core_content_point": "突出产品附近交友和快速脱单",
|
||||
"material_image_url": "/uploads/2026/06/11/product.png",
|
||||
"material_image_resource_id": "0019...",
|
||||
"idempotency_key": "optional-key",
|
||||
}
|
||||
}
|
||||
@@ -750,9 +769,10 @@ class ShotSegmentReplicationCreateRequest(BaseModel):
|
||||
target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称,最多20字;会写入 module_generation_projects.title 和第1步 material_input")
|
||||
core_content_point: str = Field(..., min_length=1, max_length=50, description="生成项目核心内容点,最多50字;用于后续图片 AI 提词和视频 AI 提词")
|
||||
material_image_url: str = Field(..., min_length=1, description="新产品/目标素材图片链接,来自已有上传接口;作为图片生成参考素材")
|
||||
material_image_resource_id: str | None = Field(None, max_length=32, description="可选,拆镜模块上传接口返回的图片 UploadResource.id;创建复刻项目后用于绑定资源")
|
||||
idempotency_key: str | None = Field(None, max_length=64, description="创建 ModuleGenerationProject 幂等键;为空时后端可按业务生成或不使用")
|
||||
|
||||
@field_validator("target_project_name", "core_content_point", "material_image_url", "idempotency_key", mode="before")
|
||||
@field_validator("target_project_name", "core_content_point", "material_image_url", "material_image_resource_id", "idempotency_key", mode="before")
|
||||
@classmethod
|
||||
def _strip_text(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.enums.upload_resource import (
|
||||
UploadResourceBindStatusEnum,
|
||||
UploadResourceDeletePolicyEnum,
|
||||
UploadResourceModuleEnum,
|
||||
UploadResourceTypeEnum,
|
||||
)
|
||||
from app.schemas.common import NaiveDatetimeOptional
|
||||
|
||||
|
||||
class UploadResourceMediaReferenceOut(BaseModel):
|
||||
"""AI 创作可直接复用的上传素材参考结构。"""
|
||||
|
||||
name: str | None = Field(None, description="参考素材名称,前端展示用")
|
||||
type: Literal["image", "video", "audio"] = Field(..., description="参考素材类型:image=图片,video=视频,audio=音频")
|
||||
url: str = Field(..., description="参考素材 URL,创建 AI 创作任务时传入 media_references[].url")
|
||||
label: str | None = Field("", description="前端展示标签,默认空,由 AI 创作页重新计算")
|
||||
duration: float | None = Field(None, ge=0, description="素材时长秒数,type=video/audio 时必须有有效值")
|
||||
source: Literal["upload_resource"] = Field("upload_resource", description="参考素材来源固定为 upload_resource")
|
||||
upload_resource_id: str = Field(..., description="UploadResource 本地资源账本 ID")
|
||||
|
||||
|
||||
class UploadResourceHistoryItemOut(BaseModel):
|
||||
"""上传资源历史素材项。"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"id": "0019e0a448a23114888",
|
||||
"source_type": "upload_resource",
|
||||
"history_source": "upload_resource",
|
||||
"history_source_label": "历史上传素材",
|
||||
"module": "common",
|
||||
"module_label": "普通上传",
|
||||
"resource_type": "video",
|
||||
"resource_type_label": "视频",
|
||||
"resource_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4",
|
||||
"display_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4",
|
||||
"preview_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4",
|
||||
"video_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4",
|
||||
"duration_seconds": 5.2,
|
||||
"bind_status": "pending",
|
||||
"delete_policy": "user_deletable",
|
||||
"deletable": True,
|
||||
"media_reference": {
|
||||
"name": "video_ref_xxx.mp4",
|
||||
"type": "video",
|
||||
"url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4",
|
||||
"duration": 5.2,
|
||||
"source": "upload_resource",
|
||||
"upload_resource_id": "0019e0a448a23114888",
|
||||
},
|
||||
"created_at": "2026-07-08T13:20:00",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
id: str = Field(..., description="UploadResource.id")
|
||||
source_type: Literal["upload_resource"] = Field("upload_resource", description="前端判定来源固定值")
|
||||
history_source: Literal["upload_resource"] = Field("upload_resource", description="素材云历史来源固定为 upload_resource")
|
||||
history_source_label: str = Field("历史上传素材", description="素材云历史来源中文名称")
|
||||
|
||||
module: str = Field(..., description="上传资源所属模块:common=普通上传,hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻")
|
||||
module_label: str = Field(..., description="上传资源所属模块中文名称")
|
||||
resource_type: Literal["image", "video", "audio"] = Field(..., description="资源类型:image=图片,video=视频,audio=音频")
|
||||
resource_type_label: str = Field(..., description="资源类型中文名称")
|
||||
|
||||
resource_url: str = Field(..., description="原始资源 URL,可传给 AI 创作 media_references[].url")
|
||||
display_url: str = Field(..., description="前端展示 URL。当前普通上传资源通常与 resource_url 一致")
|
||||
preview_url: str = Field(..., description="前端预览 URL。当前普通上传资源通常与 display_url 一致")
|
||||
image_url: str | None = Field(None, description="图片资源 URL;resource_type=image 时有值")
|
||||
video_url: str | None = Field(None, description="视频资源 URL;resource_type=video 时有值")
|
||||
audio_url: str | None = Field(None, description="音频资源 URL;resource_type=audio 时有值")
|
||||
|
||||
file_name: str | None = Field(None, description="文件名")
|
||||
file_ext: str | None = Field(None, description="文件扩展名")
|
||||
mime_type: str | None = Field(None, description="MIME 类型")
|
||||
file_size_bytes: int = Field(0, description="文件大小,单位字节")
|
||||
duration_seconds: float | None = Field(None, description="视频/音频时长秒数")
|
||||
width: int | None = Field(None, description="图片或视频宽度,未识别时为空")
|
||||
height: int | None = Field(None, description="图片或视频高度,未识别时为空")
|
||||
|
||||
bind_status: str = Field(..., description="绑定状态。历史素材接口只返回 pending=未绑定")
|
||||
delete_policy: str = Field(..., description="删除策略。历史素材接口只返回 user_deletable=用户可删除")
|
||||
deletable: bool = Field(True, description="是否允许当前用户删除。历史素材接口只返回 true")
|
||||
media_reference: UploadResourceMediaReferenceOut = Field(..., description="AI 创作可直接复用的参考素材对象")
|
||||
|
||||
created_at: NaiveDatetimeOptional = Field(None, description="上传时间")
|
||||
updated_at: NaiveDatetimeOptional = Field(None, description="更新时间")
|
||||
|
||||
|
||||
class UploadResourceHistoryDayGroupOut(BaseModel):
|
||||
"""上传资源历史按天分组响应项。"""
|
||||
|
||||
generated_date: str = Field(..., description="上传日期,格式 YYYY-MM-DD。字段名沿用生成历史,方便前端复用分组逻辑")
|
||||
total: int = Field(..., description="当前日期下可展示/可复用/可删除的上传素材总数")
|
||||
page: int = Field(1, description="当前日期下 items 的页码,分组接口固定为 1")
|
||||
items: list[UploadResourceHistoryItemOut] = Field(default_factory=list, description="当前日期倒序前若干条上传素材")
|
||||
|
||||
|
||||
class UploadResourceHistoryGroupedOut(BaseModel):
|
||||
"""上传资源历史日期分组列表响应体。"""
|
||||
|
||||
total_days: int = Field(..., description="符合条件的上传素材日期分组总数")
|
||||
page: int = Field(..., description="日期分组页码,从 1 开始")
|
||||
page_size: int = Field(..., description="日期分组每页数量,最大 10")
|
||||
groups: list[UploadResourceHistoryDayGroupOut] = Field(default_factory=list, description="上传日期倒序分组列表")
|
||||
|
||||
|
||||
class UploadResourceHistoryDayItemsOut(BaseModel):
|
||||
"""指定日期下上传资源历史分页响应体。"""
|
||||
|
||||
generated_date: str = Field(..., description="当前查询上传日期,格式 YYYY-MM-DD")
|
||||
total: int = Field(..., description="当前日期下符合条件的上传素材总数")
|
||||
page: int = Field(..., description="当前日期下分页页码,从 1 开始")
|
||||
page_size: int = Field(..., description="当前日期下每页返回数量,最大 100")
|
||||
items: list[UploadResourceHistoryItemOut] = Field(default_factory=list, description="当前日期下上传素材列表,按上传时间倒序排列")
|
||||
|
||||
|
||||
class UploadResourceHistoryBatchDeleteRequest(BaseModel):
|
||||
"""批量删除上传历史素材请求体。"""
|
||||
|
||||
resource_ids: list[str] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=30,
|
||||
description="UploadResource.id 列表。一次最多 30 条,不允许重复",
|
||||
examples=[["0019e0a448a23114888", "0019e0a448a23114889"]],
|
||||
)
|
||||
|
||||
@field_validator("resource_ids")
|
||||
@classmethod
|
||||
def validate_resource_ids(cls, values: list[str]) -> list[str]:
|
||||
cleaned = [str(value).strip() for value in values if str(value or "").strip()]
|
||||
if not cleaned:
|
||||
raise ValueError("resource_ids 不能为空")
|
||||
if len(cleaned) > 30:
|
||||
raise ValueError("单次最多删除 30 条上传素材")
|
||||
if len(cleaned) != len(set(cleaned)):
|
||||
raise ValueError("resource_ids 不允许重复")
|
||||
return cleaned
|
||||
|
||||
|
||||
class UploadResourceCleanupOut(BaseModel):
|
||||
"""真实文件清理结果。"""
|
||||
|
||||
matched: int = Field(0, description="匹配到待清理资源数量")
|
||||
deleted: int = Field(0, description="真实文件删除成功数量")
|
||||
missing: int = Field(0, description="真实文件已不存在数量")
|
||||
failed: int = Field(0, description="真实文件删除失败数量")
|
||||
legacy_deleted: int = Field(0, description="兼容历史路径删除成功数量")
|
||||
legacy_missing: int = Field(0, description="兼容历史路径不存在数量")
|
||||
legacy_failed: int = Field(0, description="兼容历史路径删除失败数量")
|
||||
|
||||
|
||||
class UploadResourceHistoryBatchDeleteOut(BaseModel):
|
||||
"""批量删除上传历史素材响应体。"""
|
||||
|
||||
message: str = Field("删除成功", description="操作结果消息")
|
||||
requested_count: int = Field(..., description="请求删除数量")
|
||||
deleted_count: int = Field(..., description="成功软删数量")
|
||||
requested_ids: list[str] = Field(default_factory=list, description="请求删除的 UploadResource.id 列表")
|
||||
deleted_ids: list[str] = Field(default_factory=list, description="已软删的 UploadResource.id 列表")
|
||||
released_size_bytes: int = Field(0, description="已释放的上传容量字节数")
|
||||
cleanup: UploadResourceCleanupOut = Field(default_factory=UploadResourceCleanupOut, description="commit 成功后的真实文件清理结果")
|
||||
|
||||
|
||||
UPLOAD_RESOURCE_HISTORY_ALLOWED_RESOURCE_TYPES = {
|
||||
UploadResourceTypeEnum.IMAGE.value,
|
||||
UploadResourceTypeEnum.VIDEO.value,
|
||||
UploadResourceTypeEnum.AUDIO.value,
|
||||
}
|
||||
|
||||
UPLOAD_RESOURCE_HISTORY_ALLOWED_BIND_STATUS = UploadResourceBindStatusEnum.PENDING.value
|
||||
UPLOAD_RESOURCE_HISTORY_ALLOWED_DELETE_POLICY = UploadResourceDeletePolicyEnum.USER_DELETABLE.value
|
||||
UPLOAD_RESOURCE_HISTORY_ALLOWED_MODULES = {
|
||||
UploadResourceModuleEnum.COMMON.value,
|
||||
UploadResourceModuleEnum.HOT_OPENING_REPLICATE.value,
|
||||
UploadResourceModuleEnum.SHOT_REPLICATE.value,
|
||||
}
|
||||
Reference in New Issue
Block a user