diff --git a/RULES.md b/RULES.md index 475a614..7609035 100644 --- a/RULES.md +++ b/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` diff --git a/api/main.py b/api/main.py index fe5c917..2d5952a 100644 --- a/api/main.py +++ b/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 diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 6746f86..f0ddd6c 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -1333,7 +1333,7 @@ ProductRefStateItem {

问题:SKG xAI 网关 https://ai.skg.com/ezlink/xai 已确认可用 grok-imagine-video 文生视频,但项目只把 Seedance 暴露给画布,后端也按单一视频网关处理,无法同时保留 Seedance 并新增 xAI。

-

改动:api/main.py 新增 xai / grok-imagine-video 视频模型别名、XAI_VIDEO_API_BASE_URL / XAI_VIDEO_API_KEY / XAI_VIDEO_CREATE_PATH / XAI_VIDEO_STATUS_PATH 配置,按模型分流到 /v1/videos/generations/v1/videos/{id};创建时识别 xAI 的 request_id,轮询完成时读取 video.url 并下载 MP4。纯文生视频不会把系统空白帧误传为参考图;图生视频会把用户上传首帧作为 image 传入。

+

改动:api/main.py 新增 xai / grok-imagine-video 视频模型别名、XAI_VIDEO_API_BASE_URL / XAI_VIDEO_API_KEY / XAI_VIDEO_CREATE_PATH / XAI_VIDEO_STATUS_PATH 配置,按模型分流到 /v1/videos/generations/v1/videos/{id};创建时识别 xAI 的 request_id,轮询完成时读取 video.url 并下载 MP4。视频创建、轮询和 MP4 下载统一复用 ai_http_client(),可走 AI_HTTP_PROXY,MP4 下载会跟随重定向并重试,避免 vidgen.x.ai TLS 握手偶发失败时直接丢结果。纯文生视频不会把系统空白帧误传为参考图;图生视频会把用户上传首帧作为 image 传入。

前端 / 配置:web/canvas-app/src/config/models.js 新增默认不可用的 xai 模型,web/canvas-app/src/stores/pinia/models.js 改为接受后端 /health 返回的可用视频模型,不再硬编码只保留 Seedance。api/.env.exampledeploy/.env.local.exampledeploy/.env.production.example 增加 xAI 私有 key 配置位,真实 key 只填本地或服务器私有 env。