1
This commit is contained in:
@@ -50,26 +50,65 @@ from app.services.private_portrait.virtual.service import create_virtual_asset,
|
||||
|
||||
router = APIRouter(tags=["私域虚拟人像素材库"])
|
||||
|
||||
_VIRTUAL_API_DESCRIPTION = (
|
||||
"私域虚拟素材库 API。真人和虚拟素材共用用户素材额度;user.private_portrait_asset_limit=0 表示关闭,"
|
||||
">0 表示启用并限制总素材数量。创建虚拟项目会同步调用火山 CreateAssetGroup,GroupType=AIGC,"
|
||||
"remote_project_name 默认使用 default。当前上传只开放 Image/Video,Audio 暂不开放。"
|
||||
)
|
||||
|
||||
|
||||
def _log_task_dispatch_failed(*, task_name: str, user_id: str | None = None, project_id: str | None = None, asset_id: str | None = None, exc: BaseException) -> None:
|
||||
log_operation_error(domain=DOMAIN, event_type=PrivatePortraitEventType.TASK_DISPATCH_FAILED.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project_id, asset_id=asset_id, exc=exc, detail={"task_name": task_name})
|
||||
log_operation_error(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.TASK_DISPATCH_FAILED.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
asset_id=asset_id,
|
||||
exc=exc,
|
||||
detail={"task_name": task_name},
|
||||
)
|
||||
|
||||
|
||||
def _log_task_dispatch_success(*, task_name: str, user_id: str | None = None, project_id: str | None = None, asset_id: str | None = None) -> None:
|
||||
log_operation_event(domain=DOMAIN, event_type=PrivatePortraitEventType.TASK_DISPATCH_SUCCESS.value, event_status=PrivatePortraitEventStatus.SUCCESS.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project_id, asset_id=asset_id, detail={"task_name": task_name})
|
||||
log_operation_event(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.TASK_DISPATCH_SUCCESS.value,
|
||||
event_status=PrivatePortraitEventStatus.SUCCESS.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
asset_id=asset_id,
|
||||
detail={"task_name": task_name},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual/config", response_model=PrivatePortraitConfigOut, summary="获取虚拟人像素材库额度配置", description="额度与真人认证素材库共用;图片/视频共用;Audio 暂不开放。")
|
||||
@router.get(
|
||||
"/private-portrait/virtual/config",
|
||||
response_model=PrivatePortraitConfigOut,
|
||||
summary="获取虚拟素材库额度配置",
|
||||
description=_VIRTUAL_API_DESCRIPTION + "返回虚拟素材库可用额度,实际与真人素材库共用。",
|
||||
)
|
||||
async def get_my_virtual_private_portrait_config(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
return await get_user_private_portrait_config(db, user_id=current_user.id)
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-meta/enums", response_model=PrivatePortraitEnumMetaOut, summary="获取虚拟人像素材库枚举说明")
|
||||
@router.get(
|
||||
"/private-portrait/virtual-meta/enums",
|
||||
response_model=PrivatePortraitEnumMetaOut,
|
||||
summary="获取虚拟素材库枚举说明",
|
||||
description="返回前端展示所需枚举:library_type、asset_type、project status、asset status、remote_delete_status 等。Audio 暂不开放上传。",
|
||||
)
|
||||
async def get_virtual_private_portrait_enum_meta():
|
||||
return build_private_portrait_enum_meta()
|
||||
|
||||
|
||||
@router.post("/private-portrait/virtual-projects", response_model=PrivatePortraitProjectOut, summary="创建虚拟人像项目组", description="创建本地虚拟人像项目,并同步调用火山 CreateAssetGroup,GroupType=AIGC。ProjectName 必须与后续生成 API Key 所属项目一致,默认使用 default。")
|
||||
@router.post(
|
||||
"/private-portrait/virtual-projects",
|
||||
response_model=PrivatePortraitProjectOut,
|
||||
summary="创建虚拟素材项目组",
|
||||
description=_VIRTUAL_API_DESCRIPTION + "创建成功后返回本地项目详情,后续上传虚拟素材必须归属到该 project_id。",
|
||||
)
|
||||
async def create_private_portrait_virtual_project(payload: PrivatePortraitVirtualProjectCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
project = await create_virtual_project(db, user_id=current_user.id, payload=payload)
|
||||
out = project_to_out(project)
|
||||
@@ -77,8 +116,20 @@ async def create_private_portrait_virtual_project(payload: PrivatePortraitVirtua
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-projects", response_model=PrivatePortraitProjectListOut, summary="查询当前用户虚拟人像项目列表")
|
||||
async def list_private_portrait_virtual_projects(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), keyword: str | None = Query(None), status: str | None = Query(None), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
@router.get(
|
||||
"/private-portrait/virtual-projects",
|
||||
response_model=PrivatePortraitProjectListOut,
|
||||
summary="查询当前用户虚拟素材项目列表",
|
||||
description="只返回 library_type=aigc_virtual 的项目。默认查询 active 项目,可通过 status 覆盖。",
|
||||
)
|
||||
async def list_private_portrait_virtual_projects(
|
||||
page: int = Query(1, ge=1, description="页码,从 1 开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,最大 100"),
|
||||
keyword: str | None = Query(None, description="项目名称模糊搜索"),
|
||||
status: str | None = Query(None, description="项目状态筛选,不传默认 active"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query_status = status or PrivatePortraitProjectStatus.ACTIVE.value
|
||||
items, total = await list_projects(db, user_id=current_user.id, page=page, page_size=page_size, keyword=keyword, status=query_status, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value)
|
||||
await refresh_project_counters(db, [item.id for item in items])
|
||||
@@ -87,12 +138,22 @@ async def list_private_portrait_virtual_projects(page: int = Query(1, ge=1), pag
|
||||
return PrivatePortraitProjectListOut(items=[project_to_out(item) for item in items], total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-projects/{project_id}", response_model=PrivatePortraitProjectOut, summary="获取虚拟人像项目详情")
|
||||
@router.get(
|
||||
"/private-portrait/virtual-projects/{project_id}",
|
||||
response_model=PrivatePortraitProjectOut,
|
||||
summary="获取虚拟素材项目详情",
|
||||
description="获取当前用户虚拟素材项目详情,project_id 必须属于当前用户且 library_type=aigc_virtual。",
|
||||
)
|
||||
async def get_private_portrait_virtual_project(project_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
return project_to_out(await get_user_project(db, user_id=current_user.id, project_id=project_id, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value))
|
||||
|
||||
|
||||
@router.put("/private-portrait/virtual-projects/{project_id}", response_model=PrivatePortraitProjectOut, summary="更新虚拟人像项目")
|
||||
@router.put(
|
||||
"/private-portrait/virtual-projects/{project_id}",
|
||||
response_model=PrivatePortraitProjectOut,
|
||||
summary="更新虚拟素材项目",
|
||||
description="更新虚拟素材项目本地展示信息。不会重新创建远端 AssetGroup。",
|
||||
)
|
||||
async def update_private_portrait_virtual_project(project_id: str, payload: PrivatePortraitProjectUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
project = await update_virtual_project(db, user_id=current_user.id, project_id=project_id, payload=payload)
|
||||
out = project_to_out(project)
|
||||
@@ -100,7 +161,12 @@ async def update_private_portrait_virtual_project(project_id: str, payload: Priv
|
||||
return out
|
||||
|
||||
|
||||
@router.delete("/private-portrait/virtual-projects/{project_id}", response_model=PrivatePortraitDeleteOut, summary="删除虚拟人像项目")
|
||||
@router.delete(
|
||||
"/private-portrait/virtual-projects/{project_id}",
|
||||
response_model=PrivatePortraitDeleteOut,
|
||||
summary="删除虚拟素材项目",
|
||||
description="软删虚拟素材项目和本地素材记录。本地先 commit,commit 成功后投递 Celery 删除远端 AssetGroup/Asset;remote_delete_status=pending 表示远端删除处理中。",
|
||||
)
|
||||
async def delete_private_portrait_virtual_project(project_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
project = await soft_delete_project(db, user_id=current_user.id, project_id=project_id, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value)
|
||||
project_id_snapshot = project.id
|
||||
@@ -115,7 +181,16 @@ async def delete_private_portrait_virtual_project(project_id: str, current_user:
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
@router.post("/private-portrait/virtual-projects/{project_id}/assets", response_model=PrivatePortraitAssetOut, summary="上传虚拟人像素材", description="当前支持 Image / Video。Audio 暂不开放。CreateAsset 是异步接口,返回后需要轮询到 Active 才可用于生成。")
|
||||
@router.post(
|
||||
"/private-portrait/virtual-projects/{project_id}/assets",
|
||||
response_model=PrivatePortraitAssetOut,
|
||||
summary="上传虚拟素材",
|
||||
description=(
|
||||
"在虚拟素材项目下创建素材。当前仅开放 asset_type=Image/Video,Audio 暂不开放。"
|
||||
"Video 必须携带 video_duration,建议前端限制 2~15 秒。CreateAsset 是异步接口,返回后会投递轮询任务,"
|
||||
"只有 status=Active 的素材才会出现在 virtual-selectable-assets 并可用于 AI 创作。"
|
||||
),
|
||||
)
|
||||
async def create_private_portrait_virtual_asset(project_id: str, payload: PrivatePortraitAssetCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
asset = await create_virtual_asset(db, user_id=current_user.id, project_id=project_id, payload=payload)
|
||||
asset_id_snapshot = asset.id
|
||||
@@ -132,13 +207,32 @@ async def create_private_portrait_virtual_asset(project_id: str, payload: Privat
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-projects/{project_id}/assets", response_model=PrivatePortraitAssetListOut, summary="查询虚拟人像素材列表")
|
||||
async def list_private_portrait_virtual_assets(project_id: str, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), status: str | None = Query(None), keyword: str | None = Query(None), asset_type: str | None = Query(None), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
@router.get(
|
||||
"/private-portrait/virtual-projects/{project_id}/assets",
|
||||
response_model=PrivatePortraitAssetListOut,
|
||||
summary="查询虚拟素材列表",
|
||||
description="查询指定虚拟项目下的素材。asset_type 可传 Image 或 Video;status 可筛选素材状态。",
|
||||
)
|
||||
async def list_private_portrait_virtual_assets(
|
||||
project_id: str,
|
||||
page: int = Query(1, ge=1, description="页码,从 1 开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,最大 100"),
|
||||
status: str | None = Query(None, description="素材状态筛选,不传查全部"),
|
||||
keyword: str | None = Query(None, description="素材名称模糊搜索"),
|
||||
asset_type: str | None = Query(None, description="素材类型:Image=图片,Video=视频;Audio 暂不开放"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
assets, total, project_name_map = await list_assets(db, user_id=current_user.id, project_id=project_id, status=status, keyword=keyword, page=page, page_size=page_size, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value, asset_type=asset_type)
|
||||
return PrivatePortraitAssetListOut(items=[asset_to_out(asset, project_name=project_name_map.get(asset.project_id)) for asset in assets], total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-assets/{asset_id}", response_model=PrivatePortraitAssetOut, summary="获取虚拟人像素材详情")
|
||||
@router.get(
|
||||
"/private-portrait/virtual-assets/{asset_id}",
|
||||
response_model=PrivatePortraitAssetOut,
|
||||
summary="获取虚拟素材详情",
|
||||
description="获取当前用户虚拟素材详情,asset_id 必须属于当前用户且 library_type=aigc_virtual。",
|
||||
)
|
||||
async def get_private_portrait_virtual_asset(asset_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
asset = (await db.execute(select(PrivatePortraitAsset).where(PrivatePortraitAsset.id == asset_id, PrivatePortraitAsset.user_id == current_user.id, PrivatePortraitAsset.library_type == PrivatePortraitLibraryType.AIGC_VIRTUAL.value).limit(1))).scalar_one_or_none()
|
||||
if not asset:
|
||||
@@ -147,7 +241,12 @@ async def get_private_portrait_virtual_asset(asset_id: str, current_user: User =
|
||||
return asset_to_out(asset, project_name=project.name if project else None)
|
||||
|
||||
|
||||
@router.post("/private-portrait/virtual-assets/{asset_id}/sync", response_model=PrivatePortraitAssetOut, summary="同步虚拟人像素材状态")
|
||||
@router.post(
|
||||
"/private-portrait/virtual-assets/{asset_id}/sync",
|
||||
response_model=PrivatePortraitAssetOut,
|
||||
summary="同步虚拟素材状态",
|
||||
description="主动向火山查询并同步虚拟素材状态。一般由轮询任务自动执行;前端排查或手动刷新时可调用。",
|
||||
)
|
||||
async def sync_private_portrait_virtual_asset(asset_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
asset = await sync_asset_status(db, user_id=current_user.id, asset_id=asset_id)
|
||||
if asset.library_type != PrivatePortraitLibraryType.AIGC_VIRTUAL.value:
|
||||
@@ -157,7 +256,12 @@ async def sync_private_portrait_virtual_asset(asset_id: str, current_user: User
|
||||
return out
|
||||
|
||||
|
||||
@router.delete("/private-portrait/virtual-assets/{asset_id}", response_model=PrivatePortraitDeleteOut, summary="删除虚拟人像素材")
|
||||
@router.delete(
|
||||
"/private-portrait/virtual-assets/{asset_id}",
|
||||
response_model=PrivatePortraitDeleteOut,
|
||||
summary="删除虚拟素材",
|
||||
description="软删本地虚拟素材记录。本地先 commit,commit 成功后投递 Celery 删除远端 Asset;remote_delete_status=pending 表示远端删除处理中。",
|
||||
)
|
||||
async def delete_private_portrait_virtual_asset(asset_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
asset = await soft_delete_asset(db, user_id=current_user.id, asset_id=asset_id, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value)
|
||||
asset_id_snapshot = asset.id
|
||||
@@ -173,7 +277,20 @@ async def delete_private_portrait_virtual_asset(asset_id: str, current_user: Use
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
@router.get("/private-portrait/virtual-selectable-assets", response_model=PrivatePortraitSelectableAssetListOut, summary="查询可用于生成的虚拟人像素材")
|
||||
async def list_private_portrait_virtual_selectable_assets(page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), project_id: str | None = Query(None), keyword: str | None = Query(None), asset_type: str | None = Query(None, description="Image 或 Video,不传查全部。"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
@router.get(
|
||||
"/private-portrait/virtual-selectable-assets",
|
||||
response_model=PrivatePortraitSelectableAssetListOut,
|
||||
summary="查询可用于 AI 创作的虚拟素材",
|
||||
description="只返回当前用户虚拟素材库中 status=Active 且未删除的 Image/Video 素材。该接口给 AI 创作参考内容选择器使用。",
|
||||
)
|
||||
async def list_private_portrait_virtual_selectable_assets(
|
||||
page: int = Query(1, ge=1, description="页码,从 1 开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,最大 100"),
|
||||
project_id: str | None = Query(None, description="按虚拟项目 ID 筛选"),
|
||||
keyword: str | None = Query(None, description="素材名称模糊搜索"),
|
||||
asset_type: str | None = Query(None, description="素材类型:Image 或 Video,不传查全部"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
items, total = await list_selectable_assets(db, user_id=current_user.id, project_id=project_id, keyword=keyword, page=page, page_size=page_size, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value, asset_type=asset_type)
|
||||
return PrivatePortraitSelectableAssetListOut(items=items, total=total, page=page, page_size=page_size)
|
||||
|
||||
Reference in New Issue
Block a user