From 66f249529626bb688a45a0c5629a99d75d0f41ba Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 20:40:23 +0800 Subject: [PATCH] auto-save 2026-05-13 20:40 (~4) --- .memory/worklog.json | 13 +++++++++++ api/.env.example | 5 +++++ api/main.py | 46 +++++++++++++++++++++++++++++++++------ docs/source-analysis.html | 6 ++--- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index b728ce5..6f6aa45 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2328,6 +2328,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 20:29 (~9)", "files_changed": 1 + }, + { + "ts": "2026-05-13T20:34:52+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 20:34 (~2)", + "hash": "bbe9864", + "files_changed": 2 + }, + { + "ts": "2026-05-13T12:39:30Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-13 20:34 (~2)", + "files_changed": 4 } ] } diff --git a/api/.env.example b/api/.env.example index 7199598..218dfdd 100644 --- a/api/.env.example +++ b/api/.env.example @@ -11,6 +11,11 @@ VIDEO_MODEL=seedance VIDEO_MODEL_SEEDANCE=seedance VIDEO_MODEL_KLING=kling VIDEO_MODEL_VEO3=veo3 +VIDEO_API_BASE_URL= +VIDEO_API_KEY= +VIDEO_CREATE_PATH=/videos +VIDEO_STATUS_PATH=/videos/{id} +VIDEO_CONTENT_PATH=/videos/{id}/content VIDEO_DURATION_FIELD=seconds # 工作目录 diff --git a/api/main.py b/api/main.py index 68d5188..1803705 100644 --- a/api/main.py +++ b/api/main.py @@ -36,6 +36,11 @@ VIDEO_MODEL_ALIASES = { "kling": os.getenv("VIDEO_MODEL_KLING", "kling").strip() or "kling", "veo3": os.getenv("VIDEO_MODEL_VEO3", "veo3").strip() or "veo3", } +VIDEO_API_BASE_URL = os.getenv("VIDEO_API_BASE_URL", "").strip() +VIDEO_API_KEY = os.getenv("VIDEO_API_KEY", "").strip() +VIDEO_CREATE_PATH = os.getenv("VIDEO_CREATE_PATH", "/videos").strip() or "/videos" +VIDEO_STATUS_PATH = os.getenv("VIDEO_STATUS_PATH", "/videos/{id}").strip() or "/videos/{id}" +VIDEO_CONTENT_PATH = os.getenv("VIDEO_CONTENT_PATH", "/videos/{id}/content").strip() or "/videos/{id}/content" VIDEO_DURATION_FIELD = os.getenv("VIDEO_DURATION_FIELD", "seconds").strip() or "seconds" # OpenAI 客户端(OpenAI 兼容网关,含 SKG ezlink) @@ -191,6 +196,32 @@ def public_api_base() -> str: return (LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/") +def video_api_base() -> str: + return (VIDEO_API_BASE_URL or LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/") + + +def video_api_key() -> str: + return VIDEO_API_KEY or LLM_API_KEY + + +def video_path(template: str, **values: str) -> str: + path = template.format(**values) + return path if path.startswith("/") else f"/{path}" + + +def ensure_video_api_configured() -> None: + base = video_api_base() + # 已探测:SKG ezlink 当前只开了 chat/images,/videos 返回 404。 + # 没有显式 VIDEO_API_BASE_URL 时,不再把这个 404 伪装成一次“生成失败”。 + if not VIDEO_API_BASE_URL and "ai.skg.com/ezlink" in base: + raise HTTPException( + 503, + "当前 SKG ezlink 网关未开通生视频 /videos 端点;请配置 VIDEO_API_BASE_URL/VIDEO_API_KEY 接 Seedance、Kling 或 Veo 3 的真实视频 API 后再生成。", + ) + if not video_api_key(): + raise HTTPException(503, "VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API") + + def storyboard_ref_path(job_id: str, ref: dict | None) -> Path | None: if not ref: return None @@ -758,6 +789,8 @@ def health() -> dict: "rewrite": REWRITE_MODEL, "video": VIDEO_MODEL, "video_aliases": VIDEO_MODEL_ALIASES, + "video_base_url": video_api_base() if VIDEO_API_BASE_URL else "", + "video_configured": bool(VIDEO_API_BASE_URL and video_api_key()), }, } @@ -1641,7 +1674,7 @@ def download_generated_video(client, base: str, headers: dict, provider_id: str, 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) else: - r = client.get(f"{base}/videos/{provider_id}/content", headers=headers) + r = client.get(f"{base}{video_path(VIDEO_CONTENT_PATH, id=provider_id)}", headers=headers) r.raise_for_status() out_mp4.write_bytes(r.content) @@ -1652,8 +1685,8 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa out_dir = job_dir(job_id) / "storyboard_videos" / local_id ref_img = out_dir / "reference.jpg" out_mp4 = out_dir / "video.mp4" - base = public_api_base() - headers = {"Authorization": f"Bearer {LLM_API_KEY}"} + base = video_api_base() + headers = {"Authorization": f"Bearer {video_api_key()}"} try: prepare_video_reference(ref_path, ref_img) @@ -1663,7 +1696,7 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa payload[VIDEO_DURATION_FIELD] = seconds with ref_img.open("rb") as fh: create = client.post( - f"{base}/videos", + f"{base}{video_path(VIDEO_CREATE_PATH)}", headers=headers, data=payload, files={"input_reference": ("reference.jpg", fh, "image/jpeg")}, @@ -1679,7 +1712,7 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa deadline = time.time() + 420 while status in {"queued", "in_progress"} and time.time() < deadline: time.sleep(8) - poll = client.get(f"{base}/videos/{video_api_id}", headers=headers) + poll = client.get(f"{base}{video_path(VIDEO_STATUS_PATH, id=video_api_id)}", headers=headers) poll.raise_for_status() pdata = poll.json() status = normalize_video_status(pdata.get("status")) @@ -1712,8 +1745,7 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide frame = next((f for f in job.frames if f.index == idx), None) if not frame: raise HTTPException(404, "frame not found") - if not LLM_API_KEY: - raise HTTPException(500, "LLM_API_KEY 未配置,无法调用视频生成 API") + ensure_video_api_configured() prompt = req.prompt.strip() if not prompt: raise HTTPException(400, "prompt required") diff --git a/docs/source-analysis.html b/docs/source-analysis.html index e00579b..e104f16 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -790,7 +790,7 @@ api/main.py
  • ASR:SKG 网关 audio endpoint 404 或渠道不可用。
  • Translate:本身 text 通,但产品流里依赖 ASR 段落。
  • Rewrite:需要 SKG 产品信息模板和目标脚本结构。
  • -
  • Video Gen:已接 OpenAI-compatible /videos 网关;前端可选 Seedance / Kling / Veo 3,具体模型 ID 由 VIDEO_MODEL_* 环境变量映射。
  • +
  • Video Gen:当前 SKG ezlink 未开 /videos(实测 404,/models 只列 sora-2);代码已支持通过 VIDEO_API_BASE_URL/VIDEO_API_KEY 显式接 Seedance / Kling / Veo 3 外部生视频 API,未配置时会前置报错,不再生成 5% 失败任务。
  • Compose:还没做本地 ffmpeg 字幕/TTS 合成。
  • @@ -838,8 +838,8 @@ api/main.py

    问题:4 图槽已经粘贴参考图后,用户要直接调用生视频 API,而不是只生成 prompt 或图片任务。

    -

    改动:分镜编排明细区增加 Seedance / Kling / Veo 3 模型选择和“调用模型生成视频”按钮;后端新增 /jobs/{job_id}/frames/{idx}/storyboard/video,提交 /videos 网关后轮询并保存 MP4;VideoGenNode 读取 job.generated_videos 展示排队、生成中、失败和完成视频。

    -

    影响:api/main.pyapi/.env.exampleweb/components/storyboard-workbench.tsxweb/components/nodes/index.tsxweb/app/page.tsxweb/lib/api.ts。Sora 不再作为默认模型,真实模型 ID 通过 VIDEO_MODEL_SEEDANCEVIDEO_MODEL_KLINGVIDEO_MODEL_VEO3 配置。

    +

    改动:分镜编排明细区增加 Seedance / Kling / Veo 3 模型选择和“调用模型生成视频”按钮;后端新增 /jobs/{job_id}/frames/{idx}/storyboard/video。若已配置真实 VIDEO_API_BASE_URL,则提交、轮询并保存 MP4;若仍使用当前 SKG ezlink,则前置返回 503,避免继续创建 404 失败任务。VideoGenNode 读取 job.generated_videos 展示排队、生成中、失败和完成视频。

    +

    影响:api/main.pyapi/.env.exampleweb/components/storyboard-workbench.tsxweb/components/nodes/index.tsxweb/app/page.tsxweb/lib/api.ts。Sora 不再作为默认模型;真实模型 ID 通过 VIDEO_MODEL_SEEDANCEVIDEO_MODEL_KLINGVIDEO_MODEL_VEO3 配置,真实视频 API 地址通过 VIDEO_API_BASE_URL/VIDEO_API_KEY 配置。