From d7caa1fa47ff44fe1b886d9924ec3df760b17f5d Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 26 May 2026 06:02:17 +0800 Subject: [PATCH] fix(license): remove cascading commercial-use bit encoding, clarify Allow Selling label (#941) - _resolve_commercial_bits() no longer has Sell-implies-Image cascading; each CommercialUse value sets only its own bit, matching CivitAI's modern array-format API. - Keep filter tag label as 'Allow Selling' for brevity; add title/tooltip 'Allow selling generated images' on hover. - Same tooltip treatment for 'No Credit Required'. - Add i18n keys for both tooltips across all 10 locales. --- locales/de.json | 2 + locales/en.json | 2 + locales/es.json | 2 + locales/fr.json | 2 + locales/he.json | 2 + locales/ja.json | 2 + locales/ko.json | 2 + locales/ru.json | 2 + locales/zh-CN.json | 2 + locales/zh-TW.json | 2 + py/utils/civitai_utils.py | 6 +- templates/components/header.html | 4 +- tests/services/test_license_filters.py | 20 ++--- tests/services/test_lora_pool_filters.py | 10 +-- tests/services/test_model_scanner.py | 6 +- tests/utils/test_civitai_utils.py | 17 ++-- .../lora-pool/sections/LicenseSection.vue | 4 +- .../vue-widgets/lora-manager-widgets.js | 90 +++++++++++++++---- .../vue-widgets/lora-manager-widgets.js.map | 2 +- 19 files changed, 122 insertions(+), 57 deletions(-) diff --git a/locales/de.json b/locales/de.json index 5f901b10..bf1974a5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -232,6 +232,8 @@ "license": "Lizenz", "noCreditRequired": "Kein Credit erforderlich", "allowSellingGeneratedContent": "Verkauf erlaubt", + "allowSellingGeneratedContentTooltip": "Verkauf generierter Bilder erlauben", + "noCreditRequiredTooltip": "Modell ohne Nennung des Erstellers verwenden", "noTags": "Keine Tags", "autoTags": "Auto-Tags", "noBaseModelMatches": "Keine Basismodelle entsprechen der aktuellen Suche.", diff --git a/locales/en.json b/locales/en.json index e1d837aa..b4255602 100644 --- a/locales/en.json +++ b/locales/en.json @@ -232,6 +232,8 @@ "license": "License", "noCreditRequired": "No Credit Required", "allowSellingGeneratedContent": "Allow Selling", + "allowSellingGeneratedContentTooltip": "Allow selling generated images", + "noCreditRequiredTooltip": "Use the model without crediting the creator", "noTags": "No tags", "autoTags": "Auto Tags", "noBaseModelMatches": "No base models match the current search.", diff --git a/locales/es.json b/locales/es.json index 7007a3c8..b8e53217 100644 --- a/locales/es.json +++ b/locales/es.json @@ -232,6 +232,8 @@ "license": "Licencia", "noCreditRequired": "Sin crédito requerido", "allowSellingGeneratedContent": "Venta permitida", + "allowSellingGeneratedContentTooltip": "Permitir la venta de imágenes generadas", + "noCreditRequiredTooltip": "Usar el modelo sin atribuir al creador", "noTags": "Sin etiquetas", "autoTags": "Etiquetas automáticas", "noBaseModelMatches": "Ningún modelo base coincide con la búsqueda actual.", diff --git a/locales/fr.json b/locales/fr.json index a9bfe5f8..c3fc3bd5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -232,6 +232,8 @@ "license": "Licence", "noCreditRequired": "Crédit non requis", "allowSellingGeneratedContent": "Vente autorisée", + "allowSellingGeneratedContentTooltip": "Autoriser la vente d\"images générées", + "noCreditRequiredTooltip": "Utiliser le modèle sans créditer le créateur", "noTags": "Aucun tag", "autoTags": "Auto-Tags", "noBaseModelMatches": "Aucun modèle de base ne correspond à la recherche actuelle.", diff --git a/locales/he.json b/locales/he.json index 6d187495..56da343c 100644 --- a/locales/he.json +++ b/locales/he.json @@ -232,6 +232,8 @@ "license": "רישיון", "noCreditRequired": "ללא קרדיט נדרש", "allowSellingGeneratedContent": "אפשר מכירה", + "allowSellingGeneratedContentTooltip": "אפשר מכירת תמונות שנוצרו", + "noCreditRequiredTooltip": "שימוש במודל ללא מתן קרדיט ליוצר", "noTags": "ללא תגיות", "autoTags": "תגיות אוטומטיות", "noBaseModelMatches": "אין מודלי בסיס התואמים לחיפוש הנוכחי.", diff --git a/locales/ja.json b/locales/ja.json index ef422633..145f6253 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -232,6 +232,8 @@ "license": "ライセンス", "noCreditRequired": "クレジット不要", "allowSellingGeneratedContent": "販売許可", + "allowSellingGeneratedContentTooltip": "生成した画像の販売を許可", + "noCreditRequiredTooltip": "クレジット表記なしでモデルを使用可能", "noTags": "タグなし", "autoTags": "自動タグ", "noBaseModelMatches": "現在の検索に一致するベースモデルはありません。", diff --git a/locales/ko.json b/locales/ko.json index 2cfeab8e..fbebd000 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -232,6 +232,8 @@ "license": "라이선스", "noCreditRequired": "크레딧 표기 없음", "allowSellingGeneratedContent": "판매 허용", + "allowSellingGeneratedContentTooltip": "생성된 이미지 판매 허용", + "noCreditRequiredTooltip": "크리에이터 저작자 표시 없이 모델 사용 가능", "noTags": "태그 없음", "autoTags": "자동 태그", "noBaseModelMatches": "현재 검색과 일치하는 베이스 모델이 없습니다.", diff --git a/locales/ru.json b/locales/ru.json index 40821803..43341ae1 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -232,6 +232,8 @@ "license": "Лицензия", "noCreditRequired": "Без указания авторства", "allowSellingGeneratedContent": "Продажа разрешена", + "allowSellingGeneratedContentTooltip": "Разрешить продажу сгенерированных изображений", + "noCreditRequiredTooltip": "Использование модели без указания автора", "noTags": "Без тегов", "autoTags": "Авто-теги", "noBaseModelMatches": "Нет базовых моделей, соответствующих текущему поиску.", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9003e42e..e9c92562 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -232,6 +232,8 @@ "license": "许可证", "noCreditRequired": "无需署名", "allowSellingGeneratedContent": "允许销售", + "allowSellingGeneratedContentTooltip": "允许出售生成的图片", + "noCreditRequiredTooltip": "使用模型时无需注明原作者", "noTags": "无标签", "autoTags": "自动标签", "noBaseModelMatches": "没有基础模型符合当前搜索。", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 30c9cff2..a601fc3c 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -232,6 +232,8 @@ "license": "授權", "noCreditRequired": "無需署名", "allowSellingGeneratedContent": "允許銷售", + "allowSellingGeneratedContentTooltip": "允許出售生成的圖片", + "noCreditRequiredTooltip": "使用模型時無需註明原作者", "noTags": "無標籤", "autoTags": "自動標籤", "noBaseModelMatches": "沒有基礎模型符合目前的搜尋。", diff --git a/py/utils/civitai_utils.py b/py/utils/civitai_utils.py index de711404..c9965473 100644 --- a/py/utils/civitai_utils.py +++ b/py/utils/civitai_utils.py @@ -239,9 +239,9 @@ def _resolve_commercial_bits(values: Sequence[str]) -> int: normalized_values.add(normalized) has_sell = "sell" in normalized_values - has_rent = has_sell or "rent" in normalized_values - has_rentcivit = has_rent or "rentcivit" in normalized_values - has_image = has_sell or "image" in normalized_values + has_rent = "rent" in normalized_values + has_rentcivit = "rentcivit" in normalized_values + has_image = "image" in normalized_values commercial_bits = ( (1 if has_sell else 0) << 3 diff --git a/templates/components/header.html b/templates/components/header.html index f7a51240..95d5a2ea 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -218,10 +218,10 @@

{{ t('header.filter.license') }}

-
+
{{ t('header.filter.noCreditRequired') }}
-
+
{{ t('header.filter.allowSellingGeneratedContent') }}
diff --git a/tests/services/test_license_filters.py b/tests/services/test_license_filters.py index 6419d88d..83b11f3d 100644 --- a/tests/services/test_license_filters.py +++ b/tests/services/test_license_filters.py @@ -65,32 +65,26 @@ async def test_allow_selling_filter(): """Test the allow selling generated content filtering logic.""" service = DummyModelService() - # Create test data with different license flags + # CommercialUse values are independent — Sell does NOT imply Image. test_data = [ - # Model allowing selling (contains Image in allowCommercialUse) {"file_path": "model1.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Image"]})}, - # Model not allowing selling (doesn't contain Image in allowCommercialUse) {"file_path": "model2.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["RentCivit"]})}, - # Model with default license flags (includes Sell by default, which implies Image) {"file_path": "model3.safetensors", "license_flags": build_license_flags(None)}, - # Model allowing selling (contains Sell in allowCommercialUse, which implies Image) {"file_path": "model4.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Sell"]})}, - # Model with empty allowCommercialUse (doesn't allow selling) {"file_path": "model5.safetensors", "license_flags": build_license_flags({"allowCommercialUse": []})}, ] - # Test allow_selling=True (should return models that allow selling - have Image permission) - # Default and Sell permissions both include Image, so model3 and model4 will be included + # Test allow_selling=True (should return only models with the Image permission) filtered = await service._apply_allow_selling_filter(test_data, allow_selling=True) - assert len(filtered) == 3 # model1, model3 (default includes Sell which implies Image), model4 + assert len(filtered) == 1 # only model1 has Image permission file_paths = {item["file_path"] for item in filtered} - assert file_paths == {"model1.safetensors", "model3.safetensors", "model4.safetensors"} + assert file_paths == {"model1.safetensors"} - # Test allow_selling=False (should return models that don't allow selling - don't have Image permission) + # Test allow_selling=False (should return models without the Image permission) filtered = await service._apply_allow_selling_filter(test_data, allow_selling=False) - assert len(filtered) == 2 # model2 and model5 + assert len(filtered) == 4 # model2, model3, model4, model5 file_paths = {item["file_path"] for item in filtered} - assert file_paths == {"model2.safetensors", "model5.safetensors"} + assert file_paths == {"model2.safetensors", "model3.safetensors", "model4.safetensors", "model5.safetensors"} @pytest.mark.asyncio diff --git a/tests/services/test_lora_pool_filters.py b/tests/services/test_lora_pool_filters.py index fde2a44a..6f9095c1 100644 --- a/tests/services/test_lora_pool_filters.py +++ b/tests/services/test_lora_pool_filters.py @@ -131,13 +131,12 @@ async def test_pool_filter_allow_selling_true(lora_service, sample_loras): filtered = await lora_service._apply_pool_filters(sample_loras, pool_config) # Should keep models with Image permission (allowSelling) - # Models: no_credit_required_for_selling, credit_required_for_selling, default_license - assert len(filtered) == 3 + # Sell alone does not imply Image, so default_license is excluded. + assert len(filtered) == 2 file_names = {lora["file_name"] for lora in filtered} assert file_names == { "no_credit_required_for_selling.safetensors", "credit_required_for_selling.safetensors", - "default_license.safetensors", } @@ -178,12 +177,11 @@ async def test_pool_filter_both_license_filters(lora_service, sample_loras): # Should keep models where both conditions are met: # - allowNoCredit=True (no credit required) # - Image permission exists (allow selling) - # Models: no_credit_required_for_selling, default_license - assert len(filtered) == 2 + # default_license has ["Sell"] without Image, so it's excluded. + assert len(filtered) == 1 file_names = {lora["file_name"] for lora in filtered} assert file_names == { "no_credit_required_for_selling.safetensors", - "default_license.safetensors", } diff --git a/tests/services/test_model_scanner.py b/tests/services/test_model_scanner.py index c0141439..02da8353 100644 --- a/tests/services/test_model_scanner.py +++ b/tests/services/test_model_scanner.py @@ -132,7 +132,8 @@ async def test_initialize_cache_populates_cache(tmp_path: Path): _normalize_path(tmp_path / "one.txt"), _normalize_path(tmp_path / "nested" / "two.txt"), } - assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS} + # build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense) + assert {item["license_flags"] for item in cache.raw_data} == {113} assert scanner._hash_index.get_path("hash-one") == _normalize_path(tmp_path / "one.txt") assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt") @@ -190,7 +191,8 @@ async def test_initialize_in_background_applies_scan_result(tmp_path: Path, monk _normalize_path(tmp_path / "one.txt"), _normalize_path(tmp_path / "nested" / "two.txt"), } - assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS} + # build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense) + assert {item["license_flags"] for item in cache.raw_data} == {113} assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt") assert scanner._tags_count == {"alpha": 1, "beta": 1} assert scanner._excluded_models == [_normalize_path(tmp_path / "skip-file.txt")] diff --git a/tests/utils/test_civitai_utils.py b/tests/utils/test_civitai_utils.py index 7f5a74e9..6ef5362a 100644 --- a/tests/utils/test_civitai_utils.py +++ b/tests/utils/test_civitai_utils.py @@ -16,7 +16,9 @@ def test_resolve_license_payload_defaults(): assert payload["allowDerivatives"] is True assert payload["allowDifferentLicense"] is True assert payload["allowCommercialUse"] == ["Sell"] - assert flags == 127 + # Default ["Sell"] only sets the Sell bit (16), plus NoCredit (1), + # Derivatives (32) and DifferentLicense (64) = 113. + assert flags == 113 def test_build_license_flags_custom_values(): @@ -34,11 +36,10 @@ def test_build_license_flags_custom_values(): assert payload["allowDifferentLicense"] is False flags = build_license_flags(source) - # Sell automatically enables all commercial bits including image. - assert flags == 30 + assert flags == 18 -def test_build_license_flags_respects_commercial_hierarchy(): +def test_build_license_flags_independent_values(): base = { "allowNoCredit": False, "allowDerivatives": False, @@ -46,14 +47,10 @@ def test_build_license_flags_respects_commercial_hierarchy(): } assert build_license_flags({**base, "allowCommercialUse": []}) == 0 - # Rent adds rent and rentcivit permissions. - assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 12 - # RentCivit alone should only set its own bit. + assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 8 assert build_license_flags({**base, "allowCommercialUse": ["RentCivit"]}) == 4 - # Image only toggles the image bit. assert build_license_flags({**base, "allowCommercialUse": ["Image"]}) == 2 - # Sell forces all commercial bits regardless of image listing. - assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 30 + assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 16 def test_build_license_flags_parses_aggregate_string(): diff --git a/vue-widgets/src/components/lora-pool/sections/LicenseSection.vue b/vue-widgets/src/components/lora-pool/sections/LicenseSection.vue index 4d21772b..cbffeb7a 100644 --- a/vue-widgets/src/components/lora-pool/sections/LicenseSection.vue +++ b/vue-widgets/src/components/lora-pool/sections/LicenseSection.vue @@ -5,7 +5,7 @@