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);