From aa8abd8d3fdfb9c33935894cde172ad66a464dee Mon Sep 17 00:00:00 2001 From: limbicnation Date: Sun, 14 Dec 2025 18:16:01 +0100 Subject: [PATCH 1/2] docs: add Publishing section with v1.0.4 registry info and deployment commands --- CLAUDE.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 181937d..3d330fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 From 45cebe95b4474056d1802197c53b25176c82d22a Mon Sep 17 00:00:00 2001 From: limbicnation Date: Wed, 28 Jan 2026 01:59:17 +0100 Subject: [PATCH 2/2] feat(nodes): add StyleApplierNode and still_image style (v1.0.5) - Add StyleApplierNode for Cinematic/Still Image style keywords - Add style_presets.py with keyword-based style system - Add still_image to PromptGeneratorNode dropdown and templates - Update templates.yaml with still_image template - Bump version to 1.0.5 --- __init__.py | 4 + config/templates.yaml | 20 +++++ nodes/style_applier_node.py | 93 ++++++++++++++++++++++ pyproject.toml | 2 +- style_presets.py | 150 ++++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 nodes/style_applier_node.py create mode 100644 style_presets.py diff --git a/__init__.py b/__init__.py index f4d09b0..e3d57f1 100644 --- a/__init__.py +++ b/__init__.py @@ -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"] + diff --git a/config/templates.yaml b/config/templates.yaml index 2ef2655..3987668 100644 --- a/config/templates.yaml +++ b/config/templates.yaml @@ -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" diff --git a/nodes/style_applier_node.py b/nodes/style_applier_node.py new file mode 100644 index 0000000..3f4f340 --- /dev/null +++ b/nodes/style_applier_node.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 89ccf00..612d01c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/style_presets.py b/style_presets.py new file mode 100644 index 0000000..499c07a --- /dev/null +++ b/style_presets.py @@ -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" +] +