fix: retry transient xai video creation failures
This commit is contained in:
1
RULES.md
1
RULES.md
@@ -168,6 +168,7 @@
|
||||
- `AZURE_TTS_MODEL` / `AZURE_TTS_VOICE_ID` / `AZURE_TTS_VOICE_POOL` / `AZURE_TTS_PATH` / `AZURE_TTS_PATHS`:Azure OpenAI TTS 模型、默认音色、音色池和 OpenAI 协议语音路径;后端会按 `AZURE_TTS_PATHS` 依次尝试,便于区分路径不对和整条语音服务不可用
|
||||
- `POE_API_KEY` / `VIDEO_API_KEY`:默认视频生成通道 Key,只能放本地环境变量;如果显式配置了 `VIDEO_API_BASE_URL`,必须同时配置 `VIDEO_API_KEY` 才会在 `/health` 暴露该默认视频通道,不能用通用 `LLM_API_KEY` 冒充视频 key。
|
||||
- `XAI_VIDEO_API_BASE_URL` / `XAI_VIDEO_API_KEY` / `VIDEO_MODEL_XAI`:xAI / Grok Imagine Video 独立视频通道;默认 base 为 `https://ai.skg.com/ezlink/xai`,模型为 `grok-imagine-video`,真实 key 只放本地 `api/.env`、本地 Docker `deploy/.env.local` 或服务器 `deploy/.env.production`,不入库。未配置 `XAI_VIDEO_API_KEY` 时 `/health` 会标记 xAI 视频不可用,画布不显示该模型;已配置时即使默认 Doubao/Seedance 视频 key 为空,也可以独立显示和生成 Grok Imagine Video。
|
||||
- `VIDEO_CREATE_RETRY_ATTEMPTS` / `VIDEO_CREATE_RETRY_BACKOFF_SECONDS`:视频创建请求的瞬时错误重试配置,默认 Grok/xAI 创建阶段遇到连接重置、超时、429 或 5xx 时最多尝试 3 次,基础退避 2 秒;400/401/403/404 等参数或权限错误不重试,避免掩盖真实配置问题。
|
||||
- `PASSWORD_AUTH_ENABLED`:生产密码登录总开关;当前固定为 `false`,只允许飞书免登录。若应急恢复密码入口,必须显式改成 `true` 并重启 API。
|
||||
- `WEB_AUTH_USERNAME` / `WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET`:生产备用网页登录和会话签名配置;密码和 session secret 只放服务器环境变量,不入库。当前密码入口被 `PASSWORD_AUTH_ENABLED=false` 禁用;即使只开飞书免登录,也必须配置 `WEB_AUTH_SESSION_SECRET` 用于签名会话 Cookie。
|
||||
- `FEISHU_APP_ID` / `FEISHU_APP_SECRET`:飞书免登录 OAuth 应用凭证;只放服务器 `deploy/.env.production` 或本地 `api/.env`,不入库。
|
||||
|
||||
@@ -115,6 +115,8 @@ VIDEO_STATUS_PATH=/videos/{id}
|
||||
VIDEO_CONTENT_PATH=/videos/{id}/content
|
||||
VIDEO_DURATION_FIELD=seconds
|
||||
VIDEO_POLL_TIMEOUT_SECONDS=900
|
||||
VIDEO_CREATE_RETRY_ATTEMPTS=3
|
||||
VIDEO_CREATE_RETRY_BACKOFF_SECONDS=2
|
||||
|
||||
# 工作目录
|
||||
KEYFRAME_COUNT=12
|
||||
|
||||
77
api/main.py
77
api/main.py
@@ -451,6 +451,8 @@ VIDEO_STATUS_PATH = os.getenv("VIDEO_STATUS_PATH", DEFAULT_VIDEO_STATUS_PATH).st
|
||||
VIDEO_CONTENT_PATH = os.getenv("VIDEO_CONTENT_PATH", DEFAULT_VIDEO_CONTENT_PATH).strip() or DEFAULT_VIDEO_CONTENT_PATH
|
||||
VIDEO_DURATION_FIELD = os.getenv("VIDEO_DURATION_FIELD", "seconds").strip() or "seconds"
|
||||
VIDEO_POLL_TIMEOUT_SECONDS = max(60, int(os.getenv("VIDEO_POLL_TIMEOUT_SECONDS", "900")))
|
||||
VIDEO_CREATE_RETRY_ATTEMPTS = max(1, int(os.getenv("VIDEO_CREATE_RETRY_ATTEMPTS", "3")))
|
||||
VIDEO_CREATE_RETRY_BACKOFF_SECONDS = max(0.5, float(os.getenv("VIDEO_CREATE_RETRY_BACKOFF_SECONDS", "2")))
|
||||
FFMPEG_BIN = os.getenv("FFMPEG_BIN", "").strip()
|
||||
FFPROBE_BIN = os.getenv("FFPROBE_BIN", "").strip()
|
||||
LOCAL_FFMPEG_CANDIDATES = [
|
||||
@@ -6729,6 +6731,8 @@ def health() -> dict:
|
||||
"video_base_url": video_api_base(),
|
||||
"video_configured": bool(video_api_key()),
|
||||
"video_create_paths": VIDEO_CREATE_PATHS,
|
||||
"video_create_retry_attempts": VIDEO_CREATE_RETRY_ATTEMPTS,
|
||||
"video_create_retry_backoff_seconds": VIDEO_CREATE_RETRY_BACKOFF_SECONDS,
|
||||
"xai_video_model": XAI_VIDEO_MODEL,
|
||||
"xai_video_base_url": XAI_VIDEO_API_BASE_URL,
|
||||
"xai_video_configured": bool(video_api_key(XAI_VIDEO_MODEL)),
|
||||
@@ -9067,6 +9071,9 @@ def _video_public_error(raw: object) -> str:
|
||||
"connecterror",
|
||||
"connecttimeout",
|
||||
"readtimeout",
|
||||
"connection reset",
|
||||
"connection aborted",
|
||||
"remote protocol error",
|
||||
"ssl:",
|
||||
"_ssl.c",
|
||||
"handshake",
|
||||
@@ -9124,6 +9131,19 @@ def _video_public_error(raw: object) -> str:
|
||||
if any(token in lower for token in ("timeout", "timed out", "readtimeout", "connecttimeout", "超时")):
|
||||
return "视频生成失败:视频模型响应超时,可能是上游繁忙或网络不稳定。请稍后重试,或缩短时长后再生成。"
|
||||
|
||||
if any(token in lower for token in (
|
||||
"http 500",
|
||||
"http 502",
|
||||
"http 503",
|
||||
"http 504",
|
||||
"internal server error",
|
||||
"bad gateway",
|
||||
"service unavailable",
|
||||
"gateway timeout",
|
||||
"server error",
|
||||
)):
|
||||
return "视频生成失败:视频模型上游服务暂时异常,系统已自动重试但仍未成功。请稍后重新生成;如果持续出现,请联系管理员检查视频网关。"
|
||||
|
||||
if any(token in lower for token in (
|
||||
"name or service not known",
|
||||
"temporary failure in name resolution",
|
||||
@@ -9131,6 +9151,9 @@ def _video_public_error(raw: object) -> str:
|
||||
"connection refused",
|
||||
"network is unreachable",
|
||||
"connecterror",
|
||||
"connection reset",
|
||||
"connection aborted",
|
||||
"remote protocol error",
|
||||
"ssl:",
|
||||
"网络",
|
||||
"dns",
|
||||
@@ -9309,6 +9332,21 @@ def submit_video_create(
|
||||
)
|
||||
|
||||
|
||||
_VIDEO_CREATE_RETRY_STATUS_CODES = {408, 409, 425, 429, 500, 502, 503, 504}
|
||||
|
||||
|
||||
def _video_create_attempts(model: str | None) -> int:
|
||||
return VIDEO_CREATE_RETRY_ATTEMPTS if video_uses_xai(model) else 1
|
||||
|
||||
|
||||
def _video_create_retry_delay(attempt: int) -> float:
|
||||
return min(20.0, VIDEO_CREATE_RETRY_BACKOFF_SECONDS * (2 ** max(0, attempt - 1)))
|
||||
|
||||
|
||||
def _video_create_transport_error(exc: Exception) -> bool:
|
||||
return isinstance(exc, (httpx.TransportError, httpx.TimeoutException))
|
||||
|
||||
|
||||
def render_storyboard_video(
|
||||
job_id: str,
|
||||
local_id: str,
|
||||
@@ -9352,22 +9390,43 @@ def render_storyboard_video(
|
||||
create = None
|
||||
create_errors: list[str] = []
|
||||
for create_path in video_create_paths(model):
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
path = video_path(create_path)
|
||||
url = f"{base}{path}"
|
||||
attempts = _video_create_attempts(model)
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
resp = submit_video_create(client, url, headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
except Exception as exc:
|
||||
create_errors.append(f"{path} attempt {attempt}/{attempts} -> {exc.__class__.__name__}: {str(exc)[:700]}")
|
||||
if attempt < attempts and _video_create_transport_error(exc):
|
||||
delay = _video_create_retry_delay(attempt)
|
||||
print(f"[video create retry] job={job_id} video={local_id} path={path} attempt={attempt}/{attempts} error={str(exc)[:300]} retry_in={delay:.1f}s", flush=True)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
raise
|
||||
if video_uses_ark(model) and source_ref and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
create_errors.append(f"{path} + reference_video -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, url, headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
if video_uses_ark(model) and prepared_last_img and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs, primary_role)
|
||||
create_errors.append(f"{path} + last_frame -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, url, headers, ref_img, payload, None, None, prepared_product_imgs, primary_role)
|
||||
if video_uses_ark(model) and prepared_product_imgs and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None, primary_role)
|
||||
create_errors.append(f"{path} + product_reference -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, url, headers, ref_img, payload, None, prepared_last_img, None, primary_role)
|
||||
if resp.status_code < 400:
|
||||
create = resp
|
||||
break
|
||||
create_errors.append(f"{video_path(create_path)} -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
create_errors.append(f"{path} attempt {attempt}/{attempts} -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
if resp.status_code in _VIDEO_CREATE_RETRY_STATUS_CODES and attempt < attempts:
|
||||
delay = _video_create_retry_delay(attempt)
|
||||
print(f"[video create retry] job={job_id} video={local_id} path={path} attempt={attempt}/{attempts} http={resp.status_code} retry_in={delay:.1f}s", flush=True)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if resp.status_code not in {400, 404, 405}:
|
||||
resp.raise_for_status()
|
||||
raise RuntimeError(_video_create_failure_message(create_errors))
|
||||
break
|
||||
if create is not None:
|
||||
break
|
||||
if create is None:
|
||||
print(f"[video create failed] job={job_id} video={local_id} errors={' | '.join(create_errors)[:1800]}", flush=True)
|
||||
raise RuntimeError(_video_create_failure_message(create_errors))
|
||||
|
||||
@@ -82,6 +82,8 @@ VIDEO_STATUS_PATH=/api/v3/contents/generations/tasks/{id}
|
||||
VIDEO_CONTENT_PATH=/api/v3/contents/generations/tasks/{id}/content
|
||||
VIDEO_DURATION_FIELD=seconds
|
||||
VIDEO_POLL_TIMEOUT_SECONDS=900
|
||||
VIDEO_CREATE_RETRY_ATTEMPTS=3
|
||||
VIDEO_CREATE_RETRY_BACKOFF_SECONDS=2
|
||||
XAI_VIDEO_API_BASE_URL=https://ai.skg.com/ezlink/xai
|
||||
XAI_VIDEO_API_KEY=
|
||||
XAI_VIDEO_CREATE_PATH=/v1/videos/generations
|
||||
|
||||
@@ -114,6 +114,8 @@ VIDEO_STATUS_PATH=/api/v3/contents/generations/tasks/{id}
|
||||
VIDEO_CONTENT_PATH=/api/v3/contents/generations/tasks/{id}/content
|
||||
VIDEO_DURATION_FIELD=seconds
|
||||
VIDEO_POLL_TIMEOUT_SECONDS=900
|
||||
VIDEO_CREATE_RETRY_ATTEMPTS=3
|
||||
VIDEO_CREATE_RETRY_BACKOFF_SECONDS=2
|
||||
XAI_VIDEO_API_BASE_URL=https://ai.skg.com/ezlink/xai
|
||||
XAI_VIDEO_API_KEY=
|
||||
XAI_VIDEO_CREATE_PATH=/v1/videos/generations
|
||||
|
||||
@@ -690,9 +690,9 @@
|
||||
<h3>后端核心</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 <code>prompt_library</code> 和 <code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;<code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化,<code>/canvas-workflows</code> 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 <code>_strip_previous_polish_boilerplate</code> 去掉旧模板尾巴,再用 <code>_classify_prompt_intent</code> 判断人物、无人、物体、场景、动物或未知主体,最后用 <code>_repair_polished_prompt</code> 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;<code>_append_reference_image_person_guard</code> 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;<code>/health</code> 返回 <code>database</code>、<code>image_options</code>、<code>image_size_options</code>、<code>video_options</code>、<code>video_size_options</code>、<code>video_duration_options</code> 和 <code>video_max_duration_seconds</code>;<code>/frames/{idx}/generate</code> 的 <code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code>、<code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code>、<code>queue_size</code>、<code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr>
|
||||
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 <code>prompt_library</code> 和 <code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。启动时会初始化 Postgres schema、扫描现有 <code>state.json</code> / 资源库并写入索引;<code>/canvas-projects</code> 系列接口把画布项目按当前登录用户持久化,<code>/canvas-workflows</code> 系列接口把我的工作流按当前登录用户持久化为可复用模板。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>POST /prompt/polish</code> 用于中性 AI 润色和通用 LLM 文本生成,只保留用户明确给出的主体、品牌、产品、地点、风格和意图,不默认加入 SKG、按摩产品、平台或短视频广告话术。润色链路会先用 <code>_strip_previous_polish_boilerplate</code> 去掉旧模板尾巴,再用 <code>_classify_prompt_intent</code> 判断人物、无人、物体、场景、动物或未知主体,最后用 <code>_repair_polished_prompt</code> 修掉有人/无人矛盾、未写人却新增人物、未写 SKG 却出现 SKG 等冲突;<code>_append_reference_image_person_guard</code> 会在视频任务最终入队前给参考图请求追加条件提示,声明参考图里若有人物则按 AI 生成的虚拟角色处理;<code>/health</code> 返回 <code>database</code>、<code>image_options</code>、<code>image_size_options</code>、<code>video_options</code>、<code>video_size_options</code>、<code>video_duration_options</code>、<code>video_max_duration_seconds</code> 和视频创建重试配置;<code>/frames/{idx}/generate</code> 的 <code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。Grok/xAI 创建阶段遇到连接重置、超时、429 或 5xx 会按 <code>VIDEO_CREATE_RETRY_ATTEMPTS</code> 和 <code>VIDEO_CREATE_RETRY_BACKOFF_SECONDS</code> 自动退避重试,400/403 等明确错误不重试。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code>、<code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code>、<code>queue_size</code>、<code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 作为明确的 SKG 营销文案接口继续保留。</td></tr>
|
||||
<tr><td><code>api/db.py</code></td><td>Postgres 适配层:在 <code>DATABASE_URL</code> 存在且 <code>psycopg</code> 可用时启用;负责建表、健康检查、用户 upsert、审计日志、画布项目 CRUD、我的工作流 CRUD,以及把 <code>Job</code>、<code>AgentRun</code>、提示词库和素材库写入索引表。数据库不可用时本地开发会降级为 disabled,生产 <code>verify-prod-docker.sh</code> 会要求 <code>database.connected=true</code>。</td></tr>
|
||||
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:按当前视频网关过滤可真实路由的业务别名,Doubao / Ark 网关只暴露 <code>doubao-seedance*</code> 真实模型,Poe 网关才允许 Poe 的 Seedance / Kling / Veo 类模型;如果显式配置了 <code>VIDEO_API_BASE_URL</code> 但 <code>VIDEO_API_KEY</code> 为空,默认视频通道会标记不可用,不再回退通用 <code>LLM_API_KEY</code>。新增 <code>xai</code> / <code>grok-imagine-video</code> 独立走 <code>XAI_VIDEO_API_BASE_URL=https://ai.skg.com/ezlink/xai</code>、<code>XAI_VIDEO_API_KEY</code>、<code>/v1/videos/generations</code> 和 <code>/v1/videos/{id}</code>,创建返回 <code>request_id</code>、轮询完成返回 <code>video.url</code>;未配置 xAI key 时 <code>/health</code> 会标记不可用,前端不显示。</td></tr>
|
||||
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:按当前视频网关过滤可真实路由的业务别名,Doubao / Ark 网关只暴露 <code>doubao-seedance*</code> 真实模型,Poe 网关才允许 Poe 的 Seedance / Kling / Veo 类模型;如果显式配置了 <code>VIDEO_API_BASE_URL</code> 但 <code>VIDEO_API_KEY</code> 为空,默认视频通道会标记不可用,不再回退通用 <code>LLM_API_KEY</code>。新增 <code>xai</code> / <code>grok-imagine-video</code> 独立走 <code>XAI_VIDEO_API_BASE_URL=https://ai.skg.com/ezlink/xai</code>、<code>XAI_VIDEO_API_KEY</code>、<code>/v1/videos/generations</code> 和 <code>/v1/videos/{id}</code>,创建返回 <code>request_id</code>、轮询完成返回 <code>video.url</code>;未配置 xAI key 时 <code>/health</code> 会标记不可用,前端不显示。创建阶段的瞬时错误重试由 <code>VIDEO_CREATE_RETRY_ATTEMPTS</code> / <code>VIDEO_CREATE_RETRY_BACKOFF_SECONDS</code> 控制,并随 <code>/health</code> 暴露非敏感数值。</td></tr>
|
||||
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
|
||||
<tr><td><code>api/character_library/skg-characters</code></td><td>内置相似主体形象库:从桌面 5 套策划形象导入,<code>manifest.json</code> 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图和一段 <code>prompt_brief</code>。相似主体生成时优先使用文字 brief 作为创意方向,避免把内置图作为强参考图复制。</td></tr>
|
||||
<tr><td><code>asset_library/</code></td><td>全局素材库目录,和 <code>jobs/</code> 平级,不写入任何 job state。四类目录为 <code>subjects</code>、<code>products</code>、<code>scenes</code>、<code>videos</code>;每个素材自带 <code>manifest.json</code> 和图片/视频文件,<code>index.json</code> 只是启动扫描重建出来的缓存。库素材选用到 job 时必须复制文件到 <code>jobs/<jobId>/assets</code> 或 <code>storyboard-videos</code>,禁止直接保存 library 引用。</td></tr>
|
||||
@@ -1324,6 +1324,18 @@ ProductRefStateItem {
|
||||
<p><strong>影响:</strong>本地只配置 <code>XAI_VIDEO_API_KEY</code> 时,画布视频下拉只显示 Grok Imagine Video;同时配置有效 <code>VIDEO_API_KEY</code> 时才显示 Seedance。Kling / Veo 不会再因旧环境变量或旧缓存进入生成下拉。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-06-04 · Grok 视频创建阶段增加瞬时错误重试</h3>
|
||||
<span class="tag blue">API</span>
|
||||
<span class="tag orange">Bugfix</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>生产排查刘凌的 Grok 视频失败时,后端状态显示模型已正确传为 <code>grok-imagine-video</code>,但 xAI 创建接口在返回 <code>request_id</code> 前出现 <code>500 Internal Server Error</code> 或 <code>Connection reset by peer</code>,旧逻辑会第一次失败就把候选视频标为失败。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 给 Grok/xAI 创建阶段增加 <code>VIDEO_CREATE_RETRY_ATTEMPTS</code> 和 <code>VIDEO_CREATE_RETRY_BACKOFF_SECONDS</code>,默认遇到连接重置、超时、429 或 5xx 自动退避重试 3 次;400/401/403/404 等明确参数或权限错误不重试。<code>/health</code> 暴露非敏感重试配置,错误提示把 5xx 归类为上游视频服务暂时异常。</p>
|
||||
<p><strong>影响:</strong>Grok 通道不再因一次上游瞬时 500/断连直接失败;仍然保留日志中的每次重试状态,方便后续区分网关波动、权限问题和内容审核失败。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-06-03 · 接入 xAI Grok Imagine Video</h3>
|
||||
|
||||
Reference in New Issue
Block a user