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 | #!/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()) | |