Files

179 lines
6.4 KiB
Python

import os
import json
import re
class PromptReplayNode:
"""
Prompt Replay (V2)
- Reads ./logs/prompt_history.jsonl
- Builds a human-friendly dropdown of recent entries
- Lets you filter by source/json/lora/system
- Outputs the exact final_prompt
"""
# Cache to map dropdown labels -> entry dict
_cache_options = []
_cache_entries = []
@staticmethod
def _log_path():
base_path = os.path.dirname(__file__)
return os.path.join(base_path, "logs", "prompt_history.jsonl")
@staticmethod
def _normalize(s: str) -> str:
return (s or "").strip()
@staticmethod
def _parse_triggers(lora_triggers):
# stored as string in your logger; accept list too just in case
if isinstance(lora_triggers, list):
return [str(x).strip() for x in lora_triggers if str(x).strip()]
if isinstance(lora_triggers, str):
return [x.strip() for x in lora_triggers.split(",") if x.strip()]
return []
@classmethod
def _load_entries(cls, max_entries: int, source_filter: str, json_filter: str, lora_filter: str, system_filter: str):
path = cls._log_path()
if not os.path.exists(path):
cls._cache_options = ["(no prompt_history.jsonl found)"]
cls._cache_entries = []
return
entries = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
except Exception:
continue
# source filter
if source_filter != "all":
if data.get("source") != source_filter:
continue
# json filter (substring, case-insensitive)
jf = cls._normalize(json_filter).lower()
if jf:
world = str(data.get("json_world", "")).lower()
if jf not in world:
continue
# system filter (substring on enhancer_mode key)
sf = cls._normalize(system_filter).lower()
if sf:
sysmode = str(data.get("system_prompt", "")).lower()
if sf not in sysmode:
continue
# lora filter (substring against triggers)
lf = cls._normalize(lora_filter).lower()
if lf:
triggers = ",".join(cls._parse_triggers(data.get("lora_triggers", ""))).lower()
if lf not in triggers:
continue
entries.append(data)
if not entries:
cls._cache_options = ["(no entries match filters)"]
cls._cache_entries = []
return
# take last N, most recent first
entries = entries[-max_entries:][::-1]
options = []
for idx, e in enumerate(entries):
ts = e.get("timestamp", "unknown-time")
world = e.get("json_world", "unknown-json")
sysmode = e.get("system_prompt", "standard")
src = e.get("source", "generated")
triggers = cls._parse_triggers(e.get("lora_triggers", ""))
triggers_short = ""
if triggers:
# keep it readable
show = triggers[:3]
triggers_short = " | LoRA: " + ", ".join(show) + ("…" if len(triggers) > 3 else "")
# label format: "0 | 2026-01-21T22:47:03 | Modern_Goth_Urban.json | enhancer_mode | generated | LoRA: ..."
label = f"{idx} | {ts} | {world} | {sysmode} | {src}{triggers_short}"
options.append(label)
cls._cache_options = options
cls._cache_entries = entries
@classmethod
def INPUT_TYPES(cls):
# Defaults for initial cache build
# Note: dropdown list updates when ComfyUI reloads nodes (or restart).
cls._load_entries(
max_entries=80,
source_filter="all",
json_filter="",
lora_filter="",
system_filter=""
)
return {
"required": {
"pick": (cls._cache_options if cls._cache_options else ["(empty)"],),
"max_entries": ("INT", {"default": 80, "min": 1, "max": 500, "step": 1}),
"source_filter": (["all", "generated", "history_lock"],),
"json_filter": ("STRING", {"default": "", "multiline": False}),
"lora_filter": ("STRING", {"default": "", "multiline": False}),
"system_filter": ("STRING", {"default": "", "multiline": False}),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("prompt", "meta")
FUNCTION = "replay"
CATEGORY = "Prompt Tools"
def replay(self, pick, max_entries, source_filter, json_filter, lora_filter, system_filter):
# Rebuild cache using current filters
self.__class__._load_entries(
max_entries=int(max_entries),
source_filter=str(source_filter),
json_filter=str(json_filter),
lora_filter=str(lora_filter),
system_filter=str(system_filter),
)
if not self.__class__._cache_entries:
print("[PromptReplay] No entries available.")
return ("", "no_entries")
# "idx | ..." -> idx
m = re.match(r"^\s*(\d+)\s*\|", str(pick))
if not m:
print("[PromptReplay] Could not parse selection index.")
return ("", "bad_selection")
idx = int(m.group(1))
if idx < 0 or idx >= len(self.__class__._cache_entries):
print("[PromptReplay] Selection index out of range.")
return ("", "out_of_range")
e = self.__class__._cache_entries[idx]
prompt = e.get("final_prompt", "") or ""
meta = {
"timestamp": e.get("timestamp"),
"json_world": e.get("json_world"),
"system_prompt": e.get("system_prompt"),
"gender": e.get("gender"),
"custom_intro": e.get("custom_intro"),
"lora_triggers": e.get("lora_triggers"),
"source": e.get("source"),
"node_version": e.get("node_version"),
}
print(f"[PromptReplay] Replayed: {meta.get('timestamp')} | {meta.get('json_world')} | {meta.get('source')}")
return (prompt, json.dumps(meta, ensure_ascii=False))