auto-save 2026-05-13 20:40 (~4)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
# 工作目录
|
||||
|
||||
46
api/main.py
46
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")
|
||||
|
||||
@@ -790,7 +790,7 @@ api/main.py
|
||||
<li>ASR:SKG 网关 audio endpoint 404 或渠道不可用。</li>
|
||||
<li>Translate:本身 text 通,但产品流里依赖 ASR 段落。</li>
|
||||
<li>Rewrite:需要 SKG 产品信息模板和目标脚本结构。</li>
|
||||
<li>Video Gen:已接 OpenAI-compatible <code>/videos</code> 网关;前端可选 Seedance / Kling / Veo 3,具体模型 ID 由 <code>VIDEO_MODEL_*</code> 环境变量映射。</li>
|
||||
<li>Video Gen:当前 SKG ezlink 未开 <code>/videos</code>(实测 404,<code>/models</code> 只列 <code>sora-2</code>);代码已支持通过 <code>VIDEO_API_BASE_URL</code>/<code>VIDEO_API_KEY</code> 显式接 Seedance / Kling / Veo 3 外部生视频 API,未配置时会前置报错,不再生成 5% 失败任务。</li>
|
||||
<li>Compose:还没做本地 ffmpeg 字幕/TTS 合成。</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -838,8 +838,8 @@ api/main.py
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>4 图槽已经粘贴参考图后,用户要直接调用生视频 API,而不是只生成 prompt 或图片任务。</p>
|
||||
<p><strong>改动:</strong>分镜编排明细区增加 Seedance / Kling / Veo 3 模型选择和“调用模型生成视频”按钮;后端新增 <code>/jobs/{job_id}/frames/{idx}/storyboard/video</code>,提交 <code>/videos</code> 网关后轮询并保存 MP4;<code>VideoGenNode</code> 读取 <code>job.generated_videos</code> 展示排队、生成中、失败和完成视频。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>api/.env.example</code>、<code>web/components/storyboard-workbench.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>web/app/page.tsx</code>、<code>web/lib/api.ts</code>。Sora 不再作为默认模型,真实模型 ID 通过 <code>VIDEO_MODEL_SEEDANCE</code>、<code>VIDEO_MODEL_KLING</code>、<code>VIDEO_MODEL_VEO3</code> 配置。</p>
|
||||
<p><strong>改动:</strong>分镜编排明细区增加 Seedance / Kling / Veo 3 模型选择和“调用模型生成视频”按钮;后端新增 <code>/jobs/{job_id}/frames/{idx}/storyboard/video</code>。若已配置真实 <code>VIDEO_API_BASE_URL</code>,则提交、轮询并保存 MP4;若仍使用当前 SKG ezlink,则前置返回 503,避免继续创建 404 失败任务。<code>VideoGenNode</code> 读取 <code>job.generated_videos</code> 展示排队、生成中、失败和完成视频。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>api/.env.example</code>、<code>web/components/storyboard-workbench.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>web/app/page.tsx</code>、<code>web/lib/api.ts</code>。Sora 不再作为默认模型;真实模型 ID 通过 <code>VIDEO_MODEL_SEEDANCE</code>、<code>VIDEO_MODEL_KLING</code>、<code>VIDEO_MODEL_VEO3</code> 配置,真实视频 API 地址通过 <code>VIDEO_API_BASE_URL</code>/<code>VIDEO_API_KEY</code> 配置。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
|
||||
Reference in New Issue
Block a user