feat(metadata): add OpenModelDB metadata provider and model source for upscalers

Add OpenModelDB (openmodeldb.info) as a metadata and download source for
the existing upscaler model type.

Metadata:
- New OpenModelDBClient: fetches the site's bulk JSON dumps, caches them
  on disk (24h TTL + ETag revalidation), and builds a local sha256 index
- New OpenModelDBModelMetadataProvider adapts catalogue entries to the
  CivitAI-shaped version dict contract; registered in the fallback chain
  behind the enable_openmodeldb_api setting (default on), gated to the
  upscaler sub-type so other model types never trigger the dump download
- Persisted provenance uses metadata_source "openmodeldb" plus a nested
  openmodeldb block (page URL, architecture, scale, license)

Images: paired-image LR/SR URLs are ephemeral imgdiff.net sessions, so
displayable images come from the site-hosted auto-generated thumbnails
(model-level cover leads images[], per-image thumbs for the rest); the
original comparison URL is kept in meta.comparisonUrl.

Downloads:
- New OpenModelDBSource (flat model ids, omdb: group prefix) with
  resource filename derivation that recovers names hidden mid-path
  (mediafire) or synthesizes {id}.{type} for folder links
- HTML-gateway mirrors (mediafire/mega/drive) are rejected with a clear
  manual-download hint instead of silently saving an HTML page as .pth
- ModelSource base gains is_valid_source_id / default_subdir_parts /
  resolve_download_url hooks so flat-id sources need no platform branches

UI: "View on OpenModelDB" link in the model modal (downloaded and
hash-enriched models), settings toggle next to the CivArchive one.
This commit is contained in:
Will Miao
2026-10-03 21:13:07 +08:00
parent 515469054c
commit 35f1ced41a
39 changed files with 2787 additions and 37 deletions
+1
View File
@@ -1807,6 +1807,7 @@ class SettingsHandler:
if key in (
"enable_metadata_archive_db",
"enable_civarchive_api",
"enable_openmodeldb_api",
"metadata_provider_order",
):
await self._metadata_provider_updater()
+13 -14
View File
@@ -31,7 +31,6 @@ from ...services.model_sources import (
detect_source,
get_download_source,
hydrate_from_source,
is_valid_source_id,
list_sources,
normalize_metadata_source,
)
@@ -277,9 +276,7 @@ class ModelSourceHandler:
"supports_enrichment": source.supports_enrichment,
"supports_download": source.supports_download,
"default_revision": source.default_revision,
"example_url": source.canonical_url(
"user/repo" if source.platform != "tensorart" else "827823520299086029"
),
"example_url": source.canonical_url(source.example_source_id),
}
for source in list_sources()
])
@@ -326,9 +323,7 @@ class ModelSourceHandler:
"error": (
"Unsupported model URL. Supported formats: "
+ ", ".join(
f"{s.label} ({s.canonical_url('user/repo')})"
if s.platform != "tensorart"
else f"{s.label} (https://tensor.art/models/<id>)"
f"{s.label} ({s.canonical_url(s.example_source_id)})"
for s in list_sources()
)
),
@@ -424,9 +419,9 @@ class ModelSourceHandler:
source = get_download_source(platform)
if source is None:
return _unsupported_platform_error(platform)
if not is_valid_source_id(repo):
if not source.is_valid_source_id(repo):
return web.json_response(
{"error": "Missing or invalid 'repo' parameter (expected owner/name)"},
{"error": "Missing or invalid 'repo' parameter"},
status=400,
)
@@ -492,10 +487,11 @@ class ModelSourceHandler:
{"error": "Missing required fields: 'repo' and 'filename'"}, status=400
)
# `owner/name` only; the components become path segments below.
if not is_valid_source_id(repo):
# The id becomes a path segment below; each site defines what a safe
# id looks like (`owner/name` for repository sites, a flat token for
# OpenModelDB).
if not source.is_valid_source_id(repo):
return web.json_response({"error": f"Invalid repo format: {repo}"}, status=400)
owner, repo_name = repo.split("/", 1)
# Validate filename — must not contain path traversal
if ".." in filename:
@@ -521,7 +517,7 @@ class ModelSourceHandler:
base_dir = os.path.normpath(os.path.join(os.getcwd(), "models", model_root))
if use_default_paths:
target_dir = os.path.join(base_dir, source.default_subdir, owner, repo_name)
target_dir = os.path.join(base_dir, *source.default_subdir_parts(repo))
elif relative_path:
target_dir = os.path.join(base_dir, relative_path)
else:
@@ -536,7 +532,10 @@ class ModelSourceHandler:
# Built per request: sites that redirect to a CDN hand out a
# time-limited token in the redirect, so the URL must never be cached.
resolve_url = source.file_download_url(repo, filename, revision)
try:
resolve_url = await source.resolve_download_url(repo, filename, revision)
except ModelSourceError as exc:
return web.json_response({"error": str(exc)}, status=exc.status)
ref = SourceRef(
platform=source.platform, source_id=repo, url=source.canonical_url(repo)
)