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
+52
View File
@@ -625,6 +625,58 @@ def generate_base_asset(*, project, user, kind: str, prompt: str, label: str = "
raise
def generate_person_triview(*, project, user, portrait_group) -> "BaseAssetGroup":
"""流程步骤4 · 据「某一版立绘」生成它配套的三视图(image_edit 以立绘为参考,锁角色一致性)。
三视图与立绘 1:1 绑定:新立绘 → 新三视图。落一个 metadata.triview_of=<立绘组id> 的 person 组。"""
from apps.ai.model_library import THREE_VIEW_PROMPT
if portrait_group.kind != BaseAssetGroup.Kind.PERSON:
raise ValueError("三视图仅用于人物立绘")
portrait_asset = portrait_group.adopted_asset
if portrait_asset is None:
raise ValueError("该立绘尚未生成,无法据它生成三视图")
model_config = get_default_model(ModelConfig.Capability.IMAGE)
if model_config is None:
raise ValueError("no active image model configured")
provider = get_image_provider(model_config)
if not hasattr(provider, "image_edit"):
raise ValueError(f"当前图像模型 {model_config.provider.name}:{model_config.name} 不支持参考图三视图(image_edit)")
ref_url = _asset_preview_url(portrait_asset)
payload = {"model": model_config.name, "prompt": THREE_VIEW_PROMPT, "kind": "person", "triview_of": str(portrait_group.id)}
task = create_ai_task(project=project, user=user, task_type=AITask.Type.PERSON_IMAGE, model_config=model_config, request_payload=payload)
reservation = task.credit_reservation
try:
response = provider.image_edit(model=model_config.name, prompt=THREE_VIEW_PROMPT, images=[ref_url], size="1536x1024")
media = provider.extract_first_media_url(response)
with transaction.atomic():
task.status = AITask.Status.SUCCEEDED
task.response_payload = response
task.actual_cost = task.estimated_cost
task.completed_at = timezone.now()
task.save(update_fields=["status", "response_payload", "actual_cost", "completed_at", "updated_at"])
charge_reserved_credit(reservation=reservation, actual_amount=task.actual_cost)
asset = _store_generated_media(
team=project.team, user=user, project=project, task=task, media=media,
name=f"{project.name}-三视图", category=Asset.Category.PERSON, asset_type=Asset.Type.IMAGE,
)
# label 带立绘组 id:前端据此把三视图归到对应立绘版本(切立绘 → 切三视图)
group = BaseAssetGroup.objects.create(
project=project, kind=BaseAssetGroup.Kind.PERSON, task=task, prompt=THREE_VIEW_PROMPT,
metadata={"label": f"{portrait_group.id} ·三视图", "triview_of": str(portrait_group.id)},
)
group.candidate_assets.add(asset)
group.adopted_asset = asset
group.save(update_fields=["adopted_asset", "updated_at"])
return group
except Exception as exc:
task.status = AITask.Status.FAILED
task.error_message = str(exc)
task.completed_at = timezone.now()
task.save(update_fields=["status", "error_message", "completed_at", "updated_at"])
release_credit(reservation=reservation, reason=str(exc))
raise
def _scene_context(project) -> str:
"""从商品 + 已采用基础资产提炼一句「风格锚点」,贯穿故事板 / 视频,保证各镜内容一致。"""
product = project.product