拆镜复刻片段重试切片API | 真人/虚拟素材库上传API
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlencode, unquote
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -18,6 +18,7 @@ from app.enums.private_portrait import (
|
||||
)
|
||||
from app.models.private_portrait import PrivatePortraitAsset, PrivatePortraitProject
|
||||
from app.models.user import User
|
||||
from app.enums.upload_resource import UploadResourceTypeEnum
|
||||
from app.schemas.private_portrait import (
|
||||
PrivatePortraitAssetCreate,
|
||||
PrivatePortraitAssetListOut,
|
||||
@@ -31,6 +32,7 @@ from app.schemas.private_portrait import (
|
||||
PrivatePortraitProjectOut,
|
||||
PrivatePortraitProjectUpdate,
|
||||
PrivatePortraitSelectableAssetListOut,
|
||||
PrivatePortraitUploadOut,
|
||||
PrivatePortraitValidateSessionCreate,
|
||||
PrivatePortraitValidateSessionOut,
|
||||
build_private_portrait_enum_meta,
|
||||
@@ -55,6 +57,8 @@ from app.services.private_portrait.project_service import (
|
||||
refresh_project_counters,
|
||||
soft_delete_project,
|
||||
)
|
||||
from app.services.private_portrait.upload_service import upload_private_portrait_asset_file
|
||||
from app.services.upload_resource import cleanup_upload_resource_files_after_commit
|
||||
from app.services.private_portrait.real_person.service import (
|
||||
create_real_person_asset,
|
||||
create_real_person_project,
|
||||
@@ -124,6 +128,62 @@ async def get_private_portrait_enum_meta():
|
||||
return build_private_portrait_enum_meta()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/uploads/image",
|
||||
response_model=PrivatePortraitUploadOut,
|
||||
summary="上传真人图片素材",
|
||||
description="上传真人图片素材并写入 UploadResource,module=private_portrait_real。创建素材时需回传 resource_id 到 upload_resource_id。",
|
||||
)
|
||||
async def upload_private_portrait_image(file: UploadFile = File(...), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
try:
|
||||
out = await upload_private_portrait_asset_file(
|
||||
db,
|
||||
file=file,
|
||||
current_user=current_user,
|
||||
library_type=PrivatePortraitLibraryType.REAL_PERSON.value,
|
||||
resource_type=UploadResourceTypeEnum.IMAGE.value,
|
||||
)
|
||||
await db.commit()
|
||||
return out
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"上传真人图片素材失败: {exc}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/uploads/video",
|
||||
response_model=PrivatePortraitUploadOut,
|
||||
summary="上传真人视频素材",
|
||||
description="上传真人视频素材并写入 UploadResource,module=private_portrait_real。创建素材时需回传 resource_id 到 upload_resource_id。",
|
||||
)
|
||||
async def upload_private_portrait_video(
|
||||
file: UploadFile = File(...),
|
||||
duration_seconds: float | None = Query(None, description="客户端解析的视频秒数,服务端会写入 UploadResource 并在创建素材时回填"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
out = await upload_private_portrait_asset_file(
|
||||
db,
|
||||
file=file,
|
||||
current_user=current_user,
|
||||
library_type=PrivatePortraitLibraryType.REAL_PERSON.value,
|
||||
resource_type=UploadResourceTypeEnum.VIDEO.value,
|
||||
duration_seconds=duration_seconds,
|
||||
)
|
||||
await db.commit()
|
||||
return out
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"上传真人视频素材失败: {exc}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/projects",
|
||||
response_model=PrivatePortraitProjectCreateWithValidateOut,
|
||||
@@ -196,6 +256,7 @@ async def update_private_portrait_project(project_id: str, payload: PrivatePortr
|
||||
async def delete_private_portrait_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.REAL_PERSON.value)
|
||||
project_id_snapshot = project.id
|
||||
pending_upload_resource_ids = list(getattr(project, "_pending_upload_resource_ids", []) or [])
|
||||
await db.commit()
|
||||
try:
|
||||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_project_remote
|
||||
@@ -204,6 +265,21 @@ async def delete_private_portrait_project(project_id: str, current_user: User =
|
||||
_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)
|
||||
if pending_upload_resource_ids:
|
||||
try:
|
||||
await cleanup_upload_resource_files_after_commit(db, resource_ids=pending_upload_resource_ids)
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
log_operation_error(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.ASSET_UPLOAD_RELEASE_FAILED.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=current_user.id,
|
||||
project_id=project_id_snapshot,
|
||||
exc=exc,
|
||||
detail={"resource_ids": pending_upload_resource_ids},
|
||||
)
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
@@ -337,6 +413,7 @@ async def delete_private_portrait_asset(asset_id: str, current_user: User = Depe
|
||||
asset = await soft_delete_asset(db, user_id=current_user.id, asset_id=asset_id, library_type=PrivatePortraitLibraryType.REAL_PERSON.value)
|
||||
asset_id_snapshot = asset.id
|
||||
project_id_snapshot = asset.project_id
|
||||
pending_upload_resource_ids = list(getattr(asset, "_pending_upload_resource_ids", []) or [])
|
||||
await db.commit()
|
||||
try:
|
||||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_asset_remote
|
||||
@@ -345,6 +422,22 @@ async def delete_private_portrait_asset(asset_id: str, current_user: User = Depe
|
||||
_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)
|
||||
if pending_upload_resource_ids:
|
||||
try:
|
||||
await cleanup_upload_resource_files_after_commit(db, resource_ids=pending_upload_resource_ids)
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
log_operation_error(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.ASSET_UPLOAD_RELEASE_FAILED.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=current_user.id,
|
||||
project_id=project_id_snapshot,
|
||||
asset_id=asset_id_snapshot,
|
||||
exc=exc,
|
||||
detail={"resource_ids": pending_upload_resource_ids},
|
||||
)
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.enums.private_portrait import (
|
||||
)
|
||||
from app.models.private_portrait import PrivatePortraitAsset, PrivatePortraitProject
|
||||
from app.models.user import User
|
||||
from app.enums.upload_resource import UploadResourceTypeEnum
|
||||
from app.schemas.private_portrait import (
|
||||
PrivatePortraitAssetCreate,
|
||||
PrivatePortraitAssetListOut,
|
||||
@@ -26,6 +27,7 @@ from app.schemas.private_portrait import (
|
||||
PrivatePortraitProjectOut,
|
||||
PrivatePortraitProjectUpdate,
|
||||
PrivatePortraitSelectableAssetListOut,
|
||||
PrivatePortraitUploadOut,
|
||||
PrivatePortraitVirtualProjectCreate,
|
||||
build_private_portrait_enum_meta,
|
||||
)
|
||||
@@ -46,6 +48,8 @@ from app.services.private_portrait.project_service import (
|
||||
refresh_project_counters,
|
||||
soft_delete_project,
|
||||
)
|
||||
from app.services.private_portrait.upload_service import upload_private_portrait_asset_file
|
||||
from app.services.upload_resource import cleanup_upload_resource_files_after_commit
|
||||
from app.services.private_portrait.virtual.service import create_virtual_asset, create_virtual_project, update_virtual_project
|
||||
|
||||
router = APIRouter(tags=["私域虚拟人像素材库"])
|
||||
@@ -103,6 +107,62 @@ async def get_virtual_private_portrait_enum_meta():
|
||||
return build_private_portrait_enum_meta()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/virtual/uploads/image",
|
||||
response_model=PrivatePortraitUploadOut,
|
||||
summary="上传虚拟图片素材",
|
||||
description="上传虚拟图片素材并写入 UploadResource,module=private_portrait_virtual。创建素材时需回传 resource_id 到 upload_resource_id。",
|
||||
)
|
||||
async def upload_private_portrait_virtual_image(file: UploadFile = File(...), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
try:
|
||||
out = await upload_private_portrait_asset_file(
|
||||
db,
|
||||
file=file,
|
||||
current_user=current_user,
|
||||
library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value,
|
||||
resource_type=UploadResourceTypeEnum.IMAGE.value,
|
||||
)
|
||||
await db.commit()
|
||||
return out
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"上传虚拟图片素材失败: {exc}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/virtual/uploads/video",
|
||||
response_model=PrivatePortraitUploadOut,
|
||||
summary="上传虚拟视频素材",
|
||||
description="上传虚拟视频素材并写入 UploadResource,module=private_portrait_virtual。创建素材时需回传 resource_id 到 upload_resource_id。",
|
||||
)
|
||||
async def upload_private_portrait_virtual_video(
|
||||
file: UploadFile = File(...),
|
||||
duration_seconds: float | None = Query(None, description="客户端解析的视频秒数,服务端会写入 UploadResource 并在创建素材时回填"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
out = await upload_private_portrait_asset_file(
|
||||
db,
|
||||
file=file,
|
||||
current_user=current_user,
|
||||
library_type=PrivatePortraitLibraryType.AIGC_VIRTUAL.value,
|
||||
resource_type=UploadResourceTypeEnum.VIDEO.value,
|
||||
duration_seconds=duration_seconds,
|
||||
)
|
||||
await db.commit()
|
||||
return out
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"上传虚拟视频素材失败: {exc}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/private-portrait/virtual-projects",
|
||||
response_model=PrivatePortraitProjectOut,
|
||||
@@ -170,6 +230,7 @@ async def update_private_portrait_virtual_project(project_id: str, payload: Priv
|
||||
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
|
||||
pending_upload_resource_ids = list(getattr(project, "_pending_upload_resource_ids", []) or [])
|
||||
await db.commit()
|
||||
try:
|
||||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_project_remote
|
||||
@@ -178,6 +239,21 @@ async def delete_private_portrait_virtual_project(project_id: str, current_user:
|
||||
_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)
|
||||
if pending_upload_resource_ids:
|
||||
try:
|
||||
await cleanup_upload_resource_files_after_commit(db, resource_ids=pending_upload_resource_ids)
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
log_operation_error(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.ASSET_UPLOAD_RELEASE_FAILED.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=current_user.id,
|
||||
project_id=project_id_snapshot,
|
||||
exc=exc,
|
||||
detail={"resource_ids": pending_upload_resource_ids},
|
||||
)
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
@@ -266,6 +342,7 @@ async def delete_private_portrait_virtual_asset(asset_id: str, current_user: Use
|
||||
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
|
||||
pending_upload_resource_ids = list(getattr(asset, "_pending_upload_resource_ids", []) or [])
|
||||
await db.commit()
|
||||
try:
|
||||
from app.tasks.private_portrait_asset_tasks import delete_private_portrait_asset_remote
|
||||
@@ -274,6 +351,22 @@ async def delete_private_portrait_virtual_asset(asset_id: str, current_user: Use
|
||||
_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)
|
||||
if pending_upload_resource_ids:
|
||||
try:
|
||||
await cleanup_upload_resource_files_after_commit(db, resource_ids=pending_upload_resource_ids)
|
||||
await db.commit()
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
log_operation_error(
|
||||
domain=DOMAIN,
|
||||
event_type=PrivatePortraitEventType.ASSET_UPLOAD_RELEASE_FAILED.value,
|
||||
source=PrivatePortraitEventSource.API.value,
|
||||
user_id=current_user.id,
|
||||
project_id=project_id_snapshot,
|
||||
asset_id=asset_id_snapshot,
|
||||
exc=exc,
|
||||
detail={"resource_ids": pending_upload_resource_ids},
|
||||
)
|
||||
return PrivatePortraitDeleteOut(success=True, remote_delete_status=PrivatePortraitRemoteDeleteStatus.PENDING.value)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Body, Depends, File, HTTPException, Path, Query,
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.dependencies import get_current_user, get_db
|
||||
from app.models.user import User
|
||||
from app.enums.shot_replicate import (
|
||||
@@ -30,6 +31,8 @@ from app.schemas.shot_replicate import (
|
||||
ShotReplicateImagePromptUpdateRequest,
|
||||
ShotReanalyzeOut,
|
||||
ShotReanalyzeRequest,
|
||||
ShotSegmentSplitRetryOut,
|
||||
ShotSplitRetryRequest,
|
||||
ShotReplicateMaterialUpdateRequest,
|
||||
ShotReplicateSpecOut,
|
||||
ShotReplicateTaskDetailOut,
|
||||
@@ -71,6 +74,7 @@ from app.services.shot_replicate_taskset_service import (
|
||||
list_task_sets,
|
||||
prepare_reanalyze_segment,
|
||||
prepare_reanalyze_task_set,
|
||||
prepare_retry_split_segment,
|
||||
segment_detail,
|
||||
task_set_detail,
|
||||
)
|
||||
@@ -685,6 +689,83 @@ async def reanalyze_segment(
|
||||
return out
|
||||
|
||||
|
||||
@router.post(
|
||||
"/segments/{segment_id}/retry-split",
|
||||
response_model=ShotSegmentSplitRetryOut,
|
||||
summary="重试拆镜片段视频切片",
|
||||
description="用于处理 ShotReplicateSegment 视频切片失败;重置 split_status 后复用现有 split_one_segment Celery 任务重新切割。",
|
||||
)
|
||||
async def retry_split_segment(
|
||||
segment_id: str = Path(..., description="拆镜片段ID,即 shot_replicate_segments.id"),
|
||||
req: ShotSplitRetryRequest = Body(default_factory=ShotSplitRetryRequest, description="切片失败重试参数"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ensure_celery_enabled(current_user=current_user, step_id=segment_id)
|
||||
try:
|
||||
out = await prepare_retry_split_segment(
|
||||
db,
|
||||
current_user=current_user,
|
||||
segment_id=segment_id,
|
||||
force=req.force,
|
||||
reason=req.reason,
|
||||
)
|
||||
task_set_id = out.task_set_id
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
await db.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
_log_api_error(
|
||||
event_type=ShotReplicateLogEventEnum.SEGMENT_SPLIT_RETRY_DISPATCH_FAILED.value,
|
||||
current_user=current_user,
|
||||
step_id=segment_id,
|
||||
message=f"切片失败重试状态重置失败: {exc}",
|
||||
detail={"segment_id": segment_id, "request": req.model_dump()},
|
||||
exc=exc,
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"切片失败重试状态重置失败: {exc}")
|
||||
|
||||
try:
|
||||
from app.tasks.shot_replicate_tasks import split_one_segment
|
||||
|
||||
await register_shot_split_task(segment_id, task_set_id=task_set_id)
|
||||
split_one_segment.apply_async(
|
||||
args=[segment_id],
|
||||
queue="gen_result_download",
|
||||
countdown=0,
|
||||
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
|
||||
)
|
||||
log_module_event_file(
|
||||
module=MODULE,
|
||||
event_type=ShotReplicateLogEventEnum.SEGMENT_SPLIT_RETRY_SUBMITTED.value,
|
||||
project_id=task_set_id,
|
||||
step_id=segment_id,
|
||||
user_id=_safe_user_id(current_user),
|
||||
message="拆镜片段切片重试任务已投递",
|
||||
detail={
|
||||
"segment_id": segment_id,
|
||||
"task_set_id": task_set_id,
|
||||
"task": "split_one_segment",
|
||||
"queue": "gen_result_download",
|
||||
"request": req.model_dump(),
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
_log_api_error(
|
||||
event_type=ShotReplicateLogEventEnum.SEGMENT_SPLIT_RETRY_DISPATCH_FAILED.value,
|
||||
current_user=current_user,
|
||||
project_id=task_set_id,
|
||||
step_id=segment_id,
|
||||
message=f"拆镜片段切片重试任务投递失败,等待恢复任务兜底: {exc}",
|
||||
detail={"segment_id": segment_id, "task_set_id": task_set_id, "task": "split_one_segment"},
|
||||
exc=exc,
|
||||
)
|
||||
out.message = "切片状态已重置,但 Celery 投递失败,将等待恢复任务兜底"
|
||||
return out
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/segments/{segment_id}",
|
||||
response_model=ShotSegmentDeleteOut,
|
||||
|
||||
Reference in New Issue
Block a user