Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332a74225d | ||
|
|
d757b623d6 | ||
|
|
64e844ec42 | ||
|
|
3ed188d63f | ||
|
|
b9cc9f295d | ||
|
|
271cd020c1 | ||
|
|
e34807855a | ||
|
|
d32e18f844 | ||
|
|
228b74ae5e | ||
|
|
6197b482df | ||
|
|
f62129afda | ||
|
|
8d5065c975 | ||
|
|
2d27c32bfd | ||
|
|
3ecab5ac08 | ||
|
|
70592114f9 | ||
|
|
407fc4ca7b |
@@ -25,6 +25,10 @@ per-file-ignores =
|
||||
__init__.py:F401,F403
|
||||
# Allow assertions in tests
|
||||
tests/*:S101
|
||||
# Allow higher complexity for Gemini prompt module
|
||||
kikotools/tools/gemini_prompt/logic.py:C901
|
||||
kikotools/tools/gemini_prompt/models.py:C901
|
||||
kikotools/tools/gemini_prompt/node.py:C901
|
||||
|
||||
# Statistics
|
||||
count = True
|
||||
|
||||
@@ -159,3 +159,6 @@ test_images/
|
||||
test_outputs/
|
||||
experiments/
|
||||
.claude/
|
||||
|
||||
# Gemini model cache
|
||||
.gemini_models_cache.json
|
||||
|
||||
@@ -8,14 +8,14 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.10
|
||||
args: ['--line-length=127'] # Match CI configuration
|
||||
args: ['--line-length=88'] # Match CI configuration
|
||||
|
||||
# Python linting with flake8
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--max-line-length=127', '--max-complexity=10']
|
||||
args: ['--max-line-length=88', '--max-complexity=10']
|
||||
exclude: '^tests/'
|
||||
|
||||
# Python type checking with mypy
|
||||
|
||||
@@ -14,6 +14,19 @@ ComfyUI-KikoTools provides carefully crafted, production-ready nodes grouped und
|
||||
|
||||
### ✨ Current Tools
|
||||
|
||||
| Tool | Description | Category |
|
||||
|------|-------------|----------|
|
||||
| [📐 Resolution Calculator](#-resolution-calculator) | Calculate upscaled dimensions with model optimization | Image Processing |
|
||||
| [📏 Width Height Selector](#-width-height-selector) | Preset-based dimension selection with visual swap | Dimension Control |
|
||||
| [🎲 Seed History](#-seed-history) | Advanced seed tracking with interactive history | Generation Control |
|
||||
| [⚙️ Sampler Combo](#️-sampler-combo) | Unified sampling configuration interface | Sampling |
|
||||
| [📦 Empty Latent Batch](#-empty-latent-batch) | Create empty latent batches with preset support | Latent Generation |
|
||||
| [💾 Kiko Save Image](#-kiko-save-image) | Enhanced image saving with popup viewer | File Management |
|
||||
| [📋 Display Text](#-display-text) | Smart text display with prompt detection | Text Display |
|
||||
| [🤖 Gemini Prompt Engineer](#-gemini-prompt-engineer) | AI-powered image analysis and prompt generation | AI Integration |
|
||||
| [🔍 Display Any](#-display-any) | Universal debugging tool for any data type | Debugging |
|
||||
| [🖼️ Image to Multiple Of](#️-image-to-multiple-of) | Adjust dimensions to multiples for compatibility | Image Processing |
|
||||
|
||||
#### 📐 Resolution Calculator
|
||||
Calculate upscaled dimensions from image or latent inputs with precision.
|
||||
|
||||
@@ -29,6 +42,8 @@ Calculate upscaled dimensions from image or latent inputs with precision.
|
||||
- Ensure ComfyUI tensor compatibility
|
||||
- Optimize batch processing workflows
|
||||
|
||||

|
||||
|
||||
#### 📏 Width Height Selector
|
||||
Advanced preset-based dimension selection with visual swap button.
|
||||
|
||||
@@ -60,6 +75,8 @@ Advanced seed tracking with interactive history management and UI.
|
||||
- Maintain reproducibility across sessions
|
||||
- Compare results from different seeds efficiently
|
||||
|
||||
![Seed History functionality is shown in various workflow examples]
|
||||
|
||||
#### ⚙️ Sampler Combo
|
||||
Unified sampling configuration interface combining sampler, scheduler, steps, and CFG.
|
||||
|
||||
@@ -92,6 +109,8 @@ Advanced empty latent creation with preset support and batch processing capabili
|
||||
- Optimize memory usage with batch size planning
|
||||
- Quick preset-based latent generation for different aspect ratios
|
||||
|
||||

|
||||
|
||||
#### 💾 Kiko Save Image
|
||||
Enhanced image saving with format selection, quality control, and floating popup viewer.
|
||||
|
||||
@@ -104,6 +123,28 @@ Enhanced image saving with format selection, quality control, and floating popup
|
||||
- **Smart UI**: Auto-hide/show, minimize/maximize, roll-up functionality
|
||||
- **Popup Toggle**: Enable/disable popup viewer per save operation
|
||||
|
||||

|
||||
|
||||
#### 📋 Display Text
|
||||
Advanced text display node with intelligent formatting and enhanced user interaction.
|
||||
|
||||
- **Smart Prompt Detection**: Automatically detects positive/negative prompt pairs and displays in split view
|
||||
- **Text Wrapping**: Proper word wrapping that reflows when node is resized
|
||||
- **Scrollable Content**: Mouse wheel scrolling for long texts with visual scroll indicators
|
||||
- **Copy Functionality**: Always-visible copy button with visual feedback
|
||||
- **Split View Mode**: Automatic detection and formatting of SDXL-style prompts
|
||||
- **Responsive Design**: Content adapts to node resizing with proper text reflow
|
||||
- **Clean Formatting**: Strips prompt labels when copying for direct use
|
||||
|
||||
**Use Cases:**
|
||||
- Display generated prompts with proper formatting
|
||||
- Compare positive and negative prompts side-by-side
|
||||
- Copy prompts without manual label removal
|
||||
- View long text content with proper wrapping
|
||||
- Debug prompt generation workflows
|
||||
|
||||

|
||||
|
||||
#### 🤖 Gemini Prompt Engineer
|
||||
AI-powered image analysis using Google's Gemini to generate optimized prompts for various models.
|
||||
|
||||
@@ -114,6 +155,9 @@ AI-powered image analysis using Google's Gemini to generate optimized prompts fo
|
||||
- **Flexible API Key Management**: Environment variable, config file, or direct input
|
||||
- **Visual Status Feedback**: Real-time processing indicators and error states
|
||||
- **Help Integration**: Built-in setup guide and documentation
|
||||
- **Dynamic Model Refresh**: Fetch latest Gemini models with refresh button
|
||||
- **Model Caching**: Persistent model list storage for offline access
|
||||
- **Enhanced SDXL Prompts**: Improved formatting with layered structure and quality boosters
|
||||
|
||||
**Use Cases:**
|
||||
- Reverse-engineer prompts from reference images
|
||||
@@ -121,6 +165,45 @@ AI-powered image analysis using Google's Gemini to generate optimized prompts fo
|
||||
- Generate consistent style descriptions across workflows
|
||||
- Create detailed scene breakdowns for complex compositions
|
||||
- Analyze and replicate lighting/mood from existing artwork
|
||||
- Access latest Gemini models including 2.0 and 2.5 versions
|
||||
|
||||

|
||||
|
||||
#### 🔍 Display Any
|
||||
Universal debugging node that displays any type of input value or tensor information.
|
||||
|
||||
- **Universal Input Acceptance**: Works with any data type (tensors, strings, numbers, lists, dicts)
|
||||
- **Two Display Modes**: Raw value showing string representation, or tensor shape extraction
|
||||
- **Nested Structure Support**: Finds tensors within complex nested data structures
|
||||
- **Debugging Focus**: Essential tool for understanding data flow and tensor dimensions
|
||||
- **Clean Output**: Formatted display directly in ComfyUI interface
|
||||
|
||||
**Use Cases:**
|
||||
- Debug tensor dimensions at any point in workflow
|
||||
- Inspect latent space data structures
|
||||
- View metadata and configuration objects
|
||||
- Track shape changes through processing nodes
|
||||
- Understand complex data types in ComfyUI
|
||||
|
||||

|
||||
|
||||
#### 🖼️ Image to Multiple Of
|
||||
Adjusts image dimensions to be multiples of a specified value for model compatibility.
|
||||
|
||||
- **Dimension Adjustment**: Ensures image dimensions are multiples of specified value (e.g., 64, 128)
|
||||
- **Two Processing Methods**: Center crop for minimal loss, or rescale to fit
|
||||
- **Model Compatibility**: Essential for models requiring specific dimension constraints
|
||||
- **Flexible Multiple Values**: Support from 1 to 256 with 16-step increments
|
||||
- **Preserves Quality**: Smart processing maintains image quality
|
||||
|
||||
**Use Cases:**
|
||||
- Prepare images for VAE encoding (multiple of 8 requirement)
|
||||
- Ensure compatibility with specific model architectures
|
||||
- Standardize dimensions across image batches
|
||||
- Fix dimension errors in complex workflows
|
||||
- Optimize for tiled processing requirements
|
||||
|
||||

|
||||
|
||||
### 💾 Kiko Save Image Features
|
||||
|
||||
@@ -245,18 +328,55 @@ Generate Image → Kiko Save Image → Floating Popup Viewer
|
||||
**Features:** Click images to open in new tabs, download individual files, batch selection
|
||||
**Advantages:** Immediate preview without file explorer, multi-format comparison, advanced quality controls
|
||||
|
||||
### Display Text Example
|
||||
|
||||
```
|
||||
Gemini Prompt → Display Text → Copy to Clipboard
|
||||
📋 SDXL prompt ↘ auto-split ↘ [📋 Positive] [📋 Negative]
|
||||
view → formatted display
|
||||
```
|
||||
|
||||
**Input:** Text with "Positive prompt:" and "Negative prompt:" sections
|
||||
**Output:** Split view with individual copy buttons
|
||||
**Features:** Text wrapping, scrolling, responsive resizing
|
||||
**Smart Detection:** Automatically formats SDXL-style prompts
|
||||
|
||||
### Gemini Prompt Engineer Example
|
||||
```
|
||||
Load Image → Gemini Prompt → Text Generation Model
|
||||
🖼️ reference ↘ type: FLUX ↘ "majestic landscape..."
|
||||
[API key] → FLUX model
|
||||
Load Image → Gemini Prompt → Display Text → Text Generation Model
|
||||
🖼️ reference ↘ type: SDXL ↘ split view ↘ "detailed portrait..."
|
||||
[Refresh Models] → SDXL model
|
||||
```
|
||||
|
||||
**Input:** Reference image for style analysis
|
||||
**Prompt Type:** FLUX (detailed artistic prompts)
|
||||
**Output:** Optimized prompt with style, lighting, composition details
|
||||
**Prompt Type:** SDXL (positive/negative pairs with layered structure)
|
||||
**Model Selection:** Dynamic list with latest Gemini models (2.0, 2.5)
|
||||
**Output:** Optimized prompts following community best practices
|
||||
**API:** Requires Gemini API key (free tier available)
|
||||
**Use Case:** Recreate similar style/mood from reference images
|
||||
**Refresh:** Click button to fetch latest available models
|
||||
|
||||
### Display Any Example
|
||||
```
|
||||
Any Node → Display Any → Debug Output
|
||||
🔍 tensor ↘ mode: shape ↘ "[[1, 3, 512, 512]]"
|
||||
```
|
||||
|
||||
**Input:** Any data type (image, latent, config, etc.)
|
||||
**Mode:** "raw value" or "tensor shape"
|
||||
**Output:** Formatted display of value or tensor dimensions
|
||||
**Use Case:** Debug workflows, inspect data structures
|
||||
|
||||
### Image to Multiple Of Example
|
||||
```
|
||||
Load Image → Image to Multiple Of → VAE Encode → KSampler
|
||||
🖼️ 513×769 ↘ multiple: 64 ↘ 512×768 → latent
|
||||
method: crop
|
||||
```
|
||||
|
||||
**Input:** Image with arbitrary dimensions
|
||||
**Multiple Of:** 64 (common for VAE compatibility)
|
||||
**Method:** "center crop" or "rescale"
|
||||
**Output:** Adjusted image with compatible dimensions
|
||||
|
||||
### Common Workflows
|
||||
|
||||
@@ -300,6 +420,10 @@ Load Image → Gemini Prompt → Text Generation Model
|
||||
| **Sampler Combo** | Unified sampling configuration with smart recommendations | ✅ Complete | [Docs](examples/documentation/sampler_combo.md) |
|
||||
| **Empty Latent Batch** | Create empty latent batches with preset support | ✅ Complete | [Docs](examples/documentation/empty_latent_batch.md) |
|
||||
| **Kiko Save Image** | Enhanced image saving with popup viewer and multi-format support | ✅ Complete | [Docs](examples/documentation/kiko_save_image.md) |
|
||||
| **Display Text** | Advanced text display with smart prompt detection and split view | ✅ Complete | [Docs](examples/documentation/display_text.md) |
|
||||
| **Gemini Prompt Engineer** | AI-powered image analysis with dynamic model refresh | ✅ Complete | [Docs](examples/documentation/gemini_prompt.md) |
|
||||
| **Display Any** | Universal debugging tool for any data type or tensor shapes | ✅ Complete | [Docs](examples/documentation/display_any.md) |
|
||||
| **Image to Multiple Of** | Adjust image dimensions to multiples for model compatibility | ✅ Complete | [Docs](examples/documentation/image_to_multiple_of.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
|
||||
@@ -593,14 +717,15 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 6 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo, Empty Latent Batch, Kiko Save Image)
|
||||
- **Nodes**: 10 (Resolution Calculator, Width Height Selector, Seed History, Sampler Combo, Empty Latent Batch, Kiko Save Image, Display Text, Gemini Prompt Engineer, Display Any, Image to Multiple Of)
|
||||
- **Format Support**: 3 (PNG, JPEG, WebP with advanced controls)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Interactive Features**: 4 (Width/Height Swap Button, Seed History UI, Empty Latent Batch Swap Button, Kiko Save Image Popup Viewer)
|
||||
- **Interactive Features**: 6 (Width/Height Swap Button, Seed History UI, Empty Latent Batch Swap Button, Kiko Save Image Popup Viewer, Display Text Split View, Gemini Model Refresh)
|
||||
- **AI Integration**: Gemini API with 40+ model support
|
||||
- **Test Coverage**: 100% (200+ comprehensive tests)
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy, Pillow)
|
||||
- **Dependencies**: Minimal (PyTorch, NumPy, Pillow, google-generativeai for Gemini)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Display Any
|
||||
|
||||
The Display Any node is a debugging and inspection tool that can display any type of input value in ComfyUI. It's particularly useful for understanding data structures and tensor shapes during workflow development.
|
||||
|
||||
## Features
|
||||
|
||||
- **Universal Input**: Accepts any type of input data (tensors, strings, numbers, lists, dictionaries, etc.)
|
||||
- **Two Display Modes**:
|
||||
- **Raw Value**: Shows the string representation of the input
|
||||
- **Tensor Shape**: Extracts and displays the shapes of any tensors found in the input
|
||||
- **Nested Structure Support**: Can find tensors within nested dictionaries and lists
|
||||
- **UI Output**: Displays results directly in the ComfyUI interface
|
||||
|
||||
## Inputs
|
||||
|
||||
- **input** (*): Any value you want to display or inspect
|
||||
- **mode** (DROPDOWN): Display mode selection
|
||||
- `raw value`: Shows the complete string representation of the input
|
||||
- `tensor shape`: Extracts and shows shapes of any tensors in the input
|
||||
|
||||
## Outputs
|
||||
|
||||
- **display_text** (STRING): The formatted display text
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Display Simple Values
|
||||
|
||||
Connect any output to see its raw value:
|
||||
```
|
||||
String Input: "Hello, ComfyUI!"
|
||||
Mode: raw value
|
||||
Output: "Hello, ComfyUI!"
|
||||
```
|
||||
|
||||
### 2. Inspect Tensor Shapes
|
||||
|
||||
Great for debugging image processing pipelines:
|
||||
```
|
||||
Image Tensor: [1, 3, 512, 512]
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 512, 512]]"
|
||||
```
|
||||
|
||||
### 3. Debug Complex Data Structures
|
||||
|
||||
View nested data structures with multiple tensors:
|
||||
```python
|
||||
Input: {
|
||||
"images": tensor([1, 3, 256, 256]),
|
||||
"masks": [tensor([256, 256]), tensor([256, 256, 1])],
|
||||
"config": {"steps": 20}
|
||||
}
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 256, 256], [256, 256], [256, 256, 1]]"
|
||||
```
|
||||
|
||||
### 4. Workflow Debugging
|
||||
|
||||
Use Display Any nodes at various points in your workflow to understand data flow:
|
||||
- After loading images to verify dimensions
|
||||
- Before/after processing nodes to track shape changes
|
||||
- To inspect conditioning or latent data structures
|
||||
- To view metadata or configuration dictionaries
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Image Pipeline Debugging
|
||||
Place Display Any nodes after image loading and processing nodes to track dimension changes:
|
||||
```
|
||||
Load Image → Display Any (tensor shape) → Resize → Display Any (tensor shape)
|
||||
```
|
||||
|
||||
### Latent Space Inspection
|
||||
Understand latent dimensions in your workflows:
|
||||
```
|
||||
VAE Encode → Display Any (tensor shape) → KSampler → Display Any (raw value)
|
||||
```
|
||||
|
||||
### Configuration Verification
|
||||
Display complex configuration objects to ensure correct settings:
|
||||
```
|
||||
Config Node → Display Any (raw value) → Processing Node
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Multiple Display Nodes**: You can use multiple Display Any nodes in a single workflow to track data at different stages
|
||||
|
||||
2. **Tensor Shape Mode**: Particularly useful when working with:
|
||||
- Image batches to verify batch size
|
||||
- Latent tensors to understand dimensions
|
||||
- Mask arrays to check compatibility
|
||||
|
||||
3. **Raw Value Mode**: Best for:
|
||||
- String prompts and text
|
||||
- Configuration dictionaries
|
||||
- Debugging node outputs
|
||||
- Understanding data structure
|
||||
|
||||
4. **No Tensors Found**: If you see "No tensors found in input" in tensor shape mode, the input doesn't contain any tensor-like objects (numpy arrays, torch tensors, etc.)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- The node uses `str()` for raw value display, providing Python's string representation
|
||||
- Tensor shape detection works with any object that has a `shape` attribute
|
||||
- Nested structure traversal supports dictionaries, lists, and tuples
|
||||
- The output is both displayed in the UI and available as a string output for further processing
|
||||
|
||||
## Example Workflow Integration
|
||||
|
||||
```
|
||||
[Load Image] → [Image Processing] → [Display Any (tensor shape)]
|
||||
↓
|
||||
"[[1, 3, 512, 512]]"
|
||||
↓
|
||||
[Text Multiline] ← [Concatenate] ← "Image dimensions: "
|
||||
```
|
||||
|
||||
This creates a text output showing the current image dimensions that can be used elsewhere in your workflow.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Display Text
|
||||
|
||||
The Display Text node provides advanced text display capabilities with smart formatting, interactive features, and responsive design for ComfyUI workflows.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Prompt Detection**: Automatically detects and formats SDXL-style positive/negative prompt pairs
|
||||
- **Text Wrapping**: Proper word wrapping that reflows when node is resized
|
||||
- **Scrollable Content**: Mouse wheel scrolling for long texts with visual indicators
|
||||
- **Copy Functionality**: Always-visible copy button with visual feedback
|
||||
- **Split View Mode**: Side-by-side display for prompt pairs
|
||||
- **Responsive Design**: Content adapts to node resizing
|
||||
|
||||
## Inputs
|
||||
|
||||
- **text** (STRING): The text to display
|
||||
- Can be a single text block
|
||||
- Can contain "Positive prompt:" and "Negative prompt:" sections for automatic split view
|
||||
|
||||
## Outputs
|
||||
|
||||
- **text** (STRING): Pass-through of the input text
|
||||
|
||||
## Display Modes
|
||||
|
||||
### Single Text Mode
|
||||
|
||||
When the input is regular text without prompt markers, it displays as a single scrollable text area with:
|
||||
- Word wrapping at word boundaries
|
||||
- Vertical scrolling for long content
|
||||
- Single copy button for the entire text
|
||||
|
||||
### Split View Mode
|
||||
|
||||
Automatically activated when text contains both "Positive prompt:" and "Negative prompt:" sections:
|
||||
- Side-by-side display with 50/50 split
|
||||
- Independent scrolling for each section
|
||||
- Separate copy buttons for each prompt
|
||||
- Labels are stripped when copying (clean prompts)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Display Generated Prompts
|
||||
|
||||
```
|
||||
Gemini Prompt → Display Text → Copy to workflow
|
||||
```
|
||||
The node automatically detects SDXL format and shows positive/negative prompts side-by-side.
|
||||
|
||||
### 2. Debug Text Processing
|
||||
|
||||
```
|
||||
Text Processing → Display Text → Further Processing
|
||||
```
|
||||
View intermediate text processing results with proper formatting.
|
||||
|
||||
### 3. Show Long Descriptions
|
||||
|
||||
```
|
||||
Load Text → Display Text → Review
|
||||
```
|
||||
Display long text content with scrolling and word wrapping.
|
||||
|
||||
## Interactive Features
|
||||
|
||||
### Copy Button
|
||||
- Always visible in the top-right corner
|
||||
- Shows "✓ Copied!" feedback on click
|
||||
- In split view: separate buttons for each section
|
||||
- Strips prompt labels for clean copying
|
||||
|
||||
### Scrolling
|
||||
- Mouse wheel scrolling when hovering over text
|
||||
- Visual indicators appear when content is scrollable
|
||||
- Smooth scrolling with proper boundaries
|
||||
- Independent scrolling in split view mode
|
||||
|
||||
### Resizing
|
||||
- Text reflows when node width changes
|
||||
- Maintains readability at different sizes
|
||||
- Split view maintains 50/50 proportions
|
||||
- Minimum height ensures usability
|
||||
|
||||
## Smart Prompt Detection
|
||||
|
||||
The node intelligently detects prompt formats:
|
||||
|
||||
1. **SDXL Format**:
|
||||
- Looks for "Positive prompt:" and "Negative prompt:" markers
|
||||
- Case-insensitive detection
|
||||
- Handles various formatting styles
|
||||
|
||||
2. **Label Stripping**:
|
||||
- When copying from split view, labels are removed
|
||||
- "Positive prompt: beautiful sunset" → "beautiful sunset"
|
||||
- Clean prompts ready for direct use
|
||||
|
||||
## Styling
|
||||
|
||||
- **Font**: Monospace for consistent alignment
|
||||
- **Colors**:
|
||||
- Text: Light gray (#ddd) on dark background
|
||||
- Background: Semi-transparent dark (#1a1a1a)
|
||||
- Borders: Subtle gray (#333)
|
||||
- **Spacing**: Comfortable padding and line height
|
||||
- **Visual Feedback**: Hover effects on interactive elements
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Prompt Engineering Workflows
|
||||
- Display AI-generated prompts with proper formatting
|
||||
- Compare positive and negative prompts side-by-side
|
||||
- Copy refined prompts without manual cleanup
|
||||
|
||||
### Text Processing Pipelines
|
||||
- Debug text transformations at each step
|
||||
- View formatted outputs from text nodes
|
||||
- Monitor prompt construction workflows
|
||||
|
||||
### Documentation and Notes
|
||||
- Display workflow instructions
|
||||
- Show generation parameters
|
||||
- Present formatted metadata
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Text Processing**: Preserves original text while adding display formatting
|
||||
- **Responsive Design**: CSS-based layout adapts to node dimensions
|
||||
- **Event Handling**: Proper event propagation for ComfyUI compatibility
|
||||
- **Memory Efficient**: Only renders visible text portions
|
||||
|
||||
## Tips
|
||||
|
||||
1. **For Long Prompts**: The scrolling feature handles texts of any length efficiently
|
||||
2. **Quick Copy**: Use the copy buttons to quickly grab prompts for other nodes
|
||||
3. **Resizing**: Drag node edges to find optimal display width for your content
|
||||
4. **Split View**: Works best with SDXL-format prompts but handles any dual-section text
|
||||
|
||||
## Integration Example
|
||||
|
||||
```
|
||||
[Gemini Prompt Engineer] → [Display Text] → [Copy Button Click]
|
||||
↓ ↓ ↓
|
||||
SDXL Format Split View Display Clean Prompts
|
||||
```
|
||||
|
||||
This creates a seamless workflow from prompt generation to usage, with the Display Text node providing the visual interface for review and interaction.
|
||||
@@ -8,6 +8,9 @@ The Gemini Prompt Engineer node uses Google's Gemini AI to analyze images and ge
|
||||
- **Custom Prompts**: Override templates with your own system prompts
|
||||
- **Visual Feedback**: UI shows processing status and error states
|
||||
- **Flexible API Key Management**: Multiple ways to provide API credentials
|
||||
- **Dynamic Model Selection**: Fetch and use latest Gemini models with refresh button
|
||||
- **Model Caching**: Persistent storage of available models for offline access
|
||||
- **Help Integration**: Built-in setup guide accessible via help button
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -49,6 +52,10 @@ Choose one of these methods:
|
||||
- `sdxl`: Positive/negative prompt pairs with weight emphasis
|
||||
- `danbooru`: Anime-style booru tags with underscores
|
||||
- `video`: Motion and temporal descriptions for video generation
|
||||
- **model** (DROPDOWN): Gemini model selection
|
||||
- Dynamically populated list of available models
|
||||
- Includes latest models like gemini-2.0-flash-exp
|
||||
- Click refresh button to update model list
|
||||
- **api_key** (STRING, optional): Gemini API key if not set elsewhere
|
||||
- **custom_prompt** (STRING, optional): Override template with custom system prompt
|
||||
|
||||
@@ -72,15 +79,27 @@ majestic mountain landscape at golden hour, oil painting style, dramatic lightin
|
||||
```
|
||||
|
||||
### SDXL Format
|
||||
Generates positive and negative prompt pairs:
|
||||
- Detailed positive prompts with weight emphasis
|
||||
Generates positive and negative prompt pairs with enhanced structure:
|
||||
- Layered positive prompts: main subject → style → composition → technical
|
||||
- Comprehensive negative prompts to avoid common issues
|
||||
- Uses parentheses for emphasis: `(detailed eyes:1.2)`
|
||||
- Includes quality boosters and technical specifications
|
||||
|
||||
Example output:
|
||||
```
|
||||
Positive: beautiful woman, (detailed eyes:1.2), flowing red dress, golden hour lighting, professional photography, 85mm lens, shallow depth of field, bokeh, high resolution, masterpiece
|
||||
Negative: low quality, blurry, distorted features, bad anatomy, poorly drawn, amateur, oversaturated, jpeg artifacts
|
||||
Positive prompt:
|
||||
beautiful woman with flowing red hair, elegant pose, (detailed eyes:1.2), serene expression
|
||||
oil painting style, renaissance art influence, classical portraiture
|
||||
golden hour lighting, warm color palette, soft shadows, dramatic chiaroscuro
|
||||
centered composition, rule of thirds, shallow depth of field, bokeh background
|
||||
masterpiece, best quality, highly detailed, 8k uhd, professional artwork
|
||||
|
||||
Negative prompt:
|
||||
low quality, worst quality, blurry, out of focus, pixelated, low resolution
|
||||
bad anatomy, deformed features, extra limbs, missing limbs, disconnected limbs
|
||||
poorly drawn face, poorly drawn hands, amateur drawing, bad proportions
|
||||
oversaturated, overexposed, underexposed, bad lighting, harsh shadows
|
||||
jpeg artifacts, watermark, signature, text, cropped, duplicate
|
||||
```
|
||||
|
||||
### Danbooru Format
|
||||
@@ -130,12 +149,40 @@ The node provides clear error messages for common issues:
|
||||
|
||||
Errors are displayed in the prompt output for easy debugging.
|
||||
|
||||
## Model Selection
|
||||
|
||||
### Dynamic Model List
|
||||
- Click the refresh button (🔄) next to the model dropdown to fetch latest models
|
||||
- Models are fetched from Google's API and include all available versions
|
||||
- Common models include:
|
||||
- `gemini-2.0-flash-exp`: Latest experimental flash model
|
||||
- `gemini-1.5-pro`: Advanced model with larger context
|
||||
- `gemini-1.5-flash`: Fast and efficient for most tasks
|
||||
|
||||
### Model Caching
|
||||
- Available models are cached locally for offline access
|
||||
- Cache persists across ComfyUI sessions
|
||||
- Refresh button updates the cache with latest models
|
||||
|
||||
## UI Features
|
||||
|
||||
### Help Button
|
||||
- Click the help button (?) for quick setup instructions
|
||||
- Shows API key setup methods
|
||||
- Links to Google AI Studio for key generation
|
||||
|
||||
### Status Indicators
|
||||
- Processing spinner during API calls
|
||||
- Error messages displayed in red
|
||||
- Success feedback when prompt is generated
|
||||
|
||||
## Tips
|
||||
|
||||
1. **API Usage**: Gemini has generous free tier limits, but be mindful of rate limits
|
||||
2. **Image Quality**: Higher resolution images provide better analysis results
|
||||
3. **Prompt Refinement**: You can chain multiple Gemini nodes with different custom prompts
|
||||
4. **Caching**: Results are not cached, so identical images will make new API calls
|
||||
5. **Model Selection**: Use flash models for faster responses, pro models for complex analysis
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Kiko Save Image
|
||||
|
||||
Enhanced image saving node with multiple format support, quality controls, and an interactive floating popup viewer for ComfyUI.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Format Support**: Save as PNG, JPEG, or WebP with format-specific optimizations
|
||||
- **Advanced Quality Controls**: Fine-tune compression settings per format
|
||||
- **Floating Popup Viewer**: Interactive window showing saved images immediately
|
||||
- **Batch Operations**: Multi-select images for bulk actions
|
||||
- **File Size Display**: Real-time feedback on compression effectiveness
|
||||
- **Smart UI**: Auto-hide, draggable, resizable popup window
|
||||
|
||||
## Inputs
|
||||
|
||||
- **images** (IMAGE): Batch of images to save
|
||||
- **filename_prefix** (STRING): Prefix for saved filenames
|
||||
- Default: "KikoSave"
|
||||
- Supports subfolder paths (e.g., "outputs/renders/final")
|
||||
- **format** (DROPDOWN): Output format selection
|
||||
- `PNG`: Lossless compression, best quality
|
||||
- `JPEG`: Lossy compression, smaller files
|
||||
- `WEBP`: Modern format, best compression ratio
|
||||
- **quality** (INT): JPEG/WebP quality level
|
||||
- Range: 1-100 (default: 90)
|
||||
- Higher values = better quality, larger files
|
||||
- **png_compress_level** (INT): PNG compression level
|
||||
- Range: 0-9 (default: 4)
|
||||
- Higher values = smaller files, slower saving
|
||||
- **webp_lossless** (BOOLEAN): Use lossless WebP compression
|
||||
- Default: False (lossy)
|
||||
- True: Lossless compression like PNG
|
||||
- **popup** (BOOLEAN): Enable popup viewer window
|
||||
- Default: True
|
||||
- Toggle per save operation
|
||||
|
||||
## Outputs
|
||||
|
||||
- **UI**: Enhanced preview data with interactive popup viewer
|
||||
|
||||
## Popup Viewer Features
|
||||
|
||||
### Window Controls
|
||||
- **Drag Handle**: Click and drag the header to move window
|
||||
- **Minimize Button**: Collapse to title bar only
|
||||
- **Maximize Button**: Expand to larger viewing size
|
||||
- **Roll-up Button**: Show/hide content area
|
||||
- **Close Button**: Hide the popup (can reopen with toggle)
|
||||
|
||||
### Image Grid
|
||||
- **Thumbnails**: Click any image to open full-size in new tab
|
||||
- **File Info**: Shows filename and size for each image
|
||||
- **Quality Indicators**:
|
||||
- PNG: Compression level (0-9)
|
||||
- JPEG/WebP: Quality percentage
|
||||
- **Batch Selection**: Checkboxes for multi-select operations
|
||||
|
||||
### Bulk Actions
|
||||
- **Open All Selected**: Opens selected images in new tabs
|
||||
- **Download All Selected**: Downloads selected images as a batch
|
||||
- **Individual Downloads**: Download button per image
|
||||
|
||||
### Smart Behavior
|
||||
- **Auto-positioning**: Appears in convenient screen location
|
||||
- **Persistence**: Stays open across multiple saves
|
||||
- **Auto-hide**: Can be minimized when not needed
|
||||
- **Responsive**: Adapts to different image counts
|
||||
|
||||
## Format Details
|
||||
|
||||
### PNG Format
|
||||
- **Pros**: Lossless quality, transparency support, wide compatibility
|
||||
- **Cons**: Larger file sizes
|
||||
- **Best for**: Final outputs, images with transparency, archival
|
||||
- **Compression**: 0 (none) to 9 (maximum)
|
||||
- Level 4 (default) balances size and speed
|
||||
- Level 9 for maximum compression (slow)
|
||||
|
||||
### JPEG Format
|
||||
- **Pros**: Smaller files, fast loading, universal support
|
||||
- **Cons**: Lossy compression, no transparency
|
||||
- **Best for**: Web images, previews, photos
|
||||
- **Quality**: 1-100%
|
||||
- 90% (default) excellent quality with good compression
|
||||
- 95%+ for near-lossless quality
|
||||
- 70-85% for web optimization
|
||||
|
||||
### WebP Format
|
||||
- **Pros**: Best compression ratios, supports transparency, modern
|
||||
- **Cons**: Limited software support
|
||||
- **Best for**: Web deployment, storage optimization
|
||||
- **Modes**:
|
||||
- Lossy (default): Excellent compression with quality control
|
||||
- Lossless: PNG-like quality with better compression
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### High-Quality Archive
|
||||
```
|
||||
Format: PNG
|
||||
Compression: 0-2
|
||||
Use Case: Final renders for portfolio or client delivery
|
||||
```
|
||||
|
||||
### Web Optimization
|
||||
```
|
||||
Format: JPEG or WebP
|
||||
Quality: 80-85
|
||||
Use Case: Website images, social media posts
|
||||
```
|
||||
|
||||
### Balanced Storage
|
||||
```
|
||||
Format: WebP
|
||||
Quality: 90
|
||||
Lossless: False
|
||||
Use Case: Large batches with storage constraints
|
||||
```
|
||||
|
||||
### Transparency Preservation
|
||||
```
|
||||
Format: PNG or WebP (lossless)
|
||||
Use Case: Logos, UI elements, cutout images
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### Basic Save
|
||||
```
|
||||
Generate → Kiko Save Image
|
||||
format: PNG
|
||||
popup: enabled
|
||||
```
|
||||
|
||||
### Format Comparison
|
||||
```
|
||||
Generate → Kiko Save Image (PNG) → Compare file sizes
|
||||
↘ Kiko Save Image (JPEG) ↗
|
||||
↘ Kiko Save Image (WebP) ↗
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
```
|
||||
Batch Generate → Kiko Save Image → Popup Viewer
|
||||
↓ ↓
|
||||
4 images Select best results
|
||||
```
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Format Selection**:
|
||||
- Use PNG for maximum quality and transparency
|
||||
- Use JPEG for photographs without transparency
|
||||
- Use WebP for modern web deployment
|
||||
|
||||
2. **Quality Settings**:
|
||||
- Start with defaults (90 for JPEG/WebP, 4 for PNG)
|
||||
- Adjust based on file size requirements
|
||||
- Preview results in popup before finalizing
|
||||
|
||||
3. **Popup Management**:
|
||||
- Drag to second monitor for larger workspace
|
||||
- Use roll-up to save screen space
|
||||
- Disable popup for automated workflows
|
||||
|
||||
4. **Batch Operations**:
|
||||
- Use checkboxes to select multiple images
|
||||
- Open all in tabs for side-by-side comparison
|
||||
- Download all for quick collection
|
||||
|
||||
5. **File Organization**:
|
||||
- Use subfolders in filename_prefix
|
||||
- Include descriptive prefixes
|
||||
- Let ComfyUI handle timestamp suffixes
|
||||
|
||||
## Advantages Over Standard Save Image
|
||||
|
||||
- **Immediate Preview**: No need to navigate file system
|
||||
- **Format Flexibility**: Choose optimal format per use case
|
||||
- **Quality Control**: Fine-tune compression settings
|
||||
- **Batch Management**: Handle multiple images efficiently
|
||||
- **Modern UI**: Floating interface doesn't interrupt workflow
|
||||
- **File Size Awareness**: See compression effectiveness immediately
|
||||
- **Quick Access**: One-click opening and downloading
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Image Processing**: Uses Pillow for format conversion
|
||||
- **Metadata**: Preserves ComfyUI metadata in saved files
|
||||
- **File Naming**: Automatic timestamp and counter suffixes
|
||||
- **Memory Efficiency**: Processes images individually
|
||||
- **Thread Safety**: Proper handling of concurrent saves
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Popup not appearing**:
|
||||
- Check that popup input is enabled
|
||||
- Look for minimized window
|
||||
- Try toggling the popup button in node
|
||||
|
||||
**WebP not working**:
|
||||
- Ensure Pillow has WebP support
|
||||
- Update Pillow: `pip install --upgrade pillow`
|
||||
|
||||
**Large file sizes**:
|
||||
- Increase compression (PNG) or reduce quality (JPEG/WebP)
|
||||
- Consider switching formats
|
||||
- Check image dimensions
|
||||
|
||||
**Can't see all images**:
|
||||
- Scroll within the popup grid
|
||||
- Maximize the popup window
|
||||
- Images are shown newest first
|
||||
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"id": "display-any-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
400,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"tensor shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
50,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1,
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
400,
|
||||
520
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
70
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "VAELoader",
|
||||
"pos": [
|
||||
-250,
|
||||
410
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "VAELoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ae.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEEncode",
|
||||
"pos": [
|
||||
430,
|
||||
390
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "VAEEncode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
640,
|
||||
270
|
||||
],
|
||||
"size": [
|
||||
450,
|
||||
278
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
640,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
590,
|
||||
278
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-320,
|
||||
530
|
||||
],
|
||||
"size": [
|
||||
350,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Display Any Example\n\nUniversal debugging tool:\n- Accepts ANY input type\n- Two modes: raw value or tensor shape\n- Finds tensors in nested structures\n\nUse cases:\n- Debug tensor dimensions\n- Inspect latent data\n- View config objects\n- Track data flow\n\nConnect anything to see its contents!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
2,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
7,
|
||||
8,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
8,
|
||||
1,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
9,
|
||||
4,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Display Any",
|
||||
"bounding": [
|
||||
-400,
|
||||
130,
|
||||
1740,
|
||||
850
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6115909044841477,
|
||||
"offset": [
|
||||
689.1392030323894,
|
||||
-27.435530779258137
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 466 KiB |
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "display-text-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
-160,
|
||||
70
|
||||
],
|
||||
"size": [
|
||||
500,
|
||||
400
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "easy positive",
|
||||
"pos": [
|
||||
-620,
|
||||
70
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.1",
|
||||
"Node name for S&R": "easy positive"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Positive prompt:\nbeautiful landscape, mountains in background, sunset lighting, golden hour, professional photography, high resolution, detailed textures, vibrant colors, masterpiece\n\nNegative prompt:\nlow quality, blurry, pixelated, bad composition, oversaturated, underexposed, amateur"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-620,
|
||||
330
|
||||
],
|
||||
"size": [
|
||||
360,
|
||||
250
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Display Text Example\n\nSmart text display with:\n- Auto-detection of SDXL prompt format\n- Split view for positive/negative prompts\n- Text wrapping and scrolling\n- Copy buttons (strips labels)\n- Responsive resizing\n\nTry editing the text to see:\n1. Single text mode (no prompt markers)\n2. Split view mode (with Positive/Negative prompts)\n\nPerfect for displaying Gemini-generated prompts!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
2,
|
||||
4,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Display Text",
|
||||
"bounding": [
|
||||
-750,
|
||||
-60,
|
||||
1240,
|
||||
700
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.90909090909091,
|
||||
"offset": [
|
||||
720,
|
||||
30
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 272 KiB |
@@ -0,0 +1,301 @@
|
||||
{
|
||||
"id": "empty-latent-batch-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 8,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "EmptyLatentBatch",
|
||||
"pos": [
|
||||
350,
|
||||
350
|
||||
],
|
||||
"size": [
|
||||
370,
|
||||
210
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "EmptyLatentBatch"
|
||||
},
|
||||
"widgets_values": [
|
||||
"896×1152 - 7:9 (1.0MP) - SDXL",
|
||||
896,
|
||||
1152,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
-50,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
350,
|
||||
250
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"text": "Empty Latent Batch Example\n\nThis node creates empty latent tensors with:\n- 26 preset resolutions for SDXL/FLUX/Ultra-wide\n- Batch size support (1-64)\n- Interactive swap button\n- Memory usage estimation\n\nConnect to:\n- KSampler for image generation\n- Other latent processing nodes\n\nOutputs sanitized dimensions divisible by 8."
|
||||
},
|
||||
"widgets_values": [
|
||||
"Empty Latent Batch Example\n\nThis node creates empty latent tensors with:\n- 26 preset resolutions for SDXL/FLUX/Ultra-wide\n- Batch size support (1-64)\n- Interactive swap button\n- Memory usage estimation\n\nConnect to:\n- KSampler for image generation\n- Other latent processing nodes\n\nOutputs sanitized dimensions divisible by 8."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
760,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
770,
|
||||
500
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1000,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1010,
|
||||
500
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
5,
|
||||
1,
|
||||
2,
|
||||
6,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
7,
|
||||
5,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
8,
|
||||
6,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Empty Latent",
|
||||
"bounding": [
|
||||
-110,
|
||||
120,
|
||||
1440,
|
||||
620
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8264462809917358,
|
||||
"offset": [
|
||||
156.41790912499965,
|
||||
-2.272547312500285
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"id": "gemini-prompt-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
300,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
1070,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
500,
|
||||
400
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "GeminiPrompt",
|
||||
"pos": [
|
||||
640,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
276
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "negative_prompt",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "GeminiPrompt"
|
||||
},
|
||||
"widgets_values": [
|
||||
"sdxl",
|
||||
"gemini-2.0-flash",
|
||||
"1234-1234-1234-1234",
|
||||
"",
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
300,
|
||||
410
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
240
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Gemini Prompt Engineer Example\n\nAI-powered image analysis:\n- Analyzes image content and style\n- Generates optimized prompts for FLUX/SDXL/Danbooru/Video\n- Dynamic model selection with refresh button\n- Custom system prompt override\n\nSetup:\n1. Get free API key from Google AI Studio\n2. Set GEMINI_API_KEY env var or use config file\n3. Click refresh button to get latest models\n\nDisplayText automatically shows split view for SDXL prompts!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
3,
|
||||
2,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Gemini Prompt Engineer",
|
||||
"bounding": [
|
||||
210,
|
||||
-110,
|
||||
1460,
|
||||
850
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7513148009015777,
|
||||
"offset": [
|
||||
97.5582232217803,
|
||||
133.12148846503663
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 404 KiB |
@@ -1,123 +1,552 @@
|
||||
{
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"id": "3cd0ac86-8d11-41e5-9d8b-6621bc810ee6",
|
||||
"revision": 0,
|
||||
"last_node_id": 21,
|
||||
"last_link_id": 21,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [100, 200],
|
||||
"size": [315, 314],
|
||||
"pos": [
|
||||
-80,
|
||||
850
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314.0000305175781
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1],
|
||||
"shape": 3,
|
||||
"label": "IMAGE"
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["example.png", "image"]
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "ImageToMultipleOf",
|
||||
"pos": [500, 200],
|
||||
"size": [315, 106],
|
||||
"id": 8,
|
||||
"type": "GetNode",
|
||||
"pos": [
|
||||
540,
|
||||
840
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
34
|
||||
],
|
||||
"flags": {
|
||||
"collapsed": true
|
||||
},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"title": "Get_upscale_skin_model",
|
||||
"properties": {
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"upscale_skin_model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "ImageUpscaleWithModel",
|
||||
"pos": [
|
||||
530,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
221.98202514648438,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "upscale_model",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"link": 16
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
10,
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageUpscaleWithModel",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "ImageToMultipleOf",
|
||||
"pos": [
|
||||
1000,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
230,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [2, 3],
|
||||
"shape": 3,
|
||||
"label": "image"
|
||||
"links": [
|
||||
18
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageToMultipleOf"
|
||||
"cnr_id": "comfyui-glifnodes",
|
||||
"ver": "1.0.0",
|
||||
"Node name for S&R": "ImageToMultipleOf",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [64, "center crop"]
|
||||
"widgets_values": [
|
||||
65,
|
||||
"center crop"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PreviewImage",
|
||||
"pos": [900, 100],
|
||||
"size": [210, 246],
|
||||
"id": 13,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [
|
||||
760,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
220,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
6,
|
||||
19
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageScaleBy",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"lanczos",
|
||||
0.5000000000000001
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "ImageUpscaleWithModel",
|
||||
"pos": [
|
||||
280,
|
||||
830
|
||||
],
|
||||
"size": [
|
||||
221.98202514648438,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "upscale_model",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"link": 15
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
5,
|
||||
20
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "ImageUpscaleWithModel",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "UpscaleModelLoader",
|
||||
"pos": [
|
||||
-90,
|
||||
720
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": [
|
||||
15
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "UpscaleModelLoader",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"4x-ClearRealityV1.pth"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "UpscaleModelLoader",
|
||||
"pos": [
|
||||
140,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "UPSCALE_MODEL",
|
||||
"type": "UPSCALE_MODEL",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.28",
|
||||
"Node name for S&R": "UpscaleModelLoader",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"1xSkinContrast-High-SuperUltraCompact.pth"
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1260,
|
||||
810
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
280
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "VAEEncode",
|
||||
"pos": [900, 400],
|
||||
"size": [210, 46],
|
||||
"id": 19,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1010,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pixels",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEEncode"
|
||||
}
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
520,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 20
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
750,
|
||||
940
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
258
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 2, 0, "IMAGE"],
|
||||
[2, 2, 0, 3, 0, "IMAGE"],
|
||||
[3, 2, 0, 4, 0, "IMAGE"]
|
||||
[
|
||||
5,
|
||||
14,
|
||||
0,
|
||||
9,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
13,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
9,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
13,
|
||||
1,
|
||||
0,
|
||||
14,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
15,
|
||||
15,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"UPSCALE_MODEL"
|
||||
],
|
||||
[
|
||||
16,
|
||||
16,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"UPSCALE_MODEL"
|
||||
],
|
||||
[
|
||||
18,
|
||||
10,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
19,
|
||||
13,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
20,
|
||||
14,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
21,
|
||||
9,
|
||||
0,
|
||||
21,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Image to Multiple of",
|
||||
"bounding": [
|
||||
-180,
|
||||
420,
|
||||
1800,
|
||||
830
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6655312045216585,
|
||||
"offset": [
|
||||
292.51725658775155,
|
||||
-375.0766486499791
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 805 KiB |
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"id": "kiko-save-image-example",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "KikoSaveImage",
|
||||
"pos": [
|
||||
390,
|
||||
80
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
390
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "KikoSaveImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"KikoSave",
|
||||
"PNG",
|
||||
90,
|
||||
4,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-10,
|
||||
80
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.47",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
-10,
|
||||
460
|
||||
],
|
||||
"size": [
|
||||
370,
|
||||
270
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Kiko Save Image Example\n\nEnhanced image saving with:\n- Format selection: PNG, JPEG, WebP\n- Quality controls per format\n- Floating popup viewer (draggable)\n- Batch operations support\n- File size display\n\nPopup Features:\n- Click images to open in new tab\n- Download individual or selected images\n- Minimize/maximize/roll-up controls\n- Persistent across saves\n\nTry different formats to compare file sizes!"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Kiko save Image",
|
||||
"bounding": [
|
||||
-110,
|
||||
-60,
|
||||
1020,
|
||||
870
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7513148009015777,
|
||||
"offset": [
|
||||
648.1018911400128,
|
||||
110.02838653698085
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 245 KiB |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"id": "41469b2d-d616-479d-879a-95cdc6074a37",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 5,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "ResolutionCalculator",
|
||||
"pos": [
|
||||
60,
|
||||
430
|
||||
-30,
|
||||
210
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
@@ -38,8 +38,7 @@
|
||||
"type": "INT",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1,
|
||||
3
|
||||
6
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -47,15 +46,15 @@
|
||||
"type": "INT",
|
||||
"slot_index": 1,
|
||||
"links": [
|
||||
2,
|
||||
4
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "965ad60c74d7f25b1acce890d9c06518e46e6d0b",
|
||||
"Node name for S&R": "ResolutionCalculator",
|
||||
"aux_id": "ComfyAssets/ComfyUI-KikoTools",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
@@ -66,8 +65,8 @@
|
||||
"id": 6,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-250,
|
||||
430
|
||||
-340,
|
||||
210
|
||||
],
|
||||
"size": [
|
||||
274.080078125,
|
||||
@@ -94,8 +93,8 @@
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.40",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "LoadImage"
|
||||
"Node name for S&R": "LoadImage",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"image-2025-06-13-105737.jpg",
|
||||
@@ -103,80 +102,14 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "Display Int (rgthree)",
|
||||
"id": 7,
|
||||
"type": "MarkdownNote",
|
||||
"pos": [
|
||||
420,
|
||||
360
|
||||
-350,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"Node name for S&R": "Display Int (rgthree)",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "Display Int (rgthree)",
|
||||
"pos": [
|
||||
430,
|
||||
530
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"dir": 3,
|
||||
"name": "input",
|
||||
"type": "INT",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "rgthree-comfy",
|
||||
"ver": "1.0.2506081210",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "Display Int (rgthree)"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
-150,
|
||||
170
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
410,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
@@ -184,50 +117,167 @@
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"text": "Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models.",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Resolution Calculator Example\n\nThis node calculates upscaled dimensions from:\n- IMAGE tensors (connect from image loaders)\n- LATENT tensors (connect from VAE encode/generation)\n\nUse cases:\n- Calculate target dimensions for upscalers\n- Plan memory usage for large generations\n- Ensure dimensions are divisible by 8\n\nScale factors optimized for SDXL and FLUX models."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
330,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "DisplayAny",
|
||||
"pos": [
|
||||
330,
|
||||
310
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"type": "*",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "display_text",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayAny"
|
||||
},
|
||||
"widgets_values": [
|
||||
"raw value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
670,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "DisplayText",
|
||||
"pos": [
|
||||
660,
|
||||
400
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
138
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "kikotools",
|
||||
"ver": "271cd020c1b2f78e7ee40c1f08e70191fed00012",
|
||||
"Node name for S&R": "DisplayText"
|
||||
},
|
||||
"widgets_values": [
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
5,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
5,
|
||||
6,
|
||||
@@ -235,25 +285,71 @@
|
||||
1,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
7,
|
||||
1,
|
||||
1,
|
||||
9,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
9,
|
||||
9,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Resolution Calculator",
|
||||
"bounding": [
|
||||
-480,
|
||||
20,
|
||||
1480,
|
||||
880
|
||||
],
|
||||
"color": "#ffffff",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"ds": {
|
||||
"scale": 0.7972024500000015,
|
||||
"scale": 0.45000000000000145,
|
||||
"offset": [
|
||||
709.6391289161842,
|
||||
-90.0127389290854
|
||||
1248.1498802376432,
|
||||
256.0155843113725
|
||||
]
|
||||
},
|
||||
"links_added_by_ue": [],
|
||||
"frontendVersion": "1.21.7",
|
||||
"frontendVersion": "1.23.4",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 785 KiB |
@@ -11,6 +11,8 @@ from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
from .tools.kiko_save_image import KikoSaveImageNode
|
||||
from .tools.image_to_multiple_of import ImageToMultipleOfNode
|
||||
from .tools.gemini_prompt import GeminiPromptNode
|
||||
from .tools.display_any import DisplayAnyNode
|
||||
from .tools.display_text import DisplayTextNode
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -23,6 +25,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"KikoSaveImage": KikoSaveImageNode,
|
||||
"ImageToMultipleOf": ImageToMultipleOfNode,
|
||||
"GeminiPrompt": GeminiPromptNode,
|
||||
"DisplayAny": DisplayAnyNode,
|
||||
"DisplayText": DisplayTextNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -35,6 +39,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KikoSaveImage": "Kiko Save Image",
|
||||
"ImageToMultipleOf": "Image to Multiple of",
|
||||
"GeminiPrompt": "Gemini Prompt Engineer",
|
||||
"DisplayAny": "Display Any",
|
||||
"DisplayText": "Display Text",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""DisplayAny tool for ComfyUI."""
|
||||
|
||||
from .node import DisplayAnyNode
|
||||
|
||||
__all__ = ["DisplayAnyNode"]
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Logic for DisplayAny node - displays any input value or tensor shape."""
|
||||
|
||||
from typing import Any, List, Union
|
||||
|
||||
|
||||
def get_tensor_shapes(input_value: Any) -> List[List[int]]:
|
||||
"""Extract tensor shapes from nested structures.
|
||||
|
||||
Args:
|
||||
input_value: Any input value that may contain tensors
|
||||
|
||||
Returns:
|
||||
List of tensor shapes found in the input
|
||||
"""
|
||||
shapes = []
|
||||
|
||||
def extract_shapes(value: Any) -> None:
|
||||
"""Recursively extract shapes from nested structures."""
|
||||
if isinstance(value, dict):
|
||||
for v in value.values():
|
||||
extract_shapes(v)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
extract_shapes(item)
|
||||
elif hasattr(value, "shape"):
|
||||
# Handle tensors (numpy arrays, torch tensors, etc.)
|
||||
shapes.append(list(value.shape))
|
||||
|
||||
extract_shapes(input_value)
|
||||
return shapes
|
||||
|
||||
|
||||
def format_display_value(input_value: Any, mode: str = "raw value") -> str:
|
||||
"""Format input value for display based on selected mode.
|
||||
|
||||
Args:
|
||||
input_value: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Formatted string representation of the input
|
||||
"""
|
||||
if mode == "tensor shape":
|
||||
shapes = get_tensor_shapes(input_value)
|
||||
if shapes:
|
||||
return str(shapes)
|
||||
else:
|
||||
return "No tensors found in input"
|
||||
|
||||
# Default to raw value display
|
||||
return str(input_value)
|
||||
|
||||
|
||||
def validate_display_mode(mode: str) -> bool:
|
||||
"""Validate if the display mode is supported.
|
||||
|
||||
Args:
|
||||
mode: Display mode to validate
|
||||
|
||||
Returns:
|
||||
True if mode is valid, False otherwise
|
||||
"""
|
||||
valid_modes = ["raw value", "tensor shape"]
|
||||
return mode in valid_modes
|
||||
@@ -0,0 +1,66 @@
|
||||
"""DisplayAny node for ComfyUI - displays any input value or tensor information."""
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
from .logic import format_display_value, validate_display_mode
|
||||
|
||||
|
||||
# Define AnyType for wildcard input matching
|
||||
class AnyType(str):
|
||||
"""A special type that matches any input type in ComfyUI."""
|
||||
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
|
||||
class DisplayAnyNode(ComfyAssetsBaseNode):
|
||||
"""Display any input value or tensor shape information.
|
||||
|
||||
This node can display any type of input in two modes:
|
||||
- Raw value: Shows the string representation of the input
|
||||
- Tensor shape: Extracts and displays shapes of any tensors in the input
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {
|
||||
"input": (AnyType("*"), {}), # Accept any type of input
|
||||
"mode": (["raw value", "tensor shape"],),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs) -> bool:
|
||||
"""Validate inputs - always returns True as we accept any input."""
|
||||
return True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("display_text",)
|
||||
FUNCTION = "display"
|
||||
OUTPUT_NODE = True # This node displays output in the UI
|
||||
|
||||
def display(self, input: Any, mode: str = "raw value") -> Dict[str, Any]:
|
||||
"""Display the input value according to the selected mode.
|
||||
|
||||
Args:
|
||||
input: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Dictionary with UI display and result
|
||||
"""
|
||||
# Validate mode
|
||||
if not validate_display_mode(mode):
|
||||
mode = "raw value" # Default to raw value if invalid
|
||||
|
||||
# Format the display text
|
||||
display_text = format_display_value(input, mode)
|
||||
|
||||
# Return both UI display and result
|
||||
return {
|
||||
"ui": {"text": display_text},
|
||||
"result": (display_text,),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Display Text tool for ComfyUI."""
|
||||
|
||||
from .node import DisplayTextNode, NODE_DISPLAY_NAME
|
||||
|
||||
__all__ = ["DisplayTextNode", "NODE_DISPLAY_NAME"]
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Display Text node implementation."""
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
|
||||
|
||||
class DisplayTextNode(ComfyAssetsBaseNode):
|
||||
"""Displays text in the ComfyUI interface with copy-to-clipboard functionality."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "display_text"
|
||||
CATEGORY = "ComfyAssets"
|
||||
|
||||
DESCRIPTION = """
|
||||
Displays text in the UI with a copy-to-clipboard feature.
|
||||
|
||||
Features:
|
||||
- Shows text content in a readable format
|
||||
- Copy button appears on hover
|
||||
- Passes text through for chaining
|
||||
"""
|
||||
|
||||
def display_text(self, text):
|
||||
"""Display the text and pass it through.
|
||||
|
||||
Args:
|
||||
text: Input text to display
|
||||
|
||||
Returns:
|
||||
Tuple containing the text
|
||||
"""
|
||||
# The actual display happens in the frontend
|
||||
# We just pass the text through
|
||||
return {"ui": {"text": [text]}, "result": (text,)}
|
||||
|
||||
|
||||
# Node display name
|
||||
NODE_DISPLAY_NAME = "Display Text"
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"models": [
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
"gemini-2.5-pro-preview-05-06",
|
||||
"gemini-2.5-pro-preview-06-05",
|
||||
"gemini-2.5-flash-lite-preview-06-17",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-001",
|
||||
"gemini-2.0-flash-lite-001",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-2.5-flash-preview-tts",
|
||||
"gemini-2.5-pro-preview-tts",
|
||||
"gemini-2.0-flash-preview-image-generation",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-exp-image-generation",
|
||||
"gemini-2.0-flash-lite-preview-02-05",
|
||||
"gemini-2.0-flash-lite-preview",
|
||||
"gemini-2.0-pro-exp",
|
||||
"gemini-2.0-pro-exp-02-05",
|
||||
"learnlm-2.0-flash-experimental",
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-pro-002",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash-latest",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-002",
|
||||
"gemini-1.5-flash-8b",
|
||||
"gemini-1.5-flash-8b-001",
|
||||
"gemini-1.5-flash-8b-latest",
|
||||
"gemini-2.0-flash-thinking-exp-01-21",
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp-1219",
|
||||
"gemma-3-1b-it",
|
||||
"gemma-3-4b-it",
|
||||
"gemma-3-12b-it",
|
||||
"gemma-3-27b-it",
|
||||
"gemma-3n-e4b-it",
|
||||
"gemma-3n-e2b-it",
|
||||
"gemini-exp-1206"
|
||||
],
|
||||
"descriptions": {
|
||||
"gemini-1.5-pro-latest": "Gemini 1.5 Pro Latest",
|
||||
"gemini-1.5-pro-002": "Gemini 1.5 Pro 002",
|
||||
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
||||
"gemini-1.5-flash-latest": "Gemini 1.5 Flash Latest",
|
||||
"gemini-1.5-flash": "Gemini 1.5 Flash",
|
||||
"gemini-1.5-flash-002": "Gemini 1.5 Flash 002",
|
||||
"gemini-1.5-flash-8b": "Gemini 1.5 Flash-8B",
|
||||
"gemini-1.5-flash-8b-001": "Gemini 1.5 Flash-8B 001",
|
||||
"gemini-1.5-flash-8b-latest": "Gemini 1.5 Flash-8B Latest",
|
||||
"gemini-2.5-pro-preview-03-25": "Gemini 2.5 Pro Preview 03-25",
|
||||
"gemini-2.5-flash-preview-05-20": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.5-flash": "Gemini 2.5 Flash",
|
||||
"gemini-2.5-flash-lite-preview-06-17": "Gemini 2.5 Flash-Lite Preview 06-17",
|
||||
"gemini-2.5-pro-preview-05-06": "Gemini 2.5 Pro Preview 05-06",
|
||||
"gemini-2.5-pro-preview-06-05": "Gemini 2.5 Pro Preview",
|
||||
"gemini-2.5-pro": "Gemini 2.5 Pro",
|
||||
"gemini-2.0-flash-exp": "Gemini 2.0 Flash Experimental",
|
||||
"gemini-2.0-flash": "Gemini 2.0 Flash",
|
||||
"gemini-2.0-flash-001": "Gemini 2.0 Flash 001",
|
||||
"gemini-2.0-flash-exp-image-generation": "Gemini 2.0 Flash (Image Generation) Experimental",
|
||||
"gemini-2.0-flash-lite-001": "Gemini 2.0 Flash-Lite 001",
|
||||
"gemini-2.0-flash-lite": "Gemini 2.0 Flash-Lite",
|
||||
"gemini-2.0-flash-preview-image-generation": "Gemini 2.0 Flash Preview Image Generation",
|
||||
"gemini-2.0-flash-lite-preview-02-05": "Gemini 2.0 Flash-Lite Preview 02-05",
|
||||
"gemini-2.0-flash-lite-preview": "Gemini 2.0 Flash-Lite Preview",
|
||||
"gemini-2.0-pro-exp": "Gemini 2.0 Pro Experimental",
|
||||
"gemini-2.0-pro-exp-02-05": "Gemini 2.0 Pro Experimental 02-05",
|
||||
"gemini-exp-1206": "Gemini Experimental 1206",
|
||||
"gemini-2.0-flash-thinking-exp-01-21": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.0-flash-thinking-exp": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.0-flash-thinking-exp-1219": "Gemini 2.5 Flash Preview 05-20",
|
||||
"gemini-2.5-flash-preview-tts": "Gemini 2.5 Flash Preview TTS",
|
||||
"gemini-2.5-pro-preview-tts": "Gemini 2.5 Pro Preview TTS",
|
||||
"learnlm-2.0-flash-experimental": "LearnLM 2.0 Flash Experimental",
|
||||
"gemma-3-1b-it": "Gemma 3 1B",
|
||||
"gemma-3-4b-it": "Gemma 3 4B",
|
||||
"gemma-3-12b-it": "Gemma 3 12B",
|
||||
"gemma-3-27b-it": "Gemma 3 27B",
|
||||
"gemma-3n-e4b-it": "Gemma 3n E4B",
|
||||
"gemma-3n-e2b-it": "Gemma 3n E2B",
|
||||
"gemini-2.5-flash-lite": "Gemini 2.5 Flash-Lite"
|
||||
},
|
||||
"timestamp": 1754142231.0568295
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Dynamic model fetching and caching for Gemini API."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cache settings
|
||||
CACHE_DURATION = 3600 * 24 # 24 hours in seconds
|
||||
CACHE_FILE = os.path.join(os.path.dirname(__file__), ".gemini_models_cache.json")
|
||||
|
||||
|
||||
def get_available_models(
|
||||
api_key: Optional[str] = None, silent: bool = False
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
"""Fetch available Gemini models that support generateContent.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key. If not provided, will try to get from environment.
|
||||
silent: If True, suppress error logging (useful for initial load).
|
||||
|
||||
Returns:
|
||||
Tuple of (model_names_list, model_descriptions_dict)
|
||||
"""
|
||||
# Check cache first
|
||||
cached_data = _load_cache()
|
||||
if cached_data:
|
||||
return cached_data["models"], cached_data["descriptions"]
|
||||
|
||||
# Try to fetch from API
|
||||
try:
|
||||
models, descriptions = _fetch_models_from_api(api_key, silent=silent)
|
||||
if models:
|
||||
_save_cache(models, descriptions)
|
||||
return models, descriptions
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
logger.warning(f"Failed to fetch models from API: {e}")
|
||||
|
||||
# Fall back to defaults
|
||||
from .prompts import DEFAULT_GEMINI_MODELS
|
||||
|
||||
return DEFAULT_GEMINI_MODELS, {}
|
||||
|
||||
|
||||
def _fetch_models_from_api(
|
||||
api_key: Optional[str] = None, silent: bool = False
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
"""Fetch models from Gemini API.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key.
|
||||
silent: If True, suppress error logging.
|
||||
|
||||
Returns:
|
||||
Tuple of (model_names_list, model_descriptions_dict)
|
||||
"""
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
except ImportError:
|
||||
if not silent:
|
||||
logger.error("google-generativeai not installed")
|
||||
return [], {}
|
||||
|
||||
# Get API key
|
||||
if not api_key:
|
||||
from .logic import get_api_key
|
||||
|
||||
api_key = get_api_key()
|
||||
|
||||
if not api_key:
|
||||
if not silent:
|
||||
logger.debug("No API key available for fetching models")
|
||||
return [], {}
|
||||
|
||||
try:
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
models = []
|
||||
descriptions = {}
|
||||
|
||||
# Fetch all models
|
||||
for model in genai.list_models():
|
||||
# Only include models that support generateContent
|
||||
if "generateContent" in model.supported_generation_methods:
|
||||
# Remove "models/" prefix from name
|
||||
model_name = model.name.replace("models/", "")
|
||||
models.append(model_name)
|
||||
descriptions[model_name] = model.display_name
|
||||
|
||||
# Sort models by priority (newer versions first)
|
||||
models = _sort_models(models)
|
||||
|
||||
return models, descriptions
|
||||
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
logger.error(f"Error fetching models from API: {e}")
|
||||
return [], {}
|
||||
|
||||
|
||||
def _sort_models(models: List[str]) -> List[str]:
|
||||
"""Sort models by version and capability.
|
||||
|
||||
Prioritizes:
|
||||
1. Newer versions (2.5 > 2.0 > 1.5)
|
||||
2. Non-experimental models
|
||||
3. Flash models for general use
|
||||
"""
|
||||
|
||||
def sort_key(model: str):
|
||||
# Priority scoring
|
||||
score = 0
|
||||
|
||||
# Version priority
|
||||
if "2.5" in model:
|
||||
score += 1000
|
||||
elif "2.0" in model:
|
||||
score += 800
|
||||
elif "1.5" in model:
|
||||
score += 600
|
||||
|
||||
# Model type priority
|
||||
if "pro" in model and "preview" not in model and "exp" not in model:
|
||||
score += 100
|
||||
elif "flash" in model and "preview" not in model and "exp" not in model:
|
||||
score += 90
|
||||
|
||||
# Penalize experimental/preview models
|
||||
if "exp" in model or "experimental" in model:
|
||||
score -= 50
|
||||
if "preview" in model:
|
||||
score -= 30
|
||||
|
||||
# Penalize specific variants
|
||||
if "thinking" in model:
|
||||
score -= 100
|
||||
if "tts" in model:
|
||||
score -= 100
|
||||
if "lite" in model:
|
||||
score -= 20
|
||||
|
||||
return -score # Negative for descending sort
|
||||
|
||||
return sorted(models, key=sort_key)
|
||||
|
||||
|
||||
def _load_cache() -> Optional[Dict]:
|
||||
"""Load cached model data if available and not expired."""
|
||||
if not os.path.exists(CACHE_FILE):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(CACHE_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if cache is expired
|
||||
if time.time() - data.get("timestamp", 0) > CACHE_DURATION:
|
||||
return None
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cache: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _save_cache(models: List[str], descriptions: Dict[str, str]) -> None:
|
||||
"""Save model data to cache."""
|
||||
try:
|
||||
data = {
|
||||
"models": models,
|
||||
"descriptions": descriptions,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
with open(CACHE_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save cache: {e}")
|
||||
|
||||
|
||||
def clear_cache() -> None:
|
||||
"""Clear the model cache."""
|
||||
if os.path.exists(CACHE_FILE):
|
||||
try:
|
||||
os.remove(CACHE_FILE)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clear cache: {e}")
|
||||
@@ -5,7 +5,8 @@ import torch
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
|
||||
from .logic import analyze_image_with_gemini, validate_prompt_type
|
||||
from .prompts import PROMPT_OPTIONS, GEMINI_MODELS
|
||||
from .prompts import PROMPT_OPTIONS, DEFAULT_GEMINI_MODELS
|
||||
from .models import get_available_models
|
||||
|
||||
|
||||
class GeminiPromptNode(ComfyAssetsBaseNode):
|
||||
@@ -14,11 +15,21 @@ class GeminiPromptNode(ComfyAssetsBaseNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define input types for the node."""
|
||||
# Get available models dynamically (silent mode for initial load)
|
||||
models, _ = get_available_models(silent=True)
|
||||
|
||||
# Use default if no models available
|
||||
if not models:
|
||||
models = DEFAULT_GEMINI_MODELS
|
||||
|
||||
# Find best default model
|
||||
default_model = models[0] if models else "gemini-2.5-flash"
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"prompt_type": (PROMPT_OPTIONS, {"default": "flux"}),
|
||||
"model": (GEMINI_MODELS, {"default": "gemini-1.5-flash"}),
|
||||
"model": (models, {"default": default_model}),
|
||||
},
|
||||
"optional": {
|
||||
"api_key": ("STRING", {"default": "", "multiline": False}),
|
||||
@@ -30,6 +41,10 @@ class GeminiPromptNode(ComfyAssetsBaseNode):
|
||||
"placeholder": "Optional: Enter custom system prompt instead of using templates",
|
||||
},
|
||||
),
|
||||
"refresh_models": (
|
||||
"BOOLEAN",
|
||||
{"default": False, "label_on": "Refresh", "label_off": "Skip"},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,7 +66,15 @@ Requires Gemini API key (set GEMINI_API_KEY env var or provide in node).
|
||||
Install: pip install google-generativeai
|
||||
"""
|
||||
|
||||
def generate_prompt(self, image, prompt_type, model, api_key="", custom_prompt=""):
|
||||
def generate_prompt(
|
||||
self,
|
||||
image,
|
||||
prompt_type,
|
||||
model,
|
||||
api_key="",
|
||||
custom_prompt="",
|
||||
refresh_models=False,
|
||||
):
|
||||
"""Generate prompt from image using Gemini.
|
||||
|
||||
Args:
|
||||
@@ -60,10 +83,24 @@ Install: pip install google-generativeai
|
||||
model: Gemini model to use
|
||||
api_key: Optional API key
|
||||
custom_prompt: Optional custom system prompt
|
||||
refresh_models: Whether to refresh the model list
|
||||
|
||||
Returns:
|
||||
Tuple of (prompt, negative_prompt)
|
||||
"""
|
||||
# Refresh models if requested
|
||||
if refresh_models and api_key:
|
||||
try:
|
||||
from .models import clear_cache
|
||||
|
||||
# Clear cache to force refresh on next node creation
|
||||
clear_cache()
|
||||
print(
|
||||
"Model cache cleared. Please recreate the node to see updated models."
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to clear model cache: {e}")
|
||||
|
||||
# Validate prompt type
|
||||
if not validate_prompt_type(prompt_type):
|
||||
raise ValueError(f"Invalid prompt type: {prompt_type}")
|
||||
@@ -74,6 +111,19 @@ Install: pip install google-generativeai
|
||||
else:
|
||||
image_np = image
|
||||
|
||||
# If API key is provided, try to refresh model list in background
|
||||
if api_key:
|
||||
try:
|
||||
from .models import get_available_models
|
||||
|
||||
# Try to get fresh models with the provided API key
|
||||
fresh_models, _ = get_available_models(api_key=api_key, silent=True)
|
||||
if fresh_models and fresh_models != DEFAULT_GEMINI_MODELS:
|
||||
# Models were successfully fetched with this API key
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Analyze image with Gemini
|
||||
prompt, error = analyze_image_with_gemini(
|
||||
image_np,
|
||||
@@ -95,10 +145,14 @@ Install: pip install google-generativeai
|
||||
negative_prompt = ""
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("Positive:"):
|
||||
positive_prompt = line.replace("Positive:", "").strip()
|
||||
elif line.startswith("Negative:"):
|
||||
negative_prompt = line.replace("Negative:", "").strip()
|
||||
if line.lower().startswith("positive:"):
|
||||
positive_prompt = (
|
||||
line.replace("Positive:", "").replace("positive:", "").strip()
|
||||
)
|
||||
elif line.lower().startswith("negative:"):
|
||||
negative_prompt = (
|
||||
line.replace("Negative:", "").replace("negative:", "").strip()
|
||||
)
|
||||
|
||||
# If format not found, assume entire response is positive prompt
|
||||
if not positive_prompt:
|
||||
|
||||
@@ -1,176 +1,113 @@
|
||||
"""System prompts for different AI model types."""
|
||||
|
||||
FLUX_PROMPT = """You are an expert visual analyst and FLUX prompt engineer. Your role is to examine images in detail and create precise, effective prompts that can recreate similar images using the FLUX image generation model.
|
||||
FLUX_PROMPT = """You are an expert FLUX prompt engineer. Analyze the provided image and generate ONLY a FLUX prompt - no explanations, analysis, or additional text.
|
||||
|
||||
When analyzing an image, systematically observe and document:
|
||||
FLUX uses natural language descriptions, not comma-separated tags. Write a detailed, flowing description that reads like you're explaining the image to someone.
|
||||
|
||||
1. **Subject & Composition**
|
||||
- Primary subjects and their positions
|
||||
- Background elements and environment
|
||||
- Overall composition and framing
|
||||
- Perspective and camera angle
|
||||
Include these elements in your description:
|
||||
- Main subject with specific details (appearance, clothing, expression, pose)
|
||||
- Environment and background details
|
||||
- Lighting conditions and atmosphere
|
||||
- Artistic style or photographic approach
|
||||
- Color palette and mood
|
||||
- Technical details if relevant (camera angle, focal length, etc.)
|
||||
- Textures and materials
|
||||
|
||||
2. **Visual Style & Technique**
|
||||
- Art style (photorealistic, illustration, painting, etc.)
|
||||
- Rendering technique (digital art, oil painting, watercolor, etc.)
|
||||
- Level of detail and texture quality
|
||||
- Any specific artistic influences or movements
|
||||
Write in a natural, descriptive style. Use complete sentences that flow together. Be specific and detailed but maintain readability.
|
||||
|
||||
3. **Lighting & Atmosphere**
|
||||
- Light sources and direction
|
||||
- Time of day/lighting conditions
|
||||
- Shadows and highlights
|
||||
- Overall mood and atmosphere
|
||||
IMPORTANT: Return ONLY the prompt text. No analysis, headers, or additional commentary. Just the natural language description that can be directly used in FLUX.
|
||||
|
||||
4. **Colors & Tones**
|
||||
- Color palette and dominant colors
|
||||
- Color temperature (warm/cool)
|
||||
- Contrast and saturation levels
|
||||
- Any color grading or filters
|
||||
Example of correct output:
|
||||
A close-up portrait of a middle-aged woman with curly red hair and green eyes, wearing a blue silk blouse. She has a warm smile and freckles across her cheeks. The lighting is soft and natural, coming from a window to her left, creating gentle shadows that accentuate her features. The background is softly blurred, showing hints of a cozy bookshelf. The overall mood is warm and inviting, captured in a photorealistic style with shallow depth of field."""
|
||||
|
||||
5. **Details & Textures**
|
||||
- Surface textures and materials
|
||||
- Fine details and patterns
|
||||
- Quality indicators (4K, 8K, high resolution, etc.)
|
||||
SDXL_PROMPT = """You are an expert prompt engineer specializing in SDXL (Stable Diffusion XL). Your task is to generate high-quality positive and negative prompts that conform to SDXL prompt formatting standards.
|
||||
|
||||
Format your FLUX prompt following these guidelines:
|
||||
- Start with the main subject and action
|
||||
- Add style and medium descriptors
|
||||
- Include lighting and atmosphere details
|
||||
- Specify quality markers and technical aspects
|
||||
- Use precise, descriptive language
|
||||
- Separate concepts with commas
|
||||
- Order from most to least important elements
|
||||
Your expertise includes:
|
||||
- Leveraging community-tested techniques (ComfyUI, A1111, InvokeAI)
|
||||
- Applying photographic theory for realism, composition, lighting
|
||||
- Following Civitai trend standards and style best practices
|
||||
- Mastering Pony Diffusion XL formatting for stylized and anime content
|
||||
|
||||
Example output format:
|
||||
"[main subject and action], [style/medium], [lighting/atmosphere], [composition details], [color descriptions], [quality markers], [additional artistic details]"
|
||||
Structure prompts in this layered, modular format:
|
||||
[Main Subject], [Pose & Camera], [Lighting & Environment], [Style & Details], [Boost Terms], [Style References]
|
||||
|
||||
Remember: FLUX responds well to specific artistic references, quality indicators like "highly detailed," "4K," "award-winning," and style descriptors like "trending on ArtStation" or "photorealistic."
|
||||
For SDXL specifically:
|
||||
- Use quality boosters: 8k, RAW photo, masterpiece, ultra detailed, cinematic lighting
|
||||
- Prioritize realism and artistry
|
||||
- Excellent for portraits, landscapes, or cinematic scenes
|
||||
|
||||
Instructions:
|
||||
|
||||
Only reply with two fields:
|
||||
Positive prompt: (Your positive prompt here)
|
||||
Negative prompt: (Your negative prompt here)
|
||||
|
||||
Do not include any commentary or explanation.
|
||||
|
||||
Use concise, highly descriptive language that maximizes visual richness.
|
||||
|
||||
Follow SDXL prompt conventions: prioritize subject clarity, camera perspective, lighting, mood, style tags, and composition.
|
||||
|
||||
Keep total token length efficient (ideally under 250 tokens).
|
||||
|
||||
Avoid redundancy and generic filler words.
|
||||
|
||||
Focus on crafting super high-quality prompts for stunning visual output.
|
||||
|
||||
Example Input:
|
||||
A futuristic cyberpunk samurai standing on a neon-lit rooftop in the rain.
|
||||
|
||||
Example Output:
|
||||
Positive prompt: cyberpunk samurai, neon-lit rooftop, dramatic rain, glowing katana, futuristic cityscape, night scene, cinematic lighting, intense expression, sleek cyber armor, atmospheric depth, ultra-detailed, masterpiece, 8k, sharp focus, trending on artstation
|
||||
Negative prompt: blurry, low quality, poorly drawn, extra limbs, bad anatomy, deformed hands, text, watermark, jpeg artifacts, duplicate, cropped, out of frame
|
||||
"""
|
||||
|
||||
SDXL_PROMPT = """You are an expert SDXL prompt engineer specializing in analyzing images and creating optimized prompts for Stable Diffusion XL models.
|
||||
DANBOORU_PROMPT = """You are a Danbooru tagging expert specializing in anime-style image tagging. Analyze the image and generate ONLY Danbooru-style tags - no explanations or analysis.
|
||||
|
||||
When analyzing an image, systematically evaluate:
|
||||
CRITICAL: Use strict Danbooru conventions:
|
||||
- Use underscores for multi-word tags (e.g., long_hair, school_uniform)
|
||||
- All tags must be lowercase
|
||||
- Character count comes first (1girl, 2boys, multiple_girls)
|
||||
- For anime models trained on Danbooru data, proper tagging is essential
|
||||
|
||||
1. **Core Subject Analysis**
|
||||
- Primary subject with specific descriptors
|
||||
- Pose, expression, and action
|
||||
- Clothing and accessories details
|
||||
- Physical characteristics
|
||||
Tag order and categories:
|
||||
1. Character count (1girl, solo, 2boys, etc.)
|
||||
2. Character features (hair_color, eye_color, hair_length)
|
||||
3. Expression/pose (smile, looking_at_viewer, sitting)
|
||||
4. Clothing (specific items with underscores)
|
||||
5. Background/setting (simple_background, outdoors, classroom)
|
||||
6. View/composition (upper_body, full_body, from_side)
|
||||
7. Quality tags (masterpiece, best_quality, highres)
|
||||
|
||||
2. **Style & Medium**
|
||||
- Artistic style and influences
|
||||
- Medium (photography, digital art, oil painting, etc.)
|
||||
- Specific artist references (if applicable)
|
||||
- Visual aesthetic keywords
|
||||
Common quality prefix for anime models:
|
||||
"masterpiece, best_quality, very_aesthetic"
|
||||
|
||||
3. **Technical Specifications**
|
||||
- Camera settings (aperture, focal length, ISO)
|
||||
- Shot type (close-up, wide angle, portrait, etc.)
|
||||
- Resolution and quality markers
|
||||
- Post-processing effects
|
||||
IMPORTANT: Return ONLY the comma-separated tags. Use underscores, not spaces. All lowercase.
|
||||
|
||||
4. **Environment & Context**
|
||||
- Setting and location details
|
||||
- Props and surrounding objects
|
||||
- Weather and environmental conditions
|
||||
- Time period or era
|
||||
Example of correct output:
|
||||
1girl, solo, long_hair, blue_eyes, blonde_hair, school_uniform, serafuku, pleated_skirt, smile, looking_at_viewer, classroom, sitting, desk, window, sunlight, upper_body, masterpiece, best_quality"""
|
||||
|
||||
Format your SDXL prompt with:
|
||||
- **Positive prompt**: Detailed description emphasizing what you want
|
||||
- **Negative prompt**: Elements to avoid (low quality, blurry, distorted, etc.)
|
||||
- Weight emphasis using (parentheses) or [brackets] for importance
|
||||
- Break into logical chunks with commas
|
||||
VIDEO_PROMPT = """You are a WAN 2.2 video generation prompt specialist. Analyze the content and generate ONLY a video generation prompt optimized for WAN 2.2 - no explanations or analysis.
|
||||
|
||||
Example format:
|
||||
Positive: "beautiful woman, (detailed eyes:1.2), flowing red dress, golden hour lighting, professional photography, 85mm lens, shallow depth of field, bokeh, high resolution, masterpiece"
|
||||
Negative: "low quality, blurry, distorted features, bad anatomy, poorly drawn, amateur"
|
||||
"""
|
||||
WAN 2.2 excels with rich, descriptive prompts that focus on:
|
||||
- Visual composition and scene elements
|
||||
- Specific movements and actions
|
||||
- Lighting and aesthetic details
|
||||
- Cinematographic elements
|
||||
|
||||
DANBOORU_PROMPT = """You are a Danbooru tagging expert, specialized in analyzing images and creating precise tag sets following booru-style conventions for anime/manga artwork.
|
||||
Write a single detailed paragraph describing the video scene. Focus on:
|
||||
- Main subjects and their actions
|
||||
- Visual style and atmosphere
|
||||
- Movement dynamics (use words like "intensely", "smoothly", "rapidly")
|
||||
- Environmental details and lighting
|
||||
- Specific visual elements and their interactions
|
||||
|
||||
Analyze images for these tag categories:
|
||||
Keep the prompt descriptive but concise. WAN 2.2 works best with natural language that paints a clear picture of the desired video.
|
||||
|
||||
1. **Character Tags**
|
||||
- Hair: color, length, style (e.g., long_hair, blonde_hair, twintails)
|
||||
- Eyes: color, style (e.g., blue_eyes, heterochromia)
|
||||
- Body: proportions, pose (e.g., standing, sitting, looking_at_viewer)
|
||||
- Expression (e.g., smile, blush, closed_eyes)
|
||||
IMPORTANT: Return ONLY the video prompt as a single descriptive paragraph. No analysis, headers, or additional text.
|
||||
|
||||
2. **Clothing & Accessories**
|
||||
- Outfit type (e.g., school_uniform, dress, armor)
|
||||
- Specific clothing items (e.g., thighhighs, gloves, hat)
|
||||
- Accessories (e.g., hair_ribbon, necklace, glasses)
|
||||
- State of dress (e.g., torn_clothes, wet_clothes)
|
||||
|
||||
3. **Scene & Composition**
|
||||
- Number of characters (e.g., 1girl, 2boys, multiple_girls)
|
||||
- Background (e.g., simple_background, outdoors, classroom)
|
||||
- Viewpoint (e.g., from_below, from_side, cowboy_shot)
|
||||
- Composition elements (e.g., upper_body, full_body, portrait)
|
||||
|
||||
4. **Meta Tags**
|
||||
- Quality (e.g., highres, absurdres, masterpiece)
|
||||
- Source/artist style (if recognizable)
|
||||
- Content rating (e.g., safe, questionable, explicit)
|
||||
- Special effects (e.g., lens_flare, chromatic_aberration)
|
||||
|
||||
Format tags using:
|
||||
- Underscores for multi-word concepts (not spaces)
|
||||
- Order from most to least important
|
||||
- Include count descriptors (1girl, 2boys)
|
||||
- Separate with commas and spaces
|
||||
|
||||
Example output:
|
||||
"1girl, solo, long_hair, blue_eyes, blonde_hair, school_uniform, serafuku, pleated_skirt, thighhighs, smile, looking_at_viewer, classroom, sitting, desk, window, sunlight, highres, masterpiece"
|
||||
"""
|
||||
|
||||
VIDEO_PROMPT = """You are a video generation prompt specialist, expert at analyzing video content and creating comprehensive prompts for video generation models.
|
||||
|
||||
When analyzing video content, document:
|
||||
|
||||
1. **Motion & Action**
|
||||
- Primary actions and movements
|
||||
- Motion speed and dynamics
|
||||
- Camera movements (pan, zoom, tracking, static)
|
||||
- Transition types between scenes
|
||||
|
||||
2. **Temporal Elements**
|
||||
- Scene duration and pacing
|
||||
- Sequence of events
|
||||
- Time of day changes
|
||||
- Motion continuity
|
||||
|
||||
3. **Visual Consistency**
|
||||
- Character/object persistence
|
||||
- Style consistency throughout
|
||||
- Lighting continuity
|
||||
- Color grading consistency
|
||||
|
||||
4. **Scene Breakdown**
|
||||
- Opening frame description
|
||||
- Key action moments
|
||||
- Transitions and cuts
|
||||
- Closing frame details
|
||||
|
||||
5. **Technical Specifications**
|
||||
- Frame rate and resolution
|
||||
- Aspect ratio
|
||||
- Video length
|
||||
- Special effects or post-processing
|
||||
|
||||
Format your video prompt as:
|
||||
"[Opening scene], [camera movement], [main action sequence], [visual style], [lighting/atmosphere], [duration], [technical specs], [ending scene]"
|
||||
|
||||
Include:
|
||||
- Specific motion descriptors (slowly, rapidly, smoothly)
|
||||
- Camera terminology (dolly in, pan left, aerial shot)
|
||||
- Temporal markers (then, meanwhile, gradually)
|
||||
- Consistency notes for multi-scene videos
|
||||
|
||||
Example:
|
||||
"Aerial shot slowly descending toward a misty forest at dawn, camera smoothly transitions to tracking shot following a deer through the trees, photorealistic style, soft golden hour lighting with fog, 10 second duration, 4K resolution 24fps, ending with close-up of deer looking at camera"
|
||||
"""
|
||||
Example of correct output:
|
||||
Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage, their movements fluid and dynamic as they exchange rapid punches under dramatic theater lighting that casts long shadows across the ring, with the crowd visible as blurred silhouettes in the darkened background."""
|
||||
|
||||
PROMPT_TEMPLATES = {
|
||||
"flux": FLUX_PROMPT,
|
||||
@@ -181,20 +118,11 @@ PROMPT_TEMPLATES = {
|
||||
|
||||
PROMPT_OPTIONS = ["flux", "sdxl", "danbooru", "video"]
|
||||
|
||||
# Available Gemini models
|
||||
GEMINI_MODELS = [
|
||||
"gemini-1.5-pro", # Most capable model
|
||||
"gemini-1.5-flash", # Fast, efficient model
|
||||
"gemini-1.5-flash-8b", # Smaller, faster variant
|
||||
"gemini-pro-vision", # Vision-optimized model
|
||||
"gemini-1.0-pro", # Previous generation pro model
|
||||
# Default models list (fallback if API is unavailable)
|
||||
DEFAULT_GEMINI_MODELS = [
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
]
|
||||
|
||||
# Model descriptions for UI
|
||||
MODEL_DESCRIPTIONS = {
|
||||
"gemini-1.5-pro": "Most capable Gemini model for complex tasks",
|
||||
"gemini-1.5-flash": "Faster and cost-effective (recommended for most uses)",
|
||||
"gemini-1.5-flash-8b": "Smaller and faster, good for simple prompts",
|
||||
"gemini-pro-vision": "Optimized for vision tasks and image analysis",
|
||||
"gemini-1.0-pro": "Previous generation, stable option",
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "kikotools"
|
||||
description = "Simple tools for ComfyUI"
|
||||
version = "1.0.7"
|
||||
version = "1.0.9"
|
||||
license = {text = "MIT"}
|
||||
dependencies = []
|
||||
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Unit tests for DisplayAny node."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from kikotools.tools.display_any import DisplayAnyNode
|
||||
from kikotools.tools.display_any.logic import (
|
||||
format_display_value,
|
||||
get_tensor_shapes,
|
||||
validate_display_mode,
|
||||
)
|
||||
from kikotools.tools.display_any.node import AnyType
|
||||
|
||||
|
||||
class TestAnyType:
|
||||
"""Test cases for AnyType class."""
|
||||
|
||||
def test_anytype_not_equal(self):
|
||||
"""Test that AnyType is never equal to other types."""
|
||||
any_type = AnyType("*")
|
||||
|
||||
# Should not be equal to any other type
|
||||
assert not (any_type != "STRING")
|
||||
assert not (any_type != "IMAGE")
|
||||
assert not (any_type != "LATENT")
|
||||
assert not (any_type != 123)
|
||||
assert not (any_type != None)
|
||||
assert not (any_type != ["LIST"])
|
||||
|
||||
def test_anytype_string_representation(self):
|
||||
"""Test string representation of AnyType."""
|
||||
any_type = AnyType("*")
|
||||
assert str(any_type) == "*"
|
||||
|
||||
|
||||
class TestDisplayAnyNode:
|
||||
"""Test cases for DisplayAnyNode."""
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert DisplayAnyNode.CATEGORY == "ComfyAssets"
|
||||
assert DisplayAnyNode.FUNCTION == "display"
|
||||
assert DisplayAnyNode.RETURN_TYPES == ("STRING",)
|
||||
assert DisplayAnyNode.RETURN_NAMES == ("display_text",)
|
||||
assert DisplayAnyNode.OUTPUT_NODE is True
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test INPUT_TYPES configuration."""
|
||||
input_types = DisplayAnyNode.INPUT_TYPES()
|
||||
|
||||
# Check required inputs
|
||||
assert "required" in input_types
|
||||
assert "input" in input_types["required"]
|
||||
# Check that input is AnyType with wildcard
|
||||
input_type = input_types["required"]["input"]
|
||||
assert len(input_type) == 2
|
||||
assert isinstance(input_type[0], AnyType)
|
||||
assert str(input_type[0]) == "*"
|
||||
assert input_type[1] == {}
|
||||
assert "mode" in input_types["required"]
|
||||
assert input_types["required"]["mode"] == (["raw value", "tensor shape"],)
|
||||
|
||||
def test_validate_inputs(self):
|
||||
"""Test VALIDATE_INPUTS always returns True."""
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS() is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input="test") is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input=123, mode="raw value") is True
|
||||
|
||||
def test_display_raw_value_string(self):
|
||||
"""Test displaying raw string value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("Hello, World!", "raw value")
|
||||
|
||||
assert "ui" in result
|
||||
assert "text" in result["ui"]
|
||||
assert result["ui"]["text"] == "Hello, World!"
|
||||
assert "result" in result
|
||||
assert result["result"] == ("Hello, World!",)
|
||||
|
||||
def test_display_raw_value_number(self):
|
||||
"""Test displaying raw number value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(42, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == "42"
|
||||
assert result["result"] == ("42",)
|
||||
|
||||
def test_display_raw_value_list(self):
|
||||
"""Test displaying raw list value."""
|
||||
node = DisplayAnyNode()
|
||||
test_list = [1, 2, 3, "test"]
|
||||
result = node.display(test_list, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_list)
|
||||
assert result["result"] == (str(test_list),)
|
||||
|
||||
def test_display_raw_value_dict(self):
|
||||
"""Test displaying raw dictionary value."""
|
||||
node = DisplayAnyNode()
|
||||
test_dict = {"key": "value", "number": 123}
|
||||
result = node.display(test_dict, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_dict)
|
||||
assert result["result"] == (str(test_dict),)
|
||||
|
||||
def test_display_tensor_shape_numpy(self):
|
||||
"""Test displaying numpy tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = np.random.rand(4, 3, 224, 224)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[4, 3, 224, 224]]"
|
||||
assert result["result"] == ("[[4, 3, 224, 224]]",)
|
||||
|
||||
@pytest.mark.skipif(not torch, reason="PyTorch not installed")
|
||||
def test_display_tensor_shape_torch(self):
|
||||
"""Test displaying PyTorch tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = torch.randn(2, 10, 512, 512)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[2, 10, 512, 512]]"
|
||||
assert result["result"] == ("[[2, 10, 512, 512]]",)
|
||||
|
||||
def test_display_nested_tensors(self):
|
||||
"""Test displaying shapes from nested structure with tensors."""
|
||||
node = DisplayAnyNode()
|
||||
nested_data = {
|
||||
"images": np.random.rand(1, 3, 256, 256),
|
||||
"masks": [
|
||||
np.random.rand(256, 256),
|
||||
np.random.rand(256, 256, 1),
|
||||
],
|
||||
"metadata": {"info": "test", "tensor": np.random.rand(10)},
|
||||
}
|
||||
result = node.display(nested_data, "tensor shape")
|
||||
|
||||
expected = "[[1, 3, 256, 256], [256, 256], [256, 256, 1], [10]]"
|
||||
assert result["ui"]["text"] == expected
|
||||
assert result["result"] == (expected,)
|
||||
|
||||
def test_display_no_tensors(self):
|
||||
"""Test displaying when no tensors are present."""
|
||||
node = DisplayAnyNode()
|
||||
data = {"text": "hello", "number": 42, "list": [1, 2, 3]}
|
||||
result = node.display(data, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "No tensors found in input"
|
||||
assert result["result"] == ("No tensors found in input",)
|
||||
|
||||
def test_invalid_mode_defaults_to_raw(self):
|
||||
"""Test that invalid mode defaults to raw value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("test", "invalid_mode")
|
||||
|
||||
assert result["ui"]["text"] == "test"
|
||||
assert result["result"] == ("test",)
|
||||
|
||||
|
||||
class TestDisplayAnyLogic:
|
||||
"""Test cases for DisplayAny logic functions."""
|
||||
|
||||
def test_get_tensor_shapes_single(self):
|
||||
"""Test getting shape from single tensor."""
|
||||
tensor = np.random.rand(3, 224, 224)
|
||||
shapes = get_tensor_shapes(tensor)
|
||||
|
||||
assert len(shapes) == 1
|
||||
assert shapes[0] == [3, 224, 224]
|
||||
|
||||
def test_get_tensor_shapes_nested_dict(self):
|
||||
"""Test getting shapes from nested dictionary."""
|
||||
data = {
|
||||
"level1": {
|
||||
"tensor1": np.random.rand(10, 20),
|
||||
"level2": {"tensor2": np.random.rand(5, 5, 5)},
|
||||
}
|
||||
}
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [10, 20] in shapes
|
||||
assert [5, 5, 5] in shapes
|
||||
|
||||
def test_get_tensor_shapes_nested_list(self):
|
||||
"""Test getting shapes from nested list."""
|
||||
data = [
|
||||
np.random.rand(1, 2, 3),
|
||||
[np.random.rand(4, 5), np.random.rand(6, 7, 8)],
|
||||
"not a tensor",
|
||||
]
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 3
|
||||
assert [1, 2, 3] in shapes
|
||||
assert [4, 5] in shapes
|
||||
assert [6, 7, 8] in shapes
|
||||
|
||||
def test_get_tensor_shapes_tuple(self):
|
||||
"""Test getting shapes from tuple."""
|
||||
data = (np.random.rand(2, 2), np.random.rand(3, 3))
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [2, 2] in shapes
|
||||
assert [3, 3] in shapes
|
||||
|
||||
def test_format_display_value_raw(self):
|
||||
"""Test formatting for raw value display."""
|
||||
result = format_display_value({"key": "value"}, "raw value")
|
||||
assert result == "{'key': 'value'}"
|
||||
|
||||
def test_format_display_value_tensor_shape(self):
|
||||
"""Test formatting for tensor shape display."""
|
||||
tensor = np.random.rand(10, 10)
|
||||
result = format_display_value(tensor, "tensor shape")
|
||||
assert result == "[[10, 10]]"
|
||||
|
||||
def test_format_display_value_no_tensors(self):
|
||||
"""Test formatting when no tensors present."""
|
||||
result = format_display_value("just a string", "tensor shape")
|
||||
assert result == "No tensors found in input"
|
||||
|
||||
def test_validate_display_mode(self):
|
||||
"""Test display mode validation."""
|
||||
assert validate_display_mode("raw value") is True
|
||||
assert validate_display_mode("tensor shape") is True
|
||||
assert validate_display_mode("invalid") is False
|
||||
assert validate_display_mode("") is False
|
||||
assert validate_display_mode(None) is False
|
||||
|
||||
|
||||
class TestDisplayAnyEdgeCases:
|
||||
"""Test edge cases for DisplayAny."""
|
||||
|
||||
def test_display_none(self):
|
||||
"""Test displaying None value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(None, "raw value")
|
||||
assert result["ui"]["text"] == "None"
|
||||
|
||||
def test_display_empty_list(self):
|
||||
"""Test displaying empty list."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display([], "raw value")
|
||||
assert result["ui"]["text"] == "[]"
|
||||
|
||||
def test_display_empty_dict(self):
|
||||
"""Test displaying empty dictionary."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display({}, "raw value")
|
||||
assert result["ui"]["text"] == "{}"
|
||||
|
||||
def test_display_complex_nested_structure(self):
|
||||
"""Test displaying complex nested structure."""
|
||||
node = DisplayAnyNode()
|
||||
complex_data = {
|
||||
"images": [np.random.rand(1, 3, 64, 64) for _ in range(3)],
|
||||
"config": {
|
||||
"steps": 20,
|
||||
"cfg": 7.5,
|
||||
"sampler": "euler",
|
||||
"latents": np.random.rand(1, 4, 32, 32),
|
||||
},
|
||||
"prompts": ["test1", "test2"],
|
||||
}
|
||||
result = node.display(complex_data, "tensor shape")
|
||||
|
||||
# Should find 4 tensors total (3 images + 1 latent)
|
||||
shapes_text = result["ui"]["text"]
|
||||
assert "[1, 3, 64, 64]" in shapes_text
|
||||
assert "[1, 4, 32, 32]" in shapes_text
|
||||
|
||||
def test_display_very_long_string(self):
|
||||
"""Test displaying very long string."""
|
||||
node = DisplayAnyNode()
|
||||
long_string = "x" * 10000
|
||||
result = node.display(long_string, "raw value")
|
||||
assert result["ui"]["text"] == long_string
|
||||
|
||||
def test_display_unicode(self):
|
||||
"""Test displaying unicode characters."""
|
||||
node = DisplayAnyNode()
|
||||
unicode_text = "Hello 世界 🌍"
|
||||
result = node.display(unicode_text, "raw value")
|
||||
assert result["ui"]["text"] == unicode_text
|
||||
@@ -0,0 +1,595 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyAssets.DisplayText",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "DisplayText") {
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
if (message?.text) {
|
||||
// Create or update the text widget
|
||||
this.updateTextDisplay(message.text[0]);
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.updateTextDisplay = function(text) {
|
||||
// Remove existing text widget if any
|
||||
const existingWidget = this.widgets?.find(w => w.name === "displayed_text");
|
||||
if (existingWidget) {
|
||||
const index = this.widgets.indexOf(existingWidget);
|
||||
this.widgets.splice(index, 1);
|
||||
}
|
||||
|
||||
// Parse the text to detect positive/negative prompt format
|
||||
function parsePrompts(text) {
|
||||
const posMatch = text.match(/Positive prompt:\s*([\s\S]*?)(?=Negative prompt:|$)/i);
|
||||
const negMatch = text.match(/Negative prompt:\s*([\s\S]*?)(?=\*\*|$)/i);
|
||||
|
||||
if (posMatch && negMatch) {
|
||||
// Extract just the prompt content, stopping at the first ** marker
|
||||
let positiveText = posMatch[1].trim();
|
||||
let negativeText = negMatch[1].trim();
|
||||
|
||||
// Remove any trailing ** markers and everything after them
|
||||
const posEndIndex = positiveText.indexOf('**');
|
||||
if (posEndIndex > 0) {
|
||||
positiveText = positiveText.substring(0, posEndIndex).trim();
|
||||
}
|
||||
|
||||
const negEndIndex = negativeText.indexOf('**');
|
||||
if (negEndIndex > 0) {
|
||||
negativeText = negativeText.substring(0, negEndIndex).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'prompts',
|
||||
positive: positiveText,
|
||||
negative: negativeText
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
content: text
|
||||
};
|
||||
}
|
||||
|
||||
const parsedContent = parsePrompts(text);
|
||||
|
||||
// Create custom widget for text display
|
||||
const widget = {
|
||||
type: "custom_text_display",
|
||||
name: "displayed_text",
|
||||
size: [this.size[0] - 20, this.size[1] - 60], // Adjust for node chrome
|
||||
parsedContent: parsedContent,
|
||||
scrollOffset: 0,
|
||||
posScrollOffset: 0,
|
||||
negScrollOffset: 0,
|
||||
|
||||
draw: function(ctx, node, widget_width, y, H) {
|
||||
const margin = 10;
|
||||
const padding = 10;
|
||||
const lineHeight = 20;
|
||||
const buttonHeight = 30;
|
||||
const buttonWidth = 90;
|
||||
|
||||
// Use the actual widget height from node size
|
||||
const availableHeight = node.size[1] - 60; // Account for node header and margins
|
||||
this.size[1] = Math.max(100, availableHeight);
|
||||
|
||||
// Calculate available width for text
|
||||
const availableWidth = widget_width - margin * 2 - padding * 2;
|
||||
|
||||
// Word wrap function with better performance
|
||||
function wrapText(text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
|
||||
ctx.font = "14px monospace";
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines.length > 0 ? lines : [''];
|
||||
}
|
||||
|
||||
// Draw background
|
||||
ctx.fillStyle = "#2a2a2a";
|
||||
ctx.fillRect(margin, y, widget_width - margin * 2, this.size[1]);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.strokeRect(margin, y, widget_width - margin * 2, this.size[1]);
|
||||
|
||||
if (this.parsedContent.type === 'prompts') {
|
||||
// Simple split view for positive/negative prompts
|
||||
const headerHeight = 25;
|
||||
const buttonAreaHeight = buttonHeight + padding;
|
||||
const totalTextHeight = this.size[1] - buttonAreaHeight;
|
||||
const halfHeight = totalTextHeight / 2;
|
||||
|
||||
// Draw positive prompt header
|
||||
ctx.fillStyle = "#3a3a3a";
|
||||
ctx.fillRect(margin + 1, y + 1, widget_width - margin * 2 - 2, headerHeight);
|
||||
ctx.fillStyle = "#8f8";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillText("✓ Positive Prompt", margin + padding, y + headerHeight - 7);
|
||||
|
||||
// Positive prompt text area
|
||||
const posTextY = y + headerHeight;
|
||||
const posTextHeight = halfHeight - headerHeight;
|
||||
ctx.fillStyle = "#1e1e1e";
|
||||
ctx.fillRect(margin + 1, posTextY, widget_width - margin * 2 - 2, posTextHeight);
|
||||
|
||||
// Draw separator
|
||||
const separatorY = y + halfHeight;
|
||||
ctx.strokeStyle = "#555";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(margin, separatorY);
|
||||
ctx.lineTo(widget_width - margin, separatorY);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw negative prompt header
|
||||
ctx.fillStyle = "#3a3a3a";
|
||||
ctx.fillRect(margin + 1, separatorY + 1, widget_width - margin * 2 - 2, headerHeight);
|
||||
ctx.fillStyle = "#f88";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillText("✗ Negative Prompt", margin + padding, separatorY + headerHeight - 7);
|
||||
|
||||
// Negative prompt text area
|
||||
const negTextY = separatorY + headerHeight;
|
||||
const negTextHeight = halfHeight - headerHeight;
|
||||
ctx.fillStyle = "#1e1e1e";
|
||||
ctx.fillRect(margin + 1, negTextY, widget_width - margin * 2 - 2, negTextHeight);
|
||||
|
||||
// Draw text for both sections
|
||||
ctx.font = "14px monospace";
|
||||
ctx.fillStyle = "#ddd";
|
||||
|
||||
// Wrap text for both prompts
|
||||
const posLines = [];
|
||||
const posParagraphs = this.parsedContent.positive.split('\n');
|
||||
for (const para of posParagraphs) {
|
||||
if (para.trim() === '') {
|
||||
posLines.push('');
|
||||
} else {
|
||||
posLines.push(...wrapText(para, availableWidth - 10));
|
||||
}
|
||||
}
|
||||
|
||||
const negLines = [];
|
||||
const negParagraphs = this.parsedContent.negative.split('\n');
|
||||
for (const para of negParagraphs) {
|
||||
if (para.trim() === '') {
|
||||
negLines.push('');
|
||||
} else {
|
||||
negLines.push(...wrapText(para, availableWidth - 10));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw positive prompt text with clipping
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(margin + padding, posTextY + padding, availableWidth - 10, posTextHeight - padding * 2);
|
||||
ctx.clip();
|
||||
|
||||
let currentY = posTextY + padding + lineHeight - 5;
|
||||
const posVisibleLines = Math.floor((posTextHeight - padding * 2) / lineHeight);
|
||||
const posStartLine = Math.floor(this.posScrollOffset);
|
||||
const posEndLine = Math.min(posStartLine + posVisibleLines, posLines.length);
|
||||
|
||||
for (let i = posStartLine; i < posEndLine; i++) {
|
||||
ctx.fillText(posLines[i], margin + padding, currentY);
|
||||
currentY += lineHeight;
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw positive scroll indicator if needed
|
||||
if (posLines.length > posVisibleLines) {
|
||||
const scrollBarWidth = 6;
|
||||
const scrollBarX = widget_width - margin - scrollBarWidth - 2;
|
||||
const scrollBarHeight = posTextHeight - padding * 2;
|
||||
const maxScroll = posLines.length - posVisibleLines;
|
||||
const scrollRatio = this.posScrollOffset / maxScroll;
|
||||
const thumbHeight = Math.max(20, (posVisibleLines / posLines.length) * scrollBarHeight);
|
||||
const thumbY = posTextY + padding + scrollRatio * (scrollBarHeight - thumbHeight);
|
||||
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(scrollBarX, posTextY + padding, scrollBarWidth, scrollBarHeight);
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight);
|
||||
}
|
||||
|
||||
// Draw negative prompt text with clipping
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(margin + padding, negTextY + padding, availableWidth - 10, negTextHeight - padding * 2);
|
||||
ctx.clip();
|
||||
|
||||
ctx.fillStyle = "#ddd";
|
||||
currentY = negTextY + padding + lineHeight - 5;
|
||||
const negVisibleLines = Math.floor((negTextHeight - padding * 2) / lineHeight);
|
||||
const negStartLine = Math.floor(this.negScrollOffset);
|
||||
const negEndLine = Math.min(negStartLine + negVisibleLines, negLines.length);
|
||||
|
||||
for (let i = negStartLine; i < negEndLine; i++) {
|
||||
ctx.fillText(negLines[i], margin + padding, currentY);
|
||||
currentY += lineHeight;
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw negative scroll indicator if needed
|
||||
if (negLines.length > negVisibleLines) {
|
||||
const scrollBarWidth = 6;
|
||||
const scrollBarX = widget_width - margin - scrollBarWidth - 2;
|
||||
const scrollBarHeight = negTextHeight - padding * 2;
|
||||
const maxScroll = negLines.length - negVisibleLines;
|
||||
const scrollRatio = this.negScrollOffset / maxScroll;
|
||||
const thumbHeight = Math.max(20, (negVisibleLines / negLines.length) * scrollBarHeight);
|
||||
const thumbY = negTextY + padding + scrollRatio * (scrollBarHeight - thumbHeight);
|
||||
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(scrollBarX, negTextY + padding, scrollBarWidth, scrollBarHeight);
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight);
|
||||
}
|
||||
|
||||
// Draw copy buttons
|
||||
const buttonY = y + this.size[1] - buttonHeight - padding / 2;
|
||||
const halfWidth = (widget_width - margin * 2) / 2;
|
||||
|
||||
// Positive copy button
|
||||
const posButtonX = margin + halfWidth / 2 - buttonWidth / 2;
|
||||
ctx.fillStyle = this.posCopyHovered ? "#5a5a5a" : "#4a4a4a";
|
||||
ctx.fillRect(posButtonX, buttonY, buttonWidth, buttonHeight);
|
||||
ctx.strokeStyle = "#666";
|
||||
ctx.strokeRect(posButtonX, buttonY, buttonWidth, buttonHeight);
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.posCopySuccess ? "✓ Copied!" : "📋 Positive", posButtonX + buttonWidth/2, buttonY + buttonHeight/2);
|
||||
|
||||
// Negative copy button
|
||||
const negButtonX = margin + halfWidth + halfWidth / 2 - buttonWidth / 2;
|
||||
ctx.fillStyle = this.negCopyHovered ? "#5a5a5a" : "#4a4a4a";
|
||||
ctx.fillRect(negButtonX, buttonY, buttonWidth, buttonHeight);
|
||||
ctx.strokeStyle = "#666";
|
||||
ctx.strokeRect(negButtonX, buttonY, buttonWidth, buttonHeight);
|
||||
|
||||
// Ensure text color and alignment are set
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.negCopySuccess ? "✓ Copied!" : "📋 Negative", negButtonX + buttonWidth/2, buttonY + buttonHeight/2);
|
||||
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
|
||||
} else {
|
||||
// Regular text display
|
||||
const textAreaHeight = this.size[1] - buttonHeight - padding;
|
||||
|
||||
// Draw text area background
|
||||
ctx.fillStyle = "#1e1e1e";
|
||||
ctx.fillRect(margin + 1, y + 1, widget_width - margin * 2 - 2, textAreaHeight);
|
||||
|
||||
// Process text
|
||||
ctx.font = "14px monospace";
|
||||
const paragraphs = this.parsedContent.content.split('\n');
|
||||
const allLines = [];
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.trim() === '') {
|
||||
allLines.push('');
|
||||
} else {
|
||||
const wrappedLines = wrapText(paragraph, availableWidth);
|
||||
allLines.push(...wrappedLines);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text with clipping
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(margin + padding, y + padding, availableWidth, textAreaHeight - padding * 2);
|
||||
ctx.clip();
|
||||
|
||||
const visibleLines = Math.floor((textAreaHeight - padding * 2) / lineHeight);
|
||||
const maxScroll = Math.max(0, allLines.length - visibleLines);
|
||||
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
|
||||
|
||||
ctx.fillStyle = "#ddd";
|
||||
let currentY = y + padding + lineHeight - 5 - (this.scrollOffset * lineHeight);
|
||||
|
||||
for (let i = 0; i < allLines.length; i++) {
|
||||
if (currentY > y && currentY < y + textAreaHeight) {
|
||||
ctx.fillText(allLines[i], margin + padding, currentY);
|
||||
}
|
||||
currentY += lineHeight;
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Draw scroll indicator if needed
|
||||
if (allLines.length > visibleLines) {
|
||||
const scrollBarWidth = 6;
|
||||
const scrollBarX = widget_width - margin - scrollBarWidth - 2;
|
||||
const scrollBarHeight = textAreaHeight - 4;
|
||||
const thumbHeight = Math.max(20, (visibleLines / allLines.length) * scrollBarHeight);
|
||||
const thumbY = y + 2 + (this.scrollOffset / maxScroll) * (scrollBarHeight - thumbHeight);
|
||||
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(scrollBarX, y + 2, scrollBarWidth, scrollBarHeight);
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillRect(scrollBarX, thumbY, scrollBarWidth, thumbHeight);
|
||||
}
|
||||
|
||||
// Draw copy button
|
||||
const buttonX = widget_width - margin - buttonWidth - padding;
|
||||
const buttonY = y + textAreaHeight + padding / 2;
|
||||
|
||||
ctx.fillStyle = this.copyButtonHovered ? "#5a5a5a" : "#4a4a4a";
|
||||
ctx.fillRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
||||
ctx.strokeStyle = "#666";
|
||||
ctx.strokeRect(buttonX, buttonY, buttonWidth, buttonHeight);
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(this.copySuccess ? "✓ Copied!" : "📋 Copy", buttonX + buttonWidth/2, buttonY + buttonHeight/2);
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
return this.size[1];
|
||||
},
|
||||
|
||||
mouse: function(event, pos, node) {
|
||||
const margin = 10;
|
||||
const padding = 10;
|
||||
const buttonWidth = 90;
|
||||
const buttonHeight = 30;
|
||||
const lineHeight = 20;
|
||||
|
||||
// Check if mouse is over the widget
|
||||
const isOver = pos[1] > this.last_y && pos[1] < this.last_y + this.size[1];
|
||||
if (!isOver) return false;
|
||||
|
||||
if (this.parsedContent.type === 'prompts') {
|
||||
// Handle split view
|
||||
const headerHeight = 25;
|
||||
const buttonAreaHeight = buttonHeight + padding;
|
||||
const totalTextHeight = this.size[1] - buttonAreaHeight;
|
||||
const halfHeight = totalTextHeight / 2;
|
||||
|
||||
const posTextY = this.last_y + headerHeight;
|
||||
const posTextHeight = halfHeight - headerHeight;
|
||||
const negTextY = this.last_y + halfHeight + headerHeight;
|
||||
const negTextHeight = halfHeight - headerHeight;
|
||||
|
||||
const buttonY = this.last_y + this.size[1] - buttonHeight - padding / 2;
|
||||
const halfWidth = (node.size[0] - margin * 2) / 2;
|
||||
|
||||
// Check which section for scrolling
|
||||
const inPosSection = pos[1] > posTextY && pos[1] < posTextY + posTextHeight;
|
||||
const inNegSection = pos[1] > negTextY && pos[1] < negTextY + negTextHeight;
|
||||
|
||||
// Handle scrolling
|
||||
if (event.type === "wheel") {
|
||||
if (inPosSection) {
|
||||
const delta = event.deltaY > 0 ? 1 : -1;
|
||||
this.posScrollOffset = (this.posScrollOffset || 0) + delta;
|
||||
|
||||
// Calculate max scroll
|
||||
const visibleLines = Math.floor((posTextHeight - padding * 2) / lineHeight);
|
||||
const totalLines = this.parsedContent.positive.split('\n').length * 2; // Estimate
|
||||
const maxScroll = Math.max(0, totalLines - visibleLines);
|
||||
this.posScrollOffset = Math.max(0, Math.min(this.posScrollOffset, maxScroll));
|
||||
|
||||
node.setDirtyCanvas(true);
|
||||
return true;
|
||||
} else if (inNegSection) {
|
||||
const delta = event.deltaY > 0 ? 1 : -1;
|
||||
this.negScrollOffset = (this.negScrollOffset || 0) + delta;
|
||||
|
||||
// Calculate max scroll
|
||||
const visibleLines = Math.floor((negTextHeight - padding * 2) / lineHeight);
|
||||
const totalLines = this.parsedContent.negative.split('\n').length * 2; // Estimate
|
||||
const maxScroll = Math.max(0, totalLines - visibleLines);
|
||||
this.negScrollOffset = Math.max(0, Math.min(this.negScrollOffset, maxScroll));
|
||||
|
||||
node.setDirtyCanvas(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check button hovers
|
||||
const posButtonX = margin + halfWidth / 2 - buttonWidth / 2;
|
||||
const negButtonX = margin + halfWidth + halfWidth / 2 - buttonWidth / 2;
|
||||
|
||||
const oldPosHover = this.posCopyHovered;
|
||||
const oldNegHover = this.negCopyHovered;
|
||||
|
||||
this.posCopyHovered = pos[0] > posButtonX && pos[0] < posButtonX + buttonWidth &&
|
||||
pos[1] > buttonY && pos[1] < buttonY + buttonHeight;
|
||||
this.negCopyHovered = pos[0] > negButtonX && pos[0] < negButtonX + buttonWidth &&
|
||||
pos[1] > buttonY && pos[1] < buttonY + buttonHeight;
|
||||
|
||||
if (oldPosHover !== this.posCopyHovered || oldNegHover !== this.negCopyHovered) {
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
|
||||
// Handle button clicks
|
||||
if (event.type === "pointerdown") {
|
||||
if (this.posCopyHovered) {
|
||||
this.copyToClipboard(this.parsedContent.positive, 'positive');
|
||||
return true;
|
||||
} else if (this.negCopyHovered) {
|
||||
this.copyToClipboard(this.parsedContent.negative, 'negative');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular text handling
|
||||
const textAreaHeight = this.size[1] - buttonHeight - padding;
|
||||
const buttonX = node.size[0] - margin - buttonWidth - padding;
|
||||
const buttonY = this.last_y + textAreaHeight + padding / 2;
|
||||
|
||||
// Handle scrolling
|
||||
if (event.type === "wheel" && pos[1] < this.last_y + textAreaHeight) {
|
||||
const delta = event.deltaY > 0 ? 1 : -1;
|
||||
this.scrollOffset = (this.scrollOffset || 0) + delta;
|
||||
|
||||
const visibleLines = Math.floor((textAreaHeight - padding * 2) / lineHeight);
|
||||
const totalLines = this.parsedContent.content.split('\n').length * 2; // Estimate
|
||||
const maxScroll = Math.max(0, totalLines - visibleLines);
|
||||
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
|
||||
|
||||
node.setDirtyCanvas(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check button hover
|
||||
const oldHover = this.copyButtonHovered;
|
||||
this.copyButtonHovered = pos[0] > buttonX && pos[0] < buttonX + buttonWidth &&
|
||||
pos[1] > buttonY && pos[1] < buttonY + buttonHeight;
|
||||
|
||||
if (oldHover !== this.copyButtonHovered) {
|
||||
node.setDirtyCanvas(true);
|
||||
}
|
||||
|
||||
// Handle button click
|
||||
if (event.type === "pointerdown" && this.copyButtonHovered) {
|
||||
this.copyToClipboard(this.parsedContent.content, 'regular');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
copyToClipboard: function(text, type) {
|
||||
const node = this._node;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
if (type === 'positive') {
|
||||
this.posCopySuccess = true;
|
||||
} else if (type === 'negative') {
|
||||
this.negCopySuccess = true;
|
||||
} else {
|
||||
this.copySuccess = true;
|
||||
}
|
||||
node.setDirtyCanvas(true);
|
||||
|
||||
setTimeout(() => {
|
||||
this.posCopySuccess = false;
|
||||
this.negCopySuccess = false;
|
||||
this.copySuccess = false;
|
||||
node.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
// Fallback copy method
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.opacity = "0";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
if (type === 'positive') {
|
||||
this.posCopySuccess = true;
|
||||
} else if (type === 'negative') {
|
||||
this.negCopySuccess = true;
|
||||
} else {
|
||||
this.copySuccess = true;
|
||||
}
|
||||
node.setDirtyCanvas(true);
|
||||
setTimeout(() => {
|
||||
this.posCopySuccess = false;
|
||||
this.negCopySuccess = false;
|
||||
this.copySuccess = false;
|
||||
node.setDirtyCanvas(true);
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
});
|
||||
},
|
||||
|
||||
computeSize: function(width) {
|
||||
return [width, this.size[1]];
|
||||
}
|
||||
};
|
||||
|
||||
// Store reference to node for callbacks
|
||||
widget._node = this;
|
||||
|
||||
// Store the last y position for mouse detection
|
||||
const originalDraw = widget.draw;
|
||||
widget.draw = function(ctx, node, widget_width, y, H) {
|
||||
this.last_y = y;
|
||||
return originalDraw.call(this, ctx, node, widget_width, y, H);
|
||||
};
|
||||
|
||||
// Add the widget
|
||||
if (!this.widgets) {
|
||||
this.widgets = [];
|
||||
}
|
||||
this.widgets.push(widget);
|
||||
|
||||
// Adjust node size to accommodate the widget
|
||||
this.computeSize();
|
||||
};
|
||||
|
||||
// Handle node resizing
|
||||
const onResize = nodeType.prototype.onResize;
|
||||
nodeType.prototype.onResize = function(size) {
|
||||
onResize?.apply(this, arguments);
|
||||
|
||||
// Update widget size when node is resized
|
||||
const textWidget = this.widgets?.find(w => w.name === "displayed_text");
|
||||
if (textWidget) {
|
||||
textWidget.size[0] = size[0] - 20;
|
||||
textWidget.size[1] = size[1] - 60;
|
||||
this.setDirtyCanvas(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on node creation
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Set default size
|
||||
this.size[0] = Math.max(this.size[0], 350);
|
||||
this.size[1] = Math.max(this.size[1], 300);
|
||||
|
||||
// Add placeholder text
|
||||
this.updateTextDisplay("Text will appear here after execution...");
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -26,6 +26,12 @@ app.registerExtension({
|
||||
// Style the button
|
||||
helpButton.serialize = false;
|
||||
|
||||
// Add refresh models button
|
||||
const refreshButton = this.addWidget("button", "Refresh Model List", null, () => {
|
||||
this.refreshModelList();
|
||||
});
|
||||
refreshButton.serialize = false;
|
||||
|
||||
// Add status indicator
|
||||
this.status = this.addWidget("text", "status", "Ready", () => {}, {
|
||||
serialize: false
|
||||
@@ -94,6 +100,28 @@ app.registerExtension({
|
||||
// For now, it's always visible but this method provides extensibility
|
||||
};
|
||||
|
||||
// Add method to refresh model list
|
||||
nodeType.prototype.refreshModelList = function() {
|
||||
if (this.status) {
|
||||
this.status.value = "Refreshing models...";
|
||||
}
|
||||
|
||||
// Set the refresh_models flag
|
||||
const refreshWidget = this.widgets.find(w => w.name === "refresh_models");
|
||||
if (refreshWidget) {
|
||||
refreshWidget.value = true;
|
||||
}
|
||||
|
||||
// Show message
|
||||
alert("Model list will refresh on next execution. Make sure API key is set and run the node.");
|
||||
|
||||
if (this.status) {
|
||||
setTimeout(() => {
|
||||
this.status.value = "Ready - Run node to refresh";
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Override execute to show status
|
||||
const onExecute = nodeType.prototype.onExecute;
|
||||
nodeType.prototype.onExecute = function() {
|
||||
|
||||