From 5ee93a27eecf1bfaf784c1f46b596d03f2a44d85 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sat, 8 Nov 2025 19:08:55 +0800 Subject: [PATCH] feat: add license flags display to model preview tooltip #613 - Add optional license_flags parameter to model preview API endpoint - Include license flags in response when requested via query parameter - Add CSS styles for license overlay and icons in tooltip - Implement license flag parsing and icon mapping logic - Display license restrictions as icons in preview tooltip overlay This allows users to see model license restrictions directly in the preview tooltip without needing to navigate to detailed model information pages. --- py/routes/handlers/model_handlers.py | 9 ++- web/comfyui/lm_styles.css | 26 +++++++ web/comfyui/preview_tooltip.js | 100 ++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 7268e0ca..dd79e099 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -659,9 +659,16 @@ class ModelQueryHandler: model_name = request.query.get("name") if not model_name: return web.Response(text=f"{self._service.model_type.capitalize()} file name is required", status=400) + include_license_flags = (request.query.get("license_flags", "").strip().lower() in {"1", "true", "yes", "on"}) preview_url = await self._service.get_model_preview_url(model_name) if preview_url: - return web.json_response({"success": True, "preview_url": preview_url}) + response_payload: dict[str, object] = {"success": True, "preview_url": preview_url} + if include_license_flags: + model_data = await self._service.get_model_info_by_name(model_name) + license_flags = (model_data or {}).get("license_flags") + if license_flags is not None: + response_payload["license_flags"] = int(license_flags) + return web.json_response(response_payload) return web.json_response({"success": False, "error": f"No preview URL found for the specified {self._service.model_type}"}, status=404) except Exception as exc: self._logger.error("Error getting %s preview URL: %s", self._service.model_type, exc, exc_info=True) diff --git a/web/comfyui/lm_styles.css b/web/comfyui/lm_styles.css index 119b3fdb..4c7b2641 100644 --- a/web/comfyui/lm_styles.css +++ b/web/comfyui/lm_styles.css @@ -43,6 +43,32 @@ -webkit-backdrop-filter: blur(4px); } +.lm-tooltip__license-overlay { + position: absolute; + top: 8px; + left: 8px; + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 4px 8px; + border-radius: 999px; + background: rgba(10, 10, 14, 0.78); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + max-width: calc(100% - 16px); +} + +.lm-tooltip__license-icon { + width: 18px; + height: 18px; + display: inline-block; + background-color: rgba(255, 255, 255, 0.9); + -webkit-mask: var(--license-icon-image) center/contain no-repeat; + mask: var(--license-icon-image) center/contain no-repeat; +} + .lm-loras-container { display: flex; flex-direction: column; diff --git a/web/comfyui/preview_tooltip.js b/web/comfyui/preview_tooltip.js index 02c1376a..fdc211b8 100644 --- a/web/comfyui/preview_tooltip.js +++ b/web/comfyui/preview_tooltip.js @@ -1,6 +1,83 @@ import { api } from "../../scripts/api.js"; import { ensureLmStyles } from "./lm_styles_loader.js"; +const LICENSE_ICON_PATH = "/loras_static/images/tabler/"; +const LICENSE_FLAG_BITS = { + allowNoCredit: 1 << 0, + allowOnImages: 1 << 1, + allowOnCivitai: 1 << 2, + allowRental: 1 << 3, + allowSellingModels: 1 << 4, + allowDerivatives: 1 << 5, + allowRelicense: 1 << 6, +}; + +const LICENSE_ICON_COPY = { + credit: "Creator credit required", + image: "No selling generated content", + rentcivit: "No Civitai generation", + rent: "No generation services", + sell: "No selling models", + derivatives: "No sharing merges", + relicense: "Same permissions required", +}; + +const COMMERCIAL_ICON_CONFIG = [ + { bit: LICENSE_FLAG_BITS.allowOnImages, icon: "photo-off.svg", label: LICENSE_ICON_COPY.image }, + { bit: LICENSE_FLAG_BITS.allowOnCivitai, icon: "brush-off.svg", label: LICENSE_ICON_COPY.rentcivit }, + { bit: LICENSE_FLAG_BITS.allowRental, icon: "world-off.svg", label: LICENSE_ICON_COPY.rent }, + { bit: LICENSE_FLAG_BITS.allowSellingModels, icon: "shopping-cart-off.svg", label: LICENSE_ICON_COPY.sell }, +]; + +function parseLicenseFlags(value) { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; +} + +function buildLicenseIconData(licenseFlags) { + if (licenseFlags == null) { + return []; + } + + const icons = []; + + if ((licenseFlags & LICENSE_FLAG_BITS.allowNoCredit) === 0) { + icons.push({ icon: "user-check.svg", label: LICENSE_ICON_COPY.credit }); + } + + COMMERCIAL_ICON_CONFIG.forEach((config) => { + if ((licenseFlags & config.bit) === 0) { + icons.push({ icon: config.icon, label: config.label }); + } + }); + + if ((licenseFlags & LICENSE_FLAG_BITS.allowDerivatives) === 0) { + icons.push({ icon: "exchange-off.svg", label: LICENSE_ICON_COPY.derivatives }); + } + + if ((licenseFlags & LICENSE_FLAG_BITS.allowRelicense) === 0) { + icons.push({ icon: "rotate-2.svg", label: LICENSE_ICON_COPY.relicense }); + } + + return icons; +} + +function createLicenseIconElement({ icon, label }) { + const element = document.createElement("span"); + element.className = "lm-tooltip__license-icon"; + element.setAttribute("role", "img"); + element.setAttribute("aria-label", label); + element.title = label; + element.style.setProperty("--license-icon-image", `url('${LICENSE_ICON_PATH}${icon}')`); + return element; +} + /** * Lightweight preview tooltip that can display images or videos for different model types. */ @@ -44,7 +121,7 @@ export class PreviewTooltip { async defaultPreviewUrlResolver(modelName) { const response = await api.fetchApi( - `/lm/${this.modelType}/preview-url?name=${encodeURIComponent(modelName)}`, + `/lm/${this.modelType}/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`, { method: "GET" } ); if (!response.ok) { @@ -57,6 +134,7 @@ export class PreviewTooltip { return { previewUrl: data.preview_url, displayName: data.display_name ?? modelName, + licenseFlags: parseLicenseFlags(data.license_flags), }; } @@ -72,7 +150,7 @@ export class PreviewTooltip { }; } - const { previewUrl, displayName } = raw; + const { previewUrl, displayName, licenseFlags } = raw; if (!previewUrl) { throw new Error("No preview URL available"); } @@ -82,6 +160,7 @@ export class PreviewTooltip { displayName !== undefined ? displayName : this.displayNameFormatter(modelName), + licenseFlags: parseLicenseFlags(licenseFlags), }; } @@ -103,7 +182,7 @@ export class PreviewTooltip { } this.currentModelName = modelName; - const { previewUrl, displayName } = await this.resolvePreviewData( + const { previewUrl, displayName, licenseFlags } = await this.resolvePreviewData( modelName ); @@ -132,6 +211,7 @@ export class PreviewTooltip { nameLabel.className = "lm-tooltip__label"; mediaContainer.appendChild(mediaElement); + this.renderLicenseOverlay(mediaContainer, licenseFlags); mediaContainer.appendChild(nameLabel); this.element.appendChild(mediaContainer); @@ -213,6 +293,20 @@ export class PreviewTooltip { } } + renderLicenseOverlay(container, licenseFlags) { + const icons = buildLicenseIconData(licenseFlags); + if (!icons.length) { + return; + } + + const overlay = document.createElement("div"); + overlay.className = "lm-tooltip__license-overlay"; + icons.forEach((descriptor) => { + overlay.appendChild(createLicenseIconElement(descriptor)); + }); + container.appendChild(overlay); + } + cleanup() { if (this.hideTimeout) { clearTimeout(this.hideTimeout);