feat(core): 基础资产 Agent 化 — 商品三视图(image_edit 参考主图)+ 人物立绘/三视图异步生成 + 演员库/详情

- 商品三视图:有真实商品主图时走 image_edit,以主图为参考锁包装一致(品牌字/配色/外形/Logo)
- 人物:据「某一版立绘」异步生成配套三视图(image_edit,worker 内跑),立绘/三视图各存版本可切换
- 基础资产出图改异步(run_base_asset_task / generate_triview_task),Web 层不被慢出图占住
- 前端:演员库浏览/新增、人物详情(立绘+三视图+版本切换/下载/查看大图)、按 busyKey 的单卡并发 loading(genBusy,替代全局 genBusyKey,各按钮互不阻塞)
- dev:本地连云端 MySQL 复用连接(CONN_MAX_AGE/health check/connect_timeout),仅 development 生效
- 含 projects 测试补充

tsc + py_compile + 26 后端测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 15:10:22 +08:00
co-authored by Claude Opus 4.8
parent da0c7edfe2
commit 296888f0bf
11 changed files with 361 additions and 59 deletions
+10 -5
View File
@@ -228,12 +228,15 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
kind = request.data.get("kind")
if kind not in BaseAssetGroup.Kind.values:
return Response({"detail": "invalid base asset kind"}, status=status.HTTP_400_BAD_REQUEST)
group = generate_base_asset(project=project, user=request.user, kind=kind, prompt=request.data.get("prompt", ""), label=request.data.get("label", ""))
try:
task = generate_base_asset(project=project, user=request.user, kind=kind, prompt=request.data.get("prompt", ""), label=request.data.get("label", ""))
except ValueError as exc: # 无可用模型 / 余额不足等,立即反馈
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.BASE_ASSETS)
stage.status = ProjectStage.Status.NEEDS_REVIEW
stage.save(update_fields=["status", "updated_at"])
promote_base_asset_stage_if_ready(project)
return Response(BaseAssetGroupSerializer(group).data, status=status.HTTP_201_CREATED)
# 异步:出图在 worker 里跑,秒回 RESERVED 任务,前端轮询 /api/ai/generate-image/?ids=… 取结果后刷新项目
return Response({"task": {"id": str(task.id), "status": task.status}}, status=status.HTTP_202_ACCEPTED)
@action(detail=True, methods=["post"], url_path="poll-reviews")
def poll_reviews(self, request, pk=None):
@@ -276,15 +279,17 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
@action(detail=True, methods=["post"], url_path="generate-triview")
def generate_triview(self, request, pk=None):
"""流程步骤4 · 据某一版立绘资产生成它配套的三视图(image_edit 锁角色一致性,三视图绑定该立绘)。"""
require_worker() # 异步出图依赖 worker 兜底执行,没 worker 直接拒绝(否则任务永远 RESERVED)
project = self.get_object()
portrait_asset = Asset.objects.filter(team=project.team, id=request.data.get("portrait_asset_id")).first()
if portrait_asset is None:
return Response({"detail": "portrait asset not found"}, status=status.HTTP_404_NOT_FOUND)
try:
group = generate_person_triview(project=project, user=request.user, portrait_asset=portrait_asset)
task = generate_person_triview(project=project, user=request.user, portrait_asset=portrait_asset)
except ValueError as exc:
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
return Response(BaseAssetGroupSerializer(group).data, status=status.HTTP_201_CREATED)
# 异步:image_edit 出图在 worker 里跑,秒回 RESERVED 任务,前端轮询取结果后刷新项目即见三视图
return Response({"task": {"id": str(task.id), "status": task.status}}, status=status.HTTP_202_ACCEPTED)
# ── Stage 1 · 镜头脚本逐字段编辑 / 增删分镜 ──