auto-save 2026-05-13 21:07 (~4)

This commit is contained in:
2026-05-13 21:07:56 +08:00
parent 21c5a2bc2e
commit efe984bb02
4 changed files with 141 additions and 16 deletions

View File

@@ -2381,6 +2381,13 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-13 20:56 (~7)",
"files_changed": 1
},
{
"ts": "2026-05-13T21:02:26+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 21:02 (~2)",
"hash": "21c5a2b",
"files_changed": 2
}
]
}

View File

@@ -8,13 +8,26 @@ TRANSLATE_MODEL=gemini-2.5-flash
REWRITE_MODEL=gemini-2.5-pro
IMAGE_MODEL=gemini-3-pro-image-preview
VIDEO_MODEL=seedance
VIDEO_MODEL_SEEDANCE=seedance
VIDEO_MODEL_KLING=kling
VIDEO_MODEL_VEO3=veo3
VIDEO_MODEL_SEEDANCE=seedance-2-fast
VIDEO_MODEL_KLING=kling-omni
VIDEO_MODEL_VEO3=veo-3.1-fast
# Poe 视频 API优先用于 Seedance / Kling / Veo
POE_API_BASE_URL=https://api.poe.com/v1
POE_API_KEY=
# 火山方舟 Seedance 视频 API 可直接覆盖这里:
# VIDEO_API_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
# VIDEO_API_KEY=
# VIDEO_MODEL_SEEDANCE=doubao-seedance-1-0-pro-fast-250528
# VIDEO_CREATE_PATHS=/contents/generations/tasks
# VIDEO_STATUS_PATH=/contents/generations/tasks/{id}
#
# 自定义视频网关覆盖;留空时如配置 POE_API_KEY则走 Poe。
VIDEO_API_BASE_URL=
VIDEO_API_KEY=
VIDEO_CREATE_PATH=/videos
VIDEO_CREATE_PATHS=/videos,/videos/generations,/video/generations
VIDEO_CREATE_PATHS=/videos
VIDEO_STATUS_PATH=/videos/{id}
VIDEO_CONTENT_PATH=/videos/{id}/content
VIDEO_DURATION_FIELD=seconds

View File

@@ -220,12 +220,30 @@ def public_api_base() -> str:
return (LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
def video_uses_poe() -> bool:
if VIDEO_API_BASE_URL:
return VIDEO_API_BASE_URL.rstrip("/") == POE_API_BASE_URL.rstrip("/")
return bool(POE_API_KEY)
def video_uses_ark() -> bool:
return "ark.cn-beijing.volces.com" in video_api_base()
def video_api_base() -> str:
return (VIDEO_API_BASE_URL or LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
if VIDEO_API_BASE_URL:
return VIDEO_API_BASE_URL.rstrip("/")
if POE_API_KEY:
return POE_API_BASE_URL.rstrip("/")
return (LLM_BASE_URL or "https://api.openai.com/v1").rstrip("/")
def video_api_key() -> str:
return VIDEO_API_KEY or LLM_API_KEY
if VIDEO_API_KEY:
return VIDEO_API_KEY
if video_uses_poe():
return POE_API_KEY
return LLM_API_KEY
def video_path(template: str, **values: str) -> str:
@@ -235,7 +253,7 @@ def video_path(template: str, **values: str) -> str:
def ensure_video_api_configured() -> None:
if not video_api_key():
raise HTTPException(503, "VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API")
raise HTTPException(503, "POE_API_KEY、VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API")
def storyboard_ref_path(job_id: str, ref: dict | None) -> Path | None:
@@ -805,7 +823,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_provider": "poe" if video_uses_poe() else ("ark" if video_uses_ark() else "custom"),
"video_base_url": video_api_base(),
"video_configured": bool(video_api_key()),
"video_create_paths": VIDEO_CREATE_PATHS,
},
@@ -1629,6 +1648,10 @@ class GenerateStoryboardVideoReq(BaseModel):
def video_seconds(duration: float) -> str:
if video_uses_ark():
if duration <= 0:
return "5"
return str(max(4, min(15, round(duration))))
if duration <= 6:
return "4"
if duration <= 10:
@@ -1683,6 +1706,12 @@ def video_url_from_response(data: dict) -> str:
v = output.get(key)
if isinstance(v, str) and v:
return v
content = data.get("content")
if isinstance(content, dict):
for key in ("video_url", "url", "download_url", "file_url"):
v = content.get(key)
if isinstance(v, str) and v:
return v
return ""
@@ -1696,6 +1725,64 @@ def download_generated_video(client, base: str, headers: dict, provider_id: str,
out_mp4.write_bytes(r.content)
def size_to_video_ratio(size: str) -> str:
try:
w, h = [int(x) for x in size.lower().replace(" ", "").split("x", 1)]
except Exception:
return "9:16"
if w <= 0 or h <= 0:
return "9:16"
ratio = w / h
known = {
"16:9": 16 / 9,
"9:16": 9 / 16,
"1:1": 1,
"4:3": 4 / 3,
"3:4": 3 / 4,
"21:9": 21 / 9,
}
return min(known, key=lambda key: abs(known[key] - ratio))
def ark_reference_data_url(ref_img: Path) -> str:
mime = "image/png" if ref_img.suffix.lower() == ".png" else "image/jpeg"
return f"data:{mime};base64,{base64.b64encode(ref_img.read_bytes()).decode('ascii')}"
def submit_video_create(client, url: str, headers: dict, ref_img: Path, payload: dict):
if video_uses_ark():
data = {
"model": payload["model"],
"content": [
{"type": "text", "text": payload["prompt"]},
{
"type": "image_url",
"image_url": {"url": ark_reference_data_url(ref_img)},
"role": "first_frame",
},
],
"ratio": size_to_video_ratio(str(payload.get("size", ""))),
"duration": int(float(str(payload.get(VIDEO_DURATION_FIELD, 5)))),
"watermark": False,
"resolution": "720p",
}
return client.post(url, headers={**headers, "Content-Type": "application/json"}, json=data)
if video_uses_poe():
data = dict(payload)
data[VIDEO_DURATION_FIELD] = int(float(str(data.get(VIDEO_DURATION_FIELD, 4))))
data["input_image"] = base64.b64encode(ref_img.read_bytes()).decode("ascii")
return client.post(url, headers=headers, json=data)
with ref_img.open("rb") as fh:
return client.post(
url,
headers=headers,
data=payload,
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
)
def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_path: Path, prompt: str, model: str, seconds: str, size: str) -> None:
import httpx
@@ -1714,13 +1801,7 @@ def render_storyboard_video(job_id: str, local_id: str, provider_id: str, ref_pa
create = None
create_errors: list[str] = []
for create_path in VIDEO_CREATE_PATHS:
with ref_img.open("rb") as fh:
resp = client.post(
f"{base}{video_path(create_path)}",
headers=headers,
data=payload,
files={"input_reference": ("reference.jpg", fh, "image/jpeg")},
)
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload)
if resp.status_code < 400:
create = resp
break

View File

@@ -790,7 +790,7 @@ api/main.py
<li>ASRSKG 网关 audio endpoint 404 或渠道不可用。</li>
<li>Translate本身 text 通,但产品流里依赖 ASR 段落。</li>
<li>Rewrite需要 SKG 产品信息模板和目标脚本结构。</li>
<li>Video Gen模型层按业务保留 Seedance / Kling / Veo/Voe 选择;网关调用层通过 <code>VIDEO_CREATE_PATHS</code> 多入口尝试,当前常见入口实测返回 404/unsupported若平台后台有其它入口要直接配置到该变量</li>
<li>Video Gen模型层按业务保留 Seedance / Kling / Veo/Voe 选择;后端已支持 Poe 视频通道,别名默认映射到 <code>seedance-2-fast</code><code>kling-omni</code><code>veo-3.1-fast</code>,提交后写入 Video Gen 节点</li>
<li>Compose还没做本地 ffmpeg 字幕/TTS 合成。</li>
</ul>
</div>
@@ -830,6 +830,30 @@ api/main.py
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-13 · 生视频支持火山方舟 Ark 异步任务</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>用户提供火山方舟 <code>https://ark.cn-beijing.volces.com/api/v3</code> 作为生视频通道;这个通道不是 Poe 的 <code>/videos</code> 形态,而是内容生成异步任务。</p>
<p><strong>改动:</strong>后端识别 Ark base 后,提交改为 <code>POST /contents/generations/tasks</code>,请求体使用 <code>content</code> 数组:文本 prompt + 首帧 <code>image_url</code> data URL轮询改为 <code>GET /contents/generations/tasks/{id}</code>,成功后读取 <code>content.video_url</code> 下载 MP4。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>docs/source-analysis.html</code>。本机 <code>api/.env</code> 需要把 <code>VIDEO_API_BASE_URL</code>/<code>VIDEO_API_KEY</code>/<code>VIDEO_CREATE_PATHS</code>/<code>VIDEO_STATUS_PATH</code> 指向 Ark。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频改接 Poe 视频模型</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>SKG ezlink 的 OpenAI 兼容 base 可列出部分模型,但常规 <code>/videos</code> 入口返回 404/unsupported用户确认可用的视频模型在 Poe 通道里。</p>
<p><strong>改动:</strong>后端新增 <code>POE_API_BASE_URL</code>/<code>POE_API_KEY</code> 配置,未显式配置 <code>VIDEO_API_BASE_URL</code> 时优先走 PoeSeedance / Kling / Veo/Voe 业务别名默认映射到 Poe 真实模型 <code>seedance-2-fast</code><code>kling-omni</code><code>veo-3.1-fast</code>。Poe 提交使用 <code>input_image</code> base64继续轮询 <code>/videos/{id}</code> 并下载 <code>/videos/{id}/content</code></p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>docs/source-analysis.html</code>。密钥只放本地 <code>api/.env</code>,不进入源码解析页。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频提交不再被前端锁死</h3>