|
@@ -499,10 +499,12 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
|
|
|
|
|
style = manifest.get("style", "")
|
|
style = manifest.get("style", "")
|
|
|
base_prompt = ", ".join(x for x in [
|
|
base_prompt = ", ".join(x for x in [
|
|
|
- boss.get("prompt", ""),
|
|
|
|
|
target.get("prompt", ""),
|
|
target.get("prompt", ""),
|
|
|
style,
|
|
style,
|
|
|
- _transparent_prompt("single separated rigging part only, centered, no text, no other body parts"),
|
|
|
|
|
|
|
+ _transparent_prompt(
|
|
|
|
|
+ f"only the {part_id} rigging part from the boss character, isolated single part, "
|
|
|
|
|
+ "not a full character, no complete body, no other body parts, centered"
|
|
|
|
|
+ ),
|
|
|
] if x)
|
|
] if x)
|
|
|
correction = ""
|
|
correction = ""
|
|
|
last_reason = ""
|
|
last_reason = ""
|
|
@@ -526,7 +528,7 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
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,
|
|
|
- write_preview=True)
|
|
|
|
|
|
|
+ write_preview=False)
|
|
|
part_files = spine_builder.write_part_pngs_from_files(boss_id, chars_out)
|
|
part_files = spine_builder.write_part_pngs_from_files(boss_id, chars_out)
|
|
|
row = next((c for c in lib.get("characters", []) if c.get("id") == boss_id), None)
|
|
row = next((c for c in lib.get("characters", []) if c.get("id") == boss_id), None)
|
|
|
if row:
|
|
if row:
|
|
@@ -544,6 +546,64 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
|
|
|
_set_job(job_id, status="error", ok=False, error=str(e), updatedAt=time.time())
|
|
_set_job(job_id, status="error", ok=False, error=str(e), updatedAt=time.time())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _run_retry_boss_preview_job(job_id, game, boss_id, creds):
|
|
|
|
|
+ _set_job(job_id, status="running", logs=[], game=game, startedAt=time.time(), updatedAt=time.time())
|
|
|
|
|
+ try:
|
|
|
|
|
+ lib = _load_library(game)
|
|
|
|
|
+ manifest = _manifest_from_library(lib)
|
|
|
|
|
+ base = os.path.join(OUT_ROOT, game)
|
|
|
|
|
+ chars_out = os.path.join(base, "characters")
|
|
|
|
|
+ boss = next((c for c in manifest.get("characters", [])
|
|
|
|
|
+ if c.get("id") == boss_id or c.get("role") == "boss"), None)
|
|
|
|
|
+ if not boss:
|
|
|
|
|
+ raise RuntimeError(f"找不到关主:{boss_id}")
|
|
|
|
|
+ style = manifest.get("style", "")
|
|
|
|
|
+ base_prompt = ", ".join(x for x in [
|
|
|
|
|
+ boss.get("prompt", ""),
|
|
|
|
|
+ style,
|
|
|
|
|
+ _transparent_prompt(
|
|
|
|
|
+ "single complete assembled boss character preview, full body, centered, readable silhouette, "
|
|
|
|
|
+ "one complete character only, no separated parts, no sprite sheet, no atlas, no cropped body"
|
|
|
|
|
+ ),
|
|
|
|
|
+ ] if x)
|
|
|
|
|
+ correction = ""
|
|
|
|
|
+ last_reason = ""
|
|
|
|
|
+ _append_job_log(job_id, f"重生关主完整预览:{boss_id}")
|
|
|
|
|
+ for attempt in range(1, 4):
|
|
|
|
|
+ if attempt > 1:
|
|
|
|
|
+ _append_job_log(job_id, f"🔁 [{boss_id}/preview] 主图不合格,重新生成(第 {attempt}/3 次)…")
|
|
|
|
|
+ prompt = base_prompt if not correction else ", ".join([base_prompt, correction])
|
|
|
|
|
+ img = _generate_alpha_image(creds, prompt, boss.get("size", creds.get("size", "1024x1024")),
|
|
|
|
|
+ f"{boss_id}/preview", lambda m: _append_job_log(job_id, m))
|
|
|
|
|
+ ok, reason, detail = asset_quality.boss_preview_quality(img)
|
|
|
|
|
+ if ok:
|
|
|
|
|
+ os.makedirs(chars_out, exist_ok=True)
|
|
|
|
|
+ spine_builder.trim_to_content(img, pad=16).save(os.path.join(chars_out, f"{boss_id}_preview.png"))
|
|
|
|
|
+ _append_job_log(job_id, f"✅ [{boss_id}/preview] 主图质量通过:最大主体 {detail.get('largestShare', 0):.0%}")
|
|
|
|
|
+ break
|
|
|
|
|
+ last_reason = reason
|
|
|
|
|
+ _append_job_log(job_id, f"⚠️ [{boss_id}/preview] 主图质量失败:{reason}")
|
|
|
|
|
+ correction = (
|
|
|
|
|
+ "这不是完整主图。请只生成一个完整、站立、全身、主体连贯的关主角色;"
|
|
|
|
|
+ "不要拆件、不要把部件分开、不要 atlas、不要只画头或半身。"
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise RuntimeError(f"主图重生失败:{last_reason}")
|
|
|
|
|
+
|
|
|
|
|
+ row = next((c for c in lib.get("characters", []) if c.get("id") == boss_id), None)
|
|
|
|
|
+ if row:
|
|
|
|
|
+ row["preview"] = f"characters/{boss_id}_preview.png"
|
|
|
|
|
+ files = row.setdefault("files", [])
|
|
|
|
|
+ if row["preview"] not in files:
|
|
|
|
|
+ files.append(row["preview"])
|
|
|
|
|
+ _save_library(game, lib)
|
|
|
|
|
+ _set_job(job_id, status="done", ok=True, game=game, updatedAt=time.time())
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ traceback.print_exc()
|
|
|
|
|
+ _append_job_log(job_id, f"❌ 主图重生失败: {e}")
|
|
|
|
|
+ _set_job(job_id, status="error", ok=False, error=str(e), updatedAt=time.time())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _split_lines(value):
|
|
def _split_lines(value):
|
|
|
if isinstance(value, list):
|
|
if isinstance(value, list):
|
|
|
return [str(x).strip() for x in value if str(x).strip()]
|
|
return [str(x).strip() for x in value if str(x).strip()]
|
|
@@ -862,6 +922,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
return self._post_retry_task()
|
|
return self._post_retry_task()
|
|
|
if route == "/api/retry-boss-part":
|
|
if route == "/api/retry-boss-part":
|
|
|
return self._post_retry_boss_part()
|
|
return self._post_retry_boss_part()
|
|
|
|
|
+ if route == "/api/retry-boss-preview":
|
|
|
|
|
+ return self._post_retry_boss_preview()
|
|
|
if route == "/api/retry-missing":
|
|
if route == "/api/retry-missing":
|
|
|
return self._post_retry_missing()
|
|
return self._post_retry_missing()
|
|
|
if route == "/api/delete":
|
|
if route == "/api/delete":
|
|
@@ -1035,6 +1097,32 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
args=(job_id, game, boss_id, part_id, creds), daemon=True).start()
|
|
args=(job_id, game, boss_id, part_id, creds), daemon=True).start()
|
|
|
return self._send(200, {"ok": True, "jobId": job_id, "game": game})
|
|
return self._send(200, {"ok": True, "jobId": job_id, "game": game})
|
|
|
|
|
|
|
|
|
|
+ def _post_retry_boss_preview(self):
|
|
|
|
|
+ try:
|
|
|
|
|
+ data = self._read_json_body()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return self._send(400, {"ok": False, "error": f"请求体不是合法 JSON: {e}"})
|
|
|
|
|
+ game = (data.get("game") or "").strip()
|
|
|
|
|
+ boss_id = (data.get("bossId") or data.get("id") or "").strip()
|
|
|
|
|
+ if not game or game not in list_games():
|
|
|
|
|
+ return self._send(400, {"ok": False, "error": f"无效的 game: {game!r}"})
|
|
|
|
|
+ if not boss_id:
|
|
|
|
|
+ return self._send(400, {"ok": False, "error": "缺少 bossId"})
|
|
|
|
|
+ creds = {
|
|
|
|
|
+ "provider": data.get("provider", "OpenAI 兼容接口"),
|
|
|
|
|
+ "api_key": (data.get("api_key") or DEFAULT_API_KEY).strip(),
|
|
|
|
|
+ "base_url": (data.get("base_url") or DEFAULT_BASE_URL).strip(),
|
|
|
|
|
+ "model": (data.get("model") or DEFAULT_IMAGE_MODEL).strip(),
|
|
|
|
|
+ "size": data.get("size", "1024x1024"),
|
|
|
|
|
+ }
|
|
|
|
|
+ job_id = uuid.uuid4().hex
|
|
|
|
|
+ with JOBS_LOCK:
|
|
|
|
|
+ JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["主图重生任务已创建,等待开始…"],
|
|
|
|
|
+ "game": game, "createdAt": time.time(), "updatedAt": time.time()}
|
|
|
|
|
+ threading.Thread(target=_run_retry_boss_preview_job,
|
|
|
|
|
+ args=(job_id, game, boss_id, creds), daemon=True).start()
|
|
|
|
|
+ return self._send(200, {"ok": True, "jobId": job_id, "game": game})
|
|
|
|
|
+
|
|
|
def _post_retry_missing(self):
|
|
def _post_retry_missing(self):
|
|
|
try:
|
|
try:
|
|
|
data = self._read_json_body()
|
|
data = self._read_json_body()
|