生图模型可选(火山/gpt-image)+ 模特上身图提示词强化 + 多会话等改动

本轮(生图模型选择 + 火山接入):
- 工作室新增「生图模型」选择器(模特上身图/平台套图头部 chip + 图片创作底部 Pill),
  默认火山 Seedream,可切 gpt-image-2;选择写入 localStorage,下次进页面读回
- 后端 resolve_image_model 解析所选模型;enqueue_standalone_images 接 image_model
- worker 按模型能力分流:有 image_edit(gpt-image)走多图编辑;无(火山)走
  image_generation(image=参考图);新增 _ratio_to_volcano_size 让火山按比例出图
  → 内衣等敏感品类用火山可绕开 gpt-image 的 sexual 内容审核

模特上身图提示词:
- 穿戴/非穿戴分流、按 index 变化动作场景镜头、负面词尾接、多图参考序号自适应
- _product_reference_urls:商品参考真实上传图优先、排除 AI 生成图、可多张

其他(并入此前各会话未提交改动):
- 图片创作多会话(ImageConversation + migration 0019)、任务中心按类型过滤
- accounts/projects/assets/team/auth 等零散调整、相关测试
- 测试脚本(tryon_*.py)、测试清单(core/bug/*.xlsx)
This commit is contained in:
zyc
2026-06-27 14:00:28 +08:00
parent 0a519ca892
commit ce7eaefe1d
21 changed files with 1279 additions and 158 deletions
+79
View File
@@ -872,3 +872,82 @@ class BaseAssetAdoptDeleteTests(TestCase):
res = self.client.post(f"/api/projects/{self.proj.id}/delete-base-asset/", {"group_id": str(self.group.id)}, format="json")
self.assertEqual(res.status_code, 200)
self.assertFalse(BaseAssetGroup.objects.filter(id=self.group.id).exists())
class AttachOfficialModelTests(TestCase):
"""ZWQ#4/#5 回归:从演员库挑「官方模特」(跨团队)新增/替换角色,旧版因 team 隔离报 asset not found
且替换失败、还留下空角色组。修复后:官方模特形象图自动克隆进本团队并真正挂上,不再 404。"""
def setUp(self):
from apps.assets.models import Asset, AssetFile, Model
from apps.projects.models import BaseAssetGroup
self.user = User.objects.create_user(username="biz", password="x")
self.team = Team.objects.create(name="商家团队", owner=self.user)
TeamMember.objects.create(team=self.team, user=self.user, role=TeamMember.Role.OWNER)
self.product = Product.objects.create(team=self.team, created_by=self.user, title="口红")
self.proj = Project.objects.create(team=self.team, name="带货视频", product=self.product, created_by=self.user)
# 官方模特属于「官方团队」(跨团队可见,但 portrait 资产不属本商家团队)——正是触发 bug 的根因。
self.official_owner = User.objects.create_user(username="ops", password="x")
self.official_team = Team.objects.create(name="官方", owner=self.official_owner)
self.portrait = Asset.objects.create(
team=self.official_team, created_by=self.official_owner, name="都市白领 林晚",
asset_type=Asset.Type.IMAGE, source=Asset.Source.AI_GENERATED, category=Asset.Category.PERSON,
metadata={"kind": "model"},
)
AssetFile.objects.create(asset=self.portrait, object_key="m/linwan.png", bucket="b", content_type="image/png", preview_url="http://x/linwan.png", is_primary=True)
self.model = Model.objects.create(
team=self.official_team, created_by=self.official_owner, name="都市白领 林晚",
is_official=True, portrait_asset=self.portrait,
)
self.BaseAssetGroup = BaseAssetGroup
self.client = APIClient()
self.client.force_authenticate(self.user)
def test_add_actor_from_official_model_clones_and_attaches(self):
"""新增角色:无 group,传 kind+label,选官方模特 → 200,克隆进本团队并采用,不再 asset not found。"""
from apps.assets.models import Asset
res = self.client.post(
f"/api/projects/{self.proj.id}/attach-base-asset/",
{"kind": "person", "label": "林晚", "asset_id": str(self.portrait.id)},
format="json",
)
self.assertEqual(res.status_code, 200, res.content)
group = self.BaseAssetGroup.objects.get(id=res.json()["id"])
self.assertIsNotNone(group.adopted_asset_id)
clone = Asset.objects.get(id=group.adopted_asset_id)
# 克隆体真正落进本商家团队(不是官方团队),并共享同一份 TOS 对象。
self.assertEqual(clone.team_id, self.team.id)
self.assertNotEqual(clone.id, self.portrait.id)
self.assertEqual(clone.files.first().object_key, "m/linwan.png")
self.assertEqual(clone.metadata.get("cloned_from_asset"), str(self.portrait.id))
def test_replace_existing_group_with_official_model(self):
"""替换:已有角色组,传 group_id + 官方模特 asset_id → 200,采用克隆体。"""
from apps.assets.models import Asset
group = self.BaseAssetGroup.objects.create(project=self.proj, kind=self.BaseAssetGroup.Kind.PERSON, metadata={"label": "主播"})
res = self.client.post(
f"/api/projects/{self.proj.id}/attach-base-asset/",
{"group_id": str(group.id), "asset_id": str(self.portrait.id)},
format="json",
)
self.assertEqual(res.status_code, 200, res.content)
group.refresh_from_db()
clone = Asset.objects.get(id=group.adopted_asset_id)
self.assertEqual(clone.team_id, self.team.id)
def test_unknown_asset_still_404_without_ghost_group(self):
"""彻底不存在的 asset_id → 仍 404,且不留下空角色组(不再在 404 前提交建组)。"""
import uuid as _uuid
before = self.BaseAssetGroup.objects.filter(project=self.proj).count()
res = self.client.post(
f"/api/projects/{self.proj.id}/attach-base-asset/",
{"kind": "person", "label": "幽灵", "asset_id": str(_uuid.uuid4())},
format="json",
)
self.assertEqual(res.status_code, 404)
self.assertEqual(self.BaseAssetGroup.objects.filter(project=self.proj).count(), before)