149 lines
5.6 KiB
Python
149 lines
5.6 KiB
Python
"""Regenerate the model lists in README.md and MODELS.md from data/catalog-snapshot.json.
|
|
|
|
The counts and names in the README come from here and nowhere else, so they cannot drift from
|
|
the nodes the package actually ships. Run after update_snapshot.py:
|
|
|
|
python scripts/render_docs.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from spicyapi_nodes.fields import model_page, node_id_for # noqa: E402
|
|
from spicyapi_nodes.pricing import badge_for, format_usd # noqa: E402
|
|
|
|
MODALITIES = (("video", "Video"), ("image", "Image"), ("audio", "Audio"))
|
|
|
|
|
|
def load() -> list[dict]:
|
|
return json.loads((ROOT / "data" / "catalog-snapshot.json").read_text())["items"]
|
|
|
|
|
|
def media_models(items: list[dict]) -> list[dict]:
|
|
return [
|
|
m
|
|
for m in items
|
|
if m.get("enabled") is not False and m.get("modality") != "text" and "chat" not in (m.get("tasks") or [])
|
|
]
|
|
|
|
|
|
def chat_models(items: list[dict]) -> list[dict]:
|
|
return [
|
|
m
|
|
for m in items
|
|
if m.get("enabled") is not False and (m.get("modality") == "text" or "chat" in (m.get("tasks") or []))
|
|
]
|
|
|
|
|
|
def provider_name(model: dict) -> str:
|
|
provider = str(model.get("provider") or "Other")
|
|
return "SpicyAPI tools" if provider.lower() == "spicyapi" else provider
|
|
|
|
|
|
def families_by_provider(models: list[dict]) -> OrderedDict[str, OrderedDict[str, str]]:
|
|
grouped: OrderedDict[str, OrderedDict[str, str]] = OrderedDict()
|
|
for model in models:
|
|
family = str(model.get("familyDisplayName") or model.get("displayName") or model["model"])
|
|
grouped.setdefault(provider_name(model), OrderedDict()).setdefault(family, model_page(model))
|
|
return grouped
|
|
|
|
|
|
def stats(items: list[dict]) -> dict[str, int]:
|
|
media = media_models(items)
|
|
counts = {key: sum(1 for m in media if m.get("modality") == key) for key, _ in MODALITIES}
|
|
return {
|
|
"nodes": len(media),
|
|
"families": len({m.get("familyDisplayName") for m in media}),
|
|
"chat": len(chat_models(items)),
|
|
**counts,
|
|
}
|
|
|
|
|
|
def readme_block(items: list[dict]) -> str:
|
|
media = media_models(items)
|
|
lines: list[str] = []
|
|
for key, title in MODALITIES:
|
|
subset = [m for m in media if m.get("modality") == key]
|
|
if not subset:
|
|
continue
|
|
lines.append(f"### {title} ({len(subset)} nodes)")
|
|
lines.append("")
|
|
lines.append("| Maker | Models |")
|
|
lines.append("| --- | --- |")
|
|
for provider, families in families_by_provider(subset).items():
|
|
links = ", ".join(f"[{name}]({url})" for name, url in families.items())
|
|
lines.append(f"| {provider} | {links} |")
|
|
lines.append("")
|
|
chats = chat_models(items)
|
|
if chats:
|
|
lines.append(f"### Text ({len(chats)} models in the SpicyAPI Chat node)")
|
|
lines.append("")
|
|
names = list(OrderedDict.fromkeys(str(m.get("familyDisplayName") or m["model"]) for m in chats))
|
|
lines.append(", ".join(names) + ".")
|
|
lines.append("")
|
|
lines.append("Every node with its model ID and starting price: [MODELS.md](MODELS.md).")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def models_md(items: list[dict]) -> str:
|
|
s = stats(items)
|
|
out = [
|
|
"# Models available as ComfyUI nodes",
|
|
"",
|
|
f"{s['nodes']} SpicyAPI models, one ComfyUI node each: {s['video']} video, {s['image']} image and "
|
|
f"{s['audio']} audio. Text models ({s['chat']}) are picked inside the **SpicyAPI Chat** node.",
|
|
"",
|
|
"The list is generated from the model catalogue bundled with this release. ComfyUI loads the live "
|
|
"catalogue at startup once an API key is set, so models added since then appear after a restart.",
|
|
"Prices are starting prices; the badge on each node shows the rate for the options you pick.",
|
|
"",
|
|
]
|
|
for key, title in MODALITIES:
|
|
subset = [m for m in media_models(items) if m.get("modality") == key]
|
|
if not subset:
|
|
continue
|
|
out += [f"## {title}", "", "| Node | Model ID | From | Node type |", "| --- | --- | --- | --- |"]
|
|
for model in sorted(subset, key=lambda m: (provider_name(m), str(m.get("displayName")))):
|
|
badge = badge_for(model)
|
|
price = f"{format_usd(badge['from'])} / {badge['unit']}" if badge else ""
|
|
name = str(model.get("displayName") or model["model"]).replace("|", "/")
|
|
out.append(
|
|
f"| [{name}]({model_page(model)}) | `{model['model']}` | {price} | `{node_id_for(model['model'])}` |"
|
|
)
|
|
out.append("")
|
|
return "\n".join(out)
|
|
|
|
|
|
def replace_between(text: str, marker: str, body: str) -> str:
|
|
pattern = re.compile(rf"(<!-- {marker}:start -->\n).*?(<!-- {marker}:end -->)", re.S)
|
|
if not pattern.search(text):
|
|
raise SystemExit(f"README.md has no {marker} markers")
|
|
return pattern.sub(lambda m: m.group(1) + body.rstrip("\n") + "\n" + m.group(2), text)
|
|
|
|
|
|
def main() -> None:
|
|
items = load()
|
|
s = stats(items)
|
|
readme = (ROOT / "README.md").read_text()
|
|
readme = replace_between(readme, "models", readme_block(items))
|
|
summary = (
|
|
f"**{s['nodes']} model nodes** ({s['video']} video, {s['image']} image, {s['audio']} audio) "
|
|
f"from {s['families']} model families, plus {s['chat']} text models in one Chat node."
|
|
)
|
|
readme = replace_between(readme, "stats", summary)
|
|
(ROOT / "README.md").write_text(readme)
|
|
(ROOT / "MODELS.md").write_text(models_md(items))
|
|
print(summary)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|