import tempfile import unittest from pathlib import Path from unittest.mock import patch from uuid import uuid4 import tools.seedance as seedance_module from tools.seedance import SeedanceTool class _FakeArkClient: submitted_body = None def __init__(self, *_args, **_kwargs): pass def __enter__(self): return self def __exit__(self, *_args): return False def post_json(self, _endpoint, body, **_kwargs): type(self).submitted_body = body return {"id": "cgt-test"} def get_json(self, _url, **_kwargs): return { "status": "succeeded", "content": {"video_url": "https://example.com/result.mp4"}, } def download(self, _url, dest, **_kwargs): Path(dest).write_bytes(b"video") class SeedanceToolTest(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.root = Path(self._tmp.name) / "user" self.root.mkdir() _FakeArkClient.submitted_body = None def tearDown(self): self._tmp.cleanup() def make_tool(self) -> SeedanceTool: return SeedanceTool( ark_cfg=object(), video_variant_cfg={ "model_id": "doubao-seedance-test", "default_resolution": "720p", "default_ratio": "16:9", "default_duration": 5, "default_watermark": False, "default_generate_audio": False, "price_cny_per_mtoken_text2video": 37.0, "price_cny_per_mtoken_video2video": 22.0, "fps": 24, "poll_interval_s": 0, }, variant_key="seedance_test", working_dir=self.root, user_root=self.root, task_id=uuid4(), user_id=uuid4(), ) def test_image_to_video_builds_first_frame_and_uses_i2v_price(self): (self.root / "source.png").write_bytes(b"\x89PNG\r\n\x1a\n") usage = {} with ( patch.object(seedance_module, "ArkClient", _FakeArkClient), patch.object( seedance_module, "record_usage_safe", side_effect=lambda _name, _fn, **kw: usage.update(kw), ), ): result = self.make_tool().execute(prompt="让水流动起来", image="source.png") content = _FakeArkClient.submitted_body["content"] self.assertEqual(content[0], {"type": "text", "text": "让水流动起来"}) self.assertEqual(content[1]["type"], "image_url") self.assertEqual(content[1]["role"], "first_frame") self.assertTrue(content[1]["image_url"]["url"].startswith("data:image/png;base64,")) self.assertIs(usage["has_video_input"], True) self.assertEqual(usage["price_cny_per_mtoken"], 22.0) self.assertIn("mode=image_to_video", result) self.assertIn("image=source.png", result) def test_text_to_video_remains_backward_compatible(self): usage = {} with ( patch.object(seedance_module, "ArkClient", _FakeArkClient), patch.object( seedance_module, "record_usage_safe", side_effect=lambda _name, _fn, **kw: usage.update(kw), ), ): result = self.make_tool().execute(prompt="水泥浆缓慢流动") self.assertEqual( _FakeArkClient.submitted_body["content"], [{"type": "text", "text": "水泥浆缓慢流动"}], ) self.assertIs(usage["has_video_input"], False) self.assertEqual(usage["price_cny_per_mtoken"], 37.0) self.assertIn("mode=text_to_video", result) def test_multi_image_references_use_reference_role(self): for name in ("product-front.png", "product-side.png"): (self.root / name).write_bytes(b"\x89PNG\r\n\x1a\n") usage = {} with ( patch.object(seedance_module, "ArkClient", _FakeArkClient), patch.object( seedance_module, "record_usage_safe", side_effect=lambda _name, _fn, **kw: usage.update(kw), ), ): result = self.make_tool().execute( prompt="参考图片1和图片2生成产品环绕展示", reference_images=["product-front.png", "product-side.png"], ) content = _FakeArkClient.submitted_body["content"] self.assertEqual([part.get("role") for part in content[1:]], [ "reference_image", "reference_image", ]) self.assertEqual( usage["extra_units"]["reference_images"], ["product-front.png", "product-side.png"], ) self.assertIn("mode=image_to_video", result) self.assertIn("images=2", result) def test_rejects_more_than_nine_images(self): result = self.make_tool().execute( prompt="多图参考", reference_images=[f"image-{i}.png" for i in range(10)], ) self.assertEqual( result, "[Error] Seedance 最多支持 9 张输入图片(image + reference_images 合计)。", ) def test_rejects_image_outside_user_root(self): outside = self.root.parent / "outside.png" outside.write_bytes(b"\x89PNG\r\n\x1a\n") result = self.make_tool().execute(prompt="动起来", image=str(outside)) self.assertTrue(result.startswith("[Error] 图片找不到或越界:")) if __name__ == "__main__": unittest.main()