fix: 修复资产/视频/三视图三处 bug + 脚本模型切 YunQi gemini

1. 视频「全部生成」串行短路:任一段提交失败就 break 致后面段不提交
   (4 个框只跑 2 个)→ 改 Promise.allSettled 并发,单段失败不拖累其余,
   部分失败也刷新并如实报告。(App.tsx)
2. 基础资产/三视图 AI 出图偶发失败「生成不出来」:中转站瞬时抖动
   (网络/5xx/429/空返回)一次失败即退费 → 加退避重试 3 次,永久错误
   (4xx 内容违规)仍立即退费。(services.py run_base_asset_task/run_triview_task)
3. 商品三视图无法用已有素材:商品卡加「使用已有素材」,选商品已有图 /
   上传本地图直接 attach 成三视图(不走平台 AI 生成、不计费)。(pipeline.tsx)
4. 脚本助手:tokenssr 欠费致 GPT-5.5/Gemini 全挂 → 文本模型切 YunQi。
   实测 YunQi gpt-5.5 频道扛不住重型结构化任务(skill 全提示词仅 1/5 成功、
   常吐空无 JSON),gemini-3.1-pro 5/5 稳定 → 默认改 gemini,停用 gpt-5.5。
   按「一 provider 一 key」拆 yunqi_gpt / yunqi_gemini 两 provider。
   (.env / settings / 迁移 0017+0018)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-25 12:00:03 +08:00
co-authored by Claude Opus 4.8
parent 8222a46202
commit dea9dbb3ed
7 changed files with 274 additions and 20 deletions
+57 -14
View File
@@ -2,11 +2,14 @@ import json
import re
import subprocess
import tempfile
import time
import uuid
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from pathlib import Path
import requests
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
@@ -865,6 +868,38 @@ def build_person_frontal_prompt(description: str = "") -> str:
return render_prompt("person_portrait", default, 描述=desc)
# --------------------------------------------------------------------------- #
# 出图重试(中转站偶发抖动 → 一次失败就「生成不出来」的根因兜底)
# --------------------------------------------------------------------------- #
_IMAGE_GEN_ATTEMPTS = 3
def _is_transient_image_error(exc: Exception) -> bool:
"""判定出图失败是否「瞬时」(值得重试)。瞬时:网络抖动 / 5xx / 429 限流 / 中转站吐空无 media url。
永久(不重试,立即退费报错):4xx 内容违规 / 参数错(如 invalid_image_file)、配置缺失等。"""
if isinstance(exc, (requests.ConnectionError, requests.Timeout)):
return True
if isinstance(exc, requests.HTTPError):
code = getattr(getattr(exc, "response", None), "status_code", 0) or 0
return code >= 500 or code == 429
# extract_first_media_url 在响应没有 url 时抛 ValueError —— 多为中转站偶发空返回,重试常能拿到
if isinstance(exc, ValueError) and "media url" in str(exc).lower():
return True
return False
def _run_image_with_retry(make, *, attempts: int = _IMAGE_GEN_ATTEMPTS):
"""跑一次出图(provider 调用 + 取 media url),瞬时错误按退避重试,永久错误立即抛。
make() 须返回 (response, media)。退避 2s / 4s,总耗时上限 ~6s + 出图本身,worker 内执行对用户无感。"""
for i in range(attempts):
try:
return make()
except Exception as exc: # noqa: BLE001
if i == attempts - 1 or not _is_transient_image_error(exc):
raise
time.sleep(2 * (i + 1))
def generate_base_asset(*, project, user, kind: str, prompt: str, label: str = "", group_id: str | None = None) -> AITask:
"""提交基础资产生成(**异步**):Web 请求只建 RESERVED 任务 + 预留额度(秒级),
慢出图(文生图 / 商品 image_edit)交给 Celery worker(run_base_asset_task)跑。
@@ -927,18 +962,22 @@ def run_base_asset_task(*, task_id: str) -> None:
provider = get_image_provider(model_config)
reservation = task.credit_reservation
try:
if use_edit and ref_url:
# 商品三视图:比例可在 admin 改(默认横)
size = prompt_ratio_size("product_triview", "1536x1024")
response = provider.image_edit(model=model_config.name, prompt=prompt, images=[ref_url], size=size)
else:
# 场景默认横、人物立绘默认竖;两者比例都可在 admin 改
if kind == BaseAssetGroup.Kind.SCENE:
gen_size = prompt_ratio_size("scene", "1536x1024")
def _make():
if use_edit and ref_url:
# 商品三视图:比例可在 admin 改(默认横)
size = prompt_ratio_size("product_triview", "1536x1024")
resp = provider.image_edit(model=model_config.name, prompt=prompt, images=[ref_url], size=size)
else:
gen_size = prompt_ratio_size("person_portrait", "1024x1536")
response = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=prompt, size=gen_size)
media = provider.extract_first_media_url(response)
# 场景默认横、人物立绘默认竖;两者比例都可在 admin 改
if kind == BaseAssetGroup.Kind.SCENE:
gen_size = prompt_ratio_size("scene", "1536x1024")
else:
gen_size = prompt_ratio_size("person_portrait", "1024x1536")
resp = provider.image_generation(model=model_config.name, endpoint=model_config.endpoint, prompt=prompt, size=gen_size)
return resp, provider.extract_first_media_url(resp)
# 中转站偶发抖动(网络/5xx/限流/空返回)按退避重试,避免「一次失败就生成不出来」;永久错误仍立即退费
response, media = _run_image_with_retry(_make)
with transaction.atomic():
task.status = AITask.Status.SUCCEEDED
task.response_payload = response
@@ -1027,9 +1066,13 @@ def run_triview_task(*, task_id: str) -> None:
provider = get_image_provider(model_config)
reservation = task.credit_reservation
try:
tri_size = prompt_ratio_size("person_triview", "1536x1024")
response = provider.image_edit(model=model_config.name, prompt=prompt, images=[ref_url], size=tri_size)
media = provider.extract_first_media_url(response)
def _make():
tri_size = prompt_ratio_size("person_triview", "1536x1024")
resp = provider.image_edit(model=model_config.name, prompt=prompt, images=[ref_url], size=tri_size)
return resp, provider.extract_first_media_url(resp)
# 同基础资产:瞬时抖动重试,永久错误立即退费报错
response, media = _run_image_with_retry(_make)
with transaction.atomic():
task.status = AITask.Status.SUCCEEDED
task.response_payload = response