almost everything
just about all the features for the browser only. Next is to make the wedge node and incorporate it.
This commit is contained in:
@@ -2,7 +2,21 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(Get-ChildItem -Path \"d:\\\\My Actual Documents\\\\Projects\\\\Code\\\\PyProjects\\\\mb_gui\" -Recurse -File)",
|
||||
"Bash(Select-Object FullName)"
|
||||
"Bash(Select-Object FullName)",
|
||||
"Bash(python mbq_parser.py \"images/straight_gen_monkeys.png\")",
|
||||
"Bash(python mbq_parser.py \"images/disc_fix_00006_.png\")",
|
||||
"Bash(python mbq_parser.py \"images/controlnet_i2i_tableLady_00004_.png\")",
|
||||
"Bash(del screenshot.png screenshot_loaded.png screenshot_with_image.png)",
|
||||
"Bash(find \"d:/My Actual Documents/Projects/Code/PyProjects/mb_gui\" -name \"ComfyUI_00101_betterHand.png\" 2>/dev/null; find \"d:/My Actual Documents\" -name \"ComfyUI_00101_betterHand.png\" 2>/dev/null | head -5)",
|
||||
"Read(//d/My Actual Documents/**)",
|
||||
"Bash(find / -name \"ComfyUI_00101_betterHand.png\" 2>/dev/null | head -5)",
|
||||
"Read(//d//**)",
|
||||
"Bash(python mbq_parser.py \"images/redux_1_00002_.png\")",
|
||||
"Bash(python dump_png_text_chunks.py \"images/redux_1_00002_.png\")",
|
||||
"Bash(python -c ' *)",
|
||||
"Bash(Get-ChildItem -Path \"d:\\\\My Actual Documents\\\\Projects\\\\Code\\\\PyProjects\\\\mb_gui\" -Filter \"*.py\")",
|
||||
"Bash(Select-Object -ExpandProperty Name)",
|
||||
"Bash(Sort-Object)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,25 +22,21 @@ too complex and was the root cause of prior pain — ignore it entirely.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `mbq_layout.py` | Main `MetaViewApp` window — PySide6 QMainWindow, UI layout |
|
||||
| `mbq_browser.py` | Main `MetaViewApp` window — PySide6 QMainWindow, UI layout, menus |
|
||||
| `mbq_logic.py` | `MetaViewLogicMixin` — image navigation, metadata display, file dialog |
|
||||
| `mbq_functions.py` | `ImageFolder`, `ImageCanvas` (QGraphicsView), `WorkflowCache` |
|
||||
| `mbq_parser.py` | `digest_workflow()` — PNG chunk extraction + ComfyUI prompt graph traversal |
|
||||
| `mbq_metadata.py` | `ImageMetadata` dataclass — unified structure for all extracted fields |
|
||||
| `mbq_nodes.py` | Static ComfyUI node registry (Jan 2025 snapshot) — used by CLI tools only |
|
||||
| `mbq_parser.py` | `get_png_metadata()` — PNG chunk extraction + ComfyUI prompt graph traversal |
|
||||
| `mbq_nodes.py` | Static ComfyUI node registry (Jan 2025 snapshot) — reference only |
|
||||
| `dump_png_text_chunks.py` | CLI: dump raw PNG text chunks for debugging |
|
||||
| `mbq_audit_current_logic.py` | CLI: audit parser decisions on a batch of PNGs |
|
||||
| `mbq_dump_testset.py` | CLI: generate metadata reports from a test image set |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
MetaViewApp (mbq_layout.py)
|
||||
MetaViewApp (mbq_browser.py)
|
||||
├─ MetaViewLogicMixin (mbq_logic.py)
|
||||
│ └─ digest_workflow() ← mbq_parser.py
|
||||
│ └─ ImageMetadata ← mbq_metadata.py
|
||||
│ └─ get_png_metadata() ← mbq_parser.py
|
||||
├─ ImageCanvas, ImageFolder, WorkflowCache (mbq_functions.py)
|
||||
└─ [mbq_nodes.py — CLI audit tools only, not used by the GUI]
|
||||
└─ [mbq_nodes.py — reference only, not used by the GUI]
|
||||
```
|
||||
|
||||
## PNG chunk format
|
||||
@@ -74,13 +70,17 @@ prompt graph (KSampler, CheckpointLoaderSimple, CLIPTextEncode, etc.).
|
||||
|
||||
```
|
||||
# Run the GUI
|
||||
python mbq_layout.py
|
||||
python mbq_browser.py
|
||||
|
||||
# Open directly to an image or folder
|
||||
python mbq_browser.py path/to/image.png
|
||||
python mbq_browser.py path/to/folder
|
||||
|
||||
# Inspect raw chunks in a PNG before debugging the parser
|
||||
python dump_png_text_chunks.py <file.png>
|
||||
|
||||
# Batch-audit parser decisions against a folder of test images
|
||||
python mbq_audit_current_logic.py <image_dir>
|
||||
# Pretty-print parsed metadata (what the GUI sees)
|
||||
python mbq_parser.py <file.png>
|
||||
```
|
||||
|
||||
No formal test runner exists. Test images are stored under `project_docs/`.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
+158
-76
@@ -2,43 +2,56 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QFrame, QGroupBox,
|
||||
QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton,
|
||||
QScrollArea, QTextEdit,
|
||||
QScrollArea, QTextEdit, QMessageBox, QFileDialog,
|
||||
)
|
||||
from PySide6.QtGui import QAction
|
||||
from mbq_functions import ImageCanvas
|
||||
from PySide6.QtCore import Qt, QTimer, QEvent
|
||||
from PySide6.QtGui import QAction, QDesktopServices
|
||||
from PySide6.QtCore import Qt, QTimer, QEvent, QUrl
|
||||
from mbq_functions import ImageCanvas, WorkflowCache
|
||||
from mbq_parser import get_png_metadata
|
||||
from mbq_functions import WorkflowCache
|
||||
from mbq_logic import MetaViewLogicMixin
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class StatusBulb(QLabel):
|
||||
_COLORS = {"on": "#44cc44", "warn": "#c8823a", "off": "#2a2a2a"}
|
||||
|
||||
def __init__(self, tooltip_name, parent=None):
|
||||
super().__init__(parent)
|
||||
self._name = tooltip_name
|
||||
self.setFixedSize(14, 14)
|
||||
self._state = "off"
|
||||
self._apply()
|
||||
|
||||
def set_state(self, state: str):
|
||||
if state != self._state:
|
||||
self._state = state
|
||||
self._apply()
|
||||
|
||||
def _apply(self):
|
||||
c = self._COLORS[self._state]
|
||||
self.setStyleSheet(f"background:{c}; border-radius:7px; border:1px solid #555;")
|
||||
self.setToolTip(f"{self._name}: {'ON' if self._state != 'off' else 'OFF'}")
|
||||
|
||||
|
||||
class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
# Now inherits from both QMainWindow and our logic mixin
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Image Viewer GL Layout")
|
||||
self.setWindowTitle("MBQ — Meta Browser")
|
||||
self.workflow_cache = WorkflowCache(max_size=50)
|
||||
|
||||
# --- 4:3 Optimized Window Size ---
|
||||
# Calculate for 1024x768 image + UI elements
|
||||
self.resize(1400, 900) # Wider window to accommodate right sidebar
|
||||
|
||||
self.resize(1400, 900)
|
||||
|
||||
self.folder_model = None
|
||||
self.image_cache = {}
|
||||
self.thumb_cache = {}
|
||||
self.cache_size = 5
|
||||
|
||||
# --- Design reference sizes for 4:3 ---
|
||||
self.design_canvas_w = 1024 # Target image width
|
||||
self.design_canvas_h = 768 # Target image height
|
||||
self.design_thumb_w = 120 # Thumbnail width
|
||||
self.design_thumb_h = 90 # Thumbnail height (4:3)
|
||||
|
||||
self.design_canvas_w = 1024
|
||||
self.design_canvas_h = 768
|
||||
self.design_thumb_w = 120
|
||||
self.design_thumb_h = 90
|
||||
|
||||
self.scale_factor = 1.0
|
||||
|
||||
# ---- Central Widget ----
|
||||
@@ -50,59 +63,56 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
self.main_layout.setContentsMargins(10, 20, 10, 20)
|
||||
self.main_layout.setSpacing(10)
|
||||
|
||||
# Column weights - center area for image, right for metadata
|
||||
self.main_layout.setColumnStretch(0, 0) # left aesthetic spacer (fixed)
|
||||
self.main_layout.setColumnStretch(1, 3) # center image area (main content)
|
||||
self.main_layout.setColumnStretch(2, 1) # right metadata panel (~1/4 width)
|
||||
self.main_layout.setRowStretch(0, 1) # main row expands
|
||||
self.main_layout.setColumnStretch(0, 0)
|
||||
self.main_layout.setColumnStretch(1, 3)
|
||||
self.main_layout.setColumnStretch(2, 1)
|
||||
self.main_layout.setRowStretch(0, 1)
|
||||
|
||||
# --- Left Spacer (Aesthetic) ---
|
||||
# --- Left Spacer ---
|
||||
self.left_spacer = QFrame()
|
||||
self.left_spacer.setFixedWidth(40) # Small fixed width spacer
|
||||
self.left_spacer.setFixedWidth(40)
|
||||
self.left_spacer.setStyleSheet("background: transparent;")
|
||||
self.main_layout.addWidget(self.left_spacer, 0, 0)
|
||||
|
||||
# --- Center Image Area ---
|
||||
self.center_group = QGroupBox("Image View")
|
||||
self.center_group.setStyleSheet("""
|
||||
QGroupBox {
|
||||
border: 2px solid #333333;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left;
|
||||
left: 10px;
|
||||
padding: 0 5px 0 5px;
|
||||
background: palette(window);
|
||||
QGroupBox {
|
||||
border: 2px solid #333333;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left;
|
||||
left: 10px;
|
||||
padding: 0 5px 0 5px;
|
||||
background: palette(window);
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
self.main_layout.addWidget(self.center_group, 0, 1)
|
||||
center_layout = QGridLayout(self.center_group)
|
||||
center_layout.setRowStretch(0, 5) # image view (most space)
|
||||
center_layout.setRowStretch(1, 1) # filmstrip
|
||||
center_layout.setRowStretch(2, 0) # buttons
|
||||
center_layout.setRowStretch(0, 5)
|
||||
center_layout.setRowStretch(1, 1)
|
||||
center_layout.setRowStretch(2, 0)
|
||||
center_layout.setColumnStretch(0, 1)
|
||||
|
||||
# ---- Image Canvas ----
|
||||
self.image_view = ImageCanvas()
|
||||
self.image_view.fileDropped.connect(self.load_image_from_path)
|
||||
self.image_view.setMinimumSize(800, 600) # 4:3 minimum
|
||||
self.image_view.setMinimumSize(800, 600)
|
||||
center_layout.addWidget(self.image_view, 0, 0)
|
||||
|
||||
# ---- Filmstrip Frame ----
|
||||
self.filmstrip_frame = QFrame()
|
||||
filmstrip_layout = QVBoxLayout(self.filmstrip_frame)
|
||||
|
||||
# Top separator
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.HLine)
|
||||
sep.setStyleSheet("color: #252222;")
|
||||
filmstrip_layout.addWidget(sep)
|
||||
|
||||
# Thumbnail scroller
|
||||
self.thumb_scroll_area = QScrollArea()
|
||||
self.thumb_scroll_area.setWidgetResizable(True)
|
||||
self.thumb_scroll_area.setMinimumHeight(100)
|
||||
@@ -110,12 +120,13 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
self.thumb_layout = QHBoxLayout(self.thumb_container)
|
||||
self.thumb_scroll_area.setWidget(self.thumb_container)
|
||||
filmstrip_layout.addWidget(self.thumb_scroll_area)
|
||||
|
||||
|
||||
center_layout.addWidget(self.filmstrip_frame, 1, 0)
|
||||
|
||||
# ---- Button Frame ----
|
||||
self.button_frame = QFrame()
|
||||
btn_layout = QHBoxLayout(self.button_frame)
|
||||
btn_layout.setContentsMargins(0, 2, 0, 2)
|
||||
|
||||
next_btn = QPushButton("Next Image")
|
||||
prev_btn = QPushButton("Previous Image")
|
||||
@@ -125,12 +136,40 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
btn_layout.addWidget(prev_btn)
|
||||
btn_layout.addWidget(open_btn)
|
||||
|
||||
# ---- Status Bulbs ----
|
||||
btn_layout.addStretch()
|
||||
|
||||
bulb_style = "color: #888; font-size: 10px;"
|
||||
|
||||
self.bulb_zoom = StatusBulb("Zoom Lock")
|
||||
zoom_lbl = QLabel("Zoom")
|
||||
zoom_lbl.setStyleSheet(bulb_style)
|
||||
zoom_group = QHBoxLayout()
|
||||
zoom_group.setSpacing(4)
|
||||
zoom_group.addWidget(self.bulb_zoom)
|
||||
zoom_group.addWidget(zoom_lbl)
|
||||
btn_layout.addLayout(zoom_group)
|
||||
|
||||
self.bulb_wedge = StatusBulb("Wedge node")
|
||||
wedge_lbl = QLabel("Wedge")
|
||||
wedge_lbl.setStyleSheet(bulb_style)
|
||||
wedge_group = QHBoxLayout()
|
||||
wedge_group.setSpacing(4)
|
||||
wedge_group.addWidget(self.bulb_wedge)
|
||||
wedge_group.addWidget(wedge_lbl)
|
||||
btn_layout.addLayout(wedge_group)
|
||||
|
||||
self.bulb_uncomfy = StatusBulb("UnComfy")
|
||||
uncomfy_lbl = QLabel("UnComfy")
|
||||
uncomfy_lbl.setStyleSheet(bulb_style)
|
||||
uncomfy_group = QHBoxLayout()
|
||||
uncomfy_group.setSpacing(4)
|
||||
uncomfy_group.addWidget(self.bulb_uncomfy)
|
||||
uncomfy_group.addWidget(uncomfy_lbl)
|
||||
btn_layout.addLayout(uncomfy_group)
|
||||
|
||||
center_layout.addWidget(self.button_frame, 2, 0)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# --- Right Metadata Panel ---
|
||||
self.right_metadata = QGroupBox("Metadata")
|
||||
self.right_metadata.setMinimumWidth(300)
|
||||
@@ -146,7 +185,6 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
lbl.setStyleSheet(label_style)
|
||||
return lbl
|
||||
|
||||
# --- File Info (compact grid) ---
|
||||
self.file_name_label = style_label(QLabel("File:"))
|
||||
self.file_dim_label = style_label(QLabel("Dimensions:"))
|
||||
self.file_size_label = style_label(QLabel("Size:"))
|
||||
@@ -181,7 +219,6 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
sep.setStyleSheet("color: #333;")
|
||||
metadata_vbox.addWidget(sep)
|
||||
|
||||
# --- Primary node display (tier 1 + tier 2) ---
|
||||
mono_style = "font-family: 'Courier New', 'Consolas', monospace; font-size: 9pt;"
|
||||
self.primary_display = QTextEdit()
|
||||
self.primary_display.setReadOnly(True)
|
||||
@@ -189,7 +226,6 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
self.primary_display.setStyleSheet(mono_style)
|
||||
metadata_vbox.addWidget(self.primary_display, stretch=1)
|
||||
|
||||
# --- Tier 3 toggle + display ---
|
||||
self.tier3_btn = QPushButton("▶ plumbing")
|
||||
self.tier3_btn.setStyleSheet("text-align: left; padding: 2px 6px; font-size: 10px;")
|
||||
self.tier3_btn.setVisible(False)
|
||||
@@ -207,52 +243,95 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
self.primary_display.installEventFilter(self)
|
||||
self.tier3_display.installEventFilter(self)
|
||||
|
||||
# --- Copy button ---
|
||||
copy_btn = QPushButton("Copy Metadata")
|
||||
metadata_vbox.addWidget(copy_btn)
|
||||
|
||||
# Add panel to main layout
|
||||
self.main_layout.addWidget(self.right_metadata, 0, 2)
|
||||
|
||||
# ---- Menu Bar ----
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# File
|
||||
file_menu = menu_bar.addMenu("File")
|
||||
file_menu.addAction("Open Image", self.open_image_file)
|
||||
file_menu.addAction("Open Path", self.open_folder)
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction("Exit", QApplication.instance().quit)
|
||||
|
||||
# ---- View Menu ----
|
||||
view_menu = self.menuBar().addMenu("View")
|
||||
# Edit
|
||||
edit_menu = menu_bar.addMenu("Edit")
|
||||
edit_menu.addAction("Copy Image", self._copy_image)
|
||||
edit_menu.addAction("Copy Workflow", self._copy_workflow)
|
||||
|
||||
# View
|
||||
view_menu = menu_bar.addMenu("View")
|
||||
self._lock_zoom_action = QAction("Lock Zoom", self, checkable=True, shortcut="Z")
|
||||
self._lock_zoom_action.toggled.connect(lambda checked: setattr(self.image_view, "zoom_locked", checked))
|
||||
self._lock_zoom_action.toggled.connect(self._on_zoom_lock_toggled)
|
||||
view_menu.addAction(self._lock_zoom_action)
|
||||
view_menu.addAction("Reset Zoom", self.image_view.reset_zoom)
|
||||
view_menu.addSeparator()
|
||||
self._freeze_scroll_action = QAction("Freeze Scroll", self, checkable=True, shortcut="S")
|
||||
view_menu.addAction(self._freeze_scroll_action)
|
||||
|
||||
# About
|
||||
about_menu = menu_bar.addMenu("About")
|
||||
about_menu.addAction("About MBQ…", self._show_about)
|
||||
about_menu.addAction("Help", self._open_help)
|
||||
|
||||
# ---- Connect Signals ----
|
||||
open_btn.clicked.connect(self.open_image_file)
|
||||
next_btn.clicked.connect(self.show_next_image)
|
||||
prev_btn.clicked.connect(self.show_prev_image)
|
||||
copy_btn.clicked.connect(self._copy_workflow)
|
||||
|
||||
# Initialize after UI is built
|
||||
QTimer.singleShot(100, self.initialize_scale)
|
||||
|
||||
|
||||
# Install event filter on THIS window only
|
||||
self.installEventFilter(self)
|
||||
|
||||
def copy_metadata_to_clipboard():
|
||||
lines = [
|
||||
f"File: {self.file_name_value.text()}",
|
||||
f"Dimensions: {self.file_dim_value.text()}",
|
||||
f"Size: {self.file_size_value.text()}",
|
||||
f"Modified: {self.file_mod_value.text()}",
|
||||
"",
|
||||
self.primary_display.toPlainText(),
|
||||
]
|
||||
t3 = self.tier3_display.toPlainText()
|
||||
if t3:
|
||||
lines += ["", "--- plumbing ---", t3]
|
||||
QApplication.clipboard().setText("\n".join(lines))
|
||||
# ---- Menu handlers ----
|
||||
|
||||
def open_folder(self):
|
||||
folder = QFileDialog.getExistingDirectory(self, "Open Folder")
|
||||
if folder:
|
||||
self.load_folder_from_path(folder)
|
||||
|
||||
copy_btn.clicked.connect(copy_metadata_to_clipboard)
|
||||
def _on_zoom_lock_toggled(self, checked: bool):
|
||||
self.image_view.zoom_locked = checked
|
||||
self.bulb_zoom.set_state("on" if checked else "off")
|
||||
|
||||
def _copy_image(self):
|
||||
if self.image_view.image_item:
|
||||
QApplication.clipboard().setPixmap(self.image_view.image_item.pixmap())
|
||||
|
||||
def _copy_workflow(self):
|
||||
lines = [
|
||||
f"File: {self.file_name_value.text()}",
|
||||
f"Dimensions: {self.file_dim_value.text()}",
|
||||
f"Size: {self.file_size_value.text()}",
|
||||
f"Modified: {self.file_mod_value.text()}",
|
||||
"",
|
||||
self.primary_display.toPlainText(),
|
||||
]
|
||||
t3 = self.tier3_display.toPlainText()
|
||||
if t3:
|
||||
lines += ["", "--- plumbing ---", t3]
|
||||
QApplication.clipboard().setText("\n".join(lines))
|
||||
|
||||
def _show_about(self):
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About MBQ",
|
||||
"MBQ — Meta Browser Qt\n"
|
||||
"PySide6 image viewer for ComfyUI PNGs.\n\n"
|
||||
"Reads ComfyUI prompt metadata from PNG text chunks\n"
|
||||
"and displays generation parameters in a side panel.",
|
||||
)
|
||||
|
||||
def _open_help(self):
|
||||
# TODO: replace with actual repo URL
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/YOUR_REPO"))
|
||||
|
||||
# ---- Event filter ----
|
||||
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.Wheel:
|
||||
if obj in (self.primary_display, self.tier3_display):
|
||||
@@ -266,6 +345,8 @@ class MetaViewApp(MetaViewLogicMixin, QMainWindow):
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
# ---- Cache helpers ----
|
||||
|
||||
def get_workflow_data(self, file_path):
|
||||
return self.workflow_cache.get(file_path, get_png_metadata)
|
||||
|
||||
@@ -281,5 +362,6 @@ if __name__ == "__main__":
|
||||
import os
|
||||
if os.path.isfile(sys.argv[1]):
|
||||
QTimer.singleShot(0, lambda: window.load_image_from_path(sys.argv[1]))
|
||||
elif os.path.isdir(sys.argv[1]):
|
||||
QTimer.singleShot(0, lambda: window.load_folder_from_path(sys.argv[1]))
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -244,6 +244,10 @@ class ImageCanvas(QGraphicsView):
|
||||
def load_image_from_pixmap(self, pixmap: QPixmap):
|
||||
self._show_pixmap(pixmap)
|
||||
|
||||
def reset_zoom(self):
|
||||
if self.image_item:
|
||||
self.fitInView(self.image_item.boundingRect(), Qt.KeepAspectRatio)
|
||||
|
||||
|
||||
|
||||
# formerly workflow_cache.py
|
||||
|
||||
+32
-2
@@ -2,7 +2,7 @@ import html as _html
|
||||
import os
|
||||
import re
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QPixmap, QPainter
|
||||
from PySide6.QtWidgets import QFileDialog, QLabel
|
||||
from mbq_functions import ImageFolder
|
||||
@@ -108,11 +108,16 @@ class MetaViewLogicMixin:
|
||||
md = self.get_workflow_data(file_info["path"])
|
||||
|
||||
prefixes = md.get("saveimage_prefixes", []) if md else []
|
||||
if md and prefixes and _is_preview_save(file_info["name"], prefixes):
|
||||
is_preview = bool(md and prefixes and _is_preview_save(file_info["name"], prefixes))
|
||||
if is_preview:
|
||||
self.file_name_value.setStyleSheet("color: #c8823a;") # amber — possible preview save
|
||||
else:
|
||||
self.file_name_value.setStyleSheet("")
|
||||
|
||||
# Save scroll position if freeze is active
|
||||
freeze = getattr(self._freeze_scroll_action, 'isChecked', lambda: False)()
|
||||
saved_scroll = self.primary_display.verticalScrollBar().value() if freeze else None
|
||||
|
||||
# Always reset tier 3 to collapsed on image change
|
||||
self.tier3_display.setVisible(False)
|
||||
|
||||
@@ -121,18 +126,32 @@ class MetaViewLogicMixin:
|
||||
primary_html = _build_display_text(t1 + t2)
|
||||
if primary_html:
|
||||
self.primary_display.setHtml(primary_html)
|
||||
if saved_scroll is not None:
|
||||
QTimer.singleShot(0, lambda v=saved_scroll: self.primary_display.verticalScrollBar().setValue(v))
|
||||
else:
|
||||
self.primary_display.setPlainText("(no ComfyUI params in this image)")
|
||||
self.tier3_display.setHtml(_build_display_text(t3))
|
||||
self._tier3_count = len(t3)
|
||||
self.tier3_btn.setText(f"▶ plumbing ({self._tier3_count} nodes)")
|
||||
self.tier3_btn.setVisible(self._tier3_count > 0)
|
||||
|
||||
# Bulbs
|
||||
self.bulb_uncomfy.set_state("warn" if is_preview else "off")
|
||||
has_wedge = any(
|
||||
"wedge" in n["class_type"].lower() or "wedge" in n["title"].lower()
|
||||
for n in t1 + t2 + t3
|
||||
)
|
||||
self.bulb_wedge.set_state("on" if has_wedge else "off")
|
||||
else:
|
||||
self.primary_display.setPlainText("(no ComfyUI metadata)")
|
||||
self.tier3_display.setPlainText("")
|
||||
self._tier3_count = 0
|
||||
self.tier3_btn.setVisible(False)
|
||||
|
||||
# Bulbs
|
||||
self.bulb_uncomfy.set_state("warn")
|
||||
self.bulb_wedge.set_state("off")
|
||||
|
||||
def toggle_tier3(self):
|
||||
visible = not self.tier3_display.isVisible()
|
||||
self.tier3_display.setVisible(visible)
|
||||
@@ -148,6 +167,17 @@ class MetaViewLogicMixin:
|
||||
self.populate_filmstrip()
|
||||
self.preload_adjacent_images()
|
||||
|
||||
def load_folder_from_path(self, folder_path):
|
||||
self.folder_model = ImageFolder(folder_path)
|
||||
self.image_cache.clear()
|
||||
self.thumb_cache.clear()
|
||||
current = self.folder_model.current()
|
||||
if current:
|
||||
self.display_image(current["path"])
|
||||
self.update_metadata(current)
|
||||
self.populate_filmstrip()
|
||||
self.preload_adjacent_images()
|
||||
|
||||
def load_image_from_path(self, file_path):
|
||||
folder = os.path.dirname(file_path)
|
||||
self.folder_model = ImageFolder(folder, start_file=file_path)
|
||||
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
# node_unify.py
|
||||
# Unified prompt/workflow merge with correct Comfy alignment rules.
|
||||
|
||||
import unicodedata
|
||||
|
||||
|
||||
def normalize_value(v):
|
||||
if v is None or v == "None":
|
||||
return None
|
||||
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
|
||||
if isinstance(v, (int, float)):
|
||||
return float(v)
|
||||
|
||||
if isinstance(v, list):
|
||||
return [normalize_value(x) for x in v]
|
||||
|
||||
if isinstance(v, dict):
|
||||
return {k: normalize_value(val) for k, val in v.items()}
|
||||
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
s = s.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
try:
|
||||
return float(s)
|
||||
except:
|
||||
return s
|
||||
|
||||
s = str(v).strip()
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
s = s.replace("\r\n", "\n").replace("\r", "\n")
|
||||
return s
|
||||
|
||||
|
||||
def values_equivalent(a, b):
|
||||
return normalize_value(a) == normalize_value(b)
|
||||
|
||||
|
||||
def unify_prompt_and_workflow(prompt_json, workflow_json):
|
||||
prompt_nodes = {}
|
||||
if prompt_json:
|
||||
for k, v in prompt_json.items():
|
||||
if isinstance(v, dict):
|
||||
try:
|
||||
prompt_nodes[int(k)] = v
|
||||
except:
|
||||
pass
|
||||
|
||||
workflow_nodes = {}
|
||||
if workflow_json and "nodes" in workflow_json:
|
||||
for n in workflow_json["nodes"]:
|
||||
nid = n.get("id")
|
||||
if isinstance(nid, int):
|
||||
workflow_nodes[nid] = n
|
||||
|
||||
all_ids = set(prompt_nodes.keys()) | set(workflow_nodes.keys())
|
||||
unified = {}
|
||||
|
||||
for nid in sorted(all_ids):
|
||||
pnode = prompt_nodes.get(nid)
|
||||
wnode = workflow_nodes.get(nid)
|
||||
|
||||
if pnode and "class_type" in pnode:
|
||||
ntype = pnode["class_type"]
|
||||
elif pnode and "type" in pnode:
|
||||
ntype = pnode["type"]
|
||||
elif wnode and "type" in wnode:
|
||||
ntype = wnode["type"]
|
||||
else:
|
||||
ntype = "Unknown"
|
||||
|
||||
params = {}
|
||||
|
||||
# PROMPT LITERAL INPUTS (skip links)
|
||||
prompt_literal_values = []
|
||||
prompt_literal_keys = []
|
||||
|
||||
if pnode and "inputs" in pnode:
|
||||
for key, val in pnode["inputs"].items():
|
||||
# Link input → skip (not a literal)
|
||||
if isinstance(val, list) and len(val) == 2 and isinstance(val[0], str):
|
||||
continue
|
||||
|
||||
params[key] = val
|
||||
prompt_literal_keys.append(key)
|
||||
prompt_literal_values.append(val)
|
||||
|
||||
# WORKFLOW WIDGETS
|
||||
w_values = wnode.get("widgets_values", []) if wnode else []
|
||||
|
||||
# --- Smart synchronization between prompt literal inputs and workflow widget values ---
|
||||
skip_indices = set()
|
||||
i = 0 # index for prompt literal values
|
||||
j = 0 # index for workflow values
|
||||
|
||||
while i < len(prompt_literal_values) and j < len(w_values):
|
||||
|
||||
if values_equivalent(prompt_literal_values[i], w_values[j]):
|
||||
# They match → skip this workflow param (no param#)
|
||||
skip_indices.add(j)
|
||||
i += 1
|
||||
j += 1
|
||||
else:
|
||||
# They do NOT match → workflow has an extra value here
|
||||
# Mark this as param#, but DO NOT advance prompt index
|
||||
j += 1
|
||||
|
||||
# After loop, all remaining workflow values are extras → include as paramN
|
||||
for idx, val in enumerate(w_values):
|
||||
if idx not in skip_indices:
|
||||
params[f"param{idx}"] = val
|
||||
|
||||
|
||||
unified[nid] = {
|
||||
"id": nid,
|
||||
"type": ntype,
|
||||
"params": params
|
||||
}
|
||||
|
||||
return unified
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user