Merge pull request #1 from Limbicnation/feature/style-applier-node
Feature/style applier node
This commit is contained in:
@@ -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,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,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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user