diff --git a/__init__.py b/__init__.py index e3d57f1..ebf4ba7 100644 --- a/__init__.py +++ b/__init__.py @@ -17,4 +17,3 @@ NODE_DISPLAY_NAME_MAPPINGS = { } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] - diff --git a/code-review-comfyui-prompt-generator.md b/code-review-comfyui-prompt-generator.md new file mode 100644 index 0000000..c0cede1 --- /dev/null +++ b/code-review-comfyui-prompt-generator.md @@ -0,0 +1,87 @@ +# Code Review: ComfyUI-PromptGenerator + +## Executive Summary + +The codebase is well-structured with good error handling and graceful degradation patterns. However, there is a **critical inconsistency** in the style system between the two nodes, plus several architectural concerns worth addressing. + +--- + +## 🚨 Critical Issues + +### 1. Style System Inconsistency (High Priority) + +**Problem:** The two nodes have completely different style options: + +| Node | Available Styles | +|------|------------------| +| `PromptGeneratorNode` | cinematic, anime, photorealistic, fantasy, abstract, cyberpunk, sci-fi, video_wan (8 styles) | +| `StyleApplierNode` | cinematic, still_image (2 styles via `StylePreset.get_style_choices()`) | + +The `style_presets.py` module only defines 2 styles, while `config/templates.yaml` and `DEFAULT_STYLES` define 8 styles. + +**Impact:** Users expect consistency. If they select "anime" style in PromptGenerator, they cannot use StyleApplier with "anime" - it will fail validation. + +**Recommendation:** Either: + +- Expand `STYLE_DEFINITIONS` in `style_presets.py` to include all 8 styles +- Or consolidate to use templates.yaml as the single source of truth + +--- + +## ⚠️ Architectural Concerns + +### 2. Triple Source of Truth for Styles + +The codebase has three different places defining styles: + +1. `config/templates.yaml` - YAML templates +2. `DEFAULT_STYLES` dict in Python (`prompt_generator_node.py`) +3. `style_presets.py` - StyleKeywords dataclass + +**Recommendation:** Consolidate to a single source. The YAML file is user-editable and should be the authoritative source. + +### 3. Unused `style_presets.py` Module + +The `StylePreset` class is only used by `StyleApplierNode`. The main `PromptGeneratorNode` uses its own `DEFAULT_STYLES` dict and YAML templates. + +--- + +## 🔧 Code Quality Issues + +### 4. Missing Style Validation in `generate()` + +In `generate()`, there's no validation that the selected style exists - it silently falls back to cinematic. + +### 5. Inconsistent Error Messages + +- `PromptGeneratorNode` returns emoji-prefixed messages: `"⚠️ Please enter an image description."` +- `StyleApplierNode` returns bracket-prefixed: `"[StyleApplier] Error: Unknown style..."` + +**Recommendation:** Standardize error message format. + +### 6. Hardcoded Values + +- `chunk_timeout = 30` is hardcoded in streaming +- Model list in `_get_available_models()` has hardcoded defaults + +--- + +## ✅ Positive Patterns + +1. **Graceful Degradation:** Excellent use of optional imports with fallbacks +2. **Streaming Implementation:** Well-designed background thread approach for timeout enforcement +3. **Cold Start Detection:** Smart 1.3x timeout multiplier for unloaded models +4. **Model Caching:** 60-second cache prevents UI freezes +5. **LoRA Prioritization:** Good sorting logic to surface fine-tuned models first + +--- + +## 📋 Recommendations Priority List + +| Priority | Issue | Action | +|----------|-------|--------| +| P0 | Style inconsistency | Sync `style_presets.py` with 8 styles or consolidate | +| P1 | Triple source of truth | Make templates.yaml the single source | +| P2 | Silent style fallback | Add validation/warning when style not found | +| P3 | Error message format | Standardize across nodes | +| P4 | Hardcoded values | Move to config/constants | diff --git a/node.zip b/node.zip new file mode 100644 index 0000000..0e56413 Binary files /dev/null and b/node.zip differ diff --git a/nodes/prompt_generator_node.py b/nodes/prompt_generator_node.py index e6386be..9eb81fa 100644 --- a/nodes/prompt_generator_node.py +++ b/nodes/prompt_generator_node.py @@ -71,12 +71,14 @@ class PromptGeneratorNode: ComfyUI node for generating Stable Diffusion prompts using Qwen3-8B via Ollama. Features: - - 7 style presets (cinematic, anime, photorealistic, fantasy, abstract, cyberpunk, sci-fi) + - 9 style presets loaded from templates.yaml (single source of truth) - Temperature and Top-P sampling controls - Optional focus area (emphasis) and mood inputs - Reasoning toggle to show/hide model's thinking process """ + CHUNK_TIMEOUT = 30 + # Default style templates (fallback when YAML not available) DEFAULT_STYLES = { "cinematic": { @@ -210,6 +212,24 @@ Format the response as a single, detailed sci-fi prompt.""", "description": "Minimalist template optimized for WanVideo LoRA", "template": "Generate a video prompt for: {{ description }}{% if emphasis %} with focus on {{ emphasis }}{% endif %}{% if mood %}, mood is {{ mood }}{% endif %}", }, + "still_image": { + "name": "Still Image (Photography)", + "description": "Sharp, realistic photography with technical camera specifications", + "template": """Write a detailed Stable Diffusion prompt for: {{ description }} + +Style: Generate a professional photography still image with sharp focus. +{% if emphasis %}Focus particularly on: {{ emphasis }}{% endif %} +{% if mood %}Mood/Atmosphere: {{ mood }}{% endif %} + +Include specific details about: +- Camera settings (ISO 100, f/2.8 aperture, sharp optics) +- Natural or studio lighting +- Realistic textures and fine details +- Clean, balanced composition +- Professional photography qualities + +Format the response as a single, detailed photography prompt.""", + }, } # Class-level cache for available models @@ -264,10 +284,25 @@ Format the response as a single, detailed sci-fi prompt.""", print(f"[PromptGenerator] Could not fetch models: {e}") return default_models + @classmethod + def _get_style_list(cls) -> list: + """Get style list from templates.yaml, falling back to DEFAULT_STYLES keys.""" + template_path = Path(__file__).parent.parent / "config" / "templates.yaml" + if YAML_AVAILABLE and template_path.exists(): + try: + with open(template_path, "r") as f: + templates = yaml.safe_load(f) + if templates: + return list(templates.keys()) + except Exception: + pass + return list(cls.DEFAULT_STYLES.keys()) + @classmethod def INPUT_TYPES(cls) -> Dict[str, Any]: """Define input parameters for the node.""" available_models = cls._get_available_models() + available_styles = cls._get_style_list() return { "required": { @@ -280,17 +315,12 @@ Format the response as a single, detailed sci-fi prompt.""", }, ), "style": ( - [ - "cinematic", - "anime", - "photorealistic", - "fantasy", - "abstract", - "cyberpunk", - "sci-fi", - "video_wan", - ], - {"default": "cinematic"}, + available_styles, + { + "default": available_styles[0] + if available_styles + else "cinematic" + }, ), "model": ( available_models, @@ -395,9 +425,13 @@ Format the response as a single, detailed sci-fi prompt.""", mood: Optional[str] = None, ) -> str: """Render a Jinja2 template with the given variables.""" - template_data = self.style_templates.get( - style, self.DEFAULT_STYLES.get("cinematic") - ) + template_data = self.style_templates.get(style) + if template_data is None: + print( + f"[PromptGenerator] Warning: style '{style}' not found in templates, " + f"falling back to 'cinematic'" + ) + template_data = self.DEFAULT_STYLES.get("cinematic") # Handle YAML format with 'template' key if isinstance(template_data, dict) and "template" in template_data: @@ -484,7 +518,7 @@ Format the response as a single, detailed sci-fi prompt.""", chunks = [] start = time.monotonic() first_chunk_timeout = min(timeout * 0.6, 90) - chunk_timeout = 30 + chunk_timeout = self.CHUNK_TIMEOUT got_first_chunk = False try: @@ -604,7 +638,7 @@ Format the response as a single, detailed sci-fi prompt.""", Tuple containing the generated prompt string """ if not description.strip(): - return ("⚠️ Please enter an image description.",) + return ("[PromptGenerator] Please enter an image description.",) # Render the template prompt = self._render_template( @@ -666,7 +700,7 @@ Format the response as a single, detailed sci-fi prompt.""", print(f"[PromptGenerator] Generated {len(output)} characters") return (output,) else: - return ("⚠️ Generation returned empty result.",) + return ("[PromptGenerator] Generation returned empty result.",) print("[PromptGenerator] Streaming failed, falling back to subprocess") @@ -680,7 +714,7 @@ Format the response as a single, detailed sci-fi prompt.""", ) if result.returncode != 0: - return (f"⚠️ Ollama error: {result.stderr}",) + return (f"[PromptGenerator] Ollama error: {result.stderr}",) output = result.stdout.strip() @@ -696,11 +730,13 @@ Format the response as a single, detailed sci-fi prompt.""", ) return (output,) else: - return ("⚠️ Generation returned empty result.",) + return ("[PromptGenerator] Generation returned empty result.",) except subprocess.TimeoutExpired: - return (f"⚠️ Generation timed out after {timeout}s",) + return (f"[PromptGenerator] Generation timed out after {timeout}s",) except FileNotFoundError: - return ("⚠️ Ollama not found. Install from: https://ollama.ai",) + return ( + "[PromptGenerator] Ollama not found. Install from: https://ollama.ai", + ) except Exception as e: - return (f"⚠️ Error: {str(e)}",) + return (f"[PromptGenerator] Error: {str(e)}",) diff --git a/nodes/style_applier_node.py b/nodes/style_applier_node.py index 3f4f340..a2664bc 100644 --- a/nodes/style_applier_node.py +++ b/nodes/style_applier_node.py @@ -9,52 +9,61 @@ from typing import Tuple class StyleApplierNode: """ ComfyUI node for applying Cinematic or Still Image style keywords to prompts. - + Inputs: - prompt: Base prompt text - style: "cinematic" or "still_image" - position: Where to add keywords ("prefix", "suffix", or "wrap") - emphasis: Optional emphasis level ("low", "medium", "high") - include_technical: Include camera/technical specs - + Outputs: - styled_prompt: The prompt with style keywords added - style_keywords: Just the style keywords (for reference) """ - + @classmethod def INPUT_TYPES(cls): """Define input parameters for the node.""" from style_presets import StylePreset - + return { "required": { - "prompt": ("STRING", { - "multiline": True, - "default": "", - "placeholder": "Enter your base prompt..." - }), + "prompt": ( + "STRING", + { + "multiline": True, + "default": "", + "placeholder": "Enter your base prompt...", + }, + ), "style": (StylePreset.get_style_choices(), {"default": "cinematic"}), }, "optional": { "position": (["suffix", "prefix", "wrap"], {"default": "suffix"}), "emphasis": (["medium", "low", "high"], {"default": "medium"}), "include_technical": ("BOOLEAN", {"default": True}), - } + }, } - - RETURN_TYPES = ("STRING", "STRING",) - RETURN_NAMES = ("styled_prompt", "style_keywords",) + + RETURN_TYPES = ( + "STRING", + "STRING", + ) + RETURN_NAMES = ( + "styled_prompt", + "style_keywords", + ) FUNCTION = "apply_style" CATEGORY = "text/generation" - + def apply_style( self, prompt: str, style: str, position: str = "suffix", emphasis: str = "medium", - include_technical: bool = True + include_technical: bool = True, ) -> Tuple[str, str]: """Apply style keywords to a prompt.""" from style_presets import StylePreset @@ -66,14 +75,15 @@ class StyleApplierNode: # Validate style available_styles = StylePreset.get_style_choices() if style not in available_styles: - return (f"[StyleApplier] Error: Unknown style '{style}'. Available: {available_styles}", "") + return ( + f"[StyleApplier] Error: Unknown style '{style}'. Available: {available_styles}", + "", + ) # Get style keywords try: style_keywords = StylePreset().get_style_prompt( - style=style, - emphasis=emphasis, - include_technical=include_technical + style=style, emphasis=emphasis, include_technical=include_technical ) except ValueError as e: return (f"[StyleApplier] Error getting style: {e}", "") diff --git a/social/2026-02-16_SOCIAL_v1.1.6-bugfix-posts.md b/social/2026-02-16_SOCIAL_v1.1.6-bugfix-posts.md new file mode 100644 index 0000000..6a2d57f --- /dev/null +++ b/social/2026-02-16_SOCIAL_v1.1.6-bugfix-posts.md @@ -0,0 +1,36 @@ +# Social Media Posts - v1.1.6 Bug Fix Release + +## LinkedIn Post + +--- + +**Fixed the 120s timeout issue in ComfyUI Prompt Generator (v1.1.6)** + +If you've been hitting "Generation Timed Out 120s" errors with the ComfyUI Prompt Generator node - this is now resolved. + +The root cause: the blocking `ollama.generate()` call had no way to handle slow model responses or cold starts gracefully. + +What changed in v1.1.6: +- Replaced blocking API call with streaming + per-chunk timeouts +- Added cold start detection with automatic timeout multiplier +- Integrated ComfyUI ProgressBar so you can see generation progress in real time + +Update via ComfyUI Manager or pull the latest from GitHub. + +https://github.com/Limbicnation/ComfyUI-PromptGenerator + +#ComfyUI #StableDiffusion #Ollama #AIArt #OpenSource #BugFix + +--- + +## YouTube Reply to @luci0c4s3 + +--- + +> @luci0c4s3: "Getting Generation Timed 120s as an error each time I run the node." + +**Reply:** + +Hey! This was a known issue and it's been fixed in v1.1.6. The blocking Ollama call was replaced with streaming + per-chunk timeouts, which eliminates the 120s timeout error. Cold starts also get extra time automatically now. Update via ComfyUI Manager or grab the latest from GitHub: https://github.com/Limbicnation/ComfyUI-PromptGenerator + +--- diff --git a/style_presets.py b/style_presets.py index 499c07a..049b5ff 100644 --- a/style_presets.py +++ b/style_presets.py @@ -1,13 +1,13 @@ """ Style Presets Module for ComfyUI -Modular style system for prompt generation with Cinematic and Still Image modes. +Modular style system for prompt generation with keyword sets for each style. Usage in ComfyUI nodes: from style_presets import StylePreset - + # Get style choices for INPUT_TYPES dropdown - choices = StylePreset.get_style_choices() # ("cinematic", "still_image") - + choices = StylePreset.get_style_choices() + # Get style keywords as a prompt string preset = StylePreset() style_prompt = preset.get_style_prompt("cinematic") @@ -20,38 +20,54 @@ from enum import Enum class StyleMode(Enum): """Enumeration of available style modes.""" + CINEMATIC = "cinematic" STILL_IMAGE = "still_image" + ANIME = "anime" + PHOTOREALISTIC = "photorealistic" + FANTASY = "fantasy" + ABSTRACT = "abstract" + CYBERPUNK = "cyberpunk" + SCI_FI = "sci-fi" + VIDEO_WAN = "video_wan" @dataclass class StyleKeywords: """Container for style-specific keywords and descriptors.""" + primary: List[str] = field(default_factory=list) lighting: List[str] = field(default_factory=list) technical: List[str] = field(default_factory=list) composition: List[str] = field(default_factory=list) texture: List[str] = field(default_factory=list) - + def to_prompt_string(self, separator: str = ", ") -> str: """Convert all keywords to a single prompt string.""" all_keywords = ( - self.primary + self.lighting + self.technical + - self.composition + self.texture + self.primary + + self.lighting + + self.technical + + self.composition + + self.texture ) return separator.join(all_keywords) - + def to_list(self) -> List[str]: """Return all keywords as a flat list (ComfyUI-compatible).""" return ( - self.primary + self.lighting + self.technical + - self.composition + self.texture + self.primary + + self.lighting + + self.technical + + self.composition + + self.texture ) @dataclass class StyleDefinition: """Complete style definition with metadata and keywords.""" + name: str description: str keywords: StyleKeywords @@ -64,23 +80,251 @@ STYLE_DEFINITIONS: Dict[StyleMode, StyleDefinition] = { description="Film-like visuals with dramatic lighting and anamorphic qualities", keywords=StyleKeywords( primary=["cinematic shot", "film grain", "movie still", "dramatic scene"], - lighting=["dramatic lighting", "volumetric light", "rim lighting", "chiaroscuro", "golden hour"], - technical=["anamorphic lens", "shallow depth of field", "bokeh", "35mm film", "wide aspect ratio"], - composition=["rule of thirds", "leading lines", "dynamic composition", "cinematic framing"], - texture=["rich color grading", "film texture", "atmospheric haze"] - ) + lighting=[ + "dramatic lighting", + "volumetric light", + "rim lighting", + "chiaroscuro", + "golden hour", + ], + technical=[ + "anamorphic lens", + "shallow depth of field", + "bokeh", + "35mm film", + "wide aspect ratio", + ], + composition=[ + "rule of thirds", + "leading lines", + "dynamic composition", + "cinematic framing", + ], + texture=["rich color grading", "film texture", "atmospheric haze"], + ), ), StyleMode.STILL_IMAGE: StyleDefinition( name="Still Image (Photography)", description="Sharp, realistic photography with technical camera specifications", keywords=StyleKeywords( - primary=["professional photography", "high resolution", "sharp focus", "studio quality"], - lighting=["natural lighting", "soft diffused light", "studio lighting", "balanced exposure"], - technical=["f/2.8 aperture", "ISO 100", "sharp optics", "full frame sensor", "RAW quality"], - composition=["centered composition", "clean framing", "balanced layout", "professional angle"], - texture=["realistic textures", "fine details", "crisp definition", "accurate colors"] - ) - ) + primary=[ + "professional photography", + "high resolution", + "sharp focus", + "studio quality", + ], + lighting=[ + "natural lighting", + "soft diffused light", + "studio lighting", + "balanced exposure", + ], + technical=[ + "f/2.8 aperture", + "ISO 100", + "sharp optics", + "full frame sensor", + "RAW quality", + ], + composition=[ + "centered composition", + "clean framing", + "balanced layout", + "professional angle", + ], + texture=[ + "realistic textures", + "fine details", + "crisp definition", + "accurate colors", + ], + ), + ), + StyleMode.ANIME: StyleDefinition( + name="Anime", + description="Vibrant anime-style illustration with dynamic colors", + keywords=StyleKeywords( + primary=[ + "anime style", + "manga illustration", + "cel shading", + "vibrant colors", + ], + lighting=[ + "soft glow", + "dramatic backlighting", + "ambient light", + "bloom effect", + ], + technical=[ + "clean linework", + "flat color areas", + "high contrast", + "detailed eyes", + ], + composition=[ + "dynamic pose", + "expressive composition", + "layered background", + ], + texture=["smooth gradients", "soft skin tones", "vivid saturation"], + ), + ), + StyleMode.PHOTOREALISTIC: StyleDefinition( + name="Photorealistic", + description="High-detail realistic images with natural lighting", + keywords=StyleKeywords( + primary=[ + "photorealistic", + "ultra realistic", + "lifelike detail", + "high fidelity", + ], + lighting=[ + "natural lighting", + "golden hour", + "soft shadows", + "ambient occlusion", + ], + technical=[ + "DSLR quality", + "85mm lens", + "shallow depth of field", + "8K resolution", + ], + composition=["rule of thirds", "natural framing", "environmental portrait"], + texture=[ + "realistic skin texture", + "fine material detail", + "natural imperfections", + ], + ), + ), + StyleMode.FANTASY: StyleDefinition( + name="Fantasy", + description="Magical elements and themes with ethereal atmosphere", + keywords=StyleKeywords( + primary=["fantasy art", "magical scene", "mythical", "enchanted"], + lighting=[ + "ethereal glow", + "mystical light rays", + "bioluminescence", + "aurora", + ], + technical=["digital painting", "matte painting", "concept art quality"], + composition=[ + "epic scale", + "layered depth", + "grand vista", + "ornate framing", + ], + texture=[ + "iridescent surfaces", + "crystalline detail", + "ancient stonework", + "enchanted flora", + ], + ), + ), + StyleMode.ABSTRACT: StyleDefinition( + name="Abstract", + description="Artistic abstract compositions with creative expression", + keywords=StyleKeywords( + primary=["abstract art", "non-representational", "artistic composition"], + lighting=["color field lighting", "gradient transitions", "luminous forms"], + technical=["mixed media", "generative art", "high dynamic range"], + composition=[ + "visual rhythm", + "asymmetric balance", + "flowing forms", + "geometric patterns", + ], + texture=[ + "paint strokes", + "textured layers", + "organic forms", + "splatter effects", + ], + ), + ), + StyleMode.CYBERPUNK: StyleDefinition( + name="Cyberpunk", + description="Neon lights, high technology, and urban dystopia", + keywords=StyleKeywords( + primary=[ + "cyberpunk", + "neon noir", + "dystopian future", + "high tech low life", + ], + lighting=[ + "neon glow", + "holographic reflections", + "LED strips", + "rain-soaked neon", + ], + technical=[ + "ray tracing", + "volumetric fog", + "chromatic aberration", + "lens flare", + ], + composition=[ + "urban canyon", + "vertical composition", + "dense layering", + "vanishing point", + ], + texture=[ + "wet asphalt reflections", + "rust and chrome", + "holographic surfaces", + "circuit patterns", + ], + ), + ), + StyleMode.SCI_FI: StyleDefinition( + name="Sci-Fi", + description="Futuristic technology and space exploration scenes", + keywords=StyleKeywords( + primary=[ + "science fiction", + "futuristic", + "space opera", + "advanced technology", + ], + lighting=["starlight", "plasma glow", "engine flare", "atmospheric entry"], + technical=[ + "hard surface modeling", + "concept art", + "matte painting", + "photobashing", + ], + composition=[ + "epic scale", + "orbital view", + "dramatic perspective", + "vast emptiness", + ], + texture=[ + "polished metal", + "energy fields", + "alien materials", + "hull plating", + ], + ), + ), + StyleMode.VIDEO_WAN: StyleDefinition( + name="Video (WanVideo)", + description="Minimalist keywords optimized for WanVideo LoRA", + keywords=StyleKeywords( + primary=["video shot", "motion capture", "fluid movement"], + lighting=["cinematic lighting", "natural light"], + technical=["smooth motion", "24fps", "high resolution video"], + composition=["dynamic camera", "tracking shot"], + texture=["temporal consistency", "clean frames"], + ), + ), } @@ -101,40 +345,37 @@ def get_available_styles() -> List[str]: class StylePreset: """ComfyUI-compatible style preset class.""" - + def __init__(self): self._styles = STYLE_DEFINITIONS - + @staticmethod def get_style_choices() -> Tuple[str, ...]: """Get available style choices as tuple (ComfyUI dropdown format).""" return tuple(mode.value for mode in StyleMode) - + def get_style_keywords(self, style: str) -> List[str]: """Get style keywords as a list.""" return get_style_keywords(style).to_list() - + def get_style_prompt( - self, - style: str, - emphasis: Optional[str] = None, - include_technical: bool = True + self, style: str, emphasis: Optional[str] = None, include_technical: bool = True ) -> str: """Get a formatted style prompt string.""" keywords = get_style_keywords(style) - + parts = [] parts.extend(keywords.primary) parts.extend(keywords.lighting) parts.extend(keywords.composition) parts.extend(keywords.texture) - + if include_technical: parts.extend(keywords.technical) - + if emphasis: parts.insert(0, f"emphasis on {emphasis}") - + return ", ".join(parts) @@ -145,6 +386,5 @@ __all__ = [ "StylePreset", "get_style_keywords", "get_available_styles", - "STYLE_DEFINITIONS" + "STYLE_DEFINITIONS", ] -