File size: 14,364 Bytes
a93548b e3b13b6 a93548b e3b13b6 728acdc e3b13b6 a93548b 57423ae a93548b 5e49b02 e3b13b6 a93548b e3b13b6 6cdaa23 e3b13b6 a93548b b54bca0 e3b13b6 b54bca0 e3b13b6 a93548b b54bca0 e3b13b6 3ac5d93 b54bca0 e3b13b6 b54bca0 a93548b b54bca0 3ac5d93 e3b13b6 b54bca0 e3b13b6 3ac5d93 ce8377c b54bca0 ce8377c b54bca0 ce8377c b54bca0 ce8377c b54bca0 ce8377c b54bca0 3ac5d93 b54bca0 3ac5d93 b54bca0 3ac5d93 b54bca0 ce8377c 3ac5d93 b54bca0 3ac5d93 caaf710 2aa979b 3ac5d93 2aa979b 3ac5d93 e3b13b6 3ac5d93 e3b13b6 3ac5d93 e3b13b6 a93548b e3b13b6 b54bca0 e3b13b6 a93548b e3b13b6 728acdc e3b13b6 728acdc e3b13b6 a93548b e3b13b6 a93548b e3b13b6 a93548b e3b13b6 a93548b b54bca0 eddf545 ce8377c b54bca0 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 eddf545 e3b13b6 a93548b e3b13b6 bc43f7c b54bca0 bc43f7c b54bca0 e3b13b6 bc43f7c e3b13b6 a93548b eddf545 ce8377c eddf545 a93548b bc43f7c a93548b e3b13b6 728acdc e3b13b6 728acdc e3b13b6 bc43f7c e3b13b6 eddf545 bc43f7c e3b13b6 2e6adfe a93548b 9d16665 b54bca0 48ae8da a93548b 2e6adfe bc43f7c a93548b bc43f7c 2e6adfe bc43f7c e3b13b6 c65bc80 e3b13b6 a93548b 3b950af a93548b bc43f7c a93548b 2e6adfe 3b950af 5e49b02 3b950af a93548b 3b950af e3b13b6 c65bc80 728acdc bc43f7c 3b950af c65bc80 b54bca0 ce8377c 6bb3162 3b950af 2e6adfe 728acdc e3b13b6 a93548b df50f19 9d16665 df50f19 0779901 6bb3162 a93548b bc43f7c b54bca0 a93548b c90f341 ce8377c a93548b 5e49b02 c90f341 e3b13b6 ce8377c a93548b e3b13b6 bc43f7c e3b13b6 c65bc80 96ab78d b54bca0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | import ast
import csv
import json
import logging
import time
from pathlib import Path
from typing import Dict, Tuple
import gradio as gr
import numpy as np
from fetch_url_util import fetch_image_from_url
from huggingface_hub import hf_hub_download
from PIL import Image
# Configure Logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(message)s")
logger = logging.getLogger("PixAITagger")
logging.getLogger().setLevel(logging.WARNING)
logging.getLogger("PixAITagger").setLevel(logging.INFO)
# Constants
MODEL_REPO = "deepghs/pixai-tagger-v0.9-onnx"
MODEL_FILENAME = "model.onnx"
TAGS_FILENAME = "selected_tags.csv"
INPUT_SIZE = 448
class HybridEngine:
"""
Handles inference with dynamic backend selection.
Priority: OpenVINO INT8 -> OpenVINO FP32 -> ONNX Runtime
"""
def __init__(self, model_path: str, use_int8: bool = True):
self.model_path = model_path
self.session = None
self.use_openvino = False
self.provider_name = None
self.use_int8 = use_int8
self._init_backend()
def _init_backend(self):
# --- Attempt 1 & 2: OpenVINO (INT8 or FP32) ---
try:
import openvino as ov
logger.info("Engine: OpenVINO available, reading model...")
core = ov.Core()
model = core.read_model(self.model_path)
# Logic for INT8 Quantization
if self.use_int8:
try:
import nncf
logger.info("Engine: Compressing weights to INT8 using NNCF...")
model = nncf.compress_weights(model)
self.provider_name = "OpenVINO (INT8 Weights)"
except ImportError:
logger.warning("Engine: NNCF not installed. Falling back to FP32.")
self.provider_name = "OpenVINO (FP32 - NNCF missing)"
except Exception as e:
logger.warning(f"Engine: NNCF Compression failed ({e}). Falling back to FP32.")
self.provider_name = "OpenVINO (FP32 - Compression error)"
else:
self.provider_name = "OpenVINO (FP32)"
# Compile
self.session = core.compile_model(model, "CPU")
self.use_openvino = True
logger.info(f"Engine: Success using {self.provider_name}")
return
except Exception as e:
logger.warning(f"Engine: OpenVINO initialization failed ({e}). Falling back to ONNX Runtime.")
# --- Attempt 3: ONNX Runtime (Fallback) ---
try:
import onnxruntime as ort
sess_options = ort.SessionOptions()
sess_options.log_severity_level = 3
self.session = ort.InferenceSession(
self.model_path,
sess_options=sess_options,
providers=["CPUExecutionProvider"],
)
self.use_openvino = False
self.provider_name = f"ONNX Runtime ({self.session.get_providers()[0]})"
logger.info("Engine: Using ONNX Runtime backend")
except Exception as e:
logger.error(f"Engine: FATAL - ONNX Runtime also failed: {e}")
self.provider_name = "Error: No backend available"
self.session = None
def run(self, input_data: np.ndarray, expected_dim: int):
if not self.session:
raise RuntimeError("Engine not initialized")
if self.use_openvino:
results = self.session(input_data)
outputs = list(results.values())
else:
input_name = self.session.get_inputs()[0].name
outputs = self.session.run(None, {input_name: input_data})
# Pick output with expected class dimension
for out in outputs:
if out.shape[1] == expected_dim:
return out[0], self.provider_name
# Fallback: largest output
out = max(outputs, key=lambda x: x.shape[1])
return out[0], self.provider_name
class PixAITagger:
def __init__(self):
self.model_path = None
self.tags_list = []
self._load_resources()
# State tracking for engine reloading
self.engine = None
self.current_int8_mode = None
def _load_resources(self):
logger.info(f"Downloading resources from {MODEL_REPO}...")
self.model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILENAME)
try:
tags_path = hf_hub_download(repo_id=MODEL_REPO, filename=TAGS_FILENAME)
self._load_tags_csv(Path(tags_path))
except Exception as e:
raise FileNotFoundError(f"Could not load tags file: {e}")
def _load_tags_csv(self, csv_path: Path):
self.tags_list = []
with csv_path.open("r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
try:
idx = int(row.get("id"))
name = row.get("name")
category = int(row.get("category", 0))
ips_raw = row.get("ips", "[]")
ips = []
if ips_raw and ips_raw != "[]":
try:
ips = json.loads(ips_raw)
except Exception:
try:
ips = ast.literal_eval(ips_raw)
except Exception:
pass
self.tags_list.append(
{"id": idx, "name": name, "is_char": category == 4, "ips": [str(ip) for ip in ips]}
)
except ValueError:
continue
self.tags_list.sort(key=lambda x: x["id"])
self.id_to_tag = {}
self.char_indices = []
self.gen_indices = []
self.mapping = {}
for item in self.tags_list:
idx = item["id"]
name = item["name"]
self.id_to_tag[idx] = name
if item["is_char"]:
self.char_indices.append(idx)
if item["ips"]:
self.mapping[name] = item["ips"]
else:
self.gen_indices.append(idx)
self.num_classes = len(self.tags_list)
logger.info(f"Loaded {self.num_classes} tags.")
def preprocess(self, image: Image.Image) -> np.ndarray:
if image.mode != "RGB":
image = image.convert("RGB")
image = image.resize((INPUT_SIZE, INPUT_SIZE), Image.BICUBIC)
img = np.array(image).astype(np.float32) / 255.0
img = (img - 0.5) / 0.5
img = img.transpose(2, 0, 1)
return np.expand_dims(img, 0)
def predict(
self, image: Image.Image, gen_threshold: float, char_threshold: float, resolve_mapping: bool, use_int8_weights: bool
) -> Tuple[Dict, Dict, str, str, float]:
# Reload engine if the INT8 preference changed or engine doesn't exist
if self.engine is None or self.current_int8_mode != use_int8_weights:
logger.info(f"Reloading engine. New mode INT8: {use_int8_weights}")
self.engine = HybridEngine(str(self.model_path), use_int8=use_int8_weights)
self.current_int8_mode = use_int8_weights
input_tensor = self.preprocess(image)
infer_start = time.time()
logits, provider_name = self.engine.run(input_tensor, self.num_classes)
infer_time = time.time() - infer_start
# Sigmoid
probs = 1 / (1 + np.exp(-logits))
# General Tags
gen_tags = {}
for idx in self.gen_indices:
if idx < len(probs):
score = float(probs[idx])
if score >= gen_threshold:
gen_tags[self.id_to_tag[idx]] = score
# Character Tags & IPs
char_tags = {}
detected_ips = set()
for idx in self.char_indices:
if idx < len(probs):
score = float(probs[idx])
if score >= char_threshold:
name = self.id_to_tag[idx]
char_tags[name] = score
if resolve_mapping and name in self.mapping:
for ip in self.mapping[name]:
detected_ips.add(ip)
gen_tags = dict(sorted(gen_tags.items(), key=lambda x: x[1], reverse=True))
char_tags = dict(sorted(char_tags.items(), key=lambda x: x[1], reverse=True))
ip_text = ", ".join(sorted(list(detected_ips))) if detected_ips else ""
return gen_tags, char_tags, ip_text, provider_name, infer_time
# --- UI Setup ---
tagger_instance = None
def get_tagger():
global tagger_instance
if tagger_instance is None:
tagger_instance = PixAITagger()
return tagger_instance
def init_app():
"""Warms up the model loader."""
get_tagger()
return None
def run_inference(image, gen_thresh, char_thresh, resolve_mapping, use_int8):
if image is None:
return "", "", "", {}, {}, ""
try:
model = get_tagger()
start_time = time.time()
gen_tags, char_tags, ip_str, provider, infer_time = model.predict(
image, gen_thresh, char_thresh, resolve_mapping, use_int8
)
char_str = ", ".join(char_tags.keys()).replace("_", " ")
gen_str = ", ".join(gen_tags.keys()).replace("_", " ")
if not resolve_mapping:
ip_disp = ""
elif not ip_str:
ip_disp = ""
else:
ip_disp = ip_str.replace("_", " ")
time_disp = f"- **Provider:** {provider} | **Inference time:** {infer_time:.4f}s"
return char_str, ip_disp, gen_str, char_tags, gen_tags, time_disp
except Exception as e:
logger.error(f"Inference Error: {e}")
return "", f"Error: {str(e)}", "", {}, {}, f"Error: {str(e)}"
with gr.Blocks(title="PixAI Tagger v0.9 ONNX") as demo:
gr.Markdown(
'<a href="https://huggingface.co/pixai-labs/pixai-tagger-v0.9" '
'target="_blank" rel="noopener noreferrer">PixAI Tagger</a>'
' is an iteration on top of '
'<a href="https://huggingface.co/SmilingWolf/wd-eva02-large-tagger-v3" '
'target="_blank" rel="noopener noreferrer">SmilingWolf/wd-eva02-large-tagger-v3</a>'
' with an updated dataset (2025-01). \n'
'It should be noted that PixAI Tagger may be worse in accuracy over eva02 large. See the PixAI page for details'
)
# Header Row
with gr.Row(elem_classes=["container"]):
with gr.Column(scale=1, elem_classes=["header-col"]):
gr.Markdown("### Input")
with gr.Column(scale=1, elem_classes=["header-col"]):
gr.Markdown("### Configuration & Results")
with gr.Row(elem_classes=["container"]):
# LEFT COLUMN
with gr.Column(scale=1):
url_input = gr.Textbox(
label="Enter Image URL (not all may work) or upload an image below",
placeholder="https://example.com/image.jpg",
)
input_img = gr.Image(type="pil", label="", show_label=False, elem_classes=["image-container"])
# RIGHT COLUMN - Controls and Outputs
with gr.Column(scale=1):
# Action Buttons
with gr.Row():
run_btn = gr.Button("Run (Image Upload)", variant="primary")
url_btn = gr.Button("Run (URL)", variant="secondary")
# Output Textboxes
with gr.Group():
with gr.Row(elem_id="d-row-container"):
char_box = gr.Textbox(label="Character Tags", interactive=False, buttons=["copy"])
ip_box = gr.Textbox(label="Character - Copyright Mapping", interactive=False, buttons=["copy"])
gen_box = gr.Textbox(label="General Tags", interactive=False, buttons=["copy"])
time_info = gr.Markdown(elem_id="time-display")
# Configuration Section
with gr.Group():
with gr.Row(elem_id="d-row-container"):
char_slider = gr.Slider(0.0, 1.0, value=0.75, step=0.05, label="Character Threshold")
gen_slider = gr.Slider(0.0, 1.0, value=0.30, step=0.05, label="General Threshold")
# Checkboxes Row
with gr.Row():
map_checkbox = gr.Checkbox(value=True, label="Resolve Copyright Mapping")
int8_checkbox = gr.Checkbox(value=True, label="INT8 Weights")
# Confidence Plots
gr.Markdown("### Confidence Scores")
char_plot = gr.Label(label="Character Probabilities", num_top_classes=50)
gen_plot = gr.Label(label="General Probabilities", num_top_classes=500)
gr.Markdown(
'Model sourced from '
'<a href="https://huggingface.co/deepghs/pixai-tagger-v0.9-onnx" '
'target="_blank" rel="noopener noreferrer">deepghs/pixai-tagger-v0.9-onnx</a>. \n'
'OpenVINO™ will be used to accelerate CPU inference with ONNX CPUExecutionProvider as fallback. \n'
'INT8 weights option may improve inference times with a deviation of roughly +-0.005 in scores.'
)
# Click Logic
# Added int8_checkbox to inputs
url_btn.click(fn=fetch_image_from_url, inputs=[url_input], outputs=[input_img]).then(
fn=run_inference,
inputs=[input_img, gen_slider, char_slider, map_checkbox, int8_checkbox],
outputs=[char_box, ip_box, gen_box, char_plot, gen_plot, time_info],
)
run_btn.click(
fn=run_inference,
inputs=[input_img, gen_slider, char_slider, map_checkbox, int8_checkbox],
outputs=[char_box, ip_box, gen_box, char_plot, gen_plot, time_info],
)
# Load logic
demo.load(fn=init_app)
if __name__ == "__main__":
demo.launch(
theme=gr.themes.Base(),
css="""
* { box-sizing: border-box; }
@media (max-width: 1022px) {
#d-row-container {
flex-direction: column !important;
}
#d-row-container > * {
width: 100% !important;
}
#d-row-container .block {
width: 100% !important;
}
}""",
) |