fix(ai): 图片创作重跑三连修——丢参考图/裂新批次/提示词矛盾

① 重跑丢参考图:「重跑这张/重跑整批」原来不带 refs → 后端收不到
reference_image_ids 落到纯文生图,出图与上传素材无关。现 GenBatch.refs
存 assetId(上传后回写;恢复批次由 tasks 接口新带的 id 还原),重跑原样带回。

② 重跑裂新聊天记录:enqueue 每次新铸 batch_id,后端没有「补进原批次」
概念,刷新/切对话后重跑图裂成新卡、原卡失败格还挂着。现生成接口收/回
batch_id(UUID 校验,非法兜底新铸),重跑沿用原批次并打 batch_append 标记;
前端批次卡存 backendBatchId(提交回调即落,整批全失败也不丢),重跑带回。
恢复分组时 rerun 任务不计入「应出张数」→ 补图成功后失败格自然收掉;
批次状态改按「有任务在跑」判定,已有好图+补图在途也能接续轮询。

③ 同批换装一半不换:带参考图模板「严格保留款式/不要换款」与用户要求
(如"生成现代服装的穿着")直接矛盾,模型每张随机听一边。改为只锁主体身份
(人物五官/发型/体型,商品品类/外形/Logo),用户明确要求改变的以用户要求
为最高优先级,未提及的才默认与参考图一致。

测试:apps 全量 214 过(需 --settings=airshelf.settings.test);
新增 test_rerun_with_batch_id_reuses_batch_and_marks_append。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-02 13:40:52 +08:00
co-authored by Claude Fable 5
parent dcc5d7fe50
commit a048c32527
7 changed files with 148 additions and 43 deletions
+32 -11
View File
@@ -1166,8 +1166,11 @@ def build_platform_cover_prompt_refs(
def build_free_reference_prompt(base_prompt: str, n_refs: int = 1, index: int = 0) -> str:
"""图片创作自由模式 · 带用户上传参考图时的提示词。
纯把用户原话丢给图生图,模型只会松散借个色调、不会真的保留参考图里的主体(背心/商品/人物),
是「没参考我上传的素材」的根因。这里显式把参考图钉成「画面主体的唯一依据」,要求严格保留其
外形/款式/配色/材质/品牌文字/图案,再在此基础上按用户要求创作。"""
这是「没参考我上传的素材」的根因参考图钉成「画面主体的唯一依据」
但一致性只锁「主体身份」(人物的五官/发型/体型,商品的品类/外形/Logo),不再无差别钉死款式/配色/材质:
用户要求本身常常就是要改变某个属性(如「参考这个角色,生成现代服装的穿着」),旧模板的
「严格保留款式、不要换款」与之直接矛盾 → 同批图模型每张随机听一边,一半换装一半原封不动。
改为:用户明确要求改变的部分以用户要求为最高优先级;用户没提的部分才默认与参考图一致。"""
base = (base_prompt or "").strip()
if n_refs <= 1:
ref_intro = "参考图是用户提供的素材,是本次画面主体(商品 / 人物 / 物体)的唯一依据。"
@@ -1178,18 +1181,21 @@ def build_free_reference_prompt(base_prompt: str, n_refs: int = 1, index: int =
ref_word = f"参考图{rng}"
lines = [
ref_intro,
f"请严格保留{ref_word}主体的外形、款式、配色、材质、纹样、品牌文字与 Logo,"
"不要重新设计、不要换、不要成相似但不同的物体;主体须与参考图高度一致",
f"画面主体必须取自{ref_word},主体身份须与参考图高度一致:人物须是同一个人(五官、发型、肤色、体型不变),"
"商品 / 物体须是同一件(品类、外形、品牌文字与 Logo 不变);不要换、不要成相似但不同的物体。",
]
if base:
lines.append(f"在此基础上,按用户要求创作:{base}")
lines.append(
f"用户要求:{base}。用户要求是最高优先级:用户明确要求改变的部分(如服装、场景、动作、风格等)"
"必须按要求大胆改变、不要保留参考图原样;用户没有要求改变的部分,保持与参考图一致。"
)
else:
lines.append("在此基础上,生成干净、专业的电商视觉画面。")
# 同批多张要不同构图/视角,否则雷同(PMC#24);主体一致性已在上面钉死,这里只变表现。
lines.append("用户未提出改变要求:请严格保留主体的外形、款式、配色、材质、纹样与图案,生成干净、专业的电商视觉画面。")
# 同批多张要不同构图/视角,否则雷同(PMC#24);主体身份一致性已在上面钉死,这里只变表现。
# 自由模式用 _FREE_VARIATIONS(只换视角/光线/机位),不能用套图 _COVER_VARIATIONS:
# 后者含"卖点文案分区/留出标题价签区",会让带参考图的第2张起也凭空长出广告文案。
if index > 0:
lines.append(_FREE_VARIATIONS[index % len(_FREE_VARIATIONS)] + ",在保持主体一致的前提下换一种表现,不要添加任何文字、卖点、标题或价签。")
lines.append(_FREE_VARIATIONS[index % len(_FREE_VARIATIONS)] + ",在保持主体身份一致、遵循用户要求的前提下换一种表现,不要添加任何文字、卖点、标题或价签。")
lines.append("画面真实、构图协调、细节清晰。")
return " ".join(lines)
@@ -2407,7 +2413,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, image_model: str | None = None, conversation=None, reference_image_ids: list[str] | None = None, platform_id: 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, conversation=None, reference_image_ids: list[str] | None = None, platform_id: str | None = None, batch_id: str | None = None) -> list[AITask]:
"""独立生图(图片创作 / 模特上身图 / 平台套图)改为**异步**:本函数在 Web 请求里只做「建任务 +
预留额度」这种秒级的活,真正 ~30s 的 ARK 出图交给 Celery worker(generate_standalone_image_task)。
@@ -2432,13 +2438,28 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
if alt is not None:
model_config = alt
# 本次提交 = 一组(模特上身图组 / 平台套图组):同一 batch_id 串起这批图,前端可成组展示。
batch_id = str(uuid.uuid4())
# 重跑/补图会带原批次的 batch_id 进来 → 沿用它并打 batch_append 标记,后端记录归回原批次
# 而不是裂成一条新批次(前端按 batch_id 归批,刷新/切换对话后重跑图仍在原卡里);
# batch_append 任务不计入批次「应出张数」,前端据此在补图成功后收掉对应的失败格。
is_append = False
if batch_id:
try:
batch_id = str(uuid.UUID(str(batch_id)))
is_append = True
except (TypeError, ValueError):
batch_id = None
if not is_append:
batch_id = str(uuid.uuid4())
# 平台套图:规范化平台 id(前端 dy/tb… → canonical),用于注入平台版式块(优化版);非 cover 模式忽略。
platform_key = str(platform_id or "").strip() if mode == "cover" else ""
platform_name = _PLATFORM_NAMES.get(platform_key, "")
tasks: list[AITask] = []
for index in range(count):
cost = estimate_cost(model_config)
request_payload = {"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index, "product_id": str(product_id) if product_id else None, "reference_product": bool(reference_product), "model_id": str(model_id) if model_id else None, "model_entity_id": str(model_entity_id) if model_entity_id else None, "batch_id": batch_id, "ratio": str(ratio) if ratio else None, "reference_image_ids": ref_ids, "platform_id": platform_key or None, "platform_name": platform_name or None}
# 只在重跑/补图时落键(不落 False):workbench 用 KeyTextTransform 抽文本,"false" 字符串也是真值,会误判
if is_append:
request_payload["batch_append"] = True
task = AITask.objects.create(
team=team,
created_by=user,
@@ -2448,7 +2469,7 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c
status=AITask.Status.CREATED,
model_config=model_config,
idempotency_key=f"standalone-image:{team.id}:{uuid.uuid4()}",
request_payload={"model": model_config.name, "endpoint": model_config.endpoint, "prompt": prompt, "mode": mode, "index": index, "product_id": str(product_id) if product_id else None, "reference_product": bool(reference_product), "model_id": str(model_id) if model_id else None, "model_entity_id": str(model_entity_id) if model_entity_id else None, "batch_id": batch_id, "ratio": str(ratio) if ratio else None, "reference_image_ids": ref_ids, "platform_id": platform_key or None, "platform_name": platform_name or None},
request_payload=request_payload,
estimated_cost=cost,
)
# 预留额度若余额不足会抛 ValueError,在同步的 Web 请求里立刻反馈给前端(不会先建半套任务)