mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-23 19:51:15 -03:00
Compare commits
5 Commits
908464bc0a
...
v1.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3631c5eb10 | ||
|
|
6d5b4b7312 | ||
|
|
7803bd542d | ||
|
|
f0a86dbbc0 | ||
|
|
682e964f89 |
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "Automatisch organisieren",
|
"autoOrganize": "Automatisch organisieren",
|
||||||
"skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen",
|
"skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen",
|
||||||
"resumeMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle fortsetzen",
|
"resumeMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle fortsetzen",
|
||||||
|
"setFavorite": "Als Favorit setzen",
|
||||||
|
"setFavoriteCount": "Als Favorit setzen ({favorited}/{total})",
|
||||||
|
"unfavorite": "Aus Favoriten entfernen",
|
||||||
"deleteAll": "Ausgewählte löschen",
|
"deleteAll": "Ausgewählte löschen",
|
||||||
"downloadMissingLoras": "Fehlende LoRAs herunterladen",
|
"downloadMissingLoras": "Fehlende LoRAs herunterladen",
|
||||||
"clear": "Auswahl löschen",
|
"clear": "Auswahl löschen",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
|
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
|
||||||
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
|
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
|
||||||
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden",
|
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden",
|
||||||
|
"bulkFavoriteUpdating": "Füge {count} Modell(e) zu Favoriten hinzu...",
|
||||||
|
"bulkUnfavoriteUpdating": "Entferne {count} Modell(e) aus Favoriten...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} Modell(e) zu Favoriten hinzugefügt, {failed} fehlgeschlagen",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} Modell(e) aus Favoriten entfernt, {failed} fehlgeschlagen",
|
||||||
|
"bulkFavoriteFailed": "Fehler beim Aktualisieren des Favoritenstatus",
|
||||||
"bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...",
|
"bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...",
|
||||||
"bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar",
|
"bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar",
|
||||||
"bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden",
|
"bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "Auto-Organize Selected",
|
"autoOrganize": "Auto-Organize Selected",
|
||||||
"skipMetadataRefresh": "Skip Metadata Refresh for Selected",
|
"skipMetadataRefresh": "Skip Metadata Refresh for Selected",
|
||||||
"resumeMetadataRefresh": "Resume Metadata Refresh for Selected",
|
"resumeMetadataRefresh": "Resume Metadata Refresh for Selected",
|
||||||
|
"setFavorite": "Set as Favorite",
|
||||||
|
"setFavoriteCount": "Set as Favorite ({favorited}/{total})",
|
||||||
|
"unfavorite": "Remove from Favorites",
|
||||||
"deleteAll": "Delete Selected",
|
"deleteAll": "Delete Selected",
|
||||||
"downloadMissingLoras": "Download Missing LoRAs",
|
"downloadMissingLoras": "Download Missing LoRAs",
|
||||||
"clear": "Clear Selection",
|
"clear": "Clear Selection",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "Set content rating to {level} for {count} model(s)",
|
"bulkContentRatingSet": "Set content rating to {level} for {count} model(s)",
|
||||||
"bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
|
"bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
|
||||||
"bulkContentRatingFailed": "Failed to update content rating for selected models",
|
"bulkContentRatingFailed": "Failed to update content rating for selected models",
|
||||||
|
"bulkFavoriteUpdating": "Adding {count} model(s) to favorites...",
|
||||||
|
"bulkUnfavoriteUpdating": "Removing {count} model(s) from favorites...",
|
||||||
|
"bulkFavoritePartialAdded": "Added {success} model(s) to favorites, {failed} failed",
|
||||||
|
"bulkFavoritePartialRemoved": "Removed {success} model(s) from favorites, {failed} failed",
|
||||||
|
"bulkFavoriteFailed": "Failed to update favorite status for selected models",
|
||||||
"bulkUpdatesChecking": "Checking selected {type}(s) for updates...",
|
"bulkUpdatesChecking": "Checking selected {type}(s) for updates...",
|
||||||
"bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)",
|
"bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)",
|
||||||
"bulkUpdatesNone": "No updates found for selected {type}(s)",
|
"bulkUpdatesNone": "No updates found for selected {type}(s)",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "Auto-organizar seleccionados",
|
"autoOrganize": "Auto-organizar seleccionados",
|
||||||
"skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados",
|
"skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados",
|
||||||
"resumeMetadataRefresh": "Reanudar actualización de metadatos para seleccionados",
|
"resumeMetadataRefresh": "Reanudar actualización de metadatos para seleccionados",
|
||||||
|
"setFavorite": "Marcar como favorito",
|
||||||
|
"setFavoriteCount": "Marcar como favorito ({favorited}/{total})",
|
||||||
|
"unfavorite": "Quitar de favoritos",
|
||||||
"deleteAll": "Eliminar seleccionados",
|
"deleteAll": "Eliminar seleccionados",
|
||||||
"downloadMissingLoras": "Descargar LoRAs faltantes",
|
"downloadMissingLoras": "Descargar LoRAs faltantes",
|
||||||
"clear": "Limpiar selección",
|
"clear": "Limpiar selección",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)",
|
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)",
|
||||||
"bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron",
|
"bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron",
|
||||||
"bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados",
|
"bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados",
|
||||||
|
"bulkFavoriteUpdating": "Añadiendo {count} modelo(s) a favoritos...",
|
||||||
|
"bulkUnfavoriteUpdating": "Eliminando {count} modelo(s) de favoritos...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} modelo(s) añadido(s) a favoritos, {failed} fallido(s)",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} modelo(s) eliminado(s) de favoritos, {failed} fallido(s)",
|
||||||
|
"bulkFavoriteFailed": "Error al actualizar el estado de favorito",
|
||||||
"bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...",
|
"bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...",
|
||||||
"bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados",
|
"bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados",
|
||||||
"bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados",
|
"bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "Auto-organiser la sélection",
|
"autoOrganize": "Auto-organiser la sélection",
|
||||||
"skipMetadataRefresh": "Ignorer l'actualisation des métadonnées pour la sélection",
|
"skipMetadataRefresh": "Ignorer l'actualisation des métadonnées pour la sélection",
|
||||||
"resumeMetadataRefresh": "Reprendre l'actualisation des métadonnées pour la sélection",
|
"resumeMetadataRefresh": "Reprendre l'actualisation des métadonnées pour la sélection",
|
||||||
|
"setFavorite": "Définir comme favori",
|
||||||
|
"setFavoriteCount": "Définir comme favori ({favorited}/{total})",
|
||||||
|
"unfavorite": "Retirer des favoris",
|
||||||
"deleteAll": "Supprimer la sélection",
|
"deleteAll": "Supprimer la sélection",
|
||||||
"downloadMissingLoras": "Télécharger les LoRAs manquants",
|
"downloadMissingLoras": "Télécharger les LoRAs manquants",
|
||||||
"clear": "Effacer la sélection",
|
"clear": "Effacer la sélection",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)",
|
"bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)",
|
||||||
"bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)",
|
"bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)",
|
||||||
"bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés",
|
"bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés",
|
||||||
|
"bulkFavoriteUpdating": "Ajout de {count} modèle(s) aux favoris...",
|
||||||
|
"bulkUnfavoriteUpdating": "Suppression de {count} modèle(s) des favoris...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} modèle(s) ajouté(s) aux favoris, {failed} échec(s)",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} modèle(s) retiré(s) des favoris, {failed} échec(s)",
|
||||||
|
"bulkFavoriteFailed": "Échec de la mise à jour du statut de favori",
|
||||||
"bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...",
|
"bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...",
|
||||||
"bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {type} sélectionnés",
|
"bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {type} sélectionnés",
|
||||||
"bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés",
|
"bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "ארגן אוטומטית נבחרים",
|
"autoOrganize": "ארגן אוטומטית נבחרים",
|
||||||
"skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים",
|
"skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים",
|
||||||
"resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים",
|
"resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים",
|
||||||
|
"setFavorite": "הגדר כמועדף",
|
||||||
|
"setFavoriteCount": "הגדר כמועדף ({favorited}/{total})",
|
||||||
|
"unfavorite": "הסר ממועדפים",
|
||||||
"deleteAll": "מחק נבחרים",
|
"deleteAll": "מחק נבחרים",
|
||||||
"downloadMissingLoras": "הורדת LoRAs חסרים",
|
"downloadMissingLoras": "הורדת LoRAs חסרים",
|
||||||
"clear": "נקה בחירה",
|
"clear": "נקה בחירה",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
|
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
|
||||||
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
|
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
|
||||||
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
|
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
|
||||||
|
"bulkFavoriteUpdating": "מוסיף {count} דגמים למועדפים...",
|
||||||
|
"bulkUnfavoriteUpdating": "מסיר {count} דגמים ממועדפים...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} דגמים נוספו למועדפים, {failed} נכשלו",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} דגמים הוסרו ממועדפים, {failed} נכשלו",
|
||||||
|
"bulkFavoriteFailed": "עדכון סטטוס מועדפים נכשל",
|
||||||
"bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...",
|
"bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...",
|
||||||
"bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו",
|
"bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו",
|
||||||
"bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו",
|
"bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "自動整理を実行",
|
"autoOrganize": "自動整理を実行",
|
||||||
"skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ",
|
"skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ",
|
||||||
"resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開",
|
"resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開",
|
||||||
|
"setFavorite": "お気に入りに設定",
|
||||||
|
"setFavoriteCount": "お気に入りに設定 ({favorited}/{total})",
|
||||||
|
"unfavorite": "お気に入りから削除",
|
||||||
"deleteAll": "選択したものを削除",
|
"deleteAll": "選択したものを削除",
|
||||||
"downloadMissingLoras": "不足している LoRA をダウンロード",
|
"downloadMissingLoras": "不足している LoRA をダウンロード",
|
||||||
"clear": "選択をクリア",
|
"clear": "選択をクリア",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
|
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
|
||||||
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
|
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
|
||||||
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
|
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
|
||||||
|
"bulkFavoriteUpdating": "{count} 個のモデルをお気に入りに追加中...",
|
||||||
|
"bulkUnfavoriteUpdating": "{count} 個のモデルをお気に入りから削除中...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} 個のモデルをお気に入りに追加、{failed} 個失敗",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} 個のモデルをお気に入りから削除、{failed} 個失敗",
|
||||||
|
"bulkFavoriteFailed": "お気に入り状態の更新に失敗しました",
|
||||||
"bulkUpdatesChecking": "選択された{type}の更新を確認しています...",
|
"bulkUpdatesChecking": "選択された{type}の更新を確認しています...",
|
||||||
"bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります",
|
"bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります",
|
||||||
"bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした",
|
"bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "자동 정리 선택",
|
"autoOrganize": "자동 정리 선택",
|
||||||
"skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기",
|
"skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기",
|
||||||
"resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개",
|
"resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개",
|
||||||
|
"setFavorite": "즐겨찾기로 설정",
|
||||||
|
"setFavoriteCount": "즐겨찾기로 설정 ({favorited}/{total})",
|
||||||
|
"unfavorite": "즐겨찾기 해제",
|
||||||
"deleteAll": "선택된 항목 삭제",
|
"deleteAll": "선택된 항목 삭제",
|
||||||
"downloadMissingLoras": "누락된 LoRA 다운로드",
|
"downloadMissingLoras": "누락된 LoRA 다운로드",
|
||||||
"clear": "선택 지우기",
|
"clear": "선택 지우기",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
|
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
|
||||||
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
|
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
|
||||||
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
|
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
|
||||||
|
"bulkFavoriteUpdating": "{count}개 모델을 즐겨찾기에 추가 중...",
|
||||||
|
"bulkUnfavoriteUpdating": "{count}개 모델을 즐겨찾기에서 제거 중...",
|
||||||
|
"bulkFavoritePartialAdded": "{success}개 모델을 즐겨찾기에 추가, {failed}개 실패",
|
||||||
|
"bulkFavoritePartialRemoved": "{success}개 모델을 즐겨찾기에서 제거, {failed}개 실패",
|
||||||
|
"bulkFavoriteFailed": "즐겨찾기 상태 업데이트 실패",
|
||||||
"bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...",
|
"bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...",
|
||||||
"bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다",
|
"bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다",
|
||||||
"bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다",
|
"bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "Автоматически организовать выбранные",
|
"autoOrganize": "Автоматически организовать выбранные",
|
||||||
"skipMetadataRefresh": "Пропустить обновление метаданных для выбранных",
|
"skipMetadataRefresh": "Пропустить обновление метаданных для выбранных",
|
||||||
"resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных",
|
"resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных",
|
||||||
|
"setFavorite": "Добавить в избранное",
|
||||||
|
"setFavoriteCount": "Добавить в избранное ({favorited}/{total})",
|
||||||
|
"unfavorite": "Удалить из избранного",
|
||||||
"deleteAll": "Удалить выбранные",
|
"deleteAll": "Удалить выбранные",
|
||||||
"downloadMissingLoras": "Скачать отсутствующие LoRAs",
|
"downloadMissingLoras": "Скачать отсутствующие LoRAs",
|
||||||
"clear": "Очистить выбор",
|
"clear": "Очистить выбор",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
|
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
|
||||||
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
|
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
|
||||||
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
|
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
|
||||||
|
"bulkFavoriteUpdating": "Добавление {count} моделей в избранное...",
|
||||||
|
"bulkUnfavoriteUpdating": "Удаление {count} моделей из избранного...",
|
||||||
|
"bulkFavoritePartialAdded": "{success} моделей добавлено в избранное, {failed} не удалось",
|
||||||
|
"bulkFavoritePartialRemoved": "{success} моделей удалено из избранного, {failed} не удалось",
|
||||||
|
"bulkFavoriteFailed": "Не удалось обновить статус избранного",
|
||||||
"bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...",
|
"bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...",
|
||||||
"bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}",
|
"bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}",
|
||||||
"bulkUpdatesNone": "Обновления для выбранных {type} не найдены",
|
"bulkUpdatesNone": "Обновления для выбранных {type} не найдены",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "自动整理所选模型",
|
"autoOrganize": "自动整理所选模型",
|
||||||
"skipMetadataRefresh": "跳过所选模型的元数据刷新",
|
"skipMetadataRefresh": "跳过所选模型的元数据刷新",
|
||||||
"resumeMetadataRefresh": "恢复所选模型的元数据刷新",
|
"resumeMetadataRefresh": "恢复所选模型的元数据刷新",
|
||||||
|
"setFavorite": "设为收藏",
|
||||||
|
"setFavoriteCount": "设为收藏 ({favorited}/{total})",
|
||||||
|
"unfavorite": "取消收藏",
|
||||||
"deleteAll": "删除已选",
|
"deleteAll": "删除已选",
|
||||||
"downloadMissingLoras": "下载缺失的 LoRAs",
|
"downloadMissingLoras": "下载缺失的 LoRAs",
|
||||||
"clear": "清除选择",
|
"clear": "清除选择",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
|
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
|
||||||
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败",
|
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败",
|
||||||
"bulkContentRatingFailed": "未能更新所选模型的内容评级",
|
"bulkContentRatingFailed": "未能更新所选模型的内容评级",
|
||||||
|
"bulkFavoriteUpdating": "正在将 {count} 个模型添加到收藏...",
|
||||||
|
"bulkUnfavoriteUpdating": "正在将 {count} 个模型从收藏移除...",
|
||||||
|
"bulkFavoritePartialAdded": "已将 {success} 个模型添加到收藏,{failed} 个失败",
|
||||||
|
"bulkFavoritePartialRemoved": "已将 {success} 个模型从收藏移除,{failed} 个失败",
|
||||||
|
"bulkFavoriteFailed": "更新收藏状态失败",
|
||||||
"bulkUpdatesChecking": "正在检查所选 {type} 的更新...",
|
"bulkUpdatesChecking": "正在检查所选 {type} 的更新...",
|
||||||
"bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新",
|
"bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新",
|
||||||
"bulkUpdatesNone": "所选 {type} 未发现更新",
|
"bulkUpdatesNone": "所选 {type} 未发现更新",
|
||||||
|
|||||||
@@ -687,6 +687,9 @@
|
|||||||
"autoOrganize": "自動整理所選模型",
|
"autoOrganize": "自動整理所選模型",
|
||||||
"skipMetadataRefresh": "跳過所選模型的元數據更新",
|
"skipMetadataRefresh": "跳過所選模型的元數據更新",
|
||||||
"resumeMetadataRefresh": "恢復所選模型的元數據更新",
|
"resumeMetadataRefresh": "恢復所選模型的元數據更新",
|
||||||
|
"setFavorite": "設為收藏",
|
||||||
|
"setFavoriteCount": "設為收藏 ({favorited}/{total})",
|
||||||
|
"unfavorite": "取消收藏",
|
||||||
"deleteAll": "刪除所選",
|
"deleteAll": "刪除所選",
|
||||||
"downloadMissingLoras": "下載缺失的 LoRAs",
|
"downloadMissingLoras": "下載缺失的 LoRAs",
|
||||||
"clear": "清除選取",
|
"clear": "清除選取",
|
||||||
@@ -1699,6 +1702,11 @@
|
|||||||
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
|
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
|
||||||
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗",
|
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗",
|
||||||
"bulkContentRatingFailed": "無法更新所選模型的內容分級",
|
"bulkContentRatingFailed": "無法更新所選模型的內容分級",
|
||||||
|
"bulkFavoriteUpdating": "正在將 {count} 個模型加入收藏...",
|
||||||
|
"bulkUnfavoriteUpdating": "正在將 {count} 個模型從收藏移除...",
|
||||||
|
"bulkFavoritePartialAdded": "已將 {success} 個模型加入收藏,{failed} 個失敗",
|
||||||
|
"bulkFavoritePartialRemoved": "已將 {success} 個模型從收藏移除,{failed} 個失敗",
|
||||||
|
"bulkFavoriteFailed": "更新收藏狀態失敗",
|
||||||
"bulkUpdatesChecking": "正在檢查所選 {type} 的更新...",
|
"bulkUpdatesChecking": "正在檢查所選 {type} 的更新...",
|
||||||
"bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新",
|
"bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新",
|
||||||
"bulkUpdatesNone": "所選 {type} 未找到更新",
|
"bulkUpdatesNone": "所選 {type} 未找到更新",
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ class CivitaiBaseModelService:
|
|||||||
"zimageturbo": "ZIT",
|
"zimageturbo": "ZIT",
|
||||||
"zimagebase": "ZIB",
|
"zimagebase": "ZIB",
|
||||||
"anima": "ANI",
|
"anima": "ANI",
|
||||||
|
"ernie": "ERNI",
|
||||||
|
"ernie turbo": "ETRB",
|
||||||
|
"nucleus": "NUCL",
|
||||||
"svd": "SVD",
|
"svd": "SVD",
|
||||||
"ltxv": "LTXV",
|
"ltxv": "LTXV",
|
||||||
"ltxv2": "LTV2",
|
"ltxv2": "LTV2",
|
||||||
@@ -418,6 +421,9 @@ class CivitaiBaseModelService:
|
|||||||
"Kolors",
|
"Kolors",
|
||||||
"NoobAI",
|
"NoobAI",
|
||||||
"Anima",
|
"Anima",
|
||||||
|
"Ernie",
|
||||||
|
"Ernie Turbo",
|
||||||
|
"Nucleus",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,59 @@ class CivitaiClient:
|
|||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self, hashes: List[str]
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
"""Fetch full version details for up to 100 SHA256 hashes via the batch endpoint.
|
||||||
|
|
||||||
|
Uses POST /api/v1/model-versions/by-hash which returns full version
|
||||||
|
details including ``usageControl`` and ``earlyAccessEndsAt`` that are
|
||||||
|
not available from the model-level API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hashes: List of SHA256 hashes (max 100 per batch; auto-split).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of version dicts or None on failure.
|
||||||
|
"""
|
||||||
|
if not hashes:
|
||||||
|
return []
|
||||||
|
|
||||||
|
BATCH_SIZE = 100
|
||||||
|
all_versions: List[Dict] = []
|
||||||
|
|
||||||
|
for start in range(0, len(hashes), BATCH_SIZE):
|
||||||
|
batch = hashes[start : start + BATCH_SIZE]
|
||||||
|
try:
|
||||||
|
success, result = await self._make_request(
|
||||||
|
"POST",
|
||||||
|
f"{self.base_url}/model-versions/by-hash",
|
||||||
|
use_auth=True,
|
||||||
|
json=batch,
|
||||||
|
)
|
||||||
|
if not success:
|
||||||
|
logger.warning(
|
||||||
|
"Batch by-hash request failed for %d hashes: %s",
|
||||||
|
len(batch),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(result, list):
|
||||||
|
all_versions.extend(result)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
"Unexpected by-hash response type: %s", type(result)
|
||||||
|
)
|
||||||
|
except RateLimitError:
|
||||||
|
raise
|
||||||
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
|
logger.error(
|
||||||
|
"Error fetching model versions by hashes: %s", exc
|
||||||
|
)
|
||||||
|
|
||||||
|
return all_versions if all_versions else None
|
||||||
|
|
||||||
async def get_user_models(self, username: str) -> Optional[List[Dict]]:
|
async def get_user_models(self, username: str) -> Optional[List[Dict]]:
|
||||||
"""Fetch all models for a specific Civitai user."""
|
"""Fetch all models for a specific Civitai user."""
|
||||||
if not username:
|
if not username:
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ class ModelMetadataProvider(ABC):
|
|||||||
"""Fetch model versions for multiple model ids when supported."""
|
"""Fetch model versions for multiple model ids when supported."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self, hashes: List[str]
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
"""Fetch full version details for multiple SHA256 hashes.
|
||||||
|
|
||||||
|
Used specifically to retrieve ``usageControl`` which is only
|
||||||
|
available from the per-version / by-hash API, not from model-level
|
||||||
|
responses. Providers that cannot resolve hashes should let the
|
||||||
|
default ``NotImplementedError`` propagate.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
||||||
"""Get specific model version with additional metadata"""
|
"""Get specific model version with additional metadata"""
|
||||||
@@ -141,6 +153,11 @@ class CivitaiModelMetadataProvider(ModelMetadataProvider):
|
|||||||
) -> Optional[Dict[int, Dict]]:
|
) -> Optional[Dict[int, Dict]]:
|
||||||
return await self.client.get_model_versions_bulk(model_ids)
|
return await self.client.get_model_versions_bulk(model_ids)
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self, hashes: List[str]
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
return await self.client.get_model_versions_by_hashes(hashes)
|
||||||
|
|
||||||
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
||||||
return await self.client.get_model_version(model_id, version_id)
|
return await self.client.get_model_version(model_id, version_id)
|
||||||
|
|
||||||
@@ -519,6 +536,32 @@ class FallbackMetadataProvider(ModelMetadataProvider):
|
|||||||
continue
|
continue
|
||||||
return None, "No provider could retrieve the data"
|
return None, "No provider could retrieve the data"
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self, hashes: List[str]
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
for provider, label in self._iter_providers():
|
||||||
|
try:
|
||||||
|
result = await self._call_with_rate_limit(
|
||||||
|
label,
|
||||||
|
provider.get_model_versions_by_hashes,
|
||||||
|
hashes,
|
||||||
|
)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
except NotImplementedError:
|
||||||
|
continue
|
||||||
|
except RateLimitError as exc:
|
||||||
|
exc.provider = exc.provider or label
|
||||||
|
raise exc
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(
|
||||||
|
"Provider %s failed for get_model_versions_by_hashes: %s",
|
||||||
|
label,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_user_models(self, username: str) -> Optional[List[Dict]]:
|
async def get_user_models(self, username: str) -> Optional[List[Dict]]:
|
||||||
for provider, label in self._iter_providers():
|
for provider, label in self._iter_providers():
|
||||||
try:
|
try:
|
||||||
@@ -593,6 +636,15 @@ class RateLimitRetryingProvider(ModelMetadataProvider):
|
|||||||
model_ids,
|
model_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self, hashes: List[str]
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
return await self._rate_limit_helper.run(
|
||||||
|
self._label,
|
||||||
|
self._provider.get_model_versions_by_hashes,
|
||||||
|
hashes,
|
||||||
|
)
|
||||||
|
|
||||||
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
async def get_model_version(self, model_id: int = None, version_id: int = None) -> Optional[Dict]:
|
||||||
return await self._rate_limit_helper.run(
|
return await self._rate_limit_helper.run(
|
||||||
self._label,
|
self._label,
|
||||||
@@ -669,6 +721,17 @@ class ModelMetadataProviderManager:
|
|||||||
provider = self._get_provider(provider_name)
|
provider = self._get_provider(provider_name)
|
||||||
return await provider.get_model_version_info(version_id)
|
return await provider.get_model_version_info(version_id)
|
||||||
|
|
||||||
|
async def get_model_versions_by_hashes(
|
||||||
|
self,
|
||||||
|
hashes: List[str],
|
||||||
|
provider_name: str = None,
|
||||||
|
) -> Optional[List[Dict]]:
|
||||||
|
provider = self._get_provider(provider_name)
|
||||||
|
try:
|
||||||
|
return await provider.get_model_versions_by_hashes(hashes)
|
||||||
|
except NotImplementedError:
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_user_models(self, username: str, provider_name: str = None) -> Optional[List[Dict]]:
|
async def get_user_models(self, username: str, provider_name: str = None) -> Optional[List[Dict]]:
|
||||||
"""Fetch models owned by the specified user"""
|
"""Fetch models owned by the specified user"""
|
||||||
provider = self._get_provider(provider_name)
|
provider = self._get_provider(provider_name)
|
||||||
|
|||||||
@@ -989,6 +989,11 @@ class ModelUpdateService:
|
|||||||
fallback_attempted = True
|
fallback_attempted = True
|
||||||
try:
|
try:
|
||||||
response = await metadata_provider.get_model_versions(model_id)
|
response = await metadata_provider.get_model_versions(model_id)
|
||||||
|
if response is not None:
|
||||||
|
await self._enrich_version_entries(
|
||||||
|
metadata_provider,
|
||||||
|
{model_id: response},
|
||||||
|
)
|
||||||
except RateLimitError:
|
except RateLimitError:
|
||||||
raise
|
raise
|
||||||
except ResourceNotFoundError as exc:
|
except ResourceNotFoundError as exc:
|
||||||
@@ -1083,6 +1088,136 @@ class ModelUpdateService:
|
|||||||
self._upsert_record(record)
|
self._upsert_record(record)
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
async def _enrich_version_entries(
|
||||||
|
self,
|
||||||
|
metadata_provider,
|
||||||
|
responses_by_model_id: Dict[int, Mapping],
|
||||||
|
) -> None:
|
||||||
|
"""Enrich version entries with ``usageControl`` via batch hash endpoint.
|
||||||
|
|
||||||
|
The model-level API does not include ``usageControl`` on version
|
||||||
|
entries. This method collects SHA256 hashes from every version's
|
||||||
|
primary model file, calls ``POST /api/v1/model-versions/by-hash``
|
||||||
|
(up to 100 hashes per request), and injects ``usageControl`` +
|
||||||
|
``earlyAccessEndsAt`` into each version entry dict in-place.
|
||||||
|
"""
|
||||||
|
if not metadata_provider or not responses_by_model_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
hashes_by_version: Dict[int, str] = {}
|
||||||
|
for response in responses_by_model_id.values():
|
||||||
|
hashes_by_version.update(
|
||||||
|
self._collect_hashes_from_response(response)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not hashes_by_version:
|
||||||
|
return
|
||||||
|
|
||||||
|
version_ids_by_hash: Dict[str, List[int]] = {}
|
||||||
|
for version_id, sha256 in hashes_by_version.items():
|
||||||
|
version_ids_by_hash.setdefault(sha256, []).append(version_id)
|
||||||
|
|
||||||
|
all_hashes = list(version_ids_by_hash.keys())
|
||||||
|
BATCH_SIZE = 100
|
||||||
|
|
||||||
|
enrichment: Dict[int, Dict] = {}
|
||||||
|
try:
|
||||||
|
for start in range(0, len(all_hashes), BATCH_SIZE):
|
||||||
|
batch = all_hashes[start : start + BATCH_SIZE]
|
||||||
|
try:
|
||||||
|
enriched = await metadata_provider.get_model_versions_by_hashes(
|
||||||
|
batch
|
||||||
|
)
|
||||||
|
except NotImplementedError:
|
||||||
|
return
|
||||||
|
except RateLimitError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not enriched:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for entry in enriched:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
version_id = entry.get("id")
|
||||||
|
if version_id is None:
|
||||||
|
continue
|
||||||
|
enrichment[version_id] = {
|
||||||
|
"usageControl": _normalize_string(
|
||||||
|
entry.get("usageControl")
|
||||||
|
),
|
||||||
|
"earlyAccessEndsAt": _normalize_string(
|
||||||
|
entry.get("earlyAccessEndsAt")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
except RateLimitError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not enrichment:
|
||||||
|
return
|
||||||
|
|
||||||
|
for response in responses_by_model_id.values():
|
||||||
|
versions = response.get("modelVersions")
|
||||||
|
if not isinstance(versions, list):
|
||||||
|
continue
|
||||||
|
for version in versions:
|
||||||
|
if not isinstance(version, dict):
|
||||||
|
continue
|
||||||
|
version_id = version.get("id")
|
||||||
|
if version_id not in enrichment:
|
||||||
|
continue
|
||||||
|
extra = enrichment[version_id]
|
||||||
|
if extra.get("usageControl") and not version.get("usageControl"):
|
||||||
|
version["usageControl"] = extra["usageControl"]
|
||||||
|
if extra.get("earlyAccessEndsAt") and not version.get(
|
||||||
|
"earlyAccessEndsAt"
|
||||||
|
):
|
||||||
|
version["earlyAccessEndsAt"] = extra["earlyAccessEndsAt"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _collect_hashes_from_response(response: Mapping) -> Dict[int, str]:
|
||||||
|
"""Extract ``{version_id: sha256}`` from a model-level API response.
|
||||||
|
|
||||||
|
Returns an empty dict if the response structure is unexpected.
|
||||||
|
"""
|
||||||
|
result: Dict[int, str] = {}
|
||||||
|
versions = response.get("modelVersions")
|
||||||
|
if not isinstance(versions, list):
|
||||||
|
return result
|
||||||
|
for entry in versions:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
version_id = _normalize_int(entry.get("id"))
|
||||||
|
if version_id is None:
|
||||||
|
continue
|
||||||
|
sha256 = ModelUpdateService._extract_sha256_from_version_entry(entry)
|
||||||
|
if sha256:
|
||||||
|
result[version_id] = sha256
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_sha256_from_version_entry(entry: Mapping) -> Optional[str]:
|
||||||
|
"""Return the SHA256 hash from the primary model file of a version entry."""
|
||||||
|
files = entry.get("files")
|
||||||
|
if not isinstance(files, list):
|
||||||
|
return None
|
||||||
|
for file_info in files:
|
||||||
|
if not isinstance(file_info, dict):
|
||||||
|
continue
|
||||||
|
if file_info.get("type") != "Model":
|
||||||
|
continue
|
||||||
|
primary = file_info.get("primary")
|
||||||
|
if primary is not True and str(primary).strip().lower() != "true":
|
||||||
|
continue
|
||||||
|
hashes = file_info.get("hashes")
|
||||||
|
if isinstance(hashes, dict):
|
||||||
|
sha256 = hashes.get("SHA256")
|
||||||
|
if sha256:
|
||||||
|
return sha256
|
||||||
|
return None
|
||||||
|
|
||||||
async def _fetch_model_versions_bulk(
|
async def _fetch_model_versions_bulk(
|
||||||
self,
|
self,
|
||||||
metadata_provider,
|
metadata_provider,
|
||||||
@@ -1134,6 +1269,7 @@ class ModelUpdateService:
|
|||||||
len(aggregated),
|
len(aggregated),
|
||||||
provider_name,
|
provider_name,
|
||||||
)
|
)
|
||||||
|
await self._enrich_version_entries(metadata_provider, aggregated)
|
||||||
return aggregated
|
return aggregated
|
||||||
|
|
||||||
async def _collect_local_versions(
|
async def _collect_local_versions(
|
||||||
@@ -1261,6 +1397,7 @@ class ModelUpdateService:
|
|||||||
sort_index=sort_map.get(version_id, index),
|
sort_index=sort_map.get(version_id, index),
|
||||||
early_access_ends_at=remote_version.early_access_ends_at,
|
early_access_ends_at=remote_version.early_access_ends_at,
|
||||||
is_early_access=remote_version.is_early_access,
|
is_early_access=remote_version.is_early_access,
|
||||||
|
usage_control=remote_version.usage_control,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -178,5 +178,8 @@ SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS = frozenset(
|
|||||||
"Wan Video 2.5 I2V",
|
"Wan Video 2.5 I2V",
|
||||||
"Hunyuan Video",
|
"Hunyuan Video",
|
||||||
"Anima",
|
"Anima",
|
||||||
|
"Ernie",
|
||||||
|
"Ernie Turbo",
|
||||||
|
"Nucleus",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -387,6 +387,10 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-action-disabled-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.versions-loading-state,
|
.versions-loading-state,
|
||||||
.versions-empty,
|
.versions-empty,
|
||||||
.versions-error {
|
.versions-error {
|
||||||
|
|||||||
@@ -74,6 +74,34 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
if (setContentRatingItem) {
|
if (setContentRatingItem) {
|
||||||
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none';
|
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setFavoriteItem = this.menu.querySelector('[data-action="set-favorite"]');
|
||||||
|
|
||||||
|
if (setFavoriteItem && config.setFavorite) {
|
||||||
|
setFavoriteItem.style.display = 'flex';
|
||||||
|
|
||||||
|
const total = state.selectedModels.size;
|
||||||
|
const favoritedCount = this.countFavoritedInSelection();
|
||||||
|
const allFavorited = total > 0 && favoritedCount === total;
|
||||||
|
|
||||||
|
const icon = setFavoriteItem.querySelector('i');
|
||||||
|
const label = setFavoriteItem.querySelector('span');
|
||||||
|
|
||||||
|
if (allFavorited) {
|
||||||
|
if (icon) { icon.className = 'far fa-star'; }
|
||||||
|
if (label) { label.textContent = translate('loras.bulkOperations.unfavorite'); }
|
||||||
|
} else {
|
||||||
|
if (icon) { icon.className = 'fas fa-star'; }
|
||||||
|
if (label) {
|
||||||
|
label.textContent = favoritedCount > 0
|
||||||
|
? translate('loras.bulkOperations.setFavoriteCount', { favorited: favoritedCount, total })
|
||||||
|
: translate('loras.bulkOperations.setFavorite');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (setFavoriteItem) {
|
||||||
|
setFavoriteItem.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
if (downloadMissingLorasItem) {
|
if (downloadMissingLorasItem) {
|
||||||
// Only show for recipes page
|
// Only show for recipes page
|
||||||
downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none';
|
downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none';
|
||||||
@@ -138,6 +166,20 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countFavoritedInSelection() {
|
||||||
|
let count = 0;
|
||||||
|
for (const filePath of state.selectedModels) {
|
||||||
|
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||||
|
? window.CSS.escape(filePath)
|
||||||
|
: filePath.replace(/["\\]/g, '\\$&');
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
|
if (card && card.dataset.favorite === 'true') {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
showMenu(x, y, card) {
|
showMenu(x, y, card) {
|
||||||
this.updateMenuItemsForModelType();
|
this.updateMenuItemsForModelType();
|
||||||
this.updateSelectedCountHeader();
|
this.updateSelectedCountHeader();
|
||||||
@@ -185,6 +227,11 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
case 'delete-all':
|
case 'delete-all':
|
||||||
bulkManager.showBulkDeleteModal();
|
bulkManager.showBulkDeleteModal();
|
||||||
break;
|
break;
|
||||||
|
case 'set-favorite': {
|
||||||
|
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
|
||||||
|
bulkManager.setBulkFavorites(!allFavorited);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'download-missing-loras':
|
case 'download-missing-loras':
|
||||||
this.handleDownloadMissingLoras();
|
this.handleDownloadMissingLoras();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ function buildActionButton(label, variant, action, options = {}) {
|
|||||||
if (action) {
|
if (action) {
|
||||||
attributes.push(`data-version-action="${escapeHtml(action)}"`);
|
attributes.push(`data-version-action="${escapeHtml(action)}"`);
|
||||||
}
|
}
|
||||||
if (options.title) {
|
if (!options.disabled && options.title) {
|
||||||
attributes.push(`title="${escapeHtml(options.title)}"`);
|
attributes.push(`title="${escapeHtml(options.title)}"`);
|
||||||
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
|
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,11 @@ function buildActionButton(label, variant, action, options = {}) {
|
|||||||
if (options.extraAttributes) {
|
if (options.extraAttributes) {
|
||||||
attributes.push(options.extraAttributes);
|
attributes.push(options.extraAttributes);
|
||||||
}
|
}
|
||||||
return `<button ${attributes.join(' ')}>${options.iconMarkup || ''}${escapeHtml(label)}</button>`;
|
const buttonHtml = `<button ${attributes.join(' ')}>${options.iconMarkup || ''}${escapeHtml(label)}</button>`;
|
||||||
|
if (options.disabled && options.title) {
|
||||||
|
return `<span class="version-action-disabled-wrapper" title="${escapeHtml(options.title)}" aria-label="${escapeHtml(options.title)}">${buttonHtml}</span>`;
|
||||||
|
}
|
||||||
|
return buttonHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DISPLAY_FILTER_MODES = Object.freeze({
|
const DISPLAY_FILTER_MODES = Object.freeze({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF
|
|||||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||||
import { RecipeSidebarApiClient } from '../api/recipeApi.js';
|
import { RecipeSidebarApiClient, updateRecipeMetadata } from '../api/recipeApi.js';
|
||||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||||
@@ -41,7 +41,9 @@ export class BulkManager {
|
|||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
setContentRating: true,
|
setContentRating: true,
|
||||||
skipMetadataRefresh: true
|
skipMetadataRefresh: true,
|
||||||
|
setFavorite: true,
|
||||||
|
unfavorite: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
addTags: true,
|
addTags: true,
|
||||||
@@ -53,7 +55,9 @@ export class BulkManager {
|
|||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
setContentRating: false,
|
setContentRating: false,
|
||||||
skipMetadataRefresh: true
|
skipMetadataRefresh: true,
|
||||||
|
setFavorite: true,
|
||||||
|
unfavorite: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.CHECKPOINT]: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
addTags: true,
|
addTags: true,
|
||||||
@@ -65,7 +69,9 @@ export class BulkManager {
|
|||||||
autoOrganize: true,
|
autoOrganize: true,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
setContentRating: true,
|
setContentRating: true,
|
||||||
skipMetadataRefresh: true
|
skipMetadataRefresh: true,
|
||||||
|
setFavorite: true,
|
||||||
|
unfavorite: true
|
||||||
},
|
},
|
||||||
recipes: {
|
recipes: {
|
||||||
addTags: false,
|
addTags: false,
|
||||||
@@ -77,7 +83,9 @@ export class BulkManager {
|
|||||||
autoOrganize: false,
|
autoOrganize: false,
|
||||||
deleteAll: true,
|
deleteAll: true,
|
||||||
setContentRating: false,
|
setContentRating: false,
|
||||||
skipMetadataRefresh: false
|
skipMetadataRefresh: false,
|
||||||
|
setFavorite: true,
|
||||||
|
unfavorite: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1090,6 +1098,60 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setBulkFavorites(value) {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = state.selectedModels.size;
|
||||||
|
const isRecipesPage = state.currentPageType === 'recipes';
|
||||||
|
|
||||||
|
state.loadingManager.showSimpleLoading(
|
||||||
|
translate(value ? 'toast.models.bulkFavoriteUpdating' : 'toast.models.bulkUnfavoriteUpdating', { count: totalCount })
|
||||||
|
);
|
||||||
|
let cancelled = false;
|
||||||
|
state.loadingManager.showCancelButton(() => {
|
||||||
|
cancelled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const filePath of state.selectedModels) {
|
||||||
|
if (cancelled) {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (isRecipesPage) {
|
||||||
|
await updateRecipeMetadata(filePath, { favorite: value });
|
||||||
|
} else {
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
await apiClient.saveModelMetadata(filePath, { favorite: value });
|
||||||
|
}
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
failureCount++;
|
||||||
|
console.error(`Failed to set favorite=${value} for ${filePath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state.loadingManager?.hide?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount === totalCount) {
|
||||||
|
const toastKey = value ? 'modelCard.favorites.added' : 'modelCard.favorites.removed';
|
||||||
|
showToast(toastKey, {}, 'success');
|
||||||
|
} else if (successCount > 0) {
|
||||||
|
const toastKey = value ? 'toast.models.bulkFavoritePartialAdded' : 'toast.models.bulkFavoritePartialRemoved';
|
||||||
|
showToast(toastKey, { success: successCount, failed: failureCount }, 'warning');
|
||||||
|
} else {
|
||||||
|
showToast('toast.models.bulkFavoriteFailed', {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show bulk base model modal
|
* Show bulk base model modal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ export const BASE_MODELS = {
|
|||||||
HUNYUAN_VIDEO: "Hunyuan Video",
|
HUNYUAN_VIDEO: "Hunyuan Video",
|
||||||
// Other models
|
// Other models
|
||||||
ANIMA: "Anima",
|
ANIMA: "Anima",
|
||||||
|
ERNIE: "Ernie",
|
||||||
|
ERNIE_TURBO: "Ernie Turbo",
|
||||||
|
NUCLEUS: "Nucleus",
|
||||||
PONY_V7: "Pony V7",
|
PONY_V7: "Pony V7",
|
||||||
// Default
|
// Default
|
||||||
UNKNOWN: "Other"
|
UNKNOWN: "Other"
|
||||||
@@ -191,6 +194,9 @@ export const BASE_MODEL_ABBREVIATIONS = {
|
|||||||
[BASE_MODELS.ZIMAGE_TURBO]: 'ZIT',
|
[BASE_MODELS.ZIMAGE_TURBO]: 'ZIT',
|
||||||
[BASE_MODELS.ZIMAGE_BASE]: 'ZIB',
|
[BASE_MODELS.ZIMAGE_BASE]: 'ZIB',
|
||||||
[BASE_MODELS.ANIMA]: 'ANI',
|
[BASE_MODELS.ANIMA]: 'ANI',
|
||||||
|
[BASE_MODELS.ERNIE]: 'ERNI',
|
||||||
|
[BASE_MODELS.ERNIE_TURBO]: 'ETRB',
|
||||||
|
[BASE_MODELS.NUCLEUS]: 'NUCL',
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
[BASE_MODELS.UNKNOWN]: 'OTH'
|
[BASE_MODELS.UNKNOWN]: 'OTH'
|
||||||
@@ -394,6 +400,7 @@ export const BASE_MODEL_CATEGORIES = {
|
|||||||
BASE_MODELS.QWEN, BASE_MODELS.AURAFLOW, BASE_MODELS.CHROMA, BASE_MODELS.ZIMAGE_TURBO, BASE_MODELS.ZIMAGE_BASE,
|
BASE_MODELS.QWEN, BASE_MODELS.AURAFLOW, BASE_MODELS.CHROMA, BASE_MODELS.ZIMAGE_TURBO, BASE_MODELS.ZIMAGE_BASE,
|
||||||
BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1,
|
BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1,
|
||||||
BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI, BASE_MODELS.ANIMA,
|
BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI, BASE_MODELS.ANIMA,
|
||||||
|
BASE_MODELS.ERNIE, BASE_MODELS.ERNIE_TURBO, BASE_MODELS.NUCLEUS,
|
||||||
BASE_MODELS.UNKNOWN
|
BASE_MODELS.UNKNOWN
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,6 +77,9 @@
|
|||||||
<div class="context-menu-item" data-action="set-base-model">
|
<div class="context-menu-item" data-action="set-base-model">
|
||||||
<i class="fas fa-layer-group"></i> <span>{{ t('loras.bulkOperations.setBaseModel') }}</span>
|
<i class="fas fa-layer-group"></i> <span>{{ t('loras.bulkOperations.setBaseModel') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="set-favorite">
|
||||||
|
<i class="fas fa-star"></i> <span>{{ t('loras.bulkOperations.setFavorite') }}</span>
|
||||||
|
</div>
|
||||||
<div class="context-menu-item" data-action="set-content-rating">
|
<div class="context-menu-item" data-action="set-content-rating">
|
||||||
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
|
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ describe('LoRA widget drag interactions', () => {
|
|||||||
dragEl.dispatchEvent(new PointerEvent('pointerup', { pointerId: 1 }));
|
dragEl.dispatchEvent(new PointerEvent('pointerup', { pointerId: 1 }));
|
||||||
expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(false);
|
expect(document.body.classList.contains('lm-lora-strength-dragging')).toBe(false);
|
||||||
expect(onDragEnd).toHaveBeenCalledTimes(1);
|
expect(onDragEnd).toHaveBeenCalledTimes(1);
|
||||||
expect(renderSpy).toHaveBeenCalledWith(widget.value, widget);
|
// 454210a4 replaced renderFunction() with widget.value setter + widget.callback()
|
||||||
|
expect(widget.callback).toHaveBeenCalledWith(widget.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the selected LoRA when backspace is pressed outside of strength inputs', async () => {
|
it('deletes the selected LoRA when backspace is pressed outside of strength inputs', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user