Publish current ComfyUI O1Key code baseline

Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
Jony
2026-09-24 19:56:48 +08:00
parent 3e337722ab
commit ba920f2b66
183 changed files with 49496 additions and 9934 deletions
+50 -29
View File
@@ -3,17 +3,17 @@ Seedance 视频生成客户端
使用 new-api 原生格式:POST /v1/video/generations → GET /v1/video/generations/{task_id}
"""
import asyncio
import json
import os
from typing import Any, Callable, Dict, Optional
import aiohttp
from ..utils.config import get_api_key_or_raise
from ..utils.config import get_api_key_or_raise, get_base_url_by_route
from ..utils.http_error import async_request_with_retry
from ..utils.video_task import (
PollDeadline,
check_interrupt,
download_video_to_file,
extract_error_message,
extract_progress,
extract_status,
@@ -25,11 +25,16 @@ from ..utils.video_task import (
class SeedanceClient:
"""Seedance 视频生成客户端(new-api 原生三段式)"""
"""Seedance 视频生成客户端(new-api 原生三段式)
# 提交任务
注意:新旧格式模型(seedance-2-0-260128-d 等)共用同一套端点,
区别仅在于请求体结构(顶层 content vs metadata.content),
由调用方(节点层)通过 use_new_format 控制请求体拼装方式。
"""
# 提交任务(新旧格式模型共用)
CREATE_ENDPOINT = "/v1/video/generations"
# 查询任务状态:{task_id} 占位
# 查询任务状态:{task_id} 占位(新旧格式模型共用)
STATUS_ENDPOINT = "/v1/video/generations/{task_id}"
POLL_INITIAL_INTERVAL = 4 # 首次轮询等待秒数
@@ -41,7 +46,7 @@ class SeedanceClient:
def __init__(self):
self.api_key = get_api_key_or_raise()
self.base_url = "https://api.o1key.com"
self.base_url = get_base_url_by_route()
def _headers(self) -> Dict[str, str]:
return {
@@ -55,9 +60,17 @@ class SeedanceClient:
self,
body: Dict[str, Any],
session: aiohttp.ClientSession,
use_new_format: bool = False,
) -> str:
"""提交视频生成任务,返回 task_id"""
"""提交视频生成任务,返回 task_id
use_new_format 仅用于调试日志标注请求体格式,不影响端点选择
(新旧格式模型统一走 CREATE_ENDPOINT)。
"""
url = f"{self.base_url}{self.CREATE_ENDPOINT}"
print(f"[Seedance] 提交 → {url} (body格式: {'' if use_new_format else ''})")
check_interrupt()
resp = await run_with_interrupt(async_request_with_retry(
session, "POST", url, json=body, headers=self._headers(), prefix="Seedance 提交: "
@@ -69,7 +82,7 @@ class SeedanceClient:
# new-api 返回字段:id / task_id
task_id = data.get("id") or data.get("task_id")
if not task_id:
raise RuntimeError(f"API 未返回任务 ID,响应:{data}")
raise RuntimeError("API 未返回任务 ID")
return task_id
# ── 2. 轮询状态 ────────────────────────────────────────────────────
@@ -79,12 +92,15 @@ class SeedanceClient:
task_id: str,
session: aiohttp.ClientSession,
on_progress: Optional[Callable[[int], None]] = None,
use_new_format: bool = False,
) -> str:
"""轮询任务状态,成功后返回视频 URL"""
"""轮询任务状态,成功后返回视频 URL(新旧格式模型统一走 STATUS_ENDPOINT"""
url = f"{self.base_url}{self.STATUS_ENDPOINT.format(task_id=task_id)}"
interval = self.POLL_INITIAL_INTERVAL
deadline = PollDeadline(label="Seedance")
while True:
deadline.check()
check_interrupt()
async with session.get(url, headers=self._headers()) as resp:
text = await resp.text()
@@ -123,7 +139,7 @@ class SeedanceClient:
or inner.get("url")
)
if not video_url:
raise RuntimeError(f"任务成功但未找到视频 URL,响应:{result}")
raise RuntimeError("任务成功但未找到视频 URL")
# 末帧图片 URL 在 inner["data"]["content"]["last_frame_url"]
last_frame_url = (
content.get("last_frame_url")
@@ -149,16 +165,9 @@ class SeedanceClient:
) -> str:
"""下载视频到本地,返回本地路径"""
print(f"[Seedance] 下载视频...")
check_interrupt()
async with session.get(video_url, allow_redirects=True) as resp:
if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192):
check_interrupt()
f.write(chunk)
return save_path
return await download_video_to_file(
session, video_url, save_path, label="Seedance",
)
# ── 全流程入口(供节点调用)────────────────────────────────────────
@@ -168,6 +177,7 @@ class SeedanceClient:
save_path: str,
on_stage: Optional[Callable[[str], None]] = None,
on_progress: Optional[Callable[[int], None]] = None,
use_new_format: bool = False,
) -> tuple:
"""提交 → 轮询 → 下载,返回 (本地视频路径, 末帧图片URL或None)"""
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
@@ -177,19 +187,30 @@ class SeedanceClient:
check_interrupt()
if on_stage:
on_stage("submitting")
task_id = await self.submit_async(body, session)
task_id = await self.submit_async(body, session, use_new_format=use_new_format)
print(f"[Seedance] 任务已提交 → {task_id}")
if on_stage:
on_stage(f"submitted:{task_id}")
# 轮询
video_url, last_frame_url = await self.poll_async(task_id, session, on_progress=on_progress)
video_url, last_frame_url = await self.poll_async(task_id, session, on_progress=on_progress, use_new_format=use_new_format)
# 下载
# 下载(带"Video not ready"重试)
if on_stage:
on_stage("downloading")
path = await self.download_async(video_url, save_path, session)
if on_stage:
on_stage("done")
return path, last_frame_url
max_retries = 5
retry_delay = 3.0
for attempt in range(max_retries):
try:
path = await self.download_async(video_url, save_path, session)
if on_stage:
on_stage("done")
return path, last_frame_url
except Exception as e:
error_msg = str(e)
if "Video not ready" in error_msg and attempt < max_retries - 1:
print(f"[Seedance] 视频未就绪,{retry_delay}秒后重试 ({attempt + 1}/{max_retries})...")
await interruptible_sleep(retry_delay)
check_interrupt()
continue
raise