Files
ComfyAssets-ComfyUI_PromptM…/utils/hashing.py
T
Vito 89d94cdb2b fix(history): join case-variant raw text to the history of the prompt it counts toward (#460) (#471)
* fix(history): join outer-whitespace raw text to the history of the prompt it saves (#438)

The Library stores text.strip() while a run keeps its exact raw text. The
history window, run detail scope id, newest pair run and prompt hints now
compare the raw text's stored form (one rule, utils/hashing.py, mirrored in
SQLite with the same code points). Hints also keep matching the exact raw
text, so older rows that kept outer whitespace stay hinted and protected.

* fix(history): join case-variant raw text to the history of the prompt it counts toward (#460)

The prompt hash folds case, so a run of "A Lighthouse" counts toward the
saved "a lighthouse" but never joined its history. History now compares the
hash's own key (prompt_key = strip + str.lower()); SQL calls Python for it
through pm_prompt_key/pm_prompt_hash registered on every managed connection,
since SQLite's lower() folds ASCII only. Hints match by text first (#438) and
fall back to the dedup hash only when no text matches, so no existing hint
becomes ambiguous. A history position from before this change still resumes.

* test(history): a real-host case-variant re-queue joins the prompt's history (#460)
2026-10-29 13:25:04 +00:00

155 lines
5.0 KiB
Python

"""Hashing utilities for PromptManager prompt deduplication.
This module provides cryptographic hashing functions to enable efficient
deduplication of prompts and content in the PromptManager system. It uses
SHA256 hashing with text normalization to ensure consistent hash generation
regardless of minor formatting differences.
Key features:
- SHA256-based prompt text hashing with normalization
- Content hashing for complex prompt metadata structures
- Duplicate detection utilities
- Consistent normalization (lowercase, trimmed whitespace)
Typical usage:
from utils.hashing import generate_prompt_hash, is_duplicate_prompt
hash1 = generate_prompt_hash("Beautiful landscape")
hash2 = generate_prompt_hash(" beautiful landscape ")
# hash1 == hash2 (normalization makes them identical)
if is_duplicate_prompt(text1, text2):
print("Duplicate prompts detected")
The hashing is designed for:
- Database deduplication (preventing duplicate prompt storage)
- Quick similarity checking
- Content integrity verification
- Efficient database indexing
"""
import hashlib
def generate_prompt_hash(text: str) -> str:
"""
Generate a SHA256 hash for prompt text to enable deduplication.
Normalizes the input text (strips whitespace, converts to lowercase)
before hashing to ensure consistent results for functionally identical
prompts with minor formatting differences.
Args:
text: The prompt text to hash
Returns:
SHA256 hexadecimal digest of the normalized text
Raises:
TypeError: If the input is not a string
Example:
>>> generate_prompt_hash("Beautiful Landscape")
'c3d5f8a9b2e1...'
>>> generate_prompt_hash(" beautiful landscape ")
'c3d5f8a9b2e1...'
# Both produce the same hash due to normalization
"""
if not isinstance(text, str):
raise TypeError("Text must be a string")
return hashlib.sha256(prompt_key(text).encode("utf-8")).hexdigest()
def stored_prompt_text(text: str) -> str:
"""The text the Library stores for raw node text: outer whitespace removed.
Every prompt writer stores ``text.strip()`` and the hash above strips too,
so this is the form recorded raw run text is compared in (#438). Case and
inner whitespace stay significant.
"""
return text.strip()
# The code points str.strip() removes (every str.isspace() character), for
# SQLite's char(); tests/test_hashing.py pins it to the running Python.
STRIPPED_CODE_POINTS = (
*range(0x09, 0x0E),
*range(0x1C, 0x21),
0x85,
0xA0,
0x1680,
*range(0x2000, 0x200B),
0x2028,
0x2029,
0x202F,
0x205F,
0x3000,
)
_STRIP_CODES = ",".join(map(str, STRIPPED_CODE_POINTS))
def prompt_key(text: str) -> str:
"""The dedup key: the stored text with case folded by ``str.lower()``.
Two texts with one key are one prompt (the hash is sha256 of the key), so
a run belongs to the history of the prompt with its key (#460).
"""
return stored_prompt_text(text).lower()
def _sql_key(value):
return prompt_key(value) if isinstance(value, str) else None
def _sql_hash(value):
return generate_prompt_hash(value) if isinstance(value, str) else None
def register_prompt_functions(conn) -> None:
"""Give ``conn`` ``pm_prompt_key(x)`` and ``pm_prompt_hash(x)`` (#460).
SQLite's lower() folds ASCII only; these call Python, so SQL gets exactly
``prompt_key`` / ``generate_prompt_hash`` (NULL for a non-text value).
Never used in schema objects, so the file stays readable without them.
"""
conn.create_function("pm_prompt_key", 1, _sql_key, deterministic=True)
conn.create_function("pm_prompt_hash", 1, _sql_hash, deterministic=True)
def stored_prompt_text_sql(expression: str) -> str:
"""``stored_prompt_text`` as an SQLite expression over ``expression``.
SQLite's one-argument trim() removes spaces only; trim(X, Y) removes every
character of Y, multi-byte ones included, so it strips what Python does.
"""
return f"trim({expression}, char({_STRIP_CODES}))"
def is_duplicate_prompt(text1: str, text2: str, threshold: float = 0.95) -> bool:
"""
Check if two prompts are likely duplicates using hash comparison.
Compares the normalized hashes of two prompt texts to determine if
they are functionally identical. This is an exact match after normalization,
not a similarity measure.
Args:
text1: First prompt text to compare
text2: Second prompt text to compare
threshold: Similarity threshold (not used - kept for API compatibility)
Returns:
True if the prompts have identical normalized hashes (i.e., are duplicates),
False otherwise
Note:
The threshold parameter is not used in the current implementation as
this performs exact hash matching rather than similarity scoring.
It's maintained for potential future fuzzy matching capabilities.
"""
hash1 = generate_prompt_hash(text1)
hash2 = generate_prompt_hash(text2)
return hash1 == hash2