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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user