Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60f30b068e | ||
|
|
1439270fe5 | ||
|
|
f8210d8f69 | ||
|
|
aceb34b9b0 | ||
|
|
e9ce1fd2cf | ||
|
|
9b888443ac | ||
|
|
f4743df3ef | ||
|
|
6a68983ef4 | ||
|
|
fb01fa24ae | ||
|
|
65f68f59a1 | ||
|
|
c3fab5581b |
@@ -38,6 +38,7 @@ I’m sharing them here with the community, and I hope you find them as useful a
|
||||
| [🔤 Embedding Autocomplete](#-embedding-autocomplete) | Smart autocomplete for embeddings, LoRAs, and tags | 🔧 Utils |
|
||||
| [🧹 Kiko Purge VRAM](#-kiko-purge-vram) | Intelligent VRAM management with detailed reporting | 🛠️ Utils |
|
||||
| [📂 Local Image Loader](#-local-image-loader) | Visual gallery browser for local media files | 💾 Images |
|
||||
| [🌐 Model Downloader](#-model-downloader) | Download models from CivitAI, HuggingFace, and custom URLs | 🛠️ Utils |
|
||||
|
||||
### 🧰 xyz-helpers Tools
|
||||
|
||||
@@ -378,6 +379,32 @@ Visual gallery browser for loading local images, videos, and audio files directl
|
||||
- Extract prompts and settings from previously generated images
|
||||
- Preview media files before loading into workflow
|
||||
|
||||
#### 🌐 Model Downloader
|
||||
Download models, LoRAs, and other assets directly from CivitAI, HuggingFace, and custom URLs within ComfyUI.
|
||||
|
||||
- **Multi-Platform Support**: CivitAI, HuggingFace, and direct download URLs
|
||||
- **Smart URL Detection**: Automatic detection of download source and file handling
|
||||
- **API Token Support**: Optional authentication for private/gated models
|
||||
- **Progress Reporting**: Real-time download progress with speed indicators
|
||||
- **Resume Support**: Skip existing files or force re-download
|
||||
- **Interrupt Handling**: Respects ComfyUI's "Cancel current run" button
|
||||
- **Automatic Cleanup**: Removes partial downloads on cancellation
|
||||
- **Custom Filenames**: Override auto-detected filenames when needed
|
||||
|
||||
**Platform Features:**
|
||||
- **CivitAI**: Model page URLs, version-specific downloads, API authentication
|
||||
- **HuggingFace**: Blob and resolve URLs, branch/revision support, gated model access
|
||||
- **Custom URLs**: Direct download links with bearer token authentication
|
||||
|
||||
**Use Cases:**
|
||||
- Download models without leaving ComfyUI
|
||||
- Automate asset acquisition in workflows
|
||||
- Access private or gated models with API tokens
|
||||
- Build reproducible workflows with automatic model fetching
|
||||
- Quickly test new models from the community
|
||||
|
||||

|
||||
|
||||
### 🔤 Embedding Autocomplete
|
||||
|
||||
**Intelligent autocomplete for embeddings, LoRAs, and custom tags in text prompts.**
|
||||
@@ -699,6 +726,8 @@ Example workflow available: [xyz_helpers_lora_testing.json](examples/workflows/x
|
||||
| **Sampler Select Helper** | Intelligent sampler selection with recommendations | ✅ Complete | [Docs](examples/documentation/sampler_select_helper.md) |
|
||||
| **Scheduler Select Helper** | Optimal scheduler selection for samplers | ✅ Complete | [Docs](examples/documentation/scheduler_select_helper.md) |
|
||||
| **Text Encode Sampler Params** | Combined text encoding and parameter management | ✅ Complete | [Docs](examples/documentation/text_encode_sampler_params.md) |
|
||||
| **Local Image Loader** | Visual gallery browser for local media files | ✅ Complete | [Docs](examples/documentation/local_image_loader.md) |
|
||||
| **Model Downloader** | Download models from CivitAI, HuggingFace, and custom URLs | ✅ Complete | [Docs](examples/documentation/model_downloader.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
|
||||
@@ -981,7 +1010,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🏷️ Tags
|
||||
|
||||
`comfyui` `custom-nodes` `image-processing` `ai-tools` `sdxl` `flux` `upscaling` `resolution` `batch-processing` `python` `pytorch`
|
||||
`comfyui` `custom-nodes` `image-processing` `ai-tools` `sdxl` `flux` `upscaling` `resolution` `batch-processing` `model-downloader` `civitai` `huggingface` `python` `pytorch`
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
@@ -992,14 +1021,15 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 19 (13 core tools + 6 xyz-helpers)
|
||||
- **Nodes**: 20 (14 core tools + 6 xyz-helpers)
|
||||
- **Features**: Embedding Autocomplete (settings-based, not a node)
|
||||
- **Categories**: 9 emoji-based categories for better organization
|
||||
- **Download Platforms**: 3 (CivitAI, HuggingFace, Custom URLs)
|
||||
- **Format Support**: 3 (PNG, JPEG, WebP with advanced controls)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Interactive Features**: 8+ (swap buttons, history UI, popup viewers, parameter visualization)
|
||||
- **AI Integration**: Gemini API with 40+ model support
|
||||
- **Test Coverage**: 100% (300+ comprehensive tests)
|
||||
- **Test Coverage**: 100% (470+ comprehensive tests)
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy, Pillow, google-generativeai for Gemini)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"last_path": "/home/vito/ai-apps/ComfyUI-3.12/output",
|
||||
"last_path": "/home/vito/ai-apps/ComfyUI/output",
|
||||
"saved_paths": [
|
||||
"/home/vito/ai-apps/ComfyUI-3.12/output/2025-05-01",
|
||||
"/home/vito/ai-apps/ComfyUI-3.12/output/"
|
||||
|
||||
@@ -8,5 +8,15 @@
|
||||
"image": {
|
||||
"path": "/home/vito/ai-apps/ComfyUI-3.12/output/CharacterName_00016_.png"
|
||||
}
|
||||
},
|
||||
"18": {
|
||||
"image": {
|
||||
"path": "/home/vito/ai-apps/ComfyUI/output/ComfyUI_00002_.png"
|
||||
}
|
||||
},
|
||||
"445": {
|
||||
"image": {
|
||||
"path": "/home/vito/ai-apps/ComfyUI/output/ComfyUI_00002_.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@ from typing import Optional, Callable
|
||||
from urllib.parse import urlparse, unquote
|
||||
import os
|
||||
|
||||
try:
|
||||
import comfy.model_management
|
||||
|
||||
COMFY_AVAILABLE = True
|
||||
except ImportError:
|
||||
COMFY_AVAILABLE = False
|
||||
|
||||
|
||||
class BaseDownloader(ABC):
|
||||
"""Abstract base class for all downloaders"""
|
||||
@@ -38,6 +45,15 @@ class BaseDownloader(ABC):
|
||||
if self._progress_callback:
|
||||
self._progress_callback(downloaded, total, message)
|
||||
|
||||
def check_interrupt(self) -> None:
|
||||
"""Check if processing has been interrupted by user
|
||||
|
||||
Raises:
|
||||
comfy.model_management.InterruptProcessingException: If user cancelled
|
||||
"""
|
||||
if COMFY_AVAILABLE:
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
def extract_filename(self, url: str, default: str = "downloaded_file") -> str:
|
||||
"""Extract filename from URL
|
||||
|
||||
|
||||
@@ -12,6 +12,18 @@ from urllib.parse import urlparse, parse_qs, unquote
|
||||
|
||||
from .base import BaseDownloader
|
||||
|
||||
try:
|
||||
import comfy.model_management
|
||||
|
||||
COMFY_AVAILABLE = True
|
||||
InterruptProcessingException = comfy.model_management.InterruptProcessingException
|
||||
except ImportError:
|
||||
COMFY_AVAILABLE = False
|
||||
# Fallback exception type that will never be raised
|
||||
InterruptProcessingException = type(
|
||||
"InterruptProcessingException", (Exception,), {}
|
||||
)
|
||||
|
||||
|
||||
CHUNK_SIZE = 1638400
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
@@ -258,61 +270,72 @@ class CivitAIDownloader(BaseDownloader):
|
||||
print(f"Size: {self.format_size(total_size)}")
|
||||
|
||||
# Download with progress
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
try:
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
|
||||
if not buffer:
|
||||
break
|
||||
if not buffer:
|
||||
break
|
||||
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
# Check for user cancellation
|
||||
self.check_interrupt()
|
||||
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, total_size, f"{speed:.2f} MB/s")
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(
|
||||
downloaded, total_size, f"{speed:.2f} MB/s"
|
||||
)
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
|
||||
# Verify file size
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
raise Exception(
|
||||
f"Download incomplete. Expected {total_size} bytes, got {actual_size} bytes"
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
|
||||
return output_file
|
||||
# Verify file size
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
raise Exception(
|
||||
f"Download incomplete. Expected {total_size} bytes, got {actual_size} bytes"
|
||||
)
|
||||
|
||||
return output_file
|
||||
except InterruptProcessingException:
|
||||
# Clean up partial download on interrupt
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
raise InterruptProcessingException("Download interrupted")
|
||||
|
||||
@@ -9,6 +9,18 @@ from typing import Optional
|
||||
|
||||
from .base import BaseDownloader
|
||||
|
||||
try:
|
||||
import comfy.model_management
|
||||
|
||||
COMFY_AVAILABLE = True
|
||||
InterruptProcessingException = comfy.model_management.InterruptProcessingException
|
||||
except ImportError:
|
||||
COMFY_AVAILABLE = False
|
||||
# Fallback exception type that will never be raised
|
||||
InterruptProcessingException = type(
|
||||
"InterruptProcessingException", (Exception,), {}
|
||||
)
|
||||
|
||||
|
||||
CHUNK_SIZE = 1638400
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
@@ -120,62 +132,73 @@ class CustomDownloader(BaseDownloader):
|
||||
print("Size: Unknown")
|
||||
|
||||
# Download with progress
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
try:
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
|
||||
if not buffer:
|
||||
break
|
||||
if not buffer:
|
||||
break
|
||||
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
# Check for user cancellation
|
||||
self.check_interrupt()
|
||||
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, total_size, f"{speed:.2f} MB/s")
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(
|
||||
downloaded, total_size, f"{speed:.2f} MB/s"
|
||||
)
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
|
||||
# Verify file size if known
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
print(
|
||||
f"⚠ Warning: Downloaded size ({actual_size} bytes) doesn't match expected size ({total_size} bytes)"
|
||||
)
|
||||
# Don't raise error for custom URLs as size mismatch might be acceptable
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
|
||||
return output_file
|
||||
# Verify file size if known
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
print(
|
||||
f"⚠ Warning: Downloaded size ({actual_size} bytes) doesn't match expected size ({total_size} bytes)"
|
||||
)
|
||||
# Don't raise error for custom URLs as size mismatch might be acceptable
|
||||
|
||||
return output_file
|
||||
except InterruptProcessingException:
|
||||
# Clean up partial download on interrupt
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
raise
|
||||
|
||||
@@ -10,6 +10,18 @@ from urllib.parse import urlparse, quote
|
||||
|
||||
from .base import BaseDownloader
|
||||
|
||||
try:
|
||||
import comfy.model_management
|
||||
|
||||
COMFY_AVAILABLE = True
|
||||
InterruptProcessingException = comfy.model_management.InterruptProcessingException
|
||||
except ImportError:
|
||||
COMFY_AVAILABLE = False
|
||||
# Fallback exception type that will never be raised
|
||||
InterruptProcessingException = type(
|
||||
"InterruptProcessingException", (Exception,), {}
|
||||
)
|
||||
|
||||
|
||||
CHUNK_SIZE = 1638400
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
@@ -188,61 +200,72 @@ class HuggingFaceDownloader(BaseDownloader):
|
||||
print(f"Size: {self.format_size(total_size)}")
|
||||
|
||||
# Download with progress
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
try:
|
||||
with open(output_file, "wb") as f:
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
while True:
|
||||
chunk_start_time = time.time()
|
||||
buffer = response.read(CHUNK_SIZE)
|
||||
chunk_end_time = time.time()
|
||||
|
||||
if not buffer:
|
||||
break
|
||||
if not buffer:
|
||||
break
|
||||
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
downloaded += len(buffer)
|
||||
f.write(buffer)
|
||||
chunk_time = chunk_end_time - chunk_start_time
|
||||
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
# Check for user cancellation
|
||||
self.check_interrupt()
|
||||
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, total_size, f"{speed:.2f} MB/s")
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
# Calculate speed
|
||||
speed = self.calculate_speed(len(buffer), chunk_time)
|
||||
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
# Report progress
|
||||
if total_size is not None:
|
||||
progress = downloaded / total_size
|
||||
sys.stdout.write(
|
||||
f'\r[{"=" * int(progress * 50):<50}] {progress * 100:.2f}% - {speed:.2f} MB/s'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(
|
||||
downloaded, total_size, f"{speed:.2f} MB/s"
|
||||
)
|
||||
else:
|
||||
sys.stdout.write(
|
||||
f"\rDownloaded: {self.format_size(downloaded)} - {speed:.2f} MB/s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
self.report_progress(downloaded, 0, f"{speed:.2f} MB/s")
|
||||
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
end_time = time.time()
|
||||
time_taken = end_time - start_time
|
||||
hours, remainder = divmod(time_taken, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
if hours > 0:
|
||||
time_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
|
||||
elif minutes > 0:
|
||||
time_str = f"{int(minutes)}m {int(seconds)}s"
|
||||
else:
|
||||
time_str = f"{int(seconds)}s"
|
||||
|
||||
# Verify file size
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
raise Exception(
|
||||
f"Download incomplete. Expected {total_size} bytes, got {actual_size} bytes"
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
print(f"✓ Download completed in {time_str}")
|
||||
print(f"✓ File saved as: {output_file}")
|
||||
|
||||
return output_file
|
||||
# Verify file size
|
||||
actual_size = os.path.getsize(output_file)
|
||||
if total_size and actual_size != total_size:
|
||||
raise Exception(
|
||||
f"Download incomplete. Expected {total_size} bytes, got {actual_size} bytes"
|
||||
)
|
||||
|
||||
return output_file
|
||||
except InterruptProcessingException:
|
||||
# Clean up partial download on interrupt
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
raise
|
||||
|
||||
@@ -28,11 +28,28 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
"default": 12345,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF,
|
||||
"control_after_generate": True,
|
||||
"tooltip": "Seed value for generation processes. "
|
||||
"History UI tracks all changes automatically.",
|
||||
"Auto-increments/decrements after each run based on mode.",
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
"optional": {
|
||||
"mode": (
|
||||
[
|
||||
"",
|
||||
"fixed",
|
||||
"increment",
|
||||
"decrement",
|
||||
"randomize",
|
||||
], # Added empty string for legacy workflows
|
||||
{
|
||||
"default": "fixed",
|
||||
"tooltip": "Seed behavior after generation: "
|
||||
"fixed (no change), increment (+1), decrement (-1), or randomize (new random)",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
@@ -40,17 +57,48 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
FUNCTION = "output_seed"
|
||||
CATEGORY = "🫶 ComfyAssets/🌱 Seeds"
|
||||
|
||||
def output_seed(self, seed: int) -> Tuple[int]:
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, seed, mode="fixed"):
|
||||
"""Validate inputs and handle legacy workflows."""
|
||||
# Handle empty or missing mode from old workflows (legacy support)
|
||||
if mode is None or mode == "" or mode == "undefined":
|
||||
return True # Will use default "fixed" in output_seed
|
||||
|
||||
# Validate mode is in allowed list
|
||||
valid_modes = ["fixed", "increment", "decrement", "randomize"]
|
||||
if mode not in valid_modes:
|
||||
return f"Invalid mode: {mode}. Must be one of {valid_modes}"
|
||||
|
||||
return True
|
||||
|
||||
def output_seed(self, seed: int, mode: str = "fixed") -> Tuple[int]:
|
||||
"""
|
||||
Output the seed value for use in other nodes.
|
||||
|
||||
Args:
|
||||
seed: Input seed value
|
||||
mode: Seed mode (fixed, increment, decrement, randomize) - not used in output,
|
||||
but controls the widget behavior via control_after_generate
|
||||
|
||||
Returns:
|
||||
Tuple containing the seed value
|
||||
"""
|
||||
try:
|
||||
# Handle empty mode from old workflows
|
||||
if not mode or mode == "":
|
||||
mode = "fixed"
|
||||
|
||||
# Validate mode is in allowed list
|
||||
valid_modes = ["fixed", "increment", "decrement", "randomize"]
|
||||
if mode not in valid_modes:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(
|
||||
f"{self.__class__.__name__}: Invalid mode '{mode}'. Using 'fixed'."
|
||||
)
|
||||
mode = "fixed"
|
||||
|
||||
# Validate and sanitize the seed
|
||||
if not validate_seed_value(seed):
|
||||
# Log the validation error but don't raise
|
||||
@@ -65,6 +113,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode):
|
||||
|
||||
clean_seed = sanitize_seed_value(seed)
|
||||
|
||||
# Note: The mode parameter controls the widget's control_after_generate behavior
|
||||
# The actual increment/decrement/randomize happens automatically in the UI
|
||||
# based on the control_after_generate setting and the mode dropdown value
|
||||
|
||||
return (clean_seed,)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -78,7 +78,37 @@ PRESET_METADATA: Dict[str, PresetMetadata] = {
|
||||
"Portrait",
|
||||
"SDXL portrait 5:12 - very tall portrait",
|
||||
),
|
||||
"704×1408": PresetMetadata(
|
||||
704,
|
||||
1408,
|
||||
"1:2",
|
||||
0.5,
|
||||
0.99,
|
||||
"SDXL",
|
||||
"Portrait",
|
||||
"SDXL portrait 1:2 - extreme tall portrait",
|
||||
),
|
||||
"960×1024": PresetMetadata(
|
||||
960,
|
||||
1024,
|
||||
"15:16",
|
||||
0.938,
|
||||
0.98,
|
||||
"SDXL",
|
||||
"Portrait",
|
||||
"SDXL near-square portrait - subtle portrait",
|
||||
),
|
||||
# SDXL Presets - Landscape
|
||||
"1024×960": PresetMetadata(
|
||||
1024,
|
||||
960,
|
||||
"16:15",
|
||||
1.067,
|
||||
0.98,
|
||||
"SDXL",
|
||||
"Landscape",
|
||||
"SDXL near-square landscape - subtle landscape",
|
||||
),
|
||||
"1152×896": PresetMetadata(
|
||||
1152,
|
||||
896,
|
||||
@@ -119,6 +149,16 @@ PRESET_METADATA: Dict[str, PresetMetadata] = {
|
||||
"Landscape",
|
||||
"SDXL landscape 12:5 - very wide landscape",
|
||||
),
|
||||
"1728×576": PresetMetadata(
|
||||
1728,
|
||||
576,
|
||||
"3:1",
|
||||
3.0,
|
||||
1.0,
|
||||
"SDXL",
|
||||
"Landscape",
|
||||
"SDXL landscape 3:1 - extreme wide panoramic",
|
||||
),
|
||||
# FLUX Presets - High Quality
|
||||
"1920×1080": PresetMetadata(
|
||||
1920,
|
||||
|
||||
@@ -33,6 +33,9 @@ class KikoEmbeddingAutocomplete {
|
||||
this.widgetCleanupMap = new WeakMap();
|
||||
this.activeWidgets = new WeakSet();
|
||||
|
||||
// Track pending fetch requests for cleanup
|
||||
this.pendingFetches = new Set();
|
||||
|
||||
// Debounce resource fetching
|
||||
this.fetchResourcesDebounced = this.debounce(() => this.fetchResources(), 500);
|
||||
this.fetchResourcesDebounced();
|
||||
@@ -42,9 +45,6 @@ class KikoEmbeddingAutocomplete {
|
||||
this.loadCustomWords(this.settings.customWordsUrl);
|
||||
}
|
||||
|
||||
// Track pending fetch requests for cleanup
|
||||
this.pendingFetches = new Set();
|
||||
|
||||
// Single suggestion container for all widgets
|
||||
this.suggestionContainer = null;
|
||||
this.currentWidget = null;
|
||||
|
||||
Reference in New Issue
Block a user