Files
Gökay Aydoğan b75f731abf feat: v2.0.0 — full fal catalog coverage, dynamic nodes, core rewrite (#75)
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.
2026-07-02 12:46:22 +03:00

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