Prechádzať zdrojové kódy

Retry slow image generation for boss parts

bang 2 týždňov pred
rodič
commit
d904f5b167
3 zmenil súbory, kde vykonal 48 pridanie a 52 odobranie
  1. 2 1
      pipeline.py
  2. 10 4
      providers.py
  3. 36 47
      server.py

+ 2 - 1
pipeline.py

@@ -92,7 +92,8 @@ def run(manifest, out_root, creds=None, log=print, merge_existing=False):
         img = providers.generate(creds["provider"], prompt, creds["api_key"],
                                  creds.get("base_url", "https://api.openai.com/v1"),
                                  creds.get("model", "gpt-image-2"),
-                                 size)
+                                 size,
+                                 timeout=int(creds.get("timeout") or 600))
         log_alpha(label, img, require_alpha)
         if not require_alpha or has_alpha(img):
             return img

+ 10 - 4
providers.py

@@ -9,6 +9,7 @@ import http.client
 import io
 import json
 import os
+import socket
 import ssl
 import urllib.request
 from urllib.parse import urlparse
@@ -152,7 +153,7 @@ def _extract_image_item(data):
 
 
 def gen_image_openai(prompt, api_key, base_url="https://api.openai.com/v1",
-                     model="gpt-image-2", size="1024x1024", timeout=180):
+                     model="gpt-image-2", size="1024x1024", timeout=600):
     """调用 OpenAI 兼容图像接口(含第三方中转),返回 PIL.Image (RGBA)。
 
     兼容性处理:
@@ -206,6 +207,8 @@ def gen_image_openai(prompt, api_key, base_url="https://api.openai.com/v1",
                                f"(model={model}, url={url})")
     except urllib.error.URLError as e:
         raise RuntimeError(f"连不上图像接口:{e}\n(检查 Base URL / 网络 / 证书;url={url})")
+    except (TimeoutError, socket.timeout) as e:
+        raise RuntimeError(f"图像接口读取超时:{e}\n(model={model}, url={url}, timeout={timeout}s)")
 
     item = _extract_image_item(data)
     source_url = None
@@ -217,7 +220,10 @@ def gen_image_openai(prompt, api_key, base_url="https://api.openai.com/v1",
         raw = base64.b64decode(item["image"])
     elif item.get("url"):
         source_url = item["url"]
-        raw = _http_bytes(source_url, timeout)
+        try:
+            raw = _http_bytes(source_url, timeout)
+        except (TimeoutError, socket.timeout) as e:
+            raise RuntimeError(f"下载生成图超时:{e}\n(source={source_url}, timeout={timeout}s)")
     else:
         raise RuntimeError(f"返回里既无 b64_json/base64/image 也无 url:{_short_json(data)}")
     img = Image.open(io.BytesIO(raw)).convert("RGBA")
@@ -228,11 +234,11 @@ def gen_image_openai(prompt, api_key, base_url="https://api.openai.com/v1",
 PROVIDERS = {"OpenAI 兼容接口": gen_image_openai}
 
 
-def generate(provider, prompt, api_key, base_url, model, size):
+def generate(provider, prompt, api_key, base_url, model, size, timeout=600):
     fn = PROVIDERS.get(provider)
     if fn is None:
         raise ValueError(f"未知 provider: {provider}")
-    return fn(prompt, api_key=api_key, base_url=base_url, model=model, size=size)
+    return fn(prompt, api_key=api_key, base_url=base_url, model=model, size=size, timeout=timeout)
 
 
 TEXT_MODEL_FALLBACKS = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5"]

+ 36 - 47
server.py

@@ -50,6 +50,7 @@ DEFAULT_BASE_URL = config.get("ANIM_STUDIO_BASE_URL", "https://x.long.bid/v1")
 DEFAULT_API_KEY = config.get("ANIM_STUDIO_API_KEY", "")
 DEFAULT_IMAGE_MODEL = config.get("ANIM_STUDIO_IMAGE_MODEL", "gpt-image-2")
 DEFAULT_TEXT_MODEL = config.get("ANIM_STUDIO_TEXT_MODEL", "gpt-5.4-mini")
+DEFAULT_IMAGE_TIMEOUT = int(config.get("ANIM_STUDIO_IMAGE_TIMEOUT", "600") or "600")
 JOBS = {}
 JOBS_LOCK = threading.Lock()
 
@@ -472,7 +473,8 @@ def _has_alpha(img):
 def _generate_alpha_image(creds, prompt, size, label, log):
     img = providers.generate(creds["provider"], prompt, creds["api_key"],
                              creds.get("base_url", "https://api.openai.com/v1"),
-                             creds.get("model", "gpt-image-2"), size)
+                             creds.get("model", "gpt-image-2"), size,
+                             timeout=int(creds.get("timeout") or DEFAULT_IMAGE_TIMEOUT))
     if _has_alpha(img):
         return img
     log(f"🧠 [{label}] 模型没有真实 Alpha,改用百度智能抠图兜底…")
@@ -482,6 +484,17 @@ def _generate_alpha_image(creds, prompt, size, label, log):
     raise RuntimeError("图片没有真实 Alpha,抠图后仍不合格")
 
 
+def _creds_from_request(data):
+    return {
+        "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"),
+        "timeout": int(data.get("timeout") or DEFAULT_IMAGE_TIMEOUT),
+    }
+
+
 def _new_asset_version():
     return f"v{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
 
@@ -565,8 +578,14 @@ def _run_retry_boss_part_job(job_id, game, boss_id, part_id, creds):
             if attempt > 1:
                 _append_job_log(job_id, f"🔁 [{part_id}] 拆件不合格,重新生成(第 {attempt}/3 次)…")
             prompt = base_prompt if not correction else ", ".join([base_prompt, correction])
-            img = _generate_alpha_image(creds, prompt, target.get("size", boss.get("size", creds.get("size", "1024x1024"))),
-                                        f"{boss_id}/{part_id}", lambda m: _append_job_log(job_id, m))
+            try:
+                img = _generate_alpha_image(creds, prompt, target.get("size", boss.get("size", creds.get("size", "1024x1024"))),
+                                            f"{boss_id}/{part_id}", lambda m: _append_job_log(job_id, m))
+            except Exception as e:
+                last_reason = str(e)
+                _append_job_log(job_id, f"⚠️ [{part_id}] 图像接口失败/超时:{last_reason}")
+                correction = "接口刚才失败或超时。请继续只输出这一件干净拆件,不要完整角色,不要其他部件。"
+                continue
             ok, reason, detail = asset_quality.boss_part_quality(part_id, img)
             if ok:
                 _append_job_log(job_id, f"✅ [{part_id}] 拆件质量通过:最大主体 {detail.get('largestShare', 0):.0%}")
@@ -708,8 +727,14 @@ def _run_retry_boss_parts_from_preview_job(job_id, game, boss_id, creds):
                 if attempt > 1:
                     _append_job_log(job_id, f"🔁 [{pid}] 拆件不合格,重新生成(第 {attempt}/3 次)…")
                 prompt = base_prompt if not correction else ", ".join([base_prompt, correction])
-                img = _generate_alpha_image(creds, prompt, part.get("size", boss.get("size", creds.get("size", "1024x1024"))),
-                                            f"{boss_id}/{pid}", lambda m: _append_job_log(job_id, m))
+                try:
+                    img = _generate_alpha_image(creds, prompt, part.get("size", boss.get("size", creds.get("size", "1024x1024"))),
+                                                f"{boss_id}/{pid}", lambda m: _append_job_log(job_id, m))
+                except Exception as e:
+                    last_reason = str(e)
+                    _append_job_log(job_id, f"⚠️ [{pid}] 图像接口失败/超时:{last_reason}")
+                    correction = "接口刚才失败或超时。请继续只输出这一件干净拆件,不要完整角色,不要其他部件。"
+                    continue
                 ok, reason, detail = asset_quality.boss_part_quality(pid, img)
                 if ok:
                     _append_job_log(job_id, f"✅ [{idx}/{len(parts)}] {pid} 通过:最大主体 {detail.get('largestShare', 0):.0%}")
@@ -1082,13 +1107,7 @@ class Handler(BaseHTTPRequestHandler):
             manifest = slot_workflow.complete_manifest(manifest)
         except Exception as e:
             return self._send(400, {"ok": False, "error": f"manifest 非法 JSON: {e}"})
-        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"),
-        }
+        creds = _creds_from_request(data)
         logs = []
         if data.get("async", True):
             job_id = uuid.uuid4().hex
@@ -1194,13 +1213,7 @@ class Handler(BaseHTTPRequestHandler):
         bad = [x for x in task_ids if x not in valid_ids]
         if bad:
             return self._send(400, {"ok": False, "error": f"任务不存在: {', '.join(bad)}"})
-        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"),
-        }
+        creds = _creds_from_request(data)
         job_id = uuid.uuid4().hex
         with JOBS_LOCK:
             JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["补生成任务已创建,等待开始…"],
@@ -1220,13 +1233,7 @@ class Handler(BaseHTTPRequestHandler):
             return self._send(400, {"ok": False, "error": f"无效的 game: {game!r}"})
         if not boss_id or not part_id:
             return self._send(400, {"ok": False, "error": "缺少 bossId 或 partId"})
-        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"),
-        }
+        creds = _creds_from_request(data)
         job_id = uuid.uuid4().hex
         with JOBS_LOCK:
             JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["拆件重生任务已创建,等待开始…"],
@@ -1246,13 +1253,7 @@ class Handler(BaseHTTPRequestHandler):
             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"),
-        }
+        creds = _creds_from_request(data)
         job_id = uuid.uuid4().hex
         with JOBS_LOCK:
             JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["主图重生任务已创建,等待开始…"],
@@ -1272,13 +1273,7 @@ class Handler(BaseHTTPRequestHandler):
             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"),
-        }
+        creds = _creds_from_request(data)
         job_id = uuid.uuid4().hex
         with JOBS_LOCK:
             JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["按主图重生拆件任务已创建,等待开始…"],
@@ -1303,13 +1298,7 @@ class Handler(BaseHTTPRequestHandler):
         groups = {k: v for k, v in groups.items() if v}
         if not groups:
             return self._send(400, {"ok": False, "error": "当前资源库没有缺失任务"})
-        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"),
-        }
+        creds = _creds_from_request(data)
         job_id = uuid.uuid4().hex
         with JOBS_LOCK:
             JOBS[job_id] = {"status": "queued", "ok": None, "logs": ["批量补生成任务已创建,等待开始…"],