180 lines
13 KiB
Python
180 lines
13 KiB
Python
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.dependencies import get_current_user, get_db
|
||
from app.enums.private_portrait import (
|
||
PrivatePortraitEventSource,
|
||
PrivatePortraitEventStatus,
|
||
PrivatePortraitEventType,
|
||
PrivatePortraitLibraryType,
|
||
PrivatePortraitProjectStatus,
|
||
PrivatePortraitRemoteDeleteStatus,
|
||
)
|
||
from app.models.private_portrait import PrivatePortraitAsset, PrivatePortraitProject
|
||
from app.models.user import User
|
||
from app.schemas.private_portrait import (
|
||
PrivatePortraitAssetCreate,
|
||
PrivatePortraitAssetListOut,
|
||
PrivatePortraitAssetOut,
|
||
PrivatePortraitConfigOut,
|
||
PrivatePortraitDeleteOut,
|
||
PrivatePortraitEnumMetaOut,
|
||
PrivatePortraitProjectListOut,
|
||
PrivatePortraitProjectOut,
|
||
PrivatePortraitProjectUpdate,
|
||
PrivatePortraitSelectableAssetListOut,
|
||
PrivatePortraitVirtualProjectCreate,
|
||
build_private_portrait_enum_meta,
|
||
)
|
||
from app.services.operation_log_service import log_operation_error, log_operation_event
|
||
from app.services.private_portrait.asset_service import (
|
||
DOMAIN,
|
||
asset_to_out,
|
||
get_user_private_portrait_config,
|
||
list_assets,
|
||
list_selectable_assets,
|
||
soft_delete_asset,
|
||
sync_asset_status,
|
||
)
|
||
from app.services.private_portrait.project_service import (
|
||
get_user_project,
|
||
list_projects,
|
||
project_to_out,
|
||
refresh_project_counters,
|
||
soft_delete_project,
|
||
)
|
||
from app.services.private_portrait.virtual.service import create_virtual_asset, create_virtual_project, update_virtual_project
|
||
|
||
router = APIRouter(tags=["私域虚拟人像素材库"])
|
||
|
||
|
||
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})
|
||
|
||
|
||
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})
|
||
|
||
|
||
@router.get("/private-portrait/virtual/config", response_model=PrivatePortraitConfigOut, summary="获取虚拟人像素材库额度配置", description="额度与真人认证素材库共用;图片/视频共用;Audio 暂不开放。")
|
||
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="获取虚拟人像素材库枚举说明")
|
||
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。")
|
||
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)
|
||
await db.commit()
|
||
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)):
|
||
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])
|
||
await db.commit()
|
||
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)
|
||
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="获取虚拟人像项目详情")
|
||
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="更新虚拟人像项目")
|
||
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)
|
||
await db.commit()
|
||
return out
|
||
|
||
|
||
@router.delete("/private-portrait/virtual-projects/{project_id}", response_model=PrivatePortraitDeleteOut, summary="删除虚拟人像项目")
|
||
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
|
||
await db.commit()
|
||
try:
|
||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_project_remote
|
||
|
||
delete_private_portrait_project_remote.delay(project_id_snapshot)
|
||
_log_task_dispatch_success(task_name="private_portrait.delete_project_remote", user_id=current_user.id, project_id=project_id_snapshot)
|
||
except Exception as exc:
|
||
_log_task_dispatch_failed(task_name="private_portrait.delete_project_remote", user_id=current_user.id, project_id=project_id_snapshot, exc=exc)
|
||
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 才可用于生成。")
|
||
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
|
||
project_id_snapshot = asset.project_id
|
||
out = asset_to_out(asset)
|
||
await db.commit()
|
||
try:
|
||
from app.tasks.private_portrait_asset_tasks import poll_private_portrait_asset_status
|
||
|
||
poll_private_portrait_asset_status.delay(asset_id_snapshot)
|
||
_log_task_dispatch_success(task_name="private_portrait.poll_asset_status", user_id=current_user.id, project_id=project_id_snapshot, asset_id=asset_id_snapshot)
|
||
except Exception as exc:
|
||
_log_task_dispatch_failed(task_name="private_portrait.poll_asset_status", user_id=current_user.id, project_id=project_id_snapshot, asset_id=asset_id_snapshot, exc=exc)
|
||
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)):
|
||
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="获取虚拟人像素材详情")
|
||
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:
|
||
raise HTTPException(status_code=404, detail="虚拟人像素材不存在")
|
||
project = (await db.execute(select(PrivatePortraitProject).where(PrivatePortraitProject.id == asset.project_id).limit(1))).scalar_one_or_none()
|
||
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="同步虚拟人像素材状态")
|
||
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:
|
||
raise HTTPException(status_code=404, detail="虚拟人像素材不存在")
|
||
out = asset_to_out(asset)
|
||
await db.commit()
|
||
return out
|
||
|
||
|
||
@router.delete("/private-portrait/virtual-assets/{asset_id}", response_model=PrivatePortraitDeleteOut, summary="删除虚拟人像素材")
|
||
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
|
||
project_id_snapshot = asset.project_id
|
||
await db.commit()
|
||
try:
|
||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_asset_remote
|
||
|
||
delete_private_portrait_asset_remote.delay(asset_id_snapshot)
|
||
_log_task_dispatch_success(task_name="private_portrait.delete_asset_remote", user_id=current_user.id, project_id=project_id_snapshot, asset_id=asset_id_snapshot)
|
||
except Exception as exc:
|
||
_log_task_dispatch_failed(task_name="private_portrait.delete_asset_remote", user_id=current_user.id, project_id=project_id_snapshot, asset_id=asset_id_snapshot, exc=exc)
|
||
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)):
|
||
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)
|