diff --git a/locales/de.json b/locales/de.json index 312ea288..21d973a4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -222,6 +222,7 @@ "modelname": "Modellname", "tags": "Tags", "creator": "Ersteller", + "hash": "Hash", "title": "Rezept-Titel", "loraName": "LoRA-Dateiname", "loraModel": "LoRA-Modellname", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "Ersteller-Profil anzeigen", "openFileLocation": "Dateispeicherort öffnen", "sendToWorkflow": "An ComfyUI senden", - "sendToWorkflowText": "An ComfyUI senden" + "sendToWorkflowText": "An ComfyUI senden", + "copyHash": "Hash kopieren" }, "openFileLocation": { "success": "Dateispeicherort erfolgreich geöffnet", @@ -1450,6 +1452,7 @@ "location": "Speicherort", "baseModel": "Basis-Modell", "size": "Größe", + "hashes": "Hashes", "unknown": "Unbekannt", "usageTips": "Nutzungstipps", "additionalNotes": "Zusätzliche Notizen", diff --git a/locales/en.json b/locales/en.json index dc728f7b..e4b682f7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -222,6 +222,7 @@ "modelname": "Model Name", "tags": "Tags", "creator": "Creator", + "hash": "Hash", "title": "Recipe Title", "loraName": "LoRA Filename", "loraModel": "LoRA Model Name", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "View Creator Profile", "openFileLocation": "Open File Location", "sendToWorkflow": "Send to ComfyUI", - "sendToWorkflowText": "Send to ComfyUI" + "sendToWorkflowText": "Send to ComfyUI", + "copyHash": "Copy hash" }, "openFileLocation": { "success": "File location opened successfully", @@ -1450,6 +1452,7 @@ "location": "Location", "baseModel": "Base Model", "size": "Size", + "hashes": "Hashes", "unknown": "Unknown", "usageTips": "Usage Tips", "additionalNotes": "Additional Notes", diff --git a/locales/es.json b/locales/es.json index 341aa9e1..a93dacec 100644 --- a/locales/es.json +++ b/locales/es.json @@ -222,6 +222,7 @@ "modelname": "Nombre del modelo", "tags": "Etiquetas", "creator": "Creador", + "hash": "Hash", "title": "Título de la receta", "loraName": "Nombre de archivo LoRA", "loraModel": "Nombre del modelo LoRA", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "Ver perfil del creador", "openFileLocation": "Abrir ubicación del archivo", "sendToWorkflow": "Enviar a ComfyUI", - "sendToWorkflowText": "Enviar a ComfyUI" + "sendToWorkflowText": "Enviar a ComfyUI", + "copyHash": "Copiar hash" }, "openFileLocation": { "success": "Ubicación del archivo abierta exitosamente", @@ -1450,6 +1452,7 @@ "location": "Ubicación", "baseModel": "Modelo base", "size": "Tamaño", + "hashes": "Hashes", "unknown": "Desconocido", "usageTips": "Consejos de uso", "additionalNotes": "Notas adicionales", diff --git a/locales/fr.json b/locales/fr.json index 6c8fa3e3..6285939e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -222,6 +222,7 @@ "modelname": "Nom du modèle", "tags": "Tags", "creator": "Créateur", + "hash": "Hash", "title": "Titre de la recipe", "loraName": "Nom de fichier LoRA", "loraModel": "Nom du modèle LoRA", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "Voir le profil du créateur", "openFileLocation": "Ouvrir l'emplacement du fichier", "sendToWorkflow": "Envoyer vers ComfyUI", - "sendToWorkflowText": "Envoyer vers ComfyUI" + "sendToWorkflowText": "Envoyer vers ComfyUI", + "copyHash": "Copier le hash" }, "openFileLocation": { "success": "Emplacement du fichier ouvert avec succès", @@ -1450,6 +1452,7 @@ "location": "Emplacement", "baseModel": "Modèle de base", "size": "Taille", + "hashes": "Hashes", "unknown": "Inconnu", "usageTips": "Conseils d'utilisation", "additionalNotes": "Notes supplémentaires", diff --git a/locales/he.json b/locales/he.json index f0565a56..2ecb064a 100644 --- a/locales/he.json +++ b/locales/he.json @@ -222,6 +222,7 @@ "modelname": "שם מודל", "tags": "תגיות", "creator": "יוצר", + "hash": "האש", "title": "כותרת מתכון", "loraName": "שם קובץ LoRA", "loraModel": "שם מודל LoRA", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "הצג פרופיל יוצר", "openFileLocation": "פתח מיקום קובץ", "sendToWorkflow": "שלח ל-ComfyUI", - "sendToWorkflowText": "שלח ל-ComfyUI" + "sendToWorkflowText": "שלח ל-ComfyUI", + "copyHash": "העתק האש" }, "openFileLocation": { "success": "מיקום הקובץ נפתח בהצלחה", @@ -1450,6 +1452,7 @@ "location": "מיקום", "baseModel": "מודל בסיס", "size": "גודל", + "hashes": "האשים", "unknown": "לא ידוע", "usageTips": "טיפים לשימוש", "additionalNotes": "הערות נוספות", diff --git a/locales/ja.json b/locales/ja.json index 990422d1..e905ed5e 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -222,6 +222,7 @@ "modelname": "モデル名", "tags": "タグ", "creator": "作成者", + "hash": "ハッシュ", "title": "レシピタイトル", "loraName": "LoRAファイル名", "loraModel": "LoRAモデル名", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "作成者プロフィールを表示", "openFileLocation": "ファイルの場所を開く", "sendToWorkflow": "ComfyUI に送信", - "sendToWorkflowText": "ComfyUI に送信" + "sendToWorkflowText": "ComfyUI に送信", + "copyHash": "ハッシュをコピー" }, "openFileLocation": { "success": "ファイルの場所を正常に開きました", @@ -1450,6 +1452,7 @@ "location": "場所", "baseModel": "ベースモデル", "size": "サイズ", + "hashes": "ハッシュ", "unknown": "不明", "usageTips": "使用のヒント", "additionalNotes": "追加メモ", diff --git a/locales/ko.json b/locales/ko.json index e4e33712..91fb81d9 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -222,6 +222,7 @@ "modelname": "모델명", "tags": "태그", "creator": "제작자", + "hash": "해시", "title": "레시피 제목", "loraName": "LoRA 파일명", "loraModel": "LoRA 모델명", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "제작자 프로필 보기", "openFileLocation": "파일 위치 열기", "sendToWorkflow": "ComfyUI로 보내기", - "sendToWorkflowText": "ComfyUI로 보내기" + "sendToWorkflowText": "ComfyUI로 보내기", + "copyHash": "해시 복사" }, "openFileLocation": { "success": "파일 위치가 성공적으로 열렸습니다", @@ -1450,6 +1452,7 @@ "location": "위치", "baseModel": "베이스 모델", "size": "크기", + "hashes": "해시", "unknown": "알 수 없음", "usageTips": "사용 팁", "additionalNotes": "추가 메모", diff --git a/locales/ru.json b/locales/ru.json index 2929a885..95f7bbdb 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -222,6 +222,7 @@ "modelname": "Название модели", "tags": "Теги", "creator": "Автор", + "hash": "Хэш", "title": "Название рецепта", "loraName": "Имя файла LoRA", "loraModel": "Название модели LoRA", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "Посмотреть профиль создателя", "openFileLocation": "Открыть расположение файла", "sendToWorkflow": "Отправить в ComfyUI", - "sendToWorkflowText": "Отправить в ComfyUI" + "sendToWorkflowText": "Отправить в ComfyUI", + "copyHash": "Копировать хэш" }, "openFileLocation": { "success": "Расположение файла успешно открыто", @@ -1450,6 +1452,7 @@ "location": "Расположение", "baseModel": "Базовая модель", "size": "Размер", + "hashes": "Хэши", "unknown": "Неизвестно", "usageTips": "Советы по использованию", "additionalNotes": "Дополнительные заметки", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 3c916759..0ef30301 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -222,6 +222,7 @@ "modelname": "模型名称", "tags": "标签", "creator": "创作者", + "hash": "哈希", "title": "配方标题", "loraName": "LoRA 文件名", "loraModel": "LoRA 模型名称", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "查看创作者主页", "openFileLocation": "打开文件位置", "sendToWorkflow": "发送到 ComfyUI", - "sendToWorkflowText": "发送到 ComfyUI" + "sendToWorkflowText": "发送到 ComfyUI", + "copyHash": "复制哈希值" }, "openFileLocation": { "success": "文件位置已成功打开", @@ -1450,6 +1452,7 @@ "location": "位置", "baseModel": "基础模型", "size": "大小", + "hashes": "哈希值", "unknown": "未知", "usageTips": "使用提示", "additionalNotes": "附加备注", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 8e274a4c..c291ad0a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -222,6 +222,7 @@ "modelname": "模型名稱", "tags": "標籤", "creator": "創作者", + "hash": "雜湊", "title": "配方標題", "loraName": "LoRA 檔案名稱", "loraModel": "LoRA 模型名稱", @@ -1433,7 +1434,8 @@ "viewCreatorProfile": "查看創作者個人檔案", "openFileLocation": "開啟檔案位置", "sendToWorkflow": "傳送到 ComfyUI", - "sendToWorkflowText": "傳送到 ComfyUI" + "sendToWorkflowText": "傳送到 ComfyUI", + "copyHash": "複製雜湊值" }, "openFileLocation": { "success": "檔案位置已成功開啟", @@ -1450,6 +1452,7 @@ "location": "位置", "baseModel": "基礎模型", "size": "大小", + "hashes": "雜湊值", "unknown": "未知", "usageTips": "使用提示", "additionalNotes": "附加備註", diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 6254ebfd..3fc2c0b6 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -364,6 +364,7 @@ class ModelListingHandler: == "true", "tags": request.query.get("search_tags", "false").lower() == "true", "creator": request.query.get("search_creator", "false").lower() == "true", + "hash": request.query.get("search_hash", "false").lower() == "true", "recursive": request.query.get("recursive", "true").lower() == "true", } diff --git a/py/services/checkpoint_service.py b/py/services/checkpoint_service.py index 1c6922e5..73469d3e 100644 --- a/py/services/checkpoint_service.py +++ b/py/services/checkpoint_service.py @@ -51,6 +51,7 @@ class CheckpointService(BaseModelService): "base_model": model_data.get("base_model", ""), "folder": folder, "sha256": model_data.get("sha256", ""), + "autov3": model_data.get("autov3"), "file_path": file_path.replace(os.sep, "/"), "file_size": model_data.get("size", 0), "modified": model_data.get("modified", ""), diff --git a/py/services/embedding_service.py b/py/services/embedding_service.py index 7913f581..8e03c493 100644 --- a/py/services/embedding_service.py +++ b/py/services/embedding_service.py @@ -51,6 +51,7 @@ class EmbeddingService(BaseModelService): "base_model": model_data.get("base_model", ""), "folder": folder, "sha256": model_data.get("sha256", ""), + "autov3": model_data.get("autov3"), "file_path": file_path.replace(os.sep, "/"), "file_size": model_data.get("size", 0), "modified": model_data.get("modified", ""), diff --git a/py/services/lora_service.py b/py/services/lora_service.py index fa4863b7..6b7789ca 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -58,6 +58,7 @@ class LoraService(BaseModelService): "base_model": model_data.get("base_model", ""), "folder": folder, "sha256": model_data.get("sha256", ""), + "autov3": model_data.get("autov3"), "file_path": file_path.replace(os.sep, "/"), "file_size": model_data.get("size", 0), "modified": model_data.get("modified", ""), diff --git a/py/services/model_query.py b/py/services/model_query.py index b5ba2daa..29d5c544 100644 --- a/py/services/model_query.py +++ b/py/services/model_query.py @@ -432,6 +432,7 @@ class SearchStrategy: "tags": False, "recursive": True, "creator": False, + "hash": False, } def __init__( @@ -494,8 +495,28 @@ class SearchStrategy: results.append(item) continue + # Hash search is always exact (never fuzzy): match the full + # sha256, its autov2 prefix (first 10 chars), or the autov3 hash. + if options.get("hash", False): + hash_query = search_lower.strip() + if hash_query and self._matches_hash(item, hash_query): + results.append(item) + continue + return results + def _matches_hash(self, item: Dict[str, Any], hash_query: str) -> bool: + """Exact-match the normalized query against the item's known hashes.""" + sha256 = item.get("sha256") + sha256_lower = sha256.lower() if isinstance(sha256, str) else "" + if sha256_lower and hash_query in (sha256_lower, sha256_lower[:10]): + return True + # autov3 is None when unchecked and "" when checked but unavailable + autov3 = item.get("autov3") + if isinstance(autov3, str) and autov3 and hash_query == autov3.lower(): + return True + return False + def _matches( self, candidate: str, search_term: str, search_lower: str, fuzzy: bool ) -> bool: diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index a9075955..fd095033 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -216,6 +216,62 @@ justify-content: space-between; } +/* Hashes footnote — borderless full-width muted line; reads as a footnote + to the file info grid rather than a peer field */ +.hash-footnote { + grid-column: 1 / -1; + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 4px 8px; + padding: 0 var(--space-1); + color: var(--text-color); +} + +.hash-footnote .hash-entry { + display: inline-flex; + align-items: baseline; + gap: 6px; +} + +.hash-footnote .hash-kind { + font-size: 0.7em; + opacity: 0.5; + text-transform: uppercase; + letter-spacing: 0.03em; + flex-shrink: 0; +} + +.hash-footnote .model-hash-value { + font-family: monospace; + font-size: 0.8em; + opacity: 0.6; + white-space: nowrap; +} + +.hash-footnote .hash-sep { + opacity: 0.3; + font-size: 0.8em; +} + +.hash-footnote .hash-copy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 2px; + border: none; + background: none; + color: var(--text-color); + opacity: 0.35; + font-size: 0.7em; + cursor: pointer; + flex-shrink: 0; +} + +.hash-footnote .hash-copy-btn:hover { + opacity: 0.9; +} + /* Toggle button — icon only, inline with the label */ .notes-toggle-btn { display: none; /* shown by JS when content exceeds threshold */ diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 6fd07250..3a5bc3ea 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -1337,6 +1337,9 @@ export class BaseModelApiClient { if (pageState.searchOptions.creator !== undefined) { params.append('search_creator', pageState.searchOptions.creator.toString()); } + if (pageState.searchOptions.hash !== undefined) { + params.append('search_hash', pageState.searchOptions.hash.toString()); + } } } diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index f6252f64..85cb55b6 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -316,6 +316,7 @@ async function showModelModalFromCard(card, modelType) { // Create model metadata object const modelMeta = { sha256: card.dataset.sha256, + autov3: card.dataset.autov3 || '', preview_url: getCardPreviewUrl(card), file_path: card.dataset.filepath, model_name: card.dataset.name, @@ -406,6 +407,7 @@ function showExampleAccessModal(card, modelType) { // Get the model data from card dataset (works for both lora and checkpoint) const modelMeta = { sha256: card.dataset.sha256, + autov3: card.dataset.autov3 || '', preview_url: getCardPreviewUrl(card), file_path: card.dataset.filepath, model_name: card.dataset.name, @@ -460,6 +462,7 @@ export function createModelCard(model, modelType) { // below, which ignore internal card drags via MODEL_CARD_DRAG_MIME_TYPE. card.draggable = true; card.dataset.sha256 = model.sha256; + card.dataset.autov3 = model.autov3 || ''; card.dataset.filepath = model.file_path; card.dataset.name = model.model_name; card.dataset.file_name = model.file_name; diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index dee5568d..a6918b54 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -1,4 +1,4 @@ -import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js'; +import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax, copyToClipboard } from '../../utils/uiHelpers.js'; import { modalManager } from '../../managers/ModalManager.js'; import { MODEL_TYPES } from '../../api/apiConfig.js'; import { @@ -351,6 +351,39 @@ export async function showModelModal(model, modelType) { }; const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || ''); const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A'); + // De-emphasized hash display: a borderless full-width footnote line below + // the info grid — sha256 middle-truncated (first 10 + last 6), autov3 in + // full (12 chars); the full value is copied via data-hash. + const modelSha256 = modelWithFullData.sha256 || ''; + const modelAutov3 = modelWithFullData.autov3 || ''; + const truncatedSha256 = modelSha256.length > 16 + ? `${modelSha256.slice(0, 10)}\u2026${modelSha256.slice(-6)}` + : modelSha256; + const copyHashTitle = translate('modals.model.actions.copyHash', {}, 'Copy hash'); + const hashEntries = []; + if (modelSha256) { + hashEntries.push(` + + SHA256 + ${escapeHtml(truncatedSha256)} + + `); + } + if (modelAutov3) { + hashEntries.push(` + + AutoV3 + ${escapeHtml(modelAutov3)} + + `); + } + const hashesMarkup = modelSha256 && hashEntries.length ? ` +