Extract server route payload handlers

This commit is contained in:
2026-06-27 02:39:31 +02:00
parent ab2a13ecde
commit 0eada863d8
5 changed files with 174 additions and 45 deletions
+17 -44
View File
@@ -397,10 +397,6 @@ try:
from .loop_nodes import ( from .loop_nodes import (
LOOP_NODE_CLASS_MAPPINGS, LOOP_NODE_CLASS_MAPPINGS,
LOOP_NODE_DISPLAY_NAME_MAPPINGS, LOOP_NODE_DISPLAY_NAME_MAPPINGS,
accumulator_delete_entries,
accumulator_list_entries,
accumulator_move_entry,
accumulator_save_entries,
) )
from .node_camera import ( from .node_camera import (
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
@@ -438,17 +434,17 @@ try:
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
) )
from .prompt_builder import ( from .server_routes import (
save_character_profile_payload, accumulator_delete_payload,
accumulator_list_payload,
accumulator_move_payload,
accumulator_save_payload,
profile_save_cached_payload,
) )
except ImportError: except ImportError:
from loop_nodes import ( from loop_nodes import (
LOOP_NODE_CLASS_MAPPINGS, LOOP_NODE_CLASS_MAPPINGS,
LOOP_NODE_DISPLAY_NAME_MAPPINGS, LOOP_NODE_DISPLAY_NAME_MAPPINGS,
accumulator_delete_entries,
accumulator_list_entries,
accumulator_move_entry,
accumulator_save_entries,
) )
from node_camera import ( from node_camera import (
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
@@ -486,8 +482,12 @@ except ImportError:
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
) )
from prompt_builder import ( from server_routes import (
save_character_profile_payload, accumulator_delete_payload,
accumulator_list_payload,
accumulator_move_payload,
accumulator_save_payload,
profile_save_cached_payload,
) )
@@ -496,10 +496,7 @@ if PromptServer is not None and web is not None:
async def sxcp_save_cached_profile(request): async def sxcp_save_cached_profile(request):
try: try:
payload = await request.json() payload = await request.json()
result = save_character_profile_payload( result = profile_save_cached_payload(payload)
profile_name=str(payload.get("profile_name") or ""),
profile_json=payload.get("profile_json") or "",
)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
return web.json_response({"error": str(exc)}, status=400) return web.json_response({"error": str(exc)}, status=400)
@@ -508,10 +505,7 @@ if PromptServer is not None and web is not None:
async def sxcp_accumulator_list(request): async def sxcp_accumulator_list(request):
try: try:
payload = await request.json() payload = await request.json()
result = accumulator_list_entries( result = accumulator_list_payload(payload)
str(payload.get("store_key") or ""),
preview_limit=int(payload.get("preview_limit") or 0),
)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
return web.json_response({"error": str(exc)}, status=400) return web.json_response({"error": str(exc)}, status=400)
@@ -520,14 +514,7 @@ if PromptServer is not None and web is not None:
async def sxcp_accumulator_delete(request): async def sxcp_accumulator_delete(request):
try: try:
payload = await request.json() payload = await request.json()
result = accumulator_delete_entries( result = accumulator_delete_payload(payload)
store_key=str(payload.get("store_key") or ""),
preview_key=str(payload.get("preview_key") or ""),
entry_id=str(payload.get("entry_id") or ""),
index=int(payload.get("index") or 0),
clear=bool(payload.get("clear")),
preview_limit=int(payload.get("preview_limit") or 0),
)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
return web.json_response({"error": str(exc)}, status=400) return web.json_response({"error": str(exc)}, status=400)
@@ -536,13 +523,7 @@ if PromptServer is not None and web is not None:
async def sxcp_accumulator_save(request): async def sxcp_accumulator_save(request):
try: try:
payload = await request.json() payload = await request.json()
result = accumulator_save_entries( result = accumulator_save_payload(payload)
store_key=str(payload.get("store_key") or ""),
save_path=str(payload.get("save_path") or "sxcp_accumulator"),
filename_prefix=str(payload.get("filename_prefix") or "sxcp_accum"),
clear_after_save=bool(payload.get("clear_after_save")),
preview_limit=int(payload.get("preview_limit") or 0),
)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
return web.json_response({"error": str(exc)}, status=400) return web.json_response({"error": str(exc)}, status=400)
@@ -551,15 +532,7 @@ if PromptServer is not None and web is not None:
async def sxcp_accumulator_move(request): async def sxcp_accumulator_move(request):
try: try:
payload = await request.json() payload = await request.json()
result = accumulator_move_entry( result = accumulator_move_payload(payload)
store_key=str(payload.get("store_key") or ""),
preview_key=str(payload.get("preview_key") or ""),
entry_id=str(payload.get("entry_id") or ""),
index=int(payload.get("index") or 0),
direction=str(payload.get("direction") or "up"),
target_index=int(payload.get("target_index") or 0),
preview_limit=int(payload.get("preview_limit") or 0),
)
return web.json_response(result) return web.json_response(result)
except Exception as exc: except Exception as exc:
return web.json_response({"error": str(exc)}, status=400) return web.json_response({"error": str(exc)}, status=400)
+5 -1
View File
@@ -416,13 +416,17 @@ Already isolated:
by `__init__.py`. by `__init__.py`.
- generation profile, advanced filter, and ethnicity list utility nodes live in - generation profile, advanced filter, and ethnicity list utility nodes live in
`node_profile_filter.py`, with registration maps imported by `__init__.py`. `node_profile_filter.py`, with registration maps imported by `__init__.py`.
- profile-save and accumulator server payload handling lives in
`server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI
JSON responses, and `tools/prompt_smoke.py` covers the handlers without
importing ComfyUI.
Improve later: Improve later:
- split remaining large node classes into files by family; - split remaining large node classes into files by family;
- keep node display names, return names, and docs in sync through the audit - keep node display names, return names, and docs in sync through the audit
helper; helper;
- add small endpoint tests for profile/accumulator/index-switch routes. - add more endpoint tests when new server routes are introduced.
## Path-Specific Improvements ## Path-Specific Improvements
+4
View File
@@ -98,6 +98,7 @@ Core helper ownership:
| `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. |
| `row_normalization.py` | Final prompt-row and pair metadata normalization: trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. | | `row_normalization.py` | Final prompt-row and pair metadata normalization: trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. |
| `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. | | `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. |
| `server_routes.py` | Pure payload handlers for profile-save and accumulator server endpoints, used by ComfyUI routes and smoke tests without importing ComfyUI. |
| `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. | | `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. |
| `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. | | `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. |
@@ -813,6 +814,9 @@ pair metadata through the core Python APIs, then verifies:
- static formatter metadata fixtures keep source-provided action families - static formatter metadata fixtures keep source-provided action families
stable across Krea2 prose, SDXL tags, and natural captions even when raw item stable across Krea2 prose, SDXL tags, and natural captions even when raw item
text contains distracting wording. text contains distracting wording.
- profile-save and accumulator endpoint payload handlers are smoke-tested
without importing ComfyUI, and the reversible index switch keeps pick/input
and route/output behavior stable.
## Editing Cheatsheet ## Editing Cheatsheet
+76
View File
@@ -0,0 +1,76 @@
from __future__ import annotations
from typing import Any
try:
from .loop_nodes import (
accumulator_delete_entries,
accumulator_list_entries,
accumulator_move_entry,
accumulator_save_entries,
)
from .prompt_builder import save_character_profile_payload
except ImportError: # Allows local smoke tests from the repository root.
from loop_nodes import (
accumulator_delete_entries,
accumulator_list_entries,
accumulator_move_entry,
accumulator_save_entries,
)
from prompt_builder import save_character_profile_payload
def _payload(payload: Any) -> dict[str, Any]:
return payload if isinstance(payload, dict) else {}
def profile_save_cached_payload(payload: Any) -> dict[str, Any]:
data = _payload(payload)
return save_character_profile_payload(
profile_name=str(data.get("profile_name") or ""),
profile_json=data.get("profile_json") or "",
)
def accumulator_list_payload(payload: Any) -> dict[str, Any]:
data = _payload(payload)
return accumulator_list_entries(
str(data.get("store_key") or ""),
preview_limit=int(data.get("preview_limit") or 0),
)
def accumulator_delete_payload(payload: Any) -> dict[str, Any]:
data = _payload(payload)
return accumulator_delete_entries(
store_key=str(data.get("store_key") or ""),
preview_key=str(data.get("preview_key") or ""),
entry_id=str(data.get("entry_id") or ""),
index=int(data.get("index") or 0),
clear=bool(data.get("clear")),
preview_limit=int(data.get("preview_limit") or 0),
)
def accumulator_save_payload(payload: Any) -> dict[str, Any]:
data = _payload(payload)
return accumulator_save_entries(
store_key=str(data.get("store_key") or ""),
save_path=str(data.get("save_path") or "sxcp_accumulator"),
filename_prefix=str(data.get("filename_prefix") or "sxcp_accum"),
clear_after_save=bool(data.get("clear_after_save")),
preview_limit=int(data.get("preview_limit") or 0),
)
def accumulator_move_payload(payload: Any) -> dict[str, Any]:
data = _payload(payload)
return accumulator_move_entry(
store_key=str(data.get("store_key") or ""),
preview_key=str(data.get("preview_key") or ""),
entry_id=str(data.get("entry_id") or ""),
index=int(data.get("index") or 0),
direction=str(data.get("direction") or "up"),
target_index=int(data.get("target_index") or 0),
preview_limit=int(data.get("preview_limit") or 0),
)
+72
View File
@@ -14,6 +14,7 @@ import json
import random import random
import re import re
import sys import sys
import tempfile
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
@@ -38,9 +39,11 @@ import generation_profile_config # noqa: E402
import krea_cast # noqa: E402 import krea_cast # noqa: E402
import krea_formatter # noqa: E402 import krea_formatter # noqa: E402
import location_config # noqa: E402 import location_config # noqa: E402
import loop_nodes # noqa: E402
import prompt_builder as pb # noqa: E402 import prompt_builder as pb # noqa: E402
import row_normalization # noqa: E402 import row_normalization # noqa: E402
import route_metadata # noqa: E402 import route_metadata # noqa: E402
import server_routes # noqa: E402
import sdxl_formatter # noqa: E402 import sdxl_formatter # noqa: E402
import sdxl_presets # noqa: E402 import sdxl_presets # noqa: E402
import seed_config # noqa: E402 import seed_config # noqa: E402
@@ -2643,6 +2646,74 @@ def smoke_node_utility_registration() -> None:
_expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch") _expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch")
def smoke_server_route_payload_policy() -> None:
switch = loop_nodes.SxCPIndexSwitch()
picked = switch.switch(
2,
"pick_input",
"one_based",
"fallback",
input_1="first",
input_2="second",
fallback="fallback",
)
_expect(picked[0] == "second", "Index Switch pick_input did not select the requested input")
_expect(picked[1] == 2, "Index Switch pick_input selected_index changed")
_expect("selected=input_2" in picked[2], "Index Switch pick_input status lost selected input")
routed = switch.switch(3, "route_output", "one_based", "fallback", route_value="routed")
_expect(routed[0] == "routed", "Index Switch route_output primary value changed")
_expect(routed[1] == 3, "Index Switch route_output selected_index changed")
_expect(routed[5] == "routed", "Index Switch route_output did not route value to output_3")
key = "smoke_route_payload"
loop_nodes._ACCUMULATOR_STORES[key] = [
{"id": "first", "value": "alpha", "_sxcp_preview_key": "first-key"},
{"id": "second", "value": "beta", "_sxcp_preview_key": "second-key"},
]
try:
listed = server_routes.accumulator_list_payload({"store_key": key, "preview_limit": "0"})
_expect(listed.get("count") == 2, "Accumulator list payload lost stored entries")
_expect(listed["entries"][0].get("value") == "alpha", "Accumulator list payload lost value summary")
moved = server_routes.accumulator_move_payload({"store_key": key, "entry_id": "second", "target_index": "1"})
_expect(moved.get("moved") is True, "Accumulator move payload did not report movement")
_expect(moved.get("from_index") == 2 and moved.get("to_index") == 1, "Accumulator move payload changed indices")
_expect(moved["entries"][0].get("id") == "second", "Accumulator move payload did not reorder entries")
deleted = server_routes.accumulator_delete_payload({"store_key": key, "preview_key": "first-key"})
_expect(deleted.get("removed") == 1, "Accumulator delete payload did not remove by preview key")
_expect(deleted.get("count") == 1, "Accumulator delete payload count changed")
cleared = server_routes.accumulator_delete_payload({"store_key": key, "clear": True})
_expect(cleared.get("removed") == 1 and cleared.get("count") == 0, "Accumulator clear payload changed")
finally:
loop_nodes._ACCUMULATOR_STORES.pop(key, None)
with tempfile.TemporaryDirectory() as tmpdir:
previous_profile_dir = character_profile.PROFILE_DIR
character_profile.PROFILE_DIR = Path(tmpdir)
try:
profile = character_profile.build_character_profile_json(
profile_name="route source",
source="manual",
subject_type="woman",
age="28-year-old adult",
body="slim",
hair="long black hair",
save_now=False,
)
saved = server_routes.profile_save_cached_payload(
{"profile_name": "Route Save!*", "profile_json": profile["profile_json"]}
)
saved_path = Path(saved.get("saved_path") or "")
_expect(saved.get("status") == "saved", "Profile save payload did not save")
_expect(saved.get("profile_name") == "Route_Save", "Profile save payload did not sanitize requested name")
_expect(saved_path.exists(), "Profile save payload did not write profile file")
finally:
character_profile.PROFILE_DIR = previous_profile_dir
def smoke_seed_config_policy() -> None: def smoke_seed_config_policy() -> None:
_expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config") _expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config")
_expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config") _expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config")
@@ -3326,6 +3397,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("expression_disabled", smoke_no_expression_fallback), ("expression_disabled", smoke_no_expression_fallback),
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
("node_utility_registration", smoke_node_utility_registration), ("node_utility_registration", smoke_node_utility_registration),
("server_route_payload_policy", smoke_server_route_payload_policy),
("seed_config_policy", smoke_seed_config_policy), ("seed_config_policy", smoke_seed_config_policy),
("node_camera_registration", smoke_node_camera_registration), ("node_camera_registration", smoke_node_camera_registration),
("node_route_config_registration", smoke_node_route_config_registration), ("node_route_config_registration", smoke_node_route_config_registration),