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
+182
View File
@@ -0,0 +1,182 @@
"""Omni Flash video generation through a normal ComfyUI execution."""
from __future__ import annotations
import io as py_io
import os
import uuid
from pathlib import Path
import folder_paths
from comfy_api.latest import InputImpl, io
from ..clients.omni_flash_client import OmniFlashClient, build_video_body
from ..utils.config import get_api_key_or_raise, get_base_url_by_route
from ..utils.image_utils import tensor_to_pil
from ..utils.r2_uploader import upload_image, upload_video
from ..utils.video_task import check_interrupt
_MODES = {
"文生视频": "text",
"参考图视频": "reference",
"首尾帧": "first_last_frame",
"视频编辑": "edit",
}
_MAX_EDIT_VIDEO_BYTES = 20 * 1024 * 1024
def _make_progress_callback():
try:
from comfy.utils import ProgressBar
bar = ProgressBar(100)
except ImportError:
bar = None
last_progress = -1
def update(stage: str, value: int, _task_id: str) -> None:
nonlocal last_progress
# The provider percentage maps directly to the node bar. Reserve the
# final point for the local video download and save.
current = max(0, min(100, int(value)))
if stage != "done":
current = min(current, 99)
if current <= last_progress:
return
last_progress = current
if bar is not None:
bar.update_absolute(current, 100)
return update
class O1keyOmniFlashVideo(io.ComfyNode):
"""One graph node owns validation, submission, polling, and VIDEO output."""
@classmethod
def fingerprint_inputs(cls, **kwargs):
"""Do not reuse a completed generation when this node is queued again."""
return uuid.uuid4().hex
@classmethod
def define_schema(cls):
return io.Schema(
node_id="O1keyOmniFlashVideo",
display_name="Omni Flash 视频生成",
description="连接图片或视频后生成;开始生成按钮运行当前节点。",
category="comfyui_o1key/视频",
is_output_node=True,
not_idempotent=True,
inputs=[
io.String.Input("提示词", multiline=True, default=""),
io.Combo.Input("生成模式", options=list(_MODES), default="文生视频"),
io.Combo.Input("分辨率", options=["720p", "1080p"], default="720p"),
io.Combo.Input("宽高比", options=["16:9", "9:16"], default="16:9"),
io.Autogrow.Input(
"参考图片",
optional=True,
template=io.Autogrow.TemplateNames(
input=io.Image.Input("参考图片"),
names=[f"参考图片{i}" for i in range(1, 6)],
min=0,
),
),
io.Image.Input("首帧图片", optional=True),
io.Image.Input("尾帧图片", optional=True, tooltip="可不连接;只连接首帧也能生成。"),
io.Video.Input("源视频", optional=True),
],
outputs=[io.Video.Output("VIDEO", display_name="视频")],
)
@staticmethod
def _reference_images(kwargs):
group = kwargs.get("参考图片")
if isinstance(group, dict):
return [value for value in group.values() if value is not None]
return [kwargs[f"参考图片{index}"] for index in range(1, 6)
if kwargs.get(f"参考图片{index}") is not None]
@staticmethod
def _one_image(value, label):
images = tensor_to_pil(value)
if len(images) != 1:
raise ValueError(f"{label}必须恰好包含 1 张图片")
return images[0].convert("RGB")
@staticmethod
def _check_source_video(video):
source = video.get_stream_source() if hasattr(video, "get_stream_source") else None
if isinstance(source, py_io.BytesIO):
size = source.getbuffer().nbytes
elif isinstance(source, str) and os.path.isfile(source):
size = os.path.getsize(source)
if Path(source).suffix.lower() not in {".mp4", ".mov"}:
raise ValueError("源视频须为 MP4 或 MOV")
else:
raise ValueError("无法读取源视频文件")
if size > _MAX_EDIT_VIDEO_BYTES:
raise ValueError("源视频不能超过 20 MB")
@classmethod
async def execute(cls, **kwargs):
mode = _MODES.get(kwargs.get("生成模式"))
if mode is None:
raise ValueError("生成模式无效")
images = cls._reference_images(kwargs)
first = kwargs.get("首帧图片")
last = kwargs.get("尾帧图片")
source = kwargs.get("源视频")
if mode == "text" and (images or first is not None or last is not None or source is not None):
raise ValueError("文生视频模式不接受媒体输入")
if mode == "reference" and (not images or first is not None or last is not None or source is not None):
raise ValueError("参考图视频模式只接受参考图片")
if mode == "first_last_frame" and (images or first is None or source is not None):
raise ValueError("首尾帧模式须连接首帧图片;尾帧图片可选")
if mode == "edit" and (source is None or first is not None or last is not None):
raise ValueError("视频编辑模式须连接源视频,不能连接首尾帧")
if mode == "edit" and len(images) > 5:
raise ValueError("视频编辑最多支持 5 张参考图")
model = "omni_flash_abra_edit" if mode == "edit" else "omni_flash_10s"
prompt = kwargs["提示词"]
resolution = kwargs["分辨率"]
ratio = kwargs["宽高比"]
# Validate the scalar contract before any upload or paid request.
upload_images = ([first] + ([last] if last is not None else [])) if mode == "first_last_frame" else images
build_video_body(model=model, prompt=prompt, resolution=resolution,
aspect_ratio=ratio, mode=mode,
references=["https://example.invalid/media"] * len(upload_images),
source_video_url="https://example.invalid/video" if mode == "edit" else "")
pil_images = [cls._one_image(value, "输入") for value in upload_images]
if source is not None:
cls._check_source_video(source)
api_key = get_api_key_or_raise("O1KEY_API_KEY")
base_url = get_base_url_by_route()
urls = []
for image in pil_images:
check_interrupt()
urls.append(await upload_image(image, base_url=base_url))
source_url = await upload_video(source, base_url=base_url) if source is not None else ""
body = build_video_body(model=model, prompt=prompt, resolution=resolution,
aspect_ratio=ratio, mode=mode, references=urls,
source_video_url=source_url)
output_dir = Path(folder_paths.get_output_directory()) / "omni_flash"
output_dir.mkdir(parents=True, exist_ok=True)
filename = f"{uuid.uuid4().hex}.mp4"
target = output_dir / filename
partial = output_dir / f"{filename}.part"
report_progress = _make_progress_callback()
report_progress("polling", 0, "")
try:
await OmniFlashClient(base_url=base_url, api_key=api_key).generate(
body, str(partial), progress=report_progress,
)
check_interrupt()
os.replace(partial, target)
report_progress("done", 100, "")
except BaseException:
partial.unlink(missing_ok=True)
raise
return io.NodeOutput(InputImpl.VideoFromFile(str(target)))