Clean: WatchPoint only

This commit is contained in:
Alex Bautista
2026-01-24 18:35:07 -06:00
parent 9c38331440
commit 7e98359e1e
12 changed files with 574 additions and 371 deletions
+31 -7
View File
@@ -1,12 +1,36 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
*.py[cod]
*$py.class
*.so
.Python
# Virtual environments
venv/
env/
ENV/
# IDEs
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
debug_logs/
# Temporal
*.tmp
*.temp
# Settings personales (opcional)
watchpoint_debug_config.json
# State files (no subir el JSON con datos)
data/list_state.json
beta/
# IDE
.vscode/
.idea/
+185 -8
View File
@@ -1,5 +1,5 @@
# 👁️ ComfyUI Watch Point
![alt text](media/preview.png)
**Dual preview system for ComfyUI workflows**
Watch Point provides two simultaneous preview methods:
@@ -16,20 +16,40 @@ Watch Point provides two simultaneous preview methods:
- 📋 Copy to clipboard
- ⚙️ Customizable settings
- 📡 **Signal Scout Panel**: Display text from your workflow in a side panel.
- 🔒 **Thread Safety**: Windows minimize instead of closing to prevent ComfyUI crashes
### Floating Preview (JavaScript)
- 📌 Uses ComfyUI's native preview system
- 🎨 Works with existing preview extensions
- 🔄 Auto-updates during workflow execution
- 💾 Configurable via browser localStorage (no file editing)
- ⌨️ Customizable keyboard shortcuts
- 🎯 Snap-to-grid positioning
- 👻 Adjustable opacity levels
## Thread Safety & Window Protection
WatchPoint includes built-in protection against threading issues that can crash ComfyUI:
### 🛡️ Window Minimization Protection
- **Close Button Behavior**: Clicking the Windows close button minimizes the window instead of closing it
- **Thread Safety**: Prevents `Tcl_AsyncDelete` errors and threading conflicts
- **ComfyUI Stability**: Ensures ComfyUI continues running even if windows are "minimized"
- **Window Recovery**: Minimized windows can be restored from the taskbar
### 🔧 Technical Details
- Uses thread-safe Tkinter operations
- Implements proper cleanup procedures
- Prevents memory leaks and threading deadlocks
- Global window management for consistent behavior
## Installation
1. Clone into your ComfyUI custom_nodes directory:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/yourusername/ComfyUI-WatchPoint.git
git clone https://github.com/gmorks/ComfyUI-WatchPoint.git
```
2. Install dependencies (if needed):
```bash
pip install pillow screeninfo
@@ -96,6 +116,12 @@ A utility node to restore minimized WatchPoint windows.
- **1**: Zoom 1:1 (100% actual size)
- **ESC**: Close window
### Window Close Behavior
**Important**: The Windows close button (❌) minimizes the window instead of closing it. This prevents threading errors and keeps ComfyUI stable. To "close" the window:
- Click the close button → Window minimizes to taskbar
- Use **ESC** key to properly close the window
- Right-click taskbar icon → "Close window" for forced closure
### Toolbar Buttons
- **↻ Reset**: Reset zoom and pan
- **⊕ Zoom In**: Increase zoom level
@@ -151,9 +177,10 @@ except ImportError as e:
### Disabling Utility Nodes
Comment out or remove the import block in `__init__.py` to disable utility nodes.
## Configuration File
## Configuration Files
Edit `watchpoint_settings.json` to customize defaults:
### Monitor Preview Settings
Edit `watchpoint_settings.json` to customize defaults for the external Tkinter window:
```json
{
@@ -168,13 +195,149 @@ Edit `watchpoint_settings.json` to customize defaults:
}
```
### Settings Explained
#### Settings Explained
- `window_x`, `window_y`: Window position (null = auto)
- `window_size_mode`: "fixed", "Half Vertical", "Half Horizontal", or "Quarter"
- `show_toolbar`: Show toolbar on startup
- `save_format`: Default save format ("png", "jpeg", "webp")
- `jpeg_quality`: JPEG compression quality (10-100)
### Floating Preview Configuration
The floating preview window is configured via browser localStorage. No file editing required!
#### Quick Start
**View Current Configuration**
Open browser console (F12) and run:
```javascript
WatchPointFloating.getConfig()
```
**Change Configuration**
```javascript
WatchPointFloating.setConfig({
window: {
defaultWidth: 500,
defaultHeight: 500,
defaultPosition: "top-right",
defaultOpacity: 80
},
shortcuts: {
toggle: "Ctrl+Alt+KeyP" // Change toggle shortcut
}
})
```
**Reset to Defaults**
```javascript
WatchPointFloating.resetConfig()
```
**Show Help**
```javascript
WatchPointFloating.showHelp()
```
**Note**: Reload the page after changing config to apply changes.
#### Available Configuration Options
##### Window Settings
```javascript
{
window: {
defaultWidth: 320, // Width in pixels
defaultHeight: 320, // Height in pixels
defaultPosition: "middle-right", // Initial position
defaultOpacity: 100 // 0-100 (percentage)
}
}
```
**Available Positions**:
- `"top-left"`, `"top-center"`, `"top-right"`
- `"middle-left"`, `"middle-center"`, `"middle-right"`
- `"bottom-left"`, `"bottom-center"`, `"bottom-right"`
##### Opacity Settings
```javascript
{
opacity: {
levels: [20, 40, 60, 80, 100], // Available opacity levels (%)
showIndicator: true, // Show opacity % when changing
indicatorDuration: 1000 // How long indicator shows (ms)
}
}
```
##### UI Settings
```javascript
{
ui: {
showHeader: true, // Show title bar
showTitle: true, // Show "👁️ Watch Point" title
showCloseButton: true, // Show X button
dragFromAnywhere: false // Drag from image area (only if header=false)
}
}
```
##### Keyboard Shortcuts
```javascript
{
shortcuts: {
// Main controls
toggle: "Ctrl+Alt+KeyW",
increaseOpacity: "Ctrl+Alt+Equal", // or Ctrl+Alt++
decreaseOpacity: "Ctrl+Alt+Minus", // or Ctrl+Alt+-
// Arrow navigation (relative)
snapUp: "Ctrl+Alt+ArrowUp",
snapDown: "Ctrl+Alt+ArrowDown",
snapLeft: "Ctrl+Alt+ArrowLeft",
snapRight: "Ctrl+Alt+ArrowRight",
// Numpad navigation (absolute positions)
snapToPosition7: "Ctrl+Alt+Numpad7", // Top-left
snapToPosition8: "Ctrl+Alt+Numpad8", // Top-center
snapToPosition9: "Ctrl+Alt+Numpad9", // Top-right
snapToPosition4: "Ctrl+Alt+Numpad4", // Middle-left
snapToPosition5: "Ctrl+Alt+Numpad5", // Middle-center
snapToPosition6: "Ctrl+Alt+Numpad6", // Middle-right
snapToPosition1: "Ctrl+Alt+Numpad1", // Bottom-left
snapToPosition2: "Ctrl+Alt+Numpad2", // Bottom-center
snapToPosition3: "Ctrl+Alt+Numpad3" // Bottom-right
}
}
```
#### Default Keyboard Shortcuts
| Action | Shortcut | Description |
|--------|----------|-------------|
| **Toggle window** | `Ctrl+Alt+W` | Show/hide floating preview |
| **Increase opacity** | `Ctrl+Alt++` | Make more opaque |
| **Decrease opacity** | `Ctrl+Alt+-` | Make more transparent |
| **Move up** | `Ctrl+Alt+↑` | Snap to position above |
| **Move down** | `Ctrl+Alt+↓` | Snap to position below |
| **Move left** | `Ctrl+Alt+←` | Snap to position left |
| **Move right** | `Ctrl+Alt+→` | Snap to position right |
| **Snap to corner** | `Ctrl+Alt+Numpad[1-9]` | Jump to specific position |
#### Storage Location
Configuration is stored in browser localStorage under the key:
```
watchpoint-floating-config
```
This means:
- ✅ Configuration persists between sessions
- ✅ Each browser/profile has independent settings
- ✅ No server-side storage needed
- ⚠️ Clearing browser data will reset to defaults
## Use Cases
### Dual Monitor Setup
@@ -232,8 +395,22 @@ pip install pywin32
## Credits
Created for ComfyUI by [Your Name]
Vibe Coded for ComfyUI by gmorks using AI.
## License
MIT License - Feel free to use and modify!
MIT License - Feel free to use and modify!
## 🚧 Development Status
**Note**: This extension is not yet compatible with ComfyUI Nodes 2.0. Support for the new node system is on our to-do list and will be implemented in a future update.
Stay tuned for updates!
## Recent Changes
### v1.0.0 - Initial Release
- Dual preview system (Monitor + Floating)
- Signal Scout text display
- Basic window controls and settings
- Multi-monitor support
-167
View File
@@ -1,167 +0,0 @@
# 🛡️ WatchPoint Conservation Guide
## 📋 Document Purpose
This document serves as a "contra PRD" - a conservation guide for maintaining WatchPoint functionality while allowing future modifications. It outlines what MUST be preserved, what can be modified, and critical implementation details.
## 🔒 CRITICAL: Must Preserve
### Core Stability Features
1. **Window Minimization Protection**
- `safe_close()` function in `watch_point.py`
- Prevents accidental window closure from crashing ComfyUI
- Uses `iconify()` as primary method, `withdraw()` as fallback
- Updates window state tracking (`minimized`, `running` flags)
2. **Thread Safety**
- All Tkinter operations must run on main thread
- Use `self.window_manager.master.after()` for thread-safe GUI updates
- Prevents `Tcl_AsyncDelete` crashes
3. **Debug Mode Persistence**
- `watchpoint_debug_config.json` file-based configuration
- `load_debug_config()` and `save_debug_config()` functions
- Debug state survives ComfyUI restarts
4. **Recursion Prevention**
- `_saving_dump` flag in `save_debug_dump()`
- Prevents infinite loops during debug dump saving
### File Structure
```
ComfyUI-WatchPoint/
├── watch_point.py # Core functionality (preserve stability features)
├── __init__.py # Optional utility imports (modify carefully)
├── watchpoint_debug_config.json # Debug persistence (auto-generated)
├── watchpoint_settings.json # User settings (auto-generated)
├── nodes/
│ ├── __init__.py # Node package init
│ ├── list_cycler.py # Template for utility nodes
│ └── watchpoint_utils.py # Optional utility nodes (safe to modify)
└── WATCHPOINT_CONSERVATION.md # This file
```
## 📝 Implementation Rules
### When Adding New Features
1. **Utility Nodes**: Add to `nodes/watchpoint_utils.py`, not `watch_point.py`
2. **Core Modifications**: Only modify `watch_point.py` for critical fixes
3. **Optional Imports**: Use try/except blocks in `__init__.py`
4. **State Management**: Use file-based persistence for critical state
### Window Management
```python
# Correct pattern for window operations
def safe_window_operation(self, display_idx, operation):
try:
self.window_manager.master.after(0, lambda: self._perform_operation(display_idx, operation))
except Exception as e:
wp_logger.warning(f"Window operation failed: {e}", "WindowManager")
```
### Debug Mode Handling
```python
# Always check debug state before operations
if self.debug_mode or load_debug_config().get("debug_enabled", False):
# Perform debug operations
pass
```
## 🔄 Utility Node Management
### Enabling/Disabling Utilities
Located in `__init__.py`:
```python
try:
from .nodes.watchpoint_utils import NODE_CLASS_MAPPINGS as UTILS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as UTILS_DISPLAY
NODE_CLASS_MAPPINGS.update(UTILS_CLASS)
NODE_DISPLAY_NAME_MAPPINGS.update(UTILS_DISPLAY)
print("WatchPoint Utils loaded: Debug Toggle and Restore Window available")
except ImportError as e:
print(f"WatchPoint Utils not available: {e}")
pass
```
### Safe Modifications
- ✅ Add new utility nodes to `watchpoint_utils.py`
- ✅ Modify utility node functionality
- ✅ Add new optional features
- ❌ Remove core stability features from `watch_point.py`
- ❌ Modify thread safety mechanisms
- ❌ Remove file-based state management
## 🚨 Common Pitfalls to Avoid
1. **Don't Create Direct Tkinter Windows**
```python
# ❌ WRONG - Causes thread issues
window = tk.Toplevel()
# ✅ CORRECT - Use existing window manager
self.window_manager.create_window(...)
```
2. **Don't Skip Error Handling**
```python
# ❌ WRONG - No error handling
root.protocol("WM_DELETE_WINDOW", root.iconify)
# ✅ CORRECT - With error handling
def safe_close():
try:
root.iconify()
except:
root.withdraw()
root.protocol("WM_DELETE_WINDOW", safe_close)
```
3. **Don't Forget State Persistence**
```python
# ❌ WRONG - State lost on restart
self.debug_mode = True
# ✅ CORRECT - Persistent state
save_debug_config({"debug_enabled": True})
```
## 🔧 Testing Checklist
Before committing changes:
- [ ] Test window minimization (click X button)
- [ ] Test debug mode persistence (restart ComfyUI)
- [ ] Test with utility nodes enabled/disabled
- [ ] Test multi-monitor setup
- [ ] Check for `Tcl_AsyncDelete` errors
- [ ] Verify no infinite recursion in debug dumps
## 📚 Key Functions Reference
### Window Management
- `safe_close()` - Window close protection
- `window_manager.create_window()` - Safe window creation
- `window_manager.restore_window()` - Window restoration
### Debug System
- `load_debug_config()` - Load persistent debug state
- `save_debug_config()` - Save debug state
- `save_debug_dump()` - Safe debug dump (with recursion protection)
### State Management
- File: `watchpoint_debug_config.json`
- Keys: `debug_enabled`, `debug_data`
- Auto-created on first use
## 🎯 Philosophy
**Conservative Approach**: This project prioritizes stability over features. Every new feature must:
1. Not compromise core stability
2. Be optional when possible
3. Include proper error handling
4. Maintain backward compatibility
**Modular Design**: Separate core functionality from utilities to allow safe experimentation without risking core stability.
---
*Last Updated: 2026-01-23*
*Version: 1.0*
*Purpose: Preserve WatchPoint stability while enabling safe evolution*
+2 -3
View File
@@ -1,9 +1,8 @@
from .watch_point import NODE_CLASS_MAPPINGS as WP_CLASS, NODE_DISPLAY_NAME_MAPPINGS as WP_DISPLAY, cleanup_all_watchpoints
from .nodes.list_cycler import NODE_CLASS_MAPPINGS as LC_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LC_DISPLAY
# Combinamos lo básico
NODE_CLASS_MAPPINGS = {**WP_CLASS, **LC_CLASS}
NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY, **LC_DISPLAY}
NODE_CLASS_MAPPINGS = {**WP_CLASS}
NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY}
# Importar nodos utils de forma opcional (pueden ser desactivados si no se necesitan)
try:
Binary file not shown.
Binary file not shown.
+199
View File
@@ -0,0 +1,199 @@
{
"id": "d744fb0e-f3c7-4705-9960-73b30103a194",
"revision": 0,
"last_node_id": 6,
"last_link_id": 7,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [
17300,
-9670
],
"size": [
222.83333587646484,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.10.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png",
"image"
]
},
{
"id": 4,
"type": "PrimitiveString",
"pos": [
17100,
-9270
],
"size": [
500,
58
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
5
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.10.0",
"Node name for S&R": "PrimitiveString"
},
"widgets_values": [
"\"opt_signal_text\" can be used to send any string, to the tkinter window"
]
},
{
"id": 5,
"type": "WatchPoint",
"pos": [
17600,
-9670
],
"size": [
210,
338
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
},
{
"name": "opt_signal_text",
"shape": 7,
"type": "STRING",
"link": 5
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
7
]
}
],
"properties": {
"aux_id": "gmorks/ComfyUI-WatchPoint",
"ver": "9c383314405e0767edb67ac208b87b4bac7a2448",
"Node name for S&R": "WatchPoint"
},
"widgets_values": [
true,
true,
"Monitor 0 (1920x1080)"
]
},
{
"id": 6,
"type": "PreviewImage",
"pos": [
17900,
-9670
],
"size": [
140,
246
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 7
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.10.0",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
4,
1,
0,
5,
0,
"IMAGE"
],
[
5,
4,
0,
5,
1,
"STRING"
],
[
7,
5,
0,
6,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.725,
"offset": [
-16829.321120689645,
9914.394612068972
]
},
"frontendVersion": "1.37.11",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
+106 -9
View File
@@ -1,6 +1,7 @@
// watchpoint_floating.js
// Watch Point Floating Preview Integration
// Only captures images from Watch Point nodes
// Configuration via localStorage
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
@@ -64,21 +65,58 @@ const DEFAULT_CONFIG = {
}
};
// Configuration loader
async function loadConfig() {
// Configuration loader with localStorage support
function loadConfig() {
try {
const response = await fetch('/extensions/ComfyUI-WatchPoint/watchpoint_config.json');
if (response.ok) {
const userConfig = await response.json();
console.log("✅ Watch Point Floating: Loaded user config");
return { ...DEFAULT_CONFIG, ...userConfig };
const saved = localStorage.getItem('watchpoint-floating-config');
if (saved) {
const userConfig = JSON.parse(saved);
console.log("✅ Watch Point Floating: Loaded config from localStorage");
// Deep merge user config with defaults
return deepMerge(DEFAULT_CONFIG, userConfig);
}
} catch (e) {
console.log("ℹ️ Watch Point Floating: No custom config found, using defaults");
console.warn("⚠️ Watch Point Floating: Error loading localStorage config:", e);
}
console.log("ℹ️ Watch Point Floating: Using default config");
return DEFAULT_CONFIG;
}
// Save configuration to localStorage
function saveConfig(config) {
try {
localStorage.setItem('watchpoint-floating-config', JSON.stringify(config));
console.log("✅ Watch Point Floating: Config saved to localStorage");
return true;
} catch (e) {
console.error("❌ Watch Point Floating: Could not save config:", e);
return false;
}
}
// Deep merge helper
function deepMerge(target, source) {
const output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target))
Object.assign(output, { [key]: source[key] });
else
output[key] = deepMerge(target[key], source[key]);
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
// Main extension
app.registerExtension({
name: "WatchPoint.FloatingPreview",
@@ -87,7 +125,7 @@ app.registerExtension({
console.log("👁️ Watch Point Floating: Initializing...");
// Load configuration
const config = await loadConfig();
const config = loadConfig();
// Create the floating preview window
const pip = new WatchPointFloatingWindow(config);
@@ -213,6 +251,65 @@ app.registerExtension({
console.log(" - Toggle:", config.shortcuts.toggle);
console.log(" - Opacity: Ctrl+Alt+[+/-]");
console.log(" - Position: Ctrl+Alt+[Arrows] or Ctrl+Alt+[Numpad1-9]");
// Expose global API for configuration
window.WatchPointFloating = {
getConfig: () => loadConfig(),
setConfig: (newConfig) => {
const merged = deepMerge(DEFAULT_CONFIG, newConfig);
if (saveConfig(merged)) {
console.log("✅ Configuration saved. Reload page to apply changes.");
return merged;
}
return null;
},
resetConfig: () => {
try {
localStorage.removeItem('watchpoint-floating-config');
console.log("✅ Watch Point Floating: Config reset to defaults. Reload page to apply.");
return true;
} catch (e) {
console.error("❌ Could not reset config:", e);
return false;
}
},
showHelp: () => {
console.log(`
🎨 Watch Point Floating Configuration Help
==========================================
View current config:
WatchPointFloating.getConfig()
Change settings:
WatchPointFloating.setConfig({
window: {
defaultWidth: 500,
defaultHeight: 500,
defaultPosition: "top-right",
defaultOpacity: 80
},
shortcuts: {
toggle: "Ctrl+Alt+KeyP"
}
})
Reset to defaults:
WatchPointFloating.resetConfig()
Available positions:
top-left, top-center, top-right
middle-left, middle-center, middle-right
bottom-left, bottom-center, bottom-right
Note: Reload page after changing config to apply changes.
`);
},
instance: pip
};
console.log("🔧 Watch Point Floating: Global API exposed as window.WatchPointFloating");
console.log(" Run WatchPointFloating.showHelp() for configuration help");
}
});
-36
View File
@@ -1,36 +0,0 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "WatchPoint.ListCyclerButtons",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "WPSmartListCycler") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
onNodeCreated?.apply(this, arguments);
// Crear el botón de Reset
const btn = this.addWidget("button", "RESET STATE", "reset", () => {
const node_id = this.id;
// Llamar a nuestra API personalizada en el servidor
api.fetchApi("/wp/list_cycler/reset", {
method: "POST",
body: JSON.stringify({ node_id }),
}).then(response => {
if (response.ok) {
if (DEBUG) {
//alert(`Nodo ${node_id}: Estado del JSON limpiado. En la próxima ejecución se regenerará la lista.`);
this.color = "#225522"; // Feedback visual momentáneo
setTimeout(() => { this.color = ""; }, 1000);
}
}
});
});
btn.serialize = false; // No necesitamos guardar el estado del botón
};
}
}
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

-115
View File
@@ -1,115 +0,0 @@
import json
import os
import random
from server import PromptServer
from aiohttp import web
DEBUG = True
class WPSmartListCycler:
def __init__(self):
self.data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "list_state.json")
os.makedirs(os.path.dirname(self.data_path), exist_ok=True)
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"list_string": ("STRING", {"default": "0,1,2,3,4,5"}),
"mode": (["increment-wrap", "random-shuffled", "fixed"],),
"fixed_index": ("INT", {"default": 0, "min": 0, "max": 1000}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("INT", "STRING")
RETURN_NAMES = ("index", "index_as_string")
FUNCTION = "get_next_value"
CATEGORY = "WatchPoint/Logic"
@classmethod
def IS_CHANGED(s, **kwargs):
"""
Forzamos a ComfyUI a que ignore la caché de este nodo devolviendo
un valor distinto en cada consulta.
"""
return random.random()
def load_state(self):
try:
if os.path.exists(self.data_path):
with open(self.data_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception: pass
return {}
def save_state(self, state):
try:
with open(self.data_path, 'w', encoding='utf-8') as f:
json.dump(state, f, indent=4)
except Exception as e:
if DEBUG:
print(f"WP_Error: Fallo al guardar JSON: {e}")
def get_next_value(self, list_string, mode, fixed_index, seed, unique_id):
# 1. Preparar items
items = [int(x.strip()) for x in list_string.split(",") if x.strip()] or [0]
state = self.load_state()
if unique_id not in state:
state[unique_id] = {"last_idx": -1, "shuffled_list": []}
node_state = state[unique_id]
out_value = items[0]
# 2. Lógica de selección
if mode == "fixed":
idx = fixed_index % len(items)
out_value = items[idx]
node_state["last_idx"] = idx
elif mode == "increment-wrap":
new_idx = (node_state.get("last_idx", -1) + 1) % len(items)
out_value = items[new_idx]
node_state["last_idx"] = new_idx
elif mode == "random-shuffled":
shuffled = node_state.get("shuffled_list", [])
# Regenerar bolsa si está vacía o el tamaño de la lista cambió
if DEBUG:
print(f"WP_Log: Nodo {unique_id} estado actual: {node_state}")
if not shuffled or max(shuffled, default=-1) >= len(items):
shuffled = list(range(len(items)))
random.shuffle(shuffled)
if DEBUG:
print(f"WP_Log: Nodo {unique_id} barajando nueva lista: {shuffled}" )
idx_to_use = shuffled.pop(0)
out_value = items[idx_to_use]
node_state["shuffled_list"] = shuffled
node_state["last_idx"] = idx_to_use
# 3. Guardado inmediato
state[unique_id] = node_state
self.save_state(state)
if DEBUG:
print(f"WP_Log: Nodo {unique_id} ejecutado. Resultado: {out_value}")
return (int(out_value), str(out_value))
# --- API ENDPOINT (Mismo de antes) ---
@PromptServer.instance.routes.post("/wp/list_cycler/reset")
async def reset_node_state(request):
json_data = await request.json()
node_id = str(json_data.get("node_id"))
temp_instance = WPSmartListCycler()
state = temp_instance.load_state()
if node_id in state:
del state[node_id]
temp_instance.save_state(state)
return web.json_response({"status": "success"})
return web.json_response({"status": "not_found"})
NODE_CLASS_MAPPINGS = {"WPSmartListCycler": WPSmartListCycler}
NODE_DISPLAY_NAME_MAPPINGS = {"WPSmartListCycler": "🔄 Smart List Cycler (WP)"}
+51 -26
View File
@@ -1,6 +1,6 @@
import os
import json
import sys # NUEVO: Importar sys para debug dumps
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
@@ -55,11 +55,11 @@ class WatchPointLogger:
self.log_level = "INFO" # DEBUG, INFO, WARNING, ERROR
self.logs = []
self.max_logs = 100
self.debug_mode = False # NUEVO: Modo debug
self.debug_log_dir = "debug_logs" # NUEVO: Directorio para logs de debug
self.debug_session_id = None # NUEVO: ID única para sesión de debug
self.debug_mode = False
self.debug_log_dir = "debug_logs"
self.debug_session_id = None
self.config_file = os.path.join(os.path.dirname(__file__), "watchpoint_debug_config.json")
self.load_debug_config() # Cargar configuración persistente
self.load_debug_config()
def log(self, level, message, component="WatchPoint"):
"""Log un mensaje con nivel y componente"""
@@ -124,13 +124,13 @@ class WatchPointLogger:
self.debug_mode = enabled
if enabled:
self.debug_session_id = time.strftime("%Y%m%d_%H%M%S")
# Crear directorio de debug si no existe
os.makedirs(self.debug_log_dir, exist_ok=True)
self.info(f"Modo debug activado - Sesión: {self.debug_session_id}", "Debug")
else:
self.info("Modo debug desactivado", "Debug")
# Guardar configuración persistente
self.save_debug_config()
def load_debug_config(self):
@@ -183,16 +183,16 @@ class WatchPointLogger:
}
}
# Agregar stats del WatchPoint si está disponible
if watch_point_instance and hasattr(watch_point_instance, 'get_health_stats'):
debug_data["health_stats"] = watch_point_instance.get_health_stats()
# Agregar info de threads SI ESTÁ DISPONIBLE Y NO ESTAMOS EN RECURSIÓN
if watch_point_instance and hasattr(watch_point_instance, 'debug_threads'):
# Llamar directamente a la función sin pasar por el logger para evitar recursión
debug_data["thread_info"] = watch_point_instance.debug_threads()
# Agregar logs recientes
debug_data["recent_logs"] = self.get_logs()[-50:] # Últimos 50 logs
# Guardar a archivo
@@ -207,7 +207,7 @@ class WatchPointLogger:
self.error(f"Error guardando debug dump: {e}", "Debug")
return None
finally:
# Limpiar bandera de recursión
# Clear recursion flag
self._saving_dump = False
# Crear logger global
@@ -283,7 +283,7 @@ class WindowManager:
os.path.join(os.path.dirname(__file__), "watchpoint_settings.json")
)
self.shutdown_event = threading.Event()
self._start_time = time.time() # Tiempo de inicio para estadísticas
self._start_time = time.time() # Start time for statistics
self.initialized = True
# Iniciar watchdog
@@ -293,23 +293,53 @@ class WindowManager:
wp_logger.info("WindowManager inicializado", "WindowManager")
def show_image(self, display_idx, pil_img, text=None):
"""Creates or updates a window to display an image and optional text."""
if display_idx in self.windows:
win_data = self.windows[display_idx]
"""Creates or updates THE SINGLE global window to display an image and optional text."""
# Check if any window exists, reuse it
if len(self.windows) > 0:
# Obtener el índice de la ventana existente (solo debería haber 1)
existing_idx = list(self.windows.keys())[0]
win_data = self.windows[existing_idx]
# Verificar que la ventana esté corriendo
if not win_data.get("running", False):
self._cleanup_window(display_idx)
# Window exists but is dead, clean up
wp_logger.warning(f"Ventana {existing_idx} existe pero no está running, limpiando", "ShowImage")
self._cleanup_window(existing_idx)
# Continuar a creación de nueva ventana (abajo)
else:
# Window is alive, REUSE
wp_logger.debug(f"Reutilizando ventana existente {existing_idx} para nueva imagen", "ShowImage")
# Actualizar imagen
with win_data["lock"]:
win_data["image"] = pil_img
# Actualizar texto si existe
if text and win_data.get("instance"):
win_data["instance"].update_signal_text(text)
# Update title if monitor changed
if existing_idx != display_idx:
wp_logger.info(f"Monitor cambiado de {existing_idx} a {display_idx} (solo actualiza título)", "ShowImage")
instance = win_data.get("instance")
if instance and hasattr(instance, 'root'):
try:
# Update simplified title
instance.root.title("Watch Point")
except Exception as e:
wp_logger.warning(f"No se pudo actualizar título: {e}", "ShowImage")
return
# If we reach here, NO window exists, create a new one
wp_logger.info(f"Creando nueva ventana global en Monitor {display_idx}", "ShowImage")
lock = Lock()
win_data = {
"image": pil_img, "lock": lock, "running": True,
"instance": None, "pending_text": text,
"minimized": False # NUEVO: Estado de minimización
"minimized": False
}
self.windows[display_idx] = win_data
@@ -324,7 +354,7 @@ class WindowManager:
self.windows[display_idx]["closing"] = True
self.windows[display_idx]["close_started"] = time.time()
# MEJORADO: Esperar hasta 3 segundos con timeout
try:
thread = self.windows[display_idx].get("thread")
if thread and thread.is_alive():
@@ -349,9 +379,9 @@ class WindowManager:
root = None
win_instance = None
try:
# === CÓDIGO EXISTENTE SE MANTIENE IGUAL ===
# Create main window
root = tk.Tk()
root.title(f"Watch Point - Monitor {display_idx}")
root.title("Watch Point")
self._apply_icon(root)
self._apply_geometry(root, display_idx)
@@ -685,11 +715,6 @@ class WatchPoint:
except (ValueError, IndexError):
display_idx = 0
# Close the previous window if the monitor has changed
if self.last_display_idx != -1 and self.last_display_idx != display_idx:
wp_logger.info(f"Cambiando de monitor {self.last_display_idx} a {display_idx}", "WatchPoint")
self.window_manager.hide_window(self.last_display_idx)
self.last_display_idx = display_idx
if monitor_preview: