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.
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Regenerate the auto-generated model list section of README.md.
|
|
|
|
Reads data/fal_registry.json and rewrites ONLY the section between
|
|
`<!-- BEGIN GENERATED MODEL LIST -->` and `<!-- END GENERATED MODEL LIST -->`
|
|
in README.md. Everything outside the markers is left untouched, and running
|
|
the script twice in a row produces no diff.
|
|
|
|
Usage:
|
|
python scripts/build_readme.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
REGISTRY_PATH = REPO_ROOT / "data" / "fal_registry.json"
|
|
README_PATH = REPO_ROOT / "README.md"
|
|
|
|
BEGIN_MARKER = "<!-- BEGIN GENERATED MODEL LIST -->"
|
|
END_MARKER = "<!-- END GENERATED MODEL LIST -->"
|
|
|
|
MODEL_URL_TEMPLATE = "https://fal.ai/models/{endpoint_id}"
|
|
|
|
|
|
def load_registry(path: Path) -> dict[str, Any]:
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
registry = json.load(handle)
|
|
except (OSError, ValueError) as err:
|
|
raise SystemExit(f"Failed to read registry at {path}: {err}") from err
|
|
if not isinstance(registry.get("models"), list):
|
|
raise SystemExit(f"Registry at {path} has no 'models' list")
|
|
return registry
|
|
|
|
|
|
def escape_cell(text: str) -> str:
|
|
"""Make a value safe inside a markdown table cell."""
|
|
return " ".join(str(text).split()).replace("|", "\\|")
|
|
|
|
|
|
def group_by_category(
|
|
models: list[dict[str, Any]],
|
|
) -> list[tuple[str, list[dict[str, Any]]]]:
|
|
"""Group models by category, categories sorted by size desc then name."""
|
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
for model in models:
|
|
category = str(model.get("category") or "other")
|
|
grouped = {**grouped, category: [*grouped.get(category, []), model]}
|
|
return sorted(grouped.items(), key=lambda item: (-len(item[1]), item[0]))
|
|
|
|
|
|
def model_sort_key(model: dict[str, Any]) -> tuple[str, str]:
|
|
title = str(model.get("title") or model.get("endpoint_id") or "")
|
|
return (title.casefold(), str(model.get("endpoint_id") or ""))
|
|
|
|
|
|
def render_model_row(model: dict[str, Any]) -> str:
|
|
endpoint_id = str(model.get("endpoint_id") or "")
|
|
title = escape_cell(model.get("title") or endpoint_id)
|
|
lab = escape_cell(model.get("lab") or "—") or "—"
|
|
output = escape_cell(model.get("output_kind") or "json")
|
|
url = MODEL_URL_TEMPLATE.format(endpoint_id=endpoint_id)
|
|
endpoint_cell = f"[`{escape_cell(endpoint_id)}`]({url})"
|
|
return f"| {title} | {endpoint_cell} | {lab} | {output} |"
|
|
|
|
|
|
def render_category(category: str, models: list[dict[str, Any]]) -> str:
|
|
rows = [render_model_row(m) for m in sorted(models, key=model_sort_key)]
|
|
count = len(models)
|
|
noun = "model" if count == 1 else "models"
|
|
return "\n".join(
|
|
[
|
|
"<details>",
|
|
f"<summary><strong>{category}</strong> — {count} {noun}</summary>",
|
|
"",
|
|
"| Model | Endpoint | Lab | Output |",
|
|
"| --- | --- | --- | --- |",
|
|
*rows,
|
|
"",
|
|
"</details>",
|
|
]
|
|
)
|
|
|
|
|
|
def render_generated_section(registry: dict[str, Any]) -> str:
|
|
models = registry["models"]
|
|
model_count = registry.get("model_count", len(models))
|
|
published = [str(m.get("published_at", "")) for m in registry.get("models", [])]
|
|
generated_date = max(published)[:10] if any(published) else "unknown"
|
|
summary = (
|
|
f"{model_count} models · newest model {generated_date} · "
|
|
"refresh with `scripts/build_registry.py`"
|
|
)
|
|
blocks = [
|
|
render_category(category, grouped)
|
|
for category, grouped in group_by_category(models)
|
|
]
|
|
return "\n\n".join([summary, *blocks])
|
|
|
|
|
|
def replace_between_markers(readme: str, generated: str) -> str:
|
|
begin = readme.find(BEGIN_MARKER)
|
|
end = readme.find(END_MARKER)
|
|
if begin == -1 or end == -1 or end < begin:
|
|
raise SystemExit(
|
|
f"README.md must contain '{BEGIN_MARKER}' followed by '{END_MARKER}'"
|
|
)
|
|
head = readme[: begin + len(BEGIN_MARKER)]
|
|
tail = readme[end:]
|
|
return f"{head}\n\n{generated}\n\n{tail}"
|
|
|
|
|
|
def main() -> int:
|
|
registry = load_registry(REGISTRY_PATH)
|
|
try:
|
|
readme = README_PATH.read_text(encoding="utf-8")
|
|
except OSError as err:
|
|
raise SystemExit(f"Failed to read {README_PATH}: {err}") from err
|
|
|
|
updated = replace_between_markers(readme, render_generated_section(registry))
|
|
if updated == readme:
|
|
print(f"README.md already up to date ({registry.get('model_count')} models)")
|
|
return 0
|
|
|
|
README_PATH.write_text(updated, encoding="utf-8")
|
|
print(
|
|
f"README.md model list regenerated: {registry.get('model_count')} models, "
|
|
f"{len(group_by_category(registry['models']))} categories"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|