bug. in some case it extract not name but .safetensor
{'model_name': '.safetensors', 'strengths': [1, 1]}
  {'model_name': 'Hyper-SD15-1step-lora', 'strengths': [0.6, 0.6]}


пример
{"inputs": {"lora_01": "SD15\\Hyper-SD15-1step-lora.safetensors", "strength_01": 0.6000000000000001,  #strength_** may be any
ты помещаешь ключ Hyper-SD15-1step-lora (имя safetensors модели) и  strength 0.6 0.6 (тут оба заданы одним значением) в массив strength. и добавить рейтинг

"widgets_values": ["SD15\\_extracted15_128.safetensors", -0.10000000000000002, -0.20000000000000004]}, #next values in array


тут strength model, clip -0.10000000000000002, -0.20000000000000004
ты помещаешь _extracted15_128    -0.1 -0.2   и добавить рейтинг

округляй до 0.001

{"inputs": {"ckpt_name": "SD15\\aaa.safetensors"}, #ckpt_name без strength
тут ckpt_name без strength
ты помещаешь strength  и добавить рейтинг

 
import os
import json
import sqlite3
import struct
import send2trash
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
from pathlib import Path
import re

# Constants
TEXT_KEY = "workflow"
SUPPORTED_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif")
DB_FILENAME = "image_ratings.db"

class ImageViewer:
    def __init__(self, master):
        self.master = master
        master.title("Image Viewer with Rating")

        # -- UI setup ------------------------------------------------------
        self.image_label = ttk.Label(master)
        self.image_label.pack(pady=10)

        # Rating
        self.rating = tk.IntVar(value=0)
        self.rating_label = ttk.Label(master, text="Rating: 0")
        self.rating_label.pack()
        self.rating_frame = ttk.Frame(master)
        self.rating_frame.pack()
        for i in range(1, 6):
            btn = ttk.Button(
                self.rating_frame,
                text=str(i),
                command=lambda v=i: self.rate_and_next(v)
            )
            btn.pack(side=tk.LEFT, padx=3)

        # Delete threshold slider
        self.delete_level = tk.IntVar(value=0)
        self.delete_level_label = ttk.Label(master,
                                            text="Delete Level: 0")
        self.delete_level_label.pack()
        self.delete_slider = tk.Scale(master,
                                      from_=0, to=5,
                                      orient=tk.HORIZONTAL,
                                      variable=self.delete_level,
                                      command=self.update_delete_level)
        self.delete_slider.pack(pady=5)

        # Control Buttons
        self.button_frame = ttk.Frame(master)
        self.button_frame.pack(pady=5)
        ttk.Button(self.button_frame, text="Rename (!)",
                   command=self.rename_image).pack(side=tk.LEFT, padx=5)
        ttk.Button(self.button_frame, text="Next (Q)",
                   command=self.next_image).pack(side=tk.LEFT, padx=5)

        # -- Database setup -----------------------------------------------
        self.db_path = os.path.join(os.getcwd(), DB_FILENAME)
        self.conn = sqlite3.connect(self.db_path)
        self.cursor = self.conn.cursor()
        self._create_table()

        # -- Image list and index -----------------------------------------
        self.image_paths = []
        self.current_index = 0

        # Bind keys: digits = rate+next, Q = just next
        for d in range(1, 6):
            master.bind(str(d), lambda e, v=d: self.rate_and_next(v))
        master.bind("q", lambda e: self.next_image())

        # Load images
        self.load_images()

    def _create_table(self):
        self.cursor.execute("""
            CREATE TABLE IF NOT EXISTS image_ratings (
                image_path TEXT,
                model_name TEXT,
                rating INTEGER,
                strengths TEXT,
                PRIMARY KEY(image_path, model_name)
            )
        """)
        self.conn.commit()

    def load_images(self):
        folder = filedialog.askdirectory(title="Select Image Folder")
        if not folder:
            self.master.destroy()
            return
        self.image_paths = [
            os.path.join(folder, f)
            for f in os.listdir(folder)
            if f.lower().endswith(SUPPORTED_EXTS)
        ]
        if not self.image_paths:
            messagebox.showinfo("Info", "No supported images found.")
            self.master.destroy()
            return
        self.current_index = 0
        self.load_image()

    def load_image(self):
        path = self.image_paths[self.current_index]
        try:
            img = Image.open(path)
            self.photo = ImageTk.PhotoImage(img)
            self.image_label.config(image=self.photo)
            self.image_label.image = self.photo
        except Exception as e:
            messagebox.showerror("Error", f"Cannot open {path}\n{e}")
            return

        # Reset UI controls
        self.rating.set(0)
        self.rating_label.config(text="Rating: 0")
        self.delete_level.set(0)
        self.delete_level_label.config(text="Delete Level: 0")

        # Extract & log for debug
        md = self.extract_and_log_model_data(path)
        print(f"Models in {os.path.basename(path)}:")
        for m in md:
            print(" ", m)

    def rate_and_next(self, value: int):
        """Set the rating, then save/delete/advance."""
        self.rating.set(value)
        self.rating_label.config(text=f"Rating: {value}")
        self.next_image()

    def update_delete_level(self, val):
        self.delete_level_label.config(text=f"Delete Level: {val}")

    def rename_image(self):
        p = self.image_paths[self.current_index]
        d, fn = os.path.split(p)
        new = os.path.join(d, "!" + fn)
        try:
            os.rename(p, new)
            self.image_paths[self.current_index] = new
            messagebox.showinfo("Info", f"Renamed to {new}")
        except Exception as e:
            messagebox.showerror("Error", str(e))

    def next_image(self):
        # Save current rating & model rows
        self.save_rating()
        # Possibly delete
        self.check_and_delete()

        if not self.image_paths:
            messagebox.showinfo("Done", "No more images.")
            self.master.destroy()
            return

        # Advance index
        self.current_index = (self.current_index + 1) % len(self.image_paths)
        self.load_image()

    def save_rating(self):
        path = self.image_paths[self.current_index]
        rating = self.rating.get()
        models = self.extract_and_log_model_data(path)

        for m in models:
            name = m.get("model_name", "<error>")
            strengths = json.dumps(m.get("strengths", []))
            try:
                self.cursor.execute("""
                    INSERT OR REPLACE INTO image_ratings
                    (image_path, model_name, rating, strengths)
                    VALUES (?, ?, ?, ?)
                """, (path, name, rating, strengths))
            except Exception as e:
                print("DB insert error:", e)
        self.conn.commit()
        print(f"Saved rating {rating} for {os.path.basename(path)}")

    def check_and_delete(self):
        rating = self.rating.get()
        thresh = self.delete_level.get()
        # delete if nonzero and ≤ threshold
        if rating != 0 and rating <= thresh:
            p = self.image_paths[self.current_index]
            try:
                send2trash.send2trash(p)
                print("Deleted to trash:", p)
                del self.image_paths[self.current_index]
            except Exception as e:
                messagebox.showerror("Error deleting", str(e))

    def extract_and_log_model_data(self, path: str):
        """
        1) Read the PNG tEXt chunk named TEXT_KEY.
        2) JSON-decode it.
        3) Recurse through dicts/lists to find .safetensors entries:
           - In dicts, match sibling strength_NN keys → [val, val]
           - In lists, take the next 1–2 numeric entries as strengths
           - Skip any subtree where "mode" == 4
        4) Drop any model whose strengths exist and are all zero.
        """
        result = []

        txt = self._extract_text_chunk(path)
        if not txt:
            return [{"error": "No workflow chunk"}]

        try:
            wf = json.loads(txt)
        except Exception as e:
            return [{"error": f"JSON decode error: {e}"}]

        def recurse(node):
            if isinstance(node, dict):
                # Skip disabled nodes
                if node.get("mode") == 4:
                    return
                for key, val in node.items():
                    if isinstance(val, str) and val.lower().endswith(".safetensors"):
                        # Robustly extract name
                        base = os.path.basename(val)
                        name, ext = os.path.splitext(base)
                        if not name:
                            continue  # skip malformed entries
                        strengths = []
                        # Look for a matching strength_NN key
                        m = re.search(r"_(\d+)$", key)
                        if m:
                            idx = m.group(1)
                            strength_key = f"strength_{idx}"
                            s_val = node.get(strength_key)
                            if isinstance(s_val, (int, float)):
                                strengths = [round(s_val, 3)] * 2

                        result.append({
                            "model_name": name,
                            "strengths": strengths
                        })
                    else:
                        recurse(val)

            elif isinstance(node, list):
                i = 0
                while i < len(node):
                    item = node[i]
                    if isinstance(item, str) and item.lower().endswith(".safetensors"):
                        base = os.path.basename(item)
                        name, ext = os.path.splitext(base)
                        if not name:
                            i += 1
                            continue
                        strengths = []
                        # Next up to two numeric items
                        if i+1 < len(node) and isinstance(node[i+1], (int, float)):
                            strengths.append(round(node[i+1], 3))
                        if i+2 < len(node) and isinstance(node[i+2], (int, float)):
                            strengths.append(round(node[i+2], 3))
                        # If only one strength, duplicate it
                        if len(strengths) == 1:
                            strengths.append(strengths[0])

                        result.append({
                            "model_name": name,
                            "strengths": strengths
                        })
                        i += 3
                    else:
                        recurse(item)
                        i += 1

            # other types: ignore

        recurse(wf)

        # Filter out any entry whose strengths exist and are all zero
        cleaned = []
        for m in result:
            s = m.get("strengths", [])
            if s and all(abs(x) < 1e-6 for x in s):
                continue
            cleaned.append(m)

        return cleaned

    def _extract_text_chunk(self, path: str):
        """Return PNG tEXt chunk named TEXT_KEY, or None."""
        try:
            with open(path, "rb") as f:
                # PNG signature
                if f.read(8) != b"\x89PNG\r\n\x1a\n":
                    return None
                while True:
                    length_data = f.read(4)
                    if len(length_data) < 4:
                        return None
                    length = struct.unpack(">I", length_data)[0]
                    ctype = f.read(4).decode("ascii", errors="ignore")
                    data = f.read(length)
                    f.read(4)  # CRC
                    if ctype == "tEXt":
                        parts = data.split(b"\x00", 1)
                        if len(parts) == 2 and parts[0].decode() == TEXT_KEY:
                            return parts[1].decode("utf-8", "ignore")
                    if ctype == "IEND":
                        break
        except Exception as e:
            print("PNG read error:", e)
        return None

    def on_closing(self):
        self.conn.close()
        self.master.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = ImageViewer(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()