from __future__ import annotations import json from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.enums.private_portrait import ( PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME, PRIVATE_PORTRAIT_VIRTUAL_GROUP_TYPE, PrivatePortraitAssetGroupStatus, PrivatePortraitEventSource, PrivatePortraitEventStatus, PrivatePortraitEventType, PrivatePortraitLibraryType, PrivatePortraitProjectStatus, ) from app.models.private_portrait import PrivatePortraitAssetGroup from app.schemas.private_portrait import PrivatePortraitAssetCreate, PrivatePortraitProjectUpdate, PrivatePortraitVirtualProjectCreate from app.services.operation_log_service import log_operation_error, log_operation_event from app.services.private_portrait.ark_client import ArkPrivateAssetClient from app.services.private_portrait.asset_service import create_asset from app.services.private_portrait.project_service import create_project, refresh_project_counters, update_project from app.utils.id_gen import generate_id DOMAIN = "private_portrait" def _json(data) -> str | None: if data is None: return None return json.dumps(data, ensure_ascii=False, default=str) def _remote_group_name(user_id: str, project_name: str) -> str: safe_name = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in project_name.strip())[:80] return f"virtual-{user_id}-{safe_name}"[:128] async def create_virtual_project(db: AsyncSession, *, user_id: str, payload: PrivatePortraitVirtualProjectCreate): project = await create_project( db, user_id=user_id, payload=payload, # type: ignore[arg-type] library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value, status=PrivatePortraitProjectStatus.CREATING_REMOTE_GROUP.value, remote_project_name=PRIVATE_PORTRAIT_REMOTE_PROJECT_NAME, ) remote_group_name = _remote_group_name(user_id, project.name) log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.VIRTUAL_ASSET_GROUP_CREATE_REMOTE_START.value, event_status=PrivatePortraitEventStatus.PENDING.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project.id, detail={"remote_group_name": remote_group_name, "remote_project_name": project.remote_project_name, "group_type": PRIVATE_PORTRAIT_VIRTUAL_GROUP_TYPE}, ) try: resp = await ArkPrivateAssetClient().create_asset_group( project_name=project.remote_project_name, name=remote_group_name, description=project.description, group_type=PRIVATE_PORTRAIT_VIRTUAL_GROUP_TYPE, ) remote_group_id = resp.get("Id") or resp.get("GroupId") or resp.get("groupId") if not remote_group_id: raise RuntimeError("CreateAssetGroup 未返回素材组 ID") group = PrivatePortraitAssetGroup( id=generate_id(), user_id=user_id, project_id=project.id, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value, remote_group_id=remote_group_id, remote_group_name=remote_group_name, remote_project_name=project.remote_project_name, group_type=PRIVATE_PORTRAIT_VIRTUAL_GROUP_TYPE, status=PrivatePortraitAssetGroupStatus.ACTIVE.value, raw_response_json=_json(resp), ) db.add(group) project.status = PrivatePortraitProjectStatus.ACTIVE.value await refresh_project_counters(db, [project.id]) await db.flush() await db.refresh(project) log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.VIRTUAL_ASSET_GROUP_CREATE_REMOTE_SUCCESS.value, event_status=PrivatePortraitEventStatus.SUCCESS.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project.id, group_id=group.id, detail={"remote_group_id": remote_group_id, "remote_group_name": remote_group_name, "remote_project_name": project.remote_project_name}, ) return project except Exception as exc: project.status = PrivatePortraitProjectStatus.CREATE_GROUP_FAILED.value await db.flush() log_operation_error( domain=DOMAIN, event_type=PrivatePortraitEventType.VIRTUAL_ASSET_GROUP_CREATE_REMOTE_FAILED.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project.id, exc=exc, ) raise HTTPException(status_code=502, detail=f"创建火山虚拟人像素材组失败:{exc}") from exc async def update_virtual_project(db: AsyncSession, *, user_id: str, project_id: str, payload: PrivatePortraitProjectUpdate): project = await update_project(db, user_id=user_id, project_id=project_id, payload=payload, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value) # 远程同步失败不影响本地更新,记录日志便于排查。 try: # 只同步当前激活组。 from app.services.private_portrait.asset_service import get_project_active_group group = await get_project_active_group(db, user_id=user_id, project_id=project_id, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value) await ArkPrivateAssetClient().update_asset_group(project_name=project.remote_project_name, group_id=group.remote_group_id, name=group.remote_group_name, title=project.name, description=project.description) except Exception as exc: log_operation_error(domain=DOMAIN, event_type=PrivatePortraitEventType.ASSET_GROUP_UPDATE_REMOTE_FAILED.value, source=PrivatePortraitEventSource.API.value, user_id=user_id, project_id=project_id, exc=exc) return project async def create_virtual_asset(db: AsyncSession, *, user_id: str, project_id: str, payload: PrivatePortraitAssetCreate): return await create_asset(db, user_id=user_id, project_id=project_id, payload=payload, library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value)