166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
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()
|