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:
Beak
2026-05-25 21:45:20 -07:00
parent 502dc26284
commit 1a94bf93df
9 changed files with 1715 additions and 216 deletions
+15 -1
View File
@@ -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)"
]
}
}
+13 -13
View File
@@ -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
View File
@@ -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())
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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