Skip to content

Commit 4d5f7c3

Browse files
committed
added preview history
1 parent fe44187 commit 4d5f7c3

File tree

3 files changed

+232
-2
lines changed

3 files changed

+232
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
__pycache__
2+
history_folder/

__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from . import raffle
2+
from . import preview_history # Import the renamed module
23
from .raffle import Raffle
4+
from .preview_history import PreviewHistory # Import the renamed class
35

46
NODE_CLASS_MAPPINGS = {
5-
"Raffle": Raffle
7+
"Raffle": Raffle,
8+
"PreviewHistory": PreviewHistory # Add the renamed mapping
69
}
710
NODE_DISPLAY_NAME_MAPPINGS = {
8-
"Raffle": "Raffle"
11+
"Raffle": "Raffle",
12+
"PreviewHistory": "Preview History (Raffle)" # Add the renamed display name
913
}
1014

1115
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']

preview_history.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import folder_paths
2+
import os
3+
import torch
4+
import numpy as np
5+
from PIL import Image, ImageDraw, ImageFont
6+
import server # Required for preview generation
7+
import threading
8+
import shutil # For potential file operations
9+
from datetime import datetime # For timestamps
10+
11+
# --- Constants ---
12+
# Determine the base path of the custom node
13+
EXTENSION_PATH = os.path.normpath(os.path.dirname(__file__))
14+
DEFAULT_HISTORY_FOLDER = os.path.join(EXTENSION_PATH, "history_folder")
15+
16+
# Tensor to PIL
17+
def tensor2pil(image):
18+
if image.dim() > 3:
19+
image = image[0]
20+
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
21+
22+
# Placeholder generation function (creates a base image, will be resized later if needed by UI)
23+
def create_placeholder(size=(128, 128), text="?"):
24+
"""Creates a simple placeholder PIL image."""
25+
img = Image.new('RGB', size, color = (40, 40, 40))
26+
d = ImageDraw.Draw(img)
27+
font = ImageFont.load_default() # Keep it simple for placeholders
28+
try:
29+
# Basic centering for default font
30+
tw, th = d.textsize(text, font=font) if hasattr(d, 'textsize') else (10, 10)
31+
d.text(((size[0]-tw)/2, (size[1]-th)/2), text, font=font, fill=(180, 180, 180))
32+
except Exception as e:
33+
print(f"[PreviewHistory] Error drawing placeholder text: {e}")
34+
d.text((10, 10), text, fill=(180, 180, 180)) # Fallback position
35+
return img
36+
37+
class PreviewHistory:
38+
# Lock for file system operations in the target directory
39+
_dir_lock = threading.Lock()
40+
41+
def __init__(self):
42+
self.output_dir = folder_paths.get_temp_directory() # For UI previews
43+
self.type = "temp"
44+
45+
@classmethod
46+
def INPUT_TYPES(s):
47+
return {
48+
"required": {
49+
"image": ("IMAGE",),
50+
"history_size": ("INT", {
51+
"default": 9,
52+
"min": 1,
53+
"max": 1000000,
54+
"step": 1,
55+
"tooltip": "How many images to keep in the history folder."
56+
}),
57+
},
58+
}
59+
60+
RETURN_TYPES = ()
61+
FUNCTION = "execute"
62+
OUTPUT_NODE = True
63+
CATEGORY = "Raffle/Previews"
64+
65+
# --- Main Execution ---
66+
67+
def execute(self, image, history_size):
68+
69+
# Use the default history folder path directly
70+
history_folder = DEFAULT_HISTORY_FOLDER
71+
72+
# Ensure target directory exists
73+
if not os.path.exists(history_folder): # Use the constant directly
74+
try:
75+
os.makedirs(history_folder, exist_ok=True) # Use the constant directly
76+
print(f"[PreviewHistory] Created history directory: {history_folder}") # Use the constant directly
77+
except OSError as e:
78+
print(f"[PreviewHistory] Error creating directory {history_folder}: {e}. Cannot proceed.") # Use the constant directly
79+
return {"ui": {"images": []}}
80+
81+
# --- Update History Files (if new image provided) ---
82+
if image is not None and image.nelement() > 0:
83+
new_pil_image = tensor2pil(image)
84+
85+
with PreviewHistory._dir_lock:
86+
try:
87+
# Save the new image using numerical month timestamp
88+
# Format: history_DD-MM-YYYY_HH-MM-SS.png
89+
now = datetime.now()
90+
# Format parts: %d=DD, %m=MM(numeric), %Y=YYYY, %H=HH(24h), %M=MM, %S=SS
91+
timestamp_final = now.strftime("%d-%m-%Y_%H-%M-%S") # Use %m for month number
92+
93+
new_filename = f"history_{timestamp_final}.png"
94+
new_path = os.path.join(history_folder, new_filename) # Use the constant indirectly via the variable
95+
# print(f"[PreviewHistory] Saving new file: {new_path}") # Debug
96+
new_pil_image.save(new_path, "PNG", compress_level=1)
97+
98+
# Cleanup: Remove oldest files beyond history_size
99+
# 1. Get all .png files with modification times
100+
all_files = []
101+
for filename in os.listdir(history_folder): # Use the constant indirectly
102+
if filename.lower().endswith(".png"):
103+
full_path = os.path.join(history_folder, filename) # Use the constant indirectly
104+
try:
105+
if os.path.isfile(full_path): # Ensure it's a file
106+
mod_time = os.path.getmtime(full_path)
107+
all_files.append((mod_time, full_path))
108+
except OSError:
109+
print(f"[PreviewHistory] Warning: Could not access file {full_path} during cleanup scan.")
110+
continue # Skip file if cannot get mod_time
111+
112+
# 2. Sort by modification time, newest first
113+
all_files.sort(key=lambda x: x[0], reverse=True)
114+
115+
# 3. Remove files exceeding the history size
116+
if len(all_files) > history_size:
117+
files_to_remove = all_files[history_size:] # Get the oldest ones
118+
# print(f"[PreviewHistory] Found {len(all_files)} files, keeping {history_size}, removing {len(files_to_remove)}.") # Debug
119+
for mod_time, path_to_remove in files_to_remove:
120+
try:
121+
# print(f"[PreviewHistory] Removing old file: {path_to_remove}") # Debug
122+
os.remove(path_to_remove)
123+
except OSError as e:
124+
print(f"[PreviewHistory] Error removing old file {path_to_remove}: {e}")
125+
126+
except Exception as e:
127+
print(f"[PreviewHistory] Error updating history files in '{history_folder}': {e}") # Use the constant indirectly
128+
# Attempt to continue to preview whatever state we're in
129+
130+
# --- Load Images for Preview ---
131+
preview_images_pil = []
132+
sorted_history_files = []
133+
with PreviewHistory._dir_lock: # Use lock for consistency during listing
134+
try:
135+
# Get all .png files with modification times again for loading
136+
temp_files = []
137+
for filename in os.listdir(history_folder): # Use the constant indirectly
138+
if filename.lower().endswith(".png"):
139+
full_path = os.path.join(history_folder, filename) # Use the constant indirectly
140+
try:
141+
if os.path.isfile(full_path):
142+
mod_time = os.path.getmtime(full_path)
143+
temp_files.append((mod_time, full_path))
144+
except OSError:
145+
# Error getting mod time during load, might be transient, skip file
146+
print(f"[PreviewHistory] Warning: Could not access file {full_path} during load scan.")
147+
continue
148+
# Sort by modification time, newest first
149+
temp_files.sort(key=lambda x: x[0], reverse=True)
150+
# Keep only the paths, up to history_size
151+
sorted_history_files = [f[1] for f in temp_files[:history_size]]
152+
except Exception as e:
153+
print(f"[PreviewHistory] Error listing files in {history_folder} for preview: {e}") # Use the constant indirectly
154+
# Fall through with empty list
155+
156+
# Determine a consistent size for placeholders if needed
157+
placeholder_size = (128, 128) # Default
158+
if sorted_history_files: # Check if we found any files
159+
try:
160+
# Try loading the first actual image (newest one)
161+
first_image_path = sorted_history_files[0]
162+
temp_img = Image.open(first_image_path)
163+
placeholder_size = temp_img.size
164+
temp_img.close()
165+
except Exception:
166+
pass # Ignore if it fails, keep default
167+
168+
# Load images from the sorted list
169+
for i, fp in enumerate(sorted_history_files): # Iterate through the paths we collected
170+
try:
171+
img = Image.open(fp).convert('RGB')
172+
preview_images_pil.append(img)
173+
except Exception as e:
174+
print(f"[PreviewHistory] Error loading history image '{fp}' for preview: {e}")
175+
# Keep error placeholder for load failures, use index 'i' for label
176+
preview_images_pil.append(create_placeholder(placeholder_size, f"Err {i:02d}"))
177+
178+
179+
# --- Generate Preview Data for the UI ---
180+
previews = []
181+
preview_server = server.PromptServer.instance # Get server instance
182+
183+
for i, pil_img in enumerate(preview_images_pil):
184+
if pil_img is None: continue # Should not happen with placeholder logic
185+
186+
try:
187+
# Use numpy array for saving temporary preview file
188+
img_array = np.array(pil_img).astype(np.uint8)
189+
# Define a unique prefix for each temp preview file in the batch
190+
# Simplify prefix - just use the index 'i' from the preview loop
191+
filename_prefix = f"PreviewHistory_Item_{i:02d}_"
192+
193+
# Get path for temporary preview file
194+
# Note: using dimensions from pil_img directly
195+
full_output_folder, fname, count, subfolder, _ = folder_paths.get_save_image_path(
196+
filename_prefix, self.output_dir, pil_img.width, pil_img.height
197+
)
198+
file = f"{fname}_{count:05}_.png" # Temp preview is always png
199+
200+
# Save the image (from history dir or placeholder) to the temporary location for UI preview
201+
pil_img.save(os.path.join(full_output_folder, file), quality=95) # Good quality for preview
202+
203+
# Append preview info for this image
204+
previews.append({
205+
"filename": file,
206+
"subfolder": subfolder,
207+
"type": self.type
208+
})
209+
210+
except Exception as e:
211+
print(f"[PreviewHistory] Error generating UI preview for image {i}: {e}")
212+
# Optionally skip this preview or add an error indicator? For now, just skip.
213+
214+
215+
# Return the list of preview data dictionaries
216+
return {"ui": {"images": previews}}
217+
218+
219+
NODE_CLASS_MAPPINGS = {
220+
"PreviewHistory": PreviewHistory
221+
}
222+
223+
NODE_DISPLAY_NAME_MAPPINGS = {
224+
"PreviewHistory": "Preview History (Raffle)"
225+
}

0 commit comments

Comments
 (0)