"""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()