Merge pull request #1 from Limbicnation/feature/style-applier-node

Feature/style applier node
This commit is contained in:
Gero Doll
2026-01-28 02:13:52 +01:00
committed by GitHub
6 changed files with 296 additions and 1 deletions
+28
View File
@@ -17,6 +17,7 @@ config/
```
**Single node design**: All functionality is in `PromptGeneratorNode`. The node:
1. Loads style templates from YAML (or uses hardcoded defaults)
2. Renders the selected template with Jinja2 (or regex fallback)
3. Calls Ollama via Python API (or subprocess fallback)
@@ -27,15 +28,18 @@ config/
## Development
**Prerequisites**:
- Ollama running locally with `qwen3:8b` model pulled
- ComfyUI installation for integration testing
**Install dependencies**:
```bash
pip install -r requirements.txt
```
**Lint with ruff** (already configured in project):
```bash
ruff check .
ruff format .
@@ -46,6 +50,7 @@ ruff format .
## Key Patterns
**ComfyUI node structure**:
- `INPUT_TYPES()`: Class method returning dict with `required` and `optional` inputs
- `RETURN_TYPES`, `RETURN_NAMES`: Output type definitions
- `FUNCTION`: Name of the method to call (`generate`)
@@ -55,6 +60,29 @@ ruff format .
**Output cleaning**: `extract_final_prompt()` strips Qwen3's "Thinking..." blocks and markdown formatting from responses.
## Publishing
**Current version**: `1.0.4` (Published 2024-12-14)
**Registry**: [registry.comfy.org](https://registry.comfy.org) - Node ID: `comfyui-prompt-generator`
**Publish a new version**:
```bash
# 1. Update version in pyproject.toml
# 2. Commit changes
git add -A && git commit -m "chore: bump version to X.Y.Z"
# 3. Tag and push (triggers GitHub Actions workflow)
git tag vX.Y.Z
git push origin main && git push origin vX.Y.Z
# Or publish manually:
comfy node publish --confirm
```
**CI/CD**: `.github/workflows/publish.yml` auto-publishes on version tags (`v*.*.*`).
## Adding New Styles
1. Add entry to `config/templates.yaml` following existing format
+4
View File
@@ -4,13 +4,17 @@ Generate Stable Diffusion prompts using Qwen3-8B via Ollama
"""
from .nodes.prompt_generator_node import PromptGeneratorNode
from .nodes.style_applier_node import StyleApplierNode
NODE_CLASS_MAPPINGS = {
"PromptGenerator": PromptGeneratorNode,
"StyleApplier": StyleApplierNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptGenerator": "🎨 Prompt Generator (Qwen)",
"StyleApplier": "🎬 Style Applier (Cinematic/Photo)",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+20
View File
@@ -20,6 +20,26 @@ cinematic:
Format the response as a single, detailed prompt.
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.
anime:
name: "Anime"
description: "Vibrant anime-style illustration with dynamic colors"
+93
View File
@@ -0,0 +1,93 @@
"""
Style Applier Node for ComfyUI
Applies Cinematic or Still Image style keywords to prompts.
"""
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..."
}),
"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",)
FUNCTION = "apply_style"
CATEGORY = "text/generation"
def apply_style(
self,
prompt: str,
style: str,
position: str = "suffix",
emphasis: str = "medium",
include_technical: bool = True
) -> Tuple[str, str]:
"""Apply style keywords to a prompt."""
from style_presets import StylePreset
# Normalize inputs
prompt = prompt.strip() if prompt else ""
style = style.strip() if style else "cinematic"
# Validate style
available_styles = StylePreset.get_style_choices()
if style not in 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
)
except ValueError as e:
return (f"[StyleApplier] Error getting style: {e}", "")
# Handle empty prompt
if not prompt:
return (style_keywords, style_keywords)
# Apply style based on position
if position == "prefix":
styled_prompt = f"{style_keywords}, {prompt}"
elif position == "suffix":
styled_prompt = f"{prompt}, {style_keywords}"
else: # wrap
styled_prompt = f"{style_keywords}, {prompt}, {style_keywords}"
return (styled_prompt, style_keywords)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-prompt-generator"
description = "Generate Stable Diffusion prompts using Qwen3-8B via Ollama with 7 style presets"
version = "1.0.4"
version = "1.0.5"
license = {file = "LICENSE"}
readme = "README.md"
requires-python = ">=3.10"
+150
View File
@@ -0,0 +1,150 @@
"""
Style Presets Module for ComfyUI
Modular style system for prompt generation with Cinematic and Still Image modes.
Usage in ComfyUI nodes:
from style_presets import StylePreset
# Get style choices for INPUT_TYPES dropdown
choices = StylePreset.get_style_choices() # ("cinematic", "still_image")
# Get style keywords as a prompt string
preset = StylePreset()
style_prompt = preset.get_style_prompt("cinematic")
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from enum import Enum
class StyleMode(Enum):
"""Enumeration of available style modes."""
CINEMATIC = "cinematic"
STILL_IMAGE = "still_image"
@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
)
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
)
@dataclass
class StyleDefinition:
"""Complete style definition with metadata and keywords."""
name: str
description: str
keywords: StyleKeywords
# Style definitions - single source of truth
STYLE_DEFINITIONS: Dict[StyleMode, StyleDefinition] = {
StyleMode.CINEMATIC: StyleDefinition(
name="Cinematic",
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"]
)
),
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"]
)
)
}
def get_style_keywords(style: str) -> StyleKeywords:
"""Get keywords for a specific style."""
try:
mode = StyleMode(style.lower())
return STYLE_DEFINITIONS[mode].keywords
except ValueError:
available = [s.value for s in StyleMode]
raise ValueError(f"Unknown style '{style}'. Available: {', '.join(available)}")
def get_available_styles() -> List[str]:
"""Get list of available style names."""
return [mode.value for mode in StyleMode]
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
) -> 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)
__all__ = [
"StyleMode",
"StyleKeywords",
"StyleDefinition",
"StylePreset",
"get_style_keywords",
"get_available_styles",
"STYLE_DEFINITIONS"
]