|
@@ -531,6 +531,35 @@ def _part_consistency_prompt(row, preview_analysis):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _save_boss_part_image(chars_out, boss_id, part_id, img):
|
|
|
|
|
+ parts_dir = os.path.join(chars_out, f"{boss_id}_parts")
|
|
|
|
|
+ os.makedirs(parts_dir, exist_ok=True)
|
|
|
|
|
+ clean = spine_builder.trim_to_content(img.convert("RGBA"), pad=2)
|
|
|
|
|
+ path = os.path.join(parts_dir, f"{part_id}.png")
|
|
|
|
|
+ clean.save(path)
|
|
|
|
|
+ return {
|
|
|
|
|
+ "id": part_id,
|
|
|
|
|
+ "file": f"characters/{boss_id}_parts/{part_id}.png",
|
|
|
|
|
+ "w": clean.width,
|
|
|
|
|
+ "h": clean.height,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _upsert_part_meta(row, part_meta, preview_version=""):
|
|
|
|
|
+ parts = row.setdefault("parts", [])
|
|
|
|
|
+ for idx, old in enumerate(parts):
|
|
|
|
|
+ if old.get("id") == part_meta["id"]:
|
|
|
|
|
+ parts[idx] = part_meta
|
|
|
|
|
+ break
|
|
|
|
|
+ else:
|
|
|
|
|
+ parts.append(part_meta)
|
|
|
|
|
+ files = row.setdefault("files", [])
|
|
|
|
|
+ if part_meta["file"] not in files:
|
|
|
|
|
+ files.append(part_meta["file"])
|
|
|
|
|
+ if preview_version:
|
|
|
|
|
+ row.setdefault("partVersions", {})[part_meta["id"]] = preview_version
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
_set_job(job_id, status="running", logs=[], game=game, startedAt=time.time(), updatedAt=time.time())
|
|
_set_job(job_id, status="running", logs=[], game=game, startedAt=time.time(), updatedAt=time.time())
|
|
|
try:
|
|
try:
|
|
@@ -551,6 +580,15 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
consistency = _part_consistency_prompt(row, row.get("previewAnalysis", ""))
|
|
consistency = _part_consistency_prompt(row, row.get("previewAnalysis", ""))
|
|
|
|
|
|
|
|
_append_job_log(job_id, f"重生关主拆件:{boss_id}/{part_id}")
|
|
_append_job_log(job_id, f"重生关主拆件:{boss_id}/{part_id}")
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": 1,
|
|
|
|
|
+ "done": 0,
|
|
|
|
|
+ "current": part_id,
|
|
|
|
|
+ "completed": [],
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
part_images = {}
|
|
part_images = {}
|
|
|
for part in parts:
|
|
for part in parts:
|
|
|
pid = part["id"]
|
|
pid = part["id"]
|
|
@@ -590,6 +628,20 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
if ok:
|
|
if ok:
|
|
|
_append_job_log(job_id, f"✅ [{part_id}] 拆件质量通过:最大主体 {detail.get('largestShare', 0):.0%}")
|
|
_append_job_log(job_id, f"✅ [{part_id}] 拆件质量通过:最大主体 {detail.get('largestShare', 0):.0%}")
|
|
|
part_images[part_id] = img
|
|
part_images[part_id] = img
|
|
|
|
|
+ if row:
|
|
|
|
|
+ part_meta = _save_boss_part_image(chars_out, boss_id, part_id, img)
|
|
|
|
|
+ _upsert_part_meta(row, part_meta, preview_version)
|
|
|
|
|
+ _save_library(game, lib)
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": 1,
|
|
|
|
|
+ "done": 1,
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": [part_id],
|
|
|
|
|
+ "lastCompleted": part_id,
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
break
|
|
break
|
|
|
last_reason = reason
|
|
last_reason = reason
|
|
|
_append_job_log(job_id, f"⚠️ [{part_id}] 拆件质量失败:{reason}")
|
|
_append_job_log(job_id, f"⚠️ [{part_id}] 拆件质量失败:{reason}")
|
|
@@ -597,6 +649,15 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
"这不是干净的单个拆件。请只生成这一件,不要包含任何其他身体部位、碎片、武器边缘或相邻格内容。"
|
|
"这不是干净的单个拆件。请只生成这一件,不要包含任何其他身体部位、碎片、武器边缘或相邻格内容。"
|
|
|
)
|
|
)
|
|
|
if part_id not in part_images:
|
|
if part_id not in part_images:
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": 1,
|
|
|
|
|
+ "done": 0,
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": [],
|
|
|
|
|
+ "failed": [{"id": part_id, "reason": last_reason}],
|
|
|
|
|
+ })
|
|
|
raise RuntimeError(f"拆件重生失败:{last_reason}")
|
|
raise RuntimeError(f"拆件重生失败:{last_reason}")
|
|
|
|
|
|
|
|
spine_builder.build_parts_character(boss_id, part_images, chars_out, boss.get("animations", ["idle"]), parts,
|
|
spine_builder.build_parts_character(boss_id, part_images, chars_out, boss.get("animations", ["idle"]), parts,
|
|
@@ -709,8 +770,27 @@ def _run_retry_boss_parts_from_preview_job(job_id, game, boss_id, creds):
|
|
|
consistency = _part_consistency_prompt(row, row.get("previewAnalysis", ""))
|
|
consistency = _part_consistency_prompt(row, row.get("previewAnalysis", ""))
|
|
|
part_images = {}
|
|
part_images = {}
|
|
|
_append_job_log(job_id, f"按当前主图重生全部拆件:{boss_id},共 {len(parts)} 个")
|
|
_append_job_log(job_id, f"按当前主图重生全部拆件:{boss_id},共 {len(parts)} 个")
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": len(parts),
|
|
|
|
|
+ "done": 0,
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": [],
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
for idx, part in enumerate(parts, start=1):
|
|
for idx, part in enumerate(parts, start=1):
|
|
|
pid = part["id"]
|
|
pid = part["id"]
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": len(parts),
|
|
|
|
|
+ "done": len(part_images),
|
|
|
|
|
+ "current": pid,
|
|
|
|
|
+ "completed": list(part_images.keys()),
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
|
|
+ _append_job_log(job_id, f"🎨 [{idx}/{len(parts)}] 正在生成拆件:{pid}")
|
|
|
base_prompt = ", ".join(x for x in [
|
|
base_prompt = ", ".join(x for x in [
|
|
|
part.get("prompt", ""),
|
|
part.get("prompt", ""),
|
|
|
style,
|
|
style,
|
|
@@ -739,11 +819,35 @@ def _run_retry_boss_parts_from_preview_job(job_id, game, boss_id, creds):
|
|
|
if ok:
|
|
if ok:
|
|
|
_append_job_log(job_id, f"✅ [{idx}/{len(parts)}] {pid} 通过:最大主体 {detail.get('largestShare', 0):.0%}")
|
|
_append_job_log(job_id, f"✅ [{idx}/{len(parts)}] {pid} 通过:最大主体 {detail.get('largestShare', 0):.0%}")
|
|
|
part_images[pid] = img
|
|
part_images[pid] = img
|
|
|
|
|
+ part_meta = _save_boss_part_image(chars_out, boss_id, pid, img)
|
|
|
|
|
+ _upsert_part_meta(row, part_meta, preview_version)
|
|
|
|
|
+ row["partsSourcePreviewVersion"] = ""
|
|
|
|
|
+ row["partsStale"] = True
|
|
|
|
|
+ _save_library(game, lib)
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": len(parts),
|
|
|
|
|
+ "done": len(part_images),
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": list(part_images.keys()),
|
|
|
|
|
+ "lastCompleted": pid,
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
break
|
|
break
|
|
|
last_reason = reason
|
|
last_reason = reason
|
|
|
_append_job_log(job_id, f"⚠️ [{pid}] 拆件质量失败:{reason}")
|
|
_append_job_log(job_id, f"⚠️ [{pid}] 拆件质量失败:{reason}")
|
|
|
correction = "请严格参考主图,只输出这一件拆件,不要完整角色,不要其他部件,不要相邻碎片。"
|
|
correction = "请严格参考主图,只输出这一件拆件,不要完整角色,不要其他部件,不要相邻碎片。"
|
|
|
if pid not in part_images:
|
|
if pid not in part_images:
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": len(parts),
|
|
|
|
|
+ "done": len(part_images),
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": list(part_images.keys()),
|
|
|
|
|
+ "failed": [{"id": pid, "reason": last_reason}],
|
|
|
|
|
+ })
|
|
|
raise RuntimeError(f"{pid} 重生失败:{last_reason}")
|
|
raise RuntimeError(f"{pid} 重生失败:{last_reason}")
|
|
|
|
|
|
|
|
spine_builder.build_parts_character(boss_id, part_images, chars_out, boss.get("animations", ["idle"]), parts,
|
|
spine_builder.build_parts_character(boss_id, part_images, chars_out, boss.get("animations", ["idle"]), parts,
|
|
@@ -758,6 +862,16 @@ def _run_retry_boss_parts_from_preview_job(job_id, game, boss_id, creds):
|
|
|
files.extend([p["file"] for p in part_files])
|
|
files.extend([p["file"] for p in part_files])
|
|
|
row["files"] = files
|
|
row["files"] = files
|
|
|
_save_library(game, lib)
|
|
_save_library(game, lib)
|
|
|
|
|
+ _set_job(job_id, progress={
|
|
|
|
|
+ "type": "boss_parts",
|
|
|
|
|
+ "bossId": boss_id,
|
|
|
|
|
+ "total": len(parts),
|
|
|
|
|
+ "done": len(parts),
|
|
|
|
|
+ "current": "",
|
|
|
|
|
+ "completed": [p["id"] for p in part_files],
|
|
|
|
|
+ "lastCompleted": "",
|
|
|
|
|
+ "failed": [],
|
|
|
|
|
+ })
|
|
|
_set_job(job_id, status="done", ok=True, game=game, updatedAt=time.time())
|
|
_set_job(job_id, status="done", ok=True, game=game, updatedAt=time.time())
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
traceback.print_exc()
|
|
traceback.print_exc()
|