Initial commit — ComfyUI Discord bot + web UI
Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI (React/TS/Vite/Tailwind), ComfyUI integration, generation history DB, preset manager, workflow inspector, and all supporting modules. Excluded from tracking: .env, invite_tokens.json, *.db (SQLite), current-workflow-changes.json, user_settings/, presets/, logs/, web-static/ (build output), frontend/node_modules/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
206
preset_manager.py
Normal file
206
preset_manager.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
preset_manager.py
|
||||
=================
|
||||
|
||||
Preset management for the Discord ComfyUI bot.
|
||||
|
||||
A preset is a named snapshot of the current workflow template and runtime
|
||||
state (prompt, negative_prompt, input_image, seed). Presets are stored as
|
||||
individual JSON files inside a ``presets/`` directory so they can be
|
||||
inspected, backed up, or shared manually.
|
||||
|
||||
Usage via bot commands:
|
||||
ttr!preset save <name> — capture current workflow + state
|
||||
ttr!preset load <name> — restore workflow + state from snapshot
|
||||
ttr!preset list — list all saved presets
|
||||
ttr!preset delete <name> — permanently remove a preset
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Only allow alphanumeric characters, hyphens, and underscores in preset names.
|
||||
_SAFE_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
|
||||
|
||||
|
||||
class PresetManager:
|
||||
"""
|
||||
Manages named workflow presets on disk.
|
||||
|
||||
Each preset is stored as ``<presets_dir>/<name>.json`` and contains:
|
||||
- ``name``: the preset name
|
||||
- ``workflow``: the full ComfyUI workflow template dict (may be null)
|
||||
- ``state``: the runtime state changes (prompt, negative_prompt,
|
||||
input_image, seed)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
presets_dir : str
|
||||
Directory where preset files are stored. Created automatically if
|
||||
it does not exist.
|
||||
"""
|
||||
|
||||
def __init__(self, presets_dir: str = "presets") -> None:
|
||||
self.presets_dir = Path(presets_dir)
|
||||
self.presets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def is_valid_name(name: str) -> bool:
|
||||
"""Return True if the name only contains safe characters."""
|
||||
return bool(_SAFE_NAME_RE.match(name))
|
||||
|
||||
def _path(self, name: str) -> Path:
|
||||
"""Return the file path for a preset by name (no validation)."""
|
||||
return self.presets_dir / f"{name}.json"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def save(
|
||||
self,
|
||||
name: str,
|
||||
workflow_template: Optional[dict[str, Any]],
|
||||
state: dict[str, Any],
|
||||
owner: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Save a preset to disk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The preset name (alphanumeric, hyphens, underscores only).
|
||||
workflow_template : Optional[dict]
|
||||
The current workflow template, or None if none is loaded.
|
||||
state : dict
|
||||
The current runtime state from WorkflowStateManager.get_changes().
|
||||
owner : Optional[str]
|
||||
The user label of the preset creator. Stored for access control.
|
||||
description : Optional[str]
|
||||
A human-readable description of what this preset does.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the name contains invalid characters.
|
||||
OSError
|
||||
If the file cannot be written.
|
||||
"""
|
||||
if not self.is_valid_name(name):
|
||||
raise ValueError(
|
||||
f"Invalid preset name '{name}'. "
|
||||
"Use only letters, digits, hyphens, and underscores (max 64 chars)."
|
||||
)
|
||||
data: dict[str, Any] = {"name": name, "workflow": workflow_template, "state": state}
|
||||
if owner is not None:
|
||||
data["owner"] = owner
|
||||
if description is not None:
|
||||
data["description"] = description
|
||||
path = self._path(name)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
logger.info("Saved preset '%s' to %s", name, path)
|
||||
|
||||
def load(self, name: str) -> Optional[dict[str, Any]]:
|
||||
"""
|
||||
Load a preset from disk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The preset name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[dict]
|
||||
The preset data dict, or None if the preset does not exist.
|
||||
"""
|
||||
if not self.is_valid_name(name):
|
||||
return None
|
||||
path = self._path(name)
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to load preset '%s': %s", name, exc)
|
||||
return None
|
||||
|
||||
def delete(self, name: str) -> bool:
|
||||
"""
|
||||
Delete a preset file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The preset name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the preset existed and was deleted, False otherwise.
|
||||
"""
|
||||
if not self.is_valid_name(name):
|
||||
return False
|
||||
path = self._path(name)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
logger.info("Deleted preset '%s'", name)
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_presets(self) -> list[str]:
|
||||
"""
|
||||
List all saved preset names, sorted alphabetically.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Sorted list of preset names (without the .json extension).
|
||||
"""
|
||||
return sorted(p.stem for p in self.presets_dir.glob("*.json"))
|
||||
|
||||
def list_preset_details(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
List all presets with their metadata, sorted alphabetically by name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[dict]
|
||||
Each entry has ``"name"``, ``"owner"`` (may be None), and
|
||||
``"description"`` (may be None).
|
||||
"""
|
||||
result = []
|
||||
for p in sorted(self.presets_dir.glob("*.json"), key=lambda x: x.stem):
|
||||
owner = None
|
||||
description = None
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
owner = data.get("owner")
|
||||
description = data.get("description")
|
||||
except Exception:
|
||||
pass
|
||||
result.append({"name": p.stem, "owner": owner, "description": description})
|
||||
return result
|
||||
|
||||
def exists(self, name: str) -> bool:
|
||||
"""Return True if a preset with this name exists on disk."""
|
||||
if not self.is_valid_name(name):
|
||||
return False
|
||||
return self._path(name).exists()
|
||||
Reference in New Issue
Block a user