fix: download xai video outputs reliably
This commit is contained in:
2
RULES.md
2
RULES.md
@@ -159,7 +159,7 @@
|
||||
- `IMAGE_FALLBACK_ENABLED` / `IMAGE_FALLBACK_MODEL`:图片主模型故障兜底;当前允许在 `gpt-image-2` 超时、429、5xx 或网络错误时临时使用 `gemini-3-pro-image-preview`,400/401/403/404 和参数错误不兜底
|
||||
- `IMAGE_CIRCUIT_FAILURE_THRESHOLD` / `IMAGE_CIRCUIT_COOLDOWN_SECONDS`:短时熔断配置,默认 `gpt-image-2` 连续 2 次上游类失败后 600 秒内直接走 Gemini 兜底;成功恢复后自动清空失败计数
|
||||
- `GPT_IMAGE_MODEL` / `SUBJECT_ASSET_IMAGE_MODEL` / `SUBJECT_ASSET_IMAGE_MODELS`:保留兼容旧环境变量名;主体 6 视图在转换层默认自动使用 `gpt-image-2`,同一套图内一旦触发 Gemini 兜底,后续视图沿用 Gemini,避免一张张等待主模型超时;用户显式选择 GPT 或 Gemini 时,`image_model_preference` 会让主体套图只走所选模型
|
||||
- `AI_HTTP_PROXY` / `IMAGE_HTTP_PROXY`:可选的 AI 网关出站代理;本地 launchd 后台进程不一定继承 shell 的 `http_proxy/https_proxy`,如生图报 DNS / ConnectError,可在本地 `api/.env` 配置后重启后端。`/health` 只回传是否配置代理,不回传代理地址。
|
||||
- `AI_HTTP_PROXY` / `IMAGE_HTTP_PROXY`:可选的 AI 网关出站代理;本地 launchd 后台进程不一定继承 shell 的 `http_proxy/https_proxy`,如生图或生视频 MP4 下载报 DNS / ConnectError / SSL 握手异常,可在本地 `api/.env` 配置后重启后端。本地 Docker 使用 `deploy/.env.local`,宿主机代理要写成 `http://host.docker.internal:端口`,不能写容器内的 `127.0.0.1`。`/health` 只回传是否配置代理,不回传代理地址。
|
||||
- `YTDLP_COOKIES_FILE` / `YTDLP_COOKIES_FROM_BROWSER`:可选 TikTok 下载登录态;生产云端固定使用 cookies 文件 `/run/secrets/tiktok_cookies.txt`(宿主机 `./secrets/tiktok_cookies.txt` 挂载进容器),本地开发可临时用浏览器 cookies。cookies 文件属于敏感登录态,只能放本机或服务器私有路径,不允许入库。
|
||||
- `VOICE_PROVIDER`:配音通道,服务端固定使用 `azure_openai`;旧环境若写 `minimax` 会被忽略
|
||||
- `AZURE_OPENAI_BASE_URL` / `AZURE_OPENAI_API_KEY`:微软 Azure OpenAI 协议配音网关;本地未单独配置 Key 时回退复用 `LLM_API_KEY`
|
||||
|
||||
41
api/main.py
41
api/main.py
@@ -9032,6 +9032,25 @@ def _video_public_error(raw: object) -> str:
|
||||
text = str(raw or "").strip()
|
||||
lower = text.lower()
|
||||
|
||||
if any(token in lower for token in (
|
||||
"name or service not known",
|
||||
"temporary failure in name resolution",
|
||||
"nodename nor servname",
|
||||
"connection refused",
|
||||
"network is unreachable",
|
||||
"connecterror",
|
||||
"connecttimeout",
|
||||
"readtimeout",
|
||||
"ssl:",
|
||||
"_ssl.c",
|
||||
"handshake",
|
||||
"unexpected_eof",
|
||||
"eof occurred",
|
||||
"网络",
|
||||
"dns",
|
||||
)):
|
||||
return "视频生成失败:服务器连接视频模型网关异常,请稍后重试;如果连续失败,请联系管理员检查视频网关网络。"
|
||||
|
||||
if any(token.lower() in lower for token in (
|
||||
"InputImageSensitiveContentDetected.PrivacyInformation".lower(),
|
||||
"privacyinformation",
|
||||
@@ -9116,15 +9135,31 @@ def _video_create_failure_message(create_errors: list[str]) -> str:
|
||||
|
||||
|
||||
def download_generated_video(client, base: str, headers: dict, provider_id: str, direct_url: str, out_mp4: Path, model: str | None = None) -> None:
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(4):
|
||||
try:
|
||||
_download_generated_video_once(client, base, headers, provider_id, direct_url, out_mp4, model)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt >= 3:
|
||||
break
|
||||
time.sleep(2 + attempt * 3)
|
||||
raise last_error or RuntimeError("视频生成完成但下载失败")
|
||||
|
||||
|
||||
def _download_generated_video_once(client, base: str, headers: dict, provider_id: str, direct_url: str, out_mp4: Path, model: str | None = None) -> None:
|
||||
if direct_url:
|
||||
url = direct_url if direct_url.startswith("http") else f"{base}{direct_url if direct_url.startswith('/') else '/' + direct_url}"
|
||||
r = client.get(url, headers=headers if url.startswith(base) else None)
|
||||
r = client.get(url, headers=headers if url.startswith(base) else None, follow_redirects=True)
|
||||
else:
|
||||
content_path = video_content_path(model)
|
||||
if not content_path:
|
||||
raise RuntimeError("视频生成完成但未返回可下载地址")
|
||||
r = client.get(f"{base}{video_path(content_path, id=provider_id)}", headers=headers)
|
||||
r = client.get(f"{base}{video_path(content_path, id=provider_id)}", headers=headers, follow_redirects=True)
|
||||
r.raise_for_status()
|
||||
if not r.content:
|
||||
raise RuntimeError("视频生成完成但下载内容为空")
|
||||
out_mp4.write_bytes(r.content)
|
||||
|
||||
|
||||
@@ -9285,7 +9320,7 @@ def render_storyboard_video(
|
||||
prepare_video_reference(product_ref_path, product_img)
|
||||
prepared_product_imgs.append(product_img)
|
||||
update_generated_video(job_id, local_id, status="in_progress", progress=5, queue_message="准备素材…")
|
||||
with httpx.Client(timeout=120) as client:
|
||||
with ai_http_client(timeout=300) as client:
|
||||
payload = {"model": model, "prompt": prompt, "size": size, "resolution": resolution}
|
||||
payload[VIDEO_DURATION_FIELD] = seconds
|
||||
create = None
|
||||
|
||||
@@ -1333,7 +1333,7 @@ ProductRefStateItem {
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>SKG xAI 网关 <code>https://ai.skg.com/ezlink/xai</code> 已确认可用 <code>grok-imagine-video</code> 文生视频,但项目只把 Seedance 暴露给画布,后端也按单一视频网关处理,无法同时保留 Seedance 并新增 xAI。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 新增 <code>xai</code> / <code>grok-imagine-video</code> 视频模型别名、<code>XAI_VIDEO_API_BASE_URL</code> / <code>XAI_VIDEO_API_KEY</code> / <code>XAI_VIDEO_CREATE_PATH</code> / <code>XAI_VIDEO_STATUS_PATH</code> 配置,按模型分流到 <code>/v1/videos/generations</code> 和 <code>/v1/videos/{id}</code>;创建时识别 xAI 的 <code>request_id</code>,轮询完成时读取 <code>video.url</code> 并下载 MP4。纯文生视频不会把系统空白帧误传为参考图;图生视频会把用户上传首帧作为 <code>image</code> 传入。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 新增 <code>xai</code> / <code>grok-imagine-video</code> 视频模型别名、<code>XAI_VIDEO_API_BASE_URL</code> / <code>XAI_VIDEO_API_KEY</code> / <code>XAI_VIDEO_CREATE_PATH</code> / <code>XAI_VIDEO_STATUS_PATH</code> 配置,按模型分流到 <code>/v1/videos/generations</code> 和 <code>/v1/videos/{id}</code>;创建时识别 xAI 的 <code>request_id</code>,轮询完成时读取 <code>video.url</code> 并下载 MP4。视频创建、轮询和 MP4 下载统一复用 <code>ai_http_client()</code>,可走 <code>AI_HTTP_PROXY</code>,MP4 下载会跟随重定向并重试,避免 <code>vidgen.x.ai</code> TLS 握手偶发失败时直接丢结果。纯文生视频不会把系统空白帧误传为参考图;图生视频会把用户上传首帧作为 <code>image</code> 传入。</p>
|
||||
<p><strong>前端 / 配置:</strong><code>web/canvas-app/src/config/models.js</code> 新增默认不可用的 <code>xai</code> 模型,<code>web/canvas-app/src/stores/pinia/models.js</code> 改为接受后端 <code>/health</code> 返回的可用视频模型,不再硬编码只保留 Seedance。<code>api/.env.example</code>、<code>deploy/.env.local.example</code> 和 <code>deploy/.env.production.example</code> 增加 xAI 私有 key 配置位,真实 key 只填本地或服务器私有 env。</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user