From f2df2bbefa9da96960ff891ff23b82cb7a4a027e Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Sun, 15 Feb 2026 18:38:39 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20Commit=20message:=20Update=20202?= =?UTF-8?q?6-02-15=2018:38:39,=202=20files,=20121=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📁 Files changed: 2 📝 Lines changed: 121 • README.md • civitai_routes.py --- README.md | 34 +++++++++++-- tensors/server/civitai_routes.py | 87 ++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 34547bc..7633dae 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,15 @@ tsr search -t lora -b sdxl # Sort by newest, limit results tsr search -t checkpoint -s newest -n 10 + +# Filter by tag and period +tsr search --tag anime -p week -b illustrious + +# By creator +tsr search -u "username" + +# SFW only with commercial use filter +tsr search --sfw --commercial sell ``` ### Get Model Info @@ -240,9 +249,10 @@ local = "http://localhost:51200" default_remote = "junkpile" ``` -Or set API key via environment variable: +Or set API keys via environment variables: ```bash -export CIVITAI_API_KEY="your-api-key" +export CIVITAI_API_KEY="your-api-key" # For CivitAI API access +export TENSORS_API_KEY="your-server-key" # For server authentication ``` ## Default Paths @@ -263,9 +273,16 @@ Data is stored in XDG-compliant paths: | Option | Values | |--------|--------| | `-t, --type` | checkpoint, lora, embedding, vae, controlnet, locon | -| `-b, --base` | sd15, sdxl, pony, flux, illustrious | +| `-b, --base` | sd14, sd15, sd2, sdxl, pony, flux, illustrious, noobai, auraflow | | `-s, --sort` | downloads, rating, newest | -| `-n, --limit` | Number of results (default: 20) | +| `-n, --limit` | Number of results (default: 25) | +| `-p, --period` | all, year, month, week, day | +| `--tag` | Filter by tag (e.g., "anime") | +| `-u, --user` | Filter by creator username | +| `--nsfw` | none, soft, mature, x | +| `--sfw` | Exclude NSFW content | +| `--commercial` | none, image, rent, sell | +| `--page` | Page number for pagination | ## Generate Options @@ -288,9 +305,16 @@ Data is stored in XDG-compliant paths: When running `tsr serve`, the following endpoints are available: +**OpenAPI Documentation:** Visit `/docs` for interactive Scalar API documentation. + +**Authentication:** If `TENSORS_API_KEY` is set, all endpoints except `/status` and `/docs` require authentication via: +- Header: `X-API-Key: your-key` +- Query param: `?api_key=your-key` + | Endpoint | Method | Description | |----------|--------|-------------| -| `/status` | GET | Server status and active model | +| `/status` | GET | Server status (public) | +| `/docs` | GET | OpenAPI documentation (public) | | `/reload` | POST | Hot-reload with new model | | `/api/images` | GET | List gallery images | | `/api/images/{id}` | GET | Get image file | diff --git a/tensors/server/civitai_routes.py b/tensors/server/civitai_routes.py index b3b8e6e..9b90ba3 100644 --- a/tensors/server/civitai_routes.py +++ b/tensors/server/civitai_routes.py @@ -3,7 +3,8 @@ from __future__ import annotations import logging -from typing import Any +from enum import Enum +from typing import Annotated, Any import httpx from fastapi import APIRouter, Query, Response @@ -17,6 +18,42 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/civitai", tags=["CivitAI"]) +class SortOrder(str, Enum): + """Sort order options for CivitAI search.""" + + most_downloaded = "Most Downloaded" + highest_rated = "Highest Rated" + newest = "Newest" + + +class Period(str, Enum): + """Time period filter options.""" + + all = "AllTime" + year = "Year" + month = "Month" + week = "Week" + day = "Day" + + +class NsfwLevel(str, Enum): + """NSFW content filter levels.""" + + none = "None" + soft = "Soft" + mature = "Mature" + x = "X" + + +class CommercialUse(str, Enum): + """Commercial use filter options.""" + + none = "None" + image = "Image" + rent = "Rent" + sell = "Sell" + + def _get_headers(api_key: str | None) -> dict[str, str]: """Get headers for CivitAI API requests.""" headers: dict[str, str] = {} @@ -27,28 +64,56 @@ def _get_headers(api_key: str | None) -> dict[str, str]: @router.get("/search", response_model=None) async def search_models( - query: str | None = Query(default=None, description="Search query"), - types: str | None = Query(default=None, description="Model type (Checkpoint, LORA, LoCon, etc.)"), - base_models: str | None = Query(default=None, alias="baseModels", description="Base model (SD 1.5, SDXL 1.0, Pony, etc.)"), - sort: str = Query(default="Most Downloaded", description="Sort order"), - limit: int = Query(default=20, le=100, description="Max results"), - nsfw: bool = Query(default=True, description="Include NSFW models"), + query: Annotated[str | None, Query(description="Search query")] = None, + types: Annotated[str | None, Query(description="Model type (Checkpoint, LORA, etc.)")] = None, + base_models: Annotated[str | None, Query(alias="baseModels", description="Base model")] = None, + sort: Annotated[SortOrder, Query(description="Sort order")] = SortOrder.most_downloaded, + limit: Annotated[int | None, Query(le=100, description="Max results (default: 25)", example=5)] = None, + period: Annotated[Period | None, Query(description="Time period filter")] = None, + tag: Annotated[str | None, Query(description="Filter by tag")] = None, + username: Annotated[str | None, Query(description="Filter by creator username")] = None, + page: Annotated[int | None, Query(ge=1, description="Page number")] = None, + nsfw: Annotated[NsfwLevel | None, Query(description="NSFW filter level")] = None, + sfw: Annotated[bool, Query(description="Exclude NSFW content")] = False, + commercial: Annotated[CommercialUse | None, Query(description="Commercial use filter")] = None, ) -> dict[str, Any] | Response: - """Search CivitAI models.""" + """Search CivitAI models. + + Supports all CivitAI search parameters including filters for type, base model, + time period, tags, creator, NSFW level, and commercial use. + """ api_key = load_api_key() + actual_limit = limit if limit is not None else 25 params: dict[str, Any] = { - "limit": min(limit, 100), - "nsfw": str(nsfw).lower(), - "sort": sort, + "limit": min(actual_limit, 100), + "sort": sort.value, } + # Handle NSFW filtering + if sfw: + params["nsfw"] = "false" + elif nsfw: + params["nsfwLevel"] = nsfw.value + else: + params["nsfw"] = "true" # Default: include all + if query: params["query"] = query if types: params["types"] = types if base_models: params["baseModels"] = base_models + if period: + params["period"] = period.value + if tag: + params["tag"] = tag + if username: + params["username"] = username + if page: + params["page"] = page + if commercial: + params["allowCommercialUse"] = commercial.value url = f"{CIVITAI_API_BASE}/models"