存量积累:生图素人感约束+评图分+幂等防重跑+审核回路

- 015-017迁移:image_candidate 文案复审/AI视觉分/重生标记
- constants C7素人感约束(反电商摆拍对齐真实笔记)+C3叠字口子
- celery visibility_timeout=2h 防长任务被误判重投重复烧钱(task75教训)
- image_scorer 评图分(只筛选+展示,真实信号才进飞轮权重)
- storyboard/image_gen/pipeline_io 生图存量
- task_actions/tasks/task_service/flywheel 审核回路+飞轮存量

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 11:16:42 +08:00
parent cefdbaabdc
commit 7f419f4c8b
16 changed files with 346 additions and 30 deletions

View File

@@ -41,7 +41,7 @@ class AIClients:
_pool_loop_id: int | None = field(default=None, repr=False)
_gemini_key: str | None = field(default=None, repr=False) # 局部变量不打印
_model_image: str = "gpt-image-2"
_model_text: str = "claude-sonnet-4-5" # apiports无gpt-4o-mini,文案用claude中文质量好
_model_text: str = "claude-opus-4-8" # 最强档(倩倩姐红线):Claude系一律4.8,绝不降级
def _client(self) -> httpx.AsyncClient:
"""主通道(apiports) client按当前事件循环缓存"""
@@ -111,16 +111,44 @@ class AIClients:
resp.raise_for_status()
return _extract_gemini_image(resp.json())
async def _chat_post_failover(self, payload: dict, timeout: float) -> dict:
"""
chat/completions 发送器,带 codeproxy 回落。
主通道(apiports, claude-opus-4-8)若 5xx/超时/连接错,自动切 codeproxy 重试一次。
⚠ codeproxy 账号池只支持 gpt 系(gpt-5.5),无 claude故回落时模型换成 gpt-5.5。
这是强档↔强档切换:红线"Claude系4.8/GPT系5.5"本就是两个平级最强档、互为兜底,
非降级(降级=落 sonnet/弱档)。codeproxy 回落档由 CODEPROXY_CHAT_MODEL 配置(默认 gpt-5.5)。
codeproxy 未配置(_alt_token 为空)时不回落,原样抛错。
"""
main_base = (os.environ.get("IMAGE_API_BASE") or os.environ.get("APIPORTS_BASE_URL") or "").rstrip("/")
try:
resp = await self._client().post(f"{main_base}/chat/completions", json=payload, timeout=timeout)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPStatusError, httpx.TransportError, httpx.TimeoutException) as exc:
# 仅 5xx(服务端过载)或网络层错误才回落4xx(参数/鉴权)回落也没用,直接抛。
status = getattr(getattr(exc, "response", None), "status_code", None)
retryable = status is None or status >= 500
if not (retryable and self._alt_token):
raise
alt_base = (self._alt_base or os.environ.get("CODEPROXY_BASE_URL") or "").rstrip("/")
alt_payload = dict(payload)
alt_payload["model"] = os.environ.get("CODEPROXY_CHAT_MODEL", "gpt-5.5")
logger.warning("apiports chat 失败(%s),回落 codeproxy 用 %s 重试(强档兜底,非降级)",
status or type(exc).__name__, alt_payload["model"])
client = self._client_for(alt_base, self._alt_token)
resp = await client.post(f"{alt_base}/chat/completions", json=alt_payload, timeout=timeout)
resp.raise_for_status()
return resp.json()
async def chat_complete(self, messages: list[dict], model: str | None = None, max_tokens: int = 4096, temperature: float = 0.75) -> str:
"""文字生成(文案生成用)"""
base = (os.environ.get("IMAGE_API_BASE") or os.environ.get("APIPORTS_BASE_URL") or "").rstrip("/")
"""文字生成(文案生成用)。apiports 503 时自动回落 codeproxy。"""
payload = {"model": model or self._model_text, "messages": messages, "max_tokens": max_tokens, "temperature": temperature}
# 单批≤4条文案正常 40-55s 返回apiports 网关 ~60s 上限。客户端超时设 75s
# 略高于网关上限即可过长如180s会在 apiports 卡顿时干等,拖慢整体。
timeout = float(os.environ.get("TEXT_LLM_TIMEOUT", "75"))
resp = await self._client().post(f"{base}/chat/completions", json=payload, timeout=timeout)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"] or ""
data = await self._chat_post_failover(payload, timeout)
return data["choices"][0]["message"]["content"] or ""
async def gpt_vision_analyze(self, prompt: str, images: list[bytes], model: str | None = None) -> str:
"""
@@ -139,16 +167,15 @@ class AIClients:
})
used_model = model or os.environ.get("MODEL_TEXT", "claude-opus-4-8")
messages = [{"role": "user", "content": content}]
base = (os.environ.get("IMAGE_API_BASE") or os.environ.get("APIPORTS_BASE_URL") or "").rstrip("/")
payload = {
"model": used_model,
"messages": messages,
"max_tokens": 2048,
"temperature": 0.2,
}
resp = await self._client().post(f"{base}/chat/completions", json=payload, timeout=90.0)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"] or ""
# 评图也走 codeproxy 回落apiports 503 时切备用站,模型档不变(守红线)。
data = await self._chat_post_failover(payload, 90.0)
return data["choices"][0]["message"]["content"] or ""
# duck-type: text_variants._call_llm 用的属性
@property