Generate app summary PDF

This commit is contained in:
Nid All
2026-03-07 13:30:39 +01:00
parent 60e84c559c
commit 7c837d1ba4
7 changed files with 137 additions and 82 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "0.3.1"
".": "0.3.2"
}
+3 -4
View File
@@ -6,10 +6,10 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
## [Unreleased]
## [0.3.2] - 2026-03-07
### Added
- `PromptStyler: Advanced Prompt -> Conditioning` for search-based style resolution, color-grade overlays, style strength controls, and JSON debug metadata.
- Shared `style_library.py` used by the runtime node and maintainer tooling.
- CI checks for version sync, style validation, style audit, and runtime regression tests.
- `tools/sync_legacy_styles.py` and `tools/version_sync.py` for compatibility snapshot and release metadata maintenance.
- New medium-first style packs: `styles/packs/32_print_reprographic.json`, `styles/packs/42_decorative_arts_material_processes.json`, and `styles/packs/43_digital_craft.json`.
@@ -22,7 +22,7 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
- `PromptStyler: Prompt -> Conditioning (Style Picker)` now uses the narrow legacy-compatible prompt-to-conditioning contract again for stability.
- Broken or non-finite conditioning now raises an explicit node error instead of silently returning a malformed output.
- `styles/styles_v1.json` is now treated as a generated compatibility snapshot of merged packs.
- README and maintainer documentation now describe the stable/advanced node split.
- README and maintainer documentation now describe the restored stable-node runtime surface and release workflow.
## [0.3.1] - 2026-01-31
@@ -71,4 +71,3 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
### Changed
- README updates for new node input.
+1 -1
View File
@@ -1 +1 @@
0.3.1
0.3.2
Binary file not shown.
+44 -76
View File
@@ -6,39 +6,10 @@ import nodes
import style_library
class _FakeBool:
def __init__(self, value: bool):
self._value = value
def item(self) -> bool:
return self._value
class _FakeFiniteResult:
def __init__(self, value: bool):
self._value = value
def all(self) -> _FakeBool:
return _FakeBool(self._value)
class FakeTensor:
def __init__(self, finite: bool = True, size: int = 3):
self._finite = finite
self._size = size
def numel(self) -> int:
return self._size
def isfinite(self) -> _FakeFiniteResult:
return _FakeFiniteResult(self._finite)
class FakeTextEncoder:
def __init__(self, conditioning=None):
def __init__(self):
self.last_text = None
self.last_tokens = None
self._conditioning = conditioning
def tokenize(self, text):
self.last_text = text
@@ -46,9 +17,7 @@ class FakeTextEncoder:
return self.last_tokens
def encode_from_tokens_scheduled(self, tokens):
if self._conditioning is not None:
return self._conditioning
return [[FakeTensor(), {"source": tokens["text"]}]]
return [[tokens, {"source": tokens["text"]}]]
class PromptStylerTests(unittest.TestCase):
@@ -56,7 +25,6 @@ class PromptStylerTests(unittest.TestCase):
def setUpClass(cls):
cls.library = style_library.get_cached_style_library(load_policy=style_library.LOAD_POLICY_STRICT)
cls.stable_node = nodes.PromptStylerConditioning()
cls.advanced_node = nodes.PromptStylerAdvancedConditioning()
cls.primary_choice = next(choice for choice in cls.library.choices if choice.endswith("| cinematic_anamorphic"))
cls.secondary_choice = next(choice for choice in cls.library.choices if choice.endswith("| cin_film_noir"))
@@ -119,6 +87,26 @@ class PromptStylerTests(unittest.TestCase):
self.assertEqual(expected, encoder.last_text)
self.assertEqual(1, len(conditioning))
def test_flux_variant_uses_variant_text_when_present(self):
encoder = FakeTextEncoder()
conditioning, styled_prompt = self.stable_node.encode(
prompt="robot detective",
apply_style=True,
style=self.primary_choice,
template_variant="flux_2_klein",
style_id_override="",
text_encoder=encoder,
)
expected_style = self.library.by_id["cinematic_anamorphic"]
expected_prompt, _details = style_library.compose_prompt_legacy(
"robot detective",
expected_style,
template_variant="flux_2_klein",
)
self.assertEqual(expected_prompt, styled_prompt)
self.assertEqual(expected_prompt, encoder.last_text)
self.assertEqual(1, len(conditioning))
def test_missing_variant_falls_back_to_default(self):
style = self.library.by_id["cinematic_anamorphic"]
default_prompt, _default_details = style_library.compose_prompt_legacy(
@@ -134,49 +122,29 @@ class PromptStylerTests(unittest.TestCase):
self.assertEqual(default_prompt, fallback_prompt)
self.assertEqual(style_library.DEFAULT_VARIANT, details["variant"])
def test_advanced_node_matches_stable_node_for_simple_dropdown_flow(self):
stable_encoder = FakeTextEncoder()
advanced_encoder = FakeTextEncoder()
stable_conditioning, stable_prompt = self.stable_node.encode(
prompt="robot detective, rainy alley",
apply_style=True,
style=self.primary_choice,
template_variant="default",
style_id_override="",
text_encoder=stable_encoder,
)
advanced_conditioning, advanced_prompt, resolved_style_id, resolved_style_meta = self.advanced_node.encode(
prompt="robot detective, rainy alley",
apply_style=True,
style=self.primary_choice,
template_variant="default",
style_id_override="",
text_encoder=advanced_encoder,
selection_mode="dropdown",
style_search="",
category_hint="",
tag_hint="",
style_strength="strong",
dedupe_mode="smart",
overlay_style_id="",
on_missing_style="error",
)
self.assertEqual(stable_prompt, advanced_prompt)
self.assertEqual("cinematic_anamorphic", resolved_style_id)
self.assertIn('"status": "ok"', resolved_style_meta)
self.assertEqual(1, len(stable_conditioning))
self.assertEqual(1, len(advanced_conditioning))
def test_missing_text_encoder_raises_explicit_error(self):
with self.assertRaisesRegex(RuntimeError, "text_encoder input is invalid"):
self.stable_node.encode(
prompt="robot detective",
apply_style=True,
style=self.primary_choice,
template_variant="default",
style_id_override="",
text_encoder=None,
)
def test_invalid_conditioning_raises_explicit_error(self):
encoder = FakeTextEncoder(conditioning=[[FakeTensor(finite=False), {}]])
with self.assertRaisesRegex(RuntimeError, "non-finite conditioning"):
nodes.encode_conditioning(encoder, "robot detective")
def test_malformed_conditioning_raises_explicit_error(self):
encoder = FakeTextEncoder(conditioning=["bad"])
with self.assertRaisesRegex(RuntimeError, "invalid conditioning item"):
nodes.encode_conditioning(encoder, "robot detective")
def test_unknown_style_id_override_raises_error(self):
encoder = FakeTextEncoder()
with self.assertRaisesRegex(ValueError, "Unknown style_id_override"):
self.stable_node.encode(
prompt="robot detective",
apply_style=True,
style=self.primary_choice,
template_variant="default",
style_id_override="does_not_exist",
text_encoder=encoder,
)
if __name__ == "__main__":
unittest.main()
unittest.main()
+88
View File
@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ComfyUI PromptStyler - App Summary</title>
<style>
@page { size: Letter; margin: 0.42in; }
:root { --ink:#13212f; --muted:#526171; --rule:#d7dee7; --panel:#f4f7fa; --accent:#1f5f8b; --accent-soft:#dbeaf5; }
* { box-sizing: border-box; }
html, body { margin:0; padding:0; font-family:"Segoe UI", Arial, sans-serif; color:var(--ink); background:#fff; }
body { font-size:10.3pt; line-height:1.28; }
.hero { display:grid; grid-template-columns:1.55fr 0.95fr; gap:14px; margin-bottom:10px; padding-bottom:10px; border-bottom:1px solid var(--rule); }
.title { margin:0 0 4px 0; font-size:22pt; line-height:1.05; letter-spacing:-0.02em; }
.subtitle { margin:0; color:var(--muted); font-size:10pt; }
.badge { display:inline-block; margin-bottom:8px; padding:4px 8px; border-radius:999px; background:var(--accent-soft); color:var(--accent); font-size:8.5pt; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; }
.meta { background:linear-gradient(180deg, #f9fbfd 0%, var(--panel) 100%); border:1px solid var(--rule); border-radius:10px; padding:10px 11px; }
.meta-row { margin:0 0 8px 0; }
.meta-row:last-child { margin-bottom:0; }
.label { display:block; margin-bottom:2px; color:var(--muted); font-size:8.2pt; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; }
.value { margin:0; font-size:9.9pt; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:12px 14px; }
section { break-inside:avoid; }
h2 { margin:0 0 6px 0; padding-bottom:3px; border-bottom:1px solid var(--rule); font-size:10pt; letter-spacing:0.02em; text-transform:uppercase; }
p { margin:0 0 6px 0; }
ul { margin:0; padding-left:16px; }
li { margin:0 0 4px 0; }
.architecture { grid-column:1 / -1; }
.small { font-size:9.1pt; color:var(--muted); }
.code { font-family:Consolas, "Courier New", monospace; font-size:9pt; }
</style>
</head>
<body>
<div class="hero">
<div>
<div class="badge">Repo Summary</div>
<h1 class="title">ComfyUI PromptStyler</h1>
<p class="subtitle">Version 3.1.1. Custom ComfyUI node pack that applies curated style templates to a positive prompt and returns conditioning for downstream generation. It is structured around JSON style packs plus lightweight Python runtime code that composes the final prompt text at node execution time.</p>
</div>
<div class="meta">
<div class="meta-row">
<span class="label">Who It’s For</span>
<p class="value">Primary persona: ComfyUI users building image-generation workflows who want reusable, curated prompt styling without hand-writing long style suffixes.</p>
</div>
<div class="meta-row">
<span class="label">Install Shape</span>
<p class="value">Drop the repo into <span class="code">ComfyUI/custom_nodes/ComfyUI_PromptStyler</span> and reload custom nodes.</p>
</div>
</div>
</div>
<div class="grid">
<section>
<h2>What It Does</h2>
<ul>
<li>Exposes a ComfyUI node named <span class="code">PromptStyler: Prompt -&gt; Conditioning (Style Picker)</span>.</li>
<li>Loads style templates from <span class="code">styles/packs/*.json</span> in filename order.</li>
<li>Falls back to <span class="code">styles/styles_v1.json</span> if packs are missing or unusable.</li>
<li>Presents styles as categorized dropdown choices in <span class="code">Category | Name | id</span> format.</li>
<li>Supports direct <span class="code">style_id_override</span> and a <span class="code">template_variant</span> selector.</li>
<li>Builds a deduplicated styled prompt, then tokenizes and encodes it through the connected CLIP text encoder.</li>
<li>Ships maintainer tools to validate, audit, generate, sync, and add style packs.</li>
</ul>
</section>
<section>
<h2>How To Run</h2>
<ul>
<li>Clone or copy this folder into <span class="code">ComfyUI/custom_nodes/ComfyUI_PromptStyler</span>.</li>
<li>Restart ComfyUI or use "Reload custom nodes".</li>
<li>Add <span class="code">CheckpointLoaderSimple</span>, PromptStyler, and <span class="code">KSampler</span> to a workflow.</li>
<li>Wire <span class="code">CLIP -&gt; text_encoder</span>, your prompt text into <span class="code">prompt</span>, and <span class="code">positive -&gt; KSampler.positive</span>.</li>
<li>Optional repo validation: run <span class="code">tools/validate_styles.py</span>; audit with <span class="code">tools/audit_styles.py</span>.</li>
</ul>
</section>
<section class="architecture">
<h2>How It Works</h2>
<ul>
<li><span class="code">__init__.py</span> is the ComfyUI entry point: it reads <span class="code">VERSION</span> and exports node mappings.</li>
<li><span class="code">nodes.py</span> defines the runtime node that accepts the user prompt, style selection inputs, and a ComfyUI <span class="code">CLIP</span> text encoder connection.</li>
<li>The node scans <span class="code">styles/packs/</span>, merges JSON packs in sorted order, or falls back to the legacy single-file library.</li>
<li>Selected style prefix/suffix phrases are combined with the user prompt, normalized, and de-duplicated before encoding.</li>
<li>The encoded result is returned as <span class="code">CONDITIONING</span> plus the exact <span class="code">styled_prompt</span> string for inspection/debugging.</li>
<li><span class="code">style_library.py</span>, tests, and maintainer docs describe broader search/overlay logic, but an advanced node implementation was <strong>Not found in repo</strong> in the current <span class="code">nodes.py</span>.</li>
<li><span class="code">.github/workflows/ci.yml</span> runs version sync, style validation, style audit, and unit tests on pushes and pull requests.</li>
</ul>
<p class="small">If you need external services, hosted APIs, or a packaged installer: <strong>Not found in repo</strong>.</p>
</section>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB