185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from spicyapi_nodes.fields import (
|
|
MODEL_DEFAULT,
|
|
OMIT,
|
|
OMIT_NUMBER,
|
|
build_fields,
|
|
category_for,
|
|
node_id_for,
|
|
plan_outputs,
|
|
widget_to_value,
|
|
)
|
|
|
|
SNAPSHOT = Path(__file__).resolve().parent.parent / "data" / "catalog-snapshot.json"
|
|
KINDS = {
|
|
"image",
|
|
"images",
|
|
"video",
|
|
"videos",
|
|
"audio",
|
|
"audios",
|
|
"file",
|
|
"string",
|
|
"combo",
|
|
"int",
|
|
"float",
|
|
"bool",
|
|
"loras",
|
|
"json",
|
|
"int_socket",
|
|
"float_socket",
|
|
}
|
|
|
|
|
|
def load_models():
|
|
items = json.loads(SNAPSHOT.read_text())["items"]
|
|
return {item["model"]: item for item in items}
|
|
|
|
|
|
def fields_of(schema):
|
|
return {spec.name: spec for spec in build_fields(schema)}
|
|
|
|
|
|
class SnapshotMappingTest(unittest.TestCase):
|
|
def test_every_media_model_maps(self):
|
|
models = load_models()
|
|
media = [m for m in models.values() if m.get("modality") != "text"]
|
|
self.assertGreater(len(media), 100, "the snapshot should hold the whole catalogue")
|
|
for model in media:
|
|
with self.subTest(model=model["model"]):
|
|
specs = build_fields(model["inputSchema"])
|
|
self.assertTrue(specs)
|
|
for spec in specs:
|
|
self.assertIn(spec.kind, KINDS)
|
|
if spec.kind == "combo":
|
|
self.assertIn(spec.default, spec.options)
|
|
self.assertEqual(len(spec.options), len(spec.option_values))
|
|
if spec.kind in ("int", "float") and spec.minimum is not None:
|
|
self.assertGreaterEqual(spec.default, spec.minimum)
|
|
if spec.kind in ("int", "float") and spec.maximum is not None:
|
|
self.assertLessEqual(spec.default, spec.maximum)
|
|
|
|
def test_node_ids_are_unique(self):
|
|
ids = [node_id_for(m) for m in load_models()]
|
|
self.assertEqual(len(ids), len(set(ids)))
|
|
|
|
def test_order_follows_x_ui_order(self):
|
|
model = load_models()["bytedance/seedance-2.5/reference-to-video"]
|
|
orders = [spec.order for spec in build_fields(model["inputSchema"])]
|
|
self.assertEqual(orders, sorted(orders, reverse=True))
|
|
|
|
|
|
class FieldMappingTest(unittest.TestCase):
|
|
def test_reference_uploads_become_socket_groups(self):
|
|
specs = fields_of(load_models()["bytedance/seedance-2.5/reference-to-video"]["inputSchema"])
|
|
self.assertEqual(specs["reference_image_urls"].kind, "images")
|
|
self.assertEqual(specs["reference_video_urls"].kind, "videos")
|
|
self.assertEqual(specs["reference_audio_urls"].kind, "audios")
|
|
self.assertFalse(specs["reference_image_urls"].required)
|
|
self.assertLessEqual(specs["reference_image_urls"].max_items, 16)
|
|
|
|
def test_contiguous_duration_enum_is_a_slider(self):
|
|
duration = fields_of(load_models()["bytedance/seedance-2.5/reference-to-video"]["inputSchema"])[
|
|
"duration_seconds"
|
|
]
|
|
self.assertEqual(duration.kind, "int")
|
|
self.assertEqual((duration.minimum, duration.maximum, duration.default), (4, 30, 5))
|
|
|
|
def test_labelled_enum_shows_the_label_and_sends_the_value(self):
|
|
duration = fields_of(load_models()["bytedance/seedance-2.0-mini/text-to-video"]["inputSchema"])[
|
|
"duration_seconds"
|
|
]
|
|
self.assertEqual(duration.kind, "combo")
|
|
self.assertIn("Smart", duration.options)
|
|
self.assertEqual(widget_to_value(duration, "Smart"), -1)
|
|
self.assertEqual(widget_to_value(duration, "8"), 8)
|
|
|
|
def test_seed_is_always_sent(self):
|
|
seed = fields_of(load_models()["alibaba/z-image-turbo-lora/text-to-image"]["inputSchema"])["seed"]
|
|
self.assertTrue(seed.seed)
|
|
self.assertEqual(widget_to_value(seed, 0), 0)
|
|
|
|
def test_lora_lists_get_their_own_socket(self):
|
|
specs = fields_of(load_models()["alibaba/z-image-turbo-lora/text-to-image"]["inputSchema"])
|
|
self.assertEqual(specs["loras"].kind, "loras")
|
|
|
|
def test_optional_number_without_default_is_omitted_at_minus_one(self):
|
|
schema = {"properties": {"weight": {"type": "number", "minimum": 0, "maximum": 1}}}
|
|
spec = fields_of(schema)["weight"]
|
|
self.assertEqual(spec.default, OMIT_NUMBER)
|
|
self.assertIs(widget_to_value(spec, -1), OMIT)
|
|
self.assertEqual(widget_to_value(spec, 0.4), 0.4)
|
|
|
|
def test_unbounded_optional_number_is_socket_only(self):
|
|
spec = fields_of({"properties": {"shift": {"type": "integer"}}})["shift"]
|
|
self.assertEqual(spec.kind, "int_socket")
|
|
self.assertIs(widget_to_value(spec, None), OMIT)
|
|
|
|
def test_switch_without_default_has_three_states(self):
|
|
spec = fields_of({"properties": {"flag": {"type": "boolean"}}})["flag"]
|
|
self.assertEqual(spec.options, [MODEL_DEFAULT, "true", "false"])
|
|
self.assertIs(widget_to_value(spec, MODEL_DEFAULT), OMIT)
|
|
self.assertIs(widget_to_value(spec, "false"), False)
|
|
|
|
def test_optional_enum_without_default_can_be_left_out(self):
|
|
spec = fields_of({"properties": {"style": {"type": "string", "enum": ["a", "b"]}}})["style"]
|
|
self.assertEqual(spec.default, MODEL_DEFAULT)
|
|
self.assertIs(widget_to_value(spec, MODEL_DEFAULT), OMIT)
|
|
self.assertEqual(widget_to_value(spec, "b"), "b")
|
|
|
|
def test_strings(self):
|
|
schema = {
|
|
"required": ["prompt"],
|
|
"properties": {
|
|
"prompt": {"type": "string", "x-ui": {"widget": "textarea"}},
|
|
"title": {"type": "string", "x-ui": {"widget": "text"}},
|
|
},
|
|
}
|
|
specs = fields_of(schema)
|
|
self.assertTrue(specs["prompt"].multiline)
|
|
self.assertIs(widget_to_value(specs["title"], " "), OMIT)
|
|
self.assertEqual(widget_to_value(specs["prompt"], ""), "")
|
|
|
|
def test_json_fields_parse_and_reject_bad_json(self):
|
|
schema = {
|
|
"properties": {
|
|
"dialogue": {
|
|
"type": "array",
|
|
"x-ui": {"widget": "object-list"},
|
|
"items": {"type": "object", "properties": {"text": {"type": "string"}}},
|
|
}
|
|
}
|
|
}
|
|
spec = fields_of(schema)["dialogue"]
|
|
self.assertEqual(spec.kind, "json")
|
|
self.assertIs(widget_to_value(spec, ""), OMIT)
|
|
self.assertEqual(widget_to_value(spec, '[{"text": "hi"}]'), [{"text": "hi"}])
|
|
with self.assertRaises(ValueError):
|
|
widget_to_value(spec, "[{")
|
|
|
|
def test_hidden_fields_are_skipped(self):
|
|
self.assertEqual(fields_of({"properties": {"x": {"type": "string", "x-ui": {"widget": "hidden"}}}}), {})
|
|
|
|
|
|
class OutputPlanTest(unittest.TestCase):
|
|
def test_plans(self):
|
|
models = load_models()
|
|
self.assertEqual(plan_outputs(models["xai/grok-stt/speech-to-text"]).primary, "text")
|
|
self.assertEqual(plan_outputs(models["spicyapi/character-swap-v1/video-analyze"]).primary, "previews")
|
|
video = plan_outputs(models["bytedance/seedance-2.5/reference-to-video"])
|
|
self.assertEqual((video.primary, video.last_frame), ("video", True))
|
|
self.assertTrue(plan_outputs(models["spicyapi/background-remover-v1/edit"]).alpha)
|
|
self.assertEqual(plan_outputs(models["suno/v6/text-to-audio"]).primary, "audio")
|
|
|
|
def test_category(self):
|
|
models = load_models()
|
|
self.assertEqual(category_for(models["bytedance/seedance-2.5/reference-to-video"]), "SpicyAPI/Video/ByteDance")
|
|
self.assertEqual(category_for(models["spicyapi/background-remover-v1/edit"]), "SpicyAPI/Image/Tools")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|