feat(core): 基础资产页按流程文档补完 step4(人物/场景详情+演员库+三视图绑定立绘)

人物/场景:
- 卡片可点开详情弹窗:立绘大图(hover 右下角 下载/查看大图)+ 立绘版本切换 +
  提示词重跑(覆盖卡片立绘,归入同角色)+ 应用到当前项目;场景详情不显示三视图
- 进入基础资产自动生成脚本提取的待生成人物/场景(带 loading),每项目一次
- 「替换」→ 演员库 replace 回填;人物区头部「演员库」入口

三视图:与「某一版立绘」1:1 绑定(metadata.triview_of=立绘组id)
- 生成/重跑立绘 → 链式据该立绘 image_edit 出配套三视图(锁角色一致性)
- 详情切换立绘版本 → 三视图跟着切;可据当前立绘再出多版,下方版本小图可切
- 后端 generate_person_triview + 端点 generate-triview,复用 model_library 三视图 SOP

商品三视图弹窗:记录版本、切换查看、重跑生成、采用此版本

演员库(新建 components/actor-library.tsx 覆盖层):
- 平台预设演员 / 我的演员两 tab + 添加演员工作台(AI 生成 / 本地上传保存)
- 替换基础资产卡:新增后端 attach-base-asset(把现有资产挂为候选并采用)

合并 dev 进来的火山真人审核绿/红盾到新的实体卡。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 12:00:56 +08:00
co-authored by Claude Opus 4.8
parent b30eaed838
commit a9b7f077b9
7 changed files with 690 additions and 73 deletions
+31
View File
@@ -19,6 +19,7 @@ from apps.ai.services import (
VOICEOVER_VOICES,
create_export_job,
generate_base_asset,
generate_person_triview,
generate_project_script,
generate_storyboard_frame,
get_default_model,
@@ -255,6 +256,36 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
promote_base_asset_stage_if_ready(project)
return Response(BaseAssetGroupSerializer(group).data)
@action(detail=True, methods=["post"], url_path="attach-base-asset")
@transaction.atomic
def attach_base_asset(self, request, pk=None):
"""流程步骤4 · 用演员库现有资产替换某基础资产卡:把团队内现有 Asset 挂为该组候选并采用。"""
project = self.get_object()
group = BaseAssetGroup.objects.select_for_update().filter(project=project, id=request.data.get("group_id")).first()
if group is None:
return Response({"detail": "group not found"}, status=status.HTTP_404_NOT_FOUND)
asset = Asset.objects.filter(team=project.team, id=request.data.get("asset_id")).first()
if asset is None:
return Response({"detail": "asset not found"}, status=status.HTTP_404_NOT_FOUND)
group.candidate_assets.add(asset)
group.adopted_asset_id = asset.id
group.save(update_fields=["adopted_asset", "updated_at"])
promote_base_asset_stage_if_ready(project)
return Response(BaseAssetGroupSerializer(group).data)
@action(detail=True, methods=["post"], url_path="generate-triview")
def generate_triview(self, request, pk=None):
"""流程步骤4 · 据某一版立绘生成它配套的三视图(image_edit 锁角色一致性)。"""
project = self.get_object()
portrait = BaseAssetGroup.objects.filter(project=project, id=request.data.get("portrait_group_id")).first()
if portrait is None:
return Response({"detail": "portrait group not found"}, status=status.HTTP_404_NOT_FOUND)
try:
group = generate_person_triview(project=project, user=request.user, portrait_group=portrait)
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)
# ── Stage 1 · 镜头脚本逐字段编辑 / 增删分镜 ──
def _sync_video_segments_to_script(self, project: Project, script: ScriptVersion) -> None: