Auto-generated nodes for the entire live fal catalog (1,391 models → 1,482 total nodes) built at startup from a committed registry, plus a full rewrite of the core layer. All 87 v1 node keys, inputs and outputs are preserved — existing workflows load and run unchanged. New: - data/fal_registry.json + scripts/build_registry.py: distills fal's catalog and per-endpoint OpenAPI schemas into a deterministic registry - nodes/dynamic/: schema→node factory (typed widgets, tooltips, pricing in node help, IMAGE/VIDEO/AUDIO auto-upload, native VIDEO/AUDIO outputs, seed -1=random, force_rerun), plus a generic Fal Any Endpoint node for arbitrary endpoints - nodes/utils/: rewritten core — FalApiError surfacing fal's real error payloads, queue progress logs, ComfyUI cancel support, retries and timeouts everywhere, parallel uploads/downloads, structured logging - tests/ (45 tests incl. a legacy-key snapshot lock), CI lint+test workflow, weekly registry-refresh workflow that opens a PR on changes - README regenerated from the registry (scripts/build_readme.py) Fixed: - GPTImage2 / GPTImage2Edit were defined but never registered - Topaz video upscale sent a nonexistent API field (desired_increase) - SeedVR video upscaler had its error handling commented out + wrong model label; ProRes enum value mismatch translated at argument level - 10x `is ""` comparisons, bare excepts, missing HTTP timeouts, temp-file leaks, fps=0 crash in LoadVideoURL, duplicate enum entry - opencv-python & friends missing from pyproject (Registry installs) - API failures now raise visible errors instead of silently returning blank images or "Error:" strings that downstream nodes tried to load BREAKING (behavioral only): failed API calls raise instead of returning blank/black images or error strings. Node signatures are unchanged.
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""Unit tests for the schema→INPUT_TYPES converter."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from helpers import _input, _model
|
|
|
|
|
|
def test_required_and_optional_buckets(schema_to_inputs):
|
|
model = _model([
|
|
_input("prompt", "string", required=True, multiline=True),
|
|
_input("guidance", "number", default=3.5, min=1, max=20),
|
|
])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
assert "prompt" in it["required"]
|
|
assert "guidance" in it["optional"]
|
|
assert it["required"]["prompt"][0] == "STRING"
|
|
assert it["required"]["prompt"][1]["multiline"] is True
|
|
|
|
|
|
def test_enum_becomes_dropdown(schema_to_inputs):
|
|
model = _model([_input("style", "enum", enum=["a", "b"], default="b")])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
spec = it["optional"]["style"]
|
|
assert spec[0] == ["a", "b"]
|
|
assert spec[1]["default"] == "b"
|
|
|
|
|
|
def test_int_range_and_default_clamp(schema_to_inputs):
|
|
model = _model([_input("steps", "integer", default=28, min=1, max=50)])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
typ, opts = it["optional"]["steps"]
|
|
assert typ == "INT"
|
|
assert opts["min"] == 1 and opts["max"] == 50 and opts["default"] == 28
|
|
|
|
|
|
def test_seed_spec(schema_to_inputs):
|
|
model = _model([_input("seed", "integer", required=True)])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
# seed is always optional regardless of the API marking it required
|
|
typ, opts = it["optional"]["seed"]
|
|
assert typ == "INT"
|
|
assert opts["default"] == -1
|
|
assert opts["min"] == -1
|
|
assert opts.get("control_after_generate") is True
|
|
|
|
|
|
def test_media_inputs(schema_to_inputs):
|
|
model = _model([
|
|
_input("image_url", "string", required=True, media_kind="image"),
|
|
_input("video_url", "string", media_kind="video"),
|
|
_input("audio_url", "string", media_kind="audio"),
|
|
])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
assert it["required"]["image_url"][0] == "IMAGE"
|
|
assert it["optional"]["video_url"][0] == "VIDEO"
|
|
assert it["optional"]["audio_url"][0] == "AUDIO"
|
|
|
|
|
|
def test_custom_size_companions(schema_to_inputs):
|
|
model = _model([
|
|
_input(
|
|
"image_size", "enum",
|
|
enum=["square", "landscape_4_3", "custom_size"],
|
|
default="landscape_4_3", has_custom_size=True,
|
|
)
|
|
])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
assert "width" in it["optional"] and "height" in it["optional"]
|
|
assert it["optional"]["width"][0] == "INT"
|
|
|
|
|
|
def test_dict_default_maps_to_custom_size(schema_to_inputs):
|
|
model = _model([
|
|
_input(
|
|
"image_size", "enum",
|
|
enum=["square", "custom_size"],
|
|
default={"width": 2048, "height": 1536},
|
|
has_custom_size=True,
|
|
)
|
|
])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
assert it["optional"]["image_size"][1]["default"] == "custom_size"
|
|
assert it["optional"]["width"][1]["default"] == 2048
|
|
assert it["optional"]["height"][1]["default"] == 1536
|
|
|
|
|
|
def test_multi_select_enum_is_comma_string(schema_to_inputs):
|
|
model = _model([
|
|
_input("stems", "enum", enum=["vocals", "drums", "bass"],
|
|
default=["vocals", "drums"], is_list=True)
|
|
])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
typ, opts = it["optional"]["stems"]
|
|
assert typ == "STRING"
|
|
assert opts["default"] == "vocals, drums"
|
|
assert "vocals, drums, bass" in opts["tooltip"]
|
|
|
|
|
|
def test_json_field_is_multiline_string(schema_to_inputs):
|
|
model = _model([_input("loras", "json")])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
typ, opts = it["optional"]["loras"]
|
|
assert typ == "STRING"
|
|
assert opts["multiline"] is True
|
|
|
|
|
|
def test_force_rerun_always_present(schema_to_inputs):
|
|
it = schema_to_inputs.build_input_types(_model([]))
|
|
typ, opts = it["optional"]["force_rerun"]
|
|
assert typ == "BOOLEAN"
|
|
assert opts["default"] is False
|
|
|
|
|
|
def test_every_input_has_tooltip_when_description_given(schema_to_inputs):
|
|
model = _model([_input("prompt", "string", required=True, description="What to draw")])
|
|
it = schema_to_inputs.build_input_types(model)
|
|
assert it["required"]["prompt"][1]["tooltip"] == "What to draw"
|