| import os |
| import csv |
| import json |
| import ast |
| import time |
| import logging |
| from pathlib import Path |
| from typing import Dict, List, Tuple, Any, Optional |
|
|
| import numpy as np |
| import gradio as gr |
| from PIL import Image |
| from huggingface_hub import hf_hub_download |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(message)s') |
| logger = logging.getLogger("PixAITagger") |
|
|
| |
| MODEL_REPO = "deepghs/pixai-tagger-v0.9-onnx" |
| MODEL_FILENAME = "model.onnx" |
| TAGS_FILENAME = "selected_tags.csv" |
| INPUT_SIZE = 448 |
|
|
| class HybridEngine: |
| """ |
| Handles inference, allowing dynamic switching between OpenVINO and ONNX Runtime. |
| """ |
| def __init__(self, model_path: str): |
| self.model_path = model_path |
| self.ov_session = None |
| self.ort_session = None |
| self.ov_available = False |
| |
| |
| try: |
| import openvino as ov |
| self.ov_available = True |
| except ImportError: |
| self.ov_available = False |
|
|
| def _get_ort_session(self): |
| """Lazy load ONNX Runtime session""" |
| if self.ort_session is None: |
| import onnxruntime as ort |
| sess_options = ort.SessionOptions() |
| sess_options.log_severity_level = 3 |
| logger.info("Engine: Initializing ONNX Runtime...") |
| self.ort_session = ort.InferenceSession(self.model_path, sess_options=sess_options, providers=["CPUExecutionProvider"]) |
| return self.ort_session |
|
|
| def _get_ov_session(self): |
| """Lazy load OpenVINO session""" |
| if not self.ov_available: |
| raise ImportError("OpenVINO not installed") |
| |
| if self.ov_session is None: |
| import openvino as ov |
| core = ov.Core() |
| |
| logger.info("Engine: Compiling OpenVINO model...") |
| model_ov = core.read_model(self.model_path) |
| self.ov_session = core.compile_model(model_ov, "CPU") |
| return self.ov_session |
|
|
| def run(self, input_data: np.ndarray, expected_dim: int) -> Tuple[np.ndarray, str]: |
| """ |
| Runs inference. Returns (logits, provider_name). |
| Tries OpenVINO first, falls back to ONNX Runtime if needed. |
| """ |
| |
| if self.ov_available: |
| try: |
| sess = self._get_ov_session() |
| |
| request = sess.create_infer_request() |
| results = request.infer(input_data) |
| |
| |
| output_tensor = None |
| for res_data in results.values(): |
| if res_data.shape[1] == expected_dim: |
| output_tensor = res_data[0] |
| break |
| |
| if output_tensor is None: |
| |
| output_tensor = max(results.values(), key=lambda x: x.shape[1])[0] |
| |
| return output_tensor, "OpenVINO (CPU)" |
| |
| except Exception as e: |
| logger.warning(f"OpenVINO execution failed: {e}. Falling back to ONNX Runtime.") |
| |
|
|
| |
| sess = self._get_ort_session() |
| input_name = sess.get_inputs()[0].name |
| outputs = sess.run(None, {input_name: input_data}) |
| |
| output_tensor = None |
| for out in outputs: |
| if out.shape[1] == expected_dim: |
| output_tensor = out[0] |
| break |
| |
| if output_tensor is None: |
| output_tensor = max(outputs, key=lambda x: x.shape[1])[0] |
|
|
| provider = sess.get_providers()[0] |
| return output_tensor, f"ONNX Runtime ({provider})" |
|
|
| class PixAITagger: |
| def __init__(self): |
| self.model_path = None |
| self.tags_list = [] |
| self._load_resources() |
| self.engine = HybridEngine(str(self.model_path)) |
|
|
| 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: |
| try: |
| ips = ast.literal_eval(ips_raw) |
| except: |
| 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) -> Tuple[Dict, Dict, str, str]: |
| |
| input_tensor = self.preprocess(image) |
| |
| |
| logits, provider_name = self.engine.run(input_tensor, self.num_classes) |
| |
| |
| probs = 1 / (1 + np.exp(-logits)) |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
|
|
| 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 and prevents Gradio return-value warnings.""" |
| get_tagger() |
| return None |
|
|
| def run_inference(image, gen_thresh, char_thresh, resolve_mapping): |
| if image is None: |
| return "", "", "", {}, {}, "" |
| try: |
| model = get_tagger() |
| start_time = time.time() |
| |
| |
| gen_tags, char_tags, ip_str, provider = model.predict(image, gen_thresh, char_thresh, resolve_mapping) |
| |
| taken = time.time() - start_time |
| |
| char_str = ", ".join(char_tags.keys()).replace("_", " ") |
| gen_str = ", ".join(gen_tags.keys()).replace("_", " ") |
| |
| if not resolve_mapping: |
| ip_disp = "Mapping Disabled" |
| elif not ip_str: |
| ip_disp = "No specific copyright detected" |
| else: |
| ip_disp = ip_str.replace("_", " ") |
|
|
| time_disp = f"- **Provider:** {provider} | **Time taken:** {taken:.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)}" |
|
|
| css = """ |
| .container { max-width: 1200px; margin: 0 auto; } |
| /* Align headers by removing default top margins */ |
| .header-col h3 { margin-top: 0 !important; margin-bottom: 10px !important; } |
| .image-container img { |
| max-height: 80dvh !important; |
| width: auto !important; |
| margin: 0 auto; |
| object-fit: contain; |
| } |
| #time-display { |
| margin-top: 10px; |
| padding-top: 5px; |
| border-top: 1px solid rgba(128,128,128,0.1); |
| font-size: 0.85em; |
| opacity: 0.8; |
| } |
| footer { visibility: hidden; } |
| """ |
|
|
| with gr.Blocks(title="PixAI Tagger v0.9") as demo: |
| |
| |
| with gr.Row(elem_classes=["container"]): |
| with gr.Column(scale=1, elem_classes=["header-col"]): |
| gr.Markdown("### Input Image") |
| with gr.Column(scale=1, elem_classes=["header-col"]): |
| gr.Markdown("### Results") |
|
|
| with gr.Row(elem_classes=["container"]): |
| |
| with gr.Column(scale=1): |
| input_img = gr.Image( |
| type="pil", |
| label="", |
| show_label=False, |
| elem_classes=["image-container"] |
| ) |
| with gr.Group(): |
| run_btn = gr.Button("Analyze Image", variant="primary") |
| |
| gr.Markdown("### Configuration") |
| with gr.Group(): |
| char_slider = gr.Slider(0.0, 1.0, value=0.85, step=0.05, label="Character Threshold") |
| gen_slider = gr.Slider(0.0, 1.0, value=0.30, step=0.05, label="General Threshold") |
| map_checkbox = gr.Checkbox(value=True, label="Resolve Copyright Mapping") |
|
|
| |
| with gr.Column(scale=1): |
| |
| with gr.Group(): |
| |
| 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") |
|
|
| gr.Markdown("### Confidence") |
| char_plot = gr.Label(label="Character Probabilities", num_top_classes=50) |
| gen_plot = gr.Label(label="General Probabilities", num_top_classes=50) |
|
|
| |
| run_btn.click( |
| fn=run_inference, |
| inputs=[input_img, gen_slider, char_slider, map_checkbox], |
| outputs=[char_box, ip_box, gen_box, char_plot, gen_plot, time_info] |
| ) |
|
|
| |
| demo.load(fn=init_app) |
|
|
| if __name__ == "__main__": |
| |
| demo.launch( |
| theme=gr.themes.Base(), |
| css=css, |
| api_open=False |
| ) |