Update video node to support more parameters and make it more dynamic
This commit is contained in:
@@ -39,6 +39,29 @@ def _install_fake_comfy_api():
|
||||
class Float(_InputFactory):
|
||||
pass
|
||||
|
||||
class Int(_InputFactory):
|
||||
pass
|
||||
|
||||
class Combo(_InputFactory):
|
||||
pass
|
||||
|
||||
class _DynamicComboOption:
|
||||
def __init__(self, key, inputs):
|
||||
self.key = key
|
||||
self.inputs = inputs
|
||||
|
||||
class _DynamicComboInputSpec(_InputSpec):
|
||||
def __init__(self, name, options=None, **kwargs):
|
||||
super().__init__(name, options=options, **kwargs)
|
||||
self.options = options or []
|
||||
|
||||
class DynamicCombo:
|
||||
Option = _DynamicComboOption
|
||||
|
||||
@staticmethod
|
||||
def Input(id, options=None, **kwargs):
|
||||
return _DynamicComboInputSpec(id, options=options, **kwargs)
|
||||
|
||||
class Video(_OutputFactory):
|
||||
pass
|
||||
|
||||
@@ -59,6 +82,9 @@ def _install_fake_comfy_api():
|
||||
io = types.SimpleNamespace(
|
||||
String=String,
|
||||
Float=Float,
|
||||
Int=Int,
|
||||
Combo=Combo,
|
||||
DynamicCombo=DynamicCombo,
|
||||
Video=Video,
|
||||
Hidden=Hidden,
|
||||
Schema=Schema,
|
||||
|
||||
+99
-4
@@ -14,6 +14,24 @@ class FakeVideoObject:
|
||||
self.error = error
|
||||
|
||||
|
||||
def _gemini_source(model="veo-3.1-fast-generate-preview", **overrides):
|
||||
settings = {
|
||||
"model": model,
|
||||
"size": "16:9",
|
||||
"resolution": "1K",
|
||||
"seconds": 8,
|
||||
"person_generation": "allow_all",
|
||||
}
|
||||
settings.update(overrides)
|
||||
return {"source": "text", "provider": {"provider": "gemini", "model": settings}}
|
||||
|
||||
|
||||
def _openai_source(model="sora-2", **overrides):
|
||||
settings = {"model": model, "size": "16:9", "resolution": "1K", "seconds": 4}
|
||||
settings.update(overrides)
|
||||
return {"source": "text", "provider": {"provider": "openai", "model": settings}}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _hidden():
|
||||
nodes.APIVideoGenerate.hidden = types.SimpleNamespace(unique_id="test-node")
|
||||
@@ -35,8 +53,8 @@ def test_generate_success_polls_until_completed():
|
||||
"asyncio.sleep", AsyncMock(return_value=None)
|
||||
):
|
||||
result = await nodes.APIVideoGenerate.execute(
|
||||
model="gemini/veo-3.1-fast-generate-preview",
|
||||
prompt="a cat riding a bike",
|
||||
source=_gemini_source(),
|
||||
)
|
||||
|
||||
assert isinstance(result, nodes.io.NodeOutput)
|
||||
@@ -45,6 +63,11 @@ def test_generate_success_polls_until_completed():
|
||||
assert video.file_obj.getvalue() == b"fake-mp4-bytes"
|
||||
assert status_mock.call_count == 2
|
||||
gen_mock.assert_awaited_once()
|
||||
assert gen_mock.call_args.kwargs["model"] == "gemini/veo-3.1-fast-generate-preview"
|
||||
assert gen_mock.call_args.kwargs["aspectRatio"] == "16:9"
|
||||
assert gen_mock.call_args.kwargs["resolution"] == "720p"
|
||||
assert gen_mock.call_args.kwargs["durationSeconds"] == 8
|
||||
assert gen_mock.call_args.kwargs["personGeneration"] == "allow_all"
|
||||
assert "api_key" not in gen_mock.call_args.kwargs
|
||||
assert "api_key" not in status_mock.call_args.kwargs
|
||||
assert "api_key" not in content_mock.call_args.kwargs
|
||||
@@ -64,11 +87,14 @@ def test_api_key_threaded_through_every_call():
|
||||
"asyncio.sleep", AsyncMock(return_value=None)
|
||||
):
|
||||
await nodes.APIVideoGenerate.execute(
|
||||
model="openai/sora-2",
|
||||
prompt="a dog surfing",
|
||||
source=_openai_source(),
|
||||
api_key="sk-test-123",
|
||||
)
|
||||
|
||||
assert gen_mock.call_args.kwargs["model"] == "openai/sora-2"
|
||||
assert gen_mock.call_args.kwargs["size"] == "1280x720"
|
||||
assert gen_mock.call_args.kwargs["seconds"] == "4"
|
||||
assert gen_mock.call_args.kwargs["api_key"] == "sk-test-123"
|
||||
assert content_mock.call_args.kwargs["api_key"] == "sk-test-123"
|
||||
status_mock.assert_not_awaited() # already completed, no polling needed
|
||||
@@ -91,8 +117,8 @@ def test_failed_generation_raises_runtime_error():
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="content filtered"):
|
||||
await nodes.APIVideoGenerate.execute(
|
||||
model="gemini/veo-3.1-fast-generate-preview",
|
||||
prompt="anything",
|
||||
source=_gemini_source(),
|
||||
)
|
||||
|
||||
content_mock.assert_not_awaited()
|
||||
@@ -110,10 +136,79 @@ def test_timeout_raises_when_generation_never_completes():
|
||||
), patch("asyncio.sleep", AsyncMock(return_value=None)):
|
||||
with pytest.raises(TimeoutError):
|
||||
await nodes.APIVideoGenerate.execute(
|
||||
model="gemini/veo-3.1-fast-generate-preview",
|
||||
prompt="anything",
|
||||
source=_gemini_source(),
|
||||
poll_interval=5.0,
|
||||
timeout=12.0,
|
||||
)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_runwayml_kwargs_mapping():
|
||||
async def run():
|
||||
gen_mock = AsyncMock(return_value=FakeVideoObject("completed"))
|
||||
content_mock = AsyncMock(return_value=b"bytes")
|
||||
|
||||
source = {
|
||||
"source": "text",
|
||||
"provider": {
|
||||
"provider": "runwayml",
|
||||
"model": {
|
||||
"model": "gen3a_turbo",
|
||||
"size": "9:16",
|
||||
"resolution": "2K",
|
||||
"seconds": 10,
|
||||
"seed": 42,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
with patch.object(nodes, "avideo_generation", gen_mock), patch.object(
|
||||
nodes, "avideo_content", content_mock
|
||||
), patch("asyncio.sleep", AsyncMock(return_value=None)):
|
||||
await nodes.APIVideoGenerate.execute(prompt="anything", source=source)
|
||||
|
||||
assert gen_mock.call_args.kwargs["model"] == "runwayml/gen3a_turbo"
|
||||
assert gen_mock.call_args.kwargs["ratio"] == "1080:1920"
|
||||
assert gen_mock.call_args.kwargs["duration"] == 10
|
||||
assert gen_mock.call_args.kwargs["seed"] == 42
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_schema_builds_nested_source_provider_model_combo():
|
||||
schema = nodes.APIVideoGenerate.define_schema()
|
||||
source_input = next(i for i in schema.inputs if i.name == "source")
|
||||
assert [o.key for o in source_input.options] == ["text"]
|
||||
|
||||
provider_input = source_input.options[0].inputs[0]
|
||||
assert provider_input.name == "provider"
|
||||
provider_keys = [o.key for o in provider_input.options]
|
||||
assert provider_keys == ["gemini", "openai", "azure", "runwayml"]
|
||||
|
||||
for option in provider_input.options:
|
||||
model_input = option.inputs[0]
|
||||
assert model_input.name == "model"
|
||||
assert len(model_input.options) > 0
|
||||
for model_option in model_input.options:
|
||||
field_names = {i.name for i in model_option.inputs}
|
||||
assert {"size", "resolution", "seconds"} <= field_names
|
||||
|
||||
|
||||
def test_azure_provider_string_uses_azure_prefix():
|
||||
async def run():
|
||||
gen_mock = AsyncMock(return_value=FakeVideoObject("completed"))
|
||||
content_mock = AsyncMock(return_value=b"bytes")
|
||||
|
||||
source = _openai_source(model="sora-2-pro")
|
||||
source["provider"]["provider"] = "azure"
|
||||
|
||||
with patch.object(nodes, "avideo_generation", gen_mock), patch.object(
|
||||
nodes, "avideo_content", content_mock
|
||||
), patch("asyncio.sleep", AsyncMock(return_value=None)):
|
||||
await nodes.APIVideoGenerate.execute(prompt="anything", source=source)
|
||||
|
||||
assert gen_mock.call_args.kwargs["model"] == "azure/sora-2-pro"
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
Reference in New Issue
Block a user