Просмотр исходного кода

Fix text model routing for creative workflow

bang 2 недель назад
Родитель
Сommit
1845429133
4 измененных файлов с 50 добавлено и 14 удалено
  1. 1 1
      local_config.example.json
  2. 47 11
      providers.py
  3. 1 1
      server.py
  4. 1 1
      web/index.html

+ 1 - 1
local_config.example.json

@@ -2,6 +2,6 @@
   "ANIM_STUDIO_BASE_URL": "https://x.long.bid/v1",
   "ANIM_STUDIO_API_KEY": "replace-with-your-key",
   "ANIM_STUDIO_IMAGE_MODEL": "gpt-image-2",
-  "ANIM_STUDIO_TEXT_MODEL": "gpt-4o-mini",
+  "ANIM_STUDIO_TEXT_MODEL": "gpt-5.4-mini",
   "ALIYUN_BGREM_APP_CODE": "replace-with-your-app-code"
 }

+ 47 - 11
providers.py

@@ -79,6 +79,18 @@ def _http_bytes(url, timeout):
     return body
 
 
+def _url_image_to_data_url(url, timeout, max_bytes=8 * 1024 * 1024):
+    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
+    with urllib.request.urlopen(req, timeout=timeout, context=_SSL) as res:
+        ctype = (res.headers.get("content-type") or "image/png").split(";")[0].strip()
+        raw = res.read(max_bytes + 1)
+    if len(raw) > max_bytes:
+        raise RuntimeError("image is larger than 8MB")
+    if not ctype.startswith("image/"):
+        ctype = "image/png"
+    return f"data:{ctype};base64," + base64.b64encode(raw).decode("ascii")
+
+
 def _short_json(data, limit=600):
     try:
         return json.dumps(data, ensure_ascii=False)[:limit]
@@ -233,10 +245,10 @@ def generate(provider, prompt, api_key, base_url, model, size):
     return fn(prompt, api_key=api_key, base_url=base_url, model=model, size=size)
 
 
-def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
-                     model="gpt-4o-mini", timeout=120):
-    url = base_url.rstrip("/") + "/chat/completions"
-    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
+TEXT_MODEL_FALLBACKS = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5"]
+
+
+def _chat_json_once(url, headers, messages, model, timeout):
     payload = {
         "model": model,
         "messages": messages,
@@ -246,28 +258,46 @@ def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
     try:
         data = _http_json(url, payload, headers, timeout)
     except urllib.error.HTTPError as e:
-        body = _read_http_error(e)
         fallback = dict(payload)
         fallback.pop("response_format", None)
         try:
             data = _http_json(url, fallback, headers, timeout)
         except urllib.error.HTTPError as e2:
-            body2 = _read_http_error(e2)
-            raise RuntimeError(f"文字模型 HTTP {e2.code}:{body2[:600]}\n"
-                               f"(model={model}, url={url};已移除 response_format 重试)")
+            raise e2
     content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip()
     if content.startswith("```"):
         content = content.strip("`")
         if content.lower().startswith("json"):
             content = content[4:].strip()
     try:
-        return json.loads(content)
+        obj = json.loads(content)
     except Exception as e:
         raise RuntimeError(f"文字模型没有返回合法 JSON:{e};内容={content[:800]}")
+    if isinstance(obj, dict):
+        obj.setdefault("_model_used", model)
+    return obj
+
+
+def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
+                     model="gpt-5.4-mini", timeout=120):
+    url = base_url.rstrip("/") + "/chat/completions"
+    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
+    candidates = [model] + [x for x in TEXT_MODEL_FALLBACKS if x != model]
+    errors = []
+    for candidate in candidates:
+        try:
+            return _chat_json_once(url, headers, messages, candidate, timeout)
+        except urllib.error.HTTPError as e:
+            body = _read_http_error(e)
+            errors.append(f"{candidate}: HTTP {e.code} {body[:180]}")
+            if e.code not in (429, 500, 502, 503, 504):
+                break
+    raise RuntimeError(f"文字模型不可用,已尝试 {', '.join(candidates)}:"
+                       f"{' | '.join(errors)}\n(url={url};已移除 response_format 重试)")
 
 
 def analyze_reference_images(reference_urls=None, image_data_urls=None, api_key="",
-                             base_url="https://api.openai.com/v1", model="gpt-4o-mini",
+                             base_url="https://api.openai.com/v1", model="gpt-5.4-mini",
                              timeout=120):
     """Use a vision-capable OpenAI-compatible chat model to summarize style refs.
 
@@ -288,7 +318,13 @@ def analyze_reference_images(reference_urls=None, image_data_urls=None, api_key=
         ),
     }]
     for url in reference_urls:
-        content.append({"type": "image_url", "image_url": {"url": url}})
+        try:
+            content.append({"type": "image_url", "image_url": {"url": _url_image_to_data_url(url, timeout=30)}})
+        except Exception as e:
+            content.append({
+                "type": "text",
+                "text": f"参考图 URL 本地下载失败,不能作为视觉输入,只能作为文字线索:{url};错误:{str(e)[:160]}",
+            })
     for data_url in image_data_urls:
         content.append({"type": "image_url", "image_url": {"url": data_url}})
     return chat_json_openai([

+ 1 - 1
server.py

@@ -42,7 +42,7 @@ PORT = 7861
 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-4o-mini")
+DEFAULT_TEXT_MODEL = config.get("ANIM_STUDIO_TEXT_MODEL", "gpt-5.4-mini")
 JOBS = {}
 JOBS_LOCK = threading.Lock()
 

+ 1 - 1
web/index.html

@@ -97,7 +97,7 @@
         <textarea id="creativeAvoidNotes" style="min-height:92px" placeholder="不要照抄 logo / IP / 角色;不要暗黑;不要复杂文字"></textarea></div>
       <div>
         <div class="field"><label>文字模型</label>
-          <input id="textModel" value="gpt-4o-mini"></div>
+          <input id="textModel" value="gpt-5.4-mini"></div>
         <button class="ghost" id="aiWorkflowBtn">AI 生成完整游戏方案</button>
         <div class="workflow-note" id="aiWorkflowMsg">先生成统一方案,再微调玩法和图片资源。</div>
       </div>