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.
73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
"""Unit tests for result→ComfyUI-outputs mapping (media decode stubbed)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from helpers import _model
|
|
|
|
_VIDEO_SENTINEL = object()
|
|
_AUDIO_SENTINEL = {"waveform": "stub", "sample_rate": 44100}
|
|
|
|
|
|
class _FakeMediaUtils:
|
|
@staticmethod
|
|
def video_from_url(_url):
|
|
return _VIDEO_SENTINEL
|
|
|
|
@staticmethod
|
|
def audio_from_url(_url):
|
|
return _AUDIO_SENTINEL
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _stub_media(monkeypatch, outputs_mod):
|
|
monkeypatch.setattr(outputs_mod, "MediaUtils", _FakeMediaUtils)
|
|
|
|
|
|
def test_return_specs_cover_all_kinds(outputs_mod):
|
|
assert set(outputs_mod.RETURN_SPECS) >= {
|
|
"images", "image", "video", "audio", "text", "file", "json",
|
|
}
|
|
for types, names in outputs_mod.RETURN_SPECS.values():
|
|
assert len(types) == len(names)
|
|
|
|
|
|
def test_video_result(outputs_mod):
|
|
model = _model([], output_kind="video", output_props=["video"])
|
|
result = {"video": {"url": "https://fal.media/v.mp4"}}
|
|
out = outputs_mod.process_result(model, result)
|
|
assert out == (_VIDEO_SENTINEL, "https://fal.media/v.mp4")
|
|
|
|
|
|
def test_audio_result(outputs_mod):
|
|
model = _model([], output_kind="audio", output_props=["audio"])
|
|
result = {"audio": {"url": "https://fal.media/a.mp3"}}
|
|
out = outputs_mod.process_result(model, result)
|
|
assert out == (_AUDIO_SENTINEL, "https://fal.media/a.mp3")
|
|
|
|
|
|
def test_text_result(outputs_mod):
|
|
model = _model([], output_kind="text", output_props=["text"])
|
|
assert outputs_mod.process_result(model, {"text": "hello"}) == ("hello",)
|
|
|
|
|
|
def test_file_result_digs_url(outputs_mod):
|
|
model = _model([], output_kind="file", output_props=["model_glb"])
|
|
result = {"model_glb": {"url": "https://fal.media/m.glb"}}
|
|
assert outputs_mod.process_result(model, result) == ("https://fal.media/m.glb",)
|
|
|
|
|
|
def test_json_fallback(outputs_mod):
|
|
model = _model([], output_kind="json", output_props=[])
|
|
result = {"anything": [1, 2, 3]}
|
|
(payload,) = outputs_mod.process_result(model, result)
|
|
assert json.loads(payload) == result
|
|
|
|
|
|
def test_find_url_recursive(outputs_mod):
|
|
nested = {"a": [{"b": {"url": "https://x/y.bin"}}]}
|
|
assert outputs_mod.find_url(nested) == "https://x/y.bin"
|
|
assert outputs_mod.find_url({"no": "url here"}) is None
|