thr3shr / backend /scripts /bench_models_comprehensive.py
Dinamush
Enhance backend API and frontend components for reclassification and debugging. Introduce new request and response schemas for reclassification, update inference settings, and improve handling of inference modes. Add a debug interface for SFW evaluation and integrate reclassification functionality into the frontend, allowing users to retry with different models. Update styles for better UI presentation and ensure proper environment variable handling during startup.
eac1c63
Raw
History Blame Contribute Delete
13.7 kB
#!/usr/bin/env python3
"""
Comprehensive multi-model benchmark (benchmark-only; no production wiring).
Models (via dghs-imgutils where available):
- WD SwinV2 v3, EVA02 Large, ViT Large, ConvNext v3
- ML-Danbooru
- Camie (initial + refined, macro_opt mode)
- PixAI v0.9
Metrics on local SFW Safebooru samples:
- latency (warm + infer)
- known-tag recall for site tags 1girl/solo @ threshold
- taxonomy evidence-tag coverage (vocab)
- taxonomy evidence hit-rate: how often any evidence tag for each
destination appears above threshold on images that carry that tag
in the Safebooru tag string (when present)
Usage (from backend/):
../.venv/Scripts/python.exe scripts/bench_models_comprehensive.py
"""
from __future__ import annotations
import json
import sys
import time
import traceback
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
BACKEND = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BACKEND))
SAMPLE_DIR = ROOT / "sample_data" / "sfw_safebooru"
META_PATH = SAMPLE_DIR / "manifest.json"
TAXONOMY_PATH = BACKEND / "app" / "data" / "taxonomy.json"
OUT_PATH = ROOT / "sample_data" / "bench_models_report.json"
GENERAL_THRESHOLD = 0.35
def _normalize(tag: str) -> str:
text = tag.strip().lower()
parts: list[str] = []
for ch in text:
if ch.isalnum():
parts.append(ch)
elif ch in {" ", "-", ".", "/", "_"}:
parts.append("_")
return "".join(parts).strip("_")
def _as_general_dict(raw: object) -> dict[str, float]:
scores: dict[str, float] = {}
if isinstance(raw, dict):
for tag, score in raw.items():
scores[_normalize(str(tag))] = float(score)
return scores
if isinstance(raw, (list, tuple)):
for part in raw:
if isinstance(part, dict):
for tag, score in part.items():
key = _normalize(str(tag))
scores[key] = max(scores.get(key, 0.0), float(score))
return scores
def load_taxonomy_evidence() -> dict[str, list[str]]:
tax = json.loads(TAXONOMY_PATH.read_text(encoding="utf-8"))
out: dict[str, list[str]] = {}
for bucket in tax["buckets"]:
out[bucket["folder"]] = [
_normalize(e["tag"]) for e in bucket.get("evidence", [])
]
return out
def score_with_model(model_id: str, path: Path) -> dict[str, float]:
if model_id.startswith("wd_"):
from imgutils.tagging import get_wd14_tags
mapping = {
"wd_swinv2_v3": "SwinV2_v3",
"wd_eva02_large": "EVA02_Large",
"wd_vit_large": "ViT_Large",
"wd_convnext_v3": "ConvNext_v3",
}
raw = get_wd14_tags(
str(path),
model_name=mapping[model_id],
general_threshold=GENERAL_THRESHOLD,
no_underline=False,
drop_overlap=False,
fmt="general",
)
return _as_general_dict(raw)
if model_id == "ml_danbooru":
from imgutils.tagging import get_mldanbooru_tags
raw = get_mldanbooru_tags(
str(path),
threshold=GENERAL_THRESHOLD,
size=448,
keep_ratio=True,
drop_overlap=False,
use_real_name=False,
)
return _as_general_dict(raw)
if model_id.startswith("camie_"):
from imgutils.tagging import get_camie_tags
# camie_initial_macro_opt / camie_refined_balanced
name = None
mode = None
for candidate in ("initial", "refined"):
prefix = f"camie_{candidate}_"
if model_id.startswith(prefix):
name = candidate
mode = model_id[len(prefix) :]
break
if name is None or not mode:
raise ValueError(f"Bad camie model id: {model_id}")
raw = get_camie_tags(
str(path),
model_name=name,
mode=mode, # type: ignore[arg-type]
no_underline=False,
drop_overlap=False,
fmt="general",
)
return _as_general_dict(raw)
if model_id == "pixai_v0_9":
from imgutils.tagging import get_pixai_tags
raw = get_pixai_tags(
str(path),
model_name="v0.9",
thresholds=GENERAL_THRESHOLD,
fmt="general",
)
return _as_general_dict(raw)
raise ValueError(f"unknown model_id={model_id}")
def vocab_for_model(model_id: str) -> set[str] | None:
"""Return tag vocabulary when cheaply available; None if unknown."""
try:
if model_id.startswith("wd_"):
from huggingface_hub import hf_hub_download
import pandas as pd
repos = {
"wd_swinv2_v3": "SmilingWolf/wd-swinv2-tagger-v3",
"wd_eva02_large": "SmilingWolf/wd-eva02-large-tagger-v3",
"wd_vit_large": "SmilingWolf/wd-vit-large-tagger-v3",
"wd_convnext_v3": "SmilingWolf/wd-convnext-tagger-v3",
}
path = hf_hub_download(repos[model_id], "selected_tags.csv")
return {_normalize(t) for t in pd.read_csv(path)["name"].tolist()}
if model_id == "ml_danbooru":
from huggingface_hub import hf_hub_download
import pandas as pd
path = hf_hub_download(
"deepghs/imgutils-models", "mldanbooru/mldanbooru_tags.csv"
)
return {_normalize(t) for t in pd.read_csv(path)["name"].tolist()}
if model_id.startswith("camie_"):
from huggingface_hub import hf_hub_download
import pandas as pd
name = "refined" if "refined" in model_id else "initial"
path = hf_hub_download(
"deepghs/camie_tagger_onnx", f"{name}/selected_tags.csv"
)
return {_normalize(t) for t in pd.read_csv(path)["name"].tolist()}
if model_id == "pixai_v0_9":
from imgutils.tagging import pixai
tags_df, _ips = pixai._open_tags("v0.9")
return {_normalize(str(t)) for t in tags_df["name"].tolist()}
except Exception as err:
print(f" vocab_error {model_id}: {err}", flush=True)
return None
return None
def warm_model(model_id: str, path: Path) -> float:
t0 = time.perf_counter()
score_with_model(model_id, path)
return time.perf_counter() - t0
def main() -> int:
# Match production DLL search so imgutils ORT sessions can use CUDA.
from app.providers import ensure_nvidia_dll_search_path, preload_onnx_runtime_dlls
ensure_nvidia_dll_search_path()
preload_onnx_runtime_dlls()
if not META_PATH.exists():
print(
f"ERROR: missing {META_PATH}. Run scripts/bench_sfw_sample.py first.",
file=sys.stderr,
)
return 2
manifest = json.loads(META_PATH.read_text(encoding="utf-8"))
paths = [Path(row["path"]) for row in manifest]
evidence = load_taxonomy_evidence()
all_evidence = sorted({t for tags in evidence.values() for t in tags})
models = [
"wd_swinv2_v3",
"wd_eva02_large",
"wd_vit_large",
"wd_convnext_v3",
"ml_danbooru",
"camie_initial_macro_opt",
"camie_refined_macro_opt",
"pixai_v0_9",
]
report: dict[str, object] = {
"general_threshold": GENERAL_THRESHOLD,
"sample_count": len(paths),
"sample_dir": str(SAMPLE_DIR),
"models": {},
"notes": [
"Camie v2 (Camais03/camie-tagger-v2) is not in dghs-imgutils 0.19; "
"benchmarked ONNX initial/refined mirrors instead.",
"Published F1 numbers are not directly comparable across vendors "
"(different splits/thresholds/tag sets).",
"SFW-only images; taxonomy NSFW folders measured via vocab coverage "
"and evidence-tag presence on images that carry those tags.",
],
}
print(f"samples={len(paths)} models={len(models)}", flush=True)
for model_id in models:
print(f"\n=== {model_id} ===", flush=True)
entry: dict[str, object] = {"ok": False}
try:
vocab = vocab_for_model(model_id)
if vocab is not None:
covered = [t for t in all_evidence if t in vocab]
missing = [t for t in all_evidence if t not in vocab]
per_folder = {}
for folder, tags in evidence.items():
hit = [t for t in tags if t in vocab]
per_folder[folder] = {
"coverage": len(hit) / max(1, len(tags)),
"present": hit,
"missing": [t for t in tags if t not in vocab],
}
entry["vocab_size"] = len(vocab)
entry["taxonomy_evidence_coverage"] = len(covered) / max(
1, len(all_evidence)
)
entry["taxonomy_missing"] = missing
entry["taxonomy_per_folder"] = per_folder
print(
f" vocab={len(vocab)} evidence_coverage="
f"{entry['taxonomy_evidence_coverage']:.1%}",
flush=True,
)
warm_s = warm_model(model_id, paths[0])
print(f" warm={warm_s:.2f}s", flush=True)
must_hits = 0
latencies: list[float] = []
per_image: list[dict] = []
# taxonomy evidence recall on images whose site tags include evidence
tax_denom = {f: 0 for f in evidence}
tax_numer = {f: 0 for f in evidence}
for row, path in zip(manifest, paths):
t0 = time.perf_counter()
scores = score_with_model(model_id, path)
dt = time.perf_counter() - t0
latencies.append(dt)
must = {_normalize(t) for t in row.get("must_have", [])}
known = {_normalize(t) for t in row.get("known_general", [])}
hit = must <= set(scores)
must_hits += int(hit)
top = sorted(scores.items(), key=lambda x: -x[1])[:8]
for folder, ev_tags in evidence.items():
# Only evaluate folders whose evidence appears in site tags
if not (known & set(ev_tags)):
continue
tax_denom[folder] += 1
if any(scores.get(t, 0.0) >= GENERAL_THRESHOLD for t in ev_tags):
tax_numer[folder] += 1
per_image.append(
{
"id": row["id"],
"must_hit": hit,
"latency_s": dt,
"top": top,
"score_count": len(scores),
}
)
print(
f" #{row['id']} must_hit={hit} n={len(scores)} "
f"{dt*1000:.0f}ms top={[t for t,_ in top[:5]]}",
flush=True,
)
tax_recall = {
f: (tax_numer[f] / tax_denom[f] if tax_denom[f] else None)
for f in evidence
}
entry.update(
{
"ok": True,
"warm_s": warm_s,
"infer_total_s": sum(latencies),
"ms_per_image": (sum(latencies) / len(latencies)) * 1000.0,
"must_have_recall": must_hits / len(paths),
"taxonomy_evidence_recall_on_labeled": tax_recall,
"taxonomy_evidence_support": tax_denom,
"per_image": per_image,
}
)
print(
f" SUMMARY recall={entry['must_have_recall']:.0%} "
f"ms/img={entry['ms_per_image']:.1f}",
flush=True,
)
except Exception as err:
entry["ok"] = False
entry["error"] = f"{type(err).__name__}: {err}"
entry["traceback"] = traceback.format_exc()
print(f" FAILED: {entry['error']}", flush=True)
report["models"][model_id] = entry
# Ranking: prioritize must_have recall, then taxonomy coverage, then speed
ranked = []
for model_id, entry in report["models"].items():
if not entry.get("ok"):
continue
ranked.append(
{
"model": model_id,
"must_have_recall": entry.get("must_have_recall"),
"taxonomy_evidence_coverage": entry.get("taxonomy_evidence_coverage"),
"ms_per_image": entry.get("ms_per_image"),
"vocab_size": entry.get("vocab_size"),
}
)
ranked.sort(
key=lambda r: (
-(r["must_have_recall"] or 0),
-(r["taxonomy_evidence_coverage"] or 0),
r["ms_per_image"] or 1e9,
)
)
report["ranking"] = ranked
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"\nWrote {OUT_PATH}", flush=True)
print("\n=== RANKING ===", flush=True)
for i, row in enumerate(ranked, 1):
print(
f"{i}. {row['model']}: recall={row['must_have_recall']:.0%} "
f"tax_cov={row['taxonomy_evidence_coverage']:.0%} "
f"ms/img={row['ms_per_image']:.1f} vocab={row['vocab_size']}",
flush=True,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())