#!/usr/bin/env python3 """Generate a LoRA trigger word dictionary HTML page from .safetensors metadata. Usage: lora-trigger-sheet # default: ~/mnt/nimo.loc/ComfyUI/models/loras lora-trigger-sheet /path/to/loras # specify directory lora-trigger-sheet . # current directory lora-trigger-sheet /path/to/loras /path/to/output # specify both input and output """ import json import os import re import struct import sys from pathlib import Path import hashlib import time from urllib.error import HTTPError, URLError from urllib.parse import quote from urllib.request import urlopen, Request def read_safetensors_meta(filepath): try: with open(filepath, "rb") as f: header_len = struct.unpack(" 0: model_id = data[0].get("modelId", "") if model_id: url = f"https://huggingface.co/{model_id}" cache[filename] = { "url": url, "label": "HuggingFace", "model_name": model_id, "last_checked": now, } return url, "HuggingFace" except (HTTPError, URLError, json.JSONDecodeError, OSError): pass # Fallback: ModelScope model search by filename try: q = ( filename.replace(".safetensors", "") .replace("_", " ") .replace("-", " ") ) data = _fetch_json( f"https://www.modelscope.cn/api/v1/models?Name={quote(q)}&PageSize=3&PageNumber=1" ) models = (data or {}).get("Data", {}).get("Models") or [] if models: m = models[0] model_id = m.get("Path") or m.get("Name", "") if model_id: url = f"https://www.modelscope.cn/models/{model_id}" cache[filename] = { "url": url, "label": "ModelScope", "model_name": model_id, "last_checked": now, } return url, "ModelScope" except (HTTPError, URLError, json.JSONDecodeError, OSError): pass cache[filename] = {"url": None, "label": None, "last_checked": now} return None, None def generate_html(loras, sources=None): if sources is None: sources = {} rows = [] no_trigger_files = [] for fname, meta in loras: tool = get_tool(meta) base = shorten_base_model(get_base_model(meta)) tags, total = get_tags(meta) fname_no_ext = re.sub(r"\.safetensors$", "", fname) if tags: sorted_tags = sorted(tags.items(), key=lambda x: -x[1]) top8 = sorted_tags[:8] rest = sorted_tags[8:] has_rest = len(rest) > 0 top8_html = ", ".join( f"{_e(t)} ({c}x)" for t, c in top8 ) rest_html = "" more_id = "" if has_rest: more_id = "m_" + html_id() more_count = sum(c for _, c in rest) rest_html = " " + str(more_count) + " more…" rest_list = ", ".join( f"{_e(t)} ({c}x)" for t, c in rest ) rest_html += "\n " + rest_list + "" triggers = top8_html + rest_html else: no_trigger_files.append((fname_no_ext, tool, base)) triggers = "(none)" src_url, src_label = sources.get(fname, (None, None)) rows.append((fname_no_ext, tool, base, triggers, src_url or "", src_label or "")) num_files = len(rows) table_rows = "" for fname, tool, base, triggers, src_url, src_label in rows: if src_url: src_cell = f'{_e(src_label)}' else: src_cell = '' table_rows += ( f" \n" f" {_e(fname)}\n" f" {_e(tool)}\n" f" {_e(base)}\n" f" {triggers}\n" f" {src_cell}\n" f" \n" ) html = f""" LoRA Info Sheet

LoRA Info Sheet

{num_files} LoRA files · {num_files} visible

{num_files} / {num_files}
{table_rows}
File Tool Base Trigger(s) Source
""" return html def _e(s): """Escape HTML entities.""" s = str(s) s = s.replace("&", "&") s = s.replace("<", "<") s = s.replace(">", ">") s = s.replace('"', """) return s def main(): args = sys.argv[1:] if len(args) >= 1: target = args[0] else: target = os.path.expanduser("~/mnt/nimo.loc/ComfyUI/models/loras") target = os.path.abspath(os.path.expanduser(target)) if not os.path.isdir(target): print(f"Error: {target} is not a directory", file=sys.stderr) sys.exit(1) safetensors = sorted([ f for f in os.listdir(target) if f.endswith(".safetensors") and not f.startswith("._") ]) if not safetensors: print(f"No .safetensors files found in {target}", file=sys.stderr) sys.exit(1) loras = [] for fname in safetensors: fpath = os.path.join(target, fname) meta = read_safetensors_meta(fpath) loras.append((fname, meta)) # Look up source URLs with disk-backed cache cache = load_source_cache() sources = {} total = len(loras) for i, (fname, meta) in enumerate(loras, 1): fpath = os.path.join(target, fname) url, label = lookup_source(fpath, fname, cache, meta) if url: sources[fname] = (url, label) if i % 10 == 0 or i == total: print(f" source lookup: {i}/{total}", file=sys.stderr) save_source_cache(cache) html = generate_html(loras, sources) if len(args) >= 2: out_path = os.path.abspath(os.path.expanduser(args[1])) else: out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lora-info-sheet.html") with open(out_path, "w") as f: f.write(html) out_count = sum(1 for v in sources.values() if v[0]) print(f"Generated {out_path} with {len(loras)} LoRA entries ({out_count} with source links)") if __name__ == "__main__": main()