lora sheet update
This commit is contained in:
@@ -15,6 +15,11 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import urlopen, Request
|
||||||
|
|
||||||
|
|
||||||
def read_safetensors_meta(filepath):
|
def read_safetensors_meta(filepath):
|
||||||
try:
|
try:
|
||||||
@@ -127,7 +132,106 @@ def html_id():
|
|||||||
return hashlib.md5(str(time.monotonic_ns()).encode()).hexdigest()[:8]
|
return hashlib.md5(str(time.monotonic_ns()).encode()).hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
def generate_html(loras):
|
# --- Source URL lookup cache ---
|
||||||
|
|
||||||
|
CACHE_DIR = os.path.expanduser("~/.cache")
|
||||||
|
CACHE_FILE = os.path.join(CACHE_DIR, "lora-sources.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_source_cache():
|
||||||
|
if os.path.exists(CACHE_FILE):
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_source_cache(cache):
|
||||||
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
with open(CACHE_FILE, "w") as f:
|
||||||
|
json.dump(cache, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def file_blake2b_hash(filepath):
|
||||||
|
h = hashlib.blake2b(digest_size=32)
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = f.read(65536)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_json(url, timeout=10):
|
||||||
|
req = Request(url, headers={"User-Agent": "lora-trigger-sheet/1.0"})
|
||||||
|
with urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read().decode())
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_source(filepath, filename, cache):
|
||||||
|
now = int(time.time())
|
||||||
|
cached = cache.get(filename)
|
||||||
|
if cached:
|
||||||
|
url = cached.get("url")
|
||||||
|
label = cached.get("label")
|
||||||
|
if url and (now - cached.get("last_checked", 0)) < 86400 * 30:
|
||||||
|
return url, label
|
||||||
|
|
||||||
|
# CivitAI model-version lookup by BLAKE2b file hash
|
||||||
|
try:
|
||||||
|
h = file_blake2b_hash(filepath)
|
||||||
|
data = _fetch_json(
|
||||||
|
f"https://civitai.com/api/v1/model-versions/by-hash/{h}"
|
||||||
|
)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
model_id = data.get("modelId")
|
||||||
|
version_id = data.get("id")
|
||||||
|
if model_id:
|
||||||
|
url = f"https://civitai.com/models/{model_id}"
|
||||||
|
if version_id:
|
||||||
|
url += f"?modelVersionId={version_id}"
|
||||||
|
cache[filename] = {
|
||||||
|
"url": url, "label": "CivitAI",
|
||||||
|
"model_name": data.get("name", ""),
|
||||||
|
"last_checked": now,
|
||||||
|
}
|
||||||
|
return url, "CivitAI"
|
||||||
|
except (HTTPError, URLError, json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: HuggingFace model search by filename
|
||||||
|
try:
|
||||||
|
q = (
|
||||||
|
filename.replace(".safetensors", "")
|
||||||
|
.replace("_", " ")
|
||||||
|
.replace("-", " ")
|
||||||
|
)
|
||||||
|
data = _fetch_json(
|
||||||
|
"https://huggingface.co/api/models"
|
||||||
|
f"?search={q}&sort=downloads&direction=-1&limit=3"
|
||||||
|
)
|
||||||
|
if isinstance(data, list) and len(data) > 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
|
||||||
|
|
||||||
|
cache[filename] = {"url": None, "label": None, "last_checked": now}
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html(loras, sources=None):
|
||||||
|
if sources is None:
|
||||||
|
sources = {}
|
||||||
rows = []
|
rows = []
|
||||||
no_trigger_files = []
|
no_trigger_files = []
|
||||||
|
|
||||||
@@ -165,18 +269,24 @@ def generate_html(loras):
|
|||||||
no_trigger_files.append((fname_no_ext, tool, base))
|
no_trigger_files.append((fname_no_ext, tool, base))
|
||||||
triggers = "<span class=\"n\">(none)</span>"
|
triggers = "<span class=\"n\">(none)</span>"
|
||||||
|
|
||||||
rows.append((fname_no_ext, tool, base, triggers))
|
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)
|
num_files = len(rows)
|
||||||
|
|
||||||
table_rows = ""
|
table_rows = ""
|
||||||
for fname, tool, base, triggers in rows:
|
for fname, tool, base, triggers, src_url, src_label in rows:
|
||||||
|
if src_url:
|
||||||
|
src_cell = f'<a href="{_e(src_url)}" target="_blank" rel="noopener">{_e(src_label)}</a>'
|
||||||
|
else:
|
||||||
|
src_cell = '<span class="n">—</span>'
|
||||||
table_rows += (
|
table_rows += (
|
||||||
f" <tr>\n"
|
f" <tr>\n"
|
||||||
f" <td class=\"fn\">{_e(fname)}</td>\n"
|
f" <td class=\"fn\">{_e(fname)}</td>\n"
|
||||||
f" <td class=\"tl\">{_e(tool)}</td>\n"
|
f" <td class=\"tl\">{_e(tool)}</td>\n"
|
||||||
f" <td class=\"bm\">{_e(base)}</td>\n"
|
f" <td class=\"bm\">{_e(base)}</td>\n"
|
||||||
f" <td class=\"tr\">{triggers}</td>\n"
|
f" <td class=\"tr\">{triggers}</td>\n"
|
||||||
|
f" <td class=\"sr\">{src_cell}</td>\n"
|
||||||
f" </tr>\n"
|
f" </tr>\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -209,8 +319,9 @@ tr:hover td{{background:#161b22}}
|
|||||||
.ml:hover{{border-bottom:1px solid #58a6ff}}
|
.ml:hover{{border-bottom:1px solid #58a6ff}}
|
||||||
.ht{{display:none}}
|
.ht{{display:none}}
|
||||||
mark.hl{{background:#264f78;color:#f0f6fc;border-radius:2px;padding:0 2px}}
|
mark.hl{{background:#264f78;color:#f0f6fc;border-radius:2px;padding:0 2px}}
|
||||||
.c0{{width:26%}}.c1{{width:9%}}.c2{{width:13%}}.c3{{width:52%}}
|
.sr a{{color:#58a6ff;text-decoration:none;font-size:0.78rem}}
|
||||||
@media(max-width:768px){{.c1,.c2,td:nth-child(2),td:nth-child(3){{display:none}}.c0{{width:30%}}.c3{{width:70%}}}}
|
.c0{{width:24%}}.c1{{width:8%}}.c2{{width:12%}}.c3{{width:48%}}.c4{{width:8%}}
|
||||||
|
@media(max-width:768px){{.c1,.c2,.c4,td:nth-child(2),td:nth-child(3),td:nth-child(5){{display:none}}.c0{{width:30%}}.c3{{width:70%}}}}
|
||||||
.foot{{margin-top:20px;color:#8b949e}}
|
.foot{{margin-top:20px;color:#8b949e}}
|
||||||
.foot h2{{font-size:1rem;color:#f0f6fc;margin-bottom:6px}}
|
.foot h2{{font-size:1rem;color:#f0f6fc;margin-bottom:6px}}
|
||||||
.foot ul{{list-style:none;columns:3;font-size:0.82rem}}
|
.foot ul{{list-style:none;columns:3;font-size:0.82rem}}
|
||||||
@@ -225,6 +336,7 @@ mark.hl{{background:#264f78;color:#f0f6fc;border-radius:2px;padding:0 2px}}
|
|||||||
<th class="c1" onclick="s(1)">Tool <span class="ar"></span></th>
|
<th class="c1" onclick="s(1)">Tool <span class="ar"></span></th>
|
||||||
<th class="c2" onclick="s(2)">Base <span class="ar"></span></th>
|
<th class="c2" onclick="s(2)">Base <span class="ar"></span></th>
|
||||||
<th class="c3" onclick="s(3)">Trigger(s) <span class="ar"></span></th>
|
<th class="c3" onclick="s(3)">Trigger(s) <span class="ar"></span></th>
|
||||||
|
<th class="c4" onclick="s(4)">Source <span class="ar"></span></th>
|
||||||
</tr></thead><tbody id="b">
|
</tr></thead><tbody id="b">
|
||||||
{table_rows}</tbody></table>
|
{table_rows}</tbody></table>
|
||||||
|
|
||||||
@@ -354,7 +466,20 @@ def main():
|
|||||||
meta = read_safetensors_meta(fpath)
|
meta = read_safetensors_meta(fpath)
|
||||||
loras.append((fname, meta))
|
loras.append((fname, meta))
|
||||||
|
|
||||||
html = generate_html(loras)
|
# Look up source URLs with disk-backed cache
|
||||||
|
cache = load_source_cache()
|
||||||
|
sources = {}
|
||||||
|
total = len(loras)
|
||||||
|
for i, (fname, _) in enumerate(loras, 1):
|
||||||
|
fpath = os.path.join(target, fname)
|
||||||
|
url, label = lookup_source(fpath, fname, cache)
|
||||||
|
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:
|
if len(args) >= 2:
|
||||||
out_path = os.path.abspath(os.path.expanduser(args[1]))
|
out_path = os.path.abspath(os.path.expanduser(args[1]))
|
||||||
@@ -364,7 +489,8 @@ def main():
|
|||||||
with open(out_path, "w") as f:
|
with open(out_path, "w") as f:
|
||||||
f.write(html)
|
f.write(html)
|
||||||
|
|
||||||
print(f"Generated {out_path} with {len(loras)} LoRA entries")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user