Files
ComfyAssets-ComfyUI_PromptM…/utils/card_seed.py
T
Vito Sansevero b5c2925468 fix(images): a deeply nested record no longer fails the workspace listing
The Python parser recurses over nested arrays where JavaScript String()
does not, so a crafted 2 KB PNG turned every workspace page holding it
(and its inspect route) into a 500. Card facts now treat RecursionError
like any record the parser cannot use: no seed and no confirmed title.

Also: Library previews skip the card parse they never show; the parse
runs after the file handle closes; the exact-seed read uses the one JSON
reader (parse_js_json(..., exact_integers=True)) and the public
number_string; record size is decided from the text length unless only
the UTF-8 byte count can tell.
2026-10-19 23:05:34 -07:00

129 lines
4.7 KiB
Python

"""Workspace card facts from the metadata records a header open already loaded.
The card shows what the inspector shows, so it reads the same shared parser
(``extract_generation_params``, the Python twin of the browser parser the
inspector fills from since #251) and nothing else: no separate seed heuristic,
no graph resolution and no extra file reads. One parse feeds both the seed and
the positive that confirms a first-line card title.
"""
import math
import re
from collections.abc import Mapping
from .generation_params import (
PLACEHOLDERS,
GenerationParamsError,
extract_from_records,
extract_generation_params,
number_string,
parse_js_json,
)
__all__ = ["MAX_RECORD_BYTES", "card_seed", "header_params", "parsed_positive"]
# Each record is parsed only up to this many UTF-8 bytes; past it, nothing is read.
MAX_RECORD_BYTES = 2 * 1024 * 1024
_RECORD_KEYS = ("prompt", "workflow")
_INTEGER = re.compile(r"-?[0-9]+\Z")
_MAX_SAFE_INTEGER = 2**53 - 1
def _utf8_size_over(text: str, limit: int) -> bool:
"""True when ``text`` is over ``limit`` UTF-8 bytes, encoding only if unclear.
A code point takes one to four bytes, so most records are decided from
their length alone without copying them.
"""
if len(text) > limit:
return True
if len(text) * 4 <= limit:
return False
return len(text.encode("utf-8", "surrogatepass")) > limit
def header_params(info) -> dict | None:
"""The shared parser's result for the ``prompt``/``workflow`` text in ``info``.
``info`` is a Pillow ``Image.info`` (or the inspection read's metadata).
None when neither record is present, a record is not text or is over
``MAX_RECORD_BYTES``, or the browser parser would throw on it.
"""
if not isinstance(info, Mapping):
return None
records = {key: info.get(key) for key in _RECORD_KEYS}
if all(text is None for text in records.values()):
return None
for text in records.values():
if text is None:
continue
if not isinstance(text, str):
return None
if _utf8_size_over(text, MAX_RECORD_BYTES):
return None
try:
params = extract_from_records(records["prompt"], records["workflow"])
except (GenerationParamsError, RecursionError):
# RecursionError: the Python parser recurses on deeply nested arrays
# where JavaScript does not throw; a crafted file gives no card facts.
return None
return _with_exact_seed(params, records)
def _with_exact_seed(params: dict, records: dict) -> dict:
"""``params`` with a rounded seed replaced by the integer the file records.
``JSON.parse`` rounds integers beyond 2**53, but the inspector reads the
record's own digits (its projection carries big numbers as text). ComfyUI's
randomized seeds are mostly that large, so the same parser is run once more
on an exact parse of the same records, and its seed is kept only when it is
the one that rounded to the parsed value. Anything else leaves the seed
as parsed.
"""
seed = params.get("seed")
if not isinstance(seed, float) or not math.isfinite(seed):
return params
if abs(seed) <= _MAX_SAFE_INTEGER or not seed.is_integer():
return params
try:
exact = extract_generation_params(
{
key: parse_js_json(text, exact_integers=True)
for key, text in records.items()
if text is not None
}
)["seed"]
except (GenerationParamsError, RecursionError):
return params
if isinstance(exact, bool) or not isinstance(exact, int) or float(exact) != seed:
return params
return {**params, "seed": exact}
def card_seed(params: dict | None) -> int | None:
"""The parsed seed as the integer the browser prints, else None.
A placeholder, text, boolean or non-integral number gives None. A double
that ``header_params`` could not make exact gives the integer whose decimal
form equals the browser's ``String(seed)``.
"""
if not isinstance(params, Mapping):
return None
value = params.get("seed")
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
if isinstance(value, float) and not math.isfinite(value):
return None
text = number_string(value)
return int(text) if _INTEGER.match(text) else None
def parsed_positive(params: dict | None) -> str | None:
"""The parsed positive prompt text, or None for the placeholder or blank."""
if not isinstance(params, Mapping):
return None
text = params.get("positivePrompt")
if not isinstance(text, str) or text == PLACEHOLDERS["positivePrompt"]:
return None
return text if text.strip() else None