Files
comfyui_o1key/tests/test_seedance_parameter_sync.py
T
Jony ba920f2b66 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.
2026-09-24 19:56:48 +08:00

295 lines
13 KiB
Python

"""Seedance 真人素材与多模态节点参数同步测试。"""
import sys
import types
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, patch
PACKAGE_DIR = Path(__file__).resolve().parents[1]
COMFY_ROOT = PACKAGE_DIR.parent.parent
sys.path.insert(0, str(COMFY_ROOT))
sys.path.insert(0, str(PACKAGE_DIR.parent))
# Load only the focused modules. Importing the plugin-wide registry would make
# this offline schema test depend on the portable build's CUDA availability.
package = types.ModuleType("comfyui_o1key")
package.__path__ = [str(PACKAGE_DIR)]
sys.modules["comfyui_o1key"] = package
for child in ("nodes", "utils", "clients"):
module = types.ModuleType(f"comfyui_o1key.{child}")
module.__path__ = [str(PACKAGE_DIR / child)]
sys.modules[f"comfyui_o1key.{child}"] = module
from comfyui_o1key.nodes import seedance_element
from comfyui_o1key.nodes.seedance_element import SeedanceElementCreate
from comfyui_o1key.nodes import seedance_video
from comfyui_o1key.nodes.seedance_video import (
SeedanceMultiModal,
_check_fast_resolution,
_is_new_format_model,
_resolve_model_matrix,
)
from comfy_api.latest import InputImpl, io
from comfy_api.latest._io import build_nested_inputs, get_finalized_class_inputs
class SeedanceParameterSyncTests(unittest.TestCase):
def test_element_schema_uses_hc_without_image_url(self):
inputs = SeedanceElementCreate.INPUT_TYPES()
self.assertEqual(inputs["required"]["素材名称"][1]["default"], "")
self.assertEqual(
inputs["required"]["请求模式"],
(["HC", "Doubao"], {"default": "HC"}),
)
self.assertNotIn("图片链接", inputs["optional"])
self.assertEqual(
set(inputs["optional"]),
{"照片", "视频", "音频", "素材描述"},
)
def test_multimodal_model_and_route_match_latest_options(self):
inputs = {
input_def.id: input_def
for input_def in SeedanceMultiModal.GET_SCHEMA().inputs
}
self.assertEqual(inputs["主模型"].options, [
"seedance 2.0",
"seedance 2.0 fast",
"seedance 2.0 mini",
"seedance 2.5",
])
self.assertEqual(inputs["模型线路"].options, ["海外", "国内"])
self.assertEqual(inputs["模型线路"].default, "国内")
self.assertEqual(inputs["分辨率"].options, ["720p", "1080p", "4k", "480p"])
self.assertEqual(
inputs["宽高比"].options,
["智能", "16:9", "9:16", "4:3", "3:4", "1:1", "21:9"],
)
self.assertIn("30秒", inputs["时长"].options)
self.assertEqual(inputs["seed"].max, 0xffffffffffffffff)
self.assertEqual(
_resolve_model_matrix("seedance 2.5", "海外HC"),
"dreamina-seedance-2-5-hc",
)
domestic_models = {
"seedance 2.0": "doubao-seedance-2-0-260128-max",
"seedance 2.0 fast": "doubao-seedance-2-0-fast-260128-max",
"seedance 2.0 mini": "doubao-seedance-2-0-mini-260615-max",
"seedance 2.5": "doubao-seedance-2-5-260628-max",
}
for base_model, expected_model in domestic_models.items():
with self.subTest(base_model=base_model):
resolved_model = _resolve_model_matrix(base_model, "国内")
self.assertEqual(resolved_model, expected_model)
self.assertTrue(_is_new_format_model(resolved_model))
self.assertTrue(_is_new_format_model("dreamina-seedance-2-0-hc"))
self.assertTrue(_is_new_format_model("dreamina-seedance-2-5-hc"))
_check_fast_resolution("dreamina-seedance-2-5-hc", "1080p")
_check_fast_resolution("dreamina-seedance-2-5-hc", "4k")
with self.assertRaisesRegex(ValueError, "不支持 4k"):
_check_fast_resolution("dreamina-seedance-2-0-fast-hc", "4k")
for limited_model in (
"doubao-seedance-2-0-fast-260128-max",
"doubao-seedance-2-0-mini-260615-max",
):
with self.subTest(limited_model=limited_model):
with self.assertRaisesRegex(ValueError, "不支持 4k"):
_check_fast_resolution(limited_model, "4k")
_check_fast_resolution("doubao-seedance-2-5-260628-max", "4k")
def test_multimodal_uses_v3_autogrow_media_inputs(self):
self.assertTrue(issubclass(SeedanceMultiModal, io.ComfyNode))
schema = SeedanceMultiModal.GET_SCHEMA()
inputs = {input_def.id: input_def for input_def in schema.inputs}
expected = {
"参考图片": (30, [f"参考图片{i}" for i in range(1, 31)]),
"参考视频": (10, [f"参考视频{i}" for i in range(1, 11)]),
"参考音频": (10, [f"参考音频{i}" for i in range(1, 11)]),
}
for group_name, (maximum, names) in expected.items():
input_def = inputs[group_name]
self.assertEqual(input_def.io_type, "COMFY_AUTOGROW_V3")
self.assertTrue(input_def.optional)
self.assertEqual(input_def.template.min, 0)
self.assertEqual(len(input_def.template.names), maximum)
self.assertEqual(input_def.template.names, names)
self.assertEqual(
[input_def.id for input_def in schema.inputs[-50:]],
[
*[f"图片素材ID{i}" for i in range(1, 10)],
*[f"视频素材ID{i}" for i in range(1, 4)],
*[f"音频素材ID{i}" for i in range(1, 4)],
*[f"图片素材ID{i}" for i in range(10, 31)],
*[f"视频素材ID{i}" for i in range(4, 11)],
*[f"音频素材ID{i}" for i in range(4, 11)],
],
)
self.assertEqual(SeedanceMultiModal.FUNCTION, "EXECUTE_NORMALIZED_ASYNC")
self.assertEqual(SeedanceMultiModal.RETURN_TYPES, ["VIDEO", "IMAGE"])
self.assertEqual(SeedanceMultiModal.RETURN_NAMES, ["视频", "末帧图片"])
def test_multimodal_autogrow_values_keep_order_and_accept_legacy_slots(self):
first = object()
third = object()
self.assertEqual(
SeedanceMultiModal._autogrow_values(
{"参考图片": {"参考图片1": first, "参考图片2": None, "参考图片3": third}},
"参考图片",
"参考图片",
30,
),
[first, third],
)
self.assertEqual(
SeedanceMultiModal._autogrow_values(
{"参考视频1": [first], "参考视频2": [], "参考视频3": [third]},
"参考视频",
"参考视频",
10,
),
[first, third],
)
def test_comfyui_builds_multimodal_autogrow_groups(self):
live_inputs = {
"提示词": "prompt",
"参考图片.参考图片1": "image-1",
"参考视频.参考视频1": "video-1",
"参考音频.参考音频1": "audio-1",
}
_, _, v3_data = get_finalized_class_inputs(
SeedanceMultiModal.INPUT_TYPES(),
live_inputs,
)
nested = build_nested_inputs(live_inputs, v3_data)
self.assertEqual(nested["参考图片"], {"参考图片1": "image-1"})
self.assertEqual(nested["参考视频"], {"参考视频1": "video-1"})
self.assertEqual(nested["参考音频"], {"参考音频1": "audio-1"})
class SeedanceParameterValidationTests(unittest.IsolatedAsyncioTestCase):
async def test_element_accepts_new_and_legacy_image_names(self):
fake_image = type("FakeImage", (), {"mode": "RGB"})()
for input_name in ("照片", "真人照片"):
with self.subTest(input_name=input_name):
fake_client = type("FakeSeedanceElementClient", (), {})()
fake_client.create_hc_asset_and_wait = AsyncMock(return_value={
"Id": "asset-doubao",
"_create_response": {"success": True},
})
with (
patch.object(seedance_element, "tensor_to_pil", return_value=[fake_image]),
patch.object(
seedance_element,
"upload_image",
new=AsyncMock(return_value="https://upload.example.com/person.png"),
),
patch.object(
seedance_element,
"get_base_url_by_route",
return_value="https://api.example.com",
),
patch.object(
seedance_element,
"SeedanceElementClient",
return_value=fake_client,
),
patch("builtins.print") as print_mock,
):
result = await SeedanceElementCreate().create_element(**{
"素材名称": "豆包真人素材",
"请求模式": "Doubao",
input_name: object(),
})
self.assertEqual(result[1], "asset-doubao")
fake_client.create_hc_asset_and_wait.assert_awaited_once_with(
name="豆包真人素材",
asset_url="https://upload.example.com/person.png",
asset_type="Image",
request_type="doubao",
)
logged_text = "\n".join(
" ".join(str(part) for part in call.args)
for call in print_mock.call_args_list
)
self.assertNotIn("https://upload.example.com/person.png", logged_text)
async def test_domestic_route_submits_exact_model_ids(self):
domestic_models = {
"seedance 2.0": "doubao-seedance-2-0-260128-max",
"seedance 2.0 fast": "doubao-seedance-2-0-fast-260128-max",
"seedance 2.0 mini": "doubao-seedance-2-0-mini-260615-max",
"seedance 2.5": "doubao-seedance-2-5-260628-max",
}
for base_model, expected_model in domestic_models.items():
with self.subTest(base_model=base_model):
fake_client = type("FakeSeedanceClient", (), {})()
fake_client.base_url = ""
fake_client.generate_async = AsyncMock(
return_value=("result.mp4", None)
)
with (
patch.object(seedance_video, "SeedanceClient", return_value=fake_client),
patch.object(seedance_video, "_show_balance"),
patch.object(
seedance_video.tempfile,
"mkstemp",
return_value=(0, "unused.mp4"),
),
patch.object(InputImpl, "VideoFromFile", return_value=object()),
patch.object(
seedance_video,
"get_base_url_by_route",
return_value="https://api.example.com",
),
):
await SeedanceMultiModal.generate(**{
"提示词": "test prompt",
"主模型": base_model,
"模型线路": "国内",
"分辨率": "720p",
"宽高比": "16:9",
"时长": "5秒",
"生成音频": "关闭",
"联网搜索": "关闭",
"返回末帧图片": "关闭",
"seed": 0,
})
request = fake_client.generate_async.await_args.kwargs
self.assertEqual(request["body"]["model"], expected_model)
self.assertTrue(request["use_new_format"])
async def test_multimodal_combines_direct_media_and_asset_ids_for_limits(self):
images = {f"参考图片{i}": object() for i in range(1, 31)}
for id_name in ("图片素材ID1", "真人素材ID1"):
with self.subTest(id_name=id_name):
with self.assertRaisesRegex(ValueError, "最多支持 30 个参考图片"):
await SeedanceMultiModal.generate(**{
"提示词": "test prompt",
"主模型": "seedance 2.5",
"模型线路": "海外HC",
"分辨率": "4k",
"宽高比": "16:9",
"时长": "30秒",
"生成音频": "关闭",
"联网搜索": "关闭",
"返回末帧图片": "关闭",
"seed": 0,
"参考图片": images,
id_name: "asset-image-1",
})
if __name__ == "__main__":
unittest.main()