feat: 生图模型可选(火山/gpt-image) + 工作台图「加入资产库」收纳 + 任务中心排序修复
- AI 工作室生图支持显式选模型:resolve_image_model 解析 volcano(Seedream 图生图, 无 image_edit 时走 image_generation 带参考图)/ gpt-image(image_edit 多图编辑), 未选回落系统默认;enqueue_standalone_images 透传 image_model - 资产库收纳:Asset 加 in_library 字段(迁移 0007,既有资产 db_default=True 不动); 工作台生成图默认 in_library=False,只在工作台展示,用户「加入资产库」后才进库列表; assets 视图/序列化器/library 页/types 配套 - 任务中心修复:AITaskViewSet 默认 order_by(-created_at),前端 aiTasks 取 page_size=200。 原因:列表无排序时 MySQL 按 UUID 主键乱序返回,把一批旧失败记录顶到首页, 前端又只取首页 20 条算 全部/已完成/失败 → 误显示「全部失败」(实为 421 成功/少量失败) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,28 @@ def get_default_model(capability: str) -> ModelConfig:
|
||||
return qs.filter(is_default=True).order_by("created_at").first() or qs.order_by("created_at").first()
|
||||
|
||||
|
||||
def resolve_image_model(key: str | None) -> "ModelConfig | None":
|
||||
"""前端「生图模型选择」→ ModelConfig。用户显式选的可以是 disabled 模型(故不按 status 过滤)。
|
||||
· "volcano" → 火山官方 Seedream(取最新一版)
|
||||
· "gpt-image"→ gpt-image-2(优先 active provider 的那个)
|
||||
· "provider:name" 或裸 name → 精确匹配
|
||||
解析不到返回 None,调用方回落 get_default_model。"""
|
||||
if not key:
|
||||
return None
|
||||
qs = ModelConfig.objects.select_related("provider").filter(capability=ModelConfig.Capability.IMAGE)
|
||||
if key == "volcano":
|
||||
# 火山 Seedream:优先版本号最高的(seedream-5 > seedream-4),按 name 倒序
|
||||
vqs = qs.filter(provider__name__in=OFFICIAL_DIRECT_PROVIDERS)
|
||||
return vqs.filter(name__icontains="seedream").order_by("-name").first() or vqs.order_by("-name").first()
|
||||
if key in ("gpt-image", "gpt-image-2"):
|
||||
return (qs.filter(name__icontains="gpt-image", provider__status="active").first()
|
||||
or qs.filter(name__icontains="gpt-image").first())
|
||||
if ":" in key:
|
||||
pname, mname = key.split(":", 1)
|
||||
return qs.filter(provider__name=pname, name=mname).first()
|
||||
return qs.filter(name=key).first()
|
||||
|
||||
|
||||
# 火山官方直连(SeeDream 生图 / Seedance 视频 / 豆包文本)走 ARK SDK;其余 provider 一律
|
||||
# 视为「OpenAI 兼容中转站」走通用适配器。加/换中转站 = DB 加一行 ModelProvider,零改代码。
|
||||
# 注意:DB 里火山 provider 实际命名为 "volcengine"(豆包),必须包含,否则会被错路由到中转站。
|
||||
@@ -1976,7 +1998,7 @@ def _reap_stale_standalone_image_tasks(*, team) -> None:
|
||||
continue
|
||||
|
||||
|
||||
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None) -> list[AITask]:
|
||||
def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None, image_model: str | None = None) -> list[AITask]:
|
||||
"""独立生图(图片创作 / 模特上身图 / 平台套图)改为**异步**:本函数在 Web 请求里只做「建任务 +
|
||||
预留额度」这种秒级的活,真正 ~30s 的 ARK 出图交给 Celery worker(generate_standalone_image_task)。
|
||||
|
||||
@@ -1986,7 +2008,8 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
|
||||
from apps.ai.tasks import generate_standalone_image_task
|
||||
|
||||
_reap_stale_standalone_image_tasks(team=team)
|
||||
model_config = get_default_model(ModelConfig.Capability.IMAGE)
|
||||
# 用户在工作室选的生图模型(火山 / gpt-image)优先;未选或解析不到则回落系统默认
|
||||
model_config = resolve_image_model(image_model) or get_default_model(ModelConfig.Capability.IMAGE)
|
||||
if model_config is None:
|
||||
raise ValueError("no active image model configured")
|
||||
task_type = _STANDALONE_TASK_TYPE.get(mode, AITask.Type.PRODUCT_IMAGE)
|
||||
@@ -2060,30 +2083,35 @@ def run_standalone_image_task(*, task_id: str) -> None:
|
||||
# 模特上身图:真实上传图优先、排除 AI 生成图,可多张(多角度更易锁外形/品牌)
|
||||
product_urls = _product_reference_urls(product, limit=3) if product is not None else []
|
||||
|
||||
# 参考图收集与 provider 无关:先把「该用哪些参考图 + 哪条提示词」定下来,再按模型能力选调用方式。
|
||||
edit_images: list[str] = []
|
||||
edit_prompt = ""
|
||||
if mode == "model" and can_edit and product_urls:
|
||||
if mode == "model" and product_urls:
|
||||
# 模特上身图:参考图1~N=商品真实图(多角度),参考图N+1=模特(模特图可缺则让模型自取真人模特)
|
||||
edit_images = product_urls + ([model_url] if model_url else [])
|
||||
edit_prompt = build_model_tryon_prompt_refs(
|
||||
product, has_model=bool(model_url), base_prompt=prompt,
|
||||
index=index, n_product=len(product_urls),
|
||||
)
|
||||
elif mode == "cover" and can_edit and product_url:
|
||||
elif mode == "cover" and product_url:
|
||||
# 平台套图:参考图1=商品真实主图(锁包装一致性),有模特则参考图2=模特(锁人脸/身形)
|
||||
edit_images = [product_url] + ([model_url] if model_url else [])
|
||||
edit_prompt = build_platform_cover_prompt_refs(product, has_model=bool(model_url), base_prompt=prompt)
|
||||
elif bool(payload.get("reference_product")) and can_edit and product_url:
|
||||
elif bool(payload.get("reference_product")) and product_url:
|
||||
edit_images = [product_url]
|
||||
edit_prompt = build_product_triview_prompt_refs(product, "")
|
||||
use_edit = bool(edit_images)
|
||||
try:
|
||||
if use_edit:
|
||||
if use_edit and can_edit:
|
||||
# gpt-image 等支持 image_edit(多图参考编辑接口)
|
||||
if payload.get("reference_product"):
|
||||
size = "1536x1024" # 三视图固定横向
|
||||
else:
|
||||
size = _ratio_to_image_size(str(payload.get("ratio") or "")) # 模特图按选中比例
|
||||
response = provider.image_edit(model=model_config.name, prompt=edit_prompt, images=edit_images, size=size)
|
||||
elif use_edit:
|
||||
# 火山 Seedream 无 image_edit:走 image_generation 带 image=参考图(图生图多参考),size 用 2K
|
||||
response = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=edit_prompt, image=edit_images, size="2K")
|
||||
else:
|
||||
response = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=prompt)
|
||||
media = provider.extract_first_media_url(response)
|
||||
@@ -2112,6 +2140,8 @@ def run_standalone_image_task(*, task_id: str) -> None:
|
||||
id=asset_id, team=team, created_by=user, name=f"AI 生成 · {asset_label} · {index + 1}",
|
||||
asset_type=Asset.Type.IMAGE, source=Asset.Source.AI_GENERATED, category=category, origin_task=task,
|
||||
metadata=asset_meta,
|
||||
# 工作台生成的图默认不进资产库列表,只在工作台展示;用户「加入资产库」后才置 True
|
||||
in_library=False,
|
||||
)
|
||||
AssetFile.objects.create(asset=asset, object_key=stored.object_key, bucket=stored.bucket, content_type=stored.content_type, size_bytes=stored.size_bytes, is_primary=True)
|
||||
except Exception as exc: # noqa: BLE001 — 失败要退费并把错误记进 AITask 供前端轮询读取;不向上抛(避免 celery 重试二次扣费)
|
||||
|
||||
Reference in New Issue
Block a user